@tencent-connect/openclaw-qqbot 1.6.4-alpha.2 → 1.6.4-alpha.4
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 +95 -43
- package/dist/src/slash-commands.d.ts +2 -0
- package/dist/src/slash-commands.js +18 -1
- package/package.json +1 -1
- package/src/gateway.ts +111 -44
- package/src/slash-commands.ts +21 -1
package/dist/src/gateway.js
CHANGED
|
@@ -263,41 +263,63 @@ async function ensureImageServer(log, publicBaseUrl) {
|
|
|
263
263
|
// 区分 gateway restart(进程重启)和 health-monitor 断线重连
|
|
264
264
|
let isFirstReadyGlobal = true;
|
|
265
265
|
const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
|
|
266
|
-
|
|
267
|
-
*
|
|
268
|
-
|
|
269
|
-
* - 普通重启(同版本) → null(不发送)
|
|
270
|
-
*/
|
|
271
|
-
function getStartupGreeting() {
|
|
272
|
-
const currentVersion = getPluginVersion();
|
|
273
|
-
let isFirstOrUpdated = true;
|
|
266
|
+
const STARTUP_GREETING_TEXT = `Haha,我的'灵魂'已上线,随时等你吩咐。`;
|
|
267
|
+
const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
|
|
268
|
+
function readStartupMarker() {
|
|
274
269
|
try {
|
|
275
270
|
if (fs.existsSync(STARTUP_MARKER_FILE)) {
|
|
276
271
|
const data = JSON.parse(fs.readFileSync(STARTUP_MARKER_FILE, "utf8"));
|
|
277
|
-
|
|
278
|
-
isFirstOrUpdated = false;
|
|
279
|
-
}
|
|
272
|
+
return data || {};
|
|
280
273
|
}
|
|
281
274
|
}
|
|
282
275
|
catch {
|
|
283
|
-
//
|
|
284
|
-
}
|
|
285
|
-
// 普通重启(同版本)不发送问候语
|
|
286
|
-
if (!isFirstOrUpdated) {
|
|
287
|
-
return null;
|
|
276
|
+
// 文件损坏或不存在,视为无 marker
|
|
288
277
|
}
|
|
289
|
-
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
function writeStartupMarker(data) {
|
|
290
281
|
try {
|
|
291
|
-
fs.writeFileSync(STARTUP_MARKER_FILE, JSON.stringify(
|
|
292
|
-
version: currentVersion,
|
|
293
|
-
startedAt: new Date().toISOString(),
|
|
294
|
-
greetedAt: new Date().toISOString(),
|
|
295
|
-
}) + "\n");
|
|
282
|
+
fs.writeFileSync(STARTUP_MARKER_FILE, JSON.stringify(data) + "\n");
|
|
296
283
|
}
|
|
297
284
|
catch {
|
|
298
285
|
// ignore
|
|
299
286
|
}
|
|
300
|
-
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 判断是否需要发送“灵魂上线”问候:
|
|
290
|
+
* - 首次安装 / 版本变更:可发送
|
|
291
|
+
* - 同版本:不发送
|
|
292
|
+
* - 同版本近期失败:冷却期内不重试,减少噪音
|
|
293
|
+
*/
|
|
294
|
+
function getStartupGreetingPlan() {
|
|
295
|
+
const currentVersion = getPluginVersion();
|
|
296
|
+
const marker = readStartupMarker();
|
|
297
|
+
if (marker.version === currentVersion) {
|
|
298
|
+
return { shouldSend: false, version: currentVersion, reason: "same-version" };
|
|
299
|
+
}
|
|
300
|
+
if (marker.lastFailureVersion === currentVersion && marker.lastFailureAt) {
|
|
301
|
+
const lastFailureAtMs = new Date(marker.lastFailureAt).getTime();
|
|
302
|
+
if (!Number.isNaN(lastFailureAtMs) && Date.now() - lastFailureAtMs < STARTUP_GREETING_RETRY_COOLDOWN_MS) {
|
|
303
|
+
return { shouldSend: false, version: currentVersion, reason: "cooldown" };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { shouldSend: true, greeting: STARTUP_GREETING_TEXT, version: currentVersion };
|
|
307
|
+
}
|
|
308
|
+
function markStartupGreetingSent(version) {
|
|
309
|
+
writeStartupMarker({
|
|
310
|
+
version,
|
|
311
|
+
startedAt: new Date().toISOString(),
|
|
312
|
+
greetedAt: new Date().toISOString(),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function markStartupGreetingFailed(version, reason) {
|
|
316
|
+
const marker = readStartupMarker();
|
|
317
|
+
writeStartupMarker({
|
|
318
|
+
...marker,
|
|
319
|
+
lastFailureVersion: version,
|
|
320
|
+
lastFailureAt: new Date().toISOString(),
|
|
321
|
+
lastFailureReason: reason,
|
|
322
|
+
});
|
|
301
323
|
}
|
|
302
324
|
/**
|
|
303
325
|
* 启动 Gateway WebSocket 连接(带自动重连)
|
|
@@ -392,6 +414,9 @@ export async function startGateway(ctx) {
|
|
|
392
414
|
// 使用模块级 isFirstReadyGlobal,确保只有进程级重启才发送问候语
|
|
393
415
|
// health-monitor 重连不会重新初始化为 true
|
|
394
416
|
const ADMIN_MARKER_FILE = path.join(getQQBotDataDir("data"), `admin-${account.accountId}.json`);
|
|
417
|
+
const safeAccountId = account.accountId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
418
|
+
const safeAppId = account.appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
419
|
+
const UPGRADE_GREETING_TARGET_FILE = path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
|
|
395
420
|
/**
|
|
396
421
|
* 读取已持久化的管理员 openid
|
|
397
422
|
*/
|
|
@@ -406,6 +431,25 @@ export async function startGateway(ctx) {
|
|
|
406
431
|
catch { /* 文件损坏视为无 */ }
|
|
407
432
|
return undefined;
|
|
408
433
|
};
|
|
434
|
+
const loadUpgradeGreetingTargetOpenId = () => {
|
|
435
|
+
try {
|
|
436
|
+
if (fs.existsSync(UPGRADE_GREETING_TARGET_FILE)) {
|
|
437
|
+
const data = JSON.parse(fs.readFileSync(UPGRADE_GREETING_TARGET_FILE, "utf8"));
|
|
438
|
+
if (data.openid)
|
|
439
|
+
return data.openid;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch { /* 文件损坏视为无 */ }
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
const clearUpgradeGreetingTargetOpenId = () => {
|
|
446
|
+
try {
|
|
447
|
+
if (fs.existsSync(UPGRADE_GREETING_TARGET_FILE)) {
|
|
448
|
+
fs.unlinkSync(UPGRADE_GREETING_TARGET_FILE);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch { /* ignore */ }
|
|
452
|
+
};
|
|
409
453
|
/**
|
|
410
454
|
* 将管理员 openid 持久化到文件
|
|
411
455
|
*/
|
|
@@ -434,30 +478,37 @@ export async function startGateway(ctx) {
|
|
|
434
478
|
/** 异步发送启动问候语(仅发给管理员) */
|
|
435
479
|
const sendStartupGreetings = (trigger) => {
|
|
436
480
|
(async () => {
|
|
481
|
+
const plan = getStartupGreetingPlan();
|
|
482
|
+
if (!plan.shouldSend || !plan.greeting) {
|
|
483
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (${plan.reason ?? "debounced"}, trigger=${trigger})`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId();
|
|
487
|
+
const targetOpenId = upgradeTargetOpenId || resolveAdminOpenId();
|
|
488
|
+
if (!targetOpenId) {
|
|
489
|
+
markStartupGreetingFailed(plan.version, "no-admin");
|
|
490
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (no admin or known user)`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
437
493
|
try {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
450
|
-
const GREETING_TIMEOUT_MS = 10_000;
|
|
451
|
-
await Promise.race([
|
|
452
|
-
sendProactiveC2CMessage(token, adminId, greeting),
|
|
453
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
454
|
-
]);
|
|
455
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to admin: ${adminId}`);
|
|
456
|
-
}
|
|
494
|
+
const receiverType = upgradeTargetOpenId ? "upgrade-requester" : "admin";
|
|
495
|
+
log?.info(`[qqbot:${account.accountId}] Sending startup greeting to ${receiverType} (trigger=${trigger}): "${plan.greeting}"`);
|
|
496
|
+
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
497
|
+
const GREETING_TIMEOUT_MS = 10_000;
|
|
498
|
+
await Promise.race([
|
|
499
|
+
sendProactiveC2CMessage(token, targetOpenId, plan.greeting),
|
|
500
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
501
|
+
]);
|
|
502
|
+
markStartupGreetingSent(plan.version);
|
|
503
|
+
if (upgradeTargetOpenId) {
|
|
504
|
+
clearUpgradeGreetingTargetOpenId();
|
|
457
505
|
}
|
|
506
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to ${receiverType}: ${targetOpenId}`);
|
|
458
507
|
}
|
|
459
508
|
catch (err) {
|
|
460
|
-
|
|
509
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
510
|
+
markStartupGreetingFailed(plan.version, message);
|
|
511
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send startup greeting: ${message}`);
|
|
461
512
|
}
|
|
462
513
|
})();
|
|
463
514
|
};
|
|
@@ -587,6 +638,7 @@ export async function startGateway(ctx) {
|
|
|
587
638
|
channelId: msg.channelId,
|
|
588
639
|
groupOpenid: msg.groupOpenid,
|
|
589
640
|
accountId: account.accountId,
|
|
641
|
+
appId: account.appId,
|
|
590
642
|
accountConfig: account.config,
|
|
591
643
|
queueSnapshot: getQueueSnapshot(peerId),
|
|
592
644
|
};
|
|
@@ -15,7 +15,7 @@ import { execFileSync, execFile } from "node:child_process";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import { getUpdateInfo } from "./update-checker.js";
|
|
18
|
-
import { getHomeDir, isWindows } from "./utils/platform.js";
|
|
18
|
+
import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
const require = createRequire(import.meta.url);
|
|
21
21
|
// 读取 package.json 中的版本号
|
|
@@ -126,6 +126,22 @@ registerCommand({
|
|
|
126
126
|
},
|
|
127
127
|
});
|
|
128
128
|
const DEFAULT_UPGRADE_URL = "https://doc.weixin.qq.com/doc/w3_AKEAGQaeACgCNHrh1CbHzTAKtT2gB?scode=AJEAIQdfAAozxFEnLZAKEAGQaeACg";
|
|
129
|
+
function saveUpgradeGreetingTarget(accountId, appId, openid) {
|
|
130
|
+
const safeAccountId = accountId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
131
|
+
const safeAppId = appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
132
|
+
const filePath = path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
|
|
133
|
+
try {
|
|
134
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
135
|
+
accountId,
|
|
136
|
+
appId,
|
|
137
|
+
openid,
|
|
138
|
+
savedAt: new Date().toISOString(),
|
|
139
|
+
}) + "\n");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
129
145
|
// ============ 热更新 ============
|
|
130
146
|
/**
|
|
131
147
|
* 找到 CLI 命令名(openclaw / clawdbot / moltbot)
|
|
@@ -294,6 +310,7 @@ registerCommand({
|
|
|
294
310
|
}
|
|
295
311
|
return `❌ 当前环境不支持热更新(需要 bash 环境)\n⬆️升级指引:[点击查看](${url})\n\n> Windows 用户请安装 Git for Windows 后重试,或手动执行升级脚本`;
|
|
296
312
|
}
|
|
313
|
+
saveUpgradeGreetingTarget(ctx.accountId, ctx.appId, ctx.senderId);
|
|
297
314
|
const lines = [
|
|
298
315
|
`🔄 开始热更新...`,
|
|
299
316
|
`📌 当前版本:v${PLUGIN_VERSION}`,
|
package/package.json
CHANGED
package/src/gateway.ts
CHANGED
|
@@ -352,44 +352,78 @@ async function ensureImageServer(log?: GatewayContext["log"], publicBaseUrl?: st
|
|
|
352
352
|
let isFirstReadyGlobal = true;
|
|
353
353
|
|
|
354
354
|
const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
|
|
355
|
+
const STARTUP_GREETING_TEXT = `Haha,我的'灵魂'已上线,随时等你吩咐。`;
|
|
356
|
+
const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
|
|
357
|
+
|
|
358
|
+
type StartupMarkerData = {
|
|
359
|
+
version?: string;
|
|
360
|
+
startedAt?: string;
|
|
361
|
+
greetedAt?: string;
|
|
362
|
+
lastFailureAt?: string;
|
|
363
|
+
lastFailureReason?: string;
|
|
364
|
+
lastFailureVersion?: string;
|
|
365
|
+
};
|
|
355
366
|
|
|
356
|
-
|
|
357
|
-
* 判断是否为首次安装或版本更新,返回对应的问候语。
|
|
358
|
-
* - 首次安装 / 版本变更 → "Haha,我的'灵魂'已上线,随时等你吩咐。"
|
|
359
|
-
* - 普通重启(同版本) → null(不发送)
|
|
360
|
-
*/
|
|
361
|
-
function getStartupGreeting(): string | null {
|
|
362
|
-
const currentVersion = getPluginVersion();
|
|
363
|
-
let isFirstOrUpdated = true;
|
|
364
|
-
|
|
367
|
+
function readStartupMarker(): StartupMarkerData {
|
|
365
368
|
try {
|
|
366
369
|
if (fs.existsSync(STARTUP_MARKER_FILE)) {
|
|
367
|
-
const data = JSON.parse(fs.readFileSync(STARTUP_MARKER_FILE, "utf8"));
|
|
368
|
-
|
|
369
|
-
isFirstOrUpdated = false;
|
|
370
|
-
}
|
|
370
|
+
const data = JSON.parse(fs.readFileSync(STARTUP_MARKER_FILE, "utf8")) as StartupMarkerData;
|
|
371
|
+
return data || {};
|
|
371
372
|
}
|
|
372
373
|
} catch {
|
|
373
|
-
//
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 普通重启(同版本)不发送问候语
|
|
377
|
-
if (!isFirstOrUpdated) {
|
|
378
|
-
return null;
|
|
374
|
+
// 文件损坏或不存在,视为无 marker
|
|
379
375
|
}
|
|
376
|
+
return {};
|
|
377
|
+
}
|
|
380
378
|
|
|
381
|
-
|
|
379
|
+
function writeStartupMarker(data: StartupMarkerData): void {
|
|
382
380
|
try {
|
|
383
|
-
fs.writeFileSync(STARTUP_MARKER_FILE, JSON.stringify(
|
|
384
|
-
version: currentVersion,
|
|
385
|
-
startedAt: new Date().toISOString(),
|
|
386
|
-
greetedAt: new Date().toISOString(),
|
|
387
|
-
}) + "\n");
|
|
381
|
+
fs.writeFileSync(STARTUP_MARKER_FILE, JSON.stringify(data) + "\n");
|
|
388
382
|
} catch {
|
|
389
383
|
// ignore
|
|
390
384
|
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 判断是否需要发送“灵魂上线”问候:
|
|
389
|
+
* - 首次安装 / 版本变更:可发送
|
|
390
|
+
* - 同版本:不发送
|
|
391
|
+
* - 同版本近期失败:冷却期内不重试,减少噪音
|
|
392
|
+
*/
|
|
393
|
+
function getStartupGreetingPlan(): { shouldSend: boolean; greeting?: string; version: string; reason?: string } {
|
|
394
|
+
const currentVersion = getPluginVersion();
|
|
395
|
+
const marker = readStartupMarker();
|
|
396
|
+
|
|
397
|
+
if (marker.version === currentVersion) {
|
|
398
|
+
return { shouldSend: false, version: currentVersion, reason: "same-version" };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (marker.lastFailureVersion === currentVersion && marker.lastFailureAt) {
|
|
402
|
+
const lastFailureAtMs = new Date(marker.lastFailureAt).getTime();
|
|
403
|
+
if (!Number.isNaN(lastFailureAtMs) && Date.now() - lastFailureAtMs < STARTUP_GREETING_RETRY_COOLDOWN_MS) {
|
|
404
|
+
return { shouldSend: false, version: currentVersion, reason: "cooldown" };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { shouldSend: true, greeting: STARTUP_GREETING_TEXT, version: currentVersion };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function markStartupGreetingSent(version: string): void {
|
|
412
|
+
writeStartupMarker({
|
|
413
|
+
version,
|
|
414
|
+
startedAt: new Date().toISOString(),
|
|
415
|
+
greetedAt: new Date().toISOString(),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
391
418
|
|
|
392
|
-
|
|
419
|
+
function markStartupGreetingFailed(version: string, reason: string): void {
|
|
420
|
+
const marker = readStartupMarker();
|
|
421
|
+
writeStartupMarker({
|
|
422
|
+
...marker,
|
|
423
|
+
lastFailureVersion: version,
|
|
424
|
+
lastFailureAt: new Date().toISOString(),
|
|
425
|
+
lastFailureReason: reason,
|
|
426
|
+
});
|
|
393
427
|
}
|
|
394
428
|
|
|
395
429
|
/**
|
|
@@ -492,6 +526,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
492
526
|
// health-monitor 重连不会重新初始化为 true
|
|
493
527
|
|
|
494
528
|
const ADMIN_MARKER_FILE = path.join(getQQBotDataDir("data"), `admin-${account.accountId}.json`);
|
|
529
|
+
const safeAccountId = account.accountId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
530
|
+
const safeAppId = account.appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
531
|
+
const UPGRADE_GREETING_TARGET_FILE = path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
|
|
495
532
|
|
|
496
533
|
/**
|
|
497
534
|
* 读取已持久化的管理员 openid
|
|
@@ -506,6 +543,24 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
506
543
|
return undefined;
|
|
507
544
|
};
|
|
508
545
|
|
|
546
|
+
const loadUpgradeGreetingTargetOpenId = (): string | undefined => {
|
|
547
|
+
try {
|
|
548
|
+
if (fs.existsSync(UPGRADE_GREETING_TARGET_FILE)) {
|
|
549
|
+
const data = JSON.parse(fs.readFileSync(UPGRADE_GREETING_TARGET_FILE, "utf8")) as { openid?: string };
|
|
550
|
+
if (data.openid) return data.openid;
|
|
551
|
+
}
|
|
552
|
+
} catch { /* 文件损坏视为无 */ }
|
|
553
|
+
return undefined;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const clearUpgradeGreetingTargetOpenId = (): void => {
|
|
557
|
+
try {
|
|
558
|
+
if (fs.existsSync(UPGRADE_GREETING_TARGET_FILE)) {
|
|
559
|
+
fs.unlinkSync(UPGRADE_GREETING_TARGET_FILE);
|
|
560
|
+
}
|
|
561
|
+
} catch { /* ignore */ }
|
|
562
|
+
};
|
|
563
|
+
|
|
509
564
|
/**
|
|
510
565
|
* 将管理员 openid 持久化到文件
|
|
511
566
|
*/
|
|
@@ -534,27 +589,38 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
534
589
|
/** 异步发送启动问候语(仅发给管理员) */
|
|
535
590
|
const sendStartupGreetings = (trigger: "READY" | "RESUMED") => {
|
|
536
591
|
(async () => {
|
|
592
|
+
const plan = getStartupGreetingPlan();
|
|
593
|
+
if (!plan.shouldSend || !plan.greeting) {
|
|
594
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (${plan.reason ?? "debounced"}, trigger=${trigger})`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId();
|
|
599
|
+
const targetOpenId = upgradeTargetOpenId || resolveAdminOpenId();
|
|
600
|
+
if (!targetOpenId) {
|
|
601
|
+
markStartupGreetingFailed(plan.version, "no-admin");
|
|
602
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (no admin or known user)`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
537
606
|
try {
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
await Promise.race([
|
|
550
|
-
sendProactiveC2CMessage(token, adminId, greeting),
|
|
551
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
552
|
-
]);
|
|
553
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to admin: ${adminId}`);
|
|
554
|
-
}
|
|
607
|
+
const receiverType = upgradeTargetOpenId ? "upgrade-requester" : "admin";
|
|
608
|
+
log?.info(`[qqbot:${account.accountId}] Sending startup greeting to ${receiverType} (trigger=${trigger}): "${plan.greeting}"`);
|
|
609
|
+
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
610
|
+
const GREETING_TIMEOUT_MS = 10_000;
|
|
611
|
+
await Promise.race([
|
|
612
|
+
sendProactiveC2CMessage(token, targetOpenId, plan.greeting),
|
|
613
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
614
|
+
]);
|
|
615
|
+
markStartupGreetingSent(plan.version);
|
|
616
|
+
if (upgradeTargetOpenId) {
|
|
617
|
+
clearUpgradeGreetingTargetOpenId();
|
|
555
618
|
}
|
|
619
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to ${receiverType}: ${targetOpenId}`);
|
|
556
620
|
} catch (err) {
|
|
557
|
-
|
|
621
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
622
|
+
markStartupGreetingFailed(plan.version, message);
|
|
623
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send startup greeting: ${message}`);
|
|
558
624
|
}
|
|
559
625
|
})();
|
|
560
626
|
};
|
|
@@ -696,6 +762,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|
|
696
762
|
channelId: msg.channelId,
|
|
697
763
|
groupOpenid: msg.groupOpenid,
|
|
698
764
|
accountId: account.accountId,
|
|
765
|
+
appId: account.appId,
|
|
699
766
|
accountConfig: account.config,
|
|
700
767
|
queueSnapshot: getQueueSnapshot(peerId),
|
|
701
768
|
};
|
package/src/slash-commands.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { execFileSync, execFile } from "node:child_process";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import { getUpdateInfo } from "./update-checker.js";
|
|
20
|
-
import { getHomeDir, isWindows } from "./utils/platform.js";
|
|
20
|
+
import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
|
|
@@ -80,6 +80,8 @@ export interface SlashCommandContext {
|
|
|
80
80
|
groupOpenid?: string;
|
|
81
81
|
/** 账号 ID */
|
|
82
82
|
accountId: string;
|
|
83
|
+
/** Bot App ID */
|
|
84
|
+
appId: string;
|
|
83
85
|
/** 账号配置(供指令读取可配置项) */
|
|
84
86
|
accountConfig?: QQBotAccountConfig;
|
|
85
87
|
/** 当前用户队列状态快照 */
|
|
@@ -197,6 +199,22 @@ registerCommand({
|
|
|
197
199
|
|
|
198
200
|
const DEFAULT_UPGRADE_URL = "https://doc.weixin.qq.com/doc/w3_AKEAGQaeACgCNHrh1CbHzTAKtT2gB?scode=AJEAIQdfAAozxFEnLZAKEAGQaeACg";
|
|
199
201
|
|
|
202
|
+
function saveUpgradeGreetingTarget(accountId: string, appId: string, openid: string): void {
|
|
203
|
+
const safeAccountId = accountId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
204
|
+
const safeAppId = appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
205
|
+
const filePath = path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
|
|
206
|
+
try {
|
|
207
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
208
|
+
accountId,
|
|
209
|
+
appId,
|
|
210
|
+
openid,
|
|
211
|
+
savedAt: new Date().toISOString(),
|
|
212
|
+
}) + "\n");
|
|
213
|
+
} catch {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
// ============ 热更新 ============
|
|
201
219
|
|
|
202
220
|
/**
|
|
@@ -385,6 +403,8 @@ registerCommand({
|
|
|
385
403
|
return `❌ 当前环境不支持热更新(需要 bash 环境)\n⬆️升级指引:[点击查看](${url})\n\n> Windows 用户请安装 Git for Windows 后重试,或手动执行升级脚本`;
|
|
386
404
|
}
|
|
387
405
|
|
|
406
|
+
saveUpgradeGreetingTarget(ctx.accountId, ctx.appId, ctx.senderId);
|
|
407
|
+
|
|
388
408
|
const lines = [
|
|
389
409
|
`🔄 开始热更新...`,
|
|
390
410
|
`📌 当前版本:v${PLUGIN_VERSION}`,
|