扩展外部生成Worker队列

新增外部生成队列概览和单任务状态契约

将跳一跳、拼消消、敲木鱼图片生成动作接入worker队列

前端生成等待页展示当前任务和队列数量

更新外部生成worker运维文档和团队决策记录
This commit is contained in:
2026-06-12 23:15:55 +08:00
parent 3bccfd1a83
commit 951caac32d
43 changed files with 1913 additions and 67 deletions

View File

@@ -1,10 +1,11 @@
import { describe, expect, test } from 'vitest';
import {
resolveMiniGameGenerationViewBusy,
resolveMiniGameGenerationProgressTickState,
} from './PlatformEntryFlowShellImpl';
import { createMiniGameDraftGenerationState } from '../../services/miniGameDraftGenerationProgress';
import {
resolveMiniGameGenerationProgressTickState,
resolveMiniGameGenerationViewBusy,
} from './PlatformEntryFlowShellImpl';
import { buildExternalGenerationQueueStatus } from './platformExternalGenerationQueueStatusModel';
import { resolveFinishedMiniGameDraftGenerationState } from './platformMiniGameDraftGenerationStateModel';
describe('resolveMiniGameGenerationProgressTickState', () => {
@@ -57,3 +58,34 @@ describe('resolveMiniGameGenerationViewBusy', () => {
);
});
});
describe('buildExternalGenerationQueueStatus', () => {
test('合并队列概览和当前任务状态', () => {
expect(
buildExternalGenerationQueueStatus(
{
pendingCount: 7,
runningCount: 3,
updatedAtMicros: 1_781_222_400_000_000,
},
{
operationId: 'extgen-1',
status: 'running',
phaseLabel: '正在生成。',
phaseDetail: '正在生成。',
progress: 35,
updatedAtMicros: 1_781_222_400_000_000,
},
),
).toEqual({
currentStatus: 'running',
currentProgress: 35,
pendingCount: 7,
runningCount: 3,
});
});
test('没有队列或任务信息时不显示状态条', () => {
expect(buildExternalGenerationQueueStatus(null, null)).toBeNull();
});
});

View File

@@ -37,6 +37,10 @@ import type {
BabyObjectMatchDraft,
CreateBabyObjectMatchDraftRequest,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
ExternalGenerationJobStatusRecord,
ExternalGenerationQueueOverview,
} from '../../../packages/shared/src/contracts/externalGeneration';
import type {
JumpHopJumpRequest,
JumpHopWorkSummaryResponse,
@@ -172,6 +176,7 @@ import {
streamCreativeAgentMessage,
streamCreativeDraftEdit,
} from '../../services/creative-agent';
import { getExternalGenerationQueueOverview } from '../../services/external-generation';
import {
readCustomWorldAgentUiState,
shouldRestoreCustomWorldAgentUiState,
@@ -454,6 +459,7 @@ import {
resolveWoodenFishCreationUrlRestoreStage,
} from './platformCreationUrlStateModel';
import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow';
import { buildExternalGenerationQueueStatus } from './platformExternalGenerationQueueStatusModel';
import {
buildPlatformErrorDialogDismissKey,
buildPlatformTaskCompletionDialogDismissKey,
@@ -717,6 +723,20 @@ export function resolveMiniGameGenerationViewBusy(
return isBusy || isMiniGameDraftGenerating(state ?? null);
}
function isExternalGenerationQueueStage(selectionStage: SelectionStage) {
return (
selectionStage === 'puzzle-generating' ||
selectionStage === 'big-fish-generating' ||
selectionStage === 'square-hole-generating' ||
selectionStage === 'match3d-generating' ||
selectionStage === 'baby-object-match-generating' ||
selectionStage === 'jump-hop-generating' ||
selectionStage === 'puzzle-clear-generating' ||
selectionStage === 'wooden-fish-generating' ||
selectionStage === 'visual-novel-generating'
);
}
type PuzzleDetailReturnTarget = {
tab: PlatformHomeTab;
};
@@ -1747,9 +1767,17 @@ export function PlatformEntryFlowShellImpl({
const [isStartingRecommendEntry, setIsStartingRecommendEntry] =
useState(false);
const recommendRuntimeStartRequestRef = useRef(0);
const [, setPuzzleOperation] = useState<PuzzleAgentOperationRecord | null>(
const [puzzleOperation, setPuzzleOperation] = useState<PuzzleAgentOperationRecord | null>(
null,
);
const [externalGenerationQueueOverview, setExternalGenerationQueueOverview] =
useState<ExternalGenerationQueueOverview | null>(null);
const [jumpHopQueueState, setJumpHopQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [puzzleClearQueueState, setPuzzleClearQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [woodenFishQueueState, setWoodenFishQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [puzzleWorks, setPuzzleWorks] = useState<PuzzleWorkSummary[]>([]);
const [puzzleGalleryEntries, setPuzzleGalleryEntries] = useState<
PuzzleWorkSummary[]
@@ -4753,6 +4781,82 @@ export function PlatformEntryFlowShellImpl({
isWoodenFishBusy,
woodenFishGenerationState,
);
const shouldShowExternalGenerationQueueStatus =
isExternalGenerationQueueStage(selectionStage);
useEffect(() => {
if (!shouldShowExternalGenerationQueueStatus) {
setExternalGenerationQueueOverview(null);
return;
}
let disposed = false;
let controller: AbortController | null = null;
const refreshQueueOverview = () => {
controller?.abort();
controller = new AbortController();
getExternalGenerationQueueOverview(controller.signal)
.then((response) => {
if (!disposed) {
setExternalGenerationQueueOverview(response.overview);
}
})
.catch(() => {
if (!disposed) {
setExternalGenerationQueueOverview(null);
}
});
};
refreshQueueOverview();
const intervalId = window.setInterval(refreshQueueOverview, 4000);
return () => {
disposed = true;
controller?.abort();
window.clearInterval(intervalId);
};
}, [shouldShowExternalGenerationQueueStatus]);
const puzzleExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
puzzleOperation?.queueState ?? null,
),
[externalGenerationQueueOverview, puzzleOperation],
);
const jumpHopExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
jumpHopQueueState,
),
[externalGenerationQueueOverview, jumpHopQueueState],
);
const puzzleClearExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
puzzleClearQueueState,
),
[externalGenerationQueueOverview, puzzleClearQueueState],
);
const woodenFishExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
woodenFishQueueState,
),
[externalGenerationQueueOverview, woodenFishQueueState],
);
const externalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
null,
),
[externalGenerationQueueOverview],
);
const platformBootstrapErrorForDisplay = isCreationEntryDisabledErrorMessage(
platformBootstrap.platformError,
)
@@ -7469,6 +7573,7 @@ export function PlatformEntryFlowShellImpl({
setJumpHopRun(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopGenerationState(generationState);
setJumpHopQueueState(null);
setIsJumpHopBusy(true);
setSelectionStage('jump-hop-generating');
markDraftGenerating('jump-hop', [
@@ -7485,6 +7590,19 @@ export function PlatformEntryFlowShellImpl({
draft: created.session.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setJumpHopQueueState(response.queueState);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? null);
writeCreationUrlState(
buildJumpHopCreationUrlState({
session: response.session,
work: response.work,
}),
);
return;
}
setJumpHopQueueState(null);
const readyState = createReadyJumpHopGenerationState(generationState);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? null);
@@ -7521,6 +7639,7 @@ export function PlatformEntryFlowShellImpl({
'生成跳一跳草稿失败。',
);
setJumpHopError(errorMessage);
setJumpHopQueueState(null);
setJumpHopGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
@@ -7590,6 +7709,7 @@ export function PlatformEntryFlowShellImpl({
const generationState = createMiniGameDraftGenerationState('jump-hop');
setJumpHopError(null);
setJumpHopGenerationState(generationState);
setJumpHopQueueState(null);
setIsJumpHopBusy(true);
setSelectionStage('jump-hop-generating');
try {
@@ -7599,6 +7719,19 @@ export function PlatformEntryFlowShellImpl({
draft: jumpHopSession.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setJumpHopQueueState(response.queueState);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? jumpHopWork);
writeCreationUrlState(
buildJumpHopCreationUrlState({
session: response.session,
work: response.work ?? jumpHopWork,
}),
);
return;
}
setJumpHopQueueState(null);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? jumpHopWork);
writeCreationUrlState(
@@ -7617,6 +7750,7 @@ export function PlatformEntryFlowShellImpl({
'重新生成跳一跳地块失败。',
);
setJumpHopError(errorMessage);
setJumpHopQueueState(null);
setJumpHopGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
@@ -7918,6 +8052,7 @@ export function PlatformEntryFlowShellImpl({
setPuzzleClearWork(null);
setPuzzleClearRun(null);
setPuzzleClearGenerationState(generationState);
setPuzzleClearQueueState(null);
setIsPuzzleClearBusy(true);
markDraftGenerating('puzzle-clear', [created.session.sessionId]);
markPendingDraftGenerating('puzzle-clear', created.session.sessionId);
@@ -7946,6 +8081,19 @@ export function PlatformEntryFlowShellImpl({
created.session.draft?.boardBackgroundAsset,
},
);
if (response.queueState && response.session.status === 'generating') {
setPuzzleClearQueueState(response.queueState);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? null);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: response.session,
work: response.work,
}),
);
return;
}
setPuzzleClearQueueState(null);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? null);
writeCreationUrlState(
@@ -7990,6 +8138,7 @@ export function PlatformEntryFlowShellImpl({
'生成拼消消草稿失败。',
);
setPuzzleClearError(errorMessage);
setPuzzleClearQueueState(null);
setPuzzleClearGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
@@ -8071,6 +8220,7 @@ export function PlatformEntryFlowShellImpl({
const generationState = createMiniGameDraftGenerationState('puzzle-clear');
setPuzzleClearError(null);
setPuzzleClearGenerationState(generationState);
setPuzzleClearQueueState(null);
setIsPuzzleClearBusy(true);
selectionStageRef.current = 'puzzle-clear-generating';
setSelectionStage('puzzle-clear-generating');
@@ -8092,6 +8242,19 @@ export function PlatformEntryFlowShellImpl({
boardBackgroundAsset: puzzleClearSession.draft?.boardBackgroundAsset,
},
);
if (response.queueState && response.session.status === 'generating') {
setPuzzleClearQueueState(response.queueState);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? puzzleClearWork);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: response.session,
work: response.work ?? puzzleClearWork,
}),
);
return;
}
setPuzzleClearQueueState(null);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? puzzleClearWork);
writeCreationUrlState(
@@ -8110,6 +8273,7 @@ export function PlatformEntryFlowShellImpl({
'重新生成拼消消图集失败。',
);
setPuzzleClearError(errorMessage);
setPuzzleClearQueueState(null);
setPuzzleClearGenerationState(
resolveFinishedMiniGameDraftGenerationState(generationState, 'failed', {
error: errorMessage,
@@ -8420,6 +8584,7 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishWork(null);
setWoodenFishRun(null);
setWoodenFishGenerationState(generationState);
setWoodenFishQueueState(null);
setIsWoodenFishBusy(true);
setSelectionStage('wooden-fish-generating');
markDraftGenerating('wooden-fish', [created.session.sessionId]);
@@ -8439,6 +8604,19 @@ export function PlatformEntryFlowShellImpl({
draft: created.session.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setWoodenFishQueueState(response.queueState);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? null);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work,
}),
);
return;
}
setWoodenFishQueueState(null);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? null);
writeCreationUrlState(
@@ -8483,6 +8661,7 @@ export function PlatformEntryFlowShellImpl({
'生成敲木鱼草稿失败。',
);
setWoodenFishError(errorMessage);
setWoodenFishQueueState(null);
setWoodenFishGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
@@ -8568,6 +8747,7 @@ export function PlatformEntryFlowShellImpl({
);
setWoodenFishError(null);
setWoodenFishGenerationState(generationState);
setWoodenFishQueueState(null);
setIsWoodenFishBusy(true);
setSelectionStage('wooden-fish-generating');
try {
@@ -8577,6 +8757,19 @@ export function PlatformEntryFlowShellImpl({
draft: woodenFishSession.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setWoodenFishQueueState(response.queueState);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? woodenFishWork);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work ?? woodenFishWork,
}),
);
return;
}
setWoodenFishQueueState(null);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? woodenFishWork);
writeCreationUrlState(
@@ -8595,6 +8788,7 @@ export function PlatformEntryFlowShellImpl({
'重新生成敲击物图案失败。',
);
setWoodenFishError(errorMessage);
setWoodenFishQueueState(null);
setWoodenFishGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
@@ -15382,6 +15576,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={jumpHopExternalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -15526,6 +15721,7 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('match3d-agent-workspace');
}}
onRetry={retryMatch3DDraftGeneration}
queueStatus={puzzleClearExternalGenerationQueueStatus}
hideBatchModule
/>
</Suspense>
@@ -15796,6 +15992,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={woodenFishExternalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -15996,6 +16193,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="图片生成中"
pausedBadgeLabel="图片生成已暂停"
idleBadgeLabel="等待返回结果页"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16200,6 +16398,7 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('jump-hop-workspace');
}}
onRetry={retryJumpHopDraftGeneration}
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16348,6 +16547,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="素材生成中"
pausedBadgeLabel="素材生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16477,6 +16677,7 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('wooden-fish-workspace');
}}
onRetry={retryWoodenFishDraftGeneration}
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16670,6 +16871,7 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('puzzle-agent-workspace');
}}
onRetry={retryPuzzleDraftGeneration}
queueStatus={puzzleExternalGenerationQueueStatus}
hideBatchModule
/>
</Suspense>
@@ -16796,6 +16998,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -17039,6 +17242,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿编译中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>

View File

@@ -0,0 +1,21 @@
import type {
ExternalGenerationJobStatusRecord,
ExternalGenerationQueueOverview,
} from '../../../packages/shared/src/contracts/externalGeneration';
import type { ExternalGenerationQueueStatus } from '../CustomWorldGenerationView';
export function buildExternalGenerationQueueStatus(
overview: ExternalGenerationQueueOverview | null,
job: ExternalGenerationJobStatusRecord | null,
): ExternalGenerationQueueStatus | null {
if (!overview && !job) {
return null;
}
return {
currentStatus: job?.status ?? null,
currentProgress: job?.progress ?? null,
pendingCount: overview?.pendingCount ?? null,
runningCount: overview?.runningCount ?? null,
};
}