From 61c8d6b3281a4ae18ae541d013b95274904fe99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=80=E9=93=AC=E9=85=B8=E9=92=BE?= <148796996+K2cr2O1@users.noreply.github.com> Date: Fri, 9 Jan 2026 04:38:51 +0800 Subject: [PATCH 1/2] Dev (#29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 滚木 * 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 导致可能误删其他插件的问题,改为精确匹配 同时调整相关测试用例验证精确匹配行为 --- core/managers/__init__.py | 3 +- core/managers/command_manager.py | 2 +- core/managers/permission_manager.py | 13 +++++-- plugins/admin.py | 6 +-- tests/test_plugin_reload_meta.py | 60 +++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 tests/test_plugin_reload_meta.py diff --git a/core/managers/__init__.py b/core/managers/__init__.py index 10780cb..5be6af9 100644 --- a/core/managers/__init__.py +++ b/core/managers/__init__.py @@ -6,7 +6,7 @@ """ from ..config_loader import global_config from .admin_manager import AdminManager -from .command_manager import CommandManager +from .command_manager import matcher as command_manager from .permission_manager import PermissionManager from .plugin_manager import PluginManager from .redis_manager import RedisManager @@ -20,7 +20,6 @@ admin_manager = AdminManager() permission_manager = PermissionManager() # 命令与事件管理器 (别名 matcher) -command_manager = CommandManager(prefixes=tuple(global_config.bot.command)) matcher = command_manager # 插件管理器 diff --git a/core/managers/command_manager.py b/core/managers/command_manager.py index 522d86e..da555e7 100644 --- a/core/managers/command_manager.py +++ b/core/managers/command_manager.py @@ -93,7 +93,7 @@ class CommandManager: self.request_handler.unregister_by_plugin_name(plugin_name) # 移除插件元信息 - plugins_to_remove = [name for name in self.plugins if name.startswith(plugin_name)] + plugins_to_remove = [name for name in self.plugins if name == plugin_name] for name in plugins_to_remove: del self.plugins[name] diff --git a/core/managers/permission_manager.py b/core/managers/permission_manager.py index de808c1..b7904c3 100644 --- a/core/managers/permission_manager.py +++ b/core/managers/permission_manager.py @@ -170,13 +170,20 @@ class PermissionManager(Singleton): user_permission = await self.get_user_permission(user_id) return user_permission >= required_permission - def get_all_user_permissions(self) -> Dict[str, str]: + async def get_all_user_permissions(self) -> Dict[str, str]: """ - 获取所有已配置的用户权限 + 获取所有已配置的用户权限(包括 AdminManager 中的管理员) :return: 一个包含所有用户权限的字典 """ - return self._data["users"].copy() + permissions = self._data["users"].copy() + + # 合并 AdminManager 中的管理员 + admins = await admin_manager.get_all_admins() + for admin_id in admins: + permissions[str(admin_id)] = Permission.ADMIN.value + + return permissions def get_all_users(self) -> Dict[str, str]: """ diff --git a/plugins/admin.py b/plugins/admin.py index f9e9aa4..6dd0c18 100644 --- a/plugins/admin.py +++ b/plugins/admin.py @@ -18,11 +18,11 @@ __plugin_meta__ = { @command_manager.command("admin", permission=Permission.ADMIN) -async def admin_management(event: MessageEvent, args: str): +async def admin_management(event: MessageEvent, args: list[str]): """ 处理所有权限管理相关的命令。 """ - parts = args.split() + parts = args if not parts: await event.reply(f"用法不正确。\n\n{__plugin_meta__['usage']}") return @@ -73,7 +73,7 @@ async def list_permissions(event: MessageEvent): """ 列出所有具有特殊权限(管理员和操作员)的用户。 """ - permissions = permission_manager.get_all_user_permissions() + permissions = await permission_manager.get_all_user_permissions() if not permissions: await event.reply("当前没有配置任何特殊权限的用户。") return diff --git a/tests/test_plugin_reload_meta.py b/tests/test_plugin_reload_meta.py new file mode 100644 index 0000000..92a9e93 --- /dev/null +++ b/tests/test_plugin_reload_meta.py @@ -0,0 +1,60 @@ + +import pytest +from unittest.mock import MagicMock +from core.managers.command_manager import CommandManager + +class TestPluginReloadMeta: + def test_plugin_meta_persistence(self): + """ + 测试插件加载、卸载和重载过程中元信息的持久性 + """ + # 初始化 CommandManager + command_manager = CommandManager(prefixes=("/",)) + + # 模拟插件名称和元信息 + plugin_name = "plugins.test_plugin" + plugin_meta = { + "name": "测试插件", + "description": "这是一个测试插件", + "usage": "/test" + } + + # 1. 模拟加载插件 + command_manager.plugins[plugin_name] = plugin_meta + + # 验证元信息已注册 + assert plugin_name in command_manager.plugins + assert command_manager.plugins[plugin_name] == plugin_meta + + # 2. 模拟卸载插件 + command_manager.unload_plugin(plugin_name) + + # 验证元信息已移除 + assert plugin_name not in command_manager.plugins + + # 3. 模拟重载插件(重新注册元信息) + # 在实际运行中,PluginManager 会在 reload 后重新赋值 + command_manager.plugins[plugin_name] = plugin_meta + + # 验证元信息已恢复 + assert plugin_name in command_manager.plugins + assert command_manager.plugins[plugin_name] == plugin_meta + + def test_unload_plugin_exact_match(self): + """ + 测试 unload_plugin 是否只移除精确匹配的插件元信息 + """ + command_manager = CommandManager(prefixes=("/",)) + + plugin1 = "plugins.test" + plugin2 = "plugins.test_extra" + + command_manager.plugins[plugin1] = {"name": "Test 1"} + command_manager.plugins[plugin2] = {"name": "Test 2"} + + # 卸载 plugin1 + command_manager.unload_plugin(plugin1) + + # 验证 plugin1 被移除,但 plugin2 仍然存在 + assert plugin1 not in command_manager.plugins + assert plugin2 in command_manager.plugins From f5cbfd6e8a6167dfd318e764f856699c0d26a424 Mon Sep 17 00:00:00 2001 From: baby20162016 <2185823427@qq.com> Date: Fri, 9 Jan 2026 22:08:56 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0thpic=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=20=E6=94=AF=E6=8C=81=E4=B8=80=E6=AC=A1=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=A4=9A=E5=BC=A0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/thpic.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/plugins/thpic.py b/plugins/thpic.py index 2112cf6..0512118 100644 --- a/plugins/thpic.py +++ b/plugins/thpic.py @@ -12,7 +12,7 @@ from models.events.message import MessageEvent, MessageSegment __plugin_meta__ = { "name": "thpic", "description": "来看看东方Project的图片吧!", - "usage": "/thpic", + "usage": "/thpic [nums](1~10)", } @@ -25,7 +25,38 @@ async def handle_echo(bot: Bot, event: MessageEvent, args: list[str]): :param event: 消息事件对象。 :param args: 指令参数列表(未使用)。 """ - try: - await event.reply(str(MessageSegment.image("https://img.paulzzh.com/touhou/random"))) - except Exception as e: - await event.reply(f"报错了。。。{e}") + parts = args + print(parts) + if not parts: + try: + await event.reply( + str(MessageSegment.image("https://img.paulzzh.com/touhou/random")) + ) + except Exception as e: + await event.reply(f"报错了。。。{e}") + else: + if parts[0].isdigit(): + nums = int(parts[0]) + if nums <= 0: + await event.reply("请输入一个大于0的整数。") + return + elif nums > 10: + await event.reply("请输入一个不大于10的整数。") + return + try: + nodes = [] + for _ in range(nums): + nodes.append( + bot.build_forward_node( + user_id=event.self_id, + nickname="机器人", + message=MessageSegment.image( + "https://img.paulzzh.com/touhou/random" + ), + ) + ) + await bot.send_forwarded_messages(event, nodes) + except Exception as e: + await event.reply(f"报错了。。。{e}") + else: + await event.reply(f"用法不正确。\n\n{__plugin_meta__['usage']}")