@openclaw/qqbot 2026.5.7 → 2026.5.9-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-BJfhwrPg.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-CqRmSFJZ.js";
2
2
  import { a as resolveQQBotAccount, i as resolveDefaultQQBotAccountId, n as applyQQBotAccountConfig, r as listQQBotAccountIds, t as DEFAULT_ACCOUNT_ID } from "./config-D6545NkC.js";
3
- import { t as qqbotPlugin } from "./channel-CC2YO9fj.js";
4
- import { C as debugLog, L as formatErrorMessage, S as debugError, o as getAccessToken } from "./sender-p-B14eLG.js";
5
- import { t as qqbotSetupPlugin } from "./channel.setup-ByCRAe5H.js";
6
- import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-Cjwx7OwW.js";
3
+ import { t as qqbotPlugin } from "./channel-nsJeL4qp.js";
4
+ import { L as formatErrorMessage, S as debugLog, o as getAccessToken, x as debugError } from "./sender-CgdXGY2H.js";
5
+ import { t as qqbotSetupPlugin } from "./channel.setup-DS8qSqfn.js";
6
+ import { r as getFrameworkCommands, t as getRequestContext } from "./request-context-DVsN666m.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
  /**
@@ -3,17 +3,20 @@ import { l as ensurePlatformAdapter, u as getBridgeLogger } from "./resolve-D_06
3
3
  import { a as resolveApprovalTarget } from "./approval-cg0SVahb.js";
4
4
  import { a as resolveQQBotAccount, n as applyQQBotAccountConfig, t as DEFAULT_ACCOUNT_ID$1 } from "./config-D6545NkC.js";
5
5
  import { a as resolveQQBotExecApprovalConfig, i as matchesQQBotApprovalAccount, n as isQQBotExecApprovalAuthorizedSender, o as shouldHandleQQBotExecApprovalRequest, r as isQQBotExecApprovalClientEnabled, t as isQQBotExecApprovalApprover } from "./exec-approvals-COUsM6wZ.js";
6
- import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-DFcjQw73.js";
6
+ import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-BUrvFkQH.js";
7
7
  import { n as writeOpenClawConfigThroughRuntime, t as toGatewayAccount } from "./narrowing-BoieBTIU.js";
8
- import { t as getQQBotRuntime } from "./runtime-BJAS3eXW.js";
9
- import { n as normalizeTarget, s as getQQBotDataPath, t as looksLikeQQBotTarget } from "./target-parser-Y0prnrXD.js";
8
+ import { t as getQQBotRuntime } from "./runtime-C3Qpr4Ph.js";
9
+ import { n as normalizeTarget, s as getQQBotDataPath, t as looksLikeQQBotTarget } from "./target-parser-DQ6J3Qe-.js";
10
10
  import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
11
+ import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
11
12
  import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
12
13
  import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
13
14
  import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
14
15
  import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
15
16
  import fs from "node:fs";
17
+ import { replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
16
18
  import path from "node:path";
19
+ import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
17
20
  //#region extensions/qqbot/src/bridge/approval/capability.ts
18
21
  /**
19
22
  * QQ Bot Approval Capability — entry point.
@@ -153,7 +156,7 @@ function createQQBotApprovalCapability() {
153
156
  },
154
157
  load: async () => {
155
158
  ensurePlatformAdapter();
156
- return (await import("./handler-runtime-Bqm6N0WG.js")).qqbotApprovalNativeRuntime;
159
+ return (await import("./handler-runtime-DraqODp_.js")).qqbotApprovalNativeRuntime;
157
160
  }
158
161
  })
159
162
  });
@@ -229,16 +232,17 @@ function saveCredentialBackup(accountId, appId, clientSecret) {
229
232
  if (!appId || !clientSecret) return;
230
233
  try {
231
234
  const backupPath = getCredentialBackupFile(accountId);
232
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
233
235
  const data = {
234
236
  accountId,
235
237
  appId,
236
238
  clientSecret,
237
239
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
238
240
  };
239
- const tmpPath = `${backupPath}.tmp`;
240
- fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
241
- fs.renameSync(tmpPath, backupPath);
241
+ replaceFileAtomicSync({
242
+ filePath: backupPath,
243
+ content: `${JSON.stringify(data, null, 2)}\n`,
244
+ tempPrefix: ".qqbot-credential-backup"
245
+ });
242
246
  } catch {}
243
247
  }
244
248
  /**
@@ -251,23 +255,20 @@ function saveCredentialBackup(accountId, appId, clientSecret) {
251
255
  function loadCredentialBackup(accountId) {
252
256
  try {
253
257
  if (accountId) {
254
- const newPath = getCredentialBackupFile(accountId);
255
- if (fs.existsSync(newPath)) {
256
- const data = JSON.parse(fs.readFileSync(newPath, "utf8"));
257
- if (data?.appId && data.clientSecret) return data;
258
- }
258
+ const data = loadJsonFile(getCredentialBackupFile(accountId));
259
+ if (data?.appId && data.clientSecret) return data;
259
260
  }
260
261
  const legacy = getLegacyCredentialBackupFile();
261
- if (fs.existsSync(legacy)) {
262
- const data = JSON.parse(fs.readFileSync(legacy, "utf8"));
262
+ const data = loadJsonFile(legacy);
263
+ if (data) {
263
264
  if (!data?.appId || !data?.clientSecret) return null;
264
265
  if (accountId && data.accountId !== accountId) return null;
265
266
  if (data.accountId) try {
266
- const backupPath = getCredentialBackupFile(data.accountId);
267
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
268
- const tmpPath = `${backupPath}.tmp`;
269
- fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
270
- fs.renameSync(tmpPath, backupPath);
267
+ replaceFileAtomicSync({
268
+ filePath: getCredentialBackupFile(data.accountId),
269
+ content: `${JSON.stringify(data, null, 2)}\n`,
270
+ tempPrefix: ".qqbot-credential-backup"
271
+ });
271
272
  fs.unlinkSync(legacy);
272
273
  } catch {}
273
274
  return data;
@@ -342,11 +343,99 @@ function clearAccountCredentials(cfg, accountId) {
342
343
  }
343
344
  //#endregion
344
345
  //#region extensions/qqbot/src/channel.ts
345
- let _gatewayModulePromise;
346
+ let gatewayModulePromise;
346
347
  function loadGatewayModule() {
347
- _gatewayModulePromise ??= import("./gateway-Cs3-_on9.js");
348
- return _gatewayModulePromise;
348
+ gatewayModulePromise ??= import("./gateway-Dcs6Ip8b.js");
349
+ return gatewayModulePromise;
350
+ }
351
+ function createQQBotSendReceipt(params) {
352
+ const messageId = params.messageId?.trim();
353
+ return createMessageReceiptFromOutboundResults({
354
+ results: messageId ? [{
355
+ channel: "qqbot",
356
+ messageId,
357
+ conversationId: params.target
358
+ }] : [],
359
+ threadId: params.target,
360
+ kind: params.kind
361
+ });
362
+ }
363
+ async function sendQQBotText(params) {
364
+ await loadGatewayModule();
365
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
366
+ const { sendText } = await import("./outbound-CqRmSFJZ.js").then((n) => n.t);
367
+ const result = await sendText({
368
+ to: params.to,
369
+ text: params.text,
370
+ accountId: params.accountId,
371
+ replyToId: params.replyToId,
372
+ account: toGatewayAccount(account)
373
+ });
374
+ return {
375
+ channel: "qqbot",
376
+ messageId: result.messageId ?? "",
377
+ receipt: createQQBotSendReceipt({
378
+ messageId: result.messageId,
379
+ target: params.to,
380
+ kind: "text"
381
+ }),
382
+ meta: result.error ? { error: result.error } : void 0
383
+ };
384
+ }
385
+ async function sendQQBotMedia(params) {
386
+ await loadGatewayModule();
387
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
388
+ const { sendMedia } = await import("./outbound-CqRmSFJZ.js").then((n) => n.t);
389
+ const result = await sendMedia({
390
+ to: params.to,
391
+ text: params.text ?? "",
392
+ mediaUrl: params.mediaUrl ?? "",
393
+ accountId: params.accountId,
394
+ replyToId: params.replyToId,
395
+ account: toGatewayAccount(account)
396
+ });
397
+ return {
398
+ channel: "qqbot",
399
+ messageId: result.messageId ?? "",
400
+ receipt: createQQBotSendReceipt({
401
+ messageId: result.messageId,
402
+ target: params.to,
403
+ kind: "media"
404
+ }),
405
+ meta: result.error ? { error: result.error } : void 0
406
+ };
407
+ }
408
+ function toQQBotMessageSendResult(result) {
409
+ return {
410
+ messageId: result.messageId,
411
+ receipt: result.receipt
412
+ };
349
413
  }
414
+ const qqbotMessageAdapter = defineChannelMessageAdapter({
415
+ id: "qqbot",
416
+ durableFinal: { capabilities: {
417
+ text: true,
418
+ media: true,
419
+ replyTo: true
420
+ } },
421
+ send: {
422
+ text: async (ctx) => toQQBotMessageSendResult(await sendQQBotText({
423
+ cfg: ctx.cfg,
424
+ to: ctx.to,
425
+ text: ctx.text,
426
+ accountId: ctx.accountId,
427
+ replyToId: ctx.replyToId
428
+ })),
429
+ media: async (ctx) => toQQBotMessageSendResult(await sendQQBotMedia({
430
+ cfg: ctx.cfg,
431
+ to: ctx.to,
432
+ text: ctx.text,
433
+ mediaUrl: ctx.mediaUrl,
434
+ accountId: ctx.accountId,
435
+ replyToId: ctx.replyToId
436
+ }))
437
+ }
438
+ });
350
439
  const EXEC_APPROVAL_COMMAND_RE = /\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+(?:allow-once|allow-always|always|deny)\b/i;
351
440
  function persistAccountCredentialSnapshot(account) {
352
441
  if (account.appId && account.clientSecret) saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
@@ -389,6 +478,7 @@ const qqbotPlugin = {
389
478
  },
390
479
  setup: { ...qqbotSetupAdapterShared },
391
480
  approvalCapability: getQQBotApprovalCapability(),
481
+ message: qqbotMessageAdapter,
392
482
  messaging: {
393
483
  targetPrefixes: ["qqbot"],
394
484
  /** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
@@ -410,41 +500,21 @@ const qqbotPlugin = {
410
500
  payload,
411
501
  hint
412
502
  }),
413
- sendText: async ({ to, text, accountId, replyToId, cfg }) => {
414
- await loadGatewayModule();
415
- const account = resolveQQBotAccount(cfg, accountId);
416
- const { sendText } = await import("./outbound-BJfhwrPg.js").then((n) => n.t);
417
- const result = await sendText({
418
- to,
419
- text,
420
- accountId,
421
- replyToId,
422
- account: toGatewayAccount(account)
423
- });
424
- return {
425
- channel: "qqbot",
426
- messageId: result.messageId ?? "",
427
- meta: result.error ? { error: result.error } : void 0
428
- };
429
- },
430
- sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
431
- await loadGatewayModule();
432
- const account = resolveQQBotAccount(cfg, accountId);
433
- const { sendMedia } = await import("./outbound-BJfhwrPg.js").then((n) => n.t);
434
- const result = await sendMedia({
435
- to,
436
- text: text ?? "",
437
- mediaUrl: mediaUrl ?? "",
438
- accountId,
439
- replyToId,
440
- account: toGatewayAccount(account)
441
- });
442
- return {
443
- channel: "qqbot",
444
- messageId: result.messageId ?? "",
445
- meta: result.error ? { error: result.error } : void 0
446
- };
447
- }
503
+ sendText: async ({ to, text, accountId, replyToId, cfg }) => await sendQQBotText({
504
+ cfg,
505
+ to,
506
+ text,
507
+ accountId,
508
+ replyToId
509
+ }),
510
+ sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => await sendQQBotMedia({
511
+ cfg,
512
+ to,
513
+ text,
514
+ mediaUrl,
515
+ accountId,
516
+ replyToId
517
+ })
448
518
  },
449
519
  gateway: {
450
520
  startAccount: async (ctx) => {
@@ -1,2 +1,2 @@
1
- import { t as qqbotPlugin } from "./channel-CC2YO9fj.js";
1
+ import { t as qqbotPlugin } from "./channel-nsJeL4qp.js";
2
2
  export { qqbotPlugin };
@@ -1,5 +1,5 @@
1
1
  import "./resolve-D_06fV6-.js";
2
- import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-DFcjQw73.js";
2
+ import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-BUrvFkQH.js";
3
3
  //#region extensions/qqbot/src/channel.setup.ts
4
4
  /**
5
5
  * Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
@@ -6,7 +6,7 @@ import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAc
6
6
  import { DEFAULT_ACCOUNT_ID, createStandardChannelSetupStatus, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
7
7
  import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
8
8
  import { AllowFromListSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
9
- import { z } from "zod";
9
+ import { z } from "openclaw/plugin-sdk/zod";
10
10
  //#region extensions/qqbot/src/engine/config/setup-logic.ts
11
11
  /**
12
12
  * QQBot setup business logic (pure layer).
@@ -1,20 +1,24 @@
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-BJfhwrPg.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-CqRmSFJZ.js";
2
2
  import { c as getPlatformAdapter, i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-Ci6NM5DE.js";
3
3
  import { d as setBridgeLogger, l as ensurePlatformAdapter, s as resolveAccountBase } from "./resolve-D_06fV6-.js";
4
4
  import { i as parseApprovalButtonData } from "./approval-cg0SVahb.js";
5
5
  import { t as toGatewayAccount } from "./narrowing-BoieBTIU.js";
6
- import { B as StreamInputState, C as debugLog, E as getNextMsgSeq, I as formatDuration, L as formatErrorMessage, N as getImageMimeType, P as getMaxUploadSize, R as StreamContentType, S as debugError, a as createRawInputNotifyFn, b as withTokenRetry, c as getMessageApi, d as initSender, f as onMessageSent, g as sendText, h as sendMedia$1, i as clearTokenCache, j as formatFileSize, k as downloadFile, 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, v as startBackgroundTokenRefresh, w as debugWarn, x as openLocalFile, y as stopBackgroundTokenRefresh, z as StreamInputMode } from "./sender-p-B14eLG.js";
7
- import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-BJAS3eXW.js";
8
- import { a as getHomeDir, c as getQQBotMediaDir, d as isLocalPath, f as isWindows, i as checkSilkWasmAvailable, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath$1, s as getQQBotDataPath, u as getTempDir } from "./target-parser-Y0prnrXD.js";
9
- import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-Cjwx7OwW.js";
6
+ import { B as StreamInputState, C as debugWarn, D as openLocalFile, I as formatDuration, L as formatErrorMessage, N as getImageMimeType, P as getMaxUploadSize, R as StreamContentType, S as debugLog, T as getNextMsgSeq, a as createRawInputNotifyFn, b as withTokenRetry, c as getMessageApi, d as initSender, f as onMessageSent, g as sendText, h as sendMedia$1, i as clearTokenCache, j as formatFileSize, k as downloadFile, 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, v as startBackgroundTokenRefresh, x as debugError, y as stopBackgroundTokenRefresh, z as StreamInputMode } from "./sender-CgdXGY2H.js";
7
+ import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-C3Qpr4Ph.js";
8
+ import { a as getHomeDir, c as getQQBotMediaDir, d as isLocalPath, f as isWindows, i as checkSilkWasmAvailable, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath$1, s as getQQBotDataPath, u as getTempDir } from "./target-parser-DQ6J3Qe-.js";
9
+ import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-DVsN666m.js";
10
10
  import * as fs$1 from "node:fs";
11
11
  import fs from "node:fs";
12
12
  import * as os$1 from "node:os";
13
13
  import crypto from "node:crypto";
14
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
15
+ import { appendRegularFileSync, privateFileStoreSync, readRegularFileSync, replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
14
16
  import * as path$1 from "node:path";
15
17
  import path from "node:path";
18
+ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
16
19
  import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime";
17
20
  import WebSocket from "ws";
21
+ import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
18
22
  import { Buffer as Buffer$1 } from "node:buffer";
19
23
  import { fileURLToPath } from "node:url";
20
24
  import { implicitMentionKindWhen, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
@@ -258,7 +262,10 @@ function ensureDir$2() {
258
262
  function appendLine(line) {
259
263
  try {
260
264
  ensureDir$2();
261
- fs.appendFileSync(getRefIndexFile(), JSON.stringify(line) + "\n", "utf-8");
265
+ appendRegularFileSync({
266
+ filePath: getRefIndexFile(),
267
+ content: JSON.stringify(line) + "\n"
268
+ });
262
269
  totalLinesOnDisk++;
263
270
  } catch (err) {
264
271
  debugError(`[ref-index-store] Failed to append: ${formatErrorMessage(err)}`);
@@ -273,7 +280,6 @@ function compactFile() {
273
280
  try {
274
281
  ensureDir$2();
275
282
  const refIndexFile = getRefIndexFile();
276
- const tmpPath = refIndexFile + ".tmp";
277
283
  const lines = [];
278
284
  for (const [key, entry] of cache) lines.push(JSON.stringify({
279
285
  k: key,
@@ -287,8 +293,11 @@ function compactFile() {
287
293
  },
288
294
  t: entry._createdAt
289
295
  }));
290
- fs.writeFileSync(tmpPath, lines.join("\n") + "\n", "utf-8");
291
- fs.renameSync(tmpPath, refIndexFile);
296
+ replaceFileAtomicSync({
297
+ filePath: refIndexFile,
298
+ content: `${lines.join("\n")}\n`,
299
+ tempPrefix: ".qqbot-ref-index"
300
+ });
292
301
  totalLinesOnDisk = cache.size;
293
302
  debugLog(`[ref-index-store] Compacted: ${before} lines → ${totalLinesOnDisk} lines`);
294
303
  } catch (err) {
@@ -778,9 +787,8 @@ function loadUsersFromFile() {
778
787
  usersCache = /* @__PURE__ */ new Map();
779
788
  try {
780
789
  const knownUsersFile = getKnownUsersFile();
781
- if (fs.existsSync(knownUsersFile)) {
782
- const data = fs.readFileSync(knownUsersFile, "utf-8");
783
- const users = JSON.parse(data);
790
+ const users = privateFileStoreSync(path.dirname(knownUsersFile)).readJsonIfExists(path.basename(knownUsersFile));
791
+ if (users) {
784
792
  for (const user of users) usersCache.set(makeUserKey(user), user);
785
793
  debugLog(`[known-users] Loaded ${usersCache.size} users`);
786
794
  }
@@ -801,7 +809,8 @@ function doSaveUsersToFile() {
801
809
  if (!usersCache || !isDirty) return;
802
810
  try {
803
811
  ensureDir$1();
804
- fs.writeFileSync(getKnownUsersFile(), JSON.stringify(Array.from(usersCache.values()), null, 2), "utf-8");
812
+ const filePath = getKnownUsersFile();
813
+ privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), Array.from(usersCache.values()));
805
814
  isDirty = false;
806
815
  } catch (err) {
807
816
  debugError(`[known-users] Failed to save users: ${formatErrorMessage(err)}`);
@@ -878,13 +887,15 @@ function getCandidateSessionPaths(accountId) {
878
887
  function loadSession(accountId, expectedAppId) {
879
888
  try {
880
889
  let filePath = null;
881
- for (const candidatePath of getCandidateSessionPaths(accountId)) if (fs.existsSync(candidatePath)) {
882
- filePath = candidatePath;
883
- break;
890
+ let state = null;
891
+ for (const candidatePath of getCandidateSessionPaths(accountId)) {
892
+ state = privateFileStoreSync(path.dirname(candidatePath)).readJsonIfExists(path.basename(candidatePath));
893
+ if (state) {
894
+ filePath = candidatePath;
895
+ break;
896
+ }
884
897
  }
885
- if (!filePath) return null;
886
- const data = fs.readFileSync(filePath, "utf-8");
887
- const state = JSON.parse(data);
898
+ if (!filePath || !state) return null;
888
899
  const now = Date.now();
889
900
  if (now - state.savedAt > SESSION_EXPIRE_TIME) {
890
901
  debugLog(`[session-store] Session expired for ${accountId}, age: ${Math.round((now - state.savedAt) / 1e3)}s`);
@@ -958,7 +969,7 @@ function doSaveSession(state) {
958
969
  ...state,
959
970
  savedAt: Date.now()
960
971
  };
961
- fs.writeFileSync(filePath, JSON.stringify(stateToSave, null, 2), "utf-8");
972
+ privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), stateToSave);
962
973
  if (legacyPath !== filePath && fs.existsSync(legacyPath)) fs.unlinkSync(legacyPath);
963
974
  debugLog(`[session-store] Saved session for ${state.accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}`);
964
975
  } catch (err) {
@@ -1740,6 +1751,15 @@ var ReconnectState = class {
1740
1751
  }
1741
1752
  };
1742
1753
  //#endregion
1754
+ //#region extensions/qqbot/src/engine/gateway/ws-client.ts
1755
+ async function createQQWSClient(options) {
1756
+ const wsAgent = await resolveAmbientNodeProxyAgent();
1757
+ return new WebSocket(options.gatewayUrl, {
1758
+ headers: { "User-Agent": options.userAgent },
1759
+ ...wsAgent ? { agent: wsAgent } : {}
1760
+ });
1761
+ }
1762
+ //#endregion
1743
1763
  //#region extensions/qqbot/src/engine/gateway/gateway-connection.ts
1744
1764
  /**
1745
1765
  * GatewayConnection — WebSocket lifecycle, heartbeat, reconnect, and session persistence.
@@ -1852,7 +1872,10 @@ var GatewayConnection = class {
1852
1872
  log?.info(`✅ Access token obtained successfully`);
1853
1873
  const gatewayUrl = await getGatewayUrl(accessToken, account.appId);
1854
1874
  log?.info(`Connecting to ${gatewayUrl}`);
1855
- const ws = new WebSocket(gatewayUrl, { headers: { "User-Agent": getPluginUserAgent() } });
1875
+ const ws = await createQQWSClient({
1876
+ gatewayUrl,
1877
+ userAgent: getPluginUserAgent()
1878
+ });
1856
1879
  this.currentWs = ws;
1857
1880
  const slashCtx = {
1858
1881
  account,
@@ -2026,20 +2049,28 @@ async function transcribeAudio(audioPath, cfg) {
2026
2049
  if (!sttCfg) return null;
2027
2050
  const fileBuffer = fs$1.readFileSync(audioPath);
2028
2051
  const fileName = sanitizeFileName(path.basename(audioPath));
2029
- const mime = fileName.endsWith(".wav") ? "audio/wav" : fileName.endsWith(".mp3") ? "audio/mpeg" : fileName.endsWith(".ogg") ? "audio/ogg" : "application/octet-stream";
2052
+ const mime = mimeTypeFromFilePath(fileName) ?? "application/octet-stream";
2030
2053
  const form = new FormData();
2031
2054
  form.append("file", new Blob([fileBuffer], { type: mime }), fileName);
2032
2055
  form.append("model", sttCfg.model);
2033
- const resp = await fetch(`${sttCfg.baseUrl}/audio/transcriptions`, {
2034
- method: "POST",
2035
- headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
2036
- body: form
2056
+ const { response: resp, release } = await fetchWithSsrFGuard({
2057
+ url: `${sttCfg.baseUrl}/audio/transcriptions`,
2058
+ auditContext: "qqbot-stt",
2059
+ init: {
2060
+ method: "POST",
2061
+ headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
2062
+ body: form
2063
+ }
2037
2064
  });
2038
- if (!resp.ok) {
2039
- const detail = await resp.text().catch(() => "");
2040
- throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
2065
+ try {
2066
+ if (!resp.ok) {
2067
+ const detail = await resp.text().catch(() => "");
2068
+ throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
2069
+ }
2070
+ return normalizeOptionalString((await resp.json()).text) ?? null;
2071
+ } finally {
2072
+ await release();
2041
2073
  }
2042
- return normalizeOptionalString((await resp.json()).text) ?? null;
2043
2074
  }
2044
2075
  //#endregion
2045
2076
  //#region extensions/qqbot/src/engine/gateway/inbound-attachments.ts
@@ -5552,8 +5583,13 @@ function stripAmrHeader(buf) {
5552
5583
  }
5553
5584
  /** Convert a SILK or AMR voice file to WAV format. */
5554
5585
  async function convertSilkToWav(inputPath, outputDir) {
5555
- if (!fs$1.existsSync(inputPath)) return null;
5556
- const strippedBuf = stripAmrHeader(fs$1.readFileSync(inputPath));
5586
+ let fileBuf;
5587
+ try {
5588
+ fileBuf = readRegularFileSync({ filePath: inputPath }).buffer;
5589
+ } catch {
5590
+ return null;
5591
+ }
5592
+ const strippedBuf = stripAmrHeader(fileBuf);
5557
5593
  const rawData = new Uint8Array(strippedBuf.buffer, strippedBuf.byteOffset, strippedBuf.byteLength);
5558
5594
  const silk = await loadSilkWasm();
5559
5595
  if (!silk || !silk.isSilk(rawData)) return null;
@@ -5642,8 +5678,12 @@ function normalizeFormats(formats) {
5642
5678
  * Attempts conversion via direct QQ-native upload → WASM decoders → null fallback chain.
5643
5679
  */
5644
5680
  async function audioFileToSilkBase64(filePath, directUploadFormats) {
5645
- if (!fs$1.existsSync(filePath)) return null;
5646
- const buf = fs$1.readFileSync(filePath);
5681
+ let buf;
5682
+ try {
5683
+ buf = readRegularFileSync({ filePath }).buffer;
5684
+ } catch {
5685
+ return null;
5686
+ }
5647
5687
  if (buf.length === 0) {
5648
5688
  debugError(`[audio-convert] file is empty: ${filePath}`);
5649
5689
  return null;
@@ -2,7 +2,7 @@ import { l as ensurePlatformAdapter, u as getBridgeLogger } from "./resolve-D_06
2
2
  import { a as resolveApprovalTarget, n as buildExecApprovalText, r as buildPluginApprovalText, t as buildApprovalKeyboard } from "./approval-cg0SVahb.js";
3
3
  import { a as resolveQQBotAccount } from "./config-D6545NkC.js";
4
4
  import { a as resolveQQBotExecApprovalConfig, i as matchesQQBotApprovalAccount, o as shouldHandleQQBotExecApprovalRequest, r as isQQBotExecApprovalClientEnabled } from "./exec-approvals-COUsM6wZ.js";
5
- import { c as getMessageApi, t as accountToCreds } from "./sender-p-B14eLG.js";
5
+ import { c as getMessageApi, t as accountToCreds } from "./sender-CgdXGY2H.js";
6
6
  import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
7
7
  import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
8
8
  //#region extensions/qqbot/src/bridge/approval/handler-runtime.ts
@@ -1,8 +1,9 @@
1
1
  import { i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-Ci6NM5DE.js";
2
- import { A as fileExistsAsync, C as debugLog, D as UPLOAD_PREPARE_FALLBACK_CODE, F as readFileAsync, L as formatErrorMessage, M as getFileTypeName, N as getImageMimeType, O as checkFileSize, P as getMaxUploadSize, S as debugError, T as UploadDailyLimitExceededError, g as sendText$1, h as sendMedia$1, j as formatFileSize, k as downloadFile, t as accountToCreds, u as initApiConfig, w as debugWarn } from "./sender-p-B14eLG.js";
3
- import { c as getQQBotMediaDir, d as isLocalPath, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath, r as parseTarget$1 } from "./target-parser-Y0prnrXD.js";
4
- import fs from "node:fs";
2
+ import { A as fileExistsAsync, C as debugWarn, E as UPLOAD_PREPARE_FALLBACK_CODE, F as readFileAsync, L as formatErrorMessage, M as getFileTypeName, N as getImageMimeType, O as checkFileSize, P as getMaxUploadSize, S as debugLog, g as sendText$1, h as sendMedia$1, j as formatFileSize, k as downloadFile, t as accountToCreds, u as initApiConfig, w as UploadDailyLimitExceededError, x as debugError } from "./sender-CgdXGY2H.js";
3
+ import { c as getQQBotMediaDir, d as isLocalPath, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath, r as parseTarget$1 } from "./target-parser-DQ6J3Qe-.js";
4
+ import { pathExistsSync, resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
5
5
  import path from "node:path";
6
+ import { getFileExtension } from "openclaw/plugin-sdk/media-mime";
6
7
  //#region \0rolldown/runtime.js
7
8
  var __defProp = Object.defineProperty;
8
9
  var __exportAll = (all, no_symbols) => {
@@ -239,53 +240,22 @@ const qqBotMediaKindLabel = {
239
240
  function isHttpOrDataSource(pathValue) {
240
241
  return pathValue.startsWith("http://") || pathValue.startsWith("https://") || pathValue.startsWith("data:");
241
242
  }
242
- function isPathWithinRoot(candidate, root) {
243
- const relative = path.relative(root, candidate);
244
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
245
- }
246
243
  function resolveMissingPathWithinMediaRoot(normalizedPath) {
247
244
  const resolvedCandidate = path.resolve(normalizedPath);
248
- if (fs.existsSync(resolvedCandidate)) return null;
249
- const allowedRoot = path.resolve(getQQBotMediaDir());
250
- let canonicalAllowedRoot;
251
- try {
252
- canonicalAllowedRoot = fs.realpathSync(allowedRoot);
253
- } catch {
254
- return null;
255
- }
256
- const missingSegments = [];
257
- let cursor = resolvedCandidate;
258
- while (!fs.existsSync(cursor)) {
259
- const parent = path.dirname(cursor);
260
- if (parent === cursor) break;
261
- missingSegments.unshift(path.basename(cursor));
262
- cursor = parent;
263
- }
264
- if (!fs.existsSync(cursor)) return null;
265
- let canonicalCursor;
266
- try {
267
- canonicalCursor = fs.realpathSync(cursor);
268
- } catch {
269
- return null;
270
- }
271
- const canonicalCandidate = missingSegments.length > 0 ? path.join(canonicalCursor, ...missingSegments) : canonicalCursor;
272
- return isPathWithinRoot(canonicalCandidate, canonicalAllowedRoot) ? canonicalCandidate : null;
245
+ if (pathExistsSync(resolvedCandidate)) return null;
246
+ return resolveLocalPathFromRootsSync({
247
+ filePath: resolvedCandidate,
248
+ roots: [getQQBotMediaDir()],
249
+ label: "QQ Bot media storage",
250
+ allowMissing: true
251
+ })?.path ?? null;
273
252
  }
274
253
  function resolveExistingPathWithinRoots(normalizedPath, allowedRoots) {
275
- const resolvedCandidate = path.resolve(normalizedPath);
276
- if (!fs.existsSync(resolvedCandidate)) return null;
277
- let canonicalCandidate;
278
- try {
279
- canonicalCandidate = fs.realpathSync(resolvedCandidate);
280
- } catch {
281
- return null;
282
- }
283
- for (const root of allowedRoots) {
284
- const resolvedRoot = path.resolve(root);
285
- const canonicalRoot = fs.existsSync(resolvedRoot) ? fs.realpathSync(resolvedRoot) : resolvedRoot;
286
- if (isPathWithinRoot(canonicalCandidate, canonicalRoot)) return canonicalCandidate;
287
- }
288
- return null;
254
+ return resolveLocalPathFromRootsSync({
255
+ filePath: normalizedPath,
256
+ roots: allowedRoots,
257
+ label: "QQ Bot local roots"
258
+ })?.path ?? null;
289
259
  }
290
260
  function resolveOutboundMediaPath(rawPath, mediaKind, options = {}) {
291
261
  const normalizedPath = normalizePath(rawPath);
@@ -1017,24 +987,15 @@ const VIDEO_EXTENSIONS = new Set([
1017
987
  ".flv",
1018
988
  ".wmv"
1019
989
  ]);
1020
- /**
1021
- * Extract a lowercase file extension from a path or URL, ignoring query and hash.
1022
- */
1023
- function getCleanExtension(filePath) {
1024
- const cleanPath = filePath.split("?")[0].split("#")[0];
1025
- const lastDot = cleanPath.lastIndexOf(".");
1026
- if (lastDot < 0) return "";
1027
- return cleanPath.slice(lastDot).toLowerCase();
1028
- }
1029
990
  /** Check whether a file is an image using MIME first and extension as fallback. */
1030
991
  function isImageFile$1(filePath, mimeType) {
1031
992
  if (mimeType?.startsWith("image/")) return true;
1032
- return IMAGE_EXTENSIONS.has(getCleanExtension(filePath));
993
+ return IMAGE_EXTENSIONS.has(getFileExtension(filePath) ?? "");
1033
994
  }
1034
995
  /** Check whether a file is a video using MIME first and extension as fallback. */
1035
996
  function isVideoFile$1(filePath, mimeType) {
1036
997
  if (mimeType?.startsWith("video/")) return true;
1037
- return VIDEO_EXTENSIONS.has(getCleanExtension(filePath));
998
+ return VIDEO_EXTENSIONS.has(getFileExtension(filePath) ?? "");
1038
999
  }
1039
1000
  //#endregion
1040
1001
  //#region extensions/qqbot/src/engine/messaging/outbound.ts