Files
knowledge-governance/docs/WIKI_CO_TRANSPORT_ACCEPTANCE.md
姚章浩 ea6e56b304 feat(release): 文档发布会带上知识库 wiki 快照并支持撤权剥离
发布通道原来只有三条: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。
2026-09-30 10:54:07 +08:00

10 KiB
Raw Permalink Blame History

wiki 一同搬运与撤权(文档级发布单元内的库级 wiki 快照)

状态:实现中(本文件随实现同步更新) 日期:2026-09-16 范围:internal/releasebundle、internal/releaseadapter/v071、internal/platform/weknora

1. 目标(主人的原话)

测试的 weknora 解析后上传到生产,如果我在来源与目录停止了该知识,后面的生产发布环境能把这条文档去掉,包括检索和 wiki。

OK 你帮我实现 wiki 一同搬运的需求。现在的知识库也得改成 wiki 知识库。然后用单文档进行测试, 导入加删除,再 5 个文档测试。新建新的知识库 wiki。

拆成四件事:

  1. 发布时:文档的检索(PG+ES+OSS)与该库的 wiki 页面一起进生产。
  2. 撤权时:文档从生产检索里去掉的同时,wiki 里这篇文档的贡献也去掉。
  3. 现有知识库开启 wiki(改成 wiki 知识库)。
  4. 验证顺序:单文档(导入+删除)→ 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. 验收标准

  1. 发布后生产 wiki 页数/slug 集合 = 源端快照(含文件夹)。
  2. 撤权后生产检索无该文档(enable_status='disabled' + ES chunks=0)且 wiki 无该文档贡献。
  3. 回滚到上一版 revision 时,wiki 一并回到上一版快照。
  4. 未启用 wiki 的库发布不改变生产 wiki。
  5. 源端 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;ES is_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)。

已知限制

  1. 首次发布失败做补偿(Compensate)时不会还原 wiki:该路径下没有上一版 wiki 快照可比对, 只把文档置为 disabled。回滚(rollback)不受影响——每个包都带整库快照。
  2. 页面里以对象存储定位符引用的图片仍走 resources 通道,wiki 正文中的图片 URL 不重写。
  3. 生产端 wiki 页面数超过 1000 时校验会失败(防失控预算),需要时调大上限。
  4. 撤权后生产桶里的对象不删(回滚要用),属既有行为。