From e4721fef0c04421b5eef07fab1d8a8daac9f35b9 Mon Sep 17 00:00:00 2001 From: DDSRem <1448139087@qq.com> Date: Tue, 12 May 2026 17:32:11 +0800 Subject: [PATCH] fix(plugin): allow non-core dependency upgrades during plugin install When a plugin requires a newer minor version of a non-core dependency (e.g. sentry-sdk ~=2.59.0 while 2.58.0 is installed), the conflict check now distinguishes upgrade-only conflicts from downgrade conflicts. Non-core packages that only need upgrading are allowed through; the runtime constraints file uses >= instead of == for non-core packages so pip can satisfy the upgrade without risking a downgrade. Core packages (fastapi, pydantic, sqlalchemy, etc.) remain strictly pinned and any version mismatch is still rejected. Co-Authored-By: Claude Sonnet 4.6 --- app/helper/plugin.py | 59 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/app/helper/plugin.py b/app/helper/plugin.py index ede4381e..a7f30e52 100644 --- a/app/helper/plugin.py +++ b/app/helper/plugin.py @@ -1064,6 +1064,40 @@ class PluginHelper(metaclass=WeakSingleton): return protected_packages + @staticmethod + def __is_upgrade_only_conflict(specifier_set: SpecifierSet, installed_version: Version) -> bool: + """ + 判断版本冲突是否只能通过升级来解决(specifier 允许的所有版本都严格高于已安装版本)。 + 返回 True 表示纯升级冲突;返回 False 表示可能需要降级或无法确定方向。 + """ + has_lower_bound = False + for spec in specifier_set: + op = spec.operator + ver_str = spec.version.rstrip("*").rstrip(".") or "0" + try: + ver = Version(ver_str) + except InvalidVersion: + return False + + if op in ("<", "<="): + upper = ver if op == "<" else Version(f"{ver}.post0") + if upper <= installed_version: + return False + elif op == "==": + if ver <= installed_version: + return False + elif op == "~=": + # ~=X.Y.Z 等价于 >=X.Y.Z, =", ">"): + has_lower_bound = True + # != 操作符:单独出现时可能允许低版本,需结合其他约束判断 + + # 若没有任何明确的下限约束(仅 != 等),保守地视为不确定 → 返回 False + return has_lower_bound + @classmethod def __validate_runtime_dependency_conflicts( cls, @@ -1110,12 +1144,16 @@ class PluginHelper(metaclass=WeakSingleton): installed_version, prereleases=True ): - conflicts.append(( - package_name, - str(installed_version), - str(requirement.specifier), - package_name in cls._protected_runtime_packages, - )) + is_core = package_name in cls._protected_runtime_packages + # 非核心包的纯升级冲突(插件要求更新版本)允许放行,由 pip 约束文件控制实际安装 + if is_core or not cls.__is_upgrade_only_conflict( + requirement.specifier, installed_version): + conflicts.append(( + package_name, + str(installed_version), + str(requirement.specifier), + is_core, + )) except Exception as e: logger.error(f"执行运行环境依赖冲突预检时发生错误:{e}") return False, f"插件依赖预检失败:{e}" @@ -1157,9 +1195,12 @@ class PluginHelper(metaclass=WeakSingleton): delete=False ) as temp_file: for package_name, version in sorted(protected_packages.items()): - temp_file.write( - f"{cls.__format_pkg_name_for_pip(package_name)}=={version}\n" - ) + if package_name in cls._protected_runtime_packages: + # 核心包严格锁定,插件不得改写 + temp_file.write(f"{cls.__format_pkg_name_for_pip(package_name)}=={version}\n") + else: + # 非核心主程序依赖:允许升级,但禁止降级 + temp_file.write(f"{cls.__format_pkg_name_for_pip(package_name)}>={version}\n") return Path(temp_file.name) @staticmethod