fix: display workflow run errors in the result panel (#39746)

(cherry picked from commit 54aced9e20)
This commit is contained in:
Joel 2026-07-29 15:24:48 +08:00 committed by 非法操作
parent 1714d9b3a0
commit 7fdbeba468
4 changed files with 109 additions and 50 deletions

View File

@ -124,13 +124,15 @@ describe('useWorkflowRun callbacks helpers', () => {
expect(fetchInspectVars).toHaveBeenCalledWith({})
expect(invalidAllLastRun).toHaveBeenCalled()
callbacks.onError?.({ error: 'failed', node_type: 'llm' } as never)
callbacks.onError?.('LLM provider and model are required.')
expect(clearAbortController).toHaveBeenCalled()
expect(handlers.handleWorkflowFailed).toHaveBeenCalled()
expect(handlers.handleWorkflowFailed).toHaveBeenCalledWith(
'LLM provider and model are required.',
)
expect(userOnError).toHaveBeenCalled()
expect(getWorkflowRunningData).toHaveBeenCalled()
expect(trackWorkflowRunFailed).toHaveBeenCalledWith(
{ error: 'failed', node_type: 'llm' },
'LLM provider and model are required.',
workflowData,
)

View File

@ -11,7 +11,7 @@ type ContainerSize = {
type WorkflowRunEventHandlers = {
handleWorkflowStarted: NonNullable<IOtherOptions['onWorkflowStarted']>
handleWorkflowFinished: NonNullable<IOtherOptions['onWorkflowFinished']>
handleWorkflowFailed: () => void
handleWorkflowFailed: (error?: string) => void
handleWorkflowNodeStarted: (
params: Parameters<NonNullable<IOtherOptions['onNodeStarted']>>[0],
containerParams: ContainerSize,
@ -150,7 +150,7 @@ export const createBaseWorkflowRunCallbacks = ({
const wrappedOnError: IOtherOptions['onError'] = (params, code) => {
clearAbortController()
handleWorkflowFailed()
handleWorkflowFailed(params)
const workflowData = getWorkflowRunningData()
invalidateRunHistory(runHistoryUrl)
clearListeningState()
@ -358,7 +358,7 @@ export const createFinalWorkflowRunCallbacks = ({
},
onError: (params, code) => {
clearAbortController()
handleWorkflowFailed()
handleWorkflowFailed(params)
const workflowData = getWorkflowRunningData()
invalidateRunHistory(runHistoryUrl)
clearListeningState()

View File

@ -66,10 +66,13 @@ describe('useWorkflowFailed', () => {
})
act(() => {
result.current.handleWorkflowFailed()
result.current.handleWorkflowFailed('LLM provider and model are required.')
})
expect(store.getState().workflowRunningData!.result.status).toBe(WorkflowRunningStatus.Failed)
expect(store.getState().workflowRunningData!.result.error).toBe(
'LLM provider and model are required.',
)
expect(store.getState().workflowRunningData!.tracing!.map((trace) => trace.status)).toEqual([
NodeRunningStatus.Failed,
NodeRunningStatus.Succeeded,
@ -115,4 +118,44 @@ describe('useWorkflowFailed', () => {
expect(store.getState().workflowRunningData!.result.status).toBe(WorkflowRunningStatus.Stopped)
})
it('adds a late error message to an already failed workflow', () => {
const { result, store } = renderRunEventHook(() => useWorkflowFailed(), {
initialStoreState: {
workflowRunningData: baseRunningData({
result: { status: WorkflowRunningStatus.Failed },
}),
},
})
act(() => {
result.current.handleWorkflowFailed('LLM provider and model are required.')
})
expect(store.getState().workflowRunningData!.result).toMatchObject({
status: WorkflowRunningStatus.Failed,
error: 'LLM provider and model are required.',
})
})
it('keeps the workflow_finished error when a generic error arrives later', () => {
const { result, store } = renderRunEventHook(() => useWorkflowFailed(), {
initialStoreState: {
workflowRunningData: baseRunningData({
result: {
status: WorkflowRunningStatus.Failed,
error: 'LLM provider and model are required.',
},
}),
},
})
act(() => {
result.current.handleWorkflowFailed('Server Error')
})
expect(store.getState().workflowRunningData!.result.error).toBe(
'LLM provider and model are required.',
)
})
})

View File

@ -8,52 +8,66 @@ export const useWorkflowFailed = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleWorkflowFailed = useCallback(() => {
const { workflowRunningData, setWorkflowRunningData } = workflowStore.getState()
if (!workflowRunningData) return
if (
workflowRunningData.result.status === WorkflowRunningStatus.Succeeded ||
workflowRunningData.result.status === WorkflowRunningStatus.Failed ||
workflowRunningData.result.status === WorkflowRunningStatus.Stopped
)
return
const handleWorkflowFailed = useCallback(
(error?: string) => {
const { workflowRunningData, setWorkflowRunningData } = workflowStore.getState()
if (!workflowRunningData) return
if (
workflowRunningData.result.status === WorkflowRunningStatus.Succeeded ||
workflowRunningData.result.status === WorkflowRunningStatus.Stopped
)
return
const { getNodes, setNodes, edges, setEdges } = store.getState()
if (workflowRunningData.result.status === WorkflowRunningStatus.Failed) {
if (!error || workflowRunningData.result.error) return
setWorkflowRunningData(
produce(workflowRunningData, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Failed,
}
draft.tracing?.forEach((trace) => {
if (trace.status === NodeRunningStatus.Running) trace.status = NodeRunningStatus.Failed
})
}),
)
setWorkflowRunningData(
produce(workflowRunningData, (draft) => {
draft.result.error = error
}),
)
return
}
setNodes(
produce(getNodes(), (draft) => {
draft.forEach((node) => {
if (node.data._runningStatus === NodeRunningStatus.Running)
node.data._runningStatus = NodeRunningStatus.Failed
node.data._waitingRun = false
})
}),
)
setEdges(
produce(edges, (draft) => {
draft.forEach((edge) => {
if (!edge.data) return
if (edge.data._sourceRunningStatus === NodeRunningStatus.Running)
edge.data._sourceRunningStatus = NodeRunningStatus.Failed
if (edge.data._targetRunningStatus === NodeRunningStatus.Running)
edge.data._targetRunningStatus = NodeRunningStatus.Failed
edge.data._waitingRun = false
})
}),
)
}, [store, workflowStore])
const { getNodes, setNodes, edges, setEdges } = store.getState()
setWorkflowRunningData(
produce(workflowRunningData, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Failed,
error,
}
draft.tracing?.forEach((trace) => {
if (trace.status === NodeRunningStatus.Running) trace.status = NodeRunningStatus.Failed
})
}),
)
setNodes(
produce(getNodes(), (draft) => {
draft.forEach((node) => {
if (node.data._runningStatus === NodeRunningStatus.Running)
node.data._runningStatus = NodeRunningStatus.Failed
node.data._waitingRun = false
})
}),
)
setEdges(
produce(edges, (draft) => {
draft.forEach((edge) => {
if (!edge.data) return
if (edge.data._sourceRunningStatus === NodeRunningStatus.Running)
edge.data._sourceRunningStatus = NodeRunningStatus.Failed
if (edge.data._targetRunningStatus === NodeRunningStatus.Running)
edge.data._targetRunningStatus = NodeRunningStatus.Failed
edge.data._waitingRun = false
})
}),
)
},
[store, workflowStore],
)
return {
handleWorkflowFailed,