本次提交包含多维度代码优化与功能增强: 1. 移除报告模块冗余导入与枚举,清理报表相关代码 2. 新增扫码登录支持方法与飞书适配器适配 3. 完善异常日志与健康检查信息 4. 扩展目录、配对管理、能力查询等接口 5. 优化出站管道与事务提交后钩子逻辑 6. 修复飞书消息解析与响应空值问题 7. 重构配置更新与服务账号创建逻辑 8. 统一传输错误分类契约与错误基类扩展
236 lines
7.5 KiB
Python
236 lines
7.5 KiB
Python
"""服务端错误类型定义。
|
||
|
||
定义 ``ServerError`` 抽象基类及其 4 个具体子类,表示由服务端引起的错误
|
||
(5xx 语义):内部错误、依赖故障、超时、未实现。``ServerError`` 额外携带
|
||
``cause`` 字段记录原始错误,便于追踪。
|
||
"""
|
||
|
||
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 ServerError(Error):
|
||
"""服务端错误抽象基类。
|
||
|
||
表示由服务端引起的错误(5xx 语义)。额外携带 ``cause`` 字段记录原始错误,
|
||
便于跨链路追踪。子类包括 ``InternalError`` / ``DependencyError`` /
|
||
``TimeoutError``。
|
||
"""
|
||
|
||
error_code = "SERVER_ERROR"
|
||
|
||
def __init__(
|
||
self,
|
||
message: str,
|
||
*,
|
||
trace_id: str | None = None,
|
||
cause: Error | Exception | None = None,
|
||
category_hint: TransportErrorCategory | None = None,
|
||
) -> None:
|
||
super().__init__(message, trace_id=trace_id, category_hint=category_hint)
|
||
self.cause = cause
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
data = super().to_dict()
|
||
if self.cause is None:
|
||
data["cause"] = None
|
||
elif isinstance(self.cause, Error):
|
||
data["cause"] = self.cause.to_dict()
|
||
else:
|
||
# 原生 Exception 无 to_dict(),仅记录类型与信息以便追踪
|
||
data["cause"] = {
|
||
"type": type(self.cause).__name__,
|
||
"message": str(self.cause),
|
||
}
|
||
return data
|
||
|
||
|
||
class InternalError(ServerError):
|
||
"""内部错误。
|
||
|
||
服务端内部异常导致的错误(HTTP 500)。``cause`` 可选:当原始错误为
|
||
``Error`` 子类时传入以保留结构化错误链;当原始错误为原生 ``Exception``
|
||
时可不传 ``cause``,通过 ``message`` 携带异常信息。
|
||
|
||
``category_hint`` 透传仅供 API 一致性:内部错误默认 ``None``(翻译器按
|
||
``isinstance`` 映射为 ``permanent``),透传仅供显式覆盖默认分类。
|
||
"""
|
||
|
||
error_code = "INTERNAL"
|
||
|
||
def __init__(
|
||
self,
|
||
cause: Error | Exception | None = None,
|
||
*,
|
||
trace_id: str | None = None,
|
||
message: str | None = None,
|
||
category_hint: TransportErrorCategory | None = None,
|
||
) -> None:
|
||
super().__init__(
|
||
message or "Internal error",
|
||
trace_id=trace_id,
|
||
cause=cause,
|
||
category_hint=category_hint,
|
||
)
|
||
|
||
|
||
class DependencyError(ServerError):
|
||
"""依赖故障错误。
|
||
|
||
外部依赖(如数据库、Redis、第三方服务)故障时抛出(HTTP 502)。
|
||
"""
|
||
|
||
error_code = "DEPENDENCY"
|
||
|
||
def __init__(
|
||
self,
|
||
dep: str,
|
||
cause: Error | Exception,
|
||
*,
|
||
message: str | None = None,
|
||
trace_id: str | None = None,
|
||
category_hint: TransportErrorCategory | None = None,
|
||
) -> None:
|
||
super().__init__(
|
||
message or f"Dependency {dep} failed",
|
||
trace_id=trace_id,
|
||
cause=cause,
|
||
category_hint=category_hint,
|
||
)
|
||
self.dep = dep
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
data = super().to_dict()
|
||
data["dep"] = self.dep
|
||
return data
|
||
|
||
|
||
class OperationTimeoutError(ServerError):
|
||
"""操作超时错误。
|
||
|
||
操作超时时抛出(HTTP 504,error_code=``TIMEOUT``)。命名为
|
||
``OperationTimeoutError`` 以避免与 Python 内置 ``TimeoutError`` 冲突,
|
||
遵循 ``PermissionDeniedError`` 的同一命名约定。
|
||
"""
|
||
|
||
error_code = "TIMEOUT"
|
||
|
||
def __init__(
|
||
self,
|
||
timeout_ms: int,
|
||
*,
|
||
message: str | None = None,
|
||
trace_id: str | None = None,
|
||
category_hint: TransportErrorCategory | None = None,
|
||
) -> None:
|
||
"""初始化超时错误。
|
||
|
||
Args:
|
||
timeout_ms: 超时毫秒数,写入 ``details`` 供客户端参考。
|
||
message: 自定义错误信息。为 ``None`` 时使用默认模板
|
||
``"Operation timed out after {timeout_ms}ms"``。
|
||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||
category_hint: 传输分类建议,供翻译器读取(F-06)。
|
||
"""
|
||
super().__init__(
|
||
message or f"Operation timed out after {timeout_ms}ms",
|
||
trace_id=trace_id,
|
||
category_hint=category_hint,
|
||
)
|
||
self.timeout_ms = timeout_ms
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
data = super().to_dict()
|
||
data["timeout_ms"] = self.timeout_ms
|
||
return data
|
||
|
||
|
||
class NotImplementedError(ServerError):
|
||
"""未实现错误。
|
||
|
||
请求的操作尚未实现时抛出(HTTP 501)。用于标记尚未落地的功能分支,
|
||
避免静默返回空结果掩盖功能缺失。
|
||
|
||
不透传 ``category_hint``:未实现属功能缺失语义,不涉及传输分类映射
|
||
(翻译器按 ``error_code`` 映射 HTTP 501),无需显式覆盖传输分类。
|
||
"""
|
||
|
||
error_code = "NOT_IMPLEMENTED"
|
||
|
||
def __init__(
|
||
self,
|
||
operation: str,
|
||
*,
|
||
message: str | None = None,
|
||
trace_id: str | None = None,
|
||
details: dict[str, Any] | None = None,
|
||
) -> None:
|
||
"""初始化未实现错误。
|
||
|
||
Args:
|
||
operation: 未实现的操作名称,写入 ``details`` 供客户端参考。
|
||
message: 自定义错误信息。为 ``None`` 时使用默认模板
|
||
``"Operation not implemented: {operation}"``。
|
||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||
details: 额外业务字段,如 ``{"reason": "directory_disabled"}``,
|
||
用于客户端区分同一 HTTP 状态码下的不同语义。
|
||
"""
|
||
super().__init__(
|
||
message or f"Operation not implemented: {operation}",
|
||
trace_id=trace_id,
|
||
)
|
||
self.operation = operation
|
||
self.extra_details = details or {}
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
data = super().to_dict()
|
||
data["operation"] = self.operation
|
||
data.update(self.extra_details)
|
||
return data
|
||
|
||
|
||
class ServiceAccountCreationError(InternalError):
|
||
"""服务账号创建失败错误。
|
||
|
||
为渠道账户自动创建服务账号(``User.user_type='service'``)失败时抛出
|
||
(HTTP 500)。``details`` 包含 ``channel_type``、``account_id`` 与
|
||
``reason``,便于定位创建失败的根因。
|
||
|
||
适配器层抛出时应使用 ``raise ServiceAccountCreationError(...) from exc``
|
||
保留原始异常 traceback,原始异常通过 ``cause`` 参数传递至
|
||
``InternalError.cause`` 字段。
|
||
"""
|
||
|
||
error_code = "CHANNEL_SERVICE_ACCOUNT_CREATE_FAILED"
|
||
|
||
def __init__(
|
||
self,
|
||
channel_type: str,
|
||
account_id: str,
|
||
reason: str,
|
||
*,
|
||
cause: Error | Exception | None = None,
|
||
trace_id: str | None = None,
|
||
) -> None:
|
||
super().__init__(
|
||
cause=cause,
|
||
trace_id=trace_id,
|
||
message=f"failed to create service account for {channel_type}:{account_id}: {reason}",
|
||
)
|
||
self.channel_type = channel_type
|
||
self.account_id = account_id
|
||
self.reason = reason
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
data = super().to_dict()
|
||
data["channel_type"] = self.channel_type
|
||
data["account_id"] = self.account_id
|
||
data["reason"] = self.reason
|
||
return data
|