diff --git a/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts b/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts index db54fe5..84f956e 100644 --- a/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts +++ b/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts @@ -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; } diff --git a/src/features/workflow/adapters/appFlowAdapter.ts b/src/features/workflow/adapters/appFlowAdapter.ts new file mode 100644 index 0000000..486727e --- /dev/null +++ b/src/features/workflow/adapters/appFlowAdapter.ts @@ -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 }; +}; \ No newline at end of file diff --git a/src/features/workflow/adapters/serverFlowAdapter.ts b/src/features/workflow/adapters/serverFlowAdapter.ts new file mode 100644 index 0000000..980a8c6 --- /dev/null +++ b/src/features/workflow/adapters/serverFlowAdapter.ts @@ -0,0 +1,1041 @@ +import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node'; +import store from '@/store/index'; +import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode'; +import { updateEventNodeList } from '@/store/ideContainer'; +import { resolveNodeComponent } from '@/features/workflow/registry/nodeRegistry'; + +/** + * 将提供的数据结构转换为适用于 flow editor 的 nodes 和 edges + * @param flowData - 原始数据结构 + * @param useDefault - 当flowData为空时是否返回默认的开始和结束节点 + * @returns 包含 nodes 和 edges 的对象 + */ +export const convertFlowData = (flowData: any, useDefault = true) => { + const nodes: any[] = []; + const edges: any[] = []; + const eventSendNodeList = []; + const eventlisteneList = []; + const currentProjectCompData = getCurrentProjectStoreData(); + + if (!flowData || Object.keys(flowData).length === 0) { + // 如果useDefault为true且flowData为空,则返回默认的开始和结束节点 + const timestamp = Date.now(); + if (useDefault) { + return { + nodes: [ + { + id: `start`, + type: 'start', + position: { x: 200, y: 200 }, + data: { + title: '开始', + parameters: { + apiIns: [], + apiOuts: [ + { name: 'start', desc: '', dataType: '', defaultValue: '' }, + ], + dataIns: [], + dataOuts: [], + }, + type: 'start', + }, + }, + { + id: `end-${timestamp}`, + type: 'end', + position: { x: 600, y: 200 }, + data: { + title: '结束', + parameters: { + apiIns: [ + { name: 'end', desc: '', dataType: '', defaultValue: '' }, + ], + apiOuts: [], + dataIns: [], + dataOuts: [], + }, + type: 'end', + }, + }, + ], + edges: [], + }; + } + // 否则返回空数组 + return { nodes, edges }; + } + + // 处理新格式的数据结构 + // 先处理所有节点 + const nodeEntries = Object.entries(flowData); + + for (const entry of nodeEntries) { + const nodeId: string = entry[0]; + const nodeConfig: any = entry[1]; + + // 更新应用中的事件节点列表 + if (nodeId.includes('EVENTLISTENE')) { + try { + const customDef = JSON.parse(nodeConfig.component.customDef); + // 使用展开运算符创建新数组,避免修改冻结对象 + eventlisteneList.splice(eventlisteneList.length, 0, { + [nodeId]: customDef.topic, + }); + } catch (error) { + console.log(error); + } + } else if ( + nodeId.includes('EVENTSEND') && + !nodeId.includes('EVENTSEND_SYNC') + ) { + try { + const customDef = JSON.parse(nodeConfig.component.customDef); + // 使用展开运算符创建新数组,避免修改冻结对象 + eventSendNodeList.splice(eventSendNodeList.length, 0, { + [nodeId]: customDef.topic, + }); + } catch (error) { + console.log(error); + } + } + + if (eventlisteneList.length > 0 || eventSendNodeList.length > 0) + store.dispatch( + updateEventNodeList({ + eventSendNodeList: [...eventSendNodeList], + eventlisteneList: [...eventlisteneList], + }) + ); + else { + store.dispatch( + updateEventNodeList({ + eventSendNodeList: [], + eventlisteneList: [], + }) + ); + } + + // 确定节点类型 + let nodeType = 'BASIC'; + if (nodeId.includes('start')) { + nodeType = 'start'; + } else if (nodeId.includes('end')) { + nodeType = 'end'; + } else if ( + nodeConfig.component?.type === 'LOOP_START' || + nodeConfig.component?.type === 'LOOP_END' + ) { + nodeType = 'LOOP'; + } else { + nodeType = nodeConfig.component?.type || 'BASIC'; + } + // 解析位置信息 + const position = nodeConfig.position || { x: 0, y: 0 }; + + // 构造节点数据 + const node: any = { + id: nodeId, + type: nodeType, + position, + data: { + title: nodeConfig.componentName || nodeId, + parameters: { + apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData), + apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData), + dataIns: getNodeDataIns(nodeConfig), + dataOuts: nodeConfig.dataOuts || [], + }, + type: nodeConfig.component?.type || nodeType, + }, + }; + + // 添加组件标识信息 + if (nodeConfig.component) { + node.data.component = { ...nodeConfig.component }; + node.data.compId = nodeConfig.component.compId; + } + + // 保留节点状态信息(用于历史实例查看) + if (nodeConfig.status) { + node.data.status = nodeConfig.status; + } + if (nodeConfig.isStatusVisible !== undefined) { + node.data.isStatusVisible = nodeConfig.isStatusVisible; + } + + // 注册循环节点类型 + if (nodeType === 'LOOP') { + const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key)); + if (!nodeMap.includes('LOOP')) { + registerNodeType('LOOP', LoopNode, '循环'); + } + } + + // 注册其他节点类型 + const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key)); + if ( + !nodeMap.includes(nodeType) && + nodeType !== 'start' && + nodeType !== 'end' && + nodeType !== 'LOOP' + ) { + registerNodeType( + nodeType, + resolveNodeComponent(nodeType), + nodeConfig.componentName + ); + } + + nodes.push(node); + } + + // 用于存储已添加的边,避免重复 + const addedEdges = new Set(); + + // 创建一个映射来存储所有连接信息 + const connections = new Map< + string, + { + source: string; + target: string; + sourceHandle: string; + targetHandle: string; + } + >(); + + // 遍历所有节点,收集连接信息 + for (const entry of nodeEntries) { + const nodeId: string = entry[0]; + const nodeConfig: any = entry[1]; + + // 处理 API 下游连接 - 确定目标节点信息 + if (nodeConfig.apiDownstream && Array.isArray(nodeConfig.apiDownstream)) { + nodeConfig.apiDownstream.forEach((targetArray: string[]) => { + if (Array.isArray(targetArray)) { + targetArray.forEach((target) => { + if (typeof target === 'string' && target.includes('$$')) { + const [targetNodeId, targetHandle] = target.split('$$'); + const connectionKey = `${nodeId}-${targetNodeId}`; + + // 存储连接信息 + if (connections.has(connectionKey)) { + // 如果连接已存在,更新目标句柄 + const existing = connections.get(connectionKey); + if (existing) { + connections.set(connectionKey, { + ...existing, + targetHandle: targetHandle, + }); + } + } else { + // 创建新的连接信息 + connections.set(connectionKey, { + source: nodeId, + target: targetNodeId, + sourceHandle: '', // 将根据节点信息填充 + targetHandle: targetHandle, + }); + } + } + }); + } + }); + } + + // 处理 API 上游连接 - 确定源节点信息 + if (nodeConfig.apiUpstream && Array.isArray(nodeConfig.apiUpstream)) { + nodeConfig.apiUpstream.forEach((sourceArray: string[]) => { + if (Array.isArray(sourceArray)) { + sourceArray.forEach((source) => { + if (typeof source === 'string' && source.includes('$$')) { + const [sourceNodeId, sourceHandle] = source.split('$$'); + const connectionKey = `${sourceNodeId}-${nodeId}`; + + // 存储连接信息 + if (connections.has(connectionKey)) { + // 如果连接已存在,更新源句柄 + const existing = connections.get(connectionKey); + if (existing) { + connections.set(connectionKey, { + ...existing, + sourceHandle: sourceHandle, + }); + } + } else { + // 创建新的连接信息 + connections.set(connectionKey, { + source: sourceNodeId, + target: nodeId, + sourceHandle: sourceHandle, + targetHandle: '', // 将根据节点信息填充 + }); + } + } + }); + } + }); + } + } + + // 根据收集的连接信息生成实际的边 + const connectionEntries = Array.from(connections.entries()); + for (const [connectionKey, connectionInfo] of connectionEntries) { + const { source, target, sourceHandle, targetHandle } = connectionInfo; + + // 获取源节点和目标节点 + const sourceNode = flowData[source]; + const targetNode = flowData[target]; + + // 确定最终的源句柄 + let finalSourceHandle = sourceHandle; + // 如果源句柄未指定,则根据源节点信息确定 + if (!finalSourceHandle) { + if (source === 'start') { + finalSourceHandle = 'start'; + } else if ( + sourceNode && + sourceNode.data && + sourceNode.data.parameters && + sourceNode.data.parameters.apiOuts && + sourceNode.data.parameters.apiOuts.length > 0 + ) { + // 查找匹配的目标句柄 + const matchingApiOut = sourceNode.data.parameters.apiOuts.find( + (apiOut: any) => apiOut.name === targetHandle + ); + + if (matchingApiOut) { + finalSourceHandle = matchingApiOut.name; + } else { + // 如果没有精确匹配,使用第一个apiOut + finalSourceHandle = sourceNode.data.parameters.apiOuts[0].name; + } + } else if ( + sourceNode && + sourceNode.component && + sourceNode.component.type + ) { + // 根据节点类型获取正确的源句柄 + finalSourceHandle = getNodeApiOutHandle(source, sourceNode); + } else { + // 默认句柄 + finalSourceHandle = 'done'; + } + } + + // 确定最终的目标句柄 + let finalTargetHandle = targetHandle; + // 如果目标句柄未指定,则根据目标节点信息确定 + if (!finalTargetHandle) { + if (target === 'end') { + finalTargetHandle = 'end'; + } else if ( + targetNode && + targetNode.data && + targetNode.data.parameters && + targetNode.data.parameters.apiIns && + targetNode.data.parameters.apiIns.length > 0 + ) { + // 查找匹配的源句柄 + const matchingApiIn = targetNode.data.parameters.apiIns.find( + (apiIn: any) => apiIn.name === sourceHandle + ); + + if (matchingApiIn) { + finalTargetHandle = matchingApiIn.name; + } else { + // 如果没有精确匹配,使用第一个apiIn + finalTargetHandle = targetNode.data.parameters.apiIns[0].name; + } + } else { + // 默认句柄 + finalTargetHandle = 'start'; + } + } + + // 创建边的唯一标识符 + const edgeId = `${source}-${target}-${finalSourceHandle}-${finalTargetHandle}`; + + // 检查是否已添加此边 + if (!addedEdges.has(edgeId)) { + addedEdges.add(edgeId); + edges.push({ + id: `${edgeId}`, + source: source, + target: target, + sourceHandle: finalSourceHandle, + targetHandle: finalTargetHandle, + type: 'custom', + lineType: 'api', + data: { + lineType: 'api', + }, + }); + } + } + + // 处理数据下游连接 + for (const entry of nodeEntries) { + const nodeId: string = entry[0]; + const nodeConfig: any = entry[1]; + + if (nodeConfig.dataDownstream && Array.isArray(nodeConfig.dataDownstream)) { + nodeConfig.dataDownstream.forEach((connectionGroup: string[]) => { + // 确保 connectionGroup 是数组并且至少包含两个元素 + if (Array.isArray(connectionGroup) && connectionGroup.length >= 2) { + // 第一个元素是源节点和句柄信息 + const [sourceInfo, targetInfo] = connectionGroup; + + if ( + typeof sourceInfo === 'string' && + sourceInfo.includes('@@') && + typeof targetInfo === 'string' && + targetInfo.includes('@@') + ) { + const [sourceNodeId, sourceHandle] = sourceInfo.split('@@'); + const [targetNodeId, targetHandle] = targetInfo.split('@@'); + + // 创建边的唯一标识符 + const edgeId = `${sourceNodeId}-${targetNodeId}-${sourceHandle}-${targetHandle}`; + + // 检查是否已添加此边 + if (!addedEdges.has(edgeId)) { + addedEdges.add(edgeId); + + edges.push({ + id: `${edgeId}`, + source: sourceNodeId, + target: targetNodeId, + sourceHandle: sourceHandle, + targetHandle: targetHandle, + type: 'custom', + lineType: 'data', + data: { + lineType: 'data', + }, + }); + } + } + } + }); + } + } + + console.log('nodes, edges:', nodes, edges); + return { nodes, edges }; +}; + +/** + * 将 flow editor 的 nodes 和 edges 数据结构转换回原始数据结构 + * @param nodes - flow editor 的节点数组 + * @param edges - flow editor 的连线数组 + * @returns 原始数据结构 + */ +export const revertFlowData = (nodes: any[], edges: any[]) => { + // 初始化返回的数据结构 + const flowData: any = { + id: 'main', + nodeConfigs: [], + lineConfigs: [], + }; + + // 转换节点数据 + if (nodes && nodes.length > 0) { + flowData.nodeConfigs = nodes.map((node) => { + // 确定 nodeId 和 nodeName + const nodeId = node.id || node.name; + const nodeName = node.data?.title || nodeId; + + // 确定节点类型 + let nodeType = node.data.type; + // 特殊处理 start 和 end 节点 + if (nodeId.includes('start')) { + nodeType = 'start'; + } else if (nodeId.includes('end')) { + nodeType = 'end'; + } + + // 构造 x6 数据(位置信息) + const x6 = JSON.stringify({ + position: node.position, + }); + + // 构造 nodeConfig 对象 + const nodeConfig: any = { + nodeId, + nodeName, + x6, + }; + + // 处理 component 信息 + if (node.data?.component) { + nodeConfig.component = { + type: nodeType, + compIdentifier: node.data.component.compIdentifier || '', + compInstanceIdentifier: + node.data.component.compInstanceIdentifier || '', + compId: node.data.compId || '', + }; + if (node.data.component?.customDef) + nodeConfig.component.customDef = node.data.component.customDef; + } else if (nodeType !== 'start' && nodeType !== 'end') { + // 对于非 start/end 节点,添加基本的 component 信息 + nodeConfig.component = { + type: nodeType, + }; + } + if (['BASIC', 'SUB'].includes(nodeType)) + nodeConfig.component.compId = node.data.compId || ''; + + // 处理参数信息 + const parameters = node.data?.parameters || {}; + + // 处理 dataIns(输入数据) + if (parameters.dataIns && parameters.dataIns.length > 0) { + nodeConfig.dataIns = parameters.dataIns.map((input: any) => ({ + id: input.name || input.id, + desc: input.desc, + dataType: input.dataType, + defaultValue: input.defaultValue, + arrayType: input.arrayType || null, + })); + } + + // 处理 dataOuts(输出数据) + if (parameters.dataOuts && parameters.dataOuts.length > 0) { + nodeConfig.dataOuts = parameters.dataOuts.map((output: any) => ({ + id: output.name || output.id, + desc: output.desc, + dataType: output.dataType, + defaultValue: output.defaultValue, + arrayType: output.arrayType || null, + })); + } + + return nodeConfig; + }); + } + + // 转换连线数据 + if (edges && edges.length > 0) { + flowData.lineConfigs = edges.map((edge, index) => { + // 查找源节点和目标节点以确定连线类型 + const sourceNode = nodes.find((node) => node.id === edge.source); + const targetNode = nodes.find((node) => node.id === edge.target); + + let lineType = 'DATA'; // 默认为DATA类型 + + // 判断是否为CONVERT类型的连线 + if ( + targetNode && + ['JSONCONVERT', 'JSON2STR', 'STR2JSON'].includes(targetNode.type) + ) { + lineType = 'CONVERT'; + } + // 判断是否为API类型的连线 + else if ( + edge.sourceHandle && + (edge.sourceHandle === 'apiOuts' || + sourceNode?.data?.parameters?.apiOuts?.some( + (out: any) => (out.name || out.id) === edge.sourceHandle + )) + ) { + lineType = 'API'; + } else if ( + edge.targetHandle && + (edge.targetHandle === 'apiIns' || + targetNode?.data?.parameters?.apiIns?.some( + (inp: any) => (inp.name || inp.id) === edge.targetHandle + )) + ) { + lineType = 'API'; + } + + return { + id: edge.id || `edge_${index}`, // 如果没有 id,则生成一个 + lineType, // 添加lineType属性 + prev: { + nodeId: edge.source, + endpointId: edge.sourceHandle || 'done', // 默认使用 'done' + }, + next: { + nodeId: edge.target, + endpointId: edge.targetHandle || 'start', // 默认使用 'start' + }, + }; + }); + } + + return flowData; +}; + +/** + * 将 React Flow 的 nodes 和 edges 数据结构反向转换为 convertFlowData 可以处理的格式 + * @param nodes - React Flow 节点数组 + * @param edges - React Flow 边数组 + * @param complexKV - 复合组件使用的组件id对照表 {数字ID/nodeId:sub_ID} + * @returns 可用于 convertFlowData 的数据结构 + */ +export const reverseConvertFlowData = ( + nodes: any[], + edges: any[], + complexKV: any +) => { + // 初始化返回的数据结构 + const flowData: any = {}; + + // 转换节点数据 + if (nodes && nodes.length > 0) { + nodes.forEach((node) => { + const nodeId = node.id; + + // 构造节点配置对象 + const nodeConfig: any = { + id: nodeId, + componentName: node.data?.title || nodeId, + position: node.position || { x: 0, y: 0 }, + }; + + // 处理 component 信息 + if (node.type === 'SUB' && !node.data.component.customDef) { + let subflowId = ''; + if (complexKV && node.data.compId) { + // 遍历complexKV找到匹配的条目,使用节点ID进行匹配 + for (const key in complexKV) { + if (key.includes('/')) { + const [numericId, nodeId] = key.split('/'); + // 使用节点ID进行匹配 + if (nodeId === node.id) { + subflowId = complexKV[key]; + break; + } + } + } + } + + nodeConfig.component = { + type: 'SUB', + compId: node.data.compId, + customDef: JSON.stringify({ + dataIns: node.data.parameters.dataIns, + dataOuts: node.data.parameters.dataOuts, + subflowId: subflowId, + name: node.data.title, + }), + }; + } else if (node.data?.component) { + nodeConfig.component = { ...node.data.component }; + } else { + nodeConfig.component = { + type: node.type, + }; + } + + // 处理参数信息 + const parameters = node.data?.parameters || {}; + + // 处理 apiIns(输入API) + if (parameters.apiIns && parameters.apiIns.length > 0) { + nodeConfig.apiIns = parameters.apiIns; + } else { + nodeConfig.apiIns = []; + } + + // 处理 apiOuts(输出API) + if (parameters.apiOuts && parameters.apiOuts.length > 0) { + nodeConfig.apiOuts = parameters.apiOuts; + } else { + nodeConfig.apiOuts = []; + } + + // 处理 dataIns(输入数据) + if (parameters.dataIns && parameters.dataIns.length > 0) { + nodeConfig.dataIns = parameters.dataIns; + } else { + nodeConfig.dataIns = []; + } + + // 处理 dataOuts(输出数据) + if (parameters.dataOuts && parameters.dataOuts.length > 0) { + nodeConfig.dataOuts = parameters.dataOuts; + } else { + nodeConfig.dataOuts = []; + } + + // 初始化连接数组 + nodeConfig.apiDownstream = []; + nodeConfig.apiUpstream = []; + nodeConfig.dataDownstream = []; + nodeConfig.dataUpstream = []; + + // 将节点配置添加到 flowData 对象中 + flowData[nodeId] = nodeConfig; + }); + } + + // 处理连接关系 + if (edges && edges.length > 0) { + // 分析边的连接关系 + edges.forEach((edge) => { + const sourceNode = edge.source; + const targetNode = edge.target; + const sourceHandle = edge.sourceHandle || 'done'; + const targetHandle = edge.targetHandle || 'start'; + + // 确定连接类型(API 还是 DATA) + const sourceNodeData = nodes.find((n) => n.id === sourceNode); + const targetNodeData = nodes.find((n) => n.id === targetNode); + + const isApiConnection = + sourceNodeData?.data?.parameters?.apiOuts?.some( + (out: any) => (out.name || out.id) === sourceHandle + ) || + targetNodeData?.data?.parameters?.apiIns?.some( + (inp: any) => (inp.name || inp.id) === targetHandle + ) || + sourceHandle === 'start' || + targetHandle === 'end' || + sourceHandle === 'end' || + targetHandle === 'start'; + + if (isApiConnection) { + // API 连接 + // 添加下游连接 + flowData[sourceNode].apiDownstream.push([ + `${targetNode}$$${targetHandle}`, + ]); + + // 添加上游连接 + flowData[targetNode].apiUpstream.push([ + `${sourceNode}$$${sourceHandle}`, + ]); + } else { + // 数据连接 + const dataConnection = [ + `${sourceNode}@@${sourceHandle}`, + `${targetNode}@@${targetHandle}`, + ]; + flowData[sourceNode].dataDownstream.push(dataConnection); + flowData[targetNode].dataUpstream.push(dataConnection); + } + }); + } + + return flowData; +}; + +// 获取节点的数据输入参数 +const getNodeDataIns = (nodeConfig: any) => { + if (Array.isArray(nodeConfig.dataIns) && nodeConfig.dataIns.length > 0) { + return nodeConfig.dataIns; + } + + if (nodeConfig.component?.type === 'EVENTSEND_SYNC') { + return [ + { + arrayType: null, + dataType: 'STRING', + defaultValue: '', + desc: '输入', + id: 'in', + }, + ]; + } + + return []; +}; + +// 获取节点的API输入参数 +const getNodeApiIns = ( + nodeId: string, + nodeConfig: any, + currentProjectCompData: any[] +) => { + // JSON2STR 和 STR2JSON 不需要 API 输入 + if ( + nodeConfig.component?.type === 'JSON2STR' || + nodeConfig.component?.type === 'STR2JSON' + ) { + return []; + } + + // 对于特定类型的节点使用预定义值 + if (nodeConfig.component?.type === 'LOOP_START') { + return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; + } else if (nodeConfig.component?.type === 'LOOP_END') { + return [{ name: 'continue', desc: '', dataType: '', defaultValue: '' }]; + } else if (nodeId.includes('end')) { + return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }]; + } else if (nodeConfig.component?.type === 'SUB') { + return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; + } else { + const comp = currentProjectCompData.filter((item) => { + return (item.id || item?.comp?.id) === nodeConfig?.component?.compId; + }); + if (comp && comp.length > 0) { + const apiIns = comp[0]?.def?.apis || comp[0]?.comp?.def?.apis || []; + return apiIns.map((v) => { + return { + ...v, + name: v.id, + desc: v.desc, + dataType: v?.dataType || '', + defaultValue: v?.defaultValue || '', + }; + }); + } else { + return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; + } + } +}; + +// 获取节点的API输出参数 +const getNodeApiOuts = ( + nodeId: string, + nodeConfig: any, + currentProjectCompData: any[] +) => { + // JSON2STR 和 STR2JSON 不需要 API 输出 + if ( + nodeConfig.component?.type === 'JSON2STR' || + nodeConfig.component?.type === 'STR2JSON' + ) { + return []; + } + + // 对于特定类型的节点使用预定义值 + if (nodeConfig.component?.type === 'LOOP_START') { + return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; + } else if (nodeConfig.component?.type === 'LOOP_END') { + // 从customDef中获取apiOutIds数组 + try { + const customDef = JSON.parse(nodeConfig.component?.customDef || '{}'); + const apiOutIds = customDef.apiOutIds || []; + + // 从"break"开始的所有项都应该作为apiOut返回 + const breakIndex = apiOutIds.indexOf('break'); + if (breakIndex !== -1) { + // 返回从"break"开始的所有项 + return apiOutIds.slice(breakIndex).map((id) => ({ + name: id, + id: id, + desc: id, + dataType: '', + defaultValue: '', + })); + } else { + // 如果没有找到"break",则返回默认值 + return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }]; + } + } catch (e) { + // 解析失败时返回默认值 + return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }]; + } + } else if (nodeConfig.component?.type === 'SWITCH') { + // 从customDef中获取apiOutIds数组 + try { + const customDef = JSON.parse(nodeConfig.component?.customDef || '{}'); + const apiOutIds = customDef.apiOutIds || []; + + // 从"break"开始的所有项都应该作为apiOut返回 + const breakIndex = apiOutIds.indexOf('default'); + if (breakIndex !== -1) { + // 返回从"break"开始的所有项 + return apiOutIds.slice(breakIndex).map((id) => ({ + name: id, + id: id, + desc: id, + dataType: '', + defaultValue: '', + })); + } else { + // 如果没有找到"break",则返回默认值 + return [{ name: 'default', desc: '', dataType: '', defaultValue: '' }]; + } + } catch (e) { + // 解析失败时返回默认值 + return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; + } + } else if (nodeId.includes('start')) { + return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; + } else if (nodeId.includes('end')) { + return []; + } else if (nodeConfig.component?.type === 'SUB') { + return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; + } else { + const comp = currentProjectCompData.filter( + (item) => item.id === nodeConfig?.component?.compId + ); + if (comp && comp.length > 0) { + return [ + { + ...comp[0].def?.apiOut, + dataType: '', + defaultValue: '', + }, + ]; + } else { + return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; + } + } +}; + +// 获取节点的API输出句柄名称 +const getNodeApiOutHandle = (nodeId: string, nodeConfig: any) => { + if (nodeConfig.component?.type === 'LOOP_START') { + return 'done'; + } else if (nodeConfig.component?.type === 'LOOP_END') { + return 'break'; + } else if (nodeId.includes('start')) { + return 'start'; + } else if (nodeId.includes('end')) { + return 'end'; + } + return 'done'; +}; + +// 获取当前工程下组件列表并扁平化处理 +const getCurrentProjectStoreData = () => { + const { info, projectComponentData } = store.getState().ideContainer; + const compData = projectComponentData[info?.id] || {}; + + const result: any[] = []; + + // 处理projectCompDto中的数据 + if (compData.projectCompDto) { + const { + mineComp = [], + pubComp = [], + teamWorkComp = [], + } = compData.projectCompDto; + + // 添加mineComp数据 + mineComp.forEach((item: any) => { + result.push({ + ...item, + type: 'mineComp', + }); + }); + + // 添加pubComp数据 + pubComp.forEach((item: any) => { + result.push({ + ...item, + type: 'pubComp', + }); + }); + + // 添加teamWorkComp数据 + teamWorkComp.forEach((item: any) => { + result.push({ + ...item, + type: 'teamWorkComp', + }); + }); + } + + // 处理projectFlowDto中的数据 + if (compData.projectFlowDto) { + const { mineFlow = [], pubFlow = [] } = compData.projectFlowDto; + + // 添加mineFlow数据 + mineFlow.forEach((item: any) => { + result.push({ + ...item, + type: 'mineFlow', + }); + }); + + // 添加pubFlow数据 + pubFlow.forEach((item: any) => { + result.push({ + ...item, + type: 'pubFlow', + }); + }); + } + + // 如果从 store 中没有获取到数据,尝试从 sessionStorage 中获取 + if (result.length === 0) { + try { + const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}'); + const compLibsKey = `compLibs${userInfo.userId}`; + const compLibsData = sessionStorage.getItem(compLibsKey); + + if (compLibsData) { + const { + myLibs = [], + pubLibs = [], + teamLibs = [], + myFlow = [], + pubFlow = [], + } = JSON.parse(compLibsData); + + // 处理 myLibs(我的组件库) + if (Array.isArray(myLibs)) { + myLibs.forEach((lib: any) => { + if (lib.children && Array.isArray(lib.children)) { + lib.children.forEach((item: any) => { + result.push({ + ...item, + type: 'mineComp', + }); + }); + } + }); + } + + // 处理 pubLibs(公共组件库) + if (Array.isArray(pubLibs)) { + pubLibs.forEach((lib: any) => { + if (lib.children && Array.isArray(lib.children)) { + lib.children.forEach((item: any) => { + result.push({ + ...item, + type: 'pubComp', + }); + }); + } + }); + } + + // 处理 teamLibs(协同组件库) + if (Array.isArray(teamLibs)) { + teamLibs.forEach((lib: any) => { + if (lib.children && Array.isArray(lib.children)) { + lib.children.forEach((item: any) => { + result.push({ + ...item, + type: 'teamWorkComp', + }); + }); + } + }); + } + + // 处理 myFlow(我的流程) + if (Array.isArray(myFlow)) { + myFlow.forEach((item: any) => { + result.push({ + ...item, + type: 'mineFlow', + }); + }); + } + + // 处理 pubFlow(公共流程) + if (Array.isArray(pubFlow)) { + pubFlow.forEach((item: any) => { + result.push({ + ...item, + type: 'pubFlow', + }); + }); + } + } + } catch (error) { + console.error('从 sessionStorage 获取组件数据失败:', error); + } + } + return result; +}; diff --git a/src/features/workflow/domain/customDef.ts b/src/features/workflow/domain/customDef.ts new file mode 100644 index 0000000..8bb7bb5 --- /dev/null +++ b/src/features/workflow/domain/customDef.ts @@ -0,0 +1,28 @@ +export type CustomDefValue = Record; + +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); diff --git a/src/features/workflow/domain/types.ts b/src/features/workflow/domain/types.ts new file mode 100644 index 0000000..f0d213b --- /dev/null +++ b/src/features/workflow/domain/types.ts @@ -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; + [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; + +export type WorkflowEdge = Edge<{ + lineType?: WorkflowLineType; + displayData?: Record; + [key: string]: any; +}>; + +export interface WorkflowGraph { + id?: string; + mode: WorkflowMode; + nodes: WorkflowNode[]; + edges: WorkflowEdge[]; +} + +export interface FlowConvertResult { + nodes: WorkflowNode[]; + edges: WorkflowEdge[]; +} diff --git a/src/features/workflow/operations/clipboardOperations.ts b/src/features/workflow/operations/clipboardOperations.ts new file mode 100644 index 0000000..5a76ac1 --- /dev/null +++ b/src/features/workflow/operations/clipboardOperations.ts @@ -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(); + + 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, +}); diff --git a/src/features/workflow/operations/connectionOperations.ts b/src/features/workflow/operations/connectionOperations.ts new file mode 100644 index 0000000..fdf4904 --- /dev/null +++ b/src/features/workflow/operations/connectionOperations.ts @@ -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 }; +}; diff --git a/src/features/workflow/operations/deleteOperations.ts b/src/features/workflow/operations/deleteOperations.ts new file mode 100644 index 0000000..f41ddb6 --- /dev/null +++ b/src/features/workflow/operations/deleteOperations.ts @@ -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) + ), + }; +}; diff --git a/src/features/workflow/operations/edgeOperations.ts b/src/features/workflow/operations/edgeOperations.ts new file mode 100644 index 0000000..51c9fbd --- /dev/null +++ b/src/features/workflow/operations/edgeOperations.ts @@ -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, +]; diff --git a/src/features/workflow/operations/loopOperations.ts b/src/features/workflow/operations/loopOperations.ts new file mode 100644 index 0000000..f33b81c --- /dev/null +++ b/src/features/workflow/operations/loopOperations.ts @@ -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, +]; diff --git a/src/features/workflow/operations/nodeFactory.ts b/src/features/workflow/operations/nodeFactory.ts new file mode 100644 index 0000000..f592791 --- /dev/null +++ b/src/features/workflow/operations/nodeFactory.ts @@ -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; +}; diff --git a/src/features/workflow/operations/nodeOnboarding.ts b/src/features/workflow/operations/nodeOnboarding.ts new file mode 100644 index 0000000..687fe4c --- /dev/null +++ b/src/features/workflow/operations/nodeOnboarding.ts @@ -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, + }, + }; +}; diff --git a/src/features/workflow/operations/snapshot.ts b/src/features/workflow/operations/snapshot.ts new file mode 100644 index 0000000..acc4e49 --- /dev/null +++ b/src/features/workflow/operations/snapshot.ts @@ -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); +}; diff --git a/src/features/workflow/persistence/flowPersistence.ts b/src/features/workflow/persistence/flowPersistence.ts new file mode 100644 index 0000000..6ba4949 --- /dev/null +++ b/src/features/workflow/persistence/flowPersistence.ts @@ -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; + setNodes: React.Dispatch>; + setEdges: React.Dispatch>; +} + +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); + } +}; diff --git a/src/features/workflow/registry/nodeDescriptors.ts b/src/features/workflow/registry/nodeDescriptors.ts new file mode 100644 index 0000000..f9aee8b --- /dev/null +++ b/src/features/workflow/registry/nodeDescriptors.ts @@ -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; +} + +export const nodeDescriptors: Record = { + 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; +}; diff --git a/src/features/workflow/registry/nodeRegistry.ts b/src/features/workflow/registry/nodeRegistry.ts new file mode 100644 index 0000000..6b28889 --- /dev/null +++ b/src/features/workflow/registry/nodeRegistry.ts @@ -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 + ); + } +}; diff --git a/src/features/workflow/runtime/flowRuntime.ts b/src/features/workflow/runtime/flowRuntime.ts new file mode 100644 index 0000000..0505e5b --- /dev/null +++ b/src/features/workflow/runtime/flowRuntime.ts @@ -0,0 +1,34 @@ +export interface FlowCurrentAppData { + id?: string; + key?: string; +} + +export interface AppRuntimeState { + nodeStatusMap: Record; + 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: [], +}); diff --git a/src/features/workflow/runtime/runtimeActions.ts b/src/features/workflow/runtime/runtimeActions.ts new file mode 100644 index 0000000..f1b8efc --- /dev/null +++ b/src/features/workflow/runtime/runtimeActions.ts @@ -0,0 +1,183 @@ +import React from 'react'; +import { Edge } from '@xyflow/react'; +import { Message } from '@arco-design/web-react'; +import { Dispatch } from 'redux'; +import { + pauseApp, + reRunApp, + resumeApp, + runMainFlow, + runSubFlow, + stopApp, +} from '@/api/apps'; +import { + clearRuntimeLogs, + resetNodeStatus, + updateIsPaused, + updateIsRunning, + updateRuntimeId, +} from '@/store/ideContainer'; +import store from '@/store'; + +interface FlowRuntimeActionParams { + dispatch: Dispatch; + getCurrentFlowAppKey: () => string | null; +} + +interface RunWorkflowParams extends FlowRuntimeActionParams { + running: boolean; + setEdges: React.Dispatch>; +} + +const setEdgesRunningState = ( + setEdges: React.Dispatch>, + 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('重跑失败'); + } +}; diff --git a/src/hooks/useFlowCallbacks.ts b/src/hooks/useFlowCallbacks.ts index d99d4ab..f0baefd 100644 --- a/src/hooks/useFlowCallbacks.ts +++ b/src/hooks/useFlowCallbacks.ts @@ -7,85 +7,58 @@ import { Node, Edge, } from '@xyflow/react'; -import { getAppInfoNew, setMainFlowNew, setSubFlowNew } from '@/api/appRes'; import { Message } from '@arco-design/web-react'; -import { - convertFlowData, - reverseConvertFlowData, - revertFlowData, -} from '@/utils/convertFlowData'; import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData'; import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines'; import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode'; -import { - updateCanvasDataMap, - resetNodeStatus, - updateIsRunning, - updateIsPaused, - updateEventListOld, - addRuntimeLog, - clearRuntimeLogs, - updateRuntimeId, - updateFlowData, -} from '@/store/ideContainer'; -import { - validateAllNodes, - showValidationErrors, - validateAllEdges, -} from '@/components/FlowEditor/nodeEditors/validators/nodeValidators'; -import { getHandleType, validateDataType } from '@/utils/flowCommon'; +import { updateCanvasDataMap } from '@/store/ideContainer'; import { projectFlowHandle } from '@/pages/flowEditor/utils/projectFlowHandle'; import { appFLowHandle } from '@/pages/flowEditor/utils/appFlowhandle'; -import { - handelEventNodeList, - updateEvent, - upDatePublish, -} from '@/pages/flowEditor/utils/common'; -import { getCurrentAppKey } from '@/utils/flow/runtime'; +import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime'; import { ensureNodeTypeRegistered, resolveNodeComponent, -} from '@/utils/flow/nodeRegistry'; +} from '@/features/workflow/registry/nodeRegistry'; import { dispatchFlowSnapshot, dispatchFlowSnapshotAsync, -} from '@/utils/flow/snapshot'; +} from '@/features/workflow/operations/snapshot'; import { createLoopGroupEdge, createLoopInsertConnectionEdges, createLoopNodePair, -} from '@/utils/flow/loopFactory'; +} from '@/features/workflow/operations/loopOperations'; import { buildInsertedNodeEdges, resolveInsertedNodeHandles, -} from '@/utils/flow/edgeInsertionFactory'; +} from '@/features/workflow/operations/edgeOperations'; +import { + buildWorkflowConnectionEdge, + validateWorkflowConnection, +} from '@/features/workflow/operations/connectionOperations'; +import { + buildCopiedFlowData, + buildPastedFlowData, + buildPastedSingleNode, +} from '@/features/workflow/operations/clipboardOperations'; +import { + isLoopBoundaryNode, + removeNodesAndConnectedEdges, + resolveDeletedNodesWithLoopPairs, +} from '@/features/workflow/operations/deleteOperations'; import { buildRuntimeNode, resolveNodeDefinition, -} from '@/utils/flow/nodeOnboarding'; +} from '@/features/workflow/operations/nodeOnboarding'; +import { saveWorkflowData } from '@/features/workflow/persistence/flowPersistence'; +import { + pauseWorkflow, + rerunWorkflow, + runWorkflow, +} from '@/features/workflow/runtime/runtimeActions'; import { Dispatch } from 'redux'; -import { - getAppListBySceneId, - runMainFlow, - runSubFlow, - stopApp, - pauseApp, - resumeApp, - reRunApp, -} from '@/api/apps'; import store from '@/store'; -import { - updateAppEvent, - updateAppEventChannel, - updateAppFlowData, -} from '@/api/appEvent'; -import { getUrlParams, sleep } from '@/utils/common'; -import { - queryEventItemBySceneIdOld, - deleteEventSub, - deleteEventPub, -} from '@/api/event'; export const useFlowCallbacks = ( nodes: Node[], @@ -179,159 +152,27 @@ export const useFlowCallbacks = ( // 边连接处理 const onConnect = useCallback( (params: any) => { - // 获取源节点和目标节点 - const sourceNode = nodes.find((node) => node.id === params.source); - const targetNode = nodes.find((node) => node.id === params.target); - - // 如果找不到节点,不创建连接 - if (!sourceNode || !targetNode) { + const validation = validateWorkflowConnection(nodes, params); + if (!validation.isValid) { + if (validation.message) console.warn(validation.message); return; } - // 不允许链接到自己的节点上 - if (sourceNode.id === targetNode.id) { - console.warn("不允许自旋链接"); - return; - } - // 获取源节点和目标节点的参数信息 - const sourceParams: any = sourceNode.data?.parameters || {}; - const targetParams: any = targetNode.data?.parameters || {}; - - // 获取源handle和目标handle的类型 (api或data) - const sourceHandleType = getHandleType(params.sourceHandle, sourceParams); - const targetHandleType = getHandleType(params.targetHandle, targetParams); - - // 验证连接类型是否匹配 (api只能连api, data只能连data) - if (sourceHandleType !== targetHandleType) { - console.warn('连接类型不匹配: ', sourceHandleType, targetHandleType); - return; - } - - // 验证数据类型是否匹配 - if ( - !validateDataType( - sourceNode, - targetNode, - params.sourceHandle, - params.targetHandle - ) - ) { - console.warn('数据类型不匹配'); - return; - } - - // 如果验证通过,创建连接 setEdges((edgesSnapshot: Edge[]) => { - // 创建带有事件信息的连接 - const edgeParams = { ...params, type: 'custom' }; - - // 添加lineType字段,用于区分API连接和数据连接 - edgeParams.data = { - ...edgeParams.data, - lineType: sourceHandleType, // 'api' 或 'data' - }; - - // 对于数据类型的边,需要额外验证dataIns和dataOuts中的数据类型是否一致 - if (sourceHandleType === 'data') { - // 查找源节点的dataOuts中对应的数据 - const sourceDataOut = (sourceParams.dataOuts || []).find( - (dataOut: any) => - dataOut.name === params.sourceHandle || - dataOut.id === params.sourceHandle - ); - - // 查找目标节点的dataIns中对应的数据 - const targetDataIn = (targetParams.dataIns || []).find( - (dataIn: any) => - dataIn.name === params.targetHandle || - dataIn.id === params.targetHandle - ); - - // 验证数据类型是否一致 - if ( - sourceDataOut && - targetDataIn && - sourceDataOut.dataType !== targetDataIn.dataType - ) { - console.warn( - '数据类型不匹配,源节点数据类型:', - sourceDataOut.dataType, - '目标节点数据类型:', - targetDataIn.dataType - ); - Message.warning( - `数据类型不匹配,源节点数据类型: ${sourceDataOut.dataType},目标节点数据类型: ${targetDataIn.dataType}` - ); - return edgesSnapshot; // 不创建连接 - } - } - - // 检查源节点和目标节点是否都有事件信息 - const sourceApi = (sourceParams.apiOuts || []).find( - (api: any) => - (api?.eventId || api.name || api.id) === params.sourceHandle - ); - const targetApi = (targetParams.apiIns || []).find( - (api: any) => - (api?.eventId || api.name || api.id) === params.targetHandle + const result = buildWorkflowConnectionEdge( + nodes, + params, + validation.lineType || 'data' ); - - // 如果源节点有事件topic信息 - if (sourceApi && sourceApi.topic) { - // 如果目标节点的topic是**empty**或没有topic,则使用源节点的事件信息 - if ( - !targetApi || - !targetApi.topic || - targetApi.topic.includes('**empty**') - ) { - edgeParams.data = { - ...edgeParams.data, - lineType: 'api', - displayData: { - name: sourceApi.eventName, - eventId: sourceApi.eventId, - topic: sourceApi.topic, - }, - }; - } - // 如果两个节点都有非empty的topic,则以源节点为准 - else if ( - sourceApi.topic && - targetApi.topic && - !sourceApi.topic.includes('**empty**') && - !targetApi.topic.includes('**empty**') - ) { - edgeParams.data = { - ...edgeParams.data, - lineType: 'api', - displayData: { - name: sourceApi.eventName, - eventId: sourceApi.eventId, - topic: sourceApi.topic, - }, - }; + if (!result?.edge) { + if (result?.message) { + console.warn(result.message); + Message.warning(result.message); } + return edgesSnapshot; } - // 如果源节点没有事件信息,但目标节点有 - else if ( - targetApi && - targetApi.topic && - !targetApi.topic.includes('**empty**') - ) { - edgeParams.data = { - ...edgeParams.data, - lineType: 'api', - displayData: { - name: targetApi.eventName, - eventId: targetApi.eventId, - topic: targetApi.topic, - }, - }; - } - - const newEdges = addEdge(edgeParams, edgesSnapshot); - // 连接建立后记录历史 + const newEdges = addEdge(result.edge, edgesSnapshot); dispatchFlowSnapshotAsync({ nodes: [...nodes], edges: [...newEdges] }); return newEdges; @@ -342,49 +183,12 @@ export const useFlowCallbacks = ( // 边重新连接处理 const onReconnect = useCallback( (oldEdge: Edge, newConnection: any) => { - // 获取源节点和目标节点 - const sourceNode = nodes.find((node) => node.id === newConnection.source); - const targetNode = nodes.find((node) => node.id === newConnection.target); - - // 如果找不到节点,不创建连接 - if (!sourceNode || !targetNode) { - return; - } - - // 获取源节点和目标节点的参数信息 - const sourceParams = sourceNode.data?.parameters || {}; - const targetParams = targetNode.data?.parameters || {}; - - // 获取源handle和目标handle的类型 (api或data) - const sourceHandleType = getHandleType( - newConnection.sourceHandle, - sourceParams - ); - const targetHandleType = getHandleType( - newConnection.targetHandle, - targetParams - ); - - // 验证连接类型是否匹配 (api只能连api, data只能连data) - if (sourceHandleType !== targetHandleType) { - console.warn('连接类型不匹配: ', sourceHandleType, targetHandleType); + const validation = validateWorkflowConnection(nodes, newConnection); + if (!validation.isValid) { + if (validation.message) console.warn(validation.message); return; } - // 验证数据类型是否匹配 - if ( - !validateDataType( - sourceNode, - targetNode, - newConnection.sourceHandle, - newConnection.targetHandle - ) - ) { - console.warn('数据类型不匹配'); - return; - } - - // 如果验证通过,重新连接 setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); }, [nodes] @@ -595,47 +399,20 @@ export const useFlowCallbacks = ( // 复制节点(支持多节点和多边) const copyNode = useCallback( (node: Node) => { - // 获取所有选中的节点(包括当前节点) - const selectedNodes = nodes.filter((n) => n.selected || n.id === node.id); - - // 过滤掉开始和结束节点 - const nodesToCopy = selectedNodes.filter( - (n) => n.type !== 'start' && n.type !== 'end' + const copiedData = buildCopiedFlowData( + node, + nodes, + edges, + initialData?.appId ); - - if (nodesToCopy.length === 0) { + if (!copiedData) { console.warn('没有可复制的节点(开始和结束节点不能复制)'); return; } - // 获取这些节点之间的边 - const nodeIds = new Set(nodesToCopy.map((n) => n.id)); - const edgesToCopy = edges.filter( - (e) => nodeIds.has(e.source) && nodeIds.has(e.target) - ); - - // 清除运行时状态 - const cleanedNodes = nodesToCopy.map((n) => ({ - ...n, - selected: false, - dragging: false, - })); - - const cleanedEdges = edgesToCopy.map((e) => ({ - ...e, - selected: false, - })); - - // 存储复制的数据 - const copiedData = { - nodes: cleanedNodes, - edges: cleanedEdges, - appId: initialData?.appId, - }; - localStorage.setItem('copiedFlowData', JSON.stringify(copiedData)); console.log( - `已复制 ${nodesToCopy.length} 个节点和 ${edgesToCopy.length} 条边` + `已复制 ${copiedData.nodes.length} 个节点和 ${copiedData.edges.length} 条边` ); }, [nodes, edges, initialData?.appId] @@ -670,97 +447,40 @@ export const useFlowCallbacks = ( return; } - const { nodes: copiedNodes, edges: copiedEdges } = copiedData; - - if (!copiedNodes || copiedNodes.length === 0) { + const pastedData = buildPastedFlowData(copiedData, position); + if (!pastedData) { console.warn('没有可粘贴的节点'); return; } - // 计算所有节点的边界框中心点 - const minX = Math.min(...copiedNodes.map((n: Node) => n.position.x)); - const minY = Math.min(...copiedNodes.map((n: Node) => n.position.y)); - const maxX = Math.max(...copiedNodes.map((n: Node) => n.position.x)); - const maxY = Math.max(...copiedNodes.map((n: Node) => n.position.y)); - const centerX = (minX + maxX) / 2; - const centerY = (minY + maxY) / 2; - - // 计算偏移量,使粘贴的节点组以鼠标位置为中心 - const offsetX = position.x - centerX; - const offsetY = position.y - centerY; - - // 创建旧ID到新ID的映射 - const idMap = new Map(); - const timestamp = Date.now(); - - // 创建新节点 - const newNodes = copiedNodes.map((node: Node, index: number) => { - const newId = `${node.type}-${timestamp}-${index}`; - idMap.set(node.id, newId); - - const newNode = { - ...node, - id: newId, - position: { - x: node.position.x + offsetX, - y: node.position.y + offsetY, - }, - selected: false, - dragging: false, - appId: undefined, - }; - - // 注册节点类型 + pastedData.nodes.forEach((newNode: Node) => { ensureNodeTypeRegistered( newNode.type, (newNode.data?.title as string) || newNode.type, resolveNodeComponent(newNode.type) ); - - return newNode; }); - // 创建新边,使用新的节点ID - const newEdges = copiedEdges - .map((edge: Edge, index: number) => { - const newSourceId = idMap.get(edge.source); - const newTargetId = idMap.get(edge.target); - - if (!newSourceId || !newTargetId) { - console.warn('边的源节点或目标节点未找到:', edge); - return null; - } - - return { - ...edge, - id: `e${newSourceId}-${newTargetId}-${timestamp}-${index}`, - source: newSourceId, - target: newTargetId, - selected: false, - }; - }) - .filter(Boolean); // 过滤掉null值 - // 更新节点和边 setNodes((nds: Node[]) => { - const updatedNodes = [...nds, ...newNodes]; + const updatedNodes = [...nds, ...pastedData.nodes]; // 添加节点后记录历史 dispatchFlowSnapshotAsync({ nodes: [...updatedNodes], - edges: [...edges, ...newEdges], + edges: [...edges, ...pastedData.edges], }); return updatedNodes; }); setEdges((eds: Edge[]) => { - const updatedEdges = [...eds, ...newEdges]; + const updatedEdges = [...eds, ...pastedData.edges]; return updatedEdges; }); console.log( - `已粘贴 ${newNodes.length} 个节点和 ${newEdges.length} 条边` + `已粘贴 ${pastedData.nodes.length} 个节点和 ${pastedData.edges.length} 条边` ); } // 处理旧格式(单节点)- 保持向后兼容 @@ -778,14 +498,7 @@ export const useFlowCallbacks = ( } // 创建新节点,更新ID和位置 - const newNode = { - ...copiedNode, - id: `${copiedNode.type}-${Date.now()}`, - position, - selected: false, - dragging: false, - appId: undefined, - }; + const newNode = buildPastedSingleNode(copiedNode, position); // 特殊处理循环节点 if (copiedNode.type === 'LOOP') { @@ -805,7 +518,7 @@ export const useFlowCallbacks = ( // 注册节点类型 ensureNodeTypeRegistered( newNode.type, - newNode.data?.title || newNode.type, + (newNode.data?.title as string) || newNode.type, resolveNodeComponent(newNode.type) ); @@ -844,88 +557,23 @@ export const useFlowCallbacks = ( return; } - // 处理循环节点删除逻辑 - if (node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END') { - // 获取关联的另一个循环节点 - let relatedNodeId = null; - - // 类型断言,将component从unknown转换为具有customDef属性的对象 - const component = node.data?.component as - | { customDef?: string } - | undefined; - - if (node.data?.type === 'LOOP_START' && component?.customDef) { - try { - const customDef = JSON.parse(component.customDef); - relatedNodeId = customDef.loopEndNodeId; - } catch (e) { - console.error('解析循环开始节点数据失败:', e); - } - } else if (node.data?.type === 'LOOP_END' && component?.customDef) { - try { - const customDef = JSON.parse(component.customDef); - relatedNodeId = customDef.loopStartNodeId; - } catch (e) { - console.error('解析循环结束节点数据失败:', e); - } - } - - // 删除两个节点及相关边 - setNodes((nds: Node[]) => { - const updatedNodes = nds.filter( - (n) => n.id !== node.id && n.id !== relatedNodeId - ); - return updatedNodes; - }); - - setEdges((eds: Edge[]) => { - const updatedEdges = eds.filter( - (e) => - e.source !== node.id && - e.target !== node.id && - e.source !== relatedNodeId && - e.target !== relatedNodeId - ); - return updatedEdges; - }); - - // 删除节点后记录历史 - setTimeout(() => { - const updatedNodes = nodes.filter( - (n) => n.id !== node.id && n.id !== relatedNodeId - ); - const updatedEdges = edges.filter( - (e) => - e.source !== node.id && - e.target !== node.id && - e.source !== relatedNodeId && - e.target !== relatedNodeId - ); - - dispatchFlowSnapshot({ - nodes: [...updatedNodes], - edges: [...updatedEdges], - }); - }, 0); - - return; - } - - // 普通节点删除逻辑 - setNodes((nds: Node[]) => nds.filter((n) => n.id !== node.id)); - setEdges((eds: Edge[]) => - eds.filter((e) => e.source !== node.id && e.target !== node.id) + const nodesToRemove = isLoopBoundaryNode(node) + ? resolveDeletedNodesWithLoopPairs([node], nodes) + : [node]; + const nextGraph = removeNodesAndConnectedEdges( + nodes, + edges, + nodesToRemove ); + setNodes(nextGraph.nodes); + setEdges(nextGraph.edges); + // 删除节点后记录历史 setTimeout(() => { dispatchFlowSnapshot({ - nodes: [...nodes.filter((n) => n.id !== node.id)], - edges: [ - ...edges.filter( - (e) => e.source !== node.id && e.target !== node.id - ), - ], + nodes: [...nextGraph.nodes], + edges: [...nextGraph.edges], }); }, 0); }, @@ -1140,341 +788,27 @@ export const useFlowCallbacks = ( // endregion const saveFlowDataToServer = useCallback(async () => { - 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); - - let params = {}; - // 更新复合组件/子流程 - if (currentAppData.key.includes('sub')) { - const appEventDefinition = updateEvent( - revertedData.nodeConfigs, - initialData.appId - ); - 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); - // 更新 flowData 中的数据 - dispatch( - updateFlowData({ [currentAppData.parentAppId]: appRes.data }) - ); - // 同步更新主流程到 canvasDataMap(使用父应用ID) - 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, - }, - }) - ); - } - // 同步更新子流程到 canvasDataMap(使用子流程key) - dispatch( - updateCanvasDataMap({ - ...canvasDataMap, - [currentAppData.key]: { nodes, edges }, - }) - ); - } else { - Message.error(res.message); - } - } - // 更新主流程 - else { - const appEventDefinition = updateEvent( - revertedData.nodeConfigs, - initialData.appId - ); - 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); - // 更新 flowData 中的数据 - dispatch(updateFlowData({ [currentAppData.id]: appRes.data })); - // 同步更新到 canvasDataMap - 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('保存失败'); - } - } else { - const appFlowParams = { - 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, - }, - }, - }); - - // 应用组件的桩点id就是事件id - const sourceId = edge.sourceHandle; - const targetId = edge.targetHandle; - const topic = edge.data.displayData?.topic; - - if (eventMap.has(topic)) { - // 如果topic已存在,将eventId添加到数组中 - eventMap.get(topic).eventId.push(sourceId); - eventMap.get(topic).eventId.push(targetId); - } else { - // 如果topic不存在,创建新的条目 - eventMap.set(topic, { - eventId: [sourceId, targetId], - topic: topic, - }); - } - }); - // 对eventId数组进行去重处理 - 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) { - console.error('保存失败:', error); - Message.error('保存失败: ' + error.message); - } - } - }, [nodes, edges, initialData?.appId]); + await saveWorkflowData({ + nodes, + edges, + useDefault, + initialData, + canvasDataMap, + dispatch, + setNodes, + setEdges, + }); + }, [nodes, edges, useDefault, initialData, canvasDataMap, dispatch]); // 运行处理函数 const handleRun = useCallback( async (running: boolean) => { - const { currentAppData, socketId, appRuntimeData } = - store.getState().ideContainer; - const appKey = getCurrentFlowAppKey(); - - if (running) { - // 子流程运行 - if (currentAppData.key.includes('sub')) { - // 启动运行 - const params = { - appId: currentAppData.parentAppId, - socketId, - subflowId: currentAppData.key, - }; - const res: any = await runSubFlow(params); - - if (res.code === 200) { - // 设置运行状态为true - dispatch(updateIsRunning(true)); - - // 重置节点状态 - dispatch(resetNodeStatus()); - - // 更新运行ID - dispatch(updateRuntimeId(res.data)); - - // 开始运行时动画 - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - data: { - ...edge.data, - isRunning: true, - animationProgress: 0, - }, - })) - ); - } else { - Message.error(res.message); - } - } - // 主流程运行 - else { - // 启动运行 - const params = { - appId: currentAppData.id, - socketId, - }; - const res: any = await runMainFlow(params); - if (res.code === 200) { - // 设置运行状态为true - dispatch(updateIsRunning(true)); - - // 重置节点状态 - dispatch(resetNodeStatus()); - - // 更新运行ID - dispatch(updateRuntimeId(res.data)); - - // 开始运行时动画 - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - data: { - ...edge.data, - isRunning: true, - animationProgress: 0, - }, - })) - ); - } else { - Message.error(res.message); - } - } - } else { - // 设置运行状态为false - dispatch(updateIsRunning(false)); - - // 使用正确的 appKey 获取 runId - 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()); - - // 更新运行ID - dispatch(updateRuntimeId('')); - - // 停止运行 - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - data: { - ...edge.data, - isRunning: false, - animationProgress: 0, - }, - })) - ); - - // 清空当前应用的运行日志 - if (appKey) { - dispatch(clearRuntimeLogs({ appId: appKey })); - } - } + await runWorkflow({ + running, + dispatch, + getCurrentFlowAppKey, + setEdges, + }); }, [getCurrentFlowAppKey] ); @@ -1482,98 +816,14 @@ export const useFlowCallbacks = ( // 暂停/恢复应用 const handlePause = useCallback( async (isPaused: boolean) => { - const { currentAppData, appRuntimeData } = store.getState().ideContainer; - const appKey = getCurrentFlowAppKey(); - - if (!currentAppData) { - Message.warning('请先选择一个应用'); - return; - } - - // 获取runId - const runId = - appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : ''; - - if (!runId) { - Message.warning('应用未运行'); - return; - } - - try { - if (isPaused) { - // 当前已暂停,执行恢复操作 - const res: any = await resumeApp({ id: runId }); - if (res.code === 200) { - Message.success('应用已恢复'); - // 更新暂停状态为 false - dispatch(updateIsPaused(false)); - } else { - Message.error(res.msg || '恢复失败'); - } - } else { - // 当前正在运行,执行暂停操作 - const res: any = await pauseApp({ id: runId }); - if (res.code === 200) { - Message.success('应用已暂停'); - // 更新暂停状态为 true - dispatch(updateIsPaused(true)); - } else { - Message.error(res.msg || '暂停失败'); - } - } - } catch (error) { - console.error('暂停/恢复失败:', error); - Message.error('操作失败'); - } + await pauseWorkflow({ isPaused, dispatch, getCurrentFlowAppKey }); }, [getCurrentFlowAppKey] ); // 重跑应用 const handleReRun = useCallback(async () => { - const { currentAppData, appRuntimeData, socketId } = - store.getState().ideContainer; - const appKey = getCurrentFlowAppKey(); - - if (!currentAppData) { - Message.warning('请先选择一个应用'); - return; - } - - // 获取runId (instanceId) - 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('重跑失败'); - } + await rerunWorkflow({ dispatch, getCurrentFlowAppKey }); }, [getCurrentFlowAppKey]); return { diff --git a/src/hooks/useFlowEditorState.ts b/src/hooks/useFlowEditorState.ts index 7acb218..5df7398 100644 --- a/src/hooks/useFlowEditorState.ts +++ b/src/hooks/useFlowEditorState.ts @@ -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'; diff --git a/src/pages/flowEditor/FlowEditorMain.tsx b/src/pages/flowEditor/FlowEditorMain.tsx index a4c4367..287949a 100644 --- a/src/pages/flowEditor/FlowEditorMain.tsx +++ b/src/pages/flowEditor/FlowEditorMain.tsx @@ -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 = (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 = (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); diff --git a/src/pages/flowEditor/components/actionBar.tsx b/src/pages/flowEditor/components/actionBar.tsx index 2cdbd10..d5d5e59 100644 --- a/src/pages/flowEditor/components/actionBar.tsx +++ b/src/pages/flowEditor/components/actionBar.tsx @@ -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; diff --git a/src/pages/flowEditor/components/historyVersionModal.tsx b/src/pages/flowEditor/components/historyVersionModal.tsx index bbc0e8e..6a8660f 100644 --- a/src/pages/flowEditor/components/historyVersionModal.tsx +++ b/src/pages/flowEditor/components/historyVersionModal.tsx @@ -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'; diff --git a/src/pages/flowEditor/utils/appFlowhandle.ts b/src/pages/flowEditor/utils/appFlowhandle.ts index 030d7ea..994a528 100644 --- a/src/pages/flowEditor/utils/appFlowhandle.ts +++ b/src/pages/flowEditor/utils/appFlowhandle.ts @@ -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 } // })); // } -}; \ No newline at end of file +}; diff --git a/src/pages/flowEditor/utils/projectFlowHandle.ts b/src/pages/flowEditor/utils/projectFlowHandle.ts index 1d90aef..ece14bd 100644 --- a/src/pages/flowEditor/utils/projectFlowHandle.ts +++ b/src/pages/flowEditor/utils/projectFlowHandle.ts @@ -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 } })); } -}; \ No newline at end of file +}; diff --git a/src/pages/ideContainer/logBar.tsx b/src/pages/ideContainer/logBar.tsx index aa3b4db..5cbd4a3 100644 --- a/src/pages/ideContainer/logBar.tsx +++ b/src/pages/ideContainer/logBar.tsx @@ -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; diff --git a/src/pages/ideContainer/sideBar.tsx b/src/pages/ideContainer/sideBar.tsx index 1cd9da9..4e04333 100644 --- a/src/pages/ideContainer/sideBar.tsx +++ b/src/pages/ideContainer/sideBar.tsx @@ -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 = ({ ); }; -export default SideBar; \ No newline at end of file +export default SideBar; diff --git a/src/store/ideContainer.ts b/src/store/ideContainer.ts index 3dddcaa..a6099c9 100644 --- a/src/store/ideContainer.ts +++ b/src/store/ideContainer.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { createDefaultAppRuntimeState, getCurrentAppKey, -} from '@/utils/flow/runtime'; +} from '@/features/workflow/runtime/flowRuntime'; // 定义初始状态类型 interface IDEContainerState { diff --git a/src/utils/convertAppFlowData.ts b/src/utils/convertAppFlowData.ts index 486727e..d78b7e6 100644 --- a/src/utils/convertAppFlowData.ts +++ b/src/utils/convertAppFlowData.ts @@ -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 }; -}; \ No newline at end of file +export { convertAppFlowData } from '@/features/workflow/adapters/appFlowAdapter'; diff --git a/src/utils/convertFlowData.ts b/src/utils/convertFlowData.ts index f650169..0c77b47 100644 --- a/src/utils/convertFlowData.ts +++ b/src/utils/convertFlowData.ts @@ -1,1041 +1,5 @@ -import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node'; -import store from '@/store/index'; -import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode'; -import { updateEventNodeList } from '@/store/ideContainer'; -import { resolveNodeComponent } from '@/utils/flow/nodeRegistry'; - -/** - * 将提供的数据结构转换为适用于 flow editor 的 nodes 和 edges - * @param flowData - 原始数据结构 - * @param useDefault - 当flowData为空时是否返回默认的开始和结束节点 - * @returns 包含 nodes 和 edges 的对象 - */ -export const convertFlowData = (flowData: any, useDefault = true) => { - const nodes: any[] = []; - const edges: any[] = []; - const eventSendNodeList = []; - const eventlisteneList = []; - const currentProjectCompData = getCurrentProjectStoreData(); - - if (!flowData || Object.keys(flowData).length === 0) { - // 如果useDefault为true且flowData为空,则返回默认的开始和结束节点 - const timestamp = Date.now(); - if (useDefault) { - return { - nodes: [ - { - id: `start`, - type: 'start', - position: { x: 200, y: 200 }, - data: { - title: '开始', - parameters: { - apiIns: [], - apiOuts: [ - { name: 'start', desc: '', dataType: '', defaultValue: '' }, - ], - dataIns: [], - dataOuts: [], - }, - type: 'start', - }, - }, - { - id: `end-${timestamp}`, - type: 'end', - position: { x: 600, y: 200 }, - data: { - title: '结束', - parameters: { - apiIns: [ - { name: 'end', desc: '', dataType: '', defaultValue: '' }, - ], - apiOuts: [], - dataIns: [], - dataOuts: [], - }, - type: 'end', - }, - }, - ], - edges: [], - }; - } - // 否则返回空数组 - return { nodes, edges }; - } - - // 处理新格式的数据结构 - // 先处理所有节点 - const nodeEntries = Object.entries(flowData); - - for (const entry of nodeEntries) { - const nodeId: string = entry[0]; - const nodeConfig: any = entry[1]; - - // 更新应用中的事件节点列表 - if (nodeId.includes('EVENTLISTENE')) { - try { - const customDef = JSON.parse(nodeConfig.component.customDef); - // 使用展开运算符创建新数组,避免修改冻结对象 - eventlisteneList.splice(eventlisteneList.length, 0, { - [nodeId]: customDef.topic, - }); - } catch (error) { - console.log(error); - } - } else if ( - nodeId.includes('EVENTSEND') && - !nodeId.includes('EVENTSEND_SYNC') - ) { - try { - const customDef = JSON.parse(nodeConfig.component.customDef); - // 使用展开运算符创建新数组,避免修改冻结对象 - eventSendNodeList.splice(eventSendNodeList.length, 0, { - [nodeId]: customDef.topic, - }); - } catch (error) { - console.log(error); - } - } - - if (eventlisteneList.length > 0 || eventSendNodeList.length > 0) - store.dispatch( - updateEventNodeList({ - eventSendNodeList: [...eventSendNodeList], - eventlisteneList: [...eventlisteneList], - }) - ); - else { - store.dispatch( - updateEventNodeList({ - eventSendNodeList: [], - eventlisteneList: [], - }) - ); - } - - // 确定节点类型 - let nodeType = 'BASIC'; - if (nodeId.includes('start')) { - nodeType = 'start'; - } else if (nodeId.includes('end')) { - nodeType = 'end'; - } else if ( - nodeConfig.component?.type === 'LOOP_START' || - nodeConfig.component?.type === 'LOOP_END' - ) { - nodeType = 'LOOP'; - } else { - nodeType = nodeConfig.component?.type || 'BASIC'; - } - // 解析位置信息 - const position = nodeConfig.position || { x: 0, y: 0 }; - - // 构造节点数据 - const node: any = { - id: nodeId, - type: nodeType, - position, - data: { - title: nodeConfig.componentName || nodeId, - parameters: { - apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData), - apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData), - dataIns: getNodeDataIns(nodeConfig), - dataOuts: nodeConfig.dataOuts || [], - }, - type: nodeConfig.component?.type || nodeType, - }, - }; - - // 添加组件标识信息 - if (nodeConfig.component) { - node.data.component = { ...nodeConfig.component }; - node.data.compId = nodeConfig.component.compId; - } - - // 保留节点状态信息(用于历史实例查看) - if (nodeConfig.status) { - node.data.status = nodeConfig.status; - } - if (nodeConfig.isStatusVisible !== undefined) { - node.data.isStatusVisible = nodeConfig.isStatusVisible; - } - - // 注册循环节点类型 - if (nodeType === 'LOOP') { - const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key)); - if (!nodeMap.includes('LOOP')) { - registerNodeType('LOOP', LoopNode, '循环'); - } - } - - // 注册其他节点类型 - const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key)); - if ( - !nodeMap.includes(nodeType) && - nodeType !== 'start' && - nodeType !== 'end' && - nodeType !== 'LOOP' - ) { - registerNodeType( - nodeType, - resolveNodeComponent(nodeType), - nodeConfig.componentName - ); - } - - nodes.push(node); - } - - // 用于存储已添加的边,避免重复 - const addedEdges = new Set(); - - // 创建一个映射来存储所有连接信息 - const connections = new Map< - string, - { - source: string; - target: string; - sourceHandle: string; - targetHandle: string; - } - >(); - - // 遍历所有节点,收集连接信息 - for (const entry of nodeEntries) { - const nodeId: string = entry[0]; - const nodeConfig: any = entry[1]; - - // 处理 API 下游连接 - 确定目标节点信息 - if (nodeConfig.apiDownstream && Array.isArray(nodeConfig.apiDownstream)) { - nodeConfig.apiDownstream.forEach((targetArray: string[]) => { - if (Array.isArray(targetArray)) { - targetArray.forEach((target) => { - if (typeof target === 'string' && target.includes('$$')) { - const [targetNodeId, targetHandle] = target.split('$$'); - const connectionKey = `${nodeId}-${targetNodeId}`; - - // 存储连接信息 - if (connections.has(connectionKey)) { - // 如果连接已存在,更新目标句柄 - const existing = connections.get(connectionKey); - if (existing) { - connections.set(connectionKey, { - ...existing, - targetHandle: targetHandle, - }); - } - } else { - // 创建新的连接信息 - connections.set(connectionKey, { - source: nodeId, - target: targetNodeId, - sourceHandle: '', // 将根据节点信息填充 - targetHandle: targetHandle, - }); - } - } - }); - } - }); - } - - // 处理 API 上游连接 - 确定源节点信息 - if (nodeConfig.apiUpstream && Array.isArray(nodeConfig.apiUpstream)) { - nodeConfig.apiUpstream.forEach((sourceArray: string[]) => { - if (Array.isArray(sourceArray)) { - sourceArray.forEach((source) => { - if (typeof source === 'string' && source.includes('$$')) { - const [sourceNodeId, sourceHandle] = source.split('$$'); - const connectionKey = `${sourceNodeId}-${nodeId}`; - - // 存储连接信息 - if (connections.has(connectionKey)) { - // 如果连接已存在,更新源句柄 - const existing = connections.get(connectionKey); - if (existing) { - connections.set(connectionKey, { - ...existing, - sourceHandle: sourceHandle, - }); - } - } else { - // 创建新的连接信息 - connections.set(connectionKey, { - source: sourceNodeId, - target: nodeId, - sourceHandle: sourceHandle, - targetHandle: '', // 将根据节点信息填充 - }); - } - } - }); - } - }); - } - } - - // 根据收集的连接信息生成实际的边 - const connectionEntries = Array.from(connections.entries()); - for (const [connectionKey, connectionInfo] of connectionEntries) { - const { source, target, sourceHandle, targetHandle } = connectionInfo; - - // 获取源节点和目标节点 - const sourceNode = flowData[source]; - const targetNode = flowData[target]; - - // 确定最终的源句柄 - let finalSourceHandle = sourceHandle; - // 如果源句柄未指定,则根据源节点信息确定 - if (!finalSourceHandle) { - if (source === 'start') { - finalSourceHandle = 'start'; - } else if ( - sourceNode && - sourceNode.data && - sourceNode.data.parameters && - sourceNode.data.parameters.apiOuts && - sourceNode.data.parameters.apiOuts.length > 0 - ) { - // 查找匹配的目标句柄 - const matchingApiOut = sourceNode.data.parameters.apiOuts.find( - (apiOut: any) => apiOut.name === targetHandle - ); - - if (matchingApiOut) { - finalSourceHandle = matchingApiOut.name; - } else { - // 如果没有精确匹配,使用第一个apiOut - finalSourceHandle = sourceNode.data.parameters.apiOuts[0].name; - } - } else if ( - sourceNode && - sourceNode.component && - sourceNode.component.type - ) { - // 根据节点类型获取正确的源句柄 - finalSourceHandle = getNodeApiOutHandle(source, sourceNode); - } else { - // 默认句柄 - finalSourceHandle = 'done'; - } - } - - // 确定最终的目标句柄 - let finalTargetHandle = targetHandle; - // 如果目标句柄未指定,则根据目标节点信息确定 - if (!finalTargetHandle) { - if (target === 'end') { - finalTargetHandle = 'end'; - } else if ( - targetNode && - targetNode.data && - targetNode.data.parameters && - targetNode.data.parameters.apiIns && - targetNode.data.parameters.apiIns.length > 0 - ) { - // 查找匹配的源句柄 - const matchingApiIn = targetNode.data.parameters.apiIns.find( - (apiIn: any) => apiIn.name === sourceHandle - ); - - if (matchingApiIn) { - finalTargetHandle = matchingApiIn.name; - } else { - // 如果没有精确匹配,使用第一个apiIn - finalTargetHandle = targetNode.data.parameters.apiIns[0].name; - } - } else { - // 默认句柄 - finalTargetHandle = 'start'; - } - } - - // 创建边的唯一标识符 - const edgeId = `${source}-${target}-${finalSourceHandle}-${finalTargetHandle}`; - - // 检查是否已添加此边 - if (!addedEdges.has(edgeId)) { - addedEdges.add(edgeId); - edges.push({ - id: `${edgeId}`, - source: source, - target: target, - sourceHandle: finalSourceHandle, - targetHandle: finalTargetHandle, - type: 'custom', - lineType: 'api', - data: { - lineType: 'api', - }, - }); - } - } - - // 处理数据下游连接 - for (const entry of nodeEntries) { - const nodeId: string = entry[0]; - const nodeConfig: any = entry[1]; - - if (nodeConfig.dataDownstream && Array.isArray(nodeConfig.dataDownstream)) { - nodeConfig.dataDownstream.forEach((connectionGroup: string[]) => { - // 确保 connectionGroup 是数组并且至少包含两个元素 - if (Array.isArray(connectionGroup) && connectionGroup.length >= 2) { - // 第一个元素是源节点和句柄信息 - const [sourceInfo, targetInfo] = connectionGroup; - - if ( - typeof sourceInfo === 'string' && - sourceInfo.includes('@@') && - typeof targetInfo === 'string' && - targetInfo.includes('@@') - ) { - const [sourceNodeId, sourceHandle] = sourceInfo.split('@@'); - const [targetNodeId, targetHandle] = targetInfo.split('@@'); - - // 创建边的唯一标识符 - const edgeId = `${sourceNodeId}-${targetNodeId}-${sourceHandle}-${targetHandle}`; - - // 检查是否已添加此边 - if (!addedEdges.has(edgeId)) { - addedEdges.add(edgeId); - - edges.push({ - id: `${edgeId}`, - source: sourceNodeId, - target: targetNodeId, - sourceHandle: sourceHandle, - targetHandle: targetHandle, - type: 'custom', - lineType: 'data', - data: { - lineType: 'data', - }, - }); - } - } - } - }); - } - } - - console.log('nodes, edges:', nodes, edges); - return { nodes, edges }; -}; - -/** - * 将 flow editor 的 nodes 和 edges 数据结构转换回原始数据结构 - * @param nodes - flow editor 的节点数组 - * @param edges - flow editor 的连线数组 - * @returns 原始数据结构 - */ -export const revertFlowData = (nodes: any[], edges: any[]) => { - // 初始化返回的数据结构 - const flowData: any = { - id: 'main', - nodeConfigs: [], - lineConfigs: [], - }; - - // 转换节点数据 - if (nodes && nodes.length > 0) { - flowData.nodeConfigs = nodes.map((node) => { - // 确定 nodeId 和 nodeName - const nodeId = node.id || node.name; - const nodeName = node.data?.title || nodeId; - - // 确定节点类型 - let nodeType = node.data.type; - // 特殊处理 start 和 end 节点 - if (nodeId.includes('start')) { - nodeType = 'start'; - } else if (nodeId.includes('end')) { - nodeType = 'end'; - } - - // 构造 x6 数据(位置信息) - const x6 = JSON.stringify({ - position: node.position, - }); - - // 构造 nodeConfig 对象 - const nodeConfig: any = { - nodeId, - nodeName, - x6, - }; - - // 处理 component 信息 - if (node.data?.component) { - nodeConfig.component = { - type: nodeType, - compIdentifier: node.data.component.compIdentifier || '', - compInstanceIdentifier: - node.data.component.compInstanceIdentifier || '', - compId: node.data.compId || '', - }; - if (node.data.component?.customDef) - nodeConfig.component.customDef = node.data.component.customDef; - } else if (nodeType !== 'start' && nodeType !== 'end') { - // 对于非 start/end 节点,添加基本的 component 信息 - nodeConfig.component = { - type: nodeType, - }; - } - if (['BASIC', 'SUB'].includes(nodeType)) - nodeConfig.component.compId = node.data.compId || ''; - - // 处理参数信息 - const parameters = node.data?.parameters || {}; - - // 处理 dataIns(输入数据) - if (parameters.dataIns && parameters.dataIns.length > 0) { - nodeConfig.dataIns = parameters.dataIns.map((input: any) => ({ - id: input.name || input.id, - desc: input.desc, - dataType: input.dataType, - defaultValue: input.defaultValue, - arrayType: input.arrayType || null, - })); - } - - // 处理 dataOuts(输出数据) - if (parameters.dataOuts && parameters.dataOuts.length > 0) { - nodeConfig.dataOuts = parameters.dataOuts.map((output: any) => ({ - id: output.name || output.id, - desc: output.desc, - dataType: output.dataType, - defaultValue: output.defaultValue, - arrayType: output.arrayType || null, - })); - } - - return nodeConfig; - }); - } - - // 转换连线数据 - if (edges && edges.length > 0) { - flowData.lineConfigs = edges.map((edge, index) => { - // 查找源节点和目标节点以确定连线类型 - const sourceNode = nodes.find((node) => node.id === edge.source); - const targetNode = nodes.find((node) => node.id === edge.target); - - let lineType = 'DATA'; // 默认为DATA类型 - - // 判断是否为CONVERT类型的连线 - if ( - targetNode && - ['JSONCONVERT', 'JSON2STR', 'STR2JSON'].includes(targetNode.type) - ) { - lineType = 'CONVERT'; - } - // 判断是否为API类型的连线 - else if ( - edge.sourceHandle && - (edge.sourceHandle === 'apiOuts' || - sourceNode?.data?.parameters?.apiOuts?.some( - (out: any) => (out.name || out.id) === edge.sourceHandle - )) - ) { - lineType = 'API'; - } else if ( - edge.targetHandle && - (edge.targetHandle === 'apiIns' || - targetNode?.data?.parameters?.apiIns?.some( - (inp: any) => (inp.name || inp.id) === edge.targetHandle - )) - ) { - lineType = 'API'; - } - - return { - id: edge.id || `edge_${index}`, // 如果没有 id,则生成一个 - lineType, // 添加lineType属性 - prev: { - nodeId: edge.source, - endpointId: edge.sourceHandle || 'done', // 默认使用 'done' - }, - next: { - nodeId: edge.target, - endpointId: edge.targetHandle || 'start', // 默认使用 'start' - }, - }; - }); - } - - return flowData; -}; - -/** - * 将 React Flow 的 nodes 和 edges 数据结构反向转换为 convertFlowData 可以处理的格式 - * @param nodes - React Flow 节点数组 - * @param edges - React Flow 边数组 - * @param complexKV - 复合组件使用的组件id对照表 {数字ID/nodeId:sub_ID} - * @returns 可用于 convertFlowData 的数据结构 - */ -export const reverseConvertFlowData = ( - nodes: any[], - edges: any[], - complexKV: any -) => { - // 初始化返回的数据结构 - const flowData: any = {}; - - // 转换节点数据 - if (nodes && nodes.length > 0) { - nodes.forEach((node) => { - const nodeId = node.id; - - // 构造节点配置对象 - const nodeConfig: any = { - id: nodeId, - componentName: node.data?.title || nodeId, - position: node.position || { x: 0, y: 0 }, - }; - - // 处理 component 信息 - if (node.type === 'SUB' && !node.data.component.customDef) { - let subflowId = ''; - if (complexKV && node.data.compId) { - // 遍历complexKV找到匹配的条目,使用节点ID进行匹配 - for (const key in complexKV) { - if (key.includes('/')) { - const [numericId, nodeId] = key.split('/'); - // 使用节点ID进行匹配 - if (nodeId === node.id) { - subflowId = complexKV[key]; - break; - } - } - } - } - - nodeConfig.component = { - type: 'SUB', - compId: node.data.compId, - customDef: JSON.stringify({ - dataIns: node.data.parameters.dataIns, - dataOuts: node.data.parameters.dataOuts, - subflowId: subflowId, - name: node.data.title, - }), - }; - } else if (node.data?.component) { - nodeConfig.component = { ...node.data.component }; - } else { - nodeConfig.component = { - type: node.type, - }; - } - - // 处理参数信息 - const parameters = node.data?.parameters || {}; - - // 处理 apiIns(输入API) - if (parameters.apiIns && parameters.apiIns.length > 0) { - nodeConfig.apiIns = parameters.apiIns; - } else { - nodeConfig.apiIns = []; - } - - // 处理 apiOuts(输出API) - if (parameters.apiOuts && parameters.apiOuts.length > 0) { - nodeConfig.apiOuts = parameters.apiOuts; - } else { - nodeConfig.apiOuts = []; - } - - // 处理 dataIns(输入数据) - if (parameters.dataIns && parameters.dataIns.length > 0) { - nodeConfig.dataIns = parameters.dataIns; - } else { - nodeConfig.dataIns = []; - } - - // 处理 dataOuts(输出数据) - if (parameters.dataOuts && parameters.dataOuts.length > 0) { - nodeConfig.dataOuts = parameters.dataOuts; - } else { - nodeConfig.dataOuts = []; - } - - // 初始化连接数组 - nodeConfig.apiDownstream = []; - nodeConfig.apiUpstream = []; - nodeConfig.dataDownstream = []; - nodeConfig.dataUpstream = []; - - // 将节点配置添加到 flowData 对象中 - flowData[nodeId] = nodeConfig; - }); - } - - // 处理连接关系 - if (edges && edges.length > 0) { - // 分析边的连接关系 - edges.forEach((edge) => { - const sourceNode = edge.source; - const targetNode = edge.target; - const sourceHandle = edge.sourceHandle || 'done'; - const targetHandle = edge.targetHandle || 'start'; - - // 确定连接类型(API 还是 DATA) - const sourceNodeData = nodes.find((n) => n.id === sourceNode); - const targetNodeData = nodes.find((n) => n.id === targetNode); - - const isApiConnection = - sourceNodeData?.data?.parameters?.apiOuts?.some( - (out: any) => (out.name || out.id) === sourceHandle - ) || - targetNodeData?.data?.parameters?.apiIns?.some( - (inp: any) => (inp.name || inp.id) === targetHandle - ) || - sourceHandle === 'start' || - targetHandle === 'end' || - sourceHandle === 'end' || - targetHandle === 'start'; - - if (isApiConnection) { - // API 连接 - // 添加下游连接 - flowData[sourceNode].apiDownstream.push([ - `${targetNode}$$${targetHandle}`, - ]); - - // 添加上游连接 - flowData[targetNode].apiUpstream.push([ - `${sourceNode}$$${sourceHandle}`, - ]); - } else { - // 数据连接 - const dataConnection = [ - `${sourceNode}@@${sourceHandle}`, - `${targetNode}@@${targetHandle}`, - ]; - flowData[sourceNode].dataDownstream.push(dataConnection); - flowData[targetNode].dataUpstream.push(dataConnection); - } - }); - } - - return flowData; -}; - -// 获取节点的数据输入参数 -const getNodeDataIns = (nodeConfig: any) => { - if (Array.isArray(nodeConfig.dataIns) && nodeConfig.dataIns.length > 0) { - return nodeConfig.dataIns; - } - - if (nodeConfig.component?.type === 'EVENTSEND_SYNC') { - return [ - { - arrayType: null, - dataType: 'STRING', - defaultValue: '', - desc: '输入', - id: 'in', - }, - ]; - } - - return []; -}; - -// 获取节点的API输入参数 -const getNodeApiIns = ( - nodeId: string, - nodeConfig: any, - currentProjectCompData: any[] -) => { - // JSON2STR 和 STR2JSON 不需要 API 输入 - if ( - nodeConfig.component?.type === 'JSON2STR' || - nodeConfig.component?.type === 'STR2JSON' - ) { - return []; - } - - // 对于特定类型的节点使用预定义值 - if (nodeConfig.component?.type === 'LOOP_START') { - return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; - } else if (nodeConfig.component?.type === 'LOOP_END') { - return [{ name: 'continue', desc: '', dataType: '', defaultValue: '' }]; - } else if (nodeId.includes('end')) { - return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }]; - } else if (nodeConfig.component?.type === 'SUB') { - return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; - } else { - const comp = currentProjectCompData.filter((item) => { - return (item.id || item?.comp?.id) === nodeConfig?.component?.compId; - }); - if (comp && comp.length > 0) { - const apiIns = comp[0]?.def?.apis || comp[0]?.comp?.def?.apis || []; - return apiIns.map((v) => { - return { - ...v, - name: v.id, - desc: v.desc, - dataType: v?.dataType || '', - defaultValue: v?.defaultValue || '', - }; - }); - } else { - return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; - } - } -}; - -// 获取节点的API输出参数 -const getNodeApiOuts = ( - nodeId: string, - nodeConfig: any, - currentProjectCompData: any[] -) => { - // JSON2STR 和 STR2JSON 不需要 API 输出 - if ( - nodeConfig.component?.type === 'JSON2STR' || - nodeConfig.component?.type === 'STR2JSON' - ) { - return []; - } - - // 对于特定类型的节点使用预定义值 - if (nodeConfig.component?.type === 'LOOP_START') { - return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; - } else if (nodeConfig.component?.type === 'LOOP_END') { - // 从customDef中获取apiOutIds数组 - try { - const customDef = JSON.parse(nodeConfig.component?.customDef || '{}'); - const apiOutIds = customDef.apiOutIds || []; - - // 从"break"开始的所有项都应该作为apiOut返回 - const breakIndex = apiOutIds.indexOf('break'); - if (breakIndex !== -1) { - // 返回从"break"开始的所有项 - return apiOutIds.slice(breakIndex).map((id) => ({ - name: id, - id: id, - desc: id, - dataType: '', - defaultValue: '', - })); - } else { - // 如果没有找到"break",则返回默认值 - return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }]; - } - } catch (e) { - // 解析失败时返回默认值 - return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }]; - } - } else if (nodeConfig.component?.type === 'SWITCH') { - // 从customDef中获取apiOutIds数组 - try { - const customDef = JSON.parse(nodeConfig.component?.customDef || '{}'); - const apiOutIds = customDef.apiOutIds || []; - - // 从"break"开始的所有项都应该作为apiOut返回 - const breakIndex = apiOutIds.indexOf('default'); - if (breakIndex !== -1) { - // 返回从"break"开始的所有项 - return apiOutIds.slice(breakIndex).map((id) => ({ - name: id, - id: id, - desc: id, - dataType: '', - defaultValue: '', - })); - } else { - // 如果没有找到"break",则返回默认值 - return [{ name: 'default', desc: '', dataType: '', defaultValue: '' }]; - } - } catch (e) { - // 解析失败时返回默认值 - return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; - } - } else if (nodeId.includes('start')) { - return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; - } else if (nodeId.includes('end')) { - return []; - } else if (nodeConfig.component?.type === 'SUB') { - return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; - } else { - const comp = currentProjectCompData.filter( - (item) => item.id === nodeConfig?.component?.compId - ); - if (comp && comp.length > 0) { - return [ - { - ...comp[0].def?.apiOut, - dataType: '', - defaultValue: '', - }, - ]; - } else { - return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; - } - } -}; - -// 获取节点的API输出句柄名称 -const getNodeApiOutHandle = (nodeId: string, nodeConfig: any) => { - if (nodeConfig.component?.type === 'LOOP_START') { - return 'done'; - } else if (nodeConfig.component?.type === 'LOOP_END') { - return 'break'; - } else if (nodeId.includes('start')) { - return 'start'; - } else if (nodeId.includes('end')) { - return 'end'; - } - return 'done'; -}; - -// 获取当前工程下组件列表并扁平化处理 -const getCurrentProjectStoreData = () => { - const { info, projectComponentData } = store.getState().ideContainer; - const compData = projectComponentData[info?.id] || {}; - - const result: any[] = []; - - // 处理projectCompDto中的数据 - if (compData.projectCompDto) { - const { - mineComp = [], - pubComp = [], - teamWorkComp = [], - } = compData.projectCompDto; - - // 添加mineComp数据 - mineComp.forEach((item: any) => { - result.push({ - ...item, - type: 'mineComp', - }); - }); - - // 添加pubComp数据 - pubComp.forEach((item: any) => { - result.push({ - ...item, - type: 'pubComp', - }); - }); - - // 添加teamWorkComp数据 - teamWorkComp.forEach((item: any) => { - result.push({ - ...item, - type: 'teamWorkComp', - }); - }); - } - - // 处理projectFlowDto中的数据 - if (compData.projectFlowDto) { - const { mineFlow = [], pubFlow = [] } = compData.projectFlowDto; - - // 添加mineFlow数据 - mineFlow.forEach((item: any) => { - result.push({ - ...item, - type: 'mineFlow', - }); - }); - - // 添加pubFlow数据 - pubFlow.forEach((item: any) => { - result.push({ - ...item, - type: 'pubFlow', - }); - }); - } - - // 如果从 store 中没有获取到数据,尝试从 sessionStorage 中获取 - if (result.length === 0) { - try { - const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}'); - const compLibsKey = `compLibs${userInfo.userId}`; - const compLibsData = sessionStorage.getItem(compLibsKey); - - if (compLibsData) { - const { - myLibs = [], - pubLibs = [], - teamLibs = [], - myFlow = [], - pubFlow = [], - } = JSON.parse(compLibsData); - - // 处理 myLibs(我的组件库) - if (Array.isArray(myLibs)) { - myLibs.forEach((lib: any) => { - if (lib.children && Array.isArray(lib.children)) { - lib.children.forEach((item: any) => { - result.push({ - ...item, - type: 'mineComp', - }); - }); - } - }); - } - - // 处理 pubLibs(公共组件库) - if (Array.isArray(pubLibs)) { - pubLibs.forEach((lib: any) => { - if (lib.children && Array.isArray(lib.children)) { - lib.children.forEach((item: any) => { - result.push({ - ...item, - type: 'pubComp', - }); - }); - } - }); - } - - // 处理 teamLibs(协同组件库) - if (Array.isArray(teamLibs)) { - teamLibs.forEach((lib: any) => { - if (lib.children && Array.isArray(lib.children)) { - lib.children.forEach((item: any) => { - result.push({ - ...item, - type: 'teamWorkComp', - }); - }); - } - }); - } - - // 处理 myFlow(我的流程) - if (Array.isArray(myFlow)) { - myFlow.forEach((item: any) => { - result.push({ - ...item, - type: 'mineFlow', - }); - }); - } - - // 处理 pubFlow(公共流程) - if (Array.isArray(pubFlow)) { - pubFlow.forEach((item: any) => { - result.push({ - ...item, - type: 'pubFlow', - }); - }); - } - } - } catch (error) { - console.error('从 sessionStorage 获取组件数据失败:', error); - } - } - return result; -}; +export { + convertFlowData, + revertFlowData, + reverseConvertFlowData, +} from '@/features/workflow/adapters/serverFlowAdapter'; diff --git a/src/utils/flow/edgeInsertionFactory.ts b/src/utils/flow/edgeInsertionFactory.ts index 51c9fbd..3395702 100644 --- a/src/utils/flow/edgeInsertionFactory.ts +++ b/src/utils/flow/edgeInsertionFactory.ts @@ -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'; diff --git a/src/utils/flow/loopFactory.ts b/src/utils/flow/loopFactory.ts index f33b81c..f287aea 100644 --- a/src/utils/flow/loopFactory.ts +++ b/src/utils/flow/loopFactory.ts @@ -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'; diff --git a/src/utils/flow/nodeFactory.ts b/src/utils/flow/nodeFactory.ts index f592791..a42c0b6 100644 --- a/src/utils/flow/nodeFactory.ts +++ b/src/utils/flow/nodeFactory.ts @@ -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'; diff --git a/src/utils/flow/nodeOnboarding.ts b/src/utils/flow/nodeOnboarding.ts index 5c2a8ad..7dc991e 100644 --- a/src/utils/flow/nodeOnboarding.ts +++ b/src/utils/flow/nodeOnboarding.ts @@ -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'; diff --git a/src/utils/flow/nodeRegistry.ts b/src/utils/flow/nodeRegistry.ts index 7a67ecf..bc7baa3 100644 --- a/src/utils/flow/nodeRegistry.ts +++ b/src/utils/flow/nodeRegistry.ts @@ -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'; diff --git a/src/utils/flow/runtime.ts b/src/utils/flow/runtime.ts index 0505e5b..e9c9dd9 100644 --- a/src/utils/flow/runtime.ts +++ b/src/utils/flow/runtime.ts @@ -1,34 +1,8 @@ -export interface FlowCurrentAppData { - id?: string; - key?: string; -} - -export interface AppRuntimeState { - nodeStatusMap: Record; - 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'; diff --git a/src/utils/flow/snapshot.ts b/src/utils/flow/snapshot.ts index acc4e49..fe13ad8 100644 --- a/src/utils/flow/snapshot.ts +++ b/src/utils/flow/snapshot.ts @@ -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'; diff --git a/src/utils/flowCommon.ts b/src/utils/flowCommon.ts index e8de188..ef41393 100644 --- a/src/utils/flowCommon.ts +++ b/src/utils/flowCommon.ts @@ -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) => {