升级外部生成为正式任务列表

将 external_generation_job 扩展为含价格、确认时间、退款流水和事件审计的正式生成任务事实源

新增外部生成任务列表与确认 BFF,并让平台入口按后端任务补弹完成或失败提示

保护画布生成队列超时、快照回写和自动保存覆盖场景

将 worker 计费退款流水写入 externalGenerationJobId,并补充冷备份后 worker/controller 恢复守护
This commit is contained in:
2026-06-24 15:17:12 +08:00
parent ed6b17442a
commit 73a17193d4
66 changed files with 2416 additions and 122 deletions
@@ -41,7 +41,7 @@ import type {
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
ExternalGenerationJobStatusRecord,
ExternalGenerationQueueOverview,
ExternalGenerationTaskRecord,
} from '../../../packages/shared/src/contracts/externalGeneration';
import type {
JumpHopJumpRequest,
@@ -193,7 +193,10 @@ import {
regenerateBabyObjectMatchDraftAssets,
saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import { getExternalGenerationQueueOverview } from '../../services/external-generation';
import {
acknowledgeExternalGenerationTasks,
listExternalGenerationTasks,
} from '../../services/external-generation';
import {
jumpHopClient,
type JumpHopGalleryCardResponse,
@@ -720,6 +723,54 @@ function isExternalGenerationQueueStage(selectionStage: SelectionStage) {
);
}
function isUnacknowledgedExternalGenerationTerminalTask(
task: ExternalGenerationTaskRecord,
) {
return (
(task.status === 'completed' || task.status === 'failed') &&
!task.notificationAcknowledgedAt
);
}
function resolveExternalGenerationTaskCompletionDialog(
tasks: readonly ExternalGenerationTaskRecord[],
): PlatformTaskCompletionDialogState | null {
const terminalTasks = tasks.filter(
isUnacknowledgedExternalGenerationTerminalTask,
);
if (terminalTasks.length === 0) {
return null;
}
const jobIds = terminalTasks.map((task) => task.jobId);
const latestUpdatedAtMicros = terminalTasks.reduce(
(latest, task) => Math.max(latest, task.updatedAtMicros),
0,
);
const completedAtMs =
latestUpdatedAtMicros > 0
? Math.floor(latestUpdatedAtMicros / 1000)
: Date.now();
return {
key: `external-generation:${jobIds.join('|')}:${latestUpdatedAtMicros}`,
source: '生成任务',
message:
terminalTasks.length > 1
? `${terminalTasks.length} 个生成任务已结束。`
: '生成任务已结束。',
completedAtMs,
taskJobIds: jobIds,
items: terminalTasks.map((task) => ({
source: task.requestLabel.trim() || task.jobKind || task.jobId,
message:
task.status === 'failed'
? (task.error?.trim() || '生成失败。')
: '生成已完成。',
})),
};
}
type PuzzleDetailReturnTarget = {
tab: PlatformHomeTab;
};
@@ -1839,8 +1890,16 @@ export function PlatformEntryFlowShellImpl({
const [puzzleOperation, setPuzzleOperation] = useState<PuzzleAgentOperationRecord | null>(
null,
);
const [externalGenerationQueueOverview, setExternalGenerationQueueOverview] =
useState<ExternalGenerationQueueOverview | null>(null);
const [externalGenerationTasks, setExternalGenerationTasks] = useState<
ExternalGenerationTaskRecord[]
>([]);
const [externalGenerationTaskOverview, setExternalGenerationTaskOverview] =
useState<{
pendingCount: number;
runningCount: number;
unacknowledgedTerminalCount: number;
updatedAtMicros: number;
} | null>(null);
const [jumpHopQueueState, setJumpHopQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [puzzleClearQueueState, setPuzzleClearQueueState] =
@@ -4922,18 +4981,21 @@ export function PlatformEntryFlowShellImpl({
platformBootstrap.platformTab === 'profile' &&
platformBootstrap.canReadProtectedData);
const isHostNetworkOnline = useHostNetworkOnline();
const shouldPollExternalGenerationTasks =
platformBootstrap.canReadProtectedData && Boolean(authUi?.user?.id);
const externalGenerationQueueOwnerKey =
shouldShowExternalGenerationQueueStatus
shouldPollExternalGenerationTasks
? (authUi?.user?.id ?? 'anonymous')
: null;
useEffect(() => {
setExternalGenerationQueueOverview(null);
setExternalGenerationTasks([]);
setExternalGenerationTaskOverview(null);
}, [externalGenerationQueueOwnerKey]);
useEffect(() => {
if (
!shouldShowExternalGenerationQueueStatus ||
!shouldPollExternalGenerationTasks ||
!isHostNetworkOnline
) {
return;
@@ -4942,24 +5004,30 @@ export function PlatformEntryFlowShellImpl({
let disposed = false;
let controller: AbortController | null = null;
const refreshQueueOverview = () => {
const refreshTaskList = () => {
controller?.abort();
controller = new AbortController();
getExternalGenerationQueueOverview(controller.signal)
listExternalGenerationTasks({
limit: 20,
includeAcknowledgedTerminal: false,
signal: controller.signal,
})
.then((response) => {
if (!disposed) {
setExternalGenerationQueueOverview(response.overview);
setExternalGenerationTasks(response.tasks);
setExternalGenerationTaskOverview(response.overview);
}
})
.catch(() => {
if (!disposed) {
setExternalGenerationQueueOverview(null);
setExternalGenerationTasks([]);
setExternalGenerationTaskOverview(null);
}
});
};
refreshQueueOverview();
const intervalId = window.setInterval(refreshQueueOverview, 4000);
refreshTaskList();
const intervalId = window.setInterval(refreshTaskList, 4000);
return () => {
disposed = true;
@@ -4969,7 +5037,7 @@ export function PlatformEntryFlowShellImpl({
}, [
externalGenerationQueueOwnerKey,
isHostNetworkOnline,
shouldShowExternalGenerationQueueStatus,
shouldPollExternalGenerationTasks,
]);
const activeExternalGenerationJobState = useMemo(() => {
const candidates = [
@@ -4998,10 +5066,15 @@ export function PlatformEntryFlowShellImpl({
const externalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
externalGenerationTaskOverview,
activeExternalGenerationJobState,
externalGenerationTasks,
),
[activeExternalGenerationJobState, externalGenerationQueueOverview],
[
activeExternalGenerationJobState,
externalGenerationTaskOverview,
externalGenerationTasks,
],
);
const profileExternalGenerationQueueStatus =
platformBootstrap.canReadProtectedData &&
@@ -5235,13 +5308,24 @@ export function PlatformEntryFlowShellImpl({
woodenFishRun?.runId,
woodenFishSession?.sessionId,
]);
const externalGenerationTaskCompletionDialog =
useMemo<PlatformTaskCompletionDialogState | null>(
() => resolveExternalGenerationTaskCompletionDialog(externalGenerationTasks),
[externalGenerationTasks],
);
const currentPlatformTaskCompletionDialog =
useMemo<PlatformTaskCompletionDialogState | null>(
() => pendingPlatformTaskCompletionDialog,
[pendingPlatformTaskCompletionDialog],
() =>
pendingPlatformTaskCompletionDialog ??
externalGenerationTaskCompletionDialog,
[
externalGenerationTaskCompletionDialog,
pendingPlatformTaskCompletionDialog,
],
);
const activePlatformTaskCompletionDialog =
selectionStage === 'image-editor'
selectionStage === 'image-editor' &&
!(currentPlatformTaskCompletionDialog?.taskJobIds?.length ?? 0)
? null
: resolveActivePlatformDialog(
currentPlatformTaskCompletionDialog,
@@ -5374,6 +5458,36 @@ export function PlatformEntryFlowShellImpl({
if (dismissKey) {
setDismissedPlatformTaskCompletionDialogKey(dismissKey);
}
const taskJobIds = currentPlatformTaskCompletionDialog.taskJobIds ?? [];
if (taskJobIds.length > 0) {
const acknowledgedJobIds = new Set(taskJobIds);
setExternalGenerationTasks((current) =>
current.filter((task) => !acknowledgedJobIds.has(task.jobId)),
);
setExternalGenerationTaskOverview((current) =>
current
? {
...current,
unacknowledgedTerminalCount: Math.max(
0,
current.unacknowledgedTerminalCount - acknowledgedJobIds.size,
),
}
: current,
);
void acknowledgeExternalGenerationTasks({ jobIds: taskJobIds })
.then((response) => {
setExternalGenerationTasks((current) =>
current.filter(
(task) =>
!response.acknowledgedTasks.some(
(acknowledgedTask) => acknowledgedTask.jobId === task.jobId,
),
),
);
})
.catch(() => undefined);
}
setPendingPlatformTaskCompletionDialog(null);
}, [currentPlatformTaskCompletionDialog]);
const shouldPollPuzzleGenerationSession =