ForcePilot/backend/package/yuxi/channels/contract/errors/client.py
Kris b10cb080de refactor: 多模块代码优化与功能增强
本次提交包含多项代码改进与功能增强:
1. 新增服务账号禁用错误类型与状态映射
2. 优化出站管道阶段顺序与上下文字段
3. 完善会话与事务管理逻辑,修复并发冲突处理
4. 调整钉钉与微信插件的适配逻辑
5. 优化内容校验与限流降级策略
6. 新增渠道降级投递事件与围栏回滚机制
7. 修正配置schema与权限校验逻辑
8. 完善审计与监控相关的日志与钩子处理
2026-07-09 13:51:58 +08:00

243 lines
6.8 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.

"""客户端错误类型定义。
定义 ``ClientError`` 抽象基类及其 4 个具体子类,表示由调用方引起的错误
4xx 语义):参数校验失败、认证失败、资源不存在、权限不足。
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from yuxi.channels.contract.errors.base import Error
if TYPE_CHECKING:
from yuxi.channels.contract.errors.transport import TransportErrorCategory
class ClientError(Error):
"""客户端错误抽象基类。
表示由调用方引起的错误4xx 语义)。子类包括 ``ValidationError`` /
``AuthError`` / ``NotFoundError`` / ``PermissionDeniedError``。
"""
error_code = "CLIENT_ERROR"
class ValidationError(ClientError):
"""字段校验失败错误。"""
error_code = "VALIDATION_ERROR"
def __init__(
self,
field: str,
message: str,
*,
trace_id: str | None = None,
category_hint: TransportErrorCategory | None = None,
errors: list[str] | tuple[str, ...] | None = None,
id: str | None = None,
) -> None:
super().__init__(message, trace_id=trace_id, category_hint=category_hint)
self.field = field
self.errors = tuple(errors) if errors else ()
self.id = id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["field"] = self.field
if self.errors:
data["errors"] = list(self.errors)
if self.id is not None:
data["id"] = self.id
return data
class AuthError(ClientError):
"""认证失败错误。
调用方身份认证失败时抛出,如 token 无效、签名错误HTTP 401
"""
error_code = "AUTH_ERROR"
def __init__(
self,
reason: str,
*,
trace_id: str | None = None,
category_hint: TransportErrorCategory | None = None,
) -> None:
super().__init__(
f"Authentication failed: {reason}",
trace_id=trace_id,
category_hint=category_hint,
)
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["reason"] = self.reason
return data
class NotFoundError(ClientError):
"""资源不存在错误。
调用方请求的资源不存在时抛出HTTP 404
"""
error_code = "NOT_FOUND"
def __init__(
self,
resource: str,
id: str,
*,
trace_id: str | None = None,
category_hint: TransportErrorCategory | None = None,
) -> None:
super().__init__(
f"{resource} not found: {id}",
trace_id=trace_id,
category_hint=category_hint,
)
self.resource = resource
self.id = id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["resource"] = self.resource
data["id"] = self.id
return data
class PermissionDeniedError(ClientError):
"""权限不足错误。
调用方无权限执行请求的操作时抛出HTTP 403。命名为 ``PermissionDeniedError``
以避免与 Python 内置 ``PermissionError`` 冲突。
``category_hint`` 透传仅供 API 一致性:权限不足场景默认 ``None``,不参与
传输分类映射(属 4xx 客户端错误,翻译器按 ``error_code`` 映射 HTTP 403
"""
error_code = "PERMISSION_DENIED"
def __init__(
self,
reason: str,
*,
trace_id: str | None = None,
category_hint: TransportErrorCategory | None = None,
) -> None:
super().__init__(
f"Permission denied: {reason}",
trace_id=trace_id,
category_hint=category_hint,
)
self.reason = reason
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["reason"] = self.reason
return data
class ServiceAccountNotFoundError(NotFoundError):
"""渠道账户未绑定服务账号错误。
渠道账户未关联服务账号(``User.user_type='service'``时抛出HTTP 404
``details`` 包含 ``channel_type`` 与 ``account_id``,便于定位缺失绑定的
渠道账户。
"""
error_code = "CHANNEL_SERVICE_ACCOUNT_NOT_FOUND"
def __init__(
self,
channel_type: str,
account_id: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
"service_account",
f"{channel_type}:{account_id}",
trace_id=trace_id,
)
self.channel_type = channel_type
self.account_id = account_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["channel_type"] = self.channel_type
data["account_id"] = self.account_id
return data
class ChannelAccessDeniedError(PermissionDeniedError):
"""渠道访客无权访问 Agent 错误。
渠道访客在当前渠道访问级别下无权访问目标 Agent 时抛出HTTP 403
``details`` 包含 ``agent_slug``、``channel_access_level``,可选携带
``unified_identity_id`` 用于跨渠道身份关联排查。
"""
error_code = "CHANNEL_ACCESS_DENIED"
def __init__(
self,
agent_slug: str,
channel_access_level: str,
*,
unified_identity_id: str | None = None,
trace_id: str | None = None,
) -> None:
super().__init__(
f"access denied to agent '{agent_slug}' at level '{channel_access_level}'",
trace_id=trace_id,
)
self.agent_slug = agent_slug
self.channel_access_level = channel_access_level
self.unified_identity_id = unified_identity_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["agent_slug"] = self.agent_slug
data["channel_access_level"] = self.channel_access_level
if self.unified_identity_id is not None:
data["unified_identity_id"] = self.unified_identity_id
return data
class ServiceAccountDisabledError(ClientError):
"""服务账号已被禁用。
服务账号 ``status`` 为 ``disabled`` 时抛出HTTP 403。``details`` 包含
``channel_type`` 与 ``account_id``,便于定位被禁用的服务账号。
"""
error_code = "SERVICE_ACCOUNT_DISABLED"
def __init__(
self,
channel_type: str,
account_id: str,
*,
trace_id: str | None = None,
) -> None:
super().__init__(
f"service account disabled: {channel_type}:{account_id}",
trace_id=trace_id,
)
self.channel_type = channel_type
self.account_id = account_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
data["channel_type"] = self.channel_type
data["account_id"] = self.account_id
return data