refactor(plugin_system): 引入 PluginMetadata 替代 manifest.json
将插件元数据定义从外部 `_manifest.json` 文件迁移到插件 `__init__.py` 文件中的 `__plugin_meta__` 变量。此举简化了插件加载流程,减少了文件I/O,并使元数据与插件代码更紧密地耦合。 主要变更: - 引入 `PluginMetadata` 数据类来标准化插件元数据。 - 插件基类 `PluginBase` 现在直接接收 `PluginMetadata` 对象,不再负责解析 JSON 文件。 - 插件管理器 `PluginManager` 调整加载逻辑,从插件模块的 `__plugin_meta__` 属性获取元数据。 - 删除了 `manifest_utils.py` 及其相关的验证和版本比较逻辑,简化了依赖关系。 - 更新了所有内置插件,以采用新的元数据定义方式,并删除了它们各自的 `_manifest.json` 文件。 BREAKING CHANGE: 插件加载机制已改变。所有插件必须在其 `__init__.py` 中定义一个 `__plugin_meta__` 变量,该变量是 `PluginMetadata` 类的实例,并移除旧的 `_manifest.json` 文件。
This commit is contained in:
@@ -9,7 +9,7 @@ from typing import Any, Optional
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.component_types import ComponentType
|
||||
from src.plugin_system.base.plugin_base import PluginBase
|
||||
from src.plugin_system.utils.manifest_utils import VersionComparator
|
||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||
|
||||
from .component_registry import component_registry
|
||||
|
||||
@@ -27,6 +27,7 @@ class PluginManager:
|
||||
self.plugin_directories: list[str] = [] # 插件根目录列表
|
||||
self.plugin_classes: dict[str, type[PluginBase]] = {} # 全局插件类注册表,插件名 -> 插件类
|
||||
self.plugin_paths: dict[str, str] = {} # 记录插件名到目录路径的映射,插件名 -> 目录路径
|
||||
self.plugin_modules: dict[str, Any] = {} # 记录插件名到模块的映射
|
||||
|
||||
self.loaded_plugins: dict[str, PluginBase] = {} # 已加载的插件类实例注册表,插件名 -> 插件类实例
|
||||
self.failed_plugins: dict[str, str] = {} # 记录加载失败的插件文件及其错误信息,插件名 -> 错误信息
|
||||
@@ -102,23 +103,25 @@ class PluginManager:
|
||||
if not plugin_dir:
|
||||
return False, 1
|
||||
|
||||
plugin_instance = plugin_class(plugin_dir=plugin_dir) # 实例化插件(可能因为缺少manifest而失败)
|
||||
module = self.plugin_modules.get(plugin_name)
|
||||
|
||||
if not module or not hasattr(module, "__plugin_meta__"):
|
||||
self.failed_plugins[plugin_name] = "插件模块中缺少 __plugin_meta__"
|
||||
logger.error(f"❌ 插件加载失败: {plugin_name} - 缺少 __plugin_meta__")
|
||||
return False, 1
|
||||
|
||||
metadata: PluginMetadata = getattr(module, "__plugin_meta__")
|
||||
|
||||
plugin_instance = plugin_class(plugin_dir=plugin_dir, metadata=metadata)
|
||||
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)
|
||||
@@ -138,21 +141,6 @@ class PluginManager:
|
||||
logger.error(f"❌ 插件注册失败: {plugin_name}")
|
||||
return False, 1
|
||||
|
||||
except FileNotFoundError as e:
|
||||
# manifest文件缺失
|
||||
error_msg = f"缺少manifest文件: {e!s}"
|
||||
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验证失败: {e!s}"
|
||||
self.failed_plugins[plugin_name] = error_msg
|
||||
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
|
||||
return False, 1
|
||||
|
||||
except Exception as e:
|
||||
# 其他错误
|
||||
error_msg = f"未知错误: {e!s}"
|
||||
@@ -284,14 +272,23 @@ class PluginManager:
|
||||
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):
|
||||
if self._load_plugin_module_file(plugin_file):
|
||||
module = self._load_plugin_module_file(plugin_file)
|
||||
if module:
|
||||
# 动态查找插件类并获取真实的 plugin_name
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if isinstance(attr, type) and issubclass(attr, PluginBase) and attr is not PluginBase:
|
||||
plugin_name = getattr(attr, "plugin_name", None)
|
||||
if plugin_name:
|
||||
self.plugin_modules[plugin_name] = module
|
||||
break
|
||||
loaded_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
return loaded_count, failed_count
|
||||
|
||||
def _load_plugin_module_file(self, plugin_file: str) -> bool:
|
||||
def _load_plugin_module_file(self, plugin_file: str) -> Optional[Any]:
|
||||
# sourcery skip: extract-method
|
||||
"""加载单个插件模块文件
|
||||
|
||||
@@ -305,62 +302,36 @@ class PluginManager:
|
||||
module_name = ".".join(plugin_path.parent.parts)
|
||||
|
||||
try:
|
||||
# 动态导入插件模块
|
||||
# 首先加载 __init__.py 来获取元数据
|
||||
init_file = os.path.join(plugin_dir, "__init__.py")
|
||||
if os.path.exists(init_file):
|
||||
init_spec = spec_from_file_location(f"{module_name}.__init__", init_file)
|
||||
if init_spec and init_spec.loader:
|
||||
init_module = module_from_spec(init_spec)
|
||||
init_spec.loader.exec_module(init_module)
|
||||
|
||||
# 然后加载 plugin.py
|
||||
spec = spec_from_file_location(module_name, plugin_file)
|
||||
if spec is None or spec.loader is None:
|
||||
logger.error(f"无法创建模块规范: {plugin_file}")
|
||||
return False
|
||||
return None
|
||||
|
||||
module = module_from_spec(spec)
|
||||
module.__package__ = module_name # 设置模块包名
|
||||
module.__package__ = module_name
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# 将 __plugin_meta__ 从 init_module 附加到主模块
|
||||
if init_module and hasattr(init_module, "__plugin_meta__"):
|
||||
setattr(module, "__plugin_meta__", getattr(init_module, "__plugin_meta__"))
|
||||
|
||||
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
|
||||
return True
|
||||
return module
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"加载插件模块 {plugin_file} 失败: {e}"
|
||||
logger.error(error_msg)
|
||||
self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg
|
||||
return False
|
||||
|
||||
# == 兼容性检查 ==
|
||||
|
||||
@staticmethod
|
||||
def _check_plugin_version_compatibility(plugin_name: str, manifest_data: dict[str, Any]) -> tuple[bool, str]:
|
||||
"""检查插件版本兼容性
|
||||
|
||||
Args:
|
||||
plugin_name: 插件名称
|
||||
manifest_data: manifest数据
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否兼容, 错误信息)
|
||||
"""
|
||||
if "host_application" not in manifest_data:
|
||||
return True, "" # 没有版本要求,默认兼容
|
||||
|
||||
host_app = manifest_data["host_application"]
|
||||
if not isinstance(host_app, dict):
|
||||
return True, ""
|
||||
|
||||
min_version = host_app.get("min_version", "")
|
||||
max_version = host_app.get("max_version", "")
|
||||
|
||||
if not min_version and not max_version:
|
||||
return True, "" # 没有版本要求,默认兼容
|
||||
|
||||
try:
|
||||
current_version = VersionComparator.get_current_host_version()
|
||||
is_compatible, error_msg = VersionComparator.is_version_in_range(current_version, min_version, max_version)
|
||||
if not is_compatible:
|
||||
return False, f"版本不兼容: {error_msg}"
|
||||
logger.debug(f"插件 {plugin_name} 版本兼容性检查通过")
|
||||
return True, ""
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"插件 {plugin_name} 版本兼容性检查失败: {e}")
|
||||
return False, f"插件 {plugin_name} 版本兼容性检查失败: {e}" # 检查失败时默认不允许加载
|
||||
return None
|
||||
|
||||
# == 显示统计与插件信息 ==
|
||||
|
||||
@@ -396,17 +367,6 @@ class PluginManager:
|
||||
|
||||
logger.info(f" 📦 {plugin_info.display_name}{extra_info}")
|
||||
|
||||
# Manifest信息
|
||||
if plugin_info.manifest_data:
|
||||
"""
|
||||
if plugin_info.keywords:
|
||||
logger.info(f" 🏷️ 关键词: {', '.join(plugin_info.keywords)}")
|
||||
if plugin_info.categories:
|
||||
logger.info(f" 📁 分类: {', '.join(plugin_info.categories)}")
|
||||
"""
|
||||
if plugin_info.homepage_url:
|
||||
logger.info(f" 🌐 主页: {plugin_info.homepage_url}")
|
||||
|
||||
# 组件列表
|
||||
if plugin_info.components:
|
||||
action_components = [
|
||||
|
||||
Reference in New Issue
Block a user