新增插件Python依赖管理系统,支持自动检查和安装依赖,优化依赖配置和错误处理,更新相关文档和示例代码。
This commit is contained in:
@@ -1,40 +1,192 @@
|
|||||||
# 📦 插件依赖管理系统
|
# 插件Python依赖管理系统
|
||||||
|
|
||||||
现在的Python依赖包管理依然存在问题,请保留你的`python_dependencies`属性,等待后续重构。
|
## 概述
|
||||||
|
|
||||||
## 📚 详细教程
|
插件系统现在支持自动检查和安装Python包依赖。当插件初始化时,系统会:
|
||||||
|
|
||||||
### PythonDependency 类详解
|
1. 检查插件所需的Python包是否已安装
|
||||||
|
2. 验证包版本是否满足要求
|
||||||
|
3. 自动安装缺失的依赖包(可配置)
|
||||||
|
4. 提供详细的错误信息和日志
|
||||||
|
|
||||||
`PythonDependency`是依赖声明的核心类:
|
## 配置依赖
|
||||||
|
|
||||||
|
### 方式1: 简单字符串列表(向后兼容)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
PythonDependency(
|
from src.plugin_system import BasePlugin
|
||||||
package_name="PIL", # 导入时的包名
|
|
||||||
version=">=11.2.0", # 版本要求
|
@register_plugin
|
||||||
optional=False, # 是否为可选依赖
|
class MyPlugin(BasePlugin):
|
||||||
description="图像处理库", # 依赖描述
|
# 简单的字符串列表格式
|
||||||
install_name="pillow" # pip安装时的包名(可选)
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 参数说明
|
## 工作流程
|
||||||
|
|
||||||
| 参数 | 类型 | 必需 | 说明 |
|
1. **插件初始化**: 当插件类被实例化时,系统自动检查依赖
|
||||||
|------|------|------|------|
|
2. **依赖标准化**: 将字符串格式的依赖转换为PythonDependency对象
|
||||||
| `package_name` | str | ✅ | Python导入时使用的包名(如`requests`) |
|
3. **检查已安装**: 尝试导入每个依赖包并检查版本
|
||||||
| `version` | str | ❌ | 版本要求,使用pip格式(如`>=1.0.0`, `==2.1.3`) |
|
4. **自动安装**: 如果启用,自动安装缺失的依赖
|
||||||
| `optional` | bool | ❌ | 是否为可选依赖,默认`False` |
|
5. **错误处理**: 记录详细的错误信息和安装日志
|
||||||
| `description` | str | ❌ | 依赖的用途描述 |
|
|
||||||
| `install_name` | str | ❌ | pip安装时的包名,默认与`package_name`相同,用于处理安装名称和导入名称不一致的情况 |
|
|
||||||
|
|
||||||
#### 版本格式示例
|
## 日志输出示例
|
||||||
|
|
||||||
```python
|
```
|
||||||
# 常用版本格式
|
[Plugin:web_search_tool] 开始自动安装Python依赖: ['asyncddgs', 'httpx[socks]']
|
||||||
PythonDependency("requests", ">=2.25.0") # 最小版本
|
[Plugin:web_search_tool] ✅ 成功安装: asyncddgs
|
||||||
PythonDependency("numpy", ">=1.20.0,<2.0.0") # 版本范围
|
[Plugin:web_search_tool] ✅ 成功安装: httpx[socks]
|
||||||
PythonDependency("pillow", "==8.3.2") # 精确版本
|
[Plugin:web_search_tool] 🎉 所有依赖安装完成
|
||||||
PythonDependency("scipy", ">=1.7.0,!=1.8.0") # 排除特定版本
|
[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. 验证安装是否成功
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from src.config.official_configs import (
|
|||||||
CustomPromptConfig,
|
CustomPromptConfig,
|
||||||
ScheduleConfig,
|
ScheduleConfig,
|
||||||
VideoAnalysisConfig,
|
VideoAnalysisConfig,
|
||||||
|
DependencyManagementConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api_ada_configs import (
|
from .api_ada_configs import (
|
||||||
@@ -354,6 +355,7 @@ class Config(ConfigBase):
|
|||||||
voice: VoiceConfig
|
voice: VoiceConfig
|
||||||
schedule: ScheduleConfig
|
schedule: ScheduleConfig
|
||||||
utils_video: VideoAnalysisConfig = field(default_factory=lambda: VideoAnalysisConfig())
|
utils_video: VideoAnalysisConfig = field(default_factory=lambda: VideoAnalysisConfig())
|
||||||
|
dependency_management: DependencyManagementConfig = field(default_factory=lambda: DependencyManagementConfig())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ class ExpressionConfig(ConfigBase):
|
|||||||
# 如果都没有匹配,返回默认值
|
# 如果都没有匹配,返回默认值
|
||||||
return True, True, 300
|
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
|
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
|
enable_frame_timing: bool = True
|
||||||
"""是否在分析中包含帧的时间信息"""
|
"""是否在分析中包含帧的时间信息"""
|
||||||
|
|
||||||
|
|
||||||
|
@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"
|
||||||
|
"""依赖安装日志级别"""
|
||||||
@@ -35,6 +35,10 @@ from .utils import (
|
|||||||
# generate_plugin_manifest,
|
# 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 (
|
from .apis import (
|
||||||
chat_api,
|
chat_api,
|
||||||
tool_api,
|
tool_api,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import toml
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.plugin_system.base.component_types import (
|
from src.plugin_system.base.component_types import (
|
||||||
@@ -42,8 +43,8 @@ class PluginBase(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def python_dependencies(self) -> List[PythonDependency]:
|
def python_dependencies(self) -> List[Union[str, PythonDependency]]:
|
||||||
return [] # Python包依赖
|
return [] # Python包依赖,支持字符串列表或PythonDependency对象列表
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -87,6 +88,12 @@ class PluginBase(ABC):
|
|||||||
self.plugin_description = self.get_manifest_info("description", "")
|
self.plugin_description = self.get_manifest_info("description", "")
|
||||||
self.plugin_author = self._get_author_name()
|
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(
|
self.plugin_info = PluginInfo(
|
||||||
name=self.plugin_name,
|
name=self.plugin_name,
|
||||||
@@ -98,7 +105,7 @@ class PluginBase(ABC):
|
|||||||
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.dependencies.copy(),
|
||||||
python_dependencies=self.python_dependencies.copy(),
|
python_dependencies=normalized_python_deps,
|
||||||
# manifest相关信息
|
# manifest相关信息
|
||||||
manifest_data=self.manifest_data.copy(),
|
manifest_data=self.manifest_data.copy(),
|
||||||
license=self.get_manifest_info("license", ""),
|
license=self.get_manifest_info("license", ""),
|
||||||
@@ -564,6 +571,62 @@ 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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
102
src/plugin_system/utils/dependency_config.py
Normal file
102
src/plugin_system/utils/dependency_config.py
Normal file
@@ -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 函数仅用于兼容性,配置更改不会持久化")
|
||||||
289
src/plugin_system/utils/dependency_manager.py
Normal file
289
src/plugin_system/utils/dependency_manager.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
@@ -694,7 +694,7 @@ class MaiZonePlugin(BasePlugin):
|
|||||||
plugin_name: str = "MaiZonePlugin"
|
plugin_name: str = "MaiZonePlugin"
|
||||||
enable_plugin: bool = True
|
enable_plugin: bool = True
|
||||||
dependencies: List[str] = []
|
dependencies: List[str] = []
|
||||||
python_dependencies: List[str] = []
|
python_dependencies: List[str] = ["pytz"]
|
||||||
config_file_name: str = "config.toml"
|
config_file_name: str = "config.toml"
|
||||||
|
|
||||||
# 配置节描述
|
# 配置节描述
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from src.plugin_system import (
|
|||||||
ComponentInfo,
|
ComponentInfo,
|
||||||
ConfigField,
|
ConfigField,
|
||||||
llm_api,
|
llm_api,
|
||||||
ToolParamType
|
ToolParamType,
|
||||||
|
PythonDependency
|
||||||
)
|
)
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@@ -398,7 +399,30 @@ class WEBSEARCHPLUGIN(BasePlugin):
|
|||||||
plugin_name: str = "web_search_tool" # 内部标识符
|
plugin_name: str = "web_search_tool" # 内部标识符
|
||||||
enable_plugin: bool = True
|
enable_plugin: bool = True
|
||||||
dependencies: List[str] = [] # 插件依赖列表
|
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" # 配置文件名
|
config_file_name: str = "config.toml" # 配置文件名
|
||||||
|
|
||||||
# 配置节描述
|
# 配置节描述
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[inner]
|
[inner]
|
||||||
version = "6.2.7"
|
version = "6.2.8"
|
||||||
|
|
||||||
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
||||||
#如果你想要修改配置文件,请递增version的值
|
#如果你想要修改配置文件,请递增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"] # 完全屏蔽的库
|
suppress_libraries = ["faiss","httpx", "urllib3", "asyncio", "websockets", "httpcore", "requests", "peewee", "openai","uvicorn","jieba","maim_message"] # 完全屏蔽的库
|
||||||
library_log_levels = { "aiohttp" = "WARNING"} # 设置特定库的日志级别
|
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]
|
[debug]
|
||||||
show_prompt = false # 是否显示prompt
|
show_prompt = false # 是否显示prompt
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user