@tencent-connect/openclaw-qqbot 1.6.6-alpha.4 → 1.6.7-beta.3
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.js +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/gateway.js +1 -1
- package/dist/src/request-context.d.ts +7 -0
- package/dist/src/request-context.js +7 -0
- package/dist/src/slash-commands.js +58 -15
- package/dist/src/tools/remind.js +17 -9
- package/dist/src/types.d.ts +9 -2
- package/dist/src/update-checker.d.ts +3 -1
- package/dist/src/update-checker.js +13 -2
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +22 -9
- package/scripts/upgrade-via-npm.ps1 +9 -0
- package/scripts/upgrade-via-npm.sh +28 -11
- package/scripts/upgrade-via-source.sh +35 -29
- package/skills/qqbot-remind/SKILL.md +21 -11
- package/src/api.ts +1 -1
- package/src/config.ts +1 -1
- package/src/gateway.ts +1 -1
- package/src/request-context.ts +10 -0
- package/src/slash-commands.ts +56 -14
- package/src/tools/remind.ts +17 -9
- package/src/types.ts +9 -2
- package/src/update-checker.ts +14 -2
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.js
CHANGED
|
@@ -106,7 +106,7 @@ async function doFetchToken(appId, clientSecret) {
|
|
|
106
106
|
const requestBody = { appId, clientSecret };
|
|
107
107
|
const requestHeaders = { "Content-Type": "application/json", "User-Agent": PLUGIN_USER_AGENT };
|
|
108
108
|
// 打印请求信息(隐藏敏感信息)
|
|
109
|
-
console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL}`);
|
|
109
|
+
console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL} [secret: ${clientSecret.slice(0, 6)}...len=${clientSecret.length}]`);
|
|
110
110
|
let response;
|
|
111
111
|
try {
|
|
112
112
|
response = await fetch(TOKEN_URL, {
|
package/dist/src/config.js
CHANGED
|
@@ -168,7 +168,7 @@ export function resolveDefaultQQBotAccountId(cfg) {
|
|
|
168
168
|
* 解析 QQBot 账户配置
|
|
169
169
|
*/
|
|
170
170
|
export function resolveQQBotAccount(cfg, accountId) {
|
|
171
|
-
const resolvedAccountId = accountId ??
|
|
171
|
+
const resolvedAccountId = accountId ?? resolveDefaultQQBotAccountId(cfg);
|
|
172
172
|
const qqbot = cfg.channels?.qqbot;
|
|
173
173
|
// 基础配置
|
|
174
174
|
let accountConfig = {};
|
package/dist/src/gateway.js
CHANGED
|
@@ -1131,7 +1131,7 @@ export async function startGateway(ctx) {
|
|
|
1131
1131
|
const sendErrorMessage = (errorText) => sendErrorToTarget(replyCtx, errorText);
|
|
1132
1132
|
// 使用 AsyncLocalStorage 建立请求级上下文,作用域内所有异步代码
|
|
1133
1133
|
// (包括 AI agent 调用、tool execute)都能安全获取当前会话信息,无并发竞态。
|
|
1134
|
-
await runWithRequestContext({ target: qualifiedTarget }, async () => {
|
|
1134
|
+
await runWithRequestContext({ target: qualifiedTarget, accountId: account.accountId }, async () => {
|
|
1135
1135
|
try {
|
|
1136
1136
|
const messagesConfig = pluginRuntime.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId);
|
|
1137
1137
|
// 追踪是否有响应
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface RequestContext {
|
|
2
2
|
/** 投递目标地址,如 qqbot:c2c:xxx 或 qqbot:group:xxx */
|
|
3
3
|
target: string;
|
|
4
|
+
/** 当前请求的 QQBot 账户 ID(多账户场景) */
|
|
5
|
+
accountId?: string;
|
|
4
6
|
}
|
|
5
7
|
/**
|
|
6
8
|
* 在请求级作用域中执行回调。
|
|
@@ -16,3 +18,8 @@ export declare function getRequestContext(): RequestContext | undefined;
|
|
|
16
18
|
* 便捷方法,等价于 getRequestContext()?.target。
|
|
17
19
|
*/
|
|
18
20
|
export declare function getRequestTarget(): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* 获取当前请求的账户 ID。
|
|
23
|
+
* 便捷方法,等价于 getRequestContext()?.accountId。
|
|
24
|
+
*/
|
|
25
|
+
export declare function getRequestAccountId(): string | undefined;
|
|
@@ -28,3 +28,10 @@ export function getRequestContext() {
|
|
|
28
28
|
export function getRequestTarget() {
|
|
29
29
|
return asyncLocalStorage.getStore()?.target;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 获取当前请求的账户 ID。
|
|
33
|
+
* 便捷方法,等价于 getRequestContext()?.accountId。
|
|
34
|
+
*/
|
|
35
|
+
export function getRequestAccountId() {
|
|
36
|
+
return asyncLocalStorage.getStore()?.accountId;
|
|
37
|
+
}
|
|
@@ -152,7 +152,7 @@ function checkUpgradeCompatibility() {
|
|
|
152
152
|
// 3. 检查 Node.js 版本
|
|
153
153
|
const nodeVer = process.version.replace(/^v/, "");
|
|
154
154
|
if (compareSemver(nodeVer, req.minNodeVersion) < 0) {
|
|
155
|
-
errors.push(`❌
|
|
155
|
+
errors.push(`❌ NoVBNde.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
|
|
156
156
|
}
|
|
157
157
|
// 4. 检查系统架构(arm 等特殊架构提示)
|
|
158
158
|
const arch = process.arch;
|
|
@@ -642,15 +642,24 @@ function cleanupTempScript() {
|
|
|
642
642
|
*
|
|
643
643
|
* 安全机制:脚本会被复制到临时目录再执行,避免升级过程中插件目录被操作导致脚本自身丢失。
|
|
644
644
|
*/
|
|
645
|
-
function fireHotUpgrade(targetVersion) {
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
645
|
+
function fireHotUpgrade(targetVersion, pkg, useLocal) {
|
|
646
|
+
// --local: 直接使用本地脚本,跳过远端下载
|
|
647
|
+
// 默认: 优先从远端下载升级脚本,避免使用本地可能过时的版本
|
|
648
|
+
const scriptPath = useLocal
|
|
649
|
+
? (() => {
|
|
650
|
+
const local = getUpgradeScriptPath();
|
|
651
|
+
if (!local)
|
|
652
|
+
return null;
|
|
653
|
+
console.log(`[qqbot] fireHotUpgrade: --local specified, using local script: ${local}`);
|
|
654
|
+
return copyScriptToTemp(local) || local;
|
|
655
|
+
})()
|
|
656
|
+
: downloadRemoteUpgradeScript() || (() => {
|
|
657
|
+
const local = getUpgradeScriptPath();
|
|
658
|
+
if (!local)
|
|
659
|
+
return null;
|
|
660
|
+
console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
|
|
661
|
+
return copyScriptToTemp(local) || local;
|
|
662
|
+
})();
|
|
654
663
|
if (!scriptPath)
|
|
655
664
|
return { ok: false, reason: "no-script" };
|
|
656
665
|
const cli = findCli();
|
|
@@ -669,6 +678,7 @@ function fireHotUpgrade(targetVersion) {
|
|
|
669
678
|
"-File", scriptPath,
|
|
670
679
|
"-NoRestart",
|
|
671
680
|
...(targetVersion ? ["-Version", targetVersion] : []),
|
|
681
|
+
...(pkg ? ["-Pkg", pkg] : []),
|
|
672
682
|
];
|
|
673
683
|
}
|
|
674
684
|
else {
|
|
@@ -677,9 +687,9 @@ function fireHotUpgrade(targetVersion) {
|
|
|
677
687
|
if (!bash)
|
|
678
688
|
return { ok: false, reason: "no-bash" };
|
|
679
689
|
shell = bash;
|
|
680
|
-
shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
|
|
690
|
+
shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : []), ...(pkg ? ["--pkg", pkg] : [])];
|
|
681
691
|
}
|
|
682
|
-
console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}`);
|
|
692
|
+
console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}, pkg=${pkg || "default"}`);
|
|
683
693
|
// 异步执行升级脚本
|
|
684
694
|
execFile(shell, shellArgs, {
|
|
685
695
|
timeout: 120_000,
|
|
@@ -786,11 +796,13 @@ registerCommand({
|
|
|
786
796
|
`/bot-upgrade 检查是否有新版本`,
|
|
787
797
|
`/bot-upgrade --latest 确认升级到最新版本(需 upgradeMode=hot-reload)`,
|
|
788
798
|
`/bot-upgrade --version X 升级到指定版本(需 upgradeMode=hot-reload)`,
|
|
799
|
+
`/bot-upgrade --pkg scope/name 指定 npm 包(如 ryantest/openclaw-qqbot)`,
|
|
789
800
|
`/bot-upgrade --force 强制重新安装当前版本(需 upgradeMode=hot-reload)`,
|
|
801
|
+
`/bot-upgrade --local 使用本地升级脚本(跳过远端下载)`,
|
|
790
802
|
].join("\n"),
|
|
791
803
|
handler: async (ctx) => {
|
|
792
804
|
const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
|
|
793
|
-
const upgradeMode = ctx.accountConfig?.upgradeMode || "
|
|
805
|
+
const upgradeMode = ctx.accountConfig?.upgradeMode || "hot-reload";
|
|
794
806
|
const args = ctx.args.trim();
|
|
795
807
|
const info = await getUpdateInfo();
|
|
796
808
|
const GITHUB_URL = "https://github.com/tencent-connect/openclaw-qqbot/";
|
|
@@ -834,7 +846,9 @@ registerCommand({
|
|
|
834
846
|
}
|
|
835
847
|
let isForce = false;
|
|
836
848
|
let isLatest = false;
|
|
849
|
+
let isLocal = false;
|
|
837
850
|
let versionArg;
|
|
851
|
+
let pkgArg;
|
|
838
852
|
const tokens = args ? args.split(/\s+/).filter(Boolean) : [];
|
|
839
853
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
840
854
|
const t = tokens[i];
|
|
@@ -846,6 +860,27 @@ registerCommand({
|
|
|
846
860
|
isLatest = true;
|
|
847
861
|
continue;
|
|
848
862
|
}
|
|
863
|
+
if (t === "--local") {
|
|
864
|
+
isLocal = true;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (t === "--pkg") {
|
|
868
|
+
const next = tokens[i + 1];
|
|
869
|
+
if (!next || next.startsWith("--")) {
|
|
870
|
+
return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
|
|
871
|
+
}
|
|
872
|
+
pkgArg = next;
|
|
873
|
+
i += 1;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (t.startsWith("--pkg=")) {
|
|
877
|
+
const v = t.slice("--pkg=".length).trim();
|
|
878
|
+
if (!v) {
|
|
879
|
+
return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
|
|
880
|
+
}
|
|
881
|
+
pkgArg = v;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
849
884
|
if (t === "--version") {
|
|
850
885
|
const next = tokens[i + 1];
|
|
851
886
|
if (!next || next.startsWith("--")) {
|
|
@@ -904,9 +939,17 @@ registerCommand({
|
|
|
904
939
|
`🌟官方 GitHub 仓库:[点击前往](${GITHUB_URL})`,
|
|
905
940
|
].join("\n");
|
|
906
941
|
}
|
|
942
|
+
// 解析 npm 包名:--pkg 参数 > 配置项 upgradePkg > 默认
|
|
943
|
+
// 支持 "scope/name"(自动补 @)和 "@scope/name" 两种格式
|
|
944
|
+
let upgradePkg = pkgArg || ctx.accountConfig?.upgradePkg;
|
|
945
|
+
if (upgradePkg) {
|
|
946
|
+
upgradePkg = upgradePkg.trim();
|
|
947
|
+
if (!upgradePkg.startsWith("@"))
|
|
948
|
+
upgradePkg = `@${upgradePkg}`;
|
|
949
|
+
}
|
|
907
950
|
// ── --version 指定版本:先校验版本号是否存在 ──
|
|
908
951
|
if (versionArg) {
|
|
909
|
-
const exists = await checkVersionExists(versionArg);
|
|
952
|
+
const exists = await checkVersionExists(versionArg, upgradePkg);
|
|
910
953
|
if (!exists) {
|
|
911
954
|
return `❌ 版本 ${versionArg} 不存在,请检查版本号`;
|
|
912
955
|
}
|
|
@@ -951,7 +994,7 @@ registerCommand({
|
|
|
951
994
|
// 热更新前保存凭证快照,防止更新过程被打断导致 appId/secret 丢失
|
|
952
995
|
preUpgradeCredentialBackup(ctx.accountId, ctx.appId);
|
|
953
996
|
// 异步执行升级
|
|
954
|
-
const startResult = fireHotUpgrade(targetVersion);
|
|
997
|
+
const startResult = fireHotUpgrade(targetVersion, upgradePkg, isLocal);
|
|
955
998
|
if (!startResult.ok) {
|
|
956
999
|
_upgrading = false;
|
|
957
1000
|
if (startResult.reason === "no-script") {
|
package/dist/src/tools/remind.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRequestTarget } from "../request-context.js";
|
|
1
|
+
import { getRequestTarget, getRequestAccountId } from "../request-context.js";
|
|
2
2
|
// ========== JSON Schema ==========
|
|
3
3
|
const RemindSchema = {
|
|
4
4
|
type: "object",
|
|
@@ -100,7 +100,7 @@ function generateJobName(content) {
|
|
|
100
100
|
/**
|
|
101
101
|
* 构建一次性提醒的 cron 工具参数
|
|
102
102
|
*/
|
|
103
|
-
function buildOnceJob(params, delayMs, to) {
|
|
103
|
+
function buildOnceJob(params, delayMs, to, accountId) {
|
|
104
104
|
const atMs = Date.now() + delayMs;
|
|
105
105
|
const content = params.content;
|
|
106
106
|
const name = params.name || generateJobName(content);
|
|
@@ -115,9 +115,12 @@ function buildOnceJob(params, delayMs, to) {
|
|
|
115
115
|
payload: {
|
|
116
116
|
kind: "agentTurn",
|
|
117
117
|
message: buildReminderPrompt(content),
|
|
118
|
-
|
|
118
|
+
},
|
|
119
|
+
delivery: {
|
|
120
|
+
mode: "announce",
|
|
119
121
|
channel: "qqbot",
|
|
120
122
|
to,
|
|
123
|
+
accountId,
|
|
121
124
|
},
|
|
122
125
|
},
|
|
123
126
|
};
|
|
@@ -125,7 +128,7 @@ function buildOnceJob(params, delayMs, to) {
|
|
|
125
128
|
/**
|
|
126
129
|
* 构建周期提醒的 cron 工具参数
|
|
127
130
|
*/
|
|
128
|
-
function buildCronJob(params, to) {
|
|
131
|
+
function buildCronJob(params, to, accountId) {
|
|
129
132
|
const content = params.content;
|
|
130
133
|
const name = params.name || generateJobName(content);
|
|
131
134
|
const tz = params.timezone || "Asia/Shanghai";
|
|
@@ -139,9 +142,12 @@ function buildCronJob(params, to) {
|
|
|
139
142
|
payload: {
|
|
140
143
|
kind: "agentTurn",
|
|
141
144
|
message: buildReminderPrompt(content),
|
|
142
|
-
|
|
145
|
+
},
|
|
146
|
+
delivery: {
|
|
147
|
+
mode: "announce",
|
|
143
148
|
channel: "qqbot",
|
|
144
149
|
to,
|
|
150
|
+
accountId,
|
|
145
151
|
},
|
|
146
152
|
},
|
|
147
153
|
};
|
|
@@ -211,15 +217,17 @@ export function registerRemindTool(api) {
|
|
|
211
217
|
if (!resolvedTo) {
|
|
212
218
|
return json({ error: "action=add 时无法确定投递目标地址。请稍后重试。" });
|
|
213
219
|
}
|
|
220
|
+
// 从请求上下文获取当前账户 ID(多账户场景),fallback 到 "default"
|
|
221
|
+
const resolvedAccountId = getRequestAccountId() || "default";
|
|
214
222
|
if (!p.time) {
|
|
215
223
|
return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
|
|
216
224
|
}
|
|
217
225
|
// 判断是 cron 表达式还是相对时间
|
|
218
226
|
if (isCronExpression(p.time)) {
|
|
219
227
|
// 周期提醒
|
|
220
|
-
const cronJob = buildCronJob(p, resolvedTo);
|
|
228
|
+
const cronJob = buildCronJob(p, resolvedTo, resolvedAccountId);
|
|
221
229
|
return json({
|
|
222
|
-
_instruction: "请立即使用 cron
|
|
230
|
+
_instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
|
|
223
231
|
cronParams: cronJob,
|
|
224
232
|
summary: `⏰ 周期提醒: "${p.content}" (${p.time}, tz=${p.timezone || "Asia/Shanghai"})`,
|
|
225
233
|
});
|
|
@@ -236,9 +244,9 @@ export function registerRemindTool(api) {
|
|
|
236
244
|
if (delayMs < 30_000) {
|
|
237
245
|
return json({ error: "提醒时间不能少于 30 秒" });
|
|
238
246
|
}
|
|
239
|
-
const onceJob = buildOnceJob(p, delayMs, resolvedTo);
|
|
247
|
+
const onceJob = buildOnceJob(p, delayMs, resolvedTo, resolvedAccountId);
|
|
240
248
|
return json({
|
|
241
|
-
_instruction: "请立即使用 cron
|
|
249
|
+
_instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
|
|
242
250
|
cronParams: onceJob,
|
|
243
251
|
summary: `⏰ ${formatDelay(delayMs)}后提醒: "${p.content}"`,
|
|
244
252
|
});
|
package/dist/src/types.d.ts
CHANGED
|
@@ -92,10 +92,17 @@ export interface QQBotAccountConfig {
|
|
|
92
92
|
upgradeUrl?: string;
|
|
93
93
|
/**
|
|
94
94
|
* /bot-upgrade 指令的行为模式
|
|
95
|
-
* - "doc"
|
|
96
|
-
* - "hot-reload":检测到新版本时直接执行 npm
|
|
95
|
+
* - "doc":展示升级文档链接(安全模式)
|
|
96
|
+
* - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新(默认)
|
|
97
97
|
*/
|
|
98
98
|
upgradeMode?: "doc" | "hot-reload";
|
|
99
|
+
/**
|
|
100
|
+
* /bot-upgrade 热更新时使用的 npm 包名
|
|
101
|
+
* 支持 "scope/name"(自动补 @)或 "@scope/name" 格式
|
|
102
|
+
* 默认: "@tencent-connect/openclaw-qqbot"
|
|
103
|
+
* 示例: "ryantest/openclaw-qqbot"
|
|
104
|
+
*/
|
|
105
|
+
upgradePkg?: string;
|
|
99
106
|
/**
|
|
100
107
|
* 出站消息合并回复(debounce)配置
|
|
101
108
|
* 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
|
|
@@ -30,5 +30,7 @@ export declare function getUpdateInfo(): Promise<UpdateInfo>;
|
|
|
30
30
|
/**
|
|
31
31
|
* 检查指定版本是否存在于 npm registry
|
|
32
32
|
* 用于 /bot-upgrade --version 的前置校验
|
|
33
|
+
* @param version 要检查的版本号
|
|
34
|
+
* @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
|
|
33
35
|
*/
|
|
34
|
-
export declare function checkVersionExists(version: string): Promise<boolean>;
|
|
36
|
+
export declare function checkVersionExists(version: string, pkgName?: string): Promise<boolean>;
|
|
@@ -99,9 +99,12 @@ export async function getUpdateInfo() {
|
|
|
99
99
|
/**
|
|
100
100
|
* 检查指定版本是否存在于 npm registry
|
|
101
101
|
* 用于 /bot-upgrade --version 的前置校验
|
|
102
|
+
* @param version 要检查的版本号
|
|
103
|
+
* @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
|
|
102
104
|
*/
|
|
103
|
-
export async function checkVersionExists(version) {
|
|
104
|
-
|
|
105
|
+
export async function checkVersionExists(version, pkgName) {
|
|
106
|
+
const registries = pkgName ? buildRegistries(pkgName) : REGISTRIES;
|
|
107
|
+
for (const baseUrl of registries) {
|
|
105
108
|
try {
|
|
106
109
|
const url = `${baseUrl}/${version}`;
|
|
107
110
|
const json = await fetchJson(url, 10_000);
|
|
@@ -114,6 +117,14 @@ export async function checkVersionExists(version) {
|
|
|
114
117
|
}
|
|
115
118
|
return false;
|
|
116
119
|
}
|
|
120
|
+
/** 根据自定义包名构建 registry URL 列表 */
|
|
121
|
+
function buildRegistries(pkgName) {
|
|
122
|
+
const encoded = encodeURIComponent(pkgName);
|
|
123
|
+
return [
|
|
124
|
+
`https://registry.npmjs.org/${encoded}`,
|
|
125
|
+
`https://registry.npmmirror.com/${encoded}`,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
117
128
|
function compareVersions(a, b) {
|
|
118
129
|
const parse = (v) => {
|
|
119
130
|
const clean = v.replace(/^v/, "");
|