refactor(plugin_system): 将依赖检查逻辑从插件基类移至插件管理器
将插件的Python包依赖和插件间依赖的检查逻辑,从各个插件实例的初始化阶段 (`PluginBase`),统一前置到插件模块加载阶段 (`PluginManager`)。 这一重构有以下好处: - **提前失败 (Fail-fast)**:在加载插件模块时立即检查依赖,如果依赖不满足,则直接跳过该插件的加载和实例化,避免了不必要的资源消耗和后续的运行时错误。 - **职责单一**: `PluginManager` 负责插件的发现、加载和依赖管理,而 `PluginBase` 更专注于插件自身的业务逻辑和生命周期,使得代码结构更清晰。 - **配置中心化**: 依赖关系现在统一在 `__plugin_meta__` 中声明,而不是分散在插件类的属性中,提高了可维护性。 - **简化插件实现**: 插件开发者不再需要在插件类中定义 `dependencies` 和 `python_dependencies` 属性,只需在 `__init__.py` 中声明元数据即可。
This commit is contained in:
committed by
Windpicker-owo
parent
fcb11e464d
commit
9380231019
@@ -135,11 +135,6 @@ class BasePlugin(PluginBase):
|
|||||||
|
|
||||||
components = self.get_plugin_components()
|
components = self.get_plugin_components()
|
||||||
|
|
||||||
# 检查依赖
|
|
||||||
if not self._check_dependencies():
|
|
||||||
logger.error(f"{self.log_prefix} 依赖检查失败,跳过注册")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 注册所有组件
|
# 注册所有组件
|
||||||
registered_components = []
|
registered_components = []
|
||||||
for component_info, component_class in components:
|
for component_info, component_class in components:
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ class PluginInfo:
|
|||||||
is_built_in: bool = False # 是否为内置插件
|
is_built_in: bool = False # 是否为内置插件
|
||||||
components: list[ComponentInfo] = field(default_factory=list) # 包含的组件列表
|
components: list[ComponentInfo] = field(default_factory=list) # 包含的组件列表
|
||||||
dependencies: list[str] = field(default_factory=list) # 依赖的其他插件
|
dependencies: list[str] = field(default_factory=list) # 依赖的其他插件
|
||||||
python_dependencies: list[PythonDependency] = field(default_factory=list) # Python包依赖
|
python_dependencies: list[str | PythonDependency] = field(default_factory=list) # Python包依赖
|
||||||
config_file: str = "" # 配置文件路径
|
config_file: str = "" # 配置文件路径
|
||||||
metadata: dict[str, Any] = field(default_factory=dict) # 额外元数据
|
metadata: dict[str, Any] = field(default_factory=dict) # 额外元数据
|
||||||
# 新增:manifest相关信息
|
# 新增:manifest相关信息
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from src.config.config import CONFIG_DIR
|
|||||||
from src.plugin_system.base.component_types import (
|
from src.plugin_system.base.component_types import (
|
||||||
PermissionNodeField,
|
PermissionNodeField,
|
||||||
PluginInfo,
|
PluginInfo,
|
||||||
PythonDependency,
|
|
||||||
)
|
)
|
||||||
from src.plugin_system.base.config_types import ConfigField
|
from src.plugin_system.base.config_types import ConfigField
|
||||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||||
@@ -30,8 +29,6 @@ class PluginBase(ABC):
|
|||||||
plugin_name: str
|
plugin_name: str
|
||||||
config_file_name: str
|
config_file_name: str
|
||||||
enable_plugin: bool = True
|
enable_plugin: bool = True
|
||||||
dependencies: list[str] = []
|
|
||||||
python_dependencies: list[str | PythonDependency] = []
|
|
||||||
|
|
||||||
config_schema: dict[str, dict[str, ConfigField] | str] = {}
|
config_schema: dict[str, dict[str, ConfigField] | str] = {}
|
||||||
|
|
||||||
@@ -64,12 +61,6 @@ class PluginBase(ABC):
|
|||||||
self.plugin_description = self.plugin_meta.description
|
self.plugin_description = self.plugin_meta.description
|
||||||
self.plugin_author = self.plugin_meta.author
|
self.plugin_author = self.plugin_meta.author
|
||||||
|
|
||||||
# 标准化Python依赖为PythonDependency对象
|
|
||||||
normalized_python_deps = self._normalize_python_dependencies(self.python_dependencies)
|
|
||||||
|
|
||||||
# 检查Python依赖
|
|
||||||
self._check_python_dependencies(normalized_python_deps)
|
|
||||||
|
|
||||||
# 创建插件信息对象
|
# 创建插件信息对象
|
||||||
self.plugin_info = PluginInfo(
|
self.plugin_info = PluginInfo(
|
||||||
name=self.plugin_name,
|
name=self.plugin_name,
|
||||||
@@ -80,8 +71,8 @@ class PluginBase(ABC):
|
|||||||
enabled=self._is_enabled,
|
enabled=self._is_enabled,
|
||||||
is_built_in=False,
|
is_built_in=False,
|
||||||
config_file=self.config_file_name or "",
|
config_file=self.config_file_name or "",
|
||||||
dependencies=self.dependencies.copy(),
|
dependencies=self.plugin_meta.dependencies.copy(),
|
||||||
python_dependencies=normalized_python_deps,
|
python_dependencies=self.plugin_meta.python_dependencies.copy(),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"{self.log_prefix} 插件基类初始化完成")
|
logger.debug(f"{self.log_prefix} 插件基类初始化完成")
|
||||||
@@ -367,20 +358,6 @@ class PluginBase(ABC):
|
|||||||
self._is_enabled = self.config["plugin"]["enabled"]
|
self._is_enabled = self.config["plugin"]["enabled"]
|
||||||
logger.info(f"{self.log_prefix} 从配置更新插件启用状态: {self._is_enabled}")
|
logger.info(f"{self.log_prefix} 从配置更新插件启用状态: {self._is_enabled}")
|
||||||
|
|
||||||
def _check_dependencies(self) -> bool:
|
|
||||||
"""检查插件依赖"""
|
|
||||||
from src.plugin_system.core.component_registry import component_registry
|
|
||||||
|
|
||||||
if not self.dependencies:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for dep in self.dependencies:
|
|
||||||
if not component_registry.get_plugin_info(dep):
|
|
||||||
logger.error(f"{self.log_prefix} 缺少依赖插件: {dep}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_config(self, key: str, default: Any = None) -> Any:
|
def get_config(self, key: str, default: Any = None) -> Any:
|
||||||
"""获取插件配置值,支持嵌套键访问
|
"""获取插件配置值,支持嵌套键访问
|
||||||
|
|
||||||
@@ -403,61 +380,6 @@ class PluginBase(ABC):
|
|||||||
|
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _normalize_python_dependencies(self, dependencies: Any) -> list[PythonDependency]:
|
|
||||||
"""将依赖列表标准化为PythonDependency对象"""
|
|
||||||
from packaging.requirements import Requirement
|
|
||||||
|
|
||||||
normalized = []
|
|
||||||
for dep in dependencies:
|
|
||||||
if isinstance(dep, str):
|
|
||||||
try:
|
|
||||||
# 尝试解析为requirement格式 (如 "package>=1.0.0")
|
|
||||||
req = Requirement(dep)
|
|
||||||
version_spec = str(req.specifier) if req.specifier else ""
|
|
||||||
|
|
||||||
normalized.append(
|
|
||||||
PythonDependency(
|
|
||||||
package_name=req.name,
|
|
||||||
version=version_spec,
|
|
||||||
install_name=dep, # 保持原始的安装名称
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# 如果解析失败,作为简单包名处理
|
|
||||||
normalized.append(PythonDependency(package_name=dep, install_name=dep))
|
|
||||||
elif isinstance(dep, PythonDependency):
|
|
||||||
normalized.append(dep)
|
|
||||||
else:
|
|
||||||
logger.warning(f"{self.log_prefix} 未知的依赖格式: {dep}")
|
|
||||||
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
def _check_python_dependencies(self, dependencies: list[PythonDependency]) -> bool:
|
|
||||||
"""检查Python依赖并尝试自动安装"""
|
|
||||||
if not dependencies:
|
|
||||||
logger.info(f"{self.log_prefix} 无Python依赖需要检查")
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 延迟导入以避免循环依赖
|
|
||||||
from src.plugin_system.utils.dependency_manager import get_dependency_manager
|
|
||||||
|
|
||||||
dependency_manager = get_dependency_manager()
|
|
||||||
success, errors = dependency_manager.check_and_install_dependencies(dependencies, self.plugin_name)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
logger.info(f"{self.log_prefix} Python依赖检查通过")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"{self.log_prefix} Python依赖检查失败:")
|
|
||||||
for error in errors:
|
|
||||||
logger.error(f"{self.log_prefix} - {error}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{self.log_prefix} Python依赖检查时发生异常: {e}", exc_info=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def register_plugin(self) -> bool:
|
def register_plugin(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from src.plugin_system.base.component_types import PythonDependency
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PluginMetadata:
|
class PluginMetadata:
|
||||||
@@ -23,5 +25,9 @@ class PluginMetadata:
|
|||||||
keywords: list[str] = field(default_factory=list) # 关键词
|
keywords: list[str] = field(default_factory=list) # 关键词
|
||||||
categories: list[str] = field(default_factory=list) # 分类
|
categories: list[str] = field(default_factory=list) # 分类
|
||||||
|
|
||||||
|
# 依赖关系
|
||||||
|
dependencies: list[str] = field(default_factory=list) # 插件依赖
|
||||||
|
python_dependencies: list[str | PythonDependency] = field(default_factory=list) # Python包依赖
|
||||||
|
|
||||||
# 扩展字段
|
# 扩展字段
|
||||||
extra: dict[str, Any] = field(default_factory=dict) # 其他任意信息
|
extra: dict[str, Any] = field(default_factory=dict) # 其他任意信息
|
||||||
|
|||||||
@@ -323,6 +323,33 @@ class PluginManager:
|
|||||||
init_module = module_from_spec(init_spec)
|
init_module = module_from_spec(init_spec)
|
||||||
init_spec.loader.exec_module(init_module)
|
init_spec.loader.exec_module(init_module)
|
||||||
|
|
||||||
|
# --- 在这里进行依赖检查 ---
|
||||||
|
if hasattr(init_module, "__plugin_meta__"):
|
||||||
|
metadata = getattr(init_module, "__plugin_meta__")
|
||||||
|
from src.plugin_system.utils.dependency_manager import get_dependency_manager
|
||||||
|
|
||||||
|
dependency_manager = get_dependency_manager()
|
||||||
|
|
||||||
|
# 1. 检查Python依赖
|
||||||
|
if metadata.python_dependencies:
|
||||||
|
success, errors = dependency_manager.check_and_install_dependencies(
|
||||||
|
metadata.python_dependencies, metadata.name
|
||||||
|
)
|
||||||
|
if not success:
|
||||||
|
error_msg = f"Python依赖检查失败: {', '.join(errors)}"
|
||||||
|
self.failed_plugins[plugin_name] = error_msg
|
||||||
|
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
|
||||||
|
return None # 依赖检查失败,不加载该模块
|
||||||
|
|
||||||
|
# 2. 检查插件依赖
|
||||||
|
if not self._check_plugin_dependencies(metadata):
|
||||||
|
error_msg = f"插件依赖检查失败: 请确保依赖 {metadata.dependencies} 已正确安装并加载。"
|
||||||
|
self.failed_plugins[plugin_name] = error_msg
|
||||||
|
logger.error(f"❌ 插件加载失败: {plugin_name} - {error_msg}")
|
||||||
|
return None # 插件依赖检查失败
|
||||||
|
|
||||||
|
# --- 依赖检查逻辑结束 ---
|
||||||
|
|
||||||
# 然后加载 plugin.py
|
# 然后加载 plugin.py
|
||||||
spec = spec_from_file_location(module_name, plugin_file)
|
spec = spec_from_file_location(module_name, plugin_file)
|
||||||
if spec is None or spec.loader is None:
|
if spec is None or spec.loader is None:
|
||||||
@@ -335,7 +362,8 @@ class PluginManager:
|
|||||||
|
|
||||||
# 将 __plugin_meta__ 从 init_module 附加到主模块
|
# 将 __plugin_meta__ 从 init_module 附加到主模块
|
||||||
if init_module and hasattr(init_module, "__plugin_meta__"):
|
if init_module and hasattr(init_module, "__plugin_meta__"):
|
||||||
setattr(module, "__plugin_meta__", getattr(init_module, "__plugin_meta__"))
|
metadata = getattr(init_module, "__plugin_meta__")
|
||||||
|
setattr(module, "__plugin_meta__", metadata)
|
||||||
|
|
||||||
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
|
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
|
||||||
return module
|
return module
|
||||||
@@ -346,6 +374,20 @@ class PluginManager:
|
|||||||
self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg
|
self.failed_plugins[plugin_name if "plugin_name" in locals() else module_name] = error_msg
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _check_plugin_dependencies(self, plugin_meta: PluginMetadata) -> bool:
|
||||||
|
"""检查插件的插件依赖"""
|
||||||
|
dependencies = plugin_meta.dependencies
|
||||||
|
if not dependencies:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for dep_name in dependencies:
|
||||||
|
# 检查依赖的插件类是否已注册
|
||||||
|
if dep_name not in self.plugin_classes:
|
||||||
|
logger.error(f"插件 '{plugin_meta.name}' 缺少依赖: 插件 '{dep_name}' 未找到或加载失败。")
|
||||||
|
return False
|
||||||
|
logger.debug(f"插件 '{plugin_meta.name}' 的所有依赖都已找到。")
|
||||||
|
return True
|
||||||
|
|
||||||
# == 显示统计与插件信息 ==
|
# == 显示统计与插件信息 ==
|
||||||
|
|
||||||
def _show_stats(self, total_registered: int, total_failed_registration: int):
|
def _show_stats(self, total_registered: int, total_failed_registration: int):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||||
|
|
||||||
|
# 定义插件元数据
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="MoFox-Bot工具箱",
|
name="MoFox-Bot工具箱",
|
||||||
description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
|
description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
|
||||||
@@ -11,4 +12,6 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
|
keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
|
||||||
categories=["Chat", "Integration"],
|
categories=["Chat", "Integration"],
|
||||||
extra={"is_built_in": "true", "plugin_type": "functional"},
|
extra={"is_built_in": "true", "plugin_type": "functional"},
|
||||||
|
dependencies=[],
|
||||||
|
python_dependencies=["httpx", "Pillow"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
extra={
|
extra={
|
||||||
"is_built_in": False,
|
"is_built_in": False,
|
||||||
"plugin_type": "tools",
|
"plugin_type": "tools",
|
||||||
}
|
},
|
||||||
|
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class TTSVoicePlugin(BasePlugin):
|
|||||||
plugin_author = "Kilo Code & 靚仔"
|
plugin_author = "Kilo Code & 靚仔"
|
||||||
config_file_name = "config.toml"
|
config_file_name = "config.toml"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
|
|
||||||
|
|
||||||
permission_nodes: list[PermissionNodeField] = [
|
permission_nodes: list[PermissionNodeField] = [
|
||||||
PermissionNodeField(node_name="command.use", description="是否可以使用 /tts 命令"),
|
PermissionNodeField(node_name="command.use", description="是否可以使用 /tts 命令"),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from src.plugin_system.base.component_types import PythonDependency
|
||||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
@@ -13,4 +14,26 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
extra={
|
extra={
|
||||||
"is_built_in": True,
|
"is_built_in": True,
|
||||||
},
|
},
|
||||||
|
# Python包依赖列表
|
||||||
|
python_dependencies = [ # noqa: RUF012
|
||||||
|
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
|
||||||
|
PythonDependency(
|
||||||
|
package_name="exa_py",
|
||||||
|
description="Exa搜索API客户端库",
|
||||||
|
optional=True, # 如果没有API密钥,这个是可选的
|
||||||
|
),
|
||||||
|
PythonDependency(
|
||||||
|
package_name="tavily",
|
||||||
|
install_name="tavily-python", # 安装时使用这个名称
|
||||||
|
description="Tavily搜索API客户端库",
|
||||||
|
optional=True, # 如果没有API密钥,这个是可选的
|
||||||
|
),
|
||||||
|
PythonDependency(
|
||||||
|
package_name="httpx",
|
||||||
|
version=">=0.20.0",
|
||||||
|
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
|
||||||
|
description="支持SOCKS代理的HTTP客户端库",
|
||||||
|
optional=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -74,29 +74,6 @@ class WEBSEARCHPLUGIN(BasePlugin):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True)
|
logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True)
|
||||||
|
|
||||||
# Python包依赖列表
|
|
||||||
python_dependencies: list[PythonDependency] = [ # noqa: RUF012
|
|
||||||
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
|
|
||||||
PythonDependency(
|
|
||||||
package_name="exa_py",
|
|
||||||
description="Exa搜索API客户端库",
|
|
||||||
optional=True, # 如果没有API密钥,这个是可选的
|
|
||||||
),
|
|
||||||
PythonDependency(
|
|
||||||
package_name="tavily",
|
|
||||||
install_name="tavily-python", # 安装时使用这个名称
|
|
||||||
description="Tavily搜索API客户端库",
|
|
||||||
optional=True, # 如果没有API密钥,这个是可选的
|
|
||||||
),
|
|
||||||
PythonDependency(
|
|
||||||
package_name="httpx",
|
|
||||||
version=">=0.20.0",
|
|
||||||
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
|
|
||||||
description="支持SOCKS代理的HTTP客户端库",
|
|
||||||
optional=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
config_file_name: str = "config.toml" # 配置文件名
|
config_file_name: str = "config.toml" # 配置文件名
|
||||||
|
|
||||||
# 配置节描述
|
# 配置节描述
|
||||||
|
|||||||
Reference in New Issue
Block a user