Merge remote-tracking branch 'origin/master' into feat/game-fork
Project CI / Backend tests (pull_request) Failing after 34s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m27s
Project CI / Native shell tests (pull_request) Failing after 2m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m58s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m23s
Project CI / AI game creator shell web tests (pull_request) Failing after 45s
Project CI / Frontend tests (pull_request) Successful in 2m24s
Project CI / Repository checks (pull_request) Failing after 1m1s

# Conflicts:
#	apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs
#	apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx
#	apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx
#	docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
#	package.json
#	packages/shared/src/contracts/gameDistribution.ts
#	scripts/check-game-distribution-dto-parity.mjs
#	server-rs/crates/api-server/src/modules/game_distribution.rs
#	server-rs/crates/module-game-distribution/src/errors.rs
#	server-rs/crates/module-game-distribution/src/lib.rs
#	server-rs/crates/shared-contracts/src/game_distribution.rs
#	server-rs/crates/spacetime-client/src/active.rs
#	server-rs/crates/spacetime-client/src/active/mapper.rs
#	server-rs/crates/spacetime-client/src/active/mapper/game_distribution.rs
#	server-rs/crates/spacetime-client/src/game_distribution.rs
#	server-rs/crates/spacetime-client/src/module_bindings.rs
#	server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_snapshot_type.rs
#	server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_type.rs
#	server-rs/crates/spacetime-module/src/game_distribution.rs
#	server-rs/crates/spacetime-module/src/migration.rs
#	src/components/game-distribution/GameDetailPage.tsx
#	src/components/game-distribution/GameDistributionPages.test.tsx
#	src/components/game-distribution/GamePlayPage.tsx
#	src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
#	src/services/gameDistributionClient.test.ts
#	src/services/gameDistributionClient.ts
This commit is contained in:
2026-10-06 16:10:52 +08:00
314 changed files with 13094 additions and 2427 deletions
@@ -0,0 +1,69 @@
# 【里程碑】游戏买断制泥点付费与播放鉴权
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | implemented-awaiting-runtime-acceptance |
| Date | 2026-10-05 |
| Parent Spec | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(「游戏买断制泥点付费与播放鉴权合同」章) |
## 目标
把游戏分发从“公开即免费可玩”扩展为“作者可选买断制泥点付费;其他用户购买后永久可玩;后台审核可见价格且审核员不限次试玩”。
## 范围
- 作者端定价:网页 `/games/publish` 与 AGC 发布面板在新建与更新模式选择“免费 / 买断制 N 泥点”;两入口共用 `packages/shared` 定价组件 `PlatformGamePricingField`,AGC 壳请求透传 `priceMudPoints`;给已上线作品发新版本时预填价格与模式,作者不改则价格不变。预填价口径:取该作品**最新版本的冻结价**(该版本可能是 `pending_review` / `rejected` 状态的版本,因此预填值可能尚未生效),仅当该冻结价缺失或为 0 时才回落作品行当前价,两条来源都取不到才按免费(0)。AGC 壳侧实现 `resolve_publication_prefill_price`(`apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:811`);网页侧读版本详情 `detail.version.priceMudPoints ?? detail.game.priceMudPoints ?? 0`(`src/components/game-distribution/GamePublishPage.tsx:164`),后端 `version_detail_payload` 已把版本详情的 `game.priceMudPoints` 一并归一为同一冻结价口径(`server-rs/crates/api-server/src/modules/game_distribution.rs:3797`)。
- 玩家端:公开详情展示价格与购买态、泥点购买、购买后游玩。
- 付费游玩鉴权:未购买不得游玩,直连公开发行路径必须失败。
- 泥点钱包扣费与 `game_purchase` 流水。
- 后台审核详情与待审列表展示价格。
## 不在范围内
- 创作者分成与结算、退款/撤销购买、促销/限时免费、订阅或游戏内购、价格运营审批流。
- 评分/评论与付费的耦合、外部 OpenAPI。
- 免费游戏现有链路的任何行为变化。
## 依赖与前置条件
- 现有游戏分发状态机、审核 CAS/幂等与版本冻结资料机制。
- runtime profile 钱包的原子写与 `profile_wallet_ledger` 流水。
- 现有发行网关 sandbox/CSP/Cookie 约束,以及后台待审版本预览会话(token 前缀承载相对资源)模式。
- 主规范“游戏买断制泥点付费与播放鉴权合同”与本文档评审通过。评审前不写业务代码。
## 验收标准
- [x] 作者网页发布可选「免费」或「买断制 N 泥点」;负数/超上限/非整数被前后端同时拦截。证据:真实本地栈 E2E 价格 `1000001` → 400 且不落版本,`0` / `1000000` → 200;`cargo test -p module-game-distribution` 29 passed、`cargo test -p api-server game_distribution` 53 passed 覆盖定价校验,网页发布表单定价定向 Vitest 覆盖前端拦截。
- [ ] AGC 发布面板与网页一致可选「免费 / 买断制 N 泥点」,非法值被前后端同时拦截;给已上线作品发新版本时按「最新版本冻结价优先、缺失或为 0 时才回落作品行当前价」预填价格与模式,作者不改则价格不变。
- 已覆盖:AGC 发布面板定价定向 Vitest `apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx`(`:333` 默认免费提交 `priceMudPoints=0`;`:342` 买断制先本地校验:空值与 `1000001` 分别报「买断价必须是整数泥点」「买断价必须是 1 到 1000000 之间的整数泥点」且都不发发布请求,`120` 随版本提交 `priceMudPoints=120`;`:802` 历史响应没有回读价格时按免费预填;`:819` 更新模式预填线上买断价 240、作者不改仍按 240 提交);AGC 壳请求层校验定向 Vitest `apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts`(`:180` 透传 `priceMudPoints=240`;`:197` `1000001` / `1.5` 在 native command 之前失败关闭);预填口径 Rust 单测 `apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:1715`(`publication_prefill_price_prefers_latest_frozen_price_and_falls_back`:最新版本冻结价 300 优先于作品行 240、历史版本无冻结价回落 240、免费作品与缺字段按 0);两入口共用 `packages/shared` 组件 `PlatformGamePricingField`。
- **未覆盖,故本项保持未勾选**:AGC 真实栈「发布付费版本」未手点一次;真实本地栈 E2E 只覆盖网页发布入口的定价链路(`1000001` → 400 且不落版本、`0` / `1000000` → 200)。
- [x] 后台审核详情展示该版本价格;无价格的历史版本按免费展示。证据:`cargo test -p api-server game_distribution` 53 passed(`private_version_payload` / `version_detail_payload` 输出 `priceMudPoints`,历史无价格版本按 0);admin-web 审核价格展示定向 Vitest。
- [x] 审核员不限次数试玩待审付费版本,不校验购买、不扣泥点。证据:真实本地栈 E2E 审核员两次试玩待审付费版本均 200、两次包内字节一致,作者与审核员余额/流水均不变。
- [x] 未购买用户详情可见资料但无可播放入口;直连 `/games/<gameId>/` 返回 404。证据:真实本地栈 E2E 详情 `priceMudPoints=30` / `purchased=false` / `entryUrl=null`,发行网关与平台同源均 404,创建会话 403,未登录 401。
- [x] 余额不足购买失败,余额、账单与购买记录都不变。证据:真实本地栈 E2E 返回 400 `INSUFFICIENT_MUD_POINTS`,余额与流水不变。
- [x] 购买成功:余额减少 N、账单出现 `game_purchase` 流水、购买记录唯一、可反复游玩不再扣费。证据:真实本地栈 E2E 扣 30(120→90),`game_purchase` 流水与购买行各 1 条,重复进入不再扣费。
- [x] 双击、并发与响应丢失重试只扣一次。证据:真实本地栈 E2E 同 key 重放、换 key 重复购买均不再扣费;两路不同 key 真并发返回 200/200、只有一个 `replayed=false`、余额只扣一次、购买记录 1 条。
- [x] 作者本人与管理员免购买即可游玩付费作品且不扣费。证据:真实本地栈 E2E 作者(自购 400 `GAME_PURCHASE_OWNER_EXEMPT`、余额/流水不变、播放会话 200)与管理员(管理员令牌创建播放会话 200、入口 200);管理员免购买经 play-session 管理员分支实现。
- [x] 免费游戏的发布、详情、游玩与计数链路无行为变化。证据:真实本地栈 E2E 免费游戏详情保留入口、发行网关 200、匿名创建会话 200 且不签发令牌。
## 未覆盖边界(保持未勾选)
- [ ] 管理员安全下架后的播放会话失效:本轮未单独运行,与作者下架共用同一 404 关闭路径。
- [ ] 多账号 / 多游戏混合并发购买:本轮只覆盖单游戏、单账号的两路真并发。
## 证据要求
- 自动化:Rust 定向测试(定价校验、购买幂等与并发、授权判定、状态机)、Vitest(发布表单定价、详情购买态、游玩页会话挂载、审核价格展示)、schema/绑定与编码门禁。
- 运行时:真实本地栈跑通「发布付费游戏 → 后台审核可见价格并试玩 → 通过 → 未购买访问失败 → 购买 → 游玩 → 重复游玩不扣费」。
- 边界:并发购买、余额不足、下架/封禁后播放会话失效、猜测 URL 直连、免费游戏回归。
## 验收证据摘要(真实栈 E2E)
- 验收命令:`npm run check:game-distribution-purchase-e2e`(等价于 `node scripts/check-game-distribution-purchase-e2e.mjs`,脚本入口见 `package.json:92`)。
- 断言总数:该脚本内共 76 处 `check(...)` 调用;`check(name, ok, detail)`(脚本 `:86`)每条打印一行 `PASS` / `FAIL`,因此 76 即本轮断言总数。
- 最近一次人工运行结论(2026-10-05,本机真实 SpacetimeDB / api-server / OSS):**76 PASS / 0 FAIL / 1 WARN**(0 FAIL,进程退出码 0)。本节只记可复核的文本结论,不记耗时秒数。
- 唯一 WARN:「管理员令牌购买」在现役登录链路下不可达 —— 后台管理员令牌在 `/api/*` 用户路由上先被 `require_bearer_auth` 判为无效登录态,走 401 前置,未进入 403 `GAME_PURCHASE_ADMIN_NOT_ALLOWED` 分支(该分支作为纵深防御保留);管理员免购买由 play-session 管理员分支覆盖(脚本 `:1122`-`:1173`)。
- WARN 语义:脚本的 `warn()`(`:91`)表示环境条件不满足而**跳过**的断言(例如未发现主站 Vite、`spacetime sql` 直查不可用),不是失败。
- 脚本定位:**人工验收脚本,不在 CI 自动门禁内**(需真实 SpacetimeDB + api-server 与 `E2E_ADMIN_USER` / `E2E_ADMIN_PASSWORD` 环境变量),CI 绿不代表该链路已验证。
- 交付状态:本里程碑结论最高到「本机真实栈验证通过,待用户验收,未部署」。
@@ -1,5 +1,16 @@
# 决策记录
## 2026-10-05 播放会话前缀在三处入口清空 Cookie,网关 403 纵深防御不变
- 背景:付费游戏的可玩入口是创建播放会话后拿到的 `/api/game-distribution/play-sessions/<token>/`(sandbox iframe 的 `src`,包内相对资源沿同一前缀解析)。该前缀落在 `/api/*` 上,边缘通用 `/api` location 必须转发 Cookie(`/api/auth/*` 需要 refresh cookie),而 `api-server` 播放网关对带可解析平台 refresh Cookie 的请求返回 403,导致真实浏览器里 iframe 与每个包内资源都 403、付费游戏实际不可玩。
- 决策(边缘):三份 nginx 模板(`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf`)在通用 `/api` location 之前内联 `location ^~ /api/game-distribution/play-sessions/`;代理头、`client_max_body_size 210m`、`limit_conn` / `limit_req`、超时与维护判断与通用 `/api` 保持一致,额外清空 Cookie。`^~` 必填(否则正则 location `~ ^/api(?:/|$)` 优先命中),且只匹配带尾斜杠的前缀。
- 决策(dev):`vite.config.ts` 在 `/api/game-distribution` 之前加同名前缀代理规则,`proxyReq.removeHeader('cookie')`。
- 决策(网关):`server-rs/crates/pingora-gateway` 新增 `RouteDecision::PlaySessionGateway`,与通用 `/api` 同口径(api 上游、api 限流分组、大小上限、维护闸),差别只在经新增的 `route_clears_cookie` 清空 Cookie(发行入口 `ReleaseGateway` 复用同一判定)。**不放宽** `api-server` 的 Cookie 拒绝。
- 决策(边界):前缀只认带尾斜杠的形式,创建会话的 `POST /api/game-distribution/play-sessions` 与 `POST /api/game-distribution/games/{gameId}/play-session` 继续走通用 `/api` 并保留 Cookie。
- 门禁:矩阵新增 `play_sessions_gateway` 用例;`check:nginx-spa-routes` 新增「该前缀 location 存在、清空 Cookie、排在通用 `/api` 之前」断言,`check:pingora-route-parity` 新增「矩阵用例必须声明清 Cookie 且不得复用通用 `/api` location / Rust 播放会话分支必须排在通用 `/api` 之前」断言,`check:pingora-gateway-smoke` 新增真实网关下「该前缀清 Cookie、创建会话端点保留 Cookie」用例。
- 影响面:`deploy/nginx/{genarrative.conf,genarrative-dev-http.conf,README.md}`、`deploy/container/nginx.conf`、`vite.config.ts`、`server-rs/crates/pingora-gateway/src/main.rs`、`deploy/pingora/nginx-route-parity.matrix.json`、`scripts/check-{nginx-spa-routes,pingora-route-parity,pingora-gateway-smoke}.mjs`、`docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
- 关联:`docs/project-memory/shared-memory/pitfalls.md`「付费游戏播放会话前缀落在 `/api/*`」条。
## 2026-10-03 每日免费发放额为 0 时前端隐藏该池
- 背景:运营需要一个可逆的「不提供每日免费泥点」状态。不给它新增 `retired` 状态位或新字段,直接把后台配置 `daily_free_points_per_day` 配成 0,让「每日免费发放额」这个普通数值自己表达;前端据此隐藏每日免费相关入口。
@@ -9,7 +20,6 @@
- 影响范围:`packages/shared/src/utils/mudPoints.ts`、`PlatformMudPointWalletEntry`、`PlatformProfileRechargeModal`(池概览 3 列变 2 列、扣点顺序提示与泥点确认页文案随可见性切换)、`apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx`(每日免费允许填 0)、`server-rs/crates/module-runtime/{commands,errors,lib}.rs`;`daily_free_grant` / `daily_free_reset` 账本文案保留,历史流水不改写。
- 验证:`cargo check -p module-runtime`、`cargo test -p module-runtime profile_wallet_config_allows_zero_daily_free_points`、`cargo fmt --check`、根 `npm run typecheck`、`npm run admin-web:typecheck`、共享层与 admin 定向 vitest 22/22、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-05 创作者主页与关注粉丝的产品边界
- 产品已确认:桌面第四项“创作者主页”默认进入当前账号主页,“我的”移到第五项;他人的关注/粉丝列表公开可查看,自己或他人的两类列表均可点击用户进入其创作者主页。
+70 -1
View File
@@ -2,6 +2,45 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 2026-10-05 PR #607 复核修复:档位点对齐/对比度、状态文案也走浮层、键盘去重、卸载 flush
- **档位圆点已删除(D1 的收口)**:这一轮把档位圆点**整体删除**(半透明备选方案未采用)。现在滑块只剩轨道 + 圆钮:轨道 6px 圆头、已选段 `--platform-accent` 由 `--strength-ratio` 驱动、**终点落在圆钮中心**(`calc(10px + ratio * (100% - 20px))`)、圆钮 20px 实心暖白(`--platform-panel-fill` + `--platform-subpanel-border` 1px 描边 + `color-mix` 柔影);强度区横向内边距 `4px 6px 2px` → `4px 0 2px`(滑块铺满卡片内容宽度,填充段与圆钮两端与轨道两端贴齐);相关 CSS(`space-between` 排布 / `z-index: 2` 抬层 / `.is-active{opacity:0}` / 点的 `color-mix` 底色 / 只为点对齐的 `padding: 0 7px`)与渲染标记一并删除。**判据**:`chatDialogFrameLayout.test.ts` 反向守卫(样式表里不再有 `.project-chat-composer-strength-stops` 规则、组件源码不再渲染该类名;滑块契约仍在:宽 100% / 高 26 / 圆钮 20×20、强度区左右内边距 0)+ `home.suite.ts` 首页菜单里查不到那组点。实测(447 视口,像素扫描):轨道 90..324(宽 234 = 卡片内容宽),档位 0 时圆钮左缘 90.5(距轨道左端 0.5px)、档位 4 时圆钮右缘 322.3(距右端 1.8px,扫描行不在圆钮正中所以略窄),填充段终点落在**圆钮中心**(`calc(10px + ratio * (100% - 20px))`,被圆钮盖住),因此圆钮右侧不会露出橙色(终点曾写成 `20px + …` = 圆钮右缘,4× 设备像素下能看到一小截溢出)。**历史成因(只留一句,细节由 Git 追溯)**:圆点此前被 6px 轨道盖住、且与圆钮两端错位 ±12.2px,曾用「抬到轨道之上 + space-between 对齐 + 浅暖色」修过一轮,最终整体删除。
- **状态文案(`role="status"`)也会顶高那一排(D2)**:`notice`(「模型列表已刷新」「正在刷新模型列表」「默认模型已停用」、推理档读取/保存失败)原先无条件渲染成控件里的 `role="status"`,`toast` 形态只接管了 `error`,于是 Direct 输入区仍会被一行状态文字顶成两行高。现行口径:浮层**收进组件内部**(`components/common/composerNotice.tsx` 的模块级单例 + toast 宿主),失败与状态分两条通道各自去重(同一条只弹一次、清空后可再弹),失败 `tone="error"`、状态 `tone="neutral"`;**任何宿主**的输入区子树里都不再有常驻 `role="alert"` / `role="status"` 行,宿主的 `errorPresentation` / `onUnavailable` / `onNotice` / `noticePresentation` 这套分支已删除。判据:`chat-composer.suite.ts`「routes model-list notices to the toast…」断言 Direct 输入区子树里 `[role='status']` 与 `[role='alert']` **都为 0**,且浮层在输入区之外。
- **键盘长按每个事件都落盘(D5)**:range 的方向键在 Chromium 里每次重复都会补 `change`,旧行为 ~30 次/秒全量写盘(写链 + 持久化 + 回读)。现行口径:`selectEffort` 先比对「最近一次**落盘成功**的档位」(`persistedEffortRef`,挂载回读时就种上),相同则不写;连续调整只写**每个新档位**一次,钳位后的重复事件 0 次。实测(真实按键序列):单击方向键 **1** 次落盘;长按 1.5s(34 个重复事件)**4** 次(`low/medium/high/max`,即每个新档位一次);已在最高档继续长按 1.2s(28 个事件)**0** 次。
- **拖动中途卸载会丢档位(D6)**:预览值只在 `change`/`keyup`/`pointerup`/`blur` 落盘,拖到一半被 Esc 关掉菜单或宿主卸载时不会提交。现行口径:卸载 cleanup 里 flush 一次(读 ref,不碰已卸载组件的 state)。实测:拖动到「中」未松手时落盘 0 次,`unmount()` 后恰好 **1** 次(`medium`);单次拖动(按下 + 6 步横移 + 松手)全程 **1** 次(`high`),终值 = 松手位置。
- **`forced-colors` 下焦点不可见(D4)**:高对比模式会丢弃 thumb 的 `box-shadow`,而焦点环正是画在 thumb 上的阴影。现行口径:加 `@media (forced-colors: active)` 回退 `outline: 2px solid Highlight; outline-offset: 2px`(系统色,不算新色值;只在 forced-colors 生效,亮色观感不变)。
- **注释与实现必须同口径(D9)**:`.resource-reference-input` 的 `min-height: 0 / padding: 0` 原先写在文件末尾那条六类规则里靠**源码顺序**盖住前面的 `min-height: 56px / padding: 0 0 4px`,与「不依赖源码顺序」的注释矛盾。现行口径:几何(内边距 / 高度下界)写回 11520 行那条六类规则,末尾那条只留网格与行距,注释同步改成"不依赖源码先后"。
- **深色主题下焦点环间隙不可靠(D3,仅记录不改代码)**:间隙色取 `--platform-input-fill`,深色主题该 token 只有 5% 不透明(`rgba(255,255,255,0.05)`),间隙会与焦点环颜色几乎一样。当前工作台根节点固定 `platform-theme--light`,走不到这条分支;将来真要启用深色主题,把间隙换成不透明 token 再验一次。
## 2026-10-05 模型菜单的观感是组件自带的共享样式面(首页同步复用)
- **现象**:工作台那颗 `ConversationModelSelect` 已经改成「强度区 + 模型列表」的卡片观感,但首页输入区右下角那颗(`className="home-input-model-select"`,同一个组件)还是**旧卡片**:161px 窄卡、10px 圆角、5px 内边距、32px 行高、旧灰投影。首页与工作台应当是同一套观感。
- **成因**:#600 那轮把卡片 / 列表的新观感全挂在 `.game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex …` 这条**宿主祖先链**上,于是只有工作台那一处命中;同一个组件换个宿主就退回基础规则。
- **处理(现行口径)**:卡片 / 列表 / 行 / hover / 选中 pill / 细滚动条这套观感抬到**组件自带的共享样式面**——选择器只用组件类名(`.conversation-model-select .conversation-model-menu-shell`、`.conversation-model-select .conversation-model-menu`、`… .conversation-model-menu button…`),**不带任何宿主祖先链**;留在宿主作用域的只有工具栏几何(控制排的宽高与收缩、队列 chip、模型触发钮),**弹层锚点也是组件级**(`position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 20`,见下方"后续")。合并形态(有外壳)与独立形态(策划/画布直接渲染菜单)**同一套值**:卡 16px 圆角 / 12px 内边距 / 1px `--platform-line-soft` 描边 / `--platform-desktop-hover-shadow`;行 34px、圆角 9px、hover `--platform-nav-item-icon-fill`、选中 `--platform-warm-bg` + 600;独立形态的卡片自己就是滚动区(`max-height: 194 = 170 + 上下各 12`,与合并形态同高),外壳形态的列表 `max-height: 170`(5×34,不出现半行)。
- **首页强度区(分两步收口)**:先只同步了卡片观感(当时首页那颗没有 effort 的 state / props,`view/home/index.tsx` 只传 `className` 与 `disabled`,直接加强度区等于新增一个首页从未暴露过的设置项);随后按要求把强度区一并对齐,见下方"后续"。
- **判据/取证**:`tests/appSurface/home.suite.ts`「首页模型菜单复用同一套共享样式面」——首页实例的 `.conversation-model-menu` 的祖先链里**没有** `.game-workbench-chat`,而只靠 `.conversation-model-select …` 就能解析出 16px 圆角 / 12px 内边距 / 34px 行,且菜单里确实有原生滑块(`input[type="range"]` 带 `aria-label="思维强度"`);`tests/chatDialogFrameLayout.test.ts` 的外壳用例按新选择器集合更新(锚点与卡片观感都来自共享面,工作台作用域只剩工具栏几何)。真机实测(无头 Chromium,同一夹具同一裁剪框):首页那颗 161×309 / 10px / 5px / 32px → **260×194 / 16px / 12px / 34px**(与工作台卡片逐项一致);工作台那颗外壳 260×262.5、内层列表 234×170、行 34/9px **前后逐值不变**。
- **边界**:策划面与画布也复用同一个组件 → 它们的菜单观感同样变成这张卡片(有意为之;窄面板下卡片最小宽度 260px,仍受 `calc(100vw - 24px)` 与各自锚点约束)。若某个宿主需要另一套尺寸,在**该宿主作用域**里覆盖,不要再把观感抄第二份。
- **后续(同一批收口)**:强度区与外壳锚点也一并抬到共享面——`view/home/index.tsx` 给首页那颗传了**同一个** `ComposerReasoningEffortSelect`(`presentation="section"`,读写仍是组件自己的 `read_game_creator_app_config` / `select_game_creator_reasoning_effort`),首页因此与工作台逐项同形:外壳 260×262.5、强度区 234×60.5、列表 234×170、滑块 222×26(447 与 1200 两档实测一致;工作台回归后逐值不变)。抬升时必须删掉工作台作用域里两条**死重**规则(给旧独立「推理档」控件排版的 `display: inline-flex` 一族),否则它们会以更高特异性盖掉共享面的 `display: grid`,把强度区重新算宽 24px。
- **滑块两点(最终收口)**:档位圆点先是改成派生浅暖色、直径收到 6px(= 轨道高度),随后**整体删除**(见上面 D1 条)。「进度条高度与滑块高度一致」保留两种解释对照:**变体 A 落地**(填充段 = 轨道高度 6px),**变体 B**(填充段做成 20px = 圆钮高度)只在夹具里渲染出图、未提交。
## 2026-10-05 给 `shared-contracts` 请求 DTO 加字段:`#[serde(default)]` 救不了 Rust 结构体字面量,AGC 壳编译失败会伪装成 `npm run dev:all` 起不来
- **现象**:`npm run dev:all` 的根栈五个服务与 AGC Vite 都就绪、终端也会打印启动汇总,但 AGC 窗口始终不出现;stdout 里夹着 `error[E0063]: missing field `price_mud_points`in initializer of`GameDistributionCreateVersionRequest``(`src\game_distribution_publish.rs:1356`)和 `error: could not compile `genarrative-ai-game-creator-shell` (bin ...) due to 1 previous error`。编译失败不会让 `tauri dev` 退出,dev:all 只会一直挂着(`waitForAgcFrontend` 的 660s 预算耗尽后才收束整棵树),容易被误判成端口占用或根栈没起来。
- **根因**:变更在 `server-rs/crates/shared-contracts/src/game_distribution.rs` 给请求 DTO 末尾追加了 `#[serde(default)] pub price_mud_points: u64`。`serde(default)` 只影响**反序列化缺字段**,Rust **结构体字面量仍必须列全字段**。AGC 壳 `apps/ai-game-creator-shell/src-tauri` 是**独立 cargo workspace**(自带 `[workspace]`,只把 `shared-contracts` 当 path 依赖),`server-rs` 侧的 `cargo test -p ...` 六个 crate 全绿也覆盖不到它。
- **处理(现行口径)**:改 `shared-contracts` 里**请求 DTO 字段**时,必须同一次编译 AGC 壳:先 `npm run agc:bundled-resources:prepare`(`build.rs` 只做只读校验,未准备资源会以「随包插件存在未声明文件」失败关闭),再 `cargo check --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute`(约 30s,feature 与 dev/release 对齐);完整口径是 `npm run ai-game-creator-shell:check:rust`。定价字段的取值口径分两段:**历史口径**(2026-10-05 之前)AGC 发布面板没有买断制定价入口,构造版本请求时显式按免费提交 `price_mud_points: 0`,与升级前行为一致;**现行口径** AGC 发布面板已支持「免费 / 买断制」定价,请求按作者选择从载荷透传价格,`#[serde(default)]` 仅用于向后兼容不传该字段的旧客户端(按免费处理)。
- **边界**:`cargo check` **不能省 feature 参数**——默认 feature 下 `build.rs` 会因为已 staging 的 godot/unity 插件 DLL「不在当前 feature 组合的声明清单里」而红,那是资源准备前置条件,不是本次代码的编译错误。门禁本身**已经存在**:CI `.gitea/workflows/project-ci.yml` 的 `AI game creator shell Rust lane 1/2`、`lane 2/2` 各顺序跑两片 `npm run check:native-shells:agc-rust-shard-N`(底层 `ai-game-creator-shell:check:rust:shell` → `cargo test --locked` 编出壳 bin),本地同序列是 `npm run check:native-shells`。漏拦的原因是本地验证只跑了 `server-rs` 侧 crate、没等 MR/CI 上的 AGC lane,不是缺门禁;因为 2466 条 bin 单测跑满很贵,**不要**把整条 lane 塞进常用本地命令,只在改动 shared-contracts 请求 DTO 时补上面的 `cargo check`。
- **判据/取证**:E0063 原文(`logs/dev-all-feature.log`);修复后 `tauri dev` watcher 重建输出 `Finished `dev` profile [unoptimized + debuginfo] target(s) in 38.16s` 并拉起 `target\debug\genarrative-ai-game-creator-shell.exe`;同一 feature 组合 `cargo check` 退出码 0;`.app/dev-stack.json` 记录的实际端口下 api-server `/healthz`、主站 `/`、admin-web `/admin/`、AGC marker `/__agc_dev_server.json` 均 200。
- **关联**:`server-rs/crates/shared-contracts/src/game_distribution.rs`、`apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs`、`.gitea/workflows/project-ci.yml`、`scripts/check-native-shells.mjs`。
## 2026-10-05 付费游戏播放会话前缀落在 `/api/*`:边缘转发 Cookie 会让 iframe 与包内每个资源都 403
- **现象**:付费游戏在真实浏览器里打不开——播放会话 `src`(`/api/game-distribution/play-sessions/<token>/`)本身和包内每个相对资源(JS/CSS/图片/音频)全是 403,同一份包的免费游戏 `/games/<gameId>/` 正常。
- **原因**:播放会话前缀落在 `/api/*` 上,而通用 `/api` location 必须转发 Cookie(`/api/auth/*` 依赖 refresh cookie);`api-server` 播放网关对带**可解析平台 refresh Cookie** 的请求返回 403(与发行网关同族的纵深防御,本次不放宽),于是沙箱 iframe 的每个同前缀请求都带 Cookie、都被拒。dev 侧同样复现:`vite.config.ts` 原本只对 `/games/...` 清 Cookie,`/api/game-distribution` 规则会转发 Cookie。
- **处理(现行口径)**:三份 nginx 模板(`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf`)在通用 `/api` location **之前**加 `location ^~ /api/game-distribution/play-sessions/`——`^~` 不能省,否则正则 location `~ ^/api(?:/|$)` 优先命中、Cookie 又被转发;代理头 / `client_max_body_size 210m` / `limit_conn` / `limit_req` / 超时 / 维护判断都与通用 `/api` 一致,只多一条 `proxy_set_header Cookie ""`。`vite.config.ts` 在 `/api/game-distribution` 之前加同名前缀规则(`proxyReq.removeHeader('cookie')`)。Pingora 侧对应 `RouteDecision::PlaySessionGateway`:路径原样走 api 上游,同样套 api 限流分组、大小上限与维护闸,差别只在新增的 `route_clears_cookie` 清空 Cookie(发行入口 `ReleaseGateway` 复用同一判定)。
- **边界**:前缀**只匹配带尾斜杠**的形式——创建会话的 `POST /api/game-distribution/play-sessions`(以及 `POST /api/game-distribution/games/{gameId}/play-session`)需要账号凭证,必须继续走通用 `/api` 并保留 Cookie。网关的 403 拒绝保持不变,只在边缘/dev 保证请求不带 Cookie。
- **门禁**:`npm run check:nginx-spa-routes` 对三份模板断言该 `^~` location 存在、块内清空 Cookie、代理头齐全且排在通用 `/api` location 之前(变异验证:删掉块内 `proxy_set_header Cookie "";` 立刻报「播放会话前缀 location 缺少代理片段」);`npm run check:pingora-route-parity` 断言矩阵 `play_sessions_gateway` 用例声明清 Cookie 片段、不复用通用 `/api` location,且 Rust `classify_path` 的播放会话分支排在通用 `/api` 之前(变异验证:把矩阵片段换成通用 location、或在 Rust 里交换两个分支,各自单独判红);`npm run check:pingora-gateway-smoke` 用真实网关二进制断言该前缀清 Cookie、创建会话端点保留 Cookie。三条都串在 `npm run lint` 链里,有自动调用方。
- **关联**:`server-rs/crates/pingora-gateway/src/main.rs`、`deploy/pingora/nginx-route-parity.matrix.json`、`deploy/nginx/README.md`、`vite.config.ts`;另见本文件「主站 SPA allowlist 有三处真相源」条的「别踩」(发行入口不转发 Cookie 的同族规则)。
## 2026-10-05 自定义作者插槽应保留昵称降级语义
- 游戏公开投影里的「创作者」「未知作者」是角色占位词。作者昵称 hook 已将加载中和查询失败分别转成 null 与空串,宿主不能再用 `|| game.author.name` 把占位词补回。
@@ -44,7 +83,7 @@
- **现象 / 风险**:改动 `preview.rs` 的注入逻辑(`inject_preview_fit_bridge` / `build_preview_response` / `PREVIEW_FIT_BRIDGE_TAG`)时,没有自动化门禁会告诉你「标签没注入」或「注入了两次」。两种失效都只在运行时才暴露:没注入等于整套运行画面点选静默失效(桥脚本的尺寸上报与点选都不执行,页面看起来完全正常);注入两次会让桥的监听、尺寸上报与点选回调各注册一遍,页面同样看不出差别。
- **现状**:`preview.rs` 的 `mod tests` 只有 4 个用例——`npm_preview_requires_build_and_prefers_bundled_assets`、`root_layout_serves_root_entry_and_keeps_legacy_paths_available`、`root_layout_does_not_expose_control_or_data_directories`、`legacy_layout_serves_root_ui_modules`;它们只断言预览路由的选取、状态行与页面自身文本,不涉及桥标签是否出现、出现几次,也不覆盖 `scan_preview_fit_bridge_html` 的「页面已带标签就不重复注入」分支(`inject_preview_fit_bridge` 里的 `if scan.has_bridge_script { return html.into_bytes(); }`)。
- **结论(现行口径)**:这类「把常量原样返回 / 标签字符串存在」的转发型行为不固化成长用例(仓库口径:复制与源码文本断言不进测试)。改注入逻辑时按人工验证清单核对:① 响应 HTML 里 ``<script src="/__genarrative/local-preview-fit.js"></script>`` 出现在 `</body>` 前,且是经典脚本(不带 `type` / `nomodule`);② 页面自身已带该标签时,响应里的标签数量不增加;③ `/__genarrative/local-preview-fit.js` 返回 200,且内容与 `resources/preview/local-preview-fit.js` 逐字节一致(`include_str!` 内嵌,可比 sha256)。
- **结论(现行口径)**:这类「把常量原样返回 / 标签字符串存在」的转发型行为不固化成长用例(仓库口径:复制与源码文本断言不进测试)。改注入逻辑时按人工验证清单核对:① 响应 HTML 里 `<script src="/__genarrative/local-preview-fit.js"></script>` 出现在 `</body>` 前,且是经典脚本(不带 `type` / `nomodule`);② 页面自身已带该标签时,响应里的标签数量不增加;③ `/__genarrative/local-preview-fit.js` 返回 200,且内容与 `resources/preview/local-preview-fit.js` 逐字节一致(`include_str!` 内嵌,可比 sha256)。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/preview.rs`(`PREVIEW_FIT_BRIDGE_SCRIPT` / `PREVIEW_FIT_BRIDGE_TAG` / `inject_preview_fit_bridge`)、`apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js`。
## 2026-10-04 Phaser 4 `hitTestPointer` 的返回顺序不是叠放顺序
@@ -71,6 +110,27 @@
- **判据/取证**:Rust `cargo test … asset_generation_task` 27 条(含 `naming_task_stays_in_flight_across_ledger_reads_until_terminal`、`naming_task_update_requires_the_project_rename_permission`、`naming_task_update_rejects_a_record_from_another_project`、`naming_task_terminal_state_rejects_a_different_status_but_allows_the_same_one`、`ledger_with_an_unsupported_schema_version_fails_closed`、`ledger_without_a_schema_version_is_accepted_as_v1`)与 `conditional_project_rename_tests` 7 条(含超长名失败关闭);前端 `homeProjectNamingAsync.test.tsx`(入队→running→终态的推进序列)、`projectNamingGenerationTaskRow.test.tsx`(行渲染)、appSurface 的「syncs the workbench title and recent project list with the AI name after creation」「recovers the naming row when the terminal event lands before the subscription is ready」「re-reads the naming ledger when the generation tasks panel opens」(后两条已做「改前红」验证:退回「先读后订阅 / 面板打开不补读」两条即红)。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rs`、`apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`、`apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`。
## 2026-10-04 输入区不放常驻状态行:那一排控件容不下第二行文字
- **现象**:AGC 工作台输入区里模型失败提示常驻一行红字,工具条上还浮着一个半透明的「模型 ⌄」压在 `@ + 默认 ⌄` 那一排上;口径是「完全没对齐」,改成 toast。
- **成因**:`ConversationModelSelect` 的 `[role='alert']` 曾被从 `bottom: 36px` 浮层收回控件排内(`flex: 1 1 100%` 独占一行),整颗模型控件因此变成两行高;工具条是 `align-items: center` 的 flex 行、行高由最高项决定,于是触发钮被顶到行线上方、提示落到行线下方——**不是"某个元素绝对定位压过来",是行内多了一行文字**。
- **处理(现行口径)**:输入区**不留常驻状态行**,与输入区同一排的失败提示一律走浮层。`ConversationModelSelect` 新增 `errorPresentation: 'inline' | 'toast'`(默认 `inline`,首页 / 策划 / 画布原样不动),Direct 输入盒传 `'toast'`,由 `ComposerModelNoticeToast` 复用外壳同一枚 `PlatformRuntimeStatusToast`(bottom-center portal、6s 自动收起);触发时机是失败消息**新出现**时一次(同条去重,清空后可再报),不做每次渲染都弹;不可用态照旧挡住发送,但不靠文字行表达。文案同步去掉误解性的「本地」前缀(`llmModelCatalog.ts` / `clientApi.ts` → 「模型服务不可用」)。
- **判据/取证**:`tests/appSurface/chat-composer.suite.ts`——不可用时产出 toast,且输入区子树里既没有 `[role='alert']` 也没有旧文案;模型失败只报一次、清空后再出现才再报。
- **关联**:`features/project-workspace/ConversationModelSelect.tsx`、`view/project-development/chat/components/DirectProjectComposer/{DirectProjectComposer.tsx,ComposerControls.tsx}`、`src/styles.css`。
## 2026-10-04 输入盒的强度选择并入模型菜单,底行只留一颗控件
- 目标口径:**减少底部空间占用** —— 底行只能有模型触发钮一颗控件。思维强度不再单独占位,而是作为**强度区嵌在模型菜单顶部**(标题「思维强度」+「当前:最高」+ 原生 `<input type="range" min=0 max=4 step=1>` + 5 个档位点),与模型列表共用同一张卡(`.conversation-model-menu-shell`,向上展开、贴触发钮右缘,里层 `.conversation-model-menu` 回到文档流只负责滚动)。
- **滑块不能放进 `role="listbox"` 子树**:参数控件混进菜单角色会让读屏进菜单模式后读不到它,所以外壳是普通容器(强度区 + listbox 并列),只能靠 `strengthSection` 这个显式 prop 由宿主注入。
- 档位枚举、默认值、持久化通道(读 `read_game_creator_app_config`、写 `select_game_creator_reasoning_effort`)与旧下拉**完全一致**;`ComposerReasoningEffortSelect` 的 `presentation` 默认 `'dropdown'`(策划输入盒继续用,零语义变化),Direct 用 `'section'`;`ConversationModelSelect` 不传 `strengthSection` 时渲染与改动前逐字一致(首页 / 策划 / 画布不受影响)。
- 关掉旧的独立触发钮(底行元素 2 → 1);浮层的开合、Esc 与点击外部关闭都由模型菜单既有行为负责,强度区不再自带浮层。
- **取舍**:强度区随菜单挂载,打开时先显示「默认」再回读成落盘档位(一次 IPC 往返),测试里要 `waitFor`;换掉这一点需要把档位状态提到宿主,暂不做。
- **判据/取证**:`tests/appSurface/chat-composer.suite.ts`(底行不再有第二颗控件、菜单里改档落同一条持久化通道、Esc/点外部关闭、滑块不在 listbox 内)+ `tests/chatDialogFrameLayout.test.ts`(外壳向上展开 / 强度区网格 / 底行高度不增)+ `tests/appSurface/design-agent.suite.ts`(策划菜单里没有滑块)。
- **视觉口径**:卡片与滑块的观感照 Codex 参考图,颜色只取我们自己的暖色 token——卡片圆角 16px、内边距 12px、极淡描边(`--platform-line-soft`)+ 既有暖色投影 token `--platform-desktop-hover-shadow`、**不画横贯分隔线**(只靠留白);强度区标题 `--platform-text-strong` 12/600、右侧当前档位 `--platform-text-soft` 11px;轨道 6px 圆头,已选段 `--platform-accent`、未选段 `--platform-subpanel-border`(进度由组件写进来的 `--strength-ratio` 驱动渐变 `calc(20px + var(--strength-ratio) * (100% - 20px))`),圆钮 20px **实心**(`--platform-panel-fill`,不用半透明 token,避免透出已选段变成"靶心")+ `--platform-subpanel-border` 1px 描边 + 从 `--platform-accent` 派生的 `color-mix` 柔影;档位点 7px 绝对定位叠在轨道**之上**(`z-index: 2` > range 的 1)、当前档位那一点隐藏(见本文件顶部 2026-10-05 那条的对齐算式与实测);模型列表行高 34px、行圆角 9px、hover `--platform-nav-item-icon-fill`、选中 `--platform-warm-bg` 轻量 pill + 右侧对勾;列表 `max-height: 170px`(=5×34,不出现半行),滚动条 6px 且平时透明、悬停卡片才显形。
- **实测(无头 Chromium 夹具,447px / 900px)**:卡片 260×263(圆角 16 / 内边距 12)、强度区 234×61、滑块 222×26、档位点 7px 且中心与轨道中线重合(点 270 / 轨道 269)、列表行高 34、`max-height` 170(内容 315 → 滚动)、默认档进度 0%,按 `End` 到 100% 且文案变「当前:最高」;卡片右缘未溢出视口。
- **关联**:`features/project-workspace/ConversationModelSelect.tsx`、`view/project-development/chat/components/DirectProjectComposer/{DirectProjectComposer.tsx,ComposerControls.tsx}`、`src/styles.css`。
## 2026-10-03 后台异步回填被「进项目」覆盖:AI 项目名只在兜底名仍成立时改
- **现象**(Issue 599):首页建项把 AI 命名改成「与建项并行、结果后台回填」后,偶尔工作台标题停在兜底名(`GameAgent 项目 <短 id>`),而磁盘 manifest 已经是 AI 名字。
@@ -79,6 +139,15 @@
- **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/homeProjectNamingAsync.test.tsx` 的「keeps the AI name when the naming result lands before the project is entered」(去掉等待即复现兜底名覆盖);appSurface 的「syncs the workbench title and recent project list with the AI name after creation」。
- **边界**:条件改名的判据必须由宿主校验(项目 ID + 当前名称仍是兜底名),前端只转述 `expectedProjectId` / `expectedName`;用户已手动改名时宿主返回 `renamed: false`,回填整体跳过、绝不覆盖用户输入。
- **关联**:`apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`、`apps/ai-game-creator-shell/src-tauri/src/commands.rs`。
## 2026-10-03 AGC 输入区控件叠在一起:输入区内的操作排与状态提示不能用绝对定位
- **现象**:#600 截图里用户输入区域「元素堆叠」——空草稿时「AI 润色」按钮就压在输入内容区右下角(实测与编辑器相交 28×24px),模型控件的失败提示「本地模型服务不可用」直接画在第 3~4 行文字上(70×36px,长草稿时把整段文字盖住);面板越窄越糟:280px 面板下推理档整颗压在「添加文件」上(28×28px),「插入素材引用」与推理档也相交(20×28px)。
- **原因**:两条「靠绝对定位把控件挪出流」的旧写法叠在一起:① `.resource-reference-input-actions` 被写成 `position: absolute; right: 0; bottom: 0`,浮到编辑器右下角(同一份样式早前其实是网格第二行,见文件里 11893 行那条注释,被后写的绝对定位规则盖掉);② `ConversationModelSelect` 的 `[role='alert'] / [role='status']` 是 `bottom: 36px` 的浮层,锚点就在输入盒正下方,提示必然落在文字上。另外 `.project-chat-composer-controls-right` 写死 `flex: 0 0 auto`,宽度不够时既不收缩也不换行,整组向右溢出输入盒(360px 面板下溢出 34px、280px 下 114px)。
- **处理(现行口径)**:`.project-chat-composer.is-direct-codex` 是单列网格(`gap: 8px`);**控件全部集中在唯一一行工具条**——左组 `@` / `+`,右组 模型 / 语音 / **「AI 润色 / 恢复原文」(紧贴发送左侧)** / 发送,六颗 `flex-wrap: nowrap` 平铺(润色钮由 `ResourceReferenceInput` 的 `actionsPortalTarget` portal 进右组发送钮之前的挂点,不再自成一行;它与发送钮同为圆角矩形按钮,相邻成组,DOM / Tab 顺序都是 模型 → 麦克风 → 润色 → 发送);状态/提示层只在有内容时占行(空输入态第二行为 0 高、行间距为 0,状态自己带 6px 上间距)。窄宽度用 `min-width` 兜(控件排 252px / 外壳 278px)+ 横向溢出可见:**不换行、不堆叠、不重叠**,发送钮仍可点击(夹具 `elementFromPoint` 命中)。模型钮封顶 160px 且名字走省略号(收缩由它承担),左组是固定 28px 动作钮、`flex: 0 0 auto` 不参与收缩。**模型失败提示后来改成走浮层**(见本文件顶部 2026-10-04 / 2026-10-05 两条):控件排里既不留常驻红字、也不留常驻状态文字——「控件排内独占一行」那次快照已退役,行高回到由模型触发钮决定。
- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住单行工具条(240 / 280 / 296 / 320 / 360 / 438 / 900 七个宽度下 `flex-wrap` 都是 `nowrap`、控件排 `min-width: 252px`、外壳 `min-width: 278px`)、输入盒最高高度算式(12 盒内上内边距 + 160 编辑器 + 0 输入区行距 + 0 状态行 + 0 输入区下内边距 + 8 输入盒与工具栏行距 + **30 工具栏行高(模型触发钮 30px 撑起来,发送/语音是 28px)** + 12 盒内下内边距 = **222**),以及组件源码里润色钮渲染在工具条挂点(`actionsPortalTarget={controlsActionsSlot}`、挂点在控件排之后);`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 随包 plugins 的 feature 档位必须与消费方一致,且门禁会因 build.rs 未重跑而假通过
- **现象**:Windows 本机 `npm run check:generated-bindings`(`npm run lint` 链内,`scripts/check-repository-ci.sh` 的 Repository checks 也走它)在 `build.rs:167:29` panic:`插件随包资源校验失败:随包插件存在未声明文件:.../src-tauri/resources/plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll(目标 x86_64-pc-windows-msvc 与当前 feature 组合不允许;请先执行随包资源准备步骤)`;树上换成 `agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe` 时报同一类错。反向还有更隐蔽的形态:门禁 2 秒就 exit 0 说「通过」,但 tree 上其实带着编辑器产物。