Compare commits
2 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
05ed81fa5a | 1 month ago |
|
|
3396cf3d62 | 1 month ago |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
export type CustomDefValue = Record<string, any>;
|
||||
|
||||
export const parseCustomDef = (
|
||||
value: unknown,
|
||||
fallback: CustomDefValue | null = {}
|
||||
): CustomDefValue | null => {
|
||||
if (!value) return fallback;
|
||||
if (typeof value === 'object') return value as CustomDefValue;
|
||||
if (typeof value !== 'string') return fallback;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' ? parsed : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
export const stringifyCustomDef = (value: unknown): string => {
|
||||
if (!value) return '{}';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
|
||||
export const getNodeCustomDef = (
|
||||
nodeData: { component?: { customDef?: unknown } } | undefined,
|
||||
fallback: CustomDefValue = {}
|
||||
) => parseCustomDef(nodeData?.component?.customDef, fallback);
|
||||
@ -0,0 +1,66 @@
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
|
||||
export type WorkflowMode = 'component' | 'application';
|
||||
|
||||
export type WorkflowPortKind = 'apiIn' | 'apiOut' | 'dataIn' | 'dataOut';
|
||||
|
||||
export type WorkflowLineType = 'api' | 'data' | 'convert';
|
||||
|
||||
export interface WorkflowPort {
|
||||
id?: string;
|
||||
name?: string;
|
||||
desc?: string;
|
||||
dataType?: string;
|
||||
defaultValue?: any;
|
||||
arrayType?: string | null;
|
||||
topic?: string;
|
||||
eventId?: string;
|
||||
eventName?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowPorts {
|
||||
apiIns: WorkflowPort[];
|
||||
apiOuts: WorkflowPort[];
|
||||
dataIns: WorkflowPort[];
|
||||
dataOuts: WorkflowPort[];
|
||||
}
|
||||
|
||||
export interface WorkflowComponentRef {
|
||||
type?: string;
|
||||
compId?: string;
|
||||
compIdentifier?: string;
|
||||
compInstanceIdentifier?: string;
|
||||
customDef?: Record<string, any> | string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface WorkflowNodeData {
|
||||
title: string;
|
||||
type: string;
|
||||
parameters: WorkflowPorts;
|
||||
component?: WorkflowComponentRef;
|
||||
compId?: string;
|
||||
status?: string;
|
||||
isStatusVisible?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type WorkflowNode = Node<WorkflowNodeData>;
|
||||
|
||||
export type WorkflowEdge = Edge<{
|
||||
lineType?: WorkflowLineType;
|
||||
displayData?: Record<string, any>;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
|
||||
export interface WorkflowGraph {
|
||||
id?: string;
|
||||
mode: WorkflowMode;
|
||||
nodes: WorkflowNode[];
|
||||
edges: WorkflowEdge[];
|
||||
}
|
||||
|
||||
export interface FlowConvertResult {
|
||||
nodes: WorkflowNode[];
|
||||
edges: WorkflowEdge[];
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
const hasObjectValues = (value: any) => {
|
||||
return Boolean(value && typeof value === 'object' && Object.keys(value).length > 0);
|
||||
};
|
||||
|
||||
const hasInitialCanvasData = (initialData: any, useDefault: boolean) => {
|
||||
if (!initialData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(initialData)) {
|
||||
return initialData.length > 0;
|
||||
}
|
||||
|
||||
if (useDefault) {
|
||||
return (
|
||||
hasObjectValues(initialData?.main?.components) ||
|
||||
hasObjectValues(initialData?.components)
|
||||
);
|
||||
}
|
||||
|
||||
return hasObjectValues(initialData);
|
||||
};
|
||||
|
||||
export const hasCanvasContent = (canvas: any) => {
|
||||
return Boolean(
|
||||
canvas &&
|
||||
((Array.isArray(canvas.nodes) && canvas.nodes.length > 0) ||
|
||||
(Array.isArray(canvas.edges) && canvas.edges.length > 0))
|
||||
);
|
||||
};
|
||||
|
||||
export const shouldUseCachedCanvas = (params: {
|
||||
cachedCanvas: any;
|
||||
initialData: any;
|
||||
useDefault: boolean;
|
||||
}) => {
|
||||
const { cachedCanvas, initialData, useDefault } = params;
|
||||
if (!cachedCanvas) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasCanvasContent(cachedCanvas)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !hasInitialCanvasData(initialData, useDefault);
|
||||
};
|
||||
|
||||
export const shouldPersistCanvas = (params: {
|
||||
nodes: any[];
|
||||
edges: any[];
|
||||
isRunning?: boolean;
|
||||
}) => {
|
||||
if (params.isRunning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasCanvasContent(params);
|
||||
};
|
||||
@ -0,0 +1,114 @@
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
|
||||
export interface CopiedFlowData {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
export const buildCopiedFlowData = (
|
||||
activeNode: Node,
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
appId?: string
|
||||
): CopiedFlowData | null => {
|
||||
const selectedNodes = nodes.filter(
|
||||
(node) => node.selected || node.id === activeNode.id
|
||||
);
|
||||
const nodesToCopy = selectedNodes.filter(
|
||||
(node) => node.type !== 'start' && node.type !== 'end'
|
||||
);
|
||||
|
||||
if (nodesToCopy.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeIds = new Set(nodesToCopy.map((node) => node.id));
|
||||
const edgesToCopy = edges.filter(
|
||||
(edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)
|
||||
);
|
||||
|
||||
return {
|
||||
nodes: nodesToCopy.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
})),
|
||||
edges: edgesToCopy.map((edge) => ({
|
||||
...edge,
|
||||
selected: false,
|
||||
})),
|
||||
appId,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildPastedFlowData = (
|
||||
copiedData: CopiedFlowData,
|
||||
position: { x: number; y: number },
|
||||
timestamp = Date.now()
|
||||
) => {
|
||||
const copiedNodes = copiedData.nodes || [];
|
||||
const copiedEdges = copiedData.edges || [];
|
||||
|
||||
if (copiedNodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const minX = Math.min(...copiedNodes.map((node) => node.position.x));
|
||||
const minY = Math.min(...copiedNodes.map((node) => node.position.y));
|
||||
const maxX = Math.max(...copiedNodes.map((node) => node.position.x));
|
||||
const maxY = Math.max(...copiedNodes.map((node) => node.position.y));
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerY = (minY + maxY) / 2;
|
||||
const offsetX = position.x - centerX;
|
||||
const offsetY = position.y - centerY;
|
||||
const idMap = new Map<string, string>();
|
||||
|
||||
const nodes = copiedNodes.map((node, index) => {
|
||||
const newId = `${node.type}-${timestamp}-${index}`;
|
||||
idMap.set(node.id, newId);
|
||||
|
||||
return {
|
||||
...node,
|
||||
id: newId,
|
||||
position: {
|
||||
x: node.position.x + offsetX,
|
||||
y: node.position.y + offsetY,
|
||||
},
|
||||
selected: false,
|
||||
dragging: false,
|
||||
appId: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const edges = copiedEdges
|
||||
.map((edge, index) => {
|
||||
const source = idMap.get(edge.source);
|
||||
const target = idMap.get(edge.target);
|
||||
if (!source || !target) return null;
|
||||
|
||||
return {
|
||||
...edge,
|
||||
id: `e${source}-${target}-${timestamp}-${index}`,
|
||||
source,
|
||||
target,
|
||||
selected: false,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as Edge[];
|
||||
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
||||
export const buildPastedSingleNode = (
|
||||
copiedNode: Node,
|
||||
position: { x: number; y: number },
|
||||
timestamp = Date.now()
|
||||
) => ({
|
||||
...copiedNode,
|
||||
id: `${copiedNode.type}-${timestamp}`,
|
||||
position,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
appId: undefined,
|
||||
});
|
||||
@ -0,0 +1,139 @@
|
||||
import { Connection, Edge, Node } from '@xyflow/react';
|
||||
import { getHandleType, validateDataType } from '@/utils/flowCommon';
|
||||
|
||||
export interface ConnectionValidationResult {
|
||||
isValid: boolean;
|
||||
lineType?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const validateWorkflowConnection = (
|
||||
nodes: Node[],
|
||||
connection: Connection
|
||||
): ConnectionValidationResult => {
|
||||
const sourceNode = nodes.find((node) => node.id === connection.source);
|
||||
const targetNode = nodes.find((node) => node.id === connection.target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
if (sourceNode.id === targetNode.id) {
|
||||
return { isValid: false, message: '不允许自旋链接' };
|
||||
}
|
||||
|
||||
const sourceParams: any = sourceNode.data?.parameters || {};
|
||||
const targetParams: any = targetNode.data?.parameters || {};
|
||||
const sourceHandleType = getHandleType(connection.sourceHandle, sourceParams);
|
||||
const targetHandleType = getHandleType(connection.targetHandle, targetParams);
|
||||
|
||||
if (sourceHandleType !== targetHandleType) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `连接类型不匹配: ${sourceHandleType}, ${targetHandleType}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!validateDataType(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
connection.sourceHandle,
|
||||
connection.targetHandle
|
||||
)
|
||||
) {
|
||||
return { isValid: false, message: '数据类型不匹配' };
|
||||
}
|
||||
|
||||
return { isValid: true, lineType: sourceHandleType };
|
||||
};
|
||||
|
||||
export const buildWorkflowConnectionEdge = (
|
||||
nodes: Node[],
|
||||
connection: Connection,
|
||||
lineType: string
|
||||
) => {
|
||||
const sourceNode = nodes.find((node) => node.id === connection.source);
|
||||
const targetNode = nodes.find((node) => node.id === connection.target);
|
||||
if (!sourceNode || !targetNode) return null;
|
||||
|
||||
const sourceParams: any = sourceNode.data?.parameters || {};
|
||||
const targetParams: any = targetNode.data?.parameters || {};
|
||||
|
||||
if (lineType === 'data') {
|
||||
const sourceDataOut = (sourceParams.dataOuts || []).find(
|
||||
(dataOut: any) =>
|
||||
dataOut.name === connection.sourceHandle ||
|
||||
dataOut.id === connection.sourceHandle
|
||||
);
|
||||
const targetDataIn = (targetParams.dataIns || []).find(
|
||||
(dataIn: any) =>
|
||||
dataIn.name === connection.targetHandle ||
|
||||
dataIn.id === connection.targetHandle
|
||||
);
|
||||
|
||||
if (
|
||||
sourceDataOut &&
|
||||
targetDataIn &&
|
||||
sourceDataOut.dataType !== targetDataIn.dataType
|
||||
) {
|
||||
return {
|
||||
edge: null,
|
||||
message: `数据类型不匹配,源节点数据类型: ${sourceDataOut.dataType},目标节点数据类型: ${targetDataIn.dataType}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const edgeParams: Edge = {
|
||||
...connection,
|
||||
id:
|
||||
connection.source && connection.target
|
||||
? `e${connection.source}-${connection.target}-${connection.sourceHandle}-${connection.targetHandle}`
|
||||
: '',
|
||||
type: 'custom',
|
||||
data: {
|
||||
...(connection as any).data,
|
||||
lineType,
|
||||
},
|
||||
} as Edge;
|
||||
|
||||
const sourceApi = (sourceParams.apiOuts || []).find(
|
||||
(api: any) =>
|
||||
(api?.eventId || api.name || api.id) === connection.sourceHandle
|
||||
);
|
||||
const targetApi = (targetParams.apiIns || []).find(
|
||||
(api: any) =>
|
||||
(api?.eventId || api.name || api.id) === connection.targetHandle
|
||||
);
|
||||
|
||||
if (sourceApi?.topic) {
|
||||
if (
|
||||
!targetApi ||
|
||||
!targetApi.topic ||
|
||||
targetApi.topic.includes('**empty**') ||
|
||||
!sourceApi.topic.includes('**empty**')
|
||||
) {
|
||||
edgeParams.data = {
|
||||
...edgeParams.data,
|
||||
lineType: 'api',
|
||||
displayData: {
|
||||
name: sourceApi.eventName,
|
||||
eventId: sourceApi.eventId,
|
||||
topic: sourceApi.topic,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (targetApi?.topic && !targetApi.topic.includes('**empty**')) {
|
||||
edgeParams.data = {
|
||||
...edgeParams.data,
|
||||
lineType: 'api',
|
||||
displayData: {
|
||||
name: targetApi.eventName,
|
||||
eventId: targetApi.eventId,
|
||||
topic: targetApi.topic,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { edge: edgeParams };
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
import { parseCustomDef } from '@/features/workflow/domain/customDef';
|
||||
|
||||
export const isLoopBoundaryNode = (node: Node) => {
|
||||
return node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END';
|
||||
};
|
||||
|
||||
export const resolveDeletedNodesWithLoopPairs = (
|
||||
deletedNodes: Node[],
|
||||
allNodes: Node[]
|
||||
) => {
|
||||
const nodesToRemove = [...deletedNodes];
|
||||
|
||||
deletedNodes.filter(isLoopBoundaryNode).forEach((loopNode) => {
|
||||
const component = loopNode.data?.component as { customDef?: unknown } | undefined;
|
||||
const customDef = parseCustomDef(component?.customDef) || {};
|
||||
const relatedNodeId =
|
||||
loopNode.data?.type === 'LOOP_START'
|
||||
? customDef.loopEndNodeId
|
||||
: customDef.loopStartNodeId;
|
||||
|
||||
if (!relatedNodeId) return;
|
||||
|
||||
const relatedNode = allNodes.find((node) => node.id === relatedNodeId);
|
||||
if (relatedNode) {
|
||||
nodesToRemove.push(relatedNode);
|
||||
}
|
||||
});
|
||||
|
||||
return nodesToRemove.filter(
|
||||
(node, index, self) => index === self.findIndex((item) => item.id === node.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const removeNodesAndConnectedEdges = (
|
||||
allNodes: Node[],
|
||||
allEdges: Edge[],
|
||||
nodesToRemove: Node[]
|
||||
) => {
|
||||
const nodeIdsToRemove = new Set(nodesToRemove.map((node) => node.id));
|
||||
|
||||
return {
|
||||
nodes: allNodes.filter((node) => !nodeIdsToRemove.has(node.id)),
|
||||
edges: allEdges.filter(
|
||||
(edge) =>
|
||||
!nodeIdsToRemove.has(edge.source) && !nodeIdsToRemove.has(edge.target)
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { Edge } from '@xyflow/react';
|
||||
|
||||
export const resolveInsertedNodeHandles = (node: any) => {
|
||||
let sourceHandle = 'done';
|
||||
let targetHandle = 'start';
|
||||
|
||||
if (node?.data?.parameters) {
|
||||
const { apiOuts, apiIns } = node.data.parameters;
|
||||
|
||||
if (apiOuts && apiOuts.length > 0) {
|
||||
sourceHandle = apiOuts[0].name || apiOuts[0].id || sourceHandle;
|
||||
}
|
||||
|
||||
if (apiIns && apiIns.length > 0) {
|
||||
targetHandle = apiIns[0].name || apiIns[0].id || targetHandle;
|
||||
}
|
||||
}
|
||||
|
||||
return { sourceHandle, targetHandle };
|
||||
};
|
||||
|
||||
export const buildInsertedNodeEdges = (params: {
|
||||
baseEdges: Edge[];
|
||||
removedEdgeId: string;
|
||||
sourceId: string;
|
||||
sourceHandle?: string;
|
||||
targetId: string;
|
||||
targetHandle?: string;
|
||||
insertedNodeId: string;
|
||||
insertedNodeSourceHandle: string;
|
||||
insertedNodeTargetHandle: string;
|
||||
}): Edge[] => [
|
||||
...params.baseEdges.filter((e) => e.id !== params.removedEdgeId),
|
||||
{
|
||||
id: `e${params.sourceId}-${params.insertedNodeId}`,
|
||||
source: params.sourceId,
|
||||
target: params.insertedNodeId,
|
||||
sourceHandle: params.sourceHandle,
|
||||
targetHandle: params.insertedNodeTargetHandle,
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
{
|
||||
id: `e${params.insertedNodeId}-${params.targetId}`,
|
||||
source: params.insertedNodeId,
|
||||
target: params.targetId,
|
||||
sourceHandle: params.insertedNodeSourceHandle,
|
||||
targetHandle: params.targetHandle,
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
];
|
||||
@ -0,0 +1,104 @@
|
||||
import { Edge } from '@xyflow/react';
|
||||
|
||||
export const createLoopNodePair = (position: { x: number; y: number }) => {
|
||||
const loopStartNode: any = {
|
||||
id: `LOOP_START-${Date.now()}`,
|
||||
type: 'LOOP',
|
||||
position: { x: position.x, y: position.y },
|
||||
data: {
|
||||
title: '循环开始',
|
||||
type: 'LOOP_START',
|
||||
parameters: {
|
||||
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
|
||||
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
|
||||
dataIns: [],
|
||||
dataOuts: [],
|
||||
},
|
||||
component: {},
|
||||
},
|
||||
};
|
||||
|
||||
const loopEndNode: any = {
|
||||
id: `LOOP_END-${Date.now()}`,
|
||||
type: 'LOOP',
|
||||
position: { x: position.x + 400, y: position.y },
|
||||
data: {
|
||||
title: '循环结束',
|
||||
type: 'LOOP_END',
|
||||
parameters: {
|
||||
apiIns: [
|
||||
{ name: 'continue', desc: '', dataType: '', defaultValue: '' },
|
||||
],
|
||||
apiOuts: [{ name: 'break', desc: '', dataType: '', defaultValue: '' }],
|
||||
dataIns: [
|
||||
{
|
||||
arrayType: null,
|
||||
dataType: 'INTEGER',
|
||||
defaultValue: 10,
|
||||
desc: '最大循环次数',
|
||||
id: 'maxTime',
|
||||
},
|
||||
],
|
||||
dataOuts: [],
|
||||
},
|
||||
component: {
|
||||
type: 'LOOP_END',
|
||||
customDef: JSON.stringify({
|
||||
apiOutIds: ['continue', 'break'],
|
||||
conditions: [],
|
||||
loopStartNodeId: loopStartNode.id,
|
||||
}),
|
||||
loopStartNodeId: loopStartNode.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
loopStartNode.data.component = {
|
||||
type: 'LOOP_START',
|
||||
customDef: JSON.stringify({ loopEndNodeId: loopEndNode.id }),
|
||||
};
|
||||
|
||||
return { loopStartNode, loopEndNode };
|
||||
};
|
||||
|
||||
export const createLoopGroupEdge = (
|
||||
loopStartId: string,
|
||||
loopEndId: string
|
||||
): Edge => ({
|
||||
id: `${loopStartId}-${loopEndId}-group`,
|
||||
source: loopStartId,
|
||||
target: loopEndId,
|
||||
sourceHandle: `${loopStartId}-group`,
|
||||
targetHandle: `${loopEndId}-group`,
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
export const createLoopInsertConnectionEdges = (params: {
|
||||
sourceId: string;
|
||||
sourceHandle: string;
|
||||
targetId: string;
|
||||
targetHandle: string;
|
||||
loopStartId: string;
|
||||
loopEndId: string;
|
||||
}): Edge[] => [
|
||||
{
|
||||
id: `e${params.sourceId}-${params.loopStartId}`,
|
||||
source: params.sourceId,
|
||||
target: params.loopStartId,
|
||||
sourceHandle: params.sourceHandle,
|
||||
targetHandle: 'start',
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
{
|
||||
id: `e${params.loopEndId}-${params.targetId}`,
|
||||
source: params.loopEndId,
|
||||
target: params.targetId,
|
||||
sourceHandle: 'break',
|
||||
targetHandle: params.targetHandle,
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
];
|
||||
@ -0,0 +1,79 @@
|
||||
import { Node } from '@xyflow/react';
|
||||
|
||||
interface FlowNodeDefinition {
|
||||
nodeName: string;
|
||||
data: any;
|
||||
id?: string;
|
||||
flowHousVO?: {
|
||||
id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type EventIdMode = 'id' | 'eventIdOptional';
|
||||
|
||||
export const createFlowNode = (
|
||||
nodeType: string,
|
||||
nodeDefinition: FlowNodeDefinition,
|
||||
position: { x: number; y: number }
|
||||
): Node => {
|
||||
const node: any = {
|
||||
id: `${nodeType}-${Date.now()}`,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {
|
||||
...nodeDefinition.data,
|
||||
title: nodeDefinition.nodeName,
|
||||
type: nodeType,
|
||||
},
|
||||
};
|
||||
|
||||
if (nodeDefinition.id || nodeDefinition?.flowHousVO?.id) {
|
||||
node.data.compId = nodeDefinition.id || nodeDefinition?.flowHousVO?.id;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
export const attachFlowNodeComponent = (
|
||||
node: any,
|
||||
nodeType: string,
|
||||
nodeDefinition: FlowNodeDefinition,
|
||||
eventList: any[],
|
||||
eventIdMode: EventIdMode
|
||||
) => {
|
||||
if (nodeType === 'SWITCH') {
|
||||
node.data.component = {
|
||||
customDef: JSON.stringify({
|
||||
apiOutIds: ['default'],
|
||||
conditions: [],
|
||||
}),
|
||||
};
|
||||
} else if (nodeType === 'SUB') {
|
||||
node.data.component = {
|
||||
type: nodeType,
|
||||
compId: node.data.compId,
|
||||
};
|
||||
} else if (nodeType === 'EVENTSEND' || nodeType === 'EVENTLISTENE') {
|
||||
const emptyEvent = eventList.find((item) =>
|
||||
item.topic.includes('**empty**')
|
||||
);
|
||||
node.data.component = {
|
||||
type: nodeType,
|
||||
customDef: {
|
||||
eventId:
|
||||
eventIdMode === 'eventIdOptional'
|
||||
? emptyEvent?.eventId ?? null
|
||||
: emptyEvent.id,
|
||||
name: emptyEvent.name,
|
||||
topic: emptyEvent.topic,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
node.data.component = {
|
||||
type: nodeType,
|
||||
compId: nodeDefinition.id,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import {
|
||||
createFlowNode,
|
||||
attachFlowNodeComponent,
|
||||
} from '@/features/workflow/operations/nodeFactory';
|
||||
import {
|
||||
ensureNodeTypeRegistered,
|
||||
resolveNodeComponent,
|
||||
} from '@/features/workflow/registry/nodeRegistry';
|
||||
|
||||
type EventIdMode = 'id' | 'eventIdOptional';
|
||||
|
||||
export const resolveNodeDefinition = (
|
||||
nodeList: any[],
|
||||
nodeType: string,
|
||||
fallbackNode?: any
|
||||
) => {
|
||||
return nodeList.find((n) => n.nodeType === nodeType) || fallbackNode || null;
|
||||
};
|
||||
|
||||
export const buildRuntimeNode = (params: {
|
||||
nodeType: string;
|
||||
nodeDefinition: any;
|
||||
position: { x: number; y: number };
|
||||
eventList: any[];
|
||||
eventIdMode: EventIdMode;
|
||||
}) => {
|
||||
const { nodeType, nodeDefinition, position, eventList, eventIdMode } = params;
|
||||
|
||||
const newNode = attachFlowNodeComponent(
|
||||
createFlowNode(nodeType, nodeDefinition, position),
|
||||
nodeType,
|
||||
nodeDefinition,
|
||||
eventList,
|
||||
eventIdMode
|
||||
);
|
||||
|
||||
ensureNodeTypeRegistered(
|
||||
nodeType,
|
||||
nodeDefinition.nodeName,
|
||||
resolveNodeComponent(nodeType)
|
||||
);
|
||||
|
||||
return newNode;
|
||||
};
|
||||
|
||||
export const buildDroppedNode = (
|
||||
nodeData: any,
|
||||
position: { x: number; y: number }
|
||||
) => {
|
||||
ensureNodeTypeRegistered(nodeData.nodeType, nodeData.nodeName);
|
||||
return {
|
||||
id: `${nodeData.nodeType}-${Date.now()}`,
|
||||
type: nodeData.nodeType,
|
||||
position,
|
||||
data: {
|
||||
...nodeData.data,
|
||||
title: nodeData.nodeName,
|
||||
type: nodeData.nodeType,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
|
||||
interface FlowSnapshotDetail {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
export const dispatchFlowSnapshot = (detail: FlowSnapshotDetail) => {
|
||||
const event = new CustomEvent('takeSnapshot', {
|
||||
detail,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export const dispatchFlowSnapshotAsync = (
|
||||
detail: FlowSnapshotDetail,
|
||||
delay = 0
|
||||
) => {
|
||||
return setTimeout(() => {
|
||||
dispatchFlowSnapshot(detail);
|
||||
}, delay);
|
||||
};
|
||||
@ -0,0 +1,266 @@
|
||||
import React from 'react';
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
import { Message } from '@arco-design/web-react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { getAppInfoNew, setMainFlowNew, setSubFlowNew } from '@/api/appRes';
|
||||
import {
|
||||
updateAppEventChannel,
|
||||
updateAppFlowData,
|
||||
} from '@/api/appEvent';
|
||||
import {
|
||||
deleteEventPub,
|
||||
deleteEventSub,
|
||||
queryEventItemBySceneIdOld,
|
||||
} from '@/api/event';
|
||||
import {
|
||||
updateCanvasDataMap,
|
||||
updateEventListOld,
|
||||
updateFlowData,
|
||||
} from '@/store/ideContainer';
|
||||
import store from '@/store';
|
||||
import {
|
||||
convertFlowData,
|
||||
reverseConvertFlowData,
|
||||
revertFlowData,
|
||||
} from '@/features/workflow/adapters/serverFlowAdapter';
|
||||
import {
|
||||
validateAllEdges,
|
||||
validateAllNodes,
|
||||
showValidationErrors,
|
||||
} from '@/components/FlowEditor/nodeEditors/validators/nodeValidators';
|
||||
import {
|
||||
handelEventNodeList,
|
||||
updateEvent,
|
||||
upDatePublish,
|
||||
} from '@/pages/flowEditor/utils/common';
|
||||
import { sleep } from '@/utils/common';
|
||||
|
||||
interface SaveWorkflowDataParams {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
useDefault: boolean;
|
||||
initialData: any;
|
||||
canvasDataMap: any;
|
||||
dispatch: Dispatch<any>;
|
||||
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
|
||||
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
|
||||
}
|
||||
|
||||
export const saveWorkflowData = async ({
|
||||
nodes,
|
||||
edges,
|
||||
useDefault,
|
||||
initialData,
|
||||
canvasDataMap,
|
||||
dispatch,
|
||||
setNodes,
|
||||
setEdges,
|
||||
}: SaveWorkflowDataParams) => {
|
||||
if (useDefault) {
|
||||
try {
|
||||
const nodeValidation = validateAllNodes(nodes);
|
||||
if (!nodeValidation.isValid) {
|
||||
showValidationErrors(nodeValidation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const edgeValidation = validateAllEdges(edges, nodes);
|
||||
if (!edgeValidation.isValid) {
|
||||
showValidationErrors(edgeValidation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const revertedData = revertFlowData(nodes, edges);
|
||||
const upDatePublishCB = await upDatePublish(revertedData.nodeConfigs);
|
||||
const newRevertedData = reverseConvertFlowData(
|
||||
nodes,
|
||||
edges,
|
||||
upDatePublishCB
|
||||
);
|
||||
const { flowData, currentAppData, info } =
|
||||
store.getState().ideContainer;
|
||||
const { deleteEventSendNodeList, deleteEventlisteneList } =
|
||||
handelEventNodeList(newRevertedData);
|
||||
|
||||
if (currentAppData.key.includes('sub')) {
|
||||
const appEventDefinition = updateEvent(
|
||||
revertedData.nodeConfigs,
|
||||
initialData.appId
|
||||
);
|
||||
const params = {
|
||||
...(currentAppData?.compData || {}),
|
||||
components: newRevertedData,
|
||||
appEventDefinition,
|
||||
sceneId: info.id,
|
||||
};
|
||||
const res: any = await setSubFlowNew(
|
||||
params,
|
||||
currentAppData.parentAppId
|
||||
);
|
||||
if (res.code === 200) {
|
||||
Message.success('保存成功');
|
||||
|
||||
const res1: any = await queryEventItemBySceneIdOld(info.id);
|
||||
if (res1.code === 200) dispatch(updateEventListOld(res1.data));
|
||||
|
||||
const appRes: any = await getAppInfoNew(currentAppData.parentAppId);
|
||||
dispatch(
|
||||
updateFlowData({ [currentAppData.parentAppId]: appRes.data })
|
||||
);
|
||||
|
||||
if (appRes.data.main?.components) {
|
||||
const { nodes: parentNodes, edges: parentEdges } =
|
||||
convertFlowData(appRes.data.main.components, true);
|
||||
dispatch(
|
||||
updateCanvasDataMap({
|
||||
...canvasDataMap,
|
||||
[currentAppData.parentAppId]: {
|
||||
nodes: parentNodes,
|
||||
edges: parentEdges,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateCanvasDataMap({
|
||||
...canvasDataMap,
|
||||
[currentAppData.key]: { nodes, edges },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
Message.error(res.message);
|
||||
}
|
||||
} else {
|
||||
const appEventDefinition = updateEvent(
|
||||
revertedData.nodeConfigs,
|
||||
initialData.appId
|
||||
);
|
||||
const params = {
|
||||
...(flowData[currentAppData.id]?.main || {}),
|
||||
components: newRevertedData,
|
||||
appEventDefinition,
|
||||
sceneId: info.id,
|
||||
};
|
||||
|
||||
const res: any = await setMainFlowNew(params, initialData.appId);
|
||||
if (res.code === 200) {
|
||||
Message.success('保存成功');
|
||||
|
||||
const res1: any = await queryEventItemBySceneIdOld(info.id);
|
||||
if (res1.code === 200) dispatch(updateEventListOld(res1.data));
|
||||
|
||||
dispatch(
|
||||
updateCanvasDataMap({
|
||||
...canvasDataMap,
|
||||
[currentAppData.id]: { nodes, edges },
|
||||
})
|
||||
);
|
||||
|
||||
const appRes: any = await getAppInfoNew(currentAppData.id);
|
||||
dispatch(updateFlowData({ [currentAppData.id]: appRes.data }));
|
||||
|
||||
if (appRes.data.main?.components) {
|
||||
const { nodes, edges } = convertFlowData(
|
||||
appRes.data.main.components,
|
||||
true
|
||||
);
|
||||
setNodes(nodes);
|
||||
setEdges(edges);
|
||||
dispatch(
|
||||
updateCanvasDataMap({
|
||||
...canvasDataMap,
|
||||
[currentAppData.id]: { nodes, edges },
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
deleteEventSendNodeList.length > 0 ||
|
||||
deleteEventlisteneList.length > 0
|
||||
) {
|
||||
deleteEventSendNodeList.length > 0 &&
|
||||
deleteEventPub({
|
||||
appId: currentAppData.id,
|
||||
topics: deleteEventSendNodeList,
|
||||
});
|
||||
deleteEventlisteneList.length > 0 &&
|
||||
deleteEventSub({
|
||||
appId: currentAppData.id,
|
||||
topics: deleteEventlisteneList,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving flow data:', error);
|
||||
Message.error('保存失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const appFlowParams: any = {
|
||||
appEventList: {},
|
||||
eventEdges: [],
|
||||
};
|
||||
|
||||
nodes.forEach((node) => {
|
||||
appFlowParams.appEventList[node.id] = {
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
};
|
||||
});
|
||||
|
||||
const eventMap = new Map();
|
||||
edges.forEach((edge: any) => {
|
||||
appFlowParams.eventEdges.push({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
lineType: 'data',
|
||||
data: {
|
||||
displayData: {
|
||||
...edge.data.displayData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sourceId = edge.sourceHandle;
|
||||
const targetId = edge.targetHandle;
|
||||
const topic = edge.data.displayData?.topic;
|
||||
|
||||
if (eventMap.has(topic)) {
|
||||
eventMap.get(topic).eventId.push(sourceId);
|
||||
eventMap.get(topic).eventId.push(targetId);
|
||||
} else {
|
||||
eventMap.set(topic, {
|
||||
eventId: [sourceId, targetId],
|
||||
topic,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const appEventParams = Array.from(eventMap.values()).map((item) => ({
|
||||
...item,
|
||||
eventId: Array.from(new Set(item.eventId)),
|
||||
}));
|
||||
|
||||
try {
|
||||
updateAppFlowData(appFlowParams);
|
||||
if (appEventParams.length > 0) {
|
||||
for (const item of appEventParams) {
|
||||
if (item.topic) {
|
||||
await sleep(500);
|
||||
await updateAppEventChannel(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message.success('保存成功');
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
Message.error('保存失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
|
||||
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
|
||||
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
|
||||
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
|
||||
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
|
||||
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
|
||||
import AppNode from '@/components/FlowEditor/node/appNode/AppNode';
|
||||
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
|
||||
import MicrophoneNode from '@/components/FlowEditor/node/microphoneNode/MicrophoneNode';
|
||||
|
||||
export interface NodeDescriptor {
|
||||
kind: string;
|
||||
label: string;
|
||||
render: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export const nodeDescriptors: Record<string, NodeDescriptor> = {
|
||||
BASIC: { kind: 'BASIC', label: '基础节点', render: BasicNode },
|
||||
BASIC_LOOP: { kind: 'BASIC_LOOP', label: '基础节点', render: BasicNode },
|
||||
SUB: { kind: 'SUB', label: '复合节点', render: BasicNode },
|
||||
APP: { kind: 'APP', label: '应用节点', render: AppNode },
|
||||
CODE: { kind: 'CODE', label: '代码节点', render: CodeNode },
|
||||
IMAGE: { kind: 'IMAGE', label: '图片节点', render: ImageNode },
|
||||
REST: { kind: 'REST', label: 'REST节点', render: RestNode },
|
||||
SWITCH: { kind: 'SWITCH', label: '条件节点', render: SwitchNode },
|
||||
LOOP: { kind: 'LOOP', label: '循环节点', render: LoopNode },
|
||||
MICRO: { kind: 'MICRO', label: '语音节点', render: MicrophoneNode },
|
||||
LOCAL: { kind: 'LOCAL', label: '本地节点', render: LocalNode },
|
||||
};
|
||||
|
||||
export const getNodeDescriptor = (nodeType: string) => {
|
||||
return nodeDescriptors[nodeType] || nodeDescriptors.LOCAL;
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
|
||||
import { getNodeDescriptor } from '@/features/workflow/registry/nodeDescriptors';
|
||||
|
||||
export const resolveNodeComponent = (nodeType: string) => {
|
||||
return getNodeDescriptor(nodeType).render;
|
||||
};
|
||||
|
||||
export const ensureNodeTypeRegistered = (
|
||||
nodeType: string,
|
||||
nodeName: string,
|
||||
component?: any
|
||||
) => {
|
||||
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
|
||||
if (!nodeMap.includes(nodeType)) {
|
||||
registerNodeType(
|
||||
nodeType,
|
||||
component || resolveNodeComponent(nodeType),
|
||||
nodeName
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
export interface FlowCurrentAppData {
|
||||
id?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface AppRuntimeState {
|
||||
nodeStatusMap: Record<string, string>;
|
||||
isRunning: boolean;
|
||||
isPaused: boolean;
|
||||
logs: any[];
|
||||
runId: string;
|
||||
eventSendNodeList: any[];
|
||||
eventlisteneList: any[];
|
||||
}
|
||||
|
||||
export const getCurrentAppKey = (
|
||||
currentAppData: FlowCurrentAppData | null | undefined
|
||||
) => {
|
||||
if (!currentAppData) return null;
|
||||
if (currentAppData.key && currentAppData.key.includes('sub')) {
|
||||
return currentAppData.key;
|
||||
}
|
||||
return currentAppData.id || null;
|
||||
};
|
||||
|
||||
export const createDefaultAppRuntimeState = (): AppRuntimeState => ({
|
||||
nodeStatusMap: {},
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
logs: [],
|
||||
runId: '',
|
||||
eventSendNodeList: [],
|
||||
eventlisteneList: [],
|
||||
});
|
||||
@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
import { Edge } from '@xyflow/react';
|
||||
import { Message } from '@arco-design/web-react';
|
||||
import { Dispatch } from 'redux';
|
||||
import {
|
||||
pauseApp,
|
||||
reRunApp,
|
||||
reconnectRun,
|
||||
resumeApp,
|
||||
runMainFlow,
|
||||
runSubFlow,
|
||||
stopApp,
|
||||
} from '@/api/apps';
|
||||
import {
|
||||
clearRuntimeLogs,
|
||||
resetNodeStatus,
|
||||
updateIsPaused,
|
||||
updateIsRunning,
|
||||
updateRuntimeId,
|
||||
} from '@/store/ideContainer';
|
||||
import store from '@/store';
|
||||
|
||||
interface FlowRuntimeActionParams {
|
||||
dispatch: Dispatch<any>;
|
||||
getCurrentFlowAppKey: () => string | null;
|
||||
}
|
||||
|
||||
interface RunWorkflowParams extends FlowRuntimeActionParams {
|
||||
running: boolean;
|
||||
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
|
||||
}
|
||||
|
||||
const setEdgesRunningState = (
|
||||
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>,
|
||||
isRunning: boolean
|
||||
) => {
|
||||
setEdges((edges) =>
|
||||
edges.map((edge) => ({
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
isRunning,
|
||||
animationProgress: 0,
|
||||
},
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
export const runWorkflow = async ({
|
||||
running,
|
||||
dispatch,
|
||||
getCurrentFlowAppKey,
|
||||
setEdges,
|
||||
}: RunWorkflowParams) => {
|
||||
const { currentAppData, socketId, appRuntimeData } =
|
||||
store.getState().ideContainer;
|
||||
const appKey = getCurrentFlowAppKey();
|
||||
|
||||
if (running) {
|
||||
const isSubFlow = currentAppData.key.includes('sub');
|
||||
const res: any = isSubFlow
|
||||
? await runSubFlow({
|
||||
appId: currentAppData.parentAppId,
|
||||
socketId,
|
||||
subflowId: currentAppData.key,
|
||||
})
|
||||
: await runMainFlow({
|
||||
appId: currentAppData.id,
|
||||
socketId,
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
dispatch(updateIsRunning(true));
|
||||
dispatch(resetNodeStatus());
|
||||
dispatch(updateRuntimeId(res.data));
|
||||
setEdgesRunningState(setEdges, true);
|
||||
} else {
|
||||
Message.error(res.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(updateIsRunning(false));
|
||||
|
||||
const runId =
|
||||
appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : '';
|
||||
if (runId) {
|
||||
await stopApp(runId);
|
||||
} else {
|
||||
await stopApp(currentAppData.instanceId);
|
||||
document.dispatchEvent(new CustomEvent('refreshAppList'));
|
||||
}
|
||||
|
||||
dispatch(resetNodeStatus());
|
||||
dispatch(updateRuntimeId(''));
|
||||
setEdgesRunningState(setEdges, false);
|
||||
|
||||
if (appKey) {
|
||||
dispatch(clearRuntimeLogs({ appId: appKey }));
|
||||
}
|
||||
};
|
||||
|
||||
export const pauseWorkflow = async ({
|
||||
isPaused,
|
||||
dispatch,
|
||||
getCurrentFlowAppKey,
|
||||
}: FlowRuntimeActionParams & { isPaused: boolean }) => {
|
||||
const { currentAppData, appRuntimeData } = store.getState().ideContainer;
|
||||
const appKey = getCurrentFlowAppKey();
|
||||
|
||||
if (!currentAppData) {
|
||||
Message.warning('请先选择一个应用');
|
||||
return;
|
||||
}
|
||||
|
||||
const runId =
|
||||
appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : '';
|
||||
|
||||
if (!runId) {
|
||||
Message.warning('应用未运行');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = isPaused
|
||||
? await resumeApp({ id: runId })
|
||||
: await pauseApp({ id: runId });
|
||||
|
||||
if (res.code === 200) {
|
||||
Message.success(isPaused ? '应用已恢复' : '应用已暂停');
|
||||
dispatch(updateIsPaused(!isPaused));
|
||||
} else {
|
||||
Message.error(res.msg || (isPaused ? '恢复失败' : '暂停失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('暂停/恢复失败:', error);
|
||||
Message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
export const rerunWorkflow = async ({
|
||||
dispatch,
|
||||
getCurrentFlowAppKey,
|
||||
}: FlowRuntimeActionParams) => {
|
||||
const { currentAppData, appRuntimeData, socketId } =
|
||||
store.getState().ideContainer;
|
||||
const appKey = getCurrentFlowAppKey();
|
||||
|
||||
if (!currentAppData) {
|
||||
Message.warning('请先选择一个应用');
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceId =
|
||||
appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : '';
|
||||
|
||||
if (!instanceId) {
|
||||
Message.warning('应用未运行');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const appId =
|
||||
currentAppData.key && currentAppData.key.includes('sub')
|
||||
? currentAppData.parentAppId
|
||||
: currentAppData.id;
|
||||
|
||||
const res: any = await reRunApp({
|
||||
appId,
|
||||
instanceId,
|
||||
socketId,
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
Message.success('应用重跑成功');
|
||||
dispatch(resetNodeStatus());
|
||||
} else {
|
||||
Message.error(res.msg || '重跑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重跑失败:', error);
|
||||
Message.error('重跑失败');
|
||||
}
|
||||
};
|
||||
|
||||
export const reconnectWorkflowRuntime = async (params: {
|
||||
instanceId: string;
|
||||
newSocketId: string;
|
||||
dispatch: Dispatch<any>;
|
||||
}) => {
|
||||
const { instanceId, newSocketId, dispatch } = params;
|
||||
|
||||
dispatch(updateRuntimeId(instanceId));
|
||||
dispatch(updateIsRunning(true));
|
||||
|
||||
const res: any = await reconnectRun({
|
||||
instanceId,
|
||||
newSocketId,
|
||||
});
|
||||
|
||||
return res?.data || res;
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
interface RuntimeReconnectApp {
|
||||
id?: string | number;
|
||||
key?: string | number;
|
||||
instanceId?: string | number;
|
||||
scheduled?: number | string | boolean;
|
||||
}
|
||||
|
||||
interface RuntimeReconnectRequestParams {
|
||||
app: RuntimeReconnectApp | null | undefined;
|
||||
socketId: string | null | undefined;
|
||||
lastReconnectKey: string;
|
||||
}
|
||||
|
||||
export const isScheduledRunning = (app: RuntimeReconnectApp | null | undefined) => {
|
||||
return Boolean(
|
||||
app && (app.scheduled === 1 || app.scheduled === '1' || app.scheduled === true)
|
||||
);
|
||||
};
|
||||
|
||||
export const buildRuntimeReconnectRequest = ({
|
||||
app,
|
||||
socketId,
|
||||
lastReconnectKey,
|
||||
}: RuntimeReconnectRequestParams) => {
|
||||
const instanceId = app?.instanceId ? String(app.instanceId) : '';
|
||||
const newSocketId = socketId ? String(socketId) : '';
|
||||
const appKey = app?.id || app?.key ? String(app.id || app.key) : '';
|
||||
|
||||
if (!isScheduledRunning(app) || !instanceId || !newSocketId || !appKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reconnectKey = `${appKey}:${instanceId}:${newSocketId}`;
|
||||
if (reconnectKey === lastReconnectKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
instanceId,
|
||||
newSocketId,
|
||||
reconnectKey,
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,54 +1,4 @@
|
||||
import { Edge } from '@xyflow/react';
|
||||
|
||||
export const resolveInsertedNodeHandles = (node: any) => {
|
||||
let sourceHandle = 'done';
|
||||
let targetHandle = 'start';
|
||||
|
||||
if (node?.data?.parameters) {
|
||||
const { apiOuts, apiIns } = node.data.parameters;
|
||||
|
||||
if (apiOuts && apiOuts.length > 0) {
|
||||
sourceHandle = apiOuts[0].name || apiOuts[0].id || sourceHandle;
|
||||
}
|
||||
|
||||
if (apiIns && apiIns.length > 0) {
|
||||
targetHandle = apiIns[0].name || apiIns[0].id || targetHandle;
|
||||
}
|
||||
}
|
||||
|
||||
return { sourceHandle, targetHandle };
|
||||
};
|
||||
|
||||
export const buildInsertedNodeEdges = (params: {
|
||||
baseEdges: Edge[];
|
||||
removedEdgeId: string;
|
||||
sourceId: string;
|
||||
sourceHandle?: string;
|
||||
targetId: string;
|
||||
targetHandle?: string;
|
||||
insertedNodeId: string;
|
||||
insertedNodeSourceHandle: string;
|
||||
insertedNodeTargetHandle: string;
|
||||
}): Edge[] => [
|
||||
...params.baseEdges.filter((e) => e.id !== params.removedEdgeId),
|
||||
{
|
||||
id: `e${params.sourceId}-${params.insertedNodeId}`,
|
||||
source: params.sourceId,
|
||||
target: params.insertedNodeId,
|
||||
sourceHandle: params.sourceHandle,
|
||||
targetHandle: params.insertedNodeTargetHandle,
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
{
|
||||
id: `e${params.insertedNodeId}-${params.targetId}`,
|
||||
source: params.insertedNodeId,
|
||||
target: params.targetId,
|
||||
sourceHandle: params.insertedNodeSourceHandle,
|
||||
targetHandle: params.targetHandle,
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
];
|
||||
export {
|
||||
buildInsertedNodeEdges,
|
||||
resolveInsertedNodeHandles,
|
||||
} from '@/features/workflow/operations/edgeOperations';
|
||||
|
||||
@ -1,104 +1,5 @@
|
||||
import { Edge } from '@xyflow/react';
|
||||
|
||||
export const createLoopNodePair = (position: { x: number; y: number }) => {
|
||||
const loopStartNode: any = {
|
||||
id: `LOOP_START-${Date.now()}`,
|
||||
type: 'LOOP',
|
||||
position: { x: position.x, y: position.y },
|
||||
data: {
|
||||
title: '循环开始',
|
||||
type: 'LOOP_START',
|
||||
parameters: {
|
||||
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
|
||||
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
|
||||
dataIns: [],
|
||||
dataOuts: [],
|
||||
},
|
||||
component: {},
|
||||
},
|
||||
};
|
||||
|
||||
const loopEndNode: any = {
|
||||
id: `LOOP_END-${Date.now()}`,
|
||||
type: 'LOOP',
|
||||
position: { x: position.x + 400, y: position.y },
|
||||
data: {
|
||||
title: '循环结束',
|
||||
type: 'LOOP_END',
|
||||
parameters: {
|
||||
apiIns: [
|
||||
{ name: 'continue', desc: '', dataType: '', defaultValue: '' },
|
||||
],
|
||||
apiOuts: [{ name: 'break', desc: '', dataType: '', defaultValue: '' }],
|
||||
dataIns: [
|
||||
{
|
||||
arrayType: null,
|
||||
dataType: 'INTEGER',
|
||||
defaultValue: 10,
|
||||
desc: '最大循环次数',
|
||||
id: 'maxTime',
|
||||
},
|
||||
],
|
||||
dataOuts: [],
|
||||
},
|
||||
component: {
|
||||
type: 'LOOP_END',
|
||||
customDef: JSON.stringify({
|
||||
apiOutIds: ['continue', 'break'],
|
||||
conditions: [],
|
||||
loopStartNodeId: loopStartNode.id,
|
||||
}),
|
||||
loopStartNodeId: loopStartNode.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
loopStartNode.data.component = {
|
||||
type: 'LOOP_START',
|
||||
customDef: JSON.stringify({ loopEndNodeId: loopEndNode.id }),
|
||||
};
|
||||
|
||||
return { loopStartNode, loopEndNode };
|
||||
};
|
||||
|
||||
export const createLoopGroupEdge = (
|
||||
loopStartId: string,
|
||||
loopEndId: string
|
||||
): Edge => ({
|
||||
id: `${loopStartId}-${loopEndId}-group`,
|
||||
source: loopStartId,
|
||||
target: loopEndId,
|
||||
sourceHandle: `${loopStartId}-group`,
|
||||
targetHandle: `${loopEndId}-group`,
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
export const createLoopInsertConnectionEdges = (params: {
|
||||
sourceId: string;
|
||||
sourceHandle: string;
|
||||
targetId: string;
|
||||
targetHandle: string;
|
||||
loopStartId: string;
|
||||
loopEndId: string;
|
||||
}): Edge[] => [
|
||||
{
|
||||
id: `e${params.sourceId}-${params.loopStartId}`,
|
||||
source: params.sourceId,
|
||||
target: params.loopStartId,
|
||||
sourceHandle: params.sourceHandle,
|
||||
targetHandle: 'start',
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
{
|
||||
id: `e${params.loopEndId}-${params.targetId}`,
|
||||
source: params.loopEndId,
|
||||
target: params.targetId,
|
||||
sourceHandle: 'break',
|
||||
targetHandle: params.targetHandle,
|
||||
type: 'custom',
|
||||
lineType: 'api',
|
||||
data: { lineType: 'api' },
|
||||
} as Edge,
|
||||
];
|
||||
export {
|
||||
createLoopGroupEdge,
|
||||
createLoopInsertConnectionEdges,
|
||||
createLoopNodePair,
|
||||
} from '@/features/workflow/operations/loopOperations';
|
||||
|
||||
@ -1,79 +1,4 @@
|
||||
import { Node } from '@xyflow/react';
|
||||
|
||||
interface FlowNodeDefinition {
|
||||
nodeName: string;
|
||||
data: any;
|
||||
id?: string;
|
||||
flowHousVO?: {
|
||||
id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type EventIdMode = 'id' | 'eventIdOptional';
|
||||
|
||||
export const createFlowNode = (
|
||||
nodeType: string,
|
||||
nodeDefinition: FlowNodeDefinition,
|
||||
position: { x: number; y: number }
|
||||
): Node => {
|
||||
const node: any = {
|
||||
id: `${nodeType}-${Date.now()}`,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {
|
||||
...nodeDefinition.data,
|
||||
title: nodeDefinition.nodeName,
|
||||
type: nodeType,
|
||||
},
|
||||
};
|
||||
|
||||
if (nodeDefinition.id || nodeDefinition?.flowHousVO?.id) {
|
||||
node.data.compId = nodeDefinition.id || nodeDefinition?.flowHousVO?.id;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
export const attachFlowNodeComponent = (
|
||||
node: any,
|
||||
nodeType: string,
|
||||
nodeDefinition: FlowNodeDefinition,
|
||||
eventList: any[],
|
||||
eventIdMode: EventIdMode
|
||||
) => {
|
||||
if (nodeType === 'SWITCH') {
|
||||
node.data.component = {
|
||||
customDef: JSON.stringify({
|
||||
apiOutIds: ['default'],
|
||||
conditions: [],
|
||||
}),
|
||||
};
|
||||
} else if (nodeType === 'SUB') {
|
||||
node.data.component = {
|
||||
type: nodeType,
|
||||
compId: node.data.compId,
|
||||
};
|
||||
} else if (nodeType === 'EVENTSEND' || nodeType === 'EVENTLISTENE') {
|
||||
const emptyEvent = eventList.find((item) =>
|
||||
item.topic.includes('**empty**')
|
||||
);
|
||||
node.data.component = {
|
||||
type: nodeType,
|
||||
customDef: {
|
||||
eventId:
|
||||
eventIdMode === 'eventIdOptional'
|
||||
? emptyEvent?.eventId ?? null
|
||||
: emptyEvent.id,
|
||||
name: emptyEvent.name,
|
||||
topic: emptyEvent.topic,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
node.data.component = {
|
||||
type: nodeType,
|
||||
compId: nodeDefinition.id,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
export {
|
||||
attachFlowNodeComponent,
|
||||
createFlowNode,
|
||||
} from '@/features/workflow/operations/nodeFactory';
|
||||
|
||||
@ -1,61 +1,5 @@
|
||||
import {
|
||||
createFlowNode,
|
||||
attachFlowNodeComponent,
|
||||
} from '@/utils/flow/nodeFactory';
|
||||
import {
|
||||
ensureNodeTypeRegistered,
|
||||
resolveNodeComponent,
|
||||
} from '@/utils/flow/nodeRegistry';
|
||||
|
||||
type EventIdMode = 'id' | 'eventIdOptional';
|
||||
|
||||
export const resolveNodeDefinition = (
|
||||
nodeList: any[],
|
||||
nodeType: string,
|
||||
fallbackNode?: any
|
||||
) => {
|
||||
return nodeList.find((n) => n.nodeType === nodeType) || fallbackNode || null;
|
||||
};
|
||||
|
||||
export const buildRuntimeNode = (params: {
|
||||
nodeType: string;
|
||||
nodeDefinition: any;
|
||||
position: { x: number; y: number };
|
||||
eventList: any[];
|
||||
eventIdMode: EventIdMode;
|
||||
}) => {
|
||||
const { nodeType, nodeDefinition, position, eventList, eventIdMode } = params;
|
||||
|
||||
const newNode = attachFlowNodeComponent(
|
||||
createFlowNode(nodeType, nodeDefinition, position),
|
||||
nodeType,
|
||||
nodeDefinition,
|
||||
eventList,
|
||||
eventIdMode
|
||||
);
|
||||
|
||||
ensureNodeTypeRegistered(
|
||||
nodeType,
|
||||
nodeDefinition.nodeName,
|
||||
resolveNodeComponent(nodeType)
|
||||
);
|
||||
|
||||
return newNode;
|
||||
};
|
||||
|
||||
export const buildDroppedNode = (
|
||||
nodeData: any,
|
||||
position: { x: number; y: number }
|
||||
) => {
|
||||
ensureNodeTypeRegistered(nodeData.nodeType, nodeData.nodeName);
|
||||
return {
|
||||
id: `${nodeData.nodeType}-${Date.now()}`,
|
||||
type: nodeData.nodeType,
|
||||
position,
|
||||
data: {
|
||||
...nodeData.data,
|
||||
title: nodeData.nodeName,
|
||||
type: nodeData.nodeType,
|
||||
},
|
||||
};
|
||||
};
|
||||
export {
|
||||
buildDroppedNode,
|
||||
buildRuntimeNode,
|
||||
resolveNodeDefinition,
|
||||
} from '@/features/workflow/operations/nodeOnboarding';
|
||||
|
||||
@ -1,40 +1,4 @@
|
||||
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
|
||||
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
|
||||
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
|
||||
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
|
||||
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
|
||||
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
|
||||
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
|
||||
|
||||
export const resolveNodeComponent = (nodeType: string) => {
|
||||
switch (nodeType) {
|
||||
case 'BASIC':
|
||||
case 'SUB':
|
||||
return BasicNode;
|
||||
case 'SWITCH':
|
||||
return SwitchNode;
|
||||
case 'IMAGE':
|
||||
return ImageNode;
|
||||
case 'CODE':
|
||||
return CodeNode;
|
||||
case 'REST':
|
||||
return RestNode;
|
||||
default:
|
||||
return LocalNode;
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureNodeTypeRegistered = (
|
||||
nodeType: string,
|
||||
nodeName: string,
|
||||
component?: any
|
||||
) => {
|
||||
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
|
||||
if (!nodeMap.includes(nodeType)) {
|
||||
registerNodeType(
|
||||
nodeType,
|
||||
component || resolveNodeComponent(nodeType),
|
||||
nodeName
|
||||
);
|
||||
}
|
||||
};
|
||||
export {
|
||||
ensureNodeTypeRegistered,
|
||||
resolveNodeComponent,
|
||||
} from '@/features/workflow/registry/nodeRegistry';
|
||||
|
||||
@ -1,34 +1,8 @@
|
||||
export interface FlowCurrentAppData {
|
||||
id?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface AppRuntimeState {
|
||||
nodeStatusMap: Record<string, string>;
|
||||
isRunning: boolean;
|
||||
isPaused: boolean;
|
||||
logs: any[];
|
||||
runId: string;
|
||||
eventSendNodeList: any[];
|
||||
eventlisteneList: any[];
|
||||
}
|
||||
|
||||
export const getCurrentAppKey = (
|
||||
currentAppData: FlowCurrentAppData | null | undefined
|
||||
) => {
|
||||
if (!currentAppData) return null;
|
||||
if (currentAppData.key && currentAppData.key.includes('sub')) {
|
||||
return currentAppData.key;
|
||||
}
|
||||
return currentAppData.id || null;
|
||||
};
|
||||
|
||||
export const createDefaultAppRuntimeState = (): AppRuntimeState => ({
|
||||
nodeStatusMap: {},
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
logs: [],
|
||||
runId: '',
|
||||
eventSendNodeList: [],
|
||||
eventlisteneList: [],
|
||||
});
|
||||
export {
|
||||
createDefaultAppRuntimeState,
|
||||
getCurrentAppKey,
|
||||
} from '@/features/workflow/runtime/flowRuntime';
|
||||
export type {
|
||||
AppRuntimeState,
|
||||
FlowCurrentAppData,
|
||||
} from '@/features/workflow/runtime/flowRuntime';
|
||||
|
||||
@ -1,22 +1,4 @@
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
|
||||
interface FlowSnapshotDetail {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
export const dispatchFlowSnapshot = (detail: FlowSnapshotDetail) => {
|
||||
const event = new CustomEvent('takeSnapshot', {
|
||||
detail,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export const dispatchFlowSnapshotAsync = (
|
||||
detail: FlowSnapshotDetail,
|
||||
delay = 0
|
||||
) => {
|
||||
return setTimeout(() => {
|
||||
dispatchFlowSnapshot(detail);
|
||||
}, delay);
|
||||
};
|
||||
export {
|
||||
dispatchFlowSnapshot,
|
||||
dispatchFlowSnapshotAsync,
|
||||
} from '@/features/workflow/operations/snapshot';
|
||||
|
||||
Loading…
Reference in New Issue