2026-07-02 03:22:12 +08:00
|
|
|
|
"""客户端错误类型定义。
|
|
|
|
|
|
|
|
|
|
|
|
定义 ``ClientError`` 抽象基类及其 4 个具体子类,表示由调用方引起的错误
|
|
|
|
|
|
(4xx 语义):参数校验失败、认证失败、资源不存在、权限不足。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
from typing import TYPE_CHECKING, Any
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.contract.errors.base import Error
|
|
|
|
|
|
|
2026-07-06 20:49:35 +08:00
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from yuxi.channels.contract.errors.transport import TransportErrorCategory
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
class ClientError(Error):
|
|
|
|
|
|
"""客户端错误抽象基类。
|
|
|
|
|
|
|
|
|
|
|
|
表示由调用方引起的错误(4xx 语义)。子类包括 ``ValidationError`` /
|
|
|
|
|
|
``AuthError`` / ``NotFoundError`` / ``PermissionDeniedError``。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
error_code = "CLIENT_ERROR"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:14:56 +08:00
|
|
|
|
class ValidationError(ClientError):
|
|
|
|
|
|
"""字段校验失败错误。"""
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
error_code = "VALIDATION_ERROR"
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
field: str,
|
|
|
|
|
|
message: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
2026-07-06 20:49:35 +08:00
|
|
|
|
category_hint: TransportErrorCategory | None = None,
|
|
|
|
|
|
errors: list[str] | tuple[str, ...] | None = None,
|
2026-07-07 16:20:40 +08:00
|
|
|
|
id: str | None = None,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
) -> None:
|
2026-07-06 20:49:35 +08:00
|
|
|
|
super().__init__(message, trace_id=trace_id, category_hint=category_hint)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
self.field = field
|
2026-07-06 20:49:35 +08:00
|
|
|
|
self.errors = tuple(errors) if errors else ()
|
2026-07-07 16:20:40 +08:00
|
|
|
|
self.id = id
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
|
|
|
|
data = super().to_dict()
|
|
|
|
|
|
data["field"] = self.field
|
2026-07-06 20:49:35 +08:00
|
|
|
|
if self.errors:
|
|
|
|
|
|
data["errors"] = list(self.errors)
|
2026-07-07 16:20:40 +08:00
|
|
|
|
if self.id is not None:
|
|
|
|
|
|
data["id"] = self.id
|
2026-07-02 03:22:12 +08:00
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AuthError(ClientError):
|
|
|
|
|
|
"""认证失败错误。
|
|
|
|
|
|
|
|
|
|
|
|
调用方身份认证失败时抛出,如 token 无效、签名错误(HTTP 401)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
error_code = "AUTH_ERROR"
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
reason: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
2026-07-06 20:49:35 +08:00
|
|
|
|
category_hint: TransportErrorCategory | None = None,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
) -> None:
|
2026-07-06 20:49:35 +08:00
|
|
|
|
super().__init__(
|
|
|
|
|
|
f"Authentication failed: {reason}",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
category_hint=category_hint,
|
|
|
|
|
|
)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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,
|
2026-07-06 20:49:35 +08:00
|
|
|
|
category_hint: TransportErrorCategory | None = None,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
) -> None:
|
2026-07-06 20:49:35 +08:00
|
|
|
|
super().__init__(
|
|
|
|
|
|
f"{resource} not found: {id}",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
category_hint=category_hint,
|
|
|
|
|
|
)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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`` 冲突。
|
2026-07-06 20:49:35 +08:00
|
|
|
|
|
|
|
|
|
|
``category_hint`` 透传仅供 API 一致性:权限不足场景默认 ``None``,不参与
|
|
|
|
|
|
传输分类映射(属 4xx 客户端错误,翻译器按 ``error_code`` 映射 HTTP 403)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
error_code = "PERMISSION_DENIED"
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
reason: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
trace_id: str | None = None,
|
2026-07-06 20:49:35 +08:00
|
|
|
|
category_hint: TransportErrorCategory | None = None,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
) -> None:
|
2026-07-06 20:49:35 +08:00
|
|
|
|
super().__init__(
|
|
|
|
|
|
f"Permission denied: {reason}",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
category_hint=category_hint,
|
|
|
|
|
|
)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
self.reason = reason
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
|
|
|
|
data = super().to_dict()
|
|
|
|
|
|
data["reason"] = self.reason
|
|
|
|
|
|
return data
|
2026-07-03 19:18:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-09 13:51:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|