astral-bridge
# Astral Bridge
Astral Bridge connects QQ, through NapCat's OneBot v11 reverse WebSocket, and optionally
Telegram, through the Telegram Bot API, to one long-lived Astral Code app-server session. It
also exposes MCP tools so the agent can reply back to chat platforms, send files, mention
people, recall/delete messages, and fetch recent conversation context when needed.
This project is intended for self-hosted personal or team automation. It is not affiliated
with QQ, Tencent, NapCat, OneBot, Telegram, or Astral Code.
## Features
- Receive QQ private and group messages from NapCat over OneBot v11.
- Receive Telegram private, group, supergroup, and channel-post messages through long polling.
- Route every accepted message into one Astral app-server thread, either configured explicitly or auto-created on first startup.
- Trigger group messages only when the bot is mentioned, replied to, or the chat is configured
as always-trigger.
- Trigger every message from configured private QQ or Telegram users/chats.
- Store allowed conversation history locally in SQLite for later MCP lookups.
- Include compact inbound context: platform, chat/group name/id, sender id, nickname,
username/card, message id, reply id, trigger kind, unread count, and attachment metadata.
- Support app-server `turn/steer` when the current Astral thread already has an active turn.
- Expose Streamable HTTP or stdio MCP tools for QQ and Telegram replies, history, media,
files, mentions, and replies to specific message ids.
- Accept generic external event webhooks and forward them into the current Astral session.
- Expose a read-only Web UI for connection status, routing, recent messages,
recent conversations, and recent bridge logs.
- Add a random 3-5 second delay before outbound QQ send actions.
## Requirements
- Node.js 26 or newer. The bridge uses `node:sqlite`.
- pnpm 10 or newer.
- A running Astral Code app-server.
- NapCat configured with OneBot v11 reverse WebSocket.
- Optional: a Telegram bot token from BotFather if Telegram support is enabled.
## Quick Start
```bash
pnpm install
cp examples/config.example.json config.json
pnpm dev -- --config ./config.json
```
Build for production:
```bash
pnpm build
pnpm start -- --config ./config.json
```
Point NapCat's OneBot v11 reverse WebSocket to:
```text
ws://127.0.0.1:6701/onebot/v11/ws
```
If the bridge is running in Docker or on another host, replace the host and port with the
address reachable from NapCat, for example:
```text
ws://bridge:6701/onebot/v11/ws
```
## Configuration
Start from `examples/config.example.json`:
```json
{
"onebot": {
"host": "127.0.0.1",
"port": 6701,
"path": "/onebot/v11/ws",
"accessToken": null,
"actionTimeoutMs": 10000
},
"mcp": {
"transport": "stdio",
"host": "127.0.0.1",
"port": 6710,
"path": "/mcp"
},
"astral": {
"appServerUrl": "ws://127.0.0.1:4222",
"authToken": null,
"threadId": "auto",
"cwd": null,
"modelConfigPath": null,
"modelProvider": null,
"model": null,
"includeImageInputs": false,
"rotateThreadOnStart": false
},
"qq": {
"enabled": true,
"botUserId": "REPLACE_WITH_BOT_QQ",
"allowedGroupIds": ["REPLACE_GROUP_ID"],
"alwaysTriggerGroupIds": [],
"allowedPrivateUserIds": ["REPLACE_USER_ID"],
"triggerKeywords": ["astral", "bot"],
"recordUntriggered": true
},
"telegram": {
"enabled": false,
"botToken": "REPLACE_WITH_TELEGRAM_BOT_TOKEN",
"botUsername": "REPLACE_WITH_TELEGRAM_BOT_USERNAME",
"allowedChatIds": ["REPLACE_TELEGRAM_CHAT_ID"],
"alwaysTriggerChatIds": [],
"triggerKeywords": ["astral", "bot"],
"recordUntriggered": true,
"pollTimeoutSeconds": 25,
"pollIntervalMs": 1000,
"apiBaseUrl": "https://api.telegram.org"
},
"tts": {
"enabled": false,
"protocol": "openai_speech",
"apiKey": null,
"baseUrl": "http://127.0.0.1:8765/v1",
"model": "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16",
"voice": null,
"format": "mp3",
"language": "Chinese",
"referenceAudioPath": "/absolute/path/on/tts-server/atri.wav",
"referenceText": "参考音频中准确说出的文字",
"timeoutMs": 60000
},
"embedding": {
"enabled": false,
"baseUrl": "http://127.0.0.1:8766/v1",
"apiKey": null,
"model": "qwen3-embedding-0.6b",
"dimensions": 1024,
"batchSize": 32,
"timeoutMs": 60000,
"queryInstruction": "Given a user query about instant-message history, retrieve messages that answer the query or express the same meaning."
},
"externalEvents": {
"enabled": true,
"path": "/api/events",
"authToken": "REPLACE_WITH_EVENT_API_TOKEN",
"maxBodyBytes": 65536,
"debounceMs": 2000,
"maxBatchEvents": 20,
"maxBatchBodyChars": 6000
},
"storage": {
"dbPath": "./data/astral-bridge.db",
"mediaDir": "./media",
"downloadMedia": false
}
}
```
Environment overrides:
| Variable | Description |
| --- | --- |
| `ASTRAL_BRIDGE_CONFIG` | Path to the JSON config file. |
| `ASTRAL_BRIDGE_APP_SERVER_URL` | Astral app-server WebSocket URL. |
| `ASTRAL_BRIDGE_APP_SERVER_AUTH_TOKEN` | Bearer token for Astral app-server. |
| `ASTRAL_BRIDGE_THREAD_ID` | Optional fixed Astral thread/session id. Leave empty or set `auto` to let bridge create and persist one in its SQLite store. |
| `ASTRAL_BRIDGE_ROTATE_THREAD_ON_START` | Auto-managed mode only. Set `true` once to create a fresh thread on bridge startup, then set it back to `false`. |
| `ASTRAL_BRIDGE_MODEL_CONFIG_PATH` | Optional Astral `config.toml` path. When set, bridge reads `model_provider` and `model` from this file before new turns and syncs the current thread settings. |
| `ASTRAL_BRIDGE_MODEL_PROVIDER` | Optional static provider override when `modelConfigPath` is not set. |
| `ASTRAL_BRIDGE_MODEL` | Optional static model override when `modelConfigPath` is not set. |
| `ASTRAL_BRIDGE_QQ_ENABLED` | Enable the QQ/NapCat channel and expose QQ MCP tools. |
| `ASTRAL_BRIDGE_BOT_QQ` | Bot QQ user id. |
| `ASTRAL_BRIDGE_ALLOWED_GROUP_IDS` | Comma-separated allowed group ids. |
| `ASTRAL_BRIDGE_ALWAYS_TRIGGER_GROUP_IDS` | Comma-separated group ids where every non-bot message is forwarded to Astral. |
| `ASTRAL_BRIDGE_ALLOWED_PRIVATE_USER_IDS` | Comma-separated allowed private user ids. |
| `ASTRAL_BRIDGE_TRIGGER_KEYWORDS` | Comma-separated QQ trigger keywords. Any allowed group/private message containing one keyword, case-insensitively, is forwarded to Astral. |
| `ASTRAL_BRIDGE_TELEGRAM_ENABLED` | Enable Telegram long polling. |
| `ASTRAL_BRIDGE_TELEGRAM_BOT_TOKEN` | Telegram bot token from BotFather. |
| `ASTRAL_BRIDGE_TELEGRAM_BOT_USERNAME` | Optional bot username, without or with `@`; `getMe` also discovers it at runtime. |
| `ASTRAL_BRIDGE_TELEGRAM_ALLOWED_CHAT_IDS` | Comma-separated Telegram `chat.id` allowlist. Use `/chatid` in a chat to retrieve it. |
| `ASTRAL_BRIDGE_TELEGRAM_ALWAYS_TRIGGER_CHAT_IDS` | Comma-separated Telegram chat ids where every non-bot message is forwarded to Astral. |
| `ASTRAL_BRIDGE_TELEGRAM_TRIGGER_KEYWORDS` | Comma-separated Telegram trigger keywords. Any allowed chat message containing one keyword, case-insensitively, is forwarded to Astral. |
| `ASTRAL_BRIDGE_TELEGRAM_RECORD_UNTRIGGERED` | Store non-triggering Telegram messages from allowed chats. |
| `ASTRAL_BRIDGE_TELEGRAM_POLL_TIMEOUT_SECONDS` | Telegram `getUpdates` long-poll timeout. |
| `ASTRAL_BRIDGE_TELEGRAM_POLL_INTERVAL_MS` | Delay after a failed Telegram poll before retrying. |
| `ASTRAL_BRIDGE_TTS_ENABLED` | Enable TTS-backed QQ/Telegram voice message tools. |
| `ASTRAL_BRIDGE_TTS_PROTOCOL` | `chat_completions` for MiMo-style audio responses or `openai_speech` for `/v1/audio/speech`, including MLX-Audio. |
| `ASTRAL_BRIDGE_TTS_API_KEY` | Optional API key. Required by `chat_completions`; local `openai_speech` servers can leave it empty. |
| `ASTRAL_BRIDGE_TTS_BASE_URL` | TTS API base URL, including `/v1`. |
| `ASTRAL_BRIDGE_TTS_MODEL` | TTS model id sent to the configured endpoint. |
| `ASTRAL_BRIDGE_TTS_VOICE` | Configured bot voice, hidden from MCP tool schemas. |
| `ASTRAL_BRIDGE_TTS_FORMAT` | Audio response format: `wav`, `mp3`, `ogg`, `opus`, or `m4a`. |
| `ASTRAL_BRIDGE_TTS_LANGUAGE` | Optional language hint sent as `lang_code` to `openai_speech`. |
| `ASTRAL_BRIDGE_TTS_REFERENCE_AUDIO_PATH` | Optional reference audio path as seen by the TTS server. |
| `ASTRAL_BRIDGE_TTS_REFERENCE_TEXT` | Exact transcript of the reference audio; must be configured together with its path. |
| `ASTRAL_BRIDGE_TTS_TIMEOUT_MS` | TTS request timeout in milliseconds. |
| `ASTRAL_BRIDGE_EMBEDDING_ENABLED` | Enable asynchronous message embeddings and semantic/hybrid `query_messages` search. |
| `ASTRAL_BRIDGE_EMBEDDING_BASE_URL` | OpenAI-compatible embeddings API base URL, including `/v1`. |
| `ASTRAL_BRIDGE_EMBEDDING_API_KEY` | Optional bearer token for the embeddings API. |
| `ASTRAL_BRIDGE_EMBEDDING_MODEL` | Embedding model id sent to the configured endpoint. |
| `ASTRAL_BRIDGE_EMBEDDING_DIMENSIONS` | Stored vector dimensions. Changing this rebuilds the vector index. |
| `ASTRAL_BRIDGE_EMBEDDING_BATCH_SIZE` | Maximum messages embedded in one background request. |
| `ASTRAL_BRIDGE_EMBEDDING_TIMEOUT_MS` | Embedding request timeout in milliseconds. |
| `ASTRAL_BRIDGE_EMBEDDING_QUERY_INSTRUCTION` | Retrieval instruction prepended to semantic search queries. |
| `ASTRAL_BRIDGE_MCP_TRANSPORT` | `stdio` or `http`. |
| `ASTRAL_BRIDGE_EVENT_API_ENABLED` | Enable or disable the external event API. |
| `ASTRAL_BRIDGE_EVENT_API_PATH` | External event API path, default `/api/events`. |
| `ASTRAL_BRIDGE_EVENT_API_TOKEN` | Optional bearer token required by the external event API. |
| `ASTRAL_BRIDGE_EVENT_API_DEBOUNCE_MS` | Window used to merge attention-worthy external events before forwarding to Astral. |
| `ASTRAL_BRIDGE_EVENT_API_MAX_BATCH_EVENTS` | Maximum external events included in one merged Astral turn. Extra events in the same window are counted and omitted. |
| `ASTRAL_BRIDGE_EVENT_API_MAX_BATCH_BODY_CHARS` | Maximum merged event body characters sent to Astral. Longer batches are truncated. |
When `astral.threadId` / `ASTRAL_BRIDGE_THREAD_ID` is empty or `auto`, bridge creates one
Astral thread on first use and stores the id in SQLite `store_meta`. To rotate an
auto-managed thread manually, call `POST /api/astral/thread/rotate` with the same bearer
token as the external event API. If a turn is currently active, the endpoint returns `409`
instead of interrupting it.
`recordUntriggered` controls whether non-triggering messages from allowed conversations
are stored. Keeping it enabled lets the agent fetch surrounding context without forwarding
every group message into Astral. For QQ this also stores member-to-member poke notices in
allowed groups; only pokes targeting the bot trigger an Astral turn.
Telegram uses long polling. On startup the bridge calls `deleteWebhook` so Telegram will
allow `getUpdates`. Send `/chatid` to the bot in a private chat, group, supergroup, or
topic to get the exact `chat_id`; this command works even before the chat is allowlisted.
All other Telegram interaction requires the chat id to be present in
`telegram.allowedChatIds` or `telegram.alwaysTriggerChatIds`.
## Web UI
When HTTP MCP is enabled, open:
```http
GET /ui
```
The dashboard polls `GET /api/dashboard/state` and shows NapCat/Astral connection status,
the active Astral turn id when one is running, configured routing, external event API
status, recent conversations, recent stored messages, and recent bridge logs.
## Astral MCP Setup
For stdio MCP, build first and point Astral at the compiled entrypoint:
```toml
[mcp_servers.bridge]
command = "node"
args = ["/path/to/astral-bridge/dist/index.js", "--config", "/path/to/astral-bridge/config.json"]
```
Use `node` directly instead of a package-manager wrapper. stdio MCP requires stdout to
contain only JSON-RPC messages.
For container or multi-process deployments, run HTTP MCP:
```json
{
"mcp": {
"transport": "http",
"host": "0.0.0.0",
"port": 6710,
"path": "/mcp"
}
}
```
Then configure Astral:
```toml
[mcp_servers.bridge]
url = "http://bridge:6710/mcp"
```
The bridge exposes both QQ and Telegram tools from the same MCP endpoint, so register it
once. Tool names remain platform-specific, for example
`mcp__bridge__qq_send_group_message` and `mcp__bridge__telegram_send_message`.
QQ tools are registered only when `qq.enabled` is true, Telegram tools only when
`telegram.enabled` is true, and voice tools only when `tts.enabled` is true.
## MCP Tools
| Shared Tool | Purpose |
| --- | --- |
| `query_messages` | Run isolated read-only JavaScript over QQ/TG history with `search`, `messages`, `context`, `conversations`, `sql`, and `schema`; embedding-enabled deployments also expose `embed`. |
`query_messages` is intended for complex recall and analysis rather than ordinary recent-message
lookups. Its `code` argument is an async JavaScript function body that must return JSON. Each
invocation runs in a separate process against a read-only SQLite connection and is terminated
after 30 seconds. Helper `limit` values are defaults rather than hard maxima; Astral applies its
normal MCP-output context limit after the bridge returns the result.
```js
return search("电路图", { platform: "qq", context_limit: 2 });
```
When embeddings are enabled, `search()` defaults to hybrid FTS5 plus semantic recall. Semantic
and hybrid searches are asynchronous, while explicit lexical searches retain the synchronous
FTS5 path:
```js
return await search("上次谁说显卡坏了", { mode: "hybrid", platform: "qq", context_limit: 2 });
```
For custom KNN joins, use `await embed(text)` and bind the returned float32 vector directly to
`sql()`. The vector is never printed into the tool result:
```js
const vector = await embed("串流画面问题");
return sql(
"SELECT m.id AS row_id, m.text, e.distance FROM message_embeddings e JOIN messages m ON m.id = e.message_row_id WHERE e.embedding MATCH ? AND e.k = ? ORDER BY e.distance",
vector,
20,
);
```
Message writes and FTS5 updates never wait for the embedding service. New and historical
messages are queued in SQLite and indexed in the background. If the local model is unavailable,
semantic/hybrid calls return a clear error and `mode: "lexical"` remains available.
Use `schema()` from inside the query when exact table fields are needed. Use `sql()` only for
joins, grouping, time buckets, and other shapes not covered naturally by the higher-level helpers.
| QQ Tool | Purpose |
| --- | --- |
| `qq_send_group_message` | Send a group message with text, images, ordered parts, mentions, or a reply target. |
| `qq_send_private_message` | Send a private message with text, images, ordered parts, or a reply target. |
| `qq_send_group_voice` | Generate TTS audio and send it as a real QQ group voice message. |
| `qq_send_private_voice` | Generate TTS audio and send it as a real QQ private voice message. |
| `qq_send_group_file` | Upload a local file or URL to a QQ group. |
| `qq_send_private_file` | Upload a local file or URL to a QQ private chat. |
| `qq_get_unread_messages` | Return the unread batch counted by the latest inbound Astral prompt. |
| `qq_get_recent_messages` | Return recent stored messages for one group or private conversation. |
| `qq_get_message` | Return one stored message by OneBot `message_id`. |
| `qq_get_forward_messages` | Expand every node in one stored, allowlisted QQ merged-forward message without persisting or downloading its contents. |
| `qq_get_conversation_state` | Return bridge state and counts for one conversation. |
| `qq_download_media` | Download a stored attachment, or one attachment selected by a merged-forward `forward_node_path`, into the local media cache. |
| `qq_set_reaction` | React to a stored QQ group message by OneBot `message_id` and QQ `emoji_id`. QQ reactions only work in group chats; the tool description lists common `emoji_id` values. |
| `qq_group_admin_help` | Explain grouped QQ administration tools and actions. |
| `qq_group_member_admin` | Kick, mute/unmute, set admin, set card, set special title, or list muted members. |
| `qq_group_request_admin` | Read or handle group join/invite requests. |
| `qq_group_settings_admin` | Change group name/avatar, whole-group mute, or make the bot leave the group. |
| `qq_group_message_admin` | Recall messages, manage essence messages, check @all quota, or mark group read. |
| `qq_group_notice_admin` | Send, list, or delete group notices. |
| `qq_group_file_admin` | Manage group files and folders. |
| `qq_group_info_admin` | Read group, member, and honor information. |
| Telegram Tool | Purpose |
| --- | --- |
| `telegram_send_message` | Send text with ordered text/mention parts, optional reply target, and optional topic thread id. |
| `telegram_send_rich_message` | Send structured rich text with Telegram Rich Message HTML or Markdown. |
| `telegram_send_file` | Send a local path, Telegram `file_id`, or HTTP URL as a document/file. Images intentionally use this tool too. |
| `telegram_send_voice` | Generate TTS audio and send it as a real Telegram voice message. |
| `telegram_delete_message` | Delete/recall a message when Telegram permissions allow it. Requires `confirm:true`. |
| `telegram_set_reaction` | React to a Telegram group or private message with one standard reaction emoji; the tool description lists the allowed emoji values. |
| `telegram_get_unread_messages` | Return the unread batch counted by the latest inbound Astral prompt. |
| `telegram_get_recent_messages` | Return recent stored messages for one Telegram chat. |
| `telegram_get_message` | Return one stored Telegram message by `message_id`. |
| `telegram_get_conversation_state` | Return bridge state and counts for one Telegram chat. |
| `telegram_download_media` | Download a stored Telegram attachment into the local media cache. |
### Group Administration
Group administration tools are grouped to keep the MCP tool list small. Call
`qq_group_admin_help` to discover action names and required fields. Disbanding groups is
intentionally not implemented; `leave_group` only makes the bot leave the group.
All group-specific administration tools reject `group_id` values outside
`qq.allowedGroupIds` when that allowlist is configured. Actions that mutate group state
require `confirm: true`; use that only after the user explicitly asks for the operation.
### Mentions, Images, and Replies
Use `parts` when a group message needs exact ordering of text, mentions, and images:
```json
[
{ "type": "text", "text": "请 " },
{ "type": "at", "user_id": "TARGET_QQ" },
{ "type": "text", "text": " 看一下 " },
{ "type": "image", "file": "/workspace/result.png" }
]
```
Reply to a specific QQ message by passing `reply_to_message_id` with the OneBot
`message_id` from the inbound prompt or one of the history tools.
Images in outbound messages use OneBot `image` segments. Non-image files use
NapCat-compatible `upload_group_file` and `upload_private_file` actions.
QQ merged-forward messages remain opaque in inbound prompts and history until the agent calls
`qq_get_forward_messages`. The tool returns every nested node in depth-first order. To fetch one
internal image, file, voice, or video afterward, pass the outer `message_id`, the returned
`forward_node_path`, and its zero-based `attachment_index` to `qq_download_media`. Forwarded
children are never added to ordinary history, FTS, or embedding indexes.
Telegram text messages use ordered `parts` for mentions:
```json
[
{ "type": "text", "text": "请 " },
{ "type": "mention", "username": "alice" },
{ "type": "text", "text": " 看一下;也同步给 " },
{ "type": "mention", "user_id": "123456789", "text": "Bob" }
]
```
Use `telegram_send_rich_message` when a Telegram reply needs structured formatting
such as headings, lists, tables, collapsible details, code blocks, or formulas. Pass
exactly one of `html` or `markdown`:
```json
{
"chat_id": "-1001234567890",
"html": "<h2>Summary</h2><ul><li>Done</li><li>Next step</li></ul>",
"reply_to_message_id": "123",
"message_thread_id": "456"
}
```
For Telegram files, call `telegram_send_file` for images and non-images alike. This sends
through Telegram `sendDocument`, preserving the original file quality. Telegram rich
message media blocks are intended for HTTP/HTTPS URLs; local `/workspace` and
`/app/media` files should still be sent with `telegram_send_file`:
```json
{
"chat_id": "-1001234567890",
"file": "/workspace/result.png",
"caption": "结果图",
"reply_to_message_id": "123",
"message_thread_id": "456"
}
```
## Message Routing
The bridge only forwards messages from configured QQ targets or Telegram chat ids:
- Group messages: forwarded when the bot is at-mentioned or the message replies to a bot
message.
- Keyword messages: in any allowed QQ/Telegram group or private chat, messages containing
any configured trigger keyword are forwarded. Keyword matching is case-insensitive.
- Always-trigger group messages: for group ids in `qq.alwaysTriggerGroupIds`, every
non-bot message is forwarded, even without an @mention or reply.
- Private messages: forwarded for every message from configured private users.
- Other allowed conversation messages: optionally stored when `recordUntriggered` is true,
but not forwarded to Astral.
- Telegram `/chatid`: always answered directly so you can discover the current Telegram
`chat.id` before editing the allowlist.
- Telegram group/supergroup/channel messages: forwarded when the bot is mentioned, the
message replies to a bot message, or the chat id is in `telegram.alwaysTriggerChatIds`.
- Telegram private messages: forwarded for every message from configured private chat ids.
- `/stop`: in any allowed QQ/Telegram group or private chat, interrupts the active Astral
turn and replies in the same conversation.
Every forwarded turn includes `conversation_unread_count`, the number of stored messages
in the same group/private conversation since the previous Astral
prompt, including the current trigger message. The agent can call `qq_get_unread_messages`
or `telegram_get_unread_messages` when that context is useful; it does not need to call it
for every message.
## External Event API
When HTTP MCP is enabled, the same HTTP server can accept generic external events:
```http
GET /api/events/schema
```
Returns a machine-readable OpenAPI 3.1 schema with examples and curl usage.
```http
POST /api/events
Authorization: Bearer REPLACE_WITH_EVENT_API_TOKEN
Content-Type: application/json
```
```json
{
"source": "minecraft:survival-main",
"event_type": "player_join",
"title": "Player joined",
"body": "Steve joined the server",
"actor": { "id": "uuid", "name": "Steve" },
"metadata": { "world": "world", "x": 120, "y": 64, "z": -33 }
}
```
The bridge queues attention-worthy events, merges short bursts into one bounded Astral
turn, and submits that merged event to the current Astral thread using the same queue as QQ
messages. Set `wants_agent_attention` to `false` to validate and accept an event without
forwarding it into Astral. Batching is controlled by `externalEvents.debounceMs`,
`externalEvents.maxBatchEvents`, and `externalEvents.maxBatchBodyChars`.
Request fields:
| Field | Required | Description |
| --- | --- | --- |
| `source` | Yes | System or integration name, such as `minecraft:survival-main` or `monitoring`. |
| `event_type` / `type` | No | Event kind. Defaults to `event`. |
| `title` | No | Short title. |
| `body` / `text` | No | Main event text. |
| `severity` | No | Severity label. Defaults to `info`. |
| `actor` | No | Entity that caused the event. |
| `metadata` | No | Structured event details. |
| `dedupe_key` | No | Optional stable key supplied by the caller. |
| `occurred_at` | No | ISO timestamp, Unix seconds, or Unix milliseconds. |
| `wants_agent_attention` | No | Defaults to `true`; set `false` for validation-only events. |
| `id` | No | Caller-supplied event id. A UUID is generated when omitted. |
Successful requests return `202 Accepted` with `{ ok, accepted_for_astral, event }`.
Unauthorized requests return `401` when `externalEvents.authToken` is configured.
## Astral App-Server Behavior
The bridge talks to Astral app-server over WebSocket and uses:
- `initialize`
- `thread/resume`
- `turn/start`
- `turn/steer` when the current thread already has an active turn
When starting a turn, the bridge requests `approvalPolicy = "never"` and
`sandboxPolicy = { type = "dangerFullAccess" }`. If you use this mode, isolate Astral at
the container, VM, or host level and only mount directories you are willing to expose.
Server approval requests for command execution and file changes are canceled by default.
QQ sending should happen through the MCP tools and Astral MCP tool approval settings, not
through the bridge approving arbitrary app-server actions.
## Docker and OrbStack
The generic bridge image is defined by `Dockerfile`.
OrbStack deployment templates live under `deploy/orbstack`. They include:
- `bridge`: OneBot reverse WebSocket plus Streamable HTTP MCP.
- `astral-code`: an Ubuntu-based Astral app-server runtime with mapped config, binary,
source, workspace, and build cache directories.
- `napcat`: NapCat Docker service.
Copy `deploy/orbstack/.env.example` to `.env`, fill in your local values, and read
the Chinese deployment guide at `deploy/orbstack/README.md` before running the compose
stack. It covers required QQ/Telegram/Astral settings, persistent volumes, memory storage,
and the memory phase2 sandbox option.
## Security Notes
- Do not commit real QQ ids, app-server tokens, API keys, NapCat WebUI tokens, SQLite
databases, downloaded media, or local config files.
- Bind services to loopback unless you intentionally need LAN access.
- The app-server danger-full-access mode is powerful. Use a dedicated container or host
account and mount only the workspace/config directories needed by the agent.
- Review allowed groups and private users before exposing the bot to busy chats.
## Development
```bash
pnpm install
pnpm typecheck
pnpm build
```
Project layout:
```text
src/ Bridge source
examples/ Example JSON config
deploy/orbstack/ Self-hosted OrbStack deployment template
data/ Runtime SQLite state, ignored by git
media/ Runtime media cache, ignored by git
```
## Acknowledgements
The design of `query_messages` was inspired by
[Obelisk](https://github.com/tommy0103/obelisk) and its agent-first approach to retrieval:
give the agent a programmable query surface over a local SQLite index, then
let the agent decide how to search, inspect context, and follow the evidence. Thank you for
showing how much more capable retrieval becomes when the agent owns the query plan.
## License
Licensed under the Apache License, Version 2.0. See `LICENSE`.
TDQS
Scored across 10 tools
Each tool targets a distinct operation: download media, retrieve various message types, send messages/files to group/private, and check conversation state. No ambiguity between tools.
All tools follow a consistent `qq_verb_noun` snake_case pattern, using clear verbs like download, get, send, search and specific objects like media, message, group_file, private_message.
10 tools is well-scoped for a messaging bridge, covering message retrieval, sending, file uploads, and state queries without superfluous or missing functionality.
Core message lifecycle is covered: read, search, send (text/files) for both group and private. Minor gaps like deleting messages or listing conversations exist but are acceptable for a bridge focused on message relay.