@ryantest/openclaw-qqbot 1.6.6-alpha.4 → 1.6.7-beta.1
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 +24 -15
- package/README.zh.md +24 -15
- package/dist/src/api.d.ts +32 -5
- package/dist/src/api.js +111 -12
- 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 +125 -1
- package/dist/src/gateway.js +566 -24
- 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 +18 -6
- package/dist/src/ref-index-store.js +5 -28
- package/dist/src/request-context.d.ts +7 -0
- package/dist/src/request-context.js +7 -0
- package/dist/src/slash-commands.d.ts +6 -0
- package/dist/src/slash-commands.js +2 -2
- package/dist/src/tools/remind.js +17 -9
- 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 +11 -2
- package/dist/src/utils/chunked-upload.js +63 -11
- package/dist/src/utils/media-send.js +1 -1
- package/dist/src/utils/text-parsing.js +7 -18
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +22 -9
- package/scripts/upgrade-via-npm.sh +11 -3
- package/scripts/upgrade-via-source.sh +63 -15
- package/skills/qqbot-remind/SKILL.md +21 -11
- package/src/api.ts +135 -7
- package/src/channel.ts +85 -2
- package/src/config.ts +170 -3
- package/src/gateway.ts +662 -29
- 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 +18 -6
- package/src/ref-index-store.ts +5 -27
- package/src/request-context.ts +10 -0
- package/src/slash-commands.ts +2 -2
- package/src/tools/remind.ts +17 -9
- package/src/types.ts +92 -0
- package/src/utils/audio-convert.ts +1 -1
- package/src/utils/chunked-upload.ts +76 -12
- package/src/utils/media-send.ts +1 -2
- package/src/utils/text-parsing.ts +7 -14
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>
|
|
@@ -46,6 +46,7 @@ Scan to join the QQ group chat
|
|
|
46
46
|
| 📝 **Markdown** | Full Markdown formatting support |
|
|
47
47
|
| 🛠️ **Commands** | Native OpenClaw command integration |
|
|
48
48
|
| 💬 **Quoted Context** | Resolve QQ `REFIDX_*` quoted messages and inject quote body into AI context |
|
|
49
|
+
| 📦 **Large File Support** | Auto chunked upload for large files (parallel upload with retry), up to 100 MB |
|
|
49
50
|
|
|
50
51
|
---
|
|
51
52
|
|
|
@@ -61,7 +62,7 @@ QQ quote events carry index keys (e.g. `REFIDX_xxx`) instead of full original me
|
|
|
61
62
|
- Store path: `~/.openclaw/qqbot/data/ref-index.jsonl` (survives gateway restart).
|
|
62
63
|
- Quote body may include text + media summary (image/voice/video/file).
|
|
63
64
|
|
|
64
|
-
<img width="360" src="docs/images/
|
|
65
|
+
<img width="360" src="docs/images/ref-msg.png" alt="Quoted Message Context Demo" />
|
|
65
66
|
|
|
66
67
|
### 🎙️ Voice Messages (STT)
|
|
67
68
|
|
|
@@ -71,7 +72,7 @@ With STT configured, the plugin automatically transcribes voice messages to text
|
|
|
71
72
|
>
|
|
72
73
|
> **QQBot**: Tomorrow (March 7, Saturday) Shenzhen weather forecast 🌤️ ...
|
|
73
74
|
|
|
74
|
-
<img width="360" src="docs/images/
|
|
75
|
+
<img width="360" src="docs/images/voice-stt.jpg" alt="Voice STT Demo" />
|
|
75
76
|
|
|
76
77
|
### 📄 File Understanding
|
|
77
78
|
|
|
@@ -81,7 +82,7 @@ Send any file to the bot — novels, reports, spreadsheets — AI automatically
|
|
|
81
82
|
>
|
|
82
83
|
> **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
84
|
|
|
84
|
-
<img width="360" src="docs/images/
|
|
85
|
+
<img width="360" src="docs/images/file-understand.jpg" alt="File Understanding Demo" />
|
|
85
86
|
|
|
86
87
|
### 🖼️ Image Understanding
|
|
87
88
|
|
|
@@ -91,7 +92,7 @@ If your main model supports vision (e.g. Tencent Hunyuan `hunyuan-vision`), AI c
|
|
|
91
92
|
>
|
|
92
93
|
> **QQBot**: Haha, so cute! Is that a QQ penguin in a lobster costume? 🦞🐧 ...
|
|
93
94
|
|
|
94
|
-
<img width="360" src="docs/images/
|
|
95
|
+
<img width="360" src="docs/images/image-understand.jpg" alt="Image Understanding Demo" />
|
|
95
96
|
|
|
96
97
|
### 🎨 Image Sending
|
|
97
98
|
|
|
@@ -101,7 +102,7 @@ If your main model supports vision (e.g. Tencent Hunyuan `hunyuan-vision`), AI c
|
|
|
101
102
|
|
|
102
103
|
AI can send images directly. Supports local paths and URLs. Formats: jpg/png/gif/webp/bmp.
|
|
103
104
|
|
|
104
|
-
<img width="360" src="docs/images/
|
|
105
|
+
<img width="360" src="docs/images/image-send.jpg" alt="Image Generation Demo" />
|
|
105
106
|
|
|
106
107
|
### 🔊 Voice Sending
|
|
107
108
|
|
|
@@ -111,7 +112,7 @@ AI can send images directly. Supports local paths and URLs. Formats: jpg/png/gif
|
|
|
111
112
|
|
|
112
113
|
AI can send voice messages directly. Formats: mp3/wav/silk/ogg. No ffmpeg required.
|
|
113
114
|
|
|
114
|
-
<img width="360" src="docs/images/
|
|
115
|
+
<img width="360" src="docs/images/voice-send.jpg" alt="TTS Voice Demo" />
|
|
115
116
|
|
|
116
117
|
### ⏰ Scheduled Reminder (Proactive Message)
|
|
117
118
|
|
|
@@ -129,9 +130,13 @@ This capability depends on OpenClaw cron scheduling and proactive messaging. If
|
|
|
129
130
|
>
|
|
130
131
|
> **QQBot**: *(sends a .txt file)*
|
|
131
132
|
|
|
132
|
-
AI can send files directly
|
|
133
|
+
AI can send files directly, in any format.
|
|
133
134
|
|
|
134
|
-
<img width="360" src="docs/images/
|
|
135
|
+
<img width="360" src="docs/images/file-send.jpg" alt="File Sending Demo" />
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
<img width="360" src="docs/images/large-file-transfer.jpg" alt="Large File Transfer Demo" />
|
|
135
140
|
|
|
136
141
|
### 🎬 Video Sending
|
|
137
142
|
|
|
@@ -141,7 +146,7 @@ AI can send files directly. Any format, up to 20MB.
|
|
|
141
146
|
|
|
142
147
|
AI can send videos directly. Supports local files and URLs.
|
|
143
148
|
|
|
144
|
-
<img width="360" src="docs/images/
|
|
149
|
+
<img width="360" src="docs/images/video-send.jpg" alt="Video Sending Demo" />
|
|
145
150
|
|
|
146
151
|
> **Under the hood:** Upload dedup caching, ordered queue delivery, and multi-layer audio format fallback.
|
|
147
152
|
|
|
@@ -207,6 +212,10 @@ All commands support a `?` suffix to show usage:
|
|
|
207
212
|
>
|
|
208
213
|
> **QQBot**: 📖 /bot-upgrade usage: …
|
|
209
214
|
|
|
215
|
+
#### `/bot-clear-storage` — Clear files generated through QQBot conversations and downloaded resources (stored on the host running OpenClaw)
|
|
216
|
+
|
|
217
|
+
`/bot-clear-storage` lists files generated by the conversation and files in the downloaded resources directory. Use `/bot-clear-storage --force` to confirm deletion.
|
|
218
|
+
|
|
210
219
|
---
|
|
211
220
|
|
|
212
221
|
## 🚀 Getting Started
|
|
@@ -220,15 +229,15 @@ All commands support a `?` suffix to show usage:
|
|
|
220
229
|
2. After scanning, tap **Agree** on your phone — you'll land on the bot configuration page.
|
|
221
230
|
3. Click **Create Bot** to create a new QQ bot.
|
|
222
231
|
|
|
223
|
-
<img width="720" alt="Create Bot" src="docs/images/
|
|
232
|
+
<img width="720" alt="Create Bot" src="docs/images/create-robot.png" />
|
|
224
233
|
|
|
225
234
|
> ⚠️ 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
235
|
|
|
227
|
-
<img width="400" alt="Bot Say Hello" src="docs/images/
|
|
236
|
+
<img width="400" alt="Bot Say Hello" src="docs/images/bot-say-hello.jpg" />
|
|
228
237
|
|
|
229
238
|
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
239
|
|
|
231
|
-
<img width="720" alt="Find AppID and AppSecret" src="docs/images/
|
|
240
|
+
<img width="720" alt="Find AppID and AppSecret" src="docs/images/find-appid-secret.png" />
|
|
232
241
|
|
|
233
242
|
> For a step-by-step walkthrough with screenshots, see the [official guide](https://cloud.tencent.com/developer/article/2626045).
|
|
234
243
|
|
|
@@ -458,7 +467,7 @@ Special thanks to [@sliverp](https://github.com/sliverp) for outstanding contrib
|
|
|
458
467
|
Thanks to [Tencent Cloud Lighthouse](https://cloud.tencent.com/product/lighthouse) for the deep collaboration. For raising crawfish, choose Tencent Cloud Lighthouse!
|
|
459
468
|
|
|
460
469
|
<a href="https://cloud.tencent.com/product/lighthouse">
|
|
461
|
-
<img alt="Tencent Cloud Lighthouse" src="./docs/images/
|
|
470
|
+
<img alt="Tencent Cloud Lighthouse" src="./docs/images/lighthouse-head.png" height="500" style="max-width:80%; height:auto;"/>
|
|
462
471
|
</a>
|
|
463
472
|
|
|
464
473
|
## ⭐ 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
|
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
| 📝 **Markdown** | 完整支持 Markdown 格式消息 |
|
|
42
42
|
| 🛠️ **原生命令** | 支持 OpenClaw 原生命令 |
|
|
43
43
|
| 💬 **引用上下文** | 解析 QQ `REFIDX_*` 引用消息,并将引用内容注入 AI 上下文 |
|
|
44
|
+
| 📦 **大文件支持** | 大文件自动分片并行上传,最大支持 100 MB |
|
|
44
45
|
|
|
45
46
|
---
|
|
46
47
|
|
|
@@ -56,7 +57,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
|
|
|
56
57
|
- 存储位置:`~/.openclaw/qqbot/data/ref-index.jsonl`(网关重启后仍可恢复)。
|
|
57
58
|
- 引用内容支持文本 + 媒体摘要(图片/语音/视频/文件)。
|
|
58
59
|
|
|
59
|
-
<img width="360" src="docs/images/
|
|
60
|
+
<img width="360" src="docs/images/ref-msg.png" alt="引用消息上下文演示" />
|
|
60
61
|
|
|
61
62
|
### 🎙️ 语音消息(STT)
|
|
62
63
|
|
|
@@ -66,7 +67,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
|
|
|
66
67
|
>
|
|
67
68
|
> **QQBot**:明天(3月7日 周六)深圳的天气预报 🌤️ ...
|
|
68
69
|
|
|
69
|
-
<img width="360" src="docs/images/
|
|
70
|
+
<img width="360" src="docs/images/voice-stt.jpg" alt="听语音演示" />
|
|
70
71
|
|
|
71
72
|
### 📄 文件理解
|
|
72
73
|
|
|
@@ -76,7 +77,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
|
|
|
76
77
|
>
|
|
77
78
|
> **QQBot**:收到!你上传了列夫·托尔斯泰的《战争与和平》中文版文本。从内容来看,这是第一章的开头……你想让我做什么?
|
|
78
79
|
|
|
79
|
-
<img width="360" src="docs/images/
|
|
80
|
+
<img width="360" src="docs/images/file-understand.jpg" alt="AI理解用户发送的文件" />
|
|
80
81
|
|
|
81
82
|
### 🖼️ 图片理解
|
|
82
83
|
|
|
@@ -86,7 +87,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
|
|
|
86
87
|
>
|
|
87
88
|
> **QQBot**:哈哈,好可爱!这是QQ企鹅穿上小龙虾套装吗?🦞🐧 ...
|
|
88
89
|
|
|
89
|
-
<img width="360" src="docs/images/
|
|
90
|
+
<img width="360" src="docs/images/image-understand.jpg" alt="图片理解演示" />
|
|
90
91
|
|
|
91
92
|
### 🎨 图片发送
|
|
92
93
|
|
|
@@ -96,7 +97,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
|
|
|
96
97
|
|
|
97
98
|
AI 可直接发送图片,支持本地文件路径和网络 URL。格式:jpg/png/gif/webp/bmp。
|
|
98
99
|
|
|
99
|
-
<img width="360" src="docs/images/
|
|
100
|
+
<img width="360" src="docs/images/image-send.jpg" alt="发图片演示" />
|
|
100
101
|
|
|
101
102
|
### 🔊 语音发送
|
|
102
103
|
|
|
@@ -106,7 +107,7 @@ AI 可直接发送图片,支持本地文件路径和网络 URL。格式:jpg/
|
|
|
106
107
|
|
|
107
108
|
AI 可直接发送语音消息。格式:mp3/wav/silk/ogg,无需安装 ffmpeg。
|
|
108
109
|
|
|
109
|
-
<img width="360" src="docs/images/
|
|
110
|
+
<img width="360" src="docs/images/voice-send.jpg" alt="发语音演示" />
|
|
110
111
|
|
|
111
112
|
### ⏰ 定时提醒(主动消息)
|
|
112
113
|
|
|
@@ -124,9 +125,13 @@ AI 可直接发送语音消息。格式:mp3/wav/silk/ogg,无需安装 ffmpeg
|
|
|
124
125
|
>
|
|
125
126
|
> **QQBot**:*(发送 .txt 文件)*
|
|
126
127
|
|
|
127
|
-
AI
|
|
128
|
+
AI 可直接发送文件,任意格式均可。
|
|
128
129
|
|
|
129
|
-
<img width="360" src="docs/images/
|
|
130
|
+
<img width="360" src="docs/images/file-send.jpg" alt="发文件演示" />
|
|
131
|
+
|
|
132
|
+
v1.6.6 起支持大文件传输:图片最大 20MB,视频最大 30MB,附件最大 100MB,每日累计传输上限 2GB。
|
|
133
|
+
|
|
134
|
+
<img width="360" src="docs/images/large-file-transfer.jpg" alt="大文件传输演示" />
|
|
130
135
|
|
|
131
136
|
### 🎬 视频发送
|
|
132
137
|
|
|
@@ -136,7 +141,7 @@ AI 可直接发送文件。任意格式,最大 20MB。
|
|
|
136
141
|
|
|
137
142
|
AI 可直接发送视频,支持本地文件和公网 URL。
|
|
138
143
|
|
|
139
|
-
<img width="360" src="docs/images/
|
|
144
|
+
<img width="360" src="docs/images/video-send.jpg" alt="发视频演示" />
|
|
140
145
|
|
|
141
146
|
> **底层细节:** 上传去重缓存、有序队列发送、音频格式多层降级。
|
|
142
147
|
|
|
@@ -202,6 +207,10 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
202
207
|
>
|
|
203
208
|
> **QQBot**:📖 /bot-upgrade 用法:…
|
|
204
209
|
|
|
210
|
+
#### `/bot-clear-storage` — 清理通过 QQBot 对话产生的文件以及下载的资源(保存在 OpenClaw 运行环境的主机上)
|
|
211
|
+
|
|
212
|
+
`/bot-clear-storage` 列出对话产生的文件以及下载的资源目录里的文件,使用`/bot-clear-storage -- force`确定删除。
|
|
213
|
+
|
|
205
214
|
---
|
|
206
215
|
|
|
207
216
|
## 🚀 快速开始
|
|
@@ -215,15 +224,15 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
215
224
|
2. 手机 QQ 扫码后选择**同意**,即完成注册,进入 QQ 机器人配置页。
|
|
216
225
|
3. 点击**创建机器人**,即可直接新建一个 QQ 机器人。
|
|
217
226
|
|
|
218
|
-
<img width="720" alt="创建机器人" src="docs/images/
|
|
227
|
+
<img width="720" alt="创建机器人" src="docs/images/create-robot.png" />
|
|
219
228
|
|
|
220
229
|
> ⚠️ 机器人创建后会自动出现在你的 QQ 消息列表中,并发送第一条消息。但在完成下面的配置之前,发消息会提示"该机器人去火星了",属于正常现象。
|
|
221
230
|
|
|
222
|
-
<img width="400" alt="机器人打招呼" src="docs/images/
|
|
231
|
+
<img width="400" alt="机器人打招呼" src="docs/images/bot-say-hello.jpg" />
|
|
223
232
|
|
|
224
233
|
4. 在机器人页面中找到 **AppID** 和 **AppSecret**,分别点击右侧**复制**按钮,保存到记事本或备忘录中。**AppSecret 不支持明文保存,离开页面后再查看会强制重置,请务必妥善保存。**
|
|
225
234
|
|
|
226
|
-
<img width="720" alt="找到 AppID 和 AppSecret" src="docs/images/
|
|
235
|
+
<img width="720" alt="找到 AppID 和 AppSecret" src="docs/images/find-appid-secret.png" />
|
|
227
236
|
|
|
228
237
|
> 详细图文教程请参阅 [官方指南](https://cloud.tencent.com/developer/article/2626045)。
|
|
229
238
|
|
|
@@ -453,7 +462,7 @@ STT 支持两级配置,按优先级查找:
|
|
|
453
462
|
感谢[腾讯云Lighthouse](https://cloud.tencent.com/product/lighthouse)的深度合作,养小龙虾,首选腾讯云Lighthouse!
|
|
454
463
|
|
|
455
464
|
<a href="https://cloud.tencent.com/product/lighthouse">
|
|
456
|
-
<img alt="腾讯云 Lighthouse" src="./docs/images/
|
|
465
|
+
<img alt="腾讯云 Lighthouse" src="./docs/images/lighthouse-head.png" height="500" style="max-width:80%; height:auto;"/>
|
|
457
466
|
</a>
|
|
458
467
|
|
|
459
468
|
## ⭐ Star History
|
package/dist/src/api.d.ts
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
* QQ Bot API 鉴权和请求封装
|
|
3
3
|
* [修复版] 已重构为支持多实例并发,消除全局变量冲突
|
|
4
4
|
*/
|
|
5
|
-
/** API 请求错误,携带 HTTP status code */
|
|
5
|
+
/** API 请求错误,携带 HTTP status code 和业务错误码 */
|
|
6
6
|
export declare class ApiError extends Error {
|
|
7
7
|
readonly status: number;
|
|
8
8
|
readonly path: string;
|
|
9
|
-
|
|
9
|
+
/** 业务错误码(回包中的 code / err_code 字段),不一定存在 */
|
|
10
|
+
readonly bizCode?: number | undefined;
|
|
11
|
+
/** 回包中的原始 message 字段(用于向用户展示兜底文案) */
|
|
12
|
+
readonly bizMessage?: string | undefined;
|
|
13
|
+
constructor(message: string, status: number, path: string,
|
|
14
|
+
/** 业务错误码(回包中的 code / err_code 字段),不一定存在 */
|
|
15
|
+
bizCode?: number | undefined,
|
|
16
|
+
/** 回包中的原始 message 字段(用于向用户展示兜底文案) */
|
|
17
|
+
bizMessage?: string | undefined);
|
|
10
18
|
}
|
|
11
19
|
export declare const PLUGIN_USER_AGENT: string;
|
|
12
20
|
/** 出站消息元信息(结构化存储,不做预格式化) */
|
|
@@ -69,7 +77,22 @@ export declare function getNextMsgSeq(_msgId: string): number;
|
|
|
69
77
|
* API 请求封装
|
|
70
78
|
*/
|
|
71
79
|
export declare function apiRequest<T = unknown>(accessToken: string, method: string, path: string, body?: unknown, timeoutMs?: number): Promise<T>;
|
|
80
|
+
/**
|
|
81
|
+
* 需要持续重试的业务错误码集合
|
|
82
|
+
* 当 upload_part_finish 返回这些错误码时,会以固定 1s 间隔持续重试直到成功或超时
|
|
83
|
+
*/
|
|
84
|
+
export declare const PART_FINISH_RETRYABLE_CODES: Set<number>;
|
|
85
|
+
/**
|
|
86
|
+
* upload_prepare 接口命中此错误码时,携带文件信息抛出 UploadDailyLimitExceededError,
|
|
87
|
+
* 由上层(outbound.ts)构造包含文件路径和大小的兜底文案发送给用户,
|
|
88
|
+
* 而非走通用的"文件发送失败,请稍后重试"
|
|
89
|
+
*/
|
|
90
|
+
export declare const UPLOAD_PREPARE_FALLBACK_CODE = 40093002;
|
|
72
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;
|
|
73
96
|
export interface MessageResponse {
|
|
74
97
|
id: string;
|
|
75
98
|
timestamp: number | string;
|
|
@@ -95,7 +118,7 @@ export declare function sendDmMessage(accessToken: string, guildId: string, cont
|
|
|
95
118
|
id: string;
|
|
96
119
|
timestamp: string;
|
|
97
120
|
}>;
|
|
98
|
-
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>;
|
|
99
122
|
export declare function sendProactiveC2CMessage(accessToken: string, openid: string, content: string): Promise<MessageResponse>;
|
|
100
123
|
export declare function sendProactiveGroupMessage(accessToken: string, groupOpenid: string, content: string): Promise<{
|
|
101
124
|
id: string;
|
|
@@ -128,6 +151,10 @@ export interface UploadPrepareResponse {
|
|
|
128
151
|
block_size: number;
|
|
129
152
|
/** 分片列表(含预签名链接) */
|
|
130
153
|
parts: UploadPart[];
|
|
154
|
+
/** 上传并发数(由服务端控制,可选,不返回时使用客户端默认值) */
|
|
155
|
+
concurrency?: number;
|
|
156
|
+
/** upload_part_finish 特定错误码的重试超时时间(秒),由服务端控制,客户端上限 10 分钟 */
|
|
157
|
+
retry_timeout?: number;
|
|
131
158
|
}
|
|
132
159
|
/** 完成文件上传响应(与 UploadMediaResponse 一致) */
|
|
133
160
|
export interface MediaUploadResponse {
|
|
@@ -171,7 +198,7 @@ export declare function c2cUploadPrepare(accessToken: string, userId: string, fi
|
|
|
171
198
|
* @param blockSize - 分块大小(字节)
|
|
172
199
|
* @param md5 - 分片数据的 MD5(十六进制)
|
|
173
200
|
*/
|
|
174
|
-
export declare function c2cUploadPartFinish(accessToken: string, userId: string, uploadId: string, partIndex: number, blockSize: number, md5: string): Promise<void>;
|
|
201
|
+
export declare function c2cUploadPartFinish(accessToken: string, userId: string, uploadId: string, partIndex: number, blockSize: number, md5: string, retryTimeoutMs?: number): Promise<void>;
|
|
175
202
|
/**
|
|
176
203
|
* 完成文件上传(C2C)
|
|
177
204
|
* POST /v2/users/{user_id}/files
|
|
@@ -191,7 +218,7 @@ export declare function groupUploadPrepare(accessToken: string, groupId: string,
|
|
|
191
218
|
* 完成分片上传(Group)
|
|
192
219
|
* POST /v2/groups/{group_id}/upload_part_finish
|
|
193
220
|
*/
|
|
194
|
-
export declare function groupUploadPartFinish(accessToken: string, groupId: string, uploadId: string, partIndex: number, blockSize: number, md5: string): Promise<void>;
|
|
221
|
+
export declare function groupUploadPartFinish(accessToken: string, groupId: string, uploadId: string, partIndex: number, blockSize: number, md5: string, retryTimeoutMs?: number): Promise<void>;
|
|
195
222
|
/**
|
|
196
223
|
* 完成文件上传(Group)
|
|
197
224
|
* POST /v2/groups/{group_id}/files
|
package/dist/src/api.js
CHANGED
|
@@ -6,14 +6,22 @@ import os from "node:os";
|
|
|
6
6
|
import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
|
|
7
7
|
import { sanitizeFileName } from "./utils/platform.js";
|
|
8
8
|
// ============ 自定义错误 ============
|
|
9
|
-
/** API 请求错误,携带 HTTP status code */
|
|
9
|
+
/** API 请求错误,携带 HTTP status code 和业务错误码 */
|
|
10
10
|
export class ApiError extends Error {
|
|
11
11
|
status;
|
|
12
12
|
path;
|
|
13
|
-
|
|
13
|
+
bizCode;
|
|
14
|
+
bizMessage;
|
|
15
|
+
constructor(message, status, path,
|
|
16
|
+
/** 业务错误码(回包中的 code / err_code 字段),不一定存在 */
|
|
17
|
+
bizCode,
|
|
18
|
+
/** 回包中的原始 message 字段(用于向用户展示兜底文案) */
|
|
19
|
+
bizMessage) {
|
|
14
20
|
super(message);
|
|
15
21
|
this.status = status;
|
|
16
22
|
this.path = path;
|
|
23
|
+
this.bizCode = bizCode;
|
|
24
|
+
this.bizMessage = bizMessage;
|
|
17
25
|
this.name = "ApiError";
|
|
18
26
|
}
|
|
19
27
|
}
|
|
@@ -264,7 +272,8 @@ export async function apiRequest(accessToken, method, path, body, timeoutMs) {
|
|
|
264
272
|
// JSON 错误响应
|
|
265
273
|
try {
|
|
266
274
|
const error = JSON.parse(rawBody);
|
|
267
|
-
|
|
275
|
+
const bizCode = error.code ?? error.err_code;
|
|
276
|
+
throw new ApiError(`API Error [${path}]: ${error.message ?? rawBody}`, res.status, path, bizCode, error.message);
|
|
268
277
|
}
|
|
269
278
|
catch (parseErr) {
|
|
270
279
|
if (parseErr instanceof ApiError)
|
|
@@ -332,10 +341,51 @@ async function completeUploadWithRetry(accessToken, method, path, body) {
|
|
|
332
341
|
}
|
|
333
342
|
throw lastError;
|
|
334
343
|
}
|
|
335
|
-
// ============
|
|
344
|
+
// ============ 分片完成重试 ============
|
|
345
|
+
/** 普通错误最大重试次数 */
|
|
336
346
|
const PART_FINISH_MAX_RETRIES = 2;
|
|
337
347
|
const PART_FINISH_BASE_DELAY_MS = 1000;
|
|
338
|
-
|
|
348
|
+
/**
|
|
349
|
+
* 需要持续重试的业务错误码集合
|
|
350
|
+
* 当 upload_part_finish 返回这些错误码时,会以固定 1s 间隔持续重试直到成功或超时
|
|
351
|
+
*/
|
|
352
|
+
export const PART_FINISH_RETRYABLE_CODES = new Set([
|
|
353
|
+
40093001,
|
|
354
|
+
]);
|
|
355
|
+
/**
|
|
356
|
+
* upload_prepare 接口命中此错误码时,携带文件信息抛出 UploadDailyLimitExceededError,
|
|
357
|
+
* 由上层(outbound.ts)构造包含文件路径和大小的兜底文案发送给用户,
|
|
358
|
+
* 而非走通用的"文件发送失败,请稍后重试"
|
|
359
|
+
*/
|
|
360
|
+
export const UPLOAD_PREPARE_FALLBACK_CODE = 40093002;
|
|
361
|
+
/** 特定错误码持续重试的默认超时(服务端未返回 retry_timeout 时的兜底) */
|
|
362
|
+
const PART_FINISH_RETRYABLE_DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
363
|
+
/** 特定错误码重试的固定间隔(1 秒) */
|
|
364
|
+
const PART_FINISH_RETRYABLE_INTERVAL_MS = 1000;
|
|
365
|
+
/**
|
|
366
|
+
* 判断错误是否命中"需要持续重试"的业务错误码
|
|
367
|
+
*/
|
|
368
|
+
function isRetryableBizCode(err) {
|
|
369
|
+
if (PART_FINISH_RETRYABLE_CODES.size === 0)
|
|
370
|
+
return false;
|
|
371
|
+
if (err instanceof ApiError && err.bizCode !== undefined) {
|
|
372
|
+
return PART_FINISH_RETRYABLE_CODES.has(err.bizCode);
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* 分片完成接口重试策略:
|
|
378
|
+
*
|
|
379
|
+
* 1. 命中 PART_FINISH_RETRYABLE_CODES 的错误码 → 每 1s 重试一次,直到成功或超时
|
|
380
|
+
* 超时时间 = min(API 返回的 retry_timeout, 10 分钟)
|
|
381
|
+
* 2. 其他错误 → 最多重试 PART_FINISH_MAX_RETRIES 次(与之前逻辑一致)
|
|
382
|
+
*
|
|
383
|
+
* 若持续重试超时或普通重试耗尽,抛出错误,调用方(chunkedUpload)
|
|
384
|
+
* 可据此中止后续分片上传。
|
|
385
|
+
*
|
|
386
|
+
* @param retryTimeoutMs - 持续重试的超时时间(毫秒),由 upload_prepare 返回的 retry_timeout 计算得出
|
|
387
|
+
*/
|
|
388
|
+
async function partFinishWithRetry(accessToken, method, path, body, retryTimeoutMs) {
|
|
339
389
|
let lastError = null;
|
|
340
390
|
for (let attempt = 0; attempt <= PART_FINISH_MAX_RETRIES; attempt++) {
|
|
341
391
|
try {
|
|
@@ -344,6 +394,13 @@ async function partFinishWithRetry(accessToken, method, path, body) {
|
|
|
344
394
|
}
|
|
345
395
|
catch (err) {
|
|
346
396
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
397
|
+
// 命中特定错误码 → 进入持续重试模式
|
|
398
|
+
if (isRetryableBizCode(err)) {
|
|
399
|
+
const timeoutMs = retryTimeoutMs ?? PART_FINISH_RETRYABLE_DEFAULT_TIMEOUT_MS;
|
|
400
|
+
console.warn(`[qqbot-api] PartFinish hit retryable bizCode=${err.bizCode}, entering persistent retry (timeout=${timeoutMs / 1000}s, interval=1s)...`);
|
|
401
|
+
await partFinishPersistentRetry(accessToken, method, path, body, timeoutMs);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
347
404
|
if (attempt < PART_FINISH_MAX_RETRIES) {
|
|
348
405
|
const delay = PART_FINISH_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
349
406
|
console.warn(`[qqbot-api] PartFinish attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 200)}`);
|
|
@@ -353,10 +410,52 @@ async function partFinishWithRetry(accessToken, method, path, body) {
|
|
|
353
410
|
}
|
|
354
411
|
throw lastError;
|
|
355
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* 特定错误码的持续重试模式
|
|
415
|
+
* 不限次数,仅受总超时时间约束,固定每 1 秒重试一次
|
|
416
|
+
*/
|
|
417
|
+
async function partFinishPersistentRetry(accessToken, method, path, body, timeoutMs) {
|
|
418
|
+
const deadline = Date.now() + timeoutMs;
|
|
419
|
+
let attempt = 0;
|
|
420
|
+
let lastError = null;
|
|
421
|
+
while (Date.now() < deadline) {
|
|
422
|
+
try {
|
|
423
|
+
await apiRequest(accessToken, method, path, body);
|
|
424
|
+
console.log(`[qqbot-api] PartFinish persistent retry succeeded after ${attempt} retries`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
429
|
+
// 如果不再是可重试的错误码,直接抛出(可能是其他类型的错误)
|
|
430
|
+
if (!isRetryableBizCode(err)) {
|
|
431
|
+
console.error(`[qqbot-api] PartFinish persistent retry: error is no longer retryable (bizCode=${err.bizCode ?? "N/A"}), aborting`);
|
|
432
|
+
throw lastError;
|
|
433
|
+
}
|
|
434
|
+
attempt++;
|
|
435
|
+
const remaining = deadline - Date.now();
|
|
436
|
+
if (remaining <= 0)
|
|
437
|
+
break;
|
|
438
|
+
const actualDelay = Math.min(PART_FINISH_RETRYABLE_INTERVAL_MS, remaining);
|
|
439
|
+
console.warn(`[qqbot-api] PartFinish persistent retry #${attempt}: bizCode=${err.bizCode}, retrying in ${actualDelay}ms (remaining=${Math.round(remaining / 1000)}s)`);
|
|
440
|
+
await new Promise(resolve => setTimeout(resolve, actualDelay));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// 超时
|
|
444
|
+
console.error(`[qqbot-api] PartFinish persistent retry timed out after ${timeoutMs / 1000}s (${attempt} attempts)`);
|
|
445
|
+
throw new Error(`upload_part_finish 持续重试超时(${timeoutMs / 1000}s, ${attempt} 次重试),中止上传`);
|
|
446
|
+
}
|
|
356
447
|
export async function getGatewayUrl(accessToken) {
|
|
357
448
|
const data = await apiRequest(accessToken, "GET", "/gateway");
|
|
358
449
|
return data.url;
|
|
359
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
|
+
}
|
|
360
459
|
/**
|
|
361
460
|
* 发送消息并自动触发 refIdx 回调
|
|
362
461
|
* 所有消息发送函数统一经过此处,确保每条出站消息的 refIdx 都被捕获
|
|
@@ -429,10 +528,10 @@ export async function sendDmMessage(accessToken, guildId, content, msgId) {
|
|
|
429
528
|
...(msgId ? { msg_id: msgId } : {}),
|
|
430
529
|
});
|
|
431
530
|
}
|
|
432
|
-
export async function sendGroupMessage(accessToken, groupOpenid, content, msgId) {
|
|
531
|
+
export async function sendGroupMessage(accessToken, groupOpenid, content, msgId, messageReference) {
|
|
433
532
|
const msgSeq = msgId ? getNextMsgSeq(msgId) : 1;
|
|
434
|
-
const body = buildMessageBody(content, msgId, msgSeq);
|
|
435
|
-
return
|
|
533
|
+
const body = buildMessageBody(content, msgId, msgSeq, messageReference);
|
|
534
|
+
return sendAndNotify(accessToken, "POST", `/v2/groups/${groupOpenid}/messages`, body, { text: content });
|
|
436
535
|
}
|
|
437
536
|
function buildProactiveMessageBody(content) {
|
|
438
537
|
if (!content || content.trim().length === 0) {
|
|
@@ -487,8 +586,8 @@ export async function c2cUploadPrepare(accessToken, userId, fileType, fileName,
|
|
|
487
586
|
* @param blockSize - 分块大小(字节)
|
|
488
587
|
* @param md5 - 分片数据的 MD5(十六进制)
|
|
489
588
|
*/
|
|
490
|
-
export async function c2cUploadPartFinish(accessToken, userId, uploadId, partIndex, blockSize, md5) {
|
|
491
|
-
await partFinishWithRetry(accessToken, "POST", `/v2/users/${userId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 });
|
|
589
|
+
export async function c2cUploadPartFinish(accessToken, userId, uploadId, partIndex, blockSize, md5, retryTimeoutMs) {
|
|
590
|
+
await partFinishWithRetry(accessToken, "POST", `/v2/users/${userId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 }, retryTimeoutMs);
|
|
492
591
|
}
|
|
493
592
|
/**
|
|
494
593
|
* 完成文件上传(C2C)
|
|
@@ -513,8 +612,8 @@ export async function groupUploadPrepare(accessToken, groupId, fileType, fileNam
|
|
|
513
612
|
* 完成分片上传(Group)
|
|
514
613
|
* POST /v2/groups/{group_id}/upload_part_finish
|
|
515
614
|
*/
|
|
516
|
-
export async function groupUploadPartFinish(accessToken, groupId, uploadId, partIndex, blockSize, md5) {
|
|
517
|
-
await partFinishWithRetry(accessToken, "POST", `/v2/groups/${groupId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 });
|
|
615
|
+
export async function groupUploadPartFinish(accessToken, groupId, uploadId, partIndex, blockSize, md5, retryTimeoutMs) {
|
|
616
|
+
await partFinishWithRetry(accessToken, "POST", `/v2/groups/${groupId}/upload_part_finish`, { upload_id: uploadId, part_index: partIndex, block_size: blockSize, md5 }, retryTimeoutMs);
|
|
518
617
|
}
|
|
519
618
|
/**
|
|
520
619
|
* 完成文件上传(Group)
|
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;
|