mirror of https://github.com/langgenius/dify.git
feat: add install check for tools, triggers and datasources
This commit is contained in:
parent
cac60a25bb
commit
6e0765fbaf
|
|
@ -63,6 +63,7 @@ const DataSources = ({
|
|||
datasource_name: toolDefaultValue?.tool_name,
|
||||
datasource_label: toolDefaultValue?.tool_label,
|
||||
title: toolDefaultValue?.title,
|
||||
plugin_unique_identifier: toolDefaultValue?.plugin_unique_identifier,
|
||||
}
|
||||
// Update defaultValue with fileExtensions if this is the local file data source
|
||||
if (toolDefaultValue?.provider_id === 'langgenius/file' && toolDefaultValue?.provider_name === 'file') {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ const ToolItem: FC<Props> = ({
|
|||
provider_type: provider.type,
|
||||
provider_name: provider.name,
|
||||
plugin_id: provider.plugin_id,
|
||||
plugin_unique_identifier: provider.plugin_unique_identifier,
|
||||
provider_icon: normalizeProviderIcon(provider.icon),
|
||||
tool_name: payload.name,
|
||||
tool_label: payload.label[language],
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ const Tool: FC<Props> = ({
|
|||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
plugin_id: payload.plugin_id,
|
||||
plugin_unique_identifier: payload.plugin_unique_identifier,
|
||||
provider_icon: normalizeProviderIcon(payload.icon),
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
|
|
@ -175,6 +176,7 @@ const Tool: FC<Props> = ({
|
|||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
plugin_id: payload.plugin_id,
|
||||
plugin_unique_identifier: payload.plugin_unique_identifier,
|
||||
provider_icon: normalizeProviderIcon(payload.icon),
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export type ToolDefaultValue = PluginCommonDefaultValue & {
|
|||
meta?: PluginMeta
|
||||
plugin_id?: string
|
||||
provider_icon?: Collection['icon']
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id'> & {
|
||||
|
|
@ -69,6 +70,7 @@ export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id
|
|||
datasource_label: string
|
||||
title: string
|
||||
fileExtensions?: string[]
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
export type PluginDefaultValue = ToolDefaultValue | DataSourceDefaultValue | TriggerDefaultValue
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
import { useCallback, useMemo } from 'react'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidToolsByType,
|
||||
} from '@/service/use-tools'
|
||||
import {
|
||||
useAllTriggerPlugins,
|
||||
useInvalidateAllTriggerPlugins,
|
||||
} from '@/service/use-triggers'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useStore } from '../store'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
type InstallationState = {
|
||||
isChecking: boolean
|
||||
isMissing: boolean
|
||||
uniqueIdentifier?: string
|
||||
canInstall: boolean
|
||||
onInstallSuccess: () => void
|
||||
shouldDim: boolean
|
||||
}
|
||||
|
||||
const useToolInstallation = (data: ToolNodeType): InstallationState => {
|
||||
const builtInQuery = useAllBuiltInTools()
|
||||
const customQuery = useAllCustomTools()
|
||||
const workflowQuery = useAllWorkflowTools()
|
||||
const mcpQuery = useAllMCPTools()
|
||||
const invalidateTools = useInvalidToolsByType(data.provider_type)
|
||||
|
||||
const collectionInfo = useMemo(() => {
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return {
|
||||
list: builtInQuery.data,
|
||||
isLoading: builtInQuery.isLoading,
|
||||
}
|
||||
case CollectionType.custom:
|
||||
return {
|
||||
list: customQuery.data,
|
||||
isLoading: customQuery.isLoading,
|
||||
}
|
||||
case CollectionType.workflow:
|
||||
return {
|
||||
list: workflowQuery.data,
|
||||
isLoading: workflowQuery.isLoading,
|
||||
}
|
||||
case CollectionType.mcp:
|
||||
return {
|
||||
list: mcpQuery.data,
|
||||
isLoading: mcpQuery.isLoading,
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}, [
|
||||
builtInQuery.data,
|
||||
builtInQuery.isLoading,
|
||||
customQuery.data,
|
||||
customQuery.isLoading,
|
||||
data.provider_type,
|
||||
mcpQuery.data,
|
||||
mcpQuery.isLoading,
|
||||
workflowQuery.data,
|
||||
workflowQuery.isLoading,
|
||||
])
|
||||
|
||||
const collection = collectionInfo?.list
|
||||
const isLoading = collectionInfo?.isLoading ?? false
|
||||
const isResolved = !!collectionInfo && !isLoading
|
||||
|
||||
const matchedCollection = useMemo(() => {
|
||||
if (!collection || !collection.length)
|
||||
return undefined
|
||||
|
||||
return collection.find((toolWithProvider) => {
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (toolWithProvider.name === data.provider_name)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [collection, data.plugin_id, data.provider_id, data.provider_name])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
if (invalidateTools)
|
||||
invalidateTools()
|
||||
}, [invalidateTools])
|
||||
|
||||
return {
|
||||
isChecking: !!collectionInfo && !isResolved,
|
||||
isMissing: isResolved && !matchedCollection,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
const useTriggerInstallation = (data: PluginTriggerNodeType): InstallationState => {
|
||||
const triggerPluginsQuery = useAllTriggerPlugins()
|
||||
const invalidateTriggers = useInvalidateAllTriggerPlugins()
|
||||
|
||||
const triggerProviders = triggerPluginsQuery.data
|
||||
const isLoading = triggerPluginsQuery.isLoading
|
||||
|
||||
const matchedProvider = useMemo(() => {
|
||||
if (!triggerProviders || !triggerProviders.length)
|
||||
return undefined
|
||||
|
||||
return triggerProviders.find(provider =>
|
||||
provider.name === data.provider_name
|
||||
|| provider.id === data.provider_id
|
||||
|| (data.plugin_id && provider.plugin_id === data.plugin_id),
|
||||
)
|
||||
}, [
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
triggerProviders,
|
||||
])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
invalidateTriggers()
|
||||
}, [invalidateTriggers])
|
||||
|
||||
return {
|
||||
isChecking: isLoading,
|
||||
isMissing: !isLoading && !!triggerProviders && !matchedProvider,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
const useDataSourceInstallation = (data: DataSourceNodeType): InstallationState => {
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const invalidateDataSourceList = useInvalidDataSourceList()
|
||||
|
||||
const matchedPlugin = useMemo(() => {
|
||||
if (!dataSourceList || !dataSourceList.length)
|
||||
return undefined
|
||||
|
||||
return dataSourceList.find((item) => {
|
||||
if (data.plugin_unique_identifier && item.plugin_unique_identifier === data.plugin_unique_identifier)
|
||||
return true
|
||||
if (data.plugin_id && item.plugin_id === data.plugin_id)
|
||||
return true
|
||||
if (data.provider_name && item.provider === data.provider_name)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [data.plugin_id, data.plugin_unique_identifier, data.provider_name, dataSourceList])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
invalidateDataSourceList()
|
||||
}, [invalidateDataSourceList])
|
||||
|
||||
const hasLoadedList = dataSourceList !== undefined
|
||||
|
||||
return {
|
||||
isChecking: !hasLoadedList,
|
||||
isMissing: hasLoadedList && !matchedPlugin,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodePluginInstallation = (data: CommonNodeType): InstallationState => {
|
||||
const toolInstallation = useToolInstallation(data as ToolNodeType)
|
||||
const triggerInstallation = useTriggerInstallation(data as PluginTriggerNodeType)
|
||||
const dataSourceInstallation = useDataSourceInstallation(data as DataSourceNodeType)
|
||||
|
||||
switch (data.type as BlockEnum) {
|
||||
case BlockEnum.Tool:
|
||||
return toolInstallation
|
||||
case BlockEnum.TriggerPlugin:
|
||||
return triggerInstallation
|
||||
case BlockEnum.DataSource:
|
||||
return dataSourceInstallation
|
||||
default:
|
||||
return {
|
||||
isChecking: false,
|
||||
isMissing: false,
|
||||
uniqueIdentifier: undefined,
|
||||
canInstall: false,
|
||||
onInstallSuccess: () => undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,36 @@
|
|||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type { DataSourceNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
const Node: FC<NodeProps<DataSourceNodeType>> = () => {
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
|
||||
import type { DataSourceNodeType } from './types'
|
||||
|
||||
const Node: FC<NodeProps<DataSourceNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const {
|
||||
isChecking,
|
||||
isMissing,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
} = useNodePluginInstallation(data)
|
||||
|
||||
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
|
||||
|
||||
if (!showInstallButton)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='relative mb-1 px-3 py-1'>
|
||||
<div className='absolute right-3 top-[-32px] z-20'>
|
||||
<InstallPluginButton
|
||||
size='small'
|
||||
className='!font-medium !text-text-accent'
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
onSuccess={onInstallSuccess}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export type DataSourceNodeType = CommonNodeType & {
|
|||
datasource_label: string
|
||||
datasource_parameters: ToolVarInputs
|
||||
datasource_configurations: Record<string, any>
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
export type CustomRunFormProps = {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,68 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ToolNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
|
||||
import type { ToolNodeType } from './types'
|
||||
|
||||
const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { tool_configurations, paramSchemas } = data
|
||||
const toolConfigs = Object.keys(tool_configurations || {})
|
||||
const {
|
||||
isChecking,
|
||||
isMissing,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
} = useNodePluginInstallation(data)
|
||||
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
|
||||
|
||||
if (!toolConfigs.length)
|
||||
const hasConfigs = toolConfigs.length > 0
|
||||
|
||||
if (!showInstallButton && !hasConfigs)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='space-y-0.5'>
|
||||
{toolConfigs.map((key, index) => (
|
||||
<div key={index} className='flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'>
|
||||
<div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary'>
|
||||
{key}
|
||||
<div className='relative mb-1 px-3 py-1'>
|
||||
{showInstallButton && (
|
||||
<div className='absolute right-3 top-[-32px] z-20'>
|
||||
<InstallPluginButton
|
||||
size='small'
|
||||
className='!font-medium !text-text-accent'
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
onSuccess={onInstallSuccess}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasConfigs && (
|
||||
<div className='space-y-0.5'>
|
||||
{toolConfigs.map((key, index) => (
|
||||
<div key={index} className='flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'>
|
||||
<div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary'>
|
||||
{key}
|
||||
</div>
|
||||
{typeof tool_configurations[key].value === 'string' && (
|
||||
<div title={tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key].value === 'number' && (
|
||||
<div title={Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
|
||||
<div title={tool_configurations[key].model} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{tool_configurations[key].model}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{typeof tool_configurations[key].value === 'string' && (
|
||||
<div title={tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key].value === 'number' && (
|
||||
<div title={Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
|
||||
<div title={tool_configurations[key].model} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{tool_configurations[key].model}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export type ToolNodeType = CommonNodeType & {
|
|||
params?: Record<string, any>
|
||||
plugin_id?: string
|
||||
provider_icon?: Collection['icon']
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import type { NodeProps } from '@/app/components/workflow/types'
|
|||
import type { FC } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
|
||||
import type { PluginTriggerNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
|
||||
|
|
@ -42,6 +44,14 @@ const Node: FC<NodeProps<PluginTriggerNodeType>> = ({
|
|||
const { subscriptions } = useConfig(id, data)
|
||||
const { config = {}, subscription_id } = data
|
||||
const configKeys = Object.keys(config)
|
||||
const {
|
||||
isChecking,
|
||||
isMissing,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
} = useNodePluginInstallation(data)
|
||||
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
@ -50,7 +60,17 @@ const Node: FC<NodeProps<PluginTriggerNodeType>> = ({
|
|||
}, [subscription_id, subscriptions])
|
||||
|
||||
return (
|
||||
<div className="mb-1 px-3 py-1">
|
||||
<div className="relative mb-1 px-3 py-1">
|
||||
{showInstallButton && (
|
||||
<div className="absolute right-3 top-[-32px] z-20">
|
||||
<InstallPluginButton
|
||||
size="small"
|
||||
className="!font-medium !text-text-accent"
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
onSuccess={onInstallSuccess}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-0.5">
|
||||
{!isValidSubscription && <NodeStatus status={NodeStatusEnum.warning} message={t('pluginTrigger.node.status.warning')} />}
|
||||
{isValidSubscription && configKeys.map((key, index) => (
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type PluginTriggerNodeType = CommonNodeType & {
|
|||
event_node_version?: string
|
||||
plugin_id?: string
|
||||
config?: Record<string, any>
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
// Use base types directly
|
||||
|
|
|
|||
|
|
@ -451,6 +451,7 @@ export type MoreInfo = {
|
|||
export type ToolWithProvider = Collection & {
|
||||
tools: Tool[]
|
||||
meta: PluginMeta
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
export type RAGRecommendedPlugins = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue