oh-my-muse/design-docs/临时-04-market-KB物化-D0fork执行plan.md
lili c12eb013cd docs(market): D0-fork KB 物化执行 plan(临时-04、supersede 临时-03)
D1 澄清坐实真越权=安装者读到发布者私有内容(非安装者间):market 对 knowledge/ragflow 零依赖(src+pom 双空 spot-check)→上架不 fork、安装者共享发布者活 dataset、上架后 KB 仍可加私有文档(上传无 market 门禁)。用户拍 D0-fork(发布侧 fork 纯公开副本、物理隔离)。

临时-04 四单元:U-fork(发布侧 fork 公开副本核心新工程:上架事件→knowledge 异步 createDataset+逐文档 uploadDocuments+重索引,RAGFlow 无 copy API 必重走三步 spot-check 确认)/U-materialize(安装侧物化去隔离、binding 指公开副本)/U-retrieve(检索去隔离)/U-verify(发布者私有不泄露、复用临时-03 U0 反向基线)。关键推荐:fork 时机=上架 markListed(副本=公开快照语义)、接线=事件驱动(复用 market EventPublishOutbox+ADR-017、不绑架审核事务、保 market 不反依赖 knowledge)、DDL=初版不需(V5/V14 列+JSONB)。临时-03 标 superseded 保留演进史、U0 证据+四硬事实继承。待拍 D4-D10 后执行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:53:19 -07:00

398 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 临时-04 · Market KB 物化 D0-fork发布侧 fork 纯公开副本)执行 Plan
- 版本v1执行版待人类 review/拍板后执行;本稿只读出文档,不改任何代码)
- 更新日期2026-06-26
- 目标读者:后端 / 架构 / PR reviewer
- 阅读时间3040 分钟
- 文档性质:**临时件**(执行依据,落定后归并入正式分册或删除,不作长期 SSOT。本稿只给"怎么做",不重定义概念:方案演进与 C/B'/暂缓权衡见 [`临时-02-market-install下游物化方案.md`](临时-02-market-install下游物化方案.md);上一版(共享发布者活 dataset + 运行时 chunk 隔离)见 [`临时-03-market-install-KB物化执行plan.md`](临时-03-market-install-KB物化执行plan.md)**已被本稿 supersede**,保留作决策演进史);术语与 owner 边界以 [`架构-02-核心数据结构与双轨模型.md`](架构-02-核心数据结构与双轨模型.md) 为准§6 市场资产、§11.2 绑定安装授权、授权快照"引用对象必须保存快照 id");表结构归 `后端-04`;检索合同归 [`专题-03`](专题-03-AI编排上下文与质量评测实现规范.md) §5授权快照字符串承载归 ADR-020Source 传播事件驱动归 ADR-017BC 边界规则归 [`.agents/rules/bc-boundaries.md`](../.agents/rules/bc-boundaries.md)。
- 配套人读图:[`临时-04-market-KB物化-D0fork执行plan.html`](临时-04-market-KB物化-D0fork执行plan.html)
> **一句话**:拍板已定——做 **D0-fork**:知识库上架时,在**发布侧** fork 出一份**只含公开文档的专用 dataset**"公开副本"),所有安装者只读共享这一份副本。这从物理上把发布者的私有内容隔在副本之外,从根上消除"安装者读到发布者私有内容"的越权,**不需要运行时 chunk 隔离、不依赖那个 `metadata_filter` 字段 bug**。代价是新增一块发布侧的 fork 工程(建副本 dataset + 逐文档复制 + 重索引,异步重活),并要打破"market 对 knowledge 零依赖"的现状接线。本 plan 把 D0-fork 拆成 **U-fork / U-materialize / U-retrieve / U-verify 四个可独立验证的实现单元**。
>
> **越权真因D1 澄清坐实,区别于临时-03 的假设)**:临时-03 当时把越权理解为"多租户共享同一 dataset → 安装者**之间**互读",方向定在"共享前补 chunk 级隔离"。D1 澄清重新坐实了更要命的一条:真越权是**安装者读到发布者的私有内容**。链条是——market 对 knowledge/ragflow **零依赖**(上架审核 `markListed` 只翻 market 自有资产状态,从不 fork 任何 dataset→ 安装者绑定后若走"共享"必然指向**发布者那一份活 dataset**→ 而发布者**上架后仍可往自己 KB 继续加私有文档**whole-KB 上架、KB 生命周期不冻结)→ 安装者检索这份活 dataset 就会读到上架时根本不存在、发布者事后才加的私有内容。**这不是租户拦截器能拦的**(两边 binding 指向同一个 RAGFlow dataset_id检索链对返回 chunk 无任何二次过滤,见 §1 事实 1。D0-fork 用"复制一份只含公开快照的独立 dataset"把这条链从物理上斩断。
---
## 0. 与临时-03 的差异(读过临时-03 的人只需看这一节就能对齐)
| 维度 | 临时-03共享发布者活 datasetsuperseded | 临时-04D0-fork本稿 |
|---|---|---|
| 越权模型 | 安装者**之间**越权(多租户共享 dataset | 安装者读到**发布者私有**内容(上架后新增/未公开文档) |
| 隔离手段 | 运行时 chunk 隔离(`metadata_filter`/`document_ids` 把 chunk 限到本授权子集) | **物理隔离**fork 一份只含公开文档的专用 dataset安装者只读它 |
| 检索时 dataset | 发布者活 dataset共享 | **公开副本 dataset**(与发布者活 dataset 物理分离) |
| `metadata_filter` 字段 bug | U3 隔离前置必须先修(`metadata_filter``metadata_condition` | **降级为潜伏 bug**:不在 D0-fork 关键路径(公开副本纯公开、检索不靠它过滤);单独记、留待修 |
| 核心新增工程 | chunk 级隔离 + 物化时给文档打安装维度元数据 | **发布侧 fork 公开副本**(建 dataset + 复制文档 + 重索引,异步重活)+ 打破 market→knowledge 零依赖接线 |
| 复制方向 | 否决了"每安装者各复制一份"C 分支) | fork **一份**公开副本、N 个安装者共享只读(副本仅 1 份,非每安装者一份) |
**临时-03 哪些资产被本稿继承**
- **U0 spike 的 2 个 spike case 保留**:机制事实"检索链对返回 chunk 无 chunk 级过滤"(混入他源 magic chunk 原样返回 + `RetrieveChunksCommand``documentIds`/`metadataFilter` 均 null = 整库扫描)仍然成立、仍是 D0-fork 必须斩断越权的根因证据;它在本稿转作 **U-verify 的反向基线**——用来证明"发布者私有不泄露"fork 后安装者检索副本,混入副本之外的发布者私有 magic chunk 应**检索不到**。spike 测试已落 `MuseKnowledgeRetrievalApiImplTest`+69 行,零业务代码),不必重写。
- **临时-03 的四条硬事实**kb_id 被 assetId 污染、跨 BC 反查链断需新 `MarketAssetSourceApi``muse_source_propagation_target` 表不存在、检索链无 chunk 二次过滤)**全部仍然有效**,是本稿 U-materialize/U-retrieve 的地基§1 复述。
---
## 1. 执行前必读:把工作量钉死的硬事实(基于真实代码,行号为只读核验所得)
**事实 1 — 检索链对返回 chunk 无任何二次过滤,"谁能进检索、就看到该 dataset 整库"。这是 D0-fork 必须做物理隔离、而非运行时隔离的根因。**
RAGFlow 检索 HTTP body`HttpRagFlowKnowledgeRuntimeClient.retrieveChunks:144-157`)只发 `dataset_ids + question + top_k + similarity_threshold + metadata_filter``tenantId/ownerUserId/kbId` **不进 RAGFlow**。检索链 `MuseKnowledgeRetrievalApiImpl.retrieveForWork`:双门 + `selectActiveDatasetByKbId` 只控制"能否拿到这个 dataset_id",一旦 dataset_id 进入检索,`document_ids` 传 null`:149` 第五个参数 null**整库返回**`parseChunks:171-198` 回来只按 dataset_id 贴归因元数据,**对 chunk 无任何按授权子集的二次过滤**。**结论**:只要安装者 binding 指向发布者活 dataset就能读到该 dataset 里发布者后加的私有 chunk。D0-fork 让安装者 binding 指向的是**只含公开快照的副本 dataset**,从源头让"整库返回"返回的也只有公开内容。
**事实 2 — 那个 `metadata_filter` 字段在 RAGFlow 侧被静默忽略(潜伏 bugD0-fork 下不在关键路径)。**
检索发 `body.put("metadata_filter", ...)``HttpRagFlowKnowledgeRuntimeClient.retrieveChunks:155`),但 RAGFlow `/api/v1/retrieval` 官方契约字段是 `metadata_condition`context7 `/infiniflow/ragflow` 核对 + 临时-03 活体对照:乱值条件仍返 baseline故当前 `metadata_filter` 被 RAGFlow 静默忽略、根本不过滤。**结论**:临时-03 的共享分支要靠它做隔离,所以必须先修;**D0-fork 不靠它**(公开副本纯公开,检索不需要再按维度过滤),故此 bug 在本期**降级为潜伏 bug**——`§7 风险 R6` 单独记、留待后续修,不进 D0-fork 关键路径。
**事实 3 — `kb_id` 字段当前被污染:存的是 market assetId不是真实 `muse_knowledge_base.id`。**
`MuseKnowledgeBindingService.createKnowledgeBindingPrecheck:110` `precheck.setKbId(parseLong(reqVO.getSourceId()))`studio 传入的 `sourceId` 实为 market **assetId**`createKnowledgeBinding:148` `binding.setKbId(precheck.getKbId())` 原样进 `muse_knowledge_binding.kb_id`。**结论**:物化要把 `kb_id` 从 assetId 改写成新建本地 installed_ref kb 行的真实主键,并保证检索/读回端口跟着改对。这是 U-materialize 的核心难点。
**事实 4 — `assetId → 发布者 kbId` 反查链数据上通、跨 BC 上断market 对外 API 包当前只有 `MarketHandoffTokenApi`。**
`muse_market_asset.source_id``V6:10` BIGINT在审核物化资产时写入发布者本地 kbId`AdminMarketReviewServiceImpl.resolveApprovedAssetBinding:454` `asset.setSourceId(longValue(draftSnapshot.get("sourceId")))`)。但 knowledge 不能直读 market 的 `.dal`ArchUnit `BcBoundaryArchTest`market `api` 包下**目前只有 `MarketHandoffTokenApi`verify/consume**,没有"按 assetId 查源对象/查公开副本 dataset"的读接口。**结论**D0-fork 下安装侧物化要拿到"公开副本 dataset id",必须**新增跨 BC 读端口**`MarketAssetSourceApi.getAssetSource(assetId) → {sourceType, sourceId=发布者kbId, publisherUserId, publicForkDatasetId, forkStatus}`,本地进程内 Bean`MarketHandoffTokenApi` 同范式、不加 Feign。**注意 handoff token 不带这个信息**token snapshot`MarketHandoffServiceImpl.handoffSnapshot:450`)只含 assetId/authorizationSnapshotId/targetWorkId/targetPage没有发布者 kbId/dataset故拿不到副本 dataset必须走这个新读端口。
**事实 5 — 发布快照 `muse_knowledge_publish_snapshot` 不含文档子集,印证"上架时刻 KB 全部文档 = 公开集"。**
`MuseKnowledgePublishService.createKBPublishSnapshot:140-159` 写入的快照只有 `sourceSnapshotId`/`authorizationSnapshotId`/`packageHash` 等字符串,**没有"哪些文档算公开"的文档清单**DO `MuseKnowledgePublishSnapshotDO` 也无文档子集字段)。**结论**:因为 draft/上架没有"公开文档子集"概念,**whole-KB 上架时刻 KB 的全部文档即公开集**U-fork §3.3 据此界定)。这也意味着 fork 的"公开快照"= 上架审核那一刻 KB 的全部文档版本,发布者**上架后新增的文档不属于这个快照、不进副本**——这正是隔离发布者私有的语义支点。
**事实 6 — dataset 与文档复制无现成 RAGFlow "copy" APIfork 必须重走 createDataset + 逐文档 upload + startParse重索引。**
`RagFlowKnowledgeRuntimeClient` 接口11 个操作)有 `createDataset`/`uploadDocuments`/`startParseDocuments`**没有 copyDataset/copyDocument**。现有上传链路 `MuseKnowledgeDocumentService.continueRagflowIfMaterialized:248-298` 的形态是:`ensureDatasetBinding`(懒建 dataset`uploadDocuments`(带 `fileRef`/`content` byte[])→ `startParseDocuments`(触发解析/索引)。**结论**fork 一份副本 = 新建一个副本 dataset`createDataset`+ 把发布者每个公开文档的物化文件(`muse_knowledge_document_version.storageRef` + 物化内容)逐个 `uploadDocuments` 到副本 + 逐个 `startParseDocuments` 重索引。这是 **异步长任务重活**(多次外部调用、可能失败/部分成功),是 D0-fork 相对临时-03 最大的新增成本U-fork §3.5 据此设计。
**事实 7 — market 召回/下架对目标 owner 的传播当前是 fail-closed blocked 记录式knowledge 侧无消费入口。**
`AdminMarketGovernanceServiceImpl.writeSourceStatusBlocked:361` 把召回/下架的目标 owner 传播落成 `propagationStatus=blocked / reason=target_owner_unavailable / errorCode=TARGET_OWNER_UNAVAILABLE`,注释自证"Market Task 9 未接入目标 owner 传播"。**结论**:召回/下架回滚到 knowledge installed_ref + 副本 dataset 的自动化,本期是**开放项**U-verify §回滚 + 决策点 D5 如实标,不假装已闭环;也不引用临时-03 已证伪的 `muse_source_propagation_target` 表)。
**事实 8 — installed_ref 落点的列与唯一键已就绪B' 共享/D0-fork 初版均不需要新 DDL除可选 fork 状态列,见 §6。**
`muse_knowledge_base``V5:4-28`)已备 `kb_type`/`source_market_asset_id`(BIGINT)/`license_snapshot_id`(BIGINT)`muse_knowledge_ragflow_binding``V14`)唯一键 `uk_..._active = (tenant_id, kb_id) WHERE status='active' AND document_id IS NULL AND deleted=FALSE`——副本 dataset binding 挂在**新建本地 installed_ref kbId** 上不撞发布者 kb 的键。**结论**安装侧物化U-materialize落在 V5/V14 已有列,**不需要新迁移**;唯一可能需要 DDL 的是发布侧"副本 dataset id + fork 状态"的存放§6 给两种落点,倾向复用 JSONB 不加列 → 仍不需要迁移)。
> 这八条把 D0-fork 的真实成本钉死:**核心增量 = 发布侧 fork 公开副本(事实 5/6异步重活全新+ 打破 market→knowledge 零依赖接线(事实 4/7+ kb_id 去污染 + 新跨 BC 读端口(事实 3/4**;运行时隔离与 `metadata_filter` 修复(临时-03 的核心增量)在 D0-fork 下**不需要**。
---
## 2. 总览:实现单元、数据流与 fork 时序
### 2.1 实现单元依赖图
```mermaid
flowchart TD
UFORK["U-fork发布侧 fork 公开副本,核心新工程)<br/>上架事件 → knowledge 异步建副本 dataset<br/>+ 逐文档复制 + 重索引 + 幂等/重试/部分成功"]
UMAT["U-materialize安装侧物化原 U2 简化去隔离)<br/>建 installed_ref kb 行 + binding 指向公开副本 dataset<br/>kb_id 去污染 + 新 MarketAssetSourceApi 跨 BC 读 + 幂等"]
URET["U-retrieve检索打通原 U3 简化)<br/>selectActiveDatasetByKbId 返非空→命中<br/>无运行时隔离(副本纯公开)+ 授权快照 ADR-020 VARCHAR"]
UVER["U-verify验证<br/>真 PG IT物化→检索命中+ studio e2e<br/>+ 发布者私有不泄露(复用 U0 反向基线)+ 召回/下架回滚"]
UFORK --> UMAT
UMAT --> URET
URET --> UVER
DEC{"前置决策门人类拍板§5<br/>fork 时机:上架 vs 懒 fork倾向上架<br/>接线:事件驱动 vs facade倾向事件<br/>副本更新策略:重新上架才更新(版本快照语义)"}
DEC --> UFORK
style UFORK fill:#fde2e2,stroke:#c0392b,stroke-width:3px
style UMAT fill:#e2f7e8,stroke:#27ae60
style URET fill:#e2f7e8,stroke:#27ae60
style UVER fill:#fff6d6,stroke:#b8860b
style DEC fill:#eef3fb,stroke:#3b6fb6,stroke-width:2px
```
### 2.2 物理隔离:发布侧 vs 安装侧D0-fork 的越权解法)
```mermaid
flowchart LR
subgraph PUB["发布者域owner = 发布者)"]
LIVE["发布者活 dataset<br/>kb-{发布者kbId}-v{ver}<br/>含公开 + 上架后新增私有文档"]
FORK["U-fork: 公开副本 dataset<br/>fork-asset-{assetId}-public<br/>只含上架时刻公开快照文档"]
LIVE -.上架时刻公开快照<br/>逐文档复制+重索引.-> FORK
end
subgraph INS["安装者域owner = 各安装者)"]
IREF["installed_ref kb 行(每安装者一行)<br/>kb_type=installed_ref<br/>source_market_asset_id=assetId"]
IBIND["ragflow_binding<br/>kb_id=本地installed_ref kbId<br/>ragflow_dataset_id=公开副本"]
end
FORK ==>|N 安装者共享只读<br/>副本仅 1 份| IBIND
IREF --> IBIND
IBIND --> RET["检索 selectActiveDatasetByKbId<br/>→ 公开副本 dataset → RAGFlow 整库返回<br/>🟢 整库 = 纯公开,发布者私有物理不在副本内"]
style LIVE fill:#fde2e2,stroke:#c0392b
style FORK fill:#e2f7e8,stroke:#27ae60,stroke-width:2px
style RET fill:#e2f7e8,stroke:#27ae60
```
### 2.3 fork 时序(上架触发 + 事件驱动 + 异步重活,含失败/重试/幂等)
```mermaid
sequenceDiagram
participant Admin as 管理员审核
participant MReview as AdminMarketReviewServicemarket
participant Outbox as market 上架事件outbox/source event
participant KConsumer as Knowledge fork 消费者knowledge
participant KFork as KnowledgeMarketForkServiceknowledge
participant RAG as RAGFlow
Admin->>MReview: approvePublishRequest含 markListed
MReview->>MReview: 资产/版本 listedmarket 自有事实,同事务)
MReview->>Outbox: 落上架事件 listed{assetId, 发布者kbId, version}(同事务,不等 fork
Note over MReview,Outbox: 审核事务到此提交,不被 fork 成败绑架
MReview-->>Admin: 审核通过fork 异步进行中)
Outbox-->>KConsumer: 异步派发上架事件
KConsumer->>KFork: forkPublicSnapshot(assetId, 发布者kbId, version)
KFork->>KFork: 幂等检查(已 fork 过该 assetId+version?)命中则跳过
KFork->>RAG: createDataset(fork-asset-{assetId}-public)
loop 每个上架时刻公开文档
KFork->>RAG: uploadDocuments(副本dataset, 文档物化内容)
KFork->>RAG: startParseDocuments(副本dataset, 重索引)
end
alt 全部成功
KFork->>KFork: forkStatus=ready + 记副本 datasetId供 MarketAssetSourceApi 带回)
else 部分失败/RAGFlow 502
KFork->>KFork: forkStatus=partial/failed + 标可重试(不阻断已建部分)
KConsumer->>KFork: 重试(幂等:已成功文档不重复传)
end
```
### 2.4 安装侧物化 + 检索数据流D0-fork 后)
```mermaid
flowchart LR
A["用户跳转→knowledge 绑定路径<br/>createKnowledgeBindingPrecheck/Binding"]
A --> B["消费 handoff token原有不动"]
B --> C["U-materialize: MarketAssetSourceApi.getAssetSource(assetId)<br/>带回 {发布者kbId, publicForkDatasetId, forkStatus}"]
C --> D{forkStatus=ready?}
D -->|否fork 中/失败| E["fail-closed: 暂不可绑定/绑定但检索 no_dataset<br/>(决策点 D6"]
D -->|是| F["建本地 muse_knowledge_base 行<br/>kb_type=installed_ref, source_market_asset_id=assetId<br/>license_snapshot_id=授权快照"]
F --> G["建 muse_knowledge_ragflow_binding<br/>kb_id=新建本地kbId, ragflow_dataset_id=公开副本"]
F --> H["binding.kb_id=新建本地kbId去污染不再是 assetId"]
H --> I["检索 selectActiveDatasetByKbId(本地kbId)<br/>→ 公开副本 dataset → 🟢 命中且纯公开"]
G --> I
style C fill:#fff6d6,stroke:#b8860b
style F fill:#e2f7e8,stroke:#27ae60
style G fill:#e2f7e8,stroke:#27ae60
style I fill:#e2f7e8,stroke:#27ae60
style E fill:#fde2e2,stroke:#c0392b
```
---
## 3. 实现单元
> 文件路径均为 repo 相对路径。每个单元含 Goal / Files / Approach / Test scenarios输入+预期)/ Verification。测试场景"输入"指具体调用入参或数据状态,"预期"指可断言的可观测结果。
### U-fork · 发布侧 fork 公开副本(核心新工程)
**Goal**:知识库上架时,在发布侧把"上架时刻 KB 的全部公开文档"fork 成一份**专用公开副本 dataset**(建副本 dataset + 逐文档复制内容 + 重索引),并把副本 dataset id + fork 状态留存,供安装侧 `MarketAssetSourceApi` 带回。物理隔离发布者私有内容(副本只含上架快照、发布者上架后新增不进副本)。**这是 D0-fork 相对临时-03 全新的、工程量最大的一块。**
**Files**
- 新增 knowledge 侧 fork 服务(核心):`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/main/java/cn/iocoder/muse/module/knowledge/application/muse/KnowledgeMarketForkService.java`(新)
- 新增上架事件消费者(接线,事件驱动方案):`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/main/java/cn/iocoder/muse/module/knowledge/application/muse/KnowledgeMarketListedEventConsumer.java`(新)
- 复用范本fork 复制三步的写法):`.../knowledge/application/muse/MuseKnowledgeDocumentService.java``ensureDatasetBinding:300-319` createDataset 形态、`continueRagflowIfMaterialized:248-298` upload+startParse 形态、`persistDatasetBinding:321-347` 数据集级 binding 写法)
- 复用运行时边界(无需改接口):`.../knowledge/application/muse/facade/RagFlowKnowledgeRuntimeClient.java``createDataset`/`uploadDocuments`/`startParseDocuments` 已够用,事实 6
- 发布侧上架事件落点接线事件驱动方案market 侧):`muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/AdminMarketReviewServiceImpl.java``markListed:338-358`/`approvePublishRequest` 内落上架事件)
- 副本 dataset id + fork 状态存放:见 §6倾向复用 `muse_knowledge_ragflow_binding` 数据集级行 + `muse_market_asset.tags` JSONB不加 DDL
- 读取发布者公开文档清单与物化内容:`.../knowledge/dal/mysql/muse/MuseKnowledgeDocumentMapper.java` / `MuseKnowledgeDocumentVersionMapper.java`(按发布者 kbId 读 active 文档版本 + storageRef+ `.../facade/KnowledgeFileFacade.java`(物化内容回读,复用上传链路同源)
**Approach**
**① fork 时机(决策点 D4给推荐 + 理由)**——两个选项中立呈现,**推荐"上架 `markListed` 时 fork"**
- **选项 A推荐上架时 fork**。审核通过 `markListed` 落 market 资产 listed 事实的同时,发上架事件触发 knowledge 异步 fork。
- 优点:① **版本快照语义最干净**——副本 = 上架审核那一刻 KB 的公开集,天然绑 `muse_market_asset_version` 版本,契合"市场资产 = 发布快照"既有模型(事实 5**安装侧保持轻**——安装者绑定时副本已就绪,只建 installed_ref kb + binding与 token 消费同事务原子无异步等待U-materialize 干净);③ 浪费可控——上架是低频审核动作,上架本就意味着"打算供人安装"。
- 代价:未必有人安装就先占一份 RAGFlow dataset 资源(浪费);且 `markListed` 当前是 market 域纯资产状态更新,要打破 market→knowledge 零依赖触发 fork接线问题见②
- **选项 B首次安装懒 fork**。第一个安装者绑定时才触发 fork。
- 优点:无人安装则不浪费 RAGFlow 资源。
- 代价:① **首装者承受 fork 全程异步延迟**(建 dataset + 逐文档上传 + 重索引是长任务);② **版本快照语义被破坏**——懒 fork 时刻发布者 KB 可能已变(已加私有文档),要么上架时先冻结文档清单(等于把"确定公开集"的工作提前到上架,等于 A 的一半),要么懒 fork 复制的不再是"上架时刻公开集"(隔离语义失守);③ 并发首装需抢锁 + 幂等防重复 fork。
- **权衡结论**浪费A 的代价)比"安装延迟 + 版本快照语义失守 + 并发抢锁"B 的代价)更可接受,且 A 的语义正确性是隔离成立的前提。**推荐 A**。RAGFlow 资源浪费可后续用"下架/无安装超时清理副本"缓解非本期§7 R5 标)。
**② 打破 market→knowledge 零依赖的接线(决策点 D5给推荐 + 理由)**——**推荐"事件驱动market 落上架事件 → knowledge 监听异步 fork"**
- **选项 A推荐事件驱动**。`markListed` 在审核事务内落一条 market 自有"上架事件"(复用 outbox/source event 模式,如 `MuseMarketSourceStatusEventDO` 同构或新事件类型knowledge 侧消费者异步消费触发 fork。
- 优点:① **审核事务不被 fork 成败绑架**——审核只管落 market 资产事实 + 发事件(快、不挂在 RAGFlow 上、RAGFlow 502 不回滚审核);② **契合已拍板 ADR-017**Source 传播 = 事件驱动 + 各模块自治);③ **保 BC 边界**——market 不新增对 knowledge 的依赖knowledge 作为消费者自治 fork与现有 `muse_knowledge_source_event` 消费模式同构);④ fork 失败在 knowledge 侧重试,天然契合异步重活。
- 代价:需建 market→knowledge 的上架事件投影 + knowledge 消费者(比 facade 直调工程量大),且引入"fork 异步、安装时副本可能未就绪"的中间态U-materialize 须 fail-closed 处理,见 U-materialize ④ / 决策点 D6
- **选项 Bmarket 调 knowledge facade**。`markListed` 同步调 `KnowledgeMarketForkApi.forkPublicSnapshot(...)`
- 优点:实现直接、无事件基础设施、无"副本未就绪"中间态。
- 致命缺点:① **fork 是异步重活,塞进审核同步事务 = 审核请求挂在 RAGFlow 多次调用上**,失败则审核回滚(审核不该被 fork 成败绑架);② **方向是 market→knowledge 的新反向依赖**(当前 knowledge→market 已有 `MuseKnowledgeBindingService` 依赖 `MarketHandoffTokenApi`),要 knowledge 定义端口、market 适配BC 上可行但与异步重活天然冲突)。
- **权衡结论**fork 异步重活 + ADR-017 已定调事件驱动,**推荐 A**。代价(事件设施 + 副本未就绪中间态)是正确解耦的必要成本,如实计入工程量。
**③ whole-KB 公开快照界定**fork 的"公开集"= **上架审核那一刻发布者 KB 的全部 active 文档版本**(事实 5draft/发布快照无文档子集概念whole-KB 上架。fork 消费上架事件时按 `发布者 kbId``muse_knowledge_document` + 各文档 `selectLatestByDocumentId` 的 active 版本作为复制清单。**发布者上架后新增的文档**因 create_time 晚于上架事件,**不在这一次 fork 的清单内**——这就是隔离发布者私有的语义支点。(若上架时刻有 `scan_blocked`/`failed`/`deleting` 的文档fork 应跳过,只复制 `searchable`/`generative` 可检索文档。)
**④ 复制机制(事实 6**fork 一份副本 dataset 三步走,复用上传链路同源逻辑:
1. `ragFlowClient.createDataset(CreateDatasetCommand)`:副本 dataset 名建议 `fork-asset-{assetId}-v{version}-public`(区别于发布者活 dataset `kb-{kbId}-v{ver}`config 带 `source=market_public_fork, assetId, version`
2. 对上架清单每个公开文档版本:从 `muse_knowledge_document_version.storageRef` + `KnowledgeFileFacade` 回读物化内容byte[]/fileRef与发布者上传时同源`ragFlowClient.uploadDocuments(UploadDocumentsCommand)` 上传到副本 dataset。
3. 对每个已上传文档 `ragFlowClient.startParseDocuments(StartParseDocumentsCommand)` 触发副本侧重解析/重索引(副本是独立 dataset必须重新索引不能共享发布者索引
**⑤ 异步重活:失败/重试/幂等/部分成功CLAUDE.md 外部交互铁律)**
- **幂等键**`(assetId, version)` 维度。fork 前先查"该 assetId+version 是否已 fork"按副本存放落点查§6命中且 `forkStatus=ready` 则整体跳过;命中但 `partial/failed` 则只补未成功的文档(已成功文档不重复上传)。
- **部分成功**:建副本 dataset 成功但部分文档上传/解析失败 → `forkStatus=partial`,记已成功文档集,**不删已建部分**,标可重试;下次消费/重试只补差集。
- **失败分类沿用**RAGFlow 调用失败按 `RagFlowKnowledgeRuntimeClient.FailureClass` 分可重试(`RAGFLOW_UNAVAILABLE`/`TIMEOUT`/`RATE_LIMITED`/`CONFIG_MISSING`/`AUTH_MISSING`)与不可重试;可重试走消费者重试,不可重试落 `forkStatus=failed` + errorCode 供人工介入。
- **审计**fork 的每次 RAGFlow 调用复用 `MuseKnowledgeAuditService.recordRagflowCall``attributionStatus``market_public_fork`correlation 区分 operation 防唯一键冲突(同 `auditRagflowCall` 范式)。
- **重索引耗时**startParse 是异步触发,副本文档真正 `searchable` 需 RAGFlow 后台解析完成;`forkStatus=ready` 的判定应是"全部文档 startParse 已 accepted"(而非"已 searchable"searchable 由 RAGFlow 后台推进——这点要在 U-verify 用真 RAGFlow 验证副本最终可检索§U-verify 场景 1
**Test scenarios输入+预期)**
- fork 公开副本|输入:发布者 kb=K含 3 个 searchable 公开文档 + 1 个上架后新增的私有文档 PassetId=X 上架触发 fork预期新建副本 dataset D_pubD_pub 含 K 上架时刻的 3 个公开文档(重索引后可检索),**不含**私有文档 P`forkStatus=ready` + 副本 datasetId 可被 `MarketAssetSourceApi` 带回。
- whole-KB 公开集界定|输入:上架时刻 K 有文档 {d1 searchable, d2 generative, d3 scan_blocked, d4 deleting}|预期:副本只复制 {d1, d2}(可检索的),跳过 d3/d4。
- 幂等-重复 fork输入同一 (assetId=X, version=1) 再次触发 fork重新派发事件/重试)|预期:**不新建第二个副本 dataset**(命中 ready 跳过);已 ready 副本不变。
- 部分成功重试|输入:首次 fork 时 d2 上传失败RAGFlow 502`forkStatus=partial`;重试再来|预期:副本 dataset 不重建d1 不重复上传,只补 d2补成功后 `forkStatus=ready`
- 失败不阻断审核输入fork 全程 RAGFlow 不可用预期market 审核已提交 listed事件已落`forkStatus=failed`+可重试;安装侧据 `forkStatus≠ready` fail-closed不会让安装者绑到不存在的副本
**Verification**
- 模块单测/IT`cd muse-cloud && mvn -pl muse-module-knowledge/muse-module-knowledge-server -am test -Dtest='KnowledgeMarketForkServiceTest'`mock `RagFlowKnowledgeRuntimeClient`,覆盖公开集界定 + 幂等 + 部分成功重试 + 私有文档不进副本)。
- 活体(真 RAGFlowmini-infra 100.64.0.8 在线):按模块 `.agent`/`.agents/knowledge` 起栈,真 fork 一份副本并对副本 `retrieveChunks` 确认可检索 + 不含私有 magic并入 U-verify 端到端)。
- **凭据红线**RAGFlow base-url/api-key 由 env 注入,脚本/文档不打印任何 key 值(过滤 `password|secret|token|sk-`)。
---
### U-materialize · 安装侧物化(原 U2 简化,去隔离)
**Goal**:在 knowledge 目标域绑定路径里,把"只写引用绑定"升级为"建出可用的本地 installed_ref 知识库实体 + binding 指向**公开副本 dataset**(非发布者活 dataset";做到 kb_id 去污染assetId→本地主键+ 新增 `MarketAssetSourceApi` 跨 BC 读(带回公开副本 dataset id + forkStatus+ 幂等 + kbId 存在性校验。**相对临时-03 U2 的简化**去掉给文档打安装维度元数据、去掉运行时隔离前置D0-fork 不需要)。
**Files**
- 主改:`.../knowledge/application/muse/MuseKnowledgeBindingService.java``createKnowledgeBindingPrecheck:71-127``createKnowledgeBinding:129-177``consumeMarketHandoff:273-284`
- 新增跨 BC 读端口(事实 4
- market 侧 API被 knowledge 依赖):`muse-cloud/muse-module-market/muse-module-market-api/src/main/java/cn/iocoder/muse/module/market/api/asset/MarketAssetSourceApi.java`(新)+ DTO `.../api/asset/dto/MarketAssetSourceRespDTO.java`(新,含 `sourceType/sourceId=发布者kbId/publisherUserId/publicForkDatasetId/forkStatus`
- market 侧实现:`muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/api/asset/MarketAssetSourceApiImpl.java`(新,读 `muse_market_asset` 的 source_id 等 + 副本 dataset 存放,**经 market 自己的 .dal**
- installed_ref kb 写入 DAL`.../knowledge/dal/mysql/muse/MuseKnowledgeBaseMapper.java`(复用 insert`MuseKnowledgeRagflowBindingMapper.java`(复用 insert
- 参照范本:`.../knowledge/application/muse/MuseKnowledgeDocumentService.java``persistDatasetBinding:321-347` 数据集级行写法 + `DataIntegrityViolationException` 回读幂等)
**Approach**
1. **新增 market 跨 BC 读端口**(事实 4`MarketAssetSourceApi.getAssetSource(Long assetId)``{sourceType, sourceId=发布者kbId, publisherUserId, publicForkDatasetId, forkStatus}`。范式同 `MarketHandoffTokenApi`(本地 Beanknowledge 注入它而非读 market.dal。**与临时-03 的关键区别**:临时-03 带回"发布者活 dataset"(要 knowledge 跨 owner 读发布者 kb 的 dataset有租户拦截器难题 D4**D0-fork 带回的是"公开副本 dataset id"**——副本是 fork 时新建的、其 dataset id 已由 U-fork 留存在 market 可读的存放§6market 直接读自有数据即可带回,**不再需要 knowledge 跨 owner 读发布者 dataset临时-03 的 D4 难题在 D0-fork 下消失**。保证不破 ArchUnit `bc-boundaries`
2. **kbId 跨 BC 存在性校验 + 去污染**market_kb 绑定时 `sourceId` 当作 **assetId** 处理(不再 `parseLong` 当 kbId`MarketAssetSourceApi.getAssetSource(assetId)` 校验资产存在且为 `knowledge_base` 类型且 `forkStatus=ready`(不存在/类型不符/副本未就绪 → fail-closed 抛错,替代现状裸信客户端)。
3. **建本地 installed_ref 实体(落点:`createKnowledgeBinding` 同一 `@Transactional` 事务内)**
-`muse_knowledge_binding` 之前先 insert `muse_knowledge_base``kb_type='installed_ref'``source_market_asset_id=assetId``license_snapshot_id=授权快照主键``owner_user_id=安装者``status='active'``active_version=1``name/description` 取资产摘要(`MarketAssetSourceApi` 可一并带回或占位);拿到**新建本地 kbId**。
- insert `muse_knowledge_ragflow_binding``kb_id=新建本地kbId``ragflow_dataset_id=公开副本datasetId``document_id=null``status='active'``active_version=1`,形态仿 `persistDatasetBinding`。**副本是共享只读的,多个安装者的 installed_ref binding 都指向同一 publicForkDatasetId**(副本仅 1 份),但各自 kb_id 是各安装者本地新建主键,不撞 `uk_..._active`
- **去污染**`binding.setKbId(新建本地kbId)`(不再是 assetId建实体放 `createKnowledgeBinding`用户确认绑定的写事实点precheck 仅校验资产存在性 + forkStatus避免 precheck 失败留孤儿 kb 行。
4. **fork 未就绪的 fail-closed决策点 D6**:若 `getAssetSource``forkStatus≠ready`fork 中/失败),有两种处置:(a) **绑定阶段直接拒**(抛"来源副本准备中"错误码,让用户稍后重试);(b) **允许建 installed_ref kb 但 binding 指向空/检索 no_dataset**(绑定成功、检索暂不命中,副本就绪后自动可用)。**倾向 (a)**fail-closed 更清晰,避免"绑了但用不了"的迷惑态);(b) 需 U-retrieve 容忍 no_dataset本就容忍。U-materialize 实现时定夺并回填。
5. **幂等**(重复 install/bind 不重复造实体):建 installed_ref kb 行前先 `SELECT ... WHERE source_market_asset_id=assetId AND owner_user_id=安装者 AND deleted=false`,命中则复用既有 installed_ref kbId不重复建dataset binding 复用 `persistDatasetBinding``DataIntegrityViolationException`→回读模式应对并发。
6. **事务原子性**:物化与"消费 handoff token + 写绑定事实"在同一事务(现状 `consumeMarketHandoff` 已沿用调用方 `@Transactional`),失败整体回滚。
**Test scenarios输入+预期)**
- 首次安装绑定副本就绪输入T_A 安装 assetId=Xknowledge_base 类型,公开副本 D_pubforkStatus=ready`createKnowledgeBinding`(合法 handoff token + precheckId预期新建 `muse_knowledge_base(kb_type=installed_ref, source_market_asset_id=X, owner_user_id=A)` 记 id=K2`muse_knowledge_binding.kb_id=K2`**非 X**);新建 `muse_knowledge_ragflow_binding(kb_id=K2, ragflow_dataset_id=D_pub, document_id=null, status=active)`
- 多安装者共享副本输入A、B 各装 assetId=X预期建两行 installed_ref kbK2、K3各自 owner两条 ragflow_binding **都指向同一 D_pub**(副本仅 1 份),不违反 `uk_..._active`kb_id 各异)。
- 幂等-重复绑定|输入:同一 (A, X) 再次 install+bind预期不新建第二行 installed_ref kb复用 K2不抛 500。
- 副本未就绪 fail-closed输入assetId=X 的 `forkStatus=partial/failed`|预期:按 D6 处置倾向拒绝绑定0 写 installed_ref/binding抛明确错误码
- 资产不存在/类型不符|输入:`sourceId` 指向不存在 assetId 或 work 类型预期fail-closed 拒绝0 写。
- BC 边界输入ArchUnit `BcBoundaryArchTest`预期knowledge 不直接 import market 的 `.dal`(只经 `MarketAssetSourceApi`test 绿。
**Verification**
- 模块单测/IT`cd muse-cloud && mvn -pl muse-module-knowledge/muse-module-knowledge-server -am test -Dtest='MuseKnowledgeBindingServiceTest,*RoundTripTest'`
- **改了 market-api 模块务必先 `mvn -pl muse-module-market/muse-module-market-api -am install`** 再跑 knowledge 模块测试,否则 stale jar 假红memory `muse-market-install-isacquired-enrich` 教训)。
- ArchUnit`mvn -pl ... test -Dtest=BcBoundaryArchTest`
---
### U-retrieve · 检索打通(原 U3 简化,无运行时隔离)
**Goal**:物化后,检索第四门 `selectActiveDatasetByKbId(本地kbId)` 返非空 → 命中(消除 `no_dataset` 静默省略);**因检索命中的是纯公开副本,无需任何运行时 chunk 隔离**;授权快照沿用 ADR-020 VARCHAR envelope不回退 BIGINT/parseLong。**相对临时-03 U3 的简化**:删掉 metadata_filter/document_ids 隔离透传D0-fork 不需要),`metadata_filter` 字段 bug 不在本单元修潜伏§7 R6
**Files**
- 主路径(**无需改**,靠 U-materialize 数据正确即自然打通):`.../knowledge/api/MuseKnowledgeRetrievalApiImpl.java`(第四门 `:122-127``parseChunks:171-198`
- 不改:`RetrieveChunksCommand` 构造(`:148-151`)保持 `document_ids=null, metadata_filter=null` 现状(副本纯公开,整库返回即纯公开内容)
**Approach**
1. **检索基线打通**U-materialize 把 `binding.kb_id` 改成本地真实 kbId、并建好数据集级 `muse_knowledge_ragflow_binding(kb_id=本地kbId, ragflow_dataset_id=公开副本)` 后,检索链第四门 `selectActiveDatasetByKbId(本地kbId)` 自然返非空 → `no_dataset` 消除 → 进 RAGFlow 检索副本。**RetrievalApiImpl 代码无需改动**。
2. **无运行时隔离D0-fork 核心简化)**:检索命中的是公开副本 dataset其"整库返回"返回的本就只有公开内容(发布者私有物理不在副本内,事实 1 的"整库返回"在这里是安全的)。因此**不需要** metadata_filter/document_ids 把 chunk 限到子集——这正是 D0-fork 比临时-03 共享分支简单的地方。
3. **授权快照沿用 ADR-020**:检索门读 `binding.getAuthorizationSnapshotId()`V14 已 VARCHARinstalled_ref binding 写入时沿用 VARCHAR 字符串承载chunk §5.3 合同字段 `authorizationSnapshot` 透传字符串,不 parseLong。
**Test scenarios输入+预期)**
- no_dataset 消除 + 命中输入U-materialize 物化后的 installed_ref本地kbId=K2关联公开副本 D_pubwork 绑定含 search 用途 + 授权快照,`retrieveForWork(question 命中 D_pub 公开内容)`|预期:结果**非** `empty("no_dataset")`,进入 RAGFlow 检索,`RetrievalResult.ok(chunks)`chunk 带齐 §5.3 字段;对照现状(同输入必返 `no_dataset`)。
- 发布者私有不泄露(承 U0 反向基线,核心)|输入:发布者活 dataset 含私有 magic chunk上架后新增、未进副本安装者 T_A 检索其 installed_ref指向 D_pub预期返回 chunk **不含**发布者私有 magic因 D_pub 物理不含该文档),证明 D0-fork 隔离成立。
- 授权快照字符串输入installed_ref binding 的 `authorization_snapshot_id` 为字符串 envelope 形态预期检索门正常放行、chunk 透传字符串,无 NumberFormatException、不落 null。
**Verification**
- 单测:`mvn -pl muse-module-knowledge/muse-module-knowledge-server -am test -Dtest=MuseKnowledgeRetrievalApiImplTest`mock RAGFlow覆盖 no_dataset 消除 + 字符串授权快照;私有不泄露并入 U-verify 真 RAGFlow
- 活体并入 U-verify 端到端。
---
### U-verify · 验证(真 PG 端到端 + studio e2e + 发布者私有不泄露 + 回滚)
**Goal**:用真实证据证明"知识库上架→fork 公开副本→安装→绑定→物化→检索命中"端到端可真用;且**发布者私有内容不泄露给安装者**D0-fork 的核心安全目标);召回/下架时已物化实体能回滚。
**Files**
- 真 PG IT`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/test/java/cn/iocoder/muse/module/knowledge/.../P1rMarketKbForkMaterializationIT.java`(参照既有 `P1r*IT` + `P1rRagFlowLiveAcceptanceIT` live 范式)
- studio e2e`muse-studio/e2e/market-install-kb-retrieval.spec.ts`(参照 `knowledge-bindings.spec.ts` + `market-install.spec.ts`
- U0 反向基线(复用,不重写):`.../knowledge/api/MuseKnowledgeRetrievalApiImplTest.java`(临时-03 已落的 2 个 spike case混入他源 magic chunk 原样返回 + documentIds/metadataFilter 均 null = 整库扫描)
- 回滚相关(据真实机制,事实 7`.../knowledge/application/muse/MuseInstalledKnowledgeBaseService.java``disableInstalledKnowledgeBase:64`/`deleteInstalledKnowledgeBase:72` 路径)+ knowledge source event/projection
**Approach / Test scenarios输入+预期)**
1. **fork→安装→检索端到端真 PG + 真 RAGFlow**|输入:发布者建 KB3 公开文档)→ 上架触发 fork 出公开副本 → 安装市场 KB 资产 → 作品绑定(物化 installed_ref + binding 指向副本)→ `retrieveForWork`|预期:检索第四门拿到公开副本 dataset → RAGFlow chunk 真命中 → 生成上下文含该来源 grounding对照现状`no_dataset`)。
2. **发布者私有不泄露D0-fork 核心安全验证,复用 U0 反向基线)**|输入:发布者上架后往活 dataset 加一段含唯一 magic token 的私有 chunk未进副本→ 安装者检索其 installed_ref指向副本预期返回**不含**该 magic chunk副本物理不含。**对照 U0 spike 反向基线**U0 钉死了"指向同一 dataset 时 magic chunk 原样返回",本场景证明"D0-fork 指向副本后 magic chunk 消失"——隔离从"原样泄露"转为"物理不可见"即验收通过。
3. **幂等**|输入:重复触发 fork同 assetId+version+ 重复 install/bind预期副本 dataset 不重建、installed_ref kb 不重复DB 中各仅一行。
4. **多安装者共享一份副本**输入A、B 各装同一资产|预期:两个 installed_ref kb 指向同一副本 dataset副本仅 1 份),各自检索命中、互不影响。
5. **回滚/来源传播(基于真实机制,事实 7不引用不存在的 `muse_source_propagation_target`**|输入:发布者资产被 admin 召回/下架(`recallAsset`/`delistAsset`)|预期:已物化 installed_ref 实体被阻断新使用(不自动删,符合 `架构-02` 来源事件"不删除正式知识"语义)。**开放项**market 召回当前对目标 owner 传播是 fail-closed blocked 记录式(事实 7`writeSourceStatusBlocked` errorCode=TARGET_OWNER_UNAVAILABLEknowledge 侧消费"召回→停用 installed_ref + 副本"的入口**本期缺**——U-verify 须如实验证现状传播是否触达 installed_ref若不触达标为开放项"召回回滚未自动化,需新增 market→knowledge 传播消费者或手动停用"),不假装已闭环。
6. **删除/停用已物化实体**|输入:用户 `disableInstalledKnowledgeBase`/`deleteInstalledKnowledgeBase`预期installed_ref 软删(现状 `updateInstalledStatus`),检索读回排除;**不删公开副本 dataset**(副本是共享的,单个安装者停用不影响其他安装者)——副本生命周期绑资产下架/无安装清理§7 R5非本期
7. **机械门禁**输入ArchUnit `BcBoundaryArchTest` + 覆盖 JSON 门预期fork 经 knowledge 自有 + 上架事件消费;安装物化只经 knowledge facade + `MarketAssetSourceApi`market 不直写 knowledge `.dal`test 绿。
**Verification**
- 真 PG IT`muse-cloud/` 跑,按 memory `muse-p1r-it-run-recipe``p1r.flyway.*` argLine + 密码经 env`~/.config/muse-repo/infra.env``set -a && . it && set +a`onlineRAGFlow/New-API 在 mini-infra 在线)。**`_test` 库铁律IT 的 flyway 目标库绝不指 muse_slice_live。**
- studio e2e起全栈 appPG 宿主 100.64.0.8 在线时),`npx playwright test market-install-kb-retrieval.spec.ts`
- 凭据红线:所有脚本/日志过滤 `password|secret|token|sk-`;不 rm `dump.rdb`/`lefthook`
---
## 4. 决策点(待人类拍板,按优先级)
| # | 决策点 | 选项 | 倾向 | 阻塞谁 |
|---|---|---|---|---|
| **D4** | **fork 时机** | 上架 `markListed` 时 fork / 首次安装懒 fork | **上架时 fork**(版本快照语义干净 + 安装侧轻 + 浪费可控;懒 fork 破坏快照语义 + 首装延迟 + 并发抢锁) | U-fork 全部 |
| **D5** | **打破 market→knowledge 零依赖的接线** | 事件驱动market 落上架事件→knowledge 异步消费 fork/ market 调 knowledge facade 同步 fork | **事件驱动**(审核不被 fork 成败绑架 + 契合 ADR-017 + 保 BC 边界facade 同步会让审核挂在 RAGFlow 上) | U-fork 接线 |
| D6 | fork 未就绪时安装侧处置 | 绑定阶段拒fail-closed/ 允许绑定但检索 no_dataset 待就绪 | **绑定阶段拒**(清晰,避免"绑了用不了"迷惑态U-materialize 实现时定夺回填 | U-materialize |
| D7 | 副本更新策略(发布者改版后) | 重新上架才更新副本(版本快照语义)/ 不更新 / 实时跟随 | **重新上架才更新**(与"市场资产 = 发布快照"模型一致;实时跟随会重新引入私有泄露风险) | U-fork |
| D8 | `license_snapshot_id` 承载 | 数值主键(不 widen/ 字符串 envelope需 V32 | **数值主键**(与 install 侧一致,避免牵动 market 两列 BIGINT 独立议题) | 是否需 V32 |
| D9 | 召回/下架回滚自动化 | 复用现有 market→目标 owner 传播 / 本期标开放项手动停用 | 先验现状传播是否触达 installed_ref**不假装已有 `muse_source_propagation_target`**;倾向本期标开放项 | U-verify |
| D10 | 副本 dataset id + fork 状态存放DDL 关键) | 复用 `muse_knowledge_ragflow_binding` 数据集级行 + `muse_market_asset.tags` JSONB不加 DDL/ 新增列/表(需 V32 | **复用现有结构不加 DDL**(见 §6新增列仅在复用不够时触发 | 是否需 V32 |
---
## 5. fork 时机推荐与接线推荐(返回必答,独立成节便于决策)
- **fork 时机推荐 = 上架 `markListed` 时 fork非首次安装懒 fork**。核心理由:副本语义必须是"上架审核那一刻的公开快照"才能成立隔离(事实 5上架时 fork 让这个语义天然成立、且安装侧无异步延迟;懒 fork 要么把"确定公开集"提前到上架(等于做了一半上架 fork、要么复制的不再是上架快照隔离失守还要扛首装延迟与并发抢锁。RAGFlow 资源浪费是上架时 fork 的唯一代价,可后续用"下架/无安装超时清理副本"缓解。
- **接线推荐 = 事件驱动market 落上架事件 → knowledge 监听异步 fork非 market 调 knowledge facade 同步**。核心理由fork 是异步重活(多次 RAGFlow 调用、可能失败/重试),绝不能塞进审核同步事务把审核绑架在 RAGFlow 上;事件驱动让审核事务只落 market 资产事实 + 发事件即返回fork 在 knowledge 侧自治重试,契合已拍板 ADR-017且保住 market 不反向依赖 knowledge 的 BC 边界。代价是需建上架事件投影 + knowledge 消费者,并引入"fork 异步、安装时副本可能未就绪"的中间态U-materialize 用 `forkStatus` fail-closed 兜住)。
---
## 6. DDL 判定:是否需要新 Flyway 迁移
**判定结论D0-fork 初版倾向不需要新 Flyway 迁移**(落在 V5/V14 已有列 + 现有 JSONB但有一个"副本存放落点"的选择会影响这一结论:
- **安装侧物化U-materialize**:落在 `muse_knowledge_base`V5 已备 `kb_type`/`source_market_asset_id`/`license_snapshot_id`+ `muse_knowledge_ragflow_binding`V14 已有列),**不需要迁移**(事实 8
- **发布侧副本 dataset id + fork 状态存放(决策点 D10唯一可能触发 DDL 处)**
- **倾向方案(不加 DDL**:副本 dataset 本身落一行 `muse_knowledge_ragflow_binding`这正是该表语义——kb→dataset 映射,副本可挂在发布者 kb 上以"market_public_fork"类型的 binding_summary 标记,或挂在一个专用载体行),`forkStatus` 与副本 datasetId 通过 `binding_summary` JSONB + `muse_market_asset.tags` JSONB`MarketAssetSourceApi` 从这里带回)承载。**这样不需要新列。**
- **需 V32 的情形**:若评审认为 fork 状态/副本映射必须强类型列(而非 JSONB则新增 `muse_market_asset``public_fork_dataset_id VARCHAR + fork_status VARCHAR` 列,或新建 `muse_market_public_fork` 映射表 → 需 V32DDL 人类门 + `_test` 库铁律 + Flyway 不可逆约定)。
- 另:`license_snapshot_id` 若改存字符串 envelopeD8 选字符串)需 `ALTER ... TYPE VARCHAR` → V32**倾向数值主键不 widen不触发**。
**给人类的明确建议**:先按"复用现有结构 + JSONB 承载 fork 状态/副本映射"做,**不引入 V32**;仅当评审坚持强类型列时才走 V32届时标 DDL 人类门 + V32 版号 + `_test` 库先验)。
---
## 7. 风险
- **R1最高发布者私有泄露**D0-fork 的全部价值在于"副本只含公开快照"。若 fork 的公开集界定错(误把私有文档纳入)、或副本更新策略选"实时跟随活 dataset"D7→ 私有重新泄露。**缓解**U-fork §3.3 严格按"上架时刻 active 文档"界定 + D7 选"重新上架才更新" + U-verify 场景 2 真验私有不泄露(复用 U0 反向基线)。
- **R2 fork 异步重活的复杂度(如实计入,不掩盖)**fork 一份副本 = 建 dataset + N 文档逐个 upload + N 文档逐个 startParse + 重索引,是多次外部调用的长任务,必须处理失败/重试/幂等/部分成功U-fork §3.5)。这是 D0-fork 相对临时-03 最大的新增工程量,**比"共享 + 运行时隔离"重**——诚实呈现D0-fork 安全模型更干净(物理隔离),但发布侧工程更重(异步复制管线)。
- **R3 打破 market→knowledge 零依赖的影响面(如实写清)**:当前 market 对 knowledge **零依赖**架构既有事实D0-fork 必须打破它。事件驱动方案下market 侧改动 = `markListed` 落上架事件(新增 outbox/事件类型knowledge 侧新增消费者;**影响面集中在"上架审核落事件"这一点 + knowledge 新增 fork 消费链**,不改 market 既有审核/资产逻辑主体。但这是架构层面的新接缝,需架构 review 确认ADR-017 事件驱动是其依据)。
- **R4 副本就绪与安装的时序竞态**:上架 fork 异步,安装者可能在 `forkStatus=ready` 前就来绑定。**缓解**U-materialize ④ 用 `forkStatus` fail-closedD6副本就绪后再绑/检索命中。
- **R5 副本生命周期/资源回收(本期不做,标开放)**:上架即 fork 会占 RAGFlow dataset 资源,下架/长期无安装的副本清理本期不做。**缓解**:标开放项("下架/无安装超时清理副本 dataset"),非 D0-fork 关键路径。
- **R6 `metadata_filter` 字段 bug潜伏单独记留待修**`HttpRagFlowKnowledgeRuntimeClient.retrieveChunks:155` 发的 `metadata_filter` 被 RAGFlow 静默忽略(应为 `metadata_condition`,事实 2。**D0-fork 不依赖它**(公开副本纯公开),故本期不修、降级为潜伏 bug但它是真 bug任何未来想靠元数据过滤检索的功能都会踩**留待后续单独修**,不随 D0-fork 关键路径处理。
- **R7 召回/下架回滚未自动化(开放项)**:事实 7本期标开放不假装闭环D9
---
## 8. 回滚(本 plan 执行后如何撤回)
- **代码回滚**U-materialize/U-retrieve 改动集中在 `MuseKnowledgeBindingService` + 新增 `MarketAssetSourceApi`market-api/serverU-fork 改动是新增 `KnowledgeMarketForkService` + 消费者 + market `markListed` 落事件。revert 这些 commit 即回到"只写引用绑定 + market 零依赖 + 不 fork"现状,检索回落 `no_dataset` 静默省略(功能不可用但不报错,与现状一致)。
- **数据回滚**:已物化的 installed_ref kb 行 + binding 经软删停用;**fork 出的公开副本 dataset 可删**(按副本存放落点找到 datasetId调 RAGFlow 删 dataset副本是 D0-fork 新建的、删除无副作用于发布者活 dataset
- **迁移回滚**:初版不引入 V32D8 数值主键 + D10 JSONB 承载),无迁移需回滚;若触发 V32 则按 Flyway 不可逆约定,靠新 forward 迁移回滚(且 `_test` 库先验)。
---
## 9. Scope 边界(明确不做什么)
- **KB 限定**:本 plan **只做知识库KB的 D0-fork 物化**
- **agent 下一阶段**智能体agent物化**不在本 plan**——`muse_agent``market_installed` 落地 + 运行期放行是独立的下一阶段任务,本 plan 不触碰 ai 模块。
- **install 解耦不动**`MarketInstallServiceImpl.installMarketplaceAsset` 维持"只记账、`targetFactsWritten=false`"现状(`:96`**不在 install 侧物化**——物化落点严格在 knowledge 目标域绑定路径。
- **handoff 非物化桥**handoff token 只负责跳转 + 被目标域消费(`MarketHandoffTokenApi.verify/consume` 不动)。
- **运行时 chunk 隔离不做**D0-fork 用物理隔离替代,临时-03 的 metadata_filter/document_ids 运行时隔离**本期不实现**。
- **`metadata_filter` 字段 bug 本期不修**:降级为潜伏 bug 单记R6留待后续。
- **副本资源回收不做**:下架/无安装清理副本 dataset 本期不做R5标开放。
- **召回/下架回滚自动化不做**本期标开放项R7/D9不假装闭环。
- **market 两列 BIGINT 议题不纳入**`muse_market_installation.authorization_snapshot_id`/`source_snapshot_id` 仍 BIGINT 是独立契约议题,本 plan 不处理。
---
## 10. 关联阅读
- 方案演进与权衡(本 plan 的 WHAT 上游):[`临时-02-market-install下游物化方案.md`](临时-02-market-install下游物化方案.md)
- 上一版执行 plan共享 + 运行时隔离,**已被本稿 supersede**,保留作决策演进史):[`临时-03-market-install-KB物化执行plan.md`](临时-03-market-install-KB物化执行plan.md)
- 概念与 owner 边界:[`架构-02-核心数据结构与双轨模型.md`](架构-02-核心数据结构与双轨模型.md)§6 市场资产、§11.2 绑定安装授权、授权快照语义)
- 检索合同 / fail-closed[`专题-03-AI编排上下文与质量评测实现规范.md`](专题-03-AI编排上下文与质量评测实现规范.md)§5 检索和图查询、§5.3 检索结果合同)
- 关键决策:`架构-03-关键决策与原则(ADR).md`ADR-017 Source 传播事件驱动、ADR-020 授权快照字符串承载)
- 表结构 SSOT`后端-04-统一数据库Schema-v1.md`§5 Knowledge
- BC 边界规则:[`.agents/rules/bc-boundaries.md`](../.agents/rules/bc-boundaries.md)