439 lines
11 KiB
Python
439 lines
11 KiB
Python
"""
|
|
统一错误处理模块
|
|
|
|
提供:
|
|
- 自定义异常类层次结构
|
|
- 错误包装和转换
|
|
- 用户友好的错误消息
|
|
- 错误日志和追踪
|
|
|
|
使用示例:
|
|
from utils.errors import PluginError, ToolExecutionError, handle_error
|
|
|
|
try:
|
|
await some_operation()
|
|
except Exception as e:
|
|
result = handle_error(e, context="执行工具")
|
|
# result = {"success": False, "error": "...", "error_type": "..."}
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import traceback
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Dict, Optional, Type
|
|
|
|
from loguru import logger
|
|
|
|
|
|
# ==================== 错误类型枚举 ====================
|
|
|
|
class ErrorType(Enum):
|
|
"""错误类型分类"""
|
|
UNKNOWN = "unknown"
|
|
PLUGIN = "plugin"
|
|
TOOL = "tool"
|
|
CONFIG = "config"
|
|
NETWORK = "network"
|
|
TIMEOUT = "timeout"
|
|
VALIDATION = "validation"
|
|
PERMISSION = "permission"
|
|
RESOURCE = "resource"
|
|
|
|
|
|
# ==================== 自定义异常基类 ====================
|
|
|
|
class BotError(Exception):
|
|
"""机器人错误基类"""
|
|
|
|
error_type: ErrorType = ErrorType.UNKNOWN
|
|
user_message: str = "发生了一个错误"
|
|
log_level: str = "error"
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
user_message: str = None,
|
|
cause: Exception = None,
|
|
context: Dict[str, Any] = None,
|
|
):
|
|
super().__init__(message)
|
|
self.message = message
|
|
self._user_message = user_message
|
|
self.cause = cause
|
|
self.context = context or {}
|
|
|
|
def get_user_message(self) -> str:
|
|
"""获取用户友好的错误消息"""
|
|
return self._user_message or self.user_message
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""转换为字典(用于 API 响应)"""
|
|
return {
|
|
"success": False,
|
|
"error": self.get_user_message(),
|
|
"error_type": self.error_type.value,
|
|
"details": self.message if self.message != self.get_user_message() else None,
|
|
}
|
|
|
|
|
|
# ==================== 具体异常类 ====================
|
|
|
|
class PluginError(BotError):
|
|
"""插件相关错误"""
|
|
error_type = ErrorType.PLUGIN
|
|
user_message = "插件执行出错"
|
|
|
|
|
|
class PluginLoadError(PluginError):
|
|
"""插件加载错误"""
|
|
user_message = "插件加载失败"
|
|
|
|
|
|
class PluginNotFoundError(PluginError):
|
|
"""插件未找到"""
|
|
user_message = "找不到指定的插件"
|
|
|
|
|
|
class ToolExecutionError(BotError):
|
|
"""工具执行错误"""
|
|
error_type = ErrorType.TOOL
|
|
user_message = "工具执行失败"
|
|
|
|
|
|
class ToolNotFoundError(ToolExecutionError):
|
|
"""工具未找到"""
|
|
user_message = "找不到指定的工具"
|
|
|
|
|
|
class ToolTimeoutError(ToolExecutionError):
|
|
"""工具执行超时"""
|
|
error_type = ErrorType.TIMEOUT
|
|
user_message = "工具执行超时"
|
|
|
|
|
|
class ConfigError(BotError):
|
|
"""配置相关错误"""
|
|
error_type = ErrorType.CONFIG
|
|
user_message = "配置错误"
|
|
|
|
|
|
class ConfigNotFoundError(ConfigError):
|
|
"""配置项未找到"""
|
|
user_message = "找不到配置项"
|
|
|
|
|
|
class ConfigValidationError(ConfigError):
|
|
"""配置验证错误"""
|
|
error_type = ErrorType.VALIDATION
|
|
user_message = "配置格式不正确"
|
|
|
|
|
|
class NetworkError(BotError):
|
|
"""网络相关错误"""
|
|
error_type = ErrorType.NETWORK
|
|
user_message = "网络请求失败"
|
|
|
|
|
|
class APIError(NetworkError):
|
|
"""API 调用错误"""
|
|
user_message = "API 调用失败"
|
|
|
|
|
|
class ValidationError(BotError):
|
|
"""验证错误"""
|
|
error_type = ErrorType.VALIDATION
|
|
user_message = "参数验证失败"
|
|
|
|
|
|
class PermissionError(BotError):
|
|
"""权限错误"""
|
|
error_type = ErrorType.PERMISSION
|
|
user_message = "没有权限执行此操作"
|
|
|
|
|
|
class ResourceError(BotError):
|
|
"""资源错误(内存、文件等)"""
|
|
error_type = ErrorType.RESOURCE
|
|
user_message = "资源访问失败"
|
|
|
|
|
|
# ==================== 错误处理工具函数 ====================
|
|
|
|
@dataclass
|
|
class ErrorResult:
|
|
"""错误处理结果"""
|
|
success: bool = False
|
|
error: str = ""
|
|
error_type: str = "unknown"
|
|
details: Optional[str] = None
|
|
logged: bool = False
|
|
original_exception: Optional[Exception] = field(default=None, repr=False)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""转换为字典"""
|
|
result = {
|
|
"success": self.success,
|
|
"error": self.error,
|
|
"error_type": self.error_type,
|
|
}
|
|
if self.details:
|
|
result["details"] = self.details
|
|
return result
|
|
|
|
|
|
def handle_error(
|
|
exception: Exception,
|
|
context: str = "",
|
|
log: bool = True,
|
|
include_traceback: bool = False,
|
|
) -> ErrorResult:
|
|
"""
|
|
统一错误处理函数
|
|
|
|
Args:
|
|
exception: 捕获的异常
|
|
context: 错误上下文描述
|
|
log: 是否记录日志
|
|
include_traceback: 是否包含完整堆栈
|
|
|
|
Returns:
|
|
ErrorResult 对象
|
|
"""
|
|
# 处理自定义异常
|
|
if isinstance(exception, BotError):
|
|
result = ErrorResult(
|
|
success=False,
|
|
error=exception.get_user_message(),
|
|
error_type=exception.error_type.value,
|
|
details=exception.message if exception.message != exception.get_user_message() else None,
|
|
original_exception=exception,
|
|
)
|
|
if log:
|
|
log_func = getattr(logger, exception.log_level, logger.error)
|
|
log_func(f"[{context}] {exception.error_type.value}: {exception.message}")
|
|
result.logged = True
|
|
return result
|
|
|
|
# 处理标准超时异常
|
|
import asyncio
|
|
if isinstance(exception, asyncio.TimeoutError):
|
|
result = ErrorResult(
|
|
success=False,
|
|
error="操作超时",
|
|
error_type=ErrorType.TIMEOUT.value,
|
|
original_exception=exception,
|
|
)
|
|
if log:
|
|
logger.warning(f"[{context}] 超时: {exception}")
|
|
result.logged = True
|
|
return result
|
|
|
|
# 处理连接错误
|
|
if isinstance(exception, (ConnectionError, OSError)):
|
|
result = ErrorResult(
|
|
success=False,
|
|
error="网络连接失败",
|
|
error_type=ErrorType.NETWORK.value,
|
|
details=str(exception),
|
|
original_exception=exception,
|
|
)
|
|
if log:
|
|
logger.error(f"[{context}] 网络错误: {exception}")
|
|
result.logged = True
|
|
return result
|
|
|
|
# 处理验证错误
|
|
if isinstance(exception, (ValueError, TypeError)):
|
|
result = ErrorResult(
|
|
success=False,
|
|
error="参数错误",
|
|
error_type=ErrorType.VALIDATION.value,
|
|
details=str(exception),
|
|
original_exception=exception,
|
|
)
|
|
if log:
|
|
logger.warning(f"[{context}] 验证错误: {exception}")
|
|
result.logged = True
|
|
return result
|
|
|
|
# 处理未知错误
|
|
error_msg = str(exception) or exception.__class__.__name__
|
|
details = None
|
|
if include_traceback:
|
|
details = traceback.format_exc()
|
|
|
|
result = ErrorResult(
|
|
success=False,
|
|
error=f"发生错误: {error_msg[:100]}",
|
|
error_type=ErrorType.UNKNOWN.value,
|
|
details=details,
|
|
original_exception=exception,
|
|
)
|
|
|
|
if log:
|
|
logger.error(f"[{context}] 未知错误: {exception}")
|
|
if include_traceback:
|
|
logger.debug(traceback.format_exc())
|
|
result.logged = True
|
|
|
|
return result
|
|
|
|
|
|
def wrap_error(
|
|
exception: Exception,
|
|
error_class: Type[BotError],
|
|
message: str = None,
|
|
user_message: str = None,
|
|
) -> BotError:
|
|
"""
|
|
将标准异常包装为自定义异常
|
|
|
|
Args:
|
|
exception: 原始异常
|
|
error_class: 目标异常类
|
|
message: 错误消息
|
|
user_message: 用户友好消息
|
|
|
|
Returns:
|
|
包装后的 BotError 子类实例
|
|
"""
|
|
msg = message or str(exception)
|
|
return error_class(
|
|
message=msg,
|
|
user_message=user_message,
|
|
cause=exception,
|
|
)
|
|
|
|
|
|
def safe_error_message(exception: Exception, max_length: int = 200) -> str:
|
|
"""
|
|
获取安全的错误消息(截断过长内容,移除敏感信息)
|
|
|
|
Args:
|
|
exception: 异常对象
|
|
max_length: 最大长度
|
|
|
|
Returns:
|
|
安全的错误消息字符串
|
|
"""
|
|
msg = str(exception)
|
|
|
|
# 移除可能的敏感信息模式
|
|
sensitive_patterns = [
|
|
r'api[_-]?key[=:]\s*\S+',
|
|
r'password[=:]\s*\S+',
|
|
r'token[=:]\s*\S+',
|
|
r'secret[=:]\s*\S+',
|
|
]
|
|
|
|
import re
|
|
for pattern in sensitive_patterns:
|
|
msg = re.sub(pattern, '[REDACTED]', msg, flags=re.IGNORECASE)
|
|
|
|
# 截断
|
|
if len(msg) > max_length:
|
|
msg = msg[:max_length] + "..."
|
|
|
|
return msg
|
|
|
|
|
|
# ==================== 装饰器 ====================
|
|
|
|
def catch_errors(
|
|
error_class: Type[BotError] = BotError,
|
|
context: str = "",
|
|
log: bool = True,
|
|
reraise: bool = False,
|
|
):
|
|
"""
|
|
错误捕获装饰器
|
|
|
|
Args:
|
|
error_class: 转换为的错误类
|
|
context: 上下文描述
|
|
log: 是否记录日志
|
|
reraise: 是否重新抛出
|
|
|
|
Usage:
|
|
@catch_errors(ToolExecutionError, context="执行工具")
|
|
async def my_tool():
|
|
...
|
|
"""
|
|
def decorator(func):
|
|
import asyncio
|
|
import functools
|
|
|
|
@functools.wraps(func)
|
|
async def async_wrapper(*args, **kwargs):
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
except BotError:
|
|
if reraise:
|
|
raise
|
|
return None
|
|
except Exception as e:
|
|
ctx = context or func.__name__
|
|
handle_error(e, context=ctx, log=log)
|
|
if reraise:
|
|
raise wrap_error(e, error_class) from e
|
|
return None
|
|
|
|
@functools.wraps(func)
|
|
def sync_wrapper(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except BotError:
|
|
if reraise:
|
|
raise
|
|
return None
|
|
except Exception as e:
|
|
ctx = context or func.__name__
|
|
handle_error(e, context=ctx, log=log)
|
|
if reraise:
|
|
raise wrap_error(e, error_class) from e
|
|
return None
|
|
|
|
if asyncio.iscoroutinefunction(func):
|
|
return async_wrapper
|
|
return sync_wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
# ==================== 导出列表 ====================
|
|
|
|
__all__ = [
|
|
# 枚举
|
|
'ErrorType',
|
|
# 异常基类
|
|
'BotError',
|
|
# 插件异常
|
|
'PluginError',
|
|
'PluginLoadError',
|
|
'PluginNotFoundError',
|
|
# 工具异常
|
|
'ToolExecutionError',
|
|
'ToolNotFoundError',
|
|
'ToolTimeoutError',
|
|
# 配置异常
|
|
'ConfigError',
|
|
'ConfigNotFoundError',
|
|
'ConfigValidationError',
|
|
# 网络异常
|
|
'NetworkError',
|
|
'APIError',
|
|
# 其他异常
|
|
'ValidationError',
|
|
'PermissionError',
|
|
'ResourceError',
|
|
# 工具函数
|
|
'ErrorResult',
|
|
'handle_error',
|
|
'wrap_error',
|
|
'safe_error_message',
|
|
# 装饰器
|
|
'catch_errors',
|
|
]
|