|
5 | 5 | import functools |
6 | 6 | import inspect |
7 | 7 | import json |
| 8 | +import keyword |
8 | 9 | import logging |
9 | 10 | import os |
10 | 11 | import sys |
@@ -421,6 +422,43 @@ def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | N |
421 | 422 |
|
422 | 423 | return metadata |
423 | 424 |
|
| 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 | + |
424 | 462 | @staticmethod |
425 | 463 | def _validate_astrbot_version_specifier( |
426 | 464 | version_spec: str | None, |
@@ -1201,10 +1239,30 @@ async def install_plugin( |
1201 | 1239 | plugin_path = "" |
1202 | 1240 | dir_name = "" |
1203 | 1241 | 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 | + ) |
1204 | 1249 | plugin_path = await self.updator.install(repo_url, proxy) |
1205 | 1250 |
|
1206 | 1251 | # reload the plugin |
1207 | 1252 | 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 |
1208 | 1266 | await self._ensure_plugin_requirements( |
1209 | 1267 | plugin_path, |
1210 | 1268 | dir_name, |
@@ -1573,52 +1631,25 @@ async def turn_on_plugin(self, plugin_name: str) -> None: |
1573 | 1631 | async def install_plugin_from_file( |
1574 | 1632 | self, zip_file_path: str, ignore_version_check: bool = False |
1575 | 1633 | ): |
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 |
1597 | 1639 |
|
1598 | 1640 | try: |
1599 | 1641 | 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 |
1622 | 1653 |
|
1623 | 1654 | # remove the zip |
1624 | 1655 | try: |
@@ -1686,3 +1717,11 @@ async def install_plugin_from_file( |
1686 | 1717 | f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}", |
1687 | 1718 | ) |
1688 | 1719 | 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