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);
+};
@@ -336,6 +341,15 @@ const viewChangelog = () => {
{{ tm("card.actions.pluginConfig") }}
+
+ {{ tm("card.actions.extensionPage") }}
+
diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json
index 52d03827b..86abb6d63 100644
--- a/dashboard/src/i18n/locales/en-US/features/extension.json
+++ b/dashboard/src/i18n/locales/en-US/features/extension.json
@@ -222,7 +222,8 @@
"togglePlugin": "Extension",
"viewHandlers": "View Handlers",
"updateTo": "Update to",
- "reinstall": "Reinstall"
+ "reinstall": "Reinstall",
+ "extensionPage": "Extension Page"
},
"status": {
"hasUpdate": "New version available",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json
index 5f838789e..d2fd64cfd 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/extension.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json
@@ -222,7 +222,8 @@
"togglePlugin": "插件",
"viewHandlers": "查看行为",
"updateTo": "更新到",
- "reinstall": "重新安装"
+ "reinstall": "重新安装",
+ "extensionPage": "插件扩展页面"
},
"status": {
"hasUpdate": "有新版本可用",
diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue
index c62637e3c..e3df245fc 100644
--- a/dashboard/src/views/ExtensionPage.vue
+++ b/dashboard/src/views/ExtensionPage.vue
@@ -130,6 +130,13 @@ const changelogDialog = reactive({
repoUrl: null,
});
+// 扩展页面对话框
+const extensionPageDialog = reactive({
+ show: false,
+ html: "",
+ title: "",
+});
+
// 新增变量支持列表视图
// 从 localStorage 恢复显示模式,默认为 false(卡片视图)
const getInitialListViewMode = () => {
@@ -680,6 +687,34 @@ const viewChangelog = (plugin) => {
changelogDialog.show = true;
};
+// 打开扩展页面
+const openExtensionPage = async (plugin) => {
+ extensionPageDialog.html = "";
+ extensionPageDialog.title = `${plugin.display_name || plugin.name} - 扩展页面`;
+ extensionPageDialog.show = true;
+ await fetchExtensionPage(plugin.name);
+};
+
+// 获取扩展页面内容
+async function fetchExtensionPage(pluginName) {
+ try {
+ // 使用 encodeURIComponent 对插件名称进行 URL 编码,防止特殊字符破坏 URL
+ const res = await axios.get(
+ `/api/plugin/extension_page?name=${encodeURIComponent(pluginName)}`
+ );
+ if (res.data.status === "ok") {
+ extensionPageDialog.html = res.data.data.html;
+ extensionPageDialog.title = res.data.data.title;
+ } else {
+ toast(res.data.message, "error");
+ extensionPageDialog.show = false;
+ }
+ } catch (err) {
+ toast("获取扩展页面失败: " + err.message, "error");
+ extensionPageDialog.show = false;
+ }
+}
+
// 为表格视图创建一个处理安装插件的函数
const handleInstallPlugin = async (plugin) => {
if (plugin.tags && plugin.tags.includes("danger")) {
@@ -1567,6 +1602,7 @@ watch(activeTab, (newTab) => {
@view-handlers="showPluginInfo(extension)"
@view-readme="viewReadme(extension)"
@view-changelog="viewChangelog(extension)"
+ @view-extension-page="openExtensionPage(extension)"
>
@@ -2658,6 +2694,29 @@ watch(activeTab, (newTab) => {
+
+
+
+
+
+
+ mdi-close
+
+ {{ extensionPageDialog.title }}
+
+
+ 关闭
+
+
+
+
+
+
+