Skip to content

Commit 51c8d22

Browse files
feat: install plugin using metadata name and validate importable identifiers (AstrBotDevs#6530)
* feat: install plugin using metadata name and validate importable identifiers * fix: cleanup temporary upload extraction directory on plugin install failure * Update astrbot/core/star/star_manager.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix: avoid unnecessary install when repository directory already exists --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 7182154 commit 51c8d22

1 file changed

Lines changed: 82 additions & 43 deletions

File tree

astrbot/core/star/star_manager.py

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import functools
66
import inspect
77
import json
8+
import keyword
89
import logging
910
import os
1011
import sys
@@ -421,6 +422,43 @@ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | N
421422

422423
return metadata
423424

425+
@staticmethod
426+
def _normalize_plugin_dir_name(plugin_name: str) -> str:
427+
return plugin_name.strip()
428+
429+
@staticmethod
430+
def _validate_importable_name(plugin_name: str) -> None:
431+
if "/" in plugin_name or "\\" in plugin_name:
432+
raise ValueError(
433+
"metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。"
434+
)
435+
if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name):
436+
raise Exception(
437+
"metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。"
438+
)
439+
440+
@staticmethod
441+
def _get_plugin_dir_name_from_metadata(plugin_path: str) -> str:
442+
metadata_path = os.path.join(plugin_path, "metadata.yaml")
443+
if not os.path.exists(metadata_path):
444+
raise Exception("未找到 metadata.yaml,无法获取插件目录名。")
445+
446+
with open(metadata_path, encoding="utf-8") as f:
447+
metadata = yaml.safe_load(f)
448+
449+
if not isinstance(metadata, dict):
450+
raise Exception("metadata.yaml 格式错误。")
451+
452+
plugin_name = metadata.get("name")
453+
if not isinstance(plugin_name, str) or not plugin_name.strip():
454+
raise Exception("metadata.yaml 中缺少 name 字段。")
455+
456+
plugin_dir_name = PluginManager._normalize_plugin_dir_name(plugin_name)
457+
if not plugin_dir_name:
458+
raise Exception("metadata.yaml 中 name 字段内容非法。")
459+
PluginManager._validate_importable_name(plugin_dir_name)
460+
return plugin_dir_name
461+
424462
@staticmethod
425463
def _validate_astrbot_version_specifier(
426464
version_spec: str | None,
@@ -1201,10 +1239,30 @@ async def install_plugin(
12011239
plugin_path = ""
12021240
dir_name = ""
12031241
try:
1242+
_, repo_name, _ = self.updator.parse_github_url(repo_url)
1243+
repo_name = self.updator.format_name(repo_name)
1244+
plugin_path = os.path.join(self.plugin_store_path, repo_name)
1245+
if os.path.exists(plugin_path):
1246+
raise Exception(
1247+
f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。"
1248+
)
12041249
plugin_path = await self.updator.install(repo_url, proxy)
12051250

12061251
# reload the plugin
12071252
dir_name = os.path.basename(plugin_path)
1253+
metadata_dir_name = self._get_plugin_dir_name_from_metadata(plugin_path)
1254+
target_plugin_path = os.path.join(
1255+
self.plugin_store_path,
1256+
metadata_dir_name,
1257+
)
1258+
if target_plugin_path != plugin_path and os.path.exists(
1259+
target_plugin_path
1260+
):
1261+
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
1262+
if target_plugin_path != plugin_path:
1263+
os.rename(plugin_path, target_plugin_path)
1264+
plugin_path = target_plugin_path
1265+
dir_name = metadata_dir_name
12081266
await self._ensure_plugin_requirements(
12091267
plugin_path,
12101268
dir_name,
@@ -1573,52 +1631,25 @@ async def turn_on_plugin(self, plugin_name: str) -> None:
15731631
async def install_plugin_from_file(
15741632
self, zip_file_path: str, ignore_version_check: bool = False
15751633
):
1576-
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
1577-
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
1578-
desti_dir = os.path.join(self.plugin_store_path, dir_name)
1579-
1580-
# 第一步:检查是否已安装同目录名的插件,先终止旧插件
1581-
existing_plugin = None
1582-
for star in self.context.get_all_stars():
1583-
if star.root_dir_name == dir_name:
1584-
existing_plugin = star
1585-
break
1586-
1587-
if existing_plugin:
1588-
logger.info(f"检测到插件 {existing_plugin.name} 已安装,正在终止旧插件...")
1589-
try:
1590-
await self._terminate_plugin(existing_plugin)
1591-
except Exception:
1592-
logger.warning(traceback.format_exc())
1593-
if existing_plugin.name and existing_plugin.module_path:
1594-
await self._unbind_plugin(
1595-
existing_plugin.name, existing_plugin.module_path
1596-
)
1634+
dir_name = os.path.splitext(os.path.basename(zip_file_path))[0]
1635+
desti_dir = tempfile.mkdtemp(
1636+
dir=self.plugin_store_path, prefix="plugin_upload_"
1637+
)
1638+
temp_desti_dir = desti_dir
15971639

15981640
try:
15991641
self.updator.unzip_file(zip_file_path, desti_dir)
1600-
1601-
# 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件
1602-
try:
1603-
new_metadata = self._load_plugin_metadata(desti_dir)
1604-
if new_metadata and new_metadata.name:
1605-
for star in self.context.get_all_stars():
1606-
if (
1607-
star.name == new_metadata.name
1608-
and star.root_dir_name != dir_name
1609-
):
1610-
logger.warning(
1611-
f"检测到同名插件 {star.name} 存在于不同目录 {star.root_dir_name},正在终止..."
1612-
)
1613-
try:
1614-
await self._terminate_plugin(star)
1615-
except Exception:
1616-
logger.warning(traceback.format_exc())
1617-
if star.name and star.module_path:
1618-
await self._unbind_plugin(star.name, star.module_path)
1619-
break # 只处理第一个匹配的
1620-
except Exception as e:
1621-
logger.debug(f"读取新插件 metadata.yaml 失败,跳过同名检查: {e!s}")
1642+
metadata_dir_name = self._get_plugin_dir_name_from_metadata(desti_dir)
1643+
target_plugin_path = os.path.join(
1644+
self.plugin_store_path,
1645+
metadata_dir_name,
1646+
)
1647+
if target_plugin_path != desti_dir and os.path.exists(target_plugin_path):
1648+
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
1649+
if target_plugin_path != desti_dir:
1650+
os.rename(desti_dir, target_plugin_path)
1651+
dir_name = metadata_dir_name
1652+
desti_dir = target_plugin_path
16221653

16231654
# remove the zip
16241655
try:
@@ -1686,3 +1717,11 @@ async def install_plugin_from_file(
16861717
f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}",
16871718
)
16881719
raise
1720+
finally:
1721+
if temp_desti_dir != desti_dir and os.path.isdir(temp_desti_dir):
1722+
try:
1723+
remove_dir(temp_desti_dir)
1724+
except Exception as e:
1725+
logger.warning(
1726+
f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}",
1727+
)

0 commit comments

Comments
 (0)