fix: 收紧拼图草稿恢复判定

This commit is contained in:
2026-06-04 03:34:06 +08:00
parent cd959b4095
commit 46a36222cb
8 changed files with 413 additions and 75 deletions

View File

@@ -571,6 +571,10 @@ import {
resolvePlatformPublicWorkStartIntent,
resolveVisiblePuzzleDetailCoverCount,
} from './platformPublicWorkDetailFlow';
import {
hasRecoverableGeneratedPuzzleDraft,
normalizeRecoveredPuzzleDraftSession,
} from './platformPuzzleDraftRecoveryModel';
import {
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
@@ -1045,77 +1049,6 @@ function openPuzzleRuntimeStage(
writePuzzleRuntimeUrlState(state);
}
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 resolveProfileWalletBalance(
dashboard: { walletBalance?: number | null } | null | undefined,
) {

View File

@@ -0,0 +1,195 @@
import { describe, expect, test } from 'vitest';
import type {
PuzzleAnchorPack,
PuzzleDraftLevel,
PuzzleGeneratedImageCandidate,
PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
import {
hasRecoverableGeneratedPuzzleDraft,
normalizeRecoveredPuzzleDraftSession,
} from './platformPuzzleDraftRecoveryModel';
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 buildCandidate(
overrides: Partial<PuzzleGeneratedImageCandidate> = {},
): PuzzleGeneratedImageCandidate {
return {
candidateId: 'candidate-1',
imageSrc: '/candidate-cover.png',
assetId: 'asset-candidate-cover',
prompt: '星桥机关',
sourceType: 'generated',
selected: true,
...overrides,
};
}
function buildLevel(overrides: Partial<PuzzleDraftLevel> = {}): PuzzleDraftLevel {
return {
levelId: 'level-1',
levelName: '星桥机关',
pictureDescription: '星桥机关画面',
candidates: [buildCandidate()],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'generating',
...overrides,
};
}
function buildDraft(overrides: Partial<PuzzleResultDraft> = {}): PuzzleResultDraft {
const anchorPack = buildAnchorPack();
return {
workTitle: '星桥拼图',
workDescription: '修复星桥机关。',
levelName: '星桥机关',
summary: '把碎片拼回原位。',
themeTags: ['星桥', '机关', '修复'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack,
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'generating',
levels: [buildLevel()],
...overrides,
};
}
function buildSession(
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
): PuzzleAgentSessionSnapshot {
const anchorPack = buildAnchorPack();
return {
sessionId: 'puzzle-session-1',
seedText: '星桥',
currentTurn: 1,
progressPercent: 100,
stage: 'draft_ready',
anchorPack,
draft: buildDraft(),
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
suggestedActions: [],
resultPreview: null,
updatedAt: '2026-06-01T10:00:00.000Z',
...overrides,
};
}
function withCompleteLevelAssets(
overrides: Partial<PuzzleDraftLevel> = {},
): PuzzleDraftLevel {
return buildLevel({
levelSceneImageSrc: '/level-scene.png',
uiSpritesheetImageSrc: '/ui-spritesheet.png',
levelBackgroundImageSrc: '/level-background.png',
...overrides,
});
}
describe('platformPuzzleDraftRecoveryModel', () => {
test('normalizes and marks recovered puzzle draft ready when asset pack is complete', () => {
const normalized = normalizeRecoveredPuzzleDraftSession(
buildSession({
draft: buildDraft({
levels: [withCompleteLevelAssets()],
}),
}),
);
expect(hasRecoverableGeneratedPuzzleDraft(normalized)).toBe(true);
expect(normalized.draft).toMatchObject({
coverImageSrc: '/candidate-cover.png',
coverAssetId: 'asset-candidate-cover',
selectedCandidateId: 'candidate-1',
generationStatus: 'ready',
});
expect(normalized.draft?.levels?.[0]).toMatchObject({
coverImageSrc: '/candidate-cover.png',
coverAssetId: 'asset-candidate-cover',
selectedCandidateId: 'candidate-1',
generationStatus: 'ready',
});
});
test('keeps half-finished draft generating when only cover candidate exists', () => {
const normalized = normalizeRecoveredPuzzleDraftSession(buildSession());
expect(hasRecoverableGeneratedPuzzleDraft(normalized)).toBe(false);
expect(normalized.draft).toMatchObject({
coverImageSrc: '/candidate-cover.png',
generationStatus: 'generating',
});
expect(normalized.draft?.levels?.[0]).toMatchObject({
coverImageSrc: '/candidate-cover.png',
generationStatus: 'generating',
});
});
test('requires level scene, ui spritesheet and level background assets together', () => {
expect(
hasRecoverableGeneratedPuzzleDraft(
buildSession({
draft: buildDraft({
coverImageSrc: '/draft-cover.png',
levels: [
withCompleteLevelAssets({
uiSpritesheetImageSrc: null,
uiSpritesheetImageObjectKey: null,
}),
],
}),
}),
),
).toBe(false);
});
test('accepts object keys as recovered asset references', () => {
expect(
hasRecoverableGeneratedPuzzleDraft(
buildSession({
draft: buildDraft({
coverImageSrc: '/draft-cover.png',
levels: [
buildLevel({
levelSceneImageObjectKey: 'level-scene.png',
uiSpritesheetImageObjectKey: 'ui-spritesheet.png',
levelBackgroundImageObjectKey: 'level-background.png',
}),
],
}),
}),
),
).toBe(true);
});
test('leaves sessions without draft unchanged and unrecoverable', () => {
const session = buildSession({ draft: null });
expect(normalizeRecoveredPuzzleDraftSession(session)).toBe(session);
expect(hasRecoverableGeneratedPuzzleDraft(session)).toBe(false);
});
});

View File

@@ -0,0 +1,154 @@
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
function normalizeRecoveryText(value: string | null | undefined) {
return value?.trim() || null;
}
function hasPuzzleAssetReference(
imageSrc: string | null | undefined,
objectKey: string | null | undefined,
) {
return Boolean(normalizeRecoveryText(imageSrc) || normalizeRecoveryText(objectKey));
}
function resolvePrimaryPuzzleLevel(session: PuzzleAgentSessionSnapshot) {
return session.draft?.levels?.[0] ?? null;
}
function resolvePuzzleRecoveryCandidate(
session: PuzzleAgentSessionSnapshot,
primaryLevel: PuzzleDraftLevel | null,
) {
const draft = session.draft;
if (!draft) {
return null;
}
return (
primaryLevel?.candidates.find((candidate) => candidate.selected) ??
primaryLevel?.candidates[0] ??
draft.candidates.find((candidate) => candidate.selected) ??
draft.candidates[0] ??
null
);
}
function resolvePuzzleRecoveryCoverFields(
session: PuzzleAgentSessionSnapshot,
) {
const draft = session.draft;
const primaryLevel = resolvePrimaryPuzzleLevel(session);
const selectedCandidate = resolvePuzzleRecoveryCandidate(
session,
primaryLevel,
);
return {
coverImageSrc:
normalizeRecoveryText(draft?.coverImageSrc) ??
normalizeRecoveryText(primaryLevel?.coverImageSrc) ??
normalizeRecoveryText(selectedCandidate?.imageSrc),
coverAssetId:
normalizeRecoveryText(draft?.coverAssetId) ??
normalizeRecoveryText(primaryLevel?.coverAssetId) ??
normalizeRecoveryText(selectedCandidate?.assetId),
selectedCandidateId:
draft?.selectedCandidateId ??
primaryLevel?.selectedCandidateId ??
selectedCandidate?.candidateId ??
null,
};
}
function hasCompleteGeneratedPuzzleLevelAssets(
level: PuzzleDraftLevel | null,
coverImageSrc: string | null,
) {
return Boolean(
normalizeRecoveryText(coverImageSrc) &&
hasPuzzleAssetReference(
level?.levelSceneImageSrc,
level?.levelSceneImageObjectKey,
) &&
hasPuzzleAssetReference(
level?.uiSpritesheetImageSrc,
level?.uiSpritesheetImageObjectKey,
) &&
hasPuzzleAssetReference(
level?.levelBackgroundImageSrc,
level?.levelBackgroundImageObjectKey,
),
);
}
export function hasRecoverableGeneratedPuzzleDraft(
session: PuzzleAgentSessionSnapshot,
) {
const draft = session.draft;
if (!draft) {
return false;
}
const primaryLevel = resolvePrimaryPuzzleLevel(session);
const { coverImageSrc } = resolvePuzzleRecoveryCoverFields(session);
return hasCompleteGeneratedPuzzleLevelAssets(primaryLevel, coverImageSrc);
}
export function normalizeRecoveredPuzzleDraftSession(
session: PuzzleAgentSessionSnapshot,
): PuzzleAgentSessionSnapshot {
const draft = session.draft;
if (!draft) {
return session;
}
const { coverImageSrc, coverAssetId, selectedCandidateId } =
resolvePuzzleRecoveryCoverFields(session);
const nextLevels = draft.levels?.map((level, index) =>
index === 0
? {
...level,
coverImageSrc: normalizeRecoveryText(level.coverImageSrc)
? level.coverImageSrc
: coverImageSrc,
coverAssetId: normalizeRecoveryText(level.coverAssetId)
? level.coverAssetId
: coverAssetId,
selectedCandidateId:
level.selectedCandidateId ?? selectedCandidateId,
}
: level,
);
const nextSession = {
...session,
draft: {
...draft,
coverImageSrc,
coverAssetId,
selectedCandidateId,
levels: nextLevels,
},
} satisfies PuzzleAgentSessionSnapshot;
const isRecoverable = hasRecoverableGeneratedPuzzleDraft(nextSession);
if (!isRecoverable) {
return nextSession;
}
return {
...nextSession,
draft: {
...nextSession.draft,
generationStatus: 'ready',
levels: nextSession.draft.levels?.map((level, index) =>
index === 0
? {
...level,
generationStatus: 'ready',
}
: level,
),
},
};
}