Files
Mofox-Core/src/plugin_system/utils/dependency_manager.py

404 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import importlib
import importlib.util
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
from packaging import version
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")
class VenvDetector:
"""虚拟环境检测器"""
@staticmethod
def detect_venv_type() -> str | None:
"""
检测虚拟环境类型
返回: 'uv' | 'venv' | 'conda' | None
"""
# 检查是否在虚拟环境中
in_venv = hasattr(sys, "real_prefix") or (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
)
if not in_venv:
logger.warning("当前不在虚拟环境中")
return None
venv_path = Path(sys.prefix)
# 1. 检测 uv (优先检查 pyvenv.cfg 文件)
pyvenv_cfg = venv_path / "pyvenv.cfg"
if pyvenv_cfg.exists():
try:
with open(pyvenv_cfg, encoding="utf-8") as f:
content = f.read()
if "uv = " in content:
logger.info("检测到 uv 虚拟环境")
return "uv"
except Exception as e:
logger.warning(f"读取 pyvenv.cfg 失败: {e}")
# 2. 检测 conda (检查环境变量和路径)
if "CONDA_DEFAULT_ENV" in os.environ or "CONDA_PREFIX" in os.environ:
logger.info("检测到 conda 虚拟环境")
return "conda"
# 通过路径特征检测 conda
if "conda" in str(venv_path).lower() or "anaconda" in str(venv_path).lower():
logger.info(f"检测到 conda 虚拟环境 (路径: {venv_path})")
return "conda"
# 3. 默认为 venv (标准 Python 虚拟环境)
logger.info(f"检测到标准 venv 虚拟环境 (路径: {venv_path})")
return "venv"
@staticmethod
def get_install_command(venv_type: str | None) -> list[str]:
"""
根据虚拟环境类型获取安装命令
Args:
venv_type: 虚拟环境类型 ('uv' | 'venv' | 'conda' | None)
Returns:
安装命令列表 (不包括包名)
"""
if venv_type == "uv":
# 检查 uv 是否可用
uv_path = shutil.which("uv")
if uv_path:
logger.debug("使用 uv pip 安装")
return [uv_path, "pip", "install"]
else:
logger.warning("未找到 uv 命令,回退到标准 pip")
return [sys.executable, "-m", "pip", "install"]
elif venv_type == "conda":
# 获取当前 conda 环境名
conda_env = os.environ.get("CONDA_DEFAULT_ENV")
if conda_env:
logger.debug(f"使用 conda 在环境 {conda_env} 中安装")
return ["conda", "install", "-n", conda_env, "-y"]
else:
logger.warning("未找到 conda 环境名,回退到 pip")
return [sys.executable, "-m", "pip", "install"]
else:
# 默认使用 pip
logger.debug("使用标准 pip 安装")
return [sys.executable, "-m", "pip", "install"]
class DependencyManager:
"""Python包依赖管理器 (整合配置和虚拟环境检测)
负责检查和自动安装插件的Python包依赖
"""
def __init__(self, auto_install: bool = True, use_mirror: bool = False, mirror_url: str | None = None):
"""初始化依赖管理器
Args:
auto_install: 是否自动安装缺失的依赖
use_mirror: 是否使用PyPI镜像源
mirror_url: PyPI镜像源URL
"""
# 延迟导入配置以避免循环依赖
try:
from src.config.config import global_config
dep_config = global_config.dependency_management
# 优先使用配置文件中的设置,参数作为覆盖
self.auto_install = dep_config.auto_install if auto_install is True else auto_install
self.use_mirror = dep_config.use_mirror if use_mirror is False else use_mirror
self.mirror_url = dep_config.mirror_url if mirror_url is None else mirror_url
self.install_timeout = dep_config.auto_install_timeout
self.prompt_before_install = dep_config.prompt_before_install
except Exception as e:
logger.warning(f"无法加载依赖配置,使用默认设置: {e}")
self.auto_install = auto_install
self.use_mirror = use_mirror or False
self.mirror_url = mirror_url or ""
self.install_timeout = 300
self.prompt_before_install = False
# 检测虚拟环境类型
self.venv_type = VenvDetector.detect_venv_type()
if self.venv_type:
logger.info(f"依赖管理器初始化完成,虚拟环境类型: {self.venv_type}")
else:
logger.warning("依赖管理器初始化完成,但未检测到虚拟环境")
# ========== 依赖检查和安装核心方法 ==========
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} 时发生错误: {e!s}"
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:
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} 时发生异常: {e!s}")
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
@staticmethod
def _normalize_dependencies(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
@staticmethod
def _check_single_dependency(dep: PythonDependency) -> bool:
"""检查单个依赖是否满足要求"""
def _try_check(import_name: str) -> bool:
"""尝试使用给定的导入名进行检查"""
try:
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:
# 尝试其他常见的版本属性
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.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:
"""安装单个包 (支持虚拟环境自动检测)"""
try:
log_prefix = f"[Plugin:{plugin_name}] " if plugin_name else ""
# 根据虚拟环境类型构建安装命令
cmd = VenvDetector.get_install_command(self.venv_type)
cmd.append(package)
# 添加镜像源设置 (仅对 pip/uv 有效)
if self.use_mirror and self.mirror_url and "pip" in cmd:
cmd.extend(["-i", self.mirror_url])
logger.debug(f"{log_prefix}使用PyPI镜像源: {self.mirror_url}")
logger.info(f"{log_prefix}执行安装命令: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore",
timeout=self.install_timeout,
check=False,
)
if result.returncode == 0:
logger.info(f"{log_prefix}安装成功: {package}")
return True
else:
logger.error(f"{log_prefix}安装失败: {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: DependencyManager | None = 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_mirror: bool = False, mirror_url: str | None = None):
"""配置全局依赖管理器"""
global _global_dependency_manager
_global_dependency_manager = DependencyManager(
auto_install=auto_install, use_mirror=use_mirror, mirror_url=mirror_url
)