From 7831503f43af34f468a73e2fdea9e97dd3b10c40 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 17 Mar 2026 23:02:09 +0800 Subject: [PATCH 1/4] feat: install plugin using metadata name and validate importable identifiers --- astrbot/core/star/star_manager.py | 113 +++++++++++++++++++----------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 57be1e9a99..b1f6271e13 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -5,6 +5,7 @@ import functools import inspect import json +import keyword import logging import os import sys @@ -421,6 +422,43 @@ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | N return metadata + @staticmethod + def _normalize_plugin_dir_name(plugin_name: str) -> str: + return plugin_name.strip() + + @staticmethod + def _validate_importable_name(plugin_name: str) -> None: + if "/" in plugin_name or "\\" in plugin_name: + raise Exception( + "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。" + ) + if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name): + raise Exception( + "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。" + ) + + @staticmethod + def _get_plugin_dir_name_from_metadata(plugin_path: str) -> str: + metadata_path = os.path.join(plugin_path, "metadata.yaml") + if not os.path.exists(metadata_path): + raise Exception("未找到 metadata.yaml,无法获取插件目录名。") + + with open(metadata_path, encoding="utf-8") as f: + metadata = yaml.safe_load(f) + + if not isinstance(metadata, dict): + raise Exception("metadata.yaml 格式错误。") + + plugin_name = metadata.get("name") + if not isinstance(plugin_name, str) or not plugin_name.strip(): + raise Exception("metadata.yaml 中缺少 name 字段。") + + plugin_dir_name = PluginManager._normalize_plugin_dir_name(plugin_name) + if not plugin_dir_name: + raise Exception("metadata.yaml 中 name 字段内容非法。") + PluginManager._validate_importable_name(plugin_dir_name) + return plugin_dir_name + @staticmethod def _validate_astrbot_version_specifier( version_spec: str | None, @@ -1201,10 +1239,31 @@ async def install_plugin( plugin_path = "" dir_name = "" try: + _, repo_name, _ = self.updator.parse_github_url(repo_url) + repo_name = self.updator.format_name(repo_name) + plugin_path = os.path.join(self.plugin_store_path, repo_name) + plugin_path_exists = os.path.exists(plugin_path) plugin_path = await self.updator.install(repo_url, proxy) # reload the plugin dir_name = os.path.basename(plugin_path) + metadata_dir_name = self._get_plugin_dir_name_from_metadata(plugin_path) + target_plugin_path = os.path.join( + self.plugin_store_path, + metadata_dir_name, + ) + if plugin_path_exists: + raise Exception( + f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" + ) + if target_plugin_path != plugin_path and os.path.exists( + target_plugin_path + ): + raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") + if target_plugin_path != plugin_path: + os.rename(plugin_path, target_plugin_path) + plugin_path = target_plugin_path + dir_name = metadata_dir_name await self._ensure_plugin_requirements( plugin_path, dir_name, @@ -1576,49 +1635,23 @@ async def install_plugin_from_file( 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) - - # 第一步:检查是否已安装同目录名的插件,先终止旧插件 - existing_plugin = None - for star in self.context.get_all_stars(): - if star.root_dir_name == dir_name: - existing_plugin = star - break - - if existing_plugin: - logger.info(f"检测到插件 {existing_plugin.name} 已安装,正在终止旧插件...") - try: - await self._terminate_plugin(existing_plugin) - except Exception: - logger.warning(traceback.format_exc()) - if existing_plugin.name and existing_plugin.module_path: - await self._unbind_plugin( - existing_plugin.name, existing_plugin.module_path - ) + desti_dir_exists = os.path.exists(desti_dir) try: self.updator.unzip_file(zip_file_path, desti_dir) - - # 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件 - try: - new_metadata = self._load_plugin_metadata(desti_dir) - if new_metadata and new_metadata.name: - for star in self.context.get_all_stars(): - if ( - star.name == new_metadata.name - and star.root_dir_name != dir_name - ): - logger.warning( - f"检测到同名插件 {star.name} 存在于不同目录 {star.root_dir_name},正在终止..." - ) - try: - await self._terminate_plugin(star) - except Exception: - logger.warning(traceback.format_exc()) - if star.name and star.module_path: - await self._unbind_plugin(star.name, star.module_path) - break # 只处理第一个匹配的 - except Exception as e: - logger.debug(f"读取新插件 metadata.yaml 失败,跳过同名检查: {e!s}") + metadata_dir_name = self._get_plugin_dir_name_from_metadata(desti_dir) + target_plugin_path = os.path.join( + self.plugin_store_path, + metadata_dir_name, + ) + if desti_dir_exists: + raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") + if target_plugin_path != desti_dir and os.path.exists(target_plugin_path): + raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") + if target_plugin_path != desti_dir: + os.rename(desti_dir, target_plugin_path) + dir_name = metadata_dir_name + desti_dir = target_plugin_path # remove the zip try: From 50a1758166aded4b54f99191e92ecd961b5df8e5 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 17 Mar 2026 23:16:29 +0800 Subject: [PATCH 2/4] fix: cleanup temporary upload extraction directory on plugin install failure --- astrbot/core/star/star_manager.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index b1f6271e13..ac3010e3df 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -1632,10 +1632,11 @@ async def turn_on_plugin(self, plugin_name: str) -> None: 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) - desti_dir_exists = os.path.exists(desti_dir) + dir_name = os.path.splitext(os.path.basename(zip_file_path))[0] + desti_dir = tempfile.mkdtemp( + dir=self.plugin_store_path, prefix="plugin_upload_" + ) + temp_desti_dir = desti_dir try: self.updator.unzip_file(zip_file_path, desti_dir) @@ -1644,8 +1645,6 @@ async def install_plugin_from_file( self.plugin_store_path, metadata_dir_name, ) - if desti_dir_exists: - raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != desti_dir and os.path.exists(target_plugin_path): raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != desti_dir: @@ -1719,3 +1718,11 @@ async def install_plugin_from_file( f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}", ) raise + finally: + if temp_desti_dir != desti_dir and os.path.isdir(temp_desti_dir): + try: + remove_dir(temp_desti_dir) + except Exception as e: + logger.warning( + f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}", + ) From 8994b58d119b7c75aae650ff884c0e609e0e1ddd Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:19:50 +0800 Subject: [PATCH 3/4] Update astrbot/core/star/star_manager.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- astrbot/core/star/star_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index ac3010e3df..cc6154e885 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -429,7 +429,7 @@ def _normalize_plugin_dir_name(plugin_name: str) -> str: @staticmethod def _validate_importable_name(plugin_name: str) -> None: if "/" in plugin_name or "\\" in plugin_name: - raise Exception( + raise ValueError( "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。" ) if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name): From 4f954cf0292891df2d4a678a3c353e2916c58f40 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 17 Mar 2026 23:20:33 +0800 Subject: [PATCH 4/4] fix: avoid unnecessary install when repository directory already exists --- astrbot/core/star/star_manager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index cc6154e885..25df73f642 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -1242,7 +1242,10 @@ async def install_plugin( _, repo_name, _ = self.updator.parse_github_url(repo_url) repo_name = self.updator.format_name(repo_name) plugin_path = os.path.join(self.plugin_store_path, repo_name) - plugin_path_exists = os.path.exists(plugin_path) + if os.path.exists(plugin_path): + raise Exception( + f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" + ) plugin_path = await self.updator.install(repo_url, proxy) # reload the plugin @@ -1252,10 +1255,6 @@ async def install_plugin( self.plugin_store_path, metadata_dir_name, ) - if plugin_path_exists: - raise Exception( - f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" - ) if target_plugin_path != plugin_path and os.path.exists( target_plugin_path ):