feat: Improve error messages for missing workflow outputs

This commit is contained in:
zhsama
2026-01-28 21:22:32 +08:00
parent 144089d3ed
commit 2aa6dcaa1a
6 changed files with 84 additions and 31 deletions

View File

@ -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,
}
}

View File

@ -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 = <T>({
hasSetInspectVar,
nodesWithInspectVars,
} = useInspectVarsCrud()
const { hasNullDependentOutputs } = useSubGraphVariablesCheck({
const { getNullDependentOutput } = useSubGraphVariablesCheck({
currentNodeId,
nodesWithInspectVars,
})
@ -153,6 +154,7 @@ const useLastRun = <T>({
const isAggregatorNode = blockType === BlockEnum.VariableAggregator
const isCustomRunNode = isSupportCustomRunForm(blockType)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const reactFlowStore = useStoreApi()
const {
getData: getDataForCheckMore,
} = useGetDataForCheckMoreHooks<T>(blockType)(currentNodeId, oneStepRunParams.data)
@ -238,6 +240,7 @@ const useLastRun = <T>({
const workflowStore = useWorkflowStore()
const { setInitShowLastRunTab, setShowVariableInspectPanel } = workflowStore.getState()
const initShowLastRunTab = useStore(s => s.initShowLastRunTab)
const parentAvailableNodes = useStore(s => s.parentAvailableNodes) || []
const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings)
useEffect(() => {
if (initShowLastRunTab)
@ -247,6 +250,31 @@ const useLastRun = <T>({
}, [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 = <T>({
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<string, any>) => {
if (blockIfChecklistFailed())
@ -281,8 +317,15 @@ const useLastRun = <T>({
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 = <T>({
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)

View File

@ -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",

View File

@ -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": "テスト実行ループ",

View File

@ -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": "测试运行循环",

View File

@ -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": "測試運行循環",