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; }>; } 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 { 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 { 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; } }