Merge remote-tracking branch 'origin/codex/unified-creation-flow-phase1'

# Conflicts:
#	server-rs/crates/api-server/src/wooden_fish.rs
This commit is contained in:
kdletters
2026-06-01 15:22:58 +08:00
86 changed files with 4944 additions and 967 deletions
+1
View File
@@ -34,6 +34,7 @@ temp*build*/
/public/generated-character-drafts
/public/generated-characters
/.codex-temp
/.app/
/target/
/logs
/server-rs/crates/*/logs/
+49 -7
View File
@@ -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=<id>`
- 关联文档:`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/<play>` 仍可正常进入对应工作台。
- 关联文档:`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`
@@ -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 约定;
+4 -2
View File
@@ -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
+38 -7
View File
@@ -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/<play>` 仍能正常进入对应工作台。
- 关联:`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=<id>` 两种情况。
- 关联:`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%`
+18
View File
@@ -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 {
@@ -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(
<AdminCreationEntrySwitchPage token="admin-token" onUnauthorized={vi.fn()} />,
);
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(
<AdminCreationEntrySwitchPage token="admin-token" onUnauthorized={vi.fn()} />,
);
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();
});
@@ -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({
/>
</label>
<section className="admin-subsection">
<div className="admin-subsection-heading">
<span></span>
<span>{unifiedCreationSpecJson.trim() ? '已配置' : '未配置'}</span>
</div>
{unifiedCreationSpecJson.trim() ? (
<UnifiedCreationSpecSummary specJson={unifiedCreationSpecJson} />
) : (
<div className="admin-muted-text"></div>
)}
<label className="admin-field">
<span> JSON</span>
<textarea
rows={12}
value={unifiedCreationSpecJson}
onChange={(event) => setUnifiedCreationSpecJson(event.target.value)}
/>
</label>
</section>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
@@ -246,6 +281,7 @@ export function AdminCreationEntrySwitchPage({
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
@@ -264,6 +300,7 @@ export function AdminCreationEntrySwitchPage({
</td>
<td>{entry.visible ? '是' : '否'}</td>
<td>{entry.open ? '是' : '否'}</td>
<td>{entry.unifiedCreationSpec ? '是' : '否'}</td>
<td>{entry.categoryLabel || entry.categoryId}</td>
<td>{entry.sortOrder}</td>
</tr>
@@ -295,3 +332,162 @@ function parseInteger(value: string) {
}
return parsed;
}
function formatUnifiedCreationSpecJson(
spec: UnifiedCreationSpecPayload | null | undefined,
) {
return spec ? JSON.stringify(spec, null, 2) : '';
}
function parseUnifiedCreationSpecJson(entryId: string, value: string) {
const parsed = parseUnifiedCreationSpecSummaryJson(value);
if (!parsed.ok || !parsed.spec) {
return parsed;
}
if (parsed.spec.playId !== entryId) {
return {ok: false as const, message: '统一创作契约 playId 必须与入口 ID 一致'};
}
return parsed;
}
function validateUnifiedCreationSpec(value: unknown) {
if (!isRecord(value)) {
return {ok: false as const, message: '统一创作契约必须是对象'};
}
const playId = readRequiredString(value, 'playId');
if (!playId) {
return {ok: false as const, message: '统一创作契约 playId 不能为空'};
}
const title = readRequiredString(value, 'title');
const workspaceStage = readRequiredString(value, 'workspaceStage');
const generationStage = readRequiredString(value, 'generationStage');
const resultStage = readRequiredString(value, 'resultStage');
if (!title || !workspaceStage || !generationStage || !resultStage) {
return {ok: false as const, message: '统一创作契约标题和阶段不能为空'};
}
if (new Set([workspaceStage, generationStage, resultStage]).size !== 3) {
return {ok: false as const, message: '统一创作契约阶段不能重复'};
}
if (!Array.isArray(value.fields) || value.fields.length === 0) {
return {ok: false as const, message: '统一创作契约 fields 不能为空'};
}
const fieldIds = new Set<string>();
const fields: UnifiedCreationFieldPayload[] = [];
for (const item of value.fields) {
if (!isRecord(item)) {
return {ok: false as const, message: '统一创作契约字段必须是对象'};
}
const id = readRequiredString(item, 'id');
const label = readRequiredString(item, 'label');
if (!id || !label) {
return {ok: false as const, message: '统一创作契约字段 id 和 label 不能为空'};
}
if (fieldIds.has(id)) {
return {ok: false as const, message: `统一创作契约字段 id 重复:${id}`};
}
fieldIds.add(id);
if (!isUnifiedCreationFieldKind(item.kind)) {
return {ok: false as const, message: `统一创作契约字段 kind 非法:${id}`};
}
if (typeof item.required !== 'boolean') {
return {ok: false as const, message: `统一创作契约字段 required 非法:${id}`};
}
fields.push({
id,
kind: item.kind,
label,
required: item.required,
});
}
return {
ok: true as const,
spec: {
playId,
title,
workspaceStage,
generationStage,
resultStage,
fields,
},
};
}
function UnifiedCreationSpecSummary({specJson}: {specJson: string}) {
const parsed = parseUnifiedCreationSpecSummaryJson(specJson);
if (!parsed.ok || !parsed.spec) {
return (
<div className="admin-alert" role="status">
{'message' in parsed ? parsed.message : '未配置统一创作页契约'}
</div>
);
}
return (
<dl className="admin-info-list">
<div>
<dt></dt>
<dd>{parsed.spec.playId}</dd>
</div>
<div>
<dt></dt>
<dd>
{parsed.spec.workspaceStage} / {parsed.spec.generationStage} /{' '}
{parsed.spec.resultStage}
</dd>
</div>
<div>
<dt></dt>
<dd>{parsed.spec.fields.map((field) => field.id).join('、')}</dd>
</div>
</dl>
);
}
function parseUnifiedCreationSpecSummaryJson(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return {ok: true as const, spec: null};
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (error) {
return {
ok: false as const,
message: error instanceof Error ? `契约 JSON 非法:${error.message}` : '契约 JSON 非法',
};
}
const validation = validateUnifiedCreationSpec(parsed);
if (!validation.ok) {
return validation;
}
return {ok: true as const, spec: validation.spec};
}
function readRequiredString(value: Record<string, unknown>, key: string) {
const raw = value[key];
return typeof raw === 'string' ? raw.trim() : '';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isUnifiedCreationFieldKind(
value: unknown,
): value is UnifiedCreationFieldPayload['kind'] {
return (
value === 'text' ||
value === 'select' ||
value === 'image' ||
value === 'audio'
);
}
+2 -2
View File
@@ -8,7 +8,7 @@
- [审计与复盘](./audits/README.md):工程审查、文本/乱码审计、专项落地审计。
- [系统设计](./design/README.md):玩法、关系、物品与对话设计。
- [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。
- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级。
- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级;创作流程统一总计划见 [【玩法创作】创作流程统一总计划-2026-05-30.md](./planning/%E3%80%90%E7%8E%A9%E6%B3%95%E5%88%9B%E4%BD%9C%E3%80%91%E5%88%9B%E4%BD%9C%E6%B5%81%E7%A8%8B%E7%BB%9F%E4%B8%80%E6%80%BB%E8%AE%A1%E5%88%92-2026-05-30.md)
- [参考目录](./reference/README.md):脚本/Function 速查入口。
重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。
- [埋点查询](./tracking/README.md):埋点原始事件与聚合投影的本地 SQL 查询。
@@ -41,7 +41,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
2. 再看 [工程审查总览](./audits/engineering/README.md) 和 [文本审计总览](./audits/text/README.md),了解当前风险。
3. 需要排期时看 [规划与优先级](./planning/README.md)。
3. 需要排期时看 [规划与优先级](./planning/README.md),创作链路相关任务优先看创作流程统一总计划
4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md);涉及后端先看 [当前后端实现基线](./technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md),涉及生产发布链路先看 [生产部署计划](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),涉及 SpacetimeDB 表结构变更时再看 [表结构变更约束](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
5. 需要对齐目标边界时再进入 [PRD](./prd)。
+13
View File
@@ -0,0 +1,13 @@
# 规划与优先级
本目录保存仍处于推进中的阶段计划、并行任务拆分、可派发任务包和验收顺序。长期稳定的产品与架构口径仍以根部融合文档为准。
## 当前计划
- [【玩法创作】创作流程统一总计划-2026-05-30.md](./【玩法创作】创作流程统一总计划-2026-05-30.md):创作入口、统一创作页、统一生成页、结果页、发布、作品架、广场和运行态的阶段计划、进度记录、并行波次和可直接派发的任务包。
## 维护规则
- 计划文档只记录可执行阶段、负责人切分、验收门禁和当前状态。
- 已经稳定为长期约定的内容,应同步沉淀到 `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md``.hermes/shared-memory/`
- 若代码事实与计划冲突,以代码和当前融合文档为准,并回写更新本目录。
@@ -0,0 +1,375 @@
# 创作流程统一总计划
更新时间:`2026-05-30`
## 总览
| 项目 | 当前值 |
| --- | --- |
| 总轮次 | 5 |
| 当前轮次 | Round 4(已收口) |
| 当前阶段 | Phase 6 |
| 当前状态 | Phase 0~6 已收口;统一创作页已升级为 `UnifiedCreationWorkspace`,平台壳不再直接依赖旧工作台文件 |
| 当前并行波次 | 波次 D(验收与冻结) |
| 当前重点 | 以跨玩法门禁作为后续新增玩法和回归的常规质量基线 |
## 目标与范围
本计划统一 Genarrative 所有玩法的创作链路,不再只跟踪首批的拼图、抓大鹅和敲木鱼。最终目标是让各玩法按同一条平台链路交付:
```text
创作入口 -> 统一创作页/工作台 -> 统一生成页 -> 结果页 -> 试玩 -> 发布 -> 统一作品详情/作品架/广场 -> 正式 runtime
```
统一不是把所有玩法 UI 做成同一个表单,而是统一阶段、契约、恢复、生成反馈、错误承接、发布后去向和验收门禁。各玩法工作台仍负责真实输入控件、资产槽位、校验和提交。
## 当前进度
| 阶段 | 状态 | 说明 |
| --- | --- | --- |
| Phase 0 总计划与门禁 | 已完成 | 本文档、`docs/planning/README.md``docs/README.md``.hermes/shared-memory/document-map.md` 已补齐入口;后续按 phase 扩展门禁。 |
| Phase 1 首批统一壳 | 已收口 | `puzzle``match3d``jump-hop``wooden-fish` 已接入 `UnifiedCreationPage` / `UnifiedGenerationPage`,竖屏滚动和字段契约已回归。 |
| Phase 1 补充统一壳 | 已收口 | `jump-hop` 也已接入 `UnifiedCreationPage` / `UnifiedGenerationPage`,统一创作页现在接管拼图、抓大鹅、跳一跳和敲木鱼四条入口的可见外壳与滚动。 |
| Phase 2 契约与配置治理 | 已完成 | `creationTypes[].unifiedCreationSpec`、前端 fallback、后台配置校验和文档门禁已按现有测试与 schema 检查收口。 |
| Phase 3 剩余表单/图片工作台接入 | 已收口 | 跳一跳、宝贝识物、方洞结果页与首批普通工作台回归已通过;方洞、大鱼按当前形态纳入最小回归,后续若迁移工作台再单独立项。 |
| Phase 4 特殊工作台接入策略 | 已收口 | RPG、视觉小说、汪汪声浪的最小例外/闭环回归已通过,例外口径已落到平台总链路文档。 |
| Phase 5 结果页、发布、作品架与广场收口 | 已收口 | 结果页、发布、公开详情、推荐 runtime 与公开 read model 最小自动回归已通过,公开详情作者展示口径已统一。 |
| Phase 6 全链路验收与冻结 | 已收口 | 跨玩法 smoke、移动端优先验收、回归矩阵、长期维护规则和冻结证据已补齐。 |
此表即总进度记录,后续每次 phase 收口、启动或回退时,只更新这里和下方任务状态。
当前已完成 Round 0~4 / Phase 0~6。后续新增玩法或统一链路改动以本文档和 `quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md` 为常规质量基线。
## 执行轮次
按可交付批次拆成 5 轮(Round 0~4)。当前已完成 Round 0~4Round 4 / 波次 D 已作为冻结基线收口。
| 轮次 | 覆盖阶段 | 目标 | 状态 |
| --- | --- | --- | --- |
| Round 0 | Phase 0 | 补齐总计划、文档入口和并行任务表 | 已完成 |
| Round 1 | Phase 2 | 契约与配置治理 | 已完成 |
| Round 2 | Phase 3 + Phase 4 | 普通工作台并行接入,特殊工作台先定例外边界 | 已完成 |
| Round 3 | Phase 5 | 结果页、发布、作品架与广场收口 | 已完成 |
| Round 4 | Phase 6 | 全链路验收与冻结 | 已完成 |
## 阶段拆分
### Phase 0:总计划与执行门禁
目标:让团队有一个唯一可查的总计划、进度表和并行任务清单。
状态:已完成。
- 新增本计划文档和 `docs/planning/README.md`,并在 `docs/README.md``.hermes/shared-memory/document-map.md` 中补上规划入口。
- 补齐 `当前进度``执行轮次` 和可并行任务表,后续每个 phase 完成后更新本文档的状态、验收命令和风险。
退出条件:
- 文档入口可从 `docs/README.md` 找到。
- 总计划包含阶段、进度、并行任务、验收和风险。
### Phase 1:首批统一壳收口
目标:用低风险的四条链路验证统一创作/生成壳。
- 范围:`puzzle``match3d``jump-hop``wooden-fish`
- 创作页统一经过 `UnifiedCreationPage`,工作台保留各自真实输入能力。
- 生成页统一经过 `UnifiedGenerationPage``CustomWorldGenerationView`
- 竖屏滚动由统一创作页承担,避免内层滚动窗。
状态:已收口。
### Phase 2:契约与配置治理
目标:把统一创作页从前端“能渲染”推进到平台配置“可治理”。
- 明确 `unifiedCreationSpec` 的字段种类、必填语义、阶段映射和兼容 fallback。
- 后台配置、前台读取和 `api-server` 路由熔断继续以 SpacetimeDB 入口配置为事实源。
- 前端本地 fallback 只服务旧后端或本地异常,不作为新增玩法事实源。
- 为字段契约、入口开放状态、阶段映射补自动测试。
退出条件:
- `creation-entry config` 契约在前后端文档中闭合。
- 新增玩法不能绕过统一入口配置接线。
状态:已完成。
验证:`npm run check:encoding``npm run typecheck``npm run admin-web:typecheck``npm run check:spacetime-schema` 和统一创作页 / 统一生成页相关测试已通过。
### Phase 3:剩余表单/图片工作台接入
目标:把结构相近的玩法先迁到统一创作与生成壳,扩大覆盖面。
候选范围:
- 第一批直接迁移:`jump-hop``baby-object-match`
- 需要先做工作台形态评估:`square-hole``big-fish`
- 其它已经是表单/图片输入工作台、且无复杂多阶段编辑器的玩法
统一要求:
- 继续复用 `CreativeImageInputPanel``CreativeAudioInputPanel` 等现有通用输入组件。
- 不在工作台 UI 中默认写规则说明或功能解释。
- 自动素材生成走统一生成页;没有自动生成的玩法需要明确跳过生成页的阶段策略。
- 结果页和 runtime 不因迁移创作页而改业务真相。
- `square-hole``big-fish` 先评估是否保留 Agent 形态还是迁到表单/图片工作台,再决定是否进入直接迁移实现。
- `jump-hop` 已纳入统一创作壳,后续若要调整字段或视觉,只能在统一壳与工作台之间协同改,不再恢复独立入口壳。
退出条件:
- 每个接入玩法都有创作页、生成页或跳过生成页的明确验收。
- 移动端竖屏能从标题、表单滚动到提交按钮。
状态:已收口。
2026-05-30 回归记录:
- `jump-hop``baby-object-match``big-fish``square-hole` 的核心工作台 / 结果页 / runtime 测试已补齐并通过。
- `jump-hop` 结果页刷新恢复已补齐 `profileId -> getWorkDetail` 回读;直达 `/creation/jump-hop/result` 且缺少恢复参数时显示“跳一跳草稿未恢复”恢复面板,不再白屏。
- `square-hole` 结果页的试玩 / 发布路径已补齐服务 mock 回归,确认保存成功后才触发试玩和发布回调。
- 首批普通工作台回归通过:拼图、抓大鹅、敲木鱼、跳一跳、宝贝识物、方洞结果页。
- 竖屏浏览器 smoke 已覆盖 `/creation/jump-hop``/creation/baby-object-match``/creation/square-hole``/creation/bark-battle``/creation/visual-novel``/creation/jump-hop/generating``/creation/jump-hop/result``/runtime/jump-hop`,截图保存在 `.app/browser-check/phase-flow-20260530/`
### Phase 4:特殊工作台接入策略
目标:处理不能直接套表单/图片工作台的玩法,先定边界再迁移。
候选范围:
- RPG / 自定义世界
- 视觉小说
- 汪汪声浪(`bark-battle`
- 其它多阶段编辑器、对话式 Agent 或特殊创作流
统一要求:
- 必须在玩法文档中写明“创作工具模式例外”。
- 例外只影响工作台内部,不影响入口配置、生成反馈、结果页、发布、作品架、广场和 runtime 的平台主链路。
- 对话式或多阶段编辑器仍需和统一作品、统一错误、统一生成完成反馈对齐。
- `bark-battle` 默认按特殊工作台收口;若后续产品决策确认可完全表单化,再单独前移到 Phase 3,不在本轮默认假设里。
退出条件:
- 每个特殊玩法都有例外声明、阶段映射和验收清单。
- 没有新增平行入口系统、平行作品架或平行公开列表。
状态:已收口。
2026-05-30 回归记录:
- RPG 例外边界指定 interaction 测试通过。
- Bark Battle 创作入口、结果页试玩/发布和正式 runtime 指定 interaction 测试通过。
- 视觉小说工作台、生成阶段、结果页和运行态测试通过,`npm run check:visual-novel-vn11` 通过。
### Phase 5:结果页、发布、作品架与广场收口
目标:把“创作页统一”推进到“交付链路统一”。
- 发布成功默认进入统一作品详情或明确的 runtime 去向。
- 草稿架能恢复生成中、失败、待发布和已发布状态。
- 公开列表、发现流、详情页优先消费后端 read model 或 BFF 缓存。
- 跨流程错误统一进入 `PlatformErrorDialog`,异步完成统一进入 `PlatformTaskCompletionDialog`
- 私有 generated 图片展示前必须换签。
退出条件:
- 每个可发布玩法都有作品架、公开详情、广场或明确不公开声明。
- 生成中刷新、失败重试、发布后回读和登录切换都有测试或手测记录。
状态:已收口。
2026-05-30 回归记录:
- 发布到作品详情、已发布作品进入详情、体验按钮直达 runtime、详情 profile 回读指定 interaction 测试通过。
- 拼图已发布作品进入首页和移动端游戏分类、Big Fish 公开隐藏口径、Match3D 推荐 runtime 资源回读指定 interaction 测试通过。
- 公开详情作者展示统一为“公开昵称 · 陶泥号”,`PlatformWorkDetailView` 与公共作者展示 helper 测试已回归。
- 本地 API smoke 已在重新拉起 `dev:spacetime``dev:api-server` 后通过:`GET http://127.0.0.1:8082/healthz` 返回 `{"ok":true,"service":"genarrative-api-server"}`
### Phase 6:全链路验收与冻结
目标:形成后续新增玩法可复用的稳定门禁。
- 按玩法输出创作入口、生成页、结果页、试玩、发布、作品架、广场、runtime smoke 矩阵。
- 增补 `quality-gates/README.md` 与跨玩法回归 / 冒烟门禁,不再只覆盖首批四条链路。
- 固化移动端竖屏优先验收,桌面端作为兼容验证。
- 补齐“新增玩法接入 PRD 检查块”和代码评审检查清单。
退出条件:
- 全部计划内玩法均有明确状态:已统一、例外接入、暂不接入。
- `npm run typecheck``npm run check:encoding` 和对应玩法门禁通过。
状态:已收口。
2026-05-30 冻结记录:
- Phase 2 自动回归通过:入口配置、统一字段 spec、统一创作页和统一生成页测试共 11 项。
- Phase 3 自动回归通过:拼图、抓大鹅、敲木鱼、跳一跳、宝贝识物、大鱼、方洞结果页相关测试共 67 项;跳一跳直达结果页恢复测试通过。
- Phase 4 / Phase 5 指定交互回归通过:RPG 例外边界、Bark Battle 闭环、作品详情、推荐 runtime、公开 read model 与跳一跳恢复相关 interaction 测试共 18 项。
- Phase 5 详情 / 弹窗 / 作品架回归通过:公开详情、错误弹窗、反馈弹窗、作品展示 helper、作品架交互测试共 63 项。
- `npm run check:visual-novel-vn11``npm run check:spacetime-schema``npm run check:encoding` 均通过。
- API smoke 通过:`GET http://127.0.0.1:8082/healthz` 返回 `{"ok":true,"service":"genarrative-api-server"}`
- 竖屏浏览器 smoke 通过并保存截图:`.app/browser-check/phase-flow-20260530-round5/`,覆盖 `/creation/jump-hop``/creation/visual-novel``/creation/square-hole``/creation/bark-battle``/creation/baby-object-match``/creation/jump-hop/generating``/creation/jump-hop/result``/runtime/jump-hop`
### Phase 6 补充:跨玩法最小验收口径
Phase 6 不再继续拆新波次,当前只把 Phase 2 到 Phase 5 的最小验证集合收束成一份可直接执行的门禁矩阵。建议顺序如下:
| 阶段 | 最小命令 | 说明 |
| --- | --- | --- |
| Phase 2 | `npm run check:encoding``npm run typecheck``npm run admin-web:typecheck``npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx` | 校验入口配置、统一字段 spec、统一创作页和统一生成页。 |
| Phase 3 | `npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/jump-hop-result/JumpHopResultView.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/big-fish-creation/BigFishAgentWorkspace.interaction.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"` | 校验普通表单 / 图片 / 音频工作台仍按结构化 payload 提交,跳一跳结果页直达恢复不白屏,并把 BabyObjectMatch / BigFish 一并纳入最小回归。 |
| Phase 4 | `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "opening RPG agent workspace does not refetch session snapshot in a render loop|create tab resumes agent workspace when draft has no compiled result yet|create tab resumes agent workspace when session has no draft profile even if summary counts look compiled|opening a compiled draft with a missing agent session falls back to draft hub"``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab opens bark battle entry form from the template card|bark battle draft result can test before publish and publish to work detail|direct bark battle runtime public code opens published runtime"``npm run check:visual-novel-vn11` | 校验特殊工作台例外、Bark Battle 公开闭环和视觉小说负向门禁。 |
| Phase 5 | `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "agent draft result publishes to gallery from publish panel|creation hub published work enters existing detail view|creation hub published work experience button enters world directly|creation hub published work start uses loaded detail profile instead of library summary"``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "published puzzle works appear on home and mobile game category channel|published big fish works stay hidden from platform home and mobile game category channel|home recommendation Match3D runtime keeps profile generated models when card summary is stale|home recommendation Match3D runtime passes top-level UI background assets|home recommendation Match3D runtime reloads detail when card only has UI assets"``npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformFeedbackView.test.tsx src/components/rpg-entry/rpgEntryWorldPresentation.test.ts``npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx` | 校验结果页、发布、作品架、公开详情、推荐 runtime 和公开 read model,并补公开详情作者展示口径与作品架恢复矩阵。 |
如果这轮还改了 SpacetimeDB schema,追加 `npm run check:spacetime-schema`;如果还改了 API 路由、BFF、公开列表或 `/works/detail` 回读,追加 `npm run dev:api-server`,并另开终端从 `.app/dev-stack.json` 读取实际 `api-server` URL 后检查 `/healthz`
## 可并行任务列表
| 任务 ID | 可并行 | 状态 | 任务 | 主要产出 | 依赖 | 建议 Owner |
| --- | --- | --- | --- | --- | --- | --- |
| T0-1 | 否 | 已完成 | 总计划与文档入口 | 本文档、`docs/planning/README.md`、文档索引更新 | 无 | 文档 owner |
| T1-1 | 否 | 已完成 | Phase 1 收口确认 | 三条首批链路验收记录、已知风险 | T0-1 | 前端 owner |
| T2-1 | 是 | 已完成 | `unifiedCreationSpec` 契约审计 | 字段种类、必填、阶段映射、fallback 规则 | T0-1 | 契约 owner |
| T2-2 | 是 | 已完成 | 后台入口配置治理 | 后台配置校验与配置说明 | T2-1 | 后台 owner |
| T2-3 | 是 | 已完成 | 前端入口读取与 fallback 测试 | 入口配置单测、异常兜底测试 | T2-1 | 前端 owner |
| T3-1 | 是 | 自动回归通过 | 跳一跳统一接入方案与实现 | 统一创作工作台、生成页迁移、验收 | T2-1 | 玩法 owner A |
| T3-2 | 是 | 自动回归通过 | 宝贝识物统一接入方案与实现 | 创作页/生成页迁移、验收 | T2-1 | 玩法 owner B |
| T3-3 | 是 | 最小回归通过 | 方洞工作台形态评估与迁移方案 | 保留 Agent 形态还是迁表单的决策、边界和风险清单 | T2-1 | 玩法 owner C |
| T3-4 | 是 | 最小回归通过 | 大鱼工作台形态评估与迁移方案 | 保留 Agent 形态还是迁表单的决策、边界和风险清单 | T2-1 | 玩法 owner D |
| T4-1 | 是 | 自动回归通过 | RPG 例外边界设计 | 例外声明、阶段映射、验收清单 | T2-1 | 特殊玩法 owner |
| T4-2 | 是 | 自动回归通过 | 视觉小说例外边界设计 | 例外声明、阶段映射、验收清单 | T2-1 | 特殊玩法 owner |
| T4-3 | 是 | 自动回归通过 | 汪汪声浪(bark-battle)统一/例外决策 | 接入或例外方案、验收清单 | T2-1 | 特殊玩法 owner |
| T5-1 | 是 | 最小回归通过 | 统一结果页能力矩阵 | 每个玩法结果页能力与缺口表 | T3/T4 方案稳定 | 结果页 owner |
| T5-2 | 是 | 最小回归通过 | 作品架恢复矩阵 | 生成中、失败、待发布、已发布恢复验收 | T3/T4 方案稳定 | 作品架 owner |
| T5-3 | 是 | 最小回归通过 | 公开 read model 对齐 | 广场/详情/分享码缺口与执行清单 | T3/T4 方案稳定 | 后端 owner |
| T5-4 | 是 | 最小回归通过 | 统一错误与完成反馈回归 | `PlatformErrorDialog``PlatformTaskCompletionDialog` 覆盖 | T3/T4 方案稳定 | 平台壳 owner |
| T5-5 | 是 | 自动回归通过 | 统一创作壳滚动收口 | `UnifiedCreationPage` 统一接管四条入口滚动、跳一跳纳入统一壳 | T3/T4 方案稳定 | 平台壳 owner |
| T6-1 | 否 | 已完成 | 全链路质量门禁扩展 | `quality-gates/README.md`、跨玩法回归 / 冒烟门禁、Phase 2 到 Phase 5 最小验证集合 | T3/T4/T5 完成 | QA owner |
| T6-2 | 否 | 已完成 | 全量验收与冻结 | 状态表、未接入声明、最终测试记录 | T6-1 | Release owner |
并行原则:
- T2 系列已完成,T3/T4 已进入回归;后续缺口修补仍不得绕过 T2 已固定的入口配置和统一 spec 规则。
- T3 各玩法可并行,但同一文件同一时间只允许一个 owner,尤其是 `PlatformEntryFlowShellImpl.tsx`、路由和 shared contracts。
- T5 可以在 T3/T4 各玩法方案稳定后分块并行,不必等所有玩法实现完再开始。
- T6 必须串行收尾,避免验收矩阵和实际实现漂移。
## 可直接派发的任务包
任务包是执行层最小分工单元。每个包都可以单独开分支、单独验收;如果两个包需要改同一个公共文件,先由公共 owner 合并接口或壳层改动,再由玩法 owner 接入,避免互相覆盖。
| 包 ID | 可并行对象 | 状态 | 目标 | 不做什么 | 交付物 | 验收 |
| --- | --- | --- | --- | --- | --- | --- |
| P2-A 契约包 | 可与 P2-B、P2-C 并行 | 已完成 | 固定 `unifiedCreationSpec` 字段类型、必填、阶段映射、fallback 规则 | 不接新玩法,不改 UI 设计方向 | 前后端契约、配置字段文档、契约测试 | `npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/platform-entry/platformEntryCreationTypes.test.ts`;涉及 schema 时追加 `npm run check:spacetime-schema` |
| P2-B 后台配置包 | 可与 P2-A、P2-C 并行 | 已完成 | 后台入口配置能编辑、校验、保存统一创作契约 | 不做玩法工作台迁移 | 后台表单、保存校验、异常提示、后台单测 | `npm run admin-web:typecheck``npm run test -- apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx` |
| P2-C 前台读取包 | 可与 P2-A、P2-B 并行 | 已完成 | 前台从 `/api/creation-entry/config` 读取统一 spec,旧后端只走兜底 | 不把 fallback 当事实源,不恢复硬编码入口 | 入口派生、fallback 单测、统一创作/生成页回归 | `npm run test -- src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx` |
| P3-A 跳一跳接入包 | 可与 P3-B、P3-C、P3-D 并行 | 自动回归通过 | `jump-hop` 接入统一创作壳、生成页或明确跳过策略 | 不改正式 runtime 规则真相,不重做作品架 | 入口阶段映射、统一工作台接入、生成/结果跳转、竖屏验收 | 跳一跳相关单测、统一创作页回归、移动端 `/creation/jump-hop` smoke |
| P3-B 宝贝识物接入包 | 可与 P3-A、P3-C、P3-D 并行 | 自动回归通过 | `baby-object-match` 接入统一创作壳、生成页或明确跳过策略 | 不复制上传/历史素材逻辑 | 工作台接入、资产槽位复用、结果跳转、竖屏验收 | 宝贝识物相关单测、统一创作页回归、移动端 `/creation/baby-object-match` smoke |
| P3-C 方洞评估包 | 可与 P3-A、P3-B、P3-D 并行 | 部分回归通过 | 判断 `square-hole` 保留 Agent 形态还是迁表单/图片工作台 | 不直接大改实现 | 例外或迁移方案、字段清单、风险、验收用例 | 文档评审通过;若改代码,补对应工作台测试 |
| P3-D 大鱼评估包 | 可与 P3-A、P3-B、P3-C 并行 | 部分回归通过 | 判断 `big-fish` 保留 Agent 形态还是迁表单/图片工作台 | 不直接大改实现 | 例外或迁移方案、字段清单、风险、验收用例 | 文档评审通过;若改代码,补对应工作台测试 |
| P4-A RPG 例外包 | 可与 P4-B、P4-C 并行 | 自动回归通过 | 明确 RPG 对话式工作台如何接入统一阶段、错误、完成、发布去向 | 不把 RPG 当新增玩法默认模板 | 例外声明、阶段映射、刷新恢复和发布验收 | RPG 指定 interaction 测试、作品详情/进入世界回归 |
| P4-B 视觉小说例外包 | 可与 P4-A、P4-C 并行 | 自动回归通过 | 明确视觉小说特殊生成和结果页边界 | 不迁入外部平台社区、支付、榜单、回放 | 例外声明、上传资产口径、生成/结果/发布验收 | `npm run check:visual-novel-vn11` 和视觉小说相关测试 |
| P4-C 汪汪声浪决策包 | 可与 P4-A、P4-B 并行 | 自动回归通过 | 判断 `bark-battle` 是特殊工作台例外还是回到表单模式 | 不同时做两套入口 | 决策记录、阶段映射、发布和 runtime 验收 | Bark Battle 创作、发布、runtime 指定测试 |
| P5-A 结果页矩阵包 | 可与 P5-B、P5-C、P5-D 并行 | 最小回归通过 | 列清每个玩法结果页能力、缺口和最小补丁 | 不在结果页新增无需求的大功能 | 结果页能力矩阵、局部重试/上传/发布边界 | 对应结果页测试和手测记录 |
| P5-B 作品架恢复包 | 可与 P5-A、P5-C、P5-D 并行 | 最小回归通过 | 生成中、失败、待发布、已发布都能从作品架恢复 | 不只靠前端内存 notice | 作品摘要字段、作品架 adapter、恢复测试 | 作品架相关 interaction 测试;移动端草稿 Tab smoke |
| P5-C 公开 read model 包 | 可与 P5-A、P5-B、P5-D 并行 | 最小回归通过 | 公开列表、详情、分享和推荐 runtime 对齐后端 read model | 不让前端拼源表当事实源 | 后端 read model/BFF 缺口清单和实现 | 公开列表/详情/API smoke;必要时 `npm run dev:api-server` + `/healthz` |
| P5-D 统一反馈包 | 可与 P5-A、P5-B、P5-C 并行 | 最小回归通过 | 错误和异步完成统一进入平台弹窗 | 不在页面内重复裸错误 banner | `PlatformErrorDialog``PlatformTaskCompletionDialog` 覆盖矩阵 | 平台弹窗测试、跨流程失败/完成手测 |
| P6-A 门禁包 | 串行,依赖 P3/P4/P5 | 已完成 | 固化跨玩法自动测试、竖屏手测和 API smoke | 不继续接新玩法 | `quality-gates/`、冻结前命令集合 | 跨玩法回归与冒烟门禁通过 |
| P6-B 冻结包 | 串行,依赖 P6-A | 已完成 | 更新总状态表,标记已统一、例外接入、暂不接入 | 不遗留“状态未知”玩法 | 总进度、风险清单、后续维护规则 | `npm run check:encoding`、关键门禁通过、文档索引有效 |
### 并行执行注意事项
- 公共壳层 owner 统一负责 `PlatformEntryFlowShellImpl.tsx``appPageRoutes.ts`、入口阶段类型、共享 contract 和统一生成页主流程;玩法 owner 只接自己的工作台和映射。
- 后端 schema owner 统一负责 SpacetimeDB 表、`migration.rs`、表目录和 bindings;玩法 owner 不单独改 schema 后跳过生成绑定。
- 文档 owner 每轮只更新本计划的状态、波次和风险,不把一次性聊天记录写进长期文档。
- 同一波次内如果发现计划和代码事实冲突,先改计划和对应融合文档,再继续实现;不要在代码里临时绕开统一链路。
### 依赖关系摘要
- 可以并行启动:`T3-1``T3-2``T3-3``T3-4`,其中 `T3-3``T3-4` 先做形态评估,`T3-1``T3-2` 可以直接进入实现。
- 可以并行启动:`T4-1``T4-2``T4-3`,但它们只做例外边界和决策,不直接扩散到新的玩法实现。
- `T5-1``T5-2``T5-3``T5-4` 可以并行预研,但最好等至少一批 Phase 3 / 4 方案稳定后再落代码。
- `T6-1``T6-2` 必须串行,且都依赖 Phase 3 到 Phase 5 的验证结果。
## 并行波次
### 波次 A:先定契约
状态:已完成。
- `T2-1` `unifiedCreationSpec` 契约审计
- `T2-2` 后台入口配置治理
- `T2-3` 前端入口读取与 fallback 测试
说明:三项可以并行,先把统一创作入口的真值源、后台编辑面和前端兜底一起收紧。
### 波次 B:第一批迁移与例外评估
状态:自动回归已通过,遗留形态评估按缺口任务继续跟踪。
- `T3-1` `jump-hop` 统一接入
- `T5-5` 统一创作壳滚动收口
- `T3-2` `baby-object-match` 统一接入
- `T3-3` `square-hole` 工作台形态评估
- `T3-4` `big-fish` 工作台形态评估
- `T4-1` `RPG` 例外边界设计
- `T4-2` `视觉小说` 例外边界设计
- `T4-3` `汪汪声浪(bark-battle` 统一/例外决策
说明:`jump-hop``baby-object-match` 已完成最小接入回归;`square-hole``big-fish` 的形态评估继续按缺口任务跟踪;特殊工作台例外边界已完成最小回归。
### 波次 C:交付链路收口
状态:最小回归通过。
- `T5-1` 统一结果页能力矩阵
- `T5-2` 作品架恢复矩阵
- `T5-3` 公开 read model 对齐
- `T5-4` 统一错误与完成反馈回归
说明:交付链路已按页面 / read model / 弹窗分块完成最小回归,后续只处理验收发现的真实缺口。
### 波次 D:验收与冻结
状态:已收口。
- `T6-1` 全链路质量门禁扩展
- `T6-2` 全量验收与冻结
说明:必须串行,先补门禁再冻结状态表。
## 验收矩阵
每个玩法推进时至少记录:
| 验收项 | 要求 |
| --- | --- |
| 创作入口 | 从创作 Tab 或直达 URL 能进入对应工作台,入口事实源来自 `/api/creation-entry/config`。 |
| 创作页 | 统一标题/阶段壳存在,工作台不重复渲染巨大旧标题,移动端可滚动到提交按钮。 |
| 输入控件 | 图片、音频、文本、选择器复用现有通用组件,不复制上传/历史图逻辑。 |
| 生成页 | 自动生成玩法使用统一圆环生成页;无生成页玩法有明确跳转策略。 |
| 结果页 | 能展示草稿、编辑作品信息、处理局部重生成、试玩和发布。 |
| 恢复 | 刷新、退出登录、生成中、失败、作品架恢复都有可观察行为。 |
| 发布与公开 | 发布后能进入统一详情或 runtime;公开列表/read model 不靠前端拼源表。 |
| runtime | 试玩与正式运行态区分清楚,正式业务真相以后端为准。 |
| 移动端 | 竖屏优先,无按钮遮挡、套滚动、文字溢出或固定底栏遮挡。 |
## 统一约束
- 不恢复前端硬编码入口配置。
- 不新建平行创作入口系统、平行作品架或平行公开列表。
- 不把功能说明、规则说明或开发解释默认写进 UI 面板。
- 不让前端承接发布、计分、胜负、资产持久化或公开状态等业务真相。
- 后端仍按 `server-rs + Axum + SpacetimeDB` 和 DDD 分层推进。
- 涉及 SpacetimeDB schema 时必须同步 migration、表目录、生成绑定,并运行 schema 检查。
## 推荐推进顺序
1. 将 Round 4 / Phase 6 作为当前冻结基线,后续只做同口径回归,不再新增波次。
2. 对 Phase 3、Phase 4、Phase 5 只处理回归发现的真实缺口,不扩新玩法。
3. 更新冻结状态表,明确每个玩法是已统一、例外接入还是暂不接入。
4. 后续新增玩法默认遵循本文档和 `quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`,无需再补新的总计划轮次。
当前轮次看 Round 4 / Phase 6Round 0~4 已作为历史完成记录保留。
@@ -80,7 +80,7 @@ jump-hop-gallery-detail
新增前端组件建议:
1. `src/components/jump-hop-creation/JumpHopWorkspace.tsx`
1. `src/components/unified-creation/workspaces/JumpHopCreationWorkspace.tsx`
2. `src/components/jump-hop-result/JumpHopResultView.tsx`
3. `src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`
4. `src/services/jump-hop/jumpHopClient.ts`
@@ -337,8 +337,8 @@ npm run check:server-rs-ddd
- Rust 结构体:`CreationEntryTypeConfig`
- 源码:`server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
- 字段:`id``title``subtitle``badge``image_src``visible``open``sort_order``updated_at``category_id``category_label``category_sort_order`
- 迁移兼容:旧迁移包缺少入口分类字段时,由 `migration.rs` 写入 `None` / `0` 默认值;入口分组展示由 `module-runtime` 和前端展示派生消费。
- 字段:`id``title``subtitle``badge``image_src``visible``open``sort_order``updated_at``category_id``category_label``category_sort_order``unified_creation_spec_json`
- 迁移兼容:旧迁移包缺少入口分类字段或统一创作契约字段时,由 `migration.rs` 写入 `None` / `0` / `None` 默认值;入口分组展示由 `module-runtime` 和前端展示派生消费,统一创作契约由 `module-runtime` 解析为 `creationTypes[].unifiedCreationSpec`,为空时只回退首批 `puzzle``match3d``wooden-fish` 默认 spec
### `custom_world_agent_message`
@@ -31,6 +31,8 @@ npm run dev
- 主站 Vite。
- 后台 Vite。
`npm run dev` 和单模块 `npm run dev:web``npm run dev:api-server``npm run dev:spacetime``npm run dev:admin-web` 启动后都会更新根目录 `.app/dev-stack.json`。该文件记录本次命令、数据库、更新时间,以及 `spacetime``api-server``web``admin-web``pid`、监听 host / port、可访问 URL、启动状态和当前命令。`.app/` 是本地运行态目录,不提交 Git;端口漂移、服务重启或子进程退出后以该文件里的实际状态为准。
单独启动主站前端:
```bash
@@ -94,6 +96,8 @@ npm run build
npm run check:content
```
一期创作流程统一化新增 `quality-gates/` 提交前门禁。涉及拼图、抓大鹅、敲木鱼统一创作页、统一生成页或 dev 栈启动脚本时,先执行 `quality-gates/README.md` 列出的脚本,再按对应门禁文档完成体验检查。
综合检查:
```bash
@@ -8,7 +8,9 @@
当前创作 Tab 只承载赛事 banner、玩法模板分类和两列模板卡;点击模板卡后直接进入对应玩法已有的入口创作表单 stage,不再经过空白占位页,也不把旧表单嵌进创作 Tab 首屏。移动端创作 Tab 顶栏在 `陶泥儿` 品牌同一行显示真实账户泥点数,数据来自 `profileDashboard.walletBalance`,不得再把活动奖池当作账号余额展示。首屏 banner 结构按参考图拆成横向可滑动赛事卡、主体宣传图文区、奖池胶囊、开始 / 结束时间条和卡片内分页点;轮播只保留 `拼图主题创作赛``抓大鹅主题创作赛`,两个主题赛事奖池均为 `1000` 泥点数。玩法列表不再套外部边框卡片,移动端需要压缩横向边距和两列间距;玩法卡统一按“上图、左上状态标签(仅非开放态显示)、封面右下 `10-20泥点数`、下方白底标题/描述”结构展示,卡片高度保持紧凑但标题、描述和预估消耗点数都必须可见。创作 Tab 根容器不再使用 `platform-page-stage` 这类全局内容卡片壳,但继续保留 `platform-remap-surface` 作为主题和输入框样式命中钩子。创作首屏字号需要对齐平台普通 UI 档位:顶栏泥点组件、banner 正文、分类 Tab 和玩法卡标题 / 副标题 / 消耗说明优先使用 `11px``14px`,不使用 `text-lg``text-xl` 或更大的展示级字号。草稿 Tab 继续承接作品架。RPG、RPG 之外的各玩法入口分别落到既有的 `agent-workspace``big-fish-agent-workspace``match3d-agent-workspace``square-hole-agent-workspace``jump-hop-workspace``wooden-fish-workspace``puzzle-agent-workspace``bark-battle-workspace``visual-novel-agent-workspace``baby-object-match-workspace`,这些入口继续承接各玩法自己的表单、草稿恢复和后续编排,不作为创作 Tab 首屏内容。
创作恢复参数只保留 `sessionId``profileId``draftId``workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。生成页恢复时只认当前进入页的时间作为新`startedAtMs`,作品摘要里的 `updatedAt` 只用于排序与摘要展示,不作为生成进度起点
创作恢复参数只保留 `sessionId``profileId``draftId``workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。生成页等待时间统一以生成状态里`startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 只用于排序与摘要展示,不作为前端自行推导业务状态的真相
一期创作流程统一化覆盖拼图、抓大鹅、跳一跳和敲木鱼。四者在前端统一经过 `UnifiedCreationWorkspace``UnifiedGenerationPage``UnifiedCreationWorkspace` 作为平台壳唯一依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace``Match3DCreationWorkspace``JumpHopCreationWorkspace``WoodenFishCreationWorkspace`;创作页字段清单由后端在 `GET /api/creation-entry/config``creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec;首期字段类型只保留 `text``select``image``audio``UnifiedCreationPage` 提供统一标题栏、统一返回入口、页面级纵向滚动、内容区和隐藏字段契约,不在 UI 中额外展示字段说明 chip;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。拼图、抓大鹅、跳一跳和敲木鱼的工作台实现都已收口到统一目录,只保留各自输入逻辑、素材选择和提交校验,不再由平台壳直接依赖旧工作台文件。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。视觉小说、`airp``component`、汪汪声浪、方洞、大鱼和宝贝识物不进入一期接线范围,已有链路保持现状。
创作表单提交前的泥点余额前置校验只允许用独立弹窗提示失败原因,不得把用户退回创作入口或玩法模板列表,也不得清空当前表单状态。当前适用拼图、抓大鹅和汪汪声浪等会在前端提交前校验泥点的生成入口;余额不足、余额读取失败都应停留在当前工作台,由用户关闭提示后继续编辑或自行补足泥点。
@@ -36,7 +38,7 @@
通用系列素材图集能力的实现真相源在 `platform-image::generated_asset_sheets``n` 是必选参数,模块负责组装 `n*n` sheet prompt、按 `n*n` 切片、绿幕 / 近白底透明化、导出 PNG 和 OSS 持久化请求。`api-server::generated_asset_sheets` 只保留 `AppError` / `AppState` 适配,不再承载图像处理和 OSS 请求构造细节。物品名称 prompt 和特殊设定 prompt 是可选输入;调用方可传入类似“每个物品生成五个不同视图”的视角约束,通用模块会把 sheet prompt、物品行 prompt、特殊设定 prompt 编码写入 OSS 元数据。玩法仍负责计费、物品规划、slot 映射、失败回写和把通用切片结果映射回自己的草稿 / profile / runtime 字段。
当前所有玩法生成页 UI 统一收敛为圆环主视觉:`media/create_bg_video.mp4` 作为生成页固定全屏背景层循环静音播放,主进度圆环居中覆盖在背景之上,围绕陶泥儿视觉展示;页面只保留当前步骤名称和当前步骤进度,不再渲染步骤列表块。视频层需要显式触发播放,不能只依赖 `autoPlay/loop/muted` 属性。共用生成页 `CustomWorldGenerationView` 和汪汪声浪生成页都必须遵循这一口径。
当前所有玩法生成页 UI 统一收敛为圆环主视觉:`media/create_bg_video.mp4` 作为生成页固定全屏背景层循环静音播放,主进度圆环居中覆盖在背景之上,围绕陶泥儿视觉展示;页面只保留当前步骤名称和当前步骤进度,不再渲染步骤列表块。视频层需要显式触发播放,不能只依赖 `autoPlay/loop/muted` 属性。圆环内部保持 `400x400` SVG 坐标系,外层显示宽度以 `400px` 为上限,窄屏按视口宽度收缩,预计等待 / 已耗时信息卡在窄屏下落到圆环下方,避免右侧裁切。共用生成页 `CustomWorldGenerationView` 和汪汪声浪生成页都必须遵循这一口径。
## 草稿与作品架
@@ -1,6 +1,6 @@
# 生成页圆环布局口径
更新时间:`2026-05-24`
更新时间:`2026-05-30`
## 目标
@@ -12,8 +12,9 @@
- 生成页背景视频必须留在生成页容器内部,直接作为 `fixed inset-0` 的底层背景,不要再通过 portal 挂到 `document.body`;页面根容器使用 `z-[1]`、背景容器使用 `z-0`,确保顶部导航、圆环和当前步骤卡都稳定覆盖在视频之上。
- 预计等待 / 已耗时信息卡要压缩为更轻的半透明窄卡,标签使用 `9px-10px`,数值使用 `12px-13px`,字号对齐其他生成页 UI 的小字号,不再使用偏大的提示文本;卡片标题和时间值都居中显示,两个数值只展示时间本身,调用侧不要再拼接“预计还需”或“已耗时”前缀。圆环中心不再保留独立白底块,空心圆环只保留条状进度,圆弧半径继续加大,进度数字与“总进度”标题整体上移,靠近圆环上半区。
- 顶部导航区采用“返回创作中心 / 状态胶囊”结构,返回按钮使用左箭头图标,字号使用 `text-xs-sm`,状态胶囊使用 `11px-12px`,展示 `素材生成中``草稿生成中` 等调用侧传入文案。
- 圆弧区域不再包独立大卡片,左右悬浮信息卡只展示“预计等待”和“已耗时”;总进度数值放在圆弧内侧偏上的位置并保持更小字号。圆环本体固定在 `400x400` 的 SVG 画布上,圆弧使用 `r=166``strokeWidth=18` 的 SVG 描边,不再跟随页面宽度缩放,也不再使用 `conic-gradient + mask`,避免进度条边缘模糊。
- 圆弧描边以圆心为中心整体按 `135deg` 起始;`270deg` 扫描角配合 `90deg` 正下方缺口时,轨道和填充都从同一个对称起点出发,轨道保持 `rotate(135 200 200)`,填充端点也使用 `rotate(135 200 200)`。圆环本体尺寸固定,不允许再随容器边长伸缩,只能由外层布局决定放置位置
- 圆弧区域不再包独立大卡片,信息卡只展示“预计等待”和“已耗时”;总进度数值放在圆弧内侧偏上的位置并保持更小字号。圆环内部坐标固定在 `400x400` 的 SVG `viewBox` 上,圆弧使用 `r=166``strokeWidth=18` 的 SVG 描边,外层显示宽度上限为 `400px`;窄屏视口按 `min(400px, calc(100vw - 2.5rem))` 等比收缩,避免等待页右侧被裁切。圆弧不再使用 `conic-gradient + mask`,避免进度条边缘模糊。
- 圆弧描边以圆心为中心整体按 `135deg` 起始;`270deg` 扫描角配合 `90deg` 正下方缺口时,轨道和填充都从同一个对称起点出发,轨道保持 `rotate(135 200 200)`,填充端点也使用 `rotate(135 200 200)`。圆环只允许在可用宽度不足 `400px` 时等比收缩,不随宽屏容器继续放大
- 在窄屏下,预计等待 / 已耗时信息卡放到圆环下方两列排布;`sm` 及以上视口再回到圆环左右悬浮,避免左右悬浮卡和圆环共同超过视口宽度。
- 总进度标题和百分比数字必须显式高于 SVG 圆环层级渲染,避免被圆环边缘压住;圆环本身只做背景层,不抢文字层。
- 总进度标题和百分比数字要比圆环再上移一点,当前内容区上边距以 `pt-[2%]` 为准,桌面端可进一步微调到 `sm:pt-[1.5%]`,确保数字不与进度条弧线重合。
- 从作品架或刷新后的持久化生成中草稿进入生成页时,前端必须重置“展示态 startedAtMs”为进入生成页的当前时间;后端 `progressPercent` 只用于后续真实步骤推进,不得参与首帧总进度展示,避免恢复生成页首帧直接显示 `80%+`
@@ -28,4 +29,5 @@
- `src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx` 覆盖汪汪声浪生成页对齐后的圆环布局。
- 两个生成页都应在测试里断言页面根容器层级高于背景视频容器,且背景视频确实是页面子节点,避免 portal 背景把业务 UI 压住。
- 还应断言圆弧正下方留空、圆环中心没有独立底色块,时间卡和总进度字号缩小后仍能在桌面与移动端正常排版;同时断言时间卡 `text-center`、标题行 `justify-center`、总进度内容区上移到 `pt-[2%]`,桌面端保持 `sm:pt-[1.5%]`,圆弧 DOM 为 SVG,包含清晰的 track/fill circle 描边。
- 还应断言圆环容器使用 `w-[min(400px,calc(100vw-2.5rem))]``max-w-full``aspect-square`,窄屏截图至少覆盖 `280px / 320px / 360px / 390px` 宽度,确认右侧没有硬性裁切。
- 页面在桌面和移动端都不应再出现生成步骤列表块,圆环和当前步骤卡不能被外层卡片嵌套出双层面板感。
+27
View File
@@ -0,0 +1,27 @@
# 提交前质量门禁
本目录记录当前创作流程统一化提交前必须执行的检查。门禁既可以是自动脚本,也可以是需要人工确认的体验流程。
## 当前必跑
- [【玩法创作】统一创作页门禁-2026-05-29.md](./【玩法创作】统一创作页门禁-2026-05-29.md)
- [【玩法创作】统一生成页门禁-2026-05-29.md](./【玩法创作】统一生成页门禁-2026-05-29.md)
- [【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md](./【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md)
- [【开发运维】dev-stack状态文件门禁-2026-05-29.md](./【开发运维】dev-stack状态文件门禁-2026-05-29.md)
## 快速基线
```bash
npm run check:encoding
npm run typecheck
npm run admin-web:typecheck
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx
npm run test -- src/components/unified-creation/UnifiedCreationWorkspace.test.tsx
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx
```
## 使用说明
- `统一创作页门禁``统一生成页门禁` 仍作为 Phase 2 的基础门禁。
- `跨玩法回归与冒烟门禁` 覆盖 Phase 2 之后的最小验证集合,Phase 5 / Phase 6 冻结前以它为主。
- `dev-stack` 状态文件门禁继续单独维护,不并入玩法回归矩阵。
@@ -0,0 +1,16 @@
# dev-stack 状态文件门禁
## 自动检查
```bash
node --check scripts/dev.mjs
npm run test -- scripts/dev.test.ts
```
## 体验检查
- `npm run dev` 启动后根目录生成 `.app/dev-stack.json`
- `npm run dev:web``npm run dev:api-server``npm run dev:spacetime``npm run dev:admin-web` 单独启动时会更新对应服务状态。
- 文件包含 `schemaVersion``command``repoRoot``database``updatedAt` 和四个服务条目。
- 每个服务条目包含 `status``pid``host``port``url``command``updatedAt`
- 端口漂移后文件记录实际端口;服务退出或重启后 PID 与状态会更新。
@@ -0,0 +1,17 @@
# 统一创作页门禁
## 自动检查
```bash
npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts
npm run test -- src/components/unified-creation/UnifiedCreationWorkspace.test.tsx
npm run test -- src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx
```
## 体验检查
- 拼图、抓大鹅、跳一跳、敲木鱼均从 `/creation/<playId>` 进入创作工作台。
- 四条链路都经过 `UnifiedCreationPage`,字段 spec 只包含 `text``select``image``audio` 四类。
- 拼图参考图仍走 `CreativeImageInputPanel`,不新增专属上传入口。
- 敲木鱼音频槽位走 `CreativeAudioInputPanel`,上传、录音、重置和默认音效状态可用。
- 抓大鹅难度只显示轻松、标准、进阶、硬核四档,提交 payload 仍派生 `clearCount``difficulty`
@@ -0,0 +1,16 @@
# 统一生成页门禁
## 自动检查
```bash
npm run test -- src/components/unified-creation/UnifiedGenerationPage.test.tsx
npm run test -- src/services/miniGameDraftGenerationProgress.test.ts
```
## 体验检查
- `/creation/puzzle/generating``/creation/match3d/generating``/creation/wooden-fish/generating` 均渲染 `UnifiedGenerationPage`
- 生成页展示后端阶段、当前步骤、总进度、错误和重试动作。
- 等待时间以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 时间戳。
- 失败后生成页保持可见错误和重试入口,作品架恢复时不得只依赖前端内存 notice。
- 不为一期新增客户端直连 SpacetimeDB 状态通道。
@@ -0,0 +1,94 @@
# 跨玩法回归与冒烟门禁
## 适用范围
- Phase 2 的配置与统一壳收口。
- Phase 3 的普通工作台接入。
- Phase 4 的特殊工作台与例外边界。
- Phase 5 的结果页、发布、作品架、公开详情与运行态收口。
- Phase 6 冻结前的最小验证集合。
## 共同前置
```bash
npm run check:encoding
npm run typecheck
npm run admin-web:typecheck
npm run check:spacetime-schema
```
## Phase 2:配置与统一壳
```bash
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx
```
- 验证 `creation-entry config``unifiedCreationSpec`、统一创作页和统一生成页的阶段映射一致。
- 需要改 SpacetimeDB schema 时,再追加 `npm run check:spacetime-schema`
## Phase 3:普通工作台
```bash
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/jump-hop-result/JumpHopResultView.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/big-fish-creation/BigFishAgentWorkspace.interaction.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx src/components/square-hole-result/SquareHoleResultView.test.tsx
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"
```
- 验证表单 / 图片 / 音频工作台仍提交结构化 payload,不会退回聊天式输入。
- 跳一跳需同时覆盖工作台结构化提交、结果页试玩 / 发布 / 局部重生成入口、结果页恢复面板和 runtime 起跳 / 重开 / 返回。
- 宝贝识物与大鱼吃小鱼也要纳入同一批最小回归,避免只验首批或单一工作台。
- 方洞结果页需覆盖“保存成功后再试玩 / 发布”的路径,避免测试绕过真实保存链路。
## Phase 4:特殊工作台与例外
```bash
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "opening RPG agent workspace does not refetch session snapshot in a render loop|create tab resumes agent workspace when draft has no compiled result yet|create tab resumes agent workspace when session has no draft profile even if summary counts look compiled|opening a compiled draft with a missing agent session falls back to draft hub"
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab opens bark battle entry form from the template card|bark battle draft result can test before publish and publish to work detail|direct bark battle runtime public code opens published runtime"
npm run test -- src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx src/components/visual-novel-creation/visualNovelEntryGeneration.test.ts
npm run check:visual-novel-vn11
```
- 验证 RPG 例外边界、Bark Battle 的创作 / 发布 / 正式 runtime 闭环,以及视觉小说入口、生成页、结果页、运行态和负向扫描门禁。
## Phase 5:结果页与公开闭环
```bash
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "agent draft result publishes to gallery from publish panel|creation hub published work enters existing detail view|creation hub published work experience button enters world directly|creation hub published work start uses loaded detail profile instead of library summary"
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "published puzzle works appear on home and mobile game category channel|published big fish works stay hidden from platform home and game category channel|home recommendation Match3D runtime keeps profile generated models when card summary is stale|home recommendation Match3D runtime passes top-level UI background assets|home recommendation Match3D runtime reloads detail when card only has UI assets"
npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformFeedbackView.test.tsx src/components/rpg-entry/rpgEntryWorldPresentation.test.ts
npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx
```
- 验证结果页、发布、作品架、公开详情、推荐 runtime 和公开 read model 的最小回归面。
- 验证作品架恢复矩阵、公开详情作者展示和跨流程错误 / 反馈弹窗仍和统一链路一致。
- 若本轮还改了 API 路由、BFF、公开列表或 `/works/detail` 回读,再补 API smoke。`npm run dev:api-server` 是长驻进程,另开终端执行:
```bash
curl -fsS "$(node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(".app/dev-stack.json","utf8")); const services=s.services||{}; const svc=services["api-server"]||services.apiServer||{}; const url=String(svc.url||s.apiTarget||"http://127.0.0.1:8082").replace(/\/$/,""); process.stdout.write(`${url}/healthz`);')"
```
## 竖屏浏览器冒烟
自动测试通过后,仍需用竖屏视口确认页面不是空白、未跳错入口、底部操作不被遮挡。当前仓库没有专用 Playwright 脚本时,可用系统 Chrome 生成截图:
```bash
out=.app/browser-check/phase-flow-$(date +%Y%m%d)
mkdir -p "$out"
for route in \
creation-jump-hop:/creation/jump-hop \
creation-visual-novel:/creation/visual-novel \
creation-square-hole:/creation/square-hole \
creation-bark-battle:/creation/bark-battle \
creation-baby-object-match:/creation/baby-object-match \
generating-jump-hop:/creation/jump-hop/generating \
result-jump-hop:/creation/jump-hop/result \
runtime-jump-hop:/runtime/jump-hop
do
name=${route%%:*}
path=${route#*:}
google-chrome --headless=new --no-sandbox --disable-gpu --window-size=390,844 --virtual-time-budget=4000 --screenshot="$out/$name.png" "http://127.0.0.1:3000$path"
done
ls -lh "$out"
```
- 执行前确保 `npm run dev:spacetime``npm run dev:api-server``npm run dev:web` 已按实际端口启动;若 `.app/dev-stack.json` 和实际端口不一致,以 `ss -ltnp``/healthz` 为准。
- 截图只保存本地 `.app/`,不提交 Git。检查时至少确认首屏有对应玩法工作台 / 生成页 / 结果页 / runtime 内容,且竖屏下无明显空白死页、入口跳错、按钮遮挡或套滚动。跳一跳结果页若缺少恢复信息,应显示恢复面板而不是空白页。
+282 -7
View File
@@ -9,7 +9,7 @@ import {
watch,
writeFileSync,
} from 'node:fs';
import {basename, relative, resolve} from 'node:path';
import {basename, join, relative, resolve} from 'node:path';
import {createInterface} from 'node:readline';
import {fileURLToPath} from 'node:url';
@@ -30,6 +30,7 @@ import {
} from './dev-utils.mjs';
const repoRoot = process.cwd();
const devStackStatePath = join(repoRoot, '.app/dev-stack.json');
const serverRsDir = resolve(repoRoot, 'server-rs');
const manifestPath = resolve(serverRsDir, 'Cargo.toml');
const modulePath = resolve(serverRsDir, 'crates/spacetime-module');
@@ -255,6 +256,136 @@ function normalizeServiceName(rawName) {
throw new Error(`未知模块: ${rawName}`);
}
function resolveDevStackStatePath(root = repoRoot) {
return join(root, '.app/dev-stack.json');
}
function buildDevStackSnapshot(runner, updatedAt = new Date().toISOString()) {
const services = {};
for (const serviceName of SERVICE_NAMES) {
services[serviceName] = buildDevStackServiceSnapshot(
runner,
serviceName,
updatedAt,
);
}
return {
schemaVersion: 1,
command: runner.command ?? 'all',
repoRoot,
database: runner.options.database,
watch: Boolean(runner.options.watch),
updatedAt,
services,
};
}
function buildDevStackServiceSnapshot(runner, serviceName, updatedAt) {
const service = runner.services.get(serviceName);
const runtime = service?.runtime ?? {};
const endpoint = resolveDevStackServiceEndpoint(runner, serviceName);
const isReusedSpacetime =
serviceName === 'spacetime' && runner.state.spacetimeReused;
const runtimeStatus = runtime.status ?? 'idle';
const status =
runtimeStatus === 'idle' && isReusedSpacetime ? 'reused' : runtimeStatus;
const childPid =
service?.child && Number.isInteger(service.child.pid)
? service.child.pid
: null;
return {
status,
pid:
childPid ??
runtime.pid ??
(isReusedSpacetime ? (runner.state.spacetimePid ?? null) : null),
host: runtime.host ?? endpoint.host,
port: runtime.port ?? endpoint.port,
url: runtime.url ?? endpoint.url,
command: runtime.command ?? resolveDevStackServiceCommand(runner, serviceName),
startedAt: runtime.startedAt ?? null,
updatedAt: runtime.updatedAt ?? updatedAt,
exitCode: runtime.exitCode ?? null,
signal: runtime.signal ?? null,
};
}
function resolveDevStackServiceEndpoint(runner, serviceName) {
const {options, state} = runner;
switch (serviceName) {
case 'spacetime':
return {
host: options.spacetimeHost,
port: options.spacetimePort,
url: state.spacetimeServer,
};
case 'api-server':
return {
host: options.apiHost,
port: options.apiPort,
url: state.apiTarget,
};
case 'web':
return {
host: options.webHost,
port: options.webPort,
url: `http://${resolveClientHost(options.webHost)}:${options.webPort}`,
};
case 'admin-web':
return {
host: options.adminWebHost,
port: options.adminWebPort,
url: `http://${state.adminWebTargetHost}:${options.adminWebPort}/admin/`,
};
default:
return {
host: null,
port: null,
url: null,
};
}
}
function resolveDevStackServiceCommand(runner, serviceName) {
const {options, state} = runner;
switch (serviceName) {
case 'spacetime':
return state.spacetimeReused
? `reuse spacetime standalone ${state.spacetimeServer}`
: [
'spacetime',
'start',
'--data-dir',
options.spacetimeDataDir,
'--listen-addr',
`${options.spacetimeHost}:${options.spacetimePort}`,
'--non-interactive',
].join(' ');
case 'api-server':
return 'cargo run -p api-server --manifest-path server-rs/Cargo.toml';
case 'web':
return [
'node',
relative(repoRoot, viteCliPath),
`--port=${options.webPort}`,
`--host=${options.webHost}`,
'--strictPort',
].join(' ');
case 'admin-web':
return [
'node',
relative(adminWebDir, viteCliPath),
`--host=${options.adminWebHost}`,
`--port=${options.adminWebPort}`,
'--strictPort',
].join(' ');
default:
return null;
}
}
function requireCommand(command) {
const result = spawnSync(command, ['--version'], {
cwd: repoRoot,
@@ -384,14 +515,37 @@ function ensureRequiredFiles(command) {
}
class DevService {
constructor(name, startFn) {
constructor(name, startFn, onStateChange = null) {
this.name = name;
this.startFn = startFn;
this.onStateChange = onStateChange;
this.child = null;
this.children = [];
this.logStream = null;
this.stopping = false;
this.restartTimer = null;
this.runtime = {
status: 'idle',
pid: null,
host: null,
port: null,
url: null,
command: null,
startedAt: null,
updatedAt: null,
exitCode: null,
signal: null,
};
}
updateRuntimeState(patch) {
const updatedAt = new Date().toISOString();
this.runtime = {
...this.runtime,
...patch,
updatedAt,
};
this.onStateChange?.();
}
async start() {
@@ -399,17 +553,36 @@ class DevService {
return;
}
await this.startFn(this);
this.updateRuntimeState({status: 'starting', exitCode: null, signal: null});
try {
await this.startFn(this);
} catch (error) {
this.updateRuntimeState({status: 'failed', pid: null});
throw error;
}
}
registerChild(child) {
this.child = child;
this.updateRuntimeState({
status: 'running',
pid: Number.isInteger(child.pid) ? child.pid : null,
exitCode: null,
signal: null,
startedAt: this.runtime.startedAt ?? new Date().toISOString(),
});
child.on('exit', (code, signal) => {
if (this.logStream && !this.logStream.destroyed) {
this.logStream.end();
}
this.child = null;
this.updateRuntimeState({
status: this.stopping ? 'stopped' : code === 0 ? 'stopped' : 'failed',
pid: null,
exitCode: code ?? null,
signal: signal ?? null,
});
if (this.stopping) {
this.stopping = false;
return;
@@ -428,6 +601,9 @@ class DevService {
const processes = [this.child, ...this.children].filter(Boolean);
this.stopping = processes.length > 0;
if (processes.length > 0) {
this.updateRuntimeState({status: 'stopping'});
}
this.child = null;
this.children = [];
@@ -440,6 +616,9 @@ class DevService {
}
this.logStream = null;
this.stopping = false;
if (processes.length > 0) {
this.updateRuntimeState({status: 'stopped', pid: null});
}
}
scheduleRestart(delayMs = 250, restartFn = null, actionLabel = '重启') {
@@ -589,6 +768,7 @@ class DevRunner {
await this.resolvePorts(command);
this.registerServices();
this.printSummary(command);
this.writeDevStackState();
}
async prepareLinuxPortRange(command) {
@@ -703,6 +883,9 @@ class DevRunner {
}
this.state.spacetimeServer = candidate;
this.state.spacetimeReused = true;
this.state.spacetimePid = Number.isInteger(pidState.pid)
? pidState.pid
: null;
const pidLabel = Number.isInteger(pidState.pid) ? ` pid=${pidState.pid}` : '';
console.log(`[dev:spacetime] 复用已启动实例${pidLabel}: ${candidate}`);
return;
@@ -807,19 +990,48 @@ class DevRunner {
}
registerServices() {
const onStateChange = () => this.writeDevStackState();
this.services.set(
'spacetime',
new DevService('spacetime', async (service) => this.startSpacetime(service)),
new DevService(
'spacetime',
async (service) => this.startSpacetime(service),
onStateChange,
),
);
this.services.set(
'api-server',
new DevService('api-server', async (service) => this.startApiServer(service)),
new DevService(
'api-server',
async (service) => this.startApiServer(service),
onStateChange,
),
);
this.services.set(
'web',
new DevService('web', async (service) => this.startWeb(service), onStateChange),
);
this.services.set('web', new DevService('web', async (service) => this.startWeb(service)));
this.services.set(
'admin-web',
new DevService('admin-web', async (service) => this.startAdminWeb(service)),
new DevService(
'admin-web',
async (service) => this.startAdminWeb(service),
onStateChange,
),
);
if (this.state.spacetimeReused) {
const spacetimeService = this.services.get('spacetime');
const endpoint = resolveDevStackServiceEndpoint(this, 'spacetime');
spacetimeService?.updateRuntimeState({
status: 'reused',
pid: this.state.spacetimePid ?? null,
host: endpoint.host,
port: endpoint.port,
url: endpoint.url,
command: resolveDevStackServiceCommand(this, 'spacetime'),
});
}
}
printSummary(command) {
@@ -843,6 +1055,16 @@ class DevRunner {
console.log(`[dev] database: ${options.database}`);
}
writeDevStackState() {
try {
ensureParentDir(devStackStatePath);
const snapshot = buildDevStackSnapshot(this);
writeFileSync(devStackStatePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
} catch (error) {
console.warn(`[dev] 写入 ${devStackStatePath} 失败: ${error.message}`);
}
}
async startCommand(command) {
if (command === 'all') {
await this.startSpacetimeForFullStack();
@@ -916,6 +1138,17 @@ class DevRunner {
);
console.log(`[dev:spacetime] log: ${logFile}`);
service.updateRuntimeState({
status: 'starting',
pid: null,
host: options.spacetimeHost,
port: options.spacetimePort,
url: this.state.spacetimeServer,
command: resolveDevStackServiceCommand(this, 'spacetime'),
startedAt: new Date().toISOString(),
exitCode: null,
signal: null,
});
const env = {
...this.baseEnv,
};
@@ -969,6 +1202,11 @@ class DevRunner {
this.options.spacetimePort = port;
this.state.spacetimeServer = `http://${this.options.spacetimeHost}:${port}`;
recordSpacetimeUrl(this.options.spacetimeDataDir, this.state.spacetimeServer);
this.services.get('spacetime')?.updateRuntimeState({
host: this.options.spacetimeHost,
port,
url: this.state.spacetimeServer,
});
console.log(`[dev:spacetime] actual: ${this.state.spacetimeServer}`);
}
@@ -1065,6 +1303,17 @@ class DevRunner {
console.log(
`[dev:api-server] SpacetimeDB ${this.options.database} @ ${this.state.spacetimeServer}`,
);
service.updateRuntimeState({
status: 'starting',
pid: null,
host: this.options.apiHost,
port: this.options.apiPort,
url: this.state.apiTarget,
command: resolveDevStackServiceCommand(this, 'api-server'),
startedAt: new Date().toISOString(),
exitCode: null,
signal: null,
});
const child = spawn(
'cargo',
@@ -1107,6 +1356,7 @@ class DevRunner {
startWeb(service) {
const apiTarget = this.resolveFrontendApiTarget();
const endpoint = resolveDevStackServiceEndpoint(this, 'web');
const env = {
...this.baseEnv,
RUST_SERVER_TARGET: apiTarget,
@@ -1117,6 +1367,17 @@ class DevRunner {
};
console.log(`[dev:web] api target: ${apiTarget}`);
service.updateRuntimeState({
status: 'starting',
pid: null,
host: endpoint.host,
port: endpoint.port,
url: endpoint.url,
command: resolveDevStackServiceCommand(this, 'web'),
startedAt: new Date().toISOString(),
exitCode: null,
signal: null,
});
const child = spawn(
'node',
[
@@ -1142,6 +1403,7 @@ class DevRunner {
startAdminWeb(service) {
const apiTarget = this.resolveFrontendApiTarget({admin: true});
const endpoint = resolveDevStackServiceEndpoint(this, 'admin-web');
const env = {
...this.baseEnv,
ADMIN_API_TARGET: apiTarget,
@@ -1150,6 +1412,17 @@ class DevRunner {
};
console.log(`[dev:admin-web] api target: ${apiTarget}`);
service.updateRuntimeState({
status: 'starting',
pid: null,
host: endpoint.host,
port: endpoint.port,
url: endpoint.url,
command: resolveDevStackServiceCommand(this, 'admin-web'),
startedAt: new Date().toISOString(),
exitCode: null,
signal: null,
});
const child = spawn(
'node',
[
@@ -1820,12 +2093,14 @@ export {
assertReusableSpacetimeProcessVersionMatchesWorkspace,
assertSpacetimeToolVersionMatchesWorkspace,
buildApiServerProcessEnv,
buildDevStackSnapshot,
buildSpacetimePublishArgs,
createDevServerSpawnOptions,
createWatchConfigs,
isSpacetimePublishPermissionError,
parseSpacetimeToolVersion,
parseArgs,
resolveDevStackStatePath,
shouldAcceptWatchEvent,
};
+75
View File
@@ -9,12 +9,14 @@ import {
assertReusableSpacetimeProcessVersionMatchesWorkspace,
assertSpacetimeToolVersionMatchesWorkspace,
buildApiServerProcessEnv,
buildDevStackSnapshot,
buildSpacetimePublishArgs,
createDevServerSpawnOptions,
createWatchConfigs,
isSpacetimePublishPermissionError,
parseSpacetimeToolVersion,
parseArgs,
resolveDevStackStatePath,
shouldAcceptWatchEvent,
} from './dev.mjs';
@@ -168,6 +170,79 @@ describe('dev scheduler api-server env', () => {
});
});
describe('dev scheduler stack state file', () => {
test('状态文件路径固定在根目录 .app/dev-stack.json', () => {
expect(resolveDevStackStatePath('C:\\repo\\Genarrative')).toBe(
join('C:\\repo\\Genarrative', '.app/dev-stack.json'),
);
});
test('状态快照记录服务 pid、端口、URL 和当前命令', () => {
const updatedAt = '2026-05-29T00:00:00.000Z';
const runner = {
command: 'web',
options: {
apiHost: '127.0.0.1',
apiPort: 8090,
webHost: '0.0.0.0',
webPort: 3010,
adminWebHost: '127.0.0.1',
adminWebPort: 3110,
spacetimeHost: '127.0.0.1',
spacetimePort: 3120,
spacetimeDataDir: 'server-rs/.spacetimedb/local/data',
database: 'genarrative-test',
watch: false,
},
state: {
apiTarget: 'http://127.0.0.1:8090',
adminWebTargetHost: '127.0.0.1',
spacetimeServer: 'http://127.0.0.1:3120',
},
services: new Map([
[
'web',
{
child: {pid: 4321},
runtime: {
status: 'running',
pid: 4321,
host: '0.0.0.0',
port: 3010,
url: 'http://127.0.0.1:3010',
command: 'node scripts/vite-cli.mjs --port=3010',
startedAt: updatedAt,
updatedAt,
exitCode: null,
signal: null,
},
},
],
]),
};
const snapshot = buildDevStackSnapshot(runner, updatedAt);
expect(snapshot.schemaVersion).toBe(1);
expect(snapshot.command).toBe('web');
expect(snapshot.database).toBe('genarrative-test');
expect(snapshot.services.web).toMatchObject({
status: 'running',
pid: 4321,
host: '0.0.0.0',
port: 3010,
url: 'http://127.0.0.1:3010',
command: 'node scripts/vite-cli.mjs --port=3010',
});
expect(snapshot.services['api-server']).toMatchObject({
status: 'idle',
pid: null,
port: 8090,
url: 'http://127.0.0.1:8090',
});
});
});
describe('dev scheduler spacetime reuse guard', () => {
test('记录 URL 可 ping 但没有 spacetime.pid 时不复用宿主', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-'));
+1
View File
@@ -3416,6 +3416,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"shared-contracts",
"shared-kernel",
"spacetimedb",
"spacetimedb-lib",
+22
View File
@@ -28,6 +28,9 @@ use shared_contracts::admin::{
AdminUpdateWorkVisibilityResponse, AdminUpsertCreationEntryTypeConfigRequest,
AdminWorkVisibilityListResponse,
};
use shared_contracts::creation_entry_config::{
encode_unified_creation_spec_response, validate_unified_creation_spec_for_play,
};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crate::{
@@ -291,6 +294,7 @@ fn map_admin_creation_entry_type_config(
category_label: entry.category_label,
category_sort_order: entry.category_sort_order,
updated_at_micros: entry.updated_at_micros,
unified_creation_spec: entry.unified_creation_spec,
}
}
@@ -305,6 +309,23 @@ fn validate_admin_creation_entry_config(
if title.is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("入口标题不能为空"));
}
let unified_creation_spec = match payload.unified_creation_spec {
Some(spec) => {
validate_unified_creation_spec_for_play(&id, &spec).map_err(|error| {
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error)
})?;
Some(spec)
}
None => None,
};
let unified_creation_spec_json = unified_creation_spec
.as_ref()
.map(|spec| {
encode_unified_creation_spec_response(spec).map_err(|error| {
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error)
})
})
.transpose()?;
Ok(module_runtime::CreationEntryTypeAdminUpsertInput {
id,
title,
@@ -317,6 +338,7 @@ fn validate_admin_creation_entry_config(
category_id: payload.category_id.trim().to_string(),
category_label: payload.category_label.trim().to_string(),
category_sort_order: payload.category_sort_order,
unified_creation_spec_json,
})
}

Some files were not shown because too many files have changed in this diff Show More