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) setVisibleCount(INITIAL_VISIBLE_COUNT)
}, [plugins]) }, [plugins])
const visiblePlugins = useMemo( const limitedPlugins = useMemo(
() => plugins.slice(0, Math.min(MAX_RECOMMENDED_COUNT, visibleCount)), () => plugins.slice(0, MAX_RECOMMENDED_COUNT),
[plugins, visibleCount], [plugins],
) )
const installedProviders = useMemo( const {
() => installedProviders,
visiblePlugins uninstalledPlugins,
.map(plugin => providerMap.get(plugin.plugin_id)) } = useMemo(() => {
.filter((provider): provider is ToolWithProvider => Boolean(provider)), const installed: ToolWithProvider[] = []
[visiblePlugins, providerMap], 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( const remainingSlots = Math.max(totalQuota - visibleInstalledProviders.length, 0)
() => visiblePlugins.filter(plugin => !providerMap.has(plugin.plugin_id)),
[visiblePlugins, providerMap], const visibleUninstalledPlugins = useMemo(
() => (remainingSlots > 0 ? uninstalledPlugins.slice(0, remainingSlots) : []),
[uninstalledPlugins, remainingSlots],
) )
const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length) const totalVisible = visibleInstalledProviders.length + visibleUninstalledPlugins.length
const showEmptyState = !isLoading && visiblePlugins.length === 0 const maxAvailable = Math.min(MAX_RECOMMENDED_COUNT, installedProviders.length + uninstalledPlugins.length)
const showMore = totalVisible < maxAvailable
const showEmptyState = !isLoading && totalVisible === 0
return ( return (
<div className='px-3 pb-3 pt-2'> <div className='px-3 pb-3 pt-2'>
@ -121,10 +151,10 @@ const FeaturedTools = ({
{!showEmptyState && !isLoading && ( {!showEmptyState && !isLoading && (
<> <>
{installedProviders.length > 0 && ( {visibleInstalledProviders.length > 0 && (
<Tools <Tools
className='p-0' className='p-0'
tools={installedProviders} tools={visibleInstalledProviders}
onSelect={onSelect} onSelect={onSelect}
canNotSelectMultiple canNotSelectMultiple
toolType={ToolTypeEnum.All} toolType={ToolTypeEnum.All}
@ -135,9 +165,9 @@ const FeaturedTools = ({
/> />
)} )}
{uninstalledPlugins.length > 0 && ( {visibleUninstalledPlugins.length > 0 && (
<div className='mt-1 flex flex-col gap-1'> <div className='mt-1 flex flex-col gap-1'>
{uninstalledPlugins.map(plugin => ( {visibleUninstalledPlugins.map(plugin => (
<FeaturedToolUninstalledItem <FeaturedToolUninstalledItem
key={plugin.plugin_id} key={plugin.plugin_id}
plugin={plugin} plugin={plugin}
@ -153,11 +183,11 @@ const FeaturedTools = ({
</> </>
)} )}
{!isLoading && visiblePlugins.length > 0 && showMore && ( {!isLoading && totalVisible > 0 && showMore && (
<div <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' 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={() => { 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'> <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 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 { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
import type { import type {
BlockEnum, BlockEnum,
@ -15,6 +15,8 @@ import DataSources from './data-sources'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useFeaturedToolsRecommendations } from '@/service/use-plugins' import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { useWorkflowStore } from '../store'
import { basePath } from '@/utils/var'
export type TabsProps = { export type TabsProps = {
activeTab: TabsEnum activeTab: TabsEnum
@ -57,12 +59,66 @@ const Tabs: FC<TabsProps> = ({
const { data: mcpTools } = useAllMCPTools() const { data: mcpTools } = useAllMCPTools()
const invalidateBuiltInTools = useInvalidateAllBuiltInTools() const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const workflowStore = useWorkflowStore()
const inRAGPipeline = dataSources.length > 0 const inRAGPipeline = dataSources.length > 0
const { const {
plugins: featuredPlugins = [], plugins: featuredPlugins = [],
isLoading: isFeaturedLoading, isLoading: isFeaturedLoading,
} = useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline) } = 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 ( return (
<div onClick={e => e.stopPropagation()}> <div onClick={e => e.stopPropagation()}>
{ {

View File

@ -10,6 +10,13 @@ import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon' import BlockIcon from '../../block-icon'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next' 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 = { type Props = {
provider: ToolWithProvider provider: ToolWithProvider
@ -64,6 +71,8 @@ const ToolItem: FC<Props> = ({
provider_id: provider.id, provider_id: provider.id,
provider_type: provider.type, provider_type: provider.type,
provider_name: provider.name, provider_name: provider.name,
plugin_id: provider.plugin_id,
provider_icon: normalizeProviderIcon(provider.icon),
tool_name: payload.name, tool_name: payload.name,
tool_label: payload.label[language], tool_label: payload.label[language],
tool_description: payload.description[language], tool_description: payload.description[language],

View File

@ -16,6 +16,13 @@ import { useTranslation } from 'react-i18next'
import { useHover } from 'ahooks' import { useHover } from 'ahooks'
import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip'
import { Mcp } from '@/app/components/base/icons/src/vender/other' 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 = { type Props = {
className?: string className?: string
@ -86,6 +93,8 @@ const Tool: FC<Props> = ({
provider_id: payload.id, provider_id: payload.id,
provider_type: payload.type, provider_type: payload.type,
provider_name: payload.name, provider_name: payload.name,
plugin_id: payload.plugin_id,
provider_icon: normalizeProviderIcon(payload.icon),
tool_name: tool.name, tool_name: tool.name,
tool_label: tool.label[language], tool_label: tool.label[language],
tool_description: tool.description[language], tool_description: tool.description[language],
@ -165,6 +174,8 @@ const Tool: FC<Props> = ({
provider_id: payload.id, provider_id: payload.id,
provider_type: payload.type, provider_type: payload.type,
provider_name: payload.name, provider_name: payload.name,
plugin_id: payload.plugin_id,
provider_icon: normalizeProviderIcon(payload.icon),
tool_name: tool.name, tool_name: tool.name,
tool_label: tool.label[language], tool_label: tool.label[language],
tool_description: tool.description[language], tool_description: tool.description[language],

View File

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

View File

@ -4,6 +4,7 @@ import {
} from 'react' } from 'react'
import type { import type {
Node, Node,
ToolWithProvider,
} from '../types' } from '../types'
import { import {
BlockEnum, BlockEnum,
@ -69,17 +70,51 @@ export const useToolIcon = (data?: Node['data']) => {
return icon || '' return icon || ''
} }
if (isToolNode(data)) { if (isToolNode(data)) {
// eslint-disable-next-line sonarjs/no-dead-store let primaryCollection: ToolWithProvider[] | undefined
let targetTools = buildInTools switch (data.provider_type) {
if (data.provider_type === CollectionType.builtIn) case CollectionType.custom:
targetTools = buildInTools primaryCollection = customTools
else if (data.provider_type === CollectionType.custom) break
targetTools = customTools case CollectionType.mcp:
else if (data.provider_type === CollectionType.mcp) primaryCollection = mcpTools
targetTools = mcpTools break
else case CollectionType.workflow:
targetTools = workflowTools primaryCollection = workflowTools
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon || '' 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)) if (isDataSourceNode(data))
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon || '' return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon || ''
@ -114,17 +149,51 @@ export const useGetToolIcon = () => {
} }
if (isToolNode(data)) { if (isToolNode(data)) {
// eslint-disable-next-line sonarjs/no-dead-store let primaryCollection: ToolWithProvider[] | undefined
let targetTools = buildInTools switch (data.provider_type) {
if (data.provider_type === CollectionType.builtIn) case CollectionType.custom:
targetTools = buildInTools primaryCollection = customTools
else if (data.provider_type === CollectionType.custom) break
targetTools = customTools case CollectionType.mcp:
else if (data.provider_type === CollectionType.mcp) primaryCollection = mcpTools
targetTools = mcpTools break
else case CollectionType.workflow:
targetTools = workflowTools primaryCollection = workflowTools
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon 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)) 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 { CommonNodeType } from '@/app/components/workflow/types'
import type { ResourceVarInputs } from '../_base/types' import type { ResourceVarInputs } from '../_base/types'
@ -20,4 +20,6 @@ export type ToolNodeType = CommonNodeType & {
tool_description?: string tool_description?: string
is_team_authorization?: boolean is_team_authorization?: boolean
params?: Record<string, any> params?: Record<string, any>
plugin_id?: string
provider_icon?: Collection['icon']
} }