feat: introduce trigger functionality (#27644)

Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Yeuoly
2025-11-12 17:59:37 +08:00
committed by GitHub
parent ca7794305b
commit b76e17b25d
785 changed files with 41186 additions and 3725 deletions

View File

@@ -10,7 +10,15 @@ import {
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
import {
BlockEnum,
InputVarType,
NodeRunningStatus,
VarType,
WorkflowRunningStatus,
} from '@/app/components/workflow/types'
import type { TriggerNodeType } from '@/app/components/workflow/types'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
import Toast from '@/app/components/base/toast'
@@ -28,7 +36,7 @@ import ParameterExtractorDefault from '@/app/components/workflow/nodes/parameter
import IterationDefault from '@/app/components/workflow/nodes/iteration/default'
import DocumentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
import LoopDefault from '@/app/components/workflow/nodes/loop/default'
import { ssePost } from '@/service/base'
import { post, ssePost } from '@/service/base'
import { noop } from 'lodash-es'
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
import type { NodeRunResult, NodeTracing } from '@/types/workflow'
@@ -50,9 +58,10 @@ import {
useStoreApi,
} from 'reactflow'
import { useInvalidLastRun } from '@/service/use-workflow'
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import type { FlowType } from '@/types/common'
import useMatchSchemaType from '../components/variable/use-match-schema-type'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import {
useAllBuiltInTools,
useAllCustomTools,
@@ -61,7 +70,7 @@ import {
} from '@/service/use-tools'
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Record<BlockEnum, Function> = {
const checkValidFns: Partial<Record<BlockEnum, Function>> = {
[BlockEnum.LLM]: checkLLMValid,
[BlockEnum.KnowledgeRetrieval]: checkKnowledgeRetrievalValid,
[BlockEnum.IfElse]: checkIfElseValid,
@@ -76,7 +85,12 @@ const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.Iteration]: checkIterationValid,
[BlockEnum.DocExtractor]: checkDocumentExtractorValid,
[BlockEnum.Loop]: checkLoopValid,
} as any
}
type RequestError = {
message: string
status: string
}
export type Params<T> = {
id: string
@@ -198,7 +212,52 @@ const useOneStepRun = <T>({
const store = useStoreApi()
const {
setShowSingleRunPanel,
setIsListening,
setListeningTriggerType,
setListeningTriggerNodeId,
setListeningTriggerNodeIds,
setListeningTriggerIsAll,
setShowVariableInspectPanel,
} = workflowStore.getState()
const updateNodeInspectRunningState = useCallback((nodeId: string, isRunning: boolean) => {
const {
nodesWithInspectVars,
setNodesWithInspectVars,
} = workflowStore.getState()
let hasChanges = false
const nodes = produce(nodesWithInspectVars, (draft) => {
const index = draft.findIndex(node => node.nodeId === nodeId)
if (index !== -1) {
const targetNode = draft[index]
if (targetNode.isSingRunRunning !== isRunning) {
targetNode.isSingRunRunning = isRunning
if (isRunning)
targetNode.isValueFetched = false
hasChanges = true
}
}
else if (isRunning) {
const { getNodes } = store.getState()
const target = getNodes().find(node => node.id === nodeId)
if (target) {
draft.unshift({
nodeId,
nodeType: target.data.type,
title: target.data.title,
vars: [],
nodePayload: target.data,
isSingRunRunning: true,
isValueFetched: false,
})
hasChanges = true
}
}
})
if (hasChanges)
setNodesWithInspectVars(nodes)
}, [workflowStore, store])
const invalidLastRun = useInvalidLastRun(flowType, flowId!, id)
const [runResult, doSetRunResult] = useState<NodeRunResult | null>(null)
const {
@@ -207,10 +266,26 @@ const useOneStepRun = <T>({
invalidateConversationVarValues,
} = useInspectVarsCrud()
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
const webhookSingleRunActiveRef = useRef(false)
const webhookSingleRunAbortRef = useRef<AbortController | null>(null)
const webhookSingleRunTimeoutRef = useRef<number | undefined>(undefined)
const webhookSingleRunTokenRef = useRef(0)
const webhookSingleRunDelayResolveRef = useRef<(() => void) | null>(null)
const pluginSingleRunActiveRef = useRef(false)
const pluginSingleRunAbortRef = useRef<AbortController | null>(null)
const pluginSingleRunTimeoutRef = useRef<number | undefined>(undefined)
const pluginSingleRunTokenRef = useRef(0)
const pluginSingleRunDelayResolveRef = useRef<(() => void) | null>(null)
const isPausedRef = useRef(isPaused)
useEffect(() => {
isPausedRef.current = isPaused
}, [isPaused])
const { eventEmitter } = useEventEmitterContextContext()
const isScheduleTriggerNode = data.type === BlockEnum.TriggerSchedule
const isWebhookTriggerNode = data.type === BlockEnum.TriggerWebhook
const isPluginTriggerNode = data.type === BlockEnum.TriggerPlugin
const isTriggerNode = isWebhookTriggerNode || isPluginTriggerNode || isScheduleTriggerNode
const setRunResult = useCallback(async (data: NodeRunResult | null) => {
const isPaused = isPausedRef.current
@@ -230,13 +305,27 @@ const useOneStepRun = <T>({
const { getNodes } = store.getState()
const nodes = getNodes()
appendNodeInspectVars(id, vars, nodes)
updateNodeInspectRunningState(id, false)
if (data?.status === NodeRunningStatus.Succeeded) {
invalidLastRun()
if (isStartNode)
if (isStartNode || isTriggerNode)
invalidateSysVarValues()
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
}
}, [isRunAfterSingleRun, runningStatus, flowId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
}, [
isRunAfterSingleRun,
runningStatus,
flowId,
id,
store,
appendNodeInspectVars,
updateNodeInspectRunningState,
invalidLastRun,
isStartNode,
isTriggerNode,
invalidateSysVarValues,
invalidateConversationVarValues,
])
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const setNodeRunning = () => {
@@ -248,6 +337,299 @@ const useOneStepRun = <T>({
},
})
}
const cancelWebhookSingleRun = useCallback(() => {
webhookSingleRunActiveRef.current = false
webhookSingleRunTokenRef.current += 1
if (webhookSingleRunAbortRef.current)
webhookSingleRunAbortRef.current.abort()
webhookSingleRunAbortRef.current = null
if (webhookSingleRunTimeoutRef.current !== undefined) {
window.clearTimeout(webhookSingleRunTimeoutRef.current)
webhookSingleRunTimeoutRef.current = undefined
}
if (webhookSingleRunDelayResolveRef.current) {
webhookSingleRunDelayResolveRef.current()
webhookSingleRunDelayResolveRef.current = null
}
}, [])
const cancelPluginSingleRun = useCallback(() => {
pluginSingleRunActiveRef.current = false
pluginSingleRunTokenRef.current += 1
if (pluginSingleRunAbortRef.current)
pluginSingleRunAbortRef.current.abort()
pluginSingleRunAbortRef.current = null
if (pluginSingleRunTimeoutRef.current !== undefined) {
window.clearTimeout(pluginSingleRunTimeoutRef.current)
pluginSingleRunTimeoutRef.current = undefined
}
if (pluginSingleRunDelayResolveRef.current) {
pluginSingleRunDelayResolveRef.current()
pluginSingleRunDelayResolveRef.current = null
}
}, [])
const startTriggerListening = useCallback(() => {
if (!isTriggerNode)
return
setIsListening(true)
setShowVariableInspectPanel(true)
setListeningTriggerType(data.type as TriggerNodeType)
setListeningTriggerNodeId(id)
setListeningTriggerNodeIds([id])
setListeningTriggerIsAll(false)
}, [
isTriggerNode,
setIsListening,
setShowVariableInspectPanel,
setListeningTriggerType,
data.type,
setListeningTriggerNodeId,
id,
setListeningTriggerNodeIds,
setListeningTriggerIsAll,
])
const stopTriggerListening = useCallback(() => {
if (!isTriggerNode)
return
setIsListening(false)
setListeningTriggerType(null)
setListeningTriggerNodeId(null)
setListeningTriggerNodeIds([])
setListeningTriggerIsAll(false)
}, [
isTriggerNode,
setIsListening,
setListeningTriggerType,
setListeningTriggerNodeId,
setListeningTriggerNodeIds,
setListeningTriggerIsAll,
])
const runScheduleSingleRun = useCallback(async (): Promise<NodeRunResult | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
try {
const response: any = await post(urlPath, {
body: JSON.stringify({}),
})
if (!response) {
const message = 'Schedule trigger run failed'
Toast.notify({ type: 'error', message })
throw new Error(message)
}
if (response?.status === 'error') {
const message = response?.message || 'Schedule trigger run failed'
Toast.notify({ type: 'error', message })
throw new Error(message)
}
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Succeeded,
},
})
return response as NodeRunResult
}
catch (error) {
console.error('handleRun: schedule trigger single run error', error)
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
Toast.notify({ type: 'error', message: 'Schedule trigger run failed' })
throw error
}
}, [flowId, id, handleNodeDataUpdate, data])
const runWebhookSingleRun = useCallback(async (): Promise<any | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
webhookSingleRunActiveRef.current = true
const token = ++webhookSingleRunTokenRef.current
while (webhookSingleRunActiveRef.current && token === webhookSingleRunTokenRef.current) {
const controller = new AbortController()
webhookSingleRunAbortRef.current = controller
try {
const response: any = await post(urlPath, {
body: JSON.stringify({}),
signal: controller.signal,
})
if (!webhookSingleRunActiveRef.current || token !== webhookSingleRunTokenRef.current)
return null
if (!response) {
const message = response?.message || 'Webhook debug failed'
Toast.notify({ type: 'error', message })
cancelWebhookSingleRun()
throw new Error(message)
}
if (response?.status === 'waiting') {
const delay = Number(response.retry_in) || 2000
webhookSingleRunAbortRef.current = null
if (!webhookSingleRunActiveRef.current || token !== webhookSingleRunTokenRef.current)
return null
await new Promise<void>((resolve) => {
const timeoutId = window.setTimeout(resolve, delay)
webhookSingleRunTimeoutRef.current = timeoutId
webhookSingleRunDelayResolveRef.current = resolve
controller.signal.addEventListener('abort', () => {
window.clearTimeout(timeoutId)
resolve()
}, { once: true })
})
webhookSingleRunTimeoutRef.current = undefined
webhookSingleRunDelayResolveRef.current = null
continue
}
if (response?.status === 'error') {
const message = response.message || 'Webhook debug failed'
Toast.notify({ type: 'error', message })
cancelWebhookSingleRun()
throw new Error(message)
}
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Listening,
},
})
cancelWebhookSingleRun()
return response
}
catch (error) {
if (controller.signal.aborted && (!webhookSingleRunActiveRef.current || token !== webhookSingleRunTokenRef.current))
return null
if (controller.signal.aborted)
return null
Toast.notify({ type: 'error', message: 'Webhook debug request failed' })
cancelWebhookSingleRun()
if (error instanceof Error)
throw error
throw new Error(String(error))
}
finally {
webhookSingleRunAbortRef.current = null
}
}
return null
}, [flowId, id, data, handleNodeDataUpdate, cancelWebhookSingleRun])
const runPluginSingleRun = useCallback(async (): Promise<any | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
pluginSingleRunActiveRef.current = true
const token = ++pluginSingleRunTokenRef.current
while (pluginSingleRunActiveRef.current && token === pluginSingleRunTokenRef.current) {
const controller = new AbortController()
pluginSingleRunAbortRef.current = controller
let requestError: RequestError | undefined
const response: any = await post(urlPath, {
body: JSON.stringify({}),
signal: controller.signal,
}).catch(async (error: Response) => {
const data = await error.clone().json() as Record<string, any>
const { error: respError, status } = data || {}
requestError = {
message: respError,
status,
}
return null
}).finally(() => {
pluginSingleRunAbortRef.current = null
})
if (!pluginSingleRunActiveRef.current || token !== pluginSingleRunTokenRef.current)
return null
if (requestError) {
if (controller.signal.aborted)
return null
Toast.notify({ type: 'error', message: requestError.message })
cancelPluginSingleRun()
throw requestError
}
if (!response) {
const message = 'Plugin debug failed'
Toast.notify({ type: 'error', message })
cancelPluginSingleRun()
throw new Error(message)
}
if (response?.status === 'waiting') {
const delay = Number(response.retry_in) || 2000
if (!pluginSingleRunActiveRef.current || token !== pluginSingleRunTokenRef.current)
return null
await new Promise<void>((resolve) => {
const timeoutId = window.setTimeout(resolve, delay)
pluginSingleRunTimeoutRef.current = timeoutId
pluginSingleRunDelayResolveRef.current = resolve
controller.signal.addEventListener('abort', () => {
window.clearTimeout(timeoutId)
resolve()
}, { once: true })
})
pluginSingleRunTimeoutRef.current = undefined
pluginSingleRunDelayResolveRef.current = null
continue
}
if (response?.status === 'error') {
const message = response.message || 'Plugin debug failed'
Toast.notify({ type: 'error', message })
cancelPluginSingleRun()
throw new Error(message)
}
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Listening,
},
})
cancelPluginSingleRun()
return response
}
return null
}, [flowId, id, data, handleNodeDataUpdate, cancelPluginSingleRun])
const checkValidWrap = () => {
if (!checkValid)
return { isValid: true, errorMessage: '' }
@@ -262,7 +644,7 @@ const useOneStepRun = <T>({
})
Toast.notify({
type: 'error',
message: res.errorMessage,
message: res.errorMessage || '',
})
}
return res
@@ -309,33 +691,84 @@ const useOneStepRun = <T>({
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
const handleRun = async (submitData: Record<string, any>) => {
if (isWebhookTriggerNode)
cancelWebhookSingleRun()
if (isPluginTriggerNode)
cancelPluginSingleRun()
updateNodeInspectRunningState(id, true)
if (isTriggerNode)
startTriggerListening()
else
stopTriggerListening()
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Running,
_singleRunningStatus: isTriggerNode
? NodeRunningStatus.Listening
: NodeRunningStatus.Running,
},
})
let res: any
let hasError = false
try {
if (!isIteration && !isLoop) {
const isStartNode = data.type === BlockEnum.Start
const postData: Record<string, any> = {}
if (isStartNode) {
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
if (isChatMode)
postData.conversation_id = ''
postData.inputs = inputs
postData.query = query
postData.files = files || []
if (isScheduleTriggerNode) {
res = await runScheduleSingleRun()
}
else if (isWebhookTriggerNode) {
res = await runWebhookSingleRun()
if (!res) {
if (webhookSingleRunActiveRef.current) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Stopped,
},
})
}
return false
}
}
else if (isPluginTriggerNode) {
res = await runPluginSingleRun()
if (!res) {
if (pluginSingleRunActiveRef.current) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Stopped,
},
})
}
return false
}
}
else {
postData.inputs = submitData
const isStartNode = data.type === BlockEnum.Start
const postData: Record<string, any> = {}
if (isStartNode) {
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
if (isChatMode)
postData.conversation_id = ''
postData.inputs = inputs
postData.query = query
postData.files = files || []
}
else {
postData.inputs = submitData
}
res = await singleNodeRun(flowType, flowId!, id, postData) as any
}
res = await singleNodeRun(flowType, flowId!, id, postData) as any
}
else if (isIteration) {
setIterationRunResult([])
@@ -566,6 +999,14 @@ const useOneStepRun = <T>({
}
}
finally {
if (isWebhookTriggerNode)
cancelWebhookSingleRun()
if (isPluginTriggerNode)
cancelPluginSingleRun()
if (isTriggerNode)
stopTriggerListening()
if (!isIteration && !isLoop)
updateNodeInspectRunningState(id, false)
if (!isPausedRef.current && !isIteration && !isLoop && res) {
setRunResult({
...res,
@@ -591,15 +1032,55 @@ const useOneStepRun = <T>({
}
}
const handleStop = () => {
const handleStop = useCallback(() => {
if (isTriggerNode) {
const isTriggerActive = runningStatus === NodeRunningStatus.Listening
|| webhookSingleRunActiveRef.current
|| pluginSingleRunActiveRef.current
if (!isTriggerActive)
return
}
else if (runningStatus !== NodeRunningStatus.Running) {
return
}
cancelWebhookSingleRun()
cancelPluginSingleRun()
handleNodeDataUpdate({
id,
data: {
...data,
_singleRunningStatus: NodeRunningStatus.NotStart,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Stopped,
},
})
}
stopTriggerListening()
updateNodeInspectRunningState(id, false)
const {
workflowRunningData,
setWorkflowRunningData,
nodesWithInspectVars,
deleteNodeInspectVars,
} = workflowStore.getState()
if (workflowRunningData) {
setWorkflowRunningData(produce(workflowRunningData, (draft) => {
draft.result.status = WorkflowRunningStatus.Stopped
}))
}
const inspectNode = nodesWithInspectVars.find(node => node.nodeId === id)
if (inspectNode && !inspectNode.isValueFetched && (!inspectNode.vars || inspectNode.vars.length === 0))
deleteNodeInspectVars(id)
}, [
isTriggerNode,
runningStatus,
cancelWebhookSingleRun,
cancelPluginSingleRun,
handleNodeDataUpdate,
id,
stopTriggerListening,
updateNodeInspectRunningState,
workflowStore,
])
const toVarInputs = (variables: Variable[]): InputVar[] => {
if (!variables)
@@ -662,6 +1143,11 @@ const useOneStepRun = <T>({
})
}
eventEmitter?.useSubscription((v: any) => {
if (v.type === EVENT_WORKFLOW_STOP)
handleStop()
})
return {
isShowSingleRun,
hideSingleRun,