Initial commit: OmniAI Web Frontend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { buildApiUrl, buildAuthHeaders } from "../../api/serverConnection";
|
||||
|
||||
function sanitizeDownloadFilename(value: string, fallback: string) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.slice(0, 80)
|
||||
.trim();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function getResultDownloadExtension(url: string, contentType: string | null, isVideo: boolean) {
|
||||
const mime = (contentType || "").split(";")[0]?.trim().toLowerCase();
|
||||
if (mime === "image/jpeg") return "jpg";
|
||||
if (mime === "image/png") return "png";
|
||||
if (mime === "image/webp") return "webp";
|
||||
if (mime === "image/gif") return "gif";
|
||||
if (mime === "video/webm") return "webm";
|
||||
if (mime === "video/quicktime") return "mov";
|
||||
if (mime === "video/mp4") return "mp4";
|
||||
|
||||
try {
|
||||
const pathname = new URL(url, window.location.href).pathname;
|
||||
const matched = pathname.match(/\.([a-z0-9]{2,5})$/i);
|
||||
if (matched?.[1]) return matched[1].toLowerCase();
|
||||
} catch {
|
||||
// Keep the fallback below for malformed but still browser-loadable URLs.
|
||||
}
|
||||
|
||||
return isVideo ? "mp4" : "png";
|
||||
}
|
||||
|
||||
type LocalFilePickerWritable = {
|
||||
write: (data: Blob) => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
type LocalFilePickerHandle = {
|
||||
createWritable: () => Promise<LocalFilePickerWritable>;
|
||||
};
|
||||
|
||||
type LocalFilePickerWindow = Window & {
|
||||
showSaveFilePicker?: (options?: {
|
||||
suggestedName?: string;
|
||||
types?: Array<{
|
||||
description: string;
|
||||
accept: Record<string, string[]>;
|
||||
}>;
|
||||
}) => Promise<LocalFilePickerHandle>;
|
||||
};
|
||||
|
||||
async function saveBlobToLocal(blob: Blob, filename: string, isVideo: boolean): Promise<"saved" | "started"> {
|
||||
const startBrowserDownload = () => {
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = objectUrl;
|
||||
anchor.download = filename;
|
||||
anchor.rel = "noopener";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
|
||||
return "started" as const;
|
||||
};
|
||||
|
||||
const savePicker = (window as LocalFilePickerWindow).showSaveFilePicker;
|
||||
if (savePicker) {
|
||||
try {
|
||||
const pickerTypes = [
|
||||
isVideo
|
||||
? { description: "Video", accept: { [blob.type || "video/mp4"]: [`.${filename.split(".").pop() || "mp4"}`] } }
|
||||
: { description: "Image", accept: { [blob.type || "image/png"]: [`.${filename.split(".").pop() || "png"}`] } },
|
||||
];
|
||||
const handle = await savePicker({ suggestedName: filename, types: pickerTypes });
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
return "saved";
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
||||
return startBrowserDownload();
|
||||
}
|
||||
}
|
||||
|
||||
return startBrowserDownload();
|
||||
}
|
||||
|
||||
async function assertDownloadBlobIsUsable(blob: Blob, isVideo: boolean) {
|
||||
const mime = String(blob.type || "").toLowerCase();
|
||||
if (/^(?:application|text)\/(?:json|xml|html|plain)|\+xml/.test(mime)) {
|
||||
throw new Error("下载失败:结果链接已过期或返回了错误内容,请重新生成后再下载。");
|
||||
}
|
||||
|
||||
const header = new Uint8Array(await blob.slice(0, 16).arrayBuffer());
|
||||
const asText = new TextDecoder("utf-8", { fatal: false }).decode(header);
|
||||
const isPng = header[0] === 0x89 && header[1] === 0x50 && header[2] === 0x4e && header[3] === 0x47;
|
||||
const isJpeg = header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff;
|
||||
const isGif = asText.startsWith("GIF8");
|
||||
const isWebp = asText.startsWith("RIFF") && asText.slice(8, 12) === "WEBP";
|
||||
const isMp4 = asText.slice(4, 8) === "ftyp";
|
||||
const looksLikeErrorDocument = asText.startsWith("<?xml") || asText.startsWith("<Error") || asText.startsWith("{\"") || asText.startsWith("<!DO");
|
||||
|
||||
if (looksLikeErrorDocument) {
|
||||
throw new Error("下载失败:结果链接已过期或返回了错误内容,请重新生成后再下载。");
|
||||
}
|
||||
|
||||
const hasExpectedMagic = isVideo ? isMp4 || mime.startsWith("video/") : isPng || isJpeg || isGif || isWebp || mime.startsWith("image/");
|
||||
if (!hasExpectedMagic) {
|
||||
throw new Error("下载失败:未收到有效的媒体文件。");
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadResultAsset(url: string, filenameBase: string, isVideo: boolean, taskId?: string): Promise<"saved" | "started"> {
|
||||
let blob: Blob | null = null;
|
||||
let contentType: string | null = null;
|
||||
|
||||
if (taskId) {
|
||||
try {
|
||||
const result = await aiGenerationClient.downloadTaskResult(taskId);
|
||||
blob = result.blob;
|
||||
contentType = result.contentType || result.blob.type || null;
|
||||
} catch {
|
||||
blob = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!blob) {
|
||||
const needsProxy = /aliyuncs\.com/i.test(url);
|
||||
const fetchUrl = needsProxy
|
||||
? buildApiUrl(`ai/proxy-download?url=${encodeURIComponent(url)}`)
|
||||
: url;
|
||||
const fetchOpts: RequestInit = needsProxy
|
||||
? { headers: buildAuthHeaders() }
|
||||
: { credentials: "omit" };
|
||||
const response = await fetch(fetchUrl, fetchOpts);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败:资源请求返回 ${response.status}`);
|
||||
}
|
||||
blob = await response.blob();
|
||||
contentType = blob.type || response.headers.get("content-type");
|
||||
}
|
||||
|
||||
if (!blob.size) {
|
||||
throw new Error("下载失败:资源内容为空");
|
||||
}
|
||||
await assertDownloadBlobIsUsable(blob, isVideo);
|
||||
|
||||
const extension = getResultDownloadExtension(url, contentType, isVideo);
|
||||
const filename = `${sanitizeDownloadFilename(filenameBase, isVideo ? "generated-video" : "generated-image")}.${extension}`;
|
||||
return saveBlobToLocal(blob, filename, isVideo);
|
||||
}
|
||||
Reference in New Issue
Block a user