在微信里记笔记,内容要进 Obsidian 知识库,手工转换既慢又容易出错。写一个 Obsidian 插件把格式规则固定化,也无法适应微信和公众号笔记五花八门的样式。我决定把 Claude 加进来:收消息→分类→起标题→格式化→落盘,再回执通知。
核心架构:Claude 决策,Node 写盘
首先明确职责边界,这是避免混乱的关键。
Claude 的工作是纯决策:给定一条消息和分类表,输出结构化结果——归入哪个分类、建新文档还是追加到现有文档、标题是什么、标签列表、正文格式化后长什么样。结果都用 JSON 表示。
Node 的工作是确定性执行:接收 Claude 的决策结果后,负责所有文件读写——新建文档、追加到已有文档、处理图片、管理去重、并发序列化。不让 Claude 直接操盘文件系统。
这个分工的好处:
- 风险隔离:LLM 的输出有概率不确定(分类可能不准),但文件操作必须确定。如果 Claude 直接写盘,分类抖动可能污染已有文档、重复建文件、覆盖历史记录。现在坏决策最多只会建错新文件,不会改坏既有内容。
- 幂等与并发:并发来的消息按消息 ID 去重,串行通过写入队列写盘。没有 LLM 的非决定性卡在关键路径上。
- 可测试性:Claude 的逻辑用假实现隔离测试,vault-writer 的逻辑用临时目录单测。
数据流与核心组件
单条消息的完整生命周期:
- listener:WeChatFerry 监听主号私聊,提取消息ID、文本、图片,构造 Capture 对象。
- dedup:msgId 按已处理集合去重(持久化到 processed.json,防 WCF 重发导致重复落盘)。
- classifier:遍历 vault 列出已有文档按分类分组,把消息文本+分类表描述发给 Claude。Claude 一次性决定归类、action(new|append)、新标题、标签、正文。失败或超时降级为 00-Inbox 原样保存。
- vault-writer:按决策新建或追加文件。图片从临时区移到 vault 附件目录。所有操作走串行队列,天然排斥并发冲突。
- receipt:回执主号:"✓ 新建 → 01-技术,标题:Redis 锁续期"。
失败处理分两层:
- 第一层:classifier 异常→降级到 Inbox 分类,原消息文本作为 body。
- 第二层:vault-writer 也失败→回执错误信息,原消息不丢(至少进了 dedup)。
Claude 分类的提示词设计
Claude 的 prompt 包三部分信息:
【可选分类】
- 01-技术: 技术笔记、踩坑、方案、命令、代码片段
- 02-项目: 具体项目的记录、进展、待办
...
- 00-Inbox: 兜底:拿不准归哪类的进这里
【各分类下已有文档标题】
- 01-技术: Redis 分布式锁 | OTA 增量包大小优化 | Python 装饰器
- 02-项目: Q2 KPI 指标 | 客户需求评审
...
【规则】
1. 只能选上面列出的分类 key
2. 若与某个"已有文档标题"主题相似且确信,用 action=append,targetFile 填该标题;否则 action=new
3. 拿不准就 00-Inbox + action=new
4. 只输出一个 ```json 代码块,字段:category, action(new|append), targetFile, title, tags(数组), body
关键设计点:
- 传已有标题清单而不是全部文档:vault 大了之后这会撑爆 token,后续优化改向量检索 Top-N 候选。v1 库空,列全量可接受。
- action=append 时 targetFile 必须来自该分类的已有标题清单:确保 append 不会创建新文件。如果 Claude 编造了不存在的标题,validator 改回 action=new。
- 拿不准就 new:错建新文件<错污染已有文档。
文件写入的不可变原则
vault-writer 的核心逻辑:
新建:
路径: <vault>/<category>/<安全标题>.md
内容:
---
title: Redis 分布式锁续期方案
category: 01-技术
tags: [redis, 分布式锁, 并发]
created: 2026-06-13 14:30
source: wechat
---
<Claude 格式化后的正文>
![[20260613-1430-image.png]]
- 文件名去除 Windows 非法字符、限长 80 字。
- 重名加紧凑时间后缀(
Redis 锁-1430.md),不覆盖。
追加:
原内容 <不改一字>
## 14:30 追加
<新增内容>
![[img2.png]]
只在文末加小节,绝不改写旧内容。追加同样走串行队列,多个消息对同一文档的追加不会交错。
图片处理:
从临时区移到 <vault>/99-附件/<紧凑时间戳>-<原文件名>.png,正文用 wikilink 引用 ![[20260613-1430-image.png]]。Obsidian 自动刷新并识别嵌入。
去重与幂等
WCF 有重发行为,msgId 相同的消息可能来两次。processed.json 记录已处理的 msgId:
["msg_id_1", "msg_id_2", "msg_id_3"]
listener 每条消息来时检查,命中过则忽略。标记操作在最后,确保消息全部处理成功才记录。
这设计下,网络抖动导致的消息重发、甚至小号掉线重连后的历史消息再次推送,都不会重复落盘。
中转站密钥隔离
Claude Code CLI 的调用走 spawn 子进程,环境变量注入:
const env = {
ANTHROPIC_BASE_URL: config.baseUrl, // https://key.agtk.cn
ANTHROPIC_AUTH_TOKEN: config.authToken, // 用户自注册的 key
};
await runCommand(nodePath, args, { timeoutMs, env });
.env 文件里存关键数据:
ANTHROPIC_BASE_URL=https://key.agtk.cn
ANTHROPIC_AUTH_TOKEN=sk_xxxx
.gitignore 排除 .env,不进代码库。自包含发行包也不带任何密钥——用户在 setup 时从 key.agtk.cn 自己取 key 粘贴,由客户自付费用。
这样做的风险隐含:内容会经第三方中转站再到 Claude(而非直连 claude.ai)。setup 里明确风险告知和确认。
配置的参数化
三个配置文件:
config.yaml:业务配置
vault_path: "D:\\Obsidian\\MyVault"
attachments_dir: "99-附件"
owner_wxid: "wxid_main" # 首条私聊自动绑定
model: "claude-opus-4-8" # 用户选择
request_timeout_ms: 30000
categories.yaml:分类表,改它即改 Claude 的分类依据
- key: "01-技术"
desc: "技术笔记、踩坑、方案、命令、代码片段"
- key: "02-项目"
desc: "具体项目的记录、进展、待办"
...
.env:敏感信息(密钥、中转站地址)
ANTHROPIC_BASE_URL=https://key.agtk.cn
ANTHROPIC_AUTH_TOKEN=sk_xxxx
三层分离使得:业务逻辑能复用,分类策略能快速迭代(改 YAML 即可),敏感信息不泄露。
自包含发行包
最终产物是一个 zip,无需用户装 Node、不连公共 npm 源:
obsidian-weix-0.1.0.zip
├─ node\node.exe (便携 Node)
├─ app\
│ ├─ src\ (核心代码)
│ ├─ node_modules\ (预装依赖)
│ └─ templates\ (配置模板)
├─ install.bat
└─ start.bat
用户下载、解压、双击 install.bat→setup 向导→粘贴 key→输入 vault 路径→风险确认→生成配置。然后双击 start.bat 启动。
构建机在 Windows x64 编译,执行 npm install(得到目标平台的 native 产物),打包后上传到项目专用 CDN 分发。
为什么不是 Obsidian 插件
Obsidian 插件跑在浏览器沙盒里,无法直接调 WCF、spawn CLI、管理进程生命周期。插件能做的是提供 UI 让用户手工输入内容,或者轮询本地文件监听外部写入。
相比之下,独立 Node 进程的好处:
- 常驻内存,实时监听 WCF 消息事件。
- 能 spawn Claude Code CLI 并注入中转站密钥(需要环境变量隔离)。
- 确定性串行写盘,无沙盒限制。
代价是多了一个进程,但换来的是解耦和可靠性。
工程检查清单
这套管线从设计到代码,需要覆盖的点:
- 单元测试:文件名净化、时间格式化、Markdown 渲染、去重、分类降级、append 格式。
- 集成测试:classifier 对中转站打桩;vault-writer 写临时目录校验产物;并发投递验证队列串行。
- 幂等性:msgId 去重持久化,重发消息不重建文件。
- 并发安全:写入队列保证串行,追加操作不交错。
- 失败降级:Claude 超时 30s 后重试 1 次,仍失败降级 Inbox;vault-writer 失败也降级。
- 脱敏:密钥进 .env 不进代码库,配置项参数化。
写盘前必须问清自己:如果这条消息落地失败了,会不会丢掉?不会——最坏情况进 00-Inbox 原文保存。如果 Claude 分类抖动,会改坏其他文档吗?不会——append 时校验 targetFile 存在性,不存在改成 new。
这些约束叠加起来,保证了管线的韧性。
■