OpenChronicle 捕捉层深度解析:AX Tree + 截图如何变成结构化上下文信号
【免费下载链接】OpenChronicle项目地址: https://gitcode.com/gh_mirrors/op/OpenChronicle
OpenChronicle 是一款开源、本地优先的 AI Agent 记忆系统,它的捕捉层负责把屏幕上的 AX Tree(辅助功能树)和截图转化为结构化上下文信号,让 AI 助手真正"看见"你正在做什么。本文用尽量少的代码,带你完整拆解这条捕捉管线:信号从哪来、如何节流降噪、又怎样落成可检索的 JSON 文件。
什么是捕捉层:唯一接触外部世界的组件
在 OpenChronicle 的架构里,捕捉层(Capture Layer)是整个系统中唯一直接接触 macOS 的层——上层的时间线聚合、会话切分、记忆写入,永远不直接调用系统 API。它每"观察"一次,就往~/.openchronicle/capture-buffer/写一个 JSON 文件。
这种"单一入口"设计带来的好处是:下游所有 LLM 阶段看到的都是同一套规范化数据结构,而不是五花八门的原始系统调用结果。
核心思路:先捕获,再压缩,后分类——捕捉层只负责忠实记录"此刻屏幕上发生了什么"。
完整管线可参考 docs/architecture.md,捕捉层由 S0 事件调度器与 S1 解析器两个子阶段组成。
两个信号源:事件驱动 + 心跳定时器
捕捉由两条路径触发,二者最终都汇入同一个capture_once流程(scheduler.py):
| 信号源 | 工作方式 | 适用场景 |
|---|---|---|
| mac-ax-watcher(主) | 一个随包发行的 Swift 二进制,订阅所有 App 的 AX 通知(窗口聚焦、输入值变化、标题变化、App 激活),每发生一个事件就在 stdout 输出一行 JSON | 你正在打字、切窗口的"活跃期" |
| 心跳定时器(兜底) | 默认每 10 分钟(heartbeat_minutes)强制拍一次快照 | 长时间静默期,保证"有迹可循" |
事件流的读取与容错由 watcher.py 负责:它逐行解析 JSONL,watcher 崩溃后按指数退避自动重连;若系统返回"辅助功能权限未授予"(退出码 2),则停止重连并记录错误,避免空转。
AX Tree 作为主信号:为什么它比截图 OCR 更香
OpenChronicle 采取AX-first策略——把 macOS 辅助功能(Accessibility)树当作第一信号源,而非截图 + OCR。理由很务实:
- 💰成本更低:结构化文本的处理成本远低于视觉 OCR 管线;
- 🎯意图更准:当前聚焦元素、正在编辑的文本、URL、交互状态,AX Tree 天然就带;
- 🧹记忆更干净:文本易于去重、归一化、索引,适合长期留存;
- 🖼️截图兜底:AX 覆盖不到的视觉信息,留给截图作为补充。
AX 树的获取是一次性调用随包的mac-ax-helperSwift 二进制(ax_capture.py),按配置的ax_depth深度裁剪后返回 JSON,敏感字段(如密码输入框)在 helper 层就被替换为[REDACTED],Python 侧根本看不到明文。
⚠️一个关键的坑——AX 深度:原生 Cocoa 应用的树通常只有 5~15 层,而 Electron 应用(VS Code、Slack、Notion 等)的用户内容藏在 20~60 层深的 DOM 之下。因此默认ax_depth = 100(config.py)。硬件有限的机器可降到 30,但不要低于 20,否则你会"静默丢失"聊天正文这类深层内容。
S1 解析器:把 AX Tree 蒸馏成三个结构化字段
原始 AX 树体积大、噪声多。S1 解析器(s1_parser.py)在每次捕获时内联运行,把树"蒸馏"成下游 LLM 真正消费的三个字段:
| 字段 | 含义 | 限制 |
|---|---|---|
focused_element | 光标所在元素:角色、标题、正在输入的值、是否可编辑 | 值截断 2000 字符 |
visible_text | 屏幕可见内容的 Markdown 渲染("你此刻读到的东西") | 上限约 10,000 字符 |
url | 仅浏览器(Chrome/Safari/Firefox/Edge 等)场景下从地址栏正则提取 | 否则为null |
聚焦元素的识别逻辑见 _extract_focused_element:它在聚焦窗口里找可编辑控件(AXTextField/AXTextArea/AXComboBox)或静态文本,这就是"用户光标上下文"——他正在往哪里打字、选中了侧边栏哪一行。
截图是次级信号:被拍下来,但不喂给 LLM
截图由 mss + Pillow 完成(screenshot.py):抓主显示器 → 宽度超过 1920 时按比例缩放 → 编码为 quality 80 的 JPEG base64,内嵌进捕获 JSON。
它当前的角色很克制:
- 不进时间线聚合器、会话 reducer、分类器的任何 prompt——因为结构化文本已经够用且便宜得多;
- 留给未来的视觉模型管线和调试排查;
- 可通过
include_screenshot = false完全关闭(config.py)。
四大事件节流机制:别让每个按键都触发一次捕获
macOS 的 AX 事件流是"消防水带"级别的。事件调度器(event_dispatcher.py)先按事件类型分类,再用四个时间旋钮层层过滤:
| 旋钮 | 默认值 | 作用 |
|---|---|---|
debounce_seconds | 3.0s | AXValueChanged事件在窗口内合并,只有最后一次触发捕获——避免"每敲一个键拍一次快照" |
dedup_interval_seconds | 1.0s | 相同(事件类型, App)组合在窗口内直接丢弃 |
min_capture_gap_seconds | 2.0s | 两次捕获之间的硬性间隔下限 |
same_window_dedup_seconds | 5.0s | 同一(bundle_id, 窗口标题)的非聚焦变化事件合并;聚焦切换永远放行 |
事件分类本身也很讲究:窗口聚焦切换、App 激活、鼠标点击、键盘输入立即捕获;值变化防抖捕获;标题变化则直接跳过(太吵,已被窗口/应用事件覆盖)。
内容指纹去重:屏幕没变,就一个字都不写
时间旋钮拦不住一种情况:锁屏整晚、暂停的视频、闲置的 IDE——画面纹丝不动,AX 事件却源源不断。
为此调度器引入了内容指纹(scheduler.py):对bundle_id + 窗口标题 + 聚焦元素值 + visible_text + url做 SHA-256 哈希,与上一次写入的捕获比对,相同则不落盘、也不触发会话管理器的空闲计时(时间戳、触发源、截图都刻意排除在指纹之外)。
这一步同时保住了两件大事:缓冲区不被"幽灵捕获"灌爆,且当前会话不会因为无意义的重复事件而永远无法进入空闲切分。
一份捕获文件里到底有什么
每次捕获最终落成如2026-04-21T17-07-32p08-00.json这样的文件(ISO-8601 时间戳做安全文件名化),骨架大致如下:
{ "timestamp": "2026-04-21T17:07:32+08:00", "trigger": { "event_type": "window_focus_changed", "app": "Claude" }, "window_meta": { "app_name": "Claude", "bundle_id": "com.anthropic.claudefordesktop" }, "focused_element": { "role": "AXTextArea", "value": "I have an interview at 18:00" }, "visible_text": "### New conversation — Claude\n...", "url": null, "ax_tree": { "...": "裁剪后的完整树" }, "screenshot": { "image_base64": "iVBORw0KG...", "width": 1920, "height": 1200 } }写入的同时还会做一次FTS5 索引写入:app_name、window_title、focused_value、visible_text、url这些可搜索文本同步进captures_fts虚拟表(fts.py),让 MCP 客户端能直接全文检索原始屏幕内容,而不必扫描磁盘上的 JSON。
缓冲区卫生:分层保留策略
截图 base64 约占单个捕获文件体积的77%,但下游暂不消费它。于是 cleanup_buffer 采用三段式清理,且只动"已被时间线吸收"的文件,绝不碰尾部未处理数据:
- 24 小时后:剥掉截图字段,文件缩到原来约 20% 的体积,文本信号全保留;
- 7 天后(
buffer_retention_hours = 168):整个 JSON 删除; - 总量超 2GB 时:按最老优先逐份驱逐。
稳态下缓冲区只占几百 MB——这就是"截图先留着、过期先瘦身、结构信号长保留"的分层思想。
小结
OpenChronicle 捕捉层的设计哲学可以浓缩成三句话:
- AX Tree 为主、截图为辅——用最低成本拿到最准的用户意图信号;
- 多层节流——事件分类 → 时间旋钮 → 内容指纹,三层漏斗把"消防水带"收敛成每天几百份高质量捕获;
- 忠实落盘、异步瘦身——结构化信号长期可检索,大体积截图按生命周期优雅降级。
想深入了解每个环节的默认值与调参方式,推荐精读 docs/capture.md;上手验证只需openchronicle capture-once一条命令,立刻就能看到一份完整的结构化上下文信号。
【免费下载链接】OpenChronicle项目地址: https://gitcode.com/gh_mirrors/op/OpenChronicle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考