-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
zh dev star guides plugin pages
插件 Pages 允许插件在 AstrBot WebUI 中提供自己的页面。页面文件放在插件目录的 pages/ 下,由 Dashboard 以受限 iframe 的方式加载;页面里的脚本通过 window.AstrBotPluginPage bridge 和 Dashboard 通信,再由 Dashboard 转发到插件注册的后端 Web API。
如果只是让用户填写少量配置项,优先使用 _conf_schema.json。Pages 更适合复杂表单、运行状态面板、日志查看、文件上传下载、SSE 实时流、图表和其他需要自定义交互的场景。
pages/ 下的每个一级子目录是一个独立 Page。AstrBot 只扫描 pages/<page_name>/index.html,没有 index.html 的目录会被忽略。
astrbot_plugin_page_demo/
├─ main.py
└─ pages/
├─ bridge-demo/
│ ├─ index.html
│ ├─ app.js
│ ├─ style.css
│ └─ assets/
│ └─ logo.svg
└─ settings/
└─ index.html
page_name 应使用简单目录名,例如 settings、bridge-demo。不要使用空目录名、.、..、以 . 开头的目录名,或包含 /、\ 的名称。
用户可以在 WebUI 的插件页点击插件卡片进入插件详情页,然后打开插件声明的 Pages。
- 在插件目录下创建
pages/<page_name>/index.html。 - 在 Page 中通过
window.AstrBotPluginPagebridge 调用后端能力。 - 在
main.py中使用context.register_web_api()注册插件后端 API。 - 后端 handler 使用
astrbot.api.web读取请求并返回响应。 - 新增或删除 Page 目录后重载插件;修改静态资源通常刷新 Page 即可。
插件后端推荐使用 astrbot.api.web,不要把 FastAPI、Starlette 或 Quart 的原始请求对象作为插件公共 API 暴露给自己的业务代码。
from astrbot.api.star import Context, Star
from astrbot.api.web import error_response, json_response, request
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",
)
context.register_web_api(
f"/{PLUGIN_NAME}/settings/save",
self.save_settings,
["POST"],
"Save Page settings",
)
async def page_ping(self):
limit = request.query.get("limit", 20, type=int)
return json_response(
{
"message": "pong",
"limit": limit,
"username": request.username,
}
)
async def save_settings(self):
payload = await request.json(default={})
if not isinstance(payload.get("enabled"), bool):
return error_response("enabled must be a boolean")
return json_response({"saved": True})pages/bridge-demo/index.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
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", { limit: 20 });
output.textContent = JSON.stringify(result, null, 2);
});不需要手动引入 bridge SDK。AstrBot 返回 HTML 时会自动插入 /api/plugin/page/bridge-sdk.js。如果内联脚本必须同步访问 window.AstrBotPluginPage,请把脚本改成外部 module 文件,或在自己的脚本前显式引入:
<script src="/api/plugin/page/bridge-sdk.js"></script>使用 context.register_web_api(route, view_handler, methods, desc) 注册插件 API。
context.register_web_api(
f"/{PLUGIN_NAME}/items/<item_id>",
self.get_item,
["GET"],
"Get item",
)路由需要包含插件名作为前缀。Page 端的 bridge endpoint 不需要包含插件名:
await bridge.apiGet("items/123");Dashboard 会把它转发到:
/api/v1/plugins/extensions/<plugin_name>/items/123
注册路由 /<plugin_name>/items/<item_id> 会匹配该请求,item_id 作为 handler 的关键字参数传入:
async def get_item(self, item_id: str):
return json_response({"item_id": item_id})支持的动态片段:
-
<name>:匹配单个路径片段。 -
<path:name>:匹配后续多级路径。
推荐导入:
from astrbot.api.web import requestrequest 是当前请求的上下文代理,只能在插件 Web API handler 执行期间访问。常用字段和方法:
| API | 说明 |
|---|---|
request.method |
HTTP 方法,例如 GET、POST
|
request.path |
当前 Dashboard API 路径 |
request.plugin_name |
从扩展路径解析出的插件名 |
request.username |
当前 Dashboard 用户名,可能为 None
|
request.headers |
请求头 |
request.cookies |
请求 cookies |
request.content_type |
请求 Content-Type |
request.client_host |
客户端地址 |
request.path_params |
路由动态参数字典 |
request.query |
query 参数,支持 get() 和 getlist()
|
await request.body() |
原始请求体 bytes |
await request.json(default={}) |
JSON 请求体,解析失败返回 default |
await request.form() |
表单字段,不含上传文件 |
await request.files() |
上传文件 |
query 参数使用示例:
limit = request.query.get("limit", 20, type=int)
tags = request.query.getlist("tag")JSON 请求体使用示例:
payload = await request.json(default={})
enabled = bool(payload.get("enabled"))文件上传使用示例:
from pathlib import Path
from astrbot.core.utils.astrbot_path import get_astrbot_plugin_data_path
from astrbot.api.web import PluginUploadFile, error_response, json_response, request
async def import_file(self):
form = await request.form()
files = await request.files()
upload: PluginUploadFile | None = files.get("file")
if not isinstance(upload, PluginUploadFile):
return error_response("missing file")
target_dir = (
Path(get_astrbot_plugin_data_path())
/ (request.plugin_name or "unknown_plugin")
/ "imports"
)
target_dir.mkdir(parents=True, exist_ok=True)
target = target_dir / Path(upload.filename).name
await upload.save(target)
return json_response(
{
"filename": upload.filename,
"content_type": upload.content_type,
"tag": form.get("tag"),
}
)request.form() 和 request.files() 会缓存解析结果,可以在同一个 handler 中各调用一次。
推荐从 astrbot.api.web 导入响应 helper:
from astrbot.api.web import (
error_response,
file_response,
json_response,
stream_response,
)JSON 响应:
return json_response({"saved": True})错误响应:
return error_response("invalid threshold", status_code=400)文件下载响应:
return file_response(
export_path,
filename="export.json",
content_type="application/json",
)SSE 响应:
import json
from astrbot.api.web import stream_response
async def stream_events(self):
async def events():
yield f"data: {json.dumps({'state': 'started'})}\n\n"
yield f"data: {json.dumps({'state': 'done'})}\n\n"
return stream_response(events())直接返回 dict、list、(body, status_code) 或底层 Response 对象仍然可用;文档和新插件推荐优先使用 astrbot.api.web helper,让插件代码和 Dashboard 内部框架解耦。
为了兼容旧插件,通过 context.register_web_api() 注册的 handler 仍会进入 Quart 兼容请求上下文。旧代码可以继续使用:
from quart import jsonify, request新插件和新文档推荐使用:
from astrbot.api.web import json_response, request不要在同一个 handler 中混用两个 request 代理,迁移时按 handler 逐步替换即可。
Page iframe 不能直接访问 Dashboard cookies、LocalStorage 或父页面 DOM。页面脚本必须通过 window.AstrBotPluginPage bridge 调用后端和读取上下文。
const bridge = window.AstrBotPluginPage;ready() 等待父页面发送初始上下文,返回 Promise<context>。页面初始化时应先等待它。
const context = await bridge.ready();上下文通常包含:
{
"pluginName": "astrbot_plugin_page_demo",
"displayName": "Plugin Page Demo",
"pageName": "bridge-demo",
"pageTitle": "Bridge Demo",
"locale": "zh-CN",
"i18n": {},
"isDark": false
}上下文相关 API:
| API | 返回值 | 说明 |
|---|---|---|
ready() |
Promise<context> |
等待 bridge 就绪并返回初始上下文 |
getContext() |
context | null |
同步读取最近一次上下文 |
getLocale() |
string |
当前 WebUI 语言,默认 zh-CN
|
getI18n() |
object |
当前插件 i18n 资源 |
t(key, fallback) |
string |
按点分隔 key 读取翻译,缺失时返回 fallback |
onContext(handler) |
() => void |
监听上下文变化,返回取消监听函数 |
监听语言或主题变化:
function render() {
document.title = bridge.t("pages.bridge-demo.title", "Bridge Demo");
document.getElementById("locale").textContent = bridge.getLocale();
}
await bridge.ready();
render();
const off = bridge.onContext(render);
window.addEventListener("beforeunload", off);apiGet、apiPost、upload、download、subscribeSSE 的 endpoint 都是插件内相对路径,例如 stats、settings/save、files/export。推荐不要以 / 开头;当前 bridge 会为了兼容旧写法去掉开头的 /。
endpoint 不能是空字符串,不能包含 \、URL scheme、query、hash,也不能包含空路径片段、. 或 ..。
不要把 query string 拼进 endpoint:
await bridge.apiGet("stats", { limit: 20 });bridge 对 JSON 类请求的返回值有一个兼容规则:
- 如果后端返回
{ "status": "ok", "data": value },Promise resolve 为value。 - 如果后端返回普通 JSON,例如
{ "message": "pong" },Promise resolve 为完整 JSON。 - 如果后端返回
{ "status": "error", "message": "..." },或 HTTP 请求失败,Promise reject 为Error。
因此 Page-only API 推荐直接返回业务 JSON:
return json_response({"message": "pong"})需要表达错误时使用:
return error_response("missing file", status_code=400)Page 端统一捕获错误:
try {
await bridge.apiPost("settings/save", { enabled: true });
} catch (error) {
console.error(error.message);
}发送 GET 请求。params 会作为 query 参数传递。
const stats = await bridge.apiGet("stats", { limit: 20, tag: "today" });后端读取:
async def stats(self):
limit = request.query.get("limit", 20, type=int)
tag = request.query.get("tag")
return json_response({"limit": limit, "tag": tag})发送 POST JSON 请求。
const result = await bridge.apiPost("settings/save", {
enabled: true,
threshold: 0.8,
});后端读取:
async def save_settings(self):
payload = await request.json(default={})
return json_response({"saved": True, "enabled": payload.get("enabled")})以 multipart/form-data 上传单个文件,字段名固定为 file。
const input = document.querySelector("input[type=file]");
const file = input.files[0];
const result = await bridge.upload("files/import", file);后端读取:
from astrbot.api.web import PluginUploadFile, error_response, json_response, request
async def import_file(self):
files = await request.files()
upload: PluginUploadFile | None = files.get("file")
if not isinstance(upload, PluginUploadFile):
return error_response("missing file", status_code=400)
return json_response({"filename": upload.filename})如果还需要普通字段,请单独使用 apiPost 传配置,或在后端根据 query 参数区分导入行为。当前 bridge 的 upload() 只发送一个文件。
请求插件后端文件接口并触发浏览器下载。params 会作为 query 参数发送;filename 可选,缺省时 bridge 会尝试从响应头读取文件名。
await bridge.download("files/export", { format: "json" }, "export.json");后端返回文件:
async def export_file(self):
fmt = request.query.get("format", "json")
return file_response(
export_path,
filename=f"export.{fmt}",
content_type="application/json",
)download() resolve 为:
{ "filename": "export.json" }订阅插件后端 SSE,返回 Promise<subscriptionId>。handlers 可以包含 onOpen、onMessage、onError。
const subscriptionId = await bridge.subscribeSSE(
"events",
{
onOpen() {
console.log("SSE opened");
},
onMessage(event) {
console.log(event.raw, event.parsed, event.lastEventId);
},
onError() {
console.warn("SSE error");
},
},
{ topic: "logs" },
);event.raw 是原始字符串;如果内容是 JSON 字符串,event.parsed 会自动解析,否则等于原始字符串。event.eventType 对应 SSE 的 event: 字段,未设置时为 message。
后端必须返回 text/event-stream:
async def events(self):
async def stream():
yield 'data: {"message": "ready"}\n\n'
return stream_response(stream())取消订阅:
await bridge.unsubscribeSSE(subscriptionId);页面卸载时建议清理:
window.addEventListener("beforeunload", () => {
bridge.unsubscribeSSE(subscriptionId);
});插件 Pages 复用插件 i18n 资源文件。给 .astrbot-plugin/i18n/<locale>.json 增加 pages.<page_name>:
{
"pages": {
"bridge-demo": {
"title": "Bridge 演示页",
"description": "演示插件页面如何读取 WebUI 语言和翻译资源。",
"heading": "插件页面",
"refresh": "重新渲染"
}
}
}title 用于 WebUI 外壳标题和插件详情页的 Page 组件名称;description 用于插件详情页的 Page 组件描述。Page 内部使用 bridge.t() 渲染文案,并通过 onContext() 响应语言切换。
function render() {
document.title = bridge.t("pages.bridge-demo.title", "Bridge Demo");
document.getElementById("heading").textContent = bridge.t(
"pages.bridge-demo.heading",
"Plugin Page",
);
}
await bridge.ready();
render();
bridge.onContext(render);AstrBot 会把当前主题同步给插件 Page。bridge SDK 会维护 <html> 的 data-theme 属性:
- 亮色模式:
<html data-theme="light"> - 暗色模式:
<html data-theme="dark">
选择“跟随系统”时,Page 收到的值仍然是 light 或 dark。
推荐使用 CSS 变量:
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #1a1a1a;
--text: #e0e0e0;
}
body {
background: var(--bg);
color: var(--text);
}服务端返回 HTML 时会预先注入 data-theme,减少初始闪烁。需要在 JavaScript 中响应主题变化时,读取 bridge.getContext()?.isDark 并监听 onContext()。
正常使用相对路径即可:
<link rel="stylesheet" href="./style.css" />
<script type="module" src="./app.js"></script>
<img src="./assets/logo.svg" alt="" />AstrBot 会重写相对资源路径并追加短期 asset_token。不要手动拼接 /api/plugin/page/content/...,不要自行追加 asset_token,也不要依赖 .. 逃逸 Page 根目录。
会被重写的资源引用包括:
- HTML
src和href - CSS
url(...) - JavaScript
import - JavaScript
export ... from - JavaScript 动态
import()
如果构建 SPA,建议使用 hash routing。静态资源服务按真实文件路径解析;history routing 刷新页面时需要对应路径上真的存在文件。
插件 Pages 运行在受限 iframe 中:
allow-scripts allow-forms allow-downloads
Page 不能直接访问 Dashboard cookies、LocalStorage 或父页面 DOM,也不能绕过 bridge 复用 Dashboard auth。所有需要 Dashboard 身份的操作都应该走 bridge。
资源响应会带上安全头,包括:
X-Frame-Options: SAMEORIGINContent-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'Cache-Control: no-storeX-Content-Type-Options: nosniff
后端 handler 仍然要验证输入。不要信任 Page 传来的路径、文件名、格式或数值范围;文件落盘时应使用安全目录,并对文件名做白名单或重新命名。
- Page 没出现:检查
pages/<page_name>/index.html是否存在、插件是否启用、插件详情页是否已刷新。 - bridge 不存在:确认脚本在 bridge SDK 注入之后运行;推荐使用外部
type="module"脚本。 - API 未匹配:确认注册路由包含插件名前缀,例如
/{PLUGIN_NAME}/stats,而 Page 端 endpoint 是stats。 - query 或 JSON 为空:GET 参数放到
apiGet(endpoint, params),POST JSON 放到apiPost(endpoint, body)。 - 文件上传为空:
upload()字段名固定为file,后端用(await request.files()).get("file")读取。 - SSE 没消息:确认后端响应是
text/event-stream,每条消息以空行结尾,例如data: ...\n\n。 - SSE 401:不要在 Page 中直接
new EventSource("/api/v1/..."),原生EventSource不能携带Authorizationheader;请通过bridge.subscribeSSE()调用。
- 首页
- 文档入口
- Top Level
- community events
- deploy
- dev
- others
- platform
- 接入 OneBot v11 协议实现
- 接入钉钉 DingTalk
- 接入 Discord
- 接入 Kook
- 接入飞书
- 接入 LINE
- 接入 Matrix
- 接入 Mattermost
- 接入 Misskey 平台
- 接入 QQ 官方机器人平台
- 通过 QQ官方机器人 接入 QQ (Webhook)
- 通过 QQ官方机器人 接入 QQ (Websockets)
- 接入 Satori 协议
- 接入 server-satori (基于 Koishi)
- 接入 Slack
- 接入消息平台
- 接入 Telegram
- 接入 VoceChat
- AstrBot 接入企业微信
- 接入企业微信智能机器人平台
- AstrBot 接入微信公众平台
- 接入个人微信
- providers
- use
- Home
- Docs Entry
- Top Level
- config
- deploy
- Deploy AstrBot on 1Panel
- Deploy AstrBot on BT Panel
- Deploy AstrBot on CasaOS
- Deploy AstrBot from Source Code
- Community-Provided Deployment Methods
- Deploy via Compshare
- Deploy with AstrBot Desktop Client
- Deploy AstrBot with Docker
- Deploy AstrBot with Kubernetes
- Deploy AstrBot with AstrBot Launcher
- Other Deployments
- Package Manager Deployment (uv)
- Installation via System Package Manager
- Preface
- dev
- AstrBot Configuration File
- AstrBot HTTP API
- Developing a Platform Adapter
- plugin
- AI
- Text to Image
- Handling Message Events
- Plugin Configuration
- Plugin Internationalization
- Plugin Pages
- Sending Messages
- Session Control
- Minimal Example
- Plugin Storage
- AstrBot Plugin Development Guide 🌠
- Publishing Plugins to the Plugin Marketplace
- ospp
- others
- platform
- Connect OneBot v11 Protocol Implementations
- Connect to DingTalk
- Connecting to Discord
- Connect to KOOK
- Connecting to Lark
- Connecting to LINE
- Connecting to Matrix
- Connecting to Mattermost
- Connecting to Misskey Platform
- Connect QQ Official Bot
- Connect QQ via QQ Official Bot (Webhook)
- Connect QQ via QQ Official Bot (Websockets)
- Connect to Satori Protocol
- Connect server-satori (Koishi)
- Connecting to Slack
- Messaging Platforms
- Connecting to Telegram
- Connect to VoceChat
- Connect AstrBot to WeCom
- Connect to WeCom AI Bot Platform
- Connect AstrBot to WeChat Official Account Platform
- Connect Personal WeChat
- providers
- Connect 302.AI
- Agent Runners
- Built-in Agent Runner
- Connect to Coze
- Connect to Alibaba Cloud Bailian Application
- Connect to DeerFlow
- Connect to Dify
- Connect AIHubMix
- coze
- dashscope
- dify
- 大语言模型提供商
- NewAPI
- Connect PPIO Cloud
- Connect LM Studio to Use DeepSeek-R1 and Other Models
- Integrating Ollama
- Connecting to SiliconFlow
- Connecting Model Services
- Connecting to TokenPony
- use
- Agent Runner
- Agent Sandbox Environment ⛵️
- astrbot sandbox
- CLI Commands
- Docker-based Code Interpreter
- Built-in Commands
- Computer Use
- Context Compression
- Custom Rules
- Function Calling
- AstrBot Knowledge Base
- MCP
- AstrBot Star
- Proactive Capabilities
- Anthropic Skills
- Agent Handoff and SubAgent
- Unified Webhook Mode
- Web Search
- WebUI