diff --git a/changes.md b/changes.md index 0776ea653..14dc39790 100644 --- a/changes.md +++ b/changes.md @@ -20,6 +20,7 @@ - `config_api.py`中的`get_global_config`和`get_plugin_config`方法现在支持嵌套访问的配置键名。 - `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时,保证了typing正确;`db_get`方法增加了`single_result`参数,与`db_query`保持一致。 5. 增加了`logging_api`,可以用`get_logger`来获取日志记录器。 +6. 增加了插件和组件管理的API。 # 插件系统修改 1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)** @@ -53,11 +54,13 @@ - 通过`disable_specific_chat_action`,`enable_specific_chat_action`,`disable_specific_chat_command`,`enable_specific_chat_command`,`disable_specific_chat_event_handler`,`enable_specific_chat_event_handler`来操作 - 同样不保存到配置文件~ +# 官方插件修改 +1. `HelloWorld`插件现在有一个样例的`EventHandler`。 + + ### TODO 把这个看起来就很别扭的config获取方式改一下 -来个API管理这些启用禁用! - # 吐槽 ```python diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 72d8e3b37..eb07dbc92 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -23,13 +23,6 @@ from .base import ( EventType, MaiMessages, ) -from .core import ( - plugin_manager, - component_registry, - dependency_manager, - events_manager, - global_announcement_manager, -) # 导入工具模块 from .utils import ( @@ -39,12 +32,42 @@ from .utils import ( # generate_plugin_manifest, ) -from .apis import register_plugin, get_logger +from .apis import ( + chat_api, + component_manage_api, + config_api, + database_api, + emoji_api, + generator_api, + llm_api, + message_api, + person_api, + plugin_manage_api, + send_api, + utils_api, + register_plugin, + get_logger, +) __version__ = "1.0.0" __all__ = [ + # API 模块 + "chat_api", + "component_manage_api", + "config_api", + "database_api", + "emoji_api", + "generator_api", + "llm_api", + "message_api", + "person_api", + "plugin_manage_api", + "send_api", + "utils_api", + "register_plugin", + "get_logger", # 基础类 "BasePlugin", "BaseAction", @@ -63,12 +86,6 @@ __all__ = [ "EventType", # 消息 "MaiMessages", - # 管理器 - "plugin_manager", - "component_registry", - "dependency_manager", - "events_manager", - "global_announcement_manager", # 装饰器 "register_plugin", "ConfigField", diff --git a/src/plugin_system/apis/__init__.py b/src/plugin_system/apis/__init__.py index 05cc62c72..0882fbdc6 100644 --- a/src/plugin_system/apis/__init__.py +++ b/src/plugin_system/apis/__init__.py @@ -7,6 +7,7 @@ # 导入所有API模块 from src.plugin_system.apis import ( chat_api, + component_manage_api, config_api, database_api, emoji_api, @@ -14,15 +15,17 @@ from src.plugin_system.apis import ( llm_api, message_api, person_api, + plugin_manage_api, send_api, utils_api, - plugin_register_api, ) from .logging_api import get_logger from .plugin_register_api import register_plugin + # 导出所有API模块,使它们可以通过 apis.xxx 方式访问 __all__ = [ "chat_api", + "component_manage_api", "config_api", "database_api", "emoji_api", @@ -30,9 +33,9 @@ __all__ = [ "llm_api", "message_api", "person_api", + "plugin_manage_api", "send_api", "utils_api", - "plugin_register_api", "get_logger", "register_plugin", ] diff --git a/src/plugin_system/apis/component_manage_api.py b/src/plugin_system/apis/component_manage_api.py new file mode 100644 index 000000000..545d4ba2b --- /dev/null +++ b/src/plugin_system/apis/component_manage_api.py @@ -0,0 +1,222 @@ +from typing import Optional, Union, Dict +from src.plugin_system.base.component_types import ( + CommandInfo, + ActionInfo, + EventHandlerInfo, + PluginInfo, + ComponentType, +) + + +# === 插件信息查询 === +def get_all_plugin_info() -> Dict[str, PluginInfo]: + """ + 获取所有插件的信息。 + + Returns: + dict: 包含所有插件信息的字典,键为插件名称,值为 PluginInfo 对象。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_all_plugins() + + +def get_plugin_info(plugin_name: str) -> Optional[PluginInfo]: + """ + 获取指定插件的信息。 + + Args: + plugin_name (str): 插件名称。 + + Returns: + PluginInfo: 插件信息对象,如果插件不存在则返回 None。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_plugin_info(plugin_name) + + +# === 组件查询方法 === +def get_component_info( + component_name: str, component_type: ComponentType +) -> Optional[Union[CommandInfo, ActionInfo, EventHandlerInfo]]: + """ + 获取指定组件的信息。 + + Args: + component_name (str): 组件名称。 + component_type (ComponentType): 组件类型。 + Returns: + Union[CommandInfo, ActionInfo, EventHandlerInfo]: 组件信息对象,如果组件不存在则返回 None。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_component_info(component_name, component_type) # type: ignore + + +def get_components_info_by_type( + component_type: ComponentType, +) -> Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]: + """ + 获取指定类型的所有组件信息。 + + Args: + component_type (ComponentType): 组件类型。 + + Returns: + dict: 包含指定类型组件信息的字典,键为组件名称,值为对应的组件信息对象。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_components_by_type(component_type) # type: ignore + + +def get_enabled_components_info_by_type( + component_type: ComponentType, +) -> Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]: + """ + 获取指定类型的所有启用的组件信息。 + + Args: + component_type (ComponentType): 组件类型。 + + Returns: + dict: 包含指定类型启用组件信息的字典,键为组件名称,值为对应的组件信息对象。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_enabled_components_by_type(component_type) # type: ignore + + +# === Action 查询方法 === +def get_registered_action_info(action_name: str) -> Optional[ActionInfo]: + """ + 获取指定 Action 的注册信息。 + + Args: + action_name (str): Action 名称。 + + Returns: + ActionInfo: Action 信息对象,如果 Action 不存在则返回 None。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_registered_action_info(action_name) + + +def get_registered_command_info(command_name: str) -> Optional[CommandInfo]: + """ + 获取指定 Command 的注册信息。 + + Args: + command_name (str): Command 名称。 + + Returns: + CommandInfo: Command 信息对象,如果 Command 不存在则返回 None。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_registered_command_info(command_name) + + +# === EventHandler 特定查询方法 === +def get_registered_event_handler_info( + event_handler_name: str, +) -> Optional[EventHandlerInfo]: + """ + 获取指定 EventHandler 的注册信息。 + + Args: + event_handler_name (str): EventHandler 名称。 + + Returns: + EventHandlerInfo: EventHandler 信息对象,如果 EventHandler 不存在则返回 None。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_registered_event_handler_info(event_handler_name) + + +# === 组件管理方法 === +def globally_enable_component(component_name: str, component_type: ComponentType) -> bool: + """ + 全局启用指定组件。 + + Args: + component_name (str): 组件名称。 + component_type (ComponentType): 组件类型。 + + Returns: + bool: 启用成功返回 True,否则返回 False。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.enable_component(component_name, component_type) + + +async def globally_disable_component(component_name: str, component_type: ComponentType) -> bool: + """ + 全局禁用指定组件。 + + **此函数是异步的,确保在异步环境中调用。** + + Args: + component_name (str): 组件名称。 + component_type (ComponentType): 组件类型。 + + Returns: + bool: 禁用成功返回 True,否则返回 False。 + """ + from src.plugin_system.core.component_registry import component_registry + + return await component_registry.disable_component(component_name, component_type) + + +def locally_enable_component(component_name: str, component_type: ComponentType, stream_id: str) -> bool: + """ + 局部启用指定组件。 + + Args: + component_name (str): 组件名称。 + component_type (ComponentType): 组件类型。 + stream_id (str): 消息流 ID。 + + Returns: + bool: 启用成功返回 True,否则返回 False。 + """ + from src.plugin_system.core.global_announcement_manager import global_announcement_manager + + match component_type: + case ComponentType.ACTION: + return global_announcement_manager.enable_specific_chat_action(stream_id, component_name) + case ComponentType.COMMAND: + return global_announcement_manager.enable_specific_chat_command(stream_id, component_name) + case ComponentType.EVENT_HANDLER: + return global_announcement_manager.enable_specific_chat_event_handler(stream_id, component_name) + case _: + raise ValueError(f"未知 component type: {component_type}") + + +def locally_disable_component(component_name: str, component_type: ComponentType, stream_id: str) -> bool: + """ + 局部禁用指定组件。 + + Args: + component_name (str): 组件名称。 + component_type (ComponentType): 组件类型。 + stream_id (str): 消息流 ID。 + + Returns: + bool: 禁用成功返回 True,否则返回 False。 + """ + from src.plugin_system.core.global_announcement_manager import global_announcement_manager + + match component_type: + case ComponentType.ACTION: + return global_announcement_manager.disable_specific_chat_action(stream_id, component_name) + case ComponentType.COMMAND: + return global_announcement_manager.disable_specific_chat_command(stream_id, component_name) + case ComponentType.EVENT_HANDLER: + return global_announcement_manager.disable_specific_chat_event_handler(stream_id, component_name) + case _: + raise ValueError(f"未知 component type: {component_type}") diff --git a/src/plugin_system/apis/plugin_manage_api.py b/src/plugin_system/apis/plugin_manage_api.py new file mode 100644 index 000000000..1c01119b2 --- /dev/null +++ b/src/plugin_system/apis/plugin_manage_api.py @@ -0,0 +1,95 @@ +from typing import Tuple, List +def list_loaded_plugins() -> List[str]: + """ + 列出所有当前加载的插件。 + + Returns: + list: 当前加载的插件名称列表。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return plugin_manager.list_loaded_plugins() + + +def list_registered_plugins() -> List[str]: + """ + 列出所有已注册的插件。 + + Returns: + list: 已注册的插件名称列表。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return plugin_manager.list_registered_plugins() + + +async def remove_plugin(plugin_name: str) -> bool: + """ + 卸载指定的插件。 + + **此函数是异步的,确保在异步环境中调用。** + + Args: + plugin_name (str): 要卸载的插件名称。 + + Returns: + bool: 卸载是否成功。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return await plugin_manager.remove_registered_plugin(plugin_name) + + +async def reload_plugin(plugin_name: str) -> bool: + """ + 重新加载指定的插件。 + + **此函数是异步的,确保在异步环境中调用。** + + Args: + plugin_name (str): 要重新加载的插件名称。 + + Returns: + bool: 重新加载是否成功。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return await plugin_manager.reload_registered_plugin(plugin_name) + + +def load_plugin(plugin_name: str) -> Tuple[bool, int]: + """ + 加载指定的插件。 + + Args: + plugin_name (str): 要加载的插件名称。 + + Returns: + Tuple[bool, int]: 加载是否成功,成功或失败个数。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return plugin_manager.load_registered_plugin_classes(plugin_name) + +def add_plugin_directory(plugin_directory: str) -> bool: + """ + 添加插件目录。 + + Args: + plugin_directory (str): 要添加的插件目录路径。 + Returns: + bool: 添加是否成功。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return plugin_manager.add_plugin_directory(plugin_directory) + +def rescan_plugin_directory() -> Tuple[int, int]: + """ + 重新扫描插件目录,加载新插件。 + Returns: + Tuple[int, int]: 成功加载的插件数量和失败的插件数量。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + return plugin_manager.rescan_plugin_directory() \ No newline at end of file diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 0112962db..772fc8bd5 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -442,7 +442,7 @@ class ComponentRegistry: command_info, ) - # === 事件处理器特定查询方法 === + # === EventHandler 特定查询方法 === def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]: """获取事件处理器注册表""" @@ -467,9 +467,9 @@ class ComponentRegistry: """获取所有插件""" return self._plugins.copy() - def get_enabled_plugins(self) -> Dict[str, PluginInfo]: - """获取所有启用的插件""" - return {name: info for name, info in self._plugins.items() if info.enabled} + # def get_enabled_plugins(self) -> Dict[str, PluginInfo]: + # """获取所有启用的插件""" + # return {name: info for name, info in self._plugins.items() if info.enabled} def get_plugin_components(self, plugin_name: str) -> List[ComponentInfo]: """获取插件的所有组件""" diff --git a/src/plugin_system/core/global_announcement_manager.py b/src/plugin_system/core/global_announcement_manager.py index afff34f11..9f7052f5d 100644 --- a/src/plugin_system/core/global_announcement_manager.py +++ b/src/plugin_system/core/global_announcement_manager.py @@ -31,6 +31,7 @@ class GlobalAnnouncementManager: self._user_disabled_actions[chat_id].remove(action_name) return True except ValueError: + logger.warning(f"动作 {action_name} 不在禁用列表中") return False return False @@ -51,6 +52,7 @@ class GlobalAnnouncementManager: self._user_disabled_commands[chat_id].remove(command_name) return True except ValueError: + logger.warning(f"命令 {command_name} 不在禁用列表中") return False return False @@ -71,6 +73,7 @@ class GlobalAnnouncementManager: self._user_disabled_event_handlers[chat_id].remove(handler_name) return True except ValueError: + logger.warning(f"事件处理器 {handler_name} 不在禁用列表中") return False return False diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 59dad8bbd..90ba16f4e 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -8,7 +8,7 @@ from pathlib import Path from src.common.logger import get_logger from src.plugin_system.base.plugin_base import PluginBase -from src.plugin_system.base.component_types import ComponentType, PluginInfo, PythonDependency +from src.plugin_system.base.component_types import ComponentType, PythonDependency from src.plugin_system.utils.manifest_utils import VersionComparator from .component_registry import component_registry from .dependency_manager import dependency_manager @@ -75,7 +75,7 @@ class PluginManager: total_failed_registration = 0 for plugin_name in self.plugin_classes.keys(): - load_status, count = self._load_registered_plugin_classes(plugin_name) + load_status, count = self.load_registered_plugin_classes(plugin_name) if load_status: total_registered += 1 else: @@ -85,7 +85,73 @@ class PluginManager: return total_registered, total_failed_registration - async def remove_registered_plugin(self, plugin_name: str) -> None: + def load_registered_plugin_classes(self, plugin_name: str) -> Tuple[bool, int]: + # sourcery skip: extract-duplicate-method, extract-method + """ + 加载已经注册的插件类 + """ + plugin_class = self.plugin_classes.get(plugin_name) + if not plugin_class: + logger.error(f"插件 {plugin_name} 的插件类未注册或不存在") + return False, 1 + try: + # 使用记录的插件目录路径 + plugin_dir = self.plugin_paths.get(plugin_name) + + # 如果没有记录,直接返回失败 + if not plugin_dir: + return False, 1 + + plugin_instance = plugin_class(plugin_dir=plugin_dir) # 实例化插件(可能因为缺少manifest而失败) + if not plugin_instance: + logger.error(f"插件 {plugin_name} 实例化失败") + return False, 1 + # 检查插件是否启用 + if not plugin_instance.enable_plugin: + logger.info(f"插件 {plugin_name} 已禁用,跳过加载") + return False, 0 + + # 检查版本兼容性 + is_compatible, compatibility_error = self._check_plugin_version_compatibility( + plugin_name, plugin_instance.manifest_data + ) + if not is_compatible: + self.failed_plugins[plugin_name] = compatibility_error + logger.error(f"❌ 插件加载失败: {plugin_name} - {compatibility_error}") + return False, 1 + if plugin_instance.register_plugin(): + self.loaded_plugins[plugin_name] = plugin_instance + self._show_plugin_components(plugin_name) + return True, 1 + else: + self.failed_plugins[plugin_name] = "插件注册失败" + logger.error(f"❌ 插件注册失败: {plugin_name}") + return False, 1 + + except FileNotFoundError as e: + # manifest文件缺失 + error_msg = f"缺少manifest文件: {str(e)}" + self.failed_plugins[plugin_name] = error_msg + logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") + return False, 1 + + except ValueError as e: + # manifest文件格式错误或验证失败 + traceback.print_exc() + error_msg = f"manifest验证失败: {str(e)}" + self.failed_plugins[plugin_name] = error_msg + logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") + return False, 1 + + except Exception as e: + # 其他错误 + error_msg = f"未知错误: {str(e)}" + self.failed_plugins[plugin_name] = error_msg + logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") + logger.debug("详细错误信息: ", exc_info=True) + return False, 1 + + async def remove_registered_plugin(self, plugin_name: str) -> bool: """ 禁用插件模块 """ @@ -93,38 +159,40 @@ class PluginManager: raise ValueError("插件名称不能为空") if plugin_name not in self.loaded_plugins: logger.warning(f"插件 {plugin_name} 未加载") - return + return False plugin_instance = self.loaded_plugins[plugin_name] plugin_info = plugin_instance.plugin_info for component in plugin_info.components: await component_registry.remove_component(component.name, component.component_type) del self.loaded_plugins[plugin_name] + return True - async def reload_registered_plugin_module(self, plugin_name: str) -> None: + async def reload_registered_plugin(self, plugin_name: str) -> bool: """ 重载插件模块 """ - await self.remove_registered_plugin(plugin_name) - self._load_registered_plugin_classes(plugin_name) + if not await self.remove_registered_plugin(plugin_name): + return False + if not self.load_registered_plugin_classes(plugin_name)[0]: + return False + logger.debug(f"插件 {plugin_name} 重载成功") + return True - def rescan_plugin_directory(self) -> None: + def rescan_plugin_directory(self) -> Tuple[int, int]: """ 重新扫描插件根目录 """ + total_success = 0 + total_fail = 0 for directory in self.plugin_directories: if os.path.exists(directory): logger.debug(f"重新扫描插件根目录: {directory}") - self._load_plugin_modules_from_directory(directory) + success, fail = self._load_plugin_modules_from_directory(directory) + total_success += success + total_fail += fail else: logger.warning(f"插件根目录不存在: {directory}") - - def get_loaded_plugins(self) -> List[PluginInfo]: - """获取所有已加载的插件信息""" - return list(component_registry.get_all_plugins().values()) - - def get_enabled_plugins(self) -> List[PluginInfo]: - """获取所有启用的插件信息""" - return list(component_registry.get_enabled_plugins().values()) + return total_success, total_fail def get_plugin_instance(self, plugin_name: str) -> Optional["PluginBase"]: """获取插件实例 @@ -235,6 +303,25 @@ class PluginManager: return dependency_manager.generate_requirements_file(all_dependencies, output_path) + # === 查询方法 === + def list_loaded_plugins(self) -> List[str]: + """ + 列出所有当前加载的插件。 + + Returns: + list: 当前加载的插件名称列表。 + """ + return list(self.loaded_plugins.keys()) + + def list_registered_plugins(self) -> List[str]: + """ + 列出所有已注册的插件类。 + + Returns: + list: 已注册的插件类名称列表。 + """ + return list(self.plugin_classes.keys()) + # === 私有方法 === # == 目录管理 == def _ensure_plugin_directories(self) -> None: @@ -310,72 +397,6 @@ class PluginManager: self.failed_plugins[module_name] = error_msg return False - def _load_registered_plugin_classes(self, plugin_name: str) -> Tuple[bool, int]: - # sourcery skip: extract-duplicate-method, extract-method - """ - 加载已经注册的插件类 - """ - plugin_class = self.plugin_classes.get(plugin_name) - if not plugin_class: - logger.error(f"插件 {plugin_name} 的插件类未注册或不存在") - return False, 1 - try: - # 使用记录的插件目录路径 - plugin_dir = self.plugin_paths.get(plugin_name) - - # 如果没有记录,直接返回失败 - if not plugin_dir: - return False, 1 - - plugin_instance = plugin_class(plugin_dir=plugin_dir) # 实例化插件(可能因为缺少manifest而失败) - if not plugin_instance: - logger.error(f"插件 {plugin_name} 实例化失败") - return False, 1 - # 检查插件是否启用 - if not plugin_instance.enable_plugin: - logger.info(f"插件 {plugin_name} 已禁用,跳过加载") - return False, 0 - - # 检查版本兼容性 - is_compatible, compatibility_error = self._check_plugin_version_compatibility( - plugin_name, plugin_instance.manifest_data - ) - if not is_compatible: - self.failed_plugins[plugin_name] = compatibility_error - logger.error(f"❌ 插件加载失败: {plugin_name} - {compatibility_error}") - return False, 1 - if plugin_instance.register_plugin(): - self.loaded_plugins[plugin_name] = plugin_instance - self._show_plugin_components(plugin_name) - return True, 1 - else: - self.failed_plugins[plugin_name] = "插件注册失败" - logger.error(f"❌ 插件注册失败: {plugin_name}") - return False, 1 - - except FileNotFoundError as e: - # manifest文件缺失 - error_msg = f"缺少manifest文件: {str(e)}" - self.failed_plugins[plugin_name] = error_msg - logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") - return False, 1 - - except ValueError as e: - # manifest文件格式错误或验证失败 - traceback.print_exc() - error_msg = f"manifest验证失败: {str(e)}" - self.failed_plugins[plugin_name] = error_msg - logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") - return False, 1 - - except Exception as e: - # 其他错误 - error_msg = f"未知错误: {str(e)}" - self.failed_plugins[plugin_name] = error_msg - logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}") - logger.debug("详细错误信息: ", exc_info=True) - return False, 1 - # == 兼容性检查 == def _check_plugin_version_compatibility(self, plugin_name: str, manifest_data: Dict[str, Any]) -> Tuple[bool, str]: