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

@@ -4,8 +4,9 @@ import {
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import { useEdges, useNodes, useStoreApi } from 'reactflow'
import type {
CommonEdgeType,
CommonNodeType,
Edge,
Node,
@@ -21,20 +22,22 @@ import {
getToolCheckParams,
getValidTreeNodes,
} from '../utils'
import { getTriggerCheckParams } from '../utils/trigger'
import {
CUSTOM_NODE,
} from '../constants'
import {
useGetToolIcon,
useWorkflow,
useNodesMetaData,
} from '../hooks'
import type { ToolNodeType } from '../nodes/tool/types'
import type { DataSourceNodeType } from '../nodes/data-source/types'
import { useNodesMetaData } from './use-nodes-meta-data'
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
import { useToastContext } from '@/app/components/base/toast'
import { useGetLanguage } from '@/context/i18n'
import type { AgentNodeType } from '../nodes/agent/types'
import { useStrategyProviders } from '@/service/use-strategy'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
import type { DataSet } from '@/models/datasets'
@@ -42,6 +45,7 @@ import { fetchDatasets } from '@/service/datasets'
import { MAX_TREE_DEPTH } from '@/config'
import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list'
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
import type { Emoji } from '@/app/components/tools/types'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types'
@@ -50,6 +54,25 @@ import {
useAllCustomTools,
useAllWorkflowTools,
} from '@/service/use-tools'
import { useStore as useAppStore } from '@/app/components/app/store'
import { AppModeEnum } from '@/types/app'
export type ChecklistItem = {
id: string
type: BlockEnum | string
title: string
toolIcon?: string | Emoji
unConnected?: boolean
errorMessage?: string
canNavigate: boolean
}
const START_NODE_TYPES: BlockEnum[] = [
BlockEnum.Start,
BlockEnum.TriggerSchedule,
BlockEnum.TriggerWebhook,
BlockEnum.TriggerPlugin,
]
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
@@ -60,9 +83,11 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { data: workflowTools } = useAllWorkflowTools()
const dataSourceList = useStore(s => s.dataSourceList)
const { data: strategyProviders } = useStrategyProviders()
const { data: triggerPlugins } = useAllTriggerPlugins()
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
const { getStartNodes } = useWorkflow()
const getToolIcon = useGetToolIcon()
const appMode = useAppStore.getState().appDetail?.mode
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
const map = useNodesAvailableVarList(nodes)
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
@@ -92,16 +117,10 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
return checkData
}, [datasetsDetail, embeddingModelList, rerankModelList])
const needWarningNodes = useMemo(() => {
const list = []
const needWarningNodes = useMemo<ChecklistItem[]>(() => {
const list: ChecklistItem[] = []
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
const startNodes = getStartNodes(filteredNodes)
const validNodesFlattened = startNodes.map(startNode => getValidTreeNodes(startNode, filteredNodes, edges))
const validNodes = validNodesFlattened.reduce((acc, curr) => {
if (curr.validNodes)
acc.push(...curr.validNodes)
return acc
}, [] as Node[])
const { validNodes } = getValidTreeNodes(filteredNodes, edges)
for (let i = 0; i < filteredNodes.length; i++) {
const node = filteredNodes[i]
@@ -114,6 +133,9 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
if (node.data.type === BlockEnum.DataSource)
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
if (node.data.type === BlockEnum.TriggerPlugin)
moreDataForCheckValid = getTriggerCheckParams(node.data as PluginTriggerNodeType, triggerPlugins, language)
const toolIcon = getToolIcon(node.data)
if (node.data.type === BlockEnum.Agent) {
const data = node.data as AgentNodeType
@@ -133,7 +155,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
if (node.type === CUSTOM_NODE) {
const checkData = getCheckData(node.data)
let { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid
let errorMessage = validator ? validator(checkData, t, moreDataForCheckValid).errorMessage : undefined
if (!errorMessage) {
const availableVars = map[node.id].availableVars
@@ -153,19 +176,43 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
}
}
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
// Start nodes and Trigger nodes should not show unConnected error if they have validation errors
// or if they are valid start nodes (even without incoming connections)
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
const isUnconnected = !validNodes.find(n => n.id === node.id)
const shouldShowError = errorMessage || (isUnconnected && !canSkipConnectionCheck)
if (shouldShowError) {
list.push({
id: node.id,
type: node.data.type,
title: node.data.title,
toolIcon,
unConnected: !validNodes.find(n => n.id === node.id),
unConnected: isUnconnected && !canSkipConnectionCheck,
errorMessage,
canNavigate: true,
})
}
}
}
// Check for start nodes (including triggers)
if (shouldCheckStartNode) {
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
if (startNodesFiltered.length === 0) {
list.push({
id: 'start-node-required',
type: BlockEnum.Start,
title: t('workflow.panel.startNode'),
errorMessage: t('workflow.common.needStartNode'),
canNavigate: false,
})
}
}
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
isRequiredNodesType.forEach((type: string) => {
@@ -175,12 +222,13 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
type,
title: t(`workflow.blocks.${type}`),
errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }),
canNavigate: false,
})
}
})
return list
}, [nodes, getStartNodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map])
}, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map, shouldCheckStartNode])
return needWarningNodes
}
@@ -194,7 +242,6 @@ export const useChecklistBeforePublish = () => {
const { data: strategyProviders } = useStrategyProviders()
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
const updateTime = useRef(0)
const { getStartNodes } = useWorkflow()
const workflowStore = useWorkflowStore()
const { getNodesAvailableVarList } = useGetNodesAvailableVarList()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
@@ -241,20 +288,11 @@ export const useChecklistBeforePublish = () => {
} = workflowStore.getState()
const nodes = getNodes()
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
const startNodes = getStartNodes(filteredNodes)
const validNodesFlattened = startNodes.map(startNode => getValidTreeNodes(startNode, filteredNodes, edges))
const validNodes = validNodesFlattened.reduce((acc, curr) => {
if (curr.validNodes)
acc.push(...curr.validNodes)
return acc
}, [] as Node[])
const maxDepthArr = validNodesFlattened.map(item => item.maxDepth)
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
for (let i = 0; i < maxDepthArr.length; i++) {
if (maxDepthArr[i] > MAX_TREE_DEPTH) {
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
return false
}
if (maxDepth > MAX_TREE_DEPTH) {
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
return false
}
// Before publish, we need to fetch datasets detail, in case of the settings of datasets have been changed
const knowledgeRetrievalNodes = filteredNodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
@@ -334,10 +372,18 @@ export const useChecklistBeforePublish = () => {
}
}
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
if (startNodesFiltered.length === 0) {
notify({ type: 'error', message: t('workflow.common.needStartNode') })
return false
}
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
for (let i = 0; i < isRequiredNodesType.length; i++) {
const type = isRequiredNodesType[i]
if (!filteredNodes.find(node => node.data.type === type)) {
notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) })
return false
@@ -345,9 +391,31 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, getStartNodes, workflowStore, buildInTools, customTools, workflowTools])
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools])
return {
handleCheckBeforePublish,
}
}
export const useWorkflowRunValidation = () => {
const { t } = useTranslation()
const nodes = useNodes<CommonNodeType>()
const edges = useEdges<CommonEdgeType>()
const needWarningNodes = useChecklist(nodes, edges)
const { notify } = useToastContext()
const validateBeforeRun = useCallback(() => {
if (needWarningNodes.length > 0) {
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
return false
}
return true
}, [needWarningNodes, notify, t])
return {
validateBeforeRun,
hasValidationErrors: needWarningNodes.length > 0,
warningNodes: needWarningNodes,
}
}