From 4c9f327833742ff3f6ecf98d173e4917bd070116 Mon Sep 17 00:00:00 2001 From: wwq Date: Tue, 21 Apr 2026 18:15:31 +0800 Subject: [PATCH 1/2] feat(quota): add quota checks during app duplication and import operations - Integrate quota check decorators into app duplication, workflow import save, and app import actions. - Explicitly validate application quotas for new app imports. --- api/app/controllers/app_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index 3d97f2a2..f3cbe5ea 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -219,6 +219,7 @@ def delete_app( @router.post("/{app_id}/copy", summary="复制应用") @cur_workspace_access_guard() +@check_app_quota def copy_app( app_id: uuid.UUID, new_name: Optional[str] = None, @@ -1144,6 +1145,7 @@ async def import_workflow_config( @router.post("/workflow/import/save") @cur_workspace_access_guard() +@check_app_quota async def save_workflow_import( data: WorkflowImportSave, db: Session = Depends(get_db), @@ -1281,6 +1283,10 @@ async def import_app( return fail(msg="YAML 格式无效,缺少 app 字段", code=BizCode.BAD_REQUEST) target_app_id = uuid.UUID(app_id) if app_id else None + # 仅新建应用时检查配额,覆盖已有应用时跳过 + if target_app_id is None: + from app.core.quota_manager import _check_quota + _check_quota(db, current_user.tenant_id, "app_quota", "app") result_app, warnings = AppDslService(db).import_dsl( dsl=dsl, workspace_id=current_user.current_workspace_id, From 8bb5a664019448a1885a094f7086961317b6ce6c Mon Sep 17 00:00:00 2001 From: wwq Date: Tue, 21 Apr 2026 18:16:38 +0800 Subject: [PATCH 2/2] feat(exception): enhance I18nException response format and add error code mapping - Standardize error response format to include business error codes, timestamps, and other fields. - Add ERROR_CODE_TO_BIZ_CODE mapping table for error code conversion. - Introduce QUOTA_EXCEEDED and RATE_LIMIT_EXCEEDED business error codes. --- api/app/core/error_codes.py | 22 +++++++++++++++++++++- api/app/i18n/exceptions.py | 27 +++++++++++++++++++-------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/api/app/core/error_codes.py b/api/app/core/error_codes.py index a4a20cbb..77bce6b4 100644 --- a/api/app/core/error_codes.py +++ b/api/app/core/error_codes.py @@ -32,6 +32,8 @@ class BizCode(IntEnum): API_KEY_DAILY_LIMIT_EXCEEDED = 3015 API_KEY_QUOTA_EXCEEDED = 3016 API_KEY_RATE_LIMIT_EXCEEDED = 3017 + QUOTA_EXCEEDED = 3018 + RATE_LIMIT_EXCEEDED = 3019 # 资源(4xxx) NOT_FOUND = 4000 USER_NOT_FOUND = 4001 @@ -156,7 +158,8 @@ HTTP_MAPPING = { BizCode.API_KEY_QPS_LIMIT_EXCEEDED: 429, BizCode.API_KEY_DAILY_LIMIT_EXCEEDED: 429, BizCode.API_KEY_QUOTA_EXCEEDED: 429, - + BizCode.QUOTA_EXCEEDED: 402, + BizCode.MODEL_CONFIG_INVALID: 400, BizCode.API_KEY_MISSING: 400, BizCode.PROVIDER_NOT_SUPPORTED: 400, @@ -185,4 +188,21 @@ HTTP_MAPPING = { BizCode.DB_ERROR: 500, BizCode.SERVICE_UNAVAILABLE: 503, BizCode.RATE_LIMITED: 429, + BizCode.RATE_LIMIT_EXCEEDED: 429, +} + +ERROR_CODE_TO_BIZ_CODE = { + "QUOTA_EXCEEDED": BizCode.QUOTA_EXCEEDED, + "RATE_LIMIT_EXCEEDED": BizCode.RATE_LIMIT_EXCEEDED, + "API_KEY_NOT_FOUND": BizCode.API_KEY_NOT_FOUND, + "API_KEY_INVALID": BizCode.API_KEY_INVALID, + "API_KEY_EXPIRED": BizCode.API_KEY_EXPIRED, + "WORKSPACE_NOT_FOUND": BizCode.WORKSPACE_NOT_FOUND, + "WORKSPACE_NO_ACCESS": BizCode.WORKSPACE_NO_ACCESS, + "PERMISSION_DENIED": BizCode.PERMISSION_DENIED, + "TOKEN_EXPIRED": BizCode.TOKEN_EXPIRED, + "TOKEN_INVALID": BizCode.TOKEN_INVALID, + "VALIDATION_FAILED": BizCode.VALIDATION_FAILED, + "INVALID_PARAMETER": BizCode.INVALID_PARAMETER, + "MISSING_PARAMETER": BizCode.MISSING_PARAMETER, } diff --git a/api/app/i18n/exceptions.py b/api/app/i18n/exceptions.py index 9a517925..93794c39 100644 --- a/api/app/i18n/exceptions.py +++ b/api/app/i18n/exceptions.py @@ -6,12 +6,14 @@ error messages based on the current request's language. """ import logging +import time from contextvars import ContextVar from typing import Any, Dict, Optional from fastapi import HTTPException, Request from app.i18n.service import get_translation_service +from app.core.error_codes import ERROR_CODE_TO_BIZ_CODE, BizCode logger = logging.getLogger(__name__) @@ -118,15 +120,24 @@ class I18nException(HTTPException): **params ) - # Build error detail - detail = { - "error_code": self.error_code, - "message": message, - } + # Convert error_code string to BizCode value + biz_code = ERROR_CODE_TO_BIZ_CODE.get( + self.error_code, + BizCode.BAD_REQUEST + ) - # Add parameters to detail if provided - if params: - detail["params"] = params + # Build error detail in standard format for compatibility + # main.py handler expects "message" and "error_code" fields for filtering + # but we also include standard format fields + detail = { + "code": biz_code.value, + "msg": message, + "message": message, + "error_code": self.error_code, + "data": params if params else {}, + "error": message, + "time": int(time.time() * 1000), + } # Initialize HTTPException super().__init__(