diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index c4e689ac..80ee8ae5 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 恢复、跳一跳 pending session、敲木鱼 detail 恢复和敲木鱼 pending session 四段纯 DTO 映射,壳层需要理解 sessionId 优先级、拼图稳定 ID 和 pending draft 默认值。 -- 决策:新增 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,收口 `buildPuzzleRuntimeWorkFromSession`、`buildJumpHopPendingSession`、`buildWoodenFishSessionFromWorkDetail` 与 `buildWoodenFishPendingSession`。Module 复用 `normalizeCreationUrlValue` 与 `platformPuzzleIdentityModel`;壳层只保留网络读取、React state、URL 写入和 stage 切换副作用。 -- 影响范围:拼图 runtime URL 恢复、跳一跳生成中作品架打开、敲木鱼生成中作品架打开和敲木鱼草稿 detail 恢复。 +- 背景:`PlatformEntryFlowShellImpl.tsx` 顶部仍保留拼图 runtime 恢复、方洞 session draft 转 profile、跳一跳 pending session、敲木鱼 detail 恢复和敲木鱼 pending session 等纯 DTO 映射,壳层需要理解 sessionId 优先级、拼图稳定 ID、方洞草稿 profile 默认值和 pending draft 默认值。 +- 决策:新增 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,收口 `buildPuzzleRuntimeWorkFromSession`、`buildSquareHoleProfileFromSession`、`buildJumpHopPendingSession`、`buildWoodenFishSessionFromWorkDetail` 与 `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`。 diff --git a/docs/README.md b/docs/README.md index 57dac10e..b4bfa104 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,7 +55,7 @@ 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、跳一跳 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、跳一跳 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)。 diff --git a/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md index c6a88d34..38ed5064 100644 --- a/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md +++ b/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md @@ -2,7 +2,7 @@ ## 背景 -`PlatformEntryFlowShellImpl.tsx` 顶部曾保留拼图 runtime 恢复、跳一跳 pending session、敲木鱼 work detail 恢复和敲木鱼 pending session 四段纯 DTO 映射。它们没有 React state、网络请求、路由、弹窗或计时副作用,却住在大型平台壳内;新增或修正生成中草稿恢复时,需要在壳层里理解 sessionId 优先级、拼图稳定 ID、pending draft 默认值和木鱼 fallback 规则。 +`PlatformEntryFlowShellImpl.tsx` 顶部曾保留拼图 runtime 恢复、方洞挑战 session draft 转 profile、跳一跳 pending session、敲木鱼 work detail 恢复和敲木鱼 pending session 多段纯 DTO 映射。它们没有 React state、网络请求、路由、弹窗或计时副作用,却住在大型平台壳内;新增或修正生成中草稿恢复时,需要在壳层里理解 sessionId 优先级、拼图稳定 ID、方洞 profile 默认值、pending draft 默认值和木鱼 fallback 规则。 这些规则属于平台壳 session / work 恢复映射,应成为可测试的 **Module**。壳层只负责调用网络、写 React state、写 URL 和切换 stage。 @@ -11,6 +11,7 @@ 新增 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts` 作为 Platform Mini Game Session Mapping **Module**。其公开 **Interface** 为: - `buildPuzzleRuntimeWorkFromSession(session, owner)`:从拼图 Agent session 构造可进入 runtime 的 draft `PuzzleWorkSummary`,缺草稿、缺 profile 或缺封面时返回 `null`。 +- `buildSquareHoleProfileFromSession(session)`:从方洞挑战 Agent session draft 构造草稿 `SquareHoleWorkProfile`,缺 session、缺 draft 或缺 profileId 时返回 `null`。 - `buildJumpHopPendingSession(item)`:从跳一跳作品架 summary 构造生成中 pending session。 - `buildWoodenFishSessionFromWorkDetail(work, fallbackItem?)`:从敲木鱼 work detail 恢复 session,并按 summary / fallback / profileId 决定 sessionId。 - `buildWoodenFishPendingSession(item)`:从敲木鱼作品架 summary 构造生成中 pending session。 @@ -22,15 +23,17 @@ - 拼图 runtime work 必须保留 `draft.coverImageSrc` 非空门槛,避免启动缺封面的草稿运行态。 - 拼图 profileId 优先 `publishedProfileId`,否则用 `buildPuzzleResultProfileId(sessionId)`;workId 使用 `buildPuzzleResultWorkId(sessionId)`,缺失时回退 profileId。 - 拼图 owner 缺省为 `current-user` / `玩家`;`publishReady` 来自 `session.resultPreview?.publishReady`。 +- 方洞 profile 的 `workId` 与 `profileId` 都来自 draft `profileId`;owner 固定为 `current-user`,`sourceSessionId` 来自 sessionId。 +- 方洞 profile 的 `updatedAt` 优先 session `updatedAt`,缺失时使用当前时间;`publicationStatus='draft'`、`playCount=0`、`publishedAt=null`,`publishReady` 来自 draft。 - 跳一跳 pending sessionId 优先 `sourceSessionId`,缺失时用 `profileId`;素材、路径和 prompt 维持空值兜底。 - 敲木鱼 detail sessionId 优先级固定为 `work.summary.sourceSessionId > fallbackItem.sourceSessionId > profileId`。 - 敲木鱼 pending session 保持 `floatingWords=['功德 +1']`、素材 / 音效 / 背景为空的旧默认。 ## Depth / Leverage / Locality -- **Depth**:壳层以四个函数取得恢复用 DTO;ID 优先级和默认 draft 字段藏入 Module Implementation。 +- **Depth**:壳层以少量函数取得恢复用 DTO;ID 优先级、方洞 profile 默认值和 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 6de3218b..a53aaffc 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -536,6 +536,7 @@ import { import { buildJumpHopPendingSession, buildPuzzleRuntimeWorkFromSession, + buildSquareHoleProfileFromSession, buildWoodenFishPendingSession, buildWoodenFishSessionFromWorkDetail, } from './platformMiniGameSessionMappingModel'; @@ -751,40 +752,6 @@ function mapVisualNovelWorkDetailToSession( }; } -function buildSquareHoleProfileFromSession( - session: SquareHoleSessionSnapshot | null, -): SquareHoleWorkProfile | null { - const draft = session?.draft; - if (!session || !draft?.profileId) { - return null; - } - - const now = session.updatedAt || new Date().toISOString(); - return { - workId: draft.profileId, - profileId: draft.profileId, - ownerUserId: 'current-user', - sourceSessionId: session.sessionId, - gameName: draft.gameName, - themeText: draft.themeText, - twistRule: draft.twistRule, - summary: draft.summary, - tags: draft.tags, - coverImageSrc: draft.coverImageSrc ?? null, - backgroundPrompt: draft.backgroundPrompt, - backgroundImageSrc: draft.backgroundImageSrc ?? null, - shapeOptions: draft.shapeOptions, - holeOptions: draft.holeOptions, - shapeCount: draft.shapeCount, - difficulty: draft.difficulty, - publicationStatus: 'draft', - playCount: 0, - updatedAt: now, - publishedAt: null, - publishReady: Boolean(draft.publishReady), - }; -} - function mergePuzzleWorkSummary( current: PuzzleWorkSummary, updated: PuzzleWorkSummary, diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts index f83824f6..02754bae 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts @@ -8,6 +8,10 @@ import type { PuzzleResultDraft, } from '../../../packages/shared/src/contracts/puzzleAgentDraft'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; +import type { + SquareHoleResultDraft, + SquareHoleSessionSnapshot, +} from '../../../packages/shared/src/contracts/squareHoleAgent'; import type { WoodenFishAudioAsset, WoodenFishImageAsset, @@ -17,6 +21,7 @@ import type { import { buildJumpHopPendingSession, buildPuzzleRuntimeWorkFromSession, + buildSquareHoleProfileFromSession, buildWoodenFishPendingSession, buildWoodenFishSessionFromWorkDetail, } from './platformMiniGameSessionMappingModel'; @@ -123,6 +128,100 @@ function buildJumpHopSummary( }; } +function buildSquareHoleDraft( + overrides: Partial = {}, +): SquareHoleResultDraft { + return { + profileId: 'square-hole-profile-1', + gameName: '星桥方洞', + themeText: '星桥机关', + twistRule: '只允许相同颜色形状入洞', + summary: '把星桥机关里的形状送入正确孔洞。', + tags: ['星桥', '机关'], + coverImageSrc: '/square-hole-cover.png', + backgroundPrompt: '星桥机关背景', + backgroundImageSrc: '/square-hole-background.png', + shapeOptions: [ + { + optionId: 'shape-1', + shapeKind: 'star', + label: '星形', + targetHoleId: 'hole-1', + imagePrompt: '星形积木', + imageSrc: '/shape-star.png', + }, + ], + holeOptions: [ + { + holeId: 'hole-1', + holeKind: 'star-hole', + label: '星形洞', + imagePrompt: '星形洞口', + imageSrc: '/hole-star.png', + }, + ], + shapeCount: 6, + difficulty: 3, + publishReady: true, + blockers: [], + ...overrides, + }; +} + +function buildSquareHoleSession( + overrides: Partial = {}, +): SquareHoleSessionSnapshot { + return { + sessionId: 'square-hole-session-1', + currentTurn: 2, + progressPercent: 100, + stage: 'draft_ready', + anchorPack: { + theme: { + key: 'theme', + label: '主题', + value: '星桥机关', + status: 'confirmed', + }, + twistRule: { + key: 'twistRule', + label: '扭转规则', + value: '只允许相同颜色形状入洞', + status: 'confirmed', + }, + shapeCount: { + key: 'shapeCount', + label: '形状数量', + value: '6', + status: 'confirmed', + }, + difficulty: { + key: 'difficulty', + label: '难度', + value: '3', + status: 'confirmed', + }, + }, + config: { + themeText: '星桥机关', + twistRule: '只允许相同颜色形状入洞', + shapeCount: 6, + difficulty: 3, + shapeOptions: [], + holeOptions: [], + backgroundPrompt: '星桥机关背景', + coverImageSrc: null, + backgroundImageSrc: null, + }, + draft: buildSquareHoleDraft(), + messages: [], + lastAssistantReply: null, + publishedProfileId: null, + updatedAt: '2026-06-01T12:30:00.000Z', + ...overrides, + }; +} + const woodenFishImageAsset: WoodenFishImageAsset = { assetId: 'asset-hit', imageSrc: '/hit.png', @@ -284,6 +383,50 @@ describe('platformMiniGameSessionMappingModel', () => { }); }); + test('builds square hole draft profile from session', () => { + expect(buildSquareHoleProfileFromSession(buildSquareHoleSession())).toEqual({ + workId: 'square-hole-profile-1', + profileId: 'square-hole-profile-1', + ownerUserId: 'current-user', + sourceSessionId: 'square-hole-session-1', + gameName: '星桥方洞', + themeText: '星桥机关', + twistRule: '只允许相同颜色形状入洞', + summary: '把星桥机关里的形状送入正确孔洞。', + tags: ['星桥', '机关'], + coverImageSrc: '/square-hole-cover.png', + backgroundPrompt: '星桥机关背景', + backgroundImageSrc: '/square-hole-background.png', + shapeOptions: buildSquareHoleDraft().shapeOptions, + holeOptions: buildSquareHoleDraft().holeOptions, + shapeCount: 6, + difficulty: 3, + publicationStatus: 'draft', + playCount: 0, + updatedAt: '2026-06-01T12:30:00.000Z', + publishedAt: null, + publishReady: true, + }); + }); + + test('returns null for square hole profile without session draft or profile id', () => { + expect(buildSquareHoleProfileFromSession(null)).toBeNull(); + expect( + buildSquareHoleProfileFromSession( + buildSquareHoleSession({ + draft: null, + }), + ), + ).toBeNull(); + expect( + buildSquareHoleProfileFromSession( + buildSquareHoleSession({ + draft: buildSquareHoleDraft({ profileId: '' }), + }), + ), + ).toBeNull(); + }); + test('builds wooden fish pending session from work summary', () => { expect(buildWoodenFishPendingSession(buildWoodenFishSummary())).toEqual({ sessionId: 'wooden-fish-session-1', diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts index cadf3d12..c0e327b3 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts @@ -1,6 +1,8 @@ import type { JumpHopSessionSnapshotResponse, JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent'; +import type { SquareHoleWorkProfile } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { WoodenFishSessionSnapshotResponse, WoodenFishWorkProfileResponse, @@ -52,6 +54,40 @@ export function buildPuzzleRuntimeWorkFromSession( }; } +export function buildSquareHoleProfileFromSession( + session: SquareHoleSessionSnapshot | null, +): SquareHoleWorkProfile | null { + const draft = session?.draft; + if (!session || !draft?.profileId) { + return null; + } + + const now = session.updatedAt || new Date().toISOString(); + return { + workId: draft.profileId, + profileId: draft.profileId, + ownerUserId: 'current-user', + sourceSessionId: session.sessionId, + gameName: draft.gameName, + themeText: draft.themeText, + twistRule: draft.twistRule, + summary: draft.summary, + tags: draft.tags, + coverImageSrc: draft.coverImageSrc ?? null, + backgroundPrompt: draft.backgroundPrompt, + backgroundImageSrc: draft.backgroundImageSrc ?? null, + shapeOptions: draft.shapeOptions, + holeOptions: draft.holeOptions, + shapeCount: draft.shapeCount, + difficulty: draft.difficulty, + publicationStatus: 'draft', + playCount: 0, + updatedAt: now, + publishedAt: null, + publishReady: Boolean(draft.publishReady), + }; +} + export function buildJumpHopPendingSession( item: JumpHopWorkSummaryResponse, ): JumpHopSessionSnapshotResponse {