feat: add TestConnection API endpoint and corresponding frontend functionality

This commit is contained in:
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 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'))

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/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/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>/edit_icon', views.ToolView.EditIcon.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')
}).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]

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)
}
/**
* @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
@ -176,5 +187,6 @@ export default {
delTool,
addInternalTool,
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)
}
/**
* @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
@ -184,5 +195,6 @@ export default {
delTool,
addInternalTool,
addStoreTool,
updateStoreTool
updateStoreTool,
postToolTestConnection
}

View File

@ -98,6 +98,7 @@
<template #footer>
<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
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) => {
if (data) {
isEdit.value = data?.id ? true : false