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):