oh-my-muse/design-docs/临时-04-market-KB物化-D0fork执行plan.html
lili c12eb013cd docs(market): D0-fork KB 物化执行 plan(临时-04、supersede 临时-03)
D1 澄清坐实真越权=安装者读到发布者私有内容(非安装者间):market 对 knowledge/ragflow 零依赖(src+pom 双空 spot-check)→上架不 fork、安装者共享发布者活 dataset、上架后 KB 仍可加私有文档(上传无 market 门禁)。用户拍 D0-fork(发布侧 fork 纯公开副本、物理隔离)。

临时-04 四单元:U-fork(发布侧 fork 公开副本核心新工程:上架事件→knowledge 异步 createDataset+逐文档 uploadDocuments+重索引,RAGFlow 无 copy API 必重走三步 spot-check 确认)/U-materialize(安装侧物化去隔离、binding 指公开副本)/U-retrieve(检索去隔离)/U-verify(发布者私有不泄露、复用临时-03 U0 反向基线)。关键推荐:fork 时机=上架 markListed(副本=公开快照语义)、接线=事件驱动(复用 market EventPublishOutbox+ADR-017、不绑架审核事务、保 market 不反依赖 knowledge)、DDL=初版不需(V5/V14 列+JSONB)。临时-03 标 superseded 保留演进史、U0 证据+四硬事实继承。待拍 D4-D10 后执行。

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

292 lines
19 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>临时-04 · Market KB 物化 D0-fork 执行 Plan人读图</title>
<style>
:root{
--bg:#0f1419; --panel:#1a2230; --ink:#e6edf3; --muted:#9fb0c3;
--pub:#c0392b; --pubbg:#2a1a1c; --ins:#27ae60; --insbg:#16271c;
--fork:#2ecc71; --warn:#b8860b; --warnbg:#2a2410; --line:#33415a;
--accent:#4aa3ff; --accentbg:#13243a;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
line-height:1.6;padding:32px 20px 64px}
.wrap{max-width:1180px;margin:0 auto}
h1{font-size:26px;margin:0 0 4px;letter-spacing:.3px}
.sub{color:var(--muted);font-size:14px;margin-bottom:6px}
.oneliner{background:var(--accentbg);border:1px solid var(--accent);border-radius:10px;
padding:14px 18px;margin:18px 0 26px;font-size:15px}
.oneliner b{color:var(--accent)}
h2{font-size:18px;margin:34px 0 14px;padding-left:10px;border-left:4px solid var(--accent)}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-bottom:18px}
/* 物理隔离主图 */
.iso{display:grid;grid-template-columns:1fr 56px 1fr;gap:0;align-items:stretch;margin:8px 0}
.col{border-radius:12px;padding:16px}
.col.pub{background:var(--pubbg);border:1px solid var(--pub)}
.col.ins{background:var(--insbg);border:1px solid var(--ins)}
.col h3{margin:0 0 12px;font-size:15px}
.col.pub h3{color:#ff8a7a}
.col.ins h3{color:#6fe0a0}
.box{background:#0d141d;border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:8px 0;font-size:13px}
.box .t{font-weight:600;margin-bottom:3px}
.box .d{color:var(--muted);font-size:12px}
.box.live{border-color:var(--pub)}
.box.fork{border-color:var(--fork);box-shadow:0 0 0 1px rgba(46,204,113,.25) inset}
.box.fork .t{color:#6fe0a0}
.arrowcol{display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative}
.bigarrow{font-size:30px;color:var(--fork);font-weight:700}
.arrowlbl{font-size:11px;color:var(--muted);text-align:center;margin-top:4px;max-width:54px}
.copyhint{font-size:11px;color:var(--fork);text-align:center;margin-top:8px;border-top:1px dashed var(--fork);padding-top:8px}
.verdict{margin-top:14px;background:var(--insbg);border:1px solid var(--ins);border-radius:8px;padding:12px 14px;font-size:13.5px}
.verdict b{color:#6fe0a0}
/* 越权 before/after */
.ba{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.ba .bad{background:var(--pubbg);border:1px solid var(--pub);border-radius:10px;padding:14px}
.ba .good{background:var(--insbg);border:1px solid var(--ins);border-radius:10px;padding:14px}
.ba h4{margin:0 0 8px;font-size:14px}
.ba .bad h4{color:#ff8a7a}
.ba .good h4{color:#6fe0a0}
.ba ul{margin:6px 0 0;padding-left:18px;font-size:13px;color:var(--muted)}
.ba li{margin:4px 0}
.tag{display:inline-block;font-size:11px;padding:1px 7px;border-radius:10px;margin-left:6px;vertical-align:middle}
.tag.new{background:#3a1418;color:#ff8a7a;border:1px solid var(--pub)}
.tag.reuse{background:#16271c;color:#6fe0a0;border:1px solid var(--ins)}
/* 实现单元流 */
.units{display:flex;gap:0;align-items:stretch;flex-wrap:wrap}
.unit{flex:1;min-width:200px;background:#0d141d;border:1px solid var(--line);border-radius:10px;padding:14px;position:relative}
.unit.core{border-color:var(--pub);background:var(--pubbg)}
.unit h4{margin:0 0 6px;font-size:14px}
.unit.core h4{color:#ff8a7a}
.unit .one{font-size:12.5px;color:var(--muted)}
.ufarrow{display:flex;align-items:center;font-size:22px;color:var(--accent);padding:0 6px}
/* 时序简版 */
.seq{font-size:13px}
.seq .step{display:flex;gap:10px;margin:7px 0;align-items:flex-start}
.seq .n{flex:none;width:22px;height:22px;border-radius:50%;background:var(--accentbg);border:1px solid var(--accent);
color:var(--accent);font-size:12px;display:flex;align-items:center;justify-content:center;font-weight:700;margin-top:1px}
.seq .who{flex:none;width:130px;color:var(--muted);font-size:12px}
.seq .what{flex:1}
.seq .async{color:var(--warn);font-size:11px}
/* 决策表 */
table{width:100%;border-collapse:collapse;font-size:13px;margin-top:6px}
th,td{border:1px solid var(--line);padding:8px 10px;text-align:left;vertical-align:top}
th{background:#0d141d;color:var(--muted);font-weight:600}
.rec{color:#6fe0a0;font-weight:600}
/* 通用 */
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.note{font-size:12px;color:var(--muted);margin-top:6px}
.pill{display:inline-block;background:var(--warnbg);border:1px solid var(--warn);color:#e0c060;
font-size:11px;padding:1px 8px;border-radius:10px}
.foot{color:var(--muted);font-size:12px;margin-top:30px;border-top:1px solid var(--line);padding-top:14px}
@media (max-width:820px){.iso{grid-template-columns:1fr}.arrowcol{flex-direction:row;padding:8px 0}
.ba,.grid2{grid-template-columns:1fr}.units{flex-direction:column}.ufarrow{transform:rotate(90deg);padding:6px 0}}
</style>
</head>
<body>
<div class="wrap">
<h1>临时-04 · Market KB 物化 <span style="color:var(--fork)">D0-fork</span> 执行 Plan</h1>
<div class="sub">发布侧 fork 纯公开副本 · v1执行版待人类 review 后执行,本稿只读出文档)· 2026-06-26</div>
<div class="sub">配套正文:<code>临时-04-market-KB物化-D0fork执行plan.md</code> 上一版 <code>临时-03</code>(共享+运行时隔离)已被本稿 supersede</div>
<div class="oneliner">
<b>一句话:</b>知识库上架时,在<b>发布侧</b> fork 一份<b>只含公开文档的专用 dataset</b>"公开副本"),所有安装者只读共享这一份。
<b>物理上</b>把发布者私有内容隔在副本之外,从根消除"安装者读到发布者私有内容"的越权——
<b>不需要运行时 chunk 隔离、不依赖那个 metadata_filter 字段 bug</b>。代价是新增一块发布侧 fork 工程(建副本+复制文档+重索引,异步重活)+ 打破 market→knowledge 零依赖接线。
</div>
<!-- ============ 越权真因 before/after ============ -->
<h2>越权解法:从"私有泄露"到"物理不可见"</h2>
<div class="card">
<div class="ba">
<div class="bad">
<h4>✗ 现状 / 共享活 dataset会越权</h4>
<ul>
<li>market 对 knowledge/ragflow <b>零依赖</b>:上架审核只翻 market 自有资产状态,<b>从不 fork</b> 任何 dataset</li>
<li>安装者绑定后指向<b>发布者那一份活 dataset</b></li>
<li>发布者<b>上架后仍可继续加私有文档</b>whole-KB 上架、KB 不冻结)</li>
<li>检索链对返回 chunk <b>无任何二次过滤</b>dataset 进检索即<b>整库返回</b> → 安装者读到上架时不存在、发布者事后才加的私有内容</li>
<li class="note">租户拦截器拦不住:两边 binding 指向同一个 RAGFlow dataset_id</li>
</ul>
</div>
<div class="good">
<h4>✓ D0-fork物理隔离</h4>
<ul>
<li>上架时 fork 出<b>只含上架时刻公开快照</b>的副本 dataset</li>
<li>安装者只读<b>副本</b>,发布者私有/上架后新增文档<b>物理不在副本内</b></li>
<li>"整库返回"在这里<b>是安全的</b>——整库 = 纯公开</li>
<li>无需运行时 chunk 隔离、无需 metadata 过滤</li>
<li class="note">副本仅 1 份N 个安装者共享只读</li>
</ul>
</div>
</div>
<div class="note" style="margin-top:12px">
与临时-03 的差异:临时-03 把越权理解为"安装者<b>之间</b>互读(多租户共享 dataset",方向是"共享前补 chunk 级隔离"
D1 澄清坐实真越权是<b>安装者读到发布者私有内容</b>故改走物理隔离fork 公开副本)。
临时-03 的 U0 spike 证据("整库无 chunk 过滤")仍有效,本稿转作 U-verify 的反向基线(证"私有不泄露")。
</div>
</div>
<!-- ============ 物理隔离主图 ============ -->
<h2>物理隔离:发布侧 vs 安装侧</h2>
<div class="card">
<div class="iso">
<div class="col pub">
<h3>发布者域owner = 发布者)</h3>
<div class="box live">
<div class="t">发布者活 dataset <code>kb-{发布者kbId}-v{ver}</code></div>
<div class="d">含公开文档 + 上架后新增的私有文档(持续变化)</div>
</div>
<div class="box fork">
<div class="t">⭐ 公开副本 dataset <code>fork-asset-{assetId}-public</code></div>
<div class="d">只含<b>上架时刻</b>的公开快照文档fork 后不随活 dataset 变)</div>
</div>
<div class="copyhint">↑ U-fork上架时逐文档复制内容 + 重索引(异步重活)</div>
</div>
<div class="arrowcol">
<div class="bigarrow"></div>
<div class="arrowlbl">N 安装者<br>共享只读<br>副本仅 1 份</div>
</div>
<div class="col ins">
<h3>安装者域owner = 各安装者)</h3>
<div class="box">
<div class="t">installed_ref kb 行(每安装者一行)</div>
<div class="d">kb_type=installed_ref · source_market_asset_id=assetId · license_snapshot_id=授权快照</div>
</div>
<div class="box">
<div class="t">ragflow_binding去污染后</div>
<div class="d">kb_id=<b>本地 installed_ref 主键</b>(不再是 assetId· ragflow_dataset_id=<b>公开副本</b></div>
</div>
<div class="box" style="border-color:var(--ins)">
<div class="t" style="color:#6fe0a0">检索 selectActiveDatasetByKbId → 公开副本</div>
<div class="d">RAGFlow 整库返回 = 🟢 纯公开,发布者私有物理不可见</div>
</div>
</div>
</div>
<div class="verdict">
<b>越权根除点:</b>安装者 binding 指向的 dataset_id 是<b>公开副本</b>而非发布者活 dataset。
即使检索链"整库返回 + 无二次过滤"的事实不变,整库里也只有上架时刻的公开内容——发布者私有从源头不在这份 dataset 里。
</div>
</div>
<!-- ============ 四个实现单元 ============ -->
<h2>四个实现单元(各一句话)</h2>
<div class="card">
<div class="units">
<div class="unit core">
<h4>U-fork <span class="tag new">全新·最重</span></h4>
<div class="one">上架事件触发 knowledge 异步 fork建副本 dataset + 逐文档复制 + 重索引 + 幂等/重试/部分成功whole-KB 公开集 = 上架时刻全部可检索文档。</div>
</div>
<div class="ufarrow"></div>
<div class="unit">
<h4>U-materialize</h4>
<div class="one">绑定路径建 installed_ref kb 行 + binding 指向<b>公开副本</b>kb_id 去污染assetId→本地主键+ 新 <code>MarketAssetSourceApi</code> 跨 BC 读带回副本 dataset + forkStatus + 幂等。</div>
</div>
<div class="ufarrow"></div>
<div class="unit">
<h4>U-retrieve</h4>
<div class="one">物化后第四门返非空→命中;<b>无运行时隔离</b>(副本纯公开);授权快照沿用 ADR-020 VARCHAR。代码几乎不改。</div>
</div>
<div class="ufarrow"></div>
<div class="unit">
<h4>U-verify</h4>
<div class="one">真 PG ITfork→安装→检索命中+ studio e2e + <b>发布者私有不泄露</b>(复用 U0 反向基线)+ 召回/下架回滚。</div>
</div>
</div>
<div class="note" style="margin-top:12px">
相对临时-03U-materialize/U-retrieve = 原 U2/U3 <b>简化去隔离</b>U-fork 是<b>全新</b>核心工程U0 spike 的 2 个 case <span class="tag reuse">复用</span> 作 U-verify 反向基线,不重写。
</div>
</div>
<!-- ============ fork 时序 ============ -->
<h2>fork 时序(上架触发 + 事件驱动 + 异步,含失败/重试/幂等)</h2>
<div class="card seq">
<div class="step"><div class="n">1</div><div class="who">管理员审核</div><div class="what">approvePublishRequest含 markListed</div></div>
<div class="step"><div class="n">2</div><div class="who">market 审核服务</div><div class="what">资产/版本 listedmarket 自有事实,同事务)</div></div>
<div class="step"><div class="n">3</div><div class="who">market 审核服务</div><div class="what"><b>上架事件</b> listed{assetId, 发布者kbId, version}(同事务)<span class="async"> · 不等 fork审核到此提交不被 fork 成败绑架</span></div></div>
<div class="step"><div class="n">4</div><div class="who">knowledge 消费者</div><div class="what"><span class="async">异步</span>消费上架事件 → forkPublicSnapshot(assetId, 发布者kbId, version)</div></div>
<div class="step"><div class="n">5</div><div class="who">knowledge fork</div><div class="what">幂等检查(该 assetId+version 已 fork命中跳过→ createDataset(副本)</div></div>
<div class="step"><div class="n">6</div><div class="who">knowledge fork</div><div class="what">对每个上架时刻公开文档uploadDocuments(副本) + startParseDocuments(副本重索引)</div></div>
<div class="step"><div class="n">7</div><div class="who">knowledge fork</div><div class="what">全成功 → forkStatus=ready + 记副本 datasetId供 MarketAssetSourceApi 带回)<br>部分失败/502 → forkStatus=partial/failed + 标可重试(不删已建部分、不重复传已成功文档)</div></div>
<div class="note" style="margin-top:10px">无现成 RAGFlow "copy dataset" API → fork 必须重走 createDataset + 逐文档 upload + startParse重索引。这是异步长任务重活是 D0-fork 相对临时-03 最大的新增成本。</div>
</div>
<!-- ============ 决策点 ============ -->
<h2>关键决策点(待人类拍板)</h2>
<div class="card">
<table>
<thead><tr><th style="width:120px">决策</th><th>选项</th><th style="width:300px">倾向 + 理由</th></tr></thead>
<tbody>
<tr>
<td><b>fork 时机</b></td>
<td>上架 markListed 时 fork 首次安装懒 fork</td>
<td><span class="rec">上架时 fork</span>:版本快照语义干净(副本=上架那刻公开集)+ 安装侧轻(无异步等待)+ 浪费可控;懒 fork 破坏快照语义 + 首装延迟 + 并发抢锁。</td>
</tr>
<tr>
<td><b>接线</b><br>(打破零依赖)</td>
<td>事件驱动 market 调 knowledge facade 同步</td>
<td><span class="rec">事件驱动</span>:审核不被 fork 成败绑架 + 契合 ADR-017 + 保 BC 边界market 不反向依赖 knowledgefacade 同步会让审核挂在 RAGFlow 多次调用上、失败回滚审核。</td>
</tr>
<tr>
<td>副本更新</td>
<td>重新上架才更新 不更新 实时跟随</td>
<td><span class="rec">重新上架才更新</span>:与"市场资产=发布快照"模型一致;实时跟随会重新引入私有泄露。</td>
</tr>
<tr>
<td>fork 未就绪<br>时安装</td>
<td>绑定阶段拒fail-closed 绑定但检索 no_dataset</td>
<td><span class="rec">绑定阶段拒</span>:避免"绑了用不了"迷惑态;实现时定夺回填。</td>
</tr>
<tr>
<td>召回/下架<br>回滚</td>
<td>复用现有传播 本期标开放项手动停用</td>
<td>现状 market→目标 owner 传播是 fail-closed blocked 记录式TARGET_OWNER_UNAVAILABLEknowledge 无消费入口 → <span class="rec">倾向本期标开放项</span>,不假装闭环。</td>
</tr>
<tr>
<td>DDL /<br>副本存放</td>
<td>复用 JSONB 不加列 新增列或表V32</td>
<td><span class="rec">复用现有结构 + JSONB 承载</span> fork 状态/副本映射 → <b>不需要新迁移</b>;仅评审坚持强类型列时才走 V32_test 库铁律)。</td>
</tr>
</tbody>
</table>
</div>
<!-- ============ 风险 + scope ============ -->
<h2>风险与边界(诚实呈现)</h2>
<div class="grid2">
<div class="card">
<h4 style="margin-top:0;color:#ff8a7a">主要风险</h4>
<ul style="font-size:13px;color:var(--muted);padding-left:18px;margin:6px 0">
<li><b style="color:var(--ink)">R1 私有泄露</b>fork 公开集界定错 / 副本"实时跟随"→私有重新泄露。缓解:严格按上架时刻 active 文档 + U-verify 真验私有不泄露。</li>
<li><b style="color:var(--ink)">R2 fork 异步重活复杂度</b>(不掩盖):建 dataset+N 文档逐个 upload+重索引,失败/重试/幂等/部分成功——<b>比临时-03"共享+运行时隔离"重</b>。诚实:安全模型更干净(物理隔离),但发布侧工程更重。</li>
<li><b style="color:var(--ink)">R3 打破 market→knowledge 零依赖</b>:影响面集中在"上架落事件"+knowledge fork 消费链,不改 market 审核主体;需架构 reviewADR-017 为据)。</li>
<li><b style="color:var(--ink)">R6 metadata_filter 字段 bug</b> <span class="pill">潜伏·留待修</span>:发的 metadata_filter 被 RAGFlow 静默忽略(应为 metadata_condition。D0-fork 不依赖它,降级潜伏 bug 单记。</li>
</ul>
</div>
<div class="card">
<h4 style="margin-top:0;color:#6fe0a0">本期不做Scope</h4>
<ul style="font-size:13px;color:var(--muted);padding-left:18px;margin:6px 0">
<li>agent 物化(下一阶段,不碰 ai 模块)</li>
<li>install 侧物化(维持只记账 targetFactsWritten=false</li>
<li>运行时 chunk 隔离D0-fork 用物理隔离替代)</li>
<li>metadata_filter 字段 bug 修复(潜伏单记)</li>
<li>副本资源回收(下架/无安装清理副本,开放)</li>
<li>召回/下架回滚自动化(开放项,不假装闭环)</li>
<li>market 两列 BIGINT 契约议题</li>
</ul>
<div class="note">DDL 判定:<b>初版不需要新 Flyway 迁移</b>installed_ref 落 V5/V14 已有列 + fork 状态走 JSONB仅评审坚持强类型列才走 V32。</div>
</div>
</div>
<div class="foot">
owner 边界引 临时-02 / 临时-03 / 架构-02§6 市场资产·§11.2 绑定授权·授权快照)/ 专题-03§5 检索合同)/ ADR-017Source 传播事件驱动)/ ADR-020授权快照字符串承载/ .agents/rules/bc-boundaries.md本稿不重定义概念。
</div>
</div>
</body>
</html>