From 2fce679aa408d6b6d872da1058a1a25ee85854d1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 15 Jun 2025 23:53:23 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=8F=92=E4=BB=B6=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E7=AE=A1=E7=90=86=EF=BC=88=E5=A6=82=E7=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MaiBot插件开发文档.md | 3 +- docs/plugins/dependency-management.md | 337 +++++++++++++ docs/plugins/examples/dependency-example.md | 477 +++++++++++++++++++ docs/plugins/quick-start.md | 59 ++- src/plugin_system/__init__.py | 4 + src/plugin_system/base/base_plugin.py | 3 + src/plugin_system/base/component_types.py | 39 ++ src/plugin_system/core/dependency_manager.py | 190 ++++++++ src/plugin_system/core/plugin_manager.py | 106 ++++- 9 files changed, 1214 insertions(+), 4 deletions(-) create mode 100644 docs/plugins/dependency-management.md create mode 100644 docs/plugins/examples/dependency-example.md create mode 100644 src/plugin_system/core/dependency_manager.py diff --git a/MaiBot插件开发文档.md b/MaiBot插件开发文档.md index df976e67f..71ed553af 100644 --- a/MaiBot插件开发文档.md +++ b/MaiBot插件开发文档.md @@ -10,7 +10,7 @@ MaiBot 是一个基于大语言模型的智能聊天机器人,采用现代化 - **统一API接口**:提供丰富的API功能,包括消息发送、数据库操作、LLM调用等 - **配置驱动**:支持TOML配置文件,实现灵活的参数配置 - **热加载机制**:支持动态加载和卸载插件 -- **依赖管理**:内置依赖检查和解析机制 +- **智能依赖管理**:自动检查和安装Python第三方包依赖 - **拦截控制**:Command组件支持消息拦截控制 - **双目录支持**:区分用户插件和系统内置插件 @@ -35,6 +35,7 @@ MaiBot支持两个插件目录: - [⚡ Action组件详解](docs/plugins/action-components.md) - 智能动作组件开发指南 - [💻 Command组件详解](docs/plugins/command-components.md) - 命令组件开发指南 - [🔧 工具系统详解](docs/plugins/tool-system.md) - 扩展麦麦信息获取能力的工具组件 +- [📦 依赖管理系统](docs/plugins/dependency-management.md) - Python包依赖管理详解 ### 🔌 API参考 - [📡 消息API](docs/plugins/api/message-api.md) - 消息发送和处理接口 diff --git a/docs/plugins/dependency-management.md b/docs/plugins/dependency-management.md new file mode 100644 index 000000000..913167a53 --- /dev/null +++ b/docs/plugins/dependency-management.md @@ -0,0 +1,337 @@ +# 📦 插件依赖管理系统 + +> 🎯 **简介**:MaiBot插件系统提供了强大的Python包依赖管理功能,让插件开发更加便捷和可靠。 + +## ✨ 功能概述 + +### 🎯 核心能力 +- **声明式依赖**:插件可以明确声明需要的Python包 +- **智能检查**:自动检查依赖包的安装状态 +- **版本控制**:精确的版本要求管理 +- **可选依赖**:区分必需依赖和可选依赖 +- **自动安装**:可选的自动安装功能 +- **批量管理**:生成统一的requirements文件 +- **安全控制**:防止意外安装和版本冲突 + +### 🔄 工作流程 +1. **声明依赖** → 在插件中声明所需的Python包 +2. **加载检查** → 插件加载时自动检查依赖状态 +3. **状态报告** → 详细报告缺失或版本不匹配的依赖 +4. **智能安装** → 可选择自动安装或手动安装 +5. **运行时处理** → 插件运行时优雅处理依赖缺失 + +## 🚀 快速开始 + +### 步骤1:声明依赖 + +在你的插件类中添加`python_dependencies`字段: + +```python +from src.plugin_system import BasePlugin, PythonDependency, register_plugin + +@register_plugin +class MyPlugin(BasePlugin): + plugin_name = "my_plugin" + plugin_description = "我的示例插件" + + # 声明Python包依赖 + python_dependencies = [ + PythonDependency( + package_name="requests", + version=">=2.25.0", + description="HTTP请求库,用于网络通信" + ), + PythonDependency( + package_name="numpy", + version=">=1.20.0", + optional=True, + description="数值计算库(可选功能)" + ), + ] + + def get_plugin_components(self): + # 返回插件组件 + return [] +``` + +### 步骤2:处理依赖 + +在组件代码中优雅处理依赖缺失: + +```python +class MyAction(BaseAction): + async def execute(self, action_input, context=None): + try: + import requests + # 使用requests进行网络请求 + response = requests.get("https://api.example.com") + return {"status": "success", "data": response.json()} + except ImportError: + return { + "status": "error", + "message": "功能不可用:缺少requests库", + "hint": "请运行: pip install requests>=2.25.0" + } +``` + +### 步骤3:检查和管理 + +使用依赖管理API: + +```python +from src.plugin_system import plugin_manager + +# 检查所有插件的依赖状态 +result = plugin_manager.check_all_dependencies() +print(f"检查了 {result['total_plugins_checked']} 个插件") +print(f"缺少必需依赖的插件: {result['plugins_with_missing_required']} 个") + +# 生成requirements文件 +plugin_manager.generate_plugin_requirements("plugin_requirements.txt") +``` + +## 📚 详细教程 + +### PythonDependency 类详解 + +`PythonDependency`是依赖声明的核心类: + +```python +PythonDependency( + package_name="requests", # 导入时的包名 + version=">=2.25.0", # 版本要求 + optional=False, # 是否为可选依赖 + description="HTTP请求库", # 依赖描述 + install_name="" # pip安装时的包名(可选) +) +``` + +#### 参数说明 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `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`相同 | + +#### 版本格式示例 + +```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") # 排除特定版本 +``` + +#### 特殊情况处理 + +**导入名与安装名不同的包:** + +```python +PythonDependency( + package_name="PIL", # import PIL + install_name="Pillow", # pip install Pillow + version=">=8.0.0" +) +``` + +**可选依赖示例:** + +```python +python_dependencies = [ + # 必需依赖 - 核心功能 + PythonDependency( + package_name="requests", + version=">=2.25.0", + description="HTTP库,插件核心功能必需" + ), + + # 可选依赖 - 增强功能 + PythonDependency( + package_name="numpy", + version=">=1.20.0", + optional=True, + description="数值计算库,用于高级数学运算" + ), + PythonDependency( + package_name="matplotlib", + version=">=3.0.0", + optional=True, + description="绘图库,用于数据可视化功能" + ), +] +``` + +### 依赖检查机制 + +系统在以下时机会自动检查依赖: + +1. **插件加载时**:检查插件声明的所有依赖 +2. **手动调用时**:通过API主动检查 +3. **运行时检查**:在组件执行时动态检查 + +#### 检查结果状态 + +| 状态 | 描述 | 处理建议 | +|------|------|----------| +| `no_dependencies` | 插件未声明任何依赖 | 无需处理 | +| `ok` | 所有依赖都已满足 | 正常使用 | +| `missing_optional` | 缺少可选依赖 | 部分功能不可用,考虑安装 | +| `missing_required` | 缺少必需依赖 | 插件功能受限,需要安装 | + +## 🎯 最佳实践 + +### 1. 依赖声明原则 + +#### ✅ 推荐做法 + +```python +python_dependencies = [ + # 明确的版本要求 + PythonDependency( + package_name="requests", + version=">=2.25.0,<3.0.0", # 主版本兼容 + description="HTTP请求库,用于API调用" + ), + + # 合理的可选依赖 + PythonDependency( + package_name="numpy", + version=">=1.20.0", + optional=True, + description="数值计算库,用于数据处理功能" + ), +] +``` + +#### ❌ 避免的做法 + +```python +python_dependencies = [ + # 过于宽泛的版本要求 + PythonDependency("requests"), # 没有版本限制 + + # 过于严格的版本要求 + PythonDependency("numpy", "==1.21.0"), # 精确版本过于严格 + + # 缺少描述 + PythonDependency("matplotlib", ">=3.0.0"), # 没有说明用途 +] +``` + +### 2. 错误处理模式 + +#### 优雅降级模式 + +```python +class SmartAction(BaseAction): + async def execute(self, action_input, context=None): + # 检查可选依赖 + try: + import numpy as np + # 使用numpy的高级功能 + return await self._advanced_processing(action_input, np) + except ImportError: + # 降级到基础功能 + return await self._basic_processing(action_input) + + async def _advanced_processing(self, input_data, np): + """使用numpy的高级处理""" + result = np.array(input_data).mean() + return {"result": result, "method": "advanced"} + + async def _basic_processing(self, input_data): + """基础处理(不依赖外部库)""" + result = sum(input_data) / len(input_data) + return {"result": result, "method": "basic"} +``` + +## 🔧 使用API + +### 检查依赖状态 + +```python +from src.plugin_system import plugin_manager + +# 检查所有插件依赖(仅检查,不安装) +result = plugin_manager.check_all_dependencies(auto_install=False) + +# 检查并自动安装缺失的必需依赖 +result = plugin_manager.check_all_dependencies(auto_install=True) +``` + +### 生成requirements文件 + +```python +# 生成包含所有插件依赖的requirements文件 +plugin_manager.generate_plugin_requirements("plugin_requirements.txt") +``` + +### 获取依赖状态报告 + +```python +# 获取详细的依赖检查报告 +result = plugin_manager.check_all_dependencies() +for plugin_name, status in result['plugin_status'].items(): + print(f"插件 {plugin_name}: {status['status']}") + if status['missing']: + print(f" 缺失必需依赖: {status['missing']}") + if status['optional_missing']: + print(f" 缺失可选依赖: {status['optional_missing']}") +``` + +## 🛡️ 安全考虑 + +### 1. 自动安装控制 +- 🛡️ **默认手动**: 自动安装默认关闭,需要明确启用 +- 🔍 **依赖审查**: 安装前会显示将要安装的包列表 +- ⏱️ **超时控制**: 安装操作有超时限制(5分钟) + +### 2. 权限管理 +- 📁 **环境隔离**: 推荐在虚拟环境中使用 +- 🔒 **版本锁定**: 支持精确的版本控制 +- 📝 **安装日志**: 记录所有安装操作 + +## 📊 故障排除 + +### 常见问题 + +1. **依赖检查失败** + ```python + # 手动检查包是否可导入 + try: + import package_name + print("包可用") + except ImportError: + print("包不可用,需要安装") + ``` + +2. **版本冲突** + ```python + # 检查已安装的包版本 + import package_name + print(f"当前版本: {package_name.__version__}") + ``` + +3. **安装失败** + ```python + # 查看安装日志 + from src.plugin_system import dependency_manager + result = dependency_manager.get_install_summary() + print("安装日志:", result['install_log']) + print("失败详情:", result['failed_installs']) + ``` + +## 🔗 相关文档 + +- [🚀 快速开始指南](quick-start.md) - 创建你的第一个插件 +- [⚡ Action组件详解](action-components.md) - Action开发指南 +- [💻 Command组件详解](command-components.md) - Command开发指南 +- [📋 开发规范](development-standards.md) - 代码规范和最佳实践 + +--- + +通过依赖管理系统,你的插件将更加健壮和易于维护。开始使用这些功能让你的插件开发更加高效吧! 🚀 \ No newline at end of file diff --git a/docs/plugins/examples/dependency-example.md b/docs/plugins/examples/dependency-example.md new file mode 100644 index 000000000..26b7fa0ff --- /dev/null +++ b/docs/plugins/examples/dependency-example.md @@ -0,0 +1,477 @@ +# 📦 依赖管理完整示例 + +> 这个示例展示了如何在插件中正确使用Python依赖管理功能。 + +## 🎯 示例插件:智能数据分析插件 + +这个插件展示了如何处理必需依赖、可选依赖,以及优雅降级处理。 + +```python +""" +智能数据分析插件 +展示依赖管理的完整用法 +""" + +from src.plugin_system import ( + BasePlugin, + BaseAction, + register_plugin, + ActionInfo, + PythonDependency, + ActionActivationType +) +from src.common.logger import get_logger + +logger = get_logger("data_analysis_plugin") + + +@register_plugin +class DataAnalysisPlugin(BasePlugin): + """智能数据分析插件""" + + plugin_name = "data_analysis_plugin" + plugin_description = "提供数据分析和可视化功能的示例插件" + plugin_version = "1.0.0" + plugin_author = "MaiBot Team" + + # 声明Python包依赖 + python_dependencies = [ + # 必需依赖 - 核心功能 + PythonDependency( + package_name="requests", + version=">=2.25.0", + description="HTTP库,用于获取外部数据" + ), + + # 可选依赖 - 数据处理 + PythonDependency( + package_name="pandas", + version=">=1.3.0", + optional=True, + description="数据处理库,提供高级数据操作功能" + ), + + # 可选依赖 - 数值计算 + PythonDependency( + package_name="numpy", + version=">=1.20.0", + optional=True, + description="数值计算库,用于数学运算" + ), + + # 可选依赖 - 数据可视化 + PythonDependency( + package_name="matplotlib", + version=">=3.3.0", + optional=True, + description="绘图库,用于生成数据图表" + ), + + # 特殊情况:导入名与安装名不同 + PythonDependency( + package_name="PIL", + install_name="Pillow", + version=">=8.0.0", + optional=True, + description="图像处理库,用于图表保存和处理" + ), + ] + + def get_plugin_components(self): + """返回插件组件""" + return [ + # 基础数据获取(只依赖requests) + (ActionInfo( + name="fetch_data_action", + description="获取外部数据", + focus_activation_type=ActionActivationType.KEYWORD, + normal_activation_type=ActionActivationType.KEYWORD, + activation_keywords=["获取数据", "下载数据"], + ), FetchDataAction), + + # 数据分析(依赖pandas和numpy) + (ActionInfo( + name="analyze_data_action", + description="数据分析和统计", + focus_activation_type=ActionActivationType.KEYWORD, + normal_activation_type=ActionActivationType.KEYWORD, + activation_keywords=["分析数据", "数据统计"], + ), AnalyzeDataAction), + + # 数据可视化(依赖matplotlib) + (ActionInfo( + name="visualize_data_action", + description="数据可视化", + focus_activation_type=ActionActivationType.KEYWORD, + normal_activation_type=ActionActivationType.KEYWORD, + activation_keywords=["数据图表", "可视化"], + ), VisualizeDataAction), + ] + + +class FetchDataAction(BaseAction): + """数据获取Action - 仅依赖必需的requests库""" + + async def execute(self, action_input, context=None): + """获取外部数据""" + try: + import requests + + # 模拟数据获取 + url = action_input.get("url", "https://api.github.com/users/octocat") + + response = requests.get(url, timeout=10) + response.raise_for_status() + + data = response.json() + + return { + "status": "success", + "message": f"成功获取数据,响应大小: {len(str(data))} 字符", + "data": data, + "capabilities": ["basic_fetch"] + } + + except ImportError: + return { + "status": "error", + "message": "缺少必需依赖:requests库", + "hint": "请运行: pip install requests>=2.25.0", + "error_code": "MISSING_DEPENDENCY" + } + except Exception as e: + return { + "status": "error", + "message": f"数据获取失败: {str(e)}", + "error_code": "FETCH_ERROR" + } + + +class AnalyzeDataAction(BaseAction): + """数据分析Action - 支持多级功能降级""" + + async def execute(self, action_input, context=None): + """分析数据,支持功能降级""" + + # 检查可用的依赖 + has_pandas = self._check_dependency("pandas") + has_numpy = self._check_dependency("numpy") + + # 获取输入数据 + data = action_input.get("data", [1, 2, 3, 4, 5]) + + if has_pandas and has_numpy: + return await self._advanced_analysis(data) + elif has_numpy: + return await self._numpy_analysis(data) + else: + return await self._basic_analysis(data) + + def _check_dependency(self, package_name): + """检查依赖是否可用""" + try: + __import__(package_name) + return True + except ImportError: + return False + + async def _advanced_analysis(self, data): + """高级分析(使用pandas + numpy)""" + import pandas as pd + import numpy as np + + # 转换为DataFrame + df = pd.DataFrame({"values": data}) + + # 高级统计分析 + stats = { + "count": len(df), + "mean": df["values"].mean(), + "median": df["values"].median(), + "std": df["values"].std(), + "min": df["values"].min(), + "max": df["values"].max(), + "quartiles": df["values"].quantile([0.25, 0.5, 0.75]).to_dict(), + "skewness": df["values"].skew(), + "kurtosis": df["values"].kurtosis() + } + + return { + "status": "success", + "message": "高级数据分析完成", + "data": stats, + "method": "advanced", + "capabilities": ["pandas", "numpy", "advanced_stats"] + } + + async def _numpy_analysis(self, data): + """中级分析(仅使用numpy)""" + import numpy as np + + arr = np.array(data) + + stats = { + "count": len(arr), + "mean": np.mean(arr), + "median": np.median(arr), + "std": np.std(arr), + "min": np.min(arr), + "max": np.max(arr), + "sum": np.sum(arr) + } + + return { + "status": "success", + "message": "数值计算分析完成", + "data": stats, + "method": "numpy", + "capabilities": ["numpy", "basic_stats"] + } + + async def _basic_analysis(self, data): + """基础分析(纯Python)""" + + stats = { + "count": len(data), + "mean": sum(data) / len(data) if data else 0, + "min": min(data) if data else None, + "max": max(data) if data else None, + "sum": sum(data) + } + + return { + "status": "success", + "message": "基础数据分析完成", + "data": stats, + "method": "basic", + "capabilities": ["pure_python"], + "note": "安装numpy和pandas可获得更多分析功能" + } + + +class VisualizeDataAction(BaseAction): + """数据可视化Action - 展示条件功能启用""" + + async def execute(self, action_input, context=None): + """数据可视化""" + + # 检查可视化依赖 + visualization_available = self._check_visualization_deps() + + if not visualization_available: + return { + "status": "unavailable", + "message": "数据可视化功能不可用", + "reason": "缺少matplotlib和PIL依赖", + "install_hint": "pip install matplotlib>=3.3.0 Pillow>=8.0.0", + "alternative": "可以使用基础数据分析功能" + } + + return await self._create_visualization(action_input) + + def _check_visualization_deps(self): + """检查可视化所需的依赖""" + try: + import matplotlib + import PIL + return True + except ImportError: + return False + + async def _create_visualization(self, action_input): + """创建数据可视化""" + import matplotlib.pyplot as plt + import io + import base64 + from PIL import Image + + # 获取数据 + data = action_input.get("data", [1, 2, 3, 4, 5]) + chart_type = action_input.get("type", "line") + + # 创建图表 + plt.figure(figsize=(10, 6)) + + if chart_type == "line": + plt.plot(data) + plt.title("线性图") + elif chart_type == "bar": + plt.bar(range(len(data)), data) + plt.title("柱状图") + elif chart_type == "hist": + plt.hist(data, bins=10) + plt.title("直方图") + else: + plt.plot(data) + plt.title("默认线性图") + + plt.xlabel("索引") + plt.ylabel("数值") + plt.grid(True) + + # 保存为字节流 + buffer = io.BytesIO() + plt.savefig(buffer, format='png', dpi=150, bbox_inches='tight') + buffer.seek(0) + + # 转换为base64 + image_base64 = base64.b64encode(buffer.getvalue()).decode() + + plt.close() # 释放内存 + + return { + "status": "success", + "message": f"生成{chart_type}图表成功", + "data": { + "chart_type": chart_type, + "data_points": len(data), + "image_base64": image_base64 + }, + "capabilities": ["matplotlib", "pillow", "visualization"] + } + + +# 测试和演示代码 +async def demo_dependency_management(): + """演示依赖管理功能""" + + print("🔍 插件依赖管理演示") + print("=" * 50) + + # 创建插件实例 + plugin = DataAnalysisPlugin() + + print("\n📦 插件依赖信息:") + for dep in plugin.python_dependencies: + status = "✅" if plugin._check_dependency_available(dep.package_name) else "❌" + optional_str = " (可选)" if dep.optional else " (必需)" + print(f" {status} {dep.package_name} {dep.version}{optional_str}") + print(f" {dep.description}") + + print("\n🧪 功能测试:") + + # 测试数据获取 + fetch_action = FetchDataAction() + result = await fetch_action.execute({"url": "https://httpbin.org/json"}) + print(f" 数据获取: {result['status']}") + + # 测试数据分析 + analyze_action = AnalyzeDataAction() + test_data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + result = await analyze_action.execute({"data": test_data}) + print(f" 数据分析: {result['status']} (方法: {result.get('method', 'unknown')})") + print(f" 可用功能: {result.get('capabilities', [])}") + + # 测试数据可视化 + viz_action = VisualizeDataAction() + result = await viz_action.execute({"data": test_data, "type": "line"}) + print(f" 数据可视化: {result['status']}") + + print("\n💡 依赖管理建议:") + missing_deps = plugin.plugin_info.get_missing_packages() + if missing_deps: + print(" 缺失的必需依赖:") + for dep in missing_deps: + print(f" - {dep.get_pip_requirement()}") + print(f"\n 安装命令:") + print(f" pip install {' '.join([dep.get_pip_requirement() for dep in missing_deps])}") + else: + print(" ✅ 所有必需依赖都已安装") + + +if __name__ == "__main__": + import asyncio + + # 为演示添加依赖检查方法 + def _check_dependency_available(package_name): + try: + __import__(package_name) + return True + except ImportError: + return False + + DataAnalysisPlugin._check_dependency_available = _check_dependency_available + + # 运行演示 + asyncio.run(demo_dependency_management()) +``` + +## 🎯 示例说明 + +### 1. 依赖分层设计 + +这个示例展示了三层依赖设计: + +- **必需依赖**: `requests` - 核心功能必需 +- **增强依赖**: `pandas`, `numpy` - 提供更强大的分析能力 +- **可选依赖**: `matplotlib`, `PIL` - 提供可视化功能 + +### 2. 优雅降级策略 + +```python +# 三级功能降级 +if has_pandas and has_numpy: + return await self._advanced_analysis(data) # 最佳体验 +elif has_numpy: + return await self._numpy_analysis(data) # 中等体验 +else: + return await self._basic_analysis(data) # 基础体验 +``` + +### 3. 条件功能启用 + +```python +# 只有依赖可用时才提供功能 +visualization_available = self._check_visualization_deps() +if not visualization_available: + return {"status": "unavailable", "install_hint": "..."} +``` + +## 🚀 使用这个示例 + +### 1. 复制代码 + +将示例代码保存为 `plugins/data_analysis_plugin/plugin.py` + +### 2. 测试依赖检查 + +```python +from src.plugin_system import plugin_manager + +# 检查这个插件的依赖 +result = plugin_manager.check_all_dependencies() +print(result['plugin_status']['data_analysis_plugin']) +``` + +### 3. 安装缺失依赖 + +```python +# 生成requirements文件 +plugin_manager.generate_plugin_requirements("data_plugin_deps.txt") + +# 手动安装 +# pip install -r data_plugin_deps.txt +``` + +### 4. 测试功能降级 + +```bash +# 测试基础功能(只安装requests) +pip install requests>=2.25.0 + +# 测试增强功能(添加数据处理) +pip install numpy>=1.20.0 pandas>=1.3.0 + +# 测试完整功能(添加可视化) +pip install matplotlib>=3.3.0 Pillow>=8.0.0 +``` + +## 💡 最佳实践总结 + +1. **分层依赖设计**: 区分核心、增强、可选依赖 +2. **优雅降级处理**: 提供多级功能体验 +3. **明确错误信息**: 告诉用户如何解决依赖问题 +4. **条件功能启用**: 根据依赖可用性动态调整功能 +5. **详细依赖描述**: 说明每个依赖的用途 + +这个示例展示了如何构建一个既强大又灵活的插件,即使在依赖不完整的情况下也能提供有用的功能。 \ No newline at end of file diff --git a/docs/plugins/quick-start.md b/docs/plugins/quick-start.md index 5b34bfaa2..a0bb5f530 100644 --- a/docs/plugins/quick-start.md +++ b/docs/plugins/quick-start.md @@ -124,6 +124,16 @@ class HelloWorldPlugin(BasePlugin): plugin_author = "你的名字" enable_plugin = True config_file_name = "config.toml" + + # Python依赖声明(可选) + python_dependencies = [ + # 如果你的插件需要额外的Python包,在这里声明 + # PythonDependency( + # package_name="requests", + # version=">=2.25.0", + # description="HTTP请求库" + # ), + ] def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: """返回插件包含的组件列表""" @@ -245,14 +255,59 @@ python main.py 2. **组件列表**: `get_plugin_components()` 返回所有组件 3. **配置加载**: 自动加载 `config.toml` 文件 +## 📦 添加依赖包(可选) + +如果你的插件需要额外的Python包,可以声明依赖: + +```python +from src.plugin_system import PythonDependency + +@register_plugin +class HelloWorldPlugin(BasePlugin): + # ... 其他配置 ... + + # 声明Python依赖 + python_dependencies = [ + PythonDependency( + package_name="requests", + version=">=2.25.0", + description="HTTP请求库,用于网络功能" + ), + PythonDependency( + package_name="numpy", + version=">=1.20.0", + optional=True, + description="数值计算库(可选功能)" + ), + ] +``` + +### 依赖检查 + +系统会自动检查依赖,你也可以手动检查: + +```python +from src.plugin_system import plugin_manager + +# 检查所有插件依赖 +result = plugin_manager.check_all_dependencies() +print(f"缺少依赖的插件: {result['plugins_with_missing_required']}个") + +# 生成requirements文件 +plugin_manager.generate_plugin_requirements("plugin_deps.txt") +``` + +📚 **详细了解**: [依赖管理系统](dependency-management.md) + ## 🎯 下一步 恭喜!你已经创建了第一个MaiBot插件。接下来可以: 1. 学习 [Action组件详解](action-components.md) 掌握更复杂的Action开发 2. 学习 [Command组件详解](command-components.md) 创建更强大的命令 -3. 查看 [API参考](api/) 了解所有可用的接口 -4. 参考 [完整示例](examples/complete-examples.md) 学习最佳实践 +3. 了解 [依赖管理系统](dependency-management.md) 管理Python包依赖 +4. 查看 [API参考](api/) 了解所有可用的接口 +5. 参考 [完整示例](examples/complete-examples.md) 学习最佳实践 ## 🐛 常见问题 diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 309e06964..7067b399b 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -16,10 +16,12 @@ from src.plugin_system.base.component_types import ( ActionInfo, CommandInfo, PluginInfo, + PythonDependency, ) from src.plugin_system.apis.plugin_api import PluginAPI, create_plugin_api, create_command_api 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 __version__ = "1.0.0" @@ -36,6 +38,7 @@ __all__ = [ "ActionInfo", "CommandInfo", "PluginInfo", + "PythonDependency", # API接口 "PluginAPI", "create_plugin_api", @@ -43,6 +46,7 @@ __all__ = [ # 管理器 "plugin_manager", "component_registry", + "dependency_manager", # 装饰器 "register_plugin", ] diff --git a/src/plugin_system/base/base_plugin.py b/src/plugin_system/base/base_plugin.py index ba202b78d..8c0f79f53 100644 --- a/src/plugin_system/base/base_plugin.py +++ b/src/plugin_system/base/base_plugin.py @@ -7,6 +7,7 @@ from src.common.logger import get_logger from src.plugin_system.base.component_types import ( PluginInfo, ComponentInfo, + PythonDependency, ) from src.plugin_system.core.component_registry import component_registry @@ -32,6 +33,7 @@ class BasePlugin(ABC): plugin_author: str = "" # 插件作者 enable_plugin: bool = True # 是否启用插件 dependencies: List[str] = [] # 依赖的其他插件 + python_dependencies: List[PythonDependency] = [] # Python包依赖 config_file_name: Optional[str] = None # 配置文件名 def __init__(self, plugin_dir: str = None): @@ -60,6 +62,7 @@ class BasePlugin(ABC): is_built_in=False, config_file=self.config_file_name or "", dependencies=self.dependencies.copy(), + python_dependencies=self.python_dependencies.copy(), ) logger.debug(f"{self.log_prefix} 插件基类初始化完成") diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index 7ee48c7f6..9ceba18ac 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -33,6 +33,27 @@ class ChatMode(Enum): ALL = "all" # 所有聊天模式 +@dataclass +class PythonDependency: + """Python包依赖信息""" + + package_name: str # 包名称 + version: str = "" # 版本要求,例如: ">=1.0.0", "==2.1.3", ""表示任意版本 + optional: bool = False # 是否为可选依赖 + description: str = "" # 依赖描述 + install_name: str = "" # 安装时的包名(如果与import名不同) + + def __post_init__(self): + if not self.install_name: + self.install_name = self.package_name + + def get_pip_requirement(self) -> str: + """获取pip安装格式的依赖字符串""" + if self.version: + return f"{self.install_name}{self.version}" + return self.install_name + + @dataclass class ComponentInfo: """组件信息""" @@ -107,6 +128,7 @@ class PluginInfo: is_built_in: bool = False # 是否为内置插件 components: List[ComponentInfo] = None # 包含的组件列表 dependencies: List[str] = None # 依赖的其他插件 + python_dependencies: List[PythonDependency] = None # Python包依赖 config_file: str = "" # 配置文件路径 metadata: Dict[str, Any] = None # 额外元数据 @@ -115,5 +137,22 @@ class PluginInfo: self.components = [] if self.dependencies is None: self.dependencies = [] + if self.python_dependencies is None: + self.python_dependencies = [] if self.metadata is None: self.metadata = {} + + def get_missing_packages(self) -> List[PythonDependency]: + """检查缺失的Python包""" + missing = [] + for dep in self.python_dependencies: + try: + __import__(dep.package_name) + except ImportError: + if not dep.optional: + missing.append(dep) + return missing + + def get_pip_requirements(self) -> List[str]: + """获取所有pip安装格式的依赖""" + return [dep.get_pip_requirement() for dep in self.python_dependencies] diff --git a/src/plugin_system/core/dependency_manager.py b/src/plugin_system/core/dependency_manager.py new file mode 100644 index 000000000..bf67dba74 --- /dev/null +++ b/src/plugin_system/core/dependency_manager.py @@ -0,0 +1,190 @@ +""" +插件依赖管理器 + +负责检查和安装插件的Python包依赖 +""" + +import subprocess +import sys +import importlib +from typing import List, Dict, Tuple, Optional +from pathlib import Path + +from src.common.logger import get_logger +from src.plugin_system.base.component_types import PythonDependency + +logger = get_logger("dependency_manager") + + +class DependencyManager: + """依赖管理器""" + + def __init__(self): + self.install_log: List[str] = [] + self.failed_installs: Dict[str, str] = {} + + def check_dependencies(self, dependencies: List[PythonDependency]) -> Tuple[List[PythonDependency], List[PythonDependency]]: + """检查依赖包状态 + + Args: + dependencies: 依赖包列表 + + Returns: + Tuple[List[PythonDependency], List[PythonDependency]]: (缺失的依赖, 可选缺失的依赖) + """ + missing_required = [] + missing_optional = [] + + for dep in dependencies: + if not self._is_package_available(dep.package_name): + if dep.optional: + missing_optional.append(dep) + logger.warning(f"可选依赖包缺失: {dep.package_name} - {dep.description}") + else: + missing_required.append(dep) + logger.error(f"必需依赖包缺失: {dep.package_name} - {dep.description}") + else: + logger.debug(f"依赖包已存在: {dep.package_name}") + + return missing_required, missing_optional + + def _is_package_available(self, package_name: str) -> bool: + """检查包是否可用""" + try: + importlib.import_module(package_name) + return True + except ImportError: + return False + + def install_dependencies(self, dependencies: List[PythonDependency], auto_install: bool = False) -> bool: + """安装依赖包 + + Args: + dependencies: 需要安装的依赖包列表 + auto_install: 是否自动安装(True时不询问用户) + + Returns: + bool: 安装是否成功 + """ + if not dependencies: + return True + + logger.info(f"需要安装 {len(dependencies)} 个依赖包") + + # 显示将要安装的包 + for dep in dependencies: + install_cmd = dep.get_pip_requirement() + logger.info(f" - {install_cmd} {'(可选)' if dep.optional else '(必需)'}") + if dep.description: + logger.info(f" 说明: {dep.description}") + + if not auto_install: + # 这里可以添加用户确认逻辑 + logger.warning("手动安装模式:请手动运行 pip install 命令安装依赖包") + return False + + # 执行安装 + success_count = 0 + for dep in dependencies: + if self._install_single_package(dep): + success_count += 1 + else: + self.failed_installs[dep.package_name] = f"安装失败: {dep.get_pip_requirement()}" + + logger.info(f"依赖安装完成: {success_count}/{len(dependencies)} 个成功") + return success_count == len(dependencies) + + def _install_single_package(self, dependency: PythonDependency) -> bool: + """安装单个包""" + pip_requirement = dependency.get_pip_requirement() + + try: + logger.info(f"正在安装: {pip_requirement}") + + # 使用subprocess安装包 + cmd = [sys.executable, "-m", "pip", "install", pip_requirement] + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300 # 5分钟超时 + ) + + if result.returncode == 0: + logger.info(f"✅ 成功安装: {pip_requirement}") + self.install_log.append(f"成功安装: {pip_requirement}") + return True + else: + logger.error(f"❌ 安装失败: {pip_requirement}") + logger.error(f"错误输出: {result.stderr}") + self.install_log.append(f"安装失败: {pip_requirement} - {result.stderr}") + return False + + except subprocess.TimeoutExpired: + logger.error(f"❌ 安装超时: {pip_requirement}") + return False + except Exception as e: + logger.error(f"❌ 安装异常: {pip_requirement} - {str(e)}") + return False + + def generate_requirements_file(self, plugins_dependencies: List[List[PythonDependency]], + output_path: str = "plugin_requirements.txt") -> bool: + """生成插件依赖的requirements文件 + + Args: + plugins_dependencies: 所有插件的依赖列表 + output_path: 输出文件路径 + + Returns: + bool: 生成是否成功 + """ + try: + all_deps = {} + + # 合并所有插件的依赖 + for plugin_deps in plugins_dependencies: + for dep in plugin_deps: + key = dep.install_name + if key in all_deps: + # 如果已存在,可以添加版本兼容性检查逻辑 + existing = all_deps[key] + if dep.version and existing.version != dep.version: + logger.warning(f"依赖版本冲突: {key} ({existing.version} vs {dep.version})") + else: + all_deps[key] = dep + + # 写入requirements文件 + with open(output_path, "w", encoding="utf-8") as f: + f.write("# 插件依赖包自动生成\n") + f.write("# Auto-generated plugin dependencies\n\n") + + # 按包名排序 + sorted_deps = sorted(all_deps.values(), key=lambda x: x.install_name) + + for dep in sorted_deps: + requirement = dep.get_pip_requirement() + if dep.description: + f.write(f"# {dep.description}\n") + if dep.optional: + f.write(f"# Optional dependency\n") + f.write(f"{requirement}\n\n") + + logger.info(f"已生成插件依赖文件: {output_path} ({len(all_deps)} 个包)") + return True + + except Exception as e: + logger.error(f"生成requirements文件失败: {str(e)}") + return False + + def get_install_summary(self) -> Dict[str, any]: + """获取安装摘要""" + return { + "install_log": self.install_log.copy(), + "failed_installs": self.failed_installs.copy(), + "total_attempts": len(self.install_log), + "failed_count": len(self.failed_installs) + } + + +# 全局依赖管理器实例 +dependency_manager = DependencyManager() \ No newline at end of file diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 17f532b30..504209cb1 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -9,7 +9,8 @@ if TYPE_CHECKING: from src.common.logger import get_logger from src.plugin_system.core.component_registry import component_registry -from src.plugin_system.base.component_types import ComponentType, PluginInfo +from src.plugin_system.core.dependency_manager import dependency_manager +from src.plugin_system.base.component_types import ComponentType, PluginInfo, PythonDependency logger = get_logger("plugin_manager") @@ -344,6 +345,109 @@ class PluginManager: logger.warning("插件热重载功能尚未实现") return False + def check_all_dependencies(self, auto_install: bool = False) -> Dict[str, any]: + """检查所有插件的Python依赖包 + + Args: + auto_install: 是否自动安装缺失的依赖包 + + Returns: + Dict[str, any]: 检查结果摘要 + """ + logger.info("开始检查所有插件的Python依赖包...") + + all_required_missing = [] + all_optional_missing = [] + plugin_status = {} + + for plugin_name, plugin_instance in self.loaded_plugins.items(): + plugin_info = component_registry.get_plugin_info(plugin_name) + if not plugin_info or not plugin_info.python_dependencies: + plugin_status[plugin_name] = {"status": "no_dependencies", "missing": []} + continue + + logger.info(f"检查插件 {plugin_name} 的依赖...") + + missing_required, missing_optional = dependency_manager.check_dependencies( + plugin_info.python_dependencies + ) + + if missing_required: + all_required_missing.extend(missing_required) + plugin_status[plugin_name] = { + "status": "missing_required", + "missing": [dep.package_name for dep in missing_required], + "optional_missing": [dep.package_name for dep in missing_optional] + } + logger.error(f"插件 {plugin_name} 缺少必需依赖: {[dep.package_name for dep in missing_required]}") + elif missing_optional: + all_optional_missing.extend(missing_optional) + plugin_status[plugin_name] = { + "status": "missing_optional", + "missing": [], + "optional_missing": [dep.package_name for dep in missing_optional] + } + logger.warning(f"插件 {plugin_name} 缺少可选依赖: {[dep.package_name for dep in missing_optional]}") + else: + plugin_status[plugin_name] = {"status": "ok", "missing": []} + logger.info(f"插件 {plugin_name} 依赖检查通过") + + # 汇总结果 + total_missing = len(set(dep.package_name for dep in all_required_missing)) + total_optional_missing = len(set(dep.package_name for dep in all_optional_missing)) + + logger.info(f"依赖检查完成 - 缺少必需包: {total_missing}个, 缺少可选包: {total_optional_missing}个") + + # 如果需要自动安装 + install_success = True + if auto_install and all_required_missing: + # 去重 + unique_required = {} + for dep in all_required_missing: + unique_required[dep.package_name] = dep + + logger.info(f"开始自动安装 {len(unique_required)} 个必需依赖包...") + install_success = dependency_manager.install_dependencies( + list(unique_required.values()), + auto_install=True + ) + + return { + "total_plugins_checked": len(plugin_status), + "plugins_with_missing_required": len([p for p in plugin_status.values() if p["status"] == "missing_required"]), + "plugins_with_missing_optional": len([p for p in plugin_status.values() if p["status"] == "missing_optional"]), + "total_missing_required": total_missing, + "total_missing_optional": total_optional_missing, + "plugin_status": plugin_status, + "auto_install_attempted": auto_install and bool(all_required_missing), + "auto_install_success": install_success, + "install_summary": dependency_manager.get_install_summary() + } + + def generate_plugin_requirements(self, output_path: str = "plugin_requirements.txt") -> bool: + """生成所有插件依赖的requirements文件 + + Args: + output_path: 输出文件路径 + + Returns: + bool: 生成是否成功 + """ + logger.info("开始生成插件依赖requirements文件...") + + all_dependencies = [] + + for plugin_name, plugin_instance in self.loaded_plugins.items(): + plugin_info = component_registry.get_plugin_info(plugin_name) + if plugin_info and plugin_info.python_dependencies: + all_dependencies.append(plugin_info.python_dependencies) + + if not all_dependencies: + logger.info("没有找到任何插件依赖") + return False + + return dependency_manager.generate_requirements_file(all_dependencies, output_path) + # 全局插件管理器实例 plugin_manager = PluginManager()