本次提交包含了大量跨模块的功能增强、bug修复与代码优化: 1. 清理冗余空行与导入格式,统一代码风格 2. 新增集成超时异常类,替换原生TimeoutError避免穿透核心层 3. 扩展审计日志DTO与用例,新增变更字段追踪能力 4. 完善各类仓储接口,新增批量操作、时间范围过滤支持 5. 重构健康检查返回格式,统一使用status字段替代冗余的reachable/healthy 6. 扩展仪表盘与各类服务端口,新增待办统计、系统dashboard等能力 7. 优化批量操作与事务处理,新增保存点支持隔离失败操作 8. 修复配额管理阈值计算逻辑,支持自定义告警阈值 9. 统一认证类型错误提示,优化插件注册校验逻辑 10. 扩展SOAP适配器预览能力,兼容bytes类型输入 11. 新增批量恢复、批量硬删除回收站资源的接口与DTO 12. 优化测试用例断言逻辑,兼容前端操作符别名与字段名差异
506 lines
18 KiB
Python
506 lines
18 KiB
Python
"""外部系统限界上下文的异常层次。
|
||
|
||
设计原则(见设计方案 §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 IntegrationTimeoutError(FrameworkError):
|
||
"""厂商集成相关操作超时。
|
||
|
||
用于替换 Python 原生 ``TimeoutError``,避免原生异常穿透到核心层。
|
||
由持久化、发现、Schema 获取等可能阻塞或网络操作的调用点捕获并转换。
|
||
"""
|
||
|
||
status_code = 504
|
||
error_code = "INTEGRATION_TIMEOUT"
|
||
|
||
|
||
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,
|
||
)
|