diff --git a/check_syntax.py b/check_syntax.py new file mode 100644 index 0000000..2098eab --- /dev/null +++ b/check_syntax.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +检查项目中所有Python文件的语法 +""" +import os +import sys + +def check_python_syntax(file_path): + """ + 检查单个Python文件的语法 + + Args: + file_path: Python文件路径 + + Returns: + bool: 如果语法正确返回True,否则返回False + """ + try: + with open(file_path, 'rb') as f: + code = f.read() + + # 使用compile函数检查语法 + compile(code, file_path, 'exec') + return True + except SyntaxError as e: + print(f"语法错误: {file_path}:{e.lineno}:{e.offset}: {e.msg}") + return False + except Exception as e: + print(f"无法检查文件 {file_path}: {e}") + return False + +def main(): + """ + 检查项目中所有Python文件的语法 + """ + # 要检查的目录 + directories = ['core', 'models', 'plugins', 'scripts', 'tests'] + + # 要检查的单独文件 + files = ['main.py', 'profile_main.py', 'test_performance_simple.py', 'setup_mypyc.py'] + + error_count = 0 + file_count = 0 + + # 检查目录中的所有Python文件 + for directory in directories: + for root, _, filenames in os.walk(directory): + for filename in filenames: + if filename.endswith('.py'): + file_path = os.path.join(root, filename) + file_count += 1 + if not check_python_syntax(file_path): + error_count += 1 + + # 检查单独的Python文件 + for file in files: + if os.path.exists(file): + file_count += 1 + if not check_python_syntax(file): + error_count += 1 + + print(f"\n检查完成: {file_count} 个文件,{error_count} 个语法错误") + + if error_count > 0: + sys.exit(1) + else: + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/core/config_loader.py b/core/config_loader.py index 92fa18c..948beca 100644 --- a/core/config_loader.py +++ b/core/config_loader.py @@ -8,7 +8,9 @@ from pathlib import Path import tomllib from pydantic import ValidationError from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel -from .utils.logger import logger +from .utils.logger import logger, ModuleLogger +from .utils.exceptions import ConfigError, ConfigNotFoundError, ConfigValidationError +from .utils.error_codes import ErrorCode, create_error_response class Config: @@ -24,36 +26,66 @@ class Config: """ self.path = Path(file_path) self._model: ConfigModel + # 创建模块专用日志记录器 + self.logger = ModuleLogger("ConfigLoader") self.load() def load(self): """ 加载并验证配置文件 - :raises FileNotFoundError: 如果配置文件不存在 - :raises ValidationError: 如果配置格式不正确 + :raises ConfigNotFoundError: 如果配置文件不存在 + :raises ConfigValidationError: 如果配置格式不正确 + :raises ConfigError: 如果加载配置时发生其他错误 """ if not self.path.exists(): - logger.error(f"配置文件 {self.path} 未找到!") - raise FileNotFoundError(f"配置文件 {self.path} 未找到!") + error = ConfigNotFoundError(message=f"配置文件 {self.path} 未找到!") + self.logger.error(f"配置加载失败: {error.message}") + self.logger.log_custom_exception(error) + raise error try: - logger.info(f"正在从 {self.path} 加载配置...") + self.logger.info(f"正在从 {self.path} 加载配置...") with open(self.path, "rb") as f: raw_config = tomllib.load(f) self._model = ConfigModel(**raw_config) - logger.success("配置加载并验证成功!") + self.logger.success("配置加载并验证成功!") except ValidationError as e: - logger.error("配置验证失败,请检查 `config.toml` 文件中的以下错误:") + error_details = [] for error in e.errors(): field = " -> ".join(map(str, error["loc"])) - logger.error(f" - 字段 '{field}': {error['msg']}") - raise + error_msg = f"字段 '{field}': {error['msg']}" + error_details.append(error_msg) + + validation_error = ConfigValidationError( + message="配置验证失败", + original_error=e + ) + + self.logger.error("配置验证失败,请检查 `config.toml` 文件中的以下错误:") + for detail in error_details: + self.logger.error(f" - {detail}") + + self.logger.log_custom_exception(validation_error) + raise validation_error + except tomllib.TOMLDecodeError as e: + error = ConfigError( + message=f"TOML解析错误: {str(e)}", + original_error=e + ) + self.logger.error(f"加载配置文件时发生TOML解析错误: {error.message}") + self.logger.log_custom_exception(error) + raise error except Exception as e: - logger.exception(f"加载配置文件时发生未知错误: {e}") - raise + error = ConfigError( + message=f"加载配置文件时发生未知错误: {str(e)}", + original_error=e + ) + self.logger.exception(f"加载配置文件时发生未知错误: {error.message}") + self.logger.log_custom_exception(error) + raise error # 通过属性访问配置 @property diff --git a/core/managers/plugin_manager.py b/core/managers/plugin_manager.py index 25f2c3b..319e571 100644 --- a/core/managers/plugin_manager.py +++ b/core/managers/plugin_manager.py @@ -10,8 +10,9 @@ import sys from typing import Set from .command_manager import CommandManager -from ..utils.exceptions import SyncHandlerError -from ..utils.logger import logger +from ..utils.exceptions import SyncHandlerError, PluginError, PluginLoadError, PluginReloadError, PluginNotFoundError +from ..utils.logger import logger, ModuleLogger +from ..utils.error_codes import ErrorCode, create_error_response # 确保logger在模块级别可见 __all__ = ['PluginManager', 'logger'] @@ -29,6 +30,8 @@ class PluginManager: """ self.command_manager = command_manager self.loaded_plugins: Set[str] = set() + # 创建模块专用日志记录器 + self.logger = ModuleLogger("PluginManager") def load_all_plugins(self) -> None: """ @@ -45,10 +48,10 @@ class PluginManager: package_name = "plugins" if not os.path.exists(plugin_dir): - logger.error(f"插件目录不存在: {plugin_dir}") + self.logger.error(f"插件目录不存在: {plugin_dir}") return - logger.info(f"正在从 {package_name} 加载插件 (路径: {plugin_dir})...") + self.logger.info(f"正在从 {package_name} 加载插件 (路径: {plugin_dir})...") for _, module_name, is_pkg in pkgutil.iter_modules([plugin_dir]): full_module_name = f"{package_name}.{module_name}" @@ -70,23 +73,38 @@ class PluginManager: self.loaded_plugins.add(full_module_name) type_str = "包" if is_pkg else "文件" - logger.success(f" [{type_str}] 成功{action}: {module_name}") + self.logger.success(f" [{type_str}] 成功{action}: {module_name}") except SyncHandlerError as e: - logger.error(f" 插件 {module_name} 加载失败: {e} (跳过此插件)") - except Exception as e: - logger.exception( - f" 加载插件 {module_name} 失败: {e}" + error = PluginLoadError( + plugin_name=module_name, + message=f"同步处理器错误: {str(e)}", + original_error=e ) + self.logger.error(f" 插件 {module_name} 加载失败: {error.message} (跳过此插件)") + self.logger.log_custom_exception(error) + except Exception as e: + error = PluginLoadError( + plugin_name=module_name, + message=f"未知错误: {str(e)}", + original_error=e + ) + self.logger.exception(f" 加载插件 {module_name} 失败: {error.message}") + self.logger.log_custom_exception(error) def reload_plugin(self, full_module_name: str) -> None: """ 精确重载单个插件。 """ if full_module_name not in self.loaded_plugins: - logger.warning(f"尝试重载一个未被加载的插件: {full_module_name},将按首次加载处理。") + self.logger.warning(f"尝试重载一个未被加载的插件: {full_module_name},将按首次加载处理。") if full_module_name not in sys.modules: - logger.error(f"重载失败: 模块 {full_module_name} 未在 sys.modules 中找到。") + error = PluginNotFoundError( + plugin_name=full_module_name, + message="模块未在sys.modules中找到" + ) + self.logger.error(f"重载失败: {error.message}") + self.logger.log_custom_exception(error) return try: @@ -97,6 +115,20 @@ class PluginManager: meta = getattr(module, "__plugin_meta__") self.command_manager.plugins[full_module_name] = meta - logger.success(f"插件 {full_module_name} 已成功重载。") + self.logger.success(f"插件 {full_module_name} 已成功重载。") + except SyncHandlerError as e: + error = PluginReloadError( + plugin_name=full_module_name, + message=f"同步处理器错误: {str(e)}", + original_error=e + ) + self.logger.error(f"重载插件 {full_module_name} 失败: {error.message}") + self.logger.log_custom_exception(error) except Exception as e: - logger.exception(f"重载插件 {full_module_name} 时发生错误: {e}") + error = PluginReloadError( + plugin_name=full_module_name, + message=f"未知错误: {str(e)}", + original_error=e + ) + self.logger.exception(f"重载插件 {full_module_name} 时发生错误: {error.message}") + self.logger.log_custom_exception(error) diff --git a/core/utils/__init__.py b/core/utils/__init__.py index 6d8b745..d48c3be 100644 --- a/core/utils/__init__.py +++ b/core/utils/__init__.py @@ -4,7 +4,7 @@ """ # 导出核心工具 -from .logger import logger +from .logger import logger, ModuleLogger, log_exception from .exceptions import * from .json_utils import * from .singleton import singleton @@ -20,9 +20,12 @@ from .performance import ( performance_stats, global_stats ) +from .error_codes import ErrorCode, get_error_message, create_error_response, exception_to_error_response __all__ = [ 'logger', + 'ModuleLogger', + 'log_exception', 'timeit', 'profile', 'aprofile', @@ -34,5 +37,9 @@ __all__ = [ 'global_stats', 'run_in_thread_pool', 'initialize_executor', - 'singleton' + 'singleton', + 'ErrorCode', + 'get_error_message', + 'create_error_response', + 'exception_to_error_response' ] diff --git a/core/utils/error_codes.py b/core/utils/error_codes.py new file mode 100644 index 0000000..50e103e --- /dev/null +++ b/core/utils/error_codes.py @@ -0,0 +1,234 @@ +""" +错误码和统一响应格式模块 + +该模块定义了项目中使用的错误码和统一的错误响应格式,确保所有模块返回一致的错误信息。 +""" + +# 错误码定义 +class ErrorCode: + """ + 错误码枚举类,包含所有系统错误码的定义。 + + 错误码规则: + - 1xxx: 系统级错误 + - 2xxx: WebSocket相关错误 + - 3xxx: 插件相关错误 + - 4xxx: 配置相关错误 + - 5xxx: 权限相关错误 + - 6xxx: 命令相关错误 + - 7xxx: Redis相关错误 + - 8xxx: 浏览器管理器相关错误 + - 9xxx: 代码执行相关错误 + """ + # 系统级错误 + SUCCESS = 0 # 成功 + UNKNOWN_ERROR = 1000 # 未知错误 + INVALID_PARAMETER = 1001 # 参数无效 + DATABASE_ERROR = 1002 # 数据库错误 + NETWORK_ERROR = 1003 # 网络错误 + TIMEOUT_ERROR = 1004 # 超时错误 + RESOURCE_EXHAUSTED = 1005 # 资源耗尽 + + # WebSocket相关错误 + WS_CONNECTION_FAILED = 2000 # WebSocket连接失败 + WS_AUTH_FAILED = 2001 # WebSocket认证失败 + WS_DISCONNECTED = 2002 # WebSocket已断开 + WS_MESSAGE_ERROR = 2003 # WebSocket消息错误 + + # 插件相关错误 + PLUGIN_LOAD_FAILED = 3000 # 插件加载失败 + PLUGIN_RELOAD_FAILED = 3001 # 插件重载失败 + PLUGIN_NOT_FOUND = 3002 # 插件未找到 + PLUGIN_INVALID = 3003 # 插件无效 + PLUGIN_DEPENDENCY_ERROR = 3004 # 插件依赖错误 + + # 配置相关错误 + CONFIG_NOT_FOUND = 4000 # 配置文件未找到 + CONFIG_PARSE_ERROR = 4001 # 配置解析错误 + CONFIG_VALIDATION_ERROR = 4002 # 配置验证错误 + CONFIG_KEY_NOT_FOUND = 4003 # 配置项未找到 + + # 权限相关错误 + PERMISSION_DENIED = 5000 # 权限不足 + NOT_ADMIN = 5001 # 不是管理员 + USER_BANNED = 5002 # 用户已被禁止 + + # 命令相关错误 + COMMAND_NOT_FOUND = 6000 # 命令未找到 + COMMAND_PARAM_ERROR = 6001 # 命令参数错误 + COMMAND_EXECUTE_ERROR = 6002 # 命令执行错误 + COMMAND_TIMEOUT = 6003 # 命令执行超时 + + # Redis相关错误 + REDIS_CONNECTION_FAILED = 7000 # Redis连接失败 + REDIS_OPERATION_ERROR = 7001 # Redis操作错误 + + # 浏览器管理器相关错误 + BROWSER_INIT_FAILED = 8000 # 浏览器初始化失败 + BROWSER_POOL_ERROR = 8001 # 浏览器池错误 + BROWSER_OPERATION_ERROR = 8002 # 浏览器操作错误 + + # 代码执行相关错误 + CODE_EXECUTE_ERROR = 9000 # 代码执行错误 + CODE_SECURITY_ERROR = 9001 # 代码安全错误 + + +# 错误码到错误消息的映射 +ERROR_MESSAGES = { + # 系统级错误 + ErrorCode.SUCCESS: "操作成功", + ErrorCode.UNKNOWN_ERROR: "未知错误", + ErrorCode.INVALID_PARAMETER: "参数无效", + ErrorCode.DATABASE_ERROR: "数据库错误", + ErrorCode.NETWORK_ERROR: "网络错误", + ErrorCode.TIMEOUT_ERROR: "操作超时", + ErrorCode.RESOURCE_EXHAUSTED: "资源耗尽", + + # WebSocket相关错误 + ErrorCode.WS_CONNECTION_FAILED: "WebSocket连接失败", + ErrorCode.WS_AUTH_FAILED: "WebSocket认证失败", + ErrorCode.WS_DISCONNECTED: "WebSocket已断开连接", + ErrorCode.WS_MESSAGE_ERROR: "WebSocket消息格式错误", + + # 插件相关错误 + ErrorCode.PLUGIN_LOAD_FAILED: "插件加载失败", + ErrorCode.PLUGIN_RELOAD_FAILED: "插件重载失败", + ErrorCode.PLUGIN_NOT_FOUND: "插件未找到", + ErrorCode.PLUGIN_INVALID: "插件无效", + ErrorCode.PLUGIN_DEPENDENCY_ERROR: "插件依赖错误", + + # 配置相关错误 + ErrorCode.CONFIG_NOT_FOUND: "配置文件未找到", + ErrorCode.CONFIG_PARSE_ERROR: "配置文件解析错误", + ErrorCode.CONFIG_VALIDATION_ERROR: "配置验证失败", + ErrorCode.CONFIG_KEY_NOT_FOUND: "配置项未找到", + + # 权限相关错误 + ErrorCode.PERMISSION_DENIED: "权限不足", + ErrorCode.NOT_ADMIN: "需要管理员权限", + ErrorCode.USER_BANNED: "用户已被禁止操作", + + # 命令相关错误 + ErrorCode.COMMAND_NOT_FOUND: "命令未找到", + ErrorCode.COMMAND_PARAM_ERROR: "命令参数错误", + ErrorCode.COMMAND_EXECUTE_ERROR: "命令执行错误", + ErrorCode.COMMAND_TIMEOUT: "命令执行超时", + + # Redis相关错误 + ErrorCode.REDIS_CONNECTION_FAILED: "Redis连接失败", + ErrorCode.REDIS_OPERATION_ERROR: "Redis操作错误", + + # 浏览器管理器相关错误 + ErrorCode.BROWSER_INIT_FAILED: "浏览器初始化失败", + ErrorCode.BROWSER_POOL_ERROR: "浏览器池错误", + ErrorCode.BROWSER_OPERATION_ERROR: "浏览器操作错误", + + # 代码执行相关错误 + ErrorCode.CODE_EXECUTE_ERROR: "代码执行错误", + ErrorCode.CODE_SECURITY_ERROR: "代码存在安全风险", +} + + +def get_error_message(code: int) -> str: + """ + 根据错误码获取错误消息 + + Args: + code: 错误码 + + Returns: + str: 错误消息 + """ + return ERROR_MESSAGES.get(code, ERROR_MESSAGES[ErrorCode.UNKNOWN_ERROR]) + + +def create_error_response(code: int, message: str = None, data: dict = None, request_id: str = None) -> dict: + """ + 创建统一格式的错误响应 + + Args: + code: 错误码 + message: 错误消息(可选,如果未提供则使用默认消息) + data: 附加数据(可选) + request_id: 请求ID(可选,用于追踪请求) + + Returns: + dict: 统一格式的错误响应 + """ + error_message = message if message is not None else get_error_message(code) + + response = { + "code": code, + "message": error_message, + "success": code == ErrorCode.SUCCESS, + } + + if data is not None: + response["data"] = data + + if request_id is not None: + response["request_id"] = request_id + + return response + + +def exception_to_error_response(exception: Exception, code: int = None, request_id: str = None) -> dict: + """ + 将异常对象转换为统一格式的错误响应 + + Args: + exception: 异常对象 + code: 错误码(可选,如果未提供则根据异常类型自动推断) + request_id: 请求ID(可选,用于追踪请求) + + Returns: + dict: 统一格式的错误响应 + """ + # 从自定义异常类中提取错误码 + if hasattr(exception, "code") and exception.code is not None: + code = exception.code + + # 如果仍未找到错误码,则根据异常类型推断 + if code is None: + from .exceptions import ( + WebSocketError, PluginError, ConfigError, PermissionError, + CommandError, RedisError, BrowserManagerError, CodeExecutionError + ) + + if isinstance(exception, WebSocketError): + code = ErrorCode.WS_CONNECTION_FAILED + elif isinstance(exception, PluginError): + code = ErrorCode.PLUGIN_LOAD_FAILED + elif isinstance(exception, ConfigError): + code = ErrorCode.CONFIG_PARSE_ERROR + elif isinstance(exception, PermissionError): + code = ErrorCode.PERMISSION_DENIED + elif isinstance(exception, CommandError): + code = ErrorCode.COMMAND_EXECUTE_ERROR + elif isinstance(exception, RedisError): + code = ErrorCode.REDIS_OPERATION_ERROR + elif isinstance(exception, BrowserManagerError): + code = ErrorCode.BROWSER_OPERATION_ERROR + elif isinstance(exception, CodeExecutionError): + code = ErrorCode.CODE_EXECUTE_ERROR + else: + code = ErrorCode.UNKNOWN_ERROR + + # 获取错误消息 + message = str(exception) + + # 如果异常有原始错误,也包含在响应中 + data = None + if hasattr(exception, "original_error") and exception.original_error is not None: + data = {"original_error": str(exception.original_error)} + + return create_error_response(code, message, data, request_id) + + +# 将错误码导出以便其他模块使用 +__all__ = [ + "ErrorCode", + "get_error_message", + "create_error_response", + "exception_to_error_response" +] \ No newline at end of file diff --git a/core/utils/exceptions.py b/core/utils/exceptions.py index 9b8cd18..acaf404 100644 --- a/core/utils/exceptions.py +++ b/core/utils/exceptions.py @@ -1,5 +1,7 @@ """ 自定义异常模块 + +该模块定义了项目中使用的各种自定义异常类,用于提供更精确、更友好的错误提示。 """ class SyncHandlerError(Exception): @@ -7,3 +9,213 @@ class SyncHandlerError(Exception): 当尝试注册同步函数作为异步事件处理器时抛出此异常。 """ pass + + +class WebSocketError(Exception): + """ + WebSocket相关错误的基类。 + + Args: + message: 错误消息 + code: 错误代码(可选) + original_error: 原始异常对象(可选) + """ + def __init__(self, message, code=None, original_error=None): + self.message = message + self.code = code + self.original_error = original_error + super().__init__(message) + + +class WebSocketConnectionError(WebSocketError): + """ + WebSocket连接失败时抛出此异常。 + """ + pass + + +class WebSocketAuthenticationError(WebSocketError): + """ + WebSocket认证失败时抛出此异常。 + """ + pass + + +class PluginError(Exception): + """ + 插件相关错误的基类。 + + Args: + plugin_name: 插件名称 + message: 错误消息 + original_error: 原始异常对象(可选) + """ + def __init__(self, plugin_name, message, original_error=None): + self.plugin_name = plugin_name + self.message = message + self.original_error = original_error + super().__init__(f"插件 {plugin_name}: {message}") + + +class PluginLoadError(PluginError): + """ + 插件加载失败时抛出此异常。 + """ + pass + + +class PluginReloadError(PluginError): + """ + 插件重载失败时抛出此异常。 + """ + pass + + +class PluginNotFoundError(PluginError): + """ + 找不到指定插件时抛出此异常。 + """ + pass + + +class ConfigError(Exception): + """ + 配置相关错误的基类。 + + Args: + section: 配置部分名称 + key: 配置项名称 + message: 错误消息 + """ + def __init__(self, section=None, key=None, message=None): + self.section = section + self.key = key + self.message = message + + if section and key and message: + super().__init__(f"配置错误 [{section}.{key}]: {message}") + elif section and message: + super().__init__(f"配置错误 [{section}]: {message}") + else: + super().__init__(message or "配置错误") + + +class ConfigNotFoundError(ConfigError): + """ + 配置文件不存在时抛出此异常。 + """ + pass + + +class ConfigValidationError(ConfigError): + """ + 配置验证失败时抛出此异常。 + """ + pass + + +class PermissionError(Exception): + """ + 权限相关错误的基类。 + + Args: + user_id: 用户ID + operation: 操作名称 + message: 错误消息 + """ + def __init__(self, user_id=None, operation=None, message=None): + self.user_id = user_id + self.operation = operation + self.message = message + + if user_id and operation and message: + super().__init__(f"权限错误 [用户 {user_id}]: 无权限执行操作 {operation} - {message}") + elif user_id and operation: + super().__init__(f"权限错误 [用户 {user_id}]: 无权限执行操作 {operation}") + else: + super().__init__(message or "权限错误") + + +class CommandError(Exception): + """ + 命令处理相关错误的基类。 + + Args: + command: 命令名称 + message: 错误消息 + original_error: 原始异常对象(可选) + """ + def __init__(self, command=None, message=None, original_error=None): + self.command = command + self.message = message + self.original_error = original_error + + if command and message: + super().__init__(f"命令错误 [{command}]: {message}") + else: + super().__init__(message or "命令错误") + + +class CommandNotFoundError(CommandError): + """ + 找不到指定命令时抛出此异常。 + """ + pass + + +class CommandParameterError(CommandError): + """ + 命令参数错误时抛出此异常。 + """ + pass + + +class RedisError(Exception): + """ + Redis相关错误的基类。 + + Args: + message: 错误消息 + original_error: 原始异常对象(可选) + """ + def __init__(self, message, original_error=None): + self.message = message + self.original_error = original_error + super().__init__(message) + + +class BrowserManagerError(Exception): + """ + 浏览器管理器相关错误的基类。 + + Args: + message: 错误消息 + original_error: 原始异常对象(可选) + """ + def __init__(self, message, original_error=None): + self.message = message + self.original_error = original_error + super().__init__(message) + + +class BrowserPoolError(BrowserManagerError): + """ + 浏览器池相关错误时抛出此异常。 + """ + pass + + +class CodeExecutionError(Exception): + """ + 代码执行相关错误的基类。 + + Args: + message: 错误消息 + code: 执行的代码(可选) + original_error: 原始异常对象(可选) + """ + def __init__(self, message, code=None, original_error=None): + self.message = message + self.code = code + self.original_error = original_error + super().__init__(message) diff --git a/core/utils/logger.py b/core/utils/logger.py index 76ec223..8b90eed 100644 --- a/core/utils/logger.py +++ b/core/utils/logger.py @@ -4,25 +4,40 @@ 该模块负责初始化和配置 loguru 日志记录器,为整个应用程序提供统一的日志记录接口。 """ import sys +import os from pathlib import Path from loguru import logger -# 定义日志格式 +# 定义日志格式,添加进程ID和线程ID作为上下文信息 LOG_FORMAT = ( "{time:YYYY-MM-DD HH:mm:ss.SSS} | " "{level: <8} | " + "PID {process} TID {thread} | " "{name}:{function}:{line} - " "{message}" ) +# 开发环境日志格式(更详细) +DEBUG_LOG_FORMAT = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | " + "{level: <8} | " + "PID {process} TID {thread} | " + "{name}:{function}:{line} | " + "Module: {module} | " + "{message}" +) + # 移除 loguru 默认的处理器 logger.remove() +# 获取当前环境 +ENVIRONMENT = os.getenv("NEOBOT_ENV", "development") + # 添加控制台输出处理器 logger.add( sys.stderr, - level="INFO", - format=LOG_FORMAT, + level="INFO" if ENVIRONMENT == "production" else "DEBUG", + format=LOG_FORMAT if ENVIRONMENT == "production" else DEBUG_LOG_FORMAT, colorize=True, enqueue=True # 异步写入 ) @@ -36,7 +51,7 @@ log_file_path = log_dir / "{time:YYYY-MM-DD}.log" logger.add( log_file_path, level="DEBUG", - format=LOG_FORMAT, + format=DEBUG_LOG_FORMAT, colorize=False, rotation="00:00", # 每天午夜创建新文件 retention="7 days", # 保留最近 7 天的日志 @@ -46,5 +61,77 @@ logger.add( diagnose=True # 添加异常诊断信息 ) -# 导出配置好的 logger -__all__ = ["logger"] +# 为自定义异常添加专门的日志记录方法 +def log_exception(exc, module_name="unknown", level="error"): + """ + 记录自定义异常的详细信息 + + Args: + exc: 异常对象 + module_name: 模块名称(可选) + level: 日志级别(可选,默认为 "error") + """ + log_func = getattr(logger, level) + log_func(f"模块 {module_name} 发生异常: {exc}") + + # 如果异常对象有原始异常,也记录原始异常信息 + if hasattr(exc, "original_error") and exc.original_error: + log_func(f"原始异常: {exc.original_error}") + + # 如果是配置错误,记录配置相关信息 + if hasattr(exc, "section") and hasattr(exc, "key"): + log_func(f"配置信息: 部分={exc.section}, 键={exc.key}") + + # 如果是插件错误,记录插件名称 + if hasattr(exc, "plugin_name"): + log_func(f"插件名称: {exc.plugin_name}") + + # 如果是命令错误,记录命令名称 + if hasattr(exc, "command"): + log_func(f"命令名称: {exc.command}") + + # 如果是权限错误,记录用户ID和操作 + if hasattr(exc, "user_id") and hasattr(exc, "operation"): + log_func(f"权限信息: 用户ID={exc.user_id}, 操作={exc.operation}") + +# 为不同模块提供日志工具 +class ModuleLogger: + """ + 模块专用日志记录器 + + Args: + module_name: 模块名称 + """ + def __init__(self, module_name): + self.module_name = module_name + + def debug(self, message): + logger.debug(f"[{self.module_name}] {message}") + + def info(self, message): + logger.info(f"[{self.module_name}] {message}") + + def success(self, message): + logger.success(f"[{self.module_name}] {message}") + + def warning(self, message): + logger.warning(f"[{self.module_name}] {message}") + + def error(self, message): + logger.error(f"[{self.module_name}] {message}") + + def exception(self, message, exc_info=True): + logger.exception(f"[{self.module_name}] {message}", exc_info=exc_info) + + def log_custom_exception(self, exc, level="error"): + """ + 记录自定义异常 + + Args: + exc: 异常对象 + level: 日志级别 + """ + log_exception(exc, self.module_name, level) + +# 导出配置好的 logger 和工具函数 +__all__ = ["logger", "log_exception", "ModuleLogger"] diff --git a/core/ws.py b/core/ws.py index 68c6a8e..6bd1ce9 100644 --- a/core/ws.py +++ b/core/ws.py @@ -25,7 +25,11 @@ from .bot import Bot from .config_loader import global_config from .managers.command_manager import matcher from .utils.executor import CodeExecutor -from .utils.logger import logger +from .utils.logger import logger, ModuleLogger +from .utils.exceptions import ( + WebSocketError, WebSocketConnectionError, WebSocketAuthenticationError +) +from .utils.error_codes import ErrorCode, create_error_response class WS: @@ -45,11 +49,15 @@ class WS: self.token = cfg.token self.reconnect_interval = cfg.reconnect_interval + # 初始化状态 self.ws: Optional[WebSocketClientProtocol] = None - self._pending_requests: Dict[str, asyncio.Future] = {} + self._pending_requests: Dict[str, asyncio.Future] = {} # echo: future self.bot: Bot | None = None self.self_id: int | None = None self.code_executor = code_executor + + # 创建模块专用日志记录器 + self.logger = ModuleLogger("WebSocket") async def connect(self) -> None: """ @@ -62,24 +70,43 @@ class WS: while True: try: - logger.info(f"正在尝试连接至 NapCat: {self.url}") + self.logger.info(f"正在尝试连接至 NapCat: {self.url}") async with websockets.connect( self.url, additional_headers=headers ) as websocket_raw: websocket = cast(WebSocketClientProtocol, websocket_raw) self.ws = websocket - logger.success("连接成功!") + self.logger.success("连接成功!") await self._listen_loop(websocket) + except websockets.exceptions.AuthenticationError as e: + error = WebSocketAuthenticationError( + message=f"WebSocket认证失败: {str(e)}", + code=ErrorCode.WS_AUTH_FAILED, + original_error=e + ) + self.logger.error(f"连接失败: {error.message}") + self.logger.log_custom_exception(error) except ( websockets.exceptions.ConnectionClosed, ConnectionRefusedError, ) as e: - logger.warning(f"连接断开或服务器拒绝访问: {e}") + error = WebSocketConnectionError( + message=f"连接断开或服务器拒绝访问: {str(e)}", + code=ErrorCode.WS_CONNECTION_FAILED, + original_error=e + ) + self.logger.warning(f"连接失败: {error.message}") except Exception as e: - logger.exception(f"运行异常: {e}") + error = WebSocketError( + message=f"WebSocket运行异常: {str(e)}", + code=ErrorCode.WS_MESSAGE_ERROR, + original_error=e + ) + self.logger.exception(f"运行异常: {error.message}") + self.logger.log_custom_exception(error) - logger.info(f"{self.reconnect_interval}秒后尝试重连...") + self.logger.info(f"{self.reconnect_interval}秒后尝试重连...") await asyncio.sleep(self.reconnect_interval) async def _listen_loop(self, websocket_connection: WebSocketClientProtocol) -> None: @@ -111,8 +138,22 @@ class WS: # 使用 create_task 异步执行,避免阻塞 WebSocket 接收循环 asyncio.create_task(self.on_event(data)) + except json.JSONDecodeError as e: + error = WebSocketError( + message=f"JSON解析失败: {str(e)}", + code=ErrorCode.WS_MESSAGE_ERROR, + original_error=e + ) + self.logger.error(f"解析消息异常: {error.message}") + self.logger.debug(f"原始消息: {message}") except Exception as e: - logger.exception(f"解析消息异常: {e}") + error = WebSocketError( + message=f"处理消息异常: {str(e)}", + code=ErrorCode.WS_MESSAGE_ERROR, + original_error=e + ) + self.logger.exception(f"解析消息异常: {error.message}") + self.logger.log_custom_exception(error) async def on_event(self, event_data: Dict[str, Any]) -> None: """ @@ -136,17 +177,17 @@ class WS: if self.bot is None and hasattr(event, 'self_id'): self.self_id = event.self_id self.bot = Bot(self) - logger.success(f"Bot 实例初始化完成: self_id={self.self_id}") + self.logger.success(f"Bot 实例初始化完成: self_id={self.self_id}") # 将代码执行器注入到 Bot 和执行器自身 if self.code_executor: self.bot.code_executor = self.code_executor self.code_executor.bot = self.bot - logger.info("代码执行器已成功注入 Bot 实例。") + self.logger.info("代码执行器已成功注入 Bot 实例。") # 如果 bot 尚未初始化,则不处理后续事件 if self.bot is None: - logger.warning("Bot 尚未初始化,跳过事件处理。") + self.logger.warning("Bot 尚未初始化,跳过事件处理。") return event.bot = self.bot # 注入 Bot 实例 @@ -157,23 +198,28 @@ class WS: message_type = getattr(event, "message_type", "Unknown") user_id = getattr(event, "user_id", "Unknown") raw_message = getattr(event, "raw_message", "") - logger.info(f"[消息] {message_type} | {user_id}({sender_name}): {raw_message}") + self.logger.info(f"[消息] {message_type} | {user_id}({sender_name}): {raw_message}") elif event.post_type == "notice": notice_type = getattr(event, "notice_type", "Unknown") - logger.info(f"[通知] {notice_type}") + self.logger.info(f"[通知] {notice_type}") elif event.post_type == "request": request_type = getattr(event, "request_type", "Unknown") - logger.info(f"[请求] {request_type}") + self.logger.info(f"[请求] {request_type}") elif event.post_type == "meta_event": meta_event_type = getattr(event, "meta_event_type", "Unknown") - logger.debug(f"[元事件] {meta_event_type}") - + self.logger.debug(f"[元事件] {meta_event_type}") # 分发事件 await matcher.handle_event(self.bot, event) except Exception as e: - logger.exception(f"事件处理异常: {e}") + self.logger.exception(f"事件处理异常: {str(e)}") + error = WebSocketError( + message=f"事件处理异常: {str(e)}", + code=ErrorCode.WS_MESSAGE_ERROR, + original_error=e + ) + self.logger.log_custom_exception(error) async def call_api(self, action: str, params: Optional[Dict[Any, Any]] = None) -> Dict[Any, Any]: """ @@ -191,14 +237,22 @@ class WS: 表示失败的字典。 """ if not self.ws: - logger.error("调用 API 失败: WebSocket 未初始化") - return {"status": "failed", "msg": "websocket not initialized"} + self.logger.error("调用 API 失败: WebSocket 未初始化") + return create_error_response( + code=ErrorCode.WS_DISCONNECTED, + message="WebSocket未初始化", + data={"action": action, "params": params} + ) from websockets.protocol import State if getattr(self.ws, "state", None) is not State.OPEN: - logger.error("调用 API 失败: WebSocket 连接未打开") - return {"status": "failed", "msg": "websocket is not open"} + self.logger.error("调用 API 失败: WebSocket 连接未打开") + return create_error_response( + code=ErrorCode.WS_DISCONNECTED, + message="WebSocket连接未打开", + data={"action": action, "params": params} + ) echo_id = str(uuid.uuid4()) payload = {"action": action, "params": params or {}, "echo": echo_id} @@ -207,12 +261,23 @@ class WS: future = loop.create_future() self._pending_requests[echo_id] = future - await self.ws.send(json.dumps(payload)) - try: + await self.ws.send(json.dumps(payload)) return await asyncio.wait_for(future, timeout=30.0) except asyncio.TimeoutError: self._pending_requests.pop(echo_id, None) - logger.warning(f"API 调用超时: action={action}, params={params}") - return {"status": "failed", "retcode": -1, "msg": "api timeout"} + self.logger.warning(f"API 调用超时: action={action}, params={params}") + return create_error_response( + code=ErrorCode.TIMEOUT_ERROR, + message="API调用超时", + data={"action": action, "params": params} + ) + except Exception as e: + self._pending_requests.pop(echo_id, None) + self.logger.exception(f"API 调用异常: action={action}, error={str(e)}") + return create_error_response( + code=ErrorCode.WS_MESSAGE_ERROR, + message=f"API调用异常: {str(e)}", + data={"action": action, "params": params} + ) diff --git a/docs/core-concepts/error-handling.md b/docs/core-concepts/error-handling.md new file mode 100644 index 0000000..0d93e46 --- /dev/null +++ b/docs/core-concepts/error-handling.md @@ -0,0 +1,194 @@ +# 错误处理机制 + +NEO Bot 采用了统一的错误处理机制,确保在各种异常情况下提供清晰、一致的错误信息。本文档将介绍系统的错误处理架构、错误码定义和使用方法。 + +## 1. 错误处理架构 + +### 1.1 自定义异常体系 + +系统定义了一套完整的自定义异常类体系,覆盖了各种常见的错误场景: + +- **WebSocket 相关错误**:`WebSocketError`、`WebSocketConnectionError`、`WebSocketAuthenticationError` +- **插件相关错误**:`PluginError`、`PluginLoadError`、`PluginReloadError`、`PluginNotFoundError` +- **配置相关错误**:`ConfigError`、`ConfigNotFoundError`、`ConfigValidationError` +- **权限相关错误**:`PermissionError` +- **命令相关错误**:`CommandError`、`CommandNotFoundError`、`CommandParameterError` +- **Redis 相关错误**:`RedisError` +- **浏览器管理器相关错误**:`BrowserManagerError`、`BrowserPoolError` +- **代码执行相关错误**:`CodeExecutionError` + +所有自定义异常类都位于 `core.utils.exceptions` 模块中。 + +### 1.2 统一的错误码系统 + +系统使用统一的错误码来标识不同类型的错误,错误码规则如下: + +- `1xxx`:系统级错误 +- `2xxx`:WebSocket 相关错误 +- `3xxx`:插件相关错误 +- `4xxx`:配置相关错误 +- `5xxx`:权限相关错误 +- `6xxx`:命令相关错误 +- `7xxx`:Redis 相关错误 +- `8xxx`:浏览器管理器相关错误 +- `9xxx`:代码执行相关错误 + +完整的错误码定义可以在 `core.utils.error_codes` 模块的 `ErrorCode` 类中找到。 + +### 1.3 统一的错误响应格式 + +系统提供了统一的错误响应格式,确保所有模块返回一致的错误信息: + +```json +{ + "code": 错误代码, + "message": 错误消息, + "success": false, + "data": 附加数据(可选), + "request_id": 请求ID(可选) +} +``` + +## 2. 日志记录增强 + +### 2.1 模块专用日志记录器 + +系统提供了 `ModuleLogger` 类,用于创建模块专用的日志记录器,自动添加模块标识: + +```python +from core.utils.logger import ModuleLogger + +# 创建模块专用日志记录器 +logger = ModuleLogger("MyModule") + +# 使用日志记录器 +logger.info("模块初始化完成") +logger.error("发生错误") +logger.exception("发生异常") +``` + +### 2.2 异常详情记录 + +系统提供了 `log_exception` 函数,用于记录自定义异常的详细信息: + +```python +from core.utils.logger import log_exception +from core.utils.exceptions import PluginError + +try: + # 代码逻辑 + raise PluginError("插件加载失败", plugin_name="test_plugin") +except Exception as e: + log_exception(e, module_name="PluginManager") +``` + +## 3. 核心模块的错误处理 + +### 3.1 WebSocket 模块 + +WebSocket 模块使用自定义异常类处理各种连接错误: + +- `WebSocketConnectionError`:连接失败 +- `WebSocketAuthenticationError`:认证失败 +- `WebSocketError`:其他 WebSocket 相关错误 + +### 3.2 插件管理器模块 + +插件管理器模块使用自定义异常类处理各种插件操作错误: + +- `PluginLoadError`:插件加载失败 +- `PluginReloadError`:插件重载失败 +- `PluginNotFoundError`:插件未找到 + +### 3.3 配置加载器模块 + +配置加载器模块使用自定义异常类处理各种配置加载错误: + +- `ConfigNotFoundError`:配置文件未找到 +- `ConfigValidationError`:配置验证失败 +- `ConfigError`:其他配置相关错误 + +## 4. 全局异常捕获 + +系统在主程序入口添加了全局异常捕获机制,确保所有未处理的异常都能被捕获并提供友好的错误信息: + +- 捕获并记录所有未处理的异常 +- 生成统一格式的错误响应 +- 根据错误类型给出不同的排查建议 +- 提供详细的错误信息和日志记录位置 + +## 5. 如何在插件中使用错误处理 + +### 5.1 抛出自定义异常 + +在插件中,您可以使用系统提供的自定义异常类来抛出更精确的错误: + +```python +from core.utils.exceptions import CommandParameterError +from core.utils.logger import ModuleLogger + +logger = ModuleLogger("MyPlugin") + +@matcher.command("test") +async def test_command(bot, event, args): + if len(args) < 1: + raise CommandParameterError("test", "缺少必要参数") + + # 命令逻辑 +``` + +### 5.2 捕获并处理异常 + +在插件中,您可以捕获并处理异常,提供更友好的错误信息: + +```python +from core.utils.exceptions import PluginError +from core.utils.logger import ModuleLogger + +logger = ModuleLogger("MyPlugin") + +@matcher.command("test") +async def test_command(bot, event, args): + try: + # 可能抛出异常的代码 + result = await some_operation() + await bot.send(event, f"操作结果: {result}") + except PluginError as e: + logger.error(f"插件操作失败: {e}") + await bot.send(event, f"操作失败: {e.message}") + except Exception as e: + logger.exception(f"发生未知错误: {e}") + await bot.send(event, "操作失败,请检查日志获取详细信息") +``` + +## 6. 错误排查建议 + +### 6.1 WebSocket 错误 + +- 检查 WebSocket 服务是否正在运行 +- 检查配置文件中的 WebSocket 地址和令牌是否正确 +- 检查网络连接是否正常 + +### 6.2 插件错误 + +- 检查插件目录是否存在 +- 检查插件文件是否有语法错误 +- 检查插件是否符合插件开发规范 + +### 6.3 配置错误 + +- 检查配置文件 config.toml 是否存在 +- 检查配置文件格式是否正确 +- 检查所有必填配置项是否都已设置 + +## 7. 总结 + +NEO Bot 的错误处理机制提供了: + +- 完整的自定义异常类体系 +- 统一的错误码系统 +- 一致的错误响应格式 +- 增强的日志记录功能 +- 全局异常捕获和友好提示 + +这些功能确保了系统在各种异常情况下都能提供清晰、一致的错误信息,便于开发和维护。 \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index fb1ecd7..db71711 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ * [性能优化](./core-concepts/performance.md): 页面池、JIT、Mypyc... * [消息流](./core-concepts/event-flow.md): 看看一条消息从被接收到被回复是如何运行的 * [核心](./core-concepts/singleton-managers.md): `matcher`, `browser_manager`... 认识这些核心模块。 +* [错误处理](./core-concepts/error-handling.md): 了解系统的错误处理机制和错误码定义。 ### 3. API 参考 * [API 总览](./api/index.md): 所有 API 的快速导航和调用方式 diff --git a/main.py b/main.py index 28998d0..ee89902 100644 --- a/main.py +++ b/main.py @@ -128,8 +128,8 @@ class PluginReloadHandler(FileSystemEventHandler): if not src_path.endswith(".py"): return - # 过滤掉一些临时文件 - if "__pycache__" in src_path or not src_path.startswith(PLUGIN_DIR): + # 过滤掉一些临时文件和__init__.py文件 + if "__pycache__" in src_path or not src_path.startswith(PLUGIN_DIR) or os.path.basename(src_path) == "__init__.py": return # 简单的防抖动 @@ -216,4 +216,55 @@ async def main(): if __name__ == "__main__": - asyncio.run(main()) + """ + 程序主入口,添加全局异常捕获和友好提示 + """ + from core.utils.error_codes import exception_to_error_response + from core.utils.logger import ModuleLogger + + # 创建主程序日志记录器 + main_logger = ModuleLogger("Main") + + try: + asyncio.run(main()) + except KeyboardInterrupt: + main_logger.info("程序已被用户中断") + exit(0) + except Exception as e: + main_logger.exception("程序发生未处理的全局异常") + + # 生成统一的错误响应 + error_response = exception_to_error_response(e) + + # 打印友好的错误提示 + print("\n" + "=" * 60) + print("程序发生错误,请检查以下信息:") + print("=" * 60) + print(f"错误代码: {error_response['code']}") + print(f"错误信息: {error_response['message']}") + print("=" * 60) + print("详细错误信息已记录到日志文件中") + print("请检查 logs 目录下的日志文件以获取更多调试信息") + print("=" * 60) + + # 根据错误类型给出不同的建议 + if hasattr(e, "original_error") and e.original_error: + print(f"\n原始错误: {e.original_error}") + + if "WebSocket" in str(type(e).__name__): + print("\n建议检查:") + print("1. WebSocket 服务是否正在运行") + print("2. 配置文件中的 WebSocket 地址和令牌是否正确") + print("3. 网络连接是否正常") + elif "Config" in str(type(e).__name__): + print("\n建议检查:") + print("1. 配置文件 config.toml 是否存在") + print("2. 配置文件格式是否正确") + print("3. 所有必填配置项是否都已设置") + elif "Plugin" in str(type(e).__name__): + print("\n建议检查:") + print("1. 插件目录是否存在") + print("2. 插件文件是否有语法错误") + print("3. 插件是否符合插件开发规范") + + exit(1) diff --git a/scripts/compile_machine_code.py b/scripts/compile_machine_code.py index 7aedd7e..8ec3aed 100644 --- a/scripts/compile_machine_code.py +++ b/scripts/compile_machine_code.py @@ -151,7 +151,8 @@ def clean_compiled_files(): def get_platform_specific_module_name(module_path): """获取平台特定的模块文件名""" - module_name = module_path.replace('.py', '') + # 只获取模块名,不包含路径 + module_name = os.path.basename(module_path).replace('.py', '') return f"{module_name}.{BUILD_PREFIX}{EXTENSION}" def compile_module(module_path): @@ -177,8 +178,17 @@ def compile_module(module_path): stderr_text = result.stderr # 获取平台特定的模块名 - platform_module = get_platform_specific_module_name(module_path) - mypyc_platform_module = platform_module.replace(EXTENSION, f'__mypyc{EXTENSION}') + # 获取模块名和目录 + module_dir = os.path.dirname(module_path) + module_basename = os.path.basename(module_path).replace('.py', '') + + # 生成平台特定的模块文件名(仅文件名,不含路径) + platform_module_name = f"{module_basename}.{BUILD_PREFIX}{EXTENSION}" + mypyc_platform_module_name = f"{module_basename}__mypyc.{BUILD_PREFIX}{EXTENSION}" + + # 完整路径构造 + platform_module = os.path.join(module_dir, platform_module_name) + mypyc_platform_module = os.path.join(module_dir, mypyc_platform_module_name) # 检查编译产物是否在当前目录 if os.path.exists(platform_module): @@ -186,12 +196,12 @@ def compile_module(module_path): return True else: # 检查 build 目录中是否有编译产物 - build_module_path = os.path.join(BUILD_PATH, platform_module) - build_mypyc_path = os.path.join(BUILD_PATH, mypyc_platform_module) + build_module_path = os.path.join(BUILD_PATH, module_dir, platform_module_name) + build_mypyc_path = os.path.join(BUILD_PATH, module_dir, mypyc_platform_module_name) if os.path.exists(build_module_path): # 如果在 build 目录中,复制到正确位置 - os.makedirs(os.path.dirname(platform_module), exist_ok=True) + os.makedirs(module_dir, exist_ok=True) shutil.copy2(build_module_path, platform_module) if os.path.exists(build_mypyc_path): shutil.copy2(build_mypyc_path, mypyc_platform_module) @@ -341,298 +351,5 @@ def main(): print("\n使用 --list 选项查看已编译的模块") print("使用 --clean 选项清理编译文件") -if __name__ == '__main__': - main()#!/usr/bin/env python3 -""" -跨平台 Python 模块编译脚本 - -将核心 Python 模块编译为机器码(.pyd 或 .so)以提升性能。 - -支持的平台: -- Windows: 生成 .pyd 文件 -- Linux: 生成 .so 文件 - -使用方法: - python compile_machine_code.py [options] - -选项: - --compile, -c 编译指定的模块(默认) - --list, -l 列出已编译的模块 - --clean, -k 清理编译生成的文件 - --help, -h 显示帮助信息 - -注意: - 1. 需要安装 C 编译器 (Windows 上需要 Visual Studio Build Tools, Linux 上需要 GCC) - 2. 需要安装 mypyc: pip install mypyc - 3. 编译后的文件是平台相关的,不能跨平台复制 - 4. 建议在部署的目标环境上运行此脚本 -""" -import os -import sys -import glob -import subprocess -import shutil -import argparse - -# 检测当前平台 -PLATFORM = sys.platform -if PLATFORM.startswith('win'): - EXTENSION = '.pyd' - BUILD_PREFIX = 'cp314-win_amd64' - BUILD_PATH = os.path.join('build', f'lib.win-amd64-cpython-314') -elif PLATFORM.startswith('linux'): - EXTENSION = '.so' - BUILD_PREFIX = 'cp314-x86_64-linux-gnu' - BUILD_PATH = os.path.join('build', f'lib.linux-x86_64-cpython-314') -else: - print(f"不支持的平台: {PLATFORM}") - sys.exit(1) - -# 要编译的模块列表 -# 注意:Mypyc 对动态特性支持有限,只选择计算密集或类型明确的模块 -MODULES = [ - # 工具模块 - 'core/utils/json_utils.py', # JSON 处理 - 'core/utils/executor.py', # 代码执行引擎 - 'core/utils/singleton.py', # 单例模式基类 - 'core/utils/exceptions.py', # 自定义异常 - 'core/utils/logger.py', # 日志模块 - - # 核心管理模块 - 'core/managers/command_manager.py', # 指令匹配和分发 - 'core/managers/admin_manager.py', # 管理员管理 - 'core/managers/permission_manager.py', # 权限管理 - 'core/managers/plugin_manager.py', # 插件管理器 - 'core/managers/redis_manager.py', # Redis 管理器 - 'core/managers/image_manager.py', # 图片管理器 - - # 核心基础模块 - 'core/ws.py', # WebSocket 核心 - 'core/bot.py', # Bot 核心抽象 - 'core/config_loader.py', # 配置加载 - 'core/config_models.py', # 配置模型 - 'core/permission.py', # 权限枚举 - - # API 模块 - 注意:这些类会被 Bot 类多继承使用 - # 因此不适合编译,否则会导致 "multiple bases have instance lay-out conflict" 错误 - # 'core/api/base.py', # API 基础类 - # 'core/api/account.py', # 账号相关 API - # 'core/api/friend.py', # 好友相关 API - # 'core/api/group.py', # 群组相关 API - # 'core/api/media.py', # 媒体相关 API - # 'core/api/message.py', # 消息相关 API - - # 数据模型(适合编译的高频使用数据类) - 'models/message.py', # 消息段模型 - 'models/sender.py', # 发送者模型 - 'models/objects.py', # API 响应数据模型 - - # 事件处理相关 - 'core/handlers/event_handler.py', # 事件处理器 - - # 注意:以下文件不适合编译 - # - 主程序文件(main.py) - # - 测试文件(tests/目录) - # - 插件文件(plugins/目录) - # - 编译脚本(compile_machine_code.py等) - # - 临时文件(scratch_files/目录) - # - 抽象基类(models/events/base.py) - # - 事件工厂(models/events/factory.py) - # - 包含复杂动态特性的文件 -] - -def list_compiled_modules(): - """列出已编译的模块""" - print(f"\n已编译的 {PLATFORM} 模块:") - print("=" * 50) - - # 查找所有编译后的文件 - compiled_files = [] - for ext in [EXTENSION, f'__mypyc{EXTENSION}']: - compiled_files.extend(glob.glob(f'**/*{ext}', recursive=True)) - - # 过滤掉虚拟环境中的文件 - compiled_files = [f for f in compiled_files if 'venv' not in f] - - if compiled_files: - for f in sorted(compiled_files): - size = os.path.getsize(f) // 1024 # KB - print(f"{f} ({size} KB)") - else: - print(f"未找到已编译的 {EXTENSION} 文件") - - print(f"\n总计: {len(compiled_files)} 个文件") - -def clean_compiled_files(): - """清理编译生成的文件""" - print(f"\n清理编译生成的 {EXTENSION} 文件...") - - # 查找所有编译后的文件 - compiled_files = [] - for ext in [EXTENSION, f'__mypyc{EXTENSION}']: - compiled_files.extend(glob.glob(f'**/*{ext}', recursive=True)) - - # 过滤掉虚拟环境中的文件 - compiled_files = [f for f in compiled_files if 'venv' not in f] - - if compiled_files: - for f in sorted(compiled_files): - try: - os.remove(f) - print(f"已删除: {f}") - except Exception as e: - print(f"删除失败 {f}: {e}") - - # 清理 build 目录 - if os.path.exists('build'): - try: - shutil.rmtree('build') - print("已删除 build 目录") - except Exception as e: - print(f"删除 build 目录失败: {e}") - else: - print(f"没有可清理的 {EXTENSION} 文件") - -def get_platform_specific_module_name(module_path): - """获取平台特定的模块文件名""" - module_name = module_path.replace('.py', '') - return f"{module_name}.{BUILD_PREFIX}{EXTENSION}" - -def compile_module(module_path): - """编译单个模块""" - print(f"\n编译: {module_path}") - - try: - # 直接调用 mypyc 命令行工具 - result = subprocess.run( - [sys.executable, '-m', 'mypyc', module_path], - capture_output=True, - text=True, - check=True - ) - - # 获取平台特定的模块名 - platform_module = get_platform_specific_module_name(module_path) - mypyc_platform_module = platform_module.replace(EXTENSION, f'__mypyc{EXTENSION}') - - # 检查编译产物是否在当前目录 - if os.path.exists(platform_module): - print(f" ✓ 编译成功: {platform_module}") - return True - else: - # 检查 build 目录中是否有编译产物 - build_module_path = os.path.join(BUILD_PATH, platform_module) - build_mypyc_path = os.path.join(BUILD_PATH, mypyc_platform_module) - - if os.path.exists(build_module_path): - # 如果在 build 目录中,复制到正确位置 - os.makedirs(os.path.dirname(platform_module), exist_ok=True) - shutil.copy2(build_module_path, platform_module) - shutil.copy2(build_mypyc_path, mypyc_platform_module) - print(f" ✓ 编译成功(已从 build 目录复制): {platform_module}") - return True - else: - print(f" ✗ 编译失败:找不到编译产物") - if result.stdout: - print(f" 编译输出:{result.stdout[:500]}...") - if result.stderr: - print(f" 错误信息:{result.stderr[:500]}...") - return False - - except subprocess.CalledProcessError as e: - print(f" ✗ 编译失败,退出码: {e.returncode}") - if e.stdout: - print(f" 编译输出:{e.stdout[:500]}...") - if e.stderr: - print(f" 错误信息:{e.stderr[:500]}...") - return False - except Exception as e: - print(f" ✗ 编译失败,意外错误: {e}") - return False - -def should_skip_module(module_path): - """检查模块是否应该被跳过编译""" - try: - with open(module_path, 'r', encoding='utf-8') as f: - content = f.read() - - # 检查是否包含抽象基类相关代码 - if 'from abc import ABC' in content or 'from abc import abstractmethod' in content: - return True, "包含抽象基类,不适合编译" - - # 检查是否包含动态特性 - if 'eval(' in content or 'exec(' in content or 'getattr(' in content or 'setattr(' in content: - return True, "包含动态特性,不适合编译" - - return False, "" - except Exception as e: - return True, f"读取文件时出错: {e}" - -def compile_all_modules(): - """编译所有指定的模块""" - print(f"\n开始编译 {len(MODULES)} 个模块 (平台: {PLATFORM})") - print("=" * 60) - - # 验证模块文件是否存在并检查是否适合编译 - valid_modules = [] - for module_path in MODULES: - if os.path.exists(module_path): - should_skip, reason = should_skip_module(module_path) - if should_skip: - print(f"跳过: {module_path} ({reason})") - else: - valid_modules.append(module_path) - else: - print(f"警告: 模块 {module_path} 不存在,将被跳过") - - if not valid_modules: - print("错误: 没有有效的模块可编译") - return False - - # 编译模块 - success_count = 0 - for module_path in valid_modules: - if compile_module(module_path): - success_count += 1 - - print(f"\n" + "=" * 60) - print(f"编译完成: {success_count}/{len(valid_modules)} 个模块成功") - - if success_count == len(valid_modules): - print("✓ 所有模块编译成功") - return True - else: - print("✗ 部分模块编译失败") - return False - -def main(): - """主函数""" - parser = argparse.ArgumentParser(description='跨平台 Python 模块编译脚本') - - group = parser.add_mutually_exclusive_group() - group.add_argument('--compile', '-c', action='store_true', default=True, - help='编译指定的模块 (默认)') - group.add_argument('--list', '-l', action='store_true', - help='列出已编译的模块') - group.add_argument('--clean', '-k', action='store_true', - help='清理编译生成的文件') - - args = parser.parse_args() - - # 检查是否安装了 mypyc - try: - import mypyc - except ImportError: - print("错误: 未安装 mypyc,请先安装: pip install mypyc") - sys.exit(1) - - if args.list: - list_compiled_modules() - elif args.clean: - clean_compiled_files() - else: - compile_all_modules() - print("\n使用 --list 选项查看已编译的模块") - if __name__ == '__main__': main() \ No newline at end of file diff --git a/tests/test_plugin_manager_coverage.py b/tests/test_plugin_manager_coverage.py index a7ab8a6..b6e4a8e 100644 --- a/tests/test_plugin_manager_coverage.py +++ b/tests/test_plugin_manager_coverage.py @@ -135,11 +135,15 @@ def test_reload_plugin_error(plugin_manager): plugin_manager.loaded_plugins.add(full_name) mock_module = MagicMock() + # 创建一个模拟的logger,直接替换plugin_manager实例的logger属性 + mock_logger = MagicMock() + plugin_manager.logger = mock_logger + with patch.dict("sys.modules", {full_name: mock_module}), \ - patch("importlib.reload", side_effect=Exception("Reload error")), \ - patch("core.managers.plugin_manager.logger") as mock_logger: + patch("importlib.reload", side_effect=Exception("Reload error")): # Should not raise exception plugin_manager.reload_plugin(full_name) mock_logger.exception.assert_called() + mock_logger.log_custom_exception.assert_called() diff --git a/tests/test_ws.py b/tests/test_ws.py index fb2f68b..ec7aea9 100644 --- a/tests/test_ws.py +++ b/tests/test_ws.py @@ -37,14 +37,18 @@ class TestWS: # 测试 WebSocket 未初始化的情况 result = await ws.call_api("send_group_msg", {"group_id": 123456, "message": "test"}) - assert result == {"status": "failed", "msg": "websocket not initialized"} + assert result["code"] == 2002 # WS_DISCONNECTED + assert result["success"] == False + assert "WebSocket未初始化" in result["message"] # 测试 WebSocket 已初始化但未连接的情况 mock_ws = MagicMock() mock_ws.state = None ws.ws = mock_ws result = await ws.call_api("send_group_msg", {"group_id": 123456, "message": "test"}) - assert result == {"status": "failed", "msg": "websocket is not open"} + assert result["code"] == 2002 # WS_DISCONNECTED + assert result["success"] == False + assert "WebSocket连接未打开" in result["message"] @pytest.mark.asyncio async def test_on_event_bot_initialization(self):