oh-my-muse/design-docs/临时-03-market-install-KB物化执行plan.md
lili 76a7e0d312 docs(market): KB 物化执行版 plan(临时-03、待 D1 拍板后执行)
承临时-02 拍板(B' installed_ref + KB 先行)出 KB 物化执行版 plan,拆 U0-U4 可独立验证实现单元。出 plan 时一手代码核验揪出四条改变工作量/性质的硬事实(均 spot-check 坐实):①kb_id 当前被 market assetId 污染(KnowledgeHandoffLanding sourceId=assetId)→物化须去污染改写;②assetId→发布者 dataset 反查链跨 BC 断(market.api 仅 HandoffTokenApi)→需新增 MarketAssetSourceApi 本地读端口;③多租户共享同一 RAGFlow dataset 必越权(retrieveChunks body 不含 tenant+document_ids=null 全量返回、parseChunks 无 chunk 租户二次过滤)=安全红线→U0 从"验证能否共享"转为"共享前先补 chunk 级隔离"硬门;④muse_source_propagation_target 表零命中→U4 回滚改基于真实 source_event 机制。

物化落点严格在 knowledge 绑定路径(非 install、handoff 非物化桥、安装解耦不动)。初版判定不需新 Flyway 迁移(V5/V14 已备列)。决策点 D1(共享 S/复制 C/暂缓 H)待人类先看 U0 spike 证据再拍。两图文(text+mermaid + html 人读图)+大纲注册。

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

39 KiB
Raw Blame History

临时-03 · Market Install KB 下游物化执行 PlanHOW供 review 后执行)

一句话:拍板已定——做 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。


0. 执行前必读:四条把工作量钉死的硬事实(基于真实代码,非评审版推断)

评审版(临时-02把方向定对了但执行前有四条一手代码事实会直接改变单元划分与工作量必须先摆明每条附证据行号为只读核验所得

事实 1 — 知识库侧 kb_id 字段当前是被污染的,存的是 market assetId不是任何真实 muse_knowledge_base.id MuseKnowledgeBindingService.createKnowledgeBindingPrecheck 第 110 行 precheck.setKbId(parseLong(reqVO.getSourceId())),而 studio 传入的 sourceId 实为 market assetIdmuse-studio/src/features/handoff/owners/KnowledgeHandoffLanding.tsx:44 sourceId: assetId,源头 HandoffLandingPage.tsx:29 取自 URL ?assetId=)。绑定时这个值原样进 muse_knowledge_binding.kb_idcreateKnowledgeBinding 第 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_idV6__init_market_schema.sql:10BIGINT在 admin 审核物化资产时确实写入发布者本地 kbIdAdminMarketReviewServiceImpl.java:454 asset.setSourceId(longValue(draftSnapshot.get("sourceId")))sourceId 源自发布者上架请求 MarketPublishDraftReqVO.sourceId)。但 knowledge 域不能直读 market 的 .dalbc-boundaries ArchUnit而 market 对外 API 包 cn.iocoder.muse.module.market.api目前只有 MarketHandoffTokenApiverify/consume,没有任何"按 assetId 查源对象"的读接口。结论:共享方案必须新增一个跨 BC 读端口(如 MarketAssetSourceApi.getAssetSource(assetId) → {sourceOwner, sourceType, sourceId=发布者kbId, publisherUserId, tenantId},本地进程内 Bean、与 MarketHandoffTokenApi 同范式、不加 Feign。这是 U2"共享分支"的隐藏前置工作,评审版未列。

事实 3 — 按当前代码,多租户共享同一 ragflow_dataset_id 必然越权(这是 U0 性质的根本改变)。 RAGFlow 检索 HTTP bodyHttpRagFlowKnowledgeRuntimeClient.retrieveChunks:143-158)只发 dataset_ids + question + top_k + similarity_threshold + metadata_filtertenantId/ownerUserId/kbId 不进 RAGFlow(仅 muse 本地做入参校验/审计/归因。RAGFlow 单 baseUrl + 单 API key、全租户共用、完全不感知 muse 租户。检索链 MuseKnowledgeRetrievalApiImpl:双门 + selectActiveDatasetByKbId 只控制"能否拿到这个 dataset_id",一旦 dataset_id 进入检索,document_ids 传 null → 整库返回全部 chunkparseChunks 回来只按 dataset_id 贴归因元数据,对 chunk 无任何按 request.tenantId() 的二次过滤。muse 的租户隔离只能保证"A 拿不到指向别人 dataset 的 binding"一旦设计上让两租户 binding 指向同一 dataset_id这道防线即被绕过。两份既有 review 已佐证(docs/agent-specs/2026-06-23-market资产物化-review.md:94docs/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_projectionV14__...:203/245unbind 软删走 sourceBindingProjectionMapper.markDeletedByBindingIdMuseKnowledgeBindingService.java:204,且 deleted@TableLogicsetSql("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. 总览:实现单元依赖图与决策门

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' 共享分支,物化后)

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.javaretrieveChunks :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.javaparseChunks: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/retrievaldocument_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.retrieveForWorkA 的 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/retrievalmetadata_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=MuseKnowledgeRetrievalApiImplTestspike 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.sqlmuse_knowledge_base 第 428 行:kb_type/source_market_asset_id/license_snapshot_id 已备)、muse-cloud/sql/muse/V14__extend_knowledge_ragflow_real_api_schema.sqlmuse_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 字符串 enveloperpe-local-<uuid>)则需 widen(见决策点 D3。U1 判定:初版填数值主键、不 widen(与 install 侧一致,避免牵动 market 两列 BIGINT 的独立议题)。
    • owner_user_id = 安装者 loginUserIdstatus='active'active_version=1name/description 取自资产摘要(经 U2 的 MarketAssetSourceApi 带回或用占位)。
  2. dataset 关联策略(据 U0 分支)
    • 分支 S共享:建一行 muse_knowledge_ragflow_bindingkb_id=新建本地 installed_ref kbIdragflow_dataset_id=发布者 dataset iddocument_id=nullstatus='active'active_version=1。形态参照 MuseKnowledgeDocumentService.persistDatasetBinding:321-347,数据集级行 setDocumentId(null))。唯一索引落点核查uk_...active(tenant_id, kb_id)installed_ref 用安装者租户内新建 kbId,与发布者 kb 的 binding 不撞键(评审版担心的"沿用 sourceId 撞键"在去污染后消失)。
    • 分支 C复制:建新 datasetragFlowClient.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_basekb_type='installed_ref' AND source_market_asset_id=X AND owner_user_id=安装者 AND status='active'
  • dataset 关联(S)|输入:发布者 kb 的 dataset=D预期新建一行 muse_knowledge_ragflow_bindingkb_id=新建本地kbId AND ragflow_dataset_id=D AND document_id IS NULL AND status='active',且不违反 uk_...active

VerificationU1 本身不跑测试(设计单元);其正确性由 U2/U3 的 IT 覆盖。若触发 V32cd 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.javacreateKnowledgeBindingPrecheck:71-127createKnowledgeBinding:129-177consumeMarketHandoff: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(复用 insertMuseKnowledgeRagflowBindingMapper.java(复用 insert
  • 参照范本:.../knowledge/application/muse/MuseKnowledgeDocumentService.javapersistDatasetBinding:321-347 数据集级行写法 + DataIntegrityViolationException 回读幂等)

Approach

  1. 新增 market 跨 BC 读端口(事实 2 前置):MarketAssetSourceApi.getAssetSource(Long assetId){sourceOwner, sourceType, sourceId(=发布者kbId), publisherUserId, tenantId},实现读 muse_market_assetsource_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_bindingkb_id=新建本地kbIdragflow_dataset_id=发布者datasetdocument_id=null),形态仿 persistDatasetBinding
    • 去污染binding.setKbId(新建本地kbId)(不再是 assetIdprecheck.setKbId(...) 同步——precheck 阶段已需建实体还是 bind 阶段建,二选一:建议放 createKnowledgeBinding(用户确认绑定的写事实点)precheck 仅校验资产存在性,避免 precheck 失败留下孤儿 kb 行。
    • 来源溯源source_market_asset_id=assetIdlicense_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 复用 persistDatasetBindingDataIntegrityViolationException→回读模式应对并发。
  5. 事务原子性:物化与"消费 handoff token + 写绑定事实"在同一事务(现状 consumeMarketHandoff 已沿用调用方 @Transactional),失败整体回滚,无中间态。契合 架构-02 §7 / 流程-02B §13"目标 owner 自己消费 token + 写 owner 事实"。

Test scenarios输入+预期)

  • 首次安装绑定输入T_A 安装 assetId=Xknowledge_base 类型,发布者 kb=K、dataset=DcreateKnowledgeBinding(带合法 handoff token + precheckId预期新建一行 muse_knowledge_base(kb_type=installed_ref, source_market_asset_id=X, owner_user_id=A),记其 id=K2muse_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(只经 MarketAssetSourceApitest 绿。

Verification

  • 模块单测/ITcd 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 教训)。
  • ArchUnitmvn -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-127parseChunks: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_idrpe-local-<uuid> 形态预期检索门正常放行、chunk 字段透传该字符串,无 NumberFormatException、不落 null。

Verification

  • 单测:mvn -pl muse-module-knowledge/muse-module-knowledge-server -am test -Dtest=MuseKnowledgeRetrievalApiImplTestmock RAGFlow覆盖 no_dataset 消除 + 字符串授权快照 + 隔离过滤断言)。
  • 活体并入 U4 端到端。

U4 · 验证:真 PG 端到端 + studio e2e + 回滚 + 隔离回归

Goal:用真实证据证明"市场 KB 安装→绑定→物化→检索命中"端到端可真用,且召回/下架时已物化实体能回滚、跨租户隔离不被共享绕过。

Files

  • 真 PG ITmuse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/test/java/cn/iocoder/muse/module/knowledge/.../P1rMarketInstallKbMaterializationIT.java(参照既有 P1r*IT 范式 + P1rRagFlowLiveAcceptanceIT live 范式)
  • studio e2emuse-studio/e2e/market-install-kb-retrieval.spec.ts(参照 muse-studio/e2e/knowledge-bindings.spec.ts + market-install.spec.ts
  • 回滚相关(据真实机制,事实 4.../knowledge/application/muse/MuseInstalledKnowledgeBaseService.javadisable/delete 路径)+ knowledge source event/projectionmuse_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 facadeMuseKnowledgeBindingService + MarketAssetSourceApi写自己的实体market 不直写 knowledge .daltest 绿。

Verification

  • 真 PG ITmuse-cloud/ 跑,按 memory muse-p1r-it-run-recipep1r.flyway.* argLine + 密码经 env~/.config/muse-repo/infra.envset -a && . it && set +aonlineRAGFlow/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 列表(MuseInstalledKnowledgeBaseServicemuse_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 + 新增 MarketAssetSourceApimarket-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_agentmarket_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. 关联阅读