Compare commits

..

2 Commits

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

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

@ -55,8 +55,7 @@
"react-redux": "^7.2.6",
"recorder-core": "^1.3.25011100",
"redux": "^4.1.2",
"remark-gfm": "^4.0.1",
"utif": "^3.1.0"
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@svgr/webpack": "^5.5.0",

@ -131,9 +131,6 @@ importers:
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
utif:
specifier: ^3.1.0
version: 3.1.0
devDependencies:
'@svgr/webpack':
specifier: ^5.5.0
@ -4993,9 +4990,6 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
utif@3.1.0:
resolution: {integrity: sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -11555,10 +11549,6 @@ snapshots:
dependencies:
react: 17.0.2
utif@3.1.0:
dependencies:
pako: 1.0.11
util-deprecate@1.0.2: {}
util.promisify@1.0.1:

@ -5,9 +5,6 @@ import DynamicIcon from '@/components/DynamicIcon';
import NodeContentImage from '@/pages/flowEditor/components/nodeContentImage';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
import { useSelector } from 'react-redux';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getRuntimeImageUrl } from '@/utils/flow/imageRuntimeData';
const setIcon = () => {
return <DynamicIcon type="IconImage" style={{ fontSize: '16px', marginRight: '5px' }} />;
@ -15,12 +12,6 @@ const setIcon = () => {
const ImageNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '图片展示';
const { currentAppData, appRuntimeData } = useSelector((state: any) => state.ideContainer);
const currentAppKey = getCurrentAppKey(currentAppData);
const runtimeNodeData = currentAppKey
? appRuntimeData[currentAppKey]?.nodeData
: [];
const imageUrl = getRuntimeImageUrl(runtimeNodeData, id);
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
@ -44,7 +35,7 @@ const ImageNode = ({ data, id }: { data: any; id: string }) => {
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContentImage data={data} imageUrl={imageUrl} />
<NodeContentImage data={data} />
</div>
);
};

@ -105,40 +105,6 @@
min-height: 10px;
}
.node-image-box {
width: 150px;
padding: 6px;
margin: -1px auto 0;
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 3px;
:global(.arco-image) {
display: block;
max-width: 100%;
}
:global(.arco-image-img) {
max-width: 100%;
max-height: 120px;
object-fit: contain;
}
.node-image-loading,
.node-image-download {
display: block;
color: #86909c;
font-size: 12px;
line-height: 22px;
text-align: center;
word-break: break-all;
}
.node-image-download {
color: #165dff;
}
}
.node-content-box {
padding: 10px;
margin: -1px auto;

@ -1,5 +1,6 @@
import { Message } from '@arco-design/web-react';
import { Edge } from '@xyflow/react';
import { parseCustomDef } from '@/features/workflow/domain/customDef';
export interface ValidationResult {
isValid: boolean;
@ -78,12 +79,8 @@ const validateRestNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('REST节点配置信息格式错误');
return errors;
}
@ -119,12 +116,8 @@ const validateCodeNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('代码节点配置信息格式错误');
return errors;
}
@ -150,12 +143,8 @@ const validateSwitchNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('条件节点配置信息格式错误');
return errors;
}
@ -200,12 +189,8 @@ const validateLoopNode = (nodeData: any, nodeType: string): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('循环节点配置信息格式错误');
return errors;
}
@ -231,12 +216,8 @@ const validateEventNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('事件节点配置信息格式错误');
return errors;
}
@ -261,12 +242,8 @@ const validateWaitNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('等待节点配置信息格式错误');
return errors;
}
@ -295,12 +272,8 @@ const validateCycleNode = (nodeData: any): string[] => {
return errors;
}
let customDef;
try {
customDef = typeof nodeData.component.customDef === 'string'
? JSON.parse(nodeData.component.customDef)
: nodeData.component.customDef;
} catch (e) {
const customDef = parseCustomDef(nodeData.component.customDef, null);
if (!customDef) {
errors.push('周期节点配置信息格式错误');
return errors;
}
@ -332,7 +305,6 @@ const validateBasicParams = (nodeData: any): string[] => {
// 检查输入参数的完整性
if (nodeData.parameters?.dataIns) {
console.log("nodeData.parameters.dataIns", nodeData.parameters);
nodeData.parameters.dataIns.forEach((param: any, index: number) => {
if (!param.id) {
errors.push(`${index + 1}个输入参数的标识不能为空`);

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,28 @@
export type CustomDefValue = Record<string, any>;
export const parseCustomDef = (
value: unknown,
fallback: CustomDefValue | null = {}
): CustomDefValue | null => {
if (!value) return fallback;
if (typeof value === 'object') return value as CustomDefValue;
if (typeof value !== 'string') return fallback;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' ? parsed : fallback;
} catch {
return fallback;
}
};
export const stringifyCustomDef = (value: unknown): string => {
if (!value) return '{}';
if (typeof value === 'string') return value;
return JSON.stringify(value);
};
export const getNodeCustomDef = (
nodeData: { component?: { customDef?: unknown } } | undefined,
fallback: CustomDefValue = {}
) => parseCustomDef(nodeData?.component?.customDef, fallback);

@ -0,0 +1,66 @@
import { Edge, Node } from '@xyflow/react';
export type WorkflowMode = 'component' | 'application';
export type WorkflowPortKind = 'apiIn' | 'apiOut' | 'dataIn' | 'dataOut';
export type WorkflowLineType = 'api' | 'data' | 'convert';
export interface WorkflowPort {
id?: string;
name?: string;
desc?: string;
dataType?: string;
defaultValue?: any;
arrayType?: string | null;
topic?: string;
eventId?: string;
eventName?: string;
}
export interface WorkflowPorts {
apiIns: WorkflowPort[];
apiOuts: WorkflowPort[];
dataIns: WorkflowPort[];
dataOuts: WorkflowPort[];
}
export interface WorkflowComponentRef {
type?: string;
compId?: string;
compIdentifier?: string;
compInstanceIdentifier?: string;
customDef?: Record<string, any> | string;
[key: string]: any;
}
export interface WorkflowNodeData {
title: string;
type: string;
parameters: WorkflowPorts;
component?: WorkflowComponentRef;
compId?: string;
status?: string;
isStatusVisible?: boolean;
[key: string]: any;
}
export type WorkflowNode = Node<WorkflowNodeData>;
export type WorkflowEdge = Edge<{
lineType?: WorkflowLineType;
displayData?: Record<string, any>;
[key: string]: any;
}>;
export interface WorkflowGraph {
id?: string;
mode: WorkflowMode;
nodes: WorkflowNode[];
edges: WorkflowEdge[];
}
export interface FlowConvertResult {
nodes: WorkflowNode[];
edges: WorkflowEdge[];
}

@ -0,0 +1,59 @@
const hasObjectValues = (value: any) => {
return Boolean(value && typeof value === 'object' && Object.keys(value).length > 0);
};
const hasInitialCanvasData = (initialData: any, useDefault: boolean) => {
if (!initialData) {
return false;
}
if (Array.isArray(initialData)) {
return initialData.length > 0;
}
if (useDefault) {
return (
hasObjectValues(initialData?.main?.components) ||
hasObjectValues(initialData?.components)
);
}
return hasObjectValues(initialData);
};
export const hasCanvasContent = (canvas: any) => {
return Boolean(
canvas &&
((Array.isArray(canvas.nodes) && canvas.nodes.length > 0) ||
(Array.isArray(canvas.edges) && canvas.edges.length > 0))
);
};
export const shouldUseCachedCanvas = (params: {
cachedCanvas: any;
initialData: any;
useDefault: boolean;
}) => {
const { cachedCanvas, initialData, useDefault } = params;
if (!cachedCanvas) {
return false;
}
if (hasCanvasContent(cachedCanvas)) {
return true;
}
return !hasInitialCanvasData(initialData, useDefault);
};
export const shouldPersistCanvas = (params: {
nodes: any[];
edges: any[];
isRunning?: boolean;
}) => {
if (params.isRunning) {
return false;
}
return hasCanvasContent(params);
};

@ -0,0 +1,114 @@
import { Edge, Node } from '@xyflow/react';
export interface CopiedFlowData {
nodes: Node[];
edges: Edge[];
appId?: string;
}
export const buildCopiedFlowData = (
activeNode: Node,
nodes: Node[],
edges: Edge[],
appId?: string
): CopiedFlowData | null => {
const selectedNodes = nodes.filter(
(node) => node.selected || node.id === activeNode.id
);
const nodesToCopy = selectedNodes.filter(
(node) => node.type !== 'start' && node.type !== 'end'
);
if (nodesToCopy.length === 0) {
return null;
}
const nodeIds = new Set(nodesToCopy.map((node) => node.id));
const edgesToCopy = edges.filter(
(edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)
);
return {
nodes: nodesToCopy.map((node) => ({
...node,
selected: false,
dragging: false,
})),
edges: edgesToCopy.map((edge) => ({
...edge,
selected: false,
})),
appId,
};
};
export const buildPastedFlowData = (
copiedData: CopiedFlowData,
position: { x: number; y: number },
timestamp = Date.now()
) => {
const copiedNodes = copiedData.nodes || [];
const copiedEdges = copiedData.edges || [];
if (copiedNodes.length === 0) {
return null;
}
const minX = Math.min(...copiedNodes.map((node) => node.position.x));
const minY = Math.min(...copiedNodes.map((node) => node.position.y));
const maxX = Math.max(...copiedNodes.map((node) => node.position.x));
const maxY = Math.max(...copiedNodes.map((node) => node.position.y));
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
const offsetX = position.x - centerX;
const offsetY = position.y - centerY;
const idMap = new Map<string, string>();
const nodes = copiedNodes.map((node, index) => {
const newId = `${node.type}-${timestamp}-${index}`;
idMap.set(node.id, newId);
return {
...node,
id: newId,
position: {
x: node.position.x + offsetX,
y: node.position.y + offsetY,
},
selected: false,
dragging: false,
appId: undefined,
};
});
const edges = copiedEdges
.map((edge, index) => {
const source = idMap.get(edge.source);
const target = idMap.get(edge.target);
if (!source || !target) return null;
return {
...edge,
id: `e${source}-${target}-${timestamp}-${index}`,
source,
target,
selected: false,
};
})
.filter(Boolean) as Edge[];
return { nodes, edges };
};
export const buildPastedSingleNode = (
copiedNode: Node,
position: { x: number; y: number },
timestamp = Date.now()
) => ({
...copiedNode,
id: `${copiedNode.type}-${timestamp}`,
position,
selected: false,
dragging: false,
appId: undefined,
});

@ -0,0 +1,139 @@
import { Connection, Edge, Node } from '@xyflow/react';
import { getHandleType, validateDataType } from '@/utils/flowCommon';
export interface ConnectionValidationResult {
isValid: boolean;
lineType?: string;
message?: string;
}
export const validateWorkflowConnection = (
nodes: Node[],
connection: Connection
): ConnectionValidationResult => {
const sourceNode = nodes.find((node) => node.id === connection.source);
const targetNode = nodes.find((node) => node.id === connection.target);
if (!sourceNode || !targetNode) {
return { isValid: false };
}
if (sourceNode.id === targetNode.id) {
return { isValid: false, message: '不允许自旋链接' };
}
const sourceParams: any = sourceNode.data?.parameters || {};
const targetParams: any = targetNode.data?.parameters || {};
const sourceHandleType = getHandleType(connection.sourceHandle, sourceParams);
const targetHandleType = getHandleType(connection.targetHandle, targetParams);
if (sourceHandleType !== targetHandleType) {
return {
isValid: false,
message: `连接类型不匹配: ${sourceHandleType}, ${targetHandleType}`,
};
}
if (
!validateDataType(
sourceNode,
targetNode,
connection.sourceHandle,
connection.targetHandle
)
) {
return { isValid: false, message: '数据类型不匹配' };
}
return { isValid: true, lineType: sourceHandleType };
};
export const buildWorkflowConnectionEdge = (
nodes: Node[],
connection: Connection,
lineType: string
) => {
const sourceNode = nodes.find((node) => node.id === connection.source);
const targetNode = nodes.find((node) => node.id === connection.target);
if (!sourceNode || !targetNode) return null;
const sourceParams: any = sourceNode.data?.parameters || {};
const targetParams: any = targetNode.data?.parameters || {};
if (lineType === 'data') {
const sourceDataOut = (sourceParams.dataOuts || []).find(
(dataOut: any) =>
dataOut.name === connection.sourceHandle ||
dataOut.id === connection.sourceHandle
);
const targetDataIn = (targetParams.dataIns || []).find(
(dataIn: any) =>
dataIn.name === connection.targetHandle ||
dataIn.id === connection.targetHandle
);
if (
sourceDataOut &&
targetDataIn &&
sourceDataOut.dataType !== targetDataIn.dataType
) {
return {
edge: null,
message: `数据类型不匹配,源节点数据类型: ${sourceDataOut.dataType},目标节点数据类型: ${targetDataIn.dataType}`,
};
}
}
const edgeParams: Edge = {
...connection,
id:
connection.source && connection.target
? `e${connection.source}-${connection.target}-${connection.sourceHandle}-${connection.targetHandle}`
: '',
type: 'custom',
data: {
...(connection as any).data,
lineType,
},
} as Edge;
const sourceApi = (sourceParams.apiOuts || []).find(
(api: any) =>
(api?.eventId || api.name || api.id) === connection.sourceHandle
);
const targetApi = (targetParams.apiIns || []).find(
(api: any) =>
(api?.eventId || api.name || api.id) === connection.targetHandle
);
if (sourceApi?.topic) {
if (
!targetApi ||
!targetApi.topic ||
targetApi.topic.includes('**empty**') ||
!sourceApi.topic.includes('**empty**')
) {
edgeParams.data = {
...edgeParams.data,
lineType: 'api',
displayData: {
name: sourceApi.eventName,
eventId: sourceApi.eventId,
topic: sourceApi.topic,
},
};
}
} else if (targetApi?.topic && !targetApi.topic.includes('**empty**')) {
edgeParams.data = {
...edgeParams.data,
lineType: 'api',
displayData: {
name: targetApi.eventName,
eventId: targetApi.eventId,
topic: targetApi.topic,
},
};
}
return { edge: edgeParams };
};

@ -0,0 +1,49 @@
import { Edge, Node } from '@xyflow/react';
import { parseCustomDef } from '@/features/workflow/domain/customDef';
export const isLoopBoundaryNode = (node: Node) => {
return node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END';
};
export const resolveDeletedNodesWithLoopPairs = (
deletedNodes: Node[],
allNodes: Node[]
) => {
const nodesToRemove = [...deletedNodes];
deletedNodes.filter(isLoopBoundaryNode).forEach((loopNode) => {
const component = loopNode.data?.component as { customDef?: unknown } | undefined;
const customDef = parseCustomDef(component?.customDef) || {};
const relatedNodeId =
loopNode.data?.type === 'LOOP_START'
? customDef.loopEndNodeId
: customDef.loopStartNodeId;
if (!relatedNodeId) return;
const relatedNode = allNodes.find((node) => node.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
});
return nodesToRemove.filter(
(node, index, self) => index === self.findIndex((item) => item.id === node.id)
);
};
export const removeNodesAndConnectedEdges = (
allNodes: Node[],
allEdges: Edge[],
nodesToRemove: Node[]
) => {
const nodeIdsToRemove = new Set(nodesToRemove.map((node) => node.id));
return {
nodes: allNodes.filter((node) => !nodeIdsToRemove.has(node.id)),
edges: allEdges.filter(
(edge) =>
!nodeIdsToRemove.has(edge.source) && !nodeIdsToRemove.has(edge.target)
),
};
};

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

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

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

@ -0,0 +1,61 @@
import {
createFlowNode,
attachFlowNodeComponent,
} from '@/features/workflow/operations/nodeFactory';
import {
ensureNodeTypeRegistered,
resolveNodeComponent,
} from '@/features/workflow/registry/nodeRegistry';
type EventIdMode = 'id' | 'eventIdOptional';
export const resolveNodeDefinition = (
nodeList: any[],
nodeType: string,
fallbackNode?: any
) => {
return nodeList.find((n) => n.nodeType === nodeType) || fallbackNode || null;
};
export const buildRuntimeNode = (params: {
nodeType: string;
nodeDefinition: any;
position: { x: number; y: number };
eventList: any[];
eventIdMode: EventIdMode;
}) => {
const { nodeType, nodeDefinition, position, eventList, eventIdMode } = params;
const newNode = attachFlowNodeComponent(
createFlowNode(nodeType, nodeDefinition, position),
nodeType,
nodeDefinition,
eventList,
eventIdMode
);
ensureNodeTypeRegistered(
nodeType,
nodeDefinition.nodeName,
resolveNodeComponent(nodeType)
);
return newNode;
};
export const buildDroppedNode = (
nodeData: any,
position: { x: number; y: number }
) => {
ensureNodeTypeRegistered(nodeData.nodeType, nodeData.nodeName);
return {
id: `${nodeData.nodeType}-${Date.now()}`,
type: nodeData.nodeType,
position,
data: {
...nodeData.data,
title: nodeData.nodeName,
type: nodeData.nodeType,
},
};
};

@ -0,0 +1,22 @@
import { Edge, Node } from '@xyflow/react';
interface FlowSnapshotDetail {
nodes: Node[];
edges: Edge[];
}
export const dispatchFlowSnapshot = (detail: FlowSnapshotDetail) => {
const event = new CustomEvent('takeSnapshot', {
detail,
});
document.dispatchEvent(event);
};
export const dispatchFlowSnapshotAsync = (
detail: FlowSnapshotDetail,
delay = 0
) => {
return setTimeout(() => {
dispatchFlowSnapshot(detail);
}, delay);
};

@ -0,0 +1,266 @@
import React from 'react';
import { Edge, Node } from '@xyflow/react';
import { Message } from '@arco-design/web-react';
import { Dispatch } from 'redux';
import { getAppInfoNew, setMainFlowNew, setSubFlowNew } from '@/api/appRes';
import {
updateAppEventChannel,
updateAppFlowData,
} from '@/api/appEvent';
import {
deleteEventPub,
deleteEventSub,
queryEventItemBySceneIdOld,
} from '@/api/event';
import {
updateCanvasDataMap,
updateEventListOld,
updateFlowData,
} from '@/store/ideContainer';
import store from '@/store';
import {
convertFlowData,
reverseConvertFlowData,
revertFlowData,
} from '@/features/workflow/adapters/serverFlowAdapter';
import {
validateAllEdges,
validateAllNodes,
showValidationErrors,
} from '@/components/FlowEditor/nodeEditors/validators/nodeValidators';
import {
handelEventNodeList,
updateEvent,
upDatePublish,
} from '@/pages/flowEditor/utils/common';
import { sleep } from '@/utils/common';
interface SaveWorkflowDataParams {
nodes: Node[];
edges: Edge[];
useDefault: boolean;
initialData: any;
canvasDataMap: any;
dispatch: Dispatch<any>;
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
}
export const saveWorkflowData = async ({
nodes,
edges,
useDefault,
initialData,
canvasDataMap,
dispatch,
setNodes,
setEdges,
}: SaveWorkflowDataParams) => {
if (useDefault) {
try {
const nodeValidation = validateAllNodes(nodes);
if (!nodeValidation.isValid) {
showValidationErrors(nodeValidation.errors);
return;
}
const edgeValidation = validateAllEdges(edges, nodes);
if (!edgeValidation.isValid) {
showValidationErrors(edgeValidation.errors);
return;
}
const revertedData = revertFlowData(nodes, edges);
const upDatePublishCB = await upDatePublish(revertedData.nodeConfigs);
const newRevertedData = reverseConvertFlowData(
nodes,
edges,
upDatePublishCB
);
const { flowData, currentAppData, info } =
store.getState().ideContainer;
const { deleteEventSendNodeList, deleteEventlisteneList } =
handelEventNodeList(newRevertedData);
if (currentAppData.key.includes('sub')) {
const appEventDefinition = updateEvent(
revertedData.nodeConfigs,
initialData.appId
);
const params = {
...(currentAppData?.compData || {}),
components: newRevertedData,
appEventDefinition,
sceneId: info.id,
};
const res: any = await setSubFlowNew(
params,
currentAppData.parentAppId
);
if (res.code === 200) {
Message.success('保存成功');
const res1: any = await queryEventItemBySceneIdOld(info.id);
if (res1.code === 200) dispatch(updateEventListOld(res1.data));
const appRes: any = await getAppInfoNew(currentAppData.parentAppId);
dispatch(
updateFlowData({ [currentAppData.parentAppId]: appRes.data })
);
if (appRes.data.main?.components) {
const { nodes: parentNodes, edges: parentEdges } =
convertFlowData(appRes.data.main.components, true);
dispatch(
updateCanvasDataMap({
...canvasDataMap,
[currentAppData.parentAppId]: {
nodes: parentNodes,
edges: parentEdges,
},
})
);
}
dispatch(
updateCanvasDataMap({
...canvasDataMap,
[currentAppData.key]: { nodes, edges },
})
);
} else {
Message.error(res.message);
}
} else {
const appEventDefinition = updateEvent(
revertedData.nodeConfigs,
initialData.appId
);
const params = {
...(flowData[currentAppData.id]?.main || {}),
components: newRevertedData,
appEventDefinition,
sceneId: info.id,
};
const res: any = await setMainFlowNew(params, initialData.appId);
if (res.code === 200) {
Message.success('保存成功');
const res1: any = await queryEventItemBySceneIdOld(info.id);
if (res1.code === 200) dispatch(updateEventListOld(res1.data));
dispatch(
updateCanvasDataMap({
...canvasDataMap,
[currentAppData.id]: { nodes, edges },
})
);
const appRes: any = await getAppInfoNew(currentAppData.id);
dispatch(updateFlowData({ [currentAppData.id]: appRes.data }));
if (appRes.data.main?.components) {
const { nodes, edges } = convertFlowData(
appRes.data.main.components,
true
);
setNodes(nodes);
setEdges(edges);
dispatch(
updateCanvasDataMap({
...canvasDataMap,
[currentAppData.id]: { nodes, edges },
})
);
}
} else {
Message.error(res.message);
}
}
if (
deleteEventSendNodeList.length > 0 ||
deleteEventlisteneList.length > 0
) {
deleteEventSendNodeList.length > 0 &&
deleteEventPub({
appId: currentAppData.id,
topics: deleteEventSendNodeList,
});
deleteEventlisteneList.length > 0 &&
deleteEventSub({
appId: currentAppData.id,
topics: deleteEventlisteneList,
});
}
} catch (error) {
console.error('Error saving flow data:', error);
Message.error('保存失败');
}
return;
}
const appFlowParams: any = {
appEventList: {},
eventEdges: [],
};
nodes.forEach((node) => {
appFlowParams.appEventList[node.id] = {
x: node.position.x,
y: node.position.y,
};
});
const eventMap = new Map();
edges.forEach((edge: any) => {
appFlowParams.eventEdges.push({
id: edge.id,
source: edge.source,
target: edge.target,
lineType: 'data',
data: {
displayData: {
...edge.data.displayData,
},
},
});
const sourceId = edge.sourceHandle;
const targetId = edge.targetHandle;
const topic = edge.data.displayData?.topic;
if (eventMap.has(topic)) {
eventMap.get(topic).eventId.push(sourceId);
eventMap.get(topic).eventId.push(targetId);
} else {
eventMap.set(topic, {
eventId: [sourceId, targetId],
topic,
});
}
});
const appEventParams = Array.from(eventMap.values()).map((item) => ({
...item,
eventId: Array.from(new Set(item.eventId)),
}));
try {
updateAppFlowData(appFlowParams);
if (appEventParams.length > 0) {
for (const item of appEventParams) {
if (item.topic) {
await sleep(500);
await updateAppEventChannel(item);
}
}
}
Message.success('保存成功');
} catch (error: any) {
console.error('保存失败:', error);
Message.error('保存失败: ' + error.message);
}
};

@ -0,0 +1,34 @@
import React from 'react';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import AppNode from '@/components/FlowEditor/node/appNode/AppNode';
import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import MicrophoneNode from '@/components/FlowEditor/node/microphoneNode/MicrophoneNode';
export interface NodeDescriptor {
kind: string;
label: string;
render: React.ComponentType<any>;
}
export const nodeDescriptors: Record<string, NodeDescriptor> = {
BASIC: { kind: 'BASIC', label: '基础节点', render: BasicNode },
BASIC_LOOP: { kind: 'BASIC_LOOP', label: '基础节点', render: BasicNode },
SUB: { kind: 'SUB', label: '复合节点', render: BasicNode },
APP: { kind: 'APP', label: '应用节点', render: AppNode },
CODE: { kind: 'CODE', label: '代码节点', render: CodeNode },
IMAGE: { kind: 'IMAGE', label: '图片节点', render: ImageNode },
REST: { kind: 'REST', label: 'REST节点', render: RestNode },
SWITCH: { kind: 'SWITCH', label: '条件节点', render: SwitchNode },
LOOP: { kind: 'LOOP', label: '循环节点', render: LoopNode },
MICRO: { kind: 'MICRO', label: '语音节点', render: MicrophoneNode },
LOCAL: { kind: 'LOCAL', label: '本地节点', render: LocalNode },
};
export const getNodeDescriptor = (nodeType: string) => {
return nodeDescriptors[nodeType] || nodeDescriptors.LOCAL;
};

@ -0,0 +1,21 @@
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
import { getNodeDescriptor } from '@/features/workflow/registry/nodeDescriptors';
export const resolveNodeComponent = (nodeType: string) => {
return getNodeDescriptor(nodeType).render;
};
export const ensureNodeTypeRegistered = (
nodeType: string,
nodeName: string,
component?: any
) => {
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (!nodeMap.includes(nodeType)) {
registerNodeType(
nodeType,
component || resolveNodeComponent(nodeType),
nodeName
);
}
};

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

@ -0,0 +1,202 @@
import React from 'react';
import { Edge } from '@xyflow/react';
import { Message } from '@arco-design/web-react';
import { Dispatch } from 'redux';
import {
pauseApp,
reRunApp,
reconnectRun,
resumeApp,
runMainFlow,
runSubFlow,
stopApp,
} from '@/api/apps';
import {
clearRuntimeLogs,
resetNodeStatus,
updateIsPaused,
updateIsRunning,
updateRuntimeId,
} from '@/store/ideContainer';
import store from '@/store';
interface FlowRuntimeActionParams {
dispatch: Dispatch<any>;
getCurrentFlowAppKey: () => string | null;
}
interface RunWorkflowParams extends FlowRuntimeActionParams {
running: boolean;
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
}
const setEdgesRunningState = (
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>,
isRunning: boolean
) => {
setEdges((edges) =>
edges.map((edge) => ({
...edge,
data: {
...edge.data,
isRunning,
animationProgress: 0,
},
}))
);
};
export const runWorkflow = async ({
running,
dispatch,
getCurrentFlowAppKey,
setEdges,
}: RunWorkflowParams) => {
const { currentAppData, socketId, appRuntimeData } =
store.getState().ideContainer;
const appKey = getCurrentFlowAppKey();
if (running) {
const isSubFlow = currentAppData.key.includes('sub');
const res: any = isSubFlow
? await runSubFlow({
appId: currentAppData.parentAppId,
socketId,
subflowId: currentAppData.key,
})
: await runMainFlow({
appId: currentAppData.id,
socketId,
});
if (res.code === 200) {
dispatch(updateIsRunning(true));
dispatch(resetNodeStatus());
dispatch(updateRuntimeId(res.data));
setEdgesRunningState(setEdges, true);
} else {
Message.error(res.message);
}
return;
}
dispatch(updateIsRunning(false));
const runId =
appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : '';
if (runId) {
await stopApp(runId);
} else {
await stopApp(currentAppData.instanceId);
document.dispatchEvent(new CustomEvent('refreshAppList'));
}
dispatch(resetNodeStatus());
dispatch(updateRuntimeId(''));
setEdgesRunningState(setEdges, false);
if (appKey) {
dispatch(clearRuntimeLogs({ appId: appKey }));
}
};
export const pauseWorkflow = async ({
isPaused,
dispatch,
getCurrentFlowAppKey,
}: FlowRuntimeActionParams & { isPaused: boolean }) => {
const { currentAppData, appRuntimeData } = store.getState().ideContainer;
const appKey = getCurrentFlowAppKey();
if (!currentAppData) {
Message.warning('请先选择一个应用');
return;
}
const runId =
appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : '';
if (!runId) {
Message.warning('应用未运行');
return;
}
try {
const res: any = isPaused
? await resumeApp({ id: runId })
: await pauseApp({ id: runId });
if (res.code === 200) {
Message.success(isPaused ? '应用已恢复' : '应用已暂停');
dispatch(updateIsPaused(!isPaused));
} else {
Message.error(res.msg || (isPaused ? '恢复失败' : '暂停失败'));
}
} catch (error) {
console.error('暂停/恢复失败:', error);
Message.error('操作失败');
}
};
export const rerunWorkflow = async ({
dispatch,
getCurrentFlowAppKey,
}: FlowRuntimeActionParams) => {
const { currentAppData, appRuntimeData, socketId } =
store.getState().ideContainer;
const appKey = getCurrentFlowAppKey();
if (!currentAppData) {
Message.warning('请先选择一个应用');
return;
}
const instanceId =
appKey && appRuntimeData[appKey] ? appRuntimeData[appKey].runId : '';
if (!instanceId) {
Message.warning('应用未运行');
return;
}
try {
const appId =
currentAppData.key && currentAppData.key.includes('sub')
? currentAppData.parentAppId
: currentAppData.id;
const res: any = await reRunApp({
appId,
instanceId,
socketId,
});
if (res.code === 200) {
Message.success('应用重跑成功');
dispatch(resetNodeStatus());
} else {
Message.error(res.msg || '重跑失败');
}
} catch (error) {
console.error('重跑失败:', error);
Message.error('重跑失败');
}
};
export const reconnectWorkflowRuntime = async (params: {
instanceId: string;
newSocketId: string;
dispatch: Dispatch<any>;
}) => {
const { instanceId, newSocketId, dispatch } = params;
dispatch(updateRuntimeId(instanceId));
dispatch(updateIsRunning(true));
const res: any = await reconnectRun({
instanceId,
newSocketId,
});
return res?.data || res;
};

@ -0,0 +1,43 @@
interface RuntimeReconnectApp {
id?: string | number;
key?: string | number;
instanceId?: string | number;
scheduled?: number | string | boolean;
}
interface RuntimeReconnectRequestParams {
app: RuntimeReconnectApp | null | undefined;
socketId: string | null | undefined;
lastReconnectKey: string;
}
export const isScheduledRunning = (app: RuntimeReconnectApp | null | undefined) => {
return Boolean(
app && (app.scheduled === 1 || app.scheduled === '1' || app.scheduled === true)
);
};
export const buildRuntimeReconnectRequest = ({
app,
socketId,
lastReconnectKey,
}: RuntimeReconnectRequestParams) => {
const instanceId = app?.instanceId ? String(app.instanceId) : '';
const newSocketId = socketId ? String(socketId) : '';
const appKey = app?.id || app?.key ? String(app.id || app.key) : '';
if (!isScheduledRunning(app) || !instanceId || !newSocketId || !appKey) {
return null;
}
const reconnectKey = `${appKey}:${instanceId}:${newSocketId}`;
if (reconnectKey === lastReconnectKey) {
return null;
}
return {
instanceId,
newSocketId,
reconnectKey,
};
};

File diff suppressed because it is too large Load Diff

@ -2,58 +2,11 @@ import { useState, useRef, useEffect, useMemo } from 'react';
import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { updateCanvasDataMap, updateNodeStatus } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getNodeData } from '@/api/appIns';
import { updateCanvasDataMap } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
import { Dispatch } from 'redux';
const getRuntimeNodeStatus = (state: any) => {
switch (state) {
case 0:
case '0':
return 'running';
case 1:
case '1':
return 'success';
case -1:
case '-1':
return 'failed';
default:
return '';
}
};
const collectRuntimeNodes = (runtimeData: any) => {
if (!runtimeData) {
return [];
}
if (Array.isArray(runtimeData)) {
return runtimeData.flatMap((item) => {
if (Array.isArray(item?.nodes)) {
return item.nodes;
}
return item?.nodeId || item?.id ? [item] : [];
});
}
if (Array.isArray(runtimeData?.main?.nodeLogs)) {
return runtimeData.main.nodeLogs;
}
if (Array.isArray(runtimeData?.nodes)) {
return runtimeData.nodes;
}
if (Array.isArray(runtimeData?.data)) {
return collectRuntimeNodes(runtimeData.data);
}
return [];
};
export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
@ -93,11 +46,6 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
// 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false);
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const syncedRuntimeKeyRef = useRef('');
const currentRunId =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].runId
: '';
// 更新节点状态将从store获取的状态应用到节点上
useEffect(() => {
@ -164,70 +112,6 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
readOnly,
]);
useEffect(() => {
if (
readOnly ||
!currentAppKey ||
!currentAppIsRunning ||
!currentRunId ||
nodes.length === 0
) {
return;
}
const syncKey = `${currentAppKey}:${currentRunId}`;
if (syncedRuntimeKeyRef.current === syncKey) {
return;
}
let canceled = false;
syncedRuntimeKeyRef.current = syncKey;
const syncRuntimeNodeStatus = async () => {
try {
const nodeDataRes: any = await getNodeData(currentRunId);
const runtimeData = nodeDataRes?.data || nodeDataRes;
const runtimeNodes = collectRuntimeNodes(runtimeData);
if (canceled || runtimeNodes.length === 0) {
return;
}
runtimeNodes.forEach((node) => {
const nodeId = node?.nodeId || node?.id;
const status = getRuntimeNodeStatus(node?.state);
if (nodeId && status) {
dispatch(updateNodeStatus({
nodeId,
status,
appId: currentAppKey,
actionType: 'RUNTIME_RECONNECT_SYNC',
}));
}
});
} catch (error) {
if (!canceled) {
syncedRuntimeKeyRef.current = '';
console.error('同步运行实例节点状态失败:', error);
}
}
};
syncRuntimeNodeStatus();
return () => {
canceled = true;
};
}, [
currentAppKey,
currentAppIsRunning,
currentRunId,
dispatch,
nodes.length,
readOnly,
]);
const updateCanvasDataMapDebounced = useRef(
debounce(
(

@ -27,7 +27,12 @@ import { NodeTypes } from '@xyflow/react';
import { useSelector } from 'react-redux';
import HandlerBar from '@/pages/flowEditor/components/handlerBar';
import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
import {
isLoopBoundaryNode,
removeNodesAndConnectedEdges,
resolveDeletedNodesWithLoopPairs,
} from '@/features/workflow/operations/deleteOperations';
const edgeTypes = {
custom: CustomEdge
@ -163,54 +168,6 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
// 流程发布弹窗状态
const [publishModalVisible, setPublishModalVisible] = React.useState(false);
const selectedCopyableNodes = useMemo(
() =>
nodes.filter(
(node) =>
node.selected && node.type !== 'start' && node.type !== 'end'
),
[nodes]
);
const selectedEdgeCount = useMemo(
() => edges.filter((edge) => edge.selected).length,
[edges]
);
const hasMultiElementSelection =
selectedCopyableNodes.length > 1 || selectedEdgeCount > 0;
const copySelectedElements = React.useCallback(
(fallbackNode?: Node) => {
const shouldCopySelection =
selectedCopyableNodes.length > 0 &&
(!fallbackNode || fallbackNode.selected);
const nodeToCopy =
(shouldCopySelection ? selectedCopyableNodes[0] : undefined) ||
(fallbackNode?.type !== 'start' && fallbackNode?.type !== 'end'
? fallbackNode
: undefined);
if (!nodeToCopy) {
return;
}
copyNode(nodeToCopy);
setIsEditModalOpen(false);
setMenu(null);
},
[copyNode, selectedCopyableNodes, setIsEditModalOpen, setMenu]
);
const contextNode =
menu?.type === 'node' ? nodes.find((node) => node.id === menu.id) : null;
const contextEdge =
menu?.type === 'edge' ? edges.find((edge) => edge.id === menu.id) : null;
const nodeContextCopiesSelection =
!!contextNode?.selected && hasMultiElementSelection;
const edgeContextCopiesSelection =
!!contextEdge?.selected && selectedCopyableNodes.length > 0;
// 监听自定义事件以隐藏/显示节点
useEffect(() => {
const handleToggleNodeVisibility = (event: CustomEvent) => {
@ -303,11 +260,12 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
// Ctrl/Cmd+C 复制选中的节点
if (isModifierKey && e.key === 'c' && canEdit) {
// 获取当前选中的节点
const selectedNode = nodes.find(
(node) =>
node.selected && node.type !== 'start' && node.type !== 'end'
);
const selectedNode = nodes.find((node) => node.selected);
if (selectedNode) {
// 不允许复制开始和结束节点
if (selectedNode.type === 'start' || selectedNode.type === 'end') {
return;
}
e.preventDefault();
copyNode(selectedNode);
}
@ -462,12 +420,6 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return false; // 阻止删除操作
}
// 检查是否有循环节点这里只是检查实际删除逻辑在onNodesDelete中处理
const loopNodes = nodes.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
// 允许删除操作继续进行
return !currentAppIsRunning; // 在运行时禁止删除任何元素
}}
@ -483,82 +435,18 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return;
}
// 检查是否有循环节点
const loopNodes = deleted.filter(
(node) =>
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
);
if (loopNodes.length > 0) {
// 处理循环节点删除
let nodesToRemove = [...deleted];
// 为每个循环节点找到其配对节点
loopNodes.forEach((loopNode) => {
const component = loopNode.data?.component as
| { customDef?: string }
| undefined;
if (
loopNode.data?.type === 'LOOP_START' &&
component?.customDef
) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopEndNodeId;
// 添加关联的结束节点到删除列表
const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
} catch (e) {
console.error('解析循环开始节点数据失败:', e);
}
}
else if (
loopNode.data?.type === 'LOOP_END' &&
component?.customDef
) {
try {
const customDef = JSON.parse(component.customDef);
const relatedNodeId = customDef.loopStartNodeId;
// 添加关联的开始节点到删除列表
const relatedNode = nodes.find((n) => n.id === relatedNodeId);
if (relatedNode) {
nodesToRemove.push(relatedNode);
}
} catch (e) {
console.error('解析循环结束节点数据失败:', e);
}
}
});
// 去重
nodesToRemove = nodesToRemove.filter(
(node, index, self) =>
index === self.findIndex((n) => n.id === node.id)
);
// 删除所有相关节点和边
setNodes((nds) =>
nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id))
);
// 删除与这些节点相关的所有边
const nodeIdsToRemove = nodesToRemove.map((node) => node.id);
setEdges((eds) =>
eds.filter(
(e) =>
!nodeIdsToRemove.includes(e.source) &&
!nodeIdsToRemove.includes(e.target)
)
);
}
else {
// 普通节点删除
setNodes((nds) =>
nds.filter((n) => !deleted.find((d) => d.id === n.id))
if (deleted.some(isLoopBoundaryNode)) {
const nodesToRemove = resolveDeletedNodesWithLoopPairs(deleted, nodes);
const nextGraph = removeNodesAndConnectedEdges(
nodes,
edges,
nodesToRemove
);
setNodes(nextGraph.nodes);
setEdges(nextGraph.edges);
} else {
const deletedIds = new Set(deleted.map((node) => node.id));
setNodes((nds) => nds.filter((node) => !deletedIds.has(node.id)));
}
setIsEditModalOpen(false);
@ -697,11 +585,10 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}}
>
<NodeContextMenu
node={contextNode!}
node={nodes.find((n) => n.id === menu.id)!}
onDelete={deleteNode}
onEdit={editNode}
onCopy={copySelectedElements}
copyLabel={nodeContextCopiesSelection ? '复制选中内容' : '复制节点'}
onCopy={copyNode}
onCloseMenu={setMenu}
onCloseOpenModal={setIsEditModalOpen}
isRunning={currentAppIsRunning}
@ -720,13 +607,9 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
}}
>
<EdgeContextMenu
edge={contextEdge!}
edge={edges.find((e) => e.id === menu.id)!}
onDelete={deleteEdge}
onEdit={editEdge}
onCopy={
edgeContextCopiesSelection ? () => copySelectedElements() : undefined
}
copyLabel="复制选中内容"
onAddNode={(edge) => {
setEdgeForNodeAdd(edge);
setIsEditModalOpen(false);
@ -748,15 +631,6 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
>
<PaneContextMenu
position={menu.position!}
onCopySelection={
selectedCopyableNodes.length > 0
? () => copySelectedElements()
: undefined
}
onPasteNode={(position) => {
pasteNode(position);
setMenu(null);
}}
onAddNode={(
nodeType: string,
position: { x: number; y: number },

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

@ -6,8 +6,6 @@ interface EdgeContextMenuProps {
edge: Edge;
onDelete?: (edge: Edge) => void;
onEdit?: (edge: Edge) => void;
onCopy?: () => void;
copyLabel?: string;
onAddNode?: (edge: Edge) => void; // 添加在边上添加节点的功能
}
@ -15,8 +13,6 @@ const EdgeContextMenu: React.FC<EdgeContextMenuProps> = ({
edge,
onDelete,
onEdit,
onCopy,
copyLabel = '复制选中内容',
onAddNode
}) => {
const handleDelete = () => {
@ -27,10 +23,6 @@ const EdgeContextMenu: React.FC<EdgeContextMenuProps> = ({
onEdit && onEdit(edge);
};
const handleCopy = () => {
onCopy && onCopy();
};
// 添加在边上添加节点的处理函数
const handleAddNode = () => {
onAddNode && onAddNode(edge);
@ -44,11 +36,6 @@ const EdgeContextMenu: React.FC<EdgeContextMenuProps> = ({
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
>
{onCopy && (
<Menu.Item key="copy" onClick={handleCopy}>
{copyLabel}
</Menu.Item>
)}
<Menu.Item key="add-node" onClick={handleAddNode}>
</Menu.Item>

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

@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position } from '@xyflow/react';
import { Handle, Position, useStore } from '@xyflow/react';
import { Image } from '@arco-design/web-react';
import { formatDataType } from '@/utils/common';
import UTIF from 'utif';
interface NodeContentData {
parameters?: {
@ -104,64 +103,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
};
const getImageBlobType = (url: string, blobType: string) => {
if (blobType && blobType !== 'application/octet-stream') {
return blobType;
}
const cleanUrl = url.split('?')[0].toLowerCase();
if (cleanUrl.endsWith('.png')) return 'image/png';
if (cleanUrl.endsWith('.jpg') || cleanUrl.endsWith('.jpeg')) return 'image/jpeg';
if (cleanUrl.endsWith('.gif')) return 'image/gif';
if (cleanUrl.endsWith('.webp')) return 'image/webp';
if (cleanUrl.endsWith('.bmp')) return 'image/bmp';
if (cleanUrl.endsWith('.svg')) return 'image/svg+xml';
if (cleanUrl.endsWith('.tif') || cleanUrl.endsWith('.tiff')) return 'image/tiff';
return blobType || 'application/octet-stream';
};
const isTiffImage = (url: string, blobType: string) => {
const imageType = getImageBlobType(url, blobType).toLowerCase();
const cleanUrl = url.split('?')[0].toLowerCase();
return (
imageType === 'image/tiff' ||
cleanUrl.endsWith('.tif') ||
cleanUrl.endsWith('.tiff')
);
};
const createTiffPreviewUrl = async (url: string, blob: Blob) => {
const buffer = await blob.arrayBuffer();
const ifds = UTIF.decode(buffer);
const firstImage = ifds[0];
if (!firstImage) {
throw new Error('empty tiff image');
}
UTIF.decodeImage(buffer, firstImage);
const rgba = UTIF.toRGBA8(firstImage);
const canvas = document.createElement('canvas');
canvas.width = firstImage.width;
canvas.height = firstImage.height;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('canvas context unavailable');
}
const imageData = context.createImageData(firstImage.width, firstImage.height);
imageData.data.set(rgba);
context.putImageData(imageData, 0, 0);
return canvas.toDataURL('image/png');
};
const NodeContent = ({ data, imageUrl = '' }: { data: NodeContentData; imageUrl?: string }) => {
const [previewUrl, setPreviewUrl] = useState('');
const [imageLoadError, setImageLoadError] = useState(false);
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
@ -172,60 +114,6 @@ const NodeContent = ({ data, imageUrl = '' }: { data: NodeContentData; imageUrl?
const isEndNode = data.type === 'end';
const isSpecialNode = isStartNode || isEndNode;
useEffect(() => {
let canceled = false;
let objectUrl = '';
setPreviewUrl('');
setImageLoadError(false);
if (!imageUrl) {
return undefined;
}
const loadImageBlob = async () => {
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`load image failed: ${response.status}`);
}
const blob = await response.blob();
if (isTiffImage(imageUrl, blob.type)) {
const tiffPreviewUrl = await createTiffPreviewUrl(imageUrl, blob);
if (!canceled) {
setPreviewUrl(tiffPreviewUrl);
}
return;
}
const typedBlob = new Blob([blob], {
type: getImageBlobType(imageUrl, blob.type),
});
objectUrl = URL.createObjectURL(typedBlob);
if (!canceled) {
setPreviewUrl(objectUrl);
}
} catch (error) {
console.error('图片下载预览失败:', error);
if (!canceled) {
setPreviewUrl(imageUrl);
setImageLoadError(true);
}
}
};
loadImageBlob();
return () => {
canceled = true;
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [imageUrl]);
return (
<>
{/*content栏-api部分*/}
@ -290,31 +178,14 @@ const NodeContent = ({ data, imageUrl = '' }: { data: NodeContentData; imageUrl?
</>
)}
{imageUrl && (
<div className={styles['node-image-box']}>
{previewUrl ? (
<Image
width={150}
src={previewUrl}
alt="图片展示"
style={{ display: 'block' }}
onError={() => setImageLoadError(true)}
/>
) : (
<div className={styles['node-image-loading']}></div>
)}
{imageLoadError && (
<a
className={styles['node-image-download']}
href={imageUrl}
target="_blank"
rel="noreferrer"
>
</a>
)}
</div>
)}
{/*图片展示 TODO 需要对接接口*/}
{/*<div className={styles['node-image-box']}>*/}
{/* <Image*/}
{/* width={150}*/}
{/* src=""*/}
{/* alt="lamp"*/}
{/* />*/}
{/*</div>*/}
{/* 根据节点类型渲染不同的句柄 */}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}

@ -9,7 +9,6 @@ interface NodeContextMenuProps {
onRename?: (node: Node) => void;
onDelete?: (node: Node) => void;
onCopy?: (node: Node) => void;
copyLabel?: string;
onEdit?: (node: Node) => void;
onCloseMenu?: (data: React.Dispatch<React.SetStateAction<any>>) => void;
onCloseOpenModal?: (boolean: boolean) => void;
@ -20,7 +19,6 @@ const NodeContextMenu: React.FC<NodeContextMenuProps> = ({
node,
onDelete,
onCopy,
copyLabel = '复制节点',
onEdit,
onCloseMenu,
onCloseOpenModal,
@ -106,7 +104,7 @@ const NodeContextMenu: React.FC<NodeContextMenuProps> = ({
menuItems.push(
<Menu.Item key="copy" onClick={handleCopy}>
{copyLabel}
</Menu.Item>
);
menuItems.push(

@ -6,15 +6,11 @@ const SubMenu = Menu.SubMenu;
interface PaneContextMenuProps {
onAddNode: (nodeType: string, position: { x: number; y: number }, node: any) => void;
onCopySelection?: () => void;
onPasteNode?: (position: { x: number; y: number }) => void;
position: { x: number; y: number };
}
const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
onAddNode,
onCopySelection,
onPasteNode,
position
}) => {
// 包装onAddNode函数以适配AddNodeMenu组件的接口
@ -24,8 +20,13 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
// 处理粘贴节点
const handlePasteNode = useCallback(() => {
onPasteNode && onPasteNode(position);
}, [onPasteNode, position]);
// 创建自定义粘贴事件
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: new DataTransfer()
});
pasteEvent.clipboardData?.setData('text', 'paste-node');
document.dispatchEvent(pasteEvent);
}, []);
// 检查是否有复制的节点数据
const hasCopiedNode = !!localStorage.getItem('copiedNode') || !!localStorage.getItem('copiedFlowData');
@ -40,11 +41,6 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
}}
triggerProps={{ trigger: 'click' }}
>
{onCopySelection && (
<Menu.Item key="copy-selection" onClick={onCopySelection}>
</Menu.Item>
)}
<SubMenu
key="add-node"
title="添加节点"

@ -84,7 +84,6 @@ const imageParameters = {
defaultValue: ''
}],
dataIns: [{
id: 'in',
name: 'in',
desc: 'url',
dataType: 'STRING',

@ -2,8 +2,7 @@
*
* */
import { convertAppFlowData } from '@/utils/convertAppFlowData';
import { convertFlowData } from '@/utils/convertFlowData';
import { convertAppFlowData } from '@/features/workflow/adapters/appFlowAdapter';
import { Edge } from '@xyflow/react';
import { updateCanvasDataMap } from '@/store/ideContainer';

@ -2,7 +2,7 @@
*
* */
// 组件编排画布数据处理(组件编排)
import { convertFlowData } from '@/utils/convertFlowData';
import { convertFlowData } from '@/features/workflow/adapters/serverFlowAdapter';
import { Edge } from '@xyflow/react';
import { updateCanvasDataMap } from '@/store/ideContainer';

@ -3,13 +3,12 @@ import { useSelector, useDispatch } from 'react-redux';
import {
updateSocketId,
updateNodeStatus,
updateEventListOld,
updateRuntimeId,
updateIsRunning
updateEventListOld
} from '@/store/ideContainer';
import useWebSocket from '@/hooks/useWebSocket';
import { isJSON, getUrlParams } from '@/utils/common';
import { buildRuntimeReconnectRequest } from '@/utils/runtimeReconnect';
import { buildRuntimeReconnectRequest } from '@/features/workflow/runtime/runtimeReconnect';
import { reconnectWorkflowRuntime } from '@/features/workflow/runtime/runtimeActions';
import styles from './style/index.module.less';
import SideBar from './sideBar';
@ -24,7 +23,7 @@ import {
updateEventList,
updateGlobalVarList
} from '@/store/ideContainer';
import { getAppListBySceneId, reconnectRun } from '@/api/apps';
import { getAppListBySceneId } from '@/api/apps';
import { getProjectComp } from '@/api/scene';
import ProjectContainer from '@/pages/orchestration/project';
@ -64,22 +63,6 @@ const ALL_PATHS = [
'systemResource', 'appGuide'
];
const getRuntimeNodeStatus = (state: any) => {
switch (state) {
case 0:
case '0':
return 'running';
case 1:
case '1':
return 'success';
case -1:
case '-1':
return 'failed';
default:
return '';
}
};
function IDEContainer() {
const [selected, setSelected] = useState<Selected>({});
const [urlParams, setUrlParams] = useState<UrlParamsOptions>({});
@ -110,12 +93,25 @@ function IDEContainer() {
// 处理节点状态更新
if (socketMessage?.nodeLog) {
const { nodeId, state, runLog, appId } = socketMessage.nodeLog;
const status = getRuntimeNodeStatus(state);
// 将状态映射为前端使用的状态
let status = 'waiting';
switch (state) {
case 0: // 运行中
status = 'running';
break;
case 1: // 运行成功
status = 'success';
break;
case -1: // 运行失败
status = 'failed';
break;
default:// 等待运行
status = 'waiting';
break;
}
// 更新节点状态使用特殊的actionType标记这是运行时状态更新
// 如果后端提供了 appId则传递给 action 以确保更新正确的应用状态
if (nodeId && status) {
dispatch(updateNodeStatus({ nodeId, status, appId, actionType: 'RUNTIME_UPDATE' }));
}
// 只有当存在runLog时才发送日志到logBar
if (runLog) {
@ -149,19 +145,15 @@ function IDEContainer() {
const reconnectRuntime = async () => {
try {
dispatch(updateRuntimeId(reconnectRequest.instanceId));
dispatch(updateIsRunning(true));
const res: any = await reconnectRun({
const reconnectResult = await reconnectWorkflowRuntime({
instanceId: reconnectRequest.instanceId,
newSocketId: reconnectRequest.newSocketId
newSocketId: reconnectRequest.newSocketId,
dispatch
});
const reconnectResult = res?.data || res;
if (!canceled && reconnectResult?.success === false) {
lastReconnectKeyRef.current = '';
Message.error(reconnectResult.message || '运行实例重连失败');
return;
}
} catch (error) {
if (!canceled) {

@ -1,11 +1,11 @@
import React, { useState, useRef, useEffect } from 'react';
import { ResizeBox, Tabs } from '@arco-design/web-react';
import styles from './style/logBar.module.less';
import { updateLogBarStatus, updateRuntimeNodeData } from '@/store/ideContainer';
import { updateLogBarStatus } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
import { getNodeData } from '@/api/appIns';
import RunTimeData from './components/runTimeData';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getCurrentAppKey } from '@/features/workflow/runtime/flowRuntime';
const TabPane = Tabs.TabPane;
@ -165,32 +165,27 @@ const LogBar: React.FC<LogBarProps> = () => {
// 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey(currentAppData);
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning;
const currentRunId = currentAppKey ? appRuntimeData[currentAppKey]?.runId : '';
const isRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning;
// 实现轮询获取运行数据 - 只在应用运行时轮询
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
const appKey = currentAppKey;
const appKey = getCurrentAppKey(currentAppData);
// 只有在应用正在运行且有 runId 时才开始轮询
if (
appKey &&
currentAppIsRunning &&
currentRunId
appRuntimeData[appKey]?.isRunning &&
appRuntimeData[appKey]?.runId
) {
const fetchRuntimeData = async () => {
try {
setLoading(true);
const response = await getNodeData(currentRunId);
const response = await getNodeData(appRuntimeData[appKey].runId);
setRuntimeData((prev) => ({
...prev,
[appKey]: response.data,
}));
dispatch(updateRuntimeNodeData({
appId: appKey,
nodeData: response.data
}));
} catch (error) {
console.error('获取运行数据失败:', error);
} finally {
@ -211,12 +206,12 @@ const LogBar: React.FC<LogBarProps> = () => {
clearInterval(intervalId);
}
};
}, [currentAppKey, currentAppIsRunning, currentRunId, dispatch]);
}, [currentAppData, appRuntimeData]);
// 当应用停止运行时,清除运行数据
useEffect(() => {
const appKey = currentAppKey;
if (appKey && !currentAppIsRunning) {
const appKey = getCurrentAppKey(currentAppData);
if (appKey && !appRuntimeData[appKey]?.isRunning) {
// 清除当前应用的运行数据
setRuntimeData((prev) => {
const newData = { ...prev };
@ -224,7 +219,7 @@ const LogBar: React.FC<LogBarProps> = () => {
return newData;
});
}
}, [currentAppKey, currentAppIsRunning]);
}, [isRunning, currentAppData]);
// 渲染校验日志内容
const renderValidationLogs = () => {

@ -46,7 +46,7 @@ import _ from 'lodash';
import { getAppInfoNew } from '@/api/appRes';
import { getAppEventData } from '@/api/appEvent';
import { queryEventItemBySceneIdOld } from '@/api/event';
import { convertFlowData } from '@/utils/convertFlowData';
import { convertFlowData } from '@/features/workflow/adapters/serverFlowAdapter';
const TreeNode = Tree.Node;
const FormItem = Form.Item;

@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
import {
createDefaultAppRuntimeState,
getCurrentAppKey,
} from '@/utils/flow/runtime';
} from '@/features/workflow/runtime/flowRuntime';
// 定义初始状态类型
interface IDEContainerState {
@ -28,7 +28,6 @@ interface IDEContainerState {
isPaused: boolean;
logs: any[];
runId: string;
nodeData: any[];
eventSendNodeList: any[]; // [{nodeID:topic}]
eventlisteneList: any[]; // [{nodeID:topic}]
}
@ -213,19 +212,6 @@ const ideContainerSlice = createSlice({
state.appRuntimeData[appId].logs = [];
}
},
// 更新运行节点数据
updateRuntimeNodeData: (state, { payload }) => {
const { appId, nodeData } = payload;
if (!appId) {
return;
}
if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appId].nodeData = Array.isArray(nodeData)
? nodeData
: [];
},
// 更新组件编码路径
updateComponentCodingPath(state, action) {
state.componentCoding = { ...action.payload };
@ -255,7 +241,6 @@ export const {
updateEventNodeList,
addRuntimeLog,
clearRuntimeLogs,
updateRuntimeNodeData,
updateComponentCodingPath,
} = ideContainerSlice.actions;

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

File diff suppressed because it is too large Load Diff

@ -1,48 +0,0 @@
function hasObjectValues(value) {
return Boolean(value && typeof value === 'object' && Object.keys(value).length > 0);
}
function hasInitialCanvasData(initialData, useDefault) {
if (!initialData) {
return false;
}
if (Array.isArray(initialData)) {
return initialData.length > 0;
}
if (useDefault) {
return hasObjectValues(initialData?.main?.components) || hasObjectValues(initialData?.components);
}
return hasObjectValues(initialData);
}
function hasCanvasContent(canvas) {
return Boolean(
canvas &&
((Array.isArray(canvas.nodes) && canvas.nodes.length > 0) ||
(Array.isArray(canvas.edges) && canvas.edges.length > 0))
);
}
function shouldUseCachedCanvas({ cachedCanvas, initialData, useDefault }) {
if (!cachedCanvas) {
return false;
}
if (hasCanvasContent(cachedCanvas)) {
return true;
}
return !hasInitialCanvasData(initialData, useDefault);
}
function shouldPersistCanvas({ nodes, edges }) {
return hasCanvasContent({ nodes, edges });
}
module.exports = {
shouldUseCachedCanvas,
shouldPersistCanvas,
};

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

@ -1,40 +0,0 @@
export interface RuntimeNodeData {
nodeId?: string;
input?: Record<string, any>;
[key: string]: any;
}
const isRuntimeNode = (item: any): item is RuntimeNodeData => {
return !!item && typeof item === 'object' && typeof item.nodeId === 'string';
};
export const flattenRuntimeNodeData = (runtimeData: any): RuntimeNodeData[] => {
if (!Array.isArray(runtimeData)) {
return [];
}
return runtimeData.reduce<RuntimeNodeData[]>((nodes, item) => {
if (isRuntimeNode(item)) {
nodes.push(item);
return nodes;
}
if (Array.isArray(item?.nodes)) {
nodes.push(...item.nodes.filter(isRuntimeNode));
}
return nodes;
}, []);
};
export const getRuntimeImageUrl = (
runtimeData: any,
nodeId: string
): string => {
const nodeData = flattenRuntimeNodeData(runtimeData).find(
(item) => item.nodeId === nodeId
);
const imageUrl = nodeData?.input?.in;
return typeof imageUrl === 'string' ? imageUrl.trim() : '';
};

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

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

@ -1,61 +1,5 @@
import {
createFlowNode,
attachFlowNodeComponent,
} from '@/utils/flow/nodeFactory';
import {
ensureNodeTypeRegistered,
resolveNodeComponent,
} from '@/utils/flow/nodeRegistry';
type EventIdMode = 'id' | 'eventIdOptional';
export const resolveNodeDefinition = (
nodeList: any[],
nodeType: string,
fallbackNode?: any
) => {
return nodeList.find((n) => n.nodeType === nodeType) || fallbackNode || null;
};
export const buildRuntimeNode = (params: {
nodeType: string;
nodeDefinition: any;
position: { x: number; y: number };
eventList: any[];
eventIdMode: EventIdMode;
}) => {
const { nodeType, nodeDefinition, position, eventList, eventIdMode } = params;
const newNode = attachFlowNodeComponent(
createFlowNode(nodeType, nodeDefinition, position),
nodeType,
nodeDefinition,
eventList,
eventIdMode
);
ensureNodeTypeRegistered(
nodeType,
nodeDefinition.nodeName,
resolveNodeComponent(nodeType)
);
return newNode;
};
export const buildDroppedNode = (
nodeData: any,
position: { x: number; y: number }
) => {
ensureNodeTypeRegistered(nodeData.nodeType, nodeData.nodeName);
return {
id: `${nodeData.nodeType}-${Date.now()}`,
type: nodeData.nodeType,
position,
data: {
...nodeData.data,
title: nodeData.nodeName,
type: nodeData.nodeType,
},
};
};
export {
buildDroppedNode,
buildRuntimeNode,
resolveNodeDefinition,
} from '@/features/workflow/operations/nodeOnboarding';

@ -1,40 +1,4 @@
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import SwitchNode from '@/components/FlowEditor/node/switchNode/SwitchNode';
import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode';
import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode';
import RestNode from '@/components/FlowEditor/node/restNode/RestNode';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
export const resolveNodeComponent = (nodeType: string) => {
switch (nodeType) {
case 'BASIC':
case 'SUB':
return BasicNode;
case 'SWITCH':
return SwitchNode;
case 'IMAGE':
return ImageNode;
case 'CODE':
return CodeNode;
case 'REST':
return RestNode;
default:
return LocalNode;
}
};
export const ensureNodeTypeRegistered = (
nodeType: string,
nodeName: string,
component?: any
) => {
const nodeMap = Array.from(Object.values(nodeTypeMap).map((key) => key));
if (!nodeMap.includes(nodeType)) {
registerNodeType(
nodeType,
component || resolveNodeComponent(nodeType),
nodeName
);
}
};
export {
ensureNodeTypeRegistered,
resolveNodeComponent,
} from '@/features/workflow/registry/nodeRegistry';

@ -1,36 +1,8 @@
export interface FlowCurrentAppData {
id?: string;
key?: string;
}
export interface AppRuntimeState {
nodeStatusMap: Record<string, string>;
isRunning: boolean;
isPaused: boolean;
logs: any[];
runId: string;
nodeData: any[];
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: '',
nodeData: [],
eventSendNodeList: [],
eventlisteneList: [],
});
export {
createDefaultAppRuntimeState,
getCurrentAppKey,
} from '@/features/workflow/runtime/flowRuntime';
export type {
AppRuntimeState,
FlowCurrentAppData,
} from '@/features/workflow/runtime/flowRuntime';

@ -1,22 +1,4 @@
import { Edge, Node } from '@xyflow/react';
interface FlowSnapshotDetail {
nodes: Node[];
edges: Edge[];
}
export const dispatchFlowSnapshot = (detail: FlowSnapshotDetail) => {
const event = new CustomEvent('takeSnapshot', {
detail,
});
document.dispatchEvent(event);
};
export const dispatchFlowSnapshotAsync = (
detail: FlowSnapshotDetail,
delay = 0
) => {
return setTimeout(() => {
dispatchFlowSnapshot(detail);
}, delay);
};
export {
dispatchFlowSnapshot,
dispatchFlowSnapshotAsync,
} from '@/features/workflow/operations/snapshot';

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

@ -1,28 +0,0 @@
function isScheduledRunning(app) {
return app && (app.scheduled === 1 || app.scheduled === '1' || app.scheduled === true);
}
function buildRuntimeReconnectRequest({ app, socketId, lastReconnectKey }) {
const instanceId = app && app.instanceId ? String(app.instanceId) : '';
const newSocketId = socketId ? String(socketId) : '';
const appKey = app && (app.id || app.key) ? String(app.id || app.key) : '';
if (!isScheduledRunning(app) || !instanceId || !newSocketId || !appKey) {
return null;
}
const reconnectKey = `${appKey}:${instanceId}:${newSocketId}`;
if (reconnectKey === lastReconnectKey) {
return null;
}
return {
instanceId,
newSocketId,
reconnectKey,
};
}
module.exports = {
buildRuntimeReconnectRequest,
};
Loading…
Cancel
Save