@ryantest/openclaw-qqbot 1.6.6-alpha.3 → 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.
Files changed (56) hide show
  1. package/README.md +24 -15
  2. package/README.zh.md +24 -15
  3. package/dist/src/api.d.ts +32 -5
  4. package/dist/src/api.js +111 -12
  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 +125 -1
  9. package/dist/src/gateway.js +566 -24
  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-deliver.js +4 -3
  17. package/dist/src/outbound.d.ts +4 -4
  18. package/dist/src/outbound.js +18 -6
  19. package/dist/src/ref-index-store.js +5 -28
  20. package/dist/src/request-context.d.ts +7 -0
  21. package/dist/src/request-context.js +7 -0
  22. package/dist/src/slash-commands.d.ts +6 -0
  23. package/dist/src/slash-commands.js +2 -2
  24. package/dist/src/tools/remind.js +17 -9
  25. package/dist/src/types.d.ts +88 -0
  26. package/dist/src/utils/audio-convert.d.ts +1 -1
  27. package/dist/src/utils/audio-convert.js +1 -1
  28. package/dist/src/utils/chunked-upload.d.ts +11 -2
  29. package/dist/src/utils/chunked-upload.js +63 -11
  30. package/dist/src/utils/media-send.d.ts +1 -0
  31. package/dist/src/utils/media-send.js +22 -0
  32. package/dist/src/utils/text-parsing.js +7 -18
  33. package/package.json +1 -1
  34. package/scripts/postinstall-link-sdk.js +22 -9
  35. package/scripts/upgrade-via-npm.sh +11 -3
  36. package/scripts/upgrade-via-source.sh +63 -15
  37. package/skills/qqbot-remind/SKILL.md +21 -11
  38. package/src/api.ts +135 -7
  39. package/src/channel.ts +85 -2
  40. package/src/config.ts +170 -3
  41. package/src/gateway.ts +662 -29
  42. package/src/group-history.ts +328 -0
  43. package/src/message-gating.ts +190 -0
  44. package/src/message-queue.ts +201 -21
  45. package/src/openclaw-plugin-sdk.d.ts +65 -0
  46. package/src/outbound-deliver.ts +4 -3
  47. package/src/outbound.ts +18 -6
  48. package/src/ref-index-store.ts +5 -27
  49. package/src/request-context.ts +10 -0
  50. package/src/slash-commands.ts +2 -2
  51. package/src/tools/remind.ts +17 -9
  52. package/src/types.ts +92 -0
  53. package/src/utils/audio-convert.ts +1 -1
  54. package/src/utils/chunked-upload.ts +76 -12
  55. package/src/utils/media-send.ts +22 -0
  56. 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.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>
@@ -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/ref_msg.png" alt="Quoted Message Context Demo" />
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/fc7b2236896cfba3a37c94be5d59ce3e_720.jpg" alt="Voice STT Demo" />
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/07bff56ab68e03173d2af586eeb3bcee_720.jpg" alt="File Understanding Demo" />
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/59d421891f813b0d3c0cbe12574b6a72_720.jpg" alt="Image Understanding Demo" />
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/4645f2b3a20822b7f8d6664a708529eb_720.jpg" alt="Image Generation Demo" />
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/21dce8bfc553ce23d1bd1b270e9c516c.jpg" alt="TTS Voice Demo" />
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. Any format, up to 20MB.
133
+ AI can send files directly, in any format.
133
134
 
134
- <img width="360" src="docs/images/17cada70df90185d45a2d6dd36e92f2f_720.jpg" alt="File Sending Demo" />
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/85d03b8a216f267ab7b2aee248a18a41_720.jpg" alt="Video Sending Demo" />
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/create_robot.png" />
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/bot_say_hello.jpg" />
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/find_appid_secret.png" />
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/lighthouse_head.png" height="500" style="max-width:80%; height:auto;"/>
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.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
 
@@ -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/ref_msg.png" alt="引用消息上下文演示" />
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/fc7b2236896cfba3a37c94be5d59ce3e_720.jpg" alt="听语音演示" />
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/07bff56ab68e03173d2af586eeb3bcee_720.jpg" alt="AI理解用户发送的文件" />
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/59d421891f813b0d3c0cbe12574b6a72_720.jpg" alt="图片理解演示" />
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/4645f2b3a20822b7f8d6664a708529eb_720.jpg" alt="发图片演示" />
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/21dce8bfc553ce23d1bd1b270e9c516c.jpg" alt="发语音演示" />
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 可直接发送文件。任意格式,最大 20MB。
128
+ AI 可直接发送文件,任意格式均可。
128
129
 
129
- <img width="360" src="docs/images/17cada70df90185d45a2d6dd36e92f2f_720.jpg" alt="发文件演示" />
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/85d03b8a216f267ab7b2aee248a18a41_720.jpg" alt="发视频演示" />
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/create_robot.png" />
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/bot_say_hello.jpg" />
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/find_appid_secret.png" />
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/lighthouse_head.png" height="500" style="max-width:80%; height:auto;"/>
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
- constructor(message: string, status: number, path: string);
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
- constructor(message, status, path) {
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
- throw new ApiError(`API Error [${path}]: ${error.message ?? rawBody}`, res.status, path);
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
- // ============ 分片完成重试(无条件,与 completeUpload 策略一致) ============
344
+ // ============ 分片完成重试 ============
345
+ /** 普通错误最大重试次数 */
336
346
  const PART_FINISH_MAX_RETRIES = 2;
337
347
  const PART_FINISH_BASE_DELAY_MS = 1000;
338
- async function partFinishWithRetry(accessToken, method, path, body) {
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 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 });
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)
@@ -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;