Compare commits

...

20 Commits

@ -332,6 +332,7 @@ 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}个输入参数的标识不能为空`);

@ -63,6 +63,10 @@ import {
buildRuntimeNode,
resolveNodeDefinition,
} from '@/utils/flow/nodeOnboarding';
import {
shouldPersistCanvas,
shouldUseCachedCanvas,
} from '@/utils/flow/canvasCache';
import { Dispatch } from 'redux';
import {
@ -125,6 +129,10 @@ export const useFlowCallbacks = (
return getCurrentAppKey(currentAppData) || initialData?.appId;
}, [initialData]);
const refreshAppList = useCallback(() => {
document.dispatchEvent(new CustomEvent('refreshAppList'));
}, []);
// region 画布操作
// 节点变更处理,添加防抖机制
const onNodesChange = useCallback(
@ -519,8 +527,9 @@ export const useFlowCallbacks = (
// 初始化画布数据
const initializeCanvasData = useCallback(() => {
const appKey = getCurrentFlowAppKey();
if (appKey && canvasDataMap[appKey]) {
const { edges, nodes } = canvasDataMap[appKey];
const cachedCanvas = appKey ? canvasDataMap[appKey] : null;
if (shouldUseCachedCanvas({ cachedCanvas, initialData, useDefault })) {
const { edges, nodes } = cachedCanvas;
setNodes(nodes);
setEdges(edges);
} else {
@ -539,11 +548,19 @@ export const useFlowCallbacks = (
// 标记历史记录已初始化
setHistoryInitialized(true);
}, [initialData, canvasDataMap, getCurrentFlowAppKey]);
}, [initialData, useDefault, canvasDataMap, getCurrentFlowAppKey]);
// 实时更新 canvasDataMap
const updateCanvasDataMapEffect = useCallback(() => {
const appKey = getCurrentFlowAppKey();
if (appKey) {
const { appRuntimeData } = store.getState().ideContainer;
const isCurrentAppRunning =
appKey && appRuntimeData[appKey]?.isRunning;
if (
appKey &&
!isCurrentAppRunning &&
shouldPersistCanvas({ nodes, edges })
) {
updateCanvasDataMapDebounced(
dispatch,
canvasDataMap,
@ -557,7 +574,13 @@ export const useFlowCallbacks = (
return () => {
// 取消防抖函数
};
}, [nodes, edges, dispatch, canvasDataMap, getCurrentFlowAppKey]);
}, [
nodes,
edges,
dispatch,
canvasDataMap,
getCurrentFlowAppKey,
]);
// 关闭编辑弹窗
const closeEditModal = useCallback(() => {
setIsEditModalOpen(false);
@ -1387,6 +1410,7 @@ export const useFlowCallbacks = (
// 更新运行ID
dispatch(updateRuntimeId(res.data));
refreshAppList();
// 开始运行时动画
setEdges((eds) =>
@ -1420,6 +1444,7 @@ export const useFlowCallbacks = (
// 更新运行ID
dispatch(updateRuntimeId(res.data));
refreshAppList();
// 开始运行时动画
setEdges((eds) =>
@ -1448,9 +1473,8 @@ export const useFlowCallbacks = (
} else {
// 特殊停止逻辑,持久化运行的应用使用这里的入参
await stopApp(currentAppData.instanceId);
// 特殊停止完成后触发事件,通知刷新应用列表
document.dispatchEvent(new CustomEvent('refreshAppList'));
}
refreshAppList();
// 重置节点状态
dispatch(resetNodeStatus());
@ -1476,7 +1500,7 @@ export const useFlowCallbacks = (
}
}
},
[getCurrentFlowAppKey]
[getCurrentFlowAppKey, refreshAppList]
);
// 暂停/恢复应用

@ -2,11 +2,58 @@ 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 } from '@/store/ideContainer';
import { updateCanvasDataMap, updateNodeStatus } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getNodeData } from '@/api/appIns';
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[]>([]);
@ -46,6 +93,11 @@ 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(() => {
@ -61,7 +113,9 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
Object.values(initialData.components).some((comp: any) => comp.status);
setNodes((prevNodes) => {
return prevNodes.map((node) => {
let hasChanges = false;
const nextNodes = prevNodes.map((node) => {
// 如果是只读模式(历史实例查看),优先使用节点自身的历史状态
// 如果是正常运行模式,只使用运行时状态
let nodeStatus = 'waiting';
@ -80,6 +134,14 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
showStatus = currentAppIsRunning;
}
if (
node.data.status === nodeStatus &&
node.data.isStatusVisible === showStatus
) {
return node;
}
hasChanges = true;
return {
...node,
data: {
@ -89,6 +151,8 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
},
};
});
return hasChanges ? nextNodes : prevNodes;
});
}, [
appRuntimeData,
@ -100,6 +164,70 @@ 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(
(

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

@ -1,8 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { updateSocketId, updateNodeStatus, updateEventListOld } from '@/store/ideContainer';
import {
updateSocketId,
updateNodeStatus,
updateEventListOld,
updateRuntimeId,
updateIsRunning
} from '@/store/ideContainer';
import useWebSocket from '@/hooks/useWebSocket';
import { isJSON, getUrlParams } from '@/utils/common';
import { buildRuntimeReconnectRequest } from '@/utils/runtimeReconnect';
import styles from './style/index.module.less';
import SideBar from './sideBar';
@ -17,7 +24,7 @@ import {
updateEventList,
updateGlobalVarList
} from '@/store/ideContainer';
import { getAppListBySceneId } from '@/api/apps';
import { getAppListBySceneId, reconnectRun } from '@/api/apps';
import { getProjectComp } from '@/api/scene';
import ProjectContainer from '@/pages/orchestration/project';
@ -57,6 +64,22 @@ 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>({});
@ -64,9 +87,10 @@ function IDEContainer() {
// 用于跟踪已打开的tab保持组件状态
const [openedTabs, setOpenedTabs] = useState<Set<string>>(new Set());
const [subMenuData, setSubMenuData] = useState<any>({});
const { menuData, flowData, currentAppData } = useSelector((state) => state.ideContainer);
const { menuData, flowData, currentAppData, socketId } = useSelector((state) => state.ideContainer);
const dispatch = useDispatch();
const navBarRef = useRef<NavBarRef>(null);
const lastReconnectKeyRef = useRef('');
// 初始化WebSocket hook
const ws = useWebSocket({
@ -86,25 +110,12 @@ function IDEContainer() {
// 处理节点状态更新
if (socketMessage?.nodeLog) {
const { nodeId, state, runLog, appId } = socketMessage.nodeLog;
// 将状态映射为前端使用的状态
let status = 'waiting';
switch (state) {
case 0: // 运行中
status = 'running';
break;
case 1: // 运行成功
status = 'success';
break;
case -1: // 运行失败
status = 'failed';
break;
default:// 等待运行
status = 'waiting';
break;
}
const status = getRuntimeNodeStatus(state);
// 更新节点状态使用特殊的actionType标记这是运行时状态更新
// 如果后端提供了 appId则传递给 action 以确保更新正确的应用状态
dispatch(updateNodeStatus({ nodeId, status, appId, actionType: 'RUNTIME_UPDATE' }));
if (nodeId && status) {
dispatch(updateNodeStatus({ nodeId, status, appId, actionType: 'RUNTIME_UPDATE' }));
}
// 只有当存在runLog时才发送日志到logBar
if (runLog) {
@ -122,6 +133,52 @@ function IDEContainer() {
}
});
useEffect(() => {
const reconnectRequest = buildRuntimeReconnectRequest({
app: currentAppData,
socketId,
lastReconnectKey: lastReconnectKeyRef.current
});
if (!reconnectRequest) {
return;
}
let canceled = false;
lastReconnectKeyRef.current = reconnectRequest.reconnectKey;
const reconnectRuntime = async () => {
try {
dispatch(updateRuntimeId(reconnectRequest.instanceId));
dispatch(updateIsRunning(true));
const res: any = await reconnectRun({
instanceId: reconnectRequest.instanceId,
newSocketId: reconnectRequest.newSocketId
});
const reconnectResult = res?.data || res;
if (!canceled && reconnectResult?.success === false) {
lastReconnectKeyRef.current = '';
Message.error(reconnectResult.message || '运行实例重连失败');
return;
}
} catch (error) {
if (!canceled) {
lastReconnectKeyRef.current = '';
console.error('运行实例重连失败:', error);
Message.error('运行实例重连失败');
}
}
};
reconnectRuntime();
return () => {
canceled = true;
};
}, [currentAppData, socketId, dispatch]);
// 监听自定义事件,处理打开子节点标签页的逻辑
useEffect(() => {
const handleOpenSubNodeTab = async (event: CustomEvent) => {

@ -4,6 +4,28 @@ import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import { updateEventNodeList } from '@/store/ideContainer';
import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
const runtimeToEditorNodeTypeMap: Record<string, string> = {
SHOW_IMAGE: 'IMAGE',
SHOW_RESULT: 'RESULT',
JSON_CONVERT: 'JSONCONVERT',
};
const editorToRuntimeNodeTypeMap: Record<string, string> = {
IMAGE: 'SHOW_IMAGE',
RESULT: 'SHOW_RESULT',
JSONCONVERT: 'JSON_CONVERT',
};
export const toEditorNodeType = (type?: string) => {
if (!type) return type;
return runtimeToEditorNodeTypeMap[type] || type;
};
export const toRuntimeComponentType = (type?: string) => {
if (!type) return type;
return editorToRuntimeNodeTypeMap[type] || type;
};
/**
* flow editor nodes edges
* @param flowData -
@ -117,17 +139,18 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
// 确定节点类型
let nodeType = 'BASIC';
const componentType = toEditorNodeType(nodeConfig.component?.type);
if (nodeId.includes('start')) {
nodeType = 'start';
} else if (nodeId.includes('end')) {
nodeType = 'end';
} else if (
nodeConfig.component?.type === 'LOOP_START' ||
nodeConfig.component?.type === 'LOOP_END'
componentType === 'LOOP_START' ||
componentType === 'LOOP_END'
) {
nodeType = 'LOOP';
} else {
nodeType = nodeConfig.component?.type || 'BASIC';
nodeType = componentType || 'BASIC';
}
// 解析位置信息
const position = nodeConfig.position || { x: 0, y: 0 };
@ -145,13 +168,13 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
dataIns: getNodeDataIns(nodeConfig),
dataOuts: nodeConfig.dataOuts || [],
},
type: nodeConfig.component?.type || nodeType,
type: componentType || nodeType,
},
};
// 添加组件标识信息
if (nodeConfig.component) {
node.data.component = { ...nodeConfig.component };
node.data.component = { ...nodeConfig.component, type: componentType };
node.data.compId = nodeConfig.component.compId;
}
@ -470,7 +493,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
// 处理 component 信息
if (node.data?.component) {
nodeConfig.component = {
type: nodeType,
type: toRuntimeComponentType(nodeType),
compIdentifier: node.data.component.compIdentifier || '',
compInstanceIdentifier:
node.data.component.compInstanceIdentifier || '',
@ -481,7 +504,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
} else if (nodeType !== 'start' && nodeType !== 'end') {
// 对于非 start/end 节点,添加基本的 component 信息
nodeConfig.component = {
type: nodeType,
type: toRuntimeComponentType(nodeType),
};
}
if (['BASIC', 'SUB'].includes(nodeType))
@ -624,10 +647,13 @@ export const reverseConvertFlowData = (
}),
};
} else if (node.data?.component) {
nodeConfig.component = { ...node.data.component };
nodeConfig.component = {
...node.data.component,
type: toRuntimeComponentType(node.data.component.type || node.type),
};
} else {
nodeConfig.component = {
type: node.type,
type: toRuntimeComponentType(node.type),
};
}

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

@ -0,0 +1,28 @@
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