@@ -1,5 +1,160 @@
# 踩坑与排障记录
## 客户端图标素材描述不能先包装再截断
- 现象:资源画布填写了具体图标需求,平台实际收到的 `iconDescriptions` 却只包含泛化的小游戏美术指令,生成结果不遵循输入。
- 原因:客户端先给用户描述套“Web 小游戏首版原型、核心美术素材”和默认 brief 模板,再把包装后的文本压入单项 `200` 字符;固定前缀已占满额度,用户描述在发给 API Server 之前就被丢弃。服务端校验只能看到被截短的文本,无法恢复原需求。
- 处理:客户端画布图标生成只 trim 描述并以单项 `iconDescriptions` 原样提交,保留内部换行;面板与原生入口按 `200` 个 Unicode 码点校验并拒绝空白或超限输入,不静默截断、不机械拆条。服务端共用的提示词流程负责规范图、背景与排布要求;其它图片生成的 `32000` 字符上限保持不变。
- 验证:检查实际请求体与 trim 后的原文一致,并覆盖 `200/201` 码点、补充平面字符、换行和空白输入;不能只断言“请求长度未超限”。完整合同见 [画板图标素材生成入口设计 ](../../【编辑器】画板图标素材生成入口设计-2026-06-15.md )。
## 2026-09-21 不同渠道的包体在同一台设备安装会互相顶掉
- **现象 ** :在一台已经装了某个渠道 AGC 客户端的设备上安装另一个渠道的安装包,装完后旧客户端直接消失(安装目录被覆盖、卸载项被接管),更新端点、平台服务器与本地登录态一起换成新渠道的;两个渠道的客户端无法共存。
- **原因 ** :渠道此前只烘焙了 `plugins.updater.endpoints` 与 `VITE_AGC_PLATFORM_CHANNEL` ( `apps/ai-game-creator-shell/scripts/build-release.mjs` 的 `createChannelConfig` ),`productName` / `identifier` 用的是渠道无关的基线值。Tauri 的 Windows 安装目录与卸载项由 `productName` 决定,WebView2 数据目录与客户端数据目录由 `identifier` 决定,于是所有渠道落到 `%LOCALAPPDATA%\陶泥儿` 、`HKCU\...\Uninstall\陶泥儿` 与 `%APPDATA%\world.genarrative.ai-game-creator` 。
- **处理(现行口径) ** :渠道进入安装身份,默认渠道保持基线身份不变,其它渠道派生 `<产品名> <渠道显示名>` 与 `<基线>.<渠道>` ;身份与端点在同一次构建期 `--config` 注入。见决策记录 2026-09-21 条目。
- **核对方式 ** :装完任渠道的包后看 `HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\<产品名>` 的 `InstallLocation` 、`%APPDATA%\<identifier>` 与主程序窗口标题是否按渠道分开;同名安装目录或同名数据目录说明身份没有生效。
- **易错点 ** :只改安装包文件名或快捷方式名而不改 `identifier` ,两个渠道仍会抢同一份 Agent Runner / 项目锁与登录态;反过来把渠道后缀加在默认渠道上,既有安装的升级链会断(客户端认不出旧安装)。相似前缀目录(如 `world.genarrative.ai-game-creator-backup` )不得进入提权 ACL 的 managed 范围。
- **关联 ** : `apps/ai-game-creator-shell/scripts/channel-identity.mjs` 、`build-release.mjs` 、`build-macos-ci.mjs` 、`src-tauri/src/config.rs` 。
## 发布器守卫拒绝时不要把 process.exit 用在 fetch 句柄未关闭处
- 现象:`agc-template-library-publish.mjs --dry-run` 撞上「同一 `templateVersion` 的 ZIP 不得变」门禁时,终端只剩一句 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c` ,看不到任何拒绝原因,看起来像脚本崩溃而不是被拒绝。
- 原因:`main().catch(...)` 里直接 `process.exit(1)` ;此时 dry-run 刚用 fetch 读过公共清单,句柄仍在关闭流程中,Node/libuv 在 Windows 上先抛断言,把真实错误信息挤掉。
- 处理:`catch` 里只设 `process.exitCode = 1` ,让事件循环自然退出;同批把说明文档纳入受管发布(见决策记录 2026-09-21 条目)。
- 验证:把模板源复制到临时目录、把某个模板的 `templateVersion` 改回与线上同版本并改动一个字节,`--dry-run` 应打印 `同版本 ZIP 内容或尺寸变化,请递增 templateVersion` 且退出码为 1,不再出现 `Assertion failed` 。
- 关联:`scripts/agc-template-library-publish.mjs` 。
## macOS 只出 arm64 单架构,universal 必须失败关闭
`stage-node-runtime.mjs` 只把**构建宿主的 Node**打成便携运行时(官方发行版是单架构,没有 universal 发行版),而 2026-09-21 之前 `build-macos-ci.mjs` 构建的是 `universal-apple-darwin` : macOS Job #7 ~#13 因此在 `stageNodeRuntime` 直接抛「Node 运行时不支持发布目标:universal-apple-darwin」。期间出现过一版「按宿主架构放行」的过渡实现(`targetRuntime` 对 universal 返回宿主架构),它能骗过通用包自检(`check-macos-bundle.mjs` 按 `process.arch` 校验),但**Intel Mac 上这份 arm64 侧车不可执行**,等于把坏包发出去。当前决策:macOS 固定只构建 `aarch64-apple-darwin` ,清单只登记 `darwin-aarch64` , `targetRuntime('universal-apple-darwin')` 保持失败关闭。恢复 Intel 的正确路径是先在 staging 支持按架构各带一份**同版本**运行时(另下载另一架构官方发行版)并让通用包自检按架构分别校验,再切回 universal 目标、把 `darwin-x86_64` 键登记回去;不得用「只带宿主架构」充数,也不得把 arm64 产物登记成 x86_64 键。
## release 冷备空间不足会把生产留在维护态
`Genarrative-Stdb-Module-Publish` 先进入维护模式、停掉 API/controller/worker,再执行发布前冷备份;archive 口径要求 `data × 1.1` ( 40.6GiB 数据 → 44.7GiB,加上生产根盘只剩 13.5GiB),于是 2026-09-21 的 release 发布在停服后失败并保持维护态,站点 503 直到人工恢复。规则:空间预检必须先于 `maintenance-on` 与停服;archive 不足且未显式禁用降级时改用 files(`max(data × 0.05, 2GiB)` ,不落地本地归档,但 files 不支持 `--defer-upload` ,会从 async 收敛为 sync);只有真正开始 `spacetime publish` 之后的失败才允许保持维护态。另:定时备份的失效锁在当前仓库版本会自动清理,但生产机 `/var/lib/genarrative/backup-tools/database-backup-to-oss.mjs` 若是旧版会拒绝抢锁并要求人工删锁,需随 provision 更新。
按上游 [#5555 ](https://github.com/clockworklabs/SpacetimeDB/pull/5555 ) 的 retention 语义,只有最近 `retain-snapshots` (默认 2)份 snapshot 与覆盖它之后的 commitlog 段是重启所需,其余历史可丢;因此备份改为 `files + full + --minimal --retain-snapshots 2` ( release 实测 40G → 3.6G,热备不停服),不再做增量差异计算,也不需要 44.7G 冷备空间。
## copyArtifacts 报「Unable to find project for artifact copy」的用户触发构建差异
Copy Artifact 插件在**非 SYSTEM 认证**下按「认证用户」判权:只有当被复制 Job 的 `CopyArtifactPermissionProperty` (仓库里由 Declarative 的 `copyArtifactPermission(...)` 维护)显式列出当前消费者,或者该 Job 对认证用户开放 Item.Read 时才放行;`ACL.SYSTEM2` 的定时构建会短路通过。因此会出现「定时调度一路成功、手动发布必挂」的现象(2026-09-21 手动发布 #6/ #7 与同期的用户触发探测全部命中,定时调度 #104 + 正常)。`Genarrative-Agc-Global-Version-Issue` 生产权限模式的授权名单必须同时包含 `Genarrative-Scheduled-Revision-Trigger` 与 `Genarrative-Manual-Build-And-Deploy` ;改完 `copyArtifactPermission` 后要先跑一次发号 Job 把 Job property 写回 Jenkins,只改仓库文件不生效。
## 手工发布目标不会自动映射成 AGC 更新渠道
`Genarrative-Manual-Build-And-Deploy` 的 `DEPLOY_TARGET=release` 只控制 Stdb / API / Web 全量发布,不会自动成为 AGC 的 `AGC_UPDATE_CHANNEL` 。2026-09-21 的手工发布 #10 就因此让 Windows #107 与 macOS #16 使用默认 `dev` ,把 `0.1.95` 上传到 `agc/dev-win` 、`agc/dev-mac` ,而 `agc/release-win/latest.json` 、`agc/release-mac/latest.json` 保持 404。现行口径:手工入口按 `release -> release` 、`development -> dev` 同时给 Windows 与 macOS AGC Build 传 `AGC_UPDATE_CHANNEL` ;补发已烧号的同一版本时用相同 `AGC_RELEASE_VERSION` 直接重跑两条 AGC Job,不重新发号。OSS 发布对象是 `agc/<channel>-win|mac/` ,不存在 `agc/release/` 这一层。
## 同一条链路两处上限不一致:平台合法产出被客户端整条丢弃
- 现象:客户端报「生成素材失败:platform-generation-result-unknown: 异步生成完成结果无法绑定到 operationId: External Editor 旧同步结果的图集切片超过 64 个」,而平台侧这次生成**其实已经成功并切完图**(任务账本耗时正常、`assetId` 为空、没有任何素材落盘,付费产物被丢)。
- 成因:图集切片上限在链路里存在两份字面量——平台切分、Agent 工具 schema `sliceCount` 与持久化产物批次都是 256,客户端结果绑定门写着 64(`agent/generation/{canvas_generation.rs,external_generation_state.rs}` )。自动切分(`connected-components` + `sliceCount=null` )切出 65~256 片是合法产出,客户端比平台更严就会把结果整条判失败。
- 处理:客户端门统一到 `PLATFORM_ART_SPRITESHEET_MAX_SLICES = 256` ,判据与文案各只留一份(数字由常量插值),并在注释里点名三处同值权威(平台切分常量、工具 schema、公开契约)。
- 复用判据:凡是「平台产出 → 客户端校验后落盘」的链路,客户端门只能表达**安全 / 预算**约束,不得比平台的产品上限更严;两边上限要引同一个常量或同一份文档,改一边时必须同时改另一边,并补一条「上限之内必须能落盘」的回归用例。
## 2026-09-21 画布卡片「拖一下就触发点击」:阈值与点击抑制必须是卡类手势的一份判据
- **现象 ** :拖动未生成的资源占位卡(背景音乐 / 音效等所有类型)松手后,卡片自己的点击语义被多执行一次——生成浮层被顺手弹开或收起。
- **成因 ** :浏览器在 `pointerdown` 与 `pointerup` 落在同一节点上时**一定会补一次 `click` **,与中间移动了多少无关。生成占位卡那条手势把「收到过 pointermove」当成拖动信号(按下时浏览器就可能补一次零位移的 move),而且没有任何点击抑制;资源卡那条链路早就用 `RESOURCE_CANVAS_DRAG_THRESHOLD` ( 5px) + `skipNextResourceCardClickRef` 处理过这件事,两条链路各写一套,于是只有占位卡漏。
- **处理(现行口径) ** :卡类手势只有一份判据 `resourceCanvasGestureExceededDragThreshold` ( `features/resource-canvas/resourceCanvasCardGestureModel.ts` ,阈值仍取 `RESOURCE_CANVAS_DRAG_THRESHOLD` );拖动收尾那次 `click` 由手势层登记一次性抑制、宿主在「点卡片」的入口消费(占位卡是 `consumeDragClick` ,资源卡是 `skipNextResourceCardClickRef` ),抑制活过一个宏任务就清干净。「拖动中」样式也从越过阈值那一刻起才亮。
- **相邻一档 ** :从删除按钮起手、松手落回卡片的手势,`click` 会被派给共同祖先(卡片)——判据要看**起手点**(`ResourceCanvasGenerationPlaceholderCardView` 的 `gestureOriginRef` ),不能只看移动距离。
- **易错点 ** :把「拖动样式」放在 `pointerdown` 上置位,等于承认「按下即拖动」,紧接着的点击又会被自己的抑制吃掉;阈值与抑制必须同时按同一判据走。
## 2026-09-21 画布浮层几何只能按卡片的真实屏幕位置算,分页锚点必须让开钉死的标题栏
- **现象一(浮层与画布边缘碰撞) ** :生成浮层底边越过画布下沿被 `overflow: hidden` 切掉,卡片靠左右边时浮层还会有一半落到画布之外。
- **成因 ** :可用高度只按「安全带高度 − 卡高」算,等于假设占位卡永远贴在安全带上沿;占位卡是追加在栏目内容最下方的,落到下半屏是常态。水平方向则完全没有收口——浮层 560px 宽、按卡片中心居中展开,卡片靠边必然越界。
- **处理(现行口径) ** : `resolveResourceCanvasGenerationPanelPlacement` 吃卡片的**屏幕顶边**,顶边与可用高度都由真实位置算(卡下面塞不下就改为盖住占位,且顶边收进安全带);水平用 `clampResourceCanvasGenerationPanelAnchorX` 把锚点收进「浮层左右各留 12px」的区间。浮层宽度口径(`min(560px, calc(100% - 24px))` )在 CSS 与模型里各一份,有声明级守卫用例钉住同源。
- **现象二(生成任务开关压在标题栏上) ** :资源栏目画布页顶部钉着整宽的栏目标题栏(`.game-resource-book-scene-titlebar` ,42px,右端是「返回资源总览」),右上角开关按总览页的坐标摆就会直接压在它上面。
- **处理(现行口径) ** :「生成任务」锚点按页分档(`canvas-overview` / `canvas` / `run` / `editor` ):画布页让开那条标题栏的高度再加一段间隙,总览页仍贴画布顶边内缩;两档之间的高度差走 `margin-top` 过渡(`prefers-reduced-motion` 下关掉)。让开多少与标题栏多高是**跨文件关系**,守卫用例读全局样式表里那条 `min-height` 断言「让开得比它高」。
- **易错点 ** :给锚点写死 `top` ,或只按某一页的 chrome 算一次坐标,工具条换行、页面切换或标题栏高度调整后都会重新压上去。
## Jenkins Windows 节点的 PATH 白名单决定 Godot 原生扩展能否构建
`Genarrative-Agc-Windows-Build` 在阶段里用 `AGC_WINDOWS_PATH` 整体替换 PATH、不继承节点机器的 PATH,所以 Godot C++ 引导需要的 CMake 与 Python 必须显式写进这份白名单,装在机器 PATH 上并不生效。2026-09-21 的 #97 – #99 连续失败都停在 `Get-Command cmake.exe` ( #93 – #96 是更早的手写 C ABI 在 MSVC C 模式下的对齐问题):节点只有 Visual Studio Build Tools( `C:\BuildTools` )自带的 CMake 3.31,缺 Python 3。修复后白名单包含 `C:\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin` 、`C:\Python312` 、`C:\Python312\Scripts` , preflight 校验 CMake ≥3.25、Python 3 和 Visual Studio 17 2022 生成器;把 `cmake.exe` 单独复制到别的目录会丢掉 `share/cmake-*/Modules` ,不能替代加入安装目录。新节点的 Python 用 `python-3.12.10-amd64.exe /quiet InstallAllUsers=1 TargetDir=C:\Python312 PrependPath=1 Include_launcher=1 InstallLauncherAllUsers=1` 静默安装即可,CMake 不必另装。
## AGC 画布绑定前置查询不能取全量项目列表
- 现象:dev 上「AI 生成图片」连续失败,卡片显示 `解析读取外部画布项目响应失败:error decoding response body` ,每条恰好 `1 分 00 秒` (三条同因,各自独立计时)。
- 原因:绑定前置的 `GET /api/(external/v1/)editor/projects` 缺省 `view=full` ,会把账号下每个项目的画布与全量资源一起返回(19 个大项目的 fixture 就已超过 4 MiB);客户端这条请求只有 60 秒预算,卡在读正文时被 reqwest 总超时打断。而 `reqwest::Error` 的 `Display` 只打印 kind,超时、正文被截断和非法 JSON 显示成同一句话,现场看不出根因。
- 处理:只确认项目身份的消费者固定取 `view=summary` (站内与外部路由都支持,缺省 `full` 不变,未知取值失败关闭);摘要视图不做内联媒体修复、不带画布与全量资源;外部请求失败文案补 kind 语义与 source 因链,且不拼接 URL。
- 验证:站内路由用例断言 `view=summary` 不回传 `canvas / layers / resources` 且不触发媒体修复、`view=unknown` 返回 400;AGC 壳用例断言失败文案不再等于 `error decoding response body` 、补出因链且不含绝对地址。
- 关联:`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` 。
## 远端资源编辑终态必须指出唯一出口
- 现象:「生成背景音乐」再次提交 0.1 秒就失败,卡片只有 `remote-terminal-failed: 远端资源编辑已明确失败,不允许再次请求` ,既没有原因也没有下一步。
- 原因:上一次同 `operationId` 的请求被平台确定性拒绝(HTTP 400 或任务 `failed` )后,账本落到 `remote-failed` ,之后所有重试都在 `ensure_resource_edit_phase_resumable` 失败关闭;唯一出口是「待恢复资源编辑」里的移出恢复队列,但终态文案没有指向它。
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担。
- 验证:`remote_failed_status_is_terminal_and_can_only_be_archived` 、`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过,账本序列化不含上游失败原文。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs` 。
## Tauri `--no-sign` 会连带跳过 updater 签名
AGC macOS 发布入口一度传入 `--no-sign` (目的是绕过没有 Apple 证书的代码签名),结果 Tauri 打印 `Warn Updater signing is skipped due to --no-sign flag.` ,产物只有 `* .app.tar.gz` 而没有 `.sig` ,发布入口按设计在「缺少更新包签名」处失败关闭(2026-09-20 首次 Jenkins 实跑命中)。正确做法是不传 `--no-sign` ,改为剥离 `APPLE_*` 凭据让 Tauri 跳过 Apple 签名——minisign 更新包签名与 Apple 代码签名这两个开关在 Tauri 里并不独立。Apple 签名状态要按 `codesign -dv` 实测记录,不能硬编码。
## 复用 workspace 的构建必须显式清理本次要写的产物
Jenkins workspace 跨构建保留:上一轮失败留下的同名 `陶泥儿_<version>_universal.dmg` 会让 `hdiutil create` 以「文件已经存在」失败,而上一轮遗留的 `* .app.tar.gz.sig` 更危险——本轮即使没签出签名,验签门禁也会读到旧签名而误判通过。构建入口必须在构建前删除本次将写出的确切路径(更新包、签名、同版本 DMG 及其校验文件、`latest.json` 、`release-notes.txt` ),`hdiutil create` 同时用 `-ov` ,让「归档里的产物来自本次构建」成为结构性事实而非假设。
## AGC macOS 单次构建耗时集中在主 crate 重复编译
AGC 主 crate( `genarrative_ai_game_creator_shell` )单架构 codegen 约 15– 20 分钟,而每次 Tauri 构建都会重新生成前端 `dist` , `build.rs` 对 `dist` 目录的 `rerun-if-changed` 因此每次都判定变化,导致两个架构各重编一次主 crate。实测:`CARGO_BUILD_JOBS=4` 时首次 Jenkins 构建 78 分钟,提到 6 后为 41–43 分钟且成功;依赖 crate 走 sccache 与 target 缓存,首轮 0 命中属预期。剩余优化空间在「不必要地重建 dist」这一层,需单独设计(例如按内容摘要决定是否重跑前端构建),不要在发布入口里用假缓存换取速度。
## Godot C++ 扩展构建与对象生命周期
- 原生引导通过官方 `godot-cpp` 管理 Variant、String 和 Ref,不自行维护 ABI 存储。Godot 类型必须在扩展终止回调内释放,不能依赖 DLL 静态对象析构;桥节点可能已经退出,应按实例 ID 核验存活再回调。
- 正式 Windows 构建使用 CMake 的 Visual Studio x64 generator,并实际验证 MSVC 编译;不能用 GCC 成功替代 MSVC 验收。固定官方归档按 SHA256 校验,缓存源码被修改时拒绝构建并保留证据。
## Rust 同步回调的测试记录按线程隔离
- `shared-contracts` 的资源 kind reporter 是进程级回调。仅给注册和断言加锁,无法阻止其他并行 manifest 测试触发该回调,导致日志数量和内容断言偶发混入其他测试记录。
- kind 解析与回调在调用线程同步执行,测试收集器使用线程局部存储,各用例开始时清空本线程记录;生产 reporter 保持不变。保留完整记录断言,并用两个线程分别解析和核对记录,验证隔离;不要通过全局串行测试或放宽断言掩盖干扰。
## AGC 自动同步必须绑定真实项目生命周期
- 正式客户端在单窗口中用 React 状态打开/切换工程,窗口 URL 不代表当前工程。原生后台同步应读取由当前窗口显式登记的活动工程;首次打开、离开、切换、关窗及退出等待分别验证,不能只用携带 `projectPath` 的独立测试窗口证明正式入口可用。
- 增量文件没有变化不等于远端清单没有变化。项目名和完整性元数据也参与提交判据,避免临时跳过恢复后永久停留在 partial,或新出现超限文件后仍显示 ready。历史清单缺少完整性字段属于未知,不能默认成完整。
- ZIP 导出按一次冻结清单恢复相对路径并逐文件核验;直接下载内容寻址的 OSS 目录不能得到可用工程。源码/素材归档不包含依赖缓存、凭据和 AGC 对话运行状态。
## 2026-09-19 资源 kind 词汇收敛后,前端判据与 fixture 必须一起按 canonical 成员重写
- **现象 ** :工具栏入口的 `assetKind` 换成共享 `GameCreationAppAssetKind` (图集从平台词 `art-spritesheet` 改成 `icon-spritesheet` )后,「图集不接受用户参考」的判据仍写在旧的 `['art-spritesheet']` 字符串清单里,判据恒假:生成面板重新给图集渲染参考图选择器,原生提交再按合同显式拒绝多余参考。
- **同类第二处 ** :把 `hasRegisteredArtImageAssets` 从 `kind === 'art-spritesheet'` 直接换成「kind ∈ 视觉族」会把候选 UI 原型图(`ui-design` )算成美术图片产出,提前顶掉「美术资源计划已完成,尚未生成或登记图片」;判据必须显式排除 `GAME_CREATION_APP_UI_DESIGN_ASSET_KIND` 。
- **处理(现行口径) ** :前端的资源 kind 判据只比较 canonical 成员或共享契约导出的谓词,不再维护第二份字符串清单;词汇收敛时先 grep 旧词在 `src/` 与 `tests/` 两侧的落点,fixture 同步按 canonical 成员重写。
- **易错点 ** :fixture 与生产代码写着同一个旧词时,单测会陪着一起变绿,用例证明不了任何事;另外 `tests/appSurface.test.ts` 不是全部同族用例,资源画布 / 画布生成参考等用例散在 `tests/*.test.ts(x)` ,词汇收敛必须连这些一起跑。
## 2026-09-17 ts-rs 生成物换目录后,忘记同步忽略规则会让「生成物抖动」假装成代码改动
- **现象 ** : `GameCreationAppAssetKind` 的 ts-rs `export_to` 从 `apps/ai-game-creator-shell/src/contracts/generated/` 换到 `packages/shared/src/contracts/generated/` 后,任何 `cargo build` / `cargo test` 都会重写生成文件;若新目录没进 `.prettierignore` 与 `.eslintrc.cjs` 的 `ignorePatterns` , lint-staged / prettier 会把生成物重新格式化,于是每次提交都出现「生成物被改」,`cargo test export_bindings` 也不再幂等(跑完 `git diff` 不为空)。
- **处理(现行口径) ** :生成目录一律成对登记 `.prettierignore` + eslint `ignorePatterns` ;改 `export_to` 时同步改这两处,并用 `cargo test --locked -p shared-contracts export_bindings --manifest-path server-rs/Cargo.toml` 后 `git diff` 为空来验证幂等。
- **易错点 ** :旧的 `apps/ai-game-creator-shell/src/contracts/generated/` 目录下的同名文件不会自动删除,换目录后必须显式删除旧文件,否则会出现「两个同名 union,改动只落在一个目录」的假绿。
## universal 主程序必须配套双架构原生依赖
AGC macOS 主程序可合并为 universal,但 Codex 原生包的 `codex-package.json` 、code-mode host 和 zsh 仍有架构身份。两套包应各自保留上游布局与摘要,放入 `coding-agent/mac-native/darwin-arm64/` 、`darwin-x64/` ,由正在运行的主程序切片选择;不能只把主程序用 lipo 合并后复用最后一次构建的单架构资源。Tauri universal 两次 Cargo 构建共用 staging,每次都必须 stage 完整的两套资源。发布清单两个平台键同 URL/签名,只在 universal 产物上成立;Rosetta 隔离 smoke 不代替 Intel 真机验收。
## 生成草稿与异步展示边界必须按身份隔离
非模态生成浮层切换占位时按 draftId 分实例,卸载保留未提交/失败草稿,成功提交不再复活草稿;旧项目占位不存在时丢弃其保存回调。失败重试保留原请求输入和引用身份,引用失效不能静默过滤;修改已绑定输入须明确另起请求,不伪装成原请求重试。
聊天真实发送时间按相同用户 itemId 与正式条目合并,不能被启动应答后的观测时间覆盖。Thread Manager 生命周期事件透传已有 userItemId,使仅历史回读、live 为空的回合仍可精确补终态时间;不得按历史尾项或时间近似猜归属。布局整理仅可合并队尾同范围意图,不能越过中间手动写入;拖动预览和保存均冻结按下时的相应坐标基准。
## Phaser 与 CSS 双重居中导致游戏画面偏移
Phaser `Scale.FIT` 与 `autoCenter: CENTER_BOTH` 会给 canvas 计算定位外边距。若 canvas 的直接父容器同时使用 `display: grid; place-items: center` 或另一套 CSS 居中,浏览器再次定位带 margin 的元素,竖屏游戏会相对预览区域偏右。应只保留一个居中责任方:Phaser 居中时直接父容器使用尺寸明确的普通块布局;CSS 居中时设置 `autoCenter: NO_CENTER` 。不禁止外围页面的 Grid/Flex 布局,不通过修改 AGC iframe 的固定偏移掩盖项目 CSS 问题。
开发 Agent 的实际系统工程提示和 `agc-web-game-development` Skill 均包含此规则。布局修改后重新构建 dist,分别在桌面、移动与 resize 后测量 canvas 相对游戏父容器的中心误差(预期居中时不超过 1 CSS px),同时检查无溢出和意外滚动条;构建成功不等于视觉验收通过。
## 发布目标与原生能力必须贯穿完整入口
显式 `--target` 不能只改变 Tauri 命令参数;AGC 发布入口必须把同一解析结果传给版本高水位、更新端点、产物目录/后缀和清单平台键,否则 macOS 构建可能错误使用 Windows 渠道。插件文件存在也不代表 native 能力可用:Cocos 在宿主注册表缺少适配器时应隐藏并拒绝启动,前端自动启动消费后端列表投影,不能仅凭项目类型推断能力。发布策略以客户端更新权威文档为准,单架构资源不能登记成双架构产物。
## macOS 安装包小不代表运行依赖齐全
AGC 的 DMG 生成成功只证明应用可以被打包。平台专属 Codex staging、Tauri resource 映射、运行时资源目录定位和辅助组件 SHA-256 清单必须同时闭合;只配置 Windows 资源会让 Mac 开发机因全局 Codex 而掩盖缺包。macOS 使用锁定原生依赖中的 Codex、code-mode host、rg 和 zsh,不能复制 Windows EXE/DLL。用 `scripts/check-macos-bundle.mjs` (AGC 应用目录下)对复制到临时目录的 `.app` 做限制 PATH、隔离 HOME 的正式查找、app-server 握手和缺组件拒绝检查;GUI、账号、Provider 与 Cocos 原生桥接另行验收。插件 JS 入口仍依赖系统 Node,不得将“插件文件随包”表述为“无需任何外部工具链”。
## AGC 空快照测试必须等待请求完成
`waitFor(() => expect(activeTurns).toEqual([]))` 在 Hook 初始状态就能成功,不能证明首次异步读取已经完成。引用稳定性回归应显式控制 Promise 完成,并同时检查首次空响应与禁用后的引用;快照签名初值必须与初始空数组一致。窗口同步测试应验证未变化状态不重复发布,不能依赖一次多余的空态更新。
## AGC Windows 开发态首次页面加载缓慢
Vite 默认监听应用根下的 Rust `src-tauri/target` ,构建产物较多时会创建大量 Windows 文件监听器。AGC 配置通过 `server.watch.ignored: ['**/src-tauri/target/**']` 排除此目录,不关闭业务源码、CSS、共享组件监听或 HMR。排查时区分后端就绪、Vite 扫描和原生窗口首绘;监听目录回归不能代替实机首绘测量,验证入口见本地开发运维文档。
@@ -20,6 +175,7 @@ Vite 默认监听应用根下的 Rust `src-tauri/target`,构建产物较多时
- **易错点 ** :① 把弹层改成 `left: 0` 或往右挪也能让它可见,但那是改变展开方向,弹层会跑到触发钮右边(用户明确否决);② 只放开最外层聊天列不够——surface 与 conversation 各自都会裁,三层必须同时放开;③ 只按宽度比大小会误判:280px 面板里控制排本身也超出(发送钮右侧溢出 22px,被窗口右缘吃掉),那不是本条的原因,别顺手去改控制排布局。
- **验证 ** : `apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts` 的 `keeps the landscape workbench edge-to-edge with internal chat scrolling` 钉住三条 override 声明在场(删掉任一条即红)。真机几何用 playwright-cli 打开一份只含真实 `styles.css` 与真实 composer DOM 的最小复现页实测(视口 1000×700、面板 280px):弹层 rect 修复前后都是 `[-42, 108]` (位置未动),`elementFromPoint` 的命中区间从修复前的 `[2, 108]` 变成整块;档位文字在截图中完整可见。
- **关联 ** : `apps/ai-game-creator-shell/src/styles.css` ( `面板纵向布局(2026-07 Codex 风格改造)` 区块之后)、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts` 。
## 2026-09-17 资源卡「内容跟着边框动」与「模型缩略图被拉伸」是两条不同的几何陷阱
- **内容跟着状态边框位移 ** :卡片底态是 `border: 0` ,悬停 / 选中才加 1px 边框;卡片是 `box-sizing: border-box` ,而卡面(`.game-resource-card-visual` )与角标都是 `position: absolute; inset: 0` (包含块 = **padding box ** )⇒ 状态一切换,内容盒四边各被吃掉 1px,卡面与角标整体位移并缩小 2px。修法:底态写成 `border: 1px solid transparent;` ,状态只点亮 `border-color` ;契约用例改成断言「资源卡规则里不得出现非 1px 的 `border` / `border-width` 」。
@@ -31,10 +187,6 @@ Vite 默认监听应用根下的 Rust `src-tauri/target`,构建产物较多时
- **原因 ** :这个 shell 以管理员身份运行,`New-Item` / `tempfile` 新建目录的 owner 是 `BUILTIN\Administrators` ,而 AGC 的校验要求 owner 等于当前用户 SID(`KDLETTERS\<user>` )。`Get-Acl <path> | Select Owner` 与 `whoami` 一比就能定性;同一台机器上由客户端自己创建的目录 owner 正确,所以「客户端自己建的项目能用、手建的不能用」。
- **处理 ** :手工夹具先 `icacls <path> /setowner "<DOMAIN>\<user>" /T` ; Rust 用例改用工程自带的 `crate::tests::canonical_test_tempdir(prefix)` (它会 canonicalize 并重置目录 owner),不要直接用 `tempfile::tempdir()` 。判「用例失败与本改动无关」时,先确认失败信息是不是这一条。
## DirectProject 历史不能按工具条目切页再按消息推进游标
原始 `response_item` 历史同时含用户/助手消息、推理与工具输出。若原生每次取 20 个原始条目、前端过滤聊天消息后再找最旧 ID,纯工具页会让消息集合为空且游标不动,看起来历史丢失。聊天读取固定显式请求 `messagesOnly: true` ,原生逐行过滤后按消息分页并返回 `oldestItemId` ;默认原始模式留给原始条目消费者。无 ID 旧消息保留并扩展到可寻址边界,不能造 ID。前端保留项目与读取代次、单飞及 ID 去重,旧请求的成功、失败与 finally 都不能覆盖新读取;真实日志只在临时目录只读重放,不能提交正文夹具。
## JSON 卡片显示与 UI 编辑能力必须同源
JSON 的文本读取分支不等于卡面应该展示原始 State 摘要。卡片、缩略图及编辑器入口共同消费受控文本预览的 `uiDesignAssetId` ;只有原生复用 UI 持久化合同校验 schema、完整 State 和项目/资产身份后才设置它。普通 JSON 保留 JSON 代码预览,不按 `kind: UI/ui` 或 schema 字符串片段猜测编辑能力。已有合法 UI State 的加载/保存不依赖 kind 精确大小写,但新建初始化仍保留正式 UI 资产门禁;缓存与项目切换须保留现有身份隔离。
@@ -70,6 +222,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **处理(现行口径) ** :① 渲染 `ProjectDevelopmentView` 的桩统一登记 `list_local_project_asset_generations` (返回**数组**,空账本 `[]` ; Rust 侧返回 `Vec<AssetGenerationTaskRecord>` ,不是 `{ tasks: [] }` );② `unexpectedCommands` 这类门禁**不要放宽**,只登记合法命令;③ 生产代码对 IPC 返回值做形状防御(`Array.isArray` 归一化),IPC 拒绝走既有提示路径,不产生 unhandled rejection( `resourceCanvasAssetGenerationTaskModel.ts` / `resourceCanvasAssetGenerationQueue.ts` / `index.tsx` 的恢复 effect)。
- **易错点 ** :① 桩返回**非数组**时用例可能"看着绿"但同时报 unhandled rejection(实测:把 `[]` 误写成 `{ tasks: [] }` 就是 8 passed + 7 unhandled error),所以判"绿"必须同时看 unhandled 计数;② 新增挂载期 IPC 后要一次性 grep 所有 `ProjectDevelopmentView` 的桩,而不是等 CI 逐个炸;③ 提示条类断言(如「无读时提示」)会把「桩抛错」翻译成「多了一条提示」,排查时先看 unhandled,再看断言。
- **关联 ** : `apps/ai-game-creator-shell/tests/resourceCanvasManualLayout.test.tsx` 、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts` 、`apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts` 。
## 2026-09-14 UI 编辑器返回后资源画布滚轮平移失效
- **现象 ** :资源管理打开 UI 编辑器再返回后,资源画布滚轮平移/缩放不再响应;返回前同一手势正常。
@@ -77,6 +230,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **处理 ** :将 `uiEditorRoute` 纳入 wheel effect 依赖,使进入/退出 UI 编辑器时先清理旧节点监听,再给返回后的新 manager 绑定同一处理器。
- **验证 ** : `npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "restores resource canvas panning"` ;回归用例覆盖打开栏目、wheel 平移、进入 UI 编辑器、返回并再次 wheel 平移。
- **关联 ** : `apps/ai-game-creator-shell/src/view/project-development/index.tsx` 、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts` 。
## 2026-09-14 AGC 就绪等待被 WMI 拖成分钟级:端口归属探测从 Get-NetTCPConnection 换成 netstat
- **现象 ** : `npm run agc` 从 `[ai-game-creator-shell] starting backend stack` 到 `backend ready` 要等约 80 秒,中途反复出现 `等待配套后端就绪时归属校验未通过(api-server-owner-mismatch: 未知进程)` ;而这段时间后端其实已经好了(实测 api-server 12:39:01 已在 8084 监听、`/healthz` 已 200, 12:40:19 才判 ready)。
@@ -85,6 +239,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **验证 ** : `apps/ai-game-creator-shell/tests/start-dev-stack.test.ts` 新增两条——「探测脚本使用 netstat 且不再出现 Get-NetTCPConnection」「命令行按 PID 缓存后随请求下发、TTL 过期即失效」;定向 vitest 55 passed。本机实测:不含 SpacetimeDB 端口的探测 368 ms(原约 22 秒)、含 SpacetimeDB 端口 3.8 秒、命中缓存 368 ms;`npm run agc:serve` 的 `starting backend stack` → `backend ready` 由约 80 秒降到 16.7 秒(其中归属校验只占 4.4 秒,其余是 SpacetimeDB + api-server 的真实启动时间)。
- **残留 ** :这台机器上首次 WMI 调用本身仍是秒级(曾见 18 秒),所以「新 SpacetimeDB PID 的第一次探测」仍可能多花几秒;命令行在进程存活期内不变,TTL 只用来限制 PID 复用造成的误判窗口。
- **关联 ** : `apps/ai-game-creator-shell/scripts/start-dev-stack.mjs` ( `readWindowsPortOwnerIdentities` )、`apps/ai-game-creator-shell/tests/start-dev-stack.test.ts` 、`apps/ai-game-creator-shell/scripts/dev-windows-process.mjs` (退出清理仍走整份 `Win32_Process` 快照,自带 1 秒缓存,不在本次范围)。
## 2026-09-15 AGC JSON API 的响应体也必须有等待上限
- `fetchClientHttp` 的超时只覆盖请求到响应头返回;随后直接等待 `response.text()` 仍可能无限挂起。模型目录共用一个在途 Promise,响应体卡住会使后续刷新复用同一挂起请求、选择器持续忙碌。
@@ -95,7 +250,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **现象 ** : `AI game creator shell Rust tests` 一直是客户端 CI 的关键路径。run 2097 实测 15 分 27 秒,其中 `apps/ai-game-creator-shell/src-tauri` 的 bin target 单测(2466 条)一条 `cargo test -- --test-threads=1` 串行占 507 秒。
- **为什么原本是整个 suite 串行 ** : 2026-07-21 `a273377b1` 的判据是「共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰」,即**同进程内**的全局后台锁、异步终态与进程级 static 被交叉触发;另有少数用例自身 spawn 当前测试二进制(`std::env::current_exe()` )跑 fixture,会碰容器里共享的 target 与固定临时路径。
- **处理(现行口径) ** :新增 `apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs` : `cargo test --no-run` 编译一次 后用 `--list` 名单把用例按 `index % shards` 切成 4 片,CI 的每个 分片 job 用 `--shard-index=<i>` 只跑自己那片(`--exact <名单> --test-threads=1` ,片内串行),片与片 之间靠 **job 级并发**摊开。配套把 `ai-game-creator-shell:check:rust` 拆成 `:rust:crates` 与 `:rust:shell` , AGC 相关门禁在 CI 里共 6 个 job( 4 个分片 + smoke + crates) 。
- **处理(现行口径) ** :新增 `apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs` : `cargo test --no-run` 编译后用 `--list` 名单把用例按 `index % shards` 切成 4 片,CI 的每次 分片调用 用 `--shard-index=<i>` 只跑自己那片(`--exact <名单> --test-threads=1` ,片内串行);两条 Rust lane 各顺序运行两片,lane 之间靠 **job 级并发**摊开,避免每片重复依赖预热 。配套把 `ai-game-creator-shell:check:rust` 拆成 `:rust:crates` 与 `:rust:shell` , AGC 相关门禁在 CI 里由两条 lane、 smoke 和 crates job 承载 。
- **反面实验(run 2102,勿重做) ** :起先把 4 片放进**同一个 job** 内的 4 个进程并行,结果门禁步骤跑满 18 分钟仍未结束,比整套串行的 507 秒还慢——同一容器内这几片共享 `HOME` 、target 目录与固定临时路径,会互相拖慢。因此 `--shard-index` 是 CI 的唯一入口;不带 `--shard-index` 的「单命令内多片并行」只留给本地全量自测。
- **易错点 ** :① 分片规则必须自校验「片并集等于 `--list` 全集且互斥」,否则改分片方式会静默漏跑门禁;② 每片要拿独立 `TMPDIR` , `tempfile::tempdir()` 默认落在它下面(测试里的硬编码 `/tmp/...` 多是「必须拒绝」的负向断言,不是真实读写);③ 不要给分片 job 装 `npm ci` ——AGC 壳 Rust 门禁与 `agent-run` smoke 只用 cargo 与 node 内建模块,那些 `npm ci` 正是达标 7 分钟的主要障碍;④ 片 job 只需预热 AGC 壳自己的 manifest(其 `Cargo.lock` 的 path 依赖已覆盖 `platform-llm` / `platform-agent` / `agent-runtime-core` / `shared-contracts` ),`server-rs` 那份预热属于 crate 级 job;⑤ 分片后 `--test-threads=1` 不再出现在 workflow 里,但它是分片运行器的片内参数,别再往 workflow 里补整套串行命令。
- **不要做的事 ** :不要退回「整套 `--test-threads=1` 」(507 秒长尾回来了),不要放开成整套并行(同进程内后台锁与异步终态会再互相干扰),也不要在单个 job 内多进程并行多个片(实测比串行还慢)。
@@ -109,6 +264,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **处理 ** : `runNativeShellGate` 收窄为 `(group, gate)` ,label 回到各门禁执行体里以字面量打印;分组能力与 `--groups=` 语义不变。脚本内已注明"不要把 label 抽成变量",外部壳的 `check-config` 是它的消费者。
- **验证 ** : `node apps/mobile-shell/scripts/check-config.mjs` exit 0; `node apps/desktop-shell/scripts/check-config.mjs` 已越过第 740–777 行的根脚本断言段(本机随后卡在本机不存在的 Tauri 生成产物目录,与本次改动无关);`--groups=contract` 、vitest `scripts/project-ci-workflow.test.ts` 、eslint 均通过。修复后 run 2097 六个 job 全绿。
- **关联 ** : `scripts/check-native-shells.mjs` ( `runNativeShellGate` )、`apps/desktop-shell/scripts/check-config.mjs` 、`apps/mobile-shell/scripts/check-config.mjs` 。
## 2026-09-14 AGC 资源画布不要恢复「无条件重派生」,否则新增一张素材就整张重排
- **现象 ** :用户生成一张新素材后,画布上既有卡片全部移位,刚摆好的位置失效。
@@ -122,7 +278,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **现象 ** :把 `Native shell tests` 拆成客户端三个 job 后,如果只跑 `npm run check:native-shells:release` ,静态契约和壳运行时门禁都不会执行;如果只跑 `--groups=contract` , `desktop-release-binary-artifact` 又会因为缺少 `build/native/desktop/` 产物而失败。
- **原因 ** :分组是执行范围,不是"额外检查"。`desktop-release-binary-artifact` 断言依赖同 job 内的 `desktop-shell-stage-release-binary` 步骤,所以它归 `release` 组,不能放进 `contract` ;反过来,任何"只跑一组"的命令都不能被当成完整门禁。
- **处理 ** :分组与 job 的对应关系固定为 `contract` +`shells` +`release` → `Native shell tests` , `agc-web` → `AI game creator shell web tests` , `agc-rust-shard-1..4 ` → `AI game creator shell Rust shard 1/4 .. 4/4 ` , `agc-rust-smoke` → `AI game creator shell Rust smoke` , `agc-rust-crates` → `AI game creator shell Rust crates` ; `scripts/project-ci-workflow.test.ts` 校验"每个分组恰好被一个 job 调用一次"和"CI 不再调用全量 `npm run check:native-shells` ",新增分组必须同步门禁脚本、根脚本与 workflow 三处。
- **处理 ** :分组与 job 的对应关系固定为 `contract` +`shells` +`release` → `Native shell tests` , `agc-web` → `AI game creator shell web tests` , `agc-rust-shard-1..2 ` → `AI game creator shell Rust lane 1/2` , `agc-rust-shard-3..4` → `AI game creator shell Rust lane 2/2 `, `agc-rust-smoke` → `AI game creator shell Rust smoke` , `agc-rust-crates` → `AI game creator shell Rust crates` ; `scripts/project-ci-workflow.test.ts` 校验"每个分组恰好被一个 lane/ job 调用一次"和"CI 不再调用全量 `npm run check:native-shells` ",新增分组必须同步门禁脚本、根脚本与 workflow 三处。
- **易错点 ** :① 拆 job / 改 job 名后要确认分支保护里没有残留已不再上报的旧 job 名(本仓库现在不配 required context,只需人工确认 CI 结果,见置顶条目的「分支保护口径」);② 每个 job 只预热自己会构建的 Cargo 依赖,`agent-run:smoke` 因为会 spawn `cargo` 必须与 AGC 壳的依赖预热同 job;③ 本地全量 `npm run check:native-shells` 仍会串行跑完所有分组,用它作为本地完整门禁,不要用单组脚本冒充。
- **关联 ** : `.gitea/workflows/project-ci.yml` 、`scripts/check-native-shells.mjs` 、`scripts/project-ci-workflow.test.ts` 、`.gitea` 分支保护设置。
@@ -248,6 +404,15 @@ AGC 的 Cocos 能力来自随客户端分发的 `agc-cocos-editor` 内置插件
首页命名回合成功后创建命令失败且不会留下项目目录。用户通过目录选择器创建的
项目仍走 user-selected 权限范围。
- 2026-09-17 补充:首页与模板建项支持用户自选 `projectsRoot` (见
`docs/project-memory/plans/【实施计划】AGC项目创建目录可选-2026-09-17.md` )。
自选目录只有一条合法来源——本机原生目录选择器返回的目录,并且必须在 Rust 侧
通过 `validate_requested_game_project_creation_root`
(绝对路径 / 无控制字符 / 已存在普通目录 / 非链接与 reparse point /
`prepare_game_creator_project_root_for_read` )。默认值仍必须是
`app_data_dir()/projects` :不要因为"用户能自选"就把默认值改成 Documents 或
其它用户目录,也不要在目录不存在时替用户创建。
## 2026-09-12 Cocos 项目识别不等于编辑器桥就绪
- 现象:能发现正确 Creator PID、Agent 也有 `agc_cocos_execute` ,但首次执行报 pipe 不存在;仅登记目标的 `connect` 会误报成功。
@@ -316,7 +481,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
## 2026-08-15 `#[cfg(windows)]` 里的代码不参与 Linux CI 编译,CI 绿不代表能构建
- 现象:把 master( `9f5c84ee7` )合进 `feat/five_min_design` 后,`cargo check --all-targets` 在 Windows 上直接 `error[E0658]: use of unstable library feature 'windows_by_handle'` ,位置是 `apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs` 的 `metadata.number_of_links()` 。该文件与 `origin/master` **逐字节相同 ** ,即 master 自身在 Windows 上就构建不过。
- 原因:`std::os::windows::fs::MetadataExt::number_of_links` 至今未稳定(rust-lang#63010 ) ,而 `rust-toolchain.toml` 锁的是 stable `1.96.0` 。引入它的提交是 `578f8019f` (优化 AGC 项目入口并识别 Godot 工作区),其中 unix 分支用 `MetadataExt::nlink()` (已稳定)、windows 分支用了未稳定的对应物。**Linux CI 上 `#[cfg(windows)]` 整块不参与编译,所以 CI 全绿。**
- 原因:`std::os::windows::fs::MetadataExt::number_of_links` 至今未稳定(rust-lang#63010 ) ;当时 `rust-toolchain.toml` 锁的 stable 是 `1.96.0` , 2026-09-20 升到 `1.98.1` 后在同一台 Windows 机器上用该 stable 实测仍报 `error[E0658]: use of unstable library feature 'windows_by_handle'` (见 decision-log 同日条),因此本条的处置口径不变 。引入它的提交是 `578f8019f` (优化 AGC 项目入口并识别 Godot 工作区),其中 unix 分支用 `MetadataExt::nlink()` (已稳定)、windows 分支用了未稳定的对应物。**Linux CI 上 `#[cfg(windows)]` 整块不参与编译,所以 CI 全绿。**
- 更普遍的形状:只要一段代码只在某个 `#[cfg(target_os)]` 下编译,它就完全绕过了其它平台的 CI——不只是 unstable feature,还包括类型错误、借用错误、缺失 import。跨平台分支是「双写」,两侧都得有人真的编译过。
- 处理:本仓库对「文件是不是无硬链接普通文件」统一自行声明 `ByHandleFileInformation` 并调用 `GetFileInformationByHandle` ,见 `runner/endpoint.rs` 、`tool_plan_handoff/storage_windows.rs` 、`project/agent_db.rs` 、`git_inspect.rs` 、`image_inspect.rs` 、`agent/generation/canvas_generation.rs` 。`manifest.rs` 当前已采用同一实现,并保留 fail-closed 语义:无法取得句柄信息或确认存在硬链接时均拒绝,同时拒绝 directory / reparse point。
- 验证:改后 `cargo check --offline --all-targets` 通过、`cargo fmt --check` 通过、`project::manifest` 与 godot 相关定向测试 65 passed / 0 failed。判断「是不是本次合并引入」的通用手法:`git diff origin/master -- <file>` 为空即说明该文件就是 master 原样,问题不在合并。
@@ -4002,6 +4167,14 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:活会话下 finalization 和 `runner.shutdown_if_idle` 必须失败关闭;分别验证 graceful handler 尾部输出、宽限超时后的 force、忽略 SIGHUP 的 npm 孙进程和 Windows Job 路径,只有 child 已终态、同组残留已处理且 PTY 尾部排空才出现唯一 terminal record。另用允许程序证明代理和固定 cwd 不是文件系统 / 网络沙箱,不得把该现象误写成测试失败或安全能力。
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` 、`apps/ai-game-creator-shell/src-tauri/src/runner.rs` 、`apps/ai-game-creator-shell/src-tauri/src/agent.rs` 。
## 一次性命令不能把孤儿僵尸误判为仍在执行的进程组
- 现象:Linux CI 的命令已退出,但 `command.exec` 返回 `needs-reconciliation` ,后续修复计划或输出读取请求一直等不到;普通 WSL 下相同命令可通过。
- 原因:容器 PID 1 未回收孤儿 `bwrap` 僵尸,`kill(-pgid, 0)` 仍返回成功;主进程已被 wait 回收,后续 leader 启动身份核对必然失败,掩盖了真实退出结果与原本应触发的日志审计错误。
- 处理:确认 target 终态且回收主进程后扫描 `/proc/<pid>/stat` ,空组或仅含 `Z / X` 成员无需发送信号;有存活成员仍保留 leader 身份门禁,读取失败保守进入 reconciliation,不放宽未知进程组的信号权限。
- 验证:隔离 subreaper 夹具覆盖 leader 已回收时的存活后代拒绝、孤儿僵尸接受和空组接受;原有诊断命令、审计失败、命令修复与长输出/历史读取测试在不回收孤儿的 PID namespace 下验证。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/command_exec.rs` 、`apps/ai-game-creator-shell/src-tauri/src/tests/command_runtime.rs` 。
## 命令环境变量、代理和进程组不能冒充 OS 沙箱
- 现象:命令看似使用隔离 HOME / TMP、离线包管理器和不可达代理,仍能直接读取宿主用户文件、用原始 socket 联网,或由 `project.verify` 的平行 npm spawn 绕开 `command.exec` 限制。
@@ -4586,7 +4759,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 现象:`Repository checks` 、`Frontend tests` 、`Backend tests` 和 `Native shell tests` 都从全新 job 容器开始,apt、setup-node、rustup 和原生系统库在不同 job 里重复安装;后端与原生壳的安装时间可达数分钟,并把软件源和代理瞬时失败放大为四份。
- 原因:Gitea Actions job 彼此隔离,上一个 job 在容器内安装的包不会自动进入下一个 job;把同一套不随 PR 变化的工具链写在 workflow step 中,必然每次重做。
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、固定 npm、Rust 1.96 、`rustfmt` 、Chrome、`bwrap` 、`rg` 、`ffmpeg` 、`clang/lld` 和 Tauri / 后端系统依赖,并按锁预热唯一根 npm workspace、server-rs、桌面壳与 AI 游戏创作壳 Cargo 四份下载缓存。四个 job 统一 `runs-on: genarrative-ci` ,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh` ,同时检查四份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0` ; `rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、固定 npm、Rust 1.98.1 、`rustfmt` 、Chrome、`bwrap` 、`rg` 、`ffmpeg` 、`clang/lld` 和 Tauri / 后端系统依赖,并按锁预热唯一根 npm workspace、server-rs、桌面壳与 AI 游戏创作壳 Cargo 四份下载缓存。四个 job 统一 `runs-on: genarrative-ci` ,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh` ,同时检查四份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0` ; `rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
- 依赖边界:每个 job 仍必须各自执行 `npm ci` ,让当前 lockfile 和 PR 依赖在干净环境中验证;区别是命中镜像 cache 时只做本地解包,锁新增依赖时才走受控网络。不要把 `node_modules` 或 Cargo `target` 烘进镜像,也不要向不受信任 PR 挂载跨 job 可写 cache。
- 锁漂移边界:runtime 校验输出任一 `*_cache_lock=partial` 说明镜像内 lock 与当前 checkout 不同,不代表新增依赖已经缓存;必须同时输出 Actions warning,提示可信分支落地后刷新镜像。必须在新镜像中对 server-rs、桌面壳和 AI 游戏创作壳当前 lock 执行真实 `cargo fetch --locked --offline` ; `cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
- 构建网络边界:`CARGO_NET_RETRY` 只覆盖部分 crate 下载,registry `config.json` / index TLS 握手仍可能直接终止整次 fetch。Dockerfile 对每个 `cargo fetch --locked` 再做最多 5 次整命令级有界重试,最终仍执行断网 fetch,不能降低为无锁重试或省略离线闭合验证。
@@ -5284,6 +5457,14 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 处理:Windows 专用 Tauri 配置设置 `bundle.useLocalToolsDir: true` ,把工具缓存到 `src-tauri/target/.tauri/NSIS` ;Jenkins 预检验证实际用户、项目工具目录可写,并在构建失败时打印实际缓存路径和绝对路径执行结果。
- 验证:不要把 PATH 中 `makensis` 可发现当作 Tauri bundler 工具可执行的充分证据;需要在 Windows Agent 上检查 `target/.tauri/NSIS/makensis.exe` 、ACL、EDR/Defender 和直接 `-VERSION` 结果。
## Tauri NSIS 工具链必须在打包前预置并重试(2026-09-21)
- 现象:AGC Windows 发布构建已完成 Rust release, Tauri 依次打印 `Downloading .../nsis-3.11.zip` 、`Info extracting NSIS` 、`Downloading .../nsis_tauri_utils.dll` 之后,直接以 ``failed to bundle project ` io: unexpected end of file` ` ` 失败(退出码 1),安装包不会产出。
- 原因:tauri-bundler 的 `download_and_verify` 现场从 GitHub 取 NSIS 工具链,只有一次机会、没有重试;响应体被截断即报 `io: unexpected end of file` ,看起来像打包错误其实是网络问题。Checkout 阶段的 `git clean -fdx` 每次都会清掉 `target/.tauri` ,所以每个构建都要重新下载,在受限网络下必然反复失败。
- 处理:新增 `apps/ai-game-creator-shell/scripts/nsis-toolset.mjs` 与 `ensure-nsis-toolset.mjs` ,在 `buildRelease` (Windows 目标且需要打包时)与 Jenkins `Tauri NSIS toolchain` 阶段按固定 SHA1 预置 `target/.tauri/NSIS` :原始归档带 4 次重试,缓存在工作区外的 `%ProgramData%\genarrative\tauri-nsis-cache` (可用 `AGC_TAURI_NSIS_CACHE_DIR` 覆盖),镜像开关沿用 bundler 的 `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE` / `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR` 。该阶段同时执行 `makensis.exe -VERSION` ,把 2026-09-02 记录的缓存目录不可执行问题也提前到编译之前暴露。Checkout 阶段改为 `git clean -fdx -e apps/ai-game-creator-shell/src-tauri/target/.tauri` :Tauri 的工具缓存位于工作区内,裸 `git clean -fdx` 会连它一起删,排除后同一节点的稳态构建不再需要联网,只有冷缓存(新节点、工作区重建)才下载。
- 验证:`node --test apps/ai-game-creator-shell/scripts/nsis-toolset.test.mjs` (已就绪零下载复用、缓存离线还原、失败重试、哈希不符与归档越界失败关闭)与 `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` ;真实节点上该阶段必须早于 Rust 编译失败关闭。
- 注意:不要改回 `bundle.useLocalToolsDir: false` 去用 `%LOCALAPPDATA%` ,也不要依赖 PATH 里预装的 `makensis` ;升级 `@tauri-apps/cli` 时同步核对归档 URL、SHA1 与必需文件清单。
## AGC 登录态续期必须同步本地运行时
- 模型目录 HTTP 请求与 DirectProject 的 Rust/app-server 使用同一账号,但凭据分别保存在 WebView 与 Rust / Runner;续期应复用 `requestPlatformSessionRefresh` 完成用户核验及本地会话安装,不能只写 localStorage。
@@ -5441,11 +5622,12 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
## 资源 kind 别名只救新登记,不救存量 category( 2026-09-10)
- 现象:真机 `What do u wanna do kitten` 的 66 项资源里 58 项落「待归类」,其中 57 项是 `kind:"ui"` 的 UI 资产,本该在「UI 交互」。
- 成因链:`ui` 不在 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS` 里、也没有别名,于是走 `canonicalGameCreationAppAssetKind` 的 `image` 兜底,再经 `image → unclassified` 被误分到 「待归类」。它并不是历史遗留值——`assets.rs:1656 ` 的 `infer_canvas_export_asset_kind` (画板导出导入)**现在 仍在写出** `ui` / `animation` / `asset` 。
- 成因链:`ui` 不在 canonical 词汇表里、也没有别名,于是落 「待归类」。它并不是历史遗留值——`assets.rs` 的 `infer_canvas_export_asset_kind` (画板导出导入)当时 仍在写出 `ui` / `animation` / `asset` 。
- **现行口径(2026-09-17 起,改这里之前必读) ** : kind 只有一份词汇表(Rust `GameCreationAppAssetKind` 声明表 → ts-rs 生成 TS union),解析是**严格等值匹配**:不 trim、不 lowercase、不查别名、不迁移;非 canonical 原值收口成 `unknown` (分类 `unclassified` )并把**原始串 + 调用上下文**交给壳层注册的 `app_log!` 回调。**别名表已整体删除,不要再加回来**——这条现象现在的表现是「日志里能查到谁还在写旧值」,先修写入方,不要在读侧做兼容。
- **关键陷阱(2026-09-11 已修) ** :补别名只影响「今后新登记」的资产。`register_local_asset_entry` ( `assets.rs` )命中同 `localPath` 的既有资产时,旧实现只覆盖 `kind` / `media_type` / `source` 、**不重算 `category` **;而读取侧优先信任落盘 `category` 、只在它缺失或非法时才按 `kind` 派生。所以这 57 条的落盘 `category: "unclassified"` 会一直有效,重导入也自愈不了。**修法与必须保留的不变量**:更新分支只在 `kind` 真的变化时才重派生 `category` ,否则「同路径重登记且 kind 变了」会留下「新 kind + 旧分类」的错位,而陈旧的非 `unclassified` 值会被无条件信任、自愈也不触发;反过来同 kind 重登记**禁止**动 `category` ,落盘分类是权威值,被 `register_local_asset_keeps_explicit_category_when_kind_is_unchanged` 钉住。
- 为什么读取侧要信任落盘值:存在用户手动改分类的正式链路 `update_manifest_asset_classification_at` ( `project/manifest.rs:1110` ),读时无条件重派生会吃掉用户的手动设置。
- 根治选项(需产品拍板):① 读时把 `unclassified` 当作「未设置」再按 kind 派生(简单但失去"我就是要 unclassified"的表达力);② 一次性回填这 57 条(保留人工设置语义,需迁移脚本);③ 只改写入侧让新素材 canonical 化(治不了存量)。
- 另一处必须成对维护:别名表有**两份实现**——TS 侧 `packages/shared/src/contracts/gameCreationApp.ts` 的 `GAME_CREATION_APP_LEGACY_ASSET_KINDS` (读投影用) 与 Rust 侧 `server-rs/crates/shared-contracts/src/game_creation_app.rs` 的 `canonical_game_creation_app_asset_kind` (写入侧按 kind 派生 category 用)。只改一边就会让落盘 category 与读侧栏目互相矛盾。交叉守卫见 `apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts` (直接 解析 Rust 源码比对) 。
- 成对维护点(**2026-09-17 起不再存在**):当时别名表有**两份实现**——TS 侧 `GAME_CREATION_APP_LEGACY_ASSET_KINDS` 与 Rust 侧 `canonical_game_creation_app_asset_kind` ,靠 `apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts` 正则 解析 Rust 源码交叉钉住。现在两份手写表与那个守卫测试都已删除:唯一词汇表由 Rust 枚举声明表派生、经 ts-rs 生成 union,跨语言一致性回到**编译期**(TS 侧 `Record<GameCreationAppAssetKind, …>` 穷举,少一个成员就编译不过)。不要再引入"正则解析 Rust 源码"的跨语言守卫 。
- 真机计数守卫见 `apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts` 。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/assets.rs` 、`apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs` 、`packages/shared/src/contracts/gameCreationApp.ts` 、`server-rs/crates/shared-contracts/src/game_creation_app.rs` 。
@@ -5454,20 +5636,20 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 规则(已拍板落地):`gameCreationAppAssetCategory` 在「落盘 `category === 'unclassified'` **且 ** 该资产 `kind` 能派生出明确的非 `unclassified` 分类」时采用派生值,其余情况信任落盘值。
- 为什么需要这条:`register_local_asset_entry` ( `assets.rs` )命中同 `localPath` 的既有资产时**只在 `kind` 变化时**重算 `category` ( 2026-09-11 起);所以历史上被写成 `unclassified` 的资产(典型是 `kind:"ui"` / `kind:"UI"` 因不在 canonical 目录而落到 `image → unclassified` )在补齐别名后**不会自愈**。选读时重派生而不是写迁移脚本:不需要迁移、且永久自愈(任何历史上被系统错判成 unclassified 的都会自动归位)。真机验证:`What do u wanna do kitten` 的待归类从 58 降到 1(只剩 code 类 `game-entry` ),UI 交互从 2 升到 59。
- **两个口径不能混用(2026-09-11 收口) ** : `gameCreationAppAssetCategory` / `game_creation_app_asset_effective_category` 是**读显示**口径;写回 manifest 必须用 `gameCreationAppAssetPersistedCategory` (只做缺失 / 非法兜底),它等于 Rust 反序列化后的落盘原值。把自愈值回写会把「只改标签」变成静默改分类——真机上同一条 `kind:"ui"` 资产因此同时存在 `unclassified` 与 `ui-interaction` 两种落盘值。
- **跨端必须同构 ** : Agent 侧资源投影(`direct_tool_bridge.rs` )走 Rust 的 `game_creation_app_asset_effective_category` ,不许直接透传落盘 `category` ;两侧不一致时同一条资产会出现「UI 显示 UI 交互、Agent 读到待归类」(真机 55 条)。守卫是 `assetKindCanonicalMapping.test.ts` 解析 Rust 源码里的 `EFFECTIVE_CATEGORY_CONTRACT` 决策矩阵 。
- **跨端必须同构 ** : Agent 侧资源投影(`direct_tool_bridge.rs` )走 Rust 的 `game_creation_app_asset_effective_category` ,不许直接透传落盘 `category` ;两侧不一致时同一条资产会出现「UI 显示 UI 交互、Agent 读到待归类」(真机 55 条)。**现行守卫(2026-09-17 起)**: `shared-contracts` 的 `EFFECTIVE_CATEGORY_CONTRACT` 决策矩阵用例 + `packages/shared` 的 `gameCreationApp.test.ts` 同口径用例;解析 Rust 源码的 `assetKindCanonicalMapping.test.ts` 已删除 。
- 为什么可以覆盖落盘值:落盘 `category` 的权威性来自「用户可在分类与标签面板手动设置」(`update_manifest_asset_classification_at` , `project/manifest.rs` )。收窄条件把覆盖窗口压到最小——只有当落盘值是 `unclassified` (即"没有明确分类")时才覆盖。
- **唯一盲区 ** :用户**手动**把一个 kind 已能明确分类的资产设成「待归类」时,该手动值会被覆盖。这是有意接受的取舍:「手动设为待归类」意图边缘,且 kind 已经表达了分类;而漏掉这条规则,所有历史误判都无法自愈。若将来产品需要"显式待归类",应改成在 manifest 里区分"未设置"与"显式 unclassified"(例如 `category` 缺省 vs 显式写入),而不是取消本条规则。
- 不受影响:`image` / `video` / `code` / `publication-material` 的 canonical 分类本身就是 `unclassified` ,派生结果等于落盘值,规则不触发(已用断言钉住)。
- 写入侧已同步 canonical 化:`infer_canvas_export_asset_kind` ( `assets.rs` )现在直接产出 canonical kind( `ui → ui-design` 、`animation → character-animation` 、`asset → image` ),不再依赖别名表兜底;`canvas_export_asset_kind_is_always_canonical` 用例逐分支钉死,别名表从此只承担存量兼容 。
- 写入侧已同步 canonical 化:`infer_canvas_export_asset_kind` ( `assets.rs` )现在直接产出 canonical kind( `ui → ui-design` 、`animation → character-animation` 、`asset → image` ),不再依赖别名表兜底;`canvas_export_asset_kind_is_always_canonical` 用例逐分支钉死。**2026-09-17 起别名表整体删除**,存量兼容也不做:读侧严格解析,认不出的原值只留痕 。
- 关联:`packages/shared/src/contracts/gameCreationApp.ts` 的 `gameCreationAppAssetCategory` 、`apps/ai-game-creator-shell/src-tauri/src/assets.rs` 、`apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts` 。
## 大写 `UI` / `font` 不在 alias 表 → 8 条真机 UI 资产永远落「待归类」,且自愈救不回(2026-09-11)
- 现象:真机 `What do u wanna do kitten` 有 8 条资产的 `kind` 是**大写** `"UI"` 、`mediaType` 是 `application/json` 、`localPath` 是 `ui/UI 设计 N.json` ,永远停在「待归类」。
- 成因链:写入侧写大写——`ui_editor/resource_bridge.rs` 的 `register_local_asset_at(root, &relative_path, "UI", "application/json", …)` , `workflow.rs` / `persistence.rs` 同;而 alias 表只有小写 `"ui" => "ui-design"` , 于是 `"UI"` 落到 `canonical_game_creation_app_asset_kind` 的 `image` 兜底 → `image → unclassified` 。
- **为什么读时自愈救不回来 ** :自愈规则的前提是「派生值不是 unclassified」,而 `"UI"` 的派生值**就是** `unclassified` ,规则永不触发。所以只能在 alias 表收口——别名表必须**大小写不敏感**( `UI` / `ui` / `ui-prototype` 都要落 `ui-design` ),并把 `font` ( `ttf / otf / woff / woff2` 上传登记的 kind,见 `commands.rs` 的 `register_local_asset_entry(root, &relative_path, "font", …)` )一并补进别名表落 `document` 。
- 守卫三层:Rust `asset_category_mapping_covers_every_canonical_ kind` 逐条 断言(旧断言曾把 `"UI" → unclassified` 钉死, 正是这条 bug 的护栏 反向加固);`ui_editor/resource_bridge.rs` 的 `bridge_is_idempotent_and_installs_source_image` 走真实生产函数 → 真实写入 → 断言落盘 `category` ; TS `assetKindCanonicalMapping.test.ts` 的「写侧 kind 字面量 → 分类」直接解析写侧源码的第 3 个实参,写点换个新字面量就会红 。
- 关联:`server-rs/crates/shared-contracts/src/game_creation_app.rs` 、`packages/shared/src/contracts/gameCreationApp.ts` 、`apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs` 、`apps/ai-game-creator-shell/tests/assetKindCanonicalMapping .test.ts` 。
- 成因链:写入侧写大写——`ui_editor/resource_bridge.rs` 的 `register_local_asset_at(root, &relative_path, "UI", "application/json", …)` , `workflow.rs` / `persistence.rs` 同;当时的 alias 表只有小写 `"ui" => "ui-design"` , `"UI"` 因此 落 `image` 兜底 → `unclassified` 。**注意 `font` 早已是正式 canonical 成员(不再靠别名落 `document` ) **。
- **为什么读时自愈救不回来 ** :自愈规则的前提是「派生值不是 unclassified」,而 `"UI"` 的派生值**就是** `unclassified` ,规则永不触发。**现行口径(2026-09-17 起)**:不给 `" UI"` 找归一口径,而是修写入侧写 canonical 字面量;读侧严格解析,认不出的原值收口 `unknown` + `app_log!` 留痕(原始串 + 上下文) 。
- 守卫三层:Rust `asset_category_mapping_covers_every_kind` 穷举 断言(旧的 `"UI" → unclassified` 断言 正是这条 bug 的反向加固,已删 );`ui_editor/resource_bridge.rs` 的 `bridge_is_idempotent_and_installs_source_image` 走真实生产函数 → 真实写入 → 断言落盘 `category` ; TS `parseGameCreationAppAssetKind` / `console.warn` 用例钉住"非 canonical 值收口 + 留痕原值" 。
- 关联:`server-rs/crates/shared-contracts/src/game_creation_app/asset_kind .rs` 、`packages/shared/src/contracts/gameCreationApp.ts` 、`apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs` 、`apps/ai-game-creator-shell/tests/assetKind.test.ts` 。
## AGC 资源搜索栏改成「临时叫出」的浮层,工具条带与它的下移逻辑一并撤掉(2026-09-11)
@@ -5679,6 +5861,23 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- Vitest 的 `toHaveBeenCalledWith` 匹配任意一次调用,失败输出会列出其它命令;应先定位相同命令的真实参数差异,不能由其它调用的序号推断时序故障。
- 存在后台轮询的 IPC mock 不应要求目标命令占据全局最后一次调用。验证刷新时先记录调用边界,再筛选该边界之后的目标命令,严格核对其最后一次参数,避免后台查询影响断言,也避免旧调用掩盖刷新未执行。
## 2026-09-16 Lexical 投影丢掉引用后的换行:`@素材` 和下一段粘成一个词
- **现象 ** :聊天输入区里先 `@` 一个素材、回车换段再写文字,提交出去的 canonical content 里没有任何分隔,直接读成 `@hero把这一版改成夜景` ;同一个字符串还会进 agent 输入、队列 chip 文案与润色判据。
- **原因 ** : `3c7b02b9f` ( 2026-09-15)为了让 content 通过 Rust 的「空 `input_text` 」校验,在投影层加了 `appendInputText` ( `if (text.trim())` 才落 part,并与相邻文本合并)。root 子节点之间补的段落分隔符与 `LineBreakNode` 传进来的都是 `'\n'` , `trim()` 为空 ⇒ 整段丢掉;chip 后那一段文字随后另起一个 part,派生文本用 `''` 直接拼接,于是粘成 `@hero把这一版改成夜景` 。`41366dd71` 又把消息正文 / 队列 chip / 快速编辑的文本派生切到这条投影上,缺陷扩散到界面与出站 prompt。
- **处理(最终口径) ** :不保留任何前端过滤,而是去掉规则和它的成因——Rust `validate_direct_codex_user_item` 改成只判整条 content( `content_has_meaningful_input` :有一段非空白文本或任意非文本 part 即有效),单个纯空白 `input_text` 合法;`ResourceReferenceInput` 的投影原样透传编辑器节点,既不丢空白也不与相邻 part 合并。中间版本(把待写文本「向前合并」到下一个 part)已随之删除:它仍会丢掉尾随换行与「两个 chip 之间只隔一个换行」的分隔,也仍要让前端替用户改写内容。
- **验证 ** : Rust `validation.rs` / `wire.rs` 新增「单个纯空白 part 通过校验、整条全空白拒绝」用例;`apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx` 「引用后面的段落分隔原样落进 content」断言 `[ref, { type: 'input_text', text: '\n' }, { type: 'input_text', text: '…' }]` 与派生文本逐字一致;`tests/appSurface/project-development.suite.ts` 的 Godot 用例断言 Shift+Enter 的两个换行各自成 part。
- **关联 ** : `apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx` ( `collectDraftParts` )、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs` 、`docs/project-memory/plans/【里程碑】DirectProject canonical content严格边界-2026-09-16.md` 。
## 2026-09-16 派生文本漏传 `manifest.assets` : `@引用` 从显示名退化成内部 id
- **现象 ** :快速编辑面板里 `@` 插了素材再点「修改」,出站 `derive_local_project_resource` 的 `prompt` 是 `把夜色改成星空@source-rules` ,而界面 chip 与聊天输入区显示的是 `@rules` ;仓库自带的 `projectResourceLiveIntegration` 用例因此长期是红的(`expected '把夜色改成星空@rules' to be '把夜色改成星空@source-rules'` )。同一根因还让排队消息 chip 显示 `@asset:…` 。
- **原因 ** : `directCodexContentToPromptText(content, assets)` 的 `assets` 有默认值 `[]` ,漏传不报错、只是把 `agc_resource_reference` 退化成 `${resourceId}` 。`ResourceReferenceInput` 自己读草稿的四处都带了 `manifest.assets` ,而它的三个消费方漏传:`chatComposerQueue.queuedChatTurnLabel` (宿主 `ComposerTurnQueue` 也没接素材清单)、`project-development/index.tsx` 的 `applyResourceQuickEditPrompt` 与 `applyResourceQuickEditDraft` ;后两个的 `useCallback` 依赖里同样没有 `manifest.assets` ,改完还会读到旧清单。
- **处理 ** :三个消费方全部补上素材清单并进依赖数组——`queuedChatTurnLabel(turn, assets)` + `ComposerTurnQueue` 新增 `assets` 属性(由 `ProjectSupervisorView` 传 `chatProjectAssets` )、快速编辑的两处改用 `manifest.assets` 。改「比较用的草稿文本」与「落进面板的文本」必须同一个口径,否则 `replaceText` 会每次输入都重跑一遍。
- **加固 ** : `directCodexContentToPromptText(content, assets)` 与 `queuedChatTurnLabel(turn, assets)` 的 `assets` 改为**必填**(删掉 `= []` 默认值),测试里刻意不传 manifest 的地方显式写 `[]` 。理由:默认值把「漏传素材清单」从编译期错误降级成运行期文案退化,正是本条缺陷的入口;队列 chip 与消息正文从此共用同一个派生(`queuedChatTurnLabel` 只多做「压成单行 + 限长」)。
- **验证 ** : `apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx` 的「快速编辑提示词里能 @ 出资源选择器」由红转绿(该文件 29 passed);`tests/appSurface/chat-composer.suite.ts` 新增「队列 chip 的 @ 引用按 manifest 显示名展开」;`appSurface.test.ts` 467 passed、定向 51 passed、`npm run ai-game-creator-shell:typecheck` 、`npm run check:encoding` 通过。
- **关联 ** : `apps/ai-game-creator-shell/src/features/project-workspace/chatComposerQueue.ts` 、`ComposerControls.tsx` 、`ProjectSupervisorView.tsx` 、`apps/ai-game-creator-shell/src/view/project-development/index.tsx` 、`apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx` 。
## 2026-09-16 并发生图遇到“登录态冲突”:身份代次与凭据轮换混用
- **现象 ** : DirectProject 长回合里并发派发的生图 / 素材生成请求中途报 `authentication-required: 陶泥儿登录态已变化,旧账号请求已停止,请使用当前账号重试` ,或平台工具返回 `HTTP 401 invalid-token` ;账号并没有切换,重新登录后短时间内可复现。
@@ -5698,11 +5897,18 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
## 2026-09-16 AGC 壳跑过 `cargo test` 后前端 typecheck 必红:ts-rs 导出把 `generated/*.ts` 重写成另一种形状
- **现象 ** :在 `apps/ai-game-creator-shell/src-tauri` 跑过 `cargo test` 之后,`npm run ai-game-creator-shell:typecheck` 报 10 条类型错(`src/features/project-workspace/resourceReferences.ts:106-112` 的 `string | undefined` 不能赋给 `string | null` ;同文件 134 行的对象字面量带 `type: 'message'` ,而 `DirectCodexUserMessageItem` 里没有该字段),`npm run ai-game-creator-shell:build` 也死在 `beforeBuildCommand` 的同一条 typecheck 上。
- **原因 ** : crate 里的 ts-rs 导出会按**本机依赖版本**重写 `src/features /project-workspace /generated/DirectCodexUser*.ts` :注释头变成 "This file was generated…"、字符串改双引号、`DirectCodexUserMessageItem` 丢掉 `type: 'message'` 判别字段、并多出一个 `DirectCodexUserMessageEnvelope.ts` 。仓库里提交的那份是前端真正依赖的形状(前端按带 `type` 的判别联合写),重写后两边就对不上——错在生成器版本漂移,不在前端。
- **处理(现行口径) ** :不要把重写结果当改动提交。跑过 `cargo test` 或构建后先 `git checkout -- apps/ai-game-creator-shell/src/features /project-workspace/generated` ,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts` ,然后才做 typecheck / 打包;绑定与前端形状冲突时以**已提交的 绑定 + 前端**为基准排查。
- **验证 ** :恢复提交 版本后 `npm run ai-game-creator-shell:typecheck` exit 0( `[skill-pack] OK` );保留重写结果时同一条命令 exit 2。release 构建本身还会在 `src/features/ui-editor/types/` 落下 `BindingChange.ts` / `BindingDTO.ts` 两个无人引用的未跟踪文件,属同类 生成产物。
- **原因 ** : crate 里的 ts-rs 导出会按**本机依赖版本**重写 `src/view /project-development/chat /generated/DirectCodexUser*.ts` :注释头变成 "This file was generated…"、字符串改双引号、`DirectCodexUserMessageItem` 丢掉 `type: 'message'` 判别字段、并多出一个 `DirectCodexUserMessageEnvelope.ts` 。仓库里提交的那份是前端真正依赖的形状(前端按带 `type` 的判别联合写),重写后两边就对不上——错在生成器版本漂移,不在前端。
- **处理(现行口径) ** :不要把重写结果当改动提交。跑过 `cargo test` 或构建后只恢复 ` apps/ai-game-creator-shell/src/view /project-development/chat/generated/DirectCodexUser*.ts` 的仓库版本,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts` ;不要恢复整个 `generated/` 目录,以免误删 DirectThread 的现役绑定。绑定与前端形状冲突时以**已提交的 DirectCodexUser 绑定 + 前端**为基准排查。
- **验证 ** :恢复仓库 版本后 `npm run ai-game-creator-shell:typecheck` exit 0( `[skill-pack] OK` );保留重写结果时同一条命令 exit 2。release 构建本身还会在 `src/features/ui-editor/types/` 落下 `BindingChange.ts` / `BindingDTO.ts` 两个无人引用的生成产物;它们不属于前端契约,发现后直接删除,不提交 。
- **关联 ** : `apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/` ( ts-rs 导出源)、`apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts` 、`apps/ai-game-creator-shell/scripts/build-release.mjs` ( `beforeBuildCommand` )。
## 2026-09-16 Node 26 下 vitest 的 jsdom 用例拿不到 window.localStorage
- **现象 ** :只声明 `@vitest-environment jsdom` 的用例里 `window.localStorage` 是 `undefined` (典型报错 `Cannot read properties of undefined (reading 'clear')` );而 `appSurface.test.ts` 里同样访问 `window.localStorage` 却一切正常。
- **原因(已核对,不是推断) ** : Node 26 在 `globalThis` 上定义了实验性 `localStorage` 访问器,不传 `--localstorage-file` 时它返回 `undefined` 。vitest 0.34 的 jsdom 环境把 jsdom window 的描述符复制到全局时,不会覆盖 `globalThis` 上已存在的键,于是 jsdom 真正的 `Storage` 被 Node 的 `undefined` 顶掉。`appSurface` 之所以不受影响,是因为 `apps/ai-game-creator-shell/tests/appSurface/harness.ts` 自己用 `Object.defineProperty(window, 'localStorage', …)` 挂了内存实现。
- **处理 ** :跑这类用例时给 Node 加 `--localstorage-file` ,例如 `NODE_OPTIONS="--localstorage-file=/tmp/vitest-node-localstorage" npx vitest run <files>` ; `apps/ai-game-creator-shell/tests/clientApi.test.ts` 与 `chatPromptPolish.test.tsx` 加这个开关后 26 项全绿。不要改业务代码或往测试里塞假 storage 来绕过。
- **关联 ** : `vitest.config.ts` ( `environment: 'node'` + 用例内 docblock 切 jsdom)、`apps/ai-game-creator-shell/tests/appSurface/harness.ts` 的内存 localStorage 垫片。
## 2026-09-17 AGC 壳首页无限 setState:effect 依赖了每次渲染都换身份的普通函数
- **现象 ** :dev 客户端停在首页、不点任何东西也会持续刷 `WEBVIEW error webview: Maximum update depth exceeded …` (5 秒涨 ~8.5 KB 日志),对应 WebView2 renderer 工作集涨到 **4.2 GB ** 、CPU 持续累计(约 0.7–1.5 核);表现上很像"模板库卡片太多/滚动卡",实际与页面内容无关。
@@ -5750,3 +5956,33 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **易错点 ** :① 不要只在面板里 `console.warn` 或靠 UI 文案兜底,这类遗漏在自动检查里是静默的;② 单槽位工具不要让「画布点选 vs 素材库选择」互相挤占——留着旧的会让用户这次的选择看起来没生效;③ 参考图上限不要沿用图片生成的默认值(图生 3D 只接受一张)。
- **验证 ** : `src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts` 、`src/components/image-editor/ImageCanvasUploadModel.test.ts` 、`src/components/image-editor/projectAssetReferencePickerModel.test.ts` 、`src/components/image-editor/ImageCanvasGenerationModel.test.ts` 中针对 `model3d-image-to-model` 的用例。
- **关联 ** : `src/components/image-editor/ImageCanvasGenerationDialogModel.ts` 、`src/components/image-editor/ImageCanvasUploadModel.ts` 、`src/components/image-editor/ImageCanvasGenerationModel.ts` 、`docs/project-memory/plans/【实施计划】Tripo生成前端入口-2026-09-21.md` 。
## 2026-09-21 受控 Lexical 输入区的回写用被动 effect:滞后渲染的 props 会把用户草稿清空
- **现象 ** : DirectProject 输入盒里粘贴(或连续输入)长文本,提交时 `chat_with_game_creator_direct_codex` 根本没发出去,界面停在空输入盒;`chat-composer` 用例里表现为「队列/终止/语音追加」五条一起红,但手工操作只在快速输入后偶发。
- **原因 ** : `ResourceReferenceInput` 的受控回写写在 `useEffect` 里,判据是「`value` /`references` 与编辑器当前草稿不一致就重建 root」。宿主对草稿的回显(`chatInput` /`chatReferences` )永远晚于编辑器的 Lexical 提交:一次渲染提交后它的被动 effect 可能排在用户下一次输入之后才执行,于是它读到的是**旧 `value` 加新编辑器状态**,判定不一致→`applyDraftToRoot` 整份重建→编辑器 `onChange` 把空草稿写回宿主→宿主再回显空值,来回清空。`references` 每次 `handleComposerDraft` 都换数组身份,进一步保证这段 effect 每次都跑。
- **处理 ** :受控回写改成 `useLayoutEffect` ,与本次提交同帧执行,读到的 props 与编辑器状态属于同一次提交;被动 effect 的滞后回调不再可能出现。旧 `value` +`references` 双轨语义本身仍是待收口的债务(见 canonical content 里程碑)。
- **验证 ** : `apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts` 的队列、终止×2、恢复回合、语音追加五条用例转绿;`tests/resourceReferenceInput.test.tsx` 全绿。
- **关联 ** : `apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx` 、`docs/project-memory/plans/【里程碑】DirectProject composer canonical content闭环-2026-09-21.md` 。
## 2026-09-21 删链路只删订阅、没删它喂的 state:留下恒为默认值的死 prop
- **现象 ** : `876529e66` 之后策划 Agent 一轮里再也看不到流式正文和「思考过程」,`planningV2Reasoning` 恒为 `''` 、`designReasoning` 这个 prop 永远传空;对应用例只在 `appSurface` 里红一条,很容易被当成「测试过期」删掉。
- **原因 ** :该提交目的是删除 Project Supervisor,但把 App 里唯一订阅 `design-agent-update` 的 effect 一并删了;Rust 侧 `design_*` 三个命令仍在 `app.emit("design-agent-update", …)` (含 `reasoning_text` /`text` /`view` ),前端却无人监听。判断线索是残留的 `designAgentEventSubscriptionReady()` ——它在发起回合前被 await,但没有任何代码再创建这个 promise,说明订阅被误删而不是有意退役。
- **处理 ** :恢复订阅(正文/工具提示 → transient reply, `reasoningText` → `planningV2Reasoning` , `view` → `applyDesignAgentViewAfterTransient` ),补回订阅就绪 promise 的建/解函数;新增 `designAgentReasoningTurnRef` 让回合结束后仍认本轮 reasoning。
- **验证 ** : `tests/appSurface/design-agent.suite.ts` 「keeps the current turn reasoning after completion and supports collapse/expand」与「renders historical reasoning as independent collapsed sections」同时通过。
- **关联 ** : `apps/ai-game-creator-shell/src/App.tsx` 、`apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs` 。
## 2026-09-21 DirectProject 聊天 IPC 里的「可读 prompt 投影」是死参数,接回模型输入就污染标签
- **现象 ** : `chat_with_game_creator_direct_codex` 的 Tauri 入参曾并列带一个 `prompt: String` (前端把 canonical content 渲染成 `@显示名` 文本),Rust 侧从不读取——`cargo check` 直接报 `src/agent/direct_runtime/user_input.rs` 的 `unused variable: prompt` ;前端每次提交仍要算一遍,界面测试也钉着这份投影文本。
- **成因 ** : DirectProject 回合真正的输入只来自 canonical `userItem` 。`direct_codex_user_item_to_codex_turn_input` ( `agent/direct_codex_user_item/wire.rs` )在 DirectProject 分支重建 `turn/start.input` , `LlmRunRequest` 里那份 `user_prompt` 会被覆盖;客户端投影走的是另一套口径(素材被删或改名时退化成裸 resourceId),一旦有人把它接回 Codex,就把 `@显示名` 标签污染了模型输入。
- **现行口径 ** : IPC 只传 `projectPath` 、`clientTurnId` 、`userItem` 与可选 `creationType` ; Rust 只从 `userItem` 派生回合输入(空判定与三维契约探测用的派生 prompt 仍在 Rust 内部生成)。前端那份 `@显示名` 投影只服务本地的 `/history` 识别与队列 chip 文案,不出 IPC;界面断言只能读 `userItem.content` 。
- **关联 ** : `apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs` 、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs` 、`apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts` 、`apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts` 。
## 2026-09-21 DirectProject 结构化消息不能逐个拒绝空白文本片段
- **现象 ** :多行正文、末尾空段落或合法引用前后的分隔空格会让有内容的消息报错;编辑器为了保持结构产生的空白文本片段被误判为“聊天内容为空”。
- **原因 ** :校验器对每个 `input_text` 单独执行 `trim().is_empty()` 并立即拒绝,混淆了结构化片段合法性和整条消息是否有实际内容。
- **处理(现行口径) ** : `input_text` 允许空字符串、空格和换行,校验过程保持全部片段的原文、分段与顺序,不做合并或删除;遍历完整条消息后,只在既没有非空白文字、也没有任意非文本 part(素材引用 / 运行画面引用 / Skill 引用 / 附件引用)时返回“聊天内容不能为空”。各类引用仍逐个执行原有校验,消息带正文也不能绕过非法引用。
- **关联 ** : `apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs` 、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md` 。