2026-06-02 12:38:01 +08:00
|
|
|
import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway";
|
|
|
|
|
import type { WebCanvasWorkflow, WebCanvasWorkflowNode, WebCanvasWorkflowTaskRef } from "../../types";
|
|
|
|
|
import { toHappyHorseDisplayModel } from "../../utils/happyHorseRouting";
|
|
|
|
|
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
|
|
|
|
import { normalizeCanvasWorkflowSchema } from "./canvasWorkflowSchema";
|
|
|
|
|
|
|
|
|
|
export interface CanvasExecutionImageReference {
|
|
|
|
|
nodeId: string;
|
|
|
|
|
title: string;
|
|
|
|
|
url: string;
|
|
|
|
|
ossKey?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CanvasNodeExecutionContext {
|
|
|
|
|
node: WebCanvasWorkflowNode;
|
|
|
|
|
prompt: string;
|
|
|
|
|
imageReferences: CanvasExecutionImageReference[];
|
|
|
|
|
upstreamTaskRefs: WebCanvasWorkflowTaskRef[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNodeText(node: WebCanvasWorkflowNode | undefined): string {
|
|
|
|
|
if (!node) return "";
|
|
|
|
|
return String(node.detail || node.params?.prompt || "").trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNodeAssetUrl(node: WebCanvasWorkflowNode | undefined): string {
|
|
|
|
|
return String(node?.assetRef?.url || node?.previewUrl || "").trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function incomingEdges(workflow: WebCanvasWorkflow, nodeId: string) {
|
|
|
|
|
return workflow.edges.filter((edge) => edge.target === nodeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveCanvasNodeExecutionContext(
|
|
|
|
|
workflowInput: WebCanvasWorkflow,
|
|
|
|
|
nodeId: string,
|
|
|
|
|
): CanvasNodeExecutionContext {
|
|
|
|
|
const workflow = normalizeCanvasWorkflowSchema(workflowInput);
|
|
|
|
|
const nodeById = new Map(workflow.nodes.map((node) => [node.id, node]));
|
|
|
|
|
const node = nodeById.get(nodeId);
|
|
|
|
|
if (!node) throw new Error(`Canvas node not found: ${nodeId}`);
|
|
|
|
|
|
|
|
|
|
const imageReferences: CanvasExecutionImageReference[] = [];
|
|
|
|
|
const upstreamPrompts: string[] = [];
|
|
|
|
|
const upstreamTaskRefs: WebCanvasWorkflowTaskRef[] = [];
|
|
|
|
|
const seenImages = new Set<string>();
|
|
|
|
|
|
|
|
|
|
incomingEdges(workflow, nodeId).forEach((edge) => {
|
|
|
|
|
const sourceNode = nodeById.get(edge.source);
|
|
|
|
|
if (!sourceNode) return;
|
|
|
|
|
if (sourceNode.taskRef) upstreamTaskRefs.push(sourceNode.taskRef);
|
|
|
|
|
const sourceKind = sourceNode.kind;
|
|
|
|
|
if (sourceKind === "image") {
|
|
|
|
|
const url = getNodeAssetUrl(sourceNode);
|
|
|
|
|
if (url && !seenImages.has(`${sourceNode.id}:${url}`)) {
|
|
|
|
|
seenImages.add(`${sourceNode.id}:${url}`);
|
|
|
|
|
imageReferences.push({
|
|
|
|
|
nodeId: sourceNode.id,
|
|
|
|
|
title: sourceNode.label,
|
|
|
|
|
url,
|
|
|
|
|
ossKey: sourceNode.assetRef?.ossKey || null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const text = getNodeText(sourceNode);
|
|
|
|
|
if (text) upstreamPrompts.push(text);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
node,
|
|
|
|
|
prompt: getNodeText(node) || upstreamPrompts.join("\n"),
|
|
|
|
|
imageReferences,
|
|
|
|
|
upstreamTaskRefs,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeDuration(value: unknown, fallback = 5): number {
|
|
|
|
|
const parsed = Number(String(value || "").replace(/s$/i, ""));
|
|
|
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function hasRunningCanvasTaskRef(node: WebCanvasWorkflowNode): boolean {
|
|
|
|
|
const status = node.taskRef?.status;
|
|
|
|
|
return status === "queued" || status === "pending" || status === "running";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildCanvasVideoTaskInput(workflow: WebCanvasWorkflow, nodeId: string): CreatePreviewTaskInput {
|
|
|
|
|
const context = resolveCanvasNodeExecutionContext(workflow, nodeId);
|
|
|
|
|
const params = context.node.params || {};
|
|
|
|
|
const referenceUrls = context.imageReferences.map((item) => item.url);
|
|
|
|
|
const displayModel = toHappyHorseDisplayModel(String(params.model || workflow.settings.model || "happyhorse-1.0"));
|
2026-06-09 12:02:30 +08:00
|
|
|
const model = resolveVideoRequestModel({ model: displayModel, referenceUrls });
|
2026-06-02 12:38:01 +08:00
|
|
|
return {
|
|
|
|
|
title: context.node.label || "视频节点生成",
|
|
|
|
|
type: "video",
|
|
|
|
|
prompt: context.prompt || (referenceUrls.length ? "根据参考图片生成视频" : ""),
|
|
|
|
|
params: {
|
|
|
|
|
model,
|
|
|
|
|
ratio: String(params.aspectRatio || params.ratio || workflow.settings.ratio || "16:9"),
|
|
|
|
|
quality: String(params.resolution || workflow.settings.resolution || "720P"),
|
|
|
|
|
resolution: String(params.resolution || workflow.settings.resolution || "720P"),
|
|
|
|
|
duration: normalizeDuration(params.duration || workflow.settings.duration),
|
|
|
|
|
frameMode: params.videoMode === "firstlast" ? "start-end" : "omni",
|
|
|
|
|
referenceUrls: referenceUrls.length ? referenceUrls : undefined,
|
|
|
|
|
muted: false,
|
|
|
|
|
hasReferenceVideo: false,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildCanvasImageTaskInput(workflow: WebCanvasWorkflow, nodeId: string): CreatePreviewTaskInput {
|
|
|
|
|
const context = resolveCanvasNodeExecutionContext(workflow, nodeId);
|
|
|
|
|
const params = context.node.params || {};
|
|
|
|
|
const referenceUrls = context.imageReferences.map((item) => item.url);
|
|
|
|
|
return {
|
|
|
|
|
title: context.node.label || "图片节点生成",
|
|
|
|
|
type: "image",
|
|
|
|
|
prompt: context.prompt || (referenceUrls.length ? "根据参考图片生成图片" : ""),
|
|
|
|
|
params: {
|
|
|
|
|
model: String(params.model || workflow.settings.model || "nano-banana-pro"),
|
|
|
|
|
ratio: String(params.aspectRatio || params.ratio || workflow.settings.ratio || "16:9"),
|
|
|
|
|
quality: String(params.imageSize || params.resolution || workflow.settings.resolution || "1K"),
|
|
|
|
|
gridMode: "single",
|
|
|
|
|
referenceUrls: referenceUrls.length ? referenceUrls : undefined,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|