修复对话动态计时与初始消息重复并补齐游戏居中指引
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m51s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m45s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m4s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m38s
Project CI / Repository checks (pull_request) Successful in 5m39s
Project CI / Frontend tests (pull_request) Successful in 7m45s
Project CI / Backend tests (pull_request) Successful in 10m3s
Project CI / Native shell tests (pull_request) Successful in 8m54s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m31s

区分整轮总耗时与工具实际阶段时间,以百毫秒刷新一位小数并冻结终态。
修复正式与乐观用户消息到达时初始占位重复,保留真实同文多次发送。
在开发 Agent 系统提示与 Skill 明确禁止 Phaser 和 CSS 双重居中。
补齐原生事件、前端回归与验收文档,保留未完成项和远程 CI 风险。
This commit is contained in:
2026-09-18 12:17:28 +08:00
parent 1bd153d7f2
commit 2ffccb844c
27 changed files with 2010 additions and 361 deletions
@@ -0,0 +1,33 @@
# 【实施计划】对话总耗时与动态工具计时
| 字段 | 值 |
| --- | --- |
| Status | implemented-awaiting-runtime-acceptance |
| Milestone | docs/project-memory/plans/【里程碑】对话总耗时与动态工具计时-2026-09-18.md |
## 修改边界与顺序
1. 独立评审主规范与里程碑后进入实现。当前问题是工具组窗口、整轮范围和运行快照混用;不能只把 setInterval 从 1000 改成 100。
2. 原生在现有生命周期事件上提供阶段时间:`turn.started` / `turn.completed` 的事件级 `at`,以及 `item.started` / `item.completed` 的事件级 `at`,不是更改条目嵌套的 `item.at`。工具计时只消费阶段事件时间,嵌套 `item.at` 继续承载条目展示时间,不当作真实开始/完成。开始/完成事件优先读取对应上游阶段字段或时长,缺失时在宿主事件产生时取钟。TypeScript 可选数值字段处理既有历史/无时间夹具,真实宿主始终提供时间;ts-rs 生成绑定,按仓库 u64→number 约定处理,不手改。
3. 前端事件 reducer 保留工具开始、完成边界以及本轮终态时间;完成边界可附在本轮聊天条目展示元数据上,沿用现有用户条目划分回合,不新增 turnId 或第二套生命周期。无时间旧历史不编造准确总耗时。
- 整轮起点:该轮用户消息的实际发送时间优先,缺失才取 `turn.started.at`;与用户要求的“发送到完成”一致,不能改为第一条工具的起点。终点只取明确终态事件,不能取最后一条正文的创建时间或最后工具更新时间。
- 只有完成没有开始事件的工具隐藏未知耗时;缺失、0、非有限或倒序边界隐藏,正常运行的零时长显示 `0.0`。
4. 视图传递同轮起止时间到各工具块,以共享或局部有界的 100ms 时钟驱动展示,不把整棵 App 状态每 100ms 重建。只在有运行中计时目标时订阅,结束/切项目/卸载清理。
5. 测试先覆盖静止事件输入下推进时钟、工具完成但回合仍运行、终态与重复事件;补阶段字段与回放时间测试。独立 review 后主 Agent 核对并返修。
6. 同视图的追加局部修复:初始消息占位判定应读取实际 Direct 用户条目/乐观用户条目和分页状态,不只检查旧 conversationMessages;增加正式气泡只出现一次、合法同文消息不被去重的回归。
## 并行写入边界
- 原生实现者:`src-tauri/src/agent/direct_thread_wire.rs`、`codex_app_server/mod.rs`、必要的 `direct_thread_manager.rs` 模式匹配/测试及 ts-rs 生成绑定。
- 前端实现者:`directThreadChat.ts`、`directThreadItemProjection.ts`、`directTurnPresentation.ts`、`ToolCallGroup.tsx`、`toolCallGroupPresentation.ts`、`ProjectSupervisorView.tsx`、必要的 App 接线与对应测试。不得修改原生或生成绑定。
- 主 Agent:文档、集成验证、独立 review 决策与浏览器 smoke;不得覆盖其他人的未提交文件。
## 验证与风险
前端使用已有 `.app/merge-deps` 隔离配置,不修改共享 node_modules。运行定向 Vitest、AGC 类型/skill/config 检查、Rust wire/manager 相关测试与 all-targets offline check、编码/文档索引/diff 检查。历史缺少完整时间时隐藏无法证明的用时,不迁移历史文件。原生协议变更需重新启动客户端才能生效,不擅自重启用户正在运行的回合。
回滚点为本次改动前的 `1bd153d7f`;若失败只回退本次文件变更,不动用户锁文件和其它分支。
## 收口
已完成本轮实现、独立 review 返修与自动化/模拟浏览器验证,证据见对应里程碑。用户已授权更新 Issue/PR 并推送当前功能分支;不得据此合并 PR、发布或恢复定时任务。真实客户端端到端验收尚待完成,因此保留计划,卡片全面重设计和画布内任务浮层不计入本轮已交付内容。
@@ -0,0 +1,35 @@
# 【里程碑】对话总耗时与动态工具计时
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | implemented-awaiting-runtime-acceptance |
| Date | 2026-09-18 |
| Parent Spec | docs/technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md |
## 目标与边界
工具组显示整轮请求总耗时,单条显示自身调用耗时,运行中均以 100 毫秒粒度动态增长并保留一位小数。沿用 Thread Manager,不新增业务状态源或计时账本。保留既有画布功能和本地未提交锁文件;用户后续已授权更新 Issue/PR 与推送本分支,不合并 PR、不发布、不写飞书、不调用真实付费 Provider。
## 验收
前置独立评审已完成:事件级时间与条目展示时间分离、缺失/倒序判据、章节结构及格式示例已明确。主 Agent 决定整轮起点以用户发送为优先、原生 turn.started 为缺失兜底,不采用会排除发送后准备时间的起点替换。
- [x] 总耗时从用户发送到回合终态,跨多个工具块共享边界,包含无工具运行时的 LLM 等待。
- [x] 工具运行中无事件亦增长,已完成工具与完成回合均冻结,失败/终止通过既有明确收口路径冻结。
- [x] 原生开始/完成时间不混用,事件重放不重新取当前时间。
- [x] 重复、迟到条目不抬高已冻结用时;缺失/倒序时间不伪造耗时。同秒新回合仍按事件顺序正常开始。
- [x] 一位小数、分钟进位、并行调用与卸载清理有定向/浏览器证据。
- [x] 类型检查、Rust 定向测试与 all-targets check、编码/文档索引及 diff 检查通过。
- [x] 模拟事件的实际浏览器验证动态增长与终态冻结;未调用真实 Provider。
- [x] 正式 Direct 用户条目或本地乐观条目出现后撤下初始占位,真实同文重复发送仍保留。
- [ ] 真实客户端切项目、取消后立即重发、重开历史及真实 Provider 通知链路验收。
## 验证与剩余风险
- appSurface 与 reducer/回合投影三文件合计 493 项通过、17 项跳过;最终初始占位与计时边界补丁另跑 5 项通过。
- 隔离依赖配置下 AGC TypeScript、skill/config、ESLint、编码与文档索引检查通过;Rust all-targets locked offline check 通过。
- 原生实现者执行 direct_thread 35 项、direct_ 312 项(1 跳过)、codex_app_server 58 项(1 跳过)及系统工程提示定向用例通过,过滤集合有重叠,不能相加作为独立用例总数。
- Edge 无头真实浏览器使用生产组件与 reducer、模拟生命周期事件:无新事件仍每 100ms 增长,单工具完成后冻结,另一工具与整轮继续增长,回合完成及重复完成事件不改变最终值。
- 无上游精确阶段时间时使用宿主观测毫秒,不宣称测得模型内部物理耗时;旧历史缺少完整生命周期时间时隐藏未知用时。
- 此证据不代表远程 CI 全绿。此前 PR 当前已推送提交的 run 2473 有两条 Rust shard 2 失败,仍需单独排查及后续 SHA 的远程结果。
@@ -1,5 +1,11 @@
# 踩坑与排障记录
## Phaser 与 CSS 双重居中导致游戏画面偏移
Phaser `Scale.FIT` 与 `autoCenter: CENTER_BOTH` 会给 canvas 计算定位外边距。若 canvas 的直接父容器同时使用 `display: grid; place-items: center` 或另一套 CSS 居中,浏览器再次定位带 margin 的元素,竖屏游戏会相对预览区域偏右。应只保留一个居中责任方:Phaser 居中时直接父容器使用尺寸明确的普通块布局;CSS 居中时设置 `autoCenter: NO_CENTER`。不禁止外围页面的 Grid/Flex 布局,不通过修改 AGC iframe 的固定偏移掩盖项目 CSS 问题。
开发 Agent 的实际系统工程提示和 `agc-web-game-development` Skill 均包含此规则。布局修改后重新构建 dist,分别在桌面、移动与 resize 后测量 canvas 相对游戏父容器的中心误差(预期居中时不超过 1 CSS px),同时检查无溢出和意外滚动条;构建成功不等于视觉验收通过。
## AGC 空快照测试必须等待请求完成
`waitFor(() => expect(activeTurns).toEqual([]))` 在 Hook 初始状态就能成功,不能证明首次异步读取已经完成。引用稳定性回归应显式控制 Promise 完成,并同时检查首次空响应与禁用后的引用;快照签名初值必须与初始空数组一致。窗口同步测试应验证未变化状态不重复发布,不能依赖一次多余的空态更新。
@@ -16,6 +16,8 @@
## 开发中
- Direct 对话计时区分条目展示时间与生命周期事件时间:整轮用用户发送到明确终态的跨度,工具用各自开始/完成边界;运行时用 100ms 叶子时钟刷新一位小数,终态冻结,旧历史缺边界不推测。不得用整秒时间的大小比较取代 Thread Manager 的事件顺序判定新回合。
- AGC 批量追加素材标签由原生在一次项目写锁与 revision CAS 下合并各项原标签,先校验全批再写 manifest;前端不能循环单素材分类命令,不回传展示层推导的分类或旧标签全集,以免部分写入或覆盖未编辑字段。
- 画布卡片类型与信息角标共用 `CanvasCardCornerActions`;菜单收纳共用 `OverflowActions`,宿主决定展示数量和资源命令。AGC 选中菜单前 5 项直显,Web 默认不折叠;浮层 portal 继续接入现有画布关闭与滚轮归属判据。
@@ -1,5 +1,9 @@
# AI 游戏创作智能体 App 实施计划
## 游戏画布居中指引
开发 Agent 的系统工程提示与 `agc-web-game-development` Skill 明确约束同一 canvas 的居中只能由一方负责:Phaser `FIT + CENTER_BOTH` 配合尺寸明确的普通块级父容器,不叠加同一父容器的 Grid/Flex 居中、自动外边距或居中 transform;如由 CSS 居中则设置 Phaser `NO_CENTER`,外围页面的 Grid/Flex 不受此限制。布局修改后构建实际 dist,并在桌面、移动和 resize 下核对 canvas 对游戏父容器中心偏差不超过 1 CSS px、无溢出与意外滚动条。出现偏移先检查游戏项目的 CSS/scale,不修改 AGC 预览固定偏移掩盖问题;这些要求通过 Agent 指引执行,不新增运行时门禁或平行校验系统。
## 多选素材批量标签
- 画布与资源面板共用选中集合。选择至少两项同项目已登记素材后,从已有“编辑标签”入口或资源面板的“批量标签”动作打开同一标签编辑器的批量模式;入口遵循既有“常用操作 + 更多”收纳,不新增平行资源管理页。
@@ -11,7 +11,22 @@
## 一句话交付
把 GameAgent 右侧对话面板里的「执行命令 / 写文件 / 调工具」从一行中文进度文本,改成 Codex 桌面客户端那样的**可折叠卡片**(折叠态一行摘要,展开态看命令与文件明细),并且在**刷新页面、重开项目后仍然存在**。
把 GameAgent 右侧对话面板里的「执行命令 / 写文件 / 调工具」改成可折叠卡片,顶部显示整轮请求总耗时,内部工具显示各自耗时;运行中的计时动态增长并保留一位小数。
## 总耗时与动态工具计时
- 工具块顶部的“总耗时”表示同一轮用户请求从发送到回合终态的墙钟跨度,包含 LLM 推理、工具调用和等待;不是工具耗时之和,也不是当前工具块的时间跨度。思考或正文将工具拆成多组时,各组读取同一轮的时间边界,不能各自重置起点。
- 回合仍在运行时,即使当前工具块内的工具已全部结束,总耗时仍持续增加。成功、失败或终止到达后按实际终态时间固定;随后打开折叠块、翻页或其它回合的新事件不得继续改变该轮用时。
- 单条工具从其实际开始到完成计时。运行中的工具按当前时间持续增长,不能把最近一次快照更新时间当作当前时间;已经结束的工具必须立即固定,即使同组其它工具或 LLM 仍在运行。
- 两种耗时均每 100 毫秒刷新,始终保留一位小数(例如 `0.0秒`、`5.1秒`、`1分钟 2.3秒`,单条可沿用 `5.1s` / `1m 2.3s` 的紧凑格式)。以时间戳计算而不是按 tick 累加,避免后台节流后的累计漂移;缺失或倒序边界不伪造 `0.0`。
- 时间范围与总耗时使用同一轮的起止边界。运行中显示“进行中”;完成后如果展示起止时间,其精度不得造成范围差与总耗时矛盾。
- 原生事件保留各阶段的时间语义:开始与完成不能都优先折叠成开始时间。优先采用上游明确提供的阶段时间或实际调用时长,缺失时使用宿主观察该阶段的时间;重放沿用原事件时间,不能在前端收到或重放时重新取当前时间。
- 工具开始/完成的权威边界是事件级 `at`,不是条目展示字段 `item.at`。整轮起点优先采用该轮实际用户消息的发送时间(与气泡一致,不取所有条目的最小时间),缺失时采用原生 `turn.started.at`;终点只采用 `turn.completed.at` 或明确的终止/失败收口事件。首次补到更早的真实发送时间可以校正起点,但旧历史或重复事件不能覆盖已经固定的终点。
- 回合阶段使用宿主观测阶段的毫秒时间,或上游明确的毫秒边界/可靠时长;不把上游已截断的整秒时间冒充十分之一秒精度。回合是否开始以 Thread Manager 的事件顺序为准,不能用时间戳大小拒绝取消后同一秒内的新请求。回合完成的展示时间在当前会话中冻结;重新打开后若历史未保存完整生命周期边界,则隐藏未知用时而非补造。
- 无效边界包括缺字段、非有限值、时间戳为 0、结束早于开始;这些情况隐藏不能证明的耗时。合法开始时刻的运行中 `0.0秒` / `0.0s` 则正常显示。只有完成快照而没有开始事件的工具不猜测开始时间。
- 沿用 Thread Manager 的生命周期与条目身份,不引入第二套回合状态源、计时账本、远程 API 或数据库字段。前端仅保存原事件的展示时间边界;旧历史缺少完整边界时不声称知道精确总耗时或工具耗时,不为补齐计时启动模型请求。
- 必须覆盖:两组工具共用整轮起点、无新事件仍增长、工具完成后 LLM 继续、并行工具分别计时、完成/失败/终止冻结、同身份重放不改终态时间、切项目与卸载停止计时、缺失与倒序时间、0.0 / 59.9 / 60.0 秒边界。真实 Provider 验证与模拟事件的客户端验证分开报告。
- Direct 对话的初始消息占位仅在正式用户消息尚未进入当前显示链路时显示;正式条目或乐观用户条目出现后撤下占位,不在消息列表外额外保留一份。只控制占位是否显示,不按文本合并或删除用户真实重复发送的消息;分页已有更早历史时不把初始占位补到当前页。
## 背景与现状(已核实)
@@ -74,11 +89,11 @@ DirectRuntime 写 `<projectRoot>/.agent/conversations/tool-calls.jsonl`;回读
```html
<section class="agent-tool-call-group" data-testid="agent-tool-call-group" data-status="completed">
<button type="button" class="agent-tool-call-group-head" aria-expanded="false" aria-controls="…"
aria-label="已执行 2 个命令、1 个文件变更,用时 42秒">
aria-label="已执行 2 个命令、1 个文件变更,总耗时 42.0秒">
<span class="agent-tool-call-group-icon" aria-hidden="true"></span>
<span class="agent-tool-call-group-summary">已执行 2 个命令、1 个文件变更</span>
<span class="agent-tool-call-group-time">14:20:05 → 14:21:02</span>
<span class="agent-tool-call-group-duration">用时 42秒</span>
<span class="agent-tool-call-group-time">14:20:05.0 → 14:20:47.0</span>
<span class="agent-tool-call-group-duration">总耗时 42.0秒</span>
<svg class="agent-tool-call-group-chevron" aria-hidden="true"></svg>
</button>
<div class="agent-tool-call-group-body" hidden>
@@ -105,8 +120,8 @@ DirectRuntime 写 `<projectRoot>/.agent/conversations/tool-calls.jsonl`;回读
- 文案规则(按 kind,不允许自由发挥):
- 块头汇总按 kind 计数、顺序固定 `command → file_change → mcp_tool → web_search → context_compaction → other`,标签 `命令`/`文件变更`/`工具调用`/`联网搜索`/`上下文整理`/`其他操作`,形如 `已执行 5 个命令、2 个文件变更`;空集合不渲染块。
- 行文案:展示工具摘要,不重复添加动词前缀;`context_compaction` 固定为“整理上下文”。状态单独放在行尾(执行中 / 已执行 / 失败),`failed` 使用现有 `--platform-*` 错误色;已结束回合不因残留 `running` 快照显示“执行中”。
- 耗时:单条工具 = `startedAt` → `updatedAt`,块头总用时 = 该回合所有工具的 `min(startedAt)` → `max(updatedAt)`。单条格式:`<1s` → `0.4s`、`<60s` → `12.3s`(整秒省略小数)、`≥60s` → `2m 5s`;块头格式:`42秒` / `4分钟` / `5分钟 45秒`。`startedAt` 为 0 或 `updatedAt < startedAt` 时不显示耗时(不显示 `0s` / 负数),耗时为 0 时同样不显示 `0s`。
- 时间:块头显示该回合结束时间(`max(updatedAt)` 的本地 `HH:mm:ss`);同一回合能拿到用户消息时间(`updatedAt > 0`)时显示 `HH:mm:ss → HH:mm:ss`(发送 → 结束),取不到就只显示结束时间,不编造。
- 耗时:执行“总耗时与动态工具计时”合同。块头是整轮请求的总耗时,单条是该工具独立耗时,均保留一位小数;运行中每 100 毫秒刷新,终态固定,不按工具更新时间推断回合结束。
- 时间:时间范围与总耗时使用同一回合边界,不再把用户发送起点与局部工具组终点混搭。缺失的历史时间不编造。
- 回合结束时间与耗时在正文下方右对齐;Direct 对话输入框提示统一为“描述你的想法,或 @ 引用素材”,引用按钮保留输入盒的 12px 内边距,不使用负边距贴边。
- 当前 Agent 与策划 Agent 共用 `packages/shared` 的 `AgentMessageContent` 表现组件:正文为 14px / `--platform-text-strong`,思考、中间输出和工具调用为 12px / `--platform-text-soft`。实时与历史思考共用同一个折叠入口;工具输入输出继承过程色,失败状态保留错误色。Markdown 标题、表格及代码高亮在过程区同步弱化,最终回复和文档预览仍保留正常排版,不按 Agent 类型复制样式。输入提示与禁用状态保持原有反馈。
- Windows 命令展示:仅 `command` 卡片识别 `pwsh` / `powershell`(含完整路径、`.exe`、常见启动选项)的 `-Command` / `-c` 外层包装,摘要和展开输入只展示脚本正文,并解开单个 shell 参数的引用拼接。摘要优先读取已脱敏的 `detail.command`,再按首行 120 字符截断,避免历史摘要被可执行文件路径占满。无法识别的启动方式、`-File`、`-EncodedCommand`、普通命令和 MCP 输入原样展示;执行参数、持久化原文、脱敏和输出均不改变。