Add generationStatus and match3d/runtime fixes

Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
This commit is contained in:
2026-05-16 22:59:02 +08:00
parent bb60ca91ef
commit a45e358e83
42 changed files with 3872 additions and 443 deletions

View File

@@ -188,6 +188,7 @@ import {
buildPuzzleGenerationAnchorEntries,
buildSquareHoleGenerationAnchorEntries,
createMiniGameDraftGenerationState,
type MiniGameDraftGenerationKind,
type MiniGameDraftGenerationState,
} from '../../services/miniGameDraftGenerationProgress';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
@@ -629,7 +630,9 @@ function resolveVisiblePuzzleDetailCoverCount(
function mapMatch3DWorkToPublicWorkDetail(
item: Match3DWorkSummary,
): PlatformPublicGalleryCard {
return mapMatch3DWorkToPlatformGalleryCard(item);
return mapMatch3DWorkToPlatformGalleryCard(
normalizeMatch3DWorkForRuntimeUi(item),
);
}
function mapSquareHoleWorkToPublicWorkDetail(
@@ -753,6 +756,23 @@ function promoteMatch3DGeneratedBackgroundAsset<
};
}
function normalizeMatch3DWorkForRuntimeUi<T extends Match3DWorkSummary>(
profile: T,
): T {
return promoteMatch3DGeneratedBackgroundAsset({
...profile,
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
profile.generatedItemAssets,
),
});
}
function mapMatch3DWorksForRuntimeUi<T extends Match3DWorkSummary>(
profiles: readonly T[],
): T[] {
return profiles.map(normalizeMatch3DWorkForRuntimeUi);
}
function buildMatch3DProfileFromSession(
session: Match3DAgentSessionSnapshot | null,
): Match3DWorkProfile | null {
@@ -1648,14 +1668,121 @@ function normalizeDraftNoticeId(id: string | null | undefined) {
function createPendingDraftShelfState(
status: DraftGenerationNoticeStatus,
seen = false,
updatedAt = new Date().toISOString(),
): PendingDraftShelfState {
return {
status,
seen,
updatedAt: new Date().toISOString(),
updatedAt,
};
}
function parseDraftGenerationStartedAtMs(value: string | null | undefined) {
const parsedMs = value ? Date.parse(value) : Number.NaN;
return Number.isFinite(parsedMs) ? parsedMs : Date.now();
}
function createMiniGameDraftGenerationStateFromStartedAt(
kind: MiniGameDraftGenerationKind,
startedAtMs: number,
): MiniGameDraftGenerationState {
return {
...createMiniGameDraftGenerationState(kind),
startedAtMs,
};
}
function resolveFinishedMiniGameDraftGenerationState(
state: MiniGameDraftGenerationState,
phase: 'ready' | 'failed',
options: {
error?: string | null;
completedAssetCount?: number;
totalAssetCount?: number;
} = {},
): MiniGameDraftGenerationState {
return {
...state,
phase,
finishedAtMs: Date.now(),
error: options.error ?? state.error,
completedAssetCount:
options.completedAssetCount ?? state.completedAssetCount,
totalAssetCount: options.totalAssetCount ?? state.totalAssetCount,
};
}
function normalizeRecoveredPuzzleDraftSession(
session: PuzzleAgentSessionSnapshot,
): PuzzleAgentSessionSnapshot {
const draft = session.draft;
if (!draft) {
return session;
}
const primaryLevel = draft.levels?.[0];
const selectedCandidate =
primaryLevel?.candidates.find((candidate) => candidate.selected) ??
primaryLevel?.candidates[0] ??
draft.candidates.find((candidate) => candidate.selected) ??
draft.candidates[0] ??
null;
const coverImageSrc =
draft.coverImageSrc?.trim() ||
primaryLevel?.coverImageSrc?.trim() ||
selectedCandidate?.imageSrc.trim() ||
null;
const coverAssetId =
draft.coverAssetId?.trim() ||
primaryLevel?.coverAssetId?.trim() ||
selectedCandidate?.assetId.trim() ||
null;
const selectedCandidateId =
draft.selectedCandidateId ??
primaryLevel?.selectedCandidateId ??
selectedCandidate?.candidateId ??
null;
return {
...session,
draft: {
...draft,
coverImageSrc,
coverAssetId,
selectedCandidateId,
generationStatus: 'ready',
levels: draft.levels?.map((level, index) =>
index === 0
? {
...level,
coverImageSrc: level.coverImageSrc ?? coverImageSrc,
coverAssetId: level.coverAssetId ?? coverAssetId,
selectedCandidateId:
level.selectedCandidateId ?? selectedCandidateId,
generationStatus: 'ready',
}
: level,
),
},
};
}
function hasRecoverableGeneratedPuzzleDraft(
session: PuzzleAgentSessionSnapshot,
) {
const draft = session.draft;
if (!draft) {
return false;
}
const firstLevel = draft.levels?.[0];
return Boolean(
draft.coverImageSrc?.trim() ||
firstLevel?.coverImageSrc?.trim() ||
firstLevel?.candidates.some((candidate) => candidate.imageSrc.trim()),
);
}
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
switch (item.source.kind) {
case 'rpg':
@@ -1790,6 +1917,7 @@ function buildPendingMatch3DWorks(
updatedAt: state.updatedAt,
publishedAt: null,
publishReady: false,
generationStatus: state.status === 'generating' ? 'generating' : 'ready',
generatedItemAssets: [],
}));
}
@@ -1867,6 +1995,7 @@ function buildPendingPuzzleWorks(
remixCount: 0,
likeCount: 0,
publishReady: false,
generationStatus: state.status === 'generating' ? 'generating' : 'ready',
levels: [],
};
});
@@ -2885,7 +3014,7 @@ export function PlatformEntryFlowShellImpl({
try {
const worksResponse = await listMatch3DWorks();
setMatch3DWorks(worksResponse.items);
setMatch3DWorks(mapMatch3DWorksForRuntimeUi(worksResponse.items));
match3DErrorSetterRef.current(null);
} catch (error) {
match3DErrorSetterRef.current(
@@ -2899,8 +3028,9 @@ export function PlatformEntryFlowShellImpl({
const refreshMatch3DGallery = useCallback(async () => {
try {
const galleryResponse = await listMatch3DGallery();
setMatch3DGalleryEntries(galleryResponse.items);
return galleryResponse.items;
const items = mapMatch3DWorksForRuntimeUi(galleryResponse.items);
setMatch3DGalleryEntries(items);
return items;
} catch {
// 中文注释:公开广场是首页展示数据,失败时只降级为空列表;
// 不写入创作错误态,避免挡住抓大鹅共创入口。
@@ -3247,7 +3377,7 @@ export function PlatformEntryFlowShellImpl({
.map(mapBabyObjectMatchDraftToPlatformGalleryCard)
: [];
const match3dPublicEntries = match3dGalleryEntries.map(
mapMatch3DWorkToPlatformGalleryCard,
mapMatch3DWorkToPublicWorkDetail,
);
const puzzlePublicEntries = puzzleGalleryEntries.map(
mapPuzzleWorkToPlatformGalleryCard,
@@ -3289,7 +3419,7 @@ export function PlatformEntryFlowShellImpl({
...(isBigFishCreationVisible
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
: []),
...match3dGalleryEntries.map(mapMatch3DWorkToPlatformGalleryCard),
...match3dGalleryEntries.map(mapMatch3DWorkToPublicWorkDetail),
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
...squareHoleGalleryEntries.map(
mapSquareHoleWorkToPlatformGalleryCard,
@@ -3395,7 +3525,8 @@ export function PlatformEntryFlowShellImpl({
getGenerationNoticeShelfKeys(item),
);
return {
isGenerating: notice?.status === 'generating',
isGenerating:
notice?.status === 'generating' || item.isGenerating === true,
hasUnreadUpdate: notice?.status === 'ready' && !notice.seen,
};
},
@@ -3769,14 +3900,12 @@ export function PlatformEntryFlowShellImpl({
}
setBigFishGenerationState((current) =>
current
? {
...current,
phase: 'ready',
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount: response.session.assetSlots.filter(
(slot) => slot.status === 'ready',
).length,
totalAssetCount: response.session.assetSlots.length,
}
})
: current,
);
const openResult = selectionStageRef.current === 'big-fish-generating';
@@ -3808,11 +3937,9 @@ export function PlatformEntryFlowShellImpl({
}
setBigFishGenerationState((current) =>
current
? {
...current,
phase: 'failed',
? resolveFinishedMiniGameDraftGenerationState(current, 'failed', {
error: errorMessage,
}
})
: current,
);
},
@@ -3864,14 +3991,12 @@ export function PlatformEntryFlowShellImpl({
const openResult = selectionStageRef.current === 'match3d-generating';
setMatch3DGenerationState((current) =>
current
? {
...current,
phase: 'ready',
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
totalAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
}
})
: current,
);
@@ -3885,7 +4010,7 @@ export function PlatformEntryFlowShellImpl({
let runtimeProfile: Match3DWorkProfile | null = null;
try {
const { item } = await getMatch3DWorkDetail(profileId);
runtimeProfile = promoteMatch3DGeneratedBackgroundAsset({
runtimeProfile = normalizeMatch3DWorkForRuntimeUi({
...item,
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
response.session.draft?.generatedItemAssets,
@@ -3949,11 +4074,9 @@ export function PlatformEntryFlowShellImpl({
}
setMatch3DGenerationState((current) =>
current
? {
...current,
phase: 'failed',
? resolveFinishedMiniGameDraftGenerationState(current, 'failed', {
error: errorMessage,
}
})
: current,
);
try {
@@ -3964,7 +4087,7 @@ export function PlatformEntryFlowShellImpl({
latestSession.draft?.profileId ?? latestSession.publishedProfileId;
if (profileId) {
const { item } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(item);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(item));
}
await refreshMatch3DShelf().catch(() => undefined);
} catch {
@@ -4101,15 +4224,19 @@ export function PlatformEntryFlowShellImpl({
const { item } = await getSquareHoleWorkDetail(assetProfileId);
const shouldOpenResult = shouldOpenSquareHoleResult();
setSquareHoleProfile(item);
setSquareHoleGenerationState((current) => ({
...(current ?? createMiniGameDraftGenerationState('square-hole')),
phase: 'ready',
completedAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
totalAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
error: null,
}));
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'ready',
{
completedAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
totalAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
error: null,
},
),
);
await refreshSquareHoleShelf().catch(() => undefined);
markPendingDraftReady(
'square-hole',
@@ -4131,11 +4258,13 @@ export function PlatformEntryFlowShellImpl({
'生成方洞挑战图片失败。',
);
setSquareHoleError(errorMessage);
setSquareHoleGenerationState((current) => ({
...(current ?? createMiniGameDraftGenerationState('square-hole')),
phase: 'failed',
error: errorMessage,
}));
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'failed',
{ error: errorMessage },
),
);
setSquareHoleProfile(
buildSquareHoleProfileFromSession(response.session),
);
@@ -4150,15 +4279,19 @@ export function PlatformEntryFlowShellImpl({
const { item } = await getSquareHoleWorkDetail(profileId);
const shouldOpenResult = shouldOpenSquareHoleResult();
setSquareHoleProfile(item);
setSquareHoleGenerationState((current) => ({
...(current ?? createMiniGameDraftGenerationState('square-hole')),
phase: 'ready',
completedAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
totalAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
error: null,
}));
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'ready',
{
completedAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
totalAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
error: null,
},
),
);
await refreshSquareHoleShelf().catch(() => undefined);
markPendingDraftReady(
'square-hole',
@@ -4198,11 +4331,13 @@ export function PlatformEntryFlowShellImpl({
payload.action === 'square_hole_compile_draft' ||
payload.action === 'square_hole_generate_visual_assets'
) {
setSquareHoleGenerationState((current) => ({
...(current ?? createMiniGameDraftGenerationState('square-hole')),
phase: 'failed',
error: errorMessage,
}));
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'failed',
{ error: errorMessage },
),
);
if (selectionStageRef.current === 'square-hole-generating') {
setSelectionStage('square-hole-generating');
}
@@ -4275,12 +4410,10 @@ export function PlatformEntryFlowShellImpl({
const openResult = selectionStageRef.current === 'puzzle-generating';
setPuzzleGenerationState((current) =>
current
? {
...current,
phase: 'ready',
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount: 1,
totalAssetCount: 1,
}
})
: current,
);
const profileId =
@@ -4385,19 +4518,56 @@ export function PlatformEntryFlowShellImpl({
markPendingDraftGenerating('puzzle', session.sessionId);
selectionStageRef.current = 'puzzle-generating';
setSelectionStage('puzzle-generating');
setPuzzleGenerationState(createMiniGameDraftGenerationState('puzzle'));
const nextGenerationState = createMiniGameDraftGenerationState('puzzle');
setPuzzleGenerationState(nextGenerationState);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[session.sessionId]: {
session,
payload: formPayload ?? buildPuzzleFormPayloadFromSession(session),
generationState: nextGenerationState,
error: null,
},
}));
},
onActionError: ({ payload, errorMessage }) => {
onActionError: async ({ payload, errorMessage, session, setSession }) => {
if (payload.action !== 'compile_puzzle_draft') {
return;
}
const generationState =
puzzleBackgroundCompileTasks[session.sessionId]?.generationState ??
puzzleGenerationState ??
createMiniGameDraftGenerationState('puzzle');
const formPayload =
buildPuzzleFormPayloadFromAction(payload) ??
puzzleBackgroundCompileTasks[session.sessionId]?.payload ??
buildPuzzleFormPayloadFromSession(session);
const recovered = await recoverCompletedPuzzleDraftGeneration({
sessionId: session.sessionId,
payload: formPayload,
generationState,
setSession,
});
if (recovered) {
return;
}
const failedGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[session.sessionId]: {
session,
payload: formPayload,
generationState: failedGenerationState,
error: errorMessage,
},
}));
setPuzzleGenerationState((current) =>
current
? {
...current,
phase: 'failed',
error: errorMessage,
}
? failedGenerationState
: current,
);
},
@@ -4527,6 +4697,124 @@ export function PlatformEntryFlowShellImpl({
setMatch3DError,
);
}, [ensureEnoughDraftGenerationPointsFromServer, setMatch3DError]);
const recoverCompletedPuzzleDraftGeneration = useCallback(
async ({
sessionId,
payload,
generationState,
setSession,
}: {
sessionId: string;
payload: CreatePuzzleAgentSessionRequest;
generationState: MiniGameDraftGenerationState;
setSession?: (session: PuzzleAgentSessionSnapshot) => void;
}) => {
let latestSession: PuzzleAgentSessionSnapshot;
try {
const response = await getPuzzleAgentSession(sessionId);
latestSession = normalizeRecoveredPuzzleDraftSession(response.session);
} catch {
return null;
}
if (!hasRecoverableGeneratedPuzzleDraft(latestSession)) {
return null;
}
const readyGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'ready',
{
completedAssetCount: 1,
totalAssetCount: 1,
error: null,
},
);
const openResult = isViewingPuzzleGeneration(sessionId);
const profileId =
latestSession.publishedProfileId ??
buildPuzzleResultProfileId(latestSession.sessionId);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[sessionId]: {
session: latestSession,
payload,
generationState: readyGenerationState,
error: null,
},
}));
setSession?.(latestSession);
setPuzzleFormDraftPayload(payload);
setPuzzleOperation(null);
puzzleErrorSetterRef.current(null);
if (isViewingPuzzleGeneration(sessionId)) {
setPuzzleGenerationState(readyGenerationState);
}
markPendingDraftReady('puzzle', latestSession.sessionId, openResult);
markDraftReady(
'puzzle',
[
latestSession.sessionId,
buildPuzzleResultWorkId(latestSession.sessionId),
profileId,
],
openResult,
);
await refreshPuzzleShelf().catch(() => undefined);
if (!openResult) {
return { openResult };
}
const draft = latestSession.draft;
if (!draft?.coverImageSrc || !profileId) {
puzzleErrorSetterRef.current(
!draft?.coverImageSrc
? '请先选择一张正式拼图图片。'
: '这份拼图草稿缺少会话信息,请重新开始创作。',
);
setSelectionStage('puzzle-result');
return { openResult: false };
}
try {
const { item } = await updatePuzzleWork(profileId, {
workTitle: draft.workTitle,
workDescription: draft.workDescription,
levelName: draft.levelName,
summary: draft.summary,
themeTags: draft.themeTags,
coverImageSrc: draft.coverImageSrc,
coverAssetId: draft.coverAssetId,
levels: draft.levels ?? [],
});
const run = startLocalPuzzleRun(item);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('puzzle-result');
setSelectionStage('puzzle-runtime');
} catch (error) {
puzzleErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'),
);
setSelectionStage('puzzle-result');
}
return { openResult: false };
},
[
isViewingPuzzleGeneration,
markDraftReady,
markPendingDraftReady,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const activeMatch3DGenerationSessionId =
selectionStage === 'match3d-generating'
@@ -4612,11 +4900,12 @@ export function PlatformEntryFlowShellImpl({
return;
}
setMatch3DProfile(item);
const normalizedItem = normalizeMatch3DWorkForRuntimeUi(item);
setMatch3DProfile(normalizedItem);
setMatch3DGenerationState((current) =>
resolveMatch3DGenerationStateFromAssets(
current,
item.generatedItemAssets,
normalizedItem.generatedItemAssets,
),
);
} catch {
@@ -4780,12 +5069,14 @@ export function PlatformEntryFlowShellImpl({
);
setPuzzleOperation(response.operation);
const openResult = isViewingPuzzleGeneration(nextSession.sessionId);
const readyGenerationState = {
...generationState,
phase: 'ready' as const,
completedAssetCount: 1,
totalAssetCount: 1,
};
const readyGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'ready',
{
completedAssetCount: 1,
totalAssetCount: 1,
},
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
@@ -4859,11 +5150,20 @@ export function PlatformEntryFlowShellImpl({
error,
'执行拼图操作失败。',
);
const failedGenerationState = {
...generationState,
phase: 'failed' as const,
error: errorMessage,
};
const recovered = await recoverCompletedPuzzleDraftGeneration({
sessionId: nextSession.sessionId,
payload,
generationState,
setSession: puzzleFlow.setSession,
});
if (recovered) {
return;
}
const failedGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
@@ -4888,6 +5188,7 @@ export function PlatformEntryFlowShellImpl({
preflightPuzzleDraftGeneration,
puzzleFlow,
refreshPuzzleShelf,
recoverCompletedPuzzleDraftGeneration,
resolvePuzzleErrorMessage,
setPuzzleError,
setSelectionStage,
@@ -4950,14 +5251,16 @@ export function PlatformEntryFlowShellImpl({
},
);
const openResult = isViewingMatch3DGeneration(nextSession.sessionId);
const readyGenerationState = {
...generationState,
phase: 'ready' as const,
completedAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
totalAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
};
const readyGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'ready',
{
completedAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
totalAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
},
);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
@@ -4984,7 +5287,7 @@ export function PlatformEntryFlowShellImpl({
let runtimeProfile: Match3DWorkProfile | null = null;
try {
const { item } = await getMatch3DWorkDetail(profileId);
runtimeProfile = promoteMatch3DGeneratedBackgroundAsset({
runtimeProfile = normalizeMatch3DWorkForRuntimeUi({
...item,
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
response.session.draft?.generatedItemAssets,
@@ -5039,11 +5342,11 @@ export function PlatformEntryFlowShellImpl({
error,
'执行抓大鹅操作失败。',
);
const failedGenerationState = {
...generationState,
phase: 'failed' as const,
error: errorMessage,
};
const failedGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
@@ -5075,7 +5378,7 @@ export function PlatformEntryFlowShellImpl({
latestSession.draft?.profileId ?? latestSession.publishedProfileId;
if (profileId) {
const { item } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(item);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(item));
}
}
await refreshMatch3DShelf().catch(() => undefined);
@@ -5203,12 +5506,10 @@ export function PlatformEntryFlowShellImpl({
setBabyObjectMatchGenerationPhase('ready');
setBabyObjectMatchGenerationState((current) =>
current
? {
...current,
phase: 'ready',
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount: response.draft.itemAssets.length,
totalAssetCount: response.draft.itemAssets.length,
}
})
: current,
);
const openResult =
@@ -5230,11 +5531,9 @@ export function PlatformEntryFlowShellImpl({
setBabyObjectMatchError(errorMessage);
setBabyObjectMatchGenerationState((current) =>
current
? {
...current,
phase: 'failed',
? resolveFinishedMiniGameDraftGenerationState(current, 'failed', {
error: errorMessage,
}
})
: current,
);
} finally {
@@ -6702,13 +7001,13 @@ export function PlatformEntryFlowShellImpl({
) {
try {
const { item } = await getMatch3DWorkDetail(profile.profileId);
runtimeProfile = {
runtimeProfile = normalizeMatch3DWorkForRuntimeUi({
...item,
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
item.generatedItemAssets,
profile.generatedItemAssets,
),
};
});
} catch {
// 中文注释:详情补读只为拿完整生成素材;失败时继续按摘要开局,避免推荐流卡死。
}
@@ -7777,7 +8076,7 @@ export function PlatformEntryFlowShellImpl({
void deleteMatch3DWork(work.profileId)
.then((response) => {
markDraftNoticeSeen(noticeKeys);
setMatch3DWorks(response.items);
setMatch3DWorks(mapMatch3DWorksForRuntimeUi(response.items));
void refreshMatch3DGallery();
})
.catch((error) => {
@@ -8632,7 +8931,12 @@ export function PlatformEntryFlowShellImpl({
);
puzzleFlow.setSession(latestSession);
setPuzzleFormDraftPayload(buildPuzzleFormPayloadFromSession(latestSession));
setPuzzleGenerationState(createMiniGameDraftGenerationState('puzzle'));
setPuzzleGenerationState(
createMiniGameDraftGenerationStateFromStartedAt(
'puzzle',
parseDraftGenerationStartedAtMs(item.updatedAt),
),
);
enterCreateTab();
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = item.sourceSessionId;
@@ -8734,13 +9038,14 @@ export function PlatformEntryFlowShellImpl({
setMatch3DFormDraftPayload(null);
const profileId = latestSession.draft?.profileId ?? item.profileId;
const { item: profile } = await getMatch3DWorkDetail(profileId);
const normalizedProfile = normalizeMatch3DWorkForRuntimeUi(profile);
match3dFlow.setIsBusy(false);
const started = await startMatch3DRunFromProfile(
profile,
normalizedProfile,
'match3d-result',
);
if (!started) {
setMatch3DProfile(profile);
setMatch3DProfile(normalizedProfile);
enterCreateTab();
setSelectionStage('match3d-result');
}
@@ -8823,7 +9128,7 @@ export function PlatformEntryFlowShellImpl({
try {
const profileId = restoredSession.draft?.profileId ?? item.profileId;
const { item: profile } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(profile);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(profile));
} catch (error) {
setMatch3DProfile(buildMatch3DProfileFromSession(restoredSession));
setMatch3DError(
@@ -11538,29 +11843,33 @@ export function PlatformEntryFlowShellImpl({
returnToCreationCenterFromGeneration();
}}
onSaved={(profile) => {
setMatch3DProfile(profile);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(profile));
}}
onPublished={(profile) => {
setMatch3DProfile(profile);
const normalizedProfile =
normalizeMatch3DWorkForRuntimeUi(profile);
setMatch3DProfile(normalizedProfile);
void Promise.allSettled([
refreshMatch3DShelf(),
refreshMatch3DGallery(),
]);
openPublicWorkDetail(
mapMatch3DWorkToPublicWorkDetail(profile),
mapMatch3DWorkToPublicWorkDetail(normalizedProfile),
);
openPublishShareModal({
title: profile.gameName,
title: normalizedProfile.gameName,
publicWorkCode: buildMatch3DPublicWorkCode(
profile.profileId,
normalizedProfile.profileId,
),
stage: 'work-detail',
});
}}
onStartTestRun={(profile, options) => {
setMatch3DProfile(profile);
const normalizedProfile =
normalizeMatch3DWorkForRuntimeUi(profile);
setMatch3DProfile(normalizedProfile);
void startMatch3DRunFromProfile(
profile,
normalizedProfile,
'match3d-result',
false,
options,