From 464b92da32325f322b5258b8e53fa4f1b342505e Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 30 Jan 2026 01:30:57 +0800 Subject: [PATCH] fix(workflow): eliminate infinite loop in plugin install state management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace useEffect-based state sync (_pluginInstallLocked/_dimmed) with render-time derived computation in BaseNode, breaking the cycle of effect → node data update → re-render → effect. Extract plugin missing check into a pure utility function for checklist reuse. --- .../workflow/hooks/use-checklist.ts | 14 ++-- .../workflow/hooks/use-nodes-interactions.ts | 2 - .../nodes/_base/components/node-control.tsx | 7 +- .../components/workflow/nodes/_base/node.tsx | 12 +++- .../workflow/nodes/data-source/node.tsx | 19 +----- .../components/workflow/nodes/tool/node.tsx | 18 +---- .../workflow/nodes/trigger-plugin/node.tsx | 17 +---- web/app/components/workflow/types.ts | 1 - .../workflow/utils/plugin-install-check.ts | 67 +++++++++++++++++++ 9 files changed, 89 insertions(+), 68 deletions(-) create mode 100644 web/app/components/workflow/utils/plugin-install-check.ts diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index f726fab6f7..029257a4de 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -33,6 +33,7 @@ import { useStrategyProviders } from '@/service/use-strategy' import { useAllBuiltInTools, useAllCustomTools, + useAllMCPTools, useAllWorkflowTools, } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' @@ -56,6 +57,7 @@ import { getToolCheckParams, getValidTreeNodes, } from '../utils' +import { isNodePluginMissing } from '../utils/plugin-install-check' import { getTriggerCheckParams } from '../utils/trigger' import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list' @@ -77,13 +79,6 @@ const START_NODE_TYPES: BlockEnum[] = [ BlockEnum.TriggerPlugin, ] -// Node types that depend on plugins -const PLUGIN_DEPENDENT_TYPES: BlockEnum[] = [ - BlockEnum.Tool, - BlockEnum.DataSource, - BlockEnum.TriggerPlugin, -] - export const useChecklist = (nodes: Node[], edges: Edge[]) => { const { t } = useTranslation() const language = useGetLanguage() @@ -91,6 +86,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() const dataSourceList = useStore(s => s.dataSourceList) const { data: strategyProviders } = useStrategyProviders() const { data: triggerPlugins } = useAllTriggerPlugins() @@ -166,7 +162,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { if (node.type === CUSTOM_NODE) { const checkData = getCheckData(node.data) const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid - const isPluginMissing = PLUGIN_DEPENDENT_TYPES.includes(node.data.type as BlockEnum) && node.data._pluginInstallLocked + const isPluginMissing = isNodePluginMissing(node.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList }) // Check if plugin is installed for plugin-dependent nodes first let errorMessage: string | undefined @@ -250,7 +246,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { }) return list - }, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map, shouldCheckStartNode]) + }, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, mcpTools, language, dataSourceList, getToolIcon, strategyProviders, triggerPlugins, getCheckData, t, map, shouldCheckStartNode]) return needWarningNodes } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 84b3ec26ef..d73d4e2c38 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -591,8 +591,6 @@ export const useNodesInteractions = () => { return if (node.data.type === BlockEnum.DataSourceEmpty) return - if (node.data._pluginInstallLocked) - return handleNodeSelect(node.id) }, [handleNodeSelect, workflowStore], diff --git a/web/app/components/workflow/nodes/_base/components/node-control.tsx b/web/app/components/workflow/nodes/_base/components/node-control.tsx index 610ec6d2a9..67cd6c9fa5 100644 --- a/web/app/components/workflow/nodes/_base/components/node-control.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-control.tsx @@ -22,10 +22,13 @@ import { NodeRunningStatus } from '../../../types' import { canRunBySingle } from '../../../utils' import PanelOperator from './panel-operator' -type NodeControlProps = Pick +type NodeControlProps = Pick & { + pluginInstallLocked?: boolean +} const NodeControl: FC = ({ id, data, + pluginInstallLocked, }) => { const { t } = useTranslation() const [open, setOpen] = useState(false) @@ -47,7 +50,7 @@ const NodeControl: FC = ({