Merge remote-tracking branch 'origin/master' into style/game-play
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m26s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Backend tests (pull_request) Successful in 4m27s
Project CI / Frontend tests (pull_request) Successful in 2m21s
Project CI / Native shell tests (pull_request) Successful in 6m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m53s
Project CI / Repository checks (pull_request) Successful in 2m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m1s

# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
2026-09-30 13:38:14 +08:00
16 changed files with 102 additions and 42 deletions
@@ -37,7 +37,7 @@
- `node --test build-release.test.mjs release-oss.test.mjs prepare-macos-codex.test.mjs cargo-features.test.mjs`:64/64 通过(新增渠道身份、身份注入与渠道 DMG 首装选择三条用例)。
- `node apps/ai-game-creator-shell/scripts/check-config.mjs`、`npm --prefix apps/ai-game-creator-shell run typecheck`:通过。
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- config::private_path_elevation_policy_tests`:12/12 通过。
- `AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug`:Tauri 接受派生的 `productName` / `identifier` 并完成构建;产物字符串实测 `陶泥儿 Release` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0。
- `AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug`:Tauri 接受 `productName=陶泥儿` / `identifier=world.genarrative.ai-game-creator.release` 并完成构建;产物字符串实测 `陶泥儿` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0。
- `cargo fmt --check`(AGC 壳)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`:通过。
- 未执行:真实渠道打包(需要签名私钥与发号/上传授权)、双渠道真机安装与并存、macOS 节点实跑。
@@ -31,7 +31,7 @@
## 验收标准
- [x] 默认渠道的 `productName` / `identifier` 与基线配置逐字一致,既有安装与升级链不断(`check-config.mjs` + 渠道身份用例)。
- [x] `release` 与自定义渠道派生独立 `productName` 与 `identifier`,与 `dev` 可在同一台设备并存(源码级;真机安装见下方未完成项)。
- [x] `release` 复用正式 `productName`、自定义渠道派生带后缀的 `productName`,所有非默认渠道派生独立 `identifier`;与 `dev` 可在同一台设备并存(源码级;真机安装见下方未完成项)。
- [x] 渠道产物(NSIS `.exe`、`.app.tar.gz`、DMG)与首装包选择跟随渠道身份且唯一匹配(发布脚本用例)。
- [x] 非默认渠道的客户端数据目录、WebView2 目录与窗口标题跟随渠道身份(构建产物字符串实测 + 运行期标题取产品名)。
- [x] AGC 自有 AppData 目录的 ACL managed 范围覆盖全部渠道身份,且不扩大到相似前缀目录(Rust 定向 12/12)。
@@ -1,5 +1,11 @@
# 决策记录
## 2026-09-30 release 渠道移除产品名与包名后缀
- 决策:`release` 渠道的正式产品名统一为 `陶泥儿`,Windows NSIS、macOS DMG / updater 归档等由 Tauri `productName` 派生的包名不再包含 `Release` 文本;`identifier=world.genarrative.ai-game-creator.release` 与 `release-win` 更新分区保持不变。
- 边界:`dev` 继续显示 `陶泥儿开发版`;自定义渠道继续使用 `陶泥儿 <渠道显示名>`,因此清单 URL 仍需保留对合法百分号编码空格的兼容。
- 验证:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs`、`node apps/ai-game-creator-shell/scripts/check-config.mjs`、`npm run check:encoding`、`git diff --check`。
> 用途:只记录当前仍有效、会影响后续开发的长期技术、产品与协作结论;同一事实只保留一处当前口径。
> 维护:阶段过程、分支合并和当轮测试数字由 Git 追溯;实现依据以当前代码和最新专题文档为准。
> 格式:参见[决策记录格式](../README.md#决策记录格式),按需填写。
@@ -9157,3 +9163,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 本地假数据补齐:`scripts/admin-web-fake-api.mjs` 新增 `agc-models`、`game-distribution/games`、`profile/recharge-products`、`profile/redeem-codes`、`profile/tasks`、`agc/tracking-events` 夹具,并对 `profile/recharge-orders` 按 `AdminRechargeOrderEntryPayload` 的真实字段补齐(缺字段会让页面抛 `Cannot read properties of undefined`——后台没有 error boundary,整页会白屏)。**已知缺口**:充值管理页仍缺一处夹具字段(页面读 `undefined.find`),本轮没能出图;该页自身 21 条单测通过、类型检查通过,仅缺截图。
- 排序验证:共享组件新增用例覆盖「正序 / 倒序 / 取消 + aria-sort + 行序」;现场实测邀请码列表按「创建」排序:正序 `EXPIRED-CODE, BETA-CREATOR, TAONIER-VIP-2026`、倒序翻回 `TAONIER…, BETA…, EXPIRED…`,`th[aria-sort]` 依次为 `ascending` / `descending`。
- 边界(未完成):未跑生产后台构建与真实后台接口联调(截图用假数据);`apps/admin-web` 目前没有 error boundary,任何接口形状不符仍会把整页渲染清空(本次只加固了 `AdminAgcTrackingPage` 一处,其余页面同类写法未逐个排查);`AdminAgcTemplatesPage` 的列表面板是本次新增的外壳(原页面没有面板),视觉上多了白底卡片。
## 2026-09-24 移动端主站顶栏收成单行、个人中心弹窗统一走页面级 portal
- 背景:移动端主站(`PlatformEntryActiveFlowShell` 工作台壳层)顶栏在 `e3682fd06` 加入「下载客户端」后,`flex-wrap` 让品牌与下载/泥点/账户入口折成两行;同一壳层里 `.platform-desktop-layout` 带 `z-index: 1` 形成 stacking context,把 `portal={false}` 的弹窗封在这个 context 内,低于 `z-index: 60` 的固定底部主菜单——头像裁剪弹窗的「取消 / 上传」正好被「游戏 / 我的」压住点不到。
- 决策(顶栏):顶栏在所有断点都保持单行(`flex-nowrap`),窄屏靠两个降级点让位:`< 640px` 下载入口收起为 44×44 图标按钮(保留 `aria-label="下载客户端"`),`< 480px` 顶栏品牌只留 IP 标识(`platform-desktop-topbar__brand` 内隐藏 `platform-brand-logo__copy`)。品牌字标、下载文案的完整形态在 `>= 640px` / `>= 480px` 恢复,桌面端外观不变。
- 决策(弹窗层级):个人中心的独立弹窗(`SquareImageCropModal`、`PlatformProfileModalShell` 两个壳层)改回 `UnifiedModal` 默认的页面级 portal(挂到 `document.body`),不再用 `portal={false}` 内联渲染;层级由现有 `z-[80]` 决定,仍在鉴权 / 法务 / 预览等 `z-[110]` 以上弹窗之下。`PlatformStatusDialog` 保持内联(承载运行态内嵌遮罩,不在本次范围)。
- 影响范围:`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/components/creation-home/ClientDownloadEntry.tsx`、`src/components/common/SquareImageCropModal.tsx`、`src/components/platform-entry/PlatformProfileModalShell.tsx`、`src/index.css`。
- 验证:本地 Vite + Chromium 真机尺寸复核(320/360/375/390/414/480/639/640/768/1024):顶栏在 320–768 全部单行、右侧留白 16px;弹窗内「上传 / 保存」`elementFromPoint` 命中自身而非底部菜单,overlay 父节点为 `BODY`。定向 Vitest(`ClientDownloadEntry`、`PlatformProfileModalShell`、`PlatformActiveProfileView`、`PlatformEntryActiveFlowShell`、`UnifiedModalPortalTheme`)与 `prettier --check`、`eslint`、`tsc`、`check:encoding` 通过。
+21 -5
View File
@@ -2,6 +2,13 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 2026-09-30 Jenkins release 渠道环境污染 AGC 构建单测
- **现象**:Jenkins `Genarrative-Agc-MacOS-Build` 的 release lane 在执行 `build-release.test.mjs` 时,`release stages Node before Tauri...` 用例报 `Cannot read properties of undefined (reading 'nsis')`。
- **原因**:用例还断言 dev-only 的 `bundle.windows.nsis.installerHooks`,但 `resolveReleaseContext()` 默认读取进程环境;Jenkins 注入 `AGC_UPDATE_CHANNEL=release` 后,测试上下文变成 release。release 渠道只保留 Node 资源映射,没有 dev 的 NSIS hook,于是 `config.bundle.windows` 不存在。macOS 只是运行测试的宿主,测试实际覆盖的是 Windows target。
- **处理**:凡是断言特定渠道配置的测试,必须通过 `resolveReleaseContext(args, { AGC_UPDATE_CHANNEL: 'dev' })` 或等价夹具显式固定渠道,不能继承 Jenkins release 环境。
- **验证**:`AGC_UPDATE_CHANNEL=release node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs`。
## 2026-09-29 逐 delta 脱敏会吃掉段尾换行:DirectProject 汇报的 Markdown 表格整块失效
- **现象**:AGC 项目对话里 agent 汇报正文渲染错位——段落并进同一行、序号项挤成一段、Markdown 表格整块不渲染(表头 `| 素材 | 用途 | 路径 |`、`|---|---|---|` 与数据行以纯文本连在同一行里);同一段文本里的路径还会出现 `game<absolute-path>ame.js`、`assets<absolute-path>anvas-generated/...` 这种被切坏的占位符。重开项目读历史时同一段话渲染正常,很容易被当成渲染器偶发或模型写坏了 Markdown。
@@ -147,12 +154,12 @@
- **处理**:契约期望同步包含现有 OSS HTTPS 项,保留完整白名单精确比较;不得删除业务所需权限或改成任意 URL 放行。更新下载与素材直传是不同调用链,变更能力配置时同步检查调用方和契约。
- **验证**:运行 `npm run check:native-shells:contract` 与 `apps/ai-game-creator-shell/tests/assetDirectUpload.test.ts`;缓存完整性以实际 artifact 和导出日志为准,不能仅看上传 step 是否成功。
## release 安装包文件名中的编码空格不能按控制字符拒绝
## 旧版 release 安装包文件名中的编码空格不能按控制字符拒绝
- **现象**:release 环境点击“下载客户端”只显示无法获取最新版本,`GET /api/client-downloads` 返回 `502 UPSTREAM_ERROR`;dev 环境正常。
- **原因**:release 渠道产品名为“陶泥儿 Release”,NSIS 首装包名因此包含空格,OSS `latest.json` 中的 URL 使用 `%20`。`validate_download_url` 的百分号解码校验把 `0x20` 当成控制字符拒绝,Windows 清单被判非法;release macOS 清单当时又未发布,聚合后两端都无下载项并返回 502。
- **处理(现行口径)**:清单 URL 校验允许百分号编码的空格,继续拒绝其它控制字符、`/`、`\`、DEL、非法编码和跨目录文件名;不得通过改写真实产物名绕过校验。
- **验证**:`platform-oss` 用 `陶泥儿%20Release_0.1.110_x64-setup.exe` 做清单解析回归;线上修复后接口应返回 release Windows 下载项,macOS 未发布时进入 `unavailablePlatforms`。
- **原因(历史)**:旧版 release 渠道产品名带英文渠道后缀,NSIS 首装包名因此包含空格,OSS `latest.json` 中的 URL 使用 `%20`。`validate_download_url` 的百分号解码校验把 `0x20` 当成控制字符拒绝,Windows 清单被判非法;release macOS 清单当时又未发布,聚合后两端都无下载项并返回 502。
- **处理(现行口径)**:release 渠道现复用正式产品名 `陶泥儿`,不再生成带 `Release` 文本的包名;清单 URL 校验仍允许合法的百分号编码空格,以兼容自定义渠道产品名,同时继续拒绝其它控制字符、`/`、`\`、DEL、非法编码和跨目录文件名。
- **验证**:`platform-oss` 继续覆盖带编码空格的自定义渠道文件名,并用 `陶泥儿_0.1.110_x64-setup.exe` 做 release 清单解析回归;线上接口应返回 release Windows 下载项,macOS 未发布时进入 `unavailablePlatforms`。
- **关联**:`server-rs/crates/platform-oss/src/client_downloads.rs`、`apps/ai-game-creator-shell/scripts/build-release.mjs`、`docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md`。
## 客户端图标素材描述不能先包装再截断
@@ -166,7 +173,7 @@
- **现象**:在一台已经装了某个渠道 AGC 客户端的设备上安装另一个渠道的安装包,装完后旧客户端直接消失(安装目录被覆盖、卸载项被接管),更新端点、平台服务器与本地登录态一起换成新渠道的;两个渠道的客户端无法共存。
- **原因**:渠道此前只烘焙了 `plugins.updater.endpoints` 与 `VITE_AGC_PLATFORM_CHANNEL`(`apps/ai-game-creator-shell/scripts/build-release.mjs` 的 `createChannelConfig`),`productName` / `identifier` 用的是渠道无关的基线值。Tauri 的 Windows 安装目录与卸载项由 `productName` 决定,WebView2 数据目录与客户端数据目录由 `identifier` 决定,于是所有渠道落到 `%LOCALAPPDATA%\陶泥儿`、`HKCU\...\Uninstall\陶泥儿` 与 `%APPDATA%\world.genarrative.ai-game-creator`。
- **处理(现行口径)**:渠道进入安装身份,默认渠道保持基线身份不变,其它渠道派生 `<产品名> <渠道显示名>` 与 `<基线>.<渠道>`;身份与端点在同一次构建期 `--config` 注入。见决策记录 2026-09-21 条目。
- **处理(现行口径)**:渠道进入安装身份,`dev` 保持既有开发版身份,`release` 复用正式产品名但保持独立 identifier,自定义渠道派生 `<产品名> <渠道显示名>` 与 `<基线>.<渠道>`;身份与端点在同一次构建期 `--config` 注入。见决策记录 2026-09-21 与 2026-09-30 条目。
- **核对方式**:装完任渠道的包后看 `HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\<产品名>` 的 `InstallLocation`、`%APPDATA%\<identifier>` 与主程序窗口标题是否按渠道分开;同名安装目录或同名数据目录说明身份没有生效。
- **易错点**:只改安装包文件名或快捷方式名而不改 `identifier`,两个渠道仍会抢同一份 Agent Runner / 项目锁与登录态;反过来把渠道后缀加在默认渠道上,既有安装的升级链会断(客户端认不出旧安装)。相似前缀目录(如 `world.genarrative.ai-game-creator-backup`)不得进入提权 ACL 的 managed 范围。
- **关联**:`apps/ai-game-creator-shell/scripts/channel-identity.mjs`、`build-release.mjs`、`build-macos-ci.mjs`、`src-tauri/src/config.rs`。
@@ -6133,3 +6140,12 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **处理**:覆盖规则带上同样的祖先作用域,`overflow: hidden` 用两个选择器(单类名 + `.platform-desktop-shell--workbench .platform-tab-panel.platform-tab-panel--game-play`)一起写,既压得过 workbench 规则也不依赖文件顺序。
- **判据**:真实 Chromium(dev 栈 390x844 / 360x640 / 844x390 横屏矮视口)实测 `overflowY=hidden`、面板与整页横竖都不溢出,同时广场舞台仍是 `overflowY=auto` 且内容可滚(689px)。同类问题排查姿势:改 `.platform-tab-panel` 或任何 workbench 内元素的样式,先在浏览器里读 `getComputedStyle`,别只看类名/源码顺序。
- **关联**:`src/index.css`(`.platform-tab-panel--game-play` 与 `.platform-desktop-shell--workbench .platform-tab-panel`)、`scripts/check-game-distribution-web-e2e.mjs`、`src/components/game-distribution/gameDistribution.css`。
## 2026-09-24 移动端固定底部菜单会压住「内联」弹窗:`z-index` 高也点不到
- **现象**:手机端主站从头像入口上传头像,裁剪弹窗的「取消 / 上传」被底部「游戏 / 我的」菜单盖住,点按钮命中底部菜单;弹窗遮罩、面板本身都正常渲染,只有底部一条区域失去交互。
- **原因**:`.platform-desktop-shell--workbench .platform-desktop-layout` 带 `position: relative; z-index: 1`,**自成一个 stacking context**。底部主菜单(`.platform-mobile-bottom-dock`,`z-index: 60`)是 layout 的兄弟节点,而弹窗用 `portal={false}` 内联渲染在 layout 内部——弹窗自己的 `z-[80]` 只在 layout 的局部层叠里比较,整体被限制在 `z-index: 1` 之上、`z-index: 60` 之下。**在祖先 stacking context 内抬高子元素 `z-index` 无效,必须让弹窗跳出该 context**。
- **处理(现行口径)**:独立弹窗一律走 `UnifiedModal` 默认的页面级 portal(挂 `document.body`),不要用 `portal={false}` 内联渲染。目前只剩 `PlatformStatusDialog` 保留内联(运行态内嵌遮罩,且它渲染位置本就在 layout 之外)。
- **同时**:主站顶栏(`platform-desktop-topbar`)在窄屏必须保持单行——加 `flex-wrap` 后新增一个动作入口就会让品牌折到第二行;窄屏降级靠 `< 640px` 图标化下载入口、`< 480px` 只留品牌 IP 标识。
- **验证**:真机尺寸下用 `document.elementFromPoint(按钮中心)` 断言命中的是按钮自身而不是底部菜单(`overlay.parentElement === document.body`);顶栏断点矩阵(320–768)断言单行且无横向溢出。
- **关联**:`src/components/common/SquareImageCropModal.tsx`、`src/components/platform-entry/PlatformProfileModalShell.tsx`、`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/index.css`。
@@ -58,7 +58,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
- DirectProject 对话先在完整历史中按回合/原始 item 身份关联,再分页渲染;每个回合只有一个呈现入口。有流按 item `seq` 交替文本和工具,无流采用历史正文;禁止位置猜配或同时展示累计回复与 item 正文。流写入单调归并,收尾等待落盘任务,不按磁盘“最后一段”猜最终回复位置。详见 AGC 实施计划的“DirectProject 回合展示唯一归属”。
- 回合生命周期只由活动 client 回合快照和 Direct 事件恢复;Provider 的历史终态通知不能创建活动 client 回合。消息发送时间保存在历史信封,原始 item 不混入宿主字段;完成后的中间文本和工具默认收进“执行过程”,最终回复及失败提示保持可见。
- AGC 安装产品名基线为“陶泥儿”,由 Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述;默认渠道 `dev` 保持基线产品名与 identifier `world.genarrative.ai-game-creator` 不变,其它渠道派生 `陶泥儿 <渠道显示名>` 与 `<基线>.<渠道>`,让不同渠道的包体在同一台设备上并存而不互相顶掉(详见《AGC客户端更新检查与下载》的渠道与安装身份合同)。Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。内部可执行文件名保持稳定。
- AGC 安装产品名由渠道身份决定:`dev` 显示“陶泥儿开发版”,`release` 复用正式产品名“陶泥儿”,自定义渠道显示“陶泥儿 <渠道显示名>”;Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述,identifier 继续按 `<基线>.<渠道>` 派生,保证渠道数据与运行身份隔离(详见《AGC客户端更新检查与下载》的渠道与安装身份合同)。Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。内部可执行文件名保持稳定。
- 新 Web 游戏为 `game/` 下的 npm + Vite + Phaser 4.2.1 工程,使用包导入且允许其它依赖;npm 预览与导出只读取 dist,运行素材需纳入构建。单 HTML → Phaser 迁移固定走 DirectProject:文件落盘后先用受控 `project.bootstrap` 在 `game` 执行无参数 `npm install`,再用支持相对 cwd 的 `project.verify` 构建并确认 `game/dist/index.html`,已有单 HTML/Godot 不通过 JSON Generator 伪装成 npm 工程。