Initial commit: OmniAI Web Frontend

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:38:01 +08:00
commit bedee3ba8d
183 changed files with 94805 additions and 0 deletions
@@ -0,0 +1,234 @@
import { aiGenerationClient } from "../../api/aiGenerationClient";
export interface PersistedWorkbenchResultAsset {
url: string;
originalUrl: string;
ossKey?: string;
mimeType?: string;
}
interface PersistWorkbenchResultAssetInput {
title: string;
sourceUrl: string;
resultType: "image" | "video";
taskId?: string;
prompt?: string;
originalUrl?: string;
existingOssKey?: string | null;
mimeType?: string;
client?: WorkbenchResultPersistenceClient;
}
interface DownloadedTaskAsset {
blob: Blob;
filename?: string;
contentType?: string;
}
interface FetchLikeResponse {
ok: boolean;
status: number;
headers: {
get(name: string): string | null;
};
blob(): Promise<Blob>;
}
interface WorkbenchResultPersistenceClient {
downloadTaskResult(taskId: string): Promise<DownloadedTaskAsset>;
uploadAsset(input: {
dataUrl: string;
name?: string;
mimeType?: string;
scope?: string;
}): Promise<{ url: string; signedUrl?: string; ossKey?: string }>;
uploadAssetByUrl(input: {
sourceUrl: string;
name?: string;
mimeType?: string;
scope?: string;
}): Promise<{ url: string; signedUrl?: string; ossKey?: string }>;
fetchAsset?: (url: string) => Promise<FetchLikeResponse>;
}
function getDefaultClient(): WorkbenchResultPersistenceClient {
return {
downloadTaskResult: (taskId) => aiGenerationClient.downloadTaskResult(taskId),
uploadAsset: (input) => aiGenerationClient.uploadAsset(input),
uploadAssetByUrl: (input) => aiGenerationClient.uploadAssetByUrl(input),
fetchAsset: (url) => fetch(url, { credentials: "omit" }),
};
}
function getGeneratedResultOssKey(url: string): string | null {
try {
const key = decodeURIComponent(new URL(url, globalThis.location?.href || "http://localhost").pathname.replace(/^\/+/, ""));
return /^users\/[^/]+\/generation-results\/.+/i.test(key) ? key : null;
} catch {
return null;
}
}
function createFallbackResult(input: PersistWorkbenchResultAssetInput): PersistedWorkbenchResultAsset {
return {
url: input.sourceUrl,
originalUrl: input.originalUrl || input.sourceUrl,
ossKey: input.existingOssKey || getGeneratedResultOssKey(input.sourceUrl) || undefined,
mimeType: input.mimeType,
};
}
function getResultExtension(url: string, contentType: string | undefined, resultType: "image" | "video") {
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, globalThis.location?.href || "http://localhost").pathname;
const matched = pathname.match(/\.([a-z0-9]{2,5})$/i);
if (matched?.[1]) return matched[1].toLowerCase();
} catch {
// Fall through to media-type fallback.
}
return resultType === "video" ? "mp4" : "png";
}
export function buildWorkbenchResultAssetName(
title: string,
sourceUrl: string,
resultType: "image" | "video",
contentType?: string,
): string {
const normalized = title
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, " ")
.slice(0, 80)
.trim();
const base = normalized || (resultType === "video" ? "generated-video" : "generated-image");
return `${base}.${getResultExtension(sourceUrl, contentType, resultType)}`;
}
function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(reader.error || new Error("资源读取失败"));
reader.readAsDataURL(blob);
});
}
async function assertMediaBlobIsUsable(blob: Blob, resultType: "image" | "video") {
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 =
resultType === "video"
? isMp4 || mime.startsWith("video/")
: isPng || isJpeg || isGif || isWebp || mime.startsWith("image/");
if (!hasExpectedMagic) throw new Error("未收到有效的媒体文件。");
}
async function downloadResultBlob(
input: PersistWorkbenchResultAssetInput,
client: WorkbenchResultPersistenceClient,
): Promise<{ blob: Blob; contentType?: string; filename?: string }> {
if (input.taskId) {
try {
const result = await client.downloadTaskResult(input.taskId);
return {
blob: result.blob,
contentType: result.contentType || result.blob.type || undefined,
filename: result.filename,
};
} catch {
// Some older tasks may not support the proxy download route; fall back to the URL while it is still valid.
}
}
const fetchAsset = client.fetchAsset || ((url: string) => fetch(url, { credentials: "omit" }));
const response = await fetchAsset(input.sourceUrl);
if (!response.ok) throw new Error(`结果资源请求失败:${response.status}`);
const blob = await response.blob();
return {
blob,
contentType: blob.type || response.headers.get("content-type") || undefined,
};
}
export async function persistWorkbenchResultAsset(input: PersistWorkbenchResultAssetInput): Promise<PersistedWorkbenchResultAsset> {
const fallbackResult = createFallbackResult(input);
if (fallbackResult.ossKey) return fallbackResult;
const client = input.client || getDefaultClient();
const mimeType = input.mimeType || (input.resultType === "video" ? "video/mp4" : "image/png");
const name = buildWorkbenchResultAssetName(input.title, input.sourceUrl, input.resultType, mimeType);
// Try server-side URL-to-OSS first (no base64, no client download)
try {
const uploaded = await client.uploadAssetByUrl({
sourceUrl: input.sourceUrl,
name,
mimeType,
scope: "workbench-result",
});
return {
url: uploaded.url,
originalUrl: input.sourceUrl,
ossKey: uploaded.ossKey,
mimeType,
};
} catch (urlError) {
console.warn("[workbench] URL upload failed, falling back to download+base64:", urlError instanceof Error ? urlError.message : urlError);
}
// Fallback: download → base64 → upload
try {
const downloaded = await downloadResultBlob(input, client);
if (!downloaded.blob.size) throw new Error("结果资源内容为空");
await assertMediaBlobIsUsable(downloaded.blob, input.resultType);
const fallbackMime = downloaded.contentType || downloaded.blob.type || mimeType;
const fallbackName = downloaded.filename || name;
const dataUrl = await blobToDataUrl(downloaded.blob);
const uploaded = await client.uploadAsset({
dataUrl,
name: fallbackName,
mimeType: fallbackMime,
scope: "workbench-result",
});
return {
url: uploaded.url,
originalUrl: input.sourceUrl,
ossKey: uploaded.ossKey,
mimeType: fallbackMime,
};
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || "");
if (/413|too large/i.test(msg)) {
console.warn("[workbench] OSS upload rejected (file too large), using temporary URL:", msg);
} else {
console.warn("[workbench] result persistence fallback:", error);
}
return fallbackResult;
}
}