From 22f6cd2d947a8ad327cc70844056f7297d5c2527 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Mon, 18 Aug 2025 13:48:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(deps):=20=E5=AE=9E=E7=8E=B0=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=8C=85=E6=99=BA=E8=83=BD=E5=88=AB=E5=90=8D=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了依赖包智能别名解析机制,以解决 Python 生态中常见的安装名与导入名不一致的问题(如 `beautifulsoup4` -> `bs4`)。 当通过包名直接导入失败时,依赖管理器会自动查询一个内置的别名映射表,并尝试使用别名再次导入。这大大提升了开发者在定义简单字符串格式依赖时的体验,减少了因名称不一致导致的依赖检查失败。 同时,更新了相关文档,详细说明了该功能的工作原理、解决了什么问题,并更新了最佳实践。 --- docs/plugins/dependency-management.md | 54 +++++-- src/plugin_system/utils/dependency_alias.py | 134 ++++++++++++++++++ src/plugin_system/utils/dependency_manager.py | 82 ++++++----- 3 files changed, 226 insertions(+), 44 deletions(-) create mode 100644 src/plugin_system/utils/dependency_alias.py diff --git a/docs/plugins/dependency-management.md b/docs/plugins/dependency-management.md index ada951db7..e5eed554a 100644 --- a/docs/plugins/dependency-management.md +++ b/docs/plugins/dependency-management.md @@ -165,10 +165,38 @@ configure_dependency_settings(auto_install_timeout=600) ## 工作流程 1. **插件初始化**: 当插件类被实例化时,系统自动检查依赖 -2. **依赖标准化**: 将字符串格式的依赖转换为PythonDependency对象 +2. **依赖标准化**: 将字符串格式的依赖转换为`PythonDependency`对象 3. **检查已安装**: 尝试导入每个依赖包并检查版本 -4. **自动安装**: 如果启用,自动安装缺失的依赖 -5. **错误处理**: 记录详细的错误信息和安装日志 +4. **智能别名解析 (新增)**: 如果直接导入失败 (例如 `import beautifulsoup4` 失败),系统会查询内置的别名映射表 (例如 `beautifulsoup4` -> `bs4`),并尝试使用别名再次导入。 +5. **自动安装**: 如果启用,自动安装缺失的依赖 +6. **错误处理**: 记录详细的错误信息和安装日志 + +## 智能别名解析 (Smart Alias Resolution) + +为了提升开发体验,依赖管理系统内置了一套智能别名解析机制。 + +### 解决的问题 + +Python生态中存在一些特殊的包,它们的**安装名** (在 `pip install` 中使用) 与**导入名** (在 `import` 语句中使用) 不一致。最典型的例子就是: +- 安装名: `beautifulsoup4`, 导入名: `bs4` +- 安装名: `Pillow`, 导入名: `PIL` +- 安装名: `scikit-learn`, 导入名: `sklearn` + +如果开发者在 `python_dependencies` 列表中使用简单的字符串格式 `"beautifulsoup4"`,标准的依赖检查会因为无法 `import beautifulsoup4` 而失败。 + +### 工作原理 + +当依赖管理器通过包名直接导入失败时,它会: +1. 查询一个内置的、包含上百个常见包的别名映射表。 +2. 如果在表中找到对应的导入名,则使用该别名再次尝试导入。 +3. 如果使用别名导入成功,则依赖检查通过,并继续进行版本验证。 + +这个过程是自动的,旨在处理绝大多数常见情况,减少开发者手动配置的麻烦。 + +### 注意事项 + +- **最佳实践**: 尽管有智能别名解析,我们仍然**强烈推荐**使用 `PythonDependency` 对象来明确指定 `package_name` (导入名) 和 `install_name` (安装名),这能确保最高的准确性和可读性。 +- **覆盖范围**: 内置的别名映射表涵盖了大量常用库,但无法保证100%覆盖所有情况。如果遇到别名库未收录的包,请使用 `PythonDependency` 对象进行精确定义。 ## 日志输出示例 @@ -192,12 +220,13 @@ configure_dependency_settings(auto_install_timeout=600) ## 最佳实践 -1. **使用详细的PythonDependency对象** 以获得更好的控制和文档 -2. **配置PyPI镜像源** 特别是在中国大陆地区,可显著提升下载速度 -3. **合理设置可选依赖** 避免非核心功能阻止插件加载 -4. **指定版本要求** 确保兼容性 -5. **添加描述信息** 帮助用户理解依赖的用途 -6. **测试依赖配置** 在不同环境中验证依赖是否正确 +1. **优先使用`PythonDependency`对象**: 这是最可靠、最明确的方式,尤其是在安装名和导入名不同时。 +2. **利用智能别名解析**: 对于常见的、安装名与导入名不一致的包 (如 `beautifulsoup4`, `Pillow` 等),可以直接在字符串列表里使用安装名,系统会自动解析。 +3. **配置PyPI镜像源**: 特别是在中国大陆地区,可显著提升下载速度。 +4. **合理设置可选依赖**: 避免非核心功能阻止插件加载。 +5. **指定版本要求**: 确保兼容性。 +6. **添加描述信息**: 帮助用户理解依赖的用途。 +7. **测试依赖配置**: 在不同环境中验证依赖是否正确。 ## 安全考虑 @@ -225,7 +254,8 @@ configure_dependency_settings(auto_install_timeout=600) ### 导入错误 -1. 确认包名与导入名一致 -2. 检查可选依赖配置 -3. 验证安装是否成功 +1. **确认包名与导入名**: 检查安装名和导入名是否一致。如果不一致,推荐使用 `PythonDependency` 对象明确指定 `package_name` 和 `install_name`。 +2. **利用自动别名解析**: 对于常见库,系统会自动尝试解析别名。如果你的库比较冷门且名称不一致,请使用 `PythonDependency` 对象。 +3. **检查可选依赖配置**: 确认 `optional=True` 是否被正确设置。 +4. **验证安装是否成功**: 查看日志,确认 `pip install` 过程没有报错。 diff --git a/src/plugin_system/utils/dependency_alias.py b/src/plugin_system/utils/dependency_alias.py new file mode 100644 index 000000000..5a817286c --- /dev/null +++ b/src/plugin_system/utils/dependency_alias.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +""" +本模块包含一个从Python包的“安装名”到其“导入名”的映射。 + +这个映射表主要用于解决一个常见问题:某些Python包通过pip安装时使用的名称 +与在代码中`import`时使用的名称不一致。例如,我们使用`pip install beautifulsoup4` +来安装,但在代码中却需要`import bs4`。 + +当插件系统检查依赖时,如果一个开发者只简单地在依赖列表中写了安装名 +(例如 "beautifulsoup4"),标准的导入检查`import('beautifulsoup4')`会失败。 +通过这个映射表,依赖管理器可以在初次导入检查失败后,查询是否存在一个 +已知的别名(例如 "bs4"),并尝试使用该别名进行二次导入检查。 + +这样做的好处是: +1. 提升开发者体验:插件开发者无需强制记忆这些特殊的名称对应关系,或者强制 + 使用更复杂的`PythonDependency`对象来分别指定安装名和导入名。 +2. 增强系统健壮性:减少因名称不一致导致的插件加载失败问题。 +3. 兼容性:对遵循最佳实践、正确指定了`package_name`和`install_name`的 + 开发者没有任何影响。 + +开发者可以持续向这个列表中贡献新的映射关系,使其更加完善。 +""" + +INSTALL_NAME_TO_IMPORT_NAME = { + # ============== 数据科学与机器学习 (Data Science & Machine Learning) ============== + "scikit-learn": "sklearn", # 机器学习库 + "scikit-image": "skimage", # 图像处理库 + "opencv-python": "cv2", # OpenCV 计算机视觉库 + "opencv-contrib-python": "cv2", # OpenCV 扩展模块 + "tensorflow-gpu": "tensorflow", # TensorFlow GPU版本 + "tensorboardx": "tensorboardX", # TensorBoard 的封装 + "torchvision": "torchvision", # PyTorch 视觉库 (通常与 torch 一起) + "torchaudio": "torchaudio", # PyTorch 音频库 + "catboost": "catboost", # CatBoost 梯度提升库 + "lightgbm": "lightgbm", # LightGBM 梯度提升库 + "xgboost": "xgboost", # XGBoost 梯度提升库 + "imbalanced-learn": "imblearn", # 处理不平衡数据集 + "seqeval": "seqeval", # 序列标注评估 + "gensim": "gensim", # 主题建模和NLP + "nltk": "nltk", # 自然语言工具包 + "spacy": "spacy", # 工业级自然语言处理 + "fuzzywuzzy": "fuzzywuzzy", # 模糊字符串匹配 + "python-levenshtein": "Levenshtein", # Levenshtein 距离计算 + + # ============== Web开发与API (Web Development & API) ============== + "python-socketio": "socketio", # Socket.IO 服务器和客户端 + "python-engineio": "engineio", # Engine.IO 底层库 + "aiohttp": "aiohttp", # 异步HTTP客户端/服务器 + "python-multipart": "multipart", # 解析 multipart/form-data + "uvloop": "uvloop", # 高性能asyncio事件循环 + "httptools": "httptools", # 高性能HTTP解析器 + "websockets": "websockets", # WebSocket实现 + "fastapi": "fastapi", # 高性能Web框架 + "starlette": "starlette", # ASGI框架 + "uvicorn": "uvicorn", # ASGI服务器 + "gunicorn": "gunicorn", # WSGI服务器 + "django-rest-framework": "rest_framework", # Django REST框架 + "django-cors-headers": "corsheaders", # Django CORS处理 + "flask-jwt-extended": "flask_jwt_extended", # Flask JWT扩展 + "flask-sqlalchemy": "flask_sqlalchemy", # Flask SQLAlchemy扩展 + "flask-migrate": "flask_migrate", # Flask Alembic迁移扩展 + "python-jose": "jose", # JOSE (JWT, JWS, JWE) 实现 + "passlib": "passlib", # 密码哈希库 + "bcrypt": "bcrypt", # Bcrypt密码哈希 + + # ============== 数据库 (Database) ============== + "mysql-connector-python": "mysql.connector", # MySQL官方驱动 + "psycopg2-binary": "psycopg2", # PostgreSQL驱动 (二进制) + "pymongo": "pymongo", # MongoDB驱动 + "redis": "redis", # Redis客户端 + "aioredis": "aioredis", # 异步Redis客户端 + "sqlalchemy": "sqlalchemy", # SQL工具包和ORM + "alembic": "alembic", # SQLAlchemy数据库迁移工具 + "tortoise-orm": "tortoise", # 异步ORM + + # ============== 图像与多媒体 (Image & Multimedia) ============== + "Pillow": "PIL", # Python图像处理库 (PIL Fork) + "moviepy": "moviepy", # 视频编辑库 + "pydub": "pydub", # 音频处理库 + "pycairo": "cairo", # Cairo 2D图形库的Python绑定 + "wand": "wand", # ImageMagick的Python绑定 + + # ============== 解析与序列化 (Parsing & Serialization) ============== + "beautifulsoup4": "bs4", # HTML/XML解析库 + "lxml": "lxml", # 高性能HTML/XML解析库 + "PyYAML": "yaml", # YAML解析库 + "python-dotenv": "dotenv", # .env文件解析 + "python-dateutil": "dateutil", # 强大的日期时间解析 + "protobuf": "google.protobuf", # Protocol Buffers + "msgpack": "msgpack", # MessagePack序列化 + "orjson": "orjson", # 高性能JSON库 + "pydantic": "pydantic", # 数据验证和设置管理 + + # ============== 系统与硬件 (System & Hardware) ============== + "pyserial": "serial", # 串口通信 + "pyusb": "usb", # USB访问 + "pybluez": "bluetooth", # 蓝牙通信 (可能因平台而异) + "psutil": "psutil", # 系统信息和进程管理 + "watchdog": "watchdog", # 文件系统事件监控 + "python-gnupg": "gnupg", # GnuPG的Python接口 + + # ============== 加密与安全 (Cryptography & Security) ============== + "pycrypto": "Crypto", # 加密库 (较旧) + "pycryptodome": "Crypto", # PyCrypto的现代分支 + "cryptography": "cryptography", # 现代加密库 + "pyopenssl": "OpenSSL", # OpenSSL的Python接口 + "service-identity": "service_identity", # 服务身份验证 + + # ============== 工具与杂项 (Utilities & Miscellaneous) ============== + "setuptools": "setuptools", # 打包工具 + "pip": "pip", # 包安装器 + "tqdm": "tqdm", # 进度条 + "regex": "regex", # 替代的正则表达式引擎 + "colorama": "colorama", # 跨平台彩色终端文本 + "termcolor": "termcolor", # 终端颜色格式化 + "requests-oauthlib": "requests_oauthlib", # OAuth for Requests + "oauthlib": "oauthlib", # 通用OAuth库 + "authlib": "authlib", # OAuth和OpenID Connect客户端/服务器 + "pyjwt": "jwt", # JSON Web Token实现 + "python-editor": "editor", # 程序化地调用编辑器 + "prompt-toolkit": "prompt_toolkit", # 构建交互式命令行 + "pygments": "pygments", # 语法高亮 + "tabulate": "tabulate", # 生成漂亮的表格 + "nats-client": "nats", # NATS客户端 + "gitpython": "git", # Git的Python接口 + "pygithub": "github", # GitHub API v3的Python接口 + "python-gitlab": "gitlab", # GitLab API的Python接口 + "jira": "jira", # JIRA API的Python接口 + "python-jenkins": "jenkins", # Jenkins API的Python接口 + "huggingface-hub": "huggingface_hub", # Hugging Face Hub API + "apache-airflow": "airflow", # Airflow工作流管理 + "pandas-stubs": "pandas-stubs", # Pandas的类型存根 + "data-science-types": "data_science_types", # 数据科学类型 +} \ No newline at end of file diff --git a/src/plugin_system/utils/dependency_manager.py b/src/plugin_system/utils/dependency_manager.py index 33363168b..e524a7bd7 100644 --- a/src/plugin_system/utils/dependency_manager.py +++ b/src/plugin_system/utils/dependency_manager.py @@ -8,6 +8,7 @@ from packaging.requirements import Requirement from src.common.logger import get_logger from src.plugin_system.base.component_types import PythonDependency +from src.plugin_system.utils.dependency_alias import INSTALL_NAME_TO_IMPORT_NAME logger = get_logger("dependency_manager") @@ -190,41 +191,58 @@ class DependencyManager: 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 - - # 检查版本要求 + + def _try_check(import_name: str) -> bool: + """尝试使用给定的导入名进行检查""" 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) + spec = importlib.util.find_spec(import_name) + if spec is None: + return False + + # 如果没有版本要求,导入成功就够了 + if not dep.version: + return True + + # 检查版本要求 + try: + module = importlib.import_module(import_name) + 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 - + # 尝试其他常见的版本属性 + installed_version = getattr(module, 'VERSION', None) + if installed_version is None: + logger.debug(f"无法获取包 {import_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"检查包 {import_name} 版本时出错: {e}") + return True # 如果无法检查版本,假设满足要求 + + except ImportError: + return False 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 + logger.error(f"检查依赖 {import_name} 时发生未知错误: {e}") + return False + + # 1. 首先尝试使用原始的 package_name 进行检查 + if _try_check(dep.package_name): + return True + + # 2. 如果失败,查询别名映射表 + # 注意:此时 dep.package_name 可能是 simple "requests" 或 "beautifulsoup4" + import_alias = INSTALL_NAME_TO_IMPORT_NAME.get(dep.package_name) + if import_alias: + logger.debug(f"依赖 '{dep.package_name}' 导入失败, 尝试使用别名 '{import_alias}'") + if _try_check(import_alias): + return True + + # 3. 如果别名也失败了,或者没有别名,最终确认失败 + return False def _install_single_package(self, package: str, plugin_name: str = "") -> bool: """安装单个包"""