发布通道原来只有三条:PostgreSQL 关系行、Elasticsearch 分片、OSS 对象。 WeKnora 的 wiki 图谱是 wiki_pages 一张纯 PG 表(in_links/out_links 存 slug), 所以它不在任何通道里——撤权后生产 wiki 会一直留着已撤权文档的页面与引用。 本次新增第 5 条通道:库级 wiki 快照,挂在现有文档发布包里,检索与 wiki 同一个 release 单元(同一次执行、同一次校验、同一次回滚): - 导出:ExportWiki 按 knowledge_base_id 全量读 wiki_pages/wiki_folders; 收敛门禁(库内无解析中/合成中文档)不满足直接失败并给中文原因; 未启用 wiki 且无 wiki 行的库不参与同步(生产 wiki 保持原样)。 - 撤权:item.Action == "delete" 时在导出端剥离该文档的 wiki 贡献 (source_refs/chunk_refs 去条目、无引用页面整页不带、摘死链、清 parent_slug、 无页面引用的空文件夹及其祖先一并去掉),使“源端仍在只停止发布”和 “源端删除”两种情况收敛到同一结果。 - 导入:库级 replace-by-kb(同一事务内 DELETE by kb 后灌入快照,文件夹先于页面), 新增 ReplaceWiki 供撤权路径使用(文档行只 disable,wiki 整体替换)。 - 校验:VerifyWiki 用生产 WeKnora wiki API 比对页面 slug 集合,撤权时额外断言 生产 wiki 不再引用被撤权文档。 驱动与验收:新增 cmd/release-channel-e2e(复用发布 worker 真实代码路径, 支持 publish/withdraw/rollback),scripts/wiki_e2e_test5.sh、 scripts/wiki_e2e_newkb.sh 两套端到端脚本;证据见 docs/WIKI_CO_TRANSPORT_ACCEPTANCE.md。
10 KiB
wiki 一同搬运与撤权(文档级发布单元内的库级 wiki 快照)
状态:实现中(本文件随实现同步更新)
日期:2026-09-16
范围:internal/releasebundle、internal/releaseadapter/v071、internal/platform/weknora
1. 目标(主人的原话)
测试的 weknora 解析后上传到生产,如果我在来源与目录停止了该知识,后面的生产发布环境能把这条文档去掉,包括检索和 wiki。
OK 你帮我实现 wiki 一同搬运的需求。现在的知识库也得改成 wiki 知识库。然后用单文档进行测试, 导入加删除,再 5 个文档测试。新建新的知识库 wiki。
拆成四件事:
- 发布时:文档的检索(PG+ES+OSS)与该库的 wiki 页面一起进生产。
- 撤权时:文档从生产检索里去掉的同时,wiki 里这篇文档的贡献也去掉。
- 现有知识库开启 wiki(改成 wiki 知识库)。
- 验证顺序:单文档(导入+删除)→ 5 文档 → 新建 wiki 知识库。
2. 现状与缺口
现状(已取证):
- 发布通道三条:PG 6 表(knowledges/chunks/knowledge_tags/knowledge_tag_relations/resources/resource_bindings)+ ES chunks/vectors + OSS 对象。
- 撤权只做两件事:
UPDATE knowledges SET enable_status='disabled'+ ES delete-by-query。 wiki 页面原样留在生产。 - wiki 图谱不是 Neo4j:节点+边都在
wiki_pages一张表里(in_links/out_links存 slug 数组), 文件夹在wiki_folders。两者都是纯 PG,同 ID 可直搬,不需要重建、不需要 Neo4j。 internal/releasebundle/manifest.go的白名单已经含wiki_pages/wiki_folders, 但导出/导入/校验/回滚全链路零实现(只有那两行白名单)。
缺口 = 第 5 条通道:库级 wiki 快照。
3. 设计(已按主人此前批准的方向实现)
3.1 快照载体:挂在文档发布包里,不新增发布单元
wiki_pages + wiki_folders 作为两张**库级(按 knowledge_base_id 全量)**表,追加进现有文档 bundle 的
PostgresSnapshot.Tables。好处:
- manifest 形状不变(
Tables白名单本来就允许这两张表),发布计划/编排/前端零改动。 - 同一 release 单元:检索与 wiki 同一次执行、同一次校验、同一次回滚(决策④)。
- 语义可判:manifest 里有这两张表 = 该包带 wiki 快照;没有 = 老包/未启用 wiki(跳过 wiki 应用,不动生产 wiki)。
3.2 撤权时重新读源端 wiki(关键)
internal/workflows/release/sidecar_publisher.go 的 Delete 路径在 Restore(item,"absent") 之前
也会先 Export,并把新包归档到 item.ReleaseRevision(Prepare → Archive.Save)。
所以撤权时 Restore("absent") 加载到的是撤权那一刻新鲜导出的包,wiki 快照就是源端当前状态。
不需要新增编排步骤。
3.3 撤权时对 wiki 做"该文档贡献剥离"(确定性,无 LLM)
导出时若 item.Action == "delete":
source_refs(格式<knowledge_id>|<doc_title>)里剥掉该 knowledge 的条目;chunk_refs里剥掉该 knowledge 的 chunk UUID;- 剥完 refs 后没有任何来源的页面 → 整页不带走(等同源端"专属页删除");
- 索引页/日志页(无 refs)保留;
- 指向被丢弃 slug 的
in_links/out_links一并摘掉(避免生产死链),父页被丢弃时parent_slug清空。
为什么需要这层:源端"仅停用"不改 wiki(WeKnora 只在删除时跑 retract 清理链)。 剥离层让"停止发布"和"源端删除"两种情况收敛到同一个结果:生产 wiki 不再引用该文档。 源端真删除时,清理链已跑过,剥离是幂等空操作。
3.4 收敛门禁(决策①:等 + 拒出)
导出 wiki 前检查该库是否收敛:
- 库内没有
deleted_at IS NULL且pending_subtasks_count > 0的文档; - 库内没有
parse_status IN ('pending','processing','finalizing')的文档。 不收敛 → 导出失败并给出中文提示(不产出半成品 wiki 快照)。
3.5 生产端应用方式:库级 replace-by-kb(决策②③)
生产端在同一个事务里(沿用 BEGIN → schema_migrations=74 断言 → advisory lock → COMMIT):
DELETE FROM wiki_pages/wiki_folders WHERE tenant_id=$1 AND knowledge_base_id=$2 → 灌入快照行。
id/slug/in_links/out_links/folder_id 原样保留,行级 scope 校验(tenant_id + knowledge_base_id)。
每次发布都推整库快照(幂等)。
3.6 未启用 wiki 的库不参与同步
源端库 wiki_enabled=false 且 wiki 行数为 0 → 包内不带 wiki 表 → 生产 wiki 保持原样(不会被清空)。
4. 开发清单(代码)
| # | 文件 | 改动 |
|---|---|---|
| 1 | internal/releaseadapter/v071/wiki_export.go(新) |
PostgresExporter.ExportWiki:收敛门禁 + 按 kb 读 wiki_pages/wiki_folders + 撤权剥离 |
| 2 | internal/releaseadapter/v071/document_export.go |
validReleaseTable 放行 wiki 两表;导出时追加 wiki 表 |
| 3 | internal/releaseadapter/v071/postgres_import.go |
prepareSnapshot 校验 wiki 行 scope;Replace 加库级 DELETE;新增 ReplaceWiki(仅 wiki) |
| 4 | internal/releaseadapter/v071/document_target.go |
Restore("absent") 追加 wiki 应用;VerifyRestored 追加 wiki 校验 |
| 5 | internal/releaseadapter/v071/verifier.go |
wiki 校验:生产 wiki API 的页数 + slug 集合与快照一致 |
| 6 | internal/platform/weknora/client.go+types.go |
新增 ListWikiPages(GET /api/v1/knowledgebase/{kb}/wiki/pages,X-API-Key) |
5. 测试(每步都要运行证据)
| 步骤 | 内容 | 证据 |
|---|---|---|
| T0 | 单测 + vet + build | go test / vet / build 输出 |
| T1 | 新建 wiki 知识库(wiki_enabled=true)+ 导入 1 篇,等 wiki 合成完成 |
wiki_pages 行数 > 0 |
| T2 | 单文档发布到生产(模拟生产 = 测试服 weknora-test-v071) | 生产 PG/ES/OSS/wiki 四通道行数 |
| T3 | 源端删除该文档 → 等 wiki 收敛 → 撤权发布 | 生产该 knowledge enable_status='disabled';ES chunks=0;wiki 该文档页/引用消失 |
| T4 | 5 文档库:发布 → 删 1 篇并撤权 | 共贡页 refs 剥离、专属页消失、检索该篇=0 |
| T5 | 新建 wiki 知识库再跑一遍单文档闭环 | 同 T2/T3 |
6. 验收标准
- 发布后生产 wiki 页数/slug 集合 = 源端快照(含文件夹)。
- 撤权后生产检索无该文档(
enable_status='disabled'+ ES chunks=0)且 wiki 无该文档贡献。 - 回滚到上一版 revision 时,wiki 一并回到上一版快照。
- 未启用 wiki 的库发布不改变生产 wiki。
- 源端 wiki 未收敛时导出拒绝并给出中文提示,不产生半成品。
7. 决策默认值(主人未反对即按此;要改告诉我)
| 点 | 采用值 |
|---|---|
| ① 撤权是否等 wiki 收敛 | 等:不收敛则该次导出直接失败(阻断这次发布/撤权)并给中文原因,不产出半成品快照 |
| ② 覆盖语义 | 库级全量 replace(幂等、可回滚) |
| ③ 推送粒度 | 每次发布/撤权都推整库快照 |
| ④ 撤权顺序 | 检索与 wiki 同一 release 单元、同一事务链 |
8. 运行证据(2026-09-16,模拟生产 = 测试服 weknora-test-v071)
环境:源端 = 本机 WKNFull(KB c12ff4d5-…,wiki 打开);目标端 = 测试服
weknora-test-v071(RDS weknora_test + ES weknora_test_chunks + 生产桶
dobot-weknora-test)。三条 ssh 隧道(25432/29200/28080)→ 本机 prod-pg/prod-es/prod-weknora。
驱动:cmd/release-channel-e2e(复用发布 worker 的真实代码路径:
DocumentExporter → DocumentTarget Prepare/Activate/Verify / Restore+VerifyRestored)。
T1 单文档:导入 + 发布
- 源端:解析完成、wiki 子任务 0;
wiki_pages=7。 - 包内容:
knowledges=1 chunks=3 resources=1 resource_bindings=1 wiki_folders=5 wiki_pages=7。 - 结果:
STEP activate: ok/STEP verify: ok (document + wiki)。 - 生产 wiki:0 → 7 页(含
index、summary/<knowledge>、entity/weknora等)。
T2 单文档:撤权(源端文档仍在,只停止发布)
- 包内容:
wiki_pages=1(只有index存活,专属页被剥掉)——撤权剥离在导出端生效。 - 结果:
STEP restore absent: ok/STEP verify withdrawn: ok (retrieval disabled + wiki retracted)。 - 生产 wiki:7 → 1 页,且
refs_still_citing=0。 - 生产检索:
enable=disabled;ESis_enabled=true分片 = 0(is_enabled=false= 2)。 - 源端该文档
parse_status=completed, deleted_at=null→ 证明是本通道剥离了 wiki, 而不是源端清理链的副作用。
T3 五文档:发布 + 撤其中一篇
- 生产:
enabled=5 / total=5,wiki_pages=23,wiki_folders=5。 - 共贡页面真实存在:
entity/weknora被 5 篇同时引用;concept/publish-channel被 2 篇引用。 - 撤权 doc
6d8d622d…后:wiki_pages 23 → 20,引用被撤权文档的页面 = 0, 其余 4 篇仍enable=enabled,ES 启用分片 = 2/2/2/3(合计 9,与该库剩余文档一致)。
T4 新建 wiki 知识库:单文档导入 → 发布 → 撤权
脚本 scripts/wiki_e2e_newkb.sh:源/目标各克隆一个全新同 ID 知识库(wiki-e2e-fresh,开 wiki)。
- 导入后源端 wiki 收敛:
completed/0,wiki_pages=8。 - 发布包:
wiki_folders=7 wiki_pages=9;activate ok/verify ok;生产 wiki 0 → 9 页。 - 撤权后:该文档
enable=disabled;wiki_pages=1 wiki_folders=0(只剩index,空文件夹一并清除);refs_citing_withdrawn=0;ES 启用分片 = 0。
单元/集成验证
go build ./.../go vet ./...全绿;go test ./...全绿。- 新增单测:
wiki_export_test.go(收敛门禁、未启用库跳过、撤权剥离、文件夹剪枝、跨域拒绝)、wiki_channel_test.go(包内带 wiki、撤权传参、verifier 页集合与引用校验、撤权路径 ReplaceWiki+VerifyWiki)。
已知限制
- 首次发布失败做补偿(Compensate)时不会还原 wiki:该路径下没有上一版 wiki 快照可比对, 只把文档置为 disabled。回滚(rollback)不受影响——每个包都带整库快照。
- 页面里以对象存储定位符引用的图片仍走
resources通道,wiki 正文中的图片 URL 不重写。 - 生产端 wiki 页面数超过 1000 时校验会失败(防失控预算),需要时调大上限。
- 撤权后生产桶里的对象不删(回滚要用),属既有行为。