bub-qq 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bub_qq-0.0.1/PKG-INFO +361 -0
- bub_qq-0.0.1/README.md +349 -0
- bub_qq-0.0.1/pyproject.toml +29 -0
- bub_qq-0.0.1/pyproject.toml.orig +25 -0
- bub_qq-0.0.1/src/bub_qq/__init__.py +37 -0
- bub_qq-0.0.1/src/bub_qq/channel.py +290 -0
- bub_qq-0.0.1/src/bub_qq/config.py +149 -0
- bub_qq-0.0.1/src/bub_qq/gateway/__init__.py +25 -0
- bub_qq-0.0.1/src/bub_qq/gateway/info.py +89 -0
- bub_qq-0.0.1/src/bub_qq/gateway/webhook.py +227 -0
- bub_qq-0.0.1/src/bub_qq/gateway/websocket.py +522 -0
- bub_qq-0.0.1/src/bub_qq/gateway/ws_errors.py +46 -0
- bub_qq-0.0.1/src/bub_qq/inbound/__init__.py +13 -0
- bub_qq-0.0.1/src/bub_qq/inbound/c2c.py +140 -0
- bub_qq-0.0.1/src/bub_qq/inbound/common.py +50 -0
- bub_qq-0.0.1/src/bub_qq/inbound/group.py +191 -0
- bub_qq-0.0.1/src/bub_qq/inbound/interaction.py +64 -0
- bub_qq-0.0.1/src/bub_qq/outbound/__init__.py +9 -0
- bub_qq-0.0.1/src/bub_qq/outbound/c2c.py +108 -0
- bub_qq-0.0.1/src/bub_qq/outbound/group.py +140 -0
- bub_qq-0.0.1/src/bub_qq/outbound/markdown.py +80 -0
- bub_qq-0.0.1/src/bub_qq/outbound/send_errors.py +142 -0
- bub_qq-0.0.1/src/bub_qq/outbound/send_flow.py +378 -0
- bub_qq-0.0.1/src/bub_qq/plugin.py +251 -0
- bub_qq-0.0.1/src/bub_qq/protocol/__init__.py +21 -0
- bub_qq-0.0.1/src/bub_qq/protocol/auth.py +113 -0
- bub_qq-0.0.1/src/bub_qq/protocol/errors.py +840 -0
- bub_qq-0.0.1/src/bub_qq/protocol/models.py +238 -0
- bub_qq-0.0.1/src/bub_qq/protocol/openapi.py +309 -0
- bub_qq-0.0.1/src/bub_qq/protocol/signature.py +56 -0
- bub_qq-0.0.1/src/bub_qq/py.typed +0 -0
- bub_qq-0.0.1/src/bub_qq/runtime.py +24 -0
- bub_qq-0.0.1/src/bub_qq/security.py +196 -0
- bub_qq-0.0.1/src/bub_qq/session.py +91 -0
- bub_qq-0.0.1/src/bub_qq/store.py +99 -0
- bub_qq-0.0.1/src/bub_qq/tools.py +87 -0
- bub_qq-0.0.1/src/skills/qq/SKILL.md +63 -0
- bub_qq-0.0.1/src/skills/qq/__init__.py +1 -0
bub_qq-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bub-qq
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: QQ Open Platform channel for Bub
|
|
5
|
+
Author: wzhongyun
|
|
6
|
+
Author-email: wzhongyun <wzhongyun@outlook.com>
|
|
7
|
+
Requires-Dist: aiohttp>=3.13.3
|
|
8
|
+
Requires-Dist: cryptography>=45.0.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# bub-qq
|
|
14
|
+
|
|
15
|
+
QQ Open Platform channel adapter for [Bub](https://bub.build).
|
|
16
|
+
|
|
17
|
+
Chinese documentation: [README.zh-CN.md](./README.zh-CN.md)
|
|
18
|
+
|
|
19
|
+
## What it provides
|
|
20
|
+
|
|
21
|
+
| Capability | Details |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Single-chat (C2C) receive/reply | `C2C_MESSAGE_CREATE` adapted to Bub `ChannelMessage`; passive text / markdown replies |
|
|
24
|
+
| Group receive/reply | `GROUP_AT_MESSAGE_CREATE` / `GROUP_MESSAGE_CREATE`; full-message mode supported, payload carries `was_mentioned` / `sender_role` |
|
|
25
|
+
| Active group messages | Proactive fallback when a passive reply is impossible (`active_messages`, requires the group admin's opt-in in the QQ client) |
|
|
26
|
+
| Reply modes & selective silence | `reply_mode: direct` (default) forwards the model's final text and swallows `<no_reply/>`; `reply_mode: tool` exposes a native `qq.send` tool so the model replies by calling it and stays silent by not calling it (see Reply modes) |
|
|
27
|
+
| Quotes and chat records | `msg_elements` parsed into `quoted_messages` (quoted messages / merged-forward chat records) for the model |
|
|
28
|
+
| Receive transport | **webhook** or **websocket** (mutually exclusive on the QQ platform side); ed25519 signature verification and reconnect included |
|
|
29
|
+
| Security | User/group allowlists, role-gated comma commands, per-scope tool policy, LLM rate limiting, audit logs (see Security) |
|
|
30
|
+
| Persisted platform state | Active-message opt-ins (`*_MSG_RECEIVE` / `*_MSG_REJECT`) and group claw_cfg survive restarts |
|
|
31
|
+
| Reliable sending | Inbound/outbound dedupe, `msg_seq` management, error catalog; async manual audit (304023/304024) treated as pending success |
|
|
32
|
+
| Onboarding | `bub onboard` collects `appid` / `secret` / `receive_mode`; bundled skill resources under `src/skills/qq` |
|
|
33
|
+
|
|
34
|
+
Plugin entry point: `qq` → `bub_qq.plugin`. Supports **single-chat (C2C)** and **group** text receive/reply. QQ Guild is not covered yet.
|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
|
|
38
|
+
1. [Install Bub](https://bub.build/docs/getting-started/install/) (recommended: `uv tool install bub`)
|
|
39
|
+
2. Run `bub onboard` and ensure model access works (`bub chat` or `bub run`)
|
|
40
|
+
3. Create a QQ bot on the [QQ Open Platform](https://bot.q.qq.com/wiki/develop/api-v2/) and obtain `APPID` / `SECRET`
|
|
41
|
+
|
|
42
|
+
## Install (end users)
|
|
43
|
+
|
|
44
|
+
`bub-qq` is not on PyPI. With a global Bub install (`uv tool install bub`), install the plugin into **Bub’s own environment**:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bub install bub-qq@main
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This resolves to the official monorepo package:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
git+https://github.com/bubbuild/bub-contrib.git@main#subdirectory=packages/bub-qq
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Equivalent forms:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Full Git URL (use https:// …, not git+https://, when passing to bub install)
|
|
60
|
+
bub install "https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-qq"
|
|
61
|
+
|
|
62
|
+
# Pin a tag or commit when available
|
|
63
|
+
bub install bub-qq@<tag-or-sha>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Verify the plugin is loaded:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bub hooks
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You should see the `qq` plugin among discovered entry points / hook providers.
|
|
73
|
+
|
|
74
|
+
### Upgrade / uninstall
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bub update bub-qq
|
|
78
|
+
bub uninstall bub-qq
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Notes
|
|
82
|
+
|
|
83
|
+
- Do **not** use bare `bub install bub-qq`. A name without `@ref` is treated as a PyPI package name.
|
|
84
|
+
- `bub install` requires Bub to run inside a virtual environment (including the environment created by `uv tool install bub`) and `uv` on `PATH`.
|
|
85
|
+
|
|
86
|
+
## Install (local development)
|
|
87
|
+
|
|
88
|
+
Editable install into the same environment that runs `bub`.
|
|
89
|
+
|
|
90
|
+
### Option A — global Bub (`uv tool`)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv pip install -e /path/to/bub-contrib/packages/bub-qq \
|
|
94
|
+
--python ~/.local/share/uv/tools/bub/bin/python
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then use the global CLI as usual:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bub hooks
|
|
101
|
+
bub gateway
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Option B — Bub / monorepo project venv
|
|
105
|
+
|
|
106
|
+
From a uv project that already depends on Bub:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv add --editable /path/to/bub-contrib/packages/bub-qq
|
|
110
|
+
# or, from bub-contrib workspace workflows:
|
|
111
|
+
uv pip install -e packages/bub-qq
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Option C — raw Git install into a chosen interpreter
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
uv pip install \
|
|
118
|
+
"git+https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-qq" \
|
|
119
|
+
--python /path/to/the/python/that/runs/bub
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
Settings can come from:
|
|
125
|
+
|
|
126
|
+
- the `qq:` section in `~/.bub/config.yml`
|
|
127
|
+
- `BUB_QQ_*` environment variables (including values loaded from `.env`)
|
|
128
|
+
- `bub onboard`, which interactively collects the required fields when the `qq` channel is enabled
|
|
129
|
+
|
|
130
|
+
Env vars override YAML, so shared policy can live in `config.yml` while secrets stay in the environment.
|
|
131
|
+
|
|
132
|
+
### Required
|
|
133
|
+
|
|
134
|
+
| YAML field (`qq.*`) | Env var | Description |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `appid` | `BUB_QQ_APPID` | QQ bot app ID |
|
|
137
|
+
| `secret` | `BUB_QQ_SECRET` | QQ bot secret |
|
|
138
|
+
| `receive_mode` | `BUB_QQ_RECEIVE_MODE` | Inbound transport: `webhook` or `websocket` |
|
|
139
|
+
|
|
140
|
+
`receive_mode` must match the QQ developer console:
|
|
141
|
+
|
|
142
|
+
- `webhook` — starts the embedded webhook server only; WebSocket is not started
|
|
143
|
+
- `websocket` — starts the WebSocket client only; the embedded webhook server is not started
|
|
144
|
+
|
|
145
|
+
QQ treats webhook and WebSocket as **mutually exclusive**. After a valid HTTPS webhook callback URL is configured successfully, WebSocket delivery is no longer supported on the platform side.
|
|
146
|
+
|
|
147
|
+
Gateway start fails if `appid` / `secret` are empty, or if `receive_mode` is not `webhook` / `websocket`.
|
|
148
|
+
|
|
149
|
+
### Optional
|
|
150
|
+
|
|
151
|
+
| YAML field (`qq.*`) | Env var | Default | Description |
|
|
152
|
+
| --- | --- | --- | --- |
|
|
153
|
+
| `token_url` | `BUB_QQ_TOKEN_URL` | `https://bots.qq.com/app/getAppAccessToken` | Access token endpoint |
|
|
154
|
+
| `openapi_base_url` | `BUB_QQ_OPENAPI_BASE_URL` | `https://api.bot.qq.com` | OpenAPI base URL (official unified endpoint; override here to use the legacy `https://api.sgroup.qq.com`) |
|
|
155
|
+
| `timeout_seconds` | `BUB_QQ_TIMEOUT_SECONDS` | `30` | HTTP timeout for token and OpenAPI |
|
|
156
|
+
| `token_refresh_skew_seconds` | `BUB_QQ_TOKEN_REFRESH_SKEW_SECONDS` | `60` | Refresh token this many seconds before expiry |
|
|
157
|
+
| `webhook_host` | `BUB_QQ_WEBHOOK_HOST` | `127.0.0.1` | Embedded webhook bind host |
|
|
158
|
+
| `webhook_port` | `BUB_QQ_WEBHOOK_PORT` | `8080` | Embedded webhook port (`80` / `443` / `8080` / `8443` allowed by QQ) |
|
|
159
|
+
| `webhook_path` | `BUB_QQ_WEBHOOK_PATH` | `/qq/webhook` | Webhook path |
|
|
160
|
+
| `webhook_callback_timeout_seconds` | `BUB_QQ_WEBHOOK_CALLBACK_TIMEOUT_SECONDS` | `15` | Reserved for future callback controls |
|
|
161
|
+
| `verify_signature` | `BUB_QQ_VERIFY_SIGNATURE` | `true` | Enforce webhook signature verification |
|
|
162
|
+
| `webhook_signature_timestamp_tolerance_seconds` | `BUB_QQ_WEBHOOK_SIGNATURE_TIMESTAMP_TOLERANCE_SECONDS` | `0` | Reject webhook requests whose signature timestamp deviates from local time by more than this many seconds; `0` disables the freshness check |
|
|
163
|
+
| `inbound_dedupe_size` | `BUB_QQ_INBOUND_DEDUPE_SIZE` | `1024` | Recent inbound `msg_id` cache size |
|
|
164
|
+
| `session_state_size` | `BUB_QQ_SESSION_STATE_SIZE` | `1024` | Max sessions / send records kept in memory for passive replies (oldest entries are evicted) |
|
|
165
|
+
| `passive_reply_window_seconds` | `BUB_QQ_PASSIVE_REPLY_WINDOW_SECONDS` | `3600` | How long after an inbound message passive replies are attempted |
|
|
166
|
+
| `active_messages` | `BUB_QQ_ACTIVE_MESSAGES` | `false` | Send proactive group messages (no `msg_id`) when a passive reply is impossible; requires the group admin to allow proactive messages in the QQ client |
|
|
167
|
+
| `passive_replies_per_msg_id` | `BUB_QQ_PASSIVE_REPLIES_PER_MSG_ID` | `4` | Local cap of passive replies per inbound `msg_id`; beyond it the send falls back to an active message (when enabled) or is skipped |
|
|
168
|
+
| `reply_mode` | `BUB_QQ_REPLY_MODE` | `direct` | How model output reaches QQ: `direct` forwards the final text (output exactly `<no_reply/>` to stay silent); `tool` disables direct forwarding and exposes the `qq.send` tool instead (see Reply modes) |
|
|
169
|
+
| `state_file` | `BUB_QQ_STATE_FILE` | empty | JSON file persisting platform switches (active-message opt-ins, group claw_cfg); empty uses `<bub home>/qq/state.json` |
|
|
170
|
+
| `admin_users` | `BUB_QQ_ADMIN_USERS` | empty | Comma-separated user openids with full comma-command and tool access in every scope |
|
|
171
|
+
| `allow_users` | `BUB_QQ_ALLOW_USERS` | empty | Comma-separated C2C allowlist; when set, C2C messages from anyone else are dropped |
|
|
172
|
+
| `allow_groups` | `BUB_QQ_ALLOW_GROUPS` | empty | Comma-separated group allowlist; when set, messages from other groups are dropped |
|
|
173
|
+
| `group_tool_policy` | `BUB_QQ_GROUP_TOOL_POLICY` | `restricted` | Tool policy for group sessions: `open` / `restricted` (denies `bash*`, `fs.write`, `fs.edit`, `subagent`) / `locked` (denies all tools) |
|
|
174
|
+
| `c2c_tool_policy` | `BUB_QQ_C2C_TOOL_POLICY` | `open` | Tool policy for C2C sessions; same values as `group_tool_policy` |
|
|
175
|
+
| `denied_tools` | `BUB_QQ_DENIED_TOOLS` | empty | Extra comma-separated tool-name glob patterns denied under `restricted`, e.g. `web.fetch,tape.*` |
|
|
176
|
+
| `llm_rate_limit_per_minute` | `BUB_QQ_LLM_RATE_LIMIT_PER_MINUTE` | `0` | Max LLM calls per sender per session per minute; `0` disables |
|
|
177
|
+
| `llm_rate_limit_notice` | `BUB_QQ_LLM_RATE_LIMIT_NOTICE` | `请求过于频繁,请稍后再试。` | Reply text used when a sender hits the LLM rate limit |
|
|
178
|
+
| `websocket_intents` | `BUB_QQ_WEBSOCKET_INTENTS` | `1 << 25` | WebSocket identify intents (`GROUP_AND_C2C_EVENT`) |
|
|
179
|
+
| `websocket_use_shard_gateway` | `BUB_QQ_WEBSOCKET_USE_SHARD_GATEWAY` | `false` | Use `/gateway/bot` recommended shard count |
|
|
180
|
+
| `websocket_reconnect_delay_seconds` | `BUB_QQ_WEBSOCKET_RECONNECT_DELAY_SECONDS` | `5` | Base delay before WebSocket reconnect (doubles per consecutive failure) |
|
|
181
|
+
| `websocket_reconnect_max_delay_seconds` | `BUB_QQ_WEBSOCKET_RECONNECT_MAX_DELAY_SECONDS` | `300` | Upper bound for the exponential reconnect backoff |
|
|
182
|
+
| `websocket_max_identify_rejections` | `BUB_QQ_WEBSOCKET_MAX_IDENTIFY_REJECTIONS` | `5` | Stop the client after this many consecutive identify rejections (op 9); `0` disables the limit |
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
qq:
|
|
188
|
+
appid: your_app_id
|
|
189
|
+
secret: your_secret
|
|
190
|
+
receive_mode: websocket
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
export BUB_QQ_APPID=your_app_id
|
|
195
|
+
export BUB_QQ_SECRET=your_secret
|
|
196
|
+
export BUB_QQ_RECEIVE_MODE=websocket
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Which group messages the bot hears is controlled in the QQ client by a group admin setting (all messages / last 10 @mentions / @only). Every received group message wakes the model; `was_mentioned` in the payload is `false` when the bot was not @mentioned.
|
|
200
|
+
|
|
201
|
+
Settings path in the latest mobile QQ client: **open the group chat → tap "More" in the top-right corner → Group Bots → Manage**. There the group owner or an admin can adjust the bot's group message scope and toggle "allow the bot to speak proactively" (pairs with `active_messages`).
|
|
202
|
+
|
|
203
|
+
## Reply modes
|
|
204
|
+
|
|
205
|
+
`reply_mode` decides how model output becomes a QQ message, and — just as important — how the model stays silent (e.g. for un-mentioned group chatter it has nothing to add to). In opt-in/opt-out terms: `direct` is **opt-out** (replying is the default; the model explicitly opts out with a sentinel), while `tool` is **opt-in** (silence is the default; the model explicitly opts in by calling the send tool — the same contract Bub's other channels use). Both modes share the same send pipeline (passive `msg_id`/`msg_seq` targeting, dedupe, markdown fallback, active-message fallback), and a per-mode `<qq_response_instruct>` block is injected into the system prompt so the model knows the active contract.
|
|
206
|
+
|
|
207
|
+
### `direct` (default)
|
|
208
|
+
|
|
209
|
+
The model's final text is forwarded to the chat as-is — delivery never depends on the model calling anything. To skip a reply, the model outputs exactly `<no_reply/>`; the channel swallows it (logged as `qq.send skip_no_reply`) and nothing is sent. Leaked model special tokens (`<|eos|>`, `<|im_end|>`, …) are stripped from the edges of outbound text; output consisting only of such tokens is also treated as silence. Recommended when the configured model's tool-calling reliability is unknown: the failure mode is an unwanted message, never a lost one.
|
|
210
|
+
|
|
211
|
+
### `tool`
|
|
212
|
+
|
|
213
|
+
Direct forwarding is disabled (model output is routed to the `null` channel) and a native `qq.send` tool is registered instead. The model replies by calling `qq.send` with the message text — `msg_id`/`msg_seq` are resolved internally, so the model never touches protocol fields — and stays silent by simply not calling it. This matches Bub's native channel contract and additionally allows several messages per turn. The failure mode is inverted: if the model forgets to call the tool, the reply is silently lost, so use this mode with models whose tool calling you trust.
|
|
214
|
+
|
|
215
|
+
`qq.send` is exempt from the tool policy (`group_tool_policy` / `c2c_tool_policy` / `denied_tools`): it is the reply path itself, which was never gated in direct mode.
|
|
216
|
+
|
|
217
|
+
Notes for `tool` mode:
|
|
218
|
+
|
|
219
|
+
- Comma-command output is always delivered directly in both modes (commands bypass the model).
|
|
220
|
+
- The `llm_rate_limit_notice` text is not delivered in tool mode (the short-circuited turn produces direct output, which tool mode drops); the rate limit itself still applies and is logged.
|
|
221
|
+
|
|
222
|
+
## Security
|
|
223
|
+
|
|
224
|
+
The plugin ships with layered, fail-closed protections for public chats:
|
|
225
|
+
|
|
226
|
+
1. **Allowlists** — when `allow_users` / `allow_groups` are set, messages from anyone else are dropped before reaching the model.
|
|
227
|
+
2. **Comma-command gate** — inbound text starting with `,` runs as a Bub command only for authorized senders: in groups the platform-reported `member_role` must be `owner` / `admin`, or the sender must be in `admin_users`; in C2C only `admin_users` qualify. Everyone else's `,` message is forwarded as plain text.
|
|
228
|
+
3. **Tool policy** — a `before_tool_call` hook denies dangerous tools per scope. Groups default to `restricted` (no `bash*`, `fs.write`, `fs.edit`, `subagent`); C2C defaults to `open`. Authorized senders (rule 2) bypass the policy.
|
|
229
|
+
4. **Rate limit** — a `before_llm_call` hook caps LLM calls per sender per session (`llm_rate_limit_per_minute`) and short-circuits the turn with `llm_rate_limit_notice` when exceeded.
|
|
230
|
+
5. **Audit log** — `after_llm_call` / `after_tool_call` hooks emit `qq.audit.llm` / `qq.audit.tool` log lines with session, sender, role, tool/model, duration, and error type.
|
|
231
|
+
|
|
232
|
+
Note: with no configuration, comma commands are unusable in C2C (fail-closed). Set `admin_users` to your own openid to keep command access.
|
|
233
|
+
|
|
234
|
+
### Ops comma commands
|
|
235
|
+
|
|
236
|
+
The plugin ships model-invisible comma commands (registered with `agent_use=False`) for authorized senders:
|
|
237
|
+
|
|
238
|
+
| Command | Description |
|
|
239
|
+
| --- | --- |
|
|
240
|
+
| `,qq.version` | Show the installed bub-qq plugin version |
|
|
241
|
+
|
|
242
|
+
Any other registered Bub tool can also be run as `,name args`, and an unknown `,name` falls back to executing the line as a bash command — which is why the comma-command gate effectively grants authorized senders full shell access.
|
|
243
|
+
|
|
244
|
+
## Run
|
|
245
|
+
|
|
246
|
+
QQ is a channel listener surface. Start Bub gateway after the plugin is installed and configured:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
bub gateway
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
For webhook mode, expose a public HTTPS URL that reaches the embedded server (host/port/path above) and register it in the QQ bot console. For websocket mode, ensure the console is **not** locked into a successful webhook-only configuration.
|
|
253
|
+
|
|
254
|
+
CLI chat (`bub chat`) does not replace the QQ channel; use gateway for QQ IO.
|
|
255
|
+
|
|
256
|
+
## Session and message mapping
|
|
257
|
+
|
|
258
|
+
| Concept | Format / behavior |
|
|
259
|
+
| --- | --- |
|
|
260
|
+
| Session ID (C2C) | `qq:c2c:<user_openid>` |
|
|
261
|
+
| Chat ID (C2C) | `c2c:<user_openid>` |
|
|
262
|
+
| Session ID (group) | `qq:group:<group_openid>` |
|
|
263
|
+
| Chat ID (group) | `group:<group_openid>` |
|
|
264
|
+
| Inbound event | `C2C_MESSAGE_CREATE`, `GROUP_AT_MESSAGE_CREATE`, `GROUP_MESSAGE_CREATE` |
|
|
265
|
+
| Group activation | every received group message is `is_active=true`; delivery scope is set in the QQ client by a group admin |
|
|
266
|
+
| Command messages | inbound text starting with `,` is forwarded as Bub `kind=command` for authorized senders only (see Security); otherwise treated as plain text |
|
|
267
|
+
| Outbound | Text (`msg_type = 0`), or markdown (`msg_type = 2`) when the reply looks like markdown; **passive reply preferred** (`msg_id` + plugin-managed `msg_seq`), with an optional **active fallback** for groups (`active_messages`, plain text only) |
|
|
268
|
+
| Passive window | passive replies stop once the latest inbound timestamp is older than 60 minutes; groups fall back to active messages when enabled |
|
|
269
|
+
| Active opt-in | `GROUP_MSG_RECEIVE` / `GROUP_MSG_REJECT` (and the C2C twins) are persisted per group/user; sends are skipped when the admin explicitly rejected active messages |
|
|
270
|
+
| Debounce | `needs_debounce = true` |
|
|
271
|
+
|
|
272
|
+
C2C stays passive-only: official docs state active C2C push stopped being provided on 2025-04-21. Group active messages are opt-in on both sides (bot config `active_messages` + the group admin's QQ client switch) and consume platform quota.
|
|
273
|
+
|
|
274
|
+
## Payload shape
|
|
275
|
+
|
|
276
|
+
Inbound non-command messages are encoded as a JSON string, including fields like:
|
|
277
|
+
|
|
278
|
+
- `message`
|
|
279
|
+
- `message_id`
|
|
280
|
+
- `type` (`text` or `attachment`)
|
|
281
|
+
- `sender_id` (C2C `user_openid`, group `member_openid`)
|
|
282
|
+
- `sender_name` / `sender_role` / `group_openid` / `chat_type` / `was_mentioned` (group)
|
|
283
|
+
- `date`
|
|
284
|
+
- `attachments` (when present)
|
|
285
|
+
- `quoted_messages` (when present: quoted message / merged-forward chat record content from `msg_elements`, with `message`, optional `sender_name`, and nested `messages`)
|
|
286
|
+
|
|
287
|
+
In `direct` mode, normal replies should return final text and let Bub outbound routing call `QQChannel.send`; in `tool` mode, replies go through the `qq.send` tool. In both cases `msg_seq` is managed inside the plugin — never invent protocol fields.
|
|
288
|
+
|
|
289
|
+
## Status
|
|
290
|
+
|
|
291
|
+
### Supported today
|
|
292
|
+
|
|
293
|
+
- Config via `qq:` YAML, `BUB_QQ_*`, and `bub onboard`
|
|
294
|
+
- Access token from `https://bots.qq.com/app/getAppAccessToken` with cached refresh (60s renewal window)
|
|
295
|
+
- `aiohttp` OpenAPI client with `Authorization: QQBot {ACCESS_TOKEN}`
|
|
296
|
+
- Embedded webhook receiver, callback validation (`op = 13`), ed25519 signature flows
|
|
297
|
+
- Webhook request verification (`X-Signature-Ed25519`, `X-Signature-Timestamp`)
|
|
298
|
+
- WebSocket receive path with reconnect / resume and optional sharding
|
|
299
|
+
- C2C / group inbound adaptation, `msg_id` dedupe, 60-minute passive text or markdown replies
|
|
300
|
+
- Group text receive/reply; message scope is controlled in the QQ client by a group admin
|
|
301
|
+
- In-memory send idempotency for the same `session_id + msg_id + msg_seq`
|
|
302
|
+
- OpenAPI error surfacing (HTTP status, platform `code` / `err_code`, trace_id from the response header or body) and error catalog metadata
|
|
303
|
+
- Layered security: allowlists, role-gated comma commands, per-scope tool policy, LLM rate limiting, and audit logs (see Security)
|
|
304
|
+
- Proactive group messages as a passive-reply fallback (`active_messages`), with persisted per-group/user opt-in state from `*_MSG_RECEIVE` / `*_MSG_REJECT` events
|
|
305
|
+
- claw_cfg round-trip: `INTERACTION_CREATE` 2002 updates are persisted per group and 2001 queries echo the real `require_mention` state
|
|
306
|
+
- 304023/304024 (async manual audit) treated as pending success instead of a failed send
|
|
307
|
+
- Selective silence in both reply modes: `<no_reply/>` sentinel filtering (`direct`) and reply-by-tool with silence-by-omission (`tool`), driven by an injected per-mode system prompt block
|
|
308
|
+
- Automated tests for config, auth, signatures, channel, webhook, websocket, gateway, plugin onboarding, C2C/group services, security policies, reply modes, and the platform store
|
|
309
|
+
|
|
310
|
+
### Not yet
|
|
311
|
+
|
|
312
|
+
- QQ Guild and rich-media send/receive
|
|
313
|
+
- Wider webhook event coverage beyond validation, basic `{"op":12}` ack, C2C/group messages, message-toggle events, and interaction query/update
|
|
314
|
+
- Active C2C push (discontinued by the platform on 2025-04-21)
|
|
315
|
+
- Markdown in active group messages (requires a registered template; active path sends plain text)
|
|
316
|
+
- Dynamic in-process shard rebalancing after startup
|
|
317
|
+
|
|
318
|
+
## Confirmed interface rules
|
|
319
|
+
|
|
320
|
+
From official QQ Bot docs (API auth + event subscription):
|
|
321
|
+
|
|
322
|
+
**Auth / OpenAPI**
|
|
323
|
+
|
|
324
|
+
- Token: `POST https://bots.qq.com/app/getAppAccessToken` body `{ appId, clientSecret }`
|
|
325
|
+
- Token lifetime up to `7200` seconds; refresh within `60` seconds of expiry returns a new token while the old remains valid during the overlap
|
|
326
|
+
- OpenAPI unified endpoint: `https://api.bot.qq.com` (the legacy `https://api.sgroup.qq.com` can be restored via `openapi_base_url`)
|
|
327
|
+
- Header: `Authorization: QQBot {ACCESS_TOKEN}`
|
|
328
|
+
- Failure response body carries `err_code`, `message`, `trace_id` (legacy format uses `code`; the plugin accepts both); trace_id is also exposed via the `X-Tps-trace-ID` response header
|
|
329
|
+
|
|
330
|
+
**Events / transport**
|
|
331
|
+
|
|
332
|
+
- Production webhooks require HTTPS; ports `80`, `443`, `8080`, `8443`
|
|
333
|
+
- Webhook and WebSocket are mutually exclusive once a valid HTTPS callback is configured
|
|
334
|
+
- Validation requests use `op = 13`; response must include `plain_token` and ed25519 signature over `event_ts + plain_token`
|
|
335
|
+
- Normal webhook verification uses `timestamp + raw_body`
|
|
336
|
+
- Event payload shape: `{ id, op, d, s, t }`
|
|
337
|
+
- `C2C_MESSAGE_CREATE` / `GROUP_AT_MESSAGE_CREATE` intent: `GROUP_AND_C2C_EVENT` (`1 << 25`)
|
|
338
|
+
- Documented `C2C_MESSAGE_CREATE.d` fields used here: `id`, `author.user_openid`, `content`, `timestamp`, `attachments`
|
|
339
|
+
- Group event `d` fields used here: `id`, `group_openid`, `author.member_openid`, `content`, `timestamp`, `mentions`, `attachments`
|
|
340
|
+
- Group send: `POST /v2/groups/{group_openid}/messages` with the same body as C2C (`msg_id`, `msg_seq`, plus either `content` + `msg_type = 0` or `markdown.content` + `msg_type = 2`)
|
|
341
|
+
- WebSocket close codes `4914` / `4915` are fatal; codes such as `4006`–`4009` and `4900`–`4913` are treated as reconnectable
|
|
342
|
+
|
|
343
|
+
## Official documentation
|
|
344
|
+
|
|
345
|
+
- [QQ Bot Developer Documentation](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/getting-started.html)
|
|
346
|
+
- [Bub docs](https://bub.build/)
|
|
347
|
+
- [Bub plugin hub](https://hub.bub.build/)
|
|
348
|
+
|
|
349
|
+
Use the QQ docs for app creation, credentials, event subscription, and callback settings (`APPID`, `SECRET`, webhook URL, intents, etc.).
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
uv run --package bub-qq pytest -q
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Tests use mocks — no live QQ network required.
|
|
358
|
+
|
|
359
|
+
## License
|
|
360
|
+
|
|
361
|
+
Same as the [bub-contrib](https://github.com/bubbuild/bub-contrib) repository.
|