@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.
- package/dist/api.js +8 -7
- package/dist/{channel-entry-C5YdhX3Y.js → channel-entry-CA2T2sf8.js} +4 -4
- package/dist/channel-entry-api.js +1 -1
- package/dist/{channel-D1UztsnG.js → channel-nz3Mkify.js} +99 -54
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-2ItDYKhz.js → channel.setup-CNoBtKXQ.js} +1 -1
- package/dist/{config-C1qZbh0K.js → config-CpOXnoEc.js} +2 -2
- package/dist/{config-schema-JZEf1dvB.js → config-schema-D7MaH5X5.js} +61 -76
- package/dist/doctor-contract-api.js +4 -10
- package/dist/{gateway-pJQppxe4.js → gateway-D8uYPtoy.js} +286 -174
- package/dist/{group-o0GmovSf.js → group-BVHG8qUZ.js} +40 -23
- package/dist/{handler-runtime-zQvT6SrI.js → handler-runtime-S1_XF8on.js} +8 -12
- package/dist/{log-DEtcoDWe.js → log-Da4jz75I.js} +4 -8
- package/dist/{outbound-BIrfvvFJ.js → outbound-FOG4zNLY.js} +8 -142
- package/dist/{runtime-DodcT_fQ.js → runtime-CyjBiGD2.js} +2 -2
- package/dist/runtime-api.js +1 -1
- package/dist/secret-contract-api.js +6 -22
- package/dist/{sender-CjDuU-uz.js → sender-BAUHZqDW.js} +225 -66
- package/dist/setup-plugin-api.js +1 -1
- package/dist/state-keys-jLJ2SmJA.js +225 -0
- package/dist/{tools-UJJ-tLHP.js → tools-CC5CKQig.js} +71 -26
- package/dist/tools-api.js +1 -1
- package/node_modules/p-map/index.d.ts +155 -0
- package/node_modules/p-map/index.js +284 -0
- package/node_modules/p-map/license +9 -0
- package/node_modules/p-map/package.json +57 -0
- package/node_modules/p-map/readme.md +190 -0
- package/node_modules/parse-ms/index.d.ts +30 -0
- package/node_modules/parse-ms/index.js +45 -0
- package/node_modules/parse-ms/license +9 -0
- package/node_modules/parse-ms/package.json +47 -0
- package/node_modules/parse-ms/readme.md +46 -0
- package/node_modules/pretty-ms/index.d.ts +157 -0
- package/node_modules/pretty-ms/index.js +149 -0
- package/node_modules/pretty-ms/license +9 -0
- package/node_modules/pretty-ms/package.json +55 -0
- package/node_modules/pretty-ms/readme.md +179 -0
- package/npm-shrinkwrap.json +44 -3
- package/openclaw.plugin.json +105 -133
- package/package.json +8 -4
- 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-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
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
|
|
1108
|
-
} else await
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
1848
|
-
msgId
|
|
1849
|
-
messageReference: opts
|
|
1850
|
-
forcePlainText: opts
|
|
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
|
|
2009
|
+
msgId
|
|
1859
2010
|
});
|
|
1860
2011
|
return api.sendChannelMessage({
|
|
1861
2012
|
channelId: target.id,
|
|
1862
2013
|
content,
|
|
1863
2014
|
creds: c,
|
|
1864
|
-
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
|
|
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 {
|
|
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 };
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as qqbotSetupPlugin } from "./channel.setup-
|
|
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 };
|