@openclaw/qqbot 2026.5.28 → 2026.5.31-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,10 @@
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-Ba5ryGDm.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-8Efx0wKu.js";
4
- import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-DWfbz21q.js";
5
- import { t as qqbotSetupPlugin } from "./channel.setup-BxGpVkdx.js";
6
- import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-BCsRtyph.js";
1
+ import { C as getMessageReplyStats, D as setOutboundAudioPort, E as OUTBOUND_ERROR_CODES, S as getMessageReplyConfig, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, a as sendText, b as MESSAGE_REPLY_LIMIT, f as buildMediaTarget, g as sendPhoto, h as sendDocument, i as sendProactiveMessage, m as resolveOutboundMediaPath, n as sendCronMessage, p as parseTarget, r as sendMedia, v as sendVoice, w as recordMessageReply, x as checkMessageReplyLimit, y as resolveUserFacingMediaError } from "./outbound-CgW0-dMd.js";
2
+ import { c as listQQBotAccountIds, l as resolveDefaultQQBotAccountId, o as DEFAULT_ACCOUNT_ID, s as applyQQBotAccountConfig, u as resolveQQBotAccount } from "./config-schema-Bc-ArbIT.js";
3
+ import { t as qqbotPlugin } from "./channel-DsUMS0Az.js";
4
+ import { C as debugError, l as getAccessToken, w as debugLog, z as formatErrorMessage } from "./runtime-DDu_1bny.js";
5
+ import { t as qqbotSetupPlugin } from "./channel.setup-_OAH-xmU.js";
6
+ import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-DNbuU0Co.js";
7
+ import { resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
7
8
  import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
8
9
  //#region extensions/qqbot/src/engine/tools/channel-api.ts
9
10
  /**
@@ -320,8 +321,7 @@ function buildReminderPrompt(content) {
320
321
  return `You are a warm reminder assistant. Please remind the user about: ${content}. Requirements: (1) do not reply with HEARTBEAT_OK (2) do not explain who you are (3) output a direct and caring reminder message (4) you may add a short encouraging line (5) keep it within 2-3 sentences (6) use a small amount of emoji.`;
321
322
  }
322
323
  /** Build cron job params for a one-shot delayed reminder. */
323
- function buildOnceJob(params, delayMs, to, accountId) {
324
- const atMs = Date.now() + delayMs;
324
+ function buildOnceJob(params, atMs, to, accountId) {
325
325
  const content = params.content;
326
326
  return {
327
327
  action: "add",
@@ -448,10 +448,15 @@ function prepareRemindCronAction(params, ctx = {}) {
448
448
  ok: false,
449
449
  error: "Reminder delay must be at least 30 seconds"
450
450
  };
451
+ const atMs = resolveExpiresAtMsFromDurationMs(delayMs);
452
+ if (atMs === void 0) return {
453
+ ok: false,
454
+ error: "Reminder time is outside the supported Date range"
455
+ };
451
456
  return {
452
457
  ok: true,
453
458
  action: "add",
454
- cronAction: buildOnceJob(params, delayMs, resolvedTo, resolvedAccountId),
459
+ cronAction: buildOnceJob(params, atMs, resolvedTo, resolvedAccountId),
455
460
  summary: `⏰ Reminder in ${formatDelay(delayMs)}: "${params.content}"`
456
461
  };
457
462
  }
@@ -1,6 +1,6 @@
1
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-DWfbz21q.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-Bc-ArbIT.js";
3
+ import { T as debugWarn, t as getQQBotRuntime, w as debugLog, z as formatErrorMessage } from "./runtime-DDu_1bny.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";
@@ -374,7 +374,7 @@ function createQQBotApprovalCapability() {
374
374
  },
375
375
  load: async () => {
376
376
  ensurePlatformAdapter();
377
- return (await import("./handler-runtime-C-5nL3Cl.js")).qqbotApprovalNativeRuntime;
377
+ return (await import("./handler-runtime-TwO5-5cH.js")).qqbotApprovalNativeRuntime;
378
378
  }
379
379
  })
380
380
  });
@@ -813,7 +813,7 @@ function clearAccountCredentials(cfg, accountId) {
813
813
  * @throws {Error} When the target format is invalid.
814
814
  */
815
815
  function parseTarget(to) {
816
- let id = to.replace(/^qqbot:/i, "");
816
+ const id = to.replace(/^qqbot:/i, "");
817
817
  if (id.startsWith("c2c:")) {
818
818
  const userId = id.slice(4);
819
819
  if (!userId) throw new Error(`Invalid c2c target format: ${to} - missing user ID`);
@@ -868,9 +868,14 @@ function looksLikeQQBotTarget(id) {
868
868
  //#region extensions/qqbot/src/channel.ts
869
869
  let gatewayModulePromise;
870
870
  function loadGatewayModule() {
871
- gatewayModulePromise ??= import("./gateway-CuTCxSqs.js");
871
+ gatewayModulePromise ??= import("./gateway-Bx-wZKqQ.js");
872
872
  return gatewayModulePromise;
873
873
  }
874
+ let outboundMessagingModulePromise;
875
+ function loadOutboundMessagingModule() {
876
+ outboundMessagingModulePromise ??= import("./outbound-CgW0-dMd.js").then((n) => n.t);
877
+ return outboundMessagingModulePromise;
878
+ }
874
879
  function createQQBotSendReceipt(params) {
875
880
  const messageId = params.messageId?.trim();
876
881
  return createMessageReceiptFromOutboundResults({
@@ -886,7 +891,7 @@ function createQQBotSendReceipt(params) {
886
891
  async function sendQQBotText(params) {
887
892
  await loadGatewayModule();
888
893
  const account = resolveQQBotAccount(params.cfg, params.accountId);
889
- const { sendText } = await import("./outbound-Ba5ryGDm.js").then((n) => n.t);
894
+ const { sendText } = await loadOutboundMessagingModule();
890
895
  const result = await sendText({
891
896
  to: params.to,
892
897
  text: params.text,
@@ -908,7 +913,7 @@ async function sendQQBotText(params) {
908
913
  async function sendQQBotMedia(params) {
909
914
  await loadGatewayModule();
910
915
  const account = resolveQQBotAccount(params.cfg, params.accountId);
911
- const { sendMedia } = await import("./outbound-Ba5ryGDm.js").then((n) => n.t);
916
+ const { sendMedia } = await loadOutboundMessagingModule();
912
917
  const result = await sendMedia({
913
918
  to: params.to,
914
919
  text: params.text ?? "",
@@ -1,2 +1,2 @@
1
- import { t as qqbotPlugin } from "./channel-8Efx0wKu.js";
1
+ import { t as qqbotPlugin } from "./channel-DsUMS0Az.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-iX2iJzKm.js";
1
+ import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-Bc-ArbIT.js";
2
2
  //#region extensions/qqbot/src/channel.setup.ts
3
3
  /**
4
4
  * Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
@@ -48,6 +48,11 @@ function getBridgeLogger() {
48
48
  * statically at the top level so they work reliably in both production and
49
49
  * vitest (which resolves bare specifiers via `resolve.alias`, not Node CJS).
50
50
  */
51
+ let mediaRuntimeModulePromise = null;
52
+ const loadMediaRuntimeModule = async () => {
53
+ mediaRuntimeModulePromise ??= import("openclaw/plugin-sdk/media-runtime");
54
+ return await mediaRuntimeModulePromise;
55
+ };
51
56
  function createBuiltinAdapter() {
52
57
  return {
53
58
  async validateRemoteUrl(_url, _options) {},
@@ -55,7 +60,7 @@ function createBuiltinAdapter() {
55
60
  if (typeof value === "string") return value || void 0;
56
61
  },
57
62
  async downloadFile(url, destDir, filename) {
58
- const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
63
+ const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
59
64
  const result = await readRemoteMediaBuffer({
60
65
  url,
61
66
  filePathHint: filename
@@ -68,7 +73,7 @@ function createBuiltinAdapter() {
68
73
  return destPath;
69
74
  },
70
75
  async fetchMedia(options) {
71
- const { readRemoteMediaBuffer } = await import("openclaw/plugin-sdk/media-runtime");
76
+ const { readRemoteMediaBuffer } = await loadMediaRuntimeModule();
72
77
  const result = await readRemoteMediaBuffer({
73
78
  url: options.url,
74
79
  filePathHint: options.filePathHint,
@@ -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-Ba5ryGDm.js";
1
+ import { D as setOutboundAudioPort, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, c as isCronReminderPayload, d as normalizeMediaTags, g as sendPhoto, h as sendDocument, l as isMediaPayload, o as decodeMediaPath, r as sendMedia, s as encodePayloadForCron, u as parseQQBotPayload, v as sendVoice, y as resolveUserFacingMediaError } from "./outbound-CgW0-dMd.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
- 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, 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-8Efx0wKu.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-DWfbz21q.js";
6
- import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-BCsRtyph.js";
3
+ import { f as resolveAccountBase, h as setBridgeLogger, p as ensurePlatformAdapter } from "./config-schema-Bc-ArbIT.js";
4
+ import { C as parseApprovalButtonData, a as getQQBotDataDir, d as isWindows, f as normalizePath, h as authorizeQQBotApprovalAction, i as getHomeDir, l as getTempDir, m as toGatewayAccount, o as getQQBotDataPath, p as resolveQQBotPayloadLocalFilePath, r as checkSilkWasmAvailable, s as getQQBotMediaDir, u as isLocalPath } from "./channel-DsUMS0Az.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-DDu_1bny.js";
6
+ import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-DNbuU0Co.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";
@@ -84,7 +84,7 @@ const TRANSCRIPT_SOURCE_LABELS = {
84
84
  * Shared grammar (both modes):
85
85
  *
86
86
  * ```
87
- * attachment_with_source := "MEDIA:" SOURCE [voice_suffix]
87
+ * attachment_with_source := "[" TYPE_LABEL ": " SOURCE "]" [voice_suffix]
88
88
  * voice_suffix := ' (transcript: "' TEXT '")' [source_suffix]
89
89
  * attachment_no_source := "[" TYPE_LABEL [": " FILENAME] [voice_suffix_bare] "]" [source_suffix_bare]
90
90
  * voice_suffix_bare := ' (transcript: "' TEXT '")'
@@ -116,7 +116,7 @@ function formatAttachmentTags(attachments) {
116
116
  * Render a single attachment.
117
117
  *
118
118
  * The function is split into two orthogonal concerns:
119
- * - `renderBody`: the shared "MEDIA:{source}…" or "[type…]" string.
119
+ * - `renderBody`: the shared "[type: source]…" or "[type…]" string.
120
120
  * - `renderSourceSuffix`: ref-mode-only `" [source: …]"` tail.
121
121
  *
122
122
  * Both consumers produce the same body; only the suffix differs.
@@ -128,8 +128,9 @@ function renderOne(att, mode) {
128
128
  function renderBody(att) {
129
129
  const source = att.localPath || att.url;
130
130
  const voiceSuffix = att.type === "voice" && att.transcript ? ` (transcript: "${att.transcript}")` : "";
131
- if (source) return `MEDIA:${source}${voiceSuffix}`;
132
- return `[${labelForType(att.type)}${att.filename ? `: ${att.filename}` : ""}${voiceSuffix}]`;
131
+ const label = labelForType(att.type);
132
+ if (source) return `[${label}: ${source}]${voiceSuffix}`;
133
+ return `[${label}${att.filename ? `: ${att.filename}` : ""}${voiceSuffix}]`;
133
134
  }
134
135
  /**
135
136
  * Ref-mode-only tail that records where a voice transcript came from.
@@ -234,7 +235,7 @@ function appendLine(line) {
234
235
  }
235
236
  }
236
237
  function shouldCompact() {
237
- return !!cache && totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1e3;
238
+ return cache !== null && totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1e3;
238
239
  }
239
240
  function compactFile() {
240
241
  if (!cache) return;
@@ -1976,7 +1977,8 @@ async function processAttachments(attachments, ctx) {
1976
1977
  attachmentLocalPaths
1977
1978
  };
1978
1979
  }
1979
- async function processVoiceAttachment(localPath, audioPath, att, asrReferText, cfg, downloadDir, audioConvert, log) {
1980
+ async function processVoiceAttachment(localPath, audioPathInput, att, asrReferText, cfg, downloadDir, audioConvert, log) {
1981
+ let audioPath = audioPathInput;
1980
1982
  const wavUrl = att.voice_wav_url ? att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url : "";
1981
1983
  const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
1982
1984
  const meta = {
@@ -3498,13 +3500,14 @@ async function autoMediaBatch(params) {
3498
3500
  }
3499
3501
  }
3500
3502
  async function sendTextChunkToTarget(params) {
3501
- const { account, event, text, consumeQuoteRef, allowDm } = params;
3503
+ const { account, event, text, consumeQuoteRef, allowDm, forcePlainText } = params;
3502
3504
  const ref = consumeQuoteRef();
3503
3505
  const target = buildDeliveryTarget(event);
3504
3506
  if (target.type === "dm" && !allowDm) return;
3505
3507
  return await sendText(target, text, accountToCreds(account), {
3506
3508
  msgId: event.messageId,
3507
- messageReference: ref
3509
+ messageReference: ref,
3510
+ forcePlainText
3508
3511
  });
3509
3512
  }
3510
3513
  async function sendTextChunks(text, event, actx, sendWithRetry, consumeQuoteRef, deps) {
@@ -3521,8 +3524,25 @@ async function sendTextChunks(text, event, actx, sendWithRetry, consumeQuoteRef,
3521
3524
  onError: (err) => `Failed to send text chunk: ${formatErrorMessage(err)}`
3522
3525
  });
3523
3526
  }
3527
+ async function sendTextOnlyReply(text, event, actx, sendWithRetry, consumeQuoteRef, deps) {
3528
+ const safeText = filterInternalMarkers(text).trim();
3529
+ if (!safeText) return;
3530
+ const { account, log } = actx;
3531
+ await sendTextChunksWithRetry({
3532
+ account,
3533
+ event,
3534
+ chunks: deps.chunkText(safeText, TEXT_CHUNK_LIMIT),
3535
+ sendWithRetry,
3536
+ consumeQuoteRef,
3537
+ allowDm: true,
3538
+ forcePlainText: true,
3539
+ log,
3540
+ onSuccess: (chunk) => `Sent text-only chunk (${chunk.length}/${safeText.length} chars): ${chunk.slice(0, 50)}...`,
3541
+ onError: (err) => `Failed to send text-only chunk: ${formatErrorMessage(err)}`
3542
+ });
3543
+ }
3524
3544
  async function sendTextChunksWithRetry(params) {
3525
- const { account, event, chunks, sendWithRetry, consumeQuoteRef, allowDm, log } = params;
3545
+ const { account, event, chunks, sendWithRetry, consumeQuoteRef, allowDm, forcePlainText, log } = params;
3526
3546
  for (const chunk of chunks) try {
3527
3547
  await sendWithRetry((token) => sendTextChunkToTarget({
3528
3548
  account,
@@ -3530,7 +3550,8 @@ async function sendTextChunksWithRetry(params) {
3530
3550
  token,
3531
3551
  text: chunk,
3532
3552
  consumeQuoteRef,
3533
- allowDm
3553
+ allowDm,
3554
+ forcePlainText
3534
3555
  }));
3535
3556
  log?.info(params.onSuccess(chunk));
3536
3557
  } catch (err) {
@@ -4242,7 +4263,7 @@ function fixPathEncoding(mediaPath, log) {
4242
4263
  const hasNonASCII = /[\u0080-\u00FF]/.test(result);
4243
4264
  if (!isWinLocal && (hasOctal || hasNonASCII)) {
4244
4265
  log?.debug?.(`Decoding path with mixed encoding: ${result}`);
4245
- let decoded = result.replace(/\\([0-7]{1,3})/g, (_, octal) => String.fromCharCode(Number.parseInt(octal, 8)));
4266
+ const decoded = result.replace(/\\([0-7]{1,3})/g, (_, octal) => String.fromCharCode(Number.parseInt(octal, 8)));
4246
4267
  const bytes = [];
4247
4268
  for (let i = 0; i < decoded.length; i++) {
4248
4269
  const code = decoded.charCodeAt(i);
@@ -4310,7 +4331,6 @@ function findFirstClosedMediaTag(text, log) {
4310
4331
  const textBefore = text.slice(0, match.index);
4311
4332
  const tagName = match[1].toLowerCase();
4312
4333
  let mediaPath = match[2]?.trim() ?? "";
4313
- if (mediaPath.startsWith("MEDIA:")) mediaPath = mediaPath.slice(6);
4314
4334
  mediaPath = normalizePath(mediaPath);
4315
4335
  mediaPath = fixPathEncoding(mediaPath, log);
4316
4336
  return {
@@ -5671,6 +5691,18 @@ function resolveResponseTimeoutMs(cfg) {
5671
5691
  const TOOL_ONLY_TIMEOUT = 6e4;
5672
5692
  const MAX_TOOL_RENEWALS = 3;
5673
5693
  const TOOL_MEDIA_SEND_TIMEOUT = 45e3;
5694
+ function shouldDeliverToolProgressImmediately(account, useOfficialC2cStream) {
5695
+ if (useOfficialC2cStream) return true;
5696
+ const streaming = account.config?.streaming;
5697
+ if (streaming === true) return true;
5698
+ return typeof streaming === "object" && streaming !== null && streaming.mode !== "off";
5699
+ }
5700
+ function immediateToolProgressText(payload) {
5701
+ const text = (payload.text ?? "").trim();
5702
+ if (!text || payload.isError || payload.audioAsVoice) return;
5703
+ if (payload.mediaUrl || payload.mediaUrls?.length) return;
5704
+ return text;
5705
+ }
5674
5706
  /**
5675
5707
  * Dispatch the AI reply for the given inbound context.
5676
5708
  *
@@ -5739,6 +5771,24 @@ async function dispatchOutbound(inbound, deps) {
5739
5771
  }
5740
5772
  if (toolTexts.length > 0) await sendErrorMessage(toolTexts.slice(-3).join("\n---\n").slice(0, 2e3));
5741
5773
  };
5774
+ const hasPendingToolFallbackPayload = () => toolTexts.length > 0 || toolMediaUrls.length > 0;
5775
+ const renewToolOnlyFallback = () => {
5776
+ if (toolFallbackSent) return false;
5777
+ if (toolOnlyTimeoutId) {
5778
+ if (toolRenewalCount >= MAX_TOOL_RENEWALS) return false;
5779
+ clearTimeout(toolOnlyTimeoutId);
5780
+ toolRenewalCount++;
5781
+ }
5782
+ toolOnlyTimeoutId = setTimeout(async () => {
5783
+ if (!hasBlockResponse && !toolFallbackSent) {
5784
+ toolFallbackSent = true;
5785
+ try {
5786
+ await sendToolFallback();
5787
+ } catch {}
5788
+ }
5789
+ }, TOOL_ONLY_TIMEOUT);
5790
+ return true;
5791
+ };
5742
5792
  const responseTimeoutMs = resolveResponseTimeoutMs(cfg);
5743
5793
  const timeoutPromise = new Promise((_, reject) => {
5744
5794
  timeoutId = setTimeout(() => {
@@ -5766,6 +5816,7 @@ async function dispatchOutbound(inbound, deps) {
5766
5816
  });
5767
5817
  const messagesConfig = runtime.channel.reply.resolveEffectiveMessagesConfig(cfg, inbound.route.agentId);
5768
5818
  const useOfficialC2cStream = shouldUseOfficialC2cStream(account, event.type === "c2c" ? "c2c" : event.type === "group" ? "group" : "channel");
5819
+ const deliverToolProgressImmediately = shouldDeliverToolProgressImmediately(account, useOfficialC2cStream);
5769
5820
  let streamingController = null;
5770
5821
  if (useOfficialC2cStream) streamingController = new StreamingController({
5771
5822
  account,
@@ -5821,6 +5872,24 @@ async function dispatchOutbound(inbound, deps) {
5821
5872
  if (info.kind === "tool") {
5822
5873
  toolDeliverCount++;
5823
5874
  const toolText = (payload.text ?? "").trim();
5875
+ const textOnlyProgress = immediateToolProgressText(payload);
5876
+ if (!hasBlockResponse && deliverToolProgressImmediately && textOnlyProgress) {
5877
+ if (toolOnlyTimeoutId || hasPendingToolFallbackPayload()) renewToolOnlyFallback();
5878
+ await sendTextOnlyReply(textOnlyProgress, {
5879
+ type: event.type,
5880
+ senderId: event.senderId,
5881
+ messageId: event.messageId,
5882
+ channelId: event.channelId,
5883
+ groupOpenid: event.groupOpenid,
5884
+ msgIdx: event.msgIdx
5885
+ }, {
5886
+ account,
5887
+ qualifiedTarget,
5888
+ log
5889
+ }, sendWithRetry, () => void 0, deliverDeps);
5890
+ recordOutbound();
5891
+ return;
5892
+ }
5824
5893
  if (toolText) toolTexts.push(toolText);
5825
5894
  if (payload.mediaUrls?.length) toolMediaUrls.push(...payload.mediaUrls);
5826
5895
  if (payload.mediaUrl && !toolMediaUrls.includes(payload.mediaUrl)) toolMediaUrls.push(payload.mediaUrl);
@@ -5840,18 +5909,7 @@ async function dispatchOutbound(inbound, deps) {
5840
5909
  return;
5841
5910
  }
5842
5911
  if (toolFallbackSent) return;
5843
- if (toolOnlyTimeoutId) if (toolRenewalCount < MAX_TOOL_RENEWALS) {
5844
- clearTimeout(toolOnlyTimeoutId);
5845
- toolRenewalCount++;
5846
- } else return;
5847
- toolOnlyTimeoutId = setTimeout(async () => {
5848
- if (!hasBlockResponse && !toolFallbackSent) {
5849
- toolFallbackSent = true;
5850
- try {
5851
- await sendToolFallback();
5852
- } catch {}
5853
- }
5854
- }, TOOL_ONLY_TIMEOUT);
5912
+ renewToolOnlyFallback();
5855
5913
  return;
5856
5914
  }
5857
5915
  hasBlockResponse = true;
@@ -6567,7 +6625,7 @@ async function startGateway(ctx) {
6567
6625
  context: { account: ctx.account },
6568
6626
  abortSignal: ctx.abortSignal
6569
6627
  });
6570
- accountLogger.info(`approval.native context registered (lease=${!!lease})`);
6628
+ accountLogger.info(`approval.native context registered (lease=${Boolean(lease)})`);
6571
6629
  } else accountLogger.info("No channelRuntime — skipping approval.native registration");
6572
6630
  return startGateway$1({
6573
6631
  account: toGatewayAccount(ctx.account),
@@ -1,6 +1,6 @@
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-8Efx0wKu.js";
3
- import { d as getMessageApi, i as accountToCreds } from "./runtime-DWfbz21q.js";
1
+ import { m as getBridgeLogger, p as ensurePlatformAdapter, u as resolveQQBotAccount } from "./config-schema-Bc-ArbIT.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-DsUMS0Az.js";
3
+ import { d as getMessageApi, i as accountToCreds } from "./runtime-DDu_1bny.js";
4
4
  import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
5
5
  import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
6
6
  //#region extensions/qqbot/src/bridge/approval/handler-runtime.ts
@@ -1,6 +1,6 @@
1
1
  import { i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
2
- import { a as getQQBotDataDir, f as normalizePath$1, n as parseTarget$1, p as resolveQQBotPayloadLocalFilePath, s as getQQBotMediaDir, u as isLocalPath } from "./channel-8Efx0wKu.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-DWfbz21q.js";
2
+ import { a as getQQBotDataDir, f as normalizePath$1, n as parseTarget$1, p as resolveQQBotPayloadLocalFilePath, s as getQQBotMediaDir, u as isLocalPath } from "./channel-DsUMS0Az.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-DDu_1bny.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";
@@ -997,8 +997,8 @@ function normalizePath(p) {
997
997
  return result;
998
998
  }
999
999
  /**
1000
- * Decode a media path by stripping `MEDIA:`, expanding `~`, and unescaping
1001
- * octal/UTF-8 byte sequences.
1000
+ * Decode a media path by expanding `~` and unescaping octal/UTF-8 byte
1001
+ * sequences.
1002
1002
  *
1003
1003
  * @param raw - Raw path string from a media tag.
1004
1004
  * @param log - Optional logger for decode diagnostics.
@@ -1006,7 +1006,6 @@ function normalizePath(p) {
1006
1006
  */
1007
1007
  function decodeMediaPath(raw, log) {
1008
1008
  let mediaPath = raw;
1009
- if (mediaPath.startsWith("MEDIA:")) mediaPath = mediaPath.slice(6);
1010
1009
  mediaPath = normalizePath(mediaPath);
1011
1010
  mediaPath = mediaPath.replace(/\\\\/g, "\\");
1012
1011
  const isWinLocal = /^[a-zA-Z]:[\\/]/.test(mediaPath) || mediaPath.startsWith("\\\\");
@@ -1078,10 +1077,24 @@ function isVideoFile$1(filePath, mimeType) {
1078
1077
  var outbound_exports = /* @__PURE__ */ __exportAll({
1079
1078
  DEFAULT_MEDIA_SEND_ERROR: () => DEFAULT_MEDIA_SEND_ERROR,
1080
1079
  MESSAGE_REPLY_LIMIT: () => 4,
1080
+ OUTBOUND_ERROR_CODES: () => OUTBOUND_ERROR_CODES,
1081
+ buildMediaTarget: () => buildMediaTarget,
1082
+ checkMessageReplyLimit: () => checkMessageReplyLimit,
1083
+ getMessageReplyConfig: () => getMessageReplyConfig,
1084
+ getMessageReplyStats: () => getMessageReplyStats,
1085
+ parseTarget: () => parseTarget,
1086
+ recordMessageReply: () => recordMessageReply,
1087
+ resolveOutboundMediaPath: () => resolveOutboundMediaPath,
1088
+ resolveUserFacingMediaError: () => resolveUserFacingMediaError,
1081
1089
  sendCronMessage: () => sendCronMessage,
1090
+ sendDocument: () => sendDocument,
1082
1091
  sendMedia: () => sendMedia,
1092
+ sendPhoto: () => sendPhoto,
1083
1093
  sendProactiveMessage: () => sendProactiveMessage,
1084
- sendText: () => sendText
1094
+ sendText: () => sendText,
1095
+ sendVideoMsg: () => sendVideoMsg,
1096
+ sendVoice: () => sendVoice,
1097
+ setOutboundAudioPort: () => setOutboundAudioPort
1085
1098
  });
1086
1099
  const isImageFile = isImageFile$1;
1087
1100
  const isVideoFile = isVideoFile$1;
@@ -1,5 +1,5 @@
1
- import { a as getQQBotDataDir, c as getQQBotMediaPath, d as isWindows, i as getHomeDir } from "./channel-8Efx0wKu.js";
2
- import { w as debugLog } from "./runtime-DWfbz21q.js";
1
+ import { a as getQQBotDataDir, c as getQQBotMediaPath, d as isWindows, i as getHomeDir } from "./channel-DsUMS0Az.js";
2
+ import { w as debugLog } from "./runtime-DDu_1bny.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";
@@ -9,7 +9,7 @@ import { FsSafeError, openLocalFileSafely, readRegularFile, statRegularFileSync
9
9
  import * as path$1 from "node:path";
10
10
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
11
11
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
12
- import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
12
+ import { asDateTimestampMs, isFutureDateTimestampMs, parseStrictPositiveInteger, resolveExpiresAtMsFromDurationSeconds, resolveTimestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
13
13
  //#region extensions/qqbot/src/engine/types.ts
14
14
  /**
15
15
  * Core API layer public types.
@@ -1173,13 +1173,18 @@ var TokenManager = class {
1173
1173
  throw new Error("QQBot access_token response was malformed JSON");
1174
1174
  }
1175
1175
  if (!data.access_token) throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`);
1176
- const expiresAt = Date.now() + resolveTokenExpiresInSeconds(data.expires_in) * 1e3;
1176
+ const nowMs = asDateTimestampMs(Date.now());
1177
+ if (nowMs === void 0) {
1178
+ this.logger?.debug?.(`[qqbot:token:${appId}] Not cached: invalid process clock`);
1179
+ return data.access_token;
1180
+ }
1181
+ const expiresAt = resolveExpiresAtMsFromDurationSeconds(resolveTokenExpiresInSeconds(data.expires_in), { nowMs }) ?? nowMs;
1177
1182
  this.cache.set(appId, {
1178
1183
  token: data.access_token,
1179
1184
  expiresAt,
1180
1185
  appId
1181
1186
  });
1182
- this.logger?.debug?.(`[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`);
1187
+ this.logger?.debug?.(`[qqbot:token:${appId}] Cached, expires at: ${resolveTimestampMsToIsoString(expiresAt)}`);
1183
1188
  return data.access_token;
1184
1189
  } finally {
1185
1190
  await release?.();
@@ -1426,15 +1431,15 @@ var MessageApi = class {
1426
1431
  async sendMessage(scope, targetId, content, creds, opts) {
1427
1432
  const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
1428
1433
  const msgSeq = opts?.msgId ? getNextMsgSeq(opts.msgId) : 1;
1429
- const body = this.buildMessageBody(content, opts?.msgId, msgSeq, opts?.messageReference, opts?.inlineKeyboard);
1434
+ const body = this.buildMessageBody(content, opts?.msgId, msgSeq, opts?.messageReference, opts?.inlineKeyboard, opts?.forcePlainText);
1430
1435
  const path = messagePath(scope, targetId);
1431
1436
  return this.sendAndNotify(creds.appId, token, "POST", path, body, { text: content });
1432
1437
  }
1433
1438
  /** Send a proactive (no msgId) message to a C2C or Group target. */
1434
- async sendProactiveMessage(scope, targetId, content, creds) {
1439
+ async sendProactiveMessage(scope, targetId, content, creds, opts) {
1435
1440
  if (!content?.trim()) throw new Error("Proactive message content must not be empty");
1436
1441
  const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
1437
- const body = this.buildProactiveBody(content);
1442
+ const body = this.buildProactiveBody(content, opts?.forcePlainText);
1438
1443
  const path = messagePath(scope, targetId);
1439
1444
  return this.sendAndNotify(creds.appId, token, "POST", path, body, { text: content });
1440
1445
  }
@@ -1508,8 +1513,9 @@ var MessageApi = class {
1508
1513
  }
1509
1514
  return result;
1510
1515
  }
1511
- buildMessageBody(content, msgId, msgSeq, messageReference, inlineKeyboard) {
1512
- const body = this.markdownSupport ? {
1516
+ buildMessageBody(content, msgId, msgSeq, messageReference, inlineKeyboard, forcePlainText = false) {
1517
+ const useMarkdown = this.markdownSupport && !forcePlainText;
1518
+ const body = useMarkdown ? {
1513
1519
  markdown: { content },
1514
1520
  msg_type: 2,
1515
1521
  msg_seq: msgSeq
@@ -1519,12 +1525,12 @@ var MessageApi = class {
1519
1525
  msg_seq: msgSeq
1520
1526
  };
1521
1527
  if (msgId) body.msg_id = msgId;
1522
- if (messageReference && !this.markdownSupport) body.message_reference = { message_id: messageReference };
1528
+ if (messageReference && !useMarkdown) body.message_reference = { message_id: messageReference };
1523
1529
  if (inlineKeyboard) body.keyboard = inlineKeyboard;
1524
1530
  return body;
1525
1531
  }
1526
- buildProactiveBody(content) {
1527
- return this.markdownSupport ? {
1532
+ buildProactiveBody(content, forcePlainText = false) {
1533
+ return this.markdownSupport && !forcePlainText ? {
1528
1534
  markdown: { content },
1529
1535
  msg_type: 2
1530
1536
  } : {
@@ -1607,7 +1613,7 @@ function getCachedFileInfo(contentHash, scope, targetId, fileType) {
1607
1613
  const key = buildCacheKey(contentHash, scope, targetId, fileType);
1608
1614
  const entry = cache.get(key);
1609
1615
  if (!entry) return null;
1610
- if (Date.now() >= entry.expiresAt) {
1616
+ if (!isFutureDateTimestampMs(entry.expiresAt)) {
1611
1617
  cache.delete(key);
1612
1618
  return null;
1613
1619
  }
@@ -1618,7 +1624,7 @@ function getCachedFileInfo(contentHash, scope, targetId, fileType) {
1618
1624
  function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fileUuid, ttl) {
1619
1625
  if (cache.size >= MAX_CACHE_SIZE) {
1620
1626
  const now = Date.now();
1621
- for (const [k, v] of cache) if (now >= v.expiresAt) cache.delete(k);
1627
+ for (const [k, v] of cache) if (!isFutureDateTimestampMs(v.expiresAt, { nowMs: now })) cache.delete(k);
1622
1628
  if (cache.size >= MAX_CACHE_SIZE) {
1623
1629
  const keys = Array.from(cache.keys());
1624
1630
  for (let i = 0; i < keys.length / 2; i++) cache.delete(keys[i]);
@@ -1626,10 +1632,15 @@ function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fil
1626
1632
  }
1627
1633
  const key = buildCacheKey(contentHash, scope, targetId, fileType);
1628
1634
  const effectiveTtl = Math.max(ttl - 60, 10);
1635
+ const expiresAt = resolveExpiresAtMsFromDurationSeconds(effectiveTtl);
1636
+ if (expiresAt === void 0) {
1637
+ cache.delete(key);
1638
+ return;
1639
+ }
1629
1640
  cache.set(key, {
1630
1641
  fileInfo,
1631
1642
  fileUuid,
1632
- expiresAt: Date.now() + effectiveTtl * 1e3
1643
+ expiresAt
1633
1644
  });
1634
1645
  debugLog(`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
1635
1646
  }
@@ -1860,9 +1871,10 @@ async function sendText(target, content, creds, opts) {
1860
1871
  const scope = target.type;
1861
1872
  if (opts?.msgId) return api.sendMessage(scope, target.id, content, c, {
1862
1873
  msgId: opts.msgId,
1863
- messageReference: opts.messageReference
1874
+ messageReference: opts.messageReference,
1875
+ forcePlainText: opts.forcePlainText
1864
1876
  });
1865
- return api.sendProactiveMessage(scope, target.id, content, c);
1877
+ return api.sendProactiveMessage(scope, target.id, content, c, { forcePlainText: opts?.forcePlainText });
1866
1878
  }
1867
1879
  if (target.type === "dm") return api.sendDmMessage({
1868
1880
  guildId: target.id,
@@ -1,2 +1,2 @@
1
- import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-DWfbz21q.js";
1
+ import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-DDu_1bny.js";
2
2
  export { getQQBotRuntime, setQQBotRuntime };
@@ -1,2 +1,2 @@
1
- import { t as qqbotSetupPlugin } from "./channel.setup-BxGpVkdx.js";
1
+ import { t as qqbotSetupPlugin } from "./channel.setup-_OAH-xmU.js";
2
2
  export { qqbotSetupPlugin };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.5.28",
3
+ "version": "2026.5.31-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/qqbot",
9
- "version": "2026.5.28",
9
+ "version": "2026.5.31-beta.1",
10
10
  "dependencies": {
11
11
  "@tencent-connect/qqbot-connector": "1.1.0",
12
12
  "mpg123-decoder": "1.0.3",
@@ -15,7 +15,7 @@
15
15
  "zod": "4.4.3"
16
16
  },
17
17
  "peerDependencies": {
18
- "openclaw": ">=2026.5.28"
18
+ "openclaw": ">=2026.5.31-beta.1"
19
19
  },
20
20
  "peerDependenciesMeta": {
21
21
  "openclaw": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.5.28",
3
+ "version": "2026.5.31-beta.1",
4
4
  "private": false,
5
5
  "description": "OpenClaw QQ Bot channel plugin for group and direct-message workflows.",
6
6
  "repository": {
@@ -16,7 +16,7 @@
16
16
  "zod": "4.4.3"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.5.28"
19
+ "openclaw": ">=2026.5.31-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.28"
48
+ "pluginApi": ">=2026.5.31-beta.1"
49
49
  },
50
50
  "build": {
51
- "openclawVersion": "2026.5.28"
51
+ "openclawVersion": "2026.5.31-beta.1"
52
52
  },
53
53
  "release": {
54
54
  "publishToClawHub": true,