e58159fc42
Phase 1: 异常体系统一 - 新增 PlatformError / PlatformErrorType 标准定义 - 改造所有 Provider 异常抛出为 PlatformError - 注册全局 PlatformError exception handler Phase 2: Adapter Protocol - 新增 app/ai/adapters/base.py(PlatformAdapter + SyncCapable + TaskCapable + CallbackCapable) - 新增 app/ai/adapters/constants.py(Method 常量) - 新增 PlatformConfigLoader(config/platform-config.yaml) Phase 3: HTTP Client 统一 - ViduProvider 从 aiohttp 迁移到 httpx(注入方式) - VolcengineCaptionService 改为注入 http_client - lifespan 统一管理所有 Client 创建和关闭 Phase 4: Gateway 骨架 + Adapter 实现 - 新增 ViduAdapter / VolcengineArkAdapter / VolcengineCaptionAdapter - 新增 PlatformGateway(call_sync / submit_task / query_task / handle_webhook) - 新增 LLMGateway(带 Fallback 降级链) - lifespan 注册所有 Adapter 和 Gateway Phase 6: 清理与验证 - 从 Settings 移除 VIDU_BASE_URL / VOLCENGINE_BASE_URL - Provider 改为从 PlatformConfigLoader 读取 base_url - 清理 volcengine_caption_service 全局单例 - config_loader 默认路径改为 platform-config.yaml - Scheduler 注入共享 HTTP client - vidu.py 回调路由使用 Adapter 验签和解析 - ruff 全量通过,应用启动测试通过
169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
"""
|
|
自定义异常类
|
|
============
|
|
|
|
分层异常体系:
|
|
- AppException: 业务层错误(参数校验、权限、资源不存在等)
|
|
- PlatformError: 第三方平台调用错误(网络、限流、认证、服务异常等)
|
|
|
|
Router 层只处理这两类异常,其余全部兜底为 500。
|
|
"""
|
|
|
|
from fastapi import HTTPException, status
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 业务层异常(AppException 体系)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
class AppException(HTTPException):
|
|
"""应用基础异常"""
|
|
|
|
def __init__(
|
|
self,
|
|
status_code: int,
|
|
message: str = "操作失败",
|
|
detail: dict | None = None,
|
|
):
|
|
super().__init__(status_code=status_code, detail=detail or {})
|
|
self.message = message
|
|
|
|
|
|
class NotFoundException(AppException):
|
|
"""资源不存在"""
|
|
|
|
def __init__(self, message: str = "资源不存在"):
|
|
super().__init__(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
message=message,
|
|
)
|
|
|
|
|
|
class ValidationException(AppException):
|
|
"""参数验证失败"""
|
|
|
|
def __init__(self, message: str = "参数验证失败"):
|
|
super().__init__(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
message=message,
|
|
)
|
|
|
|
|
|
class UnauthorizedException(AppException):
|
|
"""未授权"""
|
|
|
|
def __init__(self, message: str = "未授权,请先登录"):
|
|
super().__init__(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
message=message,
|
|
)
|
|
|
|
|
|
class ForbiddenException(AppException):
|
|
"""禁止访问"""
|
|
|
|
def __init__(self, message: str = "无权访问该资源"):
|
|
super().__init__(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
message=message,
|
|
)
|
|
|
|
|
|
class BusinessException(AppException):
|
|
"""业务逻辑错误"""
|
|
|
|
def __init__(self, message: str = "业务操作失败"):
|
|
super().__init__(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
message=message,
|
|
)
|
|
|
|
|
|
class ModelUnavailableException(AppException):
|
|
"""AI 模型不可用"""
|
|
|
|
def __init__(self, message: str = "AI 模型服务暂时不可用"):
|
|
super().__init__(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
message=message,
|
|
)
|
|
|
|
|
|
class TaskFailedException(AppException):
|
|
"""异步任务执行失败"""
|
|
|
|
def __init__(self, message: str = "任务执行失败"):
|
|
super().__init__(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
message=message,
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 第三方平台异常(PlatformError 体系)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
class PlatformErrorType:
|
|
"""第三方错误类型标准枚举
|
|
|
|
所有 Adapter 必须将供应商异常映射为以下标准类型之一,
|
|
确保前端和网关能够统一处理。
|
|
"""
|
|
|
|
RATE_LIMIT = "rate_limit" # 429,可重试
|
|
AUTH_FAILED = "auth_failed" # 401/403,不可重试
|
|
TIMEOUT = "timeout" # 连接/读取超时,可重试
|
|
SERVER_ERROR = "server_error" # 第三方 5xx,可重试
|
|
BAD_REQUEST = "bad_request" # 参数错误,不可重试
|
|
QUOTA_EXHAUSTED = "quota_exhausted" # 额度用完,不可重试(或延迟重试)
|
|
NOT_FOUND = "not_found" # 资源不存在,不可重试
|
|
UNKNOWN = "unknown" # 兜底
|
|
|
|
|
|
class PlatformError(Exception):
|
|
"""第三方平台调用失败的唯一异常类
|
|
|
|
Router 层只需 except PlatformError,即可返回标准 HTTP 状态码。
|
|
所有 app/services/ 和 app/ai/ 下的代码,对外抛出的异常必须是此类。
|
|
|
|
Example:
|
|
raise PlatformError(
|
|
"Vidu 限流",
|
|
platform="vidu",
|
|
retryable=True,
|
|
error_type=PlatformErrorType.RATE_LIMIT,
|
|
status_code=429,
|
|
)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
platform: str,
|
|
retryable: bool = False,
|
|
error_type: str = PlatformErrorType.UNKNOWN,
|
|
status_code: int | None = None,
|
|
):
|
|
super().__init__(message)
|
|
self.platform = platform
|
|
self.retryable = retryable
|
|
self.error_type = error_type
|
|
self.status_code = status_code
|
|
|
|
def to_http_status(self) -> int:
|
|
"""根据 error_type 和 retryable 返回标准 HTTP 状态码"""
|
|
mapping = {
|
|
PlatformErrorType.RATE_LIMIT: 429,
|
|
PlatformErrorType.QUOTA_EXHAUSTED: 429,
|
|
PlatformErrorType.TIMEOUT: 504,
|
|
PlatformErrorType.AUTH_FAILED: 401,
|
|
PlatformErrorType.BAD_REQUEST: 400,
|
|
PlatformErrorType.NOT_FOUND: 404,
|
|
}
|
|
if self.error_type in mapping:
|
|
return mapping[self.error_type]
|
|
return 502 if self.retryable else 400
|