import type { AgentNodeType } from '../nodes/agent/types' import type { DataSourceNodeType } from '../nodes/data-source/types' import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types' import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types' import type { ToolNodeType } from '../nodes/tool/types' import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types' import type { CommonEdgeType, CommonNodeType, Edge, ModelConfig, Node, ValueSelector, } from '../types' import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Emoji } from '@/app/components/tools/types' import type { DataSet } from '@/models/datasets' import type { I18nKeysWithPrefix } from '@/types/i18n' import { toast } from '@langgenius/dify-ui/toast' import { useQueries, useQueryClient } from '@tanstack/react-query' import isDeepEqual from 'fast-deep-equal' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useEdges, useStoreApi } from 'reactflow' import { useStore as useAppStore } from '@/app/components/app/store' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { MAX_TREE_DEPTH } from '@/config' import { useGetLanguage } from '@/context/i18n' import { useProviderContextSelector } from '@/context/provider-context' import { consoleQuery } from '@/service/client' import { fetchDatasets } from '@/service/datasets' import { useStrategyProviders } from '@/service/use-strategy' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { AppModeEnum } from '@/types/app' import { FlowType } from '@/types/common' import { CUSTOM_NODE } from '../constants' import { useDatasetsDetailStore } from '../datasets-detail-store/store' import { useGetToolIcon, useNodesMetaData } from '../hooks' import { useHooksStore } from '../hooks-store/store' import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils' import { hasValidInlineAgentBinding, isAgentV2NodeData } from '../nodes/agent-v2/types' import { IndexMethodEnum } from '../nodes/knowledge-base/types' import { getLLMModelIssue, isLLMModelProviderInstalled, LLMModelIssueCode, } from '../nodes/llm/utils' import { useStore, useWorkflowStore } from '../store' import { BlockEnum } from '../types' import { getDataSourceCheckParams, getNodeCatalogType, getToolCheckParams, getValidTreeNodes, } from '../utils' import { extractPluginId } from '../utils/plugin' import { isNodePluginMissing } from '../utils/plugin-install-check' import { getTriggerCheckParams } from '../utils/trigger' import useNodesAvailableVarList, { useGetNodesAvailableVarList, } from './use-nodes-available-var-list' export type ChecklistItem = { id: string type: BlockEnum | string title: string toolIcon?: string | Emoji unConnected?: boolean errorMessages: string[] canNavigate: boolean disableGoTo?: boolean isPluginMissing?: boolean pluginUniqueIdentifier?: string } type CheckValidExtraData = Record | undefined const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: FlowType) => { if (!flowType) return moreDataForCheckValid return { ...(moreDataForCheckValid ?? {}), flowType, } } const START_NODE_TYPES: BlockEnum[] = [ BlockEnum.Start, BlockEnum.TriggerSchedule, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin, ] const getDuplicateEndOutputMessages = ( nodes: Node[], t: ReturnType['t'], ) => { const variableOccurrences = new Map() nodes.forEach((node) => { if (node.type !== CUSTOM_NODE || node.data.type !== BlockEnum.End) return const outputs = (node.data as { outputs?: Array<{ variable?: string }> }).outputs || [] outputs.forEach((output) => { const variable = output.variable?.trim() if (!variable) return const occurrences = variableOccurrences.get(variable) || [] occurrences.push(node.id) variableOccurrences.set(variable, occurrences) }) }) const nodeMessages = new Map() variableOccurrences.forEach((nodeIds, variable) => { if (nodeIds.length <= 1) return Array.from(new Set(nodeIds)).forEach((nodeId) => { const messages = nodeMessages.get(nodeId) || [] messages.push(t(($) => $['errorMsg.duplicateOutputVariable'], { ns: 'workflow', variable })) nodeMessages.set(nodeId, messages) }) }) return nodeMessages } export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?: FlowType }) => { const { t } = useTranslation() const language = useGetLanguage() const { nodesMap: nodesExtraData } = useNodesMetaData() const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const { data: mcpTools } = useAllMCPTools() const dataSourceList = useStore((s) => s.dataSourceList) const { data: strategyProviders } = useStrategyProviders() const { data: triggerPlugins } = useAllTriggerPlugins() const datasetsDetail = useDatasetsDetailStore((s) => s.datasetsDetail) const getToolIcon = useGetToolIcon() const appMode = useAppStore.getState().appDetail?.mode const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT const modelProviders = useProviderContextSelector((s) => s.modelProviders) const workflowStore = useWorkflowStore() const configsMap = useHooksStore((s) => s.configsMap) const map = useNodesAvailableVarList(nodes) const inlineAgentNodes = useMemo( () => nodes.filter( (node) => node.type === CUSTOM_NODE && isAgentV2NodeData(node.data) && hasValidInlineAgentBinding(node.data), ), [nodes], ) const inlineAgentMissingReferences = useQueries({ queries: !configsMap?.flowId || (configsMap.flowType !== FlowType.appFlow && configsMap.flowType !== FlowType.snippet) ? [] : inlineAgentNodes.map((node) => configsMap.flowType === FlowType.snippet ? consoleQuery.snippets.bySnippetId.workflows.draft.nodes.byNodeId.agentComposer.get.queryOptions( { input: { params: { snippet_id: configsMap.flowId, node_id: node.id, }, }, }, ) : consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.get.queryOptions( { input: { params: { app_id: configsMap.flowId, node_id: node.id, }, }, }, ), ), combine: (results) => { const missingReferences: Record< string, { hasMissingFiles: boolean; hasMissingSkills: boolean } > = {} results.forEach((result, index) => { const nodeId = inlineAgentNodes[index]?.id const agentSoul = result.data?.agent_soul if (!nodeId || !agentSoul) return const hasMissingFiles = agentSoul.config_files?.some((file) => file.is_missing === true) const hasMissingSkills = agentSoul.config_skills?.some((skill) => skill.is_missing === true) if (!hasMissingFiles && !hasMissingSkills) return missingReferences[nodeId] = { hasMissingFiles: !!hasMissingFiles, hasMissingSkills: !!hasMissingSkills, } }) return missingReferences }, }) const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank) const knowledgeBaseEmbeddingProviders = useMemo(() => { const providers = new Set() nodes.forEach((node) => { if (node.type !== CUSTOM_NODE || node.data.type !== BlockEnum.KnowledgeBase) return const knowledgeBaseData = node.data as CommonNodeType if (knowledgeBaseData.indexing_technique !== IndexMethodEnum.QUALIFIED) return const provider = knowledgeBaseData.embedding_model_provider if (provider) providers.add(provider) }) return [...providers] }, [nodes]) const knowledgeBaseProviderModelMap = useQueries({ queries: knowledgeBaseEmbeddingProviders.map((provider) => consoleQuery.workspaces.current.modelProviders.byProvider.models.get.queryOptions({ input: { params: { provider } }, enabled: !!provider, refetchOnWindowFocus: false, select: normalizeModelProviderModelsResponse, }), ), combine: (results) => { const modelMap: Partial> = {} knowledgeBaseEmbeddingProviders.forEach((provider, index) => { const models = results[index]?.data if (models) modelMap[provider] = models }) return modelMap }, }) const getCheckData = useCallback( (data: CommonNodeType<{}>) => { let checkData = data if (data.type === BlockEnum.KnowledgeRetrieval) { const datasetIds = (data as CommonNodeType).dataset_ids const _datasets = datasetIds.reduce((acc, id) => { if (datasetsDetail[id]) acc.push(datasetsDetail[id]) return acc }, []) checkData = { ...data, _datasets, } as CommonNodeType } else if (data.type === BlockEnum.KnowledgeBase) { const modelProviderName = (data as CommonNodeType) .embedding_model_provider checkData = { ...data, _embeddingModelList: embeddingModelList, _embeddingProviderModelList: modelProviderName ? knowledgeBaseProviderModelMap[modelProviderName] : undefined, _rerankModelList: rerankModelList, } as CommonNodeType } return checkData }, [datasetsDetail, embeddingModelList, knowledgeBaseProviderModelMap, rerankModelList], ) const needWarningNodes = useMemo(() => { const list: ChecklistItem[] = [] const filteredNodes = nodes.filter((node) => node.type === CUSTOM_NODE) const duplicateEndOutputMessages = getDuplicateEndOutputMessages(filteredNodes, t) const { validNodes } = getValidTreeNodes(filteredNodes, edges) const installedPluginIds = new Set(modelProviders.map((p) => extractPluginId(p.provider))) for (let i = 0; i < filteredNodes.length; i++) { const node = filteredNodes[i] let moreDataForCheckValid: CheckValidExtraData let usedVars: ValueSelector[] = [] if (node!.data.type === BlockEnum.Tool) moreDataForCheckValid = getToolCheckParams( node!.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language, ) 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 && !isAgentV2NodeData(node!.data)) { const data = node!.data as AgentNodeType const isReadyForCheckValid = !!strategyProviders const provider = strategyProviders?.find( (provider) => provider.declaration.identity.name === data.agent_strategy_provider_name, ) const strategy = provider?.declaration.strategies?.find( (s) => s.identity.name === data.agent_strategy_name, ) moreDataForCheckValid = { provider, strategy, language, isReadyForCheckValid, } } else { usedVars = getNodeUsedVars(node!).filter((v) => v.length > 0) } if (node!.type === CUSTOM_NODE) { const checkData = getCheckData(node!.data) const validator = nodesExtraData?.[getNodeCatalogType(node!.data)]?.checkValid const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList, }) const errorMessages: string[] = [] if (isPluginMissing) { errorMessages.push(t(($) => $['nodes.common.pluginNotInstalled'], { ns: 'workflow' })) } else { if (node!.data.type === BlockEnum.LLM) { const modelProvider = (node!.data as CommonNodeType<{ model?: ModelConfig }>).model ?.provider const modelIssue = getLLMModelIssue({ modelProvider, isModelProviderInstalled: isLLMModelProviderInstalled( modelProvider, installedPluginIds, ), }) if (modelIssue === LLMModelIssueCode.providerPluginUnavailable) errorMessages.push(t(($) => $['errorMsg.configureModel'], { ns: 'workflow' })) } if (validator) { const validationError = validator( checkData, t, withFlowType(moreDataForCheckValid, options?.flowType), ).errorMessage if (validationError) errorMessages.push(validationError) } const missingReferences = inlineAgentMissingReferences[node!.id] if (missingReferences?.hasMissingFiles) errorMessages.push( t(($) => $['agentDetail.configure.files.missing'], { ns: 'agentV2' }), ) if (missingReferences?.hasMissingSkills) errorMessages.push( t(($) => $['agentDetail.configure.skills.missing'], { ns: 'agentV2' }), ) const availableVars = map[node!.id]!.availableVars let hasInvalidVar = false for (const variable of usedVars) { if (hasInvalidVar) break if (isSpecialVar(variable[0]!)) continue const usedNode = availableVars.find((v) => v.nodeId === variable?.[0]) if (!usedNode || !usedNode.vars.some((v) => v.variable === variable?.[1])) hasInvalidVar = true } if (hasInvalidVar) errorMessages.push(t(($) => $['errorMsg.invalidVariable'], { ns: 'workflow' })) errorMessages.push(...(duplicateEndOutputMessages.get(node!.id) || [])) } const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false const isStartPlaceholderNode = node!.data.type === BlockEnum.StartPlaceholder const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta || isStartPlaceholderNode : true const isUnconnected = !validNodes.some((n) => n.id === node!.id) const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck) if (shouldShowError) { list.push({ id: node!.id, type: node!.data.type, title: node!.data.title, toolIcon, unConnected: isUnconnected && !canSkipConnectionCheck, errorMessages, canNavigate: !isPluginMissing, disableGoTo: isPluginMissing, isPluginMissing, pluginUniqueIdentifier: isPluginMissing ? (node!.data as { plugin_unique_identifier?: string }).plugin_unique_identifier : undefined, }) } } } // Check for start nodes (including triggers) if (shouldCheckStartNode) { const startNodesFiltered = nodes.filter((node) => START_NODE_TYPES.includes(node.data.type as BlockEnum), ) const hasStartPlaceholderNode = nodes.some( (node) => node.data.type === BlockEnum.StartPlaceholder, ) if (startNodesFiltered.length === 0 && !hasStartPlaceholderNode) { list.push({ id: 'start-node-required', type: BlockEnum.Start, title: t(($) => $['panel.startNode'], { ns: 'workflow' }), errorMessages: [t(($) => $['common.needStartNode'], { ns: 'workflow' })], canNavigate: false, }) } } const isRequiredNodesType = Object.keys(nodesExtraData!).filter( (key: any) => (nodesExtraData as any)[key].metaData.isRequired, ) isRequiredNodesType.forEach((type: string) => { if (!filteredNodes.some((node) => node.data.type === type)) { list.push({ id: `${type}-need-added`, type, title: t(($) => $[`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>], { ns: 'workflow', }), errorMessages: [ t(($) => $['common.needAdd'], { ns: 'workflow', node: t(($) => $[`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>], { ns: 'workflow', }), }), ], canNavigate: false, }) } }) return list }, [ nodes, edges, shouldCheckStartNode, nodesExtraData, buildInTools, customTools, workflowTools, mcpTools, language, dataSourceList, triggerPlugins, getToolIcon, strategyProviders, getCheckData, t, map, modelProviders, inlineAgentMissingReferences, options?.flowType, ]) useEffect(() => { const currentChecklistItems = workflowStore.getState().checklistItems if (isDeepEqual(currentChecklistItems, needWarningNodes)) return workflowStore.setState({ checklistItems: needWarningNodes }) }, [needWarningNodes, workflowStore]) return needWarningNodes } export const useChecklistBeforePublish = () => { const { t } = useTranslation() const language = useGetLanguage() const queryClient = useQueryClient() const store = useStoreApi() const { nodesMap: nodesExtraData } = useNodesMetaData() const { data: strategyProviders } = useStrategyProviders() const modelProviders = useProviderContextSelector((s) => s.modelProviders) const updateDatasetsDetail = useDatasetsDetailStore((s) => s.updateDatasetsDetail) const updateTimeRef = useRef(0) const workflowStore = useWorkflowStore() const { getNodesAvailableVarList } = useGetNodesAvailableVarList() const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const flowType = useHooksStore((s) => s.configsMap?.flowType) const appMode = useAppStore.getState().appDetail?.mode const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT const getCheckData = useCallback( ( data: CommonNodeType, datasets: DataSet[], embeddingProviderModelMap?: Partial>, ) => { let checkData = data if (data.type === BlockEnum.KnowledgeRetrieval) { const datasetIds = (data as CommonNodeType).dataset_ids const datasetsDetail = datasets.reduce>((acc, dataset) => { acc[dataset.id] = dataset return acc }, {}) const _datasets = datasetIds.reduce((acc, id) => { if (datasetsDetail[id]) acc.push(datasetsDetail[id]) return acc }, []) checkData = { ...data, _datasets, } as CommonNodeType } else if (data.type === BlockEnum.KnowledgeBase) { const modelProviderName = (data as CommonNodeType) .embedding_model_provider checkData = { ...data, _embeddingModelList: embeddingModelList, _embeddingProviderModelList: modelProviderName ? embeddingProviderModelMap?.[modelProviderName] : undefined, _rerankModelList: rerankModelList, } as CommonNodeType } return checkData }, [embeddingModelList, rerankModelList], ) const handleCheckBeforePublish = useCallback(async () => { const { getNodes, edges } = store.getState() const { dataSourceList } = workflowStore.getState() const nodes = getNodes() const filteredNodes = nodes.filter((node) => node.type === CUSTOM_NODE) const duplicateEndOutputMessages = getDuplicateEndOutputMessages(filteredNodes, t) const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges) if (maxDepth > MAX_TREE_DEPTH) { toast.error(t(($) => $['common.maxTreeDepth'], { ns: 'workflow', depth: MAX_TREE_DEPTH })) return false } const knowledgeBaseEmbeddingProviders = [ ...new Set( filteredNodes .filter((node) => node.data.type === BlockEnum.KnowledgeBase) .map((node) => node.data as CommonNodeType) .filter((node) => node.indexing_technique === IndexMethodEnum.QUALIFIED) .map((node) => node.embedding_model_provider) .filter((provider): provider is string => !!provider), ), ] const fetchKnowledgeBaseProviderModelMap = async () => { const modelMap: Partial> = {} await Promise.all( knowledgeBaseEmbeddingProviders.map(async (provider) => { try { const modelList = await queryClient.fetchQuery( consoleQuery.workspaces.current.modelProviders.byProvider.models.get.queryOptions({ input: { params: { provider } }, }), ) if (modelList.data) modelMap[provider] = normalizeModelProviderModelsResponse(modelList) } catch {} }), ) return modelMap } const fetchLatestDatasets = async (): Promise => { const allDatasetIds = new Set() filteredNodes.forEach((node) => { if (node.data.type !== BlockEnum.KnowledgeRetrieval) return const datasetIds = (node.data as CommonNodeType).dataset_ids datasetIds.forEach((id) => allDatasetIds.add(id)) }) if (allDatasetIds.size === 0) return [] updateTimeRef.current = updateTimeRef.current + 1 const currUpdateTime = updateTimeRef.current const { data: datasetsDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: [...allDatasetIds] }, }) if (currUpdateTime < updateTimeRef.current) return null if (datasetsDetail?.length) updateDatasetsDetail(datasetsDetail) return datasetsDetail || [] } const [embeddingProviderModelMap, datasets] = await Promise.all([ fetchKnowledgeBaseProviderModelMap(), fetchLatestDatasets(), ]) if (datasets === null) return false const installedPluginIds = new Set(modelProviders.map((p) => extractPluginId(p.provider))) const map = getNodesAvailableVarList(nodes) for (let i = 0; i < filteredNodes.length; i++) { const node = filteredNodes[i] let moreDataForCheckValid: CheckValidExtraData let usedVars: ValueSelector[] = [] if (node!.data.type === BlockEnum.Tool) moreDataForCheckValid = getToolCheckParams( node!.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language, ) if (node!.data.type === BlockEnum.DataSource) moreDataForCheckValid = getDataSourceCheckParams( node!.data as DataSourceNodeType, dataSourceList || [], language, ) if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) { const data = node!.data as AgentNodeType const isReadyForCheckValid = !!strategyProviders const provider = strategyProviders?.find( (provider) => provider.declaration.identity.name === data.agent_strategy_provider_name, ) const strategy = provider?.declaration.strategies?.find( (s) => s.identity.name === data.agent_strategy_name, ) moreDataForCheckValid = { provider, strategy, language, isReadyForCheckValid, } } else { usedVars = getNodeUsedVars(node!).filter((v) => v.length > 0) } if (node!.data.type === BlockEnum.LLM) { const modelProvider = (node!.data as CommonNodeType<{ model?: ModelConfig }>).model ?.provider const modelIssue = getLLMModelIssue({ modelProvider, isModelProviderInstalled: isLLMModelProviderInstalled(modelProvider, installedPluginIds), }) if (modelIssue === LLMModelIssueCode.providerPluginUnavailable) { toast.error( `[${node!.data.title}] ${t(($) => $['errorMsg.configureModel'], { ns: 'workflow' })}`, ) return false } } const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap) const { errorMessage } = nodesExtraData![getNodeCatalogType(node!.data)].checkValid( checkData, t, withFlowType(moreDataForCheckValid, flowType), ) if (errorMessage) { toast.error(`[${node!.data.title}] ${errorMessage}`) return false } const duplicateOutputMessages = duplicateEndOutputMessages.get(node!.id) || [] if (duplicateOutputMessages.length > 0) { toast.error(`[${node!.data.title}] ${duplicateOutputMessages[0]}`) return false } const availableVars = map[node!.id]!.availableVars for (const variable of usedVars) { const isSpecialVars = isSpecialVar(variable[0]!) if (!isSpecialVars) { const usedNode = availableVars.find((v) => v.nodeId === variable?.[0]) if (usedNode) { const usedVar = usedNode.vars.find((v) => v.variable === variable?.[1]) if (!usedVar) { toast.error( `[${node!.data.title}] ${t(($) => $['errorMsg.invalidVariable'], { ns: 'workflow' })}`, ) return false } } else { toast.error( `[${node!.data.title}] ${t(($) => $['errorMsg.invalidVariable'], { ns: 'workflow' })}`, ) return false } } } const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true const isUnconnected = !validNodes.some((n) => n.id === node!.id) if (isUnconnected && !canSkipConnectionCheck) { toast.error( `[${node!.data.title}] ${t(($) => $['common.needConnectTip'], { ns: 'workflow' })}`, ) return false } } if (shouldCheckStartNode) { const startNodesFiltered = nodes.filter((node) => START_NODE_TYPES.includes(node.data.type as BlockEnum), ) if (startNodesFiltered.length === 0) { toast.error(t(($) => $['common.needStartNode'], { ns: 'workflow' })) 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.some((node) => node.data.type === type)) { toast.error( t(($) => $['common.needAdd'], { ns: 'workflow', node: t(($) => $[`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>], { ns: 'workflow', }), }), ) return false } } return true }, [ store, workflowStore, getNodesAvailableVarList, shouldCheckStartNode, nodesExtraData, t, updateDatasetsDetail, buildInTools, customTools, workflowTools, language, getCheckData, queryClient, strategyProviders, modelProviders, flowType, ]) return { handleCheckBeforePublish, } } export const useWorkflowRunValidation = () => { const { t } = useTranslation() const nodes = useNodes() const edges = useEdges() const flowType = useHooksStore((s) => s.configsMap?.flowType) const needWarningNodes = useChecklist(nodes, edges, { flowType }) const validateBeforeRun = useCallback(() => { if (needWarningNodes.length > 0) { toast.error(t(($) => $['panel.checklistTip'], { ns: 'workflow' })) return false } return true }, [needWarningNodes, t]) return { validateBeforeRun, hasValidationErrors: needWarningNodes.length > 0, warningNodes: needWarningNodes, } }