refactor: 收口方洞 session profile 映射

This commit is contained in:
2026-06-04 05:03:15 +08:00
parent df5e20d550
commit 0dc326b79e
6 changed files with 190 additions and 41 deletions

View File

@@ -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`

View File

@@ -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)。

View File

@@ -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**:壳层以四个函数取得恢复用 DTOID 优先级和默认 draft 字段藏入 Module Implementation。
- **Depth**:壳层以少量函数取得恢复用 DTOID 优先级、方洞 profile 默认值和 pending draft 字段藏入 Module Implementation。
- **Leverage**:后续新增生成中作品恢复或修改 sessionId 规则时,先改 Module 与单测,再保持壳层 Adapter 副作用不变。
- **Locality**:拼图、跳一跳和敲木鱼的恢复映射集中在一个纯测试面,避免在大型壳层顶部继续堆积 DTO 构造。
- **Locality**:拼图、方洞、跳一跳和敲木鱼的恢复映射集中在一个纯测试面,避免在大型壳层顶部继续堆积 DTO 构造。
## 验收

View File

@@ -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,

View File

@@ -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> = {},
): 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> = {},
): 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',

View File

@@ -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 {