@tencent-connect/openclaw-qqbot 1.6.4-alpha.11 → 1.6.4-alpha.13

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 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.2`
13
+ ### 🚀 Current Version: `v1.6.4`
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/)
@@ -36,13 +36,13 @@ Scan to join the QQ group chat
36
36
 
37
37
  | Feature | Description |
38
38
  |---------|-------------|
39
- | 🔒 **Multi-Scene** | C2C private chat, group @messages, channel messages, channel DMs |
39
+ | 🔒 **Multi-Scene** | C2C private chat, group @messages |
40
40
  | 🖼️ **Rich Media** | Send & receive images, voice, video, and files |
41
41
  | 🎙️ **Voice (STT/TTS)** | Speech-to-text transcription & text-to-speech replies |
42
+ | 🔥 **One-Click Hot Upgrade** | Send `/bot-upgrade` in private chat to upgrade — no server login needed |
42
43
  | ⏰ **Scheduled Push** | Proactive message delivery via scheduled tasks |
43
44
  | 🔗 **URL Support** | Direct URL sending in private chat (no restrictions) |
44
45
  | ⌨️ **Typing Indicator** | "Bot is typing..." status shown in real-time |
45
- | 🔄 **Hot Reload** | Install via npm with seamless hot updates |
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 |
@@ -177,15 +177,24 @@ Shows framework version, plugin version, and a direct link to the official repos
177
177
 
178
178
  <img width="360" src="docs/images/slash-help.jpg" alt="Help Demo" />
179
179
 
180
- #### `/bot-upgrade` — Upgrade Guide
180
+ #### `/bot-upgrade` — One-Click Hot Upgrade
181
181
 
182
182
  > **You**: `/bot-upgrade`
183
183
  >
184
- > **QQBot**: 📌 Current version / ✅ Up to date / ⬆️ Upgrade guide / 🌟 GitHub repo
184
+ > **QQBot**: 📌 Current: v1.6.4-alpha.12 / ✅ New version v1.6.4 available / Click button below to confirm
185
185
 
186
- Shows current version, update status, upgrade guide link, and official repository.
186
+ Send in private chat to upgrade the plugin without server login. Supported usage:
187
187
 
188
- <img width="360" src="docs/images/slash-upgrade.jpg" alt="Upgrade Demo" />
188
+ | Command | Description |
189
+ |---------|-------------|
190
+ | `/bot-upgrade` | Check for updates, show confirmation button |
191
+ | `/bot-upgrade --latest` | Confirm upgrade to the latest version |
192
+ | `/bot-upgrade --version 1.6.4` | Upgrade to a specific version |
193
+ | `/bot-upgrade --force` | Force reinstall current version |
194
+
195
+ Credentials are automatically backed up before upgrade. Version existence is verified against npm before proceeding. Auto-recovery on failure.
196
+
197
+ <!-- TODO: add /bot-upgrade screenshot -->
189
198
 
190
199
  #### `/bot-logs` — Log Export
191
200
 
@@ -197,6 +206,14 @@ Exports the last ~2000 lines of gateway logs as a file for quick troubleshooting
197
206
 
198
207
  <img width="360" src="docs/images/slash-logs.jpg" alt="Logs Demo" />
199
208
 
209
+ #### Usage Help
210
+
211
+ All commands support a `?` suffix to show usage:
212
+
213
+ > **You**: `/bot-upgrade ?`
214
+ >
215
+ > **QQBot**: 📖 /bot-upgrade usage: …
216
+
200
217
  ---
201
218
 
202
219
  ## 🚀 Getting Started
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  **让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
11
11
 
12
- ### 🚀 当前版本: `v1.6.2`
12
+ ### 🚀 当前版本: `v1.6.4`
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/)
@@ -31,13 +31,13 @@
31
31
 
32
32
  | 功能 | 说明 |
33
33
  |------|------|
34
- | 🔒 **多场景支持** | C2C 私聊、群聊 @消息、频道消息、频道私信 |
34
+ | 🔒 **多场景支持** | C2C 私聊、群聊 @消息 |
35
35
  | 🖼️ **富媒体消息** | 支持图片、语音、视频、文件的收发 |
36
36
  | 🎙️ **语音能力 (STT/TTS)** | 语音转文字自动转录 & 文字转语音回复 |
37
+ | 🔥 **一键热更新** | 私聊发送 `/bot-upgrade` 即可完成版本升级,无需登录服务器 |
37
38
  | ⏰ **定时推送** | 支持定时任务触发后主动推送消息 |
38
39
  | 🔗 **URL 无限制** | 私聊可直接发送 URL |
39
40
  | ⌨️ **输入状态** | 实时显示"Bot 正在输入中…"状态 |
40
- | 🔄 **热更新** | 支持 npm 方式安装和无缝热更新 |
41
41
  | 📝 **Markdown** | 完整支持 Markdown 格式消息 |
42
42
  | 🛠️ **原生命令** | 支持 OpenClaw 原生命令 |
43
43
  | 💬 **引用上下文** | 解析 QQ `REFIDX_*` 引用消息,并将引用内容注入 AI 上下文 |
@@ -172,15 +172,24 @@ AI 可直接发送视频,支持本地文件和公网 URL。
172
172
 
173
173
  <img width="360" src="docs/images/slash-help.jpg" alt="Help 演示" />
174
174
 
175
- #### `/bot-upgrade` — 升级指引
175
+ #### `/bot-upgrade` — 一键热更新
176
176
 
177
177
  > **你**:`/bot-upgrade`
178
178
  >
179
- > **QQBot**:📌当前版本 / ✅当前已是最新版本 / ⬆️升级指引 / 🌟官方 GitHub 仓库
179
+ > **QQBot**:📌当前版本 v1.6.4-alpha.12 / ✅发现新版本 v1.6.4 / 点击下方按钮确认升级
180
180
 
181
- 显示当前版本、更新状态、升级文档链接及官方仓库入口。
181
+ 在私聊中发送即可完成版本升级,全程无需登录服务器。支持的用法:
182
182
 
183
- <img width="360" src="docs/images/slash-upgrade.jpg" alt="Upgrade 演示" />
183
+ | 命令 | 说明 |
184
+ |------|------|
185
+ | `/bot-upgrade` | 检查是否有新版本,展示确认按钮 |
186
+ | `/bot-upgrade --latest` | 确认升级到最新版本 |
187
+ | `/bot-upgrade --version 1.6.4` | 升级到指定版本 |
188
+ | `/bot-upgrade --force` | 强制重新安装当前版本 |
189
+
190
+ 升级流程自动备份凭证,升级前校验版本是否存在于 npm,升级失败自动恢复。
191
+
192
+ <!-- TODO: 补充 /bot-upgrade 截图 -->
184
193
 
185
194
  #### `/bot-logs` — 日志导出
186
195
 
@@ -192,6 +201,14 @@ AI 可直接发送视频,支持本地文件和公网 URL。
192
201
 
193
202
  <img width="360" src="docs/images/slash-logs.jpg" alt="Logs 演示" />
194
203
 
204
+ #### 用法查询
205
+
206
+ 所有指令都支持 `?` 后缀查看用法说明:
207
+
208
+ > **你**:`/bot-upgrade ?`
209
+ >
210
+ > **QQBot**:📖 /bot-upgrade 用法:…
211
+
195
212
  ---
196
213
 
197
214
  ## 🚀 快速开始
@@ -59,7 +59,13 @@ export const qqbotPlugin = {
59
59
  accountId,
60
60
  clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
61
61
  }),
62
- isConfigured: (account) => Boolean(account?.appId && account?.clientSecret),
62
+ isConfigured: (account) => {
63
+ if (account?.appId && account?.clientSecret)
64
+ return true;
65
+ // 配置为空但有凭证备份时仍返回 true,让 startAccount 有机会恢复凭证
66
+ const backup = loadCredentialBackup(account?.accountId);
67
+ return backup !== null;
68
+ },
63
69
  describeAccount: (account) => ({
64
70
  accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
65
71
  name: account?.name,
@@ -54,6 +54,103 @@ function getFrameworkVersion() {
54
54
  _frameworkVersion = "unknown";
55
55
  return _frameworkVersion;
56
56
  }
57
+ // ============ 热更新兼容性检查 ============
58
+ /**
59
+ * 热更新可执行的环境要求:
60
+ * - 最低 OpenClaw 框架版本
61
+ * - 支持的操作系统
62
+ * - 最低 Node.js 版本
63
+ */
64
+ const UPGRADE_REQUIREMENTS = {
65
+ /** OpenClaw 最低版本(YYYY.M.D 格式,如 "2026.3.10") */
66
+ minFrameworkVersion: "2026.3.10",
67
+ /** 支持的操作系统列表(process.platform 值) */
68
+ supportedPlatforms: ["darwin", "linux", "win32"],
69
+ /** 最低 Node.js 版本 */
70
+ minNodeVersion: "18.0.0",
71
+ };
72
+ /**
73
+ * 解析框架版本字符串中的日期版本号
74
+ * 输入示例: "OpenClaw 2026.3.13 (61d171a)" → "2026.3.13"
75
+ */
76
+ function parseFrameworkDateVersion(versionStr) {
77
+ const m = versionStr.match(/(\d{4}\.\d{1,2}\.\d{1,2})/);
78
+ return m ? m[1] : null;
79
+ }
80
+ /**
81
+ * 比较 YYYY.M.D 格式的版本号
82
+ * @returns >0 if a > b, <0 if a < b, 0 if equal
83
+ */
84
+ function compareDateVersions(a, b) {
85
+ const pa = a.split(".").map(Number);
86
+ const pb = b.split(".").map(Number);
87
+ for (let i = 0; i < 3; i++) {
88
+ const diff = (pa[i] || 0) - (pb[i] || 0);
89
+ if (diff !== 0)
90
+ return diff;
91
+ }
92
+ return 0;
93
+ }
94
+ /**
95
+ * 比较 semver 版本号(简化版,仅比较 major.minor.patch)
96
+ */
97
+ function compareSemver(a, b) {
98
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
99
+ const pa = parse(a);
100
+ const pb = parse(b);
101
+ for (let i = 0; i < 3; i++) {
102
+ const diff = (pa[i] || 0) - (pb[i] || 0);
103
+ if (diff !== 0)
104
+ return diff;
105
+ }
106
+ return 0;
107
+ }
108
+ /**
109
+ * 检查当前环境是否满足热更新要求
110
+ */
111
+ function checkUpgradeCompatibility() {
112
+ const errors = [];
113
+ const req = UPGRADE_REQUIREMENTS;
114
+ // 1. 检查操作系统
115
+ const platform = process.platform;
116
+ if (!req.supportedPlatforms.includes(platform)) {
117
+ const supported = req.supportedPlatforms.map(p => {
118
+ if (p === "darwin")
119
+ return "macOS";
120
+ if (p === "linux")
121
+ return "Linux";
122
+ if (p === "win32")
123
+ return "Windows";
124
+ return p;
125
+ }).join("、");
126
+ const current = platform === "win32" ? "Windows"
127
+ : platform === "darwin" ? "macOS"
128
+ : platform;
129
+ errors.push(`❌ 当前操作系统 **${current}** 不支持热更新(支持:${supported})`);
130
+ }
131
+ // 2. 检查 OpenClaw 框架版本
132
+ const fwVersion = getFrameworkVersion();
133
+ if (fwVersion === "unknown") {
134
+ errors.push(`⚠️ 无法检测 OpenClaw 框架版本,热更新可能失败`);
135
+ }
136
+ else {
137
+ const dateVer = parseFrameworkDateVersion(fwVersion);
138
+ if (dateVer && compareDateVersions(dateVer, req.minFrameworkVersion) < 0) {
139
+ errors.push(`❌ OpenClaw 框架版本过低:当前 **${dateVer}**,热更新要求最低 **${req.minFrameworkVersion}**。请先升级框架:\`openclaw upgrade\``);
140
+ }
141
+ }
142
+ // 3. 检查 Node.js 版本
143
+ const nodeVer = process.version.replace(/^v/, "");
144
+ if (compareSemver(nodeVer, req.minNodeVersion) < 0) {
145
+ errors.push(`❌ Node.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
146
+ }
147
+ // 4. 检查系统架构(arm 等特殊架构提示)
148
+ const arch = process.arch;
149
+ if (arch !== "x64" && arch !== "arm64") {
150
+ errors.push(`⚠️ 当前 CPU 架构 **${arch}** 未经充分测试,热更新可能存在兼容性问题`);
151
+ }
152
+ return { ok: errors.length === 0, errors };
153
+ }
57
154
  // ============ 指令注册表 ============
58
155
  const commands = new Map();
59
156
  function registerCommand(cmd) {
@@ -103,13 +200,13 @@ registerCommand({
103
200
  `查看当前 QQBot 插件版本和 OpenClaw 框架版本。`,
104
201
  `同时检查是否有新版本可用。`,
105
202
  ].join("\n"),
106
- handler: () => {
203
+ handler: async () => {
107
204
  const frameworkVersion = getFrameworkVersion();
108
205
  const lines = [
109
206
  `🦞框架版本:${frameworkVersion}`,
110
207
  `🤖QQBot 插件版本:v${PLUGIN_VERSION}`,
111
208
  ];
112
- const info = getUpdateInfo();
209
+ const info = await getUpdateInfo();
113
210
  if (info.checkedAt === 0) {
114
211
  lines.push(`⏳ 版本检查中...`);
115
212
  }
@@ -180,18 +277,20 @@ function findCli() {
180
277
  }
181
278
  /**
182
279
  * 找到升级脚本路径(兼容源码运行、dist 运行、已安装扩展目录)
280
+ * Windows 优先查找 .ps1,Mac/Linux 查找 .sh
183
281
  */
184
282
  function getUpgradeScriptPath() {
185
283
  const currentFile = fileURLToPath(import.meta.url);
186
284
  const currentDir = path.dirname(currentFile);
285
+ const scriptName = isWindows() ? "upgrade-via-npm.ps1" : "upgrade-via-npm.sh";
187
286
  const candidates = [
188
- path.resolve(currentDir, "..", "..", "scripts", "upgrade-via-npm.sh"),
189
- path.resolve(currentDir, "..", "scripts", "upgrade-via-npm.sh"),
190
- path.resolve(process.cwd(), "scripts", "upgrade-via-npm.sh"),
287
+ path.resolve(currentDir, "..", "..", "scripts", scriptName),
288
+ path.resolve(currentDir, "..", "scripts", scriptName),
289
+ path.resolve(process.cwd(), "scripts", scriptName),
191
290
  ];
192
291
  const homeDir = getHomeDir();
193
292
  for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
194
- candidates.push(path.join(homeDir, `.${cli}`, "extensions", "openclaw-qqbot", "scripts", "upgrade-via-npm.sh"));
293
+ candidates.push(path.join(homeDir, `.${cli}`, "extensions", "openclaw-qqbot", "scripts", scriptName));
195
294
  }
196
295
  for (const p of candidates) {
197
296
  if (fs.existsSync(p))
@@ -201,6 +300,7 @@ function getUpgradeScriptPath() {
201
300
  }
202
301
  /**
203
302
  * 在 Windows 上查找可用的 bash(Git Bash / WSL 等)
303
+ * 仅作为 Windows 上的 fallback(优先使用 PowerShell)
204
304
  */
205
305
  function findBash() {
206
306
  if (!isWindows())
@@ -227,6 +327,8 @@ function findBash() {
227
327
  /**
228
328
  * 将 openclaw.json 中的 qqbot 插件 source 从 "path" 切换为 "npm"。
229
329
  * 用于热更新场景:从 npm 拉取新版本后,确保 openclaw 不再从本地源码加载。
330
+ *
331
+ * 安全保障:写回配置前验证 channels.qqbot 未丢失,防止竞态写入导致凭证消失。
230
332
  */
231
333
  function switchPluginSourceToNpm() {
232
334
  try {
@@ -235,13 +337,26 @@ function switchPluginSourceToNpm() {
235
337
  const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
236
338
  if (!fs.existsSync(cfgPath))
237
339
  continue;
238
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
340
+ // 读取当前配置
341
+ const raw = fs.readFileSync(cfgPath, "utf8");
342
+ const cfg = JSON.parse(raw);
239
343
  const inst = cfg?.plugins?.installs?.["openclaw-qqbot"];
240
- if (inst && inst.source !== "npm") {
241
- inst.source = "npm";
242
- delete inst.sourcePath;
243
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 4) + "\n");
344
+ if (!inst || inst.source === "npm") {
345
+ break; // 无需修改
244
346
  }
347
+ // 记录修改前的 channels.qqbot 快照,用于写后校验
348
+ const channelsBefore = JSON.stringify(cfg.channels?.qqbot ?? null);
349
+ inst.source = "npm";
350
+ delete inst.sourcePath;
351
+ const newRaw = JSON.stringify(cfg, null, 4) + "\n";
352
+ // 写后校验:重新解析确认 channels.qqbot 未被破坏
353
+ const verify = JSON.parse(newRaw);
354
+ const channelsAfter = JSON.stringify(verify.channels?.qqbot ?? null);
355
+ if (channelsBefore !== channelsAfter) {
356
+ // channels 数据异常,放弃写入
357
+ break;
358
+ }
359
+ fs.writeFileSync(cfgPath, newRaw);
245
360
  break;
246
361
  }
247
362
  }
@@ -285,17 +400,31 @@ function preUpgradeCredentialBackup(accountId, appId) {
285
400
  // 非关键操作,静默忽略
286
401
  }
287
402
  }
403
+ /**
404
+ * 在 Windows 上查找 PowerShell(pwsh 优先,powershell.exe 兜底)
405
+ */
406
+ function findPowerShell() {
407
+ // pwsh = PowerShell 7+(跨平台),powershell.exe = Windows 内置 5.1
408
+ for (const ps of ["pwsh", "powershell"]) {
409
+ try {
410
+ execFileSync("where", [ps], { timeout: 3000, encoding: "utf8", stdio: "pipe" });
411
+ return ps;
412
+ }
413
+ catch {
414
+ continue;
415
+ }
416
+ }
417
+ return null;
418
+ }
288
419
  /**
289
420
  * 执行热更新:执行脚本(--no-restart) → 立即触发 gateway restart
290
421
  *
291
422
  * fire-and-forget 操作:
292
- * - 异步执行升级脚本(--no-restart,只做文件替换)
423
+ * - 异步执行升级脚本(--no-restart / -NoRestart,只做文件替换)
293
424
  * - 脚本完成后**立即**触发 gateway restart(当前进程会被杀掉)
294
425
  * - 新进程启动时 getStartupGreeting() 检测到版本变更,自动通知管理员
295
426
  *
296
- * 注意:gateway restart 必须在文件替换完成后尽快执行,
297
- * 否则 openclaw 的配置热加载轮询(~1s)会不断检测到插件目录
298
- * 已变更但进程未重启,从而产生 "plugin not found" warning 刷屏。
427
+ * Windows 使用 PowerShell 执行 .ps1 脚本,Mac/Linux 使用 bash 执行 .sh 脚本。
299
428
  */
300
429
  function fireHotUpgrade(targetVersion) {
301
430
  const scriptPath = getUpgradeScriptPath();
@@ -304,11 +433,31 @@ function fireHotUpgrade(targetVersion) {
304
433
  const cli = findCli();
305
434
  if (!cli)
306
435
  return { ok: false, reason: "no-cli" };
307
- const bash = findBash();
308
- if (!bash)
309
- return { ok: false, reason: "no-bash" };
436
+ let shell;
437
+ let shellArgs;
438
+ if (isWindows()) {
439
+ // Windows: PowerShell 执行 .ps1
440
+ const ps = findPowerShell();
441
+ if (!ps)
442
+ return { ok: false, reason: "no-powershell" };
443
+ shell = ps;
444
+ shellArgs = [
445
+ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
446
+ "-File", scriptPath,
447
+ "-NoRestart",
448
+ ...(targetVersion ? ["-Version", targetVersion] : []),
449
+ ];
450
+ }
451
+ else {
452
+ // Mac / Linux: bash 执行 .sh
453
+ const bash = findBash();
454
+ if (!bash)
455
+ return { ok: false, reason: "no-bash" };
456
+ shell = bash;
457
+ shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
458
+ }
310
459
  // 异步执行升级脚本
311
- execFile(bash, [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])], {
460
+ execFile(shell, shellArgs, {
312
461
  timeout: 120_000,
313
462
  env: { ...process.env },
314
463
  ...(isWindows() ? { windowsHide: true } : {}),
@@ -325,10 +474,8 @@ function fireHotUpgrade(targetVersion) {
325
474
  }
326
475
  // 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
327
476
  // 确保新进程启动时读到的是 npm source,不会被本地源码覆盖。
328
- // 必须在 restart 之前同步完成,避免 openclaw 轮询检测到配置变更后
329
- // 先于我们的 restart 触发非预期的 reload。
330
477
  switchPluginSourceToNpm();
331
- // 文件替换成功,立即触发 gateway restart(不再等后续步骤)
478
+ // 文件替换成功,立即触发 gateway restart
332
479
  execFile(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
333
480
  if (restartErr) {
334
481
  // restart 失败,尝试 stop + start 作为 fallback
@@ -362,6 +509,11 @@ registerCommand({
362
509
  `/bot-upgrade --force 强制重新安装当前版本`,
363
510
  ``,
364
511
  `⚠️ 仅在私聊中可用。升级过程约 30~60 秒,期间服务短暂不可用。`,
512
+ ``,
513
+ `环境要求:`,
514
+ ` - 操作系统:macOS / Linux / Windows`,
515
+ ` - OpenClaw 框架版本 ≥ ${UPGRADE_REQUIREMENTS.minFrameworkVersion}`,
516
+ ` - Node.js ≥ v${UPGRADE_REQUIREMENTS.minNodeVersion}`,
365
517
  ].join("\n"),
366
518
  handler: async (ctx) => {
367
519
  // 升级相关指令仅在私聊中可用
@@ -374,7 +526,7 @@ registerCommand({
374
526
  }
375
527
  const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
376
528
  const args = ctx.args.trim();
377
- const info = getUpdateInfo();
529
+ const info = await getUpdateInfo();
378
530
  let isForce = false;
379
531
  let isLatest = false;
380
532
  let versionArg;
@@ -475,6 +627,17 @@ registerCommand({
475
627
  }
476
628
  }
477
629
  const targetVersion = versionArg || info.latest || undefined;
630
+ // ── 环境兼容性检查 ──
631
+ const compat = checkUpgradeCompatibility();
632
+ if (!compat.ok) {
633
+ return [
634
+ `🚫 当前环境不满足热更新要求:`,
635
+ ``,
636
+ ...compat.errors,
637
+ ``,
638
+ `查看手动升级指引:[点击查看](${url})`,
639
+ ].join("\n");
640
+ }
478
641
  // 加锁
479
642
  _upgrading = true;
480
643
  // 热更新前保存凭证快照,防止更新过程被打断导致 appId/secret 丢失
@@ -497,10 +660,17 @@ registerCommand({
497
660
  `查看手动升级指引:[点击查看](${url})`,
498
661
  ].join("\n");
499
662
  }
663
+ if (startResult.reason === "no-powershell") {
664
+ return [
665
+ `❌ 未找到 PowerShell,无法执行热更新`,
666
+ ``,
667
+ `请确认系统中已安装 PowerShell(Windows 10+ 自带)`,
668
+ `查看手动升级指引:[点击查看](${url})`,
669
+ ].join("\n");
670
+ }
500
671
  return [
501
672
  `❌ 当前环境不支持热更新(需要 bash)`,
502
673
  ``,
503
- `Windows 用户请安装 Git for Windows 后重试`,
504
674
  `查看手动升级指引:[点击查看](${url})`,
505
675
  ].join("\n");
506
676
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 后台版本检查器
2
+ * 版本检查器
3
3
  *
4
- * - triggerUpdateCheck(): gateway 启动时调用,后台检查 npm registry 是否有新版本
5
- * - getUpdateInfo(): 返回上次检查结果(供 /bot-version、/bot-help 指令使用)
4
+ * - triggerUpdateCheck(): gateway 启动时调用,后台预热缓存
5
+ * - getUpdateInfo(): 每次实时查询 npm registry,返回最新结果
6
6
  *
7
7
  * 使用 HTTPS 直接请求 npm registry API(不依赖 npm CLI),
8
8
  * 支持多 registry fallback:npmjs.org → npmmirror.com,解决国内网络问题。
@@ -14,12 +14,14 @@ export interface UpdateInfo {
14
14
  checkedAt: number;
15
15
  error?: string;
16
16
  }
17
+ /** gateway 启动时调用,保存 log 引用 */
17
18
  export declare function triggerUpdateCheck(log?: {
18
19
  info: (msg: string) => void;
19
20
  error: (msg: string) => void;
20
21
  debug?: (msg: string) => void;
21
22
  }): void;
22
- export declare function getUpdateInfo(): UpdateInfo;
23
+ /** 每次实时查询 npm registry */
24
+ export declare function getUpdateInfo(): Promise<UpdateInfo>;
23
25
  /**
24
26
  * 检查指定版本是否存在于 npm registry
25
27
  * 用于 /bot-upgrade --version 的前置校验
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 后台版本检查器
2
+ * 版本检查器
3
3
  *
4
- * - triggerUpdateCheck(): gateway 启动时调用,后台检查 npm registry 是否有新版本
5
- * - getUpdateInfo(): 返回上次检查结果(供 /bot-version、/bot-help 指令使用)
4
+ * - triggerUpdateCheck(): gateway 启动时调用,后台预热缓存
5
+ * - getUpdateInfo(): 每次实时查询 npm registry,返回最新结果
6
6
  *
7
7
  * 使用 HTTPS 直接请求 npm registry API(不依赖 npm CLI),
8
8
  * 支持多 registry fallback:npmjs.org → npmmirror.com,解决国内网络问题。
@@ -24,13 +24,7 @@ try {
24
24
  catch {
25
25
  // fallback
26
26
  }
27
- let _lastInfo = {
28
- current: CURRENT_VERSION,
29
- latest: null,
30
- hasUpdate: false,
31
- checkedAt: 0,
32
- };
33
- let _checking = false;
27
+ let _log;
34
28
  function fetchJson(url, timeoutMs) {
35
29
  return new Promise((resolve, reject) => {
36
30
  const req = https.get(url, { timeout: timeoutMs, headers: { Accept: "application/json" } }, (res) => {
@@ -54,7 +48,7 @@ function fetchJson(url, timeoutMs) {
54
48
  req.on("timeout", () => { req.destroy(); reject(new Error(`timeout fetching ${url}`)); });
55
49
  });
56
50
  }
57
- async function fetchDistTags(log) {
51
+ async function fetchDistTags() {
58
52
  for (const url of REGISTRIES) {
59
53
  try {
60
54
  const json = await fetchJson(url, 10_000);
@@ -63,43 +57,42 @@ async function fetchDistTags(log) {
63
57
  return tags;
64
58
  }
65
59
  catch (e) {
66
- log?.debug?.(`[qqbot:update-checker] ${url} failed: ${e.message}`);
60
+ _log?.debug?.(`[qqbot:update-checker] ${url} failed: ${e.message}`);
67
61
  }
68
62
  }
69
63
  throw new Error("all registries failed");
70
64
  }
65
+ function buildUpdateInfo(tags) {
66
+ const currentIsPrerelease = CURRENT_VERSION.includes("-");
67
+ const compareTarget = currentIsPrerelease
68
+ ? (tags.alpha || tags.latest || null)
69
+ : (tags.latest || null);
70
+ const hasUpdate = typeof compareTarget === "string"
71
+ && compareTarget !== CURRENT_VERSION
72
+ && compareVersions(compareTarget, CURRENT_VERSION) > 0;
73
+ return { current: CURRENT_VERSION, latest: compareTarget, hasUpdate, checkedAt: Date.now() };
74
+ }
75
+ /** gateway 启动时调用,保存 log 引用 */
71
76
  export function triggerUpdateCheck(log) {
72
- if (_checking)
73
- return;
74
- const INTERVAL_MS = 1 * 60 * 1000;
75
- if (_lastInfo.checkedAt > 0 && Date.now() - _lastInfo.checkedAt < INTERVAL_MS) {
76
- return;
77
- }
78
- _checking = true;
79
- log?.debug?.(`[qqbot:update-checker] checking (current: ${CURRENT_VERSION})...`);
80
- fetchDistTags(log).then((tags) => {
81
- const now = Date.now();
82
- const currentIsPrerelease = CURRENT_VERSION.includes("-");
83
- const compareTarget = currentIsPrerelease
84
- ? (tags.alpha || tags.latest || null)
85
- : (tags.latest || null);
86
- const hasUpdate = typeof compareTarget === "string"
87
- && compareTarget !== CURRENT_VERSION
88
- && compareVersions(compareTarget, CURRENT_VERSION) > 0;
89
- _lastInfo = { current: CURRENT_VERSION, latest: compareTarget, hasUpdate, checkedAt: now };
90
- if (hasUpdate) {
91
- log?.info?.(`[qqbot:update-checker] new version available: ${compareTarget} (current: ${CURRENT_VERSION})`);
77
+ if (log)
78
+ _log = log;
79
+ // 预热:fire-and-forget
80
+ getUpdateInfo().then((info) => {
81
+ if (info.hasUpdate) {
82
+ _log?.info?.(`[qqbot:update-checker] new version available: ${info.latest} (current: ${CURRENT_VERSION})`);
92
83
  }
93
- }).catch((err) => {
94
- const now = Date.now();
95
- log?.debug?.(`[qqbot:update-checker] check failed: ${err.message}`);
96
- _lastInfo = { current: CURRENT_VERSION, latest: null, hasUpdate: false, checkedAt: now, error: err.message };
97
- }).finally(() => {
98
- _checking = false;
99
- });
84
+ }).catch(() => { });
100
85
  }
101
- export function getUpdateInfo() {
102
- return { ..._lastInfo };
86
+ /** 每次实时查询 npm registry */
87
+ export async function getUpdateInfo() {
88
+ try {
89
+ const tags = await fetchDistTags();
90
+ return buildUpdateInfo(tags);
91
+ }
92
+ catch (err) {
93
+ _log?.debug?.(`[qqbot:update-checker] check failed: ${err.message}`);
94
+ return { current: CURRENT_VERSION, latest: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
95
+ }
103
96
  }
104
97
  /**
105
98
  * 检查指定版本是否存在于 npm registry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.4-alpha.11",
3
+ "version": "1.6.4-alpha.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",