Compare commits

..

2 Commits

@ -0,0 +1,113 @@
# 新增 Flow 节点模板(基于当前重构版本)
这份模板按你现在的代码结构整理,目标是:**新增节点只改最少文件**。
---
## 1. 最小接入步骤
新增一个普通节点(非 LOOP通常只要 3 步:
1. 新建节点组件(可先复用 `LocalNode` 风格)
2. 在 `nodeRegistry` 注册类型映射
3. 在节点配置源(`localNodeData`)增加定义
> `useFlowCallbacks` 已统一走 `resolveNodeDefinition + buildRuntimeNode + ensureNodeTypeRegistered`,不需要再在多个入口手写创建逻辑。
---
## 2. 模板代码
### 2.1 新建节点组件
**路径示例**`src/components/FlowEditor/node/httpNode/HttpNode.tsx`
```tsx
import React from 'react';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
const HttpNode = (props: any) => {
// 第一版可以直接复用 LocalNode 渲染行为
return <LocalNode {...props} />;
};
export default HttpNode;
```
---
### 2.2 注册节点类型映射(关键)
**文件**`src/utils/flow/nodeRegistry.ts`
```ts
import HttpNode from '@/components/FlowEditor/node/httpNode/HttpNode';
export const resolveNodeComponent = (nodeType: string) => {
switch (nodeType) {
// ...已有 case
case 'HTTP':
return HttpNode;
default:
return LocalNode;
}
};
```
---
### 2.3 增加节点定义(用于菜单与创建)
**文件**`src/pages/flowEditor/sideBar/config/localNodeData.ts`
```ts
export const localNodeData = [
// ...已有节点
{
nodeType: 'HTTP',
nodeName: 'HTTP请求',
data: {
title: 'HTTP请求',
type: 'HTTP',
parameters: {
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {
type: 'HTTP',
customDef: '{}',
},
},
},
];
```
---
## 3. 可选:节点专用编辑器
如果你希望配置面板是独立 UI再补
1. 新建编辑器组件(`src/components/FlowEditor/nodeEditors/...`
2. 在编辑器路由/映射处按 `nodeType` 挂载
不做这一步也能先跑通新增节点。
---
## 4. 当前推荐的接入方式
新增节点时不要再手写分散逻辑,优先复用:
- `resolveNodeDefinition(...)`
- `buildRuntimeNode(...)`
- `ensureNodeTypeRegistered(...)`
这些已覆盖:
- 侧栏拖拽到画布onDrop
- 画布空白处添加addNodeOnPane
- 在边上插入节点addNodeOnEdge

@ -0,0 +1,149 @@
# 新增 LOOP 类节点模板(基于当前重构版本)
这份模板用于你现在这套 Flow Core`loopFactory + nodeRegistry + useFlowCallbacks`)下,新增“成对节点/分支类”能力。
> 适用场景:像 `LOOP_START/LOOP_END` 这种需要一次创建多个节点和连边的节点族。
---
## 1. 推荐做法(先看)
不要在 `useFlowCallbacks` 里直接手写大段节点/边构造。按现在的模式:
1. 在 `src/utils/flow/` 新增工厂(如 `xxxFactory.ts`
2. 暴露:
- 节点对(或节点组)构造函数
- 组内连接边构造函数
- 外部插入连接边构造函数(可选)
3. 在 `useFlowCallbacks` 只负责调用工厂 + setNodes/setEdges + snapshot
---
## 2. 工厂模板
**路径示例**`src/utils/flow/retryFactory.ts`
```ts
import { Edge } from '@xyflow/react';
export const createRetryNodePair = (position: { x: number; y: number }) => {
const retryStartNode: any = {
id: `RETRY_START-${Date.now()}`,
type: 'RETRY',
position: { x: position.x, y: position.y },
data: {
title: '重试开始',
type: 'RETRY_START',
parameters: {
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {},
},
};
const retryEndNode: any = {
id: `RETRY_END-${Date.now()}`,
type: 'RETRY',
position: { x: position.x + 400, y: position.y },
data: {
title: '重试结束',
type: 'RETRY_END',
parameters: {
apiIns: [
{ name: 'continue', desc: '', dataType: '', defaultValue: '' },
],
apiOuts: [{ name: 'break', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {
type: 'RETRY_END',
customDef: JSON.stringify({ retryStartNodeId: retryStartNode.id }),
retryStartNodeId: retryStartNode.id,
},
},
};
retryStartNode.data.component = {
type: 'RETRY_START',
customDef: JSON.stringify({ retryEndNodeId: retryEndNode.id }),
};
return { retryStartNode, retryEndNode };
};
export const createRetryGroupEdge = (
retryStartId: string,
retryEndId: string
): Edge => ({
id: `${retryStartId}-${retryEndId}-group`,
source: retryStartId,
target: retryEndId,
sourceHandle: `${retryStartId}-group`,
targetHandle: `${retryEndId}-group`,
type: 'custom',
});
```
---
## 3. useFlowCallbacks 接入模板
```ts
import {
createRetryNodePair,
createRetryGroupEdge,
} from '@/utils/flow/retryFactory';
import { ensureNodeTypeRegistered } from '@/utils/flow/nodeRegistry';
import { dispatchFlowSnapshotAsync } from '@/utils/flow/snapshot';
const addRetryNodeWithStartEnd = useCallback(
(position: { x: number; y: number }) => {
const { retryStartNode, retryEndNode } = createRetryNodePair(position);
const groupEdge = createRetryGroupEdge(retryStartNode.id, retryEndNode.id);
ensureNodeTypeRegistered('RETRY', '重试');
setNodes((nds) => {
const newNodes = [...nds, retryStartNode, retryEndNode];
dispatchFlowSnapshotAsync({
nodes: [...newNodes],
edges: [...edges, groupEdge],
});
return newNodes;
});
setEdges((eds) => [...eds, groupEdge]);
},
[edges]
);
```
---
## 4. 类型映射模板
**文件**`src/utils/flow/nodeRegistry.ts`
```ts
case 'RETRY':
return LoopNode; // 或你自己的专用节点组件
```
> 早期可复用 `LoopNode/LocalNode`,后续再拆专用展示组件。
---
## 5. 新增“边上插入该类节点”的模板(可选)
如需在边上插入节点组,建议像 `LOOP` 一样提供工厂函数:
- `createXxxNodePair(position)`
- `createXxxGroupEdge(startId, endId)`
- `createXxxInsertConnectionEdges({ sourceId, sourceHandle, targetId, targetHandle, ... })`
这样 `addNodeOnEdge` 里只拼装调用,不再手写边结构。

File diff suppressed because it is too large Load Diff

@ -3,6 +3,7 @@ import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { updateCanvasDataMap } from '@/store/ideContainer'; import { updateCanvasDataMap } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
@ -11,29 +12,22 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
const [edges, setEdges] = useState<Edge[]>([]); const [edges, setEdges] = useState<Edge[]>([]);
// 使用 shallowEqual 比较器来避免不必要的重新渲染 // 使用 shallowEqual 比较器来避免不必要的重新渲染
const ideContainerState = useSelector((state: any) => ({ const ideContainerState = useSelector(
(state: any) => ({
canvasDataMap: state.ideContainer.canvasDataMap, canvasDataMap: state.ideContainer.canvasDataMap,
appRuntimeData: state.ideContainer.appRuntimeData, appRuntimeData: state.ideContainer.appRuntimeData,
currentAppData: state.ideContainer.currentAppData, currentAppData: state.ideContainer.currentAppData,
}), shallowEqual); }),
shallowEqual
);
const { canvasDataMap, appRuntimeData, currentAppData } = ideContainerState; const { canvasDataMap, appRuntimeData, currentAppData } = ideContainerState;
const dispatch = useDispatch(); const dispatch = useDispatch();
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = (currentAppData: any) => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
// 获取当前应用的运行状态 // 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey(currentAppData); const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey] const currentAppIsRunning =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning ? appRuntimeData[currentAppKey].isRunning
: false; : false;
@ -44,7 +38,10 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
// 添加节点选择弹窗状态 // 添加节点选择弹窗状态
const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null); const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null);
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null); const [positionForNodeAdd, setPositionForNodeAdd] = useState<{
x: number;
y: number;
} | null>(null);
// 在组件顶部添加历史记录相关状态 // 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false); const [historyInitialized, setHistoryInitialized] = useState(false);
@ -53,16 +50,18 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
// 更新节点状态将从store获取的状态应用到节点上 // 更新节点状态将从store获取的状态应用到节点上
useEffect(() => { useEffect(() => {
// 获取当前应用对应的节点状态映射 // 获取当前应用对应的节点状态映射
const currentNodeStatusMap = currentAppKey && appRuntimeData[currentAppKey] const currentNodeStatusMap =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].nodeStatusMap ? appRuntimeData[currentAppKey].nodeStatusMap
: {}; : {};
// 检查 initialData 中是否包含节点状态(用于查看历史实例) // 检查 initialData 中是否包含节点状态(用于查看历史实例)
const hasInitialDataStatus = initialData?.components && const hasInitialDataStatus =
initialData?.components &&
Object.values(initialData.components).some((comp: any) => comp.status); Object.values(initialData.components).some((comp: any) => comp.status);
setNodes(prevNodes =>{ setNodes((prevNodes) => {
return prevNodes.map(node => { return prevNodes.map((node) => {
// 如果是只读模式(历史实例查看),优先使用节点自身的历史状态 // 如果是只读模式(历史实例查看),优先使用节点自身的历史状态
// 如果是正常运行模式,只使用运行时状态 // 如果是正常运行模式,只使用运行时状态
let nodeStatus = 'waiting'; let nodeStatus = 'waiting';
@ -70,8 +69,11 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
if (readOnly) { if (readOnly) {
// 只读模式:使用历史状态 // 只读模式:使用历史状态
nodeStatus = node.data.status as string || 'waiting'; nodeStatus = (node.data.status as string) || 'waiting';
showStatus = hasInitialDataStatus as boolean || node.data.isStatusVisible as boolean || false; showStatus =
(hasInitialDataStatus as boolean) ||
(node.data.isStatusVisible as boolean) ||
false;
} else { } else {
// 正常模式:只使用运行时状态 // 正常模式:只使用运行时状态
nodeStatus = currentNodeStatusMap[node.id] || 'waiting'; nodeStatus = currentNodeStatusMap[node.id] || 'waiting';
@ -83,21 +85,39 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
data: { data: {
...node.data, ...node.data,
status: nodeStatus, status: nodeStatus,
isStatusVisible: showStatus isStatusVisible: showStatus,
} },
}; };
}); });
}); });
}, [appRuntimeData, currentAppKey, currentAppIsRunning, initialData?.id, initialData?.components, nodes.length, readOnly]); }, [
appRuntimeData,
currentAppKey,
currentAppIsRunning,
initialData?.id,
initialData?.components,
nodes.length,
readOnly,
]);
const updateCanvasDataMapDebounced = useRef( const updateCanvasDataMapDebounced = useRef(
debounce((dispatch: Dispatch<any>, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => { debounce(
dispatch(updateCanvasDataMap({ (
dispatch: Dispatch<any>,
canvasDataMap: any,
id: string,
nodes: Node[],
edges: Edge[]
) => {
dispatch(
updateCanvasDataMap({
...canvasDataMap, ...canvasDataMap,
[id]: { nodes, edges } [id]: { nodes, edges },
})); })
}, 500) );
},
500
)
).current; ).current;
return { return {
@ -127,6 +147,6 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
dispatch, dispatch,
// Initial data // Initial data
initialData: initialData initialData: initialData,
}; };
}; };

@ -27,6 +27,7 @@ import { NodeTypes } from '@xyflow/react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import HandlerBar from '@/pages/flowEditor/components/handlerBar'; import HandlerBar from '@/pages/flowEditor/components/handlerBar';
import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal'; import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal';
import { getCurrentAppKey } from '@/utils/flow/runtime';
const edgeTypes = { const edgeTypes = {
custom: CustomEdge custom: CustomEdge
@ -52,8 +53,10 @@ interface FlowEditorMainProps {
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>; setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
edgeForNodeAdd: Edge | null; edgeForNodeAdd: Edge | null;
setEdgeForNodeAdd: React.Dispatch<React.SetStateAction<Edge | null>>; setEdgeForNodeAdd: React.Dispatch<React.SetStateAction<Edge | null>>;
positionForNodeAdd: { x: number, y: number } | null; positionForNodeAdd: { x: number; y: number } | null;
setPositionForNodeAdd: React.Dispatch<React.SetStateAction<{ x: number, y: number } | null>>; setPositionForNodeAdd: React.Dispatch<
React.SetStateAction<{ x: number; y: number } | null>
>;
isRunning: boolean; isRunning: boolean;
initialData: any; initialData: any;
canvasDataMap: any; canvasDataMap: any;
@ -81,7 +84,11 @@ interface FlowEditorMainProps {
copyNode: (node: Node) => void; copyNode: (node: Node) => void;
pasteNode: (position: { x: number; y: number }) => void; pasteNode: (position: { x: number; y: number }) => void;
addNodeOnEdge: (nodeType: string, node: any) => void; addNodeOnEdge: (nodeType: string, node: any) => void;
addNodeOnPane: (nodeType: string, position: { x: number, y: number }, node?: any) => void; addNodeOnPane: (
nodeType: string,
position: { x: number; y: number },
node?: any
) => void;
handleAddNode: (nodeType: string, node: any) => void; handleAddNode: (nodeType: string, node: any) => void;
saveFlowDataToServer: () => void; saveFlowDataToServer: () => void;
handleRun: (running: boolean) => void; handleRun: (running: boolean) => void;
@ -145,7 +152,8 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
handleReRun handleReRun
} = props; } = props;
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines(); const { getGuidelines, clearGuidelines, AlignmentGuides } =
useAlignmentGuidelines();
const { undo, redo, canUndo, canRedo } = useHistory(); const { undo, redo, canUndo, canRedo } = useHistory();
const reactFlowId = useMemo(() => new Date().getTime().toString(), []); const reactFlowId = useMemo(() => new Date().getTime().toString(), []);
@ -162,7 +170,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
if (isVisible) { if (isVisible) {
// 显示节点 - 从隐藏节点集合中移除 // 显示节点 - 从隐藏节点集合中移除
setHiddenNodes(prev => { setHiddenNodes((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.delete(appId); newSet.delete(appId);
return newSet; return newSet;
@ -170,62 +178,34 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
} }
else { else {
// 隐藏节点 - 添加到隐藏节点集合 // 隐藏节点 - 添加到隐藏节点集合
setHiddenNodes(prev => new Set(prev).add(appId)); setHiddenNodes((prev) => new Set(prev).add(appId));
} }
}; };
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener); document.addEventListener(
'toggleNodeVisibility',
handleToggleNodeVisibility as EventListener
);
return () => { return () => {
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener); document.removeEventListener(
'toggleNodeVisibility',
handleToggleNodeVisibility as EventListener
);
}; };
}, []); }, []);
// 从Redux store中获取当前应用的运行状态 // 从Redux store中获取当前应用的运行状态
const { appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer); const { appRuntimeData, currentAppData } = useSelector(
(state: any) => state.ideContainer
// 辅助函数:获取当前应用/子流程的唯一标识符 );
const getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
const currentAppKey = getCurrentAppKey(); const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey] const currentAppIsRunning =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning ? appRuntimeData[currentAppKey].isRunning
: false; : false;
// 监听自定义事件以隐藏/显示节点
useEffect(() => {
const handleToggleNodeVisibility = (event: CustomEvent) => {
const { appId, isVisible } = event.detail;
if (isVisible) {
// 显示节点 - 从隐藏节点集合中移除
setHiddenNodes(prev => {
const newSet = new Set(prev);
newSet.delete(appId);
return newSet;
});
}
else {
// 隐藏节点 - 添加到隐藏节点集合
setHiddenNodes(prev => new Set(prev).add(appId));
}
};
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
return () => {
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
};
}, []);
// 监听键盘事件实现快捷键 // 监听键盘事件实现快捷键
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@ -275,7 +255,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
// Ctrl/Cmd+C 复制选中的节点 // Ctrl/Cmd+C 复制选中的节点
if (isModifierKey && e.key === 'c' && canEdit) { if (isModifierKey && e.key === 'c' && canEdit) {
// 获取当前选中的节点 // 获取当前选中的节点
const selectedNode = nodes.find(node => node.selected); const selectedNode = nodes.find((node) => node.selected);
if (selectedNode) { if (selectedNode) {
// 不允许复制开始和结束节点 // 不允许复制开始和结束节点
if (selectedNode.type === 'start' || selectedNode.type === 'end') { if (selectedNode.type === 'start' || selectedNode.type === 'end') {
@ -312,7 +292,18 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
}; };
}, [undo, redo, canUndo, canRedo, currentAppIsRunning, useDefault, nodes, copyNode, pasteNode, reactFlowInstance]); }, [
undo,
redo,
canUndo,
canRedo,
currentAppIsRunning,
useDefault,
nodes,
copyNode,
pasteNode,
reactFlowInstance
]);
// 处理流程发布 // 处理流程发布
const handlePublish = () => { const handlePublish = () => {
@ -338,7 +329,9 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}, [nodes, edges]); }, [nodes, edges]);
return ( return (
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }} <div
ref={reactFlowWrapper}
style={{ width: '100%', height: '100%', position: 'relative' }}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
// 添加点击事件处理器,用于关闭添加节点菜单 // 添加点击事件处理器,用于关闭添加节点菜单
onClick={() => { onClick={() => {
@ -346,10 +339,11 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
setEdgeForNodeAdd(null); setEdgeForNodeAdd(null);
setPositionForNodeAdd(null); setPositionForNodeAdd(null);
} }
}}> }}
>
<ReactFlow <ReactFlow
id={reactFlowId} id={reactFlowId}
nodes={nodes.map(node => { nodes={nodes.map((node) => {
// 检查节点是否应该被隐藏 // 检查节点是否应该被隐藏
const isHidden = hiddenNodes.has(node.id); const isHidden = hiddenNodes.has(node.id);
// 应用透明度样式 // 应用透明度样式
@ -364,12 +358,13 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
} }
}; };
})} })}
edges={edges.map(edge => { edges={edges.map((edge) => {
// 检查边连接的节点是否被隐藏 // 检查边连接的节点是否被隐藏
const isSourceHidden = hiddenNodes.has(edge.source); const isSourceHidden = hiddenNodes.has(edge.source);
const isTargetHidden = hiddenNodes.has(edge.target); const isTargetHidden = hiddenNodes.has(edge.target);
// 如果源节点或目标节点被隐藏,则边也应用透明度 // 如果源节点或目标节点被隐藏,则边也应用透明度
const style = (isSourceHidden || isTargetHidden) ? { opacity: 0.3 } : {}; const style =
isSourceHidden || isTargetHidden ? { opacity: 0.3 } : {};
// 更新边的数据,确保选择框也应用透明度 // 更新边的数据,确保选择框也应用透明度
return { return {
@ -380,7 +375,9 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}, },
data: { data: {
...edge.data, ...edge.data,
...(isSourceHidden || isTargetHidden ? { hidden: true, opacity: 0.3 } : { opacity: 1 }) ...(isSourceHidden || isTargetHidden
? { hidden: true, opacity: 0.3 }
: { opacity: 1 })
} }
}; };
})} })}
@ -410,14 +407,17 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
} }
// 检查是否有开始或结束节点 // 检查是否有开始或结束节点
const hasStartOrEndNode = nodes.some(node => node.type === 'start' || node.type === 'end'); const hasStartOrEndNode = nodes.some(
(node) => node.type === 'start' || node.type === 'end'
);
if (hasStartOrEndNode) { if (hasStartOrEndNode) {
console.warn('开始和结束节点不允许删除'); console.warn('开始和结束节点不允许删除');
return false; // 阻止删除操作 return false; // 阻止删除操作
} }
// 检查是否有循环节点这里只是检查实际删除逻辑在onNodesDelete中处理 // 检查是否有循环节点这里只是检查实际删除逻辑在onNodesDelete中处理
const loopNodes = nodes.filter(node => const loopNodes = nodes.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END' node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
); );
@ -437,7 +437,8 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
} }
// 检查是否有循环节点 // 检查是否有循环节点
const loopNodes = deleted.filter(node => const loopNodes = deleted.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END' node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
); );
@ -446,15 +447,20 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
let nodesToRemove = [...deleted]; let nodesToRemove = [...deleted];
// 为每个循环节点找到其配对节点 // 为每个循环节点找到其配对节点
loopNodes.forEach(loopNode => { loopNodes.forEach((loopNode) => {
const component = loopNode.data?.component as { customDef?: string } | undefined; const component = loopNode.data?.component as
| { customDef?: string }
if (loopNode.data?.type === 'LOOP_START' && component?.customDef) { | undefined;
if (
loopNode.data?.type === 'LOOP_START' &&
component?.customDef
) {
try { try {
const customDef = JSON.parse(component.customDef); const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopEndNodeId; const relatedNodeId = customDef.loopEndNodeId;
// 添加关联的结束节点到删除列表 // 添加关联的结束节点到删除列表
const relatedNode = nodes.find(n => n.id === relatedNodeId); const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) { if (relatedNode) {
nodesToRemove.push(relatedNode); nodesToRemove.push(relatedNode);
} }
@ -462,12 +468,15 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
console.error('解析循环开始节点数据失败:', e); console.error('解析循环开始节点数据失败:', e);
} }
} }
else if (loopNode.data?.type === 'LOOP_END' && component?.customDef) { else if (
loopNode.data?.type === 'LOOP_END' &&
component?.customDef
) {
try { try {
const customDef = JSON.parse(component.customDef); const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopStartNodeId; const relatedNodeId = customDef.loopStartNodeId;
// 添加关联的开始节点到删除列表 // 添加关联的开始节点到删除列表
const relatedNode = nodes.find(n => n.id === relatedNodeId); const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) { if (relatedNode) {
nodesToRemove.push(relatedNode); nodesToRemove.push(relatedNode);
} }
@ -478,56 +487,123 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}); });
// 去重 // 去重
nodesToRemove = nodesToRemove.filter((node, index, self) => nodesToRemove = nodesToRemove.filter(
index === self.findIndex(n => n.id === node.id) (node, index, self) =>
index === self.findIndex((n) => n.id === node.id)
); );
// 删除所有相关节点和边 // 删除所有相关节点和边
setNodes((nds) => nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id))); setNodes((nds) =>
nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id))
);
// 删除与这些节点相关的所有边 // 删除与这些节点相关的所有边
const nodeIdsToRemove = nodesToRemove.map(node => node.id); const nodeIdsToRemove = nodesToRemove.map((node) => node.id);
setEdges((eds) => eds.filter((e) => setEdges((eds) =>
!nodeIdsToRemove.includes(e.source) && !nodeIdsToRemove.includes(e.target) eds.filter(
)); (e) =>
!nodeIdsToRemove.includes(e.source) &&
!nodeIdsToRemove.includes(e.target)
)
);
} }
else { else {
// 普通节点删除 // 普通节点删除
setNodes((nds) => nds.filter((n) => !deleted.find((d) => d.id === n.id))); setNodes((nds) =>
nds.filter((n) => !deleted.find((d) => d.id === n.id))
);
} }
setIsEditModalOpen(false); setIsEditModalOpen(false);
}} }}
onNodesChange={readOnly ? undefined : (!currentAppIsRunning ? onNodesChange : undefined)} // readOnly 或运行时禁用节点变更 onNodesChange={
onEdgesChange={readOnly ? undefined : (!currentAppIsRunning ? onEdgesChange : undefined)} // readOnly 或运行时禁用边变更 readOnly
onConnect={readOnly ? undefined : (!currentAppIsRunning ? onConnect : undefined)} // readOnly 或运行时禁用连接 ? undefined
onReconnect={readOnly ? undefined : (!currentAppIsRunning ? onReconnect : undefined)} // readOnly 或运行时禁用重新连接 : !currentAppIsRunning
onDragOver={readOnly ? undefined : (!currentAppIsRunning ? onDragOver : undefined)} // readOnly 或运行时禁用拖拽 ? onNodesChange
onDrop={readOnly ? undefined : (!currentAppIsRunning ? onDrop : undefined)} // readOnly 或运行时禁用放置 : undefined
onNodeDrag={readOnly ? undefined : (!currentAppIsRunning ? onNodeDrag : undefined)} // readOnly 或运行时禁用节点拖拽 } // readOnly 或运行时禁用节点变更
onEdgesChange={
readOnly
? undefined
: !currentAppIsRunning
? onEdgesChange
: undefined
} // readOnly 或运行时禁用边变更
onConnect={
readOnly ? undefined : !currentAppIsRunning ? onConnect : undefined
} // readOnly 或运行时禁用连接
onReconnect={
readOnly ? undefined : !currentAppIsRunning ? onReconnect : undefined
} // readOnly 或运行时禁用重新连接
onDragOver={
readOnly ? undefined : !currentAppIsRunning ? onDragOver : undefined
} // readOnly 或运行时禁用拖拽
onDrop={
readOnly ? undefined : !currentAppIsRunning ? onDrop : undefined
} // readOnly 或运行时禁用放置
onNodeDrag={
readOnly ? undefined : !currentAppIsRunning ? onNodeDrag : undefined
} // readOnly 或运行时禁用节点拖拽
connectionLineType={ConnectionLineType.SmoothStep} connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine} connectionLineComponent={CustomConnectionLine}
onNodeDragStop={readOnly ? undefined : (!currentAppIsRunning ? onNodeDragStop : undefined)} // readOnly 或运行时禁用节点拖拽停止 onNodeDragStop={
onNodeContextMenu={readOnly ? undefined : ((useDefault || currentAppIsRunning) ? onNodeContextMenu : undefined)} // readOnly 或应用编排模式下禁用节点上下文菜单 readOnly
onNodeDoubleClick={readOnly ? undefined : (!currentAppIsRunning ? onNodeDoubleClick : undefined)} // readOnly 或运行时禁用节点双击 ? undefined
onEdgeContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用边上下文菜单 : !currentAppIsRunning
onPaneClick={(useDefault || currentAppIsRunning) ? onPaneClick : undefined} // 运行时禁用面板点击 ? onNodeDragStop
onPaneContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用面板上下文菜单 : undefined
} // readOnly 或运行时禁用节点拖拽停止
onNodeContextMenu={
readOnly
? undefined
: useDefault || currentAppIsRunning
? onNodeContextMenu
: undefined
} // readOnly 或应用编排模式下禁用节点上下文菜单
onNodeDoubleClick={
readOnly
? undefined
: !currentAppIsRunning
? onNodeDoubleClick
: undefined
} // readOnly 或运行时禁用节点双击
onEdgeContextMenu={
readOnly
? undefined
: !currentAppIsRunning && useDefault
? onEdgeContextMenu
: undefined
} // readOnly 或运行时或应用编排模式下禁用边上下文菜单
onPaneClick={
useDefault || currentAppIsRunning ? onPaneClick : undefined
} // 运行时禁用面板点击
onPaneContextMenu={
readOnly
? undefined
: !currentAppIsRunning && useDefault
? onPaneContextMenu
: undefined
} // readOnly 或运行时或应用编排模式下禁用面板上下文菜单
onEdgeMouseEnter={(_event, edge) => { onEdgeMouseEnter={(_event, edge) => {
setEdges((eds) => eds.map(e => { setEdges((eds) =>
eds.map((e) => {
if (e.id === edge.id) { if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: true } }; return { ...e, data: { ...e.data, hovered: true } };
} }
return e; return e;
})); })
);
}} }}
onEdgeMouseLeave={(_event, edge) => { onEdgeMouseLeave={(_event, edge) => {
setEdges((eds) => eds.map(e => { setEdges((eds) =>
eds.map((e) => {
if (e.id === edge.id) { if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: false } }; return { ...e, data: { ...e.data, hovered: false } };
} }
return e; return e;
})); })
);
}} }}
fitView fitView
selectionOnDrag={!currentAppIsRunning} // 运行时禁用拖拽选择 selectionOnDrag={!currentAppIsRunning} // 运行时禁用拖拽选择
@ -550,14 +626,16 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
></ActionBar> ></ActionBar>
</Panel> </Panel>
)} )}
{useDefault && !readOnly && <Panel position="top-right"> {useDefault && !readOnly && (
<Panel position="top-right">
<HandlerBar <HandlerBar
onPublish={handlePublish} onPublish={handlePublish}
isRunning={currentAppIsRunning} isRunning={currentAppIsRunning}
setNodes={setNodes} setNodes={setNodes}
setEdges={setEdges} setEdges={setEdges}
/> />
</Panel>} </Panel>
)}
<AlignmentGuides /> <AlignmentGuides />
</ReactFlow> </ReactFlow>
@ -572,7 +650,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}} }}
> >
<NodeContextMenu <NodeContextMenu
node={nodes.find(n => n.id === menu.id)!} node={nodes.find((n) => n.id === menu.id)!}
onDelete={deleteNode} onDelete={deleteNode}
onEdit={editNode} onEdit={editNode}
onCopy={copyNode} onCopy={copyNode}
@ -594,7 +672,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}} }}
> >
<EdgeContextMenu <EdgeContextMenu
edge={edges.find(e => e.id === menu.id)!} edge={edges.find((e) => e.id === menu.id)!}
onDelete={deleteEdge} onDelete={deleteEdge}
onEdit={editEdge} onEdit={editEdge}
onAddNode={(edge) => { onAddNode={(edge) => {
@ -618,7 +696,11 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
> >
<PaneContextMenu <PaneContextMenu
position={menu.position!} position={menu.position!}
onAddNode={(nodeType: string, position: { x: number, y: number }, node: any) => { onAddNode={(
nodeType: string,
position: { x: number; y: number },
node: any
) => {
addNodeOnPane(nodeType, position, node); addNodeOnPane(nodeType, position, node);
setIsEditModalOpen(false); setIsEditModalOpen(false);
setMenu(null); // 关闭上下文菜单 setMenu(null); // 关闭上下文菜单
@ -638,12 +720,18 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
/> />
{/*统一的添加节点菜单 - 仅在默认模式且非运行时显示*/} {/*统一的添加节点菜单 - 仅在默认模式且非运行时显示*/}
{!currentAppIsRunning && useDefault && (edgeForNodeAdd || positionForNodeAdd) && ( {!currentAppIsRunning &&
useDefault &&
(edgeForNodeAdd || positionForNodeAdd) && (
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: edgeForNodeAdd ? (edgeForNodeAdd.data?.y as number || 0) : (positionForNodeAdd?.y || 0), top: edgeForNodeAdd
left: edgeForNodeAdd ? ((edgeForNodeAdd.data?.x as number || 0) + 20) : (positionForNodeAdd?.x || 0), ? (edgeForNodeAdd.data?.y as number) || 0
: positionForNodeAdd?.y || 0,
left: edgeForNodeAdd
? ((edgeForNodeAdd.data?.x as number) || 0) + 20
: positionForNodeAdd?.x || 0,
zIndex: 1000, zIndex: 1000,
transform: 'none' transform: 'none'
}} }}

@ -7,10 +7,11 @@ import {
IconPause, IconPause,
IconSync, IconSync,
IconUndo, IconUndo,
IconRedo IconRedo,
} from '@arco-design/web-react/icon'; } from '@arco-design/web-react/icon';
import { updateLogBarStatus } from '@/store/ideContainer'; import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { getCurrentAppKey } from '@/utils/flow/runtime';
const ButtonGroup = Button.Group; const ButtonGroup = Button.Group;
@ -37,30 +38,23 @@ const ActionBar: React.FC<ActionBarProps> = ({
onRun, onRun,
onPause, onPause,
onReRun, onReRun,
isRunning = false isRunning = false,
}) => { }) => {
const { logBarStatus, appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer); const { logBarStatus, appRuntimeData, currentAppData } = useSelector(
(state: any) => state.ideContainer
);
const dispatch = useDispatch(); const dispatch = useDispatch();
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
// 获取当前应用的运行状态 // 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey(); const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey] const currentAppIsRunning =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning ? appRuntimeData[currentAppKey].isRunning
: false; : false;
// 获取当前应用的暂停状态(如果有的话) // 获取当前应用的暂停状态(如果有的话)
const currentAppIsPaused = currentAppKey && appRuntimeData[currentAppKey] const currentAppIsPaused =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isPaused ? appRuntimeData[currentAppKey].isPaused
: false; : false;
@ -84,7 +78,9 @@ const ActionBar: React.FC<ActionBarProps> = ({
return ( return (
<div className="action-bar"> <div className="action-bar">
<Button onClick={onSave} type="primary" shape="round" icon={<IconSave />}></Button> <Button onClick={onSave} type="primary" shape="round" icon={<IconSave />}>
</Button>
{useDefault && ( {useDefault && (
<> <>
<ButtonGroup style={{ marginLeft: 8 }}> <ButtonGroup style={{ marginLeft: 8 }}>

@ -5,6 +5,7 @@ import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { getNodeData } from '@/api/appIns'; import { getNodeData } from '@/api/appIns';
import RunTimeData from './components/runTimeData'; import RunTimeData from './components/runTimeData';
import { getCurrentAppKey } from '@/utils/flow/runtime';
const TabPane = Tabs.TabPane; const TabPane = Tabs.TabPane;
@ -28,17 +29,17 @@ const data = [
{ {
key: '1', key: '1',
title: '运行日志', title: '运行日志',
content: '运行时日志...' content: '运行时日志...',
}, },
{ {
key: '2', key: '2',
title: '校验日志', title: '校验日志',
content: '校验日志...' content: '校验日志...',
}, },
{ {
key: '3', key: '3',
title: '运行数据', title: '运行数据',
content: '运行数据日志...' content: '运行数据日志...',
}, },
// { // {
// key: '4', // key: '4',
@ -56,28 +57,18 @@ const LogBar: React.FC<LogBarProps> = () => {
const [logContainerHeight, setLogContainerHeight] = useState('250px'); // 添加日志容器高度状态 const [logContainerHeight, setLogContainerHeight] = useState('250px'); // 添加日志容器高度状态
const [runtimeData, setRuntimeData] = useState<RuntimeData>({}); // 添加运行数据状态 const [runtimeData, setRuntimeData] = useState<RuntimeData>({}); // 添加运行数据状态
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { logBarStatus, appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer); const { logBarStatus, appRuntimeData, currentAppData } = useSelector(
(state: any) => state.ideContainer
);
const dispatch = useDispatch(); const dispatch = useDispatch();
// 辅助函数:获取当前应用/子流程的唯一标识符
const getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
// 处理 Tab 点击事件 // 处理 Tab 点击事件
const handleTabClick = (key: string) => { const handleTabClick = (key: string) => {
// 如果点击当前激活的 tab则切换收起状态 // 如果点击当前激活的 tab则切换收起状态
if (key === activeTab) { if (key === activeTab) {
dispatch(updateLogBarStatus(!logBarStatus)); dispatch(updateLogBarStatus(!logBarStatus));
} } else {
else {
// 如果点击的是其他 tab则切换到该 tab 并展开 // 如果点击的是其他 tab则切换到该 tab 并展开
setActiveTab(key); setActiveTab(key);
dispatch(updateLogBarStatus(true)); dispatch(updateLogBarStatus(true));
@ -87,20 +78,24 @@ const LogBar: React.FC<LogBarProps> = () => {
// 当 collapsed 状态改变时,直接更新元素的样式 // 当 collapsed 状态改变时,直接更新元素的样式
useEffect(() => { useEffect(() => {
if (resizeBoxRef.current) { if (resizeBoxRef.current) {
resizeBoxRef.current.style.height = logBarStatus ? logContainerHeight : '0px'; resizeBoxRef.current.style.height = logBarStatus
? logContainerHeight
: '0px';
} }
}, [logBarStatus, logContainerHeight]); }, [logBarStatus, logContainerHeight]);
// 处理 ResizeBox 手动调整大小事件 // 处理 ResizeBox 手动调整大小事件
const handleResize = (e: MouseEvent, size: { const handleResize = (
e: MouseEvent,
size: {
width: number; width: number;
height: number; height: number;
}) => { }
) => {
// 当高度接近收起状态的高度时,同步更新 logBarStatus 状态 // 当高度接近收起状态的高度时,同步更新 logBarStatus 状态
if (size.height <= 40) { if (size.height <= 40) {
dispatch(updateLogBarStatus(false)); dispatch(updateLogBarStatus(false));
} } else {
else {
dispatch(updateLogBarStatus(true)); dispatch(updateLogBarStatus(true));
// 更新日志容器高度状态 // 更新日志容器高度状态
setLogContainerHeight(`${size.height}px`); setLogContainerHeight(`${size.height}px`);
@ -118,10 +113,10 @@ const LogBar: React.FC<LogBarProps> = () => {
id: Date.now(), id: Date.now(),
type, type,
message, message,
timestamp timestamp,
}; };
setValidationLogs(prev => [...prev, newLog]); setValidationLogs((prev) => [...prev, newLog]);
// 自动切换到校验日志tab并展开logBar // 自动切换到校验日志tab并展开logBar
// setActiveTab('2'); // setActiveTab('2');
@ -133,24 +128,24 @@ const LogBar: React.FC<LogBarProps> = () => {
id: Date.now(), id: Date.now(),
type, type,
message, message,
timestamp timestamp,
}; };
setruntimeLogs(prev => [...prev, newLog]); setruntimeLogs((prev) => [...prev, newLog]);
// 自动切换到运行日志tab并展开logBar // 自动切换到运行日志tab并展开logBar
dispatch(updateLogBarStatus(true)); dispatch(updateLogBarStatus(true));
// 同时将日志添加到对应应用的运行日志中 // 同时将日志添加到对应应用的运行日志中
// 如果提供了 appId优先使用提供的 appId否则使用当前激活的应用 // 如果提供了 appId优先使用提供的 appId否则使用当前激活的应用
const targetAppKey = appId || getCurrentAppKey(); const targetAppKey = appId || getCurrentAppKey(currentAppData);
if (targetAppKey) { if (targetAppKey) {
dispatch({ dispatch({
type: 'ideContainer/addRuntimeLog', type: 'ideContainer/addRuntimeLog',
payload: { payload: {
log: newLog, log: newLog,
appId: targetAppKey appId: targetAppKey,
} },
}); });
} }
} }
@ -161,28 +156,35 @@ const LogBar: React.FC<LogBarProps> = () => {
// 清理事件监听器 // 清理事件监听器
return () => { return () => {
document.removeEventListener('logMessage', handleLogMessage as EventListener); document.removeEventListener(
'logMessage',
handleLogMessage as EventListener
);
}; };
}, [dispatch, currentAppData]); }, [dispatch, currentAppData]);
// 获取当前应用的运行状态 // 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey(); const currentAppKey = getCurrentAppKey(currentAppData);
const isRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning; const isRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning;
// 实现轮询获取运行数据 - 只在应用运行时轮询 // 实现轮询获取运行数据 - 只在应用运行时轮询
useEffect(() => { useEffect(() => {
let intervalId: NodeJS.Timeout | null = null; let intervalId: NodeJS.Timeout | null = null;
const appKey = getCurrentAppKey(); const appKey = getCurrentAppKey(currentAppData);
// 只有在应用正在运行且有 runId 时才开始轮询 // 只有在应用正在运行且有 runId 时才开始轮询
if (appKey && appRuntimeData[appKey]?.isRunning && appRuntimeData[appKey]?.runId) { if (
appKey &&
appRuntimeData[appKey]?.isRunning &&
appRuntimeData[appKey]?.runId
) {
const fetchRuntimeData = async () => { const fetchRuntimeData = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getNodeData(appRuntimeData[appKey].runId); const response = await getNodeData(appRuntimeData[appKey].runId);
setRuntimeData(prev => ({ setRuntimeData((prev) => ({
...prev, ...prev,
[appKey]: response.data [appKey]: response.data,
})); }));
} catch (error) { } catch (error) {
console.error('获取运行数据失败:', error); console.error('获取运行数据失败:', error);
@ -208,10 +210,10 @@ const LogBar: React.FC<LogBarProps> = () => {
// 当应用停止运行时,清除运行数据 // 当应用停止运行时,清除运行数据
useEffect(() => { useEffect(() => {
const appKey = getCurrentAppKey(); const appKey = getCurrentAppKey(currentAppData);
if (appKey && !appRuntimeData[appKey]?.isRunning) { if (appKey && !appRuntimeData[appKey]?.isRunning) {
// 清除当前应用的运行数据 // 清除当前应用的运行数据
setRuntimeData(prev => { setRuntimeData((prev) => {
const newData = { ...prev }; const newData = { ...prev };
delete newData[appKey]; delete newData[appKey];
return newData; return newData;
@ -222,12 +224,25 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染校验日志内容 // 渲染校验日志内容
const renderValidationLogs = () => { const renderValidationLogs = () => {
return ( return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}> <div
style={{
padding: '10px',
height: 'calc(100% - 40px)',
overflowY: 'auto',
}}
>
{validationLogs.length === 0 ? ( {validationLogs.length === 0 ? (
<p></p> <p></p>
) : ( ) : (
validationLogs.map(log => ( validationLogs.map((log) => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}> <div
key={log.id}
style={{
marginBottom: '8px',
padding: '4px',
borderBottom: '1px solid #eee',
}}
>
<div style={{ fontSize: '12px', color: '#999' }}> <div style={{ fontSize: '12px', color: '#999' }}>
{new Date(log.timestamp).toLocaleString()} {new Date(log.timestamp).toLocaleString()}
</div> </div>
@ -244,18 +259,32 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染运行时日志内容 // 渲染运行时日志内容
const renderRuntimeLogs = () => { const renderRuntimeLogs = () => {
// 获取当前应用的运行日志 // 获取当前应用的运行日志
const currentAppKey = getCurrentAppKey(); const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppLogs = currentAppKey && appRuntimeData[currentAppKey] const currentAppLogs =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].logs || [] ? appRuntimeData[currentAppKey].logs || []
: []; : [];
return ( return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}> <div
style={{
padding: '10px',
height: 'calc(100% - 40px)',
overflowY: 'auto',
}}
>
{currentAppLogs.length === 0 ? ( {currentAppLogs.length === 0 ? (
<p></p> <p></p>
) : ( ) : (
currentAppLogs.map((log: LogMessage) => ( currentAppLogs.map((log: LogMessage) => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}> <div
key={log.id}
style={{
marginBottom: '8px',
padding: '4px',
borderBottom: '1px solid #eee',
}}
>
<div style={{ fontSize: '12px', color: '#999' }}> <div style={{ fontSize: '12px', color: '#999' }}>
{new Date(log.timestamp).toLocaleString()} {new Date(log.timestamp).toLocaleString()}
</div> </div>
@ -271,11 +300,19 @@ const LogBar: React.FC<LogBarProps> = () => {
// 渲染运行数据内容 // 渲染运行数据内容
const renderRuntimeData = () => { const renderRuntimeData = () => {
const currentAppKey = getCurrentAppKey(); const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppDataContent = currentAppKey ? runtimeData[currentAppKey] : null; const currentAppDataContent = currentAppKey
? runtimeData[currentAppKey]
: null;
return ( return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}> <div
style={{
padding: '10px',
height: 'calc(100% - 40px)',
overflowY: 'auto',
}}
>
{!currentAppDataContent ? ( {!currentAppDataContent ? (
<p></p> <p></p>
) : ( ) : (
@ -292,7 +329,7 @@ const LogBar: React.FC<LogBarProps> = () => {
className={styles.logBar} className={styles.logBar}
directions={['top']} directions={['top']}
style={{ style={{
height: logBarStatus ? logContainerHeight : '0px' height: logBarStatus ? logContainerHeight : '0px',
}} }}
onMoving={handleResize} onMoving={handleResize}
> >
@ -304,10 +341,13 @@ const LogBar: React.FC<LogBarProps> = () => {
> >
{tabs.map((x) => ( {tabs.map((x) => (
<TabPane destroyOnHide key={x.key} title={x.title}> <TabPane destroyOnHide key={x.key} title={x.title}>
{x.key === '1' ? renderRuntimeLogs() : {x.key === '1'
x.key === '2' ? renderValidationLogs() : ? renderRuntimeLogs()
x.key === '3' ? renderRuntimeData() : // 添加运行数据渲染 : x.key === '2'
x.content} ? renderValidationLogs()
: x.key === '3'
? renderRuntimeData() // 添加运行数据渲染
: x.content}
</TabPane> </TabPane>
))} ))}
</Tabs> </Tabs>

@ -1,4 +1,8 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import {
createDefaultAppRuntimeState,
getCurrentAppKey,
} from '@/utils/flow/runtime';
// 定义初始状态类型 // 定义初始状态类型
interface IDEContainerState { interface IDEContainerState {
@ -16,22 +20,25 @@ interface IDEContainerState {
nodeStatusMap: Record<string, string>; // 节点状态映射 nodeStatusMap: Record<string, string>; // 节点状态映射
isRunning: boolean; // 是否正在运行 isRunning: boolean; // 是否正在运行
// 应用运行状态和日志数据按应用ID隔离存储 // 应用运行状态和日志数据按应用ID隔离存储
appRuntimeData: Record<string, { appRuntimeData: Record<
string,
{
nodeStatusMap: Record<string, string>; nodeStatusMap: Record<string, string>;
isRunning: boolean; isRunning: boolean;
isPaused: boolean; isPaused: boolean;
logs: any[]; logs: any[];
runId: string; runId: string;
eventSendNodeList: any[], // [{nodeID:topic}] eventSendNodeList: any[]; // [{nodeID:topic}]
eventlisteneList: any[] // [{nodeID:topic}] eventlisteneList: any[]; // [{nodeID:topic}]
}>; }
>;
gloBalVarList: any; gloBalVarList: any;
// 组件开发相关状态 // 组件开发相关状态
componentCoding: { componentCoding: {
localProjectPath: string; // code-server路径 localProjectPath: string; // code-server路径
name: string; // 组件名称 name: string; // 组件名称
projectId: string; // 组件标识 projectId: string; // 组件标识
id: string,// 组件id id: string; // 组件id
}; };
} }
@ -56,20 +63,8 @@ const initialState: IDEContainerState = {
localProjectPath: '', localProjectPath: '',
name: '', name: '',
projectId: '', projectId: '',
id: '' id: '',
} },
};
// 辅助函数:获取当前应用/子流程的唯一标识符
// 对于子流程,使用 key如 sub_xxx对于主流程使用 id
const getCurrentAppKey = (currentAppData: any) => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
}; };
// 创建切片 // 创建切片
@ -90,7 +85,10 @@ const ideContainerSlice = createSlice({
state.canvasDataMap = { ...state.canvasDataMap, ...action.payload }; state.canvasDataMap = { ...state.canvasDataMap, ...action.payload };
}, },
updateProjectComponentData(state, action) { updateProjectComponentData(state, action) {
state.projectComponentData = { ...state.projectComponentData, ...action.payload }; state.projectComponentData = {
...state.projectComponentData,
...action.payload,
};
}, },
updateCurrentAppData(state, action) { updateCurrentAppData(state, action) {
state.currentAppData = action.payload; state.currentAppData = action.payload;
@ -98,7 +96,9 @@ const ideContainerSlice = createSlice({
// 切换应用时,同步全局 nodeStatusMap 为新应用的节点状态 // 切换应用时,同步全局 nodeStatusMap 为新应用的节点状态
const newAppKey = getCurrentAppKey(action.payload); const newAppKey = getCurrentAppKey(action.payload);
if (newAppKey && state.appRuntimeData[newAppKey]) { if (newAppKey && state.appRuntimeData[newAppKey]) {
state.nodeStatusMap = { ...state.appRuntimeData[newAppKey].nodeStatusMap }; state.nodeStatusMap = {
...state.appRuntimeData[newAppKey].nodeStatusMap,
};
state.isRunning = state.appRuntimeData[newAppKey].isRunning; state.isRunning = state.appRuntimeData[newAppKey].isRunning;
} else { } else {
// 如果新应用没有运行时数据,清空节点状态 // 如果新应用没有运行时数据,清空节点状态
@ -140,15 +140,7 @@ const ideContainerSlice = createSlice({
// 更新目标应用的节点状态 // 更新目标应用的节点状态
if (targetAppKey) { if (targetAppKey) {
if (!state.appRuntimeData[targetAppKey]) { if (!state.appRuntimeData[targetAppKey]) {
state.appRuntimeData[targetAppKey] = { state.appRuntimeData[targetAppKey] = createDefaultAppRuntimeState();
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
} }
state.appRuntimeData[targetAppKey].nodeStatusMap[nodeId] = status; state.appRuntimeData[targetAppKey].nodeStatusMap[nodeId] = status;
} }
@ -171,15 +163,7 @@ const ideContainerSlice = createSlice({
const appKey = getCurrentAppKey(state.currentAppData); const appKey = getCurrentAppKey(state.currentAppData);
if (appKey) { if (appKey) {
if (!state.appRuntimeData[appKey]) { if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = { state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
} }
state.appRuntimeData[appKey].isRunning = payload; state.appRuntimeData[appKey].isRunning = payload;
} }
@ -189,15 +173,7 @@ const ideContainerSlice = createSlice({
const appKey = getCurrentAppKey(state.currentAppData); const appKey = getCurrentAppKey(state.currentAppData);
if (appKey) { if (appKey) {
if (!state.appRuntimeData[appKey]) { if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = { state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
} }
state.appRuntimeData[appKey].isPaused = payload; state.appRuntimeData[appKey].isPaused = payload;
} }
@ -206,15 +182,7 @@ const ideContainerSlice = createSlice({
updateRuntimeId: (state, { payload }) => { updateRuntimeId: (state, { payload }) => {
const appKey = getCurrentAppKey(state.currentAppData); const appKey = getCurrentAppKey(state.currentAppData);
if (!state.appRuntimeData[appKey]) { if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = { state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
} }
state.appRuntimeData[appKey].runId = payload; state.appRuntimeData[appKey].runId = payload;
}, },
@ -222,31 +190,18 @@ const ideContainerSlice = createSlice({
updateEventNodeList: (state, { payload }) => { updateEventNodeList: (state, { payload }) => {
const appKey = getCurrentAppKey(state.currentAppData); const appKey = getCurrentAppKey(state.currentAppData);
if (!state.appRuntimeData[appKey]) { if (!state.appRuntimeData[appKey]) {
state.appRuntimeData[appKey] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appKey] = { state.appRuntimeData[appKey] = {
nodeStatusMap: {}, ...state.appRuntimeData[appKey],
isRunning: false, ...payload,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
}; };
}
state.appRuntimeData[appKey] = { ...state.appRuntimeData[appKey], ...payload };
}, },
// 添加运行日志 // 添加运行日志
addRuntimeLog: (state, { payload }) => { addRuntimeLog: (state, { payload }) => {
const { log, appId } = payload; const { log, appId } = payload;
if (!state.appRuntimeData[appId]) { if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = { state.appRuntimeData[appId] = createDefaultAppRuntimeState();
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: []
};
} }
state.appRuntimeData[appId].logs.push(log); state.appRuntimeData[appId].logs.push(log);
}, },
@ -260,8 +215,8 @@ const ideContainerSlice = createSlice({
// 更新组件编码路径 // 更新组件编码路径
updateComponentCodingPath(state, action) { updateComponentCodingPath(state, action) {
state.componentCoding = { ...action.payload }; state.componentCoding = { ...action.payload };
} },
} },
}); });
// 导出动作 creators // 导出动作 creators
@ -286,7 +241,7 @@ export const {
updateEventNodeList, updateEventNodeList,
addRuntimeLog, addRuntimeLog,
clearRuntimeLogs, clearRuntimeLogs,
updateComponentCodingPath updateComponentCodingPath,
} = ideContainerSlice.actions; } = ideContainerSlice.actions;
// 默认导出 reducer // 默认导出 reducer

@ -1,14 +1,8 @@
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node'; import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
import store from '@/store/index'; import store from '@/store/index';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode'; import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
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 { updateEventNodeList } from '@/store/ideContainer'; import { updateEventNodeList } from '@/store/ideContainer';
import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
/** /**
* flow editor nodes edges * flow editor nodes edges
@ -37,12 +31,14 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
title: '开始', title: '开始',
parameters: { parameters: {
apiIns: [], apiIns: [],
apiOuts: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }], apiOuts: [
{ name: 'start', desc: '', dataType: '', defaultValue: '' },
],
dataIns: [], dataIns: [],
dataOuts: [] dataOuts: [],
},
type: 'start',
}, },
type: 'start'
}
}, },
{ {
id: `end-${timestamp}`, id: `end-${timestamp}`,
@ -51,16 +47,18 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
data: { data: {
title: '结束', title: '结束',
parameters: { parameters: {
apiIns: [{ name: 'end', desc: '', dataType: '', defaultValue: '' }], apiIns: [
{ name: 'end', desc: '', dataType: '', defaultValue: '' },
],
apiOuts: [], apiOuts: [],
dataIns: [], dataIns: [],
dataOuts: [] dataOuts: [],
},
type: 'end',
},
}, },
type: 'end'
}
}
], ],
edges: [] edges: [],
}; };
} }
// 否则返回空数组 // 否则返回空数组
@ -80,44 +78,55 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
try { try {
const customDef = JSON.parse(nodeConfig.component.customDef); const customDef = JSON.parse(nodeConfig.component.customDef);
// 使用展开运算符创建新数组,避免修改冻结对象 // 使用展开运算符创建新数组,避免修改冻结对象
eventlisteneList.splice(eventlisteneList.length, 0, { [nodeId]: customDef.topic }); eventlisteneList.splice(eventlisteneList.length, 0, {
[nodeId]: customDef.topic,
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} } else if (
else if (nodeId.includes('EVENTSEND') && !nodeId.includes('EVENTSEND_SYNC')) { nodeId.includes('EVENTSEND') &&
!nodeId.includes('EVENTSEND_SYNC')
) {
try { try {
const customDef = JSON.parse(nodeConfig.component.customDef); const customDef = JSON.parse(nodeConfig.component.customDef);
// 使用展开运算符创建新数组,避免修改冻结对象 // 使用展开运算符创建新数组,避免修改冻结对象
eventSendNodeList.splice(eventSendNodeList.length, 0, { [nodeId]: customDef.topic }); eventSendNodeList.splice(eventSendNodeList.length, 0, {
[nodeId]: customDef.topic,
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} }
if (eventlisteneList.length > 0 || eventSendNodeList.length > 0) store.dispatch(updateEventNodeList({ if (eventlisteneList.length > 0 || eventSendNodeList.length > 0)
store.dispatch(
updateEventNodeList({
eventSendNodeList: [...eventSendNodeList], eventSendNodeList: [...eventSendNodeList],
eventlisteneList: [...eventlisteneList] eventlisteneList: [...eventlisteneList],
})); })
);
else { else {
store.dispatch(updateEventNodeList({ store.dispatch(
updateEventNodeList({
eventSendNodeList: [], eventSendNodeList: [],
eventlisteneList: [] eventlisteneList: [],
})); })
);
} }
// 确定节点类型 // 确定节点类型
let nodeType = 'BASIC'; let nodeType = 'BASIC';
if (nodeId.includes('start')) { if (nodeId.includes('start')) {
nodeType = 'start'; nodeType = 'start';
} } else if (nodeId.includes('end')) {
else if (nodeId.includes('end')) {
nodeType = 'end'; nodeType = 'end';
} } else if (
else if (nodeConfig.component?.type === 'LOOP_START' || nodeConfig.component?.type === 'LOOP_END') { nodeConfig.component?.type === 'LOOP_START' ||
nodeConfig.component?.type === 'LOOP_END'
) {
nodeType = 'LOOP'; nodeType = 'LOOP';
} } else {
else {
nodeType = nodeConfig.component?.type || 'BASIC'; nodeType = nodeConfig.component?.type || 'BASIC';
} }
@ -135,10 +144,10 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData), apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData),
apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData), apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData),
dataIns: nodeConfig.dataIns || [], dataIns: nodeConfig.dataIns || [],
dataOuts: nodeConfig.dataOuts || [] dataOuts: nodeConfig.dataOuts || [],
},
type: nodeConfig.component?.type || nodeType,
}, },
type: nodeConfig.component?.type || nodeType
}
}; };
// 添加组件标识信息 // 添加组件标识信息
@ -157,16 +166,25 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
// 注册循环节点类型 // 注册循环节点类型
if (nodeType === 'LOOP') { if (nodeType === 'LOOP') {
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key)); const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (!nodeMap.includes('LOOP')) { if (!nodeMap.includes('LOOP')) {
registerNodeType('LOOP', LoopNode, '循环'); registerNodeType('LOOP', LoopNode, '循环');
} }
} }
// 注册其他节点类型 // 注册其他节点类型
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key)); const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (!nodeMap.includes(nodeType) && nodeType !== 'start' && nodeType !== 'end' && nodeType !== 'LOOP') { if (
registerNodeType(nodeType, getNodeComponent(nodeType), nodeConfig.componentName); !nodeMap.includes(nodeType) &&
nodeType !== 'start' &&
nodeType !== 'end' &&
nodeType !== 'LOOP'
) {
registerNodeType(
nodeType,
resolveNodeComponent(nodeType),
nodeConfig.componentName
);
} }
nodes.push(node); nodes.push(node);
@ -176,7 +194,15 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
const addedEdges = new Set<string>(); const addedEdges = new Set<string>();
// 创建一个映射来存储所有连接信息 // 创建一个映射来存储所有连接信息
const connections = new Map<string, { source: string; target: string; sourceHandle: string; targetHandle: string }>(); const connections = new Map<
string,
{
source: string;
target: string;
sourceHandle: string;
targetHandle: string;
}
>();
// 遍历所有节点,收集连接信息 // 遍历所有节点,收集连接信息
for (const entry of nodeEntries) { for (const entry of nodeEntries) {
@ -187,7 +213,7 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (nodeConfig.apiDownstream && Array.isArray(nodeConfig.apiDownstream)) { if (nodeConfig.apiDownstream && Array.isArray(nodeConfig.apiDownstream)) {
nodeConfig.apiDownstream.forEach((targetArray: string[]) => { nodeConfig.apiDownstream.forEach((targetArray: string[]) => {
if (Array.isArray(targetArray)) { if (Array.isArray(targetArray)) {
targetArray.forEach(target => { targetArray.forEach((target) => {
if (typeof target === 'string' && target.includes('$$')) { if (typeof target === 'string' && target.includes('$$')) {
const [targetNodeId, targetHandle] = target.split('$$'); const [targetNodeId, targetHandle] = target.split('$$');
const connectionKey = `${nodeId}-${targetNodeId}`; const connectionKey = `${nodeId}-${targetNodeId}`;
@ -199,17 +225,16 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (existing) { if (existing) {
connections.set(connectionKey, { connections.set(connectionKey, {
...existing, ...existing,
targetHandle: targetHandle targetHandle: targetHandle,
}); });
} }
} } else {
else {
// 创建新的连接信息 // 创建新的连接信息
connections.set(connectionKey, { connections.set(connectionKey, {
source: nodeId, source: nodeId,
target: targetNodeId, target: targetNodeId,
sourceHandle: '', // 将根据节点信息填充 sourceHandle: '', // 将根据节点信息填充
targetHandle: targetHandle targetHandle: targetHandle,
}); });
} }
} }
@ -222,7 +247,7 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (nodeConfig.apiUpstream && Array.isArray(nodeConfig.apiUpstream)) { if (nodeConfig.apiUpstream && Array.isArray(nodeConfig.apiUpstream)) {
nodeConfig.apiUpstream.forEach((sourceArray: string[]) => { nodeConfig.apiUpstream.forEach((sourceArray: string[]) => {
if (Array.isArray(sourceArray)) { if (Array.isArray(sourceArray)) {
sourceArray.forEach(source => { sourceArray.forEach((source) => {
if (typeof source === 'string' && source.includes('$$')) { if (typeof source === 'string' && source.includes('$$')) {
const [sourceNodeId, sourceHandle] = source.split('$$'); const [sourceNodeId, sourceHandle] = source.split('$$');
const connectionKey = `${sourceNodeId}-${nodeId}`; const connectionKey = `${sourceNodeId}-${nodeId}`;
@ -234,17 +259,16 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (existing) { if (existing) {
connections.set(connectionKey, { connections.set(connectionKey, {
...existing, ...existing,
sourceHandle: sourceHandle sourceHandle: sourceHandle,
}); });
} }
} } else {
else {
// 创建新的连接信息 // 创建新的连接信息
connections.set(connectionKey, { connections.set(connectionKey, {
source: sourceNodeId, source: sourceNodeId,
target: nodeId, target: nodeId,
sourceHandle: sourceHandle, sourceHandle: sourceHandle,
targetHandle: '' // 将根据节点信息填充 targetHandle: '', // 将根据节点信息填充
}); });
} }
} }
@ -269,9 +293,13 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (!finalSourceHandle) { if (!finalSourceHandle) {
if (source === 'start') { if (source === 'start') {
finalSourceHandle = 'start'; finalSourceHandle = 'start';
} } else if (
else if (sourceNode && sourceNode.data && sourceNode.data.parameters && sourceNode &&
sourceNode.data.parameters.apiOuts && sourceNode.data.parameters.apiOuts.length > 0) { sourceNode.data &&
sourceNode.data.parameters &&
sourceNode.data.parameters.apiOuts &&
sourceNode.data.parameters.apiOuts.length > 0
) {
// 查找匹配的目标句柄 // 查找匹配的目标句柄
const matchingApiOut = sourceNode.data.parameters.apiOuts.find( const matchingApiOut = sourceNode.data.parameters.apiOuts.find(
(apiOut: any) => apiOut.name === targetHandle (apiOut: any) => apiOut.name === targetHandle
@ -279,17 +307,18 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (matchingApiOut) { if (matchingApiOut) {
finalSourceHandle = matchingApiOut.name; finalSourceHandle = matchingApiOut.name;
} } else {
else {
// 如果没有精确匹配使用第一个apiOut // 如果没有精确匹配使用第一个apiOut
finalSourceHandle = sourceNode.data.parameters.apiOuts[0].name; finalSourceHandle = sourceNode.data.parameters.apiOuts[0].name;
} }
} } else if (
else if (sourceNode && sourceNode.component && sourceNode.component.type) { sourceNode &&
sourceNode.component &&
sourceNode.component.type
) {
// 根据节点类型获取正确的源句柄 // 根据节点类型获取正确的源句柄
finalSourceHandle = getNodeApiOutHandle(source, sourceNode); finalSourceHandle = getNodeApiOutHandle(source, sourceNode);
} } else {
else {
// 默认句柄 // 默认句柄
finalSourceHandle = 'done'; finalSourceHandle = 'done';
} }
@ -301,9 +330,13 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (!finalTargetHandle) { if (!finalTargetHandle) {
if (target === 'end') { if (target === 'end') {
finalTargetHandle = 'end'; finalTargetHandle = 'end';
} } else if (
else if (targetNode && targetNode.data && targetNode.data.parameters && targetNode &&
targetNode.data.parameters.apiIns && targetNode.data.parameters.apiIns.length > 0) { targetNode.data &&
targetNode.data.parameters &&
targetNode.data.parameters.apiIns &&
targetNode.data.parameters.apiIns.length > 0
) {
// 查找匹配的源句柄 // 查找匹配的源句柄
const matchingApiIn = targetNode.data.parameters.apiIns.find( const matchingApiIn = targetNode.data.parameters.apiIns.find(
(apiIn: any) => apiIn.name === sourceHandle (apiIn: any) => apiIn.name === sourceHandle
@ -311,13 +344,11 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
if (matchingApiIn) { if (matchingApiIn) {
finalTargetHandle = matchingApiIn.name; finalTargetHandle = matchingApiIn.name;
} } else {
else {
// 如果没有精确匹配使用第一个apiIn // 如果没有精确匹配使用第一个apiIn
finalTargetHandle = targetNode.data.parameters.apiIns[0].name; finalTargetHandle = targetNode.data.parameters.apiIns[0].name;
} }
} } else {
else {
// 默认句柄 // 默认句柄
finalTargetHandle = 'start'; finalTargetHandle = 'start';
} }
@ -338,8 +369,8 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
type: 'custom', type: 'custom',
lineType: 'api', lineType: 'api',
data: { data: {
lineType: 'api' lineType: 'api',
} },
}); });
} }
} }
@ -356,9 +387,12 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
// 第一个元素是源节点和句柄信息 // 第一个元素是源节点和句柄信息
const [sourceInfo, targetInfo] = connectionGroup; const [sourceInfo, targetInfo] = connectionGroup;
if (typeof sourceInfo === 'string' && sourceInfo.includes('@@') && if (
typeof targetInfo === 'string' && targetInfo.includes('@@')) { typeof sourceInfo === 'string' &&
sourceInfo.includes('@@') &&
typeof targetInfo === 'string' &&
targetInfo.includes('@@')
) {
const [sourceNodeId, sourceHandle] = sourceInfo.split('@@'); const [sourceNodeId, sourceHandle] = sourceInfo.split('@@');
const [targetNodeId, targetHandle] = targetInfo.split('@@'); const [targetNodeId, targetHandle] = targetInfo.split('@@');
@ -378,8 +412,8 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
type: 'custom', type: 'custom',
lineType: 'data', lineType: 'data',
data: { data: {
lineType: 'data' lineType: 'data',
} },
}); });
} }
} }
@ -403,12 +437,12 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
const flowData: any = { const flowData: any = {
id: 'main', id: 'main',
nodeConfigs: [], nodeConfigs: [],
lineConfigs: [] lineConfigs: [],
}; };
// 转换节点数据 // 转换节点数据
if (nodes && nodes.length > 0) { if (nodes && nodes.length > 0) {
flowData.nodeConfigs = nodes.map(node => { flowData.nodeConfigs = nodes.map((node) => {
// 确定 nodeId 和 nodeName // 确定 nodeId 和 nodeName
const nodeId = node.id || node.name; const nodeId = node.id || node.name;
const nodeName = node.data?.title || nodeId; const nodeName = node.data?.title || nodeId;
@ -418,21 +452,20 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
// 特殊处理 start 和 end 节点 // 特殊处理 start 和 end 节点
if (nodeId.includes('start')) { if (nodeId.includes('start')) {
nodeType = 'start'; nodeType = 'start';
} } else if (nodeId.includes('end')) {
else if (nodeId.includes('end')) {
nodeType = 'end'; nodeType = 'end';
} }
// 构造 x6 数据(位置信息) // 构造 x6 数据(位置信息)
const x6 = JSON.stringify({ const x6 = JSON.stringify({
position: node.position position: node.position,
}); });
// 构造 nodeConfig 对象 // 构造 nodeConfig 对象
const nodeConfig: any = { const nodeConfig: any = {
nodeId, nodeId,
nodeName, nodeName,
x6 x6,
}; };
// 处理 component 信息 // 处理 component 信息
@ -440,18 +473,20 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
nodeConfig.component = { nodeConfig.component = {
type: nodeType, type: nodeType,
compIdentifier: node.data.component.compIdentifier || '', compIdentifier: node.data.component.compIdentifier || '',
compInstanceIdentifier: node.data.component.compInstanceIdentifier || '', compInstanceIdentifier:
compId: node.data.compId || '' node.data.component.compInstanceIdentifier || '',
compId: node.data.compId || '',
}; };
if (node.data.component?.customDef) nodeConfig.component.customDef = node.data.component.customDef; if (node.data.component?.customDef)
} nodeConfig.component.customDef = node.data.component.customDef;
else if (nodeType !== 'start' && nodeType !== 'end') { } else if (nodeType !== 'start' && nodeType !== 'end') {
// 对于非 start/end 节点,添加基本的 component 信息 // 对于非 start/end 节点,添加基本的 component 信息
nodeConfig.component = { nodeConfig.component = {
type: nodeType type: nodeType,
}; };
} }
if (['BASIC', 'SUB'].includes(nodeType)) nodeConfig.component.compId = node.data.compId || ''; if (['BASIC', 'SUB'].includes(nodeType))
nodeConfig.component.compId = node.data.compId || '';
// 处理参数信息 // 处理参数信息
const parameters = node.data?.parameters || {}; const parameters = node.data?.parameters || {};
@ -463,7 +498,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
desc: input.desc, desc: input.desc,
dataType: input.dataType, dataType: input.dataType,
defaultValue: input.defaultValue, defaultValue: input.defaultValue,
arrayType: input.arrayType || null arrayType: input.arrayType || null,
})); }));
} }
@ -474,7 +509,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
desc: output.desc, desc: output.desc,
dataType: output.dataType, dataType: output.dataType,
defaultValue: output.defaultValue, defaultValue: output.defaultValue,
arrayType: output.arrayType || null arrayType: output.arrayType || null,
})); }));
} }
@ -486,22 +521,34 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
if (edges && edges.length > 0) { if (edges && edges.length > 0) {
flowData.lineConfigs = edges.map((edge, index) => { flowData.lineConfigs = edges.map((edge, index) => {
// 查找源节点和目标节点以确定连线类型 // 查找源节点和目标节点以确定连线类型
const sourceNode = nodes.find(node => node.id === edge.source); const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find(node => node.id === edge.target); const targetNode = nodes.find((node) => node.id === edge.target);
let lineType = 'DATA'; // 默认为DATA类型 let lineType = 'DATA'; // 默认为DATA类型
// 判断是否为CONVERT类型的连线 // 判断是否为CONVERT类型的连线
if (targetNode && ['JSONCONVERT', 'JSON2STR', 'STR2JSON'].includes(targetNode.type)) { if (
targetNode &&
['JSONCONVERT', 'JSON2STR', 'STR2JSON'].includes(targetNode.type)
) {
lineType = 'CONVERT'; lineType = 'CONVERT';
} }
// 判断是否为API类型的连线 // 判断是否为API类型的连线
else if (edge.sourceHandle && (edge.sourceHandle === 'apiOuts' || else if (
sourceNode?.data?.parameters?.apiOuts?.some((out: any) => (out.name || out.id) === edge.sourceHandle))) { edge.sourceHandle &&
(edge.sourceHandle === 'apiOuts' ||
sourceNode?.data?.parameters?.apiOuts?.some(
(out: any) => (out.name || out.id) === edge.sourceHandle
))
) {
lineType = 'API'; lineType = 'API';
} } else if (
else if (edge.targetHandle && (edge.targetHandle === 'apiIns' || edge.targetHandle &&
targetNode?.data?.parameters?.apiIns?.some((inp: any) => (inp.name || inp.id) === edge.targetHandle))) { (edge.targetHandle === 'apiIns' ||
targetNode?.data?.parameters?.apiIns?.some(
(inp: any) => (inp.name || inp.id) === edge.targetHandle
))
) {
lineType = 'API'; lineType = 'API';
} }
@ -510,12 +557,12 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
lineType, // 添加lineType属性 lineType, // 添加lineType属性
prev: { prev: {
nodeId: edge.source, nodeId: edge.source,
endpointId: edge.sourceHandle || 'done' // 默认使用 'done' endpointId: edge.sourceHandle || 'done', // 默认使用 'done'
}, },
next: { next: {
nodeId: edge.target, nodeId: edge.target,
endpointId: edge.targetHandle || 'start' // 默认使用 'start' endpointId: edge.targetHandle || 'start', // 默认使用 'start'
} },
}; };
}); });
} }
@ -530,20 +577,24 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
* @param complexKV - 使id {ID/nodeIdsub_ID} * @param complexKV - 使id {ID/nodeIdsub_ID}
* @returns convertFlowData * @returns convertFlowData
*/ */
export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: any) => { export const reverseConvertFlowData = (
nodes: any[],
edges: any[],
complexKV: any
) => {
// 初始化返回的数据结构 // 初始化返回的数据结构
const flowData: any = {}; const flowData: any = {};
// 转换节点数据 // 转换节点数据
if (nodes && nodes.length > 0) { if (nodes && nodes.length > 0) {
nodes.forEach(node => { nodes.forEach((node) => {
const nodeId = node.id; const nodeId = node.id;
// 构造节点配置对象 // 构造节点配置对象
const nodeConfig: any = { const nodeConfig: any = {
id: nodeId, id: nodeId,
componentName: node.data?.title || nodeId, componentName: node.data?.title || nodeId,
position: node.position || { x: 0, y: 0 } position: node.position || { x: 0, y: 0 },
}; };
// 处理 component 信息 // 处理 component 信息
@ -570,16 +621,14 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
dataIns: node.data.parameters.dataIns, dataIns: node.data.parameters.dataIns,
dataOuts: node.data.parameters.dataOuts, dataOuts: node.data.parameters.dataOuts,
subflowId: subflowId, subflowId: subflowId,
name: node.data.title name: node.data.title,
}) }),
}; };
} } else if (node.data?.component) {
else if (node.data?.component) {
nodeConfig.component = { ...node.data.component }; nodeConfig.component = { ...node.data.component };
} } else {
else {
nodeConfig.component = { nodeConfig.component = {
type: node.type type: node.type,
}; };
} }
@ -589,32 +638,28 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
// 处理 apiIns输入API // 处理 apiIns输入API
if (parameters.apiIns && parameters.apiIns.length > 0) { if (parameters.apiIns && parameters.apiIns.length > 0) {
nodeConfig.apiIns = parameters.apiIns; nodeConfig.apiIns = parameters.apiIns;
} } else {
else {
nodeConfig.apiIns = []; nodeConfig.apiIns = [];
} }
// 处理 apiOuts输出API // 处理 apiOuts输出API
if (parameters.apiOuts && parameters.apiOuts.length > 0) { if (parameters.apiOuts && parameters.apiOuts.length > 0) {
nodeConfig.apiOuts = parameters.apiOuts; nodeConfig.apiOuts = parameters.apiOuts;
} } else {
else {
nodeConfig.apiOuts = []; nodeConfig.apiOuts = [];
} }
// 处理 dataIns输入数据 // 处理 dataIns输入数据
if (parameters.dataIns && parameters.dataIns.length > 0) { if (parameters.dataIns && parameters.dataIns.length > 0) {
nodeConfig.dataIns = parameters.dataIns; nodeConfig.dataIns = parameters.dataIns;
} } else {
else {
nodeConfig.dataIns = []; nodeConfig.dataIns = [];
} }
// 处理 dataOuts输出数据 // 处理 dataOuts输出数据
if (parameters.dataOuts && parameters.dataOuts.length > 0) { if (parameters.dataOuts && parameters.dataOuts.length > 0) {
nodeConfig.dataOuts = parameters.dataOuts; nodeConfig.dataOuts = parameters.dataOuts;
} } else {
else {
nodeConfig.dataOuts = []; nodeConfig.dataOuts = [];
} }
@ -632,33 +677,45 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
// 处理连接关系 // 处理连接关系
if (edges && edges.length > 0) { if (edges && edges.length > 0) {
// 分析边的连接关系 // 分析边的连接关系
edges.forEach(edge => { edges.forEach((edge) => {
const sourceNode = edge.source; const sourceNode = edge.source;
const targetNode = edge.target; const targetNode = edge.target;
const sourceHandle = edge.sourceHandle || 'done'; const sourceHandle = edge.sourceHandle || 'done';
const targetHandle = edge.targetHandle || 'start'; const targetHandle = edge.targetHandle || 'start';
// 确定连接类型API 还是 DATA // 确定连接类型API 还是 DATA
const sourceNodeData = nodes.find(n => n.id === sourceNode); const sourceNodeData = nodes.find((n) => n.id === sourceNode);
const targetNodeData = nodes.find(n => n.id === targetNode); const targetNodeData = nodes.find((n) => n.id === targetNode);
const isApiConnection = const isApiConnection =
(sourceNodeData?.data?.parameters?.apiOuts?.some((out: any) => (out.name || out.id) === sourceHandle)) || sourceNodeData?.data?.parameters?.apiOuts?.some(
(targetNodeData?.data?.parameters?.apiIns?.some((inp: any) => (inp.name || inp.id) === targetHandle)) || (out: any) => (out.name || out.id) === sourceHandle
sourceHandle === 'start' || targetHandle === 'end' || ) ||
sourceHandle === 'end' || targetHandle === 'start'; targetNodeData?.data?.parameters?.apiIns?.some(
(inp: any) => (inp.name || inp.id) === targetHandle
) ||
sourceHandle === 'start' ||
targetHandle === 'end' ||
sourceHandle === 'end' ||
targetHandle === 'start';
if (isApiConnection) { if (isApiConnection) {
// API 连接 // API 连接
// 添加下游连接 // 添加下游连接
flowData[sourceNode].apiDownstream.push([`${targetNode}$$${targetHandle}`]); flowData[sourceNode].apiDownstream.push([
`${targetNode}$$${targetHandle}`,
]);
// 添加上游连接 // 添加上游连接
flowData[targetNode].apiUpstream.push([`${sourceNode}$$${sourceHandle}`]); flowData[targetNode].apiUpstream.push([
} `${sourceNode}$$${sourceHandle}`,
else { ]);
} else {
// 数据连接 // 数据连接
const dataConnection = [`${sourceNode}@@${sourceHandle}`, `${targetNode}@@${targetHandle}`]; const dataConnection = [
`${sourceNode}@@${sourceHandle}`,
`${targetNode}@@${targetHandle}`,
];
flowData[sourceNode].dataDownstream.push(dataConnection); flowData[sourceNode].dataDownstream.push(dataConnection);
flowData[targetNode].dataUpstream.push(dataConnection); flowData[targetNode].dataUpstream.push(dataConnection);
} }
@ -669,59 +726,67 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an
}; };
// 获取节点的API输入参数 // 获取节点的API输入参数
const getNodeApiIns = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => { const getNodeApiIns = (
nodeId: string,
nodeConfig: any,
currentProjectCompData: any[]
) => {
// JSON2STR 和 STR2JSON 不需要 API 输入 // JSON2STR 和 STR2JSON 不需要 API 输入
if (nodeConfig.component?.type === 'JSON2STR' || nodeConfig.component?.type === 'STR2JSON') { if (
nodeConfig.component?.type === 'JSON2STR' ||
nodeConfig.component?.type === 'STR2JSON'
) {
return []; return [];
} }
// 对于特定类型的节点使用预定义值 // 对于特定类型的节点使用预定义值
if (nodeConfig.component?.type === 'LOOP_START') { if (nodeConfig.component?.type === 'LOOP_START') {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
} } else if (nodeConfig.component?.type === 'LOOP_END') {
else if (nodeConfig.component?.type === 'LOOP_END') {
return [{ name: 'continue', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'continue', desc: '', dataType: '', defaultValue: '' }];
} } else if (nodeId.includes('end')) {
else if (nodeId.includes('end')) {
return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }];
} } else if (nodeConfig.component?.type === 'SUB') {
else if (nodeConfig.component?.type === 'SUB') {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
} } else {
else { const comp = currentProjectCompData.filter((item) => {
const comp = currentProjectCompData.filter(item => {
return (item.id || item?.comp?.id) === nodeConfig?.component?.compId; return (item.id || item?.comp?.id) === nodeConfig?.component?.compId;
}); });
if (comp && comp.length > 0) { if (comp && comp.length > 0) {
const apiIns = comp[0]?.def?.apis || comp[0]?.comp?.def?.apis || []; const apiIns = comp[0]?.def?.apis || comp[0]?.comp?.def?.apis || [];
return apiIns.map(v => { return apiIns.map((v) => {
return { return {
...v, ...v,
name: v.id, name: v.id,
desc: v.desc, desc: v.desc,
dataType: v?.dataType || '', dataType: v?.dataType || '',
defaultValue: v?.defaultValue || '' defaultValue: v?.defaultValue || '',
}; };
}); });
} } else {
else {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
} }
} }
}; };
// 获取节点的API输出参数 // 获取节点的API输出参数
const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => { const getNodeApiOuts = (
nodeId: string,
nodeConfig: any,
currentProjectCompData: any[]
) => {
// JSON2STR 和 STR2JSON 不需要 API 输出 // JSON2STR 和 STR2JSON 不需要 API 输出
if (nodeConfig.component?.type === 'JSON2STR' || nodeConfig.component?.type === 'STR2JSON') { if (
nodeConfig.component?.type === 'JSON2STR' ||
nodeConfig.component?.type === 'STR2JSON'
) {
return []; return [];
} }
// 对于特定类型的节点使用预定义值 // 对于特定类型的节点使用预定义值
if (nodeConfig.component?.type === 'LOOP_START') { if (nodeConfig.component?.type === 'LOOP_START') {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
} } else if (nodeConfig.component?.type === 'LOOP_END') {
else if (nodeConfig.component?.type === 'LOOP_END') {
// 从customDef中获取apiOutIds数组 // 从customDef中获取apiOutIds数组
try { try {
const customDef = JSON.parse(nodeConfig.component?.customDef || '{}'); const customDef = JSON.parse(nodeConfig.component?.customDef || '{}');
@ -731,15 +796,14 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
const breakIndex = apiOutIds.indexOf('break'); const breakIndex = apiOutIds.indexOf('break');
if (breakIndex !== -1) { if (breakIndex !== -1) {
// 返回从"break"开始的所有项 // 返回从"break"开始的所有项
return apiOutIds.slice(breakIndex).map(id => ({ return apiOutIds.slice(breakIndex).map((id) => ({
name: id, name: id,
id: id, id: id,
desc: id, desc: id,
dataType: '', dataType: '',
defaultValue: '' defaultValue: '',
})); }));
} } else {
else {
// 如果没有找到"break",则返回默认值 // 如果没有找到"break",则返回默认值
return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }];
} }
@ -747,8 +811,7 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
// 解析失败时返回默认值 // 解析失败时返回默认值
return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'break', desc: '', dataType: '', defaultValue: '' }];
} }
} } else if (nodeConfig.component?.type === 'SWITCH') {
else if (nodeConfig.component?.type === 'SWITCH') {
// 从customDef中获取apiOutIds数组 // 从customDef中获取apiOutIds数组
try { try {
const customDef = JSON.parse(nodeConfig.component?.customDef || '{}'); const customDef = JSON.parse(nodeConfig.component?.customDef || '{}');
@ -758,15 +821,14 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
const breakIndex = apiOutIds.indexOf('default'); const breakIndex = apiOutIds.indexOf('default');
if (breakIndex !== -1) { if (breakIndex !== -1) {
// 返回从"break"开始的所有项 // 返回从"break"开始的所有项
return apiOutIds.slice(breakIndex).map(id => ({ return apiOutIds.slice(breakIndex).map((id) => ({
name: id, name: id,
id: id, id: id,
desc: id, desc: id,
dataType: '', dataType: '',
defaultValue: '' defaultValue: '',
})); }));
} } else {
else {
// 如果没有找到"break",则返回默认值 // 如果没有找到"break",则返回默认值
return [{ name: 'default', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'default', desc: '', dataType: '', defaultValue: '' }];
} }
@ -774,26 +836,25 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
// 解析失败时返回默认值 // 解析失败时返回默认值
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
} }
} } else if (nodeId.includes('start')) {
else if (nodeId.includes('start')) {
return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }];
} } else if (nodeId.includes('end')) {
else if (nodeId.includes('end')) {
return []; return [];
} } else if (nodeConfig.component?.type === 'SUB') {
else if (nodeConfig.component?.type === 'SUB') {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
} } else {
else { const comp = currentProjectCompData.filter(
const comp = currentProjectCompData.filter(item => item.id === nodeConfig?.component?.compId); (item) => item.id === nodeConfig?.component?.compId
);
if (comp && comp.length > 0) { if (comp && comp.length > 0) {
return [{ return [
{
...comp[0].def?.apiOut, ...comp[0].def?.apiOut,
dataType: '', dataType: '',
defaultValue: '' defaultValue: '',
}]; },
} ];
else { } else {
return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }]; return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];
} }
} }
@ -803,14 +864,11 @@ const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData:
const getNodeApiOutHandle = (nodeId: string, nodeConfig: any) => { const getNodeApiOutHandle = (nodeId: string, nodeConfig: any) => {
if (nodeConfig.component?.type === 'LOOP_START') { if (nodeConfig.component?.type === 'LOOP_START') {
return 'done'; return 'done';
} } else if (nodeConfig.component?.type === 'LOOP_END') {
else if (nodeConfig.component?.type === 'LOOP_END') {
return 'break'; return 'break';
} } else if (nodeId.includes('start')) {
else if (nodeId.includes('start')) {
return 'start'; return 'start';
} } else if (nodeId.includes('end')) {
else if (nodeId.includes('end')) {
return 'end'; return 'end';
} }
return 'done'; return 'done';
@ -825,13 +883,17 @@ const getCurrentProjectStoreData = () => {
// 处理projectCompDto中的数据 // 处理projectCompDto中的数据
if (compData.projectCompDto) { if (compData.projectCompDto) {
const { mineComp = [], pubComp = [], teamWorkComp = [] } = compData.projectCompDto; const {
mineComp = [],
pubComp = [],
teamWorkComp = [],
} = compData.projectCompDto;
// 添加mineComp数据 // 添加mineComp数据
mineComp.forEach((item: any) => { mineComp.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'mineComp' type: 'mineComp',
}); });
}); });
@ -839,7 +901,7 @@ const getCurrentProjectStoreData = () => {
pubComp.forEach((item: any) => { pubComp.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'pubComp' type: 'pubComp',
}); });
}); });
@ -847,7 +909,7 @@ const getCurrentProjectStoreData = () => {
teamWorkComp.forEach((item: any) => { teamWorkComp.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'teamWorkComp' type: 'teamWorkComp',
}); });
}); });
} }
@ -860,7 +922,7 @@ const getCurrentProjectStoreData = () => {
mineFlow.forEach((item: any) => { mineFlow.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'mineFlow' type: 'mineFlow',
}); });
}); });
@ -868,7 +930,7 @@ const getCurrentProjectStoreData = () => {
pubFlow.forEach((item: any) => { pubFlow.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'pubFlow' type: 'pubFlow',
}); });
}); });
} }
@ -881,7 +943,13 @@ const getCurrentProjectStoreData = () => {
const compLibsData = sessionStorage.getItem(compLibsKey); const compLibsData = sessionStorage.getItem(compLibsKey);
if (compLibsData) { if (compLibsData) {
const { myLibs = [], pubLibs = [], teamLibs = [], myFlow = [], pubFlow = [] } = JSON.parse(compLibsData); const {
myLibs = [],
pubLibs = [],
teamLibs = [],
myFlow = [],
pubFlow = [],
} = JSON.parse(compLibsData);
// 处理 myLibs我的组件库 // 处理 myLibs我的组件库
if (Array.isArray(myLibs)) { if (Array.isArray(myLibs)) {
@ -890,7 +958,7 @@ const getCurrentProjectStoreData = () => {
lib.children.forEach((item: any) => { lib.children.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'mineComp' type: 'mineComp',
}); });
}); });
} }
@ -904,7 +972,7 @@ const getCurrentProjectStoreData = () => {
lib.children.forEach((item: any) => { lib.children.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'pubComp' type: 'pubComp',
}); });
}); });
} }
@ -918,7 +986,7 @@ const getCurrentProjectStoreData = () => {
lib.children.forEach((item: any) => { lib.children.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'teamWorkComp' type: 'teamWorkComp',
}); });
}); });
} }
@ -930,7 +998,7 @@ const getCurrentProjectStoreData = () => {
myFlow.forEach((item: any) => { myFlow.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'mineFlow' type: 'mineFlow',
}); });
}); });
} }
@ -940,7 +1008,7 @@ const getCurrentProjectStoreData = () => {
pubFlow.forEach((item: any) => { pubFlow.forEach((item: any) => {
result.push({ result.push({
...item, ...item,
type: 'pubFlow' type: 'pubFlow',
}); });
}); });
} }
@ -951,22 +1019,3 @@ const getCurrentProjectStoreData = () => {
} }
return result; return result;
}; };
// 根据节点类型获取对应的节点组件
const getNodeComponent = (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;
}
};

@ -0,0 +1,54 @@
import { Edge } from '@xyflow/react';
export const resolveInsertedNodeHandles = (node: any) => {
let sourceHandle = 'done';
let targetHandle = 'start';
if (node?.data?.parameters) {
const { apiOuts, apiIns } = node.data.parameters;
if (apiOuts && apiOuts.length > 0) {
sourceHandle = apiOuts[0].name || apiOuts[0].id || sourceHandle;
}
if (apiIns && apiIns.length > 0) {
targetHandle = apiIns[0].name || apiIns[0].id || targetHandle;
}
}
return { sourceHandle, targetHandle };
};
export const buildInsertedNodeEdges = (params: {
baseEdges: Edge[];
removedEdgeId: string;
sourceId: string;
sourceHandle?: string;
targetId: string;
targetHandle?: string;
insertedNodeId: string;
insertedNodeSourceHandle: string;
insertedNodeTargetHandle: string;
}): Edge[] => [
...params.baseEdges.filter((e) => e.id !== params.removedEdgeId),
{
id: `e${params.sourceId}-${params.insertedNodeId}`,
source: params.sourceId,
target: params.insertedNodeId,
sourceHandle: params.sourceHandle,
targetHandle: params.insertedNodeTargetHandle,
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
{
id: `e${params.insertedNodeId}-${params.targetId}`,
source: params.insertedNodeId,
target: params.targetId,
sourceHandle: params.insertedNodeSourceHandle,
targetHandle: params.targetHandle,
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
];

@ -0,0 +1,104 @@
import { Edge } from '@xyflow/react';
export const createLoopNodePair = (position: { x: number; y: number }) => {
const loopStartNode: any = {
id: `LOOP_START-${Date.now()}`,
type: 'LOOP',
position: { x: position.x, y: position.y },
data: {
title: '循环开始',
type: 'LOOP_START',
parameters: {
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: [],
},
component: {},
},
};
const loopEndNode: any = {
id: `LOOP_END-${Date.now()}`,
type: 'LOOP',
position: { x: position.x + 400, y: position.y },
data: {
title: '循环结束',
type: 'LOOP_END',
parameters: {
apiIns: [
{ name: 'continue', desc: '', dataType: '', defaultValue: '' },
],
apiOuts: [{ name: 'break', desc: '', dataType: '', defaultValue: '' }],
dataIns: [
{
arrayType: null,
dataType: 'INTEGER',
defaultValue: 10,
desc: '最大循环次数',
id: 'maxTime',
},
],
dataOuts: [],
},
component: {
type: 'LOOP_END',
customDef: JSON.stringify({
apiOutIds: ['continue', 'break'],
conditions: [],
loopStartNodeId: loopStartNode.id,
}),
loopStartNodeId: loopStartNode.id,
},
},
};
loopStartNode.data.component = {
type: 'LOOP_START',
customDef: JSON.stringify({ loopEndNodeId: loopEndNode.id }),
};
return { loopStartNode, loopEndNode };
};
export const createLoopGroupEdge = (
loopStartId: string,
loopEndId: string
): Edge => ({
id: `${loopStartId}-${loopEndId}-group`,
source: loopStartId,
target: loopEndId,
sourceHandle: `${loopStartId}-group`,
targetHandle: `${loopEndId}-group`,
type: 'custom',
});
export const createLoopInsertConnectionEdges = (params: {
sourceId: string;
sourceHandle: string;
targetId: string;
targetHandle: string;
loopStartId: string;
loopEndId: string;
}): Edge[] => [
{
id: `e${params.sourceId}-${params.loopStartId}`,
source: params.sourceId,
target: params.loopStartId,
sourceHandle: params.sourceHandle,
targetHandle: 'start',
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
{
id: `e${params.loopEndId}-${params.targetId}`,
source: params.loopEndId,
target: params.targetId,
sourceHandle: 'break',
targetHandle: params.targetHandle,
type: 'custom',
lineType: 'api',
data: { lineType: 'api' },
} as Edge,
];

@ -0,0 +1,79 @@
import { Node } from '@xyflow/react';
interface FlowNodeDefinition {
nodeName: string;
data: any;
id?: string;
flowHousVO?: {
id?: string;
};
}
type EventIdMode = 'id' | 'eventIdOptional';
export const createFlowNode = (
nodeType: string,
nodeDefinition: FlowNodeDefinition,
position: { x: number; y: number }
): Node => {
const node: any = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
...nodeDefinition.data,
title: nodeDefinition.nodeName,
type: nodeType,
},
};
if (nodeDefinition.id || nodeDefinition?.flowHousVO?.id) {
node.data.compId = nodeDefinition.id || nodeDefinition?.flowHousVO?.id;
}
return node;
};
export const attachFlowNodeComponent = (
node: any,
nodeType: string,
nodeDefinition: FlowNodeDefinition,
eventList: any[],
eventIdMode: EventIdMode
) => {
if (nodeType === 'SWITCH') {
node.data.component = {
customDef: JSON.stringify({
apiOutIds: ['default'],
conditions: [],
}),
};
} else if (nodeType === 'SUB') {
node.data.component = {
type: nodeType,
compId: node.data.compId,
};
} else if (nodeType === 'EVENTSEND' || nodeType === 'EVENTLISTENE') {
const emptyEvent = eventList.find((item) =>
item.topic.includes('**empty**')
);
node.data.component = {
type: nodeType,
customDef: {
eventId:
eventIdMode === 'eventIdOptional'
? emptyEvent?.eventId ?? null
: emptyEvent.id,
name: emptyEvent.name,
topic: emptyEvent.topic,
},
};
} else {
node.data.component = {
type: nodeType,
compId: nodeDefinition.id,
};
}
return node;
};

@ -0,0 +1,61 @@
import {
createFlowNode,
attachFlowNodeComponent,
} from '@/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,
},
};
};

@ -0,0 +1,40 @@
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
);
}
};

@ -0,0 +1,34 @@
export interface FlowCurrentAppData {
id?: string;
key?: string;
}
export interface AppRuntimeState {
nodeStatusMap: Record<string, string>;
isRunning: boolean;
isPaused: boolean;
logs: any[];
runId: string;
eventSendNodeList: any[];
eventlisteneList: any[];
}
export const getCurrentAppKey = (
currentAppData: FlowCurrentAppData | null | undefined
) => {
if (!currentAppData) return null;
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
return currentAppData.id || null;
};
export const createDefaultAppRuntimeState = (): AppRuntimeState => ({
nodeStatusMap: {},
isRunning: false,
isPaused: false,
logs: [],
runId: '',
eventSendNodeList: [],
eventlisteneList: [],
});

@ -0,0 +1,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);
};

@ -1,10 +1,5 @@
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType'; import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode'; import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
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';
// 获取handle类型 (api或data) // 获取handle类型 (api或data)
const getHandleType = (handleId: string, nodeParams: any) => { const getHandleType = (handleId: string, nodeParams: any) => {
@ -12,8 +7,15 @@ const getHandleType = (handleId: string, nodeParams: any) => {
const apiOuts = nodeParams.apiOuts || []; const apiOuts = nodeParams.apiOuts || [];
const apiIns = nodeParams.apiIns || []; const apiIns = nodeParams.apiIns || [];
if (apiOuts.some((api: any) => (api?.eventId || api.name || api.id) === handleId) || if (
apiIns.some((api: any) => (api?.eventId || api.name || api.id) === handleId) || (handleId.includes('loop'))) { apiOuts.some(
(api: any) => (api?.eventId || api.name || api.id) === handleId
) ||
apiIns.some(
(api: any) => (api?.eventId || api.name || api.id) === handleId
) ||
handleId.includes('loop')
) {
return 'api'; return 'api';
} }
@ -21,8 +23,10 @@ const getHandleType = (handleId: string, nodeParams: any) => {
const dataOuts = nodeParams.dataOuts || []; const dataOuts = nodeParams.dataOuts || [];
const dataIns = nodeParams.dataIns || []; const dataIns = nodeParams.dataIns || [];
if (dataOuts.some((data: any) => (data.name || data.id) === handleId) || if (
dataIns.some((data: any) => (data.name || data.id) === handleId)) { dataOuts.some((data: any) => (data.name || data.id) === handleId) ||
dataIns.some((data: any) => (data.name || data.id) === handleId)
) {
return 'data'; return 'data';
} }
@ -31,7 +35,12 @@ const getHandleType = (handleId: string, nodeParams: any) => {
}; };
// 验证数据类型是否匹配 // 验证数据类型是否匹配
const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeTypes, sourceHandleId: string, targetHandleId: string) => { const validateDataType = (
sourceNode: defaultNodeTypes,
targetNode: defaultNodeTypes,
sourceHandleId: string,
targetHandleId: string
) => {
const sourceParams = sourceNode.data?.parameters || {}; const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {}; const targetParams = targetNode.data?.parameters || {};
@ -41,13 +50,16 @@ const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeT
const sourceDataOuts = sourceParams.dataOuts || []; const sourceDataOuts = sourceParams.dataOuts || [];
// 查找源handle的数据类型 // 查找源handle的数据类型
const sourceApi = sourceApiOuts.find((api: any) => api.name === sourceHandleId); const sourceApi = sourceApiOuts.find(
const sourceData = sourceDataOuts.find((data: any) => data.name === sourceHandleId); (api: any) => api.name === sourceHandleId
);
const sourceData = sourceDataOuts.find(
(data: any) => data.name === sourceHandleId
);
if (sourceApi) { if (sourceApi) {
sourceDataType = sourceApi.dataType || ''; sourceDataType = sourceApi.dataType || '';
} } else if (sourceData) {
else if (sourceData) {
sourceDataType = sourceData.dataType || ''; sourceDataType = sourceData.dataType || '';
} }
@ -57,13 +69,16 @@ const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeT
const targetDataIns = targetParams.dataIns || []; const targetDataIns = targetParams.dataIns || [];
// 查找目标handle的数据类型 // 查找目标handle的数据类型
const targetApi = targetApiIns.find((api: any) => api.name === targetHandleId); const targetApi = targetApiIns.find(
const targetData = targetDataIns.find((data: any) => data.name === targetHandleId); (api: any) => api.name === targetHandleId
);
const targetData = targetDataIns.find(
(data: any) => data.name === targetHandleId
);
if (targetApi) { if (targetApi) {
targetDataType = targetApi.dataType || ''; targetDataType = targetApi.dataType || '';
} } else if (targetData) {
else if (targetData) {
targetDataType = targetData.dataType || ''; targetDataType = targetData.dataType || '';
} }
@ -76,29 +91,9 @@ const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeT
return sourceDataType === targetDataType; return sourceDataType === targetDataType;
}; };
// 根据节点类型获取对应的节点组件 // 根据节点类型获取对应的节点组件
const getNodeComponent = (nodeType: string) => { const getNodeComponent = (nodeType: string) => {
switch (nodeType) { return resolveNodeComponent(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 { getHandleType, validateDataType, getNodeComponent };
export {
getHandleType,
validateDataType,
getNodeComponent
};

Loading…
Cancel
Save