mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
feat: supports plugin to register custom pages (webui) (#5940)
* feat(plugin): add webui metadata schema for plugins * feat(dashboard): serve plugin webui with scoped asset tokens * feat(dashboard): add plugin webui page and extension entry actions * test(dashboard): cover plugin webui auth and asset routing * fix(dashboard): use aiofiles for non-blocking plugin webui assets * fix(dashboard): streamline JWT extraction and validation for plugin webui paths * fix(dashboard): harden plugin webui bridge and auth cookie security * fix(dashboard): restore plugin webui bridge under sandbox iframe * refactor(dashboard): apply plugin webui review improvements * docs: 补充插件 WebUI 开发指南 * fix(plugin-webui): 统一 WebUI title 契约并修复桥接行为 * docs: 更新插件 WebUI 开发指南 * fix * feat: Introduce Plugin Pages feature - Added support for plugins to expose Dashboard pages via a `pages/` directory. - Updated `PluginDetailPage.vue` to include a button for opening plugin pages. - Refactored `useExtensionPage.js` to remove the deprecated `openPluginWebUI` function. - Updated documentation to replace references from "Plugin WebUI" to "Plugin Pages". - Created new documentation for Plugin Pages detailing structure, examples, and API usage. - Removed the old Plugin WebUI documentation. - Updated tests to reflect changes from Plugin WebUI to Plugin Pages, ensuring proper functionality and security checks. * feat: 增强插件页面功能,添加返回按钮逻辑并更新测试用例 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Soulter <905617992@qq.com> Co-authored-by: Weilong Liao <37870767+Soulter@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -191,6 +191,7 @@ export default defineConfig({
|
||||
{ text: "接收消息事件", link: "/guides/listen-message-event" },
|
||||
{ text: "发送消息", link: "/guides/send-message" },
|
||||
{ text: "插件配置", link: "/guides/plugin-config" },
|
||||
{ text: "插件 Pages", link: "/guides/plugin-pages" },
|
||||
{ text: "插件国际化", link: "/guides/plugin-i18n" },
|
||||
{ text: "调用 AI", link: "/guides/ai" },
|
||||
{ text: "存储", link: "/guides/storage" },
|
||||
@@ -434,6 +435,7 @@ export default defineConfig({
|
||||
{ text: "Listen to Message Events", link: "/guides/listen-message-event" },
|
||||
{ text: "Send Messages", link: "/guides/send-message" },
|
||||
{ text: "Plugin Configuration", link: "/guides/plugin-config" },
|
||||
{ text: "Plugin Pages", link: "/guides/plugin-pages" },
|
||||
{ text: "Plugin Internationalization", link: "/guides/plugin-i18n" },
|
||||
{ text: "AI", link: "/guides/ai" },
|
||||
{ text: "Storage", link: "/guides/storage" },
|
||||
|
||||
151
docs/en/dev/star/guides/plugin-pages.md
Normal file
151
docs/en/dev/star/guides/plugin-pages.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Plugin Pages
|
||||
|
||||
AstrBot lets a plugin expose Dashboard pages by placing static assets under `pages/`. Each direct child directory is one Page:
|
||||
|
||||
```text
|
||||
astrbot_plugin_page_demo/
|
||||
├─ main.py
|
||||
└─ pages/
|
||||
├─ bridge-demo/
|
||||
│ ├─ index.html
|
||||
│ ├─ app.js
|
||||
│ ├─ style.css
|
||||
│ └─ assets/
|
||||
│ └─ logo.svg
|
||||
└─ settings/
|
||||
└─ index.html
|
||||
```
|
||||
|
||||
AstrBot scans `pages/<page_name>/index.html`; directories without `index.html` are ignored.
|
||||
|
||||
If you only need a few editable settings, prefer [`_conf_schema.json`](./plugin-config.md). Plugin Pages are more suitable for complex forms, dashboards, logs, file transfer, SSE, and custom interaction flows.
|
||||
|
||||
## Minimal Frontend Example
|
||||
|
||||
`pages/bridge-demo/index.html`
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Plugin Page Demo</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<button id="ping">Ping</button>
|
||||
<pre id="output"></pre>
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
`pages/bridge-demo/app.js`
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
const output = document.getElementById("output");
|
||||
|
||||
const context = await bridge.ready();
|
||||
output.textContent = JSON.stringify(context, null, 2);
|
||||
|
||||
document.getElementById("ping").addEventListener("click", async () => {
|
||||
const result = await bridge.apiGet("ping");
|
||||
output.textContent = JSON.stringify(result, null, 2);
|
||||
});
|
||||
```
|
||||
|
||||
You do not need to import the bridge SDK manually. AstrBot injects `/api/plugin/page/bridge-sdk.js` into returned HTML.
|
||||
|
||||
## Register Backend APIs
|
||||
|
||||
When the frontend calls `bridge.apiGet("ping")`, the Dashboard forwards it to:
|
||||
|
||||
```text
|
||||
/api/plug/<plugin_name>/ping
|
||||
```
|
||||
|
||||
The registered Web API route must include the plugin name as a prefix:
|
||||
|
||||
```python
|
||||
from quart import jsonify
|
||||
from astrbot.api.star import Context, Star
|
||||
|
||||
PLUGIN_NAME = "astrbot_plugin_page_demo"
|
||||
|
||||
|
||||
class MyPlugin(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
context.register_web_api(
|
||||
f"/{PLUGIN_NAME}/ping",
|
||||
self.page_ping,
|
||||
["GET"],
|
||||
"Page ping",
|
||||
)
|
||||
|
||||
async def page_ping(self):
|
||||
return jsonify({"message": "pong"})
|
||||
```
|
||||
|
||||
## Bridge API
|
||||
|
||||
Inside a plugin Page, use `window.AstrBotPluginPage` directly:
|
||||
|
||||
- `ready()`: Wait until the bridge is ready and return the context
|
||||
- `getContext()`: Read the current context
|
||||
- `apiGet(endpoint, params)`: Send a GET request
|
||||
- `apiPost(endpoint, body)`: Send a POST request
|
||||
- `upload(endpoint, file)`: Upload one file as `multipart/form-data`
|
||||
- `download(endpoint, params, filename)`: Download a backend response
|
||||
- `subscribeSSE(endpoint, handlers, params)`: Subscribe to SSE
|
||||
- `unsubscribeSSE(subscriptionId)`: Cancel an SSE subscription
|
||||
|
||||
The current `ready()` context looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"pluginName": "astrbot_plugin_page_demo",
|
||||
"displayName": "Plugin Page Demo"
|
||||
}
|
||||
```
|
||||
|
||||
`endpoint` must be a plugin-local path. It must not be empty, contain `\`, contain a URL scheme, contain query strings or fragments, or contain `.` / `..` path segments.
|
||||
|
||||
## Asset Path Rules
|
||||
|
||||
AstrBot rewrites relative asset URLs and appends a short-lived `asset_token`. Write normal relative paths and do not hardcode `/api/plugin/page/content/...` yourself.
|
||||
|
||||
AstrBot rewrites:
|
||||
|
||||
- HTML `src` and `href`
|
||||
- CSS `url(...)`
|
||||
- JavaScript `import`
|
||||
- JavaScript `export ... from`
|
||||
- JavaScript dynamic `import()`
|
||||
|
||||
Keep static assets on relative paths such as `./style.css` and `./assets/logo.svg`. Do not manually append `asset_token`, and do not rely on `..` to escape the Page root directory.
|
||||
|
||||
If you build a SPA, prefer hash routing. The static asset server resolves real file paths; with history routing, refreshing a page requires an actual file to exist at that path.
|
||||
|
||||
## Security Constraints
|
||||
|
||||
Plugin Pages run inside a restricted iframe:
|
||||
|
||||
```text
|
||||
allow-scripts allow-forms allow-downloads
|
||||
```
|
||||
|
||||
The page cannot directly access Dashboard cookies, LocalStorage, or same-origin DOM, and it cannot bypass the bridge to reuse Dashboard auth directly.
|
||||
|
||||
AstrBot also adds security headers to asset responses, including:
|
||||
|
||||
- `X-Frame-Options: SAMEORIGIN`
|
||||
- `Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'`
|
||||
- `Cache-Control: no-store`
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- Reload the plugin after adding or removing a Page directory
|
||||
- For most edits under `pages/<page_name>/`, refreshing the Page is enough
|
||||
- If a Page does not appear, check that `pages/<page_name>/index.html` exists and the plugin is enabled
|
||||
151
docs/zh/dev/star/guides/plugin-pages.md
Normal file
151
docs/zh/dev/star/guides/plugin-pages.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 插件 Pages
|
||||
|
||||
AstrBot 支持插件通过 `pages/` 目录暴露 Dashboard 页面。`pages/` 下的每个一级子目录都是一个独立 Page:
|
||||
|
||||
```text
|
||||
astrbot_plugin_page_demo/
|
||||
├─ main.py
|
||||
└─ pages/
|
||||
├─ bridge-demo/
|
||||
│ ├─ index.html
|
||||
│ ├─ app.js
|
||||
│ ├─ style.css
|
||||
│ └─ assets/
|
||||
│ └─ logo.svg
|
||||
└─ settings/
|
||||
└─ index.html
|
||||
```
|
||||
|
||||
AstrBot 会扫描 `pages/<page_name>/index.html`;没有 `index.html` 的目录会被忽略。
|
||||
|
||||
如果只是让用户填写几个配置项,优先使用 [`_conf_schema.json`](./plugin-config.md)。插件 Pages 更适合复杂表单、Dashboard、日志、文件上传下载、SSE 和自定义交互流程。
|
||||
|
||||
## 最小前端示例
|
||||
|
||||
`pages/bridge-demo/index.html`
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Plugin Page Demo</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<button id="ping">Ping</button>
|
||||
<pre id="output"></pre>
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
`pages/bridge-demo/app.js`
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
const output = document.getElementById("output");
|
||||
|
||||
const context = await bridge.ready();
|
||||
output.textContent = JSON.stringify(context, null, 2);
|
||||
|
||||
document.getElementById("ping").addEventListener("click", async () => {
|
||||
const result = await bridge.apiGet("ping");
|
||||
output.textContent = JSON.stringify(result, null, 2);
|
||||
});
|
||||
```
|
||||
|
||||
这里不需要手动引入 bridge SDK。AstrBot 会在返回的 HTML 里自动插入 `/api/plugin/page/bridge-sdk.js`。
|
||||
|
||||
## 注册后端 API
|
||||
|
||||
前端调用 `bridge.apiGet("ping")` 时,Dashboard 会转发到:
|
||||
|
||||
```text
|
||||
/api/plug/<plugin_name>/ping
|
||||
```
|
||||
|
||||
因此注册 Web API 时,路由必须带上插件名作为前缀:
|
||||
|
||||
```python
|
||||
from quart import jsonify
|
||||
from astrbot.api.star import Context, Star
|
||||
|
||||
PLUGIN_NAME = "astrbot_plugin_page_demo"
|
||||
|
||||
|
||||
class MyPlugin(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
context.register_web_api(
|
||||
f"/{PLUGIN_NAME}/ping",
|
||||
self.page_ping,
|
||||
["GET"],
|
||||
"Page ping",
|
||||
)
|
||||
|
||||
async def page_ping(self):
|
||||
return jsonify({"message": "pong"})
|
||||
```
|
||||
|
||||
## Bridge API
|
||||
|
||||
插件 Page 中可直接使用 `window.AstrBotPluginPage`:
|
||||
|
||||
- `ready()`: 等待 bridge 就绪并返回上下文
|
||||
- `getContext()`: 读取当前上下文
|
||||
- `apiGet(endpoint, params)`: 发送 GET 请求
|
||||
- `apiPost(endpoint, body)`: 发送 POST 请求
|
||||
- `upload(endpoint, file)`: 以 `multipart/form-data` 上传单个文件
|
||||
- `download(endpoint, params, filename)`: 下载后端响应
|
||||
- `subscribeSSE(endpoint, handlers, params)`: 订阅 SSE
|
||||
- `unsubscribeSSE(subscriptionId)`: 取消 SSE 订阅
|
||||
|
||||
当前 `ready()` 上下文类似:
|
||||
|
||||
```json
|
||||
{
|
||||
"pluginName": "astrbot_plugin_page_demo",
|
||||
"displayName": "Plugin Page Demo"
|
||||
}
|
||||
```
|
||||
|
||||
`endpoint` 必须是插件内相对路径,不能为空,不能包含 `\`、URL scheme、query、hash,也不能包含 `.` 或 `..` 路径片段。
|
||||
|
||||
## 静态资源路径规则
|
||||
|
||||
AstrBot 会重写相对资源路径,并自动补上短期 `asset_token`。你只需要正常写相对路径,不要自己拼接 `/api/plugin/page/content/...`。
|
||||
|
||||
AstrBot 会重写:
|
||||
|
||||
- HTML `src` 和 `href`
|
||||
- CSS `url(...)`
|
||||
- JavaScript `import`
|
||||
- JavaScript `export ... from`
|
||||
- JavaScript 动态 `import()`
|
||||
|
||||
建议把静态资源写成 `./style.css`、`./assets/logo.svg` 这类相对路径。不要手动追加 `asset_token`,也不要依赖 `..` 逃逸 Page 根目录。
|
||||
|
||||
如果你构建 SPA,建议使用 hash routing。静态资源服务按真实文件路径解析;history routing 刷新页面时需要对应路径上真的存在文件。
|
||||
|
||||
## 安全约束
|
||||
|
||||
插件 Pages 运行在受限 iframe 中:
|
||||
|
||||
```text
|
||||
allow-scripts allow-forms allow-downloads
|
||||
```
|
||||
|
||||
Page 不能直接访问 Dashboard cookies、LocalStorage 或同源 DOM,也不能绕过 bridge 复用 Dashboard auth。
|
||||
|
||||
AstrBot 还会给资源响应添加安全头,包括:
|
||||
|
||||
- `X-Frame-Options: SAMEORIGIN`
|
||||
- `Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'`
|
||||
- `Cache-Control: no-store`
|
||||
|
||||
## 调试建议
|
||||
|
||||
- 新增或删除 Page 目录后重载插件
|
||||
- 修改 `pages/<page_name>/` 下的大多数静态资源后,刷新 Page 即可
|
||||
- 如果 Page 没出现,检查 `pages/<page_name>/index.html` 是否存在,以及插件是否启用
|
||||
Reference in New Issue
Block a user