feat: Human Input Node (#32060)

The frontend and backend implementation for the human input node.

Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
This commit is contained in:
QuantumGhost
2026-02-09 14:57:23 +08:00
committed by GitHub
parent 56e3a55023
commit a1fc280102
474 changed files with 32667 additions and 2050 deletions

View File

@ -2,6 +2,9 @@ export * from './use-workflow-agent-log'
export * from './use-workflow-failed'
export * from './use-workflow-finished'
export * from './use-workflow-node-finished'
export * from './use-workflow-node-human-input-form-filled'
export * from './use-workflow-node-human-input-form-timeout'
export * from './use-workflow-node-human-input-required'
export * from './use-workflow-node-iteration-finished'
export * from './use-workflow-node-iteration-next'
export * from './use-workflow-node-iteration-started'
@ -10,6 +13,7 @@ export * from './use-workflow-node-loop-next'
export * from './use-workflow-node-loop-started'
export * from './use-workflow-node-retry'
export * from './use-workflow-node-started'
export * from './use-workflow-paused'
export * from './use-workflow-started'
export * from './use-workflow-text-chunk'
export * from './use-workflow-text-replace'

View File

@ -49,6 +49,8 @@ export const useWorkflowNodeFinished = () => {
if (data.node_type === BlockEnum.QuestionClassifier)
currentNode.data._runningBranchId = data?.outputs?.class_id
if (data.node_type === BlockEnum.HumanInput)
currentNode.data._runningBranchId = data?.outputs?.__action_id
}
})
setNodes(newNodes)

View File

@ -0,0 +1,34 @@
import type { HumanInputFormFilledResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeHumanInputFormFilled = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowNodeHumanInputFormFilled = useCallback((params: HumanInputFormFilledResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
draft.humanInputFormDataList.splice(currentFormIndex, 1)
}
if (!draft.humanInputFilledFormDataList) {
draft.humanInputFilledFormDataList = [data]
}
else {
draft.humanInputFilledFormDataList.push(data)
}
})
setWorkflowRunningData(newWorkflowRunningData)
}, [workflowStore])
return {
handleWorkflowNodeHumanInputFormFilled,
}
}

View File

@ -0,0 +1,28 @@
import type { HumanInputFormTimeoutResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeHumanInputFormTimeout = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowNodeHumanInputFormTimeout = useCallback((params: HumanInputFormTimeoutResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
draft.humanInputFormDataList[currentFormIndex].expiration_time = data.expiration_time
}
})
setWorkflowRunningData(newWorkflowRunningData)
}, [workflowStore])
return {
handleWorkflowNodeHumanInputFormTimeout,
}
}

View File

@ -0,0 +1,60 @@
import type { HumanInputRequiredResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import {
useStoreApi,
} from 'reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { NodeRunningStatus } from '@/app/components/workflow/types'
export const useWorkflowNodeHumanInputRequired = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
// Notice: Human input required !== Workflow Paused
const handleWorkflowNodeHumanInputRequired = useCallback((params: HumanInputRequiredResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (!draft.humanInputFormDataList) {
draft.humanInputFormDataList = [data]
}
else {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
if (currentFormIndex > -1) {
draft.humanInputFormDataList[currentFormIndex] = data
}
else {
draft.humanInputFormDataList.push(data)
}
}
const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id)
if (currentIndex > -1) {
draft.tracing![currentIndex] = {
...draft.tracing![currentIndex],
status: NodeRunningStatus.Paused,
}
}
})
setWorkflowRunningData(newWorkflowRunningData)
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Paused
})
setNodes(newNodes)
}, [store, workflowStore])
return {
handleWorkflowNodeHumanInputRequired,
}
}

View File

@ -33,12 +33,23 @@ export const useWorkflowNodeStarted = () => {
transform,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}))
const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id)
if (currentIndex && currentIndex > -1) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing![currentIndex] = {
...data,
status: NodeRunningStatus.Running,
}
}))
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}))
}
const {
setViewport,

View File

@ -0,0 +1,26 @@
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
export const useWorkflowPaused = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowPaused = useCallback(() => {
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Paused,
}
}))
}, [workflowStore])
return {
handleWorkflowPaused,
}
}

View File

@ -3,6 +3,9 @@ import {
useWorkflowFailed,
useWorkflowFinished,
useWorkflowNodeFinished,
useWorkflowNodeHumanInputFormFilled,
useWorkflowNodeHumanInputFormTimeout,
useWorkflowNodeHumanInputRequired,
useWorkflowNodeIterationFinished,
useWorkflowNodeIterationNext,
useWorkflowNodeIterationStarted,
@ -11,6 +14,7 @@ import {
useWorkflowNodeLoopStarted,
useWorkflowNodeRetry,
useWorkflowNodeStarted,
useWorkflowPaused,
useWorkflowStarted,
useWorkflowTextChunk,
useWorkflowTextReplace,
@ -32,6 +36,10 @@ export const useWorkflowRunEvent = () => {
const { handleWorkflowTextChunk } = useWorkflowTextChunk()
const { handleWorkflowTextReplace } = useWorkflowTextReplace()
const { handleWorkflowAgentLog } = useWorkflowAgentLog()
const { handleWorkflowPaused } = useWorkflowPaused()
const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired()
const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled()
const { handleWorkflowNodeHumanInputFormTimeout } = useWorkflowNodeHumanInputFormTimeout()
return {
handleWorkflowStarted,
@ -49,5 +57,9 @@ export const useWorkflowRunEvent = () => {
handleWorkflowTextChunk,
handleWorkflowTextReplace,
handleWorkflowAgentLog,
handleWorkflowPaused,
handleWorkflowNodeHumanInputFormFilled,
handleWorkflowNodeHumanInputRequired,
handleWorkflowNodeHumanInputFormTimeout,
}
}

View File

@ -22,6 +22,15 @@ export const useWorkflowStarted = () => {
edges,
setEdges,
} = store.getState()
if (workflowRunningData?.result?.status === WorkflowRunningStatus.Paused) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Running,
}
}))
return
}
setIterParallelLogMap(new Map())
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.task_id = task_id
@ -30,6 +39,7 @@ export const useWorkflowStarted = () => {
...data,
status: WorkflowRunningStatus.Running,
}
draft.resultText = ''
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {