diff --git a/.gitignore b/.gitignore index c90efe5c..2885233d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ temp*build*/ /public/generated-character-drafts /public/generated-characters /.codex-temp +/.app/ /target/ /logs /server-rs/crates/*/logs/ diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 30602617..52ac9d5a 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -31,12 +31,54 @@ - 验证方式:`node --check scripts/dev-stack-port-utils.mjs`、`node --check scripts/dev.mjs`、`node node_modules/vitest/vitest.mjs run scripts/dev-stack-port-utils.test.ts scripts/dev.test.ts` 通过;Linux 下能看到 `[dev] port-range:` 与 `registry.json` 路径日志,自动分配从 `10000-10099` 起步,Windows 不出现注册表分配日志。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 -## 2026-05-27 生成页总进度圆弧锁定固定画布 +## 2026-05-30 创作流程统一化门禁扩展为跨玩法矩阵 -- 背景:多轮圆环角度微调后,`GenerationProgressHero` 的 SVG 圆弧仍会出现底部开口偏斜的问题,且圆环还会随着容器宽度伸缩,导致 UI 看起来时大时小、位置漂移。 -- 决策:共用 `GenerationProgressHero` 的 SVG 圆弧起始角固定为 `135deg`,轨道和橘黄色填充都从同一个对称起点 `rotate(135 200 200)` 出发;`270deg` 扫描角配合正下方 `90deg` 留空,圆环本体改为固定 `400x400` 画布,不再跟随页面宽度缩放,外层布局只负责定位,不负责改动圆环样式。 +- 背景:统一创作 / 统一生成门禁已经足够覆盖 Phase 2 的入口与壳层,但当前总计划已经推进到 Phase 3-6,继续只保留单页门禁会让 Phase 4 的特殊工作台、Phase 5 的结果页 / 作品架 / 公开详情和 Phase 6 的冻结验收没有统一入口。 +- 决策:`quality-gates/README.md` 继续保留单页门禁与 `dev-stack` 门禁,同时新增跨玩法回归 / 冒烟门禁,按 Phase 2 到 Phase 5 的最小验证集合分层执行;Phase 6 冻结前以这份矩阵为主,不再另外拆新波次。涉及入口配置、统一字段 spec、普通工作台、RPG / Bark Battle / 视觉小说特殊边界、发布 / 公开 / runtime 或本地 smoke 的变更,优先对照这份矩阵补齐验收命令。 +- 影响范围:`quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`、后续 Phase 2-6 玩法接入与冻结流程。 +- 验证方式:按矩阵执行 `npm run check:encoding`、`npm run typecheck`、`npm run admin-web:typecheck`、对应分期 `npm run test`、`npm run check:visual-novel-vn11`,以及需要时的 `npm run dev:api-server` + `/healthz` smoke。 +- 关联文档:`quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`。 + +## 2026-05-30 跳一跳结果页直达必须优先恢复作品而不是白屏 + +- 背景:跳一跳结果页已经接入统一壳,但如果用户直接打开 `/creation/jump-hop/result`,旧路径容易因为缺少 `draft` 恢复信息而看起来像白屏,误导成结果页坏了。 +- 决策:`PlatformEntryFlowShellImpl` 的跳一跳恢复顺序固定为 `profileId -> getWorkDetail`,再 `sessionId -> getSession`;两者都拿不到时必须展示 `跳一跳草稿未恢复` 恢复面板和 `返回创作`,不能继续留空白结果页。进入结果页的 smoke 允许恢复面板,但不允许纯空白。 +- 影响范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`。 +- 验证方式:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"`;手测 `/creation/jump-hop/result` 和 `/creation/jump-hop/result?profileId=`。 +- 关联文档:`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-05-29 一期统一创作页必须提供可见统一外壳 + +- 背景:`UnifiedCreationPage` 首版只暴露隐藏 spec 元数据并包裹旧玩法工作台,用户打开拼图创作页时仍只能看到旧工作台外观,无法验收“统一创作页”。 +- 决策:一期统一创作页(拼图、抓大鹅、敲木鱼)必须由 `UnifiedCreationPage` 提供统一标题栏、内容区、页面级纵向滚动和隐藏字段契约;字段元信息只留给测试和代码,不再额外作为可见 chip 占用首屏。玩法工作台只承载具体输入控件、上传、历史素材、校验和提交,不再各自渲染巨大入口标题。拼图、抓大鹅与敲木鱼的实现已经统一收口到 `src/components/unified-creation/workspaces/`,统一壳只依赖 `UnifiedCreationWorkspace`。敲木鱼右侧音效和功德面板不得再套内部滚动容器,移动端应自然跟随页面滚动。 +- 追加决策:`UnifiedCreationPage` 自己负责页面级滚动;拼图、抓大鹅、跳一跳和敲木鱼四条统一创作入口必须在同一页面壳内从统一标题、表单控件一路滑到提交按钮,避免工作台内部或右侧面板形成套滚动。 +- 影响范围:`src/components/unified-creation/UnifiedCreationPage.tsx`、`src/components/unified-creation/UnifiedCreationWorkspace.tsx`、`src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx`、`src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx`、`src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、玩法链路文档。 +- 验证方式:`UnifiedCreationPage` 测试应断言隐藏契约仍在但 UI 不再出现字段 chip;拼图和抓大鹅工作台测试应断言 `unifiedChrome=true` 时不再渲染旧巨大标题且仍保留表单输入;木鱼工作台测试或手测应确认敲击音效和功德词条不再停留在独立滚动窗内。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-05-31 统一创作壳扩展到跳一跳并接管页面级滚动 + +- 背景:最初的统一创作页只收口拼图、抓大鹅和敲木鱼,跳一跳仍通过独立工作台壳与独立生成壳渲染,导致用户在 `/creation/jump-hop` 看到的可见外壳与其它统一入口不一致。 +- 决策:`jump-hop` 也纳入统一创作壳与统一生成壳;`UnifiedCreationPage` 现在承担页面级滚动和统一标题栏,拼图、抓大鹅、跳一跳、敲木鱼四条入口都通过同一外壳承载各自工作台。`JumpHopCreationWorkspace`、`WoodenFishCreationWorkspace` 也补了 `unifiedChrome` / `showBackButton` 受控能力,避免双标题或双返回按钮。 +- 追加决策:`UnifiedCreationPage` 的统一页头现在承载唯一返回入口,工作台内部的返回按钮全部关闭,避免同一页面出现双返回按钮;`UnifiedCreationWorkspace` 统一把 `onBack` 透传给页头。 +- 追加决策:统一创作页内容区必须保持自然高度,页面级滚动只由 `UnifiedCreationPage` 外层承担,工作台内部只负责内容展开,不再额外包滚动壳。 +- 影响范围:`src/components/unified-creation/UnifiedCreationPage.tsx`、`src/components/unified-creation/unifiedCreationSpecs.ts`、`src/components/unified-creation/unifiedGenerationCopy.ts`、`src/components/unified-creation/workspaces/JumpHopCreationWorkspace.tsx`、`src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`server-rs/crates/shared-contracts/src/creation_entry_config.rs`。 +- 验证方式:`npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx`;`npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx`;`npm run test -- src/routing/appPageRoutes.test.ts`。 + +## 2026-05-31 统一创作编排层必须由 UnifiedCreationWorkspace 统一收口 + +- 背景:`PlatformEntryFlowShellImpl` 仍直接 lazy import 并渲染四个旧工作台分支,虽然统一创作页已存在,但入口壳层仍然依赖旧工作台分支。 +- 决策:新增 `UnifiedCreationWorkspace` 作为平台壳唯一依赖的统一创作编排层,由它内部按 `playId` 选择四条入口的真实工作台;平台壳层只再挂这一层,不再直接依赖旧工作台组件。旧工作台已移入 `src/components/unified-creation/workspaces/`,不再作为平台入口编排事实源。 +- 影响范围:`src/components/unified-creation/UnifiedCreationWorkspace.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、统一创作页相关测试与后续入口接入。 +- 验证方式:平台壳源码中不应再直接出现四个旧工作台的入口渲染分支;创作 Tab 与 `/creation/` 仍可正常进入对应工作台。 +- 关联文档:`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-05-27 生成页总进度圆弧锁定固定 SVG 坐标系 + +- 背景:多轮圆环角度微调后,`GenerationProgressHero` 的 SVG 圆弧仍会出现底部开口偏斜的问题;后来窄屏验收又发现固定 `400px` 外层宽度会让等待页右侧被裁切。 +- 决策:共用 `GenerationProgressHero` 的 SVG 圆弧起始角固定为 `135deg`,轨道和橘黄色填充都从同一个对称起点 `rotate(135 200 200)` 出发;`270deg` 扫描角配合正下方 `90deg` 留空。SVG 内部坐标系固定为 `400x400`,圆弧使用 `r=166` 和 `strokeWidth=18`;外层显示宽度以 `400px` 为上限,窄屏按父容器 `min(400px, calc(100% - 0.75rem))` 等比收缩,避免嵌套页面 padding 或负 margin 下用 `100vw` 误判宽度。预计等待 / 已耗时信息卡在窄屏下落到圆环下方两列,`sm` 及以上再回到左右悬浮。 - 影响范围:`src/components/GenerationProgressHero.tsx`、共用 `CustomWorldGenerationView`、汪汪声浪 `BarkBattleGeneratingView` 以及生成页圆环布局文档。 -- 验证方式:`CustomWorldGenerationView` 和 `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`、`data-ring-fill-start-degrees=135`,且圆环容器固定为 `h-[400px] w-[400px]`,track / fill transform 都是 `rotate(135 200 200)`。 +- 验证方式:`CustomWorldGenerationView` 和 `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`、`data-ring-fill-start-degrees=135`,且圆环容器包含 `w-[min(400px,calc(100%_-_0.75rem))]`、`max-w-full` 与 `aspect-square`,track / fill transform 都是 `rotate(135 200 200)`;竖屏 smoke 至少覆盖 `280px / 320px / 360px / 390px` 宽度。 - 关联文档:`docs/【玩法创作】生成页圆环布局口径-2026-05-23.md`。 ## 2026-05-26 平台跨流程错误统一用可复制来源弹窗展示 @@ -575,8 +617,8 @@ - 背景:抓大鹅草稿素材生成已经收敛为多视角 2D 图片素材,但入口页和旧参考图仍沿用黏土、低多边形、塑料、木雕、体素、金属等偏 3D 素材语言,容易让后续生成链路和用户预期继续漂移。 - 决策:抓大鹅创作入口 `2D素材风格` 固定为 `扁平图标 / 赛璐璐卡通 / 像素复古 / 手绘水彩 / 贴纸描边 / 厚涂图标 / 自定义`;默认风格为 `flat-icon`。入口参考图统一由 `npm run assets:match3d-style-references -- --live` 调用 VectorEngine `gpt-image-2` 生成,输出到 `public/match3d-style-references/`。旧 3D 风格参考图不再保留为入口资产。 -- 影响范围:`Match3DAgentWorkspace`、抓大鹅入口交互测试、Match3D PRD、素材生成流水线技术文档、F1 入口文档和 `public/match3d-style-references/` 静态资产。 -- 验证方式:执行 `npm run test -- src\components\match3d-creation\Match3DAgentWorkspace.interaction.test.tsx`、`cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml`、`npm run typecheck`、`npm run check:encoding`,并人工抽查 `.tmp/match3d-style-preview.png`。 +- 影响范围:抓大鹅统一创作工作台、抓大鹅入口交互测试、Match3D PRD、素材生成流水线技术文档、F1 入口文档和 `public/match3d-style-references/` 静态资产。 +- 验证方式:执行 `npm run test -- src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx`、`cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml`、`npm run typecheck`、`npm run check:encoding`,并人工抽查 `.tmp/match3d-style-preview.png`。 - 关联文档:`docs/prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`、`docs/technical/MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md`。 ## 2026-05-12 拼图与抓大鹅草稿背景音乐按纯音乐自动生成 @@ -1102,7 +1144,7 @@ - 背景:敲木鱼工作台只应保留生成所需输入,作品标题、简介和主题标签适合放在生成草稿后的补录阶段。 - 决策:敲木鱼的 `workTitle`、`workDescription` 和 `themeTags` 从工作台首屏移到结果页;结果页编辑后在试玩或发布前先调用 `update-work-meta` 写回当前作品信息。主题标签编辑样式对齐拼图结果页的胶囊标签编辑器。 -- 影响范围:`WoodenFishWorkspace`、`WoodenFishResultView`、`PlatformEntryFlowShellImpl`、敲木鱼 PRD 和平台入口链路文档。 +- 影响范围:敲木鱼统一创作工作台、`WoodenFishResultView`、`PlatformEntryFlowShellImpl`、敲木鱼 PRD 和平台入口链路文档。 - 验证方式:工作台首屏不再出现标题 / 简介 / 标签输入;结果页修改后点试玩或发布会先写回当前作品信息。 - 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index 7ee99e27..95eebd5f 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -59,6 +59,8 @@ Linux 多用户共享同一台机器开发时,本地 dev 脚本会为当前 Li - 主站 Vite - 后台 Vite +`npm run dev` 和单模块 `dev:*` 命令会更新根目录 `.app/dev-stack.json`,记录四个本地服务的 pid、端口、URL、启动状态和当前命令。该目录只作本机运行态观测,不提交 Git。 + 开启自动刷新: ```bash @@ -242,6 +244,7 @@ npm run check:server-rs-ddd - 移动端优先,再兼容网页端。 - 页面只展示后端返回的状态,不自行计算结论型业务状态。 - 创作中心入口配置事实源在 SpacetimeDB,通过 `GET /api/creation-entry/config` 下发;前端只在 `platformEntryCreationTypes.ts` 做展示派生,api-server 路由熔断也使用同一份配置,禁止恢复前端硬编码入口配置文件。 +- 一期统一创作页字段 spec 同样跟随 `GET /api/creation-entry/config`,由 `creationTypes[].unifiedCreationSpec` 下发;拼图、抓大鹅、敲木鱼之外的模板不接入该扩展位,前端只保留旧后端缺字段时的兜底默认。 - 优先复用现有面板、抽屉、弹窗,不新建独立大系统。 - 不在 UI 中默认写功能说明类文本。 - 弹出独立面板的交互不要实现成在当前面板下方追加内容。 @@ -255,6 +258,8 @@ npm run check:server-rs-ddd ## 提交前建议让 Hermes 执行 +涉及拼图、抓大鹅、敲木鱼统一创作 / 生成链路、Phase 2 之后的跨玩法回归或本地 dev 栈时,先按 `quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md` 和对应单项门禁文档执行自动脚本与体验检查。 + ```text 请检查当前 git diff,指出: 1. 是否违反 AGENTS.md 或 .hermes/shared-memory 约定; diff --git a/.hermes/shared-memory/document-map.md b/.hermes/shared-memory/document-map.md index 8cd29c75..a2091763 100644 --- a/.hermes/shared-memory/document-map.md +++ b/.hermes/shared-memory/document-map.md @@ -11,6 +11,7 @@ | 产品、命名、UI、协作和废弃路线 | `docs/【项目基线】当前产品与工程约束-2026-05-15.md` | | 后端、DDD、API、SpacetimeDB schema 和表目录 | `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` | | 创作入口、草稿架和玩法链路 | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` | +| 创作流程统一阶段计划 | `docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md` | | 本地启动、验证、部署、埋点和运营查询 | `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` | | 微信小程序虚拟支付 | `docs/【技术方案】微信虚拟支付接入-2026-05-26.md` | | UI 像素资产与 9-slice 规范 | `UI_CODING_STANDARD.md` | @@ -33,8 +34,9 @@ 玩法 / 创作入口 / 运行态: 1. `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` -2. `docs/【项目基线】当前产品与工程约束-2026-05-15.md` -3. 相关前端组件、service、shared contract 和后端 module +2. 若任务涉及跨玩法创作流程统一,读取 `docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md` +3. `docs/【项目基线】当前产品与工程约束-2026-05-15.md` +4. 相关前端组件、service、shared contract 和后端 module 生产部署 / 服务器 / Jenkins: diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index c7437277..c3063492 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -142,6 +142,22 @@ - 验证:浏览器里这三页的根区应仍保留 `platform-remap-surface`,但不再出现 `platform-page-stage`;草稿页顶部筛选样式应和发现页频道标签一致。 - 关联:`src/components/custom-world-home/CustomWorldCreationHub.tsx`、`src/components/custom-world-home/CustomWorldWorkTabs.tsx`、`src/components/rpg-entry/RpgEntryHomeView.tsx`、`src/index.css`。 +## 统一创作壳现在自己负责页面滚动和四条入口外壳 + +- 现象:统一创作页最初只包住拼图、抓大鹅和敲木鱼的工作台内容,跳一跳仍然保留独立工作台壳,页面级滚动职责也散落在平台入口 motion wrapper 里,导致移动端不同入口的可见外壳不一致。 +- 原因:`UnifiedCreationPage` 只做了标题和隐藏契约,入口壳还在各自工作台里保留 `platform-remap-surface` / `overflow-y-auto`,`jump-hop` 也没进入统一 spec。 +- 处理:把 `jump-hop` 纳入 `unifiedCreationSpec`,让 `UnifiedCreationPage` 自己承担页面级滚动与统一标题栏;`JumpHopCreationWorkspace`、`WoodenFishCreationWorkspace` 补 `unifiedChrome` / `showBackButton`,平台壳不再给这几条统一入口套额外滚动壳。 +- 验证:`npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx` 通过后,`/creation/puzzle`、`/creation/match3d`、`/creation/jump-hop`、`/creation/wooden-fish` 都应由同一套统一创作页外壳承载。 +- 关联:`src/components/unified-creation/UnifiedCreationPage.tsx`、`src/components/unified-creation/unifiedCreationSpecs.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`。 + +## 统一创作编排层不要再让平台壳直挂旧工作台 + +- 现象:平台入口壳已经切到统一创作外壳,但源码里仍直接 lazy import 并渲染四个旧工作台分支,看起来还是四套入口编排。 +- 原因:统一创作页只收口了可见外壳,入口层没有再抽一层统一创作编排组件,导致平台壳依旧要认识各玩法旧工作台。 +- 处理:新增 `UnifiedCreationWorkspace`,由它内部按 `playId` 选择真实工作台;平台壳只依赖这一层,不再直接挂旧工作台分支。旧工作台已迁入 `src/components/unified-creation/workspaces/`,不再是入口编排事实源。 +- 验证:`PlatformEntryFlowShellImpl.tsx` 中不应再出现四个旧工作台的入口渲染分支,创作 Tab 与 `/creation/` 仍能正常进入对应工作台。 +- 关联:`src/components/unified-creation/UnifiedCreationWorkspace.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## Jenkinsfile 开头不能带 UTF-8 BOM @@ -255,7 +271,7 @@ - 原因:通用图像输入是受控输入面板,不是只服务单页的临时实现;图片、提示词、参考图数组、重绘开关等业务真相应由外层页面持有,组件最多持有参考图预览、删除确认这类短生命周期 UI 状态。 - 处理:抽 `CreativeImageInputPanel` 时,保留上传卡、参考图入口、缩略图、预览弹层、删除确认和提交按钮的统一壳,但把主图文件读取、裁剪、历史素材、计费确认和具体提交动作留给外层页面;后续页面接入时只传业务回调和文案。 - 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。 -- 关联:`src/components/common/CreativeImageInputPanel.tsx`、`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`。 +- 关联:`src/components/common/CreativeImageInputPanel.tsx`、`src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx`。 ## RPG 发布不能只依赖 agent session seed_text @@ -694,7 +710,7 @@ - 原因:首图生成只通过 `compile_puzzle_draft.referenceImageSrc` 临时传 Data URL,不持久化到 SpacetimeDB;结果页重新生成则要把当前上传图或关卡 `pictureReference` 作为 `generate_puzzle_images.referenceImageSrc` 继续传给后端。 - 处理:浏览器 Network 里确认 action payload 带 `referenceImageSrc`;api-server 日志按同一 `session_id` 查看 `拼图参考图解析完成`、`拼图 VectorEngine 图片生成 HTTP 返回`、`拼图 VectorEngine 图片下载完成`、`拼图生成图片已写入 OSS 与资产索引`,可定位慢在参考图读取、VectorEngine、下载或 OSS。 - 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的 `pictureReference` 重新生成;后端单测覆盖 VectorEngine 请求体 `image` 字段。 -- 关联:`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle.rs`。 +- 关联:`src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle.rs`。 ## 拼图首图生成后要把入口参考图写回 `pictureReference` @@ -1448,16 +1464,16 @@ - 现象:拼图创作页或结果页打开“选择历史图片”后,历史列表显示 `账号 user-1` 之类归属文案而不是图片名;`1713686400.000000Z` 这类时间显示为未知;选中后预览或生成参考图可能被怀疑不可用。 - 原因:`/api/assets/history?kind=puzzle_cover_image` 返回的 `ownerLabel` 是资产归属账号,不是图片标题;`createdAt` 可能是 SpacetimeDB / shared-kernel 秒级时间字符串,不能只用浏览器 `new Date(value)` 解析。历史图的 `imageSrc` 是 `/generated-*` 私有兼容路径,浏览器预览必须换签。 - 处理:前端标题和选中标签从 `imageSrc` 路径末尾推导,例如 `image.png`;时间解析兼容 ISO 与 `1713686400.000000Z`;创作页主图、历史列表图和结果页参考图继续用 `ResolvedAssetImage`,提交给后端时仍保留原始 `imageSrc`。 -- 验证:`npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`,并执行 `npm run check:encoding`。 -- 关联:`src/services/puzzle-works/puzzleHistoryAsset.ts`、`src/components/puzzle-agent/PuzzleHistoryAssetPickerDialog.tsx`、`docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md`。 +- 验证:`npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`,并执行 `npm run check:encoding`。 +- 关联:`src/services/puzzle-works/puzzleHistoryAsset.ts`、`src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.tsx`、`docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md`。 ## 拼图历史图关闭 AI 重绘不要强制 Data URL - 现象:拼图创作页从历史生成图片中选择主图,再关闭 AI 重绘生成草稿时,后端报“上传图必须是图片 Data URL”。 - 原因:历史图 `imageSrc` 是 `/generated-puzzle-assets/...` 私有兼容路径;AI 重绘开启时后端参考图分支会解析该路径,但关闭 AI 重绘的“直用上传图”分支旧实现只调用 `parse_puzzle_image_data_url`。 - 处理:关闭 AI 重绘时也复用拼图参考图解析入口,允许 Data URL 与 `/generated-*` 历史路径统一转成 `PuzzleDownloadedImage` 后持久化;前端不需要下载历史图再转 base64。 -- 验证:`npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.toml`、`npm run dev:api-server` 后检查 `/healthz`。 -- 关联:`server-rs/crates/api-server/src/puzzle/draft.rs`、`server-rs/crates/api-server/src/puzzle/vector_engine.rs`、`src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx`。 +- 验证:`npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.toml`、`npm run dev:api-server` 后检查 `/healthz`。 +- 关联:`server-rs/crates/api-server/src/puzzle/draft.rs`、`server-rs/crates/api-server/src/puzzle/vector_engine.rs`、`src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx`。 ## 拼图结果页局部生图不要污染草稿生成态 @@ -1475,7 +1491,7 @@ - 原因:上传图直用路径应把 Data URL 或 `/generated-*` 历史图解析后持久化为 `sourceType=uploaded` 的正式候选,再继续生成 9:16 关卡画面、UI spritesheet 和纯背景;如果只把 `aiRedraw=false` 当作“不参考图片生成”,就会误走首图生成。 - 处理:入口页用 payload 的 `aiRedraw` 写入生成页 metadata,`puzzleAiRedraw=false` 时进度跳过 `生成拼图首图`;后端 `compile_puzzle_draft` 和结果页 `generate_puzzle_images` 都在 `aiRedraw=false && referenceImageSrc 非空` 时走上传图直用候选。结果页关卡详情必须复用 `CreativeImageInputPanel`,不要把正式图当成可重绘参考图;本次上传或历史选择的图才显示 AI 重绘开关并可删除。 - 验证:`npm run test -- src/services/miniGameDraftGenerationProgress.test.ts src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_result_level_direct_upload_skips_cover_image_generation --manifest-path server-rs\Cargo.toml`。 -- 关联:`src/services/miniGameDraftGenerationProgress.ts`、`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle/draft.rs`、`server-rs/crates/api-server/src/puzzle/generation.rs`。 +- 关联:`src/services/miniGameDraftGenerationProgress.ts`、`src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle/draft.rs`、`server-rs/crates/api-server/src/puzzle/generation.rs`。 ## Jenkins 数据库导入导出脚本先补 Node 工具链 PATH @@ -1547,6 +1563,13 @@ - 现象:移动端创作 Tab 里进入汪汪声浪表单后,页面右侧出现不自然的内层滚动条,最后的形象描述输入框容易被“生成草稿”按钮、键盘或底部 TabBar 挤压 / 遮挡;顶部玩法卡首尾也可能贴边显得被裁。 - 原因:外层 `.platform-tab-panel` 已经是纵向滚动容器,创作页中间又有多层 `overflow-hidden`,旧的 `BarkBattleConfigEditor` 根节点再加 `overflow-y-auto`,形成外层 Tab 面板 + 内层表单的套滚动;底部按钮只预留 safe-area,不预留真实操作区距离;顶部玩法卡横向滚动条隐藏且首尾没有 scroll padding。 - 处理:移动端让 Bark Battle 表单跟随父级滚动,`lg` 以上才恢复表单内滚动;创作页容器移动端使用 `overflow-visible` 和 safe-area 底部 padding;顶部模板 tablist 加 `scroll-px-3` / 横向 padding,移动端卡片宽度收窄,避免首尾 ring 和圆角贴边裁切。 + +## 统一创作页不要把竖屏滚动锁进内部内容区 + +- 现象:竖屏打开拼图、抓大鹅或敲木鱼创作页时,浏览器页面本身无法滚动,生成按钮或右侧表单面板落到视口外;木鱼的敲击音效和功德词条看起来像被塞进单独滑动窗口。 +- 原因:平台根壳固定一屏并隐藏溢出,`UnifiedCreationPage` 又使用 `h-full min-h-0 overflow-hidden` 和内容区 `overflow-y-auto`,导致滚动责任落到内部内容窗,而不是整个创作 stage。 +- 处理:`UnifiedCreationPage` 统一负责标题、隐藏字段契约、内容包装和页面级纵向滚动;拼图、抓大鹅、跳一跳和敲木鱼的外层 `motion.div` 不再额外包 `overflow-y-auto`。各工作台在 `unifiedChrome` 下收起旧 `h-full overflow-hidden` 外壳,让表单主体跟随统一页面滚动。 +- 验证:用竖屏浏览器视口打开 `/creation/wooden-fish`、`/creation/puzzle`、`/creation/match3d` 和 `/creation/jump-hop`,统一创作页应可滚动到生成按钮;`.unified-creation-page` 应包含页面级 `overflow-y-auto`,木鱼工作台内部也不应出现独立纵向滚动容器,拼图 / 抓大鹅可见标题不应重复。 - 验证:`npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"`、移动端视口检查最后一个输入框与“生成草稿”按钮不重叠。 - 关联:`src/components/bark-battle-creation/BarkBattleConfigEditor.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 @@ -1606,6 +1629,14 @@ - 验证:移动端视口检查视频 `rect` 应覆盖整个视口,`paused` 应最终变为 `false`,`currentTime` 应持续前进。 - 关联:`src/components/GenerationProgressHero.tsx`、`docs/【玩法创作】生成页圆环布局口径-2026-05-23.md`。 +## 跳一跳结果页直达时不要把恢复面板当成空白页 + +- 现象:浏览器直接打开 `/creation/jump-hop/result`,如果没有 `sessionId`、`profileId`、`draftId` 或 `workId`,页面以前会看起来像空白,容易误判成结果页坏了。 +- 原因:跳一跳结果页恢复原先只盯 `jumpHopSession.draft`,没有把“缺恢复信息”明确兜成可见恢复面板;直达结果页时也没有优先用 `profileId -> getWorkDetail` 补回完整作品。 +- 处理:`PlatformEntryFlowShellImpl` 的跳一跳恢复逻辑改成先尝试 `profileId -> getWorkDetail`,再尝试 `sessionId -> getSession`;两者都没有时显示 `跳一跳草稿未恢复` 和 `返回创作`,不再留空白页。 +- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"`,并手测 `/creation/jump-hop/result` 与 `/creation/jump-hop/result?profileId=` 两种情况。 +- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`。 + 2026-05-24 补充:`GenerationPageBackdrop` 不要通过 portal 挂到 `document.body`。body 级 fixed 背景会逃离生成页自己的 stacking context,即使业务内容有局部 `z-10`,真实浏览器里也可能把整页 UI 压住。背景视频应作为生成页根容器子节点保留 `fixed inset-0 z-0`,生成页内容保持 `relative z-10`;相关测试应同时断言背景容器低层级、生成页根容器高层级,以及视频节点仍在生成页 DOM 内部。视觉调整时还要记住:空心圆环的中心块要抽掉,时间卡与总进度标题都应缩小,不要让生成页再回到“纯色底 + 大字号说明卡”的状态。顶部返回和右上状态也不能沿用 `text-lg` / `sm:text-2xl` 这类展示级字号;当前步骤名、步骤状态和底部玩法信息标题要维持普通 UI 字号档位,优先保持 `text-xs` 到 `text-sm` 区间。 2026-05-24 补充:生成页“预计等待 / 已耗时”卡片本身已经有标签,传给 `GenerationProgressHero` 的值只能是纯时间,例如 `4 分钟`、`1 分 15 秒`,不要再拼接“预计还需”或“已耗时”;两张时间卡也要和当前步骤卡一样保持半透明。拼图总进度初始帧必须允许显示 `0%`,不要再用 `Math.max(1, nextProgress)` 之类的保护把启动态抬到 `1%`。 diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 6bcb7c11..3ba26abc 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -161,6 +161,7 @@ export interface AdminCreationEntryTypeConfigPayload { categoryLabel: string; categorySortOrder: number; updatedAtMicros: number; + unifiedCreationSpec?: UnifiedCreationSpecPayload | null; } export interface AdminUpsertCreationEntryTypeConfigRequest { @@ -175,6 +176,23 @@ export interface AdminUpsertCreationEntryTypeConfigRequest { categoryId: string; categoryLabel: string; categorySortOrder: number; + unifiedCreationSpec?: UnifiedCreationSpecPayload | null; +} + +export interface UnifiedCreationSpecPayload { + playId: string; + title: string; + workspaceStage: string; + generationStage: string; + resultStage: string; + fields: UnifiedCreationFieldPayload[]; +} + +export interface UnifiedCreationFieldPayload { + id: string; + kind: 'text' | 'select' | 'image' | 'audio'; + label: string; + required: boolean; } export interface AdminWorkVisibilityEntryPayload { diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx new file mode 100644 index 00000000..75c84504 --- /dev/null +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx @@ -0,0 +1,112 @@ +/* @vitest-environment jsdom */ + +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {beforeEach, expect, test, vi} from 'vitest'; + +import { + getAdminCreationEntryConfig, + upsertAdminCreationEntryConfig, +} from '../api/adminApiClient'; +import type { + AdminCreationEntryConfigResponse, + UnifiedCreationSpecPayload, +} from '../api/adminApiTypes'; +import {AdminCreationEntrySwitchPage} from './AdminCreationEntrySwitchPage'; + +vi.mock('../api/adminApiClient', () => ({ + formatAdminApiError: vi.fn((error: unknown) => + error instanceof Error ? error.message : '请求失败', + ), + getAdminCreationEntryConfig: vi.fn(), + isAdminApiError: vi.fn(() => false), + upsertAdminCreationEntryConfig: vi.fn(), +})); + +const puzzleSpec: UnifiedCreationSpecPayload = { + playId: 'puzzle', + title: '想做个什么玩法?', + workspaceStage: 'puzzle-agent-workspace', + generationStage: 'puzzle-generating', + resultStage: 'puzzle-result', + fields: [ + { + id: 'pictureDescription', + kind: 'text', + label: '画面描述', + required: true, + }, + ], +}; + +const configResponse: AdminCreationEntryConfigResponse = { + entries: [ + { + id: 'puzzle', + title: '拼图', + subtitle: '拼图关卡创作', + badge: '可创建', + imageSrc: '/creation-type-references/puzzle.webp', + visible: true, + open: true, + sortOrder: 30, + categoryId: 'recent', + categoryLabel: '最近创作', + categorySortOrder: 10, + updatedAtMicros: 1, + unifiedCreationSpec: puzzleSpec, + }, + ], +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getAdminCreationEntryConfig).mockResolvedValue(configResponse); + vi.mocked(upsertAdminCreationEntryConfig).mockResolvedValue(configResponse); +}); + +test('创作入口后台展示并保存统一创作契约', async () => { + const user = userEvent.setup(); + const {container} = render( + , + ); + + await screen.findByText('pictureDescription'); + expect(container.querySelector('.admin-subsection .admin-info-list')).not.toBeNull(); + expect(container.querySelector('.admin-panel .admin-panel')).toBeNull(); + expect(container.querySelector('.admin-muted')).toBeNull(); + + await user.click(screen.getByRole('button', {name: '保存入库'})); + await user.click(screen.getByRole('button', {name: '确认'})); + + await waitFor(() => { + expect(upsertAdminCreationEntryConfig).toHaveBeenCalledWith( + 'admin-token', + expect.objectContaining({ + id: 'puzzle', + unifiedCreationSpec: puzzleSpec, + }), + ); + }); +}); + +test('创作入口后台拒绝 playId 不一致的统一创作契约', async () => { + const user = userEvent.setup(); + render( + , + ); + + const textarea = await screen.findByLabelText('契约 JSON'); + fireEvent.change(textarea, { + target: { + value: JSON.stringify({ + ...puzzleSpec, + playId: 'match3d', + }), + }, + }); + await user.click(screen.getByRole('button', {name: '保存入库'})); + + expect(await screen.findByText('统一创作契约 playId 必须与入口 ID 一致')).toBeTruthy(); + expect(upsertAdminCreationEntryConfig).not.toHaveBeenCalled(); +}); diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx index fb817c65..4a06ddd9 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx @@ -5,7 +5,11 @@ import { getAdminCreationEntryConfig, upsertAdminCreationEntryConfig, } from '../api/adminApiClient'; -import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes'; +import type { + AdminCreationEntryTypeConfigPayload, + UnifiedCreationFieldPayload, + UnifiedCreationSpecPayload, +} from '../api/adminApiTypes'; import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm'; import {handlePageError} from './pageUtils'; @@ -30,6 +34,7 @@ export function AdminCreationEntrySwitchPage({ const [categoryId, setCategoryId] = useState('recent'); const [categoryLabel, setCategoryLabel] = useState('最近创作'); const [categorySortOrder, setCategorySortOrder] = useState('10'); + const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [listErrorMessage, setListErrorMessage] = useState(''); @@ -66,6 +71,14 @@ export function AdminCreationEntrySwitchPage({ const targetId = selectedId.trim(); setErrorMessage(''); + const unifiedCreationSpecResult = parseUnifiedCreationSpecJson( + targetId, + unifiedCreationSpecJson, + ); + if (!unifiedCreationSpecResult.ok) { + setErrorMessage(unifiedCreationSpecResult.message); + return; + } const confirmed = await confirmWrite({ action: '保存创作入口开关', target: targetId, @@ -88,6 +101,7 @@ export function AdminCreationEntrySwitchPage({ categoryId: categoryId.trim(), categoryLabel: categoryLabel.trim(), categorySortOrder: parseInteger(categorySortOrder), + unifiedCreationSpec: unifiedCreationSpecResult.spec, }); const nextEntries = sortEntries(response.entries); setEntries(nextEntries); @@ -114,6 +128,7 @@ export function AdminCreationEntrySwitchPage({ setCategoryId(entry.categoryId); setCategoryLabel(entry.categoryLabel); setCategorySortOrder(String(entry.categorySortOrder)); + setUnifiedCreationSpecJson(formatUnifiedCreationSpecJson(entry.unifiedCreationSpec)); } return ( @@ -224,6 +239,26 @@ export function AdminCreationEntrySwitchPage({ /> +
+
+ 统一创作契约 + {unifiedCreationSpecJson.trim() ? '已配置' : '未配置'} +
+ {unifiedCreationSpecJson.trim() ? ( + + ) : ( +
未配置统一创作页契约
+ )} +