From 6dd9bbb51677c605b824303b9697a8e9c5e542bd Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 26 Oct 2025 20:30:54 +0800 Subject: [PATCH 1/3] feat: enhance plugin metadata with display name and logo support --- astrbot/core/file_token_service.py | 7 ++++- astrbot/core/star/star.py | 6 ++++ astrbot/core/star/star_manager.py | 20 ++++++++----- astrbot/dashboard/routes/plugin.py | 29 +++++++++++++++---- .../src/components/shared/ExtensionCard.vue | 7 ++--- dashboard/src/stores/common.js | 5 ++-- dashboard/src/views/ExtensionPage.vue | 20 +++++++------ 7 files changed, 66 insertions(+), 28 deletions(-) diff --git a/astrbot/core/file_token_service.py b/astrbot/core/file_token_service.py index ce5e2349a..56fe7ea10 100644 --- a/astrbot/core/file_token_service.py +++ b/astrbot/core/file_token_service.py @@ -23,7 +23,12 @@ class FileTokenService: for token in expired_tokens: self.staged_files.pop(token, None) - async def register_file(self, file_path: str, timeout: float = None) -> str: + async def check_token_expired(self, file_token: str) -> bool: + async with self.lock: + await self._cleanup_expired_tokens() + return file_token not in self.staged_files + + async def register_file(self, file_path: str, timeout: float | None = None) -> str: """向令牌服务注册一个文件。 Args: diff --git a/astrbot/core/star/star.py b/astrbot/core/star/star.py index 0563e8cc8..bd16cb216 100644 --- a/astrbot/core/star/star.py +++ b/astrbot/core/star/star.py @@ -56,6 +56,12 @@ class StarMetadata: star_handler_full_names: list[str] = field(default_factory=list) """注册的 Handler 的全名列表""" + display_name: str | None = None + """用于展示的插件名称""" + + logo_path: str | None = None + """插件 Logo 的路径""" + 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 417006730..c1057e4b6 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -57,6 +57,7 @@ class PluginManager: ) """保留插件的路径。在 packages 目录下""" self.conf_schema_fname = "_conf_schema.json" + self.logo_fname = "logo.png" """插件配置 Schema 文件名""" self._pm_lock = asyncio.Lock() """StarManager操作互斥锁""" @@ -200,7 +201,7 @@ class PluginManager: if os.path.exists(os.path.join(plugin_path, "metadata.yaml")): with open( - os.path.join(plugin_path, "metadata.yaml"), "r", encoding="utf-8" + os.path.join(plugin_path, "metadata.yaml"), encoding="utf-8" ) as f: metadata = yaml.safe_load(f) elif plugin_obj and hasattr(plugin_obj, "info"): @@ -226,6 +227,7 @@ class PluginManager: desc=metadata["desc"], version=metadata["version"], repo=metadata["repo"] if "repo" in metadata else None, + display_name=metadata.get("display_name", None), ) return metadata @@ -407,13 +409,14 @@ class PluginManager: ) if os.path.exists(plugin_schema_path): # 加载插件配置 - with open(plugin_schema_path, "r", encoding="utf-8") as f: + with open(plugin_schema_path, encoding="utf-8") as f: plugin_config = AstrBotConfig( config_path=os.path.join( self.plugin_config_path, f"{root_dir_name}_config.json" ), schema=json.loads(f.read()), ) + logo_path = os.path.join(plugin_dir_path, self.logo_fname) if path in star_map: # 通过 __init__subclass__ 注册插件 @@ -430,6 +433,7 @@ class PluginManager: metadata.desc = metadata_yaml.desc metadata.version = metadata_yaml.version metadata.repo = metadata_yaml.repo + metadata.display_name = metadata_yaml.display_name except Exception as e: logger.warning( f"插件 {root_dir_name} 元数据载入失败: {str(e)}。使用默认元数据。" @@ -540,9 +544,11 @@ class PluginManager: if metadata.module_path in inactivated_plugins: metadata.activated = False - assert metadata.module_path is not None, ( - f"插件 {metadata.name} 的模块路径为空。" - ) + # Plugin logo path + if os.path.exists(logo_path): + metadata.logo_path = logo_path + + assert metadata.module_path, f"插件 {metadata.name} 模块路径为空" full_names = [] for handler in star_handlers_registry.get_handlers_by_module_name( @@ -642,7 +648,7 @@ class PluginManager: if os.path.exists(readme_path): try: - with open(readme_path, "r", encoding="utf-8") as f: + with open(readme_path, encoding="utf-8") as f: readme_content = f.read() except Exception as e: logger.warning( @@ -857,7 +863,7 @@ class PluginManager: if os.path.exists(readme_path): try: - with open(readme_path, "r", encoding="utf-8") as f: + with open(readme_path, encoding="utf-8") as f: readme_content = f.read() except Exception as e: logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}") diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 849339698..2df06dcbb 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -8,7 +8,7 @@ import ssl import certifi from .route import Route, Response, RouteContext -from astrbot.core import logger +from astrbot.core import logger, file_token_service from quart import request from astrbot.core.star.star_manager import PluginManager from astrbot.core.core_lifecycle import AstrBotCoreLifecycle @@ -54,6 +54,8 @@ class PluginRoute(Route): EventType.OnAfterMessageSentEvent: "发送消息后", } + self._logo_cache = {} + async def reload_plugins(self): if DEMO_MODE: return ( @@ -147,7 +149,7 @@ class PluginRoute(Route): return False # 加载缓存文件 - with open(cache_file, "r", encoding="utf-8") as f: + with open(cache_file, encoding="utf-8") as f: cache_data = json.load(f) cached_md5 = cache_data.get("md5") @@ -197,7 +199,7 @@ class PluginRoute(Route): """加载本地缓存的插件市场数据""" try: if os.path.exists(cache_file): - with open(cache_file, "r", encoding="utf-8") as f: + with open(cache_file, encoding="utf-8") as f: cache_data = json.load(f) # 检查缓存是否有效 if "data" in cache_data and "timestamp" in cache_data: @@ -209,7 +211,7 @@ class PluginRoute(Route): logger.warning(f"加载插件市场缓存失败: {e}") return None - def _save_plugin_cache(self, cache_file: str, data, md5: str = None): + def _save_plugin_cache(self, cache_file: str, data, md5: str | None = None): """保存插件市场数据到本地缓存""" try: # 确保目录存在 @@ -227,12 +229,27 @@ class PluginRoute(Route): except Exception as e: logger.warning(f"保存插件市场缓存失败: {e}") + async def get_plugin_logo_token(self, logo_path: str): + try: + if token := self._logo_cache.get(logo_path): + if not await file_token_service.check_token_expired(token): + return self._logo_cache[logo_path] + token = await file_token_service.register_file(logo_path, timeout=300) + self._logo_cache[logo_path] = token + return token + except Exception as e: + logger.warning(f"获取插件 Logo 失败: {e}") + return None + async def get_plugins(self): _plugin_resp = [] plugin_name = request.args.get("name") for plugin in self.plugin_manager.context.get_all_stars(): if plugin_name and plugin.name != plugin_name: continue + logo_url = None + if plugin.logo_path: + logo_url = await self.get_plugin_logo_token(plugin.logo_path) _t = { "name": plugin.name, "repo": "" if plugin.repo is None else plugin.repo, @@ -245,6 +262,8 @@ class PluginRoute(Route): "handlers": await self.get_plugin_handlers_info( plugin.star_handler_full_names ), + "display_name": plugin.display_name, + "logo": f"/api/file/{logo_url}" if logo_url else None, } _plugin_resp.append(_t) return ( @@ -469,7 +488,7 @@ class PluginRoute(Route): return Response().error(f"插件 {plugin_name} 没有README文件").__dict__ try: - with open(readme_path, "r", encoding="utf-8") as f: + with open(readme_path, encoding="utf-8") as f: readme_content = f.read() return ( diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue index d04b65374..82204a603 100644 --- a/dashboard/src/components/shared/ExtensionCard.vue +++ b/dashboard/src/components/shared/ExtensionCard.vue @@ -90,10 +90,9 @@ const viewReadme = () => { color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff' }"> - -
+
- +
@@ -171,7 +170,7 @@ const viewReadme = () => { {{ extension.author }} / {{ extension.name }}

- {{ extension.name }} + {{ extension.display_name || extension.name }} -