@openclaw-china/qqbot 2026.3.5 → 2026.3.7

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