diff --git a/design-docs/00-文档大纲.md b/design-docs/00-文档大纲.md index 21b4863e..359ca303 100644 --- a/design-docs/00-文档大纲.md +++ b/design-docs/00-文档大纲.md @@ -104,10 +104,8 @@ ### 临时件(评审/迁移依据,落定后归并或删除,不作长期 SSOT) -- [临时-01-AI流SSE长任务超时修复方案](临时-01-AI流SSE长任务超时修复方案.md)(评审版)——AI 真生成"长任务候选丢失"的 SSE 协议/时序 bug 修复方案对比,待人类 review 后执行;配套人读图 `.html`。SSE 接口语义归属仍属 `后端-05`,本件不重定义概念。 -- [临时-02-market-install下游物化方案](临时-02-market-install下游物化方案.md)(评审版)——市场安装的 agent/kb 到“可真用”的两个物化断点(知识库检索 `no_dataset` 门 + 智能体 runtime 实体缺失)诊断与 C/B'/B/暂缓 四方案对比,待人类拍板物化深度/时机/数据集共享;配套人读图 `.html`。owner 边界与表结构归 `架构-02`/`后端-04`,handoff 边界与 ADR-016/017/020 只引用不重定义。 -- [临时-03-market-install-KB物化执行plan](临时-03-market-install-KB物化执行plan.md)(执行版,**已被临时-04 supersede**)——承临时-02 拍板(B' installed_ref + KB 先行),把 KB 物化拆成 U0–U4 可独立验证实现单元;一手代码核验揪出四条硬事实(kb_id 被 assetId 污染、跨 BC 反查链断需新 `MarketAssetSourceApi`、检索链对返回 chunk 无二次过滤、`muse_source_propagation_target` 表不存在);物化落点在 knowledge 绑定路径(非 install)。当时方向是"多租户共享 dataset → 共享前补 chunk 级隔离",**D1 澄清后被临时-04 D0-fork 取代**(保留作决策演进史);配套人读图 `.html`。owner 边界引 临时-02/架构-02/专题-03/ADR-020,不重定义。 -- [临时-04-market-KB物化-D0fork执行plan](临时-04-market-KB物化-D0fork执行plan.md)(执行版)——**supersede 临时-03**。D1 澄清坐实真越权 =「安装者读到**发布者私有**内容」(market 对 knowledge/ragflow 零依赖→上架不 fork→安装者共享发布者活 dataset→上架后 KB 仍可加私有文档),用户拍板 **D0-fork**:知识库上架时在发布侧 fork 一份**只含公开文档的专用 dataset**(公开副本),安装者只读共享它 → **物理隔离**发布者私有、副本仅 1 份、**无需运行时 chunk 隔离**、`metadata_filter` 字段 bug 降级潜伏。拆成 U-fork(发布侧 fork 公开副本,全新核心工程:建副本 dataset+逐文档复制+重索引+异步重活)/ U-materialize(安装侧物化去隔离)/ U-retrieve(检索打通)/ U-verify(含发布者私有不泄露验证,复用临时-03 U0 反向基线)四单元;含 **fork 时机推荐(上架时 fork)+ 接线推荐(事件驱动,打破 market→knowledge 零依赖,契合 ADR-017)+ DDL 判定(初版不需新迁移)**。待人类拍 fork 时机/接线/副本更新等决策点后执行;配套人读图 `.html`。owner 边界引 临时-02/03/架构-02/专题-03/ADR-017/020,不重定义。 +- 临时-01~04(SSE 长任务超时修复 / market-install KB 物化演进)——**已于 2026-06-26 归并删除**(过程文档落定即清,不作长期 SSOT):结论落 memory(`muse-ai-generation-sse-timeout-bug`、`muse-market-kb-d0fork-materialization`)+ 各模块 `.agent` + [1.0.0 交付计划](../../docs/mvp/1.0.0-交付计划.md);KB 物化的"共享→D0-fork"决策演进史留 git 历史。 +- [临时-05-market-agent物化执行plan](临时-05-market-agent物化执行plan.md)(执行版)——临时-04 KB 物化的**姊妹方向(agent 版)**,承临时-02 断点②(智能体 runtime 实体缺失)。拍板已定 **①不 fork、拷配置**(agent 无 KB 那类私有泄露向量:version 一经 active 即不可变、运行时不回读发布者私有 prompt、`config` 即被授权出售的商品本体)**②物化成 installed 型独立 `muse_agent`+active `muse_agent_version`(config 拷自发布者)+ 改运行时授权门放行**。一手代码核验坐实唯一真断点 = 运行时授权门 `requireVisibleAgent:329-331` 对 market 来源一律拒(三入口 220/308/315 全经它),槽位闸 A 已放宽(不动)。拆成 A-source(**扩**临时-04 已落的 `MarketAssetSourceApi` 带 config+放行 agent 类型)/ A-materialize(安装侧建 installed agent 拷 config,照搬 KB `materializeInstalledRefKb` 形态+幂等+去裸引用+补 `MuseAgentDO` 映射 V27 已有 `source_market_asset_id` 列)/ A-runtime(**改授权门=trust boundary**,放行 installed 型+本人+授权有效,**他人/无授权/裸引用必拒**,负路测命门)/ A-naming(agent_type 命名收口)/ A-verify(真 PG IT+e2e+授权门负路)五单元;含 **config 跨 BC 读取途径决策(market 反调 ai vs ai 自读)+ 拷 vs 引用 prompt+命名最终值+DDL 判定(不需新迁移)**。待人类拍决策点后执行;配套人读图 `.html`。owner 边界引 临时-02/04/架构-02/ADR-017/020,不重定义。 ## 输入 / 输出闭环(你写的东西要能被别人用) diff --git a/design-docs/临时-01-AI流SSE长任务超时修复方案.html b/design-docs/临时-01-AI流SSE长任务超时修复方案.html deleted file mode 100644 index f5f26091..00000000 --- a/design-docs/临时-01-AI流SSE长任务超时修复方案.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - -临时-01 · AI 流 SSE 长任务候选丢失修复方案(人读图) - - - -
- -

AI 流 SSE 长任务候选丢失 · 修复方案

-
临时-01 · 评审版 v1 · 2026-06-25 · 不改代码,供 review 后执行 · 配套主文档 临时-01-*.md
- -
- 用户长篇 AI 生成卡在「中止生成」、候选永不出现:根因是后端 SSE 在 30 秒硬死线到点后 - 静默关连接、不补发任何终态事件,而前端 AI 流是一次性连接、不重连。一旦大模型真实时延 - (实测 11–60 秒)越过 30 秒,候选就被永久丢在后端库里送不到界面。 - 推荐 A1 启动固化 + 候选③(后端放宽死线 + 前端重连)。 -
- -

两层根因(环境隐患 + 真代码缺陷,叠加才致命)

-
-
- 第一层 · 已手工绕过 / 未固化 -

环境与启动隐患

-

启动脚本只加载基础设施凭据,漏加载大模型接入凭据那一份配置。

-

后端拿不到接入信息 → 装"不可用兜底"客户端 → AI 任务十几毫秒内秒失败报"接入不可用"。

-

已手工带齐全部环境变量重启验证通过,但脚本没改,下次重启复发,会污染验证现场。

-
-
- 第二层 · 真代码缺陷(前后端各一处) -

SSE 协议 / 时序缺陷

-

后端:连接死线与轮询上限都写死 30 秒;到点任务未完则只关连接,不补 done、不补 error。 - 这 30 秒甚至短于后端等大模型的总窗口 180 秒。

-

前端:AI 流是一次性连接、无重连(全局事件流却带重连)。连接被提前关后,流自然结束、上层也不补救。

-
-
- -

因果对比:同一缺陷,被大模型时延切到两侧

-
-
-
✓ 快路径 · 实测约 15 秒(< 30s)→ 碰巧绿
-
0s 发起生成,建立一次性 SSE 连接
-
~15s 大模型落 done 事件入库
-
~15s 后端轮询读到 done → 推给前端
-
前端渲染候选
-
候选正常出现 → accept-suggestion 用例 flaky 绿
-
-
-
✗ 慢路径 · 实测约 36 秒(> 30s)→ 稳定红
-
0s 发起生成,建立一次性 SSE 连接
-
30s 死线到,任务未完 → 后端静默关连接
不补 done / 不补 error
-
30s 前端流自然结束 → 不重连
-
~36s 大模型才落 done(晚约 8s)→ 候选只进了库
-
界面无候选也无报错,卡「中止生成」 → ai-generation 用例真红
-
-
- -

修法选项与权衡(不夸大任何一项)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
选项做法要点根治度复杂度主要代价 / 权衡
第一层 A1
启动固化
脚本一并加载大模型接入凭据,缺关键变量直接报错退出高·消除复发极低凭据成启动隐式依赖,需写明文档;唯一从源头止复发,强烈建议必做
候选①
后端放宽死线
把 30s 死线放宽到覆盖时延上限、尽量可配;一处同时管连接与轮询中·治标最低仍在赌大模型不更慢,极端慢仍无声卡死;SSE 长开占轮询线程更久
候选②
前端断连重连
非终态结束按递增退避重连续传,至 done/error/前端总时长上限止高·根治断连偏高推翻"一次性"既有设计、协议变复杂;重连有重放开销,须保候选去重幂等
候选③
①+②组合
后端窗口吸收正常偏慢,前端重连兜住极端慢与偶发断流最高最大前后端同改、回归面最广;但后端放足后重连频率低、重放代价随之降
- -

推荐

-
-
✅ A1 启动固化 + 候选③(后端放宽死线兜底 + 前端重连容错)
-
缺陷本质是长任务容错缺失,大模型时延不可控且会漂移,单点修法治不全: - 只做①是把丢候选区间后移、仍赌时延;只做②则每次长生成都靠重连去捞、徒增开销。 - 组合后正常偏慢由后端窗口稳稳吸收、重连只在真异常时触发,既根治又把代价压到最低, - 还顺带把 AI 流收敛到与全局事件流一致的容错模型。
-
排期紧时分阶段(每步独立可验可回滚):A1 → 候选①(最小改快速止血)→ 候选②(补根治与一致性)
-
- -

待人类拍板的三个关键决策点

-
    -
  1. 后端死线放到多少 / 要不要可配? - 时延实测 11–60s,上游总窗口已配 180s。建议 SSE 死线 - 不低于上游 180s 再加排队与回放余量——初稿提的 120s 不足以覆盖上游尾部,会重演"SSE 比上游先关门"。是否做成可配项请一并定。
  2. -
  3. 要不要上前端重连,即是否推翻 AI 流"一次性、不续传"的既有设计? - 这是复杂度与根治度的分水岭。上重连换来真容错与跨流一致,代价是协议变复杂、需改写既有设计决策并补测试;若求最小风险、接受"极端慢仍可能卡死",可暂缓②、先做①与 A1。
  4. -
  5. 重连续传的幂等与候选去重边界谁保证? - 一旦上重连,须明确:后端重放已发生事件时前端如何凭顺序游标避免重复渲染候选、前端总时长上限取多少。否则可能把"丢候选"换成"重复候选 / 无限重试"。
  6. -
- - - -
- - diff --git a/design-docs/临时-01-AI流SSE长任务超时修复方案.md b/design-docs/临时-01-AI流SSE长任务超时修复方案.md deleted file mode 100644 index 0cbefeda..00000000 --- a/design-docs/临时-01-AI流SSE长任务超时修复方案.md +++ /dev/null @@ -1,186 +0,0 @@ -# 临时-01 · AI 流 SSE 长任务候选丢失修复方案(评审版) - -- 版本:v1(评审版,待人类 review 后执行) -- 更新日期:2026-06-25 -- 目标读者:架构 / 后端 / 前端 / QA / PR reviewer -- 阅读时间:12–18 分钟 -- 文档性质:**临时件**(迁移依据,落定后归并入正式分册或删除)。本稿只做方案对比与决策收束,不改任何代码、不定义新概念;术语以 `架构-02` 为准,SSE 接口语义归属 `后端-05`。 -- 配套人读图:[`临时-01-AI流SSE长任务超时修复方案.html`](临时-01-AI流SSE长任务超时修复方案.html) - -> 一句话结论:用户长篇 AI 生成卡在「中止生成」、候选永不出现,根因是后端 SSE 通道在 30 秒硬死线到点后**静默关连接、不补发任何终态事件**,而前端 AI 流是一次性连接、**不重连**;当大模型真实时延越过 30 秒,候选就永久丢给了界面。推荐采用**后端放宽死线兜底 + 前端断连重连容错的组合方案**,并把启动脚本缺失 AI 凭据这一环境隐患一并固化。 - ---- - -## 1. 问题与根因 - -### 1.1 用户可见症状与工程证据 - -普通用户在写作台发起较长的 AI 续写或优化时,正文区一直停在生成中状态,候选迟迟不出现,最终只能点「中止生成」放弃;而同样的功能在生成较快时又能正常出候选。这种"时灵时不灵"不是偶发抖动,而是同一个缺陷在大模型时延分布两侧的两种表现。 - -端到端测试把这件事钉死成了铁证:采纳建议用例(accept-suggestion)会 flaky 地变绿,AI 生成用例(ai-generation)则稳定真红。差别只在于那一次大模型回话用了多久——抽到一次较快的生成(实测约 15 秒)就绿,抽到一次较慢的生成(实测约 36 秒)就红,而服务端日志显示真正的完成事件比连接关闭整整晚了约 8 秒落库。换句话说,测试的成败由大模型这次"心情"决定,而不是由代码正确性决定,这本身就是缺陷已经发生的信号。 - -需要澄清的是:大模型上游服务本身是健康在线的,已用直接调用核验过,这不是环境不可用的问题。问题出在我们自己的 SSE 通道把"等待窗口"开得太短,以及前端在连接被提前关闭后没有任何补救。 - -### 1.2 两层根因 - -这里实际叠着两层独立的问题,必须分开讲清楚,否则容易把环境隐患和真实代码缺陷混为一谈。 - -**第一层是环境与启动隐患(已临时手工绕过,但未固化)。** 本地拉起后端的启动脚本只加载了基础设施凭据(数据库、缓存),却没有加载大模型接入所需的那一份凭据配置。后端进程因此拿不到大模型的接入信息,会按设计装上一个"不可用兜底"的运行时客户端,于是任何 AI 任务都会在十几毫秒内秒失败并报"接入不可用"。这一层已经通过手工带齐全部环境变量重启后端验证过、任务能正常完成,但脚本本身没改,下次有人按脚本重启就会复发。它会污染验证现场,让人误以为是别的问题,所以必须连同真实缺陷一起固化掉。 - -**第二层是真正的代码缺陷,落在 SSE 协议与时序上,分布在后端和前端两处,二者叠加才酿成候选永久丢失。** - -后端这一侧,AI 任务的流式服务把连接死线和后台轮询上限都写死成了 30 秒(`MuseAiTaskStreamServiceImpl` 中的 `DEFAULT_TIMEOUT_MILLIS`,既用于构造流式连接的超时,也用于后台轮询循环的截止时间)。它的工作方式是:连接建立后起一个后台循环,反复去库里读这个任务新落的事件并推给浏览器,直到读到完成或失败这种终态事件才正常收尾。问题在于,一旦这 30 秒到点而任务尚未完成,它只是把连接关掉,**既不补发完成事件、也不补发错误事件**——浏览器那头收到的是一个干干净净、什么终态都没有的"流结束"。而大模型的真实出话时延实测在 11 到 60 秒之间剧烈抖动,结构性地骑跨在这条 30 秒死线上。更要命的是,这条 30 秒死线甚至比后端自己调用大模型时所允许的总时延上限(180 秒)还短——也就是说后端给浏览器开的等待窗口,比它自己等大模型的窗口还窄,只要大模型这次用了 30 到 180 秒,上游其实还在正常等、我们的 SSE 却已经先关门了。 - -前端这一侧,AI 生成走的是一条专用的一次性流连接(`sse.ts` 中的 `connectAIStream`)。它读完这一程流就结束,**没有任何重连机制**。对照之下,同一文件里的全局系统事件流(`connectEventStream`)是带重连的:连接非正常结束时会按一组递增退避时延自动重连。AI 流却被刻意设计成一次性、不续传。这就意味着,当后端在 30 秒静默关连接后,前端这条流自然结束,既收不到完成事件去渲染候选,也收不到错误事件去提示失败;它不会再去重连把后端随后几秒才落库的完成事件捞回来。再往上一层,消费这条流的写作台 AI 面板在流结束时同样既不重连也不报错——于是界面就永远停在生成中,用户唯一能做的就是点「中止生成」。 - -把两侧合起来看因果链就清楚了:大模型时延一旦越过 30 秒,后端到点静默空关、不带终态,前端不重连、上层不补救,那条本应稍后到达的候选就被永久地丢在了后端库里,再也送不到界面。这正好解释了为什么"快的生成能出候选、慢的生成出不来",也解释了两个端到端用例为什么一个 flaky 绿、一个稳定红——它们只是同一个缺陷被大模型时延切到了两侧。 - -### 1.3 因果时序(快路径绿 vs 慢路径红,同一缺陷两侧) - -```mermaid -sequenceDiagram - autonumber - participant U as 用户/AI面板 - participant FE as 前端 AI 流
(一次性·不重连) - participant BE as 后端 SSE
(30s 死线) - participant DB as 事件库 - participant LLM as 大模型上游
(总窗口 180s) - - U->>FE: 发起长 AI 生成 - FE->>BE: 建立 SSE 连接(一次性) - BE->>BE: 起后台轮询(死线=30s) - BE->>LLM: 触发生成 - - rect rgb(225,245,230) - note over U,LLM: 快路径(实测约15s<30s)——碰巧绿 - LLM-->>DB: 约15s 落 done 事件 - BE->>DB: 轮询读到 done - BE-->>FE: 推 done 并正常收尾 - FE-->>U: 渲染候选 ✓ - end - - rect rgb(250,228,228) - note over U,LLM: 慢路径(实测约36s>30s)——稳定红 - BE-->>BE: 到 30s 死线,任务未完 - BE-->>FE: 静默 complete()
不补 done / 不补 error - FE-->>FE: 流自然结束,无重连 - FE-->>U: 既无候选也无报错
界面卡「中止生成」 ✗ - LLM-->>DB: 约36s 才落 done(晚 ~8s) - note over DB: 候选已落库,却永远送不到界面 - end -``` - ---- - -## 2. 修法选项对比 - -下面按"第一层环境"和"第二层代码缺陷"分别给出可选项。每项给出意图与做法要点,以及在根治程度、资源占用、用户体验、改动复杂度、兼容性五个维度上的如实权衡——不夸大任何一项。 - -### 2.1 第一层:环境与启动固化(三选一,互不冲突,建议任选其一落地) - -| 选项 | 做法要点 | 根治程度 | 复杂度 | 权衡 | -|---|---|---|---|---| -| A1 脚本固化加载 AI 凭据 | 启动脚本在加载基础设施凭据之外,再加载大模型接入凭据那一份配置,缺失关键变量时直接报错退出 | 高:从源头消除复发 | 极低:脚本几行 | 凭据文件路径成为启动隐式依赖,需在文档写明;不入库不打印 | -| A2 仅文档固化启动步骤 | 不改脚本,在端到端启动文档里写死"必须带齐两份凭据"的操作步骤 | 低:仍靠人记得 | 极低 | 复发风险仍在,依赖自觉,与项目"门禁优先、不靠自觉"的原则相悖 | -| A3 后端缺 AI 凭据时启动告警 | 后端启动自检大模型接入是否就绪,缺失时打一条显眼告警(不泄露凭据值) | 中:缩短误诊时间但不阻止复发 | 低 | 治标不治本,更像 A1 的补充而非替代 | - -第一层的取舍很直接:**A1 是唯一从源头消除复发的做法,且改动极小**;A2 违背门禁优先原则不宜单独采用;A3 可作为 A1 之外的诊断增益,但不能替代 A1。 - -### 2.2 第二层:SSE 时序缺陷(三个候选,①②可独立、③为组合) - -**候选① — 后端放宽死线(最小改)。** 把后端那条 30 秒硬死线放宽到足以覆盖大模型真实时延上限,并尽量做成可配置而非又一个写死的魔数。意图是让 SSE 的等待窗口不再短于上游允许的时延,使绝大多数长生成能在连接存活期内等到完成事件。做法上,由于这条死线常量同时控制连接超时和后台轮询截止,调一处即可同时放宽两者,改动面非常小。 -- 根治程度:中。它把"会丢候选"的时延区间从"超过 30 秒"压缩到"超过新死线",覆盖了实测时延分布,但本质仍是"赌大模型不会比死线更慢"——只要出现极端慢的生成或上游卡顿,到点静默空关、前端不补救的根本结构没变,候选仍会丢。 -- 资源占用:SSE 连接会长开更久,单连接占用一个后台轮询线程的时间相应拉长;在高并发长生成下,线程池压力上升,需要关注轮询线程池容量。 -- 体验:长生成能稳定出候选,体验明显改善;但极端慢的情形仍会无声卡死。 -- 复杂度:最低,单点改动。 -- 兼容:不动 SSE 线格式与事件契约,前端无需任何配合,对其他走同一流路由的路径零影响。 - -**候选② — 前端断连重连(根治侧重)。** 给 AI 流加上断连重连:当流在非终态情况下结束,就按既有的那组递增退避时延自动重连、继续从库里把后续事件捞回来,直到真正读到完成、读到错误、或触及一个前端侧的总时长上限才停。意图是仿照全局系统事件流已经验证过的重连模式,让前端不再"一次性赌一程连接成功"。做法上,把现在那条一次性流改造成可重连的循环,复用现有的退避时延与解析器,并补上一个前端总时长上限作为最终止损。 -- 根治程度:高。它直击"连接被提前关闭后无人补救"这一根本,即便后端死线不变、即便偶发网络抖动断流,前端也能自己续上把候选捞回来;与全局事件流的容错模型归于一致。 -- 资源占用:重连期间会产生若干次额外的重放请求,每次重连后端都要从库里重读并回放已发生的事件,存在重复读放开销;需要确认重放是幂等、不会向界面重复渲染候选。 -- 体验:最稳,长生成、偶发断流都能恢复;代价是出候选可能比一程到底多等一个退避间隔。 -- 复杂度:偏高。AI 流要新增连接状态机、重连计数、续传游标与前端总时长上限,协议复杂度上升;这条流当前注释明确写着"一次性、不续传",改造等于推翻这条既有设计决策,需要相应的测试覆盖。 -- 兼容:不改后端;但 AI 流从"一次性"变为"可重连"是行为语义变化,需回归确认重连不破坏既有的中止生成、候选去重与采纳入参归一。 - -**候选③ — ①+② 组合(兜底加容错)。** 后端把死线放足以覆盖时延上限作兜底,前端再加重连作容错。意图是让两道防线互补:后端死线负责让"正常偏慢"的生成根本不触发断连,前端重连负责兜住"极端慢或偶发断流"这类后端死线也兜不住的尾部情形。 -- 根治程度:最高。正常偏慢由后端窗口吸收、极端与异常由前端重连兜底,两类失败都被覆盖。 -- 资源占用:兼有①的长连接占用与②的重连重放开销,但因为后端窗口已放足,真正触发重连的频率会比单用②低,重放开销随之下降。 -- 体验:最稳,覆盖面最广。 -- 复杂度:等于①与②之和,是三者里最大的。 -- 兼容:同时涉及前后端,回归面最广,需要前后端联调与端到端长任务验证。 - ---- - -## 3. 影响面 - -这次改动触及的是 SSE 协议与时序、用户的 AI 生成体验,以及连接资源占用,需要把波及范围一次说清,避免改完才发现牵连。 - -**SSE 协议与时序。** 候选①只动死线数值、不动线格式与事件契约,对协议无感知影响。候选②把 AI 流从一次性变为可重连,虽然不改单条事件的格式,却改变了"连接生命周期"这一时序契约——它会依赖事件的顺序游标做续传、依赖后端把已发生事件可重放这一既有能力。所幸后端本就是"读库回放"模型、每次连接都会先重放历史事件,这为前端重连续传提供了天然支撑,但前端必须正确携带续传游标,才能避免重连后从头重放。 - -**用户 AI 生成体验。** 三个候选都直接改善长生成出候选的稳定性。需要专门保障的是不能从一个坏体验换出另一个坏体验:重连不能让界面重复渲染候选,也不能干扰用户主动「中止生成」的语义——用户点了中止就该彻底停,而不是被重连机制又拉起来。 - -**连接资源占用。** 这是候选①和③最需要盯的代价。死线放宽意味着每条长生成连接存活更久、相应占用后台轮询线程更久,高并发长生成场景下轮询线程池的容量与饱和拒绝策略必须复核——后端目前在轮询线程池饱和时是直接结束连接的,放宽死线后这条饱和路径被触发的概率会上升。候选②的重连则会带来额外的重放请求量。 - -**与全局事件流的一致性。** 候选②让 AI 流向全局系统事件流的重连模型靠拢,这是正向收敛——两条流此后共用同一套退避与续传心智,降低长期维护成本。反过来说,如果只做候选①而不做②,AI 流就继续是全局体系里唯一一条"不重连"的特例,这个不一致会长期留着。 - -**回归面。** 后端这条 SSE 路由的唯一入口是 `streamTask`,调它的流端点单一;前端这条一次性流的唯一业务调用方是写作台的 AI 面板。回归面是收敛的、清晰的,但正因为入口唯一,任何破坏都会全量影响 AI 生成主路径,验证必须到位。候选③因前后端同改,回归面是三者里最广的。 - ---- - -## 4. 验证计划 - -验证必须以"长任务能否稳定走通"为第一目标,而不是只看一次碰巧变绿。沿用项目"无自动化绿证据不得声称完成"的脊柱原则。 - -**主验收:端到端长任务稳定转绿。** 用现有的 AI 生成与采纳建议两个端到端用例(`muse-studio/e2e/ai-generation.spec.ts`、`accept-suggestion.spec.ts`),在大模型真实时延越过原 30 秒死线的条件下反复执行多轮,要求稳定全绿而非概率性绿——重点是消灭"由大模型时延决定成败"的 flaky 性。验证前必须确认后端带齐了大模型接入凭据(即第一层已固化),否则会回到秒失败的假象,污染结论。 - -**连接与重连压力。** 针对候选①③,在并发长生成下观察轮询线程池是否被长连接占满、是否频繁触发饱和拒绝,确认放宽死线没有把线程池压垮。针对候选②③,制造连接中途断开的情形,确认前端能按退避重连并最终捞回完成事件,且重连有总时长上限、不会无限重试;同时确认重连不会让界面重复渲染候选、不会与用户主动中止冲突。 - -**数据落库不回归。** 确认候选最终的库内落库行为与改前一致——这次改的是"事件怎么送到浏览器",不是"事件怎么生成与持久化",库里 suggestion 的产生与内容不应有任何变化。 - -**回滚预案。** 三个候选都是局部改动,回滚方式统一为 `git checkout` 还原对应文件,无数据迁移、无不可逆状态,回滚成本极低。 - ---- - -## 5. 推荐 - -**推荐采用候选③(后端放宽死线兜底 + 前端断连重连容错),并同时落地第一层的 A1(启动脚本固化加载 AI 凭据)。** - -理由是这次缺陷的本质是"长任务的容错缺失",而大模型时延天然不可控、还会随模型与负载漂移,任何单点修法都治不全。只做后端放宽死线(候选①)改动最小,但它只是把"会丢候选"的时延区间往后挪,赌的是大模型不会更慢,遇到极端慢或上游卡顿仍会无声卡死,治标不治本。只做前端重连(候选②)能根治"连接断了无人补救",但若后端死线仍是 30 秒,几乎每一次长生成都要靠重连去捞,等于把本可避免的断连变成常态、徒增重放开销与一次额外退避等待。把两者组合起来,后端窗口先把正常偏慢的生成稳稳吸收、让重连只在真正异常时才触发,前端重连再兜住后端窗口也兜不住的尾部,既根治又把代价控制在最低,还顺带把 AI 流收敛到与全局事件流一致的容错模型上。第一层 A1 是这一切验证能成立的前提,且改动极小,没有不做的理由。 - -若因排期必须分阶段,建议的落地顺序是:先 A1(消除复发与误诊)→ 再候选①(用最小改动立刻把绝大多数长生成救回来、快速止血)→ 后候选②(补齐根治与一致性)。这样每一步都独立可验、可回滚,且越往后越接近根治。 - -### 5.1 选项关系与覆盖边界(一图收束) - -```mermaid -flowchart TD - P["缺陷:长任务候选丢失"] --> L1["第一层:环境/启动"] - P --> L2["第二层:SSE 时序代码缺陷"] - - L1 --> A1["A1 脚本固化加载 AI 凭据
(消除复发,推荐)"] - L1 -.弱.-> A2["A2 仅文档(靠自觉,不推荐)"] - L1 -.补充.-> A3["A3 缺 AI 凭据启动告警(缩短误诊)"] - - L2 --> C1["候选① 后端放宽死线
最小改 / 治标:赌时延上限"] - L2 --> C2["候选② 前端断连重连
根治断连 / 复杂度升·推翻一次性设计"] - C1 --> C3["候选③ ①+② 组合
正常偏慢靠后端窗口·尾部靠前端重连"] - C2 --> C3 - - C3 --> R["✅ 推荐:A1 + 候选③
排期紧则 A1→①→②"] - - style A1 fill:#d8f0e0 - style C3 fill:#d8f0e0 - style R fill:#cfe8ff - style A2 fill:#f0e0e0 -``` - ---- - -## 6. 待人类 review 的关键决策点 - -以下三处需要人类拍板,文档不替决策、只把利弊摆清。 - -**其一,后端死线放到多少、要不要做成可配。** 实测大模型时延 11–60 秒,而后端调用大模型的总时延上限已配成 180 秒。建议后端 SSE 死线**不应低于上游总时延上限(180 秒)再加 dispatcher 排队与回放余量**,否则就会重演"SSE 比上游先关门"的同一类错配——只放到任务初稿提的 120 秒并不足以覆盖上游 180 秒的尾部。是否进一步做成可配置项(而非又一个写死常量)也请一并定夺,可配的好处是日后随模型调整免改代码,代价是多一个配置面。 - -**其二,要不要上前端重连,即是否推翻 AI 流"一次性、不续传"的既有设计决策。** 这是本方案复杂度与根治度的主要分水岭。上重连换来真正的容错与跨流一致性,代价是 AI 流协议复杂度上升、且需要把现有那条明确写着"一次性"的设计决策连同其注释一起改写并补测试。若评审倾向最小风险、接受"极端慢仍可能卡死"的残余风险,可暂缓②、先只做①与 A1。 - -**其三,重连续传的幂等与候选去重边界谁来保证。** 一旦决定上重连,必须明确:重连后后端重放已发生事件时,前端如何凭顺序游标避免把同一候选重复渲染,以及前端总时长上限取多少才算"够久但不无限"。这条不定清楚,重连可能把"丢候选"换成"重复候选或无限重试",反而引入新缺陷。 diff --git a/design-docs/临时-02-market-install下游物化方案.html b/design-docs/临时-02-market-install下游物化方案.html deleted file mode 100644 index 6f890137..00000000 --- a/design-docs/临时-02-market-install下游物化方案.html +++ /dev/null @@ -1,192 +0,0 @@ - - - - - -临时-02 · Market Install 下游物化方案(人读图) - - - -
-

临时-02 · Market Install 下游物化方案

-
评审版 v1 · 2026-06-26 · 给人一眼看懂:两个物化断点 / 四方案权衡 / 关键决策点 · 配套工程稿见同名 .md
- -
- 用户从市场安装智能体或知识库后点不出效果:知识库检索静默落到“无数据集”而命不中,智能体在生成任务里被运行期直接拒绝。 - 根因不是安装坏了——安装按设计只记账、不物化是对的;错在本该承接物化的目标域绑定路径只写了引用、没造出真实体。 - 推荐 B'(installed_ref 最小留痕,知识库优先共享发布者数据集),物化落点放在目标域绑定路径;完整物化 C 留作远期;暂缓是合理过渡(至少先把“静默断点”改成“诚实标注未开放”)。 -
- -

① 两个物化断点(现状:安装→绑定只写引用,下游必然失败)

-
安装只写 muse_market_installation + 授权投影,targetFactsWritten=false。物化本该在目标域绑定路径发生,但现状那一步也只写了引用。
-
-
-

🔵 知识库 — 断点①:检索“数据集门”拿不到数据集

-
目标域绑定createKnowledgeBinding(消费 handoff token)
-
-
现状只写binding + 来源投影;kbId = parseLong(sourceId)
❌ 不建 muse_knowledge_base 实体
-
-
检索四门:用途 ✓ · 授权 ✓ · 来源状态 ✓
-
-
第四门selectActiveDatasetByKbId(kbId) → null
(无本地 KB → 无 ragflow_binding 数据集行)
-
🔴 记为 no_dataset 静默省略,检索仍 fail-closed 返回。
用户:检索悄无声息命不中,不报错,无从排查。
-
-
-

🟣 智能体 — 断点②:运行期“二次校验”拒绝

-
目标域绑定bindAgentSlot(marketHandoff 仅放宽属主可见性
-
-
现状只写slot binding 裸引用(指向 market 出身 agentId)
❌ 不建 muse_agent / muse_agent_version
-
-
运行期 resolveAgentForTask → requireVisibleAgent
-
-
关键此路径不带 marketHandoff 放宽,按“系统/本人 user + active 版本”校验
-
🔴 AI_AGENT_NOT_EXISTS / SCOPE_FORBIDDEN
用户:AI 任务直接失败,尽管市场标“已安装、已绑定”。
-
-
-
边界澄清:handoff token 不负责物化——市场侧 target owner facade 只生成跳转 URL;真正写目标事实(及未来物化)发生在消费域内部,目标域在自己的事务里核销 token + 写事实。正确链路是 install(记账) → 跳转 → 目标域绑定路径(消费 token + 写绑定 + 物化),不是“install 直接物化”、也不是“handoff 自己物化”。
- -

② B' 物化后:在目标域绑定路径补建实体(同事务、原子)

-
不复制资产,建轻量“安装引用型”实体让下游能解析;底层数据集/配置优先共享发布者的。地基大半已备:muse_knowledge_basekb_type=installed_ref / source_market_asset_id / license_snapshot_id 在 V5 就有。
-
-
-

🔵 知识库 B'

-
createKnowledgeBinding:写 binding + 投影(不变)
-
-
新增 建 muse_knowledge_base
kb_type=installed_refsource_market_asset_id 溯源
-
-
新增 建 ragflow_binding
指向发布者数据集(共享,不复制)
-
-
检索第四门 → 拿到数据集 ✓ → 命中,用户可见效果 🟢
-
-
-

🟣 智能体 B'

-
bindAgentSlot:写 slot binding(不变)
-
-
新增 建 muse_agent(market_installed 型)
+ active muse_agent_version(引用发布者 config)
-
-
需扩展 运行期 requireVisibleAgent
放行“授权有效的 market_installed”
-
-
运行期解析通过 → AI 任务可跑 🟢
-
-
-
智能体侧多一层成本:market_installed 类型有文档无实现——后端-04 写了 muse_agent 区分 system/user/market_installed,但现网 DDL/代码仅 system/user。B' 智能体需先把这类型落成真实 schema + 运行期放行,工程量略大于知识库。可考虑知识库先行、智能体视工程量分阶段
- -

③ 四方案权衡(本质区别 = 物化深度:造多真的实体 / 复制还是共享数据集)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
方案做什么用户价值工程成本数据集跨租户幂等
C 完整物化复制成租户独立 agent/kb 实体 + 复制 RAGFlow 数据集最高 自洽副本,发布者改版/下架不影响最高 数据集克隆+重索引异步重活;agent 类型还要落地复制 · N 倍放大隔离天然干净 避免重复克隆
B' 最小留痕 ★ 推荐建 installed_ref 实体;数据集/配置优先共享发布者 终态等价 C(命中/可跑),最小正确步 kb 地基已备;不碰数据集复制;agent 需补类型共享 · 不放大需验证 共享数据集的隔离 唯一键+命令回放
B 按需物化安装/绑定只留引用,首次使用时惰性物化 终态同 B',但首用有延迟反更高 热路径插写+外调,破坏读写分离共享(同 B')同 B'最难 热路径并发首用
暂缓维持现状 + 诚实标注“未开放” + 留接口位 仍不可用,但消除“静默骗用户”最低 文档/文案为主不涉及不涉及不涉及
-
演进关系:B' ──(当“安装者需独立改造副本/发布者频繁改版冲击安装者”成真实痛点)──▶ C暂缓 是合理过渡兜底,不是“什么都不做”——最低限度也要把现状的静默断点降级为诚实提示。
- -

④ 待人类拍板的关键决策点

-
-

1. 物化深度(最关键)

完整 C / 最小 B' / 暂缓。

倾向 B' 优先,C 远期演进。

-

2. 物化时机

安装同步 / 绑定同步 / 异步 worker / 惰性首用。

倾向绑定同步(与 token 消费原子);不选惰性。

-

3. 数据集:共享 vs 复制 ⚠️

B' 与 C 的分水岭,也是 B' 唯一硬风险点。

选共享前必须先验证多租户共享同一 RAGFlow 数据集的隔离是否隔得住。

-

4. 智能体 market_installed 落地

是否接受“补 schema + 运行期放行”工程量。

若暂不接受,可知识库先行、智能体暂缓。

-

5. 授权快照承载

沿用 ADR-020 字符串 envelope(VARCHAR)。

market_installation 两列 BIGINT 残留建议不纳入本方案,作独立契约议题。

-

6. 来源溯源与回滚

经 source_market_asset_id 溯源;来源召回/下架的处置。

倾向复用 muse_source_propagation_target:阻断新使用、不自动删。

-
- -
- - diff --git a/design-docs/临时-02-market-install下游物化方案.md b/design-docs/临时-02-market-install下游物化方案.md deleted file mode 100644 index 20415122..00000000 --- a/design-docs/临时-02-market-install下游物化方案.md +++ /dev/null @@ -1,326 +0,0 @@ -# 临时-02 · Market Install 下游物化方案(评审版) - -- 版本:v1(评审版,待人类 review 后拍板,不改任何代码) -- 更新日期:2026-06-26 -- 目标读者:架构 / 后端 / 产品 / PR reviewer -- 阅读时间:20–30 分钟 -- 文档性质:**临时件**(评审/决策依据,落定后归并入正式分册或删除,不作长期 SSOT)。本稿只做问题诊断、方案对比与决策收束,不改代码、不定义新概念;术语以 `架构-02` 为准,表结构归 `后端-04`,跨 BC 协作与 handoff 边界归 `架构-01` / `架构-02` §7,授权快照承载归 ADR-020。 -- 配套人读图:[`临时-02-market-install下游物化方案.html`](临时-02-market-install下游物化方案.html) - -> 一句话结论:用户从市场安装一个智能体或知识库后,**点不出可用效果**——知识库检索永远静默落到“无数据集”而命不中,智能体在生成任务里被运行期直接拒绝。根因不是安装坏了,而是安装按设计与 AI/知识两域**完全解耦、只记账不物化**,而本该承接物化的“绑定/槽位替换”这一步当前也只写了**引用绑定**、没有在安装者租户里造出真正可用的实体。本文把四个物化深度(完整物化 C / 最小留痕 B' / 纯引用按需物化 B / 暂缓)摆开权衡,**推荐以 B'(installed_ref 最小留痕,知识库优先共享发布者数据集)作为打通“可真用”的最小正确步**,完整物化 C 作为远期演进,并明确把物化落点放在**目标域的绑定路径**而非安装本身。 - ---- - -## 1. 问题与现状 - -### 1.1 用户视角的症状 - -一个普通用户在市场里看中一个智能体资产或知识库资产,走完“获取授权 → 安装到账户”,界面会如实告诉他“已安装”。接着他按产品引导进入作品工作台,把这个知识库绑定到作品的知识来源、或把这个智能体替换进作品的某个开放槽位,绑定动作也会成功返回。一切看起来都对。 - -然后他开始写作、触发 AI 生成,问题就暴露了: - -- **装的是知识库**:生成出来的内容完全没有用到这个知识库里的资料。检索环节没有报错,也没有任何“这个来源不可用”的提示,知识库就像不存在一样被静悄悄跳过了。用户会以为是自己资料没选对,反复检查也找不到原因。 -- **装的是智能体**:AI 任务直接失败,提示“智能体不存在”或“无权使用该智能体”,尽管这个智能体在市场上明明白白标着“已安装、已绑定槽位”。 - -也就是说,**对市场安装来的智能体/知识库,“已安装、已绑定”到“真正生效”之间存在一道断崖**。产品文档(`产品-02D` / `产品-02E` / `流程-02B`)描述的理想链路是“可发现 → 已授权 → 已安装到账户 → 已绑定作品 → 已用于生成/检索”,现状卡死在最后一跳。 - -需要先澄清一个容易误判的点:这不是市场发布侧的问题。发布者把自己的智能体/知识库上架成市场资产(admin 审核通过、`markListed`)这条链路是通的、已活体验证过。本文要解决的是**反方向**——安装者把市场资产变成自己租户里**能真正跑起来**的实体,即“install 侧下游物化”。 - -### 1.2 设计意图:安装解耦是对的,物化本就该在目标域发生 - -先肯定现状的合理部分,避免把正确的解耦当成 bug 去“修”。 - -按 `架构-02` 与 `流程-02B` 的设计,市场的边界非常清楚:**授权不等于安装,安装不等于绑定,绑定不等于写入作品事实**。市场只负责发现、授权、安装记账和发起一次性跳转(handoff),它**不是**任何作品事实、智能体绑定、知识来源绑定的最终写入空间。安装这一步只写“账户可用资产”,刻意不去碰 AI、知识、内容三域的事实——这是数据主权和 owner 边界的体现,是对的,**不应该改**。 - -那么物化(把市场资产变成可用实体)本该在哪里发生?答案也在设计里:**目标域的绑定/槽位替换路径**。用户从市场跳转进入知识库工作台/智能体工作台后,由目标域 owner 自己做预检、消费一次性 handoff token、在自己的事务里写下 owner 事实。换句话说,**“物化”这个动作的正确落点是目标域的 `createKnowledgeBinding` / `bindAgentSlot`,而不是市场的 `installMarketplaceAsset`**。 - -现状的真正缺陷因此可以精确表述为:**目标域的绑定路径只写了“引用绑定”,没有顺手把可用实体物化出来。** 安装的解耦没错,错在物化这一步在目标域被遗漏了。 - -### 1.3 两个物化断点的机理 - -#### 断点①:知识库检索的“数据集门”永远拿不到数据集 - -知识库检索对每个绑定的来源顺序过四道门(`MuseKnowledgeRetrievalApiImpl.retrieveForWork`,`muse-module-knowledge/.../api/MuseKnowledgeRetrievalApiImpl.java:96-137`):用途门(绑定用途须含检索)、授权快照门(须有授权快照)、来源状态门(须有来源状态投影且状态未被阻断)、以及**数据集门**。前三门,市场安装来的知识库都能过:绑定时写了检索用途、写了授权快照、写了来源状态投影(状态 active)。卡死在第四门。 - -第四门做的事是(`MuseKnowledgeRetrievalApiImpl.java:122-127`): - -``` -dataset = selectActiveDatasetByKbId(kbId) -if (dataset == null || dataset.ragflowDatasetId 为空) { - 记为 OmittedSource(kbId, "no_dataset", ...) // 静默省略,不报错 - continue -} -``` - -它要求本地存在一行把这个知识库映射到一个 RAGFlow 数据集的记录(`muse_knowledge_ragflow_binding` 表里 `kb_id=? AND status='active' AND document_id IS NULL` 的“数据集级”行)。而这行**只在“本地上传文档、首次为该知识库创建 RAGFlow 数据集”时才会落库**(`MuseKnowledgeDocumentService.persistDatasetBinding`)。 - -市场安装路径从不上传文档、从不创建数据集,所以这行根本不存在。更深一层的原因是:**绑定时根本没有为这个安装来的知识库创建本地知识库实体**。绑定服务(`MuseKnowledgeBindingService.createKnowledgeBinding`,`muse-module-knowledge/.../application/muse/MuseKnowledgeBindingService.java:129`)只 insert 了两行——一行绑定事实、一行来源状态投影——而且它写入的 `kbId` 是直接把外部来源 id(`sourceId`)当字符串解析成数字塞进去的,**并不是任何已物化的本地知识库主键**。没有本地知识库实体,自然就没有它名下的数据集映射,第四门必然返回 null。 - -后果是“静默命不中”:检索把这个来源记为 `no_dataset` 并入“被省略来源”,但整个检索仍按 fail-closed 设计返回(空或部分结果),**不抛错、不阻断主生成链**。用户因此看不到任何报错,只是结果里没有这个知识库的内容。这种“假装成功的失败”比直接报错更难排查。 - -> P1 阶段曾有一条 knowledge handoff 的 e2e 测试是绿的,那是 fixture 巧合——测试用的资产恰好映射到一个种子数据里已存在数据集的 kb_id,绕过了这个断点,不代表真实安装链路通。 - -#### 断点②:智能体在运行期被“二次校验”拒绝 - -智能体这边的机理不同,关键在于**绑定期和运行期用了两套宽严不同的校验**。 - -绑定(槽位替换)期(`MuseAgentSlotServiceImpl`,`muse-module-ai/.../application/muse/MuseAgentSlotServiceImpl.java`):对市场来源的绑定,预检和绑定都调 `requireVisibleSourceAgent`,但带了一个 `marketHandoff=true` 的放宽参数,**只放宽了“智能体属主可见性”这一项**(即接纳类型为 market、非本人所有的智能体),其合法性改由市场一次性 handoff token 背书。需要强调的是:**“智能体存在且 active、且有 active 版本”这一条,绑定期并没有放宽**。 - -运行期(`MuseAiTaskServiceImpl.resolveAgentForTask` → `requireVisibleAgent`,`muse-module-ai/.../application/muse/MuseAiTaskServiceImpl.java:306,321`):创建 AI 任务时,用绑定里存的智能体 id 重新解析一次真实智能体。这条路径**不带任何 marketHandoff 放宽**,一律按“系统智能体、或本人所有的用户智能体,且必须有 active 版本”来校验,否则抛 `AI_AGENT_NOT_EXISTS` / `AI_AGENT_SCOPE_FORBIDDEN`。 - -把两者合起来:市场安装从不在安装者租户里物化任何智能体实体(`muse_agent` / `muse_agent_version`),槽位绑定写进去的只是一个**裸引用**(一个指向 market 出身的 agentId)。到了运行期,这个裸引用被重新解析,既找不到对应的本地智能体行,即便找到也过不了“本人所有 + active 版本”的校验,于是被拒。这就是“槽位放宽接纳、运行期拒绝”的断层。 - -> 一个智能体要在运行期真正可用,最小充分条件是三条:①安装者租户里有 `muse_agent` 行且 status=active;②可见(系统智能体,或本人所有的用户智能体);③有一行 status=active 的 `muse_agent_version`。三者缺一即被运行期拒。 - -### 1.4 现状代码诚实标注(无伪装,缺口是已知的) - -值得肯定的是,现状代码没有用假数据掩盖缺口,到处是诚实的占位标注:安装服务把绑定摘要显式写成 `targetFactsWritten=false`(`MarketInstallServiceImpl.java:95-96,199`);知识库侧标注“当前没有 Market 安装表 / 现阶段没有独立 installed 表”;market 模块 `.agent` 明确把“install 后下游物化(资产变可用 agent/kb)”列为仍缺的 TODO。**这意味着本方案不是去推翻一个错误实现,而是去补一个被有意识地推迟了的物化步骤。** - ---- - -## 2. 物化方案对比(C / B' / B / 暂缓) - -四个方案的本质区别是**物化深度**:把市场资产变成安装者可用实体时,造多“真”的实体、复制还是共享底层数据集。下面每个方案给出做什么(WHAT)、怎么做的要点(HOW),以及在用户价值、工程成本、跨租户、数据集复制还是共享、授权追溯、幂等六个维度上的权衡。 - -> 重要前提(影响成本判断):`后端-04` 与现网 schema **已为物化预留了一半地基**。知识库根表 `muse_knowledge_base` 在 V5 就带了 `kb_type`(取值含 `installed_ref`)、`source_market_asset_id`、`license_snapshot_id` 三列,只是绑定/安装路径从不写它。智能体侧则**有文档无实现**:`后端-04` 写了 `muse_agent` 区分 `system/user/market_installed`,但现网 `muse_agent` 的建表与代码只用 `system/user`,`market_installed` 从未落地。这条不对称直接影响 C/B' 在两域的工程量。 - -### 2.1 方案 C:完整物化 - -**WHAT**:安装(或首次绑定)时,把市场资产**复制成安装者租户内一份独立、自洽的实体**。知识库 → 在安装者租户新建一行 `muse_knowledge_base`(`kb_type=user` 或独立类型),并**复制**一份 RAGFlow 数据集(把发布者数据集的文档/切块克隆到安装者名下的新数据集);智能体 → 在安装者租户新建 `muse_agent` + active `muse_agent_version`,复制发布者的 prompt/模型绑定/参数配置。 - -**HOW 要点**:安装链路新增对 knowledge/ai 的 facade 调用(或事件),触发“按市场资产快照建实体”;知识库需调 RAGFlow 创建新数据集并发起文档复制 + 重新解析/切块/索引(这是异步重活);智能体需把发布版本 config 落成本地 active 版本;两者都要在物化实体上写 `source_market_asset_id` 之类的来源溯源列。 - -**权衡**: - -- 用户价值:最高。安装者拿到完全自洽、可独立演进的副本,发布者后续下架/改版不影响已安装副本的可用性(除非授权失效)。 -- 工程成本:最高。数据集复制涉及跨 RAGFlow 的文档搬运 + 全量重解析重索引,是异步长任务,要处理失败/重试/部分成功;智能体侧还要先把 `market_installed` 类型从“文档概念”落成真实 schema + 代码分支。 -- 跨租户:副本天然隔离,无共享带来的可见性纠葛。 -- 数据集:**复制**。存储和索引成本随安装数线性放大(N 个安装者 = N 份数据集)。 -- 授权追溯:副本与来源的关系靠 `source_market_asset_id` + 授权快照维系;但副本一旦独立,来源召回时要不要连带停用副本,是个需要明确的策略问题。 -- 幂等:复制是重操作,重复安装的幂等更难做(要避免重复克隆数据集),需在物化任务层做幂等键。 - -### 2.2 方案 B':installed_ref 最小留痕(推荐) - -**WHAT**:不复制资产,而是在安装者租户**建一行轻量的“安装引用型”实体**,让检索/运行期能解析到它,底层数据集/配置**优先共享**发布者的。知识库 → 建一行 `muse_knowledge_base`(`kb_type=installed_ref`、`source_market_asset_id` 指回市场资产、`license_snapshot_id` 记授权来源),并为它建一行**指向发布者已有 RAGFlow 数据集**的 `muse_knowledge_ragflow_binding`(数据集级、status=active),使第四门能拿到数据集;智能体 → 建一行 `muse_agent`(`market_installed` 型,溯源指回市场资产)+ 一行引用发布者版本 config 的 active `muse_agent_version`,使运行期 `requireVisibleAgent` 能解析通过。 - -**HOW 要点**:物化落点放在**目标域绑定路径**(知识库 `createKnowledgeBinding:129`、智能体 `bindAgentSlot`)——这两处本就在消费 handoff token、本就在写绑定事实的同一个事务里,顺手补建 installed_ref 实体 + 数据集映射(知识库)/ active 版本(智能体)即可,原子性天然有保证。知识库的数据集门改为:解析 kbId 时若是 installed_ref,则用其 `source_market_asset_id` 找到发布者知识库的数据集 id 来填 `muse_knowledge_ragflow_binding`。运行期智能体解析需要识别 `market_installed` 型并按授权快照放行(这要求把 `requireVisibleAgent` 的校验从“仅 system/属主 user”扩展到“+ 授权有效的 market_installed”)。 - -**权衡**: - -- 用户价值:高,且是**打通“可真用”的最小正确步**。检索能命中、智能体能跑,用户拿到的就是发布者那份内容/能力。 -- 工程成本:中。知识库侧地基已备(`installed_ref` + `source_market_asset_id` 在 V5 就有),主要工作是绑定路径补建实体 + 第四门兼容 installed_ref,**不碰 RAGFlow 文档复制这件重活**;智能体侧需要先把 `market_installed` 落成真实 schema + 运行期校验放行,工程量略大于知识库。 -- 跨租户:**这是 B' 的主要风险点**。多个安装者的 installed_ref 知识库共享发布者同一个 RAGFlow 数据集,检索时跨租户读同一份索引,必须确认 RAGFlow 侧/检索门的租户与授权隔离不被绕过;而且 `muse_knowledge_ragflow_binding` 有唯一约束 `(tenant_id, kb_id) WHERE status='active' AND document_id IS NULL`(一个 kb 一个 active 数据集),当前 kbId 取的是 sourceId 字面值,跨租户/跨用户复用同一 kbId 值时这个唯一键的行为必须在方案里明确(建议 installed_ref 用安装者租户内新分配的 kb 主键,而非沿用 sourceId)。 -- 数据集:**共享**。存储/索引成本不随安装数放大,是 B' 相对 C 的最大成本优势。 -- 授权追溯:清晰。installed_ref 行的 `source_market_asset_id` + `license_snapshot_id` 把“我能用是因为装了哪个市场资产、凭哪份授权”钉死,符合 `架构-02` “引用对象必须保存快照 id”的要求。 -- 幂等:轻量 insert,配合“按 (作品, 来源) 或 (租户, 市场资产) 唯一 + 命令回放”即可幂等,远易于 C 的数据集克隆幂等。 - -### 2.3 方案 B:纯引用 + 按需物化 - -**WHAT**:安装和绑定都**只留引用**(维持现状的引用绑定),把物化推迟到“**第一次真正使用时**”惰性触发——知识库第一次被检索命中前、智能体第一次被任务解析前,才即时建实体/数据集映射。 - -**HOW 要点**:在检索第四门返回 null 的分支、运行期智能体解析失败的分支里,不直接判负,而是先尝试“按引用惰性物化”再重试一次;物化产物落 installed_ref(同 B' 的实体形态),只是触发时机从绑定推迟到首次使用。 - -**权衡**: - -- 用户价值:终态与 B' 相同(能命中、能跑),但**首次使用有延迟**(首检索/首生成要等物化完成,知识库若涉及数据集准备甚至是秒级以上)。 -- 工程成本:**反而比 B' 高**。惰性物化要在“读路径/运行热路径”里插入写操作和外部调用,破坏了读写分离,要处理并发首用的竞态(两个请求同时触发物化)、热路径里的失败回退、以及“物化中”这个中间态的用户反馈。把复杂度从一次性的绑定路径挪到了高频的使用路径,得不偿失。 -- 跨租户 / 数据集 / 授权追溯:与 B' 同(产物一样是 installed_ref + 共享数据集)。 -- 幂等:最难。热路径并发首用的幂等/加锁是这个方案的主要痛点。 - -### 2.4 方案:暂缓(维持现状 + 文档化 + 留接口位) - -**WHAT**:不做物化。维持安装只记账的现状,但**把“市场资产暂不可直接用”这件事在产品和文档层显式化**:安装/绑定成功后明确提示“该资产已安装,作品内生效能力即将开放”,避免用户撞上静默断点;同时在目标域绑定路径留好物化的接口位(注释 + TODO 锚点),为后续 B'/C 铺路。 - -**HOW 要点**:不动后端物化逻辑;改产品文案与状态展示,把当前的“假装可用”降级为“诚实地标注未开放”;在 `createKnowledgeBinding` / `bindAgentSlot` 留物化扩展点说明。 - -**权衡**: - -- 用户价值:低(功能仍不可用),但**消除了“静默骗用户”这一最坏体验**——用户不再误以为生效了。 -- 工程成本:最低。基本是文档和文案工作。 -- 跨租户 / 数据集 / 授权追溯:不涉及(不物化)。 -- 幂等:不涉及。 -- 适用前提:市场安装在当前产品优先级里**不是近期主路径**(no-config 主创作链路不依赖它,见 `流程-02B` 第15条),可以接受先不可用、先不骗人。 - ---- - -## 3. 数据模型增量 - -本节只说**相对现状要补什么**,不重复 `后端-04` 已有定义。owner 边界以 `后端-04` 为准,本稿不新增概念。 - -### 3.1 知识库侧(B'/C 共用,地基大半已备) - -| 项 | 现状 | 增量 | -|---|---|---| -| `muse_knowledge_base.kb_type` | V5 已有,取值含 `installed_ref` | B'/C 实际写入 `installed_ref`(现状从不写本表) | -| `muse_knowledge_base.source_market_asset_id` | V5 已有列 | 物化时回填,承载“来源市场资产”溯源 | -| `muse_knowledge_base.license_snapshot_id` | V5 已有列 | 物化时回填授权来源快照 | -| `muse_knowledge_ragflow_binding`(kb→数据集映射) | 仅本地上传文档时创建 | B':为 installed_ref 建一行**指向发布者数据集**的数据集级映射;C:建指向**新复制数据集**的映射。注意唯一键 `(tenant_id, kb_id) WHERE status='active' AND document_id IS NULL` | - -### 3.2 智能体侧(B'/C 共用,需先补真实 schema) - -| 项 | 现状 | 增量 | -|---|---|---| -| `muse_agent.agent_type` | 现网 DDL/代码仅 `system`/`user`(`market_installed` 仅见于 `后端-04` 文档,未落地) | **需新增 `market_installed` 落地**(schema 值 + 创建/解析分支),消除文档与实现的漂移 | -| `muse_agent` 来源溯源列 | 无 | 需补一列指回市场资产(参照知识库 `source_market_asset_id` 命名),承载安装智能体溯源 | -| `muse_agent_version` | 有 | 物化时写一行 active 版本(B' 引用发布者 config,C 复制 config) | - -### 3.3 安装→实体映射 与 授权快照承载 - -- **install→agent/kb 映射**:B'/C 的物化实体已通过 `source_market_asset_id`(kb)/ 新增溯源列(agent)指回市场资产,再经市场资产关联到安装记录,链路可追。是否需要一张显式的“安装→物化实体”映射表取决于是否要支持“一个安装在多作品复用同一物化实体”——建议初版不建,靠 (作品, 来源) 维度的绑定唯一键表达。 -- **ADR-020 字符串授权快照承载**:智能体槽位绑定的授权快照列已在 V31 收口为 `VARCHAR(128)` 直透传(承载 `rpe-local-` 形态 envelope),知识库相关表在 V14 已先行字符串化。物化实体若要直接携带字符串 envelope,应**沿用 VARCHAR(128) + 直透传**,不要回退 BIGINT/parseLong。 -- **ADR-020 在 market 的残留缺口(需评估)**:`muse_market_installation.authorization_snapshot_id` / `source_snapshot_id` 当前仍是 **BIGINT**(V6),V30/V31 未覆盖 market 表。现状自洽——install 存的是授权快照行的**数值主键**而非 envelope 字符串。但如果某个物化方案要让**安装事实本身**直接携带字符串 envelope,这两列的 BIGINT 会成为障碍,需随方案评估是否 widen。这是一个独立的契约小议题,**不属于本物化方案的必改项**,列此备查。 - -### 3.4 数据集关联 / 复制:共享 vs 复制是核心岔路 - -这是 C 与 B' 在数据层最本质的区别,单列强调: - -- **B'(共享)**:installed_ref 知识库不拥有自己的数据集,其 `muse_knowledge_ragflow_binding` 指向**发布者数据集 id**。成本不随安装数放大,但引入跨租户读同一份 RAGFlow 索引的隔离问题——必须验证检索门的租户/授权隔离在“多租户共享数据集”下不被绕过。 -- **C(复制)**:每个安装者拥有独立数据集,需跨 RAGFlow 克隆文档 + 重解析重索引。隔离天然干净,但存储/索引成本 N 倍放大,且复制是异步长任务(失败/重试/幂等都更重)。 - ---- - -## 4. 跨模块链路 - -### 4.1 物化触发:直接 facade vs 事件驱动 source 传播 - -两种触发风格,对应 `架构-03` 里两条既有原则: - -- **直接 facade(同步、目标域内联)**:物化发生在目标域绑定路径自己的事务里(B'/C 推荐这条)。优点是与“写绑定事实 + 消费 handoff token”天然原子(现状 handoff token 的消费就是这么做的——consume 绝不切独立事务、沿用 owner 事务),失败即整体回滚,无最终一致性窗口。契合 `架构-02` §7“目标 owner 必须自己创建并消费 target precheck、自己写 owner 事实”。 -- **事件驱动 source 传播(异步、各模块自治)**:安装发事件、knowledge/ai 监听后异步物化(ADR-017 的 Source 传播模式)。优点是安装与物化解耦、可独立重试;缺点是引入最终一致性窗口(安装成功但物化未完成时用户看到的中间态需处理),且对“物化必须在用户确认/绑定时发生”的产品语义不如同步贴合。 - -**判断**:物化的正确触发点是**绑定/槽位替换**(用户在目标域的显式确认动作),不是安装本身。因此**直接 facade 同步物化**更贴设计,事件驱动更适合用于“来源召回/下架时反向停用已物化实体”的传播(这正是 `muse_source_propagation_target` 已定义 `ai_agent_slot_binding`/`market_install` 等传播目标的用途)。 - -### 4.2 与 handoff(P2/P3)的边界:handoff 不是物化桥,物化在消费域 - -这是最容易踩错的边界,必须钉死:**handoff token 不负责物化**。市场侧的 target owner facade 只生成一个跳转 URL,明确不调 AI/Knowledge/Content、不写目标事实;真正写目标事实(以及未来的物化)发生在**消费域内部**——目标域调 `MarketHandoffTokenApi.consume()` 在自己的事务里核销 token、同步写自己的事实。 - -因此正确的链路是 **install(记账)→ 用户跳转 → 目标域绑定路径(消费 token + 写绑定 + 物化实体)**,而**不是** “install 直接物化”,也不是“handoff 自己物化”。物化是目标域绑定路径多做的一步,token 消费只是这步事务里的一环。这与 `流程-02B` §13 “来源空间只能发起跳转,不能替目标 owner 写最终事实;目标 owner API 必须消费预检结果和用户确认后才写 owner 事实”完全一致。 - -### 4.3 链路全景(现状断点 + B' 物化后) - -见 §6 mermaid 与配套 HTML。要点:现状链路在“目标域绑定”这一格只写了引用、止步于此;B' 在同一格内补上 installed_ref 实体 + 数据集映射/active 版本,使下游的“检索第四门 / 运行期智能体解析”从必然失败转为可通过。 - ---- - -## 5. 决策点(待人类拍板) - -下面是需要人来拍的关键岔路,每条给出选项与倾向,但**最终深度/时机/共享策略由人决策**。 - -1. **物化深度(最关键)**:完整复制 C / 最小留痕 B' / 暂缓。倾向 **B' 优先**(最小正确步、不碰数据集复制重活、地基大半已备),C 作为远期演进(当“安装者需要独立改造副本/发布者频繁改版冲击安装者”成为真实痛点时再上)。 -2. **物化时机**:安装同步 / 绑定同步 / 异步 worker / 惰性首用。倾向 **绑定同步**(落点在目标域绑定路径,与 token 消费原子),不选惰性首用(把复杂度推进高频热路径,幂等/竞态最难)。 -3. **数据集:共享 vs 复制**。这是 B' 与 C 的分水岭,也是 B' 唯一的硬风险点。选共享(B')必须先回答:多租户共享同一 RAGFlow 数据集时,检索的租户/授权隔离是否真隔得住?这点需要一次针对性验证才能拍。 -4. **智能体 `market_installed` 落地**:是否接受“先补 schema + 运行期校验放行”这部分工程量。若暂不接受,可考虑**知识库先行 B'、智能体暂缓**的分阶段(知识库地基更全、风险更小)。 -5. **授权快照承载**:物化实体的授权溯源沿用 ADR-020 字符串 envelope(VARCHAR);是否一并处理 `muse_market_installation` 两列 BIGINT 的残留(建议**不纳入本方案**,作为独立契约议题)。 -6. **来源溯源与回滚**:物化实体经 `source_market_asset_id` 溯源;来源被召回/下架/撤权时,已物化实体的处置策略——是停用(阻断新使用、不删)还是保留只读?倾向复用 `muse_source_propagation_target` 的既有传播目标机制做“阻断新使用、不自动删”,与 `架构-02`“来源事件不删除正式知识/正文”一致。 -7. **幂等**:重复安装/重复绑定时物化不得重复造实体。B'/C 都需在物化处加幂等键(B' 轻、C 重);惰性方案的热路径并发幂等最重(又一条不选惰性的理由)。 - ---- - -## 6. 文本+mermaid 图(给 AI / 工程) - -### 6.1 现状两断点(用户视角到机理) - -```mermaid -flowchart TD - A["用户: 市场获取授权"] --> B["安装到账户
installMarketplaceAsset"] - B --> B1["只写 muse_market_installation
+ license 投影
targetFactsWritten=false"] - B1 --> C["用户跳转 → 目标域绑定"] - - C --> K["知识库: createKnowledgeBinding
(消费 handoff token)"] - C --> G["智能体: bindAgentSlot
(消费 handoff token, marketHandoff 放宽属主可见性)"] - - K --> K1["只写 binding + 来源投影
kbId = parseLong(sourceId)
❌ 不建 muse_knowledge_base"] - G --> G1["只写 slot binding 裸引用
❌ 不建 muse_agent / version"] - - K1 --> KR["检索四门: 用途✓ 授权✓ 来源状态✓
第四门 selectActiveDatasetByKbId → null"] - KR --> KX["🔴 断点①: no_dataset 静默省略
用户: 检索悄无声息命不中, 无报错"] - - G1 --> GR["运行期 resolveAgentForTask → requireVisibleAgent
(无 marketHandoff 放宽)"] - GR --> GX["🔴 断点②: AI_AGENT_NOT_EXISTS / SCOPE_FORBIDDEN
用户: AI 任务直接失败"] - - style KX fill:#fde2e2,stroke:#c0392b - style GX fill:#fde2e2,stroke:#c0392b - style B1 fill:#eef3fb,stroke:#3b6fb6 -``` - -### 6.2 B' 物化后的链路(物化落在目标域绑定路径) - -```mermaid -flowchart TD - A["用户: 安装(记账, 不变)"] --> C["用户跳转 → 目标域绑定路径"] - C --> K["知识库 createKnowledgeBinding
同一事务内"] - C --> G["智能体 bindAgentSlot
同一事务内"] - - K --> K1["写 binding + 来源投影 (原有)"] - K --> K2["✅ 新增: 建 muse_knowledge_base
kb_type=installed_ref
source_market_asset_id 溯源"] - K2 --> K3["✅ 新增: 建 ragflow_binding
指向发布者数据集(共享)"] - K3 --> KR["检索第四门 → 拿到数据集 ✓"] - KR --> KO["🟢 检索命中, 用户可见效果"] - - G --> G1["写 slot binding (原有)"] - G --> G2["✅ 新增: 建 muse_agent (market_installed)
+ active muse_agent_version"] - G2 --> GR["运行期 requireVisibleAgent
(扩展: 放行授权有效的 market_installed)"] - GR --> GO["🟢 智能体解析通过, AI 任务可跑"] - - style K2 fill:#e2f7e8,stroke:#27ae60 - style K3 fill:#e2f7e8,stroke:#27ae60 - style G2 fill:#e2f7e8,stroke:#27ae60 - style KO fill:#e2f7e8,stroke:#27ae60 - style GO fill:#e2f7e8,stroke:#27ae60 -``` - -### 6.3 四方案权衡一览 - -```mermaid -flowchart LR - subgraph C["C 完整物化"] - C1["复制实体 + 复制数据集
价值最高 / 成本最高
数据集 N 倍放大 / 隔离干净"] - end - subgraph BP["B' 最小留痕 (推荐)"] - BP1["installed_ref + 共享数据集
价值高 / 成本中
跨租户隔离需验证 / 地基大半已备"] - end - subgraph B["B 纯引用 + 按需物化"] - B1["首用惰性物化
终态同 B' / 成本反高
热路径竞态幂等最难"] - end - subgraph H["暂缓"] - H1["维持现状 + 诚实标注未开放
价值低 / 成本最低
消除静默骗用户"] - end - BP1 -.演进.-> C1 -``` - ---- - -## 7. 验证计划 + 推荐 - -### 7.1 推荐 - -**推荐采用方案 B'(installed_ref 最小留痕),物化落点放在目标域绑定路径,知识库优先共享发布者数据集;并优先做知识库、智能体视 `market_installed` 落地工程量决定是否同期。** 暂缓作为合理的过渡兜底——若近期不投入物化,至少应立刻把现状的“静默断点”降级为“诚实标注未开放”,止住骗用户的最坏体验。 - -推荐理由: - -- B' 是“让市场安装资产真正可用”的**最小正确步**:终态与 C 等价(检索命中、智能体可跑),但避开了 C 最重的数据集复制工程。 -- **地基已备**:知识库 `installed_ref` + `source_market_asset_id` + `license_snapshot_id` 在 V5 就有,B' 主要是“去写本就该写的本地实体 + 第四门兼容 installed_ref”,而非新建表。 -- **落点正确**:物化放在目标域绑定路径,与现有 handoff token 消费同事务,原子性天然成立,完全契合 `架构-02` §7 / `流程-02B` §13 的 owner 边界,不破坏“安装解耦”这个对的设计。 -- **不偏袒**:暂缓是合理候选——物化是跨模块工程,若产品优先级不在此(no-config 主链路不依赖市场安装),先文档化“暂不可用”、不骗用户,是诚实且低成本的选择。C 不推荐为首选仅因成本与时机,不否定其远期价值。 - -不推荐 C 作首选:数据集复制是异步长任务重活、`market_installed` 智能体还要从文档落到实现,时机和成本都不划算,留作远期演进。不推荐 B(惰性):把复杂度推进高频热路径,幂等/竞态最难,终态又不比 B' 更好。 - -### 7.2 验证计划(方案落地后如何证明“真可用”,非本稿执行项) - -- **知识库 B' 端到端**:安装市场知识库 → 作品绑定 → 触发 AI 生成 → 检索第四门拿到(发布者)数据集 → chunk 真命中、生成内容含该来源 grounding。对照现状(必然 `no_dataset` 省略)。 -- **智能体 B' 端到端**:安装市场智能体 → 槽位替换 → 创建 AI 任务 → 运行期 `requireVisibleAgent` 解析通过 → 真 LLM 出候选。对照现状(必然 `AI_AGENT_NOT_EXISTS`/`SCOPE_FORBIDDEN`)。 -- **跨租户隔离(B' 共享数据集的硬验证)**:两个不同租户安装同一发布者知识库,确认 A 租户检索不会越权读到 B 租户作品上下文、授权隔离不被共享数据集绕过。**这是拍 B' 前必须先做的一次针对性验证。** -- **幂等**:重复安装 + 重复绑定,确认物化实体不重复创建(命令回放 + 唯一键)。 -- **回滚/来源传播**:发布者召回/下架/撤权后,已物化 installed_ref 实体被阻断新使用(经 `muse_source_propagation_target`),但不自动删除,符合 `架构-02` 来源事件语义。 -- **机械门禁**:物化涉及跨 BC 写入,须确认不违反 `bc-boundaries` ArchUnit(物化只能经目标 owner 自己的 facade 写自己的实体,不能让 market 直写 knowledge/ai 的 `.dal`)。 - ---- - -## 8. 关联阅读 - -- 双轨模型 / Source / Authorization / 市场资产模型 / handoff 边界:`架构-02-核心数据结构与双轨模型.md`(§4 来源与授权、§5 智能体运行权限、§6 市场资产、§7 handoff、§11.2 绑定安装授权) -- BC 边界与跨域协作规则:`架构-01-系统全貌与边界上下文.md` -- 关键决策:`架构-03-关键决策与原则(ADR).md`(ADR-016 Governance 拆散、ADR-017 Source 传播事件驱动、ADR-020 授权快照字符串承载) -- 表结构 SSOT:`后端-04-统一数据库Schema-v1.md`(§5 Knowledge、§6 Source/Authorization、§7 AI/Agent、§8 Market) -- 用户系统链路:`流程-02B-普通用户系统处理流程(系统视角).md`(§12 市场资产链路、§13 跨空间 handoff) -- 产品规格:`产品-02D-智能体工作台功能规格.md`(已安装智能体)、`产品-02E-知识库工作台功能规格.md`(已安装知识库、作品绑定)、`产品-02F-市场功能规格.md`(安装与跳转) diff --git a/design-docs/临时-03-market-install-KB物化执行plan.html b/design-docs/临时-03-market-install-KB物化执行plan.html deleted file mode 100644 index 1de8c830..00000000 --- a/design-docs/临时-03-market-install-KB物化执行plan.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - -临时-03 · Market Install KB 下游物化执行 Plan - - - -

临时-03 · Market Install KB 下游物化执行 Plan

-
执行版 v1 · 2026-06-26 · 后端/架构/PR reviewer · 承临时-02 拍板(B' installed_ref + KB 先行)· 待人类拍 D1 后执行
- -
-拍板已定——做 B'(installed_ref 最小留痕)+ KB 先行。本 plan 把它拆成 U0…U4 五个可独立验证单元,物化落点在知识库绑定路径(非 install)。但一手代码核验坐实一个比评审版更硬的事实:按当前代码,多租户"共享发布者同一 RAGFlow dataset"必然越权。所以 U0 不是"验证隔得住吗",而是"共享落地前先把 chunk 级隔离补上"的真前置硬门。 -
- -

执行前四条硬事实(一手代码核验,非推断)

-
-
1kb_id 当前被污染
绑定存的 kb_id 是 market assetIdsourceId: assetId),不是真实 muse_knowledge_base.id。物化要把 kb_id 去污染改写成新建 installed_ref 主键,并改对所有读回端口。
-
2跨 BC 反查链断
assetId→发布者 kbId→dataset 数据通、跨 BC 断(market.api 只有 HandoffToken、无按 assetId 查源接口)。需新增 MarketAssetSourceApi 本地读端口。
-
3共享必越权 安全红线
RAGFlow 检索 body 不含 tenant、document_ids=null 时 dataset 全量返回parseChunks 对返回 chunk 无租户二次过滤。两租户 binding 指同一 dataset → A 读到 B 全部 chunk。U0 转为"共享前先补 chunk 级隔离"硬门。
-
4回滚表不存在
muse_source_propagation_target 全仓零命中。U4 回滚必须基于真实机制(muse_knowledge_source_event + projection),不得引用它。
-
- -

U0 决策门:dataset 隔离硬前置(D1)

-
-
⚠ U0 spike:共享发布者同一 RAGFlow dataset 能否做到租户隔离?
(已知当前必越权 → 真问题是"补隔离手段能否落地")
-
-
分支 S · 补隔离后共享
metadata_filter / document_ids 把 chunk 限到本安装授权子集,验证生效 → 共享发布者 dataset(存储不放大)。需先投入隔离工程
-
分支 C · 每安装者复制
独立复制 dataset,隔离天然干净。但文档复制+重解析是异步重活,另起子任务。U0 红时兜底。
-
分支 H · 暂缓
只做诚实标注"市场资产暂未开放直接用",消除静默 no_dataset 骗用户。成本最低。
-
-
- -

实现单元 U0 → U4

-
-
U0
spike:chunk 级隔离硬前置门
单测层证伪租户过滤 + 活体层证 RAGFlow 整库返回 + 验证补隔离手段(metadata_filter/document_ids)→ 输出 S/C/H 三选一给人类拍。不产业务代码。
-
▾▾▾
-
U1
数据模型 + 迁移判定
installed_ref kb 行字段映射(kb_type/source_market_asset_id/license_snapshot_id,V5 已备列复用)+ dataset 关联策略。初版判定:不需新 Flyway 迁移
-
▾▾▾
-
U2
物化落点:绑定路径建实体
在 knowledge 绑定路径(非 install)建本地 kb 行 + dataset 关联;kb_id 去污染 + 新增 MarketAssetSourceApi 跨 BC 读 + 幂等 + kbId 存在性校验。核心工程量在此
-
▾▾▾
-
U3
检索打通:消除 no_dataset
U2 数据正确后第四门自然返非空→命中(共享分支 RetrievalApiImpl 多半无需改)。授权快照沿用 ADR-020 VARCHAR,不回退 parseLong。
-
▾▾▾
-
U4
验证:真 PG IT + e2e + 回滚 + 隔离回归
真 PG+真 RAGFlow 端到端命中 + studio e2e + 幂等 + 跨租户隔离回归(承 U0)+ 召回/下架回滚(基于真实 source event 机制)。
-
- -

决策点(待人类拍板)

- - - - - - - -
#决策选项倾向
D1dataset 共享 vs 复制(最硬·U0 门)S 补隔离后共享 / C 每安装者复制 / H 暂缓先看 U0 证据再拍
D2物化时机绑定同步 / 惰性首用绑定同步
D3license_snapshot_id 承载数值主键 / 字符串 envelope(需 V32)数值主键
D4跨 BC 读发布者 dataset 路径API 带回 / knowledge 内部受控查U2 实现时定夺
D5召回/下架回滚自动化复用现有传播 / 本期标开放项先验现状是否触达
- -

Scope 边界(明确不做)

-
-
KB 限定——只做知识库物化,agent(muse_agent market_installed + runtime 放行)是独立下一阶段,本 plan 不碰 ai 模块。
-
install 解耦不动——installMarketplaceAsset 维持"只记账",物化落点严格在 knowledge 绑定路径。
-
handoff 非物化桥——token 只负责跳转+被消费,不负责物化。
-
复制分支(C)细节不展开——仅作 U0 红兜底标注,异步复制实现另起子任务。
-
market 两列 BIGINT 议题不纳入——独立契约议题,本 plan 不处理。
-
- -
配套工程/AI 读本:临时-03-market-install-KB物化执行plan.md(含 mermaid 依赖图与实现单元全文)· 方案 WHAT 见临时-02 · 概念边界归 架构-02 / 专题-03 / ADR-020
- - diff --git a/design-docs/临时-03-market-install-KB物化执行plan.md b/design-docs/临时-03-market-install-KB物化执行plan.md deleted file mode 100644 index 52544799..00000000 --- a/design-docs/临时-03-market-install-KB物化执行plan.md +++ /dev/null @@ -1,299 +0,0 @@ -# 临时-03 · Market Install KB 下游物化执行 Plan(HOW,供 review 后执行) - -> ⚠️ **已被 [`临时-04-market-KB物化-D0fork执行plan.md`](临时-04-market-KB物化-D0fork执行plan.md) supersede(2026-06-26)**:D1 澄清坐实真越权是「安装者读到**发布者私有**内容」(非本稿假设的安装者间互读),用户拍板改走 **D0-fork(发布侧 fork 纯公开副本、物理隔离)**,去掉本稿 S 分支的运行时 chunk 隔离、`metadata_filter`→`metadata_condition` 字段修复降级为潜伏 bug。**本稿保留作决策演进史**,新执行依据以临时-04 为准;本稿的 U0 spike 证据与四条硬事实仍有效(被临时-04 继承)。 - -- 版本:v1(执行版,待人类 review/拍板后执行;本稿只读出文档,不改任何代码) -- 更新日期:2026-06-26 -- 目标读者:后端 / 架构 / PR reviewer -- 阅读时间:25–35 分钟 -- 文档性质:**临时件**(执行依据,落定后归并入正式分册或删除,不作长期 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-020;BC 边界规则归 [`.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/0(2 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 硬前置门
chunk 级隔离能否补上?
(已知:当前共享必越权)"} - U0 -->|"绿: 隔离补到位
(metadata_filter/document_ids 真隔)"| SHARE["分支S: 共享发布者 dataset"] - U0 -->|"红/暂不投入隔离"| COPY["分支C: 每安装者独立复制 dataset
(局部 C, 隔离天然干净)"] - U0 -->|"产品优先级不在此"| HOLD["分支H: 暂缓
(只做诚实标注未开放)"] - - SHARE --> U1 - COPY --> U1 - U1["U1 数据模型
installed_ref kb 行落点 + dataset 关联策略
判定是否需 V32 迁移(人类门)"] - U1 --> U2 - U2["U2 物化落点
绑定路径建本地 kb 行 + dataset 关联
kb_id 去污染 + 新增 MarketAssetSourceApi 跨 BC 读
幂等 + kbId 跨 BC 存在性校验"] - U2 --> U3 - U3["U3 检索打通
selectActiveDatasetByKbId 返非空→命中
消除 no_dataset, 授权快照沿用 ADR-020 VARCHAR"] - U3 --> U4 - U4["U4 验证
真 PG IT(物化→检索命中) + studio e2e
+ 召回/下架回滚 + 跨租户隔离回归"] - - 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 绑定路径
createKnowledgeBindingPrecheck/createKnowledgeBinding"] - A --> B["消费 handoff token(原有)
consumeMarketHandoff"] - B --> C["✅U2: 调 MarketAssetSourceApi.getAssetSource(assetId)
拿发布者 kbId"] - C --> D["✅U2: 建本地 muse_knowledge_base 行
kb_type=installed_ref
source_market_asset_id=assetId
license_snapshot_id=授权快照"] - D --> E["✅U2: 建 muse_knowledge_ragflow_binding
kb_id=新建本地 kbId
ragflow_dataset_id=发布者 dataset(共享)"] - D --> F["✅U2: binding.kb_id = 新建本地 kbId
(去污染:不再是 assetId)"] - F --> G["检索: selectActiveDatasetByKbId(本地kbId)
✅U3 返非空"] - E --> G - G --> H["✅U0 共享分支: metadata_filter/document_ids
把 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 专用测试类)——mock `RagFlowKnowledgeRuntimeClient` 返回"混入他租户 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 隔离怎么验)**: -1. **造数据**:seed 两个真实租户 T_A、T_B(注意当前是单租户 + `tenant_id DEFAULT 0`,需先建第二租户并确认 yudao 租户拦截器在两租户上下文都生效);让"发布者"建一个 KB → 真上传一份文档 → RAGFlow 真生成出 `ragflow_dataset_id = D`。 -2. **共享指向**:在 T_A、T_B 各建一条 installed_ref binding(spike 阶段可直接 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(暂缓)**。后续 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/retrieval` document_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__.sql` - -**Approach**: -1. **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:85` `setAuthorizationSnapshotId(authorization.getId())`),故此处填**授权快照行数值主键**与 BIGINT 相容;**但若决定改存 ADR-020 字符串 envelope(`rpe-local-`)则需 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 已有对应字段。 - - **仅以下情形需 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 越界) -- 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`(本地 Bean,knowledge 注入它而非读 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=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 假红(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()`(已是 VARCHAR,V14 `muse_knowledge_binding.authorization_snapshot_id` 已 widen),installed_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=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-` 形态|预期:检索门正常放行、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 D,D 含两租户差异内容)→ 各自检索|预期: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`),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-`;不 rm `dump.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`](临时-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) diff --git a/design-docs/临时-04-market-KB物化-D0fork执行plan.html b/design-docs/临时-04-market-KB物化-D0fork执行plan.html deleted file mode 100644 index 6cc84f6d..00000000 --- a/design-docs/临时-04-market-KB物化-D0fork执行plan.html +++ /dev/null @@ -1,291 +0,0 @@ - - - - - -临时-04 · Market KB 物化 D0-fork 执行 Plan(人读图) - - - -
-

临时-04 · Market KB 物化 D0-fork 执行 Plan

-
发布侧 fork 纯公开副本 · v1(执行版,待人类 review 后执行,本稿只读出文档)· 2026-06-26
-
配套正文:临时-04-market-KB物化-D0fork执行plan.md | 上一版 临时-03(共享+运行时隔离)已被本稿 supersede
- -
- 一句话:知识库上架时,在发布侧 fork 一份只含公开文档的专用 dataset("公开副本"),所有安装者只读共享这一份。 - 从物理上把发布者私有内容隔在副本之外,从根消除"安装者读到发布者私有内容"的越权—— - 不需要运行时 chunk 隔离、不依赖那个 metadata_filter 字段 bug。代价是新增一块发布侧 fork 工程(建副本+复制文档+重索引,异步重活)+ 打破 market→knowledge 零依赖接线。 -
- - -

越权解法:从"私有泄露"到"物理不可见"

-
-
-
-

✗ 现状 / 共享活 dataset(会越权)

-
    -
  • market 对 knowledge/ragflow 零依赖:上架审核只翻 market 自有资产状态,从不 fork 任何 dataset
  • -
  • 安装者绑定后指向发布者那一份活 dataset
  • -
  • 发布者上架后仍可继续加私有文档(whole-KB 上架、KB 不冻结)
  • -
  • 检索链对返回 chunk 无任何二次过滤:dataset 进检索即整库返回 → 安装者读到上架时不存在、发布者事后才加的私有内容
  • -
  • 租户拦截器拦不住:两边 binding 指向同一个 RAGFlow dataset_id
  • -
-
-
-

✓ D0-fork(物理隔离)

-
    -
  • 上架时 fork 出只含上架时刻公开快照的副本 dataset
  • -
  • 安装者只读副本,发布者私有/上架后新增文档物理不在副本内
  • -
  • "整库返回"在这里是安全的——整库 = 纯公开
  • -
  • 无需运行时 chunk 隔离、无需 metadata 过滤
  • -
  • 副本仅 1 份,N 个安装者共享只读
  • -
-
-
-
- 与临时-03 的差异:临时-03 把越权理解为"安装者之间互读(多租户共享 dataset)",方向是"共享前补 chunk 级隔离"; - D1 澄清坐实真越权是安装者读到发布者私有内容,故改走物理隔离(fork 公开副本)。 - 临时-03 的 U0 spike 证据("整库无 chunk 过滤")仍有效,本稿转作 U-verify 的反向基线(证"私有不泄露")。 -
-
- - -

物理隔离:发布侧 vs 安装侧

-
-
-
-

发布者域(owner = 发布者)

-
-
发布者活 dataset kb-{发布者kbId}-v{ver}
-
含公开文档 + 上架后新增的私有文档(持续变化)
-
-
-
⭐ 公开副本 dataset fork-asset-{assetId}-public
-
只含上架时刻的公开快照文档(fork 后不随活 dataset 变)
-
-
↑ U-fork:上架时逐文档复制内容 + 重索引(异步重活)
-
- -
-
-
N 安装者
共享只读
副本仅 1 份
-
- -
-

安装者域(owner = 各安装者)

-
-
installed_ref kb 行(每安装者一行)
-
kb_type=installed_ref · source_market_asset_id=assetId · license_snapshot_id=授权快照
-
-
-
ragflow_binding(去污染后)
-
kb_id=本地 installed_ref 主键(不再是 assetId)· ragflow_dataset_id=公开副本
-
-
-
检索 selectActiveDatasetByKbId → 公开副本
-
RAGFlow 整库返回 = 🟢 纯公开,发布者私有物理不可见
-
-
-
-
- 越权根除点:安装者 binding 指向的 dataset_id 是公开副本而非发布者活 dataset。 - 即使检索链"整库返回 + 无二次过滤"的事实不变,整库里也只有上架时刻的公开内容——发布者私有从源头不在这份 dataset 里。 -
-
- - -

四个实现单元(各一句话)

-
-
-
-

U-fork 全新·最重

-
上架事件触发 knowledge 异步 fork:建副本 dataset + 逐文档复制 + 重索引 + 幂等/重试/部分成功;whole-KB 公开集 = 上架时刻全部可检索文档。
-
-
-
-

U-materialize

-
绑定路径建 installed_ref kb 行 + binding 指向公开副本;kb_id 去污染(assetId→本地主键)+ 新 MarketAssetSourceApi 跨 BC 读带回副本 dataset + forkStatus + 幂等。
-
-
-
-

U-retrieve

-
物化后第四门返非空→命中;无运行时隔离(副本纯公开);授权快照沿用 ADR-020 VARCHAR。代码几乎不改。
-
-
-
-

U-verify

-
真 PG IT(fork→安装→检索命中)+ studio e2e + 发布者私有不泄露(复用 U0 反向基线)+ 召回/下架回滚。
-
-
-
- 相对临时-03:U-materialize/U-retrieve = 原 U2/U3 简化去隔离;U-fork 是全新核心工程;U0 spike 的 2 个 case 复用 作 U-verify 反向基线,不重写。 -
-
- - -

fork 时序(上架触发 + 事件驱动 + 异步,含失败/重试/幂等)

-
-
1
管理员审核
approvePublishRequest(含 markListed)
-
2
market 审核服务
资产/版本 listed(market 自有事实,同事务)
-
3
market 审核服务
上架事件 listed{assetId, 发布者kbId, version}(同事务) · 不等 fork,审核到此提交,不被 fork 成败绑架
-
4
knowledge 消费者
异步消费上架事件 → forkPublicSnapshot(assetId, 发布者kbId, version)
-
5
knowledge fork
幂等检查(该 assetId+version 已 fork?命中跳过)→ createDataset(副本)
-
6
knowledge fork
对每个上架时刻公开文档:uploadDocuments(副本) + startParseDocuments(副本重索引)
-
7
knowledge fork
全成功 → forkStatus=ready + 记副本 datasetId(供 MarketAssetSourceApi 带回)
部分失败/502 → forkStatus=partial/failed + 标可重试(不删已建部分、不重复传已成功文档)
-
无现成 RAGFlow "copy dataset" API → fork 必须重走 createDataset + 逐文档 upload + startParse(重索引)。这是异步长任务重活,是 D0-fork 相对临时-03 最大的新增成本。
-
- - -

关键决策点(待人类拍板)

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
决策选项倾向 + 理由
fork 时机上架 markListed 时 fork / 首次安装懒 fork上架时 fork:版本快照语义干净(副本=上架那刻公开集)+ 安装侧轻(无异步等待)+ 浪费可控;懒 fork 破坏快照语义 + 首装延迟 + 并发抢锁。
接线
(打破零依赖)
事件驱动 / market 调 knowledge facade 同步事件驱动:审核不被 fork 成败绑架 + 契合 ADR-017 + 保 BC 边界(market 不反向依赖 knowledge);facade 同步会让审核挂在 RAGFlow 多次调用上、失败回滚审核。
副本更新重新上架才更新 / 不更新 / 实时跟随重新上架才更新:与"市场资产=发布快照"模型一致;实时跟随会重新引入私有泄露。
fork 未就绪
时安装
绑定阶段拒(fail-closed)/ 绑定但检索 no_dataset绑定阶段拒:避免"绑了用不了"迷惑态;实现时定夺回填。
召回/下架
回滚
复用现有传播 / 本期标开放项手动停用现状 market→目标 owner 传播是 fail-closed blocked 记录式(TARGET_OWNER_UNAVAILABLE),knowledge 无消费入口 → 倾向本期标开放项,不假装闭环。
DDL /
副本存放
复用 JSONB 不加列 / 新增列或表(V32)复用现有结构 + JSONB 承载 fork 状态/副本映射 → 不需要新迁移;仅评审坚持强类型列时才走 V32(_test 库铁律)。
-
- - -

风险与边界(诚实呈现)

-
-
-

主要风险

-
    -
  • R1 私有泄露:fork 公开集界定错 / 副本"实时跟随"→私有重新泄露。缓解:严格按上架时刻 active 文档 + U-verify 真验私有不泄露。
  • -
  • R2 fork 异步重活复杂度(不掩盖):建 dataset+N 文档逐个 upload+重索引,失败/重试/幂等/部分成功——比临时-03"共享+运行时隔离"重。诚实:安全模型更干净(物理隔离),但发布侧工程更重。
  • -
  • R3 打破 market→knowledge 零依赖:影响面集中在"上架落事件"+knowledge fork 消费链,不改 market 审核主体;需架构 review(ADR-017 为据)。
  • -
  • R6 metadata_filter 字段 bug 潜伏·留待修:发的 metadata_filter 被 RAGFlow 静默忽略(应为 metadata_condition)。D0-fork 不依赖它,降级潜伏 bug 单记。
  • -
-
-
-

本期不做(Scope)

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

临时-05 · Market Agent 物化 执行 Plan

+
拷配置 · installed 型 agent · 改授权门 · v1(执行版,待人类 review 后执行,本稿只读出代码)· 2026-06-26
+
配套正文:临时-05-market-agent物化执行plan.md | 姊妹方向 临时-04(KB D0-fork,已闭环)
+ +
+ 一句话:市场安装一个智能体后,槽位能绑进去(闸 A 已放宽),但 AI 运行时授权门一律拒 market 来源 agent——这是 agent 物化的唯一真断点。 + 拍板方向:①不 fork、拷配置(agent 无 KB 那类私有泄露向量:版本不可变 / 运行时不回读私有 prompt / config 即被授权出售的商品本体); + ②物化成 installed 型独立 agent(config 拷自发布者)+ 改运行时授权门放行。物化全在安装侧同步完成,无任何 fork / 发布侧异步 / 外部资源。 +
+ + +

为什么 agent 不 fork、只拷配置

+
+
+
+

KB 为何必须 fork(临时-04)

+
    +
  • 市场对 knowledge 零依赖 → 上架不 fork 任何 dataset
  • +
  • 发布者活 dataset 上架后还能继续加私有文档(KB 不冻结)
  • +
  • 安装者共享活 dataset + 检索整库返回 → 读到上架时不存在的私有 chunk
  • +
  • → 逼出"发布侧 fork 一份只含公开快照的独立 dataset"(重活)
  • +
+
+
+

✓ Agent 无泄露向量 → 拷下即交付

+
    +
  • version 一经 active 即不可变(改动只能开新版本行)
  • +
  • 运行时不回读发布者私有 prompt,只用版本冻结 config
  • +
  • config 本就是发布者亲写、上架出售的商品本体(拿到=交付,非泄露)
  • +
  • → 安装侧同步拷一份 config 即可,无 fork / 无外部资源 / 无异步
  • +
+
+
+
+
+

发布者域(owner = 发布者)

+
+
发布者 muse_agent + active muse_agent_version
+
config(promptTemplate / slotBindings / toolGrantIds / modelKey)· 🔒 active 即不可变
+
+
↓ A-materialize:安装侧同步拷 config(无 fork·无外部资源·无异步)
+
+
+
+
拷配置
各拷各的
无共享副本
+
+
+

安装者域(owner = 安装者)

+
+
installed 型 muse_agent(本人所有)
+
source_market_asset_id=assetId(V27 列已在、补 DO 映射)
+
+
+
⭐ active muse_agent_version
+
config = 整体拷自发布者快照(保完整性,不漏 modelKey/toolGrantIds)
+
+
+
运行时 requireVisibleAgent → 取本地 config
+
🟢 命中物化 agent,AI 任务可跑
+
+
+
+
+ + +

唯一断点:运行时授权门改造(trust boundary · 本 plan 最需严防)

+
+
+ MuseAiTaskServiceImpl.requireVisibleAgent:329-331 · 三入口 :220(agentTest)/:308(override)/:315(slotKey→binding) 全经它。改门一处 = 三入口同时通。 +
+
+
+

✗ 改造前(断点②)

+
agentType = system? → 放行
+
agentType = user 且 owner == 本人? → 放行
+
🔴 其余一律拒 AI_AGENT_SCOPE_FORBIDDEN
market / installed 出身 agent 必拒 → 用户:AI 任务直接失败
+
+
+

✓ 改造后(A-runtime 放行 installed)

+
agentType = system? → 放行(不回归)
+
agentType = user 且 owner == 本人? → 放行(不回归)
+
⭐ agentType = installed 且 owner == 本人 且 授权快照有效? → 🟢 放行(新增支,三收紧点缺一不可)
+
🔴 他人 installed / 无授权 / market 裸引用 仍拒 ← 负路测命门
+
+
+
+ 放行三收紧点缺一不可:①必须 installed 型(不放开 market 裸引用)②必须本人所有(防越权用他人 installed)③授权快照有效(防授权撤销后仍可跑)。 + A-verify 负路(他人 installed 必拒 / 授权失效必拒 / market 裸引用必拒)是本 plan 安全验收的命门——缺负路不算通过。 +
+
+ + +

五个实现单元(各一句话)

+
+
+
+

A-source 扩端口

+
临时-04 已落的 MarketAssetSourceApi 支持 assetType=agent + 带回发布者 active 版本 config(决策点 D1:market 反调 ai vs ai 自读)。
+
+
+
+

A-materialize 照搬 KB

+
绑定路径建本地 installed 型 muse_agent + active version(config 拷自发布者)+ 补 DO 映射 source_market_asset_id + 幂等 + 槽位指本地 agentId(去裸引用)。
+
+
+
+

A-runtime trust boundary

+
requireVisibleAgent 放行 installed 型 + 本人 + 授权有效;三入口同时通;他人/无授权必拒
+
+
+
+

A-naming

+
agent_type 取值统一(market_installed / installed / installed_ref 四处漂移收口)+ 同步 后端-04。
+
+
+
+

A-verify

+
真 PG IT(install→物化→bind→runtime 命中物化 agent)+ studio e2e + 授权门负路
+
+
+
+ 复用 KB 已落资产:复用 MarketAssetSourceApi(扩,非新建)+ handoff token + materializeInstalledRefKb 物化形态。 + 不需要:MarketKbListedEvent 孪生事件 / MarketAssetForkApi 写回 / 发布侧 fork 管线(agent 无 fork、物化全在安装侧同步)。 +
+
+ + +

物化时序(安装侧同步 · 无异步 · 无外部调用)

+
+
1
用户
市场安装 agent 资产(install 只记账 targetFactsWritten=false,不动)→ 跳转 ai 槽位绑定
+
2
ai 绑定路径
消费 handoff token(闸 A 已放宽 market 型,不动)
+
3
A-source
MarketAssetSourceApi 读资产来源 + 带回发布者 active 版本 config(assetType=agent 放行) · 一次跨 BC 读,无外部服务
+
4
A-materialize
同一 @Transactional 内:幂等查 → 建 installed 型 muse_agent(agent_key 安装者维度唯一) · 同步
+
5
A-materialize
建 active muse_agent_version(config 整体拷贝)→ 回写 current_version_id → 槽位 binding.agentId = 本地 installed agentId
+
6
A-runtime
发起 AI 任务 → requireVisibleAgent 放行本地 installed agent → 取本地 config → 🟢 真跑出候选
+
对照 KB:KB 这里要等上架时异步 fork 副本 dataset 就绪(重活 + forkStatus fail-closed);agent 全程同步、无外部调用、无中间态——这是 agent 比 KB 轻得多的根本原因。
+
+ + +

关键决策点(待人类拍板)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
决策选项倾向 + 理由
D1 config 读取途径
(核心)
扩 MarketAssetSourceApi 让 market 反调 ai 读端口带 config / ai 绑定路径自己读发布者 agentmarket 反调 ai(A):安装侧调用对称 + 避免跨 owner 读难题;代价(market→ai 新方向)经 -api 端口+ArchUnit 可控。否决则退 B 显式处理跨 owner 读。
D2 拷 vs 引用
prompt 库
config 整体拷贝(含 promptTemplate 快照)/ 引用发布者 prompt 库整体拷贝:agent 无泄露向量 + config 即商品本体 + 版本不可变→拷下即冻结快照;引用会重新引入发布者改 prompt 影响安装者的漂移。
D3 agent_type
命名最终值
market_installed / installed / installed_refmarket_installed:与 后端-04 既有文档零漂移、语义自明;与 knowledge 风格协调即可不强求同名。
D4 授权快照承载数值主键 / 字符串 envelope(VARCHAR)字符串 envelope:沿用 ADR-020,槽位 binding 已 V31 VARCHAR,物化实体一致。
D5 installed agent
配额/列表可见
计入用户 agent 配额并列表可见 / 不计配额、列表单独分区或隐藏倾向列表可见标 installed 来源、配额随产品定;非核心链路可后置,但 A-materialize 须明确列表查询是否纳入。
D6 DTO 形状现有 DTO 增 agent 字段 / 拆 agent 专用 DTO+方法拆 agent 专用:接口隔离,KB/agent 各取所需,与临时-04 读写分离取向一致。
D7 召回/下架回滚复用现有传播 / 本期标开放项与 KB 同未触达面(临时-04 事实 7)→ 倾向本期标开放项,不假装闭环;A-runtime 授权失效兜底部分覆盖。
+
+ + +

风险与边界(诚实呈现)

+
+
+

主要风险

+
    +
  • R1 改授权门 = trust boundary 必负路测:最大风险是放过头(把"本人 installed"误放成任意/他人 installed/market 裸引用)。缓解:放行三收紧点 + A-verify 负路(他人/无授权/裸引用必拒)作验收命门。
  • +
  • R2 config 跨 BC 依赖方向(D1):选 A 引入 market→ai 新方向,需 ArchUnit 确认不成环。否决则退 B 显式处理跨 owner 读。
  • +
  • R3 config 拷贝完整性:漏拷 modelKey/promptTemplate/toolGrantIds → 物化 agent 行为与发布者不一致(买到残品)。缓解:整体拷贝 + A-verify 真验。
  • +
  • R5 agent_key 唯一键冲突:uk_muse_agent_key=(tenant_id, agent_key),直拷发布者 agent_key 会撞键 500。缓解:按安装者维度生成 agent_key,幂等查走 source_market_asset_id。
  • +
  • R4 命名漂移收口:写入值与放行值不一致 → 物化了运行时仍拒(自造断点)。缓解:A-naming 单一常量 + 同步 后端-04。
  • +
+
+
+

本期不做(Scope)

+
    +
  • KB 物化(临时-04 已闭环,不重做)
  • +
  • fork / 发布侧异步管线 / 上架事件 / 外部资源(agent 无泄露向量、全免)
  • +
  • install 侧物化(维持只记账 targetFactsWritten=false)
  • +
  • 闸 A 槽位授权门(已放宽 market,不碰)
  • +
  • 召回/下架回滚自动化(开放项,与 KB 同面,不假装闭环)
  • +
  • installed agent 列表/配额读模型(D5 横切待定)
  • +
  • market 两列 BIGINT 契约议题
  • +
+
DDL 判定:不引入新 Flyway 迁移——source_market_asset_id 列 V27 已在、仅补 MuseAgentDO 映射;授权快照沿用 V31 VARCHAR。
+
+
+ + +

与 KB D0-fork 的异同(速览)

+
+ + + + + + + + + + + + + +
维度KB(临时-04,已闭环)Agent(本 plan)
泄露向量(活 dataset 上架后能加私有文档→整库检索泄露)(version 不可变 + 不回读私有 prompt + config 即商品本体)
物化手段fork 公开副本(建 dataset+复制+重索引,异步重活)拷配置(安装侧同步拷 config,无外部资源)
发布侧工程有(上架事件+消费者+兜底 worker+ForkApi 写回)(物化全在安装侧、上架侧零改动)
唯一断点检索第四门(数据正确即通,代码几乎不改)运行时授权门(必改门放行 installed,trust boundary)
改授权门不改(A-runtime,负路测命门)
去污染/去裸引用kb_id 去污染(assetId→本地 kbId)槽位 agentId 去裸引用(发布者→本地 installed agentId)
共享 vs 各拷副本仅 1 份,N 安装者共享只读config 各拷各的,无共享(轻量 JSONB)
外部资源回滚有(删 fork 出的副本 dataset)(软删本地 agent 行即可)
共同开放项召回回滚未触达 installed 实体(同面);e2e fixture 待对齐
+
+ +
+ owner 边界引 临时-02(§2.2 B' 智能体侧·§3.2 schema 增量)/ 临时-04(KB 姊妹方向)/ 架构-02(§5 智能体运行权限·§6 市场资产·§11.2 绑定授权·授权快照)/ ADR-017(Source 传播事件驱动)/ ADR-020(授权快照字符串承载)/ .agents/rules/bc-boundaries.md,本稿不重定义概念。 +
+
+ + diff --git a/design-docs/临时-05-market-agent物化执行plan.md b/design-docs/临时-05-market-agent物化执行plan.md new file mode 100644 index 00000000..fa628fe1 --- /dev/null +++ b/design-docs/临时-05-market-agent物化执行plan.md @@ -0,0 +1,399 @@ +# 临时-05 · Market Agent 类资产物化(拷配置 · installed 型 agent · 改授权门)执行 Plan + +- 版本:v1(执行版,待人类 review/拍板后执行;本稿只读出代码、不改任何代码、不 commit) +- 更新日期:2026-06-26 +- 目标读者:后端 / 架构 / PR reviewer +- 阅读时间:30–40 分钟 +- 文档性质:**临时件**(执行依据,落定后归并入正式分册或删除,不作长期 SSOT)。本稿只给"怎么做",不重定义概念:方案演进与 C/B'/暂缓权衡见 [`临时-02-market-install下游物化方案.md`](临时-02-market-install下游物化方案.md)(§2.2 B' 智能体侧、§3.2 智能体 schema 增量为本稿 WHAT 上游);KB 类资产物化的姊妹方向(已闭环、本稿照搬其物化范式与开放项结构)见 [`临时-04-market-KB物化-D0fork执行plan.md`](临时-04-market-KB物化-D0fork执行plan.md);术语与 owner 边界以 [`架构-02-核心数据结构与双轨模型.md`](架构-02-核心数据结构与双轨模型.md) 为准(§5 智能体运行权限、§6 市场资产、§11.2 绑定安装授权、授权快照"引用对象必须保存快照 id");表结构归 `后端-04` §7 AI/Agent;授权快照字符串承载归 ADR-020;Source 传播事件驱动归 ADR-017;BC 边界规则归 [`.agents/rules/bc-boundaries.md`](../.agents/rules/bc-boundaries.md)。 +- 配套人读图:[`临时-05-market-agent物化执行plan.html`](临时-05-market-agent物化执行plan.html) + +> **一句话**:市场安装一个智能体后,槽位能绑进去(闸 A 已放宽),但 AI 运行时 `requireVisibleAgent` 仍把 market 来源的 agent 一律拒掉——这是 agent 物化的**唯一真断点**。本 plan 拍板已定的方向是 **①不 fork、拷配置**(agent 不像 KB 有"上架后还能往库里加私有文档"的泄露向量:版本一经 active 即不可变、运行时不回读发布者私有 prompt、`config` 本身就是被授权出售的商品本体),**②把市场 agent 物化成安装者租户内一个独立的 installed 型 `muse_agent` + active `muse_agent_version`(config 拷自发布者)+ 改运行时授权门放行**。物化落点严格在**绑定路径**(与 KB 同范式)、不在 install 侧。本 plan 把它拆成 **A-source / A-materialize / A-runtime / A-naming / A-verify 五个可独立验证的实现单元**。 +> +> **与 KB D0-fork 的根本差异(一句话)**:KB 的难点是"发布者私有内容不能泄露",逼出了发布侧 fork 一份只含公开快照的独立 dataset(重活);agent **没有这层泄露向量**,所以**不 fork、不碰任何外部资源、无发布侧异步工程**——物化退化为"安装侧同步把发布者的 agent 配置拷成一份本地独立 agent",唯一新增的硬约束是**改运行时授权门**(trust boundary,必须做负路测)。 + +--- + +## 0. 背景与拍板(读过临时-02/临时-04 的人只需看这一节就能对齐) + +临时-02 把市场安装的两个物化断点摆开诊断:知识库检索的"数据集门"永远命不中(断点①),智能体在运行期被授权门直接拒(断点②)。临时-04 已经把断点①用 D0-fork 完整闭环(上架 fork 公开副本 + 安装侧物化 installed_ref KB + 检索打通 + 真验私有不泄露,commit c12eb01→8b3258e 已真验收)。本 plan 处理**剩下的另一半**——断点②,即智能体类资产的物化。 + +**拍板已定(三条,本 plan 不再重开权衡)**: + +1. **不 fork、拷配置**。KB 之所以要 fork,是因为 KB 上架后生命周期不冻结、发布者还能继续往自己库里加私有文档,安装者若共享发布者活 dataset 就会读到上架时根本不存在的私有内容。**agent 没有这条链**:一个 agent 版本一旦 active 就不可变(改动只能开新版本行,见 §1 事实 4),运行时也不回读发布者的私有 prompt 正文、只用版本里冻结的 `config`,而 `config` 本身就是发布者**亲自写好、上架出售的商品本体**(name/description/promptTemplate/slotBindings/toolGrantIds)。安装者拿到这份 config = 拿到他买的东西,**这不是泄露、正是交付**。因此 agent 物化不需要 fork 任何东西,直接把发布者那一份 active 版本的 `config` **拷**进安装者租户内一份新建的 agent 版本即可。 + +2. **物化成 installed 型独立 agent + 改授权门**。对齐临时-04 的 installed_ref 范式:在安装者租户里建一行独立的 `muse_agent`(一个区别于 `system`/`user` 的 installed 型,`source_market_asset_id` 回填溯源)+ 一行 active `muse_agent_version`(config 拷自发布者)。光建实体还不够——运行时授权门当前对非 system/非本人 user 一律拒(§1 事实 1),必须把它**放行"本人所有的 installed 型 + 授权有效"**。这道改动是 trust boundary,是本 plan 最需要严防的一点(§4 R1 + A-verify 负路测)。 + +3. **物化落点在绑定路径、不在 install 侧**。与临时-02 §1.2 / 临时-04 完全一致:install 维持"只记账、`targetFactsWritten=false`"(§1 事实印证),物化是目标域(ai)绑定路径在消费 handoff token、写槽位绑定的**同一事务**里多做的一步。 + +--- + +## 1. 执行前必读:把工作量钉死的硬事实(基于真实代码,行号为只读核验所得) + +**事实 1 — 运行时授权门 `requireVisibleAgent` 对 market 来源 agent 一律拒,这是 agent 物化的唯一真断点。** +`MuseAiTaskServiceImpl.requireVisibleAgent:321-342` 的可见性判定在 `:329-331`:`agentType` 不是 `system`、且不是(`user` 且 `ownerUserId==agent.ownerUserId`)就抛 `AI_AGENT_SCOPE_FORBIDDEN`。market 出身的 agent 既非 system、也非"本人所有的 user",**必拒**。三个入口全部经它:`:220`(agentTest 试运行)、`:308`(agentOverrideRef 临时覆盖)、`:315`(agentSlotKey→查 slot binding→拿 agentId)。门内还有两道:`:326-327` 要 `agent.status=active`,`:333-336` 要存在 active 的 `muse_agent_version`(按 versionNo 或最新)。**结论**:物化只要在安装者租户造出"本人所有的 installed 型 active agent + active version",再把 `:329-331` 放行 installed 型,三入口同时通——这是本 plan 的命门,A-materialize 造实体、A-runtime 放门。 + +**事实 2 — 槽位闸 A `requireVisibleActiveSourceAgent` 已对 market 放宽,槽位已能绑 market agent 进去。** +`MuseAgentSlotServiceImpl.requireVisibleActiveSourceAgent:292-314` 的 `:304-308`:当 `marketHandoff=true`(token 已被 Market verify 通过)时**放宽 agentType 可见性、接纳 market 型 agent**;非 handoff 路径维持原校验。其 precheck(`:93`)/bindAgentSlot(`:158`)/consumeMarketHandoff(`:350`)链路已闭环,绑定持久化是稀疏 override 的 upsert(`:250`,02D 首次创建死锁已修,commit 9115d4f + V29 唯一约束)。**结论**:闸 A 不是断点、**本 plan 不动它**;断点纯在运行时闸 B(事实 1)。槽位里存的目前是一个指向 market 出身 agentId 的**裸引用**,物化后要让槽位指向**安装者本地新建的 installed agentId**(A-materialize 的去裸引用,类比 KB 的 kb_id 去污染)。 + +**事实 3 — `MuseAgentDO` 未映射 `source_market_asset_id`,但该列 V27 已就位、休眠。** +`muse_agent` 表的列由 `V4__init_ai_schema.sql:71-91` 定义(`agent_key`/`name`/`description`/`agent_type` DEFAULT 'system'/`owner_user_id`/`prompt_key`/`status` DEFAULT 'active'/`current_version_id`/`slot_bindings` JSONB/`category`/`tags` JSONB),唯一键 `uk_muse_agent_key = (tenant_id, agent_key)`。`V27__extend_ai_agent_handoff_source.sql:11` 已 `ALTER TABLE muse_agent ADD COLUMN source_market_asset_id BIGINT`,注释自证"install 自动物化为独立后续主线,本列先就位、对齐 knowledge `muse_knowledge_base.source_market_asset_id`"——**列在、但休眠**:`MuseAgentDO.java` 当前只映射到 `tags` 为止,**没有 `sourceMarketAssetId` 字段**。**结论**:A-materialize 要给 `MuseAgentDO` 补映射这一列(@TableField),物化时回填,承载溯源 + 幂等键之一。**这是唯一的 schema 触动,且只补 DO 映射、不需新 Flyway 迁移**(列已存在)。 + +**事实 4 — agent 实体 = `muse_agent` 元数据 + `muse_agent_version.config` JSONB 商品本体;version active 即不可变;runtime 从 config 取 modelKey/promptTemplateVersion。** +`muse_agent_version`(`V4:96-110`)的 `config` 是 JSONB,承载发布者亲写的 agent 配置;唯一键 `uk_muse_agent_version_agent_ver = (tenant_id, agent_id, version)`,`status` DEFAULT 'draft'。运行时 `requireVisibleAgent:338-341` 从 `config` 取 `modelKey`(缺省 `DEFAULT_MODEL_KEY`)+ `promptTemplateVersion`(缺省 `DEFAULT_PROMPT_TEMPLATE_VERSION`)组装 `AgentRuntimeRef`,**运行时只读这份冻结的 config、不回读发布者任何活数据**。版本一经 active 即不可变(改动开新版本行,由 `(agent_id, version)` 唯一键保证),故"拷一份 active 版本的 config"= 拷一个不会再变的快照。**结论**:这正是"不 fork、拷配置"成立的根据——拷 config = 完整拷下商品本体,无外部资源(无 RAGFlow dataset 那类需要重建索引的东西),无后续漂移。A-materialize 拷 config 时须**整体拷贝、保完整性**(§4 R3),不能只拷部分键导致运行时取不到 modelKey/promptTemplate。 + +**事实 5 — config 拷贝的来源读取是本 plan 的核心决策点:market 当前不回 config,且 market 对 ai 零依赖、不能回读发布者 `muse_agent_version`。** +`MuseMarketAssetDO`(`source_id` BIGINT 指发布者 `muse_agent.id`、`asset_type`、`publisher_id`、`tags` JSONB)**只存来源指针 + 元数据,不承载 config 本体**。临时-04 已落的跨 BC 读端口 `MarketAssetSourceApi.getAssetSource(assetId)` 当前回的是 **KB-fork 形状**的 `MarketAssetSourceRespDTO{exists, assetType, sourceId, publisherUserId, assetName, publicForkDatasetId, forkStatus}`(`MarketAssetSourceApiImpl:37-54` 读 `muse_market_asset` + tags JSONB 里 U-fork 写回的副本状态)——**没有 agent config 这一项**,`publicForkDatasetId/forkStatus` 是 KB 专属、对 agent 无意义。而 market 对 ai **零依赖**(不能直读 ai 的 `.dal`、ArchUnit `BcBoundaryArchTest` 拦着),所以 market 自己也回读不到发布者的 `muse_agent_version.config`。**结论**:agent 物化要拿到"发布者 active 版本的 config",必须新增一条读 config 的途径——这是本 plan 最大的决策点(§3 D1:**扩 `MarketAssetSourceApi` 让 market 反向调 ai 读端口带回 config** vs **ai 侧自有读端口 ai 自己读发布者 agent**)。这是 agent 相对 KB 物化最不一样的地方:KB 的副本 dataset id 是 market 自己 fork 时写回 tags 的、market 读自有数据即可带回;agent 的 config 在 ai 域、market 域天生够不着。 + +**事实 6 — agent 无私有泄露向量(已实证),故不需要 KB 那套 fork。** +三条实证:①版本不可变(事实 4,`(agent_id, version)` 唯一键 + active 不可变);②运行时不回读发布者私有 prompt 正文,只用版本冻结 config(事实 4,`requireVisibleAgent:338-341`);③`config` 是被授权出售的商品本体(发布者亲写的 promptTemplate/slotBindings/toolGrantIds,上架即为供人安装使用)。对照 KB 的泄露链(市场对 knowledge 零依赖 → 上架不 fork → 安装者共享发布者活 dataset → 发布者上架后还能往库里加私有文档 → 安装者整库检索读到私有 chunk,见临时-04 §0/事实 1),**agent 这条链的每一环都不成立**:没有"上架后还能改"的活资源,没有"整库返回"的运行时读放大,config 本就是公开交付物。**结论**:agent 物化**不需要** fork 公开副本、不需要发布侧异步重活、不需要上架侧任何改动——物化全部落在**安装侧同步**完成(拷 config 是一次本地 DB 写 + 一次跨 BC 读,无外部调用、无长任务)。这是 agent 比 KB **轻得多**的根本原因。 + +**事实 7 — 可复用与不需要的 KB 已落资产,钉死本 plan 的真实增量。** +**可复用(照搬/扩展)**:①跨 BC 读端口 `MarketAssetSourceApi`(临时-04 已落,本 plan **扩**它支持 `assetType=agent` 放行 + 带 config,而非新建第二个端口);②handoff token 链路 `MarketHandoffTokenApi`(P2 已闭环,agent 槽位 handoff 的 `expectedTargetOwner=agent` 已支持,`HandoffVerifyRespDTO.targetOwner/targetAction` 已具备);③KB 的 `materializeInstalledRefKb` 物化形态(`MuseKnowledgeBindingService:377-417`:写本地实体 + 幂等键 `(source_market_asset_id, owner_user_id, deleted=false)` + 与绑定/投影同 `@Transactional`)——agent 照搬这个形态,把"建 installed_ref KB + dataset binding"换成"建 installed agent + active version + 槽位指本地 agentId"。**不需要(agent 没有对应物)**:①`MarketKbListedEvent` 孪生事件(agent 无 fork、无上架侧异步,物化全在安装侧同步,**不引入任何上架事件/消费者**);②`MarketAssetForkApi` 写回端口(那是 fork 状态投影回 market 用的,agent 不 fork);③`KnowledgeMarketForkService`/消费者/兜底 worker 那一整套发布侧异步管线(agent 全免)。**结论**:agent 的真实增量 = **扩 `MarketAssetSourceApi` 带 config(A-source)+ 安装侧建 installed agent 拷 config(A-materialize)+ 改运行时授权门(A-runtime)+ 命名收口(A-naming)+ 验证(A-verify)**,没有任何 fork/异步/外部资源工程。 + +**事实 8 — 命名漂移须收口(O3 开放项)。** +`后端-04`/临时-02 §3.2 文档里写的是 `market_installed`;P2 落地的槽位绑定来源标记是 `market`(`MuseAgentSlotServiceImpl` 的 `BINDING_SOURCE_MARKET` / `SOURCE_TYPE_MARKET_AGENT`);KB 侧用的是 `installed_ref`;本 plan 拟为 agent 新增的 `agent_type` 值待定(候选 `installed` / `market_installed` / `installed_ref`)。**结论**:A-naming 必须在落地前把"安装物化 agent 的 `agent_type` 取值"统一拍定一个值,并同步 `后端-04` 文档、消除文档与实现漂移(事实 3 的 V27 注释已埋"对齐 knowledge `source_market_asset_id`"的伏笔,命名也应与 knowledge 风格一致或显式说明差异)。 + +> 这八条把 agent 物化的真实成本钉死:**核心增量 = config 跨 BC 读取途径(事实 5,决策点 D1)+ 安装侧建 installed agent 拷 config(事实 4/7)+ 改运行时授权门(事实 1,trust boundary)+ 命名收口(事实 8)**;KB 那套 fork 公开副本 / 发布侧异步管线 / 上架事件 / 副本资源回收(临时-04 的核心增量),在 agent 下**全部不需要**(事实 6/7)。 + +--- + +## 2. 总览:实现单元、数据流与授权门改造对比 + +### 2.1 实现单元依赖图 + +```mermaid +flowchart TD + ASRC["A-source(扩跨 BC 读端口带 config,核心新读路径)
MarketAssetSourceApi 放行 assetType=agent
+ 带回发布者 active 版本 config(决策点 D1:经 market 反调 ai vs ai 自有读)"] + AMAT["A-materialize(安装侧建 installed agent 拷 config)
建本地 muse_agent(installed 型)+ active muse_agent_version(config 拷自发布者)
+ DO 映射 source_market_asset_id + 幂等 + 槽位指本地 agentId(去裸引用)"] + ART["A-runtime(改运行时授权门,trust boundary)
requireVisibleAgent 放行 installed 型 + 本人所有 + 授权快照
三入口(220/308/315)同时通"] + ANAME["A-naming(命名收口)
installed agent 的 agent_type 取值统一
+ 同步 后端-04 文档"] + AVER["A-verify(验证)
真 PG IT(install→物化→bind→runtime 跑 AI 任务命中物化 agent)
+ studio e2e + 授权门负路(他人 installed agent 必拒)"] + + ASRC --> AMAT + AMAT --> ART + ANAME -.贯穿命名.-> AMAT + ANAME -.贯穿命名.-> ART + ART --> AVER + + DEC{"前置决策门(人类拍板,§3)
D1 config 读取途径(market 反调 ai vs ai 自有读)
D2 拷 vs 引用 prompt 库
D3 agent_type 命名最终值
D4 installed agent 是否计配额/列表可见"} + DEC --> ASRC + + style ASRC fill:#fde2e2,stroke:#c0392b,stroke-width:3px + style ART fill:#fde2e2,stroke:#c0392b,stroke-width:3px + style AMAT fill:#e2f7e8,stroke:#27ae60 + style ANAME fill:#e2f7e8,stroke:#27ae60 + style AVER fill:#fff6d6,stroke:#b8860b + style DEC fill:#eef3fb,stroke:#3b6fb6,stroke-width:2px +``` + +### 2.2 拷配置 vs KB fork:为什么 agent 不需要 fork + +```mermaid +flowchart LR + subgraph PUB["发布者域(owner = 发布者)"] + PAGENT["发布者 muse_agent + active muse_agent_version
config(promptTemplate/slotBindings/toolGrantIds)
🔒 active 即不可变、不会再变"] + end + subgraph INS["安装者域(owner = 安装者)"] + IAGENT["installed 型 muse_agent(本人所有)
source_market_asset_id=assetId"] + IVER["active muse_agent_version
config = 整体拷自发布者快照"] + end + PAGENT ==>|安装侧同步拷 config
无 fork·无外部资源·无异步| IVER + IAGENT --> IVER + IVER --> RT["运行时 requireVisibleAgent
放行 installed 型 + 本人所有 + 授权有效
→ 从本地 config 取 modelKey/promptTemplate
🟢 命中物化 agent,AI 任务可跑"] + + NOTE["对照 KB:KB 必须 fork 一份只含公开快照的独立 dataset
(因发布者活 dataset 上架后还能加私有文档→整库检索泄露)
agent 无此向量→config 即商品本体、拷下即交付"] + + style PAGENT fill:#fde2e2,stroke:#c0392b + style IVER fill:#e2f7e8,stroke:#27ae60,stroke-width:2px + style RT fill:#e2f7e8,stroke:#27ae60 + style NOTE fill:#eef3fb,stroke:#3b6fb6 +``` + +### 2.3 安装侧物化 + 运行时数据流(agent 物化后) + +```mermaid +flowchart LR + A["用户跳转→ai 槽位绑定路径
precheckAgentSlot / bindAgentSlot"] + A --> B["消费 handoff token(原有,闸 A 已放宽 market 型,不动)"] + B --> C["A-source: MarketAssetSourceApi.getAssetSource(assetId)
assetType=agent 放行 + 带回发布者 active 版本 config(D1)"] + C --> D{资产存在且=agent?} + D -->|否| E["fail-closed: 拒绝绑定,0 写"] + D -->|是| F["A-materialize: 建本地 muse_agent(installed 型)
source_market_asset_id=assetId, owner=安装者, status=active"] + F --> G["建 active muse_agent_version
config=整体拷自发布者快照(保完整性)"] + F --> H["槽位 binding.agentId=本地 installed agentId(去裸引用,不再指发布者 agentId)"] + G --> I["A-runtime: 运行时 requireVisibleAgent(本地 agentId)
放行 installed 型 + 本人所有 + 授权快照
→ 取本地 config → 🟢 命中物化 agent"] + H --> 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 +``` + +### 2.4 授权门改造前后对比(A-runtime · trust boundary) + +```mermaid +flowchart TD + subgraph BEFORE["改造前 requireVisibleAgent:329-331(断点②)"] + B1["agentType=system?"] -->|否| B2["agentType=user 且 owner==本人?"] + B2 -->|否| BX["🔴 AI_AGENT_SCOPE_FORBIDDEN
market/installed 一律拒"] + B1 -->|是| BOK["放行"] + B2 -->|是| BOK + end + subgraph AFTER["改造后(A-runtime 放行 installed)"] + A1["agentType=system?"] -->|否| A2["agentType=user 且 owner==本人?"] + A2 -->|否| A3["agentType=installed 且 owner==本人
且授权快照有效?"] + A3 -->|否| AX["🔴 SCOPE_FORBIDDEN
(他人 installed / 无授权 仍拒——负路测命门)"] + A3 -->|是| AOK["🟢 放行 installed"] + A1 -->|是| AOK + A2 -->|是| AOK + end + + style BX fill:#fde2e2,stroke:#c0392b + style AX fill:#fde2e2,stroke:#c0392b + style AOK fill:#e2f7e8,stroke:#27ae60,stroke-width:2px +``` + +--- + +## 3. 实现单元 + +> 文件路径均为 repo 相对路径。每个单元含 Goal / Files / Approach / Test scenarios(输入+预期)/ Verification。测试场景"输入"指具体调用入参或数据状态,"预期"指可断言的可观测结果。 + +### A-source · 扩跨 BC 读端口带 config + 放行 assetType=agent(核心新读路径) + +**Goal**:让安装侧能跨 BC 拿到"市场 agent 资产的来源 + 发布者 active 版本的 config 本体",作为拷配置的输入。复用临时-04 已落的 `MarketAssetSourceApi`,**扩**它支持 `assetType=agent`(不再只服务 KB),并解决"market 域够不着 ai 域 config"这一核心矛盾(决策点 D1)。 + +**Files**: +- 扩跨 BC 读端口(被 ai 依赖):`muse-cloud/muse-module-market/muse-module-market-api/src/main/java/cn/iocoder/muse/module/market/api/asset/MarketAssetSourceApi.java`(已存在,扩语义 + 可能新增带 config 的方法或扩 DTO)+ `.../api/asset/dto/MarketAssetSourceRespDTO.java`(已存在,KB-fork 形状,需为 agent 增 config 字段或拆 agent 专用 DTO,见 Approach) +- market 侧实现:`muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/api/asset/MarketAssetSourceApiImpl.java`(已存在,读 `MuseMarketAssetDO` 的 source_id/asset_type/publisher_id;agent config 的来源见 D1) +- **D1 选 A(market 反调 ai 读端口)时新增**:ai 侧对外读端口 `muse-cloud/muse-module-ai/muse-module-ai-api/src/main/java/cn/iocoder/muse/module/ai/api/agent/AgentConfigSourceApi.java`(新)+ DTO(回 `{exists, agentType, status, activeVersion, config, ownerUserId}`),market 注入它读发布者 active 版本 config;范式同 `MarketHandoffTokenApi`(本地 Bean、不挂 Feign)。 +- **D1 选 B(ai 自有读)时**:不扩 market,改在 ai 绑定路径内由 ai 自己读发布者 `muse_agent_version`(同域读自有 `.dal` 合规),但需 ai 凭 assetId→发布者 agentId 的映射(仍要 market 回 source_id,故仍需 `MarketAssetSourceApi` 回 sourceId,只是 config 由 ai 自取)。 +- 复用范本:`MarketAssetSourceApiImpl`(KB 已落的跨 BC 读 + tags 解析写法)、`MarketHandoffTokenApi`(本地进程内 Bean 端口范式)。 + +**Approach**: + +**① DTO 形状(与 KB 共存的取舍)**:现有 `MarketAssetSourceRespDTO` 是 KB-fork 形状(带 `publicForkDatasetId/forkStatus`,对 agent 无意义)。两种处置:(a) **给现有 DTO 增 agent 专属字段**(`agentConfig`/`agentActiveVersion`),KB 字段对 agent 留 null、反之亦然——一个 DTO 承两类资产,简单但字段半数恒 null;(b) **按 assetType 拆 agent 专用 DTO/方法**(`getAgentAssetSource(assetId)→MarketAgentSourceRespDTO{exists, sourceId, publisherUserId, agentName, agentConfig, activeVersion}`),干净但端口多一个方法。**倾向 (b)**(接口隔离,KB/agent 各取所需,与临时-04 注释里"读写分离、避免互相依赖用不到的方法"的设计取向一致);最终由实现时定夺并回填。 + +**② assetType=agent 放行**:现有 `getAssetSource` 对所有 assetType 一视同仁返回 KB 形状;扩为按 `asset.getAssetType()` 分流——`agent` 类资产走 agent 路径(带 config)、`knowledge_base` 维持现状(带副本)。安装侧据 assetType fail-closed:agent 绑定路径只接纳 `assetType=agent`,KB 绑定路径只接纳 `knowledge_base`(防止把 KB 当 agent 物化或反之)。 + +**③ config 读取途径(决策点 D1,核心,给推荐 + 理由)**——**倾向"market 反调 ai 读端口带回 config(选 A)"**: +- **选项 A(推荐):market 反调 ai 读端口**。新增 ai 侧 `AgentConfigSourceApi`(ai 域读自有 `muse_agent_version` 合规),market 的 `MarketAssetSourceApiImpl` 注入它,凭 `source_id`(发布者 agentId)读发布者 active 版本 config,组装进返回 DTO 一并带给安装侧。 + - 优点:①**安装侧只跟一个端口打交道**(`MarketAssetSourceApi`),与 KB 物化的调用形态完全对称(安装侧"一次跨 BC 读拿全物化所需信息");②config 读取的 BC 合规性由"ai 读自有 + market→ai 经 -api 端口"双重保证;③market 作为资产来源的权威出口,"按 assetId 给来源全貌"语义自洽。 + - 代价:引入 market→ai 的新依赖方向(当前 ai→market 已有 `MarketHandoffTokenApi`/`MarketAssetSourceApi` 依赖;market→ai 是新方向,需 ArchUnit 确认不成环——market 与 ai 同进程聚合、经 -api 端口不成循环依赖,但需 review)。 +- **选项 B:ai 绑定路径自己读发布者 agent**。`MarketAssetSourceApi` 仍只回 source_id(发布者 agentId),ai 绑定路径凭它读发布者 `muse_agent_version`(同域读自有)。 + - 优点:不引入 market→ai 新依赖。 + - 代价:①**跨 owner 读**——ai 绑定路径以安装者身份读发布者租户的 agent 版本,要绕过/显式处理租户拦截器(类似临时-03 被 D0-fork 消解掉的"跨 owner 读发布者 dataset"难题,在 agent 这里若选 B 会重新出现);②config 读取逻辑散在绑定路径、不如端口聚合清晰。 +- **权衡结论**:选 A 让安装侧调用对称、避免跨 owner 读难题(B 的真痛点),代价(market→ai 新方向)经 -api 端口 + ArchUnit 可控。**推荐 A**;若 review 认为 market→ai 依赖方向不可接受,退 B 并显式处理跨 owner 读(A-materialize ⑤ 标注)。 + +**④ 凭据/审计**:跨 BC 读 config 不涉及外部服务、无 key;但 config 可能含 promptTemplate 正文,端口日志**不打印 config 内容**(只记 assetId/agentType/版本号),避免商品本体进日志。 + +**Test scenarios(输入+预期)**: +- agent 资产来源 + config 带回|输入:assetId=X(asset_type=agent,source_id=发布者 agentId=G,G 有 active version v1 config={name,promptTemplate,modelKey,...}),调 `getAgentAssetSource(X)`|预期:返回 `{exists=true, assetType=agent, sourceId=G, publisherUserId, agentConfig=v1 的完整 config, activeVersion=v1}`;config 各键齐全(含 modelKey/promptTemplate)。 +- 非 agent 资产|输入:assetId 指向 knowledge_base 类型|预期:agent 读路径 fail-closed(exists=false 或显式 assetType≠agent,安装侧据此拒绝按 agent 物化)。 +- 资产不存在|输入:错 assetId/跨租户|预期:`notFound()`(exists=false),安装侧 fail-closed。 +- 发布者无 active 版本|输入:G 存在但无 active muse_agent_version|预期:带回 config=null / activeVersion=null,安装侧 fail-closed(不物化出一个无 config 的空 agent)。 +- BC 边界|输入:ArchUnit `BcBoundaryArchTest`|预期:选 A 时 market→ai 经 -api 端口、不直读 ai `.dal`,且不成循环依赖,test 绿。 + +**Verification**: +- 模块单测:`cd muse-cloud && mvn -pl muse-module-market/muse-module-market-server -am test -Dtest='MarketAssetSourceApiImplTest'`(mock ai 读端口,覆盖 agent 分流 + config 带回 + 非 agent/不存在 fail-closed)。 +- **改了 market-api / ai-api 模块务必先 `mvn -pl muse-module-market/muse-module-market-api -am install`(及 ai-api)** 再跑依赖模块测试,否则 stale jar 假红(memory `muse-market-install-isacquired-enrich` 教训)。 +- ArchUnit:`mvn -pl ... test -Dtest=BcBoundaryArchTest`(确认 market→ai 不成环)。 + +--- + +### A-materialize · 安装侧建 installed agent 拷 config(照搬 KB materializeInstalledRefKb 形态) + +**Goal**:在 ai 目标域绑定路径里,把"槽位写裸引用"升级为"建出可用的本地 installed 型 agent + active version(config 拷自发布者)+ 槽位指向本地 agentId"。做到 `MuseAgentDO` 补映射 `source_market_asset_id` + 幂等 + config 整体拷贝 + 去裸引用。**与 KB U-materialize 同范式、更轻**(无 dataset、无 fork、无外部调用)。 + +**Files**: +- DO 补映射(事实 3):`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/dataobject/muse/MuseAgentDO.java`(加 `private Long sourceMarketAssetId;` + @TableField,列 V27 已在、不需新迁移) +- 主改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAgentSlotServiceImpl.java`(`precheckAgentSlot:93`、`bindAgentSlot:158`、`consumeMarketHandoff:350`、绑定持久化 upsert `:250`) +- installed agent 写入 DAL:`.../ai/dal/mysql/muse/MuseAgentMapper.java`(复用 insert + 按 source_market_asset_id 幂等查)、`MuseAgentVersionMapper.java`(复用 insert active version) +- 参照范本(照搬物化形态):`muse-cloud/muse-module-knowledge/muse-module-knowledge-server/src/main/java/cn/iocoder/muse/module/knowledge/application/muse/MuseKnowledgeBindingService.java`(`materializeInstalledRefKb:377-417`:幂等键 `(source_market_asset_id, owner_user_id, deleted=false)` + 建本地实体 + 与绑定同 `@Transactional`;§128-139 precheck 固化物化所需事实、bind 阶段读回建实体的两段式) + +**Approach**: +1. **DO 补映射**:`MuseAgentDO` 加 `sourceMarketAssetId`(@TableField 普通列,V27 已建 `source_market_asset_id BIGINT`)。这是物化溯源 + 幂等键的载体,**唯一的 schema 触动且不需新迁移**(事实 3)。 +2. **precheck 固化 + assetType 校验(照搬 KB 两段式)**:`precheckAgentSlot` 对 market 来源(`source_owner=market`,事实 2)调 A-source 的 `getAgentAssetSource(assetId)` 校验"资产存在且 `assetType=agent` 且发布者有 active 版本 config",fail-closed(不存在/非 agent/无 config 拒),并把"发布者 agentId + 待拷 config + 授权快照"固化进 precheck 快照供 bind 读回(避免 bind 时再跨 BC 读)。**precheck 不建任何 agent 实体**(避免 precheck 失败留孤儿 agent 行,与 KB `materializeInstalledRefKb` 注释同理)。 +3. **bind 阶段建 installed agent(落点:`bindAgentSlot` 同一 `@Transactional`)**: + - insert `muse_agent`:`agent_type=`、`source_market_asset_id=assetId`、`owner_user_id=安装者`、`status=active`、`name/description/prompt_key/category` 取自拷来的 config、`agent_key=<安装者维度唯一>`(**注意 `uk_muse_agent_key=(tenant_id, agent_key)`**,事实 3:installed agent 的 agent_key 须保证安装者租户内唯一,建议 `installed-{assetId}-{ownerUserId}` 或带 uuid,避免与发布者/其他 installed agent 撞键);拿到**本地新建 agentId**。 + - insert `muse_agent_version`:`agent_id=本地agentId`、`version=1`(或拷发布者版本号)、`config=整体拷自发布者 active 版本 config`、`status=active`;**config 整体拷贝**(保完整性,R3),拿到 versionId 回写 `muse_agent.current_version_id`。 + - **去裸引用**:槽位 binding 的 `agentId` 落**本地新建 agentId**(不再是发布者 agentId),`agentVersion` 落本地版本号;建实体放 `bindAgentSlot`(用户确认绑定的写事实点)。 +4. **幂等(照搬 KB)**:建 installed agent 前先 `SELECT ... WHERE source_market_asset_id=assetId AND owner_user_id=安装者 AND agent_type= AND deleted=false`,命中则复用既有 installed agentId(连同其 active version),不重复建;并发由 `uk_muse_agent_key` 唯一键 + `DataIntegrityViolationException`→回读模式兜(仿 KB `persistDatasetBinding` 的回读幂等)。**这保证同一安装者对同一资产多次绑定只有一行 installed agent。** +5. **授权快照承载**:installed agent 的授权溯源沿用 ADR-020 字符串 envelope;槽位 binding 的 `authorization_snapshot_id` 已 V31 VARCHAR 直透传(事实见 `MuseAgentSlotServiceImpl:281-282`),不回退 BIGINT/parseLong。物化 agent 实体若需携带授权快照,沿用 VARCHAR(D4 决策)。 +6. **事务原子性**:物化(建 agent + version)与"消费 handoff token + 写槽位绑定"在同一 `@Transactional`(现状 `bindAgentSlot:157`/`consumeMarketHandoff:348` 已沿用调用方事务),失败整体回滚。 +7. **多安装者各建各的**:不同安装者各建自己的 installed agent(各自 owner + 各自 agent_key),config 各拷一份(agent config 是轻量 JSONB,不像 KB dataset 那样有共享必要;**agent 无"副本仅 1 份"概念**——这是与 KB 的又一差异:KB 副本 dataset 重、N 安装者共享只读一份;agent config 轻、各拷各的最简单且无泄露顾虑)。 + +**Test scenarios(输入+预期)**: +- 首次安装绑定|输入:T_A 安装 assetId=X(asset_type=agent,发布者 active config 完整),`bindAgentSlot`(合法 handoff token + precheckId + slotKey)|预期:新建 `muse_agent(agent_type=installed, source_market_asset_id=X, owner_user_id=A, status=active)` 记 id=G2;新建 `muse_agent_version(agent_id=G2, status=active, config=X 的完整 config)`;`muse_agent.current_version_id` 指向该 version;槽位 binding.agentId=**G2**(非发布者 agentId)。 +- config 完整性|输入:发布者 config 含 {name, description, promptKey, promptTemplate, slotBindings, toolGrantIds, modelKey, promptTemplateVersion}|预期:拷出的本地 version config **逐键齐全**(运行时取 modelKey/promptTemplate 不落缺省、不丢 toolGrantIds)。 +- 多安装者各建各的|输入:A、B 各装 assetId=X|预期:建两行 installed agent(G2、G3,各自 owner + 各自唯一 agent_key),各有自己的 active version(config 各一份),不违反 `uk_muse_agent_key`。 +- 幂等-重复绑定|输入:同一 (A, X) 再次 bind|预期:不新建第二行 installed agent(复用 G2),不抛 500。 +- 资产不存在/非 agent|输入:assetId 指向不存在 / knowledge_base 类型|预期:fail-closed 拒绝,0 写 agent/version/binding。 +- agent_key 唯一键不撞|输入:A 装 X、又装另一 agent 资产 Y|预期:两行 installed agent 的 agent_key 互不相同(按 assetId 维度),不触发 `uk_muse_agent_key` 冲突。 +- BC 边界|输入:ArchUnit `BcBoundaryArchTest`|预期:ai 经 `MarketAssetSourceApi` 读、不直读 market `.dal`,test 绿。 + +**Verification**: +- 模块单测:`cd muse-cloud && mvn -pl muse-module-ai/muse-module-ai-server -am test -Dtest='MuseAgentSlotServiceTest'`(mock `MarketAssetSourceApi`,覆盖建 installed agent + config 完整拷贝 + 幂等 + 多安装者 + fail-closed)。 +- **改了 market-api/ai-api 先 `mvn -pl ...-api -am install`** 再跑 server 模块测试(stale jar 假红教训)。 +- ArchUnit:`mvn -pl ... test -Dtest=BcBoundaryArchTest`。 + +--- + +### A-runtime · 改运行时授权门放行 installed 型(trust boundary,本 plan 最需严防处) + +**Goal**:把运行时授权门 `requireVisibleAgent` 从"只放行 system / 本人 user"扩展为"**+ 本人所有的 installed 型 + 授权快照有效**",使三入口(agentTest/agentOverrideRef/agentSlotKey)对物化出的 installed agent 同时放行;**同时严守边界——他人的 installed agent、无授权的 installed agent 仍必须拒**(这是 trust boundary,负路测是验收命门)。 + +**Files**: +- 主改(唯一断点):`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiTaskServiceImpl.java`(`requireVisibleAgent:321-342`,可见性判定 `:329-331`;三入口 `:220`/`:308`/`:315` 不改、自动随门放行) +- 不改:闸 A `MuseAgentSlotServiceImpl.requireVisibleActiveSourceAgent`(事实 2,已放宽、与运行时是两套门,运行时是本单元唯一战场) + +**Approach**: +1. **放行条件扩展(精确改 `:329-331`)**:原判定"非 system 且 非(user 且本人)→拒"扩为"非 system 且 非(user 且本人) 且 非(installed 且本人 且授权有效)→拒"。即新增一条放行支:`agent_type= 且 ownerUserId==agent.ownerUserId 且 授权快照有效`。**三个收紧点缺一不可**:①必须是 installed 型(不放开 market 裸引用——物化后槽位指的是本地 installed agent,不存在裸 market 引用进运行时);②必须本人所有(`ownerUserId` 比对,防越权用他人 installed agent);③授权快照有效(防授权已撤销/过期仍可跑)。 +2. **授权有效性判定**:installed agent 行的 `source_market_asset_id` + 授权快照(ADR-020 VARCHAR envelope)钉死"凭哪份授权可用"。运行时校验授权快照有效(非 `blocked:` 前缀等,参照闸 A `rejectBlockedSource:326-330` 的 envelope 语义),授权失效则拒(与召回/下架回滚开放项联动,§5)。 +3. **三入口自动覆盖**:`:220`/`:308`/`:315` 都调 `requireVisibleAgent`,改门体即三入口同时生效,无需逐入口改(与 KB 检索"改数据正确即第四门自然通"异曲同工——这里是"改门一处即三入口通")。 +4. **status/version 两道维持**:`:326-327`(agent.status=active)+ `:333-336`(active version 存在)不放松,installed agent 物化时已写 active(A-materialize),自然满足。 + +**Test scenarios(输入+预期)**: +- 本人 installed agent 放行(正路)|输入:A 物化的 installed agent G2(active + active version + 授权有效),A 发起 AI 任务命中 G2(经 slotKey/override/agentTest 任一入口)|预期:`requireVisibleAgent` 放行,返回 `AgentRuntimeRef`(从 G2 active version config 取 modelKey/promptTemplate),不抛 SCOPE_FORBIDDEN。 +- **他人 installed agent 必拒(负路,trust boundary 命门)**|输入:A 物化的 installed agent G2,**B** 试图用 G2(伪造 agentOverrideRef 或越权 slotKey)|预期:抛 `AI_AGENT_SCOPE_FORBIDDEN`(ownerUserId 不符),0 放行——**这是本 plan 安全验收的核心断言**。 +- 授权失效的 installed agent 必拒(负路)|输入:A 的 installed agent G2 但授权快照已 blocked/撤销|预期:拒(授权无效支),不放行。 +- market 裸引用仍拒(负路,防回归)|输入:直接用一个未物化的 market 出身 agentId(非 installed 型、非本人 user)|预期:仍抛 SCOPE_FORBIDDEN(只放行 installed 型本地实体,不放开 market 型本身)。 +- system/本人 user 不回归|输入:system agent / 本人 user agent|预期:照旧放行(原有行为不变)。 +- 三入口一致|输入:同一 installed agent 分别经 agentTest(:220)/override(:308)/slotKey(:315)|预期:三入口行为一致(都放行本人 installed、都拒他人 installed)。 + +**Verification**: +- 单测:`mvn -pl muse-module-ai/muse-module-ai-server -am test -Dtest='MuseAiTaskServiceTest'`(覆盖正路放行 + **他人 installed 必拒** + 授权失效必拒 + market 裸引用必拒 + system/user 不回归 + 三入口一致)。**负路测是本单元验收门,缺负路不算通过**。 +- 活体并入 A-verify 端到端(真跑 AI 任务命中物化 agent + 真验他人不可用)。 + +--- + +### A-naming · 命名收口(贯穿 A-materialize / A-runtime) + +**Goal**:把"安装物化 agent 的 `agent_type` 取值"统一拍定一个值,消除文档(`market_installed`)/ P2 落地(`market`)/ KB 风格(`installed_ref`)/ 本 plan 拟新增(`installed`)的四处漂移(事实 8),并同步 `后端-04`。 + +**Files**: +- 文档同步:`design-docs/后端-04-统一数据库Schema-v1.md`(§7 AI/Agent,`muse_agent.agent_type` 取值定义;当前写 `system/user/market_installed`,按拍定值订正) +- 落地一致性:A-materialize 写入的 `agent_type` 值 + A-runtime 放行判定的 `agent_type` 值必须用同一常量(建议 ai 模块内定义常量,避免散字符串漂移) + +**Approach**: +1. **取值候选与倾向**:`installed`(简洁、与 KB `installed_ref` 风格呼应但更短)/ `market_installed`(与 `后端-04` 现有文档一致、语义最明确)/ `installed_ref`(完全对齐 KB)。**倾向 `market_installed`**(与 `后端-04` 既有文档零漂移、语义自明"市场安装来的",且 V27 注释已写"对齐 knowledge `source_market_asset_id`"暗示与 knowledge 风格协调即可、不强求同名);最终由人类拍(D3)。 +2. **单一来源**:拍定后 A-materialize/A-runtime 共用一个常量,`后端-04` 同步订正,避免再次漂移。 +3. **与 `BINDING_SOURCE_MARKET` 区分**:注意 `agent_type`(agent 实体类型)与槽位 binding 的 `binding_source=market`(绑定来源标记,事实 2)是两个维度,**不要混淆**——物化后 binding_source 仍可标 market(来源是市场),但 binding 指向的 agent 其 agent_type 是 installed(实体是本地物化的)。A-naming 须在文档里点明这一区分,防后续 agent 误把两者当一回事。 + +**Test scenarios(输入+预期)**: +- 取值一致性|输入:grep `agent_type` 写入点与放行判定点|预期:用同一常量、无散字符串。 +- 文档零漂移|输入:`后端-04` §7 `agent_type` 取值 vs 代码常量|预期:一致。 + +**Verification**:人工核对 `后端-04` 与代码常量一致;A-materialize/A-runtime 单测断言用同一常量值。 + +--- + +### A-verify · 验证(真 PG 端到端 + studio e2e + 授权门负路) + +**Goal**:用真实证据证明"安装 agent→物化 installed agent→槽位绑定→运行时跑 AI 任务命中物化 agent"端到端可真用;且**授权门负路成立**(他人 installed agent / 无授权必拒——trust boundary 的真验收)。 + +**Files**: +- 真 PG IT(新):`muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rMarketAgentMaterializationIT.java`(参照 KB 的 `P1rMarketKbForkMaterializationIT` + 既有 `P1r*IT` live 范式) +- studio e2e(新):`muse-studio/e2e/market-install-agent-runtime.spec.ts`(参照 `agent-slot.spec.ts` + `market-install.spec.ts`,及 KB 的 `market-install-kb-retrieval.spec.ts` 范式) +- 单测(已在各单元):`MuseAiTaskServiceTest`(授权门负路)/ `MuseAgentSlotServiceTest`(物化)/ `MarketAssetSourceApiImplTest`(带 config) + +**Approach / Test scenarios(输入+预期)**: +1. **install→物化→bind→runtime 端到端(真 PG)**|输入:发布者建 agent(active version,config 完整)→ admin 审核上架成 market agent 资产 → 用户安装该资产 → 槽位绑定(物化 installed agent + version + 槽位指本地 agentId)→ 发起 AI 任务命中该槽位|预期:运行时 `requireVisibleAgent` 放行物化 installed agent → 真 New-API 出候选(AI 外部依赖在 mini-infra 100.64.0.8 在线,memory `muse-ai-external-deps-online`);对照现状(必 `AI_AGENT_SCOPE_FORBIDDEN`)。 +2. **授权门负路(trust boundary 核心验证)**|输入:A 物化 installed agent G2,B 试图用 G2 跑 AI 任务(伪造 agentOverrideRef)|预期:抛 `AI_AGENT_SCOPE_FORBIDDEN`,B 拿不到 G2——**这是 agent plan 安全验收的命门,缺此不算通过**。 +3. **config 完整性真验**|输入:发布者 config 含特定 promptTemplate/modelKey/toolGrantIds|预期:物化 agent 真跑时用的是拷来的 config(候选反映发布者 prompt 行为、modelKey 生效),不落缺省、不丢 toolGrantIds。 +4. **幂等**|输入:重复 install + 重复 bind|预期:DB 中该安装者该资产仅一行 installed agent + 一行 active version。 +5. **多安装者各跑各的**|输入:A、B 各装同一 agent 资产|预期:各自 installed agent(config 各一份),各自跑 AI 任务互不影响、互不可见对方实体。 +6. **召回/下架回滚(开放项,照搬 KB 的诚实标注)**|输入:发布者 agent 资产被 admin 召回/下架|预期:已物化 installed agent 被阻断新使用(授权失效→A-runtime 授权支拒)。**开放项**:market 召回当前对目标 owner 传播是 fail-closed blocked 记录式(与 KB 同源缺口,临时-04 事实 7),ai 侧消费"召回→停用 installed agent"的入口**本期缺**——A-verify 须如实验证现状传播是否触达 installed agent;若不触达,标开放项(与 KB 召回回滚同一未触达面,§5),不假装闭环。 +7. **机械门禁**|输入:ArchUnit `BcBoundaryArchTest` + 覆盖 JSON 门|预期:物化经 ai facade + `MarketAssetSourceApi`,market 不直写 ai `.dal`,market→ai(若 D1 选 A)不成环,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`),online(New-API 在 mini-infra 在线)。**`_test` 库铁律:IT 的 flyway 目标库绝不指 muse_slice_live。** +- studio e2e:起全栈 app(PG 宿主 100.64.0.8 在线时),`npx playwright test market-install-agent-runtime.spec.ts`。 +- 凭据红线:所有脚本/日志过滤 `password|secret|token|sk-`;不打印 config promptTemplate 正文;不 rm `dump.rdb`/`lefthook`。 + +--- + +## 4. 决策点(待人类拍板,按优先级) + +| # | 决策点 | 选项 | 倾向 | 阻塞谁 | +|---|---|---|---|---| +| **D1** | **config 跨 BC 读取途径**(核心) | 扩 `MarketAssetSourceApi` 让 market 反调 ai 读端口带回 config / ai 绑定路径自己读发布者 agent | **market 反调 ai(选 A)**:安装侧调用对称 + 避免跨 owner 读难题;代价(market→ai 新方向)经 -api 端口+ArchUnit 可控。若 review 否决 market→ai 方向则退 B 并显式处理跨 owner 读 | A-source 全部 | +| **D2** | **拷 vs 引用 prompt 库** | config 整体拷贝(含 promptTemplate 正文快照)/ 引用发布者 prompt 库(promptKey 指回) | **整体拷贝**(agent 无泄露向量、config 即商品本体、版本不可变→拷下即冻结快照,与"不 fork、拷配置"拍板一致;引用会重新引入"发布者改 prompt 库影响安装者"的漂移) | A-materialize config 写法 | +| **D3** | **installed agent 的 `agent_type` 命名最终值** | `market_installed` / `installed` / `installed_ref` | **`market_installed`**(与 `后端-04` 既有文档零漂移、语义自明;与 knowledge 风格协调即可不强求同名) | A-naming / A-materialize / A-runtime | +| D4 | installed agent 授权快照承载 | 数值主键 / 字符串 envelope(VARCHAR) | **字符串 envelope**(沿用 ADR-020,槽位 binding 已 V31 VARCHAR,物化实体一致) | A-materialize / A-runtime | +| D5 | installed agent 是否计用户配额 / 列表可见性 | 计入用户 agent 配额并在"我的智能体"列表可见 / 不计配额、列表单独分区或隐藏 | 倾向**列表可见但标 installed 来源、是否计配额随产品定**(不阻断本 plan 核心链路,可后置;但需 A-materialize 明确 installed agent 的列表查询是否纳入,避免"装了看不到"或"污染自建列表") | 列表/配额读模型(非核心链路) | +| D6 | DTO 形状 | 现有 `MarketAssetSourceRespDTO` 增 agent 字段 / 拆 agent 专用 DTO+方法 | **拆 agent 专用**(接口隔离,KB/agent 各取所需;与临时-04 读写分离取向一致) | A-source DTO | +| D7 | 召回/下架回滚自动化 | 复用现有 market→目标 owner 传播 / 本期标开放项 | 与 KB 同一未触达面(临时-04 事实 7/D9),**倾向本期标开放项**,不假装闭环 | A-verify | + +--- + +## 5. 横切开放项(与 KB 同未触达,如实标,不假装闭环) + +- **召回回滚未触达 installed agent(与 KB installed_ref 同一开放面)**。market 召回/下架资产时对目标 owner 的传播当前是 fail-closed blocked 记录式(临时-04 事实 7,`writeSourceStatusBlocked` errorCode=TARGET_OWNER_UNAVAILABLE),ai 侧没有"召回→停用 installed agent"的消费入口。本期 agent 物化落地后,这条回滚链与 KB 的 installed_ref 回滚是**同一个未补的传播消费面**——倾向两者一起补一个 market→(knowledge+ai)的召回传播消费者(统一处置 installed 实体停用),但**本期标开放项**,A-runtime 的"授权快照失效即拒"是兜底(授权撤销路径若与召回联动则部分覆盖,否则纯开放)。 +- **e2e fixture vs 真物化**。A-verify 的 IT 覆盖后端链路;studio e2e 的 global-setup 物化 fixture 是否走真物化(真建 installed agent)还是种子数据,需与 KB e2e 同步对齐(KB 侧 e2e fixture 也是开放项,临时-04 §诚实遗留)——避免 fixture 巧合假绿(临时-02 §1.3.1 注脚那种"种子已存在实体绕过断点"的假绿教训)。 +- **installed agent 列表/配额读模型(D5)**。非核心链路,但物化出的 installed agent 在"我的智能体"列表的可见性 + 是否计配额是产品决策,本期标横切待定,不阻断运行时打通。 + +--- + +## 6. 风险 + +- **R1(最高)改授权门 = trust boundary,必负路测**。A-runtime 放行 installed 型是对运行时权限边界的修改,最大风险是**放过头**——把"本人 installed"误放成"任意 installed / market 裸引用 / 他人 installed"。**缓解**:放行三收紧点缺一不可(installed 型 + 本人所有 + 授权有效,§A-runtime ①);A-verify 场景 2 + A-runtime 负路测把"他人 installed 必拒 / 授权失效必拒 / market 裸引用必拒"作为**验收命门**,缺负路不算通过。这是 CLAUDE.md"trust boundary 服务端强制 + 改授权必负路"红线的直接落点。 +- **R2 config 跨 BC 读取的依赖方向(D1)**。选 A 引入 market→ai 新依赖方向,需 ArchUnit 确认不成环(market 与 ai 同进程聚合、经 -api 端口不构成循环依赖,但要 review 确认)。**缓解**:A-source ArchUnit 门;若 review 否决则退 D1 选 B(ai 自读)并显式处理跨 owner 读。 +- **R3 config 拷贝完整性**。拷 config 若只拷部分键(漏 modelKey/promptTemplate/toolGrantIds),运行时取值落缺省或丢工具授权 → 物化 agent 行为与发布者不一致(用户买到"残品")。**缓解**:A-materialize ③ 整体拷贝(D2 整体拷)+ A-verify 场景 3 真验 config 完整性(候选反映发布者 prompt 行为 + modelKey 生效 + toolGrantIds 不丢)。 +- **R4 命名漂移收口(事实 8)**。`agent_type` 四处漂移若不收口,A-materialize 写入值与 A-runtime 放行值不一致 → 物化了但运行时仍拒(自造断点)。**缓解**:A-naming 拍定单一常量 + 同步 `后端-04`,A-materialize/A-runtime 共用常量。 +- **R5 agent_key 唯一键冲突**。`uk_muse_agent_key=(tenant_id, agent_key)`(事实 3):installed agent 的 agent_key 若按发布者 agent_key 直拷,多安装者/与发布者会撞键 500。**缓解**:A-materialize ③ agent_key 按安装者维度生成(`installed-{assetId}-{ownerUserId}` 或带 uuid),保安装者租户内唯一;幂等查走 `source_market_asset_id`(非 agent_key)。 +- **R6 召回回滚未自动化(开放项,与 KB 同面)**。临时-04 事实 7,本期标开放(§5 / D7),不假装闭环;A-runtime 授权失效兜底部分覆盖。 + +--- + +## 7. 回滚(本 plan 执行后如何撤回) + +- **代码回滚**:A-source 扩端口 + A-materialize(`MuseAgentSlotServiceImpl` 物化 + `MuseAgentDO` 补映射)+ A-runtime(`requireVisibleAgent` 放行支)+ A-naming(文档)。revert 这些 commit 即回到"槽位写裸引用 + 运行时拒 market agent"现状(断点②,功能不可用但与现状一致、不报新错)。 +- **数据回滚**:已物化的 installed agent 行 + active version 经软删停用(`deleted=true`);**无外部资源需清理**(agent 不像 KB 有 fork 出的 dataset,这是 agent 回滚比 KB 简单处——无 RAGFlow 删 dataset 步骤)。 +- **迁移回滚**:本 plan **不引入新 Flyway 迁移**(`source_market_asset_id` 列 V27 已在、仅补 DO 映射;授权快照 D4 沿用 V31 VARCHAR),无迁移需回滚。 + +--- + +## 8. Scope 边界(明确不做什么) + +- **agent 限定**:本 plan **只做智能体(agent)类资产的物化**;KB 物化已由临时-04 D0-fork 闭环,不重做。 +- **不 fork、不碰外部资源**:agent 无泄露向量(事实 6),物化 = 安装侧同步拷 config,**不引入任何 fork / 发布侧异步管线 / 上架事件 / 外部服务调用**(与 KB D0-fork 的核心区别)。 +- **install 解耦不动**:install 维持"只记账、`targetFactsWritten=false`"现状,**不在 install 侧物化**——物化落点严格在 ai 槽位绑定路径。 +- **闸 A 不动**:槽位授权门 `requireVisibleActiveSourceAgent` 已放宽 market(事实 2),本 plan 不碰;断点纯在运行时闸 B。 +- **handoff 非物化桥**:handoff token 只负责跳转 + 被目标域消费(`MarketHandoffTokenApi.verify/consume` 不动)。 +- **副本资源回收不适用**:agent 无副本 dataset,无 KB 那条"下架/无安装清理副本"开放项。 +- **召回/下架回滚自动化不做**:本期标开放项(R6/D7/§5),与 KB 同面,不假装闭环。 +- **installed agent 列表/配额读模型**:D5 标横切待定,非核心链路,本期不强制实现。 +- **market 两列 BIGINT 议题不纳入**:`muse_market_installation.authorization_snapshot_id`/`source_snapshot_id` 仍 BIGINT 是独立契约议题,本 plan 不处理。 + +--- + +## 9. 与 KB D0-fork 的异同摘要(一表看清) + +| 维度 | KB 物化(临时-04 D0-fork,已闭环) | Agent 物化(本 plan 临时-05) | +|---|---|---| +| 泄露向量 | **有**:发布者活 dataset 上架后还能加私有文档→整库检索泄露 | **无**:version 不可变 + 运行时不回读私有 prompt + config 即商品本体 | +| 物化手段 | **fork 公开副本**(建副本 dataset + 逐文档复制 + 重索引,异步重活) | **拷配置**(安装侧同步拷 active version config,无外部资源、无异步) | +| 发布侧工程 | 有:上架事件 + knowledge fork 消费者 + 兜底 worker + `MarketAssetForkApi` 写回 | **无**:物化全在安装侧同步、上架侧零改动 | +| 跨 BC 读端口 | `MarketAssetSourceApi`(带回副本 dataset id + forkStatus,market 读自有 tags 即可) | **扩** `MarketAssetSourceApi` 带 config(market 域够不着 ai config→需反调 ai 或 ai 自读,决策点 D1) | +| 安装侧物化形态 | `materializeInstalledRefKb`:建 installed_ref KB + dataset binding 指副本(**副本仅 1 份共享**) | 照搬形态:建 installed 型 agent + active version(**config 各拷各的、无共享**) | +| 去污染/去裸引用 | kb_id 去污染(assetId→本地 kbId) | 槽位 agentId 去裸引用(发布者 agentId→本地 installed agentId) | +| 唯一断点 | 检索第四门 `selectActiveDatasetByKbId`(数据正确即通,代码几乎不改) | 运行时授权门 `requireVisibleAgent:329-331`(**必须改门放行 installed**,trust boundary) | +| 改授权门 | 不改(检索门靠数据命中) | **改**(A-runtime,本 plan 最需严防、负路测命门) | +| schema 触动 | 无(落 V5/V14 已有列) | 仅补 `MuseAgentDO` 映射 V27 已有列(不需新迁移) | +| 外部资源回滚 | 有:删 fork 出的副本 dataset | **无**:软删本地 agent 行即可 | +| 共同开放项 | 召回回滚未触达 installed_ref;e2e fixture 待补 | 召回回滚未触达 installed agent(同面);e2e fixture 待对齐 | + +--- + +## 10. 关联阅读 + +- 方案演进与权衡(本 plan 的 WHAT 上游,agent 侧 B'):[`临时-02-market-install下游物化方案.md`](临时-02-market-install下游物化方案.md)(§1.3.2 断点② 机理、§2.2 B' 智能体侧、§3.2 智能体 schema 增量) +- 姊妹方向 KB 物化(已闭环,本 plan 照搬其物化范式与开放项结构):[`临时-04-market-KB物化-D0fork执行plan.md`](临时-04-market-KB物化-D0fork执行plan.md) +- 概念与 owner 边界:[`架构-02-核心数据结构与双轨模型.md`](架构-02-核心数据结构与双轨模型.md)(§5 智能体运行权限、§6 市场资产、§11.2 绑定安装授权、授权快照语义) +- 关键决策:`架构-03-关键决策与原则(ADR).md`(ADR-017 Source 传播事件驱动、ADR-020 授权快照字符串承载) +- 表结构 SSOT:`后端-04-统一数据库Schema-v1.md`(§7 AI/Agent,`muse_agent.agent_type` 取值待 A-naming 收口) +- BC 边界规则:[`.agents/rules/bc-boundaries.md`](../.agents/rules/bc-boundaries.md) +- 已沉淀知识:[`.agents/knowledge/market-install-downstream-materialization.md`](../.agents/knowledge/market-install-downstream-materialization.md)(KB 物化 + D0-fork 隔离单一归属,agent 断点在"二、agent 断点"已点名留作后续) diff --git a/docs/mvp/1.0.0-交付计划.md b/docs/mvp/1.0.0-交付计划.md new file mode 100644 index 00000000..9b976039 --- /dev/null +++ b/docs/mvp/1.0.0-交付计划.md @@ -0,0 +1,149 @@ +# Muse 1.0.0 交付计划与现状对账 + +> 版本:v1.0(2026-06-26 首版) +> 目标读者:人类决策者(基准/优先级/发布决策)+ 执行 agent(按 Epic 细分执行) +> 边界:本文是 **1.0.0 交付的单一事实源**——汇总 8 域"设计→实现→验证"对账、定义 1.0.0 范围与执行计划。它**不重复定义产品/架构**(那是 design-docs SSOT),只承载"做到哪、还差什么、按什么顺序补、什么算真做完"。 +> 由来:2026-06-26 用 codex 多实例 + claude 深盘对 8 域做的交付盘点(原始对账存盘点产物,关键结论已固化于本文与各模块 `.agent`)。 + +--- + +## 1. 总体结论与基准 + +**Muse 不是"开发混乱",也不是"实现与设计各跑各的"。** 设计 SSOT 成熟(33 分册)、后端骨架扎实、有真 PG/RAGFlow IT。真问题是:**实现滞后于设计 + 验证假绿掩盖了大量"装了不能用"的断层**,导致完成度被系统性高估、反复踩坑。 + +- "实现与设计脱节"有实锤但属**局部**(横切 Schema、双轨 block、API 契约 header);主体是**实现追赶设计**。 +- "开发太长导致混乱"不成立;真相是**状态漂移**——进度散在总账/memory/临时文档/git log/各模块 `.agent`,缺统一对账。本文即补上这个对账。 + +**整体完成度(按"可闭环交付"严判):约 55-65%。** 后端读侧/骨架强,前端薄、写侧/消费侧/验证侧弱。 + +**交付基准(人类已定):线 A —— 1.0.0 最小可用子集。** 先把一条核心创作闭环做到真可上线,pay/report/bpm/mp 等后置。 + +--- + +## 2. 八域完成度对账矩阵 + +| 域 | 完成度* | 最强(已真闭环) | 最痛(阻断点) | 盘点方 | +|---|---|---|---|---| +| market | ~80% | 前台主干真IT+e2e双证、KB物化D0-fork | agent物化未做、召回不触达已安装(合规风险) | claude深盘 | +| content | ~65% | 写作/自动保存/AI采纳真PG+e2e | Accept不归档、工作台缺多入口、版本历史无 | codex | +| knowledge | ~65% | KB物化、enable/disable真PG | Studio薄、Source只发不消费、no_dataset | codex | +| ai | ~60% | 生成/SSE/采纳/槽位真e2e | Agent CRUD/版本/解绑缺、拒绝不打后端、评测断 | codex | +| admin | ~55% | 市场治理真IT、e2e真连PG行反查 | 权限治理P0双断层、AI配置前端全占位 | claude深盘 | +| member | ~50% | account读侧真IT扎实 | 写侧/扣减未闭环、用量无写源、pay禁用 | claude深盘 | +| meta | ~45% | planning垂直切片真e2e | 字段值校验未落、新增schema不通、API无真IT | codex | +| 横切 | ~40% | infra文件真字节往返 | 统一Schema漂移、report/bpm/mp禁用、传播worker未落 | codex | + +*完成度为相对判断(供排序与定基准),非精确度量。 + +--- + +## 3. 五类系统性病灶(根因——收敛抓手) + +问题虽多,收敛为 5 类根因,逐类清比逐 bug 打高效: + +1. **"能配置/能展示"≠"可闭环"**(最普遍)。三类断层:①前端壳+后端缺(权限治理三件套)②前端壳+后端齐的 **wiring gap**(AI配置9按钮占位、reject仅清本地)③后端齐+前端零的**孤儿端点**(content治理、三类信息面板)。 + +2. **验证假绿系统化**(最危险,反复踩坑之源): + - 🔴 整个 P1r IT 套件**不在 CI 跑**(CI=`mvn package` 无 failsafe、本分支从未真跑),靠 2026-06 人工批准记录背书。 + - 多个 `P1r*RealApiGateTest` 是**"状态台账门禁"非真跑**(member/meta/admin)。 + - live IT 默认 opt-in **跳过**(ai/knowledge/market KB fork)。 + - 大量 **mock-green** 单测冒充验证。 + - → "51 真 IT"含金量需**大幅打折**。 + +3. **读写不对称**:读/展示完整、写/消费/扣减空——用量无写源、配额 Guard 孤儿、Source/召回只发事件无消费者、传播 worker 未落。 + +4. **设计漂移(真脱节,局部)**:横切统一 Source/Auth/Audit/Outbox Schema 未落地(用分域替代表)、content 双轨 block 结构化字段成孤儿、API 契约 header 语义与实现相反。 + +5. **模块禁用**:report/bpm/mp/pay 在 pom 注释禁用(部分设计后置、部分待启用)。 + +--- + +## 4. 1.0.0 范围(线 A) + +**核心闭环(必须真可上线)**:注册登录 → 建作品 → 写作/自动保存 → AI 生成 → 采纳(归档)→ 知识库(建/绑/检索)→ 市场(发现/安装/用)。 + +**1.0.0 做**:见第 5 节 E0–E7。 +**1.0.0 不做(后置线 B)**:pay/自助支付(用 admin 人工开通会员)、report 报表大屏、bpm 工作流、mp 公众号、安全写操作(改密/2FA/会话)、撤权独立入口、高级版本 diff、统一横切 Schema 重构、Source 全自治传播、可视化 schema 编辑器、偏好与通知页。 + +--- + +## 5. 执行计划(Epic 分解 + 依赖 + 反假绿验证标准) + +> 顺序原则:**E0 消假绿地基先行**(否则后续修复都无 CI 守护);E1–E3 核心创作闭环优先;E4 依赖 E2(agent 物化需 ai 运行时门);E5–E7 支撑。 +> 每个 Epic 的"✔ done"是**反假绿验证标准**——必须真测试(真 PG/真 e2e)通过、非 mock-green、非台账门禁。 + +### E0 · 消假绿地基(先行,全局守护) +- T0.1 根 pom 引入 failsafe,P1r\*IT 纳入可执行验证(真起栈或分层标注外部依赖)。 +- T0.2 `P1r*RealApiGateTest` 台账门禁:改真跑,或显式标注"非真验证"且不计入交付绿证。 +- T0.3 live opt-in IT:跳过时显式标 SKIPPED(非 PASSED),交付清单区分"真验证/仅mock/未验"。 +- ✔ done:CI 能拦截 install/fork/治理/采纳链破坏;产出一份**去水分的"真验证"清单**作为各 Epic done 的判据基线。 + +### E1 · content 创作闭环补齐 +- T1.1 Accept Suggestion 归档:AI owner 写 accepted decision,P1r IT 扩到校验 suggestion status 与 decision archive。 +- T1.2 修改后合并前端入口(finalContent 编辑提交)+ 旧知识草稿失效。 +- T1.3 工作台壳补知识/导入/导出/记录最小入口(后端多已就绪,补前端入口)。 +- T1.4 版本历史最小 API/UI + IndexedDB `workId+blockId+revision` 对账。 +- ✔ done:写作→AI→采纳(候选离开 Active)→工作台各入口可用,真 e2e 覆盖。 + +### E2 · ai 智能体生命周期与闭环 +- T2.1 Agent CRUD 补齐(update/delete)。 +- T2.2 Agent 版本读/切换/归档。 +- T2.3 Slot 解绑(unbind)。 +- T2.4 候选拒绝接后端(studio reject 打后端)+ 真 e2e。 +- T2.5 候选列表真 e2e;质量评测执行器落地,或明确降级出 1.0.0 范围。 +- ✔ done:agent 全生命周期可管;拒绝/候选闭环真 e2e。 + +### E3 · knowledge studio 与检索 +- T3.1 Studio 检索触发 + 绑定/上传主流程 e2e。 +- T3.2 `no_dataset` 用户可见错误与治理入口(非静默省略)。 +- T3.3 Source 传播至少一条真实跨模块消费验收。 +- ✔ done:studio 知识闭环真 e2e;检索可被用户触发、缺 dataset 有可见反馈。 + +### E4 · market agent 物化 + 召回触达 +- T4.0 先拍板临时-05 决策 D1(config 跨 BC 读取途径:market 反调 ai 读端口 vs ai 自有读端口)。 +- T4.1 agent 物化(临时-05 五单元):**槽位去裸引用 + 运行时门加 installed 分支必须同改**(并列双断点)。 +- T4.2 召回触达已物化 KB(合规底线):解锁召回事件结构 + knowledge 召回消费者 + `projection.status` 翻 recalled + 检索门生效,真 IT 验存量安装者召回后停检索。 +- T4.3 三类可信信息面板前端接入(后端 openapi 已就绪,纯前端)。 +- ✔ done:安装 agent 可在作品里跑 AI(不再 SCOPE_FORBIDDEN);召回的 KB 存量安装者检索被停;信息面板渲染。 + +### E5 · member 写侧闭环 +- T5.1 用量写路径接线(AI 生成消耗写 `muse_member_usage_record`,或确认 New-API 外部喂数真链路)。 +- T5.2 配额扣减闭环 + `AccountQuotaGuard` 接入生成链路 + entitlement 写审计真 PG IT(闭 ADR 假绿)。 +- T5.3 `NewApiAccountFacade` Real 实现(New-API 已在 mini-infra 在线)。 +- T5.4 配额请求/归因 job 的 consumer(推进 queued→completed)。 +- ✔ done:用量真有数、配额真扣、绑定生产可用、entitlement 写有真 IT。 + +### E6 · admin 权限治理 + AI 配置接线 +- T6.1 用户角色权限治理三件套后端端点 + 前端接线(封禁/授予撤销角色/权限组绑定,含自提权/自审批防护)。 +- T6.2 权限组与菜单页面权限 MVP(可复用 Yudao,但后端须校验页面/操作/数据范围)。 +- T6.3 AI 配置/质量门控前端接线(僵尸 api 函数接上 + 去硬编码假数据)——消最大假绿面、成本低。 +- T6.4 market restore 分支真 PG IT。 +- ✔ done:管理员能通过 Muse 管理端授权/配权限组;AI 配置真可写、无假数据。 + +### E7 · meta 字段校验 + schema +- T7.1 后端动态字段值校验(required/min/max/regex/enum/类型/废弃字段/版本 stale 错误码)落在可信边界。 +- T7.2 打通新增 schema 根对象,或限制 admin "新建草稿"入口。 +- T7.3 meta admin API 真 PG IT(草稿→校验→预览→发布→激活→回滚)+ admin 写动作真 e2e。 +- ✔ done:动态表单值校验后端生效;schema 可新建;有真 IT/真 e2e。 + +--- + +## 6. 验证准则(反假绿——什么算"真 done") + +1. **真测试优先**:done 以真 PG IT / 真后端 e2e 为准,不接受 mock-green、台账门禁、默认跳过的 live。 +2. **CI 守护**:E0 后,关键链路的 P1r IT 必须能在 CI(或可一键复跑的流程)拦截回归。 +3. **闭环验证**:验"用户可达的完整链路"(前端动作→后端→DB→反查),不止单点单测。 +4. **独立复核**:agent 报"已修/通过"必附运行证据;主 agent 独立复跑关键结论,不盲信子 agent。 +5. **凭据红线**:测试/日志/产物不得泄露 password/secret/token/sk-;IT 用 `_test` 库,绝不指向 muse_slice_live。 + +--- + +## 7. 收尾动作(执行前/中同步) + +- **临时文档收敛**:临时-01~05 是过程文档(SSE 方案、KB 物化过程、agent 物化 plan 等),关键结论已并入本文/各模块 `.agent`/memory;按 AGENTS.md 归档或删除,不再作为活跃跟踪源。临时-05 的 agent 物化方案并入 E4(仍正确、D1 待拍板)。 +- **进度总账对齐**:`docs/mvp/进度总账.md` 与本文去重——本文为 1.0.0 待做计划的单一事实源,总账保留为历史进度账并加指针指向本文。 +- **执行跟踪**:按 E0–E7 建可独立验证的子任务,逐个"目标→最小改动→真验证→回写"。 + +--- + +*本文随执行推进更新:每个 Epic/子任务完成后回写其真验证状态,保持"设计→实现→验证"对账不漂移。* diff --git a/docs/mvp/进度总账.md b/docs/mvp/进度总账.md index 3830f54d..d4aeffc3 100644 --- a/docs/mvp/进度总账.md +++ b/docs/mvp/进度总账.md @@ -2,6 +2,7 @@ > **本文件是项目进度的唯一 SSOT**。进度更新只进**本文件 + 各模块 `.agent`**;**不再新增“状态推进 / 收口 / completedApproval”过程文档**(过程文档 churn 是失控根因之一,见 [对抗复盘](../agent-specs/2026-06-13-目标达成对抗复盘.md))。 > **接口覆盖门机械唯一源** = [覆盖 JSON `p1r-api-coverage.json`](../superpowers/reports/p1r-api-coverage.json)(经 `P1rApiCoverageReportTest` 校验 summary 自算 + testFiles 证据),当前 **233/233 completed / 0 needs_verification**;⚠️ 该口径 = 接口门,**≠ 端到端可用**(整体 ~76%,见 §二)。 +> **🎯 1.0.0 交付计划(可闭环严判 + 待做路线、单一执行源)** = [1.0.0-交付计划.md](1.0.0-交付计划.md)(2026-06-26 八域交付盘点)。本总账记**接口门/基建进度**;**1.0.0 可闭环完成度严判(~55-65%)、五类系统性病灶、E0–E7 待做 Epic 以交付计划为准**。盘点实证口径差:覆盖 JSON 的 completed = 接口存在 + 台账标记、**≠ 真闭环可用**——多个 `P1r*RealApiGateTest` 是台账门禁(非真跑)、整套 P1r IT 不在 CI 跑(靠人工批准记录)、前端/写侧大量"装了不能用"。 > 模块现状细节见 [现状基线 spec](../agent-specs/2026-06-13-项目目标与模块现状基线.md)(**2026-06-13 历史快照**,文内 147/86 为当时值,以本文件 + 覆盖 JSON 为准);人读全局见 [项目功能与进度总览](../项目功能与进度总览.md)。 ---