@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 +24 -7
- package/README.zh.md +24 -7
- package/dist/src/channel.js +7 -1
- package/dist/src/slash-commands.js +194 -24
- package/dist/src/update-checker.d.ts +6 -4
- package/dist/src/update-checker.js +34 -41
- package/package.json +1 -1
- package/scripts/upgrade-via-alt-pkg.sh +307 -0
- package/scripts/upgrade-via-npm.ps1 +287 -0
- package/src/channel.ts +6 -1
- package/src/slash-commands.ts +209 -24
- package/src/update-checker.ts +33 -42
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.4`
|
|
14
14
|
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](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
|
|
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
|
|
180
|
+
#### `/bot-upgrade` — One-Click Hot Upgrade
|
|
181
181
|
|
|
182
182
|
> **You**: `/bot-upgrade`
|
|
183
183
|
>
|
|
184
|
-
> **QQBot**: 📌 Current
|
|
184
|
+
> **QQBot**: 📌 Current: v1.6.4-alpha.12 / ✅ New version v1.6.4 available / Click button below to confirm
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
Send in private chat to upgrade the plugin without server login. Supported usage:
|
|
187
187
|
|
|
188
|
-
|
|
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.
|
|
12
|
+
### 🚀 当前版本: `v1.6.4`
|
|
13
13
|
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](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**:📌当前版本 /
|
|
179
|
+
> **QQBot**:📌当前版本 v1.6.4-alpha.12 / ✅发现新版本 v1.6.4 / 点击下方按钮确认升级
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
在私聊中发送即可完成版本升级,全程无需登录服务器。支持的用法:
|
|
182
182
|
|
|
183
|
-
|
|
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
|
## 🚀 快速开始
|
package/dist/src/channel.js
CHANGED
|
@@ -59,7 +59,13 @@ export const qqbotPlugin = {
|
|
|
59
59
|
accountId,
|
|
60
60
|
clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
|
|
61
61
|
}),
|
|
62
|
-
isConfigured: (account) =>
|
|
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",
|
|
189
|
-
path.resolve(currentDir, "..", "scripts",
|
|
190
|
-
path.resolve(process.cwd(), "scripts",
|
|
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",
|
|
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
|
-
|
|
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
|
|
241
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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(
|
|
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
|
|
5
|
-
* - getUpdateInfo():
|
|
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
|
-
|
|
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
|
|
5
|
-
* - getUpdateInfo():
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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((
|
|
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
|
-
|
|
102
|
-
|
|
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
|