存储与上下文构建
CLI 把手机端推来的三类数据——通知、录音(含转写 / 摘要)、图片——、浏览器扩展抓取的已同步网页,以及桌面「YoooClaw Capture」应用产生的会议录音,全部落成本地文件,再用一套命令把它们整理成 Agent 能直接消费的上下文。
这一章分两半:前半讲三类数据各自怎么落盘,后半讲 CLI 怎么把这些落盘数据加工成上下文。落盘目录在 profile 里的位置见架构与实现逻辑 · profile;多账号下的 clientLabel 打标见多 api-key 设计。
设计理念:落盘即事实,纯读即上下文
三类数据共用同一套思路:
- 一切先落本地文件——通知是按天 JSON,录音 / 图片各有
index.json+ 实体文件,状态走状态机。文件就是事实来源,daemon 在不在线都不影响「读」。 - 读与写控制分离——查询类命令(🟢)纯读磁盘,冷启动就能跑,不需要 daemon。
- 同构三件套——每类数据都有:元数据索引、同步 / 转写状态、
clientLabel来源标记、可解析的本地路径。Agent 拿到的要么是完整结果,要么是清晰的「还没好」错误,不会拿到半截数据。
profiles/<name>/
├── notifications/ # 按天 JSON + 去重索引
│ ├── 2026-05-25.json
│ ├── .ids/ # 按 id 去重
│ └── .keys/ # 按内容指纹去重
├── recordings/ # smart_hardware 来源(手机端录音硬件)
│ ├── audio/ # 原始音频(.ogg)
│ ├── transcript-data/ # 转写 JSON(主存储)
│ ├── transcripts/ # 转写正文 .md(派生,文件名 <YYYYMMDDHH>_<标题>_<id>.md)
│ ├── summaries/ # 摘要 .md(派生,同上文件名约定)
│ ├── index.json # 元数据 + 状态索引
│ ├── asr-config.json # 本地 ASR 配置
│ └── state/events.jsonl # 状态事件流
├── voice/ # capture_app 来源(桌面 YoooClaw Capture)
│ ├── audio-jsonl/ # 使用记录,按天分片 YYYY-MM-DD.jsonl
│ └── recordings-jsonl/ # 会议录音元数据,按天分片
├── images/
│ ├── index.json # 元数据 + 同步状态
│ └── files/ # 下载后的图片实体
└── web-pages/
├── index.json # 元数据(URL 哈希去重)
└── files/ # 抓取正文 .md(可选 .html 归档)recording * 命令查询时会跨 recordings/(smart_hardware)与 voice/recordings-jsonl/(capture_app)两个目录合并结果,用 --source 过滤到其中一个。voice/audio-jsonl/ 是另一回事——那是录音应用本身的使用记录(哪个 app 什么时候被录了多久),只能用 voice * 命令查,和 recording * 不是一套数据。
通知:按天 JSON + 双重去重
通知按天落到 notifications/YYYY-MM-DD.json(JSON 数组,追加写)。每条 StoredNotification 结构:
{
"clientLabel": "phone-a",
"appName": "com.tencent.xin",
"appDisplayName": "微信",
"title": "王总",
"content": "下午三点的会议改到四点",
"timestamp": "2026-05-25T14:02:11+08:00",
"senderName": "王总",
"conversationType": "private"
}几个要点:
appDisplayName由后端 app-name-map 补全——落库时把包名com.tencent.xin解析成「微信」,让上下文对人和模型都更可读。- 飞书等 IM 做结构化拆分——把 title / subtitle 归一成
senderName/conversationType/conversationName,群聊私聊可区分。 - 双重去重:
.ids/按通知id去重,.keys/按「clientLabel+app+title+content+timestamp」的 SHA-256 内容指纹去重。同一条消息被手机端重推、或多 key 重复投递都不会落两份。去重以clientLabel为维度——不同账号收到的同样内容互不遮蔽。 - 保留期:
retentionDays到期自动清理数据文件与两份索引。
录音:音频 → 转写 JSON → 摘要
录音是链路最长的一类,落盘分四级目录,由 index.json 串起来。录音结果由 App / 云端经 recordings.result.write 写入:
recordings.result.write(转写/总结,可选 ossUrl)
↓ index.json 落条目(新建则 status=synced)
transcript-data/<id>.json ← 转写主存储(结构化:title/summary/text/segments)
transcripts/<id>_<标题>.md ← 正文(派生)
summaries/<id>.md ← 摘要(派生)
↓ 可选:带 ossUrl 时后台下载音频 → audio/<id>.ogg
↓ status=transcribed需要在 daemon 本机重新转写时,recordings.retranscribe 用本地 / 请求级 ASR 配置触发:status=transcribing → ASR(api/model-proxy,默认可回退 account ock- key)→ transcribed。
设计上的关键点:
transcript-data/的 JSON 是主存储,transcripts/正文和summaries/摘要都是从它派生的。readTranscript/readSummary优先读 JSON,旧数据才回退读 Markdown——保证新老数据一个口径。title/summary内置:转写完成会顺带产出 ≤ 标题级别的title和一段摘要,写进 index 与summaries/。Agent 想「快速了解这条录音讲了啥」时,读摘要就够,不必拉全文。- 状态机严格校验:
transfer_status在synced → transcribing → transcribed(及transcribe_failed)之间按状态机迁移,非法跃迁直接拒绝。result.write写入结果属于带外(out-of-band)落盘,直接置transcribed。daemon 重启时残留的transcribing会被判定为中断并落transcribe_failed。 - 事件流:每次状态变化追加到
state/events.jsonl,yc recording events --id <id> --watch可像tail -f一样跟随;同一事件也经 Relay 以recording.status推回手机端。 - 录音时间取结果里的实际录制时间,不是转写生成时间;不同来源上报的时区/格式差异会先解析成真实时间点再排序,避免「最新」选错。
duration_sec是整数秒,duration_display是配套的人读格式(16s/1m 16s/1h 1m 1s);音频下载完成后file_size_display用 Android 兼容的短格式给出大小,旧的file_size_bytes字段已下线。 - 按 clientLabel 隔离读写:经
/gateway/recordings.*写入的录音会打上发起写入的 api-key 对应的clientLabel;读侧同样按这个标签隔离——通过 Relay 隧道或某个 api-key 接入的客户端只能看到/操作自己名下的录音,本机 loopback / gateway token 请求不受限,没打标或历史遗留的条目对所有客户端可见。
ASR 配置写在 recordings/asr-config.json(yc recording setup-asr 生成),mode=api 缺 key 时回退到 account 级 ock- key。当前 Go beta 只支持 api / model-proxy;local mode 保留在 schema 中用于兼容旧请求,但会被校验拒绝。
voice/recordings-jsonl/ 下的 capture_app 来源(桌面「YoooClaw Capture」录的会议录音)走同一套 recording * 查询命令(--source capture_app),但转写结构是分句的 transcripts[].sentences[](带 begin_time/end_time/speaker_id),没有 recordings/state/events.jsonl 那样的状态事件流。
图片:与录音同构的下载通道
图片走 POST /images,落 images/index.json 后后台流式下载 OSS 原图到 images/files/<id>.<ext>:
POST /images → index.json 落条目,status=syncing
↓ 后台 fetch(ossUrl) 流式写 files/<id>.<ext>
status=synced(失败 → sync_failed,记 lastError)image path <id> 在文件还没下载完时返回 YOOOCLAW_IMAGE_NOT_READY 而不是半截文件——这样 Agent 拿本地路径喂多模态模型时,要么拿到完整图片、要么拿到明确的「还没好」。index.json 同样带 clientLabel、source_app、caption 等元数据。
网页:浏览器扩展抓取
已同步网页是唯一不经手机端、而是由浏览器扩展经 Relay 投递的数据源:扩展把当前页面正文转成 Markdown,POST /web-pages 落到 daemon,daemon 写 web-pages/index.json 并把正文存成文件,纯本地、不联网:
浏览器扩展 → POST /web-pages(Markdown 正文 + 元信息)
↓ 按 URL 哈希去重
已收录:captureCount++、firstCapturedAt 不变、capturedAt 刷新为本次
未收录:新建条目,写 files/<hash>.md(可选归档原始 HTML)index.json 每条记录 urlHash、title、siteName、canonicalUrl、capturedAt、firstCapturedAt、captureCount、bytes、relativePath、clientLabel。daemon 同时给浏览器扩展暴露两个配套只读端点:GET /web-pages/status?h=… 供 popup 打开时核对某个 URL 是否已收录,GET /web-pages/index 供扩展登录后一次性回填本地收藏状态。
yoooclaw synced-web-page search 只搜索 Markdown 正文与标题/URL 等元数据,不搜原始 HTML 归档,也不会访问互联网——它返回的是「本地已收藏的历史副本」,不代表页面当前内容;需要时效性时应另外用联网工具核实。
CLI 如何帮你构建上下文
落盘只是原料,CLI 的命令层把它们加工成 Agent 好用的上下文。核心手段有六个:
1. 高频场景预设(shortcuts)
+ 前缀把最常用的查询固化成一条命令,Agent 不必拼一堆 flag:
yc notification +today # 今日通知摘要
yc notification +recent # 最近 1 小时
yc recording +latest # 最新一条录音(含 title/summary/transcript)
yc image +latest # 最新一张图片详情2. 为「总结」而生的聚合命令
notification summary 不是把通知一条条吐出来,而是聚合统计 + 最近样例一起返回,正好是 Agent 做总结需要的形状;notification stats 支持按 date|app|sender|hour|client 多维聚合:
yc notification summary --from 2026-05-25T00:00:00+08:00 --sample 30 --top 10
yc notification stats --dim app --from 2026-05-203. 喂给记忆系统的增量游标
sync 服务(scan / fetch / commit)给「把通知持续灌进记忆系统」设计了一套游标:scan 找出各日期待处理量,fetch 取某日明细并返回 endIndex,commit 用该 endIndex 标记本批已处理。重复跑不会重复灌,断点可续。
4. 录音读「摘要优先」
recording status <id> / +latest 返回 title + summary + transcript,Agent 先看摘要决定要不要展开全文,省 token:
yc recording +latest # 最新录音的标题 + 摘要 + 正文
yc recording list --status transcribed --client phone-a5. 图片给「本地绝对路径」喂多模态
image +latest / image path <id> 直接吐本地文件绝对路径,Agent 拿去喂多模态模型;未下载完则返回 YOOOCLAW_IMAGE_NOT_READY,不会喂半截文件。
6. 面向 Agent 的输出契约
所有命令走统一 --format json|pretty|table|ndjson:
ndjson每条结果一行 JSON,适合 Agent 流式逐行消费与背压处理;- 成功
{ "ok": true, ... }/ 失败{ "ok": false, "error": { "code": "YOOOCLAW_*", ... } }共用 stdout;本地 CLI 错误会叠加非零退出码,Raw HTTP 结果还应检查ok/ HTTP status; --client <label>把多账号上下文切开,--client all表示不过滤。
把这几件事串起来,一次「帮我整理今天手机上发生了什么」的上下文构建就是:
yc notification summary +today → 今日通知聚合 + 样例
yc recording list --status transcribed
└─ yc recording status <id> → 挑出的录音读摘要
yc image +latest → 最新图片本地路径喂多模态
↓ 全部 --format ndjson / json
Agent 拼装上下文 → 回答下一步
- 命令参考 ——
notification/recording/voice/image/sync全部子命令与 flag。 - Agent Skill —— Agent 怎么自动驱动这些命令。
- 架构与实现逻辑 —— daemon、ingest、录音状态机的全貌。
- 多 api-key 设计 ——
clientLabel怎么来的、多账号怎么切。