Merge remote-tracking branch 'origin/master' into feat/ui-editor-v3
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 3m0s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 3m4s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 3m7s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 3m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m32s
Project CI / Frontend tests (pull_request) Failing after 4m47s
Project CI / Repository checks (pull_request) Failing after 4m39s
Project CI / Native shell tests (pull_request) Successful in 7m14s
Project CI / Backend tests (pull_request) Successful in 8m12s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m54s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 3m0s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 3m4s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 3m7s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 3m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m32s
Project CI / Frontend tests (pull_request) Failing after 4m47s
Project CI / Repository checks (pull_request) Failing after 4m39s
Project CI / Native shell tests (pull_request) Successful in 7m14s
Project CI / Backend tests (pull_request) Successful in 8m12s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m54s
# Conflicts: # docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
@@ -0,0 +1,15 @@
|
||||
# AGC 资源工作台三处交互收口:改动前对照图(2026-09-14)
|
||||
|
||||
本目录是三处 UI 收口改动(`fix/agc-resource-workbench-chrome`)的改动前截图,供 PR 与后续回归对照使用。三张图都取自改动前的客户端「资源管理」工作台。
|
||||
|
||||
| 文件 | 画面 | 改动前的问题 |
|
||||
| --- | --- | --- |
|
||||
| `01-task-sidebar-two-close-entries.jpg` | 「生成任务」侧栏展开态(图内红框标出两处待处理对象) | 头部右上角是一枚 `‹`(`ChevronLeft`,`aria-label="收起生成任务"`),底部 footer 里还有一枚 `×`(`aria-label="关闭生成任务"`),中间由 footer 的 `border-top` 分割线隔开——同一个 `onToggleOpen` 有两个入口,且头部那枚是「返回」语义 |
|
||||
| `02-left-rail-collapse-handle.jpg` | 资源管理工作台(图内红框标出左侧贴边竖条) | 侧栏折叠后在画布左侧留一条竖排文字「生成任务」把手,常驻遮挡画布 |
|
||||
| `03-toolbar-play-centered.jpg` | 资源管理工作台(图内红框标出顶部工具条) | 顶部「播放」按钮靠 `position: absolute; left: 50%; transform: translateX(-50%)` 居中悬浮,与它左边的「资源管理 / 运行」模式切换脱节 |
|
||||
|
||||
改动后的形态、验收判据与验证方式见当次 PR 描述与 commit message;相关文档口径已同步到:
|
||||
|
||||
- [`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`](../../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)(顶部播放按钮、任务侧栏进度面两条)
|
||||
- [`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`](../【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)
|
||||
- [`docs/project-memory/shared-memory/decision-log.md`](../../../project-memory/shared-memory/decision-log.md)(进度面形态决策)
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 1. 一句话
|
||||
|
||||
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类入口走本地 `generate_local_project_asset`,音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
|
||||
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类入口走本地 `start_local_project_asset_generation`(提交即返回、后台生成),音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
|
||||
|
||||
## 2. 入口矩阵(事实源)
|
||||
|
||||
@@ -31,23 +31,31 @@
|
||||
|
||||
## 4. 请求载荷映射
|
||||
|
||||
| 入口 | IPC | 载荷要点 |
|
||||
| --------------------- | ------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| 生成图片 | `generate_local_project_asset` | `kind: 'image'`,比例 / 尺寸可调,默认 `1:1 · 1K` |
|
||||
| 生成角色形象 | 同上 | `kind: 'character'`,默认 `1:1 · 1K` |
|
||||
| 生成规范 → 图标规范 | 同上 | `kind: 'icon-spec'`,固定 `1:1 · 1K`;缺权威规范图时 `outputPath: 'assets/art-spec.png'` |
|
||||
| 生成规范 → 角色规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
|
||||
| 生成规范 → 自定义规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
|
||||
| 生成图标素材 | 同上 | `kind: 'art-spritesheet'`,默认 `1:1 · 1K`;前置:已登记 `assets/art-spec.png` |
|
||||
| 生成 UI 设计图 | 同上 | `kind: 'ui-prototype'`,默认 `16:9 · 1K`;前置同上 |
|
||||
| 生成背景音乐 | `derive_local_project_resource` | `editKind: 'background-music'`、`generationMode: 'create'`、`sourceMediaType: 'audio/mpeg'` |
|
||||
| 生成音效 | `derive_local_project_resource` | `editKind: 'sound-effect'`、其余同上 |
|
||||
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
|
||||
| 入口 | IPC | 载荷要点 |
|
||||
| --------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| 生成图片 | `start_local_project_asset_generation` | `kind: 'image'`,比例 / 尺寸可调,默认 `1:1 · 1K` |
|
||||
| 生成角色形象 | 同上 | `kind: 'character'`,默认 `1:1 · 1K` |
|
||||
| 生成规范 → 图标规范 | 同上 | `kind: 'icon-spec'`,固定 `1:1 · 1K`;缺权威规范图时 `outputPath: 'assets/art-spec.png'` |
|
||||
| 生成规范 → 角色规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
|
||||
| 生成规范 → 自定义规范 | 同上 | `kind: 'spec'`,固定 `1:1 · 1K` |
|
||||
| 生成图标素材 | 同上 | `kind: 'art-spritesheet'`,默认 `1:1 · 1K`;前置:已登记 `assets/art-spec.png` |
|
||||
| 生成 UI 设计图 | 同上 | `kind: 'ui-prototype'`,默认 `16:9 · 1K`;前置同上 |
|
||||
| 生成背景音乐 | `derive_local_project_resource` | `editKind: 'background-music'`、`generationMode: 'create'`、`sourceMediaType: 'audio/mpeg'` |
|
||||
| 生成音效 | `derive_local_project_resource` | `editKind: 'sound-effect'`、其余同上 |
|
||||
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
|
||||
|
||||
`generate_local_project_asset` 的完整参数是 `{ projectPath, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(Rust `src-tauri/src/commands.rs` 的 `generate_local_project_asset`,返回 `{ id, localPath, absolutePath, manifestPath }`)。
|
||||
`start_local_project_asset_generation` 的完整参数是 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(Rust `src-tauri/src/asset_generation_tasks.rs`),**提交即返回**一条任务记录(`taskId / status / phaseDetail / assetId / error` 等);生成在 `tauri::async_runtime::spawn` 出来的后台任务里跑,账本落在项目内 `.agent/runtime/asset-generation-tasks/tasks.json`,进度由 `list_local_project_asset_generations` 读回。截图面板不再「等生成结束」,所以**点「生成」即同步关闭面板**(不等 IPC),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。
|
||||
|
||||
## 4a. 本地排队与「生成任务」侧栏
|
||||
|
||||
- **本地排队**:AGC 本地 durable 输出槽按**精确动作指纹**分槽(`run_id = slot-<sha256(动作材料)>`,材料为 prompt / outputPath / 比例 / 尺寸 / assetKind / assetLabel / replaceExisting / requireSlices;不同 prompt 或素材名各自独立成槽,不再按 `outputPath` 共用;旧 `{outputPath,requireSlices}` 槽账本由同一精确动作懒迁移)。因此后端已具备并行能力,但**本批前端仍按「同一时刻只派发一条」排队**——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。所以第二条提交停在**前端本地队列**里(不调用提交 IPC,阶段显示本地排队的「排队中。」),第一条终态后由同一条循环自动补发;判据是「存在 `dispatched && 未终态` 的任务时不派发下一条」。
|
||||
- **阶段文案只来自后端,且只出现在任务侧栏**:派发之后由任务侧栏渲染后端账本给的 `phaseDetail`(「排队中。」/「正在生成。」/「生成已完成。」/失败原因),提交面板已关闭、不渲染任何阶段文案;前端不拼阶段、不做百分比。
|
||||
- **「生成任务」是常驻画布的可折叠侧栏**(形态对齐网页端美术画布的任务侧栏):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除);折叠即整块让出画布、**不留贴边把手**;**收起与进场同源反向播一遍动画**(`…-leave`,160ms,播完才卸载;减动效下立即卸载);**失去焦点(点侧栏外部,不含那枚开合按钮本身)即自动收起**;位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口)。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、**不参与** `isResourceCanvasFloatingPanelOpen` / `resourceCanvasHostGenerationPanelOpen` 的遮挡判据),提交受理后自动展开;工具栏入口按钮是**唯一**开合口、**两个页签(资源管理 / 运行)下都常驻**、排在「依赖 / 类型」排列方式**之前**(动作在前、排列方式收行尾),并显示 `生成任务 · N`。每项的「定位到素材」**每次点击都终局化**(含跨栏目先切栏目、不在投影里给结论、3 秒有界兜底),不允许提示条永久停在「正在定位生成的素材…」;定位成功后必须**把画布视口居中到该卡**——这张画布是 transform 平移的,`scrollIntoView()` 碰不到滚动祖先,只改选中会让用户"定位过去了但依然见不到素材"。
|
||||
|
||||
## 5. 参数口径与复用程度
|
||||
|
||||
- **工具条动作行的间距只有一套**:行内按钮之间统一 24px(行 `gap: 7px` + 按钮左右 `padding: 11px`;「依赖 / 类型」分段控件内部是 0 间距的连通分段,靠 `margin-left: 17px` 与前一按钮保持同样的 24px)。**布局状态提示(`game-resource-reorder-status`)不进这一行**——它是随保存过程变长的文案(空 →「保存中」→「布局已保存」→ 失败原因),作为 flex 子项会把右侧按钮按文本宽度顶开,同一行不同时刻的按钮间距就会不一样;它改为工具条内的绝对定位,位置固定。
|
||||
|
||||
- **复用**:比例 / 尺寸选项与默认值来自网页端美术画布的纯模型 `src/components/image-editor/ImageCanvasGenerationModel.ts`(`EDITOR_IMAGE_DIMENSION_OPTIONS` / `IMAGE_MODEL_NANOBANANA2`),尺寸标签复用 `resolveEditorImageSizeLabel`;提示词上限复用 AGC 自己的 `resourceEditPromptMaxLength`(图片类默认 32000,与 Rust `LOCAL_PROJECT_ASSET_MAX_PROMPT_CHARS` 同口径);「AI 润色」复用 `ResourcePromptPolishSlot`(`polish_local_project_prompt`);面板外壳复用 AGC 的 `ThemedModal`(与「生成素材」面板同一套宿主 chrome)。
|
||||
- **收窄**:本地 IPC 的比例白名单是 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`,网页端还有 `4:3`。前端做的是「主站纯模型 ∩ 本地白名单」,不是另抄一份选项表(测试钉住两者包含关系与 `4:3` 缺席)。
|
||||
- **没有复用到的**:网页端的生成 composer 子视图(`ImageCanvasBasicGenerationComposerView` / `ImageCanvasCharacterGenerationComposerView` / `ImageCanvasIconSpritesheetComposerView` / `ImageCanvasSpecGenerationPanelView` / `ImageCanvasGenerationImageOptionsView`)。原因有二:① 它们的比例选项含本地通道拒绝的 `4:3`;② 它们自带模型选择器、参考图槽位、BFF 直连(`ImageCanvasSpecGenerationPanelView` 直连 `/api/editor/llm/icon-specs/*`,AGC 无该通道),本地 IPC 既没有 `model` 也没有参考图入参,照搬会渲染一批改不了请求的假控件。因此面板是 AGC 自己的薄壳,只把**纯模型**接进来。
|
||||
@@ -71,12 +79,18 @@
|
||||
npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts
|
||||
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasBottomToolbar.test.tsx
|
||||
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasGenerationEntry.test.tsx
|
||||
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationBackgroundClose.test.tsx
|
||||
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationQueue.test.ts
|
||||
npx vitest run apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksPanel.test.tsx
|
||||
npx vitest run apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx
|
||||
npm run agc:typecheck
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
cargo test -p genarrative-ai-game-creator-shell asset_generation_task
|
||||
```
|
||||
|
||||
- 模型层:四个栏目的工具集与顺序、未命中栏目返回空、「所有资源」与 `null` 返回空、二级菜单分流、比例 / 尺寸白名单收窄、前置判据、不可用原因、`outputPath` 策略。
|
||||
- 组件层:工具栏渲染与二级菜单开合、不可用入口可点击 + 原因可关闭、上传回调。
|
||||
- AppSurface 层:进入栏目出现工具栏 / 回总览与展开态消失、每个图片类入口的 `generate_local_project_asset` 载荷、前置缺失可点击说明且零请求、音频入口走 `derive_local_project_resource`、上传走 `upload_local_asset` + 配对读清单、工具栏与右下 Dock 的几何契约。
|
||||
- 生成任务层:`resourceCanvasAssetGenerationTaskModel`(状态机 / 在途判据 / 恢复 / 耗时文案)、`resourceCanvasAssetGenerationQueue`(第二条不发提交 IPC、前一条终态后自动补发、失败与账本丢失都能收口)、`ResourceCanvasAssetGenerationTasksPanelView`(非模态、阶段文案来自后端记录、按 `assetId` 定位)、两块生成浮层在提交期间可关闭、「后台运行并关闭」文案。
|
||||
- Rust 层:`asset_generation_tasks` 的账本落项目内、排队阶段文案由后端拥有、进程重启把无人推进的记录收口为中断失败、账本上限与 task id 边界。
|
||||
- AppSurface 层:进入栏目出现工具栏 / 回总览与展开态消失、每个图片类入口的 `start_local_project_asset_generation` 载荷、前置缺失可点击说明且零请求、音频入口走 `derive_local_project_resource`、上传走 `upload_local_asset` + 配对读清单、生成面板关闭后任务仍在「生成任务」面板且第二条按本地排队补发、工具栏与右下 Dock 的几何契约。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -54,7 +54,7 @@ EditorGenerationResultPersistInput {
|
||||
|
||||
### 首次提交顺序
|
||||
|
||||
1. 校验调用身份、operation 字段、fingerprint、item 数量上限和 slot 唯一性。统一提交最多接受 66 个 item,用于容纳最多 64 个图集切片以及 provider 原图和透明整图。
|
||||
1. 校验调用身份、operation 字段、fingerprint、item 数量上限和 slot 唯一性。统一提交最多接受 258 个 item,用于容纳最多 256 个图集切片以及 provider 原图和透明整图。
|
||||
2. queue 输入必须完整携带 `job_id + worker_id + lease_token + result_payload_json`;inline 输入必须全部省略,禁止半套 guard。
|
||||
3. queue 路径在同一事务快照内校验 job owner、kind、request fingerprint、running 状态和有效 lease;过期 worker 不得写业务结果。`source_entity_id` 必须精确等于本次唯一结果 `project_id`,不得用同 owner 的 job 向其他项目提交。
|
||||
4. 对每个 item 校验稳定 object/resource/asset ID、owner、project、folder、object key、source resource、task 审计字段和媒体字段的交叉一致性。project resource 和 account asset 的 `source_resource_id` 均必须单独验证:来源资源必须是本次同事务候选或已登记资源,属于同 owner,且在结果具有项目上下文时属于同 project;不接受 asset-only 分支绕过血缘校验。
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
- `GET/PUT /admin/api/agc-models` 仅 owner 可用,返回完整配置;PUT 携带上次读取的 revision,冲突拒绝覆盖。
|
||||
- `GET /api/llm/models` 返回启用项的 `id/displayName`、`defaultModelId` 和目录 `revision`,不返回实际模型名、Router 目录、凭据或能力原始数据。
|
||||
- 客户端缓存最近 `revision`,在项目切换 / 对话表面挂载 / 下拉展开 / 窗口聚焦时条件刷新:`revision` 未变化不更新界面,同一时刻只保留一个在途请求,刷新失败保留上一次有效目录与本地选择。发起对话前用同一份快照校验所选模型仍启用,已停用或删除则回退默认模型并提示。
|
||||
- 手动刷新立即显示进行中状态;真实刷新成功后显示完成反馈,即使 `revision` 未变化也有反馈。失败沿用有效缓存时仍显示失败,不能报告刷新成功;HTTP 状态和超时使用可辨认的提示。
|
||||
- 模型目录与其它客户端 JSON API 的成功、失败响应体读取均复用 `readClientHttpResponseText` 的 15 秒上限;响应头已返回但响应体卡住时必须结束本次等待、释放目录在途请求并允许重试,迟到的响应不得覆盖新目录。
|
||||
- AGC Responses 请求的 `model` 是稳定目录标识。服务端按当前目录映射实际模型名;未知、停用项拒绝,不回退其它模型。旧客户端无 AGC 标记时使用后台默认项。
|
||||
- 输入框右下角选择模型,只显示别名;选择保存到客户端配置 `selectedModelId` 与 `selectedModelIsDefault`(当前选择是否来自平台默认项),从下一次请求生效。加载失败或选项停用时禁用提交并允许刷新,不显示实际 ID 作为兜底文案。
|
||||
- `selectedModelIsDefault` 为真表示选择由平台默认项驱动(首次进入、默认项变化、所选模型失效回退),后台默认项变化时客户端跟随切换并提示;用户手动选择后置为假,不再被默认项变化覆盖。
|
||||
@@ -19,4 +21,5 @@
|
||||
- 目录领域校验、未知/停用模型拒绝、客户端响应不包含实际模型名。
|
||||
- 后台鉴权、持久化 revision 冲突处理;客户端选择保存后重新读取,设置保存不覆盖选择。
|
||||
- 目录 `revision` 条件刷新与并发触发去重、发送前回退默认模型、刷新失败可恢复。
|
||||
- 响应体超时保留有效缓存、再次刷新重新请求、迟到响应不覆盖新目录;手动刷新进行中、同版本成功与缓存兜底失败反馈。
|
||||
- AGC/admin-web 类型检查与定向测试、编码检查、Rust 定向检查、schema 一致性与 diff 检查。
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# AI 游戏创作智能体 App 实施计划
|
||||
|
||||
## 资源卡选中工具栏与导出
|
||||
|
||||
- 共享选中工具栏按实际显示的快速编辑、编辑动作、改造、导出与宿主动作组生成分隔线;空组不产生分隔线,不依赖宿主 CSS 隐藏重复线。
|
||||
- AGC 所有具有本地文件路径的素材都显示带文字的「导出」按钮,位于工具栏末组的「删除素材」之前,两者之间不插入分隔线;「重命名」继续保留在前面的常规动作组。工具栏宽度上限为 `min(92vw, 800px)`,窄屏仍可横向滚动。图片、视频、音频、动画、UI、文档及其它文件共用 `isResourceCanvasExportable`,不按媒体类型限制导出;无文件路径及虚拟项目版本不提供文件导出入口。
|
||||
- 导出继续复用 `saveProjectResourcesToDisk`:原生保存对话框选择路径,`save_local_project_asset_file` 复制原始文件字节,不转图片、不重编码、不另建 IPC。后端继续校验源文件、敏感路径和目标路径;取消不写文件,失败通过工作台提示。
|
||||
|
||||
## 文档与代码素材预览
|
||||
|
||||
- 文档与代码素材选中工具栏提供「预览」,打开独立、可滚动的只读弹窗;关闭、切换素材或项目后不残留旧内容。加载中、空文件与读取失败分别呈现,允许重试可重试的错误。
|
||||
- 复用资源预览队列、身份缓存、项目 scope、失效与权限校验,继续调用 `read_local_project_text_preview`。代码卡不做可见性预取,仅用户显式打开详情时读取;不新增 IPC,不扩大可读取文件范围,不增加编辑/保存能力。
|
||||
- 文档正文统一使用现有 Markdown 渲染器;代码文件以按扩展名标注语言的 Markdown 围栏代码块呈现。围栏必须长于正文内的反引号串,正文空行与缩进保持原样,不把源码当 Markdown 正文或 HTML 执行。
|
||||
- 现有 Markdown 渲染器统一提供代码高亮,保留 HTML 禁用及外链/图片安全策略;未知语言回退普通代码块。超大正文跳过高亮但不截断内容,避免流式对话或文件预览被高亮计算阻塞。
|
||||
|
||||
## DirectProject 用户消息契约验证
|
||||
|
||||
`chat_with_game_creator_direct_codex` 必须携带 `projectPath`、`prompt`、稳定的 `clientTurnId` 和完整 `userItem`;`creationType` 与 `attachments` 按实际输入传递。Rust 通过 `projectPath` 解析项目身份,不接收额外 `projectId`。界面测试必须核对 `userItem` 的消息身份、角色、正文及附件内容,拒绝回合用例仍验证实际返回的错误原因。重开项目的历史恢复测试使用 `read_direct_project_history_slice` 的 canonical raw items 与 `hasMore`,首屏 `limit: 20`。资源图和生成任务的读取继续遵守原有工作台恢复逻辑,不因聊天断言失败延迟、关闭或改变它们。
|
||||
|
||||
## 2026-09-16 DirectProject 回合展示唯一归属
|
||||
|
||||
- 交付合同:实时消息、历史回读、工具详情与最终回复先归一为按 `clientTurnId` 唯一的回合,再渲染一次。用户消息始终保留;同一回合的正文、工具和耗时不能从消息、实时尾部、未归属尾部等多个出口重复展示。
|
||||
- 归属来自 `direct-codex:{clientTurnId}:{role}`、文本流中保留的原始 item ID,以及项目历史内明确用户记录之后的 assistant 记录。先在已加载的完整消息集合中关联,再做可见分页;持久历史继续通过 canonical item 切片懒加载,`hasMore` 为真时,未加载回合的工具流不得漂到当前页尾部。禁止将第 N 个有工具回合配给第 N 条用户消息,禁止按文本长度、标点或时间窗猜测归属。缺身份的旧记录保留,不猜造其与其它回合的关联。
|
||||
- 有回合流时正文与工具位置仅来自 item 边界与 `seq`,工具详情按该回合的 `callId` 关联;没有流时同一个回合容器显示历史消息与工具。整轮累计文本仅在活动回合尚无流和持久 assistant 时作兜底,不另建实时消息出口。
|
||||
- 同一文本 item 的更新保持位置不变,段切换、工具边界和回合结束冲刷节流内快照;完成事件必须提交完整 item 内容。回合收尾等待已提交的流写入完成,不用磁盘上偶然可见的最后一段推测最终回复身份。JSONL upsert 先合并旧快照,不能先删旧值再“归并”;跨回合保留按回合时间、回合内按 `seq`,不能用各回合从 1 开始的 `seq` 做全局新旧裁剪。
|
||||
- 工具输入来自 command / MCP arguments,输出来自 aggregatedOutput / output / result,均经过现有脱敏和长度约束。状态相同但详情更新不能被丢弃。
|
||||
- Thread Manager 的原始事件 `seq` 与展示事件的 `sequence` 分开保存,不能将订阅游标用作工具/文本快照的已消费序号。发送队列保存完整结构化输入,上传附件在 Rust 端经过现有校验后并入 canonical user item。
|
||||
- 不改变模型、工具执行权限、项目原始历史或业务数据,不进行远程写入。缺失的历史流不得通过删除用户消息隐藏,也不得将已有原始正文截断为流前缀。
|
||||
- 验收覆盖:新建回合、工具与文本交替、重复快照、最终落盘、失败/中断、历史重开、分页边界、无流历史及工具输入输出。静态检查不等于实机通过;按用户要求不运行测试时,真实事件/UI 验收单独标为待验证。
|
||||
|
||||
## 2026-09-16 DirectProject 可修复错误回传 LLM
|
||||
|
||||
DirectProject 的 AGC 工具、构建、验证和浏览器试玩错误,若不属于鉴权、权限、余额、项目身份、历史损坏、传输断开、取消或付费操作状态不确定等安全终止边界,必须作为脱敏错误上下文回传同一 LLM 会话,由 LLM 读取当前项目、修改真实文件并重跑失败阶段。客户端最多连续反馈三次;每次保留 stage、工具 / 命令、错误正文和已有证据,不得静默吞错、伪造成功或用占位产物跳过阶段。达到三次仍失败后,才向用户投影终态错误和诊断引用。
|
||||
|
||||
## 2026-09-15 DirectProject 长回合平台会话保活
|
||||
|
||||
DirectProject 的生图、素材处理、构建和试玩可能跨越短生命周期 access token 的有效期。普通 `/api/*` 请求和 Codex app-server 已有 401 刷新路径,但 AGC 工具由 Rust 工具桥直接使用客户端当前会话,工具内部的 401 不会自动触发前端刷新。客户端在 DirectProject 回合处于 busy 状态时每 5 分钟调用现有 `requestPlatformSessionRefresh()`;刷新仍复用单飞请求、generation 校验和 native session 安装,不改变凭据来源,也不把 401 降级为成功。刷新失败保持静默,由原始 AGC 工具错误按现有鉴权失败合同返回,避免后台保活覆盖真实错误。
|
||||
|
||||
完成门禁同时允许已登记的普通平台图片作为运行时素材。此前只把 canonical art-spec、背景、图集和图集切片加入来源白名单;`agc_generate_image` 生成的 `assets/neon-*.png` 即使已经登记并被源码引用,也会被判成“未引用平台图片”,触发同一回合的重复修复。浏览器预览把本地图片 URL 改写成 UUID 路径时,验收按每个视口的已渲染本地图片数量与源码引用数量做有界匹配;仍要求两个视口都有对应观察,空视口继续进入修复。
|
||||
|
||||
## 2026-09-12 已有项目打开响应性
|
||||
|
||||
DirectProject 工作区只恢复自身对话,不按专业 Agent 默认任务占位行批量读取旧会话或生成专业 Agent 文本回执。专业 Agent 结果加载 effect 必须以当前 Runtime 模式为边界,并在模式切换时清空旧结果。仍供开发入口使用的 `read_local_conversation` 在 blocking worker 内完整执行权限校验、会话目录解析和历史读取,避免文件访问或锁等待阻塞 Tauri 窗口线程。
|
||||
@@ -57,6 +95,7 @@ npm 游戏的可预览产物固定为对应 package 目录下的 `dist/index.htm
|
||||
- AGC 客户端启动恢复按“读取本地凭据 → 刷新会话(无 token 或失效时)→ 读取当前用户 → Tauri 本地运行时会话安装”阶段执行。界面必须展示当前阶段和已等待时间;不能以无期限的单一 loading 文案隐藏网络或 Runner 故障。
|
||||
- 客户端 HTTP 传输默认使用 15 秒超时并通过独立 `AbortController` 终止请求;调用方可为确需长耗时的请求显式传入 `timeoutMs: null`。调用方主动取消仍保留原始 `AbortError`,超时使用稳定的 `ClientHttpTimeoutError`,由认证层转换为可操作的中文提示。
|
||||
- 会话恢复或本地 Runner 连接超时后必须进入登录页并提供“重试登录状态检查”。重试递增恢复代次并以运行标识忽略旧恢复任务的迟到 UI 写回;不得清除仍可用于后续重试的 access token,也不得重复并发刷新同一服务器的 refresh 请求。
|
||||
- AGC 壳启动认证遇到空响应、非 JSON 维护页或 5xx 时,必须按 HTTP 状态生成可操作的中文提示(503 明确标记服务暂不可用/可能维护),不能退化为 `读取当前用户失败`;后端返回的结构化错误 message 仍优先展示。
|
||||
- Tauri Runner 的启动与 IPC 超时继续以 `runner/protocol.rs` 的 30 秒启动、10 秒读写为权威;启动等待循环会把 endpoint 探测预算裁剪到剩余启动期限,避免单次 ping 把 30 秒门禁延长。考虑复用旧 endpoint 前可能先消耗一次 IPC 等待,前端 UI 兜底取 45 秒,不改变 Runner 协议、启动策略或认证接口。
|
||||
|
||||
## 2026-08-26 运行中自主扩图提案
|
||||
@@ -232,7 +271,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
||||
|
||||
- Windows AppData 安全迁移:首次创建客户端 AppData 时必须以进程 `TokenUser` SID 显式设置 owner,并写入当前用户私有 DACL,不能把可能为 Administrators 的 `TokenOwner` 当作用户身份。发现历史目录 owner 不属于当前 `TokenUser` 时,不在原目录上放宽权限,而是拒绝 reparse point / junction / symlink 后,将旧目录原子重命名到同级唯一 `.owner-mismatch-backup-*` 备份,再新建并验证当前用户 owner 与私有 DACL;迁移或备份失败必须失败关闭,不覆盖旧配置。
|
||||
- Windows 私有文件初始化:父目录已归当前 `TokenUser` 后,新建 `.agent/.manifest.json.lock`、`agent-runner.lock`、endpoint 临时文件、project-owner 诊断临时文件与 real-E2E 私有文件的 owner 仍可能采用 token 默认 owner `Administrators`。manifest 固定锁和 Runner 固定 stale lock 只有在 Windows 不共享独占句柄已取得、且句柄确认普通文件、非 reparse point、链接数为一时才允许初始化或修复为当前 `TokenUser`,随后必须再次复核句柄并按既有 owner/DACL 门禁验证;其它临时文件只允许在本进程 `create_new` 成功且仍持有同一独占句柄时初始化 `TokenUser` owner / DACL,再写入、原子安装并严格复核,初始化失败必须清理刚创建的文件。既有 durable endpoint / diagnostic 读取不得自动接管;活锁不得截断,只有 sharing / lock violation `32/33` 表示占用,access denied 等其它错误立即返回。父进程观察到 Runner 子进程退出后立即返回错误,不等待完整 30 秒 deadline。
|
||||
- Windows ACL 提权边界:自定义 `--config-dir` 的启动前置检查必须把 `managed / user-selected` scope 一并传入提权子进程,不能依赖父进程内存中的配置目录覆盖;native picker 返回的文件或项目目录在同一进程登记短时授权,后续导入 / 项目操作只对登记路径(目录可覆盖其后代)允许 `user-selected` 自动提权,直接伪造 IPC 绝对路径不得获得该能力。项目文件列表 / 索引递归逐项拒绝 symlink 与 Windows reparse point,并在 metadata / read 前先完成 ACL 准备。本进程刚创建的普通文件或目录只在当前进程收紧 owner / 私有 DACL,不因继承 ACE 自动 UAC;UAC 只修复允许范围内、owner 不属于当前用户的已有对象。提权 `Start-Process -ArgumentList` 必须是一条按 Windows 命令行规则加引号的字符串,不能把带空格路径拆成多个 argv。
|
||||
- Windows ACL 提权边界:自定义 `--config-dir` 的启动前置检查必须把 `managed / user-selected` scope 一并传入提权子进程,不能依赖父进程内存中的配置目录覆盖;native picker 返回的文件或项目目录在同一进程登记短时授权,后续导入 / 项目操作只对登记路径(目录可覆盖其后代)允许 `user-selected` 自动提权,直接伪造 IPC 绝对路径不得获得该能力。项目文件列表 / 索引递归逐项拒绝 symlink 与 Windows reparse point,并在 metadata / read 前先完成 ACL 准备。本进程刚创建的普通文件或目录只在当前进程收紧 owner / 私有 DACL,不因继承 ACE 自动 UAC;UAC 只修复允许范围内、owner 不属于当前用户的已有对象。Windows `\\?\` verbatim 路径在受管路径 scope 判断前必须归一化,否则会把同一 AppData 错判为非受管目录并跳过修复。提权 `Start-Process -ArgumentList` 必须是一条按 Windows 命令行规则加引号的字符串,不能把带空格路径拆成多个 argv。
|
||||
- 启动恢复和续跑边界:本条取代上一条中“只有 accepted 才可恢复”的窄口径。若进程在 Supervisor 用户消息已持久、accepted 未持久之间崩溃,只读 preflight 可以把该 `preparing` 识别为可恢复,但不改写 task/conversation;真实 resume 持有 Agent 锁后必须先幂等补写 accepted,再提升为 `pending / queued`。用户消息或 accepted conversation 已落盘而辅助审计失败时,以 conversation 为公开真相继续入队,不留下“已接收但永不执行”的任务;根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。receipt / isolated-join 等带 parent 的 Supervisor continuation 不再另写 Session 终态,只保留单一后端公开事件;`runtime-task-*` 与 `runtime-public-status-*` 共享同 run 的不透明关联摘要,秒级时间戳下多个连续任务必须按实际 run 对应的 `user -> accepted -> terminal` 顺序交错展示。
|
||||
- ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。
|
||||
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
||||
@@ -255,6 +294,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
||||
- 关联验收:快车道必须分别验证 Supervisor 决策前零 child、持久路由后只启动 `code-prototype`、主 Agent 成功 `asset.list` 后才可判断缺口、完整覆盖零图片生成/零委派、精确缺口只委派对应 owner、整体重做仍先审计且不产生无关委派、美术 child 对 `game/**` 写入拒绝而 `assets/**` 允许、回执恢复同一主 Run、主 Agent 自行完成接入/静态 smoke/desktop-mobile 试玩、4200 / 4500 秒累计预算、规范图到 icon-spritesheet 的真实引用、`iconImageSrcs` 本地持久化与资源 ID 绑定、失败续跑目标继承、非占位入口禁止整文件覆盖、纯代码核心画面、猜测单个 atlas 裁切与整图展示失败、四类独立切片可见使用通过与 action-driven `sequence`。完整 GUI / CLI 的固定 16 节点 DAG 另行保持原有回归。
|
||||
|
||||
- 开发态启动必须在 Tauri CLI 之前解析并预检 AGC Vite 最终地址。Linux 使用系统级用户端口段的 `start + 5` 槽位并只在本段内漂移,Windows / macOS 以 `3080` 为兼容首选;启动器通过 `GENARRATIVE_AGC_VITE_PORT` 绑定 `beforeDevCommand` 和配套后端预留,通过 Tauri CLI `--config` 绑定 `build.devUrl`,并通过 Vite CLI `--port` 绑定 `strictPort` 监听。任何竞态中已存在的 AGC Vite、非 HTTP 监听器或其它服务都必须在原生窗口创建前失败关闭。启动器不擅自终止无法证明归属的旧服务,也不得把当前 Rust 壳 / Runner 与其它 worktree 的旧 Vite 前端混用。Tauri CLI 任意退出后,外层启动器必须有界收束已启动的客户端进程树,避免 `beforeDevCommand` 失败后留下假在线窗口。
|
||||
- 开发态还要按后台 Web 的端口约定额外拉起 `apps/admin-web`(Linux `start + 3`,非 Linux 优先 `3102` 并允许漂移,`ADMIN_WEB_PORT` 可显式指定且必须避开已解析的 AGC Vite 端口,`AGC_DEV_ADMIN_WEB=0` 关闭)。后台 Vite 与 AGC Vite 一样由 `start-dev-stack.mjs` 直接持有并在启动器退出时有界收束,不经过 `npm run dev:admin-web`,避免整体重写 `.app/dev-stack.json` 而覆盖配套后端归属状态;端口解析、启动失败或运行中意外退出都只告警,不阻断也不连带停止客户端与配套后端。前端与配套后端就绪后,启动器必须打印一行 `[ai-game-creator-shell] 启动汇总:`,给出前端、后端、后台、数据库与 `bgfilter-worker` 的实际地址,端口漂移后以该行为准。
|
||||
- source-aware lane 的主 Run 或其经授权美术 child 可能在 UI hydration 写回时短暂恢复为 `Pending`。该例外必须从当前 root source、持久工作流决策、单主 route 与 child delegation 解析本轮已开放工作,不得从旧七节点图硬编码重启 Director、验证或试玩节点;未授权 child、第二个活跃美术 child,或缺少成功 `asset.list` 审计的美术委派仍严格失败关闭。
|
||||
|
||||
## Runtime 边界
|
||||
@@ -264,7 +304,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
||||
- 模式合同:客户端 AppData 配置新增全局 `agentMode`,只接受 `codex_cli / provider`。缺省和新安装默认使用 `codex_cli`,原有 HTTP LLM Provider 路径完整保留并可显式切回 `provider`;切换只影响下一次节点请求,不新增 Runner、任务图、会话库、配置库或业务事实源。
|
||||
- 调度边界:正式 DAG、manifest、Agent task/session/run 身份、队列、锁、委派、all-join、完成门、Provider lifecycle、持久 retry/handoff 与 `needs-reconciliation` 继续由现有 AGC Runtime 掌控。每个被调度节点在 `codex_cli` 模式下直接启动一次非交互 `codex exec` 充当该节点的推理 Agent;Codex 返回当前 Runtime 广告函数的结构化调用,Runtime 仍是唯一 ToolHost,不允许 CLI 自己写项目、执行命令、调用 MCP 或形成第二套 revision / verification 真相。
|
||||
- 安装包侧车:Windows x64 release 固定随 Tauri resource 打包 `@openai/codex@0.147.0` 的原生 `codex.exe`;Rust build script 从 AGC 子包锁定依赖 stage 到 resource,并写入版本与 SHA-256 清单。Windows 侧车映射只写入 `tauri.windows.conf.json`,通用 `tauri.conf.json` 不得让 Linux / macOS 构建依赖未生成的 Windows 二进制。运行时只在文件摘要和 `codex-cli` 版本同时匹配清单时优先选内置侧车;缺失、损坏或版本漂移时跳过它,按既有 npm 安装、PATH 顺序回退。安装包同时携带 Apache-2.0 第三方声明;API Key、`auth.json`、Cookie、Token、用户 `CODEX_HOME`、用户配置和项目数据绝不打包。
|
||||
- Windows x64 release 安装包只生成 NSIS,不生成 MSI:`tauri.windows.conf.json` 的 `bundle.targets` 固定为 `["nsis"]`,通用配置继续保留其它平台的默认打包目标。
|
||||
- Windows x64 release 安装包只生成 NSIS,不生成 MSI:`tauri.windows.conf.json` 的 `bundle.targets` 固定为 `["nsis"]`,通用配置继续保留其它平台的默认打包目标。安装后的产品名、开始菜单 / 桌面快捷方式和 EXE 产品描述统一由 `tauri.conf.json` 的 `productName: "陶泥儿"` 生成;应用 identifier 与内部可执行文件名保持稳定。内置 Codex 资源安装到顶层 `coding-agent/win-x64/`,运行时从同一路径查找 `bin/codex.exe` 与 `manifest.json`;仓库 staging 仍使用 `resources/codex/win-x64/`,包内子目录、组件名、版本和完整性校验保持原合同。
|
||||
- CLI 安全边界:CLI 固定使用 argv 启动,禁止 shell 拼接;工作目录使用本次请求专用的空临时目录,不把游戏项目绝对路径写入 prompt、stdout、stderr 或持久记录。调用固定使用 ephemeral、忽略用户配置和 exec rules、read-only sandbox、never approval,并关闭 Codex shell tool;只继承 CLI 运行和认证所需的最小环境,显式移除宿主 `CODEX_API_KEY`。用户级 Codex 登录态继续由本机 Codex 自己读取,API Key、auth 文件、Cookie、Token、`CODEX_HOME` 私有内容不得复制到项目配置、Runtime sidecar、Agent DB、conversation 或日志;stdout / stderr 无换行时也受硬上限约束,stderr 诊断只记录固定分类、字节数和 SHA-256。
|
||||
- 协议边界:Runtime 把既有 `LlmRunRequest` 的消息和当前函数目录编码为有界 prompt,并从同一函数 JSON Schema 生成 Codex structured-output schema。CLI 输出转换为现有 `LlmRunResponse / LlmToolCall` 后,继续经过 native tool / MCP 参数校验、动作上限、权限、pending、receipt、验证与格式修复链;最终回复仍走唯一提交路径,不新增平行响应协议。
|
||||
- 取消与恢复:Codex 子进程绑定当前 Provider request lifecycle,取消、暂停、Runner draining 或 GUI owner 丢失时终止并回收当前进程;started 后没有可信终态仍沿现有 Provider reconciliation 处理。`agentMode`、CLI 可执行身份和影响输出的 Codex 参数进入 `providerConfigFingerprint`,模式切换不得消费另一模式遗留的 retry/handoff。
|
||||
@@ -876,7 +916,7 @@ game-project/
|
||||
- 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-llm` 网关测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke;已退役的 `platform-agent` 不再进入 workspace 或该门禁。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`,且 `llm.stream` 默认开启;旧 Chat Completions 兼容网关设置 `llm.apiKind` 为 `openai_chat`,Anthropic Messages 网关设置 `llm.apiKind` 为 `anthropic`。不支持流式响应的兼容网关可显式设为 `false`。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、Planner 与 Generator 分别命中自己的 `agentLlm` provider 配置、provider prompt 收到图片与音频资产上下文以及最近对话上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。
|
||||
- `npm run ai-game-creator-shell:dev` 必须经受控启动器解析 AGC Vite 端口;Linux 默认使用用户端口段的 `start + 5`,非 Linux 保留 `3080` 兼容首选。启动器用动态 Tauri `build.devUrl`、`GENARRATIVE_AGC_VITE_PORT` 和 Vite CLI `--port` 保证 WebView、`beforeDevCommand`、配套后端预留和 Vite `strictPort` 对齐;不得复用无法证明 worktree 归属的现有 Vite,最终端口被竞态占用时直接失败。
|
||||
- `npm run ai-game-creator-shell:dev` 必须经受控启动器解析 AGC Vite 端口;Linux 默认使用用户端口段的 `start + 5`,非 Linux 保留 `3080` 兼容首选。启动器用动态 Tauri `build.devUrl`、`GENARRATIVE_AGC_VITE_PORT` 和 Vite CLI `--port` 保证 WebView、`beforeDevCommand`、配套后端预留和 Vite `strictPort` 对齐;不得复用无法证明 worktree 归属的现有 Vite,最终端口被竞态占用时直接失败。同一启动器还要带起后台 Web(Linux `start + 3`,非 Linux 优先 `3102`,`AGC_DEV_ADMIN_WEB=0` 关闭),并在就绪后打印含前端、后端、后台、数据库与 worker 实际地址的启动汇总行。
|
||||
- AI 游戏创作 App 的本地后端使用 gitignored 的 `server-rs/.spacetimedb/ai-game-creator/data`,不复用主站旧 standalone 数据目录。启动器从本地 `/v1/identity` 获取并持久化 API identity,再通过数据目录内 `0600` 的独立 `dev-cli/cli.toml` 发布模块;不得读取或覆盖开发者全局 SpacetimeDB 登录,也不得回退到每次变化的 `--anonymous` 身份。发布失败时 API 和 Vite 不得继续启动旧 schema,避免 `external_generation_job` 等缺表订阅进入持续重试。
|
||||
- `start-dev-stack.mjs` 在 POSIX 下以独立进程组托管后端和 Vite,关闭 Tauri 或任一子进程失败时必须收束整组;macOS 不注册仅支持 Windows/Linux 的 api-server 进程指标 observable callback,避免每轮指标采集重复输出平台不支持告警。
|
||||
- Unix 下 Agent DB、External Runner owner 和 tool-plan handoff 的相对句柄复核必须同时比较设备号、inode 和文件类型;`libc::stat` 的 `st_dev / st_ino` 先按 Rust `MetadataExt` 的 Unix 口径规范为 `u64` 再比较,保持 Linux 和 macOS 的同一安全语义,不得为了通过 macOS 编译而删除路径替换检测。
|
||||
@@ -1166,7 +1206,7 @@ game-project/
|
||||
|
||||
- `.agent/manifest.json` 的存储写边界使用同目录持久文件锁跨线程、跨进程串行化;锁必须覆盖旧 manifest 读取、不可变版本前缀校验、临时文件安装和安装后回读一致性校验。锁文件拒绝符号链接、非普通文件和异常所有权 / 硬链接;Windows 使用不共享写句柄,Unix 使用 `O_NOFOLLOW + flock`。旧快照在新版本安装后只能被拒绝,不能覆盖已追加版本。
|
||||
- 后台 Agent 的 manifest 变化以共用 Runtime 状态投影 / 终态 emitter 作为失效因果点:`game-creator-agent-runtime-update` 的 Rust / TypeScript DTO 固定携带 `manifestInvalidated`,且 App 必须在 Supervisor、selected agent、session 和 run 身份的任何 early return 之前处理失效。GUI 进程内 Runtime 直接发该事件;External Runner 是独立进程、没有 GUI `AppHandle`,因此 Runner 协议 v5 的 `runner.attach_gui_owner` 必须登记 GUI 创建的随机 loopback 端口和 64 位随机令牌,Runner 的同一 emitter 通过受令牌保护的短连接转发 `game-creator-manifest-invalidated`。两条路径都只传项目路径与 Agent 身份,不复制 manifest,也不靠轮询补偿。
|
||||
- Direct Codex 不伪造普通 Agent Runtime state。每张平台美术在本地文件与 manifest 提交成功后,统一通过 standalone `game-creator-manifest-invalidated` 发送 `projectPath + direct-codex-art`;只读恢复的已付费源图同样在 `register_local_asset_at` 成功后发送,下载、解码、文件写入或登记失败时不得发送成功失效。前端仍把 `game-creator-agent-progress` 仅用于进度文案;Direct Codex 整体命令成功、失败或超时 reject 后都追加一次 manifest 最终对账,只有完整成功才启动本地预览。
|
||||
- Direct Codex 不伪造普通 Agent Runtime state。每张平台美术在本地文件与 manifest 提交成功后,统一通过 standalone `game-creator-manifest-invalidated` 发送 `projectPath + direct-codex-art`;普通 `agc_generate_image` 同样在生成通道成功返回后、工具结果组装前发出通知,不能只覆盖标准美术包。只读恢复的已付费源图同样在 `register_local_asset_at` 成功后发送,下载、解码、文件写入或登记失败时不得发送成功失效。失效事件匹配当前项目时统一 Windows 盘符、UNC 与对应 verbatim 前缀的写法,实际重读始终使用当前项目保存的路径;该比较仅用于刷新提示,不替代后端路径与权限校验。前端仍把 `game-creator-agent-progress` 仅用于进度文案;Direct Codex 整体命令成功、失败或超时 reject 后都追加一次 manifest 最终对账,只有完整成功才启动本地预览。
|
||||
- App 收到当前项目的 Runtime / relay 失效后重新调用 `get_local_game_manifest`。重读按项目 single-flight 合并事件风暴;读取中再到达失效只追加一轮串行重读,不并发提交同项目响应。应用结果同时校验组件仍挂载、当前项目路径和项目 scope version;项目切换、组件卸载或旧 scope 的迟到响应不得覆盖新项目。Project Supervisor 对外发布前以“revision 前读 -> manifest -> revision 后读”取得一致快照,再通过 `onManifestChange(projectPath, manifest, metadata)` 携带 `projectId + revision + source`;启动器按 `projectPath + projectId` 只接受更高 revision,同 revision 只接受内容一致的重复,旧轮询和同 revision 分叉都不得覆盖。资源列表、依赖图输入、任务状态、运行入口和正式版本卡必须在当前页面实时重投影,不要求关闭或重开项目。集成测试记录“事件未重新打开项目”的调用基线前,必须先等待项目写入最近列表后触发的只读目录状态刷新完成,不能把这项合法后台检查误算成失效事件副作用。
|
||||
- `.agent/agent.db` 有界尾部读取报告截断时,审计 producer 映射失败关闭,不生成基于不完整审计的 producer、task flow 或对应任务环。前端收到截断 DTO 时只剔除 `producerAssignments`、`taskFlows` 与对应 `cyclicTaskIds`;Rust 根据当前 manifest、精确资源引用和仍可信任务深度下限返回的 `dependencyDepths` 继续保留,前端只校验资源仍存在且深度为非负安全整数,不得自行重算或压平权威深度。精确引用边、reference connection index、`cyclicResourceIds` 与 unresolved references 同样继续保留。
|
||||
-- 资源依赖 SVG 继续作为不可交互装饰层隐藏,但 dependency 画布通过 `aria-describedby` 提供当前可见精确引用和任务流的文本等价列表。中央资源聚焦按稳定 `resourceId` 驱动焦点状态:仅 `null -> id` 或 `idA -> idB` 聚焦详情 region,同一 ID 的 manifest 重投影不得抢走音频、视频、链接或关闭按钮焦点;显式收起和 Escape 恢复画布滚动并优先聚焦原触发卡片。聚焦资源被删除时清理 stale focused / selected ID,关闭详情并把焦点落到资源搜索框;项目切换或运行视图切换清除旧恢复意图,不得恢复旧项目卡片。橙色引用线及箭头使用对 `#fffdfa` 画布达到至少 `3:1` 的颜色。
|
||||
@@ -1378,3 +1418,30 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过
|
||||
## 2026-09-14 新游戏策划到真实美术接入的连续交付
|
||||
|
||||
DirectProject 在收到完整游戏策划或游戏制作请求后,必须把视觉素材作为同一交付链路处理:先读取当前项目已登记资源;策划案包含角色、对象、背景、特效、界面或其它视觉实体且现有资源不满足时,Codex 必须在同一游戏实现任务中调用审核的 `agc_tools` 生图或编辑工具,读取返回的资源身份与相对路径,把真实产物接入游戏源码,再构建并验证实际渲染。生成了素材但源码仍使用 emoji、CSS 形状或临时占位图替代策划要求的视觉元素,不能报告游戏完成。只有策划明确不需要视觉素材,或现有已登记素材完全满足需求时,才允许跳过生图;图片生成、处理、登记和接入不因用户没有重复输入“生图”而降级为可选建议。
|
||||
|
||||
## 2026-09-15 AGC 统一错误事件、诊断落库与验收反馈
|
||||
|
||||
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / retryable / occurredAt / elapsedMs / publicText / recoveryHint / detailRef`,其中 `publicText` 是脱敏后的可行动摘要,`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
|
||||
|
||||
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示 `publicText`,点击详情后按 `detailRef` 读取有界、脱敏的诊断,不直接展示私有 `detail`。
|
||||
|
||||
`turn/completed` 等待超时必须区分 `idle-timeout`、`hard-timeout`、`transport-closed`、`failed-turn`、`invalid-terminal` 和 `tool-error`;收到内置工具参数错误后必须结束当前工具调用并进入可行动终态,不能继续使用越界的试玩 `attempt` 或无限等待。试玩次数由客户端按当前 `clientTurnId` 持久化分配,模型不能自由递增;超过上限必须返回一次终态并停止回合。
|
||||
|
||||
游戏素材完成门必须扫描实际参与构建的 `game/` 源码模块,读取 manifest 的登记身份与相对路径,并把构建后的 URL 映射回登记身份。固定素材路径只能作为兼容候选,不能作为唯一准入。已登记且被真实源码引用、被构建纳入并在浏览器证据中观察到的资源通过;未登记、来源不匹配或只存在于设计规范中的资源继续失败关闭。
|
||||
|
||||
验收至少覆盖:普通错误、结构化 app-server failed turn、idle/hard timeout、MCP 参数错误、历史落库失败、脱敏边界、下一轮诊断上下文、源码子模块素材引用、Vite 构建 URL 映射以及试玩次数上限。统一错误事件和诊断落库先于 UI 美化或增加重试预算;不能用延长超时、删除完成门或把失败投影为成功来规避问题。
|
||||
|
||||
## 2026-09-15 Direct 回合跨页面生命周期与运行中项目可见性
|
||||
|
||||
Direct 回合的所有权属于进程内项目身份锁,不属于当前页面。离开工作台或切换到首页时,正在运行的回合继续执行;重新进入项目时,只读活动回合快照负责恢复 clientTurnId 与运行状态,Direct 回合事件负责更新进度。Thread Manager 的 Provider turnId 与事件序列不得当成 clientTurnId 或展示事件序列;其通知只触发快照和历史同步,回放的终态不能重新创建活动回合或“正在提交回复”。定期复核同一份活动快照以弥补页面切换时丢失的结束事件;失败读取保留已知状态,过期读取不能覆盖新回合。活动回合结束后移除快照并解除发送阻断;没有活动回合的项目保持原有发送行为。
|
||||
|
||||
壳层窗口标题栏的“正在运行”下拉入口只呈现活动 Direct 回合快照:常态只显示最后开始的项目,展开后按开始时间排序,显示全部项目的项目名、状态、活动时长并允许进入对应项目。快照读取失败只显示读取失败并保留上一份结果,不得改写成权限、审批或业务失败;入口不建立第二份运行真相。应用重启后的恢复、取消入口和非 Direct Agent 项目不在本合同内。
|
||||
|
||||
活动回合快照命令是进程内 Tauri 只读命令,不进入公共 API 或持久化协议;字段包含 `projectPath / projectName / turnId / status / activity / startedAt / updatedAt / sequence`,状态和序号与既有 Direct 回合进度事件一致。
|
||||
|
||||
### 消息时间与完成回合的过程折叠
|
||||
|
||||
- 用户消息显示发送时刻,精确到秒。新历史信封记录接收时刻 `recordedAt`(Unix 毫秒),原始 Codex item 不增加宿主字段;历史切片通过独立 `itemTimestamps` 映射返回。幂等重复写不刷新时刻,缺时间的旧记录保持未知,不使用打开页面时刻或工具开始时刻补造。
|
||||
- 运行中的正文和工具按原有唯一回合流实时显示;完成后,除最终回复和失败提示外,中间文本与所有工具调用统一放入默认收起的“执行过程”,允许手动展开,刷新或重新进入仍默认收起。
|
||||
- 最终回复沿用 Runtime 的最后一个 assistant item 合同,不按文本长度或相似度判断。失败回合不把最后一句过程输出伪装成最终回复。无流历史按同一用户消息边界划分,只保留最后一条 assistant 回复在外;用户消息与失败提示始终保留。
|
||||
- 验收覆盖已完成回合重进、真实活动回合恢复、跨项目迟到快照、运行到完成自动收起、历史无流、失败、发送时间刷新和旧记录时间缺失。不改变实际工具执行、鉴权、数据库或用户项目内容。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DirectProject Codex 原始历史与异常恢复
|
||||
|
||||
更新时间:`2026-09-11`
|
||||
更新时间:`2026-09-15`
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -16,16 +16,18 @@ DirectProject 只使用 `.agent/conversations/project.jsonl` 作为对话历史
|
||||
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"你好"}]}}
|
||||
```
|
||||
|
||||
`payload` 必须是未经改写的 Responses item。Direct 回合不由浏览器预写用户 message;Codex 返回的 `rawResponseItem/completed.params.item` 原样追加。显式的本地 user/assistant 补写只能通过受权限保护的 `append_direct_project_conversation_message` 命令完成。native 工具、MCP 工具、reasoning、调用参数和调用结果都保留完整内容,不截断、不摘要、不保存 delta/started 事件。
|
||||
`project.jsonl` 的 `payload` 必须是未经改写的 Responses item。AGC 前端 user input 先以 canonical user message item 形式写入;发送给 app-server 前由 Rust 投影为 Codex 可接受的 `message` item,AGC 私有 content part 不会穿透到 wire。Codex 返回的 `rawResponseItem/completed.params.item` 原样追加。native 工具、MCP 工具、reasoning、调用参数和调用结果都保留完整内容,不截断、不摘要、不保存运行态 delta/started 事件。
|
||||
|
||||
Thread Manager 的运行态事件是另一份内存协议:app-server 通知先经过安全投影,再只发送 item 类型、item ID、delta 文本和 turn 终态等必要字段;不得把完整 item、工具参数或调用结果转发到前端。完整 item 仍只通过上述 JSONL 历史读取。
|
||||
|
||||
DirectProject 自己的写侧只写新格式:格式切换(#282)时仍会写旧行的路径已收口——显式 Codex 返回只落在自己的 journal `.agent/conversations/codex-responses.jsonl`,不再投影进 `project.jsonl`。
|
||||
|
||||
读侧白名单兼容旧 `{role,content}` 行:格式切换前,DirectProject 主对话由通用对话写入器落到同一份 `project.jsonl`,存量用户项目的历史文件整份都是这种行。读取时把**明确枚举的那一种**旧行形状(`schemaVersion=game-creator-conversation.v1`、无 `type`、role 在 legacy 写入器自己的角色集合 `user`/`assistant`/`tool` 内、content 为非空字符串)认下来:`user`/`assistant` 投影成与 `direct_project_local_message_item` 同形状的 message item,`role` 与 `content` 逐字节保留;`tool` 行已识别但不进 Codex 上下文(它不是 Responses item,无法还原成真正的工具 item,聊天投影本来也只展示 user/assistant),与 developer/system item 同样过滤。角色集合取的是 `project/conversation.rs` 里那条 `matches!(role, "user" | "assistant" | "tool")` 校验,所以「legacy 写入器能写出的行」被完整覆盖;白名单之外的角色、换了 `schemaVersion`、带 `type`、content 非字符串或为空、缺 `payload`、坏 JSON 仍按损坏失败关闭。这条兼容是只读的,不迁移、不改写历史文件。
|
||||
读取只接受 `response_item` envelope;旧 `game-creator-conversation.v1` 行不提供迁移或 fallback,直接失败关闭。未知或无法投影的 canonical item 在发送前失败,不能产生本轮新增历史。
|
||||
|
||||
## 正常回合
|
||||
|
||||
1. 启动 `ephemeral: true` 线程,并启用 `experimentalRawEvents: true`。
|
||||
2. 新线程先把历史 item 数组一次注入;注入成功后执行新的 `turn/start`。本轮用户 item 只接受 Codex 回传的 `rawResponseItem/completed`,不由 AGC 预写。
|
||||
2. 新线程先把历史 item 数组逐项投影为 Codex 可接受 item 后一次注入;注入成功后执行新的 `turn/start`。本轮 canonical user item 在发送前完成同样的投影校验,再写入项目历史。
|
||||
3. 收到 `rawResponseItem/completed` 后立即追加其 `params.item` 并 flush。
|
||||
4. 正常 `turn/completed: completed` 不生成额外记录。
|
||||
|
||||
@@ -47,7 +49,7 @@ Codex 启动时注入的 `host_skills.instructions`、`permissions.instructions`
|
||||
|
||||
## 恢复
|
||||
|
||||
创建新的 ephemeral thread 后,读取 `project.jsonl` 中所有 `response_item.payload`,按文件行顺序一次调用 `thread/inject_items`,再执行新的 `turn/start`。Codex 负责上下文窗口管理;注入失败直接失败,AGC 不截断、摘要或改写历史。新 thread 已进入连接池但历史读取或注入失败时,必须先从池中淘汰并取消订阅该 thread,重试只能创建新 thread 并重新注入。
|
||||
创建新的 ephemeral thread 后,读取 `project.jsonl` 中所有 `response_item.payload`,按文件行顺序一次调用 `thread/inject_items`,再执行新的 `turn/start`。Codex 负责上下文窗口管理。磁盘上的 canonical 历史永不改写;恢复 wire 载荷会把 MCP `image` block(以及 `input_image` data URL)转换为每张最多 `256 KiB` 的 JPEG 预览,整次恢复图片预算为 `8 MiB`,保留图片证据并避免旧项目把完整 PNG Base64 重复注入。预算耗尽的图片只在 wire 载荷中替换为省略标记。工具新回传图片也在进入 Codex 前执行同一预览上限。除图片二进制预览外,不截断、摘要或改写历史;若其它内容仍超过单行上限,继续失败关闭并指出 `itemId`。新 thread 已进入连接池但历史读取或注入失败时,必须先从池中淘汰并取消订阅该 thread,重试只能创建新 thread 并重新注入。
|
||||
|
||||
`clientUserMessageId` 仅作为 Codex 用户消息的稳定标识随 `turn/start` 发送,不等价于 turn 级 exactly-once 幂等。断线后的重试仍须由项目侧持久化 turn ledger 或服务端去重合同决定,不能仅凭该字段再次执行。
|
||||
|
||||
@@ -55,12 +57,75 @@ Codex 启动时注入的 `host_skills.instructions`、`permissions.instructions`
|
||||
|
||||
DirectProject 的浏览器层只负责显示和乐观状态,不再调用通用对话写入器。历史读写与回合累计分别位于 `agent/direct_project_history.rs` 和 `agent/direct_project_turn_history.rs`。
|
||||
|
||||
`project.jsonl` 不是 DirectProject 独享的写入方:通用对话链(`project/conversation.rs`)把「项目主对话」(`agent_id=None`)映射到同一份文件,非 DirectProject 模式(`codex_cli`/`provider`)的项目对话、以及 Agent Runtime 的项目级公开状态消息(`agent/runtime_state.rs` 的 public status 写入点)都由它追加 `game-creator-conversation.v1` 行。两条链的行形状不同但**互读兼容**:DirectProject 侧投影旧行(见上),通用对话侧跳过 `type=response_item` 且带 `payload` 的行(不把它二次投影成自己的记录,DirectProject 侧已经拥有那份投影),其余坏行两侧都失败关闭。因此同一份文件里出现两种行不会让任何一侧失败。
|
||||
|
||||
这里**故意不给通用对话写入器加「文件已属于 DirectProject 就拒绝追加」的硬报错**:这些写入点不是尽力而为的旁路——`agent/runtime_driver/task_start.rs` 在 `ensure_game_creator_agent_runtime_accepted_public_status_at` 返回 `Err` 时会直接中止本次后台任务(「后台任务启动确认落盘失败,任务未执行」),`agent/runtime_protocol/steering.rs` 的三处调用也用 `?` 上抛。加硬报错会把「旧行噪声」换成「任务起不来」,比它要解决的问题更糟;而毒化本身已经不可能发生——legacy 写入器的行形状与角色集合都被 `conversation.rs` 的校验穷举,全部落在 DirectProject 的读侧白名单内。
|
||||
`project.jsonl` 的 DirectProject 现行合同只允许 `response_item` envelope。其它模式产生的旧 conversation 行不属于本合同,不得注入 DirectProject。
|
||||
|
||||
## 写入与损坏边界
|
||||
|
||||
写入使用 `write_all + flush`。读取时允许丢弃文件末尾一条不完整 JSON 行;白名单化的旧行投影成 message item;其余中间坏行直接失败。兼容只发生在读取侧,不对旧格式做数据迁移或改写。
|
||||
写入使用 `write_all + flush`。读取时允许丢弃文件末尾一条不完整 JSON 行;非 `response_item` 行和无法投影的 item 直接失败,不做数据迁移或 fallback。
|
||||
|
||||
该失败有专门恢复提示,并按不可重试处理:同一份历史文件每次读都会得到同一结论,重试不会改变结果,因此不会向用户显示「可直接重试」。
|
||||
|
||||
## Thread Manager 运行态事件订阅
|
||||
|
||||
DirectProject 的页面不是回合执行的所有者。Tauri 进程内的 Thread Manager 按 thread 维护运行态事件,并允许同一 thread 存在多个独立 subscriber。事件队列只服务运行期间和短期断线恢复,不替代 `project.jsonl` 历史事实源。
|
||||
|
||||
### 公开契约
|
||||
|
||||
概念接口如下:
|
||||
|
||||
```ts
|
||||
subscribe(threadId) -> {
|
||||
subscriptionId,
|
||||
lastCompletedItemId: string | null,
|
||||
events: RawEvent[],
|
||||
}
|
||||
|
||||
consume(subscriptionId) -> {
|
||||
events: RawEvent[],
|
||||
}
|
||||
|
||||
notify -> { subscriptionId }
|
||||
|
||||
readHistory(threadId, { beforeItemId?, limit }) -> {
|
||||
items: CompletedItem[],
|
||||
hasMore: boolean,
|
||||
}
|
||||
```
|
||||
|
||||
`subscribe` 不返回完整历史。`lastCompletedItemId` 只是历史读取锚点,前端自行按 item ID 懒加载需要的历史切片。`events` 是当前运行态重建所需的未完成 item 原始事件,以及当前 turn 的生命周期锚点;前端用同一个 reducer 重放 bootstrap 和后续事件。Rust 不保存或理解前端 reducer state。只要 DirectProject 历史切片返回 `hasMore`,聊天视图必须显示“显示更早的对话”入口,并允许按钮或滚动触发下一页,即使当前可见消息窗口没有隐藏消息。
|
||||
|
||||
`consume` 不接收或返回 cursor。每个 subscriber 在 Rust 内部持有自己的 cursor,并在加锁的临界区内完成过期判断、读取和 cursor 前进。前端只持有 `subscriptionId` 与 reducer state。并发 `consume` 不重复返回同一批事件。
|
||||
|
||||
`notify` 只负责唤醒,不携带事件、cursor 或持久化状态。前端收到通知后调用 `consume`;通知可合并、重复或丢失,事件完整性由 `consume` 保证。
|
||||
|
||||
### 事件和顺序
|
||||
|
||||
Thread 内所有公开事件共用一个单调递增 seq;seq 允许跳号,前端不要求连续。事件 envelope 至少包含:
|
||||
|
||||
```ts
|
||||
{
|
||||
seq: number,
|
||||
type: string,
|
||||
turnId: string,
|
||||
itemId?: string,
|
||||
payload: unknown,
|
||||
}
|
||||
```
|
||||
|
||||
进入 Thread Manager 的是已经完成安全过滤和协议标准化的公开 raw event,不是未经审查的 app-server JSON。事件可交错包含多个并发 item:`item.started`、`item.delta`、`item.completed`、approval/request/resolved 事件,以及 `turn.started`、`turn.completed` 生命周期事件。前端按 `turnId` / `itemId` 分发并 reduce,不需要 item 级 cursor 或第二套 reducer。
|
||||
|
||||
一个 thread 同时最多有一个 active turn;一个 turn 内允许多个并发 item。`turn.completed` 必须在该 turn 的完成 item 均成功持久化后进入队列,前端据此结束运行态;不能用“不存在 unfinished item”猜测 turn 是否完成。
|
||||
|
||||
### 队列、subscriber 和回收
|
||||
|
||||
每个 thread 一个 Vec-based append-only replay queue,使用逻辑 head 偏移清理前缀,不做中间删除。完成 item 的事件在持久化成功后才可进入普通 replay 回收流程;unfinished item 的事件必须保留到 item 完成,不能被普通上限截断。
|
||||
|
||||
队列有内部最大事件数和最大序列化字节数。超限时先标记长期落后的 subscriber 为 expired,并将其移出有效 subscriber 的最小 cursor 计算;随后只能清理队头连续、已无有效 subscriber 需要且所属 item 已持久化的事件。没有 subscriber 时,已持久化完成 item 的事件副本可以直接清理。未完成 item 的事件仍保留。
|
||||
|
||||
subscriber 不依赖 `unsubscribe` 或传输层断开清理。每次 `subscribe` 都创建新的独立 subscription;同一 thread 的其它 subscriber 不受影响。旧 subscription 只有在 queue eviction 后才失效,调用 `consume` 返回统一错误 `SUBSCRIPTION_EXPIRED`。前端保留旧 reducer state,重新 subscribe 完成 bootstrap 后再原子替换。
|
||||
|
||||
### Bootstrap 原子性和恢复
|
||||
|
||||
`subscribe` 必须在同一个 Thread Manager 边界注册 subscriber、捕获 queue 尾部、确定历史锚点和当前运行态事件;bootstrap 期间产生的新事件由该 subscriber 的内部 cursor 继续通过 `consume` 获取,不能丢失。
|
||||
|
||||
断线恢复优先调用 `consume(subscriptionId)`。subscription 仍有效时只返回该 subscriber 尚未消费的 queue 事件;subscription 已过期或 Thread Manager 重启后统一走新的 `subscribe`,再由前端按 `lastCompletedItemId` 从历史懒加载。Rust 不提供 `getItemSnapshot(itemId)`,已完成 item 始终通过历史读取。
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# 【技术方案】GameAgent 对话工具调用卡片(Codex 风格)-2026-09-14
|
||||
|
||||
## 一句话交付
|
||||
|
||||
把 GameAgent 右侧对话面板里的「执行命令 / 写文件 / 调工具」从一行中文进度文本,改成 Codex 桌面客户端那样的**可折叠卡片**(折叠态一行摘要,展开态看命令与文件明细),并且在**刷新页面、重开项目后仍然存在**。
|
||||
|
||||
## 背景与现状(已核实)
|
||||
|
||||
- 数据来源:Codex app-server 会推 `item/started` / `item/completed`,item 里带完整信息(`commandExecution.command`、`fileChange.changes[].path` 等)。
|
||||
- 现状投影:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs` 的 `direct_codex_item_intermediate_text()`(约 791 行)把 item **压成一行中文文本**(`正在执行命令:xxx` / `正在写入文件:xxx`),经 `DirectCodexTurnObservation::IntermediateText` 下发。
|
||||
- 前端事件:`GameCreatorDirectTurnUpdateEvent`(`src/app/types.ts:1090`)只有 `projectPath / turnId / sequence / status / activity / accumulatedText / updatedAt`,**没有结构化工具调用**。
|
||||
- 历史持久化:`.agent/conversations/project.jsonl` 现在只写 message 条目(实测 61 条全是 message),工具调用不留痕。
|
||||
- 历史回读:`read_direct_project_chat_history_at()`(`direct_project_history.rs:575`)只把 `role ∈ {user, assistant}` 且有文本的条目投影成 `LocalConversationMessageRecord`,**形状上装不下工具调用**。
|
||||
- 结论:要做成卡片必须同时改「采集 → 传输 → 持久化 → 回读 → 渲染」五段,纯前端做不出来。
|
||||
|
||||
## 契约(实现必须照此,不得自行改形状)
|
||||
|
||||
### 1. 工具调用条目(采集与持久化形状)
|
||||
|
||||
新增独立历史文件:`<projectRoot>/.agent/conversations/tool-calls.jsonl`,一行一条,行信封与既有历史一致:
|
||||
|
||||
```json
|
||||
{ "type": "tool_call_item", "payload": { "schemaVersion": "agc-tool-call.v1", "id": "...", "turnId": "...", "kind": "command|file_change|mcp_tool|web_search|context_compaction|other", "title": "执行命令", "summary": "npm run build", "status": "running|completed|failed", "detail": { "command": "...", "output": "...", "changes": [{ "path": "game/src/x.ts", "kind": "add|update|delete" }] }, "startedAt": 0, "updatedAt": 0 } }
|
||||
```
|
||||
|
||||
- `id`:Codex item 的 id;同一 item 的 `started` 与 `completed` 必须落成**同一条**(按 id 幂等 upsert,不允许写两行)。
|
||||
- **状态单调**:同一 `id` 的每条快照按 `updatedAt` 合并落盘——`updatedAt` 更旧的快照不得覆盖更新的 `status` 与 `updatedAt`。逐条快照落盘与回合末整批落盘两条路径会并发竞争,后到的旧快照不能把已经 `completed` / `failed` 的卡片打回 `running`;`updatedAt` 相同时终态优先;`startedAt` 取最早的非零值(`item/completed` 不一定带 `startedAtMs`)。
|
||||
- `title` 是折叠态的一行标题,按 kind 固定:`command` → `执行命令`、`file_change` → `编辑 N 个文件`(N = changes 去重后数量)、其余见 kind 枚举。
|
||||
- `summary` 是折叠态标题后面的短摘要:命令取命令首行(截断 120 字符),`file_change` 取首个变更路径。`summary` 的每个来源(命令、变更路径、`tool`)都必须先脱敏再落盘。
|
||||
- `detail.command` 读取原生命令或 MCP `arguments`,`detail.output` 读取 `aggregatedOutput` / `output` / `result` / `error`;对象格式化为 JSON,先脱敏再各截断到 4000 字符。展开工具行分别显示“输入”“输出”。MCP 摘要保留工具名,不把 JSON 开头的 `{` 当成摘要。状态相同但详情变化也必须更新;完成快照缺少输入字段时保留开始快照的输入。
|
||||
- **路径形状**:`detail.changes[].path` 用**项目相对路径**(如 `game/src/x.ts`,分隔符统一成 `/`);项目外的绝对路径落成 `<absolute-path>` 占位。任何情况下都不得写出项目根目录本身、用户家目录或绝对路径的原始值。
|
||||
- **必须脱敏**(落盘前统一走 `agent/direct_tool_calls.rs` 的 `sanitize_detail_text`,顺序:项目路径归一化 → `redact_absolute_path_tokens` → `redact_secret_tokens` → `sanitize_error_context`):
|
||||
- 前缀型密钥沿用 `redact_secret_tokens`(`sk-…`、`tnr_sk_…`、`ghp_…`、`AKIA…`、`eyJ…` 等);
|
||||
- 键值型凭据沿用 `sanitize_error_context`(= `redact_secret_tokens` + `redact_error_sensitive_assignments` + `redact_error_bearer_values` + `redact_error_config_names` 的既有组合),覆盖 `Authorization: Bearer …`、`Cookie: session=…`、`api_key=…`、`client_secret=…`、`token=…` 等形状;
|
||||
- 含 `--password` / `--token` / `--secret` / `--api-key` 这类敏感 CLI 标志的行按既有 fail-closed 约定**整行**替换成 `[redacted sensitive context]`(与 `sanitize_agent_runtime_text` 一致;即使标志后面只是 `$VAR` 占位符也整行替换,占位符本身不会保留);
|
||||
- 脱敏必须幂等(同一段文本连跑两次结果一致),且不得把未脱敏文本写进 `detail` / `summary`。
|
||||
|
||||
### 2. 实时事件(新增字段,不改既有字段语义)
|
||||
|
||||
`GameCreatorDirectTurnUpdateEvent` 增加**可选**字段:
|
||||
|
||||
```ts
|
||||
toolCalls?: DirectTurnToolCall[] | null;
|
||||
```
|
||||
|
||||
- 只有在本回合工具调用集合发生变化时才带(不要每个 heartbeat 都重发全量)。
|
||||
- 字段**可选**:老版本事件解析路径必须保持兼容(前端拿到 `undefined` 时行为与现在一致)。
|
||||
- `DirectTurnToolCall` 与上面 payload 同形(去掉 `turnId`)。
|
||||
|
||||
### 3. 回读命令
|
||||
|
||||
新增 Tauri 命令 `read_direct_tool_calls(projectPath)`,返回按时间正序的 `DirectTurnToolCall[]`。
|
||||
|
||||
- **上限语义**:200 条是「按时间保留最新 200 条」。超出时更早回合的卡片会被**静默丢弃**(老回合卡片会消失),不做分页、不做历史回填;同一 `id` 的多条记录先按 `updatedAt` 合并,再按时间正序裁剪。
|
||||
- 历史文件缺失 → 返回空数组,不报错。
|
||||
- 单行损坏 → 逐行读字节并逐行解码,跳过该行继续,不整体失败;只有损坏字节与下一行黏成一行(例如写入被截断、缺失换行)时,被丢掉的也只是那**一行**,其后的合法记录必须继续读回(与 Codex item 流一样是"尽力而为"的展示数据,不是业务真相)。
|
||||
|
||||
### 4. 前端合并与渲染(回合唯一归属,连续工具成块)
|
||||
|
||||
- 加载对话时按 `turnId` 归并为唯一回合容器,用户消息保留在该回合前部。有 `turn-stream.jsonl` 时,文本与工具按 item `seq` 交替,连续工具合为一块,遇到文本另起一块;没有流的历史回合才采用“工具块 + 历史正文”。
|
||||
- 回合完成后,中间文本及所有工具块统一收进默认关闭的“执行过程”;最终回复及失败提示留在外面。展开后仍按原顺序查看中间输出和工具详情;运行中不使用外层折叠区。用户消息的发送时间从消息自身的历史时间读取,不能拿工具起点补造。
|
||||
- 实时与回读共用同一投影,正文、工具和耗时不另建实时/未归属渲染出口。先在完整历史按消息身份关联,再分页;禁止按第 N 个工具回合匹配第 N 条用户消息。详情通过当前回合 `callId` 关联;同项目回读与实时增量幂等合并,切项目清空旧状态。完整合同见 [AGC 实施计划](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的“DirectProject 回合展示唯一归属”。
|
||||
- 块 DOM 与交互(对齐 Codex):
|
||||
|
||||
```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秒">
|
||||
<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>
|
||||
<svg class="agent-tool-call-group-chevron" aria-hidden="true"></svg>
|
||||
</button>
|
||||
<div class="agent-tool-call-group-body" hidden>
|
||||
<ul class="agent-tool-call-group-rows">
|
||||
<li class="agent-tool-call-group-row" data-testid="agent-tool-call-row" data-kind="command" data-duration-ms="12300">
|
||||
<button type="button" class="agent-tool-call-row-head" aria-expanded="false" aria-controls="…"
|
||||
aria-label="已运行 npm run build,耗时 12.3s">
|
||||
<span class="agent-tool-call-row-icon" aria-hidden="true"></span>
|
||||
<span class="agent-tool-call-row-text">已运行 npm run build</span>
|
||||
<span class="agent-tool-call-row-duration">12.3s</span>
|
||||
<svg class="agent-tool-call-row-chevron" aria-hidden="true"></svg>
|
||||
</button>
|
||||
<div class="agent-tool-call-row-detail" hidden>
|
||||
<pre class="agent-tool-call-row-command">…</pre>
|
||||
<ul class="agent-tool-call-row-changes"><li><span>game/src/x.ts</span><small>新增</small></li></ul>
|
||||
<pre class="agent-tool-call-row-output">…</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
- 文案规则(按 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`(发送 → 结束),取不到就只显示结束时间,不编造。
|
||||
- 回合结束时间与耗时在正文下方右对齐;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 输入原样展示;执行参数、持久化原文、脱敏和输出均不改变。
|
||||
- 调试属性:块与行都带 `data-duration-ms`(原始毫秒,无法计算时为空串)与稳定 `data-testid`(块 `agent-tool-call-group`、行 `agent-tool-call-row`)。
|
||||
- 必须用 `<button aria-expanded>` + `hidden` 控制展开(键盘可达、可读屏),块头与行都是按钮:`aria-label` = 汇总 / 行文案 + 耗时;默认折叠。
|
||||
- 输入框、消息气泡、消息列表滚动模型**不变**;块只是消息流里的一个块。
|
||||
|
||||
## 验收判据(每条都要有可复现证据)
|
||||
|
||||
1. 一轮真实回合里,`tool-calls.jsonl` 里同 id 只有一行,`completed` 后 `status` 变 `completed`。
|
||||
2. 刷新页面 / 重开项目后,工具调用卡仍在原位(不重复、不丢)。
|
||||
3. 实时回合中卡片状态从 `running` 走到 `completed`。
|
||||
4. 折叠/展开键盘可达(Tab 到头部按钮、Enter/Space 切换、`aria-expanded` 同步)。
|
||||
5. `Read` 到的既有对话(无工具调用)行为不变;老事件(无 `toolCalls` 字段)行为不变。
|
||||
6. 脱敏检查:`tool-calls.jsonl` 里不出现 API Key / Token / 绝对用户目录。
|
||||
|
||||
## 不做项(本次明确不做)
|
||||
|
||||
- 不做工具调用的重试 / 取消 / 编辑按钮。
|
||||
- 不做 diff 级展开(只在展开态列文件路径与变更类型)。
|
||||
- 不做工具输出的完整回放、不做卡片内搜索。
|
||||
- 不改 `.agent/conversations/project.jsonl` 的既有格式与注入 Codex 上下文的路径(新数据走独立文件,避免污染模型上下文)。
|
||||
- 不改工具调用之外的聊天渲染(markdown、气泡、滚动)。
|
||||
- 不做历史工具调用的迁移回填(存量项目没有卡片,属预期)。
|
||||
|
||||
## 涉及文件(实现边界)
|
||||
|
||||
- Rust(`apps/ai-game-creator-shell/src-tauri/src/`):`agent/codex_app_server.rs`(item → 结构化采集)、`agent/direct_runtime.rs`(随事件下发 + 回合结束持久化)、`agent/direct_project_history.rs` 或新增 `agent/direct_tool_calls.rs`(upsert 与回读)、`commands.rs`(新增命令注册)。
|
||||
- 前端(`apps/ai-game-creator-shell/src/`):`app/types.ts`(新增字段与类型)、`App.tsx`(订阅、累加、加载时合并)、`features/agent-runtime/` 或新增 `features/project-workspace/ToolCallCard.tsx`(卡片组件)、`styles.css`(卡片样式,新样式集中在文件末尾中文注释区块)。
|
||||
- 测试:Rust 侧单元测试(upsert 幂等、状态按 `updatedAt` 单调合并、截断、脱敏覆盖与幂等性、项目相对路径形状、非法 UTF-8 损坏行跳过、200 条上限保留最新)、前端 `tests/`(卡片渲染、折叠交互、重开项目后合并、老事件兼容)。
|
||||
|
||||
## 实施顺序(每步都要能独立验证)
|
||||
|
||||
1. Rust 采集 + 独立文件 upsert:先只落盘,单元测试证明幂等与截断。
|
||||
2. 事件字段下发 + 前端类型:老路径不受影响(回归现有 appSurface 用例)。
|
||||
3. Tauri 回读命令 + 前端加载合并:刷新后卡片存在。
|
||||
4. 卡片组件 + 样式 + 无障碍:折叠/展开 + 键盘。
|
||||
5. 真实回合联调(本地 Tauri 起一轮),截图留证。
|
||||
@@ -342,12 +342,12 @@ UI 使用“批准”和“继续修改”两个文字按钮,分别配 Lucide
|
||||
|
||||
## 14. 开发调试入口
|
||||
|
||||
项目运行模式通过 `.agent/runtime-mode.json` 持久化。新建策划项目在进入工作台前写入 `design`;“做成游戏”写入 `game`。重新打开项目时通过 `get_design_agent_runtime_mode` 读取模式,完成后一次性挂载对应工作台,不能先挂载 GameAgent 再切回策划。旧项目缺少模式文件但存在策划会话时按 `design` 恢复;明确的 `game` 标记优先于残留策划会话。无模式也无策划会话的项目仍使用游戏工作台。
|
||||
项目运行模式通过 `.agent/runtime-mode.json` 持久化。新建策划项目在进入工作台前写入 `design`;“做成游戏”先登记 `design_artifacts` 中尚未登记或登记信息已变化的策划产物,若 manifest 实际发生变化则在同一项目写锁范围内推进一次项目 revision,再写入 `game`。重复切换不重复登记或推进 revision。重新打开项目时通过 `get_design_agent_runtime_mode` 读取模式,完成后一次性挂载对应工作台,不能先挂载 GameAgent 再切回策划。旧项目缺少模式文件但存在策划会话时按 `design` 恢复;明确的 `game` 标记优先于残留策划会话。无模式也无策划会话的项目仍使用游戏工作台。
|
||||
|
||||
开发构建的策划工作区页头在“刷新”旁提供“快速准备做成游戏测试”按钮。该入口与策划 Debug 日志共用 `GENARRATIVE_AGC_DESIGN_DEBUG=1` 开关:开关未启用时按钮不显示,命令也不可执行。入口仅进行本地 fixture 和会话状态写入,不调用 Provider;完成后自动刷新文件树与阶段,通过 `design-agent-update` 状态事件同步右侧审批/阶段操作区。随后仍需点击正常的“做成游戏”按钮执行资产登记与运行时切换。
|
||||
开发构建的策划工作区页头在“刷新”旁提供“快速准备做成游戏测试”按钮。策划 Debug 日志和该入口共用运行时环境变量 `GENARRATIVE_AGC_DESIGN_DEBUG=1`;前端通过 Tauri 查询当前构建是否具备 Debug 入口,只有 Debug 构建且变量启用时显示按钮,命令也不可执行。入口仅进行本地 fixture 和会话状态写入,不调用 Provider;完成后自动刷新文件树与阶段,通过 `design-agent-update` 状态事件同步右侧审批/阶段操作区。随后仍需点击正常的“做成游戏”按钮执行资产登记与运行时切换。
|
||||
|
||||
## 15. 策划 Agent reasoning 展示现状
|
||||
## 15. 策划 Agent reasoning 展示
|
||||
|
||||
右侧栏已预留策划 Agent 的 `reasoningText` 事件字段和默认折叠的展示样式,但当前 Provider 解析链仍会过滤 reasoning 内容,尚未向策划 Runtime 产出该字段。因此现阶段只展示用户可见正文和工具状态;reasoning 折叠区在没有数据时不会出现。
|
||||
策划 Agent 的 Provider 请求显式开启 `capture_reasoning`,共享 `platform-llm` 将 Chat / Responses 的 reasoning 通过独立字段旁路传递,策划 Runtime 映射为已有 `DesignEvent.reasoningText`,前端复用右侧栏默认折叠的思考过程展示。正文、工具调用参数、正式 assistant message 和会话 history 继续使用原有字段;GameAgent、Direct/Codex 与通用 response stream 保持只消费正文的行为。
|
||||
|
||||
后续若补充 reasoning,需要在策划 Agent 专用 Provider 解析层接入,不能直接修改共享 Provider 以免影响 GameAgent。
|
||||
reasoning 捕获默认关闭。新回合和 Provider 重试会先清空同一响应槽的临时 reasoning,失败路径也会清理,避免旧内容残留。该能力不新增公开 API、SpacetimeDB 字段或独立 UI 组件。
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
| **S9** 选择与多选 | 单击卡片 → `Shift`/`Ctrl`/`Meta` 再点 → 空白处拖拽框选 | 点卡 = 选中并在卡片旁浮出工具条;三种修饰键等效追加;框选可见选框 | L51、L61–62、L318–319 | `data-resource-card-id` + class 含 `is-selected` + `aria-pressed="true"`;浮出容器 `role="toolbar"`(图片 `aria-label="图片工具栏"`,音频 `"素材工具栏"`) | ⚠️ **PRD L61–62 要求的「打开详情」按钮在代码中不存在**(测试显式断言为 `null`),按 Issue #309 C3「取消资源详情面板与全屏编辑路由」判,不要判为缺失;真正入口是 `aria-label="选中资源:<栏目> <名>"` 按钮。**只读信息浮层已回归**(2026-09-11,工具条「信息」动作,见 `decision-log`):**C3 取消的仍只是可编辑详情面板与全屏编辑路由** |
|
||||
| **S10** 快速编辑(图片) | 选中 PNG/JPEG/WEBP 卡 → 工具条「快速编辑」→ 输入 → 提交(提示词下方另有「AI 润色」) | 先 `normalize_local_project_raster_resource`,再 `derive_local_project_resource(editKind='image-reference')` → 服务端 `/api/editor/images/edits`;**产出一张新素材**,源素材与源文件不变,新资源带 `referenceResourceIds` 血缘 | L123、L193、L476–478、L557;#309 C3/C10 | 提交后 `assets[]` +1、卡片 +1 且 `data-resource-card-id` 为新 id;**源卡仍在且内容未变**;工具条真渲染的只有 `quick-edit` 与 `download`;「AI 润色」`aria-label="AI 润色"` + `aria-busy`,成功回填并出现「恢复原文」,失败保留原文并给「AI 润色失败,可重试」 | ① 服务端白名单 17 项(`editor_project.rs:4120-4141`),`character-animation`/`video`/`audio`/`document`/`code` 等**仍会 400**;② 工具条 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画)按 opt-in **不渲染**;③ 润色结果超 `resourceEditPromptMaxLength` 会**截断并提示「已按长度上限截断」**;④ 提示词变了(改写或润色回填)必须换 `operationId`/幂等键,否则 Rust `request_fingerprint` 判「已绑定到不同资源编辑请求」 |
|
||||
| **S11** 画布生成入口(视频) | 画布级入口 → 只呈现视频 → 填名与提示词 → 提交(提示词下方另有「AI 润色」) | 无源新建视频 → 新卡产出并定位(音效 / 背景音乐已移到栏目工具栏,见 S11a) | 飞书 §4(首轮由对话启动)、L122 | 提交后 `assets[]` +1;面板是独立浮层(非在当前面板下方追加);「AI 润色」走 `polish_local_project_prompt` 并带上「这是素材生成提示词(类型)」的场景约束,成功回填、失败保留原文 | ⚠️ **Issue #309 写「画布生成入口未接」,但当前代码已接三类**(`resourceCanvasGenerationModel.ts` 仅 `video`/`sound-effect`/`background-music`);图片等类型会直接报「当前资源类型不支持无源生成」;润色结果超该类型上限(背景音乐 140 / 音效 1900 / 视频 4000)会截断并提示「已按长度上限截断」 |
|
||||
| **S12** 编辑标签 | 选中已登记素材 → 工具条「编辑标签」→ 增删标签 → 保存 | 面板**只编辑 `manifest.assets[].tags`**;`category` 读一次权威值后**原样回传**;顶部没有分类 chip 排 | L360(2026-09-11 收口)、#309 C2 | 面板标题「编辑素材标签」,底部只有「取消 / 保存标签」;保存后 `assets[n].tags` 变化而 `assets[n].category` **逐字不变** | 面板内的「删除资源」按钮是**唯一**删除入口,保留;「用户手动设置 category」这项能力已移除 |
|
||||
| **S12** 编辑标签 | 选中已登记素材 → 工具条「编辑标签」→ 增删标签 → 保存 | 面板**只编辑 `manifest.assets[].tags`**;`category` 读一次权威值后**原样回传**;顶部没有分类 chip 排 | L360(2026-09-11 收口)、#309 C2 | 面板标题「编辑素材标签」,底部只有「取消 / 保存标签」;保存后 `assets[n].tags` 变化而 `assets[n].category` **逐字不变** | 面板内的「删除资源」按钮保留(删除入口另见 S15 的选中卡工具条);「用户手动设置 category」不再放在本面板,改为独立入口(工具条「素材类型」与信息浮层「分类 → 设置」,选中即落盘,见 decision-log 2026-09-14) |
|
||||
| **S13** 重命名 | 工具条「重命名」→ 新文件名 → 确认 | 磁盘文件名与 manifest `localPath` 更新;**asset id 不变**;写盘失败回滚文件 | L362(资源身份稳定)、L515 | 弹窗 `ariaLabel="重命名素材"`;改名后「选中资源:…」按钮的无障碍名同步变化;引用 chip 与 @ 候选列表显示名刷新 | ⚠️ **不改游戏源码里对旧 `assets/<name>` 的引用**,后果由用户自负 |
|
||||
| **S14** 下载 | 选中可下载卡 → 工具条「下载」 | 弹出**原生保存对话框**选目标路径 → 分块复制;取消即停止 | L507、飞书 §11.1.2 | 出现 `title="保存素材"` 的原生对话框;成功后状态提示含 `已保存 <路径>` | 语义是"另存为",不是静默直下;虚拟版本条目没有文件、不放行 |
|
||||
| **S15** 删除 | 工具条 / 标签面板「删除」→ 确认弹窗 | 三分支:① 无引用→直接删;② 被引用未勾选→只删素材、版本保留**悬空绑定**、界面不合成幽灵资源卡;③ 勾选→素材与该批版本在**同一次 manifest 写入**内一起删 | L413(版本只追加的唯一例外)、L532–533;#309 C5 | 弹窗 `ariaLabel="确认删除资源"`;被引用时出现 `被 N 个游戏版本使用` + 版本列表 + checkbox `把相关游戏版本一并删除`;无引用时该 body 整块不渲染 | **只摘登记、不删磁盘文件**;读引用命令精确名是 `read_local_project_asset_references` |
|
||||
| **S15a** 替换素材(含点选替换) | 选中被**当前版本**绑定的素材 → 工具条「替换素材」→ 候选弹窗(弹窗内可筛可选);再点 footer「点选替换」→ 弹窗关闭、进入画布点选态 → 在画布上直接点目标素材 | 弹窗确认与点选是**同一条**写入链路(`replace_local_project_version_resource`,载荷逐字一致):改该版本绑定、不建新版本;点选态下合法目标直接提交并自动退出点选态,非法目标零写入、**留在**点选态并在提示条上说明原因 | PRD §5.3 / §7.8 第 8 条;#309「直接替换」口径 | 点选态判据:候选弹窗 `role="dialog"` 已卸载;画布出现 `.game-resource-canvas-pick-hint`,文案含「在画布上点选要替换成的素材」与「点击空白处不会退出」,并有「取消」按钮;非法目标后提示条仍在且出现 `role="alert"`(「替换素材不兼容:分类不同」/「替换素材与源素材相同」/「替换素材未登记或已被删除」);点选期间卡片 `aria-pressed` 不变(单击只用于点选)、源素材选中与工具条不因点空白或点非法目标而丢;Esc 退出点选(画布全局 Esc 的清选中不随之触发);点选态下滚轮平移与缩放照常可用 | 点选只能点**当前画布上可见**的卡:目标在别的栏目时先切栏目(点选会话跨栏目存活,不因「收起资源」或切栏目结束);空白点击不退出、也不清画布选中 |
|
||||
|
||||
| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `generate_local_project_asset`,音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar="<category>"]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `generate_local_project_asset`,载荷逐字为 `{ projectPath, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条);写入命令之后有 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 |
|
||||
| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `start_local_project_asset_generation`(提交即返回、生成在后台跑),音频入口复用既有无源生成链路,上传复用 `upload_local_asset`;生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar="<category>"]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `start_local_project_asset_generation`,载荷逐字为 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条;`taskId` 是前端每次提交新铸的本地任务 id);该命令提交即返回,之后有 `list_local_project_asset_generations` 轮询与 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;提交期间点 × / 遮罩 / Esc /「后台运行并关闭」任一都能关面板且请求继续(画布立即恢复可交互),关闭后任务仍出现在「生成任务」面板(入口按钮 `aria-label="生成任务"`,非模态浮层、无 `aria-modal`)并显示后端 `phaseDetail`;第一条未终态时提交第二条 → 第二条显示「排队中。」且生成提交 IPC 次数仍为 1,第一条终态后自动补发(次数变 2);缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 |
|
||||
|
||||
### D 阶段 · 版本与运行
|
||||
|
||||
@@ -101,6 +101,8 @@
|
||||
|
||||
先记住状态枚举:`data-preview-kind` ∈ `raster-image | media-image | video | audio | code | document | version | placeholder`;`data-preview-status` **只有 `idle | loading | loaded | failed` 四个值,不存在 `unsupported`**。"不适用"不是状态,表现为**永远 idle**。
|
||||
|
||||
棋盘格底另有一条独立判据:`data-preview-has-alpha` **只在预览已加载且这张图真的带 alpha 通道时才写 `'true'`**(原生头部判据:PNG colorType 4/6 或 `tRNS`、WebP alpha 标志;JPEG 恒不透明)。缺属性与不透明同档,卡面退回纯色底。因此「真透明底」与「AI 把棋盘格画进像素里」不再同形:**看 `data-preview-has-alpha` 而不是看卡面有没有棋盘格**。
|
||||
|
||||
```js
|
||||
// ① 全量分布(首选)
|
||||
[...document.querySelectorAll('.game-resource-card[data-preview-kind]')]
|
||||
@@ -120,6 +122,11 @@
|
||||
[...document.querySelectorAll('.game-resource-card[data-preview-status="loaded"]')]
|
||||
.filter(e => !e.querySelector('.game-resource-card-visual img, .game-resource-card-visual video'))
|
||||
.map(e => [e.dataset.resourceCardId, e.dataset.previewKind])
|
||||
|
||||
// ⑤ 真假透明底:已加载的图片卡按 alpha 分组
|
||||
// 真透明(true)才应有棋盘格底;AI 画了棋盘格的不透明图这里必须是 undefined
|
||||
[...document.querySelectorAll('.game-resource-card[data-preview-status="loaded"][data-preview-kind="raster-image"], .game-resource-card[data-preview-status="loaded"][data-preview-kind="media-image"]')]
|
||||
.reduce((m, e) => { const k = e.dataset.previewHasAlpha ?? '(无 alpha 判据)'; m[k] = (m[k] || 0) + 1; return m; }, {})
|
||||
```
|
||||
|
||||
| 现象 | 说明 | 第一眼做什么 |
|
||||
@@ -128,6 +135,7 @@
|
||||
| `failed` + `data-preview-error` | 读取或解码真失败,读 `data-preview-error` 原文 | `retryable=false` 表示永久失败(超尺寸、损坏、类型不支持、不安全 SVG);可见性预取遇 failed 会直接放弃,不会自愈 |
|
||||
| `loaded` 但无主体节点 | 原生返回的 dataUrl 为空、或解码不出画面 | 这类**不报错**,只能靠脚本 ④ 抓 |
|
||||
| `placeholder` + `idle` | **不适用**(游戏代码、无法识别的二进制产物等),正常 | 不要当 bug 报 |
|
||||
| 卡面有棋盘格但 `data-preview-has-alpha` 缺失 | **这张图其实不透明**:棋盘格是图里画进去的(旧构建会额外再铺一层卡面棋盘格,两者叠在一起) | 脚本 ⑤ 分组确认;真透明卡才允许有 `'true'` |
|
||||
|
||||
**IPC 侧**:预览只走 4 条命令 `read_local_project_image_preview`、`read_local_project_media_preview`、`read_local_project_text_preview`、`cancel_local_project_resource_preview_scope`。媒体那条的 `category` **只接受 `art` / `audio`**,传栏目值会被原生拒绝,表现是卡片全空、没有缩略图。
|
||||
|
||||
@@ -238,7 +246,7 @@ api-server 是否本次重启:□ 是 □ 否
|
||||
### 7.3 已知未做 / 已取消(不要报成缺陷)
|
||||
|
||||
1. **C6 的候选素材 / 替换关系 / 替换队列 / Agent 审核 / 批量提交整条取消**(#309)。PRD §5.3 的 `ProjectVersionResourceReplacement` 与 §3.2 的「创建下一迭代版本」**当前不实现**(2026-09-11 用户 DDL 口径「替换这块先做成直接替换」),但**保留为未来合同**:今天的实现是「改该版本的绑定,不建新版本」,验收见 PRD §7.8。
|
||||
2. **画布生成入口分为两条**(2026-09-13 起):既有「生成素材」浮层入口只保留视频;音频(背景音乐 / 音效)与图片类(生成图片 / 生成规范 / 生成角色形象 / 生成图标素材 / 生成 UI 设计图)改由栏目画布左下角的底部工具栏承载,走本地 `generate_local_project_asset`。工具栏之外的其它无源生成类型仍会直接报"当前资源类型不支持无源生成",这是设计而非缺陷。见 PRD §3.10 / §7.9 与下面的 S11a。
|
||||
2. **画布生成入口分为两条**(2026-09-13 起):既有「生成素材」浮层入口只保留视频;音频(背景音乐 / 音效)与图片类(生成图片 / 生成规范 / 生成角色形象 / 生成图标素材 / 生成 UI 设计图)改由栏目画布左下角的底部工具栏承载,走本地 `start_local_project_asset_generation`(提交即返回、生成在后台跑完写回项目,任务账本在项目内 `.agent/runtime/asset-generation-tasks/`)。工具栏之外的其它无源生成类型仍会直接报"当前资源类型不支持无源生成",这是设计而非缺陷。见 PRD §3.10 / §7.9 与下面的 S11a。
|
||||
3. **工具条 7 个动作按 opt-in 不渲染**:重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画;只有 `快速编辑` 与 `下载` 真渲染(宿主另附加 `UI 编辑器` / `编辑标签` / `重命名`,以及**只在素材被当前版本绑定时**出现的 `替换素材`)。
|
||||
4. **C7 只做记录层 + UI 层**:切换版本即重载当前预览;版本化资源解析机制未实现。资源替换同样不做运行时资源重映射:改绑定只改「这个版本用哪些素材」的记录,运行画面要按游戏自身引用的资源路径渲染。
|
||||
5. **「首轮进度投影」无编码级判据**,可见性由底部 Agent 状态栏承载。
|
||||
|
||||
Reference in New Issue
Block a user