ForcePilot/backend/package/yuxi/external_systems/exceptions.py
Kris 74709ba2d3 feat: 完成外部系统限界上下文核心代码实现
新增六边形架构核心代码包,包含:
1. 协议适配器层:HTTP/SMTP/IMAP/SSH/GRPC等多协议适配器实现
2. 认证插件体系:基础认证、API密钥、HMAC等多类型认证插件
3. 执行编排框架:工具执行器、上下文构建、运行时治理组件
4. 用例端口与DTO:定义领域服务端口与数据传输对象
5. 厂商集成包框架:支持第三方系统集成扩展
6. 基础设施装配层:实现依赖注入与服务装配

所有代码遵循六边形架构设计原则,实现端口与适配器解耦,支持动态扩展与自动发现。
2026-06-20 22:12:42 +08:00

350 lines
12 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 验签/处理失败
"""
from __future__ import annotations
from typing import Any
class ExternalSystemError(Exception):
"""外部系统限界上下文根异常。
所有本上下文抛出的异常都应继承此类,最外层统一捕获后映射为 HTTP 响应。
``status_code`` 作为异常到 HTTP 状态码的映射依据,子类按需覆盖。
"""
status_code: int = 500
def __init__(
self,
message: str,
*,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message)
self.message = message
self.details = details or {}
def __str__(self) -> str:
return self.message
# ─── 业务规则异常 ──────────────────────────────────────────────────────────
class DomainValidationError(ExternalSystemError, ValueError):
"""领域校验失败。
同时继承 ``ValueError`` 以兼容 Pydantic 的 ``field_validator`` 与 ``raise ValueError(...)``
习惯写法(在 dataclass 薄校验中可直接 ``raise DomainValidationError(...)``)。
"""
status_code = 400
class InvalidSlugError(DomainValidationError):
"""slug 非法或命中保留字。"""
class EnvironmentDisabledError(DomainValidationError):
"""禁用环境不能设为默认环境。"""
class ImportConflictError(DomainValidationError):
"""导入时发现 slug 冲突。"""
class SsrfViolationError(DomainValidationError):
"""SSRF 安全规则违反。"""
status_code = 403
class CircuitBreakerStateError(DomainValidationError):
"""熔断器当前状态不允许重置。
由 ``POST /circuit-breaker/reset`` 端点在熔断器状态为 ``closed`` 时抛出,
表示熔断器未处于打开或半开状态,无需也无法重置。
"""
class EntityNotFoundError(ExternalSystemError):
"""领域实体不存在。
统一覆盖原 ``ExternalSystemNotFoundError`` / ``ExternalSystemEnvironmentNotFoundError``
/ ``ExternalToolNotFoundError`` / ``ExternalAssetNotFoundError`` 四个异常。
具体实体类型由 ``message`` 描述,``details`` 可携带 ``entity_type`` / ``identifier``。
"""
status_code = 404
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
def __init__(self, references: dict[str, dict[str, list[str]]]) -> None:
self.references = references
super().__init__(
f"以下外部系统仍被引用,无法删除: {references}",
details={"references": references},
)
class ConflictError(ExternalSystemError):
"""资源冲突。
表示写操作因唯一约束冲突或状态竞争无法完成如版本号重复、slug 重复等)。
与 ``ReferencedError`` 的区别:本异常表示新建/更新资源时与既有资源冲突,
``ReferencedError`` 表示删除资源时被其他实体引用。
"""
status_code = 409
# ─── 框架技术异常 ──────────────────────────────────────────────────────────
class FrameworkError(ExternalSystemError):
"""框架层技术异常基类。
覆盖适配器查找、认证插件查找、执行、密钥解析、认证失败等技术场景。
与 ``DomainValidationError`` 的区别:本类描述"技术层面无法完成操作"
而非"业务规则不允许"
"""
class AdapterNotFoundError(FrameworkError):
"""找不到对应 ``adapter_type`` 的适配器。
同时覆盖原 ``IntegrationNotFoundError``(厂商集成找不到的情况合并到此处)。
"""
status_code = 404
class AuthPluginNotFoundError(FrameworkError):
"""找不到对应 ``auth_type`` 的认证插件。"""
status_code = 404
class IntegrationOperationNotRegisteredError(FrameworkError):
"""厂商操作未注册。
由 ``IntegrationOperationRegistry`` 在 ``(operation, source_type)``
未注册对应 handler 时抛出PRD FR-08
与 ``AdapterNotFoundError`` 的区别:本异常表示厂商集成已找到,
但不支持指定的操作(如 salesforce 不支持 ``discover`` 操作的
``openapi`` source_type``AdapterNotFoundError`` 表示适配器类型本身不存在。
"""
status_code = 400
class ExecutionError(FrameworkError):
"""执行阶段通用异常。
覆盖原 ``ExternalSystemExecutionError``,表示适配器执行过程中发生的非业务异常
(网络错误、协议错误、目标系统返回 5xx 等)。
"""
status_code = 502
class RateLimitExceededError(ExecutionError):
"""超出限流阈值。
由 ``RateLimiter`` 在令牌桶耗尽时抛出,对应原 ``ExternalSystemThrottledError``。
结构化字段(写入 ``details``
- ``retry_after``: 建议客户端等待秒数(令牌恢复时间)。
- ``limit_type``: 限流类型,``qps`` 或 ``concurrency``。
- ``system_id`` / ``env_key``: 限流维度。
"""
status_code = 429
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,
) -> None:
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)
class CircuitOpenError(ExecutionError):
"""熔断器打开。
由 ``CircuitBreaker`` 在熔断状态打开时抛出,对应原 ``ExternalSystemCircuitOpenError``。
结构化字段(写入 ``details``
- ``retry_after``: 剩余冷却秒数(熔断器进入 half_open 的等待时间)。
- ``system_id`` / ``env_key``: 熔断维度。
"""
status_code = 503
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,
) -> None:
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)
class SecretResolutionError(FrameworkError):
"""密钥解析失败。
由 ``SecretResolver`` 在 ``secret_refs`` 引用的密钥不存在或无权访问时抛出,
对应原 ``ExternalSystemSecretError``。
"""
status_code = 400
class AuthError(FrameworkError):
"""认证失败。
由 ``TokenManager`` / 认证插件在获取/刷新 token 失败时抛出,
对应原 ``ExternalSystemAuthError``。
"""
status_code = 403
class QuotaExceededError(ExecutionError):
"""配额耗尽。
由 ``QuotaManager`` 在调用前预检发现配额已耗尽时抛出。
与 ``RateLimitExceededError`` 同属执行节流类,但语义不同:
限流是本地令牌桶节流,配额是远端系统配额耗尽。
结构化字段(写入 ``details``
- ``quota_key``: 配额键。
- ``limit_value``: 配额上限。
- ``used_value``: 已用配额。
- ``remaining``: 剩余配额(通常为 0
- ``system_id`` / ``env_key``: 配额维度。
"""
status_code = 429
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,
) -> None:
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)
class AccessDeniedError(FrameworkError):
"""访问规则拒绝。
由 ``AccessController`` 在执行前评估访问控制规则时,
命中 deny 规则时抛出。与 ``AuthError`` 的区别:
AuthError 是认证失败token 无效AccessDeniedError 是认证通过但无权限。
"""
status_code = 403
class WebhookError(FrameworkError):
"""Webhook 验签或处理失败。
由 ``WebhookVerifier`` 在验签失败、订阅不存在、密钥解密失败等场景抛出。
"""
status_code = 400