Merge origin/master(33552d2e)进 fix/agc-composer-layout
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m38s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m13s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m20s
Project CI / Frontend tests (pull_request) Successful in 3m27s
Project CI / Backend tests (pull_request) Successful in 7m2s
Project CI / Repository checks (pull_request) Failing after 5m14s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m46s
Project CI / Native shell tests (pull_request) Successful in 9m10s

- 目的:master 在上一轮 CI 期间又前进了(#602 素材引用注册表修复等 9 个提交),PR 的 base 门禁要求 head 含最新 base。上一轮 CI 的 2 条红(Backend tests 18s、Repository checks 35s)日志都是同一条门禁:`pull request head does not contain the latest base commit; update the branch and rerun CI.`,与本轮代码无关
- 冲突 2 处,均按「两侧内容都保留」解决:
  - `apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx`:#602 的 `ref` / `useImperativeHandle`(`insertReferences` + `focus` 转发)与本轮新增的 `pushModelNotice` 并存
  - `docs/project-memory/shared-memory/pitfalls.md`:本分支的三条(输入区不留常驻状态行、档位改 popover 滑块、#600 输入区叠压)与 master 的两条(画布「引用」死按钮、渠道更新互相查杀)并存
- 其余文件自动合并;本分支的 `styles.css`、两个契约测试、`ConversationModelSelect` 改动未被 master 触碰
This commit is contained in:
2026-10-04 16:33:00 +08:00
67 changed files with 2709 additions and 218 deletions
@@ -1,5 +1,40 @@
# 决策记录
## 2026-10-04 游戏游玩次数修订:关停不强制 flush、flush 失败丢弃剩余分片、客户端 IP 只信 X-Real-IP
- 变更:ADR `docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md` 修订——原「正常 SIGTERM/滚动重启必须在 `finalize_shutdown` 内 force flush」作废;崩溃、被杀、正常关停都允许丢最后一个未落库窗口,`api-server` 不再注册关停 flush。
- 新增:一次 flush 按 500 分片,任一分片失败即终止本次 flush,剩余分片直接丢弃(`Build` 只把当前分片放回下一轮),避免连接不通时每个分片各等一次连接超时把 worker 卡住。
- 理由:关停丢一个窗口概率极低,强制 flush 要为在途网络写入等待、并把 worker 生命周期接进关停顺序;按 perf 与简单优先取舍。
- 受影响实现:`game_play_counter_worker.rs`(删 `flush_game_play_counter_for_shutdown`、失败即 break)、`main.rs`(`finalize_shutdown` 去掉计数 flush)、`game_play_counter.rs`(`take_pending` 仅测试使用)、`modules/game_distribution.rs`(上报先做内存限流预检再查公开可见性)。
- 安全修正:`request_context::client_ip_from_headers` 改为优先 nginx 覆盖写入的 `X-Real-IP`,`X-Forwarded-For` 只作回退且取最后一段(nginx 用 `$proxy_add_x_forwarded_for` 追加的真实对端),不再信任可伪造的首段;公开上报端点的匿名身份/限流键与微信支付下单的 `payer_client_ip` 同时受益。无 CDN 前置时 `X-Real-IP` 即真实客户端。
## 2026-10-03 游戏游玩次数:api-server 内存去重缓冲 + 批量 procedure 落 play_count
- 背景:`game_distribution_game.play_count` 早已存在且随公开投影展示,但没有任何写入口;浏览列表、详情或发行网关加载都不能算「游玩」。需要一个不拖慢进入游戏、崩溃时最多少计一个窗口的上报链路。完整决策与备选方案见 ADR `docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md`。
- 触发与落点:游玩页点击「开始游戏」时网页 fire-and-forget 上报 `POST /api/game-distribution/games/{gameId}/plays`;不建新表,累加既有 `play_count`。
- 缓冲与写入:`api-server` 纯内存聚合,`GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS`(默认 5s)到点批量调用新 procedure `increment_game_distribution_game_play_counts_and_return`(输入 `Vec<{gameId, delta}>`);事务内只对 `published` 且有有效 `active_version_id` 的记录 `saturating_add`,且不更新 `updated_at`(避免重排作者列表)。读路径不叠加内存值,展示最多滞后一个 flush 间隔。~~正常关停强制 flush~~(2026-10-04 修订:关停不再强制 flush,见上条)。
- 身份与限流:登录用 `userId`、匿名用网页 `localStorage` 的 `clientId`(不可用时退化为会话内存值)、都拿不到回退 `IP + UA`;`identity + gameId` 30 分钟去重,`IP + gameId` 每分钟 60 次固定窗口限流。非公开/下架/封禁返回 404 且不计数;无效 Bearer 按匿名处理,绝不让计数阻断游玩。
- 失败语义:只把 `SpacetimeClientError::Build`(未发出)放回重试;`Timeout` / `ConnectDropped` / `Procedure` 直接丢弃并记录丢失量——少计优于双计,本指标不做双计补偿,也不共享跨实例去重窗口。一次 flush 按 500 分片,任一分片失败即终止本次 flush,剩余分片直接丢弃(2026-10-04 补充)。
- 影响范围:`spacetime-module/game_distribution.rs`(输入类型 + procedure + tx)、`spacetime-client` facade 与生成绑定、`api-server` 新增 `game_play_counter.rs` / `game_play_counter_worker.rs` 及 config/state/main/handler、前端 `gamePlayClientId.ts` / `gameDistributionClient.ts` / `GamePlayPage.tsx`、`.eslintrc.cjs` 白名单。
- 权威文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的 `game_distribution_game` 节,以及 `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` 的「游玩计数(已实现)」节。
- 验证:`cargo check -p api-server` 与 `cargo test -p api-server game_play_counter`(9 passed)通过;前端定向 vitest(点击上报断言 + clientId 稳定性)与 `eslint --max-warnings 0` 通过;`npm run check:server-rs-ddd`、`npm run check:generated-bindings`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过。
## 2026-10-03 AGC 画布引用统一走「活跃聊天输入区」注册表(Issue 602)
- 背景:画布的「引用」按钮与「拖拽批量引用」只派发 window 事件,消费者只有 `App.tsx` 一处,而它插的是绑在 `PlanningChatView` 上的 `chatComposerRef`;2026-09-22 DirectProject 拆分后普通项目走 `directProjectMode` 提前 return,渲染不到策划面 → ref 恒为 `null`,可选链静默吞掉点击(画布上是死按钮)。同一批合并冲突还丢了 `RESOURCE_REFERENCE_INSERT_MANY_EVENT` 的监听,批量引用连消费者都没有。
- 决策:新增 `features/project-workspace/activeChatComposer.ts`,模块级只保存**当前挂载的那一个**输入区句柄(`registerActiveChatComposer` 返回带身份校验的注销函数,并检测到第二个输入区注册时留一条 dev 告警——不改运行时语义;`insertChatReferences` 在空批次 / 无输入区 / 句柄报「这一批没插进去」三种情况返回 `false`)。`DirectProjectComposer` 用 `useImperativeHandle` 暴露 `DirectProjectComposerHandle`(按 ref 转发、由它回答插入是否真的递到输入区),`DirectProjectChatView` 与 `PlanningChatView` 挂载期间各自注册**按 ref 转发**的句柄(注册时不读输入区是否就位,因此不依赖父子 effect 顺序)(两条链路互斥渲染,同一时刻只有一个句柄)。`App.tsx` 收敛为一处监听,单条 + 批量两个事件都走 `insertChatReferences`(空批次直接返回:没有要插的东西,不能报成「没有可用的输入区」);返回 `false` 时 dev 下 `console.warn`。`chatComposerRef` 只保留给策划输入盒自己的 `getDraft` / `clear`。
- 边界:不采用「给 DirectProjectComposer 单独加 ref 出口 + App 按模式分流」的备选(那会把「哪个 ref 此刻是活的」继续留在检测点上)。插入仍经 `ResourceReferenceInput.insertReferences` + `focus()`(光标落在插入之后,连点两次按顺序追加)。真正根治的形态是画布与聊天的共同宿主用 context 下发插入能力;注册表语义与之一致,将来换实现不必动画布。
- 影响范围:`apps/ai-game-creator-shell/src/features/project-workspace/activeChatComposer.ts`(新增)、`src/App.tsx`、`src/view/project-development/chat/DirectProjectChatView.tsx`、`.../chat/components/DirectProjectComposer/DirectProjectComposer.tsx`、`.../planning/PlanningChatView.tsx`、`tests/activeChatComposer.test.ts`(新增,钉注册表合同)、`tests/resourceCanvasChatReferenceDrop.test.tsx`、`tests/appSurface/{project-development,design-agent}.suite.ts`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`、`pitfalls.md`、本文件。
- 验证(合并 master 后的最终一轮):`npx vitest run apps/ai-game-creator-shell/tests`(197 passed / 1 skipped 文件,1918 passed / 17 skipped 用例)、`npx vitest run tests/activeChatComposer.test.ts tests/resourceCanvasChatReferenceDrop.test.tsx`(2 files / 12 passed,含注册表合同:空批次、无输入区、句柄报落空、注销身份校验、重复注册告警、乱序注销)、`npx vitest run tests/appSurface.test.ts -t 引用`(4 passed)、`npm run agc:typecheck`(含 `check:tests:types`,exit 0)、`npm run check:encoding`、`git diff --check`、eslint `--max-warnings 0`(改动文件)。反向证伪:去掉注册调用后端到端用例变红;去掉空批次短路 / 重复注册告警后对应新用例各红一处。
## 2026-10-04 AGC 渠道更新进程与快捷方式隔离
- 背景:Tauri 2.11 的 Windows NSIS 模板通过 `MAINBINARYNAME` 查找并结束进程。dev 与 release 过去共用 `genarrative-ai-game-creator-shell.exe`,更新任一渠道都会结束另一渠道;release 从「陶泥儿 Release」改为「陶泥儿」后,`/UPDATE` 又不会自动重建旧快捷方式。
- 决策:dev 保留历史主程序文件名以维持升级链;release 使用 `genarrative-ai-game-creator-shell-release.exe`,其它非默认渠道使用带渠道后缀的主程序名。构建期把 `mainBinaryName` 与渠道端点、productName、identifier 同批注入,NSIS 因文件名隔离而只匹配自身渠道进程。
- 迁移:release Windows 包通过独立安装钩子读取旧 `陶泥儿 Release` 卸载项的安装目录,在原目录安装新包,迁移旧桌面/开始菜单快捷方式并清理旧主程序与孤儿卸载项;dev 继续使用原有旧展示名迁移钩子。
- 影响范围:AGC 渠道身份脚本、发布构建配置、Tauri 基线配置、Windows NSIS 钩子与渠道发布测试;不改变 OSS 分区、更新端点或客户端数据目录合同。
- 验证方式:渠道发布脚本定向测试、`check-config.mjs`、真实 NSIS 编译与双渠道安装/更新 smoke;真机安装仍需发布环境执行。
## 2026-10-03 AGC 栏目画布上传素材按入口栏目登记(Issue 359)
- 背景:AGC 客户端在资源栏目子画布(「UI 交互 / 角色与对象 / 场景与环境 / 音频」)左下角工具栏点「上传」后,提示条给出「已上传 1 个素材」,但当前栏目计数不变(仍「0 项」)、素材出现在「待归类」,用户看到的是"上传成功了但它从这一页消失了"。原因是上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套,而 `upload_local_asset` 原先不接受入口栏目。
@@ -26,6 +26,23 @@
- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住三层几何与操作排 `grid-row: 2 / position: static`、输入盒最高高度算式(12+160+6+28+0+8+28+12 = 254);`tests/appSurface/project-development.suite.ts` 钉住 composer 的 `gap: 8px`。真实渲染对照见 #600 PR:同一夹具在 495/438/360/296/280px 面板 × 空 / 长多行 / 队列+提示 四态下,改前 2~4 处相交(含文字被提示盖住),改后 0 处相交、0 处溢出。
- **关联**:`apps/ai-game-creator-shell/src/styles.css`(文件末尾「输入区工具栏与状态提示」区块)、`docs/【功能说明】AGC聊天AI润色与发送前提醒-2026-09-10.md`。
## 2026-10-03 AGC 画布「引用」死按钮:window 事件的消费者挂在一个只在另一条链路赋值的 ref 上
- **现象**(Issue 602):AGC 资源画布选中一张已登记素材,选中工具条点「引用」(图标 `@`、可见文案与 `title` 都是「引用」)没有任何反应——聊天输入框里不出现 `@素材名` 芯片,也没有任何提示。普通项目(`directProjectMode`)必现,立项策划项目(`planningStartMode`)复现不出来;把素材卡拖到对话栏的批量引用同样没反应。
- **原因**:画布侧只 `dispatchResourceReferenceInsert` / `dispatchResourceReferenceInsertMany`,而这两个 window 事件的**唯一**消费者是 `App.tsx` 里的 `chatComposerRef.current?.insertReferences(...)`,`chatComposerRef` 又只赋给 `PlanningChatView`;2026-09-22 DirectProject 拆分引入的 `directProjectMode` 提前 return 让普通项目整段跳过后面的策划面渲染 → ref 恒为 `null`,可选链把整次调用静默吞掉。同一批合并冲突还把 2026-09-21 新加的 `RESOURCE_REFERENCE_INSERT_MANY_EVENT` 监听整段丢掉,批量引用连消费者都没有。
- **处理(现行口径)**:`features/project-workspace/activeChatComposer.ts` 保存当前挂载的**那一个**输入区句柄(`registerActiveChatComposer` 返回带身份校验的注销函数 / `insertChatReferences`),`DirectProjectChatView` 与 `PlanningChatView` 挂载期间各自注册(两条链路互斥,同一时刻只有一个句柄);`App.tsx` 收敛为一处监听,单条与批量都走 `insertChatReferences`,返回 `false`(空批次 / 此刻没有输入区)时 dev 下 `console.warn`;`chatComposerRef` 只留给策划输入盒自己的 `getDraft` / `clear`。
- **判据/取证**:`npm run test -- apps/ai-game-creator-shell/tests` 里的 `resourceCanvasChatReferenceDrop.test.tsx`、`appSurface/project-development.suite.ts`(工具条「引用」)、`appSurface/design-agent.suite.ts`(策划链路)都断言**真实输入盒草稿**里出现 `[data-resource-reference-id="<assetId>"]`,不再是「事件被派发」;临时去掉注册调用后这三条会红,证明用例钉的是真链路。详见 [`【功能说明】AGC聊天素材引用-2026-09-08`](../../【功能说明】AGC聊天素材引用-2026-09-08.md) 文首一节。
- **边界**:只要还保留「window 事件 + 模块外 ref 约定」这种形态,新增聊天面就必须一起进注册表;更彻底的形态是画布与聊天的共同宿主(`ProjectDevelopmentView`)用 context 下发插入能力,注册表语义与它一致,将来换实现不必动画布。
## 2026-10-04 AGC 渠道更新按固定主程序名互相查杀
- **现象**:开发版和 release 同时运行时,更新其中一个渠道会把另一个进程一起结束;release 从旧产品名升级后,旧 `陶泥儿 Release.lnk` 仍指向旧安装目录,更新后的 release 没有可用快捷方式。
- **原因**:Tauri 2.11 的 NSIS `CheckIfAppIsRunning` / `KillProcess` 只按 `MAINBINARYNAME` 匹配。所有渠道都使用 `genarrative-ai-game-creator-shell.exe`,所以插件无法按安装目录区分进程;更新模式还会跳过快捷方式创建,产品名变化后旧图标不会自动迁移。
- **处理**:`channel-identity.mjs` 新增 `resolveChannelMainBinaryName`:dev 保留旧文件名,release 与其它渠道使用后缀文件名;`createChannelConfig` 同批注入 Tauri `mainBinaryName`。release Windows 包使用独立 `release-installer-hooks.nsh`,从旧 `陶泥儿 Release` 卸载项恢复安装目录,迁移旧快捷方式并删除旧主程序/卸载项;dev 的历史改名钩子保持不变。
- **不要踩的坑**:只改 `productName` 或 `identifier` 不能阻止 NSIS 互相查杀;只改安装包文件名也不能让 updater 选中正确的进程,必须把 `mainBinaryName` 写入构建期 Tauri 配置,并保证 dev 的历史文件名不变。NSIS 钩子仍只能在 `!macro` 内引用模板常量/插件,且文件必须 UTF-8 with BOM。
- **判据/验证**:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` 覆盖渠道主程序名、release 钩子与宏位置;`node apps/ai-game-creator-shell/scripts/check-config.mjs` 校验基线;真实 Windows NSIS 编译与 dev/release 同机更新 smoke 仍需发布环境执行。
- **关联**:`apps/ai-game-creator-shell/scripts/channel-identity.mjs`、`apps/ai-game-creator-shell/scripts/build-release.mjs`、`apps/ai-game-creator-shell/src-tauri/windows/{installer-hooks.nsh,release-installer-hooks.nsh}`、`docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md`。
## 2026-10-03 AGC 栏目画布上传素材落「待归类」:kind 派生分类不等于入口栏目
- **现象**(Issue 359):在 AGC 资源栏目子画布(如「UI 交互」「角色与对象」)左下角工具栏点「上传」选图片 / 视频 / 代码类文件,提示条给出「已上传 1 个素材」,但当前栏目计数纹丝不动(仍「0 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。
@@ -6339,6 +6356,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **验证**:`cargo test --locked -p api-server --bin api-server app::tests::http_tracing`(默认并发与 `--test-threads=1` 各连跑 20 次)、`cargo test -p platform-llm observability_tests`;更接近 CI 并发的是整段 `app::tests::`(91 用例同进程)与 `--skip bgfilter_worker --skip wallet_refund_outbox` 的全量 bin(1133 用例)连跑。
- **关联**:`server-rs/crates/api-server/src/app.rs`、`server-rs/crates/platform-llm/src/observability_tests.rs`。
## 2026-10-04 AGC 通知计数与 graceful terminate 的断言偶发都来自"跨线程 / 跨用例串台"
- **现象**:`agent::thread_manager::tests::active_turn_changes_publish_one_notification_per_real_change` 偶发 `left: 8 / right: 7`(进度内容变化必须通知一次);`process_session::tests::process_session_graceful_terminate_keeps_wrapper_alive_for_target_cleanup` 偶发 `left: "exited" / right: "terminated"`;两者都在 `AI game creator shell Rust lane 2/2` 分片里红。
@@ -58,7 +58,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
- DirectProject 对话先在完整历史中按回合/原始 item 身份关联,再分页渲染;每个回合只有一个呈现入口。有流按 item `seq` 交替文本和工具,无流采用历史正文;禁止位置猜配或同时展示累计回复与 item 正文。流写入单调归并,收尾等待落盘任务,不按磁盘“最后一段”猜最终回复位置。详见 AGC 实施计划的“DirectProject 回合展示唯一归属”。
- 回合生命周期只由活动 client 回合快照和 Direct 事件恢复;Provider 的历史终态通知不能创建活动 client 回合。消息发送时间保存在历史信封,原始 item 不混入宿主字段;完成后的中间文本和工具默认收进“执行过程”,最终回复及失败提示保持可见。
- AGC 安装产品名由渠道身份决定:`dev` 显示“陶泥儿开发版”,`release` 复用正式产品名“陶泥儿”,自定义渠道显示“陶泥儿 <渠道显示名>”;Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述,identifier 继续按 `<基线>.<渠道>` 派生,保证渠道数据与运行身份隔离(详见《AGC客户端更新检查与下载》的渠道与安装身份合同)。Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。内部可执行文件名保持稳定。
- AGC 安装产品名由渠道身份决定:`dev` 显示“陶泥儿开发版”,`release` 复用正式产品名“陶泥儿”,自定义渠道显示“陶泥儿 <渠道显示名>”;Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述,identifier 继续按 `<基线>.<渠道>` 派生,主程序文件名也按渠道隔离(dev 保留历史名,release 使用 `-release` 后缀),保证渠道数据、运行身份与更新进程隔离(详见《AGC客户端更新检查与下载》的渠道与安装身份合同)。Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。
- 新 Web 游戏为 `game/` 下的 npm + Vite + Phaser 4.2.1 工程,使用包导入且允许其它依赖;npm 预览与导出只读取 dist,运行素材需纳入构建。单 HTML → Phaser 迁移固定走 DirectProject:文件落盘后先用受控 `project.bootstrap` 在 `game` 执行无参数 `npm install`,再用支持相对 cwd 的 `project.verify` 构建并确认 `game/dist/index.html`,已有单 HTML/Godot 不通过 JSON Generator 伪装成 npm 工程。