Files
omniai-web/src/features/canvas/canvasAssetPersistence.ts
T
stringadmin 4a298d205b
Web Quality / verify (push) Has been cancelled
chore: reduce frontend lint warnings
2026-06-09 12:02:30 +08:00

196 lines
6.4 KiB
TypeScript

import { aiGenerationClient } from "../../api/aiGenerationClient";
import type { WebCanvasWorkflowAssetRef } from "../../types";
interface CanvasGeneratedResultInput {
url: string;
taskId: string;
mediaType: string;
resultType?: "image" | "video";
ossKey?: string | null;
originalUrl?: string | null;
expiresAt?: string | null;
}
interface PersistCanvasGeneratedResultInput extends CanvasGeneratedResultInput {
title: string;
client?: CanvasAssetPersistenceClient;
}
interface CanvasAssetPersistenceClient {
downloadTaskResult(taskId: string): Promise<{ blob: Blob; filename?: string; contentType?: string }>;
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<{
ok: boolean;
status: number;
headers: { get(name: string): string | null };
blob(): Promise<Blob>;
}>;
}
function getDefaultClient(): CanvasAssetPersistenceClient {
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;
}
}
export function createCanvasAssetRefFromGeneratedResult(
input: CanvasGeneratedResultInput,
): WebCanvasWorkflowAssetRef {
const inferredOssKey = input.ossKey || getGeneratedResultOssKey(input.url);
const mediaType = input.mediaType || (input.resultType === "video" ? "video/mp4" : "image/png");
return {
url: input.url,
mediaType,
sourceTaskId: input.taskId,
ossKey: inferredOssKey || null,
originalUrl: input.originalUrl || null,
expiresAt: input.expiresAt || null,
};
}
function getCanvasResultExtension(mediaType: string, fallbackUrl: string) {
const mime = mediaType.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/mp4") return "mp4";
if (mime === "video/webm") return "webm";
try {
const matched = new URL(fallbackUrl, globalThis.location?.href || "http://localhost")
.pathname
.match(/\.([a-z0-9]{2,5})$/i);
if (matched?.[1]) return matched[1].toLowerCase();
} catch {
// Keep media fallback below.
}
return mediaType.startsWith("video/") ? "mp4" : "png";
}
function buildCanvasResultFileName(title: string, mediaType: string, fallbackUrl: string) {
const normalized = title
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, " ")
.slice(0, 80)
.trim();
return `${normalized || "canvas-result"}.${getCanvasResultExtension(mediaType, fallbackUrl)}`;
}
function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
} else {
reject(new Error("Unable to read canvas result"));
}
};
reader.onerror = () => reject(reader.error || new Error("Unable to read canvas result"));
reader.readAsDataURL(blob);
});
}
async function downloadCanvasResultBlob(
input: PersistCanvasGeneratedResultInput,
client: CanvasAssetPersistenceClient,
) {
try {
return await client.downloadTaskResult(input.taskId);
} catch {
const fetchAsset = client.fetchAsset || ((url: string) => fetch(url, { credentials: "omit" }));
const response = await fetchAsset(input.url);
if (!response.ok) throw new Error(`Canvas result request failed: ${response.status}`);
const blob = await response.blob();
return {
blob,
contentType: blob.type || response.headers.get("content-type") || undefined,
};
}
}
export async function persistCanvasGeneratedResultAsset(
input: PersistCanvasGeneratedResultInput,
): Promise<WebCanvasWorkflowAssetRef> {
const fallbackRef = createCanvasAssetRefFromGeneratedResult(input);
if (fallbackRef.ossKey) return fallbackRef;
const client = input.client || getDefaultClient();
const fallbackMediaType = input.mediaType || (input.resultType === "video" ? "video/mp4" : "image/png");
const name = buildCanvasResultFileName(input.title, fallbackMediaType, input.url);
// Try server-side URL-to-OSS first
try {
const uploaded = await client.uploadAssetByUrl({
sourceUrl: input.url,
name,
mimeType: fallbackMediaType,
scope: "canvas-node-result",
});
return createCanvasAssetRefFromGeneratedResult({
...input,
url: uploaded.url,
ossKey: uploaded.ossKey || null,
mediaType: fallbackMediaType,
originalUrl: input.originalUrl || input.url,
});
} catch (urlError) {
console.warn("[canvas] URL upload failed, falling back to download+base64:", urlError instanceof Error ? urlError.message : urlError);
}
// Fallback: download → base64 → upload
try {
const downloaded = await downloadCanvasResultBlob(input, client);
const mimeType = downloaded.contentType || downloaded.blob.type || fallbackMediaType;
const dataUrl = await blobToDataUrl(downloaded.blob);
const uploaded = await client.uploadAsset({
dataUrl,
name: downloaded.filename || name,
mimeType,
scope: "canvas-node-result",
});
return createCanvasAssetRefFromGeneratedResult({
...input,
url: uploaded.url,
ossKey: uploaded.ossKey || null,
mediaType: mimeType,
originalUrl: input.originalUrl || input.url,
});
} catch (error) {
const msg = error instanceof Error ? error.message : String(error || "");
if (/413|too large/i.test(msg)) {
console.warn("[canvas] OSS upload rejected (file too large), using temporary URL:", msg);
} else {
console.warn("[canvas] result persistence fallback:", error);
}
return fallbackRef;
}
}