diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/use-llm-model-plugin-installed.ts b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/use-llm-model-plugin-installed.ts index 9b8aac8cb5..0aa98881b3 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/use-llm-model-plugin-installed.ts +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/use-llm-model-plugin-installed.ts @@ -1,12 +1,8 @@ import type { WorkflowNodesMap } from '@/app/components/base/prompt-editor/types' import { BlockEnum } from '@/app/components/workflow/types' +import { extractPluginId } from '@/app/components/workflow/utils/plugin' import { useProviderContextSelector } from '@/context/provider-context' -function extractPluginId(provider: string): string { - const parts = provider.split('/') - return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : provider -} - export function useLlmModelPluginInstalled( nodeId: string, workflowNodesMap: WorkflowNodesMap | undefined, diff --git a/web/app/components/workflow/header/checklist/node-group.tsx b/web/app/components/workflow/header/checklist/node-group.tsx index fa2147ae37..3c4a3a33da 100644 --- a/web/app/components/workflow/header/checklist/node-group.tsx +++ b/web/app/components/workflow/header/checklist/node-group.tsx @@ -25,12 +25,12 @@ export const ChecklistNodeGroup = memo(({ const subItems = useMemo(() => { const items: ChecklistSubItem[] = [] - if (item.errorMessage) - items.push({ key: 'error', message: item.errorMessage }) + for (let i = 0; i < item.errorMessages.length; i++) + items.push({ key: `error-${i}`, message: item.errorMessages[i] }) if (item.unConnected) items.push({ key: 'unconnected', message: t('common.needConnectTip', { ns: 'workflow' }) }) return items - }, [item.errorMessage, item.unConnected, t]) + }, [item.errorMessages, item.unConnected, t]) return (
diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index 1b37055134..7796361d6c 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -195,7 +195,7 @@ describe('useChecklist', () => { const warning = result.current.find((item: ChecklistItem) => item.id === 'llm') expect(warning).toBeDefined() - expect(warning!.errorMessage).toBe('Model not configured') + expect(warning!.errorMessages).toContain('Model not configured') }) it('should report missing start node in workflow mode', () => { diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 4a74f13fd7..823a2b5d1d 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -8,6 +8,7 @@ import type { CommonEdgeType, CommonNodeType, Edge, + ModelConfig, Node, ValueSelector, } from '../types' @@ -28,6 +29,7 @@ import { useModelList } from '@/app/components/header/account-setting/model-prov 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 { fetchDatasets } from '@/service/datasets' import { useStrategyProviders } from '@/service/use-strategy' import { @@ -56,6 +58,7 @@ import { getToolCheckParams, getValidTreeNodes, } from '../utils' +import { extractPluginId } from '../utils/plugin' import { getTriggerCheckParams } from '../utils/trigger' import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list' @@ -65,7 +68,7 @@ export type ChecklistItem = { title: string toolIcon?: string | Emoji unConnected?: boolean - errorMessage?: string + errorMessages: string[] canNavigate: boolean disableGoTo?: boolean isPluginMissing?: boolean @@ -100,6 +103,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { 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 map = useNodesAvailableVarList(nodes) const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) @@ -133,6 +137,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const list: ChecklistItem[] = [] const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE) 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] @@ -170,39 +175,44 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid const isPluginMissing = PLUGIN_DEPENDENT_TYPES.includes(node.data.type as BlockEnum) && node.data._pluginInstallLocked - // Check if plugin is installed for plugin-dependent nodes first - let errorMessage: string | undefined - if (isPluginMissing) - errorMessage = t('nodes.common.pluginNotInstalled', { ns: 'workflow' }) - else if (validator) - errorMessage = validator(checkData, t, moreDataForCheckValid).errorMessage + const errorMessages: string[] = [] - if (!errorMessage) { - 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) - errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' }) - } - else { - errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' }) - } - } + 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 + if (modelProvider && !installedPluginIds.has(extractPluginId(modelProvider))) + errorMessages.push(t('errorMsg.configureModel', { ns: 'workflow' })) } + + if (validator) { + const validationError = validator(checkData, t, moreDataForCheckValid).errorMessage + if (validationError) + errorMessages.push(validationError) + } + + 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.find(v => v.variable === variable?.[1])) + hasInvalidVar = true + } + if (hasInvalidVar) + errorMessages.push(t('errorMsg.invalidVariable', { ns: 'workflow' })) } - // 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) + const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck) if (shouldShowError) { list.push({ @@ -211,7 +221,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { title: node.data.title, toolIcon, unConnected: isUnconnected && !canSkipConnectionCheck, - errorMessage, + errorMessages, canNavigate: !isPluginMissing, disableGoTo: isPluginMissing, isPluginMissing, @@ -231,7 +241,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { id: 'start-node-required', type: BlockEnum.Start, title: t('panel.startNode', { ns: 'workflow' }), - errorMessage: t('common.needStartNode', { ns: 'workflow' }), + errorMessages: [t('common.needStartNode', { ns: 'workflow' })], canNavigate: false, }) } @@ -244,18 +254,15 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { list.push({ id: `${type}-need-added`, type, - // We don't have enough type info for t() here - title: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }), - - errorMessage: t('common.needAdd', { ns: 'workflow', node: 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, language, dataSourceList, triggerPlugins, getToolIcon, strategyProviders, getCheckData, t, map]) + }, [nodes, edges, shouldCheckStartNode, nodesExtraData, buildInTools, customTools, workflowTools, language, dataSourceList, triggerPlugins, getToolIcon, strategyProviders, getCheckData, t, map, modelProviders]) return needWarningNodes } @@ -267,6 +274,7 @@ export const useChecklistBeforePublish = () => { const store = useStoreApi() const { nodesMap: nodesExtraData } = useNodesMetaData() const { data: strategyProviders } = useStrategyProviders() + const modelProviders = useProviderContextSelector(s => s.modelProviders) const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail) const updateTime = useRef(0) const workflowStore = useWorkflowStore() @@ -341,6 +349,7 @@ export const useChecklistBeforePublish = () => { updateDatasetsDetail(datasetsDetail) } } + 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] @@ -367,6 +376,15 @@ export const useChecklistBeforePublish = () => { 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 + if (modelProvider && !installedPluginIds.has(extractPluginId(modelProvider))) { + notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.configureModel', { ns: 'workflow' })}` }) + return false + } + } + const checkData = getCheckData(node.data, datasets) const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid) @@ -425,7 +443,7 @@ export const useChecklistBeforePublish = () => { } return true - }, [store, workflowStore, getNodesAvailableVarList, shouldCheckStartNode, nodesExtraData, notify, t, updateDatasetsDetail, buildInTools, customTools, workflowTools, language, getCheckData, strategyProviders]) + }, [store, workflowStore, getNodesAvailableVarList, shouldCheckStartNode, nodesExtraData, notify, t, updateDatasetsDetail, buildInTools, customTools, workflowTools, language, getCheckData, strategyProviders, modelProviders]) return { handleCheckBeforePublish, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index bf8649111d..3f2c20dc86 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -159,10 +159,10 @@ const useLastRun = ({ if (!warningForNode) return false - if (warningForNode.unConnected && !warningForNode.errorMessage) + if (warningForNode.unConnected && warningForNode.errorMessages.length === 0) return false - const message = warningForNode.errorMessage || 'This node has unresolved checklist issues' + const message = warningForNode.errorMessages[0] || 'This node has unresolved checklist issues' Toast.notify({ type: 'error', message }) return true }, [warningNodes, id]) diff --git a/web/app/components/workflow/utils/plugin.ts b/web/app/components/workflow/utils/plugin.ts new file mode 100644 index 0000000000..6ebb088181 --- /dev/null +++ b/web/app/components/workflow/utils/plugin.ts @@ -0,0 +1,4 @@ +export function extractPluginId(provider: string): string { + const parts = provider.split('/') + return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : provider +} diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 8cb45d1aa0..b36660c949 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -299,6 +299,7 @@ "error.operations.updatingWorkflow": "updating workflow", "error.startNodeRequired": "Please add a start node first before {{operation}}", "errorMsg.authRequired": "Authorization is required", + "errorMsg.configureModel": "Configure a model", "errorMsg.fieldRequired": "{{field}} is required", "errorMsg.fields.code": "Code", "errorMsg.fields.model": "Model", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index fc6cde81f7..43a72fc863 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -299,6 +299,7 @@ "error.operations.updatingWorkflow": "更新工作流", "error.startNodeRequired": "请先添加开始节点,然后再{{operation}}", "errorMsg.authRequired": "请先授权", + "errorMsg.configureModel": "请配置模型", "errorMsg.fieldRequired": "{{field}} 不能为空", "errorMsg.fields.code": "代码", "errorMsg.fields.model": "模型",