import type { WebCanvasWorkflow, WebCanvasWorkflowAssetRef, WebCanvasWorkflowEdge, WebCanvasWorkflowNode, WebCanvasWorkflowPort, WebCanvasWorkflowTaskRef, } from "../../types"; export const CANVAS_WORKFLOW_SCHEMA_VERSION = 2; type LegacyNodeKind = WebCanvasWorkflowNode["kind"]; export type CanvasWorkflowNodeKind = "text" | "image" | "video"; export interface CanvasTaskResultInput { taskId: string; status: WebCanvasWorkflowTaskRef["status"]; resultUrl?: string | null; ossKey?: string | null; mediaType?: string | null; originalUrl?: string | null; expiresAt?: string | null; } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function toStringValue(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback; } function normalizeNodeKind(kind: LegacyNodeKind): CanvasWorkflowNodeKind { if (kind === "image") return "image"; if (kind === "video") return "video"; return "text"; } export function getCanvasWorkflowNodeOutputType(kind: CanvasWorkflowNodeKind): WebCanvasWorkflowPort["type"] { if (kind === "image") return "image"; if (kind === "video") return "video"; return "text"; } export function getCanvasWorkflowNodeInputType(kind: CanvasWorkflowNodeKind): WebCanvasWorkflowPort["type"] { if (kind === "image" || kind === "video") return "image"; return "text"; } export function createCanvasWorkflowPort( direction: "input" | "output", kind: CanvasWorkflowNodeKind, ): WebCanvasWorkflowPort { return { id: direction === "output" ? "out" : "in", type: direction === "output" ? getCanvasWorkflowNodeOutputType(kind) : getCanvasWorkflowNodeInputType(kind), }; } function inferMediaTypeFromNode(kind: CanvasWorkflowNodeKind): string { if (kind === "image") return "image"; if (kind === "video") return "video"; return "text"; } function normalizeAssetRef(node: WebCanvasWorkflowNode, normalizedKind: CanvasWorkflowNodeKind): WebCanvasWorkflowAssetRef | null { const metadata = isRecord(node.metadata) ? node.metadata : {}; const candidate = isRecord(node.assetRef) ? node.assetRef : isRecord(metadata.assetRef) ? metadata.assetRef : null; const url = toStringValue(candidate?.url, node.previewUrl || ""); if (!url) return null; return { url, ossKey: toStringValue(candidate?.ossKey) || null, mediaType: toStringValue(candidate?.mediaType, inferMediaTypeFromNode(normalizedKind)), sourceTaskId: toStringValue(candidate?.sourceTaskId, toStringValue(metadata.sourceTaskId)) || null, originalUrl: toStringValue(candidate?.originalUrl) || null, expiresAt: toStringValue(candidate?.expiresAt) || null, }; } function normalizeTaskRef(node: WebCanvasWorkflowNode, assetRef: WebCanvasWorkflowAssetRef | null): WebCanvasWorkflowTaskRef | null { const metadata = isRecord(node.metadata) ? node.metadata : {}; const candidate = isRecord(node.taskRef) ? node.taskRef : isRecord(metadata.taskRef) ? metadata.taskRef : null; const taskId = toStringValue(candidate?.taskId, assetRef?.sourceTaskId || toStringValue(metadata.sourceTaskId)); if (!taskId) return null; const status = toStringValue(candidate?.status, "completed") as WebCanvasWorkflowTaskRef["status"]; return { taskId, status, resultUrl: toStringValue(candidate?.resultUrl, assetRef?.url || node.previewUrl || "") || null, updatedAt: toStringValue(candidate?.updatedAt) || null, }; } function normalizeNodeParams(node: WebCanvasWorkflowNode): Record { const metadata = isRecord(node.metadata) ? node.metadata : {}; const params = isRecord(node.params) ? node.params : {}; const reserved = new Set(["assetRef", "taskRef", "sourceTaskId"]); const metadataParams = Object.fromEntries(Object.entries(metadata).filter(([key]) => !reserved.has(key))); return { ...metadataParams, ...params, }; } export function normalizeCanvasWorkflowNode(node: WebCanvasWorkflowNode): WebCanvasWorkflowNode { const kind = normalizeNodeKind(node.kind); const assetRef = normalizeAssetRef(node, kind); const taskRef = normalizeTaskRef(node, assetRef); const inputs = Array.isArray(node.inputs) && node.inputs.length ? node.inputs : [createCanvasWorkflowPort("input", kind)]; const outputs = Array.isArray(node.outputs) && node.outputs.length ? node.outputs : [createCanvasWorkflowPort("output", kind)]; return { ...node, kind, inputs, outputs, assetRef, taskRef, params: normalizeNodeParams(node), previewUrl: assetRef?.url || node.previewUrl, }; } export function normalizeCanvasWorkflowEdge( edge: WebCanvasWorkflowEdge, nodeById: Map, ): WebCanvasWorkflowEdge { const sourceNode = nodeById.get(edge.source); const targetNode = nodeById.get(edge.target); const sourcePort = edge.sourcePort || sourceNode?.outputs?.[0] || createCanvasWorkflowPort("output", "text"); const targetPort = edge.targetPort || targetNode?.inputs?.[0] || createCanvasWorkflowPort("input", "text"); return { ...edge, sourcePort, targetPort, }; } export function normalizeCanvasWorkflowSchema(workflow: WebCanvasWorkflow): WebCanvasWorkflow { const nodes = workflow.nodes.map(normalizeCanvasWorkflowNode); const nodeById = new Map(nodes.map((node) => [node.id, node])); return { ...workflow, schemaVersion: CANVAS_WORKFLOW_SCHEMA_VERSION, nodes, edges: workflow.edges.map((edge) => normalizeCanvasWorkflowEdge(edge, nodeById)), }; } export function createCanvasAssetRefFromTaskResult( kind: CanvasWorkflowNodeKind, result: CanvasTaskResultInput, ): WebCanvasWorkflowAssetRef | null { const url = String(result.resultUrl || "").trim(); if (!url) return null; return { url, ossKey: result.ossKey || null, mediaType: result.mediaType || inferMediaTypeFromNode(kind), sourceTaskId: result.taskId, originalUrl: result.originalUrl || null, expiresAt: result.expiresAt || null, }; } export function applyCanvasNodeTaskResult( workflow: WebCanvasWorkflow, nodeId: string, result: CanvasTaskResultInput, ): WebCanvasWorkflow { const normalized = normalizeCanvasWorkflowSchema(workflow); return { ...normalized, nodes: normalized.nodes.map((node) => { if (node.id !== nodeId) return node; const kind = normalizeNodeKind(node.kind); const assetRef = createCanvasAssetRefFromTaskResult(kind, result); return { ...node, assetRef: assetRef || node.assetRef || null, taskRef: { taskId: result.taskId, status: result.status, resultUrl: result.resultUrl || assetRef?.url || node.taskRef?.resultUrl || null, updatedAt: new Date().toISOString(), }, previewUrl: assetRef?.url || result.resultUrl || node.previewUrl, }; }), }; }