mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
feat: enhance plugin page internationalization (#7998)
* feat: enhance plugin page internationalization - Updated PluginRoute to read initial context from JWT and set it in the bridge SDK. - Added methods to retrieve locale and plugin metadata for better i18n support. - Enhanced pluginI18n utility to resolve page-specific translations and added new functions for page titles and descriptions. - Modified PluginPagePage and PluginDetailPage to utilize new i18n features for dynamic content rendering. - Improved documentation for plugin page i18n structure and usage. - Added tests to verify the correct integration of i18n in plugin pages and context handling. * fix test
This commit is contained in:
@@ -20,6 +20,7 @@ When the current locale has no translation, a field is missing, or the locale fi
|
||||
|
||||
- Plugin names, card short descriptions, and descriptions fall back to `display_name`, `short_desc`, and `desc` in `metadata.yaml`.
|
||||
- Configuration text falls back to `description`, `hint`, and `labels` in `_conf_schema.json`.
|
||||
- Page text falls back to the Page directory name, default Page title, or fallback text provided by page code.
|
||||
|
||||
## Metadata
|
||||
|
||||
@@ -77,6 +78,52 @@ Corresponding `.astrbot-plugin/i18n/zh-CN.json`:
|
||||
|
||||
`options` are stored configuration values and should usually not be translated. Use `labels` for select display text.
|
||||
|
||||
## Plugin Pages
|
||||
|
||||
`pages` overrides plugin Dashboard Page titles, descriptions, and custom text inside plugin pages. The structure is nested by Page directory name.
|
||||
|
||||
Example plugin page directory:
|
||||
|
||||
```text
|
||||
pages/
|
||||
settings/
|
||||
index.html
|
||||
```
|
||||
|
||||
Corresponding `.astrbot-plugin/i18n/en-US.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Manage advanced settings for this plugin.",
|
||||
"save": "Save",
|
||||
"reset": "Reset"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`title` is used by the WebUI shell title and the Page component name on the plugin detail page. `description` is used by the Page component description on the plugin detail page. Other fields are read by the page through the bridge:
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
|
||||
function render() {
|
||||
document.getElementById("save").textContent = bridge.t(
|
||||
"pages.settings.save",
|
||||
"Save",
|
||||
);
|
||||
}
|
||||
|
||||
await bridge.ready();
|
||||
render();
|
||||
bridge.onContext(render);
|
||||
```
|
||||
|
||||
Use `onContext()` to react to WebUI language changes; with this listener, the Page usually does not need a refresh.
|
||||
|
||||
## Nested Configuration
|
||||
|
||||
For `object` items in `_conf_schema.json`, translations use the same nested field structure.
|
||||
|
||||
@@ -96,6 +96,10 @@ Inside a plugin Page, use `window.AstrBotPluginPage` directly:
|
||||
|
||||
- `ready()`: Wait until the bridge is ready and return the context
|
||||
- `getContext()`: Read the current context
|
||||
- `getLocale()`: Read the current WebUI locale
|
||||
- `getI18n()`: Read the current plugin i18n resources
|
||||
- `t(key, fallback)`: Read text from plugin i18n resources by key, returning fallback when missing
|
||||
- `onContext(handler)`: Listen for context changes, such as rerendering after the WebUI locale changes
|
||||
- `apiGet(endpoint, params)`: Send a GET request
|
||||
- `apiPost(endpoint, body)`: Send a POST request
|
||||
- `upload(endpoint, file)`: Upload one file as `multipart/form-data`
|
||||
@@ -108,12 +112,62 @@ The current `ready()` context looks like this:
|
||||
```json
|
||||
{
|
||||
"pluginName": "astrbot_plugin_page_demo",
|
||||
"displayName": "Plugin Page Demo"
|
||||
"displayName": "Plugin Page Demo",
|
||||
"pageName": "bridge-demo",
|
||||
"pageTitle": "Bridge Demo",
|
||||
"locale": "en-US",
|
||||
"i18n": {}
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
## Page Internationalization
|
||||
|
||||
Plugin Pages reuse plugin i18n resource files. Add `pages.<page_name>` to `.astrbot-plugin/i18n/<locale>.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"bridge-demo": {
|
||||
"title": "Bridge Demo",
|
||||
"description": "Shows how a plugin page reads the WebUI locale and translations.",
|
||||
"heading": "Plugin Page",
|
||||
"refresh": "Render again"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`title` is used by the WebUI shell title and the Page component name on the plugin detail page. `description` is used by the Page component description on the plugin detail page.
|
||||
|
||||
Inside the Page, render text with `t()` and react to language changes with `onContext()`:
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
|
||||
function render() {
|
||||
document.title = bridge.t("pages.bridge-demo.title", "Bridge Demo");
|
||||
document.getElementById("heading").textContent = bridge.t(
|
||||
"pages.bridge-demo.heading",
|
||||
"Plugin Page",
|
||||
);
|
||||
document.getElementById("locale").textContent = bridge.getLocale();
|
||||
}
|
||||
|
||||
await bridge.ready();
|
||||
render();
|
||||
bridge.onContext(render);
|
||||
```
|
||||
|
||||
After the WebUI locale changes, the Dashboard sends the new `locale` and plugin i18n resources to the iframe through the bridge. If the Page listens with `onContext()`, it usually does not need a refresh.
|
||||
|
||||
If an inline script needs to access `window.AstrBotPluginPage` synchronously, move the code to an external module file or explicitly include the bridge SDK before your script:
|
||||
|
||||
```html
|
||||
<script src="/api/plugin/page/bridge-sdk.js"></script>
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -151,3 +205,184 @@ AstrBot also adds security headers to asset responses, including:
|
||||
- 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
|
||||
|
||||
## Appendix: Bridge API Details
|
||||
|
||||
Start page scripts by keeping a bridge reference:
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
```
|
||||
|
||||
### `ready()`
|
||||
|
||||
Waits for the parent page to send the initial context and returns `Promise<context>`. Page initialization should wait for this before reading context-dependent values.
|
||||
|
||||
```js
|
||||
const context = await bridge.ready();
|
||||
console.log(context.pluginName, context.pageName, context.locale);
|
||||
```
|
||||
|
||||
The context usually contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"pluginName": "astrbot_plugin_page_demo",
|
||||
"displayName": "Plugin Page Demo",
|
||||
"pageName": "bridge-demo",
|
||||
"pageTitle": "Bridge Demo",
|
||||
"locale": "en-US",
|
||||
"i18n": {}
|
||||
}
|
||||
```
|
||||
|
||||
### `getContext()`
|
||||
|
||||
Synchronously reads the latest context. Use it after `ready()` or inside an `onContext()` callback.
|
||||
|
||||
```js
|
||||
function renderHeader() {
|
||||
const context = bridge.getContext();
|
||||
document.getElementById("title").textContent = context.pageTitle;
|
||||
}
|
||||
```
|
||||
|
||||
### `getLocale()`
|
||||
|
||||
Synchronously reads the current WebUI locale. It returns `zh-CN` when no context exists yet.
|
||||
|
||||
```js
|
||||
document.documentElement.lang = bridge.getLocale();
|
||||
```
|
||||
|
||||
### `getI18n()`
|
||||
|
||||
Synchronously reads the full plugin i18n resource object. Prefer `t()` for normal rendering; use this for custom traversal or debugging.
|
||||
|
||||
```js
|
||||
console.log(Object.keys(bridge.getI18n()));
|
||||
```
|
||||
|
||||
### `t(key, fallback)`
|
||||
|
||||
Reads text from plugin i18n by dot-separated key. If the current locale is missing, the bridge tries fallbacks; if still missing, it returns `fallback`.
|
||||
|
||||
```js
|
||||
saveButton.textContent = bridge.t("pages.settings.save", "Save");
|
||||
```
|
||||
|
||||
### `onContext(handler)`
|
||||
|
||||
Listens for context changes and returns an unsubscribe function. The callback runs when the WebUI locale changes, so pages that need live language switching should rerender here.
|
||||
|
||||
```js
|
||||
function render() {
|
||||
document.title = bridge.t("pages.settings.title", "Settings");
|
||||
}
|
||||
|
||||
await bridge.ready();
|
||||
render();
|
||||
|
||||
const off = bridge.onContext(render);
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
off();
|
||||
});
|
||||
```
|
||||
|
||||
### `apiGet(endpoint, params)`
|
||||
|
||||
Sends a GET request to the plugin backend and returns `Promise<data>`. `endpoint` is a plugin-local relative path; the Dashboard forwards it to `/api/plug/<plugin_name>/<endpoint>`.
|
||||
|
||||
```js
|
||||
const stats = await bridge.apiGet("stats", { limit: 20 });
|
||||
```
|
||||
|
||||
Backend registration example:
|
||||
|
||||
```python
|
||||
context.register_web_api(
|
||||
f"/{PLUGIN_NAME}/stats",
|
||||
self.get_stats,
|
||||
["GET"],
|
||||
"Get stats",
|
||||
)
|
||||
```
|
||||
|
||||
### `apiPost(endpoint, body)`
|
||||
|
||||
Sends a POST request to the plugin backend. `body` is sent as JSON and the method returns `Promise<data>`.
|
||||
|
||||
```js
|
||||
await bridge.apiPost("settings/save", {
|
||||
enabled: true,
|
||||
threshold: 0.8,
|
||||
});
|
||||
```
|
||||
|
||||
### `upload(endpoint, file)`
|
||||
|
||||
Uploads one file as `multipart/form-data` with the field name `file`, returning `Promise<data>`.
|
||||
|
||||
```js
|
||||
const input = document.querySelector("input[type=file]");
|
||||
const file = input.files[0];
|
||||
const result = await bridge.upload("files/import", file);
|
||||
```
|
||||
|
||||
### `download(endpoint, params, filename)`
|
||||
|
||||
Requests a plugin backend file endpoint and triggers a browser download. `params` are sent as query parameters. `filename` is optional; when omitted, the bridge tries to use the response filename header.
|
||||
|
||||
```js
|
||||
await bridge.download("files/export", { format: "json" }, "export.json");
|
||||
```
|
||||
|
||||
### `subscribeSSE(endpoint, handlers, params)`
|
||||
|
||||
Subscribes to plugin backend SSE and returns `Promise<subscriptionId>`. `handlers` may include `onOpen`, `onMessage`, and `onError`.
|
||||
|
||||
```js
|
||||
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.parsed` is automatically parsed when the message is a JSON string; otherwise it equals the raw string.
|
||||
|
||||
### `unsubscribeSSE(subscriptionId)`
|
||||
|
||||
Cancels an SSE subscription.
|
||||
|
||||
```js
|
||||
await bridge.unsubscribeSSE(subscriptionId);
|
||||
```
|
||||
|
||||
Clean up subscriptions when the page unloads:
|
||||
|
||||
```js
|
||||
window.addEventListener("beforeunload", () => {
|
||||
bridge.unsubscribeSSE(subscriptionId);
|
||||
});
|
||||
```
|
||||
|
||||
### endpoint Rules
|
||||
|
||||
The `endpoint` used by `apiGet`, `apiPost`, `upload`, `download`, and `subscribeSSE` must be a plugin-local relative path:
|
||||
|
||||
- Allowed: `"stats"`, `"settings/save"`, `"files/export"`
|
||||
- Not allowed: empty string, `"/stats"`, `"../stats"`, `"https://example.com"`, `"stats?x=1"`, `"stats#top"`
|
||||
|
||||
Pass query parameters through `params`; do not append them to `endpoint`.
|
||||
|
||||
@@ -20,6 +20,7 @@ your_plugin/
|
||||
|
||||
- 插件名称、卡片短描述和描述回退到 `metadata.yaml` 中的 `display_name`、`short_desc`、`desc`。
|
||||
- 配置项文案回退到 `_conf_schema.json` 中的 `description`、`hint`、`labels`。
|
||||
- Page 文案回退到 Page 目录名、Page 默认标题或页面代码中提供的 fallback。
|
||||
|
||||
## 元数据
|
||||
|
||||
@@ -77,6 +78,52 @@ your_plugin/
|
||||
|
||||
`options` 是配置保存值,不建议翻译。下拉框的展示文本请使用 `labels`。
|
||||
|
||||
## 插件 Pages
|
||||
|
||||
`pages` 用于覆盖插件 Dashboard Page 的标题、描述和页面内自定义文案。结构按 Page 目录名嵌套。
|
||||
|
||||
例如插件页面目录:
|
||||
|
||||
```text
|
||||
pages/
|
||||
settings/
|
||||
index.html
|
||||
```
|
||||
|
||||
对应 `.astrbot-plugin/i18n/zh-CN.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"description": "管理这个插件的高级设置。",
|
||||
"save": "保存",
|
||||
"reset": "重置"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`title` 会用于 WebUI 外壳标题和插件详情页中的 Page 组件名称,`description` 会用于插件详情页中的 Page 组件描述。其他字段由页面通过 bridge 自行读取:
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
|
||||
function render() {
|
||||
document.getElementById("save").textContent = bridge.t(
|
||||
"pages.settings.save",
|
||||
"Save",
|
||||
);
|
||||
}
|
||||
|
||||
await bridge.ready();
|
||||
render();
|
||||
bridge.onContext(render);
|
||||
```
|
||||
|
||||
`onContext()` 用于响应 WebUI 语言切换;监听后通常不需要刷新 Page。
|
||||
|
||||
## 嵌套配置
|
||||
|
||||
如果 `_conf_schema.json` 中有 `object` 类型配置,翻译也按同样的字段结构继续嵌套。
|
||||
|
||||
@@ -96,6 +96,10 @@ class MyPlugin(Star):
|
||||
|
||||
- `ready()`: 等待 bridge 就绪并返回上下文
|
||||
- `getContext()`: 读取当前上下文
|
||||
- `getLocale()`: 读取当前 WebUI 语言
|
||||
- `getI18n()`: 读取当前插件的 i18n 资源
|
||||
- `t(key, fallback)`: 从插件 i18n 资源中按 key 获取文案,缺失时返回 fallback
|
||||
- `onContext(handler)`: 监听上下文变化,例如 WebUI 切换语言后重新渲染页面
|
||||
- `apiGet(endpoint, params)`: 发送 GET 请求
|
||||
- `apiPost(endpoint, body)`: 发送 POST 请求
|
||||
- `upload(endpoint, file)`: 以 `multipart/form-data` 上传单个文件
|
||||
@@ -108,12 +112,62 @@ class MyPlugin(Star):
|
||||
```json
|
||||
{
|
||||
"pluginName": "astrbot_plugin_page_demo",
|
||||
"displayName": "Plugin Page Demo"
|
||||
"displayName": "Plugin Page Demo",
|
||||
"pageName": "bridge-demo",
|
||||
"pageTitle": "Bridge Demo",
|
||||
"locale": "zh-CN",
|
||||
"i18n": {}
|
||||
}
|
||||
```
|
||||
|
||||
`endpoint` 必须是插件内相对路径,不能为空,不能包含 `\`、URL scheme、query、hash,也不能包含 `.` 或 `..` 路径片段。
|
||||
|
||||
## Page 国际化
|
||||
|
||||
插件 Page 复用插件 i18n 资源文件。给 `.astrbot-plugin/i18n/<locale>.json` 增加 `pages.<page_name>` 即可:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"bridge-demo": {
|
||||
"title": "Bridge 演示页",
|
||||
"description": "演示插件页面如何读取 WebUI 语言和翻译资源。",
|
||||
"heading": "插件页面",
|
||||
"refresh": "重新渲染"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`title` 会用于 WebUI 外壳标题和插件详情页的 Page 组件名称,`description` 会用于插件详情页的 Page 组件描述。
|
||||
|
||||
在 Page 内部使用 `t()` 渲染文案,并用 `onContext()` 响应语言切换:
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
|
||||
function render() {
|
||||
document.title = bridge.t("pages.bridge-demo.title", "Bridge Demo");
|
||||
document.getElementById("heading").textContent = bridge.t(
|
||||
"pages.bridge-demo.heading",
|
||||
"Plugin Page",
|
||||
);
|
||||
document.getElementById("locale").textContent = bridge.getLocale();
|
||||
}
|
||||
|
||||
await bridge.ready();
|
||||
render();
|
||||
bridge.onContext(render);
|
||||
```
|
||||
|
||||
切换 WebUI 语言后,Dashboard 会把新的 `locale` 和插件 i18n 资源通过 bridge 发送给 iframe;只要 Page 监听了 `onContext()`,通常不需要刷新页面。
|
||||
|
||||
如果你的内联脚本需要同步访问 `window.AstrBotPluginPage`,请把脚本放到外部 module 文件中,或在自己的脚本前显式引入:
|
||||
|
||||
```html
|
||||
<script src="/api/plugin/page/bridge-sdk.js"></script>
|
||||
```
|
||||
|
||||
## 静态资源路径规则
|
||||
|
||||
AstrBot 会重写相对资源路径,并自动补上短期 `asset_token`。你只需要正常写相对路径,不要自己拼接 `/api/plugin/page/content/...`。
|
||||
@@ -151,3 +205,184 @@ AstrBot 还会给资源响应添加安全头,包括:
|
||||
- 新增或删除 Page 目录后重载插件
|
||||
- 修改 `pages/<page_name>/` 下的大多数静态资源后,刷新 Page 即可
|
||||
- 如果 Page 没出现,检查 `pages/<page_name>/index.html` 是否存在,以及插件是否启用
|
||||
|
||||
## 附录:Bridge API 详解
|
||||
|
||||
建议在页面脚本开始处保存 bridge 引用:
|
||||
|
||||
```js
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
```
|
||||
|
||||
### `ready()`
|
||||
|
||||
等待父页面发送初始上下文,返回 `Promise<context>`。页面初始化时应先等待它,避免过早读取空上下文。
|
||||
|
||||
```js
|
||||
const context = await bridge.ready();
|
||||
console.log(context.pluginName, context.pageName, context.locale);
|
||||
```
|
||||
|
||||
上下文通常包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"pluginName": "astrbot_plugin_page_demo",
|
||||
"displayName": "Plugin Page Demo",
|
||||
"pageName": "bridge-demo",
|
||||
"pageTitle": "Bridge Demo",
|
||||
"locale": "zh-CN",
|
||||
"i18n": {}
|
||||
}
|
||||
```
|
||||
|
||||
### `getContext()`
|
||||
|
||||
同步读取最近一次上下文。适合在 `ready()` 之后或 `onContext()` 回调中使用。
|
||||
|
||||
```js
|
||||
function renderHeader() {
|
||||
const context = bridge.getContext();
|
||||
document.getElementById("title").textContent = context.pageTitle;
|
||||
}
|
||||
```
|
||||
|
||||
### `getLocale()`
|
||||
|
||||
同步读取当前 WebUI 语言。没有上下文时默认返回 `zh-CN`。
|
||||
|
||||
```js
|
||||
document.documentElement.lang = bridge.getLocale();
|
||||
```
|
||||
|
||||
### `getI18n()`
|
||||
|
||||
同步读取当前插件的完整 i18n 资源对象。一般优先用 `t()`,只有需要自定义遍历或调试时才直接读取。
|
||||
|
||||
```js
|
||||
console.log(Object.keys(bridge.getI18n()));
|
||||
```
|
||||
|
||||
### `t(key, fallback)`
|
||||
|
||||
按点分隔 key 从插件 i18n 中取文案。当前语言缺失时会尝试回退,仍缺失则返回 `fallback`。
|
||||
|
||||
```js
|
||||
saveButton.textContent = bridge.t("pages.settings.save", "Save");
|
||||
```
|
||||
|
||||
### `onContext(handler)`
|
||||
|
||||
监听上下文变化,返回取消监听函数。WebUI 切换语言时会触发该回调,所以需要响应语言切换的页面应在这里重新渲染。
|
||||
|
||||
```js
|
||||
function render() {
|
||||
document.title = bridge.t("pages.settings.title", "Settings");
|
||||
}
|
||||
|
||||
await bridge.ready();
|
||||
render();
|
||||
|
||||
const off = bridge.onContext(render);
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
off();
|
||||
});
|
||||
```
|
||||
|
||||
### `apiGet(endpoint, params)`
|
||||
|
||||
向插件后端发送 GET 请求,返回 `Promise<data>`。`endpoint` 是插件内相对路径,Dashboard 会转发到 `/api/plug/<plugin_name>/<endpoint>`。
|
||||
|
||||
```js
|
||||
const stats = await bridge.apiGet("stats", { limit: 20 });
|
||||
```
|
||||
|
||||
后端注册示例:
|
||||
|
||||
```python
|
||||
context.register_web_api(
|
||||
f"/{PLUGIN_NAME}/stats",
|
||||
self.get_stats,
|
||||
["GET"],
|
||||
"Get stats",
|
||||
)
|
||||
```
|
||||
|
||||
### `apiPost(endpoint, body)`
|
||||
|
||||
向插件后端发送 POST 请求,`body` 会作为 JSON 请求体发送,返回 `Promise<data>`。
|
||||
|
||||
```js
|
||||
await bridge.apiPost("settings/save", {
|
||||
enabled: true,
|
||||
threshold: 0.8,
|
||||
});
|
||||
```
|
||||
|
||||
### `upload(endpoint, file)`
|
||||
|
||||
以 `multipart/form-data` 上传单个文件,字段名为 `file`,返回 `Promise<data>`。
|
||||
|
||||
```js
|
||||
const input = document.querySelector("input[type=file]");
|
||||
const file = input.files[0];
|
||||
const result = await bridge.upload("files/import", file);
|
||||
```
|
||||
|
||||
### `download(endpoint, params, filename)`
|
||||
|
||||
请求插件后端文件接口并触发浏览器下载。`params` 会作为 query string,`filename` 可选;不传时会尝试使用响应头里的文件名。
|
||||
|
||||
```js
|
||||
await bridge.download("files/export", { format: "json" }, "export.json");
|
||||
```
|
||||
|
||||
### `subscribeSSE(endpoint, handlers, params)`
|
||||
|
||||
订阅插件后端 SSE,返回 `Promise<subscriptionId>`。`handlers` 可包含 `onOpen`、`onMessage`、`onError`。
|
||||
|
||||
```js
|
||||
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.parsed` 会在消息内容是 JSON 字符串时自动解析,否则等于原始字符串。
|
||||
|
||||
### `unsubscribeSSE(subscriptionId)`
|
||||
|
||||
取消 SSE 订阅。
|
||||
|
||||
```js
|
||||
await bridge.unsubscribeSSE(subscriptionId);
|
||||
```
|
||||
|
||||
页面卸载时建议清理订阅:
|
||||
|
||||
```js
|
||||
window.addEventListener("beforeunload", () => {
|
||||
bridge.unsubscribeSSE(subscriptionId);
|
||||
});
|
||||
```
|
||||
|
||||
### endpoint 规则
|
||||
|
||||
`apiGet`、`apiPost`、`upload`、`download`、`subscribeSSE` 的 `endpoint` 必须是插件内相对路径:
|
||||
|
||||
- 允许:`"stats"`、`"settings/save"`、`"files/export"`
|
||||
- 不允许:空字符串、`"/stats"`、`"../stats"`、`"https://example.com"`、`"stats?x=1"`、`"stats#top"`
|
||||
|
||||
query 参数请通过 `params` 传递,不要拼进 `endpoint`。
|
||||
|
||||
Reference in New Issue
Block a user