Puzzle: support history images & partial generation

Allow history-generated image paths to be submitted where Data URLs were previously required and avoid treating partial/result-page generations as blocking the whole draft. Backend: resolve history /generated-* references via resolve_puzzle_reference_image_as_data_url and convert to PuzzleDownloadedImage; add PuzzleDownloadedImage::from_resolved_reference_image; extend draft handling to apply generated level metadata (auto-naming) and normalize generation_status to treat levels with images as ready. API: add shouldAutoNameLevel to action contracts and use it to request/refine generated level names. Spacetime/module and mappers: normalize completed level statuses when saving/reading so result-page background or per-level generation doesn't mask completed drafts. Frontend: expose resolver helpers, only mark a work as generating when no usable cover or ready level exists, keep level controls enabled during UI-background regeneration, and add tests covering history-image submission, auto-naming, and UI-background/partial-generation behaviors.
This commit is contained in:
2026-05-19 10:02:13 +08:00
parent 5e03b3d2f2
commit 7b37271f17
16 changed files with 653 additions and 73 deletions

View File

@@ -641,7 +641,7 @@ function isCreationTypeReferenceCoverImageSrc(value?: string | null) {
);
}
function resolvePuzzleWorkCoverImageSrc(item: PuzzleWorkSummary) {
export function resolvePuzzleWorkCoverImageSrc(item: PuzzleWorkSummary) {
const directCoverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
if (
directCoverImageSrc &&
@@ -651,33 +651,44 @@ function resolvePuzzleWorkCoverImageSrc(item: PuzzleWorkSummary) {
}
for (const level of item.levels ?? []) {
const levelCoverImageSrc = normalizeCoverImageSrc(level.coverImageSrc);
if (
levelCoverImageSrc &&
!isCreationTypeReferenceCoverImageSrc(levelCoverImageSrc)
) {
return levelCoverImageSrc;
const levelImageSrc = resolvePuzzleLevelCoverImageSrc(level);
if (levelImageSrc) {
return levelImageSrc;
}
}
const selectedCandidateImageSrc =
level.selectedCandidateId && level.candidates.length > 0
? normalizeCoverImageSrc(
level.candidates.find(
(candidate) => candidate.candidateId === level.selectedCandidateId,
)?.imageSrc,
return null;
}
export function resolvePuzzleLevelCoverImageSrc(
level: NonNullable<PuzzleWorkSummary['levels']>[number],
) {
const levelCoverImageSrc = normalizeCoverImageSrc(level.coverImageSrc);
if (
levelCoverImageSrc &&
!isCreationTypeReferenceCoverImageSrc(levelCoverImageSrc)
) {
return levelCoverImageSrc;
}
const selectedCandidateImageSrc =
level.selectedCandidateId && level.candidates.length > 0
? normalizeCoverImageSrc(
level.candidates.find(
(candidate) => candidate.candidateId === level.selectedCandidateId,
)?.imageSrc,
)
: null;
const fallbackCandidateImageSrc = normalizeCoverImageSrc(
level.candidates[level.candidates.length - 1]?.imageSrc,
);
const candidateImageSrc = selectedCandidateImageSrc || fallbackCandidateImageSrc;
: null;
const fallbackCandidateImageSrc = normalizeCoverImageSrc(
level.candidates[level.candidates.length - 1]?.imageSrc,
);
const candidateImageSrc = selectedCandidateImageSrc || fallbackCandidateImageSrc;
if (
candidateImageSrc &&
!isCreationTypeReferenceCoverImageSrc(candidateImageSrc)
) {
return candidateImageSrc;
}
if (
candidateImageSrc &&
!isCreationTypeReferenceCoverImageSrc(candidateImageSrc)
) {
return candidateImageSrc;
}
return null;
@@ -804,12 +815,26 @@ function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) {
case 'match3d':
return item.source.item.generationStatus === 'generating';
case 'puzzle':
return item.source.item.generationStatus === 'generating';
return isPersistedPuzzleDraftGenerating(item.source.item);
default:
return false;
}
}
export function isPersistedPuzzleDraftGenerating(item: PuzzleWorkSummary) {
if (item.generationStatus !== 'generating') {
return false;
}
const hasUsableCover = Boolean(resolvePuzzleWorkCoverImageSrc(item));
const hasReadyLevel = (item.levels ?? []).some((level) =>
Boolean(resolvePuzzleLevelCoverImageSrc(level)),
);
// 中文注释:作品架“生成中”只表示初始草稿还没有可查看结果;结果页追加关卡或重绘局部图片不能锁住整张草稿卡。
return !hasUsableCover && !hasReadyLevel;
}
function buildRpgWorkShelfActions(
item: CustomWorldWorkSummary,
adapter: RpgWorkShelfAdapter,