diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 7a3c320d..05e98325 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1405,9 +1405,9 @@ ## 2026-06-04 Platform Mini Game Session Mapping Model 收口 -- 背景:`PlatformEntryFlowShellImpl.tsx` 顶部仍保留拼图 runtime 恢复、方洞 session draft 转 profile、视觉小说 work detail 转 Agent session、跳一跳 pending session、敲木鱼 detail 恢复和敲木鱼 pending session 等纯 DTO 映射,壳层需要理解 sessionId 优先级、拼图稳定 ID、方洞草稿 profile 默认值、视觉小说 work/session fallback 和 pending draft 默认值。 -- 决策:新增 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,收口 `buildPuzzleRuntimeWorkFromSession`、`buildSquareHoleProfileFromSession`、`buildVisualNovelSessionFromWorkDetail`、`buildJumpHopPendingSession`、`buildWoodenFishSessionFromWorkDetail` 与 `buildWoodenFishPendingSession`。Module 复用 `normalizeCreationUrlValue` 与 `platformPuzzleIdentityModel`;壳层只保留网络读取、React state、URL 写入和 stage 切换副作用。 -- 影响范围:拼图 runtime URL 恢复、方洞挑战草稿 profile 构造、视觉小说草稿作品架恢复、跳一跳生成中作品架打开、敲木鱼生成中作品架打开和敲木鱼草稿 detail 恢复。 +- 背景:`PlatformEntryFlowShellImpl.tsx` 顶部仍保留拼图 runtime 恢复、方洞 session draft 转 profile、视觉小说 work detail 转 Agent session、跳一跳 pending session、敲木鱼 detail 恢复、敲木鱼生成中作品摘要和敲木鱼 pending session 等纯 DTO 映射,壳层需要理解 sessionId 优先级、拼图稳定 ID、方洞草稿 profile 默认值、视觉小说 work/session fallback、敲木鱼生成中摘要和 pending draft 默认值。 +- 决策:新增 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,收口 `buildPuzzleRuntimeWorkFromSession`、`buildSquareHoleProfileFromSession`、`buildVisualNovelSessionFromWorkDetail`、`buildJumpHopPendingSession`、`buildWoodenFishSessionFromWorkDetail`、`buildWoodenFishGeneratingWorkSummary` 与 `buildWoodenFishPendingSession`。Module 复用 `normalizeCreationUrlValue` 与 `platformPuzzleIdentityModel`;壳层只保留网络读取、React state、URL 写入和 stage 切换副作用。 +- 影响范围:拼图 runtime URL 恢复、方洞挑战草稿 profile 构造、视觉小说草稿作品架恢复、跳一跳生成中作品架打开、敲木鱼生成中作品架摘要 / 作品架打开和敲木鱼草稿 detail 恢复。 - 验证方式:`npm run test -- src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 @@ -1430,9 +1430,9 @@ ## 2026-06-04 Platform Mini Game Draft Payload Model 收口 -- 背景:`PlatformEntryFlowShellImpl.tsx` 内联维护拼图 / 抓大鹅表单 payload、拼图作品更新 payload、拼图编译 action、作品摘要回填 payload 和 pending 草稿 metadata,壳层需要理解描述字段优先级、formDraft 回退、结果页 draft 到作品更新字段的映射、Match3D config / draft / anchorPack 优先级和数字解析。 -- 决策:新增 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,收口 `buildPuzzleFormPayloadFromWork`、`buildPuzzleFormPayloadFromSession`、`buildPuzzleFormPayloadFromAction`、`buildPuzzleCompileActionFromFormPayload`、`buildPuzzleWorkUpdatePayloadFromDraft`、`buildPendingPuzzleDraftMetadata`、`isPuzzleFormOnlyDraft`、`isEmptyPuzzleFormOnlyDraft`、`buildMatch3DFormPayloadFromSession`、`buildMatch3DFormPayloadFromWork` 与 `buildPendingMatch3DDraftMetadata`;`parseOptionalFiniteNumber` 留在 Module 内部。 -- 影响范围:拼图 action 完成 / 执行前 / 失败恢复、拼图结果页试玩前作品更新、拼图表单直生草稿、拼图 form-only 草稿恢复 / 分流 / 结果页渲染、拼图草稿架恢复、抓大鹅表单直生草稿与失败恢复。 +- 背景:`PlatformEntryFlowShellImpl.tsx` 内联维护拼图 / 抓大鹅表单 payload、拼图作品更新 payload、拼图编译 action、跳一跳 / 敲木鱼生成 action、作品摘要回填 payload 和 pending 草稿 metadata,壳层需要理解描述字段优先级、formDraft 回退、结果页 draft 到作品更新字段的映射、跳一跳 / 敲木鱼 payload 与 draft 优先级、Match3D config / draft / anchorPack 优先级和数字解析。 +- 决策:新增 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,收口 `buildPuzzleFormPayloadFromWork`、`buildPuzzleFormPayloadFromSession`、`buildPuzzleFormPayloadFromAction`、`buildPuzzleCompileActionFromFormPayload`、`buildPuzzleWorkUpdatePayloadFromDraft`、`buildJumpHopDraftActionPayload`、`buildWoodenFishDraftActionPayload`、`buildPendingPuzzleDraftMetadata`、`isPuzzleFormOnlyDraft`、`isEmptyPuzzleFormOnlyDraft`、`buildMatch3DFormPayloadFromSession`、`buildMatch3DFormPayloadFromWork` 与 `buildPendingMatch3DDraftMetadata`;`parseOptionalFiniteNumber` 留在 Module 内部。 +- 影响范围:拼图 action 完成 / 执行前 / 失败恢复、拼图结果页试玩前作品更新、跳一跳 / 敲木鱼生成与重生成 action、拼图表单直生草稿、拼图 form-only 草稿恢复 / 分流 / 结果页渲染、拼图草稿架恢复、抓大鹅表单直生草稿与失败恢复。 - 验证方式:`npm run test -- src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 diff --git a/docs/README.md b/docs/README.md index afb54240..dcbfb1f5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,11 +55,11 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台入口生成页进度 tick 的 stage 到生成状态映射、终态判定和视觉小说轻量生成特例收口到 `src/components/platform-entry/platformGenerationProgressTickModel.ts`,壳层只保留 `Date.now()`、`setInterval` 和 cleanup 副作用,规则见 [【前端架构】PlatformGenerationProgressTickModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformGenerationProgressTickModel收口计划-2026-06-04.md)。 -平台壳的拼图 runtime 恢复 work、方洞 session draft 转 profile、视觉小说 work detail 转 Agent session、跳一跳 pending session、敲木鱼 detail 恢复 session 和敲木鱼 pending session DTO 映射收口到 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,壳层只保留网络、状态、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md)。 +平台壳的拼图 runtime 恢复 work、方洞 session draft 转 profile、视觉小说 work detail 转 Agent session、跳一跳 pending session、敲木鱼 detail 恢复 session、敲木鱼生成中作品摘要和敲木鱼 pending session DTO 映射收口到 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,壳层只保留网络、状态、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md)。 平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并、抓大鹅生成资产旁路进度合并和 ready / generating 判定收口到 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,壳层只保留 API、后台任务、React state、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md)。 -平台小游戏草稿恢复和提交所需的拼图 / 抓大鹅表单 payload、拼图作品更新 payload、拼图编译 action、pending metadata 与拼图 form-only 草稿判定收口到 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,壳层只保留 API、Action 执行、background task 与状态副作用,规则见 [【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md)。 +平台小游戏草稿恢复和提交所需的拼图 / 抓大鹅表单 payload、拼图作品更新 payload、拼图编译 action、跳一跳 / 敲木鱼生成 action、pending metadata 与拼图 form-only 草稿判定收口到 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,壳层只保留 API、Action 执行、background task 与状态副作用,规则见 [【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md)。 平台拼图生成完成后刷新恢复的草稿归一化与可恢复完成态判定收口到 `src/components/platform-entry/platformPuzzleDraftRecoveryModel.ts`,恢复链路只有在首图、关卡画面、UI spritesheet 与关卡背景资产包完整时才抬为 ready,规则见 [【前端架构】PlatformPuzzleDraftRecoveryModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPuzzleDraftRecoveryModel收口计划-2026-06-04.md)。 diff --git a/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md index 34a7d312..cc6572cd 100644 --- a/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md +++ b/docs/technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md @@ -2,7 +2,7 @@ ## 背景 -`PlatformEntryFlowShellImpl.tsx` 曾内联维护拼图和抓大鹅草稿恢复所需的表单 payload、拼图编译 action payload、拼图作品更新 payload、作品摘要回填 payload 和 pending 草稿 metadata。壳层因此需要理解拼图描述字段优先级、formDraft 回退、结果页 draft 到作品更新字段的映射、Match3D config / draft / anchorPack 优先级,以及 pending 作品架标题摘要如何从 payload 派生。后续还残留拼图 form-only 草稿判定,影响 action 分流、草稿恢复阶段和结果页渲染。 +`PlatformEntryFlowShellImpl.tsx` 曾内联维护拼图和抓大鹅草稿恢复所需的表单 payload、拼图编译 action payload、拼图作品更新 payload、跳一跳 / 敲木鱼生成 action payload、作品摘要回填 payload 和 pending 草稿 metadata。壳层因此需要理解拼图描述字段优先级、formDraft 回退、结果页 draft 到作品更新字段的映射、Match3D config / draft / anchorPack 优先级、跳一跳 / 敲木鱼 payload 与 session draft 优先级,以及 pending 作品架标题摘要如何从 payload 派生。后续还残留拼图 form-only 草稿判定,影响 action 分流、草稿恢复阶段和结果页渲染。 这些逻辑都是 DTO 变换;不读取 React state,不请求网络,也不写 URL。壳层只应决定何时恢复、何时提交 action、何时写入生成状态。 @@ -15,6 +15,8 @@ - `buildPuzzleFormPayloadFromAction(payload)`:从拼图 action 还原表单 payload,仅接受 `compile_puzzle_draft` 与 `save_puzzle_form_draft`。 - `buildPuzzleCompileActionFromFormPayload(payload)`:从表单 payload 构造拼图编译 action。 - `buildPuzzleWorkUpdatePayloadFromDraft(draft)`:从拼图结果 draft 构造 `updatePuzzleWork(...)` 所需 payload。 +- `buildJumpHopDraftActionPayload(actionType, { payload, draft })`:从跳一跳表单 payload / session draft 构造生成或重生成 action。 +- `buildWoodenFishDraftActionPayload(actionType, { payload, draft })`:从敲木鱼表单 payload / session draft 构造生成或重生成 action。 - `buildPendingPuzzleDraftMetadata(payload)`:从拼图 payload 派生 pending 作品架 metadata。 - `isPuzzleFormOnlyDraft(session)` 与 `isEmptyPuzzleFormOnlyDraft(session)`:判断拼图 session 是否仍只是表单草稿,以及表单草稿是否没有任何可提交内容。 - `buildMatch3DFormPayloadFromSession(session)` 与 `buildMatch3DFormPayloadFromWork(item)`:从抓大鹅 session / work 恢复表单 payload。 @@ -29,6 +31,7 @@ - 拼图编译 action 的 `promptText` 来自 `pictureDescription || seedText`;`workDescription` 缺省回退到图片描述;`candidateCount` 固定为 `1`。 - 拼图 action 还原只接受 `compile_puzzle_draft` 与 `save_puzzle_form_draft`;其它 action 返回 `null`。 - 拼图作品更新 payload 必须直接映射 `workTitle`、`workDescription`、`levelName`、`summary`、`themeTags`、`coverImageSrc`、`coverAssetId`,`levels` 缺失时回退空数组。 +- 跳一跳和敲木鱼生成 action payload 的字段优先级固定为表单 payload 优先,其次 session draft;重生成 action 只传 session draft 字段。 - 拼图 form-only 草稿只在 `session.stage === 'collecting_anchors'` 且存在 `draft.formDraft` 时成立。 - 空 form-only 草稿必须同时缺少 `seedText`、`formDraft.workTitle`、`formDraft.workDescription` 与 `formDraft.pictureDescription`。 - 抓大鹅 session payload 优先读取 `config`,其次 `draft`,最后 `anchorPack`;`anchorPack.clearCount` 与 `anchorPack.difficulty` 只接受有限数字字符串或数字。 @@ -37,8 +40,8 @@ ## Depth / Leverage / Locality -- **Depth**:壳层以一组表意函数取得 payload / metadata;字段优先级、结果页 draft 更新字段、默认空资产和数字解析藏入 Module Implementation。 -- **Leverage**:后续调整拼图或抓大鹅草稿恢复表单、拼图作品更新字段时,先改 Module 与单测,再保持壳层 API / state 副作用不变。 +- **Depth**:壳层以一组表意函数取得 payload / metadata;字段优先级、结果页 draft 更新字段、跳一跳 / 敲木鱼 action 字段、默认空资产和数字解析藏入 Module Implementation。 +- **Leverage**:后续调整拼图或抓大鹅草稿恢复表单、拼图作品更新字段、跳一跳 / 敲木鱼生成 action 字段时,先改 Module 与单测,再保持壳层 API / state 副作用不变。 - **Locality**:表单恢复、作品更新与 action payload 规则集中到一个纯测试面,避免在大型平台壳的生成、重试和恢复流程里重复散落 DTO 拼装。 ## 验收 diff --git a/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md index 77b680c1..1578d439 100644 --- a/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md +++ b/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md @@ -2,7 +2,7 @@ ## 背景 -`PlatformEntryFlowShellImpl.tsx` 顶部曾保留拼图 runtime 恢复、方洞挑战 session draft 转 profile、视觉小说 work detail 转 Agent session、跳一跳 pending session、敲木鱼 work detail 恢复和敲木鱼 pending session 多段纯 DTO 映射。它们没有 React state、网络请求、路由、弹窗或计时副作用,却住在大型平台壳内;新增或修正生成中草稿恢复时,需要在壳层里理解 sessionId 优先级、拼图稳定 ID、方洞 profile 默认值、视觉小说 work/session fallback、pending draft 默认值和木鱼 fallback 规则。 +`PlatformEntryFlowShellImpl.tsx` 顶部曾保留拼图 runtime 恢复、方洞挑战 session draft 转 profile、视觉小说 work detail 转 Agent session、跳一跳 pending session、敲木鱼 work detail 恢复、敲木鱼生成中作品摘要和敲木鱼 pending session 多段纯 DTO 映射。它们没有 React state、网络请求、路由、弹窗或计时副作用,却住在大型平台壳内;新增或修正生成中草稿恢复时,需要在壳层里理解 sessionId 优先级、拼图稳定 ID、方洞 profile 默认值、视觉小说 work/session fallback、pending draft 默认值和木鱼 fallback 规则。 这些规则属于平台壳 session / work 恢复映射,应成为可测试的 **Module**。壳层只负责调用网络、写 React state、写 URL 和切换 stage。 @@ -15,6 +15,7 @@ - `buildVisualNovelSessionFromWorkDetail(work)`:从视觉小说 work detail 恢复 `VisualNovelAgentSessionSnapshot`,供草稿作品架回到结果页继续编辑。 - `buildJumpHopPendingSession(item)`:从跳一跳作品架 summary 构造生成中 pending session。 - `buildWoodenFishSessionFromWorkDetail(work, fallbackItem?)`:从敲木鱼 work detail 恢复 session,并按 summary / fallback / profileId 决定 sessionId。 +- `buildWoodenFishGeneratingWorkSummary(session, payload?)`:从敲木鱼生成 session 和可选表单 payload 构造作品架生成中摘要。 - `buildWoodenFishPendingSession(item)`:从敲木鱼作品架 summary 构造生成中 pending session。 `PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:调用这些映射后继续负责 `set*Session`、`set*Work`、`set*Run`、`createMiniGameDraftGenerationState(...)`、`writeCreationUrlState(...)`、`enterCreateTab()` 和 `setSelectionStage(...)`。 @@ -29,13 +30,14 @@ - 视觉小说恢复 session 的 `sessionId` 优先归一化后的 `sourceSessionId`,为空时回退 `workId`;`status='ready'`,`messages=[]`,`pendingAction=null`,`sourceMode` 来自 draft,`updatedAt` 来自 summary。 - 跳一跳 pending sessionId 优先 `sourceSessionId`,缺失时用 `profileId`;素材、路径和 prompt 维持空值兜底。 - 敲木鱼 detail sessionId 优先级固定为 `work.summary.sourceSessionId > fallbackItem.sourceSessionId > profileId`。 +- 敲木鱼生成中摘要的 `workId/profileId/sourceSessionId` 都来自 sessionId;标题、描述和标签优先表单 payload,其次 session draft,最后回退 `敲木鱼` / 空描述 / `['敲木鱼']`。 - 敲木鱼 pending session 保持 `floatingWords=['功德 +1']`、素材 / 音效 / 背景为空的旧默认。 ## Depth / Leverage / Locality -- **Depth**:壳层以少量函数取得恢复用 DTO;ID 优先级、方洞 profile 默认值、视觉小说 session fallback 和 pending draft 字段藏入 Module Implementation。 +- **Depth**:壳层以少量函数取得恢复用 DTO;ID 优先级、方洞 profile 默认值、视觉小说 session fallback、敲木鱼生成中摘要和 pending draft 字段藏入 Module Implementation。 - **Leverage**:后续新增生成中作品恢复或修改 sessionId 规则时,先改 Module 与单测,再保持壳层 Adapter 副作用不变。 -- **Locality**:拼图、方洞、视觉小说、跳一跳和敲木鱼的恢复映射集中在一个纯测试面,避免在大型壳层顶部继续堆积 DTO 构造。 +- **Locality**:拼图、方洞、视觉小说、跳一跳和敲木鱼的恢复 / 生成中映射集中在一个纯测试面,避免在大型壳层顶部继续堆积 DTO 构造。 ## 验收 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index a793c15e..4a3cf00c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -525,6 +525,7 @@ import { resolveFinishedMiniGameDraftGenerationState, } from './platformMiniGameDraftGenerationStateModel'; import { + buildJumpHopDraftActionPayload, buildMatch3DFormPayloadFromSession, buildMatch3DFormPayloadFromWork, buildPendingMatch3DDraftMetadata, @@ -534,6 +535,7 @@ import { buildPuzzleFormPayloadFromSession, buildPuzzleFormPayloadFromWork, buildPuzzleWorkUpdatePayloadFromDraft, + buildWoodenFishDraftActionPayload, isEmptyPuzzleFormOnlyDraft, isPuzzleFormOnlyDraft, } from './platformMiniGameDraftPayloadModel'; @@ -542,6 +544,7 @@ import { buildPuzzleRuntimeWorkFromSession, buildSquareHoleProfileFromSession, buildVisualNovelSessionFromWorkDetail, + buildWoodenFishGeneratingWorkSummary, buildWoodenFishPendingSession, buildWoodenFishSessionFromWorkDetail, } from './platformMiniGameSessionMappingModel'; @@ -6828,25 +6831,10 @@ export function PlatformEntryFlowShellImpl({ try { const response = await jumpHopClient.executeAction( created.session.sessionId, - { - actionType: 'compile-draft', - workTitle: payload?.workTitle ?? created.session.draft?.workTitle, - workDescription: - payload?.workDescription ?? - created.session.draft?.workDescription, - themeTags: payload?.themeTags ?? created.session.draft?.themeTags, - difficulty: - payload?.difficulty ?? created.session.draft?.difficulty, - stylePreset: - payload?.stylePreset ?? created.session.draft?.stylePreset, - characterPrompt: - payload?.characterPrompt ?? - created.session.draft?.characterPrompt, - tilePrompt: - payload?.tilePrompt ?? created.session.draft?.tilePrompt, - endMoodPrompt: - payload?.endMoodPrompt ?? created.session.draft?.endMoodPrompt, - }, + buildJumpHopDraftActionPayload('compile-draft', { + payload, + draft: created.session.draft, + }), ); const readyState = createReadyJumpHopGenerationState(generationState); setJumpHopSession(response.session); @@ -6958,17 +6946,9 @@ export function PlatformEntryFlowShellImpl({ try { const response = await jumpHopClient.executeAction( jumpHopSession.sessionId, - { - actionType, - workTitle: jumpHopSession.draft?.workTitle, - workDescription: jumpHopSession.draft?.workDescription, - themeTags: jumpHopSession.draft?.themeTags, - difficulty: jumpHopSession.draft?.difficulty, - stylePreset: jumpHopSession.draft?.stylePreset, - characterPrompt: jumpHopSession.draft?.characterPrompt, - tilePrompt: jumpHopSession.draft?.tilePrompt, - endMoodPrompt: jumpHopSession.draft?.endMoodPrompt, - }, + buildJumpHopDraftActionPayload(actionType, { + draft: jumpHopSession.draft, + }), ); setJumpHopSession(response.session); setJumpHopWork(response.work ?? jumpHopWork); @@ -7190,30 +7170,8 @@ export function PlatformEntryFlowShellImpl({ setSelectionStage('wooden-fish-generating'); markDraftGenerating('wooden-fish', [created.session.sessionId]); markPendingDraftGenerating('wooden-fish', created.session.sessionId); - const createdAt = created.session.updatedAt ?? created.session.createdAt; setWoodenFishWorks((current) => [ - { - runtimeKind: 'wooden-fish', - workId: created.session.sessionId, - profileId: created.session.sessionId, - ownerUserId: created.session.ownerUserId, - sourceSessionId: created.session.sessionId, - workTitle: - payload?.workTitle ?? created.session.draft?.workTitle ?? '敲木鱼', - workDescription: - payload?.workDescription ?? - created.session.draft?.workDescription ?? - '', - themeTags: payload?.themeTags ?? - created.session.draft?.themeTags ?? ['敲木鱼'], - coverImageSrc: created.session.draft?.coverImageSrc ?? null, - publicationStatus: 'draft', - playCount: 0, - updatedAt: createdAt, - publishedAt: null, - publishReady: false, - generationStatus: 'generating', - }, + buildWoodenFishGeneratingWorkSummary(created.session, payload), ...current.filter( (item) => item.sourceSessionId !== created.session.sessionId, ), @@ -7222,24 +7180,10 @@ export function PlatformEntryFlowShellImpl({ try { const response = await woodenFishClient.executeAction( created.session.sessionId, - { - actionType: 'compile-draft', - workTitle: payload?.workTitle ?? created.session.draft?.workTitle, - workDescription: - payload?.workDescription ?? - created.session.draft?.workDescription, - themeTags: payload?.themeTags ?? created.session.draft?.themeTags, - hitObjectPrompt: - payload?.hitObjectPrompt ?? - created.session.draft?.hitObjectPrompt, - hitObjectReferenceImageSrc: - payload?.hitObjectReferenceImageSrc ?? - created.session.draft?.hitObjectReferenceImageSrc, - hitSoundAsset: - payload?.hitSoundAsset ?? created.session.draft?.hitSoundAsset, - floatingWords: - payload?.floatingWords ?? created.session.draft?.floatingWords, - }, + buildWoodenFishDraftActionPayload('compile-draft', { + payload, + draft: created.session.draft, + }), ); setWoodenFishSession(response.session); setWoodenFishWork(response.work ?? null); @@ -7375,17 +7319,9 @@ export function PlatformEntryFlowShellImpl({ try { const response = await woodenFishClient.executeAction( woodenFishSession.sessionId, - { - actionType, - workTitle: woodenFishSession.draft?.workTitle, - workDescription: woodenFishSession.draft?.workDescription, - themeTags: woodenFishSession.draft?.themeTags, - hitObjectPrompt: woodenFishSession.draft?.hitObjectPrompt, - hitObjectReferenceImageSrc: - woodenFishSession.draft?.hitObjectReferenceImageSrc, - hitSoundAsset: woodenFishSession.draft?.hitSoundAsset, - floatingWords: woodenFishSession.draft?.floatingWords, - }, + buildWoodenFishDraftActionPayload(actionType, { + draft: woodenFishSession.draft, + }), ); setWoodenFishSession(response.session); setWoodenFishWork(response.work ?? woodenFishWork); diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts index 084fb40f..3573db00 100644 --- a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from 'vitest'; +import type { + JumpHopSessionSnapshotResponse, + JumpHopWorkspaceCreateRequest, +} from '../../../packages/shared/src/contracts/jumpHop'; import type { Match3DAgentSessionSnapshot, Match3DAnchorPackResponse, @@ -15,7 +19,12 @@ import type { PuzzleAgentSessionSnapshot, } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { + WoodenFishSessionSnapshotResponse, + WoodenFishWorkspaceCreateRequest, +} from '../../../packages/shared/src/contracts/woodenFish'; import { + buildJumpHopDraftActionPayload, buildMatch3DFormPayloadFromSession, buildMatch3DFormPayloadFromWork, buildPendingMatch3DDraftMetadata, @@ -25,6 +34,7 @@ import { buildPuzzleFormPayloadFromSession, buildPuzzleFormPayloadFromWork, buildPuzzleWorkUpdatePayloadFromDraft, + buildWoodenFishDraftActionPayload, isEmptyPuzzleFormOnlyDraft, isPuzzleFormOnlyDraft, } from './platformMiniGameDraftPayloadModel'; @@ -196,6 +206,91 @@ function buildMatch3DWork( }; } +function buildJumpHopDraft( + overrides: Partial> = {}, +): NonNullable { + return { + templateId: 'jump-hop', + templateName: '跳一跳', + profileId: 'jump-hop-profile-1', + workTitle: '草稿跳一跳', + workDescription: '从草稿恢复。', + themeTags: ['草稿'], + difficulty: 'standard', + stylePreset: 'paper-toy', + characterPrompt: '草稿角色', + tilePrompt: '草稿平台', + endMoodPrompt: '草稿终点', + characterAsset: null, + tileAtlasAsset: null, + tileAssets: [], + path: null, + coverComposite: null, + generationStatus: 'draft', + ...overrides, + }; +} + +function buildJumpHopPayload( + overrides: Partial = {}, +): JumpHopWorkspaceCreateRequest { + return { + templateId: 'jump-hop', + workTitle: '表单跳一跳', + workDescription: '从表单提交。', + themeTags: ['表单'], + difficulty: 'advanced', + stylePreset: 'neon-glass', + characterPrompt: '表单角色', + tilePrompt: '表单平台', + endMoodPrompt: '表单终点', + ...overrides, + }; +} + +function buildWoodenFishDraft( + overrides: Partial< + NonNullable + > = {}, +): NonNullable { + return { + templateId: 'wooden-fish', + templateName: '敲木鱼', + profileId: 'wooden-fish-profile-1', + workTitle: '草稿木鱼', + workDescription: '从草稿恢复。', + themeTags: ['草稿'], + hitObjectPrompt: '草稿敲击物', + hitObjectReferenceImageSrc: '/draft-hit-ref.png', + hitSoundPrompt: null, + floatingWords: ['草稿 +1'], + hitObjectAsset: null, + backgroundAsset: null, + backButtonAsset: null, + hitSoundAsset: null, + coverImageSrc: null, + generationStatus: 'draft', + ...overrides, + }; +} + +function buildWoodenFishPayload( + overrides: Partial = {}, +): WoodenFishWorkspaceCreateRequest { + return { + templateId: 'wooden-fish', + workTitle: '表单木鱼', + workDescription: '从表单提交。', + themeTags: ['表单'], + hitObjectPrompt: '表单敲击物', + hitObjectReferenceImageSrc: '/form-hit-ref.png', + hitSoundPrompt: null, + hitSoundAsset: null, + floatingWords: ['表单 +1'], + ...overrides, + }; +} + describe('platformMiniGameDraftPayloadModel', () => { test('builds puzzle form payload from work with fallback description priority', () => { expect( @@ -242,6 +337,64 @@ describe('platformMiniGameDraftPayloadModel', () => { ).toEqual([]); }); + test('builds jump hop draft action payload from payload or draft', () => { + expect( + buildJumpHopDraftActionPayload('compile-draft', { + payload: buildJumpHopPayload(), + draft: buildJumpHopDraft(), + }), + ).toEqual({ + actionType: 'compile-draft', + workTitle: '表单跳一跳', + workDescription: '从表单提交。', + themeTags: ['表单'], + difficulty: 'advanced', + stylePreset: 'neon-glass', + characterPrompt: '表单角色', + tilePrompt: '表单平台', + endMoodPrompt: '表单终点', + }); + + expect( + buildJumpHopDraftActionPayload('regenerate-tiles', { + draft: buildJumpHopDraft(), + }), + ).toMatchObject({ + actionType: 'regenerate-tiles', + workTitle: '草稿跳一跳', + tilePrompt: '草稿平台', + }); + }); + + test('builds wooden fish draft action payload from payload or draft', () => { + expect( + buildWoodenFishDraftActionPayload('compile-draft', { + payload: buildWoodenFishPayload(), + draft: buildWoodenFishDraft(), + }), + ).toEqual({ + actionType: 'compile-draft', + workTitle: '表单木鱼', + workDescription: '从表单提交。', + themeTags: ['表单'], + hitObjectPrompt: '表单敲击物', + hitObjectReferenceImageSrc: '/form-hit-ref.png', + hitSoundAsset: null, + floatingWords: ['表单 +1'], + }); + + expect( + buildWoodenFishDraftActionPayload('regenerate-hit-object', { + draft: buildWoodenFishDraft(), + }), + ).toMatchObject({ + actionType: 'regenerate-hit-object', + workTitle: '草稿木鱼', + hitObjectPrompt: '草稿敲击物', + floatingWords: ['草稿 +1'], + }); + }); + test('builds puzzle form payload from session form draft and fallbacks', () => { expect(buildPuzzleFormPayloadFromSession(buildPuzzleSession())).toEqual({ seedText: '表单画面', diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts index bc9f3f60..19c9c413 100644 --- a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts @@ -1,3 +1,8 @@ +import type { + JumpHopActionRequest, + JumpHopSessionSnapshotResponse, + JumpHopWorkspaceCreateRequest, +} from '../../../packages/shared/src/contracts/jumpHop'; import type { CreateMatch3DSessionRequest, Match3DAgentSessionSnapshot, @@ -13,6 +18,11 @@ import type { PuzzleAgentSessionSnapshot, } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { + WoodenFishActionRequest, + WoodenFishSessionSnapshotResponse, + WoodenFishWorkspaceCreateRequest, +} from '../../../packages/shared/src/contracts/woodenFish'; export type PuzzleWorkUpdatePayload = { workTitle?: string; @@ -65,6 +75,49 @@ export function buildPuzzleWorkUpdatePayloadFromDraft( }; } +export function buildJumpHopDraftActionPayload( + actionType: 'compile-draft' | 'regenerate-character' | 'regenerate-tiles', + input: { + payload?: JumpHopWorkspaceCreateRequest | null; + draft?: JumpHopSessionSnapshotResponse['draft'] | null; + }, +): JumpHopActionRequest { + const { payload, draft } = input; + return { + actionType, + workTitle: payload?.workTitle ?? draft?.workTitle, + workDescription: payload?.workDescription ?? draft?.workDescription, + themeTags: payload?.themeTags ?? draft?.themeTags, + difficulty: payload?.difficulty ?? draft?.difficulty, + stylePreset: payload?.stylePreset ?? draft?.stylePreset, + characterPrompt: payload?.characterPrompt ?? draft?.characterPrompt, + tilePrompt: payload?.tilePrompt ?? draft?.tilePrompt, + endMoodPrompt: payload?.endMoodPrompt ?? draft?.endMoodPrompt, + }; +} + +export function buildWoodenFishDraftActionPayload( + actionType: 'compile-draft' | 'regenerate-hit-object', + input: { + payload?: WoodenFishWorkspaceCreateRequest | null; + draft?: WoodenFishSessionSnapshotResponse['draft'] | null; + }, +): WoodenFishActionRequest { + const { payload, draft } = input; + return { + actionType, + workTitle: payload?.workTitle ?? draft?.workTitle, + workDescription: payload?.workDescription ?? draft?.workDescription, + themeTags: payload?.themeTags ?? draft?.themeTags, + hitObjectPrompt: payload?.hitObjectPrompt ?? draft?.hitObjectPrompt, + hitObjectReferenceImageSrc: + payload?.hitObjectReferenceImageSrc ?? + draft?.hitObjectReferenceImageSrc, + hitSoundAsset: payload?.hitSoundAsset ?? draft?.hitSoundAsset, + floatingWords: payload?.floatingWords ?? draft?.floatingWords, + }; +} + function parseOptionalFiniteNumber(value: string | number | null | undefined) { if (typeof value === 'number') { return Number.isFinite(value) ? value : undefined; diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts index 6e557059..7c1674d2 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts @@ -19,7 +19,9 @@ import type { import type { WoodenFishAudioAsset, WoodenFishImageAsset, + WoodenFishSessionSnapshotResponse, WoodenFishWorkProfileResponse, + WoodenFishWorkspaceCreateRequest, WoodenFishWorkSummaryResponse, } from '../../../packages/shared/src/contracts/woodenFish'; import { @@ -27,6 +29,7 @@ import { buildPuzzleRuntimeWorkFromSession, buildSquareHoleProfileFromSession, buildVisualNovelSessionFromWorkDetail, + buildWoodenFishGeneratingWorkSummary, buildWoodenFishPendingSession, buildWoodenFishSessionFromWorkDetail, } from './platformMiniGameSessionMappingModel'; @@ -423,6 +426,37 @@ function buildWoodenFishWorkProfile( }; } +function buildWoodenFishSession( + overrides: Partial = {}, +): WoodenFishSessionSnapshotResponse { + const summary = buildWoodenFishSummary(); + return { + sessionId: 'wooden-fish-session-1', + ownerUserId: 'user-1', + status: 'generating', + draft: buildWoodenFishWorkProfile({ summary }).draft, + createdAt: '2026-06-01T11:59:00.000Z', + updatedAt: '2026-06-01T12:00:00.000Z', + ...overrides, + }; +} + +function buildWoodenFishCreatePayload( + overrides: Partial = {}, +): WoodenFishWorkspaceCreateRequest { + return { + templateId: 'wooden-fish', + workTitle: '表单星灯木鱼', + workDescription: '表单里敲亮星灯。', + themeTags: ['表单星灯'], + hitObjectPrompt: '星灯', + hitObjectReferenceImageSrc: null, + hitSoundPrompt: null, + floatingWords: ['功德 +1'], + ...overrides, + }; +} + describe('platformMiniGameSessionMappingModel', () => { test('builds a draft puzzle runtime work from a session', () => { expect( @@ -607,6 +641,47 @@ describe('platformMiniGameSessionMappingModel', () => { }); }); + test('builds wooden fish generating work summary from session and payload', () => { + expect( + buildWoodenFishGeneratingWorkSummary( + buildWoodenFishSession(), + buildWoodenFishCreatePayload(), + ), + ).toEqual({ + runtimeKind: 'wooden-fish', + workId: 'wooden-fish-session-1', + profileId: 'wooden-fish-session-1', + ownerUserId: 'user-1', + sourceSessionId: 'wooden-fish-session-1', + workTitle: '表单星灯木鱼', + workDescription: '表单里敲亮星灯。', + themeTags: ['表单星灯'], + coverImageSrc: '/wooden-fish-cover.png', + publicationStatus: 'draft', + playCount: 0, + updatedAt: '2026-06-01T12:00:00.000Z', + publishedAt: null, + publishReady: false, + generationStatus: 'generating', + }); + + expect( + buildWoodenFishGeneratingWorkSummary( + buildWoodenFishSession({ + draft: null, + createdAt: '2026-06-01T11:59:00.000Z', + }), + null, + ), + ).toMatchObject({ + workTitle: '敲木鱼', + workDescription: '', + themeTags: ['敲木鱼'], + coverImageSrc: null, + updatedAt: '2026-06-01T12:00:00.000Z', + }); + }); + test('builds wooden fish recovered session with summary, fallback and profile id priority', () => { expect( buildWoodenFishSessionFromWorkDetail( diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts index a59e9a6a..f240bf09 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts @@ -10,6 +10,7 @@ import type { import type { WoodenFishSessionSnapshotResponse, WoodenFishWorkProfileResponse, + WoodenFishWorkspaceCreateRequest, WoodenFishWorkSummaryResponse, } from '../../../packages/shared/src/contracts/woodenFish'; import { normalizeCreationUrlValue } from './platformCreationUrlStateModel'; @@ -159,6 +160,31 @@ export function buildWoodenFishSessionFromWorkDetail( }; } +export function buildWoodenFishGeneratingWorkSummary( + session: WoodenFishSessionSnapshotResponse, + payload?: WoodenFishWorkspaceCreateRequest | null, +): WoodenFishWorkSummaryResponse { + const updatedAt = session.updatedAt ?? session.createdAt; + return { + runtimeKind: 'wooden-fish', + workId: session.sessionId, + profileId: session.sessionId, + ownerUserId: session.ownerUserId, + sourceSessionId: session.sessionId, + workTitle: payload?.workTitle ?? session.draft?.workTitle ?? '敲木鱼', + workDescription: + payload?.workDescription ?? session.draft?.workDescription ?? '', + themeTags: payload?.themeTags ?? session.draft?.themeTags ?? ['敲木鱼'], + coverImageSrc: session.draft?.coverImageSrc ?? null, + publicationStatus: 'draft', + playCount: 0, + updatedAt, + publishedAt: null, + publishReady: false, + generationStatus: 'generating', + }; +} + export function buildWoodenFishPendingSession( item: WoodenFishWorkSummaryResponse, ): WoodenFishSessionSnapshotResponse {