修复 DirectProject 流式正文逐 delta 脱敏吃掉段尾换行导致 Markdown 结构损坏 #533

Merged
suzmii merged 1 commits from fix/direct-delta-markdown into master 2026-09-29 21:49:44 +08:00
Member

Closes #532

问题

AGC 项目对话里 agent 汇报的 Markdown 结构在流式阶段整体失效:段落并进同一行、序号项挤成一段、表格整块不渲染(表头、|---|---| 与数据行连成一段正文);路径还会被切成 game<absolute-path>ame.js。重开项目读历史时渲染正常,所以此前一直像「渲染器偶发」。

原因

#384(30c377d0f)把 DirectProject 流式正文改成每个 delta 各自脱敏(意图正确:前端不得接触原始增量),但脱敏函数不满足拼接不变性,前端 appendLiveText 是裸拼接:

  • codex_app_server/mod.rs direct_codex_thread_delta_event → direct_thread_wire.rs direct_thread_delta_text → generation/prompt_context.rs sanitize_error_context 旧实现用 lines() + join("\n") 往返,以换行结尾的段其末尾换行被吃掉("a\n" → "a"、"\n" → "")。
  • 同链路里 redact_absolute_path_tokens 把 index == 0 当文本边界,delta 从 / 开头时片段被误判成绝对路径("assets" + "/c" + "anvas-generated" → assets<absolute-path>anvas-generated)。
  • directThreadChat.ts 的 mergeDirectChatEntry 正文取「更长的一份」,被 <absolute-path> 撑长的流式正文会一直压过 item.completed 的正确快照。

改动

文件 变更
apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs sanitize_error_context 改为 split_inclusive('\n') 逐段处理、原样保留行终止符(CRLF 仍归一成 LF;私钥块整行吞掉的旧口径不变)。逐段脱敏因此与整段脱敏同形
apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs 新增回归测试 direct_thread_delta_sanitization_preserves_line_breaks:逐段脱敏必须逐字等于整段脱敏,去掉 split_inclusive 即红
docs/project-memory/shared-memory/pitfalls.md 记入本次排障(含同源未修的路径边界缺陷)

前后对照(本机 gameagent-cfc64e7f 真实汇报全文,1445 字符,38 个换行)

取文本路径 字符 / 换行 ChatMarkdownMessage 渲染结果
修复前:逐 delta 脱敏后拼接 1435 / 0 table:0 th:0 li:0 h2:0 p:1(整条消息退化成一段正文,即问题现象)
修复后:逐 delta 脱敏后拼接 1473 / 38 table:1 th:3 h2:4 p:8 li:6
参照:整段脱敏(item.completed / 历史读回) 1473 / 38 table:1 th:3 h2:4 p:8 li:6(修复后与流式路径一致)

验证

  • cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- direct_thread(46 passed)、-- sanitize(7 passed)、新用例单跑通过。
  • node .codex/hooks/pre-submit-compile-check.mjs:rustfmt + TS typecheck + admin-web typecheck + api-server 编译全部通过。
  • 渲染侧复核:真实文本经 Rust 脱敏后送进 ChatMarkdownMessage 渲染(临时夹具已删)。
  • npm run check:encoding、git diff --check 通过。

未覆盖

redact_absolute_path_tokens 的 index == 0 边界误判(Issue 第 4 点)仍需跨 delta 的「上一个字符」上下文,本 PR 不处理,避免动到脱敏边界与协议。

Closes #532 ## 问题 AGC 项目对话里 agent 汇报的 Markdown 结构在**流式阶段**整体失效:段落并进同一行、序号项挤成一段、表格整块不渲染(表头、`|---|---|` 与数据行连成一段正文);路径还会被切成 `game<absolute-path>ame.js`。重开项目读历史时渲染正常,所以此前一直像「渲染器偶发」。 ## 原因 `#384`(`30c377d0f`)把 DirectProject 流式正文改成**每个 delta 各自脱敏**(意图正确:前端不得接触原始增量),但脱敏函数不满足**拼接不变性**,前端 `appendLiveText` 是裸拼接: - `codex_app_server/mod.rs` `direct_codex_thread_delta_event` → `direct_thread_wire.rs` `direct_thread_delta_text` → `generation/prompt_context.rs` `sanitize_error_context` 旧实现用 `lines()` + `join("\n")` 往返,**以换行结尾的段其末尾换行被吃掉**(`"a\n"` → `"a"`、`"\n"` → `""`)。 - 同链路里 `redact_absolute_path_tokens` 把 `index == 0` 当文本边界,delta 从 `/` 开头时片段被误判成绝对路径(`"assets" + "/c" + "anvas-generated"` → `assets<absolute-path>anvas-generated`)。 - `directThreadChat.ts` 的 `mergeDirectChatEntry` 正文取「更长的一份」,被 `<absolute-path>` 撑长的流式正文会一直压过 `item.completed` 的正确快照。 ## 改动 | 文件 | 变更 | | --- | --- | | `apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs` | `sanitize_error_context` 改为 `split_inclusive('\n')` 逐段处理、原样保留行终止符(CRLF 仍归一成 LF;私钥块整行吞掉的旧口径不变)。逐段脱敏因此与整段脱敏同形 | | `apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs` | 新增回归测试 `direct_thread_delta_sanitization_preserves_line_breaks`:逐段脱敏必须逐字等于整段脱敏,去掉 `split_inclusive` 即红 | | `docs/project-memory/shared-memory/pitfalls.md` | 记入本次排障(含同源未修的路径边界缺陷) | ## 前后对照(本机 `gameagent-cfc64e7f` 真实汇报全文,1445 字符,38 个换行) | 取文本路径 | 字符 / 换行 | `ChatMarkdownMessage` 渲染结果 | | --- | --- | --- | | 修复前:逐 delta 脱敏后拼接 | 1435 / **0** | `table:0 th:0 li:0 h2:0 p:1`(整条消息退化成一段正文,即问题现象) | | 修复后:逐 delta 脱敏后拼接 | 1473 / 38 | `table:1 th:3 h2:4 p:8 li:6` | | 参照:整段脱敏(`item.completed` / 历史读回) | 1473 / 38 | `table:1 th:3 h2:4 p:8 li:6`(修复后与流式路径一致) | ## 验证 - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- direct_thread`(46 passed)、`-- sanitize`(7 passed)、新用例单跑通过。 - `node .codex/hooks/pre-submit-compile-check.mjs`:rustfmt + TS typecheck + admin-web typecheck + api-server 编译全部通过。 - 渲染侧复核:真实文本经 Rust 脱敏后送进 `ChatMarkdownMessage` 渲染(临时夹具已删)。 - `npm run check:encoding`、`git diff --check` 通过。 ## 未覆盖 `redact_absolute_path_tokens` 的 `index == 0` 边界误判(Issue 第 4 点)仍需跨 delta 的「上一个字符」上下文,本 PR 不处理,避免动到脱敏边界与协议。
suzmii added 1 commit 2026-09-29 21:33:43 +08:00
修复 DirectProject 流式正文逐 delta 脱敏吃掉段尾换行导致 Markdown 结构损坏
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / Backend tests (pull_request) Successful in 4m7s
Project CI / Frontend tests (pull_request) Successful in 2m13s
Project CI / Native shell tests (pull_request) Successful in 6m8s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m4s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m29s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m15s
2cc7df8074
- prompt_context.rs:sanitize_error_context 改为 split_inclusive('\n') 逐段处理并原样保留行终止符,「逐段脱敏」与「整段脱敏」同形(CRLF 仍归一成 LF,私钥块整行吞掉的旧口径不变)
- codex_app_server/mod.rs:新增 direct_thread_delta_sanitization_preserves_line_breaks,钉住逐段脱敏 == 整段脱敏,去掉 split_inclusive 即红
- pitfalls.md:记入本次排障(含 chunk 起点路径误判这一同源未修缺陷)
suzmii merged commit 4cba964234 into master 2026-09-29 21:49:44 +08:00
suzmii deleted branch fix/direct-delta-markdown 2026-09-29 21:49:44 +08:00
Sign in to join this conversation.