ForcePilot/backend/package/yuxi/external_systems/exceptions.py
Kris 1965b9f742 refactor(external_systems): 优化时间参数处理与新增异常能力
1. 将工具、webhook的时间DTO字段从字符串改为datetime类型
2. 移除工具服务中的手动ISO时间解析逻辑
3. 新增CapabilityNotSupportedError异常与get_integration_or_raise方法
4. 格式化代码行内表达式简化写法
2026-07-03 19:18:32 +08:00

495 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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