@jerryliang122/openclaw-qqbot 1.0.0

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.
Files changed (120) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +967 -0
  3. package/README.zh.md +790 -0
  4. package/dist/index.cjs +18072 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +1222 -0
  7. package/index.ts +92 -0
  8. package/openclaw.plugin.json +38 -0
  9. package/package.json +69 -0
  10. package/preload.cjs +19 -0
  11. package/scripts/link-sdk-core.cjs +268 -0
  12. package/scripts/proactive-api-server.ts +369 -0
  13. package/scripts/send-proactive.ts +293 -0
  14. package/scripts/test-sendmedia.ts +116 -0
  15. package/skills/qqbot-channel/SKILL.md +285 -0
  16. package/skills/qqbot-channel/references/api_references.md +521 -0
  17. package/skills/qqbot-remind/SKILL.md +159 -0
  18. package/skills/qqbot-upgrade/SKILL.md +56 -0
  19. package/src/adapter/contract.ts +63 -0
  20. package/src/adapter/lint.ts +144 -0
  21. package/src/adapter/media.ts +40 -0
  22. package/src/adapter/pairing.ts +95 -0
  23. package/src/adapter/resolve.ts +255 -0
  24. package/src/adapter/setup.ts +13 -0
  25. package/src/adapter/webhook.ts +248 -0
  26. package/src/adapter/workspace.ts +21 -0
  27. package/src/agent-prompt-adapter.ts +26 -0
  28. package/src/bot-instance.ts +60 -0
  29. package/src/channel.ts +230 -0
  30. package/src/commands/bot-approve.ts +143 -0
  31. package/src/commands/bot-clear-storage.ts +114 -0
  32. package/src/commands/bot-group-always.ts +62 -0
  33. package/src/commands/bot-group-info.ts +48 -0
  34. package/src/commands/bot-help.ts +40 -0
  35. package/src/commands/bot-logs.ts +248 -0
  36. package/src/commands/bot-me.ts +18 -0
  37. package/src/commands/bot-pairing.ts +50 -0
  38. package/src/commands/bot-ping.ts +33 -0
  39. package/src/commands/bot-streaming.ts +55 -0
  40. package/src/commands/bot-upgrade.ts +56 -0
  41. package/src/commands/bot-version.ts +41 -0
  42. package/src/commands/config-util.ts +96 -0
  43. package/src/commands/index.ts +51 -0
  44. package/src/config.ts +403 -0
  45. package/src/constants.ts +6 -0
  46. package/src/dispatch/body-assembler.ts +308 -0
  47. package/src/dispatch/ctx-builder.ts +127 -0
  48. package/src/dispatch/dispatch.ts +667 -0
  49. package/src/dispatch/envelope-builder.ts +112 -0
  50. package/src/dispatch/index.ts +2 -0
  51. package/src/features/approval-capability.ts +302 -0
  52. package/src/features/approval-helpers.ts +271 -0
  53. package/src/features/approval-utils.ts +21 -0
  54. package/src/features/command-panel.ts +301 -0
  55. package/src/features/credential-backup.ts +74 -0
  56. package/src/features/group-mode-store.ts +79 -0
  57. package/src/features/history-store.ts +75 -0
  58. package/src/features/msgid-cache.ts +55 -0
  59. package/src/features/outbound-echo-store.ts +46 -0
  60. package/src/features/proactive-budget.ts +57 -0
  61. package/src/features/proactive.ts +549 -0
  62. package/src/features/question-helpers.ts +771 -0
  63. package/src/features/quota-manager.ts +173 -0
  64. package/src/features/ref-index-store.ts +289 -0
  65. package/src/features/secret-input-store.ts +118 -0
  66. package/src/features/secret-store-cli.ts +324 -0
  67. package/src/features/typing-refresh.ts +51 -0
  68. package/src/features/update-checker.ts +166 -0
  69. package/src/gateway/event-handlers.ts +456 -0
  70. package/src/gateway/index.ts +3 -0
  71. package/src/gateway/lifecycle.ts +236 -0
  72. package/src/gateway/middleware-setup.ts +173 -0
  73. package/src/gateway/qqbot-gateway.ts +458 -0
  74. package/src/gateway-adapter.ts +44 -0
  75. package/src/heartbeat-adapter.ts +57 -0
  76. package/src/message-adapter.ts +40 -0
  77. package/src/messaging-adapter.ts +78 -0
  78. package/src/middleware/access-control.ts +125 -0
  79. package/src/middleware/attachment.ts +373 -0
  80. package/src/middleware/inbound-guard.ts +102 -0
  81. package/src/middleware/policy-injector.ts +71 -0
  82. package/src/middleware/secret-capture.ts +161 -0
  83. package/src/middleware/typing.ts +110 -0
  84. package/src/openclaw-plugin-sdk.d.ts +543 -0
  85. package/src/outbound/chunker.ts +80 -0
  86. package/src/outbound/debounce.ts +102 -0
  87. package/src/outbound/deliver-pipeline.ts +235 -0
  88. package/src/outbound/index.ts +3 -0
  89. package/src/outbound/local-file-router.ts +145 -0
  90. package/src/outbound/media-send.ts +408 -0
  91. package/src/outbound/outbound-service.ts +298 -0
  92. package/src/outbound/reply-limiter.ts +139 -0
  93. package/src/outbound/sanitize.ts +32 -0
  94. package/src/outbound/streaming-controller.ts +332 -0
  95. package/src/outbound/target.ts +109 -0
  96. package/src/outbound-adapter.ts +323 -0
  97. package/src/plugin-base.ts +42 -0
  98. package/src/request-context.ts +50 -0
  99. package/src/runtime.ts +42 -0
  100. package/src/setup/account-key.ts +41 -0
  101. package/src/setup/finalize.ts +110 -0
  102. package/src/setup/login.ts +197 -0
  103. package/src/setup/surface.ts +40 -0
  104. package/src/status-adapter.ts +56 -0
  105. package/src/tools/platform.ts +149 -0
  106. package/src/tools/remind.ts +308 -0
  107. package/src/tools/secret-input.ts +185 -0
  108. package/src/types-augment.d.ts +54 -0
  109. package/src/types-plugin.ts +82 -0
  110. package/src/types.ts +620 -0
  111. package/src/typing-lifecycle.ts +182 -0
  112. package/src/utils/mention.ts +52 -0
  113. package/src/utils/pkg-version.ts +23 -0
  114. package/src/utils/platform.ts +459 -0
  115. package/src/utils/plugin-logger.ts +104 -0
  116. package/src/utils/ssrf-guard.ts +132 -0
  117. package/src/utils/stt.ts +150 -0
  118. package/src/utils/voice-text.ts +61 -0
  119. package/tsconfig.json +17 -0
  120. package/tsup.config.ts +64 -0
package/README.md ADDED
@@ -0,0 +1,967 @@
1
+ <div align="center">
2
+
3
+ <img width="120" src="https://img.shields.io/badge/🤖-QQ_Bot-blue?style=for-the-badge" alt="QQ Bot" />
4
+
5
+ # QQ Bot Channel Plugin for OpenClaw
6
+
7
+ **Independently maintained fork — framework-delegated group queueing, room-event ingestion for full-mode groups, and passive-first outbound delivery**
8
+
9
+ **Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
10
+
11
+ ### 🚀 Current Version: `v1.0.0`
12
+
13
+ [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
14
+ [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
15
+ [![Platform](https://img.shields.io/badge/OpenClaw-%3E%3D2026.9.2-orange)](https://github.com/jerryliang122/qqbot-openclaw)
16
+ [![Node.js](https://img.shields.io/badge/Node.js->=18-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
17
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
18
+ [![Fork](https://img.shields.io/badge/fork-enhanced-9cf)](https://github.com/jerryliang122/qqbot-openclaw)
19
+
20
+ <br/>
21
+
22
+ **[简体中文](README.zh.md) | English**
23
+
24
+ > This is an **independently maintained fork** with its own versioning (v1.x, decoupled from the upstream 2.x line). Upstream: [tencent-connect/openclaw-qqbot](https://github.com/tencent-connect/openclaw-qqbot).
25
+ >
26
+ > **Requirements**: OpenClaw `>= 2026.9.2` · Published to npm as [`@jerryliang122/openclaw-qqbot`](https://www.npmjs.com/package/@jerryliang122/openclaw-qqbot). See [CHANGELOG](CHANGELOG.md) for differences vs the old version and the upgrade guide.
27
+
28
+ Scan to join the QQ group chat
29
+
30
+ <img width="400" alt="QQ QR Code" src="./docs/images/developer-group.png" />
31
+
32
+ </div>
33
+
34
+ ---
35
+
36
+ ## ✨ Features
37
+
38
+ | Feature | Description |
39
+ |---------|-------------|
40
+ | 🔄 **Group Queueing** | Group messages dispatch immediately; queueing/merging is delegated to the OpenClaw framework followup queue (collect mode) — active turns are never interrupted, bursts merge into one batch |
41
+ | 👁️ **Room Events** (opt-in) | In groups with full-message push mode, the bot can read every message like a normal member; un-@'d traffic arrives as passive room events (read-only — the AI speaks only via the proactive `message` tool) |
42
+ | 🔔 **Three Wake Modes** | @mention, name patterns (`mentionPatterns`, e.g. "沈处"), and quote-of-bot-message all trigger normal replies |
43
+ | 📤 **Passive-First Outbound** | Every send prefers passive reply (msg_id) to conserve the 1000/day proactive budget; quota-aware fallback never hard-fails |
44
+ | 🔒 **Multi-Scene** | C2C private chat, group chat (@mention / autonomous / room-event modes) |
45
+ | 👥 **Group Fine-Tuning** | Per-group @trigger rules, tool policies, custom prompts, history modes, queueing config, room-event policy |
46
+ | 🌐 **Dual Transport** | WebSocket (default) or Webhook (HTTP callback) — switch via config |
47
+ | 🖼️ **Rich Media** | Send & receive images, voice, video, and files |
48
+ | 🎙️ **Voice (STT/TTS)** | Speech-to-text transcription & text-to-speech replies |
49
+ | 🔄 **Update Check** | `/bot-upgrade` checks the npm registry for new versions and links the upgrade guide |
50
+ | ⏰ **Scheduled Push** | Proactive message delivery via scheduled tasks |
51
+ | 🔗 **URL Support** | Direct URL sending in private chat (no restrictions) |
52
+ | ⌨️ **Typing Indicator** | "Bot is typing..." status shown in real-time |
53
+ | 📝 **Markdown** | Full Markdown formatting support |
54
+ | 🛠️ **Commands** | Native OpenClaw command integration |
55
+ | 💬 **Quoted Context** | Parses the original message a user is replying to and injects it into AI context, so the model always knows exactly which message is being referenced |
56
+ | 📦 **Large File Support** | Auto chunked upload for large files (parallel upload with retry), up to 100 MB |
57
+ | 🔐 **Command Execution Approval** | AI requests approval via Inline Keyboard buttons before executing commands — tap to allow or deny |
58
+
59
+ ---
60
+
61
+ ## 📸 Feature Showcase
62
+
63
+ > **Note:** This plugin serves as a **message channel** only — it relays messages between QQ and OpenClaw. Capabilities like image understanding, voice transcription, drawing, etc. depend on the **AI model** you configure and the **skills** installed in OpenClaw, not on this plugin itself.
64
+
65
+ ### 💬 Quoted Message Context
66
+
67
+ When a user quotes a message in QQ, the plugin automatically parses the quoted message content and injects it into the AI context, so the model clearly knows "which message the user is replying to" and gives more accurate responses. Supports text and media messages (image/voice/video/file), and works across devices.
68
+
69
+ <img width="360" src="docs/images/ref-msg.png" alt="Quoted Message Context Demo" />
70
+
71
+ ### 🎙️ Voice Messages (STT)
72
+
73
+ With STT configured, the plugin automatically transcribes voice messages to text before passing them to AI. The whole process is transparent to the user — sending voice feels as natural as sending text.
74
+
75
+ > **You**: *(send a voice message)* "What's the weather like tomorrow in Shenzhen?"
76
+ >
77
+ > **QQBot**: Tomorrow (March 7, Saturday) Shenzhen weather forecast 🌤️ ...
78
+
79
+ <img width="360" src="docs/images/voice-stt.jpg" alt="Voice STT Demo" />
80
+
81
+ ### 📄 File Understanding
82
+
83
+ Send any file to the bot — novels, reports, spreadsheets — AI automatically recognizes the content and gives an intelligent reply.
84
+
85
+ > **You**: *(send a TXT file of "War and Peace")*
86
+ >
87
+ > **QQBot**: Got it! You uploaded the Chinese version of "War and Peace" by Leo Tolstoy. This appears to be the opening of Chapter 1...
88
+
89
+ <img width="360" src="docs/images/file-understand.jpg" alt="File Understanding Demo" />
90
+
91
+ ### 🖼️ Image Understanding
92
+
93
+ If your main model supports vision (e.g. Tencent Hunyuan `hunyuan-vision`), AI can understand images too. This is a general multimodal capability, not plugin-specific.
94
+
95
+ > **You**: *(send an image)*
96
+ >
97
+ > **QQBot**: Haha, so cute! Is that a QQ penguin in a lobster costume? 🦞🐧 ...
98
+
99
+ <img width="360" src="docs/images/image-understand.jpg" alt="Image Understanding Demo" />
100
+
101
+ ### 🎨 Image Sending
102
+
103
+ > **You**: Draw me a cat
104
+ >
105
+ > **QQBot**: Here you go! 🐱
106
+
107
+ AI can send images directly. Supports local paths and URLs. Formats: jpg/png/gif/webp/bmp.
108
+
109
+ <img width="360" src="docs/images/image-send.jpg" alt="Image Generation Demo" />
110
+
111
+ ### 🔊 Voice Sending
112
+
113
+ > **You**: Tell me a joke in voice
114
+ >
115
+ > **QQBot**: *(sends a voice message)*
116
+
117
+ AI can send voice messages directly. Formats: mp3/wav/silk/ogg. No ffmpeg required.
118
+
119
+ <img width="360" src="docs/images/voice-send.jpg" alt="TTS Voice Demo" />
120
+
121
+ ### ⏰ Scheduled Reminder (Proactive Message)
122
+
123
+ > **You**: Remind me to eat in 5 minutes
124
+ >
125
+ > **QQBot**: confirms the reminder first, then proactively sends a voice + text reminder when time is up
126
+
127
+ This capability depends on OpenClaw cron scheduling and proactive messaging. If no reminder arrives, a common reason is QQ-side interception of bot proactive messages.
128
+
129
+ <img width="360" src="docs/images/reminder.jpg" alt="Scheduled Reminder Demo" />
130
+
131
+ ### 📎 File Sending
132
+
133
+ > **You**: Extract chapter 1 of War and Peace and send it as a file
134
+ >
135
+ > **QQBot**: *(sends a .txt file)*
136
+
137
+ AI can send files directly, in any format.
138
+
139
+ <img width="360" src="docs/images/file-send.jpg" alt="File Sending Demo" />
140
+
141
+ Large file transfer is supported: images up to 20MB, videos up to 30MB, attachments up to 100MB, with a daily transfer limit of 2GB.
142
+
143
+ <img width="360" src="docs/images/large-file-transfer.jpg" alt="Large File Transfer Demo" />
144
+
145
+ ### 🔐 Command Execution Approval
146
+
147
+ When the AI needs to execute a command, the plugin sends an approval request via QQ message with interactive buttons — tap **✅ Allow Once**, **⭐ Always Allow**, or **❌ Deny** to control whether the command runs.
148
+
149
+ Use the `/bot-approve` command to manage the approval mode (allowlist / off / strict).
150
+
151
+ <img width="360" src="docs/images/approve.png" alt="Command Execution Approval Demo" />
152
+
153
+ ### 🎬 Video Sending
154
+
155
+ > **You**: Send me a demo video
156
+ >
157
+ > **QQBot**: *(sends a video)*
158
+
159
+ AI can send videos directly. Supports local files and URLs.
160
+
161
+ <img width="360" src="docs/images/video-send.jpg" alt="Video Sending Demo" />
162
+
163
+ > **Under the hood:** Upload dedup caching, ordered queue delivery, and multi-layer audio format fallback.
164
+
165
+ ### 🛠️ Slash Commands
166
+
167
+ The plugin provides built-in slash commands that are intercepted before reaching the AI queue, giving instant responses for diagnostics and management.
168
+
169
+ #### `/bot-ping` — Latency Test
170
+
171
+ > **You**: `/bot-ping`
172
+ >
173
+ > **QQBot**: ✅ pong!⏱ Latency: 602ms (network: 602ms, plugin: 0ms)
174
+
175
+ Measures end-to-end latency from QQ server push to plugin response, broken down into network transport and plugin processing time.
176
+
177
+ <img width="360" src="docs/images/slash-ping.jpg" alt="Ping Demo" />
178
+
179
+ #### `/bot-version` — Version Info
180
+
181
+ > **You**: `/bot-version`
182
+ >
183
+ > **QQBot**: 🦞 Framework: OpenClaw 2026.9.2 / 🤖 Plugin: v1.0.0 / 🌟 GitHub repo
184
+
185
+ Shows framework version, plugin version, and a direct link to the official repository.
186
+
187
+ <img width="360" src="docs/images/slash-version.jpg" alt="Version Demo" />
188
+
189
+ #### `/bot-help` — Command List
190
+
191
+ > **You**: `/bot-help`
192
+ >
193
+ > **QQBot**: Lists all available slash commands with clickable shortcuts.
194
+
195
+ <img width="360" src="docs/images/slash-help.jpg" alt="Help Demo" />
196
+
197
+ #### `/bot-upgrade` — Version Check & Upgrade Guide
198
+
199
+ > **You**: `/bot-upgrade`
200
+ >
201
+ > **QQBot**: 📌 Current: v1.0.0 / 🆕 New version available / 📖 Upgrade guide link
202
+
203
+ Checks the installed version against the npm registry (`@jerryliang122/openclaw-qqbot`) and returns a link to the upgrade guide (repo CHANGELOG by default; override with `channels.qqbot.upgradeUrl`). Actual upgrading is done on the host via `openclaw plugins install` — see [Getting Started](#-getting-started).
204
+
205
+ <img width="360" src="docs/images/hot-update.jpg" alt="Upgrade Demo" />
206
+
207
+ #### `/bot-logs` — Log Export
208
+
209
+ > **You**: `/bot-logs`
210
+ >
211
+ > **QQBot**: 📋 Logs packaged (~2000 lines), sending file... *(sends a .txt file)*
212
+
213
+ Exports the last ~2000 lines of gateway logs as a file for quick troubleshooting.
214
+
215
+ <img width="360" src="docs/images/slash-logs.jpg" alt="Logs Demo" />
216
+
217
+ #### Usage Help
218
+
219
+ All commands support a `?` suffix to show usage:
220
+
221
+ > **You**: `/bot-upgrade ?`
222
+ >
223
+ > **QQBot**: 📖 /bot-upgrade usage: …
224
+
225
+ #### `/bot-approve` — Approval Configuration
226
+
227
+ > **You**: `/bot-approve`
228
+ >
229
+ > **QQBot**: 🔐 Command Execution Approval — Enable / Disable / Strict mode / Reset / View current config
230
+
231
+ Manage the AI command execution approval policy. Supported subcommands:
232
+
233
+ | Subcommand | Description |
234
+ |------------|-------------|
235
+ | `/bot-approve on` | Enable approval (allowlist mode, recommended) |
236
+ | `/bot-approve off` | Disable approval — commands execute directly |
237
+ | `/bot-approve always` | Strict mode — every execution requires approval |
238
+ | `/bot-approve reset` | Restore framework defaults |
239
+ | `/bot-approve status` | View current approval config |
240
+
241
+ #### `/bot-clear-storage` — Clear files generated through QQBot conversations and downloaded resources (stored on the host running OpenClaw)
242
+
243
+ `/bot-clear-storage` lists files generated by the conversation and files in the downloaded resources directory. Use `/bot-clear-storage --force` to confirm deletion.
244
+
245
+ #### `/bot-group-always` — Group Response Mode Toggle
246
+
247
+ > **You**: `/bot-group-always`
248
+ >
249
+ > **QQBot**: 🤖 Group autonomous mode: ❌ @mention required
250
+
251
+ Toggle group @trigger behavior at runtime — changes persist instantly, no restart needed:
252
+
253
+ | Subcommand | Description |
254
+ |------------|-------------|
255
+ | `/bot-group-always on` | AI decides when to speak autonomously (no @ needed) |
256
+ | `/bot-group-always off` | Only respond when @mentioned |
257
+ | `/bot-group-always` (no arg) | View current setting |
258
+
259
+ > ⚠️ This command modifies the account-level `defaultRequireMention`. It has lower priority than per-group `groups.{groupId}.requireMention` settings.
260
+
261
+ #### `/bot-group-info` — Group Push Mode & Effective Config (in-group)
262
+
263
+ > **You**: `/bot-group-info` *(sent in a group)*
264
+ >
265
+ > **QQBot**: 🤖 群信息 — push mode inference (AT / full), requireMention, queueing strategy, history mode, room-event policy, today's proactive message usage
266
+
267
+ Answers "why does this group have no context" diagnostics: the push mode is chosen by the **group owner** when adding the bot (AT only / AT + recent N / full), and this command shows what the plugin actually observes plus every effective config value.
268
+
269
+ ---
270
+
271
+ ## 🚀 Getting Started
272
+
273
+ ### Step 1 — Create a QQ Bot on the QQ Open Platform
274
+
275
+ 1. Go to the [QQ Open Platform](https://q.qq.com/) and **scan the QR code with your phone QQ** to register / log in. If you haven't registered before, scanning will automatically complete the registration and bind your QQ account.
276
+
277
+ <img width="3246" height="1886" alt="Clipboard_Screenshot_1772980354" src="https://github.com/user-attachments/assets/d8491859-57e8-47e4-9d39-b21138be54d0" />
278
+
279
+ 2. After scanning, tap **Agree** on your phone — you'll land on the bot configuration page.
280
+ 3. Click **Create Bot** to create a new QQ bot.
281
+
282
+ <img width="720" alt="Create Bot" src="docs/images/create-robot.png" />
283
+
284
+ > ⚠️ The bot will automatically appear in your QQ message list and send a first message. However, it will reply "The bot has gone to Mars" until you complete the configuration steps below.
285
+
286
+ <img width="400" alt="Bot Say Hello" src="docs/images/bot-say-hello.jpg" />
287
+
288
+ 4. Find **AppID** and **AppSecret** on the bot's page, click **Copy** for each, and save them somewhere safe (e.g., a notepad). **AppSecret is not stored in plaintext — if you leave the page without saving it, you'll have to regenerate a new one.**
289
+
290
+ <img width="720" alt="Find AppID and AppSecret" src="docs/images/find-appid-secret.png" />
291
+
292
+ > For a step-by-step walkthrough with screenshots, see the [official guide](https://cloud.tencent.com/developer/article/2626045).
293
+
294
+ ### Step 2 — Install / Upgrade the Plugin
295
+
296
+ > **Note**: The unscoped npm name `openclaw-qqbot` belongs to the original upstream project — this fork publishes as the scoped package `@jerryliang122/openclaw-qqbot`. Requires OpenClaw >= 2026.9.2.
297
+
298
+ **Option A: Install from npm (Recommended)**
299
+
300
+ ```bash
301
+ openclaw plugins install @jerryliang122/openclaw-qqbot
302
+
303
+ # Or a specific version
304
+ openclaw plugins install @jerryliang122/openclaw-qqbot@1.0.0
305
+ ```
306
+
307
+ **Option B: Install from GitHub**
308
+
309
+ ```bash
310
+ openclaw plugins install git+https://github.com/jerryliang122/qqbot-openclaw.git
311
+
312
+ # Or a specific release tag
313
+ openclaw plugins install git+https://github.com/jerryliang122/qqbot-openclaw.git#v1.0.0
314
+ ```
315
+
316
+ **Option C: Install from Local Source**
317
+
318
+ ```bash
319
+ # Clone the repo
320
+ git clone https://github.com/jerryliang122/qqbot-openclaw.git
321
+ cd openclaw-qqbot
322
+
323
+ # Build
324
+ npm install
325
+ npm run build
326
+
327
+ # Install to OpenClaw (method 1: link)
328
+ openclaw plugins link .
329
+
330
+ # Or install to OpenClaw (method 2: pack)
331
+ npm pack
332
+ openclaw plugins install ./openclaw-qqbot-1.0.0.tgz
333
+ ```
334
+
335
+ **Upgrading from the old (upstream 2.x) version?** See the [CHANGELOG](CHANGELOG.md) — it lists every breaking change (removed config keys, env vars, and behaviors) and the migration table.
336
+
337
+ **Option C: Configure Credentials**
338
+
339
+ After installation, configure your QQ bot credentials:
340
+
341
+ ```bash
342
+ # Via QR code (recommended)
343
+ openclaw channels login --channel qqbot
344
+
345
+ # Or manually
346
+ openclaw channels add --channel qqbot --token "AppID:AppSecret"
347
+
348
+ # Start / restart
349
+ openclaw gateway restart
350
+ ```
351
+
352
+ > Environment variables `QQBOT_APPID` / `QQBOT_SECRET` are also supported.
353
+
354
+
355
+ ### Step 3 — Test
356
+
357
+ Open QQ, find your bot, and send a message!
358
+
359
+ <div align="center">
360
+ <img width="500" alt="Chat Demo" src="https://github.com/user-attachments/assets/b2776c8b-de72-4e37-b34d-e8287ce45de1" />
361
+ </div>
362
+
363
+ ---
364
+
365
+ ## ⚙️ Advanced Configuration
366
+
367
+ ### Multi-Account Setup (Multi-Bot)
368
+
369
+ Run multiple QQ bots under a single OpenClaw instance.
370
+
371
+ #### Configuration
372
+
373
+ Edit `~/.openclaw/openclaw.json` and add an `accounts` field under `channels.qqbot`:
374
+
375
+ ```json
376
+ {
377
+ "channels": {
378
+ "qqbot": {
379
+ "enabled": true,
380
+ "appId": "111111111",
381
+ "clientSecret": "secret-of-bot-1",
382
+
383
+ "accounts": {
384
+ "bot2": {
385
+ "enabled": true,
386
+ "appId": "222222222",
387
+ "clientSecret": "secret-of-bot-2"
388
+ },
389
+ "bot3": {
390
+ "enabled": true,
391
+ "appId": "333333333",
392
+ "clientSecret": "secret-of-bot-3"
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ ```
399
+
400
+ **Notes:**
401
+
402
+ - The top-level `appId` / `clientSecret` is the **default account** (accountId = `"default"`)
403
+ - Each key under `accounts` (e.g. `bot2`, `bot3`) is the `accountId` for that bot
404
+ - Each account can independently configure `enabled`, `name`, `allowFrom`, `systemPrompt`, etc.
405
+ - You may also skip the top-level default account and only configure bots inside `accounts`
406
+
407
+ Add a second bot via CLI (if the framework supports the `--account` parameter):
408
+
409
+ ```bash
410
+ openclaw channels add --channel qqbot --account bot2 --token "222222222:secret-of-bot-2"
411
+ ```
412
+
413
+ #### Sending Messages to a Specific Account's Users
414
+
415
+ When using `openclaw message send`, specify which bot to use with the `--account` parameter:
416
+
417
+ ```bash
418
+ # Send with the default bot (no --account = uses "default")
419
+ openclaw message send --channel "qqbot" \
420
+ --target "qqbot:c2c:OPENID" \
421
+ --message "hello from default bot"
422
+
423
+ # Send with bot2
424
+ openclaw message send --channel "qqbot" \
425
+ --account bot2 \
426
+ --target "qqbot:c2c:OPENID" \
427
+ --message "hello from bot2"
428
+ ```
429
+
430
+ **Target Formats:**
431
+
432
+ | Format | Description |
433
+ |--------|-------------|
434
+ | `qqbot:c2c:OPENID` | Private chat (C2C) |
435
+ | `qqbot:group:GROUP_OPENID` | Group chat |
436
+ | `qqbot:channel:CHANNEL_ID` | Guild channel |
437
+
438
+ > ⚠️ **Important**: Each bot has its own set of user OpenIDs. An OpenID received by Bot A **cannot** be used to send messages via Bot B — this will result in a 500 error. Always use the matching bot's `accountId` to send messages to its users.
439
+
440
+ #### How It Works
441
+
442
+ - When `openclaw gateway` starts, all accounts with `enabled: true` launch their own connections (WebSocket or Webhook depending on `transport` config)
443
+ - Each account maintains an independent Token cache (isolated by `appId`), preventing cross-contamination
444
+ - Incoming message logs are prefixed with `[qqbot:accountId]` for easy debugging
445
+
446
+ ---
447
+
448
+ ### Webhook Transport Mode
449
+
450
+ By default, the plugin connects to QQ via **WebSocket** (outbound connection, no public IP required). You can switch to **Webhook** mode where QQ platform POSTs events to your HTTP endpoint.
451
+
452
+ | | WebSocket (default) | Webhook |
453
+ |---|---|---|
454
+ | Connection | Plugin connects to QQ gateway | QQ platform POSTs to your server |
455
+ | Public IP | Not required | Required |
456
+ | Use case | Development, single instance | Production, horizontal scaling, Serverless |
457
+ | Session resume | Supported (RESUME) | Stateless, no resume needed |
458
+ | Signature | Built-in | Ed25519 auto-verified by plugin |
459
+
460
+ #### Configuration
461
+
462
+ ```json
463
+ {
464
+ "channels": {
465
+ "qqbot": {
466
+ "appId": "111111111",
467
+ "clientSecret": "your-secret",
468
+ "transport": "webhook",
469
+ "webhook": {
470
+ "path": "/qqbot/webhook"
471
+ }
472
+ }
473
+ }
474
+ }
475
+ ```
476
+
477
+ | Field | Default | Description |
478
+ |-------|---------|-------------|
479
+ | `transport` | `"websocket"` | `"websocket"` or `"webhook"` |
480
+ | `webhook.path` | `"/qqbot/webhook"` | HTTP path for receiving callbacks |
481
+
482
+ #### Platform Setup
483
+
484
+ 1. Go to [QQ Open Platform](https://q.qq.com/) → Bot Settings → Message Receiving
485
+ 2. Select **HTTP Callback**
486
+ 3. Enter your callback URL: `https://your-domain.com/qqbot/webhook`
487
+ 4. The platform sends an `op:13` validation request — the plugin handles it automatically
488
+ 5. Once validated, all events will be POSTed to your endpoint
489
+
490
+ ---
491
+
492
+ ### Group Chat Configuration
493
+
494
+ The plugin provides flexible group chat controls, allowing you to customize trigger rules, tool permissions, and AI behavior per group.
495
+
496
+ #### @Mention Trigger Mode (`requireMention`)
497
+
498
+ By default, the bot **only responds when @mentioned** in a group. You can configure it to autonomously decide when to speak:
499
+
500
+ | Mode | Config Value | Behavior |
501
+ |------|-------------|----------|
502
+ | **@ only** | `true` (default) | Only messages that @mention the bot trigger AI processing. Non-@ messages are still cached in history but don't trigger AI |
503
+ | **Autonomous** | `false` | AI decides on its own whether each message needs a reply — no @ required |
504
+
505
+ > **Important**: Even when `requireMention: true`, non-@ messages are **still cached** in the group history buffer. They just don't trigger AI processing.
506
+
507
+ **Priority chain** (highest to lowest):
508
+
509
+ ```
510
+ groups.{groupOpenid}.requireMention
511
+ > groups."*".requireMention
512
+ > account-level defaultRequireMention
513
+ > default value true
514
+ ```
515
+
516
+ **Example:**
517
+
518
+ ```json
519
+ {
520
+ "channels": {
521
+ "qqbot": {
522
+ // Account-level default for all groups
523
+ "defaultRequireMention": false,
524
+
525
+ "accounts": {
526
+ "default": {
527
+ "groups": {
528
+ "*": {
529
+ // Wildcard fallback for all groups
530
+ "requireMention": false
531
+ },
532
+ "GROUP_OPENID": {
533
+ // Per-group override — this group still requires @
534
+ "requireMention": true
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+ ```
543
+
544
+ > **Use cases:**
545
+ >
546
+ > - Work groups → `requireMention: true` — avoid AI chiming in on every casual message
547
+ > - Dedicated AI companion groups → `requireMention: false` — participate naturally like a real person
548
+ > - Use [`/bot-group-always`](#bot-group-always--group-response-mode-toggle) to toggle account-level defaults at runtime
549
+
550
+ #### Additional Group Config Fields
551
+
552
+ Besides `requireMention`, each group supports these settings:
553
+
554
+ | Field | Type | Default | Description |
555
+ |-------|------|---------|-------------|
556
+ | `ignoreOtherMentions` | `boolean` | `false` | If enabled, messages that @mention others but not the bot are silently dropped (not recorded, no AI trigger) |
557
+ | `toolPolicy` | `"full" \| "restricted" \| "none"` | `"restricted"` | Tool scope available to AI in this group. `full`=all tools; `restricted`=sensitive tools restricted (e.g., command execution, file ops); `none`=no tool calls allowed |
558
+ | `prompt` | `string` | built-in default | Group-specific system prompt, appended after global systemPrompt |
559
+ | `historyLimit` | `number` | `20` | Cached group history message count (0 disables) |
560
+ | `historyMode` | `"clear" \| "rolling"` | `"clear"` | `clear`: wipe history after each reply (legacy). `rolling`: bot outbounds are recorded too, and history is trimmed to after the bot's last message (AI sees what it last said) |
561
+ | `unmentionedInbound` | `"user_request" \| "room_event"` | `"user_request"` | `room_event`: read all messages like a group member; un-@'d traffic becomes passive room events (see [Room Events](#room-events-for-full-mode-groups-unmentionedinbound--opt-in); requires full-push-mode group) |
562
+ | `coalesce` | `object` | `{enabled: true}` | Group queueing config (see [Group Message Queueing](#group-message-queueing-configuration-coalesce--groupcoalesce)) |
563
+
564
+ **Full example with multiple groups:**
565
+
566
+ ```json
567
+ {
568
+ "channels": {
569
+ "qqbot": {
570
+ "defaultRequireMention": false,
571
+ "accounts": {
572
+ "default": {
573
+ "groups": {
574
+ "*": {
575
+ "requireMention": true,
576
+ "toolPolicy": "restricted",
577
+ "ignoreOtherMentions": true
578
+ },
579
+ "WORK_GROUP_OPENID": {
580
+ "requireMention": true,
581
+ "toolPolicy": "none",
582
+ "prompt": "You are a work assistant. Only answer work-related questions."
583
+ },
584
+ "FRIEND_GROUP_OPENID": {
585
+ "requireMention": false,
586
+ "toolPolicy": "full",
587
+ "prompt": "You are a friend in the group. Chat casually and naturally."
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ }
594
+ }
595
+ ```
596
+
597
+ #### Group Access Control (`groupPolicy`)
598
+
599
+ Control which groups are allowed via `groupPolicy`:
600
+
601
+ | Policy | Description |
602
+ |--------|-------------|
603
+ | `"open"` (default) | All groups are allowed |
604
+ | `"allowlist"` | Only groups in `groupAllowFrom` are allowed |
605
+ | `"disabled"` | Group chats are disabled entirely |
606
+
607
+ ```json
608
+ {
609
+ "channels": {
610
+ "qqbot": {
611
+ "groupPolicy": "allowlist",
612
+ "groupAllowFrom": ["ALLOWED_GROUP_OPENID_1", "ALLOWED_GROUP_OPENID_2"]
613
+ }
614
+ }
615
+ }
616
+ ```
617
+
618
+ > You can also use [**`/bot-group-always`**](#bot-group-always--group-response-mode-toggle) to toggle account-level defaults at runtime without restarting.
619
+
620
+ ---
621
+
622
+ ### Group vs C2C Differential Handling
623
+
624
+ The plugin implements different message handling strategies for group and private chats:
625
+
626
+ #### Group Chat (Framework Queue Strategy)
627
+
628
+ - **All messages are processed** — nothing is dropped
629
+ - **Immediate dispatch** — messages go straight to the framework; no plugin-side waiting room
630
+ - **Queueing by the framework** — while a turn is active, subsequent messages queue behind it (`collect` mode: merged into one batch after the active turn finishes; 500ms debounce absorbs bursts, queue cap with overflow summarizing)
631
+ - **Never interrupts** — an active turn always completes; new messages wait their turn
632
+ - **SessionKey format**: `qqbot:{accountId}:group:{groupId}:coalescing` (legacy naming, kept for session continuity)
633
+ - **Admission strategy**: `exclusive` (framework durable-ingress convention) with **no abort signal** — interruption is structurally impossible
634
+
635
+ **Example behavior**:
636
+
637
+ ```
638
+ User A: "Question 1" → Start processing
639
+ User B: "Question 2" → Queued in framework followup queue
640
+ User C: "Question 3" → Queued in framework followup queue
641
+
642
+ Question 1 completes → [Q2, Q3] drain as one merged batch → AI sees combined context, single reply
643
+ ```
644
+
645
+ #### C2C Private Chat (Exclusive Strategy)
646
+
647
+ - **User can interrupt** — sending a new message cancels the previous one
648
+ - **Last message wins** — only the most recent message is processed
649
+ - **SessionKey format**: `qqbot:{accountId}:{userId}`
650
+ - **Admission strategy**: `exclusive` with abort signal — new message cancels old
651
+
652
+ **Example behavior**:
653
+
654
+ ```
655
+ User A: "Question 1" → Start processing
656
+ User A: "Question 2" → Cancel Q1, start processing Q2
657
+ ```
658
+
659
+ #### Why This Design?
660
+
661
+ - **In groups**: All user messages should be preserved and addressed; a group is multi-user, so interruption would steal one member's answer from another
662
+ - **In C2C**: Users can change their mind mid-conversation
663
+ - **Aligns with user expectations** in different chat contexts
664
+
665
+ ---
666
+
667
+ ### Group Message Queueing Configuration (`coalesce` / `groupCoalesce`)
668
+
669
+ Control how group messages are queued and merged when they arrive in quick succession:
670
+
671
+ ```json
672
+ {
673
+ "channels": {
674
+ "qqbot": {
675
+ "groupCoalesce": {
676
+ "enabled": true
677
+ },
678
+ "accounts": {
679
+ "default": {
680
+ "groups": {
681
+ "GROUP_123": {
682
+ "coalesce": {
683
+ "enabled": false
684
+ }
685
+ }
686
+ }
687
+ }
688
+ }
689
+ }
690
+ }
691
+ }
692
+ ```
693
+
694
+ | Field | Type | Default | Description |
695
+ |-------|------|---------|-------------|
696
+ | `enabled` | `boolean` | `true` | `true` → framework `collect` mode (queue + merge batches); `false` → `followup` mode (queue without merging — preserves the "disable merging" intent while never interrupting active turns). Queueing/merging is fully delegated to the OpenClaw followup queue |
697
+
698
+ **Priority chain**: `groups.{groupId}.coalesce` > `groupCoalesce` (account-level) > defaults
699
+
700
+ **When enabled** (framework `collect`):
701
+
702
+ - Messages during an active turn queue up and drain as one merged batch
703
+ - 500ms debounce absorbs rapid-fire bursts even on an idle group
704
+ - Queue overflow is summarized (never hard-dropped like the old buffer-full behavior)
705
+ - The AI sees combined context with per-sender attribution, single reply per batch
706
+
707
+ ---
708
+
709
+ ### Room Events for Full-Mode Groups (`unmentionedInbound`) — opt-in
710
+
711
+ > Requires the group owner to have set the group's push mode to **full message reception** (receive all messages). In AT-mode groups this setting has no observable effect (un-@'d messages never arrive).
712
+
713
+ By default, un-@'d group messages only land in the history buffer. With `unmentionedInbound: "room_event"`, the bot **reads every message like a normal group member**:
714
+
715
+ ```json
716
+ {
717
+ "channels": {
718
+ "qqbot": {
719
+ "accounts": {
720
+ "default": {
721
+ "groups": {
722
+ "GROUP_OPENID": {
723
+ "unmentionedInbound": "room_event"
724
+ }
725
+ }
726
+ }
727
+ }
728
+ }
729
+ }
730
+ }
731
+ ```
732
+
733
+ Behavior once enabled:
734
+
735
+ - **@mention / name pattern / quote-of-bot** → normal turn with full reply rights (wake modes, see below)
736
+ - **Everything else** → passive room event: the AI reads it as context, its natural text output is **not delivered** (structural silence — the framework doesn't even inject the NO_REPLY instruction), and it can only speak by proactively calling the `message` tool
737
+ - Room events never steer or interrupt an active turn — they queue behind it
738
+ - Room-event speech goes out through the passive-first outbound path (see below)
739
+
740
+ > ⚠️ **Cost note**: each room event runs one inference pass ("reading" the message). In an active group this adds real token spend — enable per group deliberately.
741
+
742
+ #### Wake Modes (what counts as "being addressed")
743
+
744
+ | Wake mode | Mechanism | Config |
745
+ |-----------|-----------|--------|
746
+ | @mention | `GROUP_AT_MESSAGE_CREATE` event / `mentions[].is_you` / content markers | built-in |
747
+ | **Name patterns** | Content matches a configured pattern (e.g. group members call the bot "沈处") | `agents.list.<id>.groupChat.mentionPatterns: ["沈处"]` — note this lives in the `agents` section, not `channels.qqbot`; effective in full-mode groups only |
748
+ | **Quote of bot message** | User quotes/replies to a bot outbound (resolved via ref-index) | built-in (`isImplicitMention`) |
749
+
750
+ Name-pattern false positives ("people talking *about* the bot") are handled gracefully: the turn runs with normal reply rights, and the LLM can output `NO_REPLY` to stay silent (the framework injects this guidance automatically — no prompt changes needed).
751
+
752
+ ---
753
+
754
+ ### Passive-First Outbound (protects the 1000/day proactive budget)
755
+
756
+ QQ Bot proactive messages (sent without msg_id) have a daily budget (~1000/day). The plugin prefers passive replies (msg_id) everywhere:
757
+
758
+ - Framework-provided `replyToId` is used when available; otherwise the freshest cached msg_id is attached (msgid-cache TTL matches the platform's passive window: 5min group / 30min c2c)
759
+ - Attaching a msg_id is **quota-aware**: the passive quota (5 replies per msg_id per 5min in groups) is atomically checked and consumed before the send — when exhausted, the send gracefully degrades to proactive instead of failing with platform error 40034128
760
+ - Residual proactive sends are counted per account per day (`/bot-group-info` shows usage; a warning is logged at 80% of the budget)
761
+ - Quiet groups (no message within the passive window) can only be reached proactively — that's a platform constraint, not a bug
762
+
763
+ ---
764
+
765
+ ### Group Rate Limiting (`rateLimit`) — enabled by default
766
+
767
+ Three-tier sliding-window throttling with conservative defaults (normal usage never hits them):
768
+
769
+ | Tier | Default | Keyed by |
770
+ |------|---------|----------|
771
+ | `perSender` | 20 msgs / min | sender openid |
772
+ | `perGroup` | 60 msgs / min | group openid (c2c falls back to sender) |
773
+ | `global` | 300 msgs / min | all messages |
774
+
775
+ ```json
776
+ {
777
+ "channels": {
778
+ "qqbot": {
779
+ "rateLimit": {
780
+ "enabled": true,
781
+ "perSender": { "max": 20, "windowMs": 60000 },
782
+ "perGroup": { "max": 60, "windowMs": 60000 },
783
+ "global": { "max": 300, "windowMs": 60000 }
784
+ }
785
+ }
786
+ }
787
+ }
788
+ ```
789
+
790
+ Rate-limited messages are dropped with an INFO log (no auto-reply, to avoid burning quota). Keep this enabled for room-event groups.
791
+
792
+ ---
793
+
794
+ ### Middleware Execution Order
795
+
796
+ The plugin processes messages through a carefully ordered middleware chain:
797
+
798
+ 1. **Error Handler** — Catches exceptions at the outermost layer
799
+ 2. **Message Filter** — Bot echo + message deduplication
800
+ 3. **Inbound Guard** — Drops outbound echoes (c2c), duplicate pushes (30min window), contentless events
801
+ 4. **Policy Injector** — Injects `ctx.state.policy` with dynamic config
802
+ 5. **History Buffer** — Caches all group messages (including non-@; skipped for room-event groups)
803
+ 6. **Access Control** — Dynamic pairing/allowlist checks
804
+ 7. **Mention Gate** — Filters based on @mention rules (+ quote-of-bot implicit mention)
805
+ 8. **Content Sanitizer** — Strips @markers, parses face tags
806
+ 9. **Rate Limiter** — Three-layer throttling (enabled by default, see `rateLimit`)
807
+ 10. **Slash Commands** — Intercepts `/bot-*` commands
808
+ 11. **Secret Capture** (c2c only) — One-shot env-var secret input interception
809
+ 12. **Typing Indicator** (C2C only) — Shows "typing..." status
810
+ 13. **Quote Reference** — Parses quoted message context
811
+ 14. **Attachment Processor** — Downloads/converts media
812
+ 15. **Envelope Formatter** — Builds final message body
813
+
814
+ **Key points**:
815
+
816
+ - Inbound guard and history buffer run **before** mention gate → junk is dropped early, all real messages are cached
817
+ - Group messages dispatch immediately; the OpenClaw followup queue handles queueing/merging (`coalesce.enabled` selects `collect` vs `followup`)
818
+ - Typing indicator only runs for **C2C** messages
819
+
820
+ ---
821
+
822
+ #### STT (Speech-to-Text) — Transcribe Incoming Voice Messages
823
+
824
+ STT supports two-level configuration with priority fallback:
825
+
826
+ | Priority | Config Path | Scope |
827
+ |----------|------------|-------|
828
+ | 1 (highest) | `channels.qqbot.stt` | Plugin-specific |
829
+ | 2 (fallback) | `tools.media.audio.models[0]` | Framework-level |
830
+
831
+ ```json
832
+ {
833
+ "channels": {
834
+ "qqbot": {
835
+ "stt": {
836
+ "provider": "your-provider",
837
+ "model": "your-stt-model"
838
+ }
839
+ }
840
+ }
841
+ }
842
+ ```
843
+
844
+ - `provider` — references a key in `models.providers` to inherit `baseUrl` and `apiKey`
845
+ - Set `enabled: false` to disable
846
+ - When configured, incoming voice messages are automatically converted (SILK→WAV) and transcribed
847
+ - `asrFallback` — platform ASR (`asr_refer_text`) participation switch. Unless explicitly set to `true`, QQ's built-in platform transcript is **discarded in all cases**: not used as a fallback when your STT fails or returns empty, and not used as the sole source when STT is not configured at all (voice messages then render as `[Voice message - transcription unavailable]`; the audio URL is still referenced via the `- Voice:` line). The flag is read from `channels.qqbot.stt.asrFallback` regardless of whether STT credentials resolve — `stt: { "asrFallback": true }` alone restores the legacy platform-transcript behavior:
848
+
849
+ ```json
850
+ {
851
+ "channels": {
852
+ "qqbot": {
853
+ "stt": {
854
+ "provider": "your-provider",
855
+ "model": "your-stt-model",
856
+ "asrFallback": true
857
+ }
858
+ }
859
+ }
860
+ }
861
+ ```
862
+
863
+ #### TTS (Text-to-Speech) — Send Voice Messages
864
+
865
+ | Priority | Config Path | Scope |
866
+ |----------|------------|-------|
867
+ | 1 (highest) | `channels.qqbot.tts` | Plugin-specific |
868
+ | 2 (fallback) | `messages.tts` | Framework-level |
869
+
870
+ ```json
871
+ {
872
+ "channels": {
873
+ "qqbot": {
874
+ "tts": {
875
+ "provider": "your-provider",
876
+ "model": "your-tts-model",
877
+ "voice": "your-voice"
878
+ }
879
+ }
880
+ }
881
+ }
882
+ ```
883
+
884
+ - `provider` — references a key in `models.providers` to inherit `baseUrl` and `apiKey`
885
+ - `voice` — voice variant
886
+ - Set `enabled: false` to disable (default: `true`)
887
+ - When configured, AI can generate and send voice messages
888
+
889
+ #### Streaming Replies — C2C private chat only
890
+
891
+ The bot can stream its reply progressively (typewriter effect) via QQ's streaming API. Group chats do not support streaming (platform constraint). Disabled unless configured.
892
+
893
+ ```json
894
+ {
895
+ "channels": {
896
+ "qqbot": {
897
+ "streaming": {
898
+ "mode": "partial",
899
+ "sendMode": "stream"
900
+ }
901
+ }
902
+ }
903
+ }
904
+ ```
905
+
906
+ | Field | Default | Description |
907
+ |-------|---------|-------------|
908
+ | `mode` | *(unset = off)* | `"partial"` enables streaming reception; `"off"` disables |
909
+ | `sendMode` | `"stream"` | `"stream"` — QQ streaming printer (typewriter; the delivered prefix is immutable, tail rewrites are merged into appends). `"static"` — accumulate while the model generates, then send one complete message at the end (no typewriter) |
910
+
911
+ - Streaming replies (`session.update` frames) still consume the passive-reply quota of the triggering message
912
+ - On stream errors the controller falls back to a single static message automatically
913
+
914
+ #### Typing Indicator — C2C private chat only
915
+
916
+ After receiving a private message, the bot shows "typing…" and renews it periodically while the AI is processing.
917
+
918
+ ```json
919
+ {
920
+ "channels": {
921
+ "qqbot": {
922
+ "typing": {
923
+ "enabled": true,
924
+ "intervalMs": 20000
925
+ }
926
+ }
927
+ }
928
+ }
929
+ ```
930
+
931
+ - `enabled` — enable/disable the indicator (default: `true`)
932
+ - `intervalMs` — renewal interval in milliseconds (default: `20000`). The QQ client clears the indicator when the user leaves and re-enters the chat; only a fresh push re-shows it, hence the periodic renewal. Values below `20000` are clamped to `20000` (QPS constraint)
933
+ - **Quota note**: typing notifications share the passive-reply quota of the user message they reply to (QQ Open Platform allows ~5 passive replies per message). Once the passive quota is exhausted, typing — just like reply messages — automatically falls back to proactive sending (no msg_id); renewal is never interrupted
934
+ - **Intermediate-message refresh**: when the bot sends a message (e.g. chain-of-thought intermediate output), the QQ client terminates the indicator; if the framework task is still running, the plugin renews the indicator 5 seconds after the message (still guarded by the 20s QPS spacing). After the final reply completes the task, no further refresh is sent
935
+
936
+ ---
937
+
938
+ ## 📚 Documentation & Links
939
+
940
+ - [Command Reference](docs/commands.md) — OpenClaw CLI commands
941
+ - [Changelog](CHANGELOG.md) — release notes
942
+
943
+ ## 🤝 Contributors
944
+
945
+ This is a forked version. For contributors to the official version, see [tencent-connect/openclaw-qqbot](https://github.com/tencent-connect/openclaw-qqbot/graphs/contributors).
946
+
947
+ <a href="https://github.com/jerryliang122/qqbot-openclaw/graphs/contributors">
948
+ <img src="https://contrib.rocks/image?repo=jerryliang122/qqbot-openclaw" />
949
+ </a>
950
+
951
+ ## 💖 Acknowledgements
952
+
953
+ - Original project: [tencent-connect/openclaw-qqbot](https://github.com/tencent-connect/openclaw-qqbot)
954
+ - Special thanks to [@sliverp](https://github.com/sliverp) for outstanding contributions to the original project!
955
+ - Thanks to [Tencent Cloud Lighthouse](https://cloud.tencent.com/product/lighthouse) for the deep collaboration.
956
+
957
+ <a href="https://cloud.tencent.com/product/lighthouse">
958
+ <img alt="Tencent Cloud Lighthouse" src="./docs/images/lighthouse-head.png" height="500" style="max-width:80%; height:auto;"/>
959
+ </a>
960
+
961
+ ## ⭐ Star History
962
+
963
+ <div align="center">
964
+
965
+ [![Star History Chart](https://api.star-history.com/svg?repos=jerryliang122/qqbot-openclaw&type=date&legend=top-left)](https://www.star-history.com/#jerryliang122/qqbot-openclaw&type=date&legend=top-left)
966
+
967
+ </div>