@openclaw-china/qqbot 2026.3.4-2 → 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 +154 -10
- package/dist/index.js +1032 -395
- 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';
|
|
@@ -4213,12 +4213,13 @@ var coerce = {
|
|
|
4213
4213
|
var NEVER = INVALID;
|
|
4214
4214
|
|
|
4215
4215
|
// src/config.ts
|
|
4216
|
+
function toTrimmedString(value) {
|
|
4217
|
+
if (value === void 0 || value === null) return void 0;
|
|
4218
|
+
const next = String(value).trim();
|
|
4219
|
+
return next ? next : void 0;
|
|
4220
|
+
}
|
|
4216
4221
|
var optionalCoercedString = external_exports.preprocess(
|
|
4217
|
-
(value) =>
|
|
4218
|
-
if (value === void 0 || value === null) return void 0;
|
|
4219
|
-
const next = String(value).trim();
|
|
4220
|
-
return next;
|
|
4221
|
-
},
|
|
4222
|
+
(value) => toTrimmedString(value),
|
|
4222
4223
|
external_exports.string().min(1).optional()
|
|
4223
4224
|
);
|
|
4224
4225
|
var QQBotAccountSchema = external_exports.object({
|
|
@@ -4241,13 +4242,31 @@ var QQBotAccountSchema = external_exports.object({
|
|
|
4241
4242
|
historyLimit: external_exports.number().int().min(0).optional().default(10),
|
|
4242
4243
|
textChunkLimit: external_exports.number().int().positive().optional().default(1500),
|
|
4243
4244
|
replyFinalOnly: external_exports.boolean().optional().default(false),
|
|
4245
|
+
longTaskNoticeDelayMs: external_exports.number().int().min(0).optional().default(3e4),
|
|
4244
4246
|
maxFileSizeMB: external_exports.number().positive().optional().default(100),
|
|
4245
|
-
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()
|
|
4246
4252
|
});
|
|
4247
4253
|
QQBotAccountSchema.extend({
|
|
4248
4254
|
defaultAccount: external_exports.string().optional(),
|
|
4249
4255
|
accounts: external_exports.record(QQBotAccountSchema).optional()
|
|
4250
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
|
+
}
|
|
4251
4270
|
var DEFAULT_ACCOUNT_ID = "default";
|
|
4252
4271
|
function listConfiguredAccountIds(cfg) {
|
|
4253
4272
|
const accounts = cfg.channels?.qqbot?.accounts;
|
|
@@ -4278,17 +4297,22 @@ function mergeQQBotAccountConfig(cfg, accountId) {
|
|
|
4278
4297
|
return { ...baseConfig, ...account };
|
|
4279
4298
|
}
|
|
4280
4299
|
function resolveQQBotCredentials(config) {
|
|
4281
|
-
|
|
4282
|
-
|
|
4300
|
+
const appId = toTrimmedString(config?.appId);
|
|
4301
|
+
const clientSecret = toTrimmedString(config?.clientSecret);
|
|
4302
|
+
if (!appId || !clientSecret) return void 0;
|
|
4303
|
+
return { appId, clientSecret };
|
|
4283
4304
|
}
|
|
4284
4305
|
function resolveQQBotASRCredentials(config) {
|
|
4285
4306
|
const asr = config?.asr;
|
|
4286
4307
|
if (!asr?.enabled) return void 0;
|
|
4287
|
-
|
|
4308
|
+
const appId = toTrimmedString(asr.appId);
|
|
4309
|
+
const secretId = toTrimmedString(asr.secretId);
|
|
4310
|
+
const secretKey = toTrimmedString(asr.secretKey);
|
|
4311
|
+
if (!appId || !secretId || !secretKey) return void 0;
|
|
4288
4312
|
return {
|
|
4289
|
-
appId
|
|
4290
|
-
secretId
|
|
4291
|
-
secretKey
|
|
4313
|
+
appId,
|
|
4314
|
+
secretId,
|
|
4315
|
+
secretKey
|
|
4292
4316
|
};
|
|
4293
4317
|
}
|
|
4294
4318
|
|
|
@@ -4633,10 +4657,10 @@ function normalizeLocalPath(raw) {
|
|
|
4633
4657
|
} catch {
|
|
4634
4658
|
}
|
|
4635
4659
|
if (p.startsWith("~/") || p === "~") {
|
|
4636
|
-
p =
|
|
4660
|
+
p = path2.join(os.homedir(), p.slice(1));
|
|
4637
4661
|
} else if (p.startsWith("~")) ;
|
|
4638
|
-
if (!
|
|
4639
|
-
p =
|
|
4662
|
+
if (!path2.isAbsolute(p)) {
|
|
4663
|
+
p = path2.resolve(process.cwd(), p);
|
|
4640
4664
|
}
|
|
4641
4665
|
return p;
|
|
4642
4666
|
}
|
|
@@ -4646,7 +4670,7 @@ function stripTitleFromUrl(value) {
|
|
|
4646
4670
|
return match ? match[1] : trimmed;
|
|
4647
4671
|
}
|
|
4648
4672
|
function getExtension(filePath) {
|
|
4649
|
-
const ext =
|
|
4673
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
4650
4674
|
return ext.startsWith(".") ? ext.slice(1) : ext;
|
|
4651
4675
|
}
|
|
4652
4676
|
function isImagePath(filePath) {
|
|
@@ -4672,11 +4696,11 @@ function createExtractedMedia(source, sourceKind, options) {
|
|
|
4672
4696
|
let fileName;
|
|
4673
4697
|
if (isLocal) {
|
|
4674
4698
|
localPath = normalizeLocalPath(cleanSource);
|
|
4675
|
-
fileName =
|
|
4699
|
+
fileName = path2.basename(localPath);
|
|
4676
4700
|
} else if (isHttp) {
|
|
4677
4701
|
try {
|
|
4678
4702
|
const url = new URL(cleanSource);
|
|
4679
|
-
fileName =
|
|
4703
|
+
fileName = path2.basename(url.pathname) || void 0;
|
|
4680
4704
|
} catch {
|
|
4681
4705
|
}
|
|
4682
4706
|
}
|
|
@@ -4834,7 +4858,7 @@ function extractMediaFromText(text, options = {}) {
|
|
|
4834
4858
|
if (media.type !== "image" && isNonImageFilePath(media.localPath || rawPath)) {
|
|
4835
4859
|
if (addMedia(media)) {
|
|
4836
4860
|
if (removeFromText && match.index !== void 0) {
|
|
4837
|
-
const fileName = media.fileName ||
|
|
4861
|
+
const fileName = media.fileName || path2.basename(rawPath);
|
|
4838
4862
|
replacements.push({
|
|
4839
4863
|
start: match.index,
|
|
4840
4864
|
end: match.index + fullMatch.length,
|
|
@@ -4879,7 +4903,7 @@ function extractMediaFromText(text, options = {}) {
|
|
|
4879
4903
|
if (media.type !== "image") {
|
|
4880
4904
|
if (addMedia(media)) {
|
|
4881
4905
|
if (removeFromText && match.index !== void 0) {
|
|
4882
|
-
const fileName = media.fileName ||
|
|
4906
|
+
const fileName = media.fileName || path2.basename(rawPath);
|
|
4883
4907
|
replacements.push({
|
|
4884
4908
|
start: match.index,
|
|
4885
4909
|
end: match.index + fullMatch.length,
|
|
@@ -4928,13 +4952,27 @@ function sanitizeFileName(name) {
|
|
|
4928
4952
|
function resolveFileNameFromUrl(url) {
|
|
4929
4953
|
try {
|
|
4930
4954
|
const parsed = new URL(url);
|
|
4931
|
-
const base =
|
|
4955
|
+
const base = path2.basename(parsed.pathname);
|
|
4932
4956
|
if (!base || base === "/") return void 0;
|
|
4933
4957
|
return base;
|
|
4934
4958
|
} catch {
|
|
4935
4959
|
return void 0;
|
|
4936
4960
|
}
|
|
4937
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
|
+
}
|
|
4938
4976
|
var EXT_TO_MIME = {
|
|
4939
4977
|
// 图片
|
|
4940
4978
|
jpg: "image/jpeg",
|
|
@@ -5029,15 +5067,15 @@ function validatePathSecurity(filePath, options = {}) {
|
|
|
5029
5067
|
);
|
|
5030
5068
|
}
|
|
5031
5069
|
if (preventTraversal) {
|
|
5032
|
-
const normalized =
|
|
5070
|
+
const normalized = path2.normalize(filePath);
|
|
5033
5071
|
if (normalized.includes("..")) {
|
|
5034
5072
|
throw new PathSecurityError(filePath, "Path traversal detected");
|
|
5035
5073
|
}
|
|
5036
5074
|
}
|
|
5037
5075
|
if (allowedPrefixes && allowedPrefixes.length > 0) {
|
|
5038
|
-
const normalizedPath =
|
|
5076
|
+
const normalizedPath = path2.normalize(filePath);
|
|
5039
5077
|
const isAllowed = allowedPrefixes.some(
|
|
5040
|
-
(prefix) => normalizedPath.startsWith(
|
|
5078
|
+
(prefix) => normalizedPath.startsWith(path2.normalize(prefix))
|
|
5041
5079
|
);
|
|
5042
5080
|
if (!isAllowed) {
|
|
5043
5081
|
throw new PathSecurityError(
|
|
@@ -5080,7 +5118,7 @@ async function fetchMediaFromUrl(url, options = {}) {
|
|
|
5080
5118
|
let fileName = "file";
|
|
5081
5119
|
try {
|
|
5082
5120
|
const urlPath = new URL(url).pathname;
|
|
5083
|
-
fileName =
|
|
5121
|
+
fileName = path2.basename(urlPath) || "file";
|
|
5084
5122
|
} catch {
|
|
5085
5123
|
}
|
|
5086
5124
|
const mimeType = response.headers.get("content-type")?.split(";")[0].trim() || getMimeType(fileName);
|
|
@@ -5153,7 +5191,7 @@ async function downloadToTempFile(url, options = {}) {
|
|
|
5153
5191
|
const ext = resolveExtension(contentType, sourceName);
|
|
5154
5192
|
const random = Math.random().toString(36).slice(2, 8);
|
|
5155
5193
|
const fileName = `${safePrefix}-${Date.now()}-${random}${ext}`;
|
|
5156
|
-
const fullPath =
|
|
5194
|
+
const fullPath = path2.join(tempDir, fileName);
|
|
5157
5195
|
await fsPromises.mkdir(tempDir, { recursive: true });
|
|
5158
5196
|
const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
5159
5197
|
await fsPromises.writeFile(fullPath, buffer);
|
|
@@ -5185,7 +5223,7 @@ async function readMediaFromLocal(filePath, options = {}) {
|
|
|
5185
5223
|
throw new FileSizeLimitError(stats.size, maxSize);
|
|
5186
5224
|
}
|
|
5187
5225
|
const buffer = await fsPromises.readFile(localPath);
|
|
5188
|
-
const fileName =
|
|
5226
|
+
const fileName = path2.basename(localPath);
|
|
5189
5227
|
const mimeType = getMimeType(localPath);
|
|
5190
5228
|
return {
|
|
5191
5229
|
buffer,
|
|
@@ -5200,6 +5238,63 @@ async function readMedia(source, options = {}) {
|
|
|
5200
5238
|
}
|
|
5201
5239
|
return readMediaFromLocal(source, options);
|
|
5202
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
|
+
}
|
|
5203
5298
|
async function cleanupFileSafe(filePath, onError) {
|
|
5204
5299
|
if (!filePath) return;
|
|
5205
5300
|
try {
|
|
@@ -5213,12 +5308,27 @@ async function cleanupFileSafe(filePath, onError) {
|
|
|
5213
5308
|
|
|
5214
5309
|
// ../../packages/shared/src/cron/index.ts
|
|
5215
5310
|
var CRON_HIDDEN_PROMPT = `
|
|
5216
|
-
When creating or updating a cron task,
|
|
5217
|
-
- Do not
|
|
5218
|
-
- sessionTarget="isolated"
|
|
5311
|
+
When creating or updating a cron task, always store a fixed delivery target in the job itself.
|
|
5312
|
+
- Use the built-in cron tool (action=add/update). Do not run shell commands.
|
|
5313
|
+
- Must use sessionTarget="isolated" for reminder jobs.
|
|
5219
5314
|
- payload.kind="agentTurn"
|
|
5220
|
-
- payload.
|
|
5221
|
-
-
|
|
5315
|
+
- payload.message must be plain user-visible reminder text only.
|
|
5316
|
+
- You must encode runtime guardrails directly into payload.message so the cron run can follow them without extra context.
|
|
5317
|
+
- Runtime guardrails to encode in payload.message:
|
|
5318
|
+
- return plain text only
|
|
5319
|
+
- never call any tool
|
|
5320
|
+
- never call the message tool
|
|
5321
|
+
- never send manually; delivery is handled by cron delivery settings
|
|
5322
|
+
- Do not include tool directives, "NO_REPLY", or heartbeat markers in payload.message.
|
|
5323
|
+
- Job name is never a message target.
|
|
5324
|
+
- During cron run, must return plain text only and never call the message tool.
|
|
5325
|
+
- Use top-level delivery with announce mode:
|
|
5326
|
+
delivery.mode="announce"
|
|
5327
|
+
delivery.channel=<OriginatingChannel> (example: "qqbot")
|
|
5328
|
+
delivery.to=<OriginatingTo> (examples: "user:<openid>" / "group:<group_openid>")
|
|
5329
|
+
delivery.accountId=<AccountId> when available
|
|
5330
|
+
- Never set delivery.channel="last" for multi-channel environments.
|
|
5331
|
+
- If OriginatingChannel/OriginatingTo are unavailable, ask a concise follow-up for channel and target.
|
|
5222
5332
|
- Do not call the message tool to send`;
|
|
5223
5333
|
var CRON_TRIGGER_KEYWORDS = [
|
|
5224
5334
|
"\u5B9A\u65F6",
|
|
@@ -6378,7 +6488,7 @@ function isCommandLike(value) {
|
|
|
6378
6488
|
}
|
|
6379
6489
|
return typeof value.command === "function" && typeof value.description === "function" && typeof value.action === "function";
|
|
6380
6490
|
}
|
|
6381
|
-
function
|
|
6491
|
+
function toTrimmedString2(value) {
|
|
6382
6492
|
if (typeof value !== "string") {
|
|
6383
6493
|
return void 0;
|
|
6384
6494
|
}
|
|
@@ -6411,7 +6521,7 @@ function getPreferredAccountConfig(channelCfg) {
|
|
|
6411
6521
|
if (!isRecord(accounts)) {
|
|
6412
6522
|
return void 0;
|
|
6413
6523
|
}
|
|
6414
|
-
const defaultAccountId =
|
|
6524
|
+
const defaultAccountId = toTrimmedString2(channelCfg.defaultAccount);
|
|
6415
6525
|
if (defaultAccountId) {
|
|
6416
6526
|
const preferred = accounts[defaultAccountId];
|
|
6417
6527
|
if (isRecord(preferred)) {
|
|
@@ -6548,12 +6658,12 @@ async function configureDingtalk(prompter, cfg) {
|
|
|
6548
6658
|
const existing = getChannelConfig(cfg, "dingtalk");
|
|
6549
6659
|
const clientId = await prompter.askText({
|
|
6550
6660
|
label: "DingTalk clientId\uFF08AppKey\uFF09",
|
|
6551
|
-
defaultValue:
|
|
6661
|
+
defaultValue: toTrimmedString2(existing.clientId),
|
|
6552
6662
|
required: true
|
|
6553
6663
|
});
|
|
6554
6664
|
const clientSecret = await prompter.askSecret({
|
|
6555
6665
|
label: "DingTalk clientSecret\uFF08AppSecret\uFF09",
|
|
6556
|
-
existingValue:
|
|
6666
|
+
existingValue: toTrimmedString2(existing.clientSecret),
|
|
6557
6667
|
required: true
|
|
6558
6668
|
});
|
|
6559
6669
|
const enableAICard = await prompter.askConfirm(
|
|
@@ -6572,12 +6682,12 @@ async function configureFeishu(prompter, cfg) {
|
|
|
6572
6682
|
const existing = getChannelConfig(cfg, "feishu-china");
|
|
6573
6683
|
const appId = await prompter.askText({
|
|
6574
6684
|
label: "Feishu appId",
|
|
6575
|
-
defaultValue:
|
|
6685
|
+
defaultValue: toTrimmedString2(existing.appId),
|
|
6576
6686
|
required: true
|
|
6577
6687
|
});
|
|
6578
6688
|
const appSecret = await prompter.askSecret({
|
|
6579
6689
|
label: "Feishu appSecret",
|
|
6580
|
-
existingValue:
|
|
6690
|
+
existingValue: toTrimmedString2(existing.appSecret),
|
|
6581
6691
|
required: true
|
|
6582
6692
|
});
|
|
6583
6693
|
const sendMarkdownAsCard = await prompter.askConfirm(
|
|
@@ -6596,17 +6706,17 @@ async function configureWecom(prompter, cfg) {
|
|
|
6596
6706
|
const existing = getChannelConfig(cfg, "wecom");
|
|
6597
6707
|
const webhookPath = await prompter.askText({
|
|
6598
6708
|
label: "Webhook \u8DEF\u5F84\uFF08\u9700\u4E0E\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u914D\u7F6E\u4E00\u81F4\uFF0C\u9ED8\u8BA4 /wecom\uFF09",
|
|
6599
|
-
defaultValue:
|
|
6709
|
+
defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom",
|
|
6600
6710
|
required: true
|
|
6601
6711
|
});
|
|
6602
6712
|
const token = await prompter.askSecret({
|
|
6603
6713
|
label: "WeCom token",
|
|
6604
|
-
existingValue:
|
|
6714
|
+
existingValue: toTrimmedString2(existing.token),
|
|
6605
6715
|
required: true
|
|
6606
6716
|
});
|
|
6607
6717
|
const encodingAESKey = await prompter.askSecret({
|
|
6608
6718
|
label: "WeCom encodingAESKey",
|
|
6609
|
-
existingValue:
|
|
6719
|
+
existingValue: toTrimmedString2(existing.encodingAESKey),
|
|
6610
6720
|
required: true
|
|
6611
6721
|
});
|
|
6612
6722
|
return mergeChannelConfig(cfg, "wecom", {
|
|
@@ -6622,17 +6732,17 @@ async function configureWecomApp(prompter, cfg) {
|
|
|
6622
6732
|
const existingAsr = isRecord(existing.asr) ? existing.asr : {};
|
|
6623
6733
|
const webhookPath = await prompter.askText({
|
|
6624
6734
|
label: "Webhook \u8DEF\u5F84\uFF08\u9700\u4E0E\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u914D\u7F6E\u4E00\u81F4\uFF0C\u9ED8\u8BA4 /wecom-app\uFF09",
|
|
6625
|
-
defaultValue:
|
|
6735
|
+
defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-app",
|
|
6626
6736
|
required: true
|
|
6627
6737
|
});
|
|
6628
6738
|
const token = await prompter.askSecret({
|
|
6629
6739
|
label: "WeCom App token",
|
|
6630
|
-
existingValue:
|
|
6740
|
+
existingValue: toTrimmedString2(existing.token),
|
|
6631
6741
|
required: true
|
|
6632
6742
|
});
|
|
6633
6743
|
const encodingAESKey = await prompter.askSecret({
|
|
6634
6744
|
label: "WeCom App encodingAESKey",
|
|
6635
|
-
existingValue:
|
|
6745
|
+
existingValue: toTrimmedString2(existing.encodingAESKey),
|
|
6636
6746
|
required: true
|
|
6637
6747
|
});
|
|
6638
6748
|
const patch = {
|
|
@@ -6642,12 +6752,12 @@ async function configureWecomApp(prompter, cfg) {
|
|
|
6642
6752
|
};
|
|
6643
6753
|
const corpId = await prompter.askText({
|
|
6644
6754
|
label: "corpId",
|
|
6645
|
-
defaultValue:
|
|
6755
|
+
defaultValue: toTrimmedString2(existing.corpId),
|
|
6646
6756
|
required: true
|
|
6647
6757
|
});
|
|
6648
6758
|
const corpSecret = await prompter.askSecret({
|
|
6649
6759
|
label: "corpSecret",
|
|
6650
|
-
existingValue:
|
|
6760
|
+
existingValue: toTrimmedString2(existing.corpSecret),
|
|
6651
6761
|
required: true
|
|
6652
6762
|
});
|
|
6653
6763
|
const agentId = await prompter.askNumber({
|
|
@@ -6675,17 +6785,17 @@ async function configureWecomApp(prompter, cfg) {
|
|
|
6675
6785
|
);
|
|
6676
6786
|
asr.appId = await prompter.askText({
|
|
6677
6787
|
label: "ASR appId\uFF08\u817E\u8BAF\u4E91\uFF09",
|
|
6678
|
-
defaultValue:
|
|
6788
|
+
defaultValue: toTrimmedString2(existingAsr.appId),
|
|
6679
6789
|
required: true
|
|
6680
6790
|
});
|
|
6681
6791
|
asr.secretId = await prompter.askSecret({
|
|
6682
6792
|
label: "ASR secretId\uFF08\u817E\u8BAF\u4E91\uFF09",
|
|
6683
|
-
existingValue:
|
|
6793
|
+
existingValue: toTrimmedString2(existingAsr.secretId),
|
|
6684
6794
|
required: true
|
|
6685
6795
|
});
|
|
6686
6796
|
asr.secretKey = await prompter.askSecret({
|
|
6687
6797
|
label: "ASR secretKey\uFF08\u817E\u8BAF\u4E91\uFF09",
|
|
6688
|
-
existingValue:
|
|
6798
|
+
existingValue: toTrimmedString2(existingAsr.secretKey),
|
|
6689
6799
|
required: true
|
|
6690
6800
|
});
|
|
6691
6801
|
}
|
|
@@ -6699,12 +6809,12 @@ async function configureQQBot(prompter, cfg) {
|
|
|
6699
6809
|
const existingAsr = isRecord(existing.asr) ? existing.asr : {};
|
|
6700
6810
|
const appId = await prompter.askText({
|
|
6701
6811
|
label: "QQBot appId",
|
|
6702
|
-
defaultValue:
|
|
6812
|
+
defaultValue: toTrimmedString2(existing.appId),
|
|
6703
6813
|
required: true
|
|
6704
6814
|
});
|
|
6705
6815
|
const clientSecret = await prompter.askSecret({
|
|
6706
6816
|
label: "QQBot clientSecret",
|
|
6707
|
-
existingValue:
|
|
6817
|
+
existingValue: toTrimmedString2(existing.clientSecret),
|
|
6708
6818
|
required: true
|
|
6709
6819
|
});
|
|
6710
6820
|
const asrEnabled = await prompter.askConfirm(
|
|
@@ -6718,17 +6828,17 @@ async function configureQQBot(prompter, cfg) {
|
|
|
6718
6828
|
Ve("ASR \u5F00\u901A\u65B9\u5F0F\u8BE6\u60C5\u8BF7\u67E5\u770B\u914D\u7F6E\u6587\u6863\u3002", "\u63D0\u793A");
|
|
6719
6829
|
asr.appId = await prompter.askText({
|
|
6720
6830
|
label: "ASR appId\uFF08\u817E\u8BAF\u4E91\uFF09",
|
|
6721
|
-
defaultValue:
|
|
6831
|
+
defaultValue: toTrimmedString2(existingAsr.appId),
|
|
6722
6832
|
required: true
|
|
6723
6833
|
});
|
|
6724
6834
|
asr.secretId = await prompter.askSecret({
|
|
6725
6835
|
label: "ASR secretId\uFF08\u817E\u8BAF\u4E91\uFF09",
|
|
6726
|
-
existingValue:
|
|
6836
|
+
existingValue: toTrimmedString2(existingAsr.secretId),
|
|
6727
6837
|
required: true
|
|
6728
6838
|
});
|
|
6729
6839
|
asr.secretKey = await prompter.askSecret({
|
|
6730
6840
|
label: "ASR secretKey\uFF08\u817E\u8BAF\u4E91\uFF09",
|
|
6731
|
-
existingValue:
|
|
6841
|
+
existingValue: toTrimmedString2(existingAsr.secretKey),
|
|
6732
6842
|
required: true
|
|
6733
6843
|
});
|
|
6734
6844
|
}
|
|
@@ -6957,11 +7067,29 @@ var MSG_SEQ_BASE = 1e6;
|
|
|
6957
7067
|
var tokenCacheMap = /* @__PURE__ */ new Map();
|
|
6958
7068
|
var tokenPromiseMap = /* @__PURE__ */ new Map();
|
|
6959
7069
|
var msgSeqMap = /* @__PURE__ */ new Map();
|
|
6960
|
-
function
|
|
6961
|
-
if (
|
|
6962
|
-
const
|
|
7070
|
+
function toTrimmedString3(value) {
|
|
7071
|
+
if (value === void 0 || value === null) return void 0;
|
|
7072
|
+
const next = String(value).trim();
|
|
7073
|
+
return next ? next : void 0;
|
|
7074
|
+
}
|
|
7075
|
+
function requireTrimmedString(value, field) {
|
|
7076
|
+
const normalized = toTrimmedString3(value);
|
|
7077
|
+
if (!normalized) {
|
|
7078
|
+
throw new Error(`QQBot ${field} is empty`);
|
|
7079
|
+
}
|
|
7080
|
+
return normalized;
|
|
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
|
+
}
|
|
7088
|
+
function nextMsgSeq(sequenceKey) {
|
|
7089
|
+
if (!sequenceKey) return MSG_SEQ_BASE + 1;
|
|
7090
|
+
const current = msgSeqMap.get(sequenceKey) ?? 0;
|
|
6963
7091
|
const next = current + 1;
|
|
6964
|
-
msgSeqMap.set(
|
|
7092
|
+
msgSeqMap.set(sequenceKey, next);
|
|
6965
7093
|
if (msgSeqMap.size > 1e3) {
|
|
6966
7094
|
const keys = Array.from(msgSeqMap.keys());
|
|
6967
7095
|
for (let i = 0; i < 500; i += 1) {
|
|
@@ -6971,20 +7099,23 @@ function nextMsgSeq(messageId) {
|
|
|
6971
7099
|
return MSG_SEQ_BASE + next;
|
|
6972
7100
|
}
|
|
6973
7101
|
function clearTokenCache(appId) {
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
7102
|
+
const normalizedAppId = toTrimmedString3(appId);
|
|
7103
|
+
if (normalizedAppId) {
|
|
7104
|
+
tokenCacheMap.delete(normalizedAppId);
|
|
7105
|
+
tokenPromiseMap.delete(normalizedAppId);
|
|
6977
7106
|
} else {
|
|
6978
7107
|
tokenCacheMap.clear();
|
|
6979
7108
|
tokenPromiseMap.clear();
|
|
6980
7109
|
}
|
|
6981
7110
|
}
|
|
6982
7111
|
async function getAccessToken(appId, clientSecret, options) {
|
|
6983
|
-
const
|
|
7112
|
+
const normalizedAppId = requireTrimmedString(appId, "appId");
|
|
7113
|
+
const normalizedClientSecret = requireTrimmedString(clientSecret, "clientSecret");
|
|
7114
|
+
const cached = tokenCacheMap.get(normalizedAppId);
|
|
6984
7115
|
if (cached && Date.now() < cached.expiresAt - 5 * 60 * 1e3) {
|
|
6985
7116
|
return cached.token;
|
|
6986
7117
|
}
|
|
6987
|
-
const existingPromise = tokenPromiseMap.get(
|
|
7118
|
+
const existingPromise = tokenPromiseMap.get(normalizedAppId);
|
|
6988
7119
|
if (existingPromise) {
|
|
6989
7120
|
return existingPromise;
|
|
6990
7121
|
}
|
|
@@ -6992,22 +7123,22 @@ async function getAccessToken(appId, clientSecret, options) {
|
|
|
6992
7123
|
try {
|
|
6993
7124
|
const data = await httpPost(
|
|
6994
7125
|
TOKEN_URL,
|
|
6995
|
-
{ appId, clientSecret },
|
|
7126
|
+
{ appId: normalizedAppId, clientSecret: normalizedClientSecret },
|
|
6996
7127
|
{ timeout: options?.timeout ?? 15e3 }
|
|
6997
7128
|
);
|
|
6998
7129
|
if (!data.access_token) {
|
|
6999
7130
|
throw new Error("access_token missing from QQ response");
|
|
7000
7131
|
}
|
|
7001
|
-
tokenCacheMap.set(
|
|
7132
|
+
tokenCacheMap.set(normalizedAppId, {
|
|
7002
7133
|
token: data.access_token,
|
|
7003
7134
|
expiresAt: Date.now() + (data.expires_in ?? 7200) * 1e3
|
|
7004
7135
|
});
|
|
7005
7136
|
return data.access_token;
|
|
7006
7137
|
} finally {
|
|
7007
|
-
tokenPromiseMap.delete(
|
|
7138
|
+
tokenPromiseMap.delete(normalizedAppId);
|
|
7008
7139
|
}
|
|
7009
7140
|
})();
|
|
7010
|
-
tokenPromiseMap.set(
|
|
7141
|
+
tokenPromiseMap.set(normalizedAppId, promise);
|
|
7011
7142
|
return promise;
|
|
7012
7143
|
}
|
|
7013
7144
|
async function apiGet(accessToken, path4, options) {
|
|
@@ -7035,7 +7166,7 @@ async function getGatewayUrl(accessToken) {
|
|
|
7035
7166
|
return data.url;
|
|
7036
7167
|
}
|
|
7037
7168
|
function buildMessageBody(params) {
|
|
7038
|
-
const msgSeq = nextMsgSeq(params.messageId);
|
|
7169
|
+
const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
|
|
7039
7170
|
const body = params.markdown ? {
|
|
7040
7171
|
markdown: { content: params.content },
|
|
7041
7172
|
msg_type: 2,
|
|
@@ -7047,6 +7178,8 @@ function buildMessageBody(params) {
|
|
|
7047
7178
|
};
|
|
7048
7179
|
if (params.messageId) {
|
|
7049
7180
|
body.msg_id = params.messageId;
|
|
7181
|
+
} else if (params.eventId) {
|
|
7182
|
+
body.event_id = params.eventId;
|
|
7050
7183
|
}
|
|
7051
7184
|
return body;
|
|
7052
7185
|
}
|
|
@@ -7054,6 +7187,7 @@ async function sendC2CMessage(params) {
|
|
|
7054
7187
|
const body = buildMessageBody({
|
|
7055
7188
|
content: params.content,
|
|
7056
7189
|
messageId: params.messageId,
|
|
7190
|
+
eventId: params.eventId,
|
|
7057
7191
|
markdown: params.markdown
|
|
7058
7192
|
});
|
|
7059
7193
|
return apiPost(params.accessToken, `/v2/users/${params.openid}/messages`, body, {
|
|
@@ -7064,10 +7198,10 @@ async function sendGroupMessage(params) {
|
|
|
7064
7198
|
const body = buildMessageBody({
|
|
7065
7199
|
content: params.content,
|
|
7066
7200
|
messageId: params.messageId,
|
|
7201
|
+
eventId: params.eventId,
|
|
7067
7202
|
markdown: params.markdown
|
|
7068
7203
|
});
|
|
7069
|
-
|
|
7070
|
-
return apiPost(params.accessToken, `/v2/groups/${groupOpenidLower}/messages`, body, {
|
|
7204
|
+
return apiPost(params.accessToken, `/v2/groups/${params.groupOpenid}/messages`, body, {
|
|
7071
7205
|
timeout: 15e3
|
|
7072
7206
|
});
|
|
7073
7207
|
}
|
|
@@ -7076,13 +7210,12 @@ async function sendChannelMessage(params) {
|
|
|
7076
7210
|
if (params.messageId) {
|
|
7077
7211
|
body.msg_id = params.messageId;
|
|
7078
7212
|
}
|
|
7079
|
-
|
|
7080
|
-
return apiPost(params.accessToken, `/channels/${channelIdLower}/messages`, body, {
|
|
7213
|
+
return apiPost(params.accessToken, `/channels/${params.channelId}/messages`, body, {
|
|
7081
7214
|
timeout: 15e3
|
|
7082
7215
|
});
|
|
7083
7216
|
}
|
|
7084
7217
|
async function sendC2CInputNotify(params) {
|
|
7085
|
-
const msgSeq = nextMsgSeq(params.messageId);
|
|
7218
|
+
const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
|
|
7086
7219
|
await apiPost(
|
|
7087
7220
|
params.accessToken,
|
|
7088
7221
|
`/v2/users/${params.openid}/messages`,
|
|
@@ -7093,14 +7226,15 @@ async function sendC2CInputNotify(params) {
|
|
|
7093
7226
|
input_second: params.inputSecond ?? 60
|
|
7094
7227
|
},
|
|
7095
7228
|
msg_seq: msgSeq,
|
|
7096
|
-
...params.messageId ? { msg_id: params.messageId } : {}
|
|
7229
|
+
...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
|
|
7097
7230
|
},
|
|
7098
7231
|
{ timeout: 15e3 }
|
|
7099
7232
|
);
|
|
7100
7233
|
}
|
|
7101
7234
|
async function uploadC2CMedia(params) {
|
|
7102
7235
|
const body = {
|
|
7103
|
-
file_type: params.fileType
|
|
7236
|
+
file_type: params.fileType,
|
|
7237
|
+
srv_send_msg: params.srvSendMsg ?? false
|
|
7104
7238
|
};
|
|
7105
7239
|
if (params.url) {
|
|
7106
7240
|
body.url = params.url;
|
|
@@ -7109,13 +7243,17 @@ async function uploadC2CMedia(params) {
|
|
|
7109
7243
|
} else {
|
|
7110
7244
|
throw new Error("uploadC2CMedia requires url or fileData");
|
|
7111
7245
|
}
|
|
7246
|
+
if (params.fileType === 4 /* FILE */ && params.fileName?.trim()) {
|
|
7247
|
+
body.file_name = sanitizeUploadFileName(params.fileName);
|
|
7248
|
+
}
|
|
7112
7249
|
return apiPost(params.accessToken, `/v2/users/${params.openid}/files`, body, {
|
|
7113
7250
|
timeout: 3e4
|
|
7114
7251
|
});
|
|
7115
7252
|
}
|
|
7116
7253
|
async function uploadGroupMedia(params) {
|
|
7117
7254
|
const body = {
|
|
7118
|
-
file_type: params.fileType
|
|
7255
|
+
file_type: params.fileType,
|
|
7256
|
+
srv_send_msg: params.srvSendMsg ?? false
|
|
7119
7257
|
};
|
|
7120
7258
|
if (params.url) {
|
|
7121
7259
|
body.url = params.url;
|
|
@@ -7124,12 +7262,15 @@ async function uploadGroupMedia(params) {
|
|
|
7124
7262
|
} else {
|
|
7125
7263
|
throw new Error("uploadGroupMedia requires url or fileData");
|
|
7126
7264
|
}
|
|
7265
|
+
if (params.fileType === 4 /* FILE */ && params.fileName?.trim()) {
|
|
7266
|
+
body.file_name = sanitizeUploadFileName(params.fileName);
|
|
7267
|
+
}
|
|
7127
7268
|
return apiPost(params.accessToken, `/v2/groups/${params.groupOpenid}/files`, body, {
|
|
7128
7269
|
timeout: 3e4
|
|
7129
7270
|
});
|
|
7130
7271
|
}
|
|
7131
7272
|
async function sendC2CMediaMessage(params) {
|
|
7132
|
-
const msgSeq = nextMsgSeq(params.messageId);
|
|
7273
|
+
const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
|
|
7133
7274
|
return apiPost(
|
|
7134
7275
|
params.accessToken,
|
|
7135
7276
|
`/v2/users/${params.openid}/messages`,
|
|
@@ -7138,13 +7279,13 @@ async function sendC2CMediaMessage(params) {
|
|
|
7138
7279
|
media: { file_info: params.fileInfo },
|
|
7139
7280
|
msg_seq: msgSeq,
|
|
7140
7281
|
...params.content ? { content: params.content } : {},
|
|
7141
|
-
...params.messageId ? { msg_id: params.messageId } : {}
|
|
7282
|
+
...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
|
|
7142
7283
|
},
|
|
7143
7284
|
{ timeout: 15e3 }
|
|
7144
7285
|
);
|
|
7145
7286
|
}
|
|
7146
7287
|
async function sendGroupMediaMessage(params) {
|
|
7147
|
-
const msgSeq = nextMsgSeq(params.messageId);
|
|
7288
|
+
const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
|
|
7148
7289
|
return apiPost(
|
|
7149
7290
|
params.accessToken,
|
|
7150
7291
|
`/v2/groups/${params.groupOpenid}/messages`,
|
|
@@ -7153,12 +7294,11 @@ async function sendGroupMediaMessage(params) {
|
|
|
7153
7294
|
media: { file_info: params.fileInfo },
|
|
7154
7295
|
msg_seq: msgSeq,
|
|
7155
7296
|
...params.content ? { content: params.content } : {},
|
|
7156
|
-
...params.messageId ? { msg_id: params.messageId } : {}
|
|
7297
|
+
...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
|
|
7157
7298
|
},
|
|
7158
7299
|
{ timeout: 15e3 }
|
|
7159
7300
|
);
|
|
7160
7301
|
}
|
|
7161
|
-
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.";
|
|
7162
7302
|
var require2 = createRequire(import.meta.url);
|
|
7163
7303
|
function resolveQQBotMediaFileType(fileName) {
|
|
7164
7304
|
const mediaType = detectMediaType(fileName);
|
|
@@ -7174,7 +7314,7 @@ function resolveQQBotMediaFileType(fileName) {
|
|
|
7174
7314
|
}
|
|
7175
7315
|
}
|
|
7176
7316
|
async function uploadQQBotFile(params) {
|
|
7177
|
-
const { accessToken, target, fileType, url, fileData } = params;
|
|
7317
|
+
const { accessToken, target, fileType, url, fileData, fileName } = params;
|
|
7178
7318
|
if (!url && !fileData) {
|
|
7179
7319
|
throw new Error("QQBot file upload requires url or fileData");
|
|
7180
7320
|
}
|
|
@@ -7182,11 +7322,15 @@ async function uploadQQBotFile(params) {
|
|
|
7182
7322
|
accessToken,
|
|
7183
7323
|
groupOpenid: target.id,
|
|
7184
7324
|
fileType,
|
|
7325
|
+
srvSendMsg: false,
|
|
7326
|
+
...fileName ? { fileName } : {},
|
|
7185
7327
|
...url ? { url } : { fileData }
|
|
7186
7328
|
}) : await uploadC2CMedia({
|
|
7187
7329
|
accessToken,
|
|
7188
7330
|
openid: target.id,
|
|
7189
7331
|
fileType,
|
|
7332
|
+
srvSendMsg: false,
|
|
7333
|
+
...fileName ? { fileName } : {},
|
|
7190
7334
|
...url ? { url } : { fileData }
|
|
7191
7335
|
});
|
|
7192
7336
|
if (!upload.file_info) {
|
|
@@ -7194,14 +7338,29 @@ async function uploadQQBotFile(params) {
|
|
|
7194
7338
|
}
|
|
7195
7339
|
return upload.file_info;
|
|
7196
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
|
+
}
|
|
7197
7356
|
async function convertAudioToSilk(audioPath) {
|
|
7198
7357
|
const ffmpegPath = require2("ffmpeg-static");
|
|
7199
7358
|
if (!ffmpegPath) {
|
|
7200
7359
|
throw new Error("ffmpeg-static not found");
|
|
7201
7360
|
}
|
|
7202
7361
|
const silkWasm = require2("silk-wasm");
|
|
7203
|
-
const tmpDir = fs3.mkdtempSync(
|
|
7204
|
-
const pcmPath =
|
|
7362
|
+
const tmpDir = fs3.mkdtempSync(path2.join(os.tmpdir(), "qqbot-silk-"));
|
|
7363
|
+
const pcmPath = path2.join(tmpDir, "audio.pcm");
|
|
7205
7364
|
try {
|
|
7206
7365
|
execFileSync(
|
|
7207
7366
|
ffmpegPath,
|
|
@@ -7219,31 +7378,33 @@ async function convertAudioToSilk(audioPath) {
|
|
|
7219
7378
|
}
|
|
7220
7379
|
}
|
|
7221
7380
|
async function sendFileQQBot(params) {
|
|
7222
|
-
const { cfg, target, mediaUrl, messageId } = params;
|
|
7223
|
-
|
|
7381
|
+
const { cfg, target, mediaUrl, text, messageId, eventId } = params;
|
|
7382
|
+
const credentials = resolveQQBotCredentials(cfg);
|
|
7383
|
+
if (!credentials) {
|
|
7224
7384
|
throw new Error("QQBot not configured (missing appId/clientSecret)");
|
|
7225
7385
|
}
|
|
7226
7386
|
const src = stripTitleFromUrl(mediaUrl);
|
|
7227
7387
|
const fileType = resolveQQBotMediaFileType(src);
|
|
7228
|
-
if (fileType === 4 /* FILE */) {
|
|
7229
|
-
throw new Error(QQBOT_UNSUPPORTED_FILE_TYPE_MESSAGE);
|
|
7230
|
-
}
|
|
7231
7388
|
const sourceIsHttp = isHttpUrl(src);
|
|
7232
7389
|
const maxFileSizeMB = cfg.maxFileSizeMB ?? 100;
|
|
7233
7390
|
const mediaTimeoutMs = cfg.mediaTimeoutMs ?? 3e4;
|
|
7234
7391
|
const maxSizeBytes = Math.floor(maxFileSizeMB * 1024 * 1024);
|
|
7235
|
-
const
|
|
7392
|
+
const messageText = text?.trim() ? text.trim() : void 0;
|
|
7393
|
+
const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
|
|
7236
7394
|
let fileInfo;
|
|
7237
7395
|
try {
|
|
7238
7396
|
if (sourceIsHttp) {
|
|
7397
|
+
const fileName = fileType === 4 /* FILE */ ? deriveUploadFileName(src) : void 0;
|
|
7239
7398
|
fileInfo = await uploadQQBotFile({
|
|
7240
7399
|
accessToken,
|
|
7241
7400
|
target,
|
|
7242
7401
|
fileType,
|
|
7243
|
-
url: src
|
|
7402
|
+
url: src,
|
|
7403
|
+
...fileName ? { fileName } : {}
|
|
7244
7404
|
});
|
|
7245
7405
|
} else {
|
|
7246
7406
|
let buffer;
|
|
7407
|
+
let fileName = fileType === 4 /* FILE */ ? deriveUploadFileName(src) : void 0;
|
|
7247
7408
|
if (fileType === 3 /* VOICE */) {
|
|
7248
7409
|
try {
|
|
7249
7410
|
const silkData = await convertAudioToSilk(src);
|
|
@@ -7261,12 +7422,16 @@ async function sendFileQQBot(params) {
|
|
|
7261
7422
|
maxSize: maxSizeBytes
|
|
7262
7423
|
});
|
|
7263
7424
|
buffer = local.buffer;
|
|
7425
|
+
if (fileType === 4 /* FILE */) {
|
|
7426
|
+
fileName = local.fileName || fileName;
|
|
7427
|
+
}
|
|
7264
7428
|
}
|
|
7265
7429
|
fileInfo = await uploadQQBotFile({
|
|
7266
7430
|
accessToken,
|
|
7267
7431
|
target,
|
|
7268
7432
|
fileType,
|
|
7269
|
-
fileData: buffer.toString("base64")
|
|
7433
|
+
fileData: buffer.toString("base64"),
|
|
7434
|
+
...fileName ? { fileName } : {}
|
|
7270
7435
|
});
|
|
7271
7436
|
}
|
|
7272
7437
|
} catch (err) {
|
|
@@ -7279,10 +7444,12 @@ async function sendFileQQBot(params) {
|
|
|
7279
7444
|
accessToken,
|
|
7280
7445
|
groupOpenid: target.id,
|
|
7281
7446
|
fileInfo,
|
|
7282
|
-
|
|
7447
|
+
...messageText ? { content: messageText } : {},
|
|
7448
|
+
...messageId ? { messageId } : {},
|
|
7449
|
+
...eventId ? { eventId } : {}
|
|
7283
7450
|
});
|
|
7284
7451
|
} catch (err) {
|
|
7285
|
-
const message =
|
|
7452
|
+
const message = formatQQBotError(err);
|
|
7286
7453
|
throw new Error(`QQBot group media send failed: ${message}`);
|
|
7287
7454
|
}
|
|
7288
7455
|
}
|
|
@@ -7291,10 +7458,12 @@ async function sendFileQQBot(params) {
|
|
|
7291
7458
|
accessToken,
|
|
7292
7459
|
openid: target.id,
|
|
7293
7460
|
fileInfo,
|
|
7294
|
-
|
|
7461
|
+
...messageText ? { content: messageText } : {},
|
|
7462
|
+
...messageId ? { messageId } : {},
|
|
7463
|
+
...eventId ? { eventId } : {}
|
|
7295
7464
|
});
|
|
7296
7465
|
} catch (err) {
|
|
7297
|
-
const message =
|
|
7466
|
+
const message = formatQQBotError(err);
|
|
7298
7467
|
throw new Error(`QQBot C2C media send failed: ${message}`);
|
|
7299
7468
|
}
|
|
7300
7469
|
}
|
|
@@ -7355,28 +7524,123 @@ function parseTarget(to) {
|
|
|
7355
7524
|
}
|
|
7356
7525
|
return { kind: "c2c", id: raw };
|
|
7357
7526
|
}
|
|
7527
|
+
function shortId(value) {
|
|
7528
|
+
const text = String(value ?? "").trim();
|
|
7529
|
+
if (!text) return "-";
|
|
7530
|
+
if (text.length <= 12) return text;
|
|
7531
|
+
return `${text.slice(0, 6)}...${text.slice(-4)}`;
|
|
7532
|
+
}
|
|
7533
|
+
function summarizeError(err) {
|
|
7534
|
+
if (err instanceof HttpError) {
|
|
7535
|
+
const body = err.body?.trim();
|
|
7536
|
+
return body ? `${err.message} - ${body}` : err.message;
|
|
7537
|
+
}
|
|
7538
|
+
return err instanceof Error ? err.message : String(err);
|
|
7539
|
+
}
|
|
7540
|
+
function logEventIdFallback(params) {
|
|
7541
|
+
const accountLabel = params.accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
|
7542
|
+
const detail = `[qqbot] event_id-fallback phase=${params.phase} action=${params.action} accountId=${accountLabel} target=${params.targetKind}:${shortId(params.targetId)} msg_id=${shortId(params.messageId)} event_id=${shortId(params.eventId)}` + (params.reason ? ` reason=${params.reason}` : "");
|
|
7543
|
+
if (params.phase === "failed") {
|
|
7544
|
+
console.error(detail);
|
|
7545
|
+
return;
|
|
7546
|
+
}
|
|
7547
|
+
if (params.phase === "start") {
|
|
7548
|
+
console.warn(detail);
|
|
7549
|
+
return;
|
|
7550
|
+
}
|
|
7551
|
+
console.info(detail);
|
|
7552
|
+
}
|
|
7553
|
+
function shouldRetryWithEventId(err) {
|
|
7554
|
+
const status = err instanceof HttpError ? err.status : void 0;
|
|
7555
|
+
let body = "";
|
|
7556
|
+
if (err instanceof HttpError) {
|
|
7557
|
+
body = err.body ?? "";
|
|
7558
|
+
} else if (err instanceof Error) {
|
|
7559
|
+
body = err.message;
|
|
7560
|
+
} else {
|
|
7561
|
+
body = String(err);
|
|
7562
|
+
}
|
|
7563
|
+
const text = body.toLowerCase();
|
|
7564
|
+
const mentionsPassiveReply = text.includes("msg_id") || text.includes("\u88AB\u52A8") || text.includes("passive") || text.includes("reply");
|
|
7565
|
+
if (!mentionsPassiveReply && !(typeof status === "number" && status >= 400 && status < 500)) {
|
|
7566
|
+
return false;
|
|
7567
|
+
}
|
|
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");
|
|
7569
|
+
}
|
|
7570
|
+
function shouldSendTextAsFollowupForMedia(mediaUrl) {
|
|
7571
|
+
return detectMediaType(stripTitleFromUrl(mediaUrl)) === "file";
|
|
7572
|
+
}
|
|
7358
7573
|
var qqbotOutbound = {
|
|
7359
7574
|
deliveryMode: "direct",
|
|
7360
7575
|
textChunkLimit: 1500,
|
|
7361
7576
|
chunkerMode: "markdown",
|
|
7362
7577
|
sendText: async (params) => {
|
|
7363
|
-
const { cfg, to, text, replyToId, accountId } = params;
|
|
7578
|
+
const { cfg, to, text, replyToId, replyEventId, accountId } = params;
|
|
7364
7579
|
const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
7365
|
-
|
|
7580
|
+
const credentials = resolveQQBotCredentials(qqCfg);
|
|
7581
|
+
if (!credentials) {
|
|
7366
7582
|
return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
|
|
7367
7583
|
}
|
|
7368
7584
|
const target = parseTarget(to);
|
|
7369
|
-
const accessToken = await getAccessToken(
|
|
7585
|
+
const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
|
|
7370
7586
|
const markdown = qqCfg.markdownSupport ?? true;
|
|
7587
|
+
const groupMarkdown = false;
|
|
7371
7588
|
try {
|
|
7372
7589
|
if (target.kind === "group") {
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7590
|
+
let result2;
|
|
7591
|
+
try {
|
|
7592
|
+
result2 = await sendGroupMessage({
|
|
7593
|
+
accessToken,
|
|
7594
|
+
groupOpenid: target.id,
|
|
7595
|
+
content: text,
|
|
7596
|
+
messageId: replyToId,
|
|
7597
|
+
markdown: groupMarkdown
|
|
7598
|
+
});
|
|
7599
|
+
} catch (err) {
|
|
7600
|
+
if (!replyToId || !replyEventId || !shouldRetryWithEventId(err)) {
|
|
7601
|
+
throw err;
|
|
7602
|
+
}
|
|
7603
|
+
logEventIdFallback({
|
|
7604
|
+
phase: "start",
|
|
7605
|
+
action: "text",
|
|
7606
|
+
accountId,
|
|
7607
|
+
targetKind: target.kind,
|
|
7608
|
+
targetId: target.id,
|
|
7609
|
+
messageId: replyToId,
|
|
7610
|
+
eventId: replyEventId,
|
|
7611
|
+
reason: summarizeError(err)
|
|
7612
|
+
});
|
|
7613
|
+
try {
|
|
7614
|
+
result2 = await sendGroupMessage({
|
|
7615
|
+
accessToken,
|
|
7616
|
+
groupOpenid: target.id,
|
|
7617
|
+
content: text,
|
|
7618
|
+
eventId: replyEventId,
|
|
7619
|
+
markdown: groupMarkdown
|
|
7620
|
+
});
|
|
7621
|
+
logEventIdFallback({
|
|
7622
|
+
phase: "success",
|
|
7623
|
+
action: "text",
|
|
7624
|
+
accountId,
|
|
7625
|
+
targetKind: target.kind,
|
|
7626
|
+
targetId: target.id,
|
|
7627
|
+
messageId: replyToId,
|
|
7628
|
+
eventId: replyEventId
|
|
7629
|
+
});
|
|
7630
|
+
} catch (retryErr) {
|
|
7631
|
+
logEventIdFallback({
|
|
7632
|
+
phase: "failed",
|
|
7633
|
+
action: "text",
|
|
7634
|
+
accountId,
|
|
7635
|
+
targetKind: target.kind,
|
|
7636
|
+
targetId: target.id,
|
|
7637
|
+
messageId: replyToId,
|
|
7638
|
+
eventId: replyEventId,
|
|
7639
|
+
reason: summarizeError(retryErr)
|
|
7640
|
+
});
|
|
7641
|
+
throw retryErr;
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7380
7644
|
return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
|
|
7381
7645
|
}
|
|
7382
7646
|
if (target.kind === "channel") {
|
|
@@ -7388,55 +7652,169 @@ var qqbotOutbound = {
|
|
|
7388
7652
|
});
|
|
7389
7653
|
return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
|
|
7390
7654
|
}
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7655
|
+
let result;
|
|
7656
|
+
try {
|
|
7657
|
+
result = await sendC2CMessage({
|
|
7658
|
+
accessToken,
|
|
7659
|
+
openid: target.id,
|
|
7660
|
+
content: text,
|
|
7661
|
+
messageId: replyToId,
|
|
7662
|
+
markdown
|
|
7663
|
+
});
|
|
7664
|
+
} catch (err) {
|
|
7665
|
+
if (!replyToId || !replyEventId || !shouldRetryWithEventId(err)) {
|
|
7666
|
+
throw err;
|
|
7667
|
+
}
|
|
7668
|
+
logEventIdFallback({
|
|
7669
|
+
phase: "start",
|
|
7670
|
+
action: "text",
|
|
7671
|
+
accountId,
|
|
7672
|
+
targetKind: target.kind,
|
|
7673
|
+
targetId: target.id,
|
|
7674
|
+
messageId: replyToId,
|
|
7675
|
+
eventId: replyEventId,
|
|
7676
|
+
reason: summarizeError(err)
|
|
7677
|
+
});
|
|
7678
|
+
try {
|
|
7679
|
+
result = await sendC2CMessage({
|
|
7680
|
+
accessToken,
|
|
7681
|
+
openid: target.id,
|
|
7682
|
+
content: text,
|
|
7683
|
+
eventId: replyEventId,
|
|
7684
|
+
markdown
|
|
7685
|
+
});
|
|
7686
|
+
logEventIdFallback({
|
|
7687
|
+
phase: "success",
|
|
7688
|
+
action: "text",
|
|
7689
|
+
accountId,
|
|
7690
|
+
targetKind: target.kind,
|
|
7691
|
+
targetId: target.id,
|
|
7692
|
+
messageId: replyToId,
|
|
7693
|
+
eventId: replyEventId
|
|
7694
|
+
});
|
|
7695
|
+
} catch (retryErr) {
|
|
7696
|
+
logEventIdFallback({
|
|
7697
|
+
phase: "failed",
|
|
7698
|
+
action: "text",
|
|
7699
|
+
accountId,
|
|
7700
|
+
targetKind: target.kind,
|
|
7701
|
+
targetId: target.id,
|
|
7702
|
+
messageId: replyToId,
|
|
7703
|
+
eventId: replyEventId,
|
|
7704
|
+
reason: summarizeError(retryErr)
|
|
7705
|
+
});
|
|
7706
|
+
throw retryErr;
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7398
7709
|
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
|
|
7399
7710
|
} catch (err) {
|
|
7400
|
-
const message =
|
|
7711
|
+
const message = summarizeError(err);
|
|
7401
7712
|
return { channel: "qqbot", error: message };
|
|
7402
7713
|
}
|
|
7403
7714
|
},
|
|
7404
7715
|
sendMedia: async (params) => {
|
|
7405
|
-
const { cfg, to, mediaUrl, text, replyToId, accountId } = params;
|
|
7716
|
+
const { cfg, to, mediaUrl, text, replyToId, replyEventId, accountId } = params;
|
|
7406
7717
|
if (!mediaUrl) {
|
|
7407
7718
|
const fallbackText = text?.trim() ?? "";
|
|
7408
7719
|
if (!fallbackText) {
|
|
7409
7720
|
return { channel: "qqbot", error: "mediaUrl is required for sendMedia" };
|
|
7410
7721
|
}
|
|
7411
|
-
return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId, accountId });
|
|
7722
|
+
return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId, replyEventId, accountId });
|
|
7412
7723
|
}
|
|
7413
7724
|
const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
7414
|
-
if (!qqCfg
|
|
7725
|
+
if (!resolveQQBotCredentials(qqCfg)) {
|
|
7415
7726
|
return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
|
|
7416
7727
|
}
|
|
7417
7728
|
const target = parseTarget(to);
|
|
7729
|
+
const trimmedText = text?.trim() ? text.trim() : void 0;
|
|
7730
|
+
const sendTextAsFollowup = trimmedText ? shouldSendTextAsFollowupForMedia(mediaUrl) : false;
|
|
7418
7731
|
if (target.kind === "channel") {
|
|
7419
|
-
const fallbackText =
|
|
7732
|
+
const fallbackText = trimmedText ? `${trimmedText}
|
|
7420
7733
|
${mediaUrl}` : mediaUrl;
|
|
7421
|
-
return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId });
|
|
7734
|
+
return qqbotOutbound.sendText({ cfg, to, text: fallbackText, replyToId, replyEventId, accountId });
|
|
7422
7735
|
}
|
|
7423
7736
|
try {
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7737
|
+
let result;
|
|
7738
|
+
try {
|
|
7739
|
+
result = await sendFileQQBot({
|
|
7740
|
+
cfg: qqCfg,
|
|
7741
|
+
target: { kind: target.kind, id: target.id },
|
|
7742
|
+
mediaUrl,
|
|
7743
|
+
text: sendTextAsFollowup ? void 0 : trimmedText,
|
|
7744
|
+
messageId: replyToId
|
|
7745
|
+
});
|
|
7746
|
+
} catch (err) {
|
|
7747
|
+
if (!replyToId || !replyEventId || !shouldRetryWithEventId(err)) {
|
|
7748
|
+
throw err;
|
|
7749
|
+
}
|
|
7750
|
+
logEventIdFallback({
|
|
7751
|
+
phase: "start",
|
|
7752
|
+
action: "media",
|
|
7753
|
+
accountId,
|
|
7754
|
+
targetKind: target.kind,
|
|
7755
|
+
targetId: target.id,
|
|
7756
|
+
messageId: replyToId,
|
|
7757
|
+
eventId: replyEventId,
|
|
7758
|
+
reason: summarizeError(err)
|
|
7759
|
+
});
|
|
7760
|
+
try {
|
|
7761
|
+
result = await sendFileQQBot({
|
|
7762
|
+
cfg: qqCfg,
|
|
7763
|
+
target: { kind: target.kind, id: target.id },
|
|
7764
|
+
mediaUrl,
|
|
7765
|
+
text: sendTextAsFollowup ? void 0 : trimmedText,
|
|
7766
|
+
eventId: replyEventId
|
|
7767
|
+
});
|
|
7768
|
+
logEventIdFallback({
|
|
7769
|
+
phase: "success",
|
|
7770
|
+
action: "media",
|
|
7771
|
+
accountId,
|
|
7772
|
+
targetKind: target.kind,
|
|
7773
|
+
targetId: target.id,
|
|
7774
|
+
messageId: replyToId,
|
|
7775
|
+
eventId: replyEventId
|
|
7776
|
+
});
|
|
7777
|
+
} catch (retryErr) {
|
|
7778
|
+
logEventIdFallback({
|
|
7779
|
+
phase: "failed",
|
|
7780
|
+
action: "media",
|
|
7781
|
+
accountId,
|
|
7782
|
+
targetKind: target.kind,
|
|
7783
|
+
targetId: target.id,
|
|
7784
|
+
messageId: replyToId,
|
|
7785
|
+
eventId: replyEventId,
|
|
7786
|
+
reason: summarizeError(retryErr)
|
|
7787
|
+
});
|
|
7788
|
+
throw retryErr;
|
|
7789
|
+
}
|
|
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
|
+
}
|
|
7430
7807
|
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
|
|
7431
7808
|
} catch (err) {
|
|
7432
|
-
const message =
|
|
7809
|
+
const message = summarizeError(err);
|
|
7433
7810
|
return { channel: "qqbot", error: message };
|
|
7434
7811
|
}
|
|
7435
7812
|
},
|
|
7436
7813
|
sendTyping: async (params) => {
|
|
7437
|
-
const { cfg, to, replyToId, inputSecond, accountId } = params;
|
|
7814
|
+
const { cfg, to, replyToId, replyEventId, inputSecond, accountId } = params;
|
|
7438
7815
|
const qqCfg = mergeQQBotAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
7439
|
-
|
|
7816
|
+
const credentials = resolveQQBotCredentials(qqCfg);
|
|
7817
|
+
if (!credentials) {
|
|
7440
7818
|
return { channel: "qqbot", error: "QQBot not configured (missing appId/clientSecret)" };
|
|
7441
7819
|
}
|
|
7442
7820
|
const target = parseTarget(to);
|
|
@@ -7444,16 +7822,62 @@ ${mediaUrl}` : mediaUrl;
|
|
|
7444
7822
|
return { channel: "qqbot" };
|
|
7445
7823
|
}
|
|
7446
7824
|
try {
|
|
7447
|
-
const accessToken = await getAccessToken(
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7825
|
+
const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
|
|
7826
|
+
try {
|
|
7827
|
+
await sendC2CInputNotify({
|
|
7828
|
+
accessToken,
|
|
7829
|
+
openid: target.id,
|
|
7830
|
+
messageId: replyToId,
|
|
7831
|
+
eventId: !replyToId ? replyEventId : void 0,
|
|
7832
|
+
inputSecond
|
|
7833
|
+
});
|
|
7834
|
+
} catch (err) {
|
|
7835
|
+
if (!replyToId || !replyEventId || !shouldRetryWithEventId(err)) {
|
|
7836
|
+
throw err;
|
|
7837
|
+
}
|
|
7838
|
+
logEventIdFallback({
|
|
7839
|
+
phase: "start",
|
|
7840
|
+
action: "typing",
|
|
7841
|
+
accountId,
|
|
7842
|
+
targetKind: target.kind,
|
|
7843
|
+
targetId: target.id,
|
|
7844
|
+
messageId: replyToId,
|
|
7845
|
+
eventId: replyEventId,
|
|
7846
|
+
reason: summarizeError(err)
|
|
7847
|
+
});
|
|
7848
|
+
try {
|
|
7849
|
+
await sendC2CInputNotify({
|
|
7850
|
+
accessToken,
|
|
7851
|
+
openid: target.id,
|
|
7852
|
+
eventId: replyEventId,
|
|
7853
|
+
inputSecond
|
|
7854
|
+
});
|
|
7855
|
+
logEventIdFallback({
|
|
7856
|
+
phase: "success",
|
|
7857
|
+
action: "typing",
|
|
7858
|
+
accountId,
|
|
7859
|
+
targetKind: target.kind,
|
|
7860
|
+
targetId: target.id,
|
|
7861
|
+
messageId: replyToId,
|
|
7862
|
+
eventId: replyEventId
|
|
7863
|
+
});
|
|
7864
|
+
} catch (retryErr) {
|
|
7865
|
+
logEventIdFallback({
|
|
7866
|
+
phase: "failed",
|
|
7867
|
+
action: "typing",
|
|
7868
|
+
accountId,
|
|
7869
|
+
targetKind: target.kind,
|
|
7870
|
+
targetId: target.id,
|
|
7871
|
+
messageId: replyToId,
|
|
7872
|
+
eventId: replyEventId,
|
|
7873
|
+
reason: summarizeError(retryErr)
|
|
7874
|
+
});
|
|
7875
|
+
throw retryErr;
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7454
7878
|
return { channel: "qqbot" };
|
|
7455
7879
|
} catch (err) {
|
|
7456
|
-
const message =
|
|
7880
|
+
const message = summarizeError(err);
|
|
7457
7881
|
return { channel: "qqbot", error: message };
|
|
7458
7882
|
}
|
|
7459
7883
|
}
|
|
@@ -7523,9 +7947,49 @@ function parseTextWithAttachments(payload) {
|
|
|
7523
7947
|
attachments
|
|
7524
7948
|
};
|
|
7525
7949
|
}
|
|
7950
|
+
function resolveEventId(payload, fallbackEventId) {
|
|
7951
|
+
return toString(payload.event_id) ?? toString(payload.eventId) ?? toString(fallbackEventId);
|
|
7952
|
+
}
|
|
7526
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";
|
|
7527
7954
|
var VOICE_EXTENSIONS = [".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex"];
|
|
7528
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
|
+
}
|
|
7529
7993
|
function isHttpUrl2(value) {
|
|
7530
7994
|
return /^https?:\/\//i.test(value);
|
|
7531
7995
|
}
|
|
@@ -7591,6 +8055,8 @@ async function resolveInboundAttachmentsForAgent(params) {
|
|
|
7591
8055
|
const maxFileSizeMB = qqCfg.maxFileSizeMB ?? 100;
|
|
7592
8056
|
const maxSize = Math.floor(maxFileSizeMB * 1024 * 1024);
|
|
7593
8057
|
const asrCredentials = resolveQQBotASRCredentials(qqCfg);
|
|
8058
|
+
const inboundMediaDir = resolveInboundMediaDir(qqCfg);
|
|
8059
|
+
const inboundMediaTempDir = resolveInboundMediaTempDir();
|
|
7594
8060
|
const resolved = [];
|
|
7595
8061
|
let hasVoiceAttachment = false;
|
|
7596
8062
|
let hasVoiceTranscript = false;
|
|
@@ -7603,11 +8069,19 @@ async function resolveInboundAttachmentsForAgent(params) {
|
|
|
7603
8069
|
timeout,
|
|
7604
8070
|
maxSize,
|
|
7605
8071
|
sourceFileName: att.filename,
|
|
7606
|
-
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
|
|
7607
8079
|
});
|
|
7608
|
-
next.localImagePath =
|
|
7609
|
-
logger.info(`inbound image cached: ${
|
|
7610
|
-
|
|
8080
|
+
next.localImagePath = finalPath;
|
|
8081
|
+
logger.info(`inbound image cached: ${finalPath}`);
|
|
8082
|
+
if (finalPath === downloaded.path) {
|
|
8083
|
+
scheduleTempCleanup(downloaded.path);
|
|
8084
|
+
}
|
|
7611
8085
|
} catch (err) {
|
|
7612
8086
|
logger.warn(`failed to download inbound attachment: ${String(err)}`);
|
|
7613
8087
|
}
|
|
@@ -7707,10 +8181,11 @@ function resolveInboundLogContent(params) {
|
|
|
7707
8181
|
function sanitizeInboundLogText(text) {
|
|
7708
8182
|
return text.replace(/\r?\n/g, "\\n");
|
|
7709
8183
|
}
|
|
7710
|
-
function parseC2CMessage(data) {
|
|
8184
|
+
function parseC2CMessage(data, fallbackEventId) {
|
|
7711
8185
|
const payload = data;
|
|
7712
8186
|
const { text, attachments } = parseTextWithAttachments(payload);
|
|
7713
8187
|
const id = toString(payload.id);
|
|
8188
|
+
const eventId = resolveEventId(payload, fallbackEventId);
|
|
7714
8189
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
7715
8190
|
const author = payload.author ?? {};
|
|
7716
8191
|
const senderId = toString(author.user_openid);
|
|
@@ -7723,14 +8198,16 @@ function parseC2CMessage(data) {
|
|
|
7723
8198
|
content: text,
|
|
7724
8199
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
7725
8200
|
messageId: id,
|
|
8201
|
+
eventId,
|
|
7726
8202
|
timestamp,
|
|
7727
8203
|
mentionedBot: false
|
|
7728
8204
|
};
|
|
7729
8205
|
}
|
|
7730
|
-
function parseGroupMessage(data) {
|
|
8206
|
+
function parseGroupMessage(data, fallbackEventId) {
|
|
7731
8207
|
const payload = data;
|
|
7732
8208
|
const { text, attachments } = parseTextWithAttachments(payload);
|
|
7733
8209
|
const id = toString(payload.id);
|
|
8210
|
+
const eventId = resolveEventId(payload, fallbackEventId);
|
|
7734
8211
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
7735
8212
|
const groupOpenid = toString(payload.group_openid);
|
|
7736
8213
|
const author = payload.author ?? {};
|
|
@@ -7743,15 +8220,17 @@ function parseGroupMessage(data) {
|
|
|
7743
8220
|
content: text,
|
|
7744
8221
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
7745
8222
|
messageId: id,
|
|
8223
|
+
eventId,
|
|
7746
8224
|
timestamp,
|
|
7747
8225
|
groupOpenid,
|
|
7748
8226
|
mentionedBot: true
|
|
7749
8227
|
};
|
|
7750
8228
|
}
|
|
7751
|
-
function parseChannelMessage(data) {
|
|
8229
|
+
function parseChannelMessage(data, fallbackEventId) {
|
|
7752
8230
|
const payload = data;
|
|
7753
8231
|
const { text, attachments } = parseTextWithAttachments(payload);
|
|
7754
8232
|
const id = toString(payload.id);
|
|
8233
|
+
const eventId = resolveEventId(payload, fallbackEventId);
|
|
7755
8234
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
7756
8235
|
const channelId = toString(payload.channel_id);
|
|
7757
8236
|
const guildId = toString(payload.guild_id);
|
|
@@ -7765,16 +8244,18 @@ function parseChannelMessage(data) {
|
|
|
7765
8244
|
content: text,
|
|
7766
8245
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
7767
8246
|
messageId: id,
|
|
8247
|
+
eventId,
|
|
7768
8248
|
timestamp,
|
|
7769
8249
|
channelId,
|
|
7770
8250
|
guildId,
|
|
7771
8251
|
mentionedBot: true
|
|
7772
8252
|
};
|
|
7773
8253
|
}
|
|
7774
|
-
function parseDirectMessage(data) {
|
|
8254
|
+
function parseDirectMessage(data, fallbackEventId) {
|
|
7775
8255
|
const payload = data;
|
|
7776
8256
|
const { text, attachments } = parseTextWithAttachments(payload);
|
|
7777
8257
|
const id = toString(payload.id);
|
|
8258
|
+
const eventId = resolveEventId(payload, fallbackEventId);
|
|
7778
8259
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
7779
8260
|
const guildId = toString(payload.guild_id);
|
|
7780
8261
|
const author = payload.author ?? {};
|
|
@@ -7787,39 +8268,42 @@ function parseDirectMessage(data) {
|
|
|
7787
8268
|
content: text,
|
|
7788
8269
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
7789
8270
|
messageId: id,
|
|
8271
|
+
eventId,
|
|
7790
8272
|
timestamp,
|
|
7791
8273
|
guildId,
|
|
7792
8274
|
mentionedBot: false
|
|
7793
8275
|
};
|
|
7794
8276
|
}
|
|
7795
|
-
function resolveInbound(eventType, data) {
|
|
8277
|
+
function resolveInbound(eventType, data, fallbackEventId) {
|
|
7796
8278
|
switch (eventType) {
|
|
7797
8279
|
case "C2C_MESSAGE_CREATE":
|
|
7798
|
-
return parseC2CMessage(data);
|
|
8280
|
+
return parseC2CMessage(data, fallbackEventId);
|
|
7799
8281
|
case "GROUP_AT_MESSAGE_CREATE":
|
|
7800
|
-
return parseGroupMessage(data);
|
|
8282
|
+
return parseGroupMessage(data, fallbackEventId);
|
|
7801
8283
|
case "AT_MESSAGE_CREATE":
|
|
7802
|
-
return parseChannelMessage(data);
|
|
8284
|
+
return parseChannelMessage(data, fallbackEventId);
|
|
7803
8285
|
case "DIRECT_MESSAGE_CREATE":
|
|
7804
|
-
return parseDirectMessage(data);
|
|
8286
|
+
return parseDirectMessage(data, fallbackEventId);
|
|
7805
8287
|
default:
|
|
7806
8288
|
return null;
|
|
7807
8289
|
}
|
|
7808
8290
|
}
|
|
7809
8291
|
function resolveChatTarget(event) {
|
|
7810
8292
|
if (event.type === "group") {
|
|
7811
|
-
const group =
|
|
8293
|
+
const group = event.groupOpenid ?? "";
|
|
8294
|
+
const normalizedGroup = group.toLowerCase();
|
|
7812
8295
|
return {
|
|
7813
8296
|
to: `group:${group}`,
|
|
7814
|
-
peerId: `group:${
|
|
8297
|
+
peerId: `group:${normalizedGroup}`,
|
|
7815
8298
|
peerKind: "group"
|
|
7816
8299
|
};
|
|
7817
8300
|
}
|
|
7818
8301
|
if (event.type === "channel") {
|
|
7819
|
-
const channel =
|
|
8302
|
+
const channel = event.channelId ?? "";
|
|
8303
|
+
const normalizedChannel = channel.toLowerCase();
|
|
7820
8304
|
return {
|
|
7821
8305
|
to: `channel:${channel}`,
|
|
7822
|
-
peerId: `channel:${
|
|
8306
|
+
peerId: `channel:${normalizedChannel}`,
|
|
7823
8307
|
peerKind: "group"
|
|
7824
8308
|
};
|
|
7825
8309
|
}
|
|
@@ -7856,7 +8340,7 @@ function extractLocalMediaFromText(params) {
|
|
|
7856
8340
|
parseBarePaths: true,
|
|
7857
8341
|
parseMarkdownLinks: true
|
|
7858
8342
|
});
|
|
7859
|
-
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);
|
|
7860
8344
|
return { text: result.text, mediaUrls };
|
|
7861
8345
|
}
|
|
7862
8346
|
function extractMediaLinesFromText(params) {
|
|
@@ -7880,24 +8364,16 @@ function extractMediaLinesFromText(params) {
|
|
|
7880
8364
|
const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
|
|
7881
8365
|
return { text: result.text, mediaUrls };
|
|
7882
8366
|
}
|
|
7883
|
-
function
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
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");
|
|
7887
|
-
}
|
|
7888
|
-
function buildMediaFallbackText(mediaUrl, errorMessage) {
|
|
7889
|
-
if (isOfficialQQFileSendLimit(errorMessage)) {
|
|
7890
|
-
return [
|
|
7891
|
-
"\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",
|
|
7892
|
-
"\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",
|
|
7893
|
-
`\u5DF2\u4E3A\u4F60\u9644\u4E0A\u6587\u4EF6\u94FE\u63A5\uFF1A${mediaUrl}`
|
|
7894
|
-
].join("\n");
|
|
8367
|
+
function buildMediaFallbackText(mediaUrl) {
|
|
8368
|
+
if (!/^https?:\/\//i.test(mediaUrl)) {
|
|
8369
|
+
return void 0;
|
|
7895
8370
|
}
|
|
7896
8371
|
return `\u{1F4CE} ${mediaUrl}`;
|
|
7897
8372
|
}
|
|
7898
8373
|
var THINK_BLOCK_RE = /<think\b[^>]*>[\s\S]*?<\/think>/gi;
|
|
7899
8374
|
var FINAL_BLOCK_RE = /<final\b[^>]*>([\s\S]*?)<\/final>/gi;
|
|
7900
8375
|
var RAW_THINK_OR_FINAL_TAG_RE = /<\/?(?:think|final)\b[^>]*>/gi;
|
|
8376
|
+
var FILE_PLACEHOLDER_RE = /\[文件:\s*[^\]\n]+\]/g;
|
|
7901
8377
|
var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_voice|tts(?::text)?|\/tts(?::text)?)\s*\]\]/gi;
|
|
7902
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;
|
|
7903
8379
|
var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
|
|
@@ -7915,6 +8391,7 @@ function sanitizeQQBotOutboundText(rawText) {
|
|
|
7915
8391
|
}
|
|
7916
8392
|
next = next.replace(THINK_BLOCK_RE, "");
|
|
7917
8393
|
next = next.replace(RAW_THINK_OR_FINAL_TAG_RE, "");
|
|
8394
|
+
next = next.replace(FILE_PLACEHOLDER_RE, " ");
|
|
7918
8395
|
next = next.replace(DIRECTIVE_TAG_RE, " ");
|
|
7919
8396
|
next = next.replace(VOICE_EMOTION_TAG_RE, " ");
|
|
7920
8397
|
next = next.replace(/[ \t]+\n/g, "\n");
|
|
@@ -7932,6 +8409,20 @@ function shouldSuppressQQBotTextWhenMediaPresent(rawText, sanitizedText) {
|
|
|
7932
8409
|
if (!sanitizedText) return true;
|
|
7933
8410
|
return !/[A-Za-z0-9\u4e00-\u9fff]/.test(sanitizedText);
|
|
7934
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
|
+
}
|
|
7935
8426
|
function evaluateReplyFinalOnlyDelivery(params) {
|
|
7936
8427
|
const { replyFinalOnly, kind, hasMedia } = params;
|
|
7937
8428
|
if (!replyFinalOnly || !kind || kind === "final") {
|
|
@@ -7943,27 +8434,38 @@ function evaluateReplyFinalOnlyDelivery(params) {
|
|
|
7943
8434
|
return { skipDelivery: true, suppressText: false };
|
|
7944
8435
|
}
|
|
7945
8436
|
async function sendQQBotMediaWithFallback(params) {
|
|
7946
|
-
const { qqCfg, to, mediaQueue, replyToId, logger } = params;
|
|
8437
|
+
const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger, onDelivered, onError } = params;
|
|
7947
8438
|
const outbound = params.outbound ?? qqbotOutbound;
|
|
7948
8439
|
for (const mediaUrl of mediaQueue) {
|
|
7949
8440
|
const result = await outbound.sendMedia({
|
|
7950
8441
|
cfg: { channels: { qqbot: qqCfg } },
|
|
7951
8442
|
to,
|
|
7952
8443
|
mediaUrl,
|
|
7953
|
-
replyToId
|
|
8444
|
+
replyToId,
|
|
8445
|
+
replyEventId
|
|
7954
8446
|
});
|
|
7955
8447
|
if (result.error) {
|
|
7956
8448
|
logger.error(`sendMedia failed: ${result.error}`);
|
|
7957
|
-
|
|
8449
|
+
onError?.(result.error);
|
|
8450
|
+
const fallback = buildMediaFallbackText(mediaUrl);
|
|
8451
|
+
if (!fallback) {
|
|
8452
|
+
continue;
|
|
8453
|
+
}
|
|
7958
8454
|
const fallbackResult = await outbound.sendText({
|
|
7959
8455
|
cfg: { channels: { qqbot: qqCfg } },
|
|
7960
8456
|
to,
|
|
7961
8457
|
text: fallback,
|
|
7962
|
-
replyToId
|
|
8458
|
+
replyToId,
|
|
8459
|
+
replyEventId
|
|
7963
8460
|
});
|
|
7964
8461
|
if (fallbackResult.error) {
|
|
7965
8462
|
logger.error(`sendText fallback failed: ${fallbackResult.error}`);
|
|
8463
|
+
onError?.(fallbackResult.error);
|
|
8464
|
+
} else {
|
|
8465
|
+
onDelivered?.();
|
|
7966
8466
|
}
|
|
8467
|
+
} else {
|
|
8468
|
+
onDelivered?.();
|
|
7967
8469
|
}
|
|
7968
8470
|
}
|
|
7969
8471
|
}
|
|
@@ -8010,6 +8512,7 @@ async function dispatchToAgent(params) {
|
|
|
8010
8512
|
cfg: { channels: { qqbot: qqCfg } },
|
|
8011
8513
|
to: `user:${inbound.c2cOpenid}`,
|
|
8012
8514
|
replyToId: inbound.messageId,
|
|
8515
|
+
replyEventId: inbound.eventId,
|
|
8013
8516
|
inputSecond: 60
|
|
8014
8517
|
});
|
|
8015
8518
|
if (typing.error) {
|
|
@@ -8027,234 +8530,319 @@ async function dispatchToAgent(params) {
|
|
|
8027
8530
|
logger.warn("reply API not available");
|
|
8028
8531
|
return;
|
|
8029
8532
|
}
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
const
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
logger
|
|
8041
|
-
});
|
|
8042
|
-
if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
|
|
8043
|
-
const fallback = await qqbotOutbound.sendText({
|
|
8044
|
-
cfg: { channels: { qqbot: qqCfg } },
|
|
8045
|
-
to: target.to,
|
|
8046
|
-
text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
|
|
8047
|
-
replyToId: inbound.messageId
|
|
8048
|
-
});
|
|
8049
|
-
if (fallback.error) {
|
|
8050
|
-
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");
|
|
8051
8543
|
}
|
|
8052
|
-
|
|
8053
|
-
}
|
|
8054
|
-
const
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
channel: "QQ",
|
|
8066
|
-
from: envelopeFrom,
|
|
8067
|
-
body: rawBody,
|
|
8068
|
-
timestamp: inbound.timestamp,
|
|
8069
|
-
previousTimestamp: previousTimestamp ?? void 0,
|
|
8070
|
-
chatType: inbound.type === "direct" ? "direct" : "group",
|
|
8071
|
-
senderLabel: inbound.senderName ?? inbound.senderId,
|
|
8072
|
-
sender: { id: inbound.senderId, name: inbound.senderName ?? void 0 },
|
|
8073
|
-
envelope: envelopeOptions
|
|
8074
|
-
}) : replyApi.formatAgentEnvelope ? replyApi.formatAgentEnvelope({
|
|
8075
|
-
channel: "QQ",
|
|
8076
|
-
from: envelopeFrom,
|
|
8077
|
-
timestamp: inbound.timestamp,
|
|
8078
|
-
previousTimestamp: previousTimestamp ?? void 0,
|
|
8079
|
-
envelope: envelopeOptions,
|
|
8080
|
-
body: rawBody
|
|
8081
|
-
}) : rawBody;
|
|
8082
|
-
const inboundCtx = buildInboundContext({
|
|
8083
|
-
event: inbound,
|
|
8084
|
-
sessionKey: route.sessionKey,
|
|
8085
|
-
accountId: route.accountId ?? accountId,
|
|
8086
|
-
body: inboundBody,
|
|
8087
|
-
rawBody,
|
|
8088
|
-
commandBody: rawBody
|
|
8089
|
-
});
|
|
8090
|
-
const finalizeInboundContext = replyApi?.finalizeInboundContext;
|
|
8091
|
-
const finalCtx = finalizeInboundContext ? finalizeInboundContext(inboundCtx) : inboundCtx;
|
|
8092
|
-
let cronBase = "";
|
|
8093
|
-
if (typeof finalCtx.RawBody === "string" && finalCtx.RawBody) {
|
|
8094
|
-
cronBase = finalCtx.RawBody;
|
|
8095
|
-
} else if (typeof finalCtx.Body === "string" && finalCtx.Body) {
|
|
8096
|
-
cronBase = finalCtx.Body;
|
|
8097
|
-
} else if (typeof finalCtx.CommandBody === "string" && finalCtx.CommandBody) {
|
|
8098
|
-
cronBase = finalCtx.CommandBody;
|
|
8099
|
-
}
|
|
8100
|
-
if (cronBase) {
|
|
8101
|
-
const nextCron = appendCronHiddenPrompt(cronBase);
|
|
8102
|
-
if (nextCron !== cronBase) {
|
|
8103
|
-
finalCtx.BodyForAgent = nextCron;
|
|
8104
|
-
}
|
|
8105
|
-
}
|
|
8106
|
-
if (storePath && sessionApi?.recordInboundSession) {
|
|
8107
|
-
try {
|
|
8108
|
-
const mainSessionKeyRaw = route?.mainSessionKey;
|
|
8109
|
-
const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw : void 0;
|
|
8110
|
-
const isGroup = inbound.type === "group" || inbound.type === "channel";
|
|
8111
|
-
const updateLastRoute = !isGroup ? {
|
|
8112
|
-
sessionKey: mainSessionKey ?? route.sessionKey,
|
|
8113
|
-
channel: "qqbot",
|
|
8114
|
-
to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${inbound.senderId}`,
|
|
8115
|
-
accountId: route.accountId ?? accountId
|
|
8116
|
-
} : void 0;
|
|
8117
|
-
const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : route.sessionKey;
|
|
8118
|
-
await sessionApi.recordInboundSession({
|
|
8119
|
-
storePath,
|
|
8120
|
-
sessionKey: recordSessionKey,
|
|
8121
|
-
ctx: finalCtx,
|
|
8122
|
-
updateLastRoute,
|
|
8123
|
-
onRecordError: (err) => {
|
|
8124
|
-
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
8125
|
-
}
|
|
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
|
|
8126
8557
|
});
|
|
8127
|
-
|
|
8128
|
-
|
|
8558
|
+
if (result.error) {
|
|
8559
|
+
logger.warn(`send long-task notice failed: ${result.error}`);
|
|
8560
|
+
markGroupMessageInterfaceBlocked(result.error);
|
|
8561
|
+
} else {
|
|
8562
|
+
replyDelivered = true;
|
|
8563
|
+
}
|
|
8129
8564
|
}
|
|
8130
|
-
}
|
|
8131
|
-
const textApi = runtime2.channel?.text;
|
|
8132
|
-
const limit = textApi?.resolveTextChunkLimit?.({
|
|
8133
|
-
cfg,
|
|
8134
|
-
channel: "qqbot",
|
|
8135
|
-
defaultLimit: qqCfg.textChunkLimit ?? 1500
|
|
8136
|
-
}) ?? (qqCfg.textChunkLimit ?? 1500);
|
|
8137
|
-
const chunkMode = textApi?.resolveChunkMode?.(cfg, "qqbot");
|
|
8138
|
-
const tableMode = textApi?.resolveMarkdownTableMode?.({
|
|
8139
|
-
cfg,
|
|
8140
|
-
channel: "qqbot",
|
|
8141
|
-
accountId: route.accountId ?? accountId
|
|
8142
8565
|
});
|
|
8143
|
-
const
|
|
8144
|
-
const
|
|
8145
|
-
|
|
8146
|
-
|
|
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;
|
|
8147
8597
|
}
|
|
8148
|
-
|
|
8149
|
-
|
|
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`);
|
|
8150
8602
|
}
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
const deliver = async (payload, info) => {
|
|
8155
|
-
const typed = payload;
|
|
8156
|
-
const mediaLineResult = extractMediaLinesFromText({
|
|
8157
|
-
text: typed?.text ?? "",
|
|
8158
|
-
logger
|
|
8603
|
+
const rawBody = buildInboundContentWithAttachments({
|
|
8604
|
+
content: inbound.content,
|
|
8605
|
+
attachments: resolvedAttachments
|
|
8159
8606
|
});
|
|
8160
|
-
const
|
|
8161
|
-
|
|
8162
|
-
|
|
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
|
|
8163
8633
|
});
|
|
8164
|
-
const
|
|
8165
|
-
const
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
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
|
+
}
|
|
8195
8670
|
});
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
}
|
|
8671
|
+
} catch (err) {
|
|
8672
|
+
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
8199
8673
|
}
|
|
8200
8674
|
}
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
|
|
8210
|
-
const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
|
|
8211
|
-
if (dispatchBuffered) {
|
|
8212
|
-
await dispatchBuffered({
|
|
8213
|
-
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?.({
|
|
8214
8683
|
cfg,
|
|
8215
|
-
|
|
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({
|
|
8216
8787
|
deliver,
|
|
8217
8788
|
humanDelay,
|
|
8218
8789
|
onError: (err, info) => {
|
|
8219
8790
|
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
8220
|
-
},
|
|
8221
|
-
onSkip: (_payload, info) => {
|
|
8222
|
-
if (info.reason !== "silent") {
|
|
8223
|
-
logger.info(`reply skipped: ${info.reason}`);
|
|
8224
|
-
}
|
|
8225
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;
|
|
8226
8806
|
}
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
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?.();
|
|
8235
8814
|
}
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
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();
|
|
8242
8833
|
}
|
|
8243
|
-
}
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
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
|
+
}
|
|
8250
8845
|
}
|
|
8251
|
-
await replyApi.dispatchReplyFromConfig({
|
|
8252
|
-
ctx: finalCtx,
|
|
8253
|
-
cfg,
|
|
8254
|
-
dispatcher: dispatcherResult.dispatcher,
|
|
8255
|
-
replyOptions: dispatcherResult.replyOptions
|
|
8256
|
-
});
|
|
8257
|
-
dispatcherResult.markDispatchIdle?.();
|
|
8258
8846
|
}
|
|
8259
8847
|
function shouldHandleMessage(event, qqCfg, logger) {
|
|
8260
8848
|
if (event.type === "direct") {
|
|
@@ -8287,7 +8875,7 @@ function shouldHandleMessage(event, qqCfg, logger) {
|
|
|
8287
8875
|
}
|
|
8288
8876
|
async function handleQQBotDispatch(params) {
|
|
8289
8877
|
const logger = params.logger ?? createLogger("qqbot");
|
|
8290
|
-
const inbound = resolveInbound(params.eventType, params.eventData);
|
|
8878
|
+
const inbound = resolveInbound(params.eventType, params.eventData, params.eventId);
|
|
8291
8879
|
if (!inbound) {
|
|
8292
8880
|
return;
|
|
8293
8881
|
}
|
|
@@ -8336,6 +8924,16 @@ var INTENTS = {
|
|
|
8336
8924
|
};
|
|
8337
8925
|
var DEFAULT_INTENTS = INTENTS.GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C;
|
|
8338
8926
|
var RECONNECT_DELAYS_MS = [1e3, 2e3, 5e3, 1e4, 2e4, 3e4];
|
|
8927
|
+
function formatGatewayConnectError(err) {
|
|
8928
|
+
if (err instanceof HttpError) {
|
|
8929
|
+
const body = err.body?.trim();
|
|
8930
|
+
if (body) {
|
|
8931
|
+
return `${err.message}; body=${body}`;
|
|
8932
|
+
}
|
|
8933
|
+
return err.message;
|
|
8934
|
+
}
|
|
8935
|
+
return String(err);
|
|
8936
|
+
}
|
|
8339
8937
|
var activeConnections = /* @__PURE__ */ new Map();
|
|
8340
8938
|
function getOrCreateConnection(accountId) {
|
|
8341
8939
|
let conn = activeConnections.get(accountId);
|
|
@@ -8378,7 +8976,7 @@ function cleanupSocket(conn) {
|
|
|
8378
8976
|
}
|
|
8379
8977
|
}
|
|
8380
8978
|
async function monitorQQBotProvider(opts = {}) {
|
|
8381
|
-
const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID } = opts;
|
|
8979
|
+
const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID, setStatus } = opts;
|
|
8382
8980
|
const logger = createLogger("qqbot", {
|
|
8383
8981
|
log: runtime2?.log,
|
|
8384
8982
|
error: runtime2?.error
|
|
@@ -8485,6 +9083,7 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
8485
9083
|
return;
|
|
8486
9084
|
}
|
|
8487
9085
|
case 11:
|
|
9086
|
+
setStatus?.({ lastEventAt: Date.now() });
|
|
8488
9087
|
return;
|
|
8489
9088
|
case 7:
|
|
8490
9089
|
cleanupSocket(conn);
|
|
@@ -8517,6 +9116,7 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
8517
9116
|
await handleQQBotDispatch({
|
|
8518
9117
|
eventType,
|
|
8519
9118
|
eventData: payload.d,
|
|
9119
|
+
eventId: payload.id,
|
|
8520
9120
|
cfg: opts.config,
|
|
8521
9121
|
accountId,
|
|
8522
9122
|
logger
|
|
@@ -8563,7 +9163,7 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
8563
9163
|
logger.error(`gateway socket error: ${String(err)}`);
|
|
8564
9164
|
});
|
|
8565
9165
|
} catch (err) {
|
|
8566
|
-
logger.error(`gateway connect failed: ${
|
|
9166
|
+
logger.error(`gateway connect failed: ${formatGatewayConnectError(err)}`);
|
|
8567
9167
|
cleanupSocket(conn);
|
|
8568
9168
|
scheduleReconnect("connect failed");
|
|
8569
9169
|
} finally {
|
|
@@ -8688,14 +9288,14 @@ var qqbotPlugin = {
|
|
|
8688
9288
|
enabled: { type: "boolean" },
|
|
8689
9289
|
name: { type: "string" },
|
|
8690
9290
|
defaultAccount: { type: "string" },
|
|
8691
|
-
appId: { type: "string" },
|
|
9291
|
+
appId: { type: ["string", "number"] },
|
|
8692
9292
|
clientSecret: { type: "string" },
|
|
8693
9293
|
asr: {
|
|
8694
9294
|
type: "object",
|
|
8695
9295
|
additionalProperties: false,
|
|
8696
9296
|
properties: {
|
|
8697
9297
|
enabled: { type: "boolean" },
|
|
8698
|
-
appId: { type: "string" },
|
|
9298
|
+
appId: { type: ["string", "number"] },
|
|
8699
9299
|
secretId: { type: "string" },
|
|
8700
9300
|
secretKey: { type: "string" }
|
|
8701
9301
|
}
|
|
@@ -8709,8 +9309,17 @@ var qqbotPlugin = {
|
|
|
8709
9309
|
historyLimit: { type: "integer", minimum: 0 },
|
|
8710
9310
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
8711
9311
|
replyFinalOnly: { type: "boolean" },
|
|
9312
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
8712
9313
|
maxFileSizeMB: { type: "number" },
|
|
8713
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
|
+
},
|
|
8714
9323
|
accounts: {
|
|
8715
9324
|
type: "object",
|
|
8716
9325
|
additionalProperties: {
|
|
@@ -8719,14 +9328,14 @@ var qqbotPlugin = {
|
|
|
8719
9328
|
properties: {
|
|
8720
9329
|
name: { type: "string" },
|
|
8721
9330
|
enabled: { type: "boolean" },
|
|
8722
|
-
appId: { type: "string" },
|
|
9331
|
+
appId: { type: ["string", "number"] },
|
|
8723
9332
|
clientSecret: { type: "string" },
|
|
8724
9333
|
asr: {
|
|
8725
9334
|
type: "object",
|
|
8726
9335
|
additionalProperties: false,
|
|
8727
9336
|
properties: {
|
|
8728
9337
|
enabled: { type: "boolean" },
|
|
8729
|
-
appId: { type: "string" },
|
|
9338
|
+
appId: { type: ["string", "number"] },
|
|
8730
9339
|
secretId: { type: "string" },
|
|
8731
9340
|
secretKey: { type: "string" }
|
|
8732
9341
|
}
|
|
@@ -8740,8 +9349,17 @@ var qqbotPlugin = {
|
|
|
8740
9349
|
historyLimit: { type: "integer", minimum: 0 },
|
|
8741
9350
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
8742
9351
|
replyFinalOnly: { type: "boolean" },
|
|
9352
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
8743
9353
|
maxFileSizeMB: { type: "number" },
|
|
8744
|
-
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
|
+
}
|
|
8745
9363
|
}
|
|
8746
9364
|
}
|
|
8747
9365
|
}
|
|
@@ -8883,7 +9501,8 @@ var qqbotPlugin = {
|
|
|
8883
9501
|
error: ctx.log?.error ?? console.error
|
|
8884
9502
|
},
|
|
8885
9503
|
abortSignal: ctx.abortSignal,
|
|
8886
|
-
accountId: ctx.accountId
|
|
9504
|
+
accountId: ctx.accountId,
|
|
9505
|
+
setStatus: ctx.setStatus
|
|
8887
9506
|
});
|
|
8888
9507
|
},
|
|
8889
9508
|
stopAccount: async (ctx) => {
|
|
@@ -8905,14 +9524,14 @@ var plugin = {
|
|
|
8905
9524
|
enabled: { type: "boolean" },
|
|
8906
9525
|
name: { type: "string" },
|
|
8907
9526
|
defaultAccount: { type: "string" },
|
|
8908
|
-
appId: { type: "string" },
|
|
9527
|
+
appId: { type: ["string", "number"] },
|
|
8909
9528
|
clientSecret: { type: "string" },
|
|
8910
9529
|
asr: {
|
|
8911
9530
|
type: "object",
|
|
8912
9531
|
additionalProperties: false,
|
|
8913
9532
|
properties: {
|
|
8914
9533
|
enabled: { type: "boolean" },
|
|
8915
|
-
appId: { type: "string" },
|
|
9534
|
+
appId: { type: ["string", "number"] },
|
|
8916
9535
|
secretId: { type: "string" },
|
|
8917
9536
|
secretKey: { type: "string" }
|
|
8918
9537
|
}
|
|
@@ -8926,8 +9545,17 @@ var plugin = {
|
|
|
8926
9545
|
historyLimit: { type: "integer", minimum: 0 },
|
|
8927
9546
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
8928
9547
|
replyFinalOnly: { type: "boolean" },
|
|
9548
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
8929
9549
|
maxFileSizeMB: { type: "number" },
|
|
8930
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
|
+
},
|
|
8931
9559
|
accounts: {
|
|
8932
9560
|
type: "object",
|
|
8933
9561
|
additionalProperties: {
|
|
@@ -8936,14 +9564,14 @@ var plugin = {
|
|
|
8936
9564
|
properties: {
|
|
8937
9565
|
name: { type: "string" },
|
|
8938
9566
|
enabled: { type: "boolean" },
|
|
8939
|
-
appId: { type: "string" },
|
|
9567
|
+
appId: { type: ["string", "number"] },
|
|
8940
9568
|
clientSecret: { type: "string" },
|
|
8941
9569
|
asr: {
|
|
8942
9570
|
type: "object",
|
|
8943
9571
|
additionalProperties: false,
|
|
8944
9572
|
properties: {
|
|
8945
9573
|
enabled: { type: "boolean" },
|
|
8946
|
-
appId: { type: "string" },
|
|
9574
|
+
appId: { type: ["string", "number"] },
|
|
8947
9575
|
secretId: { type: "string" },
|
|
8948
9576
|
secretKey: { type: "string" }
|
|
8949
9577
|
}
|
|
@@ -8957,8 +9585,17 @@ var plugin = {
|
|
|
8957
9585
|
historyLimit: { type: "integer", minimum: 0 },
|
|
8958
9586
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
8959
9587
|
replyFinalOnly: { type: "boolean" },
|
|
9588
|
+
longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
|
|
8960
9589
|
maxFileSizeMB: { type: "number" },
|
|
8961
|
-
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
|
+
}
|
|
8962
9599
|
}
|
|
8963
9600
|
}
|
|
8964
9601
|
}
|