| name | zettel-builder |
| description | 自底向上生长的 Zettelkasten 卡片笔记系统。从笔记/已发表文章/收藏素材持续消化为原子卡,
用户价值观重述,自动链接,周期性巡检文章就绪簇,缺口处可调 deep-research 补卡。
与"自顶向下选题"或"长文写作 Skill"不重叠:那些是"等到要写时再生产",本 skill 是"素材进来就长卡"。
触发词:
- "/zettel"、"/zettel-builder"
- "卡片笔记"、"zettelkasten"、"原子卡"、"二级大脑"
- "巡检卡片"、"长出来一篇"、"卡片就绪了吗"
- "把这段切卡"、"做成卡片"
四种模式(由用户调用语义路由):
- ingest:把指定素材(文本/路径/URL/本会话片段)切原子卡 → 价值观重述 → 候选链接
- scan:消化 _queue/ 里 mechanical scan 积累的 llm-wiki 新增素材
- inspect:巡检 cards/,找文章就绪簇 / 孤儿卡 / 缺口
- write:把指定簇移交给你自己的写作 Skill 出稿;过程中检测到证据缺口可调 deep-research 补卡
不做的事:
- 不自动发文(成熟簇只发 Telegram 建议,等用户点头)
- 不替代"自顶向下选题"型 Skill(本 skill 是自底向上长卡)
- 不重写写作风格规则(在 `references/voice-snapshot.md` 维护你的价值观/文风快照,长文出稿交给你自己的写作 Skill)
|
zettel-builder
让卡片自己长出来:素材进 → 切卡 → 连边 → 巡检 → 拼文。用户在每个分叉点保留决策权,但不必逐张手动建卡。
核心理念(凡修改先读一遍)
- 原子化是为了可重组,不是为了字数小。判断一张卡是否合格的硬标准:脱离上下文,半年后的你能不能独立读懂? 能,就是合格,几百字也行。不能,再短也是垃圾。
- "用自己的话重述"不是同义词替换。必须产生新判断:这个观点和我已知的什么冲突?边界条件是什么?我相不相信?没有这一步,重述只是更慢的复制。
- 链接的价值在于写出为什么连。盲连等于不连。每条 link 必须带
reason 字段(一行话)。
- 文章不会"自己长出来"——文章是从已有写作方向上的稠密簇中浮现的。本 skill 帮你更高效地组织既定方向的材料,不替你决定写什么。这是诚实的边界。
- 触发分两层:mechanical scan 由 systemd 离线跑(纯 Python,不切卡只列待办);agent scan 由用户或
/schedule 触发(真正切卡 + 价值观重述 + 连边)。
参见 references/philosophy.md(为什么这样设计 + 与 topic-inspiration 的区别)。
路径默认
| 参数 | 默认 | 说明 |
|---|
zettel_root | ~/zettel/ | 卡片仓根目录,独立 git 仓 |
wiki_root | ~/wiki/published-articles/ | 上游素材来源 |
raw_articles | ${wiki_root}/raw/articles/ | 已发表文章源(241 篇+) |
raw_getnote | ${wiki_root}/raw/getnote/ | GetNote 笔记同步(1974+) |
published_source | ~/Dropbox/cn_articles_published/ | 已发表文章上游(去重对照) |
卡片格式硬规则
完整规范见 references/card-format.md。一行话总结:
---
id: YYYYMMDD-HHMM-<kebab-标题>
status: fleeting | literature | permanent
created: <ISO8601>
source: {type: ..., ref: ...}
tags: [...]
entities: [...]
links: [{id: ..., reason: ...}]
voice_passed: true|false
---
正文(150-800 字,可超,以脱离上下文可读为准)。
永远不要:
- 同时往一张卡塞多个独立判断(切两张)
- 标题写成名词("原子化原则")而不是判断("原子化的目的是可重组")
- 链接只列 id 不写 reason
- 把 raw 原文复制进卡(literature note 只引路径 + 短摘录)
四种模式
Mode 1: ingest — 单次喂料
用法:/zettel ingest <来源>。来源可以是:
- 一段贴在对话里的文本
- 一个文件路径(
/zettel ingest /path/to/note.md)
- 一个 URL(走 markdown-proxy 先抓正文)
- 本次对话上方的若干消息("把上面那段对话切卡")
步骤:
-
来源归一:把素材转为 Markdown 正文 + 来源元数据(type/ref/timestamp)。
-
原子切分(v1.13.0 加强:长文必须多卡):
- 识别所有独立判断,每个一张卡。判断标志:"X 不是 Y,而是 Z"、"X 的真正原因是…"、"X 和 Y 的边界是…"、"X 表面 ... 实际 ..."
- wiki-article 长文必须通读全文找全 candidates,典型字数对应产卡数:短文 2-3 张 / 中长 3-6 张 / 长文 6-10 张 / 巨长 10-15 张
- 若 > 1500 字的 article 只产 1 张卡,必须在
processed/<date>.jsonl 写明理由(否则视为信息压缩失败)
- 详细判读流程见
references/voice-alignment.md Step B.2
- 列举不算判断(三条原则、四种类型),除非每条独立成段并含论证
-
价值观重述:对每张卡,调 references/voice-alignment.md 的协议——读 references/voice-snapshot.md(你自己的价值观/文风快照)的核心条款,重写卡的正文。关键:不是改文风,是产生新判断。如果原文是引述他人,重述时必须加上"我同意/不同意/部分同意,因为…"。
-
链接候选:跑 scripts/link_candidates.py --card-id <new-id>,得到 top-10 候选卡。对每个候选,agent 判定是否真连 + 写一行 reason。无连不强求。
-
去重 + 链接(v1.6.0 agent-judged):嵌入只做召回,最终判断必须 agent 读全文。
- 召回阶段(嵌入):算候选卡嵌入,跟
_index/embeddings.npy cosine,取:
- 近邻 top-20(sim 倒序)
- 远距 top-5(从 sim<0.5 的卡里随机抽,关键 — 防语义局部扎堆)
- 判断阶段(必须 Read 候选卡全文):
- 对近邻候选:Oven 读全文,判定"这是否真的同一判断"。不基于 sim 自动跳过。
- 真同一判断 → dedup-skip,记
{action:"dedup-skip", existing:<id>, agent_reason:"..."}
- 表面相似但实质不同 → 写新卡,但必须自动 link 到那张近似卡,relation + reason 说清差异
- 对远距候选:Oven 读全文,判定"有没有非显然的跨域关联"。命中即建立 link,relation 通常
extend / counter,reason 说明跨域桥梁。
- 绝不直接用 sim ≥ 0.85 自动跳过 — 必须 agent 看完判定
- 绝不只从近邻候选选 link — 至少考察 5 张远距,防扎堆
5.5. filename + id 生成(v1.9.0,关键):写盘前确定 filename 和 frontmatter:
id(frontmatter)= 13 字符 YYYYMMDD-HHMM 纯时间戳。不含标题文字
aliases(frontmatter)= 完整 H1 标题作为第一条(Obsidian 双链友好,改标题不破链)
filename = <short-id>-<主语-主结论>.md,slug ≤ 20 中文字符,首词主语,英文小写连字符
- 完整规则见
Mode 2: scan — 消化 mechanical queue
用法:/zettel scan(或由 OpenClaw cron / event-driven trigger)。
前置:scan_mechanical.py 由 systemd 每小时跑,把 raw/ 新增文件写进 _queue/*.json。Agent scan 消化这些 queue 项。
步骤:
- 状态检查:读
_queue/ 当前积压;读 _state/last_scan.json;读 _state/mode.json 判定当前模式(bootstrap 或 steady)。
- 批量限额(v1.3.0 mode-aware):
bootstrap 模式:N=20(库存高速消化期,挑战长 session token budget)
steady 模式:N=5(日常消化,event-driven 触发)
- 读
_state/mode.json 中 thresholds.bootstrap_batch_size / steady_batch_size,出错时 fallback 默认值。
- 对每个 queue 项:
- 读 source 原文(literature note 只引路径,不复制原文)
- 走 ingest 的 Step 2-5(切卡 + 重述 + 链接 + 写盘)
- 处理完把 queue 项移到
_state/processed/<date>.jsonl
- 顺手 inspect:跑一次 Mode 3 inspect 的轻量版,若发现文章就绪簇,发 Telegram 提醒。
- 更新
_state/processed/<date>.jsonl(只动 agent scan 自己的进度)。绝不修改 _state/last_scan.json — 那是 mechanical scan 的 raw 游标,agent scan 写它会造成新增 raw 文件被跳过(详见 references/scan-mechanical.md 游标所有权章节)。
- git push 硬条款(v1.2.0):
cd ~/zettel && git add -A && git commit -m "feat(scan): <N 张新卡 + Y 项处理>" && git pull --rebase origin main && git push origin main。跨机器同步零延迟。push 失败必须显式报错。
Mode 3: inspect — 巡检卡片池
用法:/zettel inspect(或 scan 末尾自动跑轻量版)。
目的:找出三类信号——文章就绪簇 / 孤儿卡 / 缺口。不自动出文,只给建议。
步骤:
- 跑
scripts/build_index.py:重建 tag/entity/link 索引。
- 跑
scripts/embed_lite.py --include-published:同时刷新 cards/ 和 published-articles 的嵌入,供 overlap 检测用。
- 跑
scripts/cluster_inspect.py:输出 JSON 报告,每个簇含:
- 成员 id 列表 + 密度 + voice_ratio
- gaps:counter_gap / case_gap / evidence_gap / time_gap(基于簇内 link relation 枚举判定)
- published_overlap(v1.3.0 新增):跟已发表文章的语义重叠 — top-3 候选 +
overlap_class(covered ≥0.75 / adjacent 0.6-0.75 / clean <0.6)
- kind:
core(densest 子图)或 whole(整连通分量)
- LLM 命题:对每个新就绪簇,把
suggested_title 占位符替换为陈述性判断句(不要名词短语)。
- 去重:对照
_state/cluster_history.json,过滤已建议过的簇(除非密度显著上升)。
- 回报(v1.3.0 协议):Telegram 摘要 + 每簇含
overlap_class 标记:
covered:⚠️ 红色 — "主题已被 <已发文章标题> 覆盖,建议跳过或明确新角度"
adjacent:橙色 — "主题相邻 <已发文章标题>,需要说明差异"
clean:绿色 — "干净新主题,可推荐 /zettel write"
簇就绪判定细则见 references/cluster-detection.md。published-overlap 算法和阈值理由见 references/published-overlap.md。
Mode 4: write — 把簇拼成文章
用法:/zettel write <cluster-id> 或 /zettel write 后选最就绪簇。
步骤:
- 读簇:从
_state/pending_articles.json 取 cluster-id,加载成员卡。
- 缺口检查:若簇含
gap 标记,询问用户是否先调 deep-research 补卡(调 /deep-research 走 Skill 路由,主题 = 缺口描述,返回后回流为新卡 + 重跑簇就绪判定)。
- 大纲合成:按卡片的链接关系排序成大纲(不是按时间)。
- 移交写作 Skill:调你自己的写作 Skill(例如
/your-writer),输入 = 大纲 + 成员卡正文 + 已链接的相关 wiki concepts/。关键:本 skill 不自己写,只把材料组好。
- 回流:文章发表后(用户手动通知或 wiki-sync 检测到 published-source 新增),把对应卡的
cluster_history 标为 published,卡的 frontmatter 加 produced_article: <wiki-path>。
- git push 硬条款(v1.2.0):若 cluster_history 或卡 frontmatter 有改动,
cd ~/zettel && git add -A && git commit -m "chore(write): <cluster-id> handed off" && git pull --rebase && git push。
调度集成(双层)
Layer 1: systemd(无 LLM,hourly)
scripts/zettel-hourly-sync.sh 由 zettel-sync.service(systemd user unit)hourly 触发,链在 wiki-sync.service 之后(After= + Wants=),保证 raw/ 已刷新。
只做无 LLM 的工作:
build_index.py 重建 tag/entity/link/term 索引
embed_lite.py 增量刷新 BGE 嵌入
scan_mechanical.py 检测 raw/ 新增 → 写 _queue/
cluster_inspect.py 重算 ready/near_ready
- git pull/push 兜底(以防 LLM session 漏 push)
Layer 2: OpenClaw cron(LLM session,daily/weekly)
~/.openclaw/cron/jobs.json 中两条 job,target=Oven(workspace-openai,GPT-5.5,@your-openclaw-bot):
| Job | 频率 | 干什么 |
|---|
zettel-daily-scan | 0 9 * * * | 消化 _queue/ 中 mechanical scan 攒下的待办,Mode 2 跑一遍,Telegram 摘要 |
zettel-weekly-inspect | 0 10 * * 0 | 周日深巡:LLM 给就绪簇命题、解读 gap,Telegram 推荐 /zettel write 候选 |
为什么是 OpenClaw 不是 Hermes:OpenClaw cron 原生支持 payload.kind: agentTurn(唤起 LLM 跑一段对话),Hermes cron 只是 script-driven(跑 Python 脚本)。zettel 的 scan/inspect 需要 LLM session,架构上只有 OpenClaw 能直接表达。
详见 references/scan-mechanical.md(systemd 层)+ references/openclaw-cron.md(Oven 层)。
Mode 5: propose — 命题作文(v1.12.0)
用法:
- 命令式:
/zettel propose <seed-text>
- 自然语言(给 Oven):"围绕 X 写一篇"、"用 X 这个想法启发,从我笔记里找点料"
与 Mode 4 write 的区别:Mode 4 是自下而上(就绪簇 → 文章);Mode 5 是自顶向下(user seed → 从网络挑材料组簇 → 文章)。
步骤:
-
Python 召回:python3 scripts/propose_seed.py --seed "<text>"
- 算 seed 的 BGE 嵌入
- 跟 cards/ 嵌入做 cosine,取 top-30 候选
- 写
_state/proposal_<ts>.json(含 seed + candidates + 元数据)
-
Agent 判定 + 分角色(必须 Read 每张候选卡正文):
- support:本卡可作论据支撑 seed
- counter:本卡反对 / 限定 seed
- case:本卡是 seed 的具体案例
- context:本卡提供背景 / 前史
- unrelated:看似相关其实不是,丢弃
不是 sim 高就纳入 — 必须看实际内容判断。
-
写 proposal 文档:~/zettel/_drafts/proposal_<ts>.md
- Seed 原文(引言)
- 分角色卡片清单(每条
[[<card-id>]] — <role>: <一句话相关性说明>)
- 拟标题(基于 seed + 候选卡综合命题,陈述性判断,不要名词短语)
- 建议大纲(把 seed 作为论点,把候选卡组织成承接它的论证链)
- 缺口标注(若主要 support 链上有空洞,可触发 deep-research 补卡)
-
回报:
- Telegram 简报 + proposal_id
- 用户审视
_drafts/proposal_<ts>.md 后,可:
/zettel write proposal_<ts>:移交你自己的写作 Skill 出稿
/zettel propose ...:换 seed 重来
- 手动编辑 proposal:增删卡 / 改大纲
-
git push 硬条款:proposal 文档写完后 cd ~/zettel && git add/commit/push,跨机器可见
触发关键词(Oven 在 Telegram 接到这些自动走 Mode 5):
/zettel propose <seed>
- "围绕 X 写一篇" / "用 X 这个想法启发" / "命题作文 X" / "基于这个想法写"
与其他 skill 的关系
| Skill | 关系 |
|---|
| 你的写作 Skill | Mode 4 移交;Mode 1 重述阶段复用其 values + style-guide(在 references/voice-snapshot.md 维护快照) |
| 选题/topic-inspiration 类 Skill | 互补:那个是看缺口选题(自顶向下),本 skill 是已有素材长卡(自底向上)。两者可以串:选题输出 → 转 deep-research → 卡入 zettel |
| deep-research | Mode 4 缺口补漏;调用方式见 references/deep-research-handoff.md |
| 你的笔记/收藏源(如 GetNote、Readwise) | 上游源(通过 wiki-sync 等方式落到 raw/) |
| 闲聊/想法延伸 Skill(如 getseed) | 不重叠:那类是把闲聊延伸成线性产物;本 skill 是积累卡片网络。可串接:线性产物输出后转 ingest |
不做的事(防边界滑坡)
- 不自动发文:就绪簇只发 Telegram 建议,等用户
/zettel write
- 不改你的写作风格规则:重述时直接读
references/voice-snapshot.md(你预先填好的价值观/文风快照),不在本 skill 复制业务文风
- 不当 GetNote 镜像:literature note 引用 raw/ 路径,不复制原文
- 不解决"该写什么":本 skill 帮你在既定方向上更高效组织,不替你决定方向(这是诚实的边界)
- 本地嵌入做语义召回(v1.1.0):BGE-small-zh-v1.5 via
fastembed,~90MB 缓存在 ~/.cache/fastembed/(不进 git);TF-IDF 作为降级路径。最终是否真连仍由 agent 判断
失败时
| 现象 | 排查 |
|---|
| ingest 找不到判断,只能列举 | 素材本身是清单/事实集,不适合做 permanent note;考虑只入 fleeting/ |
| 链接候选全是低相关度 | 检查 _index/ 是否过期,跑 scripts/build_index.py --rebuild |
| scan 反复处理同一文件 | 检查 _state/last_scan.json 的 mtime 是否被 git 重置;processed/<date>.jsonl 是否漏写 |
| 簇就绪却没收到 Telegram | 检查 _state/cluster_history.json 是否已把该簇标过(去重逻辑) |
| 价值观重述变成换词游戏 | agent 重读 references/voice-alignment.md,该协议明确要求产生新判断;否则不算 voice_passed |
版本
- v1.4.0 (2026-05-20) — Self-contained 协议:正文必须含
## 原文要点 块(blockquote 完整搬运 200-500 字原文,不是 30 字摘要)+ ## 我的判断 节。图片搬运:原素材含  时,scripts/migrate_images.py 复制图到 ~/zettel/assets/<card-id>/,卡正文改用新路径引用。脱离 source.ref 仍可读为硬测试。scan_mechanical.py queue item 增加 source_images 字段。旧 raw-source 卡(v1.3 及之前)归档到 cards/_archived_v13/,raw 重新入队 Oven 按新协议重做。
- v1.13.0 (2026-05-20) — wiki-article 多卡切分。用户发现 v1.12 时 82 篇文章只产 83 张卡(平均 1.01),信息压缩过严。新协议:长文必须通读 + 找全 atomic 判断 + 按字数对应产卡数(短 2-3 / 中长 3-6 / 长 6-10 / 巨长 10-15)。1500+ 字的文章只产 1 张需在 processed jsonl 写明理由。规则见
references/voice-alignment.md Step B.2。已切 1 张的 82 篇暂不强制重做,bootstrap 跑完后由用户决定是否回填。
- v1.16.1 (2026-05-21) — Deep-research 后台补漏 worker。新 cron
zettel-deep-research-worker(disabled 默认,手动触发或 cron schedule)。流程:读 _state/deep_research_pending.json 下一个 pending → 跑 /deep-research → 存 ~/zettel/raw/deep_research/<proposal_id>__<gap_type>/ → Mode 1 ingest 切多卡(source.type=deep-research)→ 更新 top3_outlines.json 的 deep_research_status → render HTML → push → Telegram。触发词 "/zettel deepresearch" / "补一个 gap" 等。
- v1.16.0 (2026-05-21) — 图文 TOP3 + GitHub Pages 部署。
render_top3_html.py 生成图文 HTML(SVG 关系图 + LLM outline + gap badges + 推荐动作 + GitHub blob 卡片链接)。Pages: https://your-github-user.github.io/zettel/。Oven 写 top3_outlines.json + deep_research_pending.json,Python 渲染 HTML。
- v1.15.0 (2026-05-21) — Multi-view cluster detection。详上。
- v1.12.0 (2026-05-20) — Mode 5 命题作文 + 证据图优先。
- Mode 5 propose:用户给 seed(案例/论点/问题),Python 召回 top-30 候选卡 → agent 读卡判定相关性+分角色(support/counter/case/context)→ 写
_drafts/proposal_<ts>.md → 用户审视后 /zettel write 移交写作 Skill。自顶向下"命题作文",跟 Mode 4 (自下而上从就绪簇出文) 互补。
- 证据图协议:wiki-article 源默认
migrate_images.py --skip-first --include-remote。第一张图通常是题图(封面/装饰),跳;evidence 图(idx 1+)是 R2 远程 URL,卡正文直接用 URL,不复制本地。选图标准:证据图(截图/数据图/对比图)优先,题图不要。