@openclaw/qqbot 2026.7.1-beta.6 → 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.
- package/dist/api.js +8 -7
- package/dist/{channel-D1UztsnG.js → channel-BbwpkiY3.js} +99 -54
- package/dist/{channel-entry-C5YdhX3Y.js → channel-entry-Cj1lWXpt.js} +4 -4
- package/dist/channel-entry-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-2ItDYKhz.js → channel.setup-DfhritPL.js} +1 -1
- package/dist/{config-C1qZbh0K.js → config-CpOXnoEc.js} +2 -2
- package/dist/{config-schema-JZEf1dvB.js → config-schema-B5Mle_87.js} +38 -47
- package/dist/doctor-contract-api.js +1 -1
- package/dist/{gateway-pJQppxe4.js → gateway-be5-Ckdc.js} +284 -174
- package/dist/{group-o0GmovSf.js → group-Dbpnjalm.js} +19 -17
- package/dist/{handler-runtime-zQvT6SrI.js → handler-runtime-BvR-ayNW.js} +8 -12
- package/dist/{log-DEtcoDWe.js → log-Da4jz75I.js} +4 -8
- package/dist/{outbound-BIrfvvFJ.js → outbound-BLl8Tsu7.js} +8 -142
- package/dist/{runtime-DodcT_fQ.js → runtime-Du28LbOJ.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-0vqsivoI.js} +220 -66
- package/dist/setup-plugin-api.js +1 -1
- package/dist/state-keys-jLJ2SmJA.js +225 -0
- package/dist/{tools-UJJ-tLHP.js → tools-DYb23LBU.js} +48 -6
- 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 +61 -89
- 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 { 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
|
-
|
|
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,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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1108
|
-
} else await
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
1848
|
-
msgId
|
|
1849
|
-
messageReference: opts
|
|
1850
|
-
forcePlainText: opts
|
|
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
|
|
2004
|
+
msgId
|
|
1859
2005
|
});
|
|
1860
2006
|
return api.sendChannelMessage({
|
|
1861
2007
|
channelId: target.id,
|
|
1862
2008
|
content,
|
|
1863
2009
|
creds: c,
|
|
1864
|
-
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
|
|
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 {
|
|
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 };
|
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-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 };
|