飞书知识库最大的问题不是「没有」,而是「有也没人养」------文档写过一次就再没人更新,业务上下文散在代码注释和几个老人脑子里,新人接手仓库光搞清「这接口给谁调」就得问一圈。
我们做的事很简单:把飞书知识库变成一个自动化知识库 ------不用人专门维护,它跟着代码 commit 自己更新。开发前自动把对应业务上下文拉进会话,提交时按知识库里的规范查代码,提交完自动把这次改动总结写回飞书知识库、还附一条迭代评论。本文讲这套东西的真实实现:一个 Claude Code 插件,4 个 skill + 3 个 commit hook,底层 shell out 调飞书的
lark-cli读写 wiki 节点。不卖概念,贴代码。
一、整体长什么样
一句话:代码仓库和飞书知识库之间,挂了四个会话内命令和三个 commit 钩子,靠两个配置文件指路。

四个命令各管一件事:
| 命令 | 时机 | 干什么 |
|---|---|---|
kb-read |
开发前 | 按当前项目把知识库对应节点正文拉进会话当背景 |
kb-extract |
周期性 | 扫源码,整篇覆盖写回(收敛多次增量) |
kb-sync |
提交后 | 取上次同步以来的 diff,增量追加 + 一条迭代评论 |
kb-review |
提交前 | 从知识库拉规范,逐条比对 staged diff |
剩下三个 hook 负责「让人不用记着敲这些命令」------后面细讲。
整个东西打包成一个 Claude Code 插件,真实文件结构就这么点:
bash
feishu-kb-tools/
├── .claude-plugin/
│ ├── plugin.json # 插件元信息
│ └── marketplace.json # 自带 marketplace(source=".",仓库即插件)
├── skills/
│ ├── kb-read/SKILL.md
│ ├── kb-extract/SKILL.md
│ ├── kb-sync/SKILL.md
│ ├── kb-review/SKILL.md
│ └── kb-extract/references/
│ ├── kb-adapter.md # 接缝契约,四个 skill 共用
│ └── feishu-block-mapping.md # markdown → 飞书块映射规则
├── hooks/
│ ├── hooks.json # 三个 hook 注册
│ ├── kb-commit-review.sh # PreToolUse 提交前查规范
│ ├── kb-commit-sync.sh # PostToolUse 提交后埋同步标记
│ ├── kb-stop-check-sync.sh # Stop 硬拦回要求同步
│ └── git-pre-commit.sh # 终端手敲 commit 的兜底
├── setup.sh # 一条命令接入(授权+装插件+生成.kb.yml+建基线)
├── README.md
└── OPERATIONS.md
每个 SKILL.md 就是一份给会话内 LLM 读的操作说明(不是运行时代码),hook 是普通 bash 脚本。没有后端服务、没有数据库、没有外部 API key------所有「智能」都在会话内 LLM 上,所有「读写」都 shell out 给 lark-cli。
二、三个文件撑起整条链路
这套设计里没有复杂框架,核心就是三个文件,对应三个概念。理解了它们,整条链路就清楚了。
1. 映射表 .kb.yml ------ 代码的哪部分,对应知识库哪个节点
放项目根目录。它回答一个问题:提炼出来的东西往哪写、读的时候从哪拉,不能靠人记。
yaml
businessLine: 订单中心 # 业务线标识,给索引用
kbNode: <wiki_node_token> # 业务上下文根节点,kb-read 默认拉它
sync: # 细粒度映射,extract/sync 用
- src: src/components/** # glob 匹配的源文件
kbNode: <node_token> # 归到哪个节点
type: component # component | spec | business | api
- src: docs/frontend-spec.md
kbNode: <node_token>
type: spec # review 只从 type=spec 节点拉规范
type 字段是关键------它让工具知道「这是组件说明还是规范条目」,于是 review 只去 spec 节点取规则,extract 只去 component 节点写说明。一份映射,四条工作流各取所需。
2. 索引 .kb-registry.json ------ 按业务线找节点
新人 clone 个仓库,还没建 .kb.yml,怎么找到知识库对应位置?靠一份集中注册表,按业务线反查节点 token。kb-read 的定位顺序是三级回落:
这份 registry 不是手填的------setup.sh 接入项目时自动 upsert 一条进去(按仓库绝对路径匹配,存在就更新、不存在就追加),核心就是这么一段 Python:
python
data.setdefault("projects", [])
proj = next((p for p in data["projects"] if p.get("repo") == repo), None)
if proj is None:
proj = {"name": name, "repo": repo}
data["projects"].append(proj)
proj.update({"name": name, "businessLine": bl, "kbNode": kb, "owner": owner})
为什么按 repo 路径匹配不按 name?因为同一台机器上可能 clone 多份,路径是唯一键,name 会撞。registry 路径本身也是三级定位(KB_REGISTRY 环境变量 → 向上找 .kb-registry.json → ~/.kb-registry.json),因为 repo 路径是机器相关的,各人 registry 在各自家目录,仓库里这份只是样例。
3. 接缝 kb-adapter.md ------ 把底层调用收敛成一份
这是最不起眼但最值钱的一个文件。四个 skill 都要读写飞书(解析节点、读正文、写正文、定位段落、发评论),如果每个 skill 各写一遍 lark-cli 命令,规则一改改四处。所以把它们收敛成一份「命名操作」契约,skill 只引用 §操作名:
| 命名操作 | 干什么 |
|---|---|
normalize_token |
裸 token 包成 URL 传给 CLI(CLI 靠 wik 前缀判类型,新节点 token 不带这前缀会报错,规则只存这一处) |
resolve_obj |
节点 token → 文档 obj_token |
read_doc / write_doc |
读 / 写正文(命令参数、fallback、scope 只存一份) |
fetch_block_id |
按关键词定位段落,发锚定评论用 |
add_comment |
锚定发局部评论,拿不到 block id 回落全文评论 |
关键是「接缝」二字:契约写死的是操作语义(「读一篇文档」「追加一段」),不是实现。以后底层从 CLI 换成 MCP 工具、从飞书换成别的知识库,各 skill 的编排逻辑和文本一字不动,只换 adapter 里的实现段。现在能跑,将来能换,这两件事被解耦了。
三、四个 skill 怎么干活
有映射指路、索引定位、接缝屏蔽底层,四个 skill 本身就很薄。挑两个最值得讲的。
kb-sync:增量写回,分三段,写前必经确认
sync 是整套里最复杂的一个,因为它要往知识库写东西,而自动写东西最怕写错 。所以它被拆成三段:生成草稿 → 用户确认 → 才写回。AI 自动总结 delta 和评论,但落不落笔人来定。
靠一个基线文件 .kb-sync-state.json 记「上次同步到哪个 commit」,算增量:
json
{ "sha": "<上次同步时的 HEAD sha>", "at": "2026-07-03T12:00:00+08:00" }
- 文件不存在 → 视为首次,整篇 overwrite(同 extract 语义)。
- 文件存在 →
git diff <sha>..HEAD算增量,只提炼「变了什么」,不重读全文件。
增量写回默认用 append(末尾追加一段带日期+sha 的「更新」章节,不动既有正文),不是 overwrite。这个选择是有意的:
- sync 是 append,extract 是 overwrite。 sync 高频小步追加增量,extract 低频整篇重提炼。sync 万一写了点幻觉,下一次 extract 整篇覆盖会把它收敛掉------非破坏性 + 自愈,不让每次增量都永久污染正文。
- sync 还顺手发一条锚定评论:AI 从本次 diff 总结「背景 + 解决了什么业务问题」,锚定到新追加的那段标题 block 上,让版本演进在知识库评论区一眼可见,而不是埋进正文被下次覆盖。
写回前还有两道审查直接写进了 skill:
- 溯源 :delta 每条标
文件:行可追 diff,评论背景基于 diff + commit message 推断,不得编造不存在的接口/字段。 - 脱敏 :扫草稿里的私钥、
password=、token=、AKSK、JWT、≥32 字符密钥串、身份证/手机号 → 替换成***REDACTED***,确认门里把命中项单列给用户复核。
kb-review:规范查代码,反过来代码也补规范
review 不是单向的。正向:从 type:spec 节点拉规范,逐条比对 staged diff,违规记 block/warn。反向:扫到 diff 里有反模式、但现有规范清单没覆盖 的,记一条 spec-gap,建议补一条 Do/Don't 句式的规范条目(也标 文件:行 来源)。
go
正向:规范 → 代码(违规阻断 commit)
反向:代码 → 规范(缺口提示补条目,经确认后 append 进 spec 节点)
代码和规范互相校准,谁都不会一直领先、也不会一直落后。
四、commit 时三个 hook 怎么接力
上面四个命令如果靠人记着敲,必然忘。真正让闭环成立的是 git commit 时三个 hook 接力。配置就这么多(hooks.json):
json
{
"PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}\"/hooks/kb-commit-review.sh" }] }],
"PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}\"/hooks/kb-commit-sync.sh" }] }],
"Stop": [{ "hooks": [{ "type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}\"/hooks/kb-stop-check-sync.sh" }] }]
}
接力过程:

三个细节,是踩坑后才这么写的:
1. 软提议不靠谱,硬拦截才可靠。 PostToolUse 能输出 additionalContext「建议」Claude 跑 sync,但 Claude 可以无视。真正把人拦回来的是 Stop hook 的 decision:block------Claude 想结束本轮时,Stop hook 见到 pending 标记就硬拦,喂一句「先跑 /kb-sync」。靠礼貌请求不可靠,靠状态标记 + 硬决策才稳。Stop hook 的核心就几行:
bash
if [ -f .kb-sync-pending ]; then
mv .kb-sync-pending .kb-sync-pending-blocked 2>/dev/null || true # 改名防死循环
PYTHONUTF8=1 python -c '
import json
reason = "本次提交的知识库同步还没做。请运行 /kb-sync ... 跑完(应用或放弃)后自动放行。"
print(json.dumps({"decision": "block", "reason": reason}, ensure_ascii=False))'
exit 0
fi
拦一次后把 flag 改名成 .kb-sync-pending-blocked,万一 Claude 还是不跑 sync,下次 Stop 见到 blocked 就放行------拦一次是提醒,拦两次是放生,不能死循环把人锁死。
2. 防递归。 review hook 要自己起一个 claude -p 子会话跑 review,子会话里万一再 commit 又触发 hook,就嵌套了。解法是一个环境变量 guard:进子会话前 export KB_HOOK_RECURSION_GUARD=1,三个 hook 顶部都检查它,命中就秒退:
bash
[ "${KB_HOOK_RECURSION_GUARD:-0}" = "1" ] && exit 0
子会话继承这个变量,hook 自动跳过,避免嵌套和双重计费。
3. review hook 怎么判定。 review hook 从 stdin 读 PreToolUse 的 JSON,用 Python 解析 tool_input.command,只拦 git commit,无 .kb.yml 或 KB_REVIEW_SKIP=1 跳过,然后起子会话跑 review,要求它最后一行输出判定 sentinel:
bash
export KB_HOOK_RECURSION_GUARD=1
REVIEW="$("$CLAUDE_BIN" -p "运行 kb-review ... 完成后在最后一行输出且仅输出一行判定:
存在 block 级违规输出 'KB_REVIEW_VERDICT: BLOCK' 并在前面列 findings;否则输出 'KB_REVIEW_VERDICT: PASS'。" 2>&1)" || true
unset KB_HOOK_RECURSION_GUARD
if printf '%s' "$REVIEW" | grep -q 'KB_REVIEW_VERDICT: BLOCK'; then
printf '%s\n' "$REVIEW" >&2; exit 2 # 阻断 commit,findings 回灌给 Claude 修复
fi
if printf '%s' "$REVIEW" | grep -q 'KB_REVIEW_VERDICT: PASS'; then exit 0; fi
# 无 sentinel:机制性失败,放行但警告,不锁死用户
exit 0
用 sentinel 字符串而非退出码传判定,是因为子会话的 stdout 还要带 findings 文本给 Claude 看。失败策略是 fail-open:review 机制本身崩了(claude 不在、超时、没 sentinel)就放行 + 警告,不把人锁在 commit 之外。
还有个盲区:CC hook 只拦「Claude 经 Bash 跑的 commit」,人自己在终端敲的 commit 不经过。兜底是
setup.sh可选装一个 git 原生pre-commit,跑同一套 review。两套并存,堵住终端手敲的口子。
五、串起来:一次完整跑通长什么样
把上面的 skill 和 hook 拼起来,一个开发者典型的一天是这样的:

写回知识库的那段「增量」长这样(markdown 模式,append 到节点末尾,不动既有正文):
markdown
## 更新(2026-07-08,cb66ff2)
> 来源:/kb-sync 增量同步,baseline=b407e3d,变更文件 3 个。
### 订单查询接口(api)
- `GET /orders` 新增 `pageSize`/`pageNo` 分页参数(`src/api/order.ts:42`)
- 新增 `DELETE /orders/:id` 软删除接口(`src/api/order.ts:78`)
锚定到这个标题 block 上的迭代评论(飞书评论区里能看到的那条):
json
[{"type":"text","text":"迭代总结(2026-07-08,cb66ff2)\n背景:订单列表数据量上来后一次全量拉取超时\n业务目的:支持分页 + 软删除,列表只取一页、删除不真删可追溯\n变更:订单查询补 pageSize 分页 + 新增删除订单接口"}]
两个细节体现「不写空话」:
- delta 每条都带
文件:行(src/api/order.ts:42)------能追回 diff hunk,不是 AI 凭空编的接口。这就是「溯源」防线。 - 评论三段(背景/业务目的/变更)从 diff 和 commit message 推断,让版本演进在知识库评论区一眼可见,而不是埋进正文被下次 extract 覆盖。
过段时间跑一次 /kb-extract,这些零散 append 的 delta 会被整篇重提炼收敛回 canonical 正文------sync 负责「高频小步记流水」,extract 负责「低频大步重整理」,分工明确。
六、写回知识库前,为什么必须让人点一下
工具自动往知识库写东西,最大的恐惧是写错、写泄密、写幻觉 。所以 sync 的三段里,第二段「确认门」是死纪律:草稿生成全自动,但写回前必弹「应用/编辑/放弃」,不点不写、不发评论、不更新基线。
这不止是安全措施,还顺带解决了一个现实问题:无头的 claude -p 自动化做不了「需要人判断」的事。delta 该不该进知识库、评论的「业务目的」归纳得对不对,这些不该 AI 拍板。所以分工是------脏活(扫 diff、提炼、脱敏、定位 block、发评论)交给 AI,落不落笔交给人。
四道防线放一起看:
| 防线 | 防什么 | 怎么做 |
|---|---|---|
| 确认门 | AI 擅自写回 | 草稿写前必弹确认,不点不写 |
| 溯源 | 幻觉编造 | delta 每条标 文件:行,评论背景基于 diff + commit message |
| 脱敏 | 机密上云 | 写前扫私钥/token/AKSK/JWT/长串密钥/身份证/手机号 → ***REDACTED*** |
| overwrite 兜底 | 增量累积漂移 | 周期 extract 整篇覆盖,把 sync 的增量幻觉收敛回正文 |
七、为什么先用 skill,不一开始就上 MCP
底层是 skill shell out 调 lark-cli,不是上来就 MCP。理由很务实:
- skill 是地基,MCP 是优化层。 先用 CLI 把四条工作流跑通、把真实调用模式摸清;等工具调用真成了瓶颈,再把 adapter 里的读写操作提成 MCP 工具。那时候因为有 adapter 接缝,各 skill 文本不动,只把「跑 CLI」换成「调 MCP 工具」。
- 提炼靠会话内 LLM,不需要外部 API key------成员装个 CLI + 装个插件就齐了。
- 最终交付是 plugin :skills + hooks 打包,
/plugin install一次装好,所有会话(含 hook 内部子会话)自动加载。团队分发走 git 仓库,两条命令装上:
bash
claude plugin marketplace add <git 仓库地址>
claude plugin install feishu-kb-tools@feishu-kb-tools
八、它带来什么成果
这套东西真正跑起来后,变化是具体的:
① 知识库从「死文档」变成「会自己更新的活缓存」
- 以前:写一份文档,三个月后和代码对不上,谁也不敢信。
- 现在:每次 commit 自动追加 delta + 评论,知识库跟着代码走。新人
/kb-read拉进会话的,就是当前真实状态,不是半年前的快照。
② 规范不再靠人记着遵守,提交时强制对齐
- 以前:规范写在飞书某节点,写代码时没人翻,等提了 PR 才被打回。
- 现在:commit 前自动拉规范查 staged diff,block 级违规直接拦在本地 commit,连 PR 都不用提。规范和代码还互相校准------代码里冒出的新模式会反向提示「该补这条规范了」。
③ 新人上手从「问一圈人」降到「一句话」
- 以前:clone 完仓库,搞清「这接口给谁调、这字段为啥这么存」得找老人问。
- 现在:
/kb-read+ 一句业务阐述,三步(业务阐述 → 索引匹配 → 注入)把对应业务上下文拉进会话,不用开口问人。
④ 沉淀变成提交流程的副产品,不是额外负担
- 这是闭环能持续的根本:没人需要「专门去更新文档」。开发照常写代码、照常 commit,知识库自动跟上。任何需要人额外付出意志力去维护的文档方案,最后都会荒废------这套之所以能活,是因为它把维护成本摊进了每次 commit,零额外动作。
一句话总结这套设计:映射管数据去向,索引管检索定位,接缝管底层屏蔽,四个 skill 管读写,三个 hook 管自动触发。五件事各司其职,拼进一个插件,commit 一下,闭环就自己转起来了------飞书知识库从此不用人养。