@tencent-connect/openclaw-qqbot 1.6.0-alpha.1 → 1.6.0-alpha.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/dist/src/gateway.js +143 -47
- package/dist/src/slash-commands.d.ts +9 -1
- package/dist/src/slash-commands.js +144 -122
- package/dist/src/types.d.ts +1 -1
- package/dist/src/update-checker.d.ts +21 -0
- package/dist/src/update-checker.js +116 -0
- package/dist/src/utils/media-tags.js +30 -2
- package/package.json +1 -1
- package/scripts/upgrade-via-source.sh +114 -71
- package/skills/qqbot-media/SKILL.md +1 -1
- package/src/gateway.ts +148 -43
- package/src/slash-commands.ts +152 -128
- package/src/types.ts +1 -1
- package/src/update-checker.ts +132 -0
- package/src/utils/media-tags.ts +34 -2
- package/dist/scripts/upgrade-via-npm.sh +0 -168
- package/dist/src/message-queue.d.ts +0 -267
- package/dist/src/message-queue.js +0 -558
package/dist/src/gateway.js
CHANGED
|
@@ -6,7 +6,8 @@ import { loadSession, saveSession, clearSession } from "./session-store.js";
|
|
|
6
6
|
import { recordKnownUser, flushKnownUsers, listKnownUsers } from "./known-users.js";
|
|
7
7
|
import { getQQBotRuntime } from "./runtime.js";
|
|
8
8
|
import { setRefIndex, getRefIndex, formatRefEntryForAgent, flushRefIndex } from "./ref-index-store.js";
|
|
9
|
-
import { matchSlashCommand } from "./slash-commands.js";
|
|
9
|
+
import { matchSlashCommand, getPluginVersion } from "./slash-commands.js";
|
|
10
|
+
import { triggerUpdateCheck } from "./update-checker.js";
|
|
10
11
|
import { startImageServer, isImageServerRunning, downloadFile } from "./image-server.js";
|
|
11
12
|
import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "./utils/image-size.js";
|
|
12
13
|
import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload } from "./utils/payload.js";
|
|
@@ -257,6 +258,55 @@ async function ensureImageServer(log, publicBaseUrl) {
|
|
|
257
258
|
return null;
|
|
258
259
|
}
|
|
259
260
|
}
|
|
261
|
+
// ============ 启动问候语(首次安装/版本更新 vs 普通重启) ============
|
|
262
|
+
// 模块级变量:进程生命周期内只有首次为 true
|
|
263
|
+
// 区分 gateway restart(进程重启)和 health-monitor 断线重连
|
|
264
|
+
let isFirstReadyGlobal = true;
|
|
265
|
+
const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
|
|
266
|
+
/**
|
|
267
|
+
* 判断是否为首次安装或版本更新,返回对应的问候语。
|
|
268
|
+
* - 首次安装 / 版本变更 → "Haha,我的'灵魂'已上线,随时等你吩咐。"
|
|
269
|
+
* - 普通重启 → "我重新登上了,有事随时找我。"
|
|
270
|
+
* - 短时间内重复重启(60s 内) → null(跳过,避免刷屏)
|
|
271
|
+
*/
|
|
272
|
+
function getStartupGreeting() {
|
|
273
|
+
const currentVersion = getPluginVersion();
|
|
274
|
+
let isFirstOrUpdated = true;
|
|
275
|
+
let lastGreetedAt = 0;
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(STARTUP_MARKER_FILE)) {
|
|
278
|
+
const data = JSON.parse(fs.readFileSync(STARTUP_MARKER_FILE, "utf8"));
|
|
279
|
+
if (data.version === currentVersion) {
|
|
280
|
+
isFirstOrUpdated = false;
|
|
281
|
+
}
|
|
282
|
+
if (data.greetedAt) {
|
|
283
|
+
lastGreetedAt = new Date(data.greetedAt).getTime() || 0;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// 文件损坏或不存在,视为首次
|
|
289
|
+
}
|
|
290
|
+
// 防抖:60s 内重复重启不再发送问候(升级场景 stop→start 间隔很短)
|
|
291
|
+
const GREETING_DEBOUNCE_MS = 60_000;
|
|
292
|
+
if (!isFirstOrUpdated && lastGreetedAt > 0 && Date.now() - lastGreetedAt < GREETING_DEBOUNCE_MS) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
// 更新 marker 文件
|
|
296
|
+
try {
|
|
297
|
+
fs.writeFileSync(STARTUP_MARKER_FILE, JSON.stringify({
|
|
298
|
+
version: currentVersion,
|
|
299
|
+
startedAt: new Date().toISOString(),
|
|
300
|
+
greetedAt: new Date().toISOString(),
|
|
301
|
+
}) + "\n");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// ignore
|
|
305
|
+
}
|
|
306
|
+
return isFirstOrUpdated
|
|
307
|
+
? `Haha,我的'灵魂'已上线,随时等你吩咐。`
|
|
308
|
+
: `我重新登上了,有事随时找我。`;
|
|
309
|
+
}
|
|
260
310
|
/**
|
|
261
311
|
* 启动 Gateway WebSocket 连接(带自动重连)
|
|
262
312
|
* 支持流式消息发送
|
|
@@ -273,6 +323,8 @@ export async function startGateway(ctx) {
|
|
|
273
323
|
log?.info(`[qqbot:${account.accountId}] ${w}`);
|
|
274
324
|
}
|
|
275
325
|
}
|
|
326
|
+
// 后台版本检查(detached 子进程,零阻塞)
|
|
327
|
+
triggerUpdateCheck(log);
|
|
276
328
|
// 初始化 API 配置(markdown 支持)
|
|
277
329
|
initApiConfig({
|
|
278
330
|
markdownSupport: account.markdownSupport,
|
|
@@ -345,6 +397,53 @@ export async function startGateway(ctx) {
|
|
|
345
397
|
let isConnecting = false; // 防止并发连接
|
|
346
398
|
let reconnectTimer = null; // 重连定时器
|
|
347
399
|
let shouldRefreshToken = false; // 下次连接是否需要刷新 token
|
|
400
|
+
// 使用模块级 isFirstReadyGlobal,确保只有进程级重启才发送问候语
|
|
401
|
+
// health-monitor 重连不会重新初始化为 true
|
|
402
|
+
/** 异步发送启动问候语(READY 或 RESUMED 时调用) */
|
|
403
|
+
const sendStartupGreetings = (trigger) => {
|
|
404
|
+
(async () => {
|
|
405
|
+
try {
|
|
406
|
+
const greeting = getStartupGreeting();
|
|
407
|
+
if (!greeting) {
|
|
408
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (debounced, trigger=${trigger})`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
log?.info(`[qqbot:${account.accountId}] Sending startup greeting (trigger=${trigger}): "${greeting}"`);
|
|
412
|
+
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
413
|
+
const users = listKnownUsers({ accountId: account.accountId, type: "c2c" });
|
|
414
|
+
for (const user of users) {
|
|
415
|
+
try {
|
|
416
|
+
await sendProactiveC2CMessage(token, user.openid, greeting);
|
|
417
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to c2c:${user.openid}`);
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to c2c:${user.openid}: ${err}`);
|
|
421
|
+
}
|
|
422
|
+
await new Promise(r => setTimeout(r, 500));
|
|
423
|
+
}
|
|
424
|
+
const groups = listKnownUsers({ accountId: account.accountId, type: "group" });
|
|
425
|
+
const sentGroups = new Set();
|
|
426
|
+
for (const user of groups) {
|
|
427
|
+
const gid = user.groupOpenid;
|
|
428
|
+
if (!gid || sentGroups.has(gid))
|
|
429
|
+
continue;
|
|
430
|
+
sentGroups.add(gid);
|
|
431
|
+
try {
|
|
432
|
+
await sendProactiveGroupMessage(token, gid, greeting);
|
|
433
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to group:${gid}`);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to group:${gid}: ${err}`);
|
|
437
|
+
}
|
|
438
|
+
await new Promise(r => setTimeout(r, 500));
|
|
439
|
+
}
|
|
440
|
+
log?.info(`[qqbot:${account.accountId}] Startup greetings sent (${users.length} c2c, ${sentGroups.size} groups)`);
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send startup greetings: ${err}`);
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
};
|
|
348
447
|
// ============ P1-2: 尝试从持久化存储恢复 Session ============
|
|
349
448
|
// 传入当前 appId,如果 appId 已变更(换了机器人),旧 session 自动失效
|
|
350
449
|
const savedSession = loadSession(account.accountId, account.appId);
|
|
@@ -484,18 +583,41 @@ export async function startGateway(ctx) {
|
|
|
484
583
|
// 命中插件级指令,直接回复
|
|
485
584
|
log?.info(`[qqbot:${account.accountId}] Slash command matched: ${content}, replying directly`);
|
|
486
585
|
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
586
|
+
// 解析回复:纯文本 or 带文件的结果
|
|
587
|
+
const isFileResult = typeof reply === "object" && reply !== null && "filePath" in reply;
|
|
588
|
+
const replyText = isFileResult ? reply.text : reply;
|
|
589
|
+
const replyFile = isFileResult ? reply.filePath : null;
|
|
590
|
+
// 先发送文本回复
|
|
487
591
|
if (msg.type === "c2c") {
|
|
488
|
-
await sendC2CMessage(token, msg.senderId,
|
|
592
|
+
await sendC2CMessage(token, msg.senderId, replyText, msg.messageId);
|
|
489
593
|
}
|
|
490
594
|
else if (msg.type === "group" && msg.groupOpenid) {
|
|
491
|
-
await sendGroupMessage(token, msg.groupOpenid,
|
|
595
|
+
await sendGroupMessage(token, msg.groupOpenid, replyText, msg.messageId);
|
|
492
596
|
}
|
|
493
597
|
else if (msg.channelId) {
|
|
494
|
-
await sendChannelMessage(token, msg.channelId,
|
|
598
|
+
await sendChannelMessage(token, msg.channelId, replyText, msg.messageId);
|
|
495
599
|
}
|
|
496
600
|
else if (msg.type === "dm") {
|
|
497
|
-
|
|
498
|
-
|
|
601
|
+
await sendC2CMessage(token, msg.senderId, replyText, msg.messageId);
|
|
602
|
+
}
|
|
603
|
+
// 如果有文件需要发送
|
|
604
|
+
if (replyFile) {
|
|
605
|
+
try {
|
|
606
|
+
const targetType = msg.type === "group" ? "group" : msg.type === "c2c" || msg.type === "dm" ? "c2c" : "channel";
|
|
607
|
+
const targetId = msg.type === "group" ? (msg.groupOpenid || msg.senderId) : msg.type === "c2c" || msg.type === "dm" ? msg.senderId : (msg.channelId || msg.senderId);
|
|
608
|
+
const mediaCtx = {
|
|
609
|
+
targetType,
|
|
610
|
+
targetId,
|
|
611
|
+
account,
|
|
612
|
+
replyToId: msg.messageId,
|
|
613
|
+
logPrefix: `[qqbot:${account.accountId}]`,
|
|
614
|
+
};
|
|
615
|
+
await sendDocument(mediaCtx, replyFile);
|
|
616
|
+
log?.info(`[qqbot:${account.accountId}] Slash command file sent: ${replyFile}`);
|
|
617
|
+
}
|
|
618
|
+
catch (fileErr) {
|
|
619
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send slash command file: ${fileErr}`);
|
|
620
|
+
}
|
|
499
621
|
}
|
|
500
622
|
}
|
|
501
623
|
catch (err) {
|
|
@@ -923,7 +1045,7 @@ export async function startGateway(ctx) {
|
|
|
923
1045
|
// TTS 能力声明:仅在启用时告知 AI 可以发语音(与 qqbot-media SKILL.md 互补)
|
|
924
1046
|
// STT 无需声明:转写结果已在动态上下文的 ASR 行中,AI 自然可见
|
|
925
1047
|
if (hasTTS)
|
|
926
|
-
staticParts.push("
|
|
1048
|
+
staticParts.push("语音合成已启用,发送媒体格式:<qqmedia>路径</qqmedia>");
|
|
927
1049
|
const staticInstruction = staticParts.join(" | ");
|
|
928
1050
|
// 静态指引作为 systemPrompts 的首项注入
|
|
929
1051
|
systemPrompts.unshift(staticInstruction);
|
|
@@ -2190,49 +2312,23 @@ export async function startGateway(ctx) {
|
|
|
2190
2312
|
appId: account.appId,
|
|
2191
2313
|
});
|
|
2192
2314
|
onReady?.(d);
|
|
2193
|
-
//
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to c2c:${user.openid}`);
|
|
2203
|
-
}
|
|
2204
|
-
catch (err) {
|
|
2205
|
-
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to c2c:${user.openid}: ${err}`);
|
|
2206
|
-
}
|
|
2207
|
-
// 避免频率限制
|
|
2208
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2209
|
-
}
|
|
2210
|
-
const groups = listKnownUsers({ accountId: account.accountId, type: "group" });
|
|
2211
|
-
// 群组去重(同一群只发一次)
|
|
2212
|
-
const sentGroups = new Set();
|
|
2213
|
-
for (const user of groups) {
|
|
2214
|
-
const gid = user.groupOpenid;
|
|
2215
|
-
if (!gid || sentGroups.has(gid))
|
|
2216
|
-
continue;
|
|
2217
|
-
sentGroups.add(gid);
|
|
2218
|
-
try {
|
|
2219
|
-
await sendProactiveGroupMessage(token, gid, greeting);
|
|
2220
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to group:${gid}`);
|
|
2221
|
-
}
|
|
2222
|
-
catch (err) {
|
|
2223
|
-
log?.debug?.(`[qqbot:${account.accountId}] Failed to send startup greeting to group:${gid}: ${err}`);
|
|
2224
|
-
}
|
|
2225
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2226
|
-
}
|
|
2227
|
-
log?.info(`[qqbot:${account.accountId}] Startup greetings sent (${users.length} c2c, ${sentGroups.size} groups)`);
|
|
2228
|
-
}
|
|
2229
|
-
catch (err) {
|
|
2230
|
-
log?.error(`[qqbot:${account.accountId}] Failed to send startup greetings: ${err}`);
|
|
2231
|
-
}
|
|
2232
|
-
})();
|
|
2315
|
+
// 仅 startGateway 后的首次 READY 才发送上线通知
|
|
2316
|
+
// ws 断线重连(resume 失败后重新 Identify)产生的 READY 不发送
|
|
2317
|
+
if (!isFirstReadyGlobal) {
|
|
2318
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (reconnect READY, not first startup)`);
|
|
2319
|
+
}
|
|
2320
|
+
else {
|
|
2321
|
+
isFirstReadyGlobal = false;
|
|
2322
|
+
sendStartupGreetings("READY");
|
|
2323
|
+
} // end isFirstReady
|
|
2233
2324
|
}
|
|
2234
2325
|
else if (t === "RESUMED") {
|
|
2235
2326
|
log?.info(`[qqbot:${account.accountId}] Session resumed`);
|
|
2327
|
+
// RESUMED 也属于首次启动(gateway restart 通常走 resume)
|
|
2328
|
+
if (isFirstReadyGlobal) {
|
|
2329
|
+
isFirstReadyGlobal = false;
|
|
2330
|
+
sendStartupGreetings("RESUMED");
|
|
2331
|
+
}
|
|
2236
2332
|
// P1-2: 更新 Session 连接时间
|
|
2237
2333
|
if (sessionId) {
|
|
2238
2334
|
saveSession({
|
|
@@ -51,11 +51,19 @@ export interface QueueSnapshot {
|
|
|
51
51
|
/** 当前发送者在队列中的待处理消息数 */
|
|
52
52
|
senderPending: number;
|
|
53
53
|
}
|
|
54
|
+
/** 斜杠指令返回值:文本、带文件的结果、或 null(不处理) */
|
|
55
|
+
export type SlashCommandResult = string | SlashCommandFileResult | null;
|
|
56
|
+
/** 带文件的指令结果(先回复文本,再发送文件) */
|
|
57
|
+
export interface SlashCommandFileResult {
|
|
58
|
+
text: string;
|
|
59
|
+
/** 要发送的本地文件路径 */
|
|
60
|
+
filePath: string;
|
|
61
|
+
}
|
|
54
62
|
/**
|
|
55
63
|
* 尝试匹配并执行插件级斜杠指令
|
|
56
64
|
*
|
|
57
65
|
* @returns 回复文本(匹配成功),null(不匹配,应入队正常处理)
|
|
58
66
|
*/
|
|
59
|
-
export declare function matchSlashCommand(ctx: SlashCommandContext): Promise<
|
|
67
|
+
export declare function matchSlashCommand(ctx: SlashCommandContext): Promise<SlashCommandResult>;
|
|
60
68
|
/** 获取插件版本号(供外部使用) */
|
|
61
69
|
export declare function getPluginVersion(): string;
|
|
@@ -11,13 +11,11 @@
|
|
|
11
11
|
* 从而计算「开平→插件」和「插件处理」两段耗时
|
|
12
12
|
*/
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
|
-
import {
|
|
15
|
-
import { promisify } from "node:util";
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
16
15
|
import path from "node:path";
|
|
17
16
|
import fs from "node:fs";
|
|
18
|
-
import {
|
|
17
|
+
import { getUpdateInfo } from "./update-checker.js";
|
|
19
18
|
const require = createRequire(import.meta.url);
|
|
20
|
-
const execFileAsync = promisify(execFile);
|
|
21
19
|
// 读取 package.json 中的版本号
|
|
22
20
|
let PLUGIN_VERSION = "unknown";
|
|
23
21
|
try {
|
|
@@ -27,6 +25,32 @@ try {
|
|
|
27
25
|
catch {
|
|
28
26
|
// fallback
|
|
29
27
|
}
|
|
28
|
+
// 获取 openclaw 框架版本(缓存结果,只执行一次)
|
|
29
|
+
let _frameworkVersion = null;
|
|
30
|
+
function getFrameworkVersion() {
|
|
31
|
+
if (_frameworkVersion !== null)
|
|
32
|
+
return _frameworkVersion;
|
|
33
|
+
try {
|
|
34
|
+
for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileSync(cli, ["--version"], { timeout: 3000, encoding: "utf8" }).trim();
|
|
37
|
+
// 输出格式: "OpenClaw 2026.3.13 (61d171a)"
|
|
38
|
+
if (out) {
|
|
39
|
+
_frameworkVersion = out;
|
|
40
|
+
return _frameworkVersion;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// fallback
|
|
50
|
+
}
|
|
51
|
+
_frameworkVersion = "unknown";
|
|
52
|
+
return _frameworkVersion;
|
|
53
|
+
}
|
|
30
54
|
// ============ 指令注册表 ============
|
|
31
55
|
const commands = new Map();
|
|
32
56
|
function registerCommand(cmd) {
|
|
@@ -34,103 +58,152 @@ function registerCommand(cmd) {
|
|
|
34
58
|
}
|
|
35
59
|
// ============ 内置指令 ============
|
|
36
60
|
/**
|
|
37
|
-
* /ping —
|
|
61
|
+
* /qqbot-ping — 测试当前 openclaw 与 QQ 连接的网络延迟
|
|
38
62
|
*/
|
|
39
63
|
registerCommand({
|
|
40
|
-
name: "ping",
|
|
41
|
-
description: "
|
|
64
|
+
name: "qqbot-ping",
|
|
65
|
+
description: "测试当前 openclaw 与 QQ 连接的网络延迟",
|
|
42
66
|
handler: (ctx) => {
|
|
43
67
|
const now = Date.now();
|
|
44
68
|
const eventTime = new Date(ctx.eventTimestamp).getTime();
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
if (isNaN(eventTime)) {
|
|
70
|
+
return `🏓 pong!`;
|
|
71
|
+
}
|
|
72
|
+
const totalMs = now - eventTime;
|
|
73
|
+
const qqToPlugin = ctx.receivedAt - eventTime;
|
|
74
|
+
const pluginProcess = now - ctx.receivedAt;
|
|
75
|
+
const lines = [
|
|
76
|
+
`🏓 pong!`,
|
|
77
|
+
``,
|
|
78
|
+
`⏱ 延迟: ${totalMs}ms`,
|
|
79
|
+
` ├ 网络传输: ${qqToPlugin}ms`,
|
|
80
|
+
` └ 插件处理: ${pluginProcess}ms`,
|
|
81
|
+
];
|
|
82
|
+
return lines.join("\n");
|
|
47
83
|
},
|
|
48
84
|
});
|
|
49
85
|
/**
|
|
50
|
-
* /version —
|
|
86
|
+
* /qqbot-version — 查看插件版本号
|
|
51
87
|
*/
|
|
52
88
|
registerCommand({
|
|
53
|
-
name: "version",
|
|
54
|
-
description: "
|
|
89
|
+
name: "qqbot-version",
|
|
90
|
+
description: "查看插件版本号",
|
|
55
91
|
handler: () => {
|
|
56
|
-
|
|
92
|
+
const frameworkVersion = getFrameworkVersion();
|
|
93
|
+
const lines = [
|
|
94
|
+
`🦞 框架: ${frameworkVersion}`,
|
|
95
|
+
`🤖 qqbot插件: v${PLUGIN_VERSION}`,
|
|
96
|
+
];
|
|
97
|
+
const info = getUpdateInfo();
|
|
98
|
+
if (info.checkedAt === 0) {
|
|
99
|
+
// 尚未检查过
|
|
100
|
+
lines.push(`⏳ 版本检查中...`);
|
|
101
|
+
}
|
|
102
|
+
else if (info.error) {
|
|
103
|
+
lines.push(`⚠️ 版本检查失败`);
|
|
104
|
+
}
|
|
105
|
+
else if (info.hasUpdate && info.latest) {
|
|
106
|
+
lines.push(`🆕 有新版本: v${info.latest},使用 /qqbot-upgrade 升级`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push(`✅ 当前已是最新版本`);
|
|
110
|
+
}
|
|
111
|
+
return lines.join("\n");
|
|
57
112
|
},
|
|
58
113
|
});
|
|
59
114
|
/**
|
|
60
|
-
* /help —
|
|
115
|
+
* /qqbot-help — 查看所有指令以及用途
|
|
61
116
|
*/
|
|
62
117
|
registerCommand({
|
|
63
|
-
name: "help",
|
|
64
|
-
description: "
|
|
65
|
-
handler: (
|
|
66
|
-
const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
|
|
118
|
+
name: "qqbot-help",
|
|
119
|
+
description: "查看所有指令以及用途",
|
|
120
|
+
handler: () => {
|
|
67
121
|
const lines = [`**qqbot 插件 v${PLUGIN_VERSION}**`, ``];
|
|
68
122
|
for (const [name, cmd] of commands) {
|
|
69
123
|
lines.push(`- \`/${name}\` — ${cmd.description}`);
|
|
70
124
|
}
|
|
71
|
-
lines.push(``, `**升级指引**: ${url}`);
|
|
72
125
|
return lines.join("\n");
|
|
73
126
|
},
|
|
74
127
|
});
|
|
75
|
-
const DEFAULT_UPGRADE_URL = "https://
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
const DEFAULT_UPGRADE_URL = "https://doc.weixin.qq.com/doc/w3_AKEAGQaeACgCNHrh1CbHzTAKtT2gB?scode=AJEAIQdfAAozxFEnLZAKEAGQaeACg";
|
|
129
|
+
/** 升级说明文本 */
|
|
130
|
+
function getUpgradeGuide(url) {
|
|
131
|
+
return `📖 升级指引:\n${url}`;
|
|
132
|
+
}
|
|
78
133
|
/**
|
|
79
|
-
* /upgrade
|
|
80
|
-
* 无参数: 升级到 latest
|
|
81
|
-
* 带参数: 升级到指定版本,如 /upgrade 1.6.1
|
|
134
|
+
* /qqbot-upgrade — 查看版本更新状态 + 升级指引
|
|
82
135
|
*/
|
|
83
136
|
registerCommand({
|
|
84
|
-
name: "upgrade",
|
|
85
|
-
description: "
|
|
86
|
-
handler:
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
timeout: 120_000,
|
|
94
|
-
env: { ...process.env, PATH: process.env.PATH },
|
|
95
|
-
});
|
|
96
|
-
const output = (stdout + stderr).trim();
|
|
97
|
-
// 从脚本输出解析报告文本(QQBOT_REPORT=...)和版本号
|
|
98
|
-
const reportMatch = output.match(/QQBOT_REPORT=(.+)/);
|
|
99
|
-
const versionMatch = output.match(/QQBOT_NEW_VERSION=(\S+)/);
|
|
100
|
-
const newVersion = versionMatch?.[1] || "unknown";
|
|
101
|
-
report = reportMatch?.[1] || `✅ QQBot 升级完成: v${newVersion}`;
|
|
102
|
-
upgradeOk = newVersion !== "unknown" && newVersion !== PLUGIN_VERSION;
|
|
103
|
-
if (!upgradeOk && newVersion === PLUGIN_VERSION) {
|
|
104
|
-
report = `ℹ️ 已是最新版本 v${PLUGIN_VERSION}`;
|
|
105
|
-
}
|
|
137
|
+
name: "qqbot-upgrade",
|
|
138
|
+
description: "查看版本更新与升级指引",
|
|
139
|
+
handler: (ctx) => {
|
|
140
|
+
const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
|
|
141
|
+
const info = getUpdateInfo();
|
|
142
|
+
const lines = [];
|
|
143
|
+
if (info.checkedAt === 0) {
|
|
144
|
+
lines.push(`🤖 当前版本: v${PLUGIN_VERSION}`);
|
|
145
|
+
lines.push(`⏳ 版本检查中,请稍后再试`);
|
|
106
146
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
else if (info.error) {
|
|
148
|
+
lines.push(`🤖 当前版本: v${PLUGIN_VERSION}`);
|
|
149
|
+
lines.push(`⚠️ 版本检查失败`);
|
|
110
150
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
else if (info.hasUpdate && info.latest) {
|
|
152
|
+
lines.push(`🤖 当前版本: v${PLUGIN_VERSION}`);
|
|
153
|
+
lines.push(`🆕 最新版本: v${info.latest}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
lines.push(`✅ 当前已是最新版本 v${PLUGIN_VERSION}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push("", getUpgradeGuide(url));
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* /qqbot-logs — 打包本地最近 2000 行日志,发送文件给用户
|
|
164
|
+
*/
|
|
165
|
+
registerCommand({
|
|
166
|
+
name: "qqbot-logs",
|
|
167
|
+
description: "打包本地最近 2000 行日志,发送文件给用户",
|
|
168
|
+
handler: () => {
|
|
169
|
+
const homeDir = process.env.HOME || "~";
|
|
170
|
+
const logDir = path.join(homeDir, ".openclaw", "logs");
|
|
171
|
+
const gatewayLog = path.join(logDir, "gateway.log");
|
|
172
|
+
const errLog = path.join(logDir, "gateway.err.log");
|
|
173
|
+
const lines = [];
|
|
174
|
+
// 读取 gateway.log 最后 2000 行
|
|
175
|
+
for (const logFile of [gatewayLog, errLog]) {
|
|
176
|
+
if (!fs.existsSync(logFile))
|
|
177
|
+
continue;
|
|
178
|
+
try {
|
|
179
|
+
const content = fs.readFileSync(logFile, "utf8");
|
|
180
|
+
const allLines = content.split("\n");
|
|
181
|
+
const tail = allLines.slice(-1000); // 每个文件取最后 1000 行
|
|
182
|
+
if (tail.length > 0) {
|
|
183
|
+
lines.push(`\n========== ${path.basename(logFile)} (last ${tail.length} lines) ==========\n`);
|
|
184
|
+
lines.push(...tail);
|
|
115
185
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
process.exit(0);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const cli = cliNames[idx];
|
|
124
|
-
const child = spawn(cli, ["gateway", "restart"], {
|
|
125
|
-
detached: true, stdio: "ignore", env: process.env,
|
|
126
|
-
});
|
|
127
|
-
child.on("error", () => tryRestart(idx + 1));
|
|
128
|
-
child.unref();
|
|
129
|
-
};
|
|
130
|
-
tryRestart(0);
|
|
131
|
-
}, 2000);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
lines.push(`[读取 ${path.basename(logFile)} 失败]`);
|
|
189
|
+
}
|
|
132
190
|
}
|
|
133
|
-
|
|
191
|
+
if (lines.length === 0) {
|
|
192
|
+
return "⚠️ 未找到日志文件";
|
|
193
|
+
}
|
|
194
|
+
// 写入临时文件
|
|
195
|
+
const tmpDir = path.join(homeDir, ".openclaw", "qqbot", "downloads");
|
|
196
|
+
if (!fs.existsSync(tmpDir)) {
|
|
197
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
198
|
+
}
|
|
199
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
200
|
+
const tmpFile = path.join(tmpDir, `qqbot-logs-${timestamp}.txt`);
|
|
201
|
+
fs.writeFileSync(tmpFile, lines.join("\n"), "utf8");
|
|
202
|
+
const totalLines = lines.filter(l => !l.startsWith("=")).length;
|
|
203
|
+
return {
|
|
204
|
+
text: `📋 日志已打包(约 ${totalLines} 行),正在发送文件...`,
|
|
205
|
+
filePath: tmpFile,
|
|
206
|
+
};
|
|
134
207
|
},
|
|
135
208
|
});
|
|
136
209
|
// ============ 匹配入口 ============
|
|
@@ -158,54 +231,3 @@ export async function matchSlashCommand(ctx) {
|
|
|
158
231
|
export function getPluginVersion() {
|
|
159
232
|
return PLUGIN_VERSION;
|
|
160
233
|
}
|
|
161
|
-
/**
|
|
162
|
-
* 更新 openclaw.json 中的 plugins.installs 记录。
|
|
163
|
-
* - 如果 source 是 "path"(开发目录),改为 "npm" 并删除 sourcePath
|
|
164
|
-
* - 更新 installPath、version、installedAt
|
|
165
|
-
*/
|
|
166
|
-
function updatePluginsInstalls() {
|
|
167
|
-
const cliNames = ["openclaw", "clawdbot", "moltbot"];
|
|
168
|
-
let configPath = "";
|
|
169
|
-
let cliName = "";
|
|
170
|
-
for (const name of cliNames) {
|
|
171
|
-
const candidate = path.join(process.env.HOME || "~", `.${name}`, `${name}.json`);
|
|
172
|
-
if (fs.existsSync(candidate)) {
|
|
173
|
-
configPath = candidate;
|
|
174
|
-
cliName = name;
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (!configPath)
|
|
179
|
-
return;
|
|
180
|
-
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
181
|
-
const extensionsDir = path.join(process.env.HOME || "~", `.${cliName}`, "extensions");
|
|
182
|
-
const installPath = path.join(extensionsDir, "openclaw-qqbot");
|
|
183
|
-
// 读取新安装的版本号
|
|
184
|
-
let newVersion = "unknown";
|
|
185
|
-
try {
|
|
186
|
-
const pkgPath = path.join(installPath, "package.json");
|
|
187
|
-
if (fs.existsSync(pkgPath)) {
|
|
188
|
-
newVersion = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version || "unknown";
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch { }
|
|
192
|
-
cfg.plugins = cfg.plugins || {};
|
|
193
|
-
cfg.plugins.installs = cfg.plugins.installs || {};
|
|
194
|
-
const existing = cfg.plugins.installs["openclaw-qqbot"] || {};
|
|
195
|
-
// 保留已有记录,只更新关键字段
|
|
196
|
-
cfg.plugins.installs["openclaw-qqbot"] = {
|
|
197
|
-
...existing,
|
|
198
|
-
source: "npm",
|
|
199
|
-
installPath,
|
|
200
|
-
version: newVersion,
|
|
201
|
-
installedAt: new Date().toISOString(),
|
|
202
|
-
};
|
|
203
|
-
// 如果之前是 source:"path",清除 sourcePath(指向开发目录)
|
|
204
|
-
delete cfg.plugins.installs["openclaw-qqbot"].sourcePath;
|
|
205
|
-
// 确保 plugins.entries 存在
|
|
206
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
207
|
-
if (!cfg.plugins.entries["openclaw-qqbot"]) {
|
|
208
|
-
cfg.plugins.entries["openclaw-qqbot"] = { enabled: true };
|
|
209
|
-
}
|
|
210
|
-
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 4) + "\n");
|
|
211
|
-
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后台版本检查器
|
|
3
|
+
*
|
|
4
|
+
* - triggerUpdateCheck(): gateway 启动时调用,后台检查 npm registry 是否有新版本
|
|
5
|
+
* - getUpdateInfo(): 返回上次检查结果(供 /qqbot-version、/qqbot-help 指令使用)
|
|
6
|
+
* - formatUpdateNotice(): 格式化更新提示文本
|
|
7
|
+
*/
|
|
8
|
+
export interface UpdateInfo {
|
|
9
|
+
current: string;
|
|
10
|
+
latest: string | null;
|
|
11
|
+
hasUpdate: boolean;
|
|
12
|
+
checkedAt: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function triggerUpdateCheck(log?: {
|
|
16
|
+
info: (msg: string) => void;
|
|
17
|
+
error: (msg: string) => void;
|
|
18
|
+
debug?: (msg: string) => void;
|
|
19
|
+
}): void;
|
|
20
|
+
export declare function getUpdateInfo(): UpdateInfo;
|
|
21
|
+
export declare function formatUpdateNotice(info: UpdateInfo): string;
|