@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.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';
@@ -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
- if (!config?.appId || !config?.clientSecret) return void 0;
4282
- return { appId: config.appId, clientSecret: config.clientSecret };
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
- if (!asr.appId || !asr.secretId || !asr.secretKey) return void 0;
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: asr.appId,
4290
- secretId: asr.secretId,
4291
- secretKey: asr.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 = path.join(os.homedir(), p.slice(1));
4660
+ p = path2.join(os.homedir(), p.slice(1));
4637
4661
  } else if (p.startsWith("~")) ;
4638
- if (!path.isAbsolute(p)) {
4639
- p = path.resolve(process.cwd(), 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 = path.extname(filePath).toLowerCase();
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 = path.basename(localPath);
4699
+ fileName = path2.basename(localPath);
4676
4700
  } else if (isHttp) {
4677
4701
  try {
4678
4702
  const url = new URL(cleanSource);
4679
- fileName = path.basename(url.pathname) || void 0;
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 || path.basename(rawPath);
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 || path.basename(rawPath);
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 = path.basename(parsed.pathname);
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 = path.normalize(filePath);
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 = path.normalize(filePath);
5076
+ const normalizedPath = path2.normalize(filePath);
5039
5077
  const isAllowed = allowedPrefixes.some(
5040
- (prefix) => normalizedPath.startsWith(path.normalize(prefix))
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 = path.basename(urlPath) || "file";
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 = path.join(tempDir, fileName);
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 = path.basename(localPath);
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, use the main session's last route as the delivery target.
5217
- - Do not ask the user for userId/chatId
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.deliver=true
5221
- - Do not set payload.channel/to (fall back to last route automatically)
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 toTrimmedString(value) {
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 = toTrimmedString(channelCfg.defaultAccount);
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: toTrimmedString(existing.clientId),
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: toTrimmedString(existing.clientSecret),
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: toTrimmedString(existing.appId),
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: toTrimmedString(existing.appSecret),
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: toTrimmedString(existing.webhookPath) ?? "/wecom",
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: toTrimmedString(existing.token),
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: toTrimmedString(existing.encodingAESKey),
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: toTrimmedString(existing.webhookPath) ?? "/wecom-app",
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: toTrimmedString(existing.token),
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: toTrimmedString(existing.encodingAESKey),
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: toTrimmedString(existing.corpId),
6755
+ defaultValue: toTrimmedString2(existing.corpId),
6646
6756
  required: true
6647
6757
  });
6648
6758
  const corpSecret = await prompter.askSecret({
6649
6759
  label: "corpSecret",
6650
- existingValue: toTrimmedString(existing.corpSecret),
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: toTrimmedString(existingAsr.appId),
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: toTrimmedString(existingAsr.secretId),
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: toTrimmedString(existingAsr.secretKey),
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: toTrimmedString(existing.appId),
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: toTrimmedString(existing.clientSecret),
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: toTrimmedString(existingAsr.appId),
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: toTrimmedString(existingAsr.secretId),
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: toTrimmedString(existingAsr.secretKey),
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 nextMsgSeq(messageId) {
6961
- if (!messageId) return MSG_SEQ_BASE + 1;
6962
- const current = msgSeqMap.get(messageId) ?? 0;
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(messageId, next);
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
- if (appId) {
6975
- tokenCacheMap.delete(appId);
6976
- tokenPromiseMap.delete(appId);
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 cached = tokenCacheMap.get(appId);
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(appId);
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(appId, {
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(appId);
7138
+ tokenPromiseMap.delete(normalizedAppId);
7008
7139
  }
7009
7140
  })();
7010
- tokenPromiseMap.set(appId, promise);
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
- const groupOpenidLower = params.groupOpenid.toLowerCase();
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
- const channelIdLower = params.channelId.toLowerCase();
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(path.join(os.tmpdir(), "qqbot-silk-"));
7204
- 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");
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
- if (!cfg.appId || !cfg.clientSecret) {
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 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);
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
- messageId
7447
+ ...messageText ? { content: messageText } : {},
7448
+ ...messageId ? { messageId } : {},
7449
+ ...eventId ? { eventId } : {}
7283
7450
  });
7284
7451
  } catch (err) {
7285
- const message = err instanceof Error ? err.message : String(err);
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
- messageId
7461
+ ...messageText ? { content: messageText } : {},
7462
+ ...messageId ? { messageId } : {},
7463
+ ...eventId ? { eventId } : {}
7295
7464
  });
7296
7465
  } catch (err) {
7297
- const message = err instanceof Error ? err.message : String(err);
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
- if (!qqCfg.appId || !qqCfg.clientSecret) {
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(qqCfg.appId, qqCfg.clientSecret);
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
- const result2 = await sendGroupMessage({
7374
- accessToken,
7375
- groupOpenid: target.id,
7376
- content: text,
7377
- messageId: replyToId,
7378
- markdown
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
- const result = await sendC2CMessage({
7392
- accessToken,
7393
- openid: target.id,
7394
- content: text,
7395
- messageId: replyToId,
7396
- markdown
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 = err instanceof Error ? err.message : String(err);
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.appId || !qqCfg.clientSecret) {
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 = text?.trim() ? `${text}
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
- const result = await sendFileQQBot({
7425
- cfg: qqCfg,
7426
- target: { kind: target.kind, id: target.id },
7427
- mediaUrl,
7428
- messageId: replyToId
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 = err instanceof Error ? err.message : String(err);
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
- if (!qqCfg.appId || !qqCfg.clientSecret) {
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(qqCfg.appId, qqCfg.clientSecret);
7448
- await sendC2CInputNotify({
7449
- accessToken,
7450
- openid: target.id,
7451
- messageId: replyToId,
7452
- inputSecond
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 = err instanceof Error ? err.message : String(err);
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 = downloaded.path;
7609
- logger.info(`inbound image cached: ${downloaded.path}`);
7610
- scheduleTempCleanup(downloaded.path);
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 = (event.groupOpenid ?? "").toLowerCase();
8293
+ const group = event.groupOpenid ?? "";
8294
+ const normalizedGroup = group.toLowerCase();
7812
8295
  return {
7813
8296
  to: `group:${group}`,
7814
- peerId: `group:${group}`,
8297
+ peerId: `group:${normalizedGroup}`,
7815
8298
  peerKind: "group"
7816
8299
  };
7817
8300
  }
7818
8301
  if (event.type === "channel") {
7819
- const channel = (event.channelId ?? "").toLowerCase();
8302
+ const channel = event.channelId ?? "";
8303
+ const normalizedChannel = channel.toLowerCase();
7820
8304
  return {
7821
8305
  to: `channel:${channel}`,
7822
- peerId: `channel:${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 isOfficialQQFileSendLimit(errorMessage) {
7884
- const text = (errorMessage ?? "").toLowerCase();
7885
- if (!text) return false;
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
- const fallback = buildMediaFallbackText(mediaUrl, result.error);
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
- const sessionApi = runtime2.channel?.session;
8031
- const storePath = sessionApi?.resolveStorePath?.(
8032
- cfg?.session?.store,
8033
- { agentId: route.agentId }
8034
- );
8035
- const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
8036
- const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
8037
- const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
8038
- attachments: inbound.attachments,
8039
- qqCfg,
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
- return;
8053
- }
8054
- const resolvedAttachments = resolvedAttachmentResult.attachments;
8055
- const localImageCount = resolvedAttachments.filter((item) => Boolean(item.localImagePath)).length;
8056
- if (localImageCount > 0) {
8057
- logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
8058
- }
8059
- const rawBody = buildInboundContentWithAttachments({
8060
- content: inbound.content,
8061
- attachments: resolvedAttachments
8062
- });
8063
- const envelopeFrom = resolveEnvelopeFrom(inbound);
8064
- const inboundBody = replyApi.formatInboundEnvelope ? replyApi.formatInboundEnvelope({
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
- } catch (err) {
8128
- 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
+ }
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 resolvedTableMode = tableMode ?? "bullets";
8144
- const chunkText = (text) => {
8145
- if (textApi?.chunkMarkdownText && limit > 0) {
8146
- 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;
8147
8597
  }
8148
- if (textApi?.chunkTextWithMode && limit > 0) {
8149
- 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`);
8150
8602
  }
8151
- return [text];
8152
- };
8153
- const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
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 localMediaResult = extractLocalMediaFromText({
8161
- text: mediaLineResult.text,
8162
- 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
8163
8633
  });
8164
- const cleanedText = sanitizeQQBotOutboundText(localMediaResult.text);
8165
- const payloadMediaUrls = Array.isArray(typed?.mediaUrls) ? typed?.mediaUrls : typed?.mediaUrl ? [typed.mediaUrl] : [];
8166
- const mediaQueue = [];
8167
- const seenMedia = /* @__PURE__ */ new Set();
8168
- const addMedia = (value) => {
8169
- const next = value?.trim();
8170
- if (!next) return;
8171
- if (seenMedia.has(next)) return;
8172
- seenMedia.add(next);
8173
- mediaQueue.push(next);
8174
- };
8175
- for (const url of payloadMediaUrls) addMedia(url);
8176
- for (const url of mediaLineResult.mediaUrls) addMedia(url);
8177
- for (const url of localMediaResult.mediaUrls) addMedia(url);
8178
- const deliveryDecision = evaluateReplyFinalOnlyDelivery({
8179
- replyFinalOnly,
8180
- kind: info?.kind,
8181
- hasMedia: mediaQueue.length > 0});
8182
- if (deliveryDecision.skipDelivery) return;
8183
- const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(localMediaResult.text, cleanedText);
8184
- const suppressText = deliveryDecision.suppressText || suppressEchoText;
8185
- const textToSend = suppressText ? "" : cleanedText;
8186
- if (textToSend) {
8187
- const converted = textApi?.convertMarkdownTables ? textApi.convertMarkdownTables(textToSend, resolvedTableMode) : textToSend;
8188
- const chunks = chunkText(converted);
8189
- for (const chunk of chunks) {
8190
- const result = await qqbotOutbound.sendText({
8191
- cfg: { channels: { qqbot: qqCfg } },
8192
- to: target.to,
8193
- text: chunk,
8194
- replyToId: inbound.messageId
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
- if (result.error) {
8197
- logger.error(`sendText failed: ${result.error}`);
8198
- }
8671
+ } catch (err) {
8672
+ logger.warn(`failed to record inbound session: ${String(err)}`);
8199
8673
  }
8200
8674
  }
8201
- await sendQQBotMediaWithFallback({
8202
- qqCfg,
8203
- to: target.to,
8204
- mediaQueue,
8205
- replyToId: inbound.messageId,
8206
- logger
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
- 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({
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
- return;
8229
- }
8230
- const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
8231
- deliver,
8232
- humanDelay,
8233
- onError: (err, info) => {
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
- dispatcher: replyApi.createReplyDispatcher?.({
8238
- deliver,
8239
- humanDelay,
8240
- onError: (err, info) => {
8241
- 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();
8242
8833
  }
8243
- }),
8244
- replyOptions: {},
8245
- markDispatchIdle: () => void 0
8246
- };
8247
- if (!dispatcherResult.dispatcher || !replyApi.dispatchReplyFromConfig) {
8248
- logger.warn("dispatcher not available, skipping reply");
8249
- 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
+ }
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: ${String(err)}`);
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
  }