From 4ee832b5a893d87c052485240201951e854e11f0 Mon Sep 17 00:00:00 2001 From: A0000Xz <122650088+A0000Xz@users.noreply.github.com> Date: Wed, 23 Jul 2025 01:49:28 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E5=9C=B0=E6=8C=89?= =?UTF-8?q?=E7=85=A7=E7=B1=BB=E5=9E=8B=E8=8E=B7=E5=8F=96=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/planner_actions/planner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index dd0a3457c..0218bbac5 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -119,9 +119,7 @@ class ActionPlanner: current_available_actions_dict = self.action_manager.get_using_actions() # 获取完整的动作信息 - all_registered_actions: List[ActionInfo] = list( - component_registry.get_components_by_type(ComponentType.ACTION).values() # type: ignore - ) + all_registered_actions = component_registry.get_components_by_type(ComponentType.ACTION) # type: ignore current_available_actions = {} for action_name in current_available_actions_dict: if action_name in all_registered_actions: From 7a0adba0701df71858ca88fa73912e0e300ac8c9 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 23 Jul 2025 09:16:45 +0800 Subject: [PATCH 2/5] typing --- src/chat/chat_loop/heartFC_chat.py | 2 -- src/chat/planner_actions/planner.py | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 53dd469d0..88f84e29b 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -51,8 +51,6 @@ NO_ACTION = { "action_prompt": "", } -IS_MAI4U = False - install(extra_lines=3) # 注释:原来的动作修改超时常量已移除,因为改为顺序执行 diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 0218bbac5..15eb7f9f3 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -1,7 +1,7 @@ import json import time import traceback -from typing import Dict, Any, Optional, Tuple, List +from typing import Dict, Any, Optional, Tuple from rich.traceback import install from datetime import datetime from json_repair import repair_json @@ -119,7 +119,9 @@ class ActionPlanner: current_available_actions_dict = self.action_manager.get_using_actions() # 获取完整的动作信息 - all_registered_actions = component_registry.get_components_by_type(ComponentType.ACTION) # type: ignore + all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore + ComponentType.ACTION + ) current_available_actions = {} for action_name in current_available_actions_dict: if action_name in all_registered_actions: From 56c2adbaeccee8891daef78669a296e48c0b9045 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 23 Jul 2025 11:07:26 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=92=8C=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=AE=A1=E7=90=86API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changes.md | 7 +- src/plugin_system/__init__.py | 45 ++-- src/plugin_system/apis/__init__.py | 7 +- .../apis/component_manage_api.py | 222 ++++++++++++++++++ src/plugin_system/apis/plugin_manage_api.py | 95 ++++++++ src/plugin_system/core/component_registry.py | 8 +- .../core/global_announcement_manager.py | 3 + src/plugin_system/core/plugin_manager.py | 187 ++++++++------- 8 files changed, 469 insertions(+), 105 deletions(-) create mode 100644 src/plugin_system/apis/component_manage_api.py create mode 100644 src/plugin_system/apis/plugin_manage_api.py 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]: From e15183a422c248d9a231890050fb8b5e3368345d Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 23 Jul 2025 15:53:59 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E4=BD=86=E6=98=AF=E5=8F=AA=E6=9C=89=E4=B8=80=E5=8D=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changes.md | 2 +- plugins/hello_world_plugin/plugin.py | 12 +- .../apis/component_manage_api.py | 23 ++ src/plugin_system/base/base_command.py | 5 +- src/plugins/built_in/core_actions/plugin.py | 13 +- src/plugins/built_in/core_actions/reply.py | 13 +- .../built_in/plugin_management/_manifest.json | 39 +++ .../built_in/plugin_management/plugin.py | 246 ++++++++++++++++++ src/plugins/built_in/tts_plugin/plugin.py | 14 +- 9 files changed, 337 insertions(+), 30 deletions(-) create mode 100644 src/plugins/built_in/plugin_management/_manifest.json create mode 100644 src/plugins/built_in/plugin_management/plugin.py diff --git a/changes.md b/changes.md index 14dc39790..e0746da51 100644 --- a/changes.md +++ b/changes.md @@ -56,7 +56,7 @@ # 官方插件修改 1. `HelloWorld`插件现在有一个样例的`EventHandler`。 - +2. 内置插件增加了一个通过`Command`来管理插件的功能。 ### TODO 把这个看起来就很别扭的config获取方式改一下 diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 14a9d16c5..55b9df82d 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -118,17 +118,17 @@ class HelloWorldPlugin(BasePlugin): """Hello World插件 - 你的第一个MaiCore插件""" # 插件基本信息 - plugin_name = "hello_world_plugin" # 内部标识符 - enable_plugin = True - dependencies = [] # 插件依赖列表 - python_dependencies = [] # Python包依赖列表 - config_file_name = "config.toml" # 配置文件名 + plugin_name: str = "hello_world_plugin" # 内部标识符 + enable_plugin: bool = True + dependencies: List[str] = [] # 插件依赖列表 + python_dependencies: List[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" # 配置文件名 # 配置节描述 config_section_descriptions = {"plugin": "插件基本信息", "greeting": "问候功能配置", "time": "时间查询配置"} # 配置Schema定义 - config_schema = { + config_schema: dict = { "plugin": { "name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"), "version": ConfigField(type=str, default="1.0.0", description="插件版本"), diff --git a/src/plugin_system/apis/component_manage_api.py b/src/plugin_system/apis/component_manage_api.py index 545d4ba2b..d9ea051d9 100644 --- a/src/plugin_system/apis/component_manage_api.py +++ b/src/plugin_system/apis/component_manage_api.py @@ -220,3 +220,26 @@ def locally_disable_component(component_name: str, component_type: ComponentType return global_announcement_manager.disable_specific_chat_event_handler(stream_id, component_name) case _: raise ValueError(f"未知 component type: {component_type}") + +def get_locally_disabled_components(stream_id: str, component_type: ComponentType) -> list[str]: + """ + 获取指定消息流中禁用的组件列表。 + + Args: + stream_id (str): 消息流 ID。 + component_type (ComponentType): 组件类型。 + + Returns: + list[str]: 禁用的组件名称列表。 + """ + from src.plugin_system.core.global_announcement_manager import global_announcement_manager + + match component_type: + case ComponentType.ACTION: + return global_announcement_manager.get_disabled_chat_actions(stream_id) + case ComponentType.COMMAND: + return global_announcement_manager.get_disabled_chat_commands(stream_id) + case ComponentType.EVENT_HANDLER: + return global_announcement_manager.get_disabled_chat_event_handlers(stream_id) + case _: + raise ValueError(f"未知 component type: {component_type}") \ No newline at end of file diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index 813b40529..7909980cb 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -24,9 +24,8 @@ class BaseCommand(ABC): """Command组件的名称""" command_description: str = "" """Command组件的描述""" - - # 默认命令设置(子类可以覆盖) - command_pattern: str = "" + # 默认命令设置 + command_pattern: str = r"" """命令匹配的正则表达式""" command_help: str = "" """命令帮助信息""" diff --git a/src/plugins/built_in/core_actions/plugin.py b/src/plugins/built_in/core_actions/plugin.py index d01177a0a..99bff18aa 100644 --- a/src/plugins/built_in/core_actions/plugin.py +++ b/src/plugins/built_in/core_actions/plugin.py @@ -22,6 +22,7 @@ from src.plugins.built_in.core_actions.reply import ReplyAction logger = get_logger("core_actions") + @register_plugin class CoreActionsPlugin(BasePlugin): """核心动作插件 @@ -35,11 +36,11 @@ class CoreActionsPlugin(BasePlugin): """ # 插件基本信息 - plugin_name = "core_actions" # 内部标识符 - enable_plugin = True - dependencies = [] # 插件依赖列表 - python_dependencies = [] # Python包依赖列表 - config_file_name = "config.toml" + plugin_name: str = "core_actions" # 内部标识符 + enable_plugin: bool = True + dependencies: list[str] = [] # 插件依赖列表 + python_dependencies: list[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" # 配置节描述 config_section_descriptions = { @@ -48,7 +49,7 @@ class CoreActionsPlugin(BasePlugin): } # 配置Schema定义 - config_schema = { + config_schema: dict = { "plugin": { "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), "config_version": ConfigField(type=str, default="0.4.0", description="配置文件版本"), diff --git a/src/plugins/built_in/core_actions/reply.py b/src/plugins/built_in/core_actions/reply.py index a5071c4c9..90aa4889d 100644 --- a/src/plugins/built_in/core_actions/reply.py +++ b/src/plugins/built_in/core_actions/reply.py @@ -1,4 +1,3 @@ - # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode from src.config.config import global_config @@ -8,6 +7,7 @@ from typing import Tuple import asyncio import re import traceback + # 导入依赖的系统组件 from src.common.logger import get_logger @@ -20,6 +20,7 @@ from src.mais4u.constant_s4u import ENABLE_S4U logger = get_logger("reply_action") + class ReplyAction(BaseAction): """回复动作 - 参与聊天回复""" @@ -61,10 +62,10 @@ class ReplyAction(BaseAction): user_id = self.user_id platform = self.platform # logger.info(f"{self.log_prefix} 用户ID: {user_id}, 平台: {platform}") - person_id = get_person_info_manager().get_person_id(platform, user_id) + person_id = get_person_info_manager().get_person_id(platform, user_id) # type: ignore # logger.info(f"{self.log_prefix} 人物ID: {person_id}") person_name = get_person_info_manager().get_value_sync(person_id, "person_name") - reply_to = f"{person_name}:{self.action_message.get('processed_plain_text', '')}" + reply_to = f"{person_name}:{self.action_message.get('processed_plain_text', '')}" # type: ignore logger.info(f"{self.log_prefix} 回复目标: {reply_to}") try: @@ -118,11 +119,9 @@ class ReplyAction(BaseAction): # 存储动作记录 reply_text = f"你对{person_name}进行了回复:{reply_text}" - - + if ENABLE_S4U: await mai_thinking_manager.get_mai_think(self.chat_id).do_think_after_response(reply_text) - await self.store_action_info( action_build_into_prompt=False, @@ -138,4 +137,4 @@ class ReplyAction(BaseAction): except Exception as e: logger.error(f"{self.log_prefix} 回复动作执行失败: {e}") traceback.print_exc() - return False, f"回复失败: {str(e)}" \ No newline at end of file + return False, f"回复失败: {str(e)}" diff --git a/src/plugins/built_in/plugin_management/_manifest.json b/src/plugins/built_in/plugin_management/_manifest.json new file mode 100644 index 000000000..41b3cd9ce --- /dev/null +++ b/src/plugins/built_in/plugin_management/_manifest.json @@ -0,0 +1,39 @@ +{ + "manifest_version": 1, + "name": "插件和组件管理 (Plugin and Component Management)", + "version": "1.0.0", + "description": "通过系统API管理插件和组件的生命周期,包括加载、卸载、启用和禁用等操作。", + "author": { + "name": "MaiBot团队", + "url": "https://github.com/MaiM-with-u" + }, + "license": "GPL-v3.0-or-later", + "host_application": { + "min_version": "0.9.0" + }, + "homepage_url": "https://github.com/MaiM-with-u/maibot", + "repository_url": "https://github.com/MaiM-with-u/maibot", + "keywords": [ + "plugins", + "components", + "management", + "built-in" + ], + "categories": [ + "Core System", + "Plugin Management" + ], + "default_locale": "zh-CN", + "locales_path": "_locales", + "plugin_info": { + "is_built_in": true, + "plugin_type": "plugin_management", + "components": [ + { + "type": "command", + "name": "plugin_management", + "description": "管理插件和组件的生命周期,包括加载、卸载、启用和禁用等操作。" + } + ] + } +} \ No newline at end of file diff --git a/src/plugins/built_in/plugin_management/plugin.py b/src/plugins/built_in/plugin_management/plugin.py new file mode 100644 index 000000000..15c769db2 --- /dev/null +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -0,0 +1,246 @@ +from typing import List, Tuple, Type +from src.plugin_system import ( + BasePlugin, + BaseCommand, + CommandInfo, + ConfigField, + register_plugin, + plugin_manage_api, + component_manage_api, + ComponentInfo, + ComponentType, +) +from src.plugin_system.base.base_action import BaseAction +from src.plugin_system.base.base_events_handler import BaseEventHandler +from src.plugin_system.base.component_types import ActionInfo, EventHandlerInfo + + +class ManagementCommand(BaseCommand): + command_name: str = "management" + description: str = "管理命令" + command_pattern: str = r"(?P^/p_m(\s[a-zA-Z0-9_]+)*\s*$)" + intercept_message: bool = True + + async def execute(self) -> Tuple[bool, str]: + command_list = self.matched_groups["manage_command"].strip().split(" ") + if len(command_list) == 1: + await self.show_help("all") + return True, "帮助已发送" + if len(command_list) == 2: + match command_list[1]: + case "plugin": + await self.show_help("plugin") + case "component": + await self.show_help("component") + case "help": + await self.show_help("all") + case _: + return False, "命令不合法" + if len(command_list) == 3: + if command_list[1] == "plugin": + match command_list[2]: + case "help": + await self.show_help("plugin") + case "list": + await self._list_registered_plugins() + case "list_enabled": + await self._list_loaded_plugins() + case "rescan": + await self._rescan_plugin_dirs() + case _: + return False, "命令不合法" + elif command_list[1] == "component": + match command_list[2]: + case "help": + await self.show_help("component") + return True, "帮助已发送" + case "list": + pass + case _: + return False, "命令不合法" + else: + return False, "命令不合法" + if len(command_list) == 4: + if command_list[1] == "plugin": + match command_list[2]: + case "load": + await self._load_plugin(command_list[3]) + case "unload": + await self._unload_plugin(command_list[3]) + case "reload": + await self._reload_plugin(command_list[3]) + case "add_dir": + await self._add_dir(command_list[3]) + case _: + return False, "命令不合法" + elif command_list[1] == "component": + pass + else: + return False, "命令不合法" + if len(command_list) == 5: + pass + if len(command_list) == 6: + pass + + return True, "命令执行完成" + + async def show_help(self, target: str): + help_msg = "" + match target: + case "all": + help_msg = ( + "管理命令帮助\n" + "/p_m help 管理命令提示\n" + "/p_m plugin 插件管理命令\n" + "/p_m component 组件管理命令\n" + "使用 /p_m plugin help 或 /p_m component help 获取具体帮助" + ) + case "plugin": + help_msg = ( + "插件管理命令帮助\n" + "/p_m plugin help 插件管理命令提示\n" + "/p_m plugin list 列出所有注册的插件\n" + "/p_m plugin list_enabled 列出所有加载(启用)的插件\n" + "/p_m plugin rescan 重新扫描所有目录\n" + "/p_m plugin load 加载指定插件\n" + "/p_m plugin unload 卸载指定插件\n" + "/p_m plugin reload 重新加载指定插件\n" + "/p_m plugin add_dir 添加插件目录\n" + ) + case "component": + help_msg = ( + "组件管理命令帮助\n" + "/p_m component help 组件管理命令提示\n" + "/p_m component list 列出所有注册的组件\n" + "/p_m component list enabled <可选: type> 列出所有启用的组件\n" + "/p_m component list disabled <可选: type> 列出所有禁用的组件\n" + " - 可选项: local,代表当前聊天中的;global,代表全局的\n" + " - 不填时为 global\n" + "/p_m component list type 列出指定类型的组件\n" + "/p_m component global enable <可选: component_type> 全局启用组件\n" + "/p_m component global disable <可选: component_type> 全局禁用组件\n" + "/p_m component local enable <可选: component_type> 本聊天启用组件\n" + "/p_m component local disable <可选: component_type> 本聊天禁用组件\n" + " - 可选项: action, command, event_handler\n" + ) + case _: + return + await self.send_text(help_msg) + + async def _list_loaded_plugins(self): + plugins = plugin_manage_api.list_loaded_plugins() + await self.send_text(f"已加载的插件: {', '.join(plugins)}") + + async def _list_registered_plugins(self): + plugins = plugin_manage_api.list_registered_plugins() + await self.send_text(f"已注册的插件: {', '.join(plugins)}") + + async def _rescan_plugin_dirs(self): + plugin_manage_api.rescan_plugin_directory() + await self.send_text("插件目录重新扫描执行中") + + async def _load_plugin(self, plugin_name: str): + await self.send_text(f"正在加载插件: {plugin_name}") + success, count = plugin_manage_api.load_plugin(plugin_name) + if success: + await self.send_text(f"插件加载成功: {plugin_name}") + else: + if count == 0: + await self.send_text(f"插件{plugin_name}为禁用状态") + await self.send_text(f"插件加载失败: {plugin_name}") + + async def _unload_plugin(self, plugin_name: str): + await self.send_text(f"正在卸载插件: {plugin_name}") + success = plugin_manage_api.remove_plugin(plugin_name) + if success: + await self.send_text(f"插件卸载成功: {plugin_name}") + else: + await self.send_text(f"插件卸载失败: {plugin_name}") + + async def _reload_plugin(self, plugin_name: str): + await self.send_text(f"正在重新加载插件: {plugin_name}") + success = plugin_manage_api.reload_plugin(plugin_name) + if success: + await self.send_text(f"插件重新加载成功: {plugin_name}") + else: + await self.send_text(f"插件重新加载失败: {plugin_name}") + + async def _add_dir(self, dir_path: str): + await self.send_text(f"正在添加插件目录: {dir_path}") + success = plugin_manage_api.add_plugin_directory(dir_path) + if success: + await self.send_text(f"插件目录添加成功: {dir_path}") + else: + await self.send_text(f"插件目录添加失败: {dir_path}") + + def _fetch_all_registered_components(self) -> List[ComponentInfo]: + all_plugin_info = component_manage_api.get_all_plugin_info() + if not all_plugin_info: + return [] + + components_info: List[ComponentInfo] = [] + for plugin_info in all_plugin_info.values(): + components_info.extend(plugin_info.components) + return components_info + + def _fetch_locally_disabled_components(self) -> List[str]: + locally_disabled_components_actions = component_manage_api.get_locally_disabled_components( + self.message.chat_stream.stream_id, ComponentType.ACTION + ) + locally_disabled_components_commands = component_manage_api.get_locally_disabled_components( + self.message.chat_stream.stream_id, ComponentType.COMMAND + ) + locally_disabled_components_event_handlers = component_manage_api.get_locally_disabled_components( + self.message.chat_stream.stream_id, ComponentType.EVENT_HANDLER + ) + return ( + locally_disabled_components_actions + + locally_disabled_components_commands + + locally_disabled_components_event_handlers + ) + + async def _list_all_registered_components(self): + components_info = self._fetch_all_registered_components() + if not components_info: + await self.send_text("没有注册的组件") + return + + all_components_str = ", ".join( + f"{component.name} ({component.component_type})" for component in components_info + ) + await self.send_text(f"已注册的组件: {all_components_str}") + + async def _list_enabled_components(self, target_type: str = "global"): + components_info = self._fetch_all_registered_components() + if not components_info: + await self.send_text("没有注册的组件") + return + + if target_type == "global": + enabled_components = [component for component in components_info if component.enabled] + if not enabled_components: + await self.send_text("没有启用的全局组件") + return + enabled_components_str = ", ".join( + f"{component.name} ({component.component_type})" for component in enabled_components + ) + await self.send_text(f"启用的全局组件: {enabled_components_str}") + elif target_type == "local": + locally_disabled_components = self._fetch_locally_disabled_components() + + + +@register_plugin +class PluginManagementPlugin(BasePlugin): + plugin_name: str = "plugin_management_plugin" + enable_plugin: bool = True + dependencies: list[str] = [] + python_dependencies: list[str] = [] + config_file_name: str = "config.toml" + config_schema: dict = {"plugin": {"enable": ConfigField(bool, default=True, description="是否启用插件")}} + + def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]: + components = [] + if self.get_config("plugin.enable", True): + components.append((ManagementCommand.get_command_info(), ManagementCommand)) + return components diff --git a/src/plugins/built_in/tts_plugin/plugin.py b/src/plugins/built_in/tts_plugin/plugin.py index 7d45f4d30..6683735e4 100644 --- a/src/plugins/built_in/tts_plugin/plugin.py +++ b/src/plugins/built_in/tts_plugin/plugin.py @@ -92,7 +92,7 @@ class TTSAction(BaseAction): # 确保句子结尾有合适的标点 if not any(processed_text.endswith(end) for end in [".", "?", "!", "。", "!", "?"]): - processed_text = processed_text + "。" + processed_text = f"{processed_text}。" return processed_text @@ -107,11 +107,11 @@ class TTSPlugin(BasePlugin): """ # 插件基本信息 - plugin_name = "tts_plugin" # 内部标识符 - enable_plugin = True - dependencies = [] # 插件依赖列表 - python_dependencies = [] # Python包依赖列表 - config_file_name = "config.toml" + plugin_name: str = "tts_plugin" # 内部标识符 + enable_plugin: bool = True + dependencies: list[str] = [] # 插件依赖列表 + python_dependencies: list[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" # 配置节描述 config_section_descriptions = { @@ -121,7 +121,7 @@ class TTSPlugin(BasePlugin): } # 配置Schema定义 - config_schema = { + config_schema: dict = { "plugin": { "name": ConfigField(type=str, default="tts_plugin", description="插件名称", required=True), "version": ConfigField(type=str, default="0.1.0", description="插件版本号"), From ae675faaa055cdd5ea281d0a169e6ca3290dc219 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 23 Jul 2025 15:57:22 +0800 Subject: [PATCH 5/5] version update --- src/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.py b/src/config/config.py index fcbde9871..8345a9f04 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -48,7 +48,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.9.0-snapshot.2" +MMC_VERSION = "0.9.0-snapshot.3" def get_key_comment(toml_table, key):