扩展外部生成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

@@ -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

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,
};
}

View File

@@ -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();
});
});

View File

@@ -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}
/>
);
}

View 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,
};

View File

@@ -0,0 +1,5 @@
export {
externalGenerationClient,
getExternalGenerationJobStatus,
getExternalGenerationQueueOverview,
} from './externalGenerationClient';