Browse Source

feat: workflow continue on error (#11474)

zxhlyh 4 months ago
parent
commit
bec5451f12
60 changed files with 1481 additions and 282 deletions
  1. 4 2
      web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx
  2. 53 0
      web/app/components/workflow/custom-edge-linear-gradient-render.tsx
  3. 56 1
      web/app/components/workflow/custom-edge.tsx
  4. 20 14
      web/app/components/workflow/hooks/use-edges-interactions.ts
  5. 1 0
      web/app/components/workflow/hooks/use-nodes-interactions.ts
  6. 67 11
      web/app/components/workflow/hooks/use-workflow-run.ts
  7. 1 1
      web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx
  8. 26 0
      web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx
  9. 56 0
      web/app/components/workflow/nodes/_base/components/collapse/index.tsx
  10. 3 0
      web/app/components/workflow/nodes/_base/components/editor/base.tsx
  11. 3 0
      web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
  12. 89 0
      web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx
  13. 67 0
      web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-node.tsx
  14. 90 0
      web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx
  15. 43 0
      web/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip.tsx
  16. 95 0
      web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx
  17. 32 0
      web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx
  18. 123 0
      web/app/components/workflow/nodes/_base/components/error-handle/hooks.ts
  19. 13 0
      web/app/components/workflow/nodes/_base/components/error-handle/types.ts
  20. 83 0
      web/app/components/workflow/nodes/_base/components/error-handle/utils.ts
  21. 14 6
      web/app/components/workflow/nodes/_base/components/next-step/add.tsx
  22. 12 2
      web/app/components/workflow/nodes/_base/components/next-step/container.tsx
  23. 55 35
      web/app/components/workflow/nodes/_base/components/next-step/index.tsx
  24. 28 16
      web/app/components/workflow/nodes/_base/components/node-handle.tsx
  25. 4 22
      web/app/components/workflow/nodes/_base/components/output-vars.tsx
  26. 4 2
      web/app/components/workflow/nodes/_base/components/variable-tag.tsx
  27. 18 0
      web/app/components/workflow/nodes/_base/components/variable/utils.ts
  28. 6 3
      web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx
  29. 5 1
      web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx
  30. 22 3
      web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts
  31. 29 9
      web/app/components/workflow/nodes/_base/node.tsx
  32. 14 2
      web/app/components/workflow/nodes/_base/panel.tsx
  33. 1 1
      web/app/components/workflow/nodes/document-extractor/panel.tsx
  34. 35 52
      web/app/components/workflow/nodes/http/components/timeout/index.tsx
  35. 8 10
      web/app/components/workflow/nodes/http/panel.tsx
  36. 11 1
      web/app/components/workflow/nodes/if-else/components/condition-value.tsx
  37. 2 5
      web/app/components/workflow/nodes/iteration/panel.tsx
  38. 2 2
      web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx
  39. 3 3
      web/app/components/workflow/nodes/list-operator/panel.tsx
  40. 9 11
      web/app/components/workflow/nodes/llm/panel.tsx
  41. 27 31
      web/app/components/workflow/nodes/parameter-extractor/panel.tsx
  42. 10 0
      web/app/components/workflow/nodes/question-classifier/default.ts
  43. 22 21
      web/app/components/workflow/nodes/question-classifier/panel.tsx
  44. 1 1
      web/app/components/workflow/nodes/template-transform/panel.tsx
  45. 4 4
      web/app/components/workflow/nodes/tool/panel.tsx
  46. 3 0
      web/app/components/workflow/nodes/variable-assigner/components/node-group-item.tsx
  47. 5 3
      web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx
  48. 1 0
      web/app/components/workflow/panel/workflow-preview.tsx
  49. 1 0
      web/app/components/workflow/run/index.tsx
  50. 6 0
      web/app/components/workflow/run/meta.tsx
  51. 19 2
      web/app/components/workflow/run/node.tsx
  52. 7 0
      web/app/components/workflow/run/result-panel.tsx
  53. 3 1
      web/app/components/workflow/run/status-container.tsx
  54. 56 1
      web/app/components/workflow/run/status.tsx
  55. 14 1
      web/app/components/workflow/types.ts
  56. 36 1
      web/app/components/workflow/utils.ts
  57. 27 0
      web/i18n/en-US/workflow.ts
  58. 27 0
      web/i18n/zh-Hans/workflow.ts
  59. 1 0
      web/models/log.ts
  60. 4 1
      web/types/workflow.ts

+ 4 - 2
web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx

@@ -26,6 +26,7 @@ import { VarBlockIcon } from '@/app/components/workflow/block-icon'
 import { Line3 } from '@/app/components/base/icons/src/public/common'
 import { Line3 } from '@/app/components/base/icons/src/public/common'
 import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import Tooltip from '@/app/components/base/tooltip'
 import Tooltip from '@/app/components/base/tooltip'
+import { isExceptionVariable } from '@/app/components/workflow/utils'
 
 
 type WorkflowVariableBlockComponentProps = {
 type WorkflowVariableBlockComponentProps = {
   nodeKey: string
   nodeKey: string
@@ -53,6 +54,7 @@ const WorkflowVariableBlockComponent = ({
   const node = localWorkflowNodesMap![variables[0]]
   const node = localWorkflowNodesMap![variables[0]]
   const isEnv = isENV(variables)
   const isEnv = isENV(variables)
   const isChatVar = isConversationVar(variables)
   const isChatVar = isConversationVar(variables)
+  const isException = isExceptionVariable(varName, node?.type)
 
 
   useEffect(() => {
   useEffect(() => {
     if (!editor.hasNodes([WorkflowVariableBlockNode]))
     if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -98,10 +100,10 @@ const WorkflowVariableBlockComponent = ({
         </div>
         </div>
       )}
       )}
       <div className='flex items-center text-primary-600'>
       <div className='flex items-center text-primary-600'>
-        {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
+        {!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5', isException && 'text-text-warning')} />}
         {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
         {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
         {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
         {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
-        <div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
+        <div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>
         {
         {
           !node && !isEnv && !isChatVar && (
           !node && !isEnv && !isChatVar && (
             <RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
             <RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />

+ 53 - 0
web/app/components/workflow/custom-edge-linear-gradient-render.tsx

@@ -0,0 +1,53 @@
+type CustomEdgeLinearGradientRenderProps = {
+  id: string
+  startColor: string
+  stopColor: string
+  position: {
+    x1: number
+    x2: number
+    y1: number
+    y2: number
+  }
+}
+const CustomEdgeLinearGradientRender = ({
+  id,
+  startColor,
+  stopColor,
+  position,
+}: CustomEdgeLinearGradientRenderProps) => {
+  const {
+    x1,
+    x2,
+    y1,
+    y2,
+  } = position
+  return (
+    <defs>
+      <linearGradient
+        id={id}
+        gradientUnits='userSpaceOnUse'
+        x1={x1}
+        y1={y1}
+        x2={x2}
+        y2={y2}
+      >
+        <stop
+          offset='0%'
+          style={{
+            stopColor: startColor,
+            stopOpacity: 1,
+          }}
+        />
+        <stop
+          offset='100%'
+          style={{
+            stopColor,
+            stopOpacity: 1,
+          }}
+        />
+      </linearGradient>
+    </defs>
+  )
+}
+
+export default CustomEdgeLinearGradientRender

+ 56 - 1
web/app/components/workflow/custom-edge.tsx

@@ -1,6 +1,7 @@
 import {
 import {
   memo,
   memo,
   useCallback,
   useCallback,
+  useMemo,
   useState,
   useState,
 } from 'react'
 } from 'react'
 import { intersection } from 'lodash-es'
 import { intersection } from 'lodash-es'
@@ -20,8 +21,12 @@ import type {
   Edge,
   Edge,
   OnSelectBlock,
   OnSelectBlock,
 } from './types'
 } from './types'
+import { NodeRunningStatus } from './types'
+import { getEdgeColor } from './utils'
 import { ITERATION_CHILDREN_Z_INDEX } from './constants'
 import { ITERATION_CHILDREN_Z_INDEX } from './constants'
+import CustomEdgeLinearGradientRender from './custom-edge-linear-gradient-render'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
+import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
 
 
 const CustomEdge = ({
 const CustomEdge = ({
   id,
   id,
@@ -53,6 +58,26 @@ const CustomEdge = ({
   const { handleNodeAdd } = useNodesInteractions()
   const { handleNodeAdd } = useNodesInteractions()
   const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration)
   const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration)
   const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration)
   const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration)
+  const {
+    _sourceRunningStatus,
+    _targetRunningStatus,
+  } = data
+
+  const linearGradientId = useMemo(() => {
+    if (
+      (
+        _sourceRunningStatus === NodeRunningStatus.Succeeded
+        || _sourceRunningStatus === NodeRunningStatus.Failed
+        || _sourceRunningStatus === NodeRunningStatus.Exception
+      ) && (
+        _targetRunningStatus === NodeRunningStatus.Succeeded
+        || _targetRunningStatus === NodeRunningStatus.Failed
+        || _targetRunningStatus === NodeRunningStatus.Exception
+        || _targetRunningStatus === NodeRunningStatus.Running
+      )
+    )
+      return id
+  }, [_sourceRunningStatus, _targetRunningStatus, id])
 
 
   const handleOpenChange = useCallback((v: boolean) => {
   const handleOpenChange = useCallback((v: boolean) => {
     setOpen(v)
     setOpen(v)
@@ -73,14 +98,43 @@ const CustomEdge = ({
     )
     )
   }, [handleNodeAdd, source, sourceHandleId, target, targetHandleId])
   }, [handleNodeAdd, source, sourceHandleId, target, targetHandleId])
 
 
+  const stroke = useMemo(() => {
+    if (selected)
+      return getEdgeColor(NodeRunningStatus.Running)
+
+    if (linearGradientId)
+      return `url(#${linearGradientId})`
+
+    if (data?._connectedNodeIsHovering)
+      return getEdgeColor(NodeRunningStatus.Running, sourceHandleId === ErrorHandleTypeEnum.failBranch)
+
+    return getEdgeColor()
+  }, [data._connectedNodeIsHovering, linearGradientId, selected, sourceHandleId])
+
   return (
   return (
     <>
     <>
+      {
+        linearGradientId && (
+          <CustomEdgeLinearGradientRender
+            id={linearGradientId}
+            startColor={getEdgeColor(_sourceRunningStatus)}
+            stopColor={getEdgeColor(_targetRunningStatus)}
+            position={{
+              x1: sourceX,
+              y1: sourceY,
+              x2: targetX,
+              y2: targetY,
+            }}
+          />
+        )
+      }
       <BaseEdge
       <BaseEdge
         id={id}
         id={id}
         path={edgePath}
         path={edgePath}
         style={{
         style={{
-          stroke: (selected || data?._connectedNodeIsHovering || data?._run) ? '#2970FF' : '#D0D5DD',
+          stroke,
           strokeWidth: 2,
           strokeWidth: 2,
+          opacity: data._waitingRun ? 0.7 : 1,
         }}
         }}
       />
       />
       <EdgeLabelRenderer>
       <EdgeLabelRenderer>
@@ -95,6 +149,7 @@ const CustomEdge = ({
             position: 'absolute',
             position: 'absolute',
             transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
             transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
             pointerEvents: 'all',
             pointerEvents: 'all',
+            opacity: data._waitingRun ? 0.7 : 1,
           }}
           }}
         >
         >
           <BlockSelector
           <BlockSelector

+ 20 - 14
web/app/components/workflow/hooks/use-edges-interactions.ts

@@ -63,25 +63,29 @@ export const useEdgesInteractions = () => {
       edges,
       edges,
       setEdges,
       setEdges,
     } = store.getState()
     } = store.getState()
-    const currentEdgeIndex = edges.findIndex(edge => edge.source === nodeId && edge.sourceHandle === branchId)
+    const edgeWillBeDeleted = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === branchId)
 
 
-    if (currentEdgeIndex < 0)
+    if (!edgeWillBeDeleted.length)
       return
       return
 
 
-    const currentEdge = edges[currentEdgeIndex]
-    const newNodes = produce(getNodes(), (draft: Node[]) => {
-      const sourceNode = draft.find(node => node.id === currentEdge.source)
-      const targetNode = draft.find(node => node.id === currentEdge.target)
-
-      if (sourceNode)
-        sourceNode.data._connectedSourceHandleIds = sourceNode.data._connectedSourceHandleIds?.filter(handleId => handleId !== currentEdge.sourceHandle)
-
-      if (targetNode)
-        targetNode.data._connectedTargetHandleIds = targetNode.data._connectedTargetHandleIds?.filter(handleId => handleId !== currentEdge.targetHandle)
+    const nodes = getNodes()
+    const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
+      edgeWillBeDeleted.map(edge => ({ type: 'remove', edge })),
+      nodes,
+    )
+    const newNodes = produce(nodes, (draft: Node[]) => {
+      draft.forEach((node) => {
+        if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
+          node.data = {
+            ...node.data,
+            ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
+          }
+        }
+      })
     })
     })
     setNodes(newNodes)
     setNodes(newNodes)
     const newEdges = produce(edges, (draft) => {
     const newEdges = produce(edges, (draft) => {
-      draft.splice(currentEdgeIndex, 1)
+      return draft.filter(edge => !edgeWillBeDeleted.find(e => e.id === edge.id))
     })
     })
     setEdges(newEdges)
     setEdges(newEdges)
     handleSyncWorkflowDraft()
     handleSyncWorkflowDraft()
@@ -155,7 +159,9 @@ export const useEdgesInteractions = () => {
 
 
     const newEdges = produce(edges, (draft) => {
     const newEdges = produce(edges, (draft) => {
       draft.forEach((edge) => {
       draft.forEach((edge) => {
-        edge.data._run = false
+        edge.data._sourceRunningStatus = undefined
+        edge.data._targetRunningStatus = undefined
+        edge.data._waitingRun = false
       })
       })
     })
     })
     setEdges(newEdges)
     setEdges(newEdges)

+ 1 - 0
web/app/components/workflow/hooks/use-nodes-interactions.ts

@@ -1033,6 +1033,7 @@ export const useNodesInteractions = () => {
     const newNodes = produce(nodes, (draft) => {
     const newNodes = produce(nodes, (draft) => {
       draft.forEach((node) => {
       draft.forEach((node) => {
         node.data._runningStatus = undefined
         node.data._runningStatus = undefined
+        node.data._waitingRun = false
       })
       })
     })
     })
     setNodes(newNodes)
     setNodes(newNodes)

+ 67 - 11
web/app/components/workflow/hooks/use-workflow-run.ts

@@ -1,6 +1,5 @@
 import { useCallback } from 'react'
 import { useCallback } from 'react'
 import {
 import {
-  getIncomers,
   useReactFlow,
   useReactFlow,
   useStoreApi,
   useStoreApi,
 } from 'reactflow'
 } from 'reactflow'
@@ -9,8 +8,8 @@ import { v4 as uuidV4 } from 'uuid'
 import { usePathname } from 'next/navigation'
 import { usePathname } from 'next/navigation'
 import { useWorkflowStore } from '../store'
 import { useWorkflowStore } from '../store'
 import { useNodesSyncDraft } from '../hooks'
 import { useNodesSyncDraft } from '../hooks'
-import type { Node } from '../types'
 import {
 import {
+  BlockEnum,
   NodeRunningStatus,
   NodeRunningStatus,
   WorkflowRunningStatus,
   WorkflowRunningStatus,
 } from '../types'
 } from '../types'
@@ -28,6 +27,7 @@ import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player
 import {
 import {
   getFilesInLogs,
   getFilesInLogs,
 } from '@/app/components/base/file-uploader/utils'
 } from '@/app/components/base/file-uploader/utils'
+import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
 
 
 export const useWorkflowRun = () => {
 export const useWorkflowRun = () => {
   const store = useStoreApi()
   const store = useStoreApi()
@@ -174,6 +174,8 @@ export const useWorkflowRun = () => {
             setIterParallelLogMap,
             setIterParallelLogMap,
           } = workflowStore.getState()
           } = workflowStore.getState()
           const {
           const {
+            getNodes,
+            setNodes,
             edges,
             edges,
             setEdges,
             setEdges,
           } = store.getState()
           } = store.getState()
@@ -186,12 +188,20 @@ export const useWorkflowRun = () => {
               status: WorkflowRunningStatus.Running,
               status: WorkflowRunningStatus.Running,
             }
             }
           }))
           }))
-
+          const nodes = getNodes()
+          const newNodes = produce(nodes, (draft) => {
+            draft.forEach((node) => {
+              node.data._waitingRun = true
+            })
+          })
+          setNodes(newNodes)
           const newEdges = produce(edges, (draft) => {
           const newEdges = produce(edges, (draft) => {
             draft.forEach((edge) => {
             draft.forEach((edge) => {
               edge.data = {
               edge.data = {
                 ...edge.data,
                 ...edge.data,
-                _run: false,
+                _sourceRunningStatus: undefined,
+                _targetRunningStatus: undefined,
+                _waitingRun: true,
               }
               }
             })
             })
           })
           })
@@ -311,13 +321,27 @@ export const useWorkflowRun = () => {
             }
             }
             const newNodes = produce(nodes, (draft) => {
             const newNodes = produce(nodes, (draft) => {
               draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
               draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
+              draft[currentNodeIndex].data._waitingRun = false
             })
             })
             setNodes(newNodes)
             setNodes(newNodes)
-            const incomeNodesId = getIncomers({ id: data.node_id } as Node, newNodes, edges).filter(node => node.data._runningStatus === NodeRunningStatus.Succeeded).map(node => node.id)
             const newEdges = produce(edges, (draft) => {
             const newEdges = produce(edges, (draft) => {
-              draft.forEach((edge) => {
-                if (edge.target === data.node_id && incomeNodesId.includes(edge.source))
-                  edge.data = { ...edge.data, _run: true } as any
+              const incomeEdges = draft.filter((edge) => {
+                return edge.target === data.node_id
+              })
+
+              incomeEdges.forEach((edge) => {
+                const incomeNode = nodes.find(node => node.id === edge.source)!
+                if (
+                  (!incomeNode.data._runningBranchId && edge.sourceHandle === 'source')
+                  || (incomeNode.data._runningBranchId && edge.sourceHandle === incomeNode.data._runningBranchId)
+                ) {
+                  edge.data = {
+                    ...edge.data,
+                    _sourceRunningStatus: incomeNode.data._runningStatus,
+                    _targetRunningStatus: NodeRunningStatus.Running,
+                    _waitingRun: false,
+                  }
+                }
               })
               })
             })
             })
             setEdges(newEdges)
             setEdges(newEdges)
@@ -336,6 +360,8 @@ export const useWorkflowRun = () => {
           const {
           const {
             getNodes,
             getNodes,
             setNodes,
             setNodes,
+            edges,
+            setEdges,
           } = store.getState()
           } = store.getState()
           const nodes = getNodes()
           const nodes = getNodes()
           const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId
           const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId
@@ -423,8 +449,31 @@ export const useWorkflowRun = () => {
             const newNodes = produce(nodes, (draft) => {
             const newNodes = produce(nodes, (draft) => {
               const currentNode = draft.find(node => node.id === data.node_id)!
               const currentNode = draft.find(node => node.id === data.node_id)!
               currentNode.data._runningStatus = data.status as any
               currentNode.data._runningStatus = data.status as any
+              if (data.status === NodeRunningStatus.Exception) {
+                if (data.execution_metadata.error_strategy === ErrorHandleTypeEnum.failBranch)
+                  currentNode.data._runningBranchId = ErrorHandleTypeEnum.failBranch
+              }
+              else {
+                if (data.node_type === BlockEnum.IfElse)
+                  currentNode.data._runningBranchId = data?.outputs?.selected_case_id
+
+                if (data.node_type === BlockEnum.QuestionClassifier)
+                  currentNode.data._runningBranchId = data?.outputs?.class_id
+              }
             })
             })
             setNodes(newNodes)
             setNodes(newNodes)
+            const newEdges = produce(edges, (draft) => {
+              const incomeEdges = draft.filter((edge) => {
+                return edge.target === data.node_id
+              })
+              incomeEdges.forEach((edge) => {
+                edge.data = {
+                  ...edge.data,
+                  _targetRunningStatus: data.status as any,
+                }
+              })
+            })
+            setEdges(newEdges)
             prevNodeId = data.node_id
             prevNodeId = data.node_id
           }
           }
 
 
@@ -474,13 +523,20 @@ export const useWorkflowRun = () => {
           const newNodes = produce(nodes, (draft) => {
           const newNodes = produce(nodes, (draft) => {
             draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
             draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
             draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length
             draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length
+            draft[currentNodeIndex].data._waitingRun = false
           })
           })
           setNodes(newNodes)
           setNodes(newNodes)
           const newEdges = produce(edges, (draft) => {
           const newEdges = produce(edges, (draft) => {
-            const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
+            const incomeEdges = draft.filter(edge => edge.target === data.node_id)
 
 
-            if (edge)
-              edge.data = { ...edge.data, _run: true } as any
+            incomeEdges.forEach((edge) => {
+              edge.data = {
+                ...edge.data,
+                _sourceRunningStatus: nodes.find(node => node.id === edge.source)!.data._runningStatus,
+                _targetRunningStatus: NodeRunningStatus.Running,
+                _waitingRun: false,
+              }
+            })
           })
           })
           setEdges(newEdges)
           setEdges(newEdges)
 
 

+ 1 - 1
web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx

@@ -59,7 +59,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
 }) => {
 }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 
-  const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
+  const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed || runningStatus === NodeRunningStatus.Exception
   const isRunning = runningStatus === NodeRunningStatus.Running
   const isRunning = runningStatus === NodeRunningStatus.Running
   const isFileLoaded = (() => {
   const isFileLoaded = (() => {
     // system files
     // system files

+ 26 - 0
web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx

@@ -0,0 +1,26 @@
+import Collapse from '.'
+
+type FieldCollapseProps = {
+  title: string
+  children: JSX.Element
+}
+const FieldCollapse = ({
+  title,
+  children,
+}: FieldCollapseProps) => {
+  return (
+    <div className='py-4'>
+      <Collapse
+        trigger={
+          <div className='flex items-center h-6 system-sm-semibold-uppercase text-text-secondary cursor-pointer'>{title}</div>
+        }
+      >
+        <div className='px-4'>
+          {children}
+        </div>
+      </Collapse>
+    </div>
+  )
+}
+
+export default FieldCollapse

+ 56 - 0
web/app/components/workflow/nodes/_base/components/collapse/index.tsx

@@ -0,0 +1,56 @@
+import { useState } from 'react'
+import { RiArrowDropRightLine } from '@remixicon/react'
+import cn from '@/utils/classnames'
+
+export { default as FieldCollapse } from './field-collapse'
+
+type CollapseProps = {
+  disabled?: boolean
+  trigger: JSX.Element
+  children: JSX.Element
+  collapsed?: boolean
+  onCollapse?: (collapsed: boolean) => void
+}
+const Collapse = ({
+  disabled,
+  trigger,
+  children,
+  collapsed,
+  onCollapse,
+}: CollapseProps) => {
+  const [collapsedLocal, setCollapsedLocal] = useState(true)
+  const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
+
+  return (
+    <>
+      <div
+        className='flex items-center'
+        onClick={() => {
+          if (!disabled) {
+            setCollapsedLocal(!collapsedMerged)
+            onCollapse?.(!collapsedMerged)
+          }
+        }}
+      >
+        <div className='shrink-0 w-4 h-4'>
+          {
+            !disabled && (
+              <RiArrowDropRightLine
+                className={cn(
+                  'w-4 h-4 text-text-tertiary',
+                  !collapsedMerged && 'transform rotate-90',
+                )}
+              />
+            )
+          }
+        </div>
+        {trigger}
+      </div>
+      {
+        !collapsedMerged && children
+      }
+    </>
+  )
+}
+
+export default Collapse

+ 3 - 0
web/app/components/workflow/nodes/_base/components/editor/base.tsx

@@ -33,6 +33,7 @@ type Props = {
   }[]
   }[]
   showFileList?: boolean
   showFileList?: boolean
   showCodeGenerator?: boolean
   showCodeGenerator?: boolean
+  tip?: JSX.Element
 }
 }
 
 
 const Base: FC<Props> = ({
 const Base: FC<Props> = ({
@@ -49,6 +50,7 @@ const Base: FC<Props> = ({
   fileList = [],
   fileList = [],
   showFileList,
   showFileList,
   showCodeGenerator = false,
   showCodeGenerator = false,
+  tip,
 }) => {
 }) => {
   const ref = useRef<HTMLDivElement>(null)
   const ref = useRef<HTMLDivElement>(null)
   const {
   const {
@@ -100,6 +102,7 @@ const Base: FC<Props> = ({
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
+        {tip && <div className='px-1 py-0.5'>{tip}</div>}
         <PromptEditorHeightResizeWrap
         <PromptEditorHeightResizeWrap
           height={isExpand ? editorExpandHeight : editorContentHeight}
           height={isExpand ? editorExpandHeight : editorContentHeight}
           minHeight={editorContentMinHeight}
           minHeight={editorContentMinHeight}

+ 3 - 0
web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx

@@ -34,6 +34,7 @@ export type Props = {
   onGenerated?: (value: string) => void
   onGenerated?: (value: string) => void
   showCodeGenerator?: boolean
   showCodeGenerator?: boolean
   className?: string
   className?: string
+  tip?: JSX.Element
 }
 }
 
 
 export const languageMap = {
 export const languageMap = {
@@ -69,6 +70,7 @@ const CodeEditor: FC<Props> = ({
   onGenerated,
   onGenerated,
   showCodeGenerator = false,
   showCodeGenerator = false,
   className,
   className,
+  tip,
 }) => {
 }) => {
   const [isFocus, setIsFocus] = React.useState(false)
   const [isFocus, setIsFocus] = React.useState(false)
   const [isMounted, setIsMounted] = React.useState(false)
   const [isMounted, setIsMounted] = React.useState(false)
@@ -211,6 +213,7 @@ const CodeEditor: FC<Props> = ({
             fileList={fileList as any}
             fileList={fileList as any}
             showFileList={showFileList}
             showFileList={showFileList}
             showCodeGenerator={showCodeGenerator}
             showCodeGenerator={showCodeGenerator}
+            tip={tip}
           >
           >
             {main}
             {main}
           </Base>
           </Base>

+ 89 - 0
web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx

@@ -0,0 +1,89 @@
+import { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { DefaultValueForm } from './types'
+import Input from '@/app/components/base/input'
+import { VarType } from '@/app/components/workflow/types'
+import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
+import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
+
+type DefaultValueProps = {
+  forms: DefaultValueForm[]
+  onFormChange: (form: DefaultValueForm) => void
+}
+const DefaultValue = ({
+  forms,
+  onFormChange,
+}: DefaultValueProps) => {
+  const { t } = useTranslation()
+  const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => {
+    return (payload: any) => {
+      let value
+      if (type === VarType.string || type === VarType.number)
+        value = payload.target.value
+
+      if (type === VarType.array || type === VarType.arrayNumber || type === VarType.arrayString || type === VarType.arrayObject || type === VarType.arrayFile || type === VarType.object)
+        value = payload
+
+      onFormChange({ key, type, value })
+    }
+  }, [onFormChange])
+
+  return (
+    <div className='px-4 pt-2'>
+      <div className='mb-2 body-xs-regular text-text-tertiary'>
+        {t('workflow.nodes.common.errorHandle.defaultValue.desc')}
+        &nbsp;
+        <a
+          href='https://docs.dify.ai/guides/workflow/error-handling'
+          target='_blank'
+          className='text-text-accent'
+        >
+          {t('workflow.common.learnMore')}
+        </a>
+      </div>
+      <div className='space-y-1'>
+        {
+          forms.map((form, index) => {
+            return (
+              <div
+                key={index}
+                className='py-1'
+              >
+                <div className='flex items-center mb-1'>
+                  <div className='mr-1 system-sm-medium text-text-primary'>{form.key}</div>
+                  <div className='system-xs-regular text-text-tertiary'>{form.type}</div>
+                </div>
+                {
+                  (form.type === VarType.string || form.type === VarType.number) && (
+                    <Input
+                      type={form.type}
+                      value={form.value || (form.type === VarType.string ? '' : 0)}
+                      onChange={getFormChangeHandler({ key: form.key, type: form.type })}
+                    />
+                  )
+                }
+                {
+                  (
+                    form.type === VarType.array
+                    || form.type === VarType.arrayNumber
+                    || form.type === VarType.arrayString
+                    || form.type === VarType.arrayObject
+                    || form.type === VarType.object
+                  ) && (
+                    <CodeEditor
+                      language={CodeLanguage.json}
+                      value={form.value}
+                      onChange={getFormChangeHandler({ key: form.key, type: form.type })}
+                    />
+                  )
+                }
+              </div>
+            )
+          })
+        }
+      </div>
+    </div>
+  )
+}
+
+export default DefaultValue

+ 67 - 0
web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-node.tsx

@@ -0,0 +1,67 @@
+import { useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useUpdateNodeInternals } from 'reactflow'
+import { NodeSourceHandle } from '../node-handle'
+import { ErrorHandleTypeEnum } from './types'
+import type { Node } from '@/app/components/workflow/types'
+import { NodeRunningStatus } from '@/app/components/workflow/types'
+import cn from '@/utils/classnames'
+
+type ErrorHandleOnNodeProps = Pick<Node, 'id' | 'data'>
+const ErrorHandleOnNode = ({
+  id,
+  data,
+}: ErrorHandleOnNodeProps) => {
+  const { t } = useTranslation()
+  const { error_strategy } = data
+  const updateNodeInternals = useUpdateNodeInternals()
+
+  useEffect(() => {
+    if (error_strategy === ErrorHandleTypeEnum.failBranch)
+      updateNodeInternals(id)
+  }, [error_strategy, id, updateNodeInternals])
+
+  if (!error_strategy)
+    return null
+
+  return (
+    <div className='relative pt-1 pb-2 px-3'>
+      <div className={cn(
+        'relative flex items-center justify-between px-[5px] h-6 bg-workflow-block-parma-bg rounded-md',
+        data._runningStatus === NodeRunningStatus.Exception && 'border-[0.5px] border-components-badge-status-light-warning-halo bg-state-warning-hover',
+      )}>
+        <div className='system-xs-medium-uppercase text-text-tertiary'>
+          {t('workflow.common.onFailure')}
+        </div>
+        <div className={cn(
+          'system-xs-medium text-text-secondary',
+          data._runningStatus === NodeRunningStatus.Exception && 'text-text-warning',
+        )}>
+          {
+            error_strategy === ErrorHandleTypeEnum.defaultValue && (
+              t('workflow.nodes.common.errorHandle.defaultValue.output')
+            )
+          }
+          {
+            error_strategy === ErrorHandleTypeEnum.failBranch && (
+              t('workflow.nodes.common.errorHandle.failBranch.title')
+            )
+          }
+        </div>
+        {
+          error_strategy === ErrorHandleTypeEnum.failBranch && (
+            <NodeSourceHandle
+              id={id}
+              data={data}
+              handleId={ErrorHandleTypeEnum.failBranch}
+              handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2 after:!bg-workflow-link-line-failure-button-bg'
+              nodeSelectorClassName='!bg-workflow-link-line-failure-button-bg'
+            />
+          )
+        }
+      </div>
+    </div>
+  )
+}
+
+export default ErrorHandleOnNode

+ 90 - 0
web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx

@@ -0,0 +1,90 @@
+import { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import Collapse from '../collapse'
+import { ErrorHandleTypeEnum } from './types'
+import ErrorHandleTypeSelector from './error-handle-type-selector'
+import FailBranchCard from './fail-branch-card'
+import DefaultValue from './default-value'
+import {
+  useDefaultValue,
+  useErrorHandle,
+} from './hooks'
+import type { DefaultValueForm } from './types'
+import type {
+  CommonNodeType,
+  Node,
+} from '@/app/components/workflow/types'
+import Split from '@/app/components/workflow/nodes/_base/components/split'
+import Tooltip from '@/app/components/base/tooltip'
+
+type ErrorHandleProps = Pick<Node, 'id' | 'data'>
+const ErrorHandle = ({
+  id,
+  data,
+}: ErrorHandleProps) => {
+  const { t } = useTranslation()
+  const { error_strategy, default_value } = data
+  const {
+    collapsed,
+    setCollapsed,
+    handleErrorHandleTypeChange,
+  } = useErrorHandle(id, data)
+  const { handleFormChange } = useDefaultValue(id)
+
+  const getHandleErrorHandleTypeChange = useCallback((data: CommonNodeType) => {
+    return (value: ErrorHandleTypeEnum) => {
+      handleErrorHandleTypeChange(value, data)
+    }
+  }, [handleErrorHandleTypeChange])
+
+  const getHandleFormChange = useCallback((data: CommonNodeType) => {
+    return (v: DefaultValueForm) => {
+      handleFormChange(v, data)
+    }
+  }, [handleFormChange])
+
+  return (
+    <>
+      <Split />
+      <div className='py-4'>
+        <Collapse
+          disabled={!error_strategy}
+          collapsed={collapsed}
+          onCollapse={setCollapsed}
+          trigger={
+            <div className='grow flex items-center justify-between pr-4'>
+              <div className='flex items-center'>
+                <div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>
+                  {t('workflow.nodes.common.errorHandle.title')}
+                </div>
+                <Tooltip popupContent={t('workflow.nodes.common.errorHandle.tip')} />
+              </div>
+              <ErrorHandleTypeSelector
+                value={error_strategy || ErrorHandleTypeEnum.none}
+                onSelected={getHandleErrorHandleTypeChange(data)}
+              />
+            </div>
+          }
+        >
+          <>
+            {
+              error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
+                <FailBranchCard />
+              )
+            }
+            {
+              error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
+                <DefaultValue
+                  forms={default_value}
+                  onFormChange={getHandleFormChange(data)}
+                />
+              )
+            }
+          </>
+        </Collapse>
+      </div>
+    </>
+  )
+}
+
+export default ErrorHandle

+ 43 - 0
web/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip.tsx

@@ -0,0 +1,43 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { RiAlertFill } from '@remixicon/react'
+import { ErrorHandleTypeEnum } from './types'
+
+type ErrorHandleTipProps = {
+  type?: ErrorHandleTypeEnum
+}
+const ErrorHandleTip = ({
+  type,
+}: ErrorHandleTipProps) => {
+  const { t } = useTranslation()
+
+  const text = useMemo(() => {
+    if (type === ErrorHandleTypeEnum.failBranch)
+      return t('workflow.nodes.common.errorHandle.failBranch.inLog')
+
+    if (type === ErrorHandleTypeEnum.defaultValue)
+      return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
+  }, [])
+
+  if (!type)
+    return null
+
+  return (
+    <div
+      className='relative flex p-2 pr-[52px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs'
+    >
+      <div
+        className='absolute inset-0 opacity-40 rounded-lg'
+        style={{
+          background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
+        }}
+      ></div>
+      <RiAlertFill className='shrink-0 mr-1 w-4 h-4 text-text-warning-secondary' />
+      <div className='grow system-xs-medium text-text-primary'>
+        {text}
+      </div>
+    </div>
+  )
+}
+
+export default ErrorHandleTip

+ 95 - 0
web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx

@@ -0,0 +1,95 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiArrowDownSLine,
+  RiCheckLine,
+} from '@remixicon/react'
+import { ErrorHandleTypeEnum } from './types'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import Button from '@/app/components/base/button'
+
+type ErrorHandleTypeSelectorProps = {
+  value: ErrorHandleTypeEnum
+  onSelected: (value: ErrorHandleTypeEnum) => void
+}
+const ErrorHandleTypeSelector = ({
+  value,
+  onSelected,
+}: ErrorHandleTypeSelectorProps) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+  const options = [
+    {
+      value: ErrorHandleTypeEnum.none,
+      label: t('workflow.nodes.common.errorHandle.none.title'),
+      description: t('workflow.nodes.common.errorHandle.none.desc'),
+    },
+    {
+      value: ErrorHandleTypeEnum.defaultValue,
+      label: t('workflow.nodes.common.errorHandle.defaultValue.title'),
+      description: t('workflow.nodes.common.errorHandle.defaultValue.desc'),
+    },
+    {
+      value: ErrorHandleTypeEnum.failBranch,
+      label: t('workflow.nodes.common.errorHandle.failBranch.title'),
+      description: t('workflow.nodes.common.errorHandle.failBranch.desc'),
+    },
+  ]
+  const selectedOption = options.find(option => option.value === value)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={4}
+    >
+      <PortalToFollowElemTrigger onClick={(e) => {
+        e.stopPropagation()
+        setOpen(v => !v)
+      }}>
+        <Button
+          size='small'
+        >
+          {selectedOption?.label}
+          <RiArrowDownSLine className='w-3.5 h-3.5' />
+        </Button>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className='z-[11]'>
+        <div className='p-1 w-[280px] border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur shadow-lg'>
+          {
+            options.map(option => (
+              <div
+                key={option.value}
+                className='flex p-2 pr-3 rounded-lg hover:bg-state-base-hover cursor-pointer'
+                onClick={(e) => {
+                  e.stopPropagation()
+                  onSelected(option.value)
+                  setOpen(false)
+                }}
+              >
+                <div className='mr-1 w-4 shrink-0'>
+                  {
+                    value === option.value && (
+                      <RiCheckLine className='w-4 h-4 text-text-accent' />
+                    )
+                  }
+                </div>
+                <div className='grow'>
+                  <div className='mb-0.5 system-sm-semibold text-text-secondary'>{option.label}</div>
+                  <div className='system-xs-regular text-text-tertiary'>{option.description}</div>
+                </div>
+              </div>
+            ))
+          }
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default ErrorHandleTypeSelector

+ 32 - 0
web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx

@@ -0,0 +1,32 @@
+import { RiMindMap } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+
+const FailBranchCard = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='pt-2 px-4'>
+      <div className='p-4 rounded-[10px] bg-workflow-process-bg'>
+        <div className='flex items-center justify-center mb-2 w-8 h-8 rounded-[10px] border-[0.5px] bg-components-card-bg shadow-lg'>
+          <RiMindMap className='w-5 h-5 text-text-tertiary' />
+        </div>
+        <div className='mb-1 system-sm-medium text-text-secondary'>
+          {t('workflow.nodes.common.errorHandle.failBranch.customize')}
+        </div>
+        <div className='system-xs-regular text-text-tertiary'>
+          {t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}
+          &nbsp;
+          <a
+            href='https://docs.dify.ai/guides/workflow/error-handling'
+            target='_blank'
+            className='text-text-accent'
+          >
+            {t('workflow.common.learnMore')}
+          </a>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default FailBranchCard

+ 123 - 0
web/app/components/workflow/nodes/_base/components/error-handle/hooks.ts

@@ -0,0 +1,123 @@
+import {
+  useCallback,
+  useMemo,
+  useState,
+} from 'react'
+import { ErrorHandleTypeEnum } from './types'
+import type { DefaultValueForm } from './types'
+import { getDefaultValue } from './utils'
+import type {
+  CommonNodeType,
+} from '@/app/components/workflow/types'
+import {
+  useEdgesInteractions,
+  useNodeDataUpdate,
+} from '@/app/components/workflow/hooks'
+
+export const useDefaultValue = (
+  id: string,
+) => {
+  const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+  const handleFormChange = useCallback((
+    {
+      key,
+      value,
+      type,
+    }: DefaultValueForm,
+    data: CommonNodeType,
+  ) => {
+    const default_value = data.default_value || []
+    const index = default_value.findIndex(form => form.key === key)
+
+    if (index > -1) {
+      const newDefaultValue = [...default_value]
+      newDefaultValue[index].value = value
+      handleNodeDataUpdateWithSyncDraft({
+        id,
+        data: {
+          default_value: newDefaultValue,
+        },
+      })
+      return
+    }
+
+    handleNodeDataUpdateWithSyncDraft({
+      id,
+      data: {
+        default_value: [
+          ...default_value,
+          {
+            key,
+            value,
+            type,
+          },
+        ],
+      },
+    })
+  }, [handleNodeDataUpdateWithSyncDraft, id])
+
+  return {
+    handleFormChange,
+  }
+}
+
+export const useErrorHandle = (
+  id: string,
+  data: CommonNodeType,
+) => {
+  const initCollapsed = useMemo(() => {
+    if (data.error_strategy === ErrorHandleTypeEnum.none)
+      return true
+
+    return false
+  }, [data.error_strategy])
+  const [collapsed, setCollapsed] = useState(initCollapsed)
+  const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+  const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
+
+  const handleErrorHandleTypeChange = useCallback((value: ErrorHandleTypeEnum, data: CommonNodeType) => {
+    if (data.error_strategy === value)
+      return
+
+    if (value === ErrorHandleTypeEnum.none) {
+      handleNodeDataUpdateWithSyncDraft({
+        id,
+        data: {
+          error_strategy: undefined,
+          default_value: undefined,
+        },
+      })
+      setCollapsed(true)
+      handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
+    }
+
+    if (value === ErrorHandleTypeEnum.failBranch) {
+      handleNodeDataUpdateWithSyncDraft({
+        id,
+        data: {
+          error_strategy: value,
+          default_value: undefined,
+        },
+      })
+      setCollapsed(false)
+    }
+
+    if (value === ErrorHandleTypeEnum.defaultValue) {
+      handleNodeDataUpdateWithSyncDraft({
+        id,
+        data: {
+          error_strategy: value,
+          default_value: getDefaultValue(data),
+        },
+      })
+      setCollapsed(false)
+      handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
+    }
+  }, [id, handleNodeDataUpdateWithSyncDraft, handleEdgeDeleteByDeleteBranch])
+
+  return {
+    collapsed,
+    setCollapsed,
+    handleErrorHandleTypeChange,
+  }
+}

+ 13 - 0
web/app/components/workflow/nodes/_base/components/error-handle/types.ts

@@ -0,0 +1,13 @@
+import type { VarType } from '@/app/components/workflow/types'
+
+export enum ErrorHandleTypeEnum {
+  none = 'none',
+  failBranch = 'fail-branch',
+  defaultValue = 'default-value',
+}
+
+export type DefaultValueForm = {
+  key: string
+  type: VarType
+  value?: any
+}

+ 83 - 0
web/app/components/workflow/nodes/_base/components/error-handle/utils.ts

@@ -0,0 +1,83 @@
+import type { CommonNodeType } from '@/app/components/workflow/types'
+import {
+  BlockEnum,
+  VarType,
+} from '@/app/components/workflow/types'
+import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
+
+const getDefaultValueByType = (type: VarType) => {
+  if (type === VarType.string)
+    return ''
+
+  if (type === VarType.number)
+    return 0
+
+  if (type === VarType.object)
+    return '{}'
+
+  if (type === VarType.arrayObject || type === VarType.arrayString || type === VarType.arrayNumber || type === VarType.arrayFile)
+    return '[]'
+
+  return ''
+}
+
+export const getDefaultValue = (data: CommonNodeType) => {
+  const { type } = data
+
+  if (type === BlockEnum.LLM) {
+    return [{
+      key: 'text',
+      type: VarType.string,
+      value: getDefaultValueByType(VarType.string),
+    }]
+  }
+
+  if (type === BlockEnum.HttpRequest) {
+    return [
+      {
+        key: 'body',
+        type: VarType.string,
+        value: getDefaultValueByType(VarType.string),
+      },
+      {
+        key: 'status_code',
+        type: VarType.number,
+        value: getDefaultValueByType(VarType.number),
+      },
+      {
+        key: 'headers',
+        type: VarType.object,
+        value: getDefaultValueByType(VarType.object),
+      },
+    ]
+  }
+
+  if (type === BlockEnum.Tool) {
+    return [
+      {
+        key: 'text',
+        type: VarType.string,
+        value: getDefaultValueByType(VarType.string),
+      },
+      {
+        key: 'json',
+        type: VarType.arrayObject,
+        value: getDefaultValueByType(VarType.arrayObject),
+      },
+    ]
+  }
+
+  if (type === BlockEnum.Code) {
+    const { outputs } = data as CodeNodeType
+
+    return Object.keys(outputs).map((key) => {
+      return {
+        key,
+        type: outputs[key].type,
+        value: getDefaultValueByType(outputs[key].type),
+      }
+    })
+  }
+
+  return []
+}

+ 14 - 6
web/app/components/workflow/nodes/_base/components/next-step/add.tsx

@@ -1,6 +1,7 @@
 import {
 import {
   memo,
   memo,
   useCallback,
   useCallback,
+  useMemo,
   useState,
   useState,
 } from 'react'
 } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
@@ -24,12 +25,14 @@ type AddProps = {
   nodeData: CommonNodeType
   nodeData: CommonNodeType
   sourceHandle: string
   sourceHandle: string
   isParallel?: boolean
   isParallel?: boolean
+  isFailBranch?: boolean
 }
 }
 const Add = ({
 const Add = ({
   nodeId,
   nodeId,
   nodeData,
   nodeData,
   sourceHandle,
   sourceHandle,
   isParallel,
   isParallel,
+  isFailBranch,
 }: AddProps) => {
 }: AddProps) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const [open, setOpen] = useState(false)
   const [open, setOpen] = useState(false)
@@ -58,6 +61,15 @@ const Add = ({
     setOpen(newOpen)
     setOpen(newOpen)
   }, [checkParallelLimit, nodeId, sourceHandle])
   }, [checkParallelLimit, nodeId, sourceHandle])
 
 
+  const tip = useMemo(() => {
+    if (isFailBranch)
+      return t('workflow.common.addFailureBranch')
+
+    if (isParallel)
+      return t('workflow.common.addParallelNode')
+
+    return t('workflow.panel.selectNextStep')
+  }, [isFailBranch, isParallel, t])
   const renderTrigger = useCallback((open: boolean) => {
   const renderTrigger = useCallback((open: boolean) => {
     return (
     return (
       <div
       <div
@@ -72,15 +84,11 @@ const Add = ({
           <RiAddLine className='w-3 h-3' />
           <RiAddLine className='w-3 h-3' />
         </div>
         </div>
         <div className='flex items-center uppercase'>
         <div className='flex items-center uppercase'>
-          {
-            isParallel
-              ? t('workflow.common.addParallelNode')
-              : t('workflow.panel.selectNextStep')
-          }
+          {tip}
         </div>
         </div>
       </div>
       </div>
     )
     )
-  }, [t, nodesReadOnly, isParallel])
+  }, [nodesReadOnly, tip])
 
 
   return (
   return (
     <BlockSelector
     <BlockSelector

+ 12 - 2
web/app/components/workflow/nodes/_base/components/next-step/container.tsx

@@ -4,6 +4,7 @@ import type {
   CommonNodeType,
   CommonNodeType,
   Node,
   Node,
 } from '@/app/components/workflow/types'
 } from '@/app/components/workflow/types'
+import cn from '@/utils/classnames'
 
 
 type ContainerProps = {
 type ContainerProps = {
   nodeId: string
   nodeId: string
@@ -11,6 +12,7 @@ type ContainerProps = {
   sourceHandle: string
   sourceHandle: string
   nextNodes: Node[]
   nextNodes: Node[]
   branchName?: string
   branchName?: string
+  isFailBranch?: boolean
 }
 }
 
 
 const Container = ({
 const Container = ({
@@ -19,13 +21,20 @@ const Container = ({
   sourceHandle,
   sourceHandle,
   nextNodes,
   nextNodes,
   branchName,
   branchName,
+  isFailBranch,
 }: ContainerProps) => {
 }: ContainerProps) => {
   return (
   return (
-    <div className='p-0.5 space-y-0.5 rounded-[10px] bg-background-section-burn'>
+    <div className={cn(
+      'p-0.5 space-y-0.5 rounded-[10px] bg-background-section-burn',
+      isFailBranch && 'border-[0.5px] border-state-warning-hover-alt bg-state-warning-hover',
+    )}>
       {
       {
         branchName && (
         branchName && (
           <div
           <div
-            className='flex items-center px-2 system-2xs-semibold-uppercase text-text-tertiary truncate'
+            className={cn(
+              'flex items-center px-2 system-2xs-semibold-uppercase text-text-tertiary truncate',
+              isFailBranch && 'text-text-warning',
+            )}
             title={branchName}
             title={branchName}
           >
           >
             {branchName}
             {branchName}
@@ -44,6 +53,7 @@ const Container = ({
       }
       }
       <Add
       <Add
         isParallel={!!nextNodes.length}
         isParallel={!!nextNodes.length}
+        isFailBranch={isFailBranch}
         nodeId={nodeId}
         nodeId={nodeId}
         nodeData={nodeData}
         nodeData={nodeData}
         sourceHandle={sourceHandle}
         sourceHandle={sourceHandle}

+ 55 - 35
web/app/components/workflow/nodes/_base/components/next-step/index.tsx

@@ -14,6 +14,8 @@ import type {
 import { BlockEnum } from '../../../../types'
 import { BlockEnum } from '../../../../types'
 import Line from './line'
 import Line from './line'
 import Container from './container'
 import Container from './container'
+import { hasErrorHandleNode } from '@/app/components/workflow/utils'
+import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
 
 
 type NextStepProps = {
 type NextStepProps = {
   selectedNode: Node
   selectedNode: Node
@@ -28,25 +30,54 @@ const NextStep = ({
   const branches = useMemo(() => {
   const branches = useMemo(() => {
     return data._targetBranches || []
     return data._targetBranches || []
   }, [data])
   }, [data])
-  const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier
   const edges = useEdges()
   const edges = useEdges()
   const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
   const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
   const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
   const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
 
 
-  const branchesOutgoers = useMemo(() => {
-    if (!branches?.length)
-      return []
+  const list = useMemo(() => {
+    let items = []
+    if (branches?.length) {
+      items = branches.map((branch, index) => {
+        const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
+        const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
 
 
-    return branches.map((branch) => {
-      const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
+        return {
+          branch: {
+            ...branch,
+            name: data.type === BlockEnum.QuestionClassifier ? `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}` : branch.name,
+          },
+          nextNodes,
+        }
+      })
+    }
+    else {
+      const connected = connectedEdges.filter(edge => edge.sourceHandle === 'source')
       const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
       const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
 
 
-      return {
-        branch,
+      items = [{
+        branch: {
+          id: '',
+          name: '',
+        },
         nextNodes,
         nextNodes,
+      }]
+
+      if (data.error_strategy === ErrorHandleTypeEnum.failBranch && hasErrorHandleNode(data.type)) {
+        const connected = connectedEdges.filter(edge => edge.sourceHandle === ErrorHandleTypeEnum.failBranch)
+        const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
+
+        items.push({
+          branch: {
+            id: ErrorHandleTypeEnum.failBranch,
+            name: t('workflow.common.onFailure'),
+          },
+          nextNodes,
+        })
       }
       }
-    })
-  }, [branches, connectedEdges, outgoers])
+    }
+
+    return items
+  }, [branches, connectedEdges, data.error_strategy, data.type, outgoers, t])
 
 
   return (
   return (
     <div className='flex py-1'>
     <div className='flex py-1'>
@@ -57,34 +88,23 @@ const NextStep = ({
         />
         />
       </div>
       </div>
       <Line
       <Line
-        list={nodeWithBranches ? branchesOutgoers.map(item => item.nextNodes.length + 1) : [1]}
+        list={list.length ? list.map(item => item.nextNodes.length + 1) : [1]}
       />
       />
       <div className='grow space-y-2'>
       <div className='grow space-y-2'>
         {
         {
-          !nodeWithBranches && (
-            <Container
-              nodeId={selectedNode!.id}
-              nodeData={selectedNode!.data}
-              sourceHandle='source'
-              nextNodes={outgoers}
-            />
-          )
-        }
-        {
-          nodeWithBranches && (
-            branchesOutgoers.map((item, index) => {
-              return (
-                <Container
-                  key={item.branch.id}
-                  nodeId={selectedNode!.id}
-                  nodeData={selectedNode!.data}
-                  sourceHandle={item.branch.id}
-                  nextNodes={item.nextNodes}
-                  branchName={item.branch.name || `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}`}
-                />
-              )
-            })
-          )
+          list.map((item, index) => {
+            return (
+              <Container
+                key={index}
+                nodeId={selectedNode!.id}
+                nodeData={selectedNode!.data}
+                sourceHandle={item.branch.id}
+                nextNodes={item.nextNodes}
+                branchName={item.branch.name}
+                isFailBranch={item.branch.id === ErrorHandleTypeEnum.failBranch}
+              />
+            )
+          })
         }
         }
       </div>
       </div>
     </div>
     </div>

+ 28 - 16
web/app/components/workflow/nodes/_base/components/node-handle.tsx

@@ -10,7 +10,10 @@ import {
   Position,
   Position,
 } from 'reactflow'
 } from 'reactflow'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { BlockEnum } from '../../../types'
+import {
+  BlockEnum,
+  NodeRunningStatus,
+} from '../../../types'
 import type { Node } from '../../../types'
 import type { Node } from '../../../types'
 import BlockSelector from '../../../block-selector'
 import BlockSelector from '../../../block-selector'
 import type { ToolDefaultValue } from '../../../block-selector/types'
 import type { ToolDefaultValue } from '../../../block-selector/types'
@@ -24,11 +27,13 @@ import {
 import {
 import {
   useStore,
   useStore,
 } from '../../../store'
 } from '../../../store'
+import cn from '@/utils/classnames'
 
 
 type NodeHandleProps = {
 type NodeHandleProps = {
   handleId: string
   handleId: string
   handleClassName?: string
   handleClassName?: string
   nodeSelectorClassName?: string
   nodeSelectorClassName?: string
+  showExceptionStatus?: boolean
 } & Pick<Node, 'id' | 'data'>
 } & Pick<Node, 'id' | 'data'>
 
 
 export const NodeTargetHandle = memo(({
 export const NodeTargetHandle = memo(({
@@ -72,14 +77,17 @@ export const NodeTargetHandle = memo(({
         id={handleId}
         id={handleId}
         type='target'
         type='target'
         position={Position.Left}
         position={Position.Left}
-        className={`
-          !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
-          after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-primary-500
-          hover:scale-125 transition-all
-          ${!connected && 'after:opacity-0'}
-          ${data.type === BlockEnum.Start && 'opacity-0'}
-          ${handleClassName}
-        `}
+        className={cn(
+          '!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]',
+          'after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-workflow-link-line-handle',
+          'hover:scale-125 transition-all',
+          data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
+          data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
+          data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
+          !connected && 'after:opacity-0',
+          data.type === BlockEnum.Start && 'opacity-0',
+          handleClassName,
+        )}
         isConnectable={isConnectable}
         isConnectable={isConnectable}
         onClick={handleHandleClick}
         onClick={handleHandleClick}
       >
       >
@@ -114,6 +122,7 @@ export const NodeSourceHandle = memo(({
   handleId,
   handleId,
   handleClassName,
   handleClassName,
   nodeSelectorClassName,
   nodeSelectorClassName,
+  showExceptionStatus,
 }: NodeHandleProps) => {
 }: NodeHandleProps) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
   const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
@@ -157,13 +166,16 @@ export const NodeSourceHandle = memo(({
       id={handleId}
       id={handleId}
       type='source'
       type='source'
       position={Position.Right}
       position={Position.Right}
-      className={`
-        group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
-        after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
-        hover:scale-125 transition-all
-        ${!connected && 'after:opacity-0'}
-        ${handleClassName}
-      `}
+      className={cn(
+        'group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]',
+        'after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-workflow-link-line-handle',
+        'hover:scale-125 transition-all',
+        data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
+        data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
+        showExceptionStatus && data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
+        !connected && 'after:opacity-0',
+        handleClassName,
+      )}
       isConnectable={isConnectable}
       isConnectable={isConnectable}
       onClick={handleHandleClick}
       onClick={handleHandleClick}
     >
     >

+ 4 - 22
web/app/components/workflow/nodes/_base/components/output-vars.tsx

@@ -2,11 +2,7 @@
 import type { FC } from 'react'
 import type { FC } from 'react'
 import React from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { useBoolean } from 'ahooks'
-import {
-  RiArrowDownSLine,
-} from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
 
 
 type Props = {
 type Props = {
   className?: string
   className?: string
@@ -15,28 +11,14 @@ type Props = {
 }
 }
 
 
 const OutputVars: FC<Props> = ({
 const OutputVars: FC<Props> = ({
-  className,
   title,
   title,
   children,
   children,
 }) => {
 }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
-  const [isFold, {
-    toggle: toggleFold,
-  }] = useBoolean(true)
   return (
   return (
-    <div>
-      <div
-        onClick={toggleFold}
-        className={cn(className, 'flex justify-between system-sm-semibold-uppercase text-text-secondary cursor-pointer')}>
-        <div>{title || t('workflow.nodes.common.outputVars')}</div>
-        <RiArrowDownSLine className='w-4 h-4 text-text-tertiary transform transition-transform' style={{ transform: isFold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
-      </div>
-      {!isFold && (
-        <div className='mt-2 space-y-1'>
-          {children}
-        </div>
-      )}
-    </div>
+    <FieldCollapse title={title || t('workflow.nodes.common.outputVars')}>
+      {children}
+    </FieldCollapse>
   )
   )
 }
 }
 type VarItemProps = {
 type VarItemProps = {

+ 4 - 2
web/app/components/workflow/nodes/_base/components/variable-tag.tsx

@@ -17,6 +17,7 @@ import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others
 import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import Tooltip from '@/app/components/base/tooltip'
 import Tooltip from '@/app/components/base/tooltip'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
+import { isExceptionVariable } from '@/app/components/workflow/utils'
 
 
 type VariableTagProps = {
 type VariableTagProps = {
   valueSelector: ValueSelector
   valueSelector: ValueSelector
@@ -45,6 +46,7 @@ const VariableTag = ({
   const isValid = Boolean(node) || isEnv || isChatVar
   const isValid = Boolean(node) || isEnv || isChatVar
 
 
   const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
   const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
+  const isException = isExceptionVariable(variableName, node?.data.type)
 
 
   const { t } = useTranslation()
   const { t } = useTranslation()
   return (
   return (
@@ -67,12 +69,12 @@ const VariableTag = ({
             </>
             </>
           )}
           )}
           <Line3 className='shrink-0 mx-0.5' />
           <Line3 className='shrink-0 mx-0.5' />
-          <Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent' />
+          <Variable02 className={cn('shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />
         </>)}
         </>)}
         {isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
         {isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
         {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
         {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
         <div
         <div
-          className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
+          className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary', isException && 'text-text-warning')}
           title={variableName}
           title={variableName}
         >
         >
           {variableName}
           {variableName}

+ 18 - 0
web/app/components/workflow/nodes/_base/components/variable/utils.ts

@@ -315,6 +315,24 @@ const formatItem = (
     }
     }
   }
   }
 
 
+  const { error_strategy } = data
+
+  if (error_strategy) {
+    res.vars = [
+      ...res.vars,
+      {
+        variable: 'error_message',
+        type: VarType.string,
+        isException: true,
+      },
+      {
+        variable: 'error_type',
+        type: VarType.string,
+        isException: true,
+      },
+    ]
+  }
+
   const selector = [id]
   const selector = [id]
   res.vars = res.vars.filter((v) => {
   res.vars = res.vars.filter((v) => {
     const isCurrentMatched = filterVar(v, (() => {
     const isCurrentMatched = filterVar(v, (() => {

+ 6 - 3
web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx

@@ -36,6 +36,7 @@ import TypeSelector from '@/app/components/workflow/nodes/_base/components/selec
 import AddButton from '@/app/components/base/button/add-button'
 import AddButton from '@/app/components/base/button/add-button'
 import Badge from '@/app/components/base/badge'
 import Badge from '@/app/components/base/badge'
 import Tooltip from '@/app/components/base/tooltip'
 import Tooltip from '@/app/components/base/tooltip'
+import { isExceptionVariable } from '@/app/components/workflow/utils'
 
 
 const TRIGGER_DEFAULT_WIDTH = 227
 const TRIGGER_DEFAULT_WIDTH = 227
 
 
@@ -224,16 +225,18 @@ const VarReferencePicker: FC<Props> = ({
     isConstant: !!isConstant,
     isConstant: !!isConstant,
   })
   })
 
 
-  const { isEnv, isChatVar, isValidVar } = useMemo(() => {
+  const { isEnv, isChatVar, isValidVar, isException } = useMemo(() => {
     const isEnv = isENV(value as ValueSelector)
     const isEnv = isENV(value as ValueSelector)
     const isChatVar = isConversationVar(value as ValueSelector)
     const isChatVar = isConversationVar(value as ValueSelector)
     const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar
     const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar
+    const isException = isExceptionVariable(varName, outputVarNode?.type)
     return {
     return {
       isEnv,
       isEnv,
       isChatVar,
       isChatVar,
       isValidVar,
       isValidVar,
+      isException,
     }
     }
-  }, [value, outputVarNode])
+  }, [value, outputVarNode, varName])
 
 
   // 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
   // 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
   const availableWidth = triggerWidth - 56
   const availableWidth = triggerWidth - 56
@@ -335,7 +338,7 @@ const VarReferencePicker: FC<Props> = ({
                                     {!hasValue && <Variable02 className='w-3.5 h-3.5' />}
                                     {!hasValue && <Variable02 className='w-3.5 h-3.5' />}
                                     {isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
                                     {isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
                                     {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
                                     {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
-                                    <div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700')} title={varName} style={{
+                                    <div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
                                       maxWidth: maxVarNameWidth,
                                       maxWidth: maxVarNameWidth,
                                     }}>{varName}</div>
                                     }}>{varName}</div>
                                   </div>
                                   </div>

+ 5 - 1
web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx

@@ -37,6 +37,7 @@ type ItemProps = {
   onHovering?: (value: boolean) => void
   onHovering?: (value: boolean) => void
   itemWidth?: number
   itemWidth?: number
   isSupportFileVar?: boolean
   isSupportFileVar?: boolean
+  isException?: boolean
 }
 }
 
 
 const Item: FC<ItemProps> = ({
 const Item: FC<ItemProps> = ({
@@ -48,6 +49,7 @@ const Item: FC<ItemProps> = ({
   onHovering,
   onHovering,
   itemWidth,
   itemWidth,
   isSupportFileVar,
   isSupportFileVar,
+  isException,
 }) => {
 }) => {
   const isFile = itemData.type === VarType.file
   const isFile = itemData.type === VarType.file
   const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
   const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
@@ -109,7 +111,7 @@ const Item: FC<ItemProps> = ({
           onClick={handleChosen}
           onClick={handleChosen}
         >
         >
           <div className='flex items-center w-0 grow'>
           <div className='flex items-center w-0 grow'>
-            {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />}
+            {!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />}
             {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
             {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
             {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
             {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
             {!isEnv && !isChatVar && (
             {!isEnv && !isChatVar && (
@@ -216,6 +218,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
             onChange={onChange}
             onChange={onChange}
             onHovering={setIsChildrenHovering}
             onHovering={setIsChildrenHovering}
             isSupportFileVar={isSupportFileVar}
             isSupportFileVar={isSupportFileVar}
+            isException={v.isException}
           />
           />
         ))
         ))
       }
       }
@@ -312,6 +315,7 @@ const VarReferenceVars: FC<Props> = ({
                     onChange={onChange}
                     onChange={onChange}
                     itemWidth={itemWidth}
                     itemWidth={itemWidth}
                     isSupportFileVar={isSupportFileVar}
                     isSupportFileVar={isSupportFileVar}
+                    isException={v.isException}
                   />
                   />
                 ))}
                 ))}
               </div>))
               </div>))

+ 22 - 3
web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts

@@ -1,12 +1,22 @@
 import { useCallback, useState } from 'react'
 import { useCallback, useState } from 'react'
 import produce from 'immer'
 import produce from 'immer'
 import { useBoolean } from 'ahooks'
 import { useBoolean } from 'ahooks'
-import { type OutputVar } from '../../code/types'
-import type { ValueSelector } from '@/app/components/workflow/types'
-import { VarType } from '@/app/components/workflow/types'
+import type {
+  CodeNodeType,
+  OutputVar,
+} from '../../code/types'
+import type {
+  ValueSelector,
+} from '@/app/components/workflow/types'
+import {
+  BlockEnum,
+  VarType,
+} from '@/app/components/workflow/types'
 import {
 import {
   useWorkflow,
   useWorkflow,
 } from '@/app/components/workflow/hooks'
 } from '@/app/components/workflow/hooks'
+import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
+import { getDefaultValue } from '@/app/components/workflow/nodes/_base/components/error-handle/utils'
 
 
 type Params<T> = {
 type Params<T> = {
   id: string
   id: string
@@ -29,6 +39,9 @@ function useOutputVarList<T>({
   const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => {
   const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => {
     const newInputs = produce(inputs, (draft: any) => {
     const newInputs = produce(inputs, (draft: any) => {
       draft[varKey] = newVars
       draft[varKey] = newVars
+
+      if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs')
+        draft.default_value = getDefaultValue(draft as any)
     })
     })
     setInputs(newInputs)
     setInputs(newInputs)
 
 
@@ -59,6 +72,9 @@ function useOutputVarList<T>({
           children: null,
           children: null,
         },
         },
       }
       }
+
+      if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs')
+        draft.default_value = getDefaultValue(draft as any)
     })
     })
     setInputs(newInputs)
     setInputs(newInputs)
     onOutputKeyOrdersChange([...outputKeyOrders, newKey])
     onOutputKeyOrdersChange([...outputKeyOrders, newKey])
@@ -84,6 +100,9 @@ function useOutputVarList<T>({
 
 
     const newInputs = produce(inputs, (draft: any) => {
     const newInputs = produce(inputs, (draft: any) => {
       delete draft[varKey][key]
       delete draft[varKey][key]
+
+      if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs')
+        draft.default_value = getDefaultValue(draft as any)
     })
     })
     setInputs(newInputs)
     setInputs(newInputs)
     onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index))
     onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index))

+ 29 - 9
web/app/components/workflow/nodes/_base/node.tsx

@@ -10,8 +10,9 @@ import {
   useRef,
   useRef,
 } from 'react'
 } from 'react'
 import {
 import {
-  RiCheckboxCircleLine,
-  RiErrorWarningLine,
+  RiAlertFill,
+  RiCheckboxCircleFill,
+  RiErrorWarningFill,
   RiLoader2Line,
   RiLoader2Line,
 } from '@remixicon/react'
 } from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
@@ -24,6 +25,7 @@ import {
   useNodesReadOnly,
   useNodesReadOnly,
   useToolIcon,
   useToolIcon,
 } from '../../hooks'
 } from '../../hooks'
+import { hasErrorHandleNode } from '../../utils'
 import { useNodeIterationInteractions } from '../iteration/use-interactions'
 import { useNodeIterationInteractions } from '../iteration/use-interactions'
 import type { IterationNodeType } from '../iteration/types'
 import type { IterationNodeType } from '../iteration/types'
 import {
 import {
@@ -32,6 +34,7 @@ import {
 } from './components/node-handle'
 } from './components/node-handle'
 import NodeResizer from './components/node-resizer'
 import NodeResizer from './components/node-resizer'
 import NodeControl from './components/node-control'
 import NodeControl from './components/node-control'
+import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
 import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
 import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
 import BlockIcon from '@/app/components/workflow/block-icon'
 import BlockIcon from '@/app/components/workflow/block-icon'
@@ -71,11 +74,13 @@ const BaseNode: FC<BaseNodeProps> = ({
     showRunningBorder,
     showRunningBorder,
     showSuccessBorder,
     showSuccessBorder,
     showFailedBorder,
     showFailedBorder,
+    showExceptionBorder,
   } = useMemo(() => {
   } = useMemo(() => {
     return {
     return {
       showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
       showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
       showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
       showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
       showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
       showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
+      showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
     }
     }
   }, [data._runningStatus, showSelectedBorder])
   }, [data._runningStatus, showSelectedBorder])
 
 
@@ -85,6 +90,7 @@ const BaseNode: FC<BaseNodeProps> = ({
         'flex border-[2px] rounded-2xl',
         'flex border-[2px] rounded-2xl',
         showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
         showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
         !showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
         !showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
+        data._waitingRun && 'opacity-70',
       )}
       )}
       ref={nodeRef}
       ref={nodeRef}
       style={{
       style={{
@@ -99,9 +105,10 @@ const BaseNode: FC<BaseNodeProps> = ({
           data.type !== BlockEnum.Iteration && 'w-[240px] bg-workflow-block-bg',
           data.type !== BlockEnum.Iteration && 'w-[240px] bg-workflow-block-bg',
           data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-[#fcfdff]/80',
           data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-[#fcfdff]/80',
           !data._runningStatus && 'hover:shadow-lg',
           !data._runningStatus && 'hover:shadow-lg',
-          showRunningBorder && '!border-primary-500',
-          showSuccessBorder && '!border-[#12B76A]',
-          showFailedBorder && '!border-[#F04438]',
+          showRunningBorder && '!border-state-accent-solid',
+          showSuccessBorder && '!border-state-success-solid',
+          showFailedBorder && '!border-state-destructive-solid',
+          showExceptionBorder && '!border-state-warning-solid',
           data._isBundled && '!shadow-lg',
           data._isBundled && '!shadow-lg',
         )}
         )}
       >
       >
@@ -192,24 +199,29 @@ const BaseNode: FC<BaseNodeProps> = ({
           </div>
           </div>
           {
           {
             data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
             data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
-              <div className='mr-1.5 text-xs font-medium text-primary-600'>
+              <div className='mr-1.5 text-xs font-medium text-text-accent'>
                 {data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength}
                 {data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength}
               </div>
               </div>
             )
             )
           }
           }
           {
           {
             (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
             (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
-              <RiLoader2Line className='w-3.5 h-3.5 text-primary-600 animate-spin' />
+              <RiLoader2Line className='w-3.5 h-3.5 text-text-accent animate-spin' />
             )
             )
           }
           }
           {
           {
             data._runningStatus === NodeRunningStatus.Succeeded && (
             data._runningStatus === NodeRunningStatus.Succeeded && (
-              <RiCheckboxCircleLine className='w-3.5 h-3.5 text-[#12B76A]' />
+              <RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' />
             )
             )
           }
           }
           {
           {
             data._runningStatus === NodeRunningStatus.Failed && (
             data._runningStatus === NodeRunningStatus.Failed && (
-              <RiErrorWarningLine className='w-3.5 h-3.5 text-[#F04438]' />
+              <RiErrorWarningFill className='w-3.5 h-3.5 text-text-destructive' />
+            )
+          }
+          {
+            data._runningStatus === NodeRunningStatus.Exception && (
+              <RiAlertFill className='w-3.5 h-3.5 text-text-warning-secondary' />
             )
             )
           }
           }
         </div>
         </div>
@@ -225,6 +237,14 @@ const BaseNode: FC<BaseNodeProps> = ({
             </div>
             </div>
           )
           )
         }
         }
+        {
+          hasErrorHandleNode(data.type) && (
+            <ErrorHandleOnNode
+              id={id}
+              data={data}
+            />
+          )
+        }
         {
         {
           data.desc && data.type !== BlockEnum.Iteration && (
           data.desc && data.type !== BlockEnum.Iteration && (
             <div className='px-3 pt-1 pb-2 system-xs-regular text-text-tertiary whitespace-pre-line break-words'>
             <div className='px-3 pt-1 pb-2 system-xs-regular text-text-tertiary whitespace-pre-line break-words'>

+ 14 - 2
web/app/components/workflow/nodes/_base/panel.tsx

@@ -20,6 +20,7 @@ import {
   DescriptionInput,
   DescriptionInput,
   TitleInput,
   TitleInput,
 } from './components/title-description-input'
 } from './components/title-description-input'
+import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel'
 import { useResizePanel } from './hooks/use-resize-panel'
 import { useResizePanel } from './hooks/use-resize-panel'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
 import BlockIcon from '@/app/components/workflow/block-icon'
 import BlockIcon from '@/app/components/workflow/block-icon'
@@ -34,7 +35,10 @@ import {
   useWorkflow,
   useWorkflow,
   useWorkflowHistory,
   useWorkflowHistory,
 } from '@/app/components/workflow/hooks'
 } from '@/app/components/workflow/hooks'
-import { canRunBySingle } from '@/app/components/workflow/utils'
+import {
+  canRunBySingle,
+  hasErrorHandleNode,
+} from '@/app/components/workflow/utils'
 import Tooltip from '@/app/components/base/tooltip'
 import Tooltip from '@/app/components/base/tooltip'
 import type { Node } from '@/app/components/workflow/types'
 import type { Node } from '@/app/components/workflow/types'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import { useStore as useAppStore } from '@/app/components/app/store'
@@ -161,9 +165,17 @@ const BasePanel: FC<BasePanelProps> = ({
             />
             />
           </div>
           </div>
         </div>
         </div>
-        <div className='py-2'>
+        <div>
           {cloneElement(children, { id, data })}
           {cloneElement(children, { id, data })}
         </div>
         </div>
+        {
+          hasErrorHandleNode(data.type) && (
+            <ErrorHandleOnPanel
+              id={id}
+              data={data}
+            />
+          )
+        }
         {
         {
           !!availableNextBlocks.length && (
           !!availableNextBlocks.length && (
             <div className='p-4 border-t-[0.5px] border-t-black/5'>
             <div className='p-4 border-t-[0.5px] border-t-black/5'>

+ 1 - 1
web/app/components/workflow/nodes/document-extractor/panel.tsx

@@ -72,7 +72,7 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
         </Field>
         </Field>
       </div>
       </div>
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-2'>
+      <div>
         <OutputVars>
         <OutputVars>
           <VarItem
           <VarItem
             name='text'
             name='text'

+ 35 - 52
web/app/components/workflow/nodes/http/components/timeout/index.tsx

@@ -2,11 +2,9 @@
 import type { FC } from 'react'
 import type { FC } from 'react'
 import React from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { useBoolean } from 'ahooks'
 import type { Timeout as TimeoutPayloadType } from '../../types'
 import type { Timeout as TimeoutPayloadType } from '../../types'
-import cn from '@/utils/classnames'
 import Input from '@/app/components/base/input'
 import Input from '@/app/components/base/input'
-import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
+import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
 
 
 type Props = {
 type Props = {
   readonly: boolean
   readonly: boolean
@@ -53,58 +51,43 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {}
   const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {}
 
 
-  const [isFold, {
-    toggle: toggleFold,
-  }] = useBoolean(true)
-
   return (
   return (
-    <>
-      <div>
-        <div
-          onClick={toggleFold}
-          className={cn('flex justify-between leading-[18px] text-[13px] font-semibold text-gray-700 uppercase cursor-pointer')}>
-          <div>{t(`${i18nPrefix}.timeout.title`)}</div>
-          <ChevronRight className='w-4 h-4 text-gray-500 transform transition-transform' style={{ transform: isFold ? 'rotate(0deg)' : 'rotate(90deg)' }} />
+    <FieldCollapse title={t(`${i18nPrefix}.timeout.title`)}>
+      <div className='mt-2 space-y-1'>
+        <div className="space-y-3">
+          <InputField
+            title={t('workflow.nodes.http.timeout.connectLabel')!}
+            description={t('workflow.nodes.http.timeout.connectPlaceholder')!}
+            placeholder={t('workflow.nodes.http.timeout.connectPlaceholder')!}
+            readOnly={readonly}
+            value={connect}
+            onChange={v => onChange?.({ ...payload, connect: v })}
+            min={1}
+            max={max_connect_timeout || 300}
+          />
+          <InputField
+            title={t('workflow.nodes.http.timeout.readLabel')!}
+            description={t('workflow.nodes.http.timeout.readPlaceholder')!}
+            placeholder={t('workflow.nodes.http.timeout.readPlaceholder')!}
+            readOnly={readonly}
+            value={read}
+            onChange={v => onChange?.({ ...payload, read: v })}
+            min={1}
+            max={max_read_timeout || 600}
+          />
+          <InputField
+            title={t('workflow.nodes.http.timeout.writeLabel')!}
+            description={t('workflow.nodes.http.timeout.writePlaceholder')!}
+            placeholder={t('workflow.nodes.http.timeout.writePlaceholder')!}
+            readOnly={readonly}
+            value={write}
+            onChange={v => onChange?.({ ...payload, write: v })}
+            min={1}
+            max={max_write_timeout || 600}
+          />
         </div>
         </div>
-        {!isFold && (
-          <div className='mt-2 space-y-1'>
-            <div className="space-y-3">
-              <InputField
-                title={t('workflow.nodes.http.timeout.connectLabel')!}
-                description={t('workflow.nodes.http.timeout.connectPlaceholder')!}
-                placeholder={t('workflow.nodes.http.timeout.connectPlaceholder')!}
-                readOnly={readonly}
-                value={connect}
-                onChange={v => onChange?.({ ...payload, connect: v })}
-                min={1}
-                max={max_connect_timeout || 300}
-              />
-              <InputField
-                title={t('workflow.nodes.http.timeout.readLabel')!}
-                description={t('workflow.nodes.http.timeout.readPlaceholder')!}
-                placeholder={t('workflow.nodes.http.timeout.readPlaceholder')!}
-                readOnly={readonly}
-                value={read}
-                onChange={v => onChange?.({ ...payload, read: v })}
-                min={1}
-                max={max_read_timeout || 600}
-              />
-              <InputField
-                title={t('workflow.nodes.http.timeout.writeLabel')!}
-                description={t('workflow.nodes.http.timeout.writePlaceholder')!}
-                placeholder={t('workflow.nodes.http.timeout.writePlaceholder')!}
-                readOnly={readonly}
-                value={write}
-                onChange={v => onChange?.({ ...payload, write: v })}
-                min={1}
-                max={max_write_timeout || 600}
-              />
-            </div>
-          </div>
-        )}
       </div>
       </div>
-
-    </>
+    </FieldCollapse>
   )
   )
 }
 }
 export default React.memo(Timeout)
 export default React.memo(Timeout)

+ 8 - 10
web/app/components/workflow/nodes/http/panel.tsx

@@ -65,7 +65,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
     return null
     return null
 
 
   return (
   return (
-    <div className='mt-2'>
+    <div className='pt-2'>
       <div className='px-4 pb-4 space-y-4'>
       <div className='px-4 pb-4 space-y-4'>
         <Field
         <Field
           title={t(`${i18nPrefix}.api`)}
           title={t(`${i18nPrefix}.api`)}
@@ -136,14 +136,12 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
         </Field>
         </Field>
       </div>
       </div>
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-4'>
-        <Timeout
-          nodeId={id}
-          readonly={readOnly}
-          payload={inputs.timeout}
-          onChange={setTimeout}
-        />
-      </div>
+      <Timeout
+        nodeId={id}
+        readonly={readOnly}
+        payload={inputs.timeout}
+        onChange={setTimeout}
+      />
       {(isShowAuthorization && !readOnly) && (
       {(isShowAuthorization && !readOnly) && (
         <AuthorizationModal
         <AuthorizationModal
           nodeId={id}
           nodeId={id}
@@ -154,7 +152,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
         />
         />
       )}
       )}
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-2'>
+      <div className=''>
         <OutputVars>
         <OutputVars>
           <>
           <>
             <VarItem
             <VarItem

+ 11 - 1
web/app/components/workflow/nodes/if-else/components/condition-value.tsx

@@ -3,6 +3,7 @@ import {
   useMemo,
   useMemo,
 } from 'react'
 } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
+import { useNodes } from 'reactflow'
 import { ComparisonOperator } from '../types'
 import { ComparisonOperator } from '../types'
 import {
 import {
   comparisonOperatorNotRequireValue,
   comparisonOperatorNotRequireValue,
@@ -13,6 +14,11 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
 import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
 import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
 import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
+import { isExceptionVariable } from '@/app/components/workflow/utils'
+import type {
+  CommonNodeType,
+  Node,
+} from '@/app/components/workflow/types'
 
 
 type ConditionValueProps = {
 type ConditionValueProps = {
   variableSelector: string[]
   variableSelector: string[]
@@ -27,11 +33,14 @@ const ConditionValue = ({
   value,
   value,
 }: ConditionValueProps) => {
 }: ConditionValueProps) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
+  const nodes = useNodes()
   const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
   const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
   const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
   const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
   const notHasValue = comparisonOperatorNotRequireValue(operator)
   const notHasValue = comparisonOperatorNotRequireValue(operator)
   const isEnvVar = isENV(variableSelector)
   const isEnvVar = isENV(variableSelector)
   const isChatVar = isConversationVar(variableSelector)
   const isChatVar = isConversationVar(variableSelector)
+  const node: Node<CommonNodeType> | undefined = nodes.find(n => n.id === variableSelector[0]) as Node<CommonNodeType>
+  const isException = isExceptionVariable(variableName, node?.data.type)
   const formatValue = useMemo(() => {
   const formatValue = useMemo(() => {
     if (notHasValue)
     if (notHasValue)
       return ''
       return ''
@@ -67,7 +76,7 @@ const ConditionValue = ({
 
 
   return (
   return (
     <div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
     <div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
-      {!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
+      {!isEnvVar && !isChatVar && <Variable02 className={cn('shrink-0 mr-1 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />}
       {isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
       {isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
       {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
       {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
 
 
@@ -75,6 +84,7 @@ const ConditionValue = ({
         className={cn(
         className={cn(
           'shrink-0 ml-0.5 truncate text-xs font-medium text-text-accent',
           'shrink-0 ml-0.5 truncate text-xs font-medium text-text-accent',
           !notHasValue && 'max-w-[70px]',
           !notHasValue && 'max-w-[70px]',
+          isException && 'text-text-warning',
         )}
         )}
         title={variableName}
         title={variableName}
       >
       >

+ 2 - 5
web/app/components/workflow/nodes/iteration/panel.tsx

@@ -18,7 +18,6 @@ import Switch from '@/app/components/base/switch'
 import Select from '@/app/components/base/select'
 import Select from '@/app/components/base/select'
 import Slider from '@/app/components/base/slider'
 import Slider from '@/app/components/base/slider'
 import Input from '@/app/components/base/input'
 import Input from '@/app/components/base/input'
-import Divider from '@/app/components/base/divider'
 
 
 const i18nPrefix = 'workflow.nodes.iteration'
 const i18nPrefix = 'workflow.nodes.iteration'
 
 
@@ -72,7 +71,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
   } = useConfig(id, data)
   } = useConfig(id, data)
 
 
   return (
   return (
-    <div className='mt-2'>
+    <div className='pt-2 pb-2'>
       <div className='px-4 pb-4 space-y-4'>
       <div className='px-4 pb-4 space-y-4'>
         <Field
         <Field
           title={t(`${i18nPrefix}.input`)}
           title={t(`${i18nPrefix}.input`)}
@@ -131,9 +130,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
           </Field>
           </Field>
         </div>)
         </div>)
       }
       }
-      <div className='px-4 py-2'>
-        <Divider className='h-[1px]'/>
-      </div>
+      <Split />
 
 
       <div className='px-4 py-2'>
       <div className='px-4 py-2'>
         <Field title={t(`${i18nPrefix}.errorResponseMethod`)} >
         <Field title={t(`${i18nPrefix}.errorResponseMethod`)} >

+ 2 - 2
web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx

@@ -53,7 +53,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
   }, [setRerankModelOpen])
   }, [setRerankModelOpen])
 
 
   return (
   return (
-    <div className='mt-2'>
+    <div className='pt-2'>
       <div className='px-4 pb-4 space-y-4'>
       <div className='px-4 pb-4 space-y-4'>
         {/* {JSON.stringify(inputs, null, 2)} */}
         {/* {JSON.stringify(inputs, null, 2)} */}
         <Field
         <Field
@@ -108,7 +108,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
       </div>
       </div>
 
 
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-2'>
+      <div>
         <OutputVars>
         <OutputVars>
           <>
           <>
             <VarItem
             <VarItem

+ 3 - 3
web/app/components/workflow/nodes/list-operator/panel.tsx

@@ -42,8 +42,8 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
   } = useConfig(id, data)
   } = useConfig(id, data)
 
 
   return (
   return (
-    <div className='mt-2'>
-      <div className='px-4 pb-4 space-y-4'>
+    <div className='pt-2'>
+      <div className='px-4 space-y-4'>
         <Field
         <Field
           title={t(`${i18nPrefix}.inputVar`)}
           title={t(`${i18nPrefix}.inputVar`)}
         >
         >
@@ -157,7 +157,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
         </Field>
         </Field>
         <Split />
         <Split />
       </div>
       </div>
-      <div className='px-4 pt-4 pb-2'>
+      <div>
         <OutputVars>
         <OutputVars>
           <>
           <>
             <VarItem
             <VarItem

+ 9 - 11
web/app/components/workflow/nodes/llm/panel.tsx

@@ -270,17 +270,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
         />
         />
       </div>
       </div>
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-2'>
-        <OutputVars>
-          <>
-            <VarItem
-              name='text'
-              type='string'
-              description={t(`${i18nPrefix}.outputVars.output`)}
-            />
-          </>
-        </OutputVars>
-      </div>
+      <OutputVars>
+        <>
+          <VarItem
+            name='text'
+            type='string'
+            description={t(`${i18nPrefix}.outputVars.output`)}
+          />
+        </>
+      </OutputVars>
       {isShowSingleRun && (
       {isShowSingleRun && (
         <BeforeRunForm
         <BeforeRunForm
           nodeName={inputs.title}
           nodeName={inputs.title}

+ 27 - 31
web/app/components/workflow/nodes/parameter-extractor/panel.tsx

@@ -20,6 +20,7 @@ import { InputVarType, type NodePanelProps } from '@/app/components/workflow/typ
 import Tooltip from '@/app/components/base/tooltip'
 import Tooltip from '@/app/components/base/tooltip'
 import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
 import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
 import { VarType } from '@/app/components/workflow/types'
 import { VarType } from '@/app/components/workflow/types'
+import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
 
 
 const i18nPrefix = 'workflow.nodes.parameterExtractor'
 const i18nPrefix = 'workflow.nodes.parameterExtractor'
 const i18nCommonPrefix = 'workflow.common'
 const i18nCommonPrefix = 'workflow.common'
@@ -67,8 +68,8 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
   const model = inputs.model
   const model = inputs.model
 
 
   return (
   return (
-    <div className='mt-2'>
-      <div className='px-4 pb-4 space-y-4'>
+    <div className='pt-2'>
+      <div className='px-4 space-y-4'>
         <Field
         <Field
           title={t(`${i18nCommonPrefix}.model`)}
           title={t(`${i18nCommonPrefix}.model`)}
         >
         >
@@ -157,38 +158,33 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
           nodesOutputVars={availableVars}
           nodesOutputVars={availableVars}
           availableNodes={availableNodesWithParent}
           availableNodes={availableNodesWithParent}
         />
         />
-        <Field
-          title={t(`${i18nPrefix}.advancedSetting`)}
-          supportFold
-        >
-          <>
-
-            {/* Memory */}
-            {isChatMode && (
-              <div className='mt-4'>
-                <MemoryConfig
-                  readonly={readOnly}
-                  config={{ data: inputs.memory }}
-                  onChange={handleMemoryChange}
-                  canSetRoleName={isCompletionModel}
-                />
-              </div>
-            )}
-            {isSupportFunctionCall && (
-              <div className='mt-2'>
-                <ReasoningModePicker
-                  type={inputs.reasoning_mode}
-                  onChange={handleReasoningModeChange}
-                />
-              </div>
-            )}
-          </>
-        </Field>
-
       </div>
       </div>
+      <FieldCollapse title={t(`${i18nPrefix}.advancedSetting`)}>
+        <>
+          {/* Memory */}
+          {isChatMode && (
+            <div className='mt-4'>
+              <MemoryConfig
+                readonly={readOnly}
+                config={{ data: inputs.memory }}
+                onChange={handleMemoryChange}
+                canSetRoleName={isCompletionModel}
+              />
+            </div>
+          )}
+          {isSupportFunctionCall && (
+            <div className='mt-2'>
+              <ReasoningModePicker
+                type={inputs.reasoning_mode}
+                onChange={handleReasoningModeChange}
+              />
+            </div>
+          )}
+        </>
+      </FieldCollapse>
       {inputs.parameters?.length > 0 && (<>
       {inputs.parameters?.length > 0 && (<>
         <Split />
         <Split />
-        <div className='px-4 pt-4 pb-2'>
+        <div>
           <OutputVars>
           <OutputVars>
             <>
             <>
               {inputs.parameters.map((param, index) => (
               {inputs.parameters.map((param, index) => (

+ 10 - 0
web/app/components/workflow/nodes/question-classifier/default.ts

@@ -26,6 +26,16 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
         name: '',
         name: '',
       },
       },
     ],
     ],
+    _targetBranches: [
+      {
+        id: '1',
+        name: '',
+      },
+      {
+        id: '2',
+        name: '',
+      },
+    ],
     vision: {
     vision: {
       enabled: false,
       enabled: false,
     },
     },

+ 22 - 21
web/app/components/workflow/nodes/question-classifier/panel.tsx

@@ -14,6 +14,7 @@ import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/befo
 import ResultPanel from '@/app/components/workflow/run/result-panel'
 import ResultPanel from '@/app/components/workflow/run/result-panel'
 import Split from '@/app/components/workflow/nodes/_base/components/split'
 import Split from '@/app/components/workflow/nodes/_base/components/split'
 import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
 import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
+import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
 
 
 const i18nPrefix = 'workflow.nodes.questionClassifiers'
 const i18nPrefix = 'workflow.nodes.questionClassifiers'
 
 
@@ -55,8 +56,8 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
   const model = inputs.model
   const model = inputs.model
 
 
   return (
   return (
-    <div className='mt-2'>
-      <div className='px-4 pb-4 space-y-4'>
+    <div className='pt-2'>
+      <div className='px-4 space-y-4'>
         <Field
         <Field
           title={t(`${i18nPrefix}.model`)}
           title={t(`${i18nPrefix}.model`)}
         >
         >
@@ -107,27 +108,27 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
             readonly={readOnly}
             readonly={readOnly}
           />
           />
         </Field>
         </Field>
-        <Field
-          title={t(`${i18nPrefix}.advancedSetting`)}
-          supportFold
-        >
-          <AdvancedSetting
-            hideMemorySetting={!isChatMode}
-            instruction={inputs.instruction}
-            onInstructionChange={handleInstructionChange}
-            memory={inputs.memory}
-            onMemoryChange={handleMemoryChange}
-            readonly={readOnly}
-            isChatApp={isChatMode}
-            isChatModel={isChatModel}
-            hasSetBlockStatus={hasSetBlockStatus}
-            nodesOutputVars={availableVars}
-            availableNodes={availableNodesWithParent}
-          />
-        </Field>
+        <Split />
       </div>
       </div>
+      <FieldCollapse
+        title={t(`${i18nPrefix}.advancedSetting`)}
+      >
+        <AdvancedSetting
+          hideMemorySetting={!isChatMode}
+          instruction={inputs.instruction}
+          onInstructionChange={handleInstructionChange}
+          memory={inputs.memory}
+          onMemoryChange={handleMemoryChange}
+          readonly={readOnly}
+          isChatApp={isChatMode}
+          isChatModel={isChatModel}
+          hasSetBlockStatus={hasSetBlockStatus}
+          nodesOutputVars={availableVars}
+          availableNodes={availableNodesWithParent}
+        />
+      </FieldCollapse>
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-2'>
+      <div>
         <OutputVars>
         <OutputVars>
           <>
           <>
             <VarItem
             <VarItem

+ 1 - 1
web/app/components/workflow/nodes/template-transform/panel.tsx

@@ -95,7 +95,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
         />
         />
       </div>
       </div>
       <Split />
       <Split />
-      <div className='px-4 pt-4 pb-2'>
+      <div>
         <OutputVars>
         <OutputVars>
           <>
           <>
             <VarItem
             <VarItem

+ 4 - 4
web/app/components/workflow/nodes/tool/panel.tsx

@@ -56,10 +56,10 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
   }
   }
 
 
   return (
   return (
-    <div className='mt-2'>
+    <div className='pt-2'>
       {!readOnly && isShowAuthBtn && (
       {!readOnly && isShowAuthBtn && (
         <>
         <>
-          <div className='px-4 pb-3'>
+          <div className='px-4'>
             <Button
             <Button
               variant='primary'
               variant='primary'
               className='w-full'
               className='w-full'
@@ -71,7 +71,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
         </>
         </>
       )}
       )}
       {!isShowAuthBtn && <>
       {!isShowAuthBtn && <>
-        <div className='px-4 pb-4 space-y-4'>
+        <div className='px-4 space-y-4'>
           {toolInputVarSchema.length > 0 && (
           {toolInputVarSchema.length > 0 && (
             <Field
             <Field
               title={t(`${i18nPrefix}.inputVars`)}
               title={t(`${i18nPrefix}.inputVars`)}
@@ -118,7 +118,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
         />
         />
       )}
       )}
 
 
-      <div className='px-4 pt-4 pb-2'>
+      <div>
         <OutputVars>
         <OutputVars>
           <>
           <>
             <VarItem
             <VarItem

+ 3 - 0
web/app/components/workflow/nodes/variable-assigner/components/node-group-item.tsx

@@ -21,6 +21,7 @@ import AddVariable from './add-variable'
 import NodeVariableItem from './node-variable-item'
 import NodeVariableItem from './node-variable-item'
 import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
+import { isExceptionVariable } from '@/app/components/workflow/utils'
 
 
 const i18nPrefix = 'workflow.nodes.variableAssigner'
 const i18nPrefix = 'workflow.nodes.variableAssigner'
 type GroupItem = {
 type GroupItem = {
@@ -128,12 +129,14 @@ const NodeGroupItem = ({
 
 
           const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
           const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
           const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
           const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
+          const isException = isExceptionVariable(varName, node?.data.type)
 
 
           return (
           return (
             <NodeVariableItem
             <NodeVariableItem
               key={index}
               key={index}
               isEnv={isEnv}
               isEnv={isEnv}
               isChatVar={isChatVar}
               isChatVar={isChatVar}
+              isException={isException}
               node={node as Node}
               node={node as Node}
               varName={varName}
               varName={varName}
               showBorder={showSelectedBorder || showSelectionBorder}
               showBorder={showSelectedBorder || showSelectionBorder}

+ 5 - 3
web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx

@@ -17,6 +17,7 @@ type NodeVariableItemProps = {
   writeMode?: string
   writeMode?: string
   showBorder?: boolean
   showBorder?: boolean
   className?: string
   className?: string
+  isException?: boolean
 }
 }
 
 
 const i18nPrefix = 'workflow.nodes.assigner'
 const i18nPrefix = 'workflow.nodes.assigner'
@@ -29,6 +30,7 @@ const NodeVariableItem = ({
   writeMode,
   writeMode,
   showBorder,
   showBorder,
   className,
   className,
+  isException,
 }: NodeVariableItemProps) => {
 }: NodeVariableItemProps) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   return (
   return (
@@ -50,14 +52,14 @@ const NodeVariableItem = ({
         </div>
         </div>
       )}
       )}
       <div className='flex items-center text-primary-600 w-full'>
       <div className='flex items-center text-primary-600 w-full'>
-        {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
+        {!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-primary-500', isException && 'text-text-warning')} />}
         {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
         {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
-        {!isChatVar && <div className={cn('max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis', isEnv && 'text-gray-900')} title={varName}>{varName}</div>}
+        {!isChatVar && <div className={cn('max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis', isEnv && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>}
         {isChatVar
         {isChatVar
           && <div className='flex items-center w-full gap-1'>
           && <div className='flex items-center w-full gap-1'>
             <div className='flex h-[18px] min-w-[18px] items-center gap-0.5 flex-1'>
             <div className='flex h-[18px] min-w-[18px] items-center gap-0.5 flex-1'>
               <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />
               <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />
-              <div className='max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis text-util-colors-teal-teal-700'>{varName}</div>
+              <div className={cn('max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis text-util-colors-teal-teal-700')}>{varName}</div>
             </div>
             </div>
             {writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
             {writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
           </div>
           </div>

+ 1 - 0
web/app/components/workflow/panel/workflow-preview.tsx

@@ -193,6 +193,7 @@ const WorkflowPreview = () => {
                     created_at={workflowRunningData?.result?.created_at}
                     created_at={workflowRunningData?.result?.created_at}
                     created_by={(workflowRunningData?.result?.created_by as any)?.name}
                     created_by={(workflowRunningData?.result?.created_by as any)?.name}
                     steps={workflowRunningData?.result?.total_steps}
                     steps={workflowRunningData?.result?.total_steps}
+                    exceptionCounts={workflowRunningData?.result?.exceptions_count}
                   />
                   />
                 )}
                 )}
                 {currentTab === 'DETAIL' && !workflowRunningData?.result && (
                 {currentTab === 'DETAIL' && !workflowRunningData?.result && (

+ 1 - 0
web/app/components/workflow/run/index.tsx

@@ -258,6 +258,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
             created_at={runDetail.created_at}
             created_at={runDetail.created_at}
             created_by={executor}
             created_by={executor}
             steps={runDetail.total_steps}
             steps={runDetail.total_steps}
+            exceptionCounts={runDetail.exceptions_count}
           />
           />
         )}
         )}
         {!loading && currentTab === 'TRACING' && (
         {!loading && currentTab === 'TRACING' && (

+ 6 - 0
web/app/components/workflow/run/meta.tsx

@@ -38,6 +38,12 @@ const MetaData: FC<Props> = ({
             {status === 'succeeded' && (
             {status === 'succeeded' && (
               <span>SUCCESS</span>
               <span>SUCCESS</span>
             )}
             )}
+            {status === 'partial-succeeded' && (
+              <span>PARTIAL SUCCESS</span>
+            )}
+            {status === 'exception' && (
+              <span>EXCEPTION</span>
+            )}
             {status === 'failed' && (
             {status === 'failed' && (
               <span>FAIL</span>
               <span>FAIL</span>
             )}
             )}

+ 19 - 2
web/app/components/workflow/run/node.tsx

@@ -19,6 +19,7 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/
 import Button from '@/app/components/base/button'
 import Button from '@/app/components/base/button'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
 import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
 import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
+import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
 
 
 type Props = {
 type Props = {
   className?: string
   className?: string
@@ -128,6 +129,9 @@ const NodePanel: FC<Props> = ({
           {nodeInfo.status === 'stopped' && (
           {nodeInfo.status === 'stopped' && (
             <RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
             <RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
           )}
           )}
+          {nodeInfo.status === 'exception' && (
+            <RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
+          )}
           {nodeInfo.status === 'running' && (
           {nodeInfo.status === 'running' && (
             <div className='shrink-0 flex items-center text-text-accent text-[13px] leading-[16px] font-medium'>
             <div className='shrink-0 flex items-center text-text-accent text-[13px] leading-[16px] font-medium'>
               <span className='mr-2 text-xs font-normal'>Running</span>
               <span className='mr-2 text-xs font-normal'>Running</span>
@@ -165,12 +169,24 @@ const NodePanel: FC<Props> = ({
                 <Split className='mt-2' />
                 <Split className='mt-2' />
               </div>
               </div>
             )}
             )}
-            <div className={cn('px-[10px]', hideInfo && '!px-2 !py-0.5')}>
-              {nodeInfo.status === 'stopped' && (
+            <div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
+              {(nodeInfo.status === 'stopped') && (
                 <StatusContainer status='stopped'>
                 <StatusContainer status='stopped'>
                   {t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}
                   {t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}
                 </StatusContainer>
                 </StatusContainer>
               )}
               )}
+              {(nodeInfo.status === 'exception') && (
+                <StatusContainer status='stopped'>
+                  {nodeInfo.error}
+                  <a
+                    href='https://docs.dify.ai/guides/workflow/error-handling/predefined-nodes-failure-logic'
+                    target='_blank'
+                    className='text-text-accent'
+                  >
+                    {t('workflow.common.learnMore')}
+                  </a>
+                </StatusContainer>
+              )}
               {nodeInfo.status === 'failed' && (
               {nodeInfo.status === 'failed' && (
                 <StatusContainer status='failed'>
                 <StatusContainer status='failed'>
                   {nodeInfo.error}
                   {nodeInfo.error}
@@ -207,6 +223,7 @@ const NodePanel: FC<Props> = ({
                   language={CodeLanguage.json}
                   language={CodeLanguage.json}
                   value={nodeInfo.outputs}
                   value={nodeInfo.outputs}
                   isJSONStringifyBeauty
                   isJSONStringifyBeauty
+                  tip={<ErrorHandleTip type={nodeInfo.execution_metadata?.error_strategy} />}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 7 - 0
web/app/components/workflow/run/result-panel.tsx

@@ -5,6 +5,7 @@ import StatusPanel from './status'
 import MetaData from './meta'
 import MetaData from './meta'
 import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
 import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
+import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
 
 
 type ResultPanelProps = {
 type ResultPanelProps = {
   inputs?: string
   inputs?: string
@@ -19,6 +20,8 @@ type ResultPanelProps = {
   finished_at?: number
   finished_at?: number
   steps?: number
   steps?: number
   showSteps?: boolean
   showSteps?: boolean
+  exceptionCounts?: number
+  execution_metadata?: any
 }
 }
 
 
 const ResultPanel: FC<ResultPanelProps> = ({
 const ResultPanel: FC<ResultPanelProps> = ({
@@ -33,6 +36,8 @@ const ResultPanel: FC<ResultPanelProps> = ({
   created_by,
   created_by,
   steps,
   steps,
   showSteps,
   showSteps,
+  exceptionCounts,
+  execution_metadata,
 }) => {
 }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   return (
   return (
@@ -43,6 +48,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
           time={elapsed_time}
           time={elapsed_time}
           tokens={total_tokens}
           tokens={total_tokens}
           error={error}
           error={error}
+          exceptionCounts={exceptionCounts}
         />
         />
       </div>
       </div>
       <div className='px-4 py-2 flex flex-col gap-2'>
       <div className='px-4 py-2 flex flex-col gap-2'>
@@ -69,6 +75,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
             language={CodeLanguage.json}
             language={CodeLanguage.json}
             value={outputs}
             value={outputs}
             isJSONStringifyBeauty
             isJSONStringifyBeauty
+            tip={<ErrorHandleTip type={execution_metadata?.error_strategy} />}
           />
           />
         )}
         )}
       </div>
       </div>

+ 3 - 1
web/app/components/workflow/run/status-container.tsx

@@ -14,10 +14,12 @@ const StatusContainer: FC<Props> = ({
   return (
   return (
     <div
     <div
       className={cn(
       className={cn(
-        'relative px-3 py-2.5 rounded-lg border system-xs-regular',
+        'relative px-3 py-2.5 rounded-lg border system-xs-regular break-all',
         status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
         status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
+        status === 'partial-succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
         status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-warning',
         status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-warning',
         status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
         status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
+        status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
         status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-util-colors-blue-light-blue-light-600',
         status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-util-colors-blue-light-blue-light-600',
       )}
       )}
     >
     >

+ 56 - 1
web/app/components/workflow/run/status.tsx

@@ -10,6 +10,7 @@ type ResultProps = {
   time?: number
   time?: number
   tokens?: number
   tokens?: number
   error?: string
   error?: string
+  exceptionCounts?: number
 }
 }
 
 
 const StatusPanel: FC<ResultProps> = ({
 const StatusPanel: FC<ResultProps> = ({
@@ -17,18 +18,23 @@ const StatusPanel: FC<ResultProps> = ({
   time,
   time,
   tokens,
   tokens,
   error,
   error,
+  exceptionCounts,
 }) => {
 }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 
   return (
   return (
     <StatusContainer status={status}>
     <StatusContainer status={status}>
       <div className='flex'>
       <div className='flex'>
-        <div className='flex-[33%] max-w-[120px]'>
+        <div className={cn(
+          'flex-[33%] max-w-[120px]',
+          status === 'partial-succeeded' && 'min-w-[140px]',
+        )}>
           <div className='mb-1 text-text-tertiary system-2xs-medium-uppercase'>{t('runLog.resultPanel.status')}</div>
           <div className='mb-1 text-text-tertiary system-2xs-medium-uppercase'>{t('runLog.resultPanel.status')}</div>
           <div
           <div
             className={cn(
             className={cn(
               'flex items-center gap-1 system-xs-semibold-uppercase',
               'flex items-center gap-1 system-xs-semibold-uppercase',
               status === 'succeeded' && 'text-util-colors-green-green-600',
               status === 'succeeded' && 'text-util-colors-green-green-600',
+              status === 'partial-succeeded' && 'text-util-colors-green-green-600',
               status === 'failed' && 'text-util-colors-red-red-600',
               status === 'failed' && 'text-util-colors-red-red-600',
               status === 'stopped' && 'text-util-colors-warning-warning-600',
               status === 'stopped' && 'text-util-colors-warning-warning-600',
               status === 'running' && 'text-util-colors-blue-light-blue-light-600',
               status === 'running' && 'text-util-colors-blue-light-blue-light-600',
@@ -46,6 +52,18 @@ const StatusPanel: FC<ResultProps> = ({
                 <span>SUCCESS</span>
                 <span>SUCCESS</span>
               </>
               </>
             )}
             )}
+            {status === 'partial-succeeded' && (
+              <>
+                <Indicator color={'green'} />
+                <span>PARTIAL SUCCESS</span>
+              </>
+            )}
+            {status === 'exception' && (
+              <>
+                <Indicator color={'yellow'} />
+                <span>EXCEPTION</span>
+              </>
+            )}
             {status === 'failed' && (
             {status === 'failed' && (
               <>
               <>
                 <Indicator color={'red'} />
                 <Indicator color={'red'} />
@@ -87,8 +105,45 @@ const StatusPanel: FC<ResultProps> = ({
         <>
         <>
           <div className='my-2 h-[0.5px] bg-divider-subtle'/>
           <div className='my-2 h-[0.5px] bg-divider-subtle'/>
           <div className='system-xs-regular text-text-destructive'>{error}</div>
           <div className='system-xs-regular text-text-destructive'>{error}</div>
+          {
+            !!exceptionCounts && (
+              <>
+                <div className='my-2 h-[0.5px] bg-divider-subtle'/>
+                <div className='system-xs-regular text-text-destructive'>
+                  {t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
+                </div>
+              </>
+            )
+          }
         </>
         </>
       )}
       )}
+      {
+        status === 'partial-succeeded' && !!exceptionCounts && (
+          <>
+            <div className='my-2 h-[0.5px] bg-divider-deep'/>
+            <div className='system-xs-medium text-text-warning'>
+              {t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
+            </div>
+          </>
+        )
+      }
+      {
+        status === 'exception' && (
+          <>
+            <div className='my-2 h-[0.5px] bg-divider-deep'/>
+            <div className='system-xs-medium text-text-warning'>
+              {error}
+              <a
+                href='https://docs.dify.ai/guides/workflow/error-handling/predefined-nodes-failure-logic'
+                target='_blank'
+                className='text-text-accent'
+              >
+                {t('workflow.common.learnMore')}
+              </a>
+            </div>
+          </>
+        )
+      }
     </StatusContainer>
     </StatusContainer>
   )
   )
 }
 }

+ 14 - 1
web/app/components/workflow/types.ts

@@ -9,6 +9,10 @@ import type { VarType as VarKindType } from '@/app/components/workflow/nodes/too
 import type { FileResponse, NodeTracing } from '@/types/workflow'
 import type { FileResponse, NodeTracing } from '@/types/workflow'
 import type { Collection, Tool } from '@/app/components/tools/types'
 import type { Collection, Tool } from '@/app/components/tools/types'
 import type { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
 import type { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
+import type {
+  DefaultValueForm,
+  ErrorHandleTypeEnum,
+} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
 
 
 export enum BlockEnum {
 export enum BlockEnum {
   Start = 'start',
   Start = 'start',
@@ -52,6 +56,7 @@ export type CommonNodeType<T = {}> = {
   _targetBranches?: Branch[]
   _targetBranches?: Branch[]
   _isSingleRun?: boolean
   _isSingleRun?: boolean
   _runningStatus?: NodeRunningStatus
   _runningStatus?: NodeRunningStatus
+  _runningBranchId?: string
   _singleRunningStatus?: NodeRunningStatus
   _singleRunningStatus?: NodeRunningStatus
   _isCandidate?: boolean
   _isCandidate?: boolean
   _isBundled?: boolean
   _isBundled?: boolean
@@ -62,6 +67,7 @@ export type CommonNodeType<T = {}> = {
   _iterationLength?: number
   _iterationLength?: number
   _iterationIndex?: number
   _iterationIndex?: number
   _inParallelHovering?: boolean
   _inParallelHovering?: boolean
+  _waitingRun?: boolean
   isInIteration?: boolean
   isInIteration?: boolean
   iteration_id?: string
   iteration_id?: string
   selected?: boolean
   selected?: boolean
@@ -70,14 +76,18 @@ export type CommonNodeType<T = {}> = {
   type: BlockEnum
   type: BlockEnum
   width?: number
   width?: number
   height?: number
   height?: number
+  error_strategy?: ErrorHandleTypeEnum
+  default_value?: DefaultValueForm[]
 } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
 } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
 
 
 export type CommonEdgeType = {
 export type CommonEdgeType = {
   _hovering?: boolean
   _hovering?: boolean
   _connectedNodeIsHovering?: boolean
   _connectedNodeIsHovering?: boolean
   _connectedNodeIsSelected?: boolean
   _connectedNodeIsSelected?: boolean
-  _run?: boolean
   _isBundled?: boolean
   _isBundled?: boolean
+  _sourceRunningStatus?: NodeRunningStatus
+  _targetRunningStatus?: NodeRunningStatus
+  _waitingRun?: boolean
   isInIteration?: boolean
   isInIteration?: boolean
   iteration_id?: string
   iteration_id?: string
   sourceType: BlockEnum
   sourceType: BlockEnum
@@ -242,6 +252,7 @@ export type Var = {
   options?: string[]
   options?: string[]
   required?: boolean
   required?: boolean
   des?: string
   des?: string
+  isException?: boolean
 }
 }
 
 
 export type NodeOutPutVar = {
 export type NodeOutPutVar = {
@@ -281,6 +292,7 @@ export enum NodeRunningStatus {
   Running = 'running',
   Running = 'running',
   Succeeded = 'succeeded',
   Succeeded = 'succeeded',
   Failed = 'failed',
   Failed = 'failed',
+  Exception = 'exception',
 }
 }
 
 
 export type OnNodeAdd = (
 export type OnNodeAdd = (
@@ -331,6 +343,7 @@ export type WorkflowRunningData = {
     showSteps?: boolean
     showSteps?: boolean
     total_steps?: number
     total_steps?: number
     files?: FileResponse[]
     files?: FileResponse[]
+    exceptions_count?: number
   }
   }
   tracing?: NodeTracing[]
   tracing?: NodeTracing[]
 }
 }

+ 36 - 1
web/app/components/workflow/utils.ts

@@ -19,7 +19,11 @@ import type {
   ToolWithProvider,
   ToolWithProvider,
   ValueSelector,
   ValueSelector,
 } from './types'
 } from './types'
-import { BlockEnum, ErrorHandleMode } from './types'
+import {
+  BlockEnum,
+  ErrorHandleMode,
+  NodeRunningStatus,
+} from './types'
 import {
 import {
   CUSTOM_NODE,
   CUSTOM_NODE,
   ITERATION_CHILDREN_Z_INDEX,
   ITERATION_CHILDREN_Z_INDEX,
@@ -761,3 +765,34 @@ export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: str
     hasAbnormalEdges,
     hasAbnormalEdges,
   }
   }
 }
 }
+
+export const hasErrorHandleNode = (nodeType?: BlockEnum) => {
+  return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
+}
+
+export const getEdgeColor = (nodeRunningStatus?: NodeRunningStatus, isFailBranch?: boolean) => {
+  if (nodeRunningStatus === NodeRunningStatus.Succeeded)
+    return 'var(--color-workflow-link-line-success-handle)'
+
+  if (nodeRunningStatus === NodeRunningStatus.Failed)
+    return 'var(--color-workflow-link-line-error-handle)'
+
+  if (nodeRunningStatus === NodeRunningStatus.Exception)
+    return 'var(--color-workflow-link-line-failure-handle)'
+
+  if (nodeRunningStatus === NodeRunningStatus.Running) {
+    if (isFailBranch)
+      return 'var(--color-workflow-link-line-failure-handle)'
+
+    return 'var(--color-workflow-link-line-handle)'
+  }
+
+  return 'var(--color-workflow-link-line-normal)'
+}
+
+export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
+  if ((variable === 'error_message' || variable === 'error_type') && hasErrorHandleNode(nodeType))
+    return true
+
+  return false
+}

+ 27 - 0
web/i18n/en-US/workflow.ts

@@ -102,6 +102,8 @@ const translation = {
     addParallelNode: 'Add Parallel Node',
     addParallelNode: 'Add Parallel Node',
     parallel: 'PARALLEL',
     parallel: 'PARALLEL',
     branch: 'BRANCH',
     branch: 'BRANCH',
+    onFailure: 'On Failure',
+    addFailureBranch: 'Add Fail Branch',
   },
   },
   env: {
   env: {
     envPanelTitle: 'Environment Variables',
     envPanelTitle: 'Environment Variables',
@@ -302,6 +304,31 @@ const translation = {
         tip: 'Chat memory',
         tip: 'Chat memory',
         builtIn: 'Built-in',
         builtIn: 'Built-in',
       },
       },
+      errorHandle: {
+        title: 'Error Handling',
+        tip: 'Exception handling strategy, triggered when a node encounters an exception.',
+        none: {
+          title: 'None',
+          desc: 'The node will stop running if an exception occurs and is not handled',
+        },
+        defaultValue: {
+          title: 'Default Value',
+          desc: 'When an error occurs, specify a static output content.',
+          tip: 'On error, will return below value.',
+          inLog: 'Node exception, outputting according to default values.',
+          output: 'Output Default Value',
+        },
+        failBranch: {
+          title: 'Fail Branch',
+          desc: 'When an error occurs, it will execute the exception branch',
+          customize: 'Go to the canvas to customize the fail branch logic.',
+          customizeTip: 'When the fail branch is activated, exceptions thrown by nodes will not terminate the process. Instead, it will automatically execute the predefined fail branch, allowing you to flexibly provide error messages, reports, fixes, or skip actions.',
+          inLog: 'Node exception, will automatically execute the fail branch. The node output will return an error type and error message and pass them to downstream.',
+        },
+        partialSucceeded: {
+          tip: 'There are {{num}} nodes in the process running abnormally, please go to tracing to check the logs.',
+        },
+      },
     },
     },
     start: {
     start: {
       required: 'required',
       required: 'required',

+ 27 - 0
web/i18n/zh-Hans/workflow.ts

@@ -101,6 +101,8 @@ const translation = {
     addParallelNode: '添加并行节点',
     addParallelNode: '添加并行节点',
     parallel: '并行',
     parallel: '并行',
     branch: '分支',
     branch: '分支',
+    onFailure: '异常时',
+    addFailureBranch: '添加异常分支',
   },
   },
   env: {
   env: {
     envPanelTitle: '环境变量',
     envPanelTitle: '环境变量',
@@ -301,6 +303,31 @@ const translation = {
         tip: '聊天记忆',
         tip: '聊天记忆',
         builtIn: '内置',
         builtIn: '内置',
       },
       },
+      errorHandle: {
+        title: '异常处理',
+        tip: '配置异常处理策略,当节点发生异常时触发。',
+        none: {
+          title: '无',
+          desc: '当发生异常且未处理时,节点将停止运行',
+        },
+        defaultValue: {
+          title: '默认值',
+          desc: '当发生异常时,指定默认输出内容。',
+          tip: '当发生异常时,将返回以下值。',
+          inLog: '节点异常,根据默认值输出。',
+          output: '输出默认值',
+        },
+        failBranch: {
+          title: '异常分支',
+          desc: '当发生异常时,将执行异常分支',
+          customize: '在画布自定义失败分支逻辑。',
+          customizeTip: '当节点发生异常时,将自动执行失败分支。失败分支允许您灵活地提供错误消息、报告、修复或跳过操作。',
+          inLog: '节点异常,将自动执行失败分支。节点输出将返回错误类型和错误信息,并传递给下游。',
+        },
+        partialSucceeded: {
+          tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。',
+        },
+      },
     },
     },
     start: {
     start: {
       required: '必填',
       required: '必填',

+ 1 - 0
web/models/log.ts

@@ -297,6 +297,7 @@ export type WorkflowRunDetailResponse = {
   created_by_end_user?: EndUserInfo
   created_by_end_user?: EndUserInfo
   created_at: number
   created_at: number
   finished_at: number
   finished_at: number
+  exceptions_count?: number
 }
 }
 
 
 export type AgentLogMeta = {
 export type AgentLogMeta = {

+ 4 - 1
web/types/workflow.ts

@@ -7,6 +7,7 @@ import type {
   Node,
   Node,
 } from '@/app/components/workflow/types'
 } from '@/app/components/workflow/types'
 import type { TransferMethod } from '@/types/app'
 import type { TransferMethod } from '@/types/app'
+import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
 
 
 export type NodeTracing = {
 export type NodeTracing = {
   id: string
   id: string
@@ -22,7 +23,7 @@ export type NodeTracing = {
   parallel_run_id?: string
   parallel_run_id?: string
   error?: string
   error?: string
   elapsed_time: number
   elapsed_time: number
-  execution_metadata: {
+  execution_metadata?: {
     total_tokens: number
     total_tokens: number
     total_price: number
     total_price: number
     currency: string
     currency: string
@@ -34,6 +35,7 @@ export type NodeTracing = {
     parent_parallel_start_node_id?: string
     parent_parallel_start_node_id?: string
     parallel_mode_run_id?: string
     parallel_mode_run_id?: string
     iteration_duration_map?: IterationDurationMap
     iteration_duration_map?: IterationDurationMap
+    error_strategy?: ErrorHandleTypeEnum
   }
   }
   metadata: {
   metadata: {
     iterator_length: number
     iterator_length: number
@@ -172,6 +174,7 @@ export type NodeFinishedResponse = {
       iteration_index?: number
       iteration_index?: number
       iteration_id?: string
       iteration_id?: string
       parallel_mode_run_id: string
       parallel_mode_run_id: string
+      error_strategy?: ErrorHandleTypeEnum
     }
     }
     created_at: number
     created_at: number
     files?: FileResponse[]
     files?: FileResponse[]