@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 +4 -4
- package/dist/{channel-CELcXJfZ.js → channel-BYYYPzrA.js} +129 -74
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{gateway-Do-IrU8a.js → gateway-DNHe6a8p.js} +244 -220
- package/dist/{handler-runtime-Blcp6PTS.js → handler-runtime-DEXmNJcJ.js} +2 -2
- package/dist/{outbound-BNWSEG_7.js → outbound-CVVQeQxX.js} +2 -2
- package/dist/{request-context-CDEdBceh.js → request-context-BL0hymyD.js} +2 -2
- package/dist/{runtime-CwW75z2d.js → runtime-BSgtMZ6G.js} +19 -1
- package/dist/runtime-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
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-
|
|
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-
|
|
4
|
-
import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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`.
|
|
431
|
-
*
|
|
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
|
-
*
|
|
473
|
-
*
|
|
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/
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
|
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
|
-
* -
|
|
671
|
-
*
|
|
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
|
-
* -
|
|
677
|
-
*
|
|
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
|
-
|
|
695
|
-
|
|
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
|
|
705
|
-
*
|
|
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 =
|
|
712
|
-
if (data
|
|
775
|
+
const data = createCredentialBackupStore().lookup(credentialBackupKey(accountId));
|
|
776
|
+
if (isUsableBackup(data)) return data;
|
|
713
777
|
}
|
|
714
|
-
const legacy =
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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-
|
|
926
|
+
gatewayModulePromise ??= import("./gateway-DNHe6a8p.js");
|
|
872
927
|
return gatewayModulePromise;
|
|
873
928
|
}
|
|
874
929
|
let outboundMessagingModulePromise;
|
|
875
930
|
function loadOutboundMessagingModule() {
|
|
876
|
-
outboundMessagingModulePromise ??= import("./outbound-
|
|
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 {
|
|
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-
|
|
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-
|
|
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 {
|
|
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-
|
|
6
|
-
import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-
|
|
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 {
|
|
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 —
|
|
174
|
+
* Ref-index store — SQLite KV-backed store for message reference index.
|
|
175
175
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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))
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
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]
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
store.
|
|
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 —
|
|
553
|
+
* Known user tracking — SQLite KV-backed store.
|
|
574
554
|
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
609
|
-
|
|
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
|
|
616
|
-
|
|
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
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
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
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
existing
|
|
642
|
-
existing
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
nickname
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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 —
|
|
655
|
+
* Gateway session persistence — SQLite KV-backed store.
|
|
664
656
|
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
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
|
-
|
|
697
|
-
let state =
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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
|
-
|
|
795
|
-
for (const filePath of getCandidateSessionPaths(accountId))
|
|
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 {
|
|
3
|
-
import { d as getMessageApi, i as accountToCreds } from "./runtime-
|
|
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 {
|
|
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-
|
|
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 {
|
|
2
|
-
import { w as debugLog } from "./runtime-
|
|
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
|
});
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-
|
|
1
|
+
import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-BSgtMZ6G.js";
|
|
2
2
|
export { getQQBotRuntime, setQQBotRuntime };
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/qqbot",
|
|
3
|
-
"version": "2026.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
48
|
+
"pluginApi": ">=2026.6.1-beta.2"
|
|
49
49
|
},
|
|
50
50
|
"build": {
|
|
51
|
-
"openclawVersion": "2026.
|
|
51
|
+
"openclawVersion": "2026.6.1-beta.2"
|
|
52
52
|
},
|
|
53
53
|
"release": {
|
|
54
54
|
"publishToClawHub": true,
|