@openclaw-china/qqbot 2026.3.11 → 2026.3.16

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
@@ -3,7 +3,7 @@ import { homedir, tmpdir } from 'os';
3
3
  import * as path2 from 'path';
4
4
  import { join, dirname } from 'path';
5
5
  import * as fs3 from 'fs';
6
- import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
6
+ import { existsSync, readFileSync, rmSync, writeFileSync, renameSync, copyFileSync, mkdirSync, appendFileSync } from 'fs';
7
7
  import { fileURLToPath } from 'url';
8
8
  import * as fsPromises from 'fs/promises';
9
9
  import { createHmac } from 'crypto';
@@ -651,8 +651,8 @@ function getErrorMap() {
651
651
 
652
652
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
653
653
  var makeIssue = (params) => {
654
- const { data, path: path4, errorMaps, issueData } = params;
655
- const fullPath = [...path4, ...issueData.path || []];
654
+ const { data, path: path5, errorMaps, issueData } = params;
655
+ const fullPath = [...path5, ...issueData.path || []];
656
656
  const fullIssue = {
657
657
  ...issueData,
658
658
  path: fullPath
@@ -768,11 +768,11 @@ var errorUtil;
768
768
 
769
769
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
770
770
  var ParseInputLazyPath = class {
771
- constructor(parent, value, path4, key) {
771
+ constructor(parent, value, path5, key) {
772
772
  this._cachedPath = [];
773
773
  this.parent = parent;
774
774
  this.data = value;
775
- this._path = path4;
775
+ this._path = path5;
776
776
  this._key = key;
777
777
  }
778
778
  get path() {
@@ -4223,12 +4223,17 @@ var optionalCoercedString = external_exports.preprocess(
4223
4223
  (value) => toTrimmedString(value),
4224
4224
  external_exports.string().min(1).optional()
4225
4225
  );
4226
+ var displayAliasesSchema = external_exports.record(
4227
+ external_exports.preprocess((value) => toTrimmedString(value), external_exports.string().min(1))
4228
+ ).optional();
4226
4229
  var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
4230
+ var QQBotC2CMarkdownChunkStrategySchema = external_exports.enum(["markdown-block", "length"]).optional().default("markdown-block");
4227
4231
  var QQBotAccountSchema = external_exports.object({
4228
4232
  name: external_exports.string().optional(),
4229
4233
  enabled: external_exports.boolean().optional(),
4230
4234
  appId: optionalCoercedString,
4231
4235
  clientSecret: optionalCoercedString,
4236
+ displayAliases: displayAliasesSchema,
4232
4237
  asr: external_exports.object({
4233
4238
  enabled: external_exports.boolean().optional().default(false),
4234
4239
  appId: optionalCoercedString,
@@ -4237,6 +4242,7 @@ var QQBotAccountSchema = external_exports.object({
4237
4242
  }).optional(),
4238
4243
  markdownSupport: external_exports.boolean().optional().default(true),
4239
4244
  c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4245
+ c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
4240
4246
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4241
4247
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4242
4248
  requireMention: external_exports.boolean().optional().default(true),
@@ -4261,6 +4267,21 @@ QQBotAccountSchema.extend({
4261
4267
  var DEFAULT_INBOUND_MEDIA_DIR = join(homedir(), ".openclaw", "media", "qqbot", "inbound");
4262
4268
  var DEFAULT_INBOUND_MEDIA_KEEP_DAYS = 7;
4263
4269
  var DEFAULT_INBOUND_MEDIA_TEMP_DIR = join(tmpdir(), "qqbot-media");
4270
+ function normalizeDisplayAliasesMap(raw) {
4271
+ if (!raw || typeof raw !== "object") {
4272
+ return {};
4273
+ }
4274
+ const aliases = {};
4275
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
4276
+ const key = rawKey.trim();
4277
+ const value = toTrimmedString(rawValue);
4278
+ if (!key || !value) {
4279
+ continue;
4280
+ }
4281
+ aliases[key] = value;
4282
+ }
4283
+ return aliases;
4284
+ }
4264
4285
  function resolveInboundMediaDir(config) {
4265
4286
  return String(config?.inboundMedia?.dir ?? "").trim() || DEFAULT_INBOUND_MEDIA_DIR;
4266
4287
  }
@@ -4301,7 +4322,15 @@ function mergeQQBotAccountConfig(cfg, accountId) {
4301
4322
  const base = cfg.channels?.qqbot ?? {};
4302
4323
  const { accounts: _ignored, defaultAccount: _ignored2, ...baseConfig } = base;
4303
4324
  const account = resolveAccountConfig(cfg, accountId) ?? {};
4304
- return { ...baseConfig, ...account };
4325
+ const mergedDisplayAliases = {
4326
+ ...normalizeDisplayAliasesMap(baseConfig.displayAliases),
4327
+ ...normalizeDisplayAliasesMap(account.displayAliases)
4328
+ };
4329
+ return {
4330
+ ...baseConfig,
4331
+ ...account,
4332
+ ...Object.keys(mergedDisplayAliases).length > 0 ? { displayAliases: mergedDisplayAliases } : {}
4333
+ };
4305
4334
  }
4306
4335
  function resolveQQBotCredentials(config) {
4307
4336
  const appId = toTrimmedString(config?.appId);
@@ -4876,7 +4905,7 @@ function extractMediaFromText(text, options = {}) {
4876
4905
  const {
4877
4906
  removeFromText = true,
4878
4907
  checkExists = false,
4879
- existsSync: existsSync6,
4908
+ existsSync: existsSync7,
4880
4909
  parseMediaLines = false,
4881
4910
  parseMarkdownImages = true,
4882
4911
  parseHtmlImages = true,
@@ -4891,7 +4920,7 @@ function extractMediaFromText(text, options = {}) {
4891
4920
  const key = media.localPath || media.source;
4892
4921
  if (seenSources.has(key)) return false;
4893
4922
  if (checkExists && media.isLocal && media.localPath) {
4894
- const exists = existsSync6 ? existsSync6(media.localPath) : fs3.existsSync(media.localPath);
4923
+ const exists = existsSync7 ? existsSync7(media.localPath) : fs3.existsSync(media.localPath);
4895
4924
  if (!exists) return false;
4896
4925
  }
4897
4926
  seenSources.add(key);
@@ -6518,6 +6547,7 @@ var CHANNEL_ORDER = [
6518
6547
  "qqbot",
6519
6548
  "wecom",
6520
6549
  "wecom-app",
6550
+ "wecom-kf",
6521
6551
  "feishu-china"
6522
6552
  ];
6523
6553
  var CHANNEL_DISPLAY_LABELS = {
@@ -6525,6 +6555,7 @@ var CHANNEL_DISPLAY_LABELS = {
6525
6555
  "feishu-china": "Feishu\uFF08\u98DE\u4E66\uFF09",
6526
6556
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
6527
6557
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
6558
+ "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
6528
6559
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
6529
6560
  };
6530
6561
  var CHANNEL_GUIDE_LINKS = {
@@ -6532,6 +6563,7 @@ var CHANNEL_GUIDE_LINKS = {
6532
6563
  "feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
6533
6564
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
6534
6565
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
6566
+ "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
6535
6567
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
6536
6568
  };
6537
6569
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -6740,6 +6772,8 @@ function isChannelConfigured(cfg, channelId) {
6740
6772
  return hasWecomWsCredentialPair(channelCfg);
6741
6773
  case "wecom-app":
6742
6774
  return hasTokenPair(channelCfg);
6775
+ case "wecom-kf":
6776
+ return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6743
6777
  default:
6744
6778
  return false;
6745
6779
  }
@@ -6996,6 +7030,55 @@ async function configureWecomApp(prompter, cfg) {
6996
7030
  patch.asr = asr;
6997
7031
  return mergeChannelConfig(cfg, "wecom-app", patch);
6998
7032
  }
7033
+ async function configureWecomKf(prompter, cfg) {
7034
+ section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
7035
+ showGuideLink("wecom-kf");
7036
+ const existing = getChannelConfig(cfg, "wecom-kf");
7037
+ const webhookPath = await prompter.askText({
7038
+ label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
7039
+ defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
7040
+ required: true
7041
+ });
7042
+ const token = await prompter.askSecret({
7043
+ label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 Token",
7044
+ existingValue: toTrimmedString2(existing.token),
7045
+ required: true
7046
+ });
7047
+ const encodingAESKey = await prompter.askSecret({
7048
+ label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 EncodingAESKey",
7049
+ existingValue: toTrimmedString2(existing.encodingAESKey),
7050
+ required: true
7051
+ });
7052
+ const corpId = await prompter.askText({
7053
+ label: "corpId",
7054
+ defaultValue: toTrimmedString2(existing.corpId),
7055
+ required: true
7056
+ });
7057
+ const corpSecret = await prompter.askSecret({
7058
+ label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
7059
+ existingValue: toTrimmedString2(existing.corpSecret),
7060
+ required: true
7061
+ });
7062
+ const openKfId = await prompter.askText({
7063
+ label: "open_kfid",
7064
+ defaultValue: toTrimmedString2(existing.openKfId),
7065
+ required: true
7066
+ });
7067
+ const welcomeText = await prompter.askText({
7068
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7069
+ defaultValue: toTrimmedString2(existing.welcomeText),
7070
+ required: false
7071
+ });
7072
+ return mergeChannelConfig(cfg, "wecom-kf", {
7073
+ webhookPath,
7074
+ token,
7075
+ encodingAESKey,
7076
+ corpId,
7077
+ corpSecret,
7078
+ openKfId,
7079
+ welcomeText: welcomeText || void 0
7080
+ });
7081
+ }
6999
7082
  async function configureQQBot(prompter, cfg) {
7000
7083
  section("\u914D\u7F6E QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09");
7001
7084
  showGuideLink("qqbot");
@@ -7052,6 +7135,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
7052
7135
  return configureWecom(prompter, cfg);
7053
7136
  case "wecom-app":
7054
7137
  return configureWecomApp(prompter, cfg);
7138
+ case "wecom-kf":
7139
+ return configureWecomKf(prompter, cfg);
7055
7140
  case "qqbot":
7056
7141
  return configureQQBot(prompter, cfg);
7057
7142
  default:
@@ -7192,6 +7277,7 @@ var SUPPORTED_CHANNELS = [
7192
7277
  "feishu-china",
7193
7278
  "wecom",
7194
7279
  "wecom-app",
7280
+ "wecom-kf",
7195
7281
  "qqbot"
7196
7282
  ];
7197
7283
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -7380,8 +7466,8 @@ async function getAccessToken(appId, clientSecret, options) {
7380
7466
  tokenPromiseMap.set(normalizedAppId, promise);
7381
7467
  return promise;
7382
7468
  }
7383
- async function apiGet(accessToken, path4, options) {
7384
- const url = `${API_BASE}${path4}`;
7469
+ async function apiGet(accessToken, path5, options) {
7470
+ const url = `${API_BASE}${path5}`;
7385
7471
  return httpGet(url, {
7386
7472
  ...options,
7387
7473
  headers: {
@@ -7390,8 +7476,8 @@ async function apiGet(accessToken, path4, options) {
7390
7476
  }
7391
7477
  });
7392
7478
  }
7393
- async function apiPost(accessToken, path4, body, options) {
7394
- const url = `${API_BASE}${path4}`;
7479
+ async function apiPost(accessToken, path5, body, options) {
7480
+ const url = `${API_BASE}${path5}`;
7395
7481
  return httpPost(url, body, {
7396
7482
  ...options,
7397
7483
  headers: {
@@ -7490,7 +7576,7 @@ async function sendChannelMessage(params) {
7490
7576
  });
7491
7577
  }
7492
7578
  async function sendC2CInputNotify(params) {
7493
- await postPassiveMessage({
7579
+ const response = await postPassiveMessage({
7494
7580
  accessToken: params.accessToken,
7495
7581
  path: `/v2/users/${params.openid}/messages`,
7496
7582
  sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
@@ -7505,6 +7591,8 @@ async function sendC2CInputNotify(params) {
7505
7591
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7506
7592
  })
7507
7593
  });
7594
+ const refIdx = response.ext_info?.ref_idx?.trim();
7595
+ return refIdx ? { refIdx } : {};
7508
7596
  }
7509
7597
  async function uploadC2CMedia(params) {
7510
7598
  const body = {
@@ -7574,6 +7662,233 @@ async function sendGroupMediaMessage(params) {
7574
7662
  })
7575
7663
  });
7576
7664
  }
7665
+ var REF_INDEX_FILE = join(homedir(), ".openclaw", "qqbot", "data", "ref-index.jsonl");
7666
+ var MAX_CONTENT_LENGTH = 500;
7667
+ var MAX_ENTRIES = 5e4;
7668
+ var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
7669
+ var COMPACT_THRESHOLD_RATIO = 2;
7670
+ var cache = null;
7671
+ var totalLinesOnDisk = 0;
7672
+ function normalizeRefIdx(refIdx) {
7673
+ const next = refIdx.trim();
7674
+ return next ? next : void 0;
7675
+ }
7676
+ function ensureStorageDir() {
7677
+ mkdirSync(dirname(REF_INDEX_FILE), { recursive: true });
7678
+ }
7679
+ function truncateContent(content) {
7680
+ return content.trim().slice(0, MAX_CONTENT_LENGTH);
7681
+ }
7682
+ function sanitizeAttachmentSummary(attachment) {
7683
+ const type = attachment.type;
7684
+ const filename = attachment.filename?.trim();
7685
+ const contentType = attachment.contentType?.trim();
7686
+ const localPath = attachment.localPath?.trim();
7687
+ const url = attachment.url?.trim();
7688
+ const transcript = attachment.transcript?.trim();
7689
+ if (!filename && !contentType && !localPath && !url && !transcript && type === "unknown") {
7690
+ return void 0;
7691
+ }
7692
+ return {
7693
+ type,
7694
+ ...filename ? { filename } : {},
7695
+ ...contentType ? { contentType } : {},
7696
+ ...localPath ? { localPath } : {},
7697
+ ...url ? { url } : {},
7698
+ ...transcript ? { transcript } : {},
7699
+ ...transcript && attachment.transcriptSource ? { transcriptSource: attachment.transcriptSource } : {}
7700
+ };
7701
+ }
7702
+ function sanitizeEntry(entry) {
7703
+ const senderId = entry.senderId.trim() || "unknown";
7704
+ const senderName = entry.senderName?.trim();
7705
+ const timestamp = Number.isFinite(entry.timestamp) ? Math.trunc(entry.timestamp) : Date.now();
7706
+ const attachments = entry.attachments?.map((attachment) => sanitizeAttachmentSummary(attachment)).filter((attachment) => Boolean(attachment));
7707
+ return {
7708
+ content: truncateContent(entry.content),
7709
+ senderId,
7710
+ ...senderName ? { senderName } : {},
7711
+ timestamp,
7712
+ ...entry.isBot ? { isBot: true } : {},
7713
+ ...attachments && attachments.length > 0 ? { attachments } : {}
7714
+ };
7715
+ }
7716
+ function shouldCompact() {
7717
+ if (!cache) return false;
7718
+ return totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1e3;
7719
+ }
7720
+ function compactFile() {
7721
+ if (!cache) return;
7722
+ try {
7723
+ ensureStorageDir();
7724
+ const tempPath = `${REF_INDEX_FILE}.tmp`;
7725
+ const lines = [];
7726
+ for (const [key, entry] of cache.entries()) {
7727
+ lines.push(
7728
+ JSON.stringify({
7729
+ k: key,
7730
+ v: sanitizeEntry(entry),
7731
+ t: entry._createdAt
7732
+ })
7733
+ );
7734
+ }
7735
+ writeFileSync(tempPath, lines.length > 0 ? `${lines.join("\n")}
7736
+ ` : "", "utf8");
7737
+ renameSync(tempPath, REF_INDEX_FILE);
7738
+ totalLinesOnDisk = cache.size;
7739
+ } catch {
7740
+ }
7741
+ }
7742
+ function evictIfNeeded() {
7743
+ if (!cache || cache.size < MAX_ENTRIES) return;
7744
+ const now = Date.now();
7745
+ for (const [key, entry] of cache.entries()) {
7746
+ if (now - entry._createdAt > TTL_MS) {
7747
+ cache.delete(key);
7748
+ }
7749
+ }
7750
+ if (cache.size < MAX_ENTRIES) {
7751
+ return;
7752
+ }
7753
+ const sorted = [...cache.entries()].sort((left, right) => left[1]._createdAt - right[1]._createdAt);
7754
+ const removeCount = cache.size - MAX_ENTRIES + 1;
7755
+ for (let index = 0; index < removeCount; index += 1) {
7756
+ const key = sorted[index]?.[0];
7757
+ if (key) {
7758
+ cache.delete(key);
7759
+ }
7760
+ }
7761
+ }
7762
+ function loadCache() {
7763
+ if (cache) {
7764
+ return cache;
7765
+ }
7766
+ cache = /* @__PURE__ */ new Map();
7767
+ totalLinesOnDisk = 0;
7768
+ try {
7769
+ if (!existsSync(REF_INDEX_FILE)) {
7770
+ return cache;
7771
+ }
7772
+ const now = Date.now();
7773
+ const raw = readFileSync(REF_INDEX_FILE, "utf8");
7774
+ const lines = raw.split(/\r?\n/);
7775
+ for (const line of lines) {
7776
+ const trimmed = line.trim();
7777
+ if (!trimmed) continue;
7778
+ totalLinesOnDisk += 1;
7779
+ try {
7780
+ const parsed = JSON.parse(trimmed);
7781
+ const key = typeof parsed.k === "string" ? normalizeRefIdx(parsed.k) : void 0;
7782
+ const createdAt = typeof parsed.t === "number" && Number.isFinite(parsed.t) ? parsed.t : void 0;
7783
+ if (!key || createdAt === void 0 || !parsed.v || typeof parsed.v !== "object") {
7784
+ continue;
7785
+ }
7786
+ if (now - createdAt > TTL_MS) {
7787
+ continue;
7788
+ }
7789
+ const entry = sanitizeEntry(parsed.v);
7790
+ cache.set(key, {
7791
+ ...entry,
7792
+ _createdAt: createdAt
7793
+ });
7794
+ } catch {
7795
+ }
7796
+ }
7797
+ if (shouldCompact()) {
7798
+ compactFile();
7799
+ }
7800
+ } catch {
7801
+ cache = /* @__PURE__ */ new Map();
7802
+ totalLinesOnDisk = 0;
7803
+ }
7804
+ return cache;
7805
+ }
7806
+ function appendLine(line) {
7807
+ try {
7808
+ ensureStorageDir();
7809
+ appendFileSync(REF_INDEX_FILE, `${JSON.stringify(line)}
7810
+ `, "utf8");
7811
+ totalLinesOnDisk += 1;
7812
+ } catch {
7813
+ }
7814
+ }
7815
+ function restoreEntry(entry) {
7816
+ const sanitized = sanitizeEntry(entry);
7817
+ return {
7818
+ content: sanitized.content,
7819
+ senderId: sanitized.senderId,
7820
+ ...sanitized.senderName ? { senderName: sanitized.senderName } : {},
7821
+ timestamp: sanitized.timestamp,
7822
+ ...sanitized.isBot ? { isBot: true } : {},
7823
+ ...sanitized.attachments ? { attachments: sanitized.attachments } : {}
7824
+ };
7825
+ }
7826
+ function formatAttachmentSummary(attachment) {
7827
+ const sourceParts = [
7828
+ attachment.localPath?.trim() ? `\u672C\u5730: ${attachment.localPath.trim()}` : void 0,
7829
+ attachment.url?.trim() ? `\u94FE\u63A5: ${attachment.url.trim()}` : void 0
7830
+ ].filter((value) => Boolean(value));
7831
+ const sourceSuffix = sourceParts.length > 0 ? ` (${sourceParts.join(" | ")})` : "";
7832
+ if (attachment.type === "image") {
7833
+ return `[\u56FE\u7247${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7834
+ }
7835
+ if (attachment.type === "voice") {
7836
+ if (attachment.transcript?.trim()) {
7837
+ const sourceLabel = attachment.transcriptSource === "asr" ? "\u5B98\u65B9\u8BC6\u522B" : attachment.transcriptSource === "stt" ? "\u672C\u5730\u8BC6\u522B" : attachment.transcriptSource === "tts" ? "TTS \u539F\u6587" : attachment.transcriptSource === "fallback" ? "\u515C\u5E95\u6587\u672C" : void 0;
7838
+ return `[\u8BED\u97F3\u6D88\u606F: ${attachment.transcript.trim()}${sourceLabel ? ` (${sourceLabel})` : ""}]${sourceSuffix}`;
7839
+ }
7840
+ return `[\u8BED\u97F3\u6D88\u606F]${sourceSuffix}`;
7841
+ }
7842
+ if (attachment.type === "video") {
7843
+ return `[\u89C6\u9891${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7844
+ }
7845
+ if (attachment.type === "file") {
7846
+ return `[\u6587\u4EF6${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7847
+ }
7848
+ return `[\u9644\u4EF6${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7849
+ }
7850
+ function setRefIndex(refIdx, entry) {
7851
+ const key = normalizeRefIdx(refIdx);
7852
+ if (!key) return;
7853
+ const store = loadCache();
7854
+ evictIfNeeded();
7855
+ const nextEntry = sanitizeEntry(entry);
7856
+ const createdAt = Date.now();
7857
+ store.set(key, {
7858
+ ...nextEntry,
7859
+ _createdAt: createdAt
7860
+ });
7861
+ appendLine({
7862
+ k: key,
7863
+ v: nextEntry,
7864
+ t: createdAt
7865
+ });
7866
+ if (shouldCompact()) {
7867
+ compactFile();
7868
+ }
7869
+ }
7870
+ function getRefIndex(refIdx) {
7871
+ const key = normalizeRefIdx(refIdx);
7872
+ if (!key) return null;
7873
+ const store = loadCache();
7874
+ const entry = store.get(key);
7875
+ if (!entry) {
7876
+ return null;
7877
+ }
7878
+ if (Date.now() - entry._createdAt > TTL_MS) {
7879
+ store.delete(key);
7880
+ return null;
7881
+ }
7882
+ return restoreEntry(entry);
7883
+ }
7884
+ function formatRefEntryForAgent(entry) {
7885
+ const content = entry.content.trim();
7886
+ const parts = content ? [content] : [];
7887
+ for (const attachment of entry.attachments ?? []) {
7888
+ parts.push(formatAttachmentSummary(attachment));
7889
+ }
7890
+ return parts.join("\n") || "[\u7A7A\u6D88\u606F]";
7891
+ }
7577
7892
  var require2 = createRequire(import.meta.url);
7578
7893
  function resolveQQBotMediaFileType(fileName) {
7579
7894
  const mediaType = detectMediaType(fileName);
@@ -7729,7 +8044,7 @@ async function sendFileQQBot(params) {
7729
8044
  }
7730
8045
  }
7731
8046
  try {
7732
- return await sendC2CMediaMessage({
8047
+ const result = await sendC2CMediaMessage({
7733
8048
  accessToken,
7734
8049
  openid: target.id,
7735
8050
  fileInfo,
@@ -7737,6 +8052,12 @@ async function sendFileQQBot(params) {
7737
8052
  ...messageId ? { messageId } : {},
7738
8053
  ...eventId ? { eventId } : {}
7739
8054
  });
8055
+ const refIdx = result.ext_info?.ref_idx?.trim();
8056
+ return {
8057
+ id: result.id,
8058
+ timestamp: result.timestamp,
8059
+ ...refIdx ? { refIdx } : {}
8060
+ };
7740
8061
  } catch (err) {
7741
8062
  const message = formatQQBotError(err);
7742
8063
  throw new Error(`QQBot C2C media send failed: ${message}`);
@@ -7780,21 +8101,22 @@ async function readMediaWithConfig(source, options) {
7780
8101
 
7781
8102
  // src/outbound.ts
7782
8103
  function stripPrefix(value, prefix) {
7783
- return value.startsWith(prefix) ? value.slice(prefix.length) : value;
8104
+ return value.slice(0, prefix.length).toLowerCase() === prefix ? value.slice(prefix.length) : value;
7784
8105
  }
7785
8106
  function parseTarget(to) {
7786
8107
  let raw = to.trim();
7787
8108
  raw = stripPrefix(raw, "qqbot:");
7788
- if (raw.startsWith("group:")) {
8109
+ const normalizedRaw = raw.toLowerCase();
8110
+ if (normalizedRaw.startsWith("group:")) {
7789
8111
  return { kind: "group", id: raw.slice("group:".length) };
7790
8112
  }
7791
- if (raw.startsWith("channel:")) {
8113
+ if (normalizedRaw.startsWith("channel:")) {
7792
8114
  return { kind: "channel", id: raw.slice("channel:".length) };
7793
8115
  }
7794
- if (raw.startsWith("user:")) {
8116
+ if (normalizedRaw.startsWith("user:")) {
7795
8117
  return { kind: "c2c", id: raw.slice("user:".length) };
7796
8118
  }
7797
- if (raw.startsWith("c2c:")) {
8119
+ if (normalizedRaw.startsWith("c2c:")) {
7798
8120
  return { kind: "c2c", id: raw.slice("c2c:".length) };
7799
8121
  }
7800
8122
  return { kind: "c2c", id: raw };
@@ -7853,6 +8175,85 @@ function shouldRetryWithEventId(err) {
7853
8175
  function shouldSendTextAsFollowupForMedia(mediaUrl) {
7854
8176
  return detectMediaType(stripTitleFromUrl(mediaUrl)) === "file";
7855
8177
  }
8178
+ function isHttpUrl2(value) {
8179
+ return /^https?:\/\//i.test(value);
8180
+ }
8181
+ function resolveResponseRefIdx(response) {
8182
+ if (!response || typeof response !== "object") {
8183
+ return void 0;
8184
+ }
8185
+ const direct = response.refIdx;
8186
+ if (typeof direct === "string" && direct.trim()) {
8187
+ return direct.trim();
8188
+ }
8189
+ const extInfo = response.ext_info;
8190
+ if (typeof extInfo?.ref_idx === "string" && extInfo.ref_idx.trim()) {
8191
+ return extInfo.ref_idx.trim();
8192
+ }
8193
+ return void 0;
8194
+ }
8195
+ function resolveOutboundAttachmentType(mediaUrl) {
8196
+ const detected = detectMediaType(stripTitleFromUrl(mediaUrl));
8197
+ if (detected === "image") return "image";
8198
+ if (detected === "video") return "video";
8199
+ if (detected === "audio") return "voice";
8200
+ if (detected === "file") return "file";
8201
+ return "unknown";
8202
+ }
8203
+ function resolveOutboundAttachmentFileName(mediaUrl) {
8204
+ const source = stripTitleFromUrl(mediaUrl).trim();
8205
+ if (!source) return void 0;
8206
+ if (isHttpUrl2(source)) {
8207
+ try {
8208
+ const base2 = path2.posix.basename(new URL(source).pathname);
8209
+ return base2 && base2 !== "/" ? base2 : void 0;
8210
+ } catch {
8211
+ return void 0;
8212
+ }
8213
+ }
8214
+ const base = path2.basename(source);
8215
+ return base || void 0;
8216
+ }
8217
+ function buildOutboundAttachmentSummary(params) {
8218
+ const source = stripTitleFromUrl(params.mediaUrl).trim();
8219
+ const type = resolveOutboundAttachmentType(source);
8220
+ const filename = resolveOutboundAttachmentFileName(source);
8221
+ const text = params.text?.trim();
8222
+ return {
8223
+ type,
8224
+ ...filename ? { filename } : {},
8225
+ ...isHttpUrl2(source) ? { url: source } : { localPath: source },
8226
+ ...type === "voice" && text ? {
8227
+ transcript: text,
8228
+ transcriptSource: "tts"
8229
+ } : {}
8230
+ };
8231
+ }
8232
+ function recordOutboundC2CRefIndex(params) {
8233
+ const refIdx = params.refIdx?.trim();
8234
+ if (!refIdx) return;
8235
+ const text = params.text?.trim() ?? "";
8236
+ const attachments = params.mediaUrl?.trim() ? [buildOutboundAttachmentSummary({ mediaUrl: params.mediaUrl, text })] : void 0;
8237
+ if (!text && !attachments) {
8238
+ return;
8239
+ }
8240
+ try {
8241
+ const accountLabel = params.accountId?.trim() || DEFAULT_ACCOUNT_ID;
8242
+ setRefIndex(refIdx, {
8243
+ content: text,
8244
+ senderId: accountLabel,
8245
+ senderName: accountLabel,
8246
+ timestamp: Date.now(),
8247
+ isBot: true,
8248
+ ...attachments ? { attachments } : {}
8249
+ });
8250
+ console.info(
8251
+ `[qqbot] cached outbound ref_idx=${refIdx} accountId=${accountLabel} textLen=${text.length} media=${params.mediaUrl?.trim() ? "yes" : "no"}`
8252
+ );
8253
+ } catch (err) {
8254
+ console.warn(`[qqbot] failed to cache outbound ref_idx=${refIdx}: ${String(err)}`);
8255
+ }
8256
+ }
7856
8257
  function buildPassiveReplyRefs(params) {
7857
8258
  if (params.replyToId) {
7858
8259
  return { messageId: params.replyToId };
@@ -7998,7 +8399,14 @@ var qqbotOutbound = {
7998
8399
  content: text,
7999
8400
  markdown
8000
8401
  });
8001
- return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
8402
+ const refIdx2 = resolveResponseRefIdx(result2);
8403
+ recordOutboundC2CRefIndex({ refIdx: refIdx2, accountId, text });
8404
+ return {
8405
+ channel: "qqbot",
8406
+ messageId: result2.id,
8407
+ timestamp: result2.timestamp,
8408
+ ...refIdx2 ? { refIdx: refIdx2 } : {}
8409
+ };
8002
8410
  }
8003
8411
  let result;
8004
8412
  try {
@@ -8065,7 +8473,14 @@ var qqbotOutbound = {
8065
8473
  throw retryErr;
8066
8474
  }
8067
8475
  }
8068
- return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
8476
+ const refIdx = resolveResponseRefIdx(result);
8477
+ recordOutboundC2CRefIndex({ refIdx, accountId, text });
8478
+ return {
8479
+ channel: "qqbot",
8480
+ messageId: result.id,
8481
+ timestamp: result.timestamp,
8482
+ ...refIdx ? { refIdx } : {}
8483
+ };
8069
8484
  } catch (err) {
8070
8485
  const message = summarizeError(err);
8071
8486
  return { channel: "qqbot", error: message };
@@ -8174,7 +8589,21 @@ ${mediaUrl}` : mediaUrl;
8174
8589
  };
8175
8590
  }
8176
8591
  }
8177
- return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
8592
+ const refIdx = target.kind === "c2c" ? resolveResponseRefIdx(result) : void 0;
8593
+ if (target.kind === "c2c") {
8594
+ recordOutboundC2CRefIndex({
8595
+ refIdx,
8596
+ accountId,
8597
+ text: sendTextAsFollowup ? void 0 : trimmedText,
8598
+ mediaUrl
8599
+ });
8600
+ }
8601
+ return {
8602
+ channel: "qqbot",
8603
+ messageId: result.id,
8604
+ timestamp: result.timestamp,
8605
+ ...refIdx ? { refIdx } : {}
8606
+ };
8178
8607
  } catch (err) {
8179
8608
  const message = summarizeError(err);
8180
8609
  return { channel: "qqbot", error: message };
@@ -8193,8 +8622,9 @@ ${mediaUrl}` : mediaUrl;
8193
8622
  }
8194
8623
  try {
8195
8624
  const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
8625
+ let typingResult;
8196
8626
  try {
8197
- await sendC2CInputNotify({
8627
+ typingResult = await sendC2CInputNotify({
8198
8628
  accessToken,
8199
8629
  openid: target.id,
8200
8630
  messageId: replyToId,
@@ -8216,7 +8646,7 @@ ${mediaUrl}` : mediaUrl;
8216
8646
  reason: summarizeError(err)
8217
8647
  });
8218
8648
  try {
8219
- await sendC2CInputNotify({
8649
+ typingResult = await sendC2CInputNotify({
8220
8650
  accessToken,
8221
8651
  openid: target.id,
8222
8652
  eventId: replyEventId,
@@ -8245,7 +8675,10 @@ ${mediaUrl}` : mediaUrl;
8245
8675
  throw retryErr;
8246
8676
  }
8247
8677
  }
8248
- return { channel: "qqbot" };
8678
+ return {
8679
+ channel: "qqbot",
8680
+ ...typingResult?.refIdx ? { refIdx: typingResult.refIdx } : {}
8681
+ };
8249
8682
  } catch (err) {
8250
8683
  const message = summarizeError(err);
8251
8684
  return { channel: "qqbot", error: message };
@@ -8394,13 +8827,13 @@ async function replaceAsync(input2, pattern, replacer) {
8394
8827
  return result;
8395
8828
  }
8396
8829
  async function getResolvedImageSize(params) {
8397
- const { url, cache, resolveImageSize } = params;
8398
- const existing = cache.get(url);
8830
+ const { url, cache: cache2, resolveImageSize } = params;
8831
+ const existing = cache2.get(url);
8399
8832
  if (existing) {
8400
8833
  return existing;
8401
8834
  }
8402
8835
  const pending = resolveImageSize(url).then((size) => normalizeImageSize(size) ?? DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE).catch(() => DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE);
8403
- cache.set(url, pending);
8836
+ cache2.set(url, pending);
8404
8837
  return pending;
8405
8838
  }
8406
8839
  async function normalizeTextSegment(params) {
@@ -8527,13 +8960,29 @@ async function normalizeQQBotMarkdownImages(params) {
8527
8960
 
8528
8961
  ${appendedImages.join("\n")}`;
8529
8962
  }
8530
- var DEFAULT_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "data", "qqbot", "known-targets.json");
8963
+ var DEFAULT_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "qqbot", "data", "known-targets.json");
8964
+ var LEGACY_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "data", "qqbot", "known-targets.json");
8531
8965
  function resolveKnownTargetsFilePath(options) {
8532
8966
  return options?.filePath?.trim() || DEFAULT_KNOWN_TARGETS_PATH;
8533
8967
  }
8534
8968
  function ensureKnownTargetsDir(filePath) {
8535
8969
  mkdirSync(dirname(filePath), { recursive: true });
8536
8970
  }
8971
+ function migrateLegacyKnownTargets(filePath) {
8972
+ if (filePath !== DEFAULT_KNOWN_TARGETS_PATH) {
8973
+ return;
8974
+ }
8975
+ if (existsSync(filePath) || !existsSync(LEGACY_KNOWN_TARGETS_PATH)) {
8976
+ return;
8977
+ }
8978
+ ensureKnownTargetsDir(filePath);
8979
+ try {
8980
+ renameSync(LEGACY_KNOWN_TARGETS_PATH, filePath);
8981
+ } catch {
8982
+ copyFileSync(LEGACY_KNOWN_TARGETS_PATH, filePath);
8983
+ rmSync(LEGACY_KNOWN_TARGETS_PATH, { force: true });
8984
+ }
8985
+ }
8537
8986
  function compareTargetsByLastSeenDesc(a, b) {
8538
8987
  if (b.lastSeenAt !== a.lastSeenAt) {
8539
8988
  return b.lastSeenAt - a.lastSeenAt;
@@ -8569,6 +9018,7 @@ function parseKnownTargets(raw, filePath) {
8569
9018
  }
8570
9019
  function readKnownTargets(options) {
8571
9020
  const filePath = resolveKnownTargetsFilePath(options);
9021
+ migrateLegacyKnownTargets(filePath);
8572
9022
  if (!existsSync(filePath)) {
8573
9023
  return [];
8574
9024
  }
@@ -8580,6 +9030,7 @@ function readKnownTargets(options) {
8580
9030
  }
8581
9031
  function writeKnownTargets(targets, options) {
8582
9032
  const filePath = resolveKnownTargetsFilePath(options);
9033
+ migrateLegacyKnownTargets(filePath);
8583
9034
  if (targets.length === 0) {
8584
9035
  if (existsSync(filePath)) {
8585
9036
  rmSync(filePath, { force: true });
@@ -8713,84 +9164,433 @@ function getQQBotRuntime() {
8713
9164
  return runtime;
8714
9165
  }
8715
9166
  var sessionDispatchQueue = /* @__PURE__ */ new Map();
9167
+ var QQBOT_ABORT_TRIGGERS = /* @__PURE__ */ new Set([
9168
+ "stop",
9169
+ "esc",
9170
+ "abort",
9171
+ "wait",
9172
+ "exit",
9173
+ "interrupt",
9174
+ "detente",
9175
+ "deten",
9176
+ "det\xE9n",
9177
+ "arrete",
9178
+ "arr\xEAte",
9179
+ "\u505C\u6B62",
9180
+ "\u3084\u3081\u3066",
9181
+ "\u6B62\u3081\u3066",
9182
+ "\u0930\u0941\u0915\u094B",
9183
+ "\u062A\u0648\u0642\u0641",
9184
+ "\u0441\u0442\u043E\u043F",
9185
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C",
9186
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438",
9187
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C",
9188
+ "\u043F\u0440\u0435\u043A\u0440\u0430\u0442\u0438",
9189
+ "halt",
9190
+ "anhalten",
9191
+ "aufh\xF6ren",
9192
+ "hoer auf",
9193
+ "stopp",
9194
+ "pare",
9195
+ "stop openclaw",
9196
+ "openclaw stop",
9197
+ "stop action",
9198
+ "stop current action",
9199
+ "stop run",
9200
+ "stop current run",
9201
+ "stop agent",
9202
+ "stop the agent",
9203
+ "stop don't do anything",
9204
+ "stop dont do anything",
9205
+ "stop do not do anything",
9206
+ "stop doing anything",
9207
+ "do not do that",
9208
+ "please stop",
9209
+ "stop please"
9210
+ ]);
9211
+ var QQBOT_ABORT_TRAILING_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
9212
+ function resolveQQBotRouteSessionKey(route) {
9213
+ const effectiveSessionKey = route.effectiveSessionKey?.trim();
9214
+ if (effectiveSessionKey) {
9215
+ return effectiveSessionKey;
9216
+ }
9217
+ return route.sessionKey;
9218
+ }
8716
9219
  function buildSessionDispatchQueueKey(route) {
8717
9220
  const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
8718
- return `${accountId}:${route.sessionKey}`;
9221
+ return `${accountId}:${resolveQQBotRouteSessionKey(route)}`;
8719
9222
  }
8720
- async function runSerializedSessionDispatch(queueKey, task) {
8721
- const previous = sessionDispatchQueue.get(queueKey) ?? Promise.resolve();
8722
- const run = previous.catch(() => void 0).then(task);
8723
- const cleanup = run.then(() => void 0, () => void 0);
8724
- sessionDispatchQueue.set(queueKey, cleanup);
9223
+ function createSessionDispatchState() {
9224
+ return {
9225
+ queue: [],
9226
+ processing: false,
9227
+ immediateActiveCount: 0,
9228
+ waiters: [],
9229
+ abortGeneration: 0
9230
+ };
9231
+ }
9232
+ function getSessionDispatchState(queueKey) {
9233
+ const existing = sessionDispatchQueue.get(queueKey);
9234
+ if (existing) {
9235
+ return existing;
9236
+ }
9237
+ const created = createSessionDispatchState();
9238
+ sessionDispatchQueue.set(queueKey, created);
9239
+ return created;
9240
+ }
9241
+ function signalSessionDispatchState(state) {
9242
+ const waiters = state.waiters.splice(0, state.waiters.length);
9243
+ for (const resolve3 of waiters) {
9244
+ resolve3();
9245
+ }
9246
+ }
9247
+ function waitForSessionDispatchState(state) {
9248
+ return new Promise((resolve3) => {
9249
+ state.waiters.push(resolve3);
9250
+ });
9251
+ }
9252
+ function cleanupSessionDispatchState(queueKey, state) {
9253
+ if (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0 || state.waiters.length > 0) {
9254
+ return;
9255
+ }
9256
+ if (sessionDispatchQueue.get(queueKey) === state) {
9257
+ sessionDispatchQueue.delete(queueKey);
9258
+ }
9259
+ }
9260
+ function hasSessionDispatchBacklog(queueKey) {
9261
+ const state = sessionDispatchQueue.get(queueKey);
9262
+ return Boolean(
9263
+ state && (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0)
9264
+ );
9265
+ }
9266
+ async function processSerializedSessionDispatchQueue(queueKey) {
9267
+ const state = sessionDispatchQueue.get(queueKey);
9268
+ if (!state || state.processing) {
9269
+ return;
9270
+ }
9271
+ state.processing = true;
8725
9272
  try {
8726
- return await run;
9273
+ for (; ; ) {
9274
+ if (state.immediateActiveCount > 0) {
9275
+ await waitForSessionDispatchState(state);
9276
+ continue;
9277
+ }
9278
+ const next = state.queue.shift();
9279
+ if (!next) {
9280
+ break;
9281
+ }
9282
+ try {
9283
+ await next.task();
9284
+ next.resolve();
9285
+ } catch (err) {
9286
+ next.reject(err);
9287
+ }
9288
+ }
8727
9289
  } finally {
8728
- if (sessionDispatchQueue.get(queueKey) === cleanup) {
8729
- sessionDispatchQueue.delete(queueKey);
9290
+ state.processing = false;
9291
+ if (state.queue.length > 0) {
9292
+ void processSerializedSessionDispatchQueue(queueKey);
9293
+ return;
8730
9294
  }
9295
+ cleanupSessionDispatchState(queueKey, state);
8731
9296
  }
8732
9297
  }
8733
- function toString(value) {
8734
- if (typeof value === "string" && value.trim()) return value;
8735
- return void 0;
8736
- }
8737
- function toNumber2(value) {
8738
- if (typeof value === "number" && Number.isFinite(value)) return value;
8739
- if (typeof value === "string") {
8740
- const parsed = Date.parse(value);
8741
- if (!Number.isNaN(parsed)) return parsed;
9298
+ function dropQueuedSessionDispatches(queueKey) {
9299
+ const state = sessionDispatchQueue.get(queueKey);
9300
+ if (!state || state.queue.length === 0) {
9301
+ return 0;
8742
9302
  }
8743
- return void 0;
9303
+ const dropped = state.queue.splice(0, state.queue.length);
9304
+ for (const item of dropped) {
9305
+ item.resolve();
9306
+ }
9307
+ signalSessionDispatchState(state);
9308
+ cleanupSessionDispatchState(queueKey, state);
9309
+ return dropped.length;
8744
9310
  }
8745
- function toNonNegativeNumber(value) {
8746
- if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
8747
- if (value < 0) return void 0;
8748
- return value;
9311
+ function markSessionDispatchAbort(queueKey) {
9312
+ const state = getSessionDispatchState(queueKey);
9313
+ state.abortGeneration += 1;
9314
+ signalSessionDispatchState(state);
9315
+ return state.abortGeneration;
8749
9316
  }
8750
- function normalizeAttachmentUrl(value) {
8751
- if (typeof value !== "string") return void 0;
9317
+ function normalizeQQBotSessionKeyPart(value) {
8752
9318
  const trimmed = value.trim();
8753
- if (!trimmed) return void 0;
8754
- if (trimmed.startsWith("//")) return `https:${trimmed}`;
8755
- return trimmed;
9319
+ return trimmed ? trimmed.toLowerCase() : "unknown";
8756
9320
  }
8757
- function parseAttachments(payload) {
8758
- const raw = payload.attachments;
8759
- if (!Array.isArray(raw)) return [];
8760
- const items = [];
8761
- for (const entry of raw) {
8762
- if (!entry || typeof entry !== "object") continue;
8763
- const data = entry;
8764
- const url = normalizeAttachmentUrl(data.url);
8765
- if (!url) continue;
8766
- items.push({
8767
- url,
8768
- filename: toString(data.filename),
8769
- contentType: toString(data.content_type),
8770
- size: toNonNegativeNumber(data.size)
8771
- });
9321
+ function buildQQBotDirectSessionKey(params) {
9322
+ const normalizedAccountId = normalizeQQBotSessionKeyPart(params.accountId);
9323
+ const normalizedSenderId = normalizeQQBotSessionKeyPart(params.senderStableId);
9324
+ const trimmedRouteSessionKey = params.routeSessionKey.trim();
9325
+ if (!trimmedRouteSessionKey) {
9326
+ return `agent:main:qqbot:dm:${normalizedAccountId}:${normalizedSenderId}`;
8772
9327
  }
8773
- return items;
8774
- }
8775
- function parseTextWithAttachments(payload) {
8776
- const rawContent = typeof payload.content === "string" ? payload.content : "";
8777
- const attachments = parseAttachments(payload);
8778
- return {
8779
- text: rawContent.trim(),
8780
- attachments
8781
- };
9328
+ const qqAgentRouteMatch = trimmedRouteSessionKey.match(/^(agent:[^:]+:qqbot:)(?:direct|dm):.+$/i);
9329
+ if (qqAgentRouteMatch?.[1]) {
9330
+ return `${qqAgentRouteMatch[1]}dm:${normalizedAccountId}:${normalizedSenderId}`;
9331
+ }
9332
+ return `${trimmedRouteSessionKey}:dm:${normalizedAccountId}:${normalizedSenderId}`;
8782
9333
  }
8783
- function resolveEventId(payload, fallbackEventId) {
8784
- return toString(payload.event_id) ?? toString(payload.eventId) ?? toString(fallbackEventId);
9334
+ function normalizeQQBotReplyTarget(value) {
9335
+ if (typeof value !== "string") {
9336
+ return void 0;
9337
+ }
9338
+ let trimmed = value.trim();
9339
+ if (!trimmed) {
9340
+ return void 0;
9341
+ }
9342
+ if (/^qqbot:/i.test(trimmed)) {
9343
+ trimmed = trimmed.slice("qqbot:".length).trim();
9344
+ }
9345
+ if (/^c2c:/i.test(trimmed)) {
9346
+ const openid = trimmed.slice("c2c:".length).trim();
9347
+ return openid ? `user:${openid}` : void 0;
9348
+ }
9349
+ return /^(user|group|channel):/i.test(trimmed) ? trimmed : void 0;
8785
9350
  }
8786
- 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";
8787
- var VOICE_EXTENSIONS = [".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex"];
8788
- var VOICE_ASR_ERROR_MAX_LENGTH = 500;
8789
- 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";
8790
- var DEFAULT_LONG_TASK_NOTICE_DELAY_MS = 3e4;
8791
- var QQ_GROUP_NO_REPLY_FALLBACK_TEXT = "\u6211\u5728\u3002\u4F60\u53EF\u4EE5\u76F4\u63A5\u8BF4\u5177\u4F53\u4E00\u70B9\u3002";
8792
- function startLongTaskNoticeTimer(params) {
8793
- const { delayMs, logger, sendNotice } = params;
9351
+ async function runSerializedSessionDispatch(queueKey, task) {
9352
+ const state = getSessionDispatchState(queueKey);
9353
+ return new Promise((resolve3, reject) => {
9354
+ let settled = false;
9355
+ state.queue.push({
9356
+ task: async () => {
9357
+ if (settled) {
9358
+ return;
9359
+ }
9360
+ try {
9361
+ const result = await task();
9362
+ if (!settled) {
9363
+ settled = true;
9364
+ resolve3(result);
9365
+ }
9366
+ } catch (err) {
9367
+ if (!settled) {
9368
+ settled = true;
9369
+ reject(err);
9370
+ }
9371
+ }
9372
+ },
9373
+ resolve: () => {
9374
+ if (settled) {
9375
+ return;
9376
+ }
9377
+ settled = true;
9378
+ resolve3(void 0);
9379
+ },
9380
+ reject: (err) => {
9381
+ if (settled) {
9382
+ return;
9383
+ }
9384
+ settled = true;
9385
+ reject(err);
9386
+ }
9387
+ });
9388
+ signalSessionDispatchState(state);
9389
+ void processSerializedSessionDispatchQueue(queueKey);
9390
+ });
9391
+ }
9392
+ async function runImmediateSessionDispatch(queueKey, task) {
9393
+ const state = getSessionDispatchState(queueKey);
9394
+ state.immediateActiveCount += 1;
9395
+ signalSessionDispatchState(state);
9396
+ try {
9397
+ return await task();
9398
+ } finally {
9399
+ state.immediateActiveCount = Math.max(0, state.immediateActiveCount - 1);
9400
+ signalSessionDispatchState(state);
9401
+ if (state.queue.length > 0) {
9402
+ void processSerializedSessionDispatchQueue(queueKey);
9403
+ }
9404
+ cleanupSessionDispatchState(queueKey, state);
9405
+ }
9406
+ }
9407
+ function normalizeQQBotAbortTriggerText(text) {
9408
+ return text.trim().toLowerCase().replace(/[’`]/g, "'").replace(/\s+/g, " ").replace(QQBOT_ABORT_TRAILING_PUNCTUATION_RE, "").trim();
9409
+ }
9410
+ function isQQBotAbortTrigger(text) {
9411
+ if (!text) {
9412
+ return false;
9413
+ }
9414
+ return QQBOT_ABORT_TRIGGERS.has(normalizeQQBotAbortTriggerText(text));
9415
+ }
9416
+ function isQQBotFastAbortCommandText(text) {
9417
+ if (!text) {
9418
+ return false;
9419
+ }
9420
+ const normalized = text.trim();
9421
+ if (!normalized) {
9422
+ return false;
9423
+ }
9424
+ const lower = normalized.toLowerCase();
9425
+ return lower === "/stop" || normalizeQQBotAbortTriggerText(lower) === "/stop" || isQQBotAbortTrigger(lower);
9426
+ }
9427
+ function toString(value) {
9428
+ if (typeof value === "string" && value.trim()) return value;
9429
+ return void 0;
9430
+ }
9431
+ function asRecord(value) {
9432
+ if (!value || typeof value !== "object") {
9433
+ return void 0;
9434
+ }
9435
+ return value;
9436
+ }
9437
+ function normalizeQQBotDisplayAliasesMap(raw) {
9438
+ if (!raw || typeof raw !== "object") {
9439
+ return {};
9440
+ }
9441
+ const aliases = {};
9442
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
9443
+ const key = rawKey.trim();
9444
+ const value = toString(rawValue);
9445
+ if (!key || !value) {
9446
+ continue;
9447
+ }
9448
+ aliases[key] = value;
9449
+ }
9450
+ return aliases;
9451
+ }
9452
+ function resolveQQBotDisplayAliasMaps(cfg, accountId) {
9453
+ const qqbot = cfg?.channels?.qqbot;
9454
+ return {
9455
+ globalAliases: normalizeQQBotDisplayAliasesMap(qqbot?.displayAliases),
9456
+ accountAliases: normalizeQQBotDisplayAliasesMap(qqbot?.accounts?.[accountId]?.displayAliases)
9457
+ };
9458
+ }
9459
+ function resolveQQBotSenderName(params) {
9460
+ const { inbound, cfg, accountId } = params;
9461
+ const stableId = inbound.c2cOpenid?.trim() || inbound.senderId.trim();
9462
+ const { globalAliases, accountAliases } = resolveQQBotDisplayAliasMaps(cfg, accountId);
9463
+ if (inbound.type === "direct") {
9464
+ const knownTarget = stableId ? getKnownQQBotTarget({ accountId, target: `user:${stableId}` }) : void 0;
9465
+ const knownTargetDisplayName = knownTarget?.displayName?.trim();
9466
+ if (knownTargetDisplayName) {
9467
+ return {
9468
+ displayName: knownTargetDisplayName,
9469
+ persistentDisplayName: knownTargetDisplayName,
9470
+ source: "known-target",
9471
+ knownTargetDisplayName
9472
+ };
9473
+ }
9474
+ const aliasKeys = [...new Set([`user:${stableId}`, stableId, inbound.senderId.trim()].filter(Boolean))];
9475
+ for (const aliasKey of aliasKeys) {
9476
+ const alias = accountAliases[aliasKey];
9477
+ if (alias) {
9478
+ return {
9479
+ displayName: alias,
9480
+ persistentDisplayName: alias,
9481
+ source: "account-alias",
9482
+ matchedAliasKey: aliasKey
9483
+ };
9484
+ }
9485
+ }
9486
+ for (const aliasKey of aliasKeys) {
9487
+ const alias = globalAliases[aliasKey];
9488
+ if (alias) {
9489
+ return {
9490
+ displayName: alias,
9491
+ persistentDisplayName: alias,
9492
+ source: "global-alias",
9493
+ matchedAliasKey: aliasKey
9494
+ };
9495
+ }
9496
+ }
9497
+ }
9498
+ return {
9499
+ displayName: stableId,
9500
+ source: "stable-id"
9501
+ };
9502
+ }
9503
+ function logQQBotSenderNameResolution(params) {
9504
+ const { logger, inbound, accountId, resolution } = params;
9505
+ logger.debug?.(
9506
+ `[display-name] accountId=${accountId} type=${inbound.type} senderId=${inbound.senderId} knownTarget=${resolution.knownTargetDisplayName ?? "-"} alias=${resolution.matchedAliasKey ?? "-"} final=${JSON.stringify(resolution.displayName)} source=${resolution.source}`
9507
+ );
9508
+ }
9509
+ function toNumber2(value) {
9510
+ if (typeof value === "number" && Number.isFinite(value)) return value;
9511
+ if (typeof value === "string") {
9512
+ const parsed = Date.parse(value);
9513
+ if (!Number.isNaN(parsed)) return parsed;
9514
+ }
9515
+ return void 0;
9516
+ }
9517
+ function toNonNegativeNumber(value) {
9518
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
9519
+ if (value < 0) return void 0;
9520
+ return value;
9521
+ }
9522
+ function normalizeAttachmentUrl(value) {
9523
+ if (typeof value !== "string") return void 0;
9524
+ const trimmed = value.trim();
9525
+ if (!trimmed) return void 0;
9526
+ if (trimmed.startsWith("//")) return `https:${trimmed}`;
9527
+ return trimmed;
9528
+ }
9529
+ function parseAttachments(payload) {
9530
+ const raw = payload.attachments;
9531
+ if (!Array.isArray(raw)) return [];
9532
+ const items = [];
9533
+ for (const entry of raw) {
9534
+ if (!entry || typeof entry !== "object") continue;
9535
+ const data = entry;
9536
+ const url = normalizeAttachmentUrl(data.url);
9537
+ if (!url) continue;
9538
+ items.push({
9539
+ url,
9540
+ filename: toString(data.filename),
9541
+ contentType: toString(data.content_type),
9542
+ size: toNonNegativeNumber(data.size)
9543
+ });
9544
+ }
9545
+ return items;
9546
+ }
9547
+ function parseTextWithAttachments(payload) {
9548
+ const rawContent = typeof payload.content === "string" ? payload.content : "";
9549
+ const attachments = parseAttachments(payload);
9550
+ return {
9551
+ text: rawContent.trim(),
9552
+ attachments
9553
+ };
9554
+ }
9555
+ function parseQQBotRefIndices(payload) {
9556
+ const scene = payload.message_scene;
9557
+ if (!scene || typeof scene !== "object") {
9558
+ return {};
9559
+ }
9560
+ const ext = scene.ext;
9561
+ if (!Array.isArray(ext)) {
9562
+ return {};
9563
+ }
9564
+ let refMsgIdx;
9565
+ let msgIdx;
9566
+ for (const value of ext) {
9567
+ const item = toString(value);
9568
+ if (!item) continue;
9569
+ if (item.startsWith("ref_msg_idx=")) {
9570
+ refMsgIdx = toString(item.slice("ref_msg_idx=".length));
9571
+ continue;
9572
+ }
9573
+ if (item.startsWith("msg_idx=")) {
9574
+ msgIdx = toString(item.slice("msg_idx=".length));
9575
+ }
9576
+ }
9577
+ return {
9578
+ ...refMsgIdx ? { refMsgIdx } : {},
9579
+ ...msgIdx ? { msgIdx } : {}
9580
+ };
9581
+ }
9582
+ function resolveEventId(payload, fallbackEventId) {
9583
+ return toString(payload.event_id) ?? toString(payload.eventId) ?? toString(fallbackEventId);
9584
+ }
9585
+ 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";
9586
+ var VOICE_EXTENSIONS = [".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex"];
9587
+ var VOICE_ASR_ERROR_MAX_LENGTH = 500;
9588
+ 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";
9589
+ var DEFAULT_LONG_TASK_NOTICE_DELAY_MS = 3e4;
9590
+ var QQ_GROUP_NO_REPLY_FALLBACK_TEXT = "\u6211\u5728\u3002\u4F60\u53EF\u4EE5\u76F4\u63A5\u8BF4\u5177\u4F53\u4E00\u70B9\u3002";
9591
+ var QQ_QUOTE_BODY_UNAVAILABLE_TEXT = "\u539F\u59CB\u5185\u5BB9\u4E0D\u53EF\u7528";
9592
+ function startLongTaskNoticeTimer(params) {
9593
+ const { delayMs, logger, sendNotice } = params;
8794
9594
  let completed = false;
8795
9595
  let timer = null;
8796
9596
  const clear = () => {
@@ -8823,7 +9623,7 @@ function startLongTaskNoticeTimer(params) {
8823
9623
  }
8824
9624
  };
8825
9625
  }
8826
- function isHttpUrl2(value) {
9626
+ function isHttpUrl3(value) {
8827
9627
  return /^https?:\/\//i.test(value);
8828
9628
  }
8829
9629
  function isImageAttachment(att) {
@@ -8896,7 +9696,7 @@ async function resolveInboundAttachmentsForAgent(params) {
8896
9696
  let asrErrorMessage;
8897
9697
  for (const att of list) {
8898
9698
  const next = { attachment: att };
8899
- if (isImageAttachment(att) && isHttpUrl2(att.url)) {
9699
+ if (isImageAttachment(att) && isHttpUrl3(att.url)) {
8900
9700
  try {
8901
9701
  const downloaded = await downloadToTempFile(att.url, {
8902
9702
  timeout,
@@ -8925,7 +9725,7 @@ async function resolveInboundAttachmentsForAgent(params) {
8925
9725
  logger.info("voice attachment received but ASR is disabled");
8926
9726
  } else if (!asrCredentials) {
8927
9727
  logger.warn("voice ASR enabled but credentials are missing or invalid");
8928
- } else if (!isHttpUrl2(att.url)) {
9728
+ } else if (!isHttpUrl3(att.url)) {
8929
9729
  logger.warn("voice ASR skipped: attachment URL is not an HTTP URL");
8930
9730
  } else {
8931
9731
  try {
@@ -8996,6 +9796,74 @@ function buildInboundContentWithAttachments(params) {
8996
9796
  parts.push(block);
8997
9797
  return parts.join("\n\n");
8998
9798
  }
9799
+ function resolveRefAttachmentType(attachment) {
9800
+ const contentType = attachment.contentType?.trim().toLowerCase() ?? "";
9801
+ if (contentType.startsWith("image/") || isImageAttachment(attachment)) {
9802
+ return "image";
9803
+ }
9804
+ if (contentType === "voice" || contentType.startsWith("audio/") || isVoiceAttachment(attachment)) {
9805
+ return "voice";
9806
+ }
9807
+ if (contentType.startsWith("video/")) {
9808
+ return "video";
9809
+ }
9810
+ const mediaType = detectMediaType(attachment.filename?.trim() || attachment.url);
9811
+ if (mediaType === "image") return "image";
9812
+ if (mediaType === "audio") return "voice";
9813
+ if (mediaType === "video") return "video";
9814
+ if (mediaType === "file") return "file";
9815
+ return "unknown";
9816
+ }
9817
+ function buildInboundRefAttachmentSummaries(attachments) {
9818
+ if (attachments.length === 0) {
9819
+ return void 0;
9820
+ }
9821
+ return attachments.map((item) => ({
9822
+ type: resolveRefAttachmentType(item.attachment),
9823
+ ...item.attachment.filename?.trim() ? { filename: item.attachment.filename.trim() } : {},
9824
+ ...item.attachment.contentType?.trim() ? { contentType: item.attachment.contentType.trim() } : {},
9825
+ ...item.localImagePath?.trim() ? { localPath: item.localImagePath.trim() } : {},
9826
+ ...item.attachment.url?.trim() ? { url: item.attachment.url.trim() } : {},
9827
+ ...item.voiceTranscript?.trim() ? {
9828
+ transcript: item.voiceTranscript.trim(),
9829
+ transcriptSource: "asr"
9830
+ } : {}
9831
+ }));
9832
+ }
9833
+ function buildQuotedAgentBody(params) {
9834
+ const quoteBlock = `[\u5F15\u7528\u6D88\u606F\u5F00\u59CB]
9835
+ ${params.replyToBody}
9836
+ [\u5F15\u7528\u6D88\u606F\u7ED3\u675F]`;
9837
+ return params.baseBody ? `${quoteBlock}
9838
+
9839
+ ${params.baseBody}` : quoteBlock;
9840
+ }
9841
+ function resolveAgentBodyBase(ctx) {
9842
+ if (typeof ctx.BodyForAgent === "string" && ctx.BodyForAgent.trim()) {
9843
+ return ctx.BodyForAgent;
9844
+ }
9845
+ if (typeof ctx.RawBody === "string" && ctx.RawBody.trim()) {
9846
+ return ctx.RawBody;
9847
+ }
9848
+ if (typeof ctx.Body === "string" && ctx.Body.trim()) {
9849
+ return ctx.Body;
9850
+ }
9851
+ if (typeof ctx.CommandBody === "string" && ctx.CommandBody.trim()) {
9852
+ return ctx.CommandBody;
9853
+ }
9854
+ return "";
9855
+ }
9856
+ function uniqueRefIndexKeys(...values) {
9857
+ const keys = [];
9858
+ const seen = /* @__PURE__ */ new Set();
9859
+ for (const value of values) {
9860
+ const next = value?.trim();
9861
+ if (!next || seen.has(next)) continue;
9862
+ seen.add(next);
9863
+ keys.push(next);
9864
+ }
9865
+ return keys;
9866
+ }
8999
9867
  function resolveInboundLogContent(params) {
9000
9868
  const text = params.content.trim();
9001
9869
  if (text) return text;
@@ -9017,22 +9885,23 @@ function sanitizeInboundLogText(text) {
9017
9885
  function parseC2CMessage(data, fallbackEventId) {
9018
9886
  const payload = data;
9019
9887
  const { text, attachments } = parseTextWithAttachments(payload);
9888
+ const refIndices = parseQQBotRefIndices(payload);
9020
9889
  const id = toString(payload.id);
9021
9890
  const eventId = resolveEventId(payload, fallbackEventId);
9022
9891
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9023
- const author = payload.author ?? {};
9892
+ const author = asRecord(payload.author) ?? {};
9024
9893
  const senderId = toString(author.user_openid);
9025
9894
  if (!text && attachments.length === 0 || !id || !senderId) return null;
9026
9895
  return {
9027
9896
  type: "direct",
9028
9897
  senderId,
9029
9898
  c2cOpenid: senderId,
9030
- senderName: toString(author.username),
9031
9899
  content: text,
9032
9900
  attachments: attachments.length > 0 ? attachments : void 0,
9033
9901
  messageId: id,
9034
9902
  eventId,
9035
9903
  timestamp,
9904
+ ...refIndices,
9036
9905
  mentionedBot: false
9037
9906
  };
9038
9907
  }
@@ -9043,13 +9912,12 @@ function parseGroupMessage(data, fallbackEventId) {
9043
9912
  const eventId = resolveEventId(payload, fallbackEventId);
9044
9913
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9045
9914
  const groupOpenid = toString(payload.group_openid);
9046
- const author = payload.author ?? {};
9915
+ const author = asRecord(payload.author) ?? {};
9047
9916
  const senderId = toString(author.member_openid);
9048
9917
  if (!text && attachments.length === 0 || !id || !senderId || !groupOpenid) return null;
9049
9918
  return {
9050
9919
  type: "group",
9051
9920
  senderId,
9052
- senderName: toString(author.nickname) ?? toString(author.username),
9053
9921
  content: text,
9054
9922
  attachments: attachments.length > 0 ? attachments : void 0,
9055
9923
  messageId: id,
@@ -9067,13 +9935,12 @@ function parseChannelMessage(data, fallbackEventId) {
9067
9935
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9068
9936
  const channelId = toString(payload.channel_id);
9069
9937
  const guildId = toString(payload.guild_id);
9070
- const author = payload.author ?? {};
9938
+ const author = asRecord(payload.author) ?? {};
9071
9939
  const senderId = toString(author.id);
9072
9940
  if (!text && attachments.length === 0 || !id || !senderId || !channelId) return null;
9073
9941
  return {
9074
9942
  type: "channel",
9075
9943
  senderId,
9076
- senderName: toString(author.username),
9077
9944
  content: text,
9078
9945
  attachments: attachments.length > 0 ? attachments : void 0,
9079
9946
  messageId: id,
@@ -9091,13 +9958,12 @@ function parseDirectMessage(data, fallbackEventId) {
9091
9958
  const eventId = resolveEventId(payload, fallbackEventId);
9092
9959
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9093
9960
  const guildId = toString(payload.guild_id);
9094
- const author = payload.author ?? {};
9961
+ const author = asRecord(payload.author) ?? {};
9095
9962
  const senderId = toString(author.id);
9096
9963
  if (!text && attachments.length === 0 || !id || !senderId) return null;
9097
9964
  return {
9098
9965
  type: "direct",
9099
9966
  senderId,
9100
- senderName: toString(author.username),
9101
9967
  content: text,
9102
9968
  attachments: attachments.length > 0 ? attachments : void 0,
9103
9969
  messageId: id,
@@ -9146,6 +10012,22 @@ function resolveChatTarget(event) {
9146
10012
  peerKind: "dm"
9147
10013
  };
9148
10014
  }
10015
+ function resolveQQBotEffectiveSessionKey(params) {
10016
+ const { inbound, route, accountId } = params;
10017
+ if (inbound.type !== "direct") {
10018
+ return route.sessionKey;
10019
+ }
10020
+ const senderStableId = inbound.c2cOpenid?.trim() || inbound.senderId?.trim();
10021
+ if (!senderStableId) {
10022
+ return route.sessionKey;
10023
+ }
10024
+ const resolvedAccountId = route.accountId?.trim() || accountId.trim() || DEFAULT_ACCOUNT_ID;
10025
+ return buildQQBotDirectSessionKey({
10026
+ routeSessionKey: route.sessionKey,
10027
+ accountId: resolvedAccountId,
10028
+ senderStableId
10029
+ });
10030
+ }
9149
10031
  function resolveEnvelopeFrom(event) {
9150
10032
  if (event.type === "group") {
9151
10033
  return `group:${(event.groupOpenid ?? "unknown").toLowerCase()}`;
@@ -9156,7 +10038,7 @@ function resolveEnvelopeFrom(event) {
9156
10038
  return event.senderName?.trim() || event.senderId;
9157
10039
  }
9158
10040
  function resolveKnownQQBotTargetFromInbound(params) {
9159
- const { inbound, accountId } = params;
10041
+ const { inbound, accountId, persistentDisplayName } = params;
9160
10042
  if (inbound.type === "direct") {
9161
10043
  if (!inbound.c2cOpenid?.trim()) {
9162
10044
  return void 0;
@@ -9165,7 +10047,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9165
10047
  accountId,
9166
10048
  kind: "user",
9167
10049
  target: `user:${inbound.c2cOpenid}`,
9168
- displayName: inbound.senderName,
10050
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9169
10051
  sourceChatType: "direct",
9170
10052
  firstSeenAt: inbound.timestamp,
9171
10053
  lastSeenAt: inbound.timestamp
@@ -9176,7 +10058,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9176
10058
  accountId,
9177
10059
  kind: "group",
9178
10060
  target: `group:${inbound.groupOpenid}`,
9179
- displayName: inbound.senderName,
10061
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9180
10062
  sourceChatType: "group",
9181
10063
  firstSeenAt: inbound.timestamp,
9182
10064
  lastSeenAt: inbound.timestamp
@@ -9187,7 +10069,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9187
10069
  accountId,
9188
10070
  kind: "channel",
9189
10071
  target: `channel:${inbound.channelId}`,
9190
- displayName: inbound.senderName,
10072
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9191
10073
  sourceChatType: "channel",
9192
10074
  firstSeenAt: inbound.timestamp,
9193
10075
  lastSeenAt: inbound.timestamp
@@ -9302,6 +10184,14 @@ var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_v
9302
10184
  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;
9303
10185
  var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
9304
10186
  var MARKDOWN_TABLE_SEPARATOR_RE = /^\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?)?\|?$/;
10187
+ var MARKDOWN_THEMATIC_BREAK_RE = /^\s{0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/;
10188
+ var MARKDOWN_ATX_HEADING_RE = /^\s{0,3}#{1,6}\s+\S/;
10189
+ var MARKDOWN_BLOCKQUOTE_RE = /^\s{0,3}>\s?/;
10190
+ var MARKDOWN_FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
10191
+ var MARKDOWN_LIST_ITEM_RE = /^\s*(?:[-+*]|\d+\.)\s+/;
10192
+ var MARKDOWN_LIST_CONTINUATION_RE = /^\s{2,}\S/;
10193
+ var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[^~\n]+~~|\*[^*\n]+\*)/;
10194
+ var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
9305
10195
  var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
9306
10196
  var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
9307
10197
  function extractFinalBlocks(text) {
@@ -9370,8 +10260,9 @@ function evaluateReplyFinalOnlyDelivery(params) {
9370
10260
  }
9371
10261
  function isQQBotC2CTarget(to) {
9372
10262
  const trimmed = to.trim();
9373
- const raw = trimmed.startsWith("qqbot:") ? trimmed.slice("qqbot:".length) : trimmed;
9374
- return !raw.startsWith("group:") && !raw.startsWith("channel:");
10263
+ const raw = trimmed.slice(0, "qqbot:".length).toLowerCase() === "qqbot:" ? trimmed.slice("qqbot:".length) : trimmed;
10264
+ const normalizedRaw = raw.toLowerCase();
10265
+ return !normalizedRaw.startsWith("group:") && !normalizedRaw.startsWith("channel:");
9375
10266
  }
9376
10267
  function splitQQBotMarkdownTransportMediaUrls(mediaUrls) {
9377
10268
  const markdownImageUrls = [];
@@ -9453,36 +10344,619 @@ function normalizeQQBotRenderedMarkdown(text) {
9453
10344
  if (!normalizedInner) {
9454
10345
  return block;
9455
10346
  }
9456
- changed = true;
9457
- return `${leadingLineBreak}${normalizedInner}`;
10347
+ changed = true;
10348
+ return `${leadingLineBreak}${normalizedInner}`;
10349
+ }
10350
+ );
10351
+ next = next.replace(
10352
+ GENERIC_MARKDOWN_FENCE_RE,
10353
+ (block, leadingLineBreak, _fence, inner) => {
10354
+ const normalizedInner = inner.trim();
10355
+ if (!normalizedInner) {
10356
+ return block;
10357
+ }
10358
+ if (!hasQQBotMarkdownTable(normalizedInner)) {
10359
+ return block;
10360
+ }
10361
+ changed = true;
10362
+ return `${leadingLineBreak}${normalizedInner}`;
10363
+ }
10364
+ );
10365
+ return changed ? next.trim() : text.trim();
10366
+ }
10367
+ function normalizeQQBotMarkdownSegment(text) {
10368
+ return text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
10369
+ }
10370
+ function isBlankQQBotMarkdownLine(line) {
10371
+ return line.trim().length === 0;
10372
+ }
10373
+ function resolveQQBotFenceDelimiter(line) {
10374
+ const match = line.match(MARKDOWN_FENCE_RE);
10375
+ return match?.[1];
10376
+ }
10377
+ function isQQBotFenceClosingLine(line, delimiter) {
10378
+ const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10379
+ const closingRe = new RegExp(`^\\s*${escapedDelimiter}${delimiter[0]}*\\s*$`);
10380
+ return closingRe.test(line);
10381
+ }
10382
+ function joinQQBotMarkdownPieces(parts) {
10383
+ return parts.filter(Boolean).join("\n\n").trim();
10384
+ }
10385
+ function isQQBotMarkdownTableStart(lines, index) {
10386
+ const header = lines[index]?.trim() ?? "";
10387
+ const separator = lines[index + 1]?.trim() ?? "";
10388
+ return Boolean(header.includes("|") && MARKDOWN_TABLE_SEPARATOR_RE.test(separator));
10389
+ }
10390
+ function collectQQBotFencedCodeBlock(lines, startIndex) {
10391
+ const openingLine = lines[startIndex] ?? "";
10392
+ const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
10393
+ let index = startIndex + 1;
10394
+ while (index < lines.length) {
10395
+ if (isQQBotFenceClosingLine(lines[index] ?? "", delimiter)) {
10396
+ index += 1;
10397
+ break;
10398
+ }
10399
+ index += 1;
10400
+ }
10401
+ return {
10402
+ block: {
10403
+ kind: "code",
10404
+ text: lines.slice(startIndex, index).join("\n").trimEnd()
10405
+ },
10406
+ nextIndex: index
10407
+ };
10408
+ }
10409
+ function collectQQBotMarkdownTableBlock(lines, startIndex) {
10410
+ let index = startIndex + 2;
10411
+ while (index < lines.length) {
10412
+ const line = lines[index] ?? "";
10413
+ if (isBlankQQBotMarkdownLine(line) || !line.includes("|")) {
10414
+ break;
10415
+ }
10416
+ index += 1;
10417
+ }
10418
+ return {
10419
+ block: {
10420
+ kind: "table",
10421
+ text: lines.slice(startIndex, index).join("\n").trimEnd()
10422
+ },
10423
+ nextIndex: index
10424
+ };
10425
+ }
10426
+ function collectQQBotBlockquoteBlock(lines, startIndex) {
10427
+ const collected = [];
10428
+ let index = startIndex;
10429
+ while (index < lines.length) {
10430
+ const line = lines[index] ?? "";
10431
+ if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
10432
+ collected.push(line);
10433
+ index += 1;
10434
+ continue;
10435
+ }
10436
+ if (isBlankQQBotMarkdownLine(line) && index + 1 < lines.length && MARKDOWN_BLOCKQUOTE_RE.test(lines[index + 1] ?? "")) {
10437
+ collected.push(line);
10438
+ index += 1;
10439
+ continue;
10440
+ }
10441
+ break;
10442
+ }
10443
+ return {
10444
+ block: {
10445
+ kind: "blockquote",
10446
+ text: collected.join("\n").trimEnd()
10447
+ },
10448
+ nextIndex: index
10449
+ };
10450
+ }
10451
+ function collectQQBotListBlock(lines, startIndex) {
10452
+ const collected = [];
10453
+ let index = startIndex;
10454
+ while (index < lines.length) {
10455
+ const line = lines[index] ?? "";
10456
+ if (isBlankQQBotMarkdownLine(line)) {
10457
+ break;
10458
+ }
10459
+ if (MARKDOWN_FENCE_RE.test(line) || MARKDOWN_BLOCKQUOTE_RE.test(line) || MARKDOWN_ATX_HEADING_RE.test(line) || MARKDOWN_THEMATIC_BREAK_RE.test(line) || isQQBotMarkdownTableStart(lines, index)) {
10460
+ break;
10461
+ }
10462
+ if (collected.length > 0 && !MARKDOWN_LIST_ITEM_RE.test(line) && !MARKDOWN_LIST_CONTINUATION_RE.test(line)) {
10463
+ collected.push(line);
10464
+ index += 1;
10465
+ continue;
10466
+ }
10467
+ collected.push(line);
10468
+ index += 1;
10469
+ }
10470
+ return {
10471
+ block: {
10472
+ kind: "list",
10473
+ text: collected.join("\n").trimEnd()
10474
+ },
10475
+ nextIndex: index
10476
+ };
10477
+ }
10478
+ function collectQQBotParagraphBlock(lines, startIndex) {
10479
+ const collected = [];
10480
+ let index = startIndex;
10481
+ while (index < lines.length) {
10482
+ const line = lines[index] ?? "";
10483
+ if (isBlankQQBotMarkdownLine(line)) {
10484
+ break;
10485
+ }
10486
+ if (collected.length > 0 && (MARKDOWN_FENCE_RE.test(line) || MARKDOWN_BLOCKQUOTE_RE.test(line) || MARKDOWN_ATX_HEADING_RE.test(line) || MARKDOWN_THEMATIC_BREAK_RE.test(line) || MARKDOWN_LIST_ITEM_RE.test(line) || isQQBotMarkdownTableStart(lines, index))) {
10487
+ break;
10488
+ }
10489
+ collected.push(line);
10490
+ index += 1;
10491
+ }
10492
+ return {
10493
+ block: {
10494
+ kind: "paragraph",
10495
+ text: collected.join("\n").trimEnd()
10496
+ },
10497
+ nextIndex: index
10498
+ };
10499
+ }
10500
+ function parseQQBotMarkdownBlocks(text) {
10501
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
10502
+ const blocks = [];
10503
+ let index = 0;
10504
+ while (index < lines.length) {
10505
+ while (index < lines.length && isBlankQQBotMarkdownLine(lines[index] ?? "")) {
10506
+ index += 1;
10507
+ }
10508
+ if (index >= lines.length) {
10509
+ break;
10510
+ }
10511
+ const line = lines[index] ?? "";
10512
+ if (MARKDOWN_FENCE_RE.test(line)) {
10513
+ const result2 = collectQQBotFencedCodeBlock(lines, index);
10514
+ blocks.push(result2.block);
10515
+ index = result2.nextIndex;
10516
+ continue;
10517
+ }
10518
+ if (isQQBotMarkdownTableStart(lines, index)) {
10519
+ const result2 = collectQQBotMarkdownTableBlock(lines, index);
10520
+ blocks.push(result2.block);
10521
+ index = result2.nextIndex;
10522
+ continue;
10523
+ }
10524
+ if (MARKDOWN_THEMATIC_BREAK_RE.test(line)) {
10525
+ blocks.push({ kind: "thematic-break", text: line.trim() });
10526
+ index += 1;
10527
+ continue;
10528
+ }
10529
+ if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
10530
+ const result2 = collectQQBotBlockquoteBlock(lines, index);
10531
+ blocks.push(result2.block);
10532
+ index = result2.nextIndex;
10533
+ continue;
10534
+ }
10535
+ if (MARKDOWN_ATX_HEADING_RE.test(line)) {
10536
+ blocks.push({ kind: "heading", text: line.trimEnd() });
10537
+ index += 1;
10538
+ continue;
10539
+ }
10540
+ if (MARKDOWN_LIST_ITEM_RE.test(line)) {
10541
+ const result2 = collectQQBotListBlock(lines, index);
10542
+ blocks.push(result2.block);
10543
+ index = result2.nextIndex;
10544
+ continue;
10545
+ }
10546
+ const result = collectQQBotParagraphBlock(lines, index);
10547
+ blocks.push(result.block);
10548
+ index = result.nextIndex;
10549
+ }
10550
+ return blocks;
10551
+ }
10552
+ function hasQQBotBoundaryGuard(text) {
10553
+ return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
10554
+ }
10555
+ function isQQBotSafeMarkdownBoundary(text, index) {
10556
+ const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
10557
+ const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
10558
+ const leftEdge = left.slice(-1);
10559
+ const rightEdge = right.slice(0, 1);
10560
+ return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
10561
+ }
10562
+ function findQQBotRegexBoundary(text, limit, pattern) {
10563
+ const scopedText = text.slice(0, Math.min(limit + 1, text.length));
10564
+ const regex = new RegExp(pattern.source, pattern.flags);
10565
+ let match = regex.exec(scopedText);
10566
+ let lastBoundary;
10567
+ while (match) {
10568
+ const boundary = match.index + match[0].length;
10569
+ if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10570
+ lastBoundary = boundary;
10571
+ }
10572
+ match = regex.exec(scopedText);
10573
+ }
10574
+ return lastBoundary;
10575
+ }
10576
+ function findQQBotFallbackBoundary(text, limit) {
10577
+ const minIndex = Math.max(1, limit - 120);
10578
+ for (let index = limit; index >= minIndex; index -= 1) {
10579
+ if (isQQBotSafeMarkdownBoundary(text, index)) {
10580
+ return index;
10581
+ }
10582
+ }
10583
+ return limit;
10584
+ }
10585
+ function findQQBotSafeSplitIndex(text, limit) {
10586
+ const boundaryPatterns = [
10587
+ /\n\n+/g,
10588
+ /\n/g,
10589
+ /[。!?.!?;;::](?:\s+|$)/g,
10590
+ /[,,](?:\s+|$)/g,
10591
+ /\s+/g
10592
+ ];
10593
+ for (const pattern of boundaryPatterns) {
10594
+ const boundary = findQQBotRegexBoundary(text, limit, pattern);
10595
+ if (boundary && boundary > 0) {
10596
+ return boundary;
10597
+ }
10598
+ }
10599
+ return findQQBotFallbackBoundary(text, limit);
10600
+ }
10601
+ function splitQQBotHardText(text, limit) {
10602
+ if (limit <= 0 || text.length <= limit) {
10603
+ return [text];
10604
+ }
10605
+ const chunks = [];
10606
+ let remaining = text;
10607
+ while (remaining.length > limit) {
10608
+ chunks.push(remaining.slice(0, limit));
10609
+ remaining = remaining.slice(limit);
10610
+ }
10611
+ if (remaining) {
10612
+ chunks.push(remaining);
10613
+ }
10614
+ return chunks;
10615
+ }
10616
+ function splitQQBotTextSafely(text, limit, options) {
10617
+ if (limit <= 0 || text.length <= limit) {
10618
+ return [text];
10619
+ }
10620
+ const trimLeading = options?.trimLeading ?? true;
10621
+ const trimTrailing = options?.trimTrailing ?? true;
10622
+ const chunks = [];
10623
+ let remaining = text;
10624
+ while (remaining.length > limit) {
10625
+ const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
10626
+ let nextChunk = remaining.slice(0, splitIndex);
10627
+ let nextRemaining = remaining.slice(splitIndex);
10628
+ if (trimTrailing) {
10629
+ nextChunk = nextChunk.trimEnd();
10630
+ }
10631
+ if (trimLeading) {
10632
+ nextRemaining = nextRemaining.trimStart();
10633
+ }
10634
+ if (!nextChunk) {
10635
+ const hardChunk = remaining.slice(0, limit);
10636
+ chunks.push(hardChunk);
10637
+ remaining = remaining.slice(hardChunk.length);
10638
+ continue;
10639
+ }
10640
+ chunks.push(nextChunk);
10641
+ remaining = nextRemaining;
10642
+ }
10643
+ const finalChunk = trimTrailing ? remaining.trimEnd() : remaining;
10644
+ if (finalChunk) {
10645
+ chunks.push(finalChunk);
10646
+ }
10647
+ return chunks;
10648
+ }
10649
+ function splitQQBotMarkdownLineBlock(text, limit) {
10650
+ if (limit <= 0 || text.length <= limit) {
10651
+ return [text];
10652
+ }
10653
+ const lines = text.split("\n");
10654
+ const chunks = [];
10655
+ let currentLines = [];
10656
+ const flushCurrent = () => {
10657
+ if (currentLines.length === 0) {
10658
+ return;
10659
+ }
10660
+ const chunk = currentLines.join("\n").trimEnd();
10661
+ if (chunk) {
10662
+ chunks.push(chunk);
10663
+ }
10664
+ currentLines = [];
10665
+ };
10666
+ for (const line of lines) {
10667
+ const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
10668
+ ${line}` : line;
10669
+ if (candidate.length <= limit) {
10670
+ currentLines.push(line);
10671
+ continue;
10672
+ }
10673
+ flushCurrent();
10674
+ if (line.length <= limit) {
10675
+ currentLines.push(line);
10676
+ continue;
10677
+ }
10678
+ for (const piece of splitQQBotTextSafely(line, limit, {
10679
+ trimLeading: false,
10680
+ trimTrailing: false
10681
+ })) {
10682
+ if (piece) {
10683
+ chunks.push(piece);
10684
+ }
10685
+ }
10686
+ }
10687
+ flushCurrent();
10688
+ return chunks;
10689
+ }
10690
+ function splitQQBotMarkdownTableBlock(text, limit) {
10691
+ if (limit <= 0 || text.length <= limit) {
10692
+ return [text];
10693
+ }
10694
+ const lines = text.split("\n");
10695
+ const header = lines[0] ?? "";
10696
+ const separator = lines[1] ?? "";
10697
+ const rows = lines.slice(2);
10698
+ const tablePrefix = `${header}
10699
+ ${separator}`;
10700
+ const chunks = [];
10701
+ let currentRows = [];
10702
+ const flushCurrent = () => {
10703
+ if (currentRows.length === 0) {
10704
+ return;
10705
+ }
10706
+ chunks.push(`${tablePrefix}
10707
+ ${currentRows.join("\n")}`);
10708
+ currentRows = [];
10709
+ };
10710
+ for (const row of rows) {
10711
+ const candidate = currentRows.length > 0 ? `${tablePrefix}
10712
+ ${currentRows.join("\n")}
10713
+ ${row}` : `${tablePrefix}
10714
+ ${row}`;
10715
+ if (candidate.length <= limit) {
10716
+ currentRows.push(row);
10717
+ continue;
10718
+ }
10719
+ flushCurrent();
10720
+ if (`${tablePrefix}
10721
+ ${row}`.length <= limit) {
10722
+ currentRows.push(row);
10723
+ continue;
10724
+ }
10725
+ const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
10726
+ for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
10727
+ trimLeading: false,
10728
+ trimTrailing: false
10729
+ })) {
10730
+ chunks.push(`${tablePrefix}
10731
+ ${rowPiece}`);
10732
+ }
10733
+ }
10734
+ flushCurrent();
10735
+ return chunks.length > 0 ? chunks : [text];
10736
+ }
10737
+ function splitQQBotMarkdownCodeFence(text, limit) {
10738
+ if (limit <= 0 || text.length <= limit) {
10739
+ return [text];
10740
+ }
10741
+ const lines = text.split("\n");
10742
+ const openingLine = lines[0] ?? "```";
10743
+ const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
10744
+ const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
10745
+ const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
10746
+ const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
10747
+ const fixedOverhead = openingLine.length + closingLine.length + 2;
10748
+ const availableLineLength = Math.max(1, limit - fixedOverhead);
10749
+ const chunks = [];
10750
+ let currentCodeLines = [];
10751
+ const flushCurrent = () => {
10752
+ if (currentCodeLines.length === 0) {
10753
+ return;
10754
+ }
10755
+ chunks.push(`${openingLine}
10756
+ ${currentCodeLines.join("\n")}
10757
+ ${closingLine}`);
10758
+ currentCodeLines = [];
10759
+ };
10760
+ for (const codeLine of codeLines) {
10761
+ const candidate = currentCodeLines.length > 0 ? `${openingLine}
10762
+ ${currentCodeLines.join("\n")}
10763
+ ${codeLine}
10764
+ ${closingLine}` : `${openingLine}
10765
+ ${codeLine}
10766
+ ${closingLine}`;
10767
+ if (candidate.length <= limit) {
10768
+ currentCodeLines.push(codeLine);
10769
+ continue;
10770
+ }
10771
+ flushCurrent();
10772
+ if (`${openingLine}
10773
+ ${codeLine}
10774
+ ${closingLine}`.length <= limit) {
10775
+ currentCodeLines.push(codeLine);
10776
+ continue;
10777
+ }
10778
+ for (const linePiece of splitQQBotHardText(codeLine, availableLineLength)) {
10779
+ chunks.push(`${openingLine}
10780
+ ${linePiece}
10781
+ ${closingLine}`);
10782
+ }
10783
+ }
10784
+ flushCurrent();
10785
+ return chunks.length > 0 ? chunks : [text];
10786
+ }
10787
+ function splitQQBotMarkdownBlock(block, limit) {
10788
+ if (limit <= 0 || block.text.length <= limit) {
10789
+ return [block.text];
10790
+ }
10791
+ switch (block.kind) {
10792
+ case "table":
10793
+ return splitQQBotMarkdownTableBlock(block.text, limit);
10794
+ case "code":
10795
+ return splitQQBotMarkdownCodeFence(block.text, limit);
10796
+ case "blockquote":
10797
+ return splitQQBotMarkdownLineBlock(block.text, limit);
10798
+ case "list":
10799
+ return splitQQBotMarkdownLineBlock(block.text, limit);
10800
+ case "paragraph":
10801
+ case "heading":
10802
+ return splitQQBotTextSafely(block.text, limit);
10803
+ case "thematic-break":
10804
+ return [block.text];
10805
+ default:
10806
+ return [block.text];
10807
+ }
10808
+ }
10809
+ function chunkQQBotStructuredMarkdown(text, limit) {
10810
+ const blocks = parseQQBotMarkdownBlocks(text);
10811
+ if (blocks.length === 0 || limit <= 0) {
10812
+ return [text.trim()];
10813
+ }
10814
+ const chunks = [];
10815
+ let currentPieces = [];
10816
+ let pendingPrefixPieces = [];
10817
+ const flushCurrent = () => {
10818
+ if (currentPieces.length === 0) {
10819
+ return;
10820
+ }
10821
+ const chunk = joinQQBotMarkdownPieces(currentPieces);
10822
+ if (chunk) {
10823
+ chunks.push(chunk);
10824
+ }
10825
+ currentPieces = [];
10826
+ };
10827
+ const appendPiece = (piece) => {
10828
+ if (!piece) {
10829
+ return;
10830
+ }
10831
+ const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
10832
+ for (const nextPiece of pieces) {
10833
+ const normalizedPiece = nextPiece.trim();
10834
+ if (!normalizedPiece) {
10835
+ continue;
10836
+ }
10837
+ const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
10838
+ if (currentPieces.length === 0 || candidate.length <= limit) {
10839
+ currentPieces.push(normalizedPiece);
10840
+ continue;
10841
+ }
10842
+ flushCurrent();
10843
+ currentPieces.push(normalizedPiece);
9458
10844
  }
9459
- );
9460
- next = next.replace(
9461
- GENERIC_MARKDOWN_FENCE_RE,
9462
- (block, leadingLineBreak, _fence, inner) => {
9463
- const normalizedInner = inner.trim();
9464
- if (!normalizedInner) {
9465
- return block;
10845
+ };
10846
+ const consumePendingPrefix = (piece) => {
10847
+ if (pendingPrefixPieces.length === 0) {
10848
+ return piece;
10849
+ }
10850
+ const prefixed = joinQQBotMarkdownPieces([...pendingPrefixPieces, piece]);
10851
+ pendingPrefixPieces = [];
10852
+ return prefixed;
10853
+ };
10854
+ for (let index = 0; index < blocks.length; index += 1) {
10855
+ const block = blocks[index];
10856
+ if (!block) {
10857
+ continue;
10858
+ }
10859
+ if (block.kind === "thematic-break") {
10860
+ if (currentPieces.length > 0) {
10861
+ const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
10862
+ if (candidate.length <= limit) {
10863
+ currentPieces.push(block.text);
10864
+ continue;
10865
+ }
10866
+ flushCurrent();
9466
10867
  }
9467
- if (!hasQQBotMarkdownTable(normalizedInner)) {
9468
- return block;
10868
+ pendingPrefixPieces.push(block.text);
10869
+ continue;
10870
+ }
10871
+ if (block.kind === "heading") {
10872
+ const headingText = consumePendingPrefix(block.text);
10873
+ const nextBlock = blocks[index + 1];
10874
+ if (nextBlock && nextBlock.kind !== "thematic-break") {
10875
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
10876
+ const firstBodyPiece = nextPieces[0];
10877
+ if (firstBodyPiece) {
10878
+ const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
10879
+ const pairedCandidate = joinQQBotMarkdownPieces([
10880
+ ...currentPieces,
10881
+ headingText,
10882
+ firstBodyPiece
10883
+ ]);
10884
+ if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
10885
+ currentPieces.push(headingText, firstBodyPiece);
10886
+ for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10887
+ appendPiece(nextPieces[pieceIndex] ?? "");
10888
+ }
10889
+ index += 1;
10890
+ continue;
10891
+ }
10892
+ if (currentPieces.length > 0 && pairedText.length <= limit) {
10893
+ flushCurrent();
10894
+ currentPieces.push(headingText, firstBodyPiece);
10895
+ for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10896
+ appendPiece(nextPieces[pieceIndex] ?? "");
10897
+ }
10898
+ index += 1;
10899
+ continue;
10900
+ }
10901
+ }
9469
10902
  }
9470
- changed = true;
9471
- return `${leadingLineBreak}${normalizedInner}`;
10903
+ appendPiece(headingText);
10904
+ continue;
9472
10905
  }
9473
- );
9474
- return changed ? next.trim() : text.trim();
10906
+ const blockText = consumePendingPrefix(block.text);
10907
+ for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
10908
+ appendPiece(piece);
10909
+ }
10910
+ }
10911
+ if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
10912
+ const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
10913
+ if (trailingCandidate.length <= limit) {
10914
+ currentPieces.push(...pendingPrefixPieces);
10915
+ }
10916
+ }
10917
+ flushCurrent();
10918
+ return chunks.length > 0 ? chunks : [text.trim()];
10919
+ }
10920
+ function looksLikeStructuredMarkdown(text) {
10921
+ const normalized = normalizeQQBotMarkdownSegment(text);
10922
+ if (!normalized) {
10923
+ return false;
10924
+ }
10925
+ const lines = normalized.split("\n");
10926
+ if (hasQQBotMarkdownTable(normalized)) {
10927
+ return true;
10928
+ }
10929
+ return normalized.includes("\n\n") || lines.some((line) => MARKDOWN_ATX_HEADING_RE.test(line)) || lines.some((line) => MARKDOWN_BLOCKQUOTE_RE.test(line)) || lines.some((line) => MARKDOWN_FENCE_RE.test(line)) || lines.some((line) => MARKDOWN_THEMATIC_BREAK_RE.test(line)) || lines.some((line) => MARKDOWN_LIST_ITEM_RE.test(line)) || MARKDOWN_INLINE_STRUCTURE_RE.test(normalized);
10930
+ }
10931
+ function chunkC2CMarkdownText(params) {
10932
+ const normalized = params.text.trim();
10933
+ if (!normalized) {
10934
+ return [];
10935
+ }
10936
+ const strategy = params.strategy ?? "markdown-block";
10937
+ if (strategy === "length") {
10938
+ return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10939
+ }
10940
+ if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
10941
+ return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10942
+ }
10943
+ return chunkQQBotStructuredMarkdown(normalized, params.limit);
9475
10944
  }
9476
10945
  async function sendQQBotMediaWithFallback(params) {
9477
- const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger, onDelivered, onError } = params;
10946
+ const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
9478
10947
  const outbound = params.outbound ?? qqbotOutbound;
10948
+ const shouldContinue = params.shouldContinue ?? (() => true);
9479
10949
  for (const mediaUrl of mediaQueue) {
10950
+ if (!shouldContinue()) {
10951
+ return;
10952
+ }
9480
10953
  const result = await outbound.sendMedia({
9481
10954
  cfg: { channels: { qqbot: qqCfg } },
9482
10955
  to,
9483
10956
  mediaUrl,
9484
10957
  replyToId,
9485
- replyEventId
10958
+ replyEventId,
10959
+ accountId
9486
10960
  });
9487
10961
  if (result.error) {
9488
10962
  logger.error(`sendMedia failed: ${result.error}`);
@@ -9491,12 +10965,16 @@ async function sendQQBotMediaWithFallback(params) {
9491
10965
  if (!fallback) {
9492
10966
  continue;
9493
10967
  }
10968
+ if (!shouldContinue()) {
10969
+ return;
10970
+ }
9494
10971
  const fallbackResult = await outbound.sendText({
9495
10972
  cfg: { channels: { qqbot: qqCfg } },
9496
10973
  to,
9497
10974
  text: fallback,
9498
10975
  replyToId,
9499
- replyEventId
10976
+ replyEventId,
10977
+ accountId
9500
10978
  });
9501
10979
  if (fallbackResult.error) {
9502
10980
  logger.error(`sendText fallback failed: ${fallbackResult.error}`);
@@ -9541,17 +11019,30 @@ function buildInboundContext(params) {
9541
11019
  async function dispatchToAgent(params) {
9542
11020
  const { inbound, cfg, qqCfg, accountId, logger, route } = params;
9543
11021
  const runtime2 = getQQBotRuntime();
11022
+ const routeSessionKey = resolveQQBotRouteSessionKey(route);
11023
+ const queueKey = buildSessionDispatchQueueKey(route);
11024
+ const isFastAbortCommand = isQQBotFastAbortCommandText(inbound.content);
11025
+ const dispatchAbortGeneration = getSessionDispatchState(queueKey).abortGeneration;
11026
+ const shouldSuppressVisibleReplies = () => {
11027
+ const currentAbortGeneration = sessionDispatchQueue.get(queueKey)?.abortGeneration ?? dispatchAbortGeneration;
11028
+ return currentAbortGeneration !== dispatchAbortGeneration;
11029
+ };
9544
11030
  const target = resolveChatTarget(inbound);
9545
- if (inbound.c2cOpenid) {
11031
+ const outboundAccountId = route.accountId ?? accountId;
11032
+ let typingRefIdx;
11033
+ if (inbound.c2cOpenid && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
9546
11034
  const typing = await qqbotOutbound.sendTyping({
9547
11035
  cfg: { channels: { qqbot: qqCfg } },
9548
11036
  to: `user:${inbound.c2cOpenid}`,
9549
11037
  replyToId: inbound.messageId,
9550
11038
  replyEventId: inbound.eventId,
9551
- inputSecond: 60
11039
+ inputSecond: 60,
11040
+ accountId: outboundAccountId
9552
11041
  });
9553
11042
  if (typing.error) {
9554
11043
  logger.warn(`sendTyping failed: ${typing.error}`);
11044
+ } else {
11045
+ typingRefIdx = typing.refIdx;
9555
11046
  }
9556
11047
  }
9557
11048
  const replyApi = runtime2.channel?.reply;
@@ -9576,13 +11067,14 @@ async function dispatchToAgent(params) {
9576
11067
  delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
9577
11068
  logger,
9578
11069
  sendNotice: async () => {
9579
- if (groupMessageInterfaceBlocked) return;
11070
+ if (groupMessageInterfaceBlocked || isFastAbortCommand || shouldSuppressVisibleReplies()) return;
9580
11071
  const result = await qqbotOutbound.sendText({
9581
11072
  cfg: { channels: { qqbot: qqCfg } },
9582
11073
  to: target.to,
9583
11074
  text: LONG_TASK_NOTICE_TEXT,
9584
11075
  replyToId: inbound.messageId,
9585
- replyEventId: inbound.eventId
11076
+ replyEventId: inbound.eventId,
11077
+ accountId: outboundAccountId
9586
11078
  });
9587
11079
  if (result.error) {
9588
11080
  logger.warn(`send long-task notice failed: ${result.error}`);
@@ -9602,19 +11094,23 @@ async function dispatchToAgent(params) {
9602
11094
  { agentId: route.agentId }
9603
11095
  );
9604
11096
  const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
9605
- const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
11097
+ const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: routeSessionKey }) : null;
9606
11098
  const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
9607
11099
  attachments: inbound.attachments,
9608
11100
  qqCfg,
9609
11101
  logger
9610
11102
  });
9611
11103
  if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
11104
+ if (shouldSuppressVisibleReplies()) {
11105
+ return;
11106
+ }
9612
11107
  const fallback = await qqbotOutbound.sendText({
9613
11108
  cfg: { channels: { qqbot: qqCfg } },
9614
11109
  to: target.to,
9615
11110
  text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
9616
11111
  replyToId: inbound.messageId,
9617
- replyEventId: inbound.eventId
11112
+ replyEventId: inbound.eventId,
11113
+ accountId: outboundAccountId
9618
11114
  });
9619
11115
  if (fallback.error) {
9620
11116
  logger.error(`sendText ASR fallback failed: ${fallback.error}`);
@@ -9629,6 +11125,39 @@ async function dispatchToAgent(params) {
9629
11125
  if (localImageCount > 0) {
9630
11126
  logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
9631
11127
  }
11128
+ let replyToId;
11129
+ let replyToBody;
11130
+ let replyToSender;
11131
+ let replyToIsQuote = false;
11132
+ if (inbound.c2cOpenid && inbound.refMsgIdx) {
11133
+ replyToId = inbound.refMsgIdx;
11134
+ replyToIsQuote = true;
11135
+ const refEntry = getRefIndex(inbound.refMsgIdx);
11136
+ if (refEntry) {
11137
+ replyToBody = formatRefEntryForAgent(refEntry);
11138
+ replyToSender = refEntry.senderName ?? refEntry.senderId;
11139
+ logger.info(`quote context resolved refMsgIdx=${inbound.refMsgIdx}`);
11140
+ } else {
11141
+ replyToBody = QQ_QUOTE_BODY_UNAVAILABLE_TEXT;
11142
+ logger.warn(`quote context missing refMsgIdx=${inbound.refMsgIdx}`);
11143
+ }
11144
+ }
11145
+ const refAttachmentSummaries = buildInboundRefAttachmentSummaries(resolvedAttachments);
11146
+ const currentRefIndexKeys = inbound.c2cOpenid ? uniqueRefIndexKeys(inbound.msgIdx, typingRefIdx) : [];
11147
+ if (currentRefIndexKeys.length > 0) {
11148
+ for (const currentRefIndexKey of currentRefIndexKeys) {
11149
+ setRefIndex(currentRefIndexKey, {
11150
+ content: inbound.content,
11151
+ senderId: inbound.senderId,
11152
+ ...inbound.senderName ? { senderName: inbound.senderName } : {},
11153
+ timestamp: inbound.timestamp,
11154
+ ...refAttachmentSummaries ? { attachments: refAttachmentSummaries } : {}
11155
+ });
11156
+ }
11157
+ logger.info(
11158
+ `cached inbound ref_idx keys=${currentRefIndexKeys.join(",")} msgIdx=${inbound.msgIdx ?? "-"} typingRefIdx=${typingRefIdx ?? "-"}`
11159
+ );
11160
+ }
9632
11161
  const rawBody = buildInboundContentWithAttachments({
9633
11162
  content: inbound.content,
9634
11163
  attachments: resolvedAttachments
@@ -9654,51 +11183,73 @@ async function dispatchToAgent(params) {
9654
11183
  }) : rawBody;
9655
11184
  const inboundCtx = buildInboundContext({
9656
11185
  event: inbound,
9657
- sessionKey: route.sessionKey,
9658
- accountId: route.accountId ?? accountId,
11186
+ sessionKey: routeSessionKey,
11187
+ accountId: outboundAccountId,
9659
11188
  body: inboundBody,
9660
11189
  rawBody,
9661
11190
  commandBody: rawBody
9662
11191
  });
9663
11192
  const finalizeInboundContext = replyApi?.finalizeInboundContext;
9664
11193
  const finalCtx = finalizeInboundContext ? finalizeInboundContext(inboundCtx) : inboundCtx;
9665
- let cronBase = "";
9666
- if (typeof finalCtx.RawBody === "string" && finalCtx.RawBody) {
9667
- cronBase = finalCtx.RawBody;
9668
- } else if (typeof finalCtx.Body === "string" && finalCtx.Body) {
9669
- cronBase = finalCtx.Body;
9670
- } else if (typeof finalCtx.CommandBody === "string" && finalCtx.CommandBody) {
9671
- cronBase = finalCtx.CommandBody;
9672
- }
9673
- if (cronBase) {
9674
- const nextCron = appendCronHiddenPrompt(cronBase);
9675
- if (nextCron !== cronBase) {
9676
- finalCtx.BodyForAgent = nextCron;
9677
- }
9678
- }
9679
- if (storePath && sessionApi?.recordInboundSession) {
9680
- try {
9681
- const mainSessionKeyRaw = route?.mainSessionKey;
9682
- const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw : void 0;
9683
- const isGroup = inbound.type === "group" || inbound.type === "channel";
9684
- const updateLastRoute = !isGroup ? {
9685
- sessionKey: mainSessionKey ?? route.sessionKey,
9686
- channel: "qqbot",
9687
- to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${inbound.senderId}`,
9688
- accountId: route.accountId ?? accountId
9689
- } : void 0;
9690
- const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : route.sessionKey;
9691
- await sessionApi.recordInboundSession({
9692
- storePath,
9693
- sessionKey: recordSessionKey,
9694
- ctx: finalCtx,
9695
- updateLastRoute,
9696
- onRecordError: (err) => {
9697
- logger.warn(`failed to record inbound session: ${String(err)}`);
9698
- }
11194
+ const ctxTo = normalizeQQBotReplyTarget(finalCtx.To);
11195
+ const ctxOriginatingTo = normalizeQQBotReplyTarget(finalCtx.OriginatingTo);
11196
+ const stableTo = ctxOriginatingTo ?? ctxTo ?? target.to;
11197
+ finalCtx.To = stableTo;
11198
+ finalCtx.OriginatingTo = stableTo;
11199
+ if (replyToId) {
11200
+ finalCtx.ReplyToId = replyToId;
11201
+ finalCtx.ReplyToBody = replyToBody;
11202
+ finalCtx.ReplyToSender = replyToSender;
11203
+ finalCtx.ReplyToIsQuote = replyToIsQuote;
11204
+ }
11205
+ const isSlashCommand = typeof finalCtx.CommandBody === "string" ? finalCtx.CommandBody.trim().startsWith("/") : typeof finalCtx.RawBody === "string" ? finalCtx.RawBody.trim().startsWith("/") : false;
11206
+ if (!isSlashCommand) {
11207
+ let agentBody = resolveAgentBodyBase(finalCtx);
11208
+ if (replyToIsQuote && replyToBody && replyToBody !== QQ_QUOTE_BODY_UNAVAILABLE_TEXT) {
11209
+ agentBody = buildQuotedAgentBody({
11210
+ baseBody: agentBody,
11211
+ replyToBody
9699
11212
  });
9700
- } catch (err) {
9701
- logger.warn(`failed to record inbound session: ${String(err)}`);
11213
+ }
11214
+ finalCtx.BodyForAgent = appendCronHiddenPrompt(agentBody);
11215
+ }
11216
+ if (storePath) {
11217
+ const mainSessionKeyRaw = route.mainSessionKey;
11218
+ const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw.trim() : void 0;
11219
+ const isGroup = inbound.type === "group" || inbound.type === "channel";
11220
+ const updateLastRoute = !isGroup ? {
11221
+ sessionKey: mainSessionKey ?? route.sessionKey,
11222
+ channel: "qqbot",
11223
+ to: stableTo,
11224
+ accountId: outboundAccountId
11225
+ } : void 0;
11226
+ const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : routeSessionKey;
11227
+ if (sessionApi?.recordInboundSession) {
11228
+ try {
11229
+ await sessionApi.recordInboundSession({
11230
+ storePath,
11231
+ sessionKey: recordSessionKey,
11232
+ ctx: finalCtx,
11233
+ updateLastRoute,
11234
+ onRecordError: (err) => {
11235
+ logger.warn(`failed to record inbound session: ${String(err)}`);
11236
+ }
11237
+ });
11238
+ } catch (err) {
11239
+ logger.warn(`failed to record inbound session: ${String(err)}`);
11240
+ }
11241
+ }
11242
+ if (sessionApi?.recordSessionMetaFromInbound) {
11243
+ try {
11244
+ await sessionApi.recordSessionMetaFromInbound({
11245
+ storePath,
11246
+ sessionKey: recordSessionKey,
11247
+ ctx: finalCtx,
11248
+ createIfMissing: true
11249
+ });
11250
+ } catch (err) {
11251
+ logger.warn(`failed to record inbound session meta: ${String(err)}`);
11252
+ }
9702
11253
  }
9703
11254
  }
9704
11255
  const textApi = runtime2.channel?.text;
@@ -9711,7 +11262,7 @@ async function dispatchToAgent(params) {
9711
11262
  const tableMode = textApi?.resolveMarkdownTableMode?.({
9712
11263
  cfg,
9713
11264
  channel: "qqbot",
9714
- accountId: route.accountId ?? accountId
11265
+ accountId: outboundAccountId
9715
11266
  });
9716
11267
  const resolvedTableMode = tableMode ?? "bullets";
9717
11268
  const chunkText = (text) => {
@@ -9726,90 +11277,124 @@ async function dispatchToAgent(params) {
9726
11277
  const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
9727
11278
  const markdownSupport = qqCfg.markdownSupport ?? true;
9728
11279
  const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
9729
- const useC2CMarkdownTransport = markdownSupport && isQQBotC2CTarget(target.to);
11280
+ const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11281
+ const isC2CTarget = isQQBotC2CTarget(target.to);
11282
+ const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
9730
11283
  let bufferedC2CMarkdownTexts = [];
9731
11284
  let bufferedC2CMarkdownMediaUrls = [];
9732
11285
  const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
11286
+ const hasBufferedC2CMarkdownReply = () => bufferedC2CMarkdownTexts.length > 0 || bufferedC2CMarkdownMediaUrls.length > 0;
9733
11287
  const bufferC2CMarkdownMedia = (url) => {
9734
11288
  const next = url?.trim();
9735
11289
  if (!next || bufferedC2CMarkdownMediaSeen.has(next)) return;
9736
11290
  bufferedC2CMarkdownMediaSeen.add(next);
9737
11291
  bufferedC2CMarkdownMediaUrls.push(next);
9738
11292
  };
9739
- const flushBufferedC2CMarkdownReply = async () => {
9740
- if (!useC2CMarkdownTransport || bufferedC2CMarkdownTexts.length === 0 && bufferedC2CMarkdownMediaUrls.length === 0) {
9741
- bufferedC2CMarkdownTexts = [];
9742
- bufferedC2CMarkdownMediaUrls = [];
9743
- bufferedC2CMarkdownMediaSeen.clear();
11293
+ const sendC2CMarkdownTransportPayload = async (params2) => {
11294
+ if (shouldSuppressVisibleReplies()) {
9744
11295
  return;
9745
11296
  }
9746
- const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
9747
- const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
9748
- bufferedC2CMarkdownTexts = [];
9749
- bufferedC2CMarkdownMediaUrls = [];
9750
- bufferedC2CMarkdownMediaSeen.clear();
9751
- const normalizedCombinedText = normalizeQQBotRenderedMarkdown(combinedText);
9752
- const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(combinedMediaUrls);
11297
+ const normalizedText = normalizeQQBotRenderedMarkdown(params2.text);
11298
+ const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(params2.mediaUrls);
9753
11299
  const finalMarkdownText = await normalizeQQBotMarkdownImages({
9754
- text: normalizedCombinedText,
11300
+ text: normalizedText,
9755
11301
  appendImageUrls: markdownImageUrls
9756
11302
  });
11303
+ if (shouldSuppressVisibleReplies()) {
11304
+ return;
11305
+ }
9757
11306
  const textReplyRefs = resolveQQBotTextReplyRefs({
9758
11307
  to: target.to,
9759
- text: finalMarkdownText || normalizedCombinedText,
11308
+ text: finalMarkdownText || normalizedText,
9760
11309
  markdownSupport,
9761
11310
  c2cMarkdownDeliveryMode,
9762
11311
  replyToId: inbound.messageId,
9763
11312
  replyEventId: inbound.eventId
9764
11313
  });
9765
- const textSegments = finalMarkdownText ? [finalMarkdownText] : [];
11314
+ const textChunks = finalMarkdownText ? chunkC2CMarkdownText({
11315
+ text: finalMarkdownText,
11316
+ limit,
11317
+ strategy: c2cMarkdownChunkStrategy,
11318
+ fallbackChunkText: chunkText
11319
+ }) : [];
9766
11320
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
9767
11321
  logger.info(
9768
- `delivery=${deliveryLabel} to=${target.to} segments=${textSegments.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")}`
11322
+ `delivery=${deliveryLabel} to=${target.to} chunks=${textChunks.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")} chunkStrategy=${c2cMarkdownChunkStrategy}`
9769
11323
  );
9770
- await sendQQBotMediaWithFallback({
9771
- qqCfg,
9772
- to: target.to,
9773
- mediaQueue,
9774
- replyToId: textReplyRefs.replyToId,
9775
- replyEventId: textReplyRefs.replyEventId,
9776
- logger,
9777
- onDelivered: () => {
9778
- markReplyDelivered();
9779
- },
9780
- onError: (error) => {
9781
- markGroupMessageInterfaceBlocked(error);
9782
- }
9783
- });
11324
+ if (!shouldSuppressVisibleReplies()) {
11325
+ await sendQQBotMediaWithFallback({
11326
+ qqCfg,
11327
+ to: target.to,
11328
+ mediaQueue,
11329
+ replyToId: textReplyRefs.replyToId,
11330
+ replyEventId: textReplyRefs.replyEventId,
11331
+ accountId: outboundAccountId,
11332
+ logger,
11333
+ onDelivered: () => {
11334
+ markReplyDelivered();
11335
+ },
11336
+ onError: (error) => {
11337
+ markGroupMessageInterfaceBlocked(error);
11338
+ },
11339
+ shouldContinue: () => !shouldSuppressVisibleReplies()
11340
+ });
11341
+ }
9784
11342
  if (!finalMarkdownText) {
9785
11343
  return;
9786
11344
  }
9787
- for (let segmentIndex = 0; segmentIndex < textSegments.length; segmentIndex += 1) {
9788
- const segment = textSegments[segmentIndex] ?? "";
9789
- const chunks = chunkText(segment);
9790
- for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
9791
- const chunk = chunks[chunkIndex] ?? "";
9792
- logger.info(
9793
- `delivery=${deliveryLabel} segment=${segmentIndex + 1}/${textSegments.length} chunk=${chunkIndex + 1}/${chunks.length} preview=${formatQQBotOutboundPreview(chunk)}`
9794
- );
9795
- const result = await qqbotOutbound.sendText({
9796
- cfg: { channels: { qqbot: qqCfg } },
9797
- to: target.to,
9798
- text: chunk,
9799
- replyToId: textReplyRefs.replyToId,
9800
- replyEventId: textReplyRefs.replyEventId
9801
- });
9802
- if (result.error) {
9803
- logger.error(`send buffered QQ markdown reply failed: ${result.error}`);
9804
- markGroupMessageInterfaceBlocked(result.error);
9805
- } else {
9806
- logger.info(`sent buffered QQ markdown reply (len=${chunk.length})`);
9807
- markReplyDelivered();
9808
- }
11345
+ for (let chunkIndex = 0; chunkIndex < textChunks.length; chunkIndex += 1) {
11346
+ if (shouldSuppressVisibleReplies()) {
11347
+ return;
11348
+ }
11349
+ const chunk = textChunks[chunkIndex] ?? "";
11350
+ logger.info(
11351
+ `delivery=${deliveryLabel} segment=1/1 chunk=${chunkIndex + 1}/${textChunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
11352
+ );
11353
+ const result = await qqbotOutbound.sendText({
11354
+ cfg: { channels: { qqbot: qqCfg } },
11355
+ to: target.to,
11356
+ text: chunk,
11357
+ replyToId: textReplyRefs.replyToId,
11358
+ replyEventId: textReplyRefs.replyEventId,
11359
+ accountId: outboundAccountId
11360
+ });
11361
+ if (result.error) {
11362
+ logger.error(`send QQ markdown reply failed: ${result.error}`);
11363
+ markGroupMessageInterfaceBlocked(result.error);
11364
+ } else {
11365
+ logger.info(`sent QQ markdown reply (phase=${params2.phase}, len=${chunk.length})`);
11366
+ markReplyDelivered();
9809
11367
  }
9810
11368
  }
9811
11369
  };
11370
+ const flushBufferedC2CMarkdownReply = async () => {
11371
+ if (!useC2CMarkdownTransport || bufferedC2CMarkdownTexts.length === 0 && bufferedC2CMarkdownMediaUrls.length === 0) {
11372
+ bufferedC2CMarkdownTexts = [];
11373
+ bufferedC2CMarkdownMediaUrls = [];
11374
+ bufferedC2CMarkdownMediaSeen.clear();
11375
+ return;
11376
+ }
11377
+ if (shouldSuppressVisibleReplies()) {
11378
+ bufferedC2CMarkdownTexts = [];
11379
+ bufferedC2CMarkdownMediaUrls = [];
11380
+ bufferedC2CMarkdownMediaSeen.clear();
11381
+ return;
11382
+ }
11383
+ const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
11384
+ const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
11385
+ bufferedC2CMarkdownTexts = [];
11386
+ bufferedC2CMarkdownMediaUrls = [];
11387
+ bufferedC2CMarkdownMediaSeen.clear();
11388
+ await sendC2CMarkdownTransportPayload({
11389
+ text: combinedText,
11390
+ mediaUrls: combinedMediaUrls,
11391
+ phase: "buffered"
11392
+ });
11393
+ };
9812
11394
  const deliver = async (payload, info) => {
11395
+ if (shouldSuppressVisibleReplies()) {
11396
+ return;
11397
+ }
9813
11398
  const typed = payload;
9814
11399
  const extractedTextMedia = extractQQBotReplyMedia({
9815
11400
  text: typed?.text ?? "",
@@ -9840,12 +11425,28 @@ async function dispatchToAgent(params) {
9840
11425
  const suppressText = deliveryDecision.suppressText || suppressEchoText;
9841
11426
  const textToSend = suppressText ? "" : cleanedText;
9842
11427
  if (useC2CMarkdownTransport) {
9843
- if (textToSend) {
9844
- bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
11428
+ const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
11429
+ const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
11430
+ if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
11431
+ if (textToSend) {
11432
+ bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
11433
+ }
11434
+ for (const url of mediaQueue) {
11435
+ bufferC2CMarkdownMedia(url);
11436
+ }
11437
+ return;
9845
11438
  }
9846
- for (const url of mediaQueue) {
9847
- bufferC2CMarkdownMedia(url);
11439
+ if (hasBufferedC2CMarkdownReply()) {
11440
+ await flushBufferedC2CMarkdownReply();
11441
+ if (shouldSuppressVisibleReplies()) {
11442
+ return;
11443
+ }
9848
11444
  }
11445
+ await sendC2CMarkdownTransportPayload({
11446
+ text: textToSend,
11447
+ mediaUrls: mediaQueue,
11448
+ phase: "immediate"
11449
+ });
9849
11450
  return;
9850
11451
  }
9851
11452
  if (textToSend) {
@@ -9860,12 +11461,16 @@ async function dispatchToAgent(params) {
9860
11461
  });
9861
11462
  const chunks = chunkText(converted);
9862
11463
  for (const chunk of chunks) {
11464
+ if (shouldSuppressVisibleReplies()) {
11465
+ return;
11466
+ }
9863
11467
  const result = await qqbotOutbound.sendText({
9864
11468
  cfg: { channels: { qqbot: qqCfg } },
9865
11469
  to: target.to,
9866
11470
  text: chunk,
9867
11471
  replyToId: textReplyRefs.replyToId,
9868
- replyEventId: textReplyRefs.replyEventId
11472
+ replyEventId: textReplyRefs.replyEventId,
11473
+ accountId: outboundAccountId
9869
11474
  });
9870
11475
  if (result.error) {
9871
11476
  logger.error(`sendText failed: ${result.error}`);
@@ -9875,24 +11480,54 @@ async function dispatchToAgent(params) {
9875
11480
  }
9876
11481
  }
9877
11482
  }
11483
+ if (shouldSuppressVisibleReplies()) {
11484
+ return;
11485
+ }
9878
11486
  await sendQQBotMediaWithFallback({
9879
11487
  qqCfg,
9880
11488
  to: target.to,
9881
11489
  mediaQueue,
9882
11490
  replyToId: inbound.messageId,
9883
11491
  replyEventId: inbound.eventId,
11492
+ accountId: outboundAccountId,
9884
11493
  logger,
9885
11494
  onDelivered: () => {
9886
11495
  markReplyDelivered();
9887
11496
  },
9888
11497
  onError: (error) => {
9889
11498
  markGroupMessageInterfaceBlocked(error);
9890
- }
11499
+ },
11500
+ shouldContinue: () => !shouldSuppressVisibleReplies()
9891
11501
  });
9892
11502
  };
9893
11503
  const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
11504
+ const dispatchDirect = replyApi.dispatchReplyWithDispatcher;
9894
11505
  const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
9895
- if (dispatchBuffered) {
11506
+ const streamingReplyOptions = isC2CTarget && !replyFinalOnly ? {
11507
+ disableBlockStreaming: false
11508
+ } : void 0;
11509
+ if (isC2CTarget && !replyFinalOnly && dispatchDirect) {
11510
+ logger.debug(`[dispatch] mode=direct session=${routeSessionKey} to=${target.to}`);
11511
+ await dispatchDirect({
11512
+ ctx: finalCtx,
11513
+ cfg,
11514
+ dispatcherOptions: {
11515
+ deliver,
11516
+ humanDelay,
11517
+ onError: (err, info) => {
11518
+ logger.error(`${info.kind} reply failed: ${String(err)}`);
11519
+ },
11520
+ onSkip: (_payload, info) => {
11521
+ if (info.reason !== "silent") {
11522
+ logger.info(`reply skipped: ${info.reason}`);
11523
+ }
11524
+ }
11525
+ },
11526
+ replyOptions: streamingReplyOptions
11527
+ });
11528
+ await flushBufferedC2CMarkdownReply();
11529
+ } else if (dispatchBuffered) {
11530
+ logger.debug(`[dispatch] mode=buffered session=${routeSessionKey} to=${target.to}`);
9896
11531
  await dispatchBuffered({
9897
11532
  ctx: finalCtx,
9898
11533
  cfg,
@@ -9907,10 +11542,12 @@ async function dispatchToAgent(params) {
9907
11542
  logger.info(`reply skipped: ${info.reason}`);
9908
11543
  }
9909
11544
  }
9910
- }
11545
+ },
11546
+ replyOptions: streamingReplyOptions
9911
11547
  });
9912
11548
  await flushBufferedC2CMarkdownReply();
9913
11549
  } else {
11550
+ logger.debug(`[dispatch] mode=legacy session=${routeSessionKey} to=${target.to}`);
9914
11551
  const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
9915
11552
  deliver,
9916
11553
  humanDelay,
@@ -9936,7 +11573,10 @@ async function dispatchToAgent(params) {
9936
11573
  ctx: finalCtx,
9937
11574
  cfg,
9938
11575
  dispatcher: dispatcherResult.dispatcher,
9939
- replyOptions: dispatcherResult.replyOptions
11576
+ replyOptions: {
11577
+ ...typeof dispatcherResult.replyOptions === "object" && dispatcherResult.replyOptions ? dispatcherResult.replyOptions : {},
11578
+ ...streamingReplyOptions ?? {}
11579
+ }
9940
11580
  });
9941
11581
  dispatcherResult.markDispatchIdle?.();
9942
11582
  await flushBufferedC2CMarkdownReply();
@@ -9945,14 +11585,15 @@ async function dispatchToAgent(params) {
9945
11585
  inbound,
9946
11586
  replyDelivered
9947
11587
  });
9948
- if (noReplyFallback && !groupMessageInterfaceBlocked) {
11588
+ if (noReplyFallback && !groupMessageInterfaceBlocked && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
9949
11589
  logger.info("no visible reply generated for group mention; sending fallback text");
9950
11590
  const fallbackResult = await qqbotOutbound.sendText({
9951
11591
  cfg: { channels: { qqbot: qqCfg } },
9952
11592
  to: target.to,
9953
11593
  text: noReplyFallback,
9954
11594
  replyToId: inbound.messageId,
9955
- replyEventId: inbound.eventId
11595
+ replyEventId: inbound.eventId,
11596
+ accountId: outboundAccountId
9956
11597
  });
9957
11598
  if (fallbackResult.error) {
9958
11599
  logger.error(`sendText no-reply fallback failed: ${fallbackResult.error}`);
@@ -10018,18 +11659,39 @@ async function handleQQBotDispatch(params) {
10018
11659
  logger.info("qqbot disabled, ignoring inbound message");
10019
11660
  return;
10020
11661
  }
10021
- const content = inbound.content.trim();
11662
+ const senderNameResolution = resolveQQBotSenderName({
11663
+ inbound,
11664
+ cfg: params.cfg,
11665
+ accountId
11666
+ });
11667
+ const resolvedInbound = {
11668
+ ...inbound,
11669
+ senderName: senderNameResolution.displayName
11670
+ };
11671
+ logQQBotSenderNameResolution({
11672
+ logger,
11673
+ inbound,
11674
+ accountId,
11675
+ resolution: senderNameResolution
11676
+ });
11677
+ const content = resolvedInbound.content.trim();
10022
11678
  const inboundLogContent = sanitizeInboundLogText(
10023
11679
  resolveInboundLogContent({
10024
11680
  content,
10025
- attachments: inbound.attachments
11681
+ attachments: resolvedInbound.attachments
10026
11682
  })
10027
11683
  );
10028
- logger.info(`[inbound-user] accountId=${accountId} senderId=${inbound.senderId} content=${inboundLogContent}`);
10029
- if (!shouldHandleMessage(inbound, qqCfg, logger)) {
11684
+ logger.info(
11685
+ `[inbound-user] accountId=${accountId} senderId=${resolvedInbound.senderId} senderName=${JSON.stringify(resolvedInbound.senderName ?? resolvedInbound.senderId)} content=${inboundLogContent}`
11686
+ );
11687
+ if (!shouldHandleMessage(resolvedInbound, qqCfg, logger)) {
10030
11688
  return;
10031
11689
  }
10032
- const knownTarget = resolveKnownQQBotTargetFromInbound({ inbound, accountId });
11690
+ const knownTarget = resolveKnownQQBotTargetFromInbound({
11691
+ inbound: resolvedInbound,
11692
+ accountId,
11693
+ persistentDisplayName: senderNameResolution.persistentDisplayName
11694
+ });
10033
11695
  if (knownTarget) {
10034
11696
  try {
10035
11697
  upsertKnownQQBotTarget({ target: knownTarget });
@@ -10037,7 +11699,7 @@ async function handleQQBotDispatch(params) {
10037
11699
  logger.warn(`failed to record known qqbot target: ${String(err)}`);
10038
11700
  }
10039
11701
  }
10040
- const attachmentCount = inbound.attachments?.length ?? 0;
11702
+ const attachmentCount = resolvedInbound.attachments?.length ?? 0;
10041
11703
  if (attachmentCount > 0) {
10042
11704
  logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
10043
11705
  }
@@ -10050,26 +11712,59 @@ async function handleQQBotDispatch(params) {
10050
11712
  logger.warn("routing API not available");
10051
11713
  return;
10052
11714
  }
10053
- const target = resolveChatTarget(inbound);
11715
+ const target = resolveChatTarget(resolvedInbound);
10054
11716
  const route = routing({
10055
11717
  cfg: params.cfg,
10056
11718
  channel: "qqbot",
10057
11719
  accountId,
10058
11720
  peer: { kind: target.peerKind, id: target.peerId }
10059
11721
  });
10060
- const queueKey = buildSessionDispatchQueueKey(route);
10061
- if (sessionDispatchQueue.has(queueKey)) {
10062
- logger.info(`session busy; queueing inbound dispatch sessionKey=${route.sessionKey}`);
11722
+ const effectiveSessionKey = resolveQQBotEffectiveSessionKey({
11723
+ inbound: resolvedInbound,
11724
+ route,
11725
+ accountId
11726
+ });
11727
+ const resolvedRoute = effectiveSessionKey === route.sessionKey ? route : {
11728
+ ...route,
11729
+ mainSessionKey: route.mainSessionKey?.trim() || route.sessionKey,
11730
+ effectiveSessionKey
11731
+ };
11732
+ const queueKey = buildSessionDispatchQueueKey(resolvedRoute);
11733
+ if (isQQBotFastAbortCommandText(content)) {
11734
+ const routeSessionKey = resolveQQBotRouteSessionKey(resolvedRoute);
11735
+ markSessionDispatchAbort(queueKey);
11736
+ const droppedCount = dropQueuedSessionDispatches(queueKey);
11737
+ logger.info(
11738
+ `session fast-abort command detected; executing immediately sessionKey=${routeSessionKey}`
11739
+ );
11740
+ logger.info(
11741
+ `session fast-abort command dropped ${droppedCount} queued messages sessionKey=${routeSessionKey}`
11742
+ );
11743
+ await runImmediateSessionDispatch(
11744
+ queueKey,
11745
+ async () => dispatchToAgent({
11746
+ inbound: { ...resolvedInbound, content },
11747
+ cfg: params.cfg,
11748
+ qqCfg,
11749
+ accountId,
11750
+ logger,
11751
+ route: resolvedRoute
11752
+ })
11753
+ );
11754
+ return;
11755
+ }
11756
+ if (hasSessionDispatchBacklog(queueKey)) {
11757
+ logger.info(`session busy; queueing inbound dispatch sessionKey=${resolveQQBotRouteSessionKey(resolvedRoute)}`);
10063
11758
  }
10064
11759
  await runSerializedSessionDispatch(
10065
11760
  queueKey,
10066
11761
  async () => dispatchToAgent({
10067
- inbound: { ...inbound, content },
11762
+ inbound: { ...resolvedInbound, content },
10068
11763
  cfg: params.cfg,
10069
11764
  qqCfg,
10070
11765
  accountId,
10071
11766
  logger,
10072
- route
11767
+ route: resolvedRoute
10073
11768
  })
10074
11769
  );
10075
11770
  }
@@ -10092,6 +11787,10 @@ function formatGatewayConnectError(err) {
10092
11787
  }
10093
11788
  return String(err);
10094
11789
  }
11790
+ function isConnectionIdle(conn) {
11791
+ if (!conn) return true;
11792
+ return !conn.socket && !conn.promise && !conn.connecting;
11793
+ }
10095
11794
  var activeConnections = /* @__PURE__ */ new Map();
10096
11795
  function getOrCreateConnection(accountId) {
10097
11796
  let conn = activeConnections.get(accountId);
@@ -10121,7 +11820,10 @@ function clearTimers(conn) {
10121
11820
  conn.reconnectTimer = null;
10122
11821
  }
10123
11822
  }
10124
- function cleanupSocket(conn) {
11823
+ function cleanupSocket(conn, expectedSocket) {
11824
+ if (expectedSocket && conn.socket !== expectedSocket) {
11825
+ return false;
11826
+ }
10125
11827
  clearTimers(conn);
10126
11828
  if (conn.socket) {
10127
11829
  try {
@@ -10132,6 +11834,7 @@ function cleanupSocket(conn) {
10132
11834
  }
10133
11835
  conn.socket = null;
10134
11836
  }
11837
+ return true;
10135
11838
  }
10136
11839
  async function monitorQQBotProvider(opts = {}) {
10137
11840
  const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID, setStatus } = opts;
@@ -10139,11 +11842,16 @@ async function monitorQQBotProvider(opts = {}) {
10139
11842
  log: runtime2?.log,
10140
11843
  error: runtime2?.error
10141
11844
  });
10142
- const conn = getOrCreateConnection(accountId);
10143
- if (conn.socket) {
10144
- if (conn.promise) {
10145
- return conn.promise;
10146
- }
11845
+ const existingConn = activeConnections.get(accountId);
11846
+ if (!existingConn) ; else if (isConnectionIdle(existingConn)) {
11847
+ activeConnections.delete(accountId);
11848
+ }
11849
+ const conn = activeConnections.get(accountId);
11850
+ const existingPromise = conn?.promise;
11851
+ if (existingPromise) {
11852
+ return existingPromise;
11853
+ }
11854
+ if (conn?.socket) {
10147
11855
  throw new Error(`QQBot monitor state invalid for account ${accountId}: active socket without promise`);
10148
11856
  }
10149
11857
  const qqCfg = config ? mergeQQBotAccountConfig(config, accountId) : void 0;
@@ -10153,18 +11861,20 @@ async function monitorQQBotProvider(opts = {}) {
10153
11861
  if (!qqCfg.appId || !qqCfg.clientSecret) {
10154
11862
  throw new Error(`QQBot not configured for account ${accountId} (missing appId or clientSecret)`);
10155
11863
  }
10156
- conn.promise = new Promise((resolve3, reject) => {
11864
+ const nextConn = conn ?? getOrCreateConnection(accountId);
11865
+ nextConn.promise = new Promise((resolve3, reject) => {
10157
11866
  let stopped = false;
10158
11867
  const finish = (err) => {
10159
11868
  if (stopped) return;
10160
11869
  stopped = true;
10161
11870
  abortSignal?.removeEventListener("abort", onAbort);
10162
- cleanupSocket(conn);
10163
- conn.sessionId = null;
10164
- conn.lastSeq = null;
10165
- conn.promise = null;
10166
- conn.stop = null;
10167
- conn.reconnectAttempt = 0;
11871
+ cleanupSocket(nextConn);
11872
+ nextConn.connecting = false;
11873
+ nextConn.sessionId = null;
11874
+ nextConn.lastSeq = null;
11875
+ nextConn.promise = null;
11876
+ nextConn.stop = null;
11877
+ nextConn.reconnectAttempt = 0;
10168
11878
  activeConnections.delete(accountId);
10169
11879
  {
10170
11880
  resolve3();
@@ -10174,33 +11884,33 @@ async function monitorQQBotProvider(opts = {}) {
10174
11884
  logger.info("abort signal received, stopping gateway");
10175
11885
  finish();
10176
11886
  };
10177
- conn.stop = () => {
11887
+ nextConn.stop = () => {
10178
11888
  logger.info("stop requested");
10179
11889
  finish();
10180
11890
  };
10181
11891
  const scheduleReconnect = (reason) => {
10182
11892
  if (stopped) return;
10183
- if (conn.reconnectTimer) return;
10184
- const delay = RECONNECT_DELAYS_MS[Math.min(conn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
10185
- conn.reconnectAttempt += 1;
11893
+ if (nextConn.reconnectTimer) return;
11894
+ const delay = RECONNECT_DELAYS_MS[Math.min(nextConn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
11895
+ nextConn.reconnectAttempt += 1;
10186
11896
  logger.warn(`[reconnect] ${reason}; retry in ${delay}ms`);
10187
- conn.reconnectTimer = setTimeout(() => {
10188
- conn.reconnectTimer = null;
11897
+ nextConn.reconnectTimer = setTimeout(() => {
11898
+ nextConn.reconnectTimer = null;
10189
11899
  void connect();
10190
11900
  }, delay);
10191
11901
  };
10192
11902
  const startHeartbeat = (intervalMs) => {
10193
- if (conn.heartbeatTimer) {
10194
- clearInterval(conn.heartbeatTimer);
11903
+ if (nextConn.heartbeatTimer) {
11904
+ clearInterval(nextConn.heartbeatTimer);
10195
11905
  }
10196
- conn.heartbeatTimer = setInterval(() => {
10197
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
10198
- const payload = JSON.stringify({ op: 1, d: conn.lastSeq });
10199
- conn.socket.send(payload);
11906
+ nextConn.heartbeatTimer = setInterval(() => {
11907
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
11908
+ const payload = JSON.stringify({ op: 1, d: nextConn.lastSeq });
11909
+ nextConn.socket.send(payload);
10200
11910
  }, intervalMs);
10201
11911
  };
10202
11912
  const sendIdentify = (token) => {
10203
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
11913
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
10204
11914
  const payload = {
10205
11915
  op: 2,
10206
11916
  d: {
@@ -10209,10 +11919,10 @@ async function monitorQQBotProvider(opts = {}) {
10209
11919
  shard: [0, 1]
10210
11920
  }
10211
11921
  };
10212
- conn.socket.send(JSON.stringify(payload));
11922
+ nextConn.socket.send(JSON.stringify(payload));
10213
11923
  };
10214
11924
  const sendResume = (token, session, seq) => {
10215
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
11925
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
10216
11926
  const payload = {
10217
11927
  op: 6,
10218
11928
  d: {
@@ -10221,11 +11931,14 @@ async function monitorQQBotProvider(opts = {}) {
10221
11931
  seq
10222
11932
  }
10223
11933
  };
10224
- conn.socket.send(JSON.stringify(payload));
11934
+ nextConn.socket.send(JSON.stringify(payload));
10225
11935
  };
10226
- const handleGatewayPayload = async (payload) => {
11936
+ const handleGatewayPayload = async (payload, activeSocket) => {
11937
+ if (stopped || nextConn.socket !== activeSocket) {
11938
+ return;
11939
+ }
10227
11940
  if (typeof payload.s === "number") {
10228
- conn.lastSeq = payload.s;
11941
+ nextConn.lastSeq = payload.s;
10229
11942
  }
10230
11943
  switch (payload.op) {
10231
11944
  case 10: {
@@ -10233,8 +11946,11 @@ async function monitorQQBotProvider(opts = {}) {
10233
11946
  const interval = hello?.heartbeat_interval ?? 3e4;
10234
11947
  startHeartbeat(interval);
10235
11948
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
10236
- if (conn.sessionId && typeof conn.lastSeq === "number") {
10237
- sendResume(token, conn.sessionId, conn.lastSeq);
11949
+ if (stopped || nextConn.socket !== activeSocket) {
11950
+ return;
11951
+ }
11952
+ if (nextConn.sessionId && typeof nextConn.lastSeq === "number") {
11953
+ sendResume(token, nextConn.sessionId, nextConn.lastSeq);
10238
11954
  } else {
10239
11955
  sendIdentify(token);
10240
11956
  }
@@ -10244,14 +11960,18 @@ async function monitorQQBotProvider(opts = {}) {
10244
11960
  setStatus?.({ lastEventAt: Date.now() });
10245
11961
  return;
10246
11962
  case 7:
10247
- cleanupSocket(conn);
11963
+ if (!cleanupSocket(nextConn, activeSocket)) {
11964
+ return;
11965
+ }
10248
11966
  scheduleReconnect("server requested reconnect");
10249
11967
  return;
10250
11968
  case 9:
10251
- conn.sessionId = null;
10252
- conn.lastSeq = null;
11969
+ nextConn.sessionId = null;
11970
+ nextConn.lastSeq = null;
10253
11971
  clearTokenCache(qqCfg.appId);
10254
- cleanupSocket(conn);
11972
+ if (!cleanupSocket(nextConn, activeSocket)) {
11973
+ return;
11974
+ }
10255
11975
  scheduleReconnect("invalid session");
10256
11976
  return;
10257
11977
  case 0: {
@@ -10259,14 +11979,14 @@ async function monitorQQBotProvider(opts = {}) {
10259
11979
  if (eventType === "READY") {
10260
11980
  const ready = payload.d;
10261
11981
  if (ready?.session_id) {
10262
- conn.sessionId = ready.session_id;
11982
+ nextConn.sessionId = ready.session_id;
10263
11983
  }
10264
- conn.reconnectAttempt = 0;
11984
+ nextConn.reconnectAttempt = 0;
10265
11985
  logger.info("gateway ready");
10266
11986
  return;
10267
11987
  }
10268
11988
  if (eventType === "RESUMED") {
10269
- conn.reconnectAttempt = 0;
11989
+ nextConn.reconnectAttempt = 0;
10270
11990
  logger.info("gateway resumed");
10271
11991
  return;
10272
11992
  }
@@ -10287,15 +12007,21 @@ async function monitorQQBotProvider(opts = {}) {
10287
12007
  }
10288
12008
  };
10289
12009
  const connect = async () => {
10290
- if (stopped || conn.connecting) return;
10291
- conn.connecting = true;
12010
+ if (stopped || nextConn.connecting) return;
12011
+ nextConn.connecting = true;
10292
12012
  try {
10293
- cleanupSocket(conn);
12013
+ cleanupSocket(nextConn);
10294
12014
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
12015
+ if (stopped) return;
10295
12016
  const gatewayUrl = await getGatewayUrl(token);
12017
+ if (stopped) return;
10296
12018
  logger.info(`connecting gateway: ${gatewayUrl}`);
10297
12019
  const ws = new WebSocket(gatewayUrl);
10298
- conn.socket = ws;
12020
+ nextConn.socket = ws;
12021
+ if (stopped) {
12022
+ cleanupSocket(nextConn, ws);
12023
+ return;
12024
+ }
10299
12025
  ws.on("open", () => {
10300
12026
  logger.info("gateway socket opened");
10301
12027
  });
@@ -10308,24 +12034,29 @@ async function monitorQQBotProvider(opts = {}) {
10308
12034
  logger.warn(`failed to parse gateway payload: ${String(err)}`);
10309
12035
  return;
10310
12036
  }
10311
- void handleGatewayPayload(payload).catch((err) => {
12037
+ void handleGatewayPayload(payload, ws).catch((err) => {
10312
12038
  logger.error(`gateway dispatch error: ${String(err)}`);
10313
12039
  });
10314
12040
  });
10315
12041
  ws.on("close", (code, reason) => {
12042
+ if (!cleanupSocket(nextConn, ws)) {
12043
+ return;
12044
+ }
10316
12045
  logger.warn(`gateway socket closed (${code}) ${String(reason)}`);
10317
- cleanupSocket(conn);
10318
12046
  scheduleReconnect("socket closed");
10319
12047
  });
10320
12048
  ws.on("error", (err) => {
12049
+ if (stopped || nextConn.socket !== ws) {
12050
+ return;
12051
+ }
10321
12052
  logger.error(`gateway socket error: ${String(err)}`);
10322
12053
  });
10323
12054
  } catch (err) {
10324
12055
  logger.error(`gateway connect failed: ${formatGatewayConnectError(err)}`);
10325
- cleanupSocket(conn);
12056
+ cleanupSocket(nextConn);
10326
12057
  scheduleReconnect("connect failed");
10327
12058
  } finally {
10328
- conn.connecting = false;
12059
+ nextConn.connecting = false;
10329
12060
  }
10330
12061
  };
10331
12062
  if (abortSignal?.aborted) {
@@ -10335,7 +12066,7 @@ async function monitorQQBotProvider(opts = {}) {
10335
12066
  abortSignal?.addEventListener("abort", onAbort, { once: true });
10336
12067
  void connect();
10337
12068
  });
10338
- return conn.promise;
12069
+ return nextConn.promise;
10339
12070
  }
10340
12071
  function stopQQBotMonitorForAccount(accountId = DEFAULT_ACCOUNT_ID) {
10341
12072
  const conn = activeConnections.get(accountId);
@@ -10372,7 +12103,8 @@ function resolveQQBotAccount(params) {
10372
12103
  configured,
10373
12104
  appId: credentials?.appId,
10374
12105
  markdownSupport: merged.markdownSupport ?? true,
10375
- c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only"
12106
+ c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only",
12107
+ c2cMarkdownChunkStrategy: merged.c2cMarkdownChunkStrategy ?? "markdown-block"
10376
12108
  };
10377
12109
  }
10378
12110
  var qqbotPlugin = {
@@ -10381,7 +12113,7 @@ var qqbotPlugin = {
10381
12113
  ...meta
10382
12114
  },
10383
12115
  capabilities: {
10384
- chatTypes: ["direct", "channel"],
12116
+ chatTypes: ["direct", "group", "channel"],
10385
12117
  media: true,
10386
12118
  reactions: false,
10387
12119
  threads: false,
@@ -10450,6 +12182,10 @@ var qqbotPlugin = {
10450
12182
  defaultAccount: { type: "string" },
10451
12183
  appId: { type: ["string", "number"] },
10452
12184
  clientSecret: { type: "string" },
12185
+ displayAliases: {
12186
+ type: "object",
12187
+ additionalProperties: { type: "string" }
12188
+ },
10453
12189
  asr: {
10454
12190
  type: "object",
10455
12191
  additionalProperties: false,
@@ -10465,6 +12201,10 @@ var qqbotPlugin = {
10465
12201
  type: "string",
10466
12202
  enum: ["passive", "proactive-table-only", "proactive-all"]
10467
12203
  },
12204
+ c2cMarkdownChunkStrategy: {
12205
+ type: "string",
12206
+ enum: ["markdown-block", "length"]
12207
+ },
10468
12208
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
10469
12209
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
10470
12210
  requireMention: { type: "boolean" },
@@ -10495,6 +12235,10 @@ var qqbotPlugin = {
10495
12235
  enabled: { type: "boolean" },
10496
12236
  appId: { type: ["string", "number"] },
10497
12237
  clientSecret: { type: "string" },
12238
+ displayAliases: {
12239
+ type: "object",
12240
+ additionalProperties: { type: "string" }
12241
+ },
10498
12242
  asr: {
10499
12243
  type: "object",
10500
12244
  additionalProperties: false,
@@ -10510,6 +12254,10 @@ var qqbotPlugin = {
10510
12254
  type: "string",
10511
12255
  enum: ["passive", "proactive-table-only", "proactive-all"]
10512
12256
  },
12257
+ c2cMarkdownChunkStrategy: {
12258
+ type: "string",
12259
+ enum: ["markdown-block", "length"]
12260
+ },
10513
12261
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
10514
12262
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
10515
12263
  requireMention: { type: "boolean" },
@@ -10661,7 +12409,9 @@ var qqbotPlugin = {
10661
12409
  ctx.log?.info(`[qqbot] starting gateway for account ${ctx.accountId}`);
10662
12410
  if (ctx.runtime) {
10663
12411
  const candidate = ctx.runtime;
10664
- if (candidate.channel?.routing?.resolveAgentRoute && candidate.channel?.reply?.dispatchReplyFromConfig) {
12412
+ const hasRouting = Boolean(candidate.channel?.routing?.resolveAgentRoute);
12413
+ const hasReply = Boolean(candidate.channel?.reply?.dispatchReplyWithDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyFromConfig);
12414
+ if (hasRouting && hasReply) {
10665
12415
  setQQBotRuntime(ctx.runtime);
10666
12416
  }
10667
12417
  }
@@ -10697,6 +12447,10 @@ var plugin = {
10697
12447
  defaultAccount: { type: "string" },
10698
12448
  appId: { type: ["string", "number"] },
10699
12449
  clientSecret: { type: "string" },
12450
+ displayAliases: {
12451
+ type: "object",
12452
+ additionalProperties: { type: "string" }
12453
+ },
10700
12454
  asr: {
10701
12455
  type: "object",
10702
12456
  additionalProperties: false,
@@ -10712,6 +12466,10 @@ var plugin = {
10712
12466
  type: "string",
10713
12467
  enum: ["passive", "proactive-table-only", "proactive-all"]
10714
12468
  },
12469
+ c2cMarkdownChunkStrategy: {
12470
+ type: "string",
12471
+ enum: ["markdown-block", "length"]
12472
+ },
10715
12473
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
10716
12474
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
10717
12475
  requireMention: { type: "boolean" },
@@ -10742,6 +12500,10 @@ var plugin = {
10742
12500
  enabled: { type: "boolean" },
10743
12501
  appId: { type: ["string", "number"] },
10744
12502
  clientSecret: { type: "string" },
12503
+ displayAliases: {
12504
+ type: "object",
12505
+ additionalProperties: { type: "string" }
12506
+ },
10745
12507
  asr: {
10746
12508
  type: "object",
10747
12509
  additionalProperties: false,
@@ -10757,6 +12519,10 @@ var plugin = {
10757
12519
  type: "string",
10758
12520
  enum: ["passive", "proactive-table-only", "proactive-all"]
10759
12521
  },
12522
+ c2cMarkdownChunkStrategy: {
12523
+ type: "string",
12524
+ enum: ["markdown-block", "length"]
12525
+ },
10760
12526
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
10761
12527
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
10762
12528
  requireMention: { type: "boolean" },