From fc05d26eafbea052039a0ab6aa40bfda40123f46 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Thu, 25 Sep 2025 12:05:21 +0800 Subject: [PATCH] feat: add TestConnection API endpoint and corresponding frontend functionality --- apps/tools/serializers/tool.py | 28 ++++++++++++++++--------- apps/tools/urls.py | 1 + apps/tools/views/tool.py | 25 ++++++++++++++++++++++ ui/src/api/system-shared/tool.ts | 14 ++++++++++++- ui/src/api/tool/tool.ts | 14 ++++++++++++- ui/src/views/tool/McpToolFormDrawer.vue | 16 ++++++++++++++ 6 files changed, 86 insertions(+), 12 deletions(-) diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py index 7c56c8998..d50e72206 100644 --- a/apps/tools/serializers/tool.py +++ b/apps/tools/serializers/tool.py @@ -5,11 +5,11 @@ import json import os import pickle import re -import requests import tempfile import zipfile from typing import Dict +import requests import uuid_utils.compat as uuid from django.core import validators from django.db import transaction @@ -356,9 +356,6 @@ class ToolSerializer(serializers.Serializer): ToolCreateRequest(data=instance).is_valid(raise_exception=True) # 校验代码是否包括禁止的关键字 ToolExecutor().validate_banned_keywords(instance.get('code', '')) - # 校验mcp json - if instance.get('tool_type') == ToolType.MCP.value: - validate_mcp_config(json.loads(instance.get('code'))) tool_id = uuid.uuid7() Tool( @@ -386,6 +383,18 @@ class ToolSerializer(serializers.Serializer): 'id': tool_id, 'workspace_id': self.data.get('workspace_id') }).one() + class TestConnection(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('workspace id')) + code = serializers.CharField(required=True, label=_('tool content')) + + def test_connection(self): + self.is_valid(raise_exception=True) + # 校验代码是否包括禁止的关键字 + ToolExecutor().validate_banned_keywords(self.data.get('code', '')) + # 校验mcp json + validate_mcp_config(json.loads(self.data.get('code'))) + return True + class Debug(serializers.Serializer): user_id = serializers.UUIDField(required=True, label=_('user id')) workspace_id = serializers.CharField(required=True, label=_('workspace id')) @@ -475,9 +484,7 @@ class ToolSerializer(serializers.Serializer): ToolEditRequest(data=instance).is_valid(raise_exception=True) # 校验代码是否包括禁止的关键字 ToolExecutor().validate_banned_keywords(instance.get('code', '')) - # 校验mcp json - if instance.get('tool_type') == ToolType.MCP.value: - validate_mcp_config(json.loads(instance.get('code'))) + if not QuerySet(Tool).filter(id=self.data.get('id')).exists(): raise serializers.ValidationError(_('Tool not found')) @@ -755,7 +762,8 @@ class ToolSerializer(serializers.Serializer): versions = tool.get('versions', []) tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else '' tool['version'] = next( - (version.get('name') for version in versions if version.get('downloadUrl') == tool['downloadUrl']), + (version.get('name') for version in versions if + version.get('downloadUrl') == tool['downloadUrl']), ) filter_apps.append(tool) @@ -836,7 +844,8 @@ class ToolSerializer(serializers.Serializer): raise AppApiException(500, _('Tool does not exist')) # 查找匹配的版本名称 version_name = next( - (version.get('name') for version in self.data.get('versions') if version.get('downloadUrl') == self.data.get('download_url')), + (version.get('name') for version in self.data.get('versions') if + version.get('downloadUrl') == self.data.get('download_url')), ) res = requests.get(self.data.get('download_url'), timeout=5) tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool @@ -855,7 +864,6 @@ class ToolSerializer(serializers.Serializer): return ToolModelSerializer(tool).data - class ToolTreeSerializer(serializers.Serializer): class Query(serializers.Serializer): workspace_id = serializers.CharField(required=True, label=_('workspace id')) diff --git a/apps/tools/urls.py b/apps/tools/urls.py index 7890a7885..11a2187b1 100644 --- a/apps/tools/urls.py +++ b/apps/tools/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('workspace//tool/pylint', views.ToolView.Pylint.as_view()), path('workspace//tool/debug', views.ToolView.Debug.as_view()), path('workspace//tool/tool_list', views.ToolView.Query.as_view()), + path('workspace//tool/test_connection', views.ToolView.TestConnection.as_view()), path('workspace//tool/', views.ToolView.Operate.as_view()), path('workspace//tool//edit_icon', views.ToolView.EditIcon.as_view()), path('workspace//tool//export', views.ToolView.Export.as_view()), diff --git a/apps/tools/views/tool.py b/apps/tools/views/tool.py index 1f397d024..89527a3c2 100644 --- a/apps/tools/views/tool.py +++ b/apps/tools/views/tool.py @@ -360,6 +360,31 @@ class ToolView(APIView): 'image': request.FILES.get('file') }).edit(request.data)) + class TestConnection(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['POST'], + description=_("Test tool connection"), + summary=_("Test tool connection"), + operation_id=_("Test tool connection"), # type: ignore + request=ToolReadAPI.get_request(), + responses=ToolReadAPI.get_response(), + tags=[_("Tool")] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_CREATE.get_workspace_permission(), + PermissionConstants.TOOL_CREATE.get_workspace_permission_workspace_manage_role(), + PermissionConstants.TOOL_EDIT.get_workspace_permission(), + PermissionConstants.TOOL_EDIT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role() + ) + def post(self, request: Request, workspace_id: str): + return result.success(ToolSerializer.TestConnection(data={ + 'workspace_id': workspace_id, + 'code': request.data.get('code'), + }).test_connection()) + class InternalTool(APIView): authentication_classes = [TokenAuth] diff --git a/ui/src/api/system-shared/tool.ts b/ui/src/api/system-shared/tool.ts index 4275b8a1e..a96f9b75a 100644 --- a/ui/src/api/system-shared/tool.ts +++ b/ui/src/api/system-shared/tool.ts @@ -68,6 +68,17 @@ const putTool: (tool_id: string, data: toolData, loading?: Ref) => Prom return put(`${prefix}/${tool_id}`, data, undefined, loading) } +/** + * @param 参数 + */ +const postToolTestConnection: (data: toolData, loading?: Ref) => Promise> = ( + data, + loading, +) => { + return post(`${prefix}/test_connection`, data, undefined, loading) +} + + /** * 获取工具详情 * @param tool_id 工具id @@ -176,5 +187,6 @@ export default { delTool, addInternalTool, addStoreTool, - updateStoreTool + updateStoreTool, + postToolTestConnection } diff --git a/ui/src/api/tool/tool.ts b/ui/src/api/tool/tool.ts index c67cae1de..39df702f7 100644 --- a/ui/src/api/tool/tool.ts +++ b/ui/src/api/tool/tool.ts @@ -75,6 +75,17 @@ const putTool: (tool_id: string, data: toolData, loading?: Ref) => Prom return put(`${prefix.value}/${tool_id}`, data, undefined, loading) } +/** + * @param 参数 + */ +const postToolTestConnection: (data: toolData, loading?: Ref) => Promise> = ( + data, + loading, +) => { + return post(`${prefix.value}/test_connection`, data, undefined, loading) +} + + /** * 获取工具详情 * @param tool_id 工具id @@ -184,5 +195,6 @@ export default { delTool, addInternalTool, addStoreTool, - updateStoreTool + updateStoreTool, + postToolTestConnection } diff --git a/ui/src/views/tool/McpToolFormDrawer.vue b/ui/src/views/tool/McpToolFormDrawer.vue index d6a0ccc41..93513cd29 100644 --- a/ui/src/views/tool/McpToolFormDrawer.vue +++ b/ui/src/views/tool/McpToolFormDrawer.vue @@ -98,6 +98,7 @@