@openclaw-china/qqbot 2026.3.10 → 2026.3.12

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';
@@ -14,6 +14,7 @@ import 'util';
14
14
  import { createRequire } from 'module';
15
15
  import { execFileSync } from 'child_process';
16
16
  import WebSocket from 'ws';
17
+ import { Buffer as Buffer$1 } from 'buffer';
17
18
 
18
19
  var __create = Object.create;
19
20
  var __defProp = Object.defineProperty;
@@ -650,8 +651,8 @@ function getErrorMap() {
650
651
 
651
652
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
652
653
  var makeIssue = (params) => {
653
- const { data, path: path4, errorMaps, issueData } = params;
654
- const fullPath = [...path4, ...issueData.path || []];
654
+ const { data, path: path5, errorMaps, issueData } = params;
655
+ const fullPath = [...path5, ...issueData.path || []];
655
656
  const fullIssue = {
656
657
  ...issueData,
657
658
  path: fullPath
@@ -767,11 +768,11 @@ var errorUtil;
767
768
 
768
769
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
769
770
  var ParseInputLazyPath = class {
770
- constructor(parent, value, path4, key) {
771
+ constructor(parent, value, path5, key) {
771
772
  this._cachedPath = [];
772
773
  this.parent = parent;
773
774
  this.data = value;
774
- this._path = path4;
775
+ this._path = path5;
775
776
  this._key = key;
776
777
  }
777
778
  get path() {
@@ -4222,6 +4223,7 @@ var optionalCoercedString = external_exports.preprocess(
4222
4223
  (value) => toTrimmedString(value),
4223
4224
  external_exports.string().min(1).optional()
4224
4225
  );
4226
+ var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
4225
4227
  var QQBotAccountSchema = external_exports.object({
4226
4228
  name: external_exports.string().optional(),
4227
4229
  enabled: external_exports.boolean().optional(),
@@ -4234,6 +4236,7 @@ var QQBotAccountSchema = external_exports.object({
4234
4236
  secretKey: optionalCoercedString
4235
4237
  }).optional(),
4236
4238
  markdownSupport: external_exports.boolean().optional().default(true),
4239
+ c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4237
4240
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4238
4241
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4239
4242
  requireMention: external_exports.boolean().optional().default(true),
@@ -4873,7 +4876,7 @@ function extractMediaFromText(text, options = {}) {
4873
4876
  const {
4874
4877
  removeFromText = true,
4875
4878
  checkExists = false,
4876
- existsSync: existsSync6,
4879
+ existsSync: existsSync7,
4877
4880
  parseMediaLines = false,
4878
4881
  parseMarkdownImages = true,
4879
4882
  parseHtmlImages = true,
@@ -4888,7 +4891,7 @@ function extractMediaFromText(text, options = {}) {
4888
4891
  const key = media.localPath || media.source;
4889
4892
  if (seenSources.has(key)) return false;
4890
4893
  if (checkExists && media.isLocal && media.localPath) {
4891
- const exists = existsSync6 ? existsSync6(media.localPath) : fs3.existsSync(media.localPath);
4894
+ const exists = existsSync7 ? existsSync7(media.localPath) : fs3.existsSync(media.localPath);
4892
4895
  if (!exists) return false;
4893
4896
  }
4894
4897
  seenSources.add(key);
@@ -7255,9 +7258,11 @@ function showChinaInstallHint(api) {
7255
7258
  var API_BASE = "https://api.sgroup.qq.com";
7256
7259
  var TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
7257
7260
  var MSG_SEQ_BASE = 1e6;
7261
+ var MAX_DUPLICATE_MSG_SEQ_RETRIES = 5;
7258
7262
  var tokenCacheMap = /* @__PURE__ */ new Map();
7259
7263
  var tokenPromiseMap = /* @__PURE__ */ new Map();
7260
7264
  var msgSeqMap = /* @__PURE__ */ new Map();
7265
+ var fallbackMsgSeq = 0;
7261
7266
  function toTrimmedString3(value) {
7262
7267
  if (value === void 0 || value === null) return void 0;
7263
7268
  const next = String(value).trim();
@@ -7277,7 +7282,10 @@ function sanitizeUploadFileName(fileName) {
7277
7282
  return normalized || "file";
7278
7283
  }
7279
7284
  function nextMsgSeq(sequenceKey) {
7280
- if (!sequenceKey) return MSG_SEQ_BASE + 1;
7285
+ if (!sequenceKey) {
7286
+ fallbackMsgSeq += 1;
7287
+ return MSG_SEQ_BASE + fallbackMsgSeq;
7288
+ }
7281
7289
  const current = msgSeqMap.get(sequenceKey) ?? 0;
7282
7290
  const next = current + 1;
7283
7291
  msgSeqMap.set(sequenceKey, next);
@@ -7289,6 +7297,46 @@ function nextMsgSeq(sequenceKey) {
7289
7297
  }
7290
7298
  return MSG_SEQ_BASE + next;
7291
7299
  }
7300
+ function resolveMsgSeqKey(messageId, eventId) {
7301
+ if (messageId) return `msg:${messageId}`;
7302
+ if (eventId) return `event:${eventId}`;
7303
+ return void 0;
7304
+ }
7305
+ function isDuplicateMsgSeqError(err) {
7306
+ if (!(err instanceof HttpError) || err.status !== 400) {
7307
+ return false;
7308
+ }
7309
+ const body = err.body?.trim();
7310
+ if (!body) {
7311
+ return false;
7312
+ }
7313
+ try {
7314
+ const parsed = JSON.parse(body);
7315
+ if (parsed.code === 40054005 || parsed.err_code === 40054005) {
7316
+ return true;
7317
+ }
7318
+ const message = typeof parsed.message === "string" ? parsed.message.toLowerCase() : "";
7319
+ return message.includes("msgseq") && (message.includes("\u53BB\u91CD") || message.includes("duplicate"));
7320
+ } catch {
7321
+ const lowered = body.toLowerCase();
7322
+ return lowered.includes("msgseq") && (lowered.includes("\u53BB\u91CD") || lowered.includes("duplicate"));
7323
+ }
7324
+ }
7325
+ async function postPassiveMessage(params) {
7326
+ let lastError;
7327
+ for (let attempt = 0; attempt <= MAX_DUPLICATE_MSG_SEQ_RETRIES; attempt += 1) {
7328
+ const msgSeq = nextMsgSeq(params.sequenceKey);
7329
+ try {
7330
+ return await apiPost(params.accessToken, params.path, params.buildBody(msgSeq), params.options);
7331
+ } catch (err) {
7332
+ lastError = err;
7333
+ if (!isDuplicateMsgSeqError(err) || attempt === MAX_DUPLICATE_MSG_SEQ_RETRIES) {
7334
+ throw err;
7335
+ }
7336
+ }
7337
+ }
7338
+ throw lastError;
7339
+ }
7292
7340
  function clearTokenCache(appId) {
7293
7341
  const normalizedAppId = toTrimmedString3(appId);
7294
7342
  if (normalizedAppId) {
@@ -7332,8 +7380,8 @@ async function getAccessToken(appId, clientSecret, options) {
7332
7380
  tokenPromiseMap.set(normalizedAppId, promise);
7333
7381
  return promise;
7334
7382
  }
7335
- async function apiGet(accessToken, path4, options) {
7336
- const url = `${API_BASE}${path4}`;
7383
+ async function apiGet(accessToken, path5, options) {
7384
+ const url = `${API_BASE}${path5}`;
7337
7385
  return httpGet(url, {
7338
7386
  ...options,
7339
7387
  headers: {
@@ -7342,8 +7390,8 @@ async function apiGet(accessToken, path4, options) {
7342
7390
  }
7343
7391
  });
7344
7392
  }
7345
- async function apiPost(accessToken, path4, body, options) {
7346
- const url = `${API_BASE}${path4}`;
7393
+ async function apiPost(accessToken, path5, body, options) {
7394
+ const url = `${API_BASE}${path5}`;
7347
7395
  return httpPost(url, body, {
7348
7396
  ...options,
7349
7397
  headers: {
@@ -7357,16 +7405,11 @@ async function getGatewayUrl(accessToken) {
7357
7405
  return data.url;
7358
7406
  }
7359
7407
  function buildMessageBody(params) {
7360
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7361
- const body = params.markdown ? {
7362
- markdown: { content: params.content },
7363
- msg_type: 2,
7364
- msg_seq: msgSeq
7365
- } : {
7408
+ const body = buildTextMessageBody({
7366
7409
  content: params.content,
7367
- msg_type: 0,
7368
- msg_seq: msgSeq
7369
- };
7410
+ markdown: params.markdown
7411
+ });
7412
+ body.msg_seq = params.msgSeq;
7370
7413
  if (params.messageId) {
7371
7414
  body.msg_id = params.messageId;
7372
7415
  } else if (params.eventId) {
@@ -7374,22 +7417,63 @@ function buildMessageBody(params) {
7374
7417
  }
7375
7418
  return body;
7376
7419
  }
7420
+ function buildTextMessageBody(params) {
7421
+ return params.markdown ? {
7422
+ markdown: { content: params.content },
7423
+ msg_type: 2
7424
+ } : {
7425
+ content: params.content,
7426
+ msg_type: 0
7427
+ };
7428
+ }
7429
+ function buildProactiveMessageBody(params) {
7430
+ if (!params.content.trim()) {
7431
+ throw new Error("QQBot proactive message content is empty");
7432
+ }
7433
+ return buildTextMessageBody(params);
7434
+ }
7377
7435
  async function sendC2CMessage(params) {
7378
- const body = buildMessageBody({
7436
+ return postPassiveMessage({
7437
+ accessToken: params.accessToken,
7438
+ path: `/v2/users/${params.openid}/messages`,
7439
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7440
+ options: { timeout: 15e3 },
7441
+ buildBody: (msgSeq) => buildMessageBody({
7442
+ content: params.content,
7443
+ messageId: params.messageId,
7444
+ eventId: params.eventId,
7445
+ markdown: params.markdown,
7446
+ msgSeq
7447
+ })
7448
+ });
7449
+ }
7450
+ async function sendGroupMessage(params) {
7451
+ return postPassiveMessage({
7452
+ accessToken: params.accessToken,
7453
+ path: `/v2/groups/${params.groupOpenid}/messages`,
7454
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7455
+ options: { timeout: 15e3 },
7456
+ buildBody: (msgSeq) => buildMessageBody({
7457
+ content: params.content,
7458
+ messageId: params.messageId,
7459
+ eventId: params.eventId,
7460
+ markdown: params.markdown,
7461
+ msgSeq
7462
+ })
7463
+ });
7464
+ }
7465
+ async function sendProactiveC2CMessage(params) {
7466
+ const body = buildProactiveMessageBody({
7379
7467
  content: params.content,
7380
- messageId: params.messageId,
7381
- eventId: params.eventId,
7382
7468
  markdown: params.markdown
7383
7469
  });
7384
7470
  return apiPost(params.accessToken, `/v2/users/${params.openid}/messages`, body, {
7385
7471
  timeout: 15e3
7386
7472
  });
7387
7473
  }
7388
- async function sendGroupMessage(params) {
7389
- const body = buildMessageBody({
7474
+ async function sendProactiveGroupMessage(params) {
7475
+ const body = buildProactiveMessageBody({
7390
7476
  content: params.content,
7391
- messageId: params.messageId,
7392
- eventId: params.eventId,
7393
7477
  markdown: params.markdown
7394
7478
  });
7395
7479
  return apiPost(params.accessToken, `/v2/groups/${params.groupOpenid}/messages`, body, {
@@ -7406,11 +7490,12 @@ async function sendChannelMessage(params) {
7406
7490
  });
7407
7491
  }
7408
7492
  async function sendC2CInputNotify(params) {
7409
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7410
- await apiPost(
7411
- params.accessToken,
7412
- `/v2/users/${params.openid}/messages`,
7413
- {
7493
+ const response = await postPassiveMessage({
7494
+ accessToken: params.accessToken,
7495
+ path: `/v2/users/${params.openid}/messages`,
7496
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7497
+ options: { timeout: 15e3 },
7498
+ buildBody: (msgSeq) => ({
7414
7499
  msg_type: 6,
7415
7500
  input_notify: {
7416
7501
  input_type: 1,
@@ -7418,9 +7503,10 @@ async function sendC2CInputNotify(params) {
7418
7503
  },
7419
7504
  msg_seq: msgSeq,
7420
7505
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7421
- },
7422
- { timeout: 15e3 }
7423
- );
7506
+ })
7507
+ });
7508
+ const refIdx = response.ext_info?.ref_idx?.trim();
7509
+ return refIdx ? { refIdx } : {};
7424
7510
  }
7425
7511
  async function uploadC2CMedia(params) {
7426
7512
  const body = {
@@ -7461,34 +7547,261 @@ async function uploadGroupMedia(params) {
7461
7547
  });
7462
7548
  }
7463
7549
  async function sendC2CMediaMessage(params) {
7464
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7465
- return apiPost(
7466
- params.accessToken,
7467
- `/v2/users/${params.openid}/messages`,
7468
- {
7550
+ return postPassiveMessage({
7551
+ accessToken: params.accessToken,
7552
+ path: `/v2/users/${params.openid}/messages`,
7553
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7554
+ options: { timeout: 15e3 },
7555
+ buildBody: (msgSeq) => ({
7469
7556
  msg_type: 7,
7470
7557
  media: { file_info: params.fileInfo },
7471
7558
  msg_seq: msgSeq,
7472
7559
  ...params.content ? { content: params.content } : {},
7473
7560
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7474
- },
7475
- { timeout: 15e3 }
7476
- );
7561
+ })
7562
+ });
7477
7563
  }
7478
7564
  async function sendGroupMediaMessage(params) {
7479
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7480
- return apiPost(
7481
- params.accessToken,
7482
- `/v2/groups/${params.groupOpenid}/messages`,
7483
- {
7565
+ return postPassiveMessage({
7566
+ accessToken: params.accessToken,
7567
+ path: `/v2/groups/${params.groupOpenid}/messages`,
7568
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7569
+ options: { timeout: 15e3 },
7570
+ buildBody: (msgSeq) => ({
7484
7571
  msg_type: 7,
7485
7572
  media: { file_info: params.fileInfo },
7486
7573
  msg_seq: msgSeq,
7487
7574
  ...params.content ? { content: params.content } : {},
7488
7575
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7489
- },
7490
- { timeout: 15e3 }
7491
- );
7576
+ })
7577
+ });
7578
+ }
7579
+ var REF_INDEX_FILE = join(homedir(), ".openclaw", "qqbot", "data", "ref-index.jsonl");
7580
+ var MAX_CONTENT_LENGTH = 500;
7581
+ var MAX_ENTRIES = 5e4;
7582
+ var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
7583
+ var COMPACT_THRESHOLD_RATIO = 2;
7584
+ var cache = null;
7585
+ var totalLinesOnDisk = 0;
7586
+ function normalizeRefIdx(refIdx) {
7587
+ const next = refIdx.trim();
7588
+ return next ? next : void 0;
7589
+ }
7590
+ function ensureStorageDir() {
7591
+ mkdirSync(dirname(REF_INDEX_FILE), { recursive: true });
7592
+ }
7593
+ function truncateContent(content) {
7594
+ return content.trim().slice(0, MAX_CONTENT_LENGTH);
7595
+ }
7596
+ function sanitizeAttachmentSummary(attachment) {
7597
+ const type = attachment.type;
7598
+ const filename = attachment.filename?.trim();
7599
+ const contentType = attachment.contentType?.trim();
7600
+ const localPath = attachment.localPath?.trim();
7601
+ const url = attachment.url?.trim();
7602
+ const transcript = attachment.transcript?.trim();
7603
+ if (!filename && !contentType && !localPath && !url && !transcript && type === "unknown") {
7604
+ return void 0;
7605
+ }
7606
+ return {
7607
+ type,
7608
+ ...filename ? { filename } : {},
7609
+ ...contentType ? { contentType } : {},
7610
+ ...localPath ? { localPath } : {},
7611
+ ...url ? { url } : {},
7612
+ ...transcript ? { transcript } : {},
7613
+ ...transcript && attachment.transcriptSource ? { transcriptSource: attachment.transcriptSource } : {}
7614
+ };
7615
+ }
7616
+ function sanitizeEntry(entry) {
7617
+ const senderId = entry.senderId.trim() || "unknown";
7618
+ const senderName = entry.senderName?.trim();
7619
+ const timestamp = Number.isFinite(entry.timestamp) ? Math.trunc(entry.timestamp) : Date.now();
7620
+ const attachments = entry.attachments?.map((attachment) => sanitizeAttachmentSummary(attachment)).filter((attachment) => Boolean(attachment));
7621
+ return {
7622
+ content: truncateContent(entry.content),
7623
+ senderId,
7624
+ ...senderName ? { senderName } : {},
7625
+ timestamp,
7626
+ ...entry.isBot ? { isBot: true } : {},
7627
+ ...attachments && attachments.length > 0 ? { attachments } : {}
7628
+ };
7629
+ }
7630
+ function shouldCompact() {
7631
+ if (!cache) return false;
7632
+ return totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1e3;
7633
+ }
7634
+ function compactFile() {
7635
+ if (!cache) return;
7636
+ try {
7637
+ ensureStorageDir();
7638
+ const tempPath = `${REF_INDEX_FILE}.tmp`;
7639
+ const lines = [];
7640
+ for (const [key, entry] of cache.entries()) {
7641
+ lines.push(
7642
+ JSON.stringify({
7643
+ k: key,
7644
+ v: sanitizeEntry(entry),
7645
+ t: entry._createdAt
7646
+ })
7647
+ );
7648
+ }
7649
+ writeFileSync(tempPath, lines.length > 0 ? `${lines.join("\n")}
7650
+ ` : "", "utf8");
7651
+ renameSync(tempPath, REF_INDEX_FILE);
7652
+ totalLinesOnDisk = cache.size;
7653
+ } catch {
7654
+ }
7655
+ }
7656
+ function evictIfNeeded() {
7657
+ if (!cache || cache.size < MAX_ENTRIES) return;
7658
+ const now = Date.now();
7659
+ for (const [key, entry] of cache.entries()) {
7660
+ if (now - entry._createdAt > TTL_MS) {
7661
+ cache.delete(key);
7662
+ }
7663
+ }
7664
+ if (cache.size < MAX_ENTRIES) {
7665
+ return;
7666
+ }
7667
+ const sorted = [...cache.entries()].sort((left, right) => left[1]._createdAt - right[1]._createdAt);
7668
+ const removeCount = cache.size - MAX_ENTRIES + 1;
7669
+ for (let index = 0; index < removeCount; index += 1) {
7670
+ const key = sorted[index]?.[0];
7671
+ if (key) {
7672
+ cache.delete(key);
7673
+ }
7674
+ }
7675
+ }
7676
+ function loadCache() {
7677
+ if (cache) {
7678
+ return cache;
7679
+ }
7680
+ cache = /* @__PURE__ */ new Map();
7681
+ totalLinesOnDisk = 0;
7682
+ try {
7683
+ if (!existsSync(REF_INDEX_FILE)) {
7684
+ return cache;
7685
+ }
7686
+ const now = Date.now();
7687
+ const raw = readFileSync(REF_INDEX_FILE, "utf8");
7688
+ const lines = raw.split(/\r?\n/);
7689
+ for (const line of lines) {
7690
+ const trimmed = line.trim();
7691
+ if (!trimmed) continue;
7692
+ totalLinesOnDisk += 1;
7693
+ try {
7694
+ const parsed = JSON.parse(trimmed);
7695
+ const key = typeof parsed.k === "string" ? normalizeRefIdx(parsed.k) : void 0;
7696
+ const createdAt = typeof parsed.t === "number" && Number.isFinite(parsed.t) ? parsed.t : void 0;
7697
+ if (!key || createdAt === void 0 || !parsed.v || typeof parsed.v !== "object") {
7698
+ continue;
7699
+ }
7700
+ if (now - createdAt > TTL_MS) {
7701
+ continue;
7702
+ }
7703
+ const entry = sanitizeEntry(parsed.v);
7704
+ cache.set(key, {
7705
+ ...entry,
7706
+ _createdAt: createdAt
7707
+ });
7708
+ } catch {
7709
+ }
7710
+ }
7711
+ if (shouldCompact()) {
7712
+ compactFile();
7713
+ }
7714
+ } catch {
7715
+ cache = /* @__PURE__ */ new Map();
7716
+ totalLinesOnDisk = 0;
7717
+ }
7718
+ return cache;
7719
+ }
7720
+ function appendLine(line) {
7721
+ try {
7722
+ ensureStorageDir();
7723
+ appendFileSync(REF_INDEX_FILE, `${JSON.stringify(line)}
7724
+ `, "utf8");
7725
+ totalLinesOnDisk += 1;
7726
+ } catch {
7727
+ }
7728
+ }
7729
+ function restoreEntry(entry) {
7730
+ const sanitized = sanitizeEntry(entry);
7731
+ return {
7732
+ content: sanitized.content,
7733
+ senderId: sanitized.senderId,
7734
+ ...sanitized.senderName ? { senderName: sanitized.senderName } : {},
7735
+ timestamp: sanitized.timestamp,
7736
+ ...sanitized.isBot ? { isBot: true } : {},
7737
+ ...sanitized.attachments ? { attachments: sanitized.attachments } : {}
7738
+ };
7739
+ }
7740
+ function formatAttachmentSummary(attachment) {
7741
+ const sourceParts = [
7742
+ attachment.localPath?.trim() ? `\u672C\u5730: ${attachment.localPath.trim()}` : void 0,
7743
+ attachment.url?.trim() ? `\u94FE\u63A5: ${attachment.url.trim()}` : void 0
7744
+ ].filter((value) => Boolean(value));
7745
+ const sourceSuffix = sourceParts.length > 0 ? ` (${sourceParts.join(" | ")})` : "";
7746
+ if (attachment.type === "image") {
7747
+ return `[\u56FE\u7247${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7748
+ }
7749
+ if (attachment.type === "voice") {
7750
+ if (attachment.transcript?.trim()) {
7751
+ 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;
7752
+ return `[\u8BED\u97F3\u6D88\u606F: ${attachment.transcript.trim()}${sourceLabel ? ` (${sourceLabel})` : ""}]${sourceSuffix}`;
7753
+ }
7754
+ return `[\u8BED\u97F3\u6D88\u606F]${sourceSuffix}`;
7755
+ }
7756
+ if (attachment.type === "video") {
7757
+ return `[\u89C6\u9891${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7758
+ }
7759
+ if (attachment.type === "file") {
7760
+ return `[\u6587\u4EF6${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7761
+ }
7762
+ return `[\u9644\u4EF6${attachment.filename?.trim() ? `: ${attachment.filename.trim()}` : ""}]${sourceSuffix}`;
7763
+ }
7764
+ function setRefIndex(refIdx, entry) {
7765
+ const key = normalizeRefIdx(refIdx);
7766
+ if (!key) return;
7767
+ const store = loadCache();
7768
+ evictIfNeeded();
7769
+ const nextEntry = sanitizeEntry(entry);
7770
+ const createdAt = Date.now();
7771
+ store.set(key, {
7772
+ ...nextEntry,
7773
+ _createdAt: createdAt
7774
+ });
7775
+ appendLine({
7776
+ k: key,
7777
+ v: nextEntry,
7778
+ t: createdAt
7779
+ });
7780
+ if (shouldCompact()) {
7781
+ compactFile();
7782
+ }
7783
+ }
7784
+ function getRefIndex(refIdx) {
7785
+ const key = normalizeRefIdx(refIdx);
7786
+ if (!key) return null;
7787
+ const store = loadCache();
7788
+ const entry = store.get(key);
7789
+ if (!entry) {
7790
+ return null;
7791
+ }
7792
+ if (Date.now() - entry._createdAt > TTL_MS) {
7793
+ store.delete(key);
7794
+ return null;
7795
+ }
7796
+ return restoreEntry(entry);
7797
+ }
7798
+ function formatRefEntryForAgent(entry) {
7799
+ const content = entry.content.trim();
7800
+ const parts = content ? [content] : [];
7801
+ for (const attachment of entry.attachments ?? []) {
7802
+ parts.push(formatAttachmentSummary(attachment));
7803
+ }
7804
+ return parts.join("\n") || "[\u7A7A\u6D88\u606F]";
7492
7805
  }
7493
7806
  var require2 = createRequire(import.meta.url);
7494
7807
  function resolveQQBotMediaFileType(fileName) {
@@ -7645,7 +7958,7 @@ async function sendFileQQBot(params) {
7645
7958
  }
7646
7959
  }
7647
7960
  try {
7648
- return await sendC2CMediaMessage({
7961
+ const result = await sendC2CMediaMessage({
7649
7962
  accessToken,
7650
7963
  openid: target.id,
7651
7964
  fileInfo,
@@ -7653,6 +7966,12 @@ async function sendFileQQBot(params) {
7653
7966
  ...messageId ? { messageId } : {},
7654
7967
  ...eventId ? { eventId } : {}
7655
7968
  });
7969
+ const refIdx = result.ext_info?.ref_idx?.trim();
7970
+ return {
7971
+ id: result.id,
7972
+ timestamp: result.timestamp,
7973
+ ...refIdx ? { refIdx } : {}
7974
+ };
7656
7975
  } catch (err) {
7657
7976
  const message = formatQQBotError(err);
7658
7977
  throw new Error(`QQBot C2C media send failed: ${message}`);
@@ -7741,6 +8060,14 @@ function logEventIdFallback(params) {
7741
8060
  }
7742
8061
  console.info(detail);
7743
8062
  }
8063
+ function logQQBotOutboundDispatch(params) {
8064
+ const accountLabel = params.accountId?.trim() || DEFAULT_ACCOUNT_ID;
8065
+ const textLength = typeof params.text === "string" ? params.text.length : 0;
8066
+ const mediaLabel = params.mediaUrl ? ` media=${shortId(params.mediaUrl)}` : "";
8067
+ console.info(
8068
+ `[qqbot] outbound action=${params.action} api=${params.api} accountId=${accountLabel} target=${params.targetKind}:${shortId(params.targetId)} markdown=${params.markdown ? "yes" : "no"} replyToId=${params.replyToId ? "yes" : "no"} replyEventId=${params.replyEventId ? "yes" : "no"} textLen=${textLength}${mediaLabel}`
8069
+ );
8070
+ }
7744
8071
  function shouldRetryWithEventId(err) {
7745
8072
  const status = err instanceof HttpError ? err.status : void 0;
7746
8073
  let body = "";
@@ -7761,6 +8088,94 @@ function shouldRetryWithEventId(err) {
7761
8088
  function shouldSendTextAsFollowupForMedia(mediaUrl) {
7762
8089
  return detectMediaType(stripTitleFromUrl(mediaUrl)) === "file";
7763
8090
  }
8091
+ function isHttpUrl2(value) {
8092
+ return /^https?:\/\//i.test(value);
8093
+ }
8094
+ function resolveResponseRefIdx(response) {
8095
+ if (!response || typeof response !== "object") {
8096
+ return void 0;
8097
+ }
8098
+ const direct = response.refIdx;
8099
+ if (typeof direct === "string" && direct.trim()) {
8100
+ return direct.trim();
8101
+ }
8102
+ const extInfo = response.ext_info;
8103
+ if (typeof extInfo?.ref_idx === "string" && extInfo.ref_idx.trim()) {
8104
+ return extInfo.ref_idx.trim();
8105
+ }
8106
+ return void 0;
8107
+ }
8108
+ function resolveOutboundAttachmentType(mediaUrl) {
8109
+ const detected = detectMediaType(stripTitleFromUrl(mediaUrl));
8110
+ if (detected === "image") return "image";
8111
+ if (detected === "video") return "video";
8112
+ if (detected === "audio") return "voice";
8113
+ if (detected === "file") return "file";
8114
+ return "unknown";
8115
+ }
8116
+ function resolveOutboundAttachmentFileName(mediaUrl) {
8117
+ const source = stripTitleFromUrl(mediaUrl).trim();
8118
+ if (!source) return void 0;
8119
+ if (isHttpUrl2(source)) {
8120
+ try {
8121
+ const base2 = path2.posix.basename(new URL(source).pathname);
8122
+ return base2 && base2 !== "/" ? base2 : void 0;
8123
+ } catch {
8124
+ return void 0;
8125
+ }
8126
+ }
8127
+ const base = path2.basename(source);
8128
+ return base || void 0;
8129
+ }
8130
+ function buildOutboundAttachmentSummary(params) {
8131
+ const source = stripTitleFromUrl(params.mediaUrl).trim();
8132
+ const type = resolveOutboundAttachmentType(source);
8133
+ const filename = resolveOutboundAttachmentFileName(source);
8134
+ const text = params.text?.trim();
8135
+ return {
8136
+ type,
8137
+ ...filename ? { filename } : {},
8138
+ ...isHttpUrl2(source) ? { url: source } : { localPath: source },
8139
+ ...type === "voice" && text ? {
8140
+ transcript: text,
8141
+ transcriptSource: "tts"
8142
+ } : {}
8143
+ };
8144
+ }
8145
+ function recordOutboundC2CRefIndex(params) {
8146
+ const refIdx = params.refIdx?.trim();
8147
+ if (!refIdx) return;
8148
+ const text = params.text?.trim() ?? "";
8149
+ const attachments = params.mediaUrl?.trim() ? [buildOutboundAttachmentSummary({ mediaUrl: params.mediaUrl, text })] : void 0;
8150
+ if (!text && !attachments) {
8151
+ return;
8152
+ }
8153
+ try {
8154
+ const accountLabel = params.accountId?.trim() || DEFAULT_ACCOUNT_ID;
8155
+ setRefIndex(refIdx, {
8156
+ content: text,
8157
+ senderId: accountLabel,
8158
+ senderName: accountLabel,
8159
+ timestamp: Date.now(),
8160
+ isBot: true,
8161
+ ...attachments ? { attachments } : {}
8162
+ });
8163
+ console.info(
8164
+ `[qqbot] cached outbound ref_idx=${refIdx} accountId=${accountLabel} textLen=${text.length} media=${params.mediaUrl?.trim() ? "yes" : "no"}`
8165
+ );
8166
+ } catch (err) {
8167
+ console.warn(`[qqbot] failed to cache outbound ref_idx=${refIdx}: ${String(err)}`);
8168
+ }
8169
+ }
8170
+ function buildPassiveReplyRefs(params) {
8171
+ if (params.replyToId) {
8172
+ return { messageId: params.replyToId };
8173
+ }
8174
+ if (params.replyEventId) {
8175
+ return { eventId: params.replyEventId };
8176
+ }
8177
+ return {};
8178
+ }
7764
8179
  var qqbotOutbound = {
7765
8180
  deliveryMode: "direct",
7766
8181
  textChunkLimit: 1500,
@@ -7778,13 +8193,42 @@ var qqbotOutbound = {
7778
8193
  const groupMarkdown = false;
7779
8194
  try {
7780
8195
  if (target.kind === "group") {
8196
+ if (!replyToId && !replyEventId) {
8197
+ logQQBotOutboundDispatch({
8198
+ action: "text",
8199
+ api: "sendProactiveGroupMessage",
8200
+ accountId,
8201
+ targetKind: target.kind,
8202
+ targetId: target.id,
8203
+ markdown,
8204
+ text
8205
+ });
8206
+ const result3 = await sendProactiveGroupMessage({
8207
+ accessToken,
8208
+ groupOpenid: target.id,
8209
+ content: text,
8210
+ markdown
8211
+ });
8212
+ return { channel: "qqbot", messageId: result3.id, timestamp: result3.timestamp };
8213
+ }
7781
8214
  let result2;
7782
8215
  try {
8216
+ logQQBotOutboundDispatch({
8217
+ action: "text",
8218
+ api: "sendGroupMessage",
8219
+ accountId,
8220
+ targetKind: target.kind,
8221
+ targetId: target.id,
8222
+ markdown: groupMarkdown,
8223
+ replyToId,
8224
+ replyEventId,
8225
+ text
8226
+ });
7783
8227
  result2 = await sendGroupMessage({
7784
8228
  accessToken,
7785
8229
  groupOpenid: target.id,
7786
8230
  content: text,
7787
- messageId: replyToId,
8231
+ ...buildPassiveReplyRefs({ replyToId, replyEventId }),
7788
8232
  markdown: groupMarkdown
7789
8233
  });
7790
8234
  } catch (err) {
@@ -7835,6 +8279,15 @@ var qqbotOutbound = {
7835
8279
  return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
7836
8280
  }
7837
8281
  if (target.kind === "channel") {
8282
+ logQQBotOutboundDispatch({
8283
+ action: "text",
8284
+ api: "sendChannelMessage",
8285
+ accountId,
8286
+ targetKind: target.kind,
8287
+ targetId: target.id,
8288
+ replyToId,
8289
+ text
8290
+ });
7838
8291
  const result2 = await sendChannelMessage({
7839
8292
  accessToken,
7840
8293
  channelId: target.id,
@@ -7843,13 +8296,49 @@ var qqbotOutbound = {
7843
8296
  });
7844
8297
  return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
7845
8298
  }
8299
+ if (!replyToId && !replyEventId) {
8300
+ logQQBotOutboundDispatch({
8301
+ action: "text",
8302
+ api: "sendProactiveC2CMessage",
8303
+ accountId,
8304
+ targetKind: target.kind,
8305
+ targetId: target.id,
8306
+ markdown,
8307
+ text
8308
+ });
8309
+ const result2 = await sendProactiveC2CMessage({
8310
+ accessToken,
8311
+ openid: target.id,
8312
+ content: text,
8313
+ markdown
8314
+ });
8315
+ const refIdx2 = resolveResponseRefIdx(result2);
8316
+ recordOutboundC2CRefIndex({ refIdx: refIdx2, accountId, text });
8317
+ return {
8318
+ channel: "qqbot",
8319
+ messageId: result2.id,
8320
+ timestamp: result2.timestamp,
8321
+ ...refIdx2 ? { refIdx: refIdx2 } : {}
8322
+ };
8323
+ }
7846
8324
  let result;
7847
8325
  try {
8326
+ logQQBotOutboundDispatch({
8327
+ action: "text",
8328
+ api: "sendC2CMessage",
8329
+ accountId,
8330
+ targetKind: target.kind,
8331
+ targetId: target.id,
8332
+ markdown,
8333
+ replyToId,
8334
+ replyEventId,
8335
+ text
8336
+ });
7848
8337
  result = await sendC2CMessage({
7849
8338
  accessToken,
7850
8339
  openid: target.id,
7851
8340
  content: text,
7852
- messageId: replyToId,
8341
+ ...buildPassiveReplyRefs({ replyToId, replyEventId }),
7853
8342
  markdown
7854
8343
  });
7855
8344
  } catch (err) {
@@ -7897,7 +8386,14 @@ var qqbotOutbound = {
7897
8386
  throw retryErr;
7898
8387
  }
7899
8388
  }
7900
- return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
8389
+ const refIdx = resolveResponseRefIdx(result);
8390
+ recordOutboundC2CRefIndex({ refIdx, accountId, text });
8391
+ return {
8392
+ channel: "qqbot",
8393
+ messageId: result.id,
8394
+ timestamp: result.timestamp,
8395
+ ...refIdx ? { refIdx } : {}
8396
+ };
7901
8397
  } catch (err) {
7902
8398
  const message = summarizeError(err);
7903
8399
  return { channel: "qqbot", error: message };
@@ -7927,6 +8423,17 @@ ${mediaUrl}` : mediaUrl;
7927
8423
  try {
7928
8424
  let result;
7929
8425
  try {
8426
+ logQQBotOutboundDispatch({
8427
+ action: "media",
8428
+ api: "sendFileQQBot",
8429
+ accountId,
8430
+ targetKind: target.kind,
8431
+ targetId: target.id,
8432
+ replyToId,
8433
+ replyEventId,
8434
+ text: sendTextAsFollowup ? void 0 : trimmedText,
8435
+ mediaUrl
8436
+ });
7930
8437
  result = await sendFileQQBot({
7931
8438
  cfg: qqCfg,
7932
8439
  target: { kind: target.kind, id: target.id },
@@ -7995,7 +8502,21 @@ ${mediaUrl}` : mediaUrl;
7995
8502
  };
7996
8503
  }
7997
8504
  }
7998
- return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
8505
+ const refIdx = target.kind === "c2c" ? resolveResponseRefIdx(result) : void 0;
8506
+ if (target.kind === "c2c") {
8507
+ recordOutboundC2CRefIndex({
8508
+ refIdx,
8509
+ accountId,
8510
+ text: sendTextAsFollowup ? void 0 : trimmedText,
8511
+ mediaUrl
8512
+ });
8513
+ }
8514
+ return {
8515
+ channel: "qqbot",
8516
+ messageId: result.id,
8517
+ timestamp: result.timestamp,
8518
+ ...refIdx ? { refIdx } : {}
8519
+ };
7999
8520
  } catch (err) {
8000
8521
  const message = summarizeError(err);
8001
8522
  return { channel: "qqbot", error: message };
@@ -8014,8 +8535,9 @@ ${mediaUrl}` : mediaUrl;
8014
8535
  }
8015
8536
  try {
8016
8537
  const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
8538
+ let typingResult;
8017
8539
  try {
8018
- await sendC2CInputNotify({
8540
+ typingResult = await sendC2CInputNotify({
8019
8541
  accessToken,
8020
8542
  openid: target.id,
8021
8543
  messageId: replyToId,
@@ -8037,7 +8559,7 @@ ${mediaUrl}` : mediaUrl;
8037
8559
  reason: summarizeError(err)
8038
8560
  });
8039
8561
  try {
8040
- await sendC2CInputNotify({
8562
+ typingResult = await sendC2CInputNotify({
8041
8563
  accessToken,
8042
8564
  openid: target.id,
8043
8565
  eventId: replyEventId,
@@ -8066,7 +8588,10 @@ ${mediaUrl}` : mediaUrl;
8066
8588
  throw retryErr;
8067
8589
  }
8068
8590
  }
8069
- return { channel: "qqbot" };
8591
+ return {
8592
+ channel: "qqbot",
8593
+ ...typingResult?.refIdx ? { refIdx: typingResult.refIdx } : {}
8594
+ };
8070
8595
  } catch (err) {
8071
8596
  const message = summarizeError(err);
8072
8597
  return { channel: "qqbot", error: message };
@@ -8076,13 +8601,301 @@ ${mediaUrl}` : mediaUrl;
8076
8601
 
8077
8602
  // src/logger.ts
8078
8603
  createLogger("qqbot");
8079
- var DEFAULT_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "data", "qqbot", "known-targets.json");
8604
+ var DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE = {
8605
+ width: 512,
8606
+ height: 512
8607
+ };
8608
+ var MARKDOWN_IMAGE_RE2 = /!\[([^\]]*)\]\(([^)\n]+)\)/g;
8609
+ var BARE_HTTP_IMAGE_URL_RE = /(?<![(\["'<])(https?:\/\/[^\s)"'<>]+\.(?:png|jpe?g|gif|webp)(?:\?[^\s)"'<>]*)?)/gi;
8610
+ var FENCED_CODE_BLOCK_RE = /(^|\n)(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g;
8611
+ function parsePngSize(buffer) {
8612
+ if (buffer.length < 24) return null;
8613
+ if (buffer[0] !== 137 || buffer[1] !== 80 || buffer[2] !== 78 || buffer[3] !== 71) {
8614
+ return null;
8615
+ }
8616
+ return {
8617
+ width: buffer.readUInt32BE(16),
8618
+ height: buffer.readUInt32BE(20)
8619
+ };
8620
+ }
8621
+ function parseJpegSize(buffer) {
8622
+ if (buffer.length < 4 || buffer[0] !== 255 || buffer[1] !== 216) {
8623
+ return null;
8624
+ }
8625
+ let offset = 2;
8626
+ while (offset < buffer.length - 9) {
8627
+ if (buffer[offset] !== 255) {
8628
+ offset += 1;
8629
+ continue;
8630
+ }
8631
+ const marker = buffer[offset + 1];
8632
+ if (marker === 192 || marker === 194) {
8633
+ return {
8634
+ height: buffer.readUInt16BE(offset + 5),
8635
+ width: buffer.readUInt16BE(offset + 7)
8636
+ };
8637
+ }
8638
+ if (offset + 3 >= buffer.length) {
8639
+ break;
8640
+ }
8641
+ const blockLength = buffer.readUInt16BE(offset + 2);
8642
+ offset += 2 + blockLength;
8643
+ }
8644
+ return null;
8645
+ }
8646
+ function parseGifSize(buffer) {
8647
+ if (buffer.length < 10) return null;
8648
+ const signature = buffer.toString("ascii", 0, 6);
8649
+ if (signature !== "GIF87a" && signature !== "GIF89a") {
8650
+ return null;
8651
+ }
8652
+ return {
8653
+ width: buffer.readUInt16LE(6),
8654
+ height: buffer.readUInt16LE(8)
8655
+ };
8656
+ }
8657
+ function parseWebpSize(buffer) {
8658
+ if (buffer.length < 30) return null;
8659
+ if (buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WEBP") {
8660
+ return null;
8661
+ }
8662
+ const chunkType = buffer.toString("ascii", 12, 16);
8663
+ if (chunkType === "VP8 " && buffer[23] === 157 && buffer[24] === 1 && buffer[25] === 42) {
8664
+ return {
8665
+ width: buffer.readUInt16LE(26) & 16383,
8666
+ height: buffer.readUInt16LE(28) & 16383
8667
+ };
8668
+ }
8669
+ if (chunkType === "VP8L" && buffer[20] === 47) {
8670
+ const bits = buffer.readUInt32LE(21);
8671
+ return {
8672
+ width: (bits & 16383) + 1,
8673
+ height: (bits >> 14 & 16383) + 1
8674
+ };
8675
+ }
8676
+ if (chunkType === "VP8X") {
8677
+ return {
8678
+ width: (buffer[24] | buffer[25] << 8 | buffer[26] << 16) + 1,
8679
+ height: (buffer[27] | buffer[28] << 8 | buffer[29] << 16) + 1
8680
+ };
8681
+ }
8682
+ return null;
8683
+ }
8684
+ function parseImageSize(buffer) {
8685
+ return parsePngSize(buffer) ?? parseJpegSize(buffer) ?? parseGifSize(buffer) ?? parseWebpSize(buffer);
8686
+ }
8687
+ function normalizeImageSize(size) {
8688
+ if (!size) return null;
8689
+ if (!Number.isFinite(size.width) || !Number.isFinite(size.height)) return null;
8690
+ if (size.width <= 0 || size.height <= 0) return null;
8691
+ return {
8692
+ width: Math.round(size.width),
8693
+ height: Math.round(size.height)
8694
+ };
8695
+ }
8696
+ function splitMarkdownImageDestination(rawDestination) {
8697
+ let next = rawDestination.trim();
8698
+ const whitespaceIndex = next.search(/\s/);
8699
+ if (whitespaceIndex >= 0) {
8700
+ next = next.slice(0, whitespaceIndex);
8701
+ }
8702
+ if (next.startsWith("<") && next.endsWith(">")) {
8703
+ next = next.slice(1, -1).trim();
8704
+ }
8705
+ return next;
8706
+ }
8707
+ function splitFencedCodeBlocks(text) {
8708
+ const segments = [];
8709
+ const re = new RegExp(FENCED_CODE_BLOCK_RE.source, FENCED_CODE_BLOCK_RE.flags);
8710
+ let lastIndex = 0;
8711
+ let match;
8712
+ while ((match = re.exec(text)) !== null) {
8713
+ const leading = match[1] ?? "";
8714
+ const codeStart = match.index + leading.length;
8715
+ if (codeStart > lastIndex) {
8716
+ segments.push({ kind: "text", value: text.slice(lastIndex, codeStart) });
8717
+ }
8718
+ segments.push({ kind: "code", value: text.slice(codeStart, re.lastIndex) });
8719
+ lastIndex = re.lastIndex;
8720
+ }
8721
+ if (lastIndex < text.length) {
8722
+ segments.push({ kind: "text", value: text.slice(lastIndex) });
8723
+ }
8724
+ return segments;
8725
+ }
8726
+ async function replaceAsync(input2, pattern, replacer) {
8727
+ const re = new RegExp(pattern.source, pattern.flags);
8728
+ let result = "";
8729
+ let lastIndex = 0;
8730
+ let match;
8731
+ while ((match = re.exec(input2)) !== null) {
8732
+ result += input2.slice(lastIndex, match.index);
8733
+ result += await replacer(match);
8734
+ lastIndex = match.index + match[0].length;
8735
+ if (match[0].length === 0) {
8736
+ re.lastIndex += 1;
8737
+ }
8738
+ }
8739
+ result += input2.slice(lastIndex);
8740
+ return result;
8741
+ }
8742
+ async function getResolvedImageSize(params) {
8743
+ const { url, cache: cache2, resolveImageSize } = params;
8744
+ const existing = cache2.get(url);
8745
+ if (existing) {
8746
+ return existing;
8747
+ }
8748
+ const pending = resolveImageSize(url).then((size) => normalizeImageSize(size) ?? DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE).catch(() => DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE);
8749
+ cache2.set(url, pending);
8750
+ return pending;
8751
+ }
8752
+ async function normalizeTextSegment(params) {
8753
+ const { text, seenImageUrls, imageSizeCache, resolveImageSize } = params;
8754
+ const withMarkdownImages = await replaceAsync(text, MARKDOWN_IMAGE_RE2, async (match) => {
8755
+ const fullMatch = match[0];
8756
+ const destination = splitMarkdownImageDestination(match[2] ?? "");
8757
+ if (!isQQBotHttpImageUrl(destination)) {
8758
+ return fullMatch;
8759
+ }
8760
+ seenImageUrls.add(destination);
8761
+ if (hasQQBotMarkdownImageSize(fullMatch)) {
8762
+ return fullMatch;
8763
+ }
8764
+ const size = await getResolvedImageSize({
8765
+ url: destination,
8766
+ cache: imageSizeCache,
8767
+ resolveImageSize
8768
+ });
8769
+ return formatQQBotMarkdownImage(destination, size);
8770
+ });
8771
+ return replaceAsync(withMarkdownImages, BARE_HTTP_IMAGE_URL_RE, async (match) => {
8772
+ const url = match[1] ?? match[0];
8773
+ if (!isQQBotHttpImageUrl(url)) {
8774
+ return match[0];
8775
+ }
8776
+ seenImageUrls.add(url);
8777
+ const size = await getResolvedImageSize({
8778
+ url,
8779
+ cache: imageSizeCache,
8780
+ resolveImageSize
8781
+ });
8782
+ return formatQQBotMarkdownImage(url, size);
8783
+ });
8784
+ }
8785
+ function isQQBotHttpImageUrl(url) {
8786
+ const trimmed = url.trim();
8787
+ if (!/^https?:\/\//i.test(trimmed)) {
8788
+ return false;
8789
+ }
8790
+ try {
8791
+ const parsed = new URL(trimmed);
8792
+ return /\.(?:png|jpe?g|gif|webp)$/i.test(parsed.pathname);
8793
+ } catch {
8794
+ return /\.(?:png|jpe?g|gif|webp)(?:\?[^\s)"'<>]*)?$/i.test(trimmed);
8795
+ }
8796
+ }
8797
+ async function getQQBotHttpImageSize(url, timeoutMs = 5e3) {
8798
+ if (!isQQBotHttpImageUrl(url)) {
8799
+ return null;
8800
+ }
8801
+ const controller = new AbortController();
8802
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
8803
+ try {
8804
+ const response = await fetch(url, {
8805
+ signal: controller.signal,
8806
+ headers: {
8807
+ Range: "bytes=0-65535",
8808
+ "User-Agent": "OpenClaw-QQBot-ImageSize/1.0"
8809
+ }
8810
+ });
8811
+ if (!(response.ok || response.status === 206)) {
8812
+ return null;
8813
+ }
8814
+ const buffer = Buffer$1.from(await response.arrayBuffer());
8815
+ return normalizeImageSize(parseImageSize(buffer));
8816
+ } catch {
8817
+ return null;
8818
+ } finally {
8819
+ clearTimeout(timeoutId);
8820
+ }
8821
+ }
8822
+ function formatQQBotMarkdownImage(url, size) {
8823
+ const resolved = normalizeImageSize(size) ?? DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE;
8824
+ return `![#${resolved.width}px #${resolved.height}px](${url})`;
8825
+ }
8826
+ function hasQQBotMarkdownImageSize(markdownImage) {
8827
+ return /!\[#\d+px\s+#\d+px\]\([^)]+\)/.test(markdownImage);
8828
+ }
8829
+ async function normalizeQQBotMarkdownImages(params) {
8830
+ const text = params.text ?? "";
8831
+ const appendImageUrls = params.appendImageUrls ?? [];
8832
+ const resolveImageSize = params.resolveImageSize ?? ((url) => getQQBotHttpImageSize(url, params.timeoutMs));
8833
+ const imageSizeCache = /* @__PURE__ */ new Map();
8834
+ const seenImageUrls = /* @__PURE__ */ new Set();
8835
+ const segments = splitFencedCodeBlocks(text);
8836
+ const normalizedSegments = [];
8837
+ for (const segment of segments) {
8838
+ if (segment.kind === "code") {
8839
+ normalizedSegments.push(segment.value);
8840
+ continue;
8841
+ }
8842
+ normalizedSegments.push(
8843
+ await normalizeTextSegment({
8844
+ text: segment.value,
8845
+ seenImageUrls,
8846
+ imageSizeCache,
8847
+ resolveImageSize
8848
+ })
8849
+ );
8850
+ }
8851
+ const appendedImages = [];
8852
+ for (const rawUrl of appendImageUrls) {
8853
+ const url = rawUrl.trim();
8854
+ if (!isQQBotHttpImageUrl(url) || seenImageUrls.has(url)) {
8855
+ continue;
8856
+ }
8857
+ seenImageUrls.add(url);
8858
+ const size = await getResolvedImageSize({
8859
+ url,
8860
+ cache: imageSizeCache,
8861
+ resolveImageSize
8862
+ });
8863
+ appendedImages.push(formatQQBotMarkdownImage(url, size));
8864
+ }
8865
+ const body = normalizedSegments.join("").trim();
8866
+ if (appendedImages.length === 0) {
8867
+ return body;
8868
+ }
8869
+ if (!body) {
8870
+ return appendedImages.join("\n");
8871
+ }
8872
+ return `${body}
8873
+
8874
+ ${appendedImages.join("\n")}`;
8875
+ }
8876
+ var DEFAULT_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "qqbot", "data", "known-targets.json");
8877
+ var LEGACY_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "data", "qqbot", "known-targets.json");
8080
8878
  function resolveKnownTargetsFilePath(options) {
8081
8879
  return options?.filePath?.trim() || DEFAULT_KNOWN_TARGETS_PATH;
8082
8880
  }
8083
8881
  function ensureKnownTargetsDir(filePath) {
8084
8882
  mkdirSync(dirname(filePath), { recursive: true });
8085
8883
  }
8884
+ function migrateLegacyKnownTargets(filePath) {
8885
+ if (filePath !== DEFAULT_KNOWN_TARGETS_PATH) {
8886
+ return;
8887
+ }
8888
+ if (existsSync(filePath) || !existsSync(LEGACY_KNOWN_TARGETS_PATH)) {
8889
+ return;
8890
+ }
8891
+ ensureKnownTargetsDir(filePath);
8892
+ try {
8893
+ renameSync(LEGACY_KNOWN_TARGETS_PATH, filePath);
8894
+ } catch {
8895
+ copyFileSync(LEGACY_KNOWN_TARGETS_PATH, filePath);
8896
+ rmSync(LEGACY_KNOWN_TARGETS_PATH, { force: true });
8897
+ }
8898
+ }
8086
8899
  function compareTargetsByLastSeenDesc(a, b) {
8087
8900
  if (b.lastSeenAt !== a.lastSeenAt) {
8088
8901
  return b.lastSeenAt - a.lastSeenAt;
@@ -8118,6 +8931,7 @@ function parseKnownTargets(raw, filePath) {
8118
8931
  }
8119
8932
  function readKnownTargets(options) {
8120
8933
  const filePath = resolveKnownTargetsFilePath(options);
8934
+ migrateLegacyKnownTargets(filePath);
8121
8935
  if (!existsSync(filePath)) {
8122
8936
  return [];
8123
8937
  }
@@ -8129,6 +8943,7 @@ function readKnownTargets(options) {
8129
8943
  }
8130
8944
  function writeKnownTargets(targets, options) {
8131
8945
  const filePath = resolveKnownTargetsFilePath(options);
8946
+ migrateLegacyKnownTargets(filePath);
8132
8947
  if (targets.length === 0) {
8133
8948
  if (existsSync(filePath)) {
8134
8949
  rmSync(filePath, { force: true });
@@ -8261,6 +9076,65 @@ function getQQBotRuntime() {
8261
9076
  }
8262
9077
  return runtime;
8263
9078
  }
9079
+ var sessionDispatchQueue = /* @__PURE__ */ new Map();
9080
+ function resolveQQBotRouteSessionKey(route) {
9081
+ const effectiveSessionKey = route.effectiveSessionKey?.trim();
9082
+ if (effectiveSessionKey) {
9083
+ return effectiveSessionKey;
9084
+ }
9085
+ return route.sessionKey;
9086
+ }
9087
+ function buildSessionDispatchQueueKey(route) {
9088
+ const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
9089
+ return `${accountId}:${resolveQQBotRouteSessionKey(route)}`;
9090
+ }
9091
+ function normalizeQQBotSessionKeyPart(value) {
9092
+ const trimmed = value.trim();
9093
+ return trimmed ? trimmed.toLowerCase() : "unknown";
9094
+ }
9095
+ function buildQQBotDirectSessionKey(params) {
9096
+ const normalizedAccountId = normalizeQQBotSessionKeyPart(params.accountId);
9097
+ const normalizedSenderId = normalizeQQBotSessionKeyPart(params.senderStableId);
9098
+ const trimmedRouteSessionKey = params.routeSessionKey.trim();
9099
+ if (!trimmedRouteSessionKey) {
9100
+ return `agent:main:qqbot:dm:${normalizedAccountId}:${normalizedSenderId}`;
9101
+ }
9102
+ const qqAgentRouteMatch = trimmedRouteSessionKey.match(/^(agent:[^:]+:qqbot:)(?:direct|dm):.+$/i);
9103
+ if (qqAgentRouteMatch?.[1]) {
9104
+ return `${qqAgentRouteMatch[1]}dm:${normalizedAccountId}:${normalizedSenderId}`;
9105
+ }
9106
+ return `${trimmedRouteSessionKey}:dm:${normalizedAccountId}:${normalizedSenderId}`;
9107
+ }
9108
+ function normalizeQQBotReplyTarget(value) {
9109
+ if (typeof value !== "string") {
9110
+ return void 0;
9111
+ }
9112
+ let trimmed = value.trim();
9113
+ if (!trimmed) {
9114
+ return void 0;
9115
+ }
9116
+ if (/^qqbot:/i.test(trimmed)) {
9117
+ trimmed = trimmed.slice("qqbot:".length).trim();
9118
+ }
9119
+ if (/^c2c:/i.test(trimmed)) {
9120
+ const openid = trimmed.slice("c2c:".length).trim();
9121
+ return openid ? `user:${openid}` : void 0;
9122
+ }
9123
+ return /^(user|group|channel):/i.test(trimmed) ? trimmed : void 0;
9124
+ }
9125
+ async function runSerializedSessionDispatch(queueKey, task) {
9126
+ const previous = sessionDispatchQueue.get(queueKey) ?? Promise.resolve();
9127
+ const run = previous.catch(() => void 0).then(task);
9128
+ const cleanup = run.then(() => void 0, () => void 0);
9129
+ sessionDispatchQueue.set(queueKey, cleanup);
9130
+ try {
9131
+ return await run;
9132
+ } finally {
9133
+ if (sessionDispatchQueue.get(queueKey) === cleanup) {
9134
+ sessionDispatchQueue.delete(queueKey);
9135
+ }
9136
+ }
9137
+ }
8264
9138
  function toString(value) {
8265
9139
  if (typeof value === "string" && value.trim()) return value;
8266
9140
  return void 0;
@@ -8311,6 +9185,33 @@ function parseTextWithAttachments(payload) {
8311
9185
  attachments
8312
9186
  };
8313
9187
  }
9188
+ function parseQQBotRefIndices(payload) {
9189
+ const scene = payload.message_scene;
9190
+ if (!scene || typeof scene !== "object") {
9191
+ return {};
9192
+ }
9193
+ const ext = scene.ext;
9194
+ if (!Array.isArray(ext)) {
9195
+ return {};
9196
+ }
9197
+ let refMsgIdx;
9198
+ let msgIdx;
9199
+ for (const value of ext) {
9200
+ const item = toString(value);
9201
+ if (!item) continue;
9202
+ if (item.startsWith("ref_msg_idx=")) {
9203
+ refMsgIdx = toString(item.slice("ref_msg_idx=".length));
9204
+ continue;
9205
+ }
9206
+ if (item.startsWith("msg_idx=")) {
9207
+ msgIdx = toString(item.slice("msg_idx=".length));
9208
+ }
9209
+ }
9210
+ return {
9211
+ ...refMsgIdx ? { refMsgIdx } : {},
9212
+ ...msgIdx ? { msgIdx } : {}
9213
+ };
9214
+ }
8314
9215
  function resolveEventId(payload, fallbackEventId) {
8315
9216
  return toString(payload.event_id) ?? toString(payload.eventId) ?? toString(fallbackEventId);
8316
9217
  }
@@ -8320,6 +9221,7 @@ var VOICE_ASR_ERROR_MAX_LENGTH = 500;
8320
9221
  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";
8321
9222
  var DEFAULT_LONG_TASK_NOTICE_DELAY_MS = 3e4;
8322
9223
  var QQ_GROUP_NO_REPLY_FALLBACK_TEXT = "\u6211\u5728\u3002\u4F60\u53EF\u4EE5\u76F4\u63A5\u8BF4\u5177\u4F53\u4E00\u70B9\u3002";
9224
+ var QQ_QUOTE_BODY_UNAVAILABLE_TEXT = "\u539F\u59CB\u5185\u5BB9\u4E0D\u53EF\u7528";
8323
9225
  function startLongTaskNoticeTimer(params) {
8324
9226
  const { delayMs, logger, sendNotice } = params;
8325
9227
  let completed = false;
@@ -8354,7 +9256,7 @@ function startLongTaskNoticeTimer(params) {
8354
9256
  }
8355
9257
  };
8356
9258
  }
8357
- function isHttpUrl2(value) {
9259
+ function isHttpUrl3(value) {
8358
9260
  return /^https?:\/\//i.test(value);
8359
9261
  }
8360
9262
  function isImageAttachment(att) {
@@ -8427,7 +9329,7 @@ async function resolveInboundAttachmentsForAgent(params) {
8427
9329
  let asrErrorMessage;
8428
9330
  for (const att of list) {
8429
9331
  const next = { attachment: att };
8430
- if (isImageAttachment(att) && isHttpUrl2(att.url)) {
9332
+ if (isImageAttachment(att) && isHttpUrl3(att.url)) {
8431
9333
  try {
8432
9334
  const downloaded = await downloadToTempFile(att.url, {
8433
9335
  timeout,
@@ -8456,7 +9358,7 @@ async function resolveInboundAttachmentsForAgent(params) {
8456
9358
  logger.info("voice attachment received but ASR is disabled");
8457
9359
  } else if (!asrCredentials) {
8458
9360
  logger.warn("voice ASR enabled but credentials are missing or invalid");
8459
- } else if (!isHttpUrl2(att.url)) {
9361
+ } else if (!isHttpUrl3(att.url)) {
8460
9362
  logger.warn("voice ASR skipped: attachment URL is not an HTTP URL");
8461
9363
  } else {
8462
9364
  try {
@@ -8527,6 +9429,74 @@ function buildInboundContentWithAttachments(params) {
8527
9429
  parts.push(block);
8528
9430
  return parts.join("\n\n");
8529
9431
  }
9432
+ function resolveRefAttachmentType(attachment) {
9433
+ const contentType = attachment.contentType?.trim().toLowerCase() ?? "";
9434
+ if (contentType.startsWith("image/") || isImageAttachment(attachment)) {
9435
+ return "image";
9436
+ }
9437
+ if (contentType === "voice" || contentType.startsWith("audio/") || isVoiceAttachment(attachment)) {
9438
+ return "voice";
9439
+ }
9440
+ if (contentType.startsWith("video/")) {
9441
+ return "video";
9442
+ }
9443
+ const mediaType = detectMediaType(attachment.filename?.trim() || attachment.url);
9444
+ if (mediaType === "image") return "image";
9445
+ if (mediaType === "audio") return "voice";
9446
+ if (mediaType === "video") return "video";
9447
+ if (mediaType === "file") return "file";
9448
+ return "unknown";
9449
+ }
9450
+ function buildInboundRefAttachmentSummaries(attachments) {
9451
+ if (attachments.length === 0) {
9452
+ return void 0;
9453
+ }
9454
+ return attachments.map((item) => ({
9455
+ type: resolveRefAttachmentType(item.attachment),
9456
+ ...item.attachment.filename?.trim() ? { filename: item.attachment.filename.trim() } : {},
9457
+ ...item.attachment.contentType?.trim() ? { contentType: item.attachment.contentType.trim() } : {},
9458
+ ...item.localImagePath?.trim() ? { localPath: item.localImagePath.trim() } : {},
9459
+ ...item.attachment.url?.trim() ? { url: item.attachment.url.trim() } : {},
9460
+ ...item.voiceTranscript?.trim() ? {
9461
+ transcript: item.voiceTranscript.trim(),
9462
+ transcriptSource: "asr"
9463
+ } : {}
9464
+ }));
9465
+ }
9466
+ function buildQuotedAgentBody(params) {
9467
+ const quoteBlock = `[\u5F15\u7528\u6D88\u606F\u5F00\u59CB]
9468
+ ${params.replyToBody}
9469
+ [\u5F15\u7528\u6D88\u606F\u7ED3\u675F]`;
9470
+ return params.baseBody ? `${quoteBlock}
9471
+
9472
+ ${params.baseBody}` : quoteBlock;
9473
+ }
9474
+ function resolveAgentBodyBase(ctx) {
9475
+ if (typeof ctx.BodyForAgent === "string" && ctx.BodyForAgent.trim()) {
9476
+ return ctx.BodyForAgent;
9477
+ }
9478
+ if (typeof ctx.RawBody === "string" && ctx.RawBody.trim()) {
9479
+ return ctx.RawBody;
9480
+ }
9481
+ if (typeof ctx.Body === "string" && ctx.Body.trim()) {
9482
+ return ctx.Body;
9483
+ }
9484
+ if (typeof ctx.CommandBody === "string" && ctx.CommandBody.trim()) {
9485
+ return ctx.CommandBody;
9486
+ }
9487
+ return "";
9488
+ }
9489
+ function uniqueRefIndexKeys(...values) {
9490
+ const keys = [];
9491
+ const seen = /* @__PURE__ */ new Set();
9492
+ for (const value of values) {
9493
+ const next = value?.trim();
9494
+ if (!next || seen.has(next)) continue;
9495
+ seen.add(next);
9496
+ keys.push(next);
9497
+ }
9498
+ return keys;
9499
+ }
8530
9500
  function resolveInboundLogContent(params) {
8531
9501
  const text = params.content.trim();
8532
9502
  if (text) return text;
@@ -8548,6 +9518,7 @@ function sanitizeInboundLogText(text) {
8548
9518
  function parseC2CMessage(data, fallbackEventId) {
8549
9519
  const payload = data;
8550
9520
  const { text, attachments } = parseTextWithAttachments(payload);
9521
+ const refIndices = parseQQBotRefIndices(payload);
8551
9522
  const id = toString(payload.id);
8552
9523
  const eventId = resolveEventId(payload, fallbackEventId);
8553
9524
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
@@ -8564,6 +9535,7 @@ function parseC2CMessage(data, fallbackEventId) {
8564
9535
  messageId: id,
8565
9536
  eventId,
8566
9537
  timestamp,
9538
+ ...refIndices,
8567
9539
  mentionedBot: false
8568
9540
  };
8569
9541
  }
@@ -8677,6 +9649,22 @@ function resolveChatTarget(event) {
8677
9649
  peerKind: "dm"
8678
9650
  };
8679
9651
  }
9652
+ function resolveQQBotEffectiveSessionKey(params) {
9653
+ const { inbound, route, accountId } = params;
9654
+ if (inbound.type !== "direct") {
9655
+ return route.sessionKey;
9656
+ }
9657
+ const senderStableId = inbound.c2cOpenid?.trim() || inbound.senderId?.trim();
9658
+ if (!senderStableId) {
9659
+ return route.sessionKey;
9660
+ }
9661
+ const resolvedAccountId = route.accountId?.trim() || accountId.trim() || DEFAULT_ACCOUNT_ID;
9662
+ return buildQQBotDirectSessionKey({
9663
+ routeSessionKey: route.sessionKey,
9664
+ accountId: resolvedAccountId,
9665
+ senderStableId
9666
+ });
9667
+ }
8680
9668
  function resolveEnvelopeFrom(event) {
8681
9669
  if (event.type === "group") {
8682
9670
  return `group:${(event.groupOpenid ?? "unknown").toLowerCase()}`;
@@ -8728,24 +9716,58 @@ function resolveKnownQQBotTargetFromInbound(params) {
8728
9716
  }
8729
9717
  function extractLocalMediaFromText(params) {
8730
9718
  const { text, logger } = params;
8731
- const result = extractMediaFromText(text, {
8732
- removeFromText: true,
8733
- checkExists: true,
8734
- existsSync: (p) => {
8735
- const exists = fs3.existsSync(p);
8736
- if (!exists) {
8737
- logger?.warn?.(`[media] local file not found: ${p}`);
8738
- }
8739
- return exists;
8740
- },
8741
- parseMediaLines: false,
8742
- parseMarkdownImages: true,
8743
- parseHtmlImages: false,
8744
- parseBarePaths: true,
8745
- parseMarkdownLinks: true
9719
+ const mediaUrls = [];
9720
+ const seenMedia = /* @__PURE__ */ new Set();
9721
+ let nextText = text;
9722
+ const MARKDOWN_LINKED_IMAGE_RE2 = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g;
9723
+ const MARKDOWN_IMAGE_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
9724
+ const MARKDOWN_LINK_RE2 = /\[([^\]]*)\]\(([^)]+)\)/g;
9725
+ const BARE_LOCAL_MEDIA_PATH_RE = /`?((?:\/(?:tmp|var|private|Users|home|root)\/[^\s`'",)]+|[A-Za-z]:[\\/][^\s`'",)]+)\.(?:png|jpg|jpeg|gif|bmp|webp|svg|ico|mp3|wav|ogg|m4a|amr|flac|aac|wma|mp4|mov|avi|mkv|webm|flv|wmv|m4v))`?/gi;
9726
+ const collectLocalRichMedia = (rawValue, allowedTypes) => {
9727
+ const candidate = stripTitleFromUrl(rawValue.trim());
9728
+ if (!candidate || !isLocalReference(candidate)) {
9729
+ return void 0;
9730
+ }
9731
+ if (!fs3.existsSync(candidate)) {
9732
+ logger?.warn?.(`[media] local file not found: ${candidate}`);
9733
+ return void 0;
9734
+ }
9735
+ const mediaType = detectMediaType(candidate);
9736
+ if (mediaType === "file") {
9737
+ return void 0;
9738
+ }
9739
+ if (allowedTypes && !allowedTypes.has(mediaType)) {
9740
+ return void 0;
9741
+ }
9742
+ if (seenMedia.has(candidate)) {
9743
+ return candidate;
9744
+ }
9745
+ seenMedia.add(candidate);
9746
+ mediaUrls.push(candidate);
9747
+ return candidate;
9748
+ };
9749
+ nextText = nextText.replace(MARKDOWN_LINKED_IMAGE_RE2, (fullMatch, _alt, rawPath) => {
9750
+ return collectLocalRichMedia(rawPath) ? "" : fullMatch;
8746
9751
  });
8747
- const mediaUrls = result.all.filter((m) => m.isLocal && typeof m.localPath === "string").filter((m) => m.type !== "file").map((m) => m.localPath);
8748
- return { text: result.text, mediaUrls };
9752
+ nextText = nextText.replace(MARKDOWN_IMAGE_RE3, (fullMatch, _alt, rawPath) => {
9753
+ return collectLocalRichMedia(rawPath) ? "" : fullMatch;
9754
+ });
9755
+ nextText = nextText.replace(MARKDOWN_LINK_RE2, (fullMatch, _label, rawPath) => {
9756
+ const mediaPath = collectLocalRichMedia(rawPath, /* @__PURE__ */ new Set(["audio", "video"]));
9757
+ if (!mediaPath) {
9758
+ return fullMatch;
9759
+ }
9760
+ return "";
9761
+ });
9762
+ nextText = nextText.replace(BARE_LOCAL_MEDIA_PATH_RE, (fullMatch, rawPath) => {
9763
+ return collectLocalRichMedia(rawPath) ? "" : fullMatch;
9764
+ });
9765
+ nextText = nextText.replace(/[ \t]+\n/g, "\n");
9766
+ nextText = nextText.replace(/\n{3,}/g, "\n\n");
9767
+ return {
9768
+ text: nextText.trim(),
9769
+ mediaUrls
9770
+ };
8749
9771
  }
8750
9772
  function extractMediaLinesFromText(params) {
8751
9773
  const { text, logger } = params;
@@ -8798,6 +9820,9 @@ var FILE_PLACEHOLDER_RE = /\[文件:\s*[^\]\n]+\]/g;
8798
9820
  var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_voice|tts(?::text)?|\/tts(?::text)?)\s*\]\]/gi;
8799
9821
  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;
8800
9822
  var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
9823
+ var MARKDOWN_TABLE_SEPARATOR_RE = /^\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?)?\|?$/;
9824
+ var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
9825
+ var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
8801
9826
  function extractFinalBlocks(text) {
8802
9827
  const matches = Array.from(text.matchAll(FINAL_BLOCK_RE));
8803
9828
  if (matches.length === 0) return void 0;
@@ -8822,6 +9847,14 @@ function sanitizeQQBotOutboundText(rawText) {
8822
9847
  if (/^NO_REPLY$/i.test(next)) return "";
8823
9848
  return next;
8824
9849
  }
9850
+ function formatQQBotOutboundPreview(text, maxLength = 240) {
9851
+ const normalized = text.replace(/\r\n/g, "\n").trim();
9852
+ if (!normalized) {
9853
+ return '""';
9854
+ }
9855
+ const preview = normalized.length > maxLength ? `${normalized.slice(0, Math.max(0, maxLength - 3))}...` : normalized;
9856
+ return JSON.stringify(preview);
9857
+ }
8825
9858
  function shouldSuppressQQBotTextWhenMediaPresent(rawText, sanitizedText) {
8826
9859
  const raw = rawText.trim();
8827
9860
  if (!raw) return false;
@@ -8854,8 +9887,113 @@ function evaluateReplyFinalOnlyDelivery(params) {
8854
9887
  }
8855
9888
  return { skipDelivery: true, suppressText: false };
8856
9889
  }
9890
+ function isQQBotC2CTarget(to) {
9891
+ const trimmed = to.trim();
9892
+ const raw = trimmed.startsWith("qqbot:") ? trimmed.slice("qqbot:".length) : trimmed;
9893
+ return !raw.startsWith("group:") && !raw.startsWith("channel:");
9894
+ }
9895
+ function splitQQBotMarkdownTransportMediaUrls(mediaUrls) {
9896
+ const markdownImageUrls = [];
9897
+ const mediaQueue = [];
9898
+ const seenMarkdownImages = /* @__PURE__ */ new Set();
9899
+ const seenMedia = /* @__PURE__ */ new Set();
9900
+ for (const rawUrl of mediaUrls) {
9901
+ const next = rawUrl.trim();
9902
+ if (!next) continue;
9903
+ if (isQQBotHttpImageUrl(next)) {
9904
+ if (seenMarkdownImages.has(next)) continue;
9905
+ seenMarkdownImages.add(next);
9906
+ markdownImageUrls.push(next);
9907
+ continue;
9908
+ }
9909
+ if (seenMedia.has(next)) continue;
9910
+ seenMedia.add(next);
9911
+ mediaQueue.push(next);
9912
+ }
9913
+ return { markdownImageUrls, mediaQueue };
9914
+ }
9915
+ function hasQQBotMarkdownTable(text) {
9916
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
9917
+ for (let index = 0; index < lines.length - 1; index += 1) {
9918
+ const header = lines[index]?.trim() ?? "";
9919
+ const separator = lines[index + 1]?.trim() ?? "";
9920
+ if (!header.includes("|") || !MARKDOWN_TABLE_SEPARATOR_RE.test(separator)) {
9921
+ continue;
9922
+ }
9923
+ const headerColumns = header.split("|").filter((column) => column.trim()).length;
9924
+ const separatorColumns = separator.split("|").filter((column) => column.trim()).length;
9925
+ if (headerColumns >= 2 && separatorColumns >= 2) {
9926
+ return true;
9927
+ }
9928
+ }
9929
+ return false;
9930
+ }
9931
+ function resolveQQBotTextReplyRefs(params) {
9932
+ const mode = params.c2cMarkdownDeliveryMode ?? "proactive-table-only";
9933
+ const forceProactive = params.markdownSupport && isQQBotC2CTarget(params.to) && (mode === "proactive-all" || mode === "proactive-table-only" && hasQQBotMarkdownTable(params.text));
9934
+ if (!forceProactive) {
9935
+ return {
9936
+ forceProactive: false,
9937
+ replyToId: params.replyToId,
9938
+ replyEventId: params.replyEventId
9939
+ };
9940
+ }
9941
+ return {
9942
+ forceProactive: true,
9943
+ replyToId: void 0,
9944
+ replyEventId: void 0
9945
+ };
9946
+ }
9947
+ function appendQQBotBufferedText(bufferedTexts, nextText) {
9948
+ const normalized = nextText.trim();
9949
+ if (!normalized) return bufferedTexts;
9950
+ if (bufferedTexts.length === 0) return [normalized];
9951
+ const currentCombined = bufferedTexts.join("\n\n");
9952
+ if (currentCombined === normalized || currentCombined.includes(normalized)) {
9953
+ return bufferedTexts;
9954
+ }
9955
+ if (normalized.includes(currentCombined)) {
9956
+ return [normalized];
9957
+ }
9958
+ const last = bufferedTexts[bufferedTexts.length - 1];
9959
+ if (last === normalized) {
9960
+ return bufferedTexts;
9961
+ }
9962
+ return [...bufferedTexts, normalized];
9963
+ }
9964
+ function normalizeQQBotRenderedMarkdown(text) {
9965
+ if (!text.trim()) return "";
9966
+ let next = text.trim();
9967
+ let changed = false;
9968
+ next = next.replace(
9969
+ EXPLICIT_MARKDOWN_FENCE_RE,
9970
+ (block, leadingLineBreak, _fence, inner) => {
9971
+ const normalizedInner = inner.trim();
9972
+ if (!normalizedInner) {
9973
+ return block;
9974
+ }
9975
+ changed = true;
9976
+ return `${leadingLineBreak}${normalizedInner}`;
9977
+ }
9978
+ );
9979
+ next = next.replace(
9980
+ GENERIC_MARKDOWN_FENCE_RE,
9981
+ (block, leadingLineBreak, _fence, inner) => {
9982
+ const normalizedInner = inner.trim();
9983
+ if (!normalizedInner) {
9984
+ return block;
9985
+ }
9986
+ if (!hasQQBotMarkdownTable(normalizedInner)) {
9987
+ return block;
9988
+ }
9989
+ changed = true;
9990
+ return `${leadingLineBreak}${normalizedInner}`;
9991
+ }
9992
+ );
9993
+ return changed ? next.trim() : text.trim();
9994
+ }
8857
9995
  async function sendQQBotMediaWithFallback(params) {
8858
- const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger, onDelivered, onError } = params;
9996
+ const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
8859
9997
  const outbound = params.outbound ?? qqbotOutbound;
8860
9998
  for (const mediaUrl of mediaQueue) {
8861
9999
  const result = await outbound.sendMedia({
@@ -8863,7 +10001,8 @@ async function sendQQBotMediaWithFallback(params) {
8863
10001
  to,
8864
10002
  mediaUrl,
8865
10003
  replyToId,
8866
- replyEventId
10004
+ replyEventId,
10005
+ accountId
8867
10006
  });
8868
10007
  if (result.error) {
8869
10008
  logger.error(`sendMedia failed: ${result.error}`);
@@ -8877,7 +10016,8 @@ async function sendQQBotMediaWithFallback(params) {
8877
10016
  to,
8878
10017
  text: fallback,
8879
10018
  replyToId,
8880
- replyEventId
10019
+ replyEventId,
10020
+ accountId
8881
10021
  });
8882
10022
  if (fallbackResult.error) {
8883
10023
  logger.error(`sendText fallback failed: ${fallbackResult.error}`);
@@ -8920,32 +10060,27 @@ function buildInboundContext(params) {
8920
10060
  };
8921
10061
  }
8922
10062
  async function dispatchToAgent(params) {
8923
- const { inbound, cfg, qqCfg, accountId, logger } = params;
10063
+ const { inbound, cfg, qqCfg, accountId, logger, route } = params;
8924
10064
  const runtime2 = getQQBotRuntime();
8925
- const routing = runtime2.channel?.routing?.resolveAgentRoute;
8926
- if (!routing) {
8927
- logger.warn("routing API not available");
8928
- return;
8929
- }
10065
+ const routeSessionKey = resolveQQBotRouteSessionKey(route);
8930
10066
  const target = resolveChatTarget(inbound);
10067
+ const outboundAccountId = route.accountId ?? accountId;
10068
+ let typingRefIdx;
8931
10069
  if (inbound.c2cOpenid) {
8932
10070
  const typing = await qqbotOutbound.sendTyping({
8933
10071
  cfg: { channels: { qqbot: qqCfg } },
8934
10072
  to: `user:${inbound.c2cOpenid}`,
8935
10073
  replyToId: inbound.messageId,
8936
10074
  replyEventId: inbound.eventId,
8937
- inputSecond: 60
10075
+ inputSecond: 60,
10076
+ accountId: outboundAccountId
8938
10077
  });
8939
10078
  if (typing.error) {
8940
10079
  logger.warn(`sendTyping failed: ${typing.error}`);
10080
+ } else {
10081
+ typingRefIdx = typing.refIdx;
8941
10082
  }
8942
10083
  }
8943
- const route = routing({
8944
- cfg,
8945
- channel: "qqbot",
8946
- accountId,
8947
- peer: { kind: target.peerKind, id: target.peerId }
8948
- });
8949
10084
  const replyApi = runtime2.channel?.reply;
8950
10085
  if (!replyApi) {
8951
10086
  logger.warn("reply API not available");
@@ -8974,7 +10109,8 @@ async function dispatchToAgent(params) {
8974
10109
  to: target.to,
8975
10110
  text: LONG_TASK_NOTICE_TEXT,
8976
10111
  replyToId: inbound.messageId,
8977
- replyEventId: inbound.eventId
10112
+ replyEventId: inbound.eventId,
10113
+ accountId: outboundAccountId
8978
10114
  });
8979
10115
  if (result.error) {
8980
10116
  logger.warn(`send long-task notice failed: ${result.error}`);
@@ -8994,7 +10130,7 @@ async function dispatchToAgent(params) {
8994
10130
  { agentId: route.agentId }
8995
10131
  );
8996
10132
  const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
8997
- const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
10133
+ const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: routeSessionKey }) : null;
8998
10134
  const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
8999
10135
  attachments: inbound.attachments,
9000
10136
  qqCfg,
@@ -9006,7 +10142,8 @@ async function dispatchToAgent(params) {
9006
10142
  to: target.to,
9007
10143
  text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
9008
10144
  replyToId: inbound.messageId,
9009
- replyEventId: inbound.eventId
10145
+ replyEventId: inbound.eventId,
10146
+ accountId: outboundAccountId
9010
10147
  });
9011
10148
  if (fallback.error) {
9012
10149
  logger.error(`sendText ASR fallback failed: ${fallback.error}`);
@@ -9021,6 +10158,39 @@ async function dispatchToAgent(params) {
9021
10158
  if (localImageCount > 0) {
9022
10159
  logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
9023
10160
  }
10161
+ let replyToId;
10162
+ let replyToBody;
10163
+ let replyToSender;
10164
+ let replyToIsQuote = false;
10165
+ if (inbound.c2cOpenid && inbound.refMsgIdx) {
10166
+ replyToId = inbound.refMsgIdx;
10167
+ replyToIsQuote = true;
10168
+ const refEntry = getRefIndex(inbound.refMsgIdx);
10169
+ if (refEntry) {
10170
+ replyToBody = formatRefEntryForAgent(refEntry);
10171
+ replyToSender = refEntry.senderName ?? refEntry.senderId;
10172
+ logger.info(`quote context resolved refMsgIdx=${inbound.refMsgIdx}`);
10173
+ } else {
10174
+ replyToBody = QQ_QUOTE_BODY_UNAVAILABLE_TEXT;
10175
+ logger.warn(`quote context missing refMsgIdx=${inbound.refMsgIdx}`);
10176
+ }
10177
+ }
10178
+ const refAttachmentSummaries = buildInboundRefAttachmentSummaries(resolvedAttachments);
10179
+ const currentRefIndexKeys = inbound.c2cOpenid ? uniqueRefIndexKeys(inbound.msgIdx, typingRefIdx) : [];
10180
+ if (currentRefIndexKeys.length > 0) {
10181
+ for (const currentRefIndexKey of currentRefIndexKeys) {
10182
+ setRefIndex(currentRefIndexKey, {
10183
+ content: inbound.content,
10184
+ senderId: inbound.senderId,
10185
+ ...inbound.senderName ? { senderName: inbound.senderName } : {},
10186
+ timestamp: inbound.timestamp,
10187
+ ...refAttachmentSummaries ? { attachments: refAttachmentSummaries } : {}
10188
+ });
10189
+ }
10190
+ logger.info(
10191
+ `cached inbound ref_idx keys=${currentRefIndexKeys.join(",")} msgIdx=${inbound.msgIdx ?? "-"} typingRefIdx=${typingRefIdx ?? "-"}`
10192
+ );
10193
+ }
9024
10194
  const rawBody = buildInboundContentWithAttachments({
9025
10195
  content: inbound.content,
9026
10196
  attachments: resolvedAttachments
@@ -9046,40 +10216,48 @@ async function dispatchToAgent(params) {
9046
10216
  }) : rawBody;
9047
10217
  const inboundCtx = buildInboundContext({
9048
10218
  event: inbound,
9049
- sessionKey: route.sessionKey,
9050
- accountId: route.accountId ?? accountId,
10219
+ sessionKey: routeSessionKey,
10220
+ accountId: outboundAccountId,
9051
10221
  body: inboundBody,
9052
10222
  rawBody,
9053
10223
  commandBody: rawBody
9054
10224
  });
9055
10225
  const finalizeInboundContext = replyApi?.finalizeInboundContext;
9056
10226
  const finalCtx = finalizeInboundContext ? finalizeInboundContext(inboundCtx) : inboundCtx;
9057
- let cronBase = "";
9058
- if (typeof finalCtx.RawBody === "string" && finalCtx.RawBody) {
9059
- cronBase = finalCtx.RawBody;
9060
- } else if (typeof finalCtx.Body === "string" && finalCtx.Body) {
9061
- cronBase = finalCtx.Body;
9062
- } else if (typeof finalCtx.CommandBody === "string" && finalCtx.CommandBody) {
9063
- cronBase = finalCtx.CommandBody;
9064
- }
9065
- if (cronBase) {
9066
- const nextCron = appendCronHiddenPrompt(cronBase);
9067
- if (nextCron !== cronBase) {
9068
- finalCtx.BodyForAgent = nextCron;
10227
+ const ctxTo = normalizeQQBotReplyTarget(finalCtx.To);
10228
+ const ctxOriginatingTo = normalizeQQBotReplyTarget(finalCtx.OriginatingTo);
10229
+ const stableTo = ctxOriginatingTo ?? ctxTo ?? target.to;
10230
+ finalCtx.To = stableTo;
10231
+ finalCtx.OriginatingTo = stableTo;
10232
+ if (replyToId) {
10233
+ finalCtx.ReplyToId = replyToId;
10234
+ finalCtx.ReplyToBody = replyToBody;
10235
+ finalCtx.ReplyToSender = replyToSender;
10236
+ finalCtx.ReplyToIsQuote = replyToIsQuote;
10237
+ }
10238
+ const isSlashCommand = typeof finalCtx.CommandBody === "string" ? finalCtx.CommandBody.trim().startsWith("/") : typeof finalCtx.RawBody === "string" ? finalCtx.RawBody.trim().startsWith("/") : false;
10239
+ if (!isSlashCommand) {
10240
+ let agentBody = resolveAgentBodyBase(finalCtx);
10241
+ if (replyToIsQuote && replyToBody && replyToBody !== QQ_QUOTE_BODY_UNAVAILABLE_TEXT) {
10242
+ agentBody = buildQuotedAgentBody({
10243
+ baseBody: agentBody,
10244
+ replyToBody
10245
+ });
9069
10246
  }
10247
+ finalCtx.BodyForAgent = appendCronHiddenPrompt(agentBody);
9070
10248
  }
9071
10249
  if (storePath && sessionApi?.recordInboundSession) {
9072
10250
  try {
9073
- const mainSessionKeyRaw = route?.mainSessionKey;
9074
- const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw : void 0;
10251
+ const mainSessionKeyRaw = route.mainSessionKey;
10252
+ const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw.trim() : void 0;
9075
10253
  const isGroup = inbound.type === "group" || inbound.type === "channel";
9076
10254
  const updateLastRoute = !isGroup ? {
9077
10255
  sessionKey: mainSessionKey ?? route.sessionKey,
9078
10256
  channel: "qqbot",
9079
- to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${inbound.senderId}`,
9080
- accountId: route.accountId ?? accountId
10257
+ to: stableTo,
10258
+ accountId: outboundAccountId
9081
10259
  } : void 0;
9082
- const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : route.sessionKey;
10260
+ const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : routeSessionKey;
9083
10261
  await sessionApi.recordInboundSession({
9084
10262
  storePath,
9085
10263
  sessionKey: recordSessionKey,
@@ -9103,7 +10281,7 @@ async function dispatchToAgent(params) {
9103
10281
  const tableMode = textApi?.resolveMarkdownTableMode?.({
9104
10282
  cfg,
9105
10283
  channel: "qqbot",
9106
- accountId: route.accountId ?? accountId
10284
+ accountId: outboundAccountId
9107
10285
  });
9108
10286
  const resolvedTableMode = tableMode ?? "bullets";
9109
10287
  const chunkText = (text) => {
@@ -9116,6 +10294,100 @@ async function dispatchToAgent(params) {
9116
10294
  return [text];
9117
10295
  };
9118
10296
  const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
10297
+ const markdownSupport = qqCfg.markdownSupport ?? true;
10298
+ const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
10299
+ const useC2CMarkdownTransport = markdownSupport && isQQBotC2CTarget(target.to);
10300
+ let bufferedC2CMarkdownTexts = [];
10301
+ let bufferedC2CMarkdownMediaUrls = [];
10302
+ const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
10303
+ const bufferC2CMarkdownMedia = (url) => {
10304
+ const next = url?.trim();
10305
+ if (!next || bufferedC2CMarkdownMediaSeen.has(next)) return;
10306
+ bufferedC2CMarkdownMediaSeen.add(next);
10307
+ bufferedC2CMarkdownMediaUrls.push(next);
10308
+ };
10309
+ const sendC2CMarkdownTransportPayload = async (params2) => {
10310
+ const normalizedText = normalizeQQBotRenderedMarkdown(params2.text);
10311
+ const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(params2.mediaUrls);
10312
+ const finalMarkdownText = await normalizeQQBotMarkdownImages({
10313
+ text: normalizedText,
10314
+ appendImageUrls: markdownImageUrls
10315
+ });
10316
+ const textReplyRefs = resolveQQBotTextReplyRefs({
10317
+ to: target.to,
10318
+ text: finalMarkdownText || normalizedText,
10319
+ markdownSupport,
10320
+ c2cMarkdownDeliveryMode,
10321
+ replyToId: inbound.messageId,
10322
+ replyEventId: inbound.eventId
10323
+ });
10324
+ const textSegments = finalMarkdownText ? [finalMarkdownText] : [];
10325
+ const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
10326
+ logger.info(
10327
+ `delivery=${deliveryLabel} to=${target.to} segments=${textSegments.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")}`
10328
+ );
10329
+ await sendQQBotMediaWithFallback({
10330
+ qqCfg,
10331
+ to: target.to,
10332
+ mediaQueue,
10333
+ replyToId: textReplyRefs.replyToId,
10334
+ replyEventId: textReplyRefs.replyEventId,
10335
+ accountId: outboundAccountId,
10336
+ logger,
10337
+ onDelivered: () => {
10338
+ markReplyDelivered();
10339
+ },
10340
+ onError: (error) => {
10341
+ markGroupMessageInterfaceBlocked(error);
10342
+ }
10343
+ });
10344
+ if (!finalMarkdownText) {
10345
+ return;
10346
+ }
10347
+ for (let segmentIndex = 0; segmentIndex < textSegments.length; segmentIndex += 1) {
10348
+ const segment = textSegments[segmentIndex] ?? "";
10349
+ const chunks = chunkText(segment);
10350
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
10351
+ const chunk = chunks[chunkIndex] ?? "";
10352
+ logger.info(
10353
+ `delivery=${deliveryLabel} segment=${segmentIndex + 1}/${textSegments.length} chunk=${chunkIndex + 1}/${chunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
10354
+ );
10355
+ const result = await qqbotOutbound.sendText({
10356
+ cfg: { channels: { qqbot: qqCfg } },
10357
+ to: target.to,
10358
+ text: chunk,
10359
+ replyToId: textReplyRefs.replyToId,
10360
+ replyEventId: textReplyRefs.replyEventId,
10361
+ accountId: outboundAccountId
10362
+ });
10363
+ if (result.error) {
10364
+ logger.error(`send QQ markdown reply failed: ${result.error}`);
10365
+ markGroupMessageInterfaceBlocked(result.error);
10366
+ } else {
10367
+ logger.info(`sent QQ markdown reply (phase=${params2.phase}, len=${chunk.length})`);
10368
+ markReplyDelivered();
10369
+ }
10370
+ }
10371
+ }
10372
+ };
10373
+ const flushBufferedC2CMarkdownReply = async () => {
10374
+ if (!useC2CMarkdownTransport || bufferedC2CMarkdownTexts.length === 0 && bufferedC2CMarkdownMediaUrls.length === 0) {
10375
+ bufferedC2CMarkdownTexts = [];
10376
+ bufferedC2CMarkdownMediaUrls = [];
10377
+ bufferedC2CMarkdownMediaSeen.clear();
10378
+ return;
10379
+ }
10380
+ const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
10381
+ const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
10382
+ bufferedC2CMarkdownTexts = [];
10383
+ bufferedC2CMarkdownMediaUrls = [];
10384
+ bufferedC2CMarkdownMediaSeen.clear();
10385
+ await sendC2CMarkdownTransportPayload({
10386
+ text: combinedText,
10387
+ mediaUrls: combinedMediaUrls,
10388
+ phase: "buffered"
10389
+ });
10390
+ };
9119
10391
  const deliver = async (payload, info) => {
9120
10392
  const typed = payload;
9121
10393
  const extractedTextMedia = extractQQBotReplyMedia({
@@ -9146,16 +10418,43 @@ async function dispatchToAgent(params) {
9146
10418
  const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(extractedTextMedia.text, cleanedText);
9147
10419
  const suppressText = deliveryDecision.suppressText || suppressEchoText;
9148
10420
  const textToSend = suppressText ? "" : cleanedText;
10421
+ if (useC2CMarkdownTransport) {
10422
+ const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
10423
+ if (shouldBufferFinalOnlyPayload) {
10424
+ if (textToSend) {
10425
+ bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
10426
+ }
10427
+ for (const url of mediaQueue) {
10428
+ bufferC2CMarkdownMedia(url);
10429
+ }
10430
+ return;
10431
+ }
10432
+ await sendC2CMarkdownTransportPayload({
10433
+ text: textToSend,
10434
+ mediaUrls: mediaQueue,
10435
+ phase: "immediate"
10436
+ });
10437
+ return;
10438
+ }
9149
10439
  if (textToSend) {
9150
10440
  const converted = textApi?.convertMarkdownTables ? textApi.convertMarkdownTables(textToSend, resolvedTableMode) : textToSend;
10441
+ const textReplyRefs = resolveQQBotTextReplyRefs({
10442
+ to: target.to,
10443
+ text: converted,
10444
+ markdownSupport,
10445
+ c2cMarkdownDeliveryMode,
10446
+ replyToId: inbound.messageId,
10447
+ replyEventId: inbound.eventId
10448
+ });
9151
10449
  const chunks = chunkText(converted);
9152
10450
  for (const chunk of chunks) {
9153
10451
  const result = await qqbotOutbound.sendText({
9154
10452
  cfg: { channels: { qqbot: qqCfg } },
9155
10453
  to: target.to,
9156
10454
  text: chunk,
9157
- replyToId: inbound.messageId,
9158
- replyEventId: inbound.eventId
10455
+ replyToId: textReplyRefs.replyToId,
10456
+ replyEventId: textReplyRefs.replyEventId,
10457
+ accountId: outboundAccountId
9159
10458
  });
9160
10459
  if (result.error) {
9161
10460
  logger.error(`sendText failed: ${result.error}`);
@@ -9171,6 +10470,7 @@ async function dispatchToAgent(params) {
9171
10470
  mediaQueue,
9172
10471
  replyToId: inbound.messageId,
9173
10472
  replyEventId: inbound.eventId,
10473
+ accountId: outboundAccountId,
9174
10474
  logger,
9175
10475
  onDelivered: () => {
9176
10476
  markReplyDelivered();
@@ -9199,6 +10499,7 @@ async function dispatchToAgent(params) {
9199
10499
  }
9200
10500
  }
9201
10501
  });
10502
+ await flushBufferedC2CMarkdownReply();
9202
10503
  } else {
9203
10504
  const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
9204
10505
  deliver,
@@ -9228,6 +10529,7 @@ async function dispatchToAgent(params) {
9228
10529
  replyOptions: dispatcherResult.replyOptions
9229
10530
  });
9230
10531
  dispatcherResult.markDispatchIdle?.();
10532
+ await flushBufferedC2CMarkdownReply();
9231
10533
  }
9232
10534
  const noReplyFallback = resolveQQBotNoReplyFallback({
9233
10535
  inbound,
@@ -9240,7 +10542,8 @@ async function dispatchToAgent(params) {
9240
10542
  to: target.to,
9241
10543
  text: noReplyFallback,
9242
10544
  replyToId: inbound.messageId,
9243
- replyEventId: inbound.eventId
10545
+ replyEventId: inbound.eventId,
10546
+ accountId: outboundAccountId
9244
10547
  });
9245
10548
  if (fallbackResult.error) {
9246
10549
  logger.error(`sendText no-reply fallback failed: ${fallbackResult.error}`);
@@ -9332,13 +10635,44 @@ async function handleQQBotDispatch(params) {
9332
10635
  if (!content && attachmentCount === 0) {
9333
10636
  return;
9334
10637
  }
9335
- await dispatchToAgent({
9336
- inbound: { ...inbound, content },
10638
+ const runtime2 = getQQBotRuntime();
10639
+ const routing = runtime2.channel?.routing?.resolveAgentRoute;
10640
+ if (!routing) {
10641
+ logger.warn("routing API not available");
10642
+ return;
10643
+ }
10644
+ const target = resolveChatTarget(inbound);
10645
+ const route = routing({
9337
10646
  cfg: params.cfg,
9338
- qqCfg,
10647
+ channel: "qqbot",
9339
10648
  accountId,
9340
- logger
10649
+ peer: { kind: target.peerKind, id: target.peerId }
10650
+ });
10651
+ const effectiveSessionKey = resolveQQBotEffectiveSessionKey({
10652
+ inbound,
10653
+ route,
10654
+ accountId
9341
10655
  });
10656
+ const resolvedRoute = effectiveSessionKey === route.sessionKey ? route : {
10657
+ ...route,
10658
+ mainSessionKey: route.mainSessionKey?.trim() || route.sessionKey,
10659
+ effectiveSessionKey
10660
+ };
10661
+ const queueKey = buildSessionDispatchQueueKey(resolvedRoute);
10662
+ if (sessionDispatchQueue.has(queueKey)) {
10663
+ logger.info(`session busy; queueing inbound dispatch sessionKey=${resolveQQBotRouteSessionKey(resolvedRoute)}`);
10664
+ }
10665
+ await runSerializedSessionDispatch(
10666
+ queueKey,
10667
+ async () => dispatchToAgent({
10668
+ inbound: { ...inbound, content },
10669
+ cfg: params.cfg,
10670
+ qqCfg,
10671
+ accountId,
10672
+ logger,
10673
+ route: resolvedRoute
10674
+ })
10675
+ );
9342
10676
  }
9343
10677
 
9344
10678
  // src/monitor.ts
@@ -9638,7 +10972,8 @@ function resolveQQBotAccount(params) {
9638
10972
  enabled,
9639
10973
  configured,
9640
10974
  appId: credentials?.appId,
9641
- markdownSupport: merged.markdownSupport ?? true
10975
+ markdownSupport: merged.markdownSupport ?? true,
10976
+ c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only"
9642
10977
  };
9643
10978
  }
9644
10979
  var qqbotPlugin = {
@@ -9647,7 +10982,7 @@ var qqbotPlugin = {
9647
10982
  ...meta
9648
10983
  },
9649
10984
  capabilities: {
9650
- chatTypes: ["direct", "channel"],
10985
+ chatTypes: ["direct", "group", "channel"],
9651
10986
  media: true,
9652
10987
  reactions: false,
9653
10988
  threads: false,
@@ -9727,6 +11062,10 @@ var qqbotPlugin = {
9727
11062
  }
9728
11063
  },
9729
11064
  markdownSupport: { type: "boolean" },
11065
+ c2cMarkdownDeliveryMode: {
11066
+ type: "string",
11067
+ enum: ["passive", "proactive-table-only", "proactive-all"]
11068
+ },
9730
11069
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9731
11070
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9732
11071
  requireMention: { type: "boolean" },
@@ -9768,6 +11107,10 @@ var qqbotPlugin = {
9768
11107
  }
9769
11108
  },
9770
11109
  markdownSupport: { type: "boolean" },
11110
+ c2cMarkdownDeliveryMode: {
11111
+ type: "string",
11112
+ enum: ["passive", "proactive-table-only", "proactive-all"]
11113
+ },
9771
11114
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9772
11115
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9773
11116
  requireMention: { type: "boolean" },
@@ -9966,6 +11309,10 @@ var plugin = {
9966
11309
  }
9967
11310
  },
9968
11311
  markdownSupport: { type: "boolean" },
11312
+ c2cMarkdownDeliveryMode: {
11313
+ type: "string",
11314
+ enum: ["passive", "proactive-table-only", "proactive-all"]
11315
+ },
9969
11316
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9970
11317
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9971
11318
  requireMention: { type: "boolean" },
@@ -10007,6 +11354,10 @@ var plugin = {
10007
11354
  }
10008
11355
  },
10009
11356
  markdownSupport: { type: "boolean" },
11357
+ c2cMarkdownDeliveryMode: {
11358
+ type: "string",
11359
+ enum: ["passive", "proactive-table-only", "proactive-all"]
11360
+ },
10010
11361
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
10011
11362
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
10012
11363
  requireMention: { type: "boolean" },