diff --git a/src/features/workflow/operations/canvasCache.ts b/src/features/workflow/operations/canvasCache.ts new file mode 100644 index 0000000..2f681ab --- /dev/null +++ b/src/features/workflow/operations/canvasCache.ts @@ -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); +}; diff --git a/src/features/workflow/runtime/runtimeActions.ts b/src/features/workflow/runtime/runtimeActions.ts index f1b8efc..592aaa7 100644 --- a/src/features/workflow/runtime/runtimeActions.ts +++ b/src/features/workflow/runtime/runtimeActions.ts @@ -5,6 +5,7 @@ import { Dispatch } from 'redux'; import { pauseApp, reRunApp, + reconnectRun, resumeApp, runMainFlow, runSubFlow, @@ -181,3 +182,21 @@ export const rerunWorkflow = async ({ Message.error('重跑失败'); } }; + +export const reconnectWorkflowRuntime = async (params: { + instanceId: string; + newSocketId: string; + dispatch: Dispatch; +}) => { + const { instanceId, newSocketId, dispatch } = params; + + dispatch(updateRuntimeId(instanceId)); + dispatch(updateIsRunning(true)); + + const res: any = await reconnectRun({ + instanceId, + newSocketId, + }); + + return res?.data || res; +}; diff --git a/src/features/workflow/runtime/runtimeReconnect.ts b/src/features/workflow/runtime/runtimeReconnect.ts new file mode 100644 index 0000000..4197d38 --- /dev/null +++ b/src/features/workflow/runtime/runtimeReconnect.ts @@ -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, + }; +}; diff --git a/src/hooks/useFlowCallbacks.ts b/src/hooks/useFlowCallbacks.ts index f0baefd..7c1c97f 100644 --- a/src/hooks/useFlowCallbacks.ts +++ b/src/hooks/useFlowCallbacks.ts @@ -46,6 +46,10 @@ import { removeNodesAndConnectedEdges, resolveDeletedNodesWithLoopPairs, } from '@/features/workflow/operations/deleteOperations'; +import { + shouldPersistCanvas, + shouldUseCachedCanvas, +} from '@/features/workflow/operations/canvasCache'; import { buildRuntimeNode, resolveNodeDefinition, @@ -323,8 +327,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 { @@ -343,11 +348,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 = Boolean( + appKey && appRuntimeData[appKey]?.isRunning + ); + + if ( + appKey && + shouldPersistCanvas({ nodes, edges, isRunning: isCurrentAppRunning }) + ) { updateCanvasDataMapDebounced( dispatch, canvasDataMap, diff --git a/src/hooks/useFlowEditorState.ts b/src/hooks/useFlowEditorState.ts index 5df7398..6610b65 100644 --- a/src/hooks/useFlowEditorState.ts +++ b/src/hooks/useFlowEditorState.ts @@ -61,7 +61,9 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => { Object.values(initialData.components).some((comp: any) => comp.status); setNodes((prevNodes) => { - return prevNodes.map((node) => { + let hasChanges = false; + + const nextNodes = prevNodes.map((node) => { // 如果是只读模式(历史实例查看),优先使用节点自身的历史状态 // 如果是正常运行模式,只使用运行时状态 let nodeStatus = 'waiting'; @@ -80,6 +82,14 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => { showStatus = currentAppIsRunning; } + if ( + node.data.status === nodeStatus && + node.data.isStatusVisible === showStatus + ) { + return node; + } + + hasChanges = true; return { ...node, data: { @@ -89,6 +99,8 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => { }, }; }); + + return hasChanges ? nextNodes : prevNodes; }); }, [ appRuntimeData, diff --git a/src/pages/ideContainer/index.tsx b/src/pages/ideContainer/index.tsx index a1c8cf6..434778f 100644 --- a/src/pages/ideContainer/index.tsx +++ b/src/pages/ideContainer/index.tsx @@ -1,8 +1,14 @@ import React, { useState, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { updateSocketId, updateNodeStatus, updateEventListOld } from '@/store/ideContainer'; +import { + updateSocketId, + updateNodeStatus, + updateEventListOld +} from '@/store/ideContainer'; import useWebSocket from '@/hooks/useWebSocket'; import { isJSON, getUrlParams } from '@/utils/common'; +import { buildRuntimeReconnectRequest } from '@/features/workflow/runtime/runtimeReconnect'; +import { reconnectWorkflowRuntime } from '@/features/workflow/runtime/runtimeActions'; import styles from './style/index.module.less'; import SideBar from './sideBar'; @@ -64,9 +70,10 @@ function IDEContainer() { // 用于跟踪已打开的tab,保持组件状态 const [openedTabs, setOpenedTabs] = useState>(new Set()); const [subMenuData, setSubMenuData] = useState({}); - const { menuData, flowData, currentAppData } = useSelector((state) => state.ideContainer); + const { menuData, flowData, currentAppData, socketId } = useSelector((state) => state.ideContainer); const dispatch = useDispatch(); const navBarRef = useRef(null); + const lastReconnectKeyRef = useRef(''); // 初始化WebSocket hook const ws = useWebSocket({ @@ -122,6 +129,48 @@ function IDEContainer() { } }); + useEffect(() => { + const reconnectRequest = buildRuntimeReconnectRequest({ + app: currentAppData, + socketId, + lastReconnectKey: lastReconnectKeyRef.current + }); + + if (!reconnectRequest) { + return; + } + + let canceled = false; + lastReconnectKeyRef.current = reconnectRequest.reconnectKey; + + const reconnectRuntime = async () => { + try { + const reconnectResult = await reconnectWorkflowRuntime({ + instanceId: reconnectRequest.instanceId, + newSocketId: reconnectRequest.newSocketId, + dispatch + }); + + if (!canceled && reconnectResult?.success === false) { + lastReconnectKeyRef.current = ''; + Message.error(reconnectResult.message || '运行实例重连失败'); + } + } catch (error) { + if (!canceled) { + lastReconnectKeyRef.current = ''; + console.error('运行实例重连失败:', error); + Message.error('运行实例重连失败'); + } + } + }; + + reconnectRuntime(); + + return () => { + canceled = true; + }; + }, [currentAppData, socketId, dispatch]); + // 监听自定义事件,处理打开子节点标签页的逻辑 useEffect(() => { const handleOpenSubNodeTab = async (event: CustomEvent) => { @@ -500,4 +549,4 @@ function IDEContainer() { ); } -export default IDEContainer; \ No newline at end of file +export default IDEContainer;