@openclaw-china/qqbot 2026.3.5 → 2026.3.7
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/index.d.ts +143 -2
- package/dist/index.js +625 -287
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +12 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import * as fs3 from 'fs';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
1
|
import * as os from 'os';
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
import * as
|
|
2
|
+
import { homedir, tmpdir } from 'os';
|
|
3
|
+
import * as path2 from 'path';
|
|
6
4
|
import { join } from 'path';
|
|
5
|
+
import * as fs3 from 'fs';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import * as fsPromises from 'fs/promises';
|
|
9
9
|
import { createHmac } from 'crypto';
|
|
@@ -4242,13 +4242,31 @@ var QQBotAccountSchema = external_exports.object({
|
|
|
4242
4242
|
historyLimit: external_exports.number().int().min(0).optional().default(10),
|
|
4243
4243
|
textChunkLimit: external_exports.number().int().positive().optional().default(1500),
|
|
4244
4244
|
replyFinalOnly: external_exports.boolean().optional().default(false),
|
|
4245
|
+
longTaskNoticeDelayMs: external_exports.number().int().min(0).optional().default(3e4),
|
|
4245
4246
|
maxFileSizeMB: external_exports.number().positive().optional().default(100),
|
|
4246
|
-
mediaTimeoutMs: external_exports.number().int().positive().optional().default(3e4)
|
|
4247
|
+
mediaTimeoutMs: external_exports.number().int().positive().optional().default(3e4),
|
|
4248
|
+
inboundMedia: external_exports.object({
|
|
4249
|
+
dir: external_exports.string().optional(),
|
|
4250
|
+
keepDays: external_exports.number().optional()
|
|
4251
|
+
}).optional()
|
|
4247
4252
|
});
|
|
4248
4253
|
QQBotAccountSchema.extend({
|
|
4249
4254
|
defaultAccount: external_exports.string().optional(),
|
|
4250
4255
|
accounts: external_exports.record(QQBotAccountSchema).optional()
|
|
4251
4256
|
});
|
|
4257
|
+
var DEFAULT_INBOUND_MEDIA_DIR = join(homedir(), ".openclaw", "media", "qqbot", "inbound");
|
|
4258
|
+
var DEFAULT_INBOUND_MEDIA_KEEP_DAYS = 7;
|
|
4259
|
+
var DEFAULT_INBOUND_MEDIA_TEMP_DIR = join(tmpdir(), "qqbot-media");
|
|
4260
|
+
function resolveInboundMediaDir(config) {
|
|
4261
|
+
return String(config?.inboundMedia?.dir ?? "").trim() || DEFAULT_INBOUND_MEDIA_DIR;
|
|
4262
|
+
}
|
|
4263
|
+
function resolveInboundMediaKeepDays(config) {
|
|
4264
|
+
const value = config?.inboundMedia?.keepDays;
|
|
4265
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_INBOUND_MEDIA_KEEP_DAYS;
|
|
4266
|
+
}
|
|
4267
|
+
function resolveInboundMediaTempDir() {
|
|
4268
|
+
return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
|
|
4269
|
+
}
|
|
4252
4270
|
var DEFAULT_ACCOUNT_ID = "default";
|
|
4253
4271
|
function listConfiguredAccountIds(cfg) {
|
|
4254
4272
|
const accounts = cfg.channels?.qqbot?.accounts;
|
|
@@ -4639,10 +4657,10 @@ function normalizeLocalPath(raw) {
|
|
|
4639
4657
|
} catch {
|
|
4640
4658
|
}
|
|
4641
4659
|
if (p.startsWith("~/") || p === "~") {
|
|
4642
|
-
p =
|
|
4660
|
+
p = path2.join(os.homedir(), p.slice(1));
|
|
4643
4661
|
} else if (p.startsWith("~")) ;
|
|
4644
|
-
if (!
|
|
4645
|
-
p =
|
|
4662
|
+
if (!path2.isAbsolute(p)) {
|
|
4663
|
+
p = path2.resolve(process.cwd(), p);
|
|
4646
4664
|
}
|
|
4647
4665
|
return p;
|
|
4648
4666
|
}
|
|
@@ -4652,7 +4670,7 @@ function stripTitleFromUrl(value) {
|
|
|
4652
4670
|
return match ? match[1] : trimmed;
|
|
4653
4671
|
}
|
|
4654
4672
|
function getExtension(filePath) {
|
|
4655
|
-
const ext =
|
|
4673
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
4656
4674
|
return ext.startsWith(".") ? ext.slice(1) : ext;
|
|
4657
4675
|
}
|
|
4658
4676
|
function isImagePath(filePath) {
|
|
@@ -4678,11 +4696,11 @@ function createExtractedMedia(source, sourceKind, options) {
|
|
|
4678
4696
|
let fileName;
|
|
4679
4697
|
if (isLocal) {
|
|
4680
4698
|
localPath = normalizeLocalPath(cleanSource);
|
|
4681
|
-
fileName =
|
|
4699
|
+
fileName = path2.basename(localPath);
|
|
4682
4700
|
} else if (isHttp) {
|
|
4683
4701
|
try {
|
|
4684
4702
|
const url = new URL(cleanSource);
|
|
4685
|
-
fileName =
|
|
4703
|
+
fileName = path2.basename(url.pathname) || void 0;
|
|
4686
4704
|
} catch {
|
|
4687
4705
|
}
|
|
4688
4706
|
}
|
|
@@ -4840,7 +4858,7 @@ function extractMediaFromText(text, options = {}) {
|
|
|
4840
4858
|
if (media.type !== "image" && isNonImageFilePath(media.localPath || rawPath)) {
|
|
4841
4859
|
if (addMedia(media)) {
|
|
4842
4860
|
if (removeFromText && match.index !== void 0) {
|
|
4843
|
-
const fileName = media.fileName ||
|
|
4861
|
+
const fileName = media.fileName || path2.basename(rawPath);
|
|
4844
4862
|
replacements.push({
|
|
4845
4863
|
start: match.index,
|
|
4846
4864
|
end: match.index + fullMatch.length,
|
|
@@ -4885,7 +4903,7 @@ function extractMediaFromText(text, options = {}) {
|
|
|
4885
4903
|
if (media.type !== "image") {
|
|
4886
4904
|
if (addMedia(media)) {
|
|
4887
4905
|
if (removeFromText && match.index !== void 0) {
|
|
4888
|
-
const fileName = media.fileName ||
|
|
4906
|
+
const fileName = media.fileName || path2.basename(rawPath);
|
|
4889
4907
|
replacements.push({
|
|
4890
4908
|
start: match.index,
|
|
4891
4909
|
end: match.index + fullMatch.length,
|
|
@@ -4934,13 +4952,27 @@ function sanitizeFileName(name) {
|
|
|
4934
4952
|
function resolveFileNameFromUrl(url) {
|
|
4935
4953
|
try {
|
|
4936
4954
|
const parsed = new URL(url);
|
|
4937
|
-
const base =
|
|
4955
|
+
const base = path2.basename(parsed.pathname);
|
|
4938
4956
|
if (!base || base === "/") return void 0;
|
|
4939
4957
|
return base;
|
|
4940
4958
|
} catch {
|
|
4941
4959
|
return void 0;
|
|
4942
4960
|
}
|
|
4943
4961
|
}
|
|
4962
|
+
function normalizeForCompare(value) {
|
|
4963
|
+
return path2.resolve(value).replace(/\\/g, "/").toLowerCase();
|
|
4964
|
+
}
|
|
4965
|
+
function isPathUnderDir(filePath, dirPath) {
|
|
4966
|
+
const f = normalizeForCompare(filePath);
|
|
4967
|
+
const d2 = normalizeForCompare(dirPath).replace(/\/+$/, "");
|
|
4968
|
+
return f === d2 || f.startsWith(`${d2}/`);
|
|
4969
|
+
}
|
|
4970
|
+
function formatDateDir(date = /* @__PURE__ */ new Date()) {
|
|
4971
|
+
const yyyy = date.getFullYear();
|
|
4972
|
+
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
4973
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
4974
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
4975
|
+
}
|
|
4944
4976
|
var EXT_TO_MIME = {
|
|
4945
4977
|
// 图片
|
|
4946
4978
|
jpg: "image/jpeg",
|
|
@@ -5035,15 +5067,15 @@ function validatePathSecurity(filePath, options = {}) {
|
|
|
5035
5067
|
);
|
|
5036
5068
|
}
|
|
5037
5069
|
if (preventTraversal) {
|
|
5038
|
-
const normalized =
|
|
5070
|
+
const normalized = path2.normalize(filePath);
|
|
5039
5071
|
if (normalized.includes("..")) {
|
|
5040
5072
|
throw new PathSecurityError(filePath, "Path traversal detected");
|
|
5041
5073
|
}
|
|
5042
5074
|
}
|
|
5043
5075
|
if (allowedPrefixes && allowedPrefixes.length > 0) {
|
|
5044
|
-
const normalizedPath =
|
|
5076
|
+
const normalizedPath = path2.normalize(filePath);
|
|
5045
5077
|
const isAllowed = allowedPrefixes.some(
|
|
5046
|
-
(prefix) => normalizedPath.startsWith(
|
|
5078
|
+
(prefix) => normalizedPath.startsWith(path2.normalize(prefix))
|
|
5047
5079
|
);
|
|
5048
5080
|
if (!isAllowed) {
|
|
5049
5081
|
throw new PathSecurityError(
|
|
@@ -5086,7 +5118,7 @@ async function fetchMediaFromUrl(url, options = {}) {
|
|
|
5086
5118
|
let fileName = "file";
|
|
5087
5119
|
try {
|
|
5088
5120
|
const urlPath = new URL(url).pathname;
|
|
5089
|
-
fileName =
|
|
5121
|
+
fileName = path2.basename(urlPath) || "file";
|
|
5090
5122
|
} catch {
|
|
5091
5123
|
}
|
|
5092
5124
|
const mimeType = response.headers.get("content-type")?.split(";")[0].trim() || getMimeType(fileName);
|
|
@@ -5159,7 +5191,7 @@ async function downloadToTempFile(url, options = {}) {
|
|
|
5159
5191
|
const ext = resolveExtension(contentType, sourceName);
|
|
5160
5192
|
const random = Math.random().toString(36).slice(2, 8);
|
|
5161
5193
|
const fileName = `${safePrefix}-${Date.now()}-${random}${ext}`;
|
|
5162
|
-
const fullPath =
|
|
5194
|
+
const fullPath = path2.join(tempDir, fileName);
|
|
5163
5195
|
await fsPromises.mkdir(tempDir, { recursive: true });
|
|
5164
5196
|
const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
5165
5197
|
await fsPromises.writeFile(fullPath, buffer);
|
|
@@ -5191,7 +5223,7 @@ async function readMediaFromLocal(filePath, options = {}) {
|
|
|
5191
5223
|
throw new FileSizeLimitError(stats.size, maxSize);
|
|
5192
5224
|
}
|
|
5193
5225
|
const buffer = await fsPromises.readFile(localPath);
|
|
5194
|
-
const fileName =
|
|
5226
|
+
const fileName = path2.basename(localPath);
|
|
5195
5227
|
const mimeType = getMimeType(localPath);
|
|
5196
5228
|
return {
|
|
5197
5229
|
buffer,
|
|
@@ -5206,6 +5238,63 @@ async function readMedia(source, options = {}) {
|
|
|
5206
5238
|
}
|
|
5207
5239
|
return readMediaFromLocal(source, options);
|
|
5208
5240
|
}
|
|
5241
|
+
async function finalizeInboundMediaFile(options) {
|
|
5242
|
+
const current = String(options.filePath ?? "").trim();
|
|
5243
|
+
if (!current) return current;
|
|
5244
|
+
if (!isPathUnderDir(current, options.tempDir)) {
|
|
5245
|
+
return current;
|
|
5246
|
+
}
|
|
5247
|
+
const datedDir = path2.join(options.inboundDir, formatDateDir());
|
|
5248
|
+
const target = path2.join(datedDir, path2.basename(current));
|
|
5249
|
+
try {
|
|
5250
|
+
await fsPromises.mkdir(datedDir, { recursive: true });
|
|
5251
|
+
await fsPromises.rename(current, target);
|
|
5252
|
+
return target;
|
|
5253
|
+
} catch {
|
|
5254
|
+
return current;
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
async function pruneInboundMediaDir(options) {
|
|
5258
|
+
const keepDays = Number(options.keepDays);
|
|
5259
|
+
if (!Number.isFinite(keepDays) || keepDays < 0) return;
|
|
5260
|
+
const now = options.nowMs ?? Date.now();
|
|
5261
|
+
const cutoff = now - keepDays * 24 * 60 * 60 * 1e3;
|
|
5262
|
+
let entries = [];
|
|
5263
|
+
try {
|
|
5264
|
+
entries = await fsPromises.readdir(options.inboundDir);
|
|
5265
|
+
} catch {
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
for (const entry of entries) {
|
|
5269
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry)) continue;
|
|
5270
|
+
const dirPath = path2.join(options.inboundDir, entry);
|
|
5271
|
+
let dirStats;
|
|
5272
|
+
try {
|
|
5273
|
+
dirStats = await fsPromises.stat(dirPath);
|
|
5274
|
+
} catch {
|
|
5275
|
+
continue;
|
|
5276
|
+
}
|
|
5277
|
+
if (!dirStats.isDirectory()) continue;
|
|
5278
|
+
const dirTime = dirStats.mtimeMs || dirStats.ctimeMs || 0;
|
|
5279
|
+
if (dirTime >= cutoff) continue;
|
|
5280
|
+
let files = [];
|
|
5281
|
+
try {
|
|
5282
|
+
files = await fsPromises.readdir(dirPath);
|
|
5283
|
+
} catch {
|
|
5284
|
+
continue;
|
|
5285
|
+
}
|
|
5286
|
+
for (const file of files) {
|
|
5287
|
+
const fp = path2.join(dirPath, file);
|
|
5288
|
+
try {
|
|
5289
|
+
const fst = await fsPromises.stat(fp);
|
|
5290
|
+
if (fst.isFile() && (fst.mtimeMs || fst.ctimeMs || 0) < cutoff) {
|
|
5291
|
+
await fsPromises.unlink(fp);
|
|
5292
|
+
}
|
|
5293
|
+
} catch {
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5209
5298
|
async function cleanupFileSafe(filePath, onError) {
|
|
5210
5299
|
if (!filePath) return;
|
|
5211
5300
|
try {
|
|
@@ -6990,6 +7079,12 @@ function requireTrimmedString(value, field) {
|
|
|
6990
7079
|
}
|
|
6991
7080
|
return normalized;
|
|
6992
7081
|
}
|
|
7082
|
+
function sanitizeUploadFileName(fileName) {
|
|
7083
|
+
const trimmed = fileName.trim();
|
|
7084
|
+
if (!trimmed) return "file";
|
|
7085
|
+
const normalized = trimmed.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_");
|
|
7086
|
+
return normalized || "file";
|
|
7087
|
+
}
|
|
6993
7088
|
function nextMsgSeq(sequenceKey) {
|
|
6994
7089
|
if (!sequenceKey) return MSG_SEQ_BASE + 1;
|
|
6995
7090
|
const current = msgSeqMap.get(sequenceKey) ?? 0;
|
|
@@ -7106,8 +7201,7 @@ async function sendGroupMessage(params) {
|
|
|
7106
7201
|
eventId: params.eventId,
|
|
7107
7202
|
markdown: params.markdown
|
|
7108
7203
|
});
|
|
7109
|
-
|
|
7110
|
-
return apiPost(params.accessToken, `/v2/groups/${groupOpenidLower}/messages`, body, {
|
|
7204
|
+
return apiPost(params.accessToken, `/v2/groups/${params.groupOpenid}/messages`, body, {
|
|
7111
7205
|
timeout: 15e3
|
|
7112
7206
|
});
|
|
7113
7207
|
}
|
|
@@ -7116,8 +7210,7 @@ async function sendChannelMessage(params) {
|
|
|
7116
7210
|
if (params.messageId) {
|
|
7117
7211
|
body.msg_id = params.messageId;
|
|
7118
7212
|
}
|
|
7119
|
-
|
|
7120
|
-
return apiPost(params.accessToken, `/channels/${channelIdLower}/messages`, body, {
|
|
7213
|
+
return apiPost(params.accessToken, `/channels/${params.channelId}/messages`, body, {
|
|
7121
7214
|
timeout: 15e3
|
|
7122
7215
|
});
|
|
7123
7216
|
}
|
|
@@ -7140,7 +7233,8 @@ async function sendC2CInputNotify(params) {
|
|
|
7140
7233
|
}
|
|
7141
7234
|
async function uploadC2CMedia(params) {
|
|
7142
7235
|
const body = {
|
|
7143
|
-
file_type: params.fileType
|
|
7236
|
+
file_type: params.fileType,
|
|
7237
|
+
srv_send_msg: params.srvSendMsg ?? false
|
|
7144
7238
|
};
|
|
7145
7239
|
if (params.url) {
|
|
7146
7240
|
body.url = params.url;
|
|
@@ -7149,13 +7243,17 @@ async function uploadC2CMedia(params) {
|
|
|
7149
7243
|
} else {
|
|
7150
7244
|
throw new Error("uploadC2CMedia requires url or fileData");
|
|
7151
7245
|
}
|
|
7246
|
+
if (params.fileType === 4 /* FILE */ && params.fileName?.trim()) {
|
|
7247
|
+
body.file_name = sanitizeUploadFileName(params.fileName);
|
|
7248
|
+
}
|
|
7152
7249
|
return apiPost(params.accessToken, `/v2/users/${params.openid}/files`, body, {
|
|
7153
7250
|
timeout: 3e4
|
|
7154
7251
|
});
|
|
7155
7252
|
}
|
|
7156
7253
|
async function uploadGroupMedia(params) {
|
|
7157
7254
|
const body = {
|
|
7158
|
-
file_type: params.fileType
|
|
7255
|
+
file_type: params.fileType,
|
|
7256
|
+
srv_send_msg: params.srvSendMsg ?? false
|
|
7159
7257
|
};
|
|
7160
7258
|
if (params.url) {
|
|
7161
7259
|
body.url = params.url;
|
|
@@ -7164,6 +7262,9 @@ async function uploadGroupMedia(params) {
|
|
|
7164
7262
|
} else {
|
|
7165
7263
|
throw new Error("uploadGroupMedia requires url or fileData");
|
|
7166
7264
|
}
|
|
7265
|
+
if (params.fileType === 4 /* FILE */ && params.fileName?.trim()) {
|
|
7266
|
+
body.file_name = sanitizeUploadFileName(params.fileName);
|
|
7267
|
+
}
|
|
7167
7268
|
return apiPost(params.accessToken, `/v2/groups/${params.groupOpenid}/files`, body, {
|
|
7168
7269
|
timeout: 3e4
|
|
7169
7270
|
});
|
|
@@ -7198,7 +7299,6 @@ async function sendGroupMediaMessage(params) {
|
|
|
7198
7299
|
{ timeout: 15e3 }
|
|
7199
7300
|
);
|
|
7200
7301
|
}
|
|
7201
|
-
var QQBOT_UNSUPPORTED_FILE_TYPE_MESSAGE = "QQ official C2C/group media API does not support generic files (file_type=4, e.g. PDF). Images and other supported media types are unaffected.";
|
|
7202
7302
|
var require2 = createRequire(import.meta.url);
|
|
7203
7303
|
function resolveQQBotMediaFileType(fileName) {
|
|
7204
7304
|
const mediaType = detectMediaType(fileName);
|
|
@@ -7214,7 +7314,7 @@ function resolveQQBotMediaFileType(fileName) {
|
|
|
7214
7314
|
}
|
|
7215
7315
|
}
|
|
7216
7316
|
async function uploadQQBotFile(params) {
|
|
7217
|
-
const { accessToken, target, fileType, url, fileData } = params;
|
|
7317
|
+
const { accessToken, target, fileType, url, fileData, fileName } = params;
|
|
7218
7318
|
if (!url && !fileData) {
|
|
7219
7319
|
throw new Error("QQBot file upload requires url or fileData");
|
|
7220
7320
|
}
|
|
@@ -7222,11 +7322,15 @@ async function uploadQQBotFile(params) {
|
|
|
7222
7322
|
accessToken,
|
|
7223
7323
|
groupOpenid: target.id,
|
|
7224
7324
|
fileType,
|
|
7325
|
+
srvSendMsg: false,
|
|
7326
|
+
...fileName ? { fileName } : {},
|
|
7225
7327
|
...url ? { url } : { fileData }
|
|
7226
7328
|
}) : await uploadC2CMedia({
|
|
7227
7329
|
accessToken,
|
|
7228
7330
|
openid: target.id,
|
|
7229
7331
|
fileType,
|
|
7332
|
+
srvSendMsg: false,
|
|
7333
|
+
...fileName ? { fileName } : {},
|
|
7230
7334
|
...url ? { url } : { fileData }
|
|
7231
7335
|
});
|
|
7232
7336
|
if (!upload.file_info) {
|
|
@@ -7234,14 +7338,29 @@ async function uploadQQBotFile(params) {
|
|
|
7234
7338
|
}
|
|
7235
7339
|
return upload.file_info;
|
|
7236
7340
|
}
|
|
7341
|
+
function deriveUploadFileName(source) {
|
|
7342
|
+
const trimmed = source.trim();
|
|
7343
|
+
if (!trimmed) return void 0;
|
|
7344
|
+
if (isHttpUrl(trimmed)) {
|
|
7345
|
+
try {
|
|
7346
|
+
const pathname = new URL(trimmed).pathname;
|
|
7347
|
+
const base2 = path2.posix.basename(pathname);
|
|
7348
|
+
return base2 && base2 !== "/" ? base2 : void 0;
|
|
7349
|
+
} catch {
|
|
7350
|
+
return void 0;
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
const base = path2.basename(trimmed);
|
|
7354
|
+
return base || void 0;
|
|
7355
|
+
}
|
|
7237
7356
|
async function convertAudioToSilk(audioPath) {
|
|
7238
7357
|
const ffmpegPath = require2("ffmpeg-static");
|
|
7239
7358
|
if (!ffmpegPath) {
|
|
7240
7359
|
throw new Error("ffmpeg-static not found");
|
|
7241
7360
|
}
|
|
7242
7361
|
const silkWasm = require2("silk-wasm");
|
|
7243
|
-
const tmpDir = fs3.mkdtempSync(
|
|
7244
|
-
const pcmPath =
|
|
7362
|
+
const tmpDir = fs3.mkdtempSync(path2.join(os.tmpdir(), "qqbot-silk-"));
|
|
7363
|
+
const pcmPath = path2.join(tmpDir, "audio.pcm");
|
|
7245
7364
|
try {
|
|
7246
7365
|
execFileSync(
|
|
7247
7366
|
ffmpegPath,
|
|
@@ -7259,31 +7378,33 @@ async function convertAudioToSilk(audioPath) {
|
|
|
7259
7378
|
}
|
|
7260
7379
|
}
|
|
7261
7380
|
async function sendFileQQBot(params) {
|
|
7262
|
-
const { cfg, target, mediaUrl, messageId, eventId } = params;
|
|
7263
|
-
|
|
7381
|
+
const { cfg, target, mediaUrl, text, messageId, eventId } = params;
|
|
7382
|
+
const credentials = resolveQQBotCredentials(cfg);
|
|
7383
|
+
if (!credentials) {
|
|
7264
7384
|
throw new Error("QQBot not configured (missing appId/clientSecret)");
|
|
7265
7385
|
}
|
|
7266
7386
|
const src = stripTitleFromUrl(mediaUrl);
|
|
7267
7387
|
const fileType = resolveQQBotMediaFileType(src);
|
|
7268
|
-
if (fileType === 4 /* FILE */) {
|
|
7269
|
-
throw new Error(QQBOT_UNSUPPORTED_FILE_TYPE_MESSAGE);
|
|
7270
|
-
}
|
|
7271
7388
|
const sourceIsHttp = isHttpUrl(src);
|
|
7272
7389
|
const maxFileSizeMB = cfg.maxFileSizeMB ?? 100;
|
|
7273
7390
|
const mediaTimeoutMs = cfg.mediaTimeoutMs ?? 3e4;
|
|
7274
7391
|
const maxSizeBytes = Math.floor(maxFileSizeMB * 1024 * 1024);
|
|
7275
|
-
const
|
|
7392
|
+
const messageText = text?.trim() ? text.trim() : void 0;
|
|
7393
|
+
const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
|
|
7276
7394
|
let fileInfo;
|
|
7277
7395
|
try {
|
|
7278
7396
|
if (sourceIsHttp) {
|
|
7397
|
+
const fileName = fileType === 4 /* FILE */ ? deriveUploadFileName(src) : void 0;
|
|
7279
7398
|
fileInfo = await uploadQQBotFile({
|
|
7280
7399
|
accessToken,
|
|
7281
7400
|
target,
|
|
7282
7401
|
fileType,
|
|
7283
|
-
url: src
|
|
7402
|
+
url: src,
|
|
7403
|
+
...fileName ? { fileName } : {}
|
|
7284
7404
|
});
|
|
7285
7405
|
} else {
|
|
7286
7406
|
let buffer;
|
|
7407
|
+
let fileName = fileType === 4 /* FILE */ ? deriveUploadFileName(src) : void 0;
|
|
7287
7408
|
if (fileType === 3 /* VOICE */) {
|
|
7288
7409
|
try {
|
|
7289
7410
|
const silkData = await convertAudioToSilk(src);
|
|
@@ -7301,12 +7422,16 @@ async function sendFileQQBot(params) {
|
|
|
7301
7422
|
maxSize: maxSizeBytes
|
|
7302
7423
|
});
|
|
7303
7424
|
buffer = local.buffer;
|
|
7425
|
+
if (fileType === 4 /* FILE */) {
|
|
7426
|
+
fileName = local.fileName || fileName;
|
|
7427
|
+
}
|
|
7304
7428
|
}
|
|
7305
7429
|
fileInfo = await uploadQQBotFile({
|
|
7306
7430
|
accessToken,
|
|
7307
7431
|
target,
|
|
7308
7432
|
fileType,
|
|
7309
|
-
fileData: buffer.toString("base64")
|
|
7433
|
+
fileData: buffer.toString("base64"),
|
|
7434
|
+
...fileName ? { fileName } : {}
|
|
7310
7435
|
});
|
|
7311
7436
|
}
|
|
7312
7437
|
} catch (err) {
|
|
@@ -7319,6 +7444,7 @@ async function sendFileQQBot(params) {
|
|
|
7319
7444
|
accessToken,
|
|
7320
7445
|
groupOpenid: target.id,
|
|
7321
7446
|
fileInfo,
|
|
7447
|
+
...messageText ? { content: messageText } : {},
|
|
7322
7448
|
...messageId ? { messageId } : {},
|
|
7323
7449
|
...eventId ? { eventId } : {}
|
|
7324
7450
|
});
|
|
@@ -7332,6 +7458,7 @@ async function sendFileQQBot(params) {
|
|
|
7332
7458
|
accessToken,
|
|
7333
7459
|
openid: target.id,
|
|
7334
7460
|
fileInfo,
|
|
7461
|
+
...messageText ? { content: messageText } : {},
|
|
7335
7462
|
...messageId ? { messageId } : {},
|
|
7336
7463
|
...eventId ? { eventId } : {}
|
|
7337
7464
|
});
|
|
@@ -7406,7 +7533,7 @@ function shortId(value) {
|
|
|
7406
7533
|
function summarizeError(err) {
|
|
7407
7534
|
if (err instanceof HttpError) {
|
|
7408
7535
|
const body = err.body?.trim();
|
|
7409
|
-
return body ?
|
|
7536
|
+
return body ? `${err.message} - ${body}` : err.message;
|
|
7410
7537
|
}
|
|
7411
7538
|
return err instanceof Error ? err.message : String(err);
|
|
7412
7539
|
}
|
|
@@ -7440,6 +7567,9 @@ function shouldRetryWithEventId(err) {
|
|
|
7440
7567
|
}
|
|
7441
7568
|
return text.includes("expire") || text.includes("invalid") || text.includes("not found") || text.includes("\u8D85\u8FC7") || text.includes("\u8D85\u65F6") || text.includes("\u8FC7\u671F") || text.includes("\u5931\u6548") || text.includes("\u65E0\u6548");
|
|
7442
7569
|
}
|
|
7570
|
+
function shouldSendTextAsFollowupForMedia(mediaUrl) {
|
|
7571
|
+
return detectMediaType(stripTitleFromUrl(mediaUrl)) === "file";
|
|
7572
|
+
}
|
|
7443
7573
|
var qqbotOutbound = {
|
|
7444
7574
|
deliveryMode: "direct",
|
|
7445
7575
|
textChunkLimit: 1500,
|
|
@@ -7447,12 +7577,14 @@ var qqbotOutbound = {
|
|
|
7447
7577
|
sendText: async (params) => {
|
|
7448
7578
|
const { cfg, to, text, replyToId, replyEventId, accountId } = params;
|
|
7449
7579
|
const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
7450
|
-
|
|
7580
|
+
const credentials = resolveQQBotCredentials(qqCfg);
|
|
7581
|
+
if (!credentials) {
|
|
7451
7582
|
return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
|
|
7452
7583
|
}
|
|
7453
7584
|
const target = parseTarget(to);
|
|
7454
|
-
const accessToken = await getAccessToken(
|
|
7585
|
+
const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
|
|
7455
7586
|
const markdown = qqCfg.markdownSupport ?? true;
|
|
7587
|
+
const groupMarkdown = false;
|
|
7456
7588
|
try {
|
|
7457
7589
|
if (target.kind === "group") {
|
|
7458
7590
|
let result2;
|
|
@@ -7462,7 +7594,7 @@ var qqbotOutbound = {
|
|
|
7462
7594
|
groupOpenid: target.id,
|
|
7463
7595
|
content: text,
|
|
7464
7596
|
messageId: replyToId,
|
|
7465
|
-
markdown
|
|
7597
|
+
markdown: groupMarkdown
|
|
7466
7598
|
});
|
|
7467
7599
|
} catch (err) {
|
|
7468
7600
|
if (!replyToId || !replyEventId || !shouldRetryWithEventId(err)) {
|
|
@@ -7484,7 +7616,7 @@ var qqbotOutbound = {
|
|
|
7484
7616
|
groupOpenid: target.id,
|
|
7485
7617
|
content: text,
|
|
7486
7618
|
eventId: replyEventId,
|
|
7487
|
-
markdown
|
|
7619
|
+
markdown: groupMarkdown
|
|
7488
7620
|
});
|
|
7489
7621
|
logEventIdFallback({
|
|
7490
7622
|
phase: "success",
|
|
@@ -7576,7 +7708,7 @@ var qqbotOutbound = {
|
|
|
7576
7708
|
}
|
|
7577
7709
|
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
|
|
7578
7710
|
} catch (err) {
|
|
7579
|
-
const message =
|
|
7711
|
+
const message = summarizeError(err);
|
|
7580
7712
|
return { channel: "qqbot", error: message };
|
|
7581
7713
|
}
|
|
7582
7714
|
},
|
|
@@ -7590,12 +7722,14 @@ var qqbotOutbound = {
|
|
|
7590
7722
|
return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId, replyEventId, accountId });
|
|
7591
7723
|
}
|
|
7592
7724
|
const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
7593
|
-
if (!qqCfg
|
|
7725
|
+
if (!resolveQQBotCredentials(qqCfg)) {
|
|
7594
7726
|
return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
|
|
7595
7727
|
}
|
|
7596
7728
|
const target = parseTarget(to);
|
|
7729
|
+
const trimmedText = text?.trim() ? text.trim() : void 0;
|
|
7730
|
+
const sendTextAsFollowup = trimmedText ? shouldSendTextAsFollowupForMedia(mediaUrl) : false;
|
|
7597
7731
|
if (target.kind === "channel") {
|
|
7598
|
-
const fallbackText =
|
|
7732
|
+
const fallbackText = trimmedText ? `${trimmedText}
|
|
7599
7733
|
${mediaUrl}` : mediaUrl;
|
|
7600
7734
|
return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId, replyEventId, accountId });
|
|
7601
7735
|
}
|
|
@@ -7606,6 +7740,7 @@ ${mediaUrl}` : mediaUrl;
|
|
|
7606
7740
|
cfg: qqCfg,
|
|
7607
7741
|
target: { kind: target.kind, id: target.id },
|
|
7608
7742
|
mediaUrl,
|
|
7743
|
+
text: sendTextAsFollowup ? void 0 : trimmedText,
|
|
7609
7744
|
messageId: replyToId
|
|
7610
7745
|
});
|
|
7611
7746
|
} catch (err) {
|
|
@@ -7627,6 +7762,7 @@ ${mediaUrl}` : mediaUrl;
|
|
|
7627
7762
|
cfg: qqCfg,
|
|
7628
7763
|
target: { kind: target.kind, id: target.id },
|
|
7629
7764
|
mediaUrl,
|
|
7765
|
+
text: sendTextAsFollowup ? void 0 : trimmedText,
|
|
7630
7766
|
eventId: replyEventId
|
|
7631
7767
|
});
|
|
7632
7768
|
logEventIdFallback({
|
|
@@ -7652,16 +7788,33 @@ ${mediaUrl}` : mediaUrl;
|
|
|
7652
7788
|
throw retryErr;
|
|
7653
7789
|
}
|
|
7654
7790
|
}
|
|
7791
|
+
if (sendTextAsFollowup && trimmedText) {
|
|
7792
|
+
const textResult = await qqbotOutbound.sendText({
|
|
7793
|
+
cfg,
|
|
7794
|
+
to,
|
|
7795
|
+
text: trimmedText,
|
|
7796
|
+
replyToId,
|
|
7797
|
+
replyEventId,
|
|
7798
|
+
accountId
|
|
7799
|
+
});
|
|
7800
|
+
if (textResult.error) {
|
|
7801
|
+
return {
|
|
7802
|
+
channel: "qqbot",
|
|
7803
|
+
error: `QQBot follow-up text send failed after media delivery: ${textResult.error}`
|
|
7804
|
+
};
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7655
7807
|
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
|
|
7656
7808
|
} catch (err) {
|
|
7657
|
-
const message =
|
|
7809
|
+
const message = summarizeError(err);
|
|
7658
7810
|
return { channel: "qqbot", error: message };
|
|
7659
7811
|
}
|
|
7660
7812
|
},
|
|
7661
7813
|
sendTyping: async (params) => {
|
|
7662
7814
|
const { cfg, to, replyToId, replyEventId, inputSecond, accountId } = params;
|
|
7663
7815
|
const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
7664
|
-
|
|
7816
|
+
const credentials = resolveQQBotCredentials(qqCfg);
|
|
7817
|
+
if (!credentials) {
|
|
7665
7818
|
return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
|
|
7666
7819
|
}
|
|
7667
7820
|
const target = parseTarget(to);
|
|
@@ -7669,7 +7822,7 @@ ${mediaUrl}` : mediaUrl;
|
|
|
7669
7822
|
return { channel: "qqbot" };
|
|
7670
7823
|
}
|
|
7671
7824
|
try {
|
|
7672
|
-
const accessToken = await getAccessToken(
|
|
7825
|
+
const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
|
|
7673
7826
|
try {
|
|
7674
7827
|
await sendC2CInputNotify({
|
|
7675
7828
|
accessToken,
|
|
@@ -7724,7 +7877,7 @@ ${mediaUrl}` : mediaUrl;
|
|
|
7724
7877
|
}
|
|
7725
7878
|
return { channel: "qqbot" };
|
|
7726
7879
|
} catch (err) {
|
|
7727
|
-
const message =
|
|
7880
|
+
const message = summarizeError(err);
|
|
7728
7881
|
return { channel: "qqbot", error: message };
|
|
7729
7882
|
}
|
|
7730
7883
|
}
|
|
@@ -7800,6 +7953,43 @@ function resolveEventId(payload, fallbackEventId) {
|
|
|
7800
7953
|
var VOICE_ASR_FALLBACK_TEXT = "\u5F53\u524D\u8BED\u97F3\u529F\u80FD\u672A\u542F\u52A8\u6216\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
7801
7954
|
var VOICE_EXTENSIONS = [".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex"];
|
|
7802
7955
|
var VOICE_ASR_ERROR_MAX_LENGTH = 500;
|
|
7956
|
+
var LONG_TASK_NOTICE_TEXT = "\u4EFB\u52A1\u5904\u7406\u65F6\u95F4\u8F83\u957F\uFF0C\u8BF7\u7A0D\u7B49\uFF0C\u6211\u8FD8\u5728\u7EE7\u7EED\u5904\u7406\u3002";
|
|
7957
|
+
var DEFAULT_LONG_TASK_NOTICE_DELAY_MS = 3e4;
|
|
7958
|
+
var QQ_GROUP_NO_REPLY_FALLBACK_TEXT = "\u6211\u5728\u3002\u4F60\u53EF\u4EE5\u76F4\u63A5\u8BF4\u5177\u4F53\u4E00\u70B9\u3002";
|
|
7959
|
+
function startLongTaskNoticeTimer(params) {
|
|
7960
|
+
const { delayMs, logger, sendNotice } = params;
|
|
7961
|
+
let completed = false;
|
|
7962
|
+
let timer = null;
|
|
7963
|
+
const clear = () => {
|
|
7964
|
+
if (!timer) return;
|
|
7965
|
+
clearTimeout(timer);
|
|
7966
|
+
timer = null;
|
|
7967
|
+
};
|
|
7968
|
+
if (delayMs > 0) {
|
|
7969
|
+
timer = setTimeout(() => {
|
|
7970
|
+
if (completed) return;
|
|
7971
|
+
completed = true;
|
|
7972
|
+
timer = null;
|
|
7973
|
+
void sendNotice().catch((err) => {
|
|
7974
|
+
logger.warn(`send long-task notice failed: ${String(err)}`);
|
|
7975
|
+
});
|
|
7976
|
+
}, delayMs);
|
|
7977
|
+
timer.unref?.();
|
|
7978
|
+
} else {
|
|
7979
|
+
completed = true;
|
|
7980
|
+
}
|
|
7981
|
+
return {
|
|
7982
|
+
markReplyDelivered: () => {
|
|
7983
|
+
if (completed) return;
|
|
7984
|
+
completed = true;
|
|
7985
|
+
clear();
|
|
7986
|
+
},
|
|
7987
|
+
dispose: () => {
|
|
7988
|
+
completed = true;
|
|
7989
|
+
clear();
|
|
7990
|
+
}
|
|
7991
|
+
};
|
|
7992
|
+
}
|
|
7803
7993
|
function isHttpUrl2(value) {
|
|
7804
7994
|
return /^https?:\/\//i.test(value);
|
|
7805
7995
|
}
|
|
@@ -7865,6 +8055,8 @@ async function resolveInboundAttachmentsForAgent(params) {
|
|
|
7865
8055
|
const maxFileSizeMB = qqCfg.maxFileSizeMB ?? 100;
|
|
7866
8056
|
const maxSize = Math.floor(maxFileSizeMB * 1024 * 1024);
|
|
7867
8057
|
const asrCredentials = resolveQQBotASRCredentials(qqCfg);
|
|
8058
|
+
const inboundMediaDir = resolveInboundMediaDir(qqCfg);
|
|
8059
|
+
const inboundMediaTempDir = resolveInboundMediaTempDir();
|
|
7868
8060
|
const resolved = [];
|
|
7869
8061
|
let hasVoiceAttachment = false;
|
|
7870
8062
|
let hasVoiceTranscript = false;
|
|
@@ -7877,11 +8069,19 @@ async function resolveInboundAttachmentsForAgent(params) {
|
|
|
7877
8069
|
timeout,
|
|
7878
8070
|
maxSize,
|
|
7879
8071
|
sourceFileName: att.filename,
|
|
7880
|
-
tempPrefix: "qqbot-inbound"
|
|
8072
|
+
tempPrefix: "qqbot-inbound",
|
|
8073
|
+
tempDir: inboundMediaTempDir
|
|
8074
|
+
});
|
|
8075
|
+
const finalPath = await finalizeInboundMediaFile({
|
|
8076
|
+
filePath: downloaded.path,
|
|
8077
|
+
tempDir: inboundMediaTempDir,
|
|
8078
|
+
inboundDir: inboundMediaDir
|
|
7881
8079
|
});
|
|
7882
|
-
next.localImagePath =
|
|
7883
|
-
logger.info(`inbound image cached: ${
|
|
7884
|
-
|
|
8080
|
+
next.localImagePath = finalPath;
|
|
8081
|
+
logger.info(`inbound image cached: ${finalPath}`);
|
|
8082
|
+
if (finalPath === downloaded.path) {
|
|
8083
|
+
scheduleTempCleanup(downloaded.path);
|
|
8084
|
+
}
|
|
7885
8085
|
} catch (err) {
|
|
7886
8086
|
logger.warn(`failed to download inbound attachment: ${String(err)}`);
|
|
7887
8087
|
}
|
|
@@ -8090,18 +8290,20 @@ function resolveInbound(eventType, data, fallbackEventId) {
|
|
|
8090
8290
|
}
|
|
8091
8291
|
function resolveChatTarget(event) {
|
|
8092
8292
|
if (event.type === "group") {
|
|
8093
|
-
const group =
|
|
8293
|
+
const group = event.groupOpenid ?? "";
|
|
8294
|
+
const normalizedGroup = group.toLowerCase();
|
|
8094
8295
|
return {
|
|
8095
8296
|
to: `group:${group}`,
|
|
8096
|
-
peerId: `group:${
|
|
8297
|
+
peerId: `group:${normalizedGroup}`,
|
|
8097
8298
|
peerKind: "group"
|
|
8098
8299
|
};
|
|
8099
8300
|
}
|
|
8100
8301
|
if (event.type === "channel") {
|
|
8101
|
-
const channel =
|
|
8302
|
+
const channel = event.channelId ?? "";
|
|
8303
|
+
const normalizedChannel = channel.toLowerCase();
|
|
8102
8304
|
return {
|
|
8103
8305
|
to: `channel:${channel}`,
|
|
8104
|
-
peerId: `channel:${
|
|
8306
|
+
peerId: `channel:${normalizedChannel}`,
|
|
8105
8307
|
peerKind: "group"
|
|
8106
8308
|
};
|
|
8107
8309
|
}
|
|
@@ -8138,7 +8340,7 @@ function extractLocalMediaFromText(params) {
|
|
|
8138
8340
|
parseBarePaths: true,
|
|
8139
8341
|
parseMarkdownLinks: true
|
|
8140
8342
|
});
|
|
8141
|
-
const mediaUrls = result.all.filter((m) => m.isLocal && m.localPath).map((m) => m.localPath);
|
|
8343
|
+
const mediaUrls = result.all.filter((m) => m.isLocal && typeof m.localPath === "string").filter((m) => m.type !== "file").map((m) => m.localPath);
|
|
8142
8344
|
return { text: result.text, mediaUrls };
|
|
8143
8345
|
}
|
|
8144
8346
|
function extractMediaLinesFromText(params) {
|
|
@@ -8162,24 +8364,16 @@ function extractMediaLinesFromText(params) {
|
|
|
8162
8364
|
const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
|
|
8163
8365
|
return { text: result.text, mediaUrls };
|
|
8164
8366
|
}
|
|
8165
|
-
function
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
return text.includes("file_type=4") || text.includes("generic files") || text.includes("not support generic files") || text.includes("\u6682\u4E0D\u652F\u6301\u901A\u7528\u6587\u4EF6");
|
|
8169
|
-
}
|
|
8170
|
-
function buildMediaFallbackText(mediaUrl, errorMessage) {
|
|
8171
|
-
if (isOfficialQQFileSendLimit(errorMessage)) {
|
|
8172
|
-
return [
|
|
8173
|
-
"\u8BF4\u660E\uFF1A\u6839\u636E QQ \u5B98\u65B9\u63A5\u53E3\u89C4\u8303\uFF0C\u5F53\u524D C2C/\u7FA4\u804A\u6682\u4E0D\u652F\u6301\u76F4\u63A5\u53D1\u9001 PDF/\u6587\u6863\u7B49\u901A\u7528\u6587\u4EF6\uFF08file_type=4\uFF09\u3002",
|
|
8174
|
-
"\u8FD9\u5C5E\u4E8E\u5E73\u53F0\u9650\u5236\uFF0C\u4E0D\u662F\u63D2\u4EF6\u7F3A\u9677\uFF1B\u56FE\u7247\u7B49\u5A92\u4F53\u4ECD\u53EF\u6B63\u5E38\u53D1\u9001\u3002",
|
|
8175
|
-
`\u5DF2\u4E3A\u4F60\u9644\u4E0A\u6587\u4EF6\u94FE\u63A5\uFF1A${mediaUrl}`
|
|
8176
|
-
].join("\n");
|
|
8367
|
+
function buildMediaFallbackText(mediaUrl) {
|
|
8368
|
+
if (!/^https?:\/\//i.test(mediaUrl)) {
|
|
8369
|
+
return void 0;
|
|
8177
8370
|
}
|
|
8178
8371
|
return `\u{1F4CE} ${mediaUrl}`;
|
|
8179
8372
|
}
|
|
8180
8373
|
var THINK_BLOCK_RE = /<think\b[^>]*>[\s\S]*?<\/think>/gi;
|
|
8181
8374
|
var FINAL_BLOCK_RE = /<final\b[^>]*>([\s\S]*?)<\/final>/gi;
|
|
8182
8375
|
var RAW_THINK_OR_FINAL_TAG_RE = /<\/?(?:think|final)\b[^>]*>/gi;
|
|
8376
|
+
var FILE_PLACEHOLDER_RE = /\[文件:\s*[^\]\n]+\]/g;
|
|
8183
8377
|
var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_voice|tts(?::text)?|\/tts(?::text)?)\s*\]\]/gi;
|
|
8184
8378
|
var VOICE_EMOTION_TAG_RE = /\[(?:happy|excited|calm|sad|angry|frustrated|softly|whispers|loudly|cheerfully|deadpan|sarcastically|laughs|sighs|chuckles|gasps|pause|slowly|rushed|hesitates|playfully|warmly|gently)\]/gi;
|
|
8185
8379
|
var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
|
|
@@ -8197,6 +8391,7 @@ function sanitizeQQBotOutboundText(rawText) {
|
|
|
8197
8391
|
}
|
|
8198
8392
|
next = next.replace(THINK_BLOCK_RE, "");
|
|
8199
8393
|
next = next.replace(RAW_THINK_OR_FINAL_TAG_RE, "");
|
|
8394
|
+
next = next.replace(FILE_PLACEHOLDER_RE, " ");
|
|
8200
8395
|
next = next.replace(DIRECTIVE_TAG_RE, " ");
|
|
8201
8396
|
next = next.replace(VOICE_EMOTION_TAG_RE, " ");
|
|
8202
8397
|
next = next.replace(/[ \t]+\n/g, "\n");
|
|
@@ -8214,6 +8409,20 @@ function shouldSuppressQQBotTextWhenMediaPresent(rawText, sanitizedText) {
|
|
|
8214
8409
|
if (!sanitizedText) return true;
|
|
8215
8410
|
return !/[A-Za-z0-9\u4e00-\u9fff]/.test(sanitizedText);
|
|
8216
8411
|
}
|
|
8412
|
+
function resolveQQBotNoReplyFallback(params) {
|
|
8413
|
+
const { inbound, replyDelivered } = params;
|
|
8414
|
+
if (replyDelivered) return void 0;
|
|
8415
|
+
if (!inbound.mentionedBot) return void 0;
|
|
8416
|
+
if (inbound.type !== "group" && inbound.type !== "channel") return void 0;
|
|
8417
|
+
const hasVisibleInput = inbound.content.trim().length > 0 || (inbound.attachments?.length ?? 0) > 0;
|
|
8418
|
+
if (!hasVisibleInput) return void 0;
|
|
8419
|
+
return QQ_GROUP_NO_REPLY_FALLBACK_TEXT;
|
|
8420
|
+
}
|
|
8421
|
+
function isQQBotGroupMessageInterfaceBlocked(errorMessage) {
|
|
8422
|
+
const text = (errorMessage ?? "").toLowerCase();
|
|
8423
|
+
if (!text) return false;
|
|
8424
|
+
return text.includes("304103") || text.includes("\u7FA4\u5185\u6D88\u606F\u63A5\u53E3\u88AB\u4E34\u65F6\u5C01\u7981") || text.includes("\u673A\u5668\u4EBA\u5B58\u5728\u5B89\u5168\u98CE\u9669");
|
|
8425
|
+
}
|
|
8217
8426
|
function evaluateReplyFinalOnlyDelivery(params) {
|
|
8218
8427
|
const { replyFinalOnly, kind, hasMedia } = params;
|
|
8219
8428
|
if (!replyFinalOnly || !kind || kind === "final") {
|
|
@@ -8225,7 +8434,7 @@ function evaluateReplyFinalOnlyDelivery(params) {
|
|
|
8225
8434
|
return { skipDelivery: true, suppressText: false };
|
|
8226
8435
|
}
|
|
8227
8436
|
async function sendQQBotMediaWithFallback(params) {
|
|
8228
|
-
const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger } = params;
|
|
8437
|
+
const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger, onDelivered, onError } = params;
|
|
8229
8438
|
const outbound = params.outbound ?? qqbotOutbound;
|
|
8230
8439
|
for (const mediaUrl of mediaQueue) {
|
|
8231
8440
|
const result = await outbound.sendMedia({
|
|
@@ -8237,7 +8446,11 @@ async function sendQQBotMediaWithFallback(params) {
|
|
|
8237
8446
|
});
|
|
8238
8447
|
if (result.error) {
|
|
8239
8448
|
logger.error(`sendMedia failed: ${result.error}`);
|
|
8240
|
-
|
|
8449
|
+
onError?.(result.error);
|
|
8450
|
+
const fallback = buildMediaFallbackText(mediaUrl);
|
|
8451
|
+
if (!fallback) {
|
|
8452
|
+
continue;
|
|
8453
|
+
}
|
|
8241
8454
|
const fallbackResult = await outbound.sendText({
|
|
8242
8455
|
cfg: { channels: { qqbot: qqCfg } },
|
|
8243
8456
|
to,
|
|
@@ -8247,7 +8460,12 @@ async function sendQQBotMediaWithFallback(params) {
|
|
|
8247
8460
|
});
|
|
8248
8461
|
if (fallbackResult.error) {
|
|
8249
8462
|
logger.error(`sendText fallback failed: ${fallbackResult.error}`);
|
|
8463
|
+
onError?.(fallbackResult.error);
|
|
8464
|
+
} else {
|
|
8465
|
+
onDelivered?.();
|
|
8250
8466
|
}
|
|
8467
|
+
} else {
|
|
8468
|
+
onDelivered?.();
|
|
8251
8469
|
}
|
|
8252
8470
|
}
|
|
8253
8471
|
}
|
|
@@ -8312,237 +8530,319 @@ async function dispatchToAgent(params) {
|
|
|
8312
8530
|
logger.warn("reply API not available");
|
|
8313
8531
|
return;
|
|
8314
8532
|
}
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
const
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
logger
|
|
8326
|
-
});
|
|
8327
|
-
if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
|
|
8328
|
-
const fallback = await qqbotOutbound.sendText({
|
|
8329
|
-
cfg: { channels: { qqbot: qqCfg } },
|
|
8330
|
-
to: target.to,
|
|
8331
|
-
text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
|
|
8332
|
-
replyToId: inbound.messageId,
|
|
8333
|
-
replyEventId: inbound.eventId
|
|
8334
|
-
});
|
|
8335
|
-
if (fallback.error) {
|
|
8336
|
-
logger.error(`sendText ASR fallback failed: ${fallback.error}`);
|
|
8533
|
+
let replyDelivered = false;
|
|
8534
|
+
let groupMessageInterfaceBlocked = false;
|
|
8535
|
+
const markReplyDelivered = () => {
|
|
8536
|
+
replyDelivered = true;
|
|
8537
|
+
longTaskNotice.markReplyDelivered();
|
|
8538
|
+
};
|
|
8539
|
+
const markGroupMessageInterfaceBlocked = (error) => {
|
|
8540
|
+
if (!isQQBotGroupMessageInterfaceBlocked(error)) return;
|
|
8541
|
+
if (!groupMessageInterfaceBlocked) {
|
|
8542
|
+
logger.warn("QQ group message interface is temporarily blocked by platform; suppressing extra sends");
|
|
8337
8543
|
}
|
|
8338
|
-
|
|
8339
|
-
}
|
|
8340
|
-
const
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
channel: "QQ",
|
|
8352
|
-
from: envelopeFrom,
|
|
8353
|
-
body: rawBody,
|
|
8354
|
-
timestamp: inbound.timestamp,
|
|
8355
|
-
previousTimestamp: previousTimestamp ?? void 0,
|
|
8356
|
-
chatType: inbound.type === "direct" ? "direct" : "group",
|
|
8357
|
-
senderLabel: inbound.senderName ?? inbound.senderId,
|
|
8358
|
-
sender: { id: inbound.senderId, name: inbound.senderName ?? void 0 },
|
|
8359
|
-
envelope: envelopeOptions
|
|
8360
|
-
}) : replyApi.formatAgentEnvelope ? replyApi.formatAgentEnvelope({
|
|
8361
|
-
channel: "QQ",
|
|
8362
|
-
from: envelopeFrom,
|
|
8363
|
-
timestamp: inbound.timestamp,
|
|
8364
|
-
previousTimestamp: previousTimestamp ?? void 0,
|
|
8365
|
-
envelope: envelopeOptions,
|
|
8366
|
-
body: rawBody
|
|
8367
|
-
}) : rawBody;
|
|
8368
|
-
const inboundCtx = buildInboundContext({
|
|
8369
|
-
event: inbound,
|
|
8370
|
-
sessionKey: route.sessionKey,
|
|
8371
|
-
accountId: route.accountId ?? accountId,
|
|
8372
|
-
body: inboundBody,
|
|
8373
|
-
rawBody,
|
|
8374
|
-
commandBody: rawBody
|
|
8375
|
-
});
|
|
8376
|
-
const finalizeInboundContext = replyApi?.finalizeInboundContext;
|
|
8377
|
-
const finalCtx = finalizeInboundContext ? finalizeInboundContext(inboundCtx) : inboundCtx;
|
|
8378
|
-
let cronBase = "";
|
|
8379
|
-
if (typeof finalCtx.RawBody === "string" && finalCtx.RawBody) {
|
|
8380
|
-
cronBase = finalCtx.RawBody;
|
|
8381
|
-
} else if (typeof finalCtx.Body === "string" && finalCtx.Body) {
|
|
8382
|
-
cronBase = finalCtx.Body;
|
|
8383
|
-
} else if (typeof finalCtx.CommandBody === "string" && finalCtx.CommandBody) {
|
|
8384
|
-
cronBase = finalCtx.CommandBody;
|
|
8385
|
-
}
|
|
8386
|
-
if (cronBase) {
|
|
8387
|
-
const nextCron = appendCronHiddenPrompt(cronBase);
|
|
8388
|
-
if (nextCron !== cronBase) {
|
|
8389
|
-
finalCtx.BodyForAgent = nextCron;
|
|
8390
|
-
}
|
|
8391
|
-
}
|
|
8392
|
-
if (storePath && sessionApi?.recordInboundSession) {
|
|
8393
|
-
try {
|
|
8394
|
-
const mainSessionKeyRaw = route?.mainSessionKey;
|
|
8395
|
-
const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw : void 0;
|
|
8396
|
-
const isGroup = inbound.type === "group" || inbound.type === "channel";
|
|
8397
|
-
const updateLastRoute = !isGroup ? {
|
|
8398
|
-
sessionKey: mainSessionKey ?? route.sessionKey,
|
|
8399
|
-
channel: "qqbot",
|
|
8400
|
-
to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${inbound.senderId}`,
|
|
8401
|
-
accountId: route.accountId ?? accountId
|
|
8402
|
-
} : void 0;
|
|
8403
|
-
const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : route.sessionKey;
|
|
8404
|
-
await sessionApi.recordInboundSession({
|
|
8405
|
-
storePath,
|
|
8406
|
-
sessionKey: recordSessionKey,
|
|
8407
|
-
ctx: finalCtx,
|
|
8408
|
-
updateLastRoute,
|
|
8409
|
-
onRecordError: (err) => {
|
|
8410
|
-
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
8411
|
-
}
|
|
8544
|
+
groupMessageInterfaceBlocked = true;
|
|
8545
|
+
};
|
|
8546
|
+
const longTaskNotice = startLongTaskNoticeTimer({
|
|
8547
|
+
delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
|
|
8548
|
+
logger,
|
|
8549
|
+
sendNotice: async () => {
|
|
8550
|
+
if (groupMessageInterfaceBlocked) return;
|
|
8551
|
+
const result = await qqbotOutbound.sendText({
|
|
8552
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
8553
|
+
to: target.to,
|
|
8554
|
+
text: LONG_TASK_NOTICE_TEXT,
|
|
8555
|
+
replyToId: inbound.messageId,
|
|
8556
|
+
replyEventId: inbound.eventId
|
|
8412
8557
|
});
|
|
8413
|
-
|
|
8414
|
-
|
|
8558
|
+
if (result.error) {
|
|
8559
|
+
logger.warn(`send long-task notice failed: ${result.error}`);
|
|
8560
|
+
markGroupMessageInterfaceBlocked(result.error);
|
|
8561
|
+
} else {
|
|
8562
|
+
replyDelivered = true;
|
|
8563
|
+
}
|
|
8415
8564
|
}
|
|
8416
|
-
}
|
|
8417
|
-
const textApi = runtime2.channel?.text;
|
|
8418
|
-
const limit = textApi?.resolveTextChunkLimit?.({
|
|
8419
|
-
cfg,
|
|
8420
|
-
channel: "qqbot",
|
|
8421
|
-
defaultLimit: qqCfg.textChunkLimit ?? 1500
|
|
8422
|
-
}) ?? (qqCfg.textChunkLimit ?? 1500);
|
|
8423
|
-
const chunkMode = textApi?.resolveChunkMode?.(cfg, "qqbot");
|
|
8424
|
-
const tableMode = textApi?.resolveMarkdownTableMode?.({
|
|
8425
|
-
cfg,
|
|
8426
|
-
channel: "qqbot",
|
|
8427
|
-
accountId: route.accountId ?? accountId
|
|
8428
8565
|
});
|
|
8429
|
-
const
|
|
8430
|
-
const
|
|
8431
|
-
|
|
8432
|
-
|
|
8566
|
+
const inboundMediaDir = resolveInboundMediaDir(qqCfg);
|
|
8567
|
+
const inboundMediaKeepDays = resolveInboundMediaKeepDays(qqCfg);
|
|
8568
|
+
try {
|
|
8569
|
+
const sessionApi = runtime2.channel?.session;
|
|
8570
|
+
const sessionConfig = cfg?.session;
|
|
8571
|
+
const storePath = sessionApi?.resolveStorePath?.(
|
|
8572
|
+
sessionConfig?.store,
|
|
8573
|
+
{ agentId: route.agentId }
|
|
8574
|
+
);
|
|
8575
|
+
const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
|
|
8576
|
+
const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
|
|
8577
|
+
const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
|
|
8578
|
+
attachments: inbound.attachments,
|
|
8579
|
+
qqCfg,
|
|
8580
|
+
logger
|
|
8581
|
+
});
|
|
8582
|
+
if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
|
|
8583
|
+
const fallback = await qqbotOutbound.sendText({
|
|
8584
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
8585
|
+
to: target.to,
|
|
8586
|
+
text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
|
|
8587
|
+
replyToId: inbound.messageId,
|
|
8588
|
+
replyEventId: inbound.eventId
|
|
8589
|
+
});
|
|
8590
|
+
if (fallback.error) {
|
|
8591
|
+
logger.error(`sendText ASR fallback failed: ${fallback.error}`);
|
|
8592
|
+
markGroupMessageInterfaceBlocked(fallback.error);
|
|
8593
|
+
} else {
|
|
8594
|
+
replyDelivered = true;
|
|
8595
|
+
}
|
|
8596
|
+
return;
|
|
8433
8597
|
}
|
|
8434
|
-
|
|
8435
|
-
|
|
8598
|
+
const resolvedAttachments = resolvedAttachmentResult.attachments;
|
|
8599
|
+
const localImageCount = resolvedAttachments.filter((item) => Boolean(item.localImagePath)).length;
|
|
8600
|
+
if (localImageCount > 0) {
|
|
8601
|
+
logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
|
|
8436
8602
|
}
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
const deliver = async (payload, info) => {
|
|
8441
|
-
const typed = payload;
|
|
8442
|
-
const mediaLineResult = extractMediaLinesFromText({
|
|
8443
|
-
text: typed?.text ?? "",
|
|
8444
|
-
logger
|
|
8603
|
+
const rawBody = buildInboundContentWithAttachments({
|
|
8604
|
+
content: inbound.content,
|
|
8605
|
+
attachments: resolvedAttachments
|
|
8445
8606
|
});
|
|
8446
|
-
const
|
|
8447
|
-
|
|
8448
|
-
|
|
8607
|
+
const envelopeFrom = resolveEnvelopeFrom(inbound);
|
|
8608
|
+
const inboundBody = replyApi.formatInboundEnvelope ? replyApi.formatInboundEnvelope({
|
|
8609
|
+
channel: "QQ",
|
|
8610
|
+
from: envelopeFrom,
|
|
8611
|
+
body: rawBody,
|
|
8612
|
+
timestamp: inbound.timestamp,
|
|
8613
|
+
previousTimestamp: previousTimestamp ?? void 0,
|
|
8614
|
+
chatType: inbound.type === "direct" ? "direct" : "group",
|
|
8615
|
+
senderLabel: inbound.senderName ?? inbound.senderId,
|
|
8616
|
+
sender: { id: inbound.senderId, name: inbound.senderName ?? void 0 },
|
|
8617
|
+
envelope: envelopeOptions
|
|
8618
|
+
}) : replyApi.formatAgentEnvelope ? replyApi.formatAgentEnvelope({
|
|
8619
|
+
channel: "QQ",
|
|
8620
|
+
from: envelopeFrom,
|
|
8621
|
+
timestamp: inbound.timestamp,
|
|
8622
|
+
previousTimestamp: previousTimestamp ?? void 0,
|
|
8623
|
+
envelope: envelopeOptions,
|
|
8624
|
+
body: rawBody
|
|
8625
|
+
}) : rawBody;
|
|
8626
|
+
const inboundCtx = buildInboundContext({
|
|
8627
|
+
event: inbound,
|
|
8628
|
+
sessionKey: route.sessionKey,
|
|
8629
|
+
accountId: route.accountId ?? accountId,
|
|
8630
|
+
body: inboundBody,
|
|
8631
|
+
rawBody,
|
|
8632
|
+
commandBody: rawBody
|
|
8449
8633
|
});
|
|
8450
|
-
const
|
|
8451
|
-
const
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8634
|
+
const finalizeInboundContext = replyApi?.finalizeInboundContext;
|
|
8635
|
+
const finalCtx = finalizeInboundContext ? finalizeInboundContext(inboundCtx) : inboundCtx;
|
|
8636
|
+
let cronBase = "";
|
|
8637
|
+
if (typeof finalCtx.RawBody === "string" && finalCtx.RawBody) {
|
|
8638
|
+
cronBase = finalCtx.RawBody;
|
|
8639
|
+
} else if (typeof finalCtx.Body === "string" && finalCtx.Body) {
|
|
8640
|
+
cronBase = finalCtx.Body;
|
|
8641
|
+
} else if (typeof finalCtx.CommandBody === "string" && finalCtx.CommandBody) {
|
|
8642
|
+
cronBase = finalCtx.CommandBody;
|
|
8643
|
+
}
|
|
8644
|
+
if (cronBase) {
|
|
8645
|
+
const nextCron = appendCronHiddenPrompt(cronBase);
|
|
8646
|
+
if (nextCron !== cronBase) {
|
|
8647
|
+
finalCtx.BodyForAgent = nextCron;
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8650
|
+
if (storePath && sessionApi?.recordInboundSession) {
|
|
8651
|
+
try {
|
|
8652
|
+
const mainSessionKeyRaw = route?.mainSessionKey;
|
|
8653
|
+
const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw : void 0;
|
|
8654
|
+
const isGroup = inbound.type === "group" || inbound.type === "channel";
|
|
8655
|
+
const updateLastRoute = !isGroup ? {
|
|
8656
|
+
sessionKey: mainSessionKey ?? route.sessionKey,
|
|
8657
|
+
channel: "qqbot",
|
|
8658
|
+
to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${inbound.senderId}`,
|
|
8659
|
+
accountId: route.accountId ?? accountId
|
|
8660
|
+
} : void 0;
|
|
8661
|
+
const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : route.sessionKey;
|
|
8662
|
+
await sessionApi.recordInboundSession({
|
|
8663
|
+
storePath,
|
|
8664
|
+
sessionKey: recordSessionKey,
|
|
8665
|
+
ctx: finalCtx,
|
|
8666
|
+
updateLastRoute,
|
|
8667
|
+
onRecordError: (err) => {
|
|
8668
|
+
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
8669
|
+
}
|
|
8482
8670
|
});
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
}
|
|
8671
|
+
} catch (err) {
|
|
8672
|
+
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
8486
8673
|
}
|
|
8487
8674
|
}
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
};
|
|
8497
|
-
const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
|
|
8498
|
-
const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
|
|
8499
|
-
if (dispatchBuffered) {
|
|
8500
|
-
await dispatchBuffered({
|
|
8501
|
-
ctx: finalCtx,
|
|
8675
|
+
const textApi = runtime2.channel?.text;
|
|
8676
|
+
const limit = textApi?.resolveTextChunkLimit?.({
|
|
8677
|
+
cfg,
|
|
8678
|
+
channel: "qqbot",
|
|
8679
|
+
defaultLimit: qqCfg.textChunkLimit ?? 1500
|
|
8680
|
+
}) ?? (qqCfg.textChunkLimit ?? 1500);
|
|
8681
|
+
const chunkMode = textApi?.resolveChunkMode?.(cfg, "qqbot");
|
|
8682
|
+
const tableMode = textApi?.resolveMarkdownTableMode?.({
|
|
8502
8683
|
cfg,
|
|
8503
|
-
|
|
8684
|
+
channel: "qqbot",
|
|
8685
|
+
accountId: route.accountId ?? accountId
|
|
8686
|
+
});
|
|
8687
|
+
const resolvedTableMode = tableMode ?? "bullets";
|
|
8688
|
+
const chunkText = (text) => {
|
|
8689
|
+
if (textApi?.chunkMarkdownText && limit > 0) {
|
|
8690
|
+
return textApi.chunkMarkdownText(text, limit);
|
|
8691
|
+
}
|
|
8692
|
+
if (textApi?.chunkTextWithMode && limit > 0) {
|
|
8693
|
+
return textApi.chunkTextWithMode(text, limit, chunkMode);
|
|
8694
|
+
}
|
|
8695
|
+
return [text];
|
|
8696
|
+
};
|
|
8697
|
+
const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
|
|
8698
|
+
const deliver = async (payload, info) => {
|
|
8699
|
+
const typed = payload;
|
|
8700
|
+
const mediaLineResult = extractMediaLinesFromText({
|
|
8701
|
+
text: typed?.text ?? "",
|
|
8702
|
+
logger
|
|
8703
|
+
});
|
|
8704
|
+
const localMediaResult = extractLocalMediaFromText({
|
|
8705
|
+
text: mediaLineResult.text,
|
|
8706
|
+
logger
|
|
8707
|
+
});
|
|
8708
|
+
const cleanedText = sanitizeQQBotOutboundText(localMediaResult.text);
|
|
8709
|
+
const payloadMediaUrls = Array.isArray(typed?.mediaUrls) ? typed?.mediaUrls : typed?.mediaUrl ? [typed.mediaUrl] : [];
|
|
8710
|
+
const mediaQueue = [];
|
|
8711
|
+
const seenMedia = /* @__PURE__ */ new Set();
|
|
8712
|
+
const addMedia = (value) => {
|
|
8713
|
+
const next = value?.trim();
|
|
8714
|
+
if (!next) return;
|
|
8715
|
+
if (seenMedia.has(next)) return;
|
|
8716
|
+
seenMedia.add(next);
|
|
8717
|
+
mediaQueue.push(next);
|
|
8718
|
+
};
|
|
8719
|
+
for (const url of payloadMediaUrls) addMedia(url);
|
|
8720
|
+
for (const url of mediaLineResult.mediaUrls) addMedia(url);
|
|
8721
|
+
for (const url of localMediaResult.mediaUrls) addMedia(url);
|
|
8722
|
+
const deliveryDecision = evaluateReplyFinalOnlyDelivery({
|
|
8723
|
+
replyFinalOnly,
|
|
8724
|
+
kind: info?.kind,
|
|
8725
|
+
hasMedia: mediaQueue.length > 0,
|
|
8726
|
+
sanitizedText: cleanedText
|
|
8727
|
+
});
|
|
8728
|
+
if (deliveryDecision.skipDelivery) return;
|
|
8729
|
+
const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(localMediaResult.text, cleanedText);
|
|
8730
|
+
const suppressText = deliveryDecision.suppressText || suppressEchoText;
|
|
8731
|
+
const textToSend = suppressText ? "" : cleanedText;
|
|
8732
|
+
if (textToSend) {
|
|
8733
|
+
const converted = textApi?.convertMarkdownTables ? textApi.convertMarkdownTables(textToSend, resolvedTableMode) : textToSend;
|
|
8734
|
+
const chunks = chunkText(converted);
|
|
8735
|
+
for (const chunk of chunks) {
|
|
8736
|
+
const result = await qqbotOutbound.sendText({
|
|
8737
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
8738
|
+
to: target.to,
|
|
8739
|
+
text: chunk,
|
|
8740
|
+
replyToId: inbound.messageId,
|
|
8741
|
+
replyEventId: inbound.eventId
|
|
8742
|
+
});
|
|
8743
|
+
if (result.error) {
|
|
8744
|
+
logger.error(`sendText failed: ${result.error}`);
|
|
8745
|
+
markGroupMessageInterfaceBlocked(result.error);
|
|
8746
|
+
} else {
|
|
8747
|
+
markReplyDelivered();
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
}
|
|
8751
|
+
await sendQQBotMediaWithFallback({
|
|
8752
|
+
qqCfg,
|
|
8753
|
+
to: target.to,
|
|
8754
|
+
mediaQueue,
|
|
8755
|
+
replyToId: inbound.messageId,
|
|
8756
|
+
replyEventId: inbound.eventId,
|
|
8757
|
+
logger,
|
|
8758
|
+
onDelivered: () => {
|
|
8759
|
+
markReplyDelivered();
|
|
8760
|
+
},
|
|
8761
|
+
onError: (error) => {
|
|
8762
|
+
markGroupMessageInterfaceBlocked(error);
|
|
8763
|
+
}
|
|
8764
|
+
});
|
|
8765
|
+
};
|
|
8766
|
+
const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
|
|
8767
|
+
const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
|
|
8768
|
+
if (dispatchBuffered) {
|
|
8769
|
+
await dispatchBuffered({
|
|
8770
|
+
ctx: finalCtx,
|
|
8771
|
+
cfg,
|
|
8772
|
+
dispatcherOptions: {
|
|
8773
|
+
deliver,
|
|
8774
|
+
humanDelay,
|
|
8775
|
+
onError: (err, info) => {
|
|
8776
|
+
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
8777
|
+
},
|
|
8778
|
+
onSkip: (_payload, info) => {
|
|
8779
|
+
if (info.reason !== "silent") {
|
|
8780
|
+
logger.info(`reply skipped: ${info.reason}`);
|
|
8781
|
+
}
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
});
|
|
8785
|
+
} else {
|
|
8786
|
+
const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
|
|
8504
8787
|
deliver,
|
|
8505
8788
|
humanDelay,
|
|
8506
8789
|
onError: (err, info) => {
|
|
8507
8790
|
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
8508
|
-
},
|
|
8509
|
-
onSkip: (_payload, info) => {
|
|
8510
|
-
if (info.reason !== "silent") {
|
|
8511
|
-
logger.info(`reply skipped: ${info.reason}`);
|
|
8512
|
-
}
|
|
8513
8791
|
}
|
|
8792
|
+
}) : {
|
|
8793
|
+
dispatcher: replyApi.createReplyDispatcher?.({
|
|
8794
|
+
deliver,
|
|
8795
|
+
humanDelay,
|
|
8796
|
+
onError: (err, info) => {
|
|
8797
|
+
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
8798
|
+
}
|
|
8799
|
+
}),
|
|
8800
|
+
replyOptions: {},
|
|
8801
|
+
markDispatchIdle: () => void 0
|
|
8802
|
+
};
|
|
8803
|
+
if (!dispatcherResult.dispatcher || !replyApi.dispatchReplyFromConfig) {
|
|
8804
|
+
logger.warn("dispatcher not available, skipping reply");
|
|
8805
|
+
return;
|
|
8514
8806
|
}
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
8807
|
+
await replyApi.dispatchReplyFromConfig({
|
|
8808
|
+
ctx: finalCtx,
|
|
8809
|
+
cfg,
|
|
8810
|
+
dispatcher: dispatcherResult.dispatcher,
|
|
8811
|
+
replyOptions: dispatcherResult.replyOptions
|
|
8812
|
+
});
|
|
8813
|
+
dispatcherResult.markDispatchIdle?.();
|
|
8523
8814
|
}
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8815
|
+
const noReplyFallback = resolveQQBotNoReplyFallback({
|
|
8816
|
+
inbound,
|
|
8817
|
+
replyDelivered
|
|
8818
|
+
});
|
|
8819
|
+
if (noReplyFallback && !groupMessageInterfaceBlocked) {
|
|
8820
|
+
logger.info("no visible reply generated for group mention; sending fallback text");
|
|
8821
|
+
const fallbackResult = await qqbotOutbound.sendText({
|
|
8822
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
8823
|
+
to: target.to,
|
|
8824
|
+
text: noReplyFallback,
|
|
8825
|
+
replyToId: inbound.messageId,
|
|
8826
|
+
replyEventId: inbound.eventId
|
|
8827
|
+
});
|
|
8828
|
+
if (fallbackResult.error) {
|
|
8829
|
+
logger.error(`sendText no-reply fallback failed: ${fallbackResult.error}`);
|
|
8830
|
+
markGroupMessageInterfaceBlocked(fallbackResult.error);
|
|
8831
|
+
} else {
|
|
8832
|
+
markReplyDelivered();
|
|
8530
8833
|
}
|
|
8531
|
-
}
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8834
|
+
}
|
|
8835
|
+
} finally {
|
|
8836
|
+
longTaskNotice.dispose();
|
|
8837
|
+
try {
|
|
8838
|
+
await pruneInboundMediaDir({
|
|
8839
|
+
inboundDir: inboundMediaDir,
|
|
8840
|
+
keepDays: inboundMediaKeepDays
|
|
8841
|
+
});
|
|
8842
|
+
} catch (err) {
|
|
8843
|
+
logger.warn(`failed to prune qqbot inbound media dir: ${String(err)}`);
|
|
8844
|
+
}
|
|
8538
8845
|
}
|
|
8539
|
-
await replyApi.dispatchReplyFromConfig({
|
|
8540
|
-
ctx: finalCtx,
|
|
8541
|
-
cfg,
|
|
8542
|
-
dispatcher: dispatcherResult.dispatcher,
|
|
8543
|
-
replyOptions: dispatcherResult.replyOptions
|
|
8544
|
-
});
|
|
8545
|
-
dispatcherResult.markDispatchIdle?.();
|
|
8546
8846
|
}
|
|
8547
8847
|
function shouldHandleMessage(event, qqCfg, logger) {
|
|
8548
8848
|
if (event.type === "direct") {
|
|
@@ -8676,7 +8976,7 @@ function cleanupSocket(conn) {
|
|
|
8676
8976
|
}
|
|
8677
8977
|
}
|
|
8678
8978
|
async function monitorQQBotProvider(opts = {}) {
|
|
8679
|
-
const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID } = opts;
|
|
8979
|
+
const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID, setStatus } = opts;
|
|
8680
8980
|
const logger = createLogger("qqbot", {
|
|
8681
8981
|
log: runtime2?.log,
|
|
8682
8982
|
error: runtime2?.error
|
|
@@ -8783,6 +9083,7 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
8783
9083
|
return;
|
|
8784
9084
|
}
|
|
8785
9085
|
case 11:
|
|
9086
|
+
setStatus?.({ lastEventAt: Date.now() });
|
|
8786
9087
|
return;
|
|
8787
9088
|
case 7:
|
|
8788
9089
|
cleanupSocket(conn);
|
|
@@ -9008,8 +9309,17 @@ var qqbotPlugin = {
|
|
|
9008
9309
|
historyLimit: { type: "integer", minimum: 0 },
|
|
9009
9310
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
9010
9311
|
replyFinalOnly: { type: "boolean" },
|
|
9312
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
9011
9313
|
maxFileSizeMB: { type: "number" },
|
|
9012
9314
|
mediaTimeoutMs: { type: "number" },
|
|
9315
|
+
inboundMedia: {
|
|
9316
|
+
type: "object",
|
|
9317
|
+
additionalProperties: false,
|
|
9318
|
+
properties: {
|
|
9319
|
+
dir: { type: "string" },
|
|
9320
|
+
keepDays: { type: "number", minimum: 0 }
|
|
9321
|
+
}
|
|
9322
|
+
},
|
|
9013
9323
|
accounts: {
|
|
9014
9324
|
type: "object",
|
|
9015
9325
|
additionalProperties: {
|
|
@@ -9039,8 +9349,17 @@ var qqbotPlugin = {
|
|
|
9039
9349
|
historyLimit: { type: "integer", minimum: 0 },
|
|
9040
9350
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
9041
9351
|
replyFinalOnly: { type: "boolean" },
|
|
9352
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
9042
9353
|
maxFileSizeMB: { type: "number" },
|
|
9043
|
-
mediaTimeoutMs: { type: "number" }
|
|
9354
|
+
mediaTimeoutMs: { type: "number" },
|
|
9355
|
+
inboundMedia: {
|
|
9356
|
+
type: "object",
|
|
9357
|
+
additionalProperties: false,
|
|
9358
|
+
properties: {
|
|
9359
|
+
dir: { type: "string" },
|
|
9360
|
+
keepDays: { type: "number", minimum: 0 }
|
|
9361
|
+
}
|
|
9362
|
+
}
|
|
9044
9363
|
}
|
|
9045
9364
|
}
|
|
9046
9365
|
}
|
|
@@ -9182,7 +9501,8 @@ var qqbotPlugin = {
|
|
|
9182
9501
|
error: ctx.log?.error ?? console.error
|
|
9183
9502
|
},
|
|
9184
9503
|
abortSignal: ctx.abortSignal,
|
|
9185
|
-
accountId: ctx.accountId
|
|
9504
|
+
accountId: ctx.accountId,
|
|
9505
|
+
setStatus: ctx.setStatus
|
|
9186
9506
|
});
|
|
9187
9507
|
},
|
|
9188
9508
|
stopAccount: async (ctx) => {
|
|
@@ -9225,8 +9545,17 @@ var plugin = {
|
|
|
9225
9545
|
historyLimit: { type: "integer", minimum: 0 },
|
|
9226
9546
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
9227
9547
|
replyFinalOnly: { type: "boolean" },
|
|
9548
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
9228
9549
|
maxFileSizeMB: { type: "number" },
|
|
9229
9550
|
mediaTimeoutMs: { type: "number" },
|
|
9551
|
+
inboundMedia: {
|
|
9552
|
+
type: "object",
|
|
9553
|
+
additionalProperties: false,
|
|
9554
|
+
properties: {
|
|
9555
|
+
dir: { type: "string" },
|
|
9556
|
+
keepDays: { type: "number", minimum: 0 }
|
|
9557
|
+
}
|
|
9558
|
+
},
|
|
9230
9559
|
accounts: {
|
|
9231
9560
|
type: "object",
|
|
9232
9561
|
additionalProperties: {
|
|
@@ -9256,8 +9585,17 @@ var plugin = {
|
|
|
9256
9585
|
historyLimit: { type: "integer", minimum: 0 },
|
|
9257
9586
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
9258
9587
|
replyFinalOnly: { type: "boolean" },
|
|
9588
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
9259
9589
|
maxFileSizeMB: { type: "number" },
|
|
9260
|
-
mediaTimeoutMs: { type: "number" }
|
|
9590
|
+
mediaTimeoutMs: { type: "number" },
|
|
9591
|
+
inboundMedia: {
|
|
9592
|
+
type: "object",
|
|
9593
|
+
additionalProperties: false,
|
|
9594
|
+
properties: {
|
|
9595
|
+
dir: { type: "string" },
|
|
9596
|
+
keepDays: { type: "number", minimum: 0 }
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9261
9599
|
}
|
|
9262
9600
|
}
|
|
9263
9601
|
}
|