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/ws.py b/core/WS.py
similarity index 61%
rename from core/ws.py
rename to 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/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/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/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..3a4b020 100644
--- a/scripts/compile_machine_code.py
+++ b/scripts/compile_machine_code.py
@@ -391,54 +391,50 @@ else:
# 要编译的模块列表
# 注意: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/utils/json_utils.py', # JSON 处理 - 高频使用
+ 'core/utils/executor.py', # 代码执行引擎 - 高频使用
+ 'core/utils/exceptions.py', # 自定义异常 - 基础组件
+ 'core/utils/performance.py', # 性能监控工具 - 重要组件
+ 'core/utils/logger.py', # 日志模块 - 高频使用
+ 'core/utils/singleton.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/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', # 权限枚举
+ # 核心基础模块 - 高频使用
+ 'core/ws.py', # WebSocket 核心 - 核心通信,被10个文件引用
+ # 'core/bot.py', # Bot 核心抽象 - 使用多重继承,不适合编译
+ 'core/config_loader.py', # 配置加载 - 启动必需,被7个文件引用
+ # '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 响应数据模型 - 高频数据处理
- # 数据模型(适合编译的高频使用数据类)
- 'models/message.py', # 消息段模型
- 'models/sender.py', # 发送者模型
- 'models/objects.py', # API 响应数据模型
+ # 事件处理相关 - 高频使用
+ 'core/handlers/event_handler.py', # 事件处理器 - 核心事件处理
- # 事件处理相关
- 'core/handlers/event_handler.py', # 事件处理器
+ # 事件模型 - 高频使用,但包含dataclass,可能有编译问题,暂时排除
+ # 'models/events/message.py', # 消息事件 - 最高频事件类型
+ # 'models/events/notice.py', # 通知事件 - 高频事件类型
+ # 'models/events/request.py', # 请求事件 - 高频事件类型
+ # 'models/events/meta.py', # 元事件 - 高频事件类型
# 注意:以下文件不适合编译
# - 主程序文件(main.py)
# - 测试文件(tests/目录)
# - 插件文件(plugins/目录)
- # - 编译脚本(compile_machine_code.py等)
- # - 临时文件(scratch_files/目录)
- # - 抽象基类(models/events/base.py)
- # - 事件工厂(models/events/factory.py)
+ # - 编译(脚本compile_machine_code.py等)
# - 包含复杂动态特性的文件
+ # - API 基础类(由于多重继承问题)
]
def list_compiled_modules():
@@ -452,7 +448,7 @@ def list_compiled_modules():
compiled_files.extend(glob.glob(f'**/*{ext}', recursive=True))
# 过滤掉虚拟环境中的文件
- compiled_files = [f for f in compiled_files if 'venv' not in f]
+ compiled_files = [f for f in compiled_files if 'venv' not in f and '.venv' not in f]
if compiled_files:
for f in sorted(compiled_files):
@@ -473,7 +469,7 @@ def clean_compiled_files():
compiled_files.extend(glob.glob(f'**/*{ext}', recursive=True))
# 过滤掉虚拟环境中的文件
- compiled_files = [f for f in compiled_files if 'venv' not in f]
+ compiled_files = [f for f in compiled_files if 'venv' not in f and '.venv' not in f]
if compiled_files:
for f in sorted(compiled_files):
@@ -495,7 +491,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):
@@ -504,16 +501,34 @@ def compile_module(module_path):
try:
# 直接调用 mypyc 命令行工具
+ # 使用二进制模式捕获输出以避免编码问题
result = subprocess.run(
[sys.executable, '-m', 'mypyc', module_path],
capture_output=True,
- text=True,
check=True
)
+ # 解码输出时处理可能的编码错误
+ try:
+ stdout_text = result.stdout.decode('utf-8', errors='replace')
+ stderr_text = result.stderr.decode('utf-8', errors='replace')
+ except AttributeError:
+ # 如果已经是字符串(Python 3.7+),则直接使用
+ stdout_text = result.stdout
+ 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):
@@ -521,33 +536,44 @@ 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)
- shutil.copy2(build_mypyc_path, mypyc_platform_module)
+ if os.path.exists(build_mypyc_path):
+ shutil.copy2(build_mypyc_path, mypyc_platform_module)
print(f" ✓ 编译成功(已从 build 目录复制): {platform_module}")
return True
else:
- print(f" ✗ 编译失败:找不到编译产物")
+ print(" ✗ 编译失败:找不到编译产物")
if result.stdout:
- print(f" 编译输出:{result.stdout[:500]}...")
+ print(f" 编译输出:{stdout_text[:500]}...")
if result.stderr:
- print(f" 错误信息:{result.stderr[:500]}...")
+ print(f" 错误信息:{stderr_text[: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]}...")
+ if hasattr(e, 'stdout') and e.stdout:
+ try:
+ stdout_text = e.stdout.decode('utf-8', errors='replace') if isinstance(e.stdout, bytes) else e.stdout
+ print(f" 编译输出:{stdout_text[:500]}...")
+ except Exception:
+ print(f" 编译输出:{str(e.stdout)[:500]}...")
+ if hasattr(e, 'stderr') and e.stderr:
+ try:
+ stderr_text = e.stderr.decode('utf-8', errors='replace') if isinstance(e.stderr, bytes) else e.stderr
+ print(f" 错误信息:{stderr_text[:500]}...")
+ except Exception:
+ print(f" 错误信息:{str(e.stderr)[:500]}...")
return False
except Exception as e:
print(f" ✗ 编译失败,意外错误: {e}")
+ import traceback
+ traceback.print_exc()
return False
def should_skip_module(module_path):
@@ -560,9 +586,20 @@ def should_skip_module(module_path):
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, "包含动态特性,不适合编译"
+ # 检查是否包含危险的动态特性
+ # 注意:我们允许基本的动态特性,如getattr,但对于eval、exec等危险操作仍然阻止
+ if ('eval(' in content or 'exec(' in content or
+ 'compile(' in content):
+ return True, "包含危险动态特性,不适合编译"
+
+ # 检查是否包含复杂的动态属性访问
+ if ('__dict__' in content or '__class__' in content or
+ '__module__' in content or '__bases__' in content):
+ return True, "包含复杂动态特性,不适合编译"
+
+ # 检查是否包含复杂的动态属性访问
+ if '.__dict__' in content or '.__class__' in content:
+ return True, "包含复杂动态特性,不适合编译"
return False, ""
except Exception as e:
@@ -575,29 +612,41 @@ def compile_all_modules():
# 验证模块文件是否存在并检查是否适合编译
valid_modules = []
+ skipped_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})")
+ skipped_modules.append((module_path, reason))
else:
valid_modules.append(module_path)
else:
print(f"警告: 模块 {module_path} 不存在,将被跳过")
+ print(f"\n有效模块: {len(valid_modules)}, 跳过模块: {len(skipped_modules)}")
+
if not valid_modules:
print("错误: 没有有效的模块可编译")
return False
# 编译模块
success_count = 0
+ failed_modules = []
+
for module_path in valid_modules:
if compile_module(module_path):
success_count += 1
+ else:
+ failed_modules.append(module_path)
- print(f"\n" + "=" * 60)
+ print("\n" + "=" * 60)
print(f"编译完成: {success_count}/{len(valid_modules)} 个模块成功")
+ if failed_modules:
+ print(f"失败模块: {failed_modules}")
+
if success_count == len(valid_modules):
print("✓ 所有模块编译成功")
return True
@@ -633,6 +682,7 @@ def main():
else:
compile_all_modules()
print("\n使用 --list 选项查看已编译的模块")
+ print("使用 --clean 选项清理编译文件")
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):