oh-my-muse/design-docs/临时-03-market-install-KB物化执行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

300 lines
41 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.

# 临时-03 · Market Install KB 下游物化执行 PlanHOW供 review 后执行)
> ⚠️ **已被 [`临时-04-market-KB物化-D0fork执行plan.md`](临时-04-market-KB物化-D0fork执行plan.md) supersede2026-06-26**D1 澄清坐实真越权是「安装者读到**发布者私有**内容」(非本稿假设的安装者间互读),用户拍板改走 **D0-fork发布侧 fork 纯公开副本、物理隔离)**,去掉本稿 S 分支的运行时 chunk 隔离、`metadata_filter`→`metadata_condition` 字段修复降级为潜伏 bug。**本稿保留作决策演进史**,新执行依据以临时-04 为准;本稿的 U0 spike 证据与四条硬事实仍有效(被临时-04 继承)。
- 版本v1执行版待人类 review/拍板后执行;本稿只读出文档,不改任何代码)
- 更新日期2026-06-26
- 目标读者:后端 / 架构 / PR reviewer
- 阅读时间2535 分钟
- 文档性质:**临时件**(执行依据,落定后归并入正式分册或删除,不作长期 SSOT。本稿只给"怎么做",不重定义概念:方案权衡与决策收束见 [`临时-02-market-install下游物化方案.md`](临时-02-market-install下游物化方案.md);术语与 owner 边界以 [`架构-02-核心数据结构与双轨模型.md`](架构-02-核心数据结构与双轨模型.md) 为准§6 市场资产、§11.2 绑定安装授权、第 122 行授权快照"引用对象必须保存快照 id");表结构归 `后端-04`;检索合同归 [`专题-03`](专题-03-AI编排上下文与质量评测实现规范.md) §5授权快照字符串承载归 ADR-020BC 边界规则归 [`.agents/rules/bc-boundaries.md`](../.agents/rules/bc-boundaries.md)。
- 配套人读图:[`临时-03-market-install-KB物化执行plan.html`](临时-03-market-install-KB物化执行plan.html)
> **一句话**:拍板已定——做 **B'installed_ref 最小留痕)+ KB 先行**agent 物化下一阶段、本 plan 不含 agent。本 plan 把 B' 拆成 **U0…U4 五个可独立验证的实现单元**,物化落点在**知识库目标域绑定路径**`MuseKnowledgeBindingService`,非 install。但调查坐实了一个比评审版更硬的事实**按当前代码,多租户"共享发布者同一 RAGFlow dataset"必然越权**RAGFlow 不感知 muse 租户、检索链对返回 chunk 无任何租户/授权二次过滤)。因此 **U0 不是"验证隔离是否隔得住",而是"在共享落地前先把 chunk 级隔离补上"**——U0 是真前置硬门U0 绿(隔离补到位)才允许"共享 dataset"U0 红/不做则本期 KB 物化只能走**每安装者独立复制 dataset局部 C**,或退回**暂缓**。这一岔路必须人类拍板后才进 U1。
>
> **2026-06-26 更新D1 已拍 = S 补隔离后共享)**U0 spike 已执行并坐实证据——spike 单测 13/02 case 钉死越权:他租户私有 chunk 原样返回 + `RetrieveChunksCommand` 的 documentIds/metadataFilter 均 null = 整库扫描)+ 活体真 RAGFlow 整库返回;并**揪出新潜伏 bug**:检索发 `metadata_filter`,但 RAGFlow 官方契约context7 `/infiniflow/ragflow`)是 `metadata_condition`,被 RAGFlow **静默忽略**(活体对照:乱值条件仍返 baseline。**S 分支据此定调**:① 隔离手段优先 `metadata_condition`活体验证生效document_ids 限定为备选),`metadata_filter→metadata_condition` 字段名修正纳入 U3 隔离前置;② 物化时给文档打安装维度元数据(否则 metadata_condition 把无元数据文档全滤为 0③ U4 隔离回归复用 U0 的 2 个 spike case 作反向基线(隔离补到位后应从"原样返回他租户 chunk"转红。spike 测试 +69 行已落 `MuseKnowledgeRetrievalApiImplTest`(零业务代码)。
---
## 0. 执行前必读:四条把工作量钉死的硬事实(基于真实代码,非评审版推断)
评审版(临时-02把方向定对了但执行前有四条一手代码事实会直接改变单元划分与工作量必须先摆明每条附证据行号为只读核验所得
**事实 1 — 知识库侧 `kb_id` 字段当前是被污染的,存的是 market assetId不是任何真实 `muse_knowledge_base.id`。**
`MuseKnowledgeBindingService.createKnowledgeBindingPrecheck` 第 110 行 `precheck.setKbId(parseLong(reqVO.getSourceId()))`,而 studio 传入的 `sourceId` 实为 market **assetId**`muse-studio/src/features/handoff/owners/KnowledgeHandoffLanding.tsx:44` `sourceId: assetId`,源头 `HandoffLandingPage.tsx:29` 取自 URL `?assetId=`)。绑定时这个值原样进 `muse_knowledge_binding.kb_id``createKnowledgeBinding` 第 148 行 `binding.setKbId(precheck.getKbId())`)。**结论**market_kb 绑定既没有在 knowledge 域建本地 kb 实体,其 `kb_id` 也不是真实主键——物化要做的不只是"补建实体",还要**把 `kb_id` 从 assetId 改写成新建 installed_ref 行的真实主键**,并保证检索/读回端口都跟着改对。这是 U2 的核心难点,评审版未点破。
**事实 2 — `assetId → 发布者 kbId → 发布者 dataset_id` 这条"共享"必需的反查链,数据上通、跨 BC 上断。**
`muse_market_asset.source_id``V6__init_market_schema.sql:10`BIGINT在 admin 审核物化资产时确实写入发布者本地 kbId`AdminMarketReviewServiceImpl.java:454` `asset.setSourceId(longValue(draftSnapshot.get("sourceId")))`sourceId 源自发布者上架请求 `MarketPublishDraftReqVO.sourceId`)。但 knowledge 域**不能直读 market 的 `.dal`**`bc-boundaries` ArchUnit而 market 对外 API 包 `cn.iocoder.muse.module.market.api` 下**目前只有 `MarketHandoffTokenApi`verify/consume**,没有任何"按 assetId 查源对象"的读接口。**结论**:共享方案必须**新增一个跨 BC 读端口**(如 `MarketAssetSourceApi.getAssetSource(assetId) → {sourceOwner, sourceType, sourceId=发布者kbId, publisherUserId, tenantId}`,本地进程内 Bean、与 `MarketHandoffTokenApi` 同范式、不加 Feign。这是 U2"共享分支"的隐藏前置工作,评审版未列。
**事实 3 — 按当前代码,多租户共享同一 `ragflow_dataset_id` 必然越权(这是 U0 性质的根本改变)。**
RAGFlow 检索 HTTP body`HttpRagFlowKnowledgeRuntimeClient.retrieveChunks:143-158`)只发 `dataset_ids + question + top_k + similarity_threshold + metadata_filter``tenantId/ownerUserId/kbId` **不进 RAGFlow**(仅 muse 本地做入参校验/审计/归因。RAGFlow 单 baseUrl + 单 API key、全租户共用、**完全不感知 muse 租户**。检索链 `MuseKnowledgeRetrievalApiImpl`:双门 + `selectActiveDatasetByKbId` 只控制"能否拿到这个 dataset_id",一旦 dataset_id 进入检索,`document_ids` 传 null → **整库返回全部 chunk**`parseChunks` 回来只按 dataset_id 贴归因元数据,**对 chunk 无任何按 `request.tenantId()` 的二次过滤**。muse 的租户隔离只能保证"A 拿不到指向别人 dataset 的 binding"**一旦设计上让两租户 binding 指向同一 dataset_id这道防线即被绕过**。两份既有 review 已佐证(`docs/agent-specs/2026-06-23-market资产物化-review.md:94``docs/agent-specs/2026-06-23-ai-knowledge-retrieval-review.md:26`)。**结论**U0 不能是"验证能不能共享"——已知不能U0 是"**要共享就必须先补 chunk 级隔离**metadata_filter 把 chunk 限定到本安装授权子集,或 document_ids 限定),补到位且验证通过才允许共享"。
**事实 4 — 评审版/任务背景里提到的 `muse_source_propagation_target` 表在代码里不存在U4 回滚不能引用它。**
全仓 SQL/Java 搜 `source_propagation_target` / `SourcePropagationTarget` 零命中。真实落地的 knowledge 侧来源传播是 `muse_knowledge_source_event` + `muse_knowledge_source_binding_projection``V14__...:203/245`unbind 软删走 `sourceBindingProjectionMapper.markDeletedByBindingId``MuseKnowledgeBindingService.java:204`,且 `deleted``@TableLogic``setSql("deleted = true")` 才真软删——见模块 `.agent` 2026-06-19 记录)。**结论**U4 的"召回/下架回滚已物化实体"必须基于这套真实机制设计,不得假装 `muse_source_propagation_target` 存在;若需要"market 召回→knowledge 停用 installed_ref"的跨 BC 传播,要么复用 knowledge source event 入口,要么如实标为"本期缺、需新增传播接口"的开放项。
> 这四条不推翻 B' 的方向,但把 B' 的真实成本从评审版的"中"上修:**核心增量 = 新跨 BC 读 API事实 2+ kb_id 去污染(事实 1+ chunk 级隔离(事实 3仅共享分支需要**。下面的单元据此划分。
---
## 1. 总览:实现单元依赖图与决策门
```mermaid
flowchart TD
U0{"U0 spike 硬前置门<br/>chunk 级隔离能否补上?<br/>(已知:当前共享必越权)"}
U0 -->|"绿: 隔离补到位<br/>(metadata_filter/document_ids 真隔)"| SHARE["分支S: 共享发布者 dataset"]
U0 -->|"红/暂不投入隔离"| COPY["分支C: 每安装者独立复制 dataset<br/>(局部 C, 隔离天然干净)"]
U0 -->|"产品优先级不在此"| HOLD["分支H: 暂缓<br/>(只做诚实标注未开放)"]
SHARE --> U1
COPY --> U1
U1["U1 数据模型<br/>installed_ref kb 行落点 + dataset 关联策略<br/>判定是否需 V32 迁移(人类门)"]
U1 --> U2
U2["U2 物化落点<br/>绑定路径建本地 kb 行 + dataset 关联<br/>kb_id 去污染 + 新增 MarketAssetSourceApi 跨 BC 读<br/>幂等 + kbId 跨 BC 存在性校验"]
U2 --> U3
U3["U3 检索打通<br/>selectActiveDatasetByKbId 返非空→命中<br/>消除 no_dataset, 授权快照沿用 ADR-020 VARCHAR"]
U3 --> U4
U4["U4 验证<br/>真 PG IT(物化→检索命中) + studio e2e<br/>+ 召回/下架回滚 + 跨租户隔离回归"]
style U0 fill:#fde2e2,stroke:#c0392b,stroke-width:3px
style SHARE fill:#fff6d6,stroke:#b8860b
style COPY fill:#e2f7e8,stroke:#27ae60
style HOLD fill:#eef3fb,stroke:#3b6fb6
```
**数据流B' 共享分支,物化后)**
```mermaid
flowchart LR
A["用户跳转→knowledge 绑定路径<br/>createKnowledgeBindingPrecheck/createKnowledgeBinding"]
A --> B["消费 handoff token(原有)<br/>consumeMarketHandoff"]
B --> C["✅U2: 调 MarketAssetSourceApi.getAssetSource(assetId)<br/>拿发布者 kbId"]
C --> D["✅U2: 建本地 muse_knowledge_base 行<br/>kb_type=installed_ref<br/>source_market_asset_id=assetId<br/>license_snapshot_id=授权快照"]
D --> E["✅U2: 建 muse_knowledge_ragflow_binding<br/>kb_id=新建本地 kbId<br/>ragflow_dataset_id=发布者 dataset(共享)"]
D --> F["✅U2: binding.kb_id = 新建本地 kbId<br/>(去污染:不再是 assetId)"]
F --> G["检索: selectActiveDatasetByKbId(本地kbId)<br/>✅U3 返非空"]
E --> G
G --> H["✅U0 共享分支: metadata_filter/document_ids<br/>把 chunk 限定到本安装授权子集"]
H --> I["🟢 命中且隔离, AI 生成含 grounding"]
style C fill:#fff6d6,stroke:#b8860b
style D fill:#e2f7e8,stroke:#27ae60
style E fill:#e2f7e8,stroke:#27ae60
style F fill:#e2f7e8,stroke:#27ae60
style H fill:#fde2e2,stroke:#c0392b
style I fill:#e2f7e8,stroke:#27ae60
```
---
## 2. 实现单元U0…U4
> 文件路径均为 repo 相对路径。每个单元含 Goal / Files / Approach / Test scenarios输入+预期)/ Verification。测试场景"输入"指具体调用入参或数据状态,"预期"指可断言的可观测结果。
### U0 · spikechunk 级隔离硬前置门(决定共享/复制/暂缓分支)
**Goal**:在落任何物化代码前,回答唯一硬问题——**"共享发布者同一 RAGFlow dataset"在当前检索链下能否做到租户/授权隔离**。调查已先验给出方向性结论(当前**必越权**,见 §0 事实 3U0 要做的是**用最小成本把这个结论钉成可复现的证据 + 给出"补隔离"的可行手段验证 + 输出决策门**,让人类据此在"共享(S)/复制(C)/暂缓(H)"间拍板。U0 **不产出物化业务代码**,只产出证据与结论。
**Files只读 + spike 验证,不改业务代码)**
- 只读核验:`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/main/java/cn/iocoder/muse/module/knowledge/application/muse/facade/HttpRagFlowKnowledgeRuntimeClient.java`retrieveChunks `:143-158`,确认 HTTP body 不含 tenant、document_ids/metadata_filter 透传形态)
- 只读核验:`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/main/java/cn/iocoder/muse/module/knowledge/api/MuseKnowledgeRetrievalApiImpl.java``parseChunks:170-198` 确认无 chunk 级租户过滤)
- spike 单测(验证层 A零外部、零业务改动`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/test/java/cn/iocoder/muse/module/knowledge/api/MuseKnowledgeRetrievalApiImplTest.java`(如已存在则加 case否则新建 spike 专用测试类——mock `RagFlowKnowledgeRuntimeClient` 返回"混入他租户 magic token 的 chunk 集",断言当前 `retrieveForWork` 把混入 chunk 原样返回(**证伪"有租户过滤"**)。
- spike 活体(验证层 B真 RAGFlowmini-infra 100.64.0.8 在线):参照 `.../application/muse/facade/P1rRagFlowLiveAcceptanceIT.java` 范式,**只读地**对一个真 dataset 调 `/api/v1/retrieval`document_ids=null确认 RAGFlow 整库返回、不按任何 muse 维度过滤。
**Approach / 验证方法(多租户 RAGFlow dataset 隔离怎么验)**
1. **造数据**seed 两个真实租户 T_A、T_B注意当前是单租户 + `tenant_id DEFAULT 0`,需先建第二租户并确认 yudao 租户拦截器在两租户上下文都生效);让"发布者"建一个 KB → 真上传一份文档 → RAGFlow 真生成出 `ragflow_dataset_id = D`
2. **共享指向**:在 T_A、T_B 各建一条 installed_ref bindingspike 阶段可直接 SQL 注入读模型行,不依赖 U2 代码),两条的 `muse_knowledge_ragflow_binding.ragflow_dataset_id` **都 = D**(模拟 B' 共享),各自给齐 `bindingScope=search` + `authorizationSnapshotId` + work 归属。
3. **掺入可识别越权探针**:往 D 里放一段"只有发布者/T_B 该看"的、含唯一 magic token 的 chunk**只共享同一份内容验不出问题,掺入差异内容才是验越权的核心**)。
4. **各自检索**:以 T_A 租户上下文调 `MuseKnowledgeRetrievalApi.retrieveForWork`A 的 work + question 命中那段 magic chunk看返回是否含本不该 A 看的内容T_B 同样跑一遍。
5. **判定(决策门)**
- 若 A 能检索到 D 的 magic chunk预期会**证实共享即越权** → 共享分支(S)**不可直接落地**,必须先做第 6 步的"补隔离"并复验通过,否则只能走复制(C)或暂缓(H)。
- 若已实现"补隔离"且 A 检索不到 B 的 magic chunk、且各自只拿到本授权子集 → 共享分支(S)**放行**。
6. **"补隔离"可行手段验证**(共享分支前置):验证以下任一路径能把 chunk 限定到"本安装授权子集"
- (a) `RetrieveChunksCommand.metadataFilter` —— 物化时给 RAGFlow 文档打租户/安装维度元数据,检索时按 `metadata_filter` 过滤;需验 RAGFlow `/api/v1/retrieval``metadata_filter` 语义是否真生效(**用 context7/RAGFlow 官方文档核对 + 活体打一发**,不要靠假设)。
- (b) `RetrieveChunksCommand.ragflowDocumentIds` —— 检索时把 document_ids 限定到本安装可见文档集(需本地维护"installed_ref→可见 documentId 集"映射)。
- 两条都不可行 → 共享分支判负,回落复制(C)。
**决策门输出U0 交付物)**:一页结论——(i) 当前共享越权与否的可复现证据(单测层 + 活体层各一);(ii) "补隔离"手段 (a)/(b) 哪条可行、成本几何;(iii) 明确给人类的三选一建议:**S补隔离后共享/ C每安装者复制 dataset/ H暂缓**。后续 U1U4 据此分支。
**Test scenariosspike 自身的断言)**
- 单测层输入mock RAGFlow 返回 `[{dataset_id:D, content:"含 MAGIC_B"}, {dataset_id:D, content:"公共内容"}]`,以 T_A 上下文 `retrieveForWork`|预期:返回结果**包含** `MAGIC_B`(证伪租户过滤,钉死越权事实)。
- 活体层|输入:真 dataset D含 magic 文档),`/api/v1/retrieval` document_ids=null预期响应 `data.chunks[]` 含 magic chunkRAGFlow 整库返回,无 muse 维度过滤)。
- 补隔离层(仅当尝试 metadata_filter输入检索带 `metadata_filter={install_id: A}`|预期:返回仅含 A 授权子集、**不含** `MAGIC_B`(若 RAGFlow 该语义生效则共享可行)。
**Verification**
- 单测:`cd muse-cloud && mvn -pl muse-module-knowledge/muse-module-knowledge-server -am test -Dtest=MuseKnowledgeRetrievalApiImplTest`spike case 绿=越权事实成立)。
- 活体:按模块 `.agent` / `.agents/knowledge` 起栈RAGFlow env 配齐mini-infra 在线),`P1rRagFlowLiveAcceptanceIT` 同款 live profile 跑只读检索探针。
- **凭据红线**RAGFlow base-url/api-key 由 env 注入(`muse.knowledge.ragflow.*`spike 脚本与文档**不打印任何 key 值**(过滤 `password|secret|token|sk-`);活体只读检索,**不写 muse_slice_live**。
---
### U1 · 数据模型installed_ref kb 行落点 + dataset 关联策略 + 迁移判定
**Goal**定义物化产物的数据形态——installed_ref 知识库行写哪些列、dataset 如何关联(共享 or 复制,据 U0 分支),并**判定是否需要新 Flyway 迁移**。
**Files**
- 只读核验地基:`muse-cloud/sql/muse/V5__init_knowledge_schema.sql``muse_knowledge_base` 第 428 行:`kb_type`/`source_market_asset_id`/`license_snapshot_id` 已备)、`muse-cloud/sql/muse/V14__extend_knowledge_ragflow_real_api_schema.sql``muse_knowledge_ragflow_binding` `:109-149`,含唯一索引 `uk_muse_knowledge_ragflow_binding_active` = `(tenant_id, kb_id) WHERE status='active' AND document_id IS NULL AND deleted=FALSE`
- DO 对照:`.../knowledge/dal/dataobject/muse/MuseKnowledgeRagflowBindingDO.java``.../dal/dataobject/muse/MuseKnowledgeBaseDO.java`
- 可能的新迁移(**仅当判定需要**`muse-cloud/sql/muse/V32__<name>.sql`
**Approach**
1. **installed_ref kb 行字段映射**(写入由 U2 执行U1 只定字段语义):
- `kb_type = 'installed_ref'`V5 已支持该取值,现状从不写本表)。
- `source_market_asset_id = assetId`(承载"来源市场资产"溯源;注意当前为 BIGINTassetId 为数值,类型相容)。
- `license_snapshot_id` —— **类型核查项**V5 该列是 BIGINT。market 授权快照主键 `muse_market_authorization_snapshot.id` 是 BIGINTinstall 存的是数值主键,见 `MarketInstallServiceImpl.java:85` `setAuthorizationSnapshotId(authorization.getId())`),故此处填**授权快照行数值主键**与 BIGINT 相容;**但若决定改存 ADR-020 字符串 envelope`rpe-local-<uuid>`)则需 widen**(见决策点 D3。U1 判定:**初版填数值主键、不 widen**(与 install 侧一致,避免牵动 market 两列 BIGINT 的独立议题)。
- `owner_user_id = 安装者 loginUserId``status='active'``active_version=1``name/description` 取自资产摘要(经 U2 的 MarketAssetSourceApi 带回或用占位)。
2. **dataset 关联策略(据 U0 分支)**
- **分支 S共享**:建一行 `muse_knowledge_ragflow_binding``kb_id=新建本地 installed_ref kbId``ragflow_dataset_id=发布者 dataset id``document_id=null``status='active'``active_version=1`。形态参照 `MuseKnowledgeDocumentService.persistDatasetBinding``:321-347`,数据集级行 `setDocumentId(null)`)。**唯一索引落点核查**`uk_...active``(tenant_id, kb_id)`installed_ref 用**安装者租户内新建 kbId**,与发布者 kb 的 binding 不撞键(评审版担心的"沿用 sourceId 撞键"在去污染后消失)。
- **分支 C复制**:建新 dataset`ragFlowClient.createDataset`+ 复制文档/重解析binding 指向新 dataset。**这是异步重活**,本期若选 C 需评估是否纳入(任务背景倾向 S/暂缓C 留作 U0 红时的兜底,且复制本身另起子任务,不在本 plan 细化)。
3. **迁移判定(人类门 yes/no**
- **分支 S 初版(填数值 license_snapshot_id、kb_type/source_market_asset_id 复用 V5 已备列)→ 不需要新 Flyway 迁移**。所有写入落在 V5/V14 已有列上DO 已有对应字段。
- **仅以下情形需 V32DDL 人类门)**(a) 决定 `license_snapshot_id` 改存字符串 envelope → `ALTER ... TYPE VARCHAR(128)`(b) 需要新增"installed_ref→可见 documentId 集"映射表U0 选 metadata_filter 路径 (b) 时);(c) 需要 market 召回→knowledge 传播的新读模型列。**这三项均非 B' 共享初版必需**,故**初版判定:不需要新迁移**。
**判定结论:是否需新 Flyway 迁移 = 否B' 共享/复制初版均落在 V5+V14 已备列;新 V32 仅在上述 (a)/(b)/(c) 触发,届时标 DDL 人类门 + V32 版号 + `_test` 库铁律)。**
**Test scenarios数据模型层落到 U2 实现后由 IT 验U1 阶段为设计断言)**
- 字段映射输入assetId=X 的 market_kb 安装+绑定|预期:新建一行 `muse_knowledge_base``kb_type='installed_ref' AND source_market_asset_id=X AND owner_user_id=安装者 AND status='active'`
- dataset 关联(S)|输入:发布者 kb 的 dataset=D预期新建一行 `muse_knowledge_ragflow_binding``kb_id=新建本地kbId AND ragflow_dataset_id=D AND document_id IS NULL AND status='active'`,且不违反 `uk_...active`
**Verification**U1 本身不跑测试(设计单元);其正确性由 U2/U3 的 IT 覆盖。若触发 V32`cd muse-cloud && mvn -pl muse-server flyway:migrate`**仅指向 `_test` 库,绝不指 muse_slice_live**)验证迁移可应用。
---
### U2 · 物化落点:绑定路径建本地 kb 行 + dataset 关联 + kb_id 去污染 + 跨 BC 读
**Goal**:在 knowledge 目标域绑定路径里,把"只写引用绑定"升级为"建出可用的本地 installed_ref 知识库实体 + dataset 关联",替代现 `kbId=parseLong(sourceId)` 裸引用;做到幂等(重复 install/bind 不重复造实体)+ 补 kbId 跨 BC 存在性校验。
**Files**
- 主改:`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/main/java/cn/iocoder/muse/module/knowledge/application/muse/MuseKnowledgeBindingService.java``createKnowledgeBindingPrecheck:71-127``createKnowledgeBinding:129-177``consumeMarketHandoff:273-284`
- 新增跨 BC 读端口(事实 2
- 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`(新)
- 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` 等,**经 market 自己的 .dal**,不让 knowledge 越界)
- 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 读端口**(事实 2 前置):`MarketAssetSourceApi.getAssetSource(Long assetId)``{sourceOwner, sourceType, sourceId(=发布者kbId), publisherUserId, tenantId}`,实现读 `muse_market_asset``source_id`/`asset_type`/`publisher_id`)。范式同 `MarketHandoffTokenApi`(本地 Beanknowledge 注入它而非读 market.dal。**保证不破 ArchUnit `bc-boundaries`**。
2. **kbId 跨 BC 存在性校验 + 去污染**market_kb 绑定时,`sourceId` 当作 **assetId** 处理(不再 `parseLong` 当 kbId
-`MarketAssetSourceApi.getAssetSource(assetId)` 校验资产存在且为 `knowledge_base` 类型(不存在/类型不符 → fail-closed 抛错,替代现状裸信客户端)。
- 拿到发布者 kbId共享分支经……拿发布者 dataset id。**注意**knowledge 读"发布者 kb 的 dataset"也跨 owner——发布者 kb 与安装者不同 owner/可能不同 tenant现有 `selectActiveDatasetByKbId` 带租户拦截器,需确认能在物化事务的租户上下文里读到发布者 dataset**这是共享分支的一个未决技术点**:要么 MarketAssetSourceApi 直接把发布者 dataset id 一并带回[由 market 在其能力内读取 knowledge 投影,但 dataset 属 knowledge 域→更适合 knowledge 内部按发布者 kbId + 发布者 tenant 读],要么新增 knowledge 内部"按 kbId+tenant 读 active dataset"的 `@TenantIgnore` 受控查询。**U2 需在实现时定夺并在 plan 复审时回填**)。
3. **建本地 installed_ref 实体(落点:`createKnowledgeBinding` 同一 `@Transactional` 事务内)**
- 在写 `muse_knowledge_binding` 之前,先 insert `muse_knowledge_base`(字段按 U1 §2.1),拿到**新建本地 kbId**。
- 共享分支insert `muse_knowledge_ragflow_binding``kb_id=新建本地kbId``ragflow_dataset_id=发布者dataset``document_id=null`),形态仿 `persistDatasetBinding`
- **去污染**`binding.setKbId(新建本地kbId)`(不再是 assetId`precheck.setKbId(...)` 同步——precheck 阶段已需建实体还是 bind 阶段建,二选一:**建议放 `createKnowledgeBinding`(用户确认绑定的写事实点)**precheck 仅校验资产存在性,避免 precheck 失败留下孤儿 kb 行。
- **来源溯源**`source_market_asset_id=assetId``license_snapshot_id=授权快照主键`
4. **幂等**(重复 install/bind 不重复造实体):
- 绑定本身已有命令回放(`commandService.reserveCommand`/`recordCompleted`+ `uk_muse_knowledge_binding_work_kb (tenant_id, work_id, kb_id)`——去污染后 kb_id 是稳定的本地主键,但**首次绑定才知道本地 kbId**,故幂等键需落在"(安装者, 市场资产)"维度:建 installed_ref kb 行前先 `SELECT ... WHERE source_market_asset_id=assetId AND owner_user_id=安装者 AND deleted=false`,命中则复用既有 installed_ref kbId不重复建。
- dataset binding 复用 `persistDatasetBinding``DataIntegrityViolationException`→回读模式应对并发。
5. **事务原子性**:物化与"消费 handoff token + 写绑定事实"在**同一事务**(现状 `consumeMarketHandoff` 已沿用调用方 `@Transactional`),失败整体回滚,无中间态。契合 `架构-02` §7 / `流程-02B` §13"目标 owner 自己消费 token + 写 owner 事实"。
**Test scenarios输入+预期)**
- 首次安装绑定输入T_A 安装 assetId=Xknowledge_base 类型,发布者 kb=K、dataset=D`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, document_id=null, status=active)`
- 幂等-重复绑定|输入:同一 (A, X) 再次 install+bind不同 commandId 或回放)|预期:**不新建第二行 installed_ref kb**(复用 K2不违反 `uk_...active`;不抛 500。
- 资产不存在|输入:`sourceId` 指向不存在的 assetId预期`MarketAssetSourceApi` 返空 → fail-closed 抛 `KNOWLEDGE_MARKET_HANDOFF_UNAVAILABLE` 或新增明确错误码,**不写任何 kb/binding 行**(替代现状裸信客户端)。
- 资产类型不符输入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'`(嵌入式 H2 往返,覆盖去污染 + 幂等 + fail-closed
- **改了 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`
---
### U3 · 检索打通:消除 no_dataset命中
**Goal**:物化后,检索第四门 `selectActiveDatasetByKbId(本地kbId)` 返非空 → 命中(消除 `no_dataset` 静默省略);授权快照沿用 ADR-020 VARCHAR envelope不回退 BIGINT/parseLong。
**Files**
- 主路径(多半**无需改**,靠 U2 数据正确即自然打通):`.../knowledge/api/MuseKnowledgeRetrievalApiImpl.java`(第四门 `:122-127``parseChunks:170-198`
- 仅共享分支 U0 选 metadata_filter/document_ids 时需改:`RetrieveChunksCommand` 构造处(`MuseKnowledgeRetrievalApiImpl.java:148-151`+ `HttpRagFlowKnowledgeRuntimeClient.retrieveChunks:143-158`(透传 metadata_filter/document_ids
**Approach**
1. **共享分支基线打通**U2 把 `binding.kb_id` 改成本地真实 kbId、并建好数据集级 `muse_knowledge_ragflow_binding(kb_id=本地kbId, ragflow_dataset_id=发布者dataset)` 后,检索链第四门 `selectActiveDatasetByKbId(本地kbId)` 自然返非空 → `no_dataset` 消除 → 进 RAGFlow 检索。**这一步 RetrievalApiImpl 代码无需改动**(这是 B' 共享"地基已备"的真实体现)。
2. **授权快照沿用 ADR-020**:检索门读 `binding.getAuthorizationSnapshotId()`(已是 VARCHARV14 `muse_knowledge_binding.authorization_snapshot_id` 已 wideninstalled_ref binding 写入时**沿用 VARCHAR 字符串承载**U2 写 binding 时不改这一列类型。chunk §5.3 合同字段 `authorizationSnapshot` 透传字符串,不 parseLong。
3. **隔离(仅共享分支,且仅 U0 绿后)**:把 U0 验证通过的隔离手段落到 `RetrieveChunksCommand`——metadata_filter 或 document_ids 限定本安装授权子集。**U0 红则本单元不走共享、检索直接对接复制分支的独立 dataset天然隔离**。
**Test scenarios输入+预期)**
- no_dataset 消除输入U2 物化后的 installed_ref本地kbId=K2, 关联 dataset=Dwork 绑定含 search 用途 + 授权快照,`retrieveForWork(question 命中 D 内容)`|预期:结果**非** `empty("no_dataset")``authorized` 非空,进入 RAGFlow 检索;对照现状(同输入必返 `no_dataset`)。
- 命中 grounding输入同上RAGFlow 返含 D chunk预期`RetrievalResult.ok(chunks)`chunk 带齐 §5.3 字段sourceOwner/authorizationSnapshot 等),`authorizationSnapshot` 为字符串 envelope。
- 隔离(共享分支,U0绿后)输入T_A 检索共享 dataset DD 含 T_B magic chunk预期返回**不含** T_B magic chunk隔离手段生效
- 授权快照字符串输入installed_ref binding 的 `authorization_snapshot_id``rpe-local-<uuid>` 形态预期检索门正常放行、chunk 字段透传该字符串,无 NumberFormatException、不落 null。
**Verification**
- 单测:`mvn -pl muse-module-knowledge/muse-module-knowledge-server -am test -Dtest=MuseKnowledgeRetrievalApiImplTest`mock RAGFlow覆盖 no_dataset 消除 + 字符串授权快照 + 隔离过滤断言)。
- 活体并入 U4 端到端。
---
### U4 · 验证:真 PG 端到端 + studio e2e + 回滚 + 隔离回归
**Goal**:用真实证据证明"市场 KB 安装→绑定→物化→检索命中"端到端可真用,且召回/下架时已物化实体能回滚、跨租户隔离不被共享绕过。
**Files**
- 真 PG IT`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/test/java/cn/iocoder/muse/module/knowledge/.../P1rMarketInstallKbMaterializationIT.java`(参照既有 `P1r*IT` 范式 + `P1rRagFlowLiveAcceptanceIT` live 范式)
- studio e2e`muse-studio/e2e/market-install-kb-retrieval.spec.ts`(参照 `muse-studio/e2e/knowledge-bindings.spec.ts` + `market-install.spec.ts`
- 回滚相关(据真实机制,事实 4`.../knowledge/application/muse/MuseInstalledKnowledgeBaseService.java`disable/delete 路径)+ knowledge source event/projection`muse_knowledge_source_event`/`muse_knowledge_source_binding_projection`
**Approach / Test scenarios输入+预期)**
1. **知识库 B' 端到端(真 PG + 真 RAGFlow**|输入:安装市场 KB 资产 → 作品绑定(物化 installed_ref + dataset 关联)→ 触发检索 `retrieveForWork`预期第四门拿到发布者dataset → RAGFlow chunk 真命中 → 生成上下文含该来源 grounding对照现状`no_dataset` 省略)。
2. **幂等**|输入:重复 install + 重复 bind回放 + 不同 commandId 两路)|预期:物化实体不重复创建(命令回放 + (安装者,资产) 幂等键 + 唯一键DB 中 installed_ref kb 仅一行。
3. **跨租户隔离回归(共享分支必跑,承 U0**输入T_A、T_B 各装同一发布者 KB共享 dataset DD 含两租户差异内容)→ 各自检索预期A 检索**不越权读** B 的私有 chunk隔离手段生效**若走复制分支则验各自独立 dataset 天然隔离**。
4. **回滚/来源传播(基于真实机制,不引用不存在的 `muse_source_propagation_target`**|输入:发布者资产被 admin 召回/下架market `adminRecallAsset`/`adminDelistAsset`)|预期:已物化 installed_ref 实体被**阻断新使用**(不自动删,符合 `架构-02` 来源事件"不删除正式知识"语义)。**实现路径待定项**market 召回当前对目标 owner 的传播是 fail-closed 记录式(见 market `.agent` `adminRecallAsset` 描述"目标 owner 传播 fail-closed"knowledge 侧消费"market 召回→停用 installed_ref"的入口**本期可能缺**——U4 需如实验证现状传播是否触达 knowledge installed_ref若不触达则标为**开放项**"召回回滚未自动化,需新增 market→knowledge 传播接口或手动停用"),不假装已闭环。
5. **删除/停用已物化实体**|输入:用户 `disableInstalledKnowledgeBase`/`deleteInstalledKnowledgeBase`预期installed_ref binding 软删(`@TableLogic` + `setSql("deleted = true")`,见 §0 事实 4检索读回排除物化的 dataset binding共享分支**不删发布者 dataset**(只解除本安装的引用),复制分支需评估是否清理本安装独占 dataset。
6. **机械门禁**输入ArchUnit `BcBoundaryArchTest` + 覆盖 JSON 门|预期:物化只经目标 owner facade`MuseKnowledgeBindingService` + `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`
---
## 3. 决策点(待人类拍板,按优先级)
| # | 决策点 | 选项 | 倾向 | 阻塞谁 |
|---|---|---|---|---|
| **D1** | **dataset 共享 vs 复制**最硬U0 门) | S 补隔离后共享 / C 每安装者复制 / H 暂缓 | **先看 U0 spike 证据再拍**已知当前共享必越权S 需先投入 chunk 级隔离metadata_filter/document_ids并验证不投入则 C 或 H | U1U4 全部分支 |
| D2 | 物化时机 | 绑定同步 / 惰性首用 | **绑定同步**(与 token 消费原子,不进高频热路径) | U2 |
| D3 | `license_snapshot_id` 承载 | 数值主键(不 widen/ 字符串 envelope需 V32 | **数值主键**(与 install 侧一致,避免牵动 market 两列 BIGINT 独立议题) | U1 是否需 V32 |
| D4 | 跨 BC 读"发布者 dataset"的实现路径 | MarketAssetSourceApi 带回 dataset id / knowledge 内部 `@TenantIgnore` 受控查发布者 kb dataset | U2 实现时定夺并回填本 plan | U2/U3 共享分支 |
| D5 | 召回/下架回滚自动化 | 复用现有 market→目标 owner 传播 / 本期标开放项手动停用 | 先验现状传播是否触达 installed_ref**不假装已有 `muse_source_propagation_target`** | U4 |
---
## 4. 风险
- **R1最高跨租户越权**:共享分支若未补 chunk 级隔离即上线 → A 读到 B 私有内容。**缓解**U0 硬门 + U4 隔离回归U0 红绝不放行共享。当前单租户运行使风险"暂不触发",但这是部署现状非代码隔离,不能作为放行理由。
- **R2 kb_id 去污染的回归面**`kb_id` 从 assetId 改本地主键,牵动检索/读回/installed-KB 列表(`MuseInstalledKnowledgeBaseService``muse_knowledge_binding`)等所有读 kb_id 的路径。**缓解**U2/U4 覆盖读回链;改动限定 market_kb 绑定路径user_kb/global_kb 绑定不动。
- **R3 跨 BC 读端口扩面**:新增 `MarketAssetSourceApi` 是新的 knowledge→market 依赖。**缓解**:严格本地 Bean + ArchUnit 守边界;只读不写。
- **R4 发布者改版/撤权冲击安装者**:共享分支下发布者改 dataset/撤权直接影响安装者可用性。**缓解**installed_ref 的 `license_snapshot_id` 钉授权来源召回传播D5阻断新使用。这是 B' 相对 C 的固有取舍,非 bug。
- **R5 复制分支C的异步重活**:若 U0 红回落 C文档复制+重解析是异步长任务(失败/重试/幂等/部分成功),**不在本 plan 细化,需另起子任务**。
---
## 5. 回滚(本 plan 执行后如何撤回)
- **代码回滚**U2/U3 改动集中在 `MuseKnowledgeBindingService` + 新增 `MarketAssetSourceApi`market-api/server+ 可选 `RetrievalApiImpl` 隔离透传。revert 这些 commit 即回到"只写引用绑定"现状,检索回落 `no_dataset` 静默省略(功能不可用但不报错,与现状一致)。
- **数据回滚**:已物化的 installed_ref kb 行 + dataset binding 经软删(`deleted=true`)停用;**共享分支不删发布者 dataset**(本就共享,无副作用);复制分支需清理本安装独占 dataset。
- **迁移回滚**:初版不引入 V32D3 选数值主键),无迁移需回滚;若触发 V32 则按 Flyway 不可逆约定,回滚靠新 forward 迁移(且 `_test` 库先验)。
---
## 6. Scope 边界(明确不做什么)
- **KB 限定**:本 plan **只做知识库KB物化**
- **agent 下一阶段**智能体agent物化**不在本 plan**——`muse_agent``market_installed` 落地 + 运行期 `requireVisibleAgent` 放行(`MuseAgentSlotServiceImpl`/`MuseAiTaskServiceImpl`)是独立的下一阶段任务,本 plan 不触碰 ai 模块。
- **install 解耦不动**`MarketInstallServiceImpl.installMarketplaceAsset` 维持"只记账、`targetFactsWritten=false`"现状,**不在 install 侧物化**——物化落点严格在 knowledge 目标域绑定路径。这是对的解耦,不改。
- **handoff 非物化桥**handoff token 只负责跳转 + 被目标域消费,**不负责物化**`MarketHandoffTokenApi.verify/consume` 不动)。
- **复制分支(C)细节不展开**C 仅作 U0 红时的兜底方向标注,文档复制/重解析的异步实现另起子任务。
- **market 两列 BIGINT 议题不纳入**`muse_market_installation.authorization_snapshot_id`/`source_snapshot_id` 仍 BIGINT 是独立契约议题(临时-02 §3.3 已列备查),本 plan 不处理。
---
## 7. 关联阅读
- 方案权衡与决策收束(本 plan 的 WHAT[`临时-02-market-install下游物化方案.md`](临时-02-market-install下游物化方案.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)