Speed up artifact downloads

This commit is contained in:
npc0-hue
2026-09-03 11:27:19 +08:00
parent a82f1ff01a
commit 6311eb72ba
10 changed files with 142 additions and 83 deletions
+73 -17
View File
@@ -1,49 +1,105 @@
import type { ArtifactContentChunk, ArtifactDownloadReferenceResponse } from "../api/types";
import type { ArtifactDownloadReferenceResponse } from "../api/types";
type ArtifactContentLoader = (artifactId: string) => Promise<Response>;
type DownloadChunk = ArrayBuffer;
export async function downloadArtifactReference(
reference: ArtifactDownloadReferenceResponse,
readContent: (artifactId: string, offset: number, limit?: number) => Promise<ArtifactContentChunk>,
openContent: ArtifactContentLoader,
onProgress?: (progress: number) => void
) {
const chunks: ArrayBuffer[] = [];
let offset = 0;
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);
while (offset < reference.sizeBytes) {
const chunk = await readContent(reference.artifactId, offset, reference.chunkSizeBytes);
chunks.push(chunk.payload);
offset += chunk.payload.byteLength;
onProgress?.(Math.min(100, Math.round((offset / reference.sizeBytes) * 100)));
if (chunk.payload.byteLength === 0) {
break;
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;
}
openArtifactBlob(reference, chunks);
onProgress?.(100);
}
export function openArtifactBlob(reference: ArtifactDownloadReferenceResponse, chunks: ArrayBuffer[]) {
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 blob = new Blob(chunks, { type: reference.contentType });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = safeArtifactFilename(reference.filename);
anchor.download = safeArtifactFilename(filename);
anchor.rel = "noopener";
document.body.append(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
export function safeArtifactFilename(filename: string): string {
const cleaned = filename.replace(/[\\/]/g, "").trim();
const cleaned = filename.replace(/[\\/:*?"<>|]+/g, "-").trim();
return cleaned || "artifact.bin";
}
interface ArtifactFileWriter {
write(data: DownloadChunk): Promise<void>;
close(): Promise<void>;
abort(): Promise<void>;
}
async function createArtifactFileWriter(filename: string, contentType: string): Promise<ArtifactFileWriter> {
const picker = typeof window === "undefined" ? undefined : (window as Window & { showSaveFilePicker?: (options?: { suggestedName?: string }) => Promise<{ createWritable(): Promise<FileSystemWritableFileStream> }> }).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]");
+4 -58
View File
@@ -1,72 +1,18 @@
import type { PlatformApiClient } from "../api/client";
import type { ServerFileDownloadResponse } from "../api/types";
import { downloadArtifactReference, safeArtifactFilename, saveArtifactBlob } from "./artifactTransfer";
export async function downloadServerFileResult(client: PlatformApiClient, result: ServerFileDownloadResponse): Promise<string> {
if (result.status !== "ready") {
return result.reason ?? "文件读取任务已派发,完成后可再次下载。";
}
if (result.content !== undefined) {
saveBlob(new Blob([result.content], { type: result.contentType ?? "text/plain;charset=utf-8" }), result.filename || "server-file.txt");
saveArtifactBlob(new Blob([result.content], { type: result.contentType ?? "text/plain;charset=utf-8" }), result.filename || "server-file.txt");
return `下载已开始:${result.filename || result.key}`;
}
if (!result.artifact) {
return "文件内容尚未可用。";
}
const writer = await createFileWriter(result.filename || result.artifact.filename);
let offset = 0;
const chunkSize = Math.max(1, result.artifact.chunkSizeBytes || 1024 * 1024);
try {
while (offset < result.artifact.sizeBytes) {
const chunk = await client.readArtifactContent(result.artifact.artifactId, offset, Math.min(chunkSize, result.artifact.sizeBytes - offset));
if (chunk.contentLength <= 0) throw new Error("文件下载返回空数据");
await writer.write(chunk.payload);
offset += chunk.contentLength;
}
await writer.close();
} catch (error) {
await writer.abort();
throw error;
}
return `下载已开始:${result.filename || result.artifact.filename}`;
}
interface FileWriter {
write(data: ArrayBuffer): Promise<void>;
close(): Promise<void>;
abort(): Promise<void>;
}
async function createFileWriter(filename: string): Promise<FileWriter> {
const picker = (window as Window & { showSaveFilePicker?: (options?: { suggestedName?: string }) => Promise<{ createWritable(): Promise<FileSystemWritableFileStream> }> }).showSaveFilePicker;
if (picker) {
const handle = await picker({ suggestedName: safeFilename(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: ArrayBuffer[] = [];
return {
write: async (data) => { chunks.push(data); },
close: async () => { saveBlob(new Blob(chunks, { type: "application/octet-stream" }), safeFilename(filename)); },
abort: async () => { chunks.length = 0; }
};
}
function saveBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = safeFilename(filename);
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function safeFilename(filename: string): string {
const cleaned = filename.replace(/[\\/:*?"<>|]+/g, "-").trim();
return cleaned || "server-file.txt";
await downloadArtifactReference(result.artifact, (artifactId) => client.downloadArtifactContent(artifactId));
return `下载已开始:${safeArtifactFilename(result.filename || result.artifact.filename)}`;
}