bedee3ba8d
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
203 lines
6.8 KiB
TypeScript
203 lines
6.8 KiB
TypeScript
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, WebCanvasWorkflowNode>,
|
|
): 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,
|
|
};
|
|
}),
|
|
};
|
|
}
|