放宽 DirectProject content 校验为「整条判定」并按逐字投影(review 第 1 条,① 口径)

- Rust validate_direct_codex_user_item 改为只判整条 content:有一段非空白文本或任意非文本 part 即有效,单个纯空白 input_text(段落分隔 / 软换行 / chip 后的分隔空格)合法,不再逐个 part 拒绝空文本
- 新增 content_has_meaningful_input,并与前端 hasMeaningfulDirectCodexContent 同口径;wire.rs 的「不能转换为空 prompt」只作兜底
- ResourceReferenceInput 的投影层原样透传编辑器节点:不做空白过滤、也不与相邻 part 合并,删掉 appendInputText 与中间版本的「向前合并」收集器
- 按逐字投影更新资源输入、Godot 回合用例的 canonical content 断言
- 同步里程碑实现结论与证据、pitfalls 条目;Rust validation/wire 新增「单个纯空白 part 通过、整条全空白拒绝」用例
This commit is contained in:
2026-09-17 14:01:41 +08:00
parent e469831543
commit b543a060b7
7 changed files with 165 additions and 75 deletions
@@ -38,13 +38,14 @@
## 实现结论
- 唯一的前端空白过滤留在 Lexical 投影层:投影只产出有意义的 `input_text`,因为 Rust `validate_direct_codex_user_item` 会拒绝空 `input_text`。转换函数不再重复过滤。
- 投影层的过滤口径是「向前合并」而不是「直接丢弃」:上一 part 是引用(chip)时先把待写文本攒住,等下一个文本 part 到来再合并成同一个 part。段落分隔符(root 子节点之间补的 `\n`)与软换行因此不会丢,`@素材` 后面的那一段不会被粘成 `@素材下一段`;攒到最后仍只有空白的(以换行结尾、两个 chip 之间只隔一个换行)不入 content。
- 显示文本、队列 chip 文案、草稿持久化和润色判据统一由 `directCodexContentToPromptText(content, assets)` 从 content 派生,不再维护并行的 `text` 字段。
- 前端不再做任何空白过滤:Lexical 投影层原样透传编辑器节点,段落分隔符(root 子节点之间补的 `\n`)、软换行、chip 后的分隔空格都各自成 part,既不丢弃也不与相邻 part 合并 —— 前端不替用户改写他输入的内容。
- 有效输入只判整条 content:有一段非空白文本或任何一个非文本 part 就算有效输入,单个纯空白 `input_text` 合法。前端 `hasMeaningfulDirectCodexContent` 与 Rust `validate_direct_codex_user_item`(`content_has_meaningful_input`)同口径,Rust 侧不再逐个 part 拒绝空文本;`wire.rs` 的「不能转换为空 prompt」只作兜底。
- 显示文本、队列 chip 文案、草稿持久化和润色判据统一由 `directCodexContentToPromptText(content, assets)` 从 content 派生,不再维护并行的 `text` 字段;`assets`(当前项目 manifest)必填,`agc_resource_reference` 按 `@显示名` 展开,消息正文与队列 chip 因此逐字一致。
- 需要文本草稿的旧入口(`replaceText`、快速编辑)仍由编辑器把文本 + 引用重建为 content,方向是「文本 → content」,不存在「legacy 字段 → content」的回退。
## 证据
- `apps/ai-game-creator-shell/tests/resourceReferences.test.ts`:content 原样传递、空白 part 保留、有效性判断、文本派生。
- `apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx`(含「引用后面的段落分隔不会被吞掉」)、`chatPromptPolish.test.tsx`、`tests/appSurface/*.suite.ts`:草稿读取、提醒判据、队列与 caller 迁移到 content-only。
- `apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx`(含「引用后面的段落分隔原样落进 content」)、`chatPromptPolish.test.tsx`、`tests/appSurface/*.suite.ts`:草稿读取、提醒判据、队列与 caller 迁移到 content-only,并按逐字投影断言 content。
- `apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/`:`validation.rs` 的「只有整条 content 全空白才算空输入」与 `wire.rs` 的「单个纯空白 part 通过校验、整条全空白拒绝」用例。
- AGC shell 类型检查、定向 Vitest、`npm run check:encoding`、`git diff --check` 通过。
@@ -5620,10 +5620,10 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
## 2026-09-16 Lexical 投影丢掉引用后的换行:`@素材` 和下一段粘成一个词
- **现象**:聊天输入区里先 `@` 一个素材、回车换段再写文字,提交出去的 canonical content 里没有任何分隔,直接读成 `@hero把这一版改成夜景`;同一个字符串还会进 agent 输入、队列 chip 文案与润色判据。
- **原因**:`ResourceReferenceInput` 的 `appendInputText` 只认「上一 part 是 `input_text`」这一种可追加情形,其余一律 `if (text.trim())` 才落 part。root 子节点之间补的段落分隔符与 `LineBreakNode` 传进来的都是 `'\n'`,`trim()` 为空 ⇒ 整段丢掉;chip 后那一段文字随后另起一个 part,派生文本用 `''` 直接拼接,于是粘在一起。
- **处理**:投影层改成「向前合并」——待写文本先攒在 `pendingText` 上,下一个文本到来时合并成同一个 `input_text`(`\n` 因此落在 `\n把这一版改成夜景` 里);攒到最后仍只有空白的(以换行结尾、两个 chip 之间只隔一个换行)不入 content,因为 Rust `validate_direct_codex_user_item` 会拒绝空 `input_text`,直接按 review 的 `text.includes('\n')` 落 part 会让「chip 换行 chip」这种输入整轮发不出去。
- **验证**:`apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx` 新增「引用后面的段落分隔不会被吞掉」,断言 chip 后一段的 part 为 `{ type: 'input_text', text: '\n把这一版改成夜景' }`、派生文本逐字一致、且不出现纯空白 part;去掉修复后该用例变红(比对失败在 content 相等那一行)。
- **关联**:`apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx`(`DraftContentCollector` / `appendInputText`)、`docs/project-memory/plans/【里程碑】DirectProject canonical content严格边界-2026-09-16.md`。
- **原因**:`3c7b02b9f`(2026-09-15)为了让 content 通过 Rust 的「空 `input_text`」校验,在投影层加了 `appendInputText`(`if (text.trim())` 才落 part,并与相邻文本合并)。root 子节点之间补的段落分隔符与 `LineBreakNode` 传进来的都是 `'\n'`,`trim()` 为空 ⇒ 整段丢掉;chip 后那一段文字随后另起一个 part,派生文本用 `''` 直接拼接,于是粘成 `@hero把这一版改成夜景`。`41366dd71` 又把消息正文 / 队列 chip / 快速编辑的文本派生切到这条投影上,缺陷扩散到界面与出站 prompt。
- **处理(最终口径)**:不保留任何前端过滤,而是去掉规则和它的成因——Rust `validate_direct_codex_user_item` 改成只判整条 content(`content_has_meaningful_input`:有一段非空白文本或任意非文本 part 即有效),单个纯空白 `input_text` 合法;`ResourceReferenceInput` 的投影原样透传编辑器节点,既不丢空白也不与相邻 part 合并。中间版本(把待写文本「向前合并」到下一个 part)已随之删除:它仍会丢掉尾随换行与「两个 chip 之间只隔一个换行」的分隔,也仍要让前端替用户改写内容。
- **验证**:Rust `validation.rs` / `wire.rs` 新增「单个纯空白 part 通过校验、整条全空白拒绝」用例;`apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx`「引用后面的段落分隔原样落进 content」断言 `[ref, { type: 'input_text', text: '\n' }, { type: 'input_text', text: '…' }]` 与派生文本逐字一致;`tests/appSurface/project-development.suite.ts` 的 Godot 用例断言 Shift+Enter 的两个换行各自成 part。
- **关联**:`apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx`(`collectDraftParts`)、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs`、`docs/project-memory/plans/【里程碑】DirectProject canonical content严格边界-2026-09-16.md`。
## 2026-09-16 派生文本漏传 `manifest.assets`:`@引用` 从显示名退化成内部 id