扩展外部生成Worker队列
新增外部生成队列概览和单任务状态契约 将跳一跳、拼消消、敲木鱼图片生成动作接入worker队列 前端生成等待页展示当前任务和队列数量 更新外部生成worker运维文档和团队决策记录
This commit is contained in:
@@ -31,8 +31,16 @@ interface CustomWorldGenerationViewProps {
|
||||
idleBadgeLabel?: string;
|
||||
structuredEmptyText?: string;
|
||||
hideBatchModule?: boolean;
|
||||
queueStatus?: ExternalGenerationQueueStatus | null;
|
||||
}
|
||||
|
||||
export type ExternalGenerationQueueStatus = {
|
||||
currentStatus?: 'queued' | 'running' | 'completed' | 'failed' | null;
|
||||
currentProgress?: number | null;
|
||||
pendingCount?: number | null;
|
||||
runningCount?: number | null;
|
||||
};
|
||||
|
||||
function formatDuration(ms: number) {
|
||||
const safeMs = Math.max(0, Math.round(ms));
|
||||
const totalSeconds = Math.ceil(safeMs / 1000);
|
||||
@@ -85,6 +93,49 @@ function getStepStatusLabel(step: { status: string }) {
|
||||
return '待处理';
|
||||
}
|
||||
|
||||
function resolveQueueStatusLabel(
|
||||
status: ExternalGenerationQueueStatus['currentStatus'],
|
||||
) {
|
||||
if (status === 'queued') {
|
||||
return '排队中';
|
||||
}
|
||||
|
||||
if (status === 'running') {
|
||||
return '生成中';
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
return '生成失败';
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
return '已完成';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasQueueStatus(status: ExternalGenerationQueueStatus | null | undefined) {
|
||||
return Boolean(
|
||||
status &&
|
||||
(status.currentStatus ||
|
||||
typeof status.pendingCount === 'number' ||
|
||||
typeof status.runningCount === 'number'),
|
||||
);
|
||||
}
|
||||
|
||||
function formatQueueCount(value: number | null | undefined) {
|
||||
return Math.max(0, Math.round(value ?? 0)).toString();
|
||||
}
|
||||
|
||||
function formatQueueProgress(value: number | null | undefined) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${Math.max(0, Math.min(100, Math.round(value)))}%`;
|
||||
}
|
||||
|
||||
function resolveCurrentGenerationStep(
|
||||
progress: CustomWorldGenerationProgress | null,
|
||||
) {
|
||||
@@ -111,6 +162,7 @@ export function CustomWorldGenerationView({
|
||||
activeBadgeLabel = '世界建设中',
|
||||
idleBadgeLabel = '等待操作',
|
||||
hideBatchModule = false,
|
||||
queueStatus = null,
|
||||
}: CustomWorldGenerationViewProps) {
|
||||
void hideBatchModule;
|
||||
const progressValue = getProgressPercentage(progress);
|
||||
@@ -131,6 +183,11 @@ export function CustomWorldGenerationView({
|
||||
: '校准中';
|
||||
const elapsedText =
|
||||
progress != null ? formatDuration(progress.elapsedMs) : '启动中';
|
||||
const queueStatusLabel = resolveQueueStatusLabel(
|
||||
queueStatus?.currentStatus ?? null,
|
||||
);
|
||||
const queueProgressText = formatQueueProgress(queueStatus?.currentProgress);
|
||||
const shouldShowQueueStatus = hasQueueStatus(queueStatus);
|
||||
|
||||
return (
|
||||
<div className="relative isolate z-[1] -mx-3 -my-3 flex h-[calc(100%+1.5rem)] min-h-0 flex-col overflow-hidden bg-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-4 text-[#3d1f10] sm:mx-0 sm:my-0 sm:h-full sm:rounded-[2rem] sm:px-5 sm:pt-5">
|
||||
@@ -167,6 +224,21 @@ export function CustomWorldGenerationView({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{shouldShowQueueStatus ? (
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 rounded-[1.25rem] border border-white/70 bg-white/72 px-3 py-2 text-xs font-semibold text-[#6b3a1d] shadow-[0_14px_34px_rgba(121,70,33,0.10)] backdrop-blur-md sm:px-4">
|
||||
{queueStatusLabel ? (
|
||||
<span className="rounded-full bg-[#fff4dc] px-2.5 py-1 text-[#8a4c1e]">
|
||||
{queueProgressText
|
||||
? `${queueStatusLabel} ${queueProgressText}`
|
||||
: queueStatusLabel}
|
||||
</span>
|
||||
) : null}
|
||||
<span>排队 {formatQueueCount(queueStatus?.pendingCount)}</span>
|
||||
<span className="h-1 w-1 rounded-full bg-[#d4a15d]" />
|
||||
<span>生成 {formatQueueCount(queueStatus?.runningCount)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:justify-end">
|
||||
{!isGenerating ? (
|
||||
<PlatformActionButton
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -70,4 +70,28 @@ describe('UnifiedGenerationPage', () => {
|
||||
expect(screen.queryByText('当前跳一跳信息')).toBeNull();
|
||||
expect(screen.queryByText('云端糖果塔')).toBeNull();
|
||||
});
|
||||
|
||||
test('显示外部生成队列状态', () => {
|
||||
render(
|
||||
<UnifiedGenerationPage
|
||||
playId="puzzle"
|
||||
settingText="一只发光的纸船"
|
||||
progress={createProgress()}
|
||||
isGenerating
|
||||
queueStatus={{
|
||||
currentStatus: 'queued',
|
||||
currentProgress: 18,
|
||||
pendingCount: 6,
|
||||
runningCount: 2,
|
||||
}}
|
||||
onBack={() => {}}
|
||||
onEditSetting={() => {}}
|
||||
onRetry={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('排队中 18%')).toBeTruthy();
|
||||
expect(screen.getByText('排队 6')).toBeTruthy();
|
||||
expect(screen.getByText('生成 2')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { CustomWorldGenerationProgress } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { CustomWorldStructuredAnchorEntry } from '../../services/customWorldAgentGenerationProgress';
|
||||
import { CustomWorldGenerationView } from '../CustomWorldGenerationView';
|
||||
import {
|
||||
CustomWorldGenerationView,
|
||||
type ExternalGenerationQueueStatus,
|
||||
} from '../CustomWorldGenerationView';
|
||||
import type { UnifiedGenerationPlayId } from './unifiedGenerationCopy';
|
||||
import { getUnifiedGenerationCopy } from './unifiedGenerationCopy';
|
||||
|
||||
@@ -15,6 +18,7 @@ type UnifiedGenerationPageProps = {
|
||||
onEditSetting: () => void;
|
||||
onRetry: () => void;
|
||||
hideBatchModule?: boolean;
|
||||
queueStatus?: ExternalGenerationQueueStatus | null;
|
||||
};
|
||||
|
||||
export function UnifiedGenerationPage({
|
||||
@@ -28,6 +32,7 @@ export function UnifiedGenerationPage({
|
||||
onEditSetting,
|
||||
onRetry,
|
||||
hideBatchModule = false,
|
||||
queueStatus = null,
|
||||
}: UnifiedGenerationPageProps) {
|
||||
const copy = getUnifiedGenerationCopy(playId);
|
||||
|
||||
@@ -51,6 +56,7 @@ export function UnifiedGenerationPage({
|
||||
pausedBadgeLabel="素材生成已暂停"
|
||||
idleBadgeLabel="等待返回工作区"
|
||||
hideBatchModule={hideBatchModule}
|
||||
queueStatus={queueStatus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
49
src/services/external-generation/externalGenerationClient.ts
Normal file
49
src/services/external-generation/externalGenerationClient.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
ExternalGenerationJobStatusResponse,
|
||||
ExternalGenerationQueueOverviewResponse,
|
||||
} from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import { BACKGROUND_AUTH_REQUEST_OPTIONS, requestJson } from '../apiClient';
|
||||
|
||||
const EXTERNAL_GENERATION_API_BASE = '/api/runtime/external-generation';
|
||||
const EXTERNAL_GENERATION_READ_OPTIONS = {
|
||||
...BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||
retry: {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 200,
|
||||
maxDelayMs: 600,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export async function getExternalGenerationQueueOverview(
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return requestJson<ExternalGenerationQueueOverviewResponse>(
|
||||
`${EXTERNAL_GENERATION_API_BASE}/queue-overview`,
|
||||
{
|
||||
method: 'GET',
|
||||
signal,
|
||||
},
|
||||
'读取生成队列状态失败',
|
||||
EXTERNAL_GENERATION_READ_OPTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getExternalGenerationJobStatus(
|
||||
jobId: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return requestJson<ExternalGenerationJobStatusResponse>(
|
||||
`${EXTERNAL_GENERATION_API_BASE}/jobs/${encodeURIComponent(jobId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
signal,
|
||||
},
|
||||
'读取生成任务状态失败',
|
||||
EXTERNAL_GENERATION_READ_OPTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
export const externalGenerationClient = {
|
||||
getQueueOverview: getExternalGenerationQueueOverview,
|
||||
getJobStatus: getExternalGenerationJobStatus,
|
||||
};
|
||||
5
src/services/external-generation/index.ts
Normal file
5
src/services/external-generation/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
externalGenerationClient,
|
||||
getExternalGenerationJobStatus,
|
||||
getExternalGenerationQueueOverview,
|
||||
} from './externalGenerationClient';
|
||||
Reference in New Issue
Block a user