docs(agent-specs): S7 生成主链全文通路(分支①)评审版 v0.2

现状经三路测绘实测校正:完整正文只活在 real client success() 里、构造结果前即被截成
60/80 字摘要丢弃(唯一捕获点);SSE 严格只重放已持久化事件、当前只发无正文的 done;
"三审"审的也是摘要;Dify 完成原因硬编码常量、截断护栏无效。

设计=一条"瞬态全文"通路:捕获处对完整正文做护栏→写瞬态加密缓存(复用 MuseAiRuntimePayloadStore
范式,TTL+决定即清)→事件表发 chunk 引用行、SSE 重放回缓存取全文填已声明的 chunk.data.content→
前端 streamContent 已接线零改动→采纳恒回传完整正文(所见即所写)写 Canonical。候选/正文表永不含
完整正文。含 mermaid 数据流 + svg/html 速览 + 现状锚点附录。三个材料级点待所有者裁决。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-07 23:16:05 -07:00
parent aadc0aa8ab
commit 2973e8da66
2 changed files with 344 additions and 0 deletions

View File

@ -0,0 +1,141 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>S7 生成主链全文通路(分支①)· 评审版一览</title>
<style>
:root{
--ink:#1a2233; --muted:#5b6678; --line:#dfe4ec; --bg:#f6f8fb; --card:#fff;
--durable-bg:#e8f0ff; --durable-bd:#3358cc; --durable-ink:#0b2a6b;
--trans-bg:#fff4e0; --trans-bd:#cc7a00; --trans-ink:#6b3d00;
--guard-bg:#e9f9ec; --guard-bd:#2f9e44; --guard-ink:#12521f;
--bad-bg:#fdecec; --bad-bd:#d64545; --bad-ink:#7a1c1c;
}
*{box-sizing:border-box}
body{margin:0;font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:var(--ink);background:var(--bg)}
.wrap{max-width:1080px;margin:0 auto;padding:28px 22px 60px}
header h1{font-size:23px;margin:0 0 4px}
header .sub{color:var(--muted);font-size:14px}
header .meta{color:var(--muted);font-size:12.5px;margin-top:6px}
h2{font-size:16px;margin:34px 0 12px;padding-bottom:6px;border-bottom:2px solid var(--line)}
.intent{background:linear-gradient(90deg,#eef3ff,#f6f8fb);border:1px solid var(--durable-bd);border-radius:12px;padding:14px 18px;margin-top:16px;color:var(--durable-ink);font-weight:600}
.grid{display:grid;gap:12px}
.cols-3{grid-template-columns:repeat(3,1fr)}
.cols-4{grid-template-columns:repeat(4,1fr)}
@media(max-width:820px){.cols-3,.cols-4{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--line);border-radius:11px;padding:13px 15px}
.bad{background:var(--bad-bg);border-color:var(--bad-bd)}
.bad .k{color:var(--bad-ink)}
.k{font-weight:700;font-size:13.5px;margin-bottom:4px}
.card p{margin:0;color:var(--muted);font-size:13px}
/* pipeline */
.flow{display:flex;flex-wrap:wrap;align-items:stretch;gap:8px;margin-top:6px}
.node{flex:1 1 150px;border-radius:10px;padding:10px 12px;font-size:12.8px;border:1.5px solid}
.node .t{font-weight:700;font-size:13px;margin-bottom:3px}
.node small{color:inherit;opacity:.85}
.durable{background:var(--durable-bg);border-color:var(--durable-bd);color:var(--durable-ink)}
.transient{background:var(--trans-bg);border-color:var(--trans-bd);color:var(--trans-ink)}
.guard{background:var(--guard-bg);border-color:var(--guard-bd);color:var(--guard-ink)}
.arrow{align-self:center;color:var(--muted);font-weight:700;padding:0 2px}
.legend{display:flex;gap:16px;flex-wrap:wrap;margin:10px 0 2px;font-size:12.5px;color:var(--muted)}
.chip{display:inline-flex;align-items:center;gap:6px}
.dot{width:11px;height:11px;border-radius:3px;display:inline-block;border:1.5px solid}
.dot.d{background:var(--durable-bg);border-color:var(--durable-bd)}
.dot.t{background:var(--trans-bg);border-color:var(--trans-bd)}
.dot.g{background:var(--guard-bg);border-color:var(--guard-bd)}
.redline{margin-top:14px;background:#fff;border:1.5px dashed var(--bad-bd);border-radius:11px;padding:13px 16px}
.redline .k{color:var(--bad-ink)}
table{width:100%;border-collapse:collapse;margin-top:8px;font-size:13px}
th,td{border:1px solid var(--line);padding:8px 10px;text-align:left;vertical-align:top}
th{background:#eef2f8;font-size:12.5px}
td.now{color:var(--bad-ink)} td.goal{color:var(--guard-ink)}
.decide{counter-reset:d}
.decide .card{position:relative;padding-left:40px}
.decide .card:before{counter-increment:d;content:counter(d);position:absolute;left:12px;top:12px;width:20px;height:20px;border-radius:50%;background:var(--durable-bd);color:#fff;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center}
.rec{display:inline-block;margin-top:6px;font-size:12px;font-weight:700;color:var(--guard-ink);background:var(--guard-bg);border:1px solid var(--guard-bd);border-radius:6px;padding:1px 7px}
.steps li{margin-bottom:6px}
.steps b{color:var(--durable-ink)}
.dep{background:#fff8ec;border:1px solid var(--trans-bd);border-radius:10px;padding:11px 15px;color:var(--trans-ink);font-size:13px;margin-top:8px}
footer{margin-top:34px;color:var(--muted);font-size:12px;border-top:1px solid var(--line);padding-top:12px}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>S7 生成主链全文通路(分支①)· 评审版一览</h1>
<div class="sub">让用户看到、审定、采纳 AI 的<strong>完整正文</strong>,同时守住「原始产出不落长期库、先审后入」主权。</div>
<div class="meta">v0.2 · 2026-07-07 · 现状已实测校正 · 待两轮评审转执行版 · S7.0 已裁分支①</div>
</header>
<div class="intent">核心把「事件顺序持久」与「正文载荷瞬态」分层——完整正文只走内存瞬态加密缓存SSE客户端永不进候选表/正文表。</div>
<h2>① 问题AI 的全文,从未到过用户面前</h2>
<div class="grid cols-3">
<div class="card bad"><div class="k">全文生成即丢</div><p>完整正文只在 real client 的 success() 里存在一瞬,构造结果前就被截成 60/80 字摘要,出栈丢弃;下游只见摘要。</p></div>
<div class="card bad"><div class="k">SSE 不带正文</div><p>同步生成,只发一条 done候选 id / 完成原因 / 质量分chunk 契约与前端渲染早就位,但后端从不产生 chunk 行。</p></div>
<div class="card bad"><div class="k">「审」审的是摘要</div><p>合规扫描 / 静态检查作用在 60/80 字摘要上截断护栏无效Dify 完成原因是常量、长度检查跑摘要)。</p></div>
</div>
<h2>② 目标通路:一条「瞬态全文」链</h2>
<div class="legend">
<span class="chip"><span class="dot d"></span>长期数据(主权红线内:只存摘要+三审 / 回传正文)</span>
<span class="chip"><span class="dot t"></span>瞬态(内存 / 加密缓存 / SSE / 客户端,决定即清)</span>
<span class="chip"><span class="dot g"></span>护栏(作用在完整正文)</span>
</div>
<div class="flow">
<div class="node transient"><div class="t">real client success()</div><small>完整正文唯一存在处 ★捕获</small></div>
<span class="arrow"></span>
<div class="node guard"><div class="t">★完整性+合规护栏</div><small>作用在完整正文,不信 provider 完成原因</small></div>
<span class="arrow"></span>
<div class="node transient"><div class="t">★瞬态加密缓存</div><small>复用 PayloadStore 范式·TTL·决定即清</small></div>
<span class="arrow"></span>
<div class="node transient"><div class="t">SSE chunk回缓存取全文</div><small>填已声明 chunk.data.content·非破坏</small></div>
<span class="arrow"></span>
<div class="node transient"><div class="t">前端 streamContent → 可编辑候选</div><small>显示/交接零改动</small></div>
<span class="arrow"></span>
<div class="node durable"><div class="t">采纳=回传完整正文</div><small>★所见即所写 → Canonical</small></div>
</div>
<div class="flow" style="margin-top:8px">
<div class="node durable"><div class="t">候选表 muse_ai_suggestion</div><small>恒=脱敏摘要+三审(无完整正文)</small></div>
<span class="arrow"></span>
<div class="node durable"><div class="t">Canonical 正文</div><small>由用户回传正文写入归因revision</small></div>
<span class="arrow"></span>
<div class="node transient"><div class="t">决定即清瞬态缓存</div><small>采纳/放弃后物理删除</small></div>
</div>
<div class="redline"><div class="k">主权不变式(红线)</div><p style="color:var(--muted);margin:0">完整正文只存在于:执行线程内存 · 瞬态加密缓存TTL决定即清· SSE 传输 · 客户端。<b>永不</b>写入候选表 / 正文表 / 任何长期列。候选恒为摘要三审Canonical 恒由客户端回传正文写入。</p></div>
<h2>③ 待所有者拍板的材料级点</h2>
<div class="grid cols-3 decide">
<div class="card"><div class="k">瞬态全文载体口径</div><p>完整正文瞬态存在于独立加密缓存表TTL决定即清、永不进候选/正文表是否算「不落长期库」纯内存直推现状不可行SSE 只重放持久化、重连必丢)。</p><span class="rec">推荐:可接受,同 PayloadStore 范式</span></div>
<div class="card"><div class="k">采纳=所见即所写</div><p>采纳(含原样采纳)恒由客户端回传用户审定的完整正文写 Canonical后端不再用摘要写正文。</p><span class="rec">推荐:采纳,兑现主权模型+修潜伏缺陷</span></div>
<div class="card"><div class="k">护栏/审上移到全文</div><p>完整性校验与合规扫描从「作用在摘要」改为「作用在完整正文」(捕获处执行)。</p><span class="rec">推荐:采纳,摘要上做护栏无效</span></div>
</div>
<h2>④ 现状 vs 目标</h2>
<div style="overflow-x:auto">
<table>
<tr><th>维度</th><th>现状</th><th>分支①目标</th></tr>
<tr><td>用户看到的正文</td><td class="now">60/80 字脱敏摘要</td><td class="goal">provider 完整正文</td></tr>
<tr><td>完整正文去向</td><td class="now">success() 内出栈即丢</td><td class="goal">瞬态加密缓存 → SSE 送达,决定即清</td></tr>
<tr><td>「审」的对象</td><td class="now">60/80 字摘要</td><td class="goal">完整正文</td></tr>
<tr><td>截断护栏</td><td class="now">无效(信常量/跑摘要)</td><td class="goal">对完整正文结构化/长度校验</td></tr>
<tr><td>采纳写 Canonical</td><td class="now">原样采纳写摘要(潜伏缺陷)</td><td class="goal">恒回传完整正文(所见即所写)</td></tr>
<tr><td>候选表 / 正文表</td><td class="goal">不含完整正文</td><td class="goal">不变(仍不含)</td></tr>
<tr><td>生成 provider</td><td class="now">New-API</td><td class="goal">可切 Difyfail-closedNew-API 留到 S8</td></tr>
</table>
</div>
<h2>⑤ 步骤与依赖</h2>
<ol class="steps">
<li><b>S7a</b> 捕获护栏provider 无关,不依赖 Dify结果加不落库全文内存字段两 adapter 接出完整正文,护栏移到全文。</li>
<li><b>S7b</b> 瞬态载体SSE 送达:全文写瞬态加密缓存,事件表发 chunk 引用行SSE 重放回取填 content候选表口径不变。</li>
<li><b>S7c</b> 前端接成采纳基准+所见即所写:显示/交接零改动;采纳恒回传全文;区分「流不完整/流为空」防静默采纳截断。</li>
<li><b>S7d</b> 生成 provider 切 Dify<em>依赖 Dify workspace</em>runtimeProvider=difyfail-closedNew-API 保留到 S8。</li>
</ol>
<div class="dep">⚠ 依赖S7d 需 <b>Muse 专属、与游戏项目隔离的 Dify workspace</b> 就绪所有者侧基础设施。S7ac 不依赖,可先行。</div>
<footer>配套正文见 <code>2026-07-07-S7-生成主链全文通路-review.md</code>(含 mermaid 数据流与现状锚点附录)。本页为「一眼看懂边界与核心」的图文速览,非实现细节。</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,203 @@
# S7 生成主链全文通路(分支①)评审版设计
- 版本v0.2(现状已实测校正,待两轮评审转执行版)
- 日期2026-07-07
- 承接:[2.0.0 执行计划 S7/S7.0](2026-07-07-2.0.0单人版改造-execution.md) + 总账 S7.0 裁决(分支①,所有者 2026-07-07 确认)
- 读者:执行 agent 与项目所有者
- 定位回答「S7 要做什么、为什么这样做、边界在哪、怎么验收」。主文无代码细节;现状锚点集中在文末附录,供执行版落地对照。
---
## 1. 背景AI 生成的全文,从未到过用户面前
Muse 的 AI 续写走「影子候选 → 先审后入」主权AI 产出先落 Shadow 候选,用户审阅、编辑后才合并进 Canonical 正文。这套模型隐含一个前提——**用户能看到 AI 写的完整正文**,据此决定采不采、怎么改。
实测显示这个前提当前根本不成立,而且比「少发了一个字段」严重得多:
- **完整正文在生成的那一刻就被丢弃。** provider 返回的完整正文,只在两个真实运行时客户端的 `success()` 方法里以局部变量存在过一瞬,随即被截成 60/80 字的脱敏摘要塞进结果对象方法返回后完整正文再无任何引用。运行时结果对象本身没有承载完整正文的字段因此其下游——执行器、投影、数据库、SSE——**从头到尾只见得到那 60/80 字摘要**。
- **SSE 只发一条终态、且不带正文。** 后端生成是同步非流式的:执行器一次拿到完整结果,只写一条 `done` 事件,载荷是候选 id、完成原因、质量分没有正文`chunk` 事件的契约与前端渲染都早已就位,但后端从不产生 `chunk` 行。
- **连「审」审的也是摘要。** 输出合规扫描与静态检查作用在那条 60/80 字摘要上,而非 provider 完整正文——「先审后入」审的其实是个片段。
- **截断护栏形同虚设。** Dify 的完成原因被硬编码成常量根本不反映真实完成度New-API 的完成原因只记录、不设门;长度检查跑在天然就被截断的摘要上,永不触发。
结论:这不是外部依赖问题,也不是 Dify 切换问题,而是**从运行时到用户之间缺一条完整正文通路**,且这条缺失让「可见 / 可审 / 可采纳」三件事同时落空。分支①要补的就是它,且它与切不切 Dify 相互独立。
## 2. 目标
一句话:**让用户看到、并据以编辑、采纳 AI 生成的完整正文;同时不把 AI 原始产出写进任何长期数据(候选表 / 正文表),守住「原始产出不落长期库、先审后入」主权。**
可验收的四条:
1. **可见**:一次生成后,用户在前端看到的是本次生成的完整正文(长度与 provider 实际输出一致),不再是 60/80 字摘要。
2. **可审**:完整性与合规护栏作用在**完整正文**上(当前只作用在摘要上)——疑似截断或含凭据痕迹→判失败可重试,不静默进候选。
3. **主权不破**:候选表仍只存脱敏摘要 + 三审结果Canonical 正文由用户在前端审定、回传的最终正文写入;完整正文只在「内存 / 瞬态加密缓存 / SSE 传输 / 客户端」中存在,用户做出决定后即清除,**永不进入候选表或正文表任何列**。
4. **provider 可切 Dify**:完整正文透出对 provider 透明New-API 与 Dify 两个 adapter 都把完整正文送到同一处;在此之上把在用系统 Agent 的生成 provider 切到 Dify未配置/凭据错时 fail-closed 拒绝,不静默回退 New-API。
## 3. 边界MECE
**属于 S7 分支①:**
- 在唯一可捕获处(两个 real client 的成功分支)把完整正文接出到一个**不落长期库**的传递位,并将完整性/合规护栏移到此处对**完整正文**执行;
- 用一套瞬态加密缓存承载完整正文(供 SSE 送达与断线重连),用户决定后即清除;
- 让前端把流到的完整正文接成「采纳基准」,采纳时**恒把该完整正文作为最终正文回传**所见即所写Canonical 由回传正文写入;
- 生成 provider 切 Dify配置 + fail-closedNew-API 配置本步**保留**,到 S8 验收后才摘。
**明确不做(边界外):**
- 不做 provider 逐 token 真流式(本步完整正文一次性送达即满足「可见」;真流式是后续独立增强);
- 不改「三审 + 用户回传最终正文采纳」这套主权模型本身,只给它补上「可审的完整正文」与「所见即所写的采纳」;
- 不把完整正文写进候选表 / 正文表 / 任何长期列(红线);
- 不做 S8 导入解析切 DifyNew-API 配置的最终摘除是 S8 的门);
- 不承担 Dify 专属 workspace 搭建(基础设施事项,见依赖)。
## 4. 核心机制:一条「瞬态全文」通路
设计的技术核心是一个真实存在的张力:**SSE 严格「只重放已持久化事件」——凡经 SSE 送出的每个字节,都必须先落进任务事件表;而主权要求完整正文不落长期库。** 二者直接对冲。破解办法是把「事件的顺序与可靠性」和「正文载荷本身」分层,并复用代码库**已经存在**的瞬态敏感数据范式。
一次生成的完整数据流(★=本设计新增/改动的环节):
```mermaid
flowchart TD
U["用户 · AIPanel 发送指令"] -->|"POST /ai/tasks"| Q["入队 generation+jobqueued"]
Q --> D["Dispatcher 领取 job@Scheduled 1s"]
D --> X["Executor 同步执行"]
X -->|"execute(command)"| RC["real client success()<br/>【完整正文唯一存在处】"]
RC --> G["★ 完整性+合规护栏<br/>作用在完整正文(非摘要)"]
G -->|"疑似截断/含凭据"| FAIL["判失败·可重试<br/>不进候选"]
G -->|"通过"| CAP["★ 接出完整正文到<br/>结果的不落库内存字段"]
CAP --> SUM["派生 60/80 字脱敏摘要"]
SUM --> SUG["候选落库 muse_ai_suggestion<br/>content_snapshot=摘要 + 三审<br/>(主权:无完整正文)"]
CAP --> TS["★ 完整正文写瞬态加密缓存<br/>(独立表·短 TTL·决定即清"]
X --> EV["★ 任务事件表写 chunk-ref 行 + done 行<br/>chunk 载荷只存引用,非正文)"]
EV --> SSE["SSE streamTask 重放/轮询事件"]
TS -. "★ 重放 chunk-ref 时回缓存取全文" .-> SSE
SSE -->|"chunk.data.content=完整正文"| AP["AIPanel streamContent 实时渲染"]
SSE -->|"done"| AP
AP --> CP["CandidatePanel 可编辑最终正文"]
CP -->|"★ 采纳恒回传完整正文 finalContent"| MG["Content merge_after_edit"]
MG --> CAN["Canonical 正文 = 用户回传正文<br/>+ 归因 + revision"]
MG --> PUR["★ 决定即清瞬态缓存"]
CP -->|"放弃"| PUR
classDef durable fill:#e8f0ff,stroke:#3358cc,color:#0b2a6b;
classDef transient fill:#fff4e0,stroke:#cc7a00,color:#6b3d00;
classDef guard fill:#e9f9ec,stroke:#2f9e44,color:#12521f;
class SUG,CAN durable;
class CAP,TS,EV,SSE,AP,CP transient;
class G guard;
```
四个要点:
1. **唯一捕获处 + 就地护栏**:完整正文只在两个 real client 的成功分支存在,故完整性/合规护栏必须在这里、对完整正文执行(当前它们跑在下游摘要上、无效)。护栏通过后,把完整正文接出到运行时结果的一个**仅内存、明确不落库**的字段(不破坏「结果只带摘要」的持久化契约——该字段永不进任何列)。
2. **正文与事件分层**:完整正文写进一套**瞬态加密缓存**——对称复用代码库既有的 `MuseAiRuntimePayloadStore` 范式(独立表、短 TTL、字段加密、终态即物理删除它本就是为「入向用户指令」这类瞬态敏感数据设计的分支①用同一范式装「出向完整正文」。任务事件表里的 `chunk` 行只放一个**指向缓存的引用**,不放正文本身。
3. **SSE 重放时回取**SSE 在重放该 `chunk` 引用行时回缓存取完整正文,填进契约早已声明、当前从不发的 `chunk.data.content`(填充既有字段=契约兼容,非破坏)。断线重连=全量重放事件→再次回缓存取(决定窗口内缓存仍在)→完整正文可续;缓存清除后的迟到重连拿到空正文即可(用户已决定)。前端 `streamContent` 已接线到 `chunk.data.content` 并实时渲染,**显示层零改动**。
4. **采纳=所见即所写**:因完整正文自始不落长期库,能写进 Canonical 的完整正文只能来自**客户端回传**。故采纳时前端**恒**把用户审定的完整正文作为最终正文上送无论改没改过Canonical 由回传正文写入。这既坐实主权模型「采纳=用户回传所审最终正文」,也修掉当前「原样采纳会把 60/80 字摘要写进 Canonical」的潜伏缺陷。
**主权不变式(红线,自洽)**:完整正文只存在于 ①执行线程内存 ②瞬态加密缓存TTL决定即清 ③SSE 传输 ④客户端。它**永不**写入 `muse_ai_suggestion` / `muse_content_block` 或任何长期列。候选表恒为摘要三审Canonical 恒由客户端回传正文写入。「不落库」=不进长期数据模型,瞬态加密缓存是代码库既有的、对称使用的瞬态层;「先审后入」现在有真实完整正文可审。
## 5. 三个待所有者拍板的材料级点
分支①触及三处主权/契约边界,均非最小改动能自决,请拍板(每条附我方推荐与理由):
1. **瞬态全文载体口径**完整正文瞬态存在于「独立加密缓存表TTL决定即清永不进候选/正文表)」是否满足「原始产出不落长期库」?
*推荐:可接受。* 它与代码库既有 `MuseAiRuntimePayloadStore`(入向用户指令)同范式、同保护(加密+短 TTL+终态即删只是方向相反durable 数据模型始终不含完整正文。替代是「纯内存直推」,但现有 SSE 只重放持久化事件、无内存直推通道,且重连必丢正文,故纯内存现状下不可行。
2. **采纳语义=所见即所写**:采纳时恒由客户端回传用户审定的完整正文写 Canonical含原样采纳后端不再用自己存的摘要写正文。
*推荐:采纳。* 这正是主权模型「采纳=用户回传最终正文」的字面兑现,并修掉「原样采纳写摘要进 Canonical」的潜伏缺陷。
3. **护栏与「审」上移到完整正文**:完整性校验与合规扫描从当前的「作用在 60/80 字摘要」改为「作用在完整正文」(在捕获处执行)。
*推荐:采纳。* 在天然截断的摘要上做完整性/合规检查是无效护栏;「先审后入」应审真实全文。
## 6. 完整性护栏(第 2 目标的落地要点)
- **护栏对象=完整正文**,在捕获处执行:长度/结构合理性 + 凭据痕迹扫描;疑似截断或不合规→判失败可重试,不静默进候选。
- **不信任 provider 的完成原因**Dify 的完成原因是常量、New-API 的只记录不设门,均不可作完整性信号;截断判定只能靠对完整正文的结构化/长度校验。
- 该护栏 provider 无关,同护 New-API 与 Dify。
## 7. 与切 Dify 的关系(正交)
完整正文通路§4§6provider 无关:两个 adapter 都把 provider 完整正文送到同一捕获/透出位。故通路可**先于 Dify workspace 就绪开工**。provider 切 Dify 只是通路建好后,把在用系统 Agent 的生成 provider 由 New-API 换 DifyruntimeProvider=dify + providerRef 指向「写作透传」app + 凭据 + 读超时对齐 ≥180s 总预算 + fail-closed 反向),并补 Dify 形态护栏断言。New-API 配置保留到 S8。
## 8. 步骤分解WHAT 已定HOW 入执行版)
- **S7a 捕获护栏provider 无关,不依赖 Dify**:运行时结果加不落库的完整正文内存字段,两个 real client 成功分支接出完整正文;完整性/合规护栏移到此处对完整正文执行。
- **S7b 瞬态载体SSE 送达**:完整正文写瞬态加密缓存(复用 `MuseAiRuntimePayloadStore` 范式TTL决定即清事件表发 `chunk` 引用行SSE 重放时回缓存取正文填 `chunk.data.content`;候选表存储口径不变(摘要+三审)。
- **S7c 前端接成采纳基准+所见即所写**:完整正文经既有 `streamContent`→候选链渲染(显示/交接零改动);采纳改为恒回传完整正文作 finalContent处理「流不完整」与「流为空」的区分避免静默采纳截断正文。
- **S7d 生成 provider 切 Dify依赖 Dify workspace**:在用 Agent runtimeProvider=difyproviderRef.dify`muse.ai.dify.enabled=true`读超时对齐fail-closed 反向New-API 配置保留。
S7aS7c 不依赖 Dify workspace可先做S7d 依赖 Muse 专属 Dify workspace 就绪。
## 9. 验收(每条须机械证据)
- **主权不变式(最关键)**:真实 PG 下一次真生成后,`muse_ai_suggestion.content_snapshot.content` 仍是脱敏摘要(长度显著小于 provider 完整输出),完整正文**不在**候选表/正文表任何列;瞬态缓存在采纳/放弃后被清除(查表证明已删)。
- **可见**MSW-off 创作主线 e2e 断言前端渲染的正文长度 > 阈值且与 provider 实际输出一致,不再是 60/80 字摘要。
- **所见即所写**:采纳(含原样采纳)后 Canonical 落的是用户在前端看到/编辑的完整正文长度一致非摘要归因ai_suggestion、revision 递增。
- **护栏**:截断样本→失败可重试、不进候选;正常样本→通过;护栏作用于完整正文的证据。
- **Dify 形态**live IT——Dify 生成→Shadow 候选(三审齐)→完整正文可见→回传采纳写 Canonicalfail-closed 反向dify 未配/凭据错→拒绝,不回退 New-API
- **回归**local + real-PG 全绿;契约结构门绿(`chunk.data.content` 为已声明字段、非破坏)。
## 10. 回滚
- provider 切换:在用 Agent runtimeProvider 拨回 new-api 即回(兼容逻辑未动)。
- 全文通路:以开关门控 chunk 引用行发射+瞬态缓存写入+前端恒回传;关闭即回退「仅摘要」旧行为;代码 revert 独立。
## 11. 风险与缓解
- **瞬态正文暴露**缓存含完整正文——TTL决定即清日志脱敏不打正文字段加密独立表对齐既有范式缓解候选/正文表永不含正文。
- **断线丢正文+静默采纳截断**:中途断连可能丢 chunk 尾部而前端优先采信非空流正文→静默采纳截断——须在前端区分「流不完整有终态但正文疑似截断」与「流为空回源宁可提示重生成不静默采纳截断S7c 要点)。
- **单 chunk 体积**:整章完整正文进单个 SSE 事件——校验无长度上限,必要时分片(仍非逐 token
- **决定后重连**:缓存已清→空正文→可接受(已决定)。
- **契约兼容**`chunk.data.content` 已声明→非破坏;须验证前端能吞一个大 chunk。
## 12. 依赖
- **Dify 专属 workspace所有者侧**S7d 依赖 Muse 专属、与游戏项目隔离的 Dify workspace 就绪(约束已入 `.agents/knowledge/external-deps-and-gotchas.md`。S7ac 不依赖,可先行。
---
## 附录 A · 现状锚点(实测,供执行版对照;均为现状事实,非待写代码)
模块根 `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/`
**完整正文的唯一存在处 / 捕获点**
- New-API`facade/RealNewApiMuseAiRuntimeClient.java` `success()` L163-192——完整正文 `content` L171L172 `shortOutputSummary` 派生摘要(`OUTPUT_SNIPPET_LIMIT=60` L32构造 `RuntimeResult` L186-187 后 `content` 出栈丢弃。
- Dify`facade/RealDifyMuseAiRuntimeClient.java` chat `answer` L230 / workflow `text` L253L272-273 截 ≤80 字,`RuntimeResult` L238/262。
- 结果对象无全文字段:`facade/MuseAiRuntimeClient.java` `RuntimeResult` L70-78`outputSummary` Map L72
**provider 选路(切 Dify 挂钩点)**
- `facade/RoutingMuseAiRuntimeClient.java` L18-53new-api/dify/fail-closed`facade/MuseAiRuntimeProviderSupport.java` L31-42读 agent 版本 config 的 `runtimeProvider`/`providerRef`,缺省 new-api
**候选落库与三审**
- `application/muse/MuseAiRuntimeProjectionService.java``createRuntimeSuggestion` L250-283`contentSnapshot` 映射 L324-333`content`=摘要 L328三审经 `diffSummary` L361-377 仅 `review.passed()` 时写;`source_status` 恒 active L274。
- `application/muse/MuseAiCandidateReviewService.java` L42-84——审查作用在 `resolveContentText` 取出的摘要L336-348非完整正文须上移的点
**SSE只重放持久化事件**
- `application/muse/MuseAiTaskStreamServiceImpl.java` `streamTask` L66-106、回放 `buildReplaySnapshot` L114-124、轮询 `pollPersistedEvents` L195-232、读侧 `chunkData` L279-285已就绪
- 写侧终态门禁 `MuseAiRuntimeProjectionService.appendTaskEvent` L294-317、`isValidTerminalEvent` L319-322`donePayload` L379-386无正文
- 事件表 `muse_ai_task_event` DDL `sql/muse/V12__extend_ai_real_api_schema.sql` L105-138。
- 契约 `docs/api-contracts/ai/openapi.yaml``SSEChunkEvent` L3519-3537`content`+`sequenceNo` 已 required`SSEDoneEvent` L3564-3586。
**可复用的瞬态敏感数据范式(分支①对称复用)**
- `application/muse/MuseAiRuntimePayloadStore.java` L24-46——独立表30min TTL`DEFAULT_TTL` L26字段加密终态物理删除executor `finally` 触发 `remove` L67-73
**前端**
- `muse-studio/src/features/editor/components/AIPanel.tsx``streamContent`/`streamContentRef` L48/56`onChunk` 接线 L130-132实时渲染 L189`handleStreamDone` L63-89 优先采信流正文 L71-75。
- `muse-studio/src/features/editor/hooks/useAcceptSuggestion.ts``shouldMergeAfterEdit` L39-51——仅编辑过才带 `finalContent`(所见即所写要改的写路径)。
- `muse-studio/src/features/editor/components/CandidatePanel.tsx``reviewedText` 可编辑 L97-103。
- `muse-studio/src/lib/sse.ts``connectAIStream` L255-406、重连 dedup L288-312后端不读 lastEventId客户端按 SSE id 去重)。
## 附录 B · 现状 vs 目标(一图速览的文字版)
| 维度 | 现状 | 分支①目标 |
|---|---|---|
| 用户看到的正文 | 60/80 字脱敏摘要 | provider 完整正文 |
| 完整正文去向 | success() 内出栈即丢 | 瞬态加密缓存TTL决定即清SSE 送达 |
| 「审」的对象 | 60/80 字摘要 | 完整正文 |
| 截断护栏 | 无效(信 provider 常量/跑摘要) | 对完整正文结构化/长度校验 |
| 采纳写 Canonical | 改过才回传,原样采纳写摘要 | 恒回传完整正文(所见即所写) |
| 候选表/正文表 | 摘要(正文本不落库) | 不变(仍不含完整正文) |
| 生成 provider | New-API | 可切 Difyfail-closedNew-API 留到 S8 |