fix: resolve merge conflicts and complete dashboard API migration

This commit is contained in:
LIghtJUNction
2026-06-26 09:50:15 +08:00
274 changed files with 60026 additions and 6972 deletions

View File

@@ -16,10 +16,6 @@ Welcome to submit Issues or Pull Requests:
### Tencent QQ Groups
- Group 12: 916228568 (New)
- Group 9: 1076659624 (Full)
- Group 10: 1078079676 (Full)
- Group 11: 704659519 (Full)
- Group 1: 322154837 (Full)
- Group 3: 630166526 (Full)
- Group 4: 1077826412 (Full)
@@ -27,6 +23,12 @@ Welcome to submit Issues or Pull Requests:
- Group 6: 753075035 (Full)
- Group 7: 743746109 (Full)
- Group 8: 1030353265 (Full)
- Group 9: 1076659624 (Full)
- Group 10: 1078079676 (Full)
- Group 11: 704659519 (Full)
- Group 12: 916228568 (Full)
- Group 13: 1092185289
- Group 14: 1103419483
- **AstrBot Core Development Group: 975206796** (AstrBot development members are usually active here. Welcome to anyone interested in programming/AI technology~)
## Become an AstrBot Organization Member

View File

@@ -288,12 +288,13 @@ Whether to enable AstrBot's built-in web search capability. Default is `false`.
#### `provider_settings.websearch_provider`
Web search provider type. Default is `tavily`. Currently supports `tavily`, `bocha`, `baidu_ai_search`, and `brave`.
Web search provider type. Default is `tavily`. Currently supports `tavily`, `bocha`, `baidu_ai_search`, `brave`, and `firecrawl`.
- `tavily`: Uses the Tavily search engine.
- `bocha`: Uses the BoCha search engine.
- `baidu_ai_search`: Uses Baidu AI Search (MCP).
- `brave`: Uses Brave Search API.
- `firecrawl`: Uses the Firecrawl Search API.
#### `provider_settings.websearch_tavily_key`
@@ -307,6 +308,10 @@ API Key list for the BoCha search engine. Required when using `bocha` as the web
API Key list for the Brave search engine. Required when using `brave` as the web search provider.
#### `provider_settings.websearch_firecrawl_key`
API Key list for the Firecrawl search engine. Required when using `firecrawl` as the web search provider.
#### `provider_settings.web_search_link`
Whether to prompt the model to include links to search results in the reply. Default is `false`.

View File

@@ -26,19 +26,31 @@ X-API-Key: abk_xxx
- `POST /api/v1/chat`: request body must include `username`
- `GET /api/v1/chat/sessions`: query params must include `username`
The local OpenAPI schema is available at `http://localhost:6185/api/v1/openapi.json`, and the interactive docs are available at `http://localhost:6185/api/v1/docs`.
## Scope Permissions
When creating an API Key, you can configure `scopes`. Each scope controls the range of accessible endpoints:
| Scope | Purpose | Accessible Endpoints |
| --- | --- | --- |
| `bot` | Manage bot/platform configurations | `GET /api/v1/bot-types`, `GET/POST /api/v1/bots`, `PATCH /api/v1/bots/enabled` |
| `provider` | Manage model providers and provider sources | `GET/POST /api/v1/providers`, `GET/PUT/DELETE /api/v1/provider-sources/by-id` |
| `persona` | Manage personas and persona folders | `GET/POST /api/v1/personas`, `GET/POST /api/v1/persona-folders` |
| `im` | Send proactive IM messages and query bot/platform list | `POST /api/v1/im/message`, `GET /api/v1/im/bots` |
| `config` | Manage config profiles, system config, and shared configuration. This scope also includes `bot` and `provider` access. | `GET /api/v1/configs`, `GET/PUT /api/v1/system-config`, `GET/POST /api/v1/config-profiles` |
| `chat` | Access chat capabilities and query sessions | `POST /api/v1/chat`, `GET /api/v1/chat/sessions` |
| `config` | Retrieve available config file list | `GET /api/v1/configs` |
| `file` | Upload attachment files and get `attachment_id` | `POST /api/v1/file` |
| `im` | Send proactive IM messages, query bot/platform list | `POST /api/v1/im/message`, `GET /api/v1/im/bots` |
| `file` | Upload and download chat attachments | `POST /api/v1/file`, `GET /api/v1/file`, `POST /api/v1/files` |
| `plugin` | Manage plugins, plugin config, plugin sources, and marketplace entries | `GET /api/v1/plugins`, `GET/PUT /api/v1/plugins/config`, `POST /api/v1/plugins/install/url` |
| `mcp` | Manage MCP server configurations and provider sync | `GET/POST /api/v1/mcp/servers`, `PATCH /api/v1/mcp/servers/{server_name}/enabled`, `POST /api/v1/mcp/providers/modelscope/sync` |
| `skill` | Manage skills, skill archives, skill files, and Shipyard Neo skill workflows | `GET/POST /api/v1/skills`, `PUT /api/v1/skills/{skill_name}/files/{file_path}`, `POST /api/v1/skills/neo/sync` |
If the API Key does not include the required scope for the target endpoint, the request will return `403 Insufficient API key scope`.
`config` is a broad management scope. When an API key is created with `config`, AstrBot grants the key `config`, `bot`, and `provider` access together. The WebUI mirrors this dependency: selecting `config` selects `bot` and `provider`; deselecting `bot` or `provider` removes `config`.
Developer API keys currently support only the 10 scopes listed above. `tool`, `skills`, `kb`, `data`, and `system` are not valid developer API key scopes. Use the singular `skill` scope for `/api/v1/skills/*` endpoints. The public OpenAPI reference only includes endpoints covered by supported developer API key scopes.
## Common Endpoints
**Chat**
@@ -48,10 +60,21 @@ Interact with AstrBot's built-in Agent. Supports plugin calls, tool calls, and o
- `POST /api/v1/chat`: send chat message (SSE stream, server generates UUID when `session_id` is omitted)
- `GET /api/v1/chat/sessions`: list sessions for a specific `username` with pagination
- `GET /api/v1/configs`: list available config files
- `POST /api/v1/file`: upload an attachment for later use in message segments
**File Upload**
**Bots and Providers**
- `POST /api/v1/file`: upload attachment
- `GET /api/v1/bots`: list bot/platform configurations
- `POST /api/v1/bots`: create a bot/platform configuration
- `GET /api/v1/providers`: list model provider configurations
- `GET /api/v1/provider-sources`: list provider source configurations
**Personas, Plugins, MCP, and Skills**
- `GET /api/v1/personas`: list personas
- `GET /api/v1/plugins`: list plugins
- `GET /api/v1/mcp/servers`: list MCP servers
- `GET /api/v1/skills`: list skills
**Proactive IM Messages**
@@ -99,7 +122,7 @@ Supported `type` values:
Notes:
- `attachment_id` comes from the upload result of `POST /api/v1/file`.
- `attachment_id` comes from an existing attachment record, or from `POST /api/v1/file` after uploading an attachment with the `file` scope.
- `reply` cannot be the only segment; at least one content segment (e.g. `plain/image/file/...`) is required.
- A request with only `reply` or empty content will return an error.
@@ -107,6 +130,8 @@ Notes:
`POST /api/v1/chat` additionally requires `username`, with optional `session_id` (a UUID is auto-generated if omitted).
`username` is a caller-declared WebChat identity. It is used as the message sender and session owner in the message pipeline, including sender-ID-based command permission checks. Treat API keys with the `chat` scope as trusted backend credentials. If you expose chat access to end users, proxy requests through your own service and map each external user to an allowed `username`; do not let clients submit administrator IDs or other reserved sender IDs directly.
```json
{
"username": "alice",

View File

@@ -131,7 +131,6 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
from astrbot.api.message_components import Plain, Image
from .client import FakeClient
from astrbot.core.utils.io import download_image_by_url
class FakePlatformEvent(AstrMessageEvent):
def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: FakeClient):
@@ -143,23 +142,70 @@ class FakePlatformEvent(AstrMessageEvent):
if isinstance(i, Plain): # If it's a text message
await self.client.send_text(to=self.get_sender_id(), message=i.text)
elif isinstance(i, Image): # If it's an image
img_url = i.file
img_path = ""
# The three conditions below can be used as a reference.
if img_url.startswith("file:///"):
img_path = img_url[8:]
elif i.file and i.file.startswith("http"):
img_path = await download_image_by_url(i.file)
else:
img_path = img_url
# Make good use of debugging!
# convert_to_file_path() resolves supported media refs through
# the shared media utilities.
img_path = await i.convert_to_file_path()
await self.client.send_image(to=self.get_sender_id(), image_path=img_path)
await super().send(message) # Must be called at the end to invoke the parent class's send method.
```
## Media Message Handling
Platform adapters do not need to reimplement media parsing for every platform. Convert the platform message into AstrBot message components, and put the media reference in the component's `file` / `url` field. The supported reference forms are:
- Local path, such as `/tmp/a.jpg`
- Standard `file:` URI, such as `file:///tmp/a.jpg`
- HTTP(S) URL, such as `https://example.com/a.jpg`
- `base64://`, such as `base64://iVBORw0KGgo...`
- Data URI, such as `data:image/png;base64,iVBORw0KGgo...`
- Legacy bare base64, such as `iVBORw0KGgo...`; supported for compatibility, but new code should not generate it intentionally
If you already have a local file, prefer the component factory methods. They generate standard `file:` URIs:
```py
from astrbot.api.message_components import Image, Record, Video
abm.message.append(Image.fromFileSystem("/tmp/image.png"))
abm.message.append(Record.fromFileSystem("/tmp/audio.wav"))
abm.message.append(Video.fromFileSystem("/tmp/video.mp4"))
```
If the platform gives you an accessible URL, pass it directly to the component:
```py
abm.message.append(Image(file=image_url, url=image_url))
abm.message.append(Record(file=audio_url, url=audio_url))
abm.message.append(Video(file=video_url, url=video_url))
```
Before plugins and LLM providers see the event, AstrBot's preprocess stage tries to normalize media in the message chain:
- `Image` is materialized through the shared media utilities and converted to JPEG when needed.
- `Record` is materialized through the shared media utilities and converted to WAV when needed.
- `Image` / `Record` inside `Reply` chains are normalized in the same way.
- Temporary files created by core are attached to the current event and cleaned up when the event finishes.
When sending messages, if the platform SDK needs a local file path, call the component's `convert_to_file_path()` instead of writing checks like `path.startswith("file://")`:
```py
if isinstance(i, Image):
image_path = await i.convert_to_file_path()
await self.client.send_image(to=self.get_sender_id(), image_path=image_path)
elif isinstance(i, Record):
audio_path = await i.convert_to_file_path()
await self.client.send_audio(to=self.get_sender_id(), audio_path=audio_path)
elif isinstance(i, Video):
video_path = await i.convert_to_file_path()
await self.client.send_video(to=self.get_sender_id(), video_path=video_path)
```
If the adapter downloads platform media into AstrBot's temporary directory by itself, register the path on the event after creating it so the file does not remain after the event finishes:
```py
message_event.track_temporary_local_file(temp_media_path)
```
Finally, in `main.py`, simply import the `fake_platform_adapter` module during initialization. The decorator will handle registration automatically.
```py

View File

@@ -1,6 +1,12 @@
# Plugin Pages
AstrBot lets a plugin expose Dashboard pages by placing static assets under `pages/`. Each direct child directory is one Page:
Plugin Pages let a plugin provide its own pages inside the AstrBot WebUI. Page files live under the plugin's `pages/` directory and are loaded by the Dashboard in a restricted iframe. Page scripts communicate with the Dashboard through the `window.AstrBotPluginPage` bridge, and the Dashboard forwards backend calls to Web APIs registered by the plugin.
If you only need a small set of editable settings, prefer [`_conf_schema.json`](./plugin-config.md). Pages are a better fit for complex forms, runtime dashboards, logs, file upload/download, SSE streams, charts, and other custom workflows.
## Directory Layout
Each direct child directory under `pages/` is one Page. AstrBot only discovers `pages/<page_name>/index.html`; directories without `index.html` are ignored.
```text
astrbot_plugin_page_demo/
@@ -16,13 +22,65 @@ astrbot_plugin_page_demo/
└─ index.html
```
AstrBot scans `pages/<page_name>/index.html`; directories without `index.html` are ignored.
Use simple directory names for `page_name`, such as `settings` or `bridge-demo`. Do not use an empty name, `.`, `..`, a name starting with `.`, or a name containing `/` or `\`.
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.
Users open Pages from the plugin detail page in the WebUI.
Once Pages are registered, users can open the AstrBot WebUI Plugins page, click the plugin card to enter the plugin detail page, and then view and open the registered Pages from that detail page.
## Development Flow
## Minimal Frontend Example
1. Create `pages/<page_name>/index.html` in the plugin directory.
2. Use the `window.AstrBotPluginPage` bridge from the Page.
3. Register backend APIs with `context.register_web_api()` in `main.py`.
4. Read requests and return responses with `astrbot.api.web`.
5. Reload the plugin after adding or removing Page directories; refreshing the Page is usually enough for static asset edits.
## Minimal Complete Example
### Backend
Plugin backend code should use `astrbot.api.web`. Avoid exposing raw FastAPI, Starlette, or Quart request objects as the public API for your plugin business logic.
```python
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})
```
### Frontend
`pages/bridge-demo/index.html`
@@ -52,226 +110,215 @@ const context = await bridge.ready();
output.textContent = JSON.stringify(context, null, 2);
document.getElementById("ping").addEventListener("click", async () => {
const result = await bridge.apiGet("ping");
const result = await bridge.apiGet("ping", { limit: 20 });
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
- `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`
- `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",
"pageName": "bridge-demo",
"pageTitle": "Bridge Demo",
"locale": "en-US",
"i18n": {},
"isDark": false
}
```
`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:
You do not need to import the bridge SDK manually. AstrBot injects `/api/plugin/page/bridge-sdk.js` into returned HTML. If an inline script must access `window.AstrBotPluginPage` synchronously, move it to an external module file or explicitly include the SDK before your script:
```html
<script src="/api/plugin/page/bridge-sdk.js"></script>
```
## Light/Dark Theme Adaptation
## Backend Web APIs
When the user toggles light/dark mode in AstrBot, the theme state is automatically synced to Plugin Pages. The bridge SDK maintains a `data-theme` attribute on the `<html>` element:
### Route Registration
- Light mode: `<html data-theme="light">`
- Dark mode: `<html data-theme="dark">`
Register plugin APIs with `context.register_web_api(route, view_handler, methods, desc)`.
### CSS Adaptation
CSS variables are the recommended approach:
```css
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #1a1a1a;
--text: #e0e0e0;
}
body {
background: var(--bg);
color: var(--text);
}
```python
context.register_web_api(
f"/{PLUGIN_NAME}/items/<item_id>",
self.get_item,
["GET"],
"Get item",
)
```
AstrBot injects the `data-theme` attribute on the `<html>` tag server-side when serving HTML, so there is no initial flash.
### Responding to Theme Changes in JavaScript
The `isDark` field in the context indicates whether dark mode is active. Use `onContext()` to listen for theme changes:
The registered route must include the plugin name prefix. The bridge endpoint used by the Page does not include the plugin name:
```js
const bridge = window.AstrBotPluginPage;
function render() {
if (bridge.getContext()?.isDark) {
// JS logic for dark mode (e.g. chart filters)
}
}
await bridge.ready();
render();
bridge.onContext(render);
await bridge.apiGet("items/123");
```
When the theme is toggled, the Dashboard sends the updated context to the iframe via the bridge. As long as the Page listens with `onContext()`, it will be notified.
## 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:
The Dashboard forwards it to:
```text
allow-scripts allow-forms allow-downloads
/api/v1/plugins/extensions/<plugin_name>/items/123
```
The page cannot directly access Dashboard cookies, LocalStorage, or same-origin DOM, and it cannot bypass the bridge to reuse Dashboard auth directly.
The registered route `/<plugin_name>/items/<item_id>` matches the request, and `item_id` is passed to the handler as a keyword argument:
AstrBot also adds security headers to asset responses, including:
```python
async def get_item(self, item_id: str):
return json_response({"item_id": item_id})
```
- `X-Frame-Options: SAMEORIGIN`
- `Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'`
- `Cache-Control: no-store`
Supported dynamic segments:
## Debugging Tips
- `<name>`: matches one path segment.
- `<path:name>`: matches the remaining multi-segment path.
- 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
### Request Object
## Appendix: Bridge API Details
Recommended import:
Start page scripts by keeping a bridge reference:
```python
from astrbot.api.web import request
```
`request` is a context proxy for the current request and is only available while a plugin Web API handler is running. Common fields and methods:
| API | Description |
| --- | --- |
| `request.method` | HTTP method, such as `GET` or `POST` |
| `request.path` | Current Dashboard API path |
| `request.plugin_name` | Plugin name parsed from the extension path |
| `request.username` | Current Dashboard username, possibly `None` |
| `request.headers` | Request headers |
| `request.cookies` | Request cookies |
| `request.content_type` | Request Content-Type |
| `request.client_host` | Client address |
| `request.path_params` | Dynamic route parameters |
| `request.query` | Query parameters with `get()` and `getlist()` |
| `await request.body()` | Raw request body bytes |
| `await request.json(default={})` | JSON body, returning default on parse failure |
| `await request.form()` | Form fields without uploaded files |
| `await request.files()` | Uploaded files |
Query example:
```python
limit = request.query.get("limit", 20, type=int)
tags = request.query.getlist("tag")
```
JSON example:
```python
payload = await request.json(default={})
enabled = bool(payload.get("enabled"))
```
Upload example:
```python
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()` and `request.files()` cache the parsed multipart data, so calling both in the same handler is fine.
### Responses
Recommended response helpers:
```python
from astrbot.api.web import (
error_response,
file_response,
json_response,
stream_response,
)
```
JSON response:
```python
return json_response({"saved": True})
```
Error response:
```python
return error_response("invalid threshold", status_code=400)
```
File download response:
```python
return file_response(
export_path,
filename="export.json",
content_type="application/json",
)
```
SSE response:
```python
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())
```
Returning a `dict`, `list`, `(body, status_code)`, or a lower-level Response object still works. New plugins should prefer `astrbot.api.web` helpers so plugin code remains decoupled from the Dashboard's internal web framework.
### Quart Compatibility
For backward compatibility, handlers registered through `context.register_web_api()` still run inside a Quart-compatible request context. Existing plugins can continue to use:
```python
from quart import jsonify, request
```
New plugins and new documentation should use:
```python
from astrbot.api.web import json_response, request
```
Do not mix the two `request` proxies in the same handler. Migrate one handler at a time.
## Bridge API
The Page iframe cannot directly access Dashboard cookies, LocalStorage, or the parent DOM. Page scripts must use `window.AstrBotPluginPage` to call backend APIs and read context.
```js
const bridge = window.AstrBotPluginPage;
```
### `ready()`
### Context
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.
`ready()` waits for the parent page to send the initial context and returns `Promise<context>`. Wait for it during page initialization.
```js
const context = await bridge.ready();
console.log(context.pluginName, context.pageName, context.locale);
```
The context usually contains:
@@ -288,93 +335,111 @@ The context usually contains:
}
```
### `getContext()`
Context APIs:
Synchronously reads the latest context. Use it after `ready()` or inside an `onContext()` callback.
| API | Returns | Description |
| --- | --- | --- |
| `ready()` | `Promise<context>` | Waits until the bridge is ready and returns the initial context |
| `getContext()` | `context \| null` | Synchronously reads the latest context |
| `getLocale()` | `string` | Current WebUI locale, defaulting to `zh-CN` |
| `getI18n()` | `object` | Current plugin i18n resources |
| `t(key, fallback)` | `string` | Reads a dot-separated translation key, returning fallback when missing |
| `onContext(handler)` | `() => void` | Listens for context changes and returns an unsubscribe function |
```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.
Respond to locale or theme changes:
```js
function render() {
document.title = bridge.t("pages.settings.title", "Settings");
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);
```
window.addEventListener("beforeunload", () => {
off();
});
### Request and Return Rules
The `endpoint` used by `apiGet`, `apiPost`, `upload`, `download`, and `subscribeSSE` is a plugin-local relative path, such as `stats`, `settings/save`, or `files/export`. Prefer not to start it with `/`; the current bridge strips leading `/` for compatibility.
`endpoint` must not be empty, contain `\`, contain a URL scheme, contain query strings or fragments, or contain empty, `.`, or `..` path segments.
Do not append query strings to endpoint:
```js
await bridge.apiGet("stats", { limit: 20 });
```
Bridge JSON calls use this compatibility rule:
- If the backend returns `{ "status": "ok", "data": value }`, the Promise resolves to `value`.
- If the backend returns plain JSON, such as `{ "message": "pong" }`, the Promise resolves to that full JSON body.
- If the backend returns `{ "status": "error", "message": "..." }`, or the HTTP request fails, the Promise rejects with `Error`.
For Page-only APIs, prefer returning plain business JSON:
```python
return json_response({"message": "pong"})
```
Use this for errors:
```python
return error_response("missing file", status_code=400)
```
Handle errors on the Page:
```js
try {
await bridge.apiPost("settings/save", { enabled: true });
} catch (error) {
console.error(error.message);
}
```
### `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>`.
Sends a GET request. `params` are sent as query parameters.
```js
const stats = await bridge.apiGet("stats", { limit: 20 });
const stats = await bridge.apiGet("stats", { limit: 20, tag: "today" });
```
Backend registration example:
Backend:
```python
context.register_web_api(
f"/{PLUGIN_NAME}/stats",
self.get_stats,
["GET"],
"Get stats",
)
async def stats(self):
limit = request.query.get("limit", 20, type=int)
tag = request.query.get("tag")
return json_response({"limit": limit, "tag": tag})
```
### `apiPost(endpoint, body)`
Sends a POST request to the plugin backend. `body` is sent as JSON and the method returns `Promise<data>`.
Sends a POST JSON request.
```js
await bridge.apiPost("settings/save", {
const result = await bridge.apiPost("settings/save", {
enabled: true,
threshold: 0.8,
});
```
Backend:
```python
async def save_settings(self):
payload = await request.json(default={})
return json_response({"saved": True, "enabled": payload.get("enabled")})
```
### `upload(endpoint, file)`
Uploads one file as `multipart/form-data` with the field name `file`, returning `Promise<data>`.
Uploads one file as `multipart/form-data`. The field name is always `file`.
```js
const input = document.querySelector("input[type=file]");
@@ -382,14 +447,48 @@ const file = input.files[0];
const result = await bridge.upload("files/import", file);
```
Backend:
```python
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})
```
If you need extra structured fields, send them through a separate `apiPost` call or use query parameters to select import behavior. The current `upload()` bridge method sends one 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.
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 read it from response headers.
```js
await bridge.download("files/export", { format: "json" }, "export.json");
```
Backend:
```python
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()` resolves to:
```json
{ "filename": "export.json" }
```
### `subscribeSSE(endpoint, handlers, params)`
Subscribes to plugin backend SSE and returns `Promise<subscriptionId>`. `handlers` may include `onOpen`, `onMessage`, and `onError`.
@@ -412,17 +511,25 @@ const subscriptionId = await bridge.subscribeSSE(
);
```
`event.parsed` is automatically parsed when the message is a JSON string; otherwise it equals the raw string.
`event.raw` is the raw string. If the message is a JSON string, `event.parsed` is parsed automatically; otherwise it equals the raw string. `event.eventType` matches the SSE `event:` field and defaults to `message`.
### `unsubscribeSSE(subscriptionId)`
The backend must return `text/event-stream`:
Cancels an SSE subscription.
```python
async def events(self):
async def stream():
yield 'data: {"message": "ready"}\n\n'
return stream_response(stream())
```
Unsubscribe:
```js
await bridge.unsubscribeSSE(subscriptionId);
```
Clean up subscriptions when the page unloads:
Clean up on unload:
```js
window.addEventListener("beforeunload", () => {
@@ -430,11 +537,116 @@ window.addEventListener("beforeunload", () => {
});
```
### endpoint Rules
## Page Internationalization
The `endpoint` used by `apiGet`, `apiPost`, `upload`, `download`, and `subscribeSSE` must be a plugin-local relative path:
Plugin Pages reuse plugin i18n resource files. Add `pages.<page_name>` to `.astrbot-plugin/i18n/<locale>.json`:
- Allowed: `"stats"`, `"settings/save"`, `"files/export"`
- Not allowed: empty string, `"/stats"`, `"../stats"`, `"https://example.com"`, `"stats?x=1"`, `"stats#top"`
```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"
}
}
}
```
Pass query parameters through `params`; do not append them to `endpoint`.
`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. Inside the Page, render text with `bridge.t()` and react to locale changes with `onContext()`.
```js
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);
```
## Light/Dark Theme
AstrBot syncs the current theme to Plugin Pages. The bridge SDK maintains a `data-theme` attribute on `<html>`:
- Light mode: `<html data-theme="light">`
- Dark mode: `<html data-theme="dark">`
When **Follow System** is selected, the Page still receives either `light` or `dark`.
CSS variables are recommended:
```css
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #1a1a1a;
--text: #e0e0e0;
}
body {
background: var(--bg);
color: var(--text);
}
```
The server injects `data-theme` into returned HTML to reduce initial flashing. If JavaScript needs to react to theme changes, read `bridge.getContext()?.isDark` and listen with `onContext()`.
## Static Asset Paths
Use normal relative paths:
```html
<link rel="stylesheet" href="./style.css" />
<script type="module" src="./app.js"></script>
<img src="./assets/logo.svg" alt="" />
```
AstrBot rewrites relative asset URLs and appends a short-lived `asset_token`. Do not hardcode `/api/plugin/page/content/...`, append `asset_token` yourself, or rely on `..` to escape the Page root.
AstrBot rewrites:
- HTML `src` and `href`
- CSS `url(...)`
- JavaScript `import`
- JavaScript `export ... from`
- JavaScript dynamic `import()`
If you build a SPA, prefer hash routing. The static asset server resolves real file paths; with history routing, refreshing a page requires a real file 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 the parent DOM, and it cannot bypass the bridge to reuse Dashboard auth. All operations that need Dashboard identity should go through the bridge.
Asset responses include security headers such as:
- `X-Frame-Options: SAMEORIGIN`
- `Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'`
- `Cache-Control: no-store`
- `X-Content-Type-Options: nosniff`
Backend handlers must still validate input. Do not trust paths, filenames, formats, or numeric ranges sent by the Page. Store files only in safe directories and prefer whitelisted or regenerated filenames.
## Debugging Tips
- Page is missing: check that `pages/<page_name>/index.html` exists, the plugin is enabled, and the plugin detail page has been refreshed.
- Bridge is missing: make sure your script runs after the bridge SDK is injected; external `type="module"` scripts are recommended.
- API is not matched: make sure the registered route includes the plugin name prefix, such as `/{PLUGIN_NAME}/stats`, while the Page endpoint is `stats`.
- Query or JSON is empty: pass GET values through `apiGet(endpoint, params)` and POST JSON through `apiPost(endpoint, body)`.
- Upload is empty: `upload()` always uses the field name `file`; read it with `(await request.files()).get("file")`.
- SSE has no messages: make sure the backend response is `text/event-stream` and each message ends with a blank line, such as `data: ...\n\n`.
- SSE returns 401: do not call `new EventSource("/api/v1/...")` directly from the Page. Native `EventSource` cannot send the `Authorization` header; call through `bridge.subscribeSSE()` instead.

View File

@@ -4,7 +4,12 @@ After completing your plugin development, you can choose to publish it to the As
AstrBot uses GitHub to host plugins, so you'll need to push your plugin code to the GitHub plugin repository you created earlier.
You can submit your plugin by visiting the [AstrBot Plugin Marketplace](https://plugins.astrbot.app). Once on the website, click the `+` button in the bottom-right corner, fill in the basic information, author details, repository information, and other required fields. Then click the `Submit to GITHUB` button. You will be redirected to the AstrBot repository's Issue submission page. Please verify that all information is correct, then click the `Create` button to complete the plugin publication process.
You can submit your plugin by visiting the [AstrBot Plugin Marketplace](https://plugins.astrbot.app). Once on the website, click the `+` button in the bottom-right corner, fill in the basic information, author details, repository information, and other required fields. Then click the `Submit to GITHUB` button.
> [!WARNING]
> **Main repository Issue submission is deprecated**: The previous method of submitting plugins via Issues in the AstrBot main repository is no longer used. Please submit your plugin at the **[AstrBot_Plugins_Collection](https://github.com/AstrBotDevs/AstrBot_Plugins_Collection)** repository.
You will be redirected to the AstrBot_Plugins_Collection repository's Issue submission page. Please verify that all information is correct, then click the `Create` button to complete the plugin publication process.
![fill out the form](https://files.astrbot.app/docs/source/images/plugin-publish/image.png)

View File

@@ -27,7 +27,9 @@ Set dashboard.host in data/cmd_config.json to enable remote access.
### Forgot Dashboard Password
If you forgot your AstrBot dashboard password, find the `"dashboard"` field in `AstrBot/data/cmd_config.json`, for example:
If you forgot your AstrBot dashboard password, you can use the CLI tool `astrbot password` to change the password.
Another approach you can take is to find the `"dashboard"` field in `AstrBot/data/cmd_config.json`, for example:
```json
"dashboard": {
@@ -100,7 +102,7 @@ After restart, AstrBot will reload or download WebUI files that match the curren
### No Permission to Execute Admin Commands
1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` are the default admin commands. You can use the `/sid` command to get a user's ID, then add it to the admin ID list in Settings -> Other Settings.
1. `/name, /provider, /dashboard_update, /op, /deop, /persona, /llm, /plugin, /model, /groupnew` are the default admin commands. You can use the `/sid` command to get a user's ID, then add it to the admin ID list in Settings -> Other Settings.
### Chinese Characters Garbled When Locally Rendering Markdown Images (t2i)

View File

@@ -2,8 +2,8 @@
> [!WARNING]
> 1. QQ Official Bot currently requires an IP whitelist.
> 2. It supports group chat, private chat, channel chat, and channel private chat.
> 3. You need a server with a public IP and a domain.
> 2. Webhook mode requires a server with a public IP, a domain, and HTTPS access.
> 3. It supports group chat, private chat, channel chat, and channel private chat.
## Supported Basic Message Types
@@ -19,7 +19,43 @@
Proactive message push: Supported.
## Apply for a Bot
## Create a QQ Bot in AstrBot with One-click QR Setup (Recommended)
### Setup Flow
1. In AstrBot WebUI, click `Bots` in the left sidebar, then click `+ Create Bot`.
2. Select `QQ Official Bot (Webhook)`.
3. Under `Choose setup method`, select `One-click QR setup`, click start, then scan the QR code with mobile QQ.
4. After confirming the QR binding, click `Save`.
5. Configure DNS and a reverse proxy for your server so HTTPS requests are forwarded to AstrBot's `6185` port.
6. Go back to the QQ Open Platform bot management page, open `Development -> Callback Configuration`, and enter the Webhook callback URL generated by AstrBot.
7. Select the callback events you need. To receive full group messages, make sure the group event `GROUP_MESSAGE_CREATE` is selected.
8. Save the callback configuration, then restart AstrBot.
> [!TIP]
> With `Unified Webhook Mode`, AstrBot generates a unique Webhook callback URL automatically. You can find it in the logs or on the bot card in WebUI.
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
### Use in Group Chats
#### Add to a Group Chat
Open the created QQ bot profile page (mobile QQ -> Contacts -> Bots tab). You can find `Add to group chat` near the bottom. Currently, the bot can only be added to groups where you are the group owner.
#### Set Message Access Scope and Proactive Speaking
In mobile QQ group settings, open the bot settings page. We recommend setting `Messages the bot can access` to `All group messages`, and enabling `Allow the bot to proactively speak in the group`.
With this configuration, the bot can receive full group messages and proactively push messages to the group, such as scheduled task notifications and plugin notifications.
Webhook mode also requires selecting the group event `GROUP_MESSAGE_CREATE` in QQ Open Platform callback configuration. Otherwise, AstrBot cannot receive full group message events.
![QQ Official Bot recommended group chat settings](/qqofficial-group-recommended-config.png)
## Manually Apply for a QQ Bot (Not Recommended)
### Apply for a Bot
Open [QQ Official Bot](https://q.qq.com) and sign in.
@@ -29,7 +65,7 @@ Open the created bot to enter its management page:
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## Allow Bot in Channel / Group / Private Chat
### Allow Bot in Channel / Group / Private Chat
Open `Sandbox Configuration` to set a sandbox channel / QQ group / QQ private chat (up to 20 members).
@@ -37,41 +73,62 @@ Then configure QQ groups, private chat QQ accounts, and QQ channels as needed.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## Get `appid` and `secret`
### Get `appid` and `secret`
After adding the bot where you need it, open `Development -> Development Settings`, then copy `appid` and `secret`.
## Add IP Whitelist
If you use AstrBot WebUI's `One-click QR setup`, you can skip this step. AstrBot fills in `appid` and `secret` automatically after QR binding succeeds.
### Add IP Whitelist
Open `Development -> Development Settings`, find IP whitelist, and add your server IP.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png)
## Configure in AstrBot
> [!TIP]
> If you do not know your server IP, run `curl ifconfig.me` or check [ip138.com](https://ip138.com/).
>
> In NAT environments without a public IP, the observed IP may change depending on your carrier. Use proxy/tunnel if needed.
### Configure in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `qq_official_webhook`.
4. Select `QQ Official Bot (Webhook)`.
Fill in:
Recommended: use `One-click QR setup`.
1. Under `Choose setup method`, select `One-click QR setup`.
2. Click start, then scan and confirm the QR code with mobile QQ.
3. Wait until the page shows binding success. AstrBot fills in `appid` and `secret` automatically.
4. Keep `Unified Webhook Mode` enabled, adjust `ID` and other options as needed, then click `Save`.
If QR setup is unavailable, choose `Manual setup` and fill in:
- ID (`id`): any unique identifier.
- Enable (`enable`): checked.
- `appid`: from QQ Official Bot platform.
- `secret`: from QQ Official Bot platform.
- Unified Webhook Mode (`unified_webhook_mode`): keep enabled.
Click `Save`.
## Configure Callback URL
### Configure Reverse Proxy
In `Development -> Callback Configuration`, configure callback URL.
After saving, configure DNS and reverse proxy for your server. Forward requests to AstrBot's `6185` port. If `Unified Webhook Mode` is disabled, forward requests to the port configured in the previous step instead.
Set request URL to `<your-domain>/astrbot-qo-webhook/callback`.
The Webhook callback URL must be reachable from QQ Open Platform over the public internet and must use HTTPS.
Your domain should reverse-proxy traffic to AstrBot port `6196` using `Caddy`, `Nginx`, or `Apache`.
### Configure Callback URL and Events
Then add callback events and select all four event categories (private, group, channel, etc.).
Open `Development -> Callback Configuration`.
After you save the bot in AstrBot, AstrBot generates a unique Webhook callback URL. You can find it in the logs or on the bot card in WebUI.
Use that URL as the request URL.
Then add callback events. To receive full group messages, select the group event `GROUP_MESSAGE_CREATE`; also select private chat events, channel events, and other events as needed.
![image](https://files.astrbot.app/docs/source/images/webhook/image.png)
@@ -79,10 +136,6 @@ After entering values, move focus out of the input box to trigger validation. If
Then restart AstrBot.
## Done
AstrBot should now be connected. If messages do not respond immediately, wait 1-2 minutes, restart AstrBot, and test again.
## Appendix: Reverse Proxy Setup
If you are new to reverse proxy, Caddy is recommended:

View File

@@ -14,26 +14,37 @@
Proactive message push: Supported.
## Quick Deployment Steps
## Create a QQ Bot in AstrBot with One-click QR Setup (Recommended)
> Updated: `2026/03/06`. This method only supports `private chat`.
### Setup Flow
1. Open [QQ Open Platform](https://q.qq.com/qqbot/openclaw/). Register an account if you don't have one.
2. Click the `Create Bot` button on the right.
3. Obtain your `AppID` and `AppSecret`.
4. In AstrBot WebUI, click `Bots` in the left sidebar, then click `+ Create Bot`, select `QQ Official Bot (WebSocket)`, paste the `AppID` and `AppSecret` into the form, click `Enable`, then click `Save`.
1. In AstrBot WebUI, click `Bots` in the left sidebar, then click `+ Create Bot`.
2. Select `QQ Official Bot (WebSocket)`.
3. Under `Choose setup method`, select `One-click QR setup`, click start, then scan the QR code with mobile QQ.
4. After you confirm the QR binding, AstrBot automatically fills in `AppID` and `AppSecret`. Make sure `Enable` is checked, then click `Save`.
5. Back on the QQ Open Platform page, click `Scan QR Code to Chat` next to your bot, then scan with your mobile QQ to start chatting.
To use the bot in group chats, refer to the `Allow Bot in Channel / Group / Private Chat` section below.
### Use in Group Chats
---
#### Add to a Group Chat
## Apply for a Bot
Open the created QQ bot profile page (mobile QQ -> Contacts -> Bots tab). You can find `Add to group chat` near the bottom. Currently, the bot can only be added to groups where you are the group owner.
#### Set Message Access Scope and Proactive Speaking
In mobile QQ group settings, open the bot settings page. We recommend setting `Messages the bot can access` to `All group messages`, and enabling `Allow the bot to proactively speak in the group`.
With this configuration, the bot can receive full group messages and proactively push messages to the group, such as scheduled task notifications and plugin notifications.
![QQ Official Bot recommended group chat settings](/qqofficial-group-recommended-config.png)
## Manually Apply for a QQ Bot (Not Recommended)
### Apply for a Bot
> [!WARNING]
> 1. QQ Official Bot currently requires an IP whitelist.
> 2. It supports group chat, private chat, channel chat, and channel private chat.
> 3. Tencent is phasing out Websockets access, so this method is no longer recommended. Please use [Webhook](/en/platform/qqofficial/webhook) instead.
Open [QQ Official Bot](https://q.qq.com) and sign in.
@@ -43,7 +54,7 @@ Open the created bot to enter its management page:
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## Allow Bot in Channel / Group / Private Chat
### Allow Bot in Channel / Group / Private Chat
Open `Sandbox Configuration` to set a sandbox channel / QQ group / QQ private chat (up to 20 members).
@@ -51,11 +62,13 @@ Then configure QQ groups, private chat QQ accounts, and QQ channels as needed.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## Get `appid` and `secret`
### Get `appid` and `secret`
After adding the bot where you need it, open `Development -> Development Settings`, then copy `appid` and `secret`.
## Add IP Whitelist
If you use AstrBot WebUI's `One-click QR setup`, you can skip this step. AstrBot fills in `appid` and `secret` automatically after QR binding succeeds.
### Add IP Whitelist
Open `Development -> Development Settings`, find IP whitelist, and add your server IP.
@@ -66,22 +79,27 @@ Open `Development -> Development Settings`, find IP whitelist, and add your serv
>
> In NAT environments without a public IP, the observed IP may change depending on your carrier. Use proxy/tunnel if needed.
## Configure in AstrBot
### Configure in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `qq_official`.
Fill in:
Recommended: use `One-click QR setup`.
1. Under `Choose setup method`, select `One-click QR setup`.
2. Click start, then scan and confirm the QR code with mobile QQ.
3. Wait until the page shows binding success. AstrBot fills in `appid` and `secret` automatically.
4. Adjust `ID`, `Enable group/C2C message list`, `Enable guild direct message`, and other options as needed, then click `Save`.
If QR setup is unavailable, choose `Manual setup` and fill in:
- ID (`id`): any unique identifier.
- Enable (`enable`): checked.
- `appid`: from QQ Official Bot platform.
- `secret`: from QQ Official Bot platform.
- Enable group/C2C message list (`enable_group_c2c`): keep enabled if you need QQ message-list private chat.
- Enable guild direct message (`enable_guild_direct_message`): keep enabled if you need guild direct messages.
Click `Save`.
## Done
AstrBot should now be connected. Send `/help` to the bot in QQ private chat to verify.

View File

@@ -18,6 +18,8 @@ The following commands are shipped with AstrBot and loaded by default:
- `/reset`: Reset the current conversation's LLM context.
- `/stop`: Stop Agent tasks currently running in the current session.
- `/new`: Create and switch to a new conversation.
- `/stats`: View token usage statistics for the current conversation.
- `/provider`: View or switch LLM Provider. This command requires admin permission.
- `/dashboard_update`: Update AstrBot WebUI. This command requires admin permission.
- `/set`: Set a session variable, commonly used for Agent Runner input variables such as Dify, Coze, or DashScope.
- `/unset`: Remove a session variable.
@@ -102,6 +104,45 @@ For third-party Agent Runners such as `dify`, `coze`, `dashscope`, and `deerflow
If there are no running tasks in the current session, AstrBot will report that no task is running.
### `/stats`
`/stats` shows token usage statistics for the current conversation.
It queries the database for all Provider call records in the current conversation and displays:
- Total tokens (input + output).
- Input tokens (cached) — input tokens that were cached by the provider and skipped for billing.
- Input tokens (other) — input tokens that were not cached and billed normally.
- Output tokens — tokens generated by the model.
If you are not in a conversation, AstrBot will prompt you to create one with `/new`.
### `/provider`
`/provider` views or switches the Provider (LLM / TTS / STT) used by the current UMO.
**Viewing the Provider list:**
With no arguments, `/provider` lists all configured Providers grouped by LLM, TTS, and STT. Each Provider shows:
- An index number for switching.
- Provider ID and the model currently in use (LLM type).
- Reachability status: `✅` means the connection is healthy, `❌` means a connection failure (with an error code).
- The currently active Provider is marked with `(currently in use)` at the end.
> [!NOTE]
> Reachability checks must be enabled in WebUI under `Config -> General Config -> AI Config`, expand the "More Settings" section at the bottom, and enable "Provider Reachability Check". When disabled, reachability markers are not shown and the list loads faster.
**Switching Providers:**
Use `/provider <index>` to switch the current session's LLM Provider to the Provider at the given index in the list.
- `/provider <index>`: Switch to the LLM Provider at the given index.
- `/provider tts <index>`: Switch to the TTS Provider at the given index.
- `/provider stt <index>`: Switch to the STT Provider at the given index.
This command requires admin permission.
## Built-in Commands Extension
Other commands that were previously shipped with the core have been moved to a separate plugin:

View File

@@ -19,8 +19,32 @@ Open the AstrBot admin panel, navigate to the `Plugins` page, and find `Skills`.
You can upload Skills with the following requirements:
1. The upload must be a `.zip` archive.
2. **After extraction, it must contain a single Skill folder. The folder name will be used as the identifier for the Skill in AstrBot—please name it using English characters.**
3. The Skill folder must include a file named `SKILL.md`, and its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/en/skills
2. After extraction, it can contain one or more Skill folders. Each folder name is used as the Skill identifier in AstrBot. Use English letters, numbers, dots, underscores, or hyphens.
3. Each Skill folder must include a file named exactly `SKILL.md`. The filename is case-sensitive. Its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/en/skills
## Skill Sources and Priority
AstrBot can discover Skills from several places:
- **Local Skills**: uploaded from the WebUI or placed under `data/skills/<skill_name>/SKILL.md`. These appear in the WebUI Skills management page.
- **Plugin-provided Skills**: plugins can bundle Skills in their own `skills/` directory. They appear in the WebUI, but are managed by the plugin, so they cannot be deleted or edited from the Local Skills page.
- **Sandbox preset Skills**: when the sandbox runtime is used, AstrBot reads Skills discovered inside the sandbox and provides them to the Agent.
- **Workspace Skills**: Skills under the current session workspace, at `skills/<skill_name>/SKILL.md`. They are currently injected only in local runtime, where the path is usually `data/workspaces/{normalized_umo}/skills/<skill_name>/SKILL.md`.
Workspace Skills are **request-scoped**. In local runtime, when AstrBot builds a request, it checks the current session workspace for a `skills/` directory and appends valid Skills to that request's Skill inventory. They are not shown in the WebUI Skills management page yet, and they are not written to the global Skills configuration.
If a persona is configured to select specific Skills, that list filters only local, plugin-provided, and sandbox Skills. Workspace Skills are still discovered and injected as part of the current request. Workspace Skills are disabled only when the persona is explicitly configured to use no Skills.
When multiple sources contain a Skill with the same name, request-time priority is:
1. If the current persona is explicitly configured to use no Skills, no Skills are injected, including Workspace Skills.
2. If the current persona selects a specific Skill list, that list does not filter Workspace Skills.
3. The current session's Workspace Skill has the highest priority. If it has the same name as a local, plugin, or sandbox Skill, it overrides that Skill for the current request only.
4. Local Skills take priority over plugin-provided Skills and sandbox-only Skills.
5. Plugin-provided Skills take priority over sandbox-only Skills.
6. Sandbox-only Skills are injected only when there is no local, plugin, or workspace Skill with the same name.
If a local Skill has been synced into the sandbox, AstrBot treats it as the same Skill. In sandbox runtime, the request will prefer the path that is readable inside the sandbox. Workspace Skills are not automatically synced into the sandbox yet.
## Using Skills in AstrBot

View File

@@ -14,11 +14,11 @@ When using a large language model that supports function calling with the web se
And other prompts with search intent to trigger the model to invoke the search tool.
AstrBot currently supports 4 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, and `Brave`.
AstrBot currently supports 6 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, and `Exa`.
![image](https://files.astrbot.app/docs/source/images/websearch/image.png)
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, or `Brave`.
Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, `Brave`, `Firecrawl`, or `Exa`.
### Tavily
@@ -36,6 +36,14 @@ Get an API Key from Baidu Qianfan APP Builder, then fill it in the corresponding
Get an API Key from Brave Search, then fill it in the corresponding configuration item.
### Firecrawl
Go to [Firecrawl](https://firecrawl.dev) to get an API Key, then fill it in the corresponding configuration item.
### Exa
Go to [Exa](https://dashboard.exa.ai) to get an API Key, then fill it in the corresponding configuration item. Exa is an AI-native search engine that supports keyword and semantic search with category filters, domain restrictions, and date ranges.
If you use Tavily as your web search source, you will get a better experience optimization on AstrBot ChatUI, including citation source display and more:
![](https://files.astrbot.app/docs/source/images/websearch/image1.png)

View File

@@ -119,3 +119,6 @@ Modify the `port` in the `dashboard` configuration in the data/cmd_config.json f
## Forgot Password
Modify the `password` in the `dashboard` configuration in the data/cmd_config.json file and delete the entire password key-value pair.
> [!TIP]
> For more details, see [FAQ - Forgot Dashboard Password](/en/faq.md#forgot-dashboard-password).

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
import yaml
REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_SPEC = REPO_ROOT / "openspec" / "openapi-v1.yaml"
DEFAULT_OUTPUT = REPO_ROOT / "docs" / "public" / "openapi.json"
PUBLIC_OPEN_API_TAGS = {
"Open API",
"System Config",
"Config Profiles",
"Bot Config Routes",
"Bots",
"Provider Sources",
"Providers",
"Chat",
"IM",
"Files",
"Plugins",
"Plugin Sources",
"Plugin Pages",
"MCP",
"Skills",
"Personas",
"T2I",
"Subagents",
}
PUBLIC_OPEN_API_EXCLUDED_PATHS = {
"/api/v1/live-chat/ws",
"/api/v1/unified-chat/ws",
}
COMPONENT_REF_PREFIX = "#/components/"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Update the public OpenAPI JSON document from the v1 YAML spec."
)
parser.add_argument(
"--spec",
type=Path,
default=DEFAULT_SPEC,
help=f"OpenAPI YAML source path. Default: {DEFAULT_SPEC}",
)
parser.add_argument(
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"OpenAPI JSON output path. Default: {DEFAULT_OUTPUT}",
)
return parser.parse_args()
def load_yaml(path: Path) -> dict[str, Any]:
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"Expected OpenAPI object in {path}")
return data
def iter_refs(value: Any):
"""Yield local component refs from an OpenAPI value.
Args:
value: Arbitrary OpenAPI object value.
Yields:
Local component reference strings.
"""
if isinstance(value, dict):
ref = value.get("$ref")
if isinstance(ref, str) and ref.startswith(COMPONENT_REF_PREFIX):
yield ref
for child in value.values():
yield from iter_refs(child)
elif isinstance(value, list):
for child in value:
yield from iter_refs(child)
def parse_component_ref(ref: str) -> tuple[str, str] | None:
"""Parse a local component ref into its section and name.
Args:
ref: OpenAPI local component reference.
Returns:
The component section and name, or None if the ref is not a component ref.
"""
if not ref.startswith(COMPONENT_REF_PREFIX):
return None
rest = ref.removeprefix(COMPONENT_REF_PREFIX)
if "/" not in rest:
return None
section, name = rest.split("/", 1)
return section, name
def filter_public_openapi(spec: dict[str, Any]) -> dict[str, Any]:
"""Filter the full v1 spec down to developer API key endpoints.
Args:
spec: Full OpenAPI spec loaded from the YAML source.
Returns:
A filtered OpenAPI spec for the public docs site.
"""
output = dict(spec)
output["tags"] = [
tag
for tag in spec.get("tags", [])
if isinstance(tag, dict) and tag.get("name") in PUBLIC_OPEN_API_TAGS
]
paths = {}
for path, methods in spec.get("paths", {}).items():
if path in PUBLIC_OPEN_API_EXCLUDED_PATHS:
continue
kept_methods = {
method: operation
for method, operation in methods.items()
if any(tag in PUBLIC_OPEN_API_TAGS for tag in operation.get("tags", []))
}
if kept_methods:
paths[path] = kept_methods
output["paths"] = paths
used_refs: dict[str, set[str]] = {}
pending = list(iter_refs(paths))
components = output.get("components", {})
while pending:
parsed = parse_component_ref(pending.pop())
if parsed is None:
continue
section, name = parsed
used_names = used_refs.setdefault(section, set())
if name in used_names:
continue
used_names.add(name)
component = components.get(section, {}).get(name)
pending.extend(iter_refs(component))
pruned_components = {}
for section, values in components.items():
if section == "securitySchemes":
pruned_components[section] = values
continue
if not isinstance(values, dict):
pruned_components[section] = values
continue
names = used_refs.get(section, set())
kept_values = {name: values[name] for name in values if name in names}
if kept_values:
pruned_components[section] = kept_values
output["components"] = pruned_components
return output
def main() -> int:
args = parse_args()
spec_path = args.spec.resolve()
output_path = args.output.resolve()
spec = load_yaml(spec_path)
spec = filter_public_openapi(spec)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(
json.dumps(spec, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(
f"Updated {output_path.relative_to(REPO_ROOT)} from {spec_path.relative_to(REPO_ROOT)}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,8 +1,8 @@
import sys
import unittest
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
import unittest
def load_sync_module():

View File

@@ -6,17 +6,19 @@
### QQ 群
- 12: 916228568 (新)
- 9: 1076659624 (人满)
- 10 群: 1078079676 (人满)
- 11 群: 704659519 (人满)
- 1: 322154837 (人满)
- 3: 630166526 (人满)
- 4: 1077826412 (人满)
- 5: 822130018 (人满)
- 6 群: 753075035 (人满)
- 7 群: 743746109 (人满)
- 8 群: 1030353265 (人满)
- 1 群322154837 (人满)
- 3630166526 (人满)
- 4 群1077826412 (人满)
- 5 群822130018 (人满)
- 6753075035 (人满)
- 7743746109 (人满)
- 81030353265 (人满)
- 91076659624 (人满)
- 10 群1078079676 (人满)
- 11 群704659519 (人满)
- 12 群916228568 (人满)
- 13 群1092185289
- 14 群1103419483
- **AstrBot 核心开发交流群: 975206796**AstrBot 开发成员通常活跃于此,欢迎任何对编程/AI 技术感兴趣的同学加入~
### Discord

View File

@@ -288,12 +288,13 @@ ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。
#### `provider_settings.websearch_provider`
网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily``bocha``baidu_ai_search``brave`
网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily``bocha``baidu_ai_search``brave``firecrawl`
- `tavily`:使用 Tavily 搜索引擎。
- `bocha`:使用 BoCha 搜索引擎。
- `baidu_ai_search`:使用百度 AI SearchMCP
- `brave`:使用 Brave Search API。
- `firecrawl`:使用 Firecrawl Search API。
#### `provider_settings.websearch_tavily_key`
@@ -307,6 +308,10 @@ BoCha 搜索引擎的 API Key 列表。使用 `bocha` 作为网页搜索提供
Brave 搜索引擎的 API Key 列表。使用 `brave` 作为网页搜索提供商时需要填写。
#### `provider_settings.websearch_firecrawl_key`
Firecrawl 搜索引擎的 API Key 列表。使用 `firecrawl` 作为网页搜索提供商时需要填写。
#### `provider_settings.web_search_link`
是否在回复中提示模型附上搜索结果的链接。默认为 `false`

View File

@@ -26,19 +26,31 @@ X-API-Key: abk_xxx
- `POST /api/v1/chat`:请求体必须包含 `username`
- `GET /api/v1/chat/sessions`:查询参数必须包含 `username`
本地 OpenAPI 描述文件地址为 `http://localhost:6185/api/v1/openapi.json`,交互式文档地址为 `http://localhost:6185/api/v1/docs`
## Scope 权限说明
创建 API Key 时可配置 `scopes`。每个 scope 控制可访问的接口范围:
| Scope | 作用 | 可访问接口 |
| --- | --- | --- |
| `chat` | 调用对话能力、查询对话会话 | `POST /api/v1/chat``GET /api/v1/chat/sessions` |
| `config` | 获取可用配置文件列表 | `GET /api/v1/configs` |
| `file` | 上传附件文件,获取 `attachment_id` | `POST /api/v1/file` |
| `bot` | 管理机器人/平台配置 | `GET /api/v1/bot-types``GET/POST /api/v1/bots``PATCH /api/v1/bots/enabled` |
| `provider` | 管理模型提供商和提供商源 | `GET/POST /api/v1/providers``GET/PUT/DELETE /api/v1/provider-sources/by-id` |
| `persona` | 管理人格和人格文件夹 | `GET/POST /api/v1/personas``GET/POST /api/v1/persona-folders` |
| `im` | 主动发 IM 消息、查询 bot/platform 列表 | `POST /api/v1/im/message``GET /api/v1/im/bots` |
| `config` | 管理配置文件、系统配置和通用配置。该 scope 同时包含 `bot``provider` 访问权限。 | `GET /api/v1/configs``GET/PUT /api/v1/system-config``GET/POST /api/v1/config-profiles` |
| `chat` | 调用对话能力、查询对话会话 | `POST /api/v1/chat``GET /api/v1/chat/sessions` |
| `file` | 上传和下载对话附件 | `POST /api/v1/file``GET /api/v1/file``POST /api/v1/files` |
| `plugin` | 管理插件、插件配置、插件源和插件市场 | `GET /api/v1/plugins``GET/PUT /api/v1/plugins/config``POST /api/v1/plugins/install/url` |
| `mcp` | 管理 MCP 服务器配置和服务端同步 | `GET/POST /api/v1/mcp/servers``PATCH /api/v1/mcp/servers/{server_name}/enabled``POST /api/v1/mcp/providers/modelscope/sync` |
| `skill` | 管理 Skills、Skill 压缩包、Skill 文件和 Shipyard Neo Skill 流程 | `GET/POST /api/v1/skills``PUT /api/v1/skills/{skill_name}/files/{file_path}``POST /api/v1/skills/neo/sync` |
如果 API Key 未包含目标接口所需 scope请求会返回 `403 Insufficient API key scope`
`config` 是较大的管理 scope。创建 API Key 时如果包含 `config`AstrBot 会同时授予该 Key `config``bot``provider` 访问权限。WebUI 的勾选逻辑也会体现这个依赖关系:选中 `config` 会同时选中 `bot``provider`;取消选中 `bot``provider` 时,会同步取消 `config`
当前开发者 API Key 仅开放以上 10 个 scope。`tool``skills``kb``data``system` 暂不支持作为开发者 API Key scope。`/api/v1/skills/*` 接口使用单数 `skill` scope不使用复数 `skills`。公开 OpenAPI 文档只包含这些开发者 API Key scope 覆盖的接口。
## 常用接口
**对话类**
@@ -48,10 +60,21 @@ X-API-Key: abk_xxx
- `POST /api/v1/chat`发送对话消息SSE 流式返回,不传 `session_id` 会自动创建 UUID
- `GET /api/v1/chat/sessions`:分页获取指定 `username` 的会话
- `GET /api/v1/configs`:获取可用配置文件列表
- `POST /api/v1/file`:上传附件,之后可在消息段中引用
**文件上传**
**机器人和模型提供商**
- `POST /api/v1/file`:上传附件
- `GET /api/v1/bots`:获取机器人/平台配置列表
- `POST /api/v1/bots`:创建机器人/平台配置
- `GET /api/v1/providers`:获取模型提供商配置列表
- `GET /api/v1/provider-sources`:获取提供商源配置列表
**人格、插件、MCP 和 Skills**
- `GET /api/v1/personas`:获取人格列表
- `GET /api/v1/plugins`:获取插件列表
- `GET /api/v1/mcp/servers`:获取 MCP 服务器列表
- `GET /api/v1/skills`:获取 Skills 列表
**IM 消息发送**
@@ -100,7 +123,7 @@ X-API-Key: abk_xxx
说明:
- `attachment_id` 来自 `POST /api/v1/file` 上传结果
- `attachment_id` 来自已存在的附件记录,或使用 `file` scope 调用 `POST /api/v1/file` 上传附件后的返回值
- `reply` 不能单独作为唯一内容,至少需要一个有实际内容的段(如 `plain/image/file/...`)。
-`reply` 或空内容会返回错误。
@@ -108,6 +131,8 @@ X-API-Key: abk_xxx
`POST /api/v1/chat` 额外需要 `username`,可选 `session_id`(不传会自动创建 UUID
`username` 是调用方声明的 WebChat 用户标识,会作为本次消息的 sender 和会话 owner 进入消息管道,并参与基于 sender ID 的指令权限判断。因此,带有 `chat` scope 的 API Key 应仅发放给可信后端服务。如果需要面向终端用户开放,请在自己的服务端将外部用户映射到受控的 `username`,不要允许客户端直接传入管理员 ID 或其他保留 sender ID。
```json
{
"username": "alice",

View File

@@ -1,6 +1,12 @@
# 插件 Pages
AstrBot 支持插件通过 `pages/` 目录暴露 Dashboard 页面。`pages/` 下的每个一级子目录都是一个独立 Page
插件 Pages 允许插件在 AstrBot WebUI 中提供自己的页面。页面文件放在插件目录的 `pages/` 下,由 Dashboard 以受限 iframe 的方式加载;页面里的脚本通过 `window.AstrBotPluginPage` bridge 和 Dashboard 通信,再由 Dashboard 转发到插件注册的后端 Web API。
如果只是让用户填写少量配置项,优先使用 [`_conf_schema.json`](./plugin-config.md)。Pages 更适合复杂表单、运行状态面板、日志查看、文件上传下载、SSE 实时流、图表和其他需要自定义交互的场景。
## 目录结构
`pages/` 下的每个一级子目录是一个独立 Page。AstrBot 只扫描 `pages/<page_name>/index.html`,没有 `index.html` 的目录会被忽略。
```text
astrbot_plugin_page_demo/
@@ -16,13 +22,65 @@ astrbot_plugin_page_demo/
└─ index.html
```
AstrBot 会扫描 `pages/<page_name>/index.html`;没有 `index.html` 的目录会被忽略
`page_name` 应使用简单目录名,例如 `settings``bridge-demo`。不要使用空目录名、`.``..`、以 `.` 开头的目录名,或包含 `/``\` 的名称
如果只是让用户填写几个配置项,优先使用 [`_conf_schema.json`](./plugin-config.md)。插件 Pages 更适合复杂表单、Dashboard、日志、文件上传下载、SSE 和自定义交互流程
用户可以在 WebUI 的插件页点击插件卡片进入插件详情页,然后打开插件声明的 Pages
一旦注册了 Pages用户可以在AstrBot WebUI 插件页中的插件卡片中,点击插件卡片进入插件详细页面,在插件详细页面中可以看到并进入注册的 Pages。
## 开发流程
## 最小前端示例
1. 在插件目录下创建 `pages/<page_name>/index.html`
2. 在 Page 中通过 `window.AstrBotPluginPage` bridge 调用后端能力。
3.`main.py` 中使用 `context.register_web_api()` 注册插件后端 API。
4. 后端 handler 使用 `astrbot.api.web` 读取请求并返回响应。
5. 新增或删除 Page 目录后重载插件;修改静态资源通常刷新 Page 即可。
## 最小完整示例
### 后端
插件后端推荐使用 `astrbot.api.web`,不要把 FastAPI、Starlette 或 Quart 的原始请求对象作为插件公共 API 暴露给自己的业务代码。
```python
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`
@@ -52,62 +110,218 @@ const context = await bridge.ready();
output.textContent = JSON.stringify(context, null, 2);
document.getElementById("ping").addEventListener("click", async () => {
const result = await bridge.apiGet("ping");
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`
不需要手动引入 bridge SDK。AstrBot 返回 HTML 时会自动插入 `/api/plugin/page/bridge-sdk.js`如果内联脚本必须同步访问 `window.AstrBotPluginPage`,请把脚本改成外部 module 文件,或在自己的脚本前显式引入:
## 注册后端 API
前端调用 `bridge.apiGet("ping")`Dashboard 会转发到:
```text
/api/plug/<plugin_name>/ping
```html
<script src="/api/plugin/page/bridge-sdk.js"></script>
```
因此注册 Web API 时,路由必须带上插件名作为前缀:
## 后端 Web API
### 路由注册
使用 `context.register_web_api(route, view_handler, methods, desc)` 注册插件 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"})
context.register_web_api(
f"/{PLUGIN_NAME}/items/<item_id>",
self.get_item,
["GET"],
"Get item",
)
```
路由需要包含插件名作为前缀。Page 端的 bridge endpoint 不需要包含插件名:
```js
await bridge.apiGet("items/123");
```
Dashboard 会把它转发到:
```text
/api/v1/plugins/extensions/<plugin_name>/items/123
```
注册路由 `/<plugin_name>/items/<item_id>` 会匹配该请求,`item_id` 作为 handler 的关键字参数传入:
```python
async def get_item(self, item_id: str):
return json_response({"item_id": item_id})
```
支持的动态片段:
- `<name>`:匹配单个路径片段。
- `<path:name>`:匹配后续多级路径。
### 请求对象
推荐导入:
```python
from astrbot.api.web import request
```
`request` 是当前请求的上下文代理,只能在插件 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 参数使用示例:
```python
limit = request.query.get("limit", 20, type=int)
tags = request.query.getlist("tag")
```
JSON 请求体使用示例:
```python
payload = await request.json(default={})
enabled = bool(payload.get("enabled"))
```
文件上传使用示例:
```python
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
```python
from astrbot.api.web import (
error_response,
file_response,
json_response,
stream_response,
)
```
JSON 响应:
```python
return json_response({"saved": True})
```
错误响应:
```python
return error_response("invalid threshold", status_code=400)
```
文件下载响应:
```python
return file_response(
export_path,
filename="export.json",
content_type="application/json",
)
```
SSE 响应:
```python
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 内部框架解耦。
### Quart 兼容
为了兼容旧插件,通过 `context.register_web_api()` 注册的 handler 仍会进入 Quart 兼容请求上下文。旧代码可以继续使用:
```python
from quart import jsonify, request
```
新插件和新文档推荐使用:
```python
from astrbot.api.web import json_response, request
```
不要在同一个 handler 中混用两个 `request` 代理,迁移时按 handler 逐步替换即可。
## Bridge API
插件 Page 中可直接使用 `window.AstrBotPluginPage`
Page iframe 不能直接访问 Dashboard cookies、LocalStorage 或父页面 DOM。页面脚本必须通过 `window.AstrBotPluginPage` bridge 调用后端和读取上下文。
- `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` 上传单个文件
- `download(endpoint, params, filename)`: 下载后端响应
- `subscribeSSE(endpoint, handlers, params)`: 订阅 SSE
- `unsubscribeSSE(subscriptionId)`: 取消 SSE 订阅
```js
const bridge = window.AstrBotPluginPage;
```
当前 `ready()` 上下文类似:
### 上下文
`ready()` 等待父页面发送初始上下文,返回 `Promise<context>`。页面初始化时应先等待它。
```js
const context = await bridge.ready();
```
上下文通常包含:
```json
{
@@ -121,11 +335,211 @@ class MyPlugin(Star):
}
```
`endpoint` 必须是插件内相对路径,不能为空,不能包含 `\`、URL scheme、query、hash也不能包含 `.``..` 路径片段。
上下文相关 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` | 监听上下文变化,返回取消监听函数 |
监听语言或主题变化:
```js
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
```js
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
```python
return json_response({"message": "pong"})
```
需要表达错误时使用:
```python
return error_response("missing file", status_code=400)
```
Page 端统一捕获错误:
```js
try {
await bridge.apiPost("settings/save", { enabled: true });
} catch (error) {
console.error(error.message);
}
```
### `apiGet(endpoint, params)`
发送 GET 请求。`params` 会作为 query 参数传递。
```js
const stats = await bridge.apiGet("stats", { limit: 20, tag: "today" });
```
后端读取:
```python
async def stats(self):
limit = request.query.get("limit", 20, type=int)
tag = request.query.get("tag")
return json_response({"limit": limit, "tag": tag})
```
### `apiPost(endpoint, body)`
发送 POST JSON 请求。
```js
const result = await bridge.apiPost("settings/save", {
enabled: true,
threshold: 0.8,
});
```
后端读取:
```python
async def save_settings(self):
payload = await request.json(default={})
return json_response({"saved": True, "enabled": payload.get("enabled")})
```
### `upload(endpoint, file)`
`multipart/form-data` 上传单个文件,字段名固定为 `file`
```js
const input = document.querySelector("input[type=file]");
const file = input.files[0];
const result = await bridge.upload("files/import", file);
```
后端读取:
```python
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()` 只发送一个文件。
### `download(endpoint, params, filename)`
请求插件后端文件接口并触发浏览器下载。`params` 会作为 query 参数发送;`filename` 可选,缺省时 bridge 会尝试从响应头读取文件名。
```js
await bridge.download("files/export", { format: "json" }, "export.json");
```
后端返回文件:
```python
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 为:
```json
{ "filename": "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.raw` 是原始字符串;如果内容是 JSON 字符串,`event.parsed` 会自动解析,否则等于原始字符串。`event.eventType` 对应 SSE 的 `event:` 字段,未设置时为 `message`
后端必须返回 `text/event-stream`
```python
async def events(self):
async def stream():
yield 'data: {"message": "ready"}\n\n'
return stream_response(stream())
```
取消订阅:
```js
await bridge.unsubscribeSSE(subscriptionId);
```
页面卸载时建议清理:
```js
window.addEventListener("beforeunload", () => {
bridge.unsubscribeSSE(subscriptionId);
});
```
## Page 国际化
插件 Page 复用插件 i18n 资源文件。给 `.astrbot-plugin/i18n/<locale>.json` 增加 `pages.<page_name>` 即可
插件 Pages 复用插件 i18n 资源文件。给 `.astrbot-plugin/i18n/<locale>.json` 增加 `pages.<page_name>`
```json
{
@@ -140,20 +554,15 @@ class MyPlugin(Star):
}
```
`title` 用于 WebUI 外壳标题和插件详情页的 Page 组件名称`description` 用于插件详情页的 Page 组件描述。
在 Page 内部使用 `t()` 渲染文案,并用 `onContext()` 响应语言切换:
`title` 用于 WebUI 外壳标题和插件详情页的 Page 组件名称`description` 用于插件详情页的 Page 组件描述。Page 内部使用 `bridge.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();
@@ -161,22 +570,14 @@ 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 切换亮色/暗色模式时,会自动将主题状态同步给插件 Page。bridge SDK 会在 `<html>` 元素上维护 `data-theme` 属性:
AstrBot 会把当前主题同步给插件 Page。bridge SDK 会维护 `<html>``data-theme` 属性
- 亮色模式:`<html data-theme="light">`
- 暗色模式:`<html data-theme="dark">`
### CSS 适配
选择“跟随系统”时Page 收到的值仍然是 `light``dark`
推荐使用 CSS 变量:
@@ -197,33 +598,21 @@ body {
}
```
AstrBot 在服务端返回 HTML 时,会自动在 `<html>` 标签上注入 `data-theme` 属性,不会出现初始闪烁
服务端返回 HTML 时会预先注入 `data-theme`,减少初始闪烁。需要在 JavaScript 中响应主题变化时,读取 `bridge.getContext()?.isDark` 并监听 `onContext()`
### JavaScript 响应主题变化
## 静态资源路径
上下文中的 `isDark` 字段表示当前是否为暗色模式。通过 `onContext()` 可以监听主题切换
正常使用相对路径即可
```js
const bridge = window.AstrBotPluginPage;
function render() {
if (bridge.getContext()?.isDark) {
// 暗色模式下的 JS 逻辑(如图表滤镜等)
}
}
await bridge.ready();
render();
bridge.onContext(render);
```html
<link rel="stylesheet" href="./style.css" />
<script type="module" src="./app.js"></script>
<img src="./assets/logo.svg" alt="" />
```
切换亮暗模式时Dashboard 会通过 bridge 将更新后的上下文发送给 iframe只要 Page 监听了 `onContext()`,就会收到通知
AstrBot 会重写相对资源路径并追加短期 `asset_token`。不要手动拼接 `/api/plugin/page/content/...`,不要自行追加 `asset_token`,也不要依赖 `..` 逃逸 Page 根目录
## 静态资源路径规则
AstrBot 会重写相对资源路径,并自动补上短期 `asset_token`。你只需要正常写相对路径,不要自己拼接 `/api/plugin/page/content/...`
AstrBot 会重写:
会被重写的资源引用包括:
- HTML `src``href`
- CSS `url(...)`
@@ -231,9 +620,7 @@ AstrBot 会重写:
- JavaScript `export ... from`
- JavaScript 动态 `import()`
建议把静态资源写成 `./style.css``./assets/logo.svg` 这类相对路径。不要手动追加 `asset_token`,也不要依赖 `..` 逃逸 Page 根目录
如果你构建 SPA建议使用 hash routing。静态资源服务按真实文件路径解析history routing 刷新页面时需要对应路径上真的存在文件。
如果构建 SPA建议使用 hash routing。静态资源服务按真实文件路径解析history routing 刷新页面时需要对应路径上真的存在文件
## 安全约束
@@ -243,198 +630,23 @@ AstrBot 会重写:
allow-scripts allow-forms allow-downloads
```
Page 不能直接访问 Dashboard cookies、LocalStorage 或同源 DOM也不能绕过 bridge 复用 Dashboard auth。
Page 不能直接访问 Dashboard cookies、LocalStorage 或父页面 DOM也不能绕过 bridge 复用 Dashboard auth。所有需要 Dashboard 身份的操作都应该走 bridge。
AstrBot 还会给资源响应添加安全头,包括:
资源响应会带上安全头,包括:
- `X-Frame-Options: SAMEORIGIN`
- `Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'`
- `Cache-Control: no-store`
- `X-Content-Type-Options: nosniff`
后端 handler 仍然要验证输入。不要信任 Page 传来的路径、文件名、格式或数值范围;文件落盘时应使用安全目录,并对文件名做白名单或重新命名。
## 调试建议
- 新增或删除 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": {},
"isDark": false
}
```
### `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`
- 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` 不能携带 `Authorization` header请通过 `bridge.subscribeSSE()` 调用。

View File

@@ -4,7 +4,12 @@
AstrBot 使用 GitHub 托管插件,因此你需要先将插件代码推送到之前创建的 GitHub 插件仓库中。
你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GITHUB` 按钮,你将会被导航到 AstrBot 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布
你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GITHUB` 按钮。
> [!WARNING]
> **主仓库 Issue 提交方式已废弃**:此前通过 AstrBot 主仓库 Issue 提交插件的方式已不再使用。现在请前往 **[AstrBot_Plugins_Collection](https://github.com/AstrBotDevs/AstrBot_Plugins_Collection)** 仓库提交你的插件。
你将会被导航到 AstrBot_Plugins_Collection 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。
![fill out the form](https://files.astrbot.app/docs/source/images/plugin-publish/image.png)

View File

@@ -28,7 +28,9 @@ Set dashboard.host in data/cmd_config.json to enable remote access.
### 管理面板的密码忘记了
如果你忘记了 AstrBot 管理面板的密码,你可以`AstrBot/data/cmd_config.json` 配置文件中找到 `"dashboard"` 字段,如下:
如果你忘记了 AstrBot 管理面板的密码,你可以直接使用CLI工具`astrbot password`来更改密码
另外,你也可以在 `AstrBot/data/cmd_config.json` 配置文件中找到 `"dashboard"` 字段,如下:
```json
"dashboard": {
@@ -118,7 +120,7 @@ Set dashboard.host in data/cmd_config.json to enable remote access.
### 没有权限操作管理员指令
1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` 是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。
1. `/name, /provider, /dashboard_update, /op, /deop, /persona, /llm, /plugin, /model, /groupnew` 是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。
### 本地渲染 Markdown 图片t2i时中文乱码

View File

@@ -1,12 +1,10 @@
# 通过 QQ官方机器人 接入 QQ (Webhook)
> [!WARNING]
>
> 1. 截至目前QQ 官方机器人需要设置 IP 白名单。
> 2. 支持群聊、私聊、频道聊天、频道私聊
>
> **需要**一台带有公网 IP 的服务器和域名(如果没备案,需要服务器在海外或者中国港澳台地区)
> 2. Webhook 模式需要一台带公网 IP 的服务器、域名和 HTTPS 访问能力
> 3. 支持群聊、私聊、频道聊天、频道私聊。
## 支持的基本消息类型
@@ -22,7 +20,43 @@
主动消息推送:支持。
## 申请一个机器人
## 在 AstrBot 中扫码一键创建 QQ 机器人(推荐)
### 配置流程
1. 进入 AstrBot 的 WebUI点击左边栏 `机器人`,然后点击 `+ 创建机器人`
2. 选择 `QQ 官方机器人Webhook`
3.`选择创建方式` 中选择 `扫码一键创建`,点击开始创建后,用手机 QQ 扫描页面中的二维码。
4. 扫码确认后,点击 `保存`
5. 根据服务器环境配置域名 DNS 解析和反向代理,将 HTTPS 请求转发到 AstrBot 所在服务器的 `6185` 端口。
6. 回到 QQ 开放平台的机器人管理页,在 `开发 -> 回调配置` 中填写 AstrBot 生成的 Webhook 回调地址。
7. 在回调事件中勾选需要接收的事件。如果需要接收群聊全量消息,请确保勾选群事件 `GROUP_MESSAGE_CREATE`
8. 保存回调配置后,重启 AstrBot。
> [!TIP]
> 使用 `统一 Webhook 模式` 时AstrBot 会自动生成唯一的 Webhook 回调链接。你可以在日志中,或者 WebUI 的机器人卡片上找到该链接。
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
### 在群聊中使用
#### 添加到群聊
进入创建的 QQ 机器人的资料页手机QQ -> 联系人 -> 机器人页签),在下方可以找到 “添加到群聊”。目前只能添加到自己为群主的群聊。
#### 设置机器人可获取的群聊消息范围和主动发言
在手机 QQ 的群聊设置中打开机器人设置,推荐将 `机器人可获取的群聊消息范围` 设置为 `获取群内全部消息`,并开启 `机器人主动在群聊内发言`
这样机器人可以接收群聊全量消息,也可以在群聊中主动推送消息,例如定时任务推送、插件主动通知等。
Webhook 模式还需要在 QQ 开放平台的回调配置中勾选群事件 `GROUP_MESSAGE_CREATE`,否则 AstrBot 无法收到群聊全量消息事件。
![QQ 官方机器人推荐群聊配置](/qqofficial-group-recommended-config.png)
## 手动申请 QQ 机器人(不推荐)
### 申请一个机器人
首先,打开 [QQ官方机器人](https://q.qq.com) 并登录。
@@ -32,7 +66,7 @@
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## 允许机器人加入频道/群/私聊
### 允许机器人加入频道/群/私聊
点击`沙箱配置`,这允许你立即设置一个沙箱频道/QQ群/QQ私聊用于拉入机器人需要小于等于20个人
@@ -40,26 +74,40 @@
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## 获取 appid、secret
### 获取 appid、secret
添加机器人到你想用的地方后。
点击 `开发->开发设置`,找到 appid、secret。复制并保存它们。
## 添加 IP 白名单
如果你使用 AstrBot WebUI 的 `扫码一键创建`这一步可以跳过。扫码绑定成功后AstrBot 会自动填入 `appid``secret`
### 添加 IP 白名单
点击 `开发->开发设置`,找到 IP 白名单。添加你的服务器 IP 地址。
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png)
## 在 AstrBot 配置
> [!TIP]
> 如果你不知道你的服务器 IP 地址,可以在终端中输入 `curl ifconfig.me` 来获取。或者登录 [ip138.com](https://ip138.com/) 查看。
>
> 如果你在没有公网 IP 的环境下,你看到的 IP 是运营商 NAT 的 IP这个 IP 根据你的运营商的情况可能会随时变化。如有必要,可以配置代理。
### 在 AstrBot 配置
1. 进入 AstrBot 的管理面板
2. 点击左边栏 `机器人`
3. 然后在右边的界面中,点击 `+ 创建机器人`
4. 选择 `qq_official_webhook`
4. 选择 `QQ 官方机器人Webhook`
弹出的配置项填写
推荐使用 `扫码一键创建`
1.`选择创建方式` 中选择 `扫码一键创建`
2. 点击开始创建,用手机 QQ 扫描二维码并确认。
3. 等待页面显示绑定成功。AstrBot 会自动填入 `appid``secret`
4. 保持 `统一 Webhook 模式` 开启,根据需要调整 `ID` 等配置,然后点击 `保存`
如果扫码不可用,也可以选择 `手动创建`。弹出的配置项填写:
- ID(id):随意填写,用于区分不同的消息平台实例。
- 启用(enable): 勾选。
@@ -69,24 +117,21 @@
点击 `保存`
## 反向代理
### 配置反向代理
保存之后,请根据你的服务器环境,配置域名 DNS 解析和反向代理,将请求转发到 AstrBot 所在服务器的 `6185` 端口 (如果没有开启统一 Webhook 模式,将请求转发到上一步配置指定的端口)
保存之后,请根据你的服务器环境,配置域名 DNS 解析和反向代理,将请求转发到 AstrBot 所在服务器的 `6185` 端口如果没有开启统一 Webhook 模式,将请求转发到上一步配置指定的端口
## 设置回调地址
Webhook 回调地址必须可以被 QQ 开放平台公网访问,并且需要使用 HTTPS。
`开发->回调配置` 处,配置回调地址。
### 设置回调地址和事件
`开发 -> 回调配置` 处,配置回调地址。
上一步点击保存之后AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到。
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
将请求地址填写为该地址。
> [!TIP]
> v4.8.0 之前没有 `统一 Webhook 模式`,则请求地址填写 `<你的域名>/astrbot-qo-webhook/callback`。
填写好之后,添加事件,四个事件类型都全选:单聊事件、群事件、频道事件等,如下图。
填写好之后,添加事件。需要接收群聊全量消息时,请勾选群事件 `GROUP_MESSAGE_CREATE`;同时按需勾选单聊事件、频道事件等。
![image](https://files.astrbot.app/docs/source/images/webhook/image.png)
@@ -94,10 +139,6 @@
接着重启 AstrBot。
## 🎉 大功告成
此时,你的 AstrBot 应该已经连接成功。如果发送消息没有反应,请等待一两分钟后重启 AstrBot 再进行确认(测试时发现回调地址不会立即生效)。
## 附录:如何配置反向代理
如果你还没有相关经验,这里推荐使用 Caddy 作为反向代理的工具,请参考:

View File

@@ -15,21 +15,33 @@
主动消息推送:支持。
## 快速部署通道
## 在 AstrBot 中扫码一键创建 QQ 机器人(推荐)
> 更新自: `2026/03/06`。该方法仅支持 `私聊`。
### 配置流程
1. 打开 [QQ 开放平台](https://q.qq.com/qqbot/openclaw/)。如果没注册,需要先注册
2. 点击右侧 `创建机器人` 按钮
3. 获取 `AppID``AppSecret`
4. 进入 AstrBot 的 WebUI点击左边栏 `机器人`,然后在右边的界面中,点击 `+ 创建机器人`,选择 `QQ 官方机器人WebSocket`,将之前得到的 `AppID``AppSecret` 复制到这里的表单中,然后 `启用`,然后点击保存
1. 进入 AstrBot 的 WebUI点击左边栏 `机器人`,然后点击 `+ 创建机器人`
2. 选择 `QQ 官方机器人WebSocket`
3. `选择创建方式` 中选择 `扫码一键创建`,点击开始创建后,用手机 QQ 扫描页面中的二维码
4. 扫码确认后,AstrBot 会自动写入 `AppID``AppSecret`。确认 `启用` 已勾选,然后点击 `保存`
5. 回到 QQ 开放平台页面,点击机器人右边的 `扫码聊天`。用手机 QQ 扫码即可聊天。
如果要在群聊中使用,参考下面文档的 `允许机器人加入频道/群/私聊` 一节。
### 在群聊中使用
---
#### 添加到群聊
## 申请一个机器人
进入创建的 QQ 机器人的资料页手机QQ -> 联系人 -> 机器人页签),在下方可以找到 “添加到群聊”。目前只能添加到自己为群主的群聊。
#### 设置机器人可获取的群聊消息范围和主动发言
在手机 QQ 的群聊设置中打开机器人设置,推荐将 `机器人可获取的群聊消息范围` 设置为 `获取群内全部消息`,并开启 `机器人主动在群聊内发言`
这样机器人可以接收群聊全量消息,也可以在群聊中主动推送消息,例如定时任务推送、插件主动通知等。
![QQ 官方机器人推荐群聊配置](/qqofficial-group-recommended-config.png)
## 手动申请 QQ 机器人(不推荐)
### 申请一个机器人
> [!WARNING]
>
@@ -44,7 +56,7 @@
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## 允许机器人加入频道/群/私聊
### 允许机器人加入频道/群/私聊
点击`沙箱配置`,这允许你立即设置一个沙箱频道/QQ群/QQ私聊用于拉入机器人需要小于等于20个人
@@ -52,13 +64,15 @@
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## 获取 appid、secret
### 获取 appid、secret
添加机器人到你想用的地方后。
点击 `开发->开发设置`,找到 appid、secret。复制并保存它们。
## 添加 IP 白名单(可选)
如果你使用 AstrBot WebUI 的 `扫码一键创建`这一步可以跳过。扫码绑定成功后AstrBot 会自动填入 `appid``secret`
### 添加 IP 白名单(可选)
点击 `开发->开发设置`,找到 IP 白名单。添加你的服务器 IP 地址。
@@ -69,22 +83,27 @@
>
> 如果你在没有公网 IP 的环境下,你看到的 IP 是运营商 NAT 的 IP这个 IP 根据你的运营商的情况可能会随时变化。如有必要,可以配置代理。
## 在 AstrBot 配置
### 在 AstrBot 配置
1. 进入 AstrBot 的管理面板
2. 点击左边栏 `机器人`
3. 然后在右边的界面中,点击 `+ 创建机器人`
4. 选择 `QQ 官方机器人WebSocket`
弹出的配置项填写
推荐使用 `扫码一键创建`
1.`选择创建方式` 中选择 `扫码一键创建`
2. 点击开始创建,用手机 QQ 扫描二维码并确认。
3. 等待页面显示绑定成功。AstrBot 会自动填入 `appid``secret`
4. 根据需要调整 `ID``启用消息列表单聊``启用频道私聊` 等配置,然后点击 `保存`
如果扫码不可用,也可以选择 `手动创建`。弹出的配置项填写:
- ID(id):随意填写,用于区分不同的消息平台实例。
- 启用(enable): 勾选。
- appid: QQ 官方机器人中获取的 appid。
- secret: QQ 官方机器人中获取的 secret。
- 启用消息列表单聊(enable_group_c2c): 如果需要通过 QQ 消息列表私聊机器人,保持开启。
- 启用频道私聊(enable_guild_direct_message): 如果需要频道私聊,保持开启。
点击 `保存`
## 🎉 大功告成
此时,你的 AstrBot 应该已经成功连接 QQ 官方接口。使用 `私聊` 的方式在 QQ 对机器人发送 `/help` 以检查是否连接成功。

View File

@@ -26,8 +26,8 @@ API Base URL 填写 `http://localhost:1234/v1`
API Key 填写 `lm-studio`
> 对于 Mac/Windows 使用 Docker Desktop 部署 AstrBot 部署的用户API Base URL 请填写为 `http://host.docker.internal:1234/v1`。
> 对于 Linux 使用 Docker 部署 AstrBot 部署的用户API Base URL 请填写为 `http://172.17.0.1:1234/v1`,或者将 `172.17.0.1` 替换为你的公网 IP IP确保宿主机系统放行了 1234 端口)。
> 对于 Mac/Windows 使用 Docker Desktop 部署 AstrBot 的用户API Base URL 请填写为 `http://host.docker.internal:1234/v1`。
> 对于 Linux 使用 Docker 部署 AstrBot 的用户API Base URL 请填写为 `http://172.17.0.1:1234/v1`,或者将 `172.17.0.1` 替换为你的公网 IP确保宿主机系统放行了 1234 端口)。
如果 LM Studio 使用了 Docker 部署,请确保 1234 端口已经映射到宿主机。
@@ -35,4 +35,4 @@ API Key 填写 `lm-studio`
保存配置即可。
> 输入 /provider 查看 AstrBot 配置的模型
> 输入 /provider 查看 AstrBot 配置的模型

View File

@@ -18,6 +18,8 @@ AstrBot 的指令通过插件机制注册。为了保持主程序轻量,当前
- `/reset`:重置当前会话的 LLM 上下文。
- `/stop`:停止当前会话中正在运行的 Agent 任务。
- `/new`:创建并切换到一个新对话。
- `/stats`:查看当前会话的 Token 用量统计。
- `/provider`:查看或切换 LLM Provider。该指令需要管理员权限。
- `/dashboard_update`:更新 AstrBot WebUI。该指令需要管理员权限。
- `/set`:设置当前会话变量,常用于 Dify、Coze、DashScope 等 Agent 执行器的输入变量。
- `/unset`:移除当前会话变量。
@@ -96,6 +98,45 @@ AstrBot 的指令通过插件机制注册。为了保持主程序轻量,当前
如果当前会话没有正在运行的任务AstrBot 会提示当前会话没有运行中的任务。
### `/stats`
`/stats` 用于查看当前会话的 Token 用量统计。
它从数据库中查询当前对话的所有 Provider 调用记录,汇总并展示:
- 总 Token 用量(输入 Token + 输出 Token
- 输入 Token缓存命中即被提供商缓存并跳过计费的输入 Token。
- 输入 Token其他即未被缓存、正常计费的输入 Token。
- 输出 Token即模型生成的输出 Token。
如果当前不在任何对话中AstrBot 会提示先使用 `/new` 创建对话。
### `/provider`
`/provider` 用于查看或切换当前 UMO 使用的 ProviderLLM / TTS / STT
**查看 Provider 列表:**
不带参数时,`/provider` 会列出所有已配置的 Provider按 LLM、TTS、STT 分类展示。每个 Provider 旁会显示:
- 序号,用于后续切换。
- Provider ID 和当前使用的模型LLM 类型)。
- 可达性标记:`✅` 表示连接正常,`❌` 表示连接失败(附带错误码)。
- 当前正在使用的 Provider 末尾会标注 `(当前使用)`
> [!NOTE]
> 可达性检测需要在 WebUI 的 `配置 -> 普通配置 -> AI 配置` 中,展开底部的「更多配置」,开启「提供商可达性检测」后才会生效。关闭后不显示可达性标记,列表加载更快。
**切换 Provider**
使用 `/provider <序号>` 可以将当前会话的 LLM Provider 切换为列表中对应序号的 Provider。
- `/provider <序号>`:切换到指定序号的 LLM Provider。
- `/provider tts <序号>`:切换到指定序号的 TTS Provider。
- `/provider stt <序号>`:切换到指定序号的 STT Provider。
该指令需要管理员权限。
## 内置指令扩展
除上述基础指令外,其他原本随主程序提供的内置指令已经迁移到独立插件:

View File

@@ -19,8 +19,32 @@ AstrBot 在 v4.13.0 之后引入了对 Anthropic Skills 的支持,使得用户
你可以上传 Skills上传格式要求如下
1. 是一个 .zip 压缩包
2. **解压后是一个 Skill 文件夹Skill 文件夹的名字即为这个 Skill 在 AstrBot 中的标识,请用英文命名**
3. Skill 文件夹内必须包含一个名为 `SKILL.md` 的文件,且该文件内容最好符合 Anthropic Skills 规范。你可以参考 [Anthropic 技能](https://code.claude.com/docs/zh-CN/skills)
2. 解压后可以是一个或多个 Skill 文件夹Skill 文件夹的名字即为这个 Skill 在 AstrBot 中的标识,请用英文、数字、点、下划线或短横线命名。
3. Skill 文件夹内必须包含一个名为 `SKILL.md` 的文件,且文件名大小写需要完全一致。该文件内容最好符合 Anthropic Skills 规范。你可以参考 [Anthropic 技能](https://code.claude.com/docs/zh-CN/skills)
## Skill 来源与优先级
AstrBot 会从多个位置发现 Skills
- **本地 Skills**:通过 WebUI 上传或放置在 `data/skills/<skill_name>/SKILL.md`,会显示在 WebUI 的 Skills 管理页面中。
- **插件内置 Skills**:插件可以在自己的 `skills/` 目录中提供 Skills。它们会显示在 WebUI 中,但由插件管理,因此不能在本地 Skills 页面删除或编辑。
- **Sandbox 预置 Skills**:使用 sandbox 运行环境时AstrBot 会读取沙盒中已发现的 Skills并在请求时提供给 Agent。
- **工作区 Skills**:当前会话 workspace 下的 `skills/<skill_name>/SKILL.md`。目前仅在 local 运行环境下注入,路径通常是 `data/workspaces/{normalized_umo}/skills/<skill_name>/SKILL.md`
工作区 Skills 是**请求级**能力local 运行环境下AstrBot 会在每次构建请求时检测当前会话 workspace 下的 `skills/` 目录,并把合法的 Skills 拼进本次请求的 Skills 清单。它们暂时不会显示在 WebUI 的 Skills 管理页面,也不会写入全局 Skills 配置。
如果人格配置为“选择指定 Skills”该列表只用于筛选本地、插件内置和 sandbox Skills工作区 Skills 仍会作为当前请求的一部分被检测并注入。只有人格明确配置为“不使用任何 Skills”时才会同时禁用工作区 Skills。
当不同来源出现同名 Skill 时,请求中的优先级如下:
1. 如果当前人格明确配置为“不使用任何 Skills”则不会注入任何 Skills包括工作区 Skills。
2. 如果当前人格配置了指定 Skills 列表,该列表不会过滤工作区 Skills。
3. 当前会话的工作区 Skill 优先级最高。同名时,它会覆盖本地、插件或 sandbox 中的同名 Skill仅对当前请求生效。
4. 本地 Skills 优先于插件内置 Skills 和 sandbox-only Skills。
5. 插件内置 Skills 优先于 sandbox-only Skills。
6. sandbox-only Skills 只会在没有同名本地、插件或工作区 Skill 时作为可用 Skill 注入。
如果本地 Skill 已同步到 sandboxAstrBot 会把它视为同一个 Skill在 sandbox 运行环境下,请求中会优先使用 sandbox 内可读取的路径。工作区 Skills 暂不会自动同步到 sandbox。
## 在 AstrBot 使用 Skills
@@ -35,4 +59,3 @@ Skills 提供了 Agent 操作说明书,并且内容通常包含 Python 代码
> [!NOTE]
> 需要说明的是,如果您使用 Local 作为执行环境AstrBot 目前仅允许 **AstrBot 管理员**请求时才真正让 Agent 操作你的本地环境普通用户将会被禁止Agent 将无法通过 Shell、Python 等 Tool 在本地环境执行代码,会收到相应的权限限制提示,如 `Sorry, I cannot execute code on your local environment due to permission restrictions.`。

View File

@@ -13,11 +13,11 @@ AstrBot 内置的网页搜索功能依赖大模型提供 `函数调用` 能力
等等带有搜索意味的提示让大模型触发调用搜索工具。
AstrBot 当前支持 4 种网页搜索源接入方式:`Tavily``BoCha``百度 AI 搜索``Brave`
AstrBot 当前支持 6 种网页搜索源接入方式:`Tavily``BoCha``百度 AI 搜索``Brave``Firecrawl``Exa`
![image](https://files.astrbot.app/docs/source/images/websearch/image.png)
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily``BoCha``百度 AI 搜索``Brave`
进入 `配置`,下拉找到网页搜索,您可选择 `Tavily``BoCha``百度 AI 搜索``Brave``Firecrawl``Exa`
### Tavily
@@ -35,6 +35,14 @@ AstrBot 当前支持 4 种网页搜索源接入方式:`Tavily`、`BoCha`、`
前往 Brave Search 获取 API Key然后填写在相应的配置项。
### Firecrawl
前往 [Firecrawl](https://firecrawl.dev) 获取 API Key然后填写在相应的配置项。
### Exa
前往 [Exa](https://dashboard.exa.ai) 获取 API Key然后填写在相应的配置项。Exa 是一个 AI 原生搜索引擎,支持关键词和语义搜索,提供分类过滤、域名限制和日期范围等高级搜索功能。
如果您使用 Tavily 作为网页搜索源,在 AstrBot ChatUI 上将会获得更好的体验优化,包括引用来源展示等:
![](https://files.astrbot.app/docs/source/images/websearch/image1.png)

View File

@@ -119,3 +119,6 @@ ChatUI 支持以下常用能力:
## 忘记密码
修改 data/cmd_config.json 文件内 `dashboard` 配置中的 `password`,将 password 整个键值对删除。
> [!TIP]
> 详细说明请参阅 [FAQ - 管理面板的密码忘记了](/faq.md#管理面板的密码忘记了)。