diff --git a/apps/application/serializers/application.py b/apps/application/serializers/application.py index c51437d2c02..b19a69969b7 100644 --- a/apps/application/serializers/application.py +++ b/apps/application/serializers/application.py @@ -719,7 +719,6 @@ class ApplicationImportRequest(serializers.Serializer): class ApplicationEditSerializer(serializers.Serializer): name = serializers.CharField(required=False, max_length=64, min_length=1, label=_("Application Name")) - is_portal = serializers.BooleanField(required=False, label=_("Whether to publish on portal")) desc = serializers.CharField( required=False, max_length=256, @@ -1349,6 +1348,38 @@ def publish(self, instance, with_valid=True): ) return self.one(with_valid=False) + @transaction.atomic + def portal_up(self, with_valid=True): + """上架到门户:仅已发布的应用可上架,调用方需具备门户上架权限。""" + if with_valid: + self.is_valid() + workspace_id = self.data.get("workspace_id") + application = ( + QuerySet(Application).filter(id=self.data.get("application_id"), workspace_id=workspace_id).first() + ) + if not application: + raise AppApiException(500, _("Application id does not exist")) + if not application.is_publish: + raise AppApiException(500, _("Only published applications can be published to Portal")) + application.is_portal = True + application.save() + return self.one(with_valid=False) + + @transaction.atomic + def portal_down(self, with_valid=True): + """从门户下架,调用方需具备门户下架权限。""" + if with_valid: + self.is_valid() + workspace_id = self.data.get("workspace_id") + application = ( + QuerySet(Application).filter(id=self.data.get("application_id"), workspace_id=workspace_id).first() + ) + if not application: + raise AppApiException(500, _("Application id does not exist")) + application.is_portal = False + application.save() + return self.one(with_valid=False) + @staticmethod def update_work_flow_model(instance): if "nodes" not in instance.get("work_flow"): @@ -1465,7 +1496,6 @@ def edit(self, instance: Dict, with_valid=True): ToolExecutor().validate_mcp_transport(json.dumps(instance.get("mcp_servers"))) update_keys = [ "name", - "is_portal", "desc", "model_id", "multiple_rounds_dialogue", diff --git a/apps/application/urls.py b/apps/application/urls.py index e6776f161c3..c154c27dee3 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -15,6 +15,8 @@ path('workspace//application/batch_clean_time', views.ApplicationAPI.BatchCleanTime.as_view()), path('workspace//application/', views.ApplicationAPI.Operate.as_view()), path('workspace//application//publish', views.ApplicationAPI.Publish.as_view()), + path('workspace//application//portal_up', views.ApplicationAPI.PortalUp.as_view()), + path('workspace//application//portal_down', views.ApplicationAPI.PortalDown.as_view()), path('workspace//application//move/', views.ApplicationAPI.Move.as_view()), path('workspace//application//application_key', views.ApplicationKey.as_view()), path('workspace//application//application_stats', views.ApplicationStats.as_view()), diff --git a/apps/application/views/application.py b/apps/application/views/application.py index 6bb977ab75c..a516e0f1e63 100644 --- a/apps/application/views/application.py +++ b/apps/application/views/application.py @@ -1,11 +1,12 @@ # coding=utf-8 """ - @project: MaxKB - @Author:虎虎 - @file: application.py - @date:2025/5/26 16:51 - @desc: +@project: MaxKB +@Author:虎虎 +@file: application.py +@date:2025/5/26 16:51 +@desc: """ + from django.db.models import QuerySet from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ @@ -14,12 +15,25 @@ from rest_framework.request import Request from rest_framework.views import APIView -from application.api.application_api import ApplicationCreateAPI, ApplicationQueryAPI, ApplicationImportAPI, \ - ApplicationExportAPI, ApplicationOperateAPI, ApplicationEditAPI, TextToSpeechAPI, SpeechToTextAPI, PlayDemoTextAPI, \ - ApplicationBatchOperateAPI +from application.api.application_api import ( + ApplicationCreateAPI, + ApplicationQueryAPI, + ApplicationImportAPI, + ApplicationExportAPI, + ApplicationOperateAPI, + ApplicationEditAPI, + TextToSpeechAPI, + SpeechToTextAPI, + PlayDemoTextAPI, + ApplicationBatchOperateAPI, +) from application.models import Application -from application.serializers.application import ApplicationSerializer, Query, ApplicationOperateSerializer, \ - ApplicationBatchOperateSerializer +from application.serializers.application import ( + ApplicationSerializer, + Query, + ApplicationOperateSerializer, + ApplicationBatchOperateSerializer, +) from common import result from common.auth import TokenAuth from common.auth.authentication import has_permissions, get_is_permissions, check_batch_permissions @@ -34,9 +48,7 @@ def get_application_operation_object(application_id): application_model = QuerySet(model=Application).filter(id=application_id).first() if application_model is not None: - return { - 'name': application_model.name - } + return {"name": application_model.name} return {} @@ -44,8 +56,8 @@ def get_application_operation_object_batch(application_id_list): application_model_list = QuerySet(model=Application).filter(id__in=application_id_list) if application_model_list is not None: return { - "name": f'[{",".join([app.name for app in application_model_list])}]', - 'application_list': [{'name': app.name} for app in application_model_list] + "name": f"[{','.join([app.name for app in application_model_list])}]", + "application_list": [{"name": app.name} for app in application_model_list], } return {} @@ -54,298 +66,473 @@ class ApplicationAPI(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['POST'], - description=_('Create an application'), - summary=_('Create an application'), - operation_id=_('Create an application'), # type: ignore + methods=["POST"], + description=_("Create an application"), + summary=_("Create an application"), + operation_id=_("Create an application"), # type: ignore parameters=ApplicationCreateAPI.get_parameters(), request=ApplicationCreateAPI.get_request(), responses=ApplicationCreateAPI.get_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_CREATE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Create an application", + get_operation_object=lambda r, k: {"name": r.data.get("name")}, ) - @has_permissions(PermissionConstants.APPLICATION_CREATE.get_workspace_permission(), - RoleConstants.USER.get_workspace_role(), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate='Create an application', - get_operation_object=lambda r, k: {'name': r.data.get('name')}, - ) def post(self, request: Request, workspace_id: str): return result.success( - ApplicationSerializer(data={'workspace_id': workspace_id, 'user_id': request.user.id}).insert(request.data)) + ApplicationSerializer(data={"workspace_id": workspace_id, "user_id": request.user.id}).insert(request.data) + ) @extend_schema( - methods=['GET'], - description=_('Get the application list'), - summary=_('Get the application list'), - operation_id=_('Get the application list'), # type: ignore + methods=["GET"], + description=_("Get the application list"), + summary=_("Get the application list"), + operation_id=_("Get the application list"), # type: ignore parameters=ApplicationQueryAPI.get_parameters(), responses=ApplicationQueryAPI.get_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_READ.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_permission(), - RoleConstants.USER.get_workspace_role(), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def get(self, request: Request, workspace_id: str): return result.success( - Query(data={'workspace_id': workspace_id, 'user_id': request.user.id}).list(request.query_params)) + Query(data={"workspace_id": workspace_id, "user_id": request.user.id}).list(request.query_params) + ) class Page(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['GET'], - description=_('Get the application list by page'), - summary=_('Get the application list by page'), - operation_id=_('Get the application list by page'), # type: ignore + methods=["GET"], + description=_("Get the application list by page"), + summary=_("Get the application list by page"), + operation_id=_("Get the application list by page"), # type: ignore parameters=ApplicationQueryAPI.get_parameters(), responses=ApplicationQueryAPI.get_page_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_READ.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_permission(), - RoleConstants.USER.get_workspace_role(), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def get(self, request: Request, workspace_id: str, current_page: int, page_size: int): return result.success( - Query(data={'workspace_id': workspace_id, 'user_id': request.user.id}).page(current_page, page_size, - request.query_params)) + Query(data={"workspace_id": workspace_id, "user_id": request.user.id}).page( + current_page, page_size, request.query_params + ) + ) class Import(APIView): authentication_classes = [TokenAuth] parser_classes = [MultiPartParser] @extend_schema( - methods=['POST'], - description=_('Import Application'), - summary=_('Import Application'), - operation_id=_('Import Application'), # type: ignore + methods=["POST"], + description=_("Import Application"), + summary=_("Import Application"), + operation_id=_("Import Application"), # type: ignore parameters=ApplicationImportAPI.get_parameters(), request=ApplicationImportAPI.get_request(), responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_IMPORT.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Import Application", ) - @has_permissions(PermissionConstants.APPLICATION_IMPORT.get_workspace_permission(), - RoleConstants.USER.get_workspace_role(), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate="Import Application", ) def post(self, request: Request, workspace_id: str, folder_id: str): is_import_tool = get_is_permissions(request, workspace_id=workspace_id, folder_id=folder_id)( PermissionConstants.TOOL_IMPORT.get_workspace_permission(), PermissionConstants.TOOL_IMPORT.get_workspace_permission_workspace_manage_role(), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role() + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + RoleConstants.USER.get_workspace_role(), + ) + return result.success( + ApplicationSerializer( + data={ + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).import_({"file": request.FILES.get("file"), "folder_id": folder_id}, is_import_tool) ) - return result.success(ApplicationSerializer( - data={'user_id': request.user.id, 'workspace_id': workspace_id, - }).import_({'file': request.FILES.get('file'), 'folder_id': folder_id}, is_import_tool)) class Export(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['POST'], - description=_('Export application'), - summary=_('Export application'), - operation_id=_('Export application'), # type: ignore + methods=["POST"], + description=_("Export application"), + summary=_("Export application"), + operation_id=_("Export application"), # type: ignore parameters=ApplicationExportAPI.get_parameters(), request=None, responses=ApplicationExportAPI.get_response(), - tags=[_('Application')] # type: ignore - ) - @has_permissions(PermissionConstants.APPLICATION_EXPORT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EXPORT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate="Export Application", - get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')), - ) + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_EXPORT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EXPORT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Export Application", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) def get(self, request: Request, workspace_id: str, application_id: str): return ApplicationOperateSerializer( - data={'application_id': application_id, - 'workspace_id': workspace_id, - 'user_id': request.user.id}).export() + data={"application_id": application_id, "workspace_id": workspace_id, "user_id": request.user.id} + ).export() class Operate(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['DELETE'], - description=_('Deleting application'), - summary=_('Deleting application'), - operation_id=_('Deleting application'), # type: ignore + methods=["DELETE"], + description=_("Deleting application"), + summary=_("Deleting application"), + operation_id=_("Deleting application"), # type: ignore parameters=ApplicationOperateAPI.get_parameters(), responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore - ) - @has_permissions(PermissionConstants.APPLICATION_DELETE.get_workspace_application_permission(), - PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate='Deleting application', - get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')), - - ) + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_DELETE.get_workspace_application_permission(), + PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Deleting application", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) def delete(self, request: Request, workspace_id: str, application_id: str): - return result.success(ApplicationOperateSerializer( - data={'application_id': application_id, 'user_id': request.user.id, - 'workspace_id': workspace_id, }).delete( - with_valid=True)) + return result.success( + ApplicationOperateSerializer( + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).delete(with_valid=True) + ) @extend_schema( - methods=['PUT'], - description=_('Modify the application'), - summary=_('Modify the application'), - operation_id=_('Modify the application'), # type: ignore + methods=["PUT"], + description=_("Modify the application"), + summary=_("Modify the application"), + operation_id=_("Modify the application"), # type: ignore parameters=ApplicationOperateAPI.get_parameters(), request=ApplicationEditAPI.get_request(), responses=ApplicationCreateAPI.get_response(), - tags=[_('Application')] # type: ignore - ) - @has_permissions(PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate="Modify the application", - get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')), - ) + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Modify the application", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) def put(self, request: Request, workspace_id: str, application_id: str): return result.success( ApplicationOperateSerializer( - data={'application_id': application_id, 'user_id': request.user.id, - 'workspace_id': workspace_id, }).edit( - request.data)) + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).edit(request.data) + ) @extend_schema( - methods=['GET'], - description=_('Get application details'), - summary=_('Get application details'), - operation_id=_('Get application details'), # type: ignore + methods=["GET"], + description=_("Get application details"), + summary=_("Get application details"), + operation_id=_("Get application details"), # type: ignore parameters=ApplicationOperateAPI.get_parameters(), request=ApplicationEditAPI.get_request(), responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore - ) - @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_application_permission(), - PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_READ.get_workspace_application_permission(), + PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) def get(self, request: Request, workspace_id: str, application_id: str): - return result.success(ApplicationOperateSerializer( - data={'application_id': application_id, 'user_id': request.user.id, - 'workspace_id': workspace_id, }).one()) + return result.success( + ApplicationOperateSerializer( + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).one() + ) class Move(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['PUT'], + methods=["PUT"], description=_("Move an application"), summary=_("Move an application"), operation_id=_("Move an application"), # type: ignore parameters=ApplicationOperateAPI.get_parameters(), request=None, responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore - ) - @has_permissions(PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate='Move an application', - get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id'))) + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Move an application", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) def put(self, request: Request, workspace_id: str, application_id: str, folder_id: str): return result.success( ApplicationOperateSerializer( - data={'application_id': application_id, 'user_id': request.user.id, - 'workspace_id': workspace_id, }).move(folder_id)) + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).move(folder_id) + ) class Publish(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['PUT'], + methods=["PUT"], description=_("Publishing an application"), summary=_("Publishing an application"), operation_id=_("Publishing an application"), # type: ignore parameters=ApplicationOperateAPI.get_parameters(), request=None, responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore - ) - @has_permissions(PermissionConstants.APPLICATION_PUBLISH.get_workspace_application_permission(), - PermissionConstants.APPLICATION_PUBLISH.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate='Publishing an application', - get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id'))) + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_PUBLISH.get_workspace_application_permission(), + PermissionConstants.APPLICATION_PUBLISH.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Publishing an application", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) def put(self, request: Request, workspace_id: str, application_id: str): return result.success( ApplicationOperateSerializer( - data={'application_id': application_id, 'user_id': request.user.id, - 'workspace_id': workspace_id, }).publish(request.data)) + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).publish(request.data) + ) + + class PortalUp(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["PUT"], + description=_("Publishing an application to Portal"), + summary=_("Publishing an application to Portal"), + operation_id=_("Publishing an application to Portal"), # type: ignore + parameters=ApplicationOperateAPI.get_parameters(), + request=None, + responses=result.DefaultResultSerializer, + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_PORTAL_UP.get_workspace_application_permission(), + PermissionConstants.APPLICATION_PORTAL_UP.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Publishing an application to Portal", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) + def put(self, request: Request, workspace_id: str, application_id: str): + return result.success( + ApplicationOperateSerializer( + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).portal_up() + ) + + class PortalDown(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=["PUT"], + description=_("Unpublishing an application from Portal"), + summary=_("Unpublishing an application from Portal"), + operation_id=_("Unpublishing an application from Portal"), # type: ignore + parameters=ApplicationOperateAPI.get_parameters(), + request=None, + responses=result.DefaultResultSerializer, + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_PORTAL_DOWN.get_workspace_application_permission(), + PermissionConstants.APPLICATION_PORTAL_DOWN.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="Unpublishing an application from Portal", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), + ) + def put(self, request: Request, workspace_id: str, application_id: str): + return result.success( + ApplicationOperateSerializer( + data={ + "application_id": application_id, + "user_id": request.user.id, + "workspace_id": workspace_id, + } + ).portal_down() + ) class StoreApplication(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['GET'], + methods=["GET"], description=_("Get Appstore apps"), summary=_("Get Appstore apps"), operation_id=_("Get Appstore apps"), # type: ignore responses=GetInternalToolAPI.get_response(), - tags=[_("Application")] # type: ignore + tags=[_("Application")], # type: ignore ) def get(self, request: Request): - return result.success(ApplicationSerializer.StoreApplication(data={ - 'user_id': request.user.id, - 'name': request.query_params.get('name', ''), - }).get_appstore_templates()) + return result.success( + ApplicationSerializer.StoreApplication( + data={ + "user_id": request.user.id, + "name": request.query_params.get("name", ""), + } + ).get_appstore_templates() + ) class BatchDelete(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['PUT'], + methods=["PUT"], description=_("Batch delete applications"), summary=_("Batch delete applications"), operation_id=_("Batch delete applications"), # type: ignore parameters=ApplicationBatchOperateAPI.get_parameters(), request=ApplicationBatchOperateAPI.get_request(), responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_BATCH_DELETE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_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', []) + id_list = request.data.get("id_list", []) permitted_ids = check_batch_permissions( - request, id_list, 'application_id', - (PermissionConstants.APPLICATION_DELETE.get_workspace_application_permission(), - PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), workspace_id=workspace_id + request, + id_list, + "application_id", + ( + PermissionConstants.APPLICATION_DELETE.get_workspace_application_permission(), + PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ), + workspace_id=workspace_id, ) - @log(menu='Application', operate='Batch delete applications', - get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids)) + @log( + menu="Application", + operate="Batch delete applications", + get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids), + ) def inner(view, r, **kwargs): return ApplicationBatchOperateSerializer( - data={'workspace_id': workspace_id, 'user_id': request.user.id} - ).batch_delete({'id_list': permitted_ids}) + data={"workspace_id": workspace_id, "user_id": request.user.id} + ).batch_delete({"id_list": permitted_ids}) return result.success(inner(self, request, workspace_id=workspace_id)) @@ -353,38 +540,48 @@ class BatchMove(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['PUT'], + methods=["PUT"], description=_("Batch move applications"), summary=_("Batch move applications"), operation_id=_("Batch move applications"), # type: ignore parameters=ApplicationBatchOperateAPI.get_parameters(), request=ApplicationBatchOperateAPI.get_move_request(), responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_BATCH_MOVE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_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', []) + id_list = request.data.get("id_list", []) permitted_ids = check_batch_permissions( - request, id_list, 'application_id', - (PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), - workspace_id=workspace_id + request, + id_list, + "application_id", + ( + PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ), + workspace_id=workspace_id, ) - @log(menu='Application', operate='Batch move applications', - get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids)) + @log( + menu="Application", + operate="Batch move applications", + get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids), + ) def inner(view, r, **kwargs): return ApplicationBatchOperateSerializer( - data={'workspace_id': workspace_id, 'user_id': request.user.id} - ).batch_move({'id_list': permitted_ids, 'folder_id': request.data.get('folder_id')}) + data={"workspace_id": workspace_id, "user_id": request.user.id} + ).batch_move({"id_list": permitted_ids, "folder_id": request.data.get("folder_id")}) return result.success(inner(self, request, workspace_id=workspace_id)) @@ -392,42 +589,54 @@ class BatchCleanTime(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['PUT'], + methods=["PUT"], description=_("Batch update application chat log clear policy"), summary=_("Batch update application chat log clear policy"), operation_id=_("Batch update application chat log clear policy"), # type: ignore parameters=ApplicationBatchOperateAPI.get_parameters(), request=ApplicationBatchOperateAPI.get_clean_time_request(), responses=result.DefaultResultSerializer, - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_READ.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_READ.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', []) + id_list = request.data.get("id_list", []) permitted_ids = check_batch_permissions( - request, id_list, 'application_id', - (PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_application_permission(), - PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), - workspace_id=workspace_id + request, + id_list, + "application_id", + ( + PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_application_permission(), + PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ), + workspace_id=workspace_id, ) - @log(menu='Application', operate='Batch update application chat log clear policy', - get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids)) + @log( + menu="Application", + operate="Batch update application chat log clear policy", + get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids), + ) def inner(view, r, **kwargs): return ApplicationBatchOperateSerializer( - data={'workspace_id': workspace_id, 'user_id': request.user.id} - ).batch_clean_time({ - 'id_list': permitted_ids, - 'clean_time': request.data.get('clean_time'), - 'file_clean_time': request.data.get('file_clean_time') - }) + data={"workspace_id": workspace_id, "user_id": request.user.id} + ).batch_clean_time( + { + "id_list": permitted_ids, + "clean_time": request.data.get("clean_time"), + "file_clean_time": request.data.get("file_clean_time"), + } + ) return result.success(inner(self, request, workspace_id=workspace_id)) @@ -436,105 +645,137 @@ class McpServers(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['GET'], + methods=["GET"], description=_("speech to text"), summary=_("speech to text"), operation_id=_("speech to text"), # type: ignore parameters=SpeechToTextAPI.get_parameters(), request=SpeechToTextAPI.get_request(), responses=SpeechToTextAPI.get_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_READ.get_workspace_application_permission(), + PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_application_permission(), - PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def post(self, request: Request, workspace_id, application_id: str): - return result.success(ApplicationOperateSerializer( - data={'mcp_servers': request.query_params.get('mcp_servers'), 'workspace_id': workspace_id, - 'user_id': request.user.id, - 'application_id': application_id}).get_mcp_servers(request.data)) + return result.success( + ApplicationOperateSerializer( + data={ + "mcp_servers": request.query_params.get("mcp_servers"), + "workspace_id": workspace_id, + "user_id": request.user.id, + "application_id": application_id, + } + ).get_mcp_servers(request.data) + ) class SpeechToText(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['POST'], + methods=["POST"], description=_("speech to text"), summary=_("speech to text"), operation_id=_("speech to text"), # type: ignore parameters=SpeechToTextAPI.get_parameters(), request=SpeechToTextAPI.get_request(), responses=SpeechToTextAPI.get_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def post(self, request: Request, workspace_id: str, application_id: str): return result.success( ApplicationOperateSerializer( - data={'application_id': application_id, 'workspace_id': workspace_id, 'user_id': request.user.id}) - .speech_to_text({'file': request.FILES.get('file')})) + data={"application_id": application_id, "workspace_id": workspace_id, "user_id": request.user.id} + ).speech_to_text({"file": request.FILES.get("file")}) + ) class TextToSpeech(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['POST'], + methods=["POST"], description=_("text to speech"), summary=_("text to speech"), operation_id=_("text to speech"), # type: ignore parameters=TextToSpeechAPI.get_parameters(), request=TextToSpeechAPI.get_request(), responses=TextToSpeechAPI.get_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), ) - @has_permissions(PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def post(self, request: Request, workspace_id: str, application_id: str): byte_data = ApplicationOperateSerializer( - data={'application_id': application_id, 'workspace_id': workspace_id, - 'user_id': request.user.id}).text_to_speech(request.data) - return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3', - 'Content-Disposition': 'attachment; filename="abc.mp3"'}) + data={"application_id": application_id, "workspace_id": workspace_id, "user_id": request.user.id} + ).text_to_speech(request.data) + return HttpResponse( + byte_data, + status=200, + headers={"Content-Type": "audio/mp3", "Content-Disposition": 'attachment; filename="abc.mp3"'}, + ) class PlayDemoText(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['POST'], + methods=["POST"], description=_("PlayDemo"), summary=_("PlayDemo"), operation_id=_("PlayDemo"), # type: ignore parameters=PlayDemoTextAPI.get_parameters(), request=PlayDemoTextAPI.get_request(), responses=PlayDemoTextAPI.get_response(), - tags=[_('Application')] # type: ignore + tags=[_("Application")], # type: ignore + ) + @has_permissions( + PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + compare=CompareConstants.AND, + ), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ) + @log( + menu="Application", + operate="trial listening", + get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")), ) - @has_permissions(PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), - PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), - ViewPermission([RoleConstants.USER.get_workspace_role()], - [PermissionConstants.APPLICATION.get_workspace_application_permission()], - compare=CompareConstants.AND), - RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) - @log(menu='Application', operate="trial listening", - get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id'))) def post(self, request: Request, workspace_id: str, application_id: str): byte_data = ApplicationOperateSerializer( - data={'application_id': application_id, 'workspace_id': workspace_id, - 'user_id': request.user.id}).play_demo_text(request.data) - return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3', - 'Content-Disposition': 'attachment; filename="abc.mp3"'}) + data={"application_id": application_id, "workspace_id": workspace_id, "user_id": request.user.id} + ).play_demo_text(request.data) + return HttpResponse( + byte_data, + status=200, + headers={"Content-Type": "audio/mp3", "Content-Disposition": 'attachment; filename="abc.mp3"'}, + ) diff --git a/apps/common/auth/constants/operate_constants.py b/apps/common/auth/constants/operate_constants.py index 835c5e86935..e811fe8c607 100644 --- a/apps/common/auth/constants/operate_constants.py +++ b/apps/common/auth/constants/operate_constants.py @@ -76,8 +76,10 @@ class Operate(Enum): CHANGE_PASSWORD = ("READ+CREATE", _("Change Password")) SYSTEM_API_KEY = ("READ+DELETE", _("System API Key")) PORTAL = ("READ+PORTAL", _("Portal")) + PORTAL_UP = ("READ+PORTAL_UP", _("Publish to Portal")) + PORTAL_DOWN = ("READ+PORTAL_DOWN", _("Unpublish from Portal")) - ANNOTATION_AUTH = ('ANNOTATION', _("Annotation")) + ANNOTATION_AUTH = ("ANNOTATION", _("Annotation")) PASSWORD = ("PASSWORD", _("Password verification")) LOCAL = ("LOCAL", _("Account login")) CAS = ("CAS", _("CAS")) diff --git a/apps/common/auth/constants/permission_constants.py b/apps/common/auth/constants/permission_constants.py index 7850b3531d3..701b2e852cf 100644 --- a/apps/common/auth/constants/permission_constants.py +++ b/apps/common/auth/constants/permission_constants.py @@ -1441,6 +1441,26 @@ class PermissionConstants(Enum): ), ) + APPLICATION_PORTAL_UP = ( + Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.PORTAL_UP, bit_index=17), + PermissionMeta( + role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER], + category=Category.WORKSPACE, + scope=[PermissionScopeConstants.WORKSPACE_RESOURCE], + resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE], + ), + ) + + APPLICATION_PORTAL_DOWN = ( + Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.PORTAL_DOWN, bit_index=18), + PermissionMeta( + role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER], + category=Category.WORKSPACE, + scope=[PermissionScopeConstants.WORKSPACE_RESOURCE], + resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE], + ), + ) + # ==================== 应用文件夹 ==================== APPLICATION_FOLDER_READ = ( Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.READ, bit_index=0), @@ -2696,6 +2716,36 @@ class PermissionConstants(Enum): ), ) + RESOURCE_APPLICATION_PORTAL_UP = ( + Permission( + group=Group.SYSTEM_RES_APPLICATION, + sub_group=Group.SYSTEM_RES_APPLICATION, + operate=Operate.PORTAL_UP, + bit_index=12, + ), + PermissionMeta( + role_list=[RoleConstants.ADMIN], + category=Category.RESOURCE, + is_ee=is_ee, + scope=[PermissionScopeConstants.SYSTEM], + ), + ) + + RESOURCE_APPLICATION_PORTAL_DOWN = ( + Permission( + group=Group.SYSTEM_RES_APPLICATION, + sub_group=Group.SYSTEM_RES_APPLICATION, + operate=Operate.PORTAL_DOWN, + bit_index=13, + ), + PermissionMeta( + role_list=[RoleConstants.ADMIN], + category=Category.RESOURCE, + is_ee=is_ee, + scope=[PermissionScopeConstants.SYSTEM], + ), + ) + # 资源管理 - 应用概览 RESOURCE_APPLICATION_OVERVIEW_READ = ( Permission( diff --git a/apps/common/tests/test_portal_permissions.py b/apps/common/tests/test_portal_permissions.py new file mode 100644 index 00000000000..56c1d1dbd59 --- /dev/null +++ b/apps/common/tests/test_portal_permissions.py @@ -0,0 +1,51 @@ +# coding=utf-8 + +from django.test import SimpleTestCase + +from common.auth.constants.category_constants import Category +from common.auth.constants.operate_constants import Operate +from common.auth.constants.permission_constants import PermissionConstants +from common.auth.constants.role_constants import RoleConstants + + +class ApplicationPortalPermissionTests(SimpleTestCase): + """智能体门户上下架权限已注册且分组/操作位与前端一致。""" + + def test_application_portal_up_permission(self): + perm = PermissionConstants.APPLICATION_PORTAL_UP.value + meta = PermissionConstants.APPLICATION_PORTAL_UP.meta + self.assertEqual(perm.group.value, "APPLICATION") + self.assertEqual(perm.operate, Operate.PORTAL_UP) + self.assertEqual(perm.bit_index, 17) + self.assertIn(RoleConstants.WORKSPACE_MANAGE, meta.role_list) + self.assertIn(RoleConstants.USER, meta.role_list) + self.assertEqual(meta.category, Category.WORKSPACE) + + def test_application_portal_down_permission(self): + perm = PermissionConstants.APPLICATION_PORTAL_DOWN.value + meta = PermissionConstants.APPLICATION_PORTAL_DOWN.meta + self.assertEqual(perm.group.value, "APPLICATION") + self.assertEqual(perm.operate, Operate.PORTAL_DOWN) + self.assertEqual(perm.bit_index, 18) + self.assertEqual(meta.category, Category.WORKSPACE) + + def test_resource_application_portal_up_permission(self): + perm = PermissionConstants.RESOURCE_APPLICATION_PORTAL_UP.value + meta = PermissionConstants.RESOURCE_APPLICATION_PORTAL_UP.meta + self.assertEqual(perm.group.value, "SYSTEM_RESOURCE_APPLICATION") + self.assertEqual(perm.operate, Operate.PORTAL_UP) + self.assertEqual(perm.bit_index, 12) + self.assertIn(RoleConstants.ADMIN, meta.role_list) + self.assertEqual(meta.category, Category.RESOURCE) + + def test_resource_application_portal_down_permission(self): + perm = PermissionConstants.RESOURCE_APPLICATION_PORTAL_DOWN.value + meta = PermissionConstants.RESOURCE_APPLICATION_PORTAL_DOWN.meta + self.assertEqual(perm.group.value, "SYSTEM_RESOURCE_APPLICATION") + self.assertEqual(perm.operate, Operate.PORTAL_DOWN) + self.assertEqual(perm.bit_index, 13) + self.assertEqual(meta.category, Category.RESOURCE) + + def test_operate_values_match_frontend(self): + self.assertEqual(Operate.PORTAL_UP.value, "READ+PORTAL_UP") + self.assertEqual(Operate.PORTAL_DOWN.value, "READ+PORTAL_DOWN") diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index b7d810356a1..cc13f466b59 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -9306,6 +9306,21 @@ msgstr "" msgid "Publish" msgstr "" +msgid "Publish to Portal" +msgstr "" + +msgid "Unpublish from Portal" +msgstr "" + +msgid "Only published applications can be published to Portal" +msgstr "" + +msgid "Publishing an application to Portal" +msgstr "" + +msgid "Unpublishing an application from Portal" +msgstr "" + msgid "token is required for EVENT triggers" msgstr "" diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index b5c8fb54e80..ba2fa326626 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -9431,6 +9431,21 @@ msgstr "model_params_form 中的第 {index} 项的 label 字段是必填项" msgid "Publish" msgstr "发布" +msgid "Publish to Portal" +msgstr "上架到门户" + +msgid "Unpublish from Portal" +msgstr "从门户下架" + +msgid "Only published applications can be published to Portal" +msgstr "仅已发布的应用可以上架到门户" + +msgid "Publishing an application to Portal" +msgstr "将应用上架到门户" + +msgid "Unpublishing an application from Portal" +msgstr "将应用从门户下架" + msgid "token is required for EVENT triggers" msgstr "事件触发器必须设置 token" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index a5444ff2883..34961ded98e 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -9431,6 +9431,21 @@ msgstr "model_params_form 中的第 {index} 项的 label 字段是必填项" msgid "Publish" msgstr "發布" +msgid "Publish to Portal" +msgstr "上架到門戶" + +msgid "Unpublish from Portal" +msgstr "從門戶下架" + +msgid "Only published applications can be published to Portal" +msgstr "僅已發布的應用可以上架到門戶" + +msgid "Publishing an application to Portal" +msgstr "將應用上架到門戶" + +msgid "Unpublishing an application from Portal" +msgstr "將應用從門戶下架" + msgid "token is required for EVENT triggers" msgstr "事件觸發器必須設定 token" diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 1c413df1cd4..1d2634ca6e2 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -390,8 +390,10 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外。Syst - 知识库 Workflow API 记录为 `//action//`,筛选 user_name、state; 详情和取消为 GET action/、POST action//cancel。 - 智能体复制查询完整 ApplicationDetail,model 映射 model_id 后创建,不使用卡片摘要。 -- Workspace 与 System 资源管理的 Application API 通过 `putApplicationPortal(applicationId, isPortal)` - 向各自范围的 `application/` 发送 PUT 请求,仅提交 `{ is_portal }`,返回 `ApplicationDetail`。 +- Workspace 与 System 资源管理的 Application API 通过 `putApplicationPortalUp(applicationId)` 与 + `putApplicationPortalDown(applicationId)` 分别向各自范围的 `application//portal_up`、 + `application//portal_down` 发送 PUT 请求(上下架接口与通用编辑接口分离),返回 `ApplicationDetail`。 + 上架仅允许已发布(`is_publish`)的智能体;上下架分别受门户上架/下架权限控制。 `exportApplicationSkills(applicationId, applicationName)` 仍为本地 mock,返回 `{ application_id, name }`, 不发送请求或生成 Skills 文件,真实下载接口待接入。 diff --git a/ui/src/api/admin/system/resource-management/application/application.ts b/ui/src/api/admin/system/resource-management/application/application.ts index 2b69f2352a1..2866d4f4624 100644 --- a/ui/src/api/admin/system/resource-management/application/application.ts +++ b/ui/src/api/admin/system/resource-management/application/application.ts @@ -26,9 +26,11 @@ const exportApplication = (applicationId: string, applicationName: string) => { const exportApplicationSkills = (applicationId: string, applicationName: string) => Promise.resolve({ application_id: applicationId, name: applicationName }) -/** 修改 System 资源管理智能体门户上下架状态。 */ -const putApplicationPortal = (applicationId: string, isPortal: boolean) => - put<{ is_portal: boolean }, ApplicationDetail>(`${prefix}/${applicationId}`, { is_portal: isPortal }) +/** System 资源管理智能体上架到门户(仅已发布可上架)。 */ +const putApplicationPortalUp = (applicationId: string) => put, ApplicationDetail>(`${prefix}/${applicationId}/portal_up`) + +/** System 资源管理智能体从门户下架。 */ +const putApplicationPortalDown = (applicationId: string) => put, ApplicationDetail>(`${prefix}/${applicationId}/portal_down`) /** 获取 System 资源管理智能体详情。 */ const getApplicationDetail = (applicationId: string) => get(`${prefix}/${applicationId}`) @@ -69,5 +71,6 @@ export default { deleteApplication, exportApplication, exportApplicationSkills, - putApplicationPortal, + putApplicationPortalUp, + putApplicationPortalDown, } diff --git a/ui/src/api/admin/workspace/application/application.ts b/ui/src/api/admin/workspace/application/application.ts index 36d0b5cf1b0..fce1df2b9eb 100644 --- a/ui/src/api/admin/workspace/application/application.ts +++ b/ui/src/api/admin/workspace/application/application.ts @@ -51,10 +51,12 @@ const exportApplication = (applicationId: string, applicationName: string) => { const exportApplicationSkills = (applicationId: string, applicationName: string) => Promise.resolve({ application_id: applicationId, name: applicationName }) -/** 修改工作空间智能体门户上下架状态。 */ -const putApplicationPortal = (applicationId: string, isPortal: boolean) => { - return put<{ is_portal: boolean }, ApplicationDetail>(`${getPrefix()}/${applicationId}`, { is_portal: isPortal }) -} +/** 工作空间智能体上架到门户(仅已发布可上架)。 */ +const putApplicationPortalUp = (applicationId: string) => put, ApplicationDetail>(`${getPrefix()}/${applicationId}/portal_up`) + +/** 工作空间智能体从门户下架。 */ +const putApplicationPortalDown = (applicationId: string) => + put, ApplicationDetail>(`${getPrefix()}/${applicationId}/portal_down`) /** 导入智能体文件并创建工作空间智能体。 */ const postApplicationImport = (file: File, folderId: string) => { @@ -105,7 +107,8 @@ export default { deleteApplication, exportApplication, exportApplicationSkills, - putApplicationPortal, + putApplicationPortalUp, + putApplicationPortalDown, postApplication, postApplicationImport, putApplication, diff --git a/ui/src/permission/core/common.ts b/ui/src/permission/core/common.ts index f1832528a3b..384e9149b85 100644 --- a/ui/src/permission/core/common.ts +++ b/ui/src/permission/core/common.ts @@ -145,6 +145,8 @@ export enum Operate { CHANGE_PASSWORD = 'READ+CREATE', SYSTEM_API_KEY = 'READ+DELETE', PORTAL = 'READ+PORTAL', + PORTAL_UP = 'READ+PORTAL_UP', + PORTAL_DOWN = 'READ+PORTAL_DOWN', PASSWORD = 'PASSWORD', LOCAL = 'LOCAL', CAS = 'CAS', diff --git a/ui/src/permission/core/data.ts b/ui/src/permission/core/data.ts index 6f135162b54..25959492558 100644 --- a/ui/src/permission/core/data.ts +++ b/ui/src/permission/core/data.ts @@ -186,6 +186,8 @@ export const PermissionConstants = { APPLICATION_TRIGGER_CREATE: p(Group.APPLICATION, Group.APPLICATION, Operate.TRIGGER_CREATE, 14), APPLICATION_TRIGGER_EDIT: p(Group.APPLICATION, Group.APPLICATION, Operate.TRIGGER_EDIT, 15), APPLICATION_TRIGGER_DELETE: p(Group.APPLICATION, Group.APPLICATION, Operate.TRIGGER_DELETE, 16), + APPLICATION_PORTAL_UP: p(Group.APPLICATION, Group.APPLICATION, Operate.PORTAL_UP, 17), + APPLICATION_PORTAL_DOWN: p(Group.APPLICATION, Group.APPLICATION, Operate.PORTAL_DOWN, 18), APPLICATION_FOLDER_READ: p(Group.APPLICATION, Group.FOLDER, Operate.READ, 0), APPLICATION_FOLDER_CREATE: p(Group.APPLICATION, Group.FOLDER, Operate.CREATE, 1), APPLICATION_FOLDER_EDIT: p(Group.APPLICATION, Group.FOLDER, Operate.EDIT, 2), @@ -323,6 +325,8 @@ export const PermissionConstants = { RESOURCE_APPLICATION_TRIGGER_EDIT: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_RES_APPLICATION, Operate.TRIGGER_EDIT, 9), RESOURCE_APPLICATION_TRIGGER_DELETE: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_RES_APPLICATION, Operate.TRIGGER_DELETE, 10), RESOURCE_APPLICATION_RELATE_RESOURCE_VIEW: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_RES_APPLICATION, Operate.RELATE_VIEW, 11), + RESOURCE_APPLICATION_PORTAL_UP: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_RES_APPLICATION, Operate.PORTAL_UP, 12), + RESOURCE_APPLICATION_PORTAL_DOWN: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_RES_APPLICATION, Operate.PORTAL_DOWN, 13), RESOURCE_APPLICATION_OVERVIEW_READ: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_OVERVIEW, Operate.READ, 0), RESOURCE_APPLICATION_OVERVIEW_EMBED: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_OVERVIEW, Operate.EMBED, 1), RESOURCE_APPLICATION_OVERVIEW_ACCESS: p(Group.SYSTEM_RES_APPLICATION, Group.SYSTEM_OVERVIEW, Operate.ACCESS, 2), diff --git a/ui/src/permission/modules/application/system.ts b/ui/src/permission/modules/application/system.ts index d30e627a14f..e6cd153c362 100644 --- a/ui/src/permission/modules/application/system.ts +++ b/ui/src/permission/modules/application/system.ts @@ -4,7 +4,7 @@ */ import { canSys } from '../../policy' -import { PermissionConstants as P,hasEdition} from '../../core' +import { PermissionConstants as P, hasEdition } from '../../core' import { Edition } from '@/permission/core/common' const system = { @@ -26,6 +26,8 @@ const system = { copy: () => canSys(P.RESOURCE_APPLICATION_COPY), export: () => canSys(P.RESOURCE_APPLICATION_EXPORT), publish: () => canSys(P.RESOURCE_APPLICATION_PUBLISH), + portalUp: () => canSys(P.RESOURCE_APPLICATION_PORTAL_UP), + portalDown: () => canSys(P.RESOURCE_APPLICATION_PORTAL_DOWN), auth: () => canSys(P.RESOURCE_APPLICATION_AUTH), relateMap: () => canSys(P.RESOURCE_APPLICATION_RELATE_RESOURCE_VIEW), debug: () => canSys(P.RESOURCE_APPLICATION_EDIT), diff --git a/ui/src/permission/modules/application/workspace.ts b/ui/src/permission/modules/application/workspace.ts index b6c1cd02c34..dd62b516c63 100644 --- a/ui/src/permission/modules/application/workspace.ts +++ b/ui/src/permission/modules/application/workspace.ts @@ -16,6 +16,8 @@ const workspace = { copy: (id: string) => canRes(P.APPLICATION_COPY, id), export: (id: string) => canRes(P.APPLICATION_EXPORT, id), publish: (id: string) => canRes(P.APPLICATION_PUBLISH, id), + portalUp: (id: string) => canRes(P.APPLICATION_PORTAL_UP, id), + portalDown: (id: string) => canRes(P.APPLICATION_PORTAL_DOWN, id), auth: (id: string) => canRes(P.APPLICATION_RESOURCE_AUTHORIZATION, id), relateMap: (id: string) => canRes(P.APPLICATION_RELATE_RESOURCE_VIEW, id), debug: (id: string) => canRes(P.APPLICATION_READ, id), diff --git a/ui/src/views/application/application-card/action-dropdown/PortalApplicationAction.vue b/ui/src/views/application/application-card/action-dropdown/PortalApplicationAction.vue index efbb70786d8..fbcae0b130e 100644 --- a/ui/src/views/application/application-card/action-dropdown/PortalApplicationAction.vue +++ b/ui/src/views/application/application-card/action-dropdown/PortalApplicationAction.vue @@ -1,7 +1,9 @@