@openclaw/qqbot 2026.7.1 → 2026.7.2-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.
Files changed (41) hide show
  1. package/dist/api.js +8 -7
  2. package/dist/{channel-D1UztsnG.js → channel-BbwpkiY3.js} +99 -54
  3. package/dist/{channel-entry-C5YdhX3Y.js → channel-entry-Cj1lWXpt.js} +4 -4
  4. package/dist/channel-entry-api.js +1 -1
  5. package/dist/channel-plugin-api.js +1 -1
  6. package/dist/{channel.setup-2ItDYKhz.js → channel.setup-DfhritPL.js} +1 -1
  7. package/dist/{config-C1qZbh0K.js → config-CpOXnoEc.js} +2 -2
  8. package/dist/{config-schema-JZEf1dvB.js → config-schema-B5Mle_87.js} +38 -47
  9. package/dist/doctor-contract-api.js +1 -1
  10. package/dist/{gateway-pJQppxe4.js → gateway-be5-Ckdc.js} +284 -174
  11. package/dist/{group-o0GmovSf.js → group-Dbpnjalm.js} +19 -17
  12. package/dist/{handler-runtime-zQvT6SrI.js → handler-runtime-BvR-ayNW.js} +8 -12
  13. package/dist/{log-DEtcoDWe.js → log-Da4jz75I.js} +4 -8
  14. package/dist/{outbound-BIrfvvFJ.js → outbound-BLl8Tsu7.js} +8 -142
  15. package/dist/{runtime-DodcT_fQ.js → runtime-Du28LbOJ.js} +2 -2
  16. package/dist/runtime-api.js +1 -1
  17. package/dist/secret-contract-api.js +6 -22
  18. package/dist/{sender-CjDuU-uz.js → sender-0vqsivoI.js} +220 -66
  19. package/dist/setup-plugin-api.js +1 -1
  20. package/dist/state-keys-jLJ2SmJA.js +225 -0
  21. package/dist/{tools-UJJ-tLHP.js → tools-DYb23LBU.js} +48 -6
  22. package/dist/tools-api.js +1 -1
  23. package/node_modules/p-map/index.d.ts +155 -0
  24. package/node_modules/p-map/index.js +284 -0
  25. package/node_modules/p-map/license +9 -0
  26. package/node_modules/p-map/package.json +57 -0
  27. package/node_modules/p-map/readme.md +190 -0
  28. package/node_modules/parse-ms/index.d.ts +30 -0
  29. package/node_modules/parse-ms/index.js +45 -0
  30. package/node_modules/parse-ms/license +9 -0
  31. package/node_modules/parse-ms/package.json +47 -0
  32. package/node_modules/parse-ms/readme.md +46 -0
  33. package/node_modules/pretty-ms/index.d.ts +157 -0
  34. package/node_modules/pretty-ms/index.js +149 -0
  35. package/node_modules/pretty-ms/license +9 -0
  36. package/node_modules/pretty-ms/package.json +55 -0
  37. package/node_modules/pretty-ms/readme.md +179 -0
  38. package/npm-shrinkwrap.json +44 -3
  39. package/openclaw.plugin.json +61 -89
  40. package/package.json +8 -4
  41. package/dist/state-keys-CQKlAFyo.js +0 -141
@@ -1,5 +1,5 @@
1
1
  import { c as getPlatformAdapter, i as normalizeOptionalString, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
2
- import { a as formatErrorMessage, n as debugLog, r as debugWarn, t as debugError } from "./log-DEtcoDWe.js";
2
+ import { a as formatErrorMessage, n as debugLog, r as debugWarn, t as debugError } from "./log-Da4jz75I.js";
3
3
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
4
4
  import * as fs$1 from "node:fs";
5
5
  import os from "node:os";
@@ -7,11 +7,13 @@ import { readProviderTextResponse, readResponseTextLimited } from "openclaw/plug
7
7
  import { fetchWithSsrFGuard, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
8
8
  import * as crypto$1 from "node:crypto";
9
9
  import crypto from "node:crypto";
10
- import { sleep } from "openclaw/plugin-sdk/runtime-env";
10
+ import { sleep, sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
11
+ import pMap from "p-map";
11
12
  import { FsSafeError, openLocalFileSafely, readRegularFile, statRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
12
13
  import * as path$1 from "node:path";
13
14
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
14
15
  import { asDateTimestampMs, formatByteSize, isFutureDateTimestampMs, parseStrictPositiveInteger, resolveExpiresAtMsFromDurationSeconds, resolveTimestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
16
+ import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
15
17
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
16
18
  //#region \0rolldown/runtime.js
17
19
  var __defProp = Object.defineProperty;
@@ -244,6 +246,7 @@ const QQBOT_MEDIA_SSRF_POLICY = {
244
246
  ],
245
247
  allowRfc2544BenchmarkRange: true
246
248
  };
249
+ const QQBOT_REMOTE_MEDIA_RESPONSE_HEADER_TIMEOUT_MS = 12e4;
247
250
  /** Validate that a file is within the allowed upload size. */
248
251
  function checkFileSize(filePath, maxSize = MAX_UPLOAD_SIZE) {
249
252
  try {
@@ -336,7 +339,8 @@ async function downloadFile(url, destDir, originalFilename) {
336
339
  const fetched = await getPlatformAdapter().fetchMedia({
337
340
  url: parsedUrl.toString(),
338
341
  filePathHint: originalFilename,
339
- ssrfPolicy: QQBOT_MEDIA_SSRF_POLICY
342
+ ssrfPolicy: QQBOT_MEDIA_SSRF_POLICY,
343
+ responseHeaderTimeoutMs: QQBOT_REMOTE_MEDIA_RESPONSE_HEADER_TIMEOUT_MS
340
344
  });
341
345
  let filename = normalizeOptionalString(originalFilename) ?? "";
342
346
  if (!filename) filename = (normalizeOptionalString(fetched.fileName) ?? path$1.basename(parsedUrl.pathname)) || "download";
@@ -367,9 +371,11 @@ function tryParseDataUrl(value) {
367
371
  if (!value.startsWith("data:")) return null;
368
372
  const m = value.match(DATA_URL_RE);
369
373
  if (!m) return null;
370
- return {
371
- mime: m[1],
372
- data: m[2]
374
+ const mime = m[1];
375
+ const data = m[2];
376
+ return mime === void 0 || data === void 0 ? null : {
377
+ mime,
378
+ data
373
379
  };
374
380
  }
375
381
  /**
@@ -487,23 +493,37 @@ async function normalizeSource(raw, opts = {}) {
487
493
  * @returns The result of the first successful invocation.
488
494
  */
489
495
  async function withRetry(fn, policy, persistentPolicy, logger) {
490
- let lastError = null;
491
- for (let attempt = 0; attempt <= policy.maxRetries; attempt++) try {
492
- return await fn();
493
- } catch (err) {
494
- lastError = err instanceof Error ? err : new Error(formatErrorMessage(err));
495
- if (persistentPolicy?.shouldPersistRetry(lastError)) {
496
+ const persistentFailures = /* @__PURE__ */ new WeakSet();
497
+ return await retryAsync(async () => {
498
+ try {
499
+ return await fn();
500
+ } catch (err) {
501
+ const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
502
+ if (!persistentPolicy?.shouldPersistRetry(error)) throw error;
496
503
  (logger?.warn ?? logger?.error)?.(`[qqbot:retry] Hit persistent-retry trigger, entering persistent loop (timeout=${persistentPolicy.timeoutMs / 1e3}s)`);
497
- return await persistentRetryLoop(fn, persistentPolicy, logger);
498
- }
499
- if (policy.shouldRetry?.(lastError, attempt) === false) throw lastError;
500
- if (attempt < policy.maxRetries) {
501
- const delay = policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** attempt : policy.baseDelayMs;
502
- logger?.debug?.(`[qqbot:retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms: ${truncateUtf16Safe(lastError.message, 100)}`);
503
- await sleep(delay);
504
+ try {
505
+ return await persistentRetryLoop(fn, persistentPolicy, logger);
506
+ } catch (persistentError) {
507
+ const terminal = persistentError instanceof Error ? persistentError : new Error(formatErrorMessage(persistentError));
508
+ persistentFailures.add(terminal);
509
+ throw terminal;
510
+ }
504
511
  }
505
- }
506
- throw lastError;
512
+ }, {
513
+ attempts: policy.maxRetries + 1,
514
+ minDelayMs: 0,
515
+ maxDelayMs: 2147e6,
516
+ delayMs: ({ attempt }) => policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** (attempt - 1) : policy.baseDelayMs,
517
+ shouldRetry: (err, attempt) => {
518
+ const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
519
+ return !persistentFailures.has(error) && policy.shouldRetry?.(error, attempt - 1) !== false;
520
+ },
521
+ onRetry: ({ attempt, delayMs, err }) => {
522
+ const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
523
+ logger?.debug?.(`[qqbot:retry] Attempt ${attempt} failed, retrying in ${delayMs}ms: ${truncateUtf16Safe(error.message, 100)}`);
524
+ },
525
+ sleep
526
+ });
507
527
  }
508
528
  /**
509
529
  * Persistent retry loop: fixed-interval retries bounded by a total timeout.
@@ -791,7 +811,10 @@ var ChunkedMediaApi = class {
791
811
  totalBytes: fileSize
792
812
  });
793
813
  };
794
- await runWithConcurrency(parts.map((part) => () => uploadPart(part)), maxConcurrent);
814
+ await pMap(parts, uploadPart, {
815
+ concurrency: maxConcurrent,
816
+ stopOnError: true
817
+ });
795
818
  this.logger?.info?.(`${prefix} all parts uploaded, completing...`);
796
819
  const result = await this.callCompleteUpload(opts, upload_id);
797
820
  this.logger?.info?.(`${prefix} completed: file_uuid=${result.file_uuid} ttl=${result.ttl}s`);
@@ -964,20 +987,6 @@ async function putToPresignedUrl(presignedUrl, data, partIndex, totalParts, logg
964
987
  }
965
988
  throw lastError ?? /* @__PURE__ */ new Error(`Part ${partIndex}/${totalParts} upload failed`);
966
989
  }
967
- /**
968
- * Batch-mode concurrency limiter. Deliberately simple: dispatch N tasks at
969
- * a time and wait for the whole batch to settle before the next batch.
970
- *
971
- * A pool / queue implementation would recover some throughput when tasks
972
- * have heavy variance, but part uploads are size-uniform (last part can be
973
- * short) so the extra complexity is not worth it.
974
- */
975
- async function runWithConcurrency(tasks, maxConcurrent) {
976
- for (let i = 0; i < tasks.length; i += maxConcurrent) {
977
- const batch = tasks.slice(i, i + maxConcurrent);
978
- await Promise.all(batch.map((task) => task()));
979
- }
980
- }
981
990
  //#endregion
982
991
  //#region extensions/qqbot/src/engine/api/token.ts
983
992
  /**
@@ -990,6 +999,7 @@ async function runWithConcurrency(tasks, maxConcurrent) {
990
999
  const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
991
1000
  const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 7200;
992
1001
  const QQBOT_TOKEN_RESPONSE_LIMIT_BYTES = 8 * 1024;
1002
+ const QQBOT_TOKEN_REQUEST_TIMEOUT_MS = 3e4;
993
1003
  /**
994
1004
  * Host-scoped SSRF policy for the QQ Bot token endpoint.
995
1005
  *
@@ -1094,6 +1104,7 @@ var TokenManager = class {
1094
1104
  const controller = new AbortController();
1095
1105
  this.refreshControllers.set(appId, controller);
1096
1106
  const { signal } = controller;
1107
+ const sleepAndYield = (ms) => sleepWithAbort(Number.isFinite(ms) ? Math.max(ms, 1) : 1, signal);
1097
1108
  const loop = async () => {
1098
1109
  this.logger?.info?.(`[qqbot:token:${appId}] Background refresh started`);
1099
1110
  while (!signal.aborted) try {
@@ -1104,12 +1115,12 @@ var TokenManager = class {
1104
1115
  const randomOffset = Math.random() * randomOffsetMs;
1105
1116
  const refreshIn = Math.max(expiresIn - refreshAheadMs - randomOffset, minRefreshIntervalMs);
1106
1117
  this.logger?.debug?.(`[qqbot:token:${appId}] Next refresh in ${Math.round(refreshIn / 1e3)}s`);
1107
- await this.abortableSleep(refreshIn, signal);
1108
- } else await this.abortableSleep(minRefreshIntervalMs, signal);
1118
+ await sleepAndYield(refreshIn);
1119
+ } else await sleepAndYield(minRefreshIntervalMs);
1109
1120
  } catch (err) {
1110
1121
  if (signal.aborted) break;
1111
1122
  this.logger?.error?.(`[qqbot:token:${appId}] Background refresh failed: ${formatErrorMessage(err)}`);
1112
- await this.abortableSleep(retryDelayMs, signal);
1123
+ await sleepAndYield(retryDelayMs);
1113
1124
  }
1114
1125
  this.refreshControllers.delete(appId);
1115
1126
  this.logger?.info?.(`[qqbot:token:${appId}] Background refresh stopped`);
@@ -1142,6 +1153,7 @@ var TokenManager = class {
1142
1153
  auditContext: "qqbot-token",
1143
1154
  capture: false,
1144
1155
  policy: QQBOT_TOKEN_SSRF_POLICY,
1156
+ timeoutMs: QQBOT_TOKEN_REQUEST_TIMEOUT_MS,
1145
1157
  init: {
1146
1158
  method: "POST",
1147
1159
  headers: {
@@ -1195,21 +1207,6 @@ var TokenManager = class {
1195
1207
  await release?.();
1196
1208
  }
1197
1209
  }
1198
- abortableSleep(ms, signal) {
1199
- return new Promise((resolve, reject) => {
1200
- const timer = setTimeout(resolve, ms);
1201
- if (signal.aborted) {
1202
- clearTimeout(timer);
1203
- reject(/* @__PURE__ */ new Error("Aborted"));
1204
- return;
1205
- }
1206
- const onAbort = () => {
1207
- clearTimeout(timer);
1208
- reject(/* @__PURE__ */ new Error("Aborted"));
1209
- };
1210
- signal.addEventListener("abort", onAbort, { once: true });
1211
- });
1212
- }
1213
1210
  };
1214
1211
  //#endregion
1215
1212
  //#region extensions/qqbot/src/engine/api/media.ts
@@ -1570,7 +1567,7 @@ function getCachedFileInfo(contentHash, scope, targetId, fileType) {
1570
1567
  cache.delete(key);
1571
1568
  return null;
1572
1569
  }
1573
- debugLog(`[upload-cache] Cache HIT: key=${key.slice(0, 40)}..., fileUuid=${entry.fileUuid}`);
1570
+ debugLog(`[upload-cache] Cache HIT: key=${truncateUtf16Safe(key, 40)}..., fileUuid=${entry.fileUuid}`);
1574
1571
  return entry.fileInfo;
1575
1572
  }
1576
1573
  /** Store an upload result in the cache. */
@@ -1580,7 +1577,7 @@ function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fil
1580
1577
  for (const [k, v] of cache) if (!isFutureDateTimestampMs(v.expiresAt, { nowMs: now })) cache.delete(k);
1581
1578
  if (cache.size >= MAX_CACHE_SIZE) {
1582
1579
  const keys = Array.from(cache.keys());
1583
- for (let i = 0; i < keys.length / 2; i++) cache.delete(keys[i]);
1580
+ for (const key of keys.slice(0, Math.ceil(keys.length / 2))) cache.delete(key);
1584
1581
  }
1585
1582
  }
1586
1583
  const key = buildCacheKey(contentHash, scope, targetId, fileType);
@@ -1595,7 +1592,147 @@ function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fil
1595
1592
  fileUuid,
1596
1593
  expiresAt
1597
1594
  });
1598
- debugLog(`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
1595
+ debugLog(`[upload-cache] Cache SET: key=${truncateUtf16Safe(key, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
1596
+ }
1597
+ //#endregion
1598
+ //#region extensions/qqbot/src/engine/messaging/reply-limiter.ts
1599
+ const DEFAULT_LIMIT = 5;
1600
+ const DEFAULT_TTL_MS = 3600 * 1e3;
1601
+ const DEFAULT_MAX_TRACKED = 1e4;
1602
+ /**
1603
+ * Per-account reply limiter with automatic eviction.
1604
+ *
1605
+ * Usage:
1606
+ * ```ts
1607
+ * const limiter = new ReplyLimiter({ limit: 5, ttlMs: 3600000 });
1608
+ * const claim = limiter.claim(messageId);
1609
+ * if (claim.allowed) {
1610
+ * await sendPassiveReply(...);
1611
+ * } else if (claim.shouldFallbackToProactive) {
1612
+ * await sendProactiveMessage(...);
1613
+ * }
1614
+ * ```
1615
+ */
1616
+ var ReplyLimiter = class {
1617
+ constructor(config) {
1618
+ this.tracker = /* @__PURE__ */ new Map();
1619
+ this.limit = config?.limit ?? DEFAULT_LIMIT;
1620
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
1621
+ this.maxTracked = config?.maxTrackedMessages ?? DEFAULT_MAX_TRACKED;
1622
+ }
1623
+ /** Check whether a passive reply is allowed while leaving `reserve` slots unused. */
1624
+ checkLimit(messageId, reserve = 0) {
1625
+ const now = Date.now();
1626
+ this.evictIfNeeded(now);
1627
+ const record = this.tracker.get(messageId);
1628
+ if (!record) {
1629
+ if (this.limit > reserve) return {
1630
+ allowed: true,
1631
+ remaining: this.limit,
1632
+ shouldFallbackToProactive: false
1633
+ };
1634
+ return {
1635
+ allowed: false,
1636
+ remaining: this.limit,
1637
+ shouldFallbackToProactive: true,
1638
+ fallbackReason: "limit_exceeded",
1639
+ message: `Passive reply budget reserved (${reserve} of ${this.limit} remaining); sending proactively instead`
1640
+ };
1641
+ }
1642
+ if (now - record.firstReplyAt > this.ttlMs) return {
1643
+ allowed: false,
1644
+ remaining: 0,
1645
+ shouldFallbackToProactive: true,
1646
+ fallbackReason: "expired",
1647
+ message: `Message is older than ${this.ttlMs / (3600 * 1e3)}h; sending as a proactive message instead`
1648
+ };
1649
+ const remaining = this.limit - (record?.count ?? 0);
1650
+ if (remaining <= reserve) return {
1651
+ allowed: false,
1652
+ remaining,
1653
+ shouldFallbackToProactive: true,
1654
+ fallbackReason: "limit_exceeded",
1655
+ message: reserve > 0 ? `Passive reply budget reserved (${reserve} of ${this.limit} remaining); sending proactively instead` : `Passive reply limit reached (${this.limit} per hour); sending proactively instead`
1656
+ };
1657
+ return {
1658
+ allowed: true,
1659
+ remaining,
1660
+ shouldFallbackToProactive: false
1661
+ };
1662
+ }
1663
+ /** Atomically reserve one passive-reply slot before starting the request. */
1664
+ claim(messageId, reserve = 0) {
1665
+ const check = this.checkLimit(messageId, reserve);
1666
+ if (!check.allowed) return check;
1667
+ this.record(messageId);
1668
+ return {
1669
+ ...check,
1670
+ remaining: check.remaining - 1
1671
+ };
1672
+ }
1673
+ /** Record one passive reply against a message. */
1674
+ record(messageId) {
1675
+ const now = Date.now();
1676
+ const existing = this.tracker.get(messageId);
1677
+ if (!existing) this.tracker.set(messageId, {
1678
+ count: 1,
1679
+ firstReplyAt: now
1680
+ });
1681
+ else if (now - existing.firstReplyAt > this.ttlMs) this.tracker.set(messageId, {
1682
+ count: 1,
1683
+ firstReplyAt: now
1684
+ });
1685
+ else existing.count++;
1686
+ }
1687
+ /** Return diagnostic stats. */
1688
+ getStats() {
1689
+ let totalReplies = 0;
1690
+ for (const record of this.tracker.values()) totalReplies += record.count;
1691
+ return {
1692
+ trackedMessages: this.tracker.size,
1693
+ totalReplies
1694
+ };
1695
+ }
1696
+ /** Return limiter configuration. */
1697
+ getConfig() {
1698
+ return {
1699
+ limit: this.limit,
1700
+ ttlMs: this.ttlMs,
1701
+ ttlHours: this.ttlMs / (3600 * 1e3)
1702
+ };
1703
+ }
1704
+ /** Clear all tracked records. */
1705
+ clear() {
1706
+ this.tracker.clear();
1707
+ }
1708
+ /** Opportunistically evict expired records to keep the tracker bounded. */
1709
+ evictIfNeeded(now) {
1710
+ if (this.tracker.size <= this.maxTracked) return;
1711
+ for (const [id, rec] of this.tracker) if (now - rec.firstReplyAt > this.ttlMs) this.tracker.delete(id);
1712
+ }
1713
+ };
1714
+ //#endregion
1715
+ //#region extensions/qqbot/src/engine/messaging/outbound-reply.ts
1716
+ const replyLimiter = new ReplyLimiter();
1717
+ const MESSAGE_REPLY_LIMIT = 5;
1718
+ function checkMessageReplyLimit(messageId) {
1719
+ return replyLimiter.checkLimit(messageId);
1720
+ }
1721
+ function recordMessageReply(messageId) {
1722
+ replyLimiter.record(messageId);
1723
+ debugLog(`[qqbot] recordMessageReply: ${messageId}, count=${replyLimiter.getStats().totalReplies}`);
1724
+ }
1725
+ /** Reserve one slot before a passive request so concurrent sends share one budget. */
1726
+ function claimMessageReply(messageId, reserve = 0) {
1727
+ const result = replyLimiter.claim(messageId, reserve);
1728
+ if (result.allowed) debugLog(`[qqbot] claimMessageReply: ${messageId}, remaining=${result.remaining}/5`);
1729
+ return result;
1730
+ }
1731
+ function getMessageReplyStats() {
1732
+ return replyLimiter.getStats();
1733
+ }
1734
+ function getMessageReplyConfig() {
1735
+ return replyLimiter.getConfig();
1599
1736
  }
1600
1737
  //#endregion
1601
1738
  //#region extensions/qqbot/src/engine/messaging/sender.ts
@@ -1837,17 +1974,26 @@ function notifyMediaHook(appId, result, meta) {
1837
1974
  * Handles passive (with msgId) and proactive (without msgId) modes.
1838
1975
  */
1839
1976
  async function sendText(target, content, creds, opts) {
1840
- const api = resolveAccount(creds.appId).messageApi;
1977
+ const ctx = resolveAccount(creds.appId);
1978
+ const api = ctx.messageApi;
1841
1979
  const c = {
1842
1980
  appId: creds.appId,
1843
1981
  clientSecret: creds.clientSecret
1844
1982
  };
1983
+ let msgId = opts?.msgId;
1984
+ if (msgId) {
1985
+ const passive = claimMessageReply(msgId);
1986
+ if (!passive.allowed) {
1987
+ ctx.logger.warn?.(`Passive reply unavailable for ${target.type}; falling back to a send without msg_id: ${passive.message}`);
1988
+ msgId = void 0;
1989
+ }
1990
+ }
1845
1991
  if (target.type === "c2c" || target.type === "group") {
1846
1992
  const scope = target.type;
1847
- if (opts?.msgId) return api.sendMessage(scope, target.id, content, c, {
1848
- msgId: opts.msgId,
1849
- messageReference: opts.messageReference,
1850
- forcePlainText: opts.forcePlainText
1993
+ if (msgId) return api.sendMessage(scope, target.id, content, c, {
1994
+ msgId,
1995
+ messageReference: opts?.messageReference,
1996
+ forcePlainText: opts?.forcePlainText
1851
1997
  });
1852
1998
  return api.sendProactiveMessage(scope, target.id, content, c, { forcePlainText: opts?.forcePlainText });
1853
1999
  }
@@ -1855,13 +2001,13 @@ async function sendText(target, content, creds, opts) {
1855
2001
  guildId: target.id,
1856
2002
  content,
1857
2003
  creds: c,
1858
- msgId: opts?.msgId
2004
+ msgId
1859
2005
  });
1860
2006
  return api.sendChannelMessage({
1861
2007
  channelId: target.id,
1862
2008
  content,
1863
2009
  creds: c,
1864
- msgId: opts?.msgId
2010
+ msgId
1865
2011
  });
1866
2012
  }
1867
2013
  /**
@@ -1969,8 +2115,16 @@ async function sendMediaInternal(ctx, opts) {
1969
2115
  try {
1970
2116
  const uploadResult = await dispatchUpload(ctx, scope, opts.target.id, KIND_TO_FILE_TYPE[opts.kind], source, c, opts.fileName);
1971
2117
  const msgContent = opts.kind === "image" || opts.kind === "video" ? opts.content : void 0;
2118
+ let msgId = opts.msgId;
2119
+ if (msgId) {
2120
+ const passive = claimMessageReply(msgId);
2121
+ if (!passive.allowed) {
2122
+ ctx.logger.warn?.(`Passive media reply unavailable for ${scope}; falling back to proactive send: ${passive.message}`);
2123
+ msgId = void 0;
2124
+ }
2125
+ }
1972
2126
  const result = await ctx.mediaApi.sendMediaMessage(scope, opts.target.id, uploadResult.file_info, c, {
1973
- msgId: opts.msgId,
2127
+ msgId,
1974
2128
  content: msgContent
1975
2129
  });
1976
2130
  notifyMediaHook(opts.creds.appId, result, buildOutboundMeta(opts, source));
@@ -2082,4 +2236,4 @@ function supportsRichMedia(targetType) {
2082
2236
  return targetType === "c2c" || targetType === "group";
2083
2237
  }
2084
2238
  //#endregion
2085
- export { getFileTypeName as A, getNextMsgSeq as C, downloadFile as D, checkFileSize as E, StreamInputMode as F, StreamInputState as I, __exportAll as L, getMaxUploadSize as M, readFileAsync as N, fileExistsAsync as O, StreamContentType as P, UploadDailyLimitExceededError as S, openLocalFile as T, sender_exports as _, createRawInputNotifyFn as a, stopBackgroundTokenRefresh as b, getMessageApi as c, initSender as d, onMessageSent as f, sendText as g, sendMedia as h, clearTokenCache as i, getImageMimeType as j, formatFileSize as k, getPluginUserAgent as l, sendInputNotify as m, acknowledgeInteraction as n, getAccessToken as o, registerAccount as p, buildDeliveryTarget as r, getGatewayUrl as s, accountToCreds as t, initApiConfig as u, setOpenClawVersion as v, UPLOAD_PREPARE_FALLBACK_CODE as w, withTokenRetry as x, startBackgroundTokenRefresh as y };
2239
+ export { UPLOAD_PREPARE_FALLBACK_CODE as A, StreamContentType as B, checkMessageReplyLimit as C, recordMessageReply as D, getMessageReplyStats as E, formatFileSize as F, StreamInputState as H, getFileTypeName as I, getImageMimeType as L, checkFileSize as M, downloadFile as N, UploadDailyLimitExceededError as O, fileExistsAsync as P, getMaxUploadSize as R, MESSAGE_REPLY_LIMIT as S, getMessageReplyConfig as T, __exportAll as U, StreamInputMode as V, sender_exports as _, createRawInputNotifyFn as a, stopBackgroundTokenRefresh as b, getMessageApi as c, initSender as d, onMessageSent as f, sendText as g, sendMedia as h, clearTokenCache as i, openLocalFile as j, getNextMsgSeq as k, getPluginUserAgent as l, sendInputNotify as m, acknowledgeInteraction as n, getAccessToken as o, registerAccount as p, buildDeliveryTarget as r, getGatewayUrl as s, accountToCreds as t, initApiConfig as u, setOpenClawVersion as v, claimMessageReply as w, withTokenRetry as x, startBackgroundTokenRefresh as y, readFileAsync as z };
@@ -1,2 +1,2 @@
1
- import { t as qqbotSetupPlugin } from "./channel.setup-2ItDYKhz.js";
1
+ import { t as qqbotSetupPlugin } from "./channel.setup-DfhritPL.js";
2
2
  export { qqbotSetupPlugin };
@@ -0,0 +1,225 @@
1
+ import crypto from "node:crypto";
2
+ import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor";
3
+ //#region extensions/qqbot/src/doctor-contract.ts
4
+ const RESTRICTED_GROUP_TOOLS = { deny: [
5
+ "exec",
6
+ "read",
7
+ "write"
8
+ ] };
9
+ function hasLegacyStreamingValue(value) {
10
+ const entry = asObjectRecord(value);
11
+ if (!entry) return false;
12
+ return typeof entry.streaming === "boolean" || asObjectRecord(entry.streaming)?.c2cStreamApi !== void 0;
13
+ }
14
+ function hasLegacyAccountStreamingValues(value) {
15
+ const accounts = asObjectRecord(value);
16
+ if (!accounts) return false;
17
+ return Object.values(accounts).some((account) => hasLegacyStreamingValue(account));
18
+ }
19
+ function migrateStreamingValue(params) {
20
+ const streaming = params.entry.streaming;
21
+ const path = `${params.pathPrefix}.streaming`;
22
+ if (typeof streaming === "boolean") {
23
+ const next = streaming ? {
24
+ mode: "partial",
25
+ nativeTransport: true
26
+ } : { mode: "off" };
27
+ params.changes.push(`Moved ${path} (boolean) → ${path}.mode (${next.mode}).`);
28
+ if (streaming) params.changes.push(`Moved ${path} (boolean) → ${path}.nativeTransport.`);
29
+ return {
30
+ entry: {
31
+ ...params.entry,
32
+ streaming: next
33
+ },
34
+ changed: true
35
+ };
36
+ }
37
+ const streamingRecord = asObjectRecord(streaming);
38
+ if (!streamingRecord || streamingRecord.c2cStreamApi === void 0) return {
39
+ entry: params.entry,
40
+ changed: false
41
+ };
42
+ const { c2cStreamApi, ...rest } = streamingRecord;
43
+ const next = { ...rest };
44
+ if (next.nativeTransport === void 0) {
45
+ next.nativeTransport = c2cStreamApi;
46
+ params.changes.push(`Moved ${path}.c2cStreamApi → ${path}.nativeTransport.`);
47
+ } else params.changes.push(`Removed ${path}.c2cStreamApi (${path}.nativeTransport already set).`);
48
+ return {
49
+ entry: {
50
+ ...params.entry,
51
+ streaming: next
52
+ },
53
+ changed: true
54
+ };
55
+ }
56
+ function hasLegacyGroupToolPolicy(value) {
57
+ const groups = asObjectRecord(value);
58
+ if (!groups) return false;
59
+ return Object.values(groups).some((group) => asObjectRecord(group)?.toolPolicy !== void 0);
60
+ }
61
+ function hasLegacyAccountGroupToolPolicy(value) {
62
+ const accounts = asObjectRecord(value);
63
+ if (!accounts) return false;
64
+ return Object.values(accounts).some((account) => hasLegacyGroupToolPolicy(asObjectRecord(account)?.groups));
65
+ }
66
+ function migrateToolPolicy(value) {
67
+ if (value === "none") return { deny: ["*"] };
68
+ if (value === "full") return { allow: [] };
69
+ if (value === "restricted") return { ...RESTRICTED_GROUP_TOOLS };
70
+ }
71
+ function describeToolPolicy(value) {
72
+ return typeof value === "string" ? value : String(value);
73
+ }
74
+ function migrateGroups(params) {
75
+ let changed = false;
76
+ const nextGroups = { ...params.groups };
77
+ for (const [groupId, rawGroup] of Object.entries(params.groups)) {
78
+ const group = asObjectRecord(rawGroup);
79
+ if (!group || group.toolPolicy === void 0) continue;
80
+ const { toolPolicy, ...rest } = group;
81
+ const nextGroup = { ...rest };
82
+ const policy = migrateToolPolicy(toolPolicy);
83
+ const path = `${params.pathPrefix}.${groupId}`;
84
+ if (nextGroup.tools !== void 0) params.changes.push(`Removed ${path}.toolPolicy (${path}.tools already exists).`);
85
+ else if (policy) {
86
+ nextGroup.tools = policy;
87
+ params.changes.push(`Moved ${path}.toolPolicy=${describeToolPolicy(toolPolicy)} to ${path}.tools.`);
88
+ } else params.changes.push(`Removed unsupported ${path}.toolPolicy=${describeToolPolicy(toolPolicy)}.`);
89
+ nextGroups[groupId] = nextGroup;
90
+ changed = true;
91
+ }
92
+ return {
93
+ groups: nextGroups,
94
+ changed
95
+ };
96
+ }
97
+ const legacyConfigRules = [
98
+ {
99
+ path: ["channels", "qqbot"],
100
+ message: "channels.qqbot.streaming (boolean) and channels.qqbot.streaming.c2cStreamApi are legacy; use channels.qqbot.streaming.{mode,nativeTransport}. Run \"openclaw doctor --fix\".",
101
+ match: hasLegacyStreamingValue
102
+ },
103
+ {
104
+ path: [
105
+ "channels",
106
+ "qqbot",
107
+ "accounts"
108
+ ],
109
+ message: "channels.qqbot.accounts.<id>.streaming (boolean) and streaming.c2cStreamApi are legacy; use channels.qqbot.accounts.<id>.streaming.{mode,nativeTransport}. Run \"openclaw doctor --fix\".",
110
+ match: hasLegacyAccountStreamingValues
111
+ },
112
+ {
113
+ path: [
114
+ "channels",
115
+ "qqbot",
116
+ "groups"
117
+ ],
118
+ message: "channels.qqbot.groups.<id>.toolPolicy is legacy and was ignored by QQBot group tool enforcement; use channels.qqbot.groups.<id>.tools instead. Run \"openclaw doctor --fix\".",
119
+ match: hasLegacyGroupToolPolicy
120
+ },
121
+ {
122
+ path: [
123
+ "channels",
124
+ "qqbot",
125
+ "accounts"
126
+ ],
127
+ message: "channels.qqbot.accounts.<id>.groups.<groupId>.toolPolicy is legacy and was ignored by QQBot group tool enforcement; use channels.qqbot.accounts.<id>.groups.<groupId>.tools instead. Run \"openclaw doctor --fix\".",
128
+ match: hasLegacyAccountGroupToolPolicy
129
+ }
130
+ ];
131
+ function normalizeCompatibilityConfig({ cfg }) {
132
+ const rawEntry = asObjectRecord(cfg.channels?.qqbot);
133
+ if (!rawEntry) return {
134
+ config: cfg,
135
+ changes: []
136
+ };
137
+ const changes = [];
138
+ let updated = rawEntry;
139
+ let changed = false;
140
+ const rootStreaming = migrateStreamingValue({
141
+ entry: updated,
142
+ pathPrefix: "channels.qqbot",
143
+ changes
144
+ });
145
+ updated = rootStreaming.entry;
146
+ changed = changed || rootStreaming.changed;
147
+ const groups = asObjectRecord(updated.groups);
148
+ if (groups) {
149
+ const migrated = migrateGroups({
150
+ groups,
151
+ pathPrefix: "channels.qqbot.groups",
152
+ changes
153
+ });
154
+ if (migrated.changed) {
155
+ updated = {
156
+ ...updated,
157
+ groups: migrated.groups
158
+ };
159
+ changed = true;
160
+ }
161
+ }
162
+ const accounts = asObjectRecord(updated.accounts);
163
+ if (accounts) {
164
+ let accountsChanged = false;
165
+ const nextAccounts = { ...accounts };
166
+ for (const [accountId, rawAccount] of Object.entries(accounts)) {
167
+ const account = asObjectRecord(rawAccount);
168
+ if (!account) continue;
169
+ const accountStreaming = migrateStreamingValue({
170
+ entry: account,
171
+ pathPrefix: `channels.qqbot.accounts.${accountId}`,
172
+ changes
173
+ });
174
+ let nextAccount = accountStreaming.entry;
175
+ let accountChanged = accountStreaming.changed;
176
+ const accountGroups = asObjectRecord(nextAccount.groups);
177
+ if (accountGroups) {
178
+ const migrated = migrateGroups({
179
+ groups: accountGroups,
180
+ pathPrefix: `channels.qqbot.accounts.${accountId}.groups`,
181
+ changes
182
+ });
183
+ if (migrated.changed) {
184
+ nextAccount = {
185
+ ...nextAccount,
186
+ groups: migrated.groups
187
+ };
188
+ accountChanged = true;
189
+ }
190
+ }
191
+ if (accountChanged) {
192
+ nextAccounts[accountId] = nextAccount;
193
+ accountsChanged = true;
194
+ }
195
+ }
196
+ if (accountsChanged) {
197
+ updated = {
198
+ ...updated,
199
+ accounts: nextAccounts
200
+ };
201
+ changed = true;
202
+ }
203
+ }
204
+ if (!changed) return {
205
+ config: cfg,
206
+ changes: []
207
+ };
208
+ return {
209
+ config: {
210
+ ...cfg,
211
+ channels: {
212
+ ...cfg.channels,
213
+ qqbot: updated
214
+ }
215
+ },
216
+ changes
217
+ };
218
+ }
219
+ //#endregion
220
+ //#region extensions/qqbot/src/engine/utils/state-keys.ts
221
+ function buildQQBotStateKey(...parts) {
222
+ return crypto.createHash("sha256").update(JSON.stringify(parts)).digest("hex");
223
+ }
224
+ //#endregion
225
+ export { legacyConfigRules as n, normalizeCompatibilityConfig as r, buildQQBotStateKey as t };