@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.
@@ -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
- * - 首次安装 / 版本变更 → "Haha,我的'灵魂'已上线,随时等你吩咐。"
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
- if (data.version === currentVersion) {
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
- // 更新 marker 文件
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
- return `Haha,我的'灵魂'已上线,随时等你吩咐。`;
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 greeting = getStartupGreeting();
439
- if (!greeting) {
440
- log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (debounced, trigger=${trigger})`);
441
- }
442
- else {
443
- const adminId = resolveAdminOpenId();
444
- if (!adminId) {
445
- log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (no admin or known user)`);
446
- }
447
- else {
448
- log?.info(`[qqbot:${account.accountId}] Sending startup greeting to admin (trigger=${trigger}): "${greeting}"`);
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
- log?.error(`[qqbot:${account.accountId}] Failed to send startup greeting: ${err}`);
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
  };
@@ -35,6 +35,8 @@ export interface SlashCommandContext {
35
35
  groupOpenid?: string;
36
36
  /** 账号 ID */
37
37
  accountId: string;
38
+ /** Bot App ID */
39
+ appId: string;
38
40
  /** 账号配置(供指令读取可配置项) */
39
41
  accountConfig?: QQBotAccountConfig;
40
42
  /** 当前用户队列状态快照 */
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.4-alpha.2",
3
+ "version": "1.6.4-alpha.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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
- if (data.version === currentVersion) {
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
- // 更新 marker 文件
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
- return `Haha,我的'灵魂'已上线,随时等你吩咐。`;
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 greeting = getStartupGreeting();
539
- if (!greeting) {
540
- log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (debounced, trigger=${trigger})`);
541
- } else {
542
- const adminId = resolveAdminOpenId();
543
- if (!adminId) {
544
- log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (no admin or known user)`);
545
- } else {
546
- log?.info(`[qqbot:${account.accountId}] Sending startup greeting to admin (trigger=${trigger}): "${greeting}"`);
547
- const token = await getAccessToken(account.appId, account.clientSecret);
548
- const GREETING_TIMEOUT_MS = 10_000;
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
- log?.error(`[qqbot:${account.accountId}] Failed to send startup greeting: ${err}`);
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
  };
@@ -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}`,