升级外部生成为正式任务列表
将 external_generation_job 扩展为含价格、确认时间、退款流水和事件审计的正式生成任务事实源 新增外部生成任务列表与确认 BFF,并让平台入口按后端任务补弹完成或失败提示 保护画布生成队列超时、快照回写和自动保存覆盖场景 将 worker 计费退款流水写入 externalGenerationJobId,并补充冷备份后 worker/controller 恢复守护
This commit is contained in:
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user