mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 12:37:20 +08:00
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)
|
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'>
|
||||||
|
|||||||
@ -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()}>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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'> & {
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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']
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user