import type { ArtifactDownloadReferenceResponse } from "../api/types"; type ArtifactContentLoader = (artifactId: string) => Promise; type DownloadChunk = ArrayBuffer; export async function downloadArtifactReference( reference: ArtifactDownloadReferenceResponse, openContent: ArtifactContentLoader, onProgress?: (progress: number) => void ) { const response = await openContent(reference.artifactId); const writer = await createArtifactFileWriter(reference.filename, reference.contentType); const totalBytes = Number(response.headers.get("Content-Length") ?? reference.sizeBytes); let downloadedBytes = 0; onProgress?.(0); try { const reader = response.body?.getReader(); if (!reader) { const payload = await response.arrayBuffer(); await writer.write(payload); downloadedBytes = payload.byteLength; onProgress?.(progressPercent(downloadedBytes, totalBytes)); } else { for (;;) { const { done, value } = await reader.read(); if (done) break; const chunk = copyStreamChunk(value); await writer.write(chunk); downloadedBytes += chunk.byteLength; onProgress?.(progressPercent(downloadedBytes, totalBytes)); } } await writer.close(); } catch (error) { await writer.abort(); throw error; } onProgress?.(100); } export function openArtifactBlob(reference: ArtifactDownloadReferenceResponse, chunks: DownloadChunk[]) { saveArtifactBlob(new Blob(chunks, { type: reference.contentType }), reference.filename); } export function saveArtifactBlob(blob: Blob, filename: string) { if (typeof document === "undefined" || typeof URL === "undefined") { return; } const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = safeArtifactFilename(filename); anchor.rel = "noopener"; document.body.append(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); } export function safeArtifactFilename(filename: string): string { const cleaned = filename.replace(/[\\/:*?"<>|]+/g, "-").trim(); return cleaned || "artifact.bin"; } interface ArtifactFileWriter { write(data: DownloadChunk): Promise; close(): Promise; abort(): Promise; } async function createArtifactFileWriter(filename: string, contentType: string): Promise { const picker = typeof window === "undefined" ? undefined : (window as Window & { showSaveFilePicker?: (options?: { suggestedName?: string }) => Promise<{ createWritable(): Promise }> }).showSaveFilePicker; if (picker) { const handle = await picker({ suggestedName: safeArtifactFilename(filename) }); const writable = await handle.createWritable(); return { write: async (data) => { await writable.write(data); }, close: async () => { await writable.close(); }, abort: async () => { await writable.abort(); } }; } const chunks: DownloadChunk[] = []; return { write: async (data) => { chunks.push(data); }, close: async () => { saveArtifactBlob(new Blob(chunks, { type: contentType }), filename); }, abort: async () => { chunks.length = 0; } }; } function progressPercent(downloadedBytes: number, totalBytes: number): number { if (!Number.isFinite(totalBytes) || totalBytes <= 0) return 0; return Math.min(100, Math.round((downloadedBytes / totalBytes) * 100)); } function copyStreamChunk(value: Uint8Array): ArrayBuffer { const copy = new Uint8Array(value.byteLength); copy.set(value); return copy.buffer; } export function safeArtifactError(error: unknown): string { const message = error instanceof Error ? error.message : "制品传输失败"; return message.replace(/\/Users\/[^\s]+/g, "[path]").replace(/Bearer\s+[^\s]+/gi, "[token]").replace(/sk-[A-Za-z0-9_-]+/g, "[secret]"); }