@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,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as StreamContentType, F as formatFileSize, H as StreamInputState, L as getImageMimeType, N as downloadFile, R as getMaxUploadSize, V as StreamInputMode, a as createRawInputNotifyFn, b as stopBackgroundTokenRefresh, c as getMessageApi, d as initSender, f as onMessageSent, g as sendText, h as sendMedia, i as clearTokenCache, j as openLocalFile, k as getNextMsgSeq, l as getPluginUserAgent, m as sendInputNotify, n as acknowledgeInteraction, o as getAccessToken, p as registerAccount, r as buildDeliveryTarget, s as getGatewayUrl, t as accountToCreds, u as initApiConfig, w as claimMessageReply, x as withTokenRetry, y as startBackgroundTokenRefresh } from "./sender-0vqsivoI.js";
|
|
2
2
|
import { c as getPlatformAdapter, i as normalizeOptionalString$1, n as normalizeLowercaseStringOrEmpty$1, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-R_0cKO7Q.js";
|
|
3
|
-
import { c as setBridgeLogger, o as ensurePlatformAdapter } from "./config-schema-
|
|
4
|
-
import { a as authorizeQQBotApprovalAction, i as toGatewayAccount, n as createQQBotMarkdownChunker, p as parseApprovalButtonData, r as openQQBotSyncKeyedStore } from "./channel-
|
|
5
|
-
import { d as resolveAccountBase } from "./config-
|
|
6
|
-
import { a as formatErrorMessage, i as formatDuration, n as debugLog, r as debugWarn, t as debugError } from "./log-
|
|
7
|
-
import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-
|
|
8
|
-
import { t as buildQQBotStateKey } from "./state-keys-
|
|
9
|
-
import { a as resolveMentionPatterns, c as getPluginVersion, d as PRIVATE_CHAT_ONLY_TEXT, f as classifyCoreCommandForGroup, i as resolveGroupSettings, l as initCommands, n as resolveGroupCommandLevelFromAccountConfig, r as resolveGroupConfig, s as getFrameworkVersion, u as matchSlashCommand } from "./group-
|
|
10
|
-
import {
|
|
3
|
+
import { c as setBridgeLogger, o as ensurePlatformAdapter } from "./config-schema-B5Mle_87.js";
|
|
4
|
+
import { a as authorizeQQBotApprovalAction, i as toGatewayAccount, n as createQQBotMarkdownChunker, p as parseApprovalButtonData, r as openQQBotSyncKeyedStore } from "./channel-BbwpkiY3.js";
|
|
5
|
+
import { d as resolveAccountBase } from "./config-CpOXnoEc.js";
|
|
6
|
+
import { a as formatErrorMessage, i as formatDuration, n as debugLog, r as debugWarn, t as debugError } from "./log-Da4jz75I.js";
|
|
7
|
+
import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-Du28LbOJ.js";
|
|
8
|
+
import { t as buildQQBotStateKey } from "./state-keys-jLJ2SmJA.js";
|
|
9
|
+
import { a as resolveMentionPatterns, c as getPluginVersion, d as PRIVATE_CHAT_ONLY_TEXT, f as classifyCoreCommandForGroup, i as resolveGroupSettings, l as initCommands, n as resolveGroupCommandLevelFromAccountConfig, r as resolveGroupConfig, s as getFrameworkVersion, u as matchSlashCommand } from "./group-Dbpnjalm.js";
|
|
10
|
+
import { A as getQQBotMediaDir, C as resolveUserFacingMediaError, D as checkSilkWasmAvailable, E as setOutboundAudioPort, F as normalizePath, M as getTempDir, N as isLocalPath, O as getHomeDir, P as isWindows, S as resolveWorkspaceScopedLocalRoots, _ as sendVideoMsg, b as resolveOutboundMediaLocalRoots, c as isCronReminderPayload, d as normalizeMediaTags, g as sendPhoto, h as sendDocument, k as getQQBotDataDir, l as isMediaPayload, o as decodeMediaPath, r as sendMedia$1, s as encodePayloadForCron, u as parseQQBotPayload, v as sendVoice, w as DEFAULT_MEDIA_SEND_ERROR, x as resolveWorkspacePathCandidates, y as resolveTrustedOutboundMediaPath } from "./outbound-BLl8Tsu7.js";
|
|
11
11
|
import { n as runWithRequestContext } from "./request-context-Bm7PTBD1.js";
|
|
12
|
+
import { createTypingKeepaliveLoop } from "openclaw/plugin-sdk/channel-outbound";
|
|
12
13
|
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
14
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
14
15
|
import { isImplicitSameChatApprovalAuthorization } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
@@ -18,11 +19,12 @@ import * as os$1 from "node:os";
|
|
|
18
19
|
import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
19
20
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
20
21
|
import crypto from "node:crypto";
|
|
21
|
-
import { readRegularFileSync, resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
|
|
22
|
+
import { compileSafeRegexDetailed, readRegularFileSync, resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
|
|
22
23
|
import * as path$1 from "node:path";
|
|
23
24
|
import path from "node:path";
|
|
24
|
-
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
|
|
25
|
+
import { mimeTypeFromFilePath, normalizeMimeType } from "openclaw/plugin-sdk/media-mime";
|
|
25
26
|
import { MAX_TIMER_TIMEOUT_MS, finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
|
27
|
+
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
26
28
|
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
27
29
|
import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime";
|
|
28
30
|
import WebSocket from "ws";
|
|
@@ -1079,9 +1081,9 @@ function isSlashCommand(msg) {
|
|
|
1079
1081
|
*/
|
|
1080
1082
|
function mergeGroupMessages(batch) {
|
|
1081
1083
|
if (batch.length === 0) throw new Error("mergeGroupMessages: empty batch");
|
|
1082
|
-
if (batch.length === 1) return batch
|
|
1083
|
-
const first = batch
|
|
1084
|
-
const last = batch
|
|
1084
|
+
if (batch.length === 1) return expectDefined(batch.at(0), "single-message merge batch");
|
|
1085
|
+
const first = expectDefined(batch.at(0), "non-empty merge batch first message");
|
|
1086
|
+
const last = expectDefined(batch.at(-1), "non-empty merge batch last message");
|
|
1085
1087
|
const mergedContent = batch.map((m) => `[${m.senderName ?? m.senderId}]: ${m.content}`).join("\n");
|
|
1086
1088
|
const mergedAttachments = [];
|
|
1087
1089
|
for (const m of batch) if (m.attachments?.length) mergedAttachments.push(...m.attachments);
|
|
@@ -1275,6 +1277,14 @@ function createMessageQueue(ctx) {
|
|
|
1275
1277
|
//#endregion
|
|
1276
1278
|
//#region extensions/qqbot/src/engine/gateway/reconnect.ts
|
|
1277
1279
|
/**
|
|
1280
|
+
* WebSocket reconnection state machine and close-code handler.
|
|
1281
|
+
*
|
|
1282
|
+
* Encapsulates the reconnect delay scheduling, quick-disconnect detection,
|
|
1283
|
+
* and close-code interpretation that both plugin versions share.
|
|
1284
|
+
*
|
|
1285
|
+
* Zero external dependencies — uses only the constants from `./constants.ts`.
|
|
1286
|
+
*/
|
|
1287
|
+
/**
|
|
1278
1288
|
* Reconnection state machine.
|
|
1279
1289
|
*
|
|
1280
1290
|
* Usage:
|
|
@@ -1314,7 +1324,7 @@ var ReconnectState = class {
|
|
|
1314
1324
|
* @returns Delay in milliseconds.
|
|
1315
1325
|
*/
|
|
1316
1326
|
getNextDelay(customDelay) {
|
|
1317
|
-
const delay = customDelay ?? RECONNECT_DELAYS[Math.min(this.attempts, RECONNECT_DELAYS.length - 1)];
|
|
1327
|
+
const delay = customDelay ?? expectDefined(RECONNECT_DELAYS[Math.min(this.attempts, RECONNECT_DELAYS.length - 1)], "non-empty reconnect delay schedule");
|
|
1318
1328
|
this.attempts++;
|
|
1319
1329
|
this.log?.debug?.(`Reconnecting ${this.accountId} in ${delay}ms (attempt ${this.attempts})`);
|
|
1320
1330
|
return delay;
|
|
@@ -1356,18 +1366,18 @@ var ReconnectState = class {
|
|
|
1356
1366
|
};
|
|
1357
1367
|
}
|
|
1358
1368
|
if (code === GatewayCloseCode.INVALID_SESSION || code === GatewayCloseCode.SEQ_OUT_OF_RANGE || code === GatewayCloseCode.SESSION_TIMEOUT) {
|
|
1359
|
-
const
|
|
1369
|
+
const reason = expectDefined({
|
|
1360
1370
|
[GatewayCloseCode.INVALID_SESSION]: "session no longer valid",
|
|
1361
1371
|
[GatewayCloseCode.SEQ_OUT_OF_RANGE]: "invalid seq on resume",
|
|
1362
1372
|
[GatewayCloseCode.SESSION_TIMEOUT]: "session timed out"
|
|
1363
|
-
};
|
|
1364
|
-
this.log?.info(`Error ${code} (${
|
|
1373
|
+
}[code], "recognized session close code");
|
|
1374
|
+
this.log?.info(`Error ${code} (${reason}), will re-identify`);
|
|
1365
1375
|
return {
|
|
1366
1376
|
shouldReconnect: !isAborted,
|
|
1367
1377
|
clearSession: true,
|
|
1368
1378
|
refreshToken: true,
|
|
1369
1379
|
fatal: false,
|
|
1370
|
-
reason
|
|
1380
|
+
reason
|
|
1371
1381
|
};
|
|
1372
1382
|
}
|
|
1373
1383
|
if (code >= GatewayCloseCode.SERVER_ERROR_START && code <= GatewayCloseCode.SERVER_ERROR_END) {
|
|
@@ -1683,6 +1693,14 @@ var GatewayConnection = class {
|
|
|
1683
1693
|
* of broad SDK text barrels.
|
|
1684
1694
|
*/
|
|
1685
1695
|
const STT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
|
1696
|
+
const DEFAULT_STT_TIMEOUT_MS = 6e4;
|
|
1697
|
+
function resolveSTTTimeoutMs(...timeoutSeconds) {
|
|
1698
|
+
for (const value of timeoutSeconds) {
|
|
1699
|
+
const timeoutMs = finiteSecondsToTimerSafeMilliseconds(value);
|
|
1700
|
+
if (timeoutMs !== void 0) return timeoutMs;
|
|
1701
|
+
}
|
|
1702
|
+
return DEFAULT_STT_TIMEOUT_MS;
|
|
1703
|
+
}
|
|
1686
1704
|
/** Resolve the STT configuration from the nested config object. */
|
|
1687
1705
|
function resolveSTTConfig(cfg) {
|
|
1688
1706
|
const channelStt = asOptionalObjectRecord(asOptionalObjectRecord(asOptionalObjectRecord(cfg.channels)?.qqbot)?.stt);
|
|
@@ -1696,10 +1714,12 @@ function resolveSTTConfig(cfg) {
|
|
|
1696
1714
|
if (baseUrl && apiKey) return {
|
|
1697
1715
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
1698
1716
|
apiKey,
|
|
1699
|
-
model
|
|
1717
|
+
model,
|
|
1718
|
+
timeoutMs: resolveSTTTimeoutMs(providerCfg?.timeoutSeconds)
|
|
1700
1719
|
};
|
|
1701
1720
|
}
|
|
1702
|
-
const
|
|
1721
|
+
const audio = asOptionalObjectRecord(asOptionalObjectRecord(asOptionalObjectRecord(cfg.tools)?.media)?.audio);
|
|
1722
|
+
const audioModels = audio?.models;
|
|
1703
1723
|
const audioModelEntry = Array.isArray(audioModels) ? asOptionalObjectRecord(audioModels[0]) : void 0;
|
|
1704
1724
|
if (audioModelEntry) {
|
|
1705
1725
|
const providerId = readStringField(audioModelEntry, "provider") ?? "openai";
|
|
@@ -1710,7 +1730,8 @@ function resolveSTTConfig(cfg) {
|
|
|
1710
1730
|
if (baseUrl && apiKey) return {
|
|
1711
1731
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
1712
1732
|
apiKey,
|
|
1713
|
-
model
|
|
1733
|
+
model,
|
|
1734
|
+
timeoutMs: resolveSTTTimeoutMs(audioModelEntry.timeoutSeconds, audio?.timeoutSeconds, providerCfg?.timeoutSeconds)
|
|
1714
1735
|
};
|
|
1715
1736
|
}
|
|
1716
1737
|
return null;
|
|
@@ -1728,6 +1749,7 @@ async function transcribeAudio(audioPath, cfg) {
|
|
|
1728
1749
|
const { response: resp, release } = await fetchWithSsrFGuard({
|
|
1729
1750
|
url: `${sttCfg.baseUrl}/audio/transcriptions`,
|
|
1730
1751
|
auditContext: "qqbot-stt",
|
|
1752
|
+
timeoutMs: sttCfg.timeoutMs,
|
|
1731
1753
|
init: {
|
|
1732
1754
|
method: "POST",
|
|
1733
1755
|
headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
|
|
@@ -1796,18 +1818,19 @@ async function processAttachments(attachments, ctx) {
|
|
|
1796
1818
|
});
|
|
1797
1819
|
const processTasks = (await Promise.all(downloadTasks)).map(async ({ att, attUrl, isVoice, localPath, audioPath }) => {
|
|
1798
1820
|
const asrReferText = normalizeOptionalString$1(att.asr_refer_text) ?? "";
|
|
1821
|
+
const normalizedContentType = normalizeMimeType(att.content_type) ?? "";
|
|
1799
1822
|
const voiceSourceUrl = (isVoice && att.voice_wav_url ? att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url : "") || attUrl;
|
|
1800
1823
|
const meta = {
|
|
1801
1824
|
voiceUrl: isVoice && voiceSourceUrl ? voiceSourceUrl : void 0,
|
|
1802
1825
|
asrReferText: isVoice && asrReferText ? asrReferText : void 0
|
|
1803
1826
|
};
|
|
1804
1827
|
if (localPath) {
|
|
1805
|
-
if (
|
|
1828
|
+
if (normalizedContentType.startsWith("image/")) {
|
|
1806
1829
|
log?.debug?.(`Downloaded attachment to: ${localPath}`);
|
|
1807
1830
|
return {
|
|
1808
1831
|
localPath,
|
|
1809
1832
|
type: "image",
|
|
1810
|
-
contentType:
|
|
1833
|
+
contentType: normalizedContentType,
|
|
1811
1834
|
meta
|
|
1812
1835
|
};
|
|
1813
1836
|
}
|
|
@@ -1824,11 +1847,11 @@ async function processAttachments(attachments, ctx) {
|
|
|
1824
1847
|
};
|
|
1825
1848
|
}
|
|
1826
1849
|
log?.error(`Failed to download: ${attUrl}`);
|
|
1827
|
-
if (
|
|
1850
|
+
if (normalizedContentType.startsWith("image/")) return {
|
|
1828
1851
|
localPath: null,
|
|
1829
1852
|
type: "image-fallback",
|
|
1830
1853
|
attUrl,
|
|
1831
|
-
contentType:
|
|
1854
|
+
contentType: normalizedContentType,
|
|
1832
1855
|
meta
|
|
1833
1856
|
};
|
|
1834
1857
|
if (isVoice && asrReferText) {
|
|
@@ -2106,15 +2129,52 @@ function buildQualifiedTarget(event, isGroupChat) {
|
|
|
2106
2129
|
//#endregion
|
|
2107
2130
|
//#region extensions/qqbot/src/engine/group/mention.ts
|
|
2108
2131
|
const MENTION_TAG_RE = /<@!?\w+>/;
|
|
2132
|
+
const MENTION_PATTERN_FLAGS = "i";
|
|
2133
|
+
const MAX_MENTION_PATTERN_CACHE_KEYS = 256;
|
|
2134
|
+
const MAX_MENTION_PATTERN_WARNING_KEYS = 256;
|
|
2135
|
+
const mentionPatternCompileCache = /* @__PURE__ */ new Map();
|
|
2136
|
+
const rejectedMentionPatternWarningCache = /* @__PURE__ */ new Set();
|
|
2137
|
+
function warnRejectedMentionPattern(pattern, reason) {
|
|
2138
|
+
const key = `${MENTION_PATTERN_FLAGS}::${reason}::${pattern}`;
|
|
2139
|
+
if (rejectedMentionPatternWarningCache.has(key)) return;
|
|
2140
|
+
rejectedMentionPatternWarningCache.add(key);
|
|
2141
|
+
if (rejectedMentionPatternWarningCache.size > MAX_MENTION_PATTERN_WARNING_KEYS) {
|
|
2142
|
+
rejectedMentionPatternWarningCache.clear();
|
|
2143
|
+
rejectedMentionPatternWarningCache.add(key);
|
|
2144
|
+
}
|
|
2145
|
+
debugWarn(`qqbot: mentionPattern rejected (${reason}): ${pattern}`);
|
|
2146
|
+
}
|
|
2147
|
+
function cacheMentionPatterns(cacheKey, regexes) {
|
|
2148
|
+
mentionPatternCompileCache.set(cacheKey, regexes);
|
|
2149
|
+
if (mentionPatternCompileCache.size > MAX_MENTION_PATTERN_CACHE_KEYS) {
|
|
2150
|
+
mentionPatternCompileCache.clear();
|
|
2151
|
+
mentionPatternCompileCache.set(cacheKey, regexes);
|
|
2152
|
+
}
|
|
2153
|
+
return regexes;
|
|
2154
|
+
}
|
|
2155
|
+
function compileMentionPatterns(patterns) {
|
|
2156
|
+
if (patterns.length === 0) return [];
|
|
2157
|
+
const cacheKey = patterns.join("");
|
|
2158
|
+
const cached = mentionPatternCompileCache.get(cacheKey);
|
|
2159
|
+
if (cached) return cached;
|
|
2160
|
+
const regexes = [];
|
|
2161
|
+
for (const pattern of patterns) {
|
|
2162
|
+
const result = compileSafeRegexDetailed(pattern, MENTION_PATTERN_FLAGS);
|
|
2163
|
+
if (result.reason === "empty") continue;
|
|
2164
|
+
if (result.regex) {
|
|
2165
|
+
regexes.push(result.regex);
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
warnRejectedMentionPattern(result.source, result.reason);
|
|
2169
|
+
}
|
|
2170
|
+
return cacheMentionPatterns(cacheKey, regexes);
|
|
2171
|
+
}
|
|
2109
2172
|
function detectWasMentioned(input) {
|
|
2110
2173
|
const { eventType, mentions, content, mentionPatterns } = input;
|
|
2111
2174
|
if (mentions?.some((m) => m.is_you)) return true;
|
|
2112
2175
|
if (eventType === "GROUP_AT_MESSAGE_CREATE") return true;
|
|
2113
|
-
if (mentionPatterns?.length && content)
|
|
2114
|
-
if (
|
|
2115
|
-
try {
|
|
2116
|
-
if (new RegExp(pattern, "i").test(content)) return true;
|
|
2117
|
-
} catch {}
|
|
2176
|
+
if (mentionPatterns?.length && content) {
|
|
2177
|
+
for (const regex of compileMentionPatterns(mentionPatterns)) if (regex.test(content)) return true;
|
|
2118
2178
|
}
|
|
2119
2179
|
return false;
|
|
2120
2180
|
}
|
|
@@ -2253,7 +2313,7 @@ function buildUserMessage(input) {
|
|
|
2253
2313
|
const { event, userContent, quotePart, isGroupChat, groupInfo } = input;
|
|
2254
2314
|
if (groupInfo?.isMerged && groupInfo.mergedMessages?.length) {
|
|
2255
2315
|
const preceding = groupInfo.mergedMessages.slice(0, -1);
|
|
2256
|
-
const lastMsg = groupInfo.mergedMessages
|
|
2316
|
+
const lastMsg = expectDefined(groupInfo.mergedMessages.at(-1), "non-empty merged group turn");
|
|
2257
2317
|
const atYouTag = groupInfo.gate.effectiveWasMentioned ? " (@you)" : "";
|
|
2258
2318
|
return buildMergedMessageContext({
|
|
2259
2319
|
precedingParts: preceding.map((m) => `[${formatSenderLabel(m)}] ${formatSub(m)}`),
|
|
@@ -2424,6 +2484,7 @@ function classifyMedia(processed) {
|
|
|
2424
2484
|
for (let i = 0; i < processed.imageUrls.length; i++) {
|
|
2425
2485
|
const u = processed.imageUrls[i];
|
|
2426
2486
|
const t = processed.imageMediaTypes[i] ?? "image/png";
|
|
2487
|
+
if (u === void 0) continue;
|
|
2427
2488
|
if (u.startsWith("http://") || u.startsWith("https://")) {
|
|
2428
2489
|
remoteMediaUrls.push(u);
|
|
2429
2490
|
remoteMediaTypes.push(t);
|
|
@@ -2449,6 +2510,50 @@ function classifyMedia(processed) {
|
|
|
2449
2510
|
};
|
|
2450
2511
|
}
|
|
2451
2512
|
//#endregion
|
|
2513
|
+
//#region extensions/qqbot/src/engine/group/message-gating.ts
|
|
2514
|
+
/**
|
|
2515
|
+
* Group gate Layer 1 (ignoreOtherMentions) is QQ-specific and decided here;
|
|
2516
|
+
* Layer 2+3 (command gating + mention gating + command bypass) delegate to the
|
|
2517
|
+
* mention gate port backed by the SDK's `resolveInboundMentionDecision`.
|
|
2518
|
+
*/
|
|
2519
|
+
function resolveGroupMessageGate(params) {
|
|
2520
|
+
if (params.ignoreOtherMentions && params.hasAnyMention && !params.wasMentioned && !params.implicitMention) return {
|
|
2521
|
+
action: "drop_other_mention",
|
|
2522
|
+
effectiveWasMentioned: false,
|
|
2523
|
+
shouldBypassMention: false
|
|
2524
|
+
};
|
|
2525
|
+
const decision = params.mentionGatePort.resolveInboundMentionDecision({
|
|
2526
|
+
facts: {
|
|
2527
|
+
canDetectMention: true,
|
|
2528
|
+
wasMentioned: params.wasMentioned,
|
|
2529
|
+
hasAnyMention: params.hasAnyMention,
|
|
2530
|
+
implicitMentionKinds: params.implicitMention ? ["reply_to_bot"] : []
|
|
2531
|
+
},
|
|
2532
|
+
policy: {
|
|
2533
|
+
isGroup: true,
|
|
2534
|
+
requireMention: params.requireMention,
|
|
2535
|
+
allowTextCommands: params.allowTextCommands,
|
|
2536
|
+
hasControlCommand: params.isControlCommand,
|
|
2537
|
+
commandAuthorized: params.commandAuthorized
|
|
2538
|
+
}
|
|
2539
|
+
});
|
|
2540
|
+
if (params.allowTextCommands && params.isControlCommand && !params.commandAuthorized) return {
|
|
2541
|
+
action: "block_unauthorized_command",
|
|
2542
|
+
effectiveWasMentioned: false,
|
|
2543
|
+
shouldBypassMention: false
|
|
2544
|
+
};
|
|
2545
|
+
if (decision.shouldSkip) return {
|
|
2546
|
+
action: "skip_no_mention",
|
|
2547
|
+
effectiveWasMentioned: decision.effectiveWasMentioned,
|
|
2548
|
+
shouldBypassMention: decision.shouldBypassMention
|
|
2549
|
+
};
|
|
2550
|
+
return {
|
|
2551
|
+
action: "pass",
|
|
2552
|
+
effectiveWasMentioned: decision.effectiveWasMentioned,
|
|
2553
|
+
shouldBypassMention: decision.shouldBypassMention
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
//#endregion
|
|
2452
2557
|
//#region extensions/qqbot/src/engine/gateway/stages/group-gate-stage.ts
|
|
2453
2558
|
function runGroupGateStage(input) {
|
|
2454
2559
|
const { event, deps, accountId, agentId, sessionKey, userContent, processedAttachments } = input;
|
|
@@ -2487,7 +2592,7 @@ function runGroupGateStage(input) {
|
|
|
2487
2592
|
const content = (event.content ?? "").trim();
|
|
2488
2593
|
const isControlCommand = Boolean(deps.isControlCommand?.(content));
|
|
2489
2594
|
const commandAuthorized = deps.allowTextCommands !== false && input.access.commandAccess.authorized;
|
|
2490
|
-
const gate =
|
|
2595
|
+
const gate = resolveGroupMessageGate({
|
|
2491
2596
|
mentionGatePort: deps.adapters.mentionGate,
|
|
2492
2597
|
ignoreOtherMentions,
|
|
2493
2598
|
hasAnyMention: anyMention,
|
|
@@ -2543,43 +2648,6 @@ function runGroupGateStage(input) {
|
|
|
2543
2648
|
skipReason: gate.action
|
|
2544
2649
|
};
|
|
2545
2650
|
}
|
|
2546
|
-
function resolveGateWithPort(params) {
|
|
2547
|
-
if (params.ignoreOtherMentions && params.hasAnyMention && !params.wasMentioned && !params.implicitMention) return {
|
|
2548
|
-
action: "drop_other_mention",
|
|
2549
|
-
effectiveWasMentioned: false,
|
|
2550
|
-
shouldBypassMention: false
|
|
2551
|
-
};
|
|
2552
|
-
const decision = params.mentionGatePort.resolveInboundMentionDecision({
|
|
2553
|
-
facts: {
|
|
2554
|
-
canDetectMention: true,
|
|
2555
|
-
wasMentioned: params.wasMentioned,
|
|
2556
|
-
hasAnyMention: params.hasAnyMention,
|
|
2557
|
-
implicitMentionKinds: params.implicitMention ? ["reply_to_bot"] : []
|
|
2558
|
-
},
|
|
2559
|
-
policy: {
|
|
2560
|
-
isGroup: true,
|
|
2561
|
-
requireMention: params.requireMention,
|
|
2562
|
-
allowTextCommands: params.allowTextCommands,
|
|
2563
|
-
hasControlCommand: params.isControlCommand,
|
|
2564
|
-
commandAuthorized: params.commandAuthorized
|
|
2565
|
-
}
|
|
2566
|
-
});
|
|
2567
|
-
if (params.allowTextCommands && params.isControlCommand && !params.commandAuthorized) return {
|
|
2568
|
-
action: "block_unauthorized_command",
|
|
2569
|
-
effectiveWasMentioned: false,
|
|
2570
|
-
shouldBypassMention: false
|
|
2571
|
-
};
|
|
2572
|
-
if (decision.shouldSkip) return {
|
|
2573
|
-
action: "skip_no_mention",
|
|
2574
|
-
effectiveWasMentioned: decision.effectiveWasMentioned,
|
|
2575
|
-
shouldBypassMention: decision.shouldBypassMention
|
|
2576
|
-
};
|
|
2577
|
-
return {
|
|
2578
|
-
action: "pass",
|
|
2579
|
-
effectiveWasMentioned: decision.effectiveWasMentioned,
|
|
2580
|
-
shouldBypassMention: decision.shouldBypassMention
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
2651
|
function recordGroupHistory(params) {
|
|
2584
2652
|
const { historyMap, groupOpenid, historyLimit, event, userContent, historyPort, localPaths } = params;
|
|
2585
2653
|
if (!historyMap || historyLimit <= 0) return;
|
|
@@ -3032,7 +3100,7 @@ async function handleApprovalButtonInteraction(params) {
|
|
|
3032
3100
|
cfg,
|
|
3033
3101
|
account: params.account,
|
|
3034
3102
|
event: params.event,
|
|
3035
|
-
approvalKind:
|
|
3103
|
+
approvalKind: params.parsed.approvalKind,
|
|
3036
3104
|
resolveCommandAuthorized: params.resolveCommandAuthorized
|
|
3037
3105
|
});
|
|
3038
3106
|
if (!authorization.authorized) {
|
|
@@ -3040,19 +3108,65 @@ async function handleApprovalButtonInteraction(params) {
|
|
|
3040
3108
|
params.log?.info(`Approval button rejected: id=${params.parsed.approvalId}`);
|
|
3041
3109
|
return;
|
|
3042
3110
|
}
|
|
3043
|
-
await acknowledgeApprovalInteraction(params.creds, params.event, params.log);
|
|
3111
|
+
await acknowledgeApprovalInteraction(params.creds, params.event, params.log, { content: "Approval response received." });
|
|
3044
3112
|
const adapter = getPlatformAdapter();
|
|
3045
3113
|
if (!adapter.resolveApproval) {
|
|
3114
|
+
await reportApprovalInteractionOutcome({
|
|
3115
|
+
creds: params.creds,
|
|
3116
|
+
event: params.event,
|
|
3117
|
+
log: params.log,
|
|
3118
|
+
content: "Approval is unavailable."
|
|
3119
|
+
});
|
|
3046
3120
|
params.log?.error("resolveApproval not available on PlatformAdapter");
|
|
3047
3121
|
return;
|
|
3048
3122
|
}
|
|
3049
3123
|
try {
|
|
3050
|
-
|
|
3051
|
-
|
|
3124
|
+
const result = await adapter.resolveApproval(params.parsed);
|
|
3125
|
+
const canonicalDecision = "decision" in result.approval ? `, decision=${result.approval.decision}` : "";
|
|
3126
|
+
const canonicalOutcome = formatCanonicalApprovalOutcome(result.approval);
|
|
3127
|
+
await reportApprovalInteractionOutcome({
|
|
3128
|
+
creds: params.creds,
|
|
3129
|
+
event: params.event,
|
|
3130
|
+
log: params.log,
|
|
3131
|
+
content: result.applied ? `Approval resolved: ${canonicalOutcome}.` : `This approval was already resolved: ${canonicalOutcome}.`
|
|
3132
|
+
});
|
|
3133
|
+
params.log?.info(result.applied ? `Approval resolved: id=${result.approval.id}, status=${result.approval.status}${canonicalDecision}` : `Approval already resolved: id=${result.approval.id}, status=${result.approval.status}${canonicalDecision}`);
|
|
3052
3134
|
} catch (err) {
|
|
3135
|
+
await reportApprovalInteractionOutcome({
|
|
3136
|
+
creds: params.creds,
|
|
3137
|
+
event: params.event,
|
|
3138
|
+
log: params.log,
|
|
3139
|
+
content: "Approval could not be resolved."
|
|
3140
|
+
});
|
|
3053
3141
|
params.log?.error(`Approval resolve failed: id=${params.parsed.approvalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3054
3142
|
}
|
|
3055
3143
|
}
|
|
3144
|
+
async function reportApprovalInteractionOutcome(params) {
|
|
3145
|
+
const target = params.event.group_openid ? {
|
|
3146
|
+
type: "group",
|
|
3147
|
+
id: params.event.group_openid
|
|
3148
|
+
} : params.event.user_openid ? {
|
|
3149
|
+
type: "c2c",
|
|
3150
|
+
id: params.event.user_openid
|
|
3151
|
+
} : params.event.channel_id ? {
|
|
3152
|
+
type: "channel",
|
|
3153
|
+
id: params.event.channel_id
|
|
3154
|
+
} : null;
|
|
3155
|
+
if (!target) {
|
|
3156
|
+
params.log?.info(`Approval interaction outcome: ${params.content}`);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
try {
|
|
3160
|
+
await sendText(target, params.content, params.creds, { msgId: params.event.data.resolved.message_id });
|
|
3161
|
+
} catch (err) {
|
|
3162
|
+
params.log?.error(`Approval outcome delivery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
function formatCanonicalApprovalOutcome(approval) {
|
|
3166
|
+
if (approval.status === "allowed") return approval.decision === "allow-always" ? "Allowed always" : "Allowed once";
|
|
3167
|
+
if (approval.status === "denied") return "Denied";
|
|
3168
|
+
return approval.status === "expired" ? "Expired" : "Cancelled";
|
|
3169
|
+
}
|
|
3056
3170
|
async function acknowledgeApprovalInteraction(creds, event, log, data) {
|
|
3057
3171
|
try {
|
|
3058
3172
|
await acknowledgeInteraction(creds, event.id, 0, data);
|
|
@@ -3135,9 +3249,6 @@ function resolveApprovalActorSenderIds(event) {
|
|
|
3135
3249
|
return normalized ? [normalized] : [];
|
|
3136
3250
|
}));
|
|
3137
3251
|
}
|
|
3138
|
-
function resolveApprovalKind(approvalId) {
|
|
3139
|
-
return approvalId.toLowerCase().startsWith("plugin:") ? "plugin" : "exec";
|
|
3140
|
-
}
|
|
3141
3252
|
/** Execute an async handler, ACK with the result, and handle errors. */
|
|
3142
3253
|
async function handleWithAck(creds, event, log, label, handler) {
|
|
3143
3254
|
try {
|
|
@@ -3217,7 +3328,11 @@ function parseWebpSize(buffer) {
|
|
|
3217
3328
|
if (riff !== "RIFF" || webp !== "WEBP") return null;
|
|
3218
3329
|
const chunkType = buffer.toString("ascii", 12, 16);
|
|
3219
3330
|
if (chunkType === "VP8 ") {
|
|
3220
|
-
if (buffer.length >= 30 && buffer
|
|
3331
|
+
if (buffer.length >= 30 && buffer.subarray(23, 26).equals(Buffer$1.from([
|
|
3332
|
+
157,
|
|
3333
|
+
1,
|
|
3334
|
+
42
|
|
3335
|
+
]))) return {
|
|
3221
3336
|
width: buffer.readUInt16LE(26) & 16383,
|
|
3222
3337
|
height: buffer.readUInt16LE(28) & 16383
|
|
3223
3338
|
};
|
|
@@ -3233,8 +3348,8 @@ function parseWebpSize(buffer) {
|
|
|
3233
3348
|
}
|
|
3234
3349
|
if (chunkType === "VP8X") {
|
|
3235
3350
|
if (buffer.length >= 30) return {
|
|
3236
|
-
width: (
|
|
3237
|
-
height: (
|
|
3351
|
+
width: buffer.readUIntLE(24, 3) + 1,
|
|
3352
|
+
height: buffer.readUIntLE(27, 3) + 1
|
|
3238
3353
|
};
|
|
3239
3354
|
}
|
|
3240
3355
|
return null;
|
|
@@ -3274,13 +3389,13 @@ async function getImageSizeFromUrl(url, timeoutMs = 5e3) {
|
|
|
3274
3389
|
}
|
|
3275
3390
|
});
|
|
3276
3391
|
const size = parseImageSize(buffer);
|
|
3277
|
-
if (size) debugLog(`[image-size] Got size from URL: ${size.width}x${size.height} - ${url
|
|
3392
|
+
if (size) debugLog(`[image-size] Got size from URL: ${size.width}x${size.height} - ${truncateUtf16Safe(url, 60)}...`);
|
|
3278
3393
|
return size;
|
|
3279
3394
|
} finally {
|
|
3280
3395
|
clearTimeout(timeoutId);
|
|
3281
3396
|
}
|
|
3282
3397
|
} catch (err) {
|
|
3283
|
-
debugLog(`[image-size] Error fetching ${url
|
|
3398
|
+
debugLog(`[image-size] Error fetching ${truncateUtf16Safe(url, 60)}...: ${formatErrorMessage(err)}`);
|
|
3284
3399
|
return null;
|
|
3285
3400
|
}
|
|
3286
3401
|
}
|
|
@@ -3290,6 +3405,7 @@ function getImageSizeFromDataUrl(dataUrl) {
|
|
|
3290
3405
|
const matches = dataUrl.match(/^data:image\/[^;]+;base64,(.+)$/);
|
|
3291
3406
|
if (!matches) return null;
|
|
3292
3407
|
const base64Data = matches[1];
|
|
3408
|
+
if (base64Data === void 0) return null;
|
|
3293
3409
|
const size = parseImageSize(Buffer$1.from(base64Data, "base64"));
|
|
3294
3410
|
if (size) debugLog(`[image-size] Got size from Base64: ${size.width}x${size.height}`);
|
|
3295
3411
|
return size;
|
|
@@ -3316,6 +3432,25 @@ function hasQQBotImageSize(markdownImage) {
|
|
|
3316
3432
|
return /!\[#\d+px\s+#\d+px\]/.test(markdownImage);
|
|
3317
3433
|
}
|
|
3318
3434
|
//#endregion
|
|
3435
|
+
//#region extensions/qqbot/src/engine/messaging/race-with-timeout.ts
|
|
3436
|
+
async function raceWithTimeout(operation, timeoutMs, onTimeout) {
|
|
3437
|
+
let timedOut = false;
|
|
3438
|
+
let timeout;
|
|
3439
|
+
const state = { get timedOut() {
|
|
3440
|
+
return timedOut;
|
|
3441
|
+
} };
|
|
3442
|
+
try {
|
|
3443
|
+
return await Promise.race([operation(state), new Promise((resolve) => {
|
|
3444
|
+
timeout = setTimeout(() => {
|
|
3445
|
+
timedOut = true;
|
|
3446
|
+
resolve(onTimeout());
|
|
3447
|
+
}, timeoutMs);
|
|
3448
|
+
})]);
|
|
3449
|
+
} finally {
|
|
3450
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
//#endregion
|
|
3319
3454
|
//#region extensions/qqbot/src/engine/messaging/outbound-deliver.ts
|
|
3320
3455
|
/**
|
|
3321
3456
|
* Outbound delivery helpers — core/ version.
|
|
@@ -3461,10 +3596,9 @@ async function sendVoiceWithTimeout(target, voicePath, account, mediaSender, log
|
|
|
3461
3596
|
const uploadFormats = account.config?.audioFormatPolicy?.uploadDirectFormats ?? account.config?.voiceDirectUploadFormats;
|
|
3462
3597
|
const transcodeEnabled = account.config?.audioFormatPolicy?.transcodeEnabled !== false;
|
|
3463
3598
|
const voiceTimeout = 45e3;
|
|
3464
|
-
const ac = new AbortController();
|
|
3465
3599
|
try {
|
|
3466
|
-
const result = await
|
|
3467
|
-
if (
|
|
3600
|
+
const result = await raceWithTimeout((timeoutState) => mediaSender.sendVoice(target, voicePath, uploadFormats, transcodeEnabled).then((r) => {
|
|
3601
|
+
if (timeoutState.timedOut) {
|
|
3468
3602
|
log?.debug?.(`sendVoice completed after timeout, suppressing late delivery`);
|
|
3469
3603
|
return {
|
|
3470
3604
|
channel: "qqbot",
|
|
@@ -3472,15 +3606,10 @@ async function sendVoiceWithTimeout(target, voicePath, account, mediaSender, log
|
|
|
3472
3606
|
};
|
|
3473
3607
|
}
|
|
3474
3608
|
return r;
|
|
3475
|
-
}),
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
channel: "qqbot",
|
|
3480
|
-
error: "Voice send timed out and was skipped"
|
|
3481
|
-
});
|
|
3482
|
-
}, voiceTimeout);
|
|
3483
|
-
})]);
|
|
3609
|
+
}), voiceTimeout, () => ({
|
|
3610
|
+
channel: "qqbot",
|
|
3611
|
+
error: "Voice send timed out and was skipped"
|
|
3612
|
+
}));
|
|
3484
3613
|
if (result.error) {
|
|
3485
3614
|
log?.error(`sendVoice error: ${result.error}`);
|
|
3486
3615
|
return false;
|
|
@@ -3738,7 +3867,7 @@ async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUr
|
|
|
3738
3867
|
log?.error(`Failed to send Base64 image via Rich Media API: ${String(imgErr)}`);
|
|
3739
3868
|
}
|
|
3740
3869
|
}
|
|
3741
|
-
const existingMdUrls = new Set(mdMatches.
|
|
3870
|
+
const existingMdUrls = new Set(mdMatches.flatMap((m) => m[2] === void 0 ? [] : [m[2]]));
|
|
3742
3871
|
const imagesToAppend = [];
|
|
3743
3872
|
for (const url of httpImageUrls) if (!existingMdUrls.has(url)) try {
|
|
3744
3873
|
const size = await getImageSize(url);
|
|
@@ -3752,6 +3881,7 @@ async function sendMarkdownReply(textWithoutImages, imageUrls, mdMatches, bareUr
|
|
|
3752
3881
|
for (const m of mdMatches) {
|
|
3753
3882
|
const fullMatch = m[0];
|
|
3754
3883
|
const imgUrl = m[2];
|
|
3884
|
+
if (fullMatch === void 0 || imgUrl === void 0) continue;
|
|
3755
3885
|
if (isHttpUrl(imgUrl) && !hasQQBotImageSize(fullMatch)) try {
|
|
3756
3886
|
const size = await getImageSize(imgUrl);
|
|
3757
3887
|
result = result.replace(fullMatch, formatQQBotMarkdownImage(imgUrl, size));
|
|
@@ -3888,7 +4018,7 @@ async function handleStructuredPayload(ctx, replyText, recordActivity, deps) {
|
|
|
3888
4018
|
return true;
|
|
3889
4019
|
}
|
|
3890
4020
|
function formatMediaTypeLabel(mediaType) {
|
|
3891
|
-
return mediaType
|
|
4021
|
+
return mediaType.charAt(0).toUpperCase() + mediaType.slice(1);
|
|
3892
4022
|
}
|
|
3893
4023
|
function validateStructuredPayloadLocalPath(ctx, payloadPath, mediaType) {
|
|
3894
4024
|
const candidatePaths = resolveWorkspacePathCandidates(normalizePath(payloadPath), ctx.mediaAccess?.workspaceDir);
|
|
@@ -4279,7 +4409,7 @@ function fixPathEncoding(mediaPath, log) {
|
|
|
4279
4409
|
const code = decoded.charCodeAt(i);
|
|
4280
4410
|
if (code <= 255) bytes.push(code);
|
|
4281
4411
|
else {
|
|
4282
|
-
const charBytes = Buffer.from(decoded
|
|
4412
|
+
const charBytes = Buffer.from(decoded.charAt(i), "utf8");
|
|
4283
4413
|
bytes.push(...charBytes);
|
|
4284
4414
|
}
|
|
4285
4415
|
}
|
|
@@ -4308,7 +4438,9 @@ function isInsideCodeBlock(text, position) {
|
|
|
4308
4438
|
let fenceMatch;
|
|
4309
4439
|
let openFence = null;
|
|
4310
4440
|
while ((fenceMatch = fenceRegex.exec(text)) !== null) {
|
|
4311
|
-
const
|
|
4441
|
+
const ticksText = fenceMatch[1];
|
|
4442
|
+
if (ticksText === void 0) continue;
|
|
4443
|
+
const ticks = ticksText.length;
|
|
4312
4444
|
if (!openFence) openFence = {
|
|
4313
4445
|
pos: fenceMatch.index,
|
|
4314
4446
|
ticks
|
|
@@ -4339,7 +4471,9 @@ function findFirstClosedMediaTag(text, log) {
|
|
|
4339
4471
|
continue;
|
|
4340
4472
|
}
|
|
4341
4473
|
const textBefore = text.slice(0, match.index);
|
|
4342
|
-
const
|
|
4474
|
+
const rawTagName = match[1];
|
|
4475
|
+
if (rawTagName === void 0) continue;
|
|
4476
|
+
const tagName = rawTagName.toLowerCase();
|
|
4343
4477
|
let mediaPath = match[2]?.trim() ?? "";
|
|
4344
4478
|
mediaPath = normalizePath(mediaPath);
|
|
4345
4479
|
mediaPath = fixPathEncoding(mediaPath, log);
|
|
@@ -4403,12 +4537,10 @@ async function executeSendQueue(queue, ctx, options = {}) {
|
|
|
4403
4537
|
const transcodeEnabled = account.config?.audioFormatPolicy?.transcodeEnabled !== false;
|
|
4404
4538
|
const voiceTimeout = 45e3;
|
|
4405
4539
|
try {
|
|
4406
|
-
const result = await
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
}), voiceTimeout);
|
|
4411
|
-
})]);
|
|
4540
|
+
const result = await raceWithTimeout(() => sendVoice(mediaTarget, item.content, uploadFormats, transcodeEnabled), voiceTimeout, () => ({
|
|
4541
|
+
channel: "qqbot",
|
|
4542
|
+
error: "语音发送超时,已跳过"
|
|
4543
|
+
}));
|
|
4412
4544
|
if (result.error) {
|
|
4413
4545
|
log?.error(`${prefix} sendVoice error: ${result.error}`);
|
|
4414
4546
|
await sendFallbackText(resolveUserFacingMediaError(result));
|
|
@@ -4514,7 +4646,7 @@ function stripIncompleteMediaTag(text) {
|
|
|
4514
4646
|
let searchIsClosing = false;
|
|
4515
4647
|
let fallbackPos = -1;
|
|
4516
4648
|
for (let i = lastLine.length - 1; i >= 0; i--) {
|
|
4517
|
-
const ch = lastLine
|
|
4649
|
+
const ch = lastLine.charAt(i);
|
|
4518
4650
|
if (ch !== "<" && ch !== "<") continue;
|
|
4519
4651
|
const after = lastLine.slice(i + 1);
|
|
4520
4652
|
const isClosing = after.startsWith("/");
|
|
@@ -4522,7 +4654,9 @@ function stripIncompleteMediaTag(text) {
|
|
|
4522
4654
|
const nameMatch = nameStr.match(/^(\w+)/);
|
|
4523
4655
|
if (searchTag) {
|
|
4524
4656
|
if (!nameMatch || isClosing) continue;
|
|
4525
|
-
const
|
|
4657
|
+
const candidateName = nameMatch[1];
|
|
4658
|
+
if (candidateName === void 0) continue;
|
|
4659
|
+
const cand = candidateName.toLowerCase();
|
|
4526
4660
|
if (!isMedia(cand)) continue;
|
|
4527
4661
|
if (hasClosingAfter(i, cand)) continue;
|
|
4528
4662
|
if (searchTag === "*") return cutAt(i);
|
|
@@ -4543,6 +4677,7 @@ function stripIncompleteMediaTag(text) {
|
|
|
4543
4677
|
continue;
|
|
4544
4678
|
}
|
|
4545
4679
|
const tag = nameMatch[1];
|
|
4680
|
+
if (tag === void 0) continue;
|
|
4546
4681
|
const restAfterName = nameStr.slice(tag.length);
|
|
4547
4682
|
const hasGT = /[>>]/.test(restAfterName);
|
|
4548
4683
|
if (!isMedia(tag) && !(couldBeMedia(tag) && !hasGT)) continue;
|
|
@@ -5152,6 +5287,11 @@ var StreamingController = class {
|
|
|
5152
5287
|
return;
|
|
5153
5288
|
}
|
|
5154
5289
|
const firstText = safeText;
|
|
5290
|
+
if (!claimMessageReply(this.deps.replyToMsgId).allowed) {
|
|
5291
|
+
this.logWarn(`stream budget unavailable; falling back to static delivery`);
|
|
5292
|
+
this.transition("aborted", "doStartStreaming", "passive_budget_exhausted");
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5155
5295
|
const resp = await this.sendStreamChunk(firstText, StreamInputState.GENERATING, "doStartStreaming");
|
|
5156
5296
|
if (!resp.id) throw new Error(`Stream API returned no id: ${JSON.stringify(resp)}`);
|
|
5157
5297
|
this.streamMsgId = resp.id;
|
|
@@ -5248,15 +5388,12 @@ async function sendMediaQueue(queue, ctx) {
|
|
|
5248
5388
|
}
|
|
5249
5389
|
/**
|
|
5250
5390
|
* 是否对私聊走 QQ 官方 C2C `stream_messages` 流式 API。
|
|
5251
|
-
* - `streaming: true`
|
|
5252
|
-
* -
|
|
5391
|
+
* - `streaming.nativeTransport: true` 启用;仅 C2C 场景生效。
|
|
5392
|
+
* - 旧的 `streaming: true` 布尔与 `c2cStreamApi` 键由 `openclaw doctor --fix` 迁移。
|
|
5253
5393
|
*/
|
|
5254
5394
|
function shouldUseOfficialC2cStream(account, targetType) {
|
|
5255
5395
|
if (targetType !== "c2c") return false;
|
|
5256
|
-
|
|
5257
|
-
if (s === true) return true;
|
|
5258
|
-
if (s && typeof s === "object" && s.c2cStreamApi === true) return true;
|
|
5259
|
-
return false;
|
|
5396
|
+
return account.config?.streaming?.nativeTransport === true;
|
|
5260
5397
|
}
|
|
5261
5398
|
//#endregion
|
|
5262
5399
|
//#region extensions/qqbot/src/engine/utils/audio.ts
|
|
@@ -5319,11 +5456,10 @@ async function convertSilkToWav(inputPath, outputDir) {
|
|
|
5319
5456
|
return null;
|
|
5320
5457
|
}
|
|
5321
5458
|
const strippedBuf = stripAmrHeader(fileBuf);
|
|
5322
|
-
const rawData = new Uint8Array(strippedBuf.buffer, strippedBuf.byteOffset, strippedBuf.byteLength);
|
|
5323
5459
|
const silk = await loadSilkWasm();
|
|
5324
|
-
if (!silk || !silk.isSilk(
|
|
5460
|
+
if (!silk || !silk.isSilk(strippedBuf)) return null;
|
|
5325
5461
|
const sampleRate = 24e3;
|
|
5326
|
-
const result = await silk.decode(
|
|
5462
|
+
const result = await silk.decode(strippedBuf, sampleRate);
|
|
5327
5463
|
const wavBuffer = pcmToWav(result.data, sampleRate);
|
|
5328
5464
|
const dir = outputDir || path$1.dirname(inputPath);
|
|
5329
5465
|
if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
|
|
@@ -5337,7 +5473,8 @@ async function convertSilkToWav(inputPath, outputDir) {
|
|
|
5337
5473
|
}
|
|
5338
5474
|
/** Check whether an attachment is a voice file (by MIME type or extension). */
|
|
5339
5475
|
function isVoiceAttachment(att) {
|
|
5340
|
-
|
|
5476
|
+
const contentType = normalizeLowercaseStringOrEmpty$1(att.content_type);
|
|
5477
|
+
if (contentType === "voice" || contentType.startsWith("audio/")) return true;
|
|
5341
5478
|
const ext = att.filename ? normalizeLowercaseStringOrEmpty$1(path$1.extname(att.filename)) : "";
|
|
5342
5479
|
return [
|
|
5343
5480
|
".amr",
|
|
@@ -5422,39 +5559,23 @@ async function audioFileToSilkBase64(filePath, directUploadFormats) {
|
|
|
5422
5559
|
debugLog(`[audio-convert] direct upload (QQ native format): ${ext} (${buf.length} bytes)`);
|
|
5423
5560
|
return buf.toString("base64");
|
|
5424
5561
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
if ((await loadSilkWasm())?.isSilk(raw)) {
|
|
5429
|
-
debugLog(`[audio-convert] SILK file, direct use: ${filePath} (${buf.length} bytes)`);
|
|
5430
|
-
return buf.toString("base64");
|
|
5431
|
-
}
|
|
5432
|
-
}
|
|
5433
|
-
const rawCheck = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
5434
|
-
const strippedCheck = stripAmrHeader(buf);
|
|
5435
|
-
const strippedRaw = new Uint8Array(strippedCheck.buffer, strippedCheck.byteOffset, strippedCheck.byteLength);
|
|
5436
|
-
const silkForCheck = await loadSilkWasm();
|
|
5437
|
-
if (silkForCheck?.isSilk(rawCheck) || silkForCheck?.isSilk(strippedRaw)) {
|
|
5562
|
+
const stripped = stripAmrHeader(buf);
|
|
5563
|
+
const silk = await loadSilkWasm();
|
|
5564
|
+
if (silk?.isSilk(buf) || silk?.isSilk(stripped)) {
|
|
5438
5565
|
debugLog(`[audio-convert] SILK detected by header: ${filePath} (${buf.length} bytes)`);
|
|
5439
5566
|
return buf.toString("base64");
|
|
5440
5567
|
}
|
|
5441
5568
|
const targetRate = 24e3;
|
|
5442
5569
|
debugLog(`[audio-convert] fallback: trying WASM decoders for ${ext}`);
|
|
5443
|
-
if (ext === ".pcm")
|
|
5444
|
-
const { silkBuffer } = await pcmToSilk(Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength), targetRate);
|
|
5445
|
-
return silkBuffer.toString("base64");
|
|
5446
|
-
}
|
|
5570
|
+
if (ext === ".pcm") return (await pcmToSilk(buf, targetRate)).toString("base64");
|
|
5447
5571
|
if (ext === ".wav" || buf.length >= 4 && buf.toString("ascii", 0, 4) === "RIFF") {
|
|
5448
5572
|
const wavInfo = parseWavFallback(buf);
|
|
5449
|
-
if (wavInfo)
|
|
5450
|
-
const { silkBuffer } = await pcmToSilk(wavInfo, targetRate);
|
|
5451
|
-
return silkBuffer.toString("base64");
|
|
5452
|
-
}
|
|
5573
|
+
if (wavInfo) return (await pcmToSilk(wavInfo, targetRate)).toString("base64");
|
|
5453
5574
|
}
|
|
5454
5575
|
if (ext === ".mp3" || ext === ".mpeg") {
|
|
5455
5576
|
const pcmBuf = await wasmDecodeMp3ToPCM(buf, targetRate);
|
|
5456
5577
|
if (pcmBuf) {
|
|
5457
|
-
const
|
|
5578
|
+
const silkBuffer = await pcmToSilk(pcmBuf, targetRate);
|
|
5458
5579
|
debugLog(`[audio-convert] WASM: MP3 → SILK done (${silkBuffer.length} bytes)`);
|
|
5459
5580
|
return silkBuffer.toString("base64");
|
|
5460
5581
|
}
|
|
@@ -5524,12 +5645,8 @@ async function waitForFile(filePath, timeoutMs = 3e4, pollMs = 500) {
|
|
|
5524
5645
|
async function pcmToSilk(pcmBuffer, sampleRate) {
|
|
5525
5646
|
const silk = await loadSilkWasm();
|
|
5526
5647
|
if (!silk) throw new Error("silk-wasm is not available; cannot encode PCM to SILK");
|
|
5527
|
-
const
|
|
5528
|
-
|
|
5529
|
-
return {
|
|
5530
|
-
silkBuffer: Buffer.from(result.data.buffer, result.data.byteOffset, result.data.byteLength),
|
|
5531
|
-
duration: result.duration
|
|
5532
|
-
};
|
|
5648
|
+
const result = await silk.encode(pcmBuffer, sampleRate);
|
|
5649
|
+
return Buffer.from(result.data.buffer, result.data.byteOffset, result.data.byteLength);
|
|
5533
5650
|
}
|
|
5534
5651
|
/** Decode MP3 to PCM via mpg123-decoder WASM. */
|
|
5535
5652
|
async function wasmDecodeMp3ToPCM(buf, targetRate) {
|
|
@@ -5546,20 +5663,21 @@ async function wasmDecodeMp3ToPCM(buf, targetRate) {
|
|
|
5546
5663
|
}
|
|
5547
5664
|
debugLog(`[audio-convert] WASM MP3 decode: samples=${decoded.samplesDecoded}, sampleRate=${decoded.sampleRate}, channels=${decoded.channelData.length}`);
|
|
5548
5665
|
let floatMono;
|
|
5549
|
-
if (decoded.channelData.length === 1) floatMono = decoded.channelData
|
|
5666
|
+
if (decoded.channelData.length === 1) floatMono = expectDefined(decoded.channelData.at(0), "single decoded MP3 channel");
|
|
5550
5667
|
else {
|
|
5551
5668
|
floatMono = new Float32Array(decoded.samplesDecoded);
|
|
5552
5669
|
const channels = decoded.channelData.length;
|
|
5553
5670
|
for (let i = 0; i < decoded.samplesDecoded; i++) {
|
|
5554
5671
|
let sum = 0;
|
|
5555
|
-
for (
|
|
5672
|
+
for (const channel of decoded.channelData) sum += expectDefined(channel.at(i), "decoded MP3 channel sample");
|
|
5556
5673
|
floatMono[i] = sum / channels;
|
|
5557
5674
|
}
|
|
5558
5675
|
}
|
|
5559
5676
|
const s16 = new Uint8Array(floatMono.length * 2);
|
|
5560
5677
|
const view = new DataView(s16.buffer);
|
|
5561
5678
|
for (let i = 0; i < floatMono.length; i++) {
|
|
5562
|
-
const
|
|
5679
|
+
const sample = expectDefined(floatMono.at(i), "mono MP3 sample index");
|
|
5680
|
+
const clamped = Math.max(-1, Math.min(1, sample));
|
|
5563
5681
|
const val = clamped < 0 ? clamped * 32768 : clamped * 32767;
|
|
5564
5682
|
view.setInt16(i * 2, Math.round(val), true);
|
|
5565
5683
|
}
|
|
@@ -5720,7 +5838,6 @@ const TOOL_MEDIA_SEND_TIMEOUT = 45e3;
|
|
|
5720
5838
|
function shouldDeliverToolProgressImmediately(account, useOfficialC2cStream) {
|
|
5721
5839
|
if (useOfficialC2cStream) return true;
|
|
5722
5840
|
const streaming = account.config?.streaming;
|
|
5723
|
-
if (streaming === true) return true;
|
|
5724
5841
|
return typeof streaming === "object" && streaming !== null && streaming.mode !== "off";
|
|
5725
5842
|
}
|
|
5726
5843
|
function immediateToolProgressText(payload) {
|
|
@@ -6132,11 +6249,7 @@ async function dispatchOutbound(inbound, deps) {
|
|
|
6132
6249
|
}
|
|
6133
6250
|
},
|
|
6134
6251
|
replyOptions: {
|
|
6135
|
-
disableBlockStreaming: useOfficialC2cStream ? true :
|
|
6136
|
-
const s = account.config?.streaming;
|
|
6137
|
-
if (s === false) return true;
|
|
6138
|
-
return typeof s === "object" && s !== null && s.mode === "off";
|
|
6139
|
-
})(),
|
|
6252
|
+
disableBlockStreaming: useOfficialC2cStream ? true : account.config?.streaming?.mode === "off",
|
|
6140
6253
|
...streamingController ? { onPartialReply: async (payload) => {
|
|
6141
6254
|
try {
|
|
6142
6255
|
await streamingController.onPartialReply(payload);
|
|
@@ -6262,41 +6375,35 @@ async function buildCtxPayload(inbound, runtime, cfg) {
|
|
|
6262
6375
|
/**
|
|
6263
6376
|
* Periodically refresh C2C typing state while a response is in progress.
|
|
6264
6377
|
*
|
|
6265
|
-
*
|
|
6266
|
-
*
|
|
6378
|
+
* Interval scheduling comes from the core typing keepalive loop; this module
|
|
6379
|
+
* owns the QQ passive-reply budget accounting and token-refresh retry.
|
|
6267
6380
|
*/
|
|
6268
6381
|
/** Refresh every 5s for the QQ API's 10s input-notify window. */
|
|
6269
6382
|
const TYPING_INTERVAL_MS = 5e3;
|
|
6383
|
+
const FINAL_REPLY_RESERVE_COUNT = 1;
|
|
6270
6384
|
var TypingKeepAlive = class {
|
|
6271
|
-
constructor(getToken, clearCache, sendInputNotify, openid, msgId, log) {
|
|
6385
|
+
constructor(getToken, clearCache, sendInputNotify, openid, msgId, log, claimPassiveReply = claimMessageReply) {
|
|
6272
6386
|
this.getToken = getToken;
|
|
6273
6387
|
this.clearCache = clearCache;
|
|
6274
6388
|
this.sendInputNotify = sendInputNotify;
|
|
6275
6389
|
this.openid = openid;
|
|
6276
6390
|
this.msgId = msgId;
|
|
6277
6391
|
this.log = log;
|
|
6278
|
-
this.
|
|
6392
|
+
this.claimPassiveReply = claimPassiveReply;
|
|
6279
6393
|
this.stopped = false;
|
|
6280
|
-
this.
|
|
6394
|
+
this.loop = createTypingKeepaliveLoop({
|
|
6395
|
+
intervalMs: TYPING_INTERVAL_MS,
|
|
6396
|
+
onTick: () => this.send()
|
|
6397
|
+
});
|
|
6281
6398
|
}
|
|
6282
6399
|
/** Start periodic keep-alive sends. */
|
|
6283
6400
|
start() {
|
|
6284
|
-
if (this.stopped)
|
|
6285
|
-
this.timer = setInterval(() => {
|
|
6286
|
-
if (this.stopped) {
|
|
6287
|
-
this.stop();
|
|
6288
|
-
return;
|
|
6289
|
-
}
|
|
6290
|
-
this.send().catch(() => {});
|
|
6291
|
-
}, TYPING_INTERVAL_MS);
|
|
6401
|
+
if (!this.stopped) this.loop.start();
|
|
6292
6402
|
}
|
|
6293
6403
|
/** Stop periodic keep-alive sends. */
|
|
6294
6404
|
stop() {
|
|
6295
6405
|
this.stopped = true;
|
|
6296
|
-
|
|
6297
|
-
clearInterval(this.timer);
|
|
6298
|
-
this.timer = null;
|
|
6299
|
-
}
|
|
6406
|
+
this.loop.stop();
|
|
6300
6407
|
}
|
|
6301
6408
|
async send() {
|
|
6302
6409
|
try {
|
|
@@ -6313,16 +6420,18 @@ var TypingKeepAlive = class {
|
|
|
6313
6420
|
}
|
|
6314
6421
|
}
|
|
6315
6422
|
async sendAttempt(token) {
|
|
6316
|
-
if (this.stopped
|
|
6423
|
+
if (this.stopped) return;
|
|
6424
|
+
const claim = this.claimPassiveReply(this.msgId, FINAL_REPLY_RESERVE_COUNT);
|
|
6425
|
+
if (!claim.allowed) {
|
|
6426
|
+
this.log?.debug?.(`Typing keep-alive budget exhausted for ${this.openid}`);
|
|
6317
6427
|
this.stop();
|
|
6318
6428
|
return;
|
|
6319
6429
|
}
|
|
6320
|
-
this.renewalsRemaining--;
|
|
6321
6430
|
try {
|
|
6322
6431
|
await this.sendInputNotify(token, this.openid, this.msgId, 10);
|
|
6323
6432
|
this.log?.debug?.(`Typing keep-alive sent to ${this.openid}`);
|
|
6324
6433
|
} finally {
|
|
6325
|
-
if (
|
|
6434
|
+
if (claim.remaining <= FINAL_REPLY_RESERVE_COUNT) {
|
|
6326
6435
|
this.log?.debug?.(`Typing keep-alive budget exhausted for ${this.openid}`);
|
|
6327
6436
|
this.stop();
|
|
6328
6437
|
}
|
|
@@ -6503,6 +6612,7 @@ async function startTypingForEvent(event, account, log) {
|
|
|
6503
6612
|
const creds = accountToCreds(account);
|
|
6504
6613
|
const rawNotifyFn = createRawInputNotifyFn(account.appId);
|
|
6505
6614
|
const sendNotifyAndStartKeepAlive = async () => {
|
|
6615
|
+
if (!claimMessageReply(event.messageId, 1).allowed) return { keepAlive: null };
|
|
6506
6616
|
const resp = await sendInputNotify({
|
|
6507
6617
|
openid: event.senderId,
|
|
6508
6618
|
creds,
|