1111import sys
1212import tempfile
1313import traceback
14+ from dataclasses import dataclass
15+ from enum import Enum , auto
1416from types import ModuleType
1517
1618import yaml
3739from astrbot .core .utils .io import remove_dir
3840from astrbot .core .utils .metrics import Metric
3941from astrbot .core .utils .requirements_utils import (
42+ MissingRequirementsPlan ,
4043 plan_missing_requirements_install ,
4144)
4245
@@ -77,6 +80,19 @@ def __init__(
7780 self .error = error
7881
7982
83+ class ImportDependencyRecoveryMode (Enum ):
84+ DISABLED = auto ()
85+ PRELOAD_AND_RECOVER = auto ()
86+ RECOVER_ON_FAILURE = auto ()
87+ REINSTALL_ON_FAILURE = auto ()
88+
89+
90+ @dataclass (frozen = True )
91+ class ImportDependencyRecoveryState :
92+ mode : ImportDependencyRecoveryMode
93+ install_plan : MissingRequirementsPlan | None = None
94+
95+
8096@contextlib .contextmanager
8197def _temporary_filtered_requirements_file (
8298 * ,
@@ -137,7 +153,10 @@ async def _install_requirements_with_precheck(
137153 requirements_path ,
138154 fallback_reason ,
139155 )
140- await pip_installer .install (requirements_path = requirements_path )
156+ await pip_installer .install (
157+ requirements_path = requirements_path ,
158+ allow_target_upgrade = bool (install_plan .version_mismatch_names ),
159+ )
141160 return
142161
143162 logger .info (
@@ -148,7 +167,10 @@ async def _install_requirements_with_precheck(
148167 with _temporary_filtered_requirements_file (
149168 install_lines = install_plan .install_lines ,
150169 ) as filtered_requirements_path :
151- await pip_installer .install (requirements_path = filtered_requirements_path )
170+ await pip_installer .install (
171+ requirements_path = filtered_requirements_path ,
172+ allow_target_upgrade = bool (install_plan .version_mismatch_names ),
173+ )
152174
153175
154176class PluginManager :
@@ -332,33 +354,106 @@ async def _ensure_plugin_requirements(
332354 logger .exception (str (dependency_error ))
333355 raise dependency_error from e
334356
357+ @staticmethod
358+ def _resolve_import_dependency_recovery_state (
359+ requirements_path : str ,
360+ * ,
361+ reserved : bool ,
362+ ) -> ImportDependencyRecoveryState :
363+ if reserved or not os .path .exists (requirements_path ):
364+ return ImportDependencyRecoveryState (ImportDependencyRecoveryMode .DISABLED )
365+
366+ install_plan = plan_missing_requirements_install (requirements_path )
367+ if install_plan is None :
368+ return ImportDependencyRecoveryState (
369+ ImportDependencyRecoveryMode .RECOVER_ON_FAILURE
370+ )
371+ if install_plan .version_mismatch_names :
372+ return ImportDependencyRecoveryState (
373+ ImportDependencyRecoveryMode .REINSTALL_ON_FAILURE ,
374+ install_plan = install_plan ,
375+ )
376+
377+ return ImportDependencyRecoveryState (
378+ ImportDependencyRecoveryMode .PRELOAD_AND_RECOVER ,
379+ install_plan = install_plan ,
380+ )
381+
382+ @staticmethod
383+ def _try_import_from_installed_dependencies (
384+ path : str ,
385+ module_str : str ,
386+ root_dir_name : str ,
387+ requirements_path : str ,
388+ import_exc : Exception ,
389+ ) -> ModuleType | None :
390+ try :
391+ logger .info (
392+ f"插件 { root_dir_name } 导入失败,尝试从已安装依赖恢复: { import_exc !s} "
393+ )
394+ pip_installer .prefer_installed_dependencies (
395+ requirements_path = requirements_path
396+ )
397+ module = __import__ (path , fromlist = [module_str ])
398+ logger .info (
399+ f"插件 { root_dir_name } 已从 site-packages 恢复依赖,跳过重新安装。"
400+ )
401+ return module
402+ except (ImportError , ModuleNotFoundError ) as recover_exc :
403+ logger .info (
404+ f"插件 { root_dir_name } 已安装依赖恢复失败,将重新安装依赖: { recover_exc !s} "
405+ )
406+ return None
407+
335408 async def _import_plugin_with_dependency_recovery (
336409 self ,
337410 path : str ,
338411 module_str : str ,
339412 root_dir_name : str ,
340413 requirements_path : str ,
414+ * ,
415+ reserved : bool = False ,
341416 ) -> ModuleType :
417+ recovery_state = self ._resolve_import_dependency_recovery_state (
418+ requirements_path ,
419+ reserved = reserved ,
420+ )
421+
422+ if recovery_state .mode is ImportDependencyRecoveryMode .PRELOAD_AND_RECOVER :
423+ try :
424+ pip_installer .prefer_installed_dependencies (
425+ requirements_path = requirements_path
426+ )
427+ except Exception as preload_exc :
428+ logger .info (
429+ f"插件 { root_dir_name } 预加载已安装依赖失败,将继续常规导入: { preload_exc !s} "
430+ )
431+
342432 try :
343433 return __import__ (path , fromlist = [module_str ])
344- except (ModuleNotFoundError , ImportError ) as import_exc :
345- if os .path .exists (requirements_path ):
346- try :
347- logger .info (
348- f"插件 { root_dir_name } 导入失败,尝试从已安装依赖恢复: { import_exc !s} "
349- )
350- pip_installer .prefer_installed_dependencies (
351- requirements_path = requirements_path
352- )
353- module = __import__ (path , fromlist = [module_str ])
354- logger .info (
355- f"插件 { root_dir_name } 已从 site-packages 恢复依赖,跳过重新安装。"
356- )
357- return module
358- except Exception as recover_exc :
359- logger .info (
360- f"插件 { root_dir_name } 已安装依赖恢复失败,将重新安装依赖: { recover_exc !s} "
361- )
434+ except ModuleNotFoundError as import_exc :
435+ if recovery_state .mode in {
436+ ImportDependencyRecoveryMode .PRELOAD_AND_RECOVER ,
437+ ImportDependencyRecoveryMode .RECOVER_ON_FAILURE ,
438+ }:
439+ recovered_module = self ._try_import_from_installed_dependencies (
440+ path ,
441+ module_str ,
442+ root_dir_name ,
443+ requirements_path ,
444+ import_exc ,
445+ )
446+ if recovered_module is not None :
447+ return recovered_module
448+ elif (
449+ recovery_state .mode is ImportDependencyRecoveryMode .REINSTALL_ON_FAILURE
450+ ):
451+ assert recovery_state .install_plan is not None
452+ logger .info (
453+ "插件 %s 预检查检测到版本不匹配,跳过已安装依赖恢复: %s" ,
454+ root_dir_name ,
455+ sorted (recovery_state .install_plan .version_mismatch_names ),
456+ )
362457
363458 await self ._check_plugin_dept_update (target_plugin = root_dir_name )
364459 return __import__ (path , fromlist = [module_str ])
@@ -788,6 +883,7 @@ async def load(
788883 module_str = module_str ,
789884 root_dir_name = root_dir_name ,
790885 requirements_path = requirements_path ,
886+ reserved = reserved ,
791887 )
792888 except Exception as e :
793889 error_trace = traceback .format_exc ()
0 commit comments