合并 master 并接入 DirectProject 新聊天架构

- 合并 origin/master(304 个提交:DirectProject 聊天容器重构、Project Supervisor 退役、策划附件导入、CI 隔离编译缓存等)。
- 接受 master 对 ProjectSupervisorView / SupervisorChatOnlyView 的退役与预览快捷测试收敛;发布入口改由 DirectProject 聊天头承载。
- DirectProjectChatHeader 新增「发布到游戏广场」入口(无回调不渲染、回合忙态禁用),DirectProjectChatView 透传 onRequestGamePublish。
- App.tsx 继续由工作台壳持有试玩包导出与 GameDistributionPublishPanel,沿用 project.export_package 权限确认队列;check-config 把该命令从 native-only 清单移回 App invoke。
- 后台游戏审核 API / 类型 / 路由测试与 master 新增的 AGC 模板管理按双方保留合并,并修掉拼接造成的接口与用例闭合缺陷。
- 修正 master 自带的 viteProxyConfig 断言:/api/creation-entry 属退役路由,测试改为断言不进入代理。
- 记录合并踩坑:语法结构内部的冲突不能简单按「双方保留」拼接,必须按某一侧骨架重建并跑 tsc 与单文件测试。
- 验证:全量 vitest 393 文件 / 4374 用例通过,root / AGC / admin-web 三端 typecheck,cargo check 与游戏分发 Rust 测试,encoding、doc-index、rustfmt、SpacetimeDB schema guard。
This commit is contained in:
2026-09-22 16:45:29 +08:00
651 changed files with 78860 additions and 60952 deletions
+196 -4
View File
@@ -1,5 +1,103 @@
# 踩坑与排障记录
## 客户端图标素材描述不能先包装再截断
- 现象:资源画布填写了具体图标需求,平台实际收到的 `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: 异步生成完成结果无法绑定到 operationIdExternal 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 约 1520 分钟,而每次 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 核验存活再回调。
@@ -28,6 +126,11 @@
- **现象**`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 --features ts-bindings 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 分实例,卸载保留未提交/失败草稿,成功提交不再复活草稿;旧项目占位不存在时丢弃其保存回调。失败重试保留原请求输入和引用身份,引用失效不能静默过滤;修改已绑定输入须明确另起请求,不伪装成原请求重试。
@@ -64,6 +167,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`」。
@@ -110,6 +214,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 编辑器再返回后,资源画布滚轮平移/缩放不再响应;返回前同一手势正常。
@@ -117,6 +222,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` 已 20012:40:19 才判 ready)。
@@ -125,6 +231,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,响应体卡住会使后续刷新复用同一挂起请求、选择器持续忙碌。
@@ -135,7 +242,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 个 job4 个分片 + 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 内多进程并行多个片(实测比串行还慢)。
@@ -149,6 +256,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 资源画布不要恢复「无条件重派生」,否则新增一张素材就整张重排
- **现象**:用户生成一张新素材后,画布上既有卡片全部移位,刚摆好的位置失效。
@@ -162,7 +270,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` 分支保护设置。
@@ -4051,6 +4159,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` 限制。
@@ -5333,6 +5449,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 releaseTauri 依次打印 `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 与必需文件清单。回归测试自身也必须跨平台:仓库根目录用 `fileURLToPath(new URL(...))` / `defaultAppRoot()` 解析,不能用 `URL.pathname` 得到 `/C:/...` 后再 `path.resolve`;模拟 Windows 与 POSIX 缓存路径时要分别使用 `path.win32``path.posix`,不要用当前节点的原生分隔符断言另一平台。
## AGC 登录态续期必须同步本地运行时
- 模型目录 HTTP 请求与 DirectProject 的 Rust/app-server 使用同一账号,但凭据分别保存在 WebView 与 Rust / Runner;续期应复用 `requestPlatformSessionRefresh` 完成用户核验及本地会话安装,不能只写 localStorage。
@@ -5729,6 +5853,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`;账号并没有切换,重新登录后短时间内可复现。
@@ -5748,8 +5889,8 @@ 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` 或构建后只恢复 `apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUser*.ts` 的仓库版本,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts`;不要恢复整个 `generated/` 目录,以免误删 DirectThread 的现役绑定。绑定与前端形状冲突时以**已提交的 DirectCodexUser 绑定 + 前端**为基准排查。
- **原因**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`)。
@@ -5768,6 +5909,13 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **验证**:修复后同一台机器、同一路径下 35 秒内新增 `Maximum update depth` **0 条**renderer 工作集 **254 MB**(修复前 4.24.4 GB);`apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx` 断言轮询返回值不变时快照引用不变。
- **关联**`apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx``apps/ai-game-creator-shell/src/features/agent-runtime/directActiveTurns.ts``apps/ai-game-creator-shell/src/components/WindowChrome.tsx``apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`
## 2026-09-22 合并 master 时“双方保留”不是通用解法
- **现象**:把分支与 master 的同一批冲突统一按“ours + theirs 依次保留”处理后,`apps/admin-web/src/api/adminApiTypes.ts``api/adminApiClient.ts``app/adminRoutes.test.ts` 都出现语法/结构错误(接口少闭合、函数体被截断、用例少 `});`)。prettier 能通过、vitest 里被转译的纯类型文件也不报错,只有 `tsc`admin-web typecheck)与随后单文件跑测试才暴露。
- **原因**:冲突块可能切断一个语法结构(接口、函数、`test(...)` 调用),而双方各自的块只是在“同一位置添加内容”,拼起来会丢闭合;类型文件在 vitest 里被 esbuild 直接剥离类型,不会校验。
- **处理**:冲突若落在语法结构内部,按“以某一侧为完整骨架、把另一侧的新增内容插到正确位置”重建,而不是简单拼接;重建后必须跑 `tsc`(含 `npm run admin-web:typecheck`)并对改动文件单独跑一次 vitest,别只看全量测试是否绿。
- **验证**:合并后 admin-web 23 个测试文件 212 用例全绿、两端 typecheck 通过。
## 2026-09-20 复用注册端口段时可能连到别的 worktree 的 SpacetimeDB
- **现象**:在 `/data/dsk/Genarrative``npm run dev:api-server` 后,日志显示端口段 `10000-10099 (dsk)`、spacetime `http://127.0.0.1:10002`,但 api-server 反复报 `ws://127.0.0.1:10002/v1/database/xushi-p4wfr/subscribe` 返回 `HTTP error: 404 Not Found`,且始终不响应 `/healthz`
@@ -5803,3 +5951,47 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **原因**:同目录的 `ThemedModal``createPortal` + `focus-trap-react` 渲染模态。焦点陷阱存在时,模态之外的 `click` 不会触达 React 的处理器(实测:临时把 `FocusTrap` 换成普通 `div`、其余不动,同一个被模态遮住的按钮点击立刻恢复生效),所以自动化里“点模态背后的按钮”不会报错,只是静默无效。
- **处理(现行口径)**:① 产品行为保留“导出成功后自动打开面板”(一键发布入口),但受影响的用例必须先用 `findByRole('dialog', { name: '发布到游戏广场' })` 断言面板出现、点「关闭发布面板」再继续后续会话操作;② 给这类“新增自动弹窗”改流程时,先跑一遍相关 `appSurface` 用例,避免只跑新增用例;③ 排查同类“点了没反应”时,先看当前是否有焦点陷阱模态打开,而不是先怀疑事件绑定或状态。
- **关联**`apps/ai-game-creator-shell/src/App.tsx``setPublishPanelOpen(true)`)、`apps/ai-game-creator-shell/src/components/modal/ThemedModal.tsx``apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx``apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-tools-and-preview.ts`
## 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`
## 2026-09-21 应用日志整行凭据脱敏会吃掉整条结构化诊断
`append_application_log_line` 在落盘前对整行做 `sanitize_diagnostic_message`:行内只要出现 `token=``bearer ``authorization``credential``api key` / `apikey` / `api_key` 这类标记,**整行**就被换成 `<sensitive diagnostic details redacted>`,只留下时间戳与 `RUST module:` 前缀;同时每行还会被截到 2048 字符。于是把“身份字段 + 诊断正文”拼成一行 `app_log!` 时,正文里一个凭据词就可能让整条记录连 `eventId``code` 一起消失(2026-09-21 加统一错误事件的日志投影时按两行落:身份行只放程序生成与调用方常量字段,summary / hint / detail 等自由文本一律只放详情行,且自由文本先自行压平换行——裸词标记脱敏消不掉,自由文本放错行会把 eventId、code 一起带走)。
## 2026-09-22 Rust 对象快照必须对齐 CI 编译目录和 Cargo 环境
- 现象:sccache 快照已包含数百 MiB 对象,但全新 target 的“热缓存”仍然全部 miss,甚至比直接 rustc 更慢。
- 原因:Rust cache key 包含编译 cwdsccache `0.18.0` 还会 hash `CARGO_*` 环境(jobserver、jobs 等少数例外除外)。不同 checkout 根目录、随机的 `CARGO_BUILD_RUSTC_WRAPPER` 路径、预热遗漏 workflow 的 HTTP/retry/color 环境都可能让整套缓存 miss。小型真实 Rust 实验显示仅配置 `SCCACHE_BASEDIRS` 不能消除 cwd 差异。快照存在不等于缓存有效。
- 处理:预热使用已核实的 Gitea 路径 `/workspace/GenarrativeAI/Genarrative`,并与分片运行器一样从 AGC `src-tauri` 启动 Cargo;保存 `workspace.txt`,路径不符时回退直接编译。wrapper 放在容器内固定路径,daemon 状态与 Unix socket 仍使用随机私有目录;预热环境与 workflow 的 Cargo 环境由定向契约测试核对。不要为命中率随意增加 `RUSTFLAGS`、改写源码路径或恢复共享可写 target。
- 验证:相同源码、资源上限和独立干净 target 下分别记录无缓存、冷缓存、热缓存的编译耗时和 hit/miss;只有真实热命中有净收益才切换候选镜像。PR 的写入始终留在 job 容器层,公共快照仍由可信维护流程生成。
- 统计:job 私有 daemon 设置 `SCCACHE_IDLE_TIMEOUT=0`,由 `report` 显式停止;最终测试 bin 的不可缓存编译或测试可能超过一分钟,短 idle timeout 会让 daemon 提前退出,结尾查询启动新 daemon 后误报零次请求。容器销毁仍会回收该 job 的全部进程。
- 磁盘:快照构建拒绝含 `/opt/genarrative-ci/rust-cache` 的基础镜像,始终从无对象缓存的镜像重建;容器内删除旧对象不能释放 Docker 底层。对象缓存容量上限不涵盖宿主旧镜像及导出归档,切换验证后按运维文档人工保留当前版和一个回滚版,同时保护运行中 CI 使用的镜像。
- 扩展:预热所有 Rust 测试组时保留各自 cwd、profile、features 和锁策略;同一临时 target 的 Cargo fresh 不代表不同 cwd 都已生成缓存键,AGC 提示词契约、分片和 smoke 切换入口前清理预热 target。不要把 workspace 与 spacetime-module 合并成一次编译;Native shell release step 清空双 wrapper,避免将测试缓存扩展成发布缓存。当前 sccache 0.18.0 的 READ_ONLY 在 miss 后仍打包产物并产生 cache write error,不适合用来承诺“未命中无开销”。