From 2e5232a83ab11d6e82147fff7e5a7faa08140324 Mon Sep 17 00:00:00 2001 From: zhangzhanwei Date: Mon, 30 Mar 2026 13:27:23 +0800 Subject: [PATCH] feat: Batch delete and move tools --- apps/tools/api/tool.py | 23 ++++++++ apps/tools/serializers/tool.py | 59 +++++++++++++++++++++ apps/tools/urls.py | 2 + apps/tools/views/tool.py | 95 ++++++++++++++++++++++++++++++++-- 4 files changed, 175 insertions(+), 4 deletions(-) diff --git a/apps/tools/api/tool.py b/apps/tools/api/tool.py index 2dc718436a0..387c01425b5 100644 --- a/apps/tools/api/tool.py +++ b/apps/tools/api/tool.py @@ -4,6 +4,7 @@ from common.mixins.api_mixin import APIMixin from common.result import ResultSerializer, DefaultResultSerializer +from knowledge.serializers.common import BatchSerializer, BatchMoveSerializer from tools.serializers.tool import ToolModelSerializer, ToolCreateRequest, ToolDebugRequest, ToolEditRequest, \ PylintInstance, AddInternalToolRequest @@ -323,3 +324,25 @@ def get_parameters(): ), ] +class ToolBatchOperateAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ) + ] + + @staticmethod + def get_request(): + return BatchSerializer + + @staticmethod + def get_move_request(): + return BatchMoveSerializer + + diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py index dd24d6cf925..5d071fb373d 100644 --- a/apps/tools/serializers/tool.py +++ b/apps/tools/serializers/tool.py @@ -1173,6 +1173,65 @@ def process(): 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(target_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): diff --git a/apps/tools/urls.py b/apps/tools/urls.py index c8a044cb622..075ed3e7bac 100644 --- a/apps/tools/urls.py +++ b/apps/tools/urls.py @@ -16,6 +16,8 @@ path('workspace//tool/test_connection', views.ToolView.TestConnection.as_view()), path('workspace//tool/upload_skill_file', views.ToolView.UploadSkillFile.as_view()), path('workspace//tool/generate_code', views.ToolView.GenerateCode.as_view()), + path('workspace//tool/batch_delete', views.ToolView.BatchDelete.as_view()), + path('workspace//tool/batch_move', views.ToolView.BatchMove.as_view()), path('workspace//tool/', views.ToolView.Operate.as_view()), path('workspace//tool//publish', views.ToolWorkflowView.Publish.as_view()), path('workspace//tool//debug', views.ToolWorkflowDebugView.as_view()), diff --git a/apps/tools/views/tool.py b/apps/tools/views/tool.py index 4dc35a553f4..dbc33b0cb9f 100644 --- a/apps/tools/views/tool.py +++ b/apps/tools/views/tool.py @@ -6,14 +6,15 @@ from rest_framework.views import APIView from common.auth import TokenAuth -from common.auth.authentication import has_permissions +from common.auth.authentication import has_permissions, check_batch_permissions from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants from common.log.log import log -from common.result import result +from common import result from tools.api.tool import ToolCreateAPI, ToolEditAPI, ToolReadAPI, ToolDeleteAPI, ToolTreeReadAPI, ToolDebugApi, \ - ToolExportAPI, ToolImportAPI, ToolPageAPI, PylintAPI, EditIconAPI, GetInternalToolAPI, AddInternalToolAPI + ToolExportAPI, ToolImportAPI, ToolPageAPI, PylintAPI, EditIconAPI, GetInternalToolAPI, AddInternalToolAPI, \ + ToolBatchOperateAPI from tools.models import ToolScope, Tool -from tools.serializers.tool import ToolSerializer, ToolTreeSerializer +from tools.serializers.tool import ToolSerializer, ToolTreeSerializer, ToolBatchOperateSerializer def get_tool_operation_object(tool_id): @@ -24,6 +25,15 @@ def get_tool_operation_object(tool_id): } return {} +def get_tool_operation_object_batch(tool_id_list): + tool_model_list = QuerySet(model=Tool).filter(id__in=tool_id_list) + if tool_model_list is not None: + return { + "name": f'[{",".join([t.name for t in tool_model_list])}]', + 'tool_list': [{'name': t.name} for t in tool_model_list] + } + return {} + class ToolView(APIView): authentication_classes = [TokenAuth] @@ -186,6 +196,83 @@ def delete(self, request: Request, workspace_id: str, tool_id: str): data={'id': tool_id, 'workspace_id': workspace_id} ).delete()) + class BatchDelete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_("Batch delete tools"), + summary=_("Batch delete tools"), + operation_id=_("Batch delete tools"), + parameters=ToolBatchOperateAPI.get_parameters(), + request=ToolBatchOperateAPI.get_request(), + responses=result.DefaultResultSerializer, + tags=[_('Tool')] + ) + @has_permissions(PermissionConstants.TOOL_BATCH_DELETE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role() + ) + def put(self, request: Request, workspace_id: str): + id_list = request.data.get('id_list', []) + permitted_ids = check_batch_permissions( + request, id_list, 'tool_id', + (PermissionConstants.TOOL_DELETE.get_workspace_tool_permission(), + PermissionConstants.TOOL_DELETE.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_tool_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), workspace_id=workspace_id + ) + + @log(menu='Tool', operate='Batch delete tools', + get_operation_object=lambda r, k: get_tool_operation_object_batch(permitted_ids)) + def inner(view, r, **kwargs): + return ToolBatchOperateSerializer( + data={'workspace_id': workspace_id} + ).batch_delete({'id_list': permitted_ids}) + + return result.success(inner(self, request, workspace_id=workspace_id)) + + class BatchMove(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_("Batch move tools"), + summary=_("Batch move tools"), + operation_id=_("Batch move tools"), + parameters=ToolBatchOperateAPI.get_parameters(), + request=ToolBatchOperateAPI.get_move_request(), + responses=result.DefaultResultSerializer, + tags=[_('Tool')] + ) + @has_permissions(PermissionConstants.TOOL_BATCH_MOVE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role() + ) + def put(self, request: Request, workspace_id: str): + id_list = request.data.get('id_list', []) + permitted_ids = check_batch_permissions( + request, id_list, 'tool_id', + (PermissionConstants.TOOL_EDIT.get_workspace_tool_permission(), + PermissionConstants.TOOL_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_tool_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), + workspace_id=workspace_id + ) + + @log(menu='Tool', operate='Batch move tools', + get_operation_object=lambda r, k: get_tool_operation_object_batch(permitted_ids)) + def inner(view, r, **kwargs): + return ToolBatchOperateSerializer( + data={'workspace_id': workspace_id} + ).batch_move({'id_list': permitted_ids, 'folder_id': request.data.get('folder_id')}) + + return result.success(inner(self, request, workspace_id=workspace_id)) + class Page(APIView): authentication_classes = [TokenAuth]