# 决策记录 ## 2026-10-05 创作者主页与关注粉丝的产品边界 - 产品已确认:桌面第四项“创作者主页”默认进入当前账号主页,“我的”移到第五项;他人的关注/粉丝列表公开可查看,自己或他人的两类列表均可点击用户进入其创作者主页。 - 关注为单向关系,取消回关与移除粉丝分别影响不同方向;只有本人可移除自己的粉丝,自己不能关注自己。 - 他人的关注、粉丝列表统一只读:保留头像/昵称进入用户主页,不显示任何关系操作按钮;前后端均不额外检测访问者与列表用户的关注关系,已有缓存也不用于显示关系动作。 - 已确认:移动端入口为“游戏 / 创作者主页 / 我的”;自己的主页也只展示公开游戏;自己的关注列表取消后当前行暂留以便重新关注;移除粉丝二次确认,取消关注不弹确认。 - 用户明确本次不额外改造游戏目录分页:既有游戏广场和“我的游戏”保持现状,作者主页按作者过滤后沿用最多 48 项限制。新关注/粉丝列表的分页仍按主规范设计。 - 关系纯规则放在 `module-auth::creator`,现有认证服务通过 `services` feature 隔离宿主依赖,数据库 WASM 只使用纯规则。私有 `user_follow` 不参与认证快照替换;只通过受信服务过程读写,HTTP 操作者取认证身份。 - 行为真相见[创作者主页与关注粉丝合同](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#创作者主页与关注粉丝合同),工程落点见[工程设计](../../technical/【技术方案】创作者主页与关注粉丝工程设计-2026-10-05.md)。后端已完成隔离验证并由用户验收通过;页面工程验证通过,用户已要求提交并推送;证据归并工程设计,已完成临时计划删除,未上线。 ## 2026-10-03 首页自动建项与 AI 项目命名解耦(Issue 599) - 背景:首页「开启创作」原先串行执行「Web 预检 → `await suggest_automatic_project_name` → `create_automatic_local_game_project`」。项目名称不是创建工作区、导入附件或发起首轮创作的前置条件,命名请求(`AUTOMATIC_PROJECT_NAME_TIMEOUT_MS = 15s`,正常请求同样占时)却把用户按在「正在创建工作区」上。 - 决策(建项与命名解耦):建项固定传 `name: null`,由宿主既有兜底名(`GameAgent 项目 <8 位短 id>` / `策划项目 <8 位短 id>`)落盘并立即进入项目;命名请求在建项前并行打出、结果交给后台任务。后台拿到合法名称后调用新增的 `rename_local_game_project_if_unchanged(projectPath, expectedProjectId, expectedName, name)`:只有「项目 ID 相同」且「当前名称仍是本次创建的兜底名」才改名,返回 `renamed: true/false`(跳过时不写盘、不是错误)。用户已手动改名、项目 ID 不符、名称与现状相同一律跳过;空 `expectedProjectId` / `expectedName`、空名 / 控制字符 / 超长一律失败关闭。 - 决策(纳入「生成任务」体系):自动命名做成**项目工作台那条既有「生成任务」列表里的一条后台任务**,而不只是页面里的一个 promise。账本记录加 `taskType`(`asset-generation` / `project-naming`,缺省 `asset-generation`)、`kind` 变可选,schema 升 `agc-asset-generation-task.v2`(读取同时接受 v1/v2,v1 记录按素材任务读回);新增 `enqueue_local_project_naming_task`(排队中)/ `update_local_project_naming_task`(命名中 → 已完成/失败),展示名固定「AI 项目命名」,与素材生成共用同一份账本、同一个变更事件与同一个侧栏。状态流转:排队中 → 命名中 → 已完成(已应用 AI 名称 / 建议名与现状一致 / 用户已手动改名而跳过)或失败(无可用名称 / 自动改名失败,均保留兜底名)。命名任务由命名链路推进、**不进 live 集合就会被中断收口误判**,所以 enqueue 写账本前登记 live、update 终态写盘后摘除(中断残留仍按命名口径收口为失败)。前端按 `taskType` 把命名记录从素材任务分支里剔除,命名行不渲染缩略图/提示词/派发与定位动作,结论只在终态显示。 - 边界:改名是簿记写入,与既有 `rename_local_game_project` 同口径**不推进项目 revision**(推 revision 会让运行时验证凭证无故漂移);返回的 `revision` 是当时盘上的值。后台改名结果写「当前项目上下文」与「最近项目行重检」必须等建项主体收尾(`entrySettled`):AI 比进项目更快时直接写上下文会被随后的 `enterProjectDevelopment` 用兜底名覆盖,最近项目行也要等进项目登记过才会被重检;写入前再过壳的生命周期守卫(`mounted` + 代次),关窗/卸载后只保留已落盘的改名,不写 UI 投影。账本是**展示旁路**:`enqueue` / `update` 失败只写诊断日志,绝不影响建项、命名与首轮创作;「做方案」与手动选目录建项不发起自动命名。本次未改共享契约(`packages/shared/**`)、server-rs 与任何 SpacetimeDB schema/HTTP 路由。条件改名的 `expectedProjectId` / `expectedName` 为空时按仓库同类入口口径失败关闭;`update_local_project_naming_task` 与 enqueue 共用 `project.rename` 权限位、校验记录归属,且终态只接受同状态幂等重放。**兼容性写成显式边界:兼容是单向的**——新构建读 v1 账本 OK;旧构建读到含命名记录(`kind: null`)的账本会整份解析失败(面板报读失败、同批在途素材任务被按中断收口)。前端刷新必须先订阅事件再读快照,并在面板打开时补读一次兜底(**门控**:仅当列表里确实存在在途命名行时才补读,避免给没有命名任务的项目多打一次账本 IPC)。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{commands.rs,desktop.rs,asset_generation_tasks.rs,asset_generation_tasks/runtime.rs}`、`apps/ai-game-creator-shell/src/{app/types.ts,features/app-shell/{useHomeProjectCreation.ts,WorkspaceLauncher.tsx},features/resource-canvas/{resourceCanvasAssetGenerationTaskModel.ts,ResourceCanvasAssetGenerationTasksPanelView.tsx},view/project-development/index.tsx}`、`apps/ai-game-creator-shell/tests/{homeProjectNamingAsync.test.tsx,projectNamingGenerationTaskRow.test.tsx,appSurface/home.suite.ts}`。 - 验证:`npx vitest run apps/ai-game-creator-shell/tests/homeProjectNamingAsync.test.tsx`(12 passed,含「命名请求永不返回仍进工作区」「AI 结果先于进项目落定仍不被兜底名覆盖」「手动改名不被覆盖且任务按已完成+已跳过收口」「非法/空响应 → 任务 failed 且保留兜底名」「做方案不发起命名」「切到别的工作区不被劫持」「卸载/pagehide 后不写上下文」,并断言入队→命名中→终态的账本推进序列);`projectNamingGenerationTaskRow.test.tsx`(3 passed:固定展示名/不渲染缩略图提示词定位、终态才显示结论、失败徽章与原因 + 在途计数);AGC 全量 `npm run test -- apps/ai-game-creator-shell/tests`(1964 passed / 17 skipped);`cargo test --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute asset_generation_task`(27 passed,含 `naming_task_stays_in_flight_across_ledger_reads_until_terminal`、`naming_task_update_requires_the_project_rename_permission`、`naming_task_update_rejects_a_record_from_another_project`、`naming_task_terminal_state_rejects_a_different_status_but_allows_the_same_one`、`ledger_with_an_unsupported_schema_version_fails_closed`、`ledger_without_a_schema_version_is_accepted_as_v1`)与 `conditional_project_rename_tests`(7 passed,含超长名失败关闭);`npm run agc:typecheck`(含 `check:tests:types`)、`cargo fmt --check`、`npm run check:encoding`、`git diff --check`。 ## 2026-10-03 AGC 发布版本标签改为由工程内部版本派生,取代「用户可编辑标签」口径 - 背景:用户实机验收指出发布面板「项目版本」显示 v6,而 AGC 工程内部只有 4 条正式版本记录(资源总览「项目版本」栏目 4 张卡,顶栏「智能体修订」下拉同样只有这 4 条)。核实:面板值来自本地清单 `manifest.projectVersion` 这个可编辑标量,它被三条链路反复钉到**平台** `game_distribution_version.version_number` 上——发布成功回写(`apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:1531-1535`)、打开面板回读绑定回填(`:536-541`)、用户手改(`:944-965`);而 `manifest.versions` 从头到尾不参与该值。`publicationRevision` 只做 CAS,与任何版本号都无推导关系(`module-game-distribution/src/domain.rs:27-40` 的版本号解析只比 `max_existing` 与 `requested`)。 - 决策:AGC 发布面板的「项目版本」改为**由 AGC 工程内部版本记录 `versions[]` 派生**(标签 = 当前存活内部版本条数,即最新版本卡的「版本 N」,空数组取 1),**只读**,且不得被平台 `version_number` 回填或覆盖、不得读取 `publicationRevision`。平台侧 `versionNumber` 语义不变(正整数、允许重复与回退;`None` 仍自动 `max+1` 以兼容网页端与历史客户端)。本地字段 `projectVersion` 降级为遗留兼容位:保留可解析、不再读写(Rust DTO 带 `deny_unknown_fields`,删字段会让存量清单解析失败)。 - 取代(逐条): 1. `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md:97,99`(原「项目清单保存唯一用户发行版本 `projectVersion`」「AGC 发布面板直接编辑项目清单的 `projectVersion`」)→ 已就地改写为「派生只读 + 遗留兼容位」。 2. 同文件原 `:105`(「允许用户修改项目版本标签、重复提交同版本和回退到旧版本标签」)→ 已就地改写为「用户不再编辑版本标签;回退与重复仍由平台侧接受」。 3. `docs/project-memory/plans/【实施计划】AGC已发布游戏版本更新-2026-10-02.md:11,23`(「本地唯一 `projectVersion`…不混入内部编辑迭代 `versions[]`」「发布面板编辑 `projectVersion`」)→ 行内追加取代标注。 4. `docs/project-memory/plans/【里程碑】AGC已发布游戏版本更新-2026-10-02.md:27-30` 的验收项「项目清单只有一个用户发行版本字段,发布面板编辑它」被取代;「项目版本可以低于线上最新版本」在平台侧继续成立,AGC 侧口径改为「派生标签可能因截尾删除而变小」。 - 边界:不改 SpacetimeDB 表/字段/procedure,不改 `publicationRevision` CAS,不改公开地址与审核状态机,不重写平台历史版本行,不做版本历史/回滚 UI。AGC 工程内部版本 `versions` 只能追加或按显式放行删除一个后缀(删素材连带删版本),因此派生标签不保证单调;平台必须继续接受回退标签。 - 影响范围:`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`apps/ai-game-creator-shell/src-tauri/src/{game_distribution_publish.rs,desktop.rs}`、`apps/ai-game-creator-shell/src/{services/gameDistributionPublish.ts,components/game-distribution/GameDistributionPublishPanel.tsx,App.tsx}`、`apps/ai-game-creator-shell/scripts/check-config.mjs`、`apps/ai-game-creator-shell/tests/**`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/project-memory/plans/`。 - 验证方式:`npx vitest run …gameDistributionPublish*.test.*`、`npm run agc:typecheck`(含 `scripts/check-config.mjs`)、`cargo test -p shared-contracts`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml game_distribution_publish`、`npm run check:doc-index`、`npm run check:encoding`、`git diff --check`;真机:内部版本 4 条、线上最近提交 v6 的项目打开面板显示 v4 且只读,提交 `versionNumber: 4`。 - 关联文档:[平台入口与玩法链路](../../【玩法创作】平台入口与玩法链路-2026-05-15.md)、[里程碑](【里程碑】AGC发布版本以工程内部版本为准-2026-10-03.md)。 ## 2026-10-04 游戏游玩次数修订:关停不强制 flush、flush 失败丢弃剩余分片、客户端 IP 只信 X-Real-IP - 变更:ADR `docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md` 修订——原「正常 SIGTERM/滚动重启必须在 `finalize_shutdown` 内 force flush」作废;崩溃、被杀、正常关停都允许丢最后一个未落库窗口,`api-server` 不再注册关停 flush。 - 新增:一次 flush 按 500 分片,任一分片失败即终止本次 flush,剩余分片直接丢弃(`Build` 只把当前分片放回下一轮),避免连接不通时每个分片各等一次连接超时把 worker 卡住。 - 理由:关停丢一个窗口概率极低,强制 flush 要为在途网络写入等待、并把 worker 生命周期接进关停顺序;按 perf 与简单优先取舍。 - 受影响实现:`game_play_counter_worker.rs`(删 `flush_game_play_counter_for_shutdown`、失败即 break)、`main.rs`(`finalize_shutdown` 去掉计数 flush)、`game_play_counter.rs`(`take_pending` 仅测试使用)、`modules/game_distribution.rs`(上报先做内存限流预检再查公开可见性)。 - 安全修正:`request_context::client_ip_from_headers` 改为优先 nginx 覆盖写入的 `X-Real-IP`,`X-Forwarded-For` 只作回退且取最后一段(nginx 用 `$proxy_add_x_forwarded_for` 追加的真实对端),不再信任可伪造的首段;公开上报端点的匿名身份/限流键与微信支付下单的 `payer_client_ip` 同时受益。无 CDN 前置时 `X-Real-IP` 即真实客户端。 ## 2026-10-03 游戏游玩次数:api-server 内存去重缓冲 + 批量 procedure 落 play_count - 背景:`game_distribution_game.play_count` 早已存在且随公开投影展示,但没有任何写入口;浏览列表、详情或发行网关加载都不能算「游玩」。需要一个不拖慢进入游戏、崩溃时最多少计一个窗口的上报链路。完整决策与备选方案见 ADR `docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md`。 - 触发与落点:游玩页点击「开始游戏」时网页 fire-and-forget 上报 `POST /api/game-distribution/games/{gameId}/plays`;不建新表,累加既有 `play_count`。 - 缓冲与写入:`api-server` 纯内存聚合,`GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS`(默认 5s)到点批量调用新 procedure `increment_game_distribution_game_play_counts_and_return`(输入 `Vec<{gameId, delta}>`);事务内只对 `published` 且有有效 `active_version_id` 的记录 `saturating_add`,且不更新 `updated_at`(避免重排作者列表)。读路径不叠加内存值,展示最多滞后一个 flush 间隔。~~正常关停强制 flush~~(2026-10-04 修订:关停不再强制 flush,见上条)。 - 身份与限流:登录用 `userId`、匿名用网页 `localStorage` 的 `clientId`(不可用时退化为会话内存值)、都拿不到回退 `IP + UA`;`identity + gameId` 30 分钟去重,`IP + gameId` 每分钟 60 次固定窗口限流。非公开/下架/封禁返回 404 且不计数;无效 Bearer 按匿名处理,绝不让计数阻断游玩。 - 失败语义:只把 `SpacetimeClientError::Build`(未发出)放回重试;`Timeout` / `ConnectDropped` / `Procedure` 直接丢弃并记录丢失量——少计优于双计,本指标不做双计补偿,也不共享跨实例去重窗口。一次 flush 按 500 分片,任一分片失败即终止本次 flush,剩余分片直接丢弃(2026-10-04 补充)。 - 影响范围:`spacetime-module/game_distribution.rs`(输入类型 + procedure + tx)、`spacetime-client` facade 与生成绑定、`api-server` 新增 `game_play_counter.rs` / `game_play_counter_worker.rs` 及 config/state/main/handler、前端 `gamePlayClientId.ts` / `gameDistributionClient.ts` / `GamePlayPage.tsx`、`.eslintrc.cjs` 白名单。 - 权威文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的 `game_distribution_game` 节,以及 `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` 的「游玩计数(已实现)」节。 - 验证:`cargo check -p api-server` 与 `cargo test -p api-server game_play_counter`(9 passed)通过;前端定向 vitest(点击上报断言 + clientId 稳定性)与 `eslint --max-warnings 0` 通过;`npm run check:server-rs-ddd`、`npm run check:generated-bindings`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过。 ## 2026-10-04 AGC 工作台顶栏一行到底:三档降级 + 播放并入运行页签 + 运行画面刷新 - 背景:用户现场截图指出四个问题(改动前的形态与口径见 `docs/technical/assets/agc-toolbar-layout-after-20261004/README.md`):① 顶栏放不下时两侧容器各自 `flex-wrap: wrap`,第二行只剩「播放」与版本入口,两行控件分裂;② 版本入口不贴右缘,被前面按钮的文本宽度顶开;③「播放」与「运行」两个入口说的是同一件事;④ 运行画面里的游戏不是 vite dev 的实时刷新,改完代码只能切到资源管理再切回来才能重载页面(飞书讨论里提的「单独的刷新」)。 - 决策(顶栏排版):工具条改**一行到底**(`flex-wrap: nowrap` + `overflow: hidden`),空间不足不再换行,而是按固定顺序降级、档位写在工具条的 `data-layout` 上:`full` → `compact-version`(版本入口只留 `版本 N`)→ `collapsed-actions`(`打开项目目录 / 资源面板 / 整理画布` 收进「更多」下拉)。顺序与 1px 判定余量是纯函数(`workbenchToolbarModel.ts`,单独用例钉顺序);档位由 `useWorkbenchToolbarLayout` 实测写入——`ResizeObserver` 管可用宽度、`MutationObserver` 管**内容**变化(切运行页、出现「恢复草稿」都不改顶栏宽度,只看尺寸会停在旧档位上),每次量宽都把候选档位真的写到 DOM 再读 `scrollWidth`。档位不参与 React 状态:写的是工具条自己的属性,React 不声明就不会覆盖;也没用容器查询(阈值随模式与按钮出现与否变化,写死必然抖)。 - 决策(两个「吞掉溢出」的坑,都是实测踩出来的):`overflow: hidden` 的 flex 子项能缩到 0 或靠省略号吸收溢出,档位判定就永远量不到真实溢出——所以版本入口默认 `min-width: max-content`(只在 `collapsed-actions` 档放开为 `0`,那一档已退无可退,省略号才是兜底)、「依赖 / 类型」分段补 `flex: 0 0 auto`(此前窄宽度下被压成 0 宽)。`max-width: 1000px` / `max-width: 760px` 两处把工具条改成 `flex-direction: column` 的媒体查询删除(那是「第二行」的另一个来源,降级已由档位负责),`≤1000px` 里给动作区的 `justify-content: flex-end` 一并删除(溢出会甩到左边,`scrollWidth` 看不见)。最后一档确实放不下时(视口远小于 1280 合同宽度)才改右对齐:宁可裁左边,也不把钉在最右的版本入口裁没。 - 决策(版本入口与两端分组):工具条这一行**两端留给体量最大的两枚分组控件**——左端「资源管理 / 运行」、右端「依赖 / 类型」(用户口径:「最大的这两个放两边」),中间依次是动作按钮与版本入口;版本入口 `margin-left: auto` 推到动作区右侧,且紧邻排序分段左侧(用户口径:「版本应该在依赖 / 类型左边」),不再是最右那一枚。显示 `版本 N(原因 · 时间)`,其中 `版本 N` 复用资源画布版本卡的编号口径(`manifest.versions` 落盘顺序 + 1,`formatIterationVersionTitle`),括号里那截是独立一层(`formatIterationVersionDetail`)——窄档位收掉的是这一层而不是整枚入口;可访问名、菜单项与排障文案一律保留完整标识。DOM 顺序由 `tests/resourceVersionSwitch.test.tsx`(版本入口在排序分段之前)与 `tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx`(排序分段是动作行最后一个子元素)双向钉住。 - 决策(播放并入运行 + 刷新入口):删掉独立的「播放」按钮,「运行」页签前加 ▶ 图标,点页签=`showRunView()` + `onPlay?.()`(与旧播放按钮逐字等价,含「再点一次=重跑」);不可运行时页签不置灰、点了既不切视图也不发播放请求,只出既有提示。运行画面右下角新增「刷新运行画面」(全屏那一枚左侧):`onPlay` 命中活体预览只切视图、不重启服务,真正重载页面靠换 `iframe` 的元素身份(`LocalGamePreviewFrame` 新增 `reloadNonce`)——运行页在另一个端口上,跨域 iframe 里 `contentWindow.location.reload()` 会被浏览器挡掉。 - 决策(进入项目自动载入):站在运行视图上却没有画面可看时自动补发一次 `onPlay`——典型现场是从别的项目切过来(工作台不重挂,`mode` 是工作台自己的 state,上一条项目的运行视图原样留下而画面已经没了),用户只会看到「客户端运行画面尚未载入」,像坏了一样。只在**没有画面且可运行**时发;`showRunView` 先记账再自己发播放(`autoRunPreviewProjectRef`)所以点页签不会被重复触发;同一个项目只自动补一次,失败不打转,手动重跑仍走页签或画面上的刷新按钮。 - 边界:不改后端、契约与 SpacetimeDB;`runAvailable` / `showRunView` 的门槛语义不变,自动切运行的两条路径(会话内已确认的预览、播放请求)不走 `showRunView`,不会多发播放请求。窄于合同宽度只保证不崩,不做移动端布局。 - 影响范围:`apps/ai-game-creator-shell/src/{styles.css,view/project-development/{index.tsx,workbenchToolbarModel.ts,useWorkbenchToolbarLayout.ts,WorkbenchMoreActionsMenu.tsx},features/resource-canvas/{GameRunVersionPicker.tsx,resourceCanvasVersionBindingModel.ts},features/project-workspace/LocalGamePreviewFrame.tsx}`;用例 `tests/{workbenchToolbarLayout,runPreviewRefresh,runAutoLoadOnEnter}.test.ts(x)`(新增)、`tests/appSurface/project-development.suite.ts`、`tests/{gameRunToolbarActionsStyle,resourceCanvasVersionBindingModel}.test.ts`;文档 `docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S17 改写 + 新增 S17a)、`docs/README.md`、新目录 `docs/technical/assets/agc-toolbar-layout-after-20261004/`。 - 验证:`npx vitest run apps/ai-game-creator-shell/tests`(200 passed / 1 skipped 文件,1929 passed / 17 skipped 用例,末次全量);定向 8 个文件 253 passed;`npm run typecheck`(在 `apps/ai-game-creator-shell`,含 `check:tests:types`——只跑 `tsc -p tsconfig.json` 覆盖不到 `tests/`)、eslint `--max-warnings 0`、`prettier --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 全绿。真机几何用一次性 Vite 夹具在真实 Chromium 里逐档实测(视口 1412 / 1240 / 1100 / 1024 / 960 / 860 / 800 / 760 / 700 / 640 / 560 / 480 / 400):始终单行、排序分段贴右缘(`工具栏右缘 - padding - 排序分段右缘 = 0`)、版本入口始终在排序分段左侧、1240/1100 走 `compact-version`、1024/800/700/640/560/480 走 `collapsed-actions`,「更多」下拉三条动作可点且点「资源面板」真的开面板、点「刷新运行画面」`iframe` 换新节点而 `src` 不变;截图见 `docs/technical/assets/agc-toolbar-layout-after-20261004/`。 ## 2026-10-03 AGC 画布引用统一走「活跃聊天输入区」注册表(Issue 602) - 背景:画布的「引用」按钮与「拖拽批量引用」只派发 window 事件,消费者只有 `App.tsx` 一处,而它插的是绑在 `PlanningChatView` 上的 `chatComposerRef`;2026-09-22 DirectProject 拆分后普通项目走 `directProjectMode` 提前 return,渲染不到策划面 → ref 恒为 `null`,可选链静默吞掉点击(画布上是死按钮)。同一批合并冲突还丢了 `RESOURCE_REFERENCE_INSERT_MANY_EVENT` 的监听,批量引用连消费者都没有。 - 决策:新增 `features/project-workspace/activeChatComposer.ts`,模块级只保存**当前挂载的那一个**输入区句柄(`registerActiveChatComposer` 返回带身份校验的注销函数,并检测到第二个输入区注册时留一条 dev 告警——不改运行时语义;`insertChatReferences` 在空批次 / 无输入区 / 句柄报「这一批没插进去」三种情况返回 `false`)。`DirectProjectComposer` 用 `useImperativeHandle` 暴露 `DirectProjectComposerHandle`(按 ref 转发、由它回答插入是否真的递到输入区),`DirectProjectChatView` 与 `PlanningChatView` 挂载期间各自注册**按 ref 转发**的句柄(注册时不读输入区是否就位,因此不依赖父子 effect 顺序)(两条链路互斥渲染,同一时刻只有一个句柄)。`App.tsx` 收敛为一处监听,单条 + 批量两个事件都走 `insertChatReferences`(空批次直接返回:没有要插的东西,不能报成「没有可用的输入区」);返回 `false` 时 dev 下 `console.warn`。`chatComposerRef` 只保留给策划输入盒自己的 `getDraft` / `clear`。 - 边界:不采用「给 DirectProjectComposer 单独加 ref 出口 + App 按模式分流」的备选(那会把「哪个 ref 此刻是活的」继续留在检测点上)。插入仍经 `ResourceReferenceInput.insertReferences` + `focus()`(光标落在插入之后,连点两次按顺序追加)。真正根治的形态是画布与聊天的共同宿主用 context 下发插入能力;注册表语义与之一致,将来换实现不必动画布。 - 影响范围:`apps/ai-game-creator-shell/src/features/project-workspace/activeChatComposer.ts`(新增)、`src/App.tsx`、`src/view/project-development/chat/DirectProjectChatView.tsx`、`.../chat/components/DirectProjectComposer/DirectProjectComposer.tsx`、`.../planning/PlanningChatView.tsx`、`tests/activeChatComposer.test.ts`(新增,钉注册表合同)、`tests/resourceCanvasChatReferenceDrop.test.tsx`、`tests/appSurface/{project-development,design-agent}.suite.ts`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`、`pitfalls.md`、本文件。 - 验证(合并 master 后的最终一轮):`npx vitest run apps/ai-game-creator-shell/tests`(197 passed / 1 skipped 文件,1918 passed / 17 skipped 用例)、`npx vitest run tests/activeChatComposer.test.ts tests/resourceCanvasChatReferenceDrop.test.tsx`(2 files / 12 passed,含注册表合同:空批次、无输入区、句柄报落空、注销身份校验、重复注册告警、乱序注销)、`npx vitest run tests/appSurface.test.ts -t 引用`(4 passed)、`npm run agc:typecheck`(含 `check:tests:types`,exit 0)、`npm run check:encoding`、`git diff --check`、eslint `--max-warnings 0`(改动文件)。反向证伪:去掉注册调用后端到端用例变红;去掉空批次短路 / 重复注册告警后对应新用例各红一处。 ## 2026-10-04 AGC 渠道更新进程与快捷方式隔离 - 背景:Tauri 2.11 的 Windows NSIS 模板通过 `MAINBINARYNAME` 查找并结束进程。dev 与 release 过去共用 `genarrative-ai-game-creator-shell.exe`,更新任一渠道都会结束另一渠道;release 从「陶泥儿 Release」改为「陶泥儿」后,`/UPDATE` 又不会自动重建旧快捷方式。 - 决策:dev 保留历史主程序文件名以维持升级链;release 使用 `genarrative-ai-game-creator-shell-release.exe`,其它非默认渠道使用带渠道后缀的主程序名。构建期把 `mainBinaryName` 与渠道端点、productName、identifier 同批注入,NSIS 因文件名隔离而只匹配自身渠道进程。 - 迁移:release Windows 包通过独立安装钩子读取旧 `陶泥儿 Release` 卸载项的安装目录,在原目录安装新包,迁移旧桌面/开始菜单快捷方式并清理旧主程序与孤儿卸载项;dev 继续使用原有旧展示名迁移钩子。 - 影响范围:AGC 渠道身份脚本、发布构建配置、Tauri 基线配置、Windows NSIS 钩子与渠道发布测试;不改变 OSS 分区、更新端点或客户端数据目录合同。 - 验证方式:渠道发布脚本定向测试、`check-config.mjs`、真实 NSIS 编译与双渠道安装/更新 smoke;真机安装仍需发布环境执行。 ## 2026-10-03 AGC 栏目画布上传素材按入口栏目登记(Issue 359) - 背景:AGC 客户端在资源栏目子画布(「UI 交互 / 角色与对象 / 场景与环境 / 音频」)左下角工具栏点「上传」后,提示条给出「已上传 1 个素材」,但当前栏目计数不变(仍「0 项」)、素材出现在「待归类」,用户看到的是"上传成功了但它从这一页消失了"。原因是上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套,而 `upload_local_asset` 原先不接受入口栏目。 - 决策:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(与生成入口 `start_local_project_asset_generation` 的 `targetCategory` **同一口径**:GUI 完成登记以入口栏目为准);前端 `uploadProjectAssetFilesAndReadSnapshot` 透传该字段,栏目画布工具栏上传取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。取值只接受共享分类枚举,非法值由原生失败关闭,前端不做伪分类。 - 边界:不传 `targetCategory` 时保持既有 kind 派生行为——资源面板(`ResourceCanvasPanelView`,跨栏目列表而不是栏目工具)、UI 编辑器图片导入(`import_ui_editor_local_files`)、聊天附件上传都不变。`upload_local_asset_at` 签名保持不变(委托到新的 `upload_local_asset_at_with_category`),既有约 25 处调用点与 Rust 单测零改动。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{assets.rs,commands/desktop.rs}`、`apps/ai-game-creator-shell/src/view/project-development/{projectResourceLiveUpdateModel.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、PRD §3.10、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`(§4 载荷表)、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S11a)、`pitfalls.md`。 - 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell assets::tests`(新增 `upload_registers_into_the_explicit_entry_category`:显式栏目 → `character`、不传 → `unclassified`、非法 `version` 失败关闭且不新增登记);`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`(196 passed / 9 skipped,含新增「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`);`npm run agc:typecheck`、`npm run check:encoding`、`git diff --check`。 ## 2026-10-03 生成绑定不再经 prettier:ts-rs 原始输出即提交形态 - 背景:`scripts/check-generated-bindings.mjs` 对 AGC 的 `chat/generated` / `services/generated` 在重生成后会就地跑一遍 `npx prettier --write` 再比较。这会直接改写生成文件,还把「Rust 声明真的变了」与「prettier 版本 / 配置造成的格式漂移」混在同一条告警里——本次报出的 `ThreadRequestKind.ts` / `TurnCompletedStatus.ts`「内容变化」无法复现为语义变化(已提交内容与当前 Rust 枚举一致),prettier 归一化把格式差异也报成了「与 Rust 声明不一致」;生成物被仓库格式化工具二次改写后,重跑 `cargo test export_bindings` 也不再幂等。 - 决策:生成绑定一律以 ts-rs 原始输出提交,不接受 prettier / eslint 等工具二次改写。`check-generated-bindings.mjs` 删掉 prettier 步骤,直接逐字节比较原始输出;`packages/shared/src/contracts/generated`、`chat/generated`、`services/generated`、`features/ui-editor/types` 四个目录统一登记进 `.prettierignore` 与 `.eslintrc.cjs` 的 `ignorePatterns`;`.prettierrc.json` 里为 `contracts/generated` 设的 `printWidth: 1000` / `singleQuote: false` 覆盖随之删除。 - 边界:ts-rs 原始输出在多行对象 / 枚举变体行尾带空格,`.gitattributes` 对四个生成目录设 `whitespace=-trailing-space`,CI 的 `git diff --check` 不再误报;手写文件的行尾空白检查不变。生成 `.ts` 仍提交进仓库供前端消费,并标 `linguist-generated=true`。 - 影响范围:`scripts/check-generated-bindings.mjs`、`.prettierignore`、`.prettierrc.json`、`.eslintrc.cjs`、`.gitattributes`、`apps/ai-game-creator-shell/src/{view/project-development/chat,services}/generated/**`。 - 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`、`npm run check:generated-bindings`(shared-contracts 1 + AGC 104 = 105 个文件)、`npx prettier --check` 四个生成目录(全部跳过)、`npm run check:encoding`、`git diff --check`。 ## 2026-10-03 回合错误的处置分类层与线程条目脱敏边界 - 背景:`TurnError::terminal_failure -> Option` 用一个 `None` 同时表达"没有失败"和"不是失败、要继续跑(返修 / 复核控制流)",调用方看到 `None` 只会理解成前者,`dispatch.rs` 两处只能 `.expect("回合失败必可投影成失败载荷")`;同时 ThreadManager 在搬运线程条目时做字段级脱敏与限长,前端要被截断,而且与失败载荷的脱敏是两套实现。 - 决策(分类层):`TurnError::classify(&self, root) -> TurnErrorClassified{ShouldStop(TurnFailure), ShouldContinue { detail: String }}` 取代 `terminal_failure`——真失败继续投影成 `TurnFailure`(脱敏 + 截断仍在这一处),控制流带自己的说明走 `ShouldContinue`;`dispatch.rs` 两处改成按分类 match,`ShouldContinue` 不写终态、不伪造失败。"这一轮怎么收场"仍只由 `TurnCompletion` 定义,private `SessionOutcome` 并入 `session_completion`。 - 决策(脱敏边界):ThreadManager **不做任何字段级脱敏与限长**——`wire/items.rs` 的 `bounded` / `detail_text` / `sanitize_detail_text` / `relativize_project_root_paths` / `thread_delta_text` 与三个字符上限常量删除,条目与流式增量原样透传;脱敏移到前端 `chat/conversation/directThreadSanitize`,在 bootstrap / consume / 历史切片进入聊天状态之前统一做(前端手里有 `projectPath`,能把项目内绝对路径归一成相对路径)。失败载荷(错误文案)的脱敏不属于 ThreadManager,保留在 `TurnError::classify` 投影时。订阅侧的 8 MiB / 8192 事件缓冲上限不动:那是背压,不是字段限长。 - 边界:错误尚未持久化,`TurnErrorClassified` 是纯宿主内部类型、不加 `Serialize` / `TS`;线上 `TurnFailure` 与 `ThreadEvent` 形状、占位符词表(`` / `[redacted-secret]` / `[redacted-sensitive-field]` / `[redacted-config]` / `[redacted sensitive context]`)都不变。`TurnFailure` 名字加了 `// TODO badnaming` 待后续改。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{turn_error.rs,mod.rs},thread_manager/{dispatch.rs,turn_completion.rs,wire/{items.rs,failure.rs,tests.rs}},generation/prompt_context.rs}`、`apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs`、前端 `chat/{controller/useDirectThreadChatSubscription.ts,conversation/directThreadSanitize.ts}`、`chat/generated/{ThreadItem,TurnFailure}.ts`、`tests/directThreadSanitize.test.ts`。 - 验证:`cargo test -- agent:: --skip export_bindings`(949 passed;删掉一条已无对象的流式脱敏用例)、`npm run check:generated-bindings`、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests/directThreadSanitize.test.ts`(12 passed)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`;全量 vitest 的 `gameDistributionPublish*` / `recentProjectsHook` localStorage 失败为既有问题。 ## 2026-10-02 DirectProject 失败载荷两臂改名与 typed 失败原样下发 - 背景:`turn.completed.failure` 的两个载荷臂把字符串债藏在像正常类型的名字后面(`TurnFailed { stage, detail: String }`、`TurnFailedUnclassified { detail: String }`);同时 `direct_runtime` 与 `thread_manager::dispatch` 各有一处收口把**任意** `TurnError` 重包成 `TurnFailed { stage: turn_failure_stage(), detail: 预拼收口文案 }`,typed 变体(模型调用 / 超时 / 通道断开)在下发前就被吃掉,前端只能看到 catch-all 与一个伪造的 `code-generation` 阶段。 - 决策(命名):两个载荷臂按"这是待清债"的既有约定起丑名字——`TurnFailed → SuperErrorFromStringPlusStage`(只有 `stage` 是 typed、`detail` 仍是产生层字符串)、`TurnFailedUnclassified → Unclassified`(连阶段都没有);线上 `type` 同步为 `superErrorFromStringPlusStage` / `unclassified`,生成文件 `TurnFailed.ts` / `TurnFailedUnclassified.ts` 删除。`stage` 留在变体内部,**不做成 wrapper 层的正交字段**:其余 8 个变体根本没有阶段,包到外层只会给它们编假值。 - 决策(透传):删掉上述两处收口重包,`Err(failure)` 原样透传;`.agent/runtime/errors`、应用日志与错误上报池的审计写盘保留,只是返回值不再当失败载荷的 `detail`。前端因此第一次能按真实变体(`modelCallFailed` / `timedOut` / `transportClosed`…)选文案。 - 边界:不改 `SuperErrorEnumFromStringTyped`(仍在 `is_retryable` / `is_model_repairable` 上做内部分类,detail 的 typed 化留给上游改造);不迁就存量字符串分流、不加别名;错误尚未持久化,线上 `type` 值直接改、不做迁移。 - 影响范围:`agent/codex_app_server/turn_error.rs`、`agent/direct_runtime/{mod.rs,user_input.rs}`、`agent/thread_manager/{dispatch.rs,turn_completion.rs,wire/failure.rs}`、前端 `chat/{conversation/directTurnFailure.ts,generated/**}`、`src/features/agent-runtime/model.ts`(注释)、`tests/directThreadChat.test.ts`。 - 验证:`cargo check`(bin)、`cargo test -- agent:: --skip export_bindings`(948 passed)、`cargo test export_bindings` 后 `npx prettier --write chat/generated/*.ts`、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests`(`NODE_OPTIONS=--localstorage-file=…`,196 files / 1930 passed)、`npm run check:encoding`、`git diff --check`。 ## 2026-10-02 DirectProject 回合错误命名化与 wire 模块拆分 - 背景:DirectProject 三条错误通道的类型还带着 `Direct` / `DirectCodex` 前缀(`DirectTurnError` / `DirectTurnEnqueueError` / `DirectTurnFailure`),且 `thread_manager/wire.rs`(1600+ 行)把条目投影、事件、失败载荷与终态判定混在一个文件里;`direct_turn_error.rs` 也留在 `agent/` 顶层而不是它服务的 `codex_app_server` 深模块旁边。 - 决策(命名):既然类型已经归到具名深模块,去掉冗余前缀——`DirectTurnError → TurnError`、`DirectTurnEnqueueError → EnqueueError`、`DirectTurnFailure → TurnFailure`、`DirectTurnDeadline → Deadline`、`DirectCodexFailureStage → FailureStage`、`DirectCodexNativeKind → NativeKind`、`DirectModelCallKind → ModelCallKind`;`direct_codex_user_item` 里的 `DirectCodexUserItem/UserMessageItem/UserRole/UserContentPart/UserAttachmentReferencePart/UserRuntimeRegionPart → UserItem/UserMessageItem/UserRole/UserContentPart/UserAttachmentReferencePart/UserRuntimeRegionPart`;`codex_app_server` 内部的 `DirectCodexTurnKind/DirectCodexTurnObservation/DirectTurnRunFailure/DirectTurnReport/DirectTurnTerminalContext/DirectTurnCancelView → TurnKind/TurnObservation/RunFailure/TurnReport/TerminalContext/TurnCancelView`;`direct_now_ms → now_ms`。载荷 struct 名仍是"变体裸名",`TurnFailure` 的投影点仍是 `TurnError::classify` 一处。 - 决策(模块归位):`agent/thread_manager/wire.rs` 提升为目录并按职责拆分——`wire/{mod,clock,items,turn,failure,tests}.rs`,`wire/` 只留**线上形状**(`TurnFailure` 载荷与 `ThreadEvent`);`agent/direct_turn_failure.rs` 的终态判定并入 `agent/thread_manager/turn_completion.rs`;`agent/direct_turn_error.rs`(三条错误表与分类)移到 `agent/codex_app_server/turn_error.rs`。`thread_manager::wire::*` 仍从 `mod.rs` 平铺 re-export,外部路径不变。 - 决策(终态类型):宿主侧的回合终态从 `struct TurnTerminal { status, failure }` 改为判别联合 `enum TurnCompletion { Completed, Interrupted, Aborted, Failed(TurnFailure) }`——`status` 由变体反推(投影回 `turn.completed` 时才变回字符串),失败必须带载荷、正常收场不许带;「有载荷就一定是失败」这条反推关系不变。`TurnCompletion` **不是线上形状**:不加 `Serialize` / `TS`、不导出、不下发,放在 `thread_manager/turn_completion.rs` 而不是 `wire/`;收尾阶段推出来的 `status` 字符串在这里一次性收进类型,认不出的值 fail closed(按未分类失败),不冒充正常收场。`thread_manager/mod.rs` 的过期回合释放兜底也改走 `TurnCompletion::Aborted.event(...)`。 - 边界:只改类型 / 函数 / 生成文件命名与模块位置,不改线上 JSON 形状(`TurnFailure` 的 `type` 判别值与字段名不变,`TurnCompletion` 只影响宿主内部);不迁就存量字符串分流,不保留别名或 `Display` 回落。`DirectTurnFailureKind`、`wire_kind()`、`Display for TurnError` 与 `From for String` 已在上一轮删除。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{mod.rs,execution.rs,turn_error.rs},thread_manager/{mod.rs,dispatch.rs,wire/},direct_runtime/{mod.rs,user_input.rs},direct_codex_user_item/*,runtime_driver/entrypoints.rs,...}`、前端 `chat/{controller,conversation}/**` 与 `chat/generated/**`(旧绑定文件删除、新绑定文件随 `cargo test export_bindings` 生成)、`scripts/check-generated-bindings.mjs`、`tests/**`。 - 验证:`cargo check`(bin)、`cargo test -- agent::thread_manager`(69 passed)与 `cargo test -- terminal`(73 passed)、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests`(DirectProject 相关用例全绿;`gameDistributionPublish*` / `recentProjectsHook` 的 localStorage 失败为既有问题,与本次无关)、`npm run check:generated-bindings`(104 个文件)、`npm run check:encoding`、`git diff --check`。 ## 2026-10-02 资源编辑远端失败的原始原因穿出到工具错误,资源编辑错误通道补一层 typed - 背景:轮询到 `status=failed` 时客户端只读 `status`,丢掉平台在同一个响应里给的 `error`(契约 `ExternalEditorGenerationJobResponse.error`),统一写 `terminal_failure_code = remote-generation-failed` 并返回「remote-terminal-failed: 资源编辑生成失败」。平台的可行动原因就此消失:模型与用户卡片只看到一句「失败了」,重试路径(`ensure_resource_edit_phase_resumable`)也只有分类码。这违反 `pitfalls.md`「远端资源编辑终态必须指出唯一出口」里已写下的口径——「首次失败的原始拒绝说明继续由当次错误文案承担」;提交期 HTTP 400 分支(`editor_api_rejection_reason`)兑现了,轮询分支没有。另外 `remote-terminal-failed:` 只是文案前缀(全仓没有 `starts_with` 解析它),在第一句失败文案里与「失败」重复。 - 决策(typed 承载):新增 `ResourceEditError`(`project/resource_editor/error.rs`),只两个变体:`RemoteGenerationFailed { serverMessage }` 承载平台 `error` 原文,`Other(String)` 收尚未分类的失败(`// TODO refactor string-typed`)。两个入口 `derive_local_project_resource`、`resume_local_project_resource_edit` 返回 typed;旧名 `derive_local_project_resource_at` / `resume_local_project_resource_edit_at` 保留为 `Result<_, String>` 外观(映射 `to_user_msg()`),因此 33 个既有测试调用点与两个 Tauri 命令零改动。 - 决策(不用 blanket From):不提供 `impl From`;每处 String 错误显式 `.map_err(ResourceEditError::Other)`,让「还没 typed 化」的边界处处可见,而不是被一次隐式转换吞掉。 - 决策(不按 code 分支):不按 `terminal_failure_code` 分支。它两个写入点最终落到同一个 `phase`、唯一读者只做插值不比较,值域撑不起 policy;字段上加 `// TODO clean unnecessary`。清理前置条件已核实:该字段无 `skip_serializing_if`,`.agent/resource-edits/operations/*.json` 每个文件都带这个 key,而 `ResourceEditLedger` 是 `deny_unknown_fields`、扫描循环里一个文件解析失败会让整个「待恢复资源编辑」列表报错返回。 - 决策(前缀去留):删掉第一句失败文案里的 `remote-terminal-failed:`(轮询与提交期 400 两处)。`ensure_resource_edit_phase_resumable` 里那三个 token 保留:它们与三个 phase 一一对应,是那句重试文案里区分「确定失败 / 已归档 / 待对账」的唯一手段。 - 决策(原文边界):平台原文只进当次错误文案,仍不进账本(`terminal_failure_code` 的写入边界与既有断言不变)。平台 `user_visible_external_generation_error` 已对四种 kind 做 sanitize,图片/视频两种原样透出——与同 wire 的 `canvas_generation.rs` 口径一致,要收边界应改服务端。 - 决策(命名与落点):尚未 typed 化的变体叫 `Other`,不叫 `Message`(后者分不清是「已渲染文案」还是「原始消息」);新错误单独放 `project/resource_editor/error.rs`,不再往主文件里塞类型定义。 - 决策(工具层承载):`RemoteGenerationFailed` 不能到工具层又被压回一句字符串。共用载体放 `agent/tool/error.rs` 的 `RemoteResourceEditFailure { serverMessage }`(两个工具共用的文案只写一份),`CreateOrDeriveResourceError` / `RemoveBackgroundError` 各加 `RemoteGenerationFailed(RemoteResourceEditFailure)` 变体;翻译用显式 `from_resource_edit_error`,不用 `impl From`,远端终态进 `RemoteGenerationFailed`、其余 `Other` 仍落回各工具原有的「失败:<文案>」变体。这样诊断 sidecar 的 `error` 字段(typed enum 整体序列化)天然带上平台原文,LLM 侧拿到的 `message` 也带上。 - 决策(前缀归属,2026-10-02 追加):叶子错误只给事实,不给「谁失败了」的总结前缀。`RemoteResourceEditFailure::to_user_msg` 与 `ResourceEditError::to_user_msg` 都只返回平台 `error` 原文;平台没给就回「服务器未返回错误信息」,不再说「资源编辑生成失败」这种没有信息量的总结。typed 错误新增 `phaseDetail` 字段,但它只作为结构化字段进诊断 sidecar(开发者/LLM 侧看原始值),**不参与用户文案**。前缀由使用者自己加:`agc_create_or_derive_resource` 用「生成或派生资源失败:」、`agc_remove_background` 用「抠图失败:」、桌面命令面用「资源编辑生成失败:」。同一份 typed 错误因此可以同时服务工具面(前缀各随其工具)与桌面面(保留原有文案)。 - 决策(HTTP 兜底与叶子前缀,2026-10-02 追加):叶子只给「服务端 message / code / 原始传输事实」这类事实,不把操作名写进叶子。`game_package_upload/runtime.rs` 三处 `let (_, message)` 把服务端 `code` 丢掉、再拼「读取上传状态失败(HTTP 503)」这类前缀,改成 `message` → `code` → `HTTP {status}`(操作名交给调用方的话术)。`game_distribution_publish.rs` 的 `response_data`(2xx + `ok:false`)同样用上被丢掉的 `error.code`,`account_api.rs` 的 envelope 分支补 `error.code`;服务端没给任何原因时统一回「服务器未返回错误信息」。错误类型自身的单测不再断言 `to_user_msg()` 的字面量(文案是给用户的话术,不是契约),只保留「typed 字段原样序列化进诊断」的结构断言。 - 决策(凭据作用域的 error 类型):`with_direct_editor_api_credentials` 原本把操作限定成 `Result<_, String>`,会把 typed 错误提前压掉。新增 `with_direct_editor_api_credentials_as(operation, credentials_error)` 保留调用方 error 类型,凭据解析失败由调用方显式翻译(这里传 `ResourceEditError::Other`),旧名保持 `String` 语义、零改动。 - 决策(future 装箱):`handle_direct_tool_bridge` 的状态机在调试测试线程的默认栈上已经贴着上限,资源编辑 arm 直接内联会顶穿(`bridge_write_file_waits_on_the_blocking_pool_instead_of_a_runtime_worker` 栈溢出)。桥里四个资源编辑 await 点用 `Box::pin` 只留指针进外层状态机;这是体积问题,不是错误用 `Box`。 - 决策(同类兜底,2026-10-02 追加):`agent/generation/canvas_generation.rs` 的远端 `failed` 分支原来在平台没给 `error` 时兜底成「生成任务失败」,与句首的「平台图片生成任务失败:」重复,改成「服务器未返回错误信息」;`phaseDetail` 不再参与用户文案(只作结构化字段)。 - 改动范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`project/resource_editor/error.rs`(新)、`agent/tool/error.rs`、`agent/tool/create_or_derive_resource/error.rs`、`agent/tool/remove_background/error.rs`、`agent/direct_tool_bridge.rs`、`assets.rs`、`docs/project-memory/shared-memory/pitfalls.md`。 - 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过;`cargo test --bin genarrative-ai-game-creator-shell -- project::resource_editor --test-threads=1` 66 passed(并行跑会有一批 TCP fixture 用例因争用超时,串行全绿,与本次改动无关);`-- agent::tool:: agent::direct_tool_bridge` 47 passed;`npm run check:encoding` 5111 files;`git diff --check` 干净;`cargo fmt` 已跑。 - 关联:`pitfalls.md`「远端资源编辑终态必须指出唯一出口」。 ## 2026-10-02 DirectProject 对话:更早历史自动加载、前插锚定与回到底部胶囊 - 背景:右侧对话更早历史只靠常驻按钮「显示更早的对话」拉,且没有任何加载反馈(`historyLoadingRef` 是 ref,渲染不出来);用户滚上去之后没有「回到最新」的入口;展开「执行过程」/工具组/思考块时浏览器保持 `scrollTop`,新展开的正文长在视口下方,在底部展开更是直接顶出可视区。 - 决策:删按钮改自动加载——触顶 24px 与「首帧后内容填不满视口」共用一道门(有更早历史 ∧ 不在加载中 ∧ 无失败记录),失败挂起、只留内联「加载更早对话失败 · 重试」且不自动重试;加载行挂载在列表最上方、延迟 150ms 才显示。前插按「回合 key(`data-turn-key`)+ 块内序号 + 相对列表顶边偏移」冻结锚点,列表显式 `overflow-anchor: none`;加载期间所有补偿还原同一个冻结锚点,加载结束后的下一次补偿再刷新。底部居中 `sticky` 胶囊「回到底部」(不跟随时来了新终态内容改「有新回复 · 回到底部」):距底 48px 阈值、点击平滑滚动并恢复跟随。`ResizeObserver` 观察列表直接子元素(`MutationObserver` 负责子元素变化时重订阅):跟随时任何高度变化贴底,否则冻结刚展开的折叠头,头部锚不住再对齐展开正文顶边;新一回合开始(`turnInFlight` 假转真)强制恢复跟随。滚动所有权(列表 ref、跟随最新、补偿、胶囊显隐)从 `DirectProjectChatView` 搬进 `DirectProjectConversation` 及其同目录 hook,控制器新增可渲染的 `historyLoading` / `historyError` 与 `retryEarlierHistory`。 - 边界:只做 DirectProject;`PlanningChatView` 与 `App.tsx` 里的 `message-history-more` 遗留路径不动,因此该样式保留(后续项已写进 ADR)。新增元素全部用内联 Tailwind,`styles.css` 未改;阈值、文案与判据集中在同目录 `conversationScrollPolicy.ts`,契约见 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md),旧「单一事实源」ADR 的分页条目已改为引用它。 - 验证:新增 37 条与实现同目录的用例(阈值与加载门、锚点读取/还原、折叠头冻结与正文对齐、hook 的触顶/填充/胶囊/未读/延迟加载行、组件层的按钮移除与内联重试行)由根 `vitest.config.ts` 的 include 收进门禁;`npm run ai-game-creator-shell:typecheck`、`eslint`(含新文件)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过;滚动观感(顶部加载圈、胶囊显隐、底部展开回贴、历史前插不跳)留真机手动验收。 - 追加(2026-10-02,两个真机缺陷的根因与修正):①「点回到底部只下去一屏、到不了底」——平滑动画期间滚动事件把「跟随最新」翻成假,布局补偿与锚点还原接着写 `scrollTop`,而真实浏览器里任何一次写都会取消正在跑的平滑动画;现在滚动位置由这次程序化滚动独占(滚动事件只在贴底时才结算并交还,补偿整段跳过),滚轮 / 触摸 / 键盘接手立刻交还,避免标记永远挂着。②「展开折叠块没有自动滚动到位」——旧规则只在展开正文比视口还高时才动,正文矮的折叠块(工具组、思考块)展开后正文仍在视口外;现在冻结折叠头之后统一补「刚好露出新展开正文」的最小位移(底边超出就补超出量,比视口还高则对齐正文顶边),两段位移合成一次写。落在 `conversationToggleReveal.ts`(新增纯函数 `toggleRevealDelta`)与 `useConversationScroll.ts`(新增 `programmaticScrollRef`)。 - 验证(2026-10-02 追加):同目录用例补齐到 48 条(新增 `toggleRevealDelta` 六例、hook 的四例程序化滚动用例,其中两例在修正前确实红)全部通过;Chromium 真机脚本复验三处——点回到底部收敛到 `scrollHeight - clientHeight`、底部展开贴到新底、视口外展开补 269px 后正文底边正好贴视口下缘。 - 追加(2026-10-02,换会话复位滚动所有权):`useConversationScroll` 的跟随最新 / `atBottom` / `hasNewReply` / 前插锚点 / 折叠头 / 程序化滚动标记只在挂载时初始化一次,而 `DirectProjectChatView` 切项目时不重挂载——在项目 A 往上滚过再切 B,B 首屏不贴底且胶囊直接显示「有新回复 · 回到底部」。现在把会话身份(`conversationKey`,DirectProject 传项目路径)作为显式信号传进 hook,身份变化即复位这批状态、同步终态指纹并重新贴底;不改成 `key` 重建列表,避免消息列表重建与加载行 150ms 延迟计时重来,同一个项目重开也不算换会话。 - 验证(2026-10-02 追加,换会话复位):`useConversationScroll` 新增两例换会话用例(新会话首屏贴底且不出现胶囊、切会话后在 B 里往上滚只显示「回到底部」而不是「有新回复」),修正前确实红、修正后绿;chat 范围用例全绿。 - 追加(2026-10-02,review 两处内部缺陷的修正):①更早历史读取的守卫从「项目路径相等」改成「世代号相等」(`historyLoadTokenRef`,切项目与每次读取各推进一格)——A→B→A 之后在飞的旧读取又落回同一路径,原守卫会放行,把新一代的加载态、并发闸门与游标一起改掉;现在非最新一次读取落地时整段丢弃。②锚点的块集合按列表缓存(`WeakMap`),由已有的子元素 `MutationObserver` 在同一处 `invalidateTurnBlocks` 失效——原来一个滚动帧里读锚点、还原锚点各查一遍整份列表,长会话下是 O(块数) 的 DOM 查询。 - 验证(2026-10-02 追加,review 修正):新增一例控制器用例(驱动 A→B→A 且同路径新读取在飞,修正前在「旧读取落地」处红)与两例锚点缓存用例(连续收集只查一次 DOM、子元素变化并失效后重新收集);chat 范围用例全绿,`npm run ai-game-creator-shell:typecheck`、`eslint --max-warnings 0`、`npm run check:encoding`、`git diff --check` 通过。 - 追加(2026-10-02,锚点改用稳定块身份):review 第 3 条(`findTurnBlock` 按块序号定位)成立,采纳「换一套块身份」。锚点从 `{回合 key, 块序号, 偏移}` 改为 `{回合 key, 块身份, 偏移}`,块身份即 `DirectChatBlock.key`(`${回合 key}:${条目 itemId}` / 本地说明 `messageId`),只要求同一回合内唯一;展示层给每个可锚定块加 `data-block-key`(`DirectProjectTurn` 的正文块与终态文案、折进 `
` 的过程包装块、`ToolCallGroup`、`AgentReasoning` 各自透传),`conversationScrollAnchor.ts` 的收集选择器因此收敛为 `[data-turn-key][data-block-key]`。原因:`renderTurnProcess` 对运行中的回合平铺过程块、对已结束的回合折进一个 `
`,收口时整个回合的块序号后移一格,序号锚点会解析到隔壁块并按错误基准写 `scrollTop`。同时补上 review 指出、原方案漏掉的一半:锚点块没有布局盒(被折进收起的 `
`)时读锚点跳过它、`restoreTurnAnchor` 判为失败,调用方放弃这次补偿并重新起锚——不回跳,也不按别的块硬对齐。 - 验证(2026-10-02 追加,稳定块身份):锚点纯函数用例改为按块身份构造(`buildList` 传 `[回合 key, 块身份]`),新增「回合收口后块序号整体后移,块身份仍指向同一块且还原成功」「收起的 `
` 里的块没有布局盒:读锚点跳过、还原返回 false 且不写 `scrollTop`」;组件层新增用例断言每个带 `data-turn-key` 的块都有 `data-block-key`、同回合内块身份唯一、回合 running→finished 后同一块身份仍在。修正前锚点用例在「收口后按身份定位」处红。 - 追加(2026-10-02,review:动画期间内容变高会把程序化滚动标记卡住):`scrollToBottom` 把点击那一刻的 `scrollHeight` 当动画目标,而流式正文 / 图片撑开会让它在动画期间继续变高;`programmaticScrollRef` 只在「贴底」那次滚动事件里交还,于是动画停在旧目标后标记永远为真——布局补偿整段被跳过(列表不再跟随新内容),胶囊又已按「已贴底」隐掉,用户停在底部之上却没有任何指示和自动跟随。修正:程序化滚动期间布局补偿不写 `scrollTop`,但内容变高时把动画目标重新对准新的底部(`scrollListToBottom(list, 'smooth')`),动画继续跑到真正的底,标记照常在贴底时交还。 - 验证(2026-10-02 追加,动画目标重对准):`useConversationScroll.test.tsx` 新增一例——点击回到底部后内容变高(`scrollHeight` 1200→1500)触发一次布局变化,断言动画目标从 `[1200]` 变为 `[1200, 1500]`、落到新底部后仍能交出控制权;修正前在目标数组处红。 - 追加(2026-10-02,review:错误行的重试按钮在断言式 live region 内):`DirectProjectHistoryErrorRow` 原来把重试按钮渲染在 `

` 里面,而 `role="alert"` 隐含 `aria-live="assertive"` + `aria-atomic="true"`,交互控件会被卷进整段断言性播报、读屏也不一定把它当可聚焦按钮。修正:容器改为普通 `

`,`role="alert"` 只包住「加载更早对话失败」文案本身,重试按钮是 live region 之外的兄弟。 - 验证(2026-10-02 追加,错误行结构):`DirectProjectConversation.test.tsx` 新增一例断言 `role="alert"` 节点不包含重试按钮、且文案仍在 alert 内并仍可点击;修正前红。 - 追加(2026-10-02,review:世代号推进时机):`historyLoadTokenRef` 的换项目推进原来只在复位 effect 里,而 `projectPathRef.current` 是渲染期赋值的——交接窗口里守卫不再即时生效:React 的 passive effect 走宏任务、promise 续体走微任务,切换提交之后、复位 effect 之前落地的旧读取拿到的仍是旧世代号,会照常合并条目、写游标、关加载态(复位 effect 随后清掉状态,故终态没坏,但白跑一次跨项目读取且留下瞬时脏状态)。修正:推进移到渲染期,与 `projectPathRef` 同一处、只在路径真的变化时推进;复位 effect 不再推进。安全性依据:同一提交里子组件「填充视口」effect 的判据(`turns` / `historyHasMore` / `historyLoading` / `historyError` / 稳定的 `loadEarlier`)都不变,订阅状态复位本身也是 effect,因此这个窗口里不可能新起一次带旧游标的读取,去掉 effect 里的那一次推进不会放过它。 - 验证(2026-10-02 追加,世代号推进时机):该窗口依赖 React 调度(act 会把 effect 与断言放在同一个作用域里冲掉),jsdom 下无法构造出「旧读取先于复位 effect 落地」的确定性用例,因此没有新增红灯用例;既有的控制器世代用例(切项目、A→B→A)保持全绿,行为等价性由「本窗口内不会有新读取启动」的依赖分析支撑。真机若要硬证据,需在慢读取期间切项目并观察是否多打一次跨项目读取。 ## 2026-10-01 Web、后台与 AGC 一键联调 - 背景:Web、管理后台和 AGC 同时开发时,分别启动入口容易产生两套 API/worker/SpacetimeDB,以及重复后台 Vite。 - 决策:新增 `npm run dev:all`,保持 `npm run dev` 现有主站完整栈语义不变;一键入口固定使用 AGC 的 database/data dir,先启动根完整栈,待五个服务就绪后由 AGC 复用该后端,再启动 AGC Vite 与 Tauri,并关闭 AGC 自带后台。 - 影响范围:根开发脚本、AGC 开发启动编排、本地开发运维文档;不改变 API、schema、生产部署和独立 `npm run agc` 行为。 - 验证方式:参数/状态单测、开发栈健康端点 smoke、`.app/dev-stack.json` 身份复用检查、进程树收束检查。 ## 2026-10-01 AGC 命令错误结构化与错误报告口径 - 决策:AGC 命令失败按**具体变体**建模(Rust `#[derive(Serialize, TS)]` 枚举 + `#[serde(tag = "type", rename_all = "camelCase")]` + ts-rs 导出,生成物不手改),`#[tauri::command]` 的 `Err` 直接携带结构化枚举;前端先按 `type` 选类别、可枚举细分再按类型化 `reason` 分流,**任何地方都不对错误文案做判断**。做法沿用 DirectProject 既有约定(`enqueue_direct_codex_turn -> Result<(), DirectTurnEnqueueFailure>`),不是新机制。 - 决策:错误报告池只收**没有任何调用方处理**的错误。预期业务拒绝(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给反馈,永不进池;真故障由调用方带上下文交给错误池(`ClientAuthErrorWrapper` 承载 `source/action/page`,`captureClientError` 用 `instanceof` 解包),`window.onerror` / `unhandledrejection` 只兜底没人接手的错误;408/5xx/网络的判定由调用方在 catch 里做(4xx 一律不报);Rust agent 终态失败仍由失败投影入池。删除 WebView 侧 `shouldCaptureClientError`。 - 边界:变体按**可判定的事实**命名——服务端 400 只给 `status + message`(`AppError.code` 仍是通用 `BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordLoginRejected`),不假装能区分密码长度/手机号格式。报告面板默认全选、只由通知打开的既有承诺不变。`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic` 三处显式采集点保持原行为,按同一口径改造或删除留在后续变更。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{auth_error.rs,auth_session.rs}`、`apps/ai-game-creator-shell/src/services/{clientAuthErrorWrapper.ts,errorReporting.ts,clientAuth.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src/services/generated/`(ts-rs 生成:`ClientAuthError.ts` + 有字段变体的载荷文件)。 - 决策(补充):TS 形状**只有带载荷的变体才有具名载荷类型**——无字段变体在 ts-rs 里就是 `{ type: 'x' }`,有字段的变体是 newtype 变体持有同名 `#[ts(export)]` 结构体,生成 `{ type: 'x' } & X` 与 `src/services/generated/X.ts`;可枚举的细分原因是类型化枚举字段(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`),不是字符串、也不各拆一个顶层变体。前端 `switch (error.type)` 的无字段分支用固定文案,带载荷分支先 `as X` 再读它自己的字段,`reason` 是枚举时再 `switch (payload.reason)`(`default` 同样用 `expectNever`)。不允许在前端手写这层类型,也不再包派生分类 / 提示文案函数(`clientAuthErrorKind`、`clientAuthErrorNotice`、`resolveClientAuthFailure` 已删除)。 - 验证:见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 的验收清单;关键判据是"登录 400/401 业务变体不产生 `report_client_error`、不弹「发现问题」"。 - 决策(2026-10-01,JS 侧载体与抛出时机):认证命令统一经 `invokeClientAuth(command, args)` 调用;拒绝值原样装进已有的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`,构造时把整份载荷 `JSON.stringify` 进 `Error.message`,上报事件因此拿到机器事实),**不新增手写错误类**(`ClientAuthFailure` 已删除),形状完全信任 tauri + ts-rs 映射、不做运行时嗅探。形状读取 / 文案回落 / 提示分类三层(`isClientAuthError`、`getClientAuthErrorMessage`、`presentAuthFailure`)全部删除。 - 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error`;无字段业务 / 会话变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名类型、再用它自己的 `reason` / `serverMessage` / `status` 拼上本次操作的上下文前缀(`reason` 是枚举时再 `switch (payload.reason)`);Rust 不预拼用户可见文案、服务端原文缺失就是 `null`(无兜底文案)。系统变体原样 `throw` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包 `error` 字段取原始错误),`default: expectNever(failure)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。 - 决策(2026-10-01,Rust 侧不再降级):`refresh_session_inner` 的非权威失败直接 `Err(ClientAuthError)`,`ClientAuthStateView` / `ClientAuthRefreshView` 删除 `errorMessage`,续期结果删除 `failed`;`ClientAuthState` 收敛为 `authenticated | unauthenticated`,`ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。 - 追加(2026-10-02,429 按路由判定):`/api/auth/phone/login` 验证码错误次数耗尽返回的 429 是用户可修正的输入问题,映射为 `phoneCodeLoginRejected`(复用现有业务变体、不进错误池);发码路由仍是 `smsCodeThrottled`,其余路由的 429 仍是 `unexpectedRejection`。 - 追加(2026-10-02,401 归 phoneCodeLoginRejected):`/api/auth/phone/login` 的 401 只来自「用户不存在」(验证码错误/失效/过期在服务端都是 400,已由 `phoneCodeLoginRejected { serverMessage }` 带原文);顶层变体 `smsCodeRejected` 退役删除,前端三个 catch 去掉了它那个「验证码错误或已过期」的固定分支,`phoneCodeLoginRejected` 的文案统一为「验证码登录失败:<服务端原文>」。 - 追加(2026-10-02,读 body 失败按已确认状态码归类):AGC 认证请求拿到 `status` 后 `response.text()` 失败,不再一律压成 `authNetworkFailure { unreachable }`;非 2xx 走既有分类(`serverMessage` 为 `None`,如 503 → `authServiceUnavailable { 503 }`),只有 2xx 响应没收完才算传输层故障。分类收敛在 `classify_unreadable_body`。 - 追加(2026-10-02,系统类失败保留原始错误载荷):`clientSessionPersistFailed` / `runtimeSessionInstallFailed` / `authClientInitFailed` 都带 `detail: string`(原始 error),既让调用方有机会分流处理,也让报告包带够诊断信息;原始 error 同时经 `app_log!`(落盘前过 `sanitize_diagnostic_message`)记一行本地日志。`detail` 不贴到界面上:三个 catch 用本操作的固定文案(登录检查 / 发码 / 登录各自不同)。取代上一版"本机 IO 失败不进载荷、原始 error 只进日志"。 - 影响范围(2026-10-01 第二轮):`apps/ai-game-creator-shell/src/services/{clientAuth.ts,platformSession.ts}`(`clientAuthError.ts` 删除)、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、对应 vitest 用例。 ## 2026-10-02 launcher 页面高度契约:外壳分高度,页面不再自己算窗口高度 - 背景:PR #228(`6d2c275d3`)只给项目页补了「外壳纵向 flex + 页面 `flex: 1 1 auto`」的高度修复;其余页面仍各自算高度——帮助页没写高度也没有内层滚动容器,内容一长就被外壳 `overflow: hidden` 裁掉且无法滚动;首页用 `h-screen` / `h-[calc(100vh-32px)]`,模板库用 JS 量父级高度写内联 `height`。 - 决策:把 #228 的修复提升为 `apps/ai-game-creator-shell/src/styles.css` 里的通用契约——`.launcher-main:has(<页面钩子>)` 改纵向 flex 列(`height: 100dvh`,窗口外壳 `height: 100%` 命中时贴合真实舞台;`min-height: 0`、`padding-bottom: 0`),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`;帮助页额外 `overflow-y: auto; padding-bottom: 42px`(它没有内层滚动容器)。契约钩子:`launcher-home-page`、`launcher-help-page`、`launcher-template-library`、`launcher-projects-page`。 - 边界:页面根节点不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;横幅(`.launcher-promo`)是外壳里的真实行,有横幅就靠 flex 自动少一份,不手算偏移。工作台页(`.game-project-workbench`)在窗口外壳里已有等价契约(`.window-chrome__content ...` 那几条),移动端媒体查询里遗留的 `100dvh` / `calc(100dvh - 32px)` 由外层 `flex: 1 1 auto` 收缩兜住,本次不动。 - 影响范围:`apps/ai-game-creator-shell/src/styles.css`、`src/view/home/index.tsx`、`src/view/template-library/index.tsx`。 - 验证:Chromium 真机量测(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅)——首页 / 模板库 / 项目页改动前后高度一致,帮助页从「被裁 112~150px 且无可滚动祖先」变为正好等于 `.window-chrome__content` 高度并可滚动到底;`npm run typecheck`、`npm run check:encoding`、`git diff --check`、`eslint` 通过。 ## 2026-10-01 游戏广场评分展示边界 - 用户确认广场卡片增加一位小数的 10 分制平均分与评分人数,无有效评价显示“暂无评分”;保留现有排序、筛选、卡片打开详情及返回上下文。 - 公开游戏列表和详情共用投影,随现有请求返回 ratingSummary;复用后端有效评价统计并排除隐藏记录,不逐卡请求评价,不增加统计表、缓存或重算任务。 - 共用 DTO 可选字段兼容作者及旧响应,当前公开列表/详情保证返回;缺字段显示“评分暂不可用”,不能伪装为无人评分。读取与后台管理后的下一次刷新一致,不增加推送/轮询。 - 本增量不改变持久化表或评价写入,仅同步读取投影、DTO 与生成绑定;不扩展评分排序、推荐、AGC 或外部 API。 - 用户要求测试保持简单:仅补卡片正常/零评价/缺摘要及公开摘要契约断言,复用已有统计/管理/目录测试和少量隔离数据 smoke;不新增专用 E2E、分页 fixture 或逐层重复测试。 - 权威入口:[游戏广场评分展示合同](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#游戏广场评分展示合同)。已完成工程实现及本地定向验证,待用户验收,未部署;持久化表未变,公开 procedure 返回类型与后端绑定需配套发布。 ## 2026-10-01 后台游戏评价管理规则 - 用户确认新增管理员隐藏/删除评价,后台不能修改分数或正文;隐藏整条评价并从公共列表、平均分、人数和公共分页总数排除,恢复后重新参与。个人区域固定提示“已被管理员隐藏”,用户仍可编辑但不能自动恢复公开。 - 隐藏和删除必须填写原因,恢复不要求;原因仅后台展示。删除物理移除,不能恢复,用户可重新评价,唯一规则继续成立。 - 当前方案范围为游戏名称选择/ID定位、评价用户 ID、评论关键词、状态四类组合筛选,以及分页、详情、单条操作和持久操作记录。不增加批量、导出、举报、自动审核或评分/时间范围筛选。 - 实现边界:评价表末尾追加默认 false 的 is_hidden;新增私有管理记录,事务保存操作人/原因/时间,以创建时间区分删除后重建记录,同 key 重试不得再次操作新评价。不增加统计缓存;用户编辑保持隐藏状态,管理操作不改变用户内容时间。 - 状态:用户确认按方案实施;后台与网站联动已实现并通过本地存量升级、真实 HTTP 和浏览器验证,证据见主规范。待用户验收,未部署。产生隐藏记录后不得直接回退到未过滤隐藏状态的旧后端。 - 权威入口:[后台游戏评价管理合同](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#后台游戏评价管理合同);活动[里程碑](../plans/【里程碑】后台游戏评价管理-2026-10-01.md)与[实施计划](../plans/【实施计划】后台游戏评价管理-2026-10-01.md)。 ## 2026-09-30 网站游戏评价范围与状态边界 - 已确认需求:网站游戏详情支持每账号每游戏唯一一条 1–10 分评分和可空的评论(最多 4000 字符),可修改自己的评价;个人区默认展示已有评价并提供编辑预填,公共列表分页且不排除自己。 - 已确认扩展:详情显示真实平均分与评分人数,空评论计人数,修改不增加人数;当前先由后端评价记录计算,不增加统计缓存。 - 状态:用户已确认按最新方案实现;整数评分、默认每页 20 条、创建时间排序、取消行为与游戏维度保留作为现行合同。网站与后端实现已落地,本地验收证据见主规范;用户验收与生产发布单独确认。原发行 Version 0.2 的排除项不再被解释为永久禁止新增用户评价。 - 实现边界:新增私有 `game_distribution_review` 表和三条评价 API,游戏/账号组合主键保证唯一;可见性与写入同事务,分页与均分来自一致快照,作者资料读时关联账号。网站用游戏/账号上下文隔离草稿,以读取序号隔离旧成功、错误和加载结束,保存期间冻结输入。 - 权威入口:[平台入口与玩法链路](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#网站游戏评分与评价合同);执行验收使用[独立里程碑](../plans/【里程碑】网站游戏评分与评价-2026-09-30.md),不得把既有发行或本次文档检查当作评价功能上线证据。 ## 2026-10-01 生产 Nginx 以模板为唯一来源:host-only 块独立成 snippet、退役路由不做显式 404 - 背景:线上主站 `genarrative.conf` 长期手工维护,`profile` 未进 SPA allowlist 导致 `/profile` 刷新 404,`client_max_body_size` 也停在 `64m`;同时线上存在 4 处仓库模板没有的 host-only 块(画廊读取限流、`/finance-forecast/`、`/medical-science/`、`/home/` 官网首页入口),直接用 `Genarrative-Server-Provision` 覆盖会静默删除它们。 - 决策:生产 vhost 以 `deploy/nginx/genarrative.conf` 为唯一来源;生产机专属路径收进 `deploy/nginx/snippets/genarrative-host-extras.conf`,由主模板 include、由 Server-Provision 安装,新增平台路由仍必须在主模板内声明并同步 `deploy/pingora/nginx-route-parity.matrix.json` 与 Pingora 网关。snippet 内的 location 有意不在矩阵覆盖范围,Pingora 接公网 443 前必须单独确认这些路径的处理方式。 - 边界:退役路由(`/match3d`、`/puzzle`、`/runtime/*`、`/gallery/*/detail`、`/works/detail`、`/worlds/detail`、`/bark-battle` 等)不再进 SPA allowlist,也不配置显式 404,统一落 `location /` 的 `error_page 404 /404.html`。`/home/` 是 2026-08-11 官网拆分前的历史入口,是否退役需与官网侧一起决定。 - 验证方式:`node scripts/check-nginx-spa-routes.mjs`、`node scripts/check-pingora-route-parity.mjs`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`、`bash -n scripts/jenkins-server-provision.sh`;线上改后按生效配置烟测 `/profile`、模板 12 条 SPA 路由、`/admin/`、`/home/`、`/finance-forecast/`、`/medical-science/`、画廊 API 与 `/games/game_/…` 发行网关。 ## 2026-10-01 DirectProject 审批拒绝原因留痕 - 决策:宿主拒绝 app-server 的审批 / 交互请求时,原因必须落 AppData `RUST` 日志。稳定键 `agent.direct_codex.approval.denied`,字段为 `thread_id` / `method` / `reason` / `distinct_reasons`;未绑定宿主执行器时另记 `agent.direct_codex.interaction.no_adapter`(`method` / `outcome`,`outcome` 区分 `decline` / `empty-permissions` / `unsupported-method`);回包未送达另记 `agent.direct_codex.approval.response_delivery_failed`,`cause` 区分 `response-write-failed` 与 `turn-bind-mismatch`。 - 原因:线上对审批只回 `{"decision":"decline"}`,模型与用户都看不到是哪一道闸门(合同缺失 / 预算耗尽 / 阶段已收束 / item 不在途 / 无适配器)拦下的,只能靠复现。 - 边界:`reason` 只接受 `execution.rs` 内的静态分类或宿主自己的错误文本;透传的宿主错误在留痕出口统一走 `sanitize_diagnostic_message`,不带请求参数、上游正文、路径或凭据(去重键也用脱敏后的文本)。有界去重一律按 `MAX_DENIED_REASON_LOGS = 64` 条封顶——适配器路径按回合内 `(method, reason)`、未绑定执行器路径按进程级 `(method, outcome)`、回包未送达按回合内原因各只记一次。拒绝留痕在释放审批临界区(`state`)之后进行;回合收束说明按 `cause` 区分,只有 `response-write-failed` 才说「回包丢失」,`turn-bind-mismatch` 说明作用域无法确认。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs`。 - 验证:`cargo test --bin genarrative-ai-game-creator-shell agent::codex_app_server::execution::tests`(15 passed,含 `denied_approval_records_its_reason_once_per_turn`、`denied_reason_logging_is_bounded`)、`npm run check:encoding`、`git diff --check`。 ## 2026-09-30 退役 DirectProject 工具调用与回合流账本(issue #553) - 背景:`tool-calls.jsonl` / `turn-stream.jsonl` 是只写不读的账本——读命令在 2026-09-16 随聊天真相源收敛删除后,唯一"消费方"是 `game-creator-direct-turn-update` 事件,而全仓已无监听方;工具卡片的真实读路径早已是「项目对话历史 + 运行态事件」的前端投影。issue #553 还暴露了同一时期绝对路径脱敏误伤 HTML 结束标签的问题。 - 决策:整体删除 DirectRuntime 的 `turn-stream.jsonl` / `tool-calls.jsonl` 写入器、`DirectToolCallCollector`、回合流节流器、`DirectCodexTurnObservation::ToolCall` 与 `GameCreatorDirectTurnUpdateEvent`;`update_active_turn` 保留,首页「运行中的项目」快照不变。工具卡片只由读取期在 `agent/thread_manager/wire.rs` 脱敏、截断的历史条目投影。 - 边界:不在运行时做磁盘清理;旧项目里可能残留的文件不迁移、不读取,历史由 Git 保存。 - 未修(另开):`redact_absolute_path_tokens` 的无语境绝对路径扫描仍会把 HTML 结束标签、嵌套 JSON 转义里的 `/` 误判成路径,是 issue #553 的根因;本次只删死路径,未改扫描器。 - 验证:`cargo test --bin genarrative-ai-game-creator-shell agent:: -- --test-threads=1`(929 passed / 0 failed / 5 ignored)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。 ## 2026-10-02 退役AGC独立Agent Runtime与CLI执行面 - 决策:AGC 自建 Agent Runtime 执行面整体退役,按「从未存在」处理——`src-tauri/src` 的 `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`、`provider_handoff.rs`、`provider_retry.rs`、`tool_plan_handoff/`、`user_input.rs` 及专属测试/fixture 删除;`generate_local_game_draft`、`control_agent_run`、`chat_with_game_creator_agent`、`*_game_creator_agent_goal`、`*_game_creator_agent_runtime_*`、`schedule_game_creator_agent_ready_tasks`、`start_game_creator_supervisor_runtime_task` 移出 Tauri `generate_handler!` 与 `check-config.mjs` 白名单;`--agent-run` / `--agent-task` / `--agent-goal-*` / `--agent-resume` / `--agent-context-compact` / runner 状态类 CLI 一并删除,`CliCommand` 收敛为 `LlmStatus | EnvironmentCheck | PreviewServe`。 - 决策(Runner 收缩):外部 Runner 的项目 execution-owner / known-roots / `runner.status` / read-only configure 删除,只保留编辑器桥 RPC(`*.editor.rpc` / `*.editor.ack` / `*.editor.mark_uncertain`)与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog;`--agent-runner` 模式保留,`runner.rs` 用 `TODO(retire-runner)` 记录后续整体退役条件。 - 决策(前端与 harness):删除前端 `read/resume/confirm_resume_game_creator_agent_runtimes*` 调用链、`agentRuntimeById` 状态与 `onAgentRuntimeSummariesChange` 透传、`AgentRuntime*` / `AgentGoal*` 类型、`AgentStatusCard` 的 `runtime*` 字段与 `projectAgentRuntimeSummaries` / `formatAgentCardRuntimeStatus`;删除 `agent-runtime-real-e2e*`、`agent-runtime-steer-real-e2e`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 及其 CI job 与缓存预热条目。 - 边界:AGC 会话命令、项目权限策略、DirectProject 的 `enqueue/cancel_direct_codex_turn` 链路与编辑器桥不受影响;保留项与备选方案见 ADR。 - 影响范围:`apps/ai-game-creator-shell/src/**`、`src-tauri/src/**`、`scripts/**`、`tests/**`、root / App `package.json`、`.gitea/workflows/project-ci.yml`、`deploy/container/README.md`、AGC 实施计划与 Runtime V1.1 文档。 - 验证:`cargo check --tests --bin genarrative-ai-game-creator-shell`、`cargo test --bin genarrative-ai-game-creator-shell runner::`、`node apps/ai-game-creator-shell/scripts/check-config.mjs`(App 目录内执行)、`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`npx vitest run scripts/project-ci-workflow.test.ts`、`git diff --check`。 - 关联:[【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02](../../adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md)。 ## 2026-09-30 release 渠道移除产品名与包名后缀 - 决策:`release` 渠道的正式产品名统一为 `陶泥儿`,Windows NSIS、macOS DMG / updater 归档等由 Tauri `productName` 派生的包名不再包含 `Release` 文本;`identifier=world.genarrative.ai-game-creator.release` 与 `release-win` 更新分区保持不变。 - 边界:`dev` 继续显示 `陶泥儿开发版`;自定义渠道继续使用 `陶泥儿 <渠道显示名>`,因此清单 URL 仍需保留对合法百分号编码空格的兼容。 - 验证:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs`、`node apps/ai-game-creator-shell/scripts/check-config.mjs`、`npm run check:encoding`、`git diff --check`。 > 用途:只记录当前仍有效、会影响后续开发的长期技术、产品与协作结论;同一事实只保留一处当前口径。 > 维护:阶段过程、分支合并和当轮测试数字由 Git 追溯;实现依据以当前代码和最新专题文档为准。 > 格式:参见[决策记录格式](../README.md#决策记录格式),按需填写。 ## 2026-10-01 邀请好友入口回到“我的”页,客户端账号菜单接入邀请码与玩家社区 - 背景:主站“我的”页签的常用功能宫格只剩四项,`邀请好友` 入口在平台个人页恢复时漏了;同一份邀请码在 AGC 客户端里没有任何入口,玩家社区二维码也只存在主站弹层里。 - 决策(主站):宫格按文档口径恢复五项——泥点充值、邀请好友、兑换码、玩家社区、反馈与建议,`grid-cols-5` 与 `.platform-profile-shortcut-grid` 的五列规则保持一致;邀请好友复用邀请弹层的 `invite` 面板,弹层展示邀请码与完整邀请链接,并拆成“复制邀请码 / 复制邀请链接”两个动作。邀请链接仍由后端 `inviteLinkPath` 加宿主 origin 补全,不在共享组件里拼参数。 - 决策(跨端复用,重要):邀请弹层下沉到 `packages/shared/src/components/PlatformProfileReferralModal/`(含 `model.ts`、`index.css`、`index.test.tsx` 与 `assets/` 微信群 / QQ 群二维码),与 `PlatformProfileRechargeModal`、`PlatformProfileWalletLedgerModal` 同口径,由主站 `PlatformEntryActiveFlowShell` 和 AGC 客户端 `AccountReferralDialogs` 共用一份实现。共享组件只吃 props(邀请中心事实、补全后的邀请链接、两个复制回显、宿主剪贴板粘贴能力),自己不发请求、不写剪贴板;宿主能力(网页 HostBridge / 客户端 Tauri clipboard-manager)留在各自宿主。客户端弹层标题由宿主覆盖为“邀请码 / 玩家社区”,与账号菜单入口文案保持一致;主站沿用“邀请好友 / 填邀请码 / 玩家社区”。原地删除 `src/components/platform-entry/PlatformProfileReferralModal.tsx` 与客户端专用弹层实现(含 `launcher-referral-*` 样式)。 - 决策(客户端取数):AGC 侧栏账号菜单在“使用指南”之后新增“邀请码”和“玩家社区”两项。Tauri 侧新增 `read_profile_referral_invite_center`,用当前登录 origin 把 `inviteLinkPath` 补全成完整链接后交给渲染层;客户端只展示邀请码 / 邀请链接和复制动作,不本地另存邀请码、不发放奖励。命令名同时登记进 `scripts/check-config.mjs` 的 native-only 白名单(该门禁要求 `generate_handler!` 里的命令必须被前端调用或显式白名单)。客户端邀请中心按登录账号隔离(`useAccountReferral` 按 `currentUserId` 重置并作废在飞响应)。 - 决策(奖励说明收进右上角帮助按钮):邀请面板正文不再常驻那段黄色说明,改由标题右侧的帮助按钮(`?`,仅 `invite` 面板)承载悬浮说明;鼠标 `pointerenter` 展开、离开收起,触屏 / 键盘用点击切换(按 `pointerType` 区分,两套触发互不干扰),文案随 `role="note"` 可被无障碍读取。面板正文因此只保留邀请码、邀请链接和成功邀请三段内容。 - 决策(每日上限文案改为数据驱动):说明文案不再写死“每日最多获得十次”(旧文案只在网页弹层里硬编码,客户端完全没有这句)。共享弹层用后端字段推导:每日上限 = `todayInviterRewardCount + todayInviterRewardRemaining`(当前 10),说明为“邀请一位好友注册,好友每次都能获得 N 泥点 / 好友完成注册后你也可以获得 N 泥点;每天最多 L 次,今日还剩 R 次”,两端自动一致。后端事实:`PROFILE_REFERRAL_REWARD_POINTS = 30`、`PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT = 10` 在 `module-runtime` 域内强制;被邀请人每次都拿满 30,上限只压邀请人一侧,超限后邀请关系仍成立。 - 已知口径问题(未改,待产品确认):邀请上限的“当日”用的是 `runtime_profile_day_start_micros`(UTC 00:00 边界,即北京时间 08:00 重置),而每日任务 / 每日免费泥点用的是 `runtime_profile_beijing_day_key`(北京时间 00:00)。同一个仓库里两套“每日”口径并存,跨零点前后的奖励计数会与用户直觉不一致。 - 影响范围:`packages/shared/src/components/PlatformProfileReferralModal/**`、`src/components/platform-entry/PlatformActiveProfileView.tsx`、`PlatformEntryActiveFlowShell.tsx`、`usePlatformProfileCenterController.ts`、`vite.config.ts`(移除已删除文件的 Tailwind `@source`)、`apps/ai-game-creator-shell/src/view/layout.tsx`、`src/features/app-shell/AccountReferral.tsx`、`useAccountReferral.ts`、`WorkspaceLauncher.tsx`、`src/styles.css`、`src-tauri/src/account_api.rs`、`scripts/check-config.mjs`、`src/services/accountHost.ts`、`media/social-media-group/*`(移动到共享组件 `assets/`)、`docs/【项目基线】当前产品与工程约束-2026-05-15.md`。 - 验证方式:`npx vitest run packages/shared/src/components/PlatformProfileReferralModal/index.test.tsx src/components/platform-entry apps/ai-game-creator-shell/tests/appSurface.test.ts apps/ai-game-creator-shell/tests/accountHost.test.ts`、`npm run typecheck`(主站,既有 6 处历史报错不变)、`apps/ai-game-creator-shell` 内 `npm run typecheck`(含 check-config 门禁)、`cargo check --tests` 与 `cargo test --bin … account_api::tests`、`cargo fmt --check`、ESLint / Prettier 改动文件、`npm run check:encoding`、`git diff --check`;并在真实 dev 栈上用浏览器与 CDP 附着客户端 WebView2 复截两端弹层,确认同一份实现渲染一致。 ## 2026-09-29 外壳状态栏退役:`status` 一行改由浮层承载 - 背景:首页输入框下方、项目组页面头部那一行由 `WorkspaceLauncher` 的 `status` 承接,写入的内容很杂——进行中进度(`正在创建工作区`、`正在选择项目`)、“已取消 / 已创建项目 / 已打开项目目录”这类回显、失败结论(`创建未完成,请重试`、工作区看门狗文案、Provider 报错)以及项目组页面的 `正在检查项目状态`。这一行常驻占页面,用户明确要求整行改成浮层提示,而不是只把「已取消」挑出来。 - 决策(一个通道):`status` 不再由页面渲染,统一由外壳浮层承载(`LauncherNoticeToast`)。`HomeView` 只保留 `onStatusChange`,`ProjectsPage` 去掉 `status` prop 与 `.launcher-project-page-status` 那段;项目组页面的 `正在检查项目状态` 由外壳按 `recentWorkspaceRefreshing` 与当前视图合成后进入同一通道。 - 决策(生存期按需要分两种):进行中进度与失败结论走**粘性**浮层(`sticky: true`,不自动收起)——这与原来那一行一致,切页回来仍能看到,重建的 10 分钟看门狗文案也不会一闪而过;成功、取消这类回显走**一次性**浮层(`showTransientNotice`,`tone='neutral'` / 成功 2.6 秒、失败 6 秒)。一次性提示优先显示,自己收起后粘性状态(仍在的话)继续显示。 - 决策(显示范围照旧):粘性状态只挂在 `home` / `projects` 两个页面上,进项目工作台、模板库、帮助页不再飘在界面上(原来那一行也只长在列表页上),状态本身留着,回首页 / 项目组时照旧显示;运行 / 预览反馈与取消回显不受这条限制,仍在哪里发生就在哪里弹。 - 决策(一枚 toast):`RunNoticeToast` 更名 `LauncherNoticeToast`(类型 `LauncherNotice` 增加 `sticky`、挂点 `data-launcher-notice-toast`),运行 / 预览反馈、状态栏内容、取消回显共用一个挂点与一套色调;长文案容器限宽 `min(680px, 100vw - 2rem)` 并换行。 - 决策(计时不被重渲染打断):收起回调只从 ref 读,不写进 effect 依赖——外壳每渲染一次都会换一个内联箭头函数,写进依赖会让 2.6 秒在每次重渲染时重新起算。 - 影响范围:`apps/ai-game-creator-shell/src/features/app-shell/{LauncherNoticeToast.tsx,model.ts,WorkspaceLauncher.tsx,ProjectCreation.tsx,useHomeProjectCreation.ts}`、`src/view/home/index.tsx`、`src/styles.css`;用例 `apps/ai-game-creator-shell/tests/launcherNoticeToast.test.tsx`(新增粘性不收起、重渲染不重新计时、取消短时收起)与 `launcherNoticeShellWiring.test.tsx`(原 `runNoticeToast` / `runNoticeShellWiring`;新增「进行中状态与取消回显都只走浮层、首页不留那一行」)、`tests/appSurface/home.suite.ts` 三处取消断言、`tests/{homeWebPreflight,designProjectRestore}.test.tsx` 的 hook 参数。 - 验证:`npx vitest run apps/ai-game-creator-shell/tests` → 191 passed / 1 skipped;`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`;`eslint`(改动文件)、`prettier --check`(改动代码文件)、`npm run check:encoding`、`npm run check:doc-index`、`npm run typecheck`、`git diff --check`。 - 边界:本次只改这一条状态通道的呈现。首页那行**带动作**的提示留在原地——建项已落盘但没进项目时的「已创建的工作区:<路径>」+「打开已创建的工作区」按钮要能点,不能飘走;设置弹层里「项目创建目录」选择器的「已取消」仍是弹层内自己的状态文案,未一并改动;浮层外观没有在真机窗口里目测过(取消原生选择器需要 Tauri 客户端窗口)。 ## 2026-09-29 Game Agent 读取工具接受绝对路径和项目外路径 - 决策:`agc_read_project_context` 与 `agc_list_project_files` 可以读取绝对路径,以及用 `..` 离开当前项目的路径。项目内相对路径仍拒绝 `.agent`、凭据文件名、符号链接和硬链接。项目外读取同样拒绝这些受保护名字和链接,但不因路径落在项目外而失败。 - 范围:`agc_write_file`、`agc_apply_patch`、素材导入和原生补丁仍限定在当前项目。共享 `normalize_relative_path` 不放宽。Codex 进程沙箱仍是 `read-only`。提示词本轮未改。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs`、`direct_tools_mcp.rs`、`direct_tool_bridge.rs`。 ## 2026-09-29 安装钩子的可执行逻辑只允许待在 !macro 里 - 背景:dev 渠道改名迁移钩子的第一版把迁移写成顶层 `Function`,并在函数体里调 `nsis_tauri_utils::KillProcess`;Jenkins 打 Windows 包时 makensis 在 `installer-hooks.nsh` 第 68 行报 `Plugin not found` 并中断(模板第 28 行 include 钩子,早于模板常量与 `!addplugindir`)。 - 决策(形态):`installer-hooks.nsh` 顶层只允许 `!define` / `Var` / `!macro`;所有可执行逻辑写进宏体,靠模板的 `!insertmacro NSIS_HOOK_*`(Section Install,插件目录已 add、模板常量已 define)展开求值;`${INSTALLMODE}` 这类模板常量只在宏体内引用。 - 决策(传参):旧展示名改用运行期变量 `$AgcLegacyIdentity` 传入,不再用 `Push`/`Pop` + `Function`——宏体在 section 上下文展开,既拿不到 include 期还不存在的模板常量,也不能用 `Return` 提前返回。 - 决策(守卫):把「顶层形态」写成用例(`build-release.test.mjs`:顶层不得出现 Function、插件调用、模板常量引用或裸可执行语句),让同形态回归在单测阶段就红,而不是等到构建机上的 makensis 中断。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh`、`apps/ai-game-creator-shell/scripts/build-release.test.mjs`、pitfalls。 - 验证:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` → 42 passed(含新增守卫用例);本地按 Tauri 模板渲染 + 同一份 makensis 复现 Jenkins 原始报错并在修复后零 warning 通过;`currentUser` / `perMachine` 两种形态分别编出 `KillProcessCurrentUser` / `KillProcess`。 - 边界(未验证):真实 Jenkins dev 渠道出包与真机升级仍未执行,里程碑里的升级验收项保持打开。 ## 2026-09-29 策划提示词写明工作区是相对路径根目录 - 决策:常驻提示词用一句说明工作区是相对路径根目录 `.`。策划文件仍放在工作区内并使用相对路径。不命名 `design_artifacts`,不宣布可以访问绝对路径或工作区外路径。 - 关联:`apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md`。 ## 2026-09-29 Codex 私有运行目录先解析系统临时路径上的符号链接 - 决策:新建的 Codex 临时目录确认是普通目录后,先解析为真实路径并去掉 Windows `\\?\` 前缀,再创建 `codex-home`、`workspace` 和隔离用户目录。macOS 的 `/var`、`/tmp` 这类系统符号链接不再阻断 Game Agent 启动。 - Windows 路径转换必须先将 `\\?\UNC\server\share\...` 恢复为 `\\server\share\...`;不能只删除 `\\?\`,否则网络共享路径会变成相对路径。盘符路径继续去掉 `\\?\` 前缀。 - 范围:`validate_game_creator_private_path_ancestors` 不放宽。AppData、客户端配置,以及临时目录内部新出现的符号链接,仍然拒绝。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`。 ## 2026-09-29 策划文件工具说明去掉路径硬限制 - 决策:常驻提示词仍要求策划文件放在工作区内并使用相对路径。`read_file`、`write_file`、`list_dir`、`search_text`、`patch_file`、`delete_path` 的工具说明不再写「工作目录内」或「path 使用相对路径」。说明不宣布可以访问绝对路径或工作区外路径。 - `delete_path` 保留工作区根目录及其上级目录保护,按真实路径在删除前判定;允许经过祖先链接,删除链接本身只移除链接,递归删除不跟随目录内的链接。阶段产物、共享过程文件和速览卡链接的相对路径约定不变。 - 关联文档:[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。 ## 2026-09-29 策划文件工具接受绝对路径和工作区外路径 - 决策:Design Agent 的 `list_dir`、`read_file`、`write_file`、`patch_file`、`delete_path`、`search_text` 可以读写绝对路径,以及离开 `design_artifacts` 的路径。相对路径仍以策划工作区为基准,`..` 可以离开工作区。工作区内写入继续走现有私有文件写入;工作区外写入按普通文件创建父目录并写入。 - 范围:用户工作区浏览和附件导入仍只使用 `design_artifacts`。阶段必需产物仍按工作区相对路径检查。删除允许经过祖先链接;删除链接本身只移除链接,递归删除不跟随目录内的链接。工作区根目录及其上级目录按真实路径保护。提示词本轮未改。 - 关联文档:[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。 ## 2026-09-29 dev 渠道改名迁移放进安装器钩子,且只注入 dev - 背景:dev 渠道展示名从 `陶泥儿` 改成 `陶泥儿开发版`(`identifier` 不变)后,更新路径不会重建快捷方式,旧桌面图标继续指向旧安装目录里的旧 exe,旧 exe 的更新器又把新版本装进新目录,于是用户看到「更新后自动启动新版、桌面快捷方式打开的还是旧的」。 - 决策(迁移位置):改名迁移由**安装器**承担,不做运行期清理。Tauri 的 NSIS 模板在更新模式(`/UPDATE`)与 `/NS` 下跳过快捷方式创建(`CreateOrUpdate{StartMenu,Desktop}Shortcut` 都在 `$UpdateMode = 1` 时提前返回),只有安装器能在「新版刚落地、应用还没启动」的时机把旧身份清干净并补齐当前身份的图标;旧包已经发出去,改不了旧客户端的运行期行为。 - 决策(注入范围):只有 `dev` 渠道 + Windows 目标注入 `bundle.windows.nsis.installerHooks`。旧身份表(`陶泥儿`、`Genarrative AI Game Creator`)属于 dev 的改名史,其它渠道注入会删掉 dev 的安装;macOS 包没有 NSIS 安装器,不下发。 - 决策(删除边界):只清理「旧展示名目录下确实存在我们的主程序」且不等于 `$INSTDIR` 的安装;旧快捷方式先用 `IsShortcutTarget` 校验目标命中旧安装目录再删,不按文件名裸删;桌面图标只为原本就有旧图标的用户迁移,不替用户新增桌面图标。 - 决策(改名纪律):dev 渠道以后再改展示名,必须同步往钩子的旧身份表追加旧名,否则升级后旧快捷方式继续指向旧安装。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh`(新增)、`apps/ai-game-creator-shell/scripts/build-release.mjs`(`createChannelConfig` / `writeChannelConfigFile`)、`apps/ai-game-creator-shell/scripts/build-release.test.mjs`、pitfalls、AGC 渠道安装身份隔离里程碑。 - 验证:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` → 41 passed(含新增「dev 渠道的 Windows 包注入改名迁移钩子」);`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。 - 边界(未验证):钩子只在真实 NSIS 打包 + 真机升级时执行,本轮既没有出包也没有执行安装;下一次 dev 渠道发布后才能验证「旧目录被清、当前身份的图标出现、旧图标消失」。 ## 2026-09-28 速览卡只承载概览、范围与设计入口 - 速览卡帮助读者快速理解游戏及本次范围,分类、支柱、循环与目标玩家可融入概述;不复制系统拆分、具体参数、素材数量、完整排除清单或验证计划,不设固定字数或必填章节。 - 仅在概览内容或入口变化时维护,只链接已有且有用的文档;影响方向或当前范围的重要未决问题简述并引用详细位置。注入说明与样例同步清理,概念阶段必需产物路径、资源登记及审批合同不变,已有用户项目不自动改写。详见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-28 TDD 验证按需去重,范围外内容不自动规划 - 验收判据与必要场景在对应分册完整写一次,其他位置引用并补充独有要求;总册汇总重要结论和缺口,不复制清单。必要数值验算保留,不重复行为状态推演,也不能代替运行验证。 - 实际构建、交付约束与设计验收条件保留;测试脚本、顺序、截图数量和工具由施工方安排。性能目标与专门验证须有实际依据,未执行不得记录通过。 - 当前采用值不自动登记调优待办;具体体验疑虑按需验证,不默认制作对照版本。架构和 TDD 对范围外内容只说明必要边界,已确定的后续计划或影响当前设计的要求按需补充,不自动承诺里程碑及扩展实现。TDD 独立施工、四份产物与关键缺口判据不变。详见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-28 TDD 明确设计要求,内部实现由施工方决策 - 保留“只看本套 TDD 就能完成当前范围实现”:写全设计要求、内容与数值、实际工程约束和验收条件,施工方无需回查 GDD 或猜测关键设计。代码组织、算法、内部接口、数据结构、配置载体及资源命名和打包由施工方决定,未预定这些选择不构成策划缺口。 - 已有工程契约、数据与资源格式、明确交付要求仍须遵循;实现建议不作为唯一方案,不增加逐项登记或用户确认。固定规则不要求配置化,也不禁止施工方使用配置;不承诺尚未定义的模式或开关。文档统一使用“施工方”称谓。 - TDD 总纲、三分册规则、四模板、四样例及上游交接同步这一边界;样例仍保留真实的玩法、内容、数值与表现缺口,未执行验证不写通过。四文件、资源登记、技术范围、阶段审批与已有项目产物均不变。当前合同见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-28 架构与系统按行为组织协作,减少重复展开 - 系统拆分以有用的独立规则边界为依据,简单职责可合并,不因变量、操作不同或未来替换而增加系统和协作层。架构保留职责、关键协作与共享约束,数据归属和文档位置可合写。 - 完整流程在主要负责的系统文档展开,参与方写自身接收、处理和返回,按需引用;行为已说明的协作与反馈不再另表复述。规则、相关模板与样例同步,关键顺序、失败处理及 TDD 独立施工要求保留,已有项目产物不自动改写。当前合同见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-28 数据分册按数据形态组织,避免重复列值 - 少量参数合写含义与当前值,同结构多条内容可分列共用属性说明和完整记录;文案集中列一次,其他位置引用唯一权威定义,共用规则集中说明,派生值只保留计算关系。内部字段与配置载体由施工方决定,实际数据契约按需保留。 - 数据模板、写作规则及样例采用同一口径,当前范围完整内容、数值、文案与验算要求不变。当前合同见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-28 策划文档密度按项目需求与复杂度安排 - 常驻提示词统一要求按项目需求、规模和复杂度组织内容;模板与样例仅供参考,章节和字段按需增减、合并,简单内容简述,复杂或易歧义处充分展开,不为填模板增加设计或重复论证。 - 精简保留当前阶段判断与后续实现所需信息,TDD 可独立指导当前范围实现的标准不变。当前合同见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-27 TDD 按施工信息简化,验收对象为策划案 - 保留“只看本套 TDD 就能完成当前范围实现”的标准。文档、数据引用与必要验算须完整自洽;保留实际构建与交付约束、验收判据及必要场景,具体测试安排由施工方决定,不要求游戏或素材在策划案验收前已经完成,未执行不得记录通过。 - 技术选型限定在 Game Agent 已支持的 Web、Unity、Godot、Cocos Creator 范围,不逐框架说明支持程度;新 Web 使用 npm + Vite,二维使用 Phaser 4.2.1,三维按需求选型,预览与导出读取 dist。已有工程不因模板自动迁移。 - 收编可重组内容,来源版本集中维护;取消固定编写顺序、能力清单、建表步骤、条件架构、检查分级及资产生产台账。数据保留完整内容、数值、文案与验算;美术保留表现要求、对象与状态、用途及实际接入约束;四份必需产物、资源登记与阶段审批不变。 - 星露谷 TDD 样例统一首个日常原型,具体参数是示例假设,当前施工缺口如实标明,不能将局部算术或未核验的原作资料当作完备证据。当前合同见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-27 系统文档按实际行为展开 - 系统层围绕触发、规则、状态变化、结果与协作组织内容,取消固定十二节、编号追溯、统一取舍表和枚举表达要求;类型规则与模板按需使用,不为填模板添加机制或预设玩法。 - 十二类资料保留领域问题,职责和数据归属遵循实际架构;UI 维护自身交互与临时状态,正式玩法校验和结算归对应系统。跨系统行动明确成功、失败与中断后的结果。 - 已定规则、单位和参数留在系统文档,TDD 按相关内容收编并补齐,不依赖固定交接章节;Sxx、产物路径与审批合同保持不变。战斗样例区分暂定原型、撤退与倒下后果及规格缺口,TDD 继续以当前范围可独立施工为完成标准。 - 当前规则见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-27 系统架构按职责与协作组织 - 架构保留系统编号、职责、权威数据归属、文档位置、实现范围和验证,取消固定系统数量、P0 必需性、统一图表与分类、逐轮变更记录。双向交互按含义与更新顺序判断,不以图上有环自动要求重切。 - 同一事实由明确的权威方维护;只读副本、派生视图和快照说明来源及更新或恢复方式。必要的规则与参数可在架构明确,由系统文档展开、TDD 收编补齐。 - `project/03_systems/...` 是策划文档映射,不决定代码目录。系统与 TDD 的直接引用按实际范围衔接,TDD 仍须写全当前施工所需规格;星露谷首个原型包含基础采集,单日选择与多日成长分别验证,样例缺口如实列明。 - 当前规则见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-27 顶层设计按实际玩法展开 - 顶层说明游玩过程、关键规则与反馈、资源与进展、选择后果和版本范围;取消固定循环层级、回报数量、资源消耗链、日历节奏和失败档位,允许说明玩法所需的具体参数。 - 原型范围依据验证问题确定,与完整版本范围分开;验证可结合观察、玩家反馈和指标,预期与已验证结论分清。 - 架构按顶层已有内容检查玩法覆盖,可拆分、合并能力范围并由多个系统协作,不依赖固定循环图、独立定稿章节或逐项映射。TDD 自足性、产物路径和审批合同保持不变。 - 当前规则见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-27 概念设计按内容组织,取消固定填写程序 - 概念层先利用已有对话与资料,按实际缺口补问;模板和样例按需参考,不强制参照、固定句式、字数、唯一卖点、六项锚点或调性编号。 - 概念明确核心体验、主要吸引力和重要边界。设计原则帮助判断方向,不代替具体分析;必要的数值、操作或界面信息可以用于说明概念,规模按实际团队与项目条件确定。 - 规则、模板、样例及下游引用同步调整:重要取舍与视觉设计依据按具体内容或章节引用,不要求张力编号或 T 原则;TDD 继续保留来源版本及完整施工规格。现有产物路径与审批合同不变。 - 当前规则见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-27 策划共享文档按用途和内容变化维护 - 分析文档按需保留重要取舍依据,决策台账集中待处理事项,对话摘要仅在用户需要时维护,速览卡仅随概览内容变化更新;取消概念至系统分册中的重复状态、连续编号和多处登记流程。TDD 同步取消逐项代决登记,规格直接写入 TDD,未决问题解决后补齐正文并关闭待办。 - 保留“只看 TDD 就能完成当前范围实现”的标准,以及内容收编、来源版本、变更同步和施工所需清单;影响当前实现的关键问题未解决时不能宣称完备。文件路径与现有审批存在性检查不变,不增加内容校验,不批量改写已有项目文件;旧 `resources/SKILL.md` 合并总稿已删除,现役规则由全局提示词、阶段上下文及资源目录登记的分册承接。 - 当前维护规则见[策划 Agent 生产迁移方案第 6 节](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 ## 2026-09-22 UI 编辑器预览画布补上右键拖拽平移,节点菜单改为右键抬起弹出 - 背景:预览画布此前只有中键与空格+左键平移,右键整段留给节点操作菜单(`UiTreeRenderer.onContextMenu` 直接弹 `UiNodeContextMenu`)。这次要补右键拖拽平移,并要求"拖拽过就不许再触发右键菜单"。实测(Linux Chromium 151 / Firefox 151,真实 X11 输入)确认 `contextmenu` 在**按下**瞬间触发,且原生菜单一旦弹出,页面之后收不到任何 `pointermove` / `pointerup` / `mouseup` / `auxclick`,所以"先让菜单弹、拖拽时再关"在浏览器层面不可行;headless 没有原生菜单,Playwright 复现不出该行为。macOS 的 `contextmenu` 在 mouseup 触发(本容器无法实测),但同一条实现路径对两种时序都成立。同一次实测:键盘菜单键触发的是 `button: -1`,所以"只认按钮 2"的拦截天然把键盘菜单留给原有节点菜单路径。 - 决策:预览视口在捕获阶段拦截按钮 2 的 `contextmenu`(`preventDefault` + `stopPropagation`),右键手势改由预览自己裁决:按下时记录起点、`setPointerCapture` 并交焦点;移动越过与左键拖拽共用的 `DRAG_THRESHOLD_SCREEN_PX`(2px)后本次手势定死为平移,按"按下点全量 delta"更新视口(光标复用共享 `CanvasViewport` 的 `isPanning` → `cursor: grabbing`,三个平移绑定一起生效);未越阈值且在预览内抬起时,用 `[data-node-id]` 加树容器 `data-tree-id` 命中节点并打开 `UiNodeContextMenu`,保留"右键即选中该节点"的既有语义;空白处干净右键不做事(原生菜单已被抑制)。中键、空格+左键平移不变,macOS ctrl+左键与键盘菜单键继续走原有即时菜单路径;平移是纯视图操作,不写 State、不进历史、不受 `isLocked` 与空格按住态限制。 - 原因:右键同时承载菜单与视图平移,只能等到手势结束再裁决;把判定放进预览,是因为树、节点命中、预览边界与拖动阈值都在预览手里,而 `useNodeTransformInteraction` 应保持左键变换的单一职责;`[data-node-id]` 命中也已是 `resolveHitNodeId` 的既有模式。 - 代价与取舍:右键从"按下即弹菜单"变成"抬起才弹"(与 Windows 自身右键菜单一致);预览内空白处的浏览器原生菜单被永久抑制;右键平移与节点菜单互斥(越过阈值后抬起不再弹菜单);`grabbing` 光标在悬停到节点上时仍会被节点自身的 `cursor-move` 覆盖,与共享画布现状一致。本次只改 UI 编辑器预览:AGC 美术画布仍只有空格/中键平移,资源画布保留自己的右键平移实现,都不动,也不抽 `packages/shared`。 - 验证方式:新增 `previewRightPanGesture` 纯状态机单测(阈值跨越、起点全量 delta、拖拽吞菜单 / 干净抬起开菜单、取消与失焦清理),并在 `previewRightPanDrag.test.tsx`、`previewRightPanGesture.test.ts` 补右键回归(含键盘菜单键 `button: -1` 不被拦截的用例);运行 `npx vitest run`(定向文件)、`apps/ai-game-creator-shell` `npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding`、`git diff --check`。 ## 2026-09-24 接单化 review 收口(第二轮):失败载荷分类、拒单身份与提示口径 - 决策(失败载荷的 `kind` 收成 typed 枚举):新增 `DirectTurnFailureKind`(`Serialize + Deserialize + TS`, `kebab-case`,7 个变体,含先前两份名单都漏登记的 `turn-interrupted`),`DirectTurnError::wire_kind` 返回 `Option`。线上仍是 `{kind, message}`、取值不变,只有 TS 侧从裸 `string` 变成可穷尽收窄的联合类型;全仓没有按 `failure.kind` 分流的代码,它只给界面选语气。 - 决策(并发拒单的两个身份是回合身份):`DirectThreadManager::accept_turn` 冲突时返回占用对象的 `turn_id`,`DirectTurnReservation::accept` 把这一轮请求的 `clientTurnId` 传成 `incoming_invocation_id`。改动前这两项是进程内 UUID,`TurnAlreadyRunning` 的"同一轮仍在处理中" 分支永远命中不了,也与"回合身份由 `clientTurnId` 推导"的口径冲突。占用对象自己的 `token` 仍是 UUID(`complete_direct_thread_turn_if_reserved` 靠它配对),只换错误载荷里的两项。 - 决策(目录锚不定的拒单不再写诊断):`DirectTurnError::ProjectRootUnanchored` 从 `is_reportable()` 拿掉,与 `ProjectRootUnusable` 同类——符号链接 / 权限 / 目录被删都是用户自己就能修的文件系统事实。 改动前它被命令边界覆写成 `direct-codex-failure:v2` 收口文案,界面上那句"无法锚定 Direct 调用项目 目录:{cause}"被内部诊断串顶掉;现在界面按 `Display` 显示,也不再进 `.agent/runtime/errors`。 可留痕的拒单只剩 `environmentNotReady` / `hostStateUnavailable`。 - 决策(认不出的拒单也要在聊天里有同级提示):`environmentNotReady` / `hostStateUnavailable` 除上报 + 横幅外,再补一条与用户消息同级的提示——拒单没有接单、不产生 `turn.completed`,否则那条乐观用户 气泡后面永远没有解释(改动前的注释"宿主已经把它放进了 `turn.completed.failure`"对拒单不成立)。 文案走 `projectRuntimeVisibleRejectionError`:取宿主收口文案里已脱敏的摘要与建议,**不套阶段标签** (拒单这一轮没有开始,阶段只会是默认值);非结构化错误仍只走横幅(它可能发生在接单之后)。 - 决策(失败说明的文案口径):`projectRuntimeVisibleError` 补上宿主 `Display` 事实句的模式 (`执行通道已断开` / `等待模型回合结束达到硬上限` / `宿主任务提前结束` / `收尾历史失败` 一族), 并给落盘那档补上不带"失败"二字的事实句;不回落宿主原文(`TransportClosed` 的原文带 `exitStatus=` / `stderrClass=`)。同时修掉收口文案的版本口径:解析只认 `v1`、宿主发的是多一段 `code=` 的 `v2`, 脱敏摘要一直命中不了。口径定为"不加模式就只会看到通用文案",写在 `directTurnFailure.ts` 的注释里。 - 明确不做:不改线上载荷形状与 `kind` 取值;不加新的失败阶段取值(拒单仍落默认阶段);不动 `ProjectRootUnanchored` 之外的拒单分类。 - 决策(连接死亡的失败事实先于看门狗可见):`CodexAppServerInner::closed` 的语义定为"这一段已经收束 / 失败事实已经记下",看门狗就盯着它,所以它不能再兼作死亡收口的去重标志——去重改用私有的 `connection_end_claimed`,`fail_game_creator_codex_app_server_connection` 不再置 `closed`, `closed` 只在 `shutdown_game_creator_codex_app_server_inner` 里、`record_execution_turn_failure` **之后** 置位。改动前收口路径先置 `closed` 再做"两次加锁 + 一次日志写",200ms 看门狗可能在这一段里抢跑,把 这一轮收束成 `Interrupted`,typed `TransportClosed` 记不进去,终态退化成"本轮已结束、没有原因" (失败事实是在模型终态那一刻被快照的,晚补记无用,所以只能保证"事实先于可见性")。代价是其它读 `closed` 的地方会晚几十微秒看到"连接已死",两个并发的死亡观察者仍会各自走到幂等的收束函数。回归用例 `connection_death_records_the_failure_fact_before_the_watchdog_seals_the_turn` 卡住 stderr 摘要锁把窗口 拉成确定性,把看门狗真正跑起来钉这条(顺序反了就红)。 - 决策(接单之后的失败不回命令返回值):`chat_with_game_creator_direct_codex_typed` 在接单后的历史追加 写失败时仍然写 `turn.completed` 失败终态,但 `return Ok(())`——命令的 `Err` 只表示**拒单**。改动前同一 个失败从"事件里的说明"和"命令 `Err` 的横幅"两条通道下发(且 `EnvironmentNotReady` 会写诊断 + 上报), 前端又把 `Err` 当"这一轮没开始",于是忙态与出队同时被事件和返回值两条路推。**不继续起整轮**: `project.jsonl` 是这条对话的单一事实源,用户消息没落盘时继续跑只会得到一条没有开口用户消息的助手回复, 失败还会被静默。用例:Rust `a_history_write_failure_after_accept_closes_the_turn_instead_of_rejecting` (恰好一条失败终态、不带拒单收口文案、占用释放)、前端 appSurface 的落盘失败用例(说明只来自事件且 恰好一条、忙态放掉、下一条能发)。 - 影响范围:Rust `apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/mod.rs,direct_turn_error.rs,direct_turn_failure.rs,direct_turn_accept.rs,direct_thread_manager.rs,direct_runtime/user_input.rs}`; 前端 `src/features/agent-runtime/model.ts`、`src/view/project-development/chat/{conversation/directCodexConversation.ts,conversation/directTurnFailure.ts,controller/useDirectProjectChatController.ts}`、 `src/view/project-development/chat/generated/DirectTurnFailureKind.ts` 与 `tests/{agentRuntimeModel.test.ts,directThreadChat.test.ts,appSurface/chat-composer.suite.ts,appSurface/project-conversation.suite.ts}`; 文档 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`、`docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md`。 - 验证:Rust `cargo test --bins "agent::"`(902 passed / 5 ignored)、定向 `cargo test --bins "agent::direct_turn_error"`(15 passed)、`cargo fmt`;前端 `npx vitest run tests/{appSurface.test.ts,directRunAnalytics.test.ts,directProjectTurn.test.tsx,agentRuntimeModel.test.ts,directThreadChat.test.ts}` (277 passed / 9 skipped)、`npm --prefix apps/ai-game-creator-shell run typecheck`、`npm run check:encoding`、 `git diff --check`。真实客户端观感未复核。 ## 2026-09-24 接单化 review 收口:终态写点、返修控制流、终止判据与失败投影 - 决策(终态的写点在整轮真正结束之后):Direct 回合先固定终态判定的上下文,`turn.completed` 的写出 挪到执行结果收集、历史落盘、structured output 解析都定型之后,成功与失败共用一个写点。解析失败也是 这一轮的失败,落进同一份失败载荷;改动前终态先写、再解析,解析失败时终态已是 `completed`,占用对象 的兜底变成空操作,用户看到"本轮结束、没有回复、没有任何解释"。收尾结果因此拆成 `DirectTurnReport`(报告正文 + 解析结果),占用解除与终态事件一起走 `DirectTurnTerminalContext::write`。 - 决策(封口返修要求是控制流,不是失败):`HostOutcome::RepairRequired` 不再伪装成 `LlmError::InvalidRequest("validation-source-changed: …")`,改为 typed 的 `DirectTurnRunFailure::RepairRequired` → `DirectTurnError::RepairRequired`:不写终态、不进载荷、不上报, 由 `direct_runtime` 的返修循环写回提示词继续跑(与 `ReviewRequired` 同一族,次数上限仍留在产生侧)。 改动前它被判成 `failed` 终态、界面收到一条假失败,还会让同一个逻辑回合写出第二条终态。 - 决策(用户按下的终止不算通道失败,判据收进 `fail_turn`):失败事实的判据是 `!is_closed() && !host_stop_requested()`,不再由各调用点各写一遍 `!is_host_ending()`。用户点「终止」时 标志先置位、阶段后变,原来的窗口里到达的 `TransportClosed` 会把用户自己的终止记成 `transport-failed`。 - 决策(登录态失效的两条分类路径统一可重试):认证失败不再按"重跑整轮"处理,刷新失败与重试失败都按 可重试的回合失败呈现(用户可见文案可能多一句"可直接重试",真实客户端观感未复核)。 - 决策(失败载荷的健壮性):前端 reducer 对 `failure.message` 做运行时判据(缺字段 / `null` 不再抛错, 与 `directTurnFailureNoticeText` 同口径);交付报告兜底只读一次 `terminal_report`(两次读取之间状态可能 变化,`None` 不再被 `unwrap_or_default()` 变成空回复);失败说明条目在无身份无时间时会撞成同一条 (已知边界,仅补注释)。 - 明确不做:不改线上载荷形状(仍是 `{kind, message}`);不给 DirectProject 回合补端到端集成用例(缺轻型 假 app-server 夹具),判据落在策略函数与适配器单测;不持久化"可见但不喂模型"的失败条目(TODO)。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{mod.rs,execution.rs},direct_runtime/{mod.rs,user_input.rs},direct_turn_error.rs}`、前端 `chat/{conversation/directThreadChat.ts,generated/DirectTurnError.ts}` 与 `tests/directThreadChat.test.ts`; 文档 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`、`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`。 - 验证:`cargo test --bins "agent::"`(952 passed)、`cargo test --bins "direct_"`(475 passed)、定向 `codex_app_server`(102 passed)、前端 `directThreadChat.test.ts`(36 passed)与 `npm run ai-game-creator-shell:typecheck`、`npm run check:encoding`、`git diff --check` 通过。真实客户端观感未复核 (终态写点与终止竞态落在真实宿主收尾上,单测盖不住)。 ## 2026-09-23 Direct 回合错误改 typed:调用级拒绝与回合级失败分开 - 回合失败在宿主内部改成 typed 的 `DirectTurnError`(`apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs`):每个变体自带字段,调用级拒绝(并发复用同一 `clientTurnId`、另一条回合在跑、权限策略拒绝、目录锚不定、输入校验、环境/凭据未就绪)与回合级失败(模型调用失败、通道断开、等待超时、app-server 单方面中断、阶段失败)不共用判据,分流只认 `is_turn_failure()`。 - 根因:改造前两层错误混在同一份字符串里,靠对原因文本做子串匹配决定"算不算失败""要不要反馈给模型""怎么给建议",任何文案改动都可能静默改变分流;并发拒绝还只靠一个前缀字面量给前端识别。 - 分类不再做文本匹配:原生失败分类只解析 app-server 写下的 `codex-app-server-error:` 结构化前缀,转成 `DirectCodexNativeKind` 后再 `match`。 - 明确不做:不改线上载荷(仍是 `{kind, message}`)、不改命令边界签名(仍是 `Result`)、不改前端可见文案映射与 `wire_kind` 取值;Rust 侧不再解析那份字符串,字符串只在 `Display` 一处生成。不给深层尚未 typed 的事实补 typed 出口,只留一个显式的桥变体并在注释里写明新分类必须先加 typed 变体。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{direct_turn_error.rs,direct_turn_failure.rs,direct_delivery.rs,direct_runtime/mod.rs,direct_runtime/user_input.rs,codex_app_server/mod.rs,codex_app_server/execution.rs}` 与 `apps/ai-game-creator-shell/src-tauri/src/cli.rs`;文档 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md`。 - 验证:`cargo test --bins -- direct_`(460 passed)、`cargo test --bins -- codex_app_server`(100 passed)、`cargo fmt --check`、`npm run check:encoding`、定向 `git diff --check`。整包 `cargo test --bins` 在本机被既有 `tests::provider` / `tests::project` 重型用例挂住(并发跑测试时另有一条锁竞争用例会假失败),非本次改动引入。真实客户端观感未复核。 ## 2026-09-23 ACL 提权修复按目标做 single-flight - 背景:`windows_acl_repair_target` 对 Managed 作用域返回的是「第一个读取被拒的祖先」,同一祖先下的多个项目会解析到**同一个** repair target;而唯一的去重只是单次调用内的局部 `attempted_targets`。于是启动页一次挂载(≤8 个最近项目并发检查)会启动同样多次 `powershell -Verb RunAs`,用户看到叠在一起的 UAC 弹窗(issue #498)。 - 决策:新增进程级闸门 `acl_repair_gate`,key = `(规范化 repair target, scope)`。并发调用只允许一次真实提权,其余等待并复用**同一结果**;结果在冷却窗口内直接复用(成功 30s / 失败 15s / 用户取消 120s),等待窗口 60s 超时按失败关闭。leader 异常退出由 RAII 兜底记为失败并唤醒全部等待者,避免等待者被永久挂住。 - 决策补充(key 归一化):key 的路径半边经 `windows_acl_repair_gate_key` 归一化——去掉 `\\?\` / `\\?\UNC\` 前缀并统一小写。最近项目列表里同一项目实测同时存在 `\\?\C:\...` 与 `C:\...` 两种写法(客户端 localStorage 实测),不归一化就是两个 key,同一个目录仍会弹两次 UAC。这里刻意只做前缀与大小写归一而不 `canonicalize`:待修复目标恰恰是「读不动的目录」,解析不可靠。 - 决策补充(冷却基准):冷却从**结果落库**时刻算起,不是 leader 起跑时刻。UAC 弹窗会被挂着几十秒到两分钟,用起跑时刻会让 120s 拒绝冷却在用户应答前就过期,前端 15s/45s/120s 的整表重查紧跟着再弹一次。 - 决策补充(leader 失效接管):`leader_deadline`(默认 5 分钟)之后,新调用可以接管仍是 `running` 的 key;每个 leader 带令牌,被接管后旧 leader 迟到的结果直接丢弃,不会覆盖接管者的结果。真机上无人应答的 UAC 约 2 分钟自然超时,所以这个上限只兜「提权子进程真挂死」——否则该目标会永久按失败关闭(`clear_denials` 不清理 running,只能重启客户端)。 - 错误类型化:用户取消 UAC 的错误统一带稳定标记 `AGC_ACL_ELEVATION_DENIED`,前端据此判定「不可自动重试」,不再依赖中文文案匹配。 - 用户主动操作(打开/新建项目、文件选择器选择目录、重命名刷新)会调用 `clear_game_creator_acl_elevation_denials` 清除拒绝记忆,保证显式重试仍能再次请求提权。前端唯一入口是 `features/app-shell/aclElevation.ts` 的 `clearAclElevationDenials()`:最近项目 hook(`rememberRecentWorkspace` / `refreshRecentWorkspace`)与打开/新建链路(`useHomeProjectCreation.openProject`,覆盖行内打开与 picker)共用它;漏挂入口会让用户「点了打开立即失败、也不问授权」。 - 未做:给提权子进程加有界等待(`Start-Process -Wait` 目前无超时)。理由:中断挂起的 UAC 流程比等待更糟,single-flight 已把并发弹窗收成一个,follower 的等待由 60s 窗口兜底。 ## 2026-09-24 DirectProject 状态条口径翻转、几何约束与对话 Markdown 容错 - 背景:AGC DirectProject 对话区底部的「陶泥儿正在处理 / 已耗时 12.4秒」状态条同时退化三处:① 读秒 1 秒一跳(耗时文案不足一分钟显示一位小数,小数位却一秒才动一格);② 窗口压矮时被挤扁(300px 高压到 33px、240px 时 24px,文字被 `overflow: hidden` 裁掉);③ `turn.started` 之前(模型首 token 前,实测约十秒)整条卡片不出现,界面没有任何「正在处理」的交代。同批还修了对话 Markdown 的两处代码块问题(不换行把消息拉宽、粘在正文行里的围栏导致代码块解析错位)。 - 决策(卡片口径翻转,**更正** 2026-09-22「卡片口径取保守」):卡片与已耗时起点改读 `displayBusy`(本地命令在飞 ∪ 原生已确认在跑)与「最新一个**未结束**回合的用户发送时间」。理由:`turn.started` 要等宿主应答返回才发出,只认原生真相会让首 token 之前那段没有交代;窗口期这一轮确实已经交给宿主(本地命令在飞),文案不虚报「宿主已在跑」之外的东西。(**再更正** 同日:本地乐观气泡已删,已耗时起点改读该轮的 `turn.started.at`——运行中读实时值、收口后读盖在条目上的值;接单窗口里还没有这一轮的条目,卡片只报「正在处理」、这一段不读秒。卡片口径本身不变:仍读 `displayBusy`。) - 决策(那条预言的处置):2026-09-22 那条写「若将来改成窗口期也显示卡片,`running` 在渲染层就没有消费者了,应把投影压成 `unfinished: boolean`」。本次改完后投影三态**仍有**消费者(**再更正** 同日:投影已压成两态 `running` / `finished`,`awaiting-start` 随本地乐观气泡一起删除;下面这两条消费者读的判据不变)——`DirectProjectTurn` 用 `state !== 'finished'` 做否定式判断、`state === 'running'` 挑流式正文,状态条也用 `state !== 'finished'` 定起点——所以不动 `DirectChatTurnState`,也不压缩成布尔。 - 决策(状态条几何):卡片在 `.project-chat-conversation` 这条定高 flex 列里必须 `flex: 0 0 auto`。它带 `overflow: hidden`,按 flex 规范该项的自动最小尺寸归零,是这条链上唯一还能被压缩的项;压缩只能由消息列表吸收。同一选择器只保留一条规则(几何 + 不可压缩),不留两份。 - 决策(对话 Markdown 对模型输出的容错):解析前先 `normalizeMarkdownFences` 再压缩空行;代码块 `pre` 与块内 `code` 各自都给 `whitespace-pre-wrap` + `break-words`。细则与判据见 `pitfalls.md` 同日两条。 - 影响面:`apps/ai-game-creator-shell/src/{styles.css,components/ChatMarkdownMessage/index.tsx,view/project-development/chat/{DirectProjectChatView.tsx,components/DirectProjectConversation/DirectProjectConversation.tsx,controller/useDirectProjectTurnStatus.ts}}`;用例 `tests/{ChatMarkdownMessage.test.tsx,directProjectProcessStatus.test.tsx,appSurface/{chat-composer.suite.ts,project-development.suite.ts}}`。 - 验证:`npx vitest run apps/ai-game-creator-shell/tests` 186 passed / 1 skipped(1874 条里 1860 passed / 14 skipped);真实 Chromium 夹具复核读秒 100ms、窗口 900→220px 高度下卡片恒为 36px 不被裁、代码块换行与两类粘住围栏;变异验证(读秒改回 1000ms、删 `flex: 0 0 auto`、卡片退回 `nativeRunning`、停掉围栏归一化、换行类名退回)逐条变红。 ## 策划 V1/V2 退役的现行边界 - 旧策划 V1 和 Runtime V2 均已删除,当前策划入口统一使用独立 Design Agent。V1 被 V2 接替只描述历史过程,不表示 V2 仍在使用。 - 旧策划版本的阶段审批、`plan.submit_gdd`、planning session binding、exact planning lifecycle v3、专属身份白名单、IPC 和测试约束均为历史记录,不能作为恢复代码或保留孤立实现的理由。不新增旧版本兼容别名、双跑或回退链路。 - 通用项目锁、权限、持久化和当前 Design Agent 能力按实际调用保留;清理未用参数不扩大为删除调用方的持锁范围或锁归属校验。 - 当前事实源:[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。 ## 2026-09-23 运行视窗:右下角全屏预览 + 没有内容就自动收起的信息栏 - 背景:运行页右下角缺一个把游戏画面放大到整屏的入口;运行视窗下方常驻「信息展示 / 数值微调」两张卡片,没有选中资源时就是两块空白,验收现场提出「没有功能就暂时隐藏」。 - 决策一:新增 `useElementFullscreen`(`apps/ai-game-creator-shell/src/features/project-workspace/`),用标准**元素级** Fullscreen API 把**画面那一格**(`.game-run-preview`)送进全屏——不接 Tauri 窗口级全屏,那是把整块工作台连对话栏一起放大的「全屏应用」,不是「全屏预览画面」。入口贴在画面右下角,全屏后仍在原位可点退出;按钮态只认 `fullscreenchange`,Esc、宿主退出都会回落。`requestFullscreen` 不存在或被 `fullscreenEnabled === false` 关掉时整枚入口不渲染,不留点了没反应的按钮。 - 决策二:`.game-run-panels` 改成「有内容才存在」的可收起信息栏。判据只有「有没有内容」(当前 = 存在资源选中态——在资源画布或浮层资源面板里选中一张资源后切到运行页签仍保留,信息展示渲染它的只读字段;运行画面上的「点选素材」只往对话插入引用,不改选中):内容从无到有自动展开、从有到无自动收起,同一段内容里用户手动收 / 展不被别的渲染重开。收起态只剩一行「收起信息栏 / 展开信息栏」按钮,条目卡片的 `156px` 最小高度不再变成空白色块;只有一栏内容时卡片铺满整行。手动态按资源 id 在渲染期派生(不挂 effect 回写):手动收 / 展只对做出动作时的那张资源有效,换到别的资源回到默认(有内容即展开),同一张资源即使清空选中后再选回也仍记得上一次的手动状态;这样「刚有内容」的那一帧就已经是展开态,不会先画一帧收起态再展开。 - 决策三:「数值微调」暂时没有登记表(前端没有数据源),按用户口径在没有功能时先不渲染它的区域标题与卡片,对应 `label / input` 声明一并删除;登记表接进来时与内容一起回归。这一条覆盖 PRD §3.4 原先「保留两个面板标题、不得因空内容压缩」的口径,PRD 与技术方案已同步改写。 - 决策四(同日收口全屏回归):用户报「退出全屏后画布仍保持全屏比例」。根因不在全屏本身,而在运行画面的尺寸上报回灌——自适应页面把视口原样报回(内容尺寸 = 容器尺寸),宿主把它当成「内容高水位」,`resolveLocalGamePreviewFitLayout` 的 `max(容器, 内容)` 就把画布钉在全屏那一帧的尺寸上;退出后 iframe 视口不再变化,桥也不会再上报,于是永远回不去(实测 1015×660 → 全屏 1416×808 → 退出仍是 1416×808、缩放到 0.72,画面按全屏比例缩成一条带黑边的窄幅)。修法:内容尺寸与它被接受时的容器尺寸在两个轴上都相等(<1px)时不算高水位,直接按容器尺寸给画布;真比容器高的页面(内容 ≠ 视口,桥注入的原始动机)仍按原生尺寸缩放显示。回归用例 `tests/localGamePreviewFrame.test.ts` 的 `returns the fitted iframe to the container after the host viewport shrinks`(改前必红,实测 1416px vs 1015px)。 - 决策四的残余边界(明确不修):若某个**固定尺寸**页面恰好等于它被接受时的容器尺寸,且缩小容器后它上报的内容尺寸再不变,就会一直按容器取画布(页面自身溢出被裁)。评审提过「内容尺寸没变也把这条记录改认新容器」,我实现后又**实测回退**了:那条过渡期上报(内容还是放大前的旧值、视口已是缩小后的容器)会被当成固有尺寸,全屏那类问题原样复现且同样永久(iframe 回到旧尺寸后桥不再上报)。两者在宿主拿到的数据上不可区分,按 AGC 常态(桥对自适应与「固定画布但自适应文档」两类页面实测都报「内容 = 视口」)选自适应优先;页面报告新内容尺寸时立即回到 `max(容器, 内容)` 等比缩小(用例 `refits to the reported content size after the container shrinks`)。根治方向在桥 / 协议侧:尺寸消息再带一个「本页是否视口耦合」的布尔(桥内部已有逐元素耦合采样与排除耦合后的边界),拟合直接按它判定,不必用两个数字相等去猜——属桥与协议的独立变更,本 PR 不做。 - 验证:新增 `tests/runPreviewFullscreen.test.tsx`(补出 jsdom 缺失的 Fullscreen API:按钮住在画面那一格里、点击 → `requestFullscreen` → 退出全屏,以及宿主没有该 API 时不渲染);`tests/localGamePreviewFrame.test.ts` 抽出 `renderFittedFrame` 夹具并补上面两条用例;AGC 子集补「信息栏有内容自动展开 / 手动收起 / 再展开」,并把「没有内容时运行页仍渲染两张卡片」的旧断言改成整栏不渲染(`数值微调面板` 这条已随删除面消失的 label 断言同步删掉,避免恒真)。`apps/ai-game-creator-shell:check:web` 全量通过(`tsc` + 1812 项,合并上游退役提交后的口径)、编码检查与 `git diff --check` 通过;并用真实 Chromium(挂同一份组件 + 客户端真实注入的尺寸桥脚本,`fullbleed` 与 `fixed` 两种游戏页)冒烟:右下角按钮只把画面那一格送进全屏且可退出、退出后画布缩回容器尺寸、选中资源后信息栏自动展开(190px)、收起后画面变高(26px→636px)、再展开恢复。 ## 2026-09-23 自绘标题栏是窗口边框:弹层从它下方开始,焦点陷阱放行它 - 背景:AGC 打开任意一个 `ThemedModal` 弹窗(发布面板、发布进度、资源预览、账本、错误报告等)后,右上角「最小化 / 最大化 / 关闭」点击没有任何反应,标题栏拖拽也不能移动窗口;关掉弹窗立刻恢复。原因是标题栏在模态之外,而 `focus-trap-react` 在 document 捕获阶段监听 `mousedown`/`touchstart`/`click`,模态外的点击被 `preventDefault()` 且 `click` 直接 `stopImmediatePropagation()` —— React 的监听在更内层,事件到不了它,所以表现是「点了没反应」而不是报错。另有 `.app-update-overlay` 用 `inset: 0` 真的把标题栏盖住了。 - 决策:把自绘标题栏定为**窗口边框**,不属于弹层内容:① portal 到 body 的全屏弹层一律 `top: var(--window-chrome-height)`,禁止用 `inset: 0` 盖住标题栏;② `ThemedModal` 的焦点陷阱用 `allowOutsideClick` 只放行落在 `[data-window-chrome-bar]` 内的目标,工作区内容的点击继续被拦住;③ `WindowChrome` 的标题栏加 `data-window-chrome-bar` 标记,作为这条约定的唯一契约点。 - 影响范围:`apps/ai-game-creator-shell/src/components/modal/ThemedModal.tsx`、`apps/ai-game-creator-shell/src/components/WindowChrome.tsx`、`apps/ai-game-creator-shell/src/styles.css`(`:root` 注释、`.app-update-overlay`、`.game-publish-progress-overlay`)。 - 验证方式:`tests/themedModal.test.tsx`(标题栏点击放行、工作区点击仍被拦)、`tests/WindowChrome.test.tsx`(弹窗打开时三个窗口按钮仍调用原生窗口 API)、`tests/windowChromeOverlayContract.test.ts`(7 个全屏弹层都从标题栏下方开始)、`tests/gamePublishFeedback.test.tsx` 与 appSurface(208 passed);两处新增用例都做过「去掉修复即失败」的反向确认。`npm run --workspace apps/ai-game-creator-shell typecheck`、eslint、`npm run check:encoding`、`git diff --check` 通过。 ## 2026-09-23 游戏发行包上限提升到 200 MiB(反代放行量与发行缓存同步) - 背景:游戏广场发行包上限原为 100 MiB(`module-game-distribution` 的 `MAX_PACKAGE_BYTES` 与网页端 `GAME_PACKAGE_MAX_BYTES`),而 Nginx 三份模板与 Pingora 网关的通用 `/api` 放行量是 64 MiB。上限只改一层没有意义:包体超过 100 MiB 时先在反代层被 413,`api-server` 的 ZIP 校验根本不会执行。 - 决策:发行包上限 100 MiB → 200 MiB;展开总量 250 MiB → 500 MiB(保持 2.5 倍余量);单文件 64 MiB、最多 10,000 个文件、展开/压缩比 100 三条内容规则不变;发行包路由请求体上限继续从包上限派生(200 MiB + 1 KiB)。反代放行量统一放宽到 210 MiB:`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf` 使用 `client_max_body_size 210m`,Pingora `DEFAULT_MAX_API_BODY_BYTES` 改为 `220200960` 并同步 `deploy/pingora/pingora-gateway.env.example`。发行静态资源进程内缓存字节预算 200 MiB → 256 MiB,让 200 MiB 档发行包仍能进缓存、且不独占整份预算。 - 边界:包内单个文件仍不得超过 64 MiB;线上 Pingora 环境文件若仍写 `67108864`,必须在重启网关前同步改值,否则发行包 PUT 会在网关层被 413。AGC 一键发布经 `@tauri-apps/plugin-http` 传整包字节,实际可发布体积还受该传输方式限制,200 MiB 档的客户端容量需要单独验证。(2026-09-24 更正:一键发布已改为 Rust 侧分片续传,不再经渲染层的 `@tauri-apps/plugin-http`;渲染层 HTTP 权限与插件一并退役,200 MiB 档的客户端容量仍需单独验证。) - 影响范围:`server-rs/crates/module-game-distribution/src/package.rs`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、`server-rs/crates/pingora-gateway/src/main.rs`、`src/components/game-distribution/gameZipPackage.ts`、`deploy/{nginx,container,pingora}`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md`。 - 验证方式:`cargo test -p module-game-distribution`(13 passed,其中 `accepts_package_above_the_previous_hundred_mib_limit` 用两个 50 MiB 存储型条目构造 100 MiB 出头的包;把上限临时改回 100 MiB 时该用例确实失败,证明它能守住新上限)、`cargo test -p api-server game_distribution`(20 passed,含新增的请求体上限覆盖包上限断言)、`cargo test -p pingora-gateway`(38 passed,含 `matches_nginx_route_parity_matrix`)、`npx vitest run src/components/game-distribution`(46 passed)、`cargo fmt --all -- --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过。`npm run check:pingora-route-parity` 仍在 dev-http / 容器模板缺少 `/games` 等 SPA 路由处失败,改动前同样失败,与本次口径无关。200 MiB 档真实栈容量证据(上传耗时、api-server 峰值内存、超限 413 口径)尚未复跑,发布前需按阶段 D 脚本重跑一轮。 ## 2026-09-23 引用名不允许空白:素材 / Skill / 附件共用 `normalizeMentionName` - 背景:自动评审发现 `buildContentFromTextTokens` 在前缀重叠时会多插一枚芯片——素材显示名 `hero` 与 `hero v2` 并存时,粘贴 `看 @hero v2 这一版` 得到 `[chip hero]` + `[chip hero-v2]`(短名先按 index 平局抢位,长名成了补到末尾的孤儿)。根因不是匹配算法,而是**引用名自己带空白**:token 的边界规则是「前后为空白或行首行尾」,`@hero␠` 在 `@hero v2` 内部也算一次合法命中。 - 决策:把不变量前移到引用名——`@显示名` / `$名称` / `@附件名` 的名字内部不允许空白,统一经共享 `normalizeMentionName(value)`(内部空白折成 `-`、裁掉首尾)处理。落点是名字的产生处:`resourceDisplayName()`、Skill 目录读入与 `toReference` / `mentionToken`、附件导入映射与 `toReference` / `mentionToken`,外加两个 token 投影(`chatReferenceMentionToken`、`directCodexContentToPromptText`)——token 层幂等再折一次,「token 里没有空白」就是不变量本身的性质,不依赖上游数据干净。 - 决策(不兜底):引用名假定非空,不做 `resourceId` 之类的兜底;`resourceDisplayName` 原来的 `|| asset.id` 一并去掉。 - 校正(2026-09-23,评审项):`resourceDisplayName` 的空名字兜底不能一并去掉——整名就是扩展名时(`.env` / `.gitignore`)去掉扩展名得到空串,显示名成了空串,token 退化成只有触发符的裸 `@`(候选菜单里是空芯片,粘贴解析还会认领正文里任何一处裸 `@`)。改为词干为空时回退 `asset.id`(仍过 `normalizeMentionName`);`normalizeMentionName` 自己没有兜底、只做归一化这条不变。 - 落点兜底(2026-09-23,评审项):part 落进正文的兜底链统一为 `mentionTokenOrText`(provider 的 `mentionToken`,拿不到就退 `contentPartText` 的通用文本形态,即 `@resourceId` / `$名称` / `@附件名` / `@区域标签`)。粘贴插入、润色回写的候选扫描与整根替换共用它,删掉两处静默丢弃路径(provider 答不出 token 的 part 在润色翻译里消失;整根替换时解析不出引用的 part 被吃掉)。副作用是恢复出来的初始草稿里已解析不出的引用落成 `@resourceId` 文本而不是消失——宁可留文本,也不让内容凭空少一段。 - 原因:不改反解析是因为粘贴解析与润色回包共用 `buildContentFromTextTokens`,改匹配算法要冒回归润色的风险;而「名字里带空白的 token」本来就无法手敲(候选触发器 `allowWhitespace: false`,空格处菜单就关),显示口径与输入口径早就不一致。折成 `-` 之后 token 自带边界:`@hero` 不会命中 `@hero-v2`(后一个字符是 `-`,不是空白),前缀重叠不可能再发生。 - 影响范围:`apps/ai-game-creator-shell/src/features/project-workspace/{resourceReferences.ts,reference-source/{skillReferenceProvider.ts,attachmentReferenceProvider.ts}}`、`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directCodexTurnAttachments.ts`、`apps/ai-game-creator-shell/tests/{resourceReferences.test.ts,referenceSourceProviders.test.ts}`、`CONTEXT.md`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`、`docs/project-memory/plans/【实施计划】引用粘贴解析-2026-09-22.md`。 - 代价(已接受):归一化可能撞名(`hero v2` 与 `hero-v2` 同名),走既有的「同名多候选一律按文本保留」——不认错,但两者都成不了芯片;改动前生成的旧文本(历史回合 prompt、旧气泡)里的 `@hero v2` 不再解析,重试 / 润色回填时那条引用会退化成末尾孤儿(内容不丢、位置可能不对)。 - 验证方式:`normalizeMentionName`、`resourceDisplayName`、`chatReferenceMentionToken` 的口径单测;「空白折 `-` 后 token 自带边界、前缀重叠只剩正确芯片」的回归用例;Skill 目录名带空白与附件名带空白的 provider 用例;把 `normalizeMentionName` 变异成恒等函数后以上新增用例全部变红。(2026-09-24 独立复核:把 `normalizeMentionName` 改成 `return value` 后,`resourceReferences.test.ts` + `referenceSourceProviders.test.ts` + `resourceReferenceInput.test.tsx` 三个文件共 5 条失败、74 条通过,声明成立。)另跑受影响用例、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 ## 2026-09-22 引用粘贴解析:只认显示口径的 token,宁可不成芯片也不能认错 - 背景:引用输入区的 `@` / `$` 只由 `LexicalTypeaheadMenuPlugin` 的逐字敲击触发,粘贴走 Lexical 默认路径(`text/plain` → 纯文本),所以从用户消息气泡复制回来的 `@显示名` / `$名称` 粘进来就是死文本;同时 chip 的 `text/plain` 是占位符,复制出去再粘回来必然丢引用(气泡显示文本才是完整 token 的形态)。 - 决策(口径):新增粘贴侧唯一反解析 `buildContentFromPastedText(text, references)`(`apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts`),候选由宿主注入的 provider 枚举,token 就是 `chatReferenceMentionToken`——与出站显示逐字同一个字符串,所以**不做任何兼容别名**:不认 `@hero.png`、`resourceId`、大小写变体、全角 `@`;边界仍是「行首 / 行尾或空白」(显示侧 token 前后补空白,两端自洽)。 - 决策(宁可不成芯片也不能认错):同一个 token 对应多条引用身份(同名素材)时一律按文本保留;未命中的 token 静默保留、不提示、不猜文件名或路径;附件与运行画面区域不参与(静默 provider 没有候选,`@附件名` / `@区域标签` 按文本保留)。 - 决策(provider 契约按「模糊 / 精确」分两个口):上一条里的 `match(query)` 改名 `fuzzyLookup(query)`(名字写明它是包含匹配 + 截断的模糊菜单查询),`candidates()` 改名 `lookup()`(精确查找用的、就绪的全量候选,不模糊不截断),两者共用同一份候选来源。粘贴解析只用 `lookup()` 此刻就绪的候选:不等待、不补读,也不为了解析去提前读盘;Skill 目录仍是「用户第一次敲出 `$` 才读」,冷启动时粘贴 `$名称` 保持字面文本,敲过一次 `$` 后即可重建芯片。曾一度加过的 `onPasteText` 补读钩子已删除——它既不改变本次粘贴的结果,又让输入区反过来关心 provider 的触发符。 - 决策(接管范围):输入区在 `COMMAND_PRIORITY_CRITICAL` 注册 `PASTE_COMMAND`,**只在真的解析出引用时**接管(同 namespace 的 `application/x-lexical-editor` 负载、无 token 纯文本、图片文件一律 `return false` 走默认导入);接管时一次 `editor.update(..., { tag: PASTE_TAG })` 内按选区插入,所以一次 Ctrl+Z 整体回退,token 之外逐字保留。 - 原因:粘贴是用户此刻的编辑,事后回头改写他的输入(例如清单到齐后再把文本改成芯片)等于前端替用户重写内容;而任何「多候选取其一」「按文件名猜资源」的启发式都会制造看不出错的错引用。 - 影响范围:`apps/ai-game-creator-shell/src/features/project-workspace/{resourceReferences.ts,ResourceReferenceInput.tsx,reference-source/{types.ts,resourceReferenceProvider.ts,skillReferenceProvider.ts}}`、`apps/ai-game-creator-shell/tests/{resourceReferences.test.ts,referenceSourceProviders.test.ts,resourceReferenceInput.test.tsx}`、`CONTEXT.md`、`docs/adr/【ADR】引用候选由宿主注入-2026-09-22.md`(修订节)、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`、本文件。 - 未纳入本次:斜杠命令 `/` 解析、拖拽文本(drop)、附件 / 运行画面区域 / 文件路径 / URL / 剪贴板图片、复制侧 `text/plain` 形态调整、扩展安装卸载后的目录即时失效。 - 验证方式:`buildContentFromPastedText` 规则矩阵单测(含「显示文本再粘贴回来得到同一份 content」这条逆运算)、provider 的 `fuzzyLookup` / `lookup` 用例、输入区集成用例(真 Lexical `paste` 事件 → 芯片、未命中等价于默认粘贴、Skill 冷启动保持字面且敲过 `$` 后可解析);另跑 `npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。 ## 2026-09-23 最近项目检查失败不进终态 - 背景:最近项目列表把一次性的目录检查失败当成终态——5s 超时被吞成 `null`,增量投影又把上一轮的 `null` 原样搬进下一轮,且没有重试或重查入口。AGC 一次 IPC 停顿之后,整张列表会永久停在「检查失败 + 待识别」,首页「最近项目」同时因 `canOpen` 过滤变空,只能重启客户端恢复(issue #490)。 - 决策:单次检查失败先就地重试一次(300ms);失败结果不进新投影(失败项回到「检查中」并重新检查);一轮结束仍有**可重试**失败时按 15s / 45s / 120s 重跑整张列表,重跑上限 3 次,失败集合变化或整轮无失败即重置预算;重命名后的单条刷新复用同一套重试与有界重查。 - 提权边界:Windows ACL 自动提权类失败(`DACL`、`权限`、`error 5`、`安全对象不属于当前用户`、`特权`、`1300`、`AGC ACL 提权修复未成功`)判定为不可重试——重试等于在用户刚点「否」后再弹一次 UAC(提权闸门只存在于单次 invoke 内,进程级没有冷却记忆)。这类项目在用户再次主动打开/新建项目或重命名刷新之前不再自动重试,也不驱动整表重查。 - 验证:`apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx` 覆盖「单次失败就地重试」「失败不跨轮保留」「提权类失败不重试」(前两者在改前代码上必挂);`tests/appSurface/home.suite.ts` 的失败态改为等待最终状态;退避重查用一次性脚本验证持续失败后 15s 自动恢复(脚本未入库)。 ## 2026-09-22 筛选控件选中态:类名收敛到 helper,视觉收敛到「实心填充 + 反白文字」 - 背景:`platform-category-chip` 的类名字符串此前在三个宿主各抄一份(共享筛选条 `PlatformResourceFilterBar`、资源画布筛选浮层 `ResourceFilterPanel`、模板库筛选区),「选中的筛选胶囊长什么样」随时会各自漂移;更严重的是选中态本身只用了 `--platform-cool-*` 这组低透明度暖色,实测选中/未选底色对比只有 1.09:1,用户反馈「选中和没选中的颜色看不出差别」。 - 决策:① 类名口径收敛到 `packages/shared/src/components/platformCategoryChipModel.ts` 的 `getPlatformCategoryChipClassName(active)`(从 `@genarrative/shared/components` 导出),三处宿主统一改调它;② 选中态改为**实心品牌填充 + 反白文字**,语义色收在新的 `--platform-chip-idle-fill` / `--platform-chip-active-{fill,border,text,shadow}`(浅色皮肤深暖填充、深色皮肤亮靛蓝填充 + 深文字),`PlatformSegmentedTabs` 新增 `tone="accent"` 与 chip 共用这套色;③ `src/index.css`(平台 Web/平台 H5)里那份重复的 `--active` 规则同步改口径,避免覆盖共享样式把 Web 端打回旧样子。 - 状态阶梯(同一份口径,三个状态不许互相冒充):静止 = 浅底 + 中性描边 + 常规文字;悬停 = 中性加描边 + 极淡暖底 + 深色文字(**品牌色只能属于「已选中」**,悬停用品牌色会让未选中的 chip 看起来已选中);按下 = 再压一层;选中 = 实心填充 + 反白文字,是唯一的强状态。运行时分段的 `accent` 未选中悬停同理(淡暖底 + 深文字)。 - 焦点态同批收口:`--platform-input-focus-ring` 从 15% 透明度改成实心色(合成后 1.17:1 的环等于没有),筛选 chip / 分段项 / 排序按钮的焦点提示改用 `outline: 2px solid ; outline-offset: 2px`——不再用 `box-shadow` 画环,避免被选中态自己的投影盖掉。 - 原因:二元状态必须靠**填充/明度**表达而不是色相微调;颜色只允许在 `packages/shared/src/theme.css` 的语义变量里出现,组件不再自己写颜色字面量。 - 影响范围:`packages/shared/src/theme.css`、`packages/shared/src/components/{platformCategoryChipModel.ts,styles.css,PlatformSegmentedTabs.tsx,PlatformResourceFilterBar.tsx,index.ts}`、`src/index.css`、`apps/ai-game-creator-shell/src/view/{project-development/ResourceFilterPanel.tsx,template-library/index.tsx}`。 - 验证方式:`apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts` 按 WCAG 公式断言两套皮肤都满足「选中文字 ≥ 4.5:1(渐变两端)」且「选中填充 vs 未选底色 ≥ 3:1」;`platformCategoryChipModel.test.ts` 钉住选中类名分支;`PlatformResourceFilterBar.test.tsx` / `resourceFilterPanel.test.tsx` / `src/index.test.ts` 覆盖各宿主。真机 AGC 客户端截图实测两态填充对比 6.0:1、选中文字 4.8–6.0:1。 ## 2026-09-22 退役 AGC 项目对话斜杠命令与终端 swarm chat 入口 - 背景:AGC 项目对话曾把大量能力挂在「聊天输入 `/`」上(`/history`、`/read`、`/help`、`/status`、`/trace`、`/export`、`/preview`、`/remember`、`/brief` 等),无 GUI 的终端 swarm chat 入口 `--swarm-chat` 又自带一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。两套入口都没有现役调用方,撤回成本却持续存在:命令字面量散落在前端命令分支、润色绕过、摘要模块、`swarm_cli` 终端输入解析、构建期门禁条目和文档承诺里,任何新对话形态都要额外维护这套死词汇表。 - 决策:斜杠命令语义与终端 swarm chat 入口整体退役,按「从未存在」处理。应用侧删除 Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`、`chatPromptPolish` 的 `/` 前缀绕过、`chatCommandMetadata` / `chatCommandHelp` / `memoryCommands` 的命令清单与参数解析、只服务退役 Supervisor 摘要面板的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`、草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)、无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` / `get_limited_local_commands`,以及钉住这些字符串的构建期门禁条目与专属测试。终端侧连同入口一并删除:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(命令解析与帮助输出、turn 派发、观察器、报告、专属测试)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务这些命令的 `agent.compact` / `agent.resume` / `agent.run_status` 权限门禁与 `print_runtime_response_stream_status` 打印器,以及只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`);该文件只保留自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at`。真实 E2E 的交互式 CLI 管道、`scripts/agent-swarm-test-chat.mjs`、`agentSwarmTestEntry.test.ts` 与 `agc:test:chat` / `agc:test:chat:manual` / `agc:chat` / `agc:swarm` 等 npm 脚本同步删除。 - 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。(2026-10-02:这些 CLI 控制命令与对应 Tauri IPC 已随自建 Agent Runtime 整体退役,见同日前条与 ADR。) - 影响范围:`apps/ai-game-creator-shell/src/**`(Direct 聊天控制器、润色、`project-summary`、`project-workspace`)、`src-tauri/src/**`(`cli.rs`、`main.rs`、`swarm_cli` 整目录删除、`agent/interaction.rs` 收敛、命令注册、canvas 生成、provider / project 测试)、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/**`、`scripts/check-config.mjs`、root 与 App 的 `package.json` 脚本、`tests/**`,以及 AGC 主实施计划文档、Runtime V1.1 文档与 `CONTEXT.md` 术语。 - 验证方式:`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npm run --workspace apps/ai-game-creator-shell typecheck`(含 `check-config.mjs` 的脚本与门禁一致性)、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`cargo check --tests`(告警消息集与基线一致)、`npm run check:encoding`、`git diff --check`;保留的 e2e 套件为 `supervisor-swarm`、`-transient-retry`、`-final-reply-transient-retry`、`-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。 - 关联文档:[【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../../adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。(2026-09-24:对应里程碑已完成并删除,结论以 ADR 为准。) ## 2026-09-23 AGC 发布资料免费生成与封面泥点生成 - 背景:AGC 发布面板仍展示 ZIP 路径、文件数和体积,同时一句话简介只取创作目标、分类固定为“其他”,封面只能手选;这与发布页应隐藏技术信息、使用创作上下文降低填写成本的目标不一致。 - 决策:发布面板移除发行包技术摘要。简介和分类由 AGC 调用平台内部免费文本模型生成,输入仅限有界、脱敏的项目名称、创作目标、任务状态、素材类型和最近编辑摘要;生成失败保留本地兜底,不扣用户泥点且不阻断发布。分类始终收敛到七类白名单。 - 决策:封面支持基于项目上下文生成,复用现役编辑器图片生成接口和泥点扣费 wrapper;按钮与确认弹窗显示后端运行时定价对应的具体泥点数。服务端返回的 `assetObjectId` 直接作为 `coverAssetId`,禁止生成后再直传导致素材身份分叉。生成失败原因留在发布面板内。 - 决策:发布截图同批并行上传;成功缩略图在鼠标移入时显示“删除|预览”,失败缩略图在图片内显示错误并保留独立删除按钮。单张失败只跳过该张,不阻断同批上传或发布。 - 边界:网页发布表单、游戏分发审核 API、SpacetimeDB schema、外部 `/api/external/v1` OpenAPI 均不变;临时项目上下文不落库、不进聊天记录。 - 验证:AGC 发布面板与发布 service 定向 vitest、api-server 发布资料 parser/route 测试、AGC 与 api-server 类型/编译检查、编码和 diff 检查。 ## 2026-09-23 AGC 发布前先守可运行原型门禁 - 背景:客户端已经显示“首个可运行原型尚未完成,运行视图暂不可用”,但发布入口仍会先执行用户项目的 `build`,导致未完成原型也进入构建并在后续失败。 - 决策:`project.export_package` 的发布专用导出链路先检查可玩入口;没有入口时,只有 `code-prototype` 已完成或存在运行中的预览才允许执行 `build`,否则直接返回“首个可运行原型尚未完成,暂不能发布”。发布阻断反馈使用独立提示弹窗,不写入 Direct 聊天记录;发布过程不再进入聊天确认卡,改为独立全屏进度弹窗;运行中遮罩覆盖整个工作区并阻止交互,失败留在弹窗内,成功后切换到发布资料面板。 - 边界:已有可运行入口仍直接打包;原型已完成但缺构建产物时保留原有自动构建;缺失 `exports/README.md` 仍在导出前自动生成。 - 验证:Rust `publish_export` 4/4、前端发布相关测试 16/16、AGC `tsc`、编码检查和 `git diff --check` 通过。 ## 2026-09-22 Direct 埋点与业务持久化锁隔离 - Direct 采集身份和最新成果编号改由独立纯内存状态保存,初始化时从最终执行账本冻结项目与原 run 身份;成果采集、预览采集上下文和终态成果读取不再争用业务落盘锁。 - 内存锁释放后才投递事件,不等待文件 I/O、后台队列或网络,不新增用户报错。缺少 run 元数据不套用当前用户身份,恢复不补造历史成果;退出仍尽力封存,不增加退出等待。 - 修正后台埋点查询 DTO 的分页注释:按 `(event_time, event_id)` 倒序,入库时间仅限制快照;接口和查询行为不变。 ## 2026-09-22 新项目埋点资格支持有限恢复 - 真实创建成功先登记有界进程内待办,不依赖埋点服务或身份快照;后台服务就绪及后续真实受理可重试初始化资格。资格文件格式、每项目最多一次首次提交和上传合同不变。 - 业务线程仅更新内存和非阻塞投递;资格文件读写和独立锁操作仍在后台,不向用户报错或要求介入。队列满、暂时 I/O 失败和锁竞争保留待办;已有标记、备份或项目身份不符不重新授予资格。 - 内存最多 1024 项、路径与 ID 合计 1 MiB;超限或持久化前退出仍允许漏记,不补旧项目历史,不承诺零丢失。细节与验证见客户端埋点主规范。 ## 2026-09-22 清理无调用方的 GUI 文件写入命令 - 确认 `write_local_project_file` 无现役前端或业务调用方后,删除命令、Tauri 注册及命令检查豁免;移除对应测试片段,保留 checkpoint、UI 保存和记忆写入的既有测试。 - Agent 使用的底层 `write_local_project_file_at` 保留。GUI 人工文件写入不再列为采集入口;不扩充其他文件操作埋点,不修改已有事件数据合同。 ## 2026-09-22 客户端埋点后台按发生时间排序 - 按技术负责人要求,列表改为 `(event_time, event_id)` 倒序,历史补传按发生时间归位。入库时间仍用于固定分页快照,翻页期间新入库的事件在刷新后显示。 - 分页游标改存发生时间;旧游标刷新后重新取得,不迁移持久表、不新增索引、不改变采集与上传行为。 ## 2026-09-21 客户端埋点方案进入团队共享文档 - 本期验收完成:原始需求与已确认口径的 12 类事件入口已核对,同一 writer/session/project/goal 的宿主组件链路通过真实文件、HTTP、checkpoint 与 JSONL 关联验证;最终 51 项 Rust、46 项前端测试和类型/格式/文档检查通过。仅测试辅助模拟创建投递、run 结果和构建产物,不宣称完整 GUI/Provider 端到端验证;未提交或发布。禁止把全部资源操作逐项接线重新当作本期必做范围。 - 最新口径:共 12 类启用事件;策划审批通过后实际进入下一阶段并成功持久化,复用 project_revision_created,revision_id=design::、source=design_agent、revision_source=agent、change_kind=design_document。同会话同目标阶段幂等;不严格校验文档版本/差异,阶段内文件修改不逐次采集,重开不补历史。不新增独立策划进度事件或审批、澄清状态字段。Direct 文件/补丁、UI 成果、预览与保存已有定向验收,基础事件入口已核对,同一宿主组件链路验收已通过。 - 两类 Agent run 元数据随真实受理保存,后台维护项目累计观测重试与双 Agent 当前终态槽位;重放不新建,恢复保留原身份且耗时未知,取消/不确定不伪造失败。Direct 自动认证刷新只确认最后一次原生尝试的有界内存候选,账号代次变化丢弃;缺失不回退旧失败,不为观测增加业务写盘等待。运行结果已通过独立验收,真实付费 Provider/完整 GUI run 尚未 smoke,现有 Direct 合同中断恢复行为未改变。 - Direct 宿主文件写入/正式补丁仅在已知成果内容变化且原事务 revision 成功提交后记成果,原 run 用户归属不变,末尾 projection 不重复记。当前 session 内存关联最新可信成果到 run;缺证据为 null,不据全局 fingerprint 推断作者。真实 writer、bundled patch 执行器和定向测试已通过。 - GUI 人工文件写入命令已于 2026-09-22 清理,不再作为采集入口;完整项目 checkpoint 按真实 checkpoint_id 记保存。UI State 仅 Saved 记成果;手动 Saved/Unchanged 可记保存,自动保存仅 Saved,保存并生成需全操作成功。起点冻结身份,埋点失败不影响业务。63 项 Rust、41 项前端测试及独立验收通过;无完整 GUI 跨层保存 smoke。旧 Runtime 开发/CLI 文件及 UI workflow 不因存在代码就纳入正式 GUI 必需采集。 - 正式 Web preview_ready 由 GUI 用户持续预览和 Direct 临时浏览器预览接入,冻结原身份、版本与实例;异步2秒loopback GET禁代理/重定向,原入口及响应非空、版本/实例仍匹配才记录。实例停止/替换、验证结束或取消后丢弃迟到结果;可访问不等于JS/游戏验证成功。合并后78项Rust、104项前端测试及独立验收通过,未调用真实Provider或跑完整Chrome双端验证。资源操作全面接线计划已撤销;现有采集只表示已观测变化,不代表全部资源操作或项目全部修订。 - 首次提交按本地观测口径每个新项目最多一条:真实受理候选在后台持久消费 `.agent/analytics-goal.json` 资格后投递。资格跨批次清理保留,旧项目不初始化;此前候选丢失时允许后续真实受理消费,使用后者自己的用户与时间,不宣称绝对首次。消费后事件丢失可零条,重放与恢复不补历史;不得为埋点扫描双 Agent 完整历史或阻塞业务写盘。 - 当前合同唯一维护入口为[客户端本地埋点与主站入库契约](../../technical/【技术方案】客户端本地埋点与主站入库契约-2026-09-21.md),原始需求作为仓库内历史来源保存;后续里程碑规范与实施计划放在 `docs/project-memory/plans/`。 - 本地采集阶段已验收明文 JSONL 持久化;当前仍不做加密。一个项目对应一个目标,事件按业务节点采集,5 分钟封存,7 天或 20 MiB 清理;上传失败也受保留上限约束。 - 当前上传实现已完成隔离环境验收,证据见同一主规范第 13 节:每 15 分钟上传匹配当前账号与平台的封存批次,新增一张客户端事件私有表、批次原子入库与幂等确认、成功清理及独立后台明细栏目;失败静默留待下周期重试。真实客户端文件、HTTP、数据库与后台查询已关联同一事件验证,浏览器列表/筛选/详情通过;未部署生产。保持原 12 类事件和原采集边界。埋点接收复用 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL` 的 dev/release 官方站点映射;仅 dev 渠道且 `GENARRATIVE_ENV` 为 development/test/container 时额外接受 loopback origin,无独立埋点环境变量。按数据库、API/后台、客户端顺序发布。 - 已按技术负责人授权开始实施:合同与本地队列、会话窗口与项目接入、策划阶段成果、首次提交及两类 Agent run 已实现并经独立审查;定向测试、生产编译和前序 GUI 启停证据统一见主规范第 12 节。不得宣称完整产品采集已上线。 ## 2026-09-22 引用输入区改为宿主注入引用 provider,选择器面板与输入区分离 - 背景:`ResourceReferenceInput`(`apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx`)同时承担「拿数据」与「编辑数据」:素材以未过滤 manifest 传入后由组件自己派生候选、显示名与「当前版本素材」scope,Skill 候选由组件自己 invoke `list_agc_skill_catalog` 与 `list_client_extensions`(只在用户敲出 `$` 时触发),素材选择面板与缩略图预览 invoke 也住在组件内部。后果是 5 个宿主(DirectProject 聊天、策划输入盒、画布生成面板、资源卡快速编辑、测试夹具)无差别获得 `$` Skill 候选,而只有 DirectProject 回合会把 `agc_skill_reference` 解析成真 Skill(Rust `direct_codex_user_item_to_codex_turn_input`,`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs`),其余宿主只把它退化成字面文本,形成误导入口。 - 决策(注入):输入区只接受宿主注入的 `providers: readonly ReferenceProvider[]`。每种引用一个独立工厂——`createResourceReferenceProvider({ assets })`、`useSkillReferenceProvider()`(Skill 候选是异步的应用级读取,所以它同时是宿主 hook:第一次候选菜单打开时才发起读取——`match` 保持纯函数,懒加载走 `onMenuQueryChange`,由输入区在 effect 里回调;结果到了宿主重渲染,读取不进输入区),附件与运行画面区域为静默 provider(无触发符、无候选);**没有总装 builder**,宿主按需选择性注入。provider 暴露 `trigger` / `match(query)` / `toReference(part)` / `refresh(reference)` / `mentionToken(part)` 与可选的 `onMenuQueryChange(query)`(菜单懒加载的唯一入口,`match` 保持纯函数)与 `isReady()`(数据未到齐时输入区先不把初始草稿落成文本,否则草稿里的引用会被静默丢掉),输入区只按数组顺序取第一个非空回答,不判断引用种类。 - 决策(分离):素材选择面板拆成独立组件 `ResourceReferencePicker`(含一个可复用的 `@` 触发钮 `ResourceReferencePickerAction`),自己拿数据(`assets` / `versions` / `activeVersionId` / `projectPath` 与缩略图预览 invoke),由宿主渲染并把确认结果交给输入区句柄的 `insertReferences`——这是两者之间唯一的接缝。输入区删除 `versions` / `activeVersionId` / `showTriggerButton` / `openPicker` / `skills`,改为收宿主的 `inputActions`(操作排槽位,放触发钮)与 `submitSuppressed`(宿主浮层打开时 Enter 不提交,与候选菜单同一口径);`projectPath` 只保留给输入区自己的润色链路。 - 决策(附件):附件并入 `ChatReference`,编辑器收敛为单一引用节点类型,附件 chip 的 DOM 契约逐字保留;附件在**导入成功后**以芯片插入正文,`status === 'failed'` 或异常一律不插入;控制器不再持有 `attachments` 数组,`ComposerPendingAttachments` 与提交时的附件 parts 拼接一并删除,单次上限 `MAX_CHAT_COMPOSER_ATTACHMENTS = 8` 改为按草稿中的附件芯片数计算(否则上限失效),导入进行中禁止发送。 - 决策(显示口径):`directCodexContentToPromptText` 的引用 token 前后各补一个空白(相邻已是空白或相邻即另一个 token 时不重复),并加 `// TODO we will rewrite this with ref as component later.`;该函数的 `resourceId → 显示名` 反查改由调用方注入,函数自己不再读 manifest。 - 原因:读项目/应用清单是后端副作用,按 AGENTS.md 的边界应留在宿主与后端侧,留在共享表现组件里既越界,又制造了「Skill 在所有输入区可见、只有一条路径可用」的误导。按种类分 provider 让选择性注入成为默认,避免再造一个把全部种类焊死、无法只注入资源或资源 + Skill 的总装层。 - 影响范围:`apps/ai-game-creator-shell/src/features/project-workspace/**`、`apps/ai-game-creator-shell/src/view/project-development/**`(chat composer / controller、planning、index)、`apps/ai-game-creator-shell/tests/**`、`apps/ai-game-creator-shell/src/styles.css`,以及 `docs/【功能说明】AGC聊天素材引用-2026-09-08.md`。 - 顺带清理:`ProjectChatComponentProps.activeVersionId`(`apps/ai-game-creator-shell/src/features/app-shell/model.ts`)已无消费方——它只在策划输入盒的 `@` 面板里生效,而策划输入盒的 `@` 触发钮早在本次重构前就不渲染(`showTriggerButton={false}`),工作台壳自己的那份 `activeVersionId` 仍由 `ProjectDevelopmentView`(游戏运行版本)消费,因此只删「壳 → 聊天」这一段穿不过去的参数。 - 未纳入本次:粘贴解析(未来接入点是 provider 的 `mentionToken` 与既有 `buildContentFromTextTokens`)、`resource-reference-*` CSS 类名重命名(独立机械提交)、扩展变更事件的即时失效。 - 验证方式:定向 `npx vitest run apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx` 与受影响宿主用例、`npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`;行为零变化按「改名后芯片显示名自动刷新、打开面板前重读清单、`@`/`$` 候选与键盘交互、附件上限与失败提示文案」逐条对照核验,唯一例外是已裁决的 Skill 可见性缺陷修复。 ## 2026-09-22 release 每日调度纳入正式 Full Build 并收集结果 - 背景:原 `Genarrative-Scheduled-Release-Trigger` 只处理 AGC Windows/macOS,且对下游使用 `wait: false` 后立即推进 revision;正式服务端 release 仍只能人工触发,客户端构建失败也不会回传调度器。 - 决策:release 调度同时计算 `AGC` 与 `FullBuild` 两条 scope。服务端相关路径变化时以 `DEPLOY_TARGET=release`、`CONFIRM_RELEASE_DEPLOY_AGENT=true` 触发 `Genarrative-Full-Build-And-Deploy`;客户端相关路径变化时保持统一发号并触发 Windows/macOS release。Full Build、Windows、macOS 三路均等待结果并汇总,只有成功的 lane 才推进对应 `.jenkins-last-release-full-revision` / `.jenkins-last-release-agc-revision`,失败 lane 在下一轮单独补发,避免重复部署已成功的服务端版本。`DATABASE_BACKUP_MODE` 默认 `async`,并显式提供 `STDB_API_ROLLOUT_MODE` / approvers 以保留正式维护门禁。 - 原因:正式 release 需要与 dev 小时调度隔离,同时不能继续依赖人工触发服务端;lane-level 成功状态可区分“已成功但另一 lane 失败”的部分发布,避免下一次调度重复发布 Full Build。 - 影响范围:`jenkins/Jenkinsfile.scheduled-release-trigger`、`jenkins/scheduled-release-trigger-job-config.xml`、`scripts/check-production-ops-guardrails.mjs`、开发运维文档与共享开发工作流。 - 验证方式:生产运维门禁检查 Full Build release 参数、双 scope、`wait: true` 结果聚合、三路分支和成功后才写 lane revision;并用 Jenkins 具体构建验证 Full Build/AGC 的 build number 与结果能回传到调度 Job。 ## 2026-09-22 项目快照 `.agent` 全量上传:远端工程包要能还原项目身份与 Agent 历史 - 背景:快照上传此前复用 `should_skip_project_snapshot_path`,把整个 `.agent`(`manifest.json`、`agent.db`、conversations、logs、runtime、checkpoint、workbench、`project.lock`)排除在上传集合之外。后台“项目工程”的「下载完整工程」是按清单逐文件打包的,于是导出的 ZIP 里没有项目身份与对话历史,用户在 AGC 里恢复不出同一个项目。 - 决策:快照同步改用独立口径 `should_skip_project_snapshot_sync_path`(`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`)。它与原口径共用同一份组件与后缀规则(提取为 `PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS` / `PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES`),只额外放行两点:`.agent` 组件本身,以及 `.agent` 内的 Agent 状态数据库后缀 `.db`/`.db-wal`/`.db-shm`(`agent.db` 是项目状态而不是凭据转储)。扫描入口 `src-tauri/src/project_snapshot/scan.rs` 切到新口径。 - 边界:`.agent` 内部的版本库 / 依赖 / 构建目录、凭据目录(`.ssh`、`credentials`、`secrets` 等)、`.env*` 与 `.pem`/`.key`/`.sql` 等敏感后缀继续排除;符号链接与重解析点照旧在扫描阶段跳过;单文件 64 MiB、单次 512 MiB、单项目 2 GiB 上限不变。项目索引、checkpoint、Agent 上下文与 git 检查继续使用 `should_skip_project_snapshot_path`(仍排除整个 `.agent`)——本变更只放开快照同步。 - 代价与取舍:`.agent` 里的会话记录、运行日志与诊断快照会随项目离机并写入该用户自己的私有前缀,这是“可还原”的代价,属于本轮产品决定。服务端不需要改动:快照路径校验只检查路径形状,后台归档按清单逐文件打包,都不含 `.agent` 特判。模板包导入门禁(`IMPORT_FORBIDDEN_SEGMENTS`)与 CLI 模板发布门禁继续拒绝 `.agent`,不会因为这份 ZIP 而放宽。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`、`src-tauri/src/project_snapshot/{scan.rs,tests.rs}`;主规范 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的“2026-09-17 AGC 项目定时快照上传”与“后台工程列表与下载”两节;`docs/project-memory/plans/` 的两份里程碑与实施计划。 - 验证方式:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_snapshot`(24 passed、0 failed、1 ignored),新增用例覆盖 `.agent` 整目录进入候选集、`.agent` 内凭据与 `.env*` 仍被排除、项目索引与 checkpoint 口径不变;另用同一份规则复算 15 个本机 AppData 项目的 487 个 `.agent` 普通文件,0 个仍落在排除集。 - 运行时证据(2026-09-22):真实项目副本(`.agent` 164 文件 / 5.61 MiB,`projectId=gameagent-agentsmoke1`)经真实差异引擎 → 本地 api-server → 真实 OSS `agc-dev`:`uploaded=179`(= 原口径 15 个项目文件 + 164 个 `.agent` 文件)/18,991,590 字节、`synced` 后第二轮 `no-op`;只读 GET 远端 `agc/project-snapshots/v1//gameagent-agentsmoke1/manifest.json` 得 `files=179`(`.agent/**` 164 条)、HEAD 命中 `agent.db` 等对象;后台 download 的 ZIP 含 179 条目(164 条 `.agent/`),抽样文件 SHA-256 与本地一致。注意该 bucket 仍是 `v1/` 键布局:本地 `api-server.exe`(2026-09-21 14:06)早于渠道分区提交 `4951b71d7`(16:57),重建重启后才会写 `v2/{channel}/`。 ## 2026-09-21 合并后两处回归:预览激活回接「运行」入口,聊天 `@` 清单按需重读 - 背景:合并 `origin/master` 后 CI 报两处回归。① `scripts/check-native-shells.mjs` 报 `AI game creator client preview activation drifted: missing await invoke('activate_local_game_preview'`:Supervisor 前端链路退役时(`0224ef7b9` 删 `pendingCommand` 死链那一步)把 `activate_local_game_preview` 的前端调用方一起带走,而这条守卫与 Rust `preview.rs` 的实现都还在。② `resourceTagStatsRefresh` 用例失败:聊天 `@` 选择器的标签统计与候选读 `useDirectProjectManifest` 自己那份清单,资源画布的标签写入既不发 `game-creator-manifest-invalidated`(Rust 只在 agent 驱动的写入上发),壳也不再往聊天推清单快照,于是用户改完标签打开 `@` 仍看到旧标签。 - 决策:① 预览激活按 ADR 由「运行」入口承接:`executeRunLocal` 先调 `activate_local_game_preview`(Rust 侧只核对内存 registry 与项目归属),返回 running 且拿到 loopback 地址就只切客户端运行视图并经 `DirectProjectChatHandle.announce` 说一句 `已切换到客户端运行视图:`,不再重复 `start_local_game_preview`;stopped / 不属于本项目 / 权限位要求确认都当「没有可复用预览」,回落到重新启动。同时把 `activate_local_game_preview` 从 `scripts/check-config.mjs` 的 native-only 白名单删除(该门禁要求 App invoke 与白名单互斥)。② 聊天 `@` 选择器打开时按需重读清单:`DirectProjectComposer` 的两处选择器入口(工具条按钮与附件菜单项)都先触发 `onReferencePickerOpen` 再 `openPicker`,由 `DirectProjectChatView` 用 `useDirectProjectManifest` 的 `refresh` 重读;不新增壳→聊天的清单推送通道,也不改 ADR 里「聊天自己订阅清单」的归属。 - 代价与取舍:编辑器内直接敲 `@` 的候选菜单读同一份清单,本轮没有纳入按需重读(它只列素材名、不显示标签统计)。更根治的做法是让资源写入在 Rust 侧广播 `game-creator-manifest-invalidated`,本轮不做,先按现成的刷新入口收口。 - 影响范围:`apps/ai-game-creator-shell/src/App.tsx`、`src/view/project-development/chat/DirectProjectChatView.tsx`、`.../chat/components/DirectProjectComposer/DirectProjectComposer.tsx`、`scripts/check-config.mjs`;新增 `tests/previewActivation.test.tsx`(3 条:活体预览只切视图不重启、stopped 回落重启、权限确认回落重启;第一条在去掉接线后确实失败)。 - 验证方式:`npx vitest run apps/ai-game-creator-shell/tests`(除 5 个 jsdom `localStorage` 环境失败文件外全绿:`previewActivation` 3/3、`resourceTagStatsRefresh` 2/2、`runVersionSwitchEventSubscription` 2/2、`resourceReferenceInput` 34/34、`appSurface.test.ts` 198 passed / 13 skipped)、`node scripts/check-native-shells.mjs --groups=contract`、`npm run ai-game-creator-shell:typecheck`、`npm run check:encoding`、`git diff --check`、改动文件 prettier / eslint 全过。 ## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存 - 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin,`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。 - 决策:渠道同时决定**安装身份**。默认渠道 `dev` 保持基线 `productName = 陶泥儿`、`identifier = world.genarrative.ai-game-creator`(既有安装目录、卸载项与升级链不断);其它渠道派生 `陶泥儿 <渠道显示名>`(`release` → `陶泥儿 Release`)与 `world.genarrative.ai-game-creator.<渠道>`。身份与更新端点必须在同一个构建期 `--config` 里注入,禁止分别回读默认值。窗口标题、macOS 产物名(`.app` / updater 归档 / DMG 卷名)、首装包选择与 Windows 提权 ACL 的 managed 识别范围同批跟随该身份。 - 边界:不做本地数据迁移或共享——切渠道等于换一个客户端;`release` 与自定义渠道首次以新身份安装,不接管、不迁移既有 `dev` 安装与本地项目,由用户自行决定是否卸载其一。 - 影响范围:新增 `apps/ai-game-creator-shell/scripts/channel-identity.mjs`;`build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs`、`agent-swarm-test-chat.mjs`、`src-tauri/src/{main.rs,windows.rs,config.rs}` 与对应测试;主规范 `docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md`。 - 验证方式:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs release-oss.test.mjs prepare-macos-codex.test.mjs cargo-features.test.mjs`(64/64,新增渠道身份与渠道 DMG 首装选择用例)、`node apps/ai-game-creator-shell/scripts/check-config.mjs`(基线等于默认渠道身份、非默认渠道身份隔离)、`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` 与相似前缀反向断言)、`AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug`(产物字符串实测 `陶泥儿 Release` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0)。真机双渠道安装、并存与各自更新尚未执行,按未验证项记录。 ## 2026-09-21 项目快照按部署渠道分区,后台按渠道查看并按素材查询口径展示用户 - 背景:AGC 项目快照此前统一写在 `agc/project-snapshots/v1/{user}/{project}/`,而开发与正式两套部署共用同一个 bucket(都默认 `agc-dev`)。结果是渠道混在一层前缀里:正式后台会列出开发渠道上传的项目,列表上也看不出项目属于哪个渠道;同时“项目工程”列表只有裸用户 ID,且用“加载更多”逐段追加,翻页与定位都困难。 - 决策(存储):对象键升级为 `agc/project-snapshots/v2/{channel}/{user}/{project}/`。渠道是**部署渠道**,由服务端 `GENARRATIVE_AGC_PROJECT_SNAPSHOT_CHANNEL` 决定(缺省沿用 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL`,当前缺省 `dev`),客户端不上报渠道、也不需要发新版;渠道名必须是小写字母开头的 `[a-z0-9-]{1,32}`,非法值在上传与后台查询处失败关闭(`503`/`400`),不悄悄回落。正式部署必须在 api-server 环境里显式写 `GENARRATIVE_AGC_PROJECT_SNAPSHOT_CHANNEL=release`。 - 决策(后台):新增 `GET /admin/api/project-snapshots/channels` 返回本部署渠道与远端已存在渠道的并集;列表与下载接口都接受 `channel`(缺省本部署渠道),游标里带上渠道并在解码时校验一致,跨渠道复用游标一律 400。页面顶部提供“渠道”选择框,切换渠道回到第 1 页。 - 决策(列表形态):“用户 ID”列改为与“素材查询”同口径的“用户”列——昵称 + 陶泥号 + 用户详情入口,昵称/陶泥号由 api-server 用 `resolve_work_author_by_user_id` 解析(账号不可读时回退占位作者),原始 `userId` 不再直接铺在列里;“加载更多”改为游标分页(每页 20/50/100 + 上一页/下一页 + 当前页),前端按页记录游标链,换令牌或换每页条数从第 1 页重来;翻页失败保留当前页。 - 原因:渠道属于“这套部署服务哪个客户端渠道”,客户端正式包按构建渠道连接不同平台服务(release → 正式站,dev → 开发站),所以服务端是唯一可靠的渠道来源;把渠道放进对象键第一层,既不需要迁移历史对象就能让两套部署互不可见,也让后台的渠道维度天然对齐存储布局。 - 影响范围:`server-rs/crates/platform-oss/src/{lib.rs,project_snapshots.rs,template_library.rs,examples/agc_project_snapshot_live_smoke.rs}`、`server-rs/crates/api-server/src/{config.rs,project_snapshots.rs,admin_project_snapshots.rs,modules/admin.rs}`、`server-rs/crates/shared-contracts/src/admin.rs`、`apps/admin-web/src/{api/adminApiClient.ts,api/adminApiTypes.ts,pages/AdminProjectSnapshotsPage.tsx,pages/AdminProjectSnapshotsPage.test.tsx,styles/admin.css}`、`deploy/env/api-server.env.example` 与本仓运维/技术文档。 - 未迁移的历史对象:`agc/project-snapshots/v1/` 下现存对象(只读核对过的两份历史清单)保留在 OSS,但不再写入、不再进入后台列表;需要取回时按旧前缀在 OSS 侧直接读取,确实要在后台看到时再单独开一个只读兼容视图。 - 验证方式:`cargo test -p platform-oss snapshot` 7 passed(渠道校验、键布局、v2 根渠道枚举、v1 历史键仍必须私有);`cargo test -p api-server project_snapshot` 18 passed/1 ignored(渠道失败关闭、游标跨渠道拒绝、用户昵称/陶泥号解析、归档与配额回归);`cargo test -p api-server protected_route_matrix`、`route_contract` 通过(新路由纳入后台鉴权矩阵);`npm run admin-web:typecheck` 与 `npx vitest run apps/admin-web/src` 202 passed。 ## 2026-09-21 Godot 模板入库与按模板建项的 Godot 分流 - 背景:模板库此前只有网页(html)、Cocos 与 Unity 占位,`runtime` 白名单早就接受 `godot`,但既没有 Godot 模板,也没有按模板建项的 Godot 分流——Godot 模板即使上传,建项也会落到 Web 分支,写出 `game/index.html` 占位入口并让 `godotProjectRoot` 为空。 - 决策(模板内容):新增四个仓库内手写的 Godot 4.7 模板 `godot-empty-2d`、`godot-empty-3d`、`godot-hello-world`、`godot-platformer-2d`,`entry` 统一为 `project.godot`。模板只用内置 `ui_*` 输入动作、GL Compatibility 渲染,并用 `Polygon2D` / `BoxMesh` 搭可视骨架,不引入外部贴图或音频二进制;不打包 `.godot/` 缓存与导出产物。 - 决策(建项分流):`create_project_from_installed_template_at` 在复制模板后按工程文件分流——Cocos 更新自身身份后走 Cocos 导入,Godot 先改写 `project.godot` 里 `[application]` 段的 `config/name`(只改这一行,其余字节逐字保留)再走既有 Godot 导入,写入 `godotProjectRoot: "."`,其余继续走 `init_local_game_project_at`。分流靠工程文件识别,不新增只读 `entry` 或 `runtime` 字段的契约。 - 原因:Godot 工程身份是 `project.godot` 所在目录,与 Cocos 的 `package.json` 身份同一类问题;复用既有导入流程能同时拿到相对根记录、`.agent` 初始化与「不生成 Web 占位入口」这三条既有保证,并且与 Cocos 分支保持对称。 - 影响范围:`apps/ai-game-creator-shell/template-library/v1/godot-*`(新增模板源)、`apps/ai-game-creator-shell/src-tauri/src/template_library.rs`、`.../src/project/manifest.rs`(`apply_godot_project_display_name`)、`.../src/project/manifest/import_tests.rs`、`docs/【模板规范】AGC模板包组织指南-2026-09-21.md`、`docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md`。 - 验证方式:本机 Godot `4.7.2.stable` 对四个模板逐一做「主场景实例化 + 3 帧 + GDScript `--check-only`」,平台跳跃模板另做真实物理试玩(落地 y=627.99、1 秒右移 320px、跳上平台 y=515.93、三枚金币全收集);Rust 定向回归 `import_tests::rewrites_only_the_godot_display_name_line`、`import_tests::keeps_a_godot_project_without_a_display_name_line_untouched`、`template_library::tests::godot_template_creates_native_project_with_relative_root_and_display_name` 与既有 Cocos/Web 建项回归;发布走 `--only godot-*` 定向合并。 ## 2026-09-21 Godot 工作区发现放宽与内置插件行去掉手动启动 - 背景:2026-08-10 “发现与歧义决策”把「一层多命中」和「`project.godot` 必须是普通文件」两条定为失败关闭。这两种布局都会让整个工作区被判成「没有 Godot 工程」,而 `PluginHost::list` 只按发现结果过滤,结果是 Godot 内置插件在设置→扩展 里直接消失,用户看不到任何可诊断入口。 - 决策(发现):根目录命中仍然优先;根未命中时一层直接子目录多命中改为按目录名排序取第一个,`godotProjectRoot` 记录该相对目录名,结果确定且可复现。`project.godot` 允许是符号链接 / Windows reparse point / 硬链接,判据改为「链接目标解析后是文件」,目录与悬空链接仍不算命中。工作区根本身是链接、候选子目录是链接、二层及更深不递归这三条边界不变。 - 决策(界面):设置→扩展 的内置插件行去掉手动「启动 / 停止」按钮。启动本来就由项目切换时的 `startAvailableAgcEditorPlugins` 自动完成,手动按钮只是第二个可绕过入口;停止改走该行的启用/禁用开关,`set_agc_plugin_enabled(false)` 会停止插件并断开编辑器连接。导入扩展行的启动按钮保留,因为导入扩展没有自动启动路径。 - 原因:Godot 插件列表是按项目过滤的,发现失败等于功能静默消失;把不确定性收敛成一个确定的排序选择,比让用户面对空列表更好。链接放宽只作用于只读的工程标记文件,受管描述文件、运行缓存与项目目录的链接拒绝规则不动。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs`、`.../src/commands.rs`(错误文案)、`.../src/project/manifest/import_tests.rs`、`.../src/tests/project.rs`、`apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` §3.8、`docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 - 验证方式:`cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- project::manifest::import_tests::` 与 `-- tests::project::`;前端 `agc:typecheck` 与 `runtime-settings` / `pluginHost` 套件。 ## 2026-09-21 模板正文目录门禁:CLI 打包与后台上传同一份段名单 - 背景:模板包组织指南把 `.agent/`、`.git/`、`node_modules/`、根目录 `dist/` 等列为「不要放进 ZIP」,但两条发布路径此前只校验路径安全与 `entry` 是否存在,放进去的东西会跟着建到用户项目里(模板自带 `.agent/` 会让新项目继承一个陌生身份)。这条约定只靠作者自觉。 - 决策:升级成机器门禁,CLI(`readProjectFiles`)与后台上传(`validate_import_archive`)用同一份段名单与同一句文案——任意层级拒绝 `.agent` / `.git` / `.svn` / `node_modules`,根目录拒绝 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode`。同名目录段只在根目录受限:正文内 `game/dist/**` 是模板自身内容;`.gitignore` 不等于 `.git`,Cocos 模板的 `.creator` / `.gitignore` 不受影响。 - 原因:两类目录性质不同。`.agent/`、`.git/`、`.svn/`、`node_modules/` 放哪一层都是错的,进包即污染用户项目;而 `dist` / `library` / `temp` 这类只说明「作者把编辑器缓存或构建产物当成了模板内容」,出现在根目录才是信号,全面禁止会误伤工程内的正常同名目录。 - 影响范围:`scripts/agc-template-library-publish.mjs` 及其测试、`server-rs/crates/api-server/src/admin_templates.rs`、`docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md`、`docs/【模板规范】AGC模板包组织指南-2026-09-21.md`、`docs/project-memory/plans/【里程碑】后台模板上传-2026-09-21.md`。 - 验证方式:`node --test scripts/agc-template-library-publish.test.mjs`(28 项,新增 1 项:13 条拒绝用例 + `game/dist/**` 与 `game/.gitignore` 放行断言);`cargo test --locked -p api-server admin_templates`(8 项,新增 `template_import_archive_rejects_identity_and_build_directories`);仓库现有 9 个模板源无一条命中,本地重打包的 ZIP 摘要与线上清单 `zipSha256` 逐条一致;`cargo fmt --check`、`check:encoding`、`check:doc-index`、`git diff --check` 通过。 ## 2026-09-21 后台模板上传:成品 ZIP + 每模板封面,批量全有或全无 - 背景:模板发布此前只有本地 CLI(源目录 + 确定性打包 + `--only`),后台上传需要一条不依赖本地仓库的通道,并支持一次提交多个模板。 - 决策(输入形态):后台上传**成品 ZIP**(库内字节原样发布,不重新打包),每个模板必须同时给一张 PNG/JPEG/WebP 封面(与 CLI 源布局 `v1//cover.*` 一致),`id/title/templateVersion/runtime/entry` 由页面逐行确认;简介、标签与引擎上传后用编辑接口补齐。服务端按字节嗅探封面格式,不信任 multipart 声明的 content-type。 - 决策(批量语义):一批 = 一把发布锁 + 一次清单提交,**全有或全无**;任一模板的 manifest/归档/封面不合法都在写入前整批拒绝并逐项给出原因。单批上限 20 个模板、单包 64 MiB、单封面 5 MiB、请求体 200 MiB。 - 决策(校验与安全):归档只校验不落盘——合法 zip、无符号链接、无绝对路径 / `..` / 盘符条目、必须包含声明的 `entry`、条目数 ≤ 4096 且解压后 ≤ 512 MiB。 - 决策(版本与保留):新 ID 默认上架;已存在 ID 就地更新并保留 `enabled` 分组、其它条目与未知扩展字段;同一 ID 同一 `templateVersion` 的 ZIP 字节不同时拒绝并要求递增版本,字节一致时按内容复用(`reusedObjects`)。 - 决策(存储契约):`platform-oss` 的 `put_immutable` 为 `application/zip` 单独放宽到 64 MiB(此前只有 json/图片的 5 MiB),图片与元数据上限不变。 - 影响范围:`server-rs/crates/{shared-contracts/api-server/module-assets/platform-oss}`、`apps/admin-web/src/{api,pages,styles}`、模板库技术方案与 `docs/project-memory/plans/【里程碑】后台模板上传-2026-09-21.md`。 - 验证方式:`cargo test`(module-assets 11、platform-oss 12、api-server 定向 4)、`npm run admin-web:typecheck`、后台页面与模型 40 项 vitest、`check:encoding`、`check:doc-index`、`git diff --check`;真实 dev bucket 写入验证需用户显式确认。 ## 2026-09-21 模板库线上产物对齐仓库源:递增版本重发 + 说明文档纳入发布 - 背景:`agc-dev` 上的 `templates/` 产物停在 2026-09-17 发布的那一版,仓库源在那之后改过(`5e4ff54a9` 删掉模板内嵌 `package-lock.json`、`game.js` / `main.js` 等内容调整),9 个模板里 7 个的 ZIP 与线上不一致;dry-run 被「同一 `templateVersion` 的 ZIP 不得变」门禁拒绝,发布器因此无法把仓库状态发上去。 - 决策:按门禁要求为内容已变的 7 个模板递增 `templateVersion` 到 `0.1.1`(`blank-2d-canvas`、`blank-web` 内容未变,保持 `0.1.0`),并完成一次真实发布;对象使用内容寻址键 `v1//sha256/<摘要>/…`,`index.json` 与说明文档在发布锁内最后提交,历史对象不删除。 - 决策(说明文档):`templates/README.md` 不再是手工副本——它由发布脚本从仓库源 `apps/ai-game-creator-shell/template-library/README.md`(≤64 KiB)覆盖写入并回读校验,写在清单之前;改契约只改仓库源。 - 决策(CLI 行为):发布器遇到门禁拒绝时用 `process.exitCode = 1` 正常退出,不再 `process.exit(1)` 让 Node 在 fetch 句柄未关闭时抛 libuv 断言(此前现场只剩 `Assertion failed`,看不到拒绝原因)。 - 影响范围:`apps/ai-game-creator-shell/template-library/v1/*/meta.json`、`apps/ai-game-creator-shell/template-library/README.md`、`scripts/agc-template-library-publish.mjs` 与其测试、本文件与模板库技术方案。 - 验证方式:`node --test scripts/agc-template-library-publish.test.mjs`(27 项,含新增「说明文档随发布覆盖写且写在清单之前」「源目录没有 README 时不写线上文档」);真实发布 28 个对象回读通过;匿名读取线上清单(9 个模板、7 个 `0.1.1`、全部内容寻址键)与 `templates/README.md`(与仓库源逐字节一致);AGC 壳 3 项线上用例(读清单、下载安装、下载并原生建项 Cocos)重跑通过。 ## 2026-09-21 macOS 发布改为只出 arm64 单架构(Intel 暂不支持) - 背景:Mac 发布管线按 `universal-apple-darwin` 构建,但随包 Node 便携运行时只有**单架构官方发行版**(`stage-node-runtime.mjs` 从 `process.execPath` 取材),于是 macOS Job #7~#13 连续失败在「Node 运行时不支持发布目标:universal-apple-darwin」。期间出现过一版「按宿主架构放行」的过渡实现,它能骗过通用包自检(`check-macos-bundle.mjs` 按 `process.arch` 校验),但 Intel 上那份 arm64 侧车不可执行,并且已发布的 dev-mac 0.1.86 就带着这个缺陷。 - 决策:macOS 固定只构建 `aarch64-apple-darwin`,渠道清单只登记 `darwin-aarch64`(不再登记 `darwin-x86_64`,避免把 arm64 产物发给 Intel 客户端);`targetRuntime('universal-apple-darwin')` 保持失败关闭,入口 `build-macos-ci.mjs` 只跑 arm64 隔离 smoke,首装包命名 `<产品名>_<版本>_aarch64.dmg`。 - 原因:要让 Intel 真正可用,必须让发布包按架构各带一份**同版本**运行时(另下载另一架构官方发行版)+ 通用包自检按架构分别校验,这是一条独立且更大的改动;在 DDL 前用「只带宿主架构」糊过去等于把坏包发给 Intel 用户,比暂不支持更糟。单架构同时把构建时间与产物体积减半。 - 验证:`node --test apps/ai-game-creator-shell/scripts/*.test.mjs` 92/92(含 `targetRuntime('universal-apple-darwin')` 必须抛错、入口固定 arm64 目标与 `_aarch64.dmg` 后缀的守卫用例);`npm run check:production-ops`、`check:encoding`、`check:doc-index`、prettier、eslint、`git diff --check` 通过;真实端到端由 Jenkins Mac Job 验证(清单只含 `darwin-aarch64`、DMG 与更新包唯一匹配)。 - 影响范围:`apps/ai-game-creator-shell/scripts/{build-macos-ci.mjs,stage-node-runtime.mjs,prepare-macos-codex.test.mjs}`、`jenkins/Jenkinsfile.ai-game-creator-shell-macos-build`、本仓三份技术/运维文档与共享记忆。未动 Windows 渠道、未动 Rust 侧运行时解析(单架构仍是扁平 `game-runtime/node/`)。 - 恢复 Intel 的路径:先在 staging 支持按架构各带一份同版本运行时并让通用包自检按架构校验,再切回 universal 目标、把 `darwin-x86_64` 键登记回去并补 Intel 真机验收。 - 已知未覆盖:Intel Mac 用户的更新体验(清单缺 `darwin-x86_64` 键,客户端会「无可用更新」,未实测其 UI 文案);Mac 包里仍并列携带两套 Codex 原生依赖(只运行 arm64 切片,可按需瘦身)。 ## 2026-09-21 图集切片上限:客户端结果门从 64 对齐到平台契约的 256 - 背景:现场(项目 `gameagent-6e53c9e8`,2026-09-21 07:54)「AI 生成图标素材」失败:`platform-generation-result-unknown: 异步生成完成结果无法绑定到 operationId:External Editor 旧同步结果的图集切片超过 64 个`。任务账本(`.agent/runtime/asset-generation-tasks/tasks.json`)显示它跑了 99 秒、`assetId` 为空、没有落任何素材;对应的持久化请求(`canvas-generation-requests/manual-canvas-asset-generate/slot-560175669f….json`)是 `sliceMode: connected-components` + `sliceCount: null`(自动切分)。也就是**平台已经生成并切完图了,是客户端在绑定结果这一步把整条结果判失败**,付费产物被丢弃。 - 根因:同一条链路里存在两个不同的切片上限。平台切分是 256(`server-rs/crates/api-server/src/editor_project_icon.rs` 的 `EDITOR_ICON_SPRITESHEET_MAX_SLICES`),Agent 工具 schema 的 `sliceCount` 是 1..256(`agent_native_tools.rs` / `direct_tool_bridge.rs`),持久化产物批次也是 1..256,公开契约(`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`)写的更是「最多 256 个输出」;唯独客户端**两处结果绑定门**还是 `> 64` 就拒(`agent/generation/external_generation_state.rs` 与 `agent/generation/canvas_generation.rs`,由 `602723ea0` 于 2026-08-03 引入)。自动切分落在这个窗口里(65~256 片)时,客户端比平台更严,于是把合法产出整条丢掉。 - 决策:两处门统一到 `PLATFORM_ART_SPRITESHEET_MAX_SLICES = 256`,并抽成同一条判据 `platform_art_spritesheet_slice_count_exceeds_limit` 与同一句拒绝文案 `platform_art_spritesheet_slice_limit_error`(数字由常量插值,不再手写)。注释里点名三处同值权威(平台切分常量、工具 schema `sliceCount`、公开契约),客户端不得比平台更严。 - 原因:客户端这两处门的作用是「防止把不可信/超预算的结果写进本地」,不是产品上限;真正的产品上限属于平台切分契约。两处各写一个字面量就会再次漂移,所以值只留一份、判据只留一条。 - 验证:新增 `canvas_generation_tests::spritesheet_slice_limit_matches_platform_and_tool_contract`(上限值、边界判据与文案)与 `external_generation_state_tests::legacy_result_accepts_slice_counts_up_to_platform_limit_and_rejects_beyond`(64/65/256 片必须能持久化且切片一条不少、257 片必须按同一句文案拒绝),两条都用**变异验证**确认过:把常量改回 64,回归用例立刻变红。(2026-09-24 独立复核:把 `PLATFORM_ART_SPRITESHEET_MAX_SLICES` 改成 64 后,`spritesheet_slice_limit_matches_platform_and_tool_contract` 报 `left: 64 / right: 256`、`legacy_result_accepts_slice_counts_up_to_platform_limit_and_rejects_beyond` 同样变红;改回 256 后 `cargo test -- spritesheet` 与那条 legacy 用例各自 exit 0,`cargo fmt -- --check` 通过。)定向执行 `cargo test -- spritesheet`(22 passed)、`cargo test -- external_generation_state_tests::`(10 passed)与两条新用例;`cargo fmt --check` 干净。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/generation/{canvas_generation.rs,external_generation_state.rs}`(+用例)。未动平台切分、OpenAPI、数据库或前端。 - 已知未覆盖:真实客户端复验(重新生成一次图标素材)与远程 CI 未跑;256 片时的累计下载/像素预算未实测——平台自己的总像素上限是 2048×2048,客户端预算是 4096²,按切片是整图互不重叠子矩形推算不会先撞预算,且真撞了也只是给出明确错误而不是损坏数据。 ## 2026-09-21 本批自查(PR #441):三处修正 - 背景:推 PR 后按「局部到整体」自查这一批(三需求 + 验收修正),查出三条:①拖动到对话的落点在 `pointermove` 上每帧都 `setState` 一个新对象;②替换面板相对 **stage** 写死 `top: 8.5rem`(与刚修的任务开关同一类隐患:工具条换行会压上去),且它和「生成任务」面板抢画布右上角同一个位置;③替换面板不显示「在替换哪张源素材」,而非模态化之后那点线索(画布上的源素材光环)会被一次空白点击清掉。 - 决策①:落点状态改为**逐值比较**(`sameResourceCardReferenceDrop`:条数 + 对话栏矩形四值),只有真的变了才落 state——指针在对话栏上移动不再每帧重渲染整个工作台。 - 决策②:面板从 stage 顶层移进**画布容器** `.game-resource-book-manager`(它本身就是 `position: relative`,与左下角工具栏、右下角 Dock 同一套锚定口径),`top: 3.2rem` 排在任务开关下方;并在打开替换会话时**收起「生成任务」面板**——画布右上角同一时刻只留一块浮层(任务照旧在账本里推进,收起只影响这个视图)。 - 决策③:面板上方补一行「替换源素材:<显示名>」(投影显示名优先、manifest 资产名回落),源身份不再只靠画布光环。 - 验证:`resourceVersionReplacement.test.tsx` 20 passed(新增「面板锚在画布容器里、写明源素材、并与任务面板互斥」)、`resourceCardReferenceDropModel.test.ts` 6 passed(新增逐值比较)、`resourceCanvasChatReferenceDrop.test.tsx` 4 passed;`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。 - 影响范围:`apps/ai-game-creator-shell/src/view/project-development/{index.tsx,resourceCardReferenceDropModel.ts}`、`.../features/resource-canvas/resourceCanvasChrome.css`、用例 3 个文件、验收用例 C9 注脚。未动 Rust / SpacetimeDB / 共享组件行为(只新增了宿主的源素材行)。 - 已知未覆盖:仍无真机目视;替换会话与任务面板的互斥是新行为,客户端需要复看一次(若产品希望两者能同时看,把这一句收紧即可)。 ## 2026-09-21 验收现场修正:任务开关压在工具条上、开关与面板并排 - 背景:客户端验收截图两条:①画布右上角那枚「生成任务 · N」开关与工具条(打开项目目录 / 资源面板 / 整理画布 / 管理未完成编辑 / 依赖 · 类型)**重叠**;②「生成任务」面板展开时,开关与面板**并排**摆着,产品口径是「这俩不应该并排,出来详情以后入口就应该隐藏」。 - 决策(重叠):锚点不再用 `position: absolute` + 写死的 `top: 3.5rem`(工具条是 stage 第一行,高度随按钮换行变化,写死的偏移迟早压上去),改成 **stage 网格里与工作面同一个单元格的另一个条目**:`grid-row` 按 `placement` 分档(资源画布 3 / UI 编辑器 2 / 运行表现层 `2 / -1`)+ `grid-column: 1` + `justify-self: end` + `align-self: start` + `margin: 0.85rem`(运行那档上边距 3.5rem 让开右上角版本入口)。为此把同格的三处容器也显式钉住第 1 列(`.game-workbench-stage > .game-resource-manager`、`.…[data-resource-view-state='resources.ui-editor'] > .game-workbench-editor-shell`、`.game-run-surface`)——锚点是显式定位条目,画布若走自动列放置会被挤进隐式第二列、画布直接压窄一半。 - 决策(并排):开关只在**完全收起**时渲染(`!open && phase === 'idle'`,收起动画期间也不画,否则那 160ms 又会同框);展开态画布右上角只有面板,收起走面板头部那枚 × 或点画布外部。原来「点画布外部自动收起」的判据里对开合按钮的排除保留(收起态仍靠它开合)。 - 原因:验收口径优先于「照抄美术画布」——美术画布把开关常驻在面板旁边,但产品要的是「入口与详情不同时出现」。重叠那条的根因是**猜了一个绝对偏移量**:工具条高度不是常量,锚点必须由布局自己推导。 - 验证:`resourceCanvasAssetGenerationTasksPanel.test.tsx` 12 passed(新增「展开时开关让位:同一时刻只有面板那枚 ×」;计数用例改为收起态查开关、展开态查面板头部)、`resourceCanvasAssetGenerationTasksSidebarStyle.test.ts` 11 passed(锚点改成网格条目坐标:`grid-row` / `grid-column` / `justify-self` / `align-self` / `margin` 与运行档上边距;窄屏改判 `justify-self: stretch`)、`resourceCanvasGenerationTasksSidebarDismiss.test.tsx` 5 passed(收起改走面板 ×、收起后开关回来能再打开、锚点仍在 stage 里)、`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 5 passed;`appSurface.test.ts` 538 passed / 17 skipped / 0 failed。 - 影响范围:`apps/ai-game-creator-shell/src/features/resource-canvas/{ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTasksSidebar.css}`、`apps/ai-game-creator-shell/src/styles.css`、三个同场景用例文件、PRD §3.10 与更新时间。 - 已知未覆盖:仍无真机目视(网格落点在 Tauri 里的实际观感、工具条换行时锚点是否仍贴画布顶边需要现场复核)。 ## 2026-09-21 AGC 替换面板改为非模态浮层,支持在画布上点选目标 - 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「在替换面板中支持画布点选目标」。2026-09-13 那版做的是「候选弹窗 footer 一个『点选替换』按钮 → 关掉弹窗 → 进画布点选态 → 点中即提交」:面板与画布互斥(弹窗外壳是全屏遮罩,留着它画布上的卡点不到),用户要么在面板里筛、要么离开面板。 - 决策:候选面板改成**非模态浮层**(共享组件新增 opt-in `nonModal`:不铺遮罩、不做焦点陷阱、面板自身限高 + 内部滚动、Esc 在 `document` 阶段截断后取消;网页端美术画布不传,弹窗行为逐字不变)。AGC 把它锚在画布**右上角、任务开关下方**(`.game-resource-replacement-panel` → `top: 8.5rem; right: 0.85rem`,壳样式在共享样式表里,宿主只负责锚定)。**面板开着时画布照常可点**:点中合法候选即落成面板里的当前选择(面板里随之 `aria-selected`),写入仍然只由面板「确认」发起;点中非法目标在面板里说明原因、零写入。旧的「点选替换」入口、画布提示条 `.game-resource-canvas-pick-hint` 与 `resourceReplacementPickMode` 随之退役(同一个功能不留两条 UI 路径)。 - 决策(同步口径):`selectedAssetIds` 的**数组引用不能当同步信号**(调用方每次渲染都会重建它,放进依赖会清掉用户在面板里的选择——组件里原本就为此写过一段注释),所以新增显式序号 `initialSelectionRevision`:只有宿主真的换了目标才重同步选择,搜索词与分类筛选保持原样。`confirmResourceVersionReplacement` 另外拿 `resourceReplacementPick` 兜底:点完画布当帧就按确认时不该报「请选择一个替换素材」。 - 原因:面板与画布是同一屏的两半——目标本来就在画布上,「先把面板关掉再点」是弹窗外壳带来的妥协,不是产品意图。换成非模态之后,合法性判据仍是同一条 `resolveResourceReplacementPick`、写入仍是同一个 `confirm` 函数(载荷逐字一致),所以这是**外壳**的改动,不是第二套替换实现。 - 已知取舍(相对旧口径的行为变化,均已随测试钉住):面板开着时空白处点击不再被吞——画布恢复正常的清焦点 / 框选 / 平移语义(源身份在打开面板时就已冻结,替换不依赖画布选中);「点中即提交」不再存在,提交一律由「确认」触发;关闭面板后卡片单击语义原样(一次性抑制照旧收尾)。 - 验证:`resourceVersionReplacement.test.tsx` 19 passed(4 条旧点选用例改写为:非模态判据 + 点画布候选落进面板且零写入、面板确认才写入且载荷逐字一致+血缘、四类非法目标面板内报因零写入、Esc 只收面板不清选中;新增「画布点选不清掉面板搜索/分类」)、`projectAssetPickerDialogShellStyle.test.tsx` 7 passed(新增 nonModal 形态与浮层壳样式声明,含关掉即卸载)、`src/components/image-editor/ImageCanvasEditorView.test.tsx` 64 passed、`ImageCanvasEditorGenerationIntegration.test.tsx` 44 passed(网页端弹窗行为未变);连同 `appSurface.test.ts`、`projectResourceLiveIntegration.test.tsx` 共 6 个文件 694 passed / 17 skipped / 0 failed;`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。 - 影响范围:`src/components/image-editor/ImageCanvasProjectAssetPickerDialog.tsx`、`packages/shared/src/components/styles.css`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`.../features/resource-canvas/resourceCanvasChrome.css`、`apps/ai-game-creator-shell/tests/{resourceVersionReplacement.test.tsx,projectAssetPickerDialogShellStyle.test.tsx}`、PRD §5.3 / §7.8 第 8 条、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md` 的 S15a。未动 Rust、SpacetimeDB、external v1。 - 已知未覆盖:真机观感(面板在画布右上角的落点、与任务开关同时打开时的间距)未在 Tauri 目视确认;面板里不显示「本次替换的是哪张源素材」,源身份只靠画布上那张卡的选中光环表达——点空白清掉选中后就只剩面板自己的候选列表。 ## 2026-09-21 AGC 资源卡拖到对话实现批量 @ 引用 - 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「聊天拖拽批量引用」。改前只有两条入口:聊天输入框里输入 `@` 或点 `@` 按钮开素材选择面板,以及资源卡选中工具条上那枚「引用」按钮——都要先把素材找出来再点,多选批量引用没有一次成型的路径。 - 决策:资源卡**按住拖到右侧 Agent 对话栏、松手即批量引用**。落点判据是「指针是否在对话栏矩形内」:pointermove 在对话栏上时语义从排版切成引用(卡片不再跟着指针走,改铺一层虚线落点浮层 + 「松手即可 @ 引用 N 项素材」),拖回画布内松手仍然是原来的排版语义(照旧写手动坐标)。批量范围与拖动位移**同一集合**(`drag.moves`):多选后拖任意一张 = 整批引用,拖未选中的卡 = 只引用它自己。 - 决策(引用构造与派发):引用构造收敛成纯函数 `resourceCardReferenceDropReferences`,口径与工具条「引用」按钮**逐字一致**——只认已登记 manifest 的素材(`manifestAssetId`)、`source: 'resource-card'`、kind / 分类 / 标签取自资源投影,于是同一素材从两处进来是同一枚引用(去重键同样一致)。派发走新增的批量事件 `RESOURCE_REFERENCE_INSERT_MANY_EVENT`(`dispatchResourceReferenceInsertMany`):N 条引用一次事务插进草稿、只聚焦一次,不逐条重建草稿。一条也构造不出来时(选中的素材都未登记)不静默:提示条说明原因。 - 原因:拖动本来就在指针捕获下走,指针跑到画布外仍回到卡片 handler,因此「落点」只能自己量;不落盘是因为对话栏那一段没有画布坐标可言(写下去会得到跑到画布外的坐标),而且用户在对话栏上松手的意图本来就不是排版。`0 x 0` 的对话栏矩形必须判成「没有落点」:零面积矩形会让任何点都命中,画布内正常拖动会被整段跳过。 - 验证:`resourceCardReferenceDropModel.test.ts` 5 passed(矩形/命中/构造/提示文案)、`resourceCanvasChatReferenceDrop.test.tsx` 4 passed(单卡拖到对话 → 1 条引用且零坐标写入、多选整批 → 2 条、拖回画布 → 照旧写手动坐标且零引用、pointercancel 收干净)、`resourceReferenceInput.test.tsx` 30 passed(新增批量事件一次派发且空批次不派发、一次 insertReferences 按序插入整批 chip);连同 `appSurface.test.ts` 在内 7 个用例文件 660 passed / 17 skipped / 0 failed;`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。 - 影响范围:`apps/ai-game-creator-shell/src/view/project-development/{index.tsx,resourceCardReferenceDropModel.ts}`、`.../features/project-workspace/resourceReferences.ts`、`apps/ai-game-creator-shell/src/App.tsx`、`apps/ai-game-creator-shell/src/styles.css`、`apps/ai-game-creator-shell/tests/{resourceCardReferenceDropModel.test.ts,resourceCanvasChatReferenceDrop.test.tsx,resourceReferenceInput.test.tsx}`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`。未动 Rust、SpacetimeDB、`packages/`、共享弹窗组件。 - 已知未覆盖:真实客户端里的手感(拖到对话栏的触发距离、提示条位置、多选整批的视觉反馈)未在 Tauri 目视确认;「复制对话保留有效引用」属于同一条后续需求里的另一半,本轮未做。 ## 2026-09-21 AGC「生成任务」侧栏移到画布右上角(照抄美术画布,保留 AGC 样式) - 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「生成任务列表移到画布右上角,进行中/已完成分组、失败可见、限高滚动及自动开合」。改前状态:侧栏本体是 `position: fixed; top: 4rem; bottom: 6rem; left: 0.75rem` 的左侧贴边面板,开合口只有工具条上那一枚「生成任务 · N」按钮(资源 / 运行两个页签各渲染一次),折叠态不留任何常驻入口;分组、失败可见、限高滚动、提交后自动展开这四条当时已经具备。 - 决策:侧栏改挂**画布右上角的锚点**(`.game-resource-generation-tasks-anchor`),形态照抄网页端美术画布的任务侧栏(`ImageCanvasTaskSidebarView.tsx` + `src/index.css:6072+` 的 `.image-canvas-editor__task-sidebar*`):**开关常驻右上角、面板在开关左侧展开**,收起态只剩那一枚开关;工具条上那两处重复入口删掉——同一个功能两个入口本身就是两处随时会漂移的状态。锚点是覆盖式的,仍然不 reflow 画布视口。颜色、圆角、字重、阴影**全部继续走 `--platform-*` token**,不照搬网页端的固定色值(该 CSS 文件头既有的口径)。 - 原因:右上角是画布上唯一「不被右侧『智能创作』对话面板占、也不与左下角栏目工具栏 / 右下角缩放 Dock 打架」的稳定空位;折叠态仍留一枚开关以后,用户不必先想起工具条在哪一行,也不再需要「关掉以后打不开」的兜底(原设计正是靠工具条入口常驻来解决这个问题)。 - 决策(分档坐标):锚点由 view 的 `placement`(`canvas | run | editor`)落成 `data-generation-tasks-placement`,坐标在样式里分档:资源栏目画布与 UI 编辑器用画布顶边那一档(`top: 3.5rem; right: 0.85rem`),**运行表现层下移到 `top: 7rem`**——它右上角 `top: 20px; right: 20px` 被 C7 版本入口 `game-run-version-picker` 占着,不去抢那一块。 - 实现要点:开关改由 view 自己渲染(`data-resource-generation-task-toggle` 与计数属性原样保留,可访问名 `生成任务` 唯一),在途计数只在 view 内算一次、开关与面板头部同源(宿主原先那份 `resourceAssetGenerationInFlightCount` 因此删除);面板改由 `max-height: min(30rem, calc(100vh - 12rem))` 封顶 + 内部滚动,自己不再定位(坐标只由锚点一处决定);进场 / 退场动画位移方向跟着锚点翻到正 X。宿主的「点外部收起」判据(排除侧栏本体与 `data-resource-generation-task-toggle`)与「提交受理后自动展开」均未改。 - 验证:`resourceCanvasAssetGenerationTasksPanel.test.tsx` 11 passed(新增「折叠只剩右上角开关」「开关计数与面板头部同源」「锚点按工作面分档」)、`resourceCanvasAssetGenerationTasksSidebarStyle.test.ts` 11 passed(锚点坐标 / 运行档下移 / 面板不再自定位 / 开关走 token 且无硬编码色 / 窄屏占满宽度)、`resourceCanvasGenerationTasksSidebarDismiss.test.tsx` 5 passed(入口位置改为右上角锚点、工具条不再有生成任务按钮)、`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 5 passed;`appSurface.test.ts` 538 passed / 17 skipped / 0 failed(与既有基线逐条一致);`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。 - 影响范围:`apps/ai-game-creator-shell/src/features/resource-canvas/{ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTasksSidebar.css}`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/{resourceCanvasAssetGenerationTasksPanel.test.tsx,resourceCanvasAssetGenerationTasksSidebarStyle.test.ts,resourceCanvasGenerationTasksSidebarDismiss.test.tsx}`、PRD §3.10。未动 Rust、SpacetimeDB、`packages/`、共享弹窗组件。 - 已知未覆盖:真实客户端观感(右上角坐标相对画布顶边的落点、与运行表现层版本入口的间距)未在 Tauri 里目视确认;窄屏(≤480px)只有声明级断言。 ## 2026-09-21 画布绑定前置查询收口为项目摘要,失败文案补因链 - 背景:dev 上「AI 生成图片」连续失败,卡片显示 `解析读取外部画布项目响应失败:error decoding response body`,每条恰好 `1 分 00 秒`;同批的远端资源编辑终态只提示「已明确失败」,用户看不到原因也看不到下一步。(同批「图标素材切片超过 64 个」已由本文件「图集切片上限:客户端结果门从 64 对齐到平台契约的 256」条目决策,这里不再重复。) - 决策(项目列表视图):`GET /api/editor/projects` 与 `GET /api/external/v1/editor/projects` 共用同一套 `view` 取值与摘要投影(缺省 `full` 保持兼容,未知取值失败关闭);`summary` 只回传 `projectId / title / updatedAt / cover`,既不做内联媒体修复,也不带画布与全量资源。投影实现收敛到 `editor_project.rs` 一份,外部 API 与 MCP 复用同一份;AGC 画布绑定前置查询固定使用 `?view=summary`,它只需要 `projectId`。 - 决策(错误文案与终态出口):外部请求失败文案补 kind 语义与底层因链(`reqwest::Error` 的 `Display` 只有 kind,超时 / 正文截断 / 非法 JSON 显示成同一句话),且不拼接 URL;远端资源编辑终态文案带出稳定失败码并指向唯一出口「移出恢复队列」,上游原文继续不写入账本。 - 影响范围:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/api-server/src/external_editor_api.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs`、`project/resource_editor.rs` 与对应夹具。 - 验证方式:`cargo test --locked -p api-server -- summary`(含站内 `view=summary` 跳过媒体修复、未知 view 返回 400 的新用例)、AGC 壳 `agent::generation::`(99 项)与 `project::resource_editor::`(59 项)、`cargo fmt --check`、`npm run check:encoding`、`git diff --check`。 ## Unity 与 Godot 常用操作指导 两种编辑器的操作指导复用客户端审核 Skill pack:DirectProject 通过原生 Skill 或既有审核资源读取入口按需取得,Agent Runtime 的对应执行工具说明嵌入同源参考。指南不改变插件可用性、执行授权或 Runner 回执;只读说明不能证明编辑器已连接。常用示例与执行失败/部分修改、保存、撤销边界在同一参考中维护,避免提示词和文档各存一份代码。 ## 2026-09-20 Godot 编辑器执行接入 原生引导采用固定版本的官方 `godot-cpp` 和 MSVC x64 构建,绑定及 C++ runtime 静态链接。依赖归档和缓存源码须核验,安装目录仍只分发原生载荷及许可。EDITOR 阶段动态加载/卸载时显式清理 C++ 实例绑定与单例包装,保留纯 GDScript 的异步执行和原有协议;执行权限、项目身份与缓存归属继续由现有宿主处理。 可用性边界按引擎区分:Cocos/Unity 保持不按工程类型过滤,Godot 仍绑定当前 Godot 项目,切项目撤销旧插件上下文;前端统一根据宿主投影启动插件。Runtime 工具目录只对 Godot 追加项目条件,编辑器说明沿用外置提示词及审核 Skill 参考。 Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和不确定执行回执合同,编辑器实现留在 `plugins/agc-godot-editor`。用户选择 DLL 原件随 AGC 安装资源分发,并确认按编辑器实例在 AGC 私有缓存准备临时加载副本,以满足 Godot Windows 加载器的同目录 `~DLL` 写入要求;项目内不复制 DLL,只用受管 `.gdextension` 引导。Godot 自动 UID 伴生文件必须记录归属并在确认卸载后按内容匹配清理。工作区根不迁移到 Godot 子目录,原始项目配置与场景只通过明确编辑操作修改。完整合同及验证范围见 [Godot 编辑器插件接入](<../../technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md>)。 ## 2026-09-20 DirectProject 工具并行与交付收敛 - 所有工具具备有界并行调度能力,MCP 每入口在途上限 8;独立图片在客户端进程内最多 2 个。只保留同资源冲突、编辑器实例、canonical 美术包和短提交事务的必要串行边界。同一付费动作必须在容量排队前取得原 durable 槽锁,不重写幂等算法。 - Web 工程由客户端提供经完整性校验的 Node/npm 和浏览器健康预检,保留隔离 HOME;缺失或损坏不能静默退回项目或系统中的另一份 Node。 - Direct 回合的合同、证据和预算以宿主私有账本为准,项目侧记录只作展示。GUI/CLI 共用入口;首次副作用前冻结非空验收合同,可信新 Web 工程由宿主补充构建和双端验证底线。普通无副作用聊天不强制构建。 - 视觉、固定玩法和托管命令分层;`validation.maxRuns` 按执行/返修批次管理,正常开发命令共享批次;累计执行时间与整轮墙钟分别受 `maxExecutionSeconds` / `maxTurnSeconds` 约束,显式配置与 Provider 重试独立。源码、构建输出、环境输入与证据文件摘要分别复核,项目可编辑记录不能抬高预算或伪造成功。 - 原生工具使用已验证的捆绑版本逐次审批能力,第三方 MCP 显式逐调用询问;所有 Direct 入口接受宿主同一状态,未知远端结果不得以本地进程退出代替。独立客户端 HTTP MCP 使用明确的 ExternalClient 来源,保留其既有边界,不借用另一 Direct 回合的预算。 - 交付必须先封口、排空和取得受控执行归属退出证明,再核对当前文件并提交完成;Windows 用自有 Job 约束进程树,托管命令在恢复主线程前绑定。Unix 受控进程组退役允许回合完成,但不宣称完整子树均已退出;归属清理失败仍保持未完成,不把模型最终回复当作验收。非阻塞扩项进入新的用户回合。 - 模型配置、实际请求标识和流分段耗时写入现有审计账本;统计采用并发区间并集,有界后台写入,详细条目截断后仍聚合。上游内部排队和推理耗时不可见时保持未知。 - Direct 工具集中 SDK 原生 `apply_patch` / `update_plan` 是全局串行单例,按回合为每个 Direct 连接导出一份只把 `apply_patch_tool_type` 置空的完整模型目录即可移除该注册;其余 metadata、匹配与 fallback 不变,不得伪造 `readOnlyHint` 或改造 SDK。等价能力由宿主 MCP 的 `agc_apply_patch`(官方 parser、当前回合 Write 许可、受控进程树、短项目事务)与 `agc_update_plan`(宿主计划状态,不作为验收证据)提供;缺少合法回包通道的原生问答工具一并关闭。 - 捆绑 Codex 固定版本只在 `build_support/codex_bundle.rs` 声明一次(当前 0.155.1),构建期侧车清单、宿主补丁执行器身份、逐次审批协议允许列表和模型目录捕获共同引用;升级原生依赖时同步重取同一 tag 的 vendor 解析源码与 UPSTREAM 证据,并复跑真实目录、补丁往返与并发夹具。0.155 起原生执行入口改为统一 exec(`exec_command` + `write_stdin`,旧 `shell_command` 不再注册),宿主许可与预算照常覆盖。 - 付费许可按原回合原租约绑定并传递到实际提交点:容量与同动作锁等待可取消,每次新增 POST 前与封口共用短锁复核,封口/终止/耗尽后零新增提交;已越过提交边界的请求不丢弃,保留 operation ID 与不确定状态走 GET 对账。本地写入同理,等待项目锁后必须复核原许可,未结算或失败的写入围栏未恢复前不得封口。 - 权威合同:[AI 游戏创作智能体 App 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。 ## 2026-09-20 最近项目检查保持项目级隔离 - 背景:最近项目刷新会重新检查所有路径。若其中一个目录损坏、超时或不可读,清空整张状态表会让已确认正常的项目暂时全部显示“检查中”,用户只能移除坏项目后看到列表恢复。 - 决策:最近项目状态按路径独立投影;刷新时保留仍在列表中的最后一次**成功**结果,失败结果不进新投影并在本轮重新检查(见 2026-09-23 条目),只有新增或尚未检查的项目进入“检查中”。检查代次或列表成员变化后,迟到结果不得写回,单个项目的失败不能改变其它项目的可打开状态。 - 验证:`recentProjectsHook.test.tsx` 覆盖“新增慢/坏项目刷新时保留正常项目”;`recentProjectsModel.test.ts`、`unityProjectOpen.test.tsx` 与前端类型检查一并执行。 ## 2026-09-17 GameCreationApp 资源 kind 只保留一份词汇表:严格解析 + `app_log!` 留痕 - 背景:kind 曾经有三份实现——Rust 手写 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS` + `canonical_game_creation_app_asset_kind()`(带 legacy 别名表与 `font → document` 特例)、TS 手写 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS` + `GAME_CREATION_APP_LEGACY_ASSET_KINDS` + `canonicalGameCreationAppAssetKind()`、以及 ts-rs 生成的 TS union。两份手写表互相引用又各自收口,判据直接分叉(同一个 `"UI"` 一边归一成 `ui-design`、一边收口成 `unknown`),跨语言一致性只能靠正则解析源码的测试来钉。 - 决策(唯一真源):kind 的变体、线上值、`as_str()`、`ALL`、严格解析与 ts-rs 绑定全部由 `server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs` 的声明表派生。两份手写 canonical 列表、legacy 别名表、`canonical_*()` 函数一律删除;生成的 TS union 落在 `packages/shared/src/contracts/generated/GameCreationAppAssetKind.ts`,`packages/shared/src/contracts/gameCreationApp.ts` 只 re-export 它,运行期列表 `GAME_CREATION_APP_ASSET_KINDS` 用穷举 `Record` 守住。 - 决策(单一解析入口):`GameCreationAppAssetKind::parse_with_context()` 是唯一公开解析入口,严格匹配降为私有 `match_canonical()`;易混的 `from_str_lossy()` 与公开 `from_str_or_unknown()` 都已删除,认不出 canonical 值只有这一处收口并留痕(读侧回捞 legacy kind 只查 `ALL` 判真值,不另开解析函数)。口径是等值匹配——不 trim、不 lowercase、不查别名、不迁移;认不出的值收口成 `Unknown`(分类落 `unclassified`)。这是有意接受的行为(历史误写的 kind 不会被"救回"正确栏目),不再提供任何兼容入口;`font` 是正式成员,不再走别名。 - 决策(登记边界的 kind 口径):外部登记统一走 `assets::registration_asset_kind()`——空白入参落中性 `image`,非空认不出的值严格收口成 `unknown` 并留痕;三条登记边界(Tauri `register_local_asset`、画板导入、平台导入)不再各写一份 trim/兜底。`Unknown` 是无法解析的边界结果,具体写入方必须自行决定拒绝或保留待后续归类,不能静默猜成其他 kind。栅格归一化的 `source_subtype` 只接受图片族成员,文档/字体/音频等一律按 `image` 登记;Agent 回执派生物按 `Text => Document` 落 `document`。 - 决策(判据不许散落):切片残留登记只看 `assets/art-spritesheet-slices/` 路径(不再附带 `kind == Icon`);sprite 身份比较忽略随 kind 派生的 `metadata.asset_type`;TS 侧「UI 编辑器文档资产」判据只留 `isGameCreationAppUiDesignDocAsset()` 一份,资源画布入口 / UI 编辑器桥接 / 资源引用缩略图统一调用。 - 决策(已知代价,不补救):既有项目里无法解析的 kind 读入即 `unknown`,依赖 kind 等值比较的运行门禁会按"缺少该资源"处理。这是严格解析的必然结果,本次明确不为存量数据做迁移;将来若要迁就必须单独立项,不能改写解析边界。 - 决策(留痕必须真的落地):原实现用 `tracing::warn!`,而 AGC 壳没有 tracing subscriber,等于没有日志。现在 `shared-contracts` 只暴露可注册回调 `set_non_canonical_asset_kind_reporter()`,AGC 壳在 `main()` 里接到 `app_log!`,日志同时含原始输入串与调用上下文;`kind-observability` feature 与 `tracing` 依赖一并删除。TS 侧对应 `parseGameCreationAppAssetKind()` 的 `console.warn`。 - 决策(平台/画板词汇表):平台生成输入先严格解析为 `GameCreationAppAssetKind`,登记时直接写入已解析的 enum,不再保留 `platform_art_asset_manifest_kind()` 或任何平台别名/fallback 映射;图片快速编辑来源同样只允许 canonical 静态图片成员并要求 `mediaType=image`,原 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 兼容白名单已删除。 - 验证:`cargo test -p shared-contracts`(含词汇表唯一性、严格性与留痕用例)、`cargo test --locked -p shared-contracts --features ts-bindings export_bindings` 后 `git diff` 为空、AGC bin 定向用例(`derived_asset_manifest_kind_is_never_unknown_for_text_derivatives`、`non_canonical_manifest_asset_kinds_report_raw_values_only`)、`npx vitest run packages/shared/src/contracts/gameCreationApp.test.ts apps/ai-game-creator-shell/tests/uiDesignResourceBridge.test.ts apps/ai-game-creator-shell/tests/appSurface.test.ts`。 - 关联文档:[AI 游戏创作智能体 App 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的 2026-09-17 节。 ## 2026-09-16 DirectProject 引用渲染收敛到 canonical user item 深模块 - 背景:`agent/direct_codex_references.rs`(平行 `DirectCodexTurnReference` DTO 与渲染路径)已退役,引用身份、上限与投影收敛到 `agent/direct_codex_user_item/`;本轮 prompt 里引用只投影为 `[素材引用 resourceId=…;项目路径=…]` 摘要,`MAX_DIRECT_CODEX_REFERENCES` 归 `direct_codex_user_item/validation.rs`。 - 决策:2026-09-14「Direct Codex 引用 UI 设计文档生成代码上下文」的 prompt 注入改落在 `agent/direct_codex_user_item/wire.rs`,入口是 `direct_codex_user_item_to_prompt`:引用命中 `ui-design-doc` + `application/json` 时顺序调用 `generate_ui_design_code_at`,成功追加 `请先阅读生成的带有文档的代码片段: {relative_path}`,失败追加原始 `生成代码遇到错误{error}`,其它引用继续处理,manifest 身份摘要不变。 - 决策(无写副作用):只在生成本轮 prompt 时展开该上下文;历史 item 回读走 `direct_codex_user_item_to_response_item` 的纯投影,不得触发 UI 代码导出或任何项目写入。 - 验证:`agent::direct_codex_user_item` 定向 16 条通过,覆盖成功注入、生成失败保留引用摘要、非 UI 文档不触发与历史回读不产生 `ui/generated-*.js`。 ## 2026-09-19 AGC Direct 删除每回合四项媒体资源请求上限 - 背景:2026-08-24 引入的单回合四项上限以「整个 agent run(一条用户消息到回合结束)」为窗口,计数只增不减、请求完成不释放额度;autonomous 游戏构建要求 agent 不停下跑完整局,额度耗尽后的报错实际是终态,与技能的三次重试纪律冲突,现场表现为长时间无效重试。2026-09-18 先将不计费的抠图豁免,但付费 create/derive 仍受同一窗口问题影响。 - 决策:整体删除该上限机制。移除 `DIRECT_TOOL_BRIDGE_MAX_RESOURCE_CALLS_PER_TURN` 常量、回合授权状态中的 `resource_request_ids` 计数 map 与 `resource_request_ids()` 方法;`agc_create_or_derive_resource`(含 `agc_edit_image` 委托)与 `agc_remove_background` 统一按回合身份 + 请求指纹确定性派生 operation/idempotency id,同指纹重试复用与 pending 对账语义不变。付费提交串行仍由 `resource_generation_gate` 互斥保证,成本控制由服务端计费与泥点余额兜底,客户端不再按回合计数设限。 - 验证方式:`cargo check`(ai-game-creator-shell src-tauri)通过,203 项警告与基线一致;无测试断言该上限,未新增测试。 ## 2026-09-18 AGC Direct 抠图不占每回合四项付费媒体额度 - 状态:2026-09-19 起该上限机制整体删除(见上条),本条目仅作追溯。 - 背景:2026-08-24 起 `agc_create_or_derive_resource` 与 `agc_remove_background` 共用每回合四项媒体资源请求上限。抠图服务端持久化 `generation_cost_mud_points: 0`(不计费),bgfilter 实测单张约 1 秒,上限导致一回合抠超过四张时后续请求被直接拒绝、agent 反复无效重试。 - 决策:`agc_remove_background` 不再经过 `resource_request_ids` 计数,直接按回合身份与请求指纹确定性派生 operation/idempotency id(与 map 复用结果一致),同指纹重试与 pending 对账语义不变。付费的 create/derive(含 `agc_edit_image` 委托)维持四项上限与原有报错文案,且抠图请求不再挤占其额度。 - 验证方式:`cargo check`(ai-game-creator-shell src-tauri)与 `git diff --check` 通过;未新增测试。 ## 2026-09-18 AGC 未提交快速编辑草稿按资源路径归属,正式恢复账本不动 - 背景:画布验收项 AGC-006/023 要求「点外部 / Esc 收起面板、或换素材卡」不再无条件丢弃用户刚写的提示词与 `@` 引用,于是宿主内存里多了一份未提交草稿表(`resourceCanvasQuickEditModel.ts` 的 `ResourceQuickEditDraftStore`),与原生 `list_pending_local_project_resource_edits` / `resume_local_project_resource_edit` 那条正式可恢复账本**并存**。 - 决策:草稿键是投影的稳定身份 `ProjectResource.path`,不是投影 id。资源投影本来就按 path 去重(`resourceProjectionModel` 的 `uniqueByPath`),而 id 会变——任务产物经 `normalize_local_project_raster_resource` 登记成正式素材后,同一张卡从 `task:<任务>:<路径>` 变成 `asset:`。用 id 当键时,任何**不是打开中这一笔快速编辑自身触发**的重投影(换素材卡收起面板、Agent 或外部编辑器把同一路径登记成资产)都会让草稿落到再也点不到的键上:恢复入口按 id 过滤后静默丢弃、重开面板按新 id 查不到。按路径归属后不需要任何「跟着投影搬家」的换键逻辑,那条逻辑本身就是搬丢的来源。 - 决策:本地草稿是**会话内存态**——不落盘、不进账本、不参与对账;共享入口「管理未完成编辑」同时列出来源不同的两种条目,草稿条目显式标注「本会话未提交,关闭客户端不保留」,账本读取失败时仍报出本会话草稿条数,避免用户把两者当成同一种持久事实。 - 决策:`ResourcePromptPolishSlot` 与共享聊天输入区(`ResourceReferenceInput`)共用 `usePromptPolish`;「与原文相同」以**规范化之后要写回宿主的文本**为准(回包被长度上限截回原文同样算没变化),此时不落原文快照、不回填宿主,只给提示,避免出现点了等于没点的「恢复原文」假入口。 - 边界:发送前提醒面板里「AI 润色并发送」遇到原样回包仍按用户意图直接提交,本轮按产品取舍保留(记录在 `docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md`)。 - 落地:`apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasQuickEditModel.ts`(工厂空表 / 按路径写入读取丢弃 / `listResourceQuickEditDraftEntries`)、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`(草稿接线与恢复入口)、`apps/ai-game-creator-shell/src/features/resource-canvas/ResourcePromptPolishSlot.tsx`、`apps/ai-game-creator-shell/src/features/project-workspace/{usePromptPolish.ts,ResourceReferenceInput.tsx}`;回归见 `tests/resourceCanvasQuickEditModel.test.ts`、`tests/resourceCanvasQuickEditDraft.test.tsx`(含「面板收起后旁路重投影」用例,改回 id 作键即红灯)、`tests/usePromptPolish.test.tsx`、`tests/resourcePromptPolishSlot.test.tsx`、`tests/chatPromptPolish.test.tsx`。 ## 2026-09-18 派生资源名称在前端镜像 Rust 门禁,宁可按原话拦下也不截断 - 背景:快速编辑(`-编辑版`)与角色动画(`-角色动画`)的派生资源名由前端按源资源 label 拼出,直接进 `derive_local_project_resource` 的 `input.assetName`;Rust `normalize_resource_edit_name`(`resource_editor.rs:802-808`)要求 `trim` 后非空、码点数 1..=120 且不含控制字符,任一不满足即整条请求失败。源 label 取任务标题(Agent 回执)或长生成文件名时,用户必须提交一次才看到「派生资源名称必须在 1..=120 字符内且不能包含控制字符」。这是画布验收遗留项 AGC-025 的成因之一。 - 决策:前端只做镜像,不另造中文语义——上限 120、`Cc` 控制字符判定(C0 / DEL / C1,`Cf` 继续放行)、`trim` 后判空、按 Unicode 码点计长(Rust `chars().count()`,不是 JS UTF-16 `.length`)四条与 Rust 逐条对齐,提示文案与 Rust 返回串逐字相同;两份判定由跨语言测试直接读 Rust 源码对表。 - 决策:名称不合法时**阻止并提示**,不截断。截到 120 会让同前缀的两个长源名塌成同一个名字,把一次可见失败换成一次静默重名;默认名函数因此保持返回不合法原名,由检查结果里的 `error` 让调用方先停下。 - 落地:`apps/ai-game-creator-shell/src/view/project-development/resourceEditModel.ts` 的 `RESOURCE_EDIT_NAME_MAX_LENGTH` / `RESOURCE_EDIT_NAME_INVALID_NOTICE` / `resolveResourceEditNameCheck` / `resolveDerivedResourceNameCheck(resource, 'edit' | 'character-animation')`,纯函数与跨语言对表见 `tests/resourceEditModel.test.ts`;两种后缀共用同一张后缀表,避免「-编辑版」与「-角色动画」再次分叉。 - 接线:`index.tsx` 的 `submitResourceQuickEdit` 与 `submitResourceCharacterAnimation` 都在**调 `resolveResourceDeriveSource` 之前**做检查(正规化写盘发生在那一步之后),不合法时只把 `status: 'failed'` 与 Rust 原话写进各自的浮层面板并 return,不发派生请求;`assetName` 用检查结果里的 `name`。回归见 `tests/projectResourceLiveIntegration.test.tsx` 的两条「源名超 120 码点」用例与既有的合法名校验 `source-art-编辑版` / `hero-角色动画`。 - 边界:资源画布的图片类生成走另一条门禁(`commands.rs` 的 `LOCAL_PROJECT_ASSET_MAX_ASSET_NAME_CHARS`,文案「素材名称超出安全边界」),不在本条口径内;提示词上限仍是 `resourceEditPromptMaxLength` 那一份,本决策不动它。 ## 2026-09-19 删除 Project Supervisor 前端链路:普通项目只有 DirectProject,立项策划只有策划聊天 - 背景:`ProjectSupervisorView` 同时是工作台普通项目的聊天宿主、Supervisor 独立调试窗口(`?supervisor-chat` / `?agent-chat`)和纯聊天容器(`SupervisorChatOnlyView`)的入口;同一条链路还带着 `App.tsx` 里的 Supervisor 会话/运行态轮询、工作台壳的 Supervisor 运行态与专业 Agent 面板、开发态文件/记忆/资产/预览面板和 `AgentConversationOverlay`。普通项目的 DirectProject 因此长期作为「Supervisor 宿主里的一个分支」存在,Direct 自己的订阅、历史、发送、队列、附件和中止生命周期没有独立归属。 - 决策(前端整体退役):删除 `ProjectSupervisorView`、`SupervisorChatOnlyView`、`ProjectWorkspaceChatPane`、`AgentConversationOverlay`、`DeveloperProjectPanels`、`DeveloperRuntimePanels`、`features/agent-runtime/panels.tsx`、`DeveloperAgentPanel`、`useDeveloperAgentPanel`、`useDeveloperAgentState`、`developerAgentControls`、`src-tauri/capabilities/developer.json` 与 `?supervisor-chat` / `?agent-chat` 调试入口;`App.tsx` 里只服务这些面板的 state/ref/effect/handler(`agentConversation*`、`handleFile*`、`handleMemory*`、`handleCanvas*`、`handlePreview*`、`commandLog`、`llmConfigStatus`、`editorBaseUrl`、run trace/history 面板状态等)一并删除。不保留兼容别名、feature flag、双跑路径或墓碑注释。 - 决策(策划链路独立成模块):Design Agent 与 Planning V2 的容器与表现移入 `view/project-development/planning/`(`PlanningChatView`、`PlanningUserInputCard`、`GddApprovalCard`、`DesignAgentSurface`、`PlanningLaneRuntimeStrip`、`planningLane`、`planningSessionV2`、`planningSessionContract`),策划入口不再借用 Supervisor 的组件身份。 - 决策(改名到中性概念):`ProjectSupervisorComponentProps → ProjectChatComponentProps`、`WorkspaceLauncherShellProps.ProjectSupervisor → ProjectChat`、`ProjectDevelopmentView.supervisor → chat`、`orchestrationMode → agentDockVisible`、`initialSupervisorMessageClaims → initialTurnClaims`(`claimInitialTurnForPage`)、`ProjectManifestSnapshotSource 'supervisor' → 'chat'`、CSS `project-supervisor-*` / `project-planning-*` → `project-chat-*`;`PROJECT_SUPERVISOR_AGENT_ID` / `PROJECT_SUPERVISOR_PLAN_SOURCE` 常量删除。 - 决策(清单失效事件归壳):`game-creator-manifest-invalidated` 现在由工作台壳(`WorkspaceLauncher`)与 DirectProject 聊天各自订阅——壳用一次配对读(`revision → 清单 → revision`,`source: 'asset-event'`)把运行态落盘的新资源并入 `currentProjectContext`,**不**重新检查项目目录;聊天侧仍由 `useDirectProjectManifest` 刷新自己的 `@` 引用清单。 - 决策(残留清理):随面板一起失去调用方的模块与导出一并删除,不留死代码——`features/project-workspace/projectSummaryCommands.ts`(旧 Supervisor 斜杠命令处理器,1054 行)、`app/constants.ts` 的 `AGENT_RUN_HISTORY_*`、`features/agent-runtime/model.ts` 里只被退役面板使用的 22 个导出与 `AGENT_RUNTIME_DEDICATED_CARD_TOOLS`、`features/project-summary/agentPresentation.ts` 里 17 个 Agent 面板 / LLM 配置 / 读取草稿摘要函数(含 `summarizeAgentStatusCardsForChat`、`summarizeAgentRunHistoryReadDrafts` 等),以及 `agentRunTrace` / `memoryCommands` / `projectCommandPolicy` 中失去调用方的校验与解析函数。 - 边界:后端 Rust Runtime 的 `project-supervisor` 运行身份、`start_game_creator_supervisor_runtime_task` 命令、`agent-runtime:supervisor-*` 端到端脚本与 `project-supervisor` 提示词/工具策略不在本次删除范围内:它们是 Runtime 的 Agent 身份与既有权衡,不是前端链路。 - 验证:全量 `npx vitest run apps/ai-game-creator-shell/tests` 126 失败 / 1517 通过 / 15 跳过(1658 条),`HEAD` 基线同口径 166 失败 / 1637 通过 / 17 跳过(1820 条):逐条比对失败集合**没有新增项**;基线里 40 条与 Supervisor 前端面绑定的失败用例随实现与用例一并删除(含旧调试窗口、开发者面板与 Agent 对话浮层的 51 条用例);剩余 115 条 appSurface 失败与 10 条 `clientApi.test.ts` 失败(jsdom `localStorage` 环境)在基线上同样失败;残留清理后按同一口径复跑,失败集合与失败条数均无变化。`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit --noUnusedLocals`、prettier、eslint、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过。 - 关联文档:[ADR DirectProject 独立聊天容器与工作台钱包布局](../../adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md)。(2026-09-24:对应开发期里程碑与实施计划已按代码级判据验收归档并删除,结论以该 ADR 为准。) ## 2026-09-19 DirectProject 聊天容器按「组件带逻辑」分层,线程订阅与 reducer 提成独立 hook - 背景:DirectProject 聊天虽然已经搬进 `view/project-development/chat/`,但 `DirectProjectChatView.tsx` 单文件 500+ 行,表现层只按 `components/`、`composer/` 平铺文件,线程订阅(`subscribe → consume → notify`)与聊天 reducer 状态混在 `useDirectProjectChatController` 里,组件和它自己的逻辑分散在两个目录。 - 决策(组件带逻辑):`chat/components/` 下一个组件一个目录,组件自己的逻辑与表现同目录:`ToolCallGroup/`(`ToolCallGroup.tsx` + `toolCallGroupPresentation.ts`)、`DirectProjectComposer/`(`DirectProjectComposer.tsx` + `ComposerControls.tsx` + `chatComposerQueue.ts` + `chatComposerVoice.ts`)、`DirectProjectConversation/`(`DirectProjectConversation.tsx` + `DirectProjectTurn.tsx`)、`DirectProjectChatHeader/`、`DirectProjectSettingsDialog/`。视图只做组合,不再内联渲染分区、回合和输入盒。 - 决策(订阅与 reducer 独立 hook):新增 `chat/controller/useDirectThreadChatSubscription.ts`,持有 `subscribe → consume → notify` 单飞循环与 `DirectThreadChatState`(含通知先于订阅回执的欠账处理、`SUBSCRIPTION_EXPIRED` 重订、订阅回执锚点闸门、项目切换清理),并暴露 `entries`、`turnRunning`、`mergeHistoryItems`、`markTurnStopped`。控制器只读投影结果,历史分页仍并入同一个 reducer,不复制第二份事实源。 - 决策(壳只传窄上下文):`DirectProjectChatView` 的公开入参只有项目路径、入口首轮需求(canonical `content[]`)和两条权限门;工作台级动作(`game.run_local` 预览结果等)不再写 `App.tsx` 的 `messages`,改由壳通过 `DirectProjectChatHandle.announce` 交给聊天自己的本地消息流,壳不持有 Direct 聊天消息。 - 边界:不改 Tauri/Rust 命令、DTO、Thread Manager、Codex app-server、持久化事实源、路由选择、发送/队列/附件/中止/历史锚点语义;不保留旧路径 alias、双跑或兼容 fallback。 - 验证:五个 Direct 纯模块测试 40 条通过;`appSurface.test.ts -t "direct"` 15 通过 / 4 失败,4 条与 `HEAD` 同口径基线同为失败(harness 未桩模型配置),基线同口径 14 通过 / 5 失败;全量 `appSurface.test.ts` 由基线 155 失败降到 153 失败;app `tsc --noEmit`、eslint、prettier 通过。 - 关联文档:[ADR DirectProject 独立聊天容器与工作台钱包布局](../../adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md)。(2026-09-24:对应开发期里程碑与实施计划已按代码级判据验收归档并删除,结论以该 ADR 为准。) ## 2026-09-18 Provider 瞬态重试次数严格按设置执行(游戏开发 Agent 与策划 Agent 不再被档位收进区间) - 背景:AGC 客户端此前把 `agentLlm..maxRetries` 按运行档位重新收进固定区间——`autonomous-game-build` 档位(自主构建的游戏开发 Agent 及其继承档位的专业子 Agent)被抬到 12~16,`standard` 档位(含立项策划入口的策划 Agent 与普通 Agent 对话)被压到最多 3;瞬态分类里的上游 400 还在同一预算上再收窄到 2 次。现场把 `maxRetries` 设成 5 时,游戏开发与策划两条链路都不按设置执行。 - 决策:删除这三处区间限制,`maxRetries` 严格等于允许的物理重试次数(显式 `0` 仍表示不重试)。运行档位只保留「上游 400 是否算瞬态错误」的判定(autonomous 档位才重试 400),不再改写次数:`AGENT_RUNTIME_PROVIDER_TRANSIENT_RETRY_LIMIT`、`AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_FLOOR`、`AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_LIMIT`、`AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT` 四个常量与 `game_creator_agent_runtime_provider_transient_max_retries_at` 一并删除,换成返回 `{max_retries, retry_upstream_400}` 的 `game_creator_agent_runtime_provider_transient_retry_policy_at`。 - 边界:只改重试次数的来源。瞬态错误分类(`timeout / connectivity / transport / 408 / 429 / 5xx / empty-response / deserialize / stream-unavailable`,以及 autonomous 档位下的 400)、`retryBackoffMs` 指数退避(仍封顶 30s)、durable retry sidecar / lifecycle / `-transient-N` slot 身份、cancel / steer / goal 门禁、耗尽后的 reconciliation 口径与「泥点不足不重试」都不变;前端设置面板与 `agentLlm..maxRetries` 契约不变。 - 验证方式:`provider_transient_retry_` 7 项中重写后的档位用例与 upstream-400 用例通过(断言 `maxRetries` 直取设置值、400 与其它瞬态共用同一预算),`provider_retry_` 其余 26/28 通过;该组 2 项(`provider_transient_retry_transport_failure_closes_then_stable_retry_succeeds`、`provider_transient_retry_backoff_is_exponential_and_capped_at_thirty_seconds`)与 `provider_retry_waiting_final_reply_*` 2 项在本机改动前后同为失败(`stash` 基线复跑确认,现象是等待自动重试唤醒超时)。本机串行全量套件另有既有环境失败(`tempfile::tempdir()` 归属校验、缺少 npm 构建产物、Windows 启动失败 MessageBox 阻塞 `startup_log_slot_fail_without_path...`);抽查其中 5 项在 `stash` 基线上同样失败,与本次改动无关。仓库 `cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。 - 关联文档:[AI游戏创作智能体App实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、[踩坑记录](pitfalls.md)。 ## 2026-09-20 游戏分发用灰度开关承载「关闭投稿、保在线」的回滚口径 - 背景:主规范要求回滚部署时“关闭新提交和新版本激活,保留当前可玩版本与状态读取”。此前只能靠改配置或停服实现。 - 决策:复用现役灰度配置(`game-distribution:publish`,后台「灰度发布配置」可改),没有 gate 行或 `enabled=false` 时默认开放;`enabled=true` 时只有白名单/标签/灰度命中的作者能发布,`rolloutPercent=0` 且无白名单等于紧急关闭投稿。拦截范围是作者写入(创建游戏/版本、上传、送审、撤回、下架)与管理员批准;读取、发行网关、审核队列读取、拒绝审核与安全下架始终可用,避免把“关投稿”变成“停服务”或“无法处理事故”。 - 失败姿态:开关读取失败按关闭处理(写入口 503),读取路径不受影响。 - 关联:`server-rs/crates/module-runtime/src/application.rs`、`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、[开发运维文档](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。 ## 2026-09-20 游戏发行包 PUT 采用受控重试与容量边界口径 - 背景:阶段 D 容量验证时实测 99.0 MiB 发行包单次 PUT 成功耗时 11.8s、api-server 峰值内存 378 MB(基线 82 MB),但三次尝试里出现过一次 `请求 OSS 失败:error sending request`。当时版本停在 `awaiting_upload`(可原版本重传),代价是作者白传一次整包。 - 决策:`platform-oss` 新增 `put_internal_object_with_retry`,复用既有 `oss_error_is_retryable` 分类(传输/超时/connect、408、429、5xx、400+RequestTimeout 可重试;确定性 4xx 不重试),body 只转一次引用计数的 `Bytes`,各 attempt 复用同一份字节;发行包上传配置为 3 次尝试、250/500ms 退避,参数不合法(次数为 0 或缺退避)时按配置错误失败关闭。 - 容量口径(真实栈实测,作为阶段 D 证据基线):99.0 MiB 包 11.8s / 峰值 +296 MB;声明 101 MiB 在创建版本即 413;请求体 101 MiB 被请求体限制 413 且版本保持可重传;压缩比 1000 与单文件 65 MiB、10,001 文件都是 422 `PACKAGE_VALIDATION_FAILED` 并落到 `upload_failed`/`reupload`;失败包不进公开目录。 - 关联:`server-rs/crates/platform-oss/src/lib.rs`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、[实施计划](../plans/【实施计划】游戏分发阶段A领域合同-2026-09-19.md)。 ## 2026-09-20 游戏详情的“游玩方式”以版本声明的 inputModes 为准 - 背景:公开投影里 `currentVersion.controls` 一直是空数组(首版没有自由文本操作说明的录入),详情页却只读它,于是所有已发布游戏都显示「未标注操作方式」,而作者其实在发布时声明过 `inputModes`。 - 决策:展示层优先用公开投影里已有的结构化 `inputModes`(键盘 / 鼠标 / 触屏,去重后按声明顺序拼接),再退回 `controls` 自由文本,两者都为空才显示「未标注操作方式」。不改 HTTP 契约、不新增后端字段。 - 关联:`src/components/game-distribution/GameDetailPage.tsx`、`src/components/game-distribution/GameDistributionPages.test.tsx`、[主规范](../../【玩法创作】平台入口与玩法链路-2026-05-15.md)。 ## 2026-09-20 游戏分发的版本回读、撤回与安全下架以服务端 recoveryAction 为准 - 决策:游戏发行版本的「下一步做什么」不从客户端状态推断。`GET /api/game-distribution/versions/{versionId}`(作者)与 `/admin/api/game-distribution/versions/{versionId}`(管理员)返回版本私有投影 + 服务端派生的 `recoveryAction`(`upload` / `submit` / `wait` / `none` / `reupload` / `fix_package` / `fix_metadata`),网页发布页与作者中心只按它渲染主行动作。 - 撤回语义:`POST /api/game-distribution/versions/{versionId}/cancel` 只能撤回未参与当前公开投影的版本,要求 `Idempotency-Key` 与 `expectedPublicationRevision` CAS;已公开版本必须走作者下架或管理员 `suspend`,不能借撤回关闭线上入口。 - 可见性:未知版本与非 owner 的版本一律 404,不用 403 区分「别人的版本」和「不存在的版本」。 - 幂等响应:命中既有幂等收据的写操作统一回传 `replayed: true`(此前所有写操作固定 false),客户端据此区分「本次生效」与「复用既有结果」。 - 网页恢复:`/games/publish` 只把 `{ownerUserId, gameId, versionId, versionNumber, title}` 写入 localStorage 作为恢复标识;换账号只忽略草稿、不回读也不清除,禁止展示上一账号的私有状态。 - 关联文档:[游戏分发实现计划](../plans/【实施计划】游戏分发阶段A领域合同-2026-09-19.md)、[主规范](../../【玩法创作】平台入口与玩法链路-2026-05-15.md)。 ## 2026-09-18 AGC backend 采用共享 Runtime、本地宿主与云端控制面分层 - 决策:AGC backend 统一按“`agent-runtime-core`/`agent-runtime-orchestration` 共享内核 + Tauri 本地执行宿主 + `server-rs` 云端控制面 + `module-*`/`platform-*` 领域与外部适配器”整理;先建立 application facade、能力合同和跨边界状态映射,不新建第二套 Agent Runtime、会话库或业务真相。 - 数据边界:本地项目文件、manifest、JSONL、checkpoint、锁和 Runner 状态由 AGC 本地宿主持有;认证、模型目录、编辑器资源、异步生成、计费、快照元数据和诊断由云端持有;大对象按现有 OSS 合同保存。 - 约束:Runtime core 不依赖 Tauri/Axum/SpacetimeDB/Provider;领域规则留在 `module-*`;HTTP/SSE/BFF 留在 `api-server`;SpacetimeDB 访问统一经 `spacetime-client`;外部服务统一经 `platform-*`;前端只消费后端或本地宿主投影。 - 权威文档:[AGC 后端框架整理与演进路线](../../technical/【技术方案】AGC后端框架整理与演进路线-2026-09-18.md)。 ## 2026-09-17 AGC 抠图提交使用远端画布项目身份 - 背景:AGC 已通过本地项目 ID 建立并持久化本地项目到主站远端画布项目的绑定,但 `agc_remove_background` 提交请求仍把本地 `manifest.project_id` 放入 `projectId`;`assetFolderId` 已使用远端素材目录 ID。主站因此按项目不存在或不属于当前账号返回 404,主站抠图和 BgFilter 本身均正常。 - 决策:抠图请求及工具回执统一使用 `prepare_external_canvas_generation_context` 返回的远端 `context.project_id`;本地 manifest 项目 ID 只用于绑定键和本地状态,不得作为主站业务请求的 `projectId`。 - 验证:客户端定向 Rust 测试、格式、编码和 diff 检查通过;未修改主站路由或 BgFilter。 ## 2026-09-17 图集切分模式改为显式声明 ## 2026-09-17 DirectProject 首屏历史锚点只认订阅回执的 lastCompletedItemId - 背景:ADR「首屏历史由 `subscribe` 返回的 `lastCompletedItemId` 锚定,再取最近切片」只落了一半。`DirectThreadManager` 是搬运层,内存里没有「已完成条目」的锚点,`subscribe` 一律返回 `last_completed_item_id: None`;`commands.rs` 的 `subscribe_direct_project_thread` 在为空时用 `read_direct_project_last_item_id_at` 从磁盘回填,所以线上回执里的值是真的(订阅那一刻文件里最后一条可显示条目的原始 item id)。前端侧:首屏一直在 `loadProjectConversation` 里用 `beforeItemId: null` 直接取文件尾一屏,`lastCompletedItemId` 自 `1b40f030e` 起不再被任何代码读取。 - 决策(锚点语义):首屏切片的新端(较新一侧)边界就是这个锚点,**含锚点条目本身**;切片命令新增 `throughItemId` 参数表达「取到这条为止」。比锚点更新的条目只从运行态事件来,历史切片与实时流因此不重叠(原来的文件尾读取会把订阅回执之后才完成的条目也拉进历史,与运行态事件同 id 重叠,只靠前端合并兜住)。 - 决策(读取时机):订阅回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏。手动重读保持「按当前文件尾取尾屏」的恢复语义,不锚定。 - 决策(翻页不变):向后翻页仍用切片返回的 `firstItemId` 作 `beforeItemId`(不含锚点),`hasMore` 与连拉口径不变。 - 影响范围:`agent/direct_project_history.rs`(切片锚点 + `through_item_id` 参数)、`commands.rs`(`read_direct_project_history_slice` 命令参数)、AGC 前端首屏读取接线与测试骨架。**未改**:DirectRuntime 的 `turn-stream.jsonl` / `tool-calls.jsonl` 写入与进度事件、`list_game_creator_direct_active_turns`、SpacetimeDB 与 HTTP 契约。 - 验证方式(已跑):Rust 侧 `cargo test agent::direct_project_history`(22 passed,含「窗口取到锚点那条、排除比锚点更新的条目、`beforeItemId` 与 `throughItemId` 互斥报错」三类用例);前端 `npx vitest run .../directHistoryAnchorGate.test.ts`(10 passed)与 appSurface 的 `anchors the first history page at the subscribe receipt instead of the file tail`(全量 475 tests / 457 passed / 17 skipped;唯一失败 `edits the published runtime config without leaking API keys into chat` 与本次改动无关,stash 掉本次前端改动后同样变红);`tsc` / ESLint / prettier / `check:encoding` / `check:doc-index` / `git diff --check` 全绿。变异验证:闸门忽略「已消费」、首屏不等闸门两处改动各自让对应用例变红。 - 关联文档:[ADR](../../adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md)、[里程碑](../plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md)、[实施计划](../plans/【实施计划】DirectProject聊天真相源收敛-2026-09-16.md)。 - 决策:`sliceMode` 在图标图集生成入口成为必填字段且不保留任何默认值。省略、`null` 或空字符串必须在引用解析、定价、入队和 provider / OSS 副作用之前返回 `400`(`field=sliceMode`);`grid` 必须同时提供 `gridX`/`gridY`,`connected-components` 不得携带网格尺寸,二者矛盾同样在副作用前失败关闭。 - 决策要求:只有用户或需求明确要求等分网格、固定槽位或指定行列数时才使用 `grid`,且行列数必须来自该需求;自由排布、数量不定或只要求一张图集时显式传 `connected-components`,需要约束素材张数时用 `sliceCount`,不得用网格参数表达张数,也不得用固定 `2×2` 表达“四类素材”。 - 影响面:平台两个图集生成入口(`/api/editor/...` 与 `/api/external/v1/editor/...`)、OpenAPI、画板 Agent 工具、画板前端提交计划、AGC 客户端 MCP 工具说明与桥接校验、AGC 原生工具 schema 与观察器、AGC Skill 与外部编辑器 Skill。 - 迁移影响:省略 `sliceMode` 的旧调用方(含已发布但未更新的 AGC 客户端和第三方外部 API 调用方)会在图集生成上收到 `400`;本次同时把仓库内自有调用方改为显式声明,不为旧客户端保留兜底分支。 - 错误可执行性:缺失、空白、未知取值都以 `400` + `field=sliceMode` 返回允许取值和决策分支,`grid` 缺维度提示 `sliceCount` 才是张数约束;`sliceCount` 的公开契约上限与切片上限统一为 `256`(识别数量与目标不一致返回 `422` 并回报实际数量)。 - 反馈闭环:图集生成结果回显生效的 `sliceMode`/`gridX`/`gridY` 与 `slicePaths`;严格图集提交前必须证明平台回显的模式(`grid` 时含行列数)与请求显式声明一致,缺失或不一致一律失败关闭。 - 标准美术包:客户端显式声明 `sliceMode=connected-components` + `sliceCount=4`,本地按用途位置写四张 canonical 切片前再次校验数量正好为四,数量不符时失败关闭,禁止截断或补位。 - 测试环境:在提权 shell 的 Windows 主机上,`%TEMP%` 下新建目录的默认所有者是 `BUILTIN\Administrators` 而不是当前 TokenUser,AGC 的所有者校验会拒绝测试自己创建的项目根;测试构建对该情形(仅限 `%TEMP%` 内、且失败原因为所有者不匹配)先按“本调用创建的对象”初始化所有者后重试,临时目录之外的越权所有者继续失败关闭。 - 权威合同:[画板图标素材生成入口设计](../../【编辑器】画板图标素材生成入口设计-2026-06-15.md)。 ## 2026-09-17 `agc_tools` 媒体资源提示词上限收敛为单一口径,并按 kind 暴露给模型 - 背景:有人反馈「客户端没法由 agent 调用图片快速编辑功能以及背景音乐生成功能」。核查后工具本身都在(`agc_edit_image` / `agc_create_or_derive_resource`),图片快速编辑在 2026-09-14 的真实项目日志里也有成功记录;但存在三类真实缺陷:① `agc_create_or_derive_resource` 的 `prompt` 在 schema 里只声明 4000,真实上限却是按 kind 分的(背景音乐 140、音效 1900、视频/角色动画 4000、图片 32000),MCP 层还额外写死了一条 140 判断,模型从 schema 与 skill 都看不出 140/1900,写一句正常长度的背景音乐描述就当场被拒;② 客户端 UI 用同一口径但会截断并提示,agent 侧却只有硬拒,形成「UI 能做、agent 调不动」的观感;③ `sourceLocalAssetId` 不是已登记资源时只报「不属于当前项目已登记资源」,模型会原地重试而不会先登记。 - 决策一(单一口径):提示词上限只由 `resource_edit_prompt_max_chars` 给出,MCP 工具层、客户端受控工具桥与提交校验全部从它取数;超限文案复用 `resource_edit_prompt_limit_error`,保证模型看到的数字就是真实生效的数字。传输层边界只在信封级生效,不再用一个更小的通用常量先于按 kind 上限误报。 - 决策二(按 kind 暴露):`agc_create_or_derive_resource` 的 schema 用 `allOf[oneOf]` 逐 kind 声明 `prompt.maxLength`(background-music / sound-effect / video+character-animation),顶层 `maxLength` 等于各 kind 上限的最大值,`prompt` 描述里写明每个数字;`agc_edit_image` 继续用图片口径 32000。skill 包 `agc-client-projection`(SKILL.md 与 `references/projection-contract.md`)同步写明四个数字,并说明超限要在本地收敛而不是原样重发。 - 决策三(可执行的前置提示):源资源未登记时统一返回「先用 `agc_list_registered_assets` 选已有 localAssetId;文件只在项目里时先用 `agc_list_project_files` 确认 `assetImportable=true`,再用 `agc_import_account_assets.localPaths` 登记后重试」。本轮不放开「已完成任务产物」在 agent 侧的隐式正规化:登记是带副作用与 revision 推进的事务,必须由模型显式发起。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`(上限与文案的唯一口径)、`agent/direct_tool_bridge.rs`(按 kind 判定与未登记源资源提示)、`agent/direct_tools_mcp.rs`(schema 与校验)、`resources/agc-skills/agc-client-projection/**` 与清单指纹(version `2026-08-26.18`)。**未改** `/api/external/v1` 契约与 OpenAPI、SpacetimeDB schema、前端 TS 侧 `resourceEditPromptMaxLength` 数字、客户端 UI 行为。 - 验证方式:新增 `tool_prompt_limits_agree_with_the_client_authority`(四个 kind 的 schema 上限、MCP 校验与客户端权威口径同数字,超限文案带真实上限)、`bridge_resource_prompt_limits_follow_the_client_authority`(工具桥侧同类门禁,含图片编辑的 32000 边界)、`edit_image_tool_reaches_the_platform_image_edit_route` 与 `background_music_tool_reaches_the_platform_audio_route`(MCP 工具层 → 真实工具桥 → 假平台,断言 `/api/editor/images/edits` 与 `/api/editor/audios/background-music/generations` 的路径、Bearer、Idempotency-Key、正文与派生资源落盘,图片编辑正文不得回填 assetKind)、`background_music_prompt_over_the_limit_is_rejected_before_any_bridge_call`(超限在桥请求之前失败)、`unregistered_source_reports_the_registration_follow_up_tools`;`agent::direct_tools_mcp` 22 passed、`agent::skill_pack` 4 passed、`agent::direct_tool_bridge` 17 passed(7 条本机既有失败见下)、`npm run agc:skill-pack:check` 与 `skill-pack:test` 通过。本机 `tempfile::tempdir()` 归属校验失败导致的既有用例(`project::resource_editor` 45 条、`agent::direct_tool_bridge` 7 条)在本轮改动前后**同为失败**(stash 基线复跑确认),与本次无关。 - 关联文档:[AI游戏创作智能体App实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、[踩坑记录](pitfalls.md)。 ## 2026-09-17 资源画布支持引擎资源只读预览 - 背景:Cocos Creator 工程里已有的引擎资源(模型、动画、预制体、材质、图集、压缩纹理…)此前在发现层就止步:`.glb` / `.prefab` / `.anim` / `.texture` 等扩展名既不可登记,也不进资源画布,工程导入后画布上只看得到位图、音频与脚本。 - 决策(范围):本轮只做**只读预览**。引擎资源可以被发现、登记进 manifest、进入资源画布并按类型出预览;不承接编辑、派生、生成与回写,也不解码引擎私有容器(`.texture` / `.cubemap` / `.rt` / `.skel` / `.dbbin` / `.psd` / `.exr` / `.pcm` 只出类型卡)。 - 决策(契约):**不新增 manifest 契约字段、不新增 canonical kind、不新增画布分类轴**。引擎资源复用既有 kind(模型/场景/预制体/地形 → `scene`,动画 → `character-animation`,材质/特效 → `code`,图集与容器 → `document`,图像容器 → `image`,裸 PCM → `audio`),避免 `deny_unknown_fields` 让旧客户端读不出整份 manifest;引擎语义由**类型角标**(模型 / 动画 / 材质 / 图集 / 纹理…)表达,不复用「图片 / 文档」。 - 决策(卡面与读取):新增三个卡面分支 —— `model`(`.glb` / `.gltf` / `.fbx`,由单例 WebGL 渲染器出缩略图,整页只保留一个 WebGL 上下文)、`structured`(Cocos 序列化资源的结构摘要,非 UTF-8 变体降级成类型卡而不是报错)、`binary`(不发起任何读取,不占预览读取槽)。图像容器(`.tga` / `.tif` / `.tiff` / `.hdr`)先在原生侧转码成 PNG,再走既有图片预览链路。 - 边界:`.meta` 等引擎导入侧车文件仍然只可发现、不可登记;发现层新增 `model` / `binary` 两个**发现类别**(不是 manifest kind)。多文件 glTF(外部 `.bin` / 贴图)与超限模型降级成类型卡;预览管线既有语义(可见性门禁、3 槽并发、LRU 预算、取消与重试口径)不变。 - 决策(发现过滤):引擎工程的 `library/` / `temp/` / `profiles/` / `local/` 不再进发现结果,判定收窄为「工程根直接子目录 + 当前目录确实是 Cocos Creator 工程(`package.json.creator.version` + `assets/`)」。**不放进全局跳过表**:这些名字在别的工程里可能是真实源码目录。过滤落在唯一一份目录遍历(`list_local_project_files_at`)上,因此 Agent 发现、前端资源树与提示词里的未登记清单同步生效;项目快照 / 版本指纹 / 检查点的语义本轮不动。 - 决策(模型上限与缓存键):模型预览字节上限从 16 MiB 放宽到 32 MiB,与通用媒体预览取同一上限(base64 载荷约 43 MiB);超过上限仍是类型卡,不做半渲染。缩略图缓存键改用**稳定身份**(资源身份 + 路径 + 字节数)而不是 blob URL:预览缓存淘汰后重读同一模型不会重新解析 + 重新渲染。要再往上放宽,必须先把预览载荷换成 Tauri 原始字节通道。 - 决策(模型放大预览):模型卡可以在工具条打开「3D 预览」独立浮层,浮层内是**交互式视角**(OrbitControls:左键旋转 / 右键或中键平移 / 滚轮缩放 / 复位视角),与三维建模软件同一套操作习惯。加载 / 取景 / 释放三条口径抽到共用模块 `resourceModelScene`,缩略图与浮层不许各写一套;画布上的卡片仍然是静态缩略图并继续共用**唯一**一个 WebGL 上下文,只有打开浮层时才新建交互式上下文,关闭即 dispose。浮层仍是只读预览:不写 manifest、不参与编辑与派生。 - 验证:`cargo check`、`cargo fmt --check` 通过;`cargo test … cocos` 9 条通过(发现分类 / 登记 / 提示词投影 / 插件门禁);`cargo test … resource_inspect::tests` 7 条通过(含结构化预览与二进制降级、TGA→PNG 转码、模型签名判定);`cargo test … agent_asset_import_tests` 9 条通过;生成目录过滤用例通过(引擎工程过滤、非引擎工程不过滤);`tests/resourceCocosPreviewContract.test.tsx` 7 条通过(含模型卡渲染不可用时的降级、稳定缓存键);`npx vitest run apps/ai-game-creator-shell/tests/resource apps/ai-game-creator-shell/tests/project` 52 文件 / 543 用例通过;`appSurface.test.ts` 450 通过 / 20 跳过;app `tsc --noEmit`、`npm run check:encoding`、`git diff --check`、`npm run check:doc-index` 通过。 - 真机验收(2026-09-17 补):在真实客户端内打开一个含模型 / 动画 / 序列化资源 / TGA / 引擎容器的 Cocos 夹具工程,模型卡出三维缩略图、序列化资源出结构摘要、TGA 出转码后的真实图片、引擎容器出类型卡;同现场 `list_local_project_files` 对根级 `library/` / `temp/` / `profiles/` 返回 0 条、`assets/library/` 正常列出。证据见里程碑文档「证据要求」。 - 未验证:本机其余仍用 `tempfile::tempdir()` 的既有 Rust 用例继续被 `Windows 安全对象不属于当前用户` 阻断(与本决策无关;根因与手工夹具相同,已记入 `pitfalls.md`)。 - 关联文档:`docs/project-memory/plans/【里程碑】资源画布支持引擎资源预览-2026-09-17.md`。 ## 2026-09-16 抠图模式与背景色契约 - External v1 抠图和 AGC `agc_remove_background` 支持 `complex`(语义分割识别前景)与 `flat`(纯色背景抠图);明确纯色背景优先 flat,模式缺省仍为 complex,主站前端保持现有行为。 - flat 的颜色允许 `auto`、`#RRGGBB` 或省略,自动识别完全由 BgFilter 负责。主站只校验、透传,不调用视觉模型选色;complex 携带颜色、非法值和空字符串在入队前拒绝。 - 来源、名称、模式与颜色共同区分客户端请求意图;旧参数调用及旧 External 请求的幂等指纹须保持稳定。 - 权威合同:[AGC 抠图模式与背景色透传](../../technical/【技术方案】AGC抠图模式与背景色透传-2026-09-16.md)。 ## 2026-09-16 策划 Agent 工具执行退出项目级写锁并自动接续中断批次 - 背景:策划 Agent 每个 `read_file` / `write_file` / `patch_file` 工具都在执行前竞争全局项目写锁,但同一会话已由 `.agent/design-agent/active.lock` 串行化,工具目标又限定在 `design_artifacts`;项目锁既不覆盖「工具 + 会话 checkpoint」事务,还把进程中断时的 `executing=true` 不确定窗口扩大到等锁与工具执行全程。真机项目出现 `pendingBatch.executing=true`、`function_call` 无配对 output、UI 只显示工作中且无错误的状态。 - 决策:单次策划工具不再竞争项目级写锁,只保留策划命令锁与既有原子写入;GameAgent / DirectProject 的公共项目锁实现与调用不变。重开项目 hydrate 时,若命令锁可获取且当前批次处于 `executing=true`、当前 call 无 output,则自动续跑原回合:为该 call 补写「执行结果未保存」的工具错误、跳过剩余调用并交回 Provider 自愈;不得重放文件副作用,也不要求用户手动重试。 - 验证:新增定向用例证明中断批次自动补齐工具 output、收到后续 assistant 回复、清空 pendingBatch 并结束原 turn,同时目标文件保持未修改(未重放 `patch_file`);策划 Runtime 定向 14 条、策划工具 3 条通过,`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。 - 关联文档:[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。 ## 2026-09-16 AGC 同 AppData 多窗口共享 Agent Runner - 背景:双击或再次启动 AGC 客户端时报「应用启动失败」,启动日志为 `startup.runner.owner-lock.failed details=AI 游戏创作界面已由同一 AppData 目录中的其他进程运行`。原设计(2026-07-27 / 2026-08-23)要求同一 AppData 只有一个 GUI owner,第二个界面进程在 setup 阶段就失败退出。 - 决策(锁语义):`agent-runner.gui-owner.lock` 改为**界面参与锁** `agent-runner.gui-participant.lock`,以共享句柄打开,同一 AppData 的任意数量窗口可同时持有;Runner 的启动检查、attach 门禁与 watchdog 只用“能否独占取得该文件”判断是否仍有窗口存活。全部窗口退出后才关停 Runner 并清理 endpoint。 - 决策(claim 采纳与发布):窗口启动先**采纳**durable claim(同一 epoch/revision),只有 claim 缺失或不可读才发布新 claim;登录、refresh、退出或换号才发布新 claim(新 epoch + 本窗口 revision),成为新的登录态权威。同一 claim 的重复 attach 是幂等空操作,不再清空 Runner 登录态;只有 epoch 变化或携带明确登出参数才允许替换 / 清空。并发发布以最后一次成功写入的 claim 为准,落败窗口按最新 claim 有界重试。 - 决策(事件与退出):manifest 失效与 Runtime update relay 的接收端从单槽改为按 `event_sink_token` 去重的注册表并广播,发送失败只淘汰该接收端;GUI 退出先释放本窗口参与锁,仍有其它窗口时保留 Runner(`agent.runner.gui_exit.retained_for_other_windows`),最后一个窗口才请求关闭。Runner 启动失败时先按最新 endpoint 复用一次,避免两个窗口同时冷启动时的实例锁竞争被误报成启动失败。 - 边界:本机 GUI ↔ Runner 协议方法与参数不变,不引入多 Runner、不做跨 AppData 会话共享;项目级 `.agent/project.lock` 不变,多窗口仍不能并行写同一项目;平台登录态 generation 单调与 claim 失配失败关闭语义保持不变;混用新旧版本二进制访问同一 AppData 不属于支持场景。 - 验证:定向 Rust `runner::tests::gui_owner_*` 11 条与新增的参与锁多窗口 / 存活判定 / claim 采纳与轮换 / 同 claim 第二个窗口不清空登录态用例全部通过;真实 debug 二进制 Windows smoke 证明同一 AppData 两个 GUI 都完成 `startup.setup.complete`、只存在一个 `--agent-runner` 进程、关闭一个窗口后另一个窗口与 Runner 继续存活、最后一个窗口退出后 Runner 退出并删除 endpoint;`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过。 - 未验证 / 已知环境问题:真实安装包双开需要重新构建发布后才能验证;`durable_provider_handoff_prevents_shutdown_even_when_corrupt`、`durable_provider_retry_prevents_shutdown_and_reopens_writes` 两条用例在本机改动前的基线上即失败(Windows 安全对象 owner 校验与 Provider 请求重复),与本决策无关。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(2026-09-16 节)、`docs/project-memory/plans/【里程碑】AGC同AppData多窗口共享Runner-2026-09-16.md`。 ## 2026-09-16 DirectProject 三维请求解除 Phaser 固定约束,由 Codex 自选技术栈 - 背景:三维需求("做个 3D 城市游戏")在只有 Phaser 4 二维通道的工程里没有落点,而完成判定以"源码引用已登记平台图片 + 浏览器渲染到该图"为硬门禁,模型可推进的唯一动作退化成生成图集;真机侧表现为长时间生图与反复接线,等轴伪 3D 成了默认交付。产品决定不再用"先澄清引擎"卡住三维请求,改为放开选型。 - 决策:用户消息表达三维(3D / 三维)且没有点名引擎时,本回合注入**自选技术栈合同**:不受"新 Web 游戏固定 Phaser 4.2.1"约束,由 Codex 自行选择三维技术栈(Three.js、Babylon.js 等 npm 运行时,或当前工程自带的引擎),可以按需新增 npm 依赖、调整工程结构,并在回复里说明选型;客户端不要求先澄清、不阻断任何工具、不拒绝登记产出。 - 决策(常驻口径):`DIRECT_AGC_ENGINEERING_GUIDANCE` 的 Phaser 固定约束按二维/三维分层——二维游戏仍是 Phaser 4.2.1,三维请求不受该约束。首页回合只加一行三维提示,仍允许按既有规则创建项目。 - 边界:用户点名引擎时沿用既有工程合同(Cocos / Unity / Godot 等与当前目录不匹配时先说明不匹配);用户主动选择"等轴 / 伪 3D / 2.5D"或话题是代码里的三维概念时不注入合同。唯一保留的红线是不得用等轴伪 3D 或二维图集冒充三维交付而不说明。识别只读用户原文,不改写消息、不触发额外工作流,也不做工具阻断或注册门禁。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs`。**未改** `/api/external/v1` 契约 / OpenAPI / DTO、SpacetimeDB schema、AGC 工具桥行为、前端投影与资源工作台。 - 验证方式:`three_dimensional_game_request_frees_the_engine_choice`、`explicit_flat_presentation_requests_do_not_trigger_three_dimensional_selection`、`named_engine_requests_keep_the_existing_engineering_rule`、`three_dimensional_contract_reports_the_current_project_engine`、`home_three_dimensional_note_keeps_project_creation_available`、`system_prompt_is_bounded_and_declares_direct_runtime`,以及 `agent::direct_tools_mcp` 17 passed;`cargo fmt --check` 通过。本机临时目录 owner ACL 与进程用户不一致,涉及 `init_local_game_project_at` 的既有用例(含未改动模块)在本机无法执行,全量分片与真机 smoke 未在本轮取得。 - 关联文档:[里程碑](../plans/【里程碑】Direct三维请求自选技术栈-2026-09-16.md)、[实施计划](../plans/【实施计划】Direct三维请求自选技术栈-2026-09-16.md)。 ## 2026-09-17 DirectProject 历史分页的「可显示」口径定为前端回合反馈,一次翻页连拉上限 5 页 - 背景:ADR 与里程碑要求「一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页」,但实现只落地了后端锚点与文件尾回扫,前端仍是单发一页。历史切片的 `limit` 按原始条目计,一整页全是工具卡片 / 思考文本且落进同一个已渲染回合的折叠「执行过程」时,用户点「显示更早的对话」看不到任何变化。 - 决策(可显示 = 出现新回合):一次翻页操作连续取页,停止判据是**合并后聊天投影的回合数增加**(出现新的用户气泡)。工具卡片与思考文本虽然能通过 `projectDirectThreadItem`,但它们可能整页落进已渲染回合的折叠过程区,不构成用户可见反馈。 - 决策(粒度和上限):每个用户操作最多 5 次请求,首屏那次算第 1 页、之后每次点击重新计数;每页仍是 `limit = CONVERSATION_VISIBLE_STEP`,上限只约束请求次数,不改页大小契约。 - 决策(硬性终止):`items` 为空、`firstItemId` 为 null 或与请求锚点相同 → 立即停止,不靠 5 页上限兜底。 - 决策(落点):口径与循环只在 `features/project-workspace/directHistoryPaging.ts` 一份实现里,首屏与「显示更早」共用;`DIRECT_HISTORY_MAX_PAGES_PER_ACTION` 放 `app/constants.ts`。 - 边界:循环内只累积、结束后一次性并入聊天 state;某页失败时保留已成功页并沿用现有报错文案;切换项目时丢弃整批;不新增 loading / 禁用态与新文案,不做滚动锚定补偿;运行中回合允许连拉历史。 - 验证:`directHistoryPaging` 单测 8 条覆盖口径、上限、`hasMore=false` 早停、锚点不前进、单页失败;`project-development.suite.ts` 新增「跨页同回合」集成用例作为真实回归网(变异验证:把判据退化成「有可渲染条目就停」后该用例变红);`tsc` 与 `appSurface.test.ts`(470 tests / 17 skipped)全绿。 ## 2026-09-16 图标图集自动拆图上限提高到 256 - 背景:AGC 图标图集自动连通域识别在一次生成中识别出 86 个区域,原有 64 片上限在后处理阶段阻断了请求;该上限同时影响 api-server 自动 / 手动切片、SpacetimeDB 批量落库和统一生成结果 item 数量。 - 决策:将可输出独立切片上限统一提高到 `256`;统一生成结果最多 `258` 个 item(256 个切片加 provider 原图和透明整图)。保持原始连通域 `4096`、总裁剪像素、CPU / 内存 admission、并发上传和处理时限不变。 - 边界:超过 256 仍按现有 `output-slice-limit-exceeded` / `sliceWarning` 语义失败关闭切片写入;自动路径保留可信整图,手动路径继续在持久化前返回错误。 - 验证:平台切片器、api-server 警告映射与 payload、SpacetimeDB 结果 / 批次校验均覆盖 256 成功边界和 257 溢出边界。 ## 2026-09-14 生成进度面收敛为「常驻可折叠任务侧栏」;提交即关面板、阶段文案只归侧栏;定位动作终局化 - 背景(验收人在真机上连报三条):① 提交按钮上渲染了后端 `phaseDetail`,「生成图片」的主按钮变成写着「排队中。」的状态胶囊;② 提交后提交面板不关、一直占着屏幕等生成,用户原话「不要显示排队中,点生成直接把窗口藏起来啊,你留个窗口意义何在」;③ 任务进度面是一个工具条按钮 + 非模态浮层,跟网页端美术画布的任务侧栏不是一个形态,用户原话「你把美术画布的照抄过来都不会吗」;④ 「定位到素材」点了没反应,提示条永久停在「正在定位生成的素材…」。 - 决策(提交面板):**点「生成」即同步关闭面板**——不等 IPC、不等排队、不等生成;面板内**不出现**任何阶段文案(主按钮文案恒为动作名)。**只有「点击瞬间就失败」**(后端校验 / 权限拒绝 / start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在任务侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消(请求挂在任务与项目内账本上,不挂在面板生命周期上)。 - 决策(进度面形态):改为**常驻画布的可折叠任务侧栏**(对齐网页端 `ImageCanvasTaskSidebarView`)——展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 提示「仅显示最近 N 条」),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其 border-top 分割线已删除);折叠即整块让出画布、**不留贴边把手**;每项显示状态徽标 / 后端阶段文案 / 已耗时(前端 1s 计时)/ 素材名 + 「定位到素材」;侧栏非模态(不铺遮罩、不做焦点陷阱、不进模态遮挡判据),工具栏入口按钮是**唯一**开合口、常驻(资源管理 / 运行两个页签都在)并显示 `生成任务 · N`,提交受理后自动展开。**原先的非模态浮层形态已删除,不留平行入口。** - 决策(侧栏失去焦点即收起):展开时挂 document 级 `pointerdown` 捕获监听,点在侧栏内部与工具条那枚开合按钮以外的地方即收起。两处必须排除——**侧栏内部**(点任务卡、点「定位到素材」不能收起侧栏)与**开合按钮本身**(它自己负责 toggle,若也被判成「点外部」就会先收起再被 toggle 打开,表现为按钮失灵;按钮带 `data-resource-generation-task-toggle` 标记供排除)。用 `pointerdown` 而不是 `click`:画布空白处的左键 pointerdown 会 `preventDefault()`,document 上的 click 收不到那一次点击。 - 决策(收起动画):收起不能瞬间卸载——进场有动画而消失没有,观感上是"闪一下没了"。做法是**组件自己留一帧播退场**:`open` 变 false 后进入 `leaving`,根节点挂 `is-leaving` 播 `…-leave`(与进场同向反向,160ms,`pointer-events: none`),播完(或减动效偏好的 0ms 定时)才 `setPhase('idle')` 卸载。时长在组件与 CSS 两处各写一次,**必须一致**(组件导出 `RESOURCE_CANVAS_ASSET_GENERATION_TASKS_LEAVE_MILLIS` 并在用例里钉住)。退场期间**沿用收起前那一份列表**(`lastRenderedRef`):宿主会在同一帧里收起侧栏并把在途任务收口成已完成,直接吃新 props 会让退场动画里的内容跳一下;空态分支也必须读这份冻结快照,不能读实时 `ordered`。 - 决策(定位必须让素材真的可见):`handleResourceSelect` 只改选中、不会移动画布,而这张画布是 **transform 平移**的、资源卡不在任何滚动容器里——`card.scrollIntoView()` 碰不到滚动祖先,卡在视口外时"定位过去了但依然见不到素材"。所以聚焦链在选中之后必须**显式把画布视口居中到该卡**(`centerResourceCanvasOnResource`:读该卡在当前位置表里的 `x/y` 与 `resourceCardSizeByResourceId` 的尺寸,按 `canvasSize / 2 − 卡中心 × scale` 求平移量,**缩放保持不变**,与 `ensureResourceBookContentVisible` 的既有口径一致:定位不改用户的缩放预期)。`scrollIntoView` 一并保留(栏目页仍有带滚动条的祖先)。 - 决策(侧栏位置):挂在画布**左侧、标题栏之下、工具栏之上**,宽 300px、**覆盖式**(不 reflow 挤窄画布视口)。理由:右侧已被「智能创作」对话面板占用、顶部是栏目标题栏、底部是栏目工具栏;覆盖式不触碰画布视口数学与资源卡排布,收起即完全让出画布。若产品要求「画布被挤窄」的 flex 兄弟列形态(网页端是那种),需要改 `game-resource-book-manager` 那段布局并单独排期。 - 决策(定位终局化):根因是聚焦 effect 的依赖全是画布自身状态,**手动点定位不改其中任何一项** → effect 不重跑、`pendingResourceFocusRef` 无人消费、提示条永久停在中转文案。修法:新增聚焦请求序号并加入 effect 依赖;handler 重写为「能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给『素材已不在项目里 / 已登记但尚未同步』的结论;挂 intent 后推进序号 + **3 秒有界兜底**;intent 被判 invalid 时也给『定位请求已失效』」,并修掉「已聚焦过」提前返回分支不清提示的同类问题(自动落卡那条链同源)。 - 影响范围:`src/features/resource-canvas/{ResourceCanvasAssetGenerationPanelView.tsx,ResourceCanvasGenerationPanelView.tsx,ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTaskModel.ts,resourceCanvasAssetGenerationQueue.ts}`、`src/view/project-development/index.tsx`,测试 `tests/{resourceCanvasAssetGenerationBackgroundClose.test.tsx,resourceCanvasAssetGenerationTasksPanel.test.tsx,resourceCanvasBottomToolbar.test.tsx,appSurface/project-development.suite.ts}`。**未改** IPC 形状与 Rust 生成通道、未改本地排队语义(仍单条在途)、未改 `packages/**`。 - 验证方式:`appSurface.test.ts` 439 passed、定向 7 文件 93 passed、侧栏用例 7 passed;变异验证(均已实测):① 提交后不关闭面板 → 面板用例 `expected "spy" to be called 1 times, but got 0 times` 与 AppSurface `expected
to be null` 红;② 去掉即时失败重开 → `Unable to find role="dialog" and name "生成 UI 设计图"` 红;③ 去掉提交后自动展开 → `Unable to find an accessible element with the role "region" and name "生成任务"` 红;④ 折叠顺手清空任务列表 → `expected '0' to be '1'` 红;⑤ 去掉聚焦请求依赖 → `expected null not to be null`(卡片从未被选中)与 `expected to be null`(提示条仍停在中转文案,即用户报的现象)红。 - 关联文档:[栏目画布底部工具栏入口矩阵](../../technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)、[项目开发工作台 PRD](../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)、[踩坑记录](pitfalls.md)。 ## 2026-09-14 放开 AGC 手工图片生成的本地并发:durable 输出槽身份改为「精确动作指纹」 - 背景:AGC 手工图片生成的 durable 输出槽身份是 `run_id = slot-`,而工具栏除「图标规范」首次外 `outputPath` 恒为 `null`、`requireSlices` 恒 false → 同项目所有图片类生成共用一个槽,第二条并发请求在任何远端 POST 之前就被拒(`external_generation_state.rs` 的 singleflight 报「durable 图片生成输出槽已有请求执行中」)。验收反馈里的高优问题(生图期间不能退出、也不能在生成 A 的过程中生成 B)既要前端可退出,也要后端具备并行能力。 - 决策(槽身份 = 精确动作身份):`run_id = slot-`,材料为 `prompt / output_path / aspect_ratio / image_size / asset_kind / asset_label / replace_existing / require_slices`(`canvas_generation.rs`)。不同 prompt 或素材名 → 不同槽 → 不同进程锁键与不同 `.lock` 文件 → 可同时在途。**不用随机 uuid**:随机身份会让「同一精确动作重放」落到新路径,必须再造一层 action→ledger 索引才能保幂等;用动作指纹让「槽身份 ≡ 精确动作身份」,路径查找即幂等查找。 - 决策(幂等不变):同一精确动作 → 同一路径 → 命中已有 prepared/accepted 账本并复用原 `idempotencyKey` / `operationId`,不二次 POST;相同动作并发仍被拒的既有语义保持。 - 决策(旧槽账本最小懒迁移):旧槽账本形状可读、不 panic、不 fail-closed;在 durable guard 之后、任何远端 POST 之前,**仅当**旧槽账本的 `agentId / runId / actionFingerprint` 与本次精确动作一致时,把它迁移到新路径(保留 `idempotencyKey` / `operationId` / 状态)并删除旧文件;属于其他动作的旧账本一律不动。旧「固定槽」(`run_id == agent_id`)账本的 fail-closed 拒绝保持原样。 - 当前身份边界:精确动作指纹包含 `slice_count`,Agent 图片工具同样使用 standalone 动作槽;不同精确动作可并行,同一动作在容量排队前持有原跨进程槽锁。不得再依据早期“Agent 不走 standalone 槽”的说明拆除幂等或重复提交。 - 前端口径:本批**仍保留单条在途的前端排队**(提交节流),真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs`、`.../external_generation_state.rs`。**未改** `/api/external/v1` 契约 / OpenAPI / DTO,未改 `recovery_scan.rs`(身份白名单与孤儿清理语义不变),账本 schema 仍是 v3、字段集不变,只改 `runId` 取值来源。 - 验证方式:新增 `standalone_generation_binds_each_exact_request_to_its_own_stable_slot`、`distinct_standalone_actions_hold_independent_durable_output_slots`、`concurrent_distinct_standalone_generations_both_succeed_with_one_post_each`(端到端:两条 `outputPath=None` 的不同动作要求两条 POST 同时到达,各自 poll → read-url → 下载 → 落盘)、`legacy_output_slot_ledger_is_adopted_by_the_same_exact_action_only`。变异验证(已实测):把 `run_id` 退回旧公式 → 4/4 红(含「durable 输出槽身份必须等于该精确动作的身份」与并发用例的「任何远端 POST 前拒绝并发请求」);把懒迁移短路 → 旧账本用例红。定向 `agent::generation::` + `recovery_scan` 95 passed、`direct_runtime media` 195 passed。 - 关联文档:[栏目画布底部工具栏入口矩阵](../../technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)、[踩坑记录](pitfalls.md)。 ## 2026-09-14 「素材类型」从「编辑素材标签」面板拆成独立入口 - 背景:2026-09-11 的决策把类型选择器放进「编辑素材标签」面板(与标签同一次保存、同一条写入路径,不在工具条另开入口)。真机使用暴露两个问题:① 那排 chip 在弹窗里没有任何标题,读不出是什么;② 类型改动**没有自己的提交动作** —— 点 chip 只写本地 state,落盘发生在底部「添加」上(那是标签语义的按钮),只选类型后直接关弹窗会静默丢失。 - 决策(入口独立):新增 `ResourceTypePanel`(标题与 `ariaLabel` 均为「设置素材类型」),**选中即落盘**(一次动作一步完成),`category` 传用户选中值、`tags` 传 `gameCreationAppAssetTags(asset)` 的落盘原值。入口两处:选中卡浮动工具条「素材类型」按钮 + 信息浮层「分类」行的「设置」。 - 决策(选项形态,真机反馈后修订):选项区**必须是纵向单选列表**,一行一个(容器 `role="radiogroup"`、每项 `role="radio"` + `aria-checked`,选中态与读屏共用 `aria-checked` 并由 CSS 直接驱动;roving tabindex + 方向键移焦点、**Enter/Space 才落盘**)。真机上第一版用了共享 `PlatformSegmentedTabs`(`columns="threeToSix"`)→ 6 个选项挤成一行互相叠字,验收人原话「做成列表,而不是全都一条」。**有意偏离 APG**:方向键不顺手选中——本面板「选中 = 一次 CAS 写盘 + 宿主收窗」,方向键即选中会让浏览 6 个选项变成连环写盘、第一次按键就关窗。行骨架复用共享 `PlatformNavigableListItem`(未复制共享 UI、未改 `packages/**`);列表 `max-height: min(320px, 40dvh)` + 独立滚动,标题/素材名/错误提示不滚,行高 44px 移动端优先。这一版值得后续抽成 `packages/shared` 的 `PlatformRadioList`(或给 `PlatformSegmentedTabs` 加 vertical 档),本批按边界未做。 - 决策(标签面板去掉类型控件):「编辑素材标签」面板删除 chip 与 `categoryChoice` 分叉,保存时 `category: gameCreationAppAssetPersistedCategory(asset)`;「没碰过分类就回传落盘原值」这条不变量改为**结构性保证**(面板里根本没有类型控件),两条对照用例迁到新面板并保留。 - 影响范围:`src/view/project-development/{ResourceTypePanel.tsx,ResourceClassificationPanel.tsx,ResourceInfoPanelView.tsx,resourceCanvasInfoModel.ts,index.tsx}`、`src/features/project-workspace/resourceTypePanel.css`、`tests/{resourceTypePanel.test.tsx,resourceClassificationPanel.test.tsx,projectResourceLiveIntegration.test.tsx,appSurface/project-development.suite.ts}`。不改 `update_local_project_resource_classification` 的入参形状与 CAS 口径、不改读时自愈语义、无后端与 schema 变更。 - 验证方式:`resourceTypePanel.test.tsx` 新 12 条 + `resourceClassificationPanel.test.tsx` 19 条 + `appSurface.test.ts` 431 passed。变异验证(已实测):新面板 `category` 改回回传落盘原值 → 「改类型生效」用例红;标签面板改用显示口径 → 对照用例出现 `- "category": "unclassified" / + "category": "ui-interaction"`;去掉浮层判据里的新 state → 点外部串台用例红。 - 关联文档:[AGC 资源工作台 V3 端到端验收用例](../../technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md) 的 S12。 ## 2026-09-14 同步命令 `generate_local_project_asset` 退役为「仅测试调用」 - 决策:图片类生成接线改为 `start_local_project_asset_generation` + `list_local_project_asset_generations` 后,同步命令 `generate_local_project_asset` **已无生产调用方**,只剩 `src-tauri/src/tests/project.rs` 的三条集成用例与 `commands.rs` 的自身单测在调它;因此登记进 `scripts/check-config.mjs` 的 native-only 白名单(该门禁有「App invoke 与白名单互斥」断言,谁重新给它接调用方就必须同时删掉这条白名单项)。 - 待办:它是**注册中的可调用 IPC**,一旦被将来代码调用就是一条绕过任务账本、单次阻塞最长 35 分钟的并行生成路径。下一批次应删除它,或改为转调 `start_local_project_asset_generation`(连带迁移那三条集成用例)。 - 关联文档:[栏目画布底部工具栏入口矩阵](../../technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md)。 ## 2026-09-14 AGC 壳 Rust 套件按「一片一 job」拆分,客户端 Rust 关键路径压到 7 分钟以内 - 背景:`AI game creator shell Rust tests` 是客户端 CI 的关键路径(run 2097 实测 15 分 27 秒)。拆开来看:前置 5 分 30 秒(checkout 10s + `npm ci` 2m45s + Cargo fetch 2m35s)、编译 1m39s、**AGC 壳 bin target 的 2466 条单测串行 507s**、`agent-run` smoke 51s。这 2466 条全在 `apps/ai-game-creator-shell/src-tauri` 的 bin target 里,一条 `cargo test … -- --test-threads=1` 跑完。 - 为什么原本整套串行:2026-07-21 的 `a273377b1`(「稳定AI原生壳全量测试」)把 Tauri suite 固定为 `--test-threads=1`,理由是**共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰**——即同进程内的全局锁、异步终态与进程级 static 被交叉触发;另有少量用例自身 spawn 当前测试二进制跑 fixture,会碰容器里共享的 target 与固定临时路径。当时的口径是「修正 suite 调度口径,不放宽断言」。 - 决策:**把 2466 条按名单切成 4 片,一片一个 CI job**(片内仍严格 `--test-threads=1`,不放宽任何断言),片与片之间靠 job 级并发摊开。新增 `apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs`:`cargo test --no-run` 编译一次拿到测试可执行文件,`--list` 取全部用例名,排序后按 `index % shards` 切片;CI 的每个分片 job 用 `--shard-index=` 只跑自己那片(`--exact <名单>` 加独立 `TMPDIR`),本地不传该参数时仍是一条命令把 4 片放进程里并行。 - 反面实验(run 2102,已废弃,勿重做):起先让**同一个 job** 内的 4 个进程并行跑这 4 片,门禁步骤跑满 18 分钟仍未结束,比整套串行的 507 秒还慢——同一容器内多片共享 `HOME`、target 目录与固定临时路径,会互相拖慢。所以 CI 走 job 级分片,`--shard-index` 是唯一入口。 - 不变量:片并集必须等于 `--list` 的全集且互斥,数量或成员不符立即失败(`assertShardsCoverEveryTest`);该校验与「只跑一片」无关,因此在每个分片 job 上都会执行,防止分片规则改动后静默漏跑门禁。 - 配套拆分:`npm run ai-game-creator-shell:check:rust` 拆成 `:rust:crates`(`agent-runtime-core`、`agent-runtime-orchestration`、`platform-llm`、`shared-contracts`)与 `:rust:shell`(分片运行器),聚合脚本保持同序,因而 `ai-game-creator-shell:check` 与本地 `npm run check:native-shells` 语义不变。AGC 相关门禁在 CI 里变成 6 个 job:`AI game creator shell Rust shard 1/4` ~ `4/4`、`AI game creator shell Rust smoke`、`AI game creator shell Rust crates`。 - 前置瘦身:AGC 壳有独立 `Cargo.lock`,其 path 依赖已包含 `platform-llm` / `platform-agent` / `agent-runtime-core` / `shared-contracts`,所以 4 个分片 job 与 smoke job 只需预热 AGC 壳这一份 manifest;这些 job 只用 cargo 与 node 内建模块,因此 **5 个壳 job 与 crates job 都不再执行 `npm ci`**(每个省 1~3 分钟)。 - 影响范围:`.gitea/workflows/project-ci.yml`(十一个 job)、`scripts/check-native-shells.mjs`(分组由五个到十个:新增 `agc-rust-crates`、`agc-rust-shard-1..4`、`agc-rust-smoke`,移除 `agc-rust` 与随后的 `agc-rust-shell`)、根 `package.json`、`scripts/project-ci-workflow.test.ts`(新增纯 cargo job 免 `npm ci`、分片运行器覆盖校验、crate 级 job 预热顺序断言)、开发运维文档与共享记忆。本仓库不把 Project CI 的 context 配成 `master` 分支保护的合并必需检查(2026-09-14 复核),合并前由人工确认结果,因此 job 拆分/改名不需要同步分支保护设置。 - 验证方式:`npx vitest run scripts/project-ci-workflow.test.ts`;分片运行器本地以 `agent-runtime-core`(7 条 → 2/2/2/1)与 `platform-llm`(146 条 → 49/49/48)验证分片、`--exact` 与片 TMPDIR 隔离,负例 `--shard-index=5` 立即失败;`node scripts/check-native-shells.mjs --groups=contract` 回归。预期每个分片 job 收敛到 5 分钟以内(前置约 1 分 30 秒 + 编译约 1 分 39 秒 + 约 617 条用例)。 - 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)、[踩坑记录](pitfalls.md)。 ## 2026-09-20 AGC Rust 分片收敛为两条 lane,匹配 runner 有效并发 - 背景:四个独立 shard job 让每个 job 重复 checkout、Cargo 依赖预热和测试二进制编译;Gitea run 2105 的 11 个 job 时长合计约 39 分钟,而整轮 wall-clock 为 20 分 23 秒,反推有效并发约 1.9 个 job。继续按「一片一 job」拆分已经把新增 job 开销和排队时间重新放回关键路径。 - 决策:保留 4 片名单、`--test-threads=1`、独立 `TMPDIR` 和每片的全集/互斥校验,但把 workflow 收敛为两条 Rust lane;lane 1 顺序运行 shard 1/4、2/4,lane 2 顺序运行 shard 3/4、4/4。每条 lane 只预热一次 AGC 壳 manifest,lane 之间仍保持 job 级并发;不在同一 job 内并行多个测试进程。 - 影响范围:`.gitea/workflows/project-ci.yml`、`scripts/project-ci-workflow.test.ts`、`scripts/check-native-shells.mjs` 与 Rust 分片说明文档。job 名称改为 `AI game creator shell Rust lane 1/2`、`lane 2/2`;分组脚本与 4 片测试名单保持不变。 - 验证方式:运行 `npx vitest run scripts/project-ci-workflow.test.ts`、`node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs`、`npm run check:encoding` 和 `git diff --check`;真实 Gitea run 需要确认两条 lane 均覆盖两片且 smoke、crates、Backend、Native、Frontend、Repository job 仍全部上报。 ## 2026-09-14 AGC 资源画布改为「手动整理」:新素材不再自动重排,整张重排只由「整理画布」发起 - 背景:生成一张新素材会让整张资源画布重排。两个 layout hook 都把 `rederiveAutomaticPositions` 打开(type 侧无条件 `true`,dependency 侧长期等于 `resourceGraphReady`),而该开关的语义是「每次资源协调签名变化就丢掉全部 `manuallyPlaced=false` 坐标、按当前资源与拓扑整体重算」;新增一张素材必然改签名,于是既有自动卡全部跟着挪位,用户刚记住的位置就没了。画布上也没有任何显式整理入口(`复位资源视图` 只复位视口)。 - 决策一(默认口径):两个 mode 的 `rederiveAutomaticPositions` 固定 `false`——画布默认只补新卡,不动任何既有坐标(`preserve`)。整张重排改为显式动作:资源工具条动作区新增**独立的**「整理画布」动作按钮(**不在**「资源排列方式」这个 `role="group"` 内——它是一键动作,不是第三种排列方式;真机上第一版塞进排序 tab 组里被验收人指出「整理画布的按钮独立出来,不要塞到那个里面」,已移出;第二轮又被指出「不要放在最右边」,所以**最终位置固定在「生成素材」之后、「管理未完成编辑」之前**(紧邻同类资源动作、在排序组左侧,不做这一行的行尾按钮——行尾会被读成「针对整个工具条」的动作)),调用 hook 新暴露的 `rederiveNow()`,复用既有写队列与 sidecar 写回链路,只把策略换成 `rederive`;用户可见反馈继续用既有 `resourceLayoutNotice`(成功即「布局已保存」)与 `resourceLayoutSaving`,不新增状态位。重算结果与当前坐标一致时**不落盘**(沿用既有 `changed` 门):已经整齐的画布按一下不该白推进一次 CAS / revision,关系图 `producerMappingTruncated` 时更不该把一份来自不完整关系图的自动布局写进 sidecar——既有用例 `keeps trusted truncated-graph depths through the workbench without persisting a flat automatic layout` 就是钉这条。 - 决策二(依赖图首次就绪的那一次):`dependencyDepth` 仍要在关系图就绪后按最终拓扑排一次列。这一层不再靠「让布尔长期为真」,而是按**项目作用域的一次性 flag**(`dependencyRederiveScopeRef`):关系图就绪且该侧 sidecar `ready` 的那一刻调用一次 `rederiveNow()`,之后一律 `preserve`;切排序 tab 不重新武装,切项目才重新记一次。 - 决策三(生成后自动聚焦):新增 `seenManifestAssetIdsRef` + effect,按 `manifest.assets` 的**新增 id**(不是 diff 位置、也不是文件名)把新卡交给既有 `pendingResourceFocusRef` + `advanceFocusGeneration()` 裁决链。首次打开项目 / 切项目只登记基线、不聚焦;重命名不改 id、天然不触发;已有指向同一资源的聚焦意图时不重复挂(显式生成链路在提交时就已挂好)。被搜索条件挡住时继续复用既有的「清除搜索并定位」提示与动作。 - 影响范围:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts`(写意图增 `rederive` 标记、写策略分支、`rederiveNow`)、`.../index.tsx`(两个 hook 配置 + 一次性重派生 effect + 新素材聚焦 effect + 「整理画布」按钮)、`apps/ai-game-creator-shell/tests/resourceCanvasManualLayout.test.tsx`。不改 `manuallyPlaced` 语义、不改 sidecar 的 CAS / revision 协议与字段形状、不改卡片尺寸模型、不改「搜索不重排」合同、不改 Rust 资源图与 `dependencyDepth` 权威。 - 验证方式:新增 `resourceCanvasManualLayout.test.tsx` 8 条:新素材入库后除新卡外坐标逐值不变、依赖侧首次就绪重算一次后同样不再重排、新素材自动聚焦、被搜索挡住走既有提示与动作、「整理画布」按 `rederive` 重算并保留手动坐标且给出一次可见反馈、已经整齐时再按一次不产生第二次落盘、首次打开与切项目都不聚焦。变异验证(均已实测):① type 侧改回 `true`、dependency 侧改回 `resourceGraphReady` → 3 条红;② 去掉新素材基线的首次登记 → 3 条红(既有卡被当成"刚生成"选中);③ 去掉 hook 里的 `rederive` 写策略分支 → 3 条红;④ 把显式整理改成强制写回(去掉 `changed` 门)→ 既有「截断关系图」用例红。 - 关联文档:[踩坑记录](pitfalls.md)、[项目开发工作台 PRD](../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)。 ## 2026-09-14 客户端 CI 按门禁组拆成三个 job,AGC 的 web / rust 两段并行 - 背景:`Project CI / Native shell tests` 把微信壳、Expo 移动壳、Tauri 桌面壳、H5 HostBridge 与 AI 游戏创作壳的全部门禁串在一个 job 里,实测 18 分 37 秒;同一次运行的 Repository / Frontend / Backend 分别只要 3 分 21 秒、4 分 16 秒、6 分 14 秒,其余三个 job 结束后客户端 job 还要再跑十几分钟。日志时间戳显示门禁段 932 秒里:AGC `ai-game-creator-shell:check` 占 654 秒(其中壳内 Rust 套件 2451 个用例 `--test-threads=1` 单跑 441.58 秒、编译 79 秒),AGC vitest 75 秒,两个发布构建 smoke 加落盘断言 230 秒,而 h5 / 微信 / 移动 / 桌面壳的全部运行时门禁加起来不到 50 秒。 - 决策:`scripts/check-native-shells.mjs` 引入 `--groups=`,把门禁分成 `contract`(静态契约断言)、`shells`(H5 / 微信 / Expo / 桌面壳运行时门禁)、`agc-web`(AGC typecheck 与壳内测试)、`agc-rust`(共享 / 平台 crate 测试、AGC 串行壳测试、agent-run smoke)、`release`(AGC 与桌面壳发布构建 smoke、落盘产物断言)五组,每组暴露一个 `check:native-shells:` 根脚本;不带 `--groups=` 时仍然串行跑全部分组,本地 `npm run check:native-shells` 语义不变。CI 据此把原客户端 job 拆成 `Native shell tests`(contract + shells + release)、`AI game creator shell web tests`(agc-web)、`AI game creator shell Rust tests`(agc-rust)三个 job,并把最长的 AGC Rust job 声明在最前,使 runner 领取顺序与关键路径一致。 - 命令等价:`npm run ai-game-creator-shell:check` 拆成 `:check:web`(typecheck + 壳内测试)与 `:check:rust`(agent-runtime 两个独立 crate + `platform-llm` + `shared-contracts` + AGC 壳串行测试),聚合脚本仍是 `web && rust && agent-run:smoke` 同序同命令,本地与文档入口不变。`agent-run:smoke` 会用 `src-tauri/Cargo.toml` spawn `cargo`,因此归入 `agc-rust` 分组,与 AGC 依赖预热同 job。 - 影响范围:`.gitea/workflows/project-ci.yml`(六个 job)、`scripts/check-native-shells.mjs`、根 `package.json` 门禁脚本、`scripts/project-ci-workflow.test.ts`(校验分组清单、根脚本内容与 job 覆盖,防止新增分组时静默漏跑)、开发运维文档与开发流程记忆。门禁覆盖不变,只有执行位置改变;Gitea `master` 分支保护的 required context 是追加式的(旧四个继续上报,需补上两个新 AGC context)。 - 验证方式:`npx vitest run scripts/project-ci-workflow.test.ts`(11 条);`node scripts/check-native-shells.mjs --groups=contract` 本地 0.6 秒通过;`--groups=` 未知组与空组都要报错关闭。拆分前同一类运行的 wall-clock 是 22 分 15 秒(run 2094,`Native shell tests` 单 job 19 分 50 秒);拆分后 run 2097 六 job 全绿、wall-clock 15 分 27 秒,关键路径转移到 `AI game creator shell Rust tests`(15 分 27 秒 = 前置 5 分 30 秒 + 门禁 11 分 43 秒),其余五个 job 3 分 45 秒 ~ 8 分 36 秒。AGC 壳内串行套件(2451 用例)实测 507 秒,是这条关键路径的硬底,再切 job 只会重复 `npm ci` 与 Cargo 预热。 - 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)、[踩坑记录](pitfalls.md)。 ## 2026-09-10 策划 Agent 迁移只复用生产基建 - 决策:待实施的生产迁移以自由协作策划原型为行为基线,仅复用 Provider、恢复、文件操作、审计和 UI 通信;不继承旧 Planning V2 的强制工具、问询轮数、GDD 内容校验和版本审批。保留五阶段与顾问态、当前阶段资源注入和产物存在性检查,系统阶段空必需清单不增加解析或登记功能。 - 交互边界:正式审批由 ✅/❌ 决定;❌ 只取消待审批、不唤醒 Agent,等待审批时禁止发送消息但允许浏览工作区。用户可直接查看工作区,编辑可暂不做,不引入用户与 Agent 协同编辑锁或冲突合并。 - 影响范围:策划入口、会话与工具实现、资源打包、文件浏览;迁移已完成。当前入口统一使用新 Design Agent,旧 Planning V2 会话不再继续运行。 - 关联文档:[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。 ## 2026-09-13 AGC 资源替换补「会话内血缘标注」:只保留当前有效的一条 A→B,不假装持久 - 背景:用户验收指出「当前版本使用素材替换后,一个标为当前版本素材一个不是,替换关系不明」。核实后的边界:①光环(`currentVersionBindingIds` → DOM `is-current-version` / `data-used-by-current-version`)本身同源、不会两处打架,替换后光环从源素材 A 移到替换素材 B 是**设计内**的既定变化;②替换关系在客户端**任何一层都不存在** —— 宿主只消费替换结果的 `result.versionId` 与 `result.committedProjectRevision`,`ProjectVersionResourceReplacement` 里的 `sourceResourceId` / `replacementResourceId` / `warning` 无人消费,审计 `asset.version_binding.replace` 只有写侧;③manifest 的 `.previous` 不能当数据源 —— 它只是原子安装的崩溃兜底副本,装盘成功即删,正常项目里根本不存在。 - 决策:只在**宿主会话内**记一条血缘(源素材 A → 替换素材 B,两端都是 manifest 资产 id),并把它标回画布:源素材卡「已被 替换」、替换素材卡「替换自 」,稳定 DOM 判据 `data-resource-replaced-by` / `data-resource-replacement-of`(值是对面资源的 manifest 资产 id,不是显示名)。同一会话内再次替换**整条覆盖**上一条,只保留当前有效的一条,不做历史链(与 PRD §7.8「只展示当前有效关系」同口径)。 - 生命周期(本方案的天花板,UI 与 PRD 都已写明):**只在本次会话有效** —— 切换项目 / 关闭工作台 / 重新加载 manifest 都不保留,也不写盘,不假装持久。要让关系跨会话可查,必须先有读侧事实源(例如让替换审计可从客户端读取,或新建一份替换血缘 sidecar),那是另一个切片。 - 影响范围:`apps/ai-game-creator-shell` 前端(`features/resource-canvas/resourceVersionReplacementModel.ts` 新增纯函数、`view/project-development/index.tsx` 宿主状态与卡面、`styles.css` 角标样式、`tests/resourceVersionReplacement.test.tsx` 与 `tests/appSurface/project-development.suite.ts` 行为断言);不改 Rust、不改版本绑定口径、不改 `game_iteration_resource_bindings`,也**不碰**并行的「点选替换」态。 - **遗留(独立事项,本轮未修)**:版本绑定存在口径冲突 —— 版本追加会把绑定重写成「当时全部 manifest assets」(`src-tauri/src/project/manifest.rs:727-738`,由每个成功 Agent 回合 `src-tauri/src/agent/direct_runtime.rs:3818-3821` 触发),而替换把绑定改窄(`version_resource_replacement.rs:409-440`);`activeVersionId` 为 null 时取最后一版(`src/features/project-workspace/resourceReferences.ts:226-241`),于是下一次版本追加后**被替换掉的源素材 A 会重新带上「当前版本使用」光环**。该项已由用户确认单独立项,本次不动。 - 关联文档:[项目开发工作台 PRD §5.3 / §7.8](../../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md)。 ## 2026-09-11 素材类型(功能分类)重新提供用户入口;资源卡角标改显示资源类型而非媒体类型 - 背景:`6bdc8bbd9` 把「分类与标签」面板收敛为纯标签面板,并明确记下「随之的事实是:**「用户手动设置 `category`」这项能力就此移除**」(本文件 2026-09-11 那条以「用户给出的目标样式截图里,这个面板标题是「编辑素材标签」…」开头的条目,其决策一行即该结论)。用户随后要求「用户可以自己变更素材类型」,并追加要求把资源卡右上角角标从"文件/媒体类型"(图片 / 视频 / 文档 …)改成"资源类型"(功能分类中文名)。现状核实:写入链路本来就是完整的 —— `update_local_project_resource_classification`(`src-tauri/src/commands.rs:2162`)→ `update_manifest_asset_classification_at`(`src-tauri/src/project/manifest.rs:1112`)已接受任意合法 `category` 并校验 6 个合法值,**本次不需要改 Rust**;缺的只有 UI 入口与角标口径。本条**只回收「面板不再编辑分类」这一项**,「面板标题仍是「编辑素材标签」、删除资源入口仍在选中工具条」等其余结论不变。 - 决策一(入口)【已被 2026-09-14「「素材类型」从「编辑素材标签」面板拆成独立入口」取代】:类型选择器加进**「编辑素材标签」面板**(与标签同一次保存、同一条写入路径,不新增第二条命令、不在工具条另开第二个入口)。选择器复用 `GAME_CREATION_APP_ASSET_CATEGORIES` × `resourceReferenceCategoryLabel`,不新造第二套中文译名。 - 决策二(两个口径的分叉,本次核心不变量):选择器**读显示口径** `gameCreationAppAssetCategory`(与画布栏目 `projectResourceAssetCategory` 同源,用户看到的选中项就是他看到的栏目);**写回**用 `categoryChoice` 区分用户是否主动选过 —— `null`(没碰过控件)回传 `gameCreationAppAssetPersistedCategory` 的落盘原值,非 `null` 写用户选的值。这条分叉同时满足"只改标签不漂移分类"与"用户选了就写用户的值",两个方向都有对照用例(见验证方式)。 - 决策三(角标):资源卡右上角角标改为**资源类型**,取值 `categoryLabels[resource.category]`(栏目与筛选共用的同一份文案),因此角标恒等于该卡所在栏目;媒体类型仍由卡面视觉(图片 / 视频 / 音频 / 文档摘要)表达。只改这一处渲染(`index.tsx` 的 `ResourceCard`),三处面(栏目画布卡、「所有资源」展开态卡、总览缩略摞上铺的卡)自动一致;`projectResourceTypeLabel` 保留给「资源管理面板」的「分类 · 类型」小字与总览摞分列,不再用于角标。 - 画布跟随链(核实结论,**无需额外迁移代码**):`useProjectResourceCanvasLayout` 的 `createResourceSignature` 已把 `resource.category` 计入签名,分类变化 → 签名变化 → `reconcileResourceCanvasLayout` 按新的 `section` 归并(`resourceCanvasSectionMapping.resolveResourceCanvasSection`:现行栏目值原样归到资源当前分类,x / y / `manuallyPlaced` 原样保留)→ 需要时写回 sidecar。卡片随分组落到新栏目。 - 已知盲区(**有意保留,未修**):把 `kind` 已能明确分类的资产显式设为「待归类」会被读时自愈覆盖回派生栏目(落盘 `unclassified` 无法区分"没有明确分类"与"用户显式选了待归类")。修它需要在 manifest 里区分"未设置"与"显式 `unclassified`",属契约级改动 + 迁移,超出本次范围;相应地,面板不给"卡片会挪到待归类"的承诺,只有明确用例钉住写入值是 `unclassified`。 - 影响范围:`src/view/project-development/ResourceClassificationPanel.tsx`(选择器 + 写回分叉)、`src/view/project-development/index.tsx`(角标取值)、`tests/resourceClassificationPanel.test.tsx`、`tests/projectResourceLiveIntegration.test.tsx`(真宿主跟随链)、`tests/appSurface/project-development.suite.ts`(原媒体类型角标断言改资源类型)、PRD §5.3、AGC 资源工作台 V3 端到端验收用例 S5、[`【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的写入命令一节。Rust / manifest 字段构成 / SpacetimeDB 都不动。 - 验证方式:`npm run test -- apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx apps/ai-game-creator-shell/tests/appSurface.test.ts` + `npm run typecheck` + `npm run check:encoding` + `git diff --check`。变异验证(均已实测):①写入改回"永远回传落盘原值" → 3 条面板用例红;②没碰控件时改回"回传自愈值"(`gameCreationAppAssetCategory`)→ 2 条对照用例红;③选择器改读落盘值 → 选择器用例红;④角标改回 `projectResourceTypeLabel` → 角标用例与 appSurface 断言红。**不可构造**的变异:把角标"只在挂载时算一次"—— 选中资源本身就会把画布切进它所在栏目,改类型又让卡片换到另一个栏目分组,React 每次都卸载重建卡片宿主,挂载快照与实时读数在 DOM 上同形。 ## 2026-09-11 pre-commit 补 Rust 格式守卫:lint-staged 增 \*.rs,本地不再只靠 CI 的 check:rustfmt - 背景:本 PR 已因 `check:rustfmt` 红过一次(`15660a98b` 修掉本批遗留的 8 处格式偏差)。根因是 `.husky/pre-commit` 只跑 `lint-staged`,而它的 glob 只覆盖 `*.{js,mjs,cjs,ts,tsx}` —— **Rust 格式在本地没有任何守卫**,唯一防线是 CI 那一侧(`check-repository-ci.sh` → `npm run lint` → `check:rustfmt`);本地没人跑得到的门禁等于没有门禁,「本地全绿、CI 才红」就会反复发生。 - 决策:lint-staged 增 `"*.rs": ["node scripts/lint-staged-rustfmt.mjs"]`。`cargo fmt` 只按 workspace 粒度格式化、**不接受文件参数**(lint-staged 会把命中的暂存路径追加到命令末尾),所以用包装脚本忽略 argv,对 `server-rs` 与 `apps/ai-game-creator-shell/src-tauri` 两个 workspace 各跑一次 `cargo fmt --all --manifest-path -- --check`,**只查不改** —— pre-commit 不应该自动改写别人正在改的 Rust 文件。workspace 路径与既有 `check:rustfmt` 一样写成 cwd 相对,因为 lint-staged 以 git 根为 cwd 运行任务。 - 连带维护点:`scripts/git-hooks.test.mjs` 用 `assert.deepEqual` **钉住 lint-staged 的整份配置形状**,增键必须同步该用例,否则 `check:git-hooks`(在 `npm run lint` 内)会以 `deepStrictEqual` 失败 —— 本次就是这样红了 Repository checks。该文件第 2 个用例里的 `lintStagedConfig` 是 temp repo 的测试替身,不随之增键:temp repo 没有 Rust 文件,`.rs` 只会命中 0 个。 - 已知影响(**刻意保留**):没有暂存 `.rs` 时守卫完全不触发(lint-staged 报 `[SKIPPED] *.rs — no files`);一旦暂存了 `.rs`,它检查的是**整个 workspace** 而不只是暂存文件 —— 这是为了与 CI 完全同口径而接受的取舍,副作用是「别人工作树里未格式化、且尚未暂存的 `.rs` 会挡住本次提交」,此时应按报错里的文件去找该文件的作者,不要顺手 `cargo fmt`(那会连带格式化别人的在途代码)。**裁定:保持「整个 workspace」不变**,不换成「只查暂存文件」(`rustfmt --check --skip-children` 与 `cargo fmt` 的口径不再一致)。理由是「本地绿 ≠ CI 绿」正是本批两次 CI 红的共同根因,守卫必须与 CI 走同一条口径;共树里那种硌人是**共享 worktree 的症状、不是守卫的问题**,对应的流程修正是「一条线一个 worktree」。 - 已知本地限制(**本机环境限制,不是「CI 会红」**):`check:git-hooks` 第 2 个用例(`pre-push runs repository parity only for master updates`)在 Windows 本机会红,形态是 `finally` 里 `rmSync` 报 `EBUSY: resource busy or locked`(**断言全部通过,红在清理**)。根因是 Windows/WSL 跨 `/mnt/c` 的临时目录句柄在子进程退出后仍被持有(本机 `bash` 是 WSL 的 GNU bash 5.2.21 `x86_64-pc-linux-gnu`),该句柄存活时间超过删除重试窗口 —— 给 `rmSync` 加 `maxRetries: 10`(约 5.5s 线性退避)实测**无效**,已逐字还原。**后果是残留目录会累积**:清理前本机 `%TEMP%` 有 16 个 `genarrative-pre-push-*`、最早到 2026-09-05(即每次运行都发生;本轮已清到 0),看到残留目录直接删即可。判定它是本机环境限制而非 CI 会红的依据:残留目录已持续一周,而这一周 CI 上该用例是绿的 ⇒ **Linux CI 上该用例通过**。**裁定:接受它在 Windows 上红,不改该用例语义、本批不再修。** - 验证方式:`node scripts/lint-staged-rustfmt.mjs` exit 0;lint-staged 分派层面确认 `*.rs` 任务真被触发(对 6 个 `.rs` 跑通)且无 `.rs` 时 `[SKIPPED]`;`npm run check:rustfmt`、`npm run check:encoding`、`git diff --check` 均 exit 0。变异验证:把 `*.rs` 从 `package.json` 摘掉 → `check:git-hooks` 第 1 个用例以同样的 `deepStrictEqual` operator 变红;还原(`package.json` 字节级哈希一致)后该用例回 `ok`。(2026-09-24 独立复核:摘掉 `*.rs` 那段后 `check:git-hooks` 第 1 个用例报 `operator: 'deepStrictEqual'`、本次运行 `# pass 1 / # fail 1`;从备份还原后 `package.json` SHA256 与改前逐字节一致(`A205E54E…`)且 `check:git-hooks` exit 0。) - 关联文档:`docs/project-memory/shared-memory/pitfalls.md`(`cargo fmt --all` 会扫到别人未提交半成品那条)。本文件 2026-08-12「Repository checks 采用 CI 与本地共用的单一门禁入口」条的「提交门禁」一行只描述当时的 JS/TS 范围,按本文件顶部口径历史条目只用于追溯,不再回改;提交 `dd7cf401a`(守卫本体)、`2a7bfadd7`(形状断言跟进)。 ## 2026-09-11 冷启动首屏存在同一张卡被读两次(登记回声 + 取消/重扫路径):记录为后续项,本轮不修 - 背景:为解「从首页进项目 → 资源管理页首屏等图片」,本轮按用户批准做了 A(热预取不再按投影顺序盲取前 N,改为只预取几何上可见的卡)+ B(相交卡按「先视口内、再 160px `rootMargin` 圈」两档入队)。本条记录的是**做 A/B 时顺手发现、但属于另一条独立缺陷**的重复读;A/B 只改「取哪些、按什么顺序」,不碰它。 - 现象:冷启动首屏同一张卡会被读两次。20 张登记卡(8 张视口外 + 4 张只在余量圈 + 8 张视口内)时 `read_local_project_media_preview` 共发 **15 次**:视口内 8 张各 1 次、视口外 0 次,余量圈那 4 张里 **3 张各 2 次、1 张 1 次**(distinct 12 + 重复 3)。 - 两条对照证据(证明与 A/B 无关、且先于 A/B 存在): 1. 把 `eagerPreviewLimit` 置 **0**(完全关掉热预取)→ 仍是 15 次、仍是那 3 张余量圈卡各读 2 次,只是顺序不同; 2. 把 `useProjectResourceCardPreviews.ts` **整份换回 A/B 之前的 `HEAD` 版本** → 同样 3 张余量圈卡各读 2 次(顺序为登记顺序)。 - 怀疑方向(未验证,本轮未定位):两条通路叠加 —— ①「登记即复核」的回声扫描:`observePreview` 每注册一张卡就跑一次兜底扫描,逐张注册会逐张放行一批;② `cancelQueuedVisiblePrefetches`(预取作用域变化时先下掉队列里的 `visible` 预取、再立刻重扫)与多延迟点兜底扫描(0 / 250 / 1000ms)叠加时,可能在前一次请求已完成之后又被判成"从未请求"(该 identity 在 `previews` 里没有状态)而重新入队。定位需要按 identity 打点 `requestPreview → 入队 → drain → publish` 的时序。 - 未修原因:① 用户在赶 DDL,本轮时间窗只够 A+B;② **不是用户可见故障** —— 表现只是首屏多花一两个物理读取槽 / 多一次 IPC,图片照常出来;③ 它属于「可见性门禁 + 队列时序」这条更脆的通路,改它必须先把时序定位清楚,不能凭猜测顺手改。 - 影响:首屏请求量比理论最小值多约 20%(上述夹具 15 次 vs 12 次);要再压首屏等待,必须先解开这条重复读。 - 验证方式:本条目为**待查项,无代码改动**。修复时建议的断言:同一次冷启动首屏流程里,同一个 identity 的 `read_local_project_media_preview` **只允许发一次**(除非确实发生过 LRU 驱逐或显式重试)。 - 关联:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts`(`observePreview` 的注册回声扫描、`cancelQueuedVisiblePrefetches`、`RESOURCE_PREVIEW_VISIBLE_SWEEP_DELAYS_MS` 多延迟点兜底扫描)、`apps/ai-game-creator-shell/tests/useProjectResourceCardPreviews.test.ts`(`冷启动首屏的放行范围与放行顺序` 两条用例的夹具可直接复用:20 张卡 + `eagerPreviewLimit: 12`)。 ## 2026-09-11 资源画布 sidecar 的读时归并保持写回,但不再静默,并公开丢弃口径 - 背景:2026-09-10「旧栏目坐标读时归并」那条决策在文档里写成「不写迁移脚本、不删除、不重置」,字面为真、事实上是**读时原地重写**——`useProjectResourceCanvasLayout.ts` 读盘后若 `reconcileLayout(...).changed` 为真就立刻走 `update_local_project_resource_canvas_layout` 做一次 CAS 写盘(`revision` +1)。真机量到的规模(36 份 sidecar / 300 条坐标):**112 条 legacy 分区坐标**(`art` 36×2 + `code` 20×2)会在第一次打开项目时被静默改写;另有 **82 条(27%)坐标被丢弃**,其中 **66 条**因为 `resourceId` 在 manifest 里查不到、**16 条**因为持久化分区与资源分类不匹配,丢弃点是 `resourceCanvasLayoutModel.ts` 的 `if (!normalized) return false;`,而这次丢弃同样被上面那次写回**固化**。旧文档口径与读路径真实行为冲突:不改则下一个排障的人会往「是不是有迁移脚本」的方向找。审计给出的两个候选是 A(保留重写、补可见提示 + 改文档)和 B(把丢弃/重写改成只在拖动时发生);用户拍板 **A**,因为 B 会改变布局语义、影响两次 sidecar 的既有行为。 - 决策:**重写行为本身一字不改**(不改「只在拖动时写」、不改 `resourceCanvasLayoutModel.ts` 的丢弃语义、不动跨端契约与 sidecar schema,`schemaVersion` 仍是 `v1`),只把两件已发生的事变成可见:①读时归并 → 一次性的画布提示条(`resourceWorkbenchNotice` 所在的那条 `game-resource-book-notices` 提示层、复用既有 `.game-resource-live-notice` 样式与「知道了」关闭按钮),文案点明「旧分区坐标对齐到新分区并写回、坐标位置未变」;②读时丢弃 → 同一条提示里带条数,并把三种情况在 DOM 上分开暴露(`data-resource-canvas-layout-normalized` / `-dropped` / `-dropped-missing-resource` / `-dropped-section-mismatch`,完全没丢时是 0 / 0)。提示是**操作后的一次性提示**,不是常驻说明,也不写功能说明或规则描述(符合「面板里不写功能说明 / 长期规则」的仓库规范)。统计口径直接复用丢弃判据本身(`normalizeResourceCanvasPosition`),只统计、不参与判定。两个排序模式各读一份 sidecar,实现上每个项目只提示一次、只取先到的那份,避免两条提示互相覆盖。 - 影响范围:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts`(新增只读统计 `inspectProjectResourceCanvasLayoutRead` 与文案 `describeProjectResourceCanvasLayoutRead`、新增 `readReport` 返回项)、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`(提示条渲染与每项目一次的去重)。**行为、写回时机、丢弃规则、sidecar schema 与跨端契约均不变**,因此首次打开存量项目仍会写回一次 sidecar、`revision` 仍 +1。 - 验证方式:`useProjectResourceCanvasLayout.test.ts` 覆盖三种读盘统计(归并 / 资源不在 manifest / 分区不匹配)与文案四形态、读盘无需归并且不写盘、以及「只补新资源落位的写回不报读时统计」;`appSurface/project-development.suite.ts` 覆盖提示条真的出现(文案 + 四个 `data-*` 计数可读、可关闭)与存量 sidecar 无需归并时**不出现**任何读时提示。变异验证:去掉 `setReadReport` → 读盘统计与提示条用例变红;把 `dropped` 计数改成恒 0 → 丢弃条数与 `data-resource-canvas-layout-dropped` 断言变红。 - 关联文档:`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md`(读时归并那条)、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`、本条上一条 2026-09-10 决策。 ## 2026-09-11 资源分类口径拆成「读显示 / 写回」两个口径,读显示口径下沉到 Rust 并与 TS 同构 - 背景:`category` 此前只有 TS 一处实现(`gameCreationAppAssetCategory` = 落盘 `category` 权威 + 读时自愈),Rust 侧反序列化只做「缺失 / 非法回退 `kind` 派生」,Agent 资源投影还直接透传落盘 `asset.category`。于是同一条资产有两套口径:真机 122 条资产里 **55 条** `{kind:"ui", category:"unclassified"}` 在 UI 显示「UI 交互」、Agent 读到「待归类」。另有两条写读路径在真机上继续漂移:①「编辑标签」面板用**读显示**口径取值再原样回传,把自愈值写回落盘,把「只改标签」变成静默改分类(真机同一个 `kind:"ui"` 同时存在 55 条 `unclassified` 与 2 条 `ui-interaction`,后者正是被回写的签名);②UI 设计资产的现役写入侧写的是**大写** `"UI"`(`ui_editor/resource_bridge.rs`、`workflow.rs`、`persistence.rs`)、字体上传写 `font`,两者都不在别名表里 → 落 `image → unclassified`,且派生值本身就是 `unclassified`,读时自愈的触发条件「派生值不是 unclassified」永不成立,**8 条**真机 UI 资产永远归不了类;③`register_local_asset_entry` 的更新分支从不重派生 `category`,同路径重登记换了 `kind` 就留下「新 kind + 旧分类」,且陈旧的非 `unclassified` 值会被无条件信任。 - 决策一(口径拆两半):读时自愈规则**保留**,但拆成两个口径并在两侧同构。**读显示口径**=TS `gameCreationAppAssetCategory` / Rust `game_creation_app_asset_effective_category`(落盘 `category` 权威,唯一例外是落盘 `unclassified` 且 `kind` 能派生出明确的非 `unclassified` 分类时采用派生值);**写回口径**=TS `gameCreationAppAssetPersistedCategory`(只做缺失 / 非法兜底,等于 Rust 反序列化后的落盘原值),UI 与 Agent 都走读显示口径,写 manifest 一律走写回口径。两侧不许各写一份:TS 测试直接解析 Rust 源码里的 `EFFECTIVE_CATEGORY_CONTRACT` 决策矩阵与 canonical kind→栏目表逐条对照。 - 决策二(自愈不下沉到反序列化):`GameCreationAppAssetManifestEntry` 的反序列化**刻意不做自愈**,结果就是落盘原值——「编辑标签」面板要靠它回写。自愈只属于读显示口径,Agent 资源投影改走有效分类而不是透传落盘值。这是「一条资产一个口径」的最小实现:口径在函数层统一,落盘值不被读时改写。 - 决策三(别名表大小写不敏感 + 收口 `font`):别名表对 trim 后的小写值查表,`"UI" → ui-design → UI 交互`、`font → document → 文档`。写在别名表是因为读时自愈救不回来(派生值本身就是 `unclassified`);`game_creation_app.rs` 里那句「现役写入侧仍会写出这些非 canonical 值,必须在这里收口」此前只收口了小写 `ui`,与写入侧实际写的大写矛盾,现按注释本意收口。 - 决策四(重登记重派生,但不抹掉显式分类):`register_local_asset_entry` 只在 `existing.kind != kind` 时重派生 `category`;`kind` 未变时不动 `category`(落盘分类是权威值)。既修掉「新 kind + 旧分类」的错位,又不让同 kind 重登记吃掉 Agent / 客户端写入的显式分类。 - 影响范围:`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`packages/shared/src/contracts/gameCreationApp.ts`、`apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs`、`apps/ai-game-creator-shell/src-tauri/src/assets.rs`。跨端契约与 sidecar schema 不变,manifest 字段构成与顺序不变。 - 验证方式:Rust `asset_effective_category_follows_the_shared_contract_matrix` 逐条断言决策矩阵;TS `assetKindCanonicalMapping.test.ts` 解析同一矩阵与 kind→栏目表后喂给 TS 实现对照;`resource_bridge.rs` 的既有用例走真实生产函数 → 真实 `register_local_asset_at(..., "UI", ...)` → 断言落盘 `category`;TS 写侧字面量用例解析写侧源码第 3 个实参;面板新增 `{kind:'ui', category:'unclassified'}` 用例断言回传的 `category` 仍是 `unclassified`。变异验证:把 Rust 有效分类退回「返回落盘值」→ 矩阵用例与 TS 对照用例同时变红;把面板改回 `gameCreationAppAssetCategory` → 面板漂移用例变红;把别名表退回 `value.trim()` → Rust `"UI"` 断言、resource_bridge 端到端与 TS 写侧用例一起变红;去掉更新分支的重派生 → 既有重登记用例的 `category` 断言变红。 - 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`(分类取值口径)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/pitfalls.md`。 ## 2026-09-11 DirectProject 历史格式切换改为「读侧白名单兼容 + 写侧统一」,不做数据迁移 - 背景:格式切换到 `response_item`(#282 / `d3f5d0a35`)当年的决策前提是「Rust 是该历史文件的唯一写入方」+「存量测试数据由测试手动清理」,据此在技术方案里写了「不迁移旧 `{role,content}` 行」「不会对旧格式做迁移或兼容」。这两个前提在真实用户项目上都不成立:切换前 DirectProject 主对话由通用对话写入器写到同一份 `.agent/conversations/project.jsonl`,存量项目整份都是旧行(实测 27 个项目里 25 个是纯旧格式),而读侧只认 `{"type":"response_item","payload":…}`,于是这些项目的 DirectProject 回合全部失败在「DirectProject 历史记录类型无效」;写侧也没真正唯一——`agent/direct_tools_mcp.rs` 的 `conversation.record_codex_response` 还在往同一份文件插旧行(它本来就有自己的 journal `.agent/conversations/codex-responses.jsonl`),历史就算修好,Codex 一调该工具就会再次毒化。同时这条失败没有专门 hint,落进默认的「Codex 未完成本轮代码修改,请检查运行时配置后重试」,且 `retryable=true`,用户看到「可直接重试」但重试永远不会过(同一份历史)。 - 决策:兼容口径从「不兼容不迁移」改为「**读侧白名单兼容 + 写侧统一**」,仍然**不做数据迁移**。读侧只接受一种明确枚举的旧行形状(`schemaVersion=game-creator-conversation.v1`、无 `type`、role 在 legacy 写入器自己的角色集合 `user`/`assistant`/`tool` 内、content 为非空字符串):`user`/`assistant` 投影成与 `direct_project_local_message_item` 同形状的 Responses `message` item,`role` 与 `content` 逐字节保留、不 trim、不改写,未知字段忽略;`tool` 行已识别但不注入 Codex 上下文(它不是 Responses item,无法还原成真正的工具 item,聊天投影本来也只展示 user/assistant),与 developer/system item 同样过滤。角色集合取的是 `project/conversation.rs` 里那条 `matches!(role, "user" | "assistant" | "tool")` 校验,所以「legacy 写入器能写出的行」被完整覆盖;白名单之外的角色、带别的 `type`、换了 `schemaVersion`、content 非字符串或为空、缺 `payload`、坏 JSON 继续失败关闭。写侧删掉 `conversation.record_codex_response` 那条投影(显式 Codex 返回只留在自己的 journal)。同一份文件也被通用对话链(`project/conversation.rs` 的 `agent_id=None`=项目主对话,含 `agent/runtime_state.rs` 的项目级公开状态消息)读写,两条链此前的 reader 互不兼容(一个要 `type:response_item`、一个要 `schemaVersion`),现改为**各自白名单兼容对方的行**:通用对话侧跳过 `type=response_item` 且带 `payload` 的行(不把它二次投影成自己的记录,DirectProject 侧已经拥有那份投影),其余坏行两侧都失败关闭。这条失败新增专门 hint 并改为 `retryable=false`,不再显示「可直接重试」;历史文件的打开/读取类 IO 失败仍按可重试处理。 - 一个**被明确否决**的更强做法:给通用对话写入器加「文件已属于 DirectProject 就拒绝追加旧行」的硬报错。它看似能「关上再污染入口」,实测会把「尾行噪声」换成「任务起不来」——这些写入点不是尽力而为的旁路:`agent/runtime_driver/task_start.rs` 在 `ensure_game_creator_agent_runtime_accepted_public_status_at` 返回 `Err` 时会中止本次后台任务(「后台任务启动确认落盘失败,任务未执行」),`agent/runtime_protocol/steering.rs:572/604/1300` 三处调用也用 `?` 上抛。改用「消毒」而不是「关门」:legacy 写入器的行形状与角色集合都被 `conversation.rs` 自己的校验穷举,全部落在 DirectProject 的读侧白名单内,因此任何现役写入点都不可能再产出 DirectProject 读不了的行(真机 27 个项目实测 legacy 行 role 只有 user/assistant,0 条 tool)。 - 为什么不做数据迁移:读侧白名单兼容就能让 25 个存量项目**零改动**继续跑——历史文件字节不动、不改 mtime、不需要写权限、没有迁移中途失败要回滚的窗口,用户也不需要先打开一次客户端等迁移。反过来,迁移要改用户项目目录里的私有文件,得处理全量扫描、权限(Windows 私有 DACL)、断点续跑与失败回滚,还在迁移期要求新旧两个版本客户端并存时继续保留读侧兼容——等于同时维护迁移器和兼容层;而它换来的只是「文件里不再有旧行」这一项整洁性,旧行本来就只读兼容。另一个决定性理由是存量形态无法枚举:用户手改、或把项目目录从别处拷回来都会再出现旧行,读侧兼容是唯一能覆盖这些形态的做法。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs`(两处行信封判定收敛到同一个函数+legacy 投影+`tool` 行过滤)、`agent/direct_tools_mcp.rs`(删除旧行投影与只为它存在的 `message_id` 计算)、`agent/direct_runtime.rs`(专门 hint+`retryable=false` 白名单)、`project/conversation.rs`(跳过 DirectProject 行)、`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`。跨端契约与 sidecar schema 不变,`.agent/conversations/project.jsonl` 的行形状与字段零改动。 - 验证方式:Rust 定向测试覆盖旧行投影(role/content 逐字节、无 `messageId` 时不带 `id`)、旧行+新行交替的混合文件按行顺序读取、`tool` 行被识别但不进上下文、非白名单异常行仍失败关闭、通用对话写入器产出的旧行形状落在白名单内、通用对话链跳过 DirectProject 行且坏行仍失败关闭、混合文件从两条链都能读(互不毒化)、显式 Codex 返回不再写 `project.jsonl`、专门 hint 与 `retryable=false` 落到诊断 sidecar。变异验证:去掉 legacy 兼容分支→投影与混合文件用例变红;把白名单放宽成「任意行都接受」→失败关闭用例变红。 - 关联文档:`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`、`docs/project-memory/shared-memory/pitfalls.md`。 - 背景:用户给出的目标样式截图里,这个面板标题是「编辑素材标签」、副标题是素材名、已有标签是自带删除按钮的胶囊 pill、输入框提示「新增标签,多个用逗号分隔」、底部只有「取消」与「保存标签」,**没有分类那一排**。而面板原实现同时承担 6 类 `category` 手动设置与 `assets[].tags` 编辑,PRD §5.3 也写着「用户可在「分类与标签」面板手动设置 `category`」。资源卡浮出工具条上只有这一个相关入口(`label="分类与标签"` → `setResourceClassificationAssetId`),不存在第二个「编辑素材标签」入口,所以两种读法只能二选一。截图里的标题/按钮文案在全仓(含 `docs/**`、`.codex/**`、各类型源码)检索均无命中,属仓库之外的来源,因此本次改动以用户截图为准、不宣称是 PRD 明文。 - 决策:**该面板只编辑 manifest `assets[].tags`,移除分类 chip 那一排。** 随之的事实是:**「用户手动设置 `category`」这项能力就此移除**,`category` 只由落盘值与 `assets[].kind` 派生加读时自愈决定。写入命令 `update_local_project_resource_classification` 的 `category` 是必填,前端读一次当前权威值并在保存时**原样回传**,因此「只改标签」不会顺带改动分类,Rust 侧与 manifest 字段构成都不改。面板标题改「编辑素材标签」、入口按钮 label 改「编辑标签」(否则工具条写着「分类与标签」却打开纯标签面板,属误导)。「删除资源」按钮截图未画但保留:`openDeleteResourceDialog` 只在这个面板里被调用,删掉会让用户失去唯一的资源删除入口。 - 影响范围:`apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx`(标题/副标题/标签状态由整段字符串改为字符串数组/pill 列表/底部按钮文案)、`index.tsx` 的入口按钮 label、`apps/ai-game-creator-shell/src/styles.css` 的标签 pill 选择器块;`packages/shared` 的标签归一化(`normalizeGameCreationAppAssetTags`)与写入契约不变。 - 验证方式:`ResourceClassificationPanel` 定向测试覆盖「已有标签渲染成 pill」「点某个 `×` 只删对应标签」「每个删除按钮 `aria-label` 可区分」「保存 payload 的 `tags` 是数组且分类原样回传」「取消不写盘/删除在保存前可撤销」;`npm run ai-game-creator-shell:typecheck`、AGC 全量测试、`npm run check:encoding`、`git diff --check`。 - 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` §5.3、`apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs`。 ## 2026-09-10 AGC 资源画布分区改为 6 类资产分类加项目版本栏目,旧栏目坐标读时归并 - 背景:资源画布原分区轴是「扩展名 + mediaType」派生的 `document / art / audio / code / version`,普通画布只显示其中四栏并隐藏游戏代码,与 `@` 面板、资源详情筛选已经收敛的单一权威 `category`(`ui-interaction / character / scene / audio / document / unclassified`)不一致。只登记游戏代码的项目会因资源签名非空进入分页画布,但 code 不在可见栏目里,于是四栏全空、一张卡都不显示。旧布局 sidecar 已存有旧 `section` 值,而 PRD 要求历史手动坐标只读恢复,不删除、不重置、不迁移。 - 决策:分区轴改为 `PROJECT_RESOURCE_CANVAS_SECTIONS` = 6 类资产功能分类 + 末尾独立的「项目版本」栏目;项目版本不是资源资产,6 类资产分类轴对它不适用,固定单独成栏。扩展名分类器降级为只负责准入与卡片显示类型(`.zip` / `.exe` 等无法识别的二进制产物与附件仍不进入画布),不再决定分区。`game-creator-resource-layout.v1` 不升版;Rust `ProjectResourceCanvasSection` 保留旧 `code` / `art` 白名单值继续可反序列化,且不加未知值兜底,损坏 payload 仍失败关闭。读取时按资源当前分区归并:`document / audio / version` 同名 1:1 保留;旧 `art` 按资源当前分类落入 `ui-interaction / character / scene / unclassified`;旧 `code` 落入 `unclassified`;无法精确归并时回落 `unclassified`。归并只改写 `section`,`x / y / manuallyPlaced` 原样保留:**读时会把 legacy 分区(`art` / `code`)坐标归并到新分区并写回一次 sidecar(`revision` +1)**;不写迁移脚本、不重置坐标位置;但无法归并的坐标会被跳过(跳过条数在画布提示条上可见)。旧 `art` / `code` 平面并入同一新栏目后可能出现同坐标叠卡,按既有「不重置坐标」口径原样保留,不改写自动坐标,新资源仍由既有避让规则落位。 - 影响范围:`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`resourceProjectionModel.ts`、`resourceCanvasSectionMapping.ts`、`resourceCanvasLayoutModel.ts` 的栏目顺序与协调、`ProjectDevelopmentView` 的栏目标签 / 图标 / 可见栏目表。 - 验证方式:`resourceCanvasSectionMapping.test.ts` 覆盖旧值 × 目标栏目矩阵(坐标与 `manuallyPlaced` 原样保留、`section` 改写、独有旧值改写后必然判定为变化)与回落栏目属于目标集合;`projectResourceProjectionModel.test.ts` 覆盖 6 类分区投影、项目版本独立成栏和只登记游戏代码的项目落在「待归类」;Rust 侧覆盖旧 section JSON 反序列化、未知值拒绝与既有旧 sidecar 文件读取。 - 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 2026-09-05 本进程新建 Windows 私有对象不因继承 DACL 自动 UAC - 背景:#211 要求 sidecar 满足当前用户独占、禁止继承的 DACL。新建文件会先继承父目录 ACE,生产路径把这种短暂不合格送进 UAC;`project.lock` 还在独占句柄上 harden。含空格项目路径上提权 ArgumentList 被拆开,修复以 exit 1 失败。GDD 审批改意见因此弹权限,V1 锁创建不会。 - 决策:`harden_new_game_creator_private_path` 只在本进程收紧 owner/DACL,失败则删除刚创建的对象,不 UAC 接管。项目锁先写再释放句柄再 harden,并用内容回读防换绑;UAC 仍只用于允许范围内的已有外人本对象。提权 helper 的 ArgumentList 改为一条按 Windows 规则加引号的字符串。 - 补充:逐级创建 `.agent`、`runtime`、`locks` 等目录时,即使祖先已有 `manifest.json`,刚由本进程创建的目录也必须直接走 owner/DACL 初始化,不能因 managed-path 判定进入 UAC;自动项目根目录同样在创建成功后立即本地加固。 - 影响范围:`config.rs` 的新建 harden 与提权命令行、`project/write_lock.rs` 的项目锁创建;不改变锁竞争、失效回收、Drop 删除,也不放宽 symlink / reparse / 外人本 fail-closed。 - 验证方式:Windows 定向测试覆盖 `Genarrative GameAgent\gameagent-*` 取锁与私有 DACL,以及带空格路径的 quoted ArgumentList。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/pitfalls.md`。 ## 2026-09-03 AGC 登录 route event 使用 handler 已验证主体归属 - 背景:登录请求进入时尚未拥有 `AuthenticatedAccessToken`,通用 tracking middleware 无法从响应 extensions 归属登录成功用户;将 AGC marker 直接写入按用户/业务日幂等的 `daily_login` 又会受到不同来源登录顺序影响。 - 决策:密码登录和手机号登录 handler 在认证及 session 创建成功后,仅对合法 AGC marker 请求向响应 extensions 附加一次性 `TrackingLoginSubject`。tracking middleware 在现有 `ExternalApiPrincipal`、`AuthenticatedAccessToken` 之后使用该主体生成登录 route event 的 `user_id`、`owner_user_id` 和 User scope。`daily_login` 保持原有 event key、幂等键、业务日和 metadata 语义,不承担 AGC 来源归因。 - 安全边界:主体只来自后端认证服务返回的用户 ID;Header 仅决定是否进行 AGC 来源归因,不参与身份计算;不解析、不记录 access token、refresh token 或 Cookie。 - 影响范围:`api-server` 登录 handler、资产读取 handler、tracking middleware、Issue225 技术方案和定向集成测试;不修改 SpacetimeDB schema、migration、bindings、OpenAPI、后台页面或认证响应协议。 - 资产读取边界:`/api/assets/read-url` 和 `/api/assets/read-bytes` 继续支持匿名公开读取;有效 Bearer 复用同一次可选鉴权结果,在成功响应 extensions 中传递 `AuthenticatedAccessToken`,供 AGC route tracking 归属用户。External API Key/Admin 路由继续使用各自主体和审计链路。 - 验证方式:密码/手机号登录真实 `build_router` 链路分别验证 AGC route event 的 marker、真实用户归属和 outbox 落盘;资产读取主体保留、响应 extension 与匿名不附加单测;tracking identity 单测、`cargo check --locked -p api-server`、相关 `cargo test --locked -p api-server`、格式、编码和 diff 检查通过。 - 关联文档:`docs/technical/【后端架构】Issue225登录成功AGC用户归属修复方案-2026-09-03.md`、Issue #225、`server-rs/crates/api-server/src/tracking.rs`、`server-rs/crates/api-server/src/app.rs`、`server-rs/crates/api-server/src/assets.rs`。 ## 2026-09-03 server-rs workspace 保留独立 platform-agent 排除边界 - 背景:`platform-agent` 位于 `server-rs/crates/` 下,但实际由 AGC 独立 Cargo workspace 通过路径依赖使用。若不显式排除,主 workspace 的 `cargo fmt --all` 会把它识别为“位于 workspace 内但不是 member”的非法包并直接失败。 - 决策:继续将 `crates/platform-agent` 放在 `server-rs` workspace 的 `exclude` 中。它不加入主 workspace,也不在其 manifest 中新增平行 `[workspace]`;AGC 的独立 Cargo manifest 继续负责该 crate 的构建边界。 - 影响范围:`server-rs/Cargo.toml` 与仓库 Rust 格式检查;不改变 `platform-agent` 源码、AGC 依赖关系或主站运行时。 - 验证方式:`npm run check:rustfmt` 通过;`cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check` 不再报告 `platform-agent` workspace 错误。 - 关联材料:Repository checks #5755、`server-rs/Cargo.toml`、AGC `apps/ai-game-creator-shell/src-tauri/Cargo.toml`。 ## 2026-09-03 Native shell 检查清单只维护现役 HostBridge 文件 - 背景:#251 退役并删除了 H5 个人中心 QR 扫码弹层及其测试,同时移除了不再有源码消费者的生命周期 / 网络 wrapper;`scripts/check-native-shells.mjs` 和根 Vitest include 仍保留旧路径,导致 Native shell tests 在最后的 H5 HostBridge 调用链扫描阶段失败。 - 决策:Native shell 静态检查、H5 HostBridge 定向测试和根 Vitest include 只列出现役文件;已删除的 QR 扫码组件/测试及生命周期、网络 wrapper 从清单移除,不恢复已退役实现,也不为历史路径增加兼容占位文件。 - 影响范围:`scripts/check-native-shells.mjs`、`vitest.config.ts` 和 Native shell 检查门禁;不改变移动壳现役 `QrScannerOverlay` 或 HostBridge 公共契约。 - 验证方式:运行 `npm run check:native-shells`,确认 H5 HostBridge 调用链静态扫描不再引用已删除文件;同时运行编码、格式和 diff 检查。 - 关联材料:Native shell tests #5758、主站合并提交 `025f62729`、已删除的 `PlatformProfileQrScannerModal` 文件。 --- ## 2026-09-02 Direct 过程卡按回合阶段状态驱动 - 背景:DirectProject 结果卡把工具活动词、中间文本和真实回复增量都当成“实时回复”,标题随最近一次事件跳动;上游常整包返回正文时还叠加合成打字机,用户看到的是行为名而非当前阶段。 - 决策:Direct 过程卡顶部标题只由 `GameCreatorDirectTurnUpdateStatus` 决定(accepted=需求已接收 / running=任务执行中 / streaming=回复生成中 / finalizing=结果整理中 / completed=回复已生成 / failed=处理失败),小字只展示当前正在执行的具体内容并统一加“正在”前缀;真实回复增量(AccumulatedText)才标记 streaming,计划、推理、工具输出与 Activity 一律 running。生成中的累计回复直接作为 assistant 消息气泡在会话列表中原位更新,不再拼进过程卡;进入 finalizing / completed 时保留完整累计回复直到正式消息接管,失败时清除未完成正文。移除合成打字机回放;工具说明/中间文本不再触发 streaming。计划/推理通知收敛为 `preparing` 活动并在界面显示“正在思考中”,原始推理/计划正文不进入 UI,思考期的心跳按 1.2s 限流。命令/文件/工具执行细节与回复流解耦,`stream=false` 时仍展示在过程卡;MCP 工具按用户语义显示(例如 `agc_write_file` 为“正在写入文件:<项目相对路径>”、图片/素材/搜索/试玩分别显示生成、导入、搜索、试玩等动作),未知工具只显示“正在调用工具”不暴露内部工具名;命令显示“正在执行命令:<命令>”,验证类命令显示“正在验证游戏:<命令>”。同一活动后续无正文的心跳不得用通用文案覆盖已展示的具体工作。展开/收起是同一 `project + clientTurnId` 内的持久状态,内容更新不重置,切换新回合才收起;展开详情的滚动条轨道和角落保持透明。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs` 的 DirectProject observer、`apps/ai-game-creator-shell/src/App.tsx` 的事件投影、`ProjectSupervisorView` 过程卡渲染与对应 AppSurface 回归。 - 验证方式:Rust 单测证明只有开启流式时的 AccumulatedText 是 streaming、preparing 通知只产生 thinking 活动词且不携带原始推理文本、执行细节在 `stream=false` 时仍保留,并覆盖全部 AGC MCP 工具语义、未知工具不泄漏、绝对路径 / 上跳路径不展示;AppSurface 覆盖接受态、preparing 显示“正在思考中”、running 长文本展开、command-exec 与写文件心跳不覆盖具体工作、streaming 正文进入 assistant 气泡且过程卡只显示阶段、同一回合后续 running 不覆盖正文也不收起、失败后清除未完成正文、正式消息接管不重复;样式核对确认展开详情的滚动条轨道与角落透明;AGC typecheck、全量 appSurface、rustfmt、`npm run check:encoding`、`git diff --check` 通过。 - 当前行为依据:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`;旧 `docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md` 仅用于追溯,不承诺继续生成平行日志。 --- ## 2026-09-02 GDD 审批卡的后台 hydrate 不抢占已加载决定 - 背景:项目页首次加载和运行态刷新可能并发 hydrate。卡片已经显示后,短暂的 `hydrateBusy` 会让已打开的评论弹层提交按钮瞬时变灰,用户无法提交已输入的修改意见。 - 决策:`hydrateBusy` 只控制恢复区的重试按钮;已加载审批卡的决定按钮和评论弹层继续依据 `canDecide` 与 `decisionBusy` 门控。只有权威状态显式返回 `recoveryPending=true` 时才禁止决定,并保留弹层中的输入内容。 - 影响范围:AGC GDD 审批卡前端、Fast GDD 审批交互文档;不改变 hydrate command、审批 DTO 或后端状态机。 - 验证方式:运行评论弹层恢复竞态回归、完整 `appSurface.test.ts`,并执行类型、编码和 diff 检查。 - 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`、`apps/ai-game-creator-shell/src/features/project-workspace/GddApprovalCard.tsx`。 ## 2026-08-31 DirectProject 客户端扩展按独立 Skill/MCP 导入 - 背景:DirectProject 需要使用用户在 AGC 客户端导入的市面原生 Skill、MCP 和 Plugin 内容,但第三方内容不应直接安装到运行时 Codex,也不应要求用户转换为 AGC 自定义格式。 - 决策:客户端提供一个全局“扩展”入口,统一接受文件、目录、zip 和 Agent Plugin;Plugin 父项与其中的 Skill/MCP 子项共用来源和索引。Skill/MCP 保留独立开关,父项禁用会阻断子项注入,父项移除会移除整个包的登记。有效启用的组件在下次 DirectProject Codex 启动时按原生 Skill root 和 MCP 配置注入。 - 命名:客户端列表名称与 Codex 运行时名称使用同一个原生标识,不维护 display/runtime 两套名称;重复或同名项保留为新的独立项并自动追加 `-2`、`-3`。Skill 重命名只修改客户端运行时副本中的有效名称,原始导入内容不修改。 - Plugin 边界:Agent Plugins 核心包格式由客户端解析;Codex 原生 hooks、apps、remote plugin 不注入。AGC Runtime Plugin 由通用 Plugin Host 管理,单个可执行文件或脚本不提供手动指定为 MCP 入口的功能。 - 信任边界:不审核第三方 Skill 文案、脚本、二进制、MCP tool 或网络行为;导入阶段不执行内容。客户端只做标准结构识别、必要配置解析和 zip staging 路径边界处理,且不向第三方扩展注入 AGC 凭据或内部路径。 - 影响范围:AGC 客户端扩展设置 UI、本地扩展存储、DirectProject 启动准备/pool fingerprint 和通用 Plugin Host;不新增 HTTP 服务、SpacetimeDB schema 或公开 API。 - 当前实现:客户端导入/list、Skill 临时 root 和 MCP 隔离配置注入均已落地。第三方 MCP 只从客户端已启用独立项生成本次隔离 `CODEX_HOME/config.toml`,每项固定非 required;配置错误或 app-server 启动状态失败只更新对应 `last_error`,内置 `agc_tools` 继续由客户端单独注入。客户端已启用 Skill/MCP 的名称、来源路径和内容指纹共同参与 DirectProject app-server pool identity。 - 验证方式:分三阶段验收:先验证导入拆分和完整列表,再验证 Skill 运行时发现和重命名,最后验证 MCP 配置合并、Plugin 提取和失败隔离;只增加对应的定向测试、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md`、`apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx`、`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs`。 ## 2026-08-30 批准 GDD 直接进入做游戏链路 - 背景:立项策划 GDD 批准后需要给用户一个进入做游戏的自然出口,产品决策改为点击按钮后直接开始建造。 - 决策:批准态 GDD 交付行提供“做成游戏”按钮。点击后读取当前项目的权威 `game/fast_gdd.md`,直接创建自动游戏工作区、导入 `text/markdown` 参考附件,并以固定建造指令自动启动 Direct Codex;不再回首页等待用户二次提交。该动作不复制原项目的 `approvedGddRef`、planning sidecar 或 approval receipt。 - 补充:策划项目切换到 GameAgent 时,`design_artifacts` 的新增或登记信息实际变化必须与一次项目 revision 推进配对;重复切换不重复推进,避免 manifest 已变化而 revision 仍停留在旧值,触发前端同 revision 清单冲突提示。 - 影响范围:AGC 前端 GDD 交付行与现有自动建项/附件导入/Direct Codex 链路;移除首页 RichInputArea 的 GDD 一次性预填链路;不新增 HTTP API、SpacetimeDB schema、迁移、OpenAPI 或正式构建绑定。 - 验证方式:批准态按钮直接创建工作区、导入附件、携带固定首条指令进入项目工作台且重复点击不重复创建的 appSurface 回归;类型检查、编码检查和 `git diff --check` 通过。 - 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。 --- ## DirectProject 平行审计与请求分段计时退役边界(2026-09-23 核准) - 当前合同:完整用户与 Codex 完成 item 保存在 `.agent/conversations/project.jsonl`;GUI 回合不再创建 `runtime/direct-codex/turns/.jsonl` 或对应 `agent.db` 的 `direct.codex.turn` 摘要。旧 `offeredRead` / `firstDesign` 和请求分段计时不再属于生产保证,旧审计专题仅作历史追溯。 - 实现边界:旧 `DirectCodexTurnAudit`、`DirectTurnMetrics`、可选审计 / 计时参数及专用批量 JSONL 追加包装已删除。Provider proxy 本体及独立 model-usage observer 仍有现役用途,不能随旧计时链退役;字节流透传和上游错误传递继续由现有测试验证。 - 保留边界:运行中的界面对话 / 工具耗时、`.agent/model-usage.jsonl`、产品埋点及 Runtime Agent 审计保持各自合同。`project.jsonl` 的完成 item 写入时间不等于 turn 起止或请求阶段计时;不据此补造旧历史耗时。本次不清理或迁移用户项目内的旧审计文件。 - 维护依据:AGC 实施计划“Direct 历史、审计与耗时的现行边界”和“DirectProject Codex 原始历史与异常恢复”。不能以原审计方案或已退役测试为由恢复旧 writer。 ## 2026-08-31 Direct 本轮附件只映射路径,不灌正文、不区别 GDD - 背景:issue #212。首页附件已经复制到 `assets/uploads/` 并登记,但 Direct 首轮只把用户原文发给 Codex,原文件名不是磁盘路径,模型会另起一套玩法。 - 当前决策(2026-09-23 更新):原 sidecar 已由 canonical `userItem.content` 中的 `agc_attachment_reference` 替代;每项保留名称、媒体类型、大小、项目相对路径和状态,经 validation/wire 校验投影。不灌全文、不强制读取、不按 GDD 开特例。未注册的 DirectHome 命令及其专属附件 DTO、渲染、prompt key 已清理,首页先创建项目再进入 DirectProject。 - 影响范围:`direct_codex_attachments.rs` 只保留现役附件清洗与数量边界,canonical user-item 深模块、首页建项与项目工作台继续使用现行结构化输入;历史按主实施计划的完整 canonical 条目合同记录。 - 验证边界:保留 canonical validation/wire、附件路径与状态投影及首页创建项目测试;退役 Home/sidecar 专属测试一并清理,不要求恢复旧独立 attachments 参数。 - 关联文档:`docs/technical/【技术方案】DirectProject本轮附件路径映射-2026-08-31.md`、issue #212。 ## 2026-08-26 运行中自主扩图提案留在编排层 - 背景:`agent-runtime-orchestration` 已能构造和调度动态 DAG,但 LLM 在执行中发现缺少步骤时没有通用的安全扩图合同。 - 决策:新增严格 serde 的 `GraphProposal`(`TaskProposal` + `GraphEdge`)和 `GraphLimits`,由 `TaskGraph::apply_proposal` / `expand_with_proposal` 在内存中构造不可变候选图;新节点默认 `Pending`,边方向为前置 `from` → 依赖方 `to`。 - 安全与一致性:所有 Agent、端点、重复引用、环、节点/边/深度/扇出预算在候选返回前一次校验;边只能指向新节点,禁止给已运行任务原地追加依赖。任一失败保留旧图。成功后的 epoch、基图版本、proposal 幂等和持久化由宿主负责,crate 不调用 LLM/Provider/ToolHost/Runner,也不写 `.agent/runtime/**`。 - 验证:非游戏 conformance 覆盖有效扩图、ready/wave 重算、未知 Agent/端点、重复边、已有任务修改、环、预算、严格 JSON 和原子失败;关联文档为 `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` V1.55。 ## 2026-08-26 通用多 Agent DAG 编排与执行内核分层 - 背景:`agent-runtime-core` 已承接 catalog、run/action 生命周期、lane、宿主 ToolHost、spawn/all-join 和 Provider 契约,但动态任务图的 ready 选择、依赖波次与返工下游闭包仍混在 `platform-agent::game_creation`,其它产品无法复用且非法环会被合并成伪 wave。 - 决策:新增纯 Rust `agent-runtime-orchestration`,依赖方向固定为 `agent-runtime-orchestration -> agent-runtime-core`。公共层只持有任务 ID、Agent ID、通用状态和依赖边,统一负责构图校验、ready、active/satisfied 波次、下游闭包和全量/返工选择;动态构图仍必须是 DAG,跨轮循环通过新的 pass / epoch 表达。 - 产品边界:16 个游戏任务、六组角色、产物/验收条件、Evaluator Markdown 和中文语义路由继续留在 `platform-agent`;AGC 组合根使用公共层校验任务图与 `AgentCatalog`。Runtime store、Runner、Provider、权限、ToolHost、委派 journal、isolated write scope 和 `.agent/runtime/**` 不迁移、不双写。 - 验证方式:非游戏 conformance 覆盖并行分支、汇合、repair closure、AgentCatalog 和非法图失败关闭;`platform-agent` 锁定种子 DAG 与现役波次/返工顺序,并验证环拒绝和 catalog 注入。根检查脚本必须执行新 crate 测试。 - 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` V1.54。 ## 2026-08-27 退款 emergency spool 容量溢出保持可恢复 ## 2026-08-27 退款 emergency spool 容量溢出保持可恢复 - 背景:本机 emergency spool 仅作为 SpacetimeDB 完全不可达时的最后恢复路径,原有 `MAX_BYTES` 分支会直接返回 `Dropped`,导致扣费已经完成但没有可重放记录。 - 决策:达到普通 outbox `MAX_BYTES` 时,将退款记录写入同一持久目录的 `refund-overflow-*` 文件;该文件与普通 pending 文件一样由启动恢复和后台 worker 重放到 SpacetimeDB,且按 refund ledger id 保持幂等。溢出文件不计入普通阈值,但必须触发容量告警;底层磁盘写入失败仍进入关键退款人工补偿流程。 - 影响范围:api-server wallet refund emergency spool、资产失败退款日志、loadtest / 预览 Compose 持久卷、后端架构与开发运维文档。 - 验证方式:运行 api-server `wallet_refund_outbox` 定向测试,确认超限写入并保留 overflow 文件;运行 SpacetimeDB profile 测试、Compose 配置校验、编码和 diff 门禁。 ## 2026-08-27 短期认证状态进入共享 typed projection - 背景:短信验证码和微信 OAuth state 仍只存在 API 进程内 HashMap,多节点请求或 API 重启会直接丢失,无法满足无粘性会话的鉴权恢复要求。 - 决策:`AuthStoreProjectionView` 增加 `phone_codes` 与 `wechat_states` typed 字段,由 `auth_store_projection_meta` 以 JSON 投影持久化;启动恢复、CAS 同步和失败后的权威刷新都覆盖这两类短期状态。验证码哈希使用部署级稳定盐(当前复用 `GENARRATIVE_JWT_SECRET`),各 API 节点必须一致;发码前先刷新权威投影并用占位验证码记录做一次 projection CAS,只有占用成功才调用短信 provider,避免跨节点冷却竞态;认证 handler 在发码、消费验证码、创建/消费微信 state 后都要完成 projection sync,失败即返回服务错误;所有会读取或变更本机认证工作集的认证主链路(登录、刷新、`/me`、会话管理、密码、绑定和微信 state)在领域操作前先从正式投影做一次受 CAS 保护的只读刷新,受保护 Bearer 中间件也会在进入业务 handler 前执行同样的刷新,刷新失败时 fail closed,不能依赖粘性会话;同步遇到 CAS 冲突时,若本次尝试期间没有新的本地变更则恢复正式快照,若仍有待同步 revision 则由后续认证请求重试,避免节点永久卡在 pending。微信 OAuth state 设置有界活动数量,避免单个 JSON 投影无界膨胀。短期状态仍由 `module-auth` 内存工作集执行领域校验,但不再把本机 HashMap 当作持久化或跨节点真相。 - 影响范围:`module-auth` projection、`spacetime-module` auth schema/procedure、`spacetime-client` bindings/facade、api-server 手机号 / 微信 handler、认证架构与运维文档。 - 验证方式:运行 module-auth projection roundtrip(验证码可跨恢复校验、微信 state 可跨恢复消费)、SpacetimeDB schema/runtime/DDD 门禁、api-server 定向测试、编码和 diff 检查。 --- ## 2026-08-27 外部生成历史采用受控保留清理 - 背景:`external_generation_job`、`external_generation_job_summary` 与 `external_generation_job_event` 都是持久化表;摘要和 payload 边界收紧后,已确认的终态历史仍会继续占用 SpacetimeDB 常驻内存,且事件审计链会随任务数量增长。 - 决策:新增仅 migration operator 可调用的 `prune_external_generation_job_history_and_return`。默认按 `source_module=editor-canvas`、30 天保留期和 `job_id` 游标分批运行;只删除主任务与摘要状态一致、属于 completed / failed / cancelled、摘要已有 `notification_acknowledged_at` 且终态时间达到 cutoff 的任务。事件、摘要和主任务仍按同一事务顺序删除,但每次事务最多删除 256 条事件;事件未删完时保留任务与摘要并返回同一个 job cursor,维护脚本下一次继续,避免单个任务形成无界事务写集。默认 dry-run,必须固定 dry-run 返回的 cutoff 后再 apply;pending / running、未确认通知、摘要缺失或状态不一致的数据永不删除。其他 source module 必须显式指定并单独评估;资产对象和钱包流水不随任务历史删除;不新增自动定时器或 runtime 清理权限。 - 影响范围:`server-rs/crates/spacetime-module/src/external_generation.rs`、外部生成事件 job_id 单列索引、SpacetimeDB 生成 bindings、`scripts/spacetime-maintain-external-generation-jobs.mjs`、架构与生产运维文档。 - 验证方式:覆盖终态 / 活跃态 / 已确认与未确认摘要、状态或身份不一致、cutoff 边界测试;运行 SpacetimeDB module tests/check、bindings 生成、schema/encoding/diff 门禁,并在维护窗口先 dry-run 再 apply。 - 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、PR #203。 ## 2026-08-27 SpacetimeDB 工具链统一升级到 2.8.3 - 背景:SpacetimeDB 2.8.0 引入 TypeScript submodule 与调度延迟观测,2.8.1 修复 v1 WebSocket 订阅移除死锁、TypeScript SDK `array` 读缓存别名和 Rust string 默认值支持,2.8.2 修复 table accessor 改名自动迁移,2.8.3 修复 scheduled function 从实际执行时间重排导致的长期漂移。仓库若继续锁定 2.7.0,会保留这些已知运行时与 SDK 问题。 - 决策:`server-rs/Cargo.toml` 的 `spacetimedb`、`spacetimedb-sdk`、`spacetimedb-lib` 精确锁定 2.8.3;本地 CLI / standalone、Rust bindings、worker smoke 本地镜像、官方容器压测镜像和生产 provision 下载根同步对齐 `v2.8.3`,CLI / standalone commit 门禁为 `8e410d28...`。2.8.3 不再使用 2.7.0 的 hotfix3 特殊资产标签口径,但同版本 commit 校验继续保留。 - 影响范围:Rust workspace lockfile、SpacetimeDB bindings、本地 dev 版本门禁、容器 smoke / loadtest、server provision Jenkins 与项目 SpacetimeDB skills / 文档;现役 module 未使用 submodule,本次不修改 schema 或 migration。 - 验证方式:核对 CLI 版本和 commit,重新生成 Rust bindings,运行 `npm run check:spacetime-schema`、相关 Cargo check / tests、server provision 工具测试、dev 调度测试、encoding 和 diff 门禁。 - 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 ## 2026-08-24 AGC Direct 媒体能力只通过客户端语义工具开放 - 背景:资源页已经补齐视频、角色动画、音效和背景音乐的 create/derive 能力,但 Direct Codex 只能准备标准美术包,无法查询已登记源资源或表达新增媒体意图。直接开放 Tauri invoke 会把项目路径、revision、operation、幂等键、登录态和事务权力交给模型。 - 决策:只新增 `agc_list_registered_assets` 与 `agc_create_or_derive_resource` 两个语义工具。Codex 只能提交资源过滤条件或 kind/mode/localAssetId/prompt/name;客户端权威解析 manifest 来源,生成并恢复稳定 operation/idempotency,串行付费调用,执行权限、项目锁、画布/素材目录准备、下载校验和 manifest CAS。完全匹配的 pending 请求自动恢复,不创建替代付费请求。 - 输出边界:资源查询和生成结果只投影相对路径、稳定 Canvas/resource/asset/task 身份、序列帧身份、pending 状态及脱敏告警;不返回完整 manifest、prompt、model、provider route、绝对路径、URL、Token、Cookie 或 API Key。角色动画及视频/音频新请求统一携带同名画布与素材目录上下文;已有冻结请求不迁移、不重写。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md`。 ## 2026-08-23 External 去背景绑定权威静态来源与真实画布尺寸 - 背景:External v1 去背景曾把调用方 `assetKind` 原样带入持久化,并允许 `sourceImageSrc=A + targetLayerId=B` 覆盖不同资源;Python helper 又为所有画布完成请求固定生成 `1024×1024` 占位,导致非方形透明结果按占位尺寸拉伸。 - 决策:入队前从当前 owner 的项目资源或素材库解析来源权威语义类型,资产对象存储类型只参与非静态媒体门禁;显式项目资源 ID / 素材 ID 优先于 objectKey 回退,同一纯 objectKey 对应的候选权威元数据不一致时返回 `400` 并要求用 `sourceResourceId` 或业务 ID 消歧,禁止按列表首条决定类型。请求类型冲突或任一记录属于视频、音频、动画、图片序列时返回 `400`,队列只保存服务端解析出的静态语义类型。无 `canvasCompletion` 的原位替换优先比较双方 `assetObjectId`,任一缺失时回退 canonical `(bucket, objectKey)`,并要求默认类型一致;纯 objectKey 省略 `sourceResourceId` 时自动绑定目标图层资源并写入队列,由 Worker 复验同一绑定。helper 使用画布会话时必须取得真实源宽高或显式 `canvasWidth + canvasHeight`,不再猜测方形尺寸。 - 影响范围:External v1 去背景入队与 worker 复验、OpenAPI、Python helper、外部编辑器 skill 和相关契约测试;不修改 SpacetimeDB schema、BgFilter 协议或去背景输出尺寸语义。 - 验证方式:覆盖非静态类型与权威类型冲突、来源/目标不同对象拒绝及同对象通过、非方形 helper completion;运行 api-server 定向测试、helper self-test、OpenAPI 解析、编码与 diff 门禁。 ## 2026-08-20 UI Editor LLM 递归输出与参考图单文件限制 - 背景:结构识别、界面语义建议和多图合并直接把 LLM 工具 arguments 反序列化为递归树;结构识别与语义建议还在 async command 中同步读取并 base64 编码参考图。模型异常输出或过大图片可能造成不受控内存、栈和 async worker 占用。 - 决策:三个工具调用的 arguments 统一限制为 `1 MiB`,先解析通用 JSON 并迭代检查,再进入递归业务类型。结构识别按每棵树独立限制 `512` 个 LLM 节点 / `32` 层,不跨树求和且不计 Rust 页面根;语义建议限制 `4` 节点 / `4` 层;合并计划限制 `512` 节点 / `32` 层。超限整次拒绝,不截断或交付部分结果,日志不记录 arguments 正文。 - 输入边界:`merge_ui` 继续直接接收 `State`,不修改 Tauri/frontend IPC 参数;进入 Rust 后、发起 LLM 前按每棵源树独立限制 `512` 节点 / `32` 层,不跨树求和,并限制 `2 MiB` 序列化投影。UI 设计参考图只设单张 `5 MiB` 上限,不设批次合计或像素数上限;元数据检查、有限读取和 base64 编码进入 blocking worker,不新增命令超时。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 2026-08-15 恢复重启栈溢出:主循环专用 worker 从「枚举入口」改为不变量 CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 `tokio-rt-worker` 栈溢出并 SIGABRT。属于本文件 pitfalls「Runtime 后台执行不能让大型 async frame 共用默认 worker 栈」的同一失败类,但暴露出该条目的判据形式本身有缺陷。**Windows 上可直接复现,不必去 Linux/WSL**。 - **是本分支引进的,但根因在 master。** 同机同用例、默认栈 A/B:master(`9f5c84ee7`)通过,HEAD(`deae1e08c`)溢出。二分 `RUST_MIN_STACK` 量化:started 变体 master 需 1536–1792 KiB、HEAD 需 2048–2176 KiB(默认 2048,**超出不到 128 KiB**);队列 drain(`drain_next_*`)master 需 1280–1536 KiB、HEAD 需 1792–1856 KiB。`M1B-2` 往主循环与恢复扫描加分支消耗 256–576 KiB,而 master 本就只剩 256–512 KiB 余量——**边界一直是缺的,只是以前刚好没超**。 - **不是递归**:16 MiB 下 2.14s 通过,逐级下探到 2176 KiB 仍通过,符合 pitfalls 记的「大型 async poll frame」而非业务递归。 - **根因:恢复重启是第四个入口,从未被纳入专用 worker。** `recovery_scan.rs` 手写 `tauri::async_runtime::spawn` 直接跑 `drain_game_creator_agent_background_tasks`——正是仓库明确规定必须上 16 MiB 的那个 future。pitfalls 原文按入口枚举三个(普通后台任务、静态委派子任务、manifest ready-task 首次执行),**漏掉一个入口不会产生任何信号**,故判据改写为不变量:所有会进入 Agent 主循环的 future 必须在 `agent-runtime-worker-*` 专用线程上轮询。 - **一并收拢队列 drain。** `spawn_next_..._with_lock` 与 started 变体只差一个 poll 帧(实测 256–384 KiB),却长期分两种栈待遇。HEAD 上它只剩 192–256 KiB 余量,**小于本分支单个工作包的消耗量**,即下一个同量级工作包必然顶穿;且它有 8 个生产调用点,崩在哪条取决于当时路径,比恢复路径更难定位。 - **处置**:统一常量 `AGENT_RUNTIME_BACKGROUND_WORKER_STACK_BYTES`;`spawn_next_..._with_lock` 改用专用线程,**签名保持 `-> ()`、8 个调用点不动**——该入口是 best-effort 幂等语义(拿不到锁即返回、后续 wake 重试),建线程失败只需记录并随闭包释放锁,无需像 started 入口那样交还锁、也就不需要握手;`recovery_scan.rs` 两处手写 spawn 收敛为 helper 调用。恢复重启改走 `spawn_started_..._with_lock` 顺带补上首轮轮询握手——原写法把执行锁 move 进一个无人保证会被轮询的 future,运行时关停时 run 会永远停在 running 且无主。 - **回归改为钉不变量,不钉余量。** drain 入口在 `#[cfg(test)]` 下记录 `std::thread::current().name()`,用例断言其全部以 `agent-runtime-worker-` 开头。**变异验证**:把 `recovery_scan.rs` 改回手写 spawn 并把 `RUST_MIN_STACK` 抬到 16 MiB(因而不会溢出),用例仍以 `["tokio-rt-worker", "tokio-rt-worker"]` 失败——证明该断言独立于栈余量。只断言「默认栈下没崩」的用例在这次崩溃前全部是绿的。 - **验收标准也随之改变**:不是「默认栈下通过」,而是「主循环栈依赖消失」。修复后两条用例在 `RUST_MIN_STACK=1024 KiB`(半个默认栈)下通过;修复前分别需要 2048+ 与 1792+。 - **未做**:不抬 `RUST_MIN_STACK`(pitfalls 明令禁止,CI 有意不配置该变量)、不调大 tauri 全局运行时 worker 栈、不去削 `M1B-2` 的帧——余量不是修复。 - **需回流 master**:master 同样存在「恢复重启走默认栈」与「`drain_next_*` 余量偏低」,只是尚未触发;与 `manifest.rs` 的 Windows 构建修复同理。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/{task_queue.rs,recovery_scan.rs}`;pitfalls.md 同名条目已按不变量重写。 ## 2026-08-15 M1C-0b:静态委派 durable status 前向兼容实现完成 - **落地**:`StaticDelegateContractStatus` 采用手写 serde。四个已知 durable 值保持原有字符串;结构良好的未知字符串解析为 `Unknown(raw)`,并在再次序列化及读-改-写时原样保留 raw;非字符串输入仍拒绝。该包只改读路径,不新增状态写入方。 - **门禁**:`Unknown` 进入 completion barrier 的独立计数与 waiting blocker,不能让 Supervisor 在未知状态未处理时收束;返工入口遇到 `Unknown` 无条件拒绝,即使 lineage depth 为 0;lineage 将其按“其它”质量返工分支保守计数(`depth + 1`、`round = 0`),只会拒绝、不会放宽额度。 - **损坏边界**:截断、非法 JSON、非 UTF-8 或超过 128 KiB 的 sidecar 仍维持整目录 fail closed,不改成单条跳过或告警,以免 barrier 少计数而错误放行。 - **范围与依赖**:不包含 `M1C-1` 的审批写入、`gdd-approval` pending、receipt、UI 或构建准入;不 bump durable schema 版本。为覆盖所有既有读路径,补了 planning Provider、自治 liveness 与终态扫描的 fail-closed 消费门,但没有新增或改变 `M1B-*` 功能依赖;该包仍可在 `M1C-0` 基础上独立验证。既有三种状态及历史记录行为保持不变。 - **结论**:`WP1` 的 `repair_depth≤1` 结论未被推翻;Unknown 只增加前向不兼容时的保守阻塞,不会放宽普通做游戏链路的返工深度门。 - **验收门禁与关联**:定向回归须覆盖未知 raw round-trip、barrier/waiting、无条件返工拒绝、保守 lineage 分类,以及损坏 sidecar 整体锁死;详见技术方案第 23.7 节裁决三与第 23.8 节 `M1C-0b` 门禁。 ## 2026-08-15 完美像素编码前按整数倍 nearest 放大到接近源图 - 背景:2026-08-10 起成功产物直接落逻辑网格 PNG,画布按资源实际宽高显示,结果会明显小于源图。用户要求保持逻辑图宽高比,并把产物放大到接近原图;禁止再走非整数 nearest 拉回精确源尺寸(会让逻辑块宽窄不一)。 - 决策:`style="pixelArt"` 与手动 `POST /api/editor/images/pixel-art-snaps` 仍共用 `snap_pixel_art_with_grid_policy`。检测、切线、采样、Alpha、strict 拒兜底不变。`resample` 之后、`encode_png` 之前,用单一整数 N 做 nearest 放大:`N*` 为 `(C·W + R·H) / (C² + R²)`,在 `floor` / `ceil`(小于 1 当 1)中取距离平方更小者,并列取较小 N;超单边 `10000` 或总像素 `8294400` 则降 N,最低 `N=1`。只持久化这一张 PNG。手动算法指纹升为 `perfect-pixel-v3`。 - 不做:改 walker、透明补边、裁切、横纵不同倍率、Lanczos / bilinear、另存逻辑图、前端框缩放、失败路径、新测试。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`、`docs/【编辑器】图片画布结构化持久化与迁移回滚方案-2026-07-19.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 - 背景:真实 `gpt-5.6-sol / max` 验收中,`art-director` 失败后已形成 `ready + needs-repair` delivery,但认领、合同读取、claim observation 和完成 blocker 均硬编码为 Supervisor-only;实际直属父 Run `code-prototype` 无法消费回执,随后又发起 29 次 Provider 请求。 - 验证方式:覆盖合法认领与合同精确读取、错误 Agent/Run/delegation 拒绝、delivery 身份篡改阻断、唯一安全默认返工、普通失败零后续 Provider lifecycle,以及新的真实 Provider 空项目轮次。顺带收紧 `validate_executable_inline_javascript_syntax`:正文游离 `<` 不再吞掉后续 `