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
4 changes: 3 additions & 1 deletion astrbot/dashboard/services/plugin_page_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,9 @@ async def serialize_plugin_page(
page_data = {
"name": page.name,
"title": page.title,
"i18n_key": f"pages.{page.name}",
# "views" is the canonical namespace; the dashboard resolves
# "views.<name>.*" first and falls back to "pages.<name>.*".
"i18n_key": f"views.{page.name}",
}
if include_content_path and asset_token:
page_data["content_path"] = (
Expand Down
3 changes: 3 additions & 0 deletions changelogs/unreleased.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Unreleased

- WebUI: Plugin detail pages and the `pageTitle` sent to plugin view iframes now resolve the canonical `views.<view>.*` i18n namespace before falling back to the legacy `pages.<view>.*`, matching how the sidebar already resolves view titles.
- WebUI:插件详情页以及发送给插件视图 iframe 的 `pageTitle`,现在会先解析正式的 `views.<view>.*` 命名空间,再回退到旧的 `pages.<view>.*`,与侧边栏的解析方式保持一致。
32 changes: 23 additions & 9 deletions dashboard/src/utils/pluginI18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,23 @@ export function resolvePluginI18n(i18n, locale, key, fallback = "") {
return value === undefined || value === null ? fallback : value;
}

function getPluginPageI18nBase(page) {
// "views" is the canonical i18n namespace for plugin views; "pages" stays as a
// compatibility alias so plugins written before the rename keep resolving.
const PLUGIN_VIEW_I18N_NAMESPACES = ["views", "pages"];

function getPluginViewName(page) {
if (page && typeof page === "object") {
if (typeof page.i18n_key === "string" && page.i18n_key.trim()) {
return page.i18n_key.trim();
const i18nKey = typeof page.i18n_key === "string" ? page.i18n_key.trim() : "";
if (i18nKey) {
// Drop whichever namespace prefix the API reported, so both namespaces
// are always tried even when talking to an older backend.
const separator = i18nKey.indexOf(".");
return separator === -1 ? i18nKey : i18nKey.slice(separator + 1);
}
const pageName = page.page_name || page.name;
return pageName ? `pages.${pageName}` : "";
return page.page_name || page.name || "";
}

return page ? `pages.${page}` : "";
return page || "";
}

export function usePluginI18n() {
Expand Down Expand Up @@ -72,11 +79,18 @@ export function usePluginI18n() {
};

const pluginPageText = (plugin, page, attr, fallback = "") => {
const base = getPluginPageI18nBase(page);
if (!base || !attr) {
const viewName = getPluginViewName(page);
if (!viewName || !attr) {
return fallback;
}
return resolve(plugin?.i18n, `${base}.${attr}`, fallback);
const localeData = getLocaleData(plugin?.i18n, locale.value);
for (const namespace of PLUGIN_VIEW_I18N_NAMESPACES) {
const value = getByPath(localeData, `${namespace}.${viewName}.${attr}`);
if (value !== undefined && value !== null) {
return value;
}
}
return fallback;
};

const pluginViewTitle = (plugin, page, fallback = "") => {
Expand Down
18 changes: 18 additions & 0 deletions docs/en/dev/star/guides/plugin-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ Use simple directory names for `view_name`, such as `settings` or `bridge-demo`.

Users open views from the plugin detail page in the WebUI.

### Migrating from Pages to Views

Earlier versions called plugin views Pages. `views` is now canonical, with `pages` kept only as a compatibility alias:

| Item | Current | Compatibility alias |
| --- | --- | --- |
| View directory | `views/<view_name>/` | `pages/<view_name>/` |
| i18n namespace | `views.<view_name>.*` | `pages.<view_name>.*` |
| Bridge global | `window.AstrBotPluginView` | `window.AstrBotPluginPage` |
| v1 API routes | `/plugins/views`, `/plugins/{plugin_id}/views/{view_name}` | `/plugins/pages` and the matching aliases |

> [!WARNING]
> When both `views/` and `pages/` exist, AstrBot only scans `views/` and every view under `pages/` is ignored. Move the whole directory in one step instead of copying the contents into `views/` while keeping the original `pages/` around.

A `views:` declaration (`pages:` in older plugins) in `metadata.yaml` only feeds plugin metadata. View discovery is driven purely by directory scanning, so declaring `views:` never makes a view under another directory work.

Untouched plugins keep working as before; new and maintained plugins should use `views`.

## Development Flow

1. Create `views/<view_name>/index.html` in the plugin directory.
Expand Down
18 changes: 18 additions & 0 deletions docs/zh/dev/star/guides/plugin-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ astrbot_plugin_view_demo/

用户可以在 WebUI 的插件页点击插件卡片进入插件详情页,然后打开插件声明的可视化视图。

### 从 Pages 迁移到 Views

早期版本把插件可视化视图称为 Pages。现在统一以 `views` 为准,`pages` 仅作为兼容别名保留:

| 项目 | 现在的写法 | 兼容别名 |
| --- | --- | --- |
| 视图目录 | `views/<view_name>/` | `pages/<view_name>/` |
| i18n 命名空间 | `views.<view_name>.*` | `pages.<view_name>.*` |
| bridge 全局对象 | `window.AstrBotPluginView` | `window.AstrBotPluginPage` |
| v1 API 路由 | `/plugins/views`、`/plugins/{plugin_id}/views/{view_name}` | `/plugins/pages` 等同名别名 |

> [!WARNING]
> `views/` 和 `pages/` 同时存在时,AstrBot 只扫描 `views/`,`pages/` 下的视图会被完全忽略。迁移时请一次性移动整个目录,不要先把内容复制一份到 `views/` 而保留原来的 `pages/`。

`metadata.yaml` 中的 `views:`(旧名 `pages:`)只影响插件元数据,视图发现完全以目录扫描为准;把文件留在别的目录时,声明 `views:` 并不会让它生效。

未改动的旧插件可以继续工作;新插件和维护中的插件建议统一使用 `views`。

## 开发流程

1. 在插件目录下创建 `views/<view_name>/index.html`。
Expand Down
4 changes: 2 additions & 2 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1908,7 +1908,7 @@ async def test_plugin_detail_includes_scanned_page_component(
"name": PLUGIN_PAGE_DEMO_PAGE_NAME,
"title": PLUGIN_PAGE_DEMO_PAGE_NAME,
"page_name": PLUGIN_PAGE_DEMO_PAGE_NAME,
"i18n_key": f"pages.{PLUGIN_PAGE_DEMO_PAGE_NAME}",
"i18n_key": f"views.{PLUGIN_PAGE_DEMO_PAGE_NAME}",
"description": "Plugin Page entry",
"plugin_name": PLUGIN_PAGE_DEMO_NAME,
"plugin_marketplace_name": PLUGIN_PAGE_DEMO_NAME.replace("_", "-"),
Expand All @@ -1935,7 +1935,7 @@ async def test_plugin_page_entry_returns_signed_content_path(
assert data["status"] == "ok"
assert data["data"]["name"] == PLUGIN_PAGE_DEMO_PAGE_NAME
assert data["data"]["title"] == PLUGIN_PAGE_DEMO_PAGE_NAME
assert data["data"]["i18n_key"] == f"pages.{PLUGIN_PAGE_DEMO_PAGE_NAME}"
assert data["data"]["i18n_key"] == f"views.{PLUGIN_PAGE_DEMO_PAGE_NAME}"
assert data["data"]["content_path"].startswith(
f"/api/v1/plugins/{PLUGIN_PAGE_DEMO_NAME}/views/{PLUGIN_PAGE_DEMO_PAGE_NAME}/_t/"
)
Expand Down
Loading