合并 origin/master 到 feat/adapt-xhs-skill:发布入口迁入导出产物面板并吸收发布绑定、内部版本与上传进度
Project CI / Backend tests (pull_request) Failing after 18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m18s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m28s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m20s
Project CI / Repository checks (pull_request) Failing after 33s
Project CI / Frontend tests (pull_request) Successful in 3m7s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m55s
Project CI / Native shell tests (pull_request) Successful in 8m39s

- 陶泥儿 tab 发布面板去掉前端灰度:删除 readGamePublishAvailability 调用、发布权限占位与对应测试 mock,与 master 删除原生灰度命令保持一致,只在项目清单就绪前显示「正在读取项目清单」
- App.tsx 保留本分支导出产物面板架构,发布入口不再挂 Direct 聊天头;useTaonierTab 收敛 allowed/availabilityPending 状态并同步注释
- 发布表单 hook 合并本分支表单状态与 master 的发布绑定回读、AGC 工程内部版本派生、线上最近提交与上传进度阶段
- GameDistributionPublishFormView 接入发布状态面板、只读「项目版本」标签、发布阶段步骤与「更新游戏」等文案
- 合并发布表单与发布反馈测试:恢复封面/试玩包断言,补 readGameDistributionPublication mock
- agc-skills/manifest.json 取本分支新增的 platform-abstract、vite-export-xhs-minitool 与 master 的三处 sha256,version 提升到 2026-08-26.66
- decision-log.md 保留双方新增条目;技术方案文档补回双方新增章节;玩法链路文档取 master 重写后的发行合同
This commit is contained in:
2026-10-06 20:01:16 +08:00
478 changed files with 49995 additions and 21633 deletions
@@ -65,6 +65,47 @@
- 验证方式:`npm run agc:skill-pack:check`(version=2026-08-26.40)、`node --test scripts/check-skill-pack.test.mjs`(5 passed,含隐藏路径拒绝与收集器忽略隐藏文件)、`cargo test --bin genarrative-ai-game-creator-shell -- --test-threads=1 agent::skill_pack`(7 passed)、`agent::codex_app_server::tests`(72 passed)、`agent::direct_runtime::tests`(92 passed)、`cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` 通过;`npx eslint`(变更的仓库脚本)通过,且 `resources/agc-skills/**` 经 `isPathIgnored` 全部返回 true。
- 边界:隐藏文件规则只约束随包审核与只读读取,不改变这些文件在仓库中的存在、权限或本地用途。
## 2026-10-03 每日免费发放额为 0 时前端隐藏该池
- 背景:运营需要一个可逆的「不提供每日免费泥点」状态。不给它新增 `retired` 状态位或新字段,直接把后台配置 `daily_free_points_per_day` 配成 0,让「每日免费发放额」这个普通数值自己表达;前端据此隐藏每日免费相关入口。
- 决策(信号取 `dailyFreeResetPoints` 而非剩余):`ProfileMudPointBalance.dailyFreeResetPoints <= 0` 表示不提供该池。`dailyFreePoints`(当天剩余)为 0 是每天用完后的正常状态,不能当信号。
- 决策(显示规则 `remaining > 0 || reset > 0`):当天仍有存量(配置中途改成 0,但当天已发未用完)时必须继续展示——扣点顺序不变,这部分存量会被优先扣减,隐藏后用户会「有余额看不见、还被先扣」。判定收口到 `packages/shared/src/utils/mudPoints.ts` 的 `shouldShowProfileDailyFreePool`,三端共享组件共用,消费方 controller 不改。
- 决策(后端放开校验、不新增契约字段):`server-rs/crates/module-runtime/src/commands.rs` 的 `daily_free_points_per_day` 由「必须 > 0」放宽为 `0..=i64::MAX`,错误变体改名为 `DailyFreePointsPerDayOverflow`;`0` 本就是 `u64` 合法值,`shared-contracts` / ts-rs 生成物 / OpenAPI 不改。发放与账本逻辑不变(`amount_delta == 0` 时后端本就跳过写账)。
- 影响范围:`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 创作者主页与关注粉丝的产品边界
- 产品已确认:桌面第四项“创作者主页”默认进入当前账号主页,“我的”移到第五项;他人的关注/粉丝列表公开可查看,自己或他人的两类列表均可点击用户进入其创作者主页。
- 关注为单向关系,取消回关与移除粉丝分别影响不同方向;只有本人可移除自己的粉丝,自己不能关注自己。
- 他人的关注、粉丝列表统一只读:保留头像/昵称进入用户主页,不显示任何关系操作按钮;前后端均不额外检测访问者与列表用户的关注关系,已有缓存也不用于显示关系动作。
- 已确认:移动端入口为“游戏 / 创作者主页 / 我的”;自己的主页也只展示公开游戏;自己的关注列表取消后当前行暂留以便重新关注;移除粉丝二次确认,取消关注不弹确认。
- 用户明确本次不额外改造游戏目录分页:既有游戏广场和“我的游戏”保持现状,作者主页按作者过滤后沿用最多 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`,正常请求同样占时)却把用户按在「正在创建工作区」上。
- 决策(建项与命名解耦):建项固定传 `name: null`,由宿主既有兜底名(`GameAgent 项目 <8 位短 id>` / `策划项目 <8 位短 id>`)落盘并立即进入项目;命名请求在建项前并行打出、结果交给后台任务。后台拿到合法名称后调用新增的 `rename_local_game_project_if_unchanged(projectPath, expectedProjectId, expectedName, name)`:只有「项目 ID 相同」且「当前名称仍是本次创建的兜底名」才改名,返回 `renamed: true/false`(跳过时不写盘、不是错误)。用户已手动改名、项目 ID 不符、名称与现状相同一律跳过;空 `expectedProjectId` / `expectedName`、空名 / 控制字符 / 超长一律失败关闭。
- 决策(纳入「生成任务」体系):自动命名做成**项目工作台那条既有「生成任务」列表里的一条后台任务**,而不只是页面里的一个 promise。账本记录加 `taskType`(`asset-generation` / `project-naming`,缺省 `asset-generation`)、`kind` 变可选,schema 升 `agc-asset-generation-task.v2`(读取同时接受 v1/v2,v1 记录按素材任务读回);新增 `enqueue_local_project_naming_task`(排队中)/ `update_local_project_naming_task`(命名中 → 已完成/失败),展示名固定「AI 项目命名」,与素材生成共用同一份账本、同一个变更事件与同一个侧栏。状态流转:排队中 → 命名中 → 已完成(已应用 AI 名称 / 建议名与现状一致 / 用户已手动改名而跳过)或失败(无可用名称 / 自动改名失败,均保留兜底名)。命名任务由命名链路推进、**不进 live 集合就会被中断收口误判**,所以 enqueue 写账本前登记 live、update 终态写盘后摘除(中断残留仍按命名口径收口为失败)。前端按 `taskType` 把命名记录从素材任务分支里剔除,命名行不渲染缩略图/提示词/派发与定位动作,结论只在终态显示。
- 边界:改名是簿记写入,与既有 `rename_local_game_project` 同口径**不推进项目 revision**(推 revision 会让运行时验证凭证无故漂移);返回的 `revision` 是当时盘上的值。后台改名结果写「当前项目上下文」与「最近项目行重检」必须等建项主体收尾(`entrySettled`):AI 比进项目更快时直接写上下文会被随后的 `enterProjectDevelopment` 用兜底名覆盖,最近项目行也要等进项目登记过才会被重检;写入前再过壳的生命周期守卫(`mounted` + 代次),关窗/卸载后只保留已落盘的改名,不写 UI 投影。账本是**展示旁路**:`enqueue` / `update` 失败只写诊断日志,绝不影响建项、命名与首轮创作;「做方案」与手动选目录建项不发起自动命名。本次未改共享契约(`packages/shared/**`)、server-rs 与任何 SpacetimeDB schema/HTTP 路由。条件改名的 `expectedProjectId` / `expectedName` 为空时按仓库同类入口口径失败关闭;`update_local_project_naming_task` 与 enqueue 共用 `project.rename` 权限位、校验记录归属,且终态只接受同状态幂等重放。**兼容性写成显式边界:兼容是单向的**——新构建读 v1 账本 OK;旧构建读到含命名记录(`kind: null`)的账本会整份解析失败(面板报读失败、同批在途素材任务被按中断收口)。前端刷新必须先订阅事件再读快照,并在面板打开时补读一次兜底(**门控**:仅当列表里确实存在在途命名行时才补读,避免给没有命名任务的项目多打一次账本 IPC)。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{commands.rs,desktop.rs,asset_generation_tasks.rs,asset_generation_tasks/runtime.rs}`、`apps/ai-game-creator-shell/src/{app/types.ts,features/app-shell/{useHomeProjectCreation.ts,WorkspaceLauncher.tsx},features/resource-canvas/{resourceCanvasAssetGenerationTaskModel.ts,ResourceCanvasAssetGenerationTasksPanelView.tsx},view/project-development/index.tsx}`、`apps/ai-game-creator-shell/tests/{homeProjectNamingAsync.test.tsx,projectNamingGenerationTaskRow.test.tsx,appSurface/home.suite.ts}`。
- 验证:`npx vitest run apps/ai-game-creator-shell/tests/homeProjectNamingAsync.test.tsx`(12 passed,含「命名请求永不返回仍进工作区」「AI 结果先于进项目落定仍不被兜底名覆盖」「手动改名不被覆盖且任务按已完成+已跳过收口」「非法/空响应 → 任务 failed 且保留兜底名」「做方案不发起命名」「切到别的工作区不被劫持」「卸载/pagehide 后不写上下文」,并断言入队→命名中→终态的账本推进序列);`projectNamingGenerationTaskRow.test.tsx`(3 passed:固定展示名/不渲染缩略图提示词定位、终态才显示结论、失败徽章与原因 + 在途计数);AGC 全量 `npm run test -- apps/ai-game-creator-shell/tests`(1964 passed / 17 skipped);`cargo test --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute asset_generation_task`(27 passed,含 `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 passed,含超长名失败关闭);`npm run agc:typecheck`(含 `check:tests:types`)、`cargo fmt --check`、`npm run check:encoding`、`git diff --check`。
## 2026-10-03 AGC 发布版本标签改为由工程内部版本派生,取代「用户可编辑标签」口径
- 背景:用户实机验收指出发布面板「项目版本」显示 v6,而 AGC 工程内部只有 4 条正式版本记录(资源总览「项目版本」栏目 4 张卡,顶栏「智能体修订」下拉同样只有这 4 条)。核实:面板值来自本地清单 `manifest.projectVersion` 这个可编辑标量,它被三条链路反复钉到**平台** `game_distribution_version.version_number` 上——发布成功回写(`apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:1531-1535`)、打开面板回读绑定回填(`:536-541`)、用户手改(`:944-965`);而 `manifest.versions` 从头到尾不参与该值。`publicationRevision` 只做 CAS,与任何版本号都无推导关系(`module-game-distribution/src/domain.rs:27-40` 的版本号解析只比 `max_existing` 与 `requested`)。
- 决策:AGC 发布面板的「项目版本」改为**由 AGC 工程内部版本记录 `versions[]` 派生**(标签 = 当前存活内部版本条数,即最新版本卡的「版本 N」,空数组取 1),**只读**,且不得被平台 `version_number` 回填或覆盖、不得读取 `publicationRevision`。平台侧 `versionNumber` 语义不变(正整数、允许重复与回退;`None` 仍自动 `max+1` 以兼容网页端与历史客户端)。本地字段 `projectVersion` 降级为遗留兼容位:保留可解析、不再读写(Rust DTO 带 `deny_unknown_fields`,删字段会让存量清单解析失败)。
- 取代(逐条):
1. `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md:97,99`(原「项目清单保存唯一用户发行版本 `projectVersion`」「AGC 发布面板直接编辑项目清单的 `projectVersion`」)→ 已就地改写为「派生只读 + 遗留兼容位」。
2. 同文件原 `:105`(「允许用户修改项目版本标签、重复提交同版本和回退到旧版本标签」)→ 已就地改写为「用户不再编辑版本标签;回退与重复仍由平台侧接受」。
3. `docs/project-memory/plans/【实施计划】AGC已发布游戏版本更新-2026-10-02.md:11,23`(「本地唯一 `projectVersion`…不混入内部编辑迭代 `versions[]`」「发布面板编辑 `projectVersion`」)→ 行内追加取代标注。
4. `docs/project-memory/plans/【里程碑】AGC已发布游戏版本更新-2026-10-02.md:27-30` 的验收项「项目清单只有一个用户发行版本字段,发布面板编辑它」被取代;「项目版本可以低于线上最新版本」在平台侧继续成立,AGC 侧口径改为「派生标签可能因截尾删除而变小」。
- 边界:不改 SpacetimeDB 表/字段/procedure,不改 `publicationRevision` CAS,不改公开地址与审核状态机,不重写平台历史版本行,不做版本历史/回滚 UI。AGC 工程内部版本 `versions` 只能追加或按显式放行删除一个后缀(删素材连带删版本),因此派生标签不保证单调;平台必须继续接受回退标签。
- 影响范围:`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`apps/ai-game-creator-shell/src-tauri/src/{game_distribution_publish.rs,desktop.rs}`、`apps/ai-game-creator-shell/src/{services/gameDistributionPublish.ts,components/game-distribution/GameDistributionPublishPanel.tsx,App.tsx}`、`apps/ai-game-creator-shell/scripts/check-config.mjs`、`apps/ai-game-creator-shell/tests/**`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/project-memory/plans/`。
- 验证方式:`npx vitest run …gameDistributionPublish*.test.*`、`npm run agc:typecheck`(含 `scripts/check-config.mjs`)、`cargo test -p shared-contracts`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml game_distribution_publish`、`npm run check:doc-index`、`npm run check:encoding`、`git diff --check`;真机:内部版本 4 条、线上最近提交 v6 的项目打开面板显示 v4 且只读,提交 `versionNumber: 4`。
- 关联文档:[平台入口与玩法链路](../../【玩法创作】平台入口与玩法链路-2026-05-15.md)、[里程碑](【里程碑】AGC发布版本以工程内部版本为准-2026-10-03.md)。
## 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。
@@ -84,6 +125,18 @@
- 权威文档:`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-04 AGC 工作台顶栏一行到底:三档降级 + 播放并入运行页签 + 运行画面刷新
- 背景:用户现场截图指出四个问题(改动前的形态与口径见 `docs/technical/assets/agc-toolbar-layout-after-20261004/README.md`):① 顶栏放不下时两侧容器各自 `flex-wrap: wrap`,第二行只剩「播放」与版本入口,两行控件分裂;② 版本入口不贴右缘,被前面按钮的文本宽度顶开;③「播放」与「运行」两个入口说的是同一件事;④ 运行画面里的游戏不是 vite dev 的实时刷新,改完代码只能切到资源管理再切回来才能重载页面(飞书讨论里提的「单独的刷新」)。
- 决策(顶栏排版):工具条改**一行到底**(`flex-wrap: nowrap` + `overflow: hidden`),空间不足不再换行,而是按固定顺序降级、档位写在工具条的 `data-layout` 上:`full` → `compact-version`(版本入口只留 `版本 N`)→ `collapsed-actions`(`打开项目目录 / 资源面板 / 整理画布` 收进「更多」下拉)。顺序与 1px 判定余量是纯函数(`workbenchToolbarModel.ts`,单独用例钉顺序);档位由 `useWorkbenchToolbarLayout` 实测写入——`ResizeObserver` 管可用宽度、`MutationObserver` 管**内容**变化(切运行页、出现「恢复草稿」都不改顶栏宽度,只看尺寸会停在旧档位上),每次量宽都把候选档位真的写到 DOM 再读 `scrollWidth`。档位不参与 React 状态:写的是工具条自己的属性,React 不声明就不会覆盖;也没用容器查询(阈值随模式与按钮出现与否变化,写死必然抖)。
- 决策(两个「吞掉溢出」的坑,都是实测踩出来的):`overflow: hidden` 的 flex 子项能缩到 0 或靠省略号吸收溢出,档位判定就永远量不到真实溢出——所以版本入口默认 `min-width: max-content`(只在 `collapsed-actions` 档放开为 `0`,那一档已退无可退,省略号才是兜底)、「依赖 / 类型」分段补 `flex: 0 0 auto`(此前窄宽度下被压成 0 宽)。`max-width: 1000px` / `max-width: 760px` 两处把工具条改成 `flex-direction: column` 的媒体查询删除(那是「第二行」的另一个来源,降级已由档位负责),`≤1000px` 里给动作区的 `justify-content: flex-end` 一并删除(溢出会甩到左边,`scrollWidth` 看不见)。最后一档确实放不下时(视口远小于 1280 合同宽度)才改右对齐:宁可裁左边,也不把钉在最右的版本入口裁没。
- 决策(版本入口与两端分组):工具条这一行**两端留给体量最大的两枚分组控件**——左端「资源管理 / 运行」、右端「依赖 / 类型」(用户口径:「最大的这两个放两边」),中间依次是动作按钮与版本入口;版本入口 `margin-left: auto` 推到动作区右侧,且紧邻排序分段左侧(用户口径:「版本应该在依赖 / 类型左边」),不再是最右那一枚。显示 `版本 N(原因 · 时间)`,其中 `版本 N` 复用资源画布版本卡的编号口径(`manifest.versions` 落盘顺序 + 1,`formatIterationVersionTitle`),括号里那截是独立一层(`formatIterationVersionDetail`)——窄档位收掉的是这一层而不是整枚入口;可访问名、菜单项与排障文案一律保留完整标识。DOM 顺序由 `tests/resourceVersionSwitch.test.tsx`(版本入口在排序分段之前)与 `tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx`(排序分段是动作行最后一个子元素)双向钉住。
- 决策(播放并入运行 + 刷新入口):删掉独立的「播放」按钮,「运行」页签前加 ▶ 图标,点页签=`showRunView()` + `onPlay?.()`(与旧播放按钮逐字等价,含「再点一次=重跑」);不可运行时页签不置灰、点了既不切视图也不发播放请求,只出既有提示。运行画面右下角新增「刷新运行画面」(全屏那一枚左侧):`onPlay` 命中活体预览只切视图、不重启服务,真正重载页面靠换 `iframe` 的元素身份(`LocalGamePreviewFrame` 新增 `reloadNonce`)——运行页在另一个端口上,跨域 iframe 里 `contentWindow.location.reload()` 会被浏览器挡掉。
- 决策(进入项目自动载入):站在运行视图上却没有画面可看时自动补发一次 `onPlay`——典型现场是从别的项目切过来(工作台不重挂,`mode` 是工作台自己的 state,上一条项目的运行视图原样留下而画面已经没了),用户只会看到「客户端运行画面尚未载入」,像坏了一样。只在**没有画面且可运行**时发;`showRunView` 先记账再自己发播放(`autoRunPreviewProjectRef`)所以点页签不会被重复触发;同一个项目只自动补一次,失败不打转,手动重跑仍走页签或画面上的刷新按钮。
- 边界:不改后端、契约与 SpacetimeDB;`runAvailable` / `showRunView` 的门槛语义不变,自动切运行的两条路径(会话内已确认的预览、播放请求)不走 `showRunView`,不会多发播放请求。窄于合同宽度只保证不崩,不做移动端布局。
- 影响范围:`apps/ai-game-creator-shell/src/{styles.css,view/project-development/{index.tsx,workbenchToolbarModel.ts,useWorkbenchToolbarLayout.ts,WorkbenchMoreActionsMenu.tsx},features/resource-canvas/{GameRunVersionPicker.tsx,resourceCanvasVersionBindingModel.ts},features/project-workspace/LocalGamePreviewFrame.tsx}`;用例 `tests/{workbenchToolbarLayout,runPreviewRefresh,runAutoLoadOnEnter}.test.ts(x)`(新增)、`tests/appSurface/project-development.suite.ts`、`tests/{gameRunToolbarActionsStyle,resourceCanvasVersionBindingModel}.test.ts`;文档 `docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S17 改写 + 新增 S17a)、`docs/README.md`、新目录 `docs/technical/assets/agc-toolbar-layout-after-20261004/`。
- 验证:`npx vitest run apps/ai-game-creator-shell/tests`(200 passed / 1 skipped 文件,1929 passed / 17 skipped 用例,末次全量);定向 8 个文件 253 passed;`npm run typecheck`(在 `apps/ai-game-creator-shell`,含 `check:tests:types`——只跑 `tsc -p tsconfig.json` 覆盖不到 `tests/`)、eslint `--max-warnings 0`、`prettier --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 全绿。真机几何用一次性 Vite 夹具在真实 Chromium 里逐档实测(视口 1412 / 1240 / 1100 / 1024 / 960 / 860 / 800 / 760 / 700 / 640 / 560 / 480 / 400):始终单行、排序分段贴右缘(`工具栏右缘 - padding - 排序分段右缘 = 0`)、版本入口始终在排序分段左侧、1240/1100 走 `compact-version`、1024/800/700/640/560/480 走 `collapsed-actions`,「更多」下拉三条动作可点且点「资源面板」真的开面板、点「刷新运行画面」`iframe` 换新节点而 `src` 不变;截图见 `docs/technical/assets/agc-toolbar-layout-after-20261004/`。
## 2026-10-03 AGC 画布引用统一走「活跃聊天输入区」注册表(Issue 602)
- 背景:画布的「引用」按钮与「拖拽批量引用」只派发 window 事件,消费者只有 `App.tsx` 一处,而它插的是绑在 `PlanningChatView` 上的 `chatComposerRef`;2026-09-22 DirectProject 拆分后普通项目走 `directProjectMode` 提前 return,渲染不到策划面 → ref 恒为 `null`,可选链静默吞掉点击(画布上是死按钮)。同一批合并冲突还丢了 `RESOURCE_REFERENCE_INSERT_MANY_EVENT` 的监听,批量引用连消费者都没有。
@@ -199,6 +252,25 @@
- 影响范围:`apps/ai-game-creator-shell/src/styles.css`、`src/view/home/index.tsx`、`src/view/template-library/index.tsx`。
- 验证:Chromium 真机量测(1440x800 / 1440x560 / 390x844 / 390x560,带与不带横幅)——首页 / 模板库 / 项目页改动前后高度一致,帮助页从「被裁 112~150px 且无可滚动祖先」变为正好等于 `.window-chrome__content` 高度并可滚动到底;`npm run typecheck`、`npm run check:encoding`、`git diff --check`、`eslint` 通过。
## 2026-10-02 泥点双余额与会员补差升级方案(拷问定稿)
- 决策(对外投影):余额接口并列暴露每日免费泥点 / 月度泥点 / 永久泥点三池,永久泥点不再表现为「总额的剩余」;`totalPoints` 的池语义退役,`limitedPoints` 更名 `monthlyPoints`。存储层不动——`wallet_balance` 仍是唯一权威总额,永久泥点 = 总额 − 每日免费 − 月度,并留 TODO 待后续改为独立存储(触发条件见 ADR)。
- 决策(扣减与展示):扣减顺序维持「每日免费 → 月度 → 永久」,可用总额 = 月度 + 永久,每日免费保留并单独计量与展示。
- 决策(会员侧重做):会员侧未上线,旧档位 `Month/Season/Year` 与 `Basic/Ultimate` 及后台按档位权益字段直接删除;账期改为按开通日自然月(当月最后一天夹取),年付 12 期用 `cycle_index` / `cycle_count` 表达,仍只有一行活跃会员记录。
- 决策(充点侧):6 档充点、取消首充赠送;复用旧 4 档 `points_*` id 并把赠送置 0,新增两档(`points_1280` / `points_3280`),不删除任何行(历史订单与首充资格按 `user_id + product_id` 引用)。
- 决策(升级):后端只读报价接口 + 下单重算并落订单快照;会员订单复用 `profile_recharge_order`(新增 kind 与末位变更快照字段);新增账本来源 `MembershipUpgradeGrant`,幂等锚 `membership-upgrade-grant:{user}:{order_id}`;金额向上取到分、补点向下取整,年付按「后续完整月数 + 本期实际月长占比」;会员退款维持 `manual_review`,只靠订单快照支撑人工复核。
- 边界:每日免费池、永久余数口径、账本与充点订单**在线上**;只有会员侧未上线,因此会员侧零迁移、钱包侧不允许口径漂移。
- 决策(会员档位枚举化):会员档位身份改为 Rust 枚举 `RuntimeProfileMembershipPlan { Normal, Starter, Plus, Pro, Max }`,绝不字符串;新增 `profile_membership_plan` 目录表并**以该枚举为主键**(后台按档位改价 / 改权益);`profile_membership.tier` 与 `profile_recharge_product_config.tier` 两列删除、旧 `RuntimeProfileMembershipTier` 整体退役。会员侧无存量行,属经用户授权的破坏性变更;账本来源枚举仍只允许末尾追加 `MembershipUpgradeGrant`。
- 决策(目录权益):目录一行一档,含月价、**独立可配置年价** `year_price_cents`、每期额度、模型权限 `Basic` / `Full`(本期只落字段、不执行拦截)、并发上限 `u32`(**哨兵 128 = 不设上限**,不用 `Option`);`Normal` = 0 点 / `Basic` / 并发 1 / rank 0。
- 决策(目录数值):`Starter` ¥39 / 400 / ¥390 · `Plus` ¥99 / 1150 / ¥990 · `Pro` ¥299 / 3650 / ¥2990 · `Max` ¥699 / 8650 / ¥6990。
- 权威入口:[实施计划](../../technical/【实施计划】双余额泥点与会员补差升级-2026-10-02.md);设计见 [`【技术设计】泥点三池与会员计费后端设计`](../../technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md);决策见 [`【ADR】泥点三池以单一总额为权威`](../../adr/【ADR】泥点三池以单一总额为权威-2026-10-02.md)、[`【ADR】会员账期按开通日自然月`](../../adr/【ADR】会员账期按开通日自然月-2026-10-02.md)、[`【ADR】会员订单复用充值订单与补差升级幂等锚点`](../../adr/【ADR】会员订单复用充值订单与补差升级幂等锚点-2026-10-02.md)、[`【ADR】会员档位以枚举为权威`](../../adr/【ADR】会员档位以枚举为权威-2026-10-02.md)。
- 状态:**已落地**。后端(领域模块、`profile_membership_plan` 表与播种、两处 `tier` 列删除、订单变更快照、自然月账期、6 档充点、三池余额投影、升级报价接口、后台档位接口)、契约生成(ts-rs → `packages/shared/src/contracts/generated/`)、共享组件与平台入口三池展示、AGC 壳、后台「会员档位」页与充值商品页收敛均已实现并通过门禁。明确不做:模型权限与并发上限只落字段、不提供 `availableTotalPoints`、会员购买 / 升级的 C 端 UI。
- 实现期补充决断(未改变上述范围):
- 余额 DTO 由 Rust 单一真源经 ts-rs 导出,前端 re-export 而非手写第二份字段列表;`u64` 标 `ts(type = "number")` 避免 `bigint` 分叉,`check:generated-bindings` 以「重新生成逐字节比对」守漂移。
- 「合计」由前端派生(`packages/shared/src/utils/mudPoints.ts` 的 `resolveProfileMudPointBalanceView`),月度刷新时刻固定按北京时间格式化,不把第二份口径放回后端。
- 会员订单 `product_id` 编码「档位 + 周期」(`membership-{plan}-{cycleKind}`),会员商品不进 `profile_recharge_product_config`;支付确认不做出售校验,避免付款后档位被下架导致权益丢失。
- 后台 `rank` 不可改(升级比较基准由代码内目录决定),`normal` 档位的价格与额度由后端强制归零。
## 2026-10-01 游戏广场评分展示边界
- 用户确认广场卡片增加一位小数的 10 分制平均分与评分人数,无有效评价显示“暂无评分”;保留现有排序、筛选、卡片打开详情及返回上下文。
@@ -880,7 +952,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和
- Direct 回合的合同、证据和预算以宿主私有账本为准,项目侧记录只作展示。GUI/CLI 共用入口;首次副作用前冻结非空验收合同,可信新 Web 工程由宿主补充构建和双端验证底线。普通无副作用聊天不强制构建。
- 视觉、固定玩法和托管命令分层;`validation.maxRuns` 按执行/返修批次管理,正常开发命令共享批次;累计执行时间与整轮墙钟分别受 `maxExecutionSeconds` / `maxTurnSeconds` 约束,显式配置与 Provider 重试独立。源码、构建输出、环境输入与证据文件摘要分别复核,项目可编辑记录不能抬高预算或伪造成功。
- 原生工具使用已验证的捆绑版本逐次审批能力,第三方 MCP 显式逐调用询问;所有 Direct 入口接受宿主同一状态,未知远端结果不得以本地进程退出代替。独立客户端 HTTP MCP 使用明确的 ExternalClient 来源,保留其既有边界,不借用另一 Direct 回合的预算。
- 交付必须先封口、排空和取得执行器退出证明,再核对当前文件并提交完成;Windows 用自有 Job 约束进程树,托管命令在恢复主线程前绑定。完整退出证明不足时保持未完成,不把模型最终回复当作验收。非阻塞扩项进入新的用户回合。
- 交付必须先封口、排空和取得受控执行归属退出证明,再核对当前文件并提交完成;Windows 用自有 Job 约束进程树,托管命令在恢复主线程前绑定。Unix 受控进程组退役允许回合完成,但不宣称完整子树均已退出;归属清理失败仍保持未完成,不把模型最终回复当作验收。非阻塞扩项进入新的用户回合。
- 模型配置、实际请求标识和流分段耗时写入现有审计账本;统计采用并发区间并集,有界后台写入,详细条目截断后仍聚合。上游内部排队和推理耗时不可见时保持未知。
- Direct 工具集中 SDK 原生 `apply_patch` / `update_plan` 是全局串行单例,按回合为每个 Direct 连接导出一份只把 `apply_patch_tool_type` 置空的完整模型目录即可移除该注册;其余 metadata、匹配与 fallback 不变,不得伪造 `readOnlyHint` 或改造 SDK。等价能力由宿主 MCP 的 `agc_apply_patch`(官方 parser、当前回合 Write 许可、受控进程树、短项目事务)与 `agc_update_plan`(宿主计划状态,不作为验收证据)提供;缺少合法回包通道的原生问答工具一并关闭。
- 捆绑 Codex 固定版本只在 `build_support/codex_bundle.rs` 声明一次(当前 0.155.1),构建期侧车清单、宿主补丁执行器身份、逐次审批协议允许列表和模型目录捕获共同引用;升级原生依赖时同步重取同一 tag 的 vendor 解析源码与 UPSTREAM 证据,并复跑真实目录、补丁往返与并发夹具。0.155 起原生执行入口改为统一 exec(`exec_command` + `write_stdin`,旧 `shell_command` 不再注册),宿主许可与预算照常覆盖。
@@ -9628,3 +9700,183 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 验证:`npm run agc:bundled-resources:test`(18 passed,含 sidecar 归位、跳过规则、上游缺失、版本漂移、目录被占);`npm run agc:bundled-resources:check`、`check-config.mjs`、`cargo test --bin genarrative-ai-game-creator-shell package_layout::tests`、`cargo check --no-default-features`、`cargo fmt --check`、`check:encoding`、eslint/prettier 全部通过;Windows 真机准备步骤 staging 24 个文件(含 243MB `claude.exe`)后 `cargo check` 不再出现构建期写入。
- 边界(未验证):macOS 真机的 sidecar 加载与 `check-macos-bundle.mjs` 包内容门禁未在本机验证;Linux 门禁按新配置不再要求 sidecar 资源,需 CI 实跑确认转绿。
- 关联:issue #519、master `fb130d184`、`docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md`、CI run 3083。
## 2026-10-03 后台补全会员计费运维面:订单快照、用户会员状态、三池命名与表标签
- 背景:`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.6 声称「订单列表 / 退款人工复核读取 §2.4 快照」,但管理端 `AdminRechargeOrderEntryPayload` 没有任何会员字段;用户详情不展示会员档位与账期,钱包月度池仍叫「会员限时」,通用表浏览器缺 `profile_membership_plan` 标签。
- 决策(订单快照透出):新增 `AdminMembershipOrderChangePayload`(变更类型 / 前后档位 / 前后账期 / 前后每期泥点 / 补点增量 / 期数 / 前后到期与重置 / 价格拆分 JSON)挂到 `AdminRechargeOrderEntryPayload.membershipChange`,由 `map_membership_order_change` 从 module-runtime 既有的 `RuntimeProfileMembershipOrderChangeSnapshot` 直接映射;非会员订单为 `null`。只读,不改下单 / 退款语义。
- 决策(用户会员状态):`AdminUserDetailResponse` 新增 `membership: AdminProfileMembershipPayload`,api-server 复用 `get_profile_recharge_center` 的 `membership` 投影,不再新开一条 admin 专用读路径。
- 决策(三池命名只在契约 / UI 层):`AdminProfileWalletPayload.membership_limited_points` 改名为 `monthly_points`(TS `monthlyPoints`),后台钱包区块「会员限时」改为「月度泥点」。**SpacetimeDB 列名 `membership_limited_points` 与生成绑定保持不动**——持久化改名属破坏性变更,须先确认迁移计划。
- 决策(展示复用):新建 `apps/admin-web/src/config/membershipDisplay.ts` 收敛档位 / 账期展示名与 `format*`,`AdminMembershipPlanPage`、`AdminRechargeOrderPage`、`AdminUserDetailDialog` 共用,不再各留一份 `planLabels`。
- 影响范围:`server-rs/crates/shared-contracts/src/admin.rs`、`server-rs/crates/api-server/src/admin_recharge.rs`、`apps/admin-web/src/{api/adminApiTypes.ts,config/membershipDisplay.ts,pages/AdminMembershipPlanPage.tsx,pages/AdminRechargeOrderPage.tsx,components/AdminUserDetailDialog.tsx}`、`scripts/admin-web-fake-api.mjs`、设计文档 §5.6/§10.2、本文件。
- 验证:`cargo check -p api-server`;`npm run admin-web:typecheck`;`npx vitest run apps/admin-web` → 26 files / 248 tests passed(新增「会员订单在发放泥点列展示会员变更快照」及用户详情会员断言)。
- 边界:`/admin/api/*` 不在 `/api/external/v1` OpenAPI 门禁内,本次未补接口级契约测试,会员快照只在管理端页面用例层面取证。
## 2026-10-03 模型权限与并发上限的强制边界(拷问定案)
- 背景:`model_access` 与 `concurrent_job_limit` 此前只落字段、只用于展示,服务端零拦截;用户点出 raw gpt-image-2 这类同步生成,确认「只统计 job 的 `running`」覆盖不到它,因此先把「并发单位」定义清楚再强制。
- 决策(并发上限只约束队列 job):强制点放在唯一的 `pending → running` 事务点 `claim_external_generation_jobs_tx`,认领时按账号统计 `running` 数、达上限则跳过(任务留在 `pending`,天然排队,不新建队列)。只算 `running`(含过期待回收的 `expired_running`);失败退回 `pending` 重试释放名额;自身过期 `running` 回收豁免;上限读 `profile_membership_plan.concurrent_job_limit`,缺行失败关闭到 `Normal`(=1),`128` 哨兵为不设上限;计数用新增 `(owner_user_id, status)` 组合索引现算,不建计数表;`/api/external/v1` 任务同样计入;不设排队深度上限。
- 决策(模型权限只约束 AGC LLM 模型):`AgcModel` 末位加权限档、缺省 `Basic`(失败开放),由人工在后台「AGC 模型」页手动标 `Full`;`GET /api/llm/models` 按档过滤并把 `defaultModelId` 取可用集合首个,`/api/llm/responses` 经 `resolve_requested` 后校验、越权返 `MODEL_NOT_AVAILABLE_FOR_PLAN`;本地残留旧选择明确 4xx,不静默回退;不设灰度开关。
- 边界(本轮不做):直连同步生成(raw gpt-image-2、`editor_project` 直连图片 / 图标 / 背景、角色资源、音频)没有 job 行,`running` 计数覆盖不到;统一「账号在飞生成」口径(在飞租约表 / 或统一改走 job)留作后续 TODO。
- 影响范围:`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md`(新增 §11,修订 §9/§10)、`CONTEXT.md`(并发上限 / 新增「独立生成任务」)、`docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md`、`docs/README.md`。未改代码。
- 验证:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 2026-10-03 模型权限与并发上限落地实现
- 背景:承接同日「模型权限与并发上限的强制边界(拷问定案)」,把定案按 §11.4 编码约定落地;本文记实现拆分与验证,不再重复决策理由。
- 实现(`module-runtime`,`b78003143`):新增 `agc_model_access.rs`(`AgcModelAccess::Basic/Full`、`MODEL_NOT_AVAILABLE_FOR_PLAN`、`AgcModelResolveError::{Unavailable, NotAvailableForPlan}`、`RuntimeProfileMembershipModelAccess → AgcModelAccess`);`AgcModel` 末位追加 `#[serde(default)] access`(存量 JSON 缺字段即 `Basic`,失败开放);`AgcModelCatalog` 增加 `available_models_for / default_model_id_for / resolve_requested_for`,默认模型优先取登记项、不在档内则回退目录顺序首个可用项、无可用项返回 `Unavailable`。
- 实现(`spacetime-module`,`db8d467b8`):`external_generation_job` 追加 btree 组合索引 `(owner_user_id, status)`;`claim_external_generation_jobs_tx` 在 `pending → running` 前按 owner 现算 `running` 行数并过滤,达上限留在 `pending`;只算 `running`(含过期待回收),自身过期 `running` 回收豁免,`pending` 不占名额;`effective_profile_concurrent_job_limit` 走 `profile_membership.plan → concurrent_job_limit`,缺行失败关闭到 `Normal`(=1),`128` 为不限;抽取纯函数 `external_generation_claim_within_concurrency_limit` 并补单测。
- 实现(后台,`919b4b01d`):`AdminAgcModel` 新增 `access`(缺省 `basic`),api-server 保存时只接受 `basic/full`,`GET` 回读 / `PUT` 写入;后台「AGC 模型」页新增「权限档」列与选择器,fake API 与页面用例覆盖。
- 实现(`api-server`,`9b1a538d2`):新增 `llm/model_access.rs`,用现有 `get_profile_recharge_center` 解析账号档位到 `AgcModelAccess`,缺会员行 / 缺目录行 / 读失败统一失败关闭到 `Basic`;`GET /api/llm/models` 按档过滤且默认项取该档可用集合;`/api/llm/responses`、`/api/llm/chat/completions`、`/api/llm/anthropic/*` 全改按档解析;越权映射 403 + `MODEL_NOT_AVAILABLE_FOR_PLAN`,目录外 / 停用仍为 422。
- 术语口径:`Basic` / `Full` 是模型对会员档的要求,`Normal` 等是会员档位;缺省语义统一「失败开放到基础档、失败关闭到 1 并发」——模型档缺省 `Basic` 是失败开放(不拦),并发上限缺行失败关闭到 `Normal`(=1)。
- 验证:`cargo check -p api-server`、`cargo check -p api-server --tests`;`cargo test -p api-server llm` 49 passed、`model_catalog_tests` 3 passed(含 Basic 档过滤 Full 模型、403 错误码映射);`npm run check:spacetime-schema` 的失败项全部是分支既有 `profile_membership` / `profile_recharge_product_config` 字段变更(对照 `origin/master` merge-base `25beb3ad5`),与本次仅新增 btree 索引无关,guard 不比较索引;`npm run check:encoding`、`git diff --check`。
- 边界 / TODO:账号档位仍复用带周期刷新写入的 `get_profile_recharge_center`,后续替换为轻量专用读;AGC 客户端仍靠「目录按档过滤 + 本地选择对账回退默认项」,未加专门的 403 重选提示;统一「账号在飞生成」口径(含无 job 行的同步生成)仍为后续 TODO。
## 2026-10-03 模型列表改为可用 / 不可用分桶(修订)
- 背景:`GET /api/llm/models` 原按档过滤,降级用户在客户端只看到「所选模型已停用」的笼统提示,且无法在列表里露出可升级的高档模型。
- 决策(契约):返回 `models`(本档可用)与 `unavailableModels`(目录里其余全部,带 `reason` 枚举)两个平行数组,**不新增** `available` 布尔;`reason` 取 `plan_required` / `disabled` / `unknown`,同时停用且档位不够取 `disabled`;`unavailableModels` 保持目录顺序、恒定下发(无内容为空数组);上游真实模型名两桶都不下发;`defaultModelId` 必须落在 `models` 内;某档一个可用模型都没有时保留 503 `MODEL_UNAVAILABLE`。
- 决策(兼容):`unavailableModels` 是加法字段,旧客户端忽略后行为不变,无需能力协商;`reason` 在 wire 侧带 `unknown` 兜底,后端将来新增取值不会让旧客户端解析失败;服务端按档强制不变。
- 决策(客户端):AGC 桌面选择器把 `unavailableModels` 渲染为不可点项,仅 hover / 键盘 focus 出 tooltip(`plan_required`→「订阅计划不支持」、`disabled`→「该模型已下线」、`unknown`→「暂不可用」);降级仍自动回退默认,提示按 `reason` 取;后台直接删除(两桶都没有)用泛化文案「所选模型已不可用,已切回默认模型」;不再新增「当前已选不可用」的 API 字段,前端自行从 `unavailableModels` 推断。
- 决策(ts-rs 单一真源):整个目录 DTO 与枚举导出到 `packages/shared/src/contracts/generated/`;新增 barrel `packages/shared/src/llm/modelCatalog.ts` 并由 `packages/shared/src/index.ts` 转发;`AgcAgentMode` / `AgcModelProtocol` 从 `module-runtime` 迁入 `shared-contracts`(`module-runtime` re-export);`revision: u64` 标 `#[ts(as = "f64")]`;AGC 前端删除手写 `ClientLlmModel`,改从 `@genarrative/shared` 根导入。
- 影响范围:`docs/adr/【ADR】模型权限与并发上限的强制边界-2026-10-03.md`(修订节)、技术设计 §11.2/§11.4、`docs/technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md`、`docs/project-memory/plans/【里程碑】AGC模型目录上游同步-2026-09-24.md`、本文件。
- 验证:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 2026-10-03 模型权限改走只读 procedure(性能收口)
- 背景:`api-server` 的 AGC 模型权限解析原复用 `get_profile_recharge_center`,它带幂等的账期 / 免费点刷新与钱包 / 商品等无关快照字段,位于每个 `/api/llm/*` 请求热路径;同日「落地实现」条目已把它列为 TODO。
- 决策:新增只读 procedure `get_profile_agc_model_access_and_return`(输入沿用 `RuntimeProfileMembershipGetInput`,结果 `RuntimeProfileAgcModelAccessSnapshot { user_id, plan, model_access }`,仅 editor generation runtime service identity 可调):只读会员账期投影 + `profile_membership_plan.model_access`,缺会员行 / 已过期按 `Normal`,缺档位目录行失败关闭到 `Basic`,不刷新、不写库;`api-server` 改调它,失败关闭语义不变。
- 影响范围:`module-runtime`(领域类型 `9e397fe9c`)、`spacetime-module`(procedure,同提交)、生成绑定、`spacetime-client`(`get_profile_agc_model_access`)、`api-server/src/llm/model_access.rs`(`b0dc94b46`)、技术设计 §11.4。
- 验证:`cargo check -p spacetime-module`、`cargo check -p spacetime-client`、`cargo check -p api-server --tests`;`cargo test -p api-server llm` 51 passed;`npm run check:encoding`、`git diff --check`;`npm run check:spacetime-schema` 失败项仍为分支既有 `profile_membership` / `profile_recharge_product_config` 表字段变更,与本次仅新增 procedure、未改表无关。
## 2026-10-03 会员链路错误分类改为机器可读错误码(typed error)
- 背景:`api-server/src/runtime_profile.rs` 的 `is_runtime_profile_membership_domain_error` 用中文 `error_message` 前缀 / 精确匹配决定返回 400 还是 502;文案一改(或新增拒绝点)就静默退化,且分类逻辑与 module 的错误文案跨层重复。
- 决策(错误码随 procedure 结果透传):`module-runtime` 新增 `RuntimeProfileMembershipErrorCode`(`UpgradeRejected` / `NotMember` / `AlreadyActive` / `CycleKindLocked` / `MembershipExpired` / `StateChanged` / `PlanNotPurchasable` / `PlanCatalogMissing` / `ProductIdUnparseable` / `MembershipProductRetired` / `InvalidPlanConfig`)与内部 `RuntimeProfileMembershipDomainError { code, message }`;三个会员 procedure 结果(升级报价、充值中心 / 下单、后台档位 upsert)末尾追加 `error_code: Option<...>`,拒绝点显式赋码,不再做字符串分类。
- 决策(客户端错误类型):`spacetime-client` 新增 `SpacetimeClientError::ProcedureRejected { code, message }`;`api-server` 据此返回 400(provider `runtime-profile`),基础设施错误保持 502。字段校验类错误不产码,仍落 502。
- 决策(行为变化):`会员目录缺少档位 ...` 与「已迁移的会员商品」原先落 502,现在明确 400;兑换码链路本轮未改造,仍保留一个文案匹配函数。procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs}`、生成绑定、`server-rs/crates/api-server/src/{runtime_profile.rs,asset_billing.rs,editor_project.rs}`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check --workspace`;`api-server runtime_profile::tests` 41 passed;`module-runtime --lib membership::` 39 passed;`npm run check:encoding`、`npm run check:spacetime-schema`、`cargo fmt -- --check`、`git diff --check` 通过。
## 2026-10-03 会员目录播种策略定为「保留懒播种 + 明示副作用」
- 背景:评审第 20 项指出「只读」procedure(报价 / 充值中心 / 后台查询)首次读取时会经 `ensure_default_profile_membership_plan` 写库,与代码注释「不写库」矛盾。三条候选:①保留懒播种并承认副作用;②只靠 init + 发布期 migration 播种;③只读投影不落表、内存回退代码内目录。
- 决策:选 ①。`init_profile_membership_plan_catalog` 只在数据库首次创建时执行,增量发布不重跑;「表已存在但为空」的存量库必须靠读取 helper 懒播种兜底,否则目录为空会让下单 / 报价失败。保留懒播种,并把「只读入口在目录为空的首次调用会写库、表非空后幂等只读」明确写进代码注释与设计文档 §7,消除注释与实现的矛盾。
- 未选 ②/③ 的原因:②依赖发布流程保证存量库被种上,当前没有这条保证;③改动读路径回退逻辑,风险与收益不匹配。将来若做发布期 migration,再删除懒播种。
- 影响范围:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`(`ensure_default_profile_membership_plan` / `init_profile_membership_plan_catalog` / `membership_plan_row` / `membership_plan_records` / 报价与建单函数注释)、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §7/§10.2、本文件。
## 2026-10-03 兑换码链路补机器可读错误码(typed error 收口)
- 背景:评审第 12 项指出 `SpacetimeClientError::ProcedureRejected` 的 `Display` 丢掉 `code`,而 `api-server/src/runtime_profile.rs` 的 `is_runtime_profile_redeem_code_domain_error` 仍在精确匹配 7 条中文 `error_message` 来判断兑换码拒绝该返回 400 还是 502,文案一改就静默退化,与会员链路已落地的 typed error 不一致。
- 决策(module 侧赋码):`module-runtime` 新增 `RuntimeProfileRewardCodeRedeemErrorCode`(`NotFound` / `Disabled` / `NotStarted` / `Expired` / `UsesExhausted` / `NotAllowedForUser` / `InvalidReward`);`RuntimeProfileRewardCodeRedeemProcedureResult` 末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileRewardCodeRedeemProcedureError::{Classified, Plain}` 在拒绝点显式赋码,入参与钱包类错误保持 `error_code = null`。
- 决策(客户端 / BFF):`SpacetimeClientError::ProcedureRejected` 的 `code` 改为 `RuntimeProfileProcedureRejectionCode`(聚合 `Membership` 与 `RewardCodeRedeem` 两类),`Display` 输出 `[code] message` 保留机器原因;`api-server` 的 `map_runtime_profile_client_error` 只读变体的 `code` / `message`,`AppError.code` 取 `code.as_str()`,删除 `is_runtime_profile_domain_error` 与 `is_runtime_profile_redeem_code_domain_error` 两个文案匹配函数。
- 决策(行为变化):兑换码的 `兑换码不存在` / `已停用` / `未生效` / `已过期` / `次数已用完` / `不适用于当前账号` / `奖励无效` 由原来的 502 变为 400 且带机器码;procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs}`、生成绑定、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check -p api-server`;`cargo test -p api-server runtime_profile::tests` 41 passed;`cargo test -p module-runtime`、`cargo test -p spacetime-client`、`cargo test -p spacetime-module` 全通过;`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-03 会员计费评审第 13–20 项收口:契约枚举化、生成类型、计费与结算防御
- 背景:评审剩下一批 medium / low 的契约与防御性缺陷,用户逐项确认后一次性修复;每项单独提交,便于回溯。
- 决策(契约枚举化,13/14/18):`shared-contracts` 新增 `ProfileMembershipStatusToken`(`normal` / `active`),`AdminProfileMembershipPayload` 的 `plan` / `status` / `cycle_kind` 改用 token 枚举;5 个会员 token 枚举补 `ts_rs::TS` + `ts(export, ...)`,前端 `packages/shared/src/contracts/runtime.ts` 与 `apps/admin-web/src/api/adminApiTypes.ts` 改为生成类型别名 / re-export,禁止手写联合类型;`AdminAgcModel.access` 由裸 `String` 改为 `ProfileMembershipModelAccessToken`,未知值反序列化即拒。
- 决策(计费防御,15/16):`quote_runtime_profile_membership_upgrade` 显式拒绝价格 / 每期泥点 `<=`,堵住 0 元升级;`advance_beijing_months_clamped` 去掉 `expect`,超出 i64 微秒可表示范围(约 ±29 万年)时按方向饱和到 `i64::MAX` / `i64::MIN`,不改签名以免牵动 `membership_cycle_window` / `membership_expires_at`。
- 决策(错误链 typed,17):`resolve_llm_router_client` 返回 `Result<_, AppError>`,`load_owner_llm_catalog` 的 `503 + MODEL_ACCESS_UNAVAILABLE` 与 `resolve_requested_for` 的 `MODEL_UNAVAILABLE` 不再被拍平成中文,Chat Completions 与 `/api/llm/models`、代理端点错误码对齐。
- 决策(去掉退役入参,19):后台 upsert 请求 DTO 与 SpacetimeDB procedure 入参移除 `bonus_points` / `duration_days`,`profile_recharge_product_config` 退役列保留、写死 0;删除 `RuntimeProfileFieldError::InvalidRechargeProductFields`;后台表单去掉「首充赠送」,假数据同步。
- 决策(结算归一,20):`frozen_membership_cycle_matches` 比较双方都取 `max(1)`,修复 `cycle_index == 0` 迁移行被永久判成「会员状态已变化」的问题;不新增写库副作用。
- 影响范围:`shared-contracts/{runtime,admin}.rs`、`module-runtime/{domain,commands,errors}.rs` 与 `module-runtime/src/{membership/upgrade,membership/cycle,agc_model_access}.rs`、`spacetime-client/{active,active/mapper/runtime_profile,module_bindings}`、`spacetime-module/src/runtime/active/profile.rs`、`api-server/src/{runtime_profile,llm/mod,agc_models}.rs`、`packages/shared/src/contracts/runtime.ts`、`apps/admin-web/src/api/adminApiTypes.ts`、`apps/admin-web/src/pages/AdminRechargeProductPage.tsx`、`scripts/admin-web-fake-api.mjs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、本文件。
- 验证:`cargo test -p api-server` 1206 passed;`-p module-runtime` / `-p spacetime-client` / `-p spacetime-module` 全通过;`npm run spacetime:generate`、`npm run check:spacetime-schema`(92 表)、`npm run check:generated-bindings`(117 文件)、`npm run admin-web:typecheck`、`npm run typecheck`、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-03 后台兑换码管理补机器可读错误码(typed error 收口)
- 背景:`bd06e4269` 删除中文文案匹配后,`admin_disable_profile_redeem_code` / `admin_upsert_profile_redeem_code` / `admin_list_profile_redeem_codes` 仍只回传裸 `error_message`。这些 procedure 的 `ok = false` 是明确的业务拒绝(停用不存在的码、入参非法、私有码缺可兑换用户),却被 `spacetime-client` 拍平成 `Procedure(String)`,在 `api-server` 落 502,与会员 / 核销两条链路已落地的 typed error 不一致。
- 决策(module 侧赋码):`module-runtime` 新增 `RuntimeProfileRedeemCodeAdminErrorCode`(`NotFound` / `InvalidField` / `AllowedUsersRequired`);`RuntimeProfileRedeemCodeAdminProcedureResult` 与 `RuntimeProfileRedeemCodeAdminListProcedureResult` 末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileRedeemCodeAdminProcedureError::{Classified, Plain}` 在拒绝点显式赋码,`RuntimeProfileFieldError` 统一归 `invalid_field`,私有码缺可兑换用户归 `allowed_users_required`,停用不存在的码归 `not_found`。
- 决策(客户端 / BFF):`RuntimeProfileProcedureRejectionCode` 增加 `RedeemCodeAdmin` 聚合变体,`spacetime-client` 两个后台 mapper 有 `error_code` 时走 `ProcedureRejected`、缺失时保持 `Procedure`;`api-server` 复用既有 `map_runtime_profile_client_error`,无需再引入字符串分类。
- 决策(行为变化):后台兑换码的 `兑换码不存在`、入参校验失败、私有码缺可兑换用户由 502 变为 400 且带机器码;procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs,module_bindings,module_bindings.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check`(module-runtime / spacetime-module / spacetime-client / api-server);`cargo test -p spacetime-client` 30 passed;`cargo test -p api-server runtime_profile::tests` 42 passed;`cargo test -p module-runtime --lib` 113 passed;`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
## 2026-10-03 后台邀请码管理补机器可读错误码
- 背景:后台邀请码的 `admin_upsert_profile_invite_code` / `admin_list_profile_invite_codes` 与上一轮的后台兑换码同病——`map_runtime_profile_invite_code_admin_procedure_result` 把 `ok = false` 一律拍成 `Procedure(String)`,`邀请码已被其他用户占用` 这类明确业务拒绝在 `api-server` 落 502,且 `map_runtime_profile_client_error` 的注释夸大成 «profile procedure 的业务拒绝都已 typed»。
- 决策:`module-runtime` 新增 `RuntimeProfileInviteCodeAdminErrorCode`(`InvalidField` / `InUse`);两个后台邀请码 procedure 结果末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileInviteCodeAdminProcedureError::{Classified, Plain}` 赋码,字段校验归 `invalid_field`、被他人占用归 `in_use`;`RuntimeProfileProcedureRejectionCode` 增加 `InviteCodeAdmin` 聚合变体,`spacetime-client` mapper 有码走 `ProcedureRejected`、缺失时保持 `Procedure`;`api-server` 复用既有映射,并把注释收窄为 «带 typed error_code 的四条链路»。
- 行为变化:后台邀请码的入参校验失败与 `邀请码已被其他用户占用` 由 502 变为 400 且带机器码;procedure result 新增字段仍要求 module 与 api-server 同版本部署。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs,module_bindings,module_bindings.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
- 验证:`npm run spacetime:generate`;`cargo check`(module-runtime / spacetime-module / spacetime-client / api-server);`cargo test -p spacetime-client` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-module` 271 passed。
## 2026-10-03 档位无可用模型改用独立错误码 MODEL_UNAVAILABLE_FOR_TIER
- 背景:`AgcModelResolveError::Unavailable`(422)与 `NoModelForTier`(503)此前共用 `MODEL_UNAVAILABLE`(见本文件 2026-10-03 的列表契约条目),只按 `error.code` 分派的客户端无法分辨「单个模型不可用」与「整个档位无可用模型(目录 / 档位配置问题)」,两者修法不同。
- 决策:`module-runtime` 新增常量 `MODEL_UNAVAILABLE` 与 `MODEL_UNAVAILABLE_FOR_TIER`,`NoModelForTier.code()` 返回后者,`Unavailable` 仍返回 `MODEL_UNAVAILABLE`;HTTP 状态(422 / 503)与端点行为不变,仅把错误码拆细,`resolve_error_codes_are_stable` 同步断言三码两两不同。
- 影响范围:`server-rs/crates/module-runtime/src/agc_model_access.rs`、`server-rs/crates/api-server/src/llm/{mod,model_access}.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §11.4、本文件。
- 验证:`cargo test -p module-runtime --lib agc_model_access`;`cargo test -p api-server llm::tests`。
## 2026-10-03 会员响应 token 反序列化前向兼容
- 背景:`shared-contracts` 的 5 个会员 token 枚举同时被入参 / 响应 DTO 复用,此前响应侧也严格反序列化;后端一旦新增 plan / status / cycle 取值,AGC Tauri shell 的 `read_profile_recharge_center` / `read_profile_membership_upgrade_quote`(`account_api.rs` 用 `serde_json::from_value` 解析整份响应)就会得到 `响应格式无效`,整个钱包页不可用。
- 决策:只给**响应** DTO 的 token 字段挂 `#[serde(deserialize_with = "...")]` 做前向兼容,未知取值 fail-open 兜底到已知最高档(plan→`max`、status→`active`、cycle→`yearly`、model_access→`full`;真实权益 / 下单金额以后端重算为准,避免把付费会员显示成非会员后引导重复购买)。**入参** DTO 不改,非法 token 仍在反序列化即拒;不改枚举本身的 `Deserialize`,避免 `Unknown` 变体污染入参与穷尽 match。
- 影响范围:`server-rs/crates/shared-contracts/src/runtime.rs`(4 个 helper + `ProfileMembershipPlanResponse` / `ProfileMembershipResponse` / `ProfileMembershipUpgradeQuoteResponse` 的 8 个字段)、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §2.5。后台 DTO(`AdminProfileMembershipPayload` / `AdminMembershipOrderChangePayload`)只有 TS 消费方,不做 Rust 侧处理。
- 验证:`cargo test -p shared-contracts --lib` 106 passed(含 `membership_response_tokens_tolerate_unknown_values_from_newer_backend`、`membership_request_tokens_stay_strict_for_unknown_values`)。
## 2026-10-03 会员错误码 `ProductIdUnparsable` 拼写修正(未上线,直接破坏性改名)
- 背景:`RuntimeProfileMembershipErrorCode::ProductIdUnparsable` 拼写有误,生成绑定与对外 `error.code = "product_id_unparsable"` 同错。
- 决策:该 PR 尚未上线,不留兼容,直接改名为 `ProductIdUnparseable`,wire 串同步改为 `"product_id_unparseable"`;重新生成 SpacetimeDB 绑定。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active/mapper/runtime_profile.rs,module_bindings/runtime_profile_membership_error_code_type.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、设计文档 §5.8、本文件(含上文旧条目中的同名标识符同步更正)。
- 验证:`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-client` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p spacetime-module` 271 passed。
## 2026-10-03 会员结构体也走 ts-rs 生成类型(去前端手写漂移)
- 背景:会员 token 枚举早已 `ts(export)`,但承载它们的结构体(`ProfileMembershipPlanResponse`、`ProfileMembershipResponse`、`ProfileRechargeCenterResponse`、`ProfileRechargeProductResponse`、`ProfileRechargeOrderResponse`、`ProfileMembershipUpgradeQuoteResponse`、`ProfileMembershipPlanAdminListResponse`、`AdminUpsertProfileMembershipPlanRequest`、`ProfileDailyFreePointsResponse`、`ProfileMembershipUpgradeQuoteRequest`)仍是前端手写,字段与后端可能漂移。
- 决策:这 10 个结构体补齐 `ts_rs::TS` + `ts(export)`;64 位整数字段统一补 `ts(type = "number")`(ts-rs 默认 `bigint`,JSON 实际是 number)。`packages/shared/src/contracts/runtime.ts` 与 `apps/admin-web/src/api/adminApiTypes.ts` 改为 re-export 生成类型,仅前端需要收窄的 `kind` / `status` 联合用 `Omit` 组合保留;`ProfileRechargeCenterResponse` 的 `pointProducts` / `latestOrder` 同样收窄。
- 影响范围:`server-rs/crates/shared-contracts/src/runtime.rs`、`packages/shared/src/contracts/generated/*`(新增 10 个文件)、`packages/shared/src/contracts/runtime.ts`、`apps/admin-web/src/api/adminApiTypes.ts`、设计文档 §2.5。
- 验证:`npm run check:generated-bindings`(shared-contracts 23 个文件);根 `npm run typecheck`、`apps/admin-web` 与 AGC shell 主 `tsc` 通过;`cargo test -p shared-contracts --lib` 106 passed;`PlatformProfileRechargeModal` / `usePlatformProfileCenterController` vitest 14 passed。
## 2026-10-04 AGC 窗口标题栏项目标签改认「当前项目」,运行中项目降级为圆点与徽标
- 背景:issue #618。窗口标题栏那枚标签原来被「正在运行的项目」整块接管——`WindowChrome` 只要存在活动回合或运行快照读取失败,就把当前工作区标题换成 `ActiveProjectRunsPanel`(titlebar 形态),主文案取开始时间最晚的在跑项目。于是「当前项目没在跑、后台别的项目在跑」时,标签显示的是别的项目(现场:当前项目 `gameagent-ff7f3240`,标签 `gameagent-c3af9c7e`),用户以为自己开错了项目;读取失败时标签还会变成「正在运行的项目读取失败」,同样顶掉当前项目名。
- 决策(主文案):标签主文案恒为当前项目名,与当前项目是否在跑回合无关;不在项目内(首页 / 项目组 / 模板库)才回落到最后开始的在跑项目(该回落按视图门控,见 2026-10-05 条目)。名字由 `WorkspaceLauncher` 经 `WindowChromeActiveProjectRuns.currentProjectName` 发布(`currentProjectContext.projectName`,即清单项目名)。这同时消掉了同一项目「有回合在跑显示目录名、没在跑显示项目名」的两种口径。
- 决策(运行态):圆点 + 运行中数量徽标(≥1 即显示)+ 展开菜单;菜单里当前项目那一行用与标签同名的项目名,meta 首位带「当前」标记,`aria-current` 保留。读取失败改成告警色圆点 + 菜单内一行结论;只有既没有在跑项目、又没有当前项目可显示时才保留原来的纯提示。
- 有意未做:后台项目在菜单里仍是目录名(快照 `projectName` 由 Rust 取 `thread_id` 目录名)。改成清单项目名要在 `ThreadManager` 的 claim 路径里读 `.agent/manifest.json`,等于给纯内存模块加锁内文件 I/O,本轮不做。
- 影响范围:`apps/ai-game-creator-shell/src/components/WindowChrome.tsx`、`src/components/windowChromeContext.ts`、`src/features/app-shell/ActiveProjectRunsPanel.tsx`、`src/features/app-shell/WorkspaceLauncher.tsx`、`src/styles.css` 与两个定向用例文件;同步修正主规范 2026-09-15 段的标题栏条目、里程碑文末修订与实施计划 `修改顺序` 条目。不改 Rust、快照命令字段、公共 API 与持久化。
- 验证方式:`npx vitest run apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx apps/ai-game-creator-shell/tests/WindowChrome.test.tsx apps/ai-game-creator-shell/tests/workspaceWindowSync.test.tsx` → 22 passed(三条新回归:别的项目在跑时标签仍是当前项目、当前项目自己跑时列表带「当前」、读取失败不顶掉当前项目名);评审后续修订后为 25 passed,见下条。全量 `npm run test -- apps/ai-game-creator-shell/tests` 与真实 Chromium 冒烟见 PR。
- 关联:issue #618、`docs/project-memory/plans/【里程碑】Direct回合跨页面生命周期与运行中项目可见性-2026-09-15.md`(2026-10-04 修订)。
## 2026-10-04:Direct 普通操作与交付复核解耦
- 普通工具准入只检查原回合活动状态、时间预算、并发和既有权限,不要求先登记交付合同;成功/失败/取消只结算本次操作,删除全局 Draining 与执行/返修批次计数。
- 验证失败和源码漂移影响对应证据,不阻断无关工作。远端不确定结果沿资源自身 operation/幂等记录核对;本地执行器失控或持久状态损坏仍结束回合。
- 保留累计执行时间、整轮墙钟、原生执行前审批、关闭时清理与原回合写入/付费提交检查。模型执行结束时先关闭准入,确认清理后才允许交付反馈继续;普通失败不进入关闭阶段。
- `validation.maxRuns` 只保留交付回复复核用途;合同的创建与两类要求简化见下条决策,视觉/玩法判据保持原样。图片工具仍等待结果,Codex 调度不变。
- v2 执行账本只对白名单 v1 字段迁移,保留预算与终态,不恢复旧活动权限;没有可信时间记录的更早项目侧账本不授予同回合新预算。
- 权威边界与验收入口:[AI 游戏创作智能体 App 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#direct-操作控制2026-10-04)。Windows Job 退出证明仍须由 Windows 环境验收。
## 2026-10-04:Direct 合同按用户意图登记、正常响应结束后复核
- 合同自动检查只在模型正常结束响应后触发;删除操作结算与补丁成功触发的提前封口,状态查询保持只读评估。异常终止与预算耗尽仍独立处理,不以证据齐备覆盖失败事实。
- 提示 Agent 在用户要求制作、完成或交付游戏时登记合同,由 Agent 理解用户意图;首次输入也按同一用户意图条件登记,提示与技能不另设空项目或首次输入的免登记说明。无合同既不阻断普通操作,也不阻断正常结束。
- 删除 artifact/command 合同要求及宿主自动补入项,保留普通文件、命令与构建能力。视觉/玩法的现有判据和证据真实性校验不改,仅在主动注册后应用原有新 Web 视觉/玩法补充;用户于 2026-10-05 确认这两类检查保持现状。
- 旧未完成合同不得因过滤退役要求变成成功;保留旧预算、终态和操作身份,版本迁移与恢复有独立验收。
- 权威规则:[Direct 合同规则](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#宿主验收与执行许可合同)。运行时已实现;验证边界保留在主规范,不以协议夹具代替真实模型或 Windows 专项证据。
## 2026-10-05:Direct 重构手动 GUI 检查与范围收敛
- 用户确认手动 GUI 检查已完成,[PR #608](https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/608) 已合入;已完成的临时里程碑和实施计划删除,持久规则及验证边界以主规范为准。
- 视觉与玩法检查保持现状,包括既有双端、固定场景及证据真实性校验;本次不继续重构这两类要求。
- 手动 GUI 确认与专项平台证据分别记录;未提供的 Windows 用例明细不推定为通过。
## 2026-10-05:Direct 受控归属退出与终态报告
- 回合收尾和合同完成使用平台可证明的受控归属退役:Windows 为自有 Job,Unix 为受控进程组。进程组成功退出不再被完整子树标志误判为中断,但不宣称逃逸后代已退出;缺失或失败的清理仍阻止完成。
- Linux 排除僵尸时必须保守处理不完整扫描;数字 PID 的读取/解析不确定性不能成为组已空的证据,已确认消失的 PID 可以忽略。
- 正常关闭连接的迟到通知不创建或覆盖成功回合的终态报告;无合同收尾新产生的预算报告优先返回,其他真实关闭错误保留。
- 权威边界见[Direct 合同规则](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#宿主验收与执行许可合同)。视觉/玩法判据和预算值不变。
## 2026-10-05:AGC 标题栏项目标签评审后续(空态、视图门控与文案)
- 背景:对 issue #618 的标题栏修复做 PR 评审,发现四处与文档或组件自身注释相反的行为,逐条修正;#618 现场(当前项目没在跑、后台别的项目在跑)的结论不变。
- 面板形态误报读取失败:`ActiveProjectRunsPanel` 的空态早退只判「没有在跑回合」,把「快照读取成功、只是没有在跑回合」渲染成「未能读取正在运行的项目」。补回 `!readFailed` 守卫;`panel` 是默认 prop,唯一生产调用方 `WindowChrome` 恒用 `titlebar`,但组件注释对外宣称面板布局可复用。
- 会话残留的项目身份:`currentProjectContext` 是会话级状态(离开工作台不清空),活动回合快照却是全局轮询;照搬上下文会让首页 / 项目组 / 模板库上的标签一直写「当前项目:<上次打开的项目>」,徽标数字却属于别的项目,也让「不在项目内回落到最近启动的在跑项目」实际不可达。发布时按 `launcherView === 'project-development'` 门控 `currentProjectPath` / `currentProjectName`,与同文件窗口标题 effect 同口径。
- 文案去重:读取失败结论曾在菜单头部与 `role=status` 行连排两遍,头部改为只报数量;回落态触发文案原为「正在运行的项目:X;正在运行 N 个项目」,拆成「作用域 + 运行状态」(回落态作用域记为「最近运行的项目:X」,状态位「共 N 个项目在运行」);数量徽标补 `title` 说明数字含义。
- 仍有意不做:后台项目在菜单里是目录名;判定 `aria-current` 的路径匹配仍是 `projectPathsMatchForInvalidation`;不改 Rust、快照命令字段、公共 API 与持久化。
- 验证方式:`npx vitest run apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx apps/ai-game-creator-shell/tests/WindowChrome.test.tsx apps/ai-game-creator-shell/tests/workspaceWindowSync.test.tsx` → 25 passed;对照旧实现分别临时禁用 `launcherView` 门控、去掉面板空态守卫,对应用例各自转红。真机运行时证据仍缺,见里程碑文末「仍未取得证据」。
- 关联:issue #618、本文件上一条(2026-10-04 标题栏项目标签)、`docs/project-memory/plans/【里程碑】Direct回合跨页面生命周期与运行中项目可见性-2026-09-15.md`(2026-10-05 修订)。
+128 -2
View File
@@ -2,6 +2,119 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 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 项目命名」改成进「生成任务」列表后,第一次读账本(工作台打开项目就会读)就把那条还在跑的命名任务标成**失败**;随后前端的终态推进被拒(记录已终态)。
- **根因**:账本的中断收口判据是「非终态 + 不在本进程 live 集合」= 上次运行的残留。素材生成任务由 Rust 后台任务在 `start` 里先登记 live 再落账本;项目命名任务由前端驱动(`enqueue` / `update` 两条 IPC),没有登记 live,于是任何一次 `list_local_project_asset_generations` 都把它当成残留收口。
- **处理(现行口径)**:账本记录加 `taskType`(`asset-generation` / `project-naming`,缺省 `asset-generation`)、`kind` 变可选;schema 升 `agc-asset-generation-task.v2`,读取同时接受 v1 与 v2(v1 记录按素材任务读回)。`enqueue_local_project_naming_task` 写账本前登记 live(写盘失败只回滚本轮插入的那条),`update_local_project_naming_task` 在非终态时幂等登记、终态写盘成功后摘除。命名没有素材可交叉核对,中断收口一律 failed 且文案与素材口径区分(不含「目标素材」)。
- **边界**:命名任务的结论文案(已应用 / 用户已改名跳过 / 失败保留兜底名)由命名链路给出;账本只是展示旁路——`enqueue` / `update` 失败只写诊断日志,绝不影响建项、命名与首轮创作。前端按 `taskType` 把命名记录从素材任务列表里剔除(素材分支会按 `kind` 解析),命名行固定展示名「AI 项目命名」,只在终态显示结论。
- **边界(兼容是单向的,必须写清)**:**新构建读旧账本 OK,旧构建读新账本会失败**——命名记录写 `"kind": null`,而旧构建(`AssetGenerationTaskRecord.kind` 必填且 `read_ledger` 不校验 schema)解析含命名记录的账本会**整份失败** → 面板报「生成任务列表读取失败」,且同一次读里在途的素材任务会被按「上次运行中断」收口为失败。触发条件只有「同一项目先被新构建写过命名任务、之后又被旧构建打开(降级或新旧混跑)」。潜在的低成本正向兼容方向(**未实施,需先确认**):命名记录也写一个合法 `kind`(例如 `unknown`)只靠 `taskType` 区分——旧构建会把它渲染成一张名为「AI 项目命名」的素材任务卡,但账本能解析、不会误伤在途素材任务。
- **边界(前端读快照的时序)**:命名行的推进发生在本组件之外,刷新只能靠「变更事件 + 一次受控快照读取」。**必须先订阅再读**(与素材队列「先订阅后派发」同口径):先读后订阅时,落在这一次 IPC 往返里的终态事件没有消费者、直接丢掉,行会永久停在「命名中」;订阅失败时退化为「进项目读一次 + 面板打开时补读」。补读兜底必须有,但**有门控**:仅当列表里确实存在一条在途命名行时才读——没有命名任务的项目不该因为打开面板就多打一次账本 IPC,那会改变「轮询计数」这类外部可观测行为(既有生成任务用例正是按轮询次数推进 mock 的)。
- **边界(update 的门禁与终态语义)**:`update_local_project_naming_task` 与 `enqueue` 共用 `project.rename` 权限位,并校验记录 `project_id` 与项目一致;记录一旦终态只接受**同状态幂等重放**,改写成另一种状态(含 `completed → failed`)一律 Err 且不写盘——否则一次迟到的排队/失败事件会覆盖已落定的命名结论。
- **判据/取证**: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-03 后台异步回填被「进项目」覆盖:AI 项目名只在兜底名仍成立时改
- **现象**(Issue 599):首页建项把 AI 命名改成「与建项并行、结果后台回填」后,偶尔工作台标题停在兜底名(`GameAgent 项目 <短 id>`),而磁盘 manifest 已经是 AI 名字。
- **原因**:改名落盘很快,但写「当前项目上下文」的那次 `setCurrentProjectContext` 被 `enterProjectDevelopment` 随后的兜底上下文覆盖——AI 比进项目更快时,回填写入时上下文里还没有这个项目(`current` 为 null 或仍是上一个项目)。同一原因下 `refreshRecentWorkspace` 也会被丢弃:最近项目行只在 `recentWorkspacesRef` 已包含该路径时才更新状态,而路径是进项目时才登记的。
- **处理(现行口径)**:后台改名任务里,「写项目上下文 + 重检最近项目行」这两步必须等「建项主体收尾」(进项目成功、失败或让位于别的项目)的信号再执行;改名本身照旧立即落盘。回归用 promise 闸门卡住 `get_local_game_preview_status` 复现顺序,不用 sleep。
- **判据/取证**:`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 随包 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 上其实带着编辑器产物。
- **原因**:`apps/ai-game-creator-shell/src-tauri/resources/plugins/` 是 gitignored 但被 dev / 发布 / 门禁多流程共用的目录,它的**档位**(staging 里放了哪些编辑器产物)必须与本次 cargo 调用实际生效的 feature 组合一致。`apps/ai-game-creator-shell/src-tauri/Cargo.toml` 的 `[features] default =` 是空的,而 `scripts/check-generated-bindings.mjs` 对 AGC 用的是裸 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`(**不带任何 feature**);`build.rs` 按「当前 TARGET + 已启用 `CARGO_FEATURE_*`」构造 plugins 允许集合,于是为 windows 编辑器 feature 准备的 `origin: prepared` / `libraryStaging` 产物全成了「未声明文件」。
- **关键坑(假通过)**:`resources/**` **不是**构建脚本声明的输入(设计如此,避免每次资源变化都重编),所以缓存命中的 `cargo test` **根本不会重跑 `build.rs`**,校验被整个跳过。实测:树上带着未声明的 unity 产物时,门禁仍以 2.33 秒 exit 0「通过」;`touch apps/ai-game-creator-shell/src-tauri/build.rs` 强制重跑后才暴露。
- **处理(现行口径)**:校验 / 无 feature 的消费方先复位到 featureless:`npm run agc:bundled-resources:prepare -- --features=`。要带编辑器能力的本地 AGC:`npm run agc:bundled-resources:prepare -- --target x86_64-pc-windows-msvc --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute`(`cocos-editor-bridge.dll` 的 native payload 另需 `cocos-editor-injection`)。要求门禁**真校验**过,先 `touch apps/ai-game-creator-shell/src-tauri/build.rs` 再跑门禁。
- **判据/取证**:复位后强制重跑那次输出 `生成绑定校验通过:shared-contracts(1 个文件)` / `生成绑定校验通过:ai-game-creator-shell(104 个文件)` / `生成绑定校验通过:合计 105 个文件与 Rust 声明一致`(exit 0)。featureless 判据:`resources/plugins` 只有各插件 `plugin.json` + `src/`(cocos 另有 `panels/`),不存在 `Agc.Unity.Attach.exe` / `agc_godot_editor.dll` / `cocos-editor-bridge.dll`。
- **边界**:这是同一工作树里多人共享的档位——featureless 是无 feature 构建与并发 cargo 运行的前提,带编辑器产物会让它们失败,反之亦然。`prepare-bundled-resources.mjs` 的原子替换要 rename `resources/plugins`,撞上外部目录句柄会 `EPERM` 并把 staging 留在 `resources/plugins-staging-<pid>-<hex>`(`.gitignore:61` 已声明该模式),确认无并发进程后重试即可。
- **关联**:`apps/ai-game-creator-shell/src-tauri/build.rs`(`validate_staged_plugins` / `validate_prepared_payloads`)、`src-tauri/build_support/package-layout.json`、`apps/ai-game-creator-shell/scripts/{prepare-bundled-resources.mjs,cargo-features.mjs}`、`scripts/check-generated-bindings.mjs`;另见本文件「随包资源的写入方按产物来源分界」「AGC 随包资源的布局只能改声明文件」「构建期 staging 撞上不装 npm 依赖的 Linux 门禁」三条。
## 2026-10-03 工作树位于 `.worktrees/` 下时前端 Vite 被自己的忽略规则整棵排除:改代码不热更
- **现象**:在 `.worktrees/<name>/` 里的工作树改主站或后台前端源码,浏览器看不到任何更新;重启 Vite 也不生效(本轮有人重启 3 次才发现不是缓存问题)。
- **原因**:根 `vite.config.ts`(`ignoredWatchGlobs`,约 461-468 行)与 `apps/admin-web/vite.config.ts`(约 19-24 行)的忽略列表都含 `'**/.worktrees/**'`。工作树本身就在 `.worktrees/` 目录下,该 glob 会匹配工作树内的**每一个文件**,等于把整个项目排除出 watch——不报错、不提示,只是永远不触发 HMR / 重建。
- **影响面**:主站 web 与 admin-web 都中招(两者共用这条规则);`npm run dev:web` 走 `scripts/vite-cli.mjs`,它只是转发到 Vite 自带 bin、用的是同一份根配置,所以经 CLI 入口启动也一样。`apps/ai-game-creator-shell/vite.config.ts` 只忽略 `'**/src-tauri/target/**'`,**不受影响**。
- **处理方向(本轮未改配置)**:忽略规则必须只排除**其它** worktree 而放行当前 root——例如按真实仓库根计算,形如只忽略 `<repoRoot>/.worktrees/*/` 且显式排除当前 root;或当项目 root 自身已落在 `.worktrees/` 内时不注入该条。不要保留无条件的 `'**/.worktrees/**'`。
- **验证方向**:修改 `src/**` 一句话后,主站与 admin-web 各自应打印 HMR 更新(修改后立即生效),而不是只在重启后才生效;必要时打印生效的 watch 忽略集合确认不再覆盖当前 root。
- **关联**:`vite.config.ts`、`apps/admin-web/vite.config.ts`、`apps/ai-game-creator-shell/vite.config.ts`、`scripts/vite-cli.mjs`。
## 2026-10-03 同一工作树并发拉起多份 dev 栈:`.app/dev-stack.json` 互相覆盖,启动兜底清扫会反杀健康栈
- **现象**:在同一工作树里再开一个 `npm run dev` 之后,AGC 侧报「后端归属校验失败」,或前端代理连到别的端口(「后端端口记错」);更严重的是新会话启动后,`8082` / `8083` 上原本健康的后端被清掉,旧会话随即报连接失败。
- **原因**:`.app/dev-stack.json` 是**全工作树单文件**(`scripts/dev.mjs` 的 `resolveDevStackStatePath()` → `<repoRoot>/.app/dev-stack.json`,`scripts/dev-all.mjs` 与若干 e2e 脚本也读它),每个 `DevRunner` 都整份覆写快照,端口、SpacetimeDB data-dir 与 instance id 只保留最后写入者,于是两份并发栈互相覆盖实例信息。同时 `dev.mjs` 在启动/退出时会按身份兜底清扫 `stopWindowsWorktreeBackendProcesses`(`api-server.exe` 绝对路径 + SpacetimeDB `--data-dir`),这是**按工作树**而不是按会话匹配的:其它会话留下的半死栈一旦重启,就会把当前健康栈一并收走。
- **处理(现行口径)**:同一工作树保持**单栈**;确需并发时用显式端口参数(`--api-port` / `--web-port` / `--admin-web-port` / `--spacetime-port` 等)错开,并接受状态文件只有一个「最后写入者」。清理残留必须按**端口 → PID → 命令行**确认归属,再杀该 PID 的整棵进程树;不要 `taskkill /IM node.exe`(会误伤其它会话与 IDE 的 Node 进程)。
- **排查顺序**:先比对 `.app/dev-stack.json` 的 status / 端口与实际监听(`Get-NetTCPConnection -State Listen -LocalPort ...`)是否一致,再用 `Get-CimInstance Win32_Process` 按本工作树 `server-rs\target\debug\api-server.exe` 路径与 SpacetimeDB `--data-dir` 核对归属;不要因为 `/healthz` 返回 200 就认定后端属于当前会话。
- **关联**:`scripts/dev.mjs`(`resolveDevStackStatePath` / `stopWindowsWorktreeBackendProcesses`)、`scripts/dev-windows-process.mjs`、`scripts/dev-all.mjs`、`scripts/check-game-distribution-ratings-e2e.mjs`;另见本文件「`npm run agc` 的 Ctrl+C 不能只依赖 shell 包装层与端口健康检查」与 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
## 2026-10-03 Windows 上会弹阻塞模态的启动失败用例挂死本机分片 runner
- **现象**:Windows 本机 `npm run ai-game-creator-shell:check:rust:shell -- --shard-index=3/4` 长时间无进展(可到 2400s 超时);单独跑那条用例同样超时——进程还在、CPU 不再增长、也没有子进程,形态很像「测试死锁」或「分片器坏了」。
- **原因**:`apps/ai-game-creator-shell/src-tauri/src/main.rs:2042` 的 `startup_log_slot_fail_without_path_still_reports_instead_of_going_silent` 调 `StartupLogSlot::fail()`(约 1854 行),而 `fail()` 会走 `show_startup_error_dialog()`;Windows 实现(约 1740 行)用的是 `MessageBoxW(..., MB_OK | MB_ICONERROR | MB_SETFOREGROUND)`,是**阻塞模态**,没有人点「确定」就永不返回。`STARTUP_ERROR_DIALOG_SHOWN`(约 1534 行)只在同一个进程内保证「只弹一次」,对测试用例没有任何豁免。分片 runner 用 `--exact <名单> --test-threads=1` 串行执行,一条挂死就整片挂死。
- **影响面**:Linux CI 走非 Windows 分支(约 1778 行)只写 stderr,**不受影响**;这是本机专属现象,不要据此判定 Rust 代码或分片规则有问题。
- **处理(本机绕过)**:改用等价分块跑,而不是整片上阵——同一个测试二进制、同一 `--exact <名单>` 与 `--test-threads=1` argv、同一 TMPDIR 隔离,把这条阻塞用例排除或单独限定。
- **判据/取证**:单独执行 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- --exact startup_log_slot_fail_without_path_still_reports_instead_of_going_silent --test-threads=1` 本机同样挂住;对照非 Windows 分支只产生 stderr 文案。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/main.rs`、`apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs`;另见本文件「AGC 壳 Rust 套件按「一片一 job」拆分,且分片必须自校验覆盖」条中此前记为「本机跑到约 20 分钟后长时间无进展」的同一现象。
## 2026-10-03 AGC 画布「引用」死按钮:window 事件的消费者挂在一个只在另一条链路赋值的 ref 上
- **现象**(Issue 602):AGC 资源画布选中一张已登记素材,选中工具条点「引用」(图标 `@`、可见文案与 `title` 都是「引用」)没有任何反应——聊天输入框里不出现 `@素材名` 芯片,也没有任何提示。普通项目(`directProjectMode`)必现,立项策划项目(`planningStartMode`)复现不出来;把素材卡拖到对话栏的批量引用同样没反应。
@@ -43,16 +156,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」。
@@ -6229,6 +6345,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`;首页仍会阻止自动命名、建项和首次生成。
@@ -6332,11 +6449,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` 复核。
@@ -6350,3 +6466,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`。
@@ -58,6 +58,7 @@
- 对已明确退役且无现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,直接清理实现、专属测试和说明,将权威文档更新为当前状态;历史由 Git 保存。公开契约、持久化数据和正式迁移按实际需求保留最小兼容及对应测试。
- 修改 `/api/external/v1` 时,同批更新 `docs/openapi/genarrative-external-v1.openapi.json` 与契约测试。
- 修改 SpacetimeDB schema 时遵守字段追加/default 约束,同步 migration、表目录、生成绑定,并运行 schema 检查;删除、改名、重排或改类型前先确认迁移计划。
- `shared-contracts` 响应 DTO 只增不删:新增字段一律可选(`Option`,或带安全 `#[serde(default ...)]`),身份 / 状态字段保持必填;删除、改名、改类型或改必填属性前先确认版本化计划,不保留仅服务旧客户端的 legacy shim。详见后端架构文档「shared-contracts DTO 变更规则」。
- 日志不递归输出完整配置、应用状态或 provider client;新增字段默认不进入安全摘要。所有 provider 配置类型(`LlmConfig`、`OssConfig`、`WechatConfig`、`WechatPayConfig`、`MattingConfig`、`VolcengineSpeechConfig`、`Hyper3dSettings`、`VectorEngineImageSettings`、`VectorEngineAudioSettings`)都必须手写脱敏 `Debug`:只输出枚举、数值、布尔、有界标识与 `<redacted>` 占位,凭据、私钥、路径与 URL 一律不递归格式化,并各带一条哨兵值回归用例(`cargo test -p platform-* debug`)。新增同类配置必须照此办理,不允许直接 `derive(Debug)`。
- HTTP 横切能力集中在 Axum/Tower 中间件:正常与降级路由复用追踪层;指标与 trace 使用 `MatchedPath` 模板及固定兜底,不把请求 ID、实际资源 ID 或 query 放入指标标签。在途请求通过 RAII guard 覆盖 Future 取消与 panic unwind;请求执行和响应体存活分别计量,不能把 handler 耗时当作 SSE 全生命周期。
- 业务依赖在组合根显式装配,Axum `FromRef` 只抽取可浅拷贝的窄能力。项目元数据与 External API 鉴权不持有完整 `AppState`,测试经相同接口注入替代依赖。集中鉴权仍保留方法级 fallback、公开入口、MCP 和 body limit 顺序;Provider span 跳过完整参数,不隐藏计费、重试、幂等或事务规则。