|
11 | 11 | from types import ModuleType |
12 | 12 |
|
13 | 13 | import yaml |
| 14 | +from packaging.specifiers import InvalidSpecifier, SpecifierSet |
| 15 | +from packaging.version import InvalidVersion, Version |
14 | 16 |
|
15 | 17 | from astrbot.core import logger, pip_installer, sp |
16 | 18 | from astrbot.core.agent.handoff import FunctionTool, HandoffTool |
17 | 19 | from astrbot.core.config.astrbot_config import AstrBotConfig |
| 20 | +from astrbot.core.config.default import VERSION |
18 | 21 | from astrbot.core.platform.register import unregister_platform_adapters_by_module |
19 | 22 | from astrbot.core.provider.register import llm_tools |
20 | 23 | from astrbot.core.utils.astrbot_path import ( |
|
40 | 43 | logger.warning("未安装 watchfiles,无法实现插件的热重载。") |
41 | 44 |
|
42 | 45 |
|
| 46 | +class PluginVersionIncompatibleError(Exception): |
| 47 | + """Raised when plugin astrbot_version is incompatible with current AstrBot.""" |
| 48 | + |
| 49 | + |
43 | 50 | class PluginManager: |
44 | 51 | def __init__(self, context: Context, config: AstrBotConfig) -> None: |
45 | 52 | self.updator = PluginUpdator() |
@@ -268,10 +275,58 @@ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | N |
268 | 275 | version=metadata["version"], |
269 | 276 | repo=metadata["repo"] if "repo" in metadata else None, |
270 | 277 | display_name=metadata.get("display_name", None), |
| 278 | + support_platforms=( |
| 279 | + [ |
| 280 | + platform_id |
| 281 | + for platform_id in metadata["support_platforms"] |
| 282 | + if isinstance(platform_id, str) |
| 283 | + ] |
| 284 | + if isinstance(metadata.get("support_platforms"), list) |
| 285 | + else [] |
| 286 | + ), |
| 287 | + astrbot_version=( |
| 288 | + metadata["astrbot_version"] |
| 289 | + if isinstance(metadata.get("astrbot_version"), str) |
| 290 | + else None |
| 291 | + ), |
271 | 292 | ) |
272 | 293 |
|
273 | 294 | return metadata |
274 | 295 |
|
| 296 | + @staticmethod |
| 297 | + def _validate_astrbot_version_specifier( |
| 298 | + version_spec: str | None, |
| 299 | + ) -> tuple[bool, str | None]: |
| 300 | + if not version_spec: |
| 301 | + return True, None |
| 302 | + |
| 303 | + normalized_spec = version_spec.strip() |
| 304 | + if not normalized_spec: |
| 305 | + return True, None |
| 306 | + |
| 307 | + try: |
| 308 | + specifier = SpecifierSet(normalized_spec) |
| 309 | + except InvalidSpecifier: |
| 310 | + return ( |
| 311 | + False, |
| 312 | + "astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5。", |
| 313 | + ) |
| 314 | + |
| 315 | + try: |
| 316 | + current_version = Version(VERSION) |
| 317 | + except InvalidVersion: |
| 318 | + return ( |
| 319 | + False, |
| 320 | + f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。", |
| 321 | + ) |
| 322 | + |
| 323 | + if current_version not in specifier: |
| 324 | + return ( |
| 325 | + False, |
| 326 | + f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}", |
| 327 | + ) |
| 328 | + return True, None |
| 329 | + |
275 | 330 | @staticmethod |
276 | 331 | def _get_plugin_related_modules( |
277 | 332 | plugin_root_dir: str, |
@@ -408,7 +463,12 @@ async def reload(self, specified_plugin_name=None): |
408 | 463 |
|
409 | 464 | return result |
410 | 465 |
|
411 | | - async def load(self, specified_module_path=None, specified_dir_name=None): |
| 466 | + async def load( |
| 467 | + self, |
| 468 | + specified_module_path=None, |
| 469 | + specified_dir_name=None, |
| 470 | + ignore_version_check: bool = False, |
| 471 | + ): |
412 | 472 | """载入插件。 |
413 | 473 | 当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。 |
414 | 474 |
|
@@ -507,10 +567,25 @@ async def load(self, specified_module_path=None, specified_dir_name=None): |
507 | 567 | metadata.version = metadata_yaml.version |
508 | 568 | metadata.repo = metadata_yaml.repo |
509 | 569 | metadata.display_name = metadata_yaml.display_name |
| 570 | + metadata.support_platforms = metadata_yaml.support_platforms |
| 571 | + metadata.astrbot_version = metadata_yaml.astrbot_version |
510 | 572 | except Exception as e: |
511 | 573 | logger.warning( |
512 | 574 | f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。", |
513 | 575 | ) |
| 576 | + |
| 577 | + if not ignore_version_check: |
| 578 | + is_valid, error_message = ( |
| 579 | + self._validate_astrbot_version_specifier( |
| 580 | + metadata.astrbot_version, |
| 581 | + ) |
| 582 | + ) |
| 583 | + if not is_valid: |
| 584 | + raise PluginVersionIncompatibleError( |
| 585 | + error_message |
| 586 | + or "The plugin is not compatible with the current AstrBot version." |
| 587 | + ) |
| 588 | + |
514 | 589 | logger.info(metadata) |
515 | 590 | metadata.config = plugin_config |
516 | 591 | p_name = (metadata.name or "unknown").lower().replace("/", "_") |
@@ -621,6 +696,19 @@ async def load(self, specified_module_path=None, specified_dir_name=None): |
621 | 696 | ) |
622 | 697 | if not metadata: |
623 | 698 | raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。") |
| 699 | + |
| 700 | + if not ignore_version_check: |
| 701 | + is_valid, error_message = ( |
| 702 | + self._validate_astrbot_version_specifier( |
| 703 | + metadata.astrbot_version, |
| 704 | + ) |
| 705 | + ) |
| 706 | + if not is_valid: |
| 707 | + raise PluginVersionIncompatibleError( |
| 708 | + error_message |
| 709 | + or "The plugin is not compatible with the current AstrBot version." |
| 710 | + ) |
| 711 | + |
624 | 712 | metadata.star_cls = obj |
625 | 713 | metadata.config = plugin_config |
626 | 714 | metadata.module = module |
@@ -754,7 +842,9 @@ async def _cleanup_failed_plugin_install( |
754 | 842 | f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}", |
755 | 843 | ) |
756 | 844 |
|
757 | | - async def install_plugin(self, repo_url: str, proxy=""): |
| 845 | + async def install_plugin( |
| 846 | + self, repo_url: str, proxy: str = "", ignore_version_check: bool = False |
| 847 | + ): |
758 | 848 | """从仓库 URL 安装插件 |
759 | 849 |
|
760 | 850 | 从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中 |
@@ -788,7 +878,10 @@ async def install_plugin(self, repo_url: str, proxy=""): |
788 | 878 |
|
789 | 879 | # reload the plugin |
790 | 880 | dir_name = os.path.basename(plugin_path) |
791 | | - success, error_message = await self.load(specified_dir_name=dir_name) |
| 881 | + success, error_message = await self.load( |
| 882 | + specified_dir_name=dir_name, |
| 883 | + ignore_version_check=ignore_version_check, |
| 884 | + ) |
792 | 885 | if not success: |
793 | 886 | raise Exception( |
794 | 887 | error_message |
@@ -1092,7 +1185,9 @@ async def turn_on_plugin(self, plugin_name: str) -> None: |
1092 | 1185 |
|
1093 | 1186 | await self.reload(plugin_name) |
1094 | 1187 |
|
1095 | | - async def install_plugin_from_file(self, zip_file_path: str): |
| 1188 | + async def install_plugin_from_file( |
| 1189 | + self, zip_file_path: str, ignore_version_check: bool = False |
| 1190 | + ): |
1096 | 1191 | dir_name = os.path.basename(zip_file_path).replace(".zip", "") |
1097 | 1192 | dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower() |
1098 | 1193 | desti_dir = os.path.join(self.plugin_store_path, dir_name) |
@@ -1148,7 +1243,10 @@ async def install_plugin_from_file(self, zip_file_path: str): |
1148 | 1243 | except BaseException as e: |
1149 | 1244 | logger.warning(f"删除插件压缩包失败: {e!s}") |
1150 | 1245 | # await self.reload() |
1151 | | - success, error_message = await self.load(specified_dir_name=dir_name) |
| 1246 | + success, error_message = await self.load( |
| 1247 | + specified_dir_name=dir_name, |
| 1248 | + ignore_version_check=ignore_version_check, |
| 1249 | + ) |
1152 | 1250 | if not success: |
1153 | 1251 | raise Exception( |
1154 | 1252 | error_message |
|
0 commit comments