ForcePilot/backend/package/yuxi/channels/contract/errors/client.py

243 lines
6.8 KiB
Python
Raw Normal View History

"""客户端错误类型定义。
定义 ``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