chore: split tool config

This commit is contained in:
Joel
2026-01-16 14:38:57 +08:00
parent 0f5d3f38da
commit eb4f57fb8b
5 changed files with 350 additions and 157 deletions

View File

@ -11,26 +11,20 @@ import Link from 'next/link'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Textarea from '@/app/components/base/textarea'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import ToolAuthorizationSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section'
import ToolSettingsSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section'
import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger'
import { CollectionType } from '@/app/components/tools/types'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { generateFormValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import { MARKETPLACE_API_PREFIX } from '@/config'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import {
@ -151,41 +145,6 @@ const ToolSelector: FC<Props> = ({
} as any)
}
// tool settings & params
const currentToolSettings = useMemo(() => {
if (!currentProvider)
return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || []
}, [currentProvider, value])
const currentToolParams = useMemo(() => {
if (!currentProvider)
return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || []
}, [currentProvider, value])
const [currType, setCurrType] = useState('settings')
const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)
const toolValue = {
...value,
settings: newValue,
}
onSelect(toolValue as any)
}
const handleParamsFormChange = (v: Record<string, any>) => {
const toolValue = {
...value,
parameters: v,
}
onSelect(toolValue as any)
}
const handleEnabledChange = (state: boolean) => {
onSelect({
...value,
@ -311,92 +270,21 @@ const ToolSelector: FC<Props> = ({
</div>
</div>
{/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<>
<Divider className="my-1 w-full" />
<div className="px-4 py-2">
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
providerType: currentProvider.type,
detail: currentProvider as any,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
</div>
</>
)}
<ToolAuthorizationSection
currentProvider={currentProvider}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
{/* tool settings */}
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<>
<Divider className="my-1 w-full" />
{/* tabs */}
{nodeId && showTabSlider && (
<TabSlider
className="mt-1 shrink-0 px-4"
itemClassName="py-3"
noBorderBottom
smallItem
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'settings', text: t('detailPanel.toolSelector.settings', { ns: 'plugin' })! },
{ value: 'params', text: t('detailPanel.toolSelector.params', { ns: 'plugin' })! },
]}
/>
)}
{nodeId && showTabSlider && currType === 'params' && (
<div className="px-4 py-2">
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className="p-4 pb-1">
<div className="system-sm-semibold-uppercase text-text-primary">{t('detailPanel.toolSelector.settings', { ns: 'plugin' })}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className="mb-1 p-4 pb-1">
<div className="system-sm-semibold-uppercase text-text-primary">{t('detailPanel.toolSelector.params', { ns: 'plugin' })}</div>
<div className="pb-1">
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}</div>
</div>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className="px-4 py-2">
<ToolForm
inPanel
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as any}
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)}
<ToolSettingsSection
currentProvider={currentProvider}
currentTool={currentTool}
value={value}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onChange={onSelect}
/>
</>
</div>
</PortalToFollowElemContent>

View File

@ -0,0 +1,46 @@
'use client'
import type { FC } from 'react'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import * as React from 'react'
import Divider from '@/app/components/base/divider'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
import { CollectionType } from '@/app/components/tools/types'
type ToolAuthorizationSectionProps = {
currentProvider?: ToolWithProvider
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const ToolAuthorizationSection: FC<ToolAuthorizationSectionProps> = ({
currentProvider,
credentialId,
onAuthorizationItemClick,
}) => {
if (!currentProvider || currentProvider.type !== CollectionType.builtIn || !currentProvider.allow_delete)
return null
return (
<>
<Divider className="my-1 w-full" />
<div className="px-4 py-2">
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
providerType: currentProvider.type,
detail: currentProvider as any,
}}
credentialId={credentialId}
onAuthorizationItemClick={onAuthorizationItemClick}
/>
</div>
</>
)
}
export default React.memo(ToolAuthorizationSection)

View File

@ -0,0 +1,154 @@
'use client'
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { Tool } from '@/app/components/tools/types'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import { getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
type ToolSettingsSectionProps = {
currentProvider?: ToolWithProvider
currentTool?: Tool
value?: ToolValue
nodeId?: string
nodeOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
onChange?: (value: ToolValue) => void
}
const ToolSettingsSection: FC<ToolSettingsSectionProps> = ({
currentProvider,
currentTool,
value,
nodeId,
nodeOutputVars = [],
availableNodes = [],
onChange,
}) => {
const { t } = useTranslation()
const [currType, setCurrType] = useState<'settings' | 'params'>('settings')
const safeNodeId = nodeId ?? ''
const currentToolSettings = useMemo(() => {
if (!currentTool)
return []
return currentTool.parameters?.filter(param => param.form !== 'llm') || []
}, [currentTool])
const currentToolParams = useMemo(() => {
if (!currentTool)
return []
return currentTool.parameters?.filter(param => param.form === 'llm') || []
}, [currentTool])
const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const allowReasoning = !!safeNodeId
const showTabSlider = allowReasoning && currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && (!allowReasoning || !currentToolParams.length)
const reasoningConfigOnly = allowReasoning && currentToolParams.length > 0 && currentToolSettings.length === 0
const handleSettingsFormChange = (v: Record<string, any>) => {
if (!value || !onChange)
return
const newValue = getStructureValue(v)
onChange({
...value,
settings: newValue,
})
}
const handleParamsFormChange = (v: Record<string, any>) => {
if (!value || !onChange)
return
onChange({
...value,
parameters: v,
})
}
if (!currentProvider?.is_team_authorization)
return null
if (!currentToolSettings.length && !currentToolParams.length)
return null
return (
<>
<Divider className="my-1 w-full" />
{/* tabs */}
{showTabSlider && (
<TabSlider
className="mt-1 shrink-0 px-4"
itemClassName="py-3"
noBorderBottom
smallItem
value={currType}
onChange={(value) => {
setCurrType(value as 'settings' | 'params')
}}
options={[
{ value: 'settings', text: t('detailPanel.toolSelector.settings', { ns: 'plugin' })! },
{ value: 'params', text: t('detailPanel.toolSelector.params', { ns: 'plugin' })! },
]}
/>
)}
{showTabSlider && currType === 'params' && (
<div className="px-4 py-2">
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className="p-4 pb-1">
<div className="system-sm-semibold-uppercase text-text-primary">{t('detailPanel.toolSelector.settings', { ns: 'plugin' })}</div>
</div>
)}
{/* reasoning config only */}
{reasoningConfigOnly && (
<div className="mb-1 p-4 pb-1">
<div className="system-sm-semibold-uppercase text-text-primary">{t('detailPanel.toolSelector.params', { ns: 'plugin' })}</div>
<div className="pb-1">
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}</div>
</div>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className="px-4 py-2">
<ToolForm
inPanel
readOnly={false}
nodeId={safeNodeId}
schema={settingsFormSchemas as any}
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
/>
</div>
)}
{/* reasoning config form */}
{allowReasoning && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={safeNodeId}
/>
)}
</>
)
}
export default React.memo(ToolSettingsSection)

View File

@ -1,10 +1,20 @@
import type { FC } from 'react'
import type { Emoji } from '@/app/components/tools/types'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import * as React from 'react'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks'
import ToolAuthorizationSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section'
import ToolSettingsSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section'
import { generateFormValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import {
@ -23,6 +33,7 @@ type ToolBlockComponentProps = {
nodeKey: string
provider: string
tool: string
configId: string
label?: string
icon?: string | Emoji
iconDark?: string | Emoji
@ -40,35 +51,79 @@ const ToolBlockComponent: FC<ToolBlockComponentProps> = ({
nodeKey,
provider,
tool,
configId,
label,
icon,
iconDark,
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_TOOL_BLOCK_COMMAND)
const language = useGetLanguage()
const { t } = useTranslation()
const { theme } = useTheme()
const [isSettingOpen, setIsSettingOpen] = useState(false)
const [toolValue, setToolValue] = useState<ToolValue | null>(null)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const toolMeta = useMemo(() => {
const collections = [buildInTools, customTools, workflowTools, mcpTools].filter(Boolean) as ToolWithProvider[][]
for (const collection of collections) {
const mergedTools = useMemo(() => {
return [buildInTools, customTools, workflowTools, mcpTools].filter(Boolean) as ToolWithProvider[][]
}, [buildInTools, customTools, workflowTools, mcpTools])
const currentProvider = useMemo(() => {
for (const collection of mergedTools) {
const providerItem = collection.find(item => item.name === provider || item.id === provider || canFindTool(item.id, provider))
if (!providerItem)
continue
const toolItem = providerItem.tools?.find(item => item.name === tool)
if (!toolItem)
continue
return {
label: toolItem.label?.[language] || tool,
icon: providerItem.icon,
iconDark: providerItem.icon_dark,
}
if (providerItem)
return providerItem
}
return null
}, [buildInTools, customTools, workflowTools, mcpTools, language, provider, tool])
return undefined
}, [mergedTools, provider])
const currentTool = useMemo(() => {
if (!currentProvider)
return undefined
return currentProvider.tools?.find(item => item.name === tool)
}, [currentProvider, tool])
const toolMeta = useMemo(() => {
if (!currentProvider || !currentTool)
return null
return {
label: currentTool.label?.[language] || tool,
icon: currentProvider.icon,
iconDark: currentProvider.icon_dark,
}
}, [currentProvider, currentTool, language, tool])
const defaultToolValue = useMemo(() => {
if (!currentProvider || !currentTool)
return null
const settingsSchemas = toolParametersToFormSchemas(currentTool.parameters?.filter(param => param.form !== 'llm') || [])
const paramsSchemas = toolParametersToFormSchemas(currentTool.parameters?.filter(param => param.form === 'llm') || [])
const toolLabel = currentTool.label?.[language] || tool
const toolDescription = typeof currentTool.description === 'object'
? (currentTool.description?.[language] || '')
: (currentTool.description || '')
return {
provider_name: currentProvider.id,
provider_show_name: currentProvider.name,
tool_name: currentTool.name,
tool_label: toolLabel,
tool_description: toolDescription,
settings: generateFormValue({}, settingsSchemas as any),
parameters: generateFormValue({}, paramsSchemas as any, true),
enabled: true,
extra: { description: toolDescription },
} as ToolValue
}, [currentProvider, currentTool, language, tool])
useEffect(() => {
if (!defaultToolValue)
return
if (!toolValue || toolValue.tool_name !== defaultToolValue.tool_name || toolValue.provider_name !== defaultToolValue.provider_name)
setToolValue(defaultToolValue)
}, [defaultToolValue, toolValue])
const displayLabel = label || toolMeta?.label || tool
const resolvedIcon = (() => {
@ -109,20 +164,69 @@ const ToolBlockComponent: FC<ToolBlockComponentProps> = ({
)
}
const handleToolValueChange = (nextValue: ToolValue) => {
setToolValue(nextValue)
}
const handleAuthorizationItemClick = (id: string) => {
setToolValue(prev => (prev ? { ...prev, credential_id: id } : prev))
}
return (
<span
ref={ref}
className={cn(
'inline-flex items-center gap-[2px] rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover px-[4px] py-[1px] shadow-xs',
isSelected && 'border-text-accent',
)}
title={`${provider}.${tool}`}
<PortalToFollowElem
placement="bottom-start"
offset={8}
open={isSettingOpen}
onOpenChange={setIsSettingOpen}
>
{renderIcon()}
<span className="system-xs-medium max-w-[180px] truncate text-text-accent">
{displayLabel}
</span>
</span>
<PortalToFollowElemTrigger
asChild
onClick={() => {
if (!currentProvider || !currentTool)
return
setIsSettingOpen(true)
}}
>
<span
ref={ref}
className={cn(
'inline-flex cursor-pointer items-center gap-[2px] rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover px-[4px] py-[1px] shadow-xs',
isSelected && 'border-text-accent',
)}
title={`${provider}.${tool}`}
data-tool-config-id={configId}
>
{renderIcon()}
<span className="system-xs-medium max-w-[180px] truncate text-text-accent">
{displayLabel}
</span>
</span>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[999]">
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
<div className="system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary">{t('detailPanel.toolSelector.toolSetting', { ns: 'plugin' })}</div>
{currentProvider && currentTool && toolValue && (
<>
<div className="px-4 pb-2 text-xs text-text-tertiary">{displayLabel}</div>
<ToolAuthorizationSection
currentProvider={currentProvider}
credentialId={toolValue.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
<ToolSettingsSection
currentProvider={currentProvider}
currentTool={currentTool}
value={toolValue}
onChange={handleToolValueChange}
nodeId={undefined}
nodeOutputVars={[]}
availableNodes={[]}
/>
</>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

View File

@ -71,6 +71,7 @@ export class ToolBlockNode extends DecoratorNode<React.JSX.Element> {
nodeKey={this.getKey()}
provider={this.__provider}
tool={this.__tool}
configId={this.__configId}
label={this.__label}
icon={this.__icon}
iconDark={this.__iconDark}