"""外部系统限界上下文的异常层次。 设计原则(见设计方案 §8.1): - **根异常 + 分层异常,一个文件**:保留 ``ExternalSystemError`` 根异常,按层内聚子异常。 - **统一兜底**:所有异常继承 ``ExternalSystemError``,最外层(Router / 全局异常处理器)统一捕获。 - **保留 ``ReferencedError`` 自定义 ``__init__``**:接收 ``references: dict[str, dict[str, list[str]]]`` 参数。 本文件位于包根目录(非 ``core/`` 下),因为异常被所有层共享(core / use_cases / framework / adapters),放在根目录便于统一导入路径。 异常层次: :: ExternalSystemError (500) # 根异常 ├── DomainValidationError (400, ValueError) # 业务规则异常 │ ├── InvalidSlugError # slug 非法或命中保留字 │ ├── EnvironmentDisabledError # 禁用环境不能设为默认 │ ├── ImportConflictError # 导入 slug 冲突 │ ├── SsrfViolationError (403) # SSRF 安全规则违反 │ └── CircuitBreakerStateError # 熔断器状态不允许重置 ├── EntityNotFoundError (404) # 领域实体不存在 ├── ReferencedError (409) # 被引用无法删除 ├── ConflictError (409) # 资源冲突(唯一约束/状态竞争) └── FrameworkError # 框架层技术异常 ├── AdapterNotFoundError (404) # 找不到适配器 ├── AuthPluginNotFoundError (404) # 找不到认证插件 ├── ExecutionError (502) # 执行阶段通用异常 │ ├── RateLimitExceededError (429) # 超出限流阈值 │ ├── CircuitOpenError (503) # 熔断器打开 │ └── QuotaExceededError (429) # 配额耗尽 ├── SecretResolutionError (400) # 密钥解析失败 ├── AuthError (403) # 认证失败 ├── AccessDeniedError (403) # 访问规则拒绝 ├── WebhookError (400) # Webhook 验签/处理失败 └── CapabilityNotSupportedError (501) # 适配器未实现可选能力 """ from __future__ import annotations from typing import Any class ExternalSystemError(Exception): """外部系统限界上下文根异常。 所有本上下文抛出的异常都应继承此类,最外层统一捕获后映射为 HTTP 响应。 ``status_code`` 作为异常到 HTTP 状态码的映射依据,子类按需覆盖。 ``error_code`` 作为稳定错误码,子类以类变量覆盖,用于驱动协议响应与日志关联。 ``trace_id`` 用于跨链路追踪关联。 """ status_code: int = 500 error_code: str = "EXTERNAL_SYSTEM_ERROR" def __init__( self, message: str, *, details: dict[str, Any] | None = None, trace_id: str | None = None, ) -> None: """初始化异常实例。 参数: message: 人类可读错误信息。 details: 业务字段字典,默认空字典。 trace_id: 调用链路追踪 ID,用于跨链路关联。 """ super().__init__(message) self.message = message self.details = details or {} self.trace_id = trace_id def to_dict(self) -> dict[str, Any]: """序列化为字典,便于跨层传递、日志记录与 API 响应。 返回 ``error_code`` / ``message`` / ``trace_id`` 三个基础字段, 并展开 ``details`` 中的业务字段。 """ return { "error_code": self.error_code, "message": self.message, "trace_id": self.trace_id, **self.details, } def __str__(self) -> str: """返回 ``[error_code] message`` 格式的字符串表示。""" return f"[{self.error_code}] {self.message}" # ─── 业务规则异常 ────────────────────────────────────────────────────────── class DomainValidationError(ExternalSystemError, ValueError): """领域校验失败。 同时继承 ``ValueError`` 以兼容 Pydantic 的 ``field_validator`` 与 ``raise ValueError(...)`` 习惯写法(在 dataclass 薄校验中可直接 ``raise DomainValidationError(...)``)。 """ status_code = 400 error_code = "VALIDATION_ERROR" class InvalidSlugError(DomainValidationError): """slug 非法或命中保留字。""" error_code = "INVALID_SLUG" class EnvironmentDisabledError(DomainValidationError): """禁用环境不能设为默认环境。""" error_code = "ENVIRONMENT_DISABLED" class ImportConflictError(DomainValidationError): """导入时发现 slug 冲突。""" error_code = "IMPORT_CONFLICT" class SsrfViolationError(DomainValidationError): """SSRF 安全规则违反。""" status_code = 403 error_code = "SSRF_VIOLATION" class CircuitBreakerStateError(DomainValidationError): """熔断器当前状态不允许重置。 由 ``POST /circuit-breaker/reset`` 端点在熔断器状态为 ``closed`` 时抛出, 表示熔断器未处于打开或半开状态,无需也无法重置。 """ error_code = "CIRCUIT_BREAKER_STATE_INVALID" class EntityNotFoundError(ExternalSystemError): """领域实体不存在。 统一覆盖原 ``ExternalSystemNotFoundError`` / ``ExternalSystemEnvironmentNotFoundError`` / ``ExternalToolNotFoundError`` / ``ExternalAssetNotFoundError`` 四个异常。 具体实体类型由 ``message`` 描述,``details`` 可携带 ``entity_type`` / ``identifier``。 """ status_code = 404 error_code = "NOT_FOUND" class ReferencedError(ExternalSystemError): """被引用无法删除。 保留自定义 ``__init__``,接收 ``references: dict[str, dict[str, list[str]]]`` 参数。 ``references`` 结构示例:: { "tools": {"by_slug": ["tool-a", "tool-b"]}, "environments": {"by_id": [1, 2]}, } """ status_code = 409 error_code = "REFERENCED" def __init__( self, references: dict[str, dict[str, list[str]]], *, trace_id: str | None = None, ) -> None: """初始化被引用无法删除异常。 参数: references: 被引用关系字典,结构示例见类 docstring。 trace_id: 调用链路追踪 ID,用于跨链路关联。 """ self.references = references super().__init__( f"以下外部系统仍被引用,无法删除: {references}", details={"references": references}, trace_id=trace_id, ) class ConflictError(ExternalSystemError): """资源冲突。 表示写操作因唯一约束冲突或状态竞争无法完成(如版本号重复、slug 重复等)。 与 ``ReferencedError`` 的区别:本异常表示新建/更新资源时与既有资源冲突, ``ReferencedError`` 表示删除资源时被其他实体引用。 """ status_code = 409 error_code = "CONFLICT" # ─── 框架技术异常 ────────────────────────────────────────────────────────── class FrameworkError(ExternalSystemError): """框架层技术异常基类。 覆盖适配器查找、认证插件查找、执行、密钥解析、认证失败等技术场景。 与 ``DomainValidationError`` 的区别:本类描述"技术层面无法完成操作", 而非"业务规则不允许"。 """ error_code = "FRAMEWORK_ERROR" class AdapterNotFoundError(FrameworkError): """找不到对应 ``adapter_type`` 的适配器。 同时覆盖原 ``IntegrationNotFoundError``(厂商集成找不到的情况合并到此处)。 """ status_code = 404 error_code = "ADAPTER_NOT_FOUND" class AuthPluginNotFoundError(FrameworkError): """找不到对应 ``auth_type`` 的认证插件。""" status_code = 404 error_code = "AUTH_PLUGIN_NOT_FOUND" class IntegrationOperationNotRegisteredError(FrameworkError): """厂商操作未注册。 由 ``IntegrationOperationRegistry`` 在 ``(operation, source_type)`` 未注册对应 handler 时抛出(PRD FR-08)。 与 ``AdapterNotFoundError`` 的区别:本异常表示厂商集成已找到, 但不支持指定的操作(如 salesforce 不支持 ``discover`` 操作的 ``openapi`` source_type);``AdapterNotFoundError`` 表示适配器类型本身不存在。 """ status_code = 400 error_code = "OPERATION_NOT_REGISTERED" class ExecutionError(FrameworkError): """执行阶段通用异常。 覆盖原 ``ExternalSystemExecutionError``,表示适配器执行过程中发生的非业务异常 (网络错误、协议错误、目标系统返回 5xx 等)。 """ status_code = 502 error_code = "EXECUTION_ERROR" class RateLimitExceededError(ExecutionError): """超出限流阈值。 由 ``RateLimiter`` 在令牌桶耗尽时抛出,对应原 ``ExternalSystemThrottledError``。 结构化字段(写入 ``details``): - ``retry_after``: 建议客户端等待秒数(令牌恢复时间)。 - ``limit_type``: 限流类型,``qps`` 或 ``concurrency``。 - ``system_id`` / ``env_key``: 限流维度。 """ status_code = 429 error_code = "RATE_LIMIT_EXCEEDED" def __init__( self, message: str, *, retry_after: int | None = None, limit_type: str | None = None, system_id: int | str | None = None, env_key: str | None = None, details: dict[str, Any] | None = None, trace_id: str | None = None, ) -> None: """初始化限流超限异常。 参数: message: 人类可读错误信息。 retry_after: 建议客户端等待秒数(令牌恢复时间)。 limit_type: 限流类型,``qps`` 或 ``concurrency``。 system_id: 限流维度系统 ID。 env_key: 限流维度环境键。 details: 业务字段字典,与上述参数合并。 trace_id: 调用链路追踪 ID,用于跨链路关联。 """ merged: dict[str, Any] = dict(details or {}) if retry_after is not None: merged["retry_after"] = retry_after if limit_type is not None: merged["limit_type"] = limit_type if system_id is not None: merged["system_id"] = system_id if env_key is not None: merged["env_key"] = env_key super().__init__(message, details=merged, trace_id=trace_id) class CircuitOpenError(ExecutionError): """熔断器打开。 由 ``CircuitBreaker`` 在熔断状态打开时抛出,对应原 ``ExternalSystemCircuitOpenError``。 结构化字段(写入 ``details``): - ``retry_after``: 剩余冷却秒数(熔断器进入 half_open 的等待时间)。 - ``system_id`` / ``env_key``: 熔断维度。 """ status_code = 503 error_code = "CIRCUIT_OPEN" def __init__( self, message: str, *, retry_after: int | None = None, system_id: int | str | None = None, env_key: str | None = None, details: dict[str, Any] | None = None, trace_id: str | None = None, ) -> None: """初始化熔断器打开异常。 参数: message: 人类可读错误信息。 retry_after: 剩余冷却秒数(熔断器进入 half_open 的等待时间)。 system_id: 熔断维度系统 ID。 env_key: 熔断维度环境键。 details: 业务字段字典,与上述参数合并。 trace_id: 调用链路追踪 ID,用于跨链路关联。 """ merged: dict[str, Any] = dict(details or {}) if retry_after is not None: merged["retry_after"] = retry_after if system_id is not None: merged["system_id"] = system_id if env_key is not None: merged["env_key"] = env_key super().__init__(message, details=merged, trace_id=trace_id) class SecretResolutionError(FrameworkError): """密钥解析失败。 由 ``SecretResolver`` 在 ``secret_refs`` 引用的密钥不存在或无权访问时抛出, 对应原 ``ExternalSystemSecretError``。 """ status_code = 400 error_code = "SECRET_RESOLUTION_FAILED" class AuthError(FrameworkError): """认证失败。 由 ``TokenManager`` / 认证插件在获取/刷新 token 失败时抛出, 对应原 ``ExternalSystemAuthError``。 """ status_code = 403 error_code = "AUTH_ERROR" class QuotaExceededError(ExecutionError): """配额耗尽。 由 ``QuotaManager`` 在调用前预检发现配额已耗尽时抛出。 与 ``RateLimitExceededError`` 同属执行节流类,但语义不同: 限流是本地令牌桶节流,配额是远端系统配额耗尽。 结构化字段(写入 ``details``): - ``quota_key``: 配额键。 - ``limit_value``: 配额上限。 - ``used_value``: 已用配额。 - ``remaining``: 剩余配额(通常为 0)。 - ``system_id`` / ``env_key``: 配额维度。 """ status_code = 429 error_code = "QUOTA_EXCEEDED" def __init__( self, message: str, *, quota_key: str | None = None, limit_value: int | None = None, used_value: int | None = None, remaining: int | None = None, system_id: int | str | None = None, env_key: str | None = None, details: dict[str, Any] | None = None, trace_id: str | None = None, ) -> None: """初始化配额耗尽异常。 参数: message: 人类可读错误信息。 quota_key: 配额键。 limit_value: 配额上限。 used_value: 已用配额。 remaining: 剩余配额(通常为 0)。 system_id: 配额维度系统 ID。 env_key: 配额维度环境键。 details: 业务字段字典,与上述参数合并。 trace_id: 调用链路追踪 ID,用于跨链路关联。 """ merged: dict[str, Any] = dict(details or {}) if quota_key is not None: merged["quota_key"] = quota_key if limit_value is not None: merged["limit_value"] = limit_value if used_value is not None: merged["used_value"] = used_value if remaining is not None: merged["remaining"] = remaining if system_id is not None: merged["system_id"] = system_id if env_key is not None: merged["env_key"] = env_key super().__init__(message, details=merged, trace_id=trace_id) class AccessDeniedError(FrameworkError): """访问规则拒绝。 由 ``AccessController`` 在执行前评估访问控制规则时, 命中 deny 规则时抛出。与 ``AuthError`` 的区别: AuthError 是认证失败(token 无效),AccessDeniedError 是认证通过但无权限。 """ status_code = 403 error_code = "ACCESS_DENIED" class WebhookError(FrameworkError): """Webhook 验签或处理失败。 由 ``WebhookVerifier`` 在验签失败、订阅不存在、密钥解密失败等场景抛出。 """ status_code = 400 error_code = "WEBHOOK_ERROR" class CapabilityNotSupportedError(FrameworkError): """适配器未实现某项可选能力。 当适配器存在但不实现某项可选能力(如 ``validate_config``)时抛出, 由 Router 捕获 Python 内置 ``NotImplementedError`` 后翻译为本异常, 交由 ``unified_error_handler`` 统一映射为 HTTP 501,避免原生异常 被 ``unhandled_exception_handler`` 兜底为 500。 与 ``IntegrationOperationNotRegisteredError`` 的区别:本异常表示 适配器实例不支持该能力方法;后者表示厂商集成的操作未注册。 """ status_code = 501 error_code = "CAPABILITY_NOT_SUPPORTED" def __init__( self, capability: str, *, adapter_type: str | None = None, details: dict[str, Any] | None = None, trace_id: str | None = None, ) -> None: """初始化能力未支持异常。 参数: capability: 未实现的能力名称(如 ``validate_config``)。 adapter_type: 适配器类型,写入 ``details`` 供客户端参考。 details: 业务字段字典,与上述参数合并。 trace_id: 调用链路追踪 ID,用于跨链路关联。 """ merged: dict[str, Any] = dict(details or {}) if adapter_type is not None: merged["adapter_type"] = adapter_type merged["capability"] = capability adapter_desc = f" {adapter_type}" if adapter_type else "" super().__init__( f"适配器{adapter_desc}未实现能力: {capability}", details=merged, trace_id=trace_id, )