| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653 |
- # -*- coding: utf-8 -*-
- import asyncio
- import base64
- import io
- import json
- import os
- import pickle
- import re
- import tempfile
- import zipfile
- from functools import reduce
- from typing import Dict
- import requests
- import uuid_utils.compat as uuid
- from django.core import validators
- from django.core.cache import cache
- from django.db import transaction
- from django.db.models import QuerySet, Q, Subquery, OuterRef, CharField, Value, When, Case
- from django.http import HttpResponse
- from django.utils import timezone
- from django.utils.translation import gettext_lazy as _
- from langchain_core.messages import HumanMessage, AIMessage
- from langchain_mcp_adapters.client import MultiServerMCPClient
- from pylint.lint import Run
- from pylint.reporters import JSON2Reporter
- from rest_framework import serializers, status
- from application.models import Application
- from common.constants.cache_version import Cache_Version
- from common.database_model_manage.database_model_manage import DatabaseModelManage
- from common.db.search import page_search, native_page_search, native_search
- from common.exception.app_exception import AppApiException
- from common.field.common import UploadedImageField
- from common.result import result
- from common.utils.common import get_file_content, generate_uuid, bytes_to_uploaded_file
- from common.utils.logger import maxkb_logger
- from common.utils.rsa_util import rsa_long_decrypt, rsa_long_encrypt
- from common.utils.tool_code import ToolExecutor
- from knowledge.models import File, FileSourceType, Knowledge
- from maxkb.const import PROJECT_DIR, CONFIG
- from models_provider.models import Model
- from system_manage.models import AuthTargetType, WorkspaceUserResourcePermission
- from system_manage.models.resource_mapping import ResourceMapping
- from system_manage.serializers.resource_mapping_serializers import ResourceMappingSerializer
- from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
- from tools.models import Tool, ToolScope, ToolFolder, ToolType, ToolRecord
- from tools.models.tool_workflow import ToolWorkflow
- from trigger.models import TriggerTask, Trigger
- from users.serializers.user import is_workspace_manage
- tool_executor = ToolExecutor()
- def hand_node(node, update_tool_map):
- if node.get('type') == 'tool-lib-node':
- tool_lib_id = (node.get('properties', {}).get('node_data', {}).get('tool_lib_id') or '')
- node.get('properties', {}).get('node_data', {})['tool_lib_id'] = update_tool_map.get(tool_lib_id, tool_lib_id)
- if node.get('type') == 'tool-workflow-lib-node':
- tool_lib_id = (node.get('properties', {}).get('node_data', {}).get('tool_lib_id') or '')
- node.get('properties', {}).get('node_data', {})['tool_lib_id'] = update_tool_map.get(tool_lib_id, tool_lib_id)
- if node.get('type') == 'search-knowledge-node':
- node.get('properties', {}).get('node_data', {})['knowledge_id_list'] = []
- if node.get('type') == 'ai-chat-node':
- node_data = node.get('properties', {}).get('node_data', {})
- mcp_tool_ids = node_data.get('mcp_tool_ids') or []
- node_data['mcp_tool_ids'] = [update_tool_map.get(tool_id,
- tool_id) for tool_id in mcp_tool_ids]
- tool_ids = node_data.get('tool_ids') or []
- node_data['tool_ids'] = [update_tool_map.get(tool_id,
- tool_id) for tool_id in tool_ids]
- skill_tool_ids = node_data.get('skill_tool_ids') or []
- node_data['skill_tool_ids'] = [update_tool_map.get(tool_id,
- tool_id) for tool_id in skill_tool_ids]
- if node.get('type') == 'mcp-node':
- mcp_tool_id = (node.get('properties', {}).get('node_data', {}).get('mcp_tool_id') or '')
- node.get('properties', {}).get('node_data', {})['mcp_tool_id'] = update_tool_map.get(mcp_tool_id,
- mcp_tool_id)
- class ToolInstance:
- def __init__(self, tool: dict, version: str):
- self.tool = tool
- self.version = version
- ALLOWED_CLASSES = {
- ("builtins", "dict"),
- ('uuid', 'UUID'),
- ("tools.serializers.tool", "ToolInstance")
- }
- class NewUUID:
- def __init__(self):
- self.uuid_dict = {}
- def generate_uuid(self, _id):
- _id = str(_id)
- if _id in self.uuid_dict:
- return self.uuid_dict.get(_id)
- r = str(uuid.uuid7())
- self.uuid_dict[_id] = r
- return r
- def to_dict(message, file_name):
- return {
- 'line': message.line,
- 'column': message.column,
- 'endLine': message.end_line,
- 'endColumn': message.end_column,
- 'message': (message.msg or "").replace(file_name, 'code'),
- 'type': message.category
- }
- def get_file_name():
- file_name = f"{uuid.uuid7()}"
- pylint_dir = os.path.join(PROJECT_DIR, 'data', 'pylint')
- if not os.path.exists(pylint_dir):
- os.makedirs(pylint_dir, 0o700, exist_ok=True)
- os.chmod(os.path.dirname(pylint_dir), 0o700)
- return os.path.join(pylint_dir, file_name)
- class RestrictedUnpickler(pickle.Unpickler):
- def find_class(self, folder, name):
- if (folder, name) in ALLOWED_CLASSES:
- return super().find_class(folder, name)
- raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
- (folder, name))
- def encryption(message: str):
- """
- 加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890
- :param message:
- :return:
- """
- if type(message) != str:
- return message
- if message == "":
- return ""
- max_pre_len = 8
- max_post_len = 4
- message_len = len(message)
- pre_len = int(message_len / 5 * 2)
- post_len = int(message_len / 5 * 1)
- pre_str = "".join([message[index] for index in
- range(0,
- max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int(
- pre_len))])
- end_str = "".join(
- [message[index] for index in
- range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len),
- message_len)])
- content = "***************"
- 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 Meta:
- model = Tool
- fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list', 'init_field_list', 'init_params',
- 'scope', 'is_active', 'user_id', 'template_id', 'workspace_id', 'folder_id', 'tool_type', 'label',
- 'version', 'create_time', 'update_time']
- class ToolRecordModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = ToolRecord
- fields = ['id', 'workspace_id', 'tool_id', 'source_type', 'source_id', 'meta', 'state', 'run_time',
- 'create_time', 'update_time']
- class ToolExportModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = Tool
- fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list', 'init_field_list',
- 'scope', 'is_active', 'user_id', 'template_id', 'workspace_id', 'folder_id', 'tool_type', 'label',
- 'create_time', 'update_time']
- class UploadedFileField(serializers.FileField):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- def to_representation(self, value):
- return value
- class ToolInputField(serializers.Serializer):
- name = serializers.CharField(required=True, label=_('variable name'))
- is_required = serializers.BooleanField(required=True, label=_('required'))
- type = serializers.CharField(required=True, label=_('type'), validators=[
- validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"),
- message=_('fields only support string|int|dict|array|float'), code=500)
- ])
- source = serializers.CharField(required=True, label=_('source'), validators=[
- validators.RegexValidator(regex=re.compile("^custom|reference$"),
- message=_('The field only supports custom|reference'), code=500)
- ])
- class InitField(serializers.Serializer):
- field = serializers.CharField(required=True, label=_('field name'))
- label = serializers.CharField(required=True, label=_('field label'))
- required = serializers.BooleanField(required=True, label=_('required'))
- input_type = serializers.CharField(required=True, label=_('input type'))
- default_value = serializers.CharField(required=False, allow_null=True, allow_blank=True)
- show_default_value = serializers.BooleanField(required=False, default=False)
- props_info = serializers.DictField(required=False, default=dict)
- attrs = serializers.DictField(required=False, default=dict)
- class ToolCreateRequest(serializers.Serializer):
- name = serializers.CharField(required=True, label=_('tool name'))
- desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool description'))
- code = serializers.CharField(required=True, label=_('tool content'))
- input_field_list = serializers.ListField(required=False, default=list, label=_('input field list'))
- init_field_list = serializers.ListField(required=False, default=list, label=_('init field list'))
- is_active = serializers.BooleanField(required=False, label=_('Is active'))
- folder_id = serializers.CharField(required=False, allow_null=True)
- class ToolEditRequest(serializers.Serializer):
- name = serializers.CharField(required=False, label=_('tool name'), allow_null=True)
- desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool description'))
- code = serializers.CharField(required=False, label=_('tool content'), allow_null=True, )
- input_field_list = serializers.ListField(required=False, default=list, allow_null=True, label=_('input field list'))
- init_field_list = serializers.ListField(required=False, default=list, allow_null=True, label=_('init field list'))
- init_params = serializers.DictField(required=False, default=dict, allow_null=True, label=_('init params'))
- is_active = serializers.BooleanField(required=False, label=_('Is active'), allow_null=True, )
- folder_id = serializers.CharField(required=False, allow_null=True)
- class AddInternalToolRequest(serializers.Serializer):
- name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)
- folder_id = serializers.CharField(required=False, allow_null=True, label=_("folder id"))
- class DebugField(serializers.Serializer):
- name = serializers.CharField(required=True, label=_('variable name'))
- value = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_('variable value'))
- class ToolDebugRequest(serializers.Serializer):
- code = serializers.CharField(required=True, label=_('tool content'))
- input_field_list = serializers.ListField(required=False, default=list, label=_('input field list'))
- init_field_list = serializers.ListField(required=False, default=list, label=_('init field list'))
- init_params = serializers.DictField(required=False, default=dict, label=_('init params'))
- debug_field_list = DebugField(required=True, many=True)
- class PylintInstance(serializers.Serializer):
- code = serializers.CharField(required=True, allow_null=True, allow_blank=True, label=_('function content'))
- 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 and folder_id != workspace_id:
- 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 = {
- '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):
- user_id = serializers.UUIDField(required=True, label=_('user id'))
- workspace_id = serializers.CharField(required=True, label=_('workspace id'))
- @transaction.atomic
- def insert(self, instance, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- ToolCreateRequest(data=instance).is_valid(raise_exception=True)
- # 校验代码是否包括禁止的关键字
- if instance.get('tool_type') == ToolType.MCP:
- ToolExecutor().validate_mcp_transport(instance.get('code', ''))
- # 处理 work_flow_template
- if instance.get('work_flow_template') is not None:
- template_instance = instance.get('work_flow_template')
- download_url = template_instance.get('downloadUrl')
- # 查找匹配的版本名称
- res = requests.get(download_url, timeout=5)
- tool = ToolSerializer.Import(data={
- 'file': bytes_to_uploaded_file(res.content, 'file.tool'),
- 'user_id': self.data.get('user_id'),
- 'workspace_id': self.data.get('workspace_id'),
- 'folder_id': str(instance.get('folder_id', self.data.get('workspace_id'))),
- }).import_(name=instance.get('name'), source='template')
- try:
- requests.get(template_instance.get('downloadCallbackUrl'), timeout=5)
- except Exception as e:
- maxkb_logger.error(f"callback appstore tool download error: {e}")
- return tool
- tool_id = uuid.uuid7()
- Tool(
- id=tool_id,
- name=instance.get('name'),
- desc=instance.get('desc'),
- code=instance.get('code'),
- user_id=self.data.get('user_id'),
- workspace_id=self.data.get('workspace_id'),
- input_field_list=instance.get('input_field_list', []),
- init_field_list=instance.get('init_field_list', []),
- 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')),
- is_active=False
- ).save()
- # 自动授权给创建者
- UserResourcePermissionSerializer(data={
- 'workspace_id': self.data.get('workspace_id'),
- 'user_id': self.data.get('user_id'),
- 'auth_target_type': AuthTargetType.TOOL.value
- }).auth_resource(str(tool_id))
- if instance.get('tool_type') == ToolType.WORKFLOW:
- ToolWorkflow(id=uuid.uuid7(), tool_id=tool_id, work_flow=instance.get('work_flow', {})).save()
- # 如果是SKILL类型的工具,修改file表中对应的记录
- if instance.get('tool_type') == ToolType.SKILL:
- file_id = instance.get('code')
- old_file = QuerySet(File).filter(id=file_id).first()
- if old_file:
- # 创建新的文件副本,不复制实际文件内容
- new_file_id = uuid.uuid7()
- new_file = File(
- id=new_file_id,
- file_name=old_file.file_name,
- file_size=old_file.file_size,
- sha256_hash=old_file.sha256_hash,
- source_type=FileSourceType.TOOL,
- source_id=tool_id,
- meta=old_file.meta,
- )
- new_file.save(old_file.get_bytes())
- # 更新工具的code为新的文件id
- QuerySet(Tool).filter(id=tool_id).update(code=str(new_file_id))
- return ToolSerializer.Operate(data={
- '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_mcp_transport(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'))
- def debug(self, debug_instance):
- self.is_valid(raise_exception=True)
- ToolDebugRequest(data=debug_instance).is_valid(raise_exception=True)
- input_field_list = debug_instance.get('input_field_list')
- code = debug_instance.get('code')
- debug_field_list = debug_instance.get('debug_field_list')
- init_params = debug_instance.get('init_params')
- params = {field.get('name'): self.convert_value(field.get('name'), field.get('value'), field.get('type'),
- field.get('is_required'))
- for field in
- [{'value': self.get_field_value(debug_field_list, field.get('name'), field.get('is_required')),
- **field} for field in
- input_field_list]}
- # 合并初始化参数
- if init_params is not None:
- all_params = init_params | params
- else:
- all_params = params
- return tool_executor.exec_code(code, all_params)
- @staticmethod
- def get_field_value(debug_field_list, name, is_required):
- result = [field for field in debug_field_list if field.get('name') == name]
- if len(result) > 0:
- return result[-1].get('value')
- if is_required:
- raise AppApiException(500, f"{name}" + _('field has no value set'))
- return None
- @staticmethod
- def convert_value(name: str, value: str, _type: str, is_required: bool):
- if not is_required and (value is None or (isinstance(value, str) and len(value.strip()) == 0)):
- return None
- try:
- if _type == 'int':
- return int(value)
- if _type == 'boolean':
- value = 0 if ['0', '[]'].__contains__(value) else value
- return bool(value)
- if _type == 'float':
- return float(value)
- if _type == 'dict':
- v = json.loads(value)
- if isinstance(v, dict):
- return v
- raise Exception(_('type error'))
- if _type == 'array':
- v = json.loads(value)
- if isinstance(v, list):
- return v
- raise Exception(_('type error'))
- return value
- except Exception as e:
- raise AppApiException(500, _('Field: {name} Type: {type} Value: {value} Type conversion error').format(
- name=name, type=_type, value=value
- ))
- class Operate(serializers.Serializer):
- id = serializers.UUIDField(required=True, label=_('tool id'))
- workspace_id = serializers.CharField(required=True, label=_('workspace id'))
- def is_one_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Tool).filter(id=self.data.get('id'))
- if workspace_id:
- query_set = query_set.filter(workspace_id=workspace_id)
- if not query_set.exists():
- get_authorized_tool = DatabaseModelManage.get_model('get_authorized_tool')
- if get_authorized_tool:
- if not get_authorized_tool(QuerySet(Tool).filter(id=self.data.get('id')), workspace_id).exists():
- raise AppApiException(500, _('Tool id does not exist'))
- def is_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Tool).filter(id=self.data.get('id'))
- if workspace_id:
- query_set = query_set.filter(workspace_id=workspace_id)
- if not query_set.exists():
- raise AppApiException(500, _('Tool id does not exist'))
- @transaction.atomic
- def edit(self, instance, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- ToolEditRequest(data=instance).is_valid(raise_exception=True)
- # 校验代码是否包括禁止的关键字
- if instance.get('tool_type') == ToolType.MCP:
- ToolExecutor().validate_mcp_transport(instance.get('code', ''))
- if not QuerySet(Tool).filter(id=self.data.get('id')).exists():
- raise serializers.ValidationError(_('Tool not found'))
- edit_field_list = ['name', 'desc', 'code', 'icon', 'input_field_list', 'init_field_list', 'init_params',
- 'is_active', 'folder_id']
- edit_dict = {field: instance.get(field) for field in edit_field_list if (
- field in instance and instance.get(field) is not None)}
- tool = QuerySet(Tool).filter(id=self.data.get('id')).first()
- if 'init_params' in edit_dict:
- if edit_dict['init_field_list'] is not None:
- rm_key = []
- for key in edit_dict['init_params']:
- if key not in [field['field'] for field in edit_dict['init_field_list']]:
- rm_key.append(key)
- for key in rm_key:
- edit_dict['init_params'].pop(key)
- if tool.init_params:
- old_init_params = json.loads(rsa_long_decrypt(tool.init_params))
- for key in edit_dict['init_params']:
- if key in old_init_params and edit_dict['init_params'][key] == encryption(old_init_params[key]):
- edit_dict['init_params'][key] = old_init_params[key]
- edit_dict['init_params'] = rsa_long_encrypt(json.dumps(edit_dict['init_params']))
- edit_dict['update_time'] = timezone.now()
- QuerySet(Tool).filter(id=self.data.get('id')).update(**edit_dict)
- if 'is_active' in instance:
- QuerySet(TriggerTask).filter(source_type="TOOL", source_id=self.data.get('id')).update(
- is_active=instance.get('is_active'))
- # 如果是SKILL类型的工具,修改file表中对应的记录
- if instance.get('tool_type') == ToolType.SKILL:
- old_file_id = tool.code
- file_id = instance.get('code')
- if old_file_id != file_id:
- QuerySet(File).filter(id=old_file_id).delete()
- QuerySet(File).filter(id=file_id).update(source_id=tool.id, source_type=FileSourceType.TOOL)
- return self.one()
- @transaction.atomic
- def delete(self):
- from trigger.handler.simple_tools import deploy
- from trigger.serializers.trigger import TriggerModelSerializer
- self.is_valid(raise_exception=True)
- tool = QuerySet(Tool).filter(id=self.data.get('id')).first()
- if tool.template_id is None and tool.icon != '':
- QuerySet(File).filter(id=tool.icon.split('/')[-1]).delete()
- if tool.tool_type == ToolType.SKILL:
- QuerySet(File).filter(id=tool.code).delete()
- QuerySet(WorkspaceUserResourcePermission).filter(target=tool.id).delete()
- QuerySet(Tool).filter(id=self.data.get('id')).delete()
- ResourceMapping.objects.filter(
- Q(target_id=self.data.get('id')) | Q(source_id=self.data.get('id'))).delete()
- QuerySet(ToolRecord).filter(tool_id=self.data.get('id')).delete()
- trigger_ids = list(
- QuerySet(TriggerTask).filter(
- source_type="TOOL", source_id=self.data.get('id')
- ).values('trigger_id').distinct()
- )
- QuerySet(TriggerTask).filter(source_type="TOOL", source_id=self.data.get('id')).delete()
- for trigger_id in trigger_ids:
- trigger = Trigger.objects.filter(id=trigger_id['trigger_id']).first()
- if trigger and trigger.is_active:
- deploy(TriggerModelSerializer(trigger).data, **{})
- def one(self):
- self.is_one_valid(raise_exception=True)
- tool = QuerySet(Tool).filter(id=self.data.get('id')).select_related('user').first()
- nick_name = tool.user.nick_name if tool and tool.user else None
- if tool.init_params:
- tool.init_params = json.loads(rsa_long_decrypt(tool.init_params))
- if tool.init_field_list:
- password_fields = [i["field"] for i in tool.init_field_list if
- i.get("input_type") == "PasswordInput"]
- if tool.init_params:
- for k in tool.init_params:
- if k in password_fields and tool.init_params[k]:
- tool.init_params[k] = encryption(tool.init_params[k])
- if tool.tool_type == 'SKILL':
- skill_file = QuerySet(File).filter(id=tool.code).first()
- skill_file_dict = {
- 'id': str(skill_file.id),
- 'name': skill_file.file_name,
- 'size': skill_file.file_size,
- } if skill_file else None
- work_flow = {}
- is_publish = False
- if tool.tool_type == 'WORKFLOW':
- tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=tool.id).first()
- if tool_workflow:
- work_flow = tool_workflow.work_flow
- is_publish = tool_workflow.is_publish
- return {
- **ToolModelSerializer(tool).data,
- 'init_params': tool.init_params if tool.init_params else {},
- 'nick_name': nick_name,
- 'fileList': [skill_file_dict] if tool.tool_type == 'SKILL' else [],
- 'work_flow': work_flow,
- 'is_publish': is_publish
- }
- def get_child_tool_list(self, work_flow, response):
- from application.flow.tools import get_tool_id_list
- tool_id_list = get_tool_id_list(work_flow, False)
- tool_id_list = [tool_id for tool_id in tool_id_list if
- len([r for r in response if r.get('id') == tool_id]) == 0]
- tool_list = []
- if len(tool_id_list) > 0:
- tool_list = QuerySet(Tool).filter(id__in=tool_id_list).exclude(scope=ToolScope.SHARED)
- work_flow_tools = [tool for tool in tool_list if tool.tool_type == ToolType.WORKFLOW]
- if len(work_flow_tools) > 0:
- work_flow_tool_dict = {tw.tool_id: tw for tw in
- QuerySet(ToolWorkflow).filter(tool_id__in=[t.id for t in work_flow_tools])}
- for tool in tool_list:
- if tool.tool_type == ToolType.WORKFLOW:
- response.append({**ToolExportModelSerializer(tool).data,
- 'work_flow': work_flow_tool_dict.get(tool.id).work_flow})
- self.get_child_tool_list(work_flow_tool_dict.get(tool.id).work_flow, response)
- else:
- response.append(ToolExportModelSerializer(tool).data)
- else:
- for tool in tool_list:
- response.append(ToolExportModelSerializer(tool).data)
- skill_tools = [tool for tool in tool_list if tool.tool_type == ToolType.SKILL]
- for tool in skill_tools:
- skill_file = QuerySet(File).filter(id=tool.code).first()
- if skill_file:
- tool.code = base64.b64encode(skill_file.get_bytes()).decode('utf-8')
- response.append(ToolExportModelSerializer(tool).data)
- return response
- def export(self):
- try:
- self.is_valid()
- id = self.data.get('id')
- tool = QuerySet(Tool).filter(id=id).first()
- tool_dict = ToolExportModelSerializer(tool).data
- # 如果是SKILL类型的工具,校验文件是否存在
- if tool.tool_type == ToolType.SKILL:
- skill_file = QuerySet(File).filter(id=tool.code).first()
- if skill_file:
- tool_dict['code'] = base64.b64encode(skill_file.get_bytes()).decode('utf-8')
- if tool.tool_type == ToolType.WORKFLOW:
- workflow = QuerySet(ToolWorkflow).filter(tool_id=tool.id).first()
- if workflow:
- tool_dict['work_flow'] = workflow.work_flow
- tool_dict['tool_list'] = self.get_child_tool_list(workflow.work_flow, [])
- mk_instance = ToolInstance(tool_dict, 'v2')
- tool_pickle = pickle.dumps(mk_instance)
- response = HttpResponse(content_type='text/plain', content=tool_pickle)
- response['Content-Disposition'] = f'attachment; filename="{tool.name}.tool"'
- return response
- except Exception as e:
- return result.error(str(e), response_status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- class Pylint(serializers.Serializer):
- def run(self, instance, is_valid=True):
- if is_valid:
- self.is_valid(raise_exception=True)
- PylintInstance(data=instance).is_valid(raise_exception=True)
- code = instance.get('code')
- file_name = get_file_name()
- with open(file_name, 'w') as file:
- file.write(code)
- reporter = JSON2Reporter(output=io.StringIO())
- Run([file_name,
- "--disable=line-too-long",
- '--module-rgx=[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'],
- reporter=reporter, exit=False)
- os.remove(file_name)
- return [to_dict(m, os.path.basename(file_name)) for m in reporter.messages]
- class Import(serializers.Serializer):
- file = UploadedFileField(required=True, label=_("file"))
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- folder_id = serializers.CharField(required=False, allow_null=True, label=_("folder id"))
- @staticmethod
- def to_tool_workflow(work_flow, update_tool_map):
- for node in work_flow.get('nodes', []):
- hand_node(node, update_tool_map)
- if node.get('type') == 'loop-node':
- for n in node.get('properties', {}).get('node_data', {}).get('loop_body', {}).get('nodes', []):
- hand_node(n, update_tool_map)
- return work_flow
- @staticmethod
- def to_tool(tool, workspace_id, user_id, folder_id):
- # 如果是技能类型的工具,需要将code保存为文件
- code = tool.get('code')
- if tool.get('tool_type') == ToolType.SKILL:
- skill_file_id = uuid.uuid7()
- skill_file = File(
- id=skill_file_id,
- file_name=f"{tool.get('name')}.zip",
- source_type=FileSourceType.TOOL,
- source_id=tool.get('id'),
- meta={}
- )
- skill_file.save(base64.b64decode(code))
- tool['code'] = skill_file_id
- return Tool(id=tool.get('id'),
- user_id=user_id,
- name=tool.get('name'),
- code=tool.get('code'),
- template_id=tool.get('template_id'),
- input_field_list=tool.get('input_field_list'),
- init_field_list=tool.get('init_field_list'),
- is_active=False if (len((tool.get('init_field_list') or [])) > 0 or tool.get(
- 'tool_type') == ToolType.WORKFLOW) else tool.get('is_active'),
- tool_type=tool.get('tool_type', 'CUSTOM') or 'CUSTOM',
- scope=ToolScope.SHARED if workspace_id == 'None' else ToolScope.WORKSPACE,
- folder_id=folder_id if folder_id else 'default' if workspace_id == 'None' else workspace_id,
- workspace_id=workspace_id)
- def import_workflow_tools(self, tool, workspace_id, user_id, folder_id, new_child_policy):
- """
- @param tool: 工具对象
- @param workspace_id: 工作空间id
- @param user_id: 用户id
- @param folder_id: 文件夹id
- @param new_child_policy: 子工具创建策略
- 0: 不创建
- 1: 对比创建: 如果存在就不创建 不存在则创建
- 2: 全部创建
- @return:
- """
- if new_child_policy == 0:
- tool_list = []
- else:
- tool_list = tool.get('tool_list') or []
- tool_list = {tool.get('id'): tool for tool in tool_list}.values()
- update_tool_map = {}
- if len(tool_list) > 0:
- new_uuid = NewUUID()
- tool_id_list = reduce(lambda x, y: [*x, *y],
- [[tool.get('id'), new_uuid.generate_uuid(
- tool.get('id')) if new_child_policy == 2 else generate_uuid(
- (tool.get('id') + workspace_id or ''))]
- for tool
- in
- tool_list], [])
- # 存在的工具列表
- exits_tool_id_list = [str(tool.id) for tool in
- QuerySet(Tool).filter(id__in=tool_id_list, workspace_id=workspace_id)]
- # 需要更新的工具集合
- update_tool_map = {tool.get('id'): new_uuid.generate_uuid(
- tool.get('id')) if new_child_policy == 2 else generate_uuid(
- (tool.get('id') + workspace_id or '')) for tool
- in
- tool_list if
- not exits_tool_id_list.__contains__(
- tool.get('id'))}
- tool_list = [{**tool, 'id': update_tool_map.get(tool.get('id'))} for tool in tool_list if
- not exits_tool_id_list.__contains__(
- tool.get('id')) and not exits_tool_id_list.__contains__(
- new_uuid.generate_uuid(
- tool.get('id')) if new_child_policy == 2 else generate_uuid(
- (tool.get('id') + workspace_id or '')))]
- work_flow = self.to_tool_workflow(
- tool.get('work_flow'),
- update_tool_map,
- )
- QuerySet(ToolWorkflow).update_or_create(tool_id=tool.get('id'),
- create_defaults={'id': uuid.uuid7(),
- 'tool_id': tool.get('id'),
- "workspace_id": workspace_id,
- 'work_flow': work_flow, },
- defaults={
- 'tool_id': tool.get('id'),
- 'workspace_id': workspace_id,
- 'work_flow': work_flow
- })
- tool_model_list = [self.to_tool(tool, workspace_id, user_id, folder_id) for tool in tool_list]
- workflow_tool_model_list = [{'tool_id': t.get('id'), 'workflow': self.to_tool_workflow(
- t.get('work_flow'),
- update_tool_map,
- )} for t in tool_list if t.get('tool_type') == ToolType.WORKFLOW]
- existing_records = QuerySet(ToolWorkflow).filter(
- tool_id__in=[wt.get('tool_id') for wt in workflow_tool_model_list],
- workspace_id=workspace_id)
- existing_map = {
- record.tool_id: record
- for record in existing_records
- }
- QuerySet(ToolWorkflow).bulk_create(
- [ToolWorkflow(work_flow=wt.get('workflow'), workspace_id=workspace_id,
- tool_id=wt.get('tool_id')) for wt in
- workflow_tool_model_list if wt.get('tool_id') not in existing_map])
- if len(tool_model_list) > 0:
- QuerySet(Tool).bulk_create(tool_model_list)
- UserResourcePermissionSerializer(data={
- 'workspace_id': self.data.get('workspace_id'),
- 'user_id': self.data.get('user_id'),
- 'auth_target_type': AuthTargetType.TOOL.value
- }).auth_resource_batch([t.id for t in tool_model_list])
- def update_template_workflow(self, tool_id: str):
- self.is_valid(raise_exception=True)
- tool_instance_bytes = self.data.get('file').read()
- try:
- tool_instance = RestrictedUnpickler(io.BytesIO(tool_instance_bytes)).load()
- except Exception as e:
- raise AppApiException(1001, _("Unsupported file format"))
- tool = tool_instance.tool
- tool['id'] = tool_id
- folder_id = self.data.get('folder_id')
- self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'),
- user_id=self.data.get('user_id'),
- folder_id=folder_id, new_child_policy=2)
- return True
- @transaction.atomic
- def import_(self, scope=ToolScope.WORKSPACE, name=None, source=None):
- self.is_valid()
- user_id = self.data.get('user_id')
- tool_instance_bytes = self.data.get('file').read()
- try:
- tool_instance = RestrictedUnpickler(io.BytesIO(tool_instance_bytes)).load()
- except Exception as e:
- raise AppApiException(1001, _("Unsupported file format"))
- if self.data.get('folder_id') is None:
- folder_id = self.data.get('workspace_id')
- else:
- folder_id = self.data.get('folder_id')
- tool = tool_instance.tool
- tool_id = uuid.uuid7()
- code = tool.get('code')
- if tool.get('tool_type') == ToolType.SKILL:
- skill_file_id = uuid.uuid7()
- skill_file = File(
- id=skill_file_id,
- file_name=f"{tool.get('name')}.zip",
- source_type=FileSourceType.TOOL,
- source_id=tool_id,
- meta={}
- )
- skill_file.save(base64.b64decode(code))
- code = skill_file_id
- tool_model = Tool(
- id=tool_id,
- name=name or tool.get('name'),
- desc=tool.get('desc'),
- code=code,
- user_id=user_id,
- workspace_id=self.data.get('workspace_id'),
- input_field_list=tool.get('input_field_list'),
- init_field_list=tool.get('init_field_list', []),
- tool_type=tool.get('tool_type'),
- folder_id=folder_id,
- scope=scope,
- is_active=False
- )
- tool_model.save()
- if tool.get('tool_type') == ToolType.WORKFLOW:
- tool['id'] = tool_id
- self.import_workflow_tools(tool, workspace_id=self.data.get('workspace_id'), user_id=user_id,
- folder_id=folder_id, new_child_policy=2 if source == 'template' else 1)
- # 自动授权给创建者
- UserResourcePermissionSerializer(data={
- 'workspace_id': self.data.get('workspace_id'),
- 'user_id': self.data.get('user_id'),
- 'auth_target_type': AuthTargetType.TOOL.value
- }).auth_resource(str(tool_id))
- return ToolSerializer.Operate(data={
- 'id': tool_id, 'workspace_id': self.data.get('workspace_id')
- }).one()
- class IconOperate(serializers.Serializer):
- id = serializers.UUIDField(required=True, label=_("function ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- image = UploadedImageField(required=True, label=_("picture"))
- def is_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Tool).filter(id=self.data.get('id'))
- if workspace_id:
- query_set = query_set.filter(workspace_id=workspace_id)
- if not query_set.exists():
- raise AppApiException(500, _('Tool id does not exist'))
- def edit(self, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- tool = QuerySet(Tool).filter(id=self.data.get('id')).first()
- if tool is None:
- raise AppApiException(500, _('Function does not exist'))
- # 删除旧的图片
- if tool.icon != '':
- QuerySet(File).filter(id=tool.icon.split('/')[-1]).delete()
- if self.data.get('image') is None:
- tool.icon = ''
- else:
- meta = {
- 'debug': False
- }
- file_id = uuid.uuid7()
- file = File(
- id=file_id,
- file_name=self.data.get('image').name,
- source_type=FileSourceType.TOOL,
- source_id=tool.id,
- meta=meta
- )
- file.save(self.data.get('image').read())
- tool.icon = f'./oss/file/{file_id}'
- tool.save()
- return tool.icon
- class InternalTool(serializers.Serializer):
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)
- def get_internal_tools(self):
- self.is_valid(raise_exception=True)
- query_set = QuerySet(Tool)
- if self.data.get('name', '') != '':
- query_set = query_set.filter(
- Q(name__icontains=self.data.get('name')) |
- Q(desc__icontains=self.data.get('name'))
- )
- query_set = query_set.filter(
- Q(scope=ToolScope.INTERNAL) &
- Q(is_active=True)
- )
- return ToolModelSerializer(query_set, many=True).data
- class AddInternalTool(serializers.Serializer):
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- tool_id = serializers.UUIDField(required=True, label=_("tool id"))
- def add(self, instance, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- AddInternalToolRequest(data=instance).is_valid(raise_exception=True)
- internal_tool = QuerySet(Tool).filter(id=self.data.get('tool_id')).first()
- if internal_tool is None:
- raise AppApiException(500, _('Tool does not exist'))
- tool_id = uuid.uuid7()
- tool = Tool(
- id=tool_id,
- name=instance.get('name', internal_tool.name),
- desc=internal_tool.desc,
- code=internal_tool.code,
- user_id=self.data.get('user_id'),
- icon=internal_tool.icon,
- workspace_id=self.data.get('workspace_id'),
- input_field_list=internal_tool.input_field_list,
- init_field_list=internal_tool.init_field_list,
- scope=ToolScope.WORKSPACE,
- tool_type=ToolType.CUSTOM,
- folder_id=instance.get('folder_id', self.data.get('workspace_id')),
- template_id=internal_tool.id,
- label=internal_tool.label,
- is_active=False
- )
- tool.save()
- # 自动授权给创建者
- UserResourcePermissionSerializer(data={
- 'workspace_id': self.data.get('workspace_id'),
- 'user_id': self.data.get('user_id'),
- 'auth_target_type': AuthTargetType.TOOL.value
- }).auth_resource(str(tool_id))
- return ToolModelSerializer(tool).data
- class StoreTool(serializers.Serializer):
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- name = serializers.CharField(required=False, label=_("tool name"), allow_null=True, allow_blank=True)
- def get_appstore_tools(self):
- self.is_valid(raise_exception=True)
- # 下载zip文件
- try:
- appstore_url = CONFIG.get('APPSTORE_URL', 'https://apps-assets.fit2cloud.com/stable/maxkb.json.zip')
- res = requests.get(appstore_url, timeout=5)
- res.raise_for_status()
- # 创建临时文件保存zip
- with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as temp_zip:
- temp_zip.write(res.content)
- temp_zip_path = temp_zip.name
- try:
- # 解压zip文件
- with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref:
- # 获取zip中的第一个文件(假设只有一个json文件)
- json_filename = zip_ref.namelist()[0]
- json_content = zip_ref.read(json_filename)
- # 将json转换为字典
- tool_store = json.loads(json_content.decode('utf-8'))
- tag_dict = {tag['name']: tag['key'] for tag in tool_store['additionalProperties']['tags']}
- filter_apps = []
- for tool in tool_store['apps']:
- if self.data.get('name', '') != '':
- if self.data.get('name').lower() not in tool.get('name', '').lower():
- continue
- if not tool['downloadUrl'].endswith('.tool'):
- continue
- 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']),
- )
- filter_apps.append(tool)
- tool_store['apps'] = filter_apps
- return tool_store
- finally:
- # 清理临时文件
- os.unlink(temp_zip_path)
- except Exception as e:
- maxkb_logger.error(f"fetch appstore tools error: {e}")
- return {'apps': [], 'additionalProperties': {'tags': []}}
- class AddStoreTool(serializers.Serializer):
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- tool_id = serializers.CharField(required=True, label=_("tool id"))
- def add(self, instance: Dict, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- AddInternalToolRequest(data=instance).is_valid(raise_exception=True)
- versions = instance.get('versions', [])
- download_url = instance.get('download_url')
- # 查找匹配的版本名称
- version_name = next(
- (version.get('name') for version in versions if version.get('downloadUrl') == download_url),
- )
- res = requests.get(download_url, timeout=5)
- tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool
- tool_id = uuid.uuid7()
- # 如果是SKILL类型的工具,保存文件内容到file表,并将code替换为file_id
- if tool_data.get('tool_type') == ToolType.SKILL:
- skill_file_id = uuid.uuid7()
- skill_file = File(
- id=skill_file_id,
- file_name=f"{tool_data.get('name')}.zip",
- source_type=FileSourceType.TOOL,
- source_id=tool_id,
- meta={}
- )
- skill_file.save(base64.b64decode(tool_data.get('code')))
- tool_data['code'] = skill_file_id
- tool = Tool(
- id=tool_id,
- name=instance.get('name'),
- desc=tool_data.get('desc'),
- code=tool_data.get('code'),
- user_id=self.data.get('user_id'),
- icon=instance.get('icon', ''),
- workspace_id=self.data.get('workspace_id'),
- input_field_list=tool_data.get('input_field_list', []),
- init_field_list=tool_data.get('init_field_list', []),
- scope=ToolScope.WORKSPACE,
- tool_type=tool_data.get('tool_type', ToolType.CUSTOM),
- folder_id=instance.get('folder_id', self.data.get('workspace_id')),
- template_id=self.data.get('tool_id'),
- label=instance.get('label'),
- version=version_name,
- is_active=False
- )
- tool.save()
- # 自动授权给创建者
- UserResourcePermissionSerializer(data={
- 'workspace_id': self.data.get('workspace_id'),
- 'user_id': self.data.get('user_id'),
- 'auth_target_type': AuthTargetType.TOOL.value
- }).auth_resource(str(tool_id))
- try:
- requests.get(instance.get('download_callback_url'), timeout=5)
- except Exception as e:
- maxkb_logger.error(f"callback appstore tool download error: {e}")
- return ToolModelSerializer(tool).data
- class UpdateStoreTool(serializers.Serializer):
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- tool_id = serializers.UUIDField(required=True, label=_("tool id"))
- download_url = serializers.CharField(required=True, label=_("download url"))
- download_callback_url = serializers.CharField(required=True, label=_("download callback url"))
- icon = serializers.CharField(required=True, label=_("icon"), allow_null=True, allow_blank=True)
- versions = serializers.ListField(required=True, label=_("versions"), child=serializers.DictField())
- def update_tool(self, with_valid=True):
- if with_valid:
- self.is_valid(raise_exception=True)
- tool = QuerySet(Tool).filter(id=self.data.get('tool_id')).first()
- if tool is None:
- 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')),
- )
- res = requests.get(self.data.get('download_url'), timeout=5)
- tool_data = RestrictedUnpickler(io.BytesIO(res.content)).load().tool
- # 如果是SKILL类型的工具,保存文件内容到file表,并将code替换为file_id
- if tool_data.get('tool_type') == ToolType.SKILL:
- skill_file_id = uuid.uuid7()
- skill_file = File(
- id=skill_file_id,
- file_name=f"{tool_data.get('name')}.zip",
- source_type=FileSourceType.TOOL,
- source_id=tool.id,
- meta={}
- )
- skill_file.save(base64.b64decode(tool_data.get('code')))
- tool_data['code'] = skill_file_id
- tool.desc = tool_data.get('desc')
- tool.code = tool_data.get('code')
- tool.input_field_list = tool_data.get('input_field_list', [])
- tool.init_field_list = tool_data.get('init_field_list', [])
- tool.icon = self.data.get('icon', tool.icon)
- tool.version = version_name
- # tool.is_active = False
- tool.save()
- try:
- requests.get(self.data.get('download_callback_url'), timeout=5)
- except Exception as e:
- maxkb_logger.error(f"callback appstore tool download error: {e}")
- return ToolModelSerializer(tool).data
- class ToolRecord(serializers.Serializer):
- workspace_id = serializers.CharField(required=False, allow_null=True, label=_('workspace id'))
- tool_id = serializers.UUIDField(required=True, label=_('tool id'))
- record_id = serializers.UUIDField(required=False, allow_null=True, label=_('record id'))
- source_name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('source name'))
- source_type = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('source type'))
- state = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('state'))
- class Operate(serializers.Serializer):
- id = serializers.UUIDField(required=False, allow_null=True, label=_('record id'))
- tool_id = serializers.UUIDField(required=True, label=_('tool id'))
- workspace_id = serializers.CharField(required=False, allow_null=True, label=_('workspace id'))
- def one(self):
- self.is_valid(raise_exception=True)
- tool_record = cache.get(Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.data.get('id')),
- version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version())
- if tool_record:
- return tool_record
- tool_record = QuerySet(ToolRecord).filter(id=self.data.get('id'), tool_id=self.data.get('tool_id'),
- workspace_id=self.data.get('workspace_id')).first()
- if tool_record:
- return {'id': tool_record.id,
- 'tool_id': tool_record.tool_id,
- 'workspace_id': tool_record.workspace_id,
- 'source_type': tool_record.source_type,
- 'source_id': tool_record.source_id,
- 'meta': tool_record.meta,
- 'state': tool_record.state,
- 'run_time': tool_record.run_time}
- raise AppApiException(500, _('Tool record does not exist'))
- def one(self):
- self.is_valid(raise_exception=True)
- if self.data.get('record_id'):
- page = self.get_tool_records(1, 1)
- return page.get('records')[0]
- return None
- def get_tool_records(self, current_page: int, page_size: int):
- self.is_valid(raise_exception=True)
- application_subquery = Application.objects.filter(id=OuterRef('source_id')).values('name')[:1]
- knowledge_subquery = Knowledge.objects.filter(id=OuterRef('source_id')).values('name')[:1]
- trigger_subquery = Trigger.objects.filter(id=OuterRef('source_id')).values('name')[:1]
- trigger_type_subquery = Trigger.objects.filter(id=OuterRef('source_id')).values('trigger_type')[:1]
- query_set = QuerySet(ToolRecord)
- query_set = query_set.filter(
- tool_id=self.data.get('tool_id')
- ).annotate(
- source_name=Case(
- When(source_type='APPLICATION', then=Subquery(application_subquery)),
- When(source_type='KNOWLEDGE', then=Subquery(knowledge_subquery)),
- When(source_type='TRIGGER', then=Subquery(trigger_subquery)),
- default=Value(''),
- output_field=CharField()
- )
- ).annotate(
- trigger_type=Case(
- When(source_type='TRIGGER', then=Subquery(trigger_type_subquery)),
- default=Value(''),
- output_field=CharField()
- )
- ).annotate(
- tool_name=Subquery(
- Tool.objects.filter(id=OuterRef('tool_id')).values('name')[:1]
- )
- ).annotate(
- tool_icon=Subquery(
- Tool.objects.filter(id=OuterRef('tool_id')).values('icon')[:1]
- )
- )
- if self.data.get('source_type'):
- query_set = query_set.filter(Q(source_type=self.data.get('source_type', '')))
- if self.data.get('state'):
- query_set = query_set.filter(Q(state=self.data.get('state', '')))
- if self.data.get('source_name'):
- query_set = query_set.filter(Q(tool_name__icontains=self.data.get('source_name', '')))
- if self.data.get('record_id'):
- query_set = query_set.filter(Q(id=self.data.get('record_id')))
- if self.data.get('workspace_id'):
- query_set = query_set.filter(Q(workspace_id=self.data.get('workspace_id')))
- query_set = query_set.order_by('-create_time')
- return page_search(
- current_page, page_size, query_set,
- lambda record: {
- **ToolRecordModelSerializer(record).data,
- 'source_name': record.tool_name,
- 'tool_icon': record.tool_icon,
- 'trigger_type': record.trigger_type,
- }
- )
- class UploadSkillFile(serializers.Serializer):
- file = UploadedFileField(required=True, label=_("file"))
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- def upload(self):
- self.is_valid()
- file = self.data.get('file')
- if not file.name.endswith('.zip'):
- raise AppApiException(1001, _("Unsupported file format"))
- file_id = uuid.uuid7()
- file = File(
- id=file_id,
- file_name=self.data.get('file').name,
- meta={}
- )
- file.save(self.data.get('file').read())
- return file_id
- class DownloadSkillFile(serializers.Serializer):
- user_id = serializers.UUIDField(required=True, label=_("User ID"))
- workspace_id = serializers.CharField(required=True, label=_("workspace id"))
- tool_id = serializers.CharField(required=True, label=_("tool id"))
- def download(self):
- self.is_valid(raise_exception=True)
- tool = QuerySet(Tool).filter(
- id=self.data.get('tool_id'), workspace_id=self.data.get('workspace_id'), tool_type=ToolType.SKILL
- ).first()
- if tool is None:
- raise AppApiException(500, _('Tool does not exist'))
- skill_file = QuerySet(File).filter(id=tool.code).first()
- if skill_file is None:
- raise AppApiException(500, _('Skill file does not exist'))
- response = HttpResponse(content_type='application/zip', content=skill_file.get_bytes())
- response['Content-Disposition'] = f'attachment; filename="{skill_file.file_name}"'
- return response
- class GenerateCodeSerializer(serializers.Serializer):
- workspace_id = serializers.CharField(required=True, label=_('Workspace ID'))
- model_id = serializers.UUIDField(required=True, label=_('Model ID'))
- prompt = serializers.CharField(required=True, label=_('Prompt'))
- messages = serializers.ListField(required=True, label=_('Messages'))
- model_params_setting = serializers.DictField(required=False, default=dict, label=_('Model Params Setting'))
- init_field_list = serializers.ListField(required=False, default=list, label=_('Init Field List'))
- input_field_list = serializers.ListField(required=False, default=list, label=_('Input Field List'))
- def generate_code(self):
- from models_provider.tools import get_model_instance_by_model_workspace_id
- from application.flow.tools import to_stream_response_simple
- self.is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- model_id = self.data.get('model_id')
- prompt = self.data.get('prompt')
- messages = self.data.get('messages')
- model_params_setting = self.data.get('model_params_setting')
- init_field_list = self.data.get('init_field_list')
- input_field_list = self.data.get('input_field_list')
- message = messages[-1]['content']
- q = prompt.replace(
- "{userInput}", message
- ).replace(
- "{initFieldList}", json.dumps(init_field_list)
- ).replace(
- "{inputFieldList}", json.dumps(input_field_list)
- )
- messages[-1]['content'] = q
- SUPPORTED_MODEL_TYPES = ["LLM"]
- model_exist = QuerySet(Model).filter(
- id=model_id,
- model_type__in=SUPPORTED_MODEL_TYPES
- ).exists()
- if not model_exist:
- raise Exception(_("Model does not exists or is not an LLM model"))
- def process():
- model = get_model_instance_by_model_workspace_id(
- model_id=model_id, workspace_id=workspace_id, **model_params_setting
- )
- try:
- for r in model.stream([
- # SystemMessage(content=SYSTEM_ROLE),
- *[
- HumanMessage(
- content=m.get('content')
- ) if m.get('role') == 'user' else AIMessage(
- content=m.get('content')
- ) for m in messages
- ]
- ]):
- yield 'data: ' + json.dumps({'content': r.content}) + '\n\n'
- except Exception as e:
- yield 'data: ' + json.dumps({'error': str(e)}) + '\n\n'
- return to_stream_response_simple(process())
- class ToolBatchOperateSerializer(serializers.Serializer):
- workspace_id = serializers.CharField(required=True, label=_('workspace id'))
- def is_valid(self, *, raise_exception=False):
- super().is_valid(raise_exception=True)
- @transaction.atomic
- def batch_delete(self, instance: Dict, with_valid=True):
- from knowledge.serializers.common import BatchSerializer
- from trigger.handler.simple_tools import deploy
- from trigger.serializers.trigger import TriggerModelSerializer
- if with_valid:
- BatchSerializer(data=instance).is_valid(model=Tool, raise_exception=True)
- self.is_valid(raise_exception=True)
- id_list = instance.get('id_list')
- workspace_id = self.data.get('workspace_id')
- tool_query_set = QuerySet(Tool).filter(id__in=id_list, workspace_id=workspace_id)
- for tool in tool_query_set:
- if tool.template_id is None and tool.icon != '':
- QuerySet(File).filter(id=tool.icon.split('/')[-1]).delete()
- if tool.tool_type == ToolType.SKILL:
- QuerySet(File).filter(id=tool.code).delete()
- QuerySet(WorkspaceUserResourcePermission).filter(target__in=id_list).delete()
- QuerySet(ResourceMapping).filter(Q(target_id__in=id_list) | Q(source_id__in=id_list)).delete()
- QuerySet(ToolRecord).filter(tool_id__in=id_list).delete()
- trigger_ids = list(
- QuerySet(TriggerTask).filter(
- source_type="TOOL", source_id__in=id_list
- ).values('trigger_id').distinct()
- )
- QuerySet(TriggerTask).filter(source_type="TOOL", source_id__in=id_list).delete()
- for trigger_id in trigger_ids:
- trigger = Trigger.objects.filter(id=trigger_id['trigger_id']).first()
- if trigger and trigger.is_active:
- deploy(TriggerModelSerializer(trigger).data, **{})
- tool_query_set.delete()
- return True
- def batch_move(self, instance: Dict, with_valid=True):
- from knowledge.serializers.common import BatchMoveSerializer
- if with_valid:
- BatchMoveSerializer(data=instance).is_valid(model=Tool, raise_exception=True)
- self.is_valid(raise_exception=True)
- id_list = instance.get('id_list')
- folder_id = instance.get('folder_id')
- workspace_id = self.data.get('workspace_id')
- QuerySet(Tool).filter(id__in=id_list, workspace_id=workspace_id).update(folder_id=folder_id)
- return True
- class ToolTreeSerializer(serializers.Serializer):
- class Query(serializers.Serializer):
- workspace_id = serializers.CharField(required=True, label=_('workspace id'))
- folder_id = serializers.CharField(required=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)
- tool_type_list = serializers.ListField(child=serializers.CharField(), required=False, label=_('tool type list'),
- allow_null=True, allow_empty=True)
- create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True)
- def page_tool(self, current_page: int, page_size: int):
- self.is_valid(raise_exception=True)
- folder_id = self.data.get('folder_id', self.data.get('workspace_id'))
- root = ToolFolder.objects.filter(id=folder_id).first()
- if not root:
- raise serializers.ValidationError(_('Folder not found'))
- # 使用MPTT的get_descendants()方法获取所有相关节点
- all_folders = root.get_descendants(include_self=True)
- if self.data.get('name'):
- tools = QuerySet(Tool).filter(
- Q(workspace_id=self.data.get('workspace_id')) &
- Q(folder_id__in=all_folders) &
- Q(user_id=self.data.get('user_id')) &
- Q(name__contains=self.data.get('name'))
- )
- else:
- tools = QuerySet(Tool).filter(
- Q(workspace_id=self.data.get('workspace_id')) &
- Q(folder_id__in=all_folders) &
- Q(user_id=self.data.get('user_id'))
- )
- return page_search(current_page, page_size, tools, lambda record: ToolModelSerializer(record).data)
- 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 and folder_id != workspace_id:
- 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)
- tool_type_list = self.data.get('tool_type_list')
- if tool_type_list:
- tool_query_set = tool_query_set.filter(tool_type__in=tool_type_list)
- elif tool_type:
- tool_query_set = tool_query_set.filter(tool_type=tool_type)
- query_set_dict = {
- '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
- @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 page_tool_with_folders(self, current_page: int, page_size: int):
- 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()
- result = native_page_search(
- current_page, page_size, 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'
- )
- )
- ),
- post_records_handler=lambda record: {
- **record,
- 'input_field_list': json.loads(record.get('input_field_list', '[]')),
- 'init_field_list': json.loads(record.get('init_field_list', '[]')),
- },
- )
- return ResourceMappingSerializer().get_resource_count(result)
- 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'
- )
- )
- ),
- )
- # 返回包含文件夹和工具的结构
- return {
- 'folders': [
- folder for folder in results if folder['resource_type'] == 'folder'
- ],
- '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'
- ],
- }
|