refactor: 收口小游戏会话映射

This commit is contained in:
2026-06-04 02:59:42 +08:00
parent 5dd73186b0
commit 671f5da86a
7 changed files with 538 additions and 118 deletions

View File

@@ -513,6 +513,12 @@ import {
resolveMatch3DRuntimeGeneratedBackgroundAsset,
resolveMatch3DRuntimeGeneratedItemAssets,
} from './platformMatch3DRuntimeProfile';
import {
buildJumpHopPendingSession,
buildPuzzleRuntimeWorkFromSession,
buildWoodenFishPendingSession,
buildWoodenFishSessionFromWorkDetail,
} from './platformMiniGameSessionMappingModel';
import { resolvePlatformPlayedWorkOpenIntent } from './platformPlayedWorkOpenModel';
import {
type PlatformPublicCodeSearchStep,
@@ -1156,124 +1162,6 @@ function openPuzzleRuntimeStage(
writePuzzleRuntimeUrlState(state);
}
function buildPuzzleRuntimeWorkFromSession(
session: PuzzleAgentSessionSnapshot,
owner: { userId?: string | null; displayName?: string | null },
): PuzzleWorkSummary | null {
const draft = session.draft;
const profileId =
session.publishedProfileId ?? buildPuzzleResultProfileId(session.sessionId);
if (!draft || !profileId || !draft.coverImageSrc?.trim()) {
return null;
}
return {
workId: buildPuzzleResultWorkId(session.sessionId) ?? profileId,
profileId,
ownerUserId: owner.userId ?? 'current-user',
sourceSessionId: session.sessionId,
authorDisplayName: owner.displayName ?? '玩家',
workTitle: draft.workTitle,
workDescription: draft.workDescription,
levelName: draft.levelName,
summary: draft.summary,
themeTags: draft.themeTags,
coverImageSrc: draft.coverImageSrc,
coverAssetId: draft.coverAssetId,
publicationStatus: 'draft',
updatedAt: session.updatedAt,
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: Boolean(session.resultPreview?.publishReady),
levels: draft.levels,
};
}
function buildJumpHopPendingSession(
item: JumpHopWorkSummaryResponse,
): JumpHopSessionSnapshotResponse {
const sessionId =
normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId;
return {
sessionId,
ownerUserId: item.ownerUserId,
status: item.generationStatus,
draft: {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId: item.profileId,
workTitle: item.workTitle,
workDescription: item.workDescription,
themeTags: item.themeTags,
difficulty: item.difficulty,
stylePreset: item.stylePreset,
characterPrompt: '',
tilePrompt: '',
endMoodPrompt: null,
characterAsset: null,
tileAtlasAsset: null,
tileAssets: [],
path: null,
coverComposite: item.coverImageSrc,
generationStatus: item.generationStatus,
},
createdAt: item.updatedAt,
updatedAt: item.updatedAt,
};
}
function buildWoodenFishSessionFromWorkDetail(
work: WoodenFishWorkProfileResponse,
fallbackItem?: WoodenFishWorkSummaryResponse | null,
): WoodenFishSessionSnapshotResponse {
const sessionId =
normalizeCreationUrlValue(work.summary.sourceSessionId) ??
normalizeCreationUrlValue(fallbackItem?.sourceSessionId) ??
work.summary.profileId;
return {
sessionId,
ownerUserId: work.summary.ownerUserId,
status: work.summary.generationStatus,
draft: work.draft,
createdAt: work.summary.updatedAt,
updatedAt: work.summary.updatedAt,
};
}
function buildWoodenFishPendingSession(
item: WoodenFishWorkSummaryResponse,
): WoodenFishSessionSnapshotResponse {
const sessionId =
normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId;
return {
sessionId,
ownerUserId: item.ownerUserId,
status: item.generationStatus,
draft: {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: item.profileId,
workTitle: item.workTitle,
workDescription: item.workDescription,
themeTags: item.themeTags,
hitObjectPrompt: '',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
hitObjectAsset: null,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: null,
coverImageSrc: item.coverImageSrc,
generationStatus: item.generationStatus,
},
createdAt: item.updatedAt,
updatedAt: item.updatedAt,
};
}
/** 为恢复的小游戏草稿重建生成态,保留后端开始时间作为进度事实源。 */
function createMiniGameDraftGenerationStateForRestoredDraft(
kind: MiniGameDraftGenerationKind,

View File

@@ -0,0 +1,344 @@
import { describe, expect, test } from 'vitest';
import type {
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type {
PuzzleAnchorPack,
PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type {
WoodenFishAudioAsset,
WoodenFishImageAsset,
WoodenFishWorkProfileResponse,
WoodenFishWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/woodenFish';
import {
buildJumpHopPendingSession,
buildPuzzleRuntimeWorkFromSession,
buildWoodenFishPendingSession,
buildWoodenFishSessionFromWorkDetail,
} from './platformMiniGameSessionMappingModel';
function buildAnchorPack(): PuzzleAnchorPack {
const item = {
key: 'theme',
label: '主题',
value: '星桥机关',
status: 'confirmed' as const,
};
return {
themePromise: item,
visualSubject: item,
visualMood: item,
compositionHooks: item,
tagsAndForbidden: item,
};
}
function buildPuzzleDraft(
overrides: Partial<PuzzleResultDraft> = {},
): PuzzleResultDraft {
const anchorPack = buildAnchorPack();
return {
workTitle: '星桥拼图',
workDescription: '修复星桥机关。',
levelName: '星桥机关',
summary: '把星桥碎片拼回原位。',
themeTags: ['星桥'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack,
candidates: [],
selectedCandidateId: null,
coverImageSrc: '/puzzle-cover.png',
coverAssetId: 'asset-cover',
generationStatus: 'ready',
levels: [
{
levelId: 'level-1',
levelName: '星桥机关',
pictureDescription: '星桥',
candidates: [],
selectedCandidateId: null,
coverImageSrc: '/puzzle-level-cover.png',
coverAssetId: 'asset-level-cover',
generationStatus: 'ready',
},
],
...overrides,
};
}
function buildPuzzleSession(
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
const draft = buildPuzzleDraft();
return {
sessionId: 'puzzle-session-12345678',
seedText: '星桥',
currentTurn: 1,
progressPercent: 100,
stage: 'ready_to_publish',
anchorPack: draft.anchorPack,
draft,
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
suggestedActions: [],
resultPreview: {
draft,
blockers: [],
qualityFindings: [],
publishReady: true,
},
updatedAt: '2026-06-01T10:00:00.000Z',
...overrides,
};
}
function buildJumpHopSummary(
overrides: Partial<JumpHopWorkSummaryResponse> = {},
): JumpHopWorkSummaryResponse {
return {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-1',
profileId: 'jump-hop-profile-1',
ownerUserId: 'user-1',
sourceSessionId: ' jump-hop-session-1 ',
workTitle: '云阶跳跃',
workDescription: '越过云阶。',
themeTags: ['云阶'],
difficulty: 'standard',
stylePreset: 'paper-toy',
coverImageSrc: '/jump-hop-cover.png',
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-01T11:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'generating',
...overrides,
};
}
const woodenFishImageAsset: WoodenFishImageAsset = {
assetId: 'asset-hit',
imageSrc: '/hit.png',
imageObjectKey: 'hit.png',
assetObjectId: 'asset-object-hit',
generationProvider: 'test',
prompt: '木鱼',
width: 512,
height: 512,
};
const woodenFishAudioAsset: WoodenFishAudioAsset = {
assetId: 'asset-sound',
audioSrc: '/hit.mp3',
audioObjectKey: 'hit.mp3',
assetObjectId: 'asset-object-sound',
source: 'test',
};
function buildWoodenFishSummary(
overrides: Partial<WoodenFishWorkSummaryResponse> = {},
): WoodenFishWorkSummaryResponse {
return {
runtimeKind: 'wooden-fish',
workId: 'wooden-fish-work-1',
profileId: 'wooden-fish-profile-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',
...overrides,
};
}
function buildWoodenFishWorkProfile(
overrides: Partial<WoodenFishWorkProfileResponse> = {},
): WoodenFishWorkProfileResponse {
const summary = buildWoodenFishSummary();
const draft = {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: summary.profileId,
workTitle: summary.workTitle,
workDescription: summary.workDescription,
themeTags: summary.themeTags,
hitObjectPrompt: '星灯',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
hitObjectAsset: woodenFishImageAsset,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: woodenFishAudioAsset,
coverImageSrc: summary.coverImageSrc,
generationStatus: summary.generationStatus,
};
return {
summary,
draft,
hitObjectAsset: woodenFishImageAsset,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: woodenFishAudioAsset,
floatingWords: ['功德 +1'],
...overrides,
};
}
describe('platformMiniGameSessionMappingModel', () => {
test('builds a draft puzzle runtime work from a session', () => {
expect(
buildPuzzleRuntimeWorkFromSession(buildPuzzleSession(), {
userId: 'user-1',
displayName: '玩家一号',
}),
).toMatchObject({
workId: 'puzzle-work-12345678',
profileId: 'puzzle-profile-12345678',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-12345678',
authorDisplayName: '玩家一号',
workTitle: '星桥拼图',
coverImageSrc: '/puzzle-cover.png',
publicationStatus: 'draft',
publishedAt: null,
publishReady: true,
});
});
test('prefers published puzzle profile id when present', () => {
expect(
buildPuzzleRuntimeWorkFromSession(
buildPuzzleSession({
publishedProfileId: 'published-puzzle-profile',
}),
{},
),
).toMatchObject({
profileId: 'published-puzzle-profile',
workId: 'puzzle-work-12345678',
ownerUserId: 'current-user',
authorDisplayName: '玩家',
});
});
test('returns null for puzzle runtime work without draft or cover', () => {
expect(
buildPuzzleRuntimeWorkFromSession(
buildPuzzleSession({
draft: null,
}),
{},
),
).toBeNull();
expect(
buildPuzzleRuntimeWorkFromSession(
buildPuzzleSession({
draft: buildPuzzleDraft({ coverImageSrc: ' ' }),
}),
{},
),
).toBeNull();
});
test('builds jump hop pending session from work summary', () => {
expect(buildJumpHopPendingSession(buildJumpHopSummary())).toEqual({
sessionId: 'jump-hop-session-1',
ownerUserId: 'user-1',
status: 'generating',
draft: {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId: 'jump-hop-profile-1',
workTitle: '云阶跳跃',
workDescription: '越过云阶。',
themeTags: ['云阶'],
difficulty: 'standard',
stylePreset: 'paper-toy',
characterPrompt: '',
tilePrompt: '',
endMoodPrompt: null,
characterAsset: null,
tileAtlasAsset: null,
tileAssets: [],
path: null,
coverComposite: '/jump-hop-cover.png',
generationStatus: 'generating',
},
createdAt: '2026-06-01T11:00:00.000Z',
updatedAt: '2026-06-01T11:00:00.000Z',
});
});
test('builds wooden fish pending session from work summary', () => {
expect(buildWoodenFishPendingSession(buildWoodenFishSummary())).toEqual({
sessionId: 'wooden-fish-session-1',
ownerUserId: 'user-1',
status: 'generating',
draft: {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: 'wooden-fish-profile-1',
workTitle: '星灯木鱼',
workDescription: '敲亮星灯。',
themeTags: ['星灯'],
hitObjectPrompt: '',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
hitObjectAsset: null,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: null,
coverImageSrc: '/wooden-fish-cover.png',
generationStatus: 'generating',
},
createdAt: '2026-06-01T12:00:00.000Z',
updatedAt: '2026-06-01T12:00:00.000Z',
});
});
test('builds wooden fish recovered session with summary, fallback and profile id priority', () => {
expect(
buildWoodenFishSessionFromWorkDetail(
buildWoodenFishWorkProfile({
summary: buildWoodenFishSummary({
sourceSessionId: null,
}),
}),
buildWoodenFishSummary({
sourceSessionId: ' fallback-session ',
}),
),
).toMatchObject({
sessionId: 'fallback-session',
ownerUserId: 'user-1',
status: 'generating',
});
expect(
buildWoodenFishSessionFromWorkDetail(
buildWoodenFishWorkProfile({
summary: buildWoodenFishSummary({
sourceSessionId: null,
}),
}),
null,
).sessionId,
).toBe('wooden-fish-profile-1');
});
});

View File

@@ -0,0 +1,136 @@
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 {
WoodenFishSessionSnapshotResponse,
WoodenFishWorkProfileResponse,
WoodenFishWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/woodenFish';
import { normalizeCreationUrlValue } from './platformCreationUrlStateModel';
import {
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
} from './platformPuzzleIdentityModel';
export type PlatformMiniGameSessionOwner = {
userId?: string | null;
displayName?: string | null;
};
export function buildPuzzleRuntimeWorkFromSession(
session: PuzzleAgentSessionSnapshot,
owner: PlatformMiniGameSessionOwner,
): PuzzleWorkSummary | null {
const draft = session.draft;
const profileId =
session.publishedProfileId ?? buildPuzzleResultProfileId(session.sessionId);
if (!draft || !profileId || !draft.coverImageSrc?.trim()) {
return null;
}
return {
workId: buildPuzzleResultWorkId(session.sessionId) ?? profileId,
profileId,
ownerUserId: owner.userId ?? 'current-user',
sourceSessionId: session.sessionId,
authorDisplayName: owner.displayName ?? '玩家',
workTitle: draft.workTitle,
workDescription: draft.workDescription,
levelName: draft.levelName,
summary: draft.summary,
themeTags: draft.themeTags,
coverImageSrc: draft.coverImageSrc,
coverAssetId: draft.coverAssetId,
publicationStatus: 'draft',
updatedAt: session.updatedAt,
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: Boolean(session.resultPreview?.publishReady),
levels: draft.levels,
};
}
export function buildJumpHopPendingSession(
item: JumpHopWorkSummaryResponse,
): JumpHopSessionSnapshotResponse {
const sessionId =
normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId;
return {
sessionId,
ownerUserId: item.ownerUserId,
status: item.generationStatus,
draft: {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId: item.profileId,
workTitle: item.workTitle,
workDescription: item.workDescription,
themeTags: item.themeTags,
difficulty: item.difficulty,
stylePreset: item.stylePreset,
characterPrompt: '',
tilePrompt: '',
endMoodPrompt: null,
characterAsset: null,
tileAtlasAsset: null,
tileAssets: [],
path: null,
coverComposite: item.coverImageSrc,
generationStatus: item.generationStatus,
},
createdAt: item.updatedAt,
updatedAt: item.updatedAt,
};
}
export function buildWoodenFishSessionFromWorkDetail(
work: WoodenFishWorkProfileResponse,
fallbackItem?: WoodenFishWorkSummaryResponse | null,
): WoodenFishSessionSnapshotResponse {
const sessionId =
normalizeCreationUrlValue(work.summary.sourceSessionId) ??
normalizeCreationUrlValue(fallbackItem?.sourceSessionId) ??
work.summary.profileId;
return {
sessionId,
ownerUserId: work.summary.ownerUserId,
status: work.summary.generationStatus,
draft: work.draft,
createdAt: work.summary.updatedAt,
updatedAt: work.summary.updatedAt,
};
}
export function buildWoodenFishPendingSession(
item: WoodenFishWorkSummaryResponse,
): WoodenFishSessionSnapshotResponse {
const sessionId =
normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId;
return {
sessionId,
ownerUserId: item.ownerUserId,
status: item.generationStatus,
draft: {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: item.profileId,
workTitle: item.workTitle,
workDescription: item.workDescription,
themeTags: item.themeTags,
hitObjectPrompt: '',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['功德 +1'],
hitObjectAsset: null,
backgroundAsset: null,
backButtonAsset: null,
hitSoundAsset: null,
coverImageSrc: item.coverImageSrc,
generationStatus: item.generationStatus,
},
createdAt: item.updatedAt,
updatedAt: item.updatedAt,
};
}