mirror of https://github.com/langgenius/dify.git
Optimize workflow tool sync after plugin install (#27280)
This commit is contained in:
parent
128bc2241d
commit
d478f62b49
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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()}>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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'> & {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue