diff --git a/changes.md b/changes.md index 1f53d7e50..9372fe9f4 100644 --- a/changes.md +++ b/changes.md @@ -1,16 +1,20 @@ # 插件API与规范修改 -1. 现在`plugin_system`的`__init__.py`文件中包含了所有插件API的导入,用户可以直接使用`from plugin_system import *`来导入所有API。 +1. 现在`plugin_system`的`__init__.py`文件中包含了所有插件API的导入,用户可以直接使用`from src.plugin_system import *`来导入所有API。 -2. register_plugin函数现在转移到了`plugin_system.apis.plugin_register_api`模块中,用户可以通过`from plugin_system.apis.plugin_register_api import register_plugin`来导入。 +2. register_plugin函数现在转移到了`plugin_system.apis.plugin_register_api`模块中,用户可以通过`from src.plugin_system.apis.plugin_register_api import register_plugin`来导入。 + - 顺便一提,按照1中说法,你可以这么用: + ```python + from src.plugin_system import register_plugin + ``` -3. 现在强制要求的property如下: - - `plugin_name`: 插件名称,必须是唯一的。(与文件夹相同) - - `enable_plugin`: 是否启用插件,默认为`True`。 - - `dependencies`: 插件依赖的其他插件列表,默认为空。**现在并不检查(也许)** - - `python_dependencies`: 插件依赖的Python包列表,默认为空。**现在并不检查** - - `config_file_name`: 插件配置文件名,默认为`config.toml`。 - - `config_schema`: 插件配置文件的schema,用于自动生成配置文件。 +3. 现在强制要求的property如下,即你必须覆盖的属性有: + - `plugin_name`: 插件名称,必须是唯一的。(与文件夹相同) + - `enable_plugin`: 是否启用插件,默认为`True`。 + - `dependencies`: 插件依赖的其他插件列表,默认为空。**现在并不检查(也许)** + - `python_dependencies`: 插件依赖的Python包列表,默认为空。**现在并不检查** + - `config_file_name`: 插件配置文件名,默认为`config.toml`。 + - `config_schema`: 插件配置文件的schema,用于自动生成配置文件。 # 插件系统修改 1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)** @@ -22,5 +26,32 @@ - `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时,保证了typing正确;`db_get`方法增加了`single_result`参数,与`db_query`保持一致。 4. 现在增加了参数类型检查,完善了对应注释 5. 现在插件抽象出了总基类 `PluginBase` - - 基于`Action`和`Command`的插件基类现在为`BasePlugin`,它继承自`PluginBase`,由`register_plugin`装饰器注册。 - - 基于`Event`的插件基类现在为`BaseEventPlugin`,它也继承自`PluginBase`,由`register_event_plugin`装饰器注册。 \ No newline at end of file + - 基于`Action`和`Command`的插件基类现在为`BasePlugin`。 + - 基于`Event`的插件基类现在为`BaseEventPlugin`。 + - 所有的插件都继承自`PluginBase`。 + - 所有的插件都由`register_plugin`装饰器注册。 +6. 现在我们终于可以让插件有自定义的名字了! + - 真正实现了插件的`plugin_name`**不受文件夹名称限制**的功能。(吐槽:可乐你的某个小小细节导致我搞了好久……) + - 通过在插件类中定义`plugin_name`属性来指定插件内部标识符。 + - 由于此更改一个文件中现在可以有多个插件类,但每个插件类必须有**唯一的**`plugin_name`。 + - 在某些插件加载失败时,现在会显示包名而不是插件内部标识符。 + - 例如:`MaiMBot.plugins.example_plugin`而不是`example_plugin`。 + - 仅在插件 import 失败时会如此,正常注册过程中失败的插件不会显示包名,而是显示插件内部标识符。(这是特性,但是基本上不可能出现这个情况) +7. 现在不支持单文件插件了,加载方式已经完全删除。 + + +# 吐槽 +```python +plugin_path = Path(plugin_file) +if plugin_path.parent.name != "plugins": + # 插件包格式:parent_dir.plugin + module_name = f"plugins.{plugin_path.parent.name}.plugin" +else: + # 单文件格式:plugins.filename + module_name = f"plugins.{plugin_path.stem}" +``` +```python +plugin_path = Path(plugin_file) +module_name = ".".join(plugin_path.parent.parts) +``` +这两个区别很大的。 \ No newline at end of file diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 59e240811..2dba09003 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -18,11 +18,16 @@ from .base import ( CommandInfo, PluginInfo, PythonDependency, + BaseEventHandler, + EventHandlerInfo, + EventType, + BaseEventPlugin, ) -from .core.plugin_manager import ( +from .core import ( plugin_manager, component_registry, dependency_manager, + events_manager, ) # 导入工具模块 @@ -43,6 +48,8 @@ __all__ = [ "BasePlugin", "BaseAction", "BaseCommand", + "BaseEventPlugin", + "BaseEventHandler", # 类型定义 "ComponentType", "ActionActivationType", @@ -52,10 +59,13 @@ __all__ = [ "CommandInfo", "PluginInfo", "PythonDependency", + "EventHandlerInfo", + "EventType", # 管理器 "plugin_manager", "component_registry", "dependency_manager", + "events_manager", # 装饰器 "register_plugin", "ConfigField", diff --git a/src/plugin_system/apis/plugin_register_api.py b/src/plugin_system/apis/plugin_register_api.py index 0047d4843..78f5e50dd 100644 --- a/src/plugin_system/apis/plugin_register_api.py +++ b/src/plugin_system/apis/plugin_register_api.py @@ -1,3 +1,5 @@ +from pathlib import Path + from src.common.logger import get_logger logger = get_logger("plugin_register") @@ -22,8 +24,20 @@ def register_plugin(cls): # 只是注册插件类,不立即实例化 # 插件管理器会负责实例化和注册 - plugin_name = cls.plugin_name or cls.__name__ - plugin_manager.plugin_classes[plugin_name] = cls # type: ignore - logger.debug(f"插件类已注册: {plugin_name}") + plugin_name: str = cls.plugin_name # type: ignore + plugin_manager.plugin_classes[plugin_name] = cls + splitted_name = cls.__module__.split(".") + root_path = Path(__file__) - return cls \ No newline at end of file + # 查找项目根目录 + while not (root_path / "pyproject.toml").exists() and root_path.parent != root_path: + root_path = root_path.parent + + if not (root_path / "pyproject.toml").exists(): + logger.error(f"注册 {plugin_name} 无法找到项目根目录") + return cls + + plugin_manager.plugin_paths[plugin_name] = str(Path(root_path, *splitted_name).resolve()) + logger.debug(f"插件类已注册: {plugin_name}, 路径: {plugin_manager.plugin_paths[plugin_name]}") + + return cls diff --git a/src/plugin_system/base/__init__.py b/src/plugin_system/base/__init__.py index bff325948..6df5fb90c 100644 --- a/src/plugin_system/base/__init__.py +++ b/src/plugin_system/base/__init__.py @@ -7,6 +7,8 @@ from .base_plugin import BasePlugin from .base_action import BaseAction from .base_command import BaseCommand +from .base_event_plugin import BaseEventPlugin +from .base_events_handler import BaseEventHandler from .component_types import ( ComponentType, ActionActivationType, @@ -16,6 +18,8 @@ from .component_types import ( CommandInfo, PluginInfo, PythonDependency, + EventHandlerInfo, + EventType, ) from .config_types import ConfigField @@ -32,4 +36,8 @@ __all__ = [ "PluginInfo", "PythonDependency", "ConfigField", + "EventHandlerInfo", + "EventType", + "BaseEventPlugin", + "BaseEventHandler", ] diff --git a/src/plugin_system/base/base_event_plugin.py b/src/plugin_system/base/base_event_plugin.py index 38ab116cc..df67152a4 100644 --- a/src/plugin_system/base/base_event_plugin.py +++ b/src/plugin_system/base/base_event_plugin.py @@ -1,12 +1,10 @@ from abc import abstractmethod -from typing import List, Tuple, Type, TYPE_CHECKING +from typing import List, Tuple, Type from src.common.logger import get_logger from .plugin_base import PluginBase from .component_types import EventHandlerInfo - -if TYPE_CHECKING: - from src.plugin_system.base.base_events_handler import BaseEventHandler +from .base_events_handler import BaseEventHandler logger = get_logger("base_event_plugin") diff --git a/src/plugin_system/core/__init__.py b/src/plugin_system/core/__init__.py index 50537b903..c6041ece7 100644 --- a/src/plugin_system/core/__init__.py +++ b/src/plugin_system/core/__init__.py @@ -7,9 +7,11 @@ from src.plugin_system.core.plugin_manager import plugin_manager from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.dependency_manager import dependency_manager +from src.plugin_system.core.events_manager import events_manager __all__ = [ "plugin_manager", "component_registry", "dependency_manager", + "events_manager", ] diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index b4050794f..aa9324f1b 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -1,10 +1,12 @@ -from typing import Dict, List, Optional, Tuple, Type, Any import os -from importlib.util import spec_from_file_location, module_from_spec -from inspect import getmodule -from pathlib import Path +import inspect import traceback +from typing import Dict, List, Optional, Tuple, Type, Any +from importlib.util import spec_from_file_location, module_from_spec +from pathlib import Path + + from src.common.logger import get_logger from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.dependency_manager import dependency_manager @@ -28,7 +30,7 @@ class PluginManager: self.plugin_paths: Dict[str, str] = {} # 记录插件名到目录路径的映射,插件名 -> 目录路径 self.loaded_plugins: Dict[str, PluginBase] = {} # 已加载的插件类实例注册表,插件名 -> 插件类实例 - self.failed_plugins: Dict[str, str] = {} # 记录加载失败的插件类及其错误信息,插件名 -> 错误信息 + self.failed_plugins: Dict[str, str] = {} # 记录加载失败的插件文件及其错误信息,插件名 -> 错误信息 # 确保插件目录存在 self._ensure_plugin_directories() @@ -107,13 +109,9 @@ class PluginManager: # 使用记录的插件目录路径 plugin_dir = self.plugin_paths.get(plugin_name) - # 如果没有记录,则尝试查找(fallback) + # 如果没有记录,直接返回失败 if not plugin_dir: - plugin_dir = self._find_plugin_directory(plugin_class) - if plugin_dir: - self.plugin_paths[plugin_name] = plugin_dir # 更新路径 - else: - return False, 1 + return False, 1 plugin_instance = plugin_class(plugin_dir=plugin_dir) # 实例化插件(可能因为缺少manifest而失败) if not plugin_instance: @@ -360,24 +358,14 @@ class PluginManager: logger.debug(f"正在扫描插件根目录: {directory}") - # 遍历目录中的所有Python文件和包 + # 遍历目录中的所有包 for item in os.listdir(directory): item_path = os.path.join(directory, item) - if os.path.isfile(item_path) and item.endswith(".py") and item != "__init__.py": - # 单文件插件 - plugin_name = Path(item_path).stem - if self._load_plugin_module_file(item_path, plugin_name, directory): - loaded_count += 1 - else: - failed_count += 1 - - elif os.path.isdir(item_path) and not item.startswith(".") and not item.startswith("__"): - # 插件包 + if os.path.isdir(item_path) and not item.startswith(".") and not item.startswith("__"): plugin_file = os.path.join(item_path, "plugin.py") if os.path.exists(plugin_file): - plugin_name = item # 使用目录名作为插件名 - if self._load_plugin_module_file(plugin_file, plugin_name, item_path): + if self._load_plugin_module_file(plugin_file): loaded_count += 1 else: failed_count += 1 @@ -387,14 +375,16 @@ class PluginManager: def _find_plugin_directory(self, plugin_class: Type[PluginBase]) -> Optional[str]: """查找插件类对应的目录路径""" try: - module = getmodule(plugin_class) - if module and hasattr(module, "__file__") and module.__file__: - return os.path.dirname(module.__file__) + # module = getmodule(plugin_class) + # if module and hasattr(module, "__file__") and module.__file__: + # return os.path.dirname(module.__file__) + file_path = inspect.getfile(plugin_class) + return os.path.dirname(file_path) except Exception as e: logger.debug(f"通过inspect获取插件目录失败: {e}") return None - def _load_plugin_module_file(self, plugin_file: str, plugin_name: str, plugin_dir: str) -> bool: + def _load_plugin_module_file(self, plugin_file: str) -> bool: # sourcery skip: extract-method """加载单个插件模块文件 @@ -405,12 +395,7 @@ class PluginManager: """ # 生成模块名 plugin_path = Path(plugin_file) - if plugin_path.parent.name != "plugins": - # 插件包格式:parent_dir.plugin - module_name = f"plugins.{plugin_path.parent.name}.plugin" - else: - # 单文件格式:plugins.filename - module_name = f"plugins.{plugin_path.stem}" + module_name = ".".join(plugin_path.parent.parts) try: # 动态导入插件模块 @@ -422,16 +407,13 @@ class PluginManager: module = module_from_spec(spec) spec.loader.exec_module(module) - # 记录插件名和目录路径的映射 - self.plugin_paths[plugin_name] = plugin_dir - logger.debug(f"插件模块加载成功: {plugin_file}") return True except Exception as e: error_msg = f"加载插件模块 {plugin_file} 失败: {e}" logger.error(error_msg) - self.failed_plugins[plugin_name] = error_msg + self.failed_plugins[module_name] = error_msg return False def _check_plugin_version_compatibility(self, plugin_name: str, manifest_data: Dict[str, Any]) -> Tuple[bool, str]: