From 80ad5a0d5e789c86bd562380a0a5b4d65fbc0378 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 20 Feb 2026 00:01:59 +0800 Subject: [PATCH 1/4] feat: add support for plugin astrbot-version and platform requirement checks --- .../builtin_stars/web_searcher/metadata.yaml | 7 +- .../core/star/filter/platform_adapter_type.py | 3 + astrbot/core/star/star.py | 6 + astrbot/core/star/star_manager.py | 114 +++++++- astrbot/dashboard/routes/plugin.py | 65 ++++- .../components/extension/MarketPluginCard.vue | 50 +++- .../src/components/shared/ExtensionCard.vue | 51 ++++ .../locales/en-US/features/extension.json | 14 +- .../locales/zh-CN/features/extension.json | 14 +- dashboard/src/stores/common.js | 8 + dashboard/src/utils/platformUtils.js | 26 ++ dashboard/src/views/ExtensionPage.vue | 266 +++++++++++++++++- pyproject.toml | 1 + requirements.txt | 1 + 14 files changed, 597 insertions(+), 29 deletions(-) diff --git a/astrbot/builtin_stars/web_searcher/metadata.yaml b/astrbot/builtin_stars/web_searcher/metadata.yaml index fc5309787d..ee7c5067cf 100644 --- a/astrbot/builtin_stars/web_searcher/metadata.yaml +++ b/astrbot/builtin_stars/web_searcher/metadata.yaml @@ -1,4 +1,9 @@ name: astrbot-web-searcher desc: 让 LLM 具有网页检索能力 author: Soulter -version: 1.14.514 \ No newline at end of file +version: 1.14.514 +support_platforms: + - telegram + - discord + - slack +astrbot_version: ">=4.17,<5" diff --git a/astrbot/core/star/filter/platform_adapter_type.py b/astrbot/core/star/filter/platform_adapter_type.py index 1630650a94..3ac8019ef8 100644 --- a/astrbot/core/star/filter/platform_adapter_type.py +++ b/astrbot/core/star/filter/platform_adapter_type.py @@ -11,6 +11,7 @@ class PlatformAdapterType(enum.Flag): QQOFFICIAL = enum.auto() TELEGRAM = enum.auto() WECOM = enum.auto() + WECOM_AI_BOT = enum.auto() LARK = enum.auto() DINGTALK = enum.auto() DISCORD = enum.auto() @@ -26,6 +27,7 @@ class PlatformAdapterType(enum.Flag): | QQOFFICIAL | TELEGRAM | WECOM + | WECOM_AI_BOT | LARK | DINGTALK | DISCORD @@ -44,6 +46,7 @@ class PlatformAdapterType(enum.Flag): "qq_official": PlatformAdapterType.QQOFFICIAL, "telegram": PlatformAdapterType.TELEGRAM, "wecom": PlatformAdapterType.WECOM, + "wecom_ai_bot": PlatformAdapterType.WECOM_AI_BOT, "lark": PlatformAdapterType.LARK, "dingtalk": PlatformAdapterType.DINGTALK, "discord": PlatformAdapterType.DISCORD, diff --git a/astrbot/core/star/star.py b/astrbot/core/star/star.py index c5b7b12430..8cebbd7720 100644 --- a/astrbot/core/star/star.py +++ b/astrbot/core/star/star.py @@ -61,6 +61,12 @@ class StarMetadata: logo_path: str | None = None """插件 Logo 的路径""" + support_platforms: list[str] = field(default_factory=list) + """插件声明支持的平台适配器 ID 列表(对应 ADAPTER_NAME_2_TYPE 的 key)""" + + astrbot_version: str | None = None + """插件要求的 AstrBot 版本范围(PEP 440 specifier,如 >=4.13.0,<4.17.0)""" + def __str__(self) -> str: return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}" diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 51f50aedfb..f16178cb15 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -11,10 +11,13 @@ from types import ModuleType import yaml +from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.version import InvalidVersion, Version from astrbot.core import logger, pip_installer, sp from astrbot.core.agent.handoff import FunctionTool, HandoffTool from astrbot.core.config.astrbot_config import AstrBotConfig +from astrbot.core.config.default import VERSION from astrbot.core.platform.register import unregister_platform_adapters_by_module from astrbot.core.provider.register import llm_tools from astrbot.core.utils.astrbot_path import ( @@ -40,6 +43,10 @@ logger.warning("未安装 watchfiles,无法实现插件的热重载。") +class PluginVersionIncompatibleError(Exception): + """Raised when plugin astrbot_version is incompatible with current AstrBot.""" + + class PluginManager: def __init__(self, context: Context, config: AstrBotConfig) -> None: self.updator = PluginUpdator() @@ -268,10 +275,64 @@ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | N version=metadata["version"], repo=metadata["repo"] if "repo" in metadata else None, display_name=metadata.get("display_name", None), + support_platforms=( + [ + platform_id + for platform_id in metadata["support_platforms"] + if isinstance(platform_id, str) + ] + if isinstance(metadata.get("support_platforms"), list) + else [] + ), + astrbot_version=( + metadata["astrbot_version"] + if isinstance(metadata.get("astrbot_version"), str) + else None + ), ) return metadata + @staticmethod + def _validate_astrbot_version_specifier( + version_spec: str | None, + ) -> tuple[bool, str | None]: + if not version_spec: + return True, None + + normalized_spec = version_spec.strip() + if not normalized_spec: + return True, None + + if "v" in normalized_spec.lower(): + return ( + False, + "astrbot_version 不允许包含 v/V,请使用如 >=4.16,<5 的格式。", + ) + + try: + specifier = SpecifierSet(normalized_spec) + except InvalidSpecifier: + return ( + False, + "astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5。", + ) + + try: + current_version = Version(VERSION) + except InvalidVersion: + return ( + False, + f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。", + ) + + if current_version not in specifier: + return ( + False, + f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}", + ) + return True, None + @staticmethod def _get_plugin_related_modules( plugin_root_dir: str, @@ -408,7 +469,12 @@ async def reload(self, specified_plugin_name=None): return result - async def load(self, specified_module_path=None, specified_dir_name=None): + async def load( + self, + specified_module_path=None, + specified_dir_name=None, + ignore_version_check: bool = False, + ): """载入插件。 当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。 @@ -507,10 +573,25 @@ async def load(self, specified_module_path=None, specified_dir_name=None): metadata.version = metadata_yaml.version metadata.repo = metadata_yaml.repo metadata.display_name = metadata_yaml.display_name + metadata.support_platforms = metadata_yaml.support_platforms + metadata.astrbot_version = metadata_yaml.astrbot_version except Exception as e: logger.warning( f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。", ) + + if not ignore_version_check: + is_valid, error_message = ( + self._validate_astrbot_version_specifier( + metadata.astrbot_version, + ) + ) + if not is_valid: + raise PluginVersionIncompatibleError( + error_message + or "The plugin is not compatible with the current AstrBot version." + ) + logger.info(metadata) metadata.config = plugin_config p_name = (metadata.name or "unknown").lower().replace("/", "_") @@ -621,6 +702,19 @@ async def load(self, specified_module_path=None, specified_dir_name=None): ) if not metadata: raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。") + + if not ignore_version_check: + is_valid, error_message = ( + self._validate_astrbot_version_specifier( + metadata.astrbot_version, + ) + ) + if not is_valid: + raise PluginVersionIncompatibleError( + error_message + or "The plugin is not compatible with the current AstrBot version." + ) + metadata.star_cls = obj metadata.config = plugin_config metadata.module = module @@ -754,7 +848,9 @@ async def _cleanup_failed_plugin_install( f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}", ) - async def install_plugin(self, repo_url: str, proxy=""): + async def install_plugin( + self, repo_url: str, proxy: str = "", ignore_version_check: bool = False + ): """从仓库 URL 安装插件 从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中 @@ -788,7 +884,10 @@ async def install_plugin(self, repo_url: str, proxy=""): # reload the plugin dir_name = os.path.basename(plugin_path) - success, error_message = await self.load(specified_dir_name=dir_name) + success, error_message = await self.load( + specified_dir_name=dir_name, + ignore_version_check=ignore_version_check, + ) if not success: raise Exception( error_message @@ -1092,7 +1191,9 @@ async def turn_on_plugin(self, plugin_name: str) -> None: await self.reload(plugin_name) - async def install_plugin_from_file(self, zip_file_path: str): + async def install_plugin_from_file( + self, zip_file_path: str, ignore_version_check: bool = False + ): dir_name = os.path.basename(zip_file_path).replace(".zip", "") dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower() desti_dir = os.path.join(self.plugin_store_path, dir_name) @@ -1148,7 +1249,10 @@ async def install_plugin_from_file(self, zip_file_path: str): except BaseException as e: logger.warning(f"删除插件压缩包失败: {e!s}") # await self.reload() - success, error_message = await self.load(specified_dir_name=dir_name) + success, error_message = await self.load( + specified_dir_name=dir_name, + ignore_version_check=ignore_version_check, + ) if not success: raise Exception( error_message diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index bfa4dca397..a39f0406bb 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -19,7 +19,10 @@ from astrbot.core.star.filter.permission import PermissionTypeFilter from astrbot.core.star.filter.regex import RegexFilter from astrbot.core.star.star_handler import EventType, star_handlers_registry -from astrbot.core.star.star_manager import PluginManager +from astrbot.core.star.star_manager import ( + PluginManager, + PluginVersionIncompatibleError, +) from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from .route import Response, Route, RouteContext @@ -46,6 +49,7 @@ def __init__( super().__init__(context) self.routes = { "/plugin/get": ("GET", self.get_plugins), + "/plugin/check-compat": ("POST", self.check_plugin_compatibility), "/plugin/install": ("POST", self.install_plugin), "/plugin/install-upload": ("POST", self.install_plugin_upload), "/plugin/update": ("POST", self.update_plugin), @@ -78,6 +82,27 @@ def __init__( self._logo_cache = {} + async def check_plugin_compatibility(self): + try: + data = await request.get_json() + version_spec = data.get("astrbot_version", "") + is_valid, message = self.plugin_manager._validate_astrbot_version_specifier( + version_spec + ) + return ( + Response() + .ok( + { + "compatible": is_valid, + "message": message, + "astrbot_version": version_spec, + } + ) + .__dict__ + ) + except Exception as e: + return Response().error(str(e)).__dict__ + async def reload_failed_plugins(self): if DEMO_MODE: return ( @@ -118,7 +143,7 @@ async def reload_plugins(self): try: success, message = await self.plugin_manager.reload(plugin_name) if not success: - return Response().error(message).__dict__ + return Response().error(message or "插件重载失败").__dict__ return Response().ok(None, "重载成功。").__dict__ except Exception as e: logger.error(f"/api/plugin/reload: {traceback.format_exc()}") @@ -345,6 +370,8 @@ async def get_plugins(self): ), "display_name": plugin.display_name, "logo": f"/api/file/{logo_url}" if logo_url else None, + "support_platforms": plugin.support_platforms, + "astrbot_version": plugin.astrbot_version, } # 检查是否为全空的幽灵插件 if not any( @@ -439,6 +466,7 @@ async def install_plugin(self): post_data = await request.get_json() repo_url = post_data["url"] + ignore_version_check = bool(post_data.get("ignore_version_check", False)) proxy: str = post_data.get("proxy", None) if proxy: @@ -446,10 +474,23 @@ async def install_plugin(self): try: logger.info(f"正在安装插件 {repo_url}") - plugin_info = await self.plugin_manager.install_plugin(repo_url, proxy) + plugin_info = await self.plugin_manager.install_plugin( + repo_url, + proxy, + ignore_version_check=ignore_version_check, + ) # self.core_lifecycle.restart() logger.info(f"安装插件 {repo_url} 成功。") return Response().ok(plugin_info, "安装成功。").__dict__ + except PluginVersionIncompatibleError as e: + return { + "status": "warning", + "message": str(e), + "data": { + "warning_type": "astrbot_version_incompatible", + "can_ignore": True, + }, + } except Exception as e: logger.error(traceback.format_exc()) return Response().error(str(e)).__dict__ @@ -465,16 +506,32 @@ async def install_plugin_upload(self): try: file = await request.files file = file["file"] + form_data = await request.form + ignore_version_check = ( + str(form_data.get("ignore_version_check", "false")).lower() == "true" + ) logger.info(f"正在安装用户上传的插件 {file.filename}") file_path = os.path.join( get_astrbot_temp_path(), f"plugin_upload_{file.filename}", ) await file.save(file_path) - plugin_info = await self.plugin_manager.install_plugin_from_file(file_path) + plugin_info = await self.plugin_manager.install_plugin_from_file( + file_path, + ignore_version_check=ignore_version_check, + ) # self.core_lifecycle.restart() logger.info(f"安装插件 {file.filename} 成功") return Response().ok(plugin_info, "安装成功。").__dict__ + except PluginVersionIncompatibleError as e: + return { + "status": "warning", + "message": str(e), + "data": { + "warning_type": "astrbot_version_incompatible", + "can_ignore": True, + }, + } except Exception as e: logger.error(traceback.format_exc()) return Response().error(str(e)).__dict__ diff --git a/dashboard/src/components/extension/MarketPluginCard.vue b/dashboard/src/components/extension/MarketPluginCard.vue index 03425553dc..a32bf3b85b 100644 --- a/dashboard/src/components/extension/MarketPluginCard.vue +++ b/dashboard/src/components/extension/MarketPluginCard.vue @@ -1,5 +1,6 @@