承临时-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>
39 KiB
临时-03 · Market Install KB 下游物化执行 Plan(HOW,供 review 后执行)
- 版本:v1(执行版,待人类 review/拍板后执行;本稿只读出文档,不改任何代码)
- 更新日期:2026-06-26
- 目标读者:后端 / 架构 / PR reviewer
- 阅读时间:25–35 分钟
- 文档性质:临时件(执行依据,落定后归并入正式分册或删除,不作长期 SSOT)。本稿只给"怎么做",不重定义概念:方案权衡与决策收束见
临时-02-market-install下游物化方案.md;术语与 owner 边界以架构-02-核心数据结构与双轨模型.md为准(§6 市场资产、§11.2 绑定安装授权、第 122 行授权快照"引用对象必须保存快照 id");表结构归后端-04;检索合同归专题-03§5;授权快照字符串承载归 ADR-020;BC 边界规则归.agents/rules/bc-boundaries.md。 - 配套人读图:
临时-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。
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. 总览:实现单元依赖图与决策门
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 · spike:chunk 级隔离硬前置门(决定共享/复制/暂缓分支)
Goal:在落任何物化代码前,回答唯一硬问题——"共享发布者同一 RAGFlow dataset"在当前检索链下能否做到租户/授权隔离。调查已先验给出方向性结论(当前必越权,见 §0 事实 3),U0 要做的是用最小成本把这个结论钉成可复现的证据 + 给出"补隔离"的可行手段验证 + 输出决策门,让人类据此在"共享(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 专用测试类)——mockRagFlowKnowledgeRuntimeClient返回"混入他租户 magic token 的 chunk 集",断言当前retrieveForWork把混入 chunk 原样返回(证伪"有租户过滤")。 - spike 活体(验证层 B,真 RAGFlow,mini-infra 100.64.0.8 在线):参照
.../application/muse/facade/P1rRagFlowLiveAcceptanceIT.java范式,只读地对一个真 dataset 调/api/v1/retrieval(document_ids=null),确认 RAGFlow 整库返回、不按任何 muse 维度过滤。
Approach / 验证方法(多租户 RAGFlow dataset 隔离怎么验):
- 造数据:seed 两个真实租户 T_A、T_B(注意当前是单租户 +
tenant_id DEFAULT 0,需先建第二租户并确认 yudao 租户拦截器在两租户上下文都生效);让"发布者"建一个 KB → 真上传一份文档 → RAGFlow 真生成出ragflow_dataset_id = D。 - 共享指向:在 T_A、T_B 各建一条 installed_ref binding(spike 阶段可直接 SQL 注入读模型行,不依赖 U2 代码),两条的
muse_knowledge_ragflow_binding.ragflow_dataset_id都 = D(模拟 B' 共享),各自给齐bindingScope=search+authorizationSnapshotId+ work 归属。 - 掺入可识别越权探针:往 D 里放一段"只有发布者/T_B 该看"的、含唯一 magic token 的 chunk(只共享同一份内容验不出问题,掺入差异内容才是验越权的核心)。
- 各自检索:以 T_A 租户上下文调
MuseKnowledgeRetrievalApi.retrieveForWork(A 的 work + question 命中那段 magic chunk),看返回是否含本不该 A 看的内容;T_B 同样跑一遍。 - 判定(决策门):
- 若 A 能检索到 D 的 magic chunk(预期会)→ 证实共享即越权 → 共享分支(S)不可直接落地,必须先做第 6 步的"补隔离"并复验通过,否则只能走复制(C)或暂缓(H)。
- 若已实现"补隔离"且 A 检索不到 B 的 magic chunk、且各自只拿到本授权子集 → 共享分支(S)放行。
- "补隔离"可行手段验证(共享分支前置):验证以下任一路径能把 chunk 限定到"本安装授权子集":
- (a)
RetrieveChunksCommand.metadataFilter—— 物化时给 RAGFlow 文档打租户/安装维度元数据,检索时按metadata_filter过滤;需验 RAGFlow/api/v1/retrieval的metadata_filter语义是否真生效(用 context7/RAGFlow 官方文档核对 + 活体打一发,不要靠假设)。 - (b)
RetrieveChunksCommand.ragflowDocumentIds—— 检索时把 document_ids 限定到本安装可见文档集(需本地维护"installed_ref→可见 documentId 集"映射)。 - 两条都不可行 → 共享分支判负,回落复制(C)。
- (a)
决策门输出(U0 交付物):一页结论——(i) 当前共享越权与否的可复现证据(单测层 + 活体层各一);(ii) "补隔离"手段 (a)/(b) 哪条可行、成本几何;(iii) 明确给人类的三选一建议:S(补隔离后共享)/ C(每安装者复制 dataset)/ H(暂缓)。后续 U1–U4 据此分支。
Test scenarios(spike 自身的断言):
- 单测层|输入:mock RAGFlow 返回
[{dataset_id:D, content:"含 MAGIC_B"}, {dataset_id:D, content:"公共内容"}],以 T_A 上下文retrieveForWork|预期:返回结果包含MAGIC_B(证伪租户过滤,钉死越权事实)。 - 活体层|输入:真 dataset D(含 magic 文档),
/api/v1/retrievaldocument_ids=null|预期:响应data.chunks[]含 magic chunk(RAGFlow 整库返回,无 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第 4–28 行: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:
- installed_ref kb 行字段映射(写入由 U2 执行,U1 只定字段语义):
kb_type = 'installed_ref'(V5 已支持该取值,现状从不写本表)。source_market_asset_id = assetId(承载"来源市场资产"溯源;注意当前为 BIGINT,assetId 为数值,类型相容)。license_snapshot_id—— 类型核查项:V5 该列是 BIGINT。market 授权快照主键muse_market_authorization_snapshot.id是 BIGINT(install 存的是数值主键,见MarketInstallServiceImpl.java:85setAuthorizationSnapshotId(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 带回或用占位)。
- 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 细化)。
- 分支 S(共享):建一行
- 迁移判定(人类门 yes/no):
- 分支 S 初版(填数值 license_snapshot_id、kb_type/source_market_asset_id 复用 V5 已备列)→ 不需要新 Flyway 迁移。所有写入落在 V5/V14 已有列上,DO 已有对应字段。
- 仅以下情形需 V32(DDL 人类门):(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 越界)
- market 侧 API(被 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:
- 新增 market 跨 BC 读端口(事实 2 前置):
MarketAssetSourceApi.getAssetSource(Long assetId)返{sourceOwner, sourceType, sourceId(=发布者kbId), publisherUserId, tenantId},实现读muse_market_asset(source_id/asset_type/publisher_id)。范式同MarketHandoffTokenApi(本地 Bean,knowledge 注入它而非读 market.dal)。保证不破 ArchUnitbc-boundaries。 - 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 复审时回填)。
- 调
- 建本地 installed_ref 实体(落点:
createKnowledgeBinding同一@Transactional事务内):- 在写
muse_knowledge_binding之前,先 insertmuse_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=授权快照主键。
- 在写
- 幂等(重复 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→回读模式应对并发。
- 绑定本身已有命令回放(
- 事务原子性:物化与"消费 handoff token + 写绑定事实"在同一事务(现状
consumeMarketHandoff已沿用调用方@Transactional),失败整体回滚,无中间态。契合架构-02§7 /流程-02B§13"目标 owner 自己消费 token + 写 owner 事实"。
Test scenarios(输入+预期):
- 首次安装绑定|输入:T_A 安装 assetId=X(knowledge_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 假红(memorymuse-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:
- 共享分支基线打通:U2 把
binding.kb_id改成本地真实 kbId、并建好数据集级muse_knowledge_ragflow_binding(kb_id=本地kbId, ragflow_dataset_id=发布者dataset)后,检索链第四门selectActiveDatasetByKbId(本地kbId)自然返非空 →no_dataset消除 → 进 RAGFlow 检索。这一步 RetrievalApiImpl 代码无需改动(这是 B' 共享"地基已备"的真实体现)。 - 授权快照沿用 ADR-020:检索门读
binding.getAuthorizationSnapshotId()(已是 VARCHAR,V14muse_knowledge_binding.authorization_snapshot_id已 widen),installed_ref binding 写入时沿用 VARCHAR 字符串承载(U2 写 binding 时不改这一列类型)。chunk §5.3 合同字段authorizationSnapshot透传字符串,不 parseLong。 - 隔离(仅共享分支,且仅 U0 绿后):把 U0 验证通过的隔离手段落到
RetrieveChunksCommand——metadata_filter 或 document_ids 限定本安装授权子集。U0 红则本单元不走共享、检索直接对接复制分支的独立 dataset(天然隔离)。
Test scenarios(输入+预期):
- no_dataset 消除|输入:U2 物化后的 installed_ref(本地kbId=K2, 关联 dataset=D),work 绑定含 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 D(D 含 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范式 +P1rRagFlowLiveAcceptanceITlive 范式) - 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(输入+预期):
- 知识库 B' 端到端(真 PG + 真 RAGFlow)|输入:安装市场 KB 资产 → 作品绑定(物化 installed_ref + dataset 关联)→ 触发检索
retrieveForWork|预期:第四门拿到(发布者)dataset → RAGFlow chunk 真命中 → 生成上下文含该来源 grounding;对照现状(必no_dataset省略)。 - 幂等|输入:重复 install + 重复 bind(回放 + 不同 commandId 两路)|预期:物化实体不重复创建(命令回放 + (安装者,资产) 幂等键 + 唯一键),DB 中 installed_ref kb 仅一行。
- 跨租户隔离回归(共享分支必跑,承 U0)|输入:T_A、T_B 各装同一发布者 KB(共享 dataset D,D 含两租户差异内容)→ 各自检索|预期:A 检索不越权读 B 的私有 chunk(隔离手段生效);若走复制分支则验各自独立 dataset 天然隔离。
- 回滚/来源传播(基于真实机制,不引用不存在的
muse_source_propagation_target)|输入:发布者资产被 admin 召回/下架(marketadminRecallAsset/adminDelistAsset)|预期:已物化 installed_ref 实体被阻断新使用(不自动删,符合架构-02来源事件"不删除正式知识"语义)。实现路径待定项:market 召回当前对目标 owner 的传播是 fail-closed 记录式(见 market.agentadminRecallAsset描述"目标 owner 传播 fail-closed"),knowledge 侧消费"market 召回→停用 installed_ref"的入口本期可能缺——U4 需如实验证现状传播是否触达 knowledge installed_ref;若不触达,则标为开放项("召回回滚未自动化,需新增 market→knowledge 传播接口或手动停用"),不假装已闭环。 - 删除/停用已物化实体|输入:用户
disableInstalledKnowledgeBase/deleteInstalledKnowledgeBase|预期:installed_ref binding 软删(@TableLogic+setSql("deleted = true"),见 §0 事实 4),检索读回排除;物化的 dataset binding(共享分支)不删发布者 dataset(只解除本安装的引用),复制分支需评估是否清理本安装独占 dataset。 - 机械门禁|输入:ArchUnit
BcBoundaryArchTest+ 覆盖 JSON 门|预期:物化只经目标 owner facade(MuseKnowledgeBindingService+MarketAssetSourceApi)写自己的实体,market 不直写 knowledge.dal,test 绿。
Verification:
- 真 PG IT:从
muse-cloud/跑,按 memorymuse-p1r-it-run-recipe配p1r.flyway.*argLine + 密码经 env(~/.config/muse-repo/infra.env,set -a && . it && set +a),online(RAGFlow/New-API 在 mini-infra 在线)。_test库铁律:IT 的 flyway 目标库绝不指 muse_slice_live。 - studio e2e:起全栈 app(PG 宿主 100.64.0.8 在线时),
npx playwright test market-install-kb-retrieval.spec.ts。 - 凭据红线:所有脚本/日志过滤
password|secret|token|sk-;不 rmdump.rdb/lefthook。
3. 决策点(待人类拍板,按优先级)
| # | 决策点 | 选项 | 倾向 | 阻塞谁 |
|---|---|---|---|---|
| D1 | dataset 共享 vs 复制(最硬,U0 门) | S 补隔离后共享 / C 每安装者复制 / H 暂缓 | 先看 U0 spike 证据再拍;已知当前共享必越权,S 需先投入 chunk 级隔离(metadata_filter/document_ids)并验证;不投入则 C 或 H | U1–U4 全部分支 |
| 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。 - 迁移回滚:初版不引入 V32(D3 选数值主键),无迁移需回滚;若触发 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 - 概念与 owner 边界:
架构-02-核心数据结构与双轨模型.md(§6 市场资产、§11.2 绑定安装授权、授权快照语义) - 检索合同 / fail-closed:
专题-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