From 2aa6dcaa1a40a81595dee6e3f31af95f04b97817 Mon Sep 17 00:00:00 2001 From: zhsama Date: Wed, 28 Jan 2026 21:22:32 +0800 Subject: [PATCH] feat: Improve error messages for missing workflow outputs --- .../last-run/sub-graph-variables-check.ts | 31 +++++---- .../workflow-panel/last-run/use-last-run.ts | 64 +++++++++++++++++-- web/i18n/en-US/workflow.json | 5 +- web/i18n/ja-JP/workflow.json | 5 +- web/i18n/zh-Hans/workflow.json | 5 +- web/i18n/zh-Hant/workflow.json | 5 +- 6 files changed, 84 insertions(+), 31 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/sub-graph-variables-check.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/sub-graph-variables-check.ts index d79f58b9b4..c1aa175db6 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/sub-graph-variables-check.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/sub-graph-variables-check.ts @@ -73,33 +73,32 @@ export const useSubGraphVariablesCheck = ({ } }, [currentNodeId, nodesWithInspectVars]) - const hasNullDependentOutputs = useCallback((vars?: ValueSelector[] | ValueSelector[][]) => { + const getNullDependentOutput = useCallback((vars?: ValueSelector[] | ValueSelector[][]) => { if (!vars || vars.length === 0) - return false + return undefined const isGroupedVars = Array.isArray(vars[0]) && Array.isArray((vars as ValueSelector[][])[0][0]) const selectors = isGroupedVars ? (vars as ValueSelector[][]).flat() : (vars as ValueSelector[]) const subGraphNodeIdSet = new Set(subGraphNodeIds) - const details = selectors.map((selector) => { + for (const selector of selectors) { const { found, value } = getInspectVarValueBySelector(selector) const valueType = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value const isSubgraphOutput = subGraphNodeIdSet.has(selector[0]) - return { - selector, - found, - valueType, - isSubgraphOutput, - } - }) - const hasNull = details.some((item) => { - if (!item.found) - return item.isSubgraphOutput - return item.valueType === 'null' || item.valueType === 'undefined' - }) - return hasNull + const isNull = !found + ? isSubgraphOutput + : valueType === 'null' || valueType === 'undefined' + if (isNull) + return selector + } + return undefined }, [getInspectVarValueBySelector, subGraphNodeIds]) + const hasNullDependentOutputs = useCallback((vars?: ValueSelector[] | ValueSelector[][]) => { + return !!getNullDependentOutput(vars) + }, [getNullDependentOutput]) + return { hasNullDependentOutputs, + getNullDependentOutput, } } diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index bc430a7779..cd516a0d11 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -5,6 +5,7 @@ import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useStoreApi } from 'reactflow' import Toast from '@/app/components/base/toast' import { useNodesSyncDraft, @@ -141,7 +142,7 @@ const useLastRun = ({ hasSetInspectVar, nodesWithInspectVars, } = useInspectVarsCrud() - const { hasNullDependentOutputs } = useSubGraphVariablesCheck({ + const { getNullDependentOutput } = useSubGraphVariablesCheck({ currentNodeId, nodesWithInspectVars, }) @@ -153,6 +154,7 @@ const useLastRun = ({ const isAggregatorNode = blockType === BlockEnum.VariableAggregator const isCustomRunNode = isSupportCustomRunForm(blockType) const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const reactFlowStore = useStoreApi() const { getData: getDataForCheckMore, } = useGetDataForCheckMoreHooks(blockType)(currentNodeId, oneStepRunParams.data) @@ -238,6 +240,7 @@ const useLastRun = ({ const workflowStore = useWorkflowStore() const { setInitShowLastRunTab, setShowVariableInspectPanel } = workflowStore.getState() const initShowLastRunTab = useStore(s => s.initShowLastRunTab) + const parentAvailableNodes = useStore(s => s.parentAvailableNodes) || [] const [tabType, setTabType] = useState(initShowLastRunTab ? TabType.lastRun : TabType.settings) useEffect(() => { if (initShowLastRunTab) @@ -247,6 +250,31 @@ const useLastRun = ({ }, [initShowLastRunTab]) const invalidLastRun = useInvalidLastRun(flowType, flowId, currentNodeId) + const getContextNodeLabel = useCallback((nodeId: string) => { + const nodeInFlow = reactFlowStore.getState().getNodes().find(node => node.id === nodeId) + const flowNodeTitle = nodeInFlow?.data?.title + if (flowNodeTitle && flowNodeTitle !== nodeId) + return flowNodeTitle + const parentNode = parentAvailableNodes.find(node => node.id === nodeId) + const parentNodeTitle = parentNode?.data?.title + if (parentNodeTitle && parentNodeTitle !== nodeId) + return parentNodeTitle + return '' + }, [parentAvailableNodes, reactFlowStore]) + + const formatSubgraphOutputLabel = useCallback((selector: ValueSelector) => { + const [nodeId, varName, ...restPath] = selector || [] + const nodeLabel = nodeId ? getContextNodeLabel(nodeId) : '' + const outputPath = [varName, ...restPath].filter(Boolean).join('.') + if (nodeLabel && outputPath) + return `${nodeLabel}.${outputPath}` + if (nodeLabel) + return nodeLabel + if (outputPath) + return outputPath + return t('nodes.llm.contextUnknownNode', { ns: 'workflow' }) + }, [getContextNodeLabel, t]) + const ensureLLMContextReady = useCallback(() => { if (blockType !== BlockEnum.LLM) return true @@ -265,12 +293,20 @@ const useLastRun = ({ const [nodeId, varName] = selectorKey.split('::') const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) if (!inspectVarValue) { - Toast.notify({ type: 'error', message: t('nodes.llm.contextMissing', { ns: 'workflow' }) }) + const nodeLabel = getContextNodeLabel(nodeId) + || t('nodes.llm.contextUnknownNode', { ns: 'workflow' }) + Toast.notify({ + type: 'error', + message: t('nodes.llm.contextMissing', { + ns: 'workflow', + nodeName: nodeLabel, + }), + }) return false } } return true - }, [blockType, data, t]) + }, [blockType, data, t, hasSetInspectVar, systemVars, conversationVars, getContextNodeLabel]) const handleRunWithParams = async (data: Record) => { if (blockIfChecklistFailed()) @@ -281,8 +317,15 @@ const useLastRun = ({ if (!ensureLLMContextReady()) return const dependentVars = singleRunParams?.getDependentVars?.() - if (hasNullDependentOutputs(dependentVars)) { - Toast.notify({ type: 'error', message: t('singleRun.subgraph.nullOutputError', { ns: 'workflow' }) }) + const nullOutput = getNullDependentOutput(dependentVars) + if (nullOutput) { + Toast.notify({ + type: 'error', + message: t('singleRun.subgraph.nullOutputError', { + ns: 'workflow', + output: formatSubgraphOutputLabel(nullOutput), + }), + }) return } setNodeRunning() @@ -388,8 +431,15 @@ const useLastRun = ({ if (!ensureLLMContextReady()) return const dependentVars = singleRunParams?.getDependentVars?.() - if (hasNullDependentOutputs(dependentVars)) { - Toast.notify({ type: 'error', message: t('singleRun.subgraph.nullOutputError', { ns: 'workflow' }) }) + const nullOutput = getNullDependentOutput(dependentVars) + if (nullOutput) { + Toast.notify({ + type: 'error', + message: t('singleRun.subgraph.nullOutputError', { + ns: 'workflow', + output: formatSubgraphOutputLabel(nullOutput), + }), + }) return } if (blockType === BlockEnum.TriggerWebhook || blockType === BlockEnum.TriggerPlugin || blockType === BlockEnum.TriggerSchedule) diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index c3ed1cdc9d..27783c4362 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -672,8 +672,9 @@ "nodes.llm.computerUse.tooltip": "Manage the runtime filesystem and tool access for your agent.", "nodes.llm.context": "context", "nodes.llm.contextBlock": "Context Block", - "nodes.llm.contextMissing": "Missing context from previous nodes. Please select a context variable.", + "nodes.llm.contextMissing": "Missing context from node {{nodeName}}. Please select a context variable.", "nodes.llm.contextTooltip": "You can import Knowledge as context", + "nodes.llm.contextUnknownNode": "Unknown node", "nodes.llm.files": "Files", "nodes.llm.jsonSchema.addChildField": "Add Child Field", "nodes.llm.jsonSchema.addField": "Add Field", @@ -1058,7 +1059,7 @@ "singleRun.reRun": "Re-run", "singleRun.running": "Running", "singleRun.startRun": "Start Run", - "singleRun.subgraph.nullOutputError": "Subgraph outputs contain null values. Run dependent nodes first.", + "singleRun.subgraph.nullOutputError": "Referenced output {{output}} is empty. Run upstream nodes first.", "singleRun.testRun": "Test Run", "singleRun.testRunIteration": "Test Run Iteration", "singleRun.testRunLoop": "Test Run Loop", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index 32fef9065f..6849c74678 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -648,8 +648,9 @@ "nodes.llm.addMessage": "メッセージ追加", "nodes.llm.context": "コンテキスト", "nodes.llm.contextBlock": "コンテキストブロック", - "nodes.llm.contextMissing": "前のノードのコンテキストがありません。コンテキスト変数を選択してください。", + "nodes.llm.contextMissing": "ノード「{{nodeName}}」のコンテキストがありません。コンテキスト変数を選択してください。", "nodes.llm.contextTooltip": "ナレッジベースをコンテキストとして利用", + "nodes.llm.contextUnknownNode": "不明なノード", "nodes.llm.files": "ファイル", "nodes.llm.jsonSchema.addChildField": "サブフィールドを追加", "nodes.llm.jsonSchema.addField": "フィールドを追加", @@ -1030,7 +1031,7 @@ "singleRun.reRun": "再実行", "singleRun.running": "実行中", "singleRun.startRun": "実行開始", - "singleRun.subgraph.nullOutputError": "サブグラフの出力にnullが含まれているため、単体デバッグできません。依存ノードを先に実行してください。", + "singleRun.subgraph.nullOutputError": "サブグラフで参照している出力「{{output}}」が空です。先に上流ノードを実行してください。", "singleRun.testRun": "テスト実行", "singleRun.testRunIteration": "テスト実行(イテレーション)", "singleRun.testRunLoop": "テスト実行ループ", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index f88cc858d6..9736dbd804 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -665,8 +665,9 @@ "nodes.llm.computerUse.tooltip": "管理代理的运行时文件系统与工具访问权限。", "nodes.llm.context": "上下文", "nodes.llm.contextBlock": "上下文块", - "nodes.llm.contextMissing": "缺少前序节点的上下文,请先选择上下文变量。", + "nodes.llm.contextMissing": "缺少前序节点「{{nodeName}}」的上下文,请先选择上下文变量。", "nodes.llm.contextTooltip": "您可以导入知识库作为上下文", + "nodes.llm.contextUnknownNode": "未知节点", "nodes.llm.files": "文件", "nodes.llm.jsonSchema.addChildField": "添加子字段", "nodes.llm.jsonSchema.addField": "添加字段", @@ -1050,7 +1051,7 @@ "singleRun.reRun": "重新运行", "singleRun.running": "运行中", "singleRun.startRun": "开始运行", - "singleRun.subgraph.nullOutputError": "子图输出包含空值,无法单步调试。请先运行依赖节点。", + "singleRun.subgraph.nullOutputError": "子图引用的输出「{{output}}」为空,请先运行上游节点。", "singleRun.testRun": "测试运行", "singleRun.testRunIteration": "测试运行迭代", "singleRun.testRunLoop": "测试运行循环", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index d4e98ac766..056786a277 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -648,8 +648,9 @@ "nodes.llm.addMessage": "新增消息", "nodes.llm.context": "上下文", "nodes.llm.contextBlock": "上下文區塊", - "nodes.llm.contextMissing": "缺少前序節點的上下文,請先選擇上下文變數。", + "nodes.llm.contextMissing": "缺少前序節點「{{nodeName}}」的上下文,請先選擇上下文變數。", "nodes.llm.contextTooltip": "您可以導入知識庫作為上下文", + "nodes.llm.contextUnknownNode": "未知節點", "nodes.llm.files": "文件", "nodes.llm.jsonSchema.addChildField": "新增子欄位", "nodes.llm.jsonSchema.addField": "新增字段", @@ -1031,7 +1032,7 @@ "singleRun.reRun": "重新運行", "singleRun.running": "運行中", "singleRun.startRun": "開始運行", - "singleRun.subgraph.nullOutputError": "子圖輸出包含空值,無法單步調試。請先執行依賴節點。", + "singleRun.subgraph.nullOutputError": "子圖引用的輸出「{{output}}」為空,請先執行上游節點。", "singleRun.testRun": "測試運行", "singleRun.testRunIteration": "測試運行迭代", "singleRun.testRunLoop": "測試運行循環",