抽离 DirectProject 独立聊天容器并删除工作台壳 Direct 状态
- 新增 view/project-development/chat 模块:DirectProjectChatView 自己持有项目清单订阅、首屏锚点、历史分页、发送队列、附件、终止、草稿与本地消息 - DirectProjectChatView 入参收窄为项目路径、入口首轮需求(canonical content[])和两条权限门,Direct 状态不再穿过 App.tsx - 线程订阅与聊天 reducer 提成 useDirectThreadChatSubscription:subscribe/consume/notify 单飞循环、订阅欠账、SUBSCRIPTION_EXPIRED 重订与 DirectThreadChatState - chat/components 按「一个组件一个目录、逻辑与表现同目录」重组(ChatHeader、Composer、Conversation、SettingsDialog、ToolCallGroup) - DirectProjectChatView 拆成聊天头、会话区、回合、输入盒四个组件 - App.tsx 删除 Direct 专属 state/ref/effect/handler 与 directCodex 分支,首轮需求按 canonical content 交给聊天 - 工作台级动作(运行本地预览等)经 DirectProjectChatHandle.announce 交给聊天自己的本地消息流 - Direct 首轮认领记录提成 app/initialSupervisorMessageClaims,供各入口共用 - Rust 侧 Direct 契约 ts-rs 导出目录改到 chat/generated - 补齐 direct_codex_user_item/wire.rs 既有的 rustfmt 空格漂移 - 同步 ADR、实施计划、decision-log、pitfalls 与 Direct 技术文档
This commit is contained in:
@@ -7,3 +7,16 @@ AGC 项目开发工作台的普通项目固定使用 DirectProject;它不再
|
||||
项目工作台布局拥有跨面板的账户/钱包入口。钱包不再作为 `walletEntry` 传入 DirectProject 聊天上下文或聊天设置浮层,而是在 `ProjectDevelopmentView` 的布局级头部/工具栏独立渲染;本次只改变布局归属与嵌套关系,保留钱包入口本身的可用性。
|
||||
|
||||
这项边界选择是为了让 DirectProject 的事实源与生命周期可以独立测试和演进,同时避免把工作台级账户状态与聊天状态耦合;一次性替换通过 Git revert 回滚,不增加运行时开关、兼容别名或条件兼容分支。
|
||||
|
||||
## 模块结构
|
||||
|
||||
公开入口只有 `view/project-development/chat/DirectProjectChatView.tsx`,内部按职责分层:
|
||||
|
||||
- `components/`:表现层,一个组件一个目录,组件自己的逻辑与表现同目录(`components/ToolCallGroup/` 放 `ToolCallGroup.tsx` 与 `toolCallGroupPresentation.ts`;`components/DirectProjectComposer/` 放 `DirectProjectComposer.tsx`、`ComposerControls.tsx`、`chatComposerQueue.ts`、`chatComposerVoice.ts`)。
|
||||
- `controller/`:会话控制器 `useDirectProjectChatController`、项目清单订阅 `useDirectProjectManifest`,以及线程订阅与聊天 reducer 的独立 hook `useDirectThreadChatSubscription`。
|
||||
- `conversation/`、`history/`:纯模块(条目投影、历史分页、订阅锚点闸门、附件映射)。
|
||||
- `generated/`:Rust 侧 ts-rs 导出的 Direct 契约,不在前端手抄形状。
|
||||
|
||||
`useDirectThreadChatSubscription` 持有 `subscribe → consume → notify` 单飞循环与聊天 reducer 状态,是 DirectProject 运行态的唯一事实源;控制器只读它投影后的条目与回合忙态,历史分页通过同一个 reducer 并入。
|
||||
|
||||
工作台壳要把运行/预览这类工作台级动作的结果说给用户时,只通过 `DirectProjectChatView` 的 `ref` 句柄(`announce`)交给聊天自己的本地消息流;壳不持有 Direct 的聊天消息。
|
||||
|
||||
@@ -62,3 +62,34 @@
|
||||
- **竞态回归**:重点检查 subscribe 回执前 notify 欠账、首屏锚点闸门、`turn.completed` 与 invoke finally 的 FIFO 出队窗口、提交早于 `turn.started` 的中止以及项目切换时附件/队列清理。
|
||||
- **布局回归**:钱包必须在工作台布局头部/工具栏出现,且聊天设置不再包含钱包行;账户入口功能不能因聊天抽离丢失。
|
||||
- **回滚**:本次采用一次性替换;失败时整体 Git revert,不保留旧/新双路径或运行时 fallback。
|
||||
|
||||
## 实施记录(2026-09-19)
|
||||
|
||||
### 模块落位
|
||||
|
||||
`apps/ai-game-creator-shell/src/view/project-development/chat/` 最终结构:
|
||||
|
||||
| 目录 | 内容 |
|
||||
| --- | --- |
|
||||
| `DirectProjectChatView.tsx` | 唯一公开入口:项目路径、入口首轮需求、两条权限门,以及对壳的 `announce` 句柄 |
|
||||
| `components/` | 表现层,一个组件一个目录、逻辑与组件同目录:`DirectProjectChatHeader/`、`DirectProjectComposer/`(含 `ComposerControls.tsx`、`chatComposerQueue.ts`、`chatComposerVoice.ts`)、`DirectProjectConversation/`(含 `DirectProjectTurn.tsx`)、`DirectProjectSettingsDialog/`、`ToolCallGroup/`(含 `toolCallGroupPresentation.ts`) |
|
||||
| `controller/` | `useDirectProjectChatController`(发送/队列/附件/中止/历史分页)、`useDirectProjectManifest`(清单与 `projectId` 订阅)、`useDirectThreadChatSubscription`(`subscribe → consume → notify` 单飞循环 + 聊天 reducer 状态) |
|
||||
| `conversation/` | `directThreadChat.ts`(reducer)、`directThreadItemProjection.ts`、`directTurnPresentation.ts`、`directCodexConversation.ts`、`directCodexSession.ts`、`directCodexTurnAttachments.ts` |
|
||||
| `history/` | `directHistoryPaging.ts`、`directHistoryAnchorGate.ts` |
|
||||
| `generated/` | Rust ts-rs 导出的 Direct 契约 |
|
||||
|
||||
### 关键取舍
|
||||
|
||||
- 线程订阅与聊天 reducer 状态从控制器里提成独立 hook `useDirectThreadChatSubscription`:运行态事实源只有它一份,控制器只读投影结果,历史分页仍并入同一个 reducer。
|
||||
- 工作台级动作(如 `game.run_local` 的预览结果)不再写 `App.tsx` 的 `messages`:壳通过 `DirectProjectChatHandle.announce` 把这句话交给聊天的本地消息流,壳不持有 Direct 聊天消息。
|
||||
|
||||
### 验证结果
|
||||
|
||||
- `npx vitest run` 五个 Direct 纯模块测试:40 通过。
|
||||
- `npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "direct"`:15 通过 / 4 失败,且 4 条在本次改动前(`HEAD` 的 `/tmp` worktree 基线)同样失败(用例期望 `chat_with_game_creator_direct_codex` 被调用,但 harness 未桩 `read_game_creator_app_config`,模型可用性校验把提交挡在 invoke 之前;另两条是既有交互断言)。基线同口径为 14 通过 / 5 失败,本次净增 1 条通过。
|
||||
- `npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts` 全量:153 失败 / 307 通过 / 17 跳过;`HEAD` 基线同口径 155 失败 / 305 通过。
|
||||
- `npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npx eslint --ext .ts,.tsx`、`npx prettier --check` 通过。
|
||||
|
||||
### 本计划外的后续项
|
||||
|
||||
- Supervisor 整体退役(`ProjectSupervisorView`、`SupervisorChatOnlyView`、`ProjectWorkspaceChatPane`、`App.tsx` 里的 Supervisor/Design/Planning 运行态和 `main.tsx` 的 `supervisor-chat` 入口)不在本计划范围内,尚未开始,需要先确认范围再删。
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
|
||||
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
|
||||
|
||||
## 2026-09-19 DirectProject 聊天容器按「组件带逻辑」分层,线程订阅与 reducer 提成独立 hook
|
||||
|
||||
- 背景:DirectProject 聊天虽然已经搬进 `view/project-development/chat/`,但 `DirectProjectChatView.tsx` 单文件 500+ 行,表现层只按 `components/`、`composer/` 平铺文件,线程订阅(`subscribe → consume → notify`)与聊天 reducer 状态混在 `useDirectProjectChatController` 里,组件和它自己的逻辑分散在两个目录。
|
||||
- 决策(组件带逻辑):`chat/components/` 下一个组件一个目录,组件自己的逻辑与表现同目录:`ToolCallGroup/`(`ToolCallGroup.tsx` + `toolCallGroupPresentation.ts`)、`DirectProjectComposer/`(`DirectProjectComposer.tsx` + `ComposerControls.tsx` + `chatComposerQueue.ts` + `chatComposerVoice.ts`)、`DirectProjectConversation/`(`DirectProjectConversation.tsx` + `DirectProjectTurn.tsx`)、`DirectProjectChatHeader/`、`DirectProjectSettingsDialog/`。视图只做组合,不再内联渲染分区、回合和输入盒。
|
||||
- 决策(订阅与 reducer 独立 hook):新增 `chat/controller/useDirectThreadChatSubscription.ts`,持有 `subscribe → consume → notify` 单飞循环与 `DirectThreadChatState`(含通知先于订阅回执的欠账处理、`SUBSCRIPTION_EXPIRED` 重订、订阅回执锚点闸门、项目切换清理),并暴露 `entries`、`turnRunning`、`mergeHistoryItems`、`markTurnStopped`。控制器只读投影结果,历史分页仍并入同一个 reducer,不复制第二份事实源。
|
||||
- 决策(壳只传窄上下文):`DirectProjectChatView` 的公开入参只有项目路径、入口首轮需求(canonical `content[]`)和两条权限门;工作台级动作(`game.run_local` 预览结果等)不再写 `App.tsx` 的 `messages`,改由壳通过 `DirectProjectChatHandle.announce` 交给聊天自己的本地消息流,壳不持有 Direct 聊天消息。
|
||||
- 边界:不改 Tauri/Rust 命令、DTO、Thread Manager、Codex app-server、持久化事实源、路由选择、发送/队列/附件/中止/历史锚点语义;不保留旧路径 alias、双跑或兼容 fallback。
|
||||
- 验证:五个 Direct 纯模块测试 40 条通过;`appSurface.test.ts -t "direct"` 15 通过 / 4 失败,4 条与 `HEAD` 同口径基线同为失败(harness 未桩模型配置),基线同口径 14 通过 / 5 失败;全量 `appSurface.test.ts` 由基线 155 失败降到 153 失败;app `tsc --noEmit`、eslint、prettier 通过。
|
||||
- 关联文档:[ADR DirectProject 独立聊天容器与工作台钱包布局](../../adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md)、[实施计划](../plans/【实施计划】DirectProject聊天模块抽离-2026-09-18.md)。
|
||||
|
||||
## 2026-09-18 Provider 瞬态重试次数严格按设置执行(游戏开发 Agent 与策划 Agent 不再被档位收进区间)
|
||||
|
||||
- 背景:AGC 客户端此前把 `agentLlm.<agent>.maxRetries` 按运行档位重新收进固定区间——`autonomous-game-build` 档位(自主构建的游戏开发 Agent 及其继承档位的专业子 Agent)被抬到 12~16,`standard` 档位(含立项策划入口的策划 Agent 与普通 Agent 对话)被压到最多 3;瞬态分类里的上游 400 还在同一预算上再收窄到 2 次。现场把 `maxRetries` 设成 5 时,游戏开发与策划两条链路都不按设置执行。
|
||||
|
||||
@@ -5715,8 +5715,8 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
## 2026-09-16 AGC 壳跑过 `cargo test` 后前端 typecheck 必红:ts-rs 导出把 `generated/*.ts` 重写成另一种形状
|
||||
|
||||
- **现象**:在 `apps/ai-game-creator-shell/src-tauri` 跑过 `cargo test` 之后,`npm run ai-game-creator-shell:typecheck` 报 10 条类型错(`src/features/project-workspace/resourceReferences.ts:106-112` 的 `string | undefined` 不能赋给 `string | null`;同文件 134 行的对象字面量带 `type: 'message'`,而 `DirectCodexUserMessageItem` 里没有该字段),`npm run ai-game-creator-shell:build` 也死在 `beforeBuildCommand` 的同一条 typecheck 上。
|
||||
- **原因**:crate 里的 ts-rs 导出会按**本机依赖版本**重写 `src/features/project-workspace/generated/DirectCodexUser*.ts`:注释头变成 "This file was generated…"、字符串改双引号、`DirectCodexUserMessageItem` 丢掉 `type: 'message'` 判别字段、并多出一个 `DirectCodexUserMessageEnvelope.ts`。仓库里提交的那份是前端真正依赖的形状(前端按带 `type` 的判别联合写),重写后两边就对不上——错在生成器版本漂移,不在前端。
|
||||
- **处理(现行口径)**:不要把重写结果当改动提交。跑过 `cargo test` 或构建后只恢复 `apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUser*.ts` 的仓库版本,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts`;不要恢复整个 `generated/` 目录,以免误删 DirectThread 的现役绑定。绑定与前端形状冲突时以**已提交的 DirectCodexUser 绑定 + 前端**为基准排查。
|
||||
- **原因**:crate 里的 ts-rs 导出会按**本机依赖版本**重写 `src/view/project-development/chat/generated/DirectCodexUser*.ts`:注释头变成 "This file was generated…"、字符串改双引号、`DirectCodexUserMessageItem` 丢掉 `type: 'message'` 判别字段、并多出一个 `DirectCodexUserMessageEnvelope.ts`。仓库里提交的那份是前端真正依赖的形状(前端按带 `type` 的判别联合写),重写后两边就对不上——错在生成器版本漂移,不在前端。
|
||||
- **处理(现行口径)**:不要把重写结果当改动提交。跑过 `cargo test` 或构建后只恢复 `apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectCodexUser*.ts` 的仓库版本,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts`;不要恢复整个 `generated/` 目录,以免误删 DirectThread 的现役绑定。绑定与前端形状冲突时以**已提交的 DirectCodexUser 绑定 + 前端**为基准排查。
|
||||
- **验证**:恢复仓库版本后 `npm run ai-game-creator-shell:typecheck` exit 0(`[skill-pack] OK`);保留重写结果时同一条命令 exit 2。release 构建本身还会在 `src/features/ui-editor/types/` 落下 `BindingChange.ts` / `BindingDTO.ts` 两个无人引用的生成产物;它们不属于前端契约,发现后直接删除,不提交。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/`(ts-rs 导出源)、`apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts`、`apps/ai-game-creator-shell/scripts/build-release.mjs`(`beforeBuildCommand`)。
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ readHistory(threadId, { beforeItemId?, limit }) -> {
|
||||
|
||||
Thread 内所有公开事件共用一个单调递增 seq,但 **seq 只是 Thread Manager 的内部游标事实,不下发**:同一个 subscriber 的 `consume` 按队列顺序返回事件数组,数组顺序就是前端要处理的顺序,前端因此不需要 item 级 cursor 或第二套 reducer。
|
||||
|
||||
线上模型是 ts-rs 导出的 tagged enum(`agent/direct_thread_wire.rs`),前端消费 `src/features/project-workspace/generated/` 里的生成绑定,改 Rust 模型后跑 `cargo test export_bindings` 重新生成;毫秒时间戳标 `#[ts(as = "f64")]`,因为 ts-rs 默认把 `u64` 映射成 `bigint`,而 Tauri 的 JSON 通道传的是 `number`。
|
||||
线上模型是 ts-rs 导出的 tagged enum(`agent/direct_thread_wire.rs`),前端消费 `src/view/project-development/chat/generated/` 里的生成绑定,改 Rust 模型后跑 `cargo test export_bindings` 重新生成;毫秒时间戳标 `#[ts(as = "f64")]`,因为 ts-rs 默认把 `u64` 映射成 `bigint`,而 Tauri 的 JSON 通道传的是 `number`。
|
||||
|
||||
事件按 `type` 区分,条目按 `itemType` 区分:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user