@openclaw/qqbot 2026.5.31-beta.4 → 2026.6.1-beta.2

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/api.js CHANGED
@@ -1,9 +1,9 @@
1
- import { C as getMessageReplyStats, D as setOutboundAudioPort, E as OUTBOUND_ERROR_CODES, S as getMessageReplyConfig, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, a as sendText, b as MESSAGE_REPLY_LIMIT, f as buildMediaTarget, g as sendPhoto, h as sendDocument, i as sendProactiveMessage, m as resolveOutboundMediaPath, n as sendCronMessage, p as parseTarget, r as sendMedia, v as sendVoice, w as recordMessageReply, x as checkMessageReplyLimit, y as resolveUserFacingMediaError } from "./outbound-BNWSEG_7.js";
1
+ import { C as getMessageReplyStats, D as setOutboundAudioPort, E as OUTBOUND_ERROR_CODES, S as getMessageReplyConfig, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, a as sendText, b as MESSAGE_REPLY_LIMIT, f as buildMediaTarget, g as sendPhoto, h as sendDocument, i as sendProactiveMessage, m as resolveOutboundMediaPath, n as sendCronMessage, p as parseTarget, r as sendMedia, v as sendVoice, w as recordMessageReply, x as checkMessageReplyLimit, y as resolveUserFacingMediaError } from "./outbound-CVVQeQxX.js";
2
2
  import { c as listQQBotAccountIds, l as resolveDefaultQQBotAccountId, o as DEFAULT_ACCOUNT_ID, s as applyQQBotAccountConfig, u as resolveQQBotAccount } from "./config-schema-BI7AYP6Q.js";
3
- import { t as qqbotPlugin } from "./channel-CELcXJfZ.js";
4
- import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-CwW75z2d.js";
3
+ import { t as qqbotPlugin } from "./channel-BYYYPzrA.js";
4
+ import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-BSgtMZ6G.js";
5
5
  import { t as qqbotSetupPlugin } from "./channel.setup-xBlmSU4X.js";
6
- import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-CDEdBceh.js";
6
+ import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-BL0hymyD.js";
7
7
  import { resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
8
8
  import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
9
9
  //#region extensions/qqbot/src/engine/tools/channel-api.ts
@@ -1,6 +1,6 @@
1
1
  import { c as getPlatformAdapter, t as asOptionalObjectRecord } from "./string-normalize-R_0cKO7Q.js";
2
2
  import { a as qqbotSetupAdapterShared, c as listQQBotAccountIds, i as qqbotMeta, m as getBridgeLogger, n as qqbotSetupWizard, o as DEFAULT_ACCOUNT_ID$1, p as ensurePlatformAdapter, r as qqbotConfigAdapter, s as applyQQBotAccountConfig, t as qqbotChannelConfigSchema, u as resolveQQBotAccount } from "./config-schema-BI7AYP6Q.js";
3
- import { T as debugWarn, t as getQQBotRuntime, w as debugLog, z as formatErrorMessage } from "./runtime-CwW75z2d.js";
3
+ import { T as debugWarn, t as getQQBotRuntime, w as debugLog, z as formatErrorMessage } from "./runtime-BSgtMZ6G.js";
4
4
  import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
5
5
  import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
6
6
  import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
@@ -13,10 +13,11 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
13
13
  import * as fs$1 from "node:fs";
14
14
  import fs from "node:fs";
15
15
  import * as os$1 from "node:os";
16
- import { replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
16
+ import crypto from "node:crypto";
17
17
  import * as path$1 from "node:path";
18
18
  import path from "node:path";
19
19
  import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
20
+ import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
20
21
  //#region extensions/qqbot/src/engine/approval/index.ts
21
22
  function buildExecApprovalText(request) {
22
23
  const expiresIn = Math.max(0, Math.round((request.expiresAtMs - Date.now()) / 1e3));
@@ -374,7 +375,7 @@ function createQQBotApprovalCapability() {
374
375
  },
375
376
  load: async () => {
376
377
  ensurePlatformAdapter();
377
- return (await import("./handler-runtime-Blcp6PTS.js")).qqbotApprovalNativeRuntime;
378
+ return (await import("./handler-runtime-DEXmNJcJ.js")).qqbotApprovalNativeRuntime;
378
379
  }
379
380
  })
380
381
  });
@@ -410,6 +411,41 @@ async function writeOpenClawConfigThroughRuntime(runtime, cfg) {
410
411
  });
411
412
  }
412
413
  //#endregion
414
+ //#region extensions/qqbot/src/engine/utils/data-paths.ts
415
+ /**
416
+ * Centralised filename helpers for persisted QQBot state.
417
+ *
418
+ * Every persistence module routes file paths through these helpers so the
419
+ * naming convention stays in sync and legacy migrations are handled
420
+ * consistently.
421
+ *
422
+ * Key design decisions:
423
+ * - Credential backup is keyed only by `accountId` because recovery runs
424
+ * exactly when the appId is missing from config.
425
+ */
426
+ /**
427
+ * Normalise an identifier so it is safe to embed in a filename.
428
+ * Keeps alphanumerics, dot, underscore, dash; everything else becomes `_`.
429
+ */
430
+ function safeName$1(id) {
431
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
432
+ }
433
+ function getCredentialBackupRoot() {
434
+ return path.join(resolveStateDir(process.env), "qqbot", "data");
435
+ }
436
+ /**
437
+ * Per-accountId credential backup file. Not keyed by appId because the
438
+ * whole point of this file is to recover credentials when appId is
439
+ * missing from the live config.
440
+ */
441
+ function getCredentialBackupFile(accountId) {
442
+ return path.join(getCredentialBackupRoot(), `credential-backup-${safeName$1(accountId)}.json`);
443
+ }
444
+ /** Legacy single-file credential backup (pre-multi-account-isolation). */
445
+ function getLegacyCredentialBackupFile() {
446
+ return path.join(getCredentialBackupRoot(), "credential-backup.json");
447
+ }
448
+ //#endregion
413
449
  //#region extensions/qqbot/src/engine/utils/platform.ts
414
450
  /**
415
451
  * Cross-platform path and detection helpers for core/ modules.
@@ -427,9 +463,8 @@ async function writeOpenClawConfigThroughRuntime(runtime, cfg) {
427
463
  * 3. PlatformAdapter.getTempDir() as a last resort
428
464
  *
429
465
  * This is the *operating-system* home and intentionally ignores
430
- * `OPENCLAW_HOME`. Persistent QQ Bot data (sessions, known users, refs) is
431
- * keyed on this value to keep upgrades from hiding existing state when an
432
- * operator later sets `OPENCLAW_HOME`.
466
+ * `OPENCLAW_HOME`. QQ Bot still checks this tree for legacy state imports and
467
+ * media-path remaps from older releases.
433
468
  */
434
469
  function getHomeDir() {
435
470
  try {
@@ -467,11 +502,10 @@ function resolveOpenClawHome() {
467
502
  return raw;
468
503
  }
469
504
  /**
470
- * Return a path under `~/.openclaw/qqbot` without creating it.
505
+ * Return a legacy path under `~/.openclaw/qqbot` without creating it.
471
506
  *
472
- * Anchored on the OS home (not `OPENCLAW_HOME`) so persisted QQ Bot data
473
- * (sessions, known users, ref index, credential backups) does not silently
474
- * disappear when an operator adds `OPENCLAW_HOME` after the fact.
507
+ * Current QQ Bot runtime state lives in plugin SQLite KV. This path remains for
508
+ * legacy imports and media-path remaps from older releases.
475
509
  */
476
510
  function getQQBotDataPath(...subPaths) {
477
511
  return path$1.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
@@ -621,36 +655,24 @@ function resolveQQBotPayloadLocalFilePath(p) {
621
655
  return null;
622
656
  }
623
657
  //#endregion
624
- //#region extensions/qqbot/src/engine/utils/data-paths.ts
625
- /**
626
- * Centralised filename helpers for persisted QQBot state.
627
- *
628
- * Every persistence module routes file paths through these helpers so the
629
- * naming convention stays in sync and legacy migrations are handled
630
- * consistently.
631
- *
632
- * Key design decisions:
633
- * - Credential backup is keyed only by `accountId` because recovery runs
634
- * exactly when the appId is missing from config.
635
- */
636
- /**
637
- * Normalise an identifier so it is safe to embed in a filename.
638
- * Keeps alphanumerics, dot, underscore, dash; everything else becomes `_`.
639
- */
640
- function safeName(id) {
641
- return id.replace(/[^a-zA-Z0-9._-]/g, "_");
658
+ //#region extensions/qqbot/src/engine/utils/sqlite-state.ts
659
+ function resolveStoreEnv(options) {
660
+ if (!options.stateDir) return options.env;
661
+ return {
662
+ ...options.env ?? process.env,
663
+ OPENCLAW_STATE_DIR: options.stateDir
664
+ };
642
665
  }
643
- /**
644
- * Per-accountId credential backup file. Not keyed by appId because the
645
- * whole point of this file is to recover credentials when appId is
646
- * missing from the live config.
647
- */
648
- function getCredentialBackupFile(accountId) {
649
- return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
666
+ function openQQBotSyncKeyedStore(options) {
667
+ return getQQBotRuntime().state.openSyncKeyedStore({
668
+ namespace: options.namespace,
669
+ maxEntries: options.maxEntries,
670
+ ...options.defaultTtlMs != null ? { defaultTtlMs: options.defaultTtlMs } : {},
671
+ ...resolveStoreEnv(options) ? { env: resolveStoreEnv(options) } : {}
672
+ });
650
673
  }
651
- /** Legacy single-file credential backup (pre-multi-account-isolation). */
652
- function getLegacyCredentialBackupFile() {
653
- return path.join(getQQBotDataPath("data"), "credential-backup.json");
674
+ function buildQQBotStateKey(...parts) {
675
+ return crypto.createHash("sha256").update(JSON.stringify(parts)).digest("hex");
654
676
  }
655
677
  //#endregion
656
678
  //#region extensions/qqbot/src/engine/config/credential-backup.ts
@@ -663,23 +685,69 @@ function getLegacyCredentialBackupFile() {
663
685
  *
664
686
  * Mechanics:
665
687
  * - After each successful gateway start we snapshot the currently
666
- * resolved `appId` / `clientSecret` to a per-account backup file.
688
+ * resolved `appId` / `clientSecret` to a per-account SQLite KV entry.
667
689
  * - During plugin startup, if the live config has an empty appId or
668
690
  * secret, the gateway consults the backup and restores the values
669
691
  * via the config mutation API.
670
- * - Backups live under `~/.openclaw/qqbot/data/` so they survive
671
- * plugin directory replacement.
692
+ * - Legacy JSON backups are imported on first read, then removed after
693
+ * SQLite has the canonical copy.
672
694
  *
673
695
  * Safety notes:
674
696
  * - Only restore when credentials are **actually empty** — never
675
697
  * overwrite a user's intentional config change.
676
- * - Atomic write (temp file + rename) to avoid torn files.
677
- * - Per-account file: `credential-backup-<accountId>.json`. We do
678
- * **not** also key by appId because recovery happens precisely
679
- * when appId is unknown.
680
- * - Legacy single `credential-backup.json` is migrated automatically
681
- * when the stored accountId matches the caller.
698
+ * - Per-account key only; not keyed by appId because recovery happens
699
+ * precisely when appId is unknown.
682
700
  */
701
+ const CREDENTIAL_BACKUPS_NAMESPACE = "credential-backups";
702
+ const MAX_CREDENTIAL_BACKUPS = 1e3;
703
+ function createCredentialBackupStore() {
704
+ return openQQBotSyncKeyedStore({
705
+ namespace: CREDENTIAL_BACKUPS_NAMESPACE,
706
+ maxEntries: MAX_CREDENTIAL_BACKUPS
707
+ });
708
+ }
709
+ function safeName(id) {
710
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
711
+ }
712
+ function getLegacyOsHomeCredentialBackupFile(accountId) {
713
+ return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
714
+ }
715
+ function getLegacyOsHomeCredentialBackupFileWithoutAccount() {
716
+ return path.join(getQQBotDataPath("data"), "credential-backup.json");
717
+ }
718
+ function credentialBackupKey(accountId) {
719
+ return buildQQBotStateKey("credential-backup", accountId);
720
+ }
721
+ function isUsableBackup(data) {
722
+ return Boolean(data?.accountId && data.appId && data.clientSecret);
723
+ }
724
+ function loadUsableBackupFromFile(filePath) {
725
+ const data = loadJsonFile(filePath);
726
+ return isUsableBackup(data) ? data : null;
727
+ }
728
+ function removeFileQuietly(filePath) {
729
+ try {
730
+ fs.unlinkSync(filePath);
731
+ } catch {}
732
+ }
733
+ function findLegacyBackup(accountId) {
734
+ const candidates = accountId ? [
735
+ getCredentialBackupFile(accountId),
736
+ getLegacyCredentialBackupFile(),
737
+ getLegacyOsHomeCredentialBackupFile(accountId),
738
+ getLegacyOsHomeCredentialBackupFileWithoutAccount()
739
+ ] : [getLegacyCredentialBackupFile(), getLegacyOsHomeCredentialBackupFileWithoutAccount()];
740
+ for (const filePath of candidates) {
741
+ const data = loadUsableBackupFromFile(filePath);
742
+ if (!data) continue;
743
+ if (accountId && data.accountId !== accountId) continue;
744
+ return {
745
+ data,
746
+ filePath
747
+ };
748
+ }
749
+ return null;
750
+ }
683
751
  /** Persist a credential snapshot (called once gateway reaches READY). */
684
752
  function saveCredentialBackup(accountId, appId, clientSecret) {
685
753
  if (!appId || !clientSecret) return;
@@ -691,40 +759,27 @@ function saveCredentialBackup(accountId, appId, clientSecret) {
691
759
  clientSecret,
692
760
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
693
761
  };
694
- replaceFileAtomicSync({
695
- filePath: backupPath,
696
- content: `${JSON.stringify(data, null, 2)}\n`,
697
- tempPrefix: ".qqbot-credential-backup"
698
- });
762
+ createCredentialBackupStore().register(credentialBackupKey(accountId), data);
763
+ removeFileQuietly(backupPath);
699
764
  } catch {}
700
765
  }
701
766
  /**
702
767
  * Load a credential snapshot for `accountId`.
703
768
  *
704
- * Consults the new per-account file first; falls back to the legacy
705
- * global backup file and migrates it when the embedded `accountId`
706
- * matches the request. Returns `null` when no usable backup exists.
769
+ * Consults SQLite first; falls back to shipped JSON backups and imports
770
+ * them when the embedded `accountId` matches the request.
707
771
  */
708
772
  function loadCredentialBackup(accountId) {
709
773
  try {
710
774
  if (accountId) {
711
- const data = loadJsonFile(getCredentialBackupFile(accountId));
712
- if (data?.appId && data.clientSecret) return data;
775
+ const data = createCredentialBackupStore().lookup(credentialBackupKey(accountId));
776
+ if (isUsableBackup(data)) return data;
713
777
  }
714
- const legacy = getLegacyCredentialBackupFile();
715
- const data = loadJsonFile(legacy);
716
- if (data) {
717
- if (!data?.appId || !data?.clientSecret) return null;
718
- if (accountId && data.accountId !== accountId) return null;
719
- if (data.accountId) try {
720
- replaceFileAtomicSync({
721
- filePath: getCredentialBackupFile(data.accountId),
722
- content: `${JSON.stringify(data, null, 2)}\n`,
723
- tempPrefix: ".qqbot-credential-backup"
724
- });
725
- fs.unlinkSync(legacy);
726
- } catch {}
727
- return data;
778
+ const legacy = findLegacyBackup(accountId);
779
+ if (legacy) {
780
+ createCredentialBackupStore().register(credentialBackupKey(legacy.data.accountId), legacy.data);
781
+ removeFileQuietly(legacy.filePath);
782
+ return legacy.data;
728
783
  }
729
784
  } catch {}
730
785
  return null;
@@ -868,12 +923,12 @@ function looksLikeQQBotTarget(id) {
868
923
  //#region extensions/qqbot/src/channel.ts
869
924
  let gatewayModulePromise;
870
925
  function loadGatewayModule() {
871
- gatewayModulePromise ??= import("./gateway-Do-IrU8a.js");
926
+ gatewayModulePromise ??= import("./gateway-DNHe6a8p.js");
872
927
  return gatewayModulePromise;
873
928
  }
874
929
  let outboundMessagingModulePromise;
875
930
  function loadOutboundMessagingModule() {
876
- outboundMessagingModulePromise ??= import("./outbound-BNWSEG_7.js").then((n) => n.t);
931
+ outboundMessagingModulePromise ??= import("./outbound-CVVQeQxX.js").then((n) => n.t);
877
932
  return outboundMessagingModulePromise;
878
933
  }
879
934
  function createQQBotSendReceipt(params) {
@@ -1146,4 +1201,4 @@ const qqbotPlugin = {
1146
1201
  }
1147
1202
  };
1148
1203
  //#endregion
1149
- export { parseApprovalButtonData as C, buildPluginApprovalText as S, matchesQQBotApprovalAccount as _, getQQBotDataDir as a, buildApprovalKeyboard as b, getQQBotMediaPath as c, isWindows as d, normalizePath as f, isQQBotExecApprovalClientEnabled as g, authorizeQQBotApprovalAction as h, getHomeDir as i, getTempDir as l, toGatewayAccount as m, parseTarget as n, getQQBotDataPath as o, resolveQQBotPayloadLocalFilePath as p, checkSilkWasmAvailable as r, getQQBotMediaDir as s, qqbotPlugin as t, isLocalPath as u, resolveQQBotExecApprovalConfig as v, resolveApprovalTarget as w, buildExecApprovalText as x, shouldHandleQQBotExecApprovalRequest as y };
1204
+ export { buildExecApprovalText as C, resolveApprovalTarget as E, buildApprovalKeyboard as S, parseApprovalButtonData as T, authorizeQQBotApprovalAction as _, checkSilkWasmAvailable as a, resolveQQBotExecApprovalConfig as b, getQQBotDataPath as c, getTempDir as d, isLocalPath as f, toGatewayAccount as g, resolveQQBotPayloadLocalFilePath as h, openQQBotSyncKeyedStore as i, getQQBotMediaDir as l, normalizePath as m, parseTarget as n, getHomeDir as o, isWindows as p, buildQQBotStateKey as r, getQQBotDataDir as s, qqbotPlugin as t, getQQBotMediaPath as u, isQQBotExecApprovalClientEnabled as v, buildPluginApprovalText as w, shouldHandleQQBotExecApprovalRequest as x, matchesQQBotApprovalAccount as y };
@@ -1,2 +1,2 @@
1
- import { t as qqbotPlugin } from "./channel-CELcXJfZ.js";
1
+ import { t as qqbotPlugin } from "./channel-BYYYPzrA.js";
2
2
  export { qqbotPlugin };
@@ -1,9 +1,9 @@
1
- import { D as setOutboundAudioPort, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, c as isCronReminderPayload, d as normalizeMediaTags, g as sendPhoto, h as sendDocument, l as isMediaPayload, o as decodeMediaPath, r as sendMedia, s as encodePayloadForCron, u as parseQQBotPayload, v as sendVoice, y as resolveUserFacingMediaError } from "./outbound-BNWSEG_7.js";
1
+ import { D as setOutboundAudioPort, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, c as isCronReminderPayload, d as normalizeMediaTags, g as sendPhoto, h as sendDocument, l as isMediaPayload, o as decodeMediaPath, r as sendMedia, s as encodePayloadForCron, u as parseQQBotPayload, v as sendVoice, y as resolveUserFacingMediaError } from "./outbound-CVVQeQxX.js";
2
2
  import { c as getPlatformAdapter, i as normalizeOptionalString$1, n as normalizeLowercaseStringOrEmpty$1, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-R_0cKO7Q.js";
3
3
  import { f as resolveAccountBase, h as setBridgeLogger, p as ensurePlatformAdapter } from "./config-schema-BI7AYP6Q.js";
4
- import { C as parseApprovalButtonData, a as getQQBotDataDir, d as isWindows, f as normalizePath, h as authorizeQQBotApprovalAction, i as getHomeDir, l as getTempDir, m as toGatewayAccount, o as getQQBotDataPath, p as resolveQQBotPayloadLocalFilePath, r as checkSilkWasmAvailable, s as getQQBotMediaDir, u as isLocalPath } from "./channel-CELcXJfZ.js";
5
- import { B as StreamContentType, C as debugError, D as getNextMsgSeq, F as getImageMimeType, H as StreamInputState, I as getMaxUploadSize, N as formatFileSize, R as formatDuration, S as withTokenRetry, T as debugWarn, V as StreamInputMode, _ as sendInputNotify, a as acknowledgeInteraction, b as startBackgroundTokenRefresh, c as createRawInputNotifyFn, d as getMessageApi, f as getPluginUserAgent, g as registerAccount, h as onMessageSent, i as accountToCreds, j as downloadFile, k as openLocalFile, l as getAccessToken, m as initSender, n as getQQBotRuntimeForEngine, o as buildDeliveryTarget, p as initApiConfig, s as clearTokenCache, t as getQQBotRuntime, u as getGatewayUrl, v as sendMedia$1, w as debugLog, x as stopBackgroundTokenRefresh, y as sendText, z as formatErrorMessage } from "./runtime-CwW75z2d.js";
6
- import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-CDEdBceh.js";
4
+ import { T as parseApprovalButtonData, _ as authorizeQQBotApprovalAction, a as checkSilkWasmAvailable, c as getQQBotDataPath, d as getTempDir, f as isLocalPath, g as toGatewayAccount, h as resolveQQBotPayloadLocalFilePath, i as openQQBotSyncKeyedStore, l as getQQBotMediaDir, m as normalizePath, o as getHomeDir, p as isWindows, r as buildQQBotStateKey, s as getQQBotDataDir } from "./channel-BYYYPzrA.js";
5
+ import { B as StreamContentType, C as debugError, D as getNextMsgSeq, F as getImageMimeType, H as StreamInputState, I as getMaxUploadSize, N as formatFileSize, R as formatDuration, S as withTokenRetry, T as debugWarn, V as StreamInputMode, _ as sendInputNotify, a as acknowledgeInteraction, b as startBackgroundTokenRefresh, c as createRawInputNotifyFn, d as getMessageApi, f as getPluginUserAgent, g as registerAccount, h as onMessageSent, i as accountToCreds, j as downloadFile, k as openLocalFile, l as getAccessToken, m as initSender, n as getQQBotRuntimeForEngine, o as buildDeliveryTarget, p as initApiConfig, s as clearTokenCache, t as getQQBotRuntime, u as getGatewayUrl, v as sendMedia$1, w as debugLog, x as stopBackgroundTokenRefresh, y as sendText, z as formatErrorMessage } from "./runtime-BSgtMZ6G.js";
6
+ import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-BL0hymyD.js";
7
7
  import { asBoolean, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
8
8
  import { isImplicitSameChatApprovalAuthorization } from "openclaw/plugin-sdk/approval-auth-runtime";
9
9
  import * as fs$1 from "node:fs";
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import * as os$1 from "node:os";
12
12
  import crypto from "node:crypto";
13
13
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
14
- import { appendRegularFileSync, privateFileStoreSync, readRegularFileSync, replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
14
+ import { privateFileStoreSync, readRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
15
15
  import * as path$1 from "node:path";
16
16
  import path from "node:path";
17
17
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
@@ -171,33 +171,90 @@ function formatRefEntryForAgent(entry) {
171
171
  //#endregion
172
172
  //#region extensions/qqbot/src/engine/ref/store.ts
173
173
  /**
174
- * Ref-index store — JSONL file-based store for message reference index.
174
+ * Ref-index store — SQLite KV-backed store for message reference index.
175
175
  *
176
- * Migrated from src/ref-index-store.ts. Dependencies are only Node.js
177
- * built-ins + log + platform (both zero plugin-sdk).
176
+ * Legacy JSONL entries are imported once, then deleted after SQLite has the
177
+ * canonical ref-index rows.
178
178
  */
179
179
  const MAX_ENTRIES = 5e4;
180
180
  const TTL_MS = 10080 * 60 * 1e3;
181
- const COMPACT_THRESHOLD_RATIO = 2;
182
- let cache = null;
183
- let totalLinesOnDisk = 0;
181
+ const REF_INDEX_NAMESPACE = "ref-index";
182
+ const REF_INDEX_MIGRATIONS_NAMESPACE = "ref-index-migrations";
183
+ const LEGACY_REF_INDEX_MIGRATION_KEY = "ref-index-jsonl-v1";
184
+ let legacyImported$1 = false;
184
185
  function getRefIndexFile() {
185
186
  return path.join(getQQBotDataPath("data"), "ref-index.jsonl");
186
187
  }
187
- function loadFromFile() {
188
- if (cache !== null) return cache;
189
- cache = /* @__PURE__ */ new Map();
190
- totalLinesOnDisk = 0;
188
+ function createRefIndexStore() {
189
+ return openQQBotSyncKeyedStore({
190
+ namespace: REF_INDEX_NAMESPACE,
191
+ maxEntries: MAX_ENTRIES,
192
+ defaultTtlMs: TTL_MS
193
+ });
194
+ }
195
+ function createRefIndexMigrationStore() {
196
+ return openQQBotSyncKeyedStore({
197
+ namespace: REF_INDEX_MIGRATIONS_NAMESPACE,
198
+ maxEntries: 100
199
+ });
200
+ }
201
+ function refIndexStateKey(refIdx) {
202
+ return buildQQBotStateKey("ref-index", refIdx);
203
+ }
204
+ function toStoredAttachment(attachment) {
205
+ return {
206
+ type: attachment.type,
207
+ ...attachment.filename !== void 0 ? { filename: attachment.filename } : {},
208
+ ...attachment.contentType !== void 0 ? { contentType: attachment.contentType } : {},
209
+ ...attachment.transcript !== void 0 ? { transcript: attachment.transcript } : {},
210
+ ...attachment.transcriptSource !== void 0 ? { transcriptSource: attachment.transcriptSource } : {},
211
+ ...attachment.localPath !== void 0 ? { localPath: attachment.localPath } : {},
212
+ ...attachment.url !== void 0 ? { url: attachment.url } : {}
213
+ };
214
+ }
215
+ function toStoredRefIndexEntry(entry, createdAt) {
216
+ return {
217
+ content: entry.content,
218
+ senderId: entry.senderId,
219
+ ...entry.senderName !== void 0 ? { senderName: entry.senderName } : {},
220
+ timestamp: entry.timestamp,
221
+ ...entry.isBot !== void 0 ? { isBot: entry.isBot } : {},
222
+ ...entry.attachments ? { attachments: entry.attachments.map(toStoredAttachment) } : {},
223
+ createdAt
224
+ };
225
+ }
226
+ function toRefIndexEntry(entry) {
227
+ return {
228
+ content: entry.content,
229
+ senderId: entry.senderId,
230
+ ...entry.senderName !== void 0 ? { senderName: entry.senderName } : {},
231
+ timestamp: entry.timestamp,
232
+ ...entry.isBot !== void 0 ? { isBot: entry.isBot } : {},
233
+ ...entry.attachments ? { attachments: entry.attachments.map(toStoredAttachment) } : {}
234
+ };
235
+ }
236
+ function ensureLegacyRefIndexImported() {
237
+ if (legacyImported$1) return;
238
+ const migrationStore = createRefIndexMigrationStore();
239
+ if (migrationStore.lookup(LEGACY_REF_INDEX_MIGRATION_KEY)) {
240
+ legacyImported$1 = true;
241
+ return;
242
+ }
191
243
  try {
192
244
  const refIndexFile = getRefIndexFile();
193
- if (!fs.existsSync(refIndexFile)) return cache;
245
+ if (!fs.existsSync(refIndexFile)) {
246
+ migrationStore.register(LEGACY_REF_INDEX_MIGRATION_KEY, { importedAt: (/* @__PURE__ */ new Date()).toISOString() });
247
+ legacyImported$1 = true;
248
+ return;
249
+ }
194
250
  const lines = fs.readFileSync(refIndexFile, "utf-8").split("\n");
195
251
  const now = Date.now();
196
252
  let expired = 0;
253
+ let imported = 0;
254
+ const store = createRefIndexStore();
197
255
  for (const line of lines) {
198
256
  const trimmed = line.trim();
199
257
  if (!trimmed) continue;
200
- totalLinesOnDisk++;
201
258
  try {
202
259
  const entry = JSON.parse(trimmed);
203
260
  if (!entry.k || !entry.v || !entry.t) continue;
@@ -205,122 +262,45 @@ function loadFromFile() {
205
262
  expired++;
206
263
  continue;
207
264
  }
208
- cache.set(entry.k, {
209
- ...entry.v,
210
- createdAt: entry.t
211
- });
265
+ store.register(refIndexStateKey(entry.k), toStoredRefIndexEntry(entry.v, entry.t), { ttlMs: Math.max(1, TTL_MS - (now - entry.t)) });
266
+ imported++;
212
267
  } catch {}
213
268
  }
214
- debugLog(`[ref-index-store] Loaded ${cache.size} entries from ${totalLinesOnDisk} lines (${expired} expired)`);
215
- if (shouldCompact()) compactFile();
269
+ migrationStore.register(LEGACY_REF_INDEX_MIGRATION_KEY, { importedAt: (/* @__PURE__ */ new Date()).toISOString() });
270
+ legacyImported$1 = true;
271
+ fs.rmSync(refIndexFile, { force: true });
272
+ debugLog(`[ref-index-store] Migrated ${imported} entries to SQLite (${expired} expired)`);
216
273
  } catch (err) {
217
- debugError(`[ref-index-store] Failed to load: ${formatErrorMessage(err)}`);
218
- cache = /* @__PURE__ */ new Map();
274
+ debugError(`[ref-index-store] Failed to import legacy JSONL: ${formatErrorMessage(err)}`);
219
275
  }
220
- return cache;
221
276
  }
222
- function ensureDir$2() {
223
- getQQBotDataDir("data");
224
- }
225
- function appendLine(line) {
226
- try {
227
- ensureDir$2();
228
- appendRegularFileSync({
229
- filePath: getRefIndexFile(),
230
- content: JSON.stringify(line) + "\n"
231
- });
232
- totalLinesOnDisk++;
233
- } catch (err) {
234
- debugError(`[ref-index-store] Failed to append: ${formatErrorMessage(err)}`);
235
- }
236
- }
237
- function shouldCompact() {
238
- return cache !== null && totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1e3;
239
- }
240
- function compactFile() {
241
- if (!cache) return;
242
- const before = totalLinesOnDisk;
277
+ /** Persist a refIdx mapping for one message. */
278
+ function setRefIndex(refIdx, entry) {
243
279
  try {
244
- ensureDir$2();
245
- const refIndexFile = getRefIndexFile();
246
- const lines = [];
247
- for (const [key, entry] of cache) lines.push(JSON.stringify({
248
- k: key,
249
- v: {
250
- content: entry.content,
251
- senderId: entry.senderId,
252
- senderName: entry.senderName,
253
- timestamp: entry.timestamp,
254
- isBot: entry.isBot,
255
- attachments: entry.attachments
256
- },
257
- t: entry.createdAt
258
- }));
259
- replaceFileAtomicSync({
260
- filePath: refIndexFile,
261
- content: `${lines.join("\n")}\n`,
262
- tempPrefix: ".qqbot-ref-index"
263
- });
264
- totalLinesOnDisk = cache.size;
265
- debugLog(`[ref-index-store] Compacted: ${before} lines → ${totalLinesOnDisk} lines`);
280
+ ensureLegacyRefIndexImported();
281
+ const now = Date.now();
282
+ createRefIndexStore().register(refIndexStateKey(refIdx), toStoredRefIndexEntry(entry, now), { ttlMs: TTL_MS });
266
283
  } catch (err) {
267
- debugError(`[ref-index-store] Compact failed: ${formatErrorMessage(err)}`);
268
- }
269
- }
270
- function evictIfNeeded() {
271
- if (!cache || cache.size < MAX_ENTRIES) return;
272
- const now = Date.now();
273
- for (const [key, entry] of cache) if (now - entry.createdAt > TTL_MS) cache.delete(key);
274
- if (cache.size >= MAX_ENTRIES) {
275
- const toRemove = [...cache.entries()].toSorted((a, b) => a[1].createdAt - b[1].createdAt).slice(0, cache.size - MAX_ENTRIES + 1e3);
276
- for (const [key] of toRemove) cache.delete(key);
277
- debugLog(`[ref-index-store] Evicted ${toRemove.length} oldest entries`);
284
+ debugError(`[ref-index-store] Failed to persist ref index: ${formatErrorMessage(err)}`);
278
285
  }
279
286
  }
280
- /** Persist a refIdx mapping for one message. */
281
- function setRefIndex(refIdx, entry) {
282
- const store = loadFromFile();
283
- evictIfNeeded();
284
- const now = Date.now();
285
- store.set(refIdx, {
286
- ...entry,
287
- createdAt: now
288
- });
289
- appendLine({
290
- k: refIdx,
291
- v: {
292
- content: entry.content,
293
- senderId: entry.senderId,
294
- senderName: entry.senderName,
295
- timestamp: entry.timestamp,
296
- isBot: entry.isBot,
297
- attachments: entry.attachments
298
- },
299
- t: now
300
- });
301
- if (shouldCompact()) compactFile();
302
- }
303
287
  /** Look up one quoted message by refIdx. */
304
288
  function getRefIndex(refIdx) {
305
- const store = loadFromFile();
306
- const entry = store.get(refIdx);
307
- if (!entry) return null;
308
- if (Date.now() - entry.createdAt > TTL_MS) {
309
- store.delete(refIdx);
289
+ try {
290
+ ensureLegacyRefIndexImported();
291
+ const store = createRefIndexStore();
292
+ const key = refIndexStateKey(refIdx);
293
+ const entry = store.lookup(key);
294
+ if (!entry) return null;
295
+ if (Date.now() - entry.createdAt > TTL_MS) {
296
+ store.delete(key);
297
+ return null;
298
+ }
299
+ return toRefIndexEntry(entry);
300
+ } catch (err) {
301
+ debugError(`[ref-index-store] Failed to read ref index: ${formatErrorMessage(err)}`);
310
302
  return null;
311
303
  }
312
- return {
313
- content: entry.content,
314
- senderId: entry.senderId,
315
- senderName: entry.senderName,
316
- timestamp: entry.timestamp,
317
- isBot: entry.isBot,
318
- attachments: entry.attachments
319
- };
320
- }
321
- /** Compact the store before process exit when needed. */
322
- function flushRefIndex() {
323
- if (cache && shouldCompact()) compactFile();
324
304
  }
325
305
  //#endregion
326
306
  //#region extensions/qqbot/src/engine/utils/diagnostics.ts
@@ -570,18 +550,11 @@ async function trySlashCommand(msg, ctx) {
570
550
  //#endregion
571
551
  //#region extensions/qqbot/src/engine/session/known-users.ts
572
552
  /**
573
- * Known user tracking — JSON file-based store.
553
+ * Known user tracking — SQLite KV-backed store.
574
554
  *
575
- * Migrated from src/known-users.ts. Dependencies are only Node.js
576
- * built-ins + log + platform (both zero plugin-sdk).
555
+ * Legacy `known-users.json` data is imported once, then deleted after SQLite
556
+ * has the canonical copy.
577
557
  */
578
- let usersCache = null;
579
- const SAVE_THROTTLE_MS$1 = 5e3;
580
- let saveTimer = null;
581
- let isDirty = false;
582
- function ensureDir$1() {
583
- getQQBotDataDir("data");
584
- }
585
558
  function getKnownUsersFile() {
586
559
  return path.join(getQQBotDataPath("data"), "known-users.json");
587
560
  }
@@ -589,88 +562,106 @@ function makeUserKey(user) {
589
562
  const base = `${user.accountId}:${user.type}:${user.openid}`;
590
563
  return user.type === "group" && user.groupOpenid ? `${base}:${user.groupOpenid}` : base;
591
564
  }
592
- function loadUsersFromFile() {
593
- if (usersCache !== null) return usersCache;
594
- usersCache = /* @__PURE__ */ new Map();
595
- try {
596
- const knownUsersFile = getKnownUsersFile();
597
- const users = privateFileStoreSync(path.dirname(knownUsersFile)).readJsonIfExists(path.basename(knownUsersFile));
598
- if (users) {
599
- for (const user of users) usersCache.set(makeUserKey(user), user);
600
- debugLog(`[known-users] Loaded ${usersCache.size} users`);
601
- }
602
- } catch (err) {
603
- debugError(`[known-users] Failed to load users: ${formatErrorMessage(err)}`);
604
- usersCache = /* @__PURE__ */ new Map();
605
- }
606
- return usersCache;
565
+ const KNOWN_USERS_NAMESPACE = "known-users";
566
+ const KNOWN_USERS_MIGRATIONS_NAMESPACE = "known-users-migrations";
567
+ const LEGACY_KNOWN_USERS_MIGRATION_KEY = "known-users-json-v1";
568
+ const MAX_KNOWN_USERS = 1e5;
569
+ let legacyImported = false;
570
+ function createKnownUsersStore() {
571
+ return openQQBotSyncKeyedStore({
572
+ namespace: KNOWN_USERS_NAMESPACE,
573
+ maxEntries: MAX_KNOWN_USERS
574
+ });
575
+ }
576
+ function createKnownUsersMigrationStore() {
577
+ return openQQBotSyncKeyedStore({
578
+ namespace: KNOWN_USERS_MIGRATIONS_NAMESPACE,
579
+ maxEntries: 100
580
+ });
607
581
  }
608
- function saveUsersToFile() {
609
- if (!isDirty || saveTimer) return;
610
- saveTimer = setTimeout(() => {
611
- saveTimer = null;
612
- doSaveUsersToFile();
613
- }, SAVE_THROTTLE_MS$1);
582
+ function knownUserStateKey(key) {
583
+ return crypto.createHash("sha256").update(key).digest("hex");
614
584
  }
615
- function doSaveUsersToFile() {
616
- if (!usersCache || !isDirty) return;
585
+ function toStoredKnownUser(user) {
586
+ return {
587
+ openid: user.openid,
588
+ type: user.type,
589
+ ...user.nickname ? { nickname: user.nickname } : {},
590
+ ...user.groupOpenid ? { groupOpenid: user.groupOpenid } : {},
591
+ accountId: user.accountId,
592
+ firstSeenAt: user.firstSeenAt,
593
+ lastSeenAt: user.lastSeenAt,
594
+ interactionCount: user.interactionCount
595
+ };
596
+ }
597
+ function ensureLegacyKnownUsersImported() {
598
+ if (legacyImported) return;
599
+ const migrationStore = createKnownUsersMigrationStore();
600
+ if (migrationStore.lookup(LEGACY_KNOWN_USERS_MIGRATION_KEY)) {
601
+ legacyImported = true;
602
+ return;
603
+ }
617
604
  try {
618
- ensureDir$1();
619
- const filePath = getKnownUsersFile();
620
- privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), Array.from(usersCache.values()));
621
- isDirty = false;
605
+ const knownUsersFile = getKnownUsersFile();
606
+ const users = privateFileStoreSync(path.dirname(knownUsersFile)).readJsonIfExists(path.basename(knownUsersFile));
607
+ if (Array.isArray(users)) {
608
+ const store = createKnownUsersStore();
609
+ for (const user of users) store.registerIfAbsent(knownUserStateKey(makeUserKey(user)), toStoredKnownUser(user));
610
+ debugLog(`[known-users] Migrated ${users.length} users to SQLite`);
611
+ fs.rmSync(knownUsersFile, { force: true });
612
+ }
613
+ migrationStore.register(LEGACY_KNOWN_USERS_MIGRATION_KEY, { importedAt: (/* @__PURE__ */ new Date()).toISOString() });
614
+ legacyImported = true;
622
615
  } catch (err) {
623
- debugError(`[known-users] Failed to save users: ${formatErrorMessage(err)}`);
616
+ debugError(`[known-users] Failed to import legacy users: ${formatErrorMessage(err)}`);
624
617
  }
625
618
  }
626
- /** Flush pending writes immediately, typically during shutdown. */
627
- function flushKnownUsers() {
628
- if (saveTimer) {
629
- clearTimeout(saveTimer);
630
- saveTimer = null;
631
- }
632
- doSaveUsersToFile();
633
- }
634
619
  /** Record a known user whenever a message is received. */
635
620
  function recordKnownUser(user) {
636
- const cache = loadUsersFromFile();
637
- const key = makeUserKey(user);
638
- const now = Date.now();
639
- const existing = cache.get(key);
640
- if (existing) {
641
- existing.lastSeenAt = now;
642
- existing.interactionCount++;
643
- if (user.nickname && user.nickname !== existing.nickname) existing.nickname = user.nickname;
644
- } else {
645
- cache.set(key, {
646
- openid: user.openid,
647
- type: user.type,
648
- nickname: user.nickname,
649
- groupOpenid: user.groupOpenid,
650
- accountId: user.accountId,
651
- firstSeenAt: now,
652
- lastSeenAt: now,
653
- interactionCount: 1
654
- });
655
- debugLog(`[known-users] New user: ${user.openid} (${user.type})`);
621
+ try {
622
+ ensureLegacyKnownUsersImported();
623
+ const store = createKnownUsersStore();
624
+ const stateKey = knownUserStateKey(makeUserKey(user));
625
+ const now = Date.now();
626
+ const existing = store.lookup(stateKey);
627
+ if (existing) {
628
+ const next = {
629
+ ...existing,
630
+ lastSeenAt: now,
631
+ interactionCount: existing.interactionCount + 1
632
+ };
633
+ if (user.nickname && user.nickname !== existing.nickname) next.nickname = user.nickname;
634
+ store.register(stateKey, toStoredKnownUser(next));
635
+ } else {
636
+ store.register(stateKey, toStoredKnownUser({
637
+ openid: user.openid,
638
+ type: user.type,
639
+ nickname: user.nickname,
640
+ groupOpenid: user.groupOpenid,
641
+ accountId: user.accountId,
642
+ firstSeenAt: now,
643
+ lastSeenAt: now,
644
+ interactionCount: 1
645
+ }));
646
+ debugLog(`[known-users] New user: ${user.openid} (${user.type})`);
647
+ }
648
+ } catch (err) {
649
+ debugError(`[known-users] Failed to record user: ${formatErrorMessage(err)}`);
656
650
  }
657
- isDirty = true;
658
- saveUsersToFile();
659
651
  }
660
652
  //#endregion
661
653
  //#region extensions/qqbot/src/engine/session/session-store.ts
662
654
  /**
663
- * Gateway session persistence — JSONL file-based store.
655
+ * Gateway session persistence — SQLite KV-backed store.
664
656
  *
665
- * Migrated from src/session-store.ts. Dependencies are only Node.js
666
- * built-ins + log + platform (both zero plugin-sdk).
657
+ * Legacy JSON session files are imported on first account access, then
658
+ * removed after SQLite has the canonical short-lived session entry.
667
659
  */
668
660
  const SESSION_EXPIRE_TIME = 300 * 1e3;
669
661
  const SAVE_THROTTLE_MS = 1e3;
662
+ const SESSION_NAMESPACE = "gateway-sessions";
663
+ const MAX_SESSIONS = 1e3;
670
664
  const throttleState = /* @__PURE__ */ new Map();
671
- function ensureDir() {
672
- getQQBotDataDir("sessions");
673
- }
674
665
  function getSessionDir() {
675
666
  return getQQBotDataPath("sessions");
676
667
  }
@@ -690,36 +681,74 @@ function getCandidateSessionPaths(accountId) {
690
681
  const legacyPath = getLegacySessionPath(accountId);
691
682
  return primaryPath === legacyPath ? [primaryPath] : [primaryPath, legacyPath];
692
683
  }
684
+ function createSessionStore() {
685
+ return openQQBotSyncKeyedStore({
686
+ namespace: SESSION_NAMESPACE,
687
+ maxEntries: MAX_SESSIONS,
688
+ defaultTtlMs: SESSION_EXPIRE_TIME
689
+ });
690
+ }
691
+ function sessionKey(accountId) {
692
+ return buildQQBotStateKey("gateway-session", accountId);
693
+ }
694
+ function remainingSessionTtlMs(state, now = Date.now()) {
695
+ return Math.max(1, SESSION_EXPIRE_TIME - (now - state.savedAt));
696
+ }
697
+ function toStoredSessionState(state) {
698
+ return {
699
+ sessionId: state.sessionId,
700
+ lastSeq: state.lastSeq,
701
+ lastConnectedAt: state.lastConnectedAt,
702
+ intentLevelIndex: state.intentLevelIndex,
703
+ accountId: state.accountId,
704
+ savedAt: state.savedAt,
705
+ ...state.appId ? { appId: state.appId } : {}
706
+ };
707
+ }
708
+ function removeFileQuietly(filePath) {
709
+ try {
710
+ fs.unlinkSync(filePath);
711
+ } catch {}
712
+ }
713
+ function loadLegacySession(accountId) {
714
+ for (const candidatePath of getCandidateSessionPaths(accountId)) {
715
+ const state = privateFileStoreSync(path.dirname(candidatePath)).readJsonIfExists(path.basename(candidatePath));
716
+ if (state) return {
717
+ state,
718
+ filePath: candidatePath
719
+ };
720
+ }
721
+ return null;
722
+ }
723
+ function migrateLegacySession(accountId) {
724
+ const legacy = loadLegacySession(accountId);
725
+ if (!legacy) return null;
726
+ const now = Date.now();
727
+ if (now - legacy.state.savedAt <= SESSION_EXPIRE_TIME) createSessionStore().register(sessionKey(accountId), toStoredSessionState(legacy.state), { ttlMs: remainingSessionTtlMs(legacy.state, now) });
728
+ for (const filePath of getCandidateSessionPaths(accountId)) removeFileQuietly(filePath);
729
+ return legacy.state;
730
+ }
693
731
  /** Load a saved session, rejecting expired or mismatched appId entries. */
694
732
  function loadSession(accountId, expectedAppId) {
695
733
  try {
696
- let filePath = null;
697
- let state = null;
698
- for (const candidatePath of getCandidateSessionPaths(accountId)) {
699
- state = privateFileStoreSync(path.dirname(candidatePath)).readJsonIfExists(path.basename(candidatePath));
700
- if (state) {
701
- filePath = candidatePath;
702
- break;
703
- }
704
- }
705
- if (!filePath || !state) return null;
734
+ const store = createSessionStore();
735
+ let state = store.lookup(sessionKey(accountId));
736
+ if (!state) state = migrateLegacySession(accountId) ?? void 0;
737
+ if (!state) return null;
706
738
  const now = Date.now();
707
739
  if (now - state.savedAt > SESSION_EXPIRE_TIME) {
708
740
  debugLog(`[session-store] Session expired for ${accountId}, age: ${Math.round((now - state.savedAt) / 1e3)}s`);
709
- try {
710
- fs.unlinkSync(filePath);
711
- } catch {}
741
+ store.delete(sessionKey(accountId));
712
742
  return null;
713
743
  }
714
744
  if (expectedAppId && state.appId && state.appId !== expectedAppId) {
715
745
  debugLog(`[session-store] appId mismatch for ${accountId}: saved=${state.appId}, current=${expectedAppId}. Discarding stale session.`);
716
- try {
717
- fs.unlinkSync(filePath);
718
- } catch {}
746
+ store.delete(sessionKey(accountId));
719
747
  return null;
720
748
  }
721
749
  if (!state.sessionId || state.lastSeq === null || state.lastSeq === void 0) {
722
750
  debugLog(`[session-store] Invalid session data for ${accountId}`);
751
+ store.delete(sessionKey(accountId));
723
752
  return null;
724
753
  }
725
754
  debugLog(`[session-store] Loaded session for ${accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}, appId=${state.appId ?? "unknown"}, age=${Math.round((now - state.savedAt) / 1e3)}s`);
@@ -771,13 +800,13 @@ function doSaveSession(state) {
771
800
  const filePath = getSessionPath(state.accountId);
772
801
  const legacyPath = getLegacySessionPath(state.accountId);
773
802
  try {
774
- ensureDir();
775
803
  const stateToSave = {
776
804
  ...state,
777
805
  savedAt: Date.now()
778
806
  };
779
- privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), stateToSave);
780
- if (legacyPath !== filePath && fs.existsSync(legacyPath)) fs.unlinkSync(legacyPath);
807
+ createSessionStore().register(sessionKey(state.accountId), toStoredSessionState(stateToSave), { ttlMs: SESSION_EXPIRE_TIME });
808
+ removeFileQuietly(filePath);
809
+ removeFileQuietly(legacyPath);
781
810
  debugLog(`[session-store] Saved session for ${state.accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}`);
782
811
  } catch (err) {
783
812
  debugError(`[session-store] Failed to save session for ${state.accountId}: ${formatErrorMessage(err)}`);
@@ -791,11 +820,8 @@ function clearSession(accountId) {
791
820
  throttleState.delete(accountId);
792
821
  }
793
822
  try {
794
- let cleared = false;
795
- for (const filePath of getCandidateSessionPaths(accountId)) if (fs.existsSync(filePath)) {
796
- fs.unlinkSync(filePath);
797
- cleared = true;
798
- }
823
+ const cleared = createSessionStore().delete(sessionKey(accountId));
824
+ for (const filePath of getCandidateSessionPaths(accountId)) removeFileQuietly(filePath);
799
825
  if (cleared) debugLog(`[session-store] Cleared session for ${accountId}`);
800
826
  } catch (err) {
801
827
  debugError(`[session-store] Failed to clear session for ${accountId}: ${formatErrorMessage(err)}`);
@@ -1582,8 +1608,6 @@ var GatewayConnection = class {
1582
1608
  }
1583
1609
  this.cleanup();
1584
1610
  stopBackgroundTokenRefresh(account.appId);
1585
- flushKnownUsers();
1586
- flushRefIndex();
1587
1611
  });
1588
1612
  }
1589
1613
  cleanup() {
@@ -1,6 +1,6 @@
1
1
  import { m as getBridgeLogger, p as ensurePlatformAdapter, u as resolveQQBotAccount } from "./config-schema-BI7AYP6Q.js";
2
- import { S as buildPluginApprovalText, _ as matchesQQBotApprovalAccount, b as buildApprovalKeyboard, g as isQQBotExecApprovalClientEnabled, v as resolveQQBotExecApprovalConfig, w as resolveApprovalTarget, x as buildExecApprovalText, y as shouldHandleQQBotExecApprovalRequest } from "./channel-CELcXJfZ.js";
3
- import { d as getMessageApi, i as accountToCreds } from "./runtime-CwW75z2d.js";
2
+ import { C as buildExecApprovalText, E as resolveApprovalTarget, S as buildApprovalKeyboard, b as resolveQQBotExecApprovalConfig, v as isQQBotExecApprovalClientEnabled, w as buildPluginApprovalText, x as shouldHandleQQBotExecApprovalRequest, y as matchesQQBotApprovalAccount } from "./channel-BYYYPzrA.js";
3
+ import { d as getMessageApi, i as accountToCreds } from "./runtime-BSgtMZ6G.js";
4
4
  import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
5
5
  import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
6
6
  //#region extensions/qqbot/src/bridge/approval/handler-runtime.ts
@@ -1,6 +1,6 @@
1
1
  import { i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
2
- import { a as getQQBotDataDir, f as normalizePath$1, n as parseTarget$1, p as resolveQQBotPayloadLocalFilePath, s as getQQBotMediaDir, u as isLocalPath } from "./channel-CELcXJfZ.js";
3
- import { A as checkFileSize, C as debugError, E as UploadDailyLimitExceededError, F as getImageMimeType, I as getMaxUploadSize, L as readFileAsync, M as fileExistsAsync, N as formatFileSize, O as UPLOAD_PREPARE_FALLBACK_CODE, P as getFileTypeName, T as debugWarn, i as accountToCreds, j as downloadFile, p as initApiConfig, v as sendMedia$1, w as debugLog, y as sendText$1, z as formatErrorMessage } from "./runtime-CwW75z2d.js";
2
+ import { f as isLocalPath, h as resolveQQBotPayloadLocalFilePath, l as getQQBotMediaDir, m as normalizePath$1, n as parseTarget$1, s as getQQBotDataDir } from "./channel-BYYYPzrA.js";
3
+ import { A as checkFileSize, C as debugError, E as UploadDailyLimitExceededError, F as getImageMimeType, I as getMaxUploadSize, L as readFileAsync, M as fileExistsAsync, N as formatFileSize, O as UPLOAD_PREPARE_FALLBACK_CODE, P as getFileTypeName, T as debugWarn, i as accountToCreds, j as downloadFile, p as initApiConfig, v as sendMedia$1, w as debugLog, y as sendText$1, z as formatErrorMessage } from "./runtime-BSgtMZ6G.js";
4
4
  import { pathExistsSync, resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
5
5
  import path from "node:path";
6
6
  import { getFileExtension } from "openclaw/plugin-sdk/media-mime";
@@ -1,5 +1,5 @@
1
- import { a as getQQBotDataDir, c as getQQBotMediaPath, d as isWindows, i as getHomeDir } from "./channel-CELcXJfZ.js";
2
- import { w as debugLog } from "./runtime-CwW75z2d.js";
1
+ import { o as getHomeDir, p as isWindows, s as getQQBotDataDir, u as getQQBotMediaPath } from "./channel-BYYYPzrA.js";
2
+ import { w as debugLog } from "./runtime-BSgtMZ6G.js";
3
3
  import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
@@ -1002,6 +1002,23 @@ function sleep(ms) {
1002
1002
  */
1003
1003
  const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
1004
1004
  const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 7200;
1005
+ /**
1006
+ * Host-scoped SSRF policy for the QQ Bot token endpoint.
1007
+ *
1008
+ * `TOKEN_URL` is a hard-coded `https://bots.qq.com/...` constant, so this
1009
+ * relaxation only ever applies to that single host. Fake-IP proxy stacks
1010
+ * (sing-box, Clash, Surge, WSL2 DNS, etc.) routinely map `bots.qq.com` into
1011
+ * the RFC 2544 benchmark range `198.18.0.0/15`, which the default SSRF
1012
+ * guard blocks. We mirror the existing media-path pattern
1013
+ * (`QQBOT_MEDIA_SSRF_POLICY` in `../utils/file-utils.ts`) so the relaxation
1014
+ * stays narrowly host-scoped instead of weakening the global default.
1015
+ *
1016
+ * See https://github.com/openclaw/openclaw/issues/88984.
1017
+ */
1018
+ const QQBOT_TOKEN_SSRF_POLICY = {
1019
+ hostnameAllowlist: ["bots.qq.com"],
1020
+ allowRfc2544BenchmarkRange: true
1021
+ };
1005
1022
  function resolveTokenExpiresInSeconds(value) {
1006
1023
  const parsed = parseStrictPositiveInteger(value);
1007
1024
  if (parsed !== void 0) return parsed;
@@ -1141,6 +1158,7 @@ var TokenManager = class {
1141
1158
  url: TOKEN_URL,
1142
1159
  auditContext: "qqbot-token",
1143
1160
  capture: false,
1161
+ policy: QQBOT_TOKEN_SSRF_POLICY,
1144
1162
  init: {
1145
1163
  method: "POST",
1146
1164
  headers: {
@@ -2112,7 +2130,7 @@ function supportsRichMedia(targetType) {
2112
2130
  }
2113
2131
  //#endregion
2114
2132
  //#region extensions/qqbot/src/bridge/runtime.ts
2115
- const { setRuntime: _setRuntime, getRuntime: getQQBotRuntime } = createPluginRuntimeStore({
2133
+ const { setRuntime: _setRuntime, clearRuntime: resetQQBotRuntimeForTest, getRuntime: getQQBotRuntime } = createPluginRuntimeStore({
2116
2134
  pluginId: "qqbot",
2117
2135
  errorMessage: "QQBot runtime not initialized"
2118
2136
  });
@@ -1,2 +1,2 @@
1
- import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-CwW75z2d.js";
1
+ import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-BSgtMZ6G.js";
2
2
  export { getQQBotRuntime, setQQBotRuntime };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.5.31-beta.4",
3
+ "version": "2026.6.1-beta.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/qqbot",
9
- "version": "2026.5.31-beta.4",
9
+ "version": "2026.6.1-beta.2",
10
10
  "dependencies": {
11
11
  "@tencent-connect/qqbot-connector": "1.1.0",
12
12
  "mpg123-decoder": "1.0.3",
@@ -15,7 +15,7 @@
15
15
  "zod": "4.4.3"
16
16
  },
17
17
  "peerDependencies": {
18
- "openclaw": ">=2026.5.31-beta.4"
18
+ "openclaw": ">=2026.6.1-beta.2"
19
19
  },
20
20
  "peerDependenciesMeta": {
21
21
  "openclaw": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.5.31-beta.4",
3
+ "version": "2026.6.1-beta.2",
4
4
  "private": false,
5
5
  "description": "OpenClaw QQ Bot channel plugin for group and direct-message workflows.",
6
6
  "repository": {
@@ -16,7 +16,7 @@
16
16
  "zod": "4.4.3"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.5.31-beta.4"
19
+ "openclaw": ">=2026.6.1-beta.2"
20
20
  },
21
21
  "peerDependenciesMeta": {
22
22
  "openclaw": {
@@ -45,10 +45,10 @@
45
45
  "minHostVersion": ">=2026.4.10"
46
46
  },
47
47
  "compat": {
48
- "pluginApi": ">=2026.5.31-beta.4"
48
+ "pluginApi": ">=2026.6.1-beta.2"
49
49
  },
50
50
  "build": {
51
- "openclawVersion": "2026.5.31-beta.4"
51
+ "openclawVersion": "2026.6.1-beta.2"
52
52
  },
53
53
  "release": {
54
54
  "publishToClawHub": true,