Skill 方案 S4 · 笔记批量入库与版本管理
一句话:把「一堆 Markdown 要按父子结构挂进笔记站、补摘要评价关键词、还不能覆盖旧版本」这件事固化成 pack 文件 + 一条命令;15 次以上批量入库消耗的 0.18 亿 token 可压到 0.04 亿。
一、为什么需要它
两个月里笔记站的批量入库做了 15 次以上,其中包括本次复盘本身——7 篇子篇 + 1 个父篇,就是靠 wb-create.mjs 和 wb-publish-pack.mjs 落进去的。
| 指标 | 数值 | 说明 |
|---|---|---|
| 批量入库次数 | 15+ | 单次 1–8 篇 |
| 累计 token | 0.18 亿 | 平均每次 12 万 |
| 平均工具调用 | 7–12 次 | 每条笔记一次 POST + 一次校验 |
| 失败重试 | 常见 1–2 次 | 父子顺序错、内容过短、meta 缺字段 |
| 单篇平均正文 | 1500–9000 字 | pack 文件最大 16 KB |
现状四个痛点:
- 父子顺序敏感。子篇必须先有父篇 id,手工逐条 POST 时经常先发子篇,结果
parentId为空,只能删了重发。 - meta 靠现编。
summary/evaluation每次写法不一,短到 20 字、长到 400 字都有,检索命中率随心情波动。 - 幂等缺失。同一篇重跑一次就多一份副本,标题一模一样,事后无法分辨哪份是主版本。
- 版本规则被误用。笔记站的
NoteVersion是"永不覆盖",只有一个isPrimary;有人直接 PUT 覆盖正文,把历史版本连同direction一起抹了。
二、触发词与边界
| 触发词 | 不适用场景 |
|---|---|
| 「入库」「存到笔记站」「批量发布」 | 只改一篇的一句话 → 直接编辑,不进 pack |
| 「挂到某篇下面」「做子篇」 | 要改数据库 schema → 走迁移,不走 API |
| 「更新版本」「加个方向重写」 | 要删笔记 → 手动确认,skill 不提供删除 |
| 「这批笔记补一下摘要/关键词」 | 要导入 docx 原文件 → /api/import/docx,不在本 skill |
| 「把上次那批重发一遍」 | 内容未变 → 先查重,不重发 |
边界:只新增,不删除,不覆盖。删除与覆盖属于破坏性操作,必须逐条人工确认。
三、输入与前置检查
pack 文件格式固定(wb-publish-pack.mjs 按行解析,@@ 开头为元信息,其余为正文):
@@NOTE key=s1
@@TITLE Skill 方案 S1 · 站点健康巡检与自愈
@@CATEGORY Skill 方案
@@SUMMARY 80-150 字摘要
@@EVAL 60-120 字评价
@@KEYWORDS 站点巡检|运维,自愈|运维,隧道|运维
@@PARENT root
# 正文 Markdown
前置四查:
| 检查 | 命令 | 通过标准 |
|---|---|---|
| 笔记站在线 | Invoke-WebRequest -UseBasicParsing http://127.0.0.1:3102/notes/ -TimeoutSec 8 | 200 |
| 门户密钥存在 | Test-Path "C:\Users\cag\WorkBuddy\2026-10-11-06-00-16\portal\data\portal-secret.txt" | True |
| 父篇 id 有效 | GET /notes/api/notes/<parentId> | 200 且标题匹配 |
| 正文长度 | (Get-Content s1.md -Raw).Length | > 200(脚本硬性拒绝过短内容) |
父篇 cmwftobjv0039lh8cg6a7p0dg(「二、重复操作与 Skill 方案」)已存在,7 篇子篇全部挂它下面。
四、执行步骤
步骤 1 · 单篇入库(有父篇时)。
node wb-create.mjs --file tmp-wb/s1.md \
--title "Skill 方案 S1 · 站点健康巡检与自愈" \
--category "Skill 方案" \
--summary "把门户3000/词汇站3001/笔记站3102/隧道的体检与自愈固化成一条命令,含四层检查矩阵与L0-L3分级自愈,253次巡检会话的0.99亿token可压到0.15亿。" \
--eval "方案把巡检从对话改成脚本,补上了内容指纹与分级自愈两个缺口;与现有kaoyan-site-health是超集关系,落地时把它收缩成薄入口即可。" \
--keywords "站点巡检|运维,自愈|运维,隧道|运维,健康检查|运维" \
--parent cmwftobjv0039lh8cg6a7p0dg
输出一行 JSON,含 id、url、parentId。parentId 为空就是挂靠失败,必须重发而不是将就。
步骤 2 · 多篇入库(有父子引用时)。 一个 pack 文件解决顺序问题,因为脚本按 @@NOTE 出现顺序发送,并把 key→id 记进 ids 映射:
node wb-publish-pack.mjs wb-pack-1.md
@@PARENT root 里的 root 是同一个文件里先前定义的 key;父篇必须写在最前面。跨文件引用不支持,跨批复用要显式传 --parent <真实id>。
步骤 3 · 幂等去重。 发之前先按标题查一遍,命中就跳过:
node -e "
const s=require('fs').readFileSync('C:/Users/cag/WorkBuddy/2026-10-11-06-00-16/portal/data/portal-secret.txt','utf8').trim();
const h={'x-portal-secret':s,'x-user-id':'eeefa7d5c16a49bdaeff','x-user-username':'cag','x-user-role':'admin'};
(async()=>{
const r=await fetch('http://127.0.0.1:3102/notes/api/search?q='+encodeURIComponent('Skill 方案 S1'),{headers:h});
const j=await r.json();
console.log(JSON.stringify(j).slice(0,600));
})();"
命中同标题且 parentId 相同的,视为已入库,跳过;只是标题相同但父不同,属于合法的新篇,继续发。
步骤 4 · 更新版本而不是覆盖。 内容需要重写时,走版本接口,旧的自动降级、新的置为 isPrimary:
node -e "
const s=require('fs').readFileSync('C:/Users/cag/WorkBuddy/2026-10-11-06-00-16/portal/data/portal-secret.txt','utf8').trim();
fetch('http://127.0.0.1:3102/notes/api/notes/<noteId>/regenerate',{
method:'POST',
headers:{'content-type':'application/json','x-portal-secret':s,'x-user-id':'eeefa7d5c16a49bdaeff','x-user-username':'cag','x-user-role':'admin'},
body:JSON.stringify({direction:'补上 S2 的停止顺序与 Prisma 锁窗口'})
}).then(r=>r.json()).then(j=>console.log(JSON.stringify(j).slice(0,300)));"
direction 是用户原话的直接落点,必须写清楚"改什么",不要写"优化一下"。
步骤 5 · 白皮书标记。 需要进白皮书板块的笔记,在入库后单独置 isWhitebook=true;它不复制内容,改笔记即改白皮书。
步骤 6 · 批量校验。
node -e "
(async()=>{const s=require('fs').readFileSync('C:/Users/cag/WorkBuddy/2026-10-11-06-00-16/portal/data/portal-secret.txt','utf8').trim();
const h={'x-portal-secret':s,'x-user-id':'eeefa7d5c16a49bdaeff','x-user-username':'cag','x-user-role':'admin'};
const r=await fetch('http://127.0.0.1:3102/notes/api/notes/cmwftobjv0039lh8cg6a7p0dg',{headers:h});
const j=await r.json();
console.log(j.note.children.length, j.note.children.map(c=>c.title).join(' | '));})();"
期望 7 个子篇,标题与 pack 完全一致。
五、输出与验收
{"id": "cmwft...", "title": "Skill 方案 S1 · 站点健康巡检与自愈", "url": "/notes/note/cmwft.../", "parentId": "cmwftobjv0039lh8cg6a7p0dg", "words": 3820}
三条硬断言:
- 每篇返回的
parentId必须等于cmwftobjv0039lh8cg6a7p0dg,为空即失败。 - 正文长度必须 > 200 且与源文件字符数误差 < 5 字(脚本本身会做最小值拦截)。
- 父篇
children数量必须等于本次提交篇数,且没有重复标题;重复即说明幂等失效。
六、失败处理与回滚
| 现象 | 根因 | 动作 |
|---|---|---|
内容过短,拒绝入库 | 文件读成空或路径错 | 检查 --file 相对路径,确认字符数 |
| 返回 401/403 | 密钥文件缺失或用户头不对 | 读 portal-secret.txt,四个 x-user-* 头齐全 |
parentId 为 null | 父 id 打错或父篇被删 | 先 GET 父篇确认存在,再重发 |
| 同一篇出现两份 | 没做步骤 3 的去重 | 保留 id 较小的一份,另一份改标题加「(副本)」后人工归档 |
| 版本越改越乱 | 直接 PUT 覆盖了正文 | 从 NoteVersion 里把最后一版置回 isPrimary |
回滚:新增类操作没有事务性回滚,只能"标记作废"——把误发的笔记标题前加 [作废] 并把可见性改 private,不要物理删除。版本类操作可回滚:把目标 version 重新切为 isPrimary 即可。
七、收益测算
| 项目 | 现状 | 改造后 |
|---|---|---|
| 单次 token | 12 万(逐条 POST + 反复校验) | 2.6 万(一个 pack + 一次校验) |
| 次数 | 15+ | 15+ |
| 累计 | 0.18 亿 | 0.04 亿 |
| 父子错挂 | 常见 | 接近零(key 映射) |
| 重复入库 | 时有发生 | 去重前置 |
省的是"每条一次往返 + 出错重来"。节省 0.14 亿,约 78%,是七类里比例最低的一项——因为写作本身不可压缩,能省的只有编排开销。