feat: add MCP tool support with new form and dropdown options
This commit is contained in:
parent
c468952274
commit
f1356e9b61
@ -1,5 +1,5 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
import ast
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -83,6 +83,39 @@ except Exception as e:
|
|||||||
return result.get('data')
|
return result.get('data')
|
||||||
raise Exception(result.get('msg'))
|
raise Exception(result.get('msg'))
|
||||||
|
|
||||||
|
def generate_mcp_server_code(self, _code):
|
||||||
|
self.validate_banned_keywords(_code)
|
||||||
|
|
||||||
|
# 解析代码,提取导入语句和函数定义
|
||||||
|
try:
|
||||||
|
tree = ast.parse(_code)
|
||||||
|
except SyntaxError:
|
||||||
|
return _code
|
||||||
|
|
||||||
|
imports = []
|
||||||
|
functions = []
|
||||||
|
other_code = []
|
||||||
|
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
|
||||||
|
imports.append(ast.unparse(node))
|
||||||
|
elif isinstance(node, ast.FunctionDef):
|
||||||
|
# 为函数添加 @mcp.tool() 装饰器
|
||||||
|
func_code = ast.unparse(node)
|
||||||
|
functions.append(f"@mcp.tool()\n{func_code}\n")
|
||||||
|
else:
|
||||||
|
other_code.append(ast.unparse(node))
|
||||||
|
|
||||||
|
# 构建完整的 MCP 服务器代码
|
||||||
|
code_parts = ["from mcp.server.fastmcp import FastMCP"]
|
||||||
|
code_parts.extend(imports)
|
||||||
|
code_parts.append(f"\nmcp = FastMCP(\"{uuid.uuid7()}\")\n")
|
||||||
|
code_parts.extend(other_code)
|
||||||
|
code_parts.extend(functions)
|
||||||
|
code_parts.append("\nmcp.run(transport=\"stdio\")\n")
|
||||||
|
|
||||||
|
return "\n".join(code_parts)
|
||||||
|
|
||||||
def _exec_sandbox(self, _code, _id):
|
def _exec_sandbox(self, _code, _id):
|
||||||
exec_python_file = f'{self.sandbox_path}/execute/{_id}.py'
|
exec_python_file = f'{self.sandbox_path}/execute/{_id}.py'
|
||||||
with open(exec_python_file, 'w') as file:
|
with open(exec_python_file, 'w') as file:
|
||||||
|
|||||||
18
apps/knowledge/migrations/0002_alter_file_source_type.py
Normal file
18
apps/knowledge/migrations/0002_alter_file_source_type.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-11 09:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('knowledge', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='file',
|
||||||
|
name='source_type',
|
||||||
|
field=models.CharField(choices=[('KNOWLEDGE', 'Knowledge'), ('APPLICATION', 'Application'), ('TOOL', 'Tool'), ('DOCUMENT', 'Document'), ('CHAT', 'Chat'), ('SYSTEM', 'System'), ('TEMPORARY_30_MINUTE', 'Temporary 30 Minute'), ('TEMPORARY_120_MINUTE', 'Temporary 120 Minute'), ('TEMPORARY_1_DAY', 'Temporary 1 Day')], db_index=True, default='TEMPORARY_120_MINUTE', verbose_name='资源类型'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
apps/tools/migrations/0002_alter_tool_tool_type.py
Normal file
18
apps/tools/migrations/0002_alter_tool_tool_type.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-11 09:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tools', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tool',
|
||||||
|
name='tool_type',
|
||||||
|
field=models.CharField(choices=[('INTERNAL', '内置'), ('CUSTOM', '自定义'), ('MCP', 'MCP工具')], db_index=True, default='CUSTOM', max_length=20, verbose_name='工具类型'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -31,6 +31,7 @@ class ToolScope(models.TextChoices):
|
|||||||
class ToolType(models.TextChoices):
|
class ToolType(models.TextChoices):
|
||||||
INTERNAL = "INTERNAL", '内置'
|
INTERNAL = "INTERNAL", '内置'
|
||||||
CUSTOM = "CUSTOM", "自定义"
|
CUSTOM = "CUSTOM", "自定义"
|
||||||
|
MCP = "MCP", "MCP工具"
|
||||||
|
|
||||||
|
|
||||||
class Tool(AppModelMixin):
|
class Tool(AppModelMixin):
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
import uuid_utils.compat as uuid
|
import uuid_utils.compat as uuid
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
@ -12,6 +14,7 @@ from django.db.models import QuerySet, Q
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||||
from pylint.lint import Run
|
from pylint.lint import Run
|
||||||
from pylint.reporters import JSON2Reporter
|
from pylint.reporters import JSON2Reporter
|
||||||
from rest_framework import serializers, status
|
from rest_framework import serializers, status
|
||||||
@ -22,6 +25,7 @@ from common.exception.app_exception import AppApiException
|
|||||||
from common.field.common import UploadedImageField
|
from common.field.common import UploadedImageField
|
||||||
from common.result import result
|
from common.result import result
|
||||||
from common.utils.common import get_file_content
|
from common.utils.common import get_file_content
|
||||||
|
from common.utils.logger import maxkb_logger
|
||||||
from common.utils.rsa_util import rsa_long_decrypt, rsa_long_encrypt
|
from common.utils.rsa_util import rsa_long_decrypt, rsa_long_encrypt
|
||||||
from common.utils.tool_code import ToolExecutor
|
from common.utils.tool_code import ToolExecutor
|
||||||
from knowledge.models import File, FileSourceType
|
from knowledge.models import File, FileSourceType
|
||||||
@ -103,6 +107,18 @@ def encryption(message: str):
|
|||||||
return pre_str + content + end_str
|
return pre_str + content + end_str
|
||||||
|
|
||||||
|
|
||||||
|
def validate_mcp_config(servers: Dict):
|
||||||
|
async def validate():
|
||||||
|
client = MultiServerMCPClient(servers)
|
||||||
|
await client.get_tools()
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(validate())
|
||||||
|
except Exception as e:
|
||||||
|
maxkb_logger.error(f"validate mcp config error: {e}, servers: {servers}")
|
||||||
|
raise serializers.ValidationError(_('MCP configuration is invalid'))
|
||||||
|
|
||||||
|
|
||||||
class ToolModelSerializer(serializers.ModelSerializer):
|
class ToolModelSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tool
|
model = Tool
|
||||||
@ -201,6 +217,131 @@ class PylintInstance(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class ToolSerializer(serializers.Serializer):
|
class ToolSerializer(serializers.Serializer):
|
||||||
|
class Query(serializers.Serializer):
|
||||||
|
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
|
||||||
|
folder_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_('folder id'))
|
||||||
|
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name'))
|
||||||
|
user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id'))
|
||||||
|
scope = serializers.CharField(required=True, label=_('scope'))
|
||||||
|
tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True)
|
||||||
|
create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True)
|
||||||
|
|
||||||
|
def get_query_set(self, workspace_manage, is_x_pack_ee):
|
||||||
|
tool_query_set = QuerySet(Tool).filter(workspace_id=self.data.get('workspace_id'))
|
||||||
|
folder_query_set = QuerySet(ToolFolder)
|
||||||
|
default_query_set = QuerySet(Tool)
|
||||||
|
|
||||||
|
workspace_id = self.data.get('workspace_id')
|
||||||
|
user_id = self.data.get('user_id')
|
||||||
|
scope = self.data.get('scope')
|
||||||
|
tool_type = self.data.get('tool_type')
|
||||||
|
desc = self.data.get('desc')
|
||||||
|
name = self.data.get('name')
|
||||||
|
folder_id = self.data.get('folder_id')
|
||||||
|
create_user = self.data.get('create_user')
|
||||||
|
|
||||||
|
if workspace_id is not None:
|
||||||
|
folder_query_set = folder_query_set.filter(workspace_id=workspace_id)
|
||||||
|
default_query_set = default_query_set.filter(workspace_id=workspace_id)
|
||||||
|
if folder_id is not None:
|
||||||
|
folder_query_set = folder_query_set.filter(parent=folder_id)
|
||||||
|
default_query_set = default_query_set.filter(folder_id=folder_id)
|
||||||
|
if name is not None:
|
||||||
|
folder_query_set = folder_query_set.filter(name__icontains=name)
|
||||||
|
default_query_set = default_query_set.filter(name__icontains=name)
|
||||||
|
if desc is not None:
|
||||||
|
folder_query_set = folder_query_set.filter(desc__icontains=desc)
|
||||||
|
default_query_set = default_query_set.filter(desc__icontains=desc)
|
||||||
|
if create_user is not None:
|
||||||
|
tool_query_set = tool_query_set.filter(user_id=create_user)
|
||||||
|
folder_query_set = folder_query_set.filter(user_id=create_user)
|
||||||
|
|
||||||
|
default_query_set = default_query_set.order_by("-create_time")
|
||||||
|
|
||||||
|
if scope is not None:
|
||||||
|
tool_query_set = tool_query_set.filter(scope=scope)
|
||||||
|
if tool_type:
|
||||||
|
tool_query_set = tool_query_set.filter(tool_type=tool_type)
|
||||||
|
|
||||||
|
query_set_dict = {
|
||||||
|
'folder_query_set': folder_query_set,
|
||||||
|
'tool_query_set': tool_query_set,
|
||||||
|
'default_query_set': default_query_set,
|
||||||
|
}
|
||||||
|
if not workspace_manage:
|
||||||
|
query_set_dict['workspace_user_resource_permission_query_set'] = QuerySet(
|
||||||
|
WorkspaceUserResourcePermission).filter(
|
||||||
|
auth_target_type="TOOL",
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
return query_set_dict
|
||||||
|
|
||||||
|
def get_authorized_query_set(self):
|
||||||
|
default_query_set = QuerySet(Tool)
|
||||||
|
tool_type = self.data.get('tool_type')
|
||||||
|
desc = self.data.get('desc')
|
||||||
|
name = self.data.get('name')
|
||||||
|
create_user = self.data.get('create_user')
|
||||||
|
|
||||||
|
default_query_set = default_query_set.filter(workspace_id='None')
|
||||||
|
default_query_set = default_query_set.filter(scope=ToolScope.SHARED)
|
||||||
|
if name is not None:
|
||||||
|
default_query_set = default_query_set.filter(name__icontains=name)
|
||||||
|
if desc is not None:
|
||||||
|
default_query_set = default_query_set.filter(desc__icontains=desc)
|
||||||
|
if create_user is not None:
|
||||||
|
default_query_set = default_query_set.filter(user_id=create_user)
|
||||||
|
if tool_type:
|
||||||
|
default_query_set = default_query_set.filter(tool_type=tool_type)
|
||||||
|
|
||||||
|
default_query_set = default_query_set.order_by("-create_time")
|
||||||
|
|
||||||
|
return default_query_set
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_x_pack_ee():
|
||||||
|
workspace_user_role_mapping_model = DatabaseModelManage.get_model("workspace_user_role_mapping")
|
||||||
|
role_permission_mapping_model = DatabaseModelManage.get_model("role_permission_mapping_model")
|
||||||
|
return workspace_user_role_mapping_model is not None and role_permission_mapping_model is not None
|
||||||
|
|
||||||
|
def get_tools(self):
|
||||||
|
self.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
workspace_manage = is_workspace_manage(self.data.get('user_id'), self.data.get('workspace_id'))
|
||||||
|
is_x_pack_ee = self.is_x_pack_ee()
|
||||||
|
results = native_search(
|
||||||
|
self.get_query_set(workspace_manage, is_x_pack_ee),
|
||||||
|
get_file_content(
|
||||||
|
os.path.join(
|
||||||
|
PROJECT_DIR,
|
||||||
|
"apps", "tools", 'sql',
|
||||||
|
'list_tool.sql' if workspace_manage else (
|
||||||
|
'list_tool_user_ee.sql' if is_x_pack_ee else 'list_tool_user.sql'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
get_authorized_tool = DatabaseModelManage.get_model("get_authorized_tool")
|
||||||
|
shared_queryset = QuerySet(Tool).none()
|
||||||
|
if get_authorized_tool is not None:
|
||||||
|
shared_queryset = self.get_authorized_query_set()
|
||||||
|
shared_queryset = get_authorized_tool(shared_queryset, self.data.get('workspace_id'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'shared_tools': [
|
||||||
|
ToolModelSerializer(data).data for data in shared_queryset
|
||||||
|
],
|
||||||
|
'tools': [
|
||||||
|
{
|
||||||
|
**tool,
|
||||||
|
'input_field_list': json.loads(tool.get('input_field_list', '[]')),
|
||||||
|
'init_field_list': json.loads(tool.get('init_field_list', '[]')),
|
||||||
|
} for tool in results if tool['resource_type'] == 'tool'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
class Create(serializers.Serializer):
|
class Create(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'))
|
||||||
@ -212,6 +353,10 @@ 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(
|
||||||
id=tool_id,
|
id=tool_id,
|
||||||
@ -223,6 +368,7 @@ class ToolSerializer(serializers.Serializer):
|
|||||||
input_field_list=instance.get('input_field_list', []),
|
input_field_list=instance.get('input_field_list', []),
|
||||||
init_field_list=instance.get('init_field_list', []),
|
init_field_list=instance.get('init_field_list', []),
|
||||||
scope=instance.get('scope', ToolScope.WORKSPACE),
|
scope=instance.get('scope', ToolScope.WORKSPACE),
|
||||||
|
tool_type=instance.get('tool_type', ToolType.CUSTOM),
|
||||||
folder_id=instance.get('folder_id', self.data.get('workspace_id')),
|
folder_id=instance.get('folder_id', self.data.get('workspace_id')),
|
||||||
is_active=False
|
is_active=False
|
||||||
).save()
|
).save()
|
||||||
@ -326,6 +472,10 @@ 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'))
|
||||||
|
|
||||||
@ -574,6 +724,7 @@ class ToolTreeSerializer(serializers.Serializer):
|
|||||||
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name'))
|
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name'))
|
||||||
user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id'))
|
user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id'))
|
||||||
scope = serializers.CharField(required=True, label=_('scope'))
|
scope = serializers.CharField(required=True, label=_('scope'))
|
||||||
|
tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True)
|
||||||
create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True)
|
create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True)
|
||||||
|
|
||||||
def page_tool(self, current_page: int, page_size: int):
|
def page_tool(self, current_page: int, page_size: int):
|
||||||
@ -609,6 +760,7 @@ class ToolTreeSerializer(serializers.Serializer):
|
|||||||
workspace_id = self.data.get('workspace_id')
|
workspace_id = self.data.get('workspace_id')
|
||||||
user_id = self.data.get('user_id')
|
user_id = self.data.get('user_id')
|
||||||
scope = self.data.get('scope')
|
scope = self.data.get('scope')
|
||||||
|
tool_type = self.data.get('tool_type')
|
||||||
desc = self.data.get('desc')
|
desc = self.data.get('desc')
|
||||||
name = self.data.get('name')
|
name = self.data.get('name')
|
||||||
folder_id = self.data.get('folder_id')
|
folder_id = self.data.get('folder_id')
|
||||||
@ -634,6 +786,8 @@ class ToolTreeSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
if scope is not None:
|
if scope is not None:
|
||||||
tool_query_set = tool_query_set.filter(scope=scope)
|
tool_query_set = tool_query_set.filter(scope=scope)
|
||||||
|
if tool_type:
|
||||||
|
tool_query_set = tool_query_set.filter(tool_type=tool_type)
|
||||||
|
|
||||||
query_set_dict = {
|
query_set_dict = {
|
||||||
'folder_query_set': folder_query_set,
|
'folder_query_set': folder_query_set,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
|
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
|
||||||
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/<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()),
|
||||||
|
|||||||
@ -73,6 +73,7 @@ class ToolView(APIView):
|
|||||||
'folder_id': request.query_params.get('folder_id'),
|
'folder_id': request.query_params.get('folder_id'),
|
||||||
'name': request.query_params.get('name'),
|
'name': request.query_params.get('name'),
|
||||||
'scope': request.query_params.get('scope', ToolScope.WORKSPACE),
|
'scope': request.query_params.get('scope', ToolScope.WORKSPACE),
|
||||||
|
'tool_type': request.query_params.get('tool_type'),
|
||||||
'user_id': request.user.id,
|
'user_id': request.user.id,
|
||||||
'create_user': request.query_params.get('create_user'),
|
'create_user': request.query_params.get('create_user'),
|
||||||
}
|
}
|
||||||
@ -209,11 +210,43 @@ class ToolView(APIView):
|
|||||||
'folder_id': request.query_params.get('folder_id'),
|
'folder_id': request.query_params.get('folder_id'),
|
||||||
'name': request.query_params.get('name'),
|
'name': request.query_params.get('name'),
|
||||||
'scope': request.query_params.get('scope'),
|
'scope': request.query_params.get('scope'),
|
||||||
|
'tool_type': request.query_params.get('tool_type'),
|
||||||
'user_id': request.user.id,
|
'user_id': request.user.id,
|
||||||
'create_user': request.query_params.get('create_user'),
|
'create_user': request.query_params.get('create_user'),
|
||||||
}
|
}
|
||||||
).page_tool_with_folders(current_page, page_size))
|
).page_tool_with_folders(current_page, page_size))
|
||||||
|
|
||||||
|
class Query(APIView):
|
||||||
|
authentication_classes = [TokenAuth]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
methods=['GET'],
|
||||||
|
description=_('Get tool list '),
|
||||||
|
summary=_('Get tool list'),
|
||||||
|
operation_id=_('Get tool list'), # type: ignore
|
||||||
|
parameters=ToolReadAPI.get_parameters(),
|
||||||
|
responses=ToolReadAPI.get_response(),
|
||||||
|
tags=[_('Tool')] # type: ignore
|
||||||
|
)
|
||||||
|
@has_permissions(
|
||||||
|
PermissionConstants.TOOL_READ.get_workspace_permission(),
|
||||||
|
PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(),
|
||||||
|
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role()
|
||||||
|
)
|
||||||
|
@log(menu='Tool', operate='Get tool list')
|
||||||
|
def get(self, request: Request, workspace_id: str):
|
||||||
|
return result.success(ToolSerializer.Query(
|
||||||
|
data={
|
||||||
|
'workspace_id': workspace_id,
|
||||||
|
'folder_id': request.query_params.get('folder_id'),
|
||||||
|
'name': request.query_params.get('name'),
|
||||||
|
'scope': request.query_params.get('scope'),
|
||||||
|
'tool_type': request.query_params.get('tool_type'),
|
||||||
|
'user_id': request.user.id,
|
||||||
|
'create_user': request.query_params.get('create_user'),
|
||||||
|
}
|
||||||
|
).get_tools())
|
||||||
|
|
||||||
class Import(APIView):
|
class Import(APIView):
|
||||||
authentication_classes = [TokenAuth]
|
authentication_classes = [TokenAuth]
|
||||||
parser_classes = [MultiPartParser]
|
parser_classes = [MultiPartParser]
|
||||||
|
|||||||
@ -21,6 +21,21 @@ const getToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<
|
|||||||
return get(`${prefix}`, data, loading)
|
return get(`${prefix}`, data, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具列表带分页(无分页)
|
||||||
|
* @params 参数
|
||||||
|
* param {
|
||||||
|
"name": "string",
|
||||||
|
"tool_type": "string",
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const getAllToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
) => {
|
||||||
|
return get(`${prefix}/tool_list`, data, loading)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具列表带分页
|
* 工具列表带分页
|
||||||
* @param 参数
|
* @param 参数
|
||||||
@ -110,6 +125,7 @@ const postPylint: (code: string, loading?: Ref<boolean>) => Promise<Result<any>>
|
|||||||
export default {
|
export default {
|
||||||
getToolListPage,
|
getToolListPage,
|
||||||
getToolList,
|
getToolList,
|
||||||
|
getAllToolList,
|
||||||
putTool,
|
putTool,
|
||||||
getToolById,
|
getToolById,
|
||||||
postToolDebug,
|
postToolDebug,
|
||||||
|
|||||||
@ -17,6 +17,16 @@ const getToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<
|
|||||||
return get(`${prefix}`, data, loading)
|
return get(`${prefix}`, data, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具列表带分页(无分页)
|
||||||
|
*/
|
||||||
|
const getAllToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
) => {
|
||||||
|
return get(`${prefix}/tool_list`, data, loading)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具列表带分页
|
* 工具列表带分页
|
||||||
* @param 参数
|
* @param 参数
|
||||||
@ -135,6 +145,7 @@ const addInternalTool: (
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
getToolList,
|
getToolList,
|
||||||
|
getAllToolList,
|
||||||
getToolListPage,
|
getToolListPage,
|
||||||
putTool,
|
putTool,
|
||||||
getToolById,
|
getToolById,
|
||||||
|
|||||||
@ -24,6 +24,16 @@ const getToolList: (
|
|||||||
return get(`${prefix.value}`, data, loading)
|
return get(`${prefix.value}`, data, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具列表带分页(无分页)
|
||||||
|
*/
|
||||||
|
const getAllToolList: (
|
||||||
|
data?: any,
|
||||||
|
loading?: Ref<boolean>,
|
||||||
|
) => Promise<Result<{ tools: any[]; folders: any[] }>> = (data, loading) => {
|
||||||
|
return get(`${prefix.value}/tool_list`, data, loading)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具列表带分页
|
* 工具列表带分页
|
||||||
* @param 参数
|
* @param 参数
|
||||||
@ -140,6 +150,7 @@ const addInternalTool: (
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
getToolList,
|
getToolList,
|
||||||
|
getAllToolList,
|
||||||
getToolListPage,
|
getToolListPage,
|
||||||
putTool,
|
putTool,
|
||||||
getToolById,
|
getToolById,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ interface toolData {
|
|||||||
init_field_list?: Array<any>
|
init_field_list?: Array<any>
|
||||||
is_active?: boolean
|
is_active?: boolean
|
||||||
folder_id?: string
|
folder_id?: string
|
||||||
|
tool_type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddInternalToolParam {
|
interface AddInternalToolParam {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
title: 'Tool',
|
title: 'Tool',
|
||||||
|
all: 'All',
|
||||||
createTool: 'Create Tool',
|
createTool: 'Create Tool',
|
||||||
editTool: 'Edit Tool',
|
editTool: 'Edit Tool',
|
||||||
copyTool: 'Copy Tool',
|
copyTool: 'Copy Tool',
|
||||||
@ -66,6 +67,11 @@ export default {
|
|||||||
selectPlaceholder: 'Please select parameter',
|
selectPlaceholder: 'Please select parameter',
|
||||||
inputPlaceholder: 'Please enter parameter values',
|
inputPlaceholder: 'Please enter parameter values',
|
||||||
},
|
},
|
||||||
|
mcp: {
|
||||||
|
label: 'MCP Server Config',
|
||||||
|
placeholder: 'Please enter MCP Server config',
|
||||||
|
tip: 'Only supports SSE and Streamable HTTP calling methods',
|
||||||
|
},
|
||||||
debug: {
|
debug: {
|
||||||
run: 'Run',
|
run: 'Run',
|
||||||
output: 'Output',
|
output: 'Output',
|
||||||
|
|||||||
@ -238,6 +238,7 @@ export default {
|
|||||||
mcpServerTip: '请输入JSON格式的MCP服务器配置',
|
mcpServerTip: '请输入JSON格式的MCP服务器配置',
|
||||||
mcpToolTip: '请选择工具',
|
mcpToolTip: '请选择工具',
|
||||||
configLabel: 'MCP Server Config (仅支持SSE/Streamable HTTP调用方式)',
|
configLabel: 'MCP Server Config (仅支持SSE/Streamable HTTP调用方式)',
|
||||||
|
reference: '引用MCP',
|
||||||
},
|
},
|
||||||
imageGenerateNode: {
|
imageGenerateNode: {
|
||||||
label: '图片生成',
|
label: '图片生成',
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
export default {
|
export default {
|
||||||
title: '工具',
|
title: '工具',
|
||||||
|
all: '全部',
|
||||||
createTool: '创建工具',
|
createTool: '创建工具',
|
||||||
editTool: '编辑工具',
|
editTool: '编辑工具',
|
||||||
|
createMcpTool: '创建MCP',
|
||||||
|
editMcpTool: '编辑MCP',
|
||||||
copyTool: '复制工具',
|
copyTool: '复制工具',
|
||||||
importTool: '导入工具',
|
importTool: '导入工具',
|
||||||
toolStore: {
|
toolStore: {
|
||||||
@ -60,6 +63,11 @@ export default {
|
|||||||
selectPlaceholder: '请选择参数',
|
selectPlaceholder: '请选择参数',
|
||||||
inputPlaceholder: '请输入参数值',
|
inputPlaceholder: '请输入参数值',
|
||||||
},
|
},
|
||||||
|
mcp: {
|
||||||
|
label: 'MCP Server Config',
|
||||||
|
placeholder: '请输入MCP Server配置',
|
||||||
|
tip: '仅支持SSE、Streamable HTTP调用方式',
|
||||||
|
},
|
||||||
debug: {
|
debug: {
|
||||||
run: '运行',
|
run: '运行',
|
||||||
output: '输出',
|
output: '输出',
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
title: '工具',
|
title: '工具',
|
||||||
|
all: '全部',
|
||||||
createTool: '建立工具',
|
createTool: '建立工具',
|
||||||
editTool: '編輯工具',
|
editTool: '編輯工具',
|
||||||
copyTool: '複製工具',
|
copyTool: '複製工具',
|
||||||
@ -63,6 +64,11 @@ export default {
|
|||||||
selectPlaceholder: '請选择參數',
|
selectPlaceholder: '請选择參數',
|
||||||
inputPlaceholder: '請輸入參數值',
|
inputPlaceholder: '請輸入參數值',
|
||||||
},
|
},
|
||||||
|
mcp: {
|
||||||
|
label: 'MCP Server Config',
|
||||||
|
placeholder: '請輸入MCP Server配置',
|
||||||
|
tip: '僅支援SSE、Streamable HTTP呼叫方式',
|
||||||
|
},
|
||||||
debug: {
|
debug: {
|
||||||
run: '運行',
|
run: '運行',
|
||||||
output: '輸出',
|
output: '輸出',
|
||||||
|
|||||||
@ -8,11 +8,15 @@ import useFolderStore from './folder'
|
|||||||
const useToolStore = defineStore('tool', {
|
const useToolStore = defineStore('tool', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
toolList: [] as any[],
|
toolList: [] as any[],
|
||||||
|
tool_type: '' as string,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setToolList(list: any[]) {
|
setToolList(list: any[]) {
|
||||||
this.toolList = list
|
this.toolList = list
|
||||||
},
|
},
|
||||||
|
setToolType(type: string) {
|
||||||
|
this.tool_type = type
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -72,9 +72,14 @@
|
|||||||
|
|
||||||
<el-table-column prop="tool_type" :label="$t('views.system.resource_management.type')">
|
<el-table-column prop="tool_type" :label="$t('views.system.resource_management.type')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
|
<span v-if="scope.row.tool_type === 'MCP'">
|
||||||
|
MCP
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
{{
|
{{
|
||||||
$t(ToolType[scope.row.template_id ? 'INTERNAL' : ('CUSTOM' as keyof typeof ToolType)])
|
$t(ToolType[scope.row.template_id ? 'INTERNAL' : ('CUSTOM' as keyof typeof ToolType)])
|
||||||
}}
|
}}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('common.status.label')" width="120">
|
<el-table-column :label="$t('common.status.label')" width="120">
|
||||||
|
|||||||
@ -8,6 +8,13 @@
|
|||||||
<h5 class="ml-4 color-text-primary">{{ t('views.tool.title') }}</h5>
|
<h5 class="ml-4 color-text-primary">{{ t('views.tool.title') }}</h5>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
|
<div class="mt-16 mb-16">
|
||||||
|
<el-radio-group v-model="toolType" @change="radioChange" class="app-radio-button-group">
|
||||||
|
<el-radio-button value="">{{ $t('views.tool.all') }}</el-radio-button>
|
||||||
|
<el-radio-button value="CUSTOM">{{ $t('views.tool.title') }}</el-radio-button>
|
||||||
|
<el-radio-button value="MCP">MCP</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ToolListContainer>
|
</ToolListContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -19,6 +26,16 @@ import { onMounted, ref, reactive, computed } from 'vue'
|
|||||||
import ToolListContainer from '@/views/tool/component/ToolListContainer.vue'
|
import ToolListContainer from '@/views/tool/component/ToolListContainer.vue'
|
||||||
|
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
|
import useStore from "@/stores";
|
||||||
|
|
||||||
|
const { tool } = useStore()
|
||||||
|
|
||||||
|
|
||||||
|
const toolType = ref('')
|
||||||
|
|
||||||
|
function radioChange() {
|
||||||
|
tool.setToolType(toolType.value)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {})
|
onMounted(() => {})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
284
ui/src/views/tool/McpToolFormDrawer.vue
Normal file
284
ui/src/views/tool/McpToolFormDrawer.vue
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="visible" size="60%" :before-close="close">
|
||||||
|
<template #header>
|
||||||
|
<h4>{{ title }}</h4>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<h4 class="title-decoration-1 mb-16">
|
||||||
|
{{ $t('views.model.modelForm.title.baseInfo') }}
|
||||||
|
</h4>
|
||||||
|
<el-form
|
||||||
|
ref="FormRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
require-asterisk-position="right"
|
||||||
|
v-loading="loading"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
|
<el-form-item :label="$t('views.tool.form.toolName.label')" prop="name">
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div
|
||||||
|
v-if="form.id"
|
||||||
|
class="edit-avatar mr-12"
|
||||||
|
@mouseenter="showEditIcon = true"
|
||||||
|
@mouseleave="showEditIcon = false"
|
||||||
|
>
|
||||||
|
<el-Avatar
|
||||||
|
v-if="isAppIcon(form.icon)"
|
||||||
|
:id="form.id"
|
||||||
|
shape="square"
|
||||||
|
:size="32"
|
||||||
|
style="background: none"
|
||||||
|
>
|
||||||
|
<img :src="String(form.icon)" alt=""/>
|
||||||
|
</el-Avatar>
|
||||||
|
<el-avatar v-else class="avatar-green" shape="square" :size="32">
|
||||||
|
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt=""/>
|
||||||
|
</el-avatar>
|
||||||
|
<el-Avatar
|
||||||
|
v-if="showEditIcon"
|
||||||
|
:id="form.id"
|
||||||
|
shape="square"
|
||||||
|
class="edit-mask"
|
||||||
|
:size="32"
|
||||||
|
@click="openEditAvatar"
|
||||||
|
>
|
||||||
|
<AppIcon iconName="app-edit"></AppIcon>
|
||||||
|
</el-Avatar>
|
||||||
|
</div>
|
||||||
|
<el-avatar v-else class="avatar-green mr-12" shape="square" :size="32">
|
||||||
|
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt=""/>
|
||||||
|
</el-avatar>
|
||||||
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
:placeholder="$t('views.tool.form.toolName.placeholder')"
|
||||||
|
maxlength="64"
|
||||||
|
show-word-limit
|
||||||
|
@blur="form.name = form.name?.trim()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('views.tool.form.toolDescription.label')">
|
||||||
|
<el-input
|
||||||
|
v-model="form.desc"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="$t('views.tool.form.toolDescription.placeholder')"
|
||||||
|
maxlength="128"
|
||||||
|
show-word-limit
|
||||||
|
:autosize="{ minRows: 3 }"
|
||||||
|
@blur="form.desc = form.desc?.trim()"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<h4 class="title-decoration-1 mb-16">
|
||||||
|
{{ $t('views.tool.form.mcp.label') }}
|
||||||
|
<span style="color: red; margin-left: -10px">*</span>
|
||||||
|
<el-text type="info" class="color-secondary">
|
||||||
|
{{ $t('views.tool.form.mcp.tip') }}
|
||||||
|
</el-text>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
:placeholder="mcpServerJson"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 5 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button :loading="loading" @click="visible = false">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submit(FormRef)"
|
||||||
|
:loading="loading"
|
||||||
|
v-if="isEdit ? permissionPrecise.edit(form?.id as string) : permissionPrecise.create()"
|
||||||
|
>
|
||||||
|
{{ isEdit ? $t('common.save') : $t('common.create') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshTool"/>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from 'vue'
|
||||||
|
import EditAvatarDialog from '@/views/tool/component/EditAvatarDialog.vue'
|
||||||
|
import type {toolData} from '@/api/type/tool'
|
||||||
|
import type {FormInstance} from 'element-plus'
|
||||||
|
import {MsgConfirm, MsgSuccess} from '@/utils/message'
|
||||||
|
import {cloneDeep} from 'lodash'
|
||||||
|
import {t} from '@/locales'
|
||||||
|
import {isAppIcon} from '@/utils/common'
|
||||||
|
import {useRoute} from 'vue-router'
|
||||||
|
import useStore from '@/stores'
|
||||||
|
import permissionMap from '@/permission'
|
||||||
|
import {loadSharedApi} from '@/utils/dynamics-api/shared-api'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: String,
|
||||||
|
})
|
||||||
|
const {folder, user} = useStore()
|
||||||
|
|
||||||
|
const apiType = computed(() => {
|
||||||
|
if (route.path.includes('shared')) {
|
||||||
|
return 'systemShare'
|
||||||
|
} else if (route.path.includes('resource-management')) {
|
||||||
|
return 'systemManage'
|
||||||
|
} else {
|
||||||
|
return 'workspace'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const permissionPrecise = computed(() => {
|
||||||
|
return permissionMap['tool'][apiType.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
const EditAvatarDialogRef = ref()
|
||||||
|
const mcpServerJson = `{
|
||||||
|
"math": {
|
||||||
|
"url": "your_server",
|
||||||
|
"transport": "sse"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const FormRef = ref()
|
||||||
|
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const visible = ref(false)
|
||||||
|
const showEditor = ref(false)
|
||||||
|
const currentIndex = ref<any>(null)
|
||||||
|
const showEditIcon = ref(false)
|
||||||
|
|
||||||
|
const form = ref<toolData>({
|
||||||
|
name: '',
|
||||||
|
desc: '',
|
||||||
|
code: '',
|
||||||
|
icon: '',
|
||||||
|
input_field_list: [],
|
||||||
|
init_field_list: [],
|
||||||
|
tool_type: 'MCP',
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, (bool) => {
|
||||||
|
if (!bool) {
|
||||||
|
isEdit.value = false
|
||||||
|
showEditor.value = false
|
||||||
|
currentIndex.value = null
|
||||||
|
form.value = {
|
||||||
|
name: '',
|
||||||
|
desc: '',
|
||||||
|
code: '',
|
||||||
|
icon: '',
|
||||||
|
input_field_list: [],
|
||||||
|
init_field_list: [],
|
||||||
|
tool_type: 'MCP',
|
||||||
|
}
|
||||||
|
FormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('views.tool.form.toolName.requiredMessage'),
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (isEdit.value || !areAllValuesNonEmpty(form.value)) {
|
||||||
|
visible.value = false
|
||||||
|
} else {
|
||||||
|
MsgConfirm(t('common.tip'), t('views.tool.tip.saveMessage'), {
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
visible.value = false
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAllValuesNonEmpty(obj: any) {
|
||||||
|
return Object.values(obj).some((value) => {
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value.length !== 0
|
||||||
|
: value !== null && value !== undefined && value !== ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTool(data: any) {
|
||||||
|
form.value.icon = data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function openEditAvatar() {
|
||||||
|
EditAvatarDialogRef.value.open(form.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
await formEl.validate((valid: any) => {
|
||||||
|
if (valid) {
|
||||||
|
if (isEdit.value) {
|
||||||
|
loadSharedApi({type: 'tool', systemType: apiType.value})
|
||||||
|
.putTool(form.value?.id as string, form.value, loading)
|
||||||
|
.then((res: any) => {
|
||||||
|
MsgSuccess(t('common.editSuccess'))
|
||||||
|
emit('refresh', res.data)
|
||||||
|
return user.profile()
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
visible.value = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const obj = {
|
||||||
|
folder_id: folder.currentFolder?.id,
|
||||||
|
...form.value,
|
||||||
|
}
|
||||||
|
loadSharedApi({type: 'tool', systemType: apiType.value})
|
||||||
|
.postTool(obj, loading)
|
||||||
|
.then((res: any) => {
|
||||||
|
MsgSuccess(t('common.createSuccess'))
|
||||||
|
emit('refresh')
|
||||||
|
return user.profile()
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
visible.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = (data: any) => {
|
||||||
|
if (data) {
|
||||||
|
isEdit.value = data?.id ? true : false
|
||||||
|
form.value = cloneDeep(data)
|
||||||
|
}
|
||||||
|
visible.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
showEditor.value = true
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
@ -54,6 +54,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openCreateMcpDialog()">
|
||||||
|
<div class="flex align-center">
|
||||||
|
<el-avatar class="avatar-green" shape="square" :size="32">
|
||||||
|
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt="" />
|
||||||
|
</el-avatar>
|
||||||
|
<div class="pre-wrap ml-8">
|
||||||
|
<div class="lighter">创建MCP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dropdown-item>
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="elUploadRef"
|
ref="elUploadRef"
|
||||||
:file-list="[]"
|
:file-list="[]"
|
||||||
@ -284,6 +294,7 @@
|
|||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
<InitParamDrawer ref="InitParamDrawerRef" @refresh="refresh" />
|
<InitParamDrawer ref="InitParamDrawerRef" @refresh="refresh" />
|
||||||
<ToolFormDrawer ref="ToolFormDrawerRef" @refresh="refresh" :title="ToolDrawertitle" />
|
<ToolFormDrawer ref="ToolFormDrawerRef" @refresh="refresh" :title="ToolDrawertitle" />
|
||||||
|
<McpToolFormDrawer ref="McpToolFormDrawerRef" @refresh="refresh" :title="McpToolDrawertitle" />
|
||||||
<CreateFolderDialog ref="CreateFolderDialogRef" v-if="!isShared" @refresh="refreshFolder" />
|
<CreateFolderDialog ref="CreateFolderDialogRef" v-if="!isShared" @refresh="refreshFolder" />
|
||||||
<ToolStoreDialog ref="toolStoreDialogRef" :api-type="apiType" @refresh="refresh" />
|
<ToolStoreDialog ref="toolStoreDialogRef" :api-type="apiType" @refresh="refresh" />
|
||||||
<AddInternalToolDialog ref="AddInternalToolDialogRef" @refresh="confirmAddInternalTool" />
|
<AddInternalToolDialog ref="AddInternalToolDialogRef" @refresh="confirmAddInternalTool" />
|
||||||
@ -305,6 +316,7 @@ import { cloneDeep } from 'lodash'
|
|||||||
import { useRoute, onBeforeRouteLeave } from 'vue-router'
|
import { useRoute, onBeforeRouteLeave } from 'vue-router'
|
||||||
import InitParamDrawer from '@/views/tool/component/InitParamDrawer.vue'
|
import InitParamDrawer from '@/views/tool/component/InitParamDrawer.vue'
|
||||||
import ToolFormDrawer from '@/views/tool/ToolFormDrawer.vue'
|
import ToolFormDrawer from '@/views/tool/ToolFormDrawer.vue'
|
||||||
|
import McpToolFormDrawer from '@/views/tool/McpToolFormDrawer.vue'
|
||||||
import CreateFolderDialog from '@/components/folder-tree/CreateFolderDialog.vue'
|
import CreateFolderDialog from '@/components/folder-tree/CreateFolderDialog.vue'
|
||||||
import AuthorizedWorkspace from '@/views/system-shared/AuthorizedWorkspaceDialog.vue'
|
import AuthorizedWorkspace from '@/views/system-shared/AuthorizedWorkspaceDialog.vue'
|
||||||
import ToolStoreDialog from '@/views/tool/toolStore/ToolStoreDialog.vue'
|
import ToolStoreDialog from '@/views/tool/toolStore/ToolStoreDialog.vue'
|
||||||
@ -374,7 +386,9 @@ const search_type_change = () => {
|
|||||||
search_form.value = { name: '', create_user: '' }
|
search_form.value = { name: '', create_user: '' }
|
||||||
}
|
}
|
||||||
const ToolFormDrawerRef = ref()
|
const ToolFormDrawerRef = ref()
|
||||||
|
const McpToolFormDrawerRef = ref()
|
||||||
const ToolDrawertitle = ref('')
|
const ToolDrawertitle = ref('')
|
||||||
|
const McpToolDrawertitle = ref('')
|
||||||
|
|
||||||
const MoveToDialogRef = ref()
|
const MoveToDialogRef = ref()
|
||||||
function openMoveToDialog(data: any) {
|
function openMoveToDialog(data: any) {
|
||||||
@ -400,6 +414,11 @@ function openAuthorizedWorkspaceDialog(row: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog(data?: any) {
|
function openCreateDialog(data?: any) {
|
||||||
|
// mcp工具
|
||||||
|
if (data?.tool_type === 'MCP') {
|
||||||
|
openCreateMcpDialog(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
// 有template_id的不允许编辑,是模板转换来的
|
// 有template_id的不允许编辑,是模板转换来的
|
||||||
if (data?.template_id) {
|
if (data?.template_id) {
|
||||||
return
|
return
|
||||||
@ -420,6 +439,27 @@ function openCreateDialog(data?: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCreateMcpDialog(data?: any) {
|
||||||
|
// 有template_id的不允许编辑,是模板转换来的
|
||||||
|
if (data?.template_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 共享过来的工具不让编辑
|
||||||
|
if (isShared.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
McpToolDrawertitle.value = data ? t('views.tool.editMcpTool') : t('views.tool.createMcpTool')
|
||||||
|
if (data) {
|
||||||
|
loadSharedApi({ type: 'tool', systemType: apiType.value })
|
||||||
|
.getToolById(data?.id, loading)
|
||||||
|
.then((res: any) => {
|
||||||
|
McpToolFormDrawerRef.value.open(res.data)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
McpToolFormDrawerRef.value.open(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function changeState(row: any) {
|
async function changeState(row: any) {
|
||||||
if (row.is_active) {
|
if (row.is_active) {
|
||||||
MsgConfirm(
|
MsgConfirm(
|
||||||
@ -616,10 +656,21 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true, immediate: true },
|
{ deep: true, immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tool.tool_type,
|
||||||
|
() => {
|
||||||
|
paginationConfig.current_page = 1
|
||||||
|
tool.setToolList([])
|
||||||
|
getList()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function getList() {
|
function getList() {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
|
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
|
||||||
scope: apiType.value === 'systemShare' ? 'SHARED' : 'WORKSPACE',
|
scope: apiType.value === 'systemShare' ? 'SHARED' : 'WORKSPACE',
|
||||||
|
tool_type: tool.tool_type || '',
|
||||||
}
|
}
|
||||||
if (search_form.value[search_type.value]) {
|
if (search_form.value[search_type.value]) {
|
||||||
params[search_type.value] = search_form.value[search_type.value]
|
params[search_type.value] = search_form.value[search_type.value]
|
||||||
|
|||||||
@ -20,6 +20,13 @@
|
|||||||
{{ $t('views.shared.shared_tool') }}
|
{{ $t('views.shared.shared_tool') }}
|
||||||
</h2>
|
</h2>
|
||||||
<FolderBreadcrumb :folderList="folderList" @click="folderClickHandle" v-else />
|
<FolderBreadcrumb :folderList="folderList" @click="folderClickHandle" v-else />
|
||||||
|
<div class="mt-16 mb-16">
|
||||||
|
<el-radio-group v-model="toolType" @change="radioChange" class="app-radio-button-group">
|
||||||
|
<el-radio-button value="">{{ $t('views.tool.all') }}</el-radio-button>
|
||||||
|
<el-radio-button value="CUSTOM">{{ $t('views.tool.title') }}</el-radio-button>
|
||||||
|
<el-radio-button value="MCP">MCP</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ToolListContainer>
|
</ToolListContainer>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
@ -49,6 +56,7 @@ const permissionPrecise = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const toolType = ref('')
|
||||||
|
|
||||||
const folderList = ref<any[]>([])
|
const folderList = ref<any[]>([])
|
||||||
|
|
||||||
@ -71,6 +79,10 @@ function folderClickHandle(row: any) {
|
|||||||
tool.setToolList([])
|
tool.setToolList([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function radioChange() {
|
||||||
|
tool.setToolType(toolType.value)
|
||||||
|
}
|
||||||
|
|
||||||
function refreshFolder() {
|
function refreshFolder() {
|
||||||
getFolder()
|
getFolder()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,28 @@
|
|||||||
hide-required-asterisk
|
hide-required-asterisk
|
||||||
>
|
>
|
||||||
<el-form-item label="MCP Server Config">
|
<el-form-item label="MCP Server Config">
|
||||||
|
<template #label>
|
||||||
|
<div class="flex-between">
|
||||||
|
<div>
|
||||||
|
MCP Server Config
|
||||||
|
<span class="color-danger">*</span>
|
||||||
|
</div>
|
||||||
|
<el-select
|
||||||
|
:teleported="false"
|
||||||
|
v-model="form_data.mcp_source"
|
||||||
|
size="small"
|
||||||
|
style="width: 85px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
:label="$t('views.applicationWorkflow.nodes.mcpNode.reference')"
|
||||||
|
value="referencing"
|
||||||
|
/>
|
||||||
|
<el-option :label="$t('common.custom')" value="custom" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<MdEditorMagnify
|
<MdEditorMagnify
|
||||||
|
v-if="form_data.mcp_source === 'custom'"
|
||||||
@wheel="wheel"
|
@wheel="wheel"
|
||||||
title="MCP Server Config"
|
title="MCP Server Config"
|
||||||
v-model="form_data.mcp_servers"
|
v-model="form_data.mcp_servers"
|
||||||
@ -20,6 +41,19 @@
|
|||||||
@submitDialog="submitDialog"
|
@submitDialog="submitDialog"
|
||||||
:placeholder="mcpServerJson"
|
:placeholder="mcpServerJson"
|
||||||
/>
|
/>
|
||||||
|
<el-select v-else v-model="form_data.mcp_tool_id" filterable @change="mcpToolSelectChange">
|
||||||
|
<el-option
|
||||||
|
v-for="mcpTool in mcpToolSelectOptions"
|
||||||
|
:key="mcpTool.id"
|
||||||
|
:label="mcpTool.name"
|
||||||
|
:value="mcpTool.id"
|
||||||
|
>
|
||||||
|
<span>{{ mcpTool.name }}</span>
|
||||||
|
<el-tag v-if="mcpTool.scope === 'SHARED'" type="info" class="info-tag ml-8 mt-4">
|
||||||
|
{{ t('views.shared.title') }}
|
||||||
|
</el-tag>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
@ -201,7 +235,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cloneDeep, set } from 'lodash'
|
import { cloneDeep, set } from 'lodash'
|
||||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, inject } from 'vue'
|
||||||
import { isLastNode } from '@/workflow/common/data'
|
import { isLastNode } from '@/workflow/common/data'
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message'
|
import { MsgError, MsgSuccess } from '@/utils/message'
|
||||||
@ -209,12 +243,16 @@ import TooltipLabel from '@/components/dynamics-form/items/label/TooltipLabel.vu
|
|||||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
||||||
|
import useStore from "@/stores";
|
||||||
const props = defineProps<{ nodeModel: any }>()
|
const props = defineProps<{ nodeModel: any }>()
|
||||||
|
const { user } = useStore()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const {
|
const {
|
||||||
params: { id },
|
params: { id },
|
||||||
} = route as any
|
} = route as any
|
||||||
|
const getApplicationDetail = inject('getApplicationDetail') as any
|
||||||
|
const applicationDetail = getApplicationDetail()
|
||||||
|
|
||||||
const apiType = computed(() => {
|
const apiType = computed(() => {
|
||||||
if (route.path.includes('resource-management')) {
|
if (route.path.includes('resource-management')) {
|
||||||
@ -248,17 +286,31 @@ const form = {
|
|||||||
mcp_tools: [],
|
mcp_tools: [],
|
||||||
mcp_servers: '',
|
mcp_servers: '',
|
||||||
mcp_server: '',
|
mcp_server: '',
|
||||||
|
mcp_source: 'referencing',
|
||||||
|
mcp_tool_id: '',
|
||||||
tool_params: {},
|
tool_params: {},
|
||||||
tool_form_field: [],
|
tool_form_field: [],
|
||||||
params_nested: '',
|
params_nested: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mcpToolSelectOptions = ref<any[]>([])
|
||||||
|
|
||||||
function submitDialog(val: string) {
|
function submitDialog(val: string) {
|
||||||
set(props.nodeModel.properties.node_data, 'mcp_servers', val)
|
set(props.nodeModel.properties.node_data, 'mcp_servers', val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function mcpToolSelectChange() {
|
||||||
|
const tool = await loadSharedApi({ type: 'tool', systemType: apiType.value })
|
||||||
|
.getToolById(form_data.value.mcp_tool_id, loading)
|
||||||
|
form_data.value.mcp_servers = tool.data.code
|
||||||
|
}
|
||||||
|
|
||||||
function getTools() {
|
function getTools() {
|
||||||
if (!form_data.value.mcp_servers) {
|
if (form_data.value.mcp_source === 'referencing' && !form_data.value.mcp_tool_id) {
|
||||||
|
MsgError(t('views.applicationWorkflow.nodes.mcpNode.mcpToolTip'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form_data.value.mcp_source === 'custom' && !form_data.value.mcp_servers) {
|
||||||
MsgError(t('views.applicationWorkflow.nodes.mcpNode.mcpServerTip'))
|
MsgError(t('views.applicationWorkflow.nodes.mcpNode.mcpServerTip'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -434,13 +486,34 @@ const validate = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMcpToolSelectOptions() {
|
||||||
|
const obj =
|
||||||
|
apiType.value === 'systemManage'
|
||||||
|
? {
|
||||||
|
scope: 'WORKSPACE',
|
||||||
|
tool_type: 'MCP',
|
||||||
|
workspace_id: applicationDetail.value?.workspace_id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
scope: 'WORKSPACE',
|
||||||
|
tool_type: 'MCP',
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSharedApi({type: 'tool', systemType: apiType.value})
|
||||||
|
.getAllToolList(obj, loading)
|
||||||
|
.then((res: any) => {
|
||||||
|
mcpToolSelectOptions.value = [...res.data.shared_tools, ...res.data.tools]
|
||||||
|
.filter((item: any) => item.is_active)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
||||||
if (isLastNode(props.nodeModel)) {
|
if (isLastNode(props.nodeModel)) {
|
||||||
set(props.nodeModel.properties.node_data, 'is_result', true)
|
set(props.nodeModel.properties.node_data, 'is_result', true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
getMcpToolSelectOptions()
|
||||||
set(props.nodeModel, 'validate', validate)
|
set(props.nodeModel, 'validate', validate)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user