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": "模型",