2026-06-02 12:38:01 +08:00
|
|
|
import { aiGenerationClient } from "./aiGenerationClient";
|
2026-06-05 01:00:33 +08:00
|
|
|
import {
|
|
|
|
|
buildLocalTimeoutMessage,
|
|
|
|
|
getTaskTimeoutPolicy,
|
|
|
|
|
isTaskLocallyTimedOut,
|
|
|
|
|
} from "../utils/taskLifecycle";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
export interface TaskProgressEvent {
|
|
|
|
|
taskId: string;
|
|
|
|
|
status: string;
|
|
|
|
|
progress: number;
|
|
|
|
|
resultUrl?: string | null;
|
|
|
|
|
error?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface WaitForTaskOptions {
|
|
|
|
|
onProgress?: (event: TaskProgressEvent) => void;
|
|
|
|
|
abortRef?: { current: boolean };
|
|
|
|
|
timeoutMs?: number;
|
2026-06-05 01:00:33 +08:00
|
|
|
noProgressTimeoutMs?: number;
|
|
|
|
|
startedAt?: number;
|
|
|
|
|
kind?: "image" | "video" | "text";
|
|
|
|
|
model?: string | null;
|
|
|
|
|
operation?: string | null;
|
2026-06-02 12:38:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const POLL_INTERVAL = 3000;
|
|
|
|
|
|
|
|
|
|
export function waitForTask(
|
|
|
|
|
taskId: string,
|
|
|
|
|
options: WaitForTaskOptions = {},
|
|
|
|
|
): Promise<string | null> {
|
2026-06-05 01:00:33 +08:00
|
|
|
const { onProgress, abortRef } = options;
|
|
|
|
|
const timeoutPolicy = getTaskTimeoutPolicy({
|
|
|
|
|
kind: options.kind,
|
|
|
|
|
model: options.model,
|
|
|
|
|
operation: options.operation,
|
|
|
|
|
});
|
|
|
|
|
const timeoutMs = options.timeoutMs ?? timeoutPolicy.maxRuntimeMs;
|
|
|
|
|
const noProgressTimeoutMs = options.noProgressTimeoutMs ?? timeoutPolicy.noProgressTimeoutMs;
|
|
|
|
|
const startedAt = options.startedAt ?? Date.now();
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
let settled = false;
|
|
|
|
|
let cleanup: (() => void) | null = null;
|
|
|
|
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
let sseConnected = false;
|
|
|
|
|
let fallbackTimerId: ReturnType<typeof setTimeout> | null = null;
|
2026-06-05 01:00:33 +08:00
|
|
|
let lastProgress = 0;
|
|
|
|
|
let lastProgressAt = startedAt;
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
const settle = (fn: () => void) => {
|
|
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
|
|
|
if (fallbackTimerId) clearTimeout(fallbackTimerId);
|
|
|
|
|
if (cleanup) cleanup();
|
|
|
|
|
fn();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
timeoutId = setTimeout(
|
2026-06-05 01:00:33 +08:00
|
|
|
() => settle(() => reject(new Error(buildLocalTimeoutMessage(options.kind || "video")))),
|
2026-06-02 12:38:01 +08:00
|
|
|
timeoutMs,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleUpdate = (event: TaskProgressEvent) => {
|
|
|
|
|
if (settled) return;
|
|
|
|
|
if (abortRef?.current) {
|
|
|
|
|
settle(() => resolve(null));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-05 01:00:33 +08:00
|
|
|
const progress = Number(event.progress || 0);
|
|
|
|
|
if (progress > lastProgress || event.status === "completed") {
|
|
|
|
|
lastProgress = Math.max(lastProgress, progress);
|
|
|
|
|
lastProgressAt = Date.now();
|
|
|
|
|
}
|
2026-06-02 12:38:01 +08:00
|
|
|
onProgress?.(event);
|
|
|
|
|
if (event.status === "completed") {
|
|
|
|
|
settle(() => resolve(event.resultUrl || null));
|
|
|
|
|
} else if (event.status === "failed" || event.status === "cancelled") {
|
2026-06-04 21:07:48 +08:00
|
|
|
settle(() => reject(new Error(event.error || "任务失败,请稍后重试")));
|
2026-06-02 12:38:01 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cleanup = aiGenerationClient.subscribeTaskStatus(taskId, handleUpdate);
|
|
|
|
|
sseConnected = true;
|
|
|
|
|
|
|
|
|
|
fallbackTimerId = setTimeout(() => {
|
|
|
|
|
if (settled || !sseConnected) return;
|
|
|
|
|
if (cleanup) cleanup();
|
|
|
|
|
startPolling();
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
function startPolling() {
|
|
|
|
|
const poll = async () => {
|
|
|
|
|
while (!settled) {
|
2026-06-04 21:07:48 +08:00
|
|
|
if (abortRef?.current) {
|
|
|
|
|
settle(() => resolve(null));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-02 12:38:01 +08:00
|
|
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
|
|
|
if (settled || abortRef?.current) return;
|
2026-06-05 01:00:33 +08:00
|
|
|
const timeoutReason = isTaskLocallyTimedOut({
|
|
|
|
|
startedAt,
|
|
|
|
|
lastProgressAt,
|
|
|
|
|
progress: lastProgress,
|
|
|
|
|
policy: { ...timeoutPolicy, noProgressTimeoutMs },
|
|
|
|
|
});
|
|
|
|
|
if (timeoutReason) {
|
|
|
|
|
settle(() => reject(new Error(buildLocalTimeoutMessage(options.kind || "video"))));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-02 12:38:01 +08:00
|
|
|
try {
|
|
|
|
|
const task = await aiGenerationClient.getTaskStatus(taskId);
|
|
|
|
|
handleUpdate({
|
|
|
|
|
taskId,
|
|
|
|
|
status: task.status,
|
|
|
|
|
progress: task.progress || 0,
|
|
|
|
|
resultUrl: task.resultUrl,
|
|
|
|
|
error: task.error,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (!settled) settle(() => reject(e));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-06-05 01:00:33 +08:00
|
|
|
void poll();
|
2026-06-02 12:38:01 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|