196 lines
6.4 KiB
TypeScript
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;
|
|
}
|
|
}
|