@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.
- package/README.md +19 -15
- package/README.zh.md +19 -15
- package/dist/src/api.d.ts +7 -2
- package/dist/src/api.js +13 -4
- package/dist/src/channel.d.ts +18 -0
- package/dist/src/channel.js +85 -2
- package/dist/src/config.d.ts +33 -2
- package/dist/src/config.js +124 -0
- package/dist/src/gateway.js +565 -23
- package/dist/src/group-history.d.ts +136 -0
- package/dist/src/group-history.js +226 -0
- package/dist/src/message-gating.d.ts +53 -0
- package/dist/src/message-gating.js +107 -0
- package/dist/src/message-queue.d.ts +36 -0
- package/dist/src/message-queue.js +164 -22
- package/dist/src/outbound.d.ts +4 -4
- package/dist/src/outbound.js +8 -8
- package/dist/src/ref-index-store.js +5 -28
- package/dist/src/slash-commands.d.ts +6 -0
- package/dist/src/slash-commands.js +2 -2
- package/dist/src/types.d.ts +88 -0
- package/dist/src/utils/audio-convert.d.ts +1 -1
- package/dist/src/utils/audio-convert.js +1 -1
- package/dist/src/utils/chunked-upload.d.ts +2 -2
- package/dist/src/utils/chunked-upload.js +5 -5
- package/dist/src/utils/text-parsing.js +7 -18
- package/package.json +2 -3
- package/scripts/upgrade-via-source.sh +62 -15
- package/src/api.ts +21 -4
- package/src/channel.ts +85 -2
- package/src/config.ts +169 -2
- package/src/gateway.ts +661 -28
- package/src/group-history.ts +328 -0
- package/src/message-gating.ts +190 -0
- package/src/message-queue.ts +201 -21
- package/src/openclaw-plugin-sdk.d.ts +65 -0
- package/src/outbound.ts +8 -8
- package/src/ref-index-store.ts +5 -27
- package/src/slash-commands.ts +2 -2
- package/src/types.ts +92 -0
- package/src/utils/audio-convert.ts +1 -1
- package/src/utils/chunked-upload.ts +5 -5
- package/src/utils/text-parsing.ts +7 -14
- 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.
|
|
13
|
+
### 🚀 Current Version: `v1.6.6`
|
|
14
14
|
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
132
|
+
AI can send files directly, in any format.
|
|
133
133
|
|
|
134
|
-
<img width="360" src="docs/images/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
12
|
+
### 🚀 当前版本: `v1.6.6`
|
|
13
13
|
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
127
|
+
AI 可直接发送文件,任意格式均可。
|
|
128
128
|
|
|
129
|
-
<img width="360" src="docs/images/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/dist/src/channel.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/channel.js
CHANGED
|
@@ -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-
|
|
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
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/src/config.js
CHANGED
|
@@ -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 "";
|