""" 插件管理插件 提供插件的热重载、启用、禁用等管理功能 支持的指令: /插件列表 - 查看所有插件状态 /重载插件 <名称> - 重载指定插件 /重载所有插件 - 重载所有插件 /启用插件 <名称> - 启用指定插件 /禁用插件 <名称> - 禁用指定插件 /刷新插件 - 扫描并发现新插件 /插件帮助 - 显示帮助信息 """ import sys import tomllib from pathlib import Path from loguru import logger from utils.plugin_base import PluginBase from utils.decorators import on_text_message from utils.plugin_manager import PluginManager class ManagePlugin(PluginBase): """插件管理插件""" # 插件元数据 description = "插件管理,支持热重载、启用、禁用" author = "ShiHao" version = "2.0.0" # 最高加载优先级,确保最先加载 load_priority = 100 def __init__(self): super().__init__() self.config = None self.admins = [] async def async_init(self): """插件异步初始化""" # 从主配置文件读取管理员列表 with open("main_config.toml", "rb") as f: main_config = tomllib.load(f) self.admins = main_config.get("Bot", {}).get("admins", []) logger.info(f"插件管理插件已加载,管理员: {self.admins}") def _check_admin(self, message: dict) -> bool: """检查是否是管理员""" sender_wxid = message.get("SenderWxid", "") from_wxid = message.get("FromWxid", "") is_group = message.get("IsGroup", False) # 私聊时 sender_wxid 可能为空,使用 from_wxid user_wxid = sender_wxid if is_group else from_wxid return user_wxid in self.admins @on_text_message(priority=99) async def handle_command(self, bot, message: dict): """处理管理命令""" content = message.get("Content", "").strip() from_wxid = message.get("FromWxid", "") # 检查权限 if not self._check_admin(message): return True # 继续传递给其他插件 # 插件帮助 if content == "/插件帮助" or content == "/plugin help": await self._show_help(bot, from_wxid) return False # 插件列表 elif content == "/插件列表" or content == "/plugins": await self._list_plugins(bot, from_wxid) return False # 重载所有插件 elif content == "/重载所有插件" or content == "/reload all": await self._reload_all_plugins(bot, from_wxid) return False # 重载插件 elif content.startswith("/重载插件 ") or content.startswith("/reload "): plugin_name = content.split(maxsplit=1)[1].strip() await self._reload_plugin(bot, from_wxid, plugin_name) return False # 启用插件 elif content.startswith("/启用插件 ") or content.startswith("/enable "): plugin_name = content.split(maxsplit=1)[1].strip() await self._enable_plugin(bot, from_wxid, plugin_name) return False # 禁用插件 elif content.startswith("/禁用插件 ") or content.startswith("/disable "): plugin_name = content.split(maxsplit=1)[1].strip() await self._disable_plugin(bot, from_wxid, plugin_name) return False # 刷新插件(发现新插件) elif content == "/刷新插件" or content == "/refresh": await self._refresh_plugins(bot, from_wxid) return False # 加载新插件(从目录加载全新插件) elif content.startswith("/加载插件 ") or content.startswith("/load "): plugin_name = content.split(maxsplit=1)[1].strip() await self._load_new_plugin(bot, from_wxid, plugin_name) return False return True # 不是管理命令,继续传递 async def _list_plugins(self, bot, to_wxid: str): """列出所有插件""" try: pm = PluginManager() plugins = pm.get_plugin_info() if not plugins: await bot.send_text(to_wxid, "暂无插件") return lines = ["📦 插件列表\n"] for plugin in plugins: status = "✅ 已启用" if plugin.get("enabled", False) else "❌ 已禁用" lines.append( f"{status} {plugin.get('name', 'Unknown')}\n" f" 版本: {plugin.get('version', 'Unknown')}\n" f" 作者: {plugin.get('author', 'Unknown')}\n" ) response = "\n".join(lines) await bot.send_text(to_wxid, response) logger.info(f"已发送插件列表给 {to_wxid}") except Exception as e: logger.error(f"列出插件失败: {e}") await bot.send_text(to_wxid, f"❌ 获取插件列表失败: {e}") async def _reload_plugin(self, bot, to_wxid: str, plugin_name: str): """重载插件""" pm = PluginManager() if plugin_name == "ManagePlugin": await bot.send_text(to_wxid, "❌ ManagePlugin 不能被重载") return success = await pm.reload_plugin(plugin_name) if success: await bot.send_text(to_wxid, f"✅ 插件 {plugin_name} 重载成功") logger.info(f"插件 {plugin_name} 已被重载") else: await bot.send_text(to_wxid, f"❌ 插件 {plugin_name} 重载失败") async def _enable_plugin(self, bot, to_wxid: str, plugin_name: str): """启用插件""" pm = PluginManager() # 检查插件是否存在 plugin_info = pm.get_plugin_info(plugin_name) if not plugin_info: await bot.send_text(to_wxid, f"❌ 插件 {plugin_name} 不存在") return # 检查是否已启用 if plugin_info["enabled"]: await bot.send_text(to_wxid, f"ℹ️ 插件 {plugin_name} 已经是启用状态") return # 加载插件 success = await pm.load_plugin(plugin_name) if success: await bot.send_text(to_wxid, f"✅ 插件 {plugin_name} 已启用") logger.info(f"插件 {plugin_name} 已被启用") else: await bot.send_text(to_wxid, f"❌ 插件 {plugin_name} 启用失败") async def _disable_plugin(self, bot, to_wxid: str, plugin_name: str): """禁用插件""" pm = PluginManager() if plugin_name == "ManagePlugin": await bot.send_text(to_wxid, "❌ ManagePlugin 不能被禁用") return # 检查插件是否存在 plugin_info = pm.get_plugin_info(plugin_name) if not plugin_info: await bot.send_text(to_wxid, f"❌ 插件 {plugin_name} 不存在") return # 检查是否已禁用 if not plugin_info["enabled"]: await bot.send_text(to_wxid, f"ℹ️ 插件 {plugin_name} 已经是禁用状态") return # 卸载插件 success = await pm.unload_plugin(plugin_name) if success: await bot.send_text(to_wxid, f"✅ 插件 {plugin_name} 已禁用") logger.info(f"插件 {plugin_name} 已被禁用") else: await bot.send_text(to_wxid, f"❌ 插件 {plugin_name} 禁用失败") async def _show_help(self, bot, to_wxid: str): """显示帮助信息""" help_text = """📦 插件管理帮助 /插件列表 - 查看所有插件状态 /插件帮助 - 显示此帮助信息 /加载插件 <名称> - 加载新插件(无需重启) /重载插件 <名称> - 热重载指定插件 /重载所有插件 - 热重载所有插件 /启用插件 <名称> - 启用已禁用的插件 /禁用插件 <名称> - 禁用指定插件 /刷新插件 - 扫描发现新插件 示例: /加载插件 NewPlugin /重载插件 AIChat /禁用插件 Weather""" await bot.send_text(to_wxid, help_text) async def _reload_all_plugins(self, bot, to_wxid: str): """重载所有插件""" pm = PluginManager() await bot.send_text(to_wxid, "⏳ 正在重载所有插件...") try: # 清理插件相关的模块缓存 modules_to_remove = [ name for name in sys.modules.keys() if name.startswith('plugins.') and 'ManagePlugin' not in name ] for module_name in modules_to_remove: del sys.modules[module_name] # 重载所有插件 reloaded = await pm.reload_plugins() if reloaded: await bot.send_text( to_wxid, f"✅ 重载完成\n已加载 {len(reloaded)} 个插件:\n" + "\n".join(f" • {name}" for name in reloaded) ) logger.success(f"已重载 {len(reloaded)} 个插件") else: await bot.send_text(to_wxid, "⚠️ 没有插件被重载") except Exception as e: logger.error(f"重载所有插件失败: {e}") await bot.send_text(to_wxid, f"❌ 重载失败: {e}") async def _refresh_plugins(self, bot, to_wxid: str): """刷新插件列表,发现新插件""" pm = PluginManager() try: # 记录刷新前的插件数量 old_count = len(pm.plugin_info) # 刷新插件列表 await pm.refresh_plugins() # 计算新发现的插件 new_count = len(pm.plugin_info) new_plugins = new_count - old_count if new_plugins > 0: # 获取新发现的插件名称 new_plugin_names = [ info["name"] for info in pm.plugin_info.values() if not info.get("enabled", False) ][-new_plugins:] await bot.send_text( to_wxid, f"✅ 发现 {new_plugins} 个新插件:\n" + "\n".join(f" • {name}" for name in new_plugin_names) + "\n\n使用 /启用插件 <名称> 来启用" ) logger.info(f"发现 {new_plugins} 个新插件: {new_plugin_names}") else: await bot.send_text(to_wxid, "ℹ️ 没有发现新插件") except Exception as e: logger.error(f"刷新插件失败: {e}") await bot.send_text(to_wxid, f"❌ 刷新失败: {e}") async def _load_new_plugin(self, bot, to_wxid: str, plugin_name: str): """加载全新的插件(支持插件类名或目录名)""" import os import importlib import inspect pm = PluginManager() # 检查是否已加载 if plugin_name in pm.plugins: await bot.send_text(to_wxid, f"ℹ️ 插件 {plugin_name} 已经加载,如需重载请使用 /重载插件") return try: # 尝试查找插件 found = False plugin_class = None actual_plugin_name = None for dirname in os.listdir("plugins"): dirpath = f"plugins/{dirname}" if not os.path.isdir(dirpath) or not os.path.exists(f"{dirpath}/main.py"): continue # 支持通过目录名或类名查找 if dirname == plugin_name or dirname.lower() == plugin_name.lower(): # 通过目录名匹配 module_name = f"plugins.{dirname}.main" # 清理旧的模块缓存 if module_name in sys.modules: del sys.modules[module_name] # 导入模块 module = importlib.import_module(module_name) # 查找插件类 for name, obj in inspect.getmembers(module): if (inspect.isclass(obj) and issubclass(obj, PluginBase) and obj != PluginBase): plugin_class = obj actual_plugin_name = obj.__name__ found = True break if found: break # 尝试通过类名匹配 try: module_name = f"plugins.{dirname}.main" if module_name in sys.modules: del sys.modules[module_name] module = importlib.import_module(module_name) for name, obj in inspect.getmembers(module): if (inspect.isclass(obj) and issubclass(obj, PluginBase) and obj != PluginBase and (obj.__name__ == plugin_name or obj.__name__.lower() == plugin_name.lower())): plugin_class = obj actual_plugin_name = obj.__name__ found = True break if found: break except: continue if not found or not plugin_class: await bot.send_text( to_wxid, f"❌ 未找到插件 {plugin_name}\n" f"请确认:\n" f"1. plugins/{plugin_name}/main.py 存在\n" f"2. main.py 中有继承 PluginBase 的类" ) return # 检查是否已加载(用实际类名再检查一次) if actual_plugin_name in pm.plugins: await bot.send_text(to_wxid, f"ℹ️ 插件 {actual_plugin_name} 已经加载") return # 加载插件 success = await pm._load_plugin_class(plugin_class) if success: await bot.send_text( to_wxid, f"✅ 插件加载成功\n" f"名称: {actual_plugin_name}\n" f"版本: {plugin_class.version}\n" f"作者: {plugin_class.author}" ) logger.success(f"新插件 {actual_plugin_name} 已加载") else: await bot.send_text(to_wxid, f"❌ 插件 {actual_plugin_name} 加载失败") except Exception as e: import traceback logger.error(f"加载新插件失败: {e}\n{traceback.format_exc()}") await bot.send_text(to_wxid, f"❌ 加载失败: {e}")