feat(workflow): 工作流提供方 - 集成 RunningHub 与 ComfyUI Bridge

This commit is contained in:
yepwait
2026-08-26 15:02:24 +08:00
committed by ddCat
parent 6141bd688c
commit 86fc3fda2f
82 changed files with 11405 additions and 248 deletions

View File

@@ -10,9 +10,9 @@ import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor,
import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage";
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame";
import type { AiConfig } from "@/stores/use-config-store";
import { normalizeRunningHubCapability, type AiConfig } from "@/stores/use-config-store";
import { useUserStore } from "@/stores/use-user-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
type UseCanvasConnectionControllerOptions = {
projectId: string;
@@ -45,6 +45,20 @@ const CONNECTION_HANDLE_HIT_RADIUS = 40;
const CONNECTION_NODE_HIT_PADDING = 32;
const NODE_STATUS_IDLE = "idle" as const;
function selectRunningHubWorkflow(config: AiConfig) {
const capability = normalizeRunningHubCapability(config.runningHub.capability);
return config.runningHub.workflows.find((item) => item.workflowId.trim() === config.runningHub.workflowId.trim()
&& (item.kind === "app" ? "app" : "workflow") === config.runningHub.selectedKind)
|| config.runningHub.workflows.find((item) => normalizeRunningHubCapability(item.capability, capability) === capability)
|| config.runningHub.workflows[0];
}
function selectComfyBridgeWorkflow(config: AiConfig) {
return config.comfyBridge.workflows.find((item) => item.workflowId.trim() === config.comfyBridge.workflowId.trim())
|| config.comfyBridge.workflows.find((item) => item.capability === config.comfyBridge.capability)
|| config.comfyBridge.workflows[0];
}
export function useCanvasConnectionController({
projectId,
config,
@@ -174,8 +188,9 @@ export function useCanvasConnectionController({
setContextMenu(null);
}, [config, connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
if (type === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine, tldrawLicenseKey)) {
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config, pending: PendingConnectionCreate, workflowProvider?: "runninghub" | "comfyui") => {
const nodeType = type;
if (nodeType === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine, tldrawLicenseKey)) {
message.error("当前生产构建未配置 tldraw License Key,不能创建 tldraw 绘图");
return;
}
@@ -183,7 +198,7 @@ export function useCanvasConnectionController({
const batchSourceNodes = batchSourceNodeIds
.map((nodeId) => nodesRef.current.find((node) => node.id === nodeId))
.filter((node): node is CanvasNodeData => Boolean(node));
const storyboardRow = batchSourceNodeIds.length ? undefined : type === CanvasNodeType.Video ? storyboardRowFromHandle(nodesRef.current, pending.connection.nodeId, pending.connection.handleId) : undefined;
const storyboardRow = batchSourceNodeIds.length ? undefined : nodeType === CanvasNodeType.Video ? storyboardRowFromHandle(nodesRef.current, pending.connection.nodeId, pending.connection.handleId) : undefined;
const videoPrompt = storyboardRow ? (storyboardRow.videoMotionPrompt || storyboardRow.plotDescription).trim() : "";
const sourceNode = pending.connection.handleType === "source" ? nodesRef.current.find((node) => node.id === pending.connection.nodeId) : undefined;
const batchScriptPrompt = batchSourceNodes
@@ -191,18 +206,39 @@ export function useCanvasConnectionController({
.map((node) => (node.metadata?.content || node.metadata?.prompt || "").trim())
.filter(Boolean)
.join("\n\n");
const scriptPrompt = type === CanvasNodeType.Script
const scriptPrompt = nodeType === CanvasNodeType.Script
? batchSourceNodeIds.length ? batchScriptPrompt : sourceNode?.type === CanvasNodeType.Text ? (sourceNode.metadata?.content || sourceNode.metadata?.prompt || "").trim() : ""
: "";
const metadata = type === CanvasNodeType.Drawing
const selectedWorkflowProvider = nodeType === CanvasNodeType.Config
? workflowProvider || (config.runningHub.enabled && config.runningHub.workflows.length ? "runninghub" : config.comfyBridge.enabled && config.comfyBridge.workflows.length ? "comfyui" : "runninghub")
: undefined;
const runningHubWorkflow = selectedWorkflowProvider === "runninghub" ? selectRunningHubWorkflow(config) : undefined;
const comfyBridgeWorkflow = selectedWorkflowProvider === "comfyui" ? selectComfyBridgeWorkflow(config) : undefined;
const workflowCapability = selectedWorkflowProvider === "runninghub"
? normalizeRunningHubCapability(runningHubWorkflow?.capability, normalizeRunningHubCapability(config.runningHub.capability))
: comfyBridgeWorkflow?.capability || "image";
const metadata: CanvasNodeMetadata | undefined = nodeType === CanvasNodeType.Config
? {
generationMode: selectedWorkflowProvider ? workflowCapability === "video" ? "video" as const : workflowCapability === "audio" ? "audio" as const : "image" as const : "image" as const,
workflowProvider: selectedWorkflowProvider || "model",
status: NODE_STATUS_IDLE,
...(selectedWorkflowProvider === "runninghub" ? {
workflowTitle: "RunningHub 工作流",
...(runningHubWorkflow ? { runningHubWorkflowId: runningHubWorkflow.workflowId, runningHubWorkflowKind: runningHubWorkflow.kind === "app" ? "app" as const : "workflow" as const } : {}),
} : selectedWorkflowProvider === "comfyui" ? {
workflowTitle: "ComfyUI Bridge",
...(comfyBridgeWorkflow ? { comfyBridgeWorkflowId: comfyBridgeWorkflow.workflowId } : {}),
} : {})
}
: nodeType === CanvasNodeType.Drawing
? { drawingEngine: defaultDrawingEngine }
: type === CanvasNodeType.Script && scriptPrompt
: nodeType === CanvasNodeType.Script && scriptPrompt
? { prompt: scriptPrompt, composerContent: scriptPrompt }
: type === CanvasNodeType.Video && storyboardRow
: nodeType === CanvasNodeType.Video && storyboardRow
? { prompt: videoPrompt, composerContent: videoPrompt, ...storyboardPromptTemplateMetadata(storyboardRow, "video"), generationMode: "video" as const, videoEditOperation: "text_to_video" as const, workflowKind: "shot" as const, workflowTitle: `镜头 ${storyboardRow.shotNumber} 视频`, shotIndex: storyboardRow.shotNumber, seconds: String(storyboardRow.durationSeconds), status: NODE_STATUS_IDLE }
: undefined;
const sourceNodeForQuickCreate = pending.quick ? nodesRef.current.find((node) => node.id === pending.connection.nodeId) : undefined;
const spec = getNodeSpec(type);
const spec = getNodeSpec(nodeType);
const anchorY = sourceNodeForQuickCreate ? sourceNodeForQuickCreate.position.y + sourceNodeForQuickCreate.height * (pending.connection.anchorRatio ?? 0.5) : pending.position.y;
const position = sourceNodeForQuickCreate
? {
@@ -212,9 +248,10 @@ export function useCanvasConnectionController({
y: anchorY,
}
: pending.position;
const newNode = createCanvasNode(type, position, metadata);
const newNode = createCanvasNode(nodeType, position, metadata);
if (nodeType === CanvasNodeType.Config && selectedWorkflowProvider) newNode.title = selectedWorkflowProvider === "runninghub" ? "RunningHub 工作流" : "ComfyUI Bridge";
if (storyboardRow) newNode.title = `镜头 ${storyboardRow.shotNumber} · 视频`;
if (batchSourceNodeIds.length && type === CanvasNodeType.Drawing) {
if (batchSourceNodeIds.length && nodeType === CanvasNodeType.Drawing) {
message.error("批量连接暂不支持创建绘图,请先连接到普通节点");
closeConnectionCreateMenu();
return;
@@ -238,7 +275,7 @@ export function useCanvasConnectionController({
setConnections(nextConnections);
setSelectedNodeIds(new Set([newNode.id]));
setSelectedConnectionId(null);
if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
if (nodeType !== CanvasNodeType.Text && nodeType !== CanvasNodeType.Script && nodeType !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
const skippedCount = batchPlan.skipped.length;
const duplicateCount = batchPlan.duplicates.length;
const suffix = skippedCount || duplicateCount ? `,跳过 ${skippedCount + duplicateCount} 个` : "";
@@ -250,7 +287,7 @@ export function useCanvasConnectionController({
}
const connection = normalizeConnection(pending.connection.nodeId, newNode.id, [...nodesRef.current, newNode], pending.connection.handleType);
if (!connection) {
message.warning("配置节点之间不能连接");
message.warning("当前节点不能建立这条连线");
return;
}
const policyError = canvasConnectionError(config, [...nodesRef.current, newNode], connectionsRef.current, connection);
@@ -258,7 +295,7 @@ export function useCanvasConnectionController({
message.warning(policyError);
return;
}
if (type === CanvasNodeType.Drawing) {
if (nodeType === CanvasNodeType.Drawing) {
const drawingSourceNode = nodesRef.current.find((node) => node.id === pending.connection.nodeId);
const sourceUrl = drawingSourceNode?.type === CanvasNodeType.Image ? drawingSourceNode.metadata?.content : "";
if (pending.connection.handleType !== "source" || !drawingSourceNode || !sourceUrl || !newNode.metadata?.drawingId) {
@@ -297,21 +334,22 @@ export function useCanvasConnectionController({
setConnections((currentConnections) => [...currentConnections, { id: nanoid(), ...connected }]);
setSelectedNodeIds(new Set([newNode.id]));
setSelectedConnectionId(null);
if (type === CanvasNodeType.Drawing) setDrawingNodeId(newNode.id);
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
if (nodeType === CanvasNodeType.Drawing) setDrawingNodeId(newNode.id);
else if (nodeType !== CanvasNodeType.Text && nodeType !== CanvasNodeType.Script && nodeType !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
closeConnectionCreateMenu();
setConnecting(null);
}, [closeConnectionCreateMenu, config, connectionsRef, defaultDrawingEngine, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, tldrawLicenseKey]);
const getConnectionCreateDisabledReason = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
const getConnectionCreateDisabledReason = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing | CanvasNodeType.Config, pending: PendingConnectionCreate, workflowProvider?: "runninghub" | "comfyui") => {
const nodeType = type;
if (pending.batchSourceNodeIds?.length) {
if (type === CanvasNodeType.Drawing) return "批量连接暂不支持绘图";
const pendingNode: CanvasNodeData = { id: "__pending-connection-node__", type, title: "", position: pending.position, width: getNodeSpec(type).width, height: getNodeSpec(type).height };
if (nodeType === CanvasNodeType.Drawing || nodeType === CanvasNodeType.Config) return "批量连接暂不支持此节点类型";
const pendingNode: CanvasNodeData = { id: "__pending-connection-node__", type: nodeType, title: "", position: pending.position, width: getNodeSpec(nodeType).width, height: getNodeSpec(nodeType).height };
const plan = planBatchConnections({ sourceNodeIds: pending.batchSourceNodeIds, targetNodeId: pendingNode.id, nodes: [...nodesRef.current, pendingNode], connections: connectionsRef.current, config, allowCapacityOverflow: true });
return plan.connections.length ? "" : plan.skipped[0]?.reason || "当前选中的节点不能连接到此类型";
}
const spec = getNodeSpec(type);
const pendingNode: CanvasNodeData = { id: "__pending-connection-node__", type, title: "", position: pending.position, width: spec.width, height: spec.height };
const spec = getNodeSpec(nodeType);
const pendingNode: CanvasNodeData = { id: "__pending-connection-node__", type: nodeType, title: "", position: pending.position, width: spec.width, height: spec.height };
const pendingNodes = [...nodesRef.current, pendingNode];
const connection = normalizeConnection(pending.connection.nodeId, pendingNode.id, pendingNodes, pending.connection.handleType);
if (!connection) return "当前节点类型不能这样连接";