export type CanvasZipTaskView = { id: string; status: "pending" | "running" | "completed" | "failed"; stage: string; progress: number; totalCount: number; successCount: number; failedCount: number; error?: string; downloadUrl?: string; }; const endpoint = "/api/canvas/download-zip-task"; const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); async function taskRequest(url: string, init?: RequestInit) { for (let attempt = 0; ; attempt++) { let response: Response; try { response = await fetch(url, { ...init, cache: "no-store", signal: AbortSignal.timeout(20_000) }); } catch { if (attempt < 3) { await wait(1000 * (attempt + 1)); continue; } throw new Error("打包连接暂时中断,再次点击可继续查询原任务"); } const payload = await response.json().catch(() => null); if ((response.status === 429 || response.status >= 500) && attempt < 3) { await wait(Math.min(30_000, Math.max(1000, Number(response.headers.get("retry-after") || 2) * 1000))); continue; } if (!response.ok || !payload?.task) throw new Error(payload?.error || "无法查询打包任务,请稍后重试"); return payload.task as CanvasZipTaskView; } } export async function downloadCanvasSelectionZip(params: { folderName: string; files: Array<{ url: string; name: string }>; selectionKey: string; onProgress: (task: CanvasZipTaskView) => void; }) { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([params.selectionKey, params.files]))); const key = "canvas-zip:" + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join(""); let taskId = ""; try { taskId = sessionStorage.getItem(key) || ""; } catch { /* Storage is optional. */ } if (!/^[0-9a-f-]{36}$/i.test(taskId)) taskId = crypto.randomUUID(); try { sessionStorage.setItem(key, taskId); } catch { /* Downloads work without browser persistence. */ } let task = await taskRequest(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ taskId, folderName: params.folderName, files: params.files, retry: true }) }); taskId = task.id; try { sessionStorage.setItem(key, taskId); } catch {} const deadline = Date.now() + 30 * 60_000; for (;;) { params.onProgress(task); if (task.status === "failed") throw new Error(task.error || "打包失败,再次点击可重试"); if (task.status === "completed") break; if (Date.now() > deadline) throw new Error("后台仍在准备图片,再次点击可继续查询进度"); await wait(2500); task = await taskRequest(`${endpoint}?taskId=${encodeURIComponent(taskId)}`); } if (!task.successCount || !task.downloadUrl) throw new Error("没有成功准备的图片,请稍后重试"); // A GET for a complete immutable file supports browser retries and byte ranges. const frame = document.createElement("iframe"); frame.hidden = true; frame.src = task.downloadUrl; document.body.appendChild(frame); window.setTimeout(() => frame.remove(), 60 * 60_000); return task; }