Skip to content

Commit 23d70db

Browse files
lxfightlxfight
andauthored
feat(webui): add direct access button on plugin cards and improve embedded page height (#8369)
* feat(plugin): add pages field to plugin list API response Include discovered page names for each plugin in the /api/plugin/get response, so the frontend can determine whether a plugin has a WebUI without an extra detail request. * feat(webui): add direct WebUI access button on plugin cards - Add "open-webui" button on plugin cards when plugin has pages - Navigate to plugin's first page directly from the card - Adjust PluginPagePage iframe height for better UX - Add i18n labels (zh-CN/en-US/ru-RU) * perf(plugin): use asyncio.gather for concurrent page discovery Replace sequential await loop with concurrent processing to avoid blocking on disk I/O when discovering plugin pages. * fix(webui): disable open plugin UI button when plugin is deactivated Prevent navigation to a disabled plugin's WebUI page which would result in an error. * test: update plugin API tests to match pages field in list response - test_plugin_get_excludes_scanned_pages: expect pages field now present - test_plugins: use name-based lookup instead of exact count assertion --------- Co-authored-by: lxfight <lxfight@192.168.5.50>
1 parent ae44163 commit 23d70db

8 files changed

Lines changed: 68 additions & 9 deletions

File tree

astrbot/dashboard/routes/plugin.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,12 +1262,23 @@ def _get_plugin_installed_at(self, plugin) -> str | None:
12621262
async def get_plugins(self):
12631263
_plugin_resp = []
12641264
plugin_name = request.args.get("name")
1265-
for plugin in self.plugin_manager.context.get_all_stars():
1266-
if plugin_name and plugin.name != plugin_name:
1267-
continue
1265+
1266+
plugins = [
1267+
p
1268+
for p in self.plugin_manager.context.get_all_stars()
1269+
if not (plugin_name and p.name != plugin_name)
1270+
]
1271+
1272+
async def process_plugin(plugin):
12681273
logo_url = None
12691274
if plugin.logo_path:
12701275
logo_url = await self.get_plugin_logo_token(plugin.logo_path)
1276+
pages = await self._discover_plugin_pages(plugin)
1277+
return plugin, logo_url, pages
1278+
1279+
results = await asyncio.gather(*(process_plugin(p) for p in plugins))
1280+
1281+
for plugin, logo_url, pages in results:
12711282
_t = {
12721283
"name": plugin.name,
12731284
"repo": "" if plugin.repo is None else plugin.repo,
@@ -1283,6 +1294,7 @@ async def get_plugins(self):
12831294
"astrbot_version": plugin.astrbot_version,
12841295
"installed_at": self._get_plugin_installed_at(plugin),
12851296
"i18n": plugin.i18n,
1297+
"pages": [p.name for p in pages],
12861298
}
12871299
# 检查是否为全空的幽灵插件
12881300
if not any(
@@ -1296,6 +1308,7 @@ async def get_plugins(self):
12961308
):
12971309
continue
12981310
_plugin_resp.append(_t)
1311+
_plugin_resp.append(_t)
12991312
return (
13001313
Response()
13011314
.ok(_plugin_resp, message=self.plugin_manager.failed_plugin_info)

dashboard/src/components/shared/ExtensionCard.vue

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,13 @@ const emit = defineEmits([
3838
"view-readme",
3939
"view-changelog",
4040
"toggle-pin",
41+
"open-webui",
4142
]);
4243
44+
const hasPages = computed(() => {
45+
return Array.isArray(props.extension?.pages) && props.extension.pages.length > 0;
46+
});
47+
4348
const showUninstallDialog = ref(false);
4449
4550
const attrs = useAttrs();
@@ -130,6 +135,10 @@ const togglePin = () => {
130135
emit("toggle-pin", props.extension);
131136
};
132137
138+
const openWebui = () => {
139+
emit("open-webui", props.extension);
140+
};
141+
133142
</script>
134143

135144
<template>
@@ -322,6 +331,20 @@ const togglePin = () => {
322331
</template>
323332
</v-tooltip>
324333

334+
<v-tooltip v-if="hasPages" location="top" :text="tm('buttons.openWebui')">
335+
<template v-slot:activator="{ props: actionProps }">
336+
<v-btn
337+
v-bind="actionProps"
338+
icon="mdi-monitor-dashboard"
339+
size="small"
340+
variant="tonal"
341+
color="primary"
342+
:disabled="!extension.activated"
343+
@click.stop="openWebui"
344+
></v-btn>
345+
</template>
346+
</v-tooltip>
347+
325348
<v-tooltip location="top" :text="tm('card.actions.pluginConfig')">
326349
<template v-slot:activator="{ props: actionProps }">
327350
<v-btn

dashboard/src/i18n/locales/en-US/features/extension.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"viewRepo": "Repository",
4646
"openPages": "Pages",
4747
"openPage": "Open",
48+
"openWebui": "Open Plugin UI",
4849
"close": "Close",
4950
"save": "Save",
5051
"saveAndClose": "Save and Close",

dashboard/src/i18n/locales/ru-RU/features/extension.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"viewRepo": "Репозиторий",
4646
"openPages": "Pages",
4747
"openPage": "Открыть",
48+
"openWebui": "Открыть UI плагина",
4849
"close": "Закрыть",
4950
"save": "Сохранить",
5051
"saveAndClose": "Сохранить и закрыть",

dashboard/src/i18n/locales/zh-CN/features/extension.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"viewRepo": "仓库",
4646
"openPages": "Pages",
4747
"openPage": "打开",
48+
"openWebui": "打开插件UI界面",
4849
"close": "关闭",
4950
"save": "保存",
5051
"saveAndClose": "保存并关闭",

dashboard/src/views/PluginPagePage.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,13 +513,13 @@ watch(locale, () => {
513513
514514
.plugin-page-frame {
515515
width: 100%;
516-
min-height: calc(100vh - 220px);
516+
min-height: calc(100vh - 140px);
517517
border: 0;
518518
background: transparent;
519519
}
520520
521521
.plugin-page-state {
522-
min-height: calc(100vh - 220px);
522+
min-height: calc(100vh - 140px);
523523
display: flex;
524524
align-items: center;
525525
justify-content: center;

dashboard/src/views/extension/InstalledPluginsTab.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ const openPluginDetail = (extension) => {
152152
});
153153
};
154154
155+
const openPluginWebui = (extension) => {
156+
const pages = extension?.pages;
157+
if (!Array.isArray(pages) || pages.length === 0 || !extension?.name) return;
158+
router.push({
159+
name: "PluginPage",
160+
params: {
161+
pluginName: extension.name,
162+
pageName: pages[0],
163+
},
164+
});
165+
};
166+
155167
const pinnedExtensionNames = ref(readPinnedExtensions());
156168
157169
const pinnedExtensionOrder = computed(() => {
@@ -344,6 +356,7 @@ const togglePinnedExtension = (extension) => {
344356
@view-handlers="showPluginInfo(extension)"
345357
@view-readme="viewReadme(extension)"
346358
@view-changelog="viewChangelog(extension)"
359+
@open-webui="openPluginWebui(extension)"
347360
>
348361
</ExtensionCard>
349362
</v-col>

tests/test_dashboard.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,9 @@ async def test_plugin_get_excludes_scanned_pages(
855855
)
856856
assert plugin["activated"] is True
857857
assert "page" not in plugin
858-
assert "pages" not in plugin
858+
assert "pages" in plugin
859+
assert isinstance(plugin["pages"], list)
860+
assert PLUGIN_PAGE_DEMO_PAGE_NAME in plugin["pages"]
859861

860862

861863
@pytest.mark.asyncio
@@ -1433,9 +1435,14 @@ async def test_plugins(
14331435
assert response.status_code == 200
14341436
data = await response.get_json()
14351437
assert data["status"] == "ok"
1436-
assert len(data["data"]) == 1
1437-
assert "components" not in data["data"][0]
1438-
installed_at = data["data"][0]["installed_at"]
1438+
assert len(data["data"]) >= 1
1439+
target = next(
1440+
(item for item in data["data"] if item["name"] == test_plugin_name),
1441+
None,
1442+
)
1443+
assert target is not None
1444+
assert "components" not in target
1445+
installed_at = target["installed_at"]
14391446
assert installed_at is not None
14401447
datetime.fromisoformat(installed_at)
14411448

0 commit comments

Comments
 (0)