From 97351ce1aeec8075425509a93c2a2555770d687c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Wed, 13 Aug 2025 12:32:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8F=92=E4=BB=B6Python?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=8A=A8=E6=A3=80=E6=9F=A5=E5=92=8C?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E4=BE=9D=E8=B5=96=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=85=8D=E7=BD=AE=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=92=8C=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/dependency-management.md | 204 +++++++++++-- src/config/config.py | 2 + src/config/official_configs.py | 38 ++- src/plugin_system/__init__.py | 4 + src/plugin_system/base/plugin_base.py | 69 ++++- src/plugin_system/utils/dependency_config.py | 102 +++++++ src/plugin_system/utils/dependency_manager.py | 289 ++++++++++++++++++ src/plugins/built_in/Maizone/plugin.py | 2 +- .../built_in/WEB_SEARCH_TOOL/plugin.py | 28 +- template/bot_config_template.toml | 28 +- 10 files changed, 730 insertions(+), 36 deletions(-) create mode 100644 src/plugin_system/utils/dependency_config.py create mode 100644 src/plugin_system/utils/dependency_manager.py diff --git a/docs/plugins/dependency-management.md b/docs/plugins/dependency-management.md index 4bb4ed000..d558c6703 100644 --- a/docs/plugins/dependency-management.md +++ b/docs/plugins/dependency-management.md @@ -1,40 +1,192 @@ -# 📦 插件依赖管理系统 +# 插件Python依赖管理系统 -现在的Python依赖包管理依然存在问题,请保留你的`python_dependencies`属性,等待后续重构。 +## 概述 -## 📚 详细教程 +插件系统现在支持自动检查和安装Python包依赖。当插件初始化时,系统会: -### PythonDependency 类详解 +1. 检查插件所需的Python包是否已安装 +2. 验证包版本是否满足要求 +3. 自动安装缺失的依赖包(可配置) +4. 提供详细的错误信息和日志 -`PythonDependency`是依赖声明的核心类: +## 配置依赖 + +### 方式1: 简单字符串列表(向后兼容) ```python -PythonDependency( - package_name="PIL", # 导入时的包名 - version=">=11.2.0", # 版本要求 - optional=False, # 是否为可选依赖 - description="图像处理库", # 依赖描述 - install_name="pillow" # pip安装时的包名(可选) +from src.plugin_system import BasePlugin + +@register_plugin +class MyPlugin(BasePlugin): + # 简单的字符串列表格式 + python_dependencies: List[str] = [ + "requests", + "beautifulsoup4>=4.9.0", + "httpx[socks]" + ] +``` + +### 方式2: 详细的PythonDependency对象(推荐) + +```python +from src.plugin_system import BasePlugin, PythonDependency + +@register_plugin +class MyPlugin(BasePlugin): + python_dependencies: List[PythonDependency] = [ + PythonDependency( + package_name="requests", + version=">=2.25.0", + description="HTTP请求库", + optional=False + ), + PythonDependency( + package_name="beautifulsoup4", + version=">=4.9.0", + description="HTML解析库", + optional=False + ), + PythonDependency( + package_name="httpx", + install_name="httpx[socks]", # 安装时使用的名称 + description="支持SOCKS代理的HTTP客户端", + optional=True + ) + ] +``` + +## PythonDependency参数说明 + +- `package_name`: 包名称(用于import检查) +- `version`: 版本要求,支持PEP 440格式(如 ">=1.0.0", "==2.1.3") +- `install_name`: pip安装时使用的名称(如果与package_name不同) +- `description`: 依赖描述,用于日志和错误信息 +- `optional`: 是否为可选依赖,可选依赖缺失不会阻止插件加载 + +## 全局配置 + +创建 `mmc/config/dependency_config.toml` 文件来配置依赖管理行为: + +```toml +[dependency_management] +# 是否启用自动安装 +auto_install = true + +# 安装超时时间(秒) +auto_install_timeout = 300 + +# 是否使用代理 +use_proxy = false +proxy_url = "" + +# pip安装选项 +pip_options = [ + "--no-warn-script-location", + "--disable-pip-version-check" +] + +# 是否允许自动安装(主开关) +allowed_auto_install = true + +# 安装前是否提示用户 +prompt_before_install = false + +# 日志级别 +install_log_level = "INFO" +``` + +## 代理配置 + +如果需要通过代理安装包,可以配置: + +```toml +[dependency_management] +use_proxy = true +proxy_url = "http://proxy.example.com:8080" +# 或者 SOCKS5 代理 +# proxy_url = "socks5://proxy.example.com:1080" +``` + +## 编程方式配置 + +也可以通过代码动态配置依赖管理: + +```python +from src.plugin_system.utils.dependency_config import configure_dependency_settings + +# 禁用自动安装 +configure_dependency_settings(auto_install=False) + +# 设置代理 +configure_dependency_settings( + use_proxy=True, + proxy_url="http://proxy.example.com:8080" ) + +# 修改超时时间 +configure_dependency_settings(auto_install_timeout=600) ``` -#### 参数说明 +## 工作流程 -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `package_name` | str | ✅ | Python导入时使用的包名(如`requests`) | -| `version` | str | ❌ | 版本要求,使用pip格式(如`>=1.0.0`, `==2.1.3`) | -| `optional` | bool | ❌ | 是否为可选依赖,默认`False` | -| `description` | str | ❌ | 依赖的用途描述 | -| `install_name` | str | ❌ | pip安装时的包名,默认与`package_name`相同,用于处理安装名称和导入名称不一致的情况 | +1. **插件初始化**: 当插件类被实例化时,系统自动检查依赖 +2. **依赖标准化**: 将字符串格式的依赖转换为PythonDependency对象 +3. **检查已安装**: 尝试导入每个依赖包并检查版本 +4. **自动安装**: 如果启用,自动安装缺失的依赖 +5. **错误处理**: 记录详细的错误信息和安装日志 -#### 版本格式示例 +## 日志输出示例 -```python -# 常用版本格式 -PythonDependency("requests", ">=2.25.0") # 最小版本 -PythonDependency("numpy", ">=1.20.0,<2.0.0") # 版本范围 -PythonDependency("pillow", "==8.3.2") # 精确版本 -PythonDependency("scipy", ">=1.7.0,!=1.8.0") # 排除特定版本 +``` +[Plugin:web_search_tool] 开始自动安装Python依赖: ['asyncddgs', 'httpx[socks]'] +[Plugin:web_search_tool] ✅ 成功安装: asyncddgs +[Plugin:web_search_tool] ✅ 成功安装: httpx[socks] +[Plugin:web_search_tool] 🎉 所有依赖安装完成 +[Plugin:web_search_tool] Python依赖检查通过 ``` +## 错误处理 + +当依赖检查失败时,系统会: + +1. 记录详细的错误信息 +2. 如果是可选依赖缺失,仅记录警告 +3. 如果是必需依赖缺失且自动安装失败,阻止插件加载 +4. 提供清晰的解决建议 + +## 最佳实践 + +1. **使用详细的PythonDependency对象** 以获得更好的控制和文档 +2. **合理设置可选依赖** 避免非核心功能阻止插件加载 +3. **指定版本要求** 确保兼容性 +4. **添加描述信息** 帮助用户理解依赖的用途 +5. **测试依赖配置** 在不同环境中验证依赖是否正确 + +## 安全考虑 + +- 自动安装功能默认启用,但可以通过配置禁用 +- 所有安装操作都有详细的日志记录 +- 支持设置安装超时以避免长时间挂起 +- 可以通过`allowed_auto_install`全局禁用自动安装 + +## 故障排除 + +### 依赖安装失败 + +1. 检查网络连接 +2. 验证代理设置 +3. 检查pip配置 +4. 查看详细的错误日志 + +### 版本冲突 + +1. 检查现有包的版本 +2. 调整版本要求 +3. 考虑使用虚拟环境 + +### 导入错误 + +1. 确认包名与导入名一致 +2. 检查可选依赖配置 +3. 验证安装是否成功 + diff --git a/src/config/config.py b/src/config/config.py index 2b15c4952..529345076 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -38,6 +38,7 @@ from src.config.official_configs import ( CustomPromptConfig, ScheduleConfig, VideoAnalysisConfig, + DependencyManagementConfig, ) from .api_ada_configs import ( @@ -354,6 +355,7 @@ class Config(ConfigBase): voice: VoiceConfig schedule: ScheduleConfig utils_video: VideoAnalysisConfig = field(default_factory=lambda: VideoAnalysisConfig()) + dependency_management: DependencyManagementConfig = field(default_factory=lambda: DependencyManagementConfig()) @dataclass diff --git a/src/config/official_configs.py b/src/config/official_configs.py index f328ea4a1..7b0c54f1d 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -451,7 +451,7 @@ class ExpressionConfig(ConfigBase): # 如果都没有匹配,返回默认值 return True, True, 300 - def _get_stream_specific_config(self, chat_stream_id: str) -> Optional[tuple[bool, bool, int]]: + def _get_stream_specific_config(self, chat_stream_id: str) -> Optional[tuple[bool, bool, float]]: """ 获取特定聊天流的表达配置 @@ -491,7 +491,7 @@ class ExpressionConfig(ConfigBase): return None - def _get_global_config(self) -> Optional[tuple[bool, bool, int]]: + def _get_global_config(self) -> Optional[tuple[bool, bool, float]]: """ 获取全局表达配置 @@ -863,4 +863,36 @@ class VideoAnalysisConfig(ConfigBase): """批量分析时使用的提示词""" enable_frame_timing: bool = True - """是否在分析中包含帧的时间信息""" \ No newline at end of file + """是否在分析中包含帧的时间信息""" + + +@dataclass +class DependencyManagementConfig(ConfigBase): + """插件Python依赖管理配置类""" + + auto_install: bool = True + """是否启用自动安装Python依赖包""" + + auto_install_timeout: int = 300 + """安装超时时间(秒)""" + + use_proxy: bool = False + """是否使用代理进行包安装""" + + proxy_url: str = "" + """代理URL,如: "http://proxy.example.com:8080" 或 "socks5://proxy.example.com:1080" """ + + pip_options: list[str] = field(default_factory=lambda: [ + "--no-warn-script-location", + "--disable-pip-version-check" + ]) + """pip安装选项""" + + allowed_auto_install: bool = True + """是否允许自动安装(主开关),关闭后所有插件都不会自动安装依赖""" + + prompt_before_install: bool = False + """安装前是否提示用户(暂未实现)""" + + install_log_level: str = "INFO" + """依赖安装日志级别""" \ No newline at end of file diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index a102ecd06..fffed63da 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -35,6 +35,10 @@ from .utils import ( # generate_plugin_manifest, ) +# 导入依赖管理模块 +from .utils.dependency_manager import get_dependency_manager, configure_dependency_manager +from .utils.dependency_config import get_dependency_config, configure_dependency_settings + from .apis import ( chat_api, tool_api, diff --git a/src/plugin_system/base/plugin_base.py b/src/plugin_system/base/plugin_base.py index 0b7f15d17..5cdafa483 100644 --- a/src/plugin_system/base/plugin_base.py +++ b/src/plugin_system/base/plugin_base.py @@ -6,6 +6,7 @@ import toml import json import shutil import datetime +from typing import Union from src.common.logger import get_logger from src.plugin_system.base.component_types import ( @@ -42,8 +43,8 @@ class PluginBase(ABC): @property @abstractmethod - def python_dependencies(self) -> List[PythonDependency]: - return [] # Python包依赖 + def python_dependencies(self) -> List[Union[str, PythonDependency]]: + return [] # Python包依赖,支持字符串列表或PythonDependency对象列表 @property @abstractmethod @@ -87,6 +88,12 @@ class PluginBase(ABC): self.plugin_description = self.get_manifest_info("description", "") self.plugin_author = self._get_author_name() + # 标准化Python依赖为PythonDependency对象 + normalized_python_deps = self._normalize_python_dependencies(self.python_dependencies) + + # 检查Python依赖 + self._check_python_dependencies(normalized_python_deps) + # 创建插件信息对象 self.plugin_info = PluginInfo( name=self.plugin_name, @@ -98,7 +105,7 @@ class PluginBase(ABC): is_built_in=False, config_file=self.config_file_name or "", dependencies=self.dependencies.copy(), - python_dependencies=self.python_dependencies.copy(), + python_dependencies=normalized_python_deps, # manifest相关信息 manifest_data=self.manifest_data.copy(), license=self.get_manifest_info("license", ""), @@ -564,6 +571,62 @@ class PluginBase(ABC): 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 def register_plugin(self) -> bool: """ diff --git a/src/plugin_system/utils/dependency_config.py b/src/plugin_system/utils/dependency_config.py new file mode 100644 index 000000000..6d87ad6fb --- /dev/null +++ b/src/plugin_system/utils/dependency_config.py @@ -0,0 +1,102 @@ +from typing import Optional +from src.common.logger import get_logger + +logger = get_logger("dependency_config") + + +class DependencyConfig: + """依赖管理配置类 - 现在使用全局配置""" + + def __init__(self, global_config=None): + self._global_config = global_config + + def _get_config(self): + """获取全局配置对象""" + if self._global_config is not None: + return self._global_config + + # 延迟导入以避免循环依赖 + try: + from src.config.config import global_config + return global_config + except ImportError: + logger.warning("无法导入全局配置,使用默认设置") + return None + + @property + def auto_install(self) -> bool: + """是否启用自动安装""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.auto_install + return True + + @property + def use_proxy(self) -> bool: + """是否使用代理""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.use_proxy + return False + + @property + def proxy_url(self) -> str: + """代理URL""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.proxy_url + return "" + + @property + def install_timeout(self) -> int: + """安装超时时间(秒)""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.auto_install_timeout + return 300 + + @property + def pip_options(self) -> list: + """pip安装选项""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.pip_options + return [ + "--no-warn-script-location", + "--disable-pip-version-check" + ] + + @property + def allowed_auto_install(self) -> bool: + """是否允许自动安装""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.allowed_auto_install + return True + + @property + def prompt_before_install(self) -> bool: + """安装前是否提示用户""" + config = self._get_config() + if config and hasattr(config, 'dependency_management'): + return config.dependency_management.prompt_before_install + return False + + +# 全局配置实例 +_global_dependency_config: Optional[DependencyConfig] = None + + +def get_dependency_config() -> DependencyConfig: + """获取全局依赖配置实例""" + global _global_dependency_config + if _global_dependency_config is None: + _global_dependency_config = DependencyConfig() + return _global_dependency_config + + +def configure_dependency_settings(**kwargs) -> None: + """配置依赖管理设置 - 注意:这个函数现在仅用于兼容性,实际配置需要修改bot_config.toml""" + logger.info("依赖管理设置现在通过 bot_config.toml 的 [dependency_management] 节进行配置") + logger.info(f"请求的配置更改: {kwargs}") + logger.warning("configure_dependency_settings 函数仅用于兼容性,配置更改不会持久化") \ No newline at end of file diff --git a/src/plugin_system/utils/dependency_manager.py b/src/plugin_system/utils/dependency_manager.py new file mode 100644 index 000000000..b51a54c0a --- /dev/null +++ b/src/plugin_system/utils/dependency_manager.py @@ -0,0 +1,289 @@ +import subprocess +import sys +import importlib +import importlib.util +from typing import List, Dict, Tuple, Optional, Union, Any +from packaging import version +from packaging.requirements import Requirement +import re + +from src.common.logger import get_logger +from src.plugin_system.base.component_types import PythonDependency + +logger = get_logger("dependency_manager") + + +class DependencyManager: + """Python包依赖管理器 + + 负责检查和自动安装插件的Python包依赖 + """ + + def __init__(self, auto_install: bool = True, use_proxy: bool = False, proxy_url: Optional[str] = None): + """初始化依赖管理器 + + Args: + auto_install: 是否自动安装缺失的依赖 + use_proxy: 是否使用代理 + proxy_url: 代理URL + """ + # 延迟导入配置以避免循环依赖 + try: + from src.plugin_system.utils.dependency_config import get_dependency_config + config = get_dependency_config() + + # 优先使用配置文件中的设置,参数作为覆盖 + self.auto_install = config.auto_install if auto_install is True else auto_install + self.use_proxy = config.use_proxy if use_proxy is False else use_proxy + self.proxy_url = config.proxy_url if proxy_url is None else proxy_url + self.install_timeout = config.install_timeout + self.pip_options = config.pip_options.copy() + self.allowed_auto_install = config.allowed_auto_install + + except Exception as e: + logger.warning(f"无法加载依赖配置,使用默认设置: {e}") + self.auto_install = auto_install + self.use_proxy = use_proxy + self.proxy_url = proxy_url + self.install_timeout = 300 + self.pip_options = ["--no-warn-script-location", "--disable-pip-version-check"] + self.allowed_auto_install = True + + def check_dependencies(self, dependencies: Any, plugin_name: str = "") -> Tuple[bool, List[str], List[str]]: + """检查依赖包是否满足要求 + + Args: + dependencies: 依赖列表,支持字符串或PythonDependency对象 + plugin_name: 插件名称,用于日志记录 + + Returns: + Tuple[bool, List[str], List[str]]: (是否全部满足, 缺失的包, 错误信息) + """ + missing_packages = [] + error_messages = [] + log_prefix = f"[Plugin:{plugin_name}] " if plugin_name else "" + + # 标准化依赖格式 + normalized_deps = self._normalize_dependencies(dependencies) + + for dep in normalized_deps: + try: + if not self._check_single_dependency(dep): + logger.info(f"{log_prefix}缺少依赖包: {dep.get_pip_requirement()}") + missing_packages.append(dep.get_pip_requirement()) + except Exception as e: + error_msg = f"检查依赖 {dep.package_name} 时发生错误: {str(e)}" + error_messages.append(error_msg) + logger.error(f"{log_prefix}{error_msg}") + + all_satisfied = len(missing_packages) == 0 and len(error_messages) == 0 + + if all_satisfied: + logger.debug(f"{log_prefix}所有Python依赖检查通过") + else: + logger.warning(f"{log_prefix}Python依赖检查失败: 缺失{len(missing_packages)}个包, {len(error_messages)}个错误") + + return all_satisfied, missing_packages, error_messages + + def install_dependencies(self, packages: List[str], plugin_name: str = "") -> Tuple[bool, List[str]]: + """自动安装缺失的依赖包 + + Args: + packages: 要安装的包列表 + plugin_name: 插件名称,用于日志记录 + + Returns: + Tuple[bool, List[str]]: (是否全部安装成功, 失败的包列表) + """ + if not packages: + return True, [] + + if not self.auto_install or not self.allowed_auto_install: + logger.info(f"[Plugin:{plugin_name}] 自动安装已禁用,跳过安装: {packages}") + return False, packages + + log_prefix = f"[Plugin:{plugin_name}] " if plugin_name else "" + logger.info(f"{log_prefix}开始自动安装Python依赖: {packages}") + + failed_packages = [] + + for package in packages: + try: + if self._install_single_package(package, plugin_name): + logger.info(f"{log_prefix}✅ 成功安装: {package}") + else: + failed_packages.append(package) + logger.error(f"{log_prefix}❌ 安装失败: {package}") + except Exception as e: + failed_packages.append(package) + logger.error(f"{log_prefix}❌ 安装 {package} 时发生异常: {str(e)}") + + success = len(failed_packages) == 0 + if success: + logger.info(f"{log_prefix}🎉 所有依赖安装完成") + else: + logger.error(f"{log_prefix}⚠️ 部分依赖安装失败: {failed_packages}") + + return success, failed_packages + + def check_and_install_dependencies(self, dependencies: Any, plugin_name: str = "") -> Tuple[bool, List[str]]: + """检查并自动安装依赖(组合操作) + + Args: + dependencies: 依赖列表 + plugin_name: 插件名称 + + Returns: + Tuple[bool, List[str]]: (是否全部满足, 错误信息列表) + """ + # 第一步:检查依赖 + all_satisfied, missing_packages, check_errors = self.check_dependencies(dependencies, plugin_name) + + if all_satisfied: + return True, [] + + all_errors = check_errors.copy() + + # 第二步:尝试安装缺失的包 + if missing_packages and self.auto_install: + install_success, failed_packages = self.install_dependencies(missing_packages, plugin_name) + + if not install_success: + all_errors.extend([f"安装失败: {pkg}" for pkg in failed_packages]) + else: + # 安装成功后重新检查 + recheck_satisfied, recheck_missing, recheck_errors = self.check_dependencies(dependencies, plugin_name) + if not recheck_satisfied: + all_errors.extend(recheck_errors) + all_errors.extend([f"安装后仍缺失: {pkg}" for pkg in recheck_missing]) + else: + return True, [] + else: + all_errors.extend([f"缺失依赖: {pkg}" for pkg in missing_packages]) + + return False, all_errors + + def _normalize_dependencies(self, dependencies: Any) -> List[PythonDependency]: + """将依赖列表标准化为PythonDependency对象""" + 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"未知的依赖格式: {dep}") + + return normalized + + def _check_single_dependency(self, dep: PythonDependency) -> bool: + """检查单个依赖是否满足要求""" + try: + # 尝试导入包 + spec = importlib.util.find_spec(dep.package_name) + if spec is None: + return False + + # 如果没有版本要求,导入成功就够了 + if not dep.version: + return True + + # 检查版本要求 + try: + module = importlib.import_module(dep.package_name) + installed_version = getattr(module, '__version__', None) + + if installed_version is None: + # 尝试其他常见的版本属性 + installed_version = getattr(module, 'VERSION', None) + if installed_version is None: + logger.debug(f"无法获取包 {dep.package_name} 的版本信息,假设满足要求") + return True + + # 解析版本要求 + req = Requirement(f"{dep.package_name}{dep.version}") + return version.parse(str(installed_version)) in req.specifier + + except Exception as e: + logger.debug(f"检查包 {dep.package_name} 版本时出错: {e}") + return True # 如果无法检查版本,假设满足要求 + + except ImportError: + return False + except Exception as e: + logger.error(f"检查依赖 {dep.package_name} 时发生未知错误: {e}") + return False + + def _install_single_package(self, package: str, plugin_name: str = "") -> bool: + """安装单个包""" + try: + cmd = [sys.executable, "-m", "pip", "install", package] + + # 添加代理设置 + if self.use_proxy and self.proxy_url: + cmd.extend(["--proxy", self.proxy_url]) + + # 添加配置的pip选项 + cmd.extend(self.pip_options) + + logger.debug(f"[Plugin:{plugin_name}] 执行安装命令: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=self.install_timeout, + check=False + ) + + if result.returncode == 0: + return True + else: + logger.error(f"[Plugin:{plugin_name}] pip安装失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + logger.error(f"[Plugin:{plugin_name}] 安装 {package} 超时") + return False + except Exception as e: + logger.error(f"[Plugin:{plugin_name}] 安装 {package} 时发生异常: {e}") + return False + + +# 全局依赖管理器实例 +_global_dependency_manager: Optional[DependencyManager] = None + + +def get_dependency_manager() -> DependencyManager: + """获取全局依赖管理器实例""" + global _global_dependency_manager + if _global_dependency_manager is None: + _global_dependency_manager = DependencyManager() + return _global_dependency_manager + + +def configure_dependency_manager(auto_install: bool = True, use_proxy: bool = False, proxy_url: Optional[str] = None): + """配置全局依赖管理器""" + global _global_dependency_manager + _global_dependency_manager = DependencyManager( + auto_install=auto_install, + use_proxy=use_proxy, + proxy_url=proxy_url + ) \ No newline at end of file diff --git a/src/plugins/built_in/Maizone/plugin.py b/src/plugins/built_in/Maizone/plugin.py index c1d65e7e7..16ac4c6b0 100644 --- a/src/plugins/built_in/Maizone/plugin.py +++ b/src/plugins/built_in/Maizone/plugin.py @@ -694,7 +694,7 @@ class MaiZonePlugin(BasePlugin): plugin_name: str = "MaiZonePlugin" enable_plugin: bool = True dependencies: List[str] = [] - python_dependencies: List[str] = [] + python_dependencies: List[str] = ["pytz"] config_file_name: str = "config.toml" # 配置节描述 diff --git a/src/plugins/built_in/WEB_SEARCH_TOOL/plugin.py b/src/plugins/built_in/WEB_SEARCH_TOOL/plugin.py index 8c635a7bc..9f6137c50 100644 --- a/src/plugins/built_in/WEB_SEARCH_TOOL/plugin.py +++ b/src/plugins/built_in/WEB_SEARCH_TOOL/plugin.py @@ -14,7 +14,8 @@ from src.plugin_system import ( ComponentInfo, ConfigField, llm_api, - ToolParamType + ToolParamType, + PythonDependency ) import httpx from bs4 import BeautifulSoup @@ -398,7 +399,30 @@ class WEBSEARCHPLUGIN(BasePlugin): plugin_name: str = "web_search_tool" # 内部标识符 enable_plugin: bool = True dependencies: List[str] = [] # 插件依赖列表 - python_dependencies: List[str] = ["asyncddgs","exa_py","httpx[socks]"] # Python包依赖列表 + # Python包依赖列表 - 支持两种格式: + # 方式1: 简单字符串列表(向后兼容) + # python_dependencies: List[str] = ["asyncddgs", "exa_py", "httpx[socks]"] + + # 方式2: 详细的PythonDependency对象(推荐) + python_dependencies: List[PythonDependency] = [ + PythonDependency( + package_name="asyncddgs", + description="异步DuckDuckGo搜索库", + optional=False + ), + PythonDependency( + package_name="exa_py", + description="Exa搜索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" # 配置文件名 # 配置节描述 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d96ca1729..634161f28 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.2.7" +version = "6.2.8" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -254,6 +254,32 @@ file_log_level = "DEBUG" # 文件日志级别,可选: DEBUG, INFO, WARNING, ER suppress_libraries = ["faiss","httpx", "urllib3", "asyncio", "websockets", "httpcore", "requests", "peewee", "openai","uvicorn","jieba","maim_message"] # 完全屏蔽的库 library_log_levels = { "aiohttp" = "WARNING"} # 设置特定库的日志级别 +[dependency_management] # 插件Python依赖管理配置 +# 是否启用自动安装Python依赖包 +auto_install = true + +# 安装超时时间(秒) +auto_install_timeout = 300 + +# 是否使用代理进行包安装 +use_proxy = false +proxy_url = "" # 代理URL,如: "http://proxy.example.com:8080" 或 "socks5://proxy.example.com:1080" + +# pip安装选项 +pip_options = [ + "--no-warn-script-location", + "--disable-pip-version-check" +] + +# 是否允许自动安装(主开关),关闭后所有插件都不会自动安装依赖 +allowed_auto_install = true + +# 安装前是否提示用户(暂未实现) +prompt_before_install = false + +# 依赖安装日志级别 +install_log_level = "INFO" + [debug] show_prompt = false # 是否显示prompt