@openclaw/qqbot 2026.7.1-beta.1 → 2026.7.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
- import { C as getNextMsgSeq, D as downloadFile, F as StreamInputMode, I as StreamInputState, M as getMaxUploadSize, P as StreamContentType, T as openLocalFile, a as createRawInputNotifyFn, b as stopBackgroundTokenRefresh, c as getMessageApi, d as initSender, f as onMessageSent, g as sendText, h as sendMedia, i as clearTokenCache, j as getImageMimeType, k as formatFileSize, l as getPluginUserAgent, m as sendInputNotify, n as acknowledgeInteraction, o as getAccessToken, p as registerAccount, r as buildDeliveryTarget, s as getGatewayUrl, t as accountToCreds, u as initApiConfig, x as withTokenRetry, y as startBackgroundTokenRefresh } from "./sender-BHWpE_yH.js";
1
+ import { C as getNextMsgSeq, D as downloadFile, F as StreamInputMode, I as StreamInputState, M as getMaxUploadSize, P as StreamContentType, T as openLocalFile, a as createRawInputNotifyFn, b as stopBackgroundTokenRefresh, c as getMessageApi, d as initSender, f as onMessageSent, g as sendText, h as sendMedia, i as clearTokenCache, j as getImageMimeType, k as formatFileSize, l as getPluginUserAgent, m as sendInputNotify, n as acknowledgeInteraction, o as getAccessToken, p as registerAccount, r as buildDeliveryTarget, s as getGatewayUrl, t as accountToCreds, u as initApiConfig, x as withTokenRetry, y as startBackgroundTokenRefresh } from "./sender-BfVLJJYp.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 { c as setBridgeLogger, o as ensurePlatformAdapter } from "./config-schema-gMW9zKvQ.js";
4
- import { a as authorizeQQBotApprovalAction, i as toGatewayAccount, n as createQQBotMarkdownChunker, p as parseApprovalButtonData, r as openQQBotSyncKeyedStore } from "./channel-Bh0MrYvU.js";
3
+ import { c as setBridgeLogger, o as ensurePlatformAdapter } from "./config-schema-JZEf1dvB.js";
4
+ import { a as authorizeQQBotApprovalAction, i as toGatewayAccount, n as createQQBotMarkdownChunker, p as parseApprovalButtonData, r as openQQBotSyncKeyedStore } from "./channel-BQM6pDlD.js";
5
5
  import { d as resolveAccountBase } from "./config-C1qZbh0K.js";
6
6
  import { a as formatErrorMessage, i as formatDuration, n as debugLog, r as debugWarn, t as debugError } from "./log-SDfMMBWe.js";
7
- import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-BL0xZzWe.js";
7
+ import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-B4HWCOa-.js";
8
8
  import { t as buildQQBotStateKey } from "./state-keys-CQKlAFyo.js";
9
- import { a as resolveMentionPatterns, c as getPluginVersion, d as PRIVATE_CHAT_ONLY_TEXT, f as classifyCoreCommandForGroup, i as resolveGroupSettings, l as initCommands, n as resolveGroupCommandLevelFromAccountConfig, r as resolveGroupConfig, s as getFrameworkVersion, u as matchSlashCommand } from "./group-DQQsZ6Y0.js";
10
- import { A as getHomeDir, E as DEFAULT_MEDIA_SEND_ERROR, F as isLocalPath, I as isWindows, L as normalizePath, M as getQQBotMediaDir, O as setOutboundAudioPort, P as getTempDir, _ as sendVideoMsg, b as resolveUserFacingMediaError, c as isCronReminderPayload, d as normalizeMediaTags, g as sendPhoto, h as sendDocument, j as getQQBotDataDir, k as checkSilkWasmAvailable, l as isMediaPayload, o as decodeMediaPath, r as sendMedia$1, s as encodePayloadForCron, u as parseQQBotPayload, v as sendVoice, y as resolveTrustedOutboundMediaPath } from "./outbound-Dfa2uOEp.js";
9
+ import { a as resolveMentionPatterns, c as getPluginVersion, d as PRIVATE_CHAT_ONLY_TEXT, f as classifyCoreCommandForGroup, i as resolveGroupSettings, l as initCommands, n as resolveGroupCommandLevelFromAccountConfig, r as resolveGroupConfig, s as getFrameworkVersion, u as matchSlashCommand } from "./group-BmS1Qwst.js";
10
+ import { B as normalizePath, C as resolveUserFacingMediaError, F as getQQBotMediaDir, L as getTempDir, M as checkSilkWasmAvailable, N as getHomeDir, P as getQQBotDataDir, R as isLocalPath, S as resolveWorkspaceScopedLocalRoots, _ as sendVideoMsg, b as resolveOutboundMediaLocalRoots, c as isCronReminderPayload, d as normalizeMediaTags, g as sendPhoto, h as sendDocument, j as setOutboundAudioPort, k as DEFAULT_MEDIA_SEND_ERROR, l as isMediaPayload, o as decodeMediaPath, r as sendMedia$1, s as encodePayloadForCron, u as parseQQBotPayload, v as sendVoice, x as resolveWorkspacePathCandidates, y as resolveTrustedOutboundMediaPath, z as isWindows } from "./outbound-BZHwMFUX.js";
11
11
  import { n as runWithRequestContext } from "./request-context-Bm7PTBD1.js";
12
12
  import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
13
13
  import { isImplicitSameChatApprovalAuthorization } from "openclaw/plugin-sdk/approval-auth-runtime";
@@ -17,7 +17,7 @@ import * as os$1 from "node:os";
17
17
  import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
18
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
19
19
  import crypto from "node:crypto";
20
- import { readRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
20
+ import { readRegularFileSync, resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
21
21
  import * as path$1 from "node:path";
22
22
  import path from "node:path";
23
23
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
@@ -26,6 +26,7 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
26
26
  import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime";
27
27
  import WebSocket from "ws";
28
28
  import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
29
+ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
29
30
  import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound";
30
31
  import { SILENT_REPLY_TOKEN, isSilentReplyPayloadText } from "openclaw/plugin-sdk/reply-chunking";
31
32
  import { Buffer as Buffer$1 } from "node:buffer";
@@ -3298,15 +3299,30 @@ function hasQQBotImageSize(markdownImage) {
3298
3299
  //#region extensions/qqbot/src/engine/messaging/outbound-deliver.ts
3299
3300
  /** Maximum text length for a single QQ Bot message. */
3300
3301
  const TEXT_CHUNK_LIMIT = 5e3;
3301
- function resolveMediaTargetContext(event, account) {
3302
+ function resolveMediaTargetContext(event, actx) {
3303
+ const { account } = actx;
3302
3304
  return {
3303
3305
  targetType: event.type === "c2c" ? "c2c" : event.type === "group" ? "group" : event.type === "dm" ? "dm" : "channel",
3304
3306
  targetId: event.type === "c2c" ? event.senderId : event.type === "group" ? event.groupOpenid : event.type === "dm" ? event.guildId : event.channelId,
3305
3307
  account,
3306
- replyToId: event.messageId
3308
+ replyToId: event.messageId,
3309
+ ...actx.mediaAccess ? { mediaAccess: actx.mediaAccess } : {},
3310
+ ...actx.mediaLocalRoots ? { mediaLocalRoots: actx.mediaLocalRoots } : {},
3311
+ ...actx.mediaReadFile ? { mediaReadFile: actx.mediaReadFile } : {}
3307
3312
  };
3308
3313
  }
3314
+ function isHttpUrl(value) {
3315
+ return value.startsWith("http://") || value.startsWith("https://");
3316
+ }
3317
+ function isImageDataUrl(value) {
3318
+ return value.startsWith("data:image/");
3319
+ }
3320
+ function isBareRelativeMediaPath(value) {
3321
+ const trimmed = value.trim();
3322
+ return Boolean(trimmed) && !trimmed.startsWith("#") && !trimmed.startsWith("//") && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed);
3323
+ }
3309
3324
  async function autoMediaBatch(params) {
3325
+ let sentCount = 0;
3310
3326
  for (const mediaUrl of params.mediaUrls) try {
3311
3327
  const result = await params.mediaSender.sendMedia({
3312
3328
  to: params.qualifiedTarget,
@@ -3314,17 +3330,22 @@ async function autoMediaBatch(params) {
3314
3330
  mediaUrl,
3315
3331
  accountId: params.account.accountId,
3316
3332
  replyToId: params.replyToId,
3317
- account: params.account
3333
+ account: params.account,
3334
+ ...params.mediaAccess ? { mediaAccess: params.mediaAccess } : {},
3335
+ ...params.mediaLocalRoots ? { mediaLocalRoots: params.mediaLocalRoots } : {},
3336
+ ...params.mediaReadFile ? { mediaReadFile: params.mediaReadFile } : {}
3318
3337
  });
3319
3338
  if (result.error) {
3320
3339
  params.log?.error(params.onResultError(mediaUrl, result.error));
3321
3340
  continue;
3322
3341
  }
3342
+ sentCount++;
3323
3343
  const successMessage = params.onSuccess?.(mediaUrl);
3324
3344
  if (successMessage) params.log?.info(successMessage);
3325
3345
  } catch (err) {
3326
3346
  params.log?.error(params.onThrownError(mediaUrl, formatErrorMessage(err)));
3327
3347
  }
3348
+ return sentCount;
3328
3349
  }
3329
3350
  async function sendTextChunkToTarget(params) {
3330
3351
  const { account, event, text, consumeQuoteRef, allowDm, forcePlainText } = params;
@@ -3390,16 +3411,18 @@ async function sendWithResultLogging(params) {
3390
3411
  const result = await params.run();
3391
3412
  if (result.error) {
3392
3413
  params.log?.error(params.onError(result.error));
3393
- return;
3414
+ return false;
3394
3415
  }
3395
3416
  const successMessage = params.onSuccess?.();
3396
3417
  if (successMessage) params.log?.info(successMessage);
3418
+ return true;
3397
3419
  } catch (err) {
3398
3420
  params.log?.error(params.onError(formatErrorMessage(err)));
3421
+ return false;
3399
3422
  }
3400
3423
  }
3401
3424
  async function sendPhotoWithLogging(params) {
3402
- await sendWithResultLogging({
3425
+ return await sendWithResultLogging({
3403
3426
  run: async () => await params.mediaSender.sendPhoto(params.target, params.imageUrl),
3404
3427
  log: params.log,
3405
3428
  onSuccess: params.onSuccess ? () => params.onSuccess?.(params.imageUrl) : void 0,
@@ -3431,9 +3454,14 @@ async function sendVoiceWithTimeout(target, voicePath, account, mediaSender, log
3431
3454
  });
3432
3455
  }, voiceTimeout);
3433
3456
  })]);
3434
- if (result.error) log?.error(`sendVoice error: ${result.error}`);
3457
+ if (result.error) {
3458
+ log?.error(`sendVoice error: ${result.error}`);
3459
+ return false;
3460
+ }
3461
+ return true;
3435
3462
  } catch (err) {
3436
3463
  log?.error(`sendVoice unexpected error: ${formatErrorMessage(err)}`);
3464
+ return false;
3437
3465
  }
3438
3466
  }
3439
3467
  /**
@@ -3489,38 +3517,62 @@ async function parseAndSendMediaTags(replyText, event, actx, sendWithRetry, cons
3489
3517
  content: filterInternalMarkers(textAfter)
3490
3518
  });
3491
3519
  log?.debug?.(`Send queue: ${sendQueue.map((item) => item.type).join(" -> ")}`);
3492
- const mediaTarget = resolveMediaTargetContext(event, account);
3493
- for (const item of sendQueue) if (item.type === "text") await sendTextChunks(item.content, event, actx, sendWithRetry, consumeQuoteRef, deps);
3494
- else if (item.type === "image") await sendPhotoWithLogging({
3495
- target: mediaTarget,
3496
- imageUrl: item.content,
3497
- mediaSender: deps.mediaSender,
3498
- log,
3499
- onError: (error) => `sendPhoto error: ${error}`
3500
- });
3501
- else if (item.type === "voice") await sendVoiceWithTimeout(mediaTarget, item.content, account, deps.mediaSender, log);
3502
- else if (item.type === "video") await sendWithResultLogging({
3503
- run: async () => await deps.mediaSender.sendVideoMsg(mediaTarget, item.content),
3504
- log,
3505
- onError: (error) => `sendVideoMsg error: ${error}`
3506
- });
3507
- else if (item.type === "file") await sendWithResultLogging({
3508
- run: async () => await deps.mediaSender.sendDocument(mediaTarget, item.content),
3509
- log,
3510
- onError: (error) => `sendDocument error: ${error}`
3511
- });
3512
- else if (item.type === "media") await sendWithResultLogging({
3513
- run: async () => await deps.mediaSender.sendMedia({
3514
- to: actx.qualifiedTarget,
3515
- text: "",
3516
- mediaUrl: item.content,
3517
- accountId: account.accountId,
3518
- replyToId: event.messageId,
3519
- account
3520
- }),
3521
- log,
3522
- onError: (error) => `sendMedia(auto) error: ${error}`
3523
- });
3520
+ const mediaTarget = resolveMediaTargetContext(event, actx);
3521
+ let deliveredVisibleOutput = false;
3522
+ for (const item of sendQueue) if (item.type === "text") {
3523
+ await sendTextChunks(item.content, event, actx, sendWithRetry, consumeQuoteRef, deps);
3524
+ if (item.content.trim()) deliveredVisibleOutput = true;
3525
+ } else if (item.type === "image") {
3526
+ const sent = await sendPhotoWithLogging({
3527
+ target: mediaTarget,
3528
+ imageUrl: item.content,
3529
+ mediaSender: deps.mediaSender,
3530
+ log,
3531
+ onError: (error) => `sendPhoto error: ${error}`
3532
+ });
3533
+ deliveredVisibleOutput = deliveredVisibleOutput || sent;
3534
+ } else if (item.type === "voice") {
3535
+ const sent = await sendVoiceWithTimeout(mediaTarget, item.content, account, deps.mediaSender, log);
3536
+ deliveredVisibleOutput = deliveredVisibleOutput || sent;
3537
+ } else if (item.type === "video") {
3538
+ const sent = await sendWithResultLogging({
3539
+ run: async () => await deps.mediaSender.sendVideoMsg(mediaTarget, item.content),
3540
+ log,
3541
+ onError: (error) => `sendVideoMsg error: ${error}`
3542
+ });
3543
+ deliveredVisibleOutput = deliveredVisibleOutput || sent;
3544
+ } else if (item.type === "file") {
3545
+ const sent = await sendWithResultLogging({
3546
+ run: async () => await deps.mediaSender.sendDocument(mediaTarget, item.content),
3547
+ log,
3548
+ onError: (error) => `sendDocument error: ${error}`
3549
+ });
3550
+ deliveredVisibleOutput = deliveredVisibleOutput || sent;
3551
+ } else if (item.type === "media") {
3552
+ const sent = await sendWithResultLogging({
3553
+ run: async () => await deps.mediaSender.sendMedia({
3554
+ to: actx.qualifiedTarget,
3555
+ text: "",
3556
+ mediaUrl: item.content,
3557
+ accountId: account.accountId,
3558
+ replyToId: event.messageId,
3559
+ account,
3560
+ ...actx.mediaAccess ? { mediaAccess: actx.mediaAccess } : {},
3561
+ ...actx.mediaLocalRoots ? { mediaLocalRoots: actx.mediaLocalRoots } : {},
3562
+ ...actx.mediaReadFile ? { mediaReadFile: actx.mediaReadFile } : {}
3563
+ }),
3564
+ log,
3565
+ onError: (error) => `sendMedia(auto) error: ${error}`
3566
+ });
3567
+ deliveredVisibleOutput = deliveredVisibleOutput || sent;
3568
+ }
3569
+ if (!deliveredVisibleOutput) {
3570
+ await sendTextChunks(DEFAULT_MEDIA_SEND_ERROR, event, actx, sendWithRetry, consumeQuoteRef, deps);
3571
+ return {
3572
+ handled: true,
3573
+ normalizedText: ""
3574
+ };
3575
+ }
3524
3576
  return {
3525
3577
  handled: true,
3526
3578
  normalizedText: text
@@ -3534,18 +3586,18 @@ async function sendPlainReply(payload, replyText, event, actx, sendWithRetry, co
3534
3586
  const { account, qualifiedTarget, log } = actx;
3535
3587
  const collectedImageUrls = [];
3536
3588
  const localMediaToSend = [];
3537
- const collectImageUrl = (url) => {
3589
+ const collectImageUrl = (url, allowBareRelativeMedia = false) => {
3538
3590
  if (!url) return false;
3539
- const isHttpUrl = url.startsWith("http://") || url.startsWith("https://");
3540
- const isDataUrl = url.startsWith("data:image/");
3541
- if (isHttpUrl || isDataUrl) {
3591
+ const isRemoteHttpUrl = isHttpUrl(url);
3592
+ const isDataUrl = isImageDataUrl(url);
3593
+ if (isRemoteHttpUrl || isDataUrl) {
3542
3594
  if (!collectedImageUrls.includes(url)) {
3543
3595
  collectedImageUrls.push(url);
3544
3596
  log?.debug?.(`Collected ${isDataUrl ? "Base64" : "media URL"}: ${isDataUrl ? `(length: ${url.length})` : url.slice(0, 80) + "..."}`);
3545
3597
  }
3546
3598
  return true;
3547
3599
  }
3548
- if (isLocalPath(url)) {
3600
+ if (isLocalPath(url) || allowBareRelativeMedia && isBareRelativeMediaPath(url)) {
3549
3601
  if (!localMediaToSend.includes(url)) {
3550
3602
  localMediaToSend.push(url);
3551
3603
  log?.debug?.(`Collected local media for auto-routing: ${url}`);
@@ -3554,13 +3606,13 @@ async function sendPlainReply(payload, replyText, event, actx, sendWithRetry, co
3554
3606
  }
3555
3607
  return false;
3556
3608
  };
3557
- if (payload.mediaUrls?.length) for (const url of payload.mediaUrls) collectImageUrl(url);
3558
- if (payload.mediaUrl) collectImageUrl(payload.mediaUrl);
3609
+ if (payload.mediaUrls?.length) for (const url of payload.mediaUrls) collectImageUrl(url, true);
3610
+ if (payload.mediaUrl) collectImageUrl(payload.mediaUrl, true);
3559
3611
  const mdMatches = [...replyText.matchAll(/!\[([^\]]*)\]\(([^)]+)\)/gi)];
3560
3612
  for (const m of mdMatches) {
3561
3613
  const url = m[2]?.trim();
3562
3614
  if (url && !collectedImageUrls.includes(url)) {
3563
- if (url.startsWith("http://") || url.startsWith("https://")) {
3615
+ if (isHttpUrl(url)) {
3564
3616
  collectedImageUrls.push(url);
3565
3617
  log?.debug?.(`Extracted HTTP image from markdown: ${url.slice(0, 80)}...`);
3566
3618
  } else if (isLocalPath(url)) {
@@ -3584,37 +3636,51 @@ async function sendPlainReply(payload, replyText, event, actx, sendWithRetry, co
3584
3636
  let textWithoutImages = filterInternalMarkers(replyText);
3585
3637
  for (const m of mdMatches) {
3586
3638
  const url = m[2]?.trim();
3587
- if (url && !url.startsWith("http://") && !url.startsWith("https://") && !isLocalPath(url)) textWithoutImages = textWithoutImages.replace(m[0], "").trim();
3639
+ if (url && !isHttpUrl(url) && !isLocalPath(url)) textWithoutImages = textWithoutImages.replace(m[0], "").trim();
3588
3640
  }
3589
3641
  if (useMarkdown) await sendMarkdownReply(textWithoutImages, collectedImageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef, deps);
3590
3642
  else await sendPlainTextReply(textWithoutImages, collectedImageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef, deps);
3643
+ const hasVisibleTextOrInlineImage = Boolean(textWithoutImages.trim() || collectedImageUrls.length > 0);
3644
+ let sentMediaCount = 0;
3645
+ let sentFailureFallback = false;
3591
3646
  if (localMediaToSend.length > 0) {
3592
3647
  log?.debug?.(`Sending ${localMediaToSend.length} local media via sendMedia auto-routing`);
3593
- await autoMediaBatch({
3648
+ sentMediaCount += await autoMediaBatch({
3594
3649
  qualifiedTarget,
3595
3650
  account,
3596
3651
  replyToId: event.messageId,
3597
3652
  mediaUrls: localMediaToSend,
3598
3653
  mediaSender: deps.mediaSender,
3654
+ ...actx.mediaAccess ? { mediaAccess: actx.mediaAccess } : {},
3655
+ ...actx.mediaLocalRoots ? { mediaLocalRoots: actx.mediaLocalRoots } : {},
3656
+ ...actx.mediaReadFile ? { mediaReadFile: actx.mediaReadFile } : {},
3599
3657
  log,
3600
3658
  onSuccess: (mediaPath) => `Sent local media: ${mediaPath}`,
3601
3659
  onResultError: (mediaPath, error) => `sendMedia(auto) error for ${mediaPath}: ${error}`,
3602
3660
  onThrownError: (mediaPath, error) => `sendMedia(auto) failed for ${mediaPath}: ${error}`
3603
3661
  });
3662
+ if (!hasVisibleTextOrInlineImage && sentMediaCount === 0) {
3663
+ await sendTextChunks(DEFAULT_MEDIA_SEND_ERROR, event, actx, sendWithRetry, consumeQuoteRef, deps);
3664
+ sentFailureFallback = true;
3665
+ }
3604
3666
  }
3605
3667
  if (toolMediaUrls.length > 0) {
3606
3668
  log?.debug?.(`Forwarding ${toolMediaUrls.length} tool-collected media URL(s) after block deliver`);
3607
- await autoMediaBatch({
3669
+ sentMediaCount += await autoMediaBatch({
3608
3670
  qualifiedTarget,
3609
3671
  account,
3610
3672
  replyToId: event.messageId,
3611
3673
  mediaUrls: toolMediaUrls,
3612
3674
  mediaSender: deps.mediaSender,
3675
+ ...actx.mediaAccess ? { mediaAccess: actx.mediaAccess } : {},
3676
+ ...actx.mediaLocalRoots ? { mediaLocalRoots: actx.mediaLocalRoots } : {},
3677
+ ...actx.mediaReadFile ? { mediaReadFile: actx.mediaReadFile } : {},
3613
3678
  log,
3614
3679
  onSuccess: (mediaUrl) => `Forwarded tool media: ${mediaUrl.slice(0, 80)}...`,
3615
3680
  onResultError: (_mediaUrl, error) => `Tool media forward error: ${error}`,
3616
3681
  onThrownError: (_mediaUrl, error) => `Tool media forward failed: ${error}`
3617
3682
  });
3683
+ if (!hasVisibleTextOrInlineImage && sentMediaCount === 0 && !sentFailureFallback) await sendTextChunks(DEFAULT_MEDIA_SEND_ERROR, event, actx, sendWithRetry, consumeQuoteRef, deps);
3618
3684
  toolMediaUrls.length = 0;
3619
3685
  }
3620
3686
  }
@@ -3622,8 +3688,8 @@ async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUr
3622
3688
  const { account, log } = actx;
3623
3689
  const httpImageUrls = [];
3624
3690
  const base64ImageUrls = [];
3625
- for (const url of imageUrls) if (url.startsWith("data:image/")) base64ImageUrls.push(url);
3626
- else if (url.startsWith("http://") || url.startsWith("https://")) httpImageUrls.push(url);
3691
+ for (const url of imageUrls) if (isImageDataUrl(url)) base64ImageUrls.push(url);
3692
+ else if (isHttpUrl(url)) httpImageUrls.push(url);
3627
3693
  log?.debug?.(`Image classification: httpUrls=${httpImageUrls.length}, base64=${base64ImageUrls.length}`);
3628
3694
  if (base64ImageUrls.length > 0) {
3629
3695
  log?.debug?.(`Sending ${base64ImageUrls.length} image(s) via Rich Media API...`);
@@ -3659,7 +3725,7 @@ async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUr
3659
3725
  for (const m of mdMatches) {
3660
3726
  const fullMatch = m[0];
3661
3727
  const imgUrl = m[2];
3662
- if ((imgUrl.startsWith("http://") || imgUrl.startsWith("https://")) && !hasQQBotImageSize(fullMatch)) try {
3728
+ if (isHttpUrl(imgUrl) && !hasQQBotImageSize(fullMatch)) try {
3663
3729
  const size = await getImageSize(imgUrl);
3664
3730
  result = result.replace(fullMatch, formatQQBotMarkdownImage(imgUrl, size));
3665
3731
  log?.debug?.(`Updated image with size: ${size ? `${size.width}x${size.height}` : "default"} - ${imgUrl.slice(0, 60)}...`);
@@ -3687,7 +3753,7 @@ async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUr
3687
3753
  }
3688
3754
  async function sendPlainTextReply(textWithoutImages, imageUrls, mdMatches, bareUrlMatches, event, actx, sendWithRetry, consumeQuoteRef, deps) {
3689
3755
  const { account, log } = actx;
3690
- const imgMediaTarget = resolveMediaTargetContext(event, account);
3756
+ const imgMediaTarget = resolveMediaTargetContext(event, actx);
3691
3757
  let result = textWithoutImages;
3692
3758
  for (const m of mdMatches) result = result.replace(m[0], "").trim();
3693
3759
  for (const m of bareUrlMatches) result = result.replace(m[0], "").trim();
@@ -3798,13 +3864,27 @@ function formatMediaTypeLabel(mediaType) {
3798
3864
  return mediaType[0].toUpperCase() + mediaType.slice(1);
3799
3865
  }
3800
3866
  function validateStructuredPayloadLocalPath(ctx, payloadPath, mediaType) {
3801
- const allowedPath = resolveTrustedOutboundMediaPath(payloadPath);
3802
- if (allowedPath) return allowedPath;
3867
+ const candidatePaths = resolveWorkspacePathCandidates(normalizePath(payloadPath), ctx.mediaAccess?.workspaceDir);
3868
+ const localRoots = resolveWorkspaceScopedLocalRoots(resolveOutboundMediaLocalRoots(ctx), ctx.mediaAccess?.workspaceDir);
3869
+ const allowMissingHostRead = Boolean(resolveStructuredPayloadReadFile(ctx));
3870
+ for (const candidatePath of candidatePaths) {
3871
+ const allowedPath = resolveTrustedOutboundMediaPath(candidatePath, { allowMissing: allowMissingHostRead });
3872
+ if (allowedPath) return allowedPath;
3873
+ if (localRoots) {
3874
+ const scopedPath = resolveLocalPathFromRootsSync({
3875
+ filePath: candidatePath,
3876
+ roots: localRoots,
3877
+ label: "QQ Bot local roots",
3878
+ allowMissing: allowMissingHostRead
3879
+ })?.path;
3880
+ if (scopedPath) return scopedPath;
3881
+ }
3882
+ }
3803
3883
  ctx.log?.error(`Blocked ${mediaType} payload local path outside QQ Bot media storage`);
3804
3884
  return null;
3805
3885
  }
3806
3886
  function isRemoteHttpUrl(p) {
3807
- return p.startsWith("http://") || p.startsWith("https://");
3887
+ return /^https?:\/\//i.test(p);
3808
3888
  }
3809
3889
  function isInlineImageDataUrl(p) {
3810
3890
  return /^data:image\/[^;]+;base64,/i.test(p);
@@ -3839,14 +3919,48 @@ function describeMediaTargetForLog(pathValue, isHttpUrl) {
3839
3919
  return "<invalid-url>";
3840
3920
  }
3841
3921
  }
3842
- /**
3843
- * Read a local file into memory for image base64 inlining.
3844
- *
3845
- * Non-image media (video / file) should pass `source: { localPath }` to
3846
- * `sender.sendMedia` directly — the sender pipeline handles chunked
3847
- * routing once this function validates the per-type ceiling.
3848
- */
3849
- async function readLocalFileForInlineBase64(filePath, fileType) {
3922
+ function resolveStructuredPayloadReadFile(ctx) {
3923
+ return ctx.mediaAccess?.readFile ?? ctx.mediaReadFile;
3924
+ }
3925
+ function assertBufferWithinTypeLimit(buffer, fileType) {
3926
+ const maxSize = getMaxUploadSize(fileType);
3927
+ if (buffer.length > maxSize) throw new Error(`File is too large (${formatFileSize(buffer.length)}); QQ Bot API limit is ${formatFileSize(maxSize)}`);
3928
+ }
3929
+ function imageBufferMatchesMime(buffer, mimeType) {
3930
+ if (mimeType === "image/png") return buffer.subarray(0, 8).equals(Buffer.from([
3931
+ 137,
3932
+ 80,
3933
+ 78,
3934
+ 71,
3935
+ 13,
3936
+ 10,
3937
+ 26,
3938
+ 10
3939
+ ]));
3940
+ if (mimeType === "image/jpeg") return buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255;
3941
+ if (mimeType === "image/gif") {
3942
+ const header = buffer.subarray(0, 6).toString("ascii");
3943
+ return header === "GIF87a" || header === "GIF89a";
3944
+ }
3945
+ if (mimeType === "image/webp") return buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP";
3946
+ if (mimeType === "image/bmp") return buffer.subarray(0, 2).toString("ascii") === "BM";
3947
+ return false;
3948
+ }
3949
+ async function readLocalFileForInlineBase64(ctx, filePath, fileType) {
3950
+ const mediaReadFile = resolveStructuredPayloadReadFile(ctx);
3951
+ if (mediaReadFile) {
3952
+ let buffer = null;
3953
+ try {
3954
+ buffer = await mediaReadFile(filePath);
3955
+ } catch (err) {
3956
+ ctx.log?.debug?.(`Structured payload host read failed: ${formatErrorMessage(err)}`);
3957
+ }
3958
+ if (buffer !== null) {
3959
+ assertBufferWithinTypeLimit(buffer, fileType);
3960
+ if (buffer.length === 0) throw new Error(`File is empty: ${filePath}`);
3961
+ return buffer;
3962
+ }
3963
+ }
3850
3964
  const opened = await openLocalFile(filePath, { maxSize: getMaxUploadSize(fileType) });
3851
3965
  try {
3852
3966
  return await opened.handle.readFile();
@@ -3854,15 +3968,20 @@ async function readLocalFileForInlineBase64(filePath, fileType) {
3854
3968
  await opened.close();
3855
3969
  }
3856
3970
  }
3857
- /**
3858
- * Enforce the per-{@link MediaFileType} upload ceiling before handing a
3859
- * local path to `sender.sendMedia`. The sender's internal `normalizeSource`
3860
- * uses an unlimited cap so it can accept whatever size the policy layer
3861
- * (outbound / reply-dispatcher) approves; the policy gate lives here.
3862
- *
3863
- * Returns the validated byte size. Throws via {@link openLocalFile} with a
3864
- * human-readable "File is too large" message when exceeding the ceiling.
3865
- */
3971
+ async function readPayloadFileBuffer(ctx, filePath, fileType) {
3972
+ const mediaReadFile = resolveStructuredPayloadReadFile(ctx);
3973
+ if (!mediaReadFile) return null;
3974
+ let buffer;
3975
+ try {
3976
+ buffer = await mediaReadFile(filePath);
3977
+ } catch (err) {
3978
+ ctx.log?.debug?.(`Structured payload host read failed: ${formatErrorMessage(err)}`);
3979
+ return null;
3980
+ }
3981
+ assertBufferWithinTypeLimit(buffer, fileType);
3982
+ if (buffer.length === 0) throw new Error(`File is empty: ${filePath}`);
3983
+ return buffer;
3984
+ }
3866
3985
  async function assertLocalFileWithinTypeLimit(filePath, fileType) {
3867
3986
  const opened = await openLocalFile(filePath, { maxSize: getMaxUploadSize(fileType) });
3868
3987
  try {
@@ -3884,15 +4003,15 @@ async function handleImagePayload(ctx, payload) {
3884
4003
  if (!imageUrl) return;
3885
4004
  const originalImagePath = payload.source === "file" ? imageUrl : void 0;
3886
4005
  if (payload.source === "file") try {
3887
- const fileBuffer = await readLocalFileForInlineBase64(imageUrl, 1);
3888
- const base64Data = fileBuffer.toString("base64");
4006
+ const fileBuffer = await readLocalFileForInlineBase64(ctx, imageUrl, 1);
3889
4007
  const mimeType = getImageMimeType(imageUrl);
3890
4008
  if (!mimeType) {
3891
4009
  const ext = normalizeLowercaseStringOrEmpty$1(path.extname(imageUrl));
3892
4010
  log?.error(`Unsupported image format: ${ext}`);
3893
4011
  return;
3894
4012
  }
3895
- imageUrl = `data:${mimeType};base64,${base64Data}`;
4013
+ if (!imageBufferMatchesMime(fileBuffer, mimeType)) throw new Error(`File is not an image: ${imageUrl}`);
4014
+ imageUrl = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
3896
4015
  log?.debug?.(`Converted local image to Base64 (size: ${formatFileSize(fileBuffer.length)})`);
3897
4016
  } catch (readErr) {
3898
4017
  log?.error(`Failed to read local image: ${readErr instanceof Error ? readErr.message : JSON.stringify(readErr)}`);
@@ -3910,8 +4029,8 @@ async function handleImagePayload(ctx, payload) {
3910
4029
  msgId: target.messageId,
3911
4030
  localPathForMeta: originalImagePath
3912
4031
  });
3913
- else if (deliveryTarget.type === "dm") await sendText(deliveryTarget, `![](${payload.path})`, creds, { msgId: target.messageId });
3914
- else await sendText(deliveryTarget, `![](${payload.path})`, creds, { msgId: target.messageId });
4032
+ else if (deliveryTarget.type === "dm") await sendText(deliveryTarget, `![](${imageUrl})`, creds, { msgId: target.messageId });
4033
+ else await sendText(deliveryTarget, `![](${imageUrl})`, creds, { msgId: target.messageId });
3915
4034
  }, log, account.accountId);
3916
4035
  log?.debug?.(`Sent image via media payload`);
3917
4036
  if (payload.caption) await sendTextToTarget(ctx, payload.caption);
@@ -4001,6 +4120,20 @@ async function handleVideoPayload(ctx, payload) {
4001
4120
  msgId: target.messageId
4002
4121
  });
4003
4122
  else {
4123
+ const payloadBuffer = await readPayloadFileBuffer(ctx, videoPath, 2);
4124
+ if (payloadBuffer) {
4125
+ await sendMedia({
4126
+ target: deliveryTarget,
4127
+ creds,
4128
+ kind: "video",
4129
+ source: {
4130
+ buffer: payloadBuffer,
4131
+ fileName: sanitizeFileName(path.basename(videoPath))
4132
+ },
4133
+ msgId: target.messageId
4134
+ });
4135
+ return;
4136
+ }
4004
4137
  const size = await assertLocalFileWithinTypeLimit(videoPath, 2);
4005
4138
  log?.debug?.(`Video local (${formatFileSize(size)}): ${describeMediaTargetForLog(videoPath, false)}`);
4006
4139
  await sendMedia({
@@ -4044,6 +4177,21 @@ async function handleFilePayload(ctx, payload) {
4044
4177
  fileName
4045
4178
  });
4046
4179
  else {
4180
+ const payloadBuffer = await readPayloadFileBuffer(ctx, filePath, 4);
4181
+ if (payloadBuffer) {
4182
+ await sendMedia({
4183
+ target: deliveryTarget,
4184
+ creds,
4185
+ kind: "file",
4186
+ source: {
4187
+ buffer: payloadBuffer,
4188
+ fileName
4189
+ },
4190
+ msgId: target.messageId,
4191
+ fileName
4192
+ });
4193
+ return;
4194
+ }
4047
4195
  const size = await assertLocalFileWithinTypeLimit(filePath, 4);
4048
4196
  log?.debug?.(`File local (${formatFileSize(size)}): ${describeMediaTargetForLog(filePath, false)}`);
4049
4197
  await sendMedia({
@@ -4186,7 +4334,7 @@ function findFirstClosedMediaTag(text, log) {
4186
4334
  * 媒体发送失败时,通过 onSendText 发送兜底文本通知用户。
4187
4335
  */
4188
4336
  async function executeSendQueue(queue, ctx, options = {}) {
4189
- const { mediaTarget, qualifiedTarget, account, replyToId, log } = ctx;
4337
+ const { mediaTarget, qualifiedTarget, account, replyToId, log, mediaAccess, mediaLocalRoots, mediaReadFile } = ctx;
4190
4338
  const prefix = mediaTarget.logPrefix ?? `[qqbot:${account.accountId}]`;
4191
4339
  /** 媒体发送失败时的兜底:通过 onSendText 发送错误文本给用户 */
4192
4340
  const sendFallbackText = async (errorMsg) => {
@@ -4255,7 +4403,10 @@ async function executeSendQueue(queue, ctx, options = {}) {
4255
4403
  mediaUrl: item.content,
4256
4404
  accountId: account.accountId,
4257
4405
  replyToId,
4258
- account
4406
+ account,
4407
+ ...mediaAccess ? { mediaAccess } : {},
4408
+ ...mediaLocalRoots ? { mediaLocalRoots } : {},
4409
+ ...mediaReadFile ? { mediaReadFile } : {}
4259
4410
  });
4260
4411
  if (result.error) {
4261
4412
  log?.error(`${prefix} sendMedia(auto) error: ${result.error}`);
@@ -5035,18 +5186,25 @@ var StreamingController = class {
5035
5186
  */
5036
5187
  function toMediaSendContext(ctx) {
5037
5188
  const { account, event, log } = ctx;
5189
+ const mediaAccessContext = {
5190
+ ...ctx.mediaAccess ? { mediaAccess: ctx.mediaAccess } : {},
5191
+ ...ctx.mediaLocalRoots ? { mediaLocalRoots: ctx.mediaLocalRoots } : {},
5192
+ ...ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}
5193
+ };
5038
5194
  return {
5039
5195
  mediaTarget: {
5040
5196
  targetType: event.type,
5041
5197
  targetId: event.type === "c2c" ? event.senderId : event.type === "group" ? event.groupOpenid : event.channelId,
5042
5198
  account,
5043
5199
  replyToId: event.messageId,
5044
- logPrefix: `[qqbot:${account.accountId}]`
5200
+ logPrefix: `[qqbot:${account.accountId}]`,
5201
+ ...mediaAccessContext
5045
5202
  },
5046
5203
  qualifiedTarget: event.type === "group" ? `qqbot:group:${event.groupOpenid}` : `qqbot:c2c:${event.senderId}`,
5047
5204
  account,
5048
5205
  replyToId: event.messageId,
5049
- log
5206
+ log,
5207
+ ...mediaAccessContext
5050
5208
  };
5051
5209
  }
5052
5210
  /**
@@ -5551,6 +5709,9 @@ function blockReplyTextForDelivery(payload) {
5551
5709
  function isSilentBlockReply(payload) {
5552
5710
  return !hasReplyMedia(payload) && isSilentBlockReplyText((payload.text ?? "").trim());
5553
5711
  }
5712
+ function isMediaOnlyBlockReply(payload) {
5713
+ return hasReplyMedia(payload) && isSilentBlockReplyText((payload.text ?? "").trim());
5714
+ }
5554
5715
  /**
5555
5716
  * Dispatch the AI reply for the given inbound context.
5556
5717
  *
@@ -5560,6 +5721,13 @@ function isSilentBlockReply(payload) {
5560
5721
  async function dispatchOutbound(inbound, deps) {
5561
5722
  const { runtime, cfg, account, log } = deps;
5562
5723
  const { event, qualifiedTarget } = inbound;
5724
+ const openClawCfg = cfg;
5725
+ const routeAgentId = inbound.route.agentId ?? resolveDefaultAgentId(openClawCfg);
5726
+ const workspaceDir = resolveAgentWorkspaceDir(openClawCfg, routeAgentId);
5727
+ const gatewayMediaContext = workspaceDir ? {
5728
+ mediaAccess: { workspaceDir },
5729
+ mediaLocalRoots: [workspaceDir]
5730
+ } : {};
5563
5731
  const replyCtx = {
5564
5732
  target: {
5565
5733
  type: event.type,
@@ -5571,7 +5739,8 @@ async function dispatchOutbound(inbound, deps) {
5571
5739
  },
5572
5740
  account,
5573
5741
  cfg,
5574
- log
5742
+ log,
5743
+ ...gatewayMediaContext
5575
5744
  };
5576
5745
  const sendWithRetry = (sendFn) => sendWithTokenRetry(account.appId, account.clientSecret, sendFn, log, account.accountId);
5577
5746
  const sendErrorMessage = (errorText) => sendErrorToTarget(replyCtx, errorText);
@@ -5609,7 +5778,8 @@ async function dispatchOutbound(inbound, deps) {
5609
5778
  mediaUrl,
5610
5779
  accountId: account.accountId,
5611
5780
  replyToId: event.messageId,
5612
- account
5781
+ account,
5782
+ ...gatewayMediaContext
5613
5783
  }).then((r) => {
5614
5784
  if (ac.signal.aborted) return {
5615
5785
  channel: "qqbot",
@@ -5633,13 +5803,10 @@ async function dispatchOutbound(inbound, deps) {
5633
5803
  }
5634
5804
  };
5635
5805
  const sendToolFallback = async () => {
5636
- if (toolMediaUrls.length > 0) {
5637
- for (const mediaUrl of toolMediaUrls) await sendToolMediaWithTimeout(mediaUrl, {
5638
- resultError: "Tool fallback error",
5639
- thrownError: "Tool fallback failed"
5640
- });
5641
- return;
5642
- }
5806
+ if (toolMediaUrls.length > 0) for (const mediaUrl of toolMediaUrls) await sendToolMediaWithTimeout(mediaUrl, {
5807
+ resultError: "Tool fallback error",
5808
+ thrownError: "Tool fallback failed"
5809
+ });
5643
5810
  if (toolTexts.length > 0) await sendErrorMessage(toolTexts.slice(-3).join("\n---\n").slice(0, 2e3));
5644
5811
  };
5645
5812
  const hasPendingToolFallbackPayload = () => toolTexts.length > 0 || toolMediaUrls.length > 0;
@@ -5731,7 +5898,8 @@ async function dispatchOutbound(inbound, deps) {
5731
5898
  }, {
5732
5899
  account,
5733
5900
  qualifiedTarget,
5734
- log
5901
+ log,
5902
+ ...gatewayMediaContext
5735
5903
  }, sendWithRetry, () => void 0, deliverDeps);
5736
5904
  }
5737
5905
  };
@@ -5740,7 +5908,7 @@ async function dispatchOutbound(inbound, deps) {
5740
5908
  accountId: account.accountId,
5741
5909
  direction: "outbound"
5742
5910
  });
5743
- const messagesConfig = runtime.channel.reply.resolveEffectiveMessagesConfig(cfg, inbound.route.agentId);
5911
+ const messagesConfig = runtime.channel.reply.resolveEffectiveMessagesConfig(cfg, routeAgentId);
5744
5912
  const useOfficialC2cStream = shouldUseOfficialC2cStream(account, event.type === "c2c" ? "c2c" : event.type === "group" ? "group" : "channel");
5745
5913
  const deliverToolProgressImmediately = shouldDeliverToolProgressImmediately(account, useOfficialC2cStream);
5746
5914
  let streamingController = null;
@@ -5760,12 +5928,12 @@ async function dispatchOutbound(inbound, deps) {
5760
5928
  groupOpenid: event.groupOpenid,
5761
5929
  channelId: event.channelId
5762
5930
  },
5763
- log
5931
+ log,
5932
+ ...gatewayMediaContext
5764
5933
  }
5765
5934
  });
5766
5935
  const cfgWithSession = cfg;
5767
- const agentId = inbound.route.agentId ?? "default";
5768
- const storePath = runtime.channel.session.resolveStorePath(cfgWithSession.session?.store, { agentId });
5936
+ const storePath = runtime.channel.session.resolveStorePath(cfgWithSession.session?.store, { agentId: routeAgentId });
5769
5937
  const dispatchPromise = runtime.channel.inbound.run({
5770
5938
  channel: "qqbot",
5771
5939
  accountId: inbound.route.accountId,
@@ -5811,7 +5979,8 @@ async function dispatchOutbound(inbound, deps) {
5811
5979
  }, {
5812
5980
  account,
5813
5981
  qualifiedTarget,
5814
- log
5982
+ log,
5983
+ ...gatewayMediaContext
5815
5984
  }, sendWithRetry, () => void 0, deliverDeps);
5816
5985
  recordOutbound();
5817
5986
  return;
@@ -5829,7 +5998,8 @@ async function dispatchOutbound(inbound, deps) {
5829
5998
  mediaUrl,
5830
5999
  accountId: account.accountId,
5831
6000
  replyToId: event.messageId,
5832
- account
6001
+ account,
6002
+ ...gatewayMediaContext
5833
6003
  });
5834
6004
  } catch {}
5835
6005
  return;
@@ -5844,7 +6014,7 @@ async function dispatchOutbound(inbound, deps) {
5844
6014
  return;
5845
6015
  }
5846
6016
  hasVisibleBlockResponse = true;
5847
- if (streamingController && !streamingController.isTerminalPhase) {
6017
+ if (streamingController && !streamingController.isTerminalPhase && !isMediaOnlyBlockReply(payload)) {
5848
6018
  try {
5849
6019
  await streamingController.onDeliver(payload);
5850
6020
  } catch (err) {
@@ -5881,7 +6051,8 @@ async function dispatchOutbound(inbound, deps) {
5881
6051
  const deliverActx = {
5882
6052
  account,
5883
6053
  qualifiedTarget,
5884
- log
6054
+ log,
6055
+ ...gatewayMediaContext
5885
6056
  };
5886
6057
  const mediaResult = await parseAndSendMediaTags(replyText, deliverEvent, deliverActx, sendWithRetry, consumeQuoteRef, deliverDeps);
5887
6058
  if (mediaResult.handled) {
@@ -6002,7 +6173,7 @@ async function buildCtxPayload(inbound, runtime, cfg) {
6002
6173
  id: inbound.peerId
6003
6174
  },
6004
6175
  route: {
6005
- agentId: inbound.route.agentId ?? "main",
6176
+ agentId: inbound.route.agentId ?? resolveDefaultAgentId(cfg),
6006
6177
  routeSessionKey: inbound.route.sessionKey,
6007
6178
  accountId: inbound.route.accountId
6008
6179
  },