# 踩坑与排障记录 ## 2026-09-17 AGC 输入盒的「推理档」弹层被祖先裁切:要放开裁切而不是挪弹层 - **现象**:窄窗口下(视口 ≤1000px 时右侧对话面板只有 280px 宽)点开输入盒右下角的「推理档」,弹层是个**空盒子**:档位文字(默认 / 低 / 中 / 高 / 最高)整片看不见,只剩一个方框。 - **成因**:推理档是控制排里最靠左的弹层锚点,`.conversation-model-menu` 默认 `right: 0` 贴触发钮右缘**向左**展开;触发钮右边还压着模型选择、语音、发送三颗钮,所以 150px 宽的弹层在 280px 面板里会伸到面板左侧 42px 之外。`.game-workbench-chat`、`.project-supervisor-surface.is-direct-codex`、`.project-supervisor-conversation` 三层各自的 `overflow: hidden` 沿自己的溢出边界裁掉它,而档位文字起点才 14px(面板左内边距 5px + 按钮左内边距 9px),正好落在被裁掉的那半边。 - **处理(用户指定口径)**:不挪弹层位置——只让 direct-codex 那三层不再裁切:`.game-workbench-chat:has(.project-supervisor-composer.is-direct-codex)`、`.game-workbench-chat .project-supervisor-surface.is-direct-codex`、`.game-workbench-chat .project-supervisor-surface.is-direct-codex .project-supervisor-conversation` 三条 `overflow: visible`。弹层的 `right: 0`、尺寸和触发钮锚点全不变,只是允许它盖到左侧资源面板上完整显示。消息列表自带 `overflow-y: auto`(另一轴按规范计算为 auto),消息内容仍由列表自身裁剪。 - **易错点**:① 把弹层改成 `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`」。 - **模型缩略图看起来被拉伸 / 被裁**:三个原因叠在一起 —— ① 缩略图渲染器的 `PerspectiveCamera` 是单例复用件,`aspect` 默认 `1` 且从没更新,方形投影被塞进宽扁缓冲;② 卡面里 `width/height: 100%` 的图片挂在 `place-items: center` 的网格里,网格项高度会退化成"按内容定高"(百分比高度解析成 `auto`),图片按自然比例长过卡片、被 `overflow: hidden` 裁掉,看起来就像被拉伸;③ 栏目画布用 `transform: scale(var(--resource-section-zoom))` 放大(真机 1.5 倍),而 `ResizeObserver` / `offsetWidth` 只看**布局盒**,缩放变化既不触发 observer,按布局尺寸 1:1 渲染的图也会被放大到发虚。修法:渲染前设 `camera.aspect`;图片改 `position: absolute; inset: 0` + `object-fit: contain`;渲染尺寸取 `offsetWidth/offsetHeight × 2` 超采样(上限 1024),并把实际渲染尺寸暴露到 `data-model-render-size` 方便排障。 ## 2026-09-17 从提权会话创建的目录会被 AGC 的 Windows owner 校验直接拒绝 - **现象**:在 Codex 会话里手工创建的工程目录(例如 `C:\Users\\Documents\Codex\...\cocos-preview-fixture`),用客户端打开时报 `Windows 安全对象不属于当前用户:`;Rust 侧同样用 `tempfile::tempdir()` 建夹具的用例也成片失败在同一句上。 - **原因**:这个 shell 以管理员身份运行,`New-Item` / `tempfile` 新建目录的 owner 是 `BUILTIN\Administrators`,而 AGC 的校验要求 owner 等于当前用户 SID(`KDLETTERS\`)。`Get-Acl | Select Owner` 与 `whoami` 一比就能定性;同一台机器上由客户端自己创建的目录 owner 正确,所以「客户端自己建的项目能用、手建的不能用」。 - **处理**:手工夹具先 `icacls /setowner "\" /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 资产门禁;缓存与项目切换须保留现有身份隔离。 ## 窗口 Context 发布不得依赖每次渲染新建的业务回调 工作台向窗口标题栏发布运行项目时,若 effect 依赖普通函数派生的回调,发布 Context 会重新渲染工作台,进而再次发布并清理,形成更新深度循环。转发入口须稳定,并在提交阶段更新实际处理器引用;发布数据变化与卸载清理分开。回归测试必须组合真实窗口 Provider 和工作台消费者,只有独立画布测试无法覆盖这条反馈链;回归时用有界发布次数阻止测试失控。画布快速操作时暴露的更新深度错误,也须检查外层状态同步,不能直接归因于滚轮频率。 活动回合快照的初始签名须与初始空数组一致,首次异步返回空数组不能额外换引用。停用、重新启用或切换读取器时应使旧请求失效,避免晚到结果覆盖新快照;测试需控制 Promise 完成时机,不能用“初始数组已为空”当作请求已结束。无原生读取器时窗口只发布一次空状态。 ## 2026-09-17 工具 schema 声明的上限与真实校验不一致,会表现成「agent 调不动这个功能」 - **现象**:用户反馈「客户端没法由 agent 调用图片快速编辑功能以及背景音乐生成功能」。查工具目录时两个工具都在(`agc_edit_image`、`agc_create_or_derive_resource`),图片快速编辑在真实项目日志里还有成功记录;但 agent 侧写一句正常长度的背景音乐描述就失败,而客户端 UI 用同一个提示词却只是被截断加提示。 - **原因**:`agc_create_or_derive_resource.prompt` 在 MCP schema 里只声明 `maxLength: 4000`,真实上限按 kind 分(背景音乐 140 / 音效 1900 / 视频、角色动画 4000 / 图片 32000),MCP 层还额外写死一条 `kind == background-music && > 140` 的判断;skill 包没有任何一处写这两个数字。模型从 schema 与 skill 都无法得知 140,于是必然踩一次硬拒。同类隐患还有两处:客户端工具桥用通用 4000 校验 prompt,会把 4000 以上的图片编辑提示词误报成「超出安全边界」;按 kind 校验散落在 MCP 与桥两处,新增类型容易只改一处。 - **处理**:上限收敛到 `resource_edit_prompt_max_chars` 单一权威(工具层、桥、提交校验共用),超限文案复用 `resource_edit_prompt_limit_error`;工具 schema 用 `allOf[oneOf]` 逐 kind 声明 `prompt.maxLength` 并在描述里写明数字;prompt 的传输层边界退到信封级,避免通用常量先于按 kind 上限报错;两端 skill 文档同步写明四个数字。新增 `tool_prompt_limits_agree_with_the_client_authority` 作为门禁:四类 kind 的 schema 上限、桥上限与权威口径必须同数字,且超限文案必须带真实上限。 - **验证**:`cargo test --bin genarrative-ai-game-creator-shell -- --test-threads=1 agent::direct_tools_mcp::tests`(22 passed,含两条走 MCP 工具层 → 真实工具桥 → 假平台的媒体工具契约用例与一条超限零请求用例)、`agent::direct_tool_bridge::tests`(17 passed,含新增的按 kind 上限门禁;另有 7 条本机既有失败)、`agent::skill_pack`(4 passed)、`npm run agc:skill-pack:check`。本机 `tempfile::tempdir()` 归属校验失败会让 `project::resource_editor` 45 条与 `agent::direct_tool_bridge` 7 条既有用例失败,改动前后同为失败,不要据此误判回归。 - **关联**:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`src-tauri/src/agent/direct_tool_bridge.rs`、`src-tauri/src/agent/direct_tools_mcp.rs`、`src-tauri/resources/agc-skills/agc-client-projection/`。 ## 2026-09-16 从 Codex 里启动 AGC 客户端会看到被重定向的 `%APPDATA%` - **现象**:在 Codex 会话里用 `Start-Process` 启动 `genarrative-ai-game-creator-shell.exe` 做排障时,子进程写 `C:\Users\\AppData\Roaming\world.genarrative.ai-game-creator\...` 的内容会落到 `C:\Users\\AppData\Local\Packages\OpenAI.Codex_2p2nqsd0c76g0\LocalCache\Roaming\...`;同一个 `Test-Path` / `Get-ChildItem` 命中的是重定向视图,只有 `\\?\C:\Users\...` 形式能区分真实路径。 - **影响**:用 agent 拉起的客户端复现「多开 / 登录态冲突 / 锁文件被占用」类问题时,可能与用户双击开始菜单快捷方式的真实进程不是同一份 AppData,从而得出「两个实例没有互相冲突」或「日志里没有那条失败」的错误结论。 - **处理**:把用户双击快捷方式(或从 Codex 之外启动)的进程作为唯一用户侧证据;核对待查文件时同时比对真实 `AppData\Roaming` 路径与 `Packages\...\LocalCache\Roaming` 路径;排障结论里写明客户端是「谁启动的」。 ## Windows 已登记生图资产未刷新 Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\` / `\\?\UNC\`,而前端项目路径仍是普通盘符或 UNC。失效监听不能直接比较原始字符串;识别为同一项目后,用当前项目路径重读 manifest,保留项目切换与 revision 门禁。普通 `agc_generate_image` 成功提交也必须发出失效通知,不能依赖整轮 Agent 结束。回归需覆盖两种 Windows 前缀、其它项目事件拒收,以及 Agent 尚未结束和后续失败时已登记图片卡片仍可见。 ## 2026-09-14 严格 IPC 桩缺登记新命令时,症状可能是「unhandled rejection + 不相干的提示断言」,而不是同一处报错 - **现象**:`ProjectDevelopmentView` 新增「项目打开时读生成任务账本」(`list_local_project_asset_generations`)后,两个**别的关注点**的用例同时红:`resourceCanvasManualLayout.test.tsx` 报 `AssertionError: expected [ Array(1) ] to deeply equal []`(严格桩把新命令记进 `unexpectedCommands`),并伴随 7 条 `Unhandled Rejection: TypeError: Cannot read properties of undefined (reading 'map')`;`appSurface/project-development.suite.ts` 的「布局读时提示」用例则因为新命令被当成 unexpected invoke 抛错、触发了新的提示条,导致 `queryBySelector('.game-resource-live-notice')` 断言失败。 - **原因**:这些用例的 `invoke` 桩是**严格白名单**(未登记即抛错或返回 `undefined`)。新命令在挂载期就被调用,于是:① 桩把未登记命令记进 `unexpectedCommands`/抛错;② 生产代码若对返回值无形状防御,就在 `undefined` 上 `.map` 产生 unhandled rejection。**两条失败都指不到真正的新增调用点**,很容易被误判成各自关注点的回归。 - **处理(现行口径)**:① 渲染 `ProjectDevelopmentView` 的桩统一登记 `list_local_project_asset_generations`(返回**数组**,空账本 `[]`;Rust 侧返回 `Vec`,不是 `{ 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 编辑器再返回后,资源画布滚轮平移/缩放不再响应;返回前同一手势正常。 - **原因**:资源画布的非 passive `wheel` 监听绑定在 `resourceBookManagerRef` 当前 DOM 上,但 effect 只依赖 `handleResourceBookWheel` 与 `mode`。UI 编辑器切换会卸载旧 manager 并挂载新 manager,依赖不变导致新节点没有重新绑定监听。 - **处理**:将 `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)。 - **根因(本机实测,不是推断)**:端口归属探测原实现用 `Get-NetTCPConnection -State Listen -LocalPort` 逐端口取 owner,而它底层走 WMI:**单端口单次 11.2 秒**;再叠加每个 PID 的 `Get-CimInstance Win32_Process`(热调用 3.3 秒、首次 18 秒)。三个端口一轮 ≈ 43 秒,而就绪等待每约 1 秒轮询一次 ⇒ 首轮几乎必然判负、要等好几轮才通过。同机对照:`netstat -ano -p tcp` 29 毫秒、`[System.Diagnostics.Process].MainModule.FileName` 4 毫秒、`Get-Process -Id` 19 毫秒;`Get-WmiObject` 4.2 秒、`wmic` 本机已被移除。结论是慢在 WMI 本身,换 cmdlet 没用。 - **处理**:探测脚本改为 ①`netstat -ano -p tcp` 取「端口 → PID」——监听行判据用**外部地址 `0.0.0.0:0` / `[::]:0`**(不依赖会被本地化的 State 文本),PID 取**最后一列**而不是硬编码下标(状态列本地化或被合并时也取不错,这个下标一旦写错会被 `$ErrorActionPreference = "SilentlyContinue"` 静默吞掉,表现为「探测永远返回空」);②`[System.Diagnostics.Process]::GetProcessById(...)` 读进程名与可执行文件路径;③只有核对 SpacetimeDB `--data-dir` 归属(或路径读不到要兜底标签)时才按 PID 取命令行,并按 PID 记 5 分钟 TTL 缓存、随探测请求经 `GENARRATIVE_KNOWN_COMMAND_LINES` 下发,让轮询只在首个周期付一次 WMI 成本。探测本身失败仍返回 null 走旧的退化分支,「归属无法证明就不复用」的语义不变。 - **验证**:`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,响应体卡住会使后续刷新复用同一挂起请求、选择器持续忙碌。 - 成功 JSON 与错误响应体均复用 `readClientHttpResponseText` 的 15 秒上限;超时后保留最后一次有效目录并释放在途请求,手动重试重新发起请求。迟到的响应不得覆盖重试获得的新目录。 - 排查时区分接口未挂载(404)、未授权(401)、网络或响应体超时以及刷新无变化但缺少反馈;不能仅凭客户端启动 IPC 回退警告判断刷新失败原因。 ## 2026-09-14 AGC 壳 Rust 套件按「一片一 job」拆分,且分片必须自校验覆盖 - **现象**:`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=` 只跑自己那片(`--exact <名单> --test-threads=1`,片内串行),片与片之间靠 **job 级并发**摊开。配套把 `ai-game-creator-shell:check:rust` 拆成 `:rust:crates` 与 `:rust:shell`,AGC 相关门禁在 CI 里共 6 个 job(4 个分片 + smoke + crates)。 - **反面实验(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 内多进程并行多个片(实测比串行还慢)。 - **分支保护口径**(2026-09-14 复核):本仓库不把 Project CI 的 context 配成 `master` 分支保护的合并必需检查,合并前由人工确认最近一次结果;因此 job 拆分或改名不需要同步分支保护设置,代价是门禁红了不会自动阻止合并。 - **关联**:`apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs`、`.gitea/workflows/project-ci.yml`、`scripts/check-native-shells.mjs`(`agc-rust-shard-1..4` / `agc-rust-smoke` / `agc-rust-crates` 分组)、`package.json`。 ## 2026-09-14 根门禁的 `[check:native-shells]