收口DirectProject输入区canonical content唯一事实源

删除输入区 text/references 双轨草稿状态

Direct 与 Planning 改为从 Lexical content 读取

润色保留 chip 并保护附件 canonical content

增加 legacy content DTO 单向翻译与附件回归测试

补充 DirectProject composer canonical content 实施计划
This commit is contained in:
2026-09-21 14:11:18 +08:00
parent 94471ff191
commit ab52805d3c
4 changed files with 110 additions and 8 deletions
@@ -5,10 +5,7 @@ import { useEffect, useImperativeHandle, useRef, useState } from 'react';
import { AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD } from '../../../app/constants';
import { claimInitialTurnForPage } from '../../../app/initialTurnClaims';
import { projectNameFromPath } from '../../../features/agent-runtime';
import {
directCodexContentToPromptText,
directCodexUserItemFromContent,
} from '../../../features/project-workspace/resourceReferences';
import { directCodexUserItemFromContent } from '../../../features/project-workspace/resourceReferences';
import { type ApprovalMode, approvalModeLabel } from '../approvalMode';
import { ApprovalModeDialog } from '../ApprovalModeDialog';
import { DirectProjectChatHeader } from './components/DirectProjectChatHeader/DirectProjectChatHeader';
@@ -163,7 +160,6 @@ export function DirectProjectChatView({
);
shouldFollowLatestRef.current = true;
startInitialTurn({
prompt: directCodexContentToPromptText(userItem.content, assets),
// 首轮那条用户消息按入口原话展示,引用/附件仍按 canonical content 发给运行时。
messageText: directInitialTurnText(initialTurn.content),
clientTurnId,
@@ -223,7 +223,6 @@ export function useDirectProjectChatController({
if (rest.length === 0) setComposerNotice('');
startTurn({
clientTurnId: next.clientTurnId,
prompt: directCodexContentToPromptText(next.userItem.content, assets),
userItem: next.userItem,
});
}
@@ -394,7 +393,6 @@ export function useDirectProjectChatController({
directCodexConversationMessageId(clientTurnId, 'user'),
);
const turn: DirectProjectTurnInput = {
prompt: directCodexContentToPromptText(userItem.content, assets),
clientTurnId,
userItem,
};
@@ -498,7 +496,6 @@ export function useDirectProjectChatController({
await withDirectCodexSessionRefresh(() =>
invoke<string>('chat_with_game_creator_direct_codex', {
projectPath: nextProjectPath,
prompt: input.prompt,
clientTurnId: input.clientTurnId,
userItem: input.userItem,
...(input.creationType ? { creationType: input.creationType } : {}),
@@ -0,0 +1,55 @@
# 【实施计划】DirectProject composer canonical content 闭环
| 字段 | 值 |
| --------- | --- |
| Milestone | `docs/project-memory/plans/【里程碑】DirectProject composer canonical content闭环-2026-09-21.md` |
| Status | planned |
| Owner | Codex |
## 交付结果
删除 Direct composer 的 `text + references` 回写事实源,改为完整 `content[]` 的单向读写;同时修复附件在 editor root 重建、润色回填和提交合并中的丢失窗口。Planning V2 / 快速编辑只保留明确的非 Direct adapter,不进入 Direct canonical 发送路径。
## 实施顺序
1. **锁定当前失败面**:补一个最小回归测试,先证明 `applyDraftToRoot` 对含 `agc_attachment_reference` 的 root 重建会丢附件,并记录 Direct 当前的三份并行状态与附件 sidecar 合并点。
2. **收窄编辑器接口**:把 `ChatComposerDraft` 改成 content-only;将 `ResourceReferenceInput` 的受控恢复改为 `initialContent` / `content` 语义,删除 Direct 使用的 `value` + `references` 同步 effect。保留 `ChatReference` 仅用于 picker 节点创建和只读投影。
3. **替换所有 root 写入**:删除 `buildDraftSegments` / `applyDraftToRoot` 的 Direct 调用;提供一个完整的 `applyContentToRoot`(或同等深模块接口),保留文本换写、引用和附件的顺序与字段。AI 润色改为“读取 content → 只替换文本 part 的受控变换 → 按原序写回 content”,禁止用 prompt 文本重新解析 chip。
4. **收口 Direct controller**:以 `content[]` 作为草稿事实源;上传完成的附件在一个明确的 `append`/`merge` seam 合并,不再长期维护 `chatInput``chatReferences``chatContent` 三份状态。prompt 只在调用边界派生,队列和策略重试保留原 user item。
5. **隔离非 Direct caller**Planning V2 和资源快速编辑按各自需求选择 content-only 或独立文本 adapter;不得为了兼容它们把旧字段放回共享 Direct 接口。若快速编辑只需要文本,使用不承诺引用/附件语义的窄接口。
6. **补齐验收证据并清理**:删除无调用方的旧 helper、字段和注释;更新测试 fixture、专题文档和本计划状态。完成后把稳定结论合并回功能说明/共享记忆,并删除已完成的临时计划。
## 重点修改边界
- `apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx`
- `apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts`
- `apps/ai-game-creator-shell/src/features/project-workspace/usePromptPolish.ts` / `chatPromptPolish.ts`(仅在需要支持 content 变换时)
- `apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts`
- `apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/`
- `apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx``App.tsx` 中 Planning adapter
- `apps/ai-game-creator-shell/src/view/project-development/index.tsx` 的快速编辑输入 adapter
- 对应 `resourceReferences``resourceReferenceInput``chatPromptPolish`、Direct composer/appSurface 测试
## 关键不变量
- `content[]` 是唯一可发送事实;任何 `prompt`、label、chip 文案都是派生值。
- `agc_attachment_reference` 不属于 `ChatReference`,不能借 `references` 传递。
- 文本润色只允许改变文本 part;若产品决定重排/删除引用,必须显式产生新的 content 变换,不得通过字符串 token 猜测。
- Direct 初始首轮、普通提交、队列、权限确认重试都使用同一个 `DirectCodexUserItem` 语义。
## 验证命令
```bash
npm run test -- apps/ai-game-creator-shell/tests/resourceReferences.test.ts apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts
npm run ai-game-creator-shell:typecheck
npm run check:encoding
npm run check:doc-index
git diff --check
```
## 风险与回滚点
- `ResourceReferenceInput` 当前同时服务 Direct、Planning、快速编辑;先建立 adapter seam,再删除旧 props,避免一次性把策划链路打坏。
- 附件目前在 Direct controller 里作为待发 sidecar,若直接移入编辑器必须同步处理上传失败、移除、清空和项目切换;任何阶段都不能把附件只留在展示 chip 而未进入 user item。
- 润色服务只返回文本,不能让其响应覆盖原 content;失败时保留原 content。回滚使用 Git revert,不增加运行时开关或兼容分支。
- 工作树已有用户未提交修改,实施时只 stage 本里程碑涉及的 hunk。
@@ -0,0 +1,54 @@
# 【里程碑】DirectProject composer canonical content 闭环
| 字段 | 值 |
| ----------- | --- |
| Version | 1.0 |
| Status | planned |
| Date | 2026-09-21 |
| Parent Spec | `docs/【功能说明】AGC聊天素材引用-2026-09-08.md` |
| Related ADR | `docs/adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md` |
| Supersedes | `docs/project-memory/plans/【实施计划】DirectProject canonical content严格边界-2026-09-16.md` 的已实现边界清理,补齐其遗漏的编辑器回写路径 |
## 问题确认
上一里程碑把 Direct 发送、队列和 Rust user item 收口到了 `content[]`,但输入编辑器仍保留一条旧的派生状态接口:
- `ChatComposerDraft` 仍包含 `text``references``content` 三个字段。
- `ResourceReferenceInput` 仍接收 `value` + `references`,并在受控同步 effect 中调用 `applyDraftToRoot(value, references)`
- `replaceText``usePromptPolish``applyPromptText` 也调用 `applyDraftToRoot(text, current.references)`
- `applyDraftToRoot` 只重建文本和 `ChatReference` 节点,不能表达 `agc_attachment_reference`,会在任何包含附件节点的 editor root 重建中丢附件。
- Direct controller 仍把同一份 `content` 拆成 `chatContent``chatInput``chatReferences` 三份状态;提交时又把附件 sidecar 追加回 `content`,导致编辑器 canonical content 与本轮完整 canonical user content 不是同一份事实源。
## 目标
让 DirectProject 的 composer 从读取、润色、受控恢复、队列、附件到提交都只围绕一份有序 `content[]` 运作:
1. 编辑器对外草稿类型只有 `content`
2. 文本展示、提醒、队列 chip 和 prompt 参数全部由 `content[]` 派生。
3. 任意编辑器回写只能接收完整 `content[]`;不能用文本和引用列表重建 root。
4. `agc_attachment_reference` 在恢复、润色回写、队列和提交中保持顺序和值,不得丢失、搬移或重复。
5. DirectProject 不再维护 `chatInput` / `chatReferences` / `chatContent` 并行事实源;待发上传附件也必须在进入提交前显式合并为同一 canonical content。
## 边界与非目标
- 保留 `directCodexContentToPromptText` 作为只读展示投影,不把它反向解析成 content。
- 保留 `ChatReference` 作为 picker/UI 的候选与节点显示模型;它不是 Direct 草稿或发送模型。
- Planning V2 与资源快速编辑若仍需要文本 props,只能通过明确的 adapter/独立表象接口接入,不能让这些 legacy props 继续成为 Direct composer 的接口。
- 不修改 Rust user item schema、SpacetimeDB、HTTP API 或附件协议字段。
- 不引入兼容 alias、`text + references → content` fallback、隐式附件 sidecar 拼接或双写状态。
## 验收标准
- `ChatComposerDraft``ResourceReferenceInput` 的 Direct 使用面和 Direct composer props 不再暴露 `text` / `references` 双轨草稿。
- 代码中不存在 Direct 路径对 `applyDraftToRoot(value, references)``replaceText(text)` 或等价 legacy 重建器的调用。
- `content[]` 中交错的文本、资源引用、运行区域引用、Skill 和附件经过:初始恢复 → 读取 → 润色回写 → 再读取,顺序和值完全一致。
- 附件-only、文本+附件、资源引用+附件、文本+资源引用+附件四类提交均只生成一份 canonical user item,附件字段完整保留。
- 队列入队、出队、策略确认重试复用同一份 `userItem.content`,不从 prompt 文本重建。
- Direct 提交清理草稿后,待发附件和 editor content 同时清空;失败或取消不会悄悄丢失待发附件。
- Planning/快速编辑的迁移或隔离有明确类型接口和定向测试,不再借 Direct 的双轨 props。
## 证据要求
- ResourceReferenceInput 纯投影/回写测试:包含附件节点的 round-trip 与润色回写不丢附件。
- Direct composer appSurface:四类 content 组合、队列、策略重试及清理语义。
- 类型检查、定向 Vitest、`npm run check:encoding``npm run check:doc-index``git diff --check`