diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index b6160ff1e..e13854de9 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -2,6 +2,7 @@ import hashlib import json import os +import re import ssl import traceback from dataclasses import dataclass @@ -56,6 +57,7 @@ def __init__( "/plugin/reload": ("POST", self.reload_plugins), "/plugin/readme": ("GET", self.get_plugin_readme), "/plugin/changelog": ("GET", self.get_plugin_changelog), + "/plugin/extension_page": ("GET", self.get_plugin_extension_page), "/plugin/source/get": ("GET", self.get_custom_source), "/plugin/source/save": ("POST", self.save_custom_source), } @@ -315,6 +317,21 @@ async def get_plugins(self): "display_name": plugin.display_name, "logo": f"/api/file/{logo_url}" if logo_url else None, } + # 检查扩展页面是否存在(固定位置:web/index.html) + if plugin.reserved: + plugin_dir = os.path.join( + self.plugin_manager.reserved_plugin_path, + plugin.root_dir_name or "", + ) + else: + plugin_dir = os.path.join( + self.plugin_manager.plugin_store_path, + plugin.root_dir_name or "", + ) + _t["extension_page"] = await asyncio.to_thread( + os.path.exists, + os.path.join(plugin_dir, "web", "index.html") + ) # 检查是否为全空的幽灵插件 if not any( [ @@ -604,13 +621,13 @@ async def get_plugin_readme(self): plugin_obj.root_dir_name or "", ) - if not os.path.isdir(plugin_dir): + if not await asyncio.to_thread(os.path.isdir, plugin_dir): logger.warning(f"无法找到插件目录: {plugin_dir}") return Response().error(f"无法找到插件 {plugin_name} 的目录").__dict__ readme_path = os.path.join(plugin_dir, "README.md") - if not os.path.isfile(readme_path): + if not await asyncio.to_thread(os.path.isfile, readme_path): logger.warning(f"插件 {plugin_name} 没有README文件") return Response().error(f"插件 {plugin_name} 没有README文件").__dict__ @@ -660,7 +677,7 @@ async def get_plugin_changelog(self): changelog_names = ["CHANGELOG.md", "changelog.md", "CHANGELOG", "changelog"] for name in changelog_names: changelog_path = os.path.join(plugin_dir, name) - if os.path.isfile(changelog_path): + if await asyncio.to_thread(os.path.isfile, changelog_path): try: with open(changelog_path, encoding="utf-8") as f: changelog_content = f.read() @@ -694,3 +711,75 @@ async def save_custom_source(self): except Exception as e: logger.error(f"/api/plugin/source/save: {traceback.format_exc()}") return Response().error(str(e)).__dict__ + + async def get_plugin_extension_page(self): + """获取插件的扩展页面 HTML""" + plugin_name = request.args.get("name") + + if not plugin_name: + return Response().error("插件名称不能为空").__dict__ + + # 查找插件 + plugin_obj = None + for plugin in self.plugin_manager.context.get_all_stars(): + if plugin.name == plugin_name: + plugin_obj = plugin + break + + if not plugin_obj: + return Response().error(f"插件 {plugin_name} 不存在").__dict__ + + # 构建插件目录 + if plugin_obj.reserved: + plugin_dir = os.path.join( + self.plugin_manager.reserved_plugin_path, + plugin_obj.root_dir_name or "", + ) + else: + plugin_dir = os.path.join( + self.plugin_manager.plugin_store_path, + plugin_obj.root_dir_name or "", + ) + + # 扩展页面位于 web/index.html + extension_page_path = os.path.join(plugin_dir, "web", "index.html") + + if not await asyncio.to_thread(os.path.exists, extension_page_path): + return Response().error(f"插件 {plugin_name} 未配置扩展页面").__dict__ + + try: + with open(extension_page_path, encoding="utf-8") as f: + html_content = f.read() + + # 注入配置脚本 - 对插件名使用 json.dumps 防止 XSS,但保持 apiToken 为可执行代码 + safe_plugin_name = json.dumps(plugin_obj.name) + + injected_script = f""" + +""" + + # 使用正则表达式不区分大小写查找 标签 + pattern = re.compile(r"", re.IGNORECASE) + html_content = pattern.sub(injected_script + r"", html_content) + + return ( + Response() + .ok( + { + "html": html_content, + "title": f"{plugin_obj.display_name or plugin_obj.name} 扩展页面", + }, + "成功获取扩展页面", + ) + .__dict__ + ) + + except Exception as e: + logger.error(f"读取扩展页面失败: {traceback.format_exc()}") + return Response().error(f"读取扩展页面失败: {e}").__dict__ diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue index 3ad621b29..301d4df7d 100644 --- a/dashboard/src/components/shared/ExtensionCard.vue +++ b/dashboard/src/components/shared/ExtensionCard.vue @@ -30,6 +30,7 @@ const emit = defineEmits([ "view-handlers", "view-readme", "view-changelog", + "view-extension-page", ]); const reveal = ref(false); @@ -83,6 +84,10 @@ const viewReadme = () => { const viewChangelog = () => { emit("view-changelog", props.extension); }; + +const viewExtensionPage = () => { + emit("view-extension-page", props.extension); +};