语雀文章转移 WorkBuddy 资料库
概览
把语雀知识库(公开或私有)全量迁移到 WorkBuddy 资料库,要求:
- 文字、图片完整
- 分组层级与语雀一致(含「文档下挂子文档」这种嵌套)
- 显示顺序与语雀一致
- 迁移后用机械对照证明一致,而不是靠肉眼
两条抓取路线,产物契约完全一致,后半段共用:
| 路线 | 适用 | 拿正文的方式 |
|---|---|---|
| A 公开库 | 匿名可读的知识库 | 在线抓语雀 Markdown 端点 |
| B 私有库 | 私有 / 需登录的知识库 | 用户导出 .lakebook,离线解析 |
何时使用
- 「把语雀的文章搬到资料库 / 迁移语雀知识库 / 语雀导出到工作台」
- 「资料库里图片显示为空」「文档是空的」「图片没搬过来」
- 「目录结构 / 顺序跟语雀不一样」「有些文档没放进子文件夹」
核心概念:tree.json 是结构真源
语雀的目录不是扁平分组,而是树:
TITLE节点 → 分组DOC节点 → 文档DOC节点也可能挂子文档 → 语雀允许「文档嵌套文档」
第三条最容易被忽略。若按「最近的 TITLE 祖先」把文档拍平成一层,
这些子文档就会散落到父级——层级丢了,顺序也乱了。
所以 lakebook_adapter.py 产出两个清单,职责分开:
| 文件 | 内容 | 用途 |
|---|---|---|
docs.json |
扁平清单(title/slug/group) | 图片转存、生成导入稿 |
tree.json |
嵌套树(层级 + 顺序) | 建结构、对照验收 |
容器文档(有子文档的文档)在 tree.json 中表示为:
{"kind": "folder", "title": "泛微:AI+UI自动化测试", "containerDoc": true,
"children": [
{"kind": "doc", "title": "泛微:AI+UI自动化测试", "slug": "zy5gi58bpauxc2fl"},
{"kind": "doc", "title": "AI自动化测试介绍", "slug": "..."}
]}
即:同名文件夹 + 第一个子节点是它自己。这样既保留文档正文,又能表达它下面挂了东西。
流程总览
preflight → 取正文(A 或 B)→ images → build → push → layout_sync → verify → compare → report
顺序不可颠倒:图片要先转存;结构要在写入后单独摆正;最后必须内容校验 + 结构对照双验收。
第一步:前置环境检查(必须)
python scripts/preflight.py --node-id <目标文件夹节点ID> --book-url <语雀知识库地址>
| # | 检查 | 失败含义 | 修复方向 |
|---|---|---|---|
| 1 | Python ≥ 3.8 | 解释器过旧 | 升级 Python |
| 2 | 工作目录可写 | 路径无权限 | 用 --workdir 换目录 |
| 3 | 资料库技能目录 + 关键脚本齐全 | WorkBuddy 未装 / 版本过旧 | 设 WORKBUDDY_LIB_DIR 指向 .../skills/library |
| 4 | 资料库 API 连通(只读探测目标节点) | 未登录 / 网络不通 | 登录 WorkBuddy |
| 5 | 目标节点存在且为 folder |
传了文档节点 ID | 换文件夹地址 /space/d/<id> |
| 6 | 语雀库可读性 → 路线判定 | — | 404 即私有库,自动建议走 B |
| 7 | lakebook 转换脚本存在 | 缺「Lakebook 转 Markdown」技能 | 安装该技能或设 LAKEBOOK2MD |
| 8 | 图片 CDN(cdn.nlark.com)可达 | 图片转存会全失败 | 放通网络,或用本地下载模式 |
加 --json 返回 {ok, route, libraryDir, target, checks[]}。
第二步:确认用户输入(缺一不可)
| 输入 | 说明 | 从哪来 |
|---|---|---|
| 语雀知识库地址 | https://www.yuque.com/<用户名>/<知识库slug> |
用户提供;必须是知识库首页 |
| 资料库文件夹地址 | https://www.workbuddy.cn/space/d/<节点ID> |
用户提供;末段即 --node-id |
.lakebook 路径 |
仅路线 B | 用户导出的本地文件 |
python scripts/library_import.py --node-id <节点ID> # 回显 title/kind/spaceId
路线 A:公开知识库(在线抓取)
python scripts/yuque_export.py --book-url <语雀地址> --out ./yuque_export --list-only
python scripts/yuque_export.py --book-url <语雀地址> --out ./yuque_export --delay 2
产物:docs.json(扁平)+ tree.json(嵌套结构)+ md/<slug>.md。
--list-only 也会写 tree.json,可以先看结构再决定是否下载。
目录解析从页面内嵌的 URL 编码 JSON 里取 toc 数组:
type 为 DOC 是文档、TITLE 是分组,文档 slug 存在 url 字段里(不是 slug)。
build_tree() 会按 parent_uuid 还原嵌套,并处理「文档下挂子文档」。
路线 B:私有知识库(.lakebook)
判定依据:匿名访问返回 404(对照公开库 200)。这不是链接写错。
登录态下点「分享」,弹层会写「当前知识库为私密,仅自己和协作者可访问」。
B1. 引导用户导出
> 语雀网页端不提供整库 Markdown 导出(设置页「导出知识库」只有 PDF / .lakebook)。
> 请在该知识库页面点右上角「更多 / ···」→「导出」→ 语雀知识库 (.lakebook),
> 下载完成后把文件路径发我。
B2. 转换
python scripts/lakebook_adapter.py --lakebook "<路径>/AI.lakebook" --workdir ./yuque_export
做四件事:读 $meta.json(.lakebook 是未压缩 tar,非 zip)→ 解析 book.tocYml
得到嵌套树 → 调 lakebook2md.py 转正文(默认 --no-download,保留 CDN 外链)→
按 slug 对齐、剥离 frontmatter,输出 docs.json + tree.json + md/。
分组必须自解析 tocYml:lakebook2md.py 面向 Obsidian,按 level 建目录,
多层知识库还原不全(实测 9 个分组只建出 2 个目录,且把「文档下挂子文档」建成了普通子目录)。
tocYml 里 type: TITLE|DOC + parent_uuid 才是权威结构。
第三步:导入正文(两条路线共用)
# 1) 图片转存(外链 → 资料库托管链接)
python scripts/library_import.py --export-dir ./yuque_export --step images --workers 4
# 2) 生成导入稿(替换图片链接 + 规范化图片块)
python scripts/library_import.py --export-dir ./yuque_export --step build
# 3) 写入文档(按 tree.json 递归建目录 + 写文档;断点续传)
python scripts/library_import.py --export-dir ./yuque_export --step push \
--space-id <spaceId> --parent-id <节点ID>
> push 建议后台执行:76 篇约 5 分钟,前台可能被会话超时打断(SIGTERM)。
> 中断不丢进度,push_state.json 记录已建目录/已写文档,重跑自动跳过。
幂等:同名文档默认跳过;要覆盖重写加 --overwrite(修复空壳/缺图的标准姿势)。
建目录必须现场查重:push_state.json 只记录「上次创建过什么」,一旦状态里缺了某个
路径键(例如老版本只存分组名、没有嵌套路径),就会把同名目录再建一遍,产生无法删除的重复目录。
所以 push 在创建前会 list 一次父级子节点,同名就复用;layout_sync.py 同理。
只信状态、不信现场是这类重复目录的唯一来源。
第四步:摆正结构(必须单独一步)
python scripts/layout_sync.py --export-dir ./yuque_export \
--node-id <节点ID> --space-id <spaceId>
它按 tree.json 深度优先做三件事:
- 补齐缺失目录(含容器文档的「同名文件夹」)
- 把归属错误的节点搬到正确父级
- 每个容器内按语雀顺序重排
为什么必须独立于 push:资料库 move-node 不带锚点时是「前置插入」,
逐个搬会把顺序整体反转;而顺序要靠 --after-node-id 精确锚定。
把「写正文」和「摆位置」拆开,两边各自幂等,可以反复跑。
先看计划再执行(推荐):
python scripts/layout_sync.py ... --dry-run # 只播报
第五步:双验收(必须)
5.1 内容校验
python scripts/library_import.py --export-dir ./yuque_export --step verify
逐篇回读正文,比对「源图数 / 库图数 / 正文字符数」,结果落盘 verify.json。
create_doc.py 返回 HTTP_524 时节点可能已建但正文为空壳,必须靠这一步发现。
5.2 结构对照(1:1)
python scripts/compare.py --export-dir ./yuque_export \
--node-id <节点ID> --space-id <spaceId>
按 tree.json 递归比对四个维度,结果落盘 compare.json:
| 维度 | 检查内容 |
|---|---|
| 存在 | 源有的目录/文档目标都有;目标无多余节点 |
| 归属 | 每个节点是否挂在正确父容器下 |
| 顺序 | 每层「文件夹与文档混排」的显示顺序 |
| 层级 | 嵌套深度是否一致 |
全部一致才输出 结论: 完全一致 ✓。
> 只做 verify 不做 compare 是常见的漏检:verify 按节点 ID 回读正文,
> 不校验父节点、也不校验顺序——文档放错位置它照样全绿。
第六步:结果报告
python scripts/report.py --export-dir ./yuque_export \
--node-id <节点ID> --space-id <spaceId>
默认写 <export-dir>/report.md,--json 同时输出机读摘要。六节:
概览 / 分组明细 / 逐篇校验 / 结构与顺序对照 / 异常与遗留 / 复核建议。
回复用户时必须包含:结论一句话、已写入 X/Y 篇、目录 N 个、图片 M/K 张、
结构对照是否一致、目标文件夹链接、异常清单(若有);报告全文作为附件。
七个必须遵守的约束
约束 1:图片必须先转存为资料库托管链接
直接把外链写进资料库,正文会显示为空。
python "$LIB/manage/upload_image.py" --url "<图片URL>" # 外链模式
python "$LIB/manage/upload_image.py" "<本地图片路径>" # 本地模式(≤10 MiB)
# KS_IMAGE_UPLOAD_OK {"url":"https://workbuddy-space-static.codebuddy.work/image/xxx.png", ...}
输出是 KS_IMAGE_UPLOAD_OK 紧跟一个空格再接 JSON,用
line[len('KS_IMAGE_UPLOAD_OK'):].strip() 解析,不要 split('\t')。
约束 2:每张图片必须独占一行,且与相邻文字留空行
### 标题 ← 图片丢失
1. 列表项
 ← 图片丢失
比较查询 ← 文字被吞!
--step build 已双向规范化。兜底扫描(应无输出):
grep -nE '!\[[^]]*\]\([^)]+\)[^ )\s]' <导入稿目录>/*.md
约束 3:写入后必须回读校验正文
HTTP_524 会留下「有节点、空正文」的空壳。修复姿势是整篇覆盖:
python "$LIB/doc/create_doc.py" \
--node-block-id <节点ID> --space-id <spaceId> \
--content-file <导入稿.md> --confirm-overwrite --title "<标题>"
期望图片数要「剥离行内代码」后再统计:作者写在反引号里的示例语法
`` 不会成为图片;而失效相对引用
 仍会在资料库生成 <Image> 标签,必须计入。
verify 已按此实现(先 re.sub(r'[^\n]*', '', text)` 再计数)。
约束 4:资料库回执是「多字段制表符行」,只能取第一个字段
KS_FOLDER_CREATE <nodeId> <kind> <url>
KS_DOC_CREATE <nodeId> <kind> <url> <failedCount> <fatalCount>
整行当 ID 会得到 "29ihp...\tfolder\thttps://...",传回去报
parent_id 格式非法:请传纯 ID,不要传 URL。必须 split('\t')[0]。
约束 5:每条写入都必须显式指定 --parent-id(含根级文档)
只在「有分组」时才传 --parent-id,源库的根级文档会被建到空间根目录
(parentId == spaceId),散落到用户空间顶层;而 verify 依然全绿。
parent = folders.get(group) or parent_id # 无分组回落到目标文件夹
cmd += ["--parent-id", parent] # 无条件追加
验收:列一次目标文件夹的真实子节点,断言
直接子节点中 doc 数 + 各子目录内 doc 数 == 源文档总数。
约束 6:结构必须按 tree.json 落位,不能拍平
语雀的「文档下挂子文档」必须建成「同名文件夹 + 容器文档」。
拍平会把子文档提升到父级,层级与顺序同时出错,且 verify 不会报错。
判定方法:tocYml 中某 DOC 节点的 parent_uuid 指向另一个 DOC → 存在嵌套。
lakebook_adapter.py 已按此生成 tree.json。
约束 7:顺序只能用 --after-node-id 锚定
move-node 不带锚点时是前置插入:按 A→B→C 顺序逐个搬,最终得到 C→B→A。
重排一个容器的正确做法是:按期望顺序遍历,每个节点移动时把前一个兄弟作为锚点,
首个节点不带锚点(置顶):
prev = ""
for node in desired:
space.move(node.id, parent_id, after=prev)
prev = node.id
layout_sync.py 已按此实现,且幂等——顺序正确时不会产生任何移动。
故障处置速查
| 编号 | 现象 | 根因 | 处置 |
|---|---|---|---|
| F1 | 正文显示为空 / 图片全无 | 图片仍是外链 | --step images 后 --step push --overwrite |
| F2 | 只有标题,正文空 | HTTP_524 空壳 | verify 找出 → --node-block-id 覆盖重写 |
| F3 | 个别图片缺失 | 图片与标题/列表文字同行 | --step build 重新生成 → 覆盖重写该篇 |
| F4 | 批量 HTTP 000 | 本机代理限流 | 串行 + 间隔;--delay 3,调小 --workers |
| F5 | 语雀目录解析失败 | 未公开或链接指向单篇 | 确认 404 → 改走路线 B |
| F6 | 图片可达性大量失败 | 用了并发 HEAD | 改串行 GET |
| F7 | parent_id 格式非法 |
回执多字段被整行当 ID | 升级脚本;push_state.json 会自愈 |
| F8 | push 中途进程消失 | 前台会话超时 SIGTERM | 后台重跑,断点续传 |
| F9 | lakebook2md.py 执行失败 |
文件不完整 / 后缀被改 | 校验 tar 文件头,重新导出 |
| F10 | 目录层级比语雀少 | 转换器按 level 建目录的局限 | 用 lakebook_adapter.py 的 tocYml 解析 |
| F11 | 文档总数对但目标里找不到 | 根级文档建到了空间根 | layout_sync.py 搬回;已修为无条件传 --parent-id |
| F12 | verify 报「源图 N / 库图 0」但该篇没图 | 图片语法写在行内代码里 | 已内置剥离行内代码 |
| F13 | 顺序整体颠倒 | move-node 无锚点是前置插入 |
用 --after-node-id 链式定位(约束 7) |
| F14 | 子文档跑到了父级 | 文档下挂子文档被拍平 | 重建 tree.json → layout_sync.py(约束 6) |
| F15 | 建了空目录无法删除 | 资料库无删除节点接口 | 只能用户手动删;验证环境请用 preflight.py 只读探测 |
| F16 | 出现同名重复目录 | 建目录时只信 push_state,没查现场 |
已修为「先 list 父级、同名复用」;重复目录改名加 【待删除】 前缀,用户手动删 |
| F17 | 结构对照把遗留目录算成错误 | 无法删除的目录会一直存在 | 以 【待删除】 开头的节点被 compare.py 识别为遗留,单列不判错 |
F4 补充:代理限流会造成大量假失败。判别——单张或间隔 5 秒的请求若 200,即限流而非资源不可用。
图片 URL 比对的正确方式
资料库会重新托管图片,库中 URL 与原文必然不同。只能按:
- 数量:源
次数 vs 库中<Image src=个数 - 位置:按前后文字判断图片是否在正确段落
两个不要做
- 不要用
create_folder.py造测试目录:资料库没有删除节点接口
(只有rename-node/move-node/export-node),造出来只能用户手动删。
验证环境用preflight.py的只读探测。 - 不要试图用浏览器登录态抓私有库:Chrome 启用 App-Bound Encryption 后
Cookie 为 v20 加密、外部进程无法解密,且 Cookies 库被浏览器独占锁定;
CUA 通道打开下拉菜单 / 滚动 / 地址栏输入均不可靠。私有库就走.lakebook。
目录结构
语雀文章转移workBuddy资料库/
├── SKILL.md
├── scripts/
│ ├── preflight.py # 前置环境检查 + 路线判定
│ ├── common_env.py # 定位 library 目录 / lakebook2md / Python 版本
│ ├── yuque_export.py # 路线 A:公开库在线抓取
│ ├── lakebook_adapter.py # 路线 B:.lakebook → docs.json + tree.json + md/
│ ├── library_import.py # 图片转存 / 导入稿 / 写入 / 内容校验
│ ├── layout_sync.py # 结构同步(建目录 / 搬节点 / 排顺序)
│ ├── compare.py # 源↔目标 1:1 结构对照
│ └── report.py # 结果报告
└── references/
└── pitfalls.md # 完整踩坑实录与排查细节
环境变量
| 变量 | 用途 | 默认 |
|---|---|---|
WORKBUDDY_LIB_DIR |
指定资料库技能目录 | 自动搜索 |
LAKEBOOK2MD |
指定 lakebook2md.py 路径 | 自动搜索 |
common_env.find_library_dir() 顺序:环境变量 → 常见安装路径
(D:/Program Files/WorkBuddy、C:/Program Files/WorkBuddy、
~/AppData/Local/Programs/WorkBuddy、/Applications/WorkBuddy.app/...)→ 浅层搜索。
换一台机器不需要改脚本。
已知限制
- 私密知识库必须由用户导出
.lakebook;本技能不尝试任何绕过登录的手段。 - 语雀网页端整库导出只提供 PDF / .lakebook(2026-10 实测),没有整库 Markdown。
- 语雀内联样式(
<font style>颜色/字号)不保留,仅保留结构与文字。 - 语雀画板 / 思维导图 / 附件 / LaTeX 公式源码无法还原(导出本身即不含)。
- 原文写成相对路径的图片(
)在语雀端已失效,迁移后同样为空,
报告中单列为「原文失效图片引用」。 - 语雀端本身就是空白的文档会输出
_(空文档)_占位(本次 76 篇里有 3 篇),
迁移后仍是空文档,属正常,不算失败。 - 资料库无「删除节点」接口,冗余目录需用户手动清理。