Optimize workflow tool sync after plugin install (#27280)

This commit is contained in:
lyzno1 2025-10-23 09:58:54 +08:00 committed by GitHub
parent 128bc2241d
commit d478f62b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 223 additions and 45 deletions

View File

@ -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<string>()
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 (
<div className='px-3 pb-3 pt-2'>
@ -121,10 +151,10 @@ const FeaturedTools = ({
{!showEmptyState && !isLoading && (
<>
{installedProviders.length > 0 && (
{visibleInstalledProviders.length > 0 && (
<Tools
className='p-0'
tools={installedProviders}
tools={visibleInstalledProviders}
onSelect={onSelect}
canNotSelectMultiple
toolType={ToolTypeEnum.All}
@ -135,9 +165,9 @@ const FeaturedTools = ({
/>
)}
{uninstalledPlugins.length > 0 && (
{visibleUninstalledPlugins.length > 0 && (
<div className='mt-1 flex flex-col gap-1'>
{uninstalledPlugins.map(plugin => (
{visibleUninstalledPlugins.map(plugin => (
<FeaturedToolUninstalledItem
key={plugin.plugin_id}
plugin={plugin}
@ -153,11 +183,11 @@ const FeaturedTools = ({
</>
)}
{!isLoading && visiblePlugins.length > 0 && showMore && (
{!isLoading && totalVisible > 0 && showMore && (
<div
className='group mt-1 flex cursor-pointer items-center gap-x-2 rounded-lg py-1 pl-3 pr-2 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary'
onClick={() => {
setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, MAX_RECOMMENDED_COUNT, plugins.length))
setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, maxAvailable))
}}
>
<div className='flex items-center px-1 text-text-tertiary transition-colors group-hover:text-text-secondary'>

View File

@ -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<TabsProps> = ({
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<typeof state> = {}
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 (
<div onClick={e => e.stopPropagation()}>
{

View File

@ -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<Props> = ({
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],

View File

@ -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<Props> = ({
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<Props> = ({
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],

View File

@ -57,7 +57,8 @@ export type ToolDefaultValue = PluginCommonDefaultValue & {
output_schema?: Record<string, any>
credential_id?: string
meta?: PluginMeta
output_schema?: Record<string, any>
plugin_id?: string
provider_icon?: Collection['icon']
}
export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id'> & {

View File

@ -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<ToolWithProvider[] | undefined>
const seen = new Set<ToolWithProvider[]>()
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<ToolWithProvider[] | undefined>
const seen = new Set<ToolWithProvider[]>()
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))

View File

@ -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<string, any>
plugin_id?: string
provider_icon?: Collection['icon']
}