@openclaw/qqbot 2026.5.24-beta.2 → 2026.5.26-beta.1

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 recordMessageReply, E as setOutboundAudioPort, S as getMessageReplyStats, T as OUTBOUND_ERROR_CODES, _ as sendVoice, a as sendText, b as checkMessageReplyLimit, d as buildMediaTarget, f as parseTarget, g as sendVideoMsg, h as sendPhoto, i as sendProactiveMessage, m as sendDocument, n as sendCronMessage, p as resolveOutboundMediaPath, r as sendMedia, v as resolveUserFacingMediaError, w as DEFAULT_MEDIA_SEND_ERROR, x as getMessageReplyConfig, y as MESSAGE_REPLY_LIMIT } from "./outbound-CVDwCPRR.js";
2
- import { c as listQQBotAccountIds, l as resolveDefaultQQBotAccountId, o as DEFAULT_ACCOUNT_ID, s as applyQQBotAccountConfig, u as resolveQQBotAccount } from "./config-schema-CfbaUZcI.js";
3
- import { t as qqbotPlugin } from "./channel-M_xgk02s.js";
4
- import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-CMJ5DY9K.js";
5
- import { t as qqbotSetupPlugin } from "./channel.setup-CNTkQaYC.js";
6
- import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-ykvLAJFX.js";
1
+ import { C as recordMessageReply, E as setOutboundAudioPort, S as getMessageReplyStats, T as OUTBOUND_ERROR_CODES, _ as sendVoice, a as sendText, b as checkMessageReplyLimit, d as buildMediaTarget, f as parseTarget, g as sendVideoMsg, h as sendPhoto, i as sendProactiveMessage, m as sendDocument, n as sendCronMessage, p as resolveOutboundMediaPath, r as sendMedia, v as resolveUserFacingMediaError, w as DEFAULT_MEDIA_SEND_ERROR, x as getMessageReplyConfig, y as MESSAGE_REPLY_LIMIT } from "./outbound-Ce2aX078.js";
2
+ import { c as listQQBotAccountIds, l as resolveDefaultQQBotAccountId, o as DEFAULT_ACCOUNT_ID, s as applyQQBotAccountConfig, u as resolveQQBotAccount } from "./config-schema-iX2iJzKm.js";
3
+ import { t as qqbotPlugin } from "./channel-DtN9gbLm.js";
4
+ import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-TzkQ0YbR.js";
5
+ import { t as qqbotSetupPlugin } from "./channel.setup-BxGpVkdx.js";
6
+ import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-D-rKRREP.js";
7
7
  import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
8
8
  //#region extensions/qqbot/src/engine/tools/channel-api.ts
9
9
  /**
@@ -1,6 +1,6 @@
1
- import { c as getPlatformAdapter, t as asOptionalObjectRecord } from "./string-normalize-du8a5Uk3.js";
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-CfbaUZcI.js";
3
- import { T as debugWarn, t as getQQBotRuntime, w as debugLog, z as formatErrorMessage } from "./runtime-CMJ5DY9K.js";
1
+ import { c as getPlatformAdapter, t as asOptionalObjectRecord } from "./string-normalize-R_0cKO7Q.js";
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-iX2iJzKm.js";
3
+ import { T as debugWarn, t as getQQBotRuntime, w as debugLog, z as formatErrorMessage } from "./runtime-TzkQ0YbR.js";
4
4
  import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
5
5
  import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
6
6
  import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
@@ -374,7 +374,7 @@ function createQQBotApprovalCapability() {
374
374
  },
375
375
  load: async () => {
376
376
  ensurePlatformAdapter();
377
- return (await import("./handler-runtime-BcPUK_xf.js")).qqbotApprovalNativeRuntime;
377
+ return (await import("./handler-runtime-I6vCvj2m.js")).qqbotApprovalNativeRuntime;
378
378
  }
379
379
  })
380
380
  });
@@ -419,12 +419,17 @@ async function writeOpenClawConfigThroughRuntime(runtime, cfg) {
419
419
  * The temp-directory fallback is delegated to the PlatformAdapter.
420
420
  */
421
421
  /**
422
- * Resolve the current user's home directory safely across platforms.
422
+ * Resolve the current user's OS home directory safely across platforms.
423
423
  *
424
424
  * Priority:
425
425
  * 1. `os.homedir()`
426
426
  * 2. `$HOME` or `%USERPROFILE%`
427
427
  * 3. PlatformAdapter.getTempDir() as a last resort
428
+ *
429
+ * 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`.
428
433
  */
429
434
  function getHomeDir() {
430
435
  try {
@@ -435,7 +440,39 @@ function getHomeDir() {
435
440
  if (envHome && fs$1.existsSync(envHome)) return envHome;
436
441
  return getPlatformAdapter().getTempDir();
437
442
  }
438
- /** Return a path under `~/.openclaw/qqbot` without creating it. */
443
+ /**
444
+ * Resolve the effective OpenClaw home directory.
445
+ *
446
+ * Mirrors the contract from core (`src/infra/home-dir.ts::resolveEffectiveHomeDir`)
447
+ * so QQ Bot media roots live under the same tree the rest of OpenClaw treats as
448
+ * `~`. The extension cannot import the core helper directly (it is a separate
449
+ * package with `openclaw` as a peer dependency), so this re-implements the
450
+ * minimal contract:
451
+ *
452
+ * 1. `OPENCLAW_HOME` when set (with `~` / `~/...` expanded against the OS home).
453
+ * 2. Otherwise fall back to {@link getHomeDir} so existing single-home
454
+ * deployments are unaffected.
455
+ *
456
+ * Empty / `"undefined"` / `"null"` strings are treated as unset to match how
457
+ * core normalizes the variable.
458
+ */
459
+ function resolveOpenClawHome() {
460
+ const raw = process.env.OPENCLAW_HOME?.trim();
461
+ if (!raw || raw === "undefined" || raw === "null") return getHomeDir();
462
+ if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
463
+ const osHome = getHomeDir();
464
+ if (raw === "~") return osHome;
465
+ return path$1.join(osHome, raw.slice(2));
466
+ }
467
+ return raw;
468
+ }
469
+ /**
470
+ * Return a path under `~/.openclaw/qqbot` without creating it.
471
+ *
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.
475
+ */
439
476
  function getQQBotDataPath(...subPaths) {
440
477
  return path$1.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
441
478
  }
@@ -446,32 +483,36 @@ function getQQBotDataDir(...subPaths) {
446
483
  return dir;
447
484
  }
448
485
  /**
449
- * Return a path under `~/.openclaw/media/qqbot` without creating it.
486
+ * Return a path under `<openclaw-home>/.openclaw/media/qqbot` without creating it.
450
487
  *
451
- * Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist so
452
- * downloaded images and audio can be accessed by framework media tooling.
488
+ * Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist
489
+ * so downloaded images and audio can be accessed by framework media tooling.
490
+ * The base honors `OPENCLAW_HOME` (when set) so files written by agents into
491
+ * the OpenClaw-managed media tree are reachable by this plugin even when
492
+ * `HOME` and `OPENCLAW_HOME` differ (Docker, multi-user hosts). Fixes #83562.
453
493
  */
454
494
  function getQQBotMediaPath(...subPaths) {
455
- return path$1.join(getHomeDir(), ".openclaw", "media", "qqbot", ...subPaths);
495
+ return path$1.join(resolveOpenClawHome(), ".openclaw", "media", "qqbot", ...subPaths);
456
496
  }
457
- /** Return a path under `~/.openclaw/media/qqbot`, creating it on demand. */
497
+ /** Return a path under `<openclaw-home>/.openclaw/media/qqbot`, creating it on demand. */
458
498
  function getQQBotMediaDir(...subPaths) {
459
499
  const dir = getQQBotMediaPath(...subPaths);
460
500
  if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
461
501
  return dir;
462
502
  }
463
503
  /**
464
- * Return `~/.openclaw/media`, OpenClaw's shared media root.
504
+ * Return `<openclaw-home>/.openclaw/media`, OpenClaw's shared media root.
465
505
  *
466
506
  * This mirrors the directory that core's `buildMediaLocalRoots` exposes as an
467
507
  * allowlisted location (see `openclaw/src/media/local-roots.ts`). Using it as a
468
508
  * QQ Bot payload root lets the plugin trust framework-produced files that live
469
509
  * in sibling subdirectories such as `outbound/` (written by
470
510
  * `saveMediaBuffer(..., "outbound", ...)`) or `inbound/`, while still keeping
471
- * the check anchored to a single, well-known directory.
511
+ * the check anchored to a single, well-known directory. Like
512
+ * {@link getQQBotMediaPath}, the base honors `OPENCLAW_HOME`.
472
513
  */
473
514
  function getOpenClawMediaDir() {
474
- return path$1.join(getHomeDir(), ".openclaw", "media");
515
+ return path$1.join(resolveOpenClawHome(), ".openclaw", "media");
475
516
  }
476
517
  function isWindows() {
477
518
  return process.platform === "win32";
@@ -533,14 +574,15 @@ function isPathWithinRoot(candidate, root) {
533
574
  function resolveQQBotLocalMediaPath(p) {
534
575
  const normalized = normalizePath(p);
535
576
  if (!isLocalPath(normalized) || fs$1.existsSync(normalized)) return normalized;
536
- const homeDir = getHomeDir();
577
+ const osHomeDir = getHomeDir();
578
+ const openclawHomeDir = resolveOpenClawHome();
537
579
  const mediaRoot = getQQBotMediaPath();
538
580
  const dataRoot = getQQBotDataPath();
539
581
  const candidateRoots = [
540
- {
541
- from: path$1.join(homeDir, ".openclaw", "workspace", "qqbot"),
582
+ ...Array.from(new Set([path$1.join(osHomeDir, ".openclaw", "workspace", "qqbot"), path$1.join(openclawHomeDir, ".openclaw", "workspace", "qqbot")])).map((from) => ({
583
+ from,
542
584
  to: mediaRoot
543
- },
585
+ })),
544
586
  {
545
587
  from: dataRoot,
546
588
  to: mediaRoot
@@ -826,7 +868,7 @@ function looksLikeQQBotTarget(id) {
826
868
  //#region extensions/qqbot/src/channel.ts
827
869
  let gatewayModulePromise;
828
870
  function loadGatewayModule() {
829
- gatewayModulePromise ??= import("./gateway-CkzNqy_N.js");
871
+ gatewayModulePromise ??= import("./gateway-DuIz6XAh.js");
830
872
  return gatewayModulePromise;
831
873
  }
832
874
  function createQQBotSendReceipt(params) {
@@ -844,7 +886,7 @@ function createQQBotSendReceipt(params) {
844
886
  async function sendQQBotText(params) {
845
887
  await loadGatewayModule();
846
888
  const account = resolveQQBotAccount(params.cfg, params.accountId);
847
- const { sendText } = await import("./outbound-CVDwCPRR.js").then((n) => n.t);
889
+ const { sendText } = await import("./outbound-Ce2aX078.js").then((n) => n.t);
848
890
  const result = await sendText({
849
891
  to: params.to,
850
892
  text: params.text,
@@ -866,7 +908,7 @@ async function sendQQBotText(params) {
866
908
  async function sendQQBotMedia(params) {
867
909
  await loadGatewayModule();
868
910
  const account = resolveQQBotAccount(params.cfg, params.accountId);
869
- const { sendMedia } = await import("./outbound-CVDwCPRR.js").then((n) => n.t);
911
+ const { sendMedia } = await import("./outbound-Ce2aX078.js").then((n) => n.t);
870
912
  const result = await sendMedia({
871
913
  to: params.to,
872
914
  text: params.text ?? "",
@@ -1,2 +1,2 @@
1
- import { t as qqbotPlugin } from "./channel-M_xgk02s.js";
1
+ import { t as qqbotPlugin } from "./channel-DtN9gbLm.js";
2
2
  export { qqbotPlugin };
@@ -1,4 +1,4 @@
1
- import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-CfbaUZcI.js";
1
+ import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-iX2iJzKm.js";
2
2
  //#region extensions/qqbot/src/channel.setup.ts
3
3
  /**
4
4
  * Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
@@ -1,4 +1,4 @@
1
- import { a as normalizeStringifiedOptionalString, c as getPlatformAdapter, d as registerPlatformAdapterFactory, l as hasPlatformAdapter, n as normalizeLowercaseStringOrEmpty, o as readStringField, r as normalizeOptionalLowercaseString, t as asOptionalObjectRecord, u as registerPlatformAdapter } from "./string-normalize-du8a5Uk3.js";
1
+ import { a as normalizeStringifiedEntries, c as getPlatformAdapter, d as registerPlatformAdapterFactory, l as hasPlatformAdapter, n as normalizeLowercaseStringOrEmpty, o as readStringField, r as normalizeOptionalLowercaseString, t as asOptionalObjectRecord, u as registerPlatformAdapter } from "./string-normalize-R_0cKO7Q.js";
2
2
  import { buildSecretInputSchema, coerceSecretRef, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
3
3
  import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
4
4
  import fs from "node:fs";
@@ -285,7 +285,7 @@ function describeAccount(account) {
285
285
  }
286
286
  /** Normalize allowFrom entries into uppercase strings without the qqbot: prefix. */
287
287
  function formatAllowFrom(allowFrom) {
288
- return (allowFrom ?? []).map((entry) => normalizeStringifiedOptionalString(entry)).filter((entry) => Boolean(entry)).map((entry) => entry.replace(/^qqbot:/i, "")).map((entry) => entry.toUpperCase());
288
+ return normalizeStringifiedEntries(allowFrom ?? []).map((entry) => entry.replace(/^qqbot:/i, "")).map((entry) => entry.toUpperCase());
289
289
  }
290
290
  //#endregion
291
291
  //#region extensions/qqbot/src/bridge/config.ts
@@ -1,9 +1,10 @@
1
- import { E as setOutboundAudioPort, _ as sendVoice, c as isMediaPayload, g as sendVideoMsg, h as sendPhoto, l as parseQQBotPayload, m as sendDocument, o as encodePayloadForCron, r as sendMedia, s as isCronReminderPayload, u as normalizeMediaTags, v as resolveUserFacingMediaError, w as DEFAULT_MEDIA_SEND_ERROR } from "./outbound-CVDwCPRR.js";
2
- import { c as getPlatformAdapter, i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-du8a5Uk3.js";
3
- import { f as resolveAccountBase, h as setBridgeLogger, p as ensurePlatformAdapter } from "./config-schema-CfbaUZcI.js";
4
- import { C as parseApprovalButtonData, a as getQQBotDataDir, d as isWindows, f as normalizePath$1, 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-M_xgk02s.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-CMJ5DY9K.js";
6
- import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-ykvLAJFX.js";
1
+ import { E as setOutboundAudioPort, _ as sendVoice, c as isMediaPayload, g as sendVideoMsg, h as sendPhoto, l as parseQQBotPayload, m as sendDocument, o as encodePayloadForCron, r as sendMedia, s as isCronReminderPayload, u as normalizeMediaTags, v as resolveUserFacingMediaError, w as DEFAULT_MEDIA_SEND_ERROR } from "./outbound-Ce2aX078.js";
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
+ import { f as resolveAccountBase, h as setBridgeLogger, p as ensurePlatformAdapter } from "./config-schema-iX2iJzKm.js";
4
+ import { C as parseApprovalButtonData, a as getQQBotDataDir, d as isWindows, f as normalizePath$1, 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-DtN9gbLm.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-TzkQ0YbR.js";
6
+ import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-D-rKRREP.js";
7
+ import { asBoolean, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
7
8
  import * as fs$1 from "node:fs";
8
9
  import fs from "node:fs";
9
10
  import * as os$1 from "node:os";
@@ -1822,7 +1823,7 @@ async function transcribeAudio(audioPath, cfg) {
1822
1823
  const detail = await resp.text().catch(() => "");
1823
1824
  throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
1824
1825
  }
1825
- return normalizeOptionalString((await resp.json()).text) ?? null;
1826
+ return normalizeOptionalString$1((await resp.json()).text) ?? null;
1826
1827
  } finally {
1827
1828
  await release();
1828
1829
  }
@@ -1878,7 +1879,7 @@ async function processAttachments(attachments, ctx) {
1878
1879
  };
1879
1880
  });
1880
1881
  const processTasks = (await Promise.all(downloadTasks)).map(async ({ att, attUrl, isVoice, localPath, audioPath }) => {
1881
- const asrReferText = normalizeOptionalString(att.asr_refer_text) ?? "";
1882
+ const asrReferText = normalizeOptionalString$1(att.asr_refer_text) ?? "";
1882
1883
  const voiceSourceUrl = (isVoice && att.voice_wav_url ? att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url : "") || attUrl;
1883
1884
  const meta = {
1884
1885
  voiceUrl: isVoice && voiceSourceUrl ? voiceSourceUrl : void 0,
@@ -2450,6 +2451,14 @@ function escapeRegex(str) {
2450
2451
  }
2451
2452
  //#endregion
2452
2453
  //#region extensions/qqbot/src/engine/gateway/stages/envelope-stage.ts
2454
+ /**
2455
+ * Envelope stage — render the Web UI body, the dynamic-context block,
2456
+ * the final group system prompt, and the media classification arrays.
2457
+ *
2458
+ * All logic here is presentation-layer glue: it combines fields built by
2459
+ * earlier stages into the display-friendly strings the outbound
2460
+ * dispatcher needs. No decisions / gating.
2461
+ */
2453
2462
  /** Format the inbound envelope (Web UI body). */
2454
2463
  function buildBody(input) {
2455
2464
  const { event, deps, userContent, isGroupChat, imageUrls } = input;
@@ -2506,8 +2515,8 @@ function classifyMedia(processed) {
2506
2515
  localMediaTypes.push(t);
2507
2516
  }
2508
2517
  }
2509
- const uniqueVoicePaths = [...new Set(processed.voiceAttachmentPaths)];
2510
- const uniqueVoiceUrls = [...new Set(processed.voiceAttachmentUrls)];
2518
+ const uniqueVoicePaths = uniqueStrings(processed.voiceAttachmentPaths);
2519
+ const uniqueVoiceUrls = uniqueStrings(processed.voiceAttachmentUrls);
2511
2520
  const voiceMediaTypes = [...uniqueVoicePaths, ...uniqueVoiceUrls].map(() => "audio/wav");
2512
2521
  return {
2513
2522
  localMediaPaths,
@@ -2516,7 +2525,7 @@ function classifyMedia(processed) {
2516
2525
  remoteMediaTypes,
2517
2526
  uniqueVoicePaths,
2518
2527
  uniqueVoiceUrls,
2519
- uniqueVoiceAsrReferTexts: [...new Set(processed.voiceAsrReferTexts)].filter(Boolean),
2528
+ uniqueVoiceAsrReferTexts: uniqueStrings(processed.voiceAsrReferTexts).filter(Boolean),
2520
2529
  voiceMediaTypes,
2521
2530
  hasAsrReferFallback: processed.voiceTranscriptSources.includes("asr"),
2522
2531
  voiceTranscriptSources: processed.voiceTranscriptSources
@@ -2540,8 +2549,7 @@ function readGroupsMap(cfg, accountId) {
2540
2549
  return normalized;
2541
2550
  }
2542
2551
  function readBoolean(obj, key) {
2543
- const v = obj[key];
2544
- return typeof v === "boolean" ? v : void 0;
2552
+ return asBoolean(obj[key]);
2545
2553
  }
2546
2554
  function readString(obj, key) {
2547
2555
  const v = obj[key];
@@ -3209,11 +3217,10 @@ function authorizeApprovalButtonActor(params) {
3209
3217
  };
3210
3218
  }
3211
3219
  function resolveApprovalActorSenderIds(event) {
3212
- const ids = [event.group_member_openid, event.user_openid].flatMap((value) => {
3220
+ return uniqueStrings([event.group_member_openid, event.user_openid].flatMap((value) => {
3213
3221
  const normalized = typeof value === "string" ? value.trim() : "";
3214
3222
  return normalized ? [normalized] : [];
3215
- });
3216
- return Array.from(new Set(ids));
3223
+ }));
3217
3224
  }
3218
3225
  function resolveApprovalKind(approvalId) {
3219
3226
  return approvalId.toLowerCase().startsWith("plugin:") ? "plugin" : "exec";
@@ -3589,7 +3596,7 @@ async function parseAndSendMediaTags(replyText, event, actx, sendWithRetry, cons
3589
3596
  normalizedText: text
3590
3597
  };
3591
3598
  const tagCounts = mediaTagMatches.reduce((acc, m) => {
3592
- const t = normalizeLowercaseStringOrEmpty(m[1]);
3599
+ const t = normalizeLowercaseStringOrEmpty$1(m[1]);
3593
3600
  acc[t] = (acc[t] ?? 0) + 1;
3594
3601
  return acc;
3595
3602
  }, {});
@@ -3604,8 +3611,8 @@ async function parseAndSendMediaTags(replyText, event, actx, sendWithRetry, cons
3604
3611
  type: "text",
3605
3612
  content: filterInternalMarkers(textBefore)
3606
3613
  });
3607
- const tagName = normalizeLowercaseStringOrEmpty(match[1]);
3608
- const mediaPath = decodeMediaPath(normalizeOptionalString(match[2]) ?? "", log);
3614
+ const tagName = normalizeLowercaseStringOrEmpty$1(match[1]);
3615
+ const mediaPath = decodeMediaPath(normalizeOptionalString$1(match[2]) ?? "", log);
3609
3616
  if (mediaPath) {
3610
3617
  const itemType = {
3611
3618
  qqmedia: "media",
@@ -4026,7 +4033,7 @@ async function handleImagePayload(ctx, payload) {
4026
4033
  const base64Data = fileBuffer.toString("base64");
4027
4034
  const mimeType = getImageMimeType(imageUrl);
4028
4035
  if (!mimeType) {
4029
- const ext = normalizeLowercaseStringOrEmpty(path.extname(imageUrl));
4036
+ const ext = normalizeLowercaseStringOrEmpty$1(path.extname(imageUrl));
4030
4037
  log?.error(`Unsupported image format: ${ext}`);
4031
4038
  return;
4032
4039
  }
@@ -5282,7 +5289,7 @@ async function convertSilkToWav(inputPath, outputDir) {
5282
5289
  /** Check whether an attachment is a voice file (by MIME type or extension). */
5283
5290
  function isVoiceAttachment(att) {
5284
5291
  if (att.content_type === "voice" || att.content_type?.startsWith("audio/")) return true;
5285
- const ext = att.filename ? normalizeLowercaseStringOrEmpty(path$1.extname(att.filename)) : "";
5292
+ const ext = att.filename ? normalizeLowercaseStringOrEmpty$1(path$1.extname(att.filename)) : "";
5286
5293
  return [
5287
5294
  ".amr",
5288
5295
  ".silk",
@@ -5295,7 +5302,7 @@ function isAudioFile(filePath, mimeType) {
5295
5302
  if (mimeType) {
5296
5303
  if (mimeType === "voice" || mimeType.startsWith("audio/")) return true;
5297
5304
  }
5298
- const ext = normalizeLowercaseStringOrEmpty(path$1.extname(filePath));
5305
+ const ext = normalizeLowercaseStringOrEmpty$1(path$1.extname(filePath));
5299
5306
  return [
5300
5307
  ".silk",
5301
5308
  ".slk",
@@ -5329,8 +5336,8 @@ const QQ_NATIVE_VOICE_EXTS = new Set([
5329
5336
  ]);
5330
5337
  /** Check whether a voice file needs transcoding for upload (QQ-native formats skip it). */
5331
5338
  function shouldTranscodeVoice(filePath, mimeType) {
5332
- if (mimeType && QQ_NATIVE_VOICE_MIMES.has(normalizeLowercaseStringOrEmpty(mimeType))) return false;
5333
- const ext = normalizeLowercaseStringOrEmpty(path$1.extname(filePath));
5339
+ if (mimeType && QQ_NATIVE_VOICE_MIMES.has(normalizeLowercaseStringOrEmpty$1(mimeType))) return false;
5340
+ const ext = normalizeLowercaseStringOrEmpty$1(path$1.extname(filePath));
5334
5341
  if (QQ_NATIVE_VOICE_EXTS.has(ext)) return false;
5335
5342
  return isAudioFile(filePath, mimeType);
5336
5343
  }
@@ -5341,7 +5348,7 @@ const QQ_NATIVE_UPLOAD_FORMATS = [
5341
5348
  ];
5342
5349
  function normalizeFormats(formats) {
5343
5350
  return formats.map((f) => {
5344
- const lower = normalizeLowercaseStringOrEmpty(f);
5351
+ const lower = normalizeLowercaseStringOrEmpty$1(f);
5345
5352
  return lower.startsWith(".") ? lower : `.${lower}`;
5346
5353
  });
5347
5354
  }
@@ -5361,7 +5368,7 @@ async function audioFileToSilkBase64(filePath, directUploadFormats) {
5361
5368
  debugError(`[audio-convert] file is empty: ${filePath}`);
5362
5369
  return null;
5363
5370
  }
5364
- const ext = normalizeLowercaseStringOrEmpty(path$1.extname(filePath));
5371
+ const ext = normalizeLowercaseStringOrEmpty$1(path$1.extname(filePath));
5365
5372
  if ((directUploadFormats ? normalizeFormats(directUploadFormats) : QQ_NATIVE_UPLOAD_FORMATS).includes(ext)) {
5366
5373
  debugLog(`[audio-convert] direct upload (QQ native format): ${ext} (${buf.length} bytes)`);
5367
5374
  return buf.toString("base64");
@@ -5586,8 +5593,75 @@ function parseWavFallback(buf) {
5586
5593
  return null;
5587
5594
  }
5588
5595
  //#endregion
5596
+ //#region extensions/qqbot/src/engine/gateway/response-timeout.ts
5597
+ /**
5598
+ * QQBot outbound response watchdog timeout resolver.
5599
+ *
5600
+ * Background — issue #85267:
5601
+ * The reporter ran openclaw + ollama + `qwen3.5:27b` (a slow local model)
5602
+ * with `models.providers.ollama.timeoutSeconds: 1800` and saw the
5603
+ * QQBot reply path abort at ~5 minutes with "LLM request timed out",
5604
+ * despite the direct ollama call to the same model working. The
5605
+ * embedded-runner / idle-timeout layer already honors longer
5606
+ * provider timeouts (see `src/agents/pi-embedded-runner/run/llm-idle-timeout.ts`),
5607
+ * but the QQBot outbound dispatcher held an independent hardcoded
5608
+ * `RESPONSE_TIMEOUT = 300_000` watchdog that quietly undercut the
5609
+ * configured ceiling.
5610
+ *
5611
+ * Fix shape (clawsweeper `clawsweeper:fix-shape-clear`):
5612
+ * Don't add a new QQBot-only knob. Instead derive the QQBot wait
5613
+ * budget from the existing agent/provider timeout settings the user
5614
+ * already configured:
5615
+ * - `agents.defaults.timeoutSeconds`
5616
+ * - `models.providers.<id>.timeoutSeconds` (max across configured providers)
5617
+ * Take the maximum and clamp to `[DEFAULT_RESPONSE_TIMEOUT_MS, MAX_SAFE_TIMEOUT_MS]`.
5618
+ * The default floor preserves the existing 5-minute guard for users
5619
+ * that have not configured any longer ceiling — i.e. a no-op for
5620
+ * typical cloud-model deployments.
5621
+ */
5622
+ /**
5623
+ * Default QQBot outbound response watchdog when no config override is
5624
+ * present. Preserves the historical 5-minute guard for unconfigured
5625
+ * deployments.
5626
+ */
5627
+ const DEFAULT_RESPONSE_TIMEOUT_MS = 3e5;
5628
+ /**
5629
+ * Upper bound to keep the watchdog inside the safe `setTimeout` range
5630
+ * (approximately 24.8 days). Mirrors `MAX_SAFE_TIMEOUT_MS` in
5631
+ * `src/agents/pi-embedded-runner/run/llm-idle-timeout.ts`.
5632
+ */
5633
+ const MAX_SAFE_TIMEOUT_MS = 2147e6;
5634
+ function positiveSecondsToMs(value) {
5635
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
5636
+ return Math.floor(value * 1e3);
5637
+ }
5638
+ /**
5639
+ * Resolve the QQBot outbound response watchdog (ms).
5640
+ *
5641
+ * The watchdog is the longest of:
5642
+ * - `DEFAULT_RESPONSE_TIMEOUT_MS` (5 min, historical floor)
5643
+ * - `cfg.agents.defaults.timeoutSeconds` converted to ms
5644
+ * - the maximum `cfg.models.providers.<id>.timeoutSeconds` across
5645
+ * configured providers, converted to ms
5646
+ *
5647
+ * Returns at most `MAX_SAFE_TIMEOUT_MS` so the chosen value is always
5648
+ * a safe `setTimeout` argument.
5649
+ */
5650
+ function resolveResponseTimeoutMs(cfg) {
5651
+ const candidates = [DEFAULT_RESPONSE_TIMEOUT_MS];
5652
+ const typed = cfg ?? {};
5653
+ const agentDefaultMs = positiveSecondsToMs(typed.agents?.defaults?.timeoutSeconds);
5654
+ if (agentDefaultMs !== void 0) candidates.push(agentDefaultMs);
5655
+ const providers = typed.models?.providers;
5656
+ if (providers && typeof providers === "object") for (const entry of Object.values(providers)) {
5657
+ const providerMs = positiveSecondsToMs(entry?.timeoutSeconds);
5658
+ if (providerMs !== void 0) candidates.push(providerMs);
5659
+ }
5660
+ const chosen = Math.max(...candidates);
5661
+ return Math.min(chosen, MAX_SAFE_TIMEOUT_MS);
5662
+ }
5663
+ //#endregion
5589
5664
  //#region extensions/qqbot/src/engine/gateway/outbound-dispatch.ts
5590
- const RESPONSE_TIMEOUT = 3e5;
5591
5665
  const TOOL_ONLY_TIMEOUT = 6e4;
5592
5666
  const MAX_TOOL_RENEWALS = 3;
5593
5667
  const TOOL_MEDIA_SEND_TIMEOUT = 45e3;
@@ -5659,10 +5733,11 @@ async function dispatchOutbound(inbound, deps) {
5659
5733
  }
5660
5734
  if (toolTexts.length > 0) await sendErrorMessage(toolTexts.slice(-3).join("\n---\n").slice(0, 2e3));
5661
5735
  };
5736
+ const responseTimeoutMs = resolveResponseTimeoutMs(cfg);
5662
5737
  const timeoutPromise = new Promise((_, reject) => {
5663
5738
  timeoutId = setTimeout(() => {
5664
5739
  if (!hasResponse) reject(/* @__PURE__ */ new Error("Response timeout"));
5665
- }, RESPONSE_TIMEOUT);
5740
+ }, responseTimeoutMs);
5666
5741
  });
5667
5742
  const deliverDeps = {
5668
5743
  mediaSender: {
@@ -1,6 +1,6 @@
1
- import { m as getBridgeLogger, p as ensurePlatformAdapter, u as resolveQQBotAccount } from "./config-schema-CfbaUZcI.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-M_xgk02s.js";
3
- import { d as getMessageApi, i as accountToCreds } from "./runtime-CMJ5DY9K.js";
1
+ import { m as getBridgeLogger, p as ensurePlatformAdapter, u as resolveQQBotAccount } from "./config-schema-iX2iJzKm.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-DtN9gbLm.js";
3
+ import { d as getMessageApi, i as accountToCreds } from "./runtime-TzkQ0YbR.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
- import { i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-du8a5Uk3.js";
2
- import { a as getQQBotDataDir, f as normalizePath, n as parseTarget$1, p as resolveQQBotPayloadLocalFilePath, s as getQQBotMediaDir, u as isLocalPath } from "./channel-M_xgk02s.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-CMJ5DY9K.js";
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, n as parseTarget$1, p as resolveQQBotPayloadLocalFilePath, s as getQQBotMediaDir, u as isLocalPath } from "./channel-DtN9gbLm.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-TzkQ0YbR.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,6 @@
1
- import { a as getQQBotDataDir, c as getQQBotMediaPath, d as isWindows, i as getHomeDir } from "./channel-M_xgk02s.js";
2
- import { w as debugLog } from "./runtime-CMJ5DY9K.js";
1
+ import { a as getQQBotDataDir, c as getQQBotMediaPath, d as isWindows, i as getHomeDir } from "./channel-DtN9gbLm.js";
2
+ import { w as debugLog } from "./runtime-TzkQ0YbR.js";
3
+ import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
@@ -648,7 +649,7 @@ function buildBotLogsResult() {
648
649
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
649
650
  const tmpFile = writeNewTextFileSync(path.join(tmpDir, `bot-logs-${timestamp}.txt`), lines.join("\n"));
650
651
  const fileCount = recentFiles.length;
651
- const topSources = Array.from(new Set(recentFiles.map((item) => item.sourceDir))).slice(0, 3);
652
+ const topSources = uniqueStrings(recentFiles.map((item) => item.sourceDir)).slice(0, 3);
652
653
  let summaryText = `共 ${fileCount} 个日志文件,包含 ${totalIncluded} 行内容`;
653
654
  if (truncatedCount > 0) summaryText += `(其中 ${truncatedCount} 个文件已截断为最后 ${MAX_LINES_PER_FILE} 行,总计原始 ${totalOriginal} 行)`;
654
655
  return {
@@ -1,4 +1,4 @@
1
- import { c as getPlatformAdapter, i as normalizeOptionalString, s as sanitizeFileName } from "./string-normalize-du8a5Uk3.js";
1
+ import { c as getPlatformAdapter, i as normalizeOptionalString, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
2
2
  import * as fs$1 from "node:fs";
3
3
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
4
4
  import os from "node:os";
@@ -985,6 +985,194 @@ function sleep(ms) {
985
985
  return new Promise((resolve) => setTimeout(resolve, ms));
986
986
  }
987
987
  //#endregion
988
+ //#region extensions/qqbot/src/engine/api/token.ts
989
+ const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
990
+ /**
991
+ * Per-appId token manager with caching, singleflight, and background refresh.
992
+ *
993
+ * Usage:
994
+ * ```ts
995
+ * const tm = new TokenManager({ logger, userAgent: 'QQBotPlugin/1.0' });
996
+ * const token = await tm.getAccessToken('appId', 'secret');
997
+ * ```
998
+ */
999
+ var TokenManager = class {
1000
+ constructor(config) {
1001
+ this.cache = /* @__PURE__ */ new Map();
1002
+ this.fetchPromises = /* @__PURE__ */ new Map();
1003
+ this.refreshControllers = /* @__PURE__ */ new Map();
1004
+ this.logger = config?.logger;
1005
+ const ua = config?.userAgent ?? "QQBotPlugin/unknown";
1006
+ this.resolveUserAgent = typeof ua === "function" ? ua : () => ua;
1007
+ }
1008
+ /**
1009
+ * Obtain an access token with caching and singleflight semantics.
1010
+ *
1011
+ * When multiple callers request a token for the same appId concurrently,
1012
+ * only one actual HTTP request is made — the others await the same promise.
1013
+ */
1014
+ async getAccessToken(appId, clientSecret) {
1015
+ const normalizedId = appId.trim();
1016
+ const cached = this.cache.get(normalizedId);
1017
+ const refreshAheadMs = cached ? Math.min(300 * 1e3, (cached.expiresAt - Date.now()) / 3) : 0;
1018
+ if (cached && Date.now() < cached.expiresAt - refreshAheadMs) return cached.token;
1019
+ let pending = this.fetchPromises.get(normalizedId);
1020
+ if (pending) {
1021
+ this.logger?.debug?.(`[qqbot:token:${normalizedId}] Fetch in progress, reusing promise`);
1022
+ return pending;
1023
+ }
1024
+ pending = (async () => {
1025
+ try {
1026
+ return await this.doFetchToken(normalizedId, clientSecret);
1027
+ } finally {
1028
+ this.fetchPromises.delete(normalizedId);
1029
+ }
1030
+ })();
1031
+ this.fetchPromises.set(normalizedId, pending);
1032
+ return pending;
1033
+ }
1034
+ /** Clear the cached token for one appId, or all. */
1035
+ clearCache(appId) {
1036
+ if (appId) {
1037
+ this.cache.delete(appId.trim());
1038
+ this.logger?.debug?.(`[qqbot:token:${appId}] Cache cleared`);
1039
+ } else {
1040
+ this.cache.clear();
1041
+ this.logger?.debug?.(`[token] All caches cleared`);
1042
+ }
1043
+ }
1044
+ /** Return token status for diagnostics. */
1045
+ getStatus(appId) {
1046
+ if (this.fetchPromises.has(appId)) return {
1047
+ status: "refreshing",
1048
+ expiresAt: this.cache.get(appId)?.expiresAt ?? null
1049
+ };
1050
+ const cached = this.cache.get(appId);
1051
+ if (!cached) return {
1052
+ status: "none",
1053
+ expiresAt: null
1054
+ };
1055
+ const remaining = cached.expiresAt - Date.now();
1056
+ return {
1057
+ status: remaining > Math.min(300 * 1e3, remaining / 3) ? "valid" : "expired",
1058
+ expiresAt: cached.expiresAt
1059
+ };
1060
+ }
1061
+ /** Start a background token refresh loop for one appId. */
1062
+ startBackgroundRefresh(appId, clientSecret, options) {
1063
+ if (this.refreshControllers.has(appId)) {
1064
+ this.logger?.info?.(`[qqbot:token:${appId}] Background refresh already running`);
1065
+ return;
1066
+ }
1067
+ const { refreshAheadMs = 300 * 1e3, randomOffsetMs = 30 * 1e3, minRefreshIntervalMs = 60 * 1e3, retryDelayMs = 5 * 1e3 } = options ?? {};
1068
+ const controller = new AbortController();
1069
+ this.refreshControllers.set(appId, controller);
1070
+ const { signal } = controller;
1071
+ const loop = async () => {
1072
+ this.logger?.info?.(`[qqbot:token:${appId}] Background refresh started`);
1073
+ while (!signal.aborted) try {
1074
+ await this.getAccessToken(appId, clientSecret);
1075
+ const cached = this.cache.get(appId);
1076
+ if (cached) {
1077
+ const expiresIn = cached.expiresAt - Date.now();
1078
+ const randomOffset = Math.random() * randomOffsetMs;
1079
+ const refreshIn = Math.max(expiresIn - refreshAheadMs - randomOffset, minRefreshIntervalMs);
1080
+ this.logger?.debug?.(`[qqbot:token:${appId}] Next refresh in ${Math.round(refreshIn / 1e3)}s`);
1081
+ await this.abortableSleep(refreshIn, signal);
1082
+ } else await this.abortableSleep(minRefreshIntervalMs, signal);
1083
+ } catch (err) {
1084
+ if (signal.aborted) break;
1085
+ this.logger?.error?.(`[qqbot:token:${appId}] Background refresh failed: ${formatErrorMessage(err)}`);
1086
+ await this.abortableSleep(retryDelayMs, signal);
1087
+ }
1088
+ this.refreshControllers.delete(appId);
1089
+ this.logger?.info?.(`[qqbot:token:${appId}] Background refresh stopped`);
1090
+ };
1091
+ loop().catch((err) => {
1092
+ this.refreshControllers.delete(appId);
1093
+ this.logger?.error?.(`[qqbot:token:${appId}] Background refresh crashed: ${err}`);
1094
+ });
1095
+ }
1096
+ /** Stop background refresh for one appId, or all. */
1097
+ stopBackgroundRefresh(appId) {
1098
+ if (appId) {
1099
+ const ctrl = this.refreshControllers.get(appId);
1100
+ if (ctrl) {
1101
+ ctrl.abort();
1102
+ this.refreshControllers.delete(appId);
1103
+ }
1104
+ } else {
1105
+ for (const ctrl of this.refreshControllers.values()) ctrl.abort();
1106
+ this.refreshControllers.clear();
1107
+ }
1108
+ }
1109
+ /** Check whether background refresh is running. */
1110
+ isBackgroundRefreshRunning(appId) {
1111
+ if (appId) return this.refreshControllers.has(appId);
1112
+ return this.refreshControllers.size > 0;
1113
+ }
1114
+ async doFetchToken(appId, clientSecret) {
1115
+ this.logger?.debug?.(`[qqbot:token:${appId}] >>> POST ${TOKEN_URL}`);
1116
+ let response;
1117
+ try {
1118
+ response = await fetch(TOKEN_URL, {
1119
+ method: "POST",
1120
+ headers: {
1121
+ "Content-Type": "application/json",
1122
+ "User-Agent": this.resolveUserAgent()
1123
+ },
1124
+ body: JSON.stringify({
1125
+ appId,
1126
+ clientSecret
1127
+ })
1128
+ });
1129
+ } catch (err) {
1130
+ this.logger?.error?.(`[qqbot:token:${appId}] Network error: ${formatErrorMessage(err)}`);
1131
+ throw new Error(`Network error getting access_token: ${formatErrorMessage(err)}`, { cause: err });
1132
+ }
1133
+ const traceId = response.headers.get("x-tps-trace-id") ?? "";
1134
+ this.logger?.debug?.(`[qqbot:token:${appId}] <<< ${response.status}${traceId ? ` | TraceId: ${traceId}` : ""}`);
1135
+ let rawBody;
1136
+ try {
1137
+ rawBody = await response.text();
1138
+ } catch (err) {
1139
+ throw new Error(`Failed to read access_token response: ${formatErrorMessage(err)}`, { cause: err });
1140
+ }
1141
+ const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, "\"access_token\": \"***\"");
1142
+ this.logger?.debug?.(`[qqbot:token:${appId}] <<< Body: ${logBody}`);
1143
+ let data;
1144
+ try {
1145
+ data = JSON.parse(rawBody);
1146
+ } catch {
1147
+ throw new Error("QQBot access_token response was malformed JSON");
1148
+ }
1149
+ if (!data.access_token) throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
1150
+ const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1e3;
1151
+ this.cache.set(appId, {
1152
+ token: data.access_token,
1153
+ expiresAt,
1154
+ appId
1155
+ });
1156
+ this.logger?.debug?.(`[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`);
1157
+ return data.access_token;
1158
+ }
1159
+ abortableSleep(ms, signal) {
1160
+ return new Promise((resolve, reject) => {
1161
+ const timer = setTimeout(resolve, ms);
1162
+ if (signal.aborted) {
1163
+ clearTimeout(timer);
1164
+ reject(/* @__PURE__ */ new Error("Aborted"));
1165
+ return;
1166
+ }
1167
+ const onAbort = () => {
1168
+ clearTimeout(timer);
1169
+ reject(/* @__PURE__ */ new Error("Aborted"));
1170
+ };
1171
+ signal.addEventListener("abort", onAbort, { once: true });
1172
+ });
1173
+ }
1174
+ };
1175
+ //#endregion
988
1176
  //#region extensions/qqbot/src/engine/api/media.ts
989
1177
  /**
990
1178
  * Media upload API for the QQ Open Platform (small-file direct upload).
@@ -1240,194 +1428,6 @@ var MessageApi = class {
1240
1428
  }
1241
1429
  };
1242
1430
  //#endregion
1243
- //#region extensions/qqbot/src/engine/api/token.ts
1244
- const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
1245
- /**
1246
- * Per-appId token manager with caching, singleflight, and background refresh.
1247
- *
1248
- * Usage:
1249
- * ```ts
1250
- * const tm = new TokenManager({ logger, userAgent: 'QQBotPlugin/1.0' });
1251
- * const token = await tm.getAccessToken('appId', 'secret');
1252
- * ```
1253
- */
1254
- var TokenManager = class {
1255
- constructor(config) {
1256
- this.cache = /* @__PURE__ */ new Map();
1257
- this.fetchPromises = /* @__PURE__ */ new Map();
1258
- this.refreshControllers = /* @__PURE__ */ new Map();
1259
- this.logger = config?.logger;
1260
- const ua = config?.userAgent ?? "QQBotPlugin/unknown";
1261
- this.resolveUserAgent = typeof ua === "function" ? ua : () => ua;
1262
- }
1263
- /**
1264
- * Obtain an access token with caching and singleflight semantics.
1265
- *
1266
- * When multiple callers request a token for the same appId concurrently,
1267
- * only one actual HTTP request is made — the others await the same promise.
1268
- */
1269
- async getAccessToken(appId, clientSecret) {
1270
- const normalizedId = appId.trim();
1271
- const cached = this.cache.get(normalizedId);
1272
- const refreshAheadMs = cached ? Math.min(300 * 1e3, (cached.expiresAt - Date.now()) / 3) : 0;
1273
- if (cached && Date.now() < cached.expiresAt - refreshAheadMs) return cached.token;
1274
- let pending = this.fetchPromises.get(normalizedId);
1275
- if (pending) {
1276
- this.logger?.debug?.(`[qqbot:token:${normalizedId}] Fetch in progress, reusing promise`);
1277
- return pending;
1278
- }
1279
- pending = (async () => {
1280
- try {
1281
- return await this.doFetchToken(normalizedId, clientSecret);
1282
- } finally {
1283
- this.fetchPromises.delete(normalizedId);
1284
- }
1285
- })();
1286
- this.fetchPromises.set(normalizedId, pending);
1287
- return pending;
1288
- }
1289
- /** Clear the cached token for one appId, or all. */
1290
- clearCache(appId) {
1291
- if (appId) {
1292
- this.cache.delete(appId.trim());
1293
- this.logger?.debug?.(`[qqbot:token:${appId}] Cache cleared`);
1294
- } else {
1295
- this.cache.clear();
1296
- this.logger?.debug?.(`[token] All caches cleared`);
1297
- }
1298
- }
1299
- /** Return token status for diagnostics. */
1300
- getStatus(appId) {
1301
- if (this.fetchPromises.has(appId)) return {
1302
- status: "refreshing",
1303
- expiresAt: this.cache.get(appId)?.expiresAt ?? null
1304
- };
1305
- const cached = this.cache.get(appId);
1306
- if (!cached) return {
1307
- status: "none",
1308
- expiresAt: null
1309
- };
1310
- const remaining = cached.expiresAt - Date.now();
1311
- return {
1312
- status: remaining > Math.min(300 * 1e3, remaining / 3) ? "valid" : "expired",
1313
- expiresAt: cached.expiresAt
1314
- };
1315
- }
1316
- /** Start a background token refresh loop for one appId. */
1317
- startBackgroundRefresh(appId, clientSecret, options) {
1318
- if (this.refreshControllers.has(appId)) {
1319
- this.logger?.info?.(`[qqbot:token:${appId}] Background refresh already running`);
1320
- return;
1321
- }
1322
- const { refreshAheadMs = 300 * 1e3, randomOffsetMs = 30 * 1e3, minRefreshIntervalMs = 60 * 1e3, retryDelayMs = 5 * 1e3 } = options ?? {};
1323
- const controller = new AbortController();
1324
- this.refreshControllers.set(appId, controller);
1325
- const { signal } = controller;
1326
- const loop = async () => {
1327
- this.logger?.info?.(`[qqbot:token:${appId}] Background refresh started`);
1328
- while (!signal.aborted) try {
1329
- await this.getAccessToken(appId, clientSecret);
1330
- const cached = this.cache.get(appId);
1331
- if (cached) {
1332
- const expiresIn = cached.expiresAt - Date.now();
1333
- const randomOffset = Math.random() * randomOffsetMs;
1334
- const refreshIn = Math.max(expiresIn - refreshAheadMs - randomOffset, minRefreshIntervalMs);
1335
- this.logger?.debug?.(`[qqbot:token:${appId}] Next refresh in ${Math.round(refreshIn / 1e3)}s`);
1336
- await this.abortableSleep(refreshIn, signal);
1337
- } else await this.abortableSleep(minRefreshIntervalMs, signal);
1338
- } catch (err) {
1339
- if (signal.aborted) break;
1340
- this.logger?.error?.(`[qqbot:token:${appId}] Background refresh failed: ${formatErrorMessage(err)}`);
1341
- await this.abortableSleep(retryDelayMs, signal);
1342
- }
1343
- this.refreshControllers.delete(appId);
1344
- this.logger?.info?.(`[qqbot:token:${appId}] Background refresh stopped`);
1345
- };
1346
- loop().catch((err) => {
1347
- this.refreshControllers.delete(appId);
1348
- this.logger?.error?.(`[qqbot:token:${appId}] Background refresh crashed: ${err}`);
1349
- });
1350
- }
1351
- /** Stop background refresh for one appId, or all. */
1352
- stopBackgroundRefresh(appId) {
1353
- if (appId) {
1354
- const ctrl = this.refreshControllers.get(appId);
1355
- if (ctrl) {
1356
- ctrl.abort();
1357
- this.refreshControllers.delete(appId);
1358
- }
1359
- } else {
1360
- for (const ctrl of this.refreshControllers.values()) ctrl.abort();
1361
- this.refreshControllers.clear();
1362
- }
1363
- }
1364
- /** Check whether background refresh is running. */
1365
- isBackgroundRefreshRunning(appId) {
1366
- if (appId) return this.refreshControllers.has(appId);
1367
- return this.refreshControllers.size > 0;
1368
- }
1369
- async doFetchToken(appId, clientSecret) {
1370
- this.logger?.debug?.(`[qqbot:token:${appId}] >>> POST ${TOKEN_URL}`);
1371
- let response;
1372
- try {
1373
- response = await fetch(TOKEN_URL, {
1374
- method: "POST",
1375
- headers: {
1376
- "Content-Type": "application/json",
1377
- "User-Agent": this.resolveUserAgent()
1378
- },
1379
- body: JSON.stringify({
1380
- appId,
1381
- clientSecret
1382
- })
1383
- });
1384
- } catch (err) {
1385
- this.logger?.error?.(`[qqbot:token:${appId}] Network error: ${formatErrorMessage(err)}`);
1386
- throw new Error(`Network error getting access_token: ${formatErrorMessage(err)}`, { cause: err });
1387
- }
1388
- const traceId = response.headers.get("x-tps-trace-id") ?? "";
1389
- this.logger?.debug?.(`[qqbot:token:${appId}] <<< ${response.status}${traceId ? ` | TraceId: ${traceId}` : ""}`);
1390
- let rawBody;
1391
- try {
1392
- rawBody = await response.text();
1393
- } catch (err) {
1394
- throw new Error(`Failed to read access_token response: ${formatErrorMessage(err)}`, { cause: err });
1395
- }
1396
- const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, "\"access_token\": \"***\"");
1397
- this.logger?.debug?.(`[qqbot:token:${appId}] <<< Body: ${logBody}`);
1398
- let data;
1399
- try {
1400
- data = JSON.parse(rawBody);
1401
- } catch {
1402
- throw new Error("QQBot access_token response was malformed JSON");
1403
- }
1404
- if (!data.access_token) throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
1405
- const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1e3;
1406
- this.cache.set(appId, {
1407
- token: data.access_token,
1408
- expiresAt,
1409
- appId
1410
- });
1411
- this.logger?.debug?.(`[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`);
1412
- return data.access_token;
1413
- }
1414
- abortableSleep(ms, signal) {
1415
- return new Promise((resolve, reject) => {
1416
- const timer = setTimeout(resolve, ms);
1417
- if (signal.aborted) {
1418
- clearTimeout(timer);
1419
- reject(/* @__PURE__ */ new Error("Aborted"));
1420
- return;
1421
- }
1422
- const onAbort = () => {
1423
- clearTimeout(timer);
1424
- reject(/* @__PURE__ */ new Error("Aborted"));
1425
- };
1426
- signal.addEventListener("abort", onAbort, { once: true });
1427
- });
1428
- }
1429
- };
1430
- //#endregion
1431
1431
  //#region extensions/qqbot/src/engine/utils/log.ts
1432
1432
  /**
1433
1433
  * QQBot debug logging utilities.
@@ -1,2 +1,2 @@
1
- import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-CMJ5DY9K.js";
1
+ import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-TzkQ0YbR.js";
2
2
  export { getQQBotRuntime, setQQBotRuntime };
@@ -1,2 +1,2 @@
1
- import { t as qqbotSetupPlugin } from "./channel.setup-CNTkQaYC.js";
1
+ import { t as qqbotSetupPlugin } from "./channel.setup-BxGpVkdx.js";
2
2
  export { qqbotSetupPlugin };
@@ -45,6 +45,9 @@ function normalizeStringifiedOptionalString(value) {
45
45
  if (typeof value === "string") return normalizeOptionalString(value);
46
46
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return normalizeOptionalString(String(value));
47
47
  }
48
+ function normalizeStringifiedEntries(values) {
49
+ return (values ?? []).map((entry) => normalizeStringifiedOptionalString(entry)).filter((entry) => Boolean(entry));
50
+ }
48
51
  /** Return the trimmed lowercase string or `undefined`. */
49
52
  function normalizeOptionalLowercaseString(value) {
50
53
  return normalizeOptionalString(value)?.toLowerCase();
@@ -79,4 +82,4 @@ function sanitizeFileName(name) {
79
82
  return result;
80
83
  }
81
84
  //#endregion
82
- export { normalizeStringifiedOptionalString as a, getPlatformAdapter as c, registerPlatformAdapterFactory as d, normalizeOptionalString as i, hasPlatformAdapter as l, normalizeLowercaseStringOrEmpty as n, readStringField as o, normalizeOptionalLowercaseString as r, sanitizeFileName as s, asOptionalObjectRecord as t, registerPlatformAdapter as u };
85
+ export { normalizeStringifiedEntries as a, getPlatformAdapter as c, registerPlatformAdapterFactory as d, normalizeOptionalString as i, hasPlatformAdapter as l, normalizeLowercaseStringOrEmpty as n, readStringField as o, normalizeOptionalLowercaseString as r, sanitizeFileName as s, asOptionalObjectRecord as t, registerPlatformAdapter as u };
@@ -40,6 +40,10 @@ class Receiver extends Writable {
40
40
  * extensions
41
41
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
42
42
  * client or server mode
43
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
44
+ * buffered data chunks
45
+ * @param {Number} [options.maxFragments=0] The maximum number of message
46
+ * fragments
43
47
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
44
48
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
45
49
  * not to skip UTF-8 validation for text and close messages
@@ -54,6 +58,8 @@ class Receiver extends Writable {
54
58
  this._binaryType = options.binaryType || BINARY_TYPES[0];
55
59
  this._extensions = options.extensions || {};
56
60
  this._isServer = !!options.isServer;
61
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
62
+ this._maxFragments = options.maxFragments | 0;
57
63
  this._maxPayload = options.maxPayload | 0;
58
64
  this._skipUTF8Validation = !!options.skipUTF8Validation;
59
65
  this[kWebSocket] = undefined;
@@ -89,6 +95,22 @@ class Receiver extends Writable {
89
95
  _write(chunk, encoding, cb) {
90
96
  if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
91
97
 
98
+ if (
99
+ this._maxBufferedChunks > 0 &&
100
+ this._buffers.length >= this._maxBufferedChunks
101
+ ) {
102
+ cb(
103
+ this.createError(
104
+ RangeError,
105
+ 'Too many buffered chunks',
106
+ false,
107
+ 1008,
108
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
109
+ )
110
+ );
111
+ return;
112
+ }
113
+
92
114
  this._bufferedBytes += chunk.length;
93
115
  this._buffers.push(chunk);
94
116
  this.startLoop(cb);
@@ -485,6 +507,22 @@ class Receiver extends Writable {
485
507
  }
486
508
 
487
509
  if (data.length) {
510
+ if (
511
+ this._maxFragments > 0 &&
512
+ this._fragments.length >= this._maxFragments
513
+ ) {
514
+ const error = this.createError(
515
+ RangeError,
516
+ 'Too many message fragments',
517
+ false,
518
+ 1008,
519
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
520
+ );
521
+
522
+ cb(error);
523
+ return;
524
+ }
525
+
488
526
  //
489
527
  // This message is not compressed so its length is the sum of the payload
490
528
  // length of all fragments.
@@ -524,6 +562,22 @@ class Receiver extends Writable {
524
562
  return;
525
563
  }
526
564
 
565
+ if (
566
+ this._maxFragments > 0 &&
567
+ this._fragments.length >= this._maxFragments
568
+ ) {
569
+ const error = this.createError(
570
+ RangeError,
571
+ 'Too many message fragments',
572
+ false,
573
+ 1008,
574
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
575
+ );
576
+
577
+ cb(error);
578
+ return;
579
+ }
580
+
527
581
  this._fragments.push(buf);
528
582
  }
529
583
 
@@ -43,6 +43,10 @@ class WebSocketServer extends EventEmitter {
43
43
  * called
44
44
  * @param {Function} [options.handleProtocols] A hook to handle protocols
45
45
  * @param {String} [options.host] The hostname where to bind the server
46
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
47
+ * buffered data chunks
48
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
49
+ * fragments
46
50
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
47
51
  * size
48
52
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter {
65
69
  options = {
66
70
  allowSynchronousEvents: true,
67
71
  autoPong: true,
72
+ maxBufferedChunks: 1024 * 1024,
73
+ maxFragments: 128 * 1024,
68
74
  maxPayload: 100 * 1024 * 1024,
69
75
  skipUTF8Validation: false,
70
76
  perMessageDeflate: false,
@@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter {
424
430
 
425
431
  ws.setSocket(socket, head, {
426
432
  allowSynchronousEvents: this.options.allowSynchronousEvents,
433
+ maxBufferedChunks: this.options.maxBufferedChunks,
434
+ maxFragments: this.options.maxFragments,
427
435
  maxPayload: this.options.maxPayload,
428
436
  skipUTF8Validation: this.options.skipUTF8Validation
429
437
  });
@@ -201,6 +201,10 @@ class WebSocket extends EventEmitter {
201
201
  * multiple times in the same tick
202
202
  * @param {Function} [options.generateMask] The function used to generate the
203
203
  * masking key
204
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
205
+ * buffered data chunks
206
+ * @param {Number} [options.maxFragments=0] The maximum number of message
207
+ * fragments
204
208
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
205
209
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
206
210
  * not to skip UTF-8 validation for text and close messages
@@ -212,6 +216,8 @@ class WebSocket extends EventEmitter {
212
216
  binaryType: this.binaryType,
213
217
  extensions: this._extensions,
214
218
  isServer: this._isServer,
219
+ maxBufferedChunks: options.maxBufferedChunks,
220
+ maxFragments: options.maxFragments,
215
221
  maxPayload: options.maxPayload,
216
222
  skipUTF8Validation: options.skipUTF8Validation
217
223
  });
@@ -640,6 +646,10 @@ module.exports = WebSocket;
640
646
  * masking key
641
647
  * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
642
648
  * handshake request
649
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
650
+ * buffered data chunks
651
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
652
+ * fragments
643
653
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
644
654
  * size
645
655
  * @param {Number} [options.maxRedirects=10] The maximum number of redirects
@@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) {
660
670
  autoPong: true,
661
671
  closeTimeout: CLOSE_TIMEOUT,
662
672
  protocolVersion: protocolVersions[1],
673
+ maxBufferedChunks: 1024 * 1024,
674
+ maxFragments: 128 * 1024,
663
675
  maxPayload: 100 * 1024 * 1024,
664
676
  skipUTF8Validation: false,
665
677
  perMessageDeflate: true,
@@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) {
1017
1029
  websocket.setSocket(socket, head, {
1018
1030
  allowSynchronousEvents: opts.allowSynchronousEvents,
1019
1031
  generateMask: opts.generateMask,
1032
+ maxBufferedChunks: opts.maxBufferedChunks,
1033
+ maxFragments: opts.maxFragments,
1020
1034
  maxPayload: opts.maxPayload,
1021
1035
  skipUTF8Validation: opts.skipUTF8Validation
1022
1036
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ws",
3
- "version": "8.20.1",
3
+ "version": "8.21.0",
4
4
  "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
5
  "keywords": [
6
6
  "HyBi",
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.5.24-beta.2",
3
+ "version": "2026.5.26-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/qqbot",
9
- "version": "2026.5.24-beta.2",
9
+ "version": "2026.5.26-beta.1",
10
10
  "dependencies": {
11
11
  "@tencent-connect/qqbot-connector": "1.1.0",
12
12
  "mpg123-decoder": "1.0.3",
13
13
  "silk-wasm": "3.7.1",
14
- "ws": "8.20.1",
14
+ "ws": "8.21.0",
15
15
  "zod": "4.4.3"
16
16
  },
17
17
  "peerDependencies": {
18
- "openclaw": ">=2026.5.24-beta.2"
18
+ "openclaw": ">=2026.5.26-beta.1"
19
19
  },
20
20
  "peerDependenciesMeta": {
21
21
  "openclaw": {
@@ -92,9 +92,9 @@
92
92
  }
93
93
  },
94
94
  "node_modules/ws": {
95
- "version": "8.20.1",
96
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
97
- "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
95
+ "version": "8.21.0",
96
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
97
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
98
98
  "license": "MIT",
99
99
  "engines": {
100
100
  "node": ">=10.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.5.24-beta.2",
3
+ "version": "2026.5.26-beta.1",
4
4
  "private": false,
5
5
  "description": "OpenClaw QQ Bot channel plugin",
6
6
  "repository": {
@@ -12,11 +12,11 @@
12
12
  "@tencent-connect/qqbot-connector": "1.1.0",
13
13
  "mpg123-decoder": "1.0.3",
14
14
  "silk-wasm": "3.7.1",
15
- "ws": "8.20.1",
15
+ "ws": "8.21.0",
16
16
  "zod": "4.4.3"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.5.24-beta.2"
19
+ "openclaw": ">=2026.5.26-beta.1"
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.24-beta.2"
48
+ "pluginApi": ">=2026.5.26-beta.1"
49
49
  },
50
50
  "build": {
51
- "openclawVersion": "2026.5.24-beta.2"
51
+ "openclawVersion": "2026.5.26-beta.1"
52
52
  },
53
53
  "release": {
54
54
  "publishToClawHub": true,