feat(deps): 实现依赖包智能别名解析

引入了依赖包智能别名解析机制,以解决 Python 生态中常见的安装名与导入名不一致的问题(如 `beautifulsoup4` -> `bs4`)。

当通过包名直接导入失败时,依赖管理器会自动查询一个内置的别名映射表,并尝试使用别名再次导入。这大大提升了开发者在定义简单字符串格式依赖时的体验,减少了因名称不一致导致的依赖检查失败。

同时,更新了相关文档,详细说明了该功能的工作原理、解决了什么问题,并更新了最佳实践。
This commit is contained in:
minecraft1024a
2025-08-18 13:48:55 +08:00
parent d43d352ca5
commit 22f6cd2d94
3 changed files with 226 additions and 44 deletions

View File

@@ -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` 过程没有报错。

View File

@@ -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", # 数据科学类型
}

View File

@@ -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,9 +191,11 @@ class DependencyManager:
def _check_single_dependency(self, dep: PythonDependency) -> bool:
"""检查单个依赖是否满足要求"""
def _try_check(import_name: str) -> bool:
"""尝试使用给定的导入名进行检查"""
try:
# 尝试导入包
spec = importlib.util.find_spec(dep.package_name)
spec = importlib.util.find_spec(import_name)
if spec is None:
return False
@@ -202,14 +205,14 @@ class DependencyManager:
# 检查版本要求
try:
module = importlib.import_module(dep.package_name)
module = importlib.import_module(import_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} 的版本信息,假设满足要求")
logger.debug(f"无法获取包 {import_name} 的版本信息,假设满足要求")
return True
# 解析版本要求
@@ -217,13 +220,28 @@ class DependencyManager:
return version.parse(str(installed_version)) in req.specifier
except Exception as e:
logger.debug(f"检查包 {dep.package_name} 版本时出错: {e}")
logger.debug(f"检查包 {import_name} 版本时出错: {e}")
return True # 如果无法检查版本,假设满足要求
except ImportError:
return False
except Exception as e:
logger.error(f"检查依赖 {dep.package_name} 时发生未知错误: {e}")
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: