diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 7e509be5f2..a4d4655988 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -71,26 +71,56 @@ const FeaturedTools = ({ setVisibleCount(INITIAL_VISIBLE_COUNT) }, [plugins]) - const visiblePlugins = useMemo( - () => plugins.slice(0, Math.min(MAX_RECOMMENDED_COUNT, visibleCount)), - [plugins, visibleCount], + const limitedPlugins = useMemo( + () => plugins.slice(0, MAX_RECOMMENDED_COUNT), + [plugins], ) - const installedProviders = useMemo( - () => - visiblePlugins - .map(plugin => providerMap.get(plugin.plugin_id)) - .filter((provider): provider is ToolWithProvider => Boolean(provider)), - [visiblePlugins, providerMap], + const { + installedProviders, + uninstalledPlugins, + } = useMemo(() => { + const installed: ToolWithProvider[] = [] + const uninstalled: Plugin[] = [] + const visitedProviderIds = new Set() + + limitedPlugins.forEach((plugin) => { + const provider = providerMap.get(plugin.plugin_id) + if (provider) { + if (!visitedProviderIds.has(provider.id)) { + installed.push(provider) + visitedProviderIds.add(provider.id) + } + } + else { + uninstalled.push(plugin) + } + }) + + return { + installedProviders: installed, + uninstalledPlugins: uninstalled, + } + }, [limitedPlugins, providerMap]) + + const totalQuota = Math.min(visibleCount, MAX_RECOMMENDED_COUNT) + + const visibleInstalledProviders = useMemo( + () => installedProviders.slice(0, totalQuota), + [installedProviders, totalQuota], ) - const uninstalledPlugins = useMemo( - () => visiblePlugins.filter(plugin => !providerMap.has(plugin.plugin_id)), - [visiblePlugins, providerMap], + const remainingSlots = Math.max(totalQuota - visibleInstalledProviders.length, 0) + + const visibleUninstalledPlugins = useMemo( + () => (remainingSlots > 0 ? uninstalledPlugins.slice(0, remainingSlots) : []), + [uninstalledPlugins, remainingSlots], ) - const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length) - const showEmptyState = !isLoading && visiblePlugins.length === 0 + const totalVisible = visibleInstalledProviders.length + visibleUninstalledPlugins.length + const maxAvailable = Math.min(MAX_RECOMMENDED_COUNT, installedProviders.length + uninstalledPlugins.length) + const showMore = totalVisible < maxAvailable + const showEmptyState = !isLoading && totalVisible === 0 return (
@@ -121,10 +151,10 @@ const FeaturedTools = ({ {!showEmptyState && !isLoading && ( <> - {installedProviders.length > 0 && ( + {visibleInstalledProviders.length > 0 && ( )} - {uninstalledPlugins.length > 0 && ( + {visibleUninstalledPlugins.length > 0 && (
- {uninstalledPlugins.map(plugin => ( + {visibleUninstalledPlugins.map(plugin => ( )} - {!isLoading && visiblePlugins.length > 0 && showMore && ( + {!isLoading && totalVisible > 0 && showMore && (
{ - setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, MAX_RECOMMENDED_COUNT, plugins.length)) + setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, maxAvailable)) }} >
diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 17de4b74d2..a1b2839b45 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,5 +1,5 @@ import type { Dispatch, FC, SetStateAction } from 'react' -import { memo } from 'react' +import { memo, useEffect, useMemo } from 'react' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools' import type { BlockEnum, @@ -15,6 +15,8 @@ import DataSources from './data-sources' import cn from '@/utils/classnames' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' import { useGlobalPublicStore } from '@/context/global-public-context' +import { useWorkflowStore } from '../store' +import { basePath } from '@/utils/var' export type TabsProps = { activeTab: TabsEnum @@ -57,12 +59,66 @@ const Tabs: FC = ({ const { data: mcpTools } = useAllMCPTools() const invalidateBuiltInTools = useInvalidateAllBuiltInTools() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const workflowStore = useWorkflowStore() const inRAGPipeline = dataSources.length > 0 const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading, } = useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline) + const normalizeToolList = useMemo(() => { + return (list?: ToolWithProvider[]) => { + if (!list) + return list + if (!basePath) + return list + let changed = false + const normalized = list.map((provider) => { + if (typeof provider.icon === 'string') { + const icon = provider.icon + const shouldPrefix = Boolean(basePath) + && icon.startsWith('/') + && !icon.startsWith(`${basePath}/`) + + if (shouldPrefix) { + changed = true + return { + ...provider, + icon: `${basePath}${icon}`, + } + } + } + return provider + }) + return changed ? normalized : list + } + }, [basePath]) + + useEffect(() => { + workflowStore.setState((state) => { + const updates: Partial = {} + const normalizedBuiltIn = normalizeToolList(buildInTools) + const normalizedCustom = normalizeToolList(customTools) + const normalizedWorkflow = normalizeToolList(workflowTools) + const normalizedMCP = normalizeToolList(mcpTools) + + if (normalizedBuiltIn !== undefined && state.buildInTools !== normalizedBuiltIn) + updates.buildInTools = normalizedBuiltIn + if (normalizedCustom !== undefined && state.customTools !== normalizedCustom) + updates.customTools = normalizedCustom + if (normalizedWorkflow !== undefined && state.workflowTools !== normalizedWorkflow) + updates.workflowTools = normalizedWorkflow + if (normalizedMCP !== undefined && state.mcpTools !== normalizedMCP) + updates.mcpTools = normalizedMCP + if (!Object.keys(updates).length) + return state + return { + ...state, + ...updates, + } + }) + }, [workflowStore, normalizeToolList, buildInTools, customTools, workflowTools, mcpTools]) + return (
e.stopPropagation()}> { 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 e5aa46b807..e2c28602f8 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -10,6 +10,13 @@ import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' import cn from '@/utils/classnames' import { useTranslation } from 'react-i18next' +import { basePath } from '@/utils/var' + +const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => { + if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`)) + return `${basePath}${icon}` + return icon +} type Props = { provider: ToolWithProvider @@ -64,6 +71,8 @@ const ToolItem: FC = ({ provider_id: provider.id, provider_type: provider.type, provider_name: provider.name, + plugin_id: provider.plugin_id, + provider_icon: normalizeProviderIcon(provider.icon), tool_name: payload.name, tool_label: payload.label[language], tool_description: payload.description[language], diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 0cbe5f6086..5ac043e933 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -16,6 +16,13 @@ import { useTranslation } from 'react-i18next' import { useHover } from 'ahooks' import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' import { Mcp } from '@/app/components/base/icons/src/vender/other' +import { basePath } from '@/utils/var' + +const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => { + if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`)) + return `${basePath}${icon}` + return icon +} type Props = { className?: string @@ -86,6 +93,8 @@ const Tool: FC = ({ provider_id: payload.id, provider_type: payload.type, provider_name: payload.name, + plugin_id: payload.plugin_id, + provider_icon: normalizeProviderIcon(payload.icon), tool_name: tool.name, tool_label: tool.label[language], tool_description: tool.description[language], @@ -165,6 +174,8 @@ const Tool: FC = ({ provider_id: payload.id, provider_type: payload.type, provider_name: payload.name, + plugin_id: payload.plugin_id, + provider_icon: normalizeProviderIcon(payload.icon), tool_name: tool.name, tool_label: tool.label[language], tool_description: tool.description[language], diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index b30c1edef1..512621a552 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -57,7 +57,8 @@ export type ToolDefaultValue = PluginCommonDefaultValue & { output_schema?: Record credential_id?: string meta?: PluginMeta - output_schema?: Record + plugin_id?: string + provider_icon?: Collection['icon'] } export type DataSourceDefaultValue = Omit & { diff --git a/web/app/components/workflow/hooks/use-tool-icon.ts b/web/app/components/workflow/hooks/use-tool-icon.ts index 8071b3a42b..097e7fd038 100644 --- a/web/app/components/workflow/hooks/use-tool-icon.ts +++ b/web/app/components/workflow/hooks/use-tool-icon.ts @@ -4,6 +4,7 @@ import { } from 'react' import type { Node, + ToolWithProvider, } from '../types' import { BlockEnum, @@ -69,17 +70,51 @@ export const useToolIcon = (data?: Node['data']) => { return icon || '' } if (isToolNode(data)) { - // eslint-disable-next-line sonarjs/no-dead-store - let targetTools = buildInTools - if (data.provider_type === CollectionType.builtIn) - targetTools = buildInTools - else if (data.provider_type === CollectionType.custom) - targetTools = customTools - else if (data.provider_type === CollectionType.mcp) - targetTools = mcpTools - else - targetTools = workflowTools - return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon || '' + let primaryCollection: ToolWithProvider[] | undefined + switch (data.provider_type) { + case CollectionType.custom: + primaryCollection = customTools + break + case CollectionType.mcp: + primaryCollection = mcpTools + break + case CollectionType.workflow: + primaryCollection = workflowTools + break + case CollectionType.builtIn: + default: + primaryCollection = buildInTools + break + } + + const collectionsToSearch = [ + primaryCollection, + buildInTools, + customTools, + workflowTools, + mcpTools, + ] as Array + + const seen = new Set() + for (const collection of collectionsToSearch) { + if (!collection || seen.has(collection)) + continue + seen.add(collection) + const matched = collection.find((toolWithProvider) => { + if (canFindTool(toolWithProvider.id, data.provider_id)) + return true + if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id) + return true + return data.provider_name === toolWithProvider.name + }) + if (matched?.icon) + return matched.icon + } + + if (data.provider_icon) + return data.provider_icon + + return '' } if (isDataSourceNode(data)) return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon || '' @@ -114,17 +149,51 @@ export const useGetToolIcon = () => { } if (isToolNode(data)) { - // eslint-disable-next-line sonarjs/no-dead-store - let targetTools = buildInTools - if (data.provider_type === CollectionType.builtIn) - targetTools = buildInTools - else if (data.provider_type === CollectionType.custom) - targetTools = customTools - else if (data.provider_type === CollectionType.mcp) - targetTools = mcpTools - else - targetTools = workflowTools - return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon + let primaryCollection: ToolWithProvider[] | undefined + switch (data.provider_type) { + case CollectionType.custom: + primaryCollection = customTools + break + case CollectionType.mcp: + primaryCollection = mcpTools + break + case CollectionType.workflow: + primaryCollection = workflowTools + break + case CollectionType.builtIn: + default: + primaryCollection = buildInTools + break + } + + const collectionsToSearch = [ + primaryCollection, + buildInTools, + customTools, + workflowTools, + mcpTools, + ] as Array + + const seen = new Set() + for (const collection of collectionsToSearch) { + if (!collection || seen.has(collection)) + continue + seen.add(collection) + const matched = collection.find((toolWithProvider) => { + if (canFindTool(toolWithProvider.id, data.provider_id)) + return true + if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id) + return true + return data.provider_name === toolWithProvider.name + }) + if (matched?.icon) + return matched.icon + } + + if (data.provider_icon) + return data.provider_icon + + return undefined } if (isDataSourceNode(data)) diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index f03c3efd71..12a4283cf6 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -1,4 +1,4 @@ -import type { CollectionType } from '@/app/components/tools/types' +import type { Collection, CollectionType } from '@/app/components/tools/types' import type { CommonNodeType } from '@/app/components/workflow/types' import type { ResourceVarInputs } from '../_base/types' @@ -20,4 +20,6 @@ export type ToolNodeType = CommonNodeType & { tool_description?: string is_team_authorization?: boolean params?: Record + plugin_id?: string + provider_icon?: Collection['icon'] }