* 滚木

* feat: 重构核心架构,增强类型安全与插件管理

本次提交对核心模块进行了深度重构,引入 Pydantic 增强配置管理的类型安全性,并全面优化了插件管理系统。

主要变更详情:

1. 核心架构与配置
   - 重构配置加载模块:引入 Pydantic 模型 (`core/config_models.py`),提供严格的配置项类型检查、验证及默认值管理。
   - 统一模块结构:规范化模块导入路径,移除冗余的 `__init__.py` 文件,提升项目结构的清晰度。
   - 性能优化:集成 Redis 缓存支持 (`RedisManager`),有效降低高频 API 调用开销,提升响应速度。

2. 插件系统升级
   - 实现热重载机制:新增插件文件变更监听功能,支持开发过程中自动重载插件,提升开发效率。
   - 优化生命周期管理:改进插件加载与卸载逻辑,支持精确卸载指定插件及其关联的命令、事件处理器和定时任务。

3. 功能特性增强
   - 新增媒体 API:引入 `MediaAPI` 模块,封装图片、语音等富媒体资源的获取与处理接口。
   - 完善权限体系:重构权限管理系统,实现管理员与操作员的分级控制,支持更细粒度的命令权限校验。

4. 代码质量与稳定性
   - 全面类型修复:解决 `mypy` 静态类型检查发现的大量类型错误(包括 `CommandManager`、`EventFactory` 及 `Bot` API 签名不匹配问题)。
   - 增强错误处理:优化消息处理管道的异常捕获机制,完善关键路径的日志记录,提升系统运行稳定性。

* feat: 添加测试用例并优化代码结构

refactor(permission_manager): 调整初始化顺序和逻辑
fix(admin_manager): 修复初始化逻辑和目录创建问题
feat(ws): 优化Bot实例初始化条件
feat(message): 增强MessageSegment功能并添加测试
feat(events): 支持字符串格式的消息解析
test: 添加核心功能测试用例
refactor(plugin_manager): 改进插件路径处理
style: 清理无用导入和代码
chore: 更新依赖项

* refactor(handler): 移除TYPE_CHECKING并直接导入Bot类

简化类型注解,直接导入Bot类而非使用TYPE_CHECKING条件导入,提高代码可读性和维护性

* fix(command_manager): 修复插件卸载时元信息移除不精确的问题

修复 CommandManager 中 unload_plugin 方法移除插件元信息时使用 startswith 导致可能误删其他插件的问题,改为精确匹配
同时调整相关测试用例验证精确匹配行为

* refactor: 清理未使用的导入和更新文档结构

docs: 添加config_models.py到项目结构文档
docs: 调整数据目录位置到core/data下
docs: 更新权限管理器文档描述

* 文档更新

* 更新thpic插件 支持一次返回多张图

* feat: 添加测试覆盖率并修复相关问题

refactor(redis_manager): 移除冗余的ConnectionError处理
refactor(event_handler): 优化Bot类型注解
refactor(factory): 移除未使用的GroupCardNoticeEvent

test: 添加全面的单元测试覆盖
- 添加test_import.py测试模块导入
- 添加test_debug.py测试插件加载调试
- 添加test_plugin_error.py测试错误处理
- 添加test_config_loader.py测试配置加载
- 添加test_redis_manager.py测试Redis管理
- 添加test_bot.py测试Bot功能
- 扩展test_models.py测试消息模型
- 添加test_plugin_manager_coverage.py测试插件管理
- 添加test_executor.py测试代码执行器
- 添加test_ws.py测试WebSocket
- 添加test_api.py测试API接口
- 添加test_core_managers.py测试核心管理模块

fix(plugin_manager): 修复插件加载日志变量问题

覆盖率已到达86%(忽略插件)

* 更新/help指令,现在会发送图片

* feat(help): 重构帮助系统为图片渲染模式

添加浏览器管理器和图片管理器,用于通过 Playwright 渲染帮助菜单为图片
重构命令管理器以支持图片缓存和同步功能
添加 HTML 模板用于帮助菜单渲染

* build: 更新依赖文件 requirements.txt

* build: 更新依赖文件

* feat: 添加性能优化和架构文档,更新依赖和核心模块

refactor(browser_manager): 实现页面池机制以提升性能
refactor(image_manager): 添加模板缓存并集成页面池
refactor(bili_parser): 迁移到异步HTTP请求并实现会话复用
docs: 新增性能优化、架构设计和最佳实践文档
chore: 更新requirements.txt添加新依赖

* docs: 更新文档内容并优化语言风格

重构所有文档内容,使用更简洁直接的语言风格
更新架构、插件开发、部署等核心文档
优化代码示例和图表说明
统一术语和格式规范

* docs: 更新文档内容,简化语言并修正格式

- 简化插件开发指南中的描述,移除冗余内容
- 调整部署文档中的Python版本说明
- 优化最佳实践文档的措辞和格式
- 更新性能优化文档,删除不准确的数据
- 重构核心概念文档,使用更简洁的语言
- 修正README中的项目描述和技术栈说明
- 更新快速上手文档,简化安装步骤
- 调整事件流转文档的描述方式
- 简化架构文档内容
- 更新指令处理文档,添加参数注入示例
- 优化单例管理器文档的表述

* refactor(core): 优化权限管理和事件模型

- 重构 AdminManager 和 PermissionManager 以 Redis 为主要数据源
- 为所有事件模型添加 slots=True 提升性能
- 更新文档说明 Mypyc 编译注意事项
- 清理测试和调试文件
- 移动静态资源到 web_static 目录

---------

Co-authored-by: baby20162016 <2185823427@qq.com>
This commit is contained in:
镀铬酸钾
2026-01-13 08:37:30 +08:00
committed by GitHub
parent 95e98dea9c
commit d9ad6af444
23 changed files with 434 additions and 383 deletions

6
.gitignore vendored
View File

@@ -140,3 +140,9 @@ dmypy.json
# End of https://www.toptal.com/developers/gitignore/api/python
/ca
# Build artifacts
build/
# Scratch files
scratch_files/

65
compile_modules.py Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
编译模块脚本
这个脚本会单独编译每个Python模块确保每个模块都在正确位置生成独立的.pyd文件。
"""
import os
import sys
import glob
from mypyc.build import mypycify
from distutils.core import setup
def compile_module(module_path):
"""
编译单个模块
Args:
module_path: 要编译的Python模块路径
"""
print(f"\nCompiling {module_path}...")
try:
ext_modules = mypycify([module_path])
setup(name=f'compiled_{os.path.basename(module_path).replace(".py", "")}',
ext_modules=ext_modules)
return True
except Exception as e:
print(f"Error compiling {module_path}: {e}")
return False
def main():
"""
主函数
"""
# 要编译的模块列表
modules = [
'core/utils/json_utils.py', # JSON 处理
'core/utils/executor.py', # 代码执行引擎
'core/managers/command_manager.py', # 指令匹配和分发
'core/managers/admin_manager.py', # 管理员管理
'core/managers/permission_manager.py', # 权限管理
'core/ws.py', # WebSocket 核心
'core/managers/plugin_manager.py', # 插件管理器
'core/bot.py', # Bot 核心抽象
'core/config_loader.py', # 配置加载
]
# 自动添加 events 模型
event_models = glob.glob('models/events/*.py')
event_models = [m for m in event_models if not m.endswith('__init__.py')]
modules.extend(event_models)
print(f"Found {len(modules)} modules to compile.")
success_count = 0
for module in modules:
if compile_module(module):
success_count += 1
print(f"\n--- Compilation Summary ---")
print(f"Total modules: {len(modules)}")
print(f"Successfully compiled: {success_count}")
print(f"Failed: {len(modules) - success_count}")
if __name__ == '__main__':
main()

View File

@@ -10,13 +10,14 @@ Bot 核心抽象模块
- 提供高级消息发送功能,如 `send_forwarded_messages`。
- 整合所有细分的 API 调用(消息、群组、好友等)。
"""
from typing import TYPE_CHECKING, Dict, Any, List, Union
from typing import TYPE_CHECKING, Dict, Any, List, Union, Optional
from models.events.base import OneBotEvent
from models.message import MessageSegment
from models.objects import GroupInfo, StrangerInfo
if TYPE_CHECKING:
from .ws import WS
from .utils.executor import CodeExecutor
from .api import MessageAPI, GroupAPI, FriendAPI, AccountAPI, MediaAPI
@@ -37,7 +38,7 @@ class Bot(MessageAPI, GroupAPI, FriendAPI, AccountAPI, MediaAPI):
ws_client (WS): WebSocket 客户端实例,负责底层的 API 请求和响应处理。
"""
super().__init__(ws_client, ws_client.self_id or 0)
self.code_executor = None
self.code_executor: Optional["CodeExecutor"] = None
async def get_group_list(self, no_cache: bool = False) -> List[GroupInfo]:
# GroupAPI.get_group_list 不支持 no_cache 参数,这里忽略它

View File

@@ -2,8 +2,7 @@
管理员管理器模块
该模块负责管理机器人的管理员列表。
实现了文件和 Redis 缓存之间的数据同步,并提供了一套清晰的 API
供其他模块调用。
现在以 Redis 作为主要数据源,文件仅用作备份。
"""
import json
import os
@@ -18,10 +17,11 @@ class AdminManager(Singleton):
"""
管理员管理器类
负责加载、缓存和管理管理员列表
使用单例模式,确保全局只有一个实例
以 Redis Set 作为管理员列表的唯一真实来源,提供高速的读写能力
文件 (admin.json) 仅用于首次启动时的数据迁移和作为灾备
"""
_REDIS_KEY = "neobot:admins" # 用于存储管理员集合的 Redis 键
def __init__(self):
"""
初始化 AdminManager
@@ -29,7 +29,7 @@ class AdminManager(Singleton):
if hasattr(self, '_initialized') and self._initialized:
return
# 管理员数据文件路径
# 管理员数据文件路径,主要用于备份和首次迁移
self.data_file = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..",
@@ -37,124 +37,113 @@ class AdminManager(Singleton):
"admin.json"
)
self._admins: Set[int] = set()
# 确保数据目录存在
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
logger.info("管理员管理器初始化完成")
super().__init__()
async def initialize(self):
"""
异步初始化,加载数据并同步到 Redis
异步初始化,检查 Redis 数据,如果为空则尝试从文件迁移
"""
await self._load_from_file()
await self._sync_to_redis()
logger.info("管理员数据加载并同步到 Redis 完成")
try:
# 检查 Redis 中是否已存在数据
if await redis_manager.redis.exists(self._REDIS_KEY):
admin_count = await redis_manager.redis.scard(self._REDIS_KEY)
logger.info(f"Redis 中已存在管理员数据,共 {admin_count} 位。")
else:
# Redis 为空,尝试从文件迁移
logger.info("Redis 中未找到管理员数据,尝试从 admin.json 文件迁移...")
await self._migrate_from_file_to_redis()
except Exception as e:
logger.error(f"初始化管理员数据时发生错误: {e}")
async def _load_from_file(self):
async def _migrate_from_file_to_redis(self):
"""
从 admin.json 加载管理员列表
从 admin.json 加载管理员列表并存入 Redis
这通常只在首次启动或 Redis 数据丢失时执行一次
"""
admins_to_migrate = set()
try:
if os.path.exists(self.data_file):
with open(self.data_file, "r", encoding="utf-8") as f:
data = json.load(f)
admins = data.get("admins", [])
self._admins = set(int(admin_id) for admin_id in admins)
logger.debug(f"{self.data_file} 加载了 {len(self._admins)} 位管理员")
else:
# 如果文件不存在,创建一个空的
self._admins = set()
await self._save_to_file()
except (json.JSONDecodeError, ValueError) as e:
logger.error(f"加载或解析 admin.json 失败: {e}")
self._admins = set()
admins_to_migrate = set(int(admin_id) for admin_id in admins)
async def _save_to_file(self):
if admins_to_migrate:
await redis_manager.redis.sadd(self._REDIS_KEY, *admins_to_migrate)
logger.success(f"成功从文件迁移 {len(admins_to_migrate)} 位管理员到 Redis。")
else:
logger.info("admin.json 文件为空或不存在,无需迁移。")
except (json.JSONDecodeError, ValueError) as e:
logger.error(f"解析 admin.json 失败,无法迁移: {e}")
except Exception as e:
logger.error(f"迁移管理员数据到 Redis 失败: {e}")
async def _save_to_file_backup(self):
"""
当前管理员列表保存回 admin.json
Redis 中的管理员列表备份到 admin.json
"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
# 将 set 转换为 list 以便 JSON 序列化
admin_list = [str(admin_id) for admin_id in self._admins]
admins = await self.get_all_admins()
admin_list = [str(admin_id) for admin_id in admins]
with open(self.data_file, "w", encoding="utf-8") as f:
json.dump({"admins": admin_list}, f, indent=2, ensure_ascii=False)
logger.debug(f"管理员列表已保存{self.data_file}")
logger.debug(f"管理员列表已备份{self.data_file}")
except Exception as e:
logger.error(f"保存 admin.json 失败: {e}")
async def _sync_to_redis(self):
"""
将内存中的管理员集合同步到 Redis
"""
from core.managers.redis_manager import redis_manager
try:
# 首先清空旧的集合
await redis_manager.redis.delete(self._REDIS_KEY)
if self._admins:
# 将所有管理员ID添加到集合中
await redis_manager.redis.sadd(self._REDIS_KEY, *self._admins)
logger.debug(f"已将 {len(self._admins)} 位管理员同步到 Redis")
except Exception as e:
logger.error(f"同步管理员到 Redis 失败: {e}")
logger.error(f"备份管理员列表到 admin.json 失败: {e}")
async def is_admin(self, user_id: int) -> bool:
"""
检查用户是否为管理员(从 Redis 缓存读取)
检查用户是否为管理员(直接从 Redis 读取)
"""
try:
return await redis_manager.redis.sismember(self._REDIS_KEY, user_id)
except Exception as e:
logger.error(f"从 Redis 检查管理员权限失败: {e}")
# Redis 失败时,回退到内存检查
return user_id in self._admins
return False
async def add_admin(self, user_id: int) -> bool:
"""
添加管理员,并同步到文件和 Redis
添加管理员到 Redis并更新文件备份
"""
from .redis_manager import redis_manager
if user_id in self._admins:
return False # 用户已经是管理员
self._admins.add(user_id)
await self._save_to_file()
try:
await redis_manager.redis.sadd(self._REDIS_KEY, user_id)
logger.info(f"已添加新管理员 {user_id} 并更新缓存")
# sadd 返回成功添加的成员数量1 表示成功0 表示已存在
if await redis_manager.redis.sadd(self._REDIS_KEY, user_id) == 1:
logger.info(f"已添加新管理员 {user_id} 到 Redis")
await self._save_to_file_backup() # 更新备份
return True
return False # 用户已经是管理员
except Exception as e:
logger.error(f"添加管理员 {user_id} 到 Redis 失败: {e}")
return False
async def remove_admin(self, user_id: int) -> bool:
"""
移除管理员,并同步到文件和 Redis
从 Redis 移除管理员,并更新文件备份
"""
from .redis_manager import redis_manager
if user_id not in self._admins:
return False # 用户不是管理员
self._admins.remove(user_id)
await self._save_to_file()
try:
await redis_manager.redis.srem(self._REDIS_KEY, user_id)
logger.info(f"已移除管理员 {user_id} 并更新缓存")
# srem 返回成功移除的成员数量1 表示成功0 表示不存在
if await redis_manager.redis.srem(self._REDIS_KEY, user_id) == 1:
logger.info(f"已从 Redis 移除管理员 {user_id}")
await self._save_to_file_backup() # 更新备份
return True
return False # 用户不是管理员
except Exception as e:
logger.error(f"从 Redis 移除管理员 {user_id} 失败: {e}")
return False
async def get_all_admins(self) -> Set[int]:
"""
获取所有管理员的集合
从 Redis 获取所有管理员的集合
"""
return self._admins.copy()
try:
admins = await redis_manager.redis.smembers(self._REDIS_KEY)
return {int(admin_id) for admin_id in admins}
except Exception as e:
logger.error(f"从 Redis 获取所有管理员失败: {e}")
return set()
# 全局 AdminManager 实例

View File

@@ -2,14 +2,7 @@
权限管理器模块
该模块负责管理用户权限,支持 admin、op、user 三个权限级别。
权限数据存储在 `permissions.json` 文件中,格式为:
{
"users": {
"123456": "admin",
"789012": "op",
"345678": "user"
}
}
以 Redis Hash 作为主要数据源,文件仅用作备份和首次数据迁移。
"""
import json
import os
@@ -18,6 +11,7 @@ from typing import Dict
from ..utils.logger import logger
from ..utils.singleton import Singleton
from .admin_manager import admin_manager
from .redis_manager import redis_manager
from ..permission import Permission
@@ -31,20 +25,19 @@ class PermissionManager(Singleton):
"""
权限管理器类
负责加载、保存和查询用户权限数据
使用单例模式,确保全局只有一个权限管理器实例
以 Redis Hash 作为权限数据的唯一真实来源,提供高速的读写能力
文件 (permissions.json) 仅用于首次启动时的数据迁移和作为灾备
"""
_REDIS_KEY = "neobot:permissions" # 用于存储用户权限的 Redis Hash 键
def __init__(self):
"""
初始化权限管理器
如果已经初始化过,则直接返回。
"""
if hasattr(self, '_initialized') and self._initialized:
return
# 权限数据文件路径
# 权限数据文件路径,主要用于备份和首次迁移
self.data_file = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..",
@@ -52,155 +45,147 @@ class PermissionManager(Singleton):
"permissions.json"
)
# 确保数据目录存在
data_dir = os.path.dirname(self.data_file)
os.makedirs(data_dir, exist_ok=True)
# 权限数据存储结构:{"users": {"user_id": "level_name"}}
self._data: Dict[str, Dict[str, str]] = {"users": {}}
# 加载现有数据
self.load()
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
logger.info("权限管理器初始化完成")
super().__init__()
def load(self) -> None:
async def initialize(self):
"""
从文件加载权限数据
异步初始化,检查 Redis 数据,如果为空则尝试从文件迁移
"""
try:
if not await redis_manager.redis.exists(self._REDIS_KEY):
logger.info("Redis 中未找到权限数据,尝试从 permissions.json 文件迁移...")
await self._migrate_from_file_to_redis()
else:
perm_count = await redis_manager.redis.hlen(self._REDIS_KEY)
logger.info(f"Redis 中已存在权限数据,共 {perm_count} 条。")
except Exception as e:
logger.error(f"初始化权限数据时发生错误: {e}")
如果文件不存在,则创建空文件并初始化默认数据结构。
async def _migrate_from_file_to_redis(self):
"""
从 permissions.json 加载权限数据并存入 Redis Hash
"""
perms_to_migrate = {}
try:
if os.path.exists(self.data_file):
with open(self.data_file, "r", encoding="utf-8") as f:
data = json.load(f)
# 兼容旧格式
if "users" in data:
self._data["users"] = data["users"]
else:
self._data["users"] = {}
logger.debug(f"权限数据已从 {self.data_file} 加载")
else:
# 文件不存在,创建空文件
self.save()
logger.debug(f"创建空的权限数据文件: {self.data_file}")
except json.JSONDecodeError as e:
logger.error(f"权限数据文件格式错误: {e}")
# 文件损坏,重置为空数据
self._data["users"] = {}
self.save()
except Exception as e:
logger.error(f"加载权限数据失败: {e}")
self._data["users"] = {}
perms_to_migrate = data.get("users", {})
def save(self) -> None:
if perms_to_migrate:
# 使用 pipeline 批量写入,提高效率
async with redis_manager.redis.pipeline(transaction=True) as pipe:
for user_id, level_name in perms_to_migrate.items():
pipe.hset(self._REDIS_KEY, user_id, level_name)
await pipe.execute()
logger.success(f"成功从文件迁移 {len(perms_to_migrate)} 条权限数据到 Redis。")
else:
logger.info("permissions.json 文件为空或不存在,无需迁移。")
except (json.JSONDecodeError, ValueError) as e:
logger.error(f"解析 permissions.json 失败,无法迁移: {e}")
except Exception as e:
logger.error(f"迁移权限数据到 Redis 失败: {e}")
async def _save_to_file_backup(self):
"""
权限数据保存到文件
Redis 中的权限数据完整备份到 permissions.json
"""
try:
all_perms = await redis_manager.redis.hgetall(self._REDIS_KEY)
# Redis 返回的是 bytes需要解码
users_data = {k.decode('utf-8'): v.decode('utf-8') for k, v in all_perms.items()}
with open(self.data_file, "w", encoding="utf-8") as f:
json.dump(self._data, f, indent=2, ensure_ascii=False)
logger.debug(f"权限数据已保存{self.data_file}")
json.dump({"users": users_data}, f, indent=2, ensure_ascii=False)
logger.debug(f"权限数据已备份{self.data_file}")
except Exception as e:
logger.error(f"保存权限数据失败: {e}")
logger.error(f"备份权限数据到 permissions.json 失败: {e}")
async def get_user_permission(self, user_id: int) -> Permission:
"""
获取指定用户的权限对象
Args:
user_id (int): 用户 QQ 号
Returns:
Permission: 用户的权限对象,如果用户不存在则返回默认级别 USER
优先检查是否为机器人管理员,然后从 Redis 查询。
"""
# 首先,通过 AdminManager 检查是否为管理员
if await admin_manager.is_admin(user_id):
return Permission.ADMIN
# 如果不是管理员,则从 permissions.json 中查找
user_id_str = str(user_id)
level_name = self._data["users"].get(user_id_str, Permission.USER.value)
try:
level_name_bytes = await redis_manager.redis.hget(self._REDIS_KEY, str(user_id))
if level_name_bytes:
level_name = level_name_bytes.decode('utf-8')
return _PERMISSIONS.get(level_name, Permission.USER)
except Exception as e:
logger.error(f"从 Redis 获取用户 {user_id} 权限失败: {e}")
def set_user_permission(self, user_id: int, permission: Permission) -> None:
return Permission.USER
async def set_user_permission(self, user_id: int, permission: Permission) -> None:
"""
设置指定用户的权限级别
Args:
user_id (int): 用户 QQ 号
permission (Permission): 权限对象
Raises:
ValueError: 如果权限对象无效
在 Redis 中设置指定用户的权限级别,并更新文件备份
"""
if not isinstance(permission, Permission):
raise ValueError(f"无效的权限对象: {permission}")
user_id_str = str(user_id)
self._data["users"][user_id_str] = permission.value
self.save()
logger.info(f"设置用户 {user_id} 的权限级别{permission.value}")
try:
await redis_manager.redis.hset(self._REDIS_KEY, str(user_id), permission.value)
await self._save_to_file_backup()
logger.info(f"已在 Redis 中设置用户 {user_id} 的权限为 {permission.value}")
except Exception as e:
logger.error(f"在 Redis 中设置用户 {user_id} 权限失败: {e}")
def remove_user(self, user_id: int) -> None:
async def remove_user(self, user_id: int) -> None:
"""
移除指定用户的权限设置,恢复为默认级别
Args:
user_id (int): 用户 QQ 号
从 Redis 中移除指定用户的权限设置,并更新文件备份
"""
user_id_str = str(user_id)
if user_id_str in self._data["users"]:
del self._data["users"][user_id_str]
self.save()
logger.info(f"移除用户 {user_id} 的权限设置")
try:
if await redis_manager.redis.hdel(self._REDIS_KEY, str(user_id)):
await self._save_to_file_backup()
logger.info(f"已从 Redis 中移除用户 {user_id} 的权限设置")
except Exception as e:
logger.error(f"从 Redis 移除用户 {user_id} 权限失败: {e}")
async def check_permission(self, user_id: int, required_permission: Permission) -> bool:
"""
检查用户是否具有指定权限级别
Args:
user_id (int): 用户 QQ 号
required_permission (Permission): 所需的权限对象
Returns:
bool: 如果用户权限 >= 所需权限,返回 True否则返回 False
"""
user_permission = await self.get_user_permission(user_id)
return user_permission >= required_permission
async def get_all_user_permissions(self) -> Dict[str, str]:
"""
获取所有已配置的用户权限(包括 AdminManager 中的管理员
:return: 一个包含所有用户权限的字典
获取所有已配置的用户权限(合并 Redis 和 AdminManager
"""
permissions = self._data["users"].copy()
permissions = {}
try:
# 从 Redis 获取基础权限
all_perms = await redis_manager.redis.hgetall(self._REDIS_KEY)
permissions = {k.decode('utf-8'): v.decode('utf-8') for k, v in all_perms.items()}
except Exception as e:
logger.error(f"从 Redis 获取所有权限失败: {e}")
# 合并 AdminManager 中的管理员
# 合并 AdminManager 中的管理员ADMIN 权限覆盖一切
try:
admins = await admin_manager.get_all_admins()
for admin_id in admins:
permissions[str(admin_id)] = Permission.ADMIN.value
except Exception as e:
logger.error(f"获取管理员列表以合并权限时失败: {e}")
return permissions
def get_all_users(self) -> Dict[str, str]:
async def clear_all(self) -> None:
"""
获取所有设置了权限的用户及其级别名称
Returns:
Dict[str, str]: 用户ID到权限级别名称的映射
清空 Redis 中的所有权限设置,并更新备份文件
"""
return self._data["users"].copy()
def clear_all(self) -> None:
"""
清空所有权限设置
"""
self._data["users"].clear()
self.save()
logger.info("已清空所有权限设置")
try:
await redis_manager.redis.delete(self._REDIS_KEY)
await self._save_to_file_backup()
logger.info("已清空 Redis 中的所有权限设置")
except Exception as e:
logger.error(f"清空 Redis 权限数据失败: {e}")
def require_admin(func):

View File

@@ -8,6 +8,7 @@ import os
import pkgutil
import sys
from typing import Set
from .command_manager import CommandManager
from ..utils.exceptions import SyncHandlerError
from ..utils.logger import logger
@@ -20,7 +21,7 @@ class PluginManager:
"""
插件管理器类
"""
def __init__(self, command_manager):
def __init__(self, command_manager: "CommandManager") -> None:
"""
初始化插件管理器
@@ -29,7 +30,7 @@ class PluginManager:
self.command_manager = command_manager
self.loaded_plugins: Set[str] = set()
def load_all_plugins(self):
def load_all_plugins(self) -> None:
"""
扫描并加载 `plugins` 目录下的所有插件。
"""
@@ -77,7 +78,7 @@ class PluginManager:
f" 加载插件 {module_name} 失败: {e}"
)
def reload_plugin(self, full_module_name: str):
def reload_plugin(self, full_module_name: str) -> None:
"""
精确重载单个插件。
"""

View File

@@ -1,6 +1,9 @@
"""
通用单例模式基类
"""
from typing import Any, Optional, Type, TypeVar
T = TypeVar('T')
class Singleton:
"""
@@ -10,18 +13,25 @@ class Singleton:
它通过重写 __new__ 方法来确保每个类只有一个实例。
同时,它处理了重复初始化的问题,确保 __init__ 方法只在第一次实例化时被调用。
"""
_instance = None
_initialized = False
_instance: Optional[Any] = None
_initialized: bool = False
def __new__(cls, *args, **kwargs):
def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T:
"""
创建或返回现有的实例
Args:
*args: 传递给构造函数的位置参数
**kwargs: 传递给构造函数的关键字参数
Returns:
T: 单例实例
"""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
def __init__(self) -> None:
"""
确保初始化逻辑只执行一次
"""

View File

@@ -13,16 +13,18 @@ WebSocket 连接。它是整个机器人框架的底层通信基础。
"""
import asyncio
import json
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, cast
import uuid
import websockets
from websockets.legacy.client import WebSocketClientProtocol
from models.events.factory import EventFactory
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
@@ -31,7 +33,7 @@ class WS:
WebSocket 客户端,负责与 OneBot v11 实现进行底层通信。
"""
def __init__(self, code_executor=None):
def __init__(self, code_executor: Optional[CodeExecutor] = None) -> None:
"""
初始化 WebSocket 客户端。
@@ -43,13 +45,13 @@ class WS:
self.token = cfg.token
self.reconnect_interval = cfg.reconnect_interval
self.ws = None
self._pending_requests = {}
self.ws: Optional[WebSocketClientProtocol] = None
self._pending_requests: Dict[str, asyncio.Future] = {}
self.bot: Bot | None = None
self.self_id: int | None = None
self.code_executor = code_executor
async def connect(self):
async def connect(self) -> None:
"""
启动并管理 WebSocket 连接。
@@ -63,7 +65,8 @@ class WS:
logger.info(f"正在尝试连接至 NapCat: {self.url}")
async with websockets.connect(
self.url, additional_headers=headers
) as websocket:
) as websocket_raw:
websocket = cast(WebSocketClientProtocol, websocket_raw)
self.ws = websocket
logger.success("连接成功!")
await self._listen_loop(websocket)
@@ -79,7 +82,7 @@ class WS:
logger.info(f"{self.reconnect_interval}秒后尝试重连...")
await asyncio.sleep(self.reconnect_interval)
async def _listen_loop(self, websocket_connection):
async def _listen_loop(self, websocket_connection: WebSocketClientProtocol) -> None:
"""
核心监听循环,处理所有接收到的 WebSocket 消息。
@@ -111,7 +114,7 @@ class WS:
except Exception as e:
logger.exception(f"解析消息异常: {e}")
async def on_event(self, event_data: dict):
async def on_event(self, event_data: Dict[str, Any]) -> None:
"""
事件处理和分发层。

View File

@@ -60,14 +60,33 @@ Python 自带的 `json` 库性能好像不太好,特别是在处理 OneBot 这
* Rust 编写
* 支持直接返回 `bytes`,减少内存复制。
## 5. Mypyc 编译
## 5. Mypyc 编译 (AOT Compilation)
### 痛点
Python太慢了。。
Python 作为一种解释型语言,在处理 CPU 密集型任务时性能较差。对于机器人框架的核心部分,如 WebSocket 消息解析、事件分发和插件管理,这些代码被高频调用,其性能直接影响机器人的响应速度和吞吐量
### 解决方案
利用 `setup_mypyc.py` 将核心模块编译为 C 扩展
* `core/ws.py`: WebSocket 消息处理循环。
* `core/managers/*.py`: 事件分发逻辑。
我们引入了 `Mypyc`,一个将类型注解的 Python 代码编译为高性能 C 扩展的工具。通过项目根目录下的 `setup_mypyc.py` 脚本,我们可以选择性地将核心模块编译为二进制文件(在 Windows 上是 `.pyd`,在 Linux 上是 `.so`
这些高频调用的代码变成了机器码
**哪些模块被编译了?**
- `core/ws.py`: WebSocket 消息处理循环,这是整个机器人框架的 I/O 中枢。
- `core/managers/*.py`: 所有的核心管理器,如指令管理器、插件管理器等,负责事件分发和业务逻辑。
- `core/utils/*.py`: 高频使用的工具函数。
- `models/*.py`: 数据模型类,如消息段、发送者等。
这些高频调用的代码路径被编译为接近原生机器码的速度,极大地提升了性能。
### 如何编译?
在项目根目录下运行以下指令:
```bash
python setup_mypyc.py
```
脚本会自动查找并编译预设的模块列表。
### 特别注意:关于事件模型的编译
`Mypyc` 对 Python 的某些动态特性和高级用法支持尚不完善。在实践中,我们发现 `dataclass``Mypyc` 存在一些兼容性问题,尤其是在使用继承和某些高级特性(如 `slots=True`)时,可能会导致编译失败或运行时错误(例如 `AttributeError: attribute '__dict__' of 'type' objects is not writable`)。
- **当前状态**:为了确保稳定性,`setup_mypyc.py` 脚本**默认不编译** `models/events/` 目录下的事件模型文件。这些文件虽然也被频繁使用,但它们的结构相对复杂,与 `Mypyc` 的兼容性问题仍在探索中。
- **未来展望**:我们会持续关注 `Mypyc` 的更新,当其对 `dataclass` 的支持得到改善后,会重新尝试将事件模型加入编译列表,以实现极致的性能。
通过这种方式,我们在保证核心模块性能的同时,也维持了项目的稳定性和可维护性。

View File

@@ -22,14 +22,19 @@ source venv/bin/activate
pip install -r requirements.txt
```
### c. 编译核心模块 (可选,但强烈建议)
### c. 编译核心模块 (可选,但为获得最佳性能强烈建议)
为了性能,把核心模块编译成 C 扩展
为了最大化性能,你可以将项目中的核心 Python 模块编译成 C 语言扩展。这将大幅提升机器人的响应速度和处理效率
```bash
python setup_mypyc.py build_ext --inplace
# 确保你在虚拟环境中
python setup_mypyc.py
```
该脚本会自动编译 `core``models` 目录下的指定模块。编译后的文件(`.pyd``.so`)会直接生成在源码旁边。
> **注意**: 编译产物是平台相关的(例如,在 Windows 上编译的 `.pyd` 文件不能在 Linux 上使用)。因此,**请务必在你最终部署的服务器环境(例如 Linux上执行此编译步骤**。更多关于 Mypyc 编译的细节,请参考 [性能优化详解](core-concepts/performance.md)。
## 2. 使用进程管理器
你想直接 `python main.py` 然后关掉 SSH那机器人也跟着停了。必须用进程管理器来守护它。

View File

@@ -22,7 +22,7 @@
├── .gitignore # Git 忽略配置
├── main.py # 主入口文件
├── requirements.txt # Python 依赖列表
└── setup_mypyc.py # Mypyc 编译脚本
└── setup_mypyc.py # [可选] Mypyc 编译脚本,用于将核心模块编译为 C 扩展以提升性能
```
## 重点目录说明

View File

@@ -5,7 +5,7 @@
事件类型常量 `EventType`。所有具体的事件模型都应继承自 `OneBotEvent`。
"""
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Final
from abc import ABC, abstractmethod
if TYPE_CHECKING:
@@ -18,15 +18,15 @@ class EventType:
用于标识不同种类的事件上报。
"""
META = 'meta_event'
META: Final[str] = 'meta_event'
"""元事件 (meta_event): 如心跳、生命周期等。"""
REQUEST = 'request'
REQUEST: Final[str] = 'request'
"""请求事件 (request): 如加好友请求、加群请求等。"""
NOTICE = 'notice'
NOTICE: Final[str] = 'notice'
"""通知事件 (notice): 如群成员增加、文件上传等。"""
MESSAGE = 'message'
MESSAGE: Final[str] = 'message'
"""消息事件 (message): 如私聊消息、群消息等。"""
MESSAGE_SENT = 'message_sent'
MESSAGE_SENT: Final[str] = 'message_sent'
"""消息发送事件 (message_sent): 机器人自己发送消息的上报。"""

View File

@@ -4,7 +4,7 @@
定义了消息相关的事件类,包括 MessageEvent, PrivateMessageEvent, GroupMessageEvent。
"""
from dataclasses import dataclass, field
from typing import List, Optional, Union
from typing import List, Optional, Union, ClassVar
from core.permission import Permission
from models.message import MessageSegment
@@ -27,16 +27,16 @@ class Anonymous:
"""匿名用户 flag"""
@dataclass
@dataclass(slots=True)
class MessageEvent(OneBotEvent):
"""
消息事件基类
"""
# 权限级别常量,用于装饰器参数
ADMIN = Permission.ADMIN
OP = Permission.OP
USER = Permission.USER
ADMIN: ClassVar[Permission] = Permission.ADMIN
OP: ClassVar[Permission] = Permission.OP
USER: ClassVar[Permission] = Permission.USER
message_type: str
"""消息类型: private (私聊), group (群聊)"""
@@ -80,7 +80,7 @@ class MessageEvent(OneBotEvent):
raise NotImplementedError("reply method must be implemented by subclasses")
@dataclass
@dataclass(slots=True)
class PrivateMessageEvent(MessageEvent):
"""
私聊消息事件
@@ -98,7 +98,7 @@ class PrivateMessageEvent(MessageEvent):
)
@dataclass
@dataclass(slots=True)
class GroupMessageEvent(MessageEvent):
"""
群聊消息事件

View File

@@ -4,7 +4,7 @@
定义了元事件相关的事件类,包括心跳事件和生命周期事件。
"""
from dataclasses import dataclass, field
from typing import Optional
from typing import Optional, Final
from .base import OneBotEvent, EventType
@@ -21,12 +21,12 @@ class LifeCycleSubType:
"""
生命周期子类型枚举
"""
ENABLE = 'enable' # 启用
DISABLE = 'disable' # 禁用
CONNECT = 'connect' # 连接
ENABLE: Final[str] = 'enable' # 启用
DISABLE: Final[str] = 'disable' # 禁用
CONNECT: Final[str] = 'connect' # 连接
@dataclass
@dataclass(slots=True)
class MetaEvent(OneBotEvent):
"""
元事件基类
@@ -40,7 +40,7 @@ class MetaEvent(OneBotEvent):
return EventType.META
@dataclass
@dataclass(slots=True)
class HeartbeatEvent(MetaEvent):
"""
心跳事件,用于确认连接状态
@@ -55,7 +55,7 @@ class HeartbeatEvent(MetaEvent):
"""心跳间隔时间(ms)"""
@dataclass
@dataclass(slots=True)
class LifeCycleEvent(MetaEvent):
"""
生命周期事件,用于通知框架生命周期变化

View File

@@ -21,7 +21,7 @@ class NoticeEvent(OneBotEvent):
return EventType.NOTICE
@dataclass
@dataclass(slots=True)
class FriendAddNoticeEvent(NoticeEvent):
"""
好友添加通知
@@ -30,7 +30,7 @@ class FriendAddNoticeEvent(NoticeEvent):
"""新好友 QQ 号"""
@dataclass
@dataclass(slots=True)
class FriendRecallNoticeEvent(NoticeEvent):
"""
好友消息撤回通知
@@ -42,7 +42,7 @@ class FriendRecallNoticeEvent(NoticeEvent):
"""被撤回的消息 ID"""
@dataclass
@dataclass(slots=True)
class GroupNoticeEvent(NoticeEvent):
"""
群组通知事件基类
@@ -54,7 +54,7 @@ class GroupNoticeEvent(NoticeEvent):
"""用户 QQ 号"""
@dataclass
@dataclass(slots=True)
class GroupRecallNoticeEvent(GroupNoticeEvent):
"""
群消息撤回通知
@@ -66,7 +66,7 @@ class GroupRecallNoticeEvent(GroupNoticeEvent):
"""被撤回的消息 ID"""
@dataclass
@dataclass(slots=True)
class GroupIncreaseNoticeEvent(GroupNoticeEvent):
"""
群成员增加通知
@@ -82,7 +82,7 @@ class GroupIncreaseNoticeEvent(GroupNoticeEvent):
"""
@dataclass
@dataclass(slots=True)
class GroupDecreaseNoticeEvent(GroupNoticeEvent):
"""
群成员减少通知
@@ -100,7 +100,7 @@ class GroupDecreaseNoticeEvent(GroupNoticeEvent):
"""
@dataclass
@dataclass(slots=True)
class GroupAdminNoticeEvent(GroupNoticeEvent):
"""
群管理员变动通知
@@ -113,7 +113,7 @@ class GroupAdminNoticeEvent(GroupNoticeEvent):
"""
@dataclass
@dataclass(slots=True)
class GroupBanNoticeEvent(GroupNoticeEvent):
"""
群禁言通知
@@ -132,7 +132,7 @@ class GroupBanNoticeEvent(GroupNoticeEvent):
"""
@dataclass
@dataclass(slots=True)
class GroupUploadFile:
"""
群文件信息
@@ -150,7 +150,7 @@ class GroupUploadFile:
"""文件总线 ID"""
@dataclass
@dataclass(slots=True)
class GroupUploadNoticeEvent(GroupNoticeEvent):
"""
群文件上传通知
@@ -159,7 +159,7 @@ class GroupUploadNoticeEvent(GroupNoticeEvent):
"""文件信息"""
@dataclass
@dataclass(slots=True)
class NotifyNoticeEvent(NoticeEvent):
"""
系统通知事件基类 (notify)
@@ -175,7 +175,7 @@ class NotifyNoticeEvent(NoticeEvent):
"""发送者 QQ 号"""
@dataclass
@dataclass(slots=True)
class PokeNotifyEvent(NotifyNoticeEvent):
"""
戳一戳通知
@@ -187,7 +187,7 @@ class PokeNotifyEvent(NotifyNoticeEvent):
"""群号 (如果是群内戳一戳)"""
@dataclass
@dataclass(slots=True)
class LuckyKingNotifyEvent(NotifyNoticeEvent):
"""
群红包运气王通知
@@ -199,7 +199,7 @@ class LuckyKingNotifyEvent(NotifyNoticeEvent):
"""运气王 QQ 号"""
@dataclass
@dataclass(slots=True)
class HonorNotifyEvent(NotifyNoticeEvent):
"""
群荣誉变更通知
@@ -216,7 +216,7 @@ class HonorNotifyEvent(NotifyNoticeEvent):
"""
@dataclass
@dataclass(slots=True)
class GroupCardNoticeEvent(GroupNoticeEvent):
"""
群成员名片更新通知
@@ -228,7 +228,7 @@ class GroupCardNoticeEvent(GroupNoticeEvent):
"""旧名片"""
@dataclass
@dataclass(slots=True)
class OfflineFile:
"""
离线文件信息
@@ -243,7 +243,7 @@ class OfflineFile:
"""下载链接"""
@dataclass
@dataclass(slots=True)
class OfflineFileNoticeEvent(NoticeEvent):
"""
接收离线文件通知
@@ -255,7 +255,7 @@ class OfflineFileNoticeEvent(NoticeEvent):
"""文件数据"""
@dataclass
@dataclass(slots=True)
class ClientStatus:
"""
客户端状态
@@ -267,7 +267,7 @@ class ClientStatus:
"""状态描述"""
@dataclass
@dataclass(slots=True)
class ClientStatusNoticeEvent(NoticeEvent):
"""
其他客户端在线状态变更通知
@@ -276,7 +276,7 @@ class ClientStatusNoticeEvent(NoticeEvent):
"""客户端信息"""
@dataclass
@dataclass(slots=True)
class EssenceNoticeEvent(GroupNoticeEvent):
"""
精华消息变动通知

View File

@@ -21,7 +21,7 @@ class RequestEvent(OneBotEvent):
return EventType.REQUEST
@dataclass
@dataclass(slots=True)
class FriendRequestEvent(RequestEvent):
"""
加好友请求事件
@@ -36,7 +36,7 @@ class FriendRequestEvent(RequestEvent):
"""请求 flag在调用处理请求的 API 时需要传入此 flag"""
@dataclass
@dataclass(slots=True)
class GroupRequestEvent(RequestEvent):
"""
加群请求/邀请事件

View File

@@ -31,7 +31,7 @@ class GroupInfo:
"""是否全员禁言"""
@dataclass
@dataclass(slots=True)
class GroupMemberInfo:
"""
群成员信息
@@ -82,7 +82,7 @@ class GroupMemberInfo:
"""是否允许修改群名片"""
@dataclass
@dataclass(slots=True)
class FriendInfo:
"""
好友信息
@@ -97,7 +97,7 @@ class FriendInfo:
"""备注"""
@dataclass
@dataclass(slots=True)
class StrangerInfo:
"""
陌生人信息
@@ -115,7 +115,7 @@ class StrangerInfo:
"""年龄"""
@dataclass
@dataclass(slots=True)
class LoginInfo:
"""
登录号信息
@@ -127,7 +127,7 @@ class LoginInfo:
"""昵称"""
@dataclass
@dataclass(slots=True)
class VersionInfo:
"""
版本信息
@@ -142,7 +142,7 @@ class VersionInfo:
"""OneBot 标准版本"""
@dataclass
@dataclass(slots=True)
class Status:
"""
运行状态
@@ -154,7 +154,7 @@ class Status:
"""运行状态是否良好"""
@dataclass
@dataclass(slots=True)
class EssenceMessage:
"""
精华消息
@@ -181,7 +181,7 @@ class EssenceMessage:
"""消息 ID"""
@dataclass
@dataclass(slots=True)
class CurrentTalkative:
"""
龙王信息
@@ -199,7 +199,7 @@ class CurrentTalkative:
"""持续天数"""
@dataclass
@dataclass(slots=True)
class HonorInfo:
"""
荣誉信息
@@ -217,7 +217,7 @@ class HonorInfo:
"""荣誉描述"""
@dataclass
@dataclass(slots=True)
class GroupHonorInfo:
"""
群荣誉信息

View File

@@ -14,16 +14,47 @@ from distutils.core import setup
from mypyc.build import mypycify
import os
import sys
import glob
import subprocess
# 待编译的模块列表
# 基础模块列表
# 注意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/ws.py', # WebSocket 核心
'core/managers/admin_manager.py', # 管理员管理
'core/managers/permission_manager.py', # 权限管理
'core/managers/plugin_manager.py', # 插件管理器
# 核心基础模块
'core/ws.py', # WebSocket 核心
'core/bot.py', # Bot 核心抽象
'core/config_loader.py', # 配置加载
'core/config_models.py', # 配置模型
'core/permission.py', # 权限枚举
# API 基础模块
'core/api/base.py', # API 基础类
# 数据模型(适合编译的高频使用数据类)
'models/message.py', # 消息段模型
'models/sender.py', # 发送者模型
'models/objects.py', # API 响应数据模型
]
# 注意:事件模型文件暂时不编译,因为它们与 mypyc 存在兼容性问题
# mypyc 对某些数据类特性和继承结构的支持有限,会导致运行时错误
# event_models = glob.glob('models/events/*.py')
# event_models = [m for m in event_models if not m.endswith('__init__.py')]
# modules.extend(event_models)
# 确保文件存在
valid_modules = []
for m in modules:
@@ -36,7 +67,55 @@ if not valid_modules:
print("No valid modules found to compile.")
sys.exit(1)
setup(
name='neobot_core_compiled',
ext_modules=mypycify(valid_modules),
print(f"Compiling the following modules with mypyc: {valid_modules}")
# 使用 mypyc 命令行工具单独编译每个模块,确保位置正确
success_count = 0
for module_path in valid_modules:
print(f"\nCompiling {module_path}...")
try:
# 直接调用 mypyc 命令行工具
result = subprocess.run(
[sys.executable, '-m', 'mypyc', module_path],
capture_output=True,
text=True,
check=True
)
# 验证编译产物是否在正确位置
module_name = module_path.replace('.py', '')
pyd_path = module_name + '.cp314-win_amd64.pyd'
mypyc_path = module_name + '__mypyc.cp314-win_amd64.pyd'
if os.path.exists(pyd_path):
print(f" ✓ Compiled successfully: {pyd_path}")
success_count += 1
else:
# 检查 build 目录中是否有编译产物
build_pyd_path = os.path.join('build', 'lib.win-amd64-cpython-314', pyd_path)
if os.path.exists(build_pyd_path):
# 如果在 build 目录中,复制到正确位置
os.makedirs(os.path.dirname(pyd_path), exist_ok=True)
import shutil
shutil.copy2(build_pyd_path, pyd_path)
shutil.copy2(os.path.join('build', 'lib.win-amd64-cpython-314', mypyc_path), mypyc_path)
print(f" ✓ Compiled successfully (copied from build directory): {pyd_path}")
success_count += 1
else:
print(f" ✗ Compiled but cannot find pyd file")
print(f" Build output:\n{result.stdout[:500]}...")
except subprocess.CalledProcessError as e:
print(f" ✗ Compilation failed with exit code {e.returncode}")
print(f" Error:\n{e.stderr[:500]}...")
except Exception as e:
print(f" ✗ Unexpected error: {e}")
print(f"\n--- Compilation Summary ---")
print(f"Total modules: {len(valid_modules)}")
print(f"Successfully compiled: {success_count}")
print(f"Failed: {len(valid_modules) - success_count}")
if success_count == 0:
print("No modules were compiled successfully. Exiting with error.")
sys.exit(1)

View File

@@ -1,33 +0,0 @@
import importlib
import sys
from unittest.mock import patch, MagicMock
# 模拟插件管理器
class MockPluginManager:
def __init__(self):
self.loaded_plugins = set()
self.command_manager = MagicMock()
self.command_manager.plugins = {}
def load_all_plugins(self):
from core.utils.logger import logger
package_name = "plugins"
module_name = "bad_plugin"
full_module_name = f"{package_name}.{module_name}"
action = "加载"
try:
module = importlib.import_module(full_module_name)
self.loaded_plugins.add(full_module_name)
logger.success(f"成功{action}: {module_name}")
except Exception as e:
print(f"DEBUG: Exception caught in mock: {e}")
print(f"DEBUG: action exists: {'action' in locals()}")
logger.exception(f" {action}插件 {module_name} 失败: {e}")
# 测试
if __name__ == "__main__":
with patch("importlib.import_module", side_effect=Exception("Load error")):
pm = MockPluginManager()
pm.load_all_plugins()
print("Test completed")

View File

@@ -1,24 +0,0 @@
import sys
import os
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# 测试直接导入
print("Testing direct import...")
try:
from core.managers.plugin_manager import logger
print(f"SUCCESS: Imported logger: {logger}")
except Exception as e:
print(f"ERROR: Failed to import logger: {e}")
# 测试模块导入
print("\nTesting module import...")
try:
import core.managers.plugin_manager
print(f"SUCCESS: Imported module: {core.managers.plugin_manager}")
print(f"SUCCESS: Module has logger attribute: {hasattr(core.managers.plugin_manager, 'logger')}")
if hasattr(core.managers.plugin_manager, 'logger'):
print(f"SUCCESS: Logger in module: {core.managers.plugin_manager.logger}")
except Exception as e:
print(f"ERROR: Failed to import module: {e}")

View File

@@ -1,55 +0,0 @@
import sys
import os
from unittest.mock import patch, MagicMock
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# 导入插件管理器
from core.managers.plugin_manager import PluginManager
# 创建测试用例
def test_plugin_error_handling():
# 创建命令管理器模拟
mock_command_manager = MagicMock()
mock_command_manager.plugins = {}
# 创建插件管理器
pm = PluginManager(mock_command_manager)
# 模拟导入错误
def import_side_effect(name, *args, **kwargs):
if name == "plugins.bad_plugin":
raise Exception("Load error")
mock_module = MagicMock()
mock_module.__plugin_meta__ = {"name": "Test Plugin"}
return mock_module
# 打桩
with patch("pkgutil.iter_modules") as mock_iter, \
patch("importlib.import_module", side_effect=import_side_effect), \
patch("os.path.exists", return_value=True), \
patch("core.managers.plugin_manager.logger") as mock_logger:
mock_iter.return_value = [(None, "bad_plugin", False)]
# 执行加载
pm.load_all_plugins()
# 验证
assert "plugins.bad_plugin" not in pm.loaded_plugins
print(f"DEBUG: mock_logger.exception.called: {mock_logger.exception.called}")
print(f"DEBUG: mock_logger.error.called: {mock_logger.error.called}")
print(f"DEBUG: mock_logger method calls: {mock_logger.method_calls}")
# 检查是否调用了日志
if mock_logger.exception.called:
print("SUCCESS: logger.exception was called")
elif mock_logger.error.called:
print("SUCCESS: logger.error was called")
else:
print("ERROR: No logger method was called!")
# 运行测试
if __name__ == "__main__":
test_plugin_error_handling()