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

55 KiB
Raw Blame History

临时-04 · Market KB 物化 D0-fork发布侧 fork 纯公开副本)执行 Plan

一句话:拍板已定——做 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_filtermetadata_condition 降级为潜伏 bug:不在 D0-fork 关键路径(公开副本纯公开、检索不靠它过滤);单独记、留待修
核心新增工程 chunk 级隔离 + 物化时给文档打安装维度元数据 发布侧 fork 公开副本(建 dataset + 复制文档 + 重索引,异步重活)+ 打破 market→knowledge 零依赖接线
复制方向 否决了"每安装者各复制一份"C 分支) fork 一份公开副本、N 个安装者共享只读(副本仅 1 份,非每安装者一份)

临时-03 哪些资产被本稿继承

  • U0 spike 的 2 个 spike case 保留:机制事实"检索链对返回 chunk 无 chunk 级过滤"(混入他源 magic chunk 原样返回 + RetrieveChunksCommanddocumentIds/metadataFilter 均 null = 整库扫描)仍然成立、仍是 D0-fork 必须斩断越权的根因证据;它在本稿转作 U-verify 的反向基线——用来证明"发布者私有不泄露"fork 后安装者检索副本,混入副本之外的发布者私有 magic chunk 应检索不到。spike 测试已落 MuseKnowledgeRetrievalApiImplTest+69 行,零业务代码),不必重写。
  • 临时-03 的四条硬事实kb_id 被 assetId 污染、跨 BC 反查链断需新 MarketAssetSourceApimuse_source_propagation_target 表不存在、检索链无 chunk 二次过滤)全部仍然有效,是本稿 U-materialize/U-retrieve 的地基§1 复述。

1. 执行前必读:把工作量钉死的硬事实(基于真实代码,行号为只读核验所得)

事实 1 — 检索链对返回 chunk 无任何二次过滤,"谁能进检索、就看到该 dataset 整库"。这是 D0-fork 必须做物理隔离、而非运行时隔离的根因。 RAGFlow 检索 HTTP bodyHttpRagFlowKnowledgeRuntimeClient.retrieveChunks:144-157)只发 dataset_ids + question + top_k + similarity_threshold + metadata_filtertenantId/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_conditioncontext7 /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 assetIdcreateKnowledgeBinding: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_idV6:10 BIGINT在审核物化资产时写入发布者本地 kbIdAdminMarketReviewServiceImpl.resolveApprovedAssetBinding:454 asset.setSourceId(longValue(draftSnapshot.get("sourceId"))))。但 knowledge 不能直读 market 的 .dalArchUnit BcBoundaryArchTestmarket api 包下目前只有 MarketHandoffTokenApiverify/consume,没有"按 assetId 查源对象/查公开副本 dataset"的读接口。结论D0-fork 下安装侧物化要拿到"公开副本 dataset id",必须新增跨 BC 读端口MarketAssetSourceApi.getAssetSource(assetId) → {sourceType, sourceId=发布者kbId, publisherUserId, publicForkDatasetId, forkStatus},本地进程内 BeanMarketHandoffTokenApi 同范式、不加 Feign注意 handoff token 不带这个信息token snapshotMarketHandoffServiceImpl.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(懒建 datasetuploadDocuments(带 fileRef/content byte[])→ startParseDocuments(触发解析/索引)。结论fork 一份副本 = 新建一个副本 datasetcreateDataset+ 把发布者每个公开文档的物化文件(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_baseV5:4-28)已备 kb_type/source_market_asset_id(BIGINT)/license_snapshot_id(BIGINT)muse_knowledge_ragflow_bindingV14)唯一键 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 实现单元依赖图

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 的越权解法)

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 时序(上架触发 + 事件驱动 + 异步重活,含失败/重试/幂等)

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 后)

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.javaensureDatasetBinding:300-319 createDataset 形态、continueRagflowIfMaterialized:248-298 upload+startParse 形态、persistDatasetBinding:321-347 数据集级 binding 写法)
  • 复用运行时边界(无需改接口):.../knowledge/application/muse/facade/RagFlowKnowledgeRuntimeClient.javacreateDataset/uploadDocuments/startParseDocuments 已够用,事实 6
  • 发布侧上架事件落点接线事件驱动方案market 侧):muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/AdminMarketReviewServiceImpl.javamarkListed: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-017Source 传播 = 事件驱动 + 各模块自治);③ 保 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 facademarkListed 同步调 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 消费上架事件时按 发布者 kbIdmuse_knowledge_document + 各文档 selectLatestByDocumentId 的 active 版本作为复制清单。发布者上架后新增的文档因 create_time 晚于上架事件,不在这一次 fork 的清单内——这就是隔离发布者私有的语义支点。(若上架时刻有 scan_blocked/failed/deleting 的文档fork 应跳过,只复制 searchable/generative 可检索文档。)

④ 复制机制(事实 6fork 一份副本 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.recordRagflowCallattributionStatusmarket_public_forkcorrelation 区分 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 个公开文档(重索引后可检索),不含私有文档 PforkStatus=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 502forkStatus=partial;重试再来|预期:副本 dataset 不重建d1 不重复上传,只补 d2补成功后 forkStatus=ready
  • 失败不阻断审核输入fork 全程 RAGFlow 不可用预期market 审核已提交 listed事件已落forkStatus=failed+可重试;安装侧据 forkStatus≠ready fail-closed不会让安装者绑到不存在的副本

Verification

  • 模块单测/ITcd 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.javacreateKnowledgeBindingPrecheck:71-127createKnowledgeBinding:129-177consumeMarketHandoff: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(复用 insertMuseKnowledgeRagflowBindingMapper.java(复用 insert
  • 参照范本:.../knowledge/application/muse/MuseKnowledgeDocumentService.javapersistDatasetBinding:321-347 数据集级行写法 + DataIntegrityViolationException 回读幂等)

Approach

  1. 新增 market 跨 BC 读端口(事实 4MarketAssetSourceApi.getAssetSource(Long assetId){sourceType, sourceId=发布者kbId, publisherUserId, publicForkDatasetId, forkStatus}。范式同 MarketHandoffTokenApi(本地 Beanknowledge 注入它而非读 market.dal与临时-03 的关键区别:临时-03 带回"发布者活 dataset"(要 knowledge 跨 owner 读发布者 kb 的 dataset有租户拦截器难题 D4D0-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 当 kbIdMarketAssetSourceApi.getAssetSource(assetId) 校验资产存在且为 knowledge_base 类型且 forkStatus=ready(不存在/类型不符/副本未就绪 → fail-closed 抛错,替代现状裸信客户端)。
  3. 建本地 installed_ref 实体(落点:createKnowledgeBinding 同一 @Transactional 事务内)
    • muse_knowledge_binding 之前先 insert muse_knowledge_basekb_type='installed_ref'source_market_asset_id=assetIdlicense_snapshot_id=授权快照主键owner_user_id=安装者status='active'active_version=1name/description 取资产摘要(MarketAssetSourceApi 可一并带回或占位);拿到新建本地 kbId
    • insert muse_knowledge_ragflow_bindingkb_id=新建本地kbIdragflow_dataset_id=公开副本datasetIddocument_id=nullstatus='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:若 getAssetSourceforkStatus≠readyfork 中/失败),有两种处置:(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 复用 persistDatasetBindingDataIntegrityViolationException→回读模式应对并发。
  6. 事务原子性:物化与"消费 handoff token + 写绑定事实"在同一事务(现状 consumeMarketHandoff 已沿用调用方 @Transactional),失败整体回滚。

Test scenarios输入+预期)

  • 首次安装绑定副本就绪输入T_A 安装 assetId=Xknowledge_base 类型,公开副本 D_pubforkStatus=readycreateKnowledgeBinding(合法 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_pub, document_id=null, status=active)
  • 多安装者共享副本输入A、B 各装 assetId=X预期建两行 installed_ref kbK2、K3各自 owner两条 ragflow_binding 都指向同一 D_pub(副本仅 1 份),不违反 uk_..._activekb_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(只经 MarketAssetSourceApitest 绿。

Verification

  • 模块单测/ITcd 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 教训)。
  • ArchUnitmvn -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-127parseChunks: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=MuseKnowledgeRetrievalApiImplTestmock RAGFlow覆盖 no_dataset 消除 + 字符串授权快照;私有不泄露并入 U-verify 真 RAGFlow
  • 活体并入 U-verify 端到端。

U-verify · 验证(真 PG 端到端 + studio e2e + 发布者私有不泄露 + 回滚)

Goal:用真实证据证明"知识库上架→fork 公开副本→安装→绑定→物化→检索命中"端到端可真用;且发布者私有内容不泄露给安装者D0-fork 的核心安全目标);召回/下架时已物化实体能回滚。

Files

  • 真 PG ITmuse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/test/java/cn/iocoder/muse/module/knowledge/.../P1rMarketKbForkMaterializationIT.java(参照既有 P1r*IT + P1rRagFlowLiveAcceptanceIT live 范式)
  • studio e2emuse-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.javadisableInstalledKnowledgeBase: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 记录式(事实 7writeSourceStatusBlocked 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 + MarketAssetSourceApimarket 不直写 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

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_baseV5 已备 kb_type/source_market_asset_id/license_snapshot_id+ muse_knowledge_ragflow_bindingV14 已有列),不需要迁移(事实 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 JSONBMarketAssetSourceApi 从这里带回)承载。这样不需要新列。
    • 需 V32 的情形:若评审认为 fork 状态/副本映射必须强类型列(而非 JSONB则新增 muse_market_assetpublic_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,事实 2D0-fork 不依赖它(公开副本纯公开),故本期不修、降级为潜伏 bug但它是真 bug任何未来想靠元数据过滤检索的功能都会踩留待后续单独修,不随 D0-fork 关键路径处理。
  • R7 召回/下架回滚未自动化(开放项):事实 7本期标开放不假装闭环D9

8. 回滚(本 plan 执行后如何撤回)

  • 代码回滚U-materialize/U-retrieve 改动集中在 MuseKnowledgeBindingService + 新增 MarketAssetSourceApimarket-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_agentmarket_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. 关联阅读