新增 knowledge/market-install-downstream-materialization.md(market↔knowledge 接缝单一归属):安装"空账"物化断层(两断点)、真越权澄清(安装者读发布者私有≠安装者间)、D0-fork 四单元闭环、三关键衔接(进程内 Spring 事件 AFTER_COMMIT/MarketAssetSourceApi 跨 BC 读 tags/kb_id 去污染)、开放项独立成节。external-deps 补 RAGFlow 两坑(无 copy API、metadata_filter vs metadata_condition 字段错配静默忽略)。security-and-reliability §三.A 补 market 物化 3 红线(本地键不存外域 id/私有不泄露须物理隔离非运行时过滤/forkStatus fail-closed)+§六门禁表诚实标 kb_id 去污染暂无独立 ArchUnit。module-reality-baseline 加薄指针;README 索引更新。AgentsInfraIntegrityTest 3/3 绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
9.5 KiB
Markdown
56 lines
9.5 KiB
Markdown
# knowledge:市场安装下游物化 + D0-fork 知识库隔离
|
|
|
|
> **类型**:事实与蓝图(knowledge) · **范围**:market 安装如何把市场资产物化成下游可用实体,以及知识库类型资产的私有内容隔离方案 · **状态**:KB 物化 D0-fork 已实现并真验收(2026-06-26),agent / 副本回收 / 召回回滚等为开放项
|
|
> **关联红线**:[`../rules/security-and-reliability.md`](../rules/security-and-reliability.md)(私有不泄露须物理隔离、forkStatus fail-closed、kb_id 不存 assetId)、[`../rules/bc-boundaries.md`](../rules/bc-boundaries.md)(跨域经 -api 端口)
|
|
|
|
本文件解释一个长期被忽视的事实:**市场"安装"在很长一段时间里只是一笔账,并不真正让安装的东西能用**;以及为知识库类型资产补上这条下游链路时,如何顺带堵住一个真实的越权口子。它是 market 与 knowledge 之间这道接缝的单一归属文档,其他地方只做指针引用。
|
|
|
|
## 一、安装曾经是一笔"空账":物化断层
|
|
|
|
market 的安装流程原本只写两样东西——`muse_market_installation` 安装记录和 license 授权投影——然后就结束了。它和 ai、knowledge 这些真正承载内容的模块完全解耦(安装侧代码与 pom 都不依赖它们,双向都验证过)。换句话说,用户在市场点了"安装",系统记下了"这个人有权用这个资产",却没有在任何下游模块里把资产变成可检索、可生成、可引用的真实实体。这就是"物化断层"。
|
|
|
|
这个断层在知识库资产上表现为两个具体断点:
|
|
|
|
第一个是**知识库断点**。安装一个 KB 类型资产后,本地没有对应的 `muse_knowledge_base` 行,也没有可检索的 RAGFlow dataset。于是当用户把它绑到作品、发起检索时,检索链路走到第四道门 `selectActiveDatasetByKbId` 拿不到 dataset,这个来源就被静默地以 `no_dataset` 理由省略掉了——检索看起来正常返回,实际上这个知识库从未参与。更隐蔽的是,绑定时把 `binding.kb_id` 写成了市场资产 id 而不是本地知识库主键,导致这个字段从源头就被污染,后续任何按 kbId 的查询都注定落空。
|
|
|
|
第二个是**agent 断点**。安装 agent 类资产后,虽然授权槽位放宽了,但 AI 运行时的 `requireVisibleAgent` 仍会拒绝裸引用一个没有本地实体的 agent。本期只解决了知识库断点,agent 断点留作后续。
|
|
|
|
## 二、真正的越权:不是安装者之间,而是安装者读到发布者私有内容
|
|
|
|
补这条链路时澄清了一个关键的、容易被误判的安全问题。
|
|
|
|
它**不是**"安装者之间互相越权"。在只读共享的语义下,所有安装者看的本就是同一份发布者发布的内容,大家看到全部公开内容是符合预期的,没有问题。
|
|
|
|
真正的越权是**安装者读到了发布者的私有内容**。原因在于知识库的生命周期在上架后并不冻结:发布者把整个 KB 上架到市场后,仍然可以继续往自己这个 KB 里上传新文档,而上传动作没有任何市场门禁。如果安装者直接共享发布者那一份**活的** dataset,那么发布者上架之后才加进去的私有文档,会被整库检索读到——安装者于是看到了从未被发布的私有 chunk。租户拦截器在这里拦不住,因为安装者读的就是发布者那一份 dataset 本身。
|
|
|
|
## 三、D0-fork 解法:上架即 fork 一份只含公开快照的专用副本
|
|
|
|
解决思路是在**上架那一刻**,由发布侧 fork 出一份专用 dataset,只包含上架审核时刻处于可检索状态的公开文档快照。安装者只读这份公开副本,与发布者的活 dataset 物理隔离。这样发布者事后往自己 KB 加多少私有内容都进不了副本,越权从源头被斩断。
|
|
|
|
这个方案有几个刻意的取舍:副本对每个被上架的资产版本只有一份(不是每个安装者一份),多个安装者共享同一份只读副本;它**不依赖运行时按 chunk 过滤**(后述 metadata 字段有 bug,刻意不踩),而是靠"副本物理上就只有公开内容"这一事实来保证隔离,因此检索侧零改动。
|
|
|
|
整套机制由四个单元构成,已全部实现并通过真 PG + 真 RAGFlow 的集成测试验收。
|
|
|
|
**U-fork(发布侧 fork 公开副本)**。知识库上架(market 侧 `markListed`)时,在已提交的事务里发一个进程内 Spring 事件;knowledge 侧的消费者以 `@TransactionalEventListener(AFTER_COMMIT)` + `@Async` 异步接收,调用 `KnowledgeMarketForkService` 执行 fork:建一个新 dataset,逐篇回读公开文档内容、上传、触发重索引。RAGFlow 没有 copy API,所以 fork 必须重走"建库 + 逐文档上传 + 重新解析索引"这条完整路径,不能共享发布者的索引。整个 fork 是一个多次外部调用的异步长任务,按外部交互铁律处理了幂等(键是资产 id + 版本,已 ready 则整体跳过)、部分成功(只补未成功的文档差集)、失败分类(临时故障可重试、校验类不可重试)、以及一个 `@Scheduled` 兜底 worker 扫描中间态补做。fork 的成败**绝不**反向影响 market 的上架审核——审核在自己的事务里落完上架事件就返回,fork 是 knowledge 自治的后续异步活。副本 dataset id 和 fork 状态机(pending→partial→ready/failed)存在 knowledge 自有 binding 表的 summary 里(不加 DDL),并写回 market 资产的 tags 供安装侧读出。
|
|
|
|
**U-materialize(安装侧物化)**。用户把市场 KB 绑到作品时,预检阶段经 `MarketAssetSourceApi` 这个 market 对外读端口,跨 BC 读资产来源 + 公开副本状态(经 -api 端口,不直读 market 的 dal,这是 BC 边界硬约束)。这里做三层 fail-closed 校验:资产必须存在、必须是 knowledge_base 类型、且 `forkStatus` 必须为 `ready`——副本还在 fork 中(pending/partial/failed)一律拒绝绑定,让用户稍后重试,绝不建出一个"绑了但检索不到"的迷惑实体。校验通过后把物化所需信息固化进预检快照,在用户确认绑定的写事实点,把公开副本物化成一个本地 `installed_ref` 类型的知识库实体(它引用市场副本、自身不持有文档,与 user/global KB 区分),并建一条指向副本 dataset 的绑定。这里同时**去污染**:`binding.kb_id` 一律落本地新建的知识库主键,不再是市场资产 id。物化对同一安装者同一资产幂等(命中既有 installed_ref KB 则复用);多个安装者各建自己的本地 KB 行,但它们的 dataset 绑定都指向同一份公开副本。
|
|
|
|
**U-retrieve(检索打通)**。检索 API 实现**零改动**。前两个单元做完之后,本地有了 installed_ref KB、有了指向公开副本的 dataset 绑定,检索走到第四道门 `selectActiveDatasetByKbId` 自然命中,原先的 `no_dataset` 静默省略消失。因为副本物理上只含公开内容,整库检索即纯公开内容,无需任何运行时隔离。
|
|
|
|
**U-verify(端到端真验收)**。两个集成测试在真 PG + 真 RAGFlow 上验证完整链路:上架 fork → 安装物化 → 检索命中,且发布者上架后才加的私有探针文档**检索不到**(隔离成立);另一个用例用真 Spring 上下文 + 真事务验证 AFTER_COMMIT 时序——事务提交才触发 fork、事务回滚则不投递事件,把 fork 接线从 mock 单测层抬到真事务层。
|
|
|
|
## 四、几条关键的架构衔接事实
|
|
|
|
**为什么用进程内 Spring 事件做跨模块通知**。muse 的业务模块此前没有用过 Spring ApplicationEvent 的先例;项目里那套统一的 Events 机制是给前端 SSE 用的、后端没有消费方。所以这次跨模块从 market 通知 knowledge,选了进程内 Spring 事件这条路(由人类拍板)。配合 AFTER_COMMIT,保证只有上架真正提交后才触发 fork。
|
|
|
|
**跨 BC 读经 -api 端口、读写分离**。安装侧读 market 资产来源走 `MarketAssetSourceApi`(读端口),发布侧把 fork 状态写回 market 走 `MarketAssetForkApi`(写端口)。两者调用方、方向、生命周期都不同,按读写分离拆成两个窄端口,避免两类消费者互相依赖对方用不到的方法。两个端口都是本地进程内 Bean、不挂 Feign(market 与各 owner 同进程聚合)。knowledge 全程不碰 market 的 dal。
|
|
|
|
**kb_id 去污染是检索能否命中的命门**。市场 KB 绑定请求里的 sourceId 是市场资产 id,不是本地 kbId。一旦把它当主键写进 binding.kb_id,后续按 kbId 的检索查询永远落空、表现为静默 no_dataset。所以 user/global KB 沿用 sourceId 即本地主键,market KB 则在预检阶段留空、在物化后回填本地新建的 kbId。这条已上升为红线(见 security-and-reliability.md)。
|
|
|
|
## 五、诚实遗留(开放项,未闭环)
|
|
|
|
- **召回回滚未触达 installed_ref**。market 召回资产时只写了源状态 blocked 的传播记录,但 knowledge 侧没有召回的消费者,已物化的 installed_ref KB 和副本在召回后不会被回收或停用。这是一个开放项,需要补 knowledge 侧对召回/下架事件的消费。
|
|
- **安装侧物化的端到端 e2e 待补**。现有 IT 覆盖了后端链路,但前端 global-setup 的物化 e2e fixture 还没补齐。
|
|
- **副本资源回收待做**。资产被删/版本更替后,旧的公开副本 dataset 没有清理路径。
|
|
- **metadata_filter 字段 bug 待修(已知、刻意未依赖)**。代码里发的是 `metadata_filter`,而 RAGFlow 官方契约是 `metadata_condition`,字段名不匹配被 RAGFlow 静默忽略。D0-fork 刻意不依赖运行时 metadata 过滤(改用物理隔离),所以这个 bug 目前潜伏不影响隔离;但它仍是一个待修的真实缺陷。详见 [`external-deps-and-gotchas.md`](external-deps-and-gotchas.md) 的 RAGFlow 集成坑。
|