From 20a21ee78bd85f96ae906427ea0391869b710c5f Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 05:09:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E8=A7=86?= =?UTF-8?q?=E8=A7=89=E5=B0=8F=E8=AF=B4=E8=AF=A6=E6=83=85=20session=20?= =?UTF-8?q?=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 6 +- docs/README.md | 2 +- ...iniGameSessionMappingModel收口计划-2026-06-04.md | 8 +- .../PlatformEntryFlowShellImpl.tsx | 19 +-- ...latformMiniGameSessionMappingModel.test.ts | 152 ++++++++++++++++++ .../platformMiniGameSessionMappingModel.ts | 20 +++ 6 files changed, 183 insertions(+), 24 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 80ee8ae5..8f23f1bc 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、跳一跳 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 恢复。 +- 背景:`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 恢复。 - 验证方式:`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 b4bfa104..4c79cdd5 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、方洞 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)。 +平台壳的拼图 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)。 diff --git a/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md index 38ed5064..77b680c1 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、跳一跳 pending session、敲木鱼 work detail 恢复和敲木鱼 pending session 多段纯 DTO 映射。它们没有 React state、网络请求、路由、弹窗或计时副作用,却住在大型平台壳内;新增或修正生成中草稿恢复时,需要在壳层里理解 sessionId 优先级、拼图稳定 ID、方洞 profile 默认值、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。 @@ -12,6 +12,7 @@ - `buildPuzzleRuntimeWorkFromSession(session, owner)`:从拼图 Agent session 构造可进入 runtime 的 draft `PuzzleWorkSummary`,缺草稿、缺 profile 或缺封面时返回 `null`。 - `buildSquareHoleProfileFromSession(session)`:从方洞挑战 Agent session draft 构造草稿 `SquareHoleWorkProfile`,缺 session、缺 draft 或缺 profileId 时返回 `null`。 +- `buildVisualNovelSessionFromWorkDetail(work)`:从视觉小说 work detail 恢复 `VisualNovelAgentSessionSnapshot`,供草稿作品架回到结果页继续编辑。 - `buildJumpHopPendingSession(item)`:从跳一跳作品架 summary 构造生成中 pending session。 - `buildWoodenFishSessionFromWorkDetail(work, fallbackItem?)`:从敲木鱼 work detail 恢复 session,并按 summary / fallback / profileId 决定 sessionId。 - `buildWoodenFishPendingSession(item)`:从敲木鱼作品架 summary 构造生成中 pending session。 @@ -25,15 +26,16 @@ - 拼图 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。 +- 视觉小说恢复 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`。 - 敲木鱼 pending session 保持 `floatingWords=['功德 +1']`、素材 / 音效 / 背景为空的旧默认。 ## Depth / Leverage / Locality -- **Depth**:壳层以少量函数取得恢复用 DTO;ID 优先级、方洞 profile 默认值和 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 a53aaffc..d2ebd2bf 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -537,6 +537,7 @@ import { buildJumpHopPendingSession, buildPuzzleRuntimeWorkFromSession, buildSquareHoleProfileFromSession, + buildVisualNovelSessionFromWorkDetail, buildWoodenFishPendingSession, buildWoodenFishSessionFromWorkDetail, } from './platformMiniGameSessionMappingModel'; @@ -736,22 +737,6 @@ const PUZZLE_DRAFT_GENERATION_POINT_COST = 2; const MATCH3D_DRAFT_GENERATION_POINT_COST = 10; const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3; -function mapVisualNovelWorkDetailToSession( - work: VisualNovelWorkDetail, -): VisualNovelAgentSessionSnapshot { - return { - sessionId: work.sourceSessionId?.trim() || work.workId, - ownerUserId: work.summary.ownerUserId, - sourceMode: work.draft.sourceMode, - status: 'ready', - messages: [], - draft: work.draft, - pendingAction: null, - createdAt: work.createdAt, - updatedAt: work.summary.updatedAt, - }; -} - function mergePuzzleWorkSummary( current: PuzzleWorkSummary, updated: PuzzleWorkSummary, @@ -11094,7 +11079,7 @@ export function PlatformEntryFlowShellImpl({ try { const { work } = await getVisualNovelWorkDetail(item.profileId); setVisualNovelWork(work); - setVisualNovelSession(mapVisualNovelWorkDetailToSession(work)); + setVisualNovelSession(buildVisualNovelSessionFromWorkDetail(work)); enterCreateTab(); setSelectionStage('visual-novel-result'); } catch (error) { diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts index 02754bae..6e557059 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts @@ -12,6 +12,10 @@ import type { SquareHoleResultDraft, SquareHoleSessionSnapshot, } from '../../../packages/shared/src/contracts/squareHoleAgent'; +import type { + VisualNovelResultDraft, + VisualNovelWorkDetail, +} from '../../../packages/shared/src/contracts/visualNovel'; import type { WoodenFishAudioAsset, WoodenFishImageAsset, @@ -22,6 +26,7 @@ import { buildJumpHopPendingSession, buildPuzzleRuntimeWorkFromSession, buildSquareHoleProfileFromSession, + buildVisualNovelSessionFromWorkDetail, buildWoodenFishPendingSession, buildWoodenFishSessionFromWorkDetail, } from './platformMiniGameSessionMappingModel'; @@ -222,6 +227,126 @@ function buildSquareHoleSession( }; } +function buildVisualNovelDraft( + overrides: Partial = {}, +): VisualNovelResultDraft { + return { + profileId: 'visual-novel-profile-1', + workTitle: '雪线电台', + workDescription: '旧电台牵出雪夜列车谜案。', + workTags: ['雪夜', '电台'], + coverImageSrc: '/visual-novel-cover.png', + sourceMode: 'idea', + sourceAssetIds: ['asset-source-1'], + world: { + title: '北境终点线', + summary: '边境小城与旧电台。', + background: '十二年前的雪崩留下夜间广播。', + premise: '玩家需要在日出前找出列车停摆的原因。', + literaryStyle: '克制冷光感。', + playerRole: '临时广播员', + defaultTone: '安静紧张', + }, + characters: [ + { + characterId: 'vn-char-1', + name: '林遥', + gender: '女', + role: 'main', + appearance: '灰色长外套。', + personality: '谨慎敏锐。', + tone: '短句多。', + background: '旧电台夜班实习生。', + relationshipToPlayer: '临时搭档', + imageAssets: [], + defaultExpression: 'calm', + isPlayerVisible: false, + }, + ], + scenes: [ + { + sceneId: 'vn-scene-1', + name: '风雪站台', + description: '站灯忽明忽暗。', + backgroundImageSrc: null, + musicSrc: null, + ambientSoundSrc: null, + availability: 'opening', + phaseIds: ['vn-phase-1'], + }, + ], + storyPhases: [ + { + phaseId: 'vn-phase-1', + title: '重启站台', + goal: '确认列车为何停在废弃站台。', + summary: '玩家抵达风雪站台。', + entryCondition: '开场进入', + exitCondition: '找到车长日志', + sceneIds: ['vn-scene-1'], + characterIds: ['vn-char-1'], + suggestedChoices: ['检查广播柜'], + }, + ], + opening: { + sceneId: 'vn-scene-1', + narration: '雪落得很慢。', + speakerCharacterId: 'vn-char-1', + firstDialogue: '你听见了吗?', + initialChoices: [ + { + choiceId: 'vn-choice-1', + text: '靠近广播柜。', + actionHint: 'inspect_radio', + }, + ], + }, + runtimeConfig: { + textModeEnabled: true, + defaultTextMode: false, + maxHistoryEntries: 80, + maxAssistantStepCountPerTurn: 8, + allowFreeTextAction: true, + allowHistoryRegeneration: true, + attributePanelMode: 'template_config', + saveArchiveEnabled: true, + }, + publishReady: true, + validationIssues: [], + updatedAt: '2026-06-01T13:00:00.000Z', + ...overrides, + }; +} + +function buildVisualNovelWorkDetail( + overrides: Partial = {}, +): VisualNovelWorkDetail { + const draft = buildVisualNovelDraft(); + return { + workId: 'visual-novel-work-1', + summary: { + runtimeKind: 'visual-novel', + profileId: 'visual-novel-profile-1', + ownerUserId: 'user-visual-novel-1', + title: draft.workTitle, + description: draft.workDescription, + coverImageSrc: draft.coverImageSrc, + tags: draft.workTags, + publishStatus: 'draft', + publishReady: draft.publishReady, + playCount: 0, + updatedAt: '2026-06-01T13:30:00.000Z', + publishedAt: null, + }, + sourceSessionId: ' visual-novel-session-1 ', + authorDisplayName: '视觉小说作者', + sourceAssetIds: draft.sourceAssetIds, + draft, + createdAt: '2026-06-01T12:50:00.000Z', + ...overrides, + }; +} + const woodenFishImageAsset: WoodenFishImageAsset = { assetId: 'asset-hit', imageSrc: '/hit.png', @@ -427,6 +552,33 @@ describe('platformMiniGameSessionMappingModel', () => { ).toBeNull(); }); + test('builds visual novel recovered session from work detail', () => { + const work = buildVisualNovelWorkDetail(); + + expect(buildVisualNovelSessionFromWorkDetail(work)).toEqual({ + sessionId: 'visual-novel-session-1', + ownerUserId: 'user-visual-novel-1', + sourceMode: 'idea', + status: 'ready', + messages: [], + draft: work.draft, + pendingAction: null, + createdAt: '2026-06-01T12:50:00.000Z', + updatedAt: '2026-06-01T13:30:00.000Z', + }); + }); + + test('falls back visual novel recovered session id to work id', () => { + expect( + buildVisualNovelSessionFromWorkDetail( + buildVisualNovelWorkDetail({ + sourceSessionId: ' ', + workId: 'visual-novel-work-fallback', + }), + ).sessionId, + ).toBe('visual-novel-work-fallback'); + }); + 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 c0e327b3..a59e9a6a 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts @@ -3,6 +3,10 @@ import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/co 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 { + VisualNovelAgentSessionSnapshot, + VisualNovelWorkDetail, +} from '../../../packages/shared/src/contracts/visualNovel'; import type { WoodenFishSessionSnapshotResponse, WoodenFishWorkProfileResponse, @@ -88,6 +92,22 @@ export function buildSquareHoleProfileFromSession( }; } +export function buildVisualNovelSessionFromWorkDetail( + work: VisualNovelWorkDetail, +): VisualNovelAgentSessionSnapshot { + return { + sessionId: normalizeCreationUrlValue(work.sourceSessionId) ?? work.workId, + ownerUserId: work.summary.ownerUserId, + sourceMode: work.draft.sourceMode, + status: 'ready', + messages: [], + draft: work.draft, + pendingAction: null, + createdAt: work.createdAt, + updatedAt: work.summary.updatedAt, + }; +} + export function buildJumpHopPendingSession( item: JumpHopWorkSummaryResponse, ): JumpHopSessionSnapshotResponse {