@openclaw/qqbot 2026.7.1 → 2026.7.2-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.
Files changed (41) hide show
  1. package/dist/api.js +8 -7
  2. package/dist/{channel-entry-C5YdhX3Y.js → channel-entry-CA2T2sf8.js} +4 -4
  3. package/dist/channel-entry-api.js +1 -1
  4. package/dist/{channel-D1UztsnG.js → channel-nz3Mkify.js} +99 -54
  5. package/dist/channel-plugin-api.js +1 -1
  6. package/dist/{channel.setup-2ItDYKhz.js → channel.setup-CNoBtKXQ.js} +1 -1
  7. package/dist/{config-C1qZbh0K.js → config-CpOXnoEc.js} +2 -2
  8. package/dist/{config-schema-JZEf1dvB.js → config-schema-D7MaH5X5.js} +61 -76
  9. package/dist/doctor-contract-api.js +4 -10
  10. package/dist/{gateway-pJQppxe4.js → gateway-D8uYPtoy.js} +286 -174
  11. package/dist/{group-o0GmovSf.js → group-BVHG8qUZ.js} +40 -23
  12. package/dist/{handler-runtime-zQvT6SrI.js → handler-runtime-S1_XF8on.js} +8 -12
  13. package/dist/{log-DEtcoDWe.js → log-Da4jz75I.js} +4 -8
  14. package/dist/{outbound-BIrfvvFJ.js → outbound-FOG4zNLY.js} +8 -142
  15. package/dist/{runtime-DodcT_fQ.js → runtime-CyjBiGD2.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-BAUHZqDW.js} +225 -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-CC5CKQig.js} +71 -26
  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 +105 -133
  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 { createChannelApiRetryRunner, resolveRetryConfig } 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,42 @@ 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
- (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);
496
+ const persistentFailures = /* @__PURE__ */ new WeakSet();
497
+ const retryConfig = resolveRetryConfig(void 0, {
498
+ attempts: policy.maxRetries + 1,
499
+ minDelayMs: policy.baseDelayMs,
500
+ maxDelayMs: policy.backoff === "fixed" ? policy.baseDelayMs : 2147e6,
501
+ jitter: 0
502
+ });
503
+ return await createChannelApiRetryRunner({
504
+ retry: retryConfig,
505
+ strictShouldRetry: true,
506
+ retryAfterMs: () => void 0,
507
+ shouldRetry: (err, attempt) => {
508
+ const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
509
+ const shouldRetry = !persistentFailures.has(error) && policy.shouldRetry?.(error, attempt - 1) !== false;
510
+ if (shouldRetry) {
511
+ const delayMs = policy.backoff === "fixed" ? retryConfig.minDelayMs : Math.min(retryConfig.minDelayMs * 2 ** (attempt - 1), retryConfig.maxDelayMs);
512
+ logger?.debug?.(`[qqbot:retry] Attempt ${attempt} failed, retrying in ${delayMs}ms: ${truncateUtf16Safe(error.message, 100)}`);
513
+ }
514
+ return shouldRetry;
498
515
  }
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);
516
+ })(async () => {
517
+ try {
518
+ return await fn();
519
+ } catch (err) {
520
+ const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
521
+ if (!persistentPolicy?.shouldPersistRetry(error)) throw error;
522
+ (logger?.warn ?? logger?.error)?.(`[qqbot:retry] Hit persistent-retry trigger, entering persistent loop (timeout=${persistentPolicy.timeoutMs / 1e3}s)`);
523
+ try {
524
+ return await persistentRetryLoop(fn, persistentPolicy, logger);
525
+ } catch (persistentError) {
526
+ const terminal = persistentError instanceof Error ? persistentError : new Error(formatErrorMessage(persistentError));
527
+ persistentFailures.add(terminal);
528
+ throw terminal;
529
+ }
504
530
  }
505
- }
506
- throw lastError;
531
+ });
507
532
  }
508
533
  /**
509
534
  * Persistent retry loop: fixed-interval retries bounded by a total timeout.
@@ -791,7 +816,10 @@ var ChunkedMediaApi = class {
791
816
  totalBytes: fileSize
792
817
  });
793
818
  };
794
- await runWithConcurrency(parts.map((part) => () => uploadPart(part)), maxConcurrent);
819
+ await pMap(parts, uploadPart, {
820
+ concurrency: maxConcurrent,
821
+ stopOnError: true
822
+ });
795
823
  this.logger?.info?.(`${prefix} all parts uploaded, completing...`);
796
824
  const result = await this.callCompleteUpload(opts, upload_id);
797
825
  this.logger?.info?.(`${prefix} completed: file_uuid=${result.file_uuid} ttl=${result.ttl}s`);
@@ -964,20 +992,6 @@ async function putToPresignedUrl(presignedUrl, data, partIndex, totalParts, logg
964
992
  }
965
993
  throw lastError ?? /* @__PURE__ */ new Error(`Part ${partIndex}/${totalParts} upload failed`);
966
994
  }
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
995
  //#endregion
982
996
  //#region extensions/qqbot/src/engine/api/token.ts
983
997
  /**
@@ -990,6 +1004,7 @@ async function runWithConcurrency(tasks, maxConcurrent) {
990
1004
  const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
991
1005
  const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 7200;
992
1006
  const QQBOT_TOKEN_RESPONSE_LIMIT_BYTES = 8 * 1024;
1007
+ const QQBOT_TOKEN_REQUEST_TIMEOUT_MS = 3e4;
993
1008
  /**
994
1009
  * Host-scoped SSRF policy for the QQ Bot token endpoint.
995
1010
  *
@@ -1094,6 +1109,7 @@ var TokenManager = class {
1094
1109
  const controller = new AbortController();
1095
1110
  this.refreshControllers.set(appId, controller);
1096
1111
  const { signal } = controller;
1112
+ const sleepAndYield = (ms) => sleepWithAbort(Number.isFinite(ms) ? Math.max(ms, 1) : 1, signal);
1097
1113
  const loop = async () => {
1098
1114
  this.logger?.info?.(`[qqbot:token:${appId}] Background refresh started`);
1099
1115
  while (!signal.aborted) try {
@@ -1104,12 +1120,12 @@ var TokenManager = class {
1104
1120
  const randomOffset = Math.random() * randomOffsetMs;
1105
1121
  const refreshIn = Math.max(expiresIn - refreshAheadMs - randomOffset, minRefreshIntervalMs);
1106
1122
  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);
1123
+ await sleepAndYield(refreshIn);
1124
+ } else await sleepAndYield(minRefreshIntervalMs);
1109
1125
  } catch (err) {
1110
1126
  if (signal.aborted) break;
1111
1127
  this.logger?.error?.(`[qqbot:token:${appId}] Background refresh failed: ${formatErrorMessage(err)}`);
1112
- await this.abortableSleep(retryDelayMs, signal);
1128
+ await sleepAndYield(retryDelayMs);
1113
1129
  }
1114
1130
  this.refreshControllers.delete(appId);
1115
1131
  this.logger?.info?.(`[qqbot:token:${appId}] Background refresh stopped`);
@@ -1142,6 +1158,7 @@ var TokenManager = class {
1142
1158
  auditContext: "qqbot-token",
1143
1159
  capture: false,
1144
1160
  policy: QQBOT_TOKEN_SSRF_POLICY,
1161
+ timeoutMs: QQBOT_TOKEN_REQUEST_TIMEOUT_MS,
1145
1162
  init: {
1146
1163
  method: "POST",
1147
1164
  headers: {
@@ -1195,21 +1212,6 @@ var TokenManager = class {
1195
1212
  await release?.();
1196
1213
  }
1197
1214
  }
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
1215
  };
1214
1216
  //#endregion
1215
1217
  //#region extensions/qqbot/src/engine/api/media.ts
@@ -1570,7 +1572,7 @@ function getCachedFileInfo(contentHash, scope, targetId, fileType) {
1570
1572
  cache.delete(key);
1571
1573
  return null;
1572
1574
  }
1573
- debugLog(`[upload-cache] Cache HIT: key=${key.slice(0, 40)}..., fileUuid=${entry.fileUuid}`);
1575
+ debugLog(`[upload-cache] Cache HIT: key=${truncateUtf16Safe(key, 40)}..., fileUuid=${entry.fileUuid}`);
1574
1576
  return entry.fileInfo;
1575
1577
  }
1576
1578
  /** Store an upload result in the cache. */
@@ -1580,7 +1582,7 @@ function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fil
1580
1582
  for (const [k, v] of cache) if (!isFutureDateTimestampMs(v.expiresAt, { nowMs: now })) cache.delete(k);
1581
1583
  if (cache.size >= MAX_CACHE_SIZE) {
1582
1584
  const keys = Array.from(cache.keys());
1583
- for (let i = 0; i < keys.length / 2; i++) cache.delete(keys[i]);
1585
+ for (const key of keys.slice(0, Math.ceil(keys.length / 2))) cache.delete(key);
1584
1586
  }
1585
1587
  }
1586
1588
  const key = buildCacheKey(contentHash, scope, targetId, fileType);
@@ -1595,7 +1597,147 @@ function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fil
1595
1597
  fileUuid,
1596
1598
  expiresAt
1597
1599
  });
1598
- debugLog(`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
1600
+ debugLog(`[upload-cache] Cache SET: key=${truncateUtf16Safe(key, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
1601
+ }
1602
+ //#endregion
1603
+ //#region extensions/qqbot/src/engine/messaging/reply-limiter.ts
1604
+ const DEFAULT_LIMIT = 5;
1605
+ const DEFAULT_TTL_MS = 3600 * 1e3;
1606
+ const DEFAULT_MAX_TRACKED = 1e4;
1607
+ /**
1608
+ * Per-account reply limiter with automatic eviction.
1609
+ *
1610
+ * Usage:
1611
+ * ```ts
1612
+ * const limiter = new ReplyLimiter({ limit: 5, ttlMs: 3600000 });
1613
+ * const claim = limiter.claim(messageId);
1614
+ * if (claim.allowed) {
1615
+ * await sendPassiveReply(...);
1616
+ * } else if (claim.shouldFallbackToProactive) {
1617
+ * await sendProactiveMessage(...);
1618
+ * }
1619
+ * ```
1620
+ */
1621
+ var ReplyLimiter = class {
1622
+ constructor(config) {
1623
+ this.tracker = /* @__PURE__ */ new Map();
1624
+ this.limit = config?.limit ?? DEFAULT_LIMIT;
1625
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
1626
+ this.maxTracked = config?.maxTrackedMessages ?? DEFAULT_MAX_TRACKED;
1627
+ }
1628
+ /** Check whether a passive reply is allowed while leaving `reserve` slots unused. */
1629
+ checkLimit(messageId, reserve = 0) {
1630
+ const now = Date.now();
1631
+ this.evictIfNeeded(now);
1632
+ const record = this.tracker.get(messageId);
1633
+ if (!record) {
1634
+ if (this.limit > reserve) return {
1635
+ allowed: true,
1636
+ remaining: this.limit,
1637
+ shouldFallbackToProactive: false
1638
+ };
1639
+ return {
1640
+ allowed: false,
1641
+ remaining: this.limit,
1642
+ shouldFallbackToProactive: true,
1643
+ fallbackReason: "limit_exceeded",
1644
+ message: `Passive reply budget reserved (${reserve} of ${this.limit} remaining); sending proactively instead`
1645
+ };
1646
+ }
1647
+ if (now - record.firstReplyAt > this.ttlMs) return {
1648
+ allowed: false,
1649
+ remaining: 0,
1650
+ shouldFallbackToProactive: true,
1651
+ fallbackReason: "expired",
1652
+ message: `Message is older than ${this.ttlMs / (3600 * 1e3)}h; sending as a proactive message instead`
1653
+ };
1654
+ const remaining = this.limit - (record?.count ?? 0);
1655
+ if (remaining <= reserve) return {
1656
+ allowed: false,
1657
+ remaining,
1658
+ shouldFallbackToProactive: true,
1659
+ fallbackReason: "limit_exceeded",
1660
+ 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`
1661
+ };
1662
+ return {
1663
+ allowed: true,
1664
+ remaining,
1665
+ shouldFallbackToProactive: false
1666
+ };
1667
+ }
1668
+ /** Atomically reserve one passive-reply slot before starting the request. */
1669
+ claim(messageId, reserve = 0) {
1670
+ const check = this.checkLimit(messageId, reserve);
1671
+ if (!check.allowed) return check;
1672
+ this.record(messageId);
1673
+ return {
1674
+ ...check,
1675
+ remaining: check.remaining - 1
1676
+ };
1677
+ }
1678
+ /** Record one passive reply against a message. */
1679
+ record(messageId) {
1680
+ const now = Date.now();
1681
+ const existing = this.tracker.get(messageId);
1682
+ if (!existing) this.tracker.set(messageId, {
1683
+ count: 1,
1684
+ firstReplyAt: now
1685
+ });
1686
+ else if (now - existing.firstReplyAt > this.ttlMs) this.tracker.set(messageId, {
1687
+ count: 1,
1688
+ firstReplyAt: now
1689
+ });
1690
+ else existing.count++;
1691
+ }
1692
+ /** Return diagnostic stats. */
1693
+ getStats() {
1694
+ let totalReplies = 0;
1695
+ for (const record of this.tracker.values()) totalReplies += record.count;
1696
+ return {
1697
+ trackedMessages: this.tracker.size,
1698
+ totalReplies
1699
+ };
1700
+ }
1701
+ /** Return limiter configuration. */
1702
+ getConfig() {
1703
+ return {
1704
+ limit: this.limit,
1705
+ ttlMs: this.ttlMs,
1706
+ ttlHours: this.ttlMs / (3600 * 1e3)
1707
+ };
1708
+ }
1709
+ /** Clear all tracked records. */
1710
+ clear() {
1711
+ this.tracker.clear();
1712
+ }
1713
+ /** Opportunistically evict expired records to keep the tracker bounded. */
1714
+ evictIfNeeded(now) {
1715
+ if (this.tracker.size <= this.maxTracked) return;
1716
+ for (const [id, rec] of this.tracker) if (now - rec.firstReplyAt > this.ttlMs) this.tracker.delete(id);
1717
+ }
1718
+ };
1719
+ //#endregion
1720
+ //#region extensions/qqbot/src/engine/messaging/outbound-reply.ts
1721
+ const replyLimiter = new ReplyLimiter();
1722
+ const MESSAGE_REPLY_LIMIT = 5;
1723
+ function checkMessageReplyLimit(messageId) {
1724
+ return replyLimiter.checkLimit(messageId);
1725
+ }
1726
+ function recordMessageReply(messageId) {
1727
+ replyLimiter.record(messageId);
1728
+ debugLog(`[qqbot] recordMessageReply: ${messageId}, count=${replyLimiter.getStats().totalReplies}`);
1729
+ }
1730
+ /** Reserve one slot before a passive request so concurrent sends share one budget. */
1731
+ function claimMessageReply(messageId, reserve = 0) {
1732
+ const result = replyLimiter.claim(messageId, reserve);
1733
+ if (result.allowed) debugLog(`[qqbot] claimMessageReply: ${messageId}, remaining=${result.remaining}/5`);
1734
+ return result;
1735
+ }
1736
+ function getMessageReplyStats() {
1737
+ return replyLimiter.getStats();
1738
+ }
1739
+ function getMessageReplyConfig() {
1740
+ return replyLimiter.getConfig();
1599
1741
  }
1600
1742
  //#endregion
1601
1743
  //#region extensions/qqbot/src/engine/messaging/sender.ts
@@ -1837,17 +1979,26 @@ function notifyMediaHook(appId, result, meta) {
1837
1979
  * Handles passive (with msgId) and proactive (without msgId) modes.
1838
1980
  */
1839
1981
  async function sendText(target, content, creds, opts) {
1840
- const api = resolveAccount(creds.appId).messageApi;
1982
+ const ctx = resolveAccount(creds.appId);
1983
+ const api = ctx.messageApi;
1841
1984
  const c = {
1842
1985
  appId: creds.appId,
1843
1986
  clientSecret: creds.clientSecret
1844
1987
  };
1988
+ let msgId = opts?.msgId;
1989
+ if (msgId) {
1990
+ const passive = claimMessageReply(msgId);
1991
+ if (!passive.allowed) {
1992
+ ctx.logger.warn?.(`Passive reply unavailable for ${target.type}; falling back to a send without msg_id: ${passive.message}`);
1993
+ msgId = void 0;
1994
+ }
1995
+ }
1845
1996
  if (target.type === "c2c" || target.type === "group") {
1846
1997
  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
1998
+ if (msgId) return api.sendMessage(scope, target.id, content, c, {
1999
+ msgId,
2000
+ messageReference: opts?.messageReference,
2001
+ forcePlainText: opts?.forcePlainText
1851
2002
  });
1852
2003
  return api.sendProactiveMessage(scope, target.id, content, c, { forcePlainText: opts?.forcePlainText });
1853
2004
  }
@@ -1855,13 +2006,13 @@ async function sendText(target, content, creds, opts) {
1855
2006
  guildId: target.id,
1856
2007
  content,
1857
2008
  creds: c,
1858
- msgId: opts?.msgId
2009
+ msgId
1859
2010
  });
1860
2011
  return api.sendChannelMessage({
1861
2012
  channelId: target.id,
1862
2013
  content,
1863
2014
  creds: c,
1864
- msgId: opts?.msgId
2015
+ msgId
1865
2016
  });
1866
2017
  }
1867
2018
  /**
@@ -1969,8 +2120,16 @@ async function sendMediaInternal(ctx, opts) {
1969
2120
  try {
1970
2121
  const uploadResult = await dispatchUpload(ctx, scope, opts.target.id, KIND_TO_FILE_TYPE[opts.kind], source, c, opts.fileName);
1971
2122
  const msgContent = opts.kind === "image" || opts.kind === "video" ? opts.content : void 0;
2123
+ let msgId = opts.msgId;
2124
+ if (msgId) {
2125
+ const passive = claimMessageReply(msgId);
2126
+ if (!passive.allowed) {
2127
+ ctx.logger.warn?.(`Passive media reply unavailable for ${scope}; falling back to proactive send: ${passive.message}`);
2128
+ msgId = void 0;
2129
+ }
2130
+ }
1972
2131
  const result = await ctx.mediaApi.sendMediaMessage(scope, opts.target.id, uploadResult.file_info, c, {
1973
- msgId: opts.msgId,
2132
+ msgId,
1974
2133
  content: msgContent
1975
2134
  });
1976
2135
  notifyMediaHook(opts.creds.appId, result, buildOutboundMeta(opts, source));
@@ -2082,4 +2241,4 @@ function supportsRichMedia(targetType) {
2082
2241
  return targetType === "c2c" || targetType === "group";
2083
2242
  }
2084
2243
  //#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 };
2244
+ 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-CNoBtKXQ.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 };