Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 92 additions & 3 deletions astrbot/dashboard/routes/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import hashlib
import json
import os
import re
import ssl
import traceback
from dataclasses import dataclass
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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(
[
Expand Down Expand Up @@ -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__

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"""
<script>
window.ASTRBOT_CONFIG = {{
apiUrl: "/api/plug",
pluginName: {safe_plugin_name},
apiToken: localStorage.getItem("token") || ""
}};
</script>
"""

# 使用正则表达式不区分大小写查找 </head> 标签
pattern = re.compile(r"</head\s*>", re.IGNORECASE)
html_content = pattern.sub(injected_script + r"</head>", 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__
14 changes: 14 additions & 0 deletions dashboard/src/components/shared/ExtensionCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const emit = defineEmits([
"view-handlers",
"view-readme",
"view-changelog",
"view-extension-page",
]);

const reveal = ref(false);
Expand Down Expand Up @@ -83,6 +84,10 @@ const viewReadme = () => {
const viewChangelog = () => {
emit("view-changelog", props.extension);
};

const viewExtensionPage = () => {
emit("view-extension-page", props.extension);
};
</script>

<template>
Expand Down Expand Up @@ -336,6 +341,15 @@ const viewChangelog = () => {
<v-btn v-if="!marketMode" color="primary" size="small" @click="configure">
{{ tm("card.actions.pluginConfig") }}
</v-btn>
<v-btn
v-if="!marketMode && extension.extension_page"
color="secondary"
size="small"
prepend-icon="mdi-web"
@click="viewExtensionPage"
>
{{ tm("card.actions.extensionPage") }}
</v-btn>
</v-card-actions>
</v-card>

Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/i18n/locales/en-US/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@
"togglePlugin": "Extension",
"viewHandlers": "View Handlers",
"updateTo": "Update to",
"reinstall": "Reinstall"
"reinstall": "Reinstall",
"extensionPage": "Extension Page"
},
"status": {
"hasUpdate": "New version available",
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@
"togglePlugin": "插件",
"viewHandlers": "查看行为",
"updateTo": "更新到",
"reinstall": "重新安装"
"reinstall": "重新安装",
"extensionPage": "插件扩展页面"
},
"status": {
"hasUpdate": "有新版本可用",
Expand Down
59 changes: 59 additions & 0 deletions dashboard/src/views/ExtensionPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ const changelogDialog = reactive({
repoUrl: null,
});

// 扩展页面对话框
const extensionPageDialog = reactive({
show: false,
html: "",
title: "",
});

// 新增变量支持列表视图
// 从 localStorage 恢复显示模式,默认为 false(卡片视图)
const getInitialListViewMode = () => {
Expand Down Expand Up @@ -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")) {
Expand Down Expand Up @@ -1567,6 +1602,7 @@ watch(activeTab, (newTab) => {
@view-handlers="showPluginInfo(extension)"
@view-readme="viewReadme(extension)"
@view-changelog="viewChangelog(extension)"
@view-extension-page="openExtensionPage(extension)"
>
</ExtensionCard>
</v-col>
Expand Down Expand Up @@ -2658,6 +2694,29 @@ watch(activeTab, (newTab) => {
</v-card-actions>
</v-card>
</v-dialog>

<!-- 扩展页面对话框 -->
<v-dialog v-model="extensionPageDialog.show" fullscreen>
<v-card>
<v-toolbar color="primary" dark>
<v-btn icon @click="extensionPageDialog.show = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>{{ extensionPageDialog.title }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn variant="text" @click="extensionPageDialog.show = false">
关闭
</v-btn>
</v-toolbar>
<v-card-text style="padding: 0; height: calc(100vh - 64px);">
<iframe
:srcdoc="extensionPageDialog.html"
style="width: 100%; height: 100%; border: none;"
sandbox="allow-scripts allow-forms allow-same-origin"
></iframe>
</v-card-text>
</v-card>
</v-dialog>
</template>

<style scoped>
Expand Down