Merge branch 'master' into feat/agc-ui-motion-ux
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m59s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m53s
Project CI / Frontend tests (pull_request) Successful in 3m39s
Project CI / Backend tests (pull_request) Successful in 8m27s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Successful in 8m56s
Project CI / Repository checks (pull_request) Successful in 7m21s

This commit is contained in:
2026-10-05 18:46:06 +08:00
1004 changed files with 72132 additions and 254383 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,24 @@
# AGC 顶栏布局收口:改动后对照图(2026-10-04)
本目录是「优化 AGC 标题栏布局」(`fix/agc-toolbar-layout`)的改动后截图,供 PR 与后续回归对照使用。四张图都取自一次性冒烟夹具(`.codex/skills/agc-workbench-browser-smoke`:真实 Chromium 挂 `ProjectDevelopmentView`,不是 Tauri 客户端),因此没有客户端窗口边框与左侧启动器侧栏。
| 文件 | 画面 | 改动后的形态 |
| ------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `01-toolbar-resources-full-1412.jpg` | 资源管理页(窗口 1412) | 顶栏一行到底:**两端是两枚大分组**——左端「资源管理 / 运行」,右端「依赖 / 类型」;中间依次是动作按钮与版本入口,版本入口紧邻排序分段左侧并显示完整标识 `版本 1(智能体修订 · 2026/10/3 14:21:05)` |
| `02-toolbar-resources-compact-1240.jpg` | 资源管理页(窗口 1240) | 空间不足第一档:版本入口收成 `版本 1`,其余一枚不动、不换行,行尾仍由「依赖 / 类型」收住 |
| `03-toolbar-more-menu-800.jpg` | 资源管理页(窗口 800) | 空间不足第二档:`打开项目目录 / 资源面板 / 整理画布` 收进「更多」下拉,展开即同一批动作;行尾仍由「依赖 / 类型」收住 |
| `04-toolbar-run-tab-and-preview-refresh-1412.jpg` | 运行页(窗口 1412) | 「播放」并入「运行」页签(页签前带 ▶);运行画面右下角多一枚「刷新运行画面」,与「全屏预览」并列 |
改动前的问题(用户现场截图):
1. 顶栏放不下时两侧各自 `flex-wrap: wrap`:第二行只剩「播放」与版本入口,两行控件分裂;
2. 版本入口不贴右缘,被前面按钮的文本宽度顶开;
3. 「播放」与「运行」是两个入口说同一件事;
4. 运行画面里的游戏不是实时刷新,改完代码只能切到资源管理再切回来重载;
5. 两枚体量最大的分组控件(「资源管理 / 运行」「依赖 / 类型」)挤在同一侧,版本入口又压在「依赖 / 类型」右边——现在两枚大分组分居一行两端,版本入口在排序分段左侧。
相关文档口径已同步到:
- [`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`](../../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)(顶部工具条那一行)
- [`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`](../【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md)(S17 播放、S17a 刷新)
- [`docs/project-memory/shared-memory/decision-log.md`](../../../project-memory/shared-memory/decision-log.md)(顶栏降级档位与两端分组决策)
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

@@ -6,7 +6,7 @@
## 1. 一句话
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`(带当前栏目 `targetCategory`,素材登记进上传它的那一栏);生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
## 2. 入口矩阵(事实源)
@@ -42,7 +42,7 @@
| 生成 UI 设计图 | 同上 | `kind: 'ui-design'`,默认 `16:9 · 1K`;前置同上 |
| 生成背景音乐 | `start_local_project_asset_generation` | `kind: 'background-music'`、`idempotencyKey`;任务 id 即该次生成的 operation id |
| 生成音效 | `start_local_project_asset_generation` | `kind: 'sound-effect'`、其余同上 |
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes, targetCategory }`;`targetCategory` = 当前栏目(Issue 359) |
`start_local_project_asset_generation` 的完整参数是 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath, idempotencyKey }`(Rust `src-tauri/src/asset_generation_tasks.rs`);图片类入口沿用 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }` 逐字不变,音频入口只带 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`idempotencyKey` = 该次生成的幂等键,任务 id 即 operation id;原生命令内部仍复用既有音频无源生成链路,不新增平台路由与请求体口径),**提交即返回**一条任务记录(`taskId / status / phaseDetail / assetId / error` 等);生成在 `tauri::async_runtime::spawn` 出来的后台任务里跑,账本落在项目内 `.agent/runtime/asset-generation-tasks/tasks.json`,进度由 `list_local_project_asset_generations` 读回。截图面板不再「等生成结束」,所以**点「生成」即同步关闭面板**(不等 IPC),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。
@@ -3,6 +3,7 @@
## 范围
- 后台管理默认入口为 `#dashboard`,原服务 / 数据库状态页保留为 `#overview`,导航展示名为“服务总览”。
- 桌面端后台侧边栏按“工作台 / 数据与监控 / 运营配置 / 充值与支付 / AGC 管理 / 内容运营 / 账号权限”分组,默认只展开当前路由所在组。分组标题可独立折叠;每次切换路由(包括同组内跳转和浏览器 hash 导航)自动展开目标组,折叠动作不切换页面。只展示当前账号有权访问的页签,空组不渲染,组内数量只统计可访问页签。折叠状态仅保留在当前页面会话;分组不改变路由 hash、页签权限或移动端底栏的平铺顺序。
- Dashboard 由 `GET /admin/api/dashboard` 提供统一 BFF 投影,前端只展示后端返回的 `range`、`metrics`、`charts`、`operations` 和 `warnings`。
- 不新增持久化统计表或字段;profile / tracking 私有事实由仅 runtime service identity 可调用的 `get_admin_dashboard_stats_and_return` procedure 在同一事务快照内聚合,再经 `spacetime-client` facade 返回 api-server;素材与钱包继续复用原有查询。api-server 不再通过固定 `LIMIT` 拉取原始访问明细后自行拼装精确指标;该权威聚合失败时整个 Dashboard 请求失败,不用 0 伪装未知值。
- 当前 profile / tracking 表没有覆盖这些跨 scope、跨日期统计的现成索引,因此 procedure 为保证精确性仍需遍历相关事实;上线后需监控调用耗时,数据规模继续增长时再以日期前缀索引或持久化日聚合事实替换,不能重新引入固定行数截断。
@@ -80,8 +80,8 @@
- `project.jsonl` 与项目主对话共用信封类型,不要为了"可见但不喂模型"新增行结构。
- 埋点 `settle` 早于成绩入库会静默丢事件(未来"进历史但不喂模型"的条目同理要落在注入侧,不在读取侧)。
- `cargo test export_bindings` 会重写全部 `chat/generated/`(引号风格漂移),跑完要 `git checkout --`
掉不是本次新增的文件。
- `cargo test export_bindings` 会重写全部 `chat/generated/`;生成目录的提交形态就是 ts-rs 原始输出
(2026-10-03 起已从 prettier / eslint 排除),跑完用 `git diff` 为空确认幂等,不要做 prettier 归一化。
- 本机 rust 全量 `--bins` 测试会挂在 mock server 的 `inet_csk_accept` 上,用 `--bins "agent::"` 之类过滤跑。
## review 收口第二轮(2026-09-24)
@@ -63,7 +63,7 @@ npm run check:pingora-release-readiness
`check:pingora-gateway-smoke` 会临时启动 mock `api-server`、mock SpacetimeDB、mock Gitea 和 `pingora-gateway`,覆盖精确主站 SPA fallback、大小写与尾部斜杠兼容、同前缀未知路径真实 404、后台静态路由、HTML / 普通静态资源 `no-cache`、Vite 指纹静态资源 immutable 缓存、静态 `ETag` / `Last-Modified` 与 `304` 协商缓存、静态 `HEAD` 响应、静态 Range、静态 access log method/path/status 对账、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、ACME、TLS 直连、HTTP/2 ALPN、HTTP 到 HTTPS 重定向、内部路由拒绝、shadow probe、API 代理头(`Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `X-Real-IP` / `X-Forwarded-For`)、Gitea Host 整站转发、请求体上限、429 接流保护、上游断连 / 超时 JSON 错误、维护模式、维护模式不拦截 Gitea Host 和 SpacetimeDB WebSocket Upgrade,并复用 `check-pingora-direct-live.mjs` 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。该本地 fixture 会让首页同时引用普通静态资源和 Vite 指纹静态资源,direct live JSON 必须确认指纹资源 GET / HEAD / `Range: bytes=0-0` 以及 access log method/path/status 证据,避免正式直连前只证明普通静态读取。排查失败时可追加 `-- --verbose` 输出网关 stderr / stdout;已确认二进制无需重编时可追加 `-- --skip-build`。
`check:nginx-spa-routes` 从 `appPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` / `APP_RUNTIME_ROUTES`、`appRoutes.tsx` 的精确路由判断和兼容恢复路径 `/creation/rpg/agent` 提取当前主站 SPA allowlist,确认生产、开发和容器三套 Nginx 模板集合一致,并验证大小写、尾部斜杠和 `/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 等未知反例。
`check:nginx-spa-routes` 从 `appPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` 与 `APP_PREFIX_ROUTE_ENTRIES`、`appRoutes.tsx` 的精确路由判断和兼容恢复路径 `/creation/rpg/agent` 提取当前主站 SPA allowlist,确认生产、开发和容器三套 Nginx 模板集合一致,并验证大小写、尾部斜杠、前缀路由的「前缀 + 恰好一个路径段」锚定形状(收银台深链 `/pay/<checkoutToken>` 必须整体回退 `index.html`,只放行裸前缀会让真实链接落到默认 location 变 404)和 `/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 等未知反例。该脚本自带正/反用例(`node --test scripts/check-nginx-spa-routes.test.mjs`,由 `npm run check:nginx-spa-routes` 一起执行),防止有人把前缀路由改回精确匹配。
`check:pingora-route-parity` 会先执行同一 Nginx SPA 路由门禁,再读取 `deploy/pingora/nginx-route-parity.matrix.json`,静态确认生产 / 开发 Nginx 模板、Pingora Rust 路由 allowlist / 单测和本文档都覆盖同一组核心路由,并做**反向覆盖**(模板里的每条 `location` 都必须被矩阵声明)。`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix` 会读取同一份矩阵,逐条断言 `classify_path` 的路由结果、body limit 和接流保护分组。`check:nginx-spa-routes` 与 `check:pingora-route-parity` 已串进 `npm run lint`(因此 `check:repository-ci`、CI 与 pre-push 都会执行),接线本身由 `check:production-ops` 的 guardrail 锁定。
@@ -535,7 +535,8 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
| `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
| `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 |
| `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 |
| 主站 SPA allowlist | 只对 `/`、`/components`、`/creation`、`/design-system`、`/editor/canvas`、`/games`、`/games/detail`、`/games/mine`、`/games/play`、`/games/publish`、`/profile`、`/project` 失败回退 `/index.html`(集合与前端路由源、Nginx 三份模板逐条一致,由 `npm run check:pingora-route-parity` 与 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 比对);匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。`/games/game_<32 位十六进制 id>/…` 是发行网关路由,不在 SPA allowlist 内。 |
| 主站 SPA allowlist | 只对 `/`、`/components`、`/creation`、`/design-system`、`/editor/canvas`、`/games`、`/games/detail`、`/games/mine`、`/games/play`、`/games/publish`、`/pay`、`/profile`、`/profile/payment`、`/project` 失败回退 `/index.html`(集合与前端路由源、Nginx 三份模板逐条一致,由 `npm run check:pingora-route-parity` 与 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 比对);匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。`/games/game_<32 位十六进制 id>/…` 是发行网关路由,不在 SPA allowlist 内。 |
| 主站 SPA 前缀路由 | 收银台深链 `/pay/<checkoutToken>` 走 `MAIN_SPA_PREFIX_PATHS`:只放行「前缀 + 恰好一个路径段」(大小写不敏感),裸前缀由上面的精确集合负责,多段路径与 `/payment/x` 这类前缀同名邻居都不进 SPA fallback;Nginx 三份模板同口径写成 `location ~* "^/pay/[^/]+/?$"`,由矩阵的 `pay_checkout_spa_fallback` 用例固定。 |
| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 不进入 SPA fallback。 |
SPA allowlist 里属于游戏分发入口的深链(游戏目录 / 详情 / 游玩 / 我的 / 发布深链:`/games`、`/games/detail`、`/games/play`、`/games/mine`、`/games/publish`)与 Nginx 三份模板同口径;Pingora 侧由路由对照矩阵的 `games_spa_fallback` 用例与 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 逐条断言。根路径 `/` 精确回退 `/index.html`(Nginx 在 `location = /` 里用 `try_files /index.html =404;`,不带 `$uri`),由矩阵的 `web_root_spa` 用例固定。发行网关路径 `/games/game_<32 位十六进制 id>/…` 不走 SPA,见下一节的对照说明。
@@ -56,6 +56,9 @@
## 模型绑定 Agent 执行模式
- 后台 AGC 模型目录每项新增 `agentMode`,只接受 `codex` 与 `cc`;缺少该字段的历史目录按 `codex` 解释,后台新增模型默认 `codex`。
- 2026-10-01 修正(原先误把协议塞进 `agentMode`):**执行器与线协议是两个正交维度**。目录新增独立字段 `protocol`,取值 `openai_responses` / `openai_chat` / `anthropic`;`agentMode` 只表示客户端启动哪个执行器。合法组合是封闭的——`codex` 只能用 `openai_responses` 或 `openai_chat`,`cc` 只能用 `anthropic`,其余组合在后台保存与目录加载时都失败关闭。存量目录缺少 `protocol` 时按执行器推导(codex → `openai_responses`,cc → `anthropic`);历史误写的 `agentMode: "anthropic"` 读取时归一到 `cc`。
- 选路由协议决定:`openai_responses` → `/api/llm/v1/responses`,`openai_chat` → `/api/llm/v1/chat/completions`,`anthropic` → 独立路由 `/api/llm/anthropic/{*path}`。客户端把所选模型的协议与执行器一起持久化(`agentMode` + `selectedModelProtocol`),Codex 的 `wire_api` 按协议在 `responses` / `chat` 间切换。
- 2026-10-01 补充:`cc` 一律经平台 LLM 网关的独立 Anthropic 路由,客户端 `ANTHROPIC_BASE_URL` 为 `{apiBaseUrl}/api/llm/anthropic`,不得再用配置里的 `llm.baseUrl`(那是 Router 地址,平台不会把账号 Router key 下发给客户端)。Claude Agent SDK 自己拼 `{base}/v1/messages` 并带 `authorization: bearer` 与 `anthropic-version`,网关据此转发上游 `{routerBase}/v1/messages`。
- `codex` 仍表示现有 AGC Codex app-server 执行链路;`cc` 表示 AGC 客户端启动随包的 Claude Agent SDK sidecar,模型的 `modelId` 原样作为 Claude Agent SDK 的模型标识,不把 Claude 协议伪装成 OpenAI Responses。sidecar 随 AGC 安装包携带 SDK 及匹配平台的 Claude Code runtime,用户不需要预先安装 `claude` 命令。
- `/api/llm/models` 的启用模型摘要返回 `agentMode`,后台完整目录、管理 DTO 和客户端目录保持同一绑定快照。客户端在选择模型时同时持久化模型 ID 与执行模式;默认模型变化、模型被停用或目录刷新回退时一并更新执行模式。
- 客户端设置保存不得把后台模型绑定覆盖回 Codex。旧客户端配置缺少执行模式时继续按 Codex 运行;旧后台响应缺少 `agentMode` 时客户端按 Codex 兼容。
@@ -21,6 +21,7 @@
### 渠道与安装身份合同
- 渠道同时决定**更新端点**与**安装身份**,两者都由构建期写入产物。默认渠道 `dev` 保持基线身份 `productName = 陶泥儿开发版`、`identifier = world.genarrative.ai-game-creator`;`release` 渠道使用正式产品名 `陶泥儿` 并派生 `identifier = world.genarrative.ai-game-creator.release`;其它自定义渠道派生 `productName = 陶泥儿 <渠道显示名>`(如 `beta-2` → `陶泥儿 Beta-2`)与 `identifier = world.genarrative.ai-game-creator.<渠道>`。渠道显示名按连字符分段首字母大写,不改动渠道本身。
- Windows 主程序文件名也是渠道安装身份的一部分:`dev` 保留 `genarrative-ai-game-creator-shell.exe` 以延续既有升级链,`release` 使用 `genarrative-ai-game-creator-shell-release.exe`,其它渠道使用 `genarrative-ai-game-creator-shell-<channel>.exe`。NSIS 更新/卸载按该文件名查找进程,保证更新一个渠道不会结束另一个渠道。
- 默认渠道身份**不可变更**:既有安装目录、卸载项、快捷方式与已发布客户端的升级链都建立在基线身份上。渠道身份由 `apps/ai-game-creator-shell/scripts/channel-identity.mjs` 单点定义,构建入口、macOS 发布入口与配置门禁共同消费;基线 `tauri.conf.json` 必须逐字等于默认渠道身份。
- 安装身份决定的持久与可见事实:Windows 安装目录 `%LOCALAPPDATA%\<产品名>`、卸载项与 `HKCU\Software\genarrative\<产品名>`、WebView2 数据目录 `%LOCALAPPDATA%\<identifier>`、客户端数据目录 `%APPDATA%\<identifier>`;macOS `.app` 名、bundle id、DMG 卷名与菜单栏应用名。
- 同机并存:不同渠道的包体可以在同一台设备上同时安装并同时运行,互不覆盖、互不顶掉;同一渠道的新版本仍是原地升级,因为更新端点与安装身份同属一个渠道。
@@ -124,19 +125,20 @@
- 渠道安装身份映射(`<channel>` 为 `dev`、`release` 或自定义名称;`<Channel>` 为渠道显示名):
| 渠道 | productName | identifier | Windows 安装目录 | 客户端数据目录 |
| -------------------- | ------------------ | ---------------------------------------------- | ----------------------------------- | ----------------------------------------------- |
| `dev`(默认) | `陶泥儿` | `world.genarrative.ai-game-creator` | `%LOCALAPPDATA%\陶泥儿` | `%APPDATA%\world.genarrative.ai-game-creator` |
| `release` / 自定义 | `陶泥儿 <Channel>` | `world.genarrative.ai-game-creator.<channel>` | `%LOCALAPPDATA%\陶泥儿 <Channel>` | `%APPDATA%\world.genarrative.ai-game-creator.<channel>` |
| 渠道 | productName | 主程序文件名 | identifier | Windows 安装目录 | 客户端数据目录 |
| -------------------- | ------------------ | ------------------------------------------------- | ---------------------------------------------- | ----------------------------------- | ----------------------------------------------- |
| `dev`(默认) | `陶泥儿开发版` | `genarrative-ai-game-creator-shell.exe` | `world.genarrative.ai-game-creator` | `%LOCALAPPDATA%\陶泥儿开发版` | `%APPDATA%\world.genarrative.ai-game-creator` |
| `release` | `陶泥儿` | `genarrative-ai-game-creator-shell-release.exe` | `world.genarrative.ai-game-creator.release` | `%LOCALAPPDATA%\陶泥儿` | `%APPDATA%\world.genarrative.ai-game-creator.release` |
| 自定义渠道 | `陶泥儿 <Channel>` | `genarrative-ai-game-creator-shell-<channel>.exe` | `world.genarrative.ai-game-creator.<channel>` | `%LOCALAPPDATA%\陶泥儿 <Channel>` | `%APPDATA%\world.genarrative.ai-game-creator.<channel>` |
- 安装身份迁移:`dev` 客户端保持原身份,升级链路连续;`release` 与自定义渠道首次以新身份安装,**不接管也不迁移**任何既有 `dev` 安装、本地项目或登录态,设备上因此可以同时存在两个渠道的客户端,由用户自行决定是否卸载其一。
- 安装身份迁移:`dev` 客户端保持原 identifier 与主程序文件名,升级链路连续;dev Windows 包继续清理历史展示名 `陶泥儿` / `Genarrative AI Game Creator` 的旧快捷方式。release 从旧展示名 `陶泥儿 Release` 迁移安装目录与快捷方式到当前 `陶泥儿` 身份;release 与自定义渠道不接管也不迁移其它渠道的数据或登录态。
## 构建与发布
- 发布入口:`npm run ai-game-creator-shell:release:upload`(构建 + 按渠道上传);仅构建不发布的 smoke 使用 `--no-bundle` 分支,不读远端版本、不改版本、不生成清单。
- 发布入口只解析一次目标,优先级为 CLI `--target value` / `--target=value` / `-t value`、`AGC_BUILD_TARGET`、Windows 默认值;重复/空目标与不支持目标失败关闭。版本高水位、构建 feature/渠道端点、bundle 路径、产物后缀、清单平台键及摘要必须消费同一个发布上下文,不能分别回读默认目标。
- 渠道由 `AGC_UPDATE_CHANNEL` 显式指定,默认 dev;Windows 与 macOS 目标均支持 dev、release 和自定义渠道,目标校验独立进行。
- 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier`、updater 端点与窗口标题:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。
- 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier`、`mainBinaryName`、updater 端点与窗口标题:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。
- 渠道配置走 Tauri 的 JSON Merge Patch 语义:对象递归合并,**数组整体替换**。因此 `app.windows` 必须按基线 `tauri.conf.json` 的完整 client 窗口对象下发、只覆盖 `title`(脚本从基线读取后展开);任何"只写 `{ title }`"的写法都会让 `label` / `decorations` / 尺寸回落成 Tauri 默认值(`label=main`、`decorations=true`、800x600),表现为打包产物重新出现系统标题栏,并按 label 连带失效承载平台 HTTP 权限等 capability。守卫用例:`build-release.test.mjs` 的渠道配置合并用例与 `check-config.mjs` 的 `decorations` 门禁。
- 定时调度分别判断服务端与客户端 scope:dev 小时调度在提交含 AGC 相关路径时发布对应渠道,纯文档或流水线自身的提交仍只跑 Full Build;release 每日调度在服务端相关路径变化时发布正式 Full Build,在 AGC 相关路径变化时发布 release 客户端,并在同一调度内等待、汇总各 lane 结果,失败 lane 下一轮补发。判定失败或勾选强制触发时按"需要发布"处理。
- 更新摘要不再自动生成:发布脚本不读取提交记录生成 `notes`;只有 `AGC_UPDATE_RELEASE_NOTES` 非空时,才把显式手动文案写入渠道清单和旧协议清单的 `releaseNotes`。未设置时清单不携带更新说明,归档文件 `release-notes.txt` 记录“本次没有可用的更新摘要”。
@@ -184,10 +186,11 @@
| 条款 | 验收方式 | 结果 |
| --- | --- | --- |
| 渠道身份派生与默认渠道不变 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过;`dev` 逐字等于基线 `陶泥儿开发版` / `world.genarrative.ai-game-creator`,`release` 使用 `陶泥儿` 并保持独立 identifier,`beta-2` 派生带后缀的产品名与独立 identifier,非法渠道失败关闭 |
| 渠道身份派生与默认渠道不变 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过;`dev` 逐字等于基线 `陶泥儿开发版` / `world.genarrative.ai-game-creator` / `genarrative-ai-game-creator-shell`,`release` 使用 `陶泥儿` 并保持独立 identifier 与 `-release` 主程序名,`beta-2` 派生带后缀的产品名、identifier 与主程序名,非法渠道失败关闭 |
| 身份与端点同批注入 | 同上的渠道 `--config` 用例 | 通过;`productName` / `identifier` 与 `/<channel>-win|mac/latest.json` 来自同一次解析 |
| 渠道产物首装包选择 | 同上的渠道 DMG 夹具用例 | 通过;`陶泥儿_<版本>_aarch64.dmg` 仍按 `<版本>_<架构>.dmg` 唯一匹配 |
| 基线配置等于默认渠道身份 | `node apps/ai-game-creator-shell/scripts/check-config.mjs` | 通过;基线漂移与非默认渠道身份不隔离都会失败关闭 |
| Windows 更新进程与快捷方式隔离 | `build-release.test.mjs` 渠道配置/NSIS 钩子用例 | 通过;dev/release 主程序文件名不同,release 迁移旧 `陶泥儿 Release` 安装目录与快捷方式 |
| 全量发布脚本回归 | `node --test build-release.test.mjs release-oss.test.mjs prepare-macos-codex.test.mjs cargo-features.test.mjs` | 通过(64/64,含 macOS 入口按渠道解析产品名的守卫) |
| AGC 自有 AppData 提权 ACL 范围 | `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- config::private_path_elevation_policy_tests` | 通过(12/12;含基线、`<基线>.release`、`<基线>.beta-2` 与相似前缀 `-backup` 的反向断言) |
| 渠道身份进入真实构建产物 | `AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug` | 通过;Tauri 接受 `productName=陶泥儿` / `identifier=world.genarrative.ai-game-creator.release` 并完成构建;产物字符串实测 `陶泥儿` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0 |
@@ -8,12 +8,13 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
- 诊断 URL 保留可定位的 API 路由路径,隐藏 origin、URL 账号密码、查询参数、fragment 和路径中的敏感标识;普通资源 URL 与本地文件路径继续隐藏。网络错误、HTTP 错误与响应体超时均应带安全路由,不能只剩 `<url>`。历史已经脱敏的归档不推测或补造原路由。
- 捕获 React render error、`window.onerror`、`unhandledrejection` 以及显式标记的 Tauri/API/Agent 错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。
- 报告池只收**没有任何调用方处理**的错误:React render error、`window.onerror`、`unhandledrejection`,以及调用方判定为真故障后带上下文交给错误池(`ClientAuthErrorWrapper`)的错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。分层口径见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。
- 事件字段包括 eventId、fingerprint、source、message、stack、时间和次数;重复事件合并。不再携带 severity、errorCode、page、action、requestId 等无法稳定关联的字段。
- 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。
- 客户端 API 自动采集只覆盖网络错误、408 和 5xx;预期的 4xx 登录/鉴权失败不进入错误报告池。
- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段,无字段变体生成 `{ type }`,带载荷变体生成 `{ type } & 载荷类型`),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。认证命令的封装形态:`invokeClientAuth` 只是薄包装,把结构化拒绝原样装进**已有**的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`,构造时把整份载荷 `JSON.stringify` 进 `Error.message`,上报事件因此拿到机器事实),不新增手写错误类;判定只写在 catch 子句里,无字段变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名载荷类型(`reason` 是枚举时再 `switch (payload.reason)`)、再用它自己的字段拼上本次操作的上下文前缀,`default` 用 `expectNever` 在编译期挡住漏接变体。
- 客户端 API 自动采集只覆盖网络错误、408 和 5xx(`clientApi` 作为 `fetch` 的调用方在抛出前判定);预期的 4xx 登录/鉴权失败不进入错误报告池。
- Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。
- 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/<eventId>.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef,全部是程序生成或调用方常量)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata,自由文本只出现在这里)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(summary 由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。`sanitize_diagnostic_message` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。
- 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/<eventId>.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / clientTurnId / elapsedMs / detailRef,全部是程序生成或调用方常量)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata,自由文本只出现在这里)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(summary 由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。`sanitize_diagnostic_message` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。
- 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中当前快照中的全部事件,用户可取消不想提交的事件。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。
- 报告面板读取当前错误快照失败时,必须明确显示“错误事件暂不可用,请关闭后重试”,不能把失败误显示为“当前没有待报告的错误”。
- 通知中的“查看并报告”打开面板时必须保留该次通知快照;最新快照读取瞬时失败时使用这份 fallback 继续展示和提交,不能因先清空通知而丢失用户刚看到的事件。
@@ -5,7 +5,7 @@
原始版本:`2026-07-14`;口径复核:`2026-08-25`
> 当前口径(2026-08-25):本文件只保留 Runtime V1.1 的持久化、恢复、安全和验证约束。AGC 当前产品入口、DirectProject、语义工具和 UI workflow 以 [`AI游戏创作智能体App实施计划-2026-06-24.md`](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、当前 `apps/ai-game-creator-shell` 代码及 `docs/README.md` 为准;本文件与其冲突时以后者为准。旧 ToolHost / DirectHome 兼容条款不得重新成为产品入口或第二套 Runtime 真相。
> 当前口径(2026-10-02):本文件描述的自建 Agent Runtime 执行面(runtime driver / protocol / tools / actions)、`--agent-*` CLI、目标 / 协作协议与真实 e2e harness 已整体退役,实现、测试、门禁与 CI job 均已删除,见 [`【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`](../../adr/【ADR】退役AGC独立Agent%20Runtime与CLI执行面-2026-10-02.md)。外部 Runner 仅保留编辑器桥 RPC 与 GUI owner 参与锁 / watchdog。AGC 当前产品入口、DirectProject、语义工具和 UI workflow 以 [`AI游戏创作智能体App实施计划-2026-06-24.md`](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、当前 `apps/ai-game-creator-shell` 代码及 `docs/README.md` 为准;本文件与其冲突时以后者为准。旧 ToolHost / DirectHome 兼容条款不得重新成为产品入口或第二套 Runtime 真相。
## 目标
@@ -861,13 +861,13 @@ V1.20 对标 Codex CLI 的可选 Web Search,但只声明当前 `platform-llm`
## V1.21 单 Agent token-aware 持久上下文压缩
V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_limit`、`tool_output_token_limit` 和显式手动压缩。它替换“固定保留最近 12 条就算压缩”的能力口径,但不删除原始 conversation、task、event 或工具事实,也不把模型摘要提升为 Goal、计划、权限、验证或副作用事实源。
V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_limit` 和显式手动压缩。它替换“固定保留最近 12 条就算压缩”的能力口径,但不删除原始 conversation、task、event 或工具事实,也不把模型摘要提升为 Goal、计划、权限、验证或副作用事实源。
### 配置与预算
- `llm` 新增 `contextWindowTokens / autoCompactTokenLimit / toolOutputTokenLimit`,发布默认分别为 `128000 / 64000 / 12000`;`agentLlm.<agentId>` 复用现有 patch 继承,显式 Agent 值覆盖全局。三项都必须大于 0,自动阈值必须小于 context window,并为当前请求的生成 token 预算与固定安全余量留下空间。既有配置和持久协议键 `maxOutputTokens` 保持冻结以兼容恢复;它表示包含可见输出与隐藏 reasoning token 的生成侧预算,不表示输入加输出总量,也不保证可见正文长度。
- `llm` 新增 `contextWindowTokens / autoCompactTokenLimit`,发布默认分别为 `128000 / 64000`;`agentLlm.<agentId>` 复用现有 patch 继承,显式 Agent 值覆盖全局。两项都必须大于 0,自动阈值必须小于 context window,并为当前请求的生成 token 预算与固定安全余量留下空间。既有配置和持久协议键 `maxOutputTokens` 保持冻结以兼容恢复;它表示包含可见输出与隐藏 reasoning token 的生成侧预算,不表示输入加输出总量,也不保证可见正文长度。
- Runtime 在发送 tool-plan、context-compaction 或 final-reply 前,按消息、multimodal 文本和 function schema 的规范序列化字符数做保守 token 估算;Provider 返回 usage 时再记录真实 `prompt/completion/total`。估算只用于提前门禁,不能伪装成 Provider 计费事实。
- 单条 observation 进入模型上下文前按 `toolOutputTokenLimit` 收紧;完整命令输出仍留在 owning Agent 的私有 sidecar,通过既有分页工具读取。公共状态只显示估算 token、最近真实 usage、阈值、压缩次数和时间,不显示被压缩正文。
- 完整命令输出仍留在 owning Agent 的私有 sidecar,通过既有分页工具读取。公共状态只显示估算 token、最近真实 usage、阈值、压缩次数和时间,不显示被压缩正文。
### 可压缩内容与不可压缩事实
@@ -921,7 +921,7 @@ V1.22 对标 Codex CLI 的 MCP tool 能力,在现有单 Agent Runtime 内增
- 共享命令契约新增 `mcp.call`,项目/Agent policy 仍可统一 deny 或 confirm。server/tool 配置只会进一步收紧:`deny` 直接返回 blocked observation,`confirm` 复用现有确认卡,`auto/writes` 也必须先可靠写入 durable pending action。隔离 child 默认禁止 MCP;后续若开放必须有模板级显式 allowlist,不继承父 Agent 的宽权限。
- MCP 调用复用当前 action fingerprint、steer cursor、Goal、project/repository revision、verification gate、Provider request identity 与 `approved -> executing -> observed-*` 账本。调用前再次读取 AppData、刷新目标 tool 并核对两个 fingerprint;Runner 在 `executing` 后退出、超时后无法确定服务端是否完成、SDK transport 断开或审计落盘失败均进入 reconciliation。只有服务端明确返回 result/error,才形成可继续 planning 的确定终态。
- 完整 `CallToolResult` 原子写入 `.agent/runtime/mcp-results/<agentHash>/<runHash>/<actionHash>.json` 私有 sidecar,绑定项目/Agent/Task/Session/Run/action/server/tool/catalog/tool/result fingerprint、执行时 `toolOutputTokenLimit`、字符/内容块计数、isError 和时间。恢复时必须从完整 result 按原预算重算计数、摘要、二进制元数据和 observation 并全量比对;任一派生字段不一致都进入 reconciliation,不能接受局部损坏摘要。模型 observation 只读取受 `toolOutputTokenLimit` 约束的 text/structured content;image/audio/embedded resource 只给类型、MIME、大小和 SHA-256 元数据,本切片不把任意 MCP 二进制转发给 Provider。
- 完整 `CallToolResult` 原子写入 `.agent/runtime/mcp-results/<agentHash>/<runHash>/<actionHash>.json` 私有 sidecar,绑定项目/Agent/Task/Session/Run/action/server/tool/catalog/tool/result fingerprint、字符/内容块计数、isError 和时间。恢复时必须从完整 result 按原预算重算计数、摘要、二进制元数据和 observation 并全量比对;任一派生字段不一致都进入 reconciliation,不能接受局部损坏摘要。模型 observation 只读取 text/structured content;image/audio/embedded resource 只给类型、MIME、大小和 SHA-256 元数据,本切片不把任意 MCP 二进制转发给 Provider。
- 公共 Runtime state/event/task/Agent DB/receipt/activity/output/report 只保存 server/tool、审批、状态、参数字符数与 SHA-256、结果内容块计数/字符数与 SHA-256、sidecar 相对路径和安全错误分类,不保存 arguments、返回正文、Bearer/header/env、server instructions、绝对路径或 SDK 原始错误。`agent.action_history` 同样只返回该安全摘要。
### 开发入口与验收
@@ -1,5 +1,35 @@
# AI 游戏创作智能体 App 实施计划
## 2026-10-03 首页建项不等 AI 命名(Issue 599)
本节覆盖下文「2026-08-20 Direct Codex 审核 Skill Pack 与受控工具内核」里「自动项目命名仍是首页建项前置」的隐含口径。
- 首页「开启创作 / 进入立项策划」的提交链路是「Web 环境预检 → 立即用宿主兜底名创建工作区 → 进入项目 → 首轮创作」。`create_automatic_local_game_project` 不再接收前端命名结果(固定 `name: null`,由宿主生成 `GameAgent 项目 <8 位短 id>` / `策划项目 <8 位短 id>`):AI 命名请求在建项前并行打出,命名慢、超时、失败或返回非法内容都不影响建项、附件导入与首轮创作。
- AI 返回合法名称后走新增的条件改名命令 `rename_local_game_project_if_unchanged(projectPath, expectedProjectId, expectedName, name)`:`expectedProjectId` 取自建项返回的 `manifest.projectId`,`expectedName` 是本次创建的兜底名。只有「项目身份一致」且「当前名称仍是兜底名」才落盘改名;用户已手动改名、项目 ID 不符、名称与现状相同都返回 `renamed: false` 并保持现状,空名 / 控制字符 / 超长失败关闭。改名与既有 `rename_local_game_project` 同口径:簿记写入,不推进项目 revision。
- 改名成功后,当前项目上下文(窗口标题与工作台项目名)与最近项目行的目录重检在「进项目落定」之后写入;AI 结果比进项目更快返回时同样以 AI 名称为准,不被 `enterProjectDevelopment` 的兜底上下文覆盖。
- 「做方案」不发起自动项目命名请求;手动选择目录建项保持原有行为(沿用目录名,不做 AI 命名)。
- 自动命名是**项目工作台「生成任务」列表里的一条后台任务**:账本记录用 `taskType`(`asset-generation` / `project-naming`)区分,schema `agc-asset-generation-task.v2`(读取兼容 v1,v1 记录按素材任务读回),`kind` 对命名任务为 null;入队/推进走 `enqueue_local_project_naming_task` / `update_local_project_naming_task`,展示名固定「AI 项目命名」,状态流转「排队中 → 命名中 → 已完成(已应用 AI 名称 / 建议名一致 / 用户已改名跳过)或失败(无可用名称 / 自动改名失败,均保留兜底名)」。命名行的结论只在终态显示,没有缩略图、提示词与「定位到素材」。命名任务由命名链路推进,因此入队要登记 live、终态写盘后摘除,否则任何一次账本读取都会把它按「上次运行中断」误收口为失败;账本只作展示旁路,入队/推进失败只写诊断日志,不影响建项、命名与首轮创作。
- 回归入口:`npx vitest run apps/ai-game-creator-shell/tests/homeProjectNamingAsync.test.tsx`(时序用 promise 闸门,不用 sleep)、`npx vitest run apps/ai-game-creator-shell/tests/projectNamingGenerationTaskRow.test.tsx`、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`cargo test --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute asset_generation_task`、`cargo test --features=… conditional_project_rename_tests`。
## 2026-10-02 退役自建 Agent Runtime 与 CLI 执行面
自建 Agent Runtime(`agent/runtime_driver`、`runtime_protocol`、`runtime_tools`、`runtime_actions`、`runtime_state`、`runtime_adapter`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`tool_plan_handoff/`、`user_input.rs`)与它的 Tauri 命令、`--agent-*` CLI、真实 e2e harness、CI job 一并退役,历史由 Git 保存,见 `docs/adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`。本文正文里所有关于 `--agent-run` / `--agent-task` / `--agent-runner-status` / `agent-run:smoke` / `agent-runtime-real-e2e.mjs`、后台单 Agent 队列、`read_game_creator_agent_runtime(s)`、Supervisor 自主协作与 Goal 生命周期的描述均已失效,不再作为实现依据。外部 Runner(`--agent-runner`)仅保留编辑器桥 RPC 与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog。AGC 现役链路以 DirectProject(`agent/direct_*` + codex app-server)、资源工作台与本文其后的 DirectProject 相关条目为准。
## 2026-10-02 固定试玩控件的指针命中边界
固定试玩只按目标控件自身的计算样式判断 `pointer-events:none`,不累计祖先的 `none`。覆盖层为 `none`、按钮显式恢复 `auto` 是合法布局;按钮未覆盖时仍会继承 `none` 并被拒绝。保留祖先可见性、中心点 `elementFromPoint` 命中、原生 disabled、aria-disabled 和 inert 检查;目标自身为 `none` 时,即使内部子元素恢复 `auto`,也不放行目标控件。
继续使用现有真实鼠标/触摸输入及游戏状态变化验收,不以 JS 强制点击或改写游戏样式绕过宿主检查。公共探针源码已进入场景指纹,修改后旧指纹证据按现行 stale 规则处理。定向回归入口为 `real_chrome_generic_playtest_pointer_events_`:在 desktop/mobile 两个视口验证祖先穿透、子元素命中和开始/主要操作/重开状态推进,并验证继承禁用、目标禁用、原生禁用、aria-disabled、inert、隐藏及遮挡仍拒绝。generic 的移动视口沿用鼠标输入,不将其表述为真实触摸验收;现场游戏开始流程需另行复核。
## 2026-10-01 项目文件列举的范围与并发边界
`agc_list_project_files` 的项目内 `path` 在遍历时限制到目标文件、目标目录及其必要祖先;不递归无关子树,也不读取无关条目的元数据。Agent 的控制面、敏感文件和快照排除规则在读取条目元数据之前生效;通用 UI 文件树保持原有可见范围。引擎生成目录仍按项目根身份排除,不能因定向列举改变判定基准。
文件列举是实时观察,不是文件系统快照。枚举后消失的文件或待进入目录(`NotFound`)跳过;权限、非目录和其它 IO 错误保留路径及原因,不统一吞错或重试。目标不存在时继续返回空列表;符号链接和 Windows 重解析点不得作为遍历入口。过滤后按项目相对路径排序,再计算 `total`、`offset`、`limit` 和 `nextOffset`;多次分页之间的并发变更不承诺快照一致性。
本修复不调整项目写锁等待、写入许可、项目外读取或工具 DTO。验收通过受控的枚举/元数据间隙释放真实项目锁,并覆盖定向范围、消失目录、真实 IO 错误、受保护路径、链接、引擎目录及分页;不依赖概率性循环碰撞。
## 2026-09-29 Codex 私有运行目录路径解析
新建的私有运行目录先确认是普通目录并收紧权限,再解析真实路径,后续 `codex-home`、`workspace` 和隔离用户目录均在真实路径下创建,避免 macOS 系统临时目录的符号链接阻断启动。Windows 扩展 UNC 路径 `\\?\UNC\server\share\...` 必须转换为 `\\server\share\...`,盘符路径才直接去掉 `\\?\` 前缀;转换后保留绝对路径语义。私有子目录仍执行原有祖先符号链接与 reparse point 检查。
@@ -97,12 +127,12 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
## 2026-09-20 DirectProject 七项效率闭环(补齐合同)
本节补齐并覆盖下节中仅靠 Skill 要求预检、收尾、批读和原生命令预算的部分。完整目标仍为:自动预检、宿主验收与收尾、分层验证、统一执行/返修预算、稳定测试基线、请求耗时与批量读取、所有工具并行。已有代码及测试不等于全部目标已完成;按下表逐项验收。
本节补齐并覆盖下节中仅靠 Skill 要求预检、收尾、批读和原生命令预算的部分。完整目标仍为:自动预检、宿主验收与收尾、分层验证、统一时间预算、稳定测试基线、请求耗时与批量读取、所有工具并行。已有代码及测试不等于全部目标已完成;按下表逐项验收。
| 要求 | 必须成立的行为 | 完成证据 |
| --- | --- | --- |
| 自动预检 | 新建 Web 游戏由实际用户入口和宿主自动执行,不依赖模型主动调用;失败不得启动正式生成或付费素材 | 首页/后端正反用例、真实构建与双端截图 |
| 验收与收尾 | 必需范围和验收项在宿主持久化;证据绑定当前输入;全部必需项通过后关闭本轮新的修改/执行/付费扩项并产生交付报告 | 真实工具链状态转移、重启/并发/新增扩项拒绝用例 |
| 验收与收尾 | 必需范围和验收项在宿主持久化;证据绑定当前输入;模型正常响应结束后复核,通过后关闭本轮新的修改/执行/付费扩项并产生交付报告 | 真实工具链状态转移、重启/并发/新增扩项拒绝用例 |
| 分层验证 | 视觉、定点玩法、项目测试和必要完整闭环按已登记标准执行;不混淆证明范围 | 双端正例与单端失败反例 |
| 统一预算 | 内置验证、托管脚本和原生命令执行受同一宿主预算约束;不以命令文本猜测“是不是试玩”,不把每条普通开发命令单独计为一次返修 | 捆绑 app-server 的执行前控制、拒绝无副作用、跨入口/重启/耗尽/超时用例 |
| 稳定基线 | 可复用的固定种子跑酷基线,真实短按/长按跳跃、单次收力、滑铲释放及公平越障窗口 | 物理单测、双端真实输入、原案例缺陷参数反例 |
@@ -116,6 +146,7 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
- 对客户端刚创建且内容仍匹配可信模板的 Web 脚手架,在正式生成前由宿主执行受控依赖准备和真实 Vite 构建。依赖安装禁用生命周期脚本;不自动安装或覆盖导入/用户修改过的工程。
- 准备凭证的 `ready` 只证明初次环境准备成功,不代表当前游戏已验收,后续正常修改不得因此重新安装。`preparing` 中断恢复必须证明原拥有者已结束且其执行子树已回收;身份或归属未知时保留阻断,不重复执行。
- 输出明确区分“Node/npm 构建能力通过”与“本项目 Vite 构建通过”;任何一步失败保留可行动错误,不降级为预检成功。
- 首页必须区分宿主预检阻塞与 Tauri IPC 调用失败:Rust 返回的安全错误码可透传到状态栏;无错误码的瞬态 IPC 失败只允许一次有界重试后显示独立的客户端连接故障,不得统一伪装成 Node/npm 或浏览器故障。预检仍保持失败关闭,不得因为展示错误变得可绕过。浏览器 WS 握手、WS 超时和 CDP 连接失败只允许在确认本轮 AGC 浏览器进程树已退出、Profile/owner/临时目录已按归属清理后使用新的隔离 Profile 自动恢复一次;清理未确认时不得重试,连续失败必须返回包含阶段、子进程退出确认、清理确认和是否重试的安全诊断。
- 安装载荷提供相同的安全预检 CLI,验证无系统 Node 的独立运行、缺包/篡改失败关闭。NSIS 解包载荷 smoke 与真实安装器注册流程分开报告,不覆盖当前用户的既有安装。
### 跑酷固定基线
@@ -135,30 +166,49 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
### 宿主控制与后续验收边界
- 统一预算按执行/验证批次与实际运行输入管理,允许正常构建和相关测试在一个批次内执行;原生读取和结构化文件编辑不按每条命令消耗返修次数。任意代码执行必须具备当前回合的有效执行许可及累计执行时长边界。
- 执行预算按工具占用累计时间和回合墙钟管理,不设置执行/返修批次或按普通失败扣减次数。任意代码执行必须具备当前回合的有效执行许可及累计执行时长边界。
- 原生命令入口必须以捆绑版本的真实协议证明可在执行前拒绝;不得用执行后的日志通知或文本分类器冒充执行门。能力检测失败不得静默退回无控制模式。
- 原生执行控制的精确协议与许可持久化,在对应里程碑评审后落地;不得提前宣布这一项已完成。
- 不修改 Provider 的 maxRetries;不减少引擎或任意代码执行的合法能力,不引入平行 Agent 框架,不改公开 API/数据库,不提交私密运行记录。
### Direct 操作控制(2026-10-04)
本节与下节共同描述当前 Direct 操作控制,执行/返修批次与全局 Draining 已移除。操作控制不改变 Codex 的工具调度、模型循环或图片工具的等待返回方式。
- 普通操作开始前,宿主检查原回合是否活动、时间预算是否足够、并发容量是否可用,并执行原有权限、路径及资源约束。交付合同是否存在、前一个工具是否成功不构成普通操作的准入条件。
- 操作成功、失败或取消后结算占用与用时,返回真实结果,由 Agent 决定后续操作。没有全局 Draining、执行/返修批次计数及对应重试门禁;验证输入变化只使相关证据失效,不使无关工作停顿。不新增租约到期续租、Agent 释放租约或独立后台任务接口。
- 保留原时间预算数值与累计口径、并发上限及回合身份。回合关闭、取消或时间耗尽后拒绝新操作,取消自有在途工作并核实本地进程退出;等待资源锁后的写入及每次新增付费提交仍核验原回合权限。操作记录清除不能代替执行结束证明。
- 普通工具失败和资源级结果不确定不终止整个回合;资源自身的幂等、operation ID、恢复及对账继续有效,不能因客户端取消而推断远端取消,也不能盲目重复付费。宿主控制状态损坏或无法确认自有执行器退出仍按实际控制失败处理。
- 交付复核与操作准入分离。合同由 Agent 按用户制作/交付游戏的意图登记,验收仅在正常响应结束后启动,当前只保留视觉/玩法要求。`validation.maxRuns` 暂保留现有交付复核次数用途,不再用于执行/返修批次;时间配置不变。
- 同回合重试或进程重启不刷新预算;仅保留预算、回合归属、必要退出记录和资源自身恢复所需的持久状态,不恢复旧租约为可执行权限。旧版本未结束状态须先确认旧执行器退出,不能通过直接删除账本解除控制。
验收至少覆盖失败图片与长命令并存时无关写入成功、连续工具失败不触发返修次数限制、所有退出路径释放并发占用、关闭后零新增执行/付费提交、迟到写入被拒绝,以及远端不确定任务不重复提交。Linux 进程组清理不冒充 Windows Job 的完整子树退出证明。
验证边界:Linux 定向回归已覆盖操作准入、合同复核与恢复、实际子进程退出、无合同及 ready 合同完整回复、进程扫描不确定性、迟到关闭通知与收尾预算报告。用户已确认手动 GUI 检查完成。未提供 Windows Job 完整子树退出及 Windows 捆绑补丁专项结果,也未以真实模型多语言意图或付费 Provider 场景替代协议夹具;这些边界不因合入或清理临时计划而视为已验证。后续目标平台验证沿用现有测试与本规范。
### 宿主验收与执行许可合同
- 正式 GUI 和 CLI 的共同 Direct 回合入口建立宿主控制状态,绑定 canonical 项目路径、稳定 clientTurnId 和原始用户输入摘要;宿主私有目录保存权威账本并独占该回合,项目 `.agent` 仅允许保存展示副本。配置或项目侧文件被改写、工具切换、Provider 重试和进程重启不得刷新同一回合的预算。
- Direct 回合集成测试也按生产入口计算原始用户输入的 SHA-256 十六进制摘要(64 字符),不能用请求名称替代。用户回显过滤回归继续覆盖实时消息去重、回合起止身份关联及历史落盘过滤。
- 直接启动 Direct 工具桥的图片生成通知测试,须复用真实宿主执行会话与已登记交付合同夹具,再发起工具请求;继续验证资源提交后发出 manifest 失效通知,以及空提示词被参数校验拒绝且不发通知,不绕过执行许可门禁。
- 普通聊天与读取不要求交付合同。首次修改、代码执行或付费扩项之前,模型通过结构化工具登记本轮必需范围与验收项;合同非空、有界且只冻结一次。模型只能声明要求,不能提交“通过”作为证据。后续扩项留到新的用户回合。
- 明确新 Web 创建由宿主可信脚手架凭证及尚未交付的宿主记录判定,CLI 同样据此判定,不从提示文本猜测;这种回合即使模型没有调用工具或没有登记合同,也不得按普通聊天宣布交付。已有项目只有未激活合同且从未产生副作用时才允许直接聊天结束。
- 验收项为明确类型的产物、构建/测试命令、双端视觉或指定固定场景的双端玩法。可信新 Web 游戏由宿主补充构建、双端视觉和玩法底线,不能由模型声明“已有项目”降低。已有项目按冻结的变更范围选择层级;平台美术只在用户目标要求时成为必需项。
- 同一份双端玩法证据可同时满足视觉项,避免重复浏览器运行。构建证据分别绑定源码输入摘要与输出摘要,正常生成 dist 不算源码漂移;浏览器证据绑定构建后的实际运行输入。只有宿主验证完成产生的结构化结果和证据文件摘要能满足合同,项目内自行写出的验证 JSON 无效。
- 产物项的初始摘要由宿主冻结,模型不能提供或在重放时重算。仅登记已经存在的文件不能立即交付:产物必须实际变化/新出现,或有当前指纹的宿主可信验证证据;原生修改和工具修改遵守相同判据。
- `validation.maxRuns` 保留已配置值,语义为执行/返修批次上限;首次执行开启第一批。开发期正常成功命令和源码编辑共享本批,不逐条消耗次数。开始验证后绑定输入,执行失败或验证期间输入漂移使本批进入排空状态,关闭新的执行入口,等已受理操作结束后才开启下一返修批次。读取与结构化编辑不单独消耗次数。
- `validation.maxExecutionSeconds` 默认 900,必须为正整数,是整个 clientTurnId 的累计执行时间上限,换批次不清零。并行操作分别计时累加,内置工具不与 app-server 的外层 MCP 事件重复计费。时间耗尽立即拒绝新执行、写入和付费扩项,保留最近证据与未完成项;Provider 的重试次数保持独立。
- 直接启动 Direct 工具桥的图片生成通知测试,须复用真实宿主执行会话夹具,再发起工具请求;继续验证资源提交后发出 manifest 失效通知,以及空提示词被参数校验拒绝且不发通知,不绕过执行许可门禁。
- 交付合同不参与普通工具准入。模型通过结构化工具登记本轮游戏交付的必需范围与验收项;合同非空、有界且只冻结一次。模型只能声明要求,不能提交“通过”作为证据。后续扩项留到新的用户回合。
- 当用户要求制作、完成或交付游戏时,由 Agent 理解意图并登记合同。首次输入也按同一用户意图条件登记;宿主不做关键词或 LLM 意图分类。无合同回合可以正常写入、执行、生成和验证,也可在必要清理后结束,不触发缺合同返修或生成游戏交付证明。系统提示、工具描述及随包技能遵循同一条件。
- 新合同格式为 `agc-direct-delivery.v2`,只接受非空、有界的 visual/gameplay 要求。artifact/command 类型、初始文件变化摘要及 host-entry/host-build 已删除;普通文件、命令、构建和 agc_run_validation 工具及真实失败回执保留。主动登记新 Web 合同时,宿主沿用首次交付事实补充既有双端视觉/玩法底线并返回完整要求;未登记不补合同。冻结后同参重放复用 newWebGame,不重算范围。用户于 2026-10-05 确认视觉/玩法检查保持现状,本次重构不继续调整这两类判据。
- 同一份双端玩法证据可同时满足视觉项,避免重复浏览器运行。视觉/玩法仍校验双端、固定场景及版本、当前运行指纹、报告与截图摘要;只有宿主真实验证回执有效,项目内自行写出的验证 JSON 无效。删除 artifact 要求不删除浏览器证据文件的防篡改校验。
- `validation.maxRuns` 保留已配置值,仅用于交付回复复核次数;普通执行成功、失败、取消和验证输入漂移均不消耗该次数。验证证据继续绑定输入,失败或漂移仅使相应证据不能证明交付;Agent 可以继续无关工作。
- `validation.maxExecutionSeconds` 默认 900,必须为正整数,是整个 clientTurnId 的累计执行时间上限,同回合重试不清零。并行操作分别计时累加,内置工具不与 app-server 的外层 MCP 事件重复计费。时间耗尽立即拒绝新执行、写入和付费扩项,保留最近证据与未完成项;Provider 的重试次数保持独立。
- `validation.maxTurnSeconds` 默认 1800,必须为正整数,是同一宿主回合从开始起的墙钟上限,重启不重置,用于约束模型空转和超出单个工具事件边界的后台会话。墙钟上限与累计执行时间分别记录,任一耗尽都收束自有执行器;不能把模型等待时间报告成工具执行时间。
- 捆绑 app-server 的原生命令使用已验证的逐次审批能力;宿主只返回单次接受/拒绝,不允许会话授权或 exec policy 修订。第三方 MCP 必须显式启用逐调用询问,不能依赖不可信 readOnlyHint。询问缺少调用 ID 时,按服务器与回合中的并发组保守管理,不能解析展示文案猜测归属。
- 原生、内置与第三方所有入口都经过同一宿主状态;独立工具继续并行,只有身份、批次切换、收尾与必要资源冲突形成短临界区。能力检测失败不得退回无控制执行。
- 原生、内置与第三方所有入口都经过同一宿主状态;独立工具继续并行,只有身份、回合关闭、收尾与必要资源冲突形成短临界区。能力检测失败不得退回无控制执行。
- 上述回合控制适用于 DirectProject。独立客户端 HTTP MCP 显式使用 ExternalClient 来源,保持其原有权限、幂等和浏览器能力,不借用当前项目另一条 Direct 回合的预算或可信证据;新交付合同与托管验证命令工具要求 Direct 会话。服务端 external_mcp 不变。
- 必需证据齐备后,宿主先进入封口状态,拒绝新副作用,再确认在途归零、收束模型执行器并取得进程退出证明,最后重新核对源码、产物和证据摘要;核验成功原子进入 completed 并产出宿主报告。不能先写 completed 再尝试停止后台进程。宿主主动结束模型回合属于交付终态,不触发普通错误反馈或重试。未达标不得用模型最终回复替代验收。
- Windows app-server 在任何模型工具执行前绑定不可脱离的自有 Job,超时/取消/断连时验证整个 Job 已退出。其它平台继续保留受控进程组;未取得完整子树退出证据时按不确定状态报告,不宣称全部后台执行已停止。已受理的远端付费任务保留原不确定围栏,断连不构成自动重放授权。
- 付费许可始终绑定原回合和原租约,不能在容量或同动作锁排队结束后借用新回合。排队可取消,每次新 POST 前与封口共用宿主状态短锁,核对原许可、期限与阶段并持久化提交边界;封口、终止或耗尽后不得新增提交。已经越过提交边界的请求不丢弃,其 operation ID 和不确定状态继续持久化并允许原 GET 对账,多阶段生成的下一次 POST 仍须重新核验。ExternalClient 与手工资源操作保持既有语义。
- 自动交付验收只在模型正常结束本次响应后启动;操作结算、AGC 补丁成功、登记与状态查询都不触发封口。正常完成后证据齐备则进入 Sealing,拒绝新副作用,确认在途归零和执行器退出,再复核运行输入及证据,成功才进入 Completed 并返回宿主报告。未达标仍在原预算与复核次数内反馈修复;取消、耗尽、断连等异常终止保持其原因,不能因证据齐备改成成功。
- Windows app-server 在任何模型工具执行前绑定不可脱离的自有 Job,超时/取消/断连时验证整个 Job 已退出。其它平台继续保留受控进程组;受控归属退出可用于回合收尾及合同完成,但进程组证明不宣称逃逸后代或完整子树均已退出。正常操作回合在 Closing 清理后返回 Working,再由回复复核完成;已封口回合保持 Sealing 直至验收完成。缺失或失败的归属清理仍进入 Interrupted。已受理的远端付费任务保留原不确定围栏,断连不构成自动重放授权。
- Linux 进程组检查排除僵尸;目录枚举失败或数字 PID 的状态不可读取、不可解析时,不以不完整扫描证明组为空,回退到进程组存在性检查。已确认消失的 PID 可以忽略;非进程目录不参与扫描。
- 无合同收尾在预算检查中产生终态报告时,回复复核优先返回已持久化报告;没有终态报告的关闭失败仍保留真实错误,不把预算耗尽改成 Completed。
- 宿主正常关闭连接产生的迟到通知仅作为诊断,不为 Working/Closing/Sealing 生成终态报告,也不改写 Completed 的回复;只有 Interrupted/Exhausted 的既有失败终态可以追加收尾说明。
- 付费许可始终绑定原回合和原操作,不能在容量或同动作锁排队结束后借用新回合。排队可取消,每次新 POST 前与封口共用宿主状态短锁,核对原许可、期限与阶段并持久化提交边界;封口、终止或耗尽后不得新增提交。已经越过提交边界的请求不丢弃,其 operation ID 和不确定状态继续持久化并允许原 GET 对账,多阶段生成的下一次 POST 仍须重新核验。ExternalClient 与手工资源操作保持既有语义。
- 执行账本使用 `agc-direct-execution.v3`,删除 requiresContract,保留回合预算、归属、活动操作、退出证明及合同证据。读取 v1/v2 只白名单移除退役字段,未知字段仍拒绝;旧终态不重开,含旧合同的非终态保留要求并转为 Interrupted,提示新用户回合继续,不因删除要求宣告成功。旧无合同账本移除强制标记后仍按原预算与活动操作恢复规则处理;更早项目侧验证账本缺少可信时间,不能授予同回合新预算。旧合同终态可查询历史但不再按新标准评估。
- 模型一次执行结束时先进入 Closing,关闭新操作和迟到提交,核实自有执行器退出及活动操作归零后才能回到 Working 接受交付反馈;整个用户回合结束后撤销旧许可。Closing 只用于实际关闭,不由普通工具失败触发。普通无合同回合完成不宣称游戏验收通过。
- 本地写事务未结算或失败仍需核对时不得封口。与失败写入重叠的旧写入/旧验证不能清除该围栏;只有失败之后新准入的成功修复或可信验证可以恢复验收。普通文件写入与账户/本地资产导入显式携带原写入许可,取得项目锁后再与宿主状态锁共同核验期限并提交短本地事务;等待锁或下载期间终止的请求不得继续落盘,网络等待不持宿主状态锁。
@@ -169,13 +219,15 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
### 环境与工作流
- 客户端交付配套 Node/npm;发布包从本机已安装且与目标平台/架构一致的工具链制作受校验资源,保留许可并校验内容摘要。安装态不依赖系统 PATH 的 Node;开发态可使用已验证的宿主运行时。不得从项目或相对 PATH 加载伪造运行时。随包运行时是**单架构**官方发行版,因此 macOS 当前只构建 `aarch64-apple-darwin` 单架构包;要出 universal 必须先让 staging 支持按架构各带一份同版本运行时,在此之前 universal 目标失败关闭,不得只带宿主架构那一份糊过去。
- 新建 Web 游戏在生图和大量实现前执行客户端环境预检,检查 Node/npm 的实际版本、浏览器启动和 CDP 可用性。报告只包含安全状态、版本、耗时和错误码。缺失或异常必须尽早返回阻塞,不能指示模型改宿主环境、全盘搜索或自行下载一套运行时。编辑器工程不强制 Web 工具链。
- 新建 Web 游戏在生图和大量实现前执行客户端环境预检,检查 Node/npm 的实际版本、浏览器启动和 CDP 可用性。报告只包含安全状态、版本、耗时和错误码;错误码必须按真实原因分流,浏览器验证只允许在确有证据时使用 `web-preflight-browser-missing` / `-launch-timeout` / `-launch-failed` / `-browser-environment-failed` / `-browser-cleanup-failed` / `-browser-recovery-failed`,取消、证据写入、输入与页面校验各有独立码,未识别原因落回专用 `web-preflight-unclassified`,不得用一个具体子系统码兜底。浏览器恢复诊断只保留阶段、进程退出、清理确认和重试状态等安全字段,不暴露命令行、路径或上游原文。缺失或异常必须尽早返回阻塞,不能指示模型改宿主环境、全盘搜索或自行下载一套运行时。编辑器工程不强制 Web 工具链。
- 预检不安装依赖、不修改项目 revision、不请求平台生成;构建仍执行项目自己的 npm 脚本。Codex 隔离 HOME 与平台凭据边界保持不变,客户端把已验证的运行时加入执行 PATH,不能把宿主凭据目录交给模型。
- 第一轮先明确本次必需玩法、素材和验收项。同批独立读取尽量合并,必需图片一次规划;已有且可用的资产复用。已有目标全部通过后给出交付结果,非阻塞的新点子列为后续工作,不在收尾时主动开启新的生产链。
### 分层验证与预算
- 复用客户端浏览器与现有固定玩法场景。视觉检查采集双端画面/布局/资源/诊断;玩法检查分别在 desktop/mobile 执行明确的固定场景和真实输入,按视口保存结果。旧报告缺少移动端玩法结果时保持未知,不补通过。报告必须声明检查层级;视觉通过不能宣称玩法通过,固定场景通过也不能宣称覆盖未执行的完整关卡。
- 浏览器工具回执在 `mode=gameplay` 时新增 `gameplayResults` 双端有界投影,每个视口包含 `viewport`、`passed`、`diagnostics`、`assertions` 及 `initialPhase` / `initialSequence` / `initialLevel`、`finalPhase` / `finalSequence` / `finalLevel`。`assertions` 仅列当前固定场景的断言名称和通过状态;每端最多公开 4 条诊断,每条最多 512 字符。缺少某端结果时仍返回该视口 `passed=false`,诊断说明证据缺失,断言为空且六个状态字段为 `null`;断言的 `passed=false` 可包含未执行,不能据此断言该断言已独立执行且失败。`mode=visual` 的 `gameplayResults` 为空,并在摘要中明确玩法未执行。摘要从同一投影给出每端首条诊断和首个未通过断言,不能与结构化结果相矛盾。工具回执不得展开完整宿主报告;`reportPath` 只作宿主证据引用。宿主报告结构、验收门禁、持久化预算和私有路径保护保持原合同,旧回执不回填新字段。
- 回执验收覆盖 start 禁用、点击后 phase 不符合和玩法成功;逐视口核对持久报告与摘要中的状态、首条诊断和首个未通过断言,并检查成功、失败两条最终 MCP 回包路径。
- 输入或碰撞改变先做定点玩法检查;纯图像/颜色变化做视觉检查;首次交付和影响闭环的修改做所需玩法验证。新增失败或相关代码变化才重跑对应层,不因改说明文字重复完整验证。
- 内置试玩和客户端托管的外部 Node/npm 验证共用当前 clientTurnId 的持久化预算。客户端分配递增执行序号,模型提供的旧 attempt 仅作兼容输入,不能减少计数或重置预算;同一轮错误反馈、工具切换和进程重启均不能刷新已消费次数。
- 新增本地 validation.maxRuns(默认 3,正整数)独立于 llm.maxRetries;显式配置原样使用,不按角色或运行模式改写。超限直接返回已用/上限和最近证据,停止新的验证。预检与正常构建不计作重复试玩。
@@ -310,7 +362,7 @@ Rust 侧在 `server-rs/crates/shared-contracts` 维护唯一权威 `GameCreation
- 参考选择范围为同一项目已登记图片,可跨栏目、多选,无规范前置时最多 5 张,有规范前置时最多 4 张用户参考(总计最多 5 张);复用资源引用选择组件,不允许文档、音视频、占位或跨项目素材。本地生成命令补最小引用 ID 参数并转换为当前账号绑定下的远端资源 ID,沿用图片生成 API 已有 `referenceImageSrcs`。需要规范图的普通图片请求合并并去重规范引用,总数不超过现有 API 限制;只接受单规范引用的图集操作不显示用户参考选择器,原生提交拒绝额外参考而非静默丢弃。不得降级成纯提示词。
- 占位由宿主按项目与独立草稿 ID 管理,提交后关联任务 ID;失败重试使用同一占位。切项目清理未提交草稿与界面位置,已提交任务继续沿用账本恢复,重开后不承诺恢复未持久化的占位位置。迟到结果先核对项目和任务归属;只有本会话仍存在的占位才应用最新位置。删除占位只隐藏展示,不取消后台任务或丢弃正式结果。
- 参考必须是原生可解码的栅格图片,SVG 不进入参考候选;原生在上传任何引用前预校验整组素材的归属、受控路径、文件及解码,失败不静默丢图。需要重新上传当前账号绑定的参考遵循 `asset.upload` 权限。manifest 读侧的引用形状检查不证明远端账号归属,实际生成始终通过当前账号 binding 解析,不凭历史来源 ID 发起请求。
- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。
- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。GUI 上传(栏目画布底部工具栏的 `upload_local_asset`)与图片类生成**同一口径**:带可选 `targetCategory` 在当前栏目登记,不传(资源面板、UI 编辑器导入、聊天附件)时保持按内容证据派生 kind 的行为。
- 生成面板打开后,占位和面板需处于当前画布标题栏与底部工具栏之间;面板复用公共外观,空间不足时面板内部滚动,提交按钮可达。音频/BGM 占位绑定原有 operation/idempotency 身份,进行中不允许换身份重复提交;失败可用原身份重试,成功结果与图片一样接管占位。关闭未提交浮层保留可继续编辑的草稿,删除占位才丢弃该草稿。
- 整理范围为当前栏目页全部资源;“所有资源”页为当前项目所有可展示资源,总览不新增整理行为。重排结果成为自动坐标,可撤销恢复原坐标与手动标记;历史仅保留当前会话,切项目清空。多选仅作用于当前画布可见选中资源,不携带筛选隐藏或跨栏目残留选择;取消手势恢复拖动前坐标,切项目清空选择。
- 当前素材名以现有正式命名链路为准:生成时 assetName 参与落盘名称,重命名更新文件名;卡片消费正式资源 label,不从临时输入或历史任务名覆盖后续重命名,不新增平行显示名持久化。若原有命名链路丢失 assetName,则修复原链路,而非只在卡片本地伪造。文档卡不显示任何正文摘要,但详情原文与 JSON 识别读取不变。
@@ -504,6 +556,11 @@ npm 游戏的可预览产物固定为对应 package 目录下的 `dist/index.htm
## 2026-08-25 账户 / 项目画布 / 本地素材导入
- 账户素材库总量不作为客户端查询、预览或按 ID 导入的拒绝条件。账户查询继续先读取后端快照,再按静态图片、文件夹与关键词筛选并分页;`offset` 接受可表示的非负整数,超过结果总数返回空页,`limit` 保持 1–100(默认 100),结果保留 `total / offset / limit / nextOffset`。Direct MCP 的关键词匹配素材名或文件夹名,Runtime 保留现有素材名匹配语义。
- Runtime 下发给模型的 `asset.library.list` strict 原生函数 schema 必须与执行层分页边界一致;验收须校验 `build_agent_runtime_native_function_tools` 生成的完整函数参数,确认 `offset=501/600` 可用、负数和非整数偏移及 `limit>100` 被拒绝,同时保留可选字段的 `null` 语义。直接调用 observation 函数不能替代模型可见 schema 的验证;定向用例为 `account_asset_native_function_schema_accepts_later_pages`。
- 账户查询只把当前页安全元数据作为工具文本返回,不把全库作为附件交给 Agent。指定 ID 导入仍先读取账号快照再选取目标;单批数量、下载体积、账号/项目归属、会话、媒体类型和原子登记校验保持有效。绑定画布资源数量与项目文件发现的既有边界不随账户总量限制解除而放宽。无需后端 API、DTO、数据库或持久化迁移;全库传输与解析开销由后续 #574 的服务端分页及按 ID 读取处理,不阻塞 #549 的客户端修复。
- 平台账号导入在下载后重新校验会话并取得身份租约,租约覆盖本地写入、manifest 登记与 revision 推进;提交过程不得再次读取同一登录态互斥锁,避免在身份保护内自锁。身份变更仍须在取得租约时拒绝,取消写入许可仍不得写入。
- 验收覆盖 500/501 项账户快照、按文件夹/关键词查询、偏移超过 500 的后续页、越界空页和大库指定 ID 导入,并复核无效 ID/不支持媒体拒绝、安全投影、取消写入和账号切换。使用 `agent_asset_import_tests`、`account_asset`、`semantic_resource_tools_reject_unreviewed_or_inconsistent_arguments`、`cancelled_import_write_permit`、`platform_session::tests` 定向 Rust 用例及提示词合同测试;模拟平台证据与真实账号验证分别记录。
- 素材读取区分三类来源:`asset.list` / `agc_list_registered_assets` 是当前项目本地 manifest,`agc_list_project_files` / `file.list` 只发现项目目录中实际存在但可能未登记的文件,`asset.library.list` 是当前登录账号素材库,项目画布资源读取是当前网页项目/画布的完整图片清单;账户素材库不能替代项目画布清单。
- Agent 只接收稳定素材 ID、类型、尺寸和项目相对路径等安全投影。客户端负责重新校验账号/项目归属、换签下载、媒体校验,以及 manifest/画布原子登记;不得向 Agent 暴露绝对路径、签名 URL、objectKey、token 或 Cookie。
- `canvas.asset_import` 支持账户/画布资源 ID 和项目内本地相对路径。项目文件发现结果以 `assetImportable` 明确区分当前可登记的已识别图片、字体、音频、视频、文档、代码与**引擎资源**(Cocos Creator 的模型、动画、场景/预制体、材质/特效、图集与压缩纹理容器)与其它文件;Agent 只能提交前者。引擎资源在资源画布上是**只读预览**:模型出缩略图、序列化资源出结构摘要、客户端解不了的容器出类型卡,不承接编辑与派生;`.meta`、`library/`、`temp/` 等引擎生成物仍然只可发现、不可登记。导入拒绝路径穿越、`.agent`、符号链接/reparse point 及敏感配置文件;外部宿主文件须由 UI 原生文件选择器授权后导入,不开放任意绝对路径。
@@ -922,6 +979,10 @@ Agent Runtime 负责:
- 2026-07-10 补充:后台任务工具箱已加入 `preview.start`。Agent 可在 loop 中自行请求启动当前项目的本地 HTTP 预览;Runtime 会复用 `preview.start` 策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑,并把 `agent.runtime.preview.start` 写入 `.agent/agent.db`。该 observation 只向 LLM 返回 localhost URL 与端口,不返回用户项目绝对路径。
- External Editor 的远端项目和资源属于服务 principal,不属于本地项目的跨账号身份。`.agent/manifest.json` 中 `source.canvasProjectId / resourceId / assetObjectId` 只记录生成来源,不得作为当前登录账号的 active editable binding。客户端在 `.agent/runtime/external-editor-bindings/` 私有保存账号级投影:项目键至少包含服务 origin、principal 和本地 `projectId`,资源键再包含远端项目、本地 asset ID、源文件 SHA-256、媒体类型与 canonical asset kind;值保存该 principal 下的远端 project/folder/resource/object 身份。Token 轮换继续复用同一 principal binding,账号、服务、源字节或 canonical kind 变化必须重新建立;同名本地项目和项目改名不得参与权威匹配。
- binding 的 lookup key 与稳定 payload 内容指纹分开校验;远端 project/folder/resource/object ID、object key 或尺寸被单独改写时必须失败关闭。同一本地项目和 principal 的首次项目 binding 建立必须串行化覆盖“复读 binding → 必要的远端创建 → 原子安装 binding”,不得因并发任务创建孤儿画布。External v1 项目、素材目录和项目资源创建请求携带由 binding key 派生的稳定 `Idempotency-Key`;服务端在 owner 与接口命名空间内生成稳定 ID,并允许同键同正文安全重试,使远端响应后、本地 sidecar 落盘前崩溃也不会重复创建。
- 素材生成准备的目录确认采用单目录读取:站内 `GET /api/editor/assets/folders/{folderId}` 与开发者 `GET /api/external/v1/editor/assets/folders/{folderId}` 返回现有 `{folder: ...}` 数据结构,后端按目录主键读取并校验 owner,不读取账户素材、不创建默认目录、不执行媒体修复。External 入口要求 `editor:asset` scope;缺失目录返回 404,越权返回 403,未认证返回 401。原素材库快照及目录创建、修改、删除接口的合同不变;不修改持久化表结构。先部署新增后端接口,再发布依赖该接口的客户端;新客户端不回退完整素材库。
- 已有当前账号项目 binding 或恢复 partial 的目录身份时,在新的生成准备中按该 ID 确认目录并核对响应 ID;合法空目录也可继续。首次没有可复用目录身份时直接沿用稳定幂等创建和冻结正文,不先列举素材库,不按同名认领目录。partial 继续只承载中间恢复进度,成功写入完整 binding 后删除;既有生成账本的 `prepared / accepted / legacy-completed` 恢复仍使用冻结快照,不重新查询或创建目录。
- 单目录查询超时、断网、服务异常、鉴权失效、越权或会话切换时停止准备并保留现有 binding / 恢复证据,不解释为目录缺失、不自动 POST 新目录。明确 404 时提示绑定目录不存在并停止,不用原键复活目录、不换键重建或重发生成;自动重新绑定与素材分页、关键词筛选、按素材 ID 导入不属于该行为。验证覆盖全库不可用而单目录正常、空目录、首次建绑、缺失与越权、查询失败及切号,并保留并发单次创建、创建响应丢失、冻结正文及生成账本恢复回归。
- 单目录合同的定向验证使用 AGC Rust `external_canvas_context`、`canvas_generation_tests`、`project::resource_editor::tests`、`background_removal_tests` 与 `direct_tools_mcp::tests`,工具链 HTTP 夹具按单目录 GET 返回 `data.folder`;后端使用 `external_editor`、`external_mcp` 和 `protected_route_matrix_keeps_auth_methods_and_head_behavior`,覆盖 OpenAPI 自动增加单目录 MCP 工具后的目录数量及名称唯一性。`npm run check:editor-idempotency-procedures` 在独立 SpacetimeDB 中验证真实目录读取、归属、删除与只读行为,host 单测不能代替该运行时证据。
- 当前 binding wire schema 为 v2,payload fingerprint 必填。已落盘的 v1 project/resource JSON 只能经独立严格 wire 结构读取,在 lookup key、principal、本地 source、远端字段、对象键、时间和尺寸等不变量全部通过后重算指纹并原子回写 v2。不得用 `serde(default)` 把未知或篡改文档混入迁移。
- 已完成的本地正式资源以本地文件和 manifest asset 身份为权威。切换账号后,当前账号没有资源 binding 时,必须从本地字节上传、confirm 并登记到当前账号的远端项目后再编辑或派生;任何请求都不得把旧账号的 `canvasProjectId / resourceId / objectKey` 与新账号 Token 组合。新绑定不得覆盖历史来源,也不得迁移或删除旧账号远端对象。已经 `prepared / accepted / running` 的生成或编辑 operation 仍冻结原 principal、原正文和原 operation;切换账号只能停止恢复并保留对账证据,不能改绑、重放或重复扣费。
- 远端提交返回 `operationId` 后先持久化 accepted 身份,再在每次 poll、download 和本地 commit 前复验冻结会话;同步本地 commit 持有冻结会话租约,使切号只能发生在完整安装前或安装后。Tauri 手工生成同样使用稳定的 durable operation slot,不得使用进程内临时幂等键。历史资源编辑账本从 manifest 远端 resource ID 迁移到本地 asset ID 时,只允许在本地 asset、路径和内容摘要仍一致时兼容旧指纹,新请求不得恢复信任 manifest 远端 ID。
@@ -1612,6 +1673,22 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过
- **素材重命名 UI**:新增 `ResourceRenameDialog`,入口在资源卡选中工具条的 `extraActions`(与「分类与标签」并列),只输入新文件名。前端严格按 `deny_unknown_fields` 只传 `{ projectPath, assetId, newFileName }`;成功后复用 `reloadManifestAfterAssetCommand` 这条既有 manifest 重载路径刷新资源卡与 `@` 面板显示名。资料性边界沿用 Rust 侧口径:不改游戏源码里对旧 `assets/<name>` 的引用。
- **过渡 allowlist 收敛**:`check-config.mjs` 的 `allowedUncalledTauriCommands` 删除 `normalize_local_project_raster_resource`——该命令在 C3 之后的选中工具条里已有真实前端调用方,不再是「无人调用」的过渡项。
- **验证**:AGC 前端全量 1080 passed / 4 skipped / 5 failed(5 条失败全部是 C3 交互改造后仍待更新的既有用例,与本包改动无关:`renders immutable manifest versions…`、`keeps the resource toolbar and canvas interactive…`、`loads registered documents, art media, video, and audio…`、`moves historical resource positions…`,以及 `projectResourceLiveIntegration` 的一条选中按钮选择器);本包新增的 25 条测试(标签库 5、筛选条 3、参考图弹窗 6、参考图模型 7、重命名 2、版本判定 5、版本切换 3,其中 5 条落在共享契约与共享组件目录)全部通过;共享美术画布组件 1385 passed;`cargo check --all-targets` 通过;`asset_rename` 9 passed;`npm run check:encoding` 与 `git diff --check` 干净。
## 素材导入与资源提交的项目锁等待
- 本地素材导入、账户素材下载后的本地提交、资源编辑资产提交及其已完成结果恢复必须吸收短暂项目写锁争用;等待沿用现有有界预算与类型化失败分类。耗尽时报告可读取的持锁方身份,身份不可读时明确说明,不推断锁无人持有。Windows 删除挂起与 ACL 的现有终态判据保持不变。
- 同步取锁、等待、提交和释放在同一个阻塞任务内完成,不占用异步 runtime worker。等待不持有平台登录态互斥锁;取得项目锁后再验证账号身份并持有身份租约覆盖本地提交。导入的原始 WritePermit 在等待后重新校验,取消或过期的许可不能提交。
- 项目锁内重新读取 manifest 与 revision,复核项目和资源身份,保持并行提交无更新丢失。跨进程排他、同线程重入和现有残留回收判据不变,不新增同 PID 豁免。
- 资源编辑继续使用原 operation、幂等键、账本和 staging;本地提交争用不得重发远端生成。阻塞提交开始后,operation 与项目提交互斥 guard 归阻塞任务所有,即使等待它的异步调用被取消也不能提前释放它们。网络请求不在项目写锁内执行。
- 不改变 API、DTO、账本 schema 或已有项目数据,不要求迁移;其它零等待入口按各自专题处理。本节覆盖下文旧事项中本地/账户素材导入与资源提交的部分。
- 验收证据:三条入口在 conversation.write 持锁时等待后成功;等待期间单线程 runtime 心跳继续推进;取消导入零写入;并行提交 manifest/revision 完整;耗尽、权限拒绝、活跨进程锁和残留回收沿用锁测试;远端已完成的资源编辑恢复保持同 operation 且零新增远端请求。真实 Windows 删除挂起行为仍需 Windows 环境验证。
| 行为 | 自动化证据入口 |
| --- | --- |
| 导入等待、账号切换与许可取消 | `commands::agent_asset_import_tests`、`direct_tool_bridge::tests::cancelled_import_write_permit_keeps_files_manifest_and_revision_unchanged` |
| Runtime 与切图登记不阻塞 worker | `runtime_local_import_waits_without_blocking_worker`、`cut_image_registration_waits_without_blocking_worker` |
| 并行提交、超时恢复、取消时互斥生命周期 | `project_mutation_lock_serializes_independent_asset_commits_without_lost_updates`、`resource_commit_timeout_preserves_staging_and_resumes_original_operation`、`cancelled_resource_commit_wait_keeps_operation_and_project_guards` |
| 已下载结果和已提交回包复用、权限与跨进程边界 | `downloaded_resource_commit_and_replay_wait_without_new_remote_work`、`resource_editor::background_removal_tests`、`project_lock_recovery`、`project::write_lock` |
## 2026-09-10 Direct 写通道项目锁等待、持锁方可诊断与权限分类
- `agc_write_file` 是用户直接触发、失败即整轮无法落盘的项目写入通道,原先却用零等待 `acquire_project_write_lock`:任何重叠都在 24-42ms 内被判成“项目正在被其他写操作占用”,而 `file.write / file.patch / file.delete` 等入口用的是约 10 秒有界等待。现统一为 `acquire_game_creator_agent_runtime_project_write_lock_with_wait`:短暂重叠排队等成功,只有预算耗尽才报出带持锁方身份的错误;同一轮并行写多个文件按同一把锁串行。这是 2026-07-22 同一形状修复在 Direct 通道上的补齐,与 2026-08-13 一节“这些结果统一投影为争用并进入既有有界等待”的口径一致。**失败耗时是判据**:几十毫秒说明该入口没等,不是锁没释放。
@@ -1653,9 +1730,9 @@ DirectProject 在收到完整游戏策划或游戏制作请求后,必须把视
## 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 不得进入对话或用户可见文本。
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / occurredAt / elapsedMs / message / error / detailRef`,其中 `message` 是产生失败的 typed 错误在失败现场写好、脱敏后的人类可读文案,`error` 是同一个 typed 错误 enum 的原样序列化(一个 case 一个变体,供开发者按变体与字段定位;没有 typed 错误的调用方写 `null`),`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示 `publicText`,点击详情后按 `detailRef` 读取有界、脱敏的诊断,不直接展示私有 `detail`。
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示宿主给的安全文案;诊断正文只留在 `detailRef` 指向的有界、脱敏记录里,不进入用户可见文本。
前端取回 `detailRef` 的口径是失败文案末尾的固定后缀「;详情:<detailRef>」:Rust 侧 `direct_codex_failure_text_keeps_the_detail_ref_marker_for_the_renderer` 与前端 `tests/agentRuntimeErrorDetail.test.ts` 各自钉住同一份文案形状与它的解析,任一侧改文案或改解析都会变红。失败提示只展示映射后的安全 `publicText`(v1 历史形状与 v2 现行形状都映射成「阶段 + 摘要 + 建议 + 是否可直接重试」),诊断正文不在提示里预读、也不写入历史投影,而是由聊天状态栏下方的「查看详情」入口按需调用只读命令读取并二次脱敏;这条交互由 `tests/appSurface/chat-composer.suite.ts` 的「失败提示保留可执行原因,诊断正文只在「查看详情」时读取」与 `tests/agentRuntimeModel.test.ts` 的 v2 映射用例钉住,渲染层仍不参与错误分类。
@@ -1669,7 +1746,7 @@ DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执
Direct 回合的所有权属于进程内项目身份锁,不属于当前页面。离开工作台或切换到首页时,正在运行的回合继续执行;重新进入项目时,只读活动回合快照负责恢复 clientTurnId 与运行状态,Direct 回合事件负责更新进度。Thread Manager 的 Provider turnId 与事件序列不得当成 clientTurnId 或展示事件序列;其通知只触发快照和历史同步,回放的终态不能重新创建活动回合或“正在提交回复”。定期复核同一份活动快照以弥补页面切换时丢失的结束事件;失败读取保留已知状态,过期读取不能覆盖新回合。活动回合结束后移除快照并解除发送阻断;没有活动回合的项目保持原有发送行为。
壳层窗口标题栏的“正在运行”下拉入口只呈现活动 Direct 回合快照:常态只显示最后开始的项目,展开后按开始时间排序,显示全部项目的项目名、状态、活动时长并允许进入对应项目。快照读取失败只显示读取失败并保留上一份结果,不得改写成权限、审批或业务失败;入口不建立第二份运行真相。应用重启后的恢复、取消入口和非 Direct Agent 项目不在本合同内。
壳层窗口标题栏的项目入口只呈现活动 Direct 回合快照:常态显示当前打开的项目名(只在工作台视图认这个身份,不在项目内时回落到最近启动的在跑项目),正在运行的项目只占圆点、数量徽标与展开菜单;展开后按开始时间排序,显示全部在跑项目的项目名、状态、活动时长、当前项目标记并允许进入对应项目。快照读取失败保留上一份结果、不顶掉当前项目名,读取结论落在圆点告警色、标签提示与展开菜单里、不占主文案,不得改写成权限、审批或业务失败;入口不建立第二份运行真相。应用重启后的恢复、取消入口和非 Direct Agent 项目不在本合同内。
活动回合快照命令是进程内 Tauri 只读命令,不进入公共 API 或持久化协议;字段包含 `projectPath / projectName / turnId / status / activity / startedAt / updatedAt / sequence`,状态和序号与既有 Direct 回合进度事件一致。
@@ -1806,9 +1883,9 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
## 2026-09-21 统一错误事件同时落到 AppData 应用日志
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`hint / summary / detail / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`message / error / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;`summary` 按 320 字符、`detail` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(summary / hint / detail)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;sidecar 里的 `message` 按 8 KiB 上限、应用日志的 `message` / `error` / `metadata` 按(1200 / 400 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(message)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
## 2026-09-23 AGC UI 设计文档 Agent 工具化重写
@@ -98,7 +98,7 @@ readHistory(threadId, { beforeItemId?, limit }) -> {
}
```
`subscribe` 不返回完整历史。`lastCompletedItemId` 只是历史读取锚点,前端自行按 item ID 懒加载需要的历史切片。`events` 是当前运行态重建所需的未完成 item 原始事件,以及当前 turn 的生命周期锚点;前端用同一个 reducer 重放 bootstrap 和后续事件。Rust 不保存或理解前端 reducer state。只要 DirectProject 历史切片返回 `hasMore`,聊天视图必须显示“显示更早的对话”入口,并允许按钮或滚动触发下一页,即使当前可见消息窗口没有隐藏消息。
`subscribe` 不返回完整历史。`lastCompletedItemId` 只是历史读取锚点,前端自行按 item ID 懒加载需要的历史切片。`events` 是当前运行态重建所需的未完成 item 原始事件,以及当前 turn 的生命周期锚点;前端用同一个 reducer 重放 bootstrap 和后续事件。Rust 不保存或理解前端 reducer state。只要 DirectProject 历史切片返回 `hasMore`,聊天视图就必须能继续往前取页,即使当前可见消息窗口没有隐藏消息;入口形态(触顶自动加载 + 填充视口补载、加载行与失败重试、前插不跳)由 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md) 规定,已不再有常驻按钮。
`consume` 不接收或返回 cursor。每个 subscriber 在 Rust 内部持有自己的 cursor,并在加锁的临界区内完成过期判断、读取和 cursor 前进。前端只持有 `subscriptionId` 与 reducer state。并发 `consume` 不重复返回同一批事件。
@@ -106,7 +106,7 @@ readHistory(threadId, { beforeItemId?, limit }) -> {
`subscribe` 在同一个边界内先把新 subscriber 的游标钉在当时的队尾,再收集 bootstrap 的运行态事件,因此 bootstrap 返回的那批事件**就是**该 subscriber 此刻应处理的事件:前端直接 reduce 它们即可,不存在"先补一次 `consume` 才能拿到已暂存事件"的步骤。第一个例外只有回执竞态:Rust 侧一注册完 subscriber 就开始 `notify`,而前端要等回执到达才知道自己的 `subscriptionId`,这段窗口内的通知拿不到订阅身份。前端因此必须记一笔欠账,回执到达后立刻补一次 `consume` 取回那批事件;否则事件会卡在队列里等下一次通知,而一次回合的最后一个事件之后可能再也没有通知。除此之外不轮询,也不设任何定时 `consume`——唤醒只由 `notify` 负责。用定时器兜底既自举不了(判断"有活动回合"本身依赖事件),也把唤醒机制变成两套。
首屏历史不通过"读取整份对话"的命令获取:`subscribe` 返回的 `lastCompletedItemId` 就是首屏锚点,前端据此调用 `readHistory` 取最近的切片,再按滚动或按钮继续向前分页。系统不提供返回整份对话历史的命令。
首屏历史不通过"读取整份对话"的命令获取:`subscribe` 返回的 `lastCompletedItemId` 就是首屏锚点,前端据此调用 `readHistory` 取最近的切片,再按滚动继续向前分页(入口形态见上条 ADR)。系统不提供返回整份对话历史的命令。
### 事件和顺序
@@ -4,10 +4,10 @@
本方案的**卡片表现层**(折叠 / 展开、标题与摘要文案、耗时与时间显示、脱敏、无障碍、样式)仍然是有效契约;**数据来源层**已被 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 取代,边界改为:
- DirectProject 聊天框的工具卡片由**运行态事件 + 项目对话历史**在前端投影生成(`features/project-workspace/directThreadItemProjection.ts`),不再读取 `tool-calls.jsonl`;`read_direct_tool_calls` 命令与 Rust 侧 `read_direct_tool_calls_at` 回读函数已删除,该文件现在只有 DirectRuntime 的写入。
- 报文中不再有 `toolCalls` 增量字段与 `GameCreatorDirectTurnUpdateEvent` 这条实时链路:卡片形状由前端从脱敏原始条目生成,事件里只有 `item.started` / `item.completed` / `item.delta`(线上模型见 `agent/direct_thread_wire.rs`,由 ts-rs 导出绑定)。
- DirectProject 聊天框的工具卡片由**运行态事件 + 项目对话历史**在前端投影生成(`src/view/project-development/chat/conversation/directThreadItemProjection.ts`);工具条目在读取期由 `agent/thread_manager/wire.rs` 统一脱敏与截断,不再有独立的工具调用账本或回读命令。
- 报文中不再有 `toolCalls` 增量字段与 `GameCreatorDirectTurnUpdateEvent` 这条实时链路:卡片形状由前端从脱敏原始条目生成,事件里只有 `item.started` / `item.completed` / `item.delta`(线上模型见 `agent/thread_manager/wire.rs`,由 ts-rs 导出绑定)。
- 卡片身份只有一个 `itemId`(工具条目在 `project.jsonl` 里带的两个 id 已在 Rust 边界归一),前端卡片形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`:聊天卡片不再有回合身份。
- 下面「### 1. 工具调用条目」「### 2. 实时事件」「### 3. 回读命令」三节描述的是 DirectRuntime 自己的账本(`tool-calls.jsonl` 的写入形状与脱敏规则仍然有效,DirectRuntime 保留),**不再是 DirectProject 聊天框的读路径**;「### 4. 前端合并与渲染」中按 `turnId` 归并、按 `turn-stream.jsonl` 的 `seq` 交替的规则已作废,改为按事件顺序 + 历史文件顺序投影。
- DirectRuntime 曾经的 `tool-calls.jsonl` / `turn-stream.jsonl` 账本、写入器与进度事件已整体删除(2026-09-30,issue #553);本方案原先描述这些落盘与事件契约的「工具调用条目」「实时事件」「回读命令」三节一并移除,改为按事件顺序 + 历史文件顺序投影。
## 一句话交付
@@ -44,60 +44,24 @@
- 同一用户消息从本地发送转为正式条目时必须保留原发送时间,不能因去重丢掉该时间而改用启动应答后的观测时间。回合明确结束时,即使当前 live 集合为空,也应将终态边界关联到已知的本轮用户条目;不得仅按“历史最后一项”猜测或向无关旧回合补时间。
- 生命周期事件可携带已有用户条目的 `userItemId`,用于把时间边界精确关联到同一条历史消息(不产生新的回合 ID 或持久化字段)。恢复时该关联随 Thread Manager 原事件重放;带旧用户身份的终态不能收口另一条新请求。缺少身份的旧事件不据时间猜测归属。
## 背景与现状(已核实)
## 背景与现状(2026-09-14 立项时点)
> 以下记录立项时点的现状,用来解释这次改造的动机;其中「工具调用不留痕」「没有结构化工具调用」等结论都已被后续实现与 2026-09-16 修订取代,**当前状态以文首修订与 ADR 为准**。
- 数据来源: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`,**没有结构化工具调用**。
- 前端事件:`GameCreatorDirectTurnUpdateEvent`(该事件已随 issue #553 删除)只有 `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. 工具调用条目(采集与持久化形状)
工具卡片的两处数据来源是 `.agent/conversations/project.jsonl` 的原始条目与 Thread Manager 的运行态事件;Rust 出口只做挑字段、脱敏、截断,卡片形状、可见性、合并与计时全部由前端投影完成。
新增独立历史文件:`<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. 落盘契约(写侧)
DirectRuntime 写 `<projectRoot>/.agent/conversations/tool-calls.jsonl`;回读命令与 Rust 侧回读函数已随聊天读路径退役删除,下面的语义约束的是**写进文件的行**。
- **上限语义**:200 条是「按时间保留最新 200 条」。超出时更早回合的卡片会被**静默丢弃**(老回合卡片会消失),不做分页、不做历史回填;同一 `id` 的多条记录先按 `updatedAt` 合并,再按时间正序裁剪。
- 历史文件缺失 → 返回空数组,不报错。
- 单行损坏 → 逐行读字节并逐行解码,跳过该行继续,不整体失败;只有损坏字节与下一行黏成一行(例如写入被截断、缺失换行)时,被丢掉的也只是那**一行**,其后的合法记录必须继续读回(与 Codex item 流一样是"尽力而为"的展示数据,不是业务真相)。
### 4. 前端合并与渲染(回合唯一归属,连续工具成块)——已作废,见文首修订
- 加载对话时按 `turnId` 归并为唯一回合容器,用户消息保留在该回合前部。有 `turn-stream.jsonl` 时,文本与工具按 item `seq` 交替,连续工具合为一块,遇到文本另起一块;没有流的历史回合才采用“工具块 + 历史正文”。
- 加载对话时按历史条目进入 `project.jsonl` 的原始顺序投影:用户消息、文本与工具块保持原序,连续工具合为一块、遇到文本另起一块,不按 `turnId` 重新归并、也不再有 `seq` 交替。
- 回合完成后,中间文本及所有工具块统一收进默认关闭的“执行过程”;最终回复及失败提示留在外面。展开后仍按原顺序查看中间输出和工具详情;运行中不使用外层折叠区。用户消息的发送时间从消息自身的历史时间读取,不能拿工具起点补造。
- 实时与回读共用同一投影,正文、工具和耗时不另建实时/未归属渲染出口。先在完整历史按消息身份关联,再分页;禁止按第 N 个工具回合匹配第 N 条用户消息。详情通过当前回合 `callId` 关联;同项目回读与实时增量幂等合并,切项目清空旧状态。完整合同见 [AGC 实施计划](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的“DirectProject 回合展示唯一归属”。
- 块 DOM 与交互(对齐 Codex):
@@ -146,32 +110,28 @@ DirectRuntime 写 `<projectRoot>/.agent/conversations/tool-calls.jsonl`;回读
## 验收判据(每条都要有可复现证据)
1. 一轮真实回合里,`tool-calls.jsonl` 里同 id 只有一行,`completed` 后 `status` 变 `completed`。
1. 一轮真实回合里,按项目对话历史投影出的工具卡片同 id 只有一张,`completed` 后状态变 `completed`。
2. 刷新页面 / 重开项目后,工具调用卡仍在原位(不重复、不丢)。
3. 实时回合中卡片状态从 `running` 走到 `completed`。
4. 折叠/展开键盘可达(Tab 到头部按钮、Enter/Space 切换、`aria-expanded` 同步)。
5. `Read` 到的既有对话(无工具调用)行为不变;老事件(无 `toolCalls` 字段)行为不变。
6. 脱敏检查:`tool-calls.jsonl` 里不出现 API Key / Token / 绝对用户目录。
5. `Read` 到的既有对话(无工具调用)行为不变。
6. 脱敏检查:卡片摘要、输入 / 输出与展开详情的落盘内容中不出现 API Key / Token / 绝对用户目录。
## 不做项(本次明确不做)
- 不做工具调用的重试 / 取消 / 编辑按钮。
- 不做 diff 级展开(只在展开态列文件路径与变更类型)。
- 不做工具输出的完整回放、不做卡片内搜索。
- 不改 `.agent/conversations/project.jsonl` 的既有格式与注入 Codex 上下文的路径(新数据走独立文件,避免污染模型上下文)。
- 不改 `.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/`(卡片渲染、折叠交互、重开项目后合并、老事件兼容)。
- Rust(`apps/ai-game-creator-shell/src-tauri/src/`):`agent/codex_app_server/mod.rs`(item → 结构化采集)、`agent/thread_manager/wire.rs`(读取期脱敏、截断与 ts-rs 导出模型)、`agent/direct_runtime/mod.rs`(运行态事件)。
- 前端(`apps/ai-game-creator-shell/src/`):`app/types.ts`(卡片类型)、`view/project-development/chat/conversation/directThreadItemProjection.ts`(历史条目 → 卡片投影)、`view/project-development/chat/components/ToolCallGroup/`(卡片组件与文案)、`packages/shared`(共享过程样式)。
- 测试:Rust 侧单元测试(历史条目投影、脱敏覆盖与幂等性、相对路径形状、截断)、前端 `tests/`(卡片渲染、折叠交互、重开项目后合并)。
## 实施顺序(每步都要能独立验证)
## 实施状态
1. Rust 采集 + 独立文件 upsert:先只落盘,单元测试证明幂等与截断。
2. 事件字段下发 + 前端类型:老路径不受影响(回归现有 appSurface 用例)。
3. Tauri 回读命令 + 前端加载合并:刷新后卡片存在。
4. 卡片组件 + 样式 + 无障碍:折叠/展开 + 键盘。
5. 真实回合联调(本地 Tauri 起一轮),截图留证。
卡片表现层已按本方案落地;数据来源层在 2026-09-16 改为「运行态事件 + 项目对话历史」前端投影(见文首修订),原先的直连工具账本链路已随 issue #553 清理移除。
File diff suppressed because one or more lines are too long