ai-chatbot/lib/client/canvas-zip-download.ts

51 lines
3.0 KiB
TypeScript

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;
}