feat: add TestConnection API endpoint and corresponding frontend functionality

v3.2
CaptainB 2025-09-25 12:05:21 +08:00
parent 2735540dd6
commit fc05d26eaf
6 changed files with 86 additions and 12 deletions

View File

@ -5,11 +5,11 @@ import json
import os import os
import pickle import pickle
import re import re
import requests
import tempfile import tempfile
import zipfile import zipfile
from typing import Dict from typing import Dict
import requests
import uuid_utils.compat as uuid import uuid_utils.compat as uuid
from django.core import validators from django.core import validators
from django.db import transaction from django.db import transaction
@ -356,9 +356,6 @@ class ToolSerializer(serializers.Serializer):
ToolCreateRequest(data=instance).is_valid(raise_exception=True) ToolCreateRequest(data=instance).is_valid(raise_exception=True)
# 校验代码是否包括禁止的关键字 # 校验代码是否包括禁止的关键字
ToolExecutor().validate_banned_keywords(instance.get('code', '')) 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_id = uuid.uuid7()
Tool( Tool(
@ -386,6 +383,18 @@ class ToolSerializer(serializers.Serializer):
'id': tool_id, 'workspace_id': self.data.get('workspace_id') 'id': tool_id, 'workspace_id': self.data.get('workspace_id')
}).one() }).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): class Debug(serializers.Serializer):
user_id = serializers.UUIDField(required=True, label=_('user id')) user_id = serializers.UUIDField(required=True, label=_('user id'))
workspace_id = serializers.CharField(required=True, label=_('workspace 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) ToolEditRequest(data=instance).is_valid(raise_exception=True)
# 校验代码是否包括禁止的关键字 # 校验代码是否包括禁止的关键字
ToolExecutor().validate_banned_keywords(instance.get('code', '')) 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(): if not QuerySet(Tool).filter(id=self.data.get('id')).exists():
raise serializers.ValidationError(_('Tool not found')) raise serializers.ValidationError(_('Tool not found'))
@ -755,7 +762,8 @@ class ToolSerializer(serializers.Serializer):
versions = tool.get('versions', []) versions = tool.get('versions', [])
tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else '' tool['label'] = tag_dict[tool.get('tags')[0]] if tool.get('tags') else ''
tool['version'] = next( 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) filter_apps.append(tool)
@ -836,7 +844,8 @@ class ToolSerializer(serializers.Serializer):
raise AppApiException(500, _('Tool does not exist')) raise AppApiException(500, _('Tool does not exist'))
# 查找匹配的版本名称 # 查找匹配的版本名称
version_name = next( 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) res = requests.get(self.data.get('download_url'), timeout=5)
tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool
@ -855,7 +864,6 @@ class ToolSerializer(serializers.Serializer):
return ToolModelSerializer(tool).data return ToolModelSerializer(tool).data
class ToolTreeSerializer(serializers.Serializer): class ToolTreeSerializer(serializers.Serializer):
class Query(serializers.Serializer): class Query(serializers.Serializer):
workspace_id = serializers.CharField(required=True, label=_('workspace id')) workspace_id = serializers.CharField(required=True, label=_('workspace id'))

View File

@ -12,6 +12,7 @@ urlpatterns = [
path('workspace/<str:workspace_id>/tool/pylint', views.ToolView.Pylint.as_view()), path('workspace/<str:workspace_id>/tool/pylint', views.ToolView.Pylint.as_view()),
path('workspace/<str:workspace_id>/tool/debug', views.ToolView.Debug.as_view()), path('workspace/<str:workspace_id>/tool/debug', views.ToolView.Debug.as_view()),
path('workspace/<str:workspace_id>/tool/tool_list', views.ToolView.Query.as_view()), path('workspace/<str:workspace_id>/tool/tool_list', views.ToolView.Query.as_view()),
path('workspace/<str:workspace_id>/tool/test_connection', views.ToolView.TestConnection.as_view()),
path('workspace/<str:workspace_id>/tool/<str:tool_id>', views.ToolView.Operate.as_view()), path('workspace/<str:workspace_id>/tool/<str:tool_id>', views.ToolView.Operate.as_view()),
path('workspace/<str:workspace_id>/tool/<str:tool_id>/edit_icon', views.ToolView.EditIcon.as_view()), path('workspace/<str:workspace_id>/tool/<str:tool_id>/edit_icon', views.ToolView.EditIcon.as_view()),
path('workspace/<str:workspace_id>/tool/<str:tool_id>/export', views.ToolView.Export.as_view()), path('workspace/<str:workspace_id>/tool/<str:tool_id>/export', views.ToolView.Export.as_view()),

View File

@ -360,6 +360,31 @@ class ToolView(APIView):
'image': request.FILES.get('file') 'image': request.FILES.get('file')
}).edit(request.data)) }).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): class InternalTool(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]

View File

@ -68,6 +68,17 @@ const putTool: (tool_id: string, data: toolData, loading?: Ref<boolean>) => Prom
return put(`${prefix}/${tool_id}`, data, undefined, loading) return put(`${prefix}/${tool_id}`, data, undefined, loading)
} }
/**
* @param
*/
const postToolTestConnection: (data: toolData, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading,
) => {
return post(`${prefix}/test_connection`, data, undefined, loading)
}
/** /**
* *
* @param tool_id id * @param tool_id id
@ -176,5 +187,6 @@ export default {
delTool, delTool,
addInternalTool, addInternalTool,
addStoreTool, addStoreTool,
updateStoreTool updateStoreTool,
postToolTestConnection
} }

View File

@ -75,6 +75,17 @@ const putTool: (tool_id: string, data: toolData, loading?: Ref<boolean>) => Prom
return put(`${prefix.value}/${tool_id}`, data, undefined, loading) return put(`${prefix.value}/${tool_id}`, data, undefined, loading)
} }
/**
* @param
*/
const postToolTestConnection: (data: toolData, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading,
) => {
return post(`${prefix.value}/test_connection`, data, undefined, loading)
}
/** /**
* *
* @param tool_id id * @param tool_id id
@ -184,5 +195,6 @@ export default {
delTool, delTool,
addInternalTool, addInternalTool,
addStoreTool, addStoreTool,
updateStoreTool updateStoreTool,
postToolTestConnection
} }

View File

@ -98,6 +98,7 @@
<template #footer> <template #footer>
<div> <div>
<el-button :loading="loading" @click="testConnection">{{ $t('views.system.test') }}</el-button>
<el-button :loading="loading" @click="visible = false">{{ $t('common.cancel') }}</el-button> <el-button :loading="loading" @click="visible = false">{{ $t('common.cancel') }}</el-button>
<el-button <el-button
type="primary" type="primary"
@ -275,6 +276,21 @@ const submit = async (formEl: FormInstance | undefined) => {
}) })
} }
function testConnection() {
if (!form.value.code) {
return
}
loading.value = true
loadSharedApi({ type: 'tool', systemType: apiType.value })
.postToolTestConnection({ code: form.value.code }, loading)
.then(() => {
MsgSuccess(t('views.system.testSuccess'))
})
.finally(() => {
loading.value = false
})
}
const open = (data: any) => { const open = (data: any) => {
if (data) { if (data) {
isEdit.value = data?.id ? true : false isEdit.value = data?.id ? true : false