"""两类错误的异常与信封(契约 `openapi-internal.yaml` 的核心设计)。 **两类失败必须分开**,因为调用方对它们的反应完全不同: - `service`(Java↔Python):我方 bug 或配置问题。除 `internal_error` / `provider_unavailable` 外都不该重试——重试一个拼错的请求体不会变好。 - `provider`(Python↔服务商):以 `retry` 字段为准,调用方不该自己解读厂商码。 还有**第三类不在这里**:任务本身失败(生成跑完但失败了)走 `200 + state=failed` + `failure`。因为那种情况下 **HTTP 调用是成功的**,失败的是任务。做成错误响应会 让「查不到任务」与「任务失败了」难以区分。 判别字段是 `error.kind`,**不是 HTTP 状态码**:状态码在两类之间会撞(502 既可能是 服务商挂了也可能是我们打错地址),而 `kind` 不会。 """ from __future__ import annotations from enum import Enum from app.domain.generation import Failure class ServiceErrorCode(Enum): """`service` 类错误码,与契约的枚举逐一对应。""" #: 服务票缺失、过期、签名错、aud/typ 不符。细节只进日志。 UNAUTHENTICATED = ("unauthenticated", 401) #: 请求体不合契约,或参数超出该模型的限制(本地预校验拦下)。 INVALID_REQUEST = ("invalid_request", 400) #: 路径里的服务商任务 ID 不存在(含超过服务商 7 天保留期)。 TASK_NOT_FOUND = ("task_not_found", 404) #: 适配器未装配(如缺服务商凭证)或 media_kind 无对应适配器。 PROVIDER_UNAVAILABLE = ("provider_unavailable", 503) #: 本服务 bug。 INTERNAL_ERROR = ("internal_error", 500) def __init__(self, code: str, http_status: int): self.code = code self.http_status = http_status class ServiceError(Exception): """Java↔Python 之间的问题。""" def __init__(self, code: ServiceErrorCode, message: str): super().__init__(message) self.code = code self.message = message def to_envelope(self) -> dict: return { "error": { "kind": "service", "code": self.code.code, "message": self.message, } } class ProviderError(Exception): """Python↔服务商之间的问题,携带完整分类。 字段与 `Failure` 一致**是刻意的**:调用方因此可以复用同一段处置逻辑,不必为 「提交时被限流」和「任务失败于限流」写两套分支。 """ #: 502:本服务本身没问题,是上游调用没成功。 #: 刻意不透传服务商的状态码——429 既可能是「等几秒」也可能是「等到明天」, #: 状态码表达不了这个区别,`retry` 才是权威信号。 HTTP_STATUS = 502 def __init__(self, failure: Failure): super().__init__(failure.message or failure.provider_code) self.failure = failure def to_envelope(self) -> dict: return { "error": { "kind": "provider", "retry": self.failure.retry.value, "user_facing": self.failure.user_facing.value, "provider_code": self.failure.provider_code, "message": self.failure.message, } }