Compare commits

...

2 Commits

Author SHA1 Message Date
钟良源 05ed81fa5a refactor: 重连功能迁移重构,画布数据更新迁移 1 month ago
钟良源 3396cf3d62 refactor: 重构流程编排相关功能
- 新增 src/features/workflow/ 目录,按职责拆成:
      - domain/:工作流类型、customDef 解析工具。
      - adapters/:流程数据转换逻辑。
      - registry/:节点注册和节点描述。
      - operations/:连接、复制粘贴、删除、节点插入、循环节点等图操作。
      - persistence/:流程保存、校验、事件同步、缓存更新。
      - runtime/:运行、停止、暂停/恢复、重跑。

  - 将原 src/utils/convertFlowData.ts、convertAppFlowData.ts、src/utils/flow/* 改成兼容 re-export,老路径仍可用。
  - useFlowCallbacks.ts 大幅瘦身,从约 1617 行降到 867 行,把保存、运行、连接、复制粘贴、循环删除等逻辑迁出。
  - FlowEditorMain.tsx 中循环节点成对删除逻辑迁到 deleteOperations。
  - 节点注册开始走 nodeDescriptors,为后续新增节点统一入口打基础。
  - nodeValidators.ts 开始使用统一的 parseCustomDef,减少散落的 JSON.parse。
1 month ago

@ -1,5 +1,6 @@
import { Message } from '@arco-design/web-react';
import { Edge } from '@xyflow/react';
import { parseCustomDef } from '@/features/workflow/domain/customDef';
export interface ValidationResult {
isValid: boolean;
@ -78,12 +79,8 @@ const validateRestNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('REST节点配置信息格式错误');
return errors;
}
@ -119,12 +116,8 @@ const validateCodeNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('代码节点配置信息格式错误');
return errors;
}
@ -150,12 +143,8 @@ const validateSwitchNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('条件节点配置信息格式错误');
return errors;
}
@ -200,12 +189,8 @@ const validateLoopNode = (nodeData: any, nodeType: string): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('循环节点配置信息格式错误');
return errors;
}
@ -231,12 +216,8 @@ const validateEventNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('事件节点配置信息格式错误');
return errors;
}
@ -261,12 +242,8 @@ const validateWaitNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('等待节点配置信息格式错误');
return errors;
}
@ -295,12 +272,8 @@ const validateCycleNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('周期节点配置信息格式错误');
return errors;
}

@ -0,0 +1,139 @@
/**
* flow editor nodes edges
* @param appFlowData - 2
* @returns nodes edges convertFlowData
*/
export const convertAppFlowData = (appFlowData: any[]) => {
const nodes: any[] = [];
const edges: any[] = [];
// 如果没有数据,返回空数组
if (!appFlowData || appFlowData.length === 0) {
return { nodes, edges };
}
// 处理每个应用流程数据项(每个应用作为一个节点)
appFlowData.forEach((app: any, index: number) => {
// 添加过滤逻辑:如果 eventListenes 和 eventSends 都为空,则不生成节点
const hasEventListenes = app.eventListenes && app.eventListenes.length > 0;
const hasEventSends = app.eventSends && app.eventSends.length > 0;
// 如果两者都为空,则跳过当前应用节点的创建
if (!hasEventListenes && !hasEventSends) {
return;
}
// 构造节点数据
const node: any = {
id: app.appId || `app_${index}`,
type: 'APP',
position: app.position || { x: 200 + index * 300, y: 200 },
data: {
title: app.name || `应用${index + 1}`,
parameters: {
// eventListenes 作为 apiIns输入
apiIns: app.eventListenes ? app.eventListenes.map((event: any) => ({
name: event.nodeName,
desc: event.description || '',
dataType: '',
defaultValue: '',
topic: event.topic,
eventId: event.eventId,
eventName: event.eventName
})) : [],
// eventSends 作为 apiOuts输出
apiOuts: app.eventSends ? app.eventSends.map((event: any) => ({
name: event.nodeName,
desc: event.description || '',
dataType: '',
defaultValue: '',
topic: event.topic,
eventId: event.eventId,
eventName: event.eventName
})) : [],
// 提取 dataIns 和 dataOuts 属性
dataIns: [],
dataOuts: []
},
type: 'APP',
component: {
type: 'APP',
appId: app.appId,
customDef: JSON.stringify({
eventListenes: app.eventListenes || [],
eventSends: app.eventSends || []
})
}
}
};
// 处理 dataIns来自 eventListenes 的 dataOuts
if (app.eventListenes && app.eventListenes.length > 0) {
app.eventListenes.forEach((event: any) => {
if (event.dataOuts && event.dataOuts.length > 0) {
node.data.parameters.dataIns = [...node.data.parameters.dataIns, ...event.dataOuts];
}
});
}
// 处理 dataOuts来自 eventSends 的 dataIns
if (app.eventSends && app.eventSends.length > 0) {
app.eventSends.forEach((event: any) => {
if (event.dataIns && event.dataIns.length > 0) {
node.data.parameters.dataOuts = [...node.data.parameters.dataOuts, ...event.dataIns];
}
});
}
nodes.push(node);
});
// 遍历所有节点对
for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes.length; j++) {
if (i !== j) { // 不与自己比较
const sourceNode = nodes[i];
const targetNode = nodes[j];
// 检查源节点的 eventSends (apiOuts) 和目标节点的 eventListenes (apiIns)
const sourceEvents = sourceNode.data.component?.customDef ?
JSON.parse(sourceNode.data.component.customDef).eventSends || [] :
[];
const targetEvents = targetNode.data.component?.customDef ?
JSON.parse(targetNode.data.component.customDef).eventListenes || [] :
[];
// 比较事件的 topic 是否匹配
sourceEvents.forEach((sourceEvent: any, outIndex: number) => {
targetEvents.forEach((targetEvent: any, inIndex: number) => {
// 当 topic 匹配且不是 **empty** 占位符时创建边
if (sourceEvent.topic &&
targetEvent.topic &&
sourceEvent.topic === targetEvent.topic &&
!sourceEvent.topic.includes('**empty**') &&
!targetEvent.topic.includes('**empty**')) {
edges.push({
id: `e-${sourceNode.id}-${targetNode.id}-${outIndex}-${inIndex}`,
source: sourceNode.id,
target: targetNode.id,
sourceHandle: sourceEvent.eventId,
targetHandle: targetEvent.eventId,
type: 'custom',
data: {
lineType: 'api', // 应用间连接始终是API类型
displayData: {
name: sourceEvent.eventName,
eventId: sourceEvent.eventId,
topic: sourceEvent.topic
}
}
});
}
});
});
}
}
}
return { nodes, edges };
};

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

@ -3,7 +3,7 @@ import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { updateCanvasDataMap } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
import { Dispatch } from 'redux';
@ -61,7 +61,9 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
Object.values(initialData.components).some((comp: any) => comp.status);
setNodes((prevNodes) => {
return prevNodes.map((node) => {
let hasChanges = false;
const nextNodes = prevNodes.map((node) => {
// 如果是只读模式(历史实例查看),优先使用节点自身的历史状态
// 如果是正常运行模式,只使用运行时状态
let nodeStatus = 'waiting';
@ -80,6 +82,14 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
showStatus = currentAppIsRunning;
}
if (
node.data.status === nodeStatus &&
node.data.isStatusVisible === showStatus
) {
return node;
}
hasChanges = true;
return {
...node,
data: {
@ -89,6 +99,8 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
},
};
});
return hasChanges ? nextNodes : prevNodes;
});
}, [
appRuntimeData,

@ -27,7 +27,12 @@ import { NodeTypes } from '@xyflow/react';
import { useSelector } from 'react-redux';
import HandlerBar from '@/pages/flowEditor/components/handlerBar';
import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
import {
isLoopBoundaryNode,
removeNodesAndConnectedEdges,
resolveDeletedNodesWithLoopPairs,
} from '@/features/workflow/operations/deleteOperations';
const edgeTypes = {
custom: CustomEdge
@ -415,12 +420,6 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return false; // 阻止删除操作
}
// 检查是否有循环节点这里只是检查实际删除逻辑在onNodesDelete中处理
const loopNodes = nodes.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
// 允许删除操作继续进行
return !currentAppIsRunning; // 在运行时禁止删除任何元素
}}
@ -436,82 +435,18 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return;
}
// 检查是否有循环节点
const loopNodes = deleted.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
if (loopNodes.length > 0) {
// 处理循环节点删除
let nodesToRemove = [...deleted];
// 为每个循环节点找到其配对节点
loopNodes.forEach((loopNode) => {
const component = loopNode.data?.component as
| { customDef?: string }
| undefined;
if (
loopNode.data?.type === 'LOOP_START' &&
component?.customDef
) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopEndNodeId;
// 添加关联的结束节点到删除列表
const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
} catch (e) {
console.error('解析循环开始节点数据失败:', e);
}
}
else if (
loopNode.data?.type === 'LOOP_END' &&
component?.customDef
) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopStartNodeId;
// 添加关联的开始节点到删除列表
const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
} catch (e) {
console.error('解析循环结束节点数据失败:', e);
}
}
});
// 去重
nodesToRemove = nodesToRemove.filter(
(node, index, self) =>
index === self.findIndex((n) => n.id === node.id)
);
// 删除所有相关节点和边
setNodes((nds) =>
nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id))
);
// 删除与这些节点相关的所有边
const nodeIdsToRemove = nodesToRemove.map((node) => node.id);
setEdges((eds) =>
eds.filter(
(e) =>
!nodeIdsToRemove.includes(e.source) &&
!nodeIdsToRemove.includes(e.target)
)
);
}
else {
// 普通节点删除
setNodes((nds) =>
nds.filter((n) => !deleted.find((d) => d.id === n.id))
if (deleted.some(isLoopBoundaryNode)) {
const nodesToRemove = resolveDeletedNodesWithLoopPairs(deleted, nodes);
const nextGraph = removeNodesAndConnectedEdges(
nodes,
edges,
nodesToRemove
);
setNodes(nextGraph.nodes);
setEdges(nextGraph.edges);
} else {
const deletedIds = new Set(deleted.map((node) => node.id));
setNodes((nds) => nds.filter((node) => !deletedIds.has(node.id)));
}
setIsEditModalOpen(false);

@ -11,7 +11,7 @@ import {
} from '@arco-design/web-react/icon';
import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
const ButtonGroup = Button.Group;

@ -3,7 +3,7 @@ import { Modal, List, Message, Spin, Pagination, Button, Space } from '@arco-des
import { ReactFlowProvider, ReactFlow, Background, Controls } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { historyPage } from '@/api/appRes';
import { convertFlowData } from '@/utils/convertFlowData';
import { convertFlowData } from '@/features/workflow/adapters/serverFlowAdapter';
import { nodeTypes } from '@/components/FlowEditor/node';
import { useSelector, useDispatch } from 'react-redux';
import { updateFlowData, updateCanvasDataMap } from '@/store/ideContainer';

@ -2,8 +2,7 @@
*
* */
import { convertAppFlowData } from '@/utils/convertAppFlowData';
import { convertFlowData } from '@/utils/convertFlowData';
import { convertAppFlowData } from '@/features/workflow/adapters/appFlowAdapter';
import { Edge } from '@xyflow/react';
import { updateCanvasDataMap } from '@/store/ideContainer';
@ -29,4 +28,4 @@ export const appFLowHandle = (initialData, useDefault, setNodes, setEdges, dispa
// [initialData.appId]: { nodes: convertedNodes, edges: initialEdges }
// }));
// }
};
};

@ -2,7 +2,7 @@
*
* */
// 组件编排画布数据处理(组件编排)
import { convertFlowData } from '@/utils/convertFlowData';
import { convertFlowData } from '@/features/workflow/adapters/serverFlowAdapter';
import { Edge } from '@xyflow/react';
import { updateCanvasDataMap } from '@/store/ideContainer';
@ -30,4 +30,4 @@ export const projectFlowHandle = (initialData, useDefault, setNodes, setEdges, d
[cacheKey]: { nodes: convertedNodes, edges: initialEdges }
}));
}
};
};

@ -1,8 +1,14 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { updateSocketId, updateNodeStatus, updateEventListOld } from '@/store/ideContainer';
import {
updateSocketId,
updateNodeStatus,
updateEventListOld
} from '@/store/ideContainer';
import useWebSocket from '@/hooks/useWebSocket';
import { isJSON, getUrlParams } from '@/utils/common';
import { buildRuntimeReconnectRequest } from '@/features/workflow/runtime/runtimeReconnect';
import { reconnectWorkflowRuntime } from '@/features/workflow/runtime/runtimeActions';
import styles from './style/index.module.less';
import SideBar from './sideBar';
@ -64,9 +70,10 @@ function IDEContainer() {
// 用于跟踪已打开的tab保持组件状态
const [openedTabs, setOpenedTabs] = useState<Set<string>>(new Set());
const [subMenuData, setSubMenuData] = useState<any>({});
const { menuData, flowData, currentAppData } = useSelector((state) => state.ideContainer);
const { menuData, flowData, currentAppData, socketId } = useSelector((state) => state.ideContainer);
const dispatch = useDispatch();
const navBarRef = useRef<NavBarRef>(null);
const lastReconnectKeyRef = useRef('');
// 初始化WebSocket hook
const ws = useWebSocket({
@ -122,6 +129,48 @@ function IDEContainer() {
}
});
useEffect(() => {
const reconnectRequest = buildRuntimeReconnectRequest({
app: currentAppData,
socketId,
lastReconnectKey: lastReconnectKeyRef.current
});
if (!reconnectRequest) {
return;
}
let canceled = false;
lastReconnectKeyRef.current = reconnectRequest.reconnectKey;
const reconnectRuntime = async () => {
try {
const reconnectResult = await reconnectWorkflowRuntime({
instanceId: reconnectRequest.instanceId,
newSocketId: reconnectRequest.newSocketId,
dispatch
});
if (!canceled && reconnectResult?.success === false) {
lastReconnectKeyRef.current = '';
Message.error(reconnectResult.message || '运行实例重连失败');
}
} catch (error) {
if (!canceled) {
lastReconnectKeyRef.current = '';
console.error('运行实例重连失败:', error);
Message.error('运行实例重连失败');
}
}
};
reconnectRuntime();
return () => {
canceled = true;
};
}, [currentAppData, socketId, dispatch]);
// 监听自定义事件,处理打开子节点标签页的逻辑
useEffect(() => {
const handleOpenSubNodeTab = async (event: CustomEvent) => {
@ -500,4 +549,4 @@ function IDEContainer() {
);
}
export default IDEContainer;
export default IDEContainer;

@ -5,7 +5,7 @@ import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
import { getNodeData } from '@/api/appIns';
import RunTimeData from './components/runTimeData';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
const TabPane = Tabs.TabPane;

@ -46,7 +46,7 @@ import _ from 'lodash';
import { getAppInfoNew } from '@/api/appRes';
import { getAppEventData } from '@/api/appEvent';
import { queryEventItemBySceneIdOld } from '@/api/event';
import { convertFlowData } from '@/utils/convertFlowData';
import { convertFlowData } from '@/features/workflow/adapters/serverFlowAdapter';
const TreeNode = Tree.Node;
const FormItem = Form.Item;
@ -1067,4 +1067,4 @@ const SideBar: React.FC<SideBarProps> = ({
);
};
export default SideBar;
export default SideBar;

@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
import {
createDefaultAppRuntimeState,
getCurrentAppKey,
} from '@/utils/flow/runtime';
} from '@/features/workflow/runtime/flowRuntime';
// 定义初始状态类型
interface IDEContainerState {

@ -1,139 +1 @@
/**
* flow editor nodes edges
* @param appFlowData - 2
* @returns nodes edges convertFlowData
*/
export const convertAppFlowData = (appFlowData: any[]) => {
const nodes: any[] = [];
const edges: any[] = [];
// 如果没有数据,返回空数组
if (!appFlowData || appFlowData.length === 0) {
return { nodes, edges };
}
// 处理每个应用流程数据项(每个应用作为一个节点)
appFlowData.forEach((app: any, index: number) => {
// 添加过滤逻辑:如果 eventListenes 和 eventSends 都为空,则不生成节点
const hasEventListenes = app.eventListenes && app.eventListenes.length > 0;
const hasEventSends = app.eventSends && app.eventSends.length > 0;
// 如果两者都为空,则跳过当前应用节点的创建
if (!hasEventListenes && !hasEventSends) {
return;
}
// 构造节点数据
const node: any = {
id: app.appId || `app_${index}`,
type: 'APP',
position: app.position || { x: 200 + index * 300, y: 200 },
data: {
title: app.name || `应用${index + 1}`,
parameters: {
// eventListenes 作为 apiIns输入
apiIns: app.eventListenes ? app.eventListenes.map((event: any) => ({
name: event.nodeName,
desc: event.description || '',
dataType: '',
defaultValue: '',
topic: event.topic,
eventId: event.eventId,
eventName: event.eventName
})) : [],
// eventSends 作为 apiOuts输出
apiOuts: app.eventSends ? app.eventSends.map((event: any) => ({
name: event.nodeName,
desc: event.description || '',
dataType: '',
defaultValue: '',
topic: event.topic,
eventId: event.eventId,
eventName: event.eventName
})) : [],
// 提取 dataIns 和 dataOuts 属性
dataIns: [],
dataOuts: []
},
type: 'APP',
component: {
type: 'APP',
appId: app.appId,
customDef: JSON.stringify({
eventListenes: app.eventListenes || [],
eventSends: app.eventSends || []
})
}
}
};
// 处理 dataIns来自 eventListenes 的 dataOuts
if (app.eventListenes && app.eventListenes.length > 0) {
app.eventListenes.forEach((event: any) => {
if (event.dataOuts && event.dataOuts.length > 0) {
node.data.parameters.dataIns = [...node.data.parameters.dataIns, ...event.dataOuts];
}
});
}
// 处理 dataOuts来自 eventSends 的 dataIns
if (app.eventSends && app.eventSends.length > 0) {
app.eventSends.forEach((event: any) => {
if (event.dataIns && event.dataIns.length > 0) {
node.data.parameters.dataOuts = [...node.data.parameters.dataOuts, ...event.dataIns];
}
});
}
nodes.push(node);
});
// 遍历所有节点对
for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes.length; j++) {
if (i !== j) { // 不与自己比较
const sourceNode = nodes[i];
const targetNode = nodes[j];
// 检查源节点的 eventSends (apiOuts) 和目标节点的 eventListenes (apiIns)
const sourceEvents = sourceNode.data.component?.customDef ?
JSON.parse(sourceNode.data.component.customDef).eventSends || [] :
[];
const targetEvents = targetNode.data.component?.customDef ?
JSON.parse(targetNode.data.component.customDef).eventListenes || [] :
[];
// 比较事件的 topic 是否匹配
sourceEvents.forEach((sourceEvent: any, outIndex: number) => {
targetEvents.forEach((targetEvent: any, inIndex: number) => {
// 当 topic 匹配且不是 **empty** 占位符时创建边
if (sourceEvent.topic &&
targetEvent.topic &&
sourceEvent.topic === targetEvent.topic &&
!sourceEvent.topic.includes('**empty**') &&
!targetEvent.topic.includes('**empty**')) {
edges.push({
id: `e-${sourceNode.id}-${targetNode.id}-${outIndex}-${inIndex}`,
source: sourceNode.id,
target: targetNode.id,
sourceHandle: sourceEvent.eventId,
targetHandle: targetEvent.eventId,
type: 'custom',
data: {
lineType: 'api', // 应用间连接始终是API类型
displayData: {
name: sourceEvent.eventName,
eventId: sourceEvent.eventId,
topic: sourceEvent.topic
}
}
});
}
});
});
}
}
}
return { nodes, edges };
};
export { convertAppFlowData } from '@/features/workflow/adapters/appFlowAdapter';

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';

@ -1,5 +1,5 @@
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
import { resolveNodeComponent } from '@/features/workflow/registry/nodeRegistry';
// 获取handle类型 (api或data)
const getHandleType = (handleId: string, nodeParams: any) => {

Loading…
Cancel
Save