import type { Asset, GenerationJob } from "@/lib/types"; export async function waitForJobOutputAssets(job: GenerationJob, signal: AbortSignal) { let currentJob = job; for (let attempt = 0; attempt < 25; attempt += 1) { if (attempt > 0 || currentJob.outputAssetIds.length === 0) { currentJob = await fetchImageJob(job.id, signal); if (["failed", "expired", "cancelled"].includes(currentJob.status)) { throw new Error(currentJob.error?.message || "图层编辑任务未成功完成"); } } if (currentJob.outputAssetIds.length) { const outputs = await fetchAssetsByID(currentJob.outputAssetIds, signal); if (outputs.length) return outputs; } await abortableDelay(800, signal); } return []; } async function fetchImageJob(jobId: string, signal: AbortSignal) { const response = await fetch(`/api/generations/image/${encodeURIComponent(jobId)}`, { cache: "no-store", signal }); const payload = await readJSON(response); if (!response.ok) throw new Error(payloadError(payload, "刷新图层编辑任务失败")); const job = payload.job as GenerationJob | undefined; if (!job?.id || !Array.isArray(job.outputAssetIds)) throw new Error("图层编辑任务返回数据不完整"); return job; } async function fetchAssetsByID(ids: string[], signal: AbortSignal) { const response = await fetch("/api/assets", { cache: "no-store", signal }); const payload = await readJSON(response); if (!response.ok) throw new Error(payloadError(payload, "读取编辑结果素材失败")); const assets = Array.isArray(payload.assets) ? payload.assets.filter(isAsset) : []; const assetsByID = new Map(assets.map((asset) => [asset.id, asset])); return ids.map((id) => assetsByID.get(id)).filter((asset): asset is Asset => Boolean(asset)); } function isAsset(value: unknown): value is Asset { if (!value || typeof value !== "object") return false; const record = value as Record; return typeof record.id === "string" && typeof record.name === "string" && typeof record.url === "string"; } async function readJSON(response: Response): Promise> { return response.json().catch(() => ({})) as Promise>; } function payloadError(payload: Record, fallback: string) { return typeof payload.error === "string" && payload.error.trim() ? payload.error : fallback; } function abortableDelay(milliseconds: number, signal: AbortSignal) { return new Promise((resolve, reject) => { if (signal.aborted) { reject(new DOMException("Aborted", "AbortError")); return; } const onAbort = () => { window.clearTimeout(timer); reject(new DOMException("Aborted", "AbortError")); }; const timer = window.setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, milliseconds); signal.addEventListener("abort", onAbort, { once: true }); }); }