@tencent-connect/openclaw-qqbot 1.6.6-alpha.3 → 1.6.6

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 (44) hide show
  1. package/README.md +19 -15
  2. package/README.zh.md +19 -15
  3. package/dist/src/api.d.ts +7 -2
  4. package/dist/src/api.js +13 -4
  5. package/dist/src/channel.d.ts +18 -0
  6. package/dist/src/channel.js +85 -2
  7. package/dist/src/config.d.ts +33 -2
  8. package/dist/src/config.js +124 -0
  9. package/dist/src/gateway.js +565 -23
  10. package/dist/src/group-history.d.ts +136 -0
  11. package/dist/src/group-history.js +226 -0
  12. package/dist/src/message-gating.d.ts +53 -0
  13. package/dist/src/message-gating.js +107 -0
  14. package/dist/src/message-queue.d.ts +36 -0
  15. package/dist/src/message-queue.js +164 -22
  16. package/dist/src/outbound.d.ts +4 -4
  17. package/dist/src/outbound.js +8 -8
  18. package/dist/src/ref-index-store.js +5 -28
  19. package/dist/src/slash-commands.d.ts +6 -0
  20. package/dist/src/slash-commands.js +2 -2
  21. package/dist/src/types.d.ts +88 -0
  22. package/dist/src/utils/audio-convert.d.ts +1 -1
  23. package/dist/src/utils/audio-convert.js +1 -1
  24. package/dist/src/utils/chunked-upload.d.ts +2 -2
  25. package/dist/src/utils/chunked-upload.js +5 -5
  26. package/dist/src/utils/text-parsing.js +7 -18
  27. package/package.json +2 -3
  28. package/scripts/upgrade-via-source.sh +62 -15
  29. package/src/api.ts +21 -4
  30. package/src/channel.ts +85 -2
  31. package/src/config.ts +169 -2
  32. package/src/gateway.ts +661 -28
  33. package/src/group-history.ts +328 -0
  34. package/src/message-gating.ts +190 -0
  35. package/src/message-queue.ts +201 -21
  36. package/src/openclaw-plugin-sdk.d.ts +65 -0
  37. package/src/outbound.ts +8 -8
  38. package/src/ref-index-store.ts +5 -27
  39. package/src/slash-commands.ts +2 -2
  40. package/src/types.ts +92 -0
  41. package/src/utils/audio-convert.ts +1 -1
  42. package/src/utils/chunked-upload.ts +5 -5
  43. package/src/utils/text-parsing.ts +7 -14
  44. package/scripts/prebuild-stub.cjs +0 -172
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  **Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
12
12
 
13
- ### 🚀 Current Version: `v1.6.5`
13
+ ### 🚀 Current Version: `v1.6.6`
14
14
 
15
15
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
16
16
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
@@ -25,7 +25,7 @@
25
25
 
26
26
  Scan to join the QQ group chat
27
27
 
28
- <img width="400" alt="QQ QR Code" src="./docs/images/developer_group.png" />
28
+ <img width="400" alt="QQ QR Code" src="./docs/images/developer-group.png" />
29
29
 
30
30
 
31
31
  </div>
@@ -61,7 +61,7 @@ QQ quote events carry index keys (e.g. `REFIDX_xxx`) instead of full original me
61
61
  - Store path: `~/.openclaw/qqbot/data/ref-index.jsonl` (survives gateway restart).
62
62
  - Quote body may include text + media summary (image/voice/video/file).
63
63
 
64
- <img width="360" src="docs/images/ref_msg.png" alt="Quoted Message Context Demo" />
64
+ <img width="360" src="docs/images/ref-msg.png" alt="Quoted Message Context Demo" />
65
65
 
66
66
  ### 🎙️ Voice Messages (STT)
67
67
 
@@ -71,7 +71,7 @@ With STT configured, the plugin automatically transcribes voice messages to text
71
71
  >
72
72
  > **QQBot**: Tomorrow (March 7, Saturday) Shenzhen weather forecast 🌤️ ...
73
73
 
74
- <img width="360" src="docs/images/fc7b2236896cfba3a37c94be5d59ce3e_720.jpg" alt="Voice STT Demo" />
74
+ <img width="360" src="docs/images/voice-stt.jpg" alt="Voice STT Demo" />
75
75
 
76
76
  ### 📄 File Understanding
77
77
 
@@ -81,7 +81,7 @@ Send any file to the bot — novels, reports, spreadsheets — AI automatically
81
81
  >
82
82
  > **QQBot**: Got it! You uploaded the Chinese version of "War and Peace" by Leo Tolstoy. This appears to be the opening of Chapter 1...
83
83
 
84
- <img width="360" src="docs/images/07bff56ab68e03173d2af586eeb3bcee_720.jpg" alt="File Understanding Demo" />
84
+ <img width="360" src="docs/images/file-understand.jpg" alt="File Understanding Demo" />
85
85
 
86
86
  ### 🖼️ Image Understanding
87
87
 
@@ -91,7 +91,7 @@ If your main model supports vision (e.g. Tencent Hunyuan `hunyuan-vision`), AI c
91
91
  >
92
92
  > **QQBot**: Haha, so cute! Is that a QQ penguin in a lobster costume? 🦞🐧 ...
93
93
 
94
- <img width="360" src="docs/images/59d421891f813b0d3c0cbe12574b6a72_720.jpg" alt="Image Understanding Demo" />
94
+ <img width="360" src="docs/images/image-understand.jpg" alt="Image Understanding Demo" />
95
95
 
96
96
  ### 🎨 Image Sending
97
97
 
@@ -101,7 +101,7 @@ If your main model supports vision (e.g. Tencent Hunyuan `hunyuan-vision`), AI c
101
101
 
102
102
  AI can send images directly. Supports local paths and URLs. Formats: jpg/png/gif/webp/bmp.
103
103
 
104
- <img width="360" src="docs/images/4645f2b3a20822b7f8d6664a708529eb_720.jpg" alt="Image Generation Demo" />
104
+ <img width="360" src="docs/images/image-send.jpg" alt="Image Generation Demo" />
105
105
 
106
106
  ### 🔊 Voice Sending
107
107
 
@@ -111,7 +111,7 @@ AI can send images directly. Supports local paths and URLs. Formats: jpg/png/gif
111
111
 
112
112
  AI can send voice messages directly. Formats: mp3/wav/silk/ogg. No ffmpeg required.
113
113
 
114
- <img width="360" src="docs/images/21dce8bfc553ce23d1bd1b270e9c516c.jpg" alt="TTS Voice Demo" />
114
+ <img width="360" src="docs/images/voice-send.jpg" alt="TTS Voice Demo" />
115
115
 
116
116
  ### ⏰ Scheduled Reminder (Proactive Message)
117
117
 
@@ -129,9 +129,13 @@ This capability depends on OpenClaw cron scheduling and proactive messaging. If
129
129
  >
130
130
  > **QQBot**: *(sends a .txt file)*
131
131
 
132
- AI can send files directly. Any format, up to 20MB.
132
+ AI can send files directly, in any format.
133
133
 
134
- <img width="360" src="docs/images/17cada70df90185d45a2d6dd36e92f2f_720.jpg" alt="File Sending Demo" />
134
+ <img width="360" src="docs/images/file-send.jpg" alt="File Sending Demo" />
135
+
136
+ Since v1.6.6, large file transfer is supported: images up to 20MB, videos up to 30MB, attachments up to 100MB, with a daily transfer limit of 2GB.
137
+
138
+ <img width="360" src="docs/images/large-file-transfer.jpg" alt="Large File Transfer Demo" />
135
139
 
136
140
  ### 🎬 Video Sending
137
141
 
@@ -141,7 +145,7 @@ AI can send files directly. Any format, up to 20MB.
141
145
 
142
146
  AI can send videos directly. Supports local files and URLs.
143
147
 
144
- <img width="360" src="docs/images/85d03b8a216f267ab7b2aee248a18a41_720.jpg" alt="Video Sending Demo" />
148
+ <img width="360" src="docs/images/video-send.jpg" alt="Video Sending Demo" />
145
149
 
146
150
  > **Under the hood:** Upload dedup caching, ordered queue delivery, and multi-layer audio format fallback.
147
151
 
@@ -220,15 +224,15 @@ All commands support a `?` suffix to show usage:
220
224
  2. After scanning, tap **Agree** on your phone — you'll land on the bot configuration page.
221
225
  3. Click **Create Bot** to create a new QQ bot.
222
226
 
223
- <img width="720" alt="Create Bot" src="docs/images/create_robot.png" />
227
+ <img width="720" alt="Create Bot" src="docs/images/create-robot.png" />
224
228
 
225
229
  > ⚠️ 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.
226
230
 
227
- <img width="400" alt="Bot Say Hello" src="docs/images/bot_say_hello.jpg" />
231
+ <img width="400" alt="Bot Say Hello" src="docs/images/bot-say-hello.jpg" />
228
232
 
229
233
  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.**
230
234
 
231
- <img width="720" alt="Find AppID and AppSecret" src="docs/images/find_appid_secret.png" />
235
+ <img width="720" alt="Find AppID and AppSecret" src="docs/images/find-appid-secret.png" />
232
236
 
233
237
  > For a step-by-step walkthrough with screenshots, see the [official guide](https://cloud.tencent.com/developer/article/2626045).
234
238
 
@@ -458,7 +462,7 @@ Special thanks to [@sliverp](https://github.com/sliverp) for outstanding contrib
458
462
  Thanks to [Tencent Cloud Lighthouse](https://cloud.tencent.com/product/lighthouse) for the deep collaboration. For raising crawfish, choose Tencent Cloud Lighthouse!
459
463
 
460
464
  <a href="https://cloud.tencent.com/product/lighthouse">
461
- <img alt="Tencent Cloud Lighthouse" src="./docs/images/lighthouse_head.png" height="500" style="max-width:80%; height:auto;"/>
465
+ <img alt="Tencent Cloud Lighthouse" src="./docs/images/lighthouse-head.png" height="500" style="max-width:80%; height:auto;"/>
462
466
  </a>
463
467
 
464
468
  ## ⭐ Star History
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  **让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
11
11
 
12
- ### 🚀 当前版本: `v1.6.5`
12
+ ### 🚀 当前版本: `v1.6.6`
13
13
 
14
14
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
15
15
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
@@ -21,7 +21,7 @@
21
21
 
22
22
  扫描二维码加入群聊,一起交流
23
23
 
24
- <img width="400" alt="QQ 群二维码" src="./docs/images/developer_group.png" />
24
+ <img width="400" alt="QQ 群二维码" src="./docs/images/developer-group.png" />
25
25
 
26
26
  </div>
27
27
 
@@ -56,7 +56,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
56
56
  - 存储位置:`~/.openclaw/qqbot/data/ref-index.jsonl`(网关重启后仍可恢复)。
57
57
  - 引用内容支持文本 + 媒体摘要(图片/语音/视频/文件)。
58
58
 
59
- <img width="360" src="docs/images/ref_msg.png" alt="引用消息上下文演示" />
59
+ <img width="360" src="docs/images/ref-msg.png" alt="引用消息上下文演示" />
60
60
 
61
61
  ### 🎙️ 语音消息(STT)
62
62
 
@@ -66,7 +66,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
66
66
  >
67
67
  > **QQBot**:明天(3月7日 周六)深圳的天气预报 🌤️ ...
68
68
 
69
- <img width="360" src="docs/images/fc7b2236896cfba3a37c94be5d59ce3e_720.jpg" alt="听语音演示" />
69
+ <img width="360" src="docs/images/voice-stt.jpg" alt="听语音演示" />
70
70
 
71
71
  ### 📄 文件理解
72
72
 
@@ -76,7 +76,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
76
76
  >
77
77
  > **QQBot**:收到!你上传了列夫·托尔斯泰的《战争与和平》中文版文本。从内容来看,这是第一章的开头……你想让我做什么?
78
78
 
79
- <img width="360" src="docs/images/07bff56ab68e03173d2af586eeb3bcee_720.jpg" alt="AI理解用户发送的文件" />
79
+ <img width="360" src="docs/images/file-understand.jpg" alt="AI理解用户发送的文件" />
80
80
 
81
81
  ### 🖼️ 图片理解
82
82
 
@@ -86,7 +86,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
86
86
  >
87
87
  > **QQBot**:哈哈,好可爱!这是QQ企鹅穿上小龙虾套装吗?🦞🐧 ...
88
88
 
89
- <img width="360" src="docs/images/59d421891f813b0d3c0cbe12574b6a72_720.jpg" alt="图片理解演示" />
89
+ <img width="360" src="docs/images/image-understand.jpg" alt="图片理解演示" />
90
90
 
91
91
  ### 🎨 图片发送
92
92
 
@@ -96,7 +96,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
96
96
 
97
97
  AI 可直接发送图片,支持本地文件路径和网络 URL。格式:jpg/png/gif/webp/bmp。
98
98
 
99
- <img width="360" src="docs/images/4645f2b3a20822b7f8d6664a708529eb_720.jpg" alt="发图片演示" />
99
+ <img width="360" src="docs/images/image-send.jpg" alt="发图片演示" />
100
100
 
101
101
  ### 🔊 语音发送
102
102
 
@@ -106,7 +106,7 @@ AI 可直接发送图片,支持本地文件路径和网络 URL。格式:jpg/
106
106
 
107
107
  AI 可直接发送语音消息。格式:mp3/wav/silk/ogg,无需安装 ffmpeg。
108
108
 
109
- <img width="360" src="docs/images/21dce8bfc553ce23d1bd1b270e9c516c.jpg" alt="发语音演示" />
109
+ <img width="360" src="docs/images/voice-send.jpg" alt="发语音演示" />
110
110
 
111
111
  ### ⏰ 定时提醒(主动消息)
112
112
 
@@ -124,9 +124,13 @@ AI 可直接发送语音消息。格式:mp3/wav/silk/ogg,无需安装 ffmpeg
124
124
  >
125
125
  > **QQBot**:*(发送 .txt 文件)*
126
126
 
127
- AI 可直接发送文件。任意格式,最大 20MB。
127
+ AI 可直接发送文件,任意格式均可。
128
128
 
129
- <img width="360" src="docs/images/17cada70df90185d45a2d6dd36e92f2f_720.jpg" alt="发文件演示" />
129
+ <img width="360" src="docs/images/file-send.jpg" alt="发文件演示" />
130
+
131
+ v1.6.6 起支持大文件传输:图片最大 20MB,视频最大 30MB,附件最大 100MB,每日累计传输上限 2GB。
132
+
133
+ <img width="360" src="docs/images/large-file-transfer.jpg" alt="大文件传输演示" />
130
134
 
131
135
  ### 🎬 视频发送
132
136
 
@@ -136,7 +140,7 @@ AI 可直接发送文件。任意格式,最大 20MB。
136
140
 
137
141
  AI 可直接发送视频,支持本地文件和公网 URL。
138
142
 
139
- <img width="360" src="docs/images/85d03b8a216f267ab7b2aee248a18a41_720.jpg" alt="发视频演示" />
143
+ <img width="360" src="docs/images/video-send.jpg" alt="发视频演示" />
140
144
 
141
145
  > **底层细节:** 上传去重缓存、有序队列发送、音频格式多层降级。
142
146
 
@@ -215,15 +219,15 @@ AI 可直接发送视频,支持本地文件和公网 URL。
215
219
  2. 手机 QQ 扫码后选择**同意**,即完成注册,进入 QQ 机器人配置页。
216
220
  3. 点击**创建机器人**,即可直接新建一个 QQ 机器人。
217
221
 
218
- <img width="720" alt="创建机器人" src="docs/images/create_robot.png" />
222
+ <img width="720" alt="创建机器人" src="docs/images/create-robot.png" />
219
223
 
220
224
  > ⚠️ 机器人创建后会自动出现在你的 QQ 消息列表中,并发送第一条消息。但在完成下面的配置之前,发消息会提示"该机器人去火星了",属于正常现象。
221
225
 
222
- <img width="400" alt="机器人打招呼" src="docs/images/bot_say_hello.jpg" />
226
+ <img width="400" alt="机器人打招呼" src="docs/images/bot-say-hello.jpg" />
223
227
 
224
228
  4. 在机器人页面中找到 **AppID** 和 **AppSecret**,分别点击右侧**复制**按钮,保存到记事本或备忘录中。**AppSecret 不支持明文保存,离开页面后再查看会强制重置,请务必妥善保存。**
225
229
 
226
- <img width="720" alt="找到 AppID 和 AppSecret" src="docs/images/find_appid_secret.png" />
230
+ <img width="720" alt="找到 AppID 和 AppSecret" src="docs/images/find-appid-secret.png" />
227
231
 
228
232
  > 详细图文教程请参阅 [官方指南](https://cloud.tencent.com/developer/article/2626045)。
229
233
 
@@ -453,7 +457,7 @@ STT 支持两级配置,按优先级查找:
453
457
  感谢[腾讯云Lighthouse](https://cloud.tencent.com/product/lighthouse)的深度合作,养小龙虾,首选腾讯云Lighthouse!
454
458
 
455
459
  <a href="https://cloud.tencent.com/product/lighthouse">
456
- <img alt="腾讯云 Lighthouse" src="./docs/images/lighthouse_head.png" height="500" style="max-width:80%; height:auto;"/>
460
+ <img alt="腾讯云 Lighthouse" src="./docs/images/lighthouse-head.png" height="500" style="max-width:80%; height:auto;"/>
457
461
  </a>
458
462
 
459
463
  ## ⭐ Star History
package/dist/src/api.d.ts CHANGED
@@ -83,11 +83,16 @@ export declare function apiRequest<T = unknown>(accessToken: string, method: str
83
83
  */
84
84
  export declare const PART_FINISH_RETRYABLE_CODES: Set<number>;
85
85
  /**
86
- * upload_prepare 接口命中此错误码时,使用回包中的 message 字段作为兜底文案发送给用户
86
+ * upload_prepare 接口命中此错误码时,携带文件信息抛出 UploadDailyLimitExceededError,
87
+ * 由上层(outbound.ts)构造包含文件路径和大小的兜底文案发送给用户,
87
88
  * 而非走通用的"文件发送失败,请稍后重试"
88
89
  */
89
90
  export declare const UPLOAD_PREPARE_FALLBACK_CODE = 40093002;
90
91
  export declare function getGatewayUrl(accessToken: string): Promise<string>;
92
+ /** 回应按钮交互(INTERACTION_CREATE),避免客户端按钮持续 loading */
93
+ export declare function acknowledgeInteraction(accessToken: string, interactionId: string, code?: 0 | 1 | 2 | 3 | 4 | 5, data?: Record<string, unknown>): Promise<void>;
94
+ /** 获取插件版本号(从 package.json 读取,和 PLUGIN_USER_AGENT 同源) */
95
+ export declare function getApiPluginVersion(): string;
91
96
  export interface MessageResponse {
92
97
  id: string;
93
98
  timestamp: number | string;
@@ -113,7 +118,7 @@ export declare function sendDmMessage(accessToken: string, guildId: string, cont
113
118
  id: string;
114
119
  timestamp: string;
115
120
  }>;
116
- export declare function sendGroupMessage(accessToken: string, groupOpenid: string, content: string, msgId?: string): Promise<MessageResponse>;
121
+ export declare function sendGroupMessage(accessToken: string, groupOpenid: string, content: string, msgId?: string, messageReference?: string): Promise<MessageResponse>;
117
122
  export declare function sendProactiveC2CMessage(accessToken: string, openid: string, content: string): Promise<MessageResponse>;
118
123
  export declare function sendProactiveGroupMessage(accessToken: string, groupOpenid: string, content: string): Promise<{
119
124
  id: string;
package/dist/src/api.js CHANGED
@@ -353,7 +353,8 @@ export const PART_FINISH_RETRYABLE_CODES = new Set([
353
353
  40093001,
354
354
  ]);
355
355
  /**
356
- * upload_prepare 接口命中此错误码时,使用回包中的 message 字段作为兜底文案发送给用户
356
+ * upload_prepare 接口命中此错误码时,携带文件信息抛出 UploadDailyLimitExceededError,
357
+ * 由上层(outbound.ts)构造包含文件路径和大小的兜底文案发送给用户,
357
358
  * 而非走通用的"文件发送失败,请稍后重试"
358
359
  */
359
360
  export const UPLOAD_PREPARE_FALLBACK_CODE = 40093002;
@@ -447,6 +448,14 @@ export async function getGatewayUrl(accessToken) {
447
448
  const data = await apiRequest(accessToken, "GET", "/gateway");
448
449
  return data.url;
449
450
  }
451
+ /** 回应按钮交互(INTERACTION_CREATE),避免客户端按钮持续 loading */
452
+ export async function acknowledgeInteraction(accessToken, interactionId, code = 0, data) {
453
+ await apiRequest(accessToken, "PUT", `/interactions/${interactionId}`, { code, ...(data ? { data } : {}) });
454
+ }
455
+ /** 获取插件版本号(从 package.json 读取,和 PLUGIN_USER_AGENT 同源) */
456
+ export function getApiPluginVersion() {
457
+ return _pluginVersion;
458
+ }
450
459
  /**
451
460
  * 发送消息并自动触发 refIdx 回调
452
461
  * 所有消息发送函数统一经过此处,确保每条出站消息的 refIdx 都被捕获
@@ -519,10 +528,10 @@ export async function sendDmMessage(accessToken, guildId, content, msgId) {
519
528
  ...(msgId ? { msg_id: msgId } : {}),
520
529
  });
521
530
  }
522
- export async function sendGroupMessage(accessToken, groupOpenid, content, msgId) {
531
+ export async function sendGroupMessage(accessToken, groupOpenid, content, msgId, messageReference) {
523
532
  const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
524
- const body = buildMessageBody(content, msgId, msgSeq);
525
- return apiRequest(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body);
533
+ const body = buildMessageBody(content, msgId, msgSeq, messageReference);
534
+ return sendAndNotify(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body, { text: content });
526
535
  }
527
536
  function buildProactiveMessageBody(content) {
528
537
  if (!content || content.trim().length === 0) {
@@ -9,3 +9,21 @@ export declare const TEXT_CHUNK_LIMIT = 5000;
9
9
  */
10
10
  export declare function chunkText(text: string, limit: number): string[];
11
11
  export declare const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount>;
12
+ /** 清理 @mention:替换 <@openid> 为 @用户名,去除 @机器人自身 */
13
+ export declare function stripMentionText(text: string, mentions?: Array<{
14
+ member_openid?: string;
15
+ id?: string;
16
+ user_openid?: string;
17
+ is_you?: boolean;
18
+ nickname?: string;
19
+ username?: string;
20
+ }>): string;
21
+ /** 检测消息是否 @了机器人(mentions > eventType > mentionPatterns) */
22
+ export declare function detectWasMentioned({ eventType, mentions, content, mentionPatterns }: {
23
+ eventType?: string;
24
+ mentions?: Array<{
25
+ is_you?: boolean;
26
+ }>;
27
+ content?: string;
28
+ mentionPatterns?: string[];
29
+ }): boolean;
@@ -1,5 +1,5 @@
1
1
  import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/core";
2
- import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId } from "./config.js";
2
+ import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId, resolveRequireMention, resolveToolPolicy, resolveGroupConfig } from "./config.js";
3
3
  import { sendText, sendMedia } from "./outbound.js";
4
4
  import { startGateway } from "./gateway.js";
5
5
  import { qqbotOnboardingAdapter } from "./onboarding.js";
@@ -39,8 +39,50 @@ export const qqbotPlugin = {
39
39
  blockStreaming: true,
40
40
  },
41
41
  reload: { configPrefixes: ["channels.qqbot"] },
42
+ // ============ 群消息策略适配器 ============
43
+ groups: {
44
+ /** 是否需要 @机器人才响应 */
45
+ resolveRequireMention: ({ cfg, accountId, groupId }) => {
46
+ if (!groupId)
47
+ return undefined;
48
+ return resolveRequireMention(cfg, groupId, accountId ?? undefined);
49
+ },
50
+ /** 群聊工具范围 */
51
+ resolveToolPolicy: ({ cfg, accountId, groupId }) => {
52
+ if (!groupId)
53
+ return undefined;
54
+ const policy = resolveToolPolicy(cfg, groupId, accountId ?? undefined);
55
+ // 将简单字符串策略映射为 GroupToolPolicyConfig 对象
56
+ if (policy === "full")
57
+ return undefined; // full = 默认不限制
58
+ if (policy === "none")
59
+ return { allow: [], deny: ["*"] };
60
+ // restricted: 默认空 allow(框架会使用内置 restricted 列表)
61
+ return { allow: [] };
62
+ },
63
+ /** QQ Bot 平台特有的群聊行为提示 */
64
+ resolveGroupIntroHint: ({ cfg, accountId, groupId }) => {
65
+ if (!groupId)
66
+ return undefined;
67
+ const groupCfg = resolveGroupConfig(cfg, groupId, accountId ?? undefined);
68
+ const hints = [];
69
+ if (groupCfg.name) {
70
+ hints.push(`当前群: ${groupCfg.name}`);
71
+ }
72
+ // bot 互聊防护、@状态行为指引在 gateway.ts 动态注入
73
+ return hints.join(" ") || undefined;
74
+ },
75
+ },
76
+ // ============ @mention 检测与清理 ============
77
+ mentions: {
78
+ /** 清理 @mention 文本(SDK ChannelMentionAdapter 接口) */
79
+ stripMentions: ({ text, ctx }) => {
80
+ const mentions = ctx?.mentions;
81
+ return stripMentionText(text, mentions);
82
+ },
83
+ },
42
84
  // CLI onboarding wizard
43
- // @ts-expect-error onboarding removed from ChannelPlugin type in 2026.3.23 but still supported at runtime
85
+ // @ts-ignore onboarding removed from ChannelPlugin type in 2026.3.23 but still supported at runtime
44
86
  onboarding: qqbotOnboardingAdapter,
45
87
  config: {
46
88
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
@@ -367,3 +409,44 @@ export const qqbotPlugin = {
367
409
  }),
368
410
  },
369
411
  };
412
+ // ============ 独立的 mention 工具函数(供 gateway.ts 等直接调用) ============
413
+ /** 清理 @mention:替换 <@openid> 为 @用户名,去除 @机器人自身 */
414
+ export function stripMentionText(text, mentions) {
415
+ if (!text || !mentions?.length)
416
+ return text;
417
+ let cleaned = text;
418
+ for (const m of mentions) {
419
+ const openid = m.member_openid ?? m.id ?? m.user_openid;
420
+ if (!openid)
421
+ continue;
422
+ if (m.is_you) {
423
+ cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, "g"), "").trim();
424
+ }
425
+ else {
426
+ const displayName = m.nickname ?? m.username;
427
+ if (displayName) {
428
+ cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, "g"), `@${displayName}`);
429
+ }
430
+ }
431
+ }
432
+ return cleaned;
433
+ }
434
+ /** 检测消息是否 @了机器人(mentions > eventType > mentionPatterns) */
435
+ export function detectWasMentioned({ eventType, mentions, content, mentionPatterns }) {
436
+ if (mentions?.some((m) => m.is_you))
437
+ return true;
438
+ if (eventType === "GROUP_AT_MESSAGE_CREATE")
439
+ return true;
440
+ if (mentionPatterns?.length && content) {
441
+ for (const pattern of mentionPatterns) {
442
+ try {
443
+ if (new RegExp(pattern, "i").test(content))
444
+ return true;
445
+ }
446
+ catch {
447
+ // 无效正则,跳过
448
+ }
449
+ }
450
+ }
451
+ return false;
452
+ }
@@ -1,6 +1,36 @@
1
- import type { ResolvedQQBotAccount } from "./types.js";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { ResolvedQQBotAccount, ToolPolicy, GroupConfig } from "./types.js";
2
+ import type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk";
3
+ /**
4
+ * 解析 mentionPatterns(agent → global → 空数组)
5
+ *
6
+ * 优先级:
7
+ * 1. agents.list[agentId].groupChat.mentionPatterns
8
+ * 2. messages.groupChat.mentionPatterns
9
+ * 3. []
10
+ */
11
+ export declare function resolveMentionPatterns(cfg: OpenClawConfig, agentId?: string): string[];
3
12
  export declare const DEFAULT_ACCOUNT_ID = "default";
13
+ /** 解析群消息策略 */
14
+ export declare function resolveGroupPolicy(cfg: OpenClawConfig, accountId?: string): GroupPolicy;
15
+ /** 解析群白名单(统一转大写) */
16
+ export declare function resolveGroupAllowFrom(cfg: OpenClawConfig, accountId?: string): string[];
17
+ /** 检查指定群是否被允许(使用标准策略引擎) */
18
+ export declare function isGroupAllowed(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
19
+ type ResolvedGroupConfig = Omit<Required<GroupConfig>, "prompt"> & Pick<GroupConfig, "prompt">;
20
+ /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
21
+ export declare function resolveGroupConfig(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ResolvedGroupConfig;
22
+ /** 解析群历史消息缓存条数 */
23
+ export declare function resolveHistoryLimit(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): number;
24
+ /** 解析群行为 PE(具体群 > "*" > 默认值) */
25
+ export declare function resolveGroupPrompt(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
26
+ /** 解析群是否需要 @机器人才响应 */
27
+ export declare function resolveRequireMention(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
28
+ /** 解析群是否忽略 @了其他人(非 bot)的消息 */
29
+ export declare function resolveIgnoreOtherMentions(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
30
+ /** 解析群工具策略 */
31
+ export declare function resolveToolPolicy(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ToolPolicy;
32
+ /** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
33
+ export declare function resolveGroupName(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
4
34
  /**
5
35
  * 列出所有 QQBot 账户 ID
6
36
  */
@@ -23,3 +53,4 @@ export declare function applyQQBotAccountConfig(cfg: OpenClawConfig, accountId:
23
53
  name?: string;
24
54
  imageServerBaseUrl?: string;
25
55
  }): OpenClawConfig;
56
+ export {};
@@ -1,4 +1,128 @@
1
+ /**
2
+ * 解析 mentionPatterns(agent → global → 空数组)
3
+ *
4
+ * 优先级:
5
+ * 1. agents.list[agentId].groupChat.mentionPatterns
6
+ * 2. messages.groupChat.mentionPatterns
7
+ * 3. []
8
+ */
9
+ export function resolveMentionPatterns(cfg, agentId) {
10
+ // 1. agent 级别
11
+ if (agentId) {
12
+ const agents = cfg.agents;
13
+ const entry = agents?.list?.find((a) => a.id?.trim().toLowerCase() === agentId.trim().toLowerCase());
14
+ const agentGroupChat = entry?.groupChat;
15
+ if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
16
+ return agentGroupChat.mentionPatterns ?? [];
17
+ }
18
+ }
19
+ // 2. 全局级别
20
+ const globalGroupChat = cfg?.messages?.groupChat;
21
+ if (globalGroupChat && typeof globalGroupChat === "object" && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
22
+ return globalGroupChat.mentionPatterns ?? [];
23
+ }
24
+ // 3. 空数组
25
+ return [];
26
+ }
1
27
  export const DEFAULT_ACCOUNT_ID = "default";
28
+ function evaluateMatchedGroupAccessForPolicy(params) {
29
+ if (params.groupPolicy === "disabled") {
30
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
31
+ }
32
+ if (params.groupPolicy === "allowlist") {
33
+ if (params.requireMatchInput && !params.hasMatchInput) {
34
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "missing_match_input" };
35
+ }
36
+ if (!params.allowlistConfigured) {
37
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
38
+ }
39
+ if (!params.allowlistMatched) {
40
+ return { allowed: false, groupPolicy: params.groupPolicy, reason: "not_allowlisted" };
41
+ }
42
+ }
43
+ return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
44
+ }
45
+ // ============ 群消息策略 ============
46
+ const DEFAULT_GROUP_POLICY = "open";
47
+ /** 群历史缓存条数默认值 */
48
+ const DEFAULT_GROUP_HISTORY_LIMIT = 50;
49
+ const DEFAULT_GROUP_CONFIG = {
50
+ requireMention: true,
51
+ ignoreOtherMentions: false,
52
+ toolPolicy: "restricted",
53
+ name: "",
54
+ historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
55
+ };
56
+ /** 默认群消息行为 PE(可通过配置覆盖) */
57
+ const DEFAULT_GROUP_PROMPT = [
58
+ "若发送者为机器人,仅在对方明确@你提问或请求协助具体任务时,以简洁明了的内容回复,",
59
+ "避免与其他机器人产生抢答或多轮无意义对话。",
60
+ "在群聊中优先让人类用户的消息得到响应,机器人之间保持协作而非竞争,确保对话有序不刷屏。",
61
+ ].join("");
62
+ /** 解析群消息策略 */
63
+ export function resolveGroupPolicy(cfg, accountId) {
64
+ const account = resolveQQBotAccount(cfg, accountId);
65
+ return account.config?.groupPolicy ?? DEFAULT_GROUP_POLICY;
66
+ }
67
+ /** 解析群白名单(统一转大写) */
68
+ export function resolveGroupAllowFrom(cfg, accountId) {
69
+ const account = resolveQQBotAccount(cfg, accountId);
70
+ return (account.config?.groupAllowFrom ?? []).map((id) => String(id).trim().toUpperCase());
71
+ }
72
+ /** 检查指定群是否被允许(使用标准策略引擎) */
73
+ export function isGroupAllowed(cfg, groupOpenid, accountId) {
74
+ const policy = resolveGroupPolicy(cfg, accountId);
75
+ const allowList = resolveGroupAllowFrom(cfg, accountId);
76
+ const allowlistConfigured = allowList.length > 0;
77
+ const allowlistMatched = allowList.some((id) => id === "*" || id === groupOpenid.toUpperCase());
78
+ return evaluateMatchedGroupAccessForPolicy({
79
+ groupPolicy: policy,
80
+ allowlistConfigured,
81
+ allowlistMatched,
82
+ }).allowed;
83
+ }
84
+ /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
85
+ export function resolveGroupConfig(cfg, groupOpenid, accountId) {
86
+ const account = resolveQQBotAccount(cfg, accountId);
87
+ const groups = account.config?.groups ?? {};
88
+ const wildcardCfg = groups["*"] ?? {};
89
+ const specificCfg = groups[groupOpenid] ?? {};
90
+ return {
91
+ requireMention: specificCfg.requireMention ?? wildcardCfg.requireMention ?? DEFAULT_GROUP_CONFIG.requireMention,
92
+ ignoreOtherMentions: specificCfg.ignoreOtherMentions ?? wildcardCfg.ignoreOtherMentions ?? DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
93
+ toolPolicy: specificCfg.toolPolicy ?? wildcardCfg.toolPolicy ?? DEFAULT_GROUP_CONFIG.toolPolicy,
94
+ name: specificCfg.name ?? wildcardCfg.name ?? DEFAULT_GROUP_CONFIG.name,
95
+ prompt: specificCfg.prompt ?? wildcardCfg.prompt,
96
+ historyLimit: specificCfg.historyLimit ?? wildcardCfg.historyLimit ?? DEFAULT_GROUP_CONFIG.historyLimit,
97
+ };
98
+ }
99
+ /** 解析群历史消息缓存条数 */
100
+ export function resolveHistoryLimit(cfg, groupOpenid, accountId) {
101
+ return Math.max(0, resolveGroupConfig(cfg, groupOpenid, accountId).historyLimit);
102
+ }
103
+ /** 解析群行为 PE(具体群 > "*" > 默认值) */
104
+ export function resolveGroupPrompt(cfg, groupOpenid, accountId) {
105
+ const account = resolveQQBotAccount(cfg, accountId);
106
+ const groups = account.config?.groups ?? {};
107
+ return groups[groupOpenid]?.prompt ?? groups["*"]?.prompt ?? DEFAULT_GROUP_PROMPT;
108
+ }
109
+ /** 解析群是否需要 @机器人才响应 */
110
+ export function resolveRequireMention(cfg, groupOpenid, accountId) {
111
+ return resolveGroupConfig(cfg, groupOpenid, accountId).requireMention;
112
+ }
113
+ /** 解析群是否忽略 @了其他人(非 bot)的消息 */
114
+ export function resolveIgnoreOtherMentions(cfg, groupOpenid, accountId) {
115
+ return resolveGroupConfig(cfg, groupOpenid, accountId).ignoreOtherMentions;
116
+ }
117
+ /** 解析群工具策略 */
118
+ export function resolveToolPolicy(cfg, groupOpenid, accountId) {
119
+ return resolveGroupConfig(cfg, groupOpenid, accountId).toolPolicy;
120
+ }
121
+ /** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
122
+ export function resolveGroupName(cfg, groupOpenid, accountId) {
123
+ const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
124
+ return name || groupOpenid.slice(0, 8);
125
+ }
2
126
  function normalizeAppId(raw) {
3
127
  if (raw === null || raw === undefined)
4
128
  return "";