ForcePilot/backend/package/yuxi/external_systems/exceptions.py

495 lines
17 KiB
Python
Raw Normal View History

"""外部系统限界上下文的异常层次。
设计原则见设计方案 §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,
)