Compare commits

...

4 Commits

Author SHA1 Message Date
CodingOnStar
e6aa30f776 test(tool-selector): add comprehensive tests for AppSelector and ToolSettingsPanel
- Introduced unit tests for the AppSelector component, covering app selection, form handling, and loading states.
- Enhanced ToolSettingsPanel tests by utilizing a mock form schema for improved clarity and maintainability.
- Updated existing tests to ensure compatibility with the new form schema structure.
- Added edge case handling and integration tests to validate user flows and interactions.
2026-01-15 11:51:54 +08:00
CodingOnStar
3b58b0d129 refactor(tool-selector): remove unused components and consolidate imports
- Deleted several unused files related to the tool selector, including hooks, forms, and modals.
- Consolidated imports in the tool selector index file to streamline component usage.
- Updated the tool selector to utilize a custom hook for state management and improved the rendering logic for tool items and settings panels.
2026-01-14 18:36:26 +08:00
-LAN-
01f17b7ddc refactor(http_request_node): apply DI for http request node (#30509)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-14 14:19:48 +08:00
yyh
14b2e5bd0d refactor(web): MCP tool availability to context-based version gating (#30955) 2026-01-14 13:40:16 +08:00
41 changed files with 6293 additions and 467 deletions

View File

@@ -33,6 +33,10 @@ class MaxRetriesExceededError(ValueError):
pass
request_error = httpx.RequestError
max_retries_exceeded_error = MaxRetriesExceededError
def _create_proxy_mounts() -> dict[str, httpx.HTTPTransport]:
return {
"http://": httpx.HTTPTransport(

View File

@@ -17,6 +17,7 @@ from core.helper import ssrf_proxy
from core.variables.segments import ArrayFileSegment, FileSegment
from core.workflow.runtime import VariablePool
from ..protocols import FileManagerProtocol, HttpClientProtocol
from .entities import (
HttpRequestNodeAuthorization,
HttpRequestNodeData,
@@ -78,6 +79,8 @@ class Executor:
timeout: HttpRequestNodeTimeout,
variable_pool: VariablePool,
max_retries: int = dify_config.SSRF_DEFAULT_MAX_RETRIES,
http_client: HttpClientProtocol = ssrf_proxy,
file_manager: FileManagerProtocol = file_manager,
):
# If authorization API key is present, convert the API key using the variable pool
if node_data.authorization.type == "api-key":
@@ -104,6 +107,8 @@ class Executor:
self.data = None
self.json = None
self.max_retries = max_retries
self._http_client = http_client
self._file_manager = file_manager
# init template
self.variable_pool = variable_pool
@@ -200,7 +205,7 @@ class Executor:
if file_variable is None:
raise FileFetchError(f"cannot fetch file with selector {file_selector}")
file = file_variable.value
self.content = file_manager.download(file)
self.content = self._file_manager.download(file)
case "x-www-form-urlencoded":
form_data = {
self.variable_pool.convert_template(item.key).text: self.variable_pool.convert_template(
@@ -239,7 +244,7 @@ class Executor:
):
file_tuple = (
file.filename,
file_manager.download(file),
self._file_manager.download(file),
file.mime_type or "application/octet-stream",
)
if key not in files:
@@ -332,19 +337,18 @@ class Executor:
do http request depending on api bundle
"""
_METHOD_MAP = {
"get": ssrf_proxy.get,
"head": ssrf_proxy.head,
"post": ssrf_proxy.post,
"put": ssrf_proxy.put,
"delete": ssrf_proxy.delete,
"patch": ssrf_proxy.patch,
"get": self._http_client.get,
"head": self._http_client.head,
"post": self._http_client.post,
"put": self._http_client.put,
"delete": self._http_client.delete,
"patch": self._http_client.patch,
}
method_lc = self.method.lower()
if method_lc not in _METHOD_MAP:
raise InvalidHttpMethodError(f"Invalid http method {self.method}")
request_args = {
"url": self.url,
"data": self.data,
"files": self.files,
"json": self.json,
@@ -357,8 +361,12 @@ class Executor:
}
# request_args = {k: v for k, v in request_args.items() if v is not None}
try:
response: httpx.Response = _METHOD_MAP[method_lc](**request_args, max_retries=self.max_retries)
except (ssrf_proxy.MaxRetriesExceededError, httpx.RequestError) as e:
response: httpx.Response = _METHOD_MAP[method_lc](
url=self.url,
**request_args,
max_retries=self.max_retries,
)
except (self._http_client.max_retries_exceeded_error, self._http_client.request_error) as e:
raise HttpRequestNodeError(str(e)) from e
# FIXME: fix type ignore, this maybe httpx type issue
return response

View File

@@ -1,10 +1,11 @@
import logging
import mimetypes
from collections.abc import Mapping, Sequence
from typing import Any
from collections.abc import Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Any
from configs import dify_config
from core.file import File, FileTransferMethod
from core.file import File, FileTransferMethod, file_manager
from core.helper import ssrf_proxy
from core.tools.tool_file_manager import ToolFileManager
from core.variables.segments import ArrayFileSegment
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
@@ -13,6 +14,7 @@ from core.workflow.nodes.base import variable_template_parser
from core.workflow.nodes.base.entities import VariableSelector
from core.workflow.nodes.base.node import Node
from core.workflow.nodes.http_request.executor import Executor
from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol
from factories import file_factory
from .entities import (
@@ -30,10 +32,35 @@ HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout(
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from core.workflow.entities import GraphInitParams
from core.workflow.runtime import GraphRuntimeState
class HttpRequestNode(Node[HttpRequestNodeData]):
node_type = NodeType.HTTP_REQUEST
def __init__(
self,
id: str,
config: Mapping[str, Any],
graph_init_params: "GraphInitParams",
graph_runtime_state: "GraphRuntimeState",
*,
http_client: HttpClientProtocol = ssrf_proxy,
tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
file_manager: FileManagerProtocol = file_manager,
) -> None:
super().__init__(
id=id,
config=config,
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
)
self._http_client = http_client
self._tool_file_manager_factory = tool_file_manager_factory
self._file_manager = file_manager
@classmethod
def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]:
return {
@@ -71,6 +98,8 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
timeout=self._get_request_timeout(self.node_data),
variable_pool=self.graph_runtime_state.variable_pool,
max_retries=0,
http_client=self._http_client,
file_manager=self._file_manager,
)
process_data["request"] = http_executor.to_log()
@@ -199,7 +228,7 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
mime_type = (
content_disposition_type or content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream"
)
tool_file_manager = ToolFileManager()
tool_file_manager = self._tool_file_manager_factory()
tool_file = tool_file_manager.create_file_by_raw(
user_id=self.user_id,

View File

@@ -1,16 +1,21 @@
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, final
from typing_extensions import override
from configs import dify_config
from core.file import file_manager
from core.helper import ssrf_proxy
from core.helper.code_executor.code_executor import CodeExecutor
from core.helper.code_executor.code_node_provider import CodeNodeProvider
from core.tools.tool_file_manager import ToolFileManager
from core.workflow.enums import NodeType
from core.workflow.graph import NodeFactory
from core.workflow.nodes.base.node import Node
from core.workflow.nodes.code.code_node import CodeNode
from core.workflow.nodes.code.limits import CodeNodeLimits
from core.workflow.nodes.http_request.node import HttpRequestNode
from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol
from core.workflow.nodes.template_transform.template_renderer import (
CodeExecutorJinja2TemplateRenderer,
Jinja2TemplateRenderer,
@@ -43,6 +48,9 @@ class DifyNodeFactory(NodeFactory):
code_providers: Sequence[type[CodeNodeProvider]] | None = None,
code_limits: CodeNodeLimits | None = None,
template_renderer: Jinja2TemplateRenderer | None = None,
http_request_http_client: HttpClientProtocol = ssrf_proxy,
http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
http_request_file_manager: FileManagerProtocol = file_manager,
) -> None:
self.graph_init_params = graph_init_params
self.graph_runtime_state = graph_runtime_state
@@ -61,6 +69,9 @@ class DifyNodeFactory(NodeFactory):
max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH,
)
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer()
self._http_request_http_client = http_request_http_client
self._http_request_tool_file_manager_factory = http_request_tool_file_manager_factory
self._http_request_file_manager = http_request_file_manager
@override
def create_node(self, node_config: dict[str, object]) -> Node:
@@ -113,6 +124,7 @@ class DifyNodeFactory(NodeFactory):
code_providers=self._code_providers,
code_limits=self._code_limits,
)
if node_type == NodeType.TEMPLATE_TRANSFORM:
return TemplateTransformNode(
id=node_id,
@@ -122,6 +134,17 @@ class DifyNodeFactory(NodeFactory):
template_renderer=self._template_renderer,
)
if node_type == NodeType.HTTP_REQUEST:
return HttpRequestNode(
id=node_id,
config=node_config,
graph_init_params=self.graph_init_params,
graph_runtime_state=self.graph_runtime_state,
http_client=self._http_request_http_client,
tool_file_manager_factory=self._http_request_tool_file_manager_factory,
file_manager=self._http_request_file_manager,
)
return node_class(
id=node_id,
config=node_config,

View File

@@ -0,0 +1,29 @@
from typing import Protocol
import httpx
from core.file import File
class HttpClientProtocol(Protocol):
@property
def max_retries_exceeded_error(self) -> type[Exception]: ...
@property
def request_error(self) -> type[Exception]: ...
def get(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
def head(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
def post(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
def put(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
def delete(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
def patch(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
class FileManagerProtocol(Protocol):
def download(self, f: File, /) -> bytes: ...

View File

@@ -183,7 +183,6 @@ const AgentTools: FC = () => {
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
selectedTools={tools as unknown as ToolValue[]}
canChooseMCPTool
/>
</>
)}

View File

@@ -55,7 +55,6 @@ type FormProps<
nodeId?: string
nodeOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
canChooseMCPTool?: boolean
}
function Form<
@@ -81,7 +80,6 @@ function Form<
nodeId,
nodeOutputVars,
availableNodes,
canChooseMCPTool,
}: FormProps<CustomFormSchema>) {
const language = useLanguage()
const [changeKey, setChangeKey] = useState('')
@@ -407,7 +405,6 @@ function Form<
value={value[variable] || []}
onChange={item => handleFormChange(variable, item as any)}
supportCollapse
canChooseMCPTool={canChooseMCPTool}
/>
{fieldMoreInfo?.(formSchema)}
{validating && changeKey === variable && <ValidatingTip />}

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
// ==================== Imports (after mocks) ====================
import { MCPToolAvailabilityProvider } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
import MultipleToolSelector from './index'
// ==================== Mock Setup ====================
@@ -190,10 +191,11 @@ type RenderOptions = {
nodeOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
nodeId?: string
canChooseMCPTool?: boolean
versionSupported?: boolean
}
const renderComponent = (options: RenderOptions = {}) => {
const { versionSupported, ...overrides } = options
const defaultProps = {
disabled: false,
value: [],
@@ -206,16 +208,17 @@ const renderComponent = (options: RenderOptions = {}) => {
nodeOutputVars: [createNodeOutputVar()],
availableNodes: [createNode()],
nodeId: 'test-node-id',
canChooseMCPTool: false,
}
const props = { ...defaultProps, ...options }
const props = { ...defaultProps, ...overrides }
const queryClient = createQueryClient()
return {
...render(
<QueryClientProvider client={queryClient}>
<MultipleToolSelector {...props} />
<MCPToolAvailabilityProvider versionSupported={versionSupported}>
<MultipleToolSelector {...props} />
</MCPToolAvailabilityProvider>
</QueryClientProvider>,
),
props,
@@ -410,7 +413,7 @@ describe('MultipleToolSelector', () => {
expect(screen.getByText('2/3')).toBeInTheDocument()
})
it('should track enabled count with MCP tools when canChooseMCPTool is true', () => {
it('should track enabled count with MCP tools when version is supported', () => {
// Arrange
const mcpTools = [createMCPTool({ id: 'mcp-provider' })]
mockMCPToolsData.mockReturnValue(mcpTools)
@@ -421,13 +424,13 @@ describe('MultipleToolSelector', () => {
]
// Act
renderComponent({ value: tools, canChooseMCPTool: true })
renderComponent({ value: tools, versionSupported: true })
// Assert
expect(screen.getByText('2/2')).toBeInTheDocument()
})
it('should not count MCP tools when canChooseMCPTool is false', () => {
it('should not count MCP tools when version is unsupported', () => {
// Arrange
const mcpTools = [createMCPTool({ id: 'mcp-provider' })]
mockMCPToolsData.mockReturnValue(mcpTools)
@@ -438,7 +441,7 @@ describe('MultipleToolSelector', () => {
]
// Act
renderComponent({ value: tools, canChooseMCPTool: false })
renderComponent({ value: tools, versionSupported: false })
// Assert
expect(screen.getByText('1/2')).toBeInTheDocument()
@@ -721,14 +724,6 @@ describe('MultipleToolSelector', () => {
expect(screen.getByTestId('tool-selector-add')).toBeInTheDocument()
})
it('should pass canChooseMCPTool prop correctly', () => {
// Arrange & Act
renderComponent({ canChooseMCPTool: true })
// Assert
expect(screen.getByTestId('tool-selector-add')).toBeInTheDocument()
})
it('should render with supportEnableSwitch for edit selectors', () => {
// Arrange
const tools = [createToolValue()]
@@ -771,13 +766,13 @@ describe('MultipleToolSelector', () => {
]
// Act
renderComponent({ value: tools, canChooseMCPTool: true })
renderComponent({ value: tools, versionSupported: true })
// Assert
expect(screen.getByText('2/2')).toBeInTheDocument()
})
it('should exclude MCP tools from enabled count when canChooseMCPTool is false', () => {
it('should exclude MCP tools from enabled count when strategy version is unsupported', () => {
// Arrange
const mcpTools = [createMCPTool({ id: 'mcp-provider' })]
mockMCPToolsData.mockReturnValue(mcpTools)
@@ -788,7 +783,7 @@ describe('MultipleToolSelector', () => {
]
// Act
renderComponent({ value: tools, canChooseMCPTool: false })
renderComponent({ value: tools, versionSupported: false })
// Assert - Only regular tool should be counted
expect(screen.getByText('1/2')).toBeInTheDocument()

View File

@@ -12,6 +12,7 @@ import Divider from '@/app/components/base/divider'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import Tooltip from '@/app/components/base/tooltip'
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
import { useAllMCPTools } from '@/service/use-tools'
import { cn } from '@/utils/classnames'
@@ -27,7 +28,6 @@ type Props = {
nodeOutputVars: NodeOutPutVar[]
availableNodes: Node[]
nodeId?: string
canChooseMCPTool?: boolean
}
const MultipleToolSelector = ({
@@ -42,14 +42,14 @@ const MultipleToolSelector = ({
nodeOutputVars,
availableNodes,
nodeId,
canChooseMCPTool,
}: Props) => {
const { t } = useTranslation()
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
const { data: mcpTools } = useAllMCPTools()
const enabledCount = value.filter((item) => {
const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name)
if (isMCPTool)
return item.enabled && canChooseMCPTool
return item.enabled && isMCPToolAllowed
return item.enabled
}).length
// collapse control
@@ -167,7 +167,6 @@ const MultipleToolSelector = ({
onSelectMultiple={handleAddMultiple}
onDelete={() => handleDelete(index)}
supportEnableSwitch
canChooseMCPTool={canChooseMCPTool}
isEdit
/>
</div>
@@ -190,7 +189,6 @@ const MultipleToolSelector = ({
panelShowState={panelShowState}
onPanelShowStateChange={setPanelShowState}
isEdit={false}
canChooseMCPTool={canChooseMCPTool}
onSelectMultiple={handleAddMultiple}
/>
</>

View File

@@ -0,0 +1,8 @@
export { default as ReasoningConfigForm } from './reasoning-config-form'
export { default as SchemaModal } from './schema-modal'
export { default as ToolAuthorizationSection } from './tool-authorization-section'
export { default as ToolBaseForm } from './tool-base-form'
export { default as ToolCredentialsForm } from './tool-credentials-form'
export { default as ToolItem } from './tool-item'
export { default as ToolSettingsPanel } from './tool-settings-panel'
export { default as ToolTrigger } from './tool-trigger'

View File

@@ -1,4 +1,6 @@
import type { Node } from 'reactflow'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import type {
@@ -35,7 +37,7 @@ import SchemaModal from './schema-modal'
type Props = {
value: Record<string, any>
onChange: (val: Record<string, any>) => void
schemas: any[]
schemas: ToolFormSchema[]
nodeOutputVars: NodeOutPutVar[]
availableNodes: Node[]
nodeId: string
@@ -51,7 +53,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
}) => {
const { t } = useTranslation()
const language = useLanguage()
const getVarKindType = (type: FormTypeEnum) => {
const getVarKindType = (type: string) => {
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return VarKindType.variable
if (type === FormTypeEnum.select || type === FormTypeEnum.checkbox || type === FormTypeEnum.textNumber || type === FormTypeEnum.array || type === FormTypeEnum.object)
@@ -60,7 +62,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
return VarKindType.mixed
}
const handleAutomatic = (key: string, val: any, type: FormTypeEnum) => {
const handleAutomatic = (key: string, val: any, type: string) => {
onChange({
...value,
[key]: {
@@ -80,7 +82,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
onChange(res)
}
}, [onChange, value])
const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => {
const handleValueChange = useCallback((variable: string, varType: string) => {
return (newValue: any) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
@@ -134,7 +136,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
const [schema, setSchema] = useState<SchemaRoot | null>(null)
const [schemaRootName, setSchemaRootName] = useState<string>('')
const renderField = (schema: any, showSchema: (schema: SchemaRoot, rootName: string) => void) => {
const renderField = (schema: ToolFormSchema, showSchema: (schema: SchemaRoot, rootName: string) => void) => {
const {
default: defaultValue,
variable,
@@ -275,16 +277,16 @@ const ReasoningConfigForm: React.FC<Props> = ({
onChange={handleValueChange(variable, type)}
/>
)}
{isSelect && (
{isSelect && options && (
<SimpleSelect
wrapperClassName="h-8 grow"
defaultValue={varInput?.value}
items={options.filter((option: { show_on: any[] }) => {
items={options.filter((option) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any, label: { [x: string]: any, en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
}).map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleValueChange(variable, type)(item.value as string)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
@@ -332,7 +334,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
value={varInput?.value || []}
onChange={handleVariableSelectorChange(variable)}
filterVar={getFilterVar()}
schema={schema}
schema={schema as Partial<CredentialFormSchema>}
valueTypePlaceHolder={targetVarType()}
/>
)}

View File

@@ -0,0 +1,48 @@
'use client'
import type { FC } from 'react'
import type { ToolWithProvider } from '@/app/components/workflow/types'
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,
}) => {
// Only show for built-in providers that allow deletion
const shouldShow = currentProvider
&& currentProvider.type === CollectionType.builtIn
&& currentProvider.allow_delete
if (!shouldShow)
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,
}}
credentialId={credentialId}
onAuthorizationItemClick={onAuthorizationItemClick}
/>
</div>
</>
)
}
export default ToolAuthorizationSection

View File

@@ -0,0 +1,98 @@
'use client'
import type { OffsetOptions } from '@floating-ui/react'
import type { FC } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import { ReadmeEntrance } from '../../../readme-panel/entrance'
import ToolTrigger from './tool-trigger'
type ToolBaseFormProps = {
value?: ToolValue
currentProvider?: ToolWithProvider
offset?: OffsetOptions
scope?: string
selectedTools?: ToolValue[]
isShowChooseTool: boolean
panelShowState?: boolean
hasTrigger: boolean
onShowChange: (show: boolean) => void
onPanelShowStateChange?: (state: boolean) => void
onSelectTool: (tool: ToolDefaultValue) => void
onSelectMultipleTool: (tools: ToolDefaultValue[]) => void
onDescriptionChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
}
const ToolBaseForm: FC<ToolBaseFormProps> = ({
value,
currentProvider,
offset = 4,
scope,
selectedTools,
isShowChooseTool,
panelShowState,
hasTrigger,
onShowChange,
onPanelShowStateChange,
onSelectTool,
onSelectMultipleTool,
onDescriptionChange,
}) => {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-3 px-4 py-2">
{/* Tool picker */}
<div className="flex flex-col gap-1">
<div className="system-sm-semibold flex h-6 items-center justify-between text-text-secondary">
{t('detailPanel.toolSelector.toolLabel', { ns: 'plugin' })}
{currentProvider?.plugin_unique_identifier && (
<ReadmeEntrance
pluginDetail={currentProvider as unknown as PluginDetail}
showShortTip
className="pb-0"
/>
)}
</div>
<ToolPicker
placement="bottom"
offset={offset}
trigger={(
<ToolTrigger
open={panelShowState || isShowChooseTool}
value={value}
provider={currentProvider}
/>
)}
isShow={panelShowState || isShowChooseTool}
onShowChange={hasTrigger ? (onPanelShowStateChange || (() => {})) : onShowChange}
disabled={false}
supportAddCustomTool
onSelect={onSelectTool}
onSelectMultiple={onSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
/>
</div>
{/* Description */}
<div className="flex flex-col gap-1">
<div className="system-sm-semibold flex h-6 items-center text-text-secondary">
{t('detailPanel.toolSelector.descriptionLabel', { ns: 'plugin' })}
</div>
<Textarea
className="resize-none"
placeholder={t('detailPanel.toolSelector.descriptionPlaceholder', { ns: 'plugin' })}
value={value?.extra?.description || ''}
onChange={onDescriptionChange}
disabled={!value?.provider_name}
/>
</div>
</div>
)
}
export default ToolBaseForm

View File

@@ -16,6 +16,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { ToolTipContent } from '@/app/components/base/tooltip/content'
import Indicator from '@/app/components/header/indicator'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip'
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
import { cn } from '@/utils/classnames'
@@ -39,7 +40,6 @@ type Props = {
versionMismatch?: boolean
open: boolean
authRemoved?: boolean
canChooseMCPTool?: boolean
}
const ToolItem = ({
@@ -61,13 +61,13 @@ const ToolItem = ({
errorTip,
versionMismatch,
authRemoved,
canChooseMCPTool,
}: Props) => {
const { t } = useTranslation()
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop()
const isTransparent = uninstalled || versionMismatch || isError
const [isDeleting, setIsDeleting] = useState(false)
const isShowCanNotChooseMCPTip = isMCPTool && !canChooseMCPTool
const isShowCanNotChooseMCPTip = isMCPTool && !isMCPToolAllowed
return (
<div className={cn(
@@ -125,9 +125,7 @@ const ToolItem = ({
/>
</div>
)}
{isShowCanNotChooseMCPTip && (
<McpToolNotSupportTooltip />
)}
{isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />}
{!isError && !uninstalled && !versionMismatch && noAuth && (
<Button variant="secondary" size="small">
{t('notAuthorized', { ns: 'tools' })}

View File

@@ -0,0 +1,155 @@
'use client'
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { TabType } from '../hooks/use-tool-selector-state'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import ReasoningConfigForm from './reasoning-config-form'
type ToolSettingsPanelProps = {
value?: ToolValue
currentProvider?: ToolWithProvider
nodeId: string
currType: TabType
settingsFormSchemas: ToolFormSchema[]
paramsFormSchemas: ToolFormSchema[]
settingsValue: Record<string, any>
showTabSlider: boolean
userSettingsOnly: boolean
reasoningConfigOnly: boolean
nodeOutputVars: NodeOutPutVar[]
availableNodes: Node[]
onCurrTypeChange: (type: TabType) => void
onSettingsFormChange: (v: Record<string, any>) => void
onParamsFormChange: (v: Record<string, any>) => void
}
/**
* Renders the settings/params tips section
*/
const ParamsTips: FC = () => {
const { t } = useTranslation()
return (
<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>
)
}
const ToolSettingsPanel: FC<ToolSettingsPanelProps> = ({
value,
currentProvider,
nodeId,
currType,
settingsFormSchemas,
paramsFormSchemas,
settingsValue,
showTabSlider,
userSettingsOnly,
reasoningConfigOnly,
nodeOutputVars,
availableNodes,
onCurrTypeChange,
onSettingsFormChange,
onParamsFormChange,
}) => {
const { t } = useTranslation()
// Check if panel should be shown
const hasSettings = settingsFormSchemas.length > 0
const hasParams = paramsFormSchemas.length > 0
const isTeamAuthorized = currentProvider?.is_team_authorization
if ((!hasSettings && !hasParams) || !isTeamAuthorized)
return null
return (
<>
<Divider className="my-1 w-full" />
{/* Tab slider - shown only when both settings and params exist */}
{nodeId && showTabSlider && (
<TabSlider
className="mt-1 shrink-0 px-4"
itemClassName="py-3"
noBorderBottom
smallItem
value={currType}
onChange={(v) => {
if (v === 'settings' || v === 'params')
onCurrTypeChange(v)
}}
options={[
{ value: 'settings', text: t('detailPanel.toolSelector.settings', { ns: 'plugin' })! },
{ value: 'params', text: t('detailPanel.toolSelector.params', { ns: 'plugin' })! },
]}
/>
)}
{/* Params tips when tab slider and params tab is active */}
{nodeId && showTabSlider && currType === 'params' && (
<div className="px-4 py-2">
<ParamsTips />
</div>
)}
{/* User settings only header */}
{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 header */}
{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>
<ParamsTips />
</div>
)}
{/* User settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className="px-4 py-2">
<ToolForm
inPanel
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as CredentialFormSchema[]}
value={settingsValue}
onChange={onSettingsFormChange}
/>
</div>
)}
{/* Reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={onParamsFormChange}
schemas={paramsFormSchemas}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)
}
export default ToolSettingsPanel

View File

@@ -0,0 +1,3 @@
export { usePluginInstalledCheck } from './use-plugin-installed-check'
export { useToolSelectorState } from './use-tool-selector-state'
export type { TabType, ToolSelectorState, UseToolSelectorStateProps } from './use-tool-selector-state'

View File

@@ -10,5 +10,6 @@ export const usePluginInstalledCheck = (providerName = '') => {
return {
inMarketPlace: !!manifest,
manifest: manifest?.data.plugin,
pluginID,
}
}

View File

@@ -0,0 +1,247 @@
'use client'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import { useCallback, useMemo, useState } from 'react'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
} from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { usePluginInstalledCheck } from './use-plugin-installed-check'
export type TabType = 'settings' | 'params'
export type UseToolSelectorStateProps = {
value?: ToolValue
onSelect: (tool: ToolValue) => void
onSelectMultiple?: (tool: ToolValue[]) => void
}
/**
* Custom hook for managing tool selector state and computed values.
* Consolidates state management, data fetching, and event handlers.
*/
export const useToolSelectorState = ({
value,
onSelect,
onSelectMultiple,
}: UseToolSelectorStateProps) => {
// Panel visibility states
const [isShow, setIsShow] = useState(false)
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const [currType, setCurrType] = useState<TabType>('settings')
// Fetch all tools data
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
// Plugin info check
const { inMarketPlace, manifest, pluginID } = usePluginInstalledCheck(value?.provider_name)
// Merge all tools and find current provider
const currentProvider = useMemo(() => {
const mergedTools = [
...(buildInTools || []),
...(customTools || []),
...(workflowTools || []),
...(mcpTools || []),
]
return mergedTools.find(toolWithProvider => toolWithProvider.id === value?.provider_name)
}, [value, buildInTools, customTools, workflowTools, mcpTools])
// Current tool from provider
const currentTool = useMemo(() => {
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
}, [currentProvider?.tools, value?.tool_name])
// Tool settings and 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])
// Form schemas
const settingsFormSchemas = useMemo(
() => toolParametersToFormSchemas(currentToolSettings),
[currentToolSettings],
)
const paramsFormSchemas = useMemo(
() => toolParametersToFormSchemas(currentToolParams),
[currentToolParams],
)
// Tab visibility flags
const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
// Manifest icon URL
const manifestIcon = useMemo(() => {
if (!manifest || !pluginID)
return ''
return getIconFromMarketPlace(pluginID)
}, [manifest, pluginID])
// Convert tool default value to tool value format
const getToolValue = useCallback((tool: ToolDefaultValue): ToolValue => {
const settingValues = generateFormValue(
tool.params,
toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any),
)
const paramValues = generateFormValue(
tool.params,
toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any),
true,
)
return {
provider_name: tool.provider_id,
provider_show_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_description: tool.tool_description,
settings: settingValues,
parameters: paramValues,
enabled: tool.is_team_authorization,
extra: {
description: tool.tool_description,
},
}
}, [])
// Event handlers
const handleSelectTool = useCallback((tool: ToolDefaultValue) => {
const toolValue = getToolValue(tool)
onSelect(toolValue)
}, [getToolValue, onSelect])
const handleSelectMultipleTool = useCallback((tools: ToolDefaultValue[]) => {
const toolValues = tools.map(item => getToolValue(item))
onSelectMultiple?.(toolValues)
}, [getToolValue, onSelectMultiple])
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!value)
return
onSelect({
...value,
extra: {
...value.extra,
description: e.target.value || '',
},
})
}, [value, onSelect])
const handleSettingsFormChange = useCallback((v: Record<string, any>) => {
if (!value)
return
const newValue = getStructureValue(v)
onSelect({
...value,
settings: newValue,
})
}, [value, onSelect])
const handleParamsFormChange = useCallback((v: Record<string, any>) => {
if (!value)
return
onSelect({
...value,
parameters: v,
})
}, [value, onSelect])
const handleEnabledChange = useCallback((state: boolean) => {
if (!value)
return
onSelect({
...value,
enabled: state,
})
}, [value, onSelect])
const handleAuthorizationItemClick = useCallback((id: string) => {
if (!value)
return
onSelect({
...value,
credential_id: id,
})
}, [value, onSelect])
const handleInstall = useCallback(async () => {
try {
await invalidateAllBuiltinTools()
}
catch (error) {
console.error('Failed to invalidate built-in tools cache', error)
}
try {
await invalidateInstalledPluginList()
}
catch (error) {
console.error('Failed to invalidate installed plugin list cache', error)
}
}, [invalidateAllBuiltinTools, invalidateInstalledPluginList])
const getSettingsValue = useCallback(() => {
return getPlainValue(value?.settings || {})
}, [value?.settings])
return {
// State
isShow,
setIsShow,
isShowChooseTool,
setIsShowChooseTool,
currType,
setCurrType,
// Computed values
currentProvider,
currentTool,
currentToolSettings,
currentToolParams,
settingsFormSchemas,
paramsFormSchemas,
showTabSlider,
userSettingsOnly,
reasoningConfigOnly,
manifestIcon,
inMarketPlace,
manifest,
// Event handlers
handleSelectTool,
handleSelectMultipleTool,
handleDescriptionChange,
handleSettingsFormChange,
handleParamsFormChange,
handleEnabledChange,
handleAuthorizationItemClick,
handleInstall,
getSettingsValue,
}
}
export type ToolSelectorState = ReturnType<typeof useToolSelectorState>

File diff suppressed because it is too large Load Diff

View File

@@ -5,43 +5,26 @@ import type {
} from '@floating-ui/react'
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
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 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 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 {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
} from '@/service/use-tools'
import { cn } from '@/utils/classnames'
import { ReadmeEntrance } from '../../readme-panel/entrance'
import {
ToolAuthorizationSection,
ToolBaseForm,
ToolItem,
ToolSettingsPanel,
ToolTrigger,
} from './components'
import { useToolSelectorState } from './hooks/use-tool-selector-state'
type Props = {
disabled?: boolean
@@ -64,8 +47,8 @@ type Props = {
nodeOutputVars: NodeOutPutVar[]
availableNodes: Node[]
nodeId?: string
canChooseMCPTool?: boolean
}
const ToolSelector: FC<Props> = ({
value,
selectedTools,
@@ -86,326 +69,179 @@ const ToolSelector: FC<Props> = ({
nodeOutputVars,
availableNodes,
nodeId = '',
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const [isShow, onShowChange] = useState(false)
// Use custom hook for state management
const state = useToolSelectorState({ value, onSelect, onSelectMultiple })
const {
isShow,
setIsShow,
isShowChooseTool,
setIsShowChooseTool,
currType,
setCurrType,
currentProvider,
currentTool,
settingsFormSchemas,
paramsFormSchemas,
showTabSlider,
userSettingsOnly,
reasoningConfigOnly,
manifestIcon,
inMarketPlace,
manifest,
handleSelectTool,
handleSelectMultipleTool,
handleDescriptionChange,
handleSettingsFormChange,
handleParamsFormChange,
handleEnabledChange,
handleAuthorizationItemClick,
handleInstall,
getSettingsValue,
} = state
const handleTriggerClick = () => {
if (disabled)
return
onShowChange(true)
setIsShow(true)
}
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
// Determine portal open state based on controlled vs uncontrolled mode
const portalOpen = trigger ? controlledState : isShow
const onPortalOpenChange = trigger ? onControlledStateChange : setIsShow
// plugin info check
const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider_name
})
}, [value, buildInTools, customTools, workflowTools, mcpTools])
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const getToolValue = (tool: ToolDefaultValue) => {
const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
return {
provider_name: tool.provider_id,
provider_show_name: tool.provider_name,
type: tool.provider_type,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_description: tool.tool_description,
settings: settingValues,
parameters: paramValues,
enabled: tool.is_team_authorization,
extra: {
description: tool.tool_description,
},
schemas: tool.paramSchemas,
}
}
const handleSelectTool = (tool: ToolDefaultValue) => {
const toolValue = getToolValue(tool)
onSelect(toolValue)
// setIsShowChooseTool(false)
}
const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
const toolValues = tool.map(item => getToolValue(item))
onSelectMultiple?.(toolValues)
}
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onSelect({
...value,
extra: {
...value?.extra,
description: e.target.value || '',
},
} 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,
enabled: state,
} as any)
}
// install from marketplace
const currentTool = useMemo(() => {
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
}, [currentProvider?.tools, value?.tool_name])
const manifestIcon = useMemo(() => {
if (!manifest)
return ''
return `${MARKETPLACE_API_PREFIX}/plugins/${(manifest as any).plugin_id}/icon`
}, [manifest])
const handleInstall = async () => {
invalidateAllBuiltinTools()
invalidateInstalledPluginList()
}
const handleAuthorizationItemClick = (id: string) => {
onSelect({
...value,
credential_id: id,
} as any)
}
// Build error tooltip content
const renderErrorTip = () => (
<div className="max-w-[240px] space-y-1 text-xs">
<h3 className="font-semibold text-text-primary">
{currentTool
? t('detailPanel.toolSelector.uninstalledTitle', { ns: 'plugin' })
: t('detailPanel.toolSelector.unsupportedTitle', { ns: 'plugin' })}
</h3>
<p className="tracking-tight text-text-secondary">
{currentTool
? t('detailPanel.toolSelector.uninstalledContent', { ns: 'plugin' })
: t('detailPanel.toolSelector.unsupportedContent', { ns: 'plugin' })}
</p>
<p>
<Link href="/plugins" className="tracking-tight text-text-accent">
{t('detailPanel.toolSelector.uninstalledLink', { ns: 'plugin' })}
</Link>
</p>
</div>
)
return (
<>
<PortalToFollowElem
placement={placement}
offset={offset}
open={trigger ? controlledState : isShow}
onOpenChange={trigger ? onControlledStateChange : onShowChange}
<PortalToFollowElem
placement={placement}
offset={offset}
open={portalOpen}
onOpenChange={onPortalOpenChange}
>
<PortalToFollowElemTrigger
className="w-full"
onClick={() => {
if (!currentProvider || !currentTool)
return
handleTriggerClick()
}}
>
<PortalToFollowElemTrigger
className="w-full"
onClick={() => {
if (!currentProvider || !currentTool)
return
handleTriggerClick()
}}
{trigger}
{/* Default trigger - no value */}
{!trigger && !value?.provider_name && (
<ToolTrigger
isConfigure
open={isShow}
value={value}
provider={currentProvider}
/>
)}
{/* Default trigger - with value */}
{!trigger && value?.provider_name && (
<ToolItem
open={isShow}
icon={currentProvider?.icon || manifestIcon}
isMCPTool={currentProvider?.type === CollectionType.mcp}
providerName={value.provider_name}
providerShowName={value.provider_show_name}
toolLabel={value.tool_label || value.tool_name}
showSwitch={supportEnableSwitch}
switchValue={value.enabled}
onSwitchChange={handleEnabledChange}
onDelete={onDelete}
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
uninstalled={!currentProvider && inMarketPlace}
versionMismatch={currentProvider && inMarketPlace && !currentTool}
installInfo={manifest?.latest_package_identifier}
onInstall={handleInstall}
isError={(!currentProvider || !currentTool) && !inMarketPlace}
errorTip={renderErrorTip()}
/>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-10">
<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',
'overflow-y-auto pb-2 pb-4 shadow-lg backdrop-blur-sm',
)}
>
{trigger}
{!trigger && !value?.provider_name && (
<ToolTrigger
isConfigure
open={isShow}
value={value}
provider={currentProvider}
/>
)}
{!trigger && value?.provider_name && (
<ToolItem
open={isShow}
icon={currentProvider?.icon || manifestIcon}
isMCPTool={currentProvider?.type === CollectionType.mcp}
providerName={value.provider_name}
providerShowName={value.provider_show_name}
toolLabel={value.tool_label || value.tool_name}
showSwitch={supportEnableSwitch}
switchValue={value.enabled}
onSwitchChange={handleEnabledChange}
onDelete={onDelete}
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
uninstalled={!currentProvider && inMarketPlace}
versionMismatch={currentProvider && inMarketPlace && !currentTool}
installInfo={manifest?.latest_package_identifier}
onInstall={() => handleInstall()}
isError={(!currentProvider || !currentTool) && !inMarketPlace}
errorTip={(
<div className="max-w-[240px] space-y-1 text-xs">
<h3 className="font-semibold text-text-primary">{currentTool ? t('detailPanel.toolSelector.uninstalledTitle', { ns: 'plugin' }) : t('detailPanel.toolSelector.unsupportedTitle', { ns: 'plugin' })}</h3>
<p className="tracking-tight text-text-secondary">{currentTool ? t('detailPanel.toolSelector.uninstalledContent', { ns: 'plugin' }) : t('detailPanel.toolSelector.unsupportedContent', { ns: 'plugin' })}</p>
<p>
<Link href="/plugins" className="tracking-tight text-text-accent">{t('detailPanel.toolSelector.uninstalledLink', { ns: 'plugin' })}</Link>
</p>
</div>
)}
canChooseMCPTool={canChooseMCPTool}
/>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-10">
<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.${isEdit ? 'toolSetting' : 'title'}`, { ns: 'plugin' })}</div>
{/* base form */}
<div className="flex flex-col gap-3 px-4 py-2">
<div className="flex flex-col gap-1">
<div className="system-sm-semibold flex h-6 items-center justify-between text-text-secondary">
{t('detailPanel.toolSelector.toolLabel', { ns: 'plugin' })}
<ReadmeEntrance pluginDetail={currentProvider as any} showShortTip className="pb-0" />
</div>
<ToolPicker
placement="bottom"
offset={offset}
trigger={(
<ToolTrigger
open={panelShowState || isShowChooseTool}
value={value}
provider={currentProvider}
/>
)}
isShow={panelShowState || isShowChooseTool}
onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
<div className="flex flex-col gap-1">
<div className="system-sm-semibold flex h-6 items-center text-text-secondary">{t('detailPanel.toolSelector.descriptionLabel', { ns: 'plugin' })}</div>
<Textarea
className="resize-none"
placeholder={t('detailPanel.toolSelector.descriptionPlaceholder', { ns: 'plugin' })}
value={value?.extra?.description || ''}
onChange={handleDescriptionChange}
disabled={!value?.provider_name}
/>
</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>
</>
)}
{/* 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}
/>
)}
</>
)}
</>
{/* Header */}
<div className="system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary">
{t(`detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`, { ns: 'plugin' })}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</>
{/* Base form: tool picker + description */}
<ToolBaseForm
value={value}
currentProvider={currentProvider}
offset={offset}
scope={scope}
selectedTools={selectedTools}
isShowChooseTool={isShowChooseTool}
panelShowState={panelShowState}
hasTrigger={!!trigger}
onShowChange={setIsShowChooseTool}
onPanelShowStateChange={onPanelShowStateChange}
onSelectTool={handleSelectTool}
onSelectMultipleTool={handleSelectMultipleTool}
onDescriptionChange={handleDescriptionChange}
/>
{/* Authorization section */}
<ToolAuthorizationSection
currentProvider={currentProvider}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
{/* Settings panel */}
<ToolSettingsPanel
value={value}
currentProvider={currentProvider}
nodeId={nodeId}
currType={currType}
settingsFormSchemas={settingsFormSchemas}
paramsFormSchemas={paramsFormSchemas}
settingsValue={getSettingsValue()}
showTabSlider={showTabSlider}
userSettingsOnly={userSettingsOnly}
reasoningConfigOnly={reasoningConfigOnly}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onCurrTypeChange={setCurrType}
onSettingsFormChange={handleSettingsFormChange}
onParamsFormChange={handleParamsFormChange}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(ToolSelector)

View File

@@ -1,8 +1,41 @@
import type { TriggerEventParameter } from '../../plugins/types'
import type { ToolCredential, ToolParameter } from '../types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
/**
* Form schema type for tool parameters.
* This type represents the schema returned by toolParametersToFormSchemas.
*/
export type ToolFormSchema = {
name: string
variable: string
label: TypeWithI18N
type: string
_type: string
form: string
required: boolean
default?: string
tooltip?: TypeWithI18N
show_on: { variable: string, value: string }[]
options?: {
label: TypeWithI18N
value: string
show_on: { variable: string, value: string }[]
}[]
placeholder?: TypeWithI18N
min?: number
max?: number
llm_description?: string
human_description?: TypeWithI18N
multiple?: boolean
url?: string
scope?: string
input_schema?: SchemaRoot
}
export const toType = (type: string) => {
switch (type) {
case 'string':
@@ -30,11 +63,11 @@ export const triggerEventParametersToFormSchemas = (parameters: TriggerEventPara
})
}
export const toolParametersToFormSchemas = (parameters: ToolParameter[]) => {
export const toolParametersToFormSchemas = (parameters: ToolParameter[]): ToolFormSchema[] => {
if (!parameters)
return []
const formSchemas = parameters.map((parameter) => {
const formSchemas = parameters.map((parameter): ToolFormSchema => {
return {
...parameter,
variable: parameter.name,

View File

@@ -47,7 +47,6 @@ type AllToolsProps = {
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
onTagsChange?: Dispatch<SetStateAction<string[]>>
isInRAGPipeline?: boolean
featuredPlugins?: Plugin[]
@@ -71,7 +70,6 @@ const AllTools = ({
customTools,
mcpTools = [],
selectedTools,
canChooseMCPTool,
onTagsChange,
isInRAGPipeline = false,
featuredPlugins = [],
@@ -249,7 +247,6 @@ const AllTools = ({
providerMap={providerMap}
onSelect={onSelect}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
isLoading={featuredLoading}
onInstallSuccess={async () => {
await onFeaturedInstallSuccess?.()
@@ -275,7 +272,6 @@ const AllTools = ({
viewType={isSupportGroupView ? activeView : ViewType.flat}
hasSearchText={hasSearchText}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</>
)}

View File

@@ -30,7 +30,6 @@ type FeaturedToolsProps = {
providerMap: Map<string, ToolWithProvider>
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
isLoading?: boolean
onInstallSuccess?: () => void
}
@@ -42,7 +41,6 @@ const FeaturedTools = ({
providerMap,
onSelect,
selectedTools,
canChooseMCPTool,
isLoading = false,
onInstallSuccess,
}: FeaturedToolsProps) => {
@@ -166,7 +164,6 @@ const FeaturedTools = ({
viewType={ViewType.flat}
hasSearchText={false}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
)}

View File

@@ -223,7 +223,6 @@ const Tabs: FC<TabsProps> = ({
customTools={customTools || []}
workflowTools={workflowTools || []}
mcpTools={mcpTools || []}
canChooseMCPTool
onTagsChange={onTagsChange}
isInRAGPipeline={inRAGPipeline}
featuredPlugins={featuredPlugins}

View File

@@ -50,7 +50,6 @@ type Props = {
supportAddCustomTool?: boolean
scope?: string
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}
const ToolPicker: FC<Props> = ({
@@ -66,7 +65,6 @@ const ToolPicker: FC<Props> = ({
scope = 'all',
selectedTools,
panelClassName,
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
@@ -198,7 +196,6 @@ const ToolPicker: FC<Props> = ({
workflowTools={workflowToolList || []}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
onTagsChange={setTags}
featuredPlugins={featuredPlugins}
featuredLoading={isFeaturedLoading}

View File

@@ -18,7 +18,6 @@ type Props = {
letters: string[]
toolRefs: any
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}
const ToolViewFlatView: FC<Props> = ({
@@ -32,7 +31,6 @@ const ToolViewFlatView: FC<Props> = ({
onSelectMultiple,
toolRefs,
selectedTools,
canChooseMCPTool,
}) => {
const firstLetterToolIds = useMemo(() => {
const res: Record<string, string> = {}
@@ -63,7 +61,6 @@ const ToolViewFlatView: FC<Props> = ({
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
))}

View File

@@ -14,7 +14,6 @@ type Props = {
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}
const Item: FC<Props> = ({
@@ -25,7 +24,6 @@ const Item: FC<Props> = ({
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
canChooseMCPTool,
}) => {
return (
<div>
@@ -43,7 +41,6 @@ const Item: FC<Props> = ({
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
))}
</div>

View File

@@ -15,7 +15,6 @@ type Props = {
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}
const ToolListTreeView: FC<Props> = ({
@@ -25,7 +24,6 @@ const ToolListTreeView: FC<Props> = ({
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const getI18nGroupName = useCallback((name: string) => {
@@ -56,7 +54,6 @@ const ToolListTreeView: FC<Props> = ({
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
))}
</div>

View File

@@ -9,6 +9,7 @@ import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Mcp } from '@/app/components/base/icons/src/vender/other'
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
@@ -38,7 +39,6 @@ type Props = {
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
isShowLetterIndex?: boolean
}
@@ -51,9 +51,9 @@ const Tool: FC<Props> = ({
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat
const notShowProvider = payload.type === CollectionType.workflow
@@ -63,7 +63,7 @@ const Tool: FC<Props> = ({
const ref = useRef(null)
const isHovering = useHover(ref)
const isMCPTool = payload.type === CollectionType.mcp
const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool
const isShowCanNotChooseMCPTip = !isMCPToolAllowed && isMCPTool
const { theme } = useTheme()
const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => {
return normalizeProviderIcon(payload.icon) ?? payload.icon

View File

@@ -21,7 +21,6 @@ type ToolsProps = {
className?: string
indexBarClassName?: string
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}
const Tools = ({
onSelect,
@@ -35,7 +34,6 @@ const Tools = ({
className,
indexBarClassName,
selectedTools,
canChooseMCPTool,
}: ToolsProps) => {
// const tools: any = []
const language = useGetLanguage()
@@ -109,7 +107,6 @@ const Tools = ({
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
/>
)
@@ -121,7 +118,6 @@ const Tools = ({
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
)
)}

View File

@@ -92,13 +92,12 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s
export type AgentStrategySelectorProps = {
value?: Strategy
onChange: (value?: Strategy) => void
canChooseMCPTool: boolean
}
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { value, onChange, canChooseMCPTool } = props
const { value, onChange } = props
const [open, setOpen] = useState(false)
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
const [query, setQuery] = useState('')
@@ -242,7 +241,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
indexBarClassName="top-0 xl:top-36"
hasSearchText={false}
canNotSelectMultiple
canChooseMCPTool={canChooseMCPTool}
isAgent
/>
{enable_marketplace && (

View File

@@ -43,7 +43,6 @@ export type AgentStrategyProps = {
nodeOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
nodeId?: string
canChooseMCPTool: boolean
}
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
@@ -54,7 +53,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
const { t } = useTranslation()
const docLink = useDocLink()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
@@ -189,7 +188,6 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
value={value}
onSelect={item => onChange(item)}
onDelete={() => onChange(null)}
canChooseMCPTool={canChooseMCPTool}
onSelectMultiple={noop}
/>
</Field>
@@ -212,7 +210,6 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
onChange={onChange}
supportCollapse
required={schema.required}
canChooseMCPTool={canChooseMCPTool}
/>
)
}
@@ -220,7 +217,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
}
return (
<div className="space-y-2">
<AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} />
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
{
strategy
? (
@@ -241,7 +238,6 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
)

View File

@@ -0,0 +1,38 @@
'use client'
import type { ReactNode } from 'react'
import { createContext, useContext } from 'react'
type MCPToolAvailabilityContextValue = {
versionSupported?: boolean
}
const MCPToolAvailabilityContext = createContext<MCPToolAvailabilityContextValue | undefined>(undefined)
export type MCPToolAvailability = {
allowed: boolean
versionSupported?: boolean
}
export const MCPToolAvailabilityProvider = ({
versionSupported,
children,
}: {
versionSupported?: boolean
children: ReactNode
}) => (
<MCPToolAvailabilityContext.Provider value={{ versionSupported }}>
{children}
</MCPToolAvailabilityContext.Provider>
)
export const useMCPToolAvailability = (): MCPToolAvailability => {
const context = useContext(MCPToolAvailabilityContext)
if (context === undefined)
return { allowed: true }
const { versionSupported } = context
return {
allowed: versionSupported === true,
versionSupported,
}
}

View File

@@ -6,9 +6,11 @@ import type { StrategyParamItem } from '@/app/components/plugins/types'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { toType } from '@/app/components/tools/utils/to-form-schema'
import { isSupportMCP } from '@/utils/plugin-version-feature'
import { useStore } from '../../store'
import { AgentStrategy } from '../_base/components/agent-strategy'
import Field from '../_base/components/field'
import { MCPToolAvailabilityProvider } from '../_base/components/mcp-tool-availability'
import MemoryConfig from '../_base/components/memory-config'
import OutputVars, { VarItem } from '../_base/components/output-vars'
import Split from '../_base/components/split'
@@ -40,9 +42,9 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
readOnly,
outputSchema,
handleMemoryChange,
canChooseMCPTool,
} = useConfig(props.id, props.data)
const { t } = useTranslation()
const isMCPVersionSupported = isSupportMCP(inputs.meta?.version)
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
return (
@@ -53,37 +55,38 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
className="px-4 py-2"
tooltip={t('nodes.agent.strategy.tooltip', { ns: 'workflow' })}
>
<AgentStrategy
strategy={inputs.agent_strategy_name
? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!,
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
plugin_unique_identifier: inputs.plugin_unique_identifier!,
meta: inputs.meta,
}
: undefined}
onStrategyChange={(strategy) => {
setInputs({
...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name,
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
plugin_unique_identifier: strategy!.plugin_unique_identifier,
meta: strategy?.meta,
})
resetEditor(Date.now())
}}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={formData}
onFormValueChange={onFormChange}
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodeId={props.id}
canChooseMCPTool={canChooseMCPTool}
/>
<MCPToolAvailabilityProvider versionSupported={isMCPVersionSupported}>
<AgentStrategy
strategy={inputs.agent_strategy_name
? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!,
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
plugin_unique_identifier: inputs.plugin_unique_identifier!,
meta: inputs.meta,
}
: undefined}
onStrategyChange={(strategy) => {
setInputs({
...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name,
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
plugin_unique_identifier: strategy!.plugin_unique_identifier,
meta: strategy?.meta,
})
resetEditor(Date.now())
}}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={formData}
onFormValueChange={onFormChange}
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodeId={props.id}
/>
</MCPToolAvailabilityProvider>
</Field>
<div className="px-4 py-2">
{isChatMode && currentStrategy?.features?.includes(AgentFeature.HISTORY_MESSAGES) && (

View File

@@ -11,7 +11,6 @@ import {
} from '@/app/components/workflow/hooks'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import { useStrategyProviderDetail } from '@/service/use-strategy'
import { isSupportMCP } from '@/utils/plugin-version-feature'
import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list'
import useNodeCrud from '../_base/hooks/use-node-crud'
@@ -222,7 +221,6 @@ const useConfig = (id: string, payload: AgentNodeType) => {
outputSchema,
handleMemoryChange,
isChatMode,
canChooseMCPTool: isSupportMCP(inputs.meta?.version),
}
}

View File

@@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components'
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
type Props = {

View File

@@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components'
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
type Props = {