diff --git a/web/app/components/workflow/block-selector/data-sources.tsx b/web/app/components/workflow/block-selector/data-sources.tsx index 3961f63dbe..b98a52dcff 100644 --- a/web/app/components/workflow/block-selector/data-sources.tsx +++ b/web/app/components/workflow/block-selector/data-sources.tsx @@ -63,6 +63,7 @@ const DataSources = ({ datasource_name: toolDefaultValue?.tool_name, datasource_label: toolDefaultValue?.tool_label, title: toolDefaultValue?.title, + plugin_unique_identifier: toolDefaultValue?.plugin_unique_identifier, } // Update defaultValue with fileExtensions if this is the local file data source if (toolDefaultValue?.provider_id === 'langgenius/file' && toolDefaultValue?.provider_name === 'file') { diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index e2c28602f8..01c319327a 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -72,6 +72,7 @@ const ToolItem: FC = ({ provider_type: provider.type, provider_name: provider.name, plugin_id: provider.plugin_id, + plugin_unique_identifier: provider.plugin_unique_identifier, provider_icon: normalizeProviderIcon(provider.icon), tool_name: payload.name, tool_label: payload.label[language], diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 5ac043e933..38be8d19d6 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -94,6 +94,7 @@ const Tool: FC = ({ provider_type: payload.type, provider_name: payload.name, plugin_id: payload.plugin_id, + plugin_unique_identifier: payload.plugin_unique_identifier, provider_icon: normalizeProviderIcon(payload.icon), tool_name: tool.name, tool_label: tool.label[language], @@ -175,6 +176,7 @@ const Tool: FC = ({ provider_type: payload.type, provider_name: payload.name, plugin_id: payload.plugin_id, + plugin_unique_identifier: payload.plugin_unique_identifier, provider_icon: normalizeProviderIcon(payload.icon), tool_name: tool.name, tool_label: tool.label[language], diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 512621a552..e995974b87 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -59,6 +59,7 @@ export type ToolDefaultValue = PluginCommonDefaultValue & { meta?: PluginMeta plugin_id?: string provider_icon?: Collection['icon'] + plugin_unique_identifier?: string } export type DataSourceDefaultValue = Omit & { @@ -69,6 +70,7 @@ export type DataSourceDefaultValue = Omit void + shouldDim: boolean +} + +const useToolInstallation = (data: ToolNodeType): InstallationState => { + const builtInQuery = useAllBuiltInTools() + const customQuery = useAllCustomTools() + const workflowQuery = useAllWorkflowTools() + const mcpQuery = useAllMCPTools() + const invalidateTools = useInvalidToolsByType(data.provider_type) + + const collectionInfo = useMemo(() => { + switch (data.provider_type) { + case CollectionType.builtIn: + return { + list: builtInQuery.data, + isLoading: builtInQuery.isLoading, + } + case CollectionType.custom: + return { + list: customQuery.data, + isLoading: customQuery.isLoading, + } + case CollectionType.workflow: + return { + list: workflowQuery.data, + isLoading: workflowQuery.isLoading, + } + case CollectionType.mcp: + return { + list: mcpQuery.data, + isLoading: mcpQuery.isLoading, + } + default: + return undefined + } + }, [ + builtInQuery.data, + builtInQuery.isLoading, + customQuery.data, + customQuery.isLoading, + data.provider_type, + mcpQuery.data, + mcpQuery.isLoading, + workflowQuery.data, + workflowQuery.isLoading, + ]) + + const collection = collectionInfo?.list + const isLoading = collectionInfo?.isLoading ?? false + const isResolved = !!collectionInfo && !isLoading + + const matchedCollection = useMemo(() => { + if (!collection || !collection.length) + return undefined + + return collection.find((toolWithProvider) => { + if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id) + return true + if (canFindTool(toolWithProvider.id, data.provider_id)) + return true + if (toolWithProvider.name === data.provider_name) + return true + return false + }) + }, [collection, data.plugin_id, data.provider_id, data.provider_name]) + + const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id + const canInstall = Boolean(data.plugin_unique_identifier) + + const onInstallSuccess = useCallback(() => { + if (invalidateTools) + invalidateTools() + }, [invalidateTools]) + + return { + isChecking: !!collectionInfo && !isResolved, + isMissing: isResolved && !matchedCollection, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } +} + +const useTriggerInstallation = (data: PluginTriggerNodeType): InstallationState => { + const triggerPluginsQuery = useAllTriggerPlugins() + const invalidateTriggers = useInvalidateAllTriggerPlugins() + + const triggerProviders = triggerPluginsQuery.data + const isLoading = triggerPluginsQuery.isLoading + + const matchedProvider = useMemo(() => { + if (!triggerProviders || !triggerProviders.length) + return undefined + + return triggerProviders.find(provider => + provider.name === data.provider_name + || provider.id === data.provider_id + || (data.plugin_id && provider.plugin_id === data.plugin_id), + ) + }, [ + data.plugin_id, + data.provider_id, + data.provider_name, + triggerProviders, + ]) + + const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id + const canInstall = Boolean(data.plugin_unique_identifier) + + const onInstallSuccess = useCallback(() => { + invalidateTriggers() + }, [invalidateTriggers]) + + return { + isChecking: isLoading, + isMissing: !isLoading && !!triggerProviders && !matchedProvider, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } +} + +const useDataSourceInstallation = (data: DataSourceNodeType): InstallationState => { + const dataSourceList = useStore(s => s.dataSourceList) + const invalidateDataSourceList = useInvalidDataSourceList() + + const matchedPlugin = useMemo(() => { + if (!dataSourceList || !dataSourceList.length) + return undefined + + return dataSourceList.find((item) => { + if (data.plugin_unique_identifier && item.plugin_unique_identifier === data.plugin_unique_identifier) + return true + if (data.plugin_id && item.plugin_id === data.plugin_id) + return true + if (data.provider_name && item.provider === data.provider_name) + return true + return false + }) + }, [data.plugin_id, data.plugin_unique_identifier, data.provider_name, dataSourceList]) + + const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id + const canInstall = Boolean(data.plugin_unique_identifier) + + const onInstallSuccess = useCallback(() => { + invalidateDataSourceList() + }, [invalidateDataSourceList]) + + const hasLoadedList = dataSourceList !== undefined + + return { + isChecking: !hasLoadedList, + isMissing: hasLoadedList && !matchedPlugin, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } +} + +export const useNodePluginInstallation = (data: CommonNodeType): InstallationState => { + const toolInstallation = useToolInstallation(data as ToolNodeType) + const triggerInstallation = useTriggerInstallation(data as PluginTriggerNodeType) + const dataSourceInstallation = useDataSourceInstallation(data as DataSourceNodeType) + + switch (data.type as BlockEnum) { + case BlockEnum.Tool: + return toolInstallation + case BlockEnum.TriggerPlugin: + return triggerInstallation + case BlockEnum.DataSource: + return dataSourceInstallation + default: + return { + isChecking: false, + isMissing: false, + uniqueIdentifier: undefined, + canInstall: false, + onInstallSuccess: () => undefined, + } + } +} diff --git a/web/app/components/workflow/nodes/data-source/node.tsx b/web/app/components/workflow/nodes/data-source/node.tsx index f97098e52f..6e6c565dc2 100644 --- a/web/app/components/workflow/nodes/data-source/node.tsx +++ b/web/app/components/workflow/nodes/data-source/node.tsx @@ -1,10 +1,36 @@ import type { FC } from 'react' import { memo } from 'react' -import type { DataSourceNodeType } from './types' import type { NodeProps } from '@/app/components/workflow/types' -const Node: FC> = () => { +import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' +import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' +import type { DataSourceNodeType } from './types' + +const Node: FC> = ({ + data, +}) => { + const { + isChecking, + isMissing, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } = useNodePluginInstallation(data) + + const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier + + if (!showInstallButton) + return null + return ( -
+
+
+ +
) } diff --git a/web/app/components/workflow/nodes/data-source/types.ts b/web/app/components/workflow/nodes/data-source/types.ts index da887244b8..cd22b305d1 100644 --- a/web/app/components/workflow/nodes/data-source/types.ts +++ b/web/app/components/workflow/nodes/data-source/types.ts @@ -30,6 +30,7 @@ export type DataSourceNodeType = CommonNodeType & { datasource_label: string datasource_parameters: ToolVarInputs datasource_configurations: Record + plugin_unique_identifier?: string } export type CustomRunFormProps = { diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index 8cc3ec580d..466cbb577f 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -1,46 +1,68 @@ import type { FC } from 'react' import React from 'react' -import type { ToolNodeType } from './types' import type { NodeProps } from '@/app/components/workflow/types' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' +import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' +import type { ToolNodeType } from './types' const Node: FC> = ({ data, }) => { const { tool_configurations, paramSchemas } = data const toolConfigs = Object.keys(tool_configurations || {}) + const { + isChecking, + isMissing, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } = useNodePluginInstallation(data) + const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier - if (!toolConfigs.length) + const hasConfigs = toolConfigs.length > 0 + + if (!showInstallButton && !hasConfigs) return null return ( -
-
- {toolConfigs.map((key, index) => ( -
-
- {key} +
+ {showInstallButton && ( +
+ +
+ )} + {hasConfigs && ( +
+ {toolConfigs.map((key, index) => ( +
+
+ {key} +
+ {typeof tool_configurations[key].value === 'string' && ( +
+ {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value} +
+ )} + {typeof tool_configurations[key].value === 'number' && ( +
+ {Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} +
+ )} + {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && ( +
+ {tool_configurations[key].model} +
+ )}
- {typeof tool_configurations[key].value === 'string' && ( -
- {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value} -
- )} - {typeof tool_configurations[key].value === 'number' && ( -
- {Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} -
- )} - {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && ( -
- {tool_configurations[key].model} -
- )} -
- - ))} - -
+ ))} +
+ )}
) } diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index 12a4283cf6..6e6ef858dc 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -22,4 +22,5 @@ export type ToolNodeType = CommonNodeType & { params?: Record plugin_id?: string provider_icon?: Collection['icon'] + plugin_unique_identifier?: string } diff --git a/web/app/components/workflow/nodes/trigger-plugin/node.tsx b/web/app/components/workflow/nodes/trigger-plugin/node.tsx index 9be517e97d..bfd807f30e 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/node.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/node.tsx @@ -3,6 +3,8 @@ import type { NodeProps } from '@/app/components/workflow/types' import type { FC } from 'react' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' +import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' import type { PluginTriggerNodeType } from './types' import useConfig from './use-config' @@ -42,6 +44,14 @@ const Node: FC> = ({ const { subscriptions } = useConfig(id, data) const { config = {}, subscription_id } = data const configKeys = Object.keys(config) + const { + isChecking, + isMissing, + uniqueIdentifier, + canInstall, + onInstallSuccess, + } = useNodePluginInstallation(data) + const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier const { t } = useTranslation() @@ -50,7 +60,17 @@ const Node: FC> = ({ }, [subscription_id, subscriptions]) return ( -
+
+ {showInstallButton && ( +
+ +
+ )}
{!isValidSubscription && } {isValidSubscription && configKeys.map((key, index) => ( diff --git a/web/app/components/workflow/nodes/trigger-plugin/types.ts b/web/app/components/workflow/nodes/trigger-plugin/types.ts index 43268e9096..6dba97d795 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/types.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/types.ts @@ -16,6 +16,7 @@ export type PluginTriggerNodeType = CommonNodeType & { event_node_version?: string plugin_id?: string config?: Record + plugin_unique_identifier?: string } // Use base types directly diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index a4e7002960..d126daa350 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -451,6 +451,7 @@ export type MoreInfo = { export type ToolWithProvider = Collection & { tools: Tool[] meta: PluginMeta + plugin_unique_identifier?: string } export type RAGRecommendedPlugins = {