合并 origin/master(51cb05f41,22 个提交)到 feat/game-purchase

- 逐块合并 server-rs/crates/api-server/src/modules/game_distribution.rs:以 master 的 ReleaseAssetResponseInput / 5 参 release_asset_response_with_cache / ETag / 304 / gzip 结构为准,并在其上保留本分支的付费 404 守卫(price_mud_points > 0 仍在 release_package_bytes 读包之前返回);播放会话资源响应改用同一结构(etag None、cache_control no-store、沿用 accept-encoding 协商)。
- 合并 src/components/game-distribution/GamePlayPage.tsx:接入 master 的共享 PlatformGameLoadingSurface 与 PLATFORM_GAME_LOADING_TIMEOUT_MS,删除本地重复常量 GAME_PLAY_STARTUP_TIMEOUT_MS 与裸 div,买断制播放会话准备态改由共享加载面承载。
- 合并 src/components/game-distribution/GameDetailPage.tsx:同时保留 master 的 onOpenCreator 作者插槽与本分支的买断制购买弹窗、onOpenRecharge 充值入口。
- 合并 vite.config.ts:保留 master 的 /api/creators 代理,并保留 play-sessions 前缀清 Cookie 规则(顺序仍在通用 /api/game-distribution 之前)。
- 合并 deploy/nginx/README.md:保留 master 的 SPA allowlist 门禁口径(含 /creators、/creators/connections)与本分支的播放会话前缀章节;三份 nginx 模板的 ^~ play-sessions location 与清 Cookie 原样保留。
- 合并 apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx:保留 master 的加载面/超时用例与本分支的审核价格(冻结价优先、历史按 0)用例。
- 合并 src/components/game-distribution/GameDistributionPages.test.tsx:保留双方 mock,并新增「付费作品在会话签发期间显示共享加载面」用例。
- 合并 docs/【玩法创作】平台入口与玩法链路-2026-05-15.md(保留买断制合同并回填 master 的创作者主页与关注粉丝合同)、decision-log.md、pitfalls.md:追加双方条目,不改写任一侧正文。
- 保留 master 侧新增能力:release_asset_etag / if_none_match_matches / accepts_gzip_encoding / gzip_release_asset / release_asset_not_modified_response、SPA 加载面、创作者主页与关注粉丝(user_follow 表、creator 查询与 author_id 过滤)。
This commit is contained in:
2026-10-06 00:28:36 +08:00
132 changed files with 15843 additions and 5874 deletions
@@ -0,0 +1,77 @@
# 【实施计划】AGC 运行画面点选识别 Phaser 4 与 three.js 画面上的已登记素材
| 字段 | 值 |
| --------- | ----------------------------------------------------------------------- |
| Milestone | `docs/project-memory/plans/【里程碑】AGC运行画面点选引擎适配-2026-10-04.md` |
| Status | in-progress |
| Owner | omp agent(issue #612) |
## 修改边界
允许修改:
- `apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js`(新增):现有预览桥脚本(尺寸上报 + DOM 点选)整体迁入,并新增 canvas 分支与两个引擎适配器。
- `apps/ai-game-creator-shell/src-tauri/src/preview.rs`:`PREVIEW_FIT_BRIDGE_SCRIPT` 改为 `include_str!` 引用上面的文件;注入、去重扫描、`/__genarrative/local-preview-fit.js` 路由与响应逻辑不变。
- `apps/ai-game-creator-shell/src/features/project-workspace/runtimeInspectResourceMatch.ts`(新增):把宿主侧「点选结果 → manifest 素材 id」的匹配抽成纯函数。
- `apps/ai-game-creator-shell/src/view/project-development/index.tsx`:`handleRuntimeInspectSelection` 改为调用该纯函数,派发载荷结构不变。
- `apps/ai-game-creator-shell/tests/runtimeInspectEngines.test.ts`、`apps/ai-game-creator-shell/tests/runtimeInspectResourceMatch.test.ts`(新增)。
- `apps/ai-game-creator-shell/src-tauri/src/main.rs`:`DEFAULT_GAME_SCRIPT_JS` 补引擎句柄发布。
- `apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/game.js`、`.../threejs-3d-starter/project/game/main.js`:补引擎句柄发布(只改本地模板源,不重新发布 zip)。
- 文档收口:`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`(「当前已完成」清单与本里程碑证据表)、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` §3.4(如行为与草案有出入)。
明确不修改:
- `runtime-region` 引用契约、`genarrative.local-preview-inspect.v1` 消息版本号、`LocalGamePreviewFrame.tsx` 的解析与净化、Rust `direct_codex_user_item` 的校验口径(不新增字段;若实测必须加字段,单独评审)。
- `packages/shared`、manifest / 版本绑定 / 素材替换链路、Runtime Prompt 文案、模板 zip 发布与线上索引(AGC 玩法 Skill 的生成期契约属阶段二,见下 `## 阶段二进展`)。
- DOM 点选语义与 `enabled / disabled / cancelled` 协议。
## 实现顺序
1. **搬运桥脚本(行为零变化)**:把 `PREVIEW_FIT_BRIDGE_SCRIPT` 的字符串体原样落到 `resources/preview/local-preview-fit.js`,`preview.rs` 改 `include_str!`。跑 Rust 定向测试 + 一次真机 DOM 点选,确认尺寸上报与 DOM 点选完全不变。
2. **拆出命中解析**:桥内把 `handleInspectMove` / `handleInspectClick` 共同的部分抽成 `resolveInspectTarget(clientX, clientY)`,返回 `{ selection, rect }`;DOM 分支逻辑等价搬运。
3. **引擎句柄契约**:桥惰性读取 `window.__GENARRATIVE_PREVIEW_GAME__ = { engine: 'phaser', game }` 或 `{ engine: 'three', THREE, scene, camera, renderer }`(`THREE` 命名空间要求见下「风险」第 2 条);只认显式句柄,不做鸭子类型猜引擎。缺句柄或 `engine` 未知 → 走第 6 步退化。
4. **Phaser 4 适配**:`game.input.activePointer` 取指针 → `scene.input.hitTestPointer(pointer)` 优先;无命中对当前激活场景的 `scene.children.list` 逆序(含 `depth`)用 `getBounds()` 与指针世界坐标求交兜底。素材身份:`getData('genarrativeResourceId' | 'genarrativeResourcePath')` 优先,其次 `texture.getSourceImage()` / `texture.source[].image` 的 `currentSrc || src` 与 `texture.key` + `frame.name`。矩形:`getBounds()` 世界矩形按相机换算屏幕矩形。多场景只取激活场景;全程 try/catch,任何一步抛错按"未命中"处理。
5. **three.js 适配**:`renderer.domElement` 矩形归一化出 NDC → `new THREE.Raycaster()` + `setFromCamera` → `intersectObjects(scene.children, true)` 取最近命中。素材身份:`object.userData.genarrativeResourceId | genarrativeResourcePath` 优先,其次沿 `object.material` 的贴图槽取 `texture.image.currentSrc || texture.source.data.currentSrc`。矩形:命中对象 `Box3` 八角投影取 min/max。
6. **退化路径**:以上任一步拿不到结果时,按现状输出(`label` = 元素/画面可得的名字,`sourcePath` / `resourceIds` 为空,矩形 = 现状 DOM 语义),不报错、不伪造 id。
7. **宿主匹配抽纯函数**:`runtimeInspectResourceMatch.ts` 实现优先级——显式 `resourceIds`(已按 manifest 过滤)→ `sourcePath` 与 `localPath` 的精确后缀匹配 → 文件名匹配(现有口径);序列帧 `imageSequenceFrames[].imageSrc` 参与后两档;结果去重且只保留 manifest 现有素材 id。`index.tsx` 改为调用它。
8. **脚手架与模板句柄**:`main.rs` 默认脚本与两个模板源补发布句柄(Phaser 在 `new Phaser.Game(...)` 处持有实例;three.js 在场景装配后发布)。
9. **真机与文档收口**:两条 smoke(见下),补「当前已完成」清单与证据表。
## 验证命令
1. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- preview`
2. `npx vitest run apps/ai-game-creator-shell/tests/runtimeInspectEngines.test.ts apps/ai-game-creator-shell/tests/runtimeInspectResourceMatch.test.ts apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx`
3. `npm run agc:typecheck`(含 `check:tests:types`、`tsc`、skill-pack 指纹、包布局声明、`check-config`)
4. `npm run check:encoding`、`npm run check:doc-index`、`git diff --check`
5. 真机:`npm run agc` 拉起客户端 → 新建 Phaser 4 项目(默认脚手架)与 three.js 项目各点一次已登记素材,核对引用芯片名与载荷 `resourceIds`,并各点一次空白与 DOM HUD 确认退化与不回归。
## 风险与回滚点
1. **桥脚本迁移**(顺序 1):纯搬运,但桥同时承载尺寸上报(`resolveLocalGamePreviewFitLayout` 依赖它),任何字符差异都可能表现为画面适配回归。回滚点:该步单独一次提交,红了就 revert 该提交再重做。
2. **three.js 需要 `THREE` 命名空间**:Raycaster / Vector2 / Box3 都是模块内类,桥拿不到;从 `camera.constructor` 只能拿到相机类,因此句柄契约要求同时暴露 `THREE`。若实测希望避免暴露命名空间,退路是要求玩法暴露少量构造器(`Raycaster`)——需要改契约,先评审再动手。
3. **Phaser 4 相机换算**:世界↔屏幕矩形依赖 4.2.1 的相机字段与缩放行为;实测不稳定时降级为"命中对象但只给点标记矩形",不阻塞引用插入(主规范第 5 条的退化分支)。
4. **`hitTestPointer` 只覆盖 `setInteractive()` 对象**:几何兜底必须存在,否则未开交互的精灵点不中。
5. **three.js 递归 raycast 成本**:只在点选模式下按需执行(move 节流),不做常驻监听。
6. **three.js 真机验收的模板来源**:本里程碑只改本地模板源,不重新发布 `template.zip`;three.js smoke 用模板工程 + 手工补一行句柄进行(句柄就是对外契约),正式"开箱可用"由下一个里程碑的模板发布覆盖。这条限制必须写进交付记录。
7. **净化为不可绕过**:引擎档取到的 id / 路径先过既有 `sanitizeInspectResourceId` / `sanitizeInspectSourcePath` 再进载荷;宿主匹配只保留 manifest 现有 id,未知值一律丢弃(Rust 拒绝口径不变)。
## 已完成
- 桥脚本搬运 + 引擎档落地:桥脚本落到 `resources/preview/local-preview-fit.js`,`preview.rs` 改 `include_str!`,DOM 点选语义与协议不变,新增 canvas 引擎档。
- 宿主匹配抽成纯函数 `runtimeInspectResourceMatch.ts`,`index.tsx` 改为调用它,派发载荷结构不变。
- 脚手架与三个起步模板发布只读引擎句柄:`DEFAULT_GAME_SCRIPT_JS`、`phaser-2d-starter`、`threejs-3d-starter`、`blank-3d-scene`。
- 自动化验证通过:五个前端用例文件(`runtimeInspectEngines` / `runtimeInspectResourceMatch` / `localGamePreviewFrame` / `resourceReferenceInput` / `appSurface`)292 passed / 9 skipped(301 项);`npm run agc:typecheck` 通过;`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- preview` 通过(43 passed)。
- 分四个提交落盘:6c6850352(桥与引擎档)、6264fed6b(宿主匹配)、ab518ea57(脚手架与模板)、f729203c6(规范与踩坑);验收证据见里程碑文档。
## 阶段二进展
阶段一让点选能识别素材,但可用性依赖玩法满足三条约定(发布句柄、Phaser 用 Image 装载、对象标注素材身份);阶段二把这几条写进**生成期契约**,新生成的 Phaser 4 / three.js 项目不改代码即可点出素材。同一 PR 续推,`template.zip` 与线上索引仍未重新发布。
- 主源(唯一写全契约处):`resources/agc-skills/agc-web-game-development/SKILL.md` 新增「运行画面点选契约(运行态素材身份)」四条——发布只读引擎句柄(`{ engine:'phaser', game }` / `{ engine:'three', three: { Raycaster, Vector2, Vector3, Box3 }, scene, camera, renderer }`,three 瘦身形态见下条)、Phaser 固定 `loader: { imageLoadType: 'HTMLImageElement' }`、给素材渲染对象标注 manifest 身份(Phaser `setData('genarrativeResourceId' | 'genarrativeResourcePath', …)`,three.js `mesh.userData.…`,值只取 `agc_list_registered_assets` 的 `localAssetId` / `localPath`)、素材进 dist 走与 `localPath` 对齐的稳定路径(`public/` 原样拷贝,不用 `import` 的 hash 产物名);并写明不满足时不报错、不伪造 id、退化为不带素材的区域引用(= 验收面)。
- 派生副本只补指针,不复制契约:`agc-web-game-development/references/game-quality-checklist.md` 增加一条勾选项;`agc-game-production-workflow/SKILL.md` 第 4 步(Game implementation)指向该契约(新游戏编排必读)。
- 指纹同步:`npm run agc:skill-pack:sync` 更新 `resources/agc-skills/manifest.json` 的 `version` 与两个 skill 的 `sha256`;`skill_pack.rs` 逐文件 `include_bytes!`,因此**未新增 skill 文件、未改任何 Rust**。
- 验证:`npm run agc:typecheck`(含 skill-pack 指纹校验)、`node scripts/check-doc-index.mjs`、`npm run check:encoding`、`git diff --check` 全部通过(退出码与结论行见 PR 评论)。
- 冻结面不变:`preview.rs`、`resources/preview/local-preview-fit.js`(sha256 仍为 `6276ac48…917a`)、宿主匹配、脚手架与模板源均未改;未合并主线。
- 可见性补强 + 句柄瘦身:桥端 three 句柄改推荐瘦身形态 `three: { Raycaster, Vector2, Vector3, Box3 }`(旧 `THREE` 命名空间仍兼容读取,缺任一构造器按未命中退化),并补 label 兜底——three `object.name` → `geometry.type` → `object.type`、Phaser `texture.key` → `name` → `type`,命中对象但取不到素材身份时芯片不再一律是 `@canvas`;契约文本、`threejs-3d-starter` 与 `blank-3d-scene` 模板同步瘦身(整命名空间实测把 517,682 B 的构建撑到 730,831 B,+41%)。
- 该轮验证:`tests/runtimeInspectEngines.test.ts` 33 例(新增 6 例:瘦身句柄、旧命名空间兼容、name / geometry.type / object.type 兜底、缺构造器退化);真实 Chromium + 真实 three 0.184 + 真实桥脚本实测 slim 句柄命中、`Crate` / `SphereGeometry` / `LegacyMesh` 三种芯片名与对象区域矩形;`agc:typecheck`、`check-doc-index`、`check:encoding`、`git diff --check` 通过。
- 真机复验(待执行):环境已就绪——客户端从本分支源码编译启动(`npm run agc` → `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs`,Vite `127.0.0.1:3080`)、exe 内已核实内嵌新桥(6 处新代码标记:`handle.three || handle.THREE`、`inspectObjectLabel` 及其 4 个调用点)、CDP `http://127.0.0.1:9222` 可用(`Edg/154.0.4258.53`,page target 为 `陶泥儿 @ 127.0.0.1:3080`)。**点选复验待执行**:客户端登录态已过期(localStorage 里唯一 access token `exp=2026-10-01T08:24:51Z`),UI 停在登录页,需人工登录后才能打开 `gameagent-73ab2832` 进「运行」页挑选;未取得芯片文本 / 高亮矩形 / iframe 内句柄,不写"已完成"。
@@ -0,0 +1,61 @@
# 【里程碑】AGC 运行画面点选识别 Phaser 4 与 three.js 画面上的已登记素材
| 字段 | 值 |
| ----------- | -------------------------------------------------------- |
| Version | 1.0 |
| Status | in-progress |
| Date | 2026-10-04 |
| Parent Spec | `docs/【功能说明】AGC聊天素材引用-2026-09-08.md`「运行画面素材点选」、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` §3.4 |
## 目标
- 运行画面点选在 Phaser 4 与 three.js 玩法上,能识别点中对象用的是哪一张已登记素材,并让引用载荷带上该素材的稳定 id。
- 识别不出来时不伪造、不报错,按主规范退化为不带素材的区域引用。
- HTML 区域点选语义、预览消息协议、引用发送链路与 Rust 校验口径逐字不变。
- 新建 Phaser 4 项目与 three.js 起步工程开箱即具备点选所需的引擎句柄。
## 范围
- 运行画面点选的引擎档识别:命中对象解析、素材身份来源与优先级、命中矩形高亮。
- 宿主侧把识别结果映射成 manifest 素材 id(含"精确优先于文件名"的口径)。
- 起步工程与新建项目默认脚手架的引擎句柄发布。
- 上述行为的自动化证据与真机 smoke 证据。
## 不在范围内
- 生成期契约落位:把句柄发布与对象素材标注写进 AGC 玩法 Skill(**同一 PR 的阶段二**,见实施计划 `## 阶段二进展`);重新发布模板产物(`template.zip` 与线上索引)仍不在本次范围内。
- 运行期资源使用关系持久化(不记录"某素材在某位置被用过",不注入代码、不写侧车)。
- Cocos Creator / Godot / Unity 的点选;纯 2D canvas 引擎与 Babylon.js。
- `runtime-region` 引用契约、预览消息版本号、Rust `resourceIds` 校验、资源选中与信息栏判据的任何变化。
- 旧项目自动获得能力(不写迁移、不做兼容分支;限制如实写进能力说明)。
## 依赖与前置条件
- 主规范(`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`)与本节里程碑评审通过。
- 可拉起的 AGC 真机客户端(`npm run agc`,端口与栈规约按 `genarrative-dev-stack-port-routing`),以及一份可复现的 Phaser 4 / three.js 起步工程。
- 一张已登记到 manifest 的项目素材(图片;three.js 侧另需一张贴图)。
- 不依赖后端服务、Provider 或登录态。
## 验收标准
- [x] Phaser 4 起步工程:点中由已登记素材渲染的精灵 / 图集帧,引用芯片显示该素材名,载荷 `resourceIds` 命中该素材 id。(浏览器实测:`loader.imageLoadType='HTMLImageElement'` 下自动档 `@hero` + `["hero"]`;默认 XHR 装载贴图为 blob,须由玩法显式标注,见主规范边界)
- [x] three.js 起步工程:点中贴了已登记贴图的 Mesh(含 GLB 内嵌纹理的模型走显式标注路径),结果同上。(浏览器实测:贴图 Mesh `@hero` + `["hero"]`;`userData.genarrativeResourcePath` 档 `@enemy` + `["enemy-tag"]`)
- [x] 同一对象同时存在显式素材标注与贴图地址推断时,取显式标注。(`tests/runtimeInspectEngines.test.ts` 33 例;浏览器重叠位取最上层且显式 id 胜出 `["enemy-tag"]`)
- [x] 无引擎句柄 / 点中空白 / 程序化绘制三种情况:退化为不带素材的区域引用,零报错、零伪造 id,引用可插入可发送。(`runtimeInspectEngines.test.ts` 33 例;浏览器点空白 `@canvas` + `[]` 零报错)
- [x] 命中对象时高亮为该对象在画面上的区域;取不到对象矩形时高亮范围与 DOM 档一致。(矩形换算改用 `camera.matrixCombined`;浏览器实测与期望逐值相减 Phaser 差 0px、three 最大差 ≈0.0006px,DOM 档为元素矩形)
- [x] DOM 点选(HTML HUD、媒体 `src`、资源标识属性)行为与现有用例逐字不变。(`localGamePreviewFrame` 14、`resourceReferenceInput` 43、`appSurface` 197 passed / 9 skipped;浏览器实测 HUD 芯片名 `@HUD 区域` 未被素材名改写)
- [x] 起步工程与新建项目默认脚手架发布引擎句柄;新建 Phaser 4 项目不额外改代码即可点选。(`DEFAULT_GAME_SCRIPT_JS` 与三个起步模板源已发布句柄;`cargo test -- scaffold` 4 passed / 1 既有 ignore。开箱即用未跑真机链路)
## 证据要求
- 自动化:引擎档识别的 JS 单元用例(jsdom 中加载真实桥脚本 + 假引擎对象)、宿主素材映射纯函数用例、桥注入与协议回归用例、`npm run agc:typecheck`。
- 运行时:Phaser 4 与 three.js 各一条真机点选 smoke(截图或录制,含引用芯片与载荷证据)。
- 边界:三种退化情况各一条、净化与数量上限不回归、`resourceIds` 只取 manifest 现有素材。
## 验收证据(2026-10-04)
- 命令:`npm run agc:typecheck` 通过(`check:tests:types` / `tsc` / skill-pack 指纹 / 包布局声明 / `check-config` 全过)。
- 前端用例:`runtimeInspectEngines.test.ts`(33)+ `runtimeInspectResourceMatch.test.ts`(11)+ `localGamePreviewFrame.test.ts`(14)+ `resourceReferenceInput.test.tsx`(43)= 101 passed;`appSurface.test.ts` 197 passed / 9 skipped。
- Rust:`cargo test … -- preview` 43 passed / 0 failed;`cargo test … -- scaffold` 4 passed / 1 既有 `#[ignore]`(需 npm 装依赖 + Provider)。
- 真实浏览器冒烟(真实 Phaser 4.2.1 / three 0.184 + 真实桥脚本 `local-preview-fit.js`,被测 sha256 `6276ac48…917a`):已登记贴图对象得 `["hero"]`、显式标注对象得 `["enemy-tag"]`、非交互精灵经几何兜底命中;点空白 `@canvas` + `[]` 零报错;DOM 档芯片名不被改写;高亮矩形与期望逐值相减,Phaser 差 0px、three 最大差 ≈0.0006px。
- 未覆盖:Tauri 真机链路(预览服务器注入与路由由 Rust 定向测试覆盖)、多场景叠加、非默认相机(scroll/zoom/rotation)、高亮真机目视。
@@ -49,3 +49,10 @@
- 自动化:admin-web 详情/动作测试、api-server 预览会话和权限测试、DTO/类型检查、编码和 diff 检查。
- 运行时:本地管理员打开待审版本,确认资料、封面/截图和待审包试玩;确认旧公开版本不会被误播。
- 边界:过期 Token、错误 versionId、Cookie、越权管理员、缺入口、资源 404、CAS 冲突和幂等重放。
## 本轮核对(2026-10-05,试玩加载体验与超时收口)
- 「试玩当前待审版本」原先只渲染 `<iframe class="admin-game-review-preview">`:加载期是一块 `min-height: 42rem` 的空白,没有加载文案、没有超时、没有失败提示(会话过期或资源 404 会永远停在空白)。2026-10-05 起与网页游玩页共用共享加载面 `PlatformGameLoadingSurface`(`packages/shared`):加载期显示游戏标题/封面/动效进度与阶段文案,iframe `onLoad` 后才让位给画面。
- 加载上限与网页游玩页统一为 `PLATFORM_GAME_LOADING_TIMEOUT_MS`(20 秒):超时后加载面换成「试玩版本加载超时」并给出「重新创建试玩会话」入口;新建会话会重置加载态,不会沿用上一个会话的 ready。
- 用例:`apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx` 新增两条(加载面在画面载入后让位、超过上限后给出超时说明与重建入口,重建后新会话 URL 生效且回到加载态)。
- 仍未变化:预览会话 token 与 `no-store`、`sandbox="allow-scripts"`、Cookie 拒绝、版本绑定与审核 CAS 口径都不动。
@@ -13,7 +13,7 @@
## 范围
- 作者端定价:网页 `/games/publish` 与 AGC 发布面板在新建与更新模式选择“免费 / 买断制 N 泥点”;两入口共用 `packages/shared` 定价组件,AGC 壳请求透传 `priceMudPoints`;给已上线作品发新版本时预填当前价格与模式,作者不改则价格不变。
- 作者端定价:网页 `/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` 流水。
@@ -35,7 +35,9 @@
## 验收标准
- [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 泥点」,非法值被前后端同时拦截;给已上线作品发新版本时预填当前价格与模式,作者不改则价格不变。证据:待补(由实现 worker 提供:AGC 发布面板定价定向 Vitest、AGC 壳 `cargo check` 编译、真实本地栈 E2E 发布付费版本);两入口共用 `packages/shared` 组件 `PlatformGamePricingField`。
- [ ] 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。
@@ -56,4 +58,12 @@
- 运行时:真实本地栈跑通「发布付费游戏 → 后台审核可见价格并试玩 → 通过 → 未购买访问失败 → 购买 → 游玩 → 重复游玩不扣费」。
- 边界:并发购买、余额不足、下架/封禁后播放会话失效、猜测 URL 直连、免费游戏回归。
已执行(2026-10-05,人工验收):`npm run check:game-distribution-purchase-e2e` 在本机真实 SpacetimeDB / api-server / OSS 一次跑通 76 PASS / 0 FAIL / 1 WARN,exit 0,24.1s,teardown 无残留。唯一 WARN 为「管理员令牌购买」在当前产品形态不可达(管理员免购买经 play-session 管理员分支实现,购买路由的 403 `GAME_PURCHASE_ADMIN_NOT_ALLOWED` 分支作为纵深防御保留)。该脚本不在 CI 自动门禁内,CI 绿不代表该链路已验证;本里程碑结论最高到“本机真实栈验证通过,待用户验收,未部署”。
## 验收证据摘要(真实栈 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 绿不代表该链路已验证。
- 交付状态:本里程碑结论最高到「本机真实栈验证通过,待用户验收,未部署」。
@@ -333,3 +333,9 @@
- **详情页返回按来源回到上一页**:`GameDetailPage` 左上角原来是写死的「返回游戏广场」,并且直接 `setSelectionStage('games')`(等于 push 一条新的 `/games`)。从「我的游戏」点进详情再返回就落到广场,从广场进详情返回也会多压一条重复历史。现在文案简化为「返回」,行为改成优先 `window.history.back()`,由 `ActiveApp` 已有的 `popstate` 同步把舞台还原成真正的来源页(我的游戏 / 广场);游玩页的返回同样处理,避免详情↔游玩互相 push。协议侧给应用写入的历史条目补了 `__genarrativeAppHistoryDepth`,新增 `hasAppHistoryBackEntry()` 判断「当前条目是应用内导航写入的且存在上一页」;直接打开详情深链、或原生壳里没有可回退条目时,才 `replaceAppHistoryPath` 兜底到广场(游玩页兜底到自己的详情)。回归用例 `activeAppPageRoutes.test.ts` 锁定深度标记语义,`PlatformEntryActiveFlowShell.test.tsx`「游戏详情返回」两条锁定原生返回与深链兜底。
- **游戏分发的返回文案统一成「返回」**:`GameDetailPage`、`GamePlayPage`(含超时面板里的按钮)、`MyGamesPage`、`GamePublishPage` 四处原本是「返回游戏广场 / 返回详情」,同一套流程里出现三种写法;现在统一成「返回」,目标页仍由各自的 `onBack`(广场 / 我的游戏 / 详情)决定,按钮不再承诺一个可能不对的目的地。`GameDistributionPages.test.tsx` 的断言同步改为 `/^返回$/u`。游玩页工具栏上那个按钮展示的是游戏名(退出开始面板后就靠它认游戏),不属于「返回 xx」文案,保持不动。
- 验证:`npx vitest run src/components/game-distribution/GameDistributionPages.test.tsx`(21 passed)、`npx vitest run src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx`(22 passed)、`npx vitest run src/routing/activeAppPageRoutes.test.ts`、`npm run typecheck`、`npx eslint`(五个改动文件)、`npm run check:encoding`、`git diff --check` 全部通过;启动面板的溢出可达性、「我的游戏」移动端横向溢出都用真实 Chromium 静态夹具量过(见 pitfalls 同日条目),详情返回链在真实 Chromium(dev 栈 390x844)实测 `/games`→详情→返回=`/games`、深链直开详情→返回=`/games`、详情→立即玩→返回详情=`/games/detail?id=…`。视觉与真机横竖屏走查仍需人工/截图评审(沿用阶段 C 第 8 条未取证口径)。
## 本轮核对(2026-10-05,游玩页加载体验与发行资源传输)
- 游玩页此前在 `onLoad` 前只盖一行 `游戏正在启动…`(近黑壳上),用户反馈「一直黑屏」。现在加载期显示共享加载面 `PlatformGameLoadingSurface`(`packages/shared`,封面/标题 + 动效进度 + 按时长推进的阶段文案),`onLoad` 后淡入画面;超时面板与重试/返回不变,上限仍与后台试玩统一为 `PLATFORM_GAME_LOADING_TIMEOUT_MS`(20 秒)。
- 发行网关(`api-server`)对文本类发行资源下发 `Content-Encoding: gzip` + `Vary: Accept-Encoding`,并下发强 `ETag`:`If-None-Match` 命中返回 304。`max-age=60, must-revalidate` 与撤销窗口不变,差别只是 60 秒之后重复游玩不再重下整包。后台试玩会话仍是 `no-store` 且无 ETag。
- 证据:真实栈 4 Mbps/100 ms 模拟链路对照(改前 3298 ms / 4587 ms 才出画面,且期间无有效加载反馈),发行网关字节数对照见 PR;`cargo test -p api-server --bin api-server -- game_distribution` 与三条前端用例(共享加载面、游玩页、后台试玩)为回归门禁。
@@ -11,6 +11,16 @@
- 影响面:`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-05 创作者主页与关注粉丝的产品边界
- 产品已确认:桌面第四项“创作者主页”默认进入当前账号主页,“我的”移到第五项;他人的关注/粉丝列表公开可查看,自己或他人的两类列表均可点击用户进入其创作者主页。
- 关注为单向关系,取消回关与移除粉丝分别影响不同方向;只有本人可移除自己的粉丝,自己不能关注自己。
- 他人的关注、粉丝列表统一只读:保留头像/昵称进入用户主页,不显示任何关系操作按钮;前后端均不额外检测访问者与列表用户的关注关系,已有缓存也不用于显示关系动作。
- 已确认:移动端入口为“游戏 / 创作者主页 / 我的”;自己的主页也只展示公开游戏;自己的关注列表取消后当前行暂留以便重新关注;移除粉丝二次确认,取消关注不弹确认。
- 用户明确本次不额外改造游戏目录分页:既有游戏广场和“我的游戏”保持现状,作者主页按作者过滤后沿用最多 48 项限制。新关注/粉丝列表的分页仍按主规范设计。
- 关系纯规则放在 `module-auth::creator`,现有认证服务通过 `services` feature 隔离宿主依赖,数据库 WASM 只使用纯规则。私有 `user_follow` 不参与认证快照替换;只通过受信服务过程读写,HTTP 操作者取认证身份。
- 行为真相见[创作者主页与关注粉丝合同](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#创作者主页与关注粉丝合同),工程落点见[工程设计](../../technical/【技术方案】创作者主页与关注粉丝工程设计-2026-10-05.md)。后端已完成隔离验证并由用户验收通过;页面工程验证通过,用户已要求提交并推送;证据归并工程设计,已完成临时计划删除,未上线。
## 2026-10-03 首页自动建项与 AI 项目命名解耦(Issue 599)
- 背景:首页「开启创作」原先串行执行「Web 预检 → `await suggest_automatic_project_name` → `create_automatic_local_game_project`」。项目名称不是创建工作区、导入附件或发起首轮创作的前置条件,命名请求(`AUTOMATIC_PROJECT_NAME_TIMEOUT_MS = 15s`,正常请求同样占时)却把用户按在「正在创建工作区」上。
+72 -2
View File
@@ -20,6 +20,63 @@
- **门禁**:`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` 把占位词补回。
- 替换共享详情组件的作者插槽时,同时接管了默认作者行的空名降级;只展示解析后的昵称,空名仍保留有可访问名称的主页入口,关注操作按作者 ID 和关系状态控制。回归覆盖补查等待、成功、空名和失败。
## 2026-10-05 页面返回兜底不能新增历史条目
- 普通链接整页打开的条目可能没有应用导航标记;此时返回若调用 `pushAppHistoryPath`,再点返回就会退回原深链,形成循环。无应用内历史的返回应使用 `replaceAppHistoryPath`,有历史时保留原生后退。
- 创作者主页顶部资料不自链接;详情和关系列表中的用户链接保留。具体兜底目标见创作者专题合同;验证须覆盖真实浏览器连续返回及列表分页/滚动恢复,不能只检查 `history.back()` 被调用。
## 2026-10-05 运行画面点选的高亮框被尺寸上报当成页面内容:点选后预览自己缩放
- **现象**:three 项目里点选 3D 对象后预览画面自己缩放、视角异常(相机 aspect 与重新适配的距离都变了),点选结束后视口仍停在放大后的尺寸。实测夹具里是**自激振荡**:10 秒内 1459 条尺寸消息、1457 次 iframe 尺寸/缩放变更、1454 次 `resize`,相机 aspect 在 1.607143 ↔ 1.421112 之间来回变。
- **原因**:高亮框是 `document.body` 下的 `div[data-genarrative-preview-inspect]`(`position: fixed`、尺寸随命中对象),而尺寸上报的 `measureContentBounds` 用 `createTreeWalker(body, SHOW_ELEMENT)` 遍历所有元素并把 `rect.right/bottom` 计入内容尺寸。three 档的高亮矩形来自 `Box3` 八角投影,物体贴近相机时投影盒远超画布(实测 6607×4721、left/top 为负)→ 上报内容被撑到 3756×2643(视口只有 900×560)→ 宿主 `resolveLocalGamePreviewFitLayout` 把 iframe 改成 3756×2643 + scale 0.2119 → 游戏 `resize` 重算相机 → 视口变大又让投影盒更大,如此循环。
- **结论(现行口径)**:① 桥自己的节点一律不进内容尺寸测量——`measureContentBounds` 的 TreeWalker 用 `acceptNode` 对 `data-genarrative-preview-fit` / `data-genarrative-preview-inspect` 返回 `FILTER_REJECT`;以后新增任何桥注入的 DOM 节点都要带上这两个标记之一,否则会重新引入这条反馈。② 引擎档命中矩形统一裁剪到画布可见范围(`clipInspectRect`;与画布无交集时退化为指针点矩形),不再出现比画面还大的高亮框。
- **回归**:`apps/ai-game-creator-shell/tests/localPreviewInspectSizeStability.test.ts`(5 例:桥节点不参与测量、同尺寸普通节点仍计入的对照组、进入检查模式与 hover 后尺寸与宿主适配布局不变、投影盒超出画布时载荷被裁剪、与画布无交集时退化为 1×1)。
- **关联**:`resources/preview/local-preview-fit.js`(`measureContentBounds` / `clipInspectRect` / `threeInspectTarget` / `phaserSelection`)、`features/project-workspace/LocalGamePreviewFrame.tsx`(`resolveLocalGamePreviewFitLayout`)。
## 2026-10-05 把整个 `THREE` 命名空间塞进预览句柄会让 tree-shaking 失效
- **现象**:three 项目的运行画面点选句柄写成 `{ engine: 'three', THREE, scene, camera, renderer }` 时,同一个 Vite 构建的产物从 517,682 B 涨到 730,831 B(+213,149 B ≈ +41%)。
- **原因**:句柄引用整个 `import * as THREE` 命名空间,打包器无法证明未使用的导出可以剪掉;预览桥实际只用 `Raycaster` / `Vector2` / `Vector3` / `Box3` 四个构造器。
- **结论(现行口径)**:句柄用瘦身形态 `three: { Raycaster, Vector2, Vector3, Box3 }`;该字段只给预览桥用,玩法代码不要引用它。旧形态仍被兼容读取(两种形态都有用例锁定),但新代码与模板一律用瘦身形态。
## 2026-10-05 运行画面点选 three 档的句柄少了构造器会整体失效
- **现象**:three 项目句柄里 `Raycaster` / `Vector2` / `Vector3` / `Box3` 只缺一个,桥就**不进引擎档**:芯片回落 `@canvas`、控制台不报错——症状与「根本没发句柄」完全一样,容易误判成「点选没生效」。
- **原因**:引擎档入口先认句柄形态、再逐个校验这四个构造器是否为 function,缺任一即按未命中退化(不降级、不猜);射线需要 `Raycaster` + `Vector2`,对象矩形需要 `Box3` + `Vector3`,两组能力缺一方都点不出对象。
- **排查顺序(现行口径)**:① 预览页有没有 `window.__GENARRATIVE_PREVIEW_GAME__`;② `engine` 是不是 `phaser` / `three`;③ 这四个构造器齐不齐(瘦身形态是 `three.Raycaster` 等,旧形态是 `THREE.Raycaster`);④ `scene` / `camera` / `renderer`,three 侧还要 `renderer.domElement` 带 `getBoundingClientRect`。
- **关联**:`apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js`(`threeInspectTarget`)、`agc-web-game-development` 的「运行画面点选契约」、`tests/runtimeInspectEngines.test.ts`(瘦身句柄命中、缺构造器退化两例)。
## 2026-10-05 预览桥脚本是编译期内嵌的:改完必须重启 AGC 客户端
- **现象**:改了桥脚本(运行画面点选 / 尺寸上报逻辑),Vite HMR 与刷新预览页都不换——预览页仍跑旧桥,新加的判定与兜底完全不生效。
- **原因**:`apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js` 由 `preview.rs` 用 `include_str!` **编译期**编进客户端 exe,预览服务器 `/__genarrative/local-preview-fit.js` 返回的就是 exe 里那份常量,重读磁盘不会发生。
- **结论(现行口径)**:改桥后必须重启 AGC 客户端才生效——先确认没有在跑的 AGC Vite(3080 等端口空闲)再 `npm run agc`;只刷新页面、只重启后端或只重装 npm 依赖都无效。核实内嵌版本:在 exe 二进制里搜新代码标记,或比对 `resources/preview/local-preview-fit.js` 的 sha256。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/preview.rs`(`PREVIEW_FIT_BRIDGE_SCRIPT`)、`apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js`、`genarrative-dev-stack-port-routing`(端口探测与 `npm run agc` 口径)。
## 2026-10-05 预览桥的注入与去重在 Rust 侧没有用例覆盖
- **现象 / 风险**:改动 `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)。
- **关联**:`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` 的返回顺序不是叠放顺序
- **现象**:运行画面点选 Phaser 4 画面时,按「数组第一个 / 最后一个」当最上层会点错对象——点的是上层精灵,引用却落到下层的素材上;对象越多越容易错。
- **原因**:`hitTestPointer` 返回的是输入对象的注册 / 内部列表顺序,与真实叠放无关;叠放由命中相机 `renderList` 的索引决定(含 depth 与入序)。
- **结论(现行口径)**:点选命中要按 `pointer.camera.renderList` 的索引取最上层;几何兜底也必须按 depth + 入序排序,不能沿用注册顺序取值。
## 2026-10-04 Phaser 4 默认 XHR 装载下贴图元素是 blob 地址
- **现象**:运行画面点选拿不到原始素材路径——贴图元素上的地址是会话内的 blob 地址,按文件名反推素材会得到无意义的临时名。
- **原因**:Phaser 4 默认用 XHR 装载图片,地址只在当前会话内有效,不携带仓库 / manifest 里的路径信息。
- **结论(现行口径)**:Phaser 4 项目的素材身份必须靠玩法显式标注(`setData`)发布,不要试图从 blob 地址反推文件名;没有标注就退化为不带素材的区域引用。与主规范「运行画面素材点选(2026-10-04)」的边界一致。
## 2026-10-04 把非素材任务塞进「生成任务」账本:taskType 维度、v1 兼容与 live 集合
- **现象**:首页「AI 项目命名」改成进「生成任务」列表后,第一次读账本(工作台打开项目就会读)就把那条还在跑的命名任务标成**失败**;随后前端的终态推进被拒(记录已终态)。
@@ -117,16 +174,19 @@
- **原因**:Windows 的 `npm.cmd` 是批处理入口;Node `child_process.spawn('npm.cmd', args, { shell: false })` 会直接返回 `EINVAL`,还未执行根 `npm run dev`。
- **处理**:`scripts/dev-all.mjs` 在 Windows 使用 `shell: true`、`windowsHide: true` 启动 npm 子进程;POSIX 仍使用独立进程组,退出时按进程组收束。
- **验证**:Windows 实测根开发栈已启动并完成端口漂移(Web `3001`、API `8084`、worker `8085`、SpacetimeDB `3104`、后台 `3105`),之后 AGC 因当前工作区缺少 `@anthropic-ai/claude-agent-sdk` 退出;dev:all 已收束根栈进程。
## 2026-10-02 AGC 页面在自绘标题栏外壳里自己算 `100vh`:底部被裁而且没得滚
- **现象**:帮助页(使用指南 / 联系客服 / 更新日志)在矮窗口里底部卡片看不到,把窗口拉高才出现;外壳 `.launcher-main { overflow: hidden }` 之下没有任何可滚动祖先,页面既滚不动也裁得干净。首页在通知横幅出现时用 `h-[calc(100vh-32px)]`,同样把窗口高度当成了舞台高度。
- **原因**:AGC 桌面外壳是自绘标题栏(`--window-chrome-height`;窗口 100vh=800 时舞台只有 750),页面根节点写 `100vh` / `100dvh` / `calc(100vh - Npx)` 就比真实舞台高一整个标题栏,差额被外壳裁掉;横幅是 `.launcher-main` 里的真实行,再写 `-32px` 等于重复扣一次。帮助页还没有内层滚动容器,连「内容超高就在内部滚动」这条兜底也不存在。
- **处理(现行口径)**:页面高度只由外壳分配——`apps/ai-game-creator-shell/src/styles.css` 里 `.launcher-main:has(<页面钩子>)` 是纵向 flex 列(`height: 100dvh`,窗口外壳命中 `height: 100%` 时贴合真实舞台),`.launcher-main > <页面根节点>` 统一 `flex: 1 1 auto; height: auto; min-height: 0`,帮助页这类没有内层滚动容器的再加 `overflow-y: auto`。页面根节点一律不再写 `100vh` / `100dvh` / `calc(100vh - Npx)`;有横幅就靠 flex 自动少一份,不要手算偏移。
- **验证**:真机判据是 Vite + Chromium 量页面根节点是否正好等于 `.window-chrome__content` 的高度(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅),帮助页应可滚动到底。
## 2026-10-02 固定试玩误判祖先的指针穿透样式
- `pointer-events:none` 不会强制禁用整棵子树;后代显式 `auto` 可以恢复命中。控件探针只检查目标的计算样式,继承未覆盖的 `none` 仍拒绝;可见性、遮挡、disabled 与 inert 保留各自检查。
- 修复和回归必须经过生产输入入口及可信事件驱动的状态变化,不能用程序化点击证明真实可玩。双视口 generic 回归与真实触摸验收需要区分,详见 AGC 实施计划“固定试玩控件的指针命中边界”。
## 2026-10-01 Rust 分片编译失败只剩汇总错误
- **原因**:`--message-format=json` 把编译诊断写到 stdout;只读取 `compiler-artifact` 的运行器会丢弃 `compiler-message`,CI 只能看到「due to 1 previous error」。
@@ -6303,6 +6363,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **现象**:模型目录把回合路由到 `cc`,本地 `game-creator.config.json` 的 `llm.apiKey` 为空时,Claude Agent SDK 返回失败终态;界面只显示“执行通道未能建立或已断开”。
- **根因**:Claude sidecar 只从 `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` 或本地 `llm.apiKey` 读取认证,没有复用已登录的 AGC 平台会话;同时失败终态解析丢弃了上游错误摘要。
- **处理**:官方模型且未启用自定义目录时,将当前平台会话令牌仅注入 sidecar 子进程环境;保留最多 512 字符的 Claude 终态错误摘要,继续由统一诊断层脱敏,避免凭据落盘。
## 2026-10-01 AGC 首页把 Web 预检错误与 Tauri IPC 错误合并,造成无法诊断的生成阻拦
- **现象**:用户在首页点击「做游戏」后看到「Web 游戏环境预检未通过,请检查 Node/npm 或浏览器」,但同一安装包的 `--environment-check` 可能已经返回 `status=ready`;首页仍会阻止自动命名、建项和首次生成。
@@ -6406,11 +6467,10 @@ 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` 分片里红。
- **原因 1(通知计数串台)**:测试计数器 `DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT` 在 *2026-10-01 已按线程作用域隔离*(`thread_local! Cell`),但 2026-10-02 退役 `runtime_driver` 把这段接缝搬进 `agent/direct_events.rs` 时**降级回进程级 `static AtomicU64`**。`--test-threads=1` 只串行测试线程,宿主 `tauri::async_runtime` 的后台回合仍在自己的工作线程上广播「运行中的项目」变了,于是断言取到别的回合的广播。
- **原因 1(通知计数串台)**:测试计数器 `DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT` 在 _2026-10-01 已按线程作用域隔离_(`thread_local! Cell`),但 2026-10-02 退役 `runtime_driver` 把这段接缝搬进 `agent/direct_events.rs` 时**降级回进程级 `static AtomicU64`**。`--test-threads=1` 只串行测试线程,宿主 `tauri::async_runtime` 的后台回合仍在自己的工作线程上广播「运行中的项目」变了,于是断言取到别的回合的广播。
- **原因 2(terminate 竞速)**:测试命令里 leader 打印 READY 后立刻 `exit 0`,同组后代仍存活,trampoline 从 leader 被回收那一刻开始 `PROCESS_SESSION_TARGET_TERMINATE_GRACE_MS=800ms` 宽限;客户端只要在 leader 退出后 >800ms 才发出 terminate(CI 高负载下要跨 durable record 写盘、registry 注册、线程 spawn),会话已按 `exited` 收口,terminate 只能读到既成事实——不是产品缺陷,是测试赌了客户端调度。
- **处理(现行口径)**:①测试专用的通知计数必须留在测试线程作用域(`thread_local! Cell`),不要用进程级 Atomic;②graceful terminate 用例的 leader 打印 READY 后要用 `wait` 等后台子进程,让 terminate 必然落在会话仍 running 时(断言、trap、`sleep 0.4`、marker 名字都不改)。
- **验证**:①修复前把计数器临时改回 Atomic 时同一并行口径 42/50 红;修复后并行 50 次 0 红、`--test-threads=1` 200 次 0 红、CI 现场等价块(145 用例)3 次 0 红;②该用例是 `#[cfg(target_os = "linux")]`,Windows 本机跑不到,用真实 Linux 内核(WSL Alpine)验证命令形状:leader 活到 TERM、同组后代完成 400ms 延迟清理(marker=done,real 0.41s)、清理后组内零残留;CI 侧仍应跑 `node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4 --shard-index=4` 复核。
@@ -6424,3 +6484,13 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **别踩**:不要写成裸前缀正则(`^/pay`)——它会吞掉 `/payment/x`、`/paycheckout/x` 这类同名邻居;也不要把深链塞进精确 allowlist 的 alternatives 里(`pay` 的 alternatives 只匹配 `/pay`)。
- **判据/取证**:`node --test scripts/check-nginx-spa-routes.test.mjs`(正/反用例,含「写回精确匹配即红」)、`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix`;线上复验 `curl -s -o /dev/null -w '%{http_code}' https://<平台域名>/pay/<checkoutToken>` → 200 且正文与 `/` 同一份 `index.html`。
- **关联**:`scripts/check-nginx-spa-routes.mjs`、`deploy/pingora/nginx-route-parity.matrix.json`、`server-rs/crates/pingora-gateway/src/main.rs`、`server-rs/crates/api-server/src/payment.rs`、`deploy/nginx/genarrative.conf`。
## 2026-10-05 在线游玩「一直黑屏」:加载面缺失 + 发行网关不压不发 ETag
- **现象**:用户反馈「进入游玩…加载有点慢,一直黑屏体验不好」。真实栈(真实发行包 + Chromium + 4 Mbps/100 ms 模拟链路)实测:网页游玩页点击「开始游戏」后 99.3% 像素亮度 < 24 的近黑面板 + 一行 `游戏正在启动…`,游戏画面 **3298 ms** 才出现;后台审核页点「试玩当前待审版本」后 iframe 直接以 `opacity:1` 出现、区域**全白空白** 4587 ms,页面**全程没有任何加载文案**。
- **根因**:① 网页游玩页的加载态只有一行小字盖在 `#17131b` 近黑壳上,没有封面/进度/阶段文案,`ready` 判定又只看 iframe `onLoad`(文档加载完成 ≠ 游戏可玩);② 后台试玩只有 `<iframe class="admin-game-review-preview">`,没有加载态、没有超时、没有失败处理,会话过期或资源 404 会永远停在空白;③ 发行网关把 ZIP 解压后的原文直出,`phaser.min.js` 1,375,976 B 原样下发(`Content-Encoding: none`),也没有 ETag —— 4 Mbps 下光这一个文件就 ~2.7 s,且 60 秒 `max-age` 过后浏览器只能重下整包。
- **处理(现行口径)**:新增共享组件 `PlatformGameLoadingSurface`(`packages/shared`,封面/标题 + 动效进度 + 按时长推进的阶段文案 + 慢加载提示),网页游玩页与后台试玩共用;两端的加载上限统一为 `PLATFORM_GAME_LOADING_TIMEOUT_MS`(20 秒),后台超时后给出「重新创建试玩会话」。发行网关对文本类资源(HTML/JS/CSS/JSON/SVG/WASM,≥1 KiB、客户端接受 gzip)下发 `Content-Encoding: gzip` + `Vary: Accept-Encoding`,并下发按 `versionId + 资源路径` 摘要的强 ETag,命中 `If-None-Match` 返回 304(无正文)。
- **边界**:图片/音频/视频等已是压缩格式的资源不压;后台试玩会话仍是 `no-store` 且不给 ETag;`max-age=60, must-revalidate` 与撤销窗口不变(换版/下架仍最迟 60 秒对新请求生效,304 只是在窗口之后省掉重下)。边缘 gzip/Brotli 与本层不冲突:两边都以「响应已带 `Content-Encoding` 就跳过」收口;但 Pingora 网关在自身压缩关闭时会**移除** `accept-encoding`(`normalize_accept_encoding_for_gateway_compression`),那种部署形态下源站压缩不会生效,属于网关侧口径。
- **CPU 边界**:发行网关是公开无鉴权端点,压缩按请求实时算。release 构建实测 level 6 为 1.3 MiB→10 ms、8 MiB→59 ms、64 MiB(单文件上限)→522 ms 纯 CPU,因此在 `RELEASE_COMPRESSION_FAST_ABOVE_BYTES`(2 MiB)以上改用 level 1(zlib 端实测 level 1 约为 level 6 的 1/3 耗时、压缩比只差约 3%)。若后续要再做减法,优先把压缩结果按 `(对象键, 资源路径)` 缓存,而不是放宽级别。
- **验证**:`cargo test -p api-server --bin api-server -- game_distribution`(新增 ETag 作用域、`If-None-Match` 列表/弱校验命中、`gzip;q=0` 拒绝、文本压缩与二进制/小文件不压、304 无正文、`*` 对包内缺失路径仍 404 等用例);`npx vitest run packages/shared/src/components/PlatformGameLoadingSurface.test.tsx`、`src/components/game-distribution/GameDistributionPages.test.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx`;`npm run check:game-distribution-ops-rollback-e2e` 47 项通过(含压缩/Vary/ETag/304/不接受 gzip 四条新断言)。真实栈同链路复跑:`phaser.min.js` 1,375,976 B → 353,336 B(gzip,4 Mbps 下 2724 ms → 774 ms),用户端游戏画面 3298 ms → 1543 ms、后台试玩 4587 ms → 2694 ms。
- **关联**:`packages/shared/src/components/PlatformGameLoadingSurface.tsx`、`src/components/game-distribution/GamePlayPage.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、`deploy/nginx/README.md`。