@openclaw-china/qqbot 2026.3.11 → 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';
@@ -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() {
@@ -4876,7 +4876,7 @@ function extractMediaFromText(text, options = {}) {
4876
4876
  const {
4877
4877
  removeFromText = true,
4878
4878
  checkExists = false,
4879
- existsSync: existsSync6,
4879
+ existsSync: existsSync7,
4880
4880
  parseMediaLines = false,
4881
4881
  parseMarkdownImages = true,
4882
4882
  parseHtmlImages = true,
@@ -4891,7 +4891,7 @@ function extractMediaFromText(text, options = {}) {
4891
4891
  const key = media.localPath || media.source;
4892
4892
  if (seenSources.has(key)) return false;
4893
4893
  if (checkExists && media.isLocal && media.localPath) {
4894
- const exists = existsSync6 ? existsSync6(media.localPath) : fs3.existsSync(media.localPath);
4894
+ const exists = existsSync7 ? existsSync7(media.localPath) : fs3.existsSync(media.localPath);
4895
4895
  if (!exists) return false;
4896
4896
  }
4897
4897
  seenSources.add(key);
@@ -7380,8 +7380,8 @@ async function getAccessToken(appId, clientSecret, options) {
7380
7380
  tokenPromiseMap.set(normalizedAppId, promise);
7381
7381
  return promise;
7382
7382
  }
7383
- async function apiGet(accessToken, path4, options) {
7384
- const url = `${API_BASE}${path4}`;
7383
+ async function apiGet(accessToken, path5, options) {
7384
+ const url = `${API_BASE}${path5}`;
7385
7385
  return httpGet(url, {
7386
7386
  ...options,
7387
7387
  headers: {
@@ -7390,8 +7390,8 @@ async function apiGet(accessToken, path4, options) {
7390
7390
  }
7391
7391
  });
7392
7392
  }
7393
- async function apiPost(accessToken, path4, body, options) {
7394
- const url = `${API_BASE}${path4}`;
7393
+ async function apiPost(accessToken, path5, body, options) {
7394
+ const url = `${API_BASE}${path5}`;
7395
7395
  return httpPost(url, body, {
7396
7396
  ...options,
7397
7397
  headers: {
@@ -7490,7 +7490,7 @@ async function sendChannelMessage(params) {
7490
7490
  });
7491
7491
  }
7492
7492
  async function sendC2CInputNotify(params) {
7493
- await postPassiveMessage({
7493
+ const response = await postPassiveMessage({
7494
7494
  accessToken: params.accessToken,
7495
7495
  path: `/v2/users/${params.openid}/messages`,
7496
7496
  sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
@@ -7505,6 +7505,8 @@ async function sendC2CInputNotify(params) {
7505
7505
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7506
7506
  })
7507
7507
  });
7508
+ const refIdx = response.ext_info?.ref_idx?.trim();
7509
+ return refIdx ? { refIdx } : {};
7508
7510
  }
7509
7511
  async function uploadC2CMedia(params) {
7510
7512
  const body = {
@@ -7574,6 +7576,233 @@ async function sendGroupMediaMessage(params) {
7574
7576
  })
7575
7577
  });
7576
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]";
7805
+ }
7577
7806
  var require2 = createRequire(import.meta.url);
7578
7807
  function resolveQQBotMediaFileType(fileName) {
7579
7808
  const mediaType = detectMediaType(fileName);
@@ -7729,7 +7958,7 @@ async function sendFileQQBot(params) {
7729
7958
  }
7730
7959
  }
7731
7960
  try {
7732
- return await sendC2CMediaMessage({
7961
+ const result = await sendC2CMediaMessage({
7733
7962
  accessToken,
7734
7963
  openid: target.id,
7735
7964
  fileInfo,
@@ -7737,6 +7966,12 @@ async function sendFileQQBot(params) {
7737
7966
  ...messageId ? { messageId } : {},
7738
7967
  ...eventId ? { eventId } : {}
7739
7968
  });
7969
+ const refIdx = result.ext_info?.ref_idx?.trim();
7970
+ return {
7971
+ id: result.id,
7972
+ timestamp: result.timestamp,
7973
+ ...refIdx ? { refIdx } : {}
7974
+ };
7740
7975
  } catch (err) {
7741
7976
  const message = formatQQBotError(err);
7742
7977
  throw new Error(`QQBot C2C media send failed: ${message}`);
@@ -7853,6 +8088,85 @@ function shouldRetryWithEventId(err) {
7853
8088
  function shouldSendTextAsFollowupForMedia(mediaUrl) {
7854
8089
  return detectMediaType(stripTitleFromUrl(mediaUrl)) === "file";
7855
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
+ }
7856
8170
  function buildPassiveReplyRefs(params) {
7857
8171
  if (params.replyToId) {
7858
8172
  return { messageId: params.replyToId };
@@ -7998,7 +8312,14 @@ var qqbotOutbound = {
7998
8312
  content: text,
7999
8313
  markdown
8000
8314
  });
8001
- return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
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
+ };
8002
8323
  }
8003
8324
  let result;
8004
8325
  try {
@@ -8065,7 +8386,14 @@ var qqbotOutbound = {
8065
8386
  throw retryErr;
8066
8387
  }
8067
8388
  }
8068
- 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
+ };
8069
8397
  } catch (err) {
8070
8398
  const message = summarizeError(err);
8071
8399
  return { channel: "qqbot", error: message };
@@ -8174,7 +8502,21 @@ ${mediaUrl}` : mediaUrl;
8174
8502
  };
8175
8503
  }
8176
8504
  }
8177
- 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
+ };
8178
8520
  } catch (err) {
8179
8521
  const message = summarizeError(err);
8180
8522
  return { channel: "qqbot", error: message };
@@ -8193,8 +8535,9 @@ ${mediaUrl}` : mediaUrl;
8193
8535
  }
8194
8536
  try {
8195
8537
  const accessToken = await getAccessToken(credentials.appId, credentials.clientSecret);
8538
+ let typingResult;
8196
8539
  try {
8197
- await sendC2CInputNotify({
8540
+ typingResult = await sendC2CInputNotify({
8198
8541
  accessToken,
8199
8542
  openid: target.id,
8200
8543
  messageId: replyToId,
@@ -8216,7 +8559,7 @@ ${mediaUrl}` : mediaUrl;
8216
8559
  reason: summarizeError(err)
8217
8560
  });
8218
8561
  try {
8219
- await sendC2CInputNotify({
8562
+ typingResult = await sendC2CInputNotify({
8220
8563
  accessToken,
8221
8564
  openid: target.id,
8222
8565
  eventId: replyEventId,
@@ -8245,7 +8588,10 @@ ${mediaUrl}` : mediaUrl;
8245
8588
  throw retryErr;
8246
8589
  }
8247
8590
  }
8248
- return { channel: "qqbot" };
8591
+ return {
8592
+ channel: "qqbot",
8593
+ ...typingResult?.refIdx ? { refIdx: typingResult.refIdx } : {}
8594
+ };
8249
8595
  } catch (err) {
8250
8596
  const message = summarizeError(err);
8251
8597
  return { channel: "qqbot", error: message };
@@ -8394,13 +8740,13 @@ async function replaceAsync(input2, pattern, replacer) {
8394
8740
  return result;
8395
8741
  }
8396
8742
  async function getResolvedImageSize(params) {
8397
- const { url, cache, resolveImageSize } = params;
8398
- const existing = cache.get(url);
8743
+ const { url, cache: cache2, resolveImageSize } = params;
8744
+ const existing = cache2.get(url);
8399
8745
  if (existing) {
8400
8746
  return existing;
8401
8747
  }
8402
8748
  const pending = resolveImageSize(url).then((size) => normalizeImageSize(size) ?? DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE).catch(() => DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE);
8403
- cache.set(url, pending);
8749
+ cache2.set(url, pending);
8404
8750
  return pending;
8405
8751
  }
8406
8752
  async function normalizeTextSegment(params) {
@@ -8527,13 +8873,29 @@ async function normalizeQQBotMarkdownImages(params) {
8527
8873
 
8528
8874
  ${appendedImages.join("\n")}`;
8529
8875
  }
8530
- var DEFAULT_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "data", "qqbot", "known-targets.json");
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");
8531
8878
  function resolveKnownTargetsFilePath(options) {
8532
8879
  return options?.filePath?.trim() || DEFAULT_KNOWN_TARGETS_PATH;
8533
8880
  }
8534
8881
  function ensureKnownTargetsDir(filePath) {
8535
8882
  mkdirSync(dirname(filePath), { recursive: true });
8536
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
+ }
8537
8899
  function compareTargetsByLastSeenDesc(a, b) {
8538
8900
  if (b.lastSeenAt !== a.lastSeenAt) {
8539
8901
  return b.lastSeenAt - a.lastSeenAt;
@@ -8569,6 +8931,7 @@ function parseKnownTargets(raw, filePath) {
8569
8931
  }
8570
8932
  function readKnownTargets(options) {
8571
8933
  const filePath = resolveKnownTargetsFilePath(options);
8934
+ migrateLegacyKnownTargets(filePath);
8572
8935
  if (!existsSync(filePath)) {
8573
8936
  return [];
8574
8937
  }
@@ -8580,6 +8943,7 @@ function readKnownTargets(options) {
8580
8943
  }
8581
8944
  function writeKnownTargets(targets, options) {
8582
8945
  const filePath = resolveKnownTargetsFilePath(options);
8946
+ migrateLegacyKnownTargets(filePath);
8583
8947
  if (targets.length === 0) {
8584
8948
  if (existsSync(filePath)) {
8585
8949
  rmSync(filePath, { force: true });
@@ -8713,9 +9077,50 @@ function getQQBotRuntime() {
8713
9077
  return runtime;
8714
9078
  }
8715
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
+ }
8716
9087
  function buildSessionDispatchQueueKey(route) {
8717
9088
  const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
8718
- return `${accountId}:${route.sessionKey}`;
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;
8719
9124
  }
8720
9125
  async function runSerializedSessionDispatch(queueKey, task) {
8721
9126
  const previous = sessionDispatchQueue.get(queueKey) ?? Promise.resolve();
@@ -8780,6 +9185,33 @@ function parseTextWithAttachments(payload) {
8780
9185
  attachments
8781
9186
  };
8782
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
+ }
8783
9215
  function resolveEventId(payload, fallbackEventId) {
8784
9216
  return toString(payload.event_id) ?? toString(payload.eventId) ?? toString(fallbackEventId);
8785
9217
  }
@@ -8789,6 +9221,7 @@ var VOICE_ASR_ERROR_MAX_LENGTH = 500;
8789
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";
8790
9222
  var DEFAULT_LONG_TASK_NOTICE_DELAY_MS = 3e4;
8791
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";
8792
9225
  function startLongTaskNoticeTimer(params) {
8793
9226
  const { delayMs, logger, sendNotice } = params;
8794
9227
  let completed = false;
@@ -8823,7 +9256,7 @@ function startLongTaskNoticeTimer(params) {
8823
9256
  }
8824
9257
  };
8825
9258
  }
8826
- function isHttpUrl2(value) {
9259
+ function isHttpUrl3(value) {
8827
9260
  return /^https?:\/\//i.test(value);
8828
9261
  }
8829
9262
  function isImageAttachment(att) {
@@ -8896,7 +9329,7 @@ async function resolveInboundAttachmentsForAgent(params) {
8896
9329
  let asrErrorMessage;
8897
9330
  for (const att of list) {
8898
9331
  const next = { attachment: att };
8899
- if (isImageAttachment(att) && isHttpUrl2(att.url)) {
9332
+ if (isImageAttachment(att) && isHttpUrl3(att.url)) {
8900
9333
  try {
8901
9334
  const downloaded = await downloadToTempFile(att.url, {
8902
9335
  timeout,
@@ -8925,7 +9358,7 @@ async function resolveInboundAttachmentsForAgent(params) {
8925
9358
  logger.info("voice attachment received but ASR is disabled");
8926
9359
  } else if (!asrCredentials) {
8927
9360
  logger.warn("voice ASR enabled but credentials are missing or invalid");
8928
- } else if (!isHttpUrl2(att.url)) {
9361
+ } else if (!isHttpUrl3(att.url)) {
8929
9362
  logger.warn("voice ASR skipped: attachment URL is not an HTTP URL");
8930
9363
  } else {
8931
9364
  try {
@@ -8996,6 +9429,74 @@ function buildInboundContentWithAttachments(params) {
8996
9429
  parts.push(block);
8997
9430
  return parts.join("\n\n");
8998
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
+ }
8999
9500
  function resolveInboundLogContent(params) {
9000
9501
  const text = params.content.trim();
9001
9502
  if (text) return text;
@@ -9017,6 +9518,7 @@ function sanitizeInboundLogText(text) {
9017
9518
  function parseC2CMessage(data, fallbackEventId) {
9018
9519
  const payload = data;
9019
9520
  const { text, attachments } = parseTextWithAttachments(payload);
9521
+ const refIndices = parseQQBotRefIndices(payload);
9020
9522
  const id = toString(payload.id);
9021
9523
  const eventId = resolveEventId(payload, fallbackEventId);
9022
9524
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
@@ -9033,6 +9535,7 @@ function parseC2CMessage(data, fallbackEventId) {
9033
9535
  messageId: id,
9034
9536
  eventId,
9035
9537
  timestamp,
9538
+ ...refIndices,
9036
9539
  mentionedBot: false
9037
9540
  };
9038
9541
  }
@@ -9146,6 +9649,22 @@ function resolveChatTarget(event) {
9146
9649
  peerKind: "dm"
9147
9650
  };
9148
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
+ }
9149
9668
  function resolveEnvelopeFrom(event) {
9150
9669
  if (event.type === "group") {
9151
9670
  return `group:${(event.groupOpenid ?? "unknown").toLowerCase()}`;
@@ -9474,7 +9993,7 @@ function normalizeQQBotRenderedMarkdown(text) {
9474
9993
  return changed ? next.trim() : text.trim();
9475
9994
  }
9476
9995
  async function sendQQBotMediaWithFallback(params) {
9477
- const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger, onDelivered, onError } = params;
9996
+ const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
9478
9997
  const outbound = params.outbound ?? qqbotOutbound;
9479
9998
  for (const mediaUrl of mediaQueue) {
9480
9999
  const result = await outbound.sendMedia({
@@ -9482,7 +10001,8 @@ async function sendQQBotMediaWithFallback(params) {
9482
10001
  to,
9483
10002
  mediaUrl,
9484
10003
  replyToId,
9485
- replyEventId
10004
+ replyEventId,
10005
+ accountId
9486
10006
  });
9487
10007
  if (result.error) {
9488
10008
  logger.error(`sendMedia failed: ${result.error}`);
@@ -9496,7 +10016,8 @@ async function sendQQBotMediaWithFallback(params) {
9496
10016
  to,
9497
10017
  text: fallback,
9498
10018
  replyToId,
9499
- replyEventId
10019
+ replyEventId,
10020
+ accountId
9500
10021
  });
9501
10022
  if (fallbackResult.error) {
9502
10023
  logger.error(`sendText fallback failed: ${fallbackResult.error}`);
@@ -9541,17 +10062,23 @@ function buildInboundContext(params) {
9541
10062
  async function dispatchToAgent(params) {
9542
10063
  const { inbound, cfg, qqCfg, accountId, logger, route } = params;
9543
10064
  const runtime2 = getQQBotRuntime();
10065
+ const routeSessionKey = resolveQQBotRouteSessionKey(route);
9544
10066
  const target = resolveChatTarget(inbound);
10067
+ const outboundAccountId = route.accountId ?? accountId;
10068
+ let typingRefIdx;
9545
10069
  if (inbound.c2cOpenid) {
9546
10070
  const typing = await qqbotOutbound.sendTyping({
9547
10071
  cfg: { channels: { qqbot: qqCfg } },
9548
10072
  to: `user:${inbound.c2cOpenid}`,
9549
10073
  replyToId: inbound.messageId,
9550
10074
  replyEventId: inbound.eventId,
9551
- inputSecond: 60
10075
+ inputSecond: 60,
10076
+ accountId: outboundAccountId
9552
10077
  });
9553
10078
  if (typing.error) {
9554
10079
  logger.warn(`sendTyping failed: ${typing.error}`);
10080
+ } else {
10081
+ typingRefIdx = typing.refIdx;
9555
10082
  }
9556
10083
  }
9557
10084
  const replyApi = runtime2.channel?.reply;
@@ -9582,7 +10109,8 @@ async function dispatchToAgent(params) {
9582
10109
  to: target.to,
9583
10110
  text: LONG_TASK_NOTICE_TEXT,
9584
10111
  replyToId: inbound.messageId,
9585
- replyEventId: inbound.eventId
10112
+ replyEventId: inbound.eventId,
10113
+ accountId: outboundAccountId
9586
10114
  });
9587
10115
  if (result.error) {
9588
10116
  logger.warn(`send long-task notice failed: ${result.error}`);
@@ -9602,7 +10130,7 @@ async function dispatchToAgent(params) {
9602
10130
  { agentId: route.agentId }
9603
10131
  );
9604
10132
  const envelopeOptions = replyApi.resolveEnvelopeFormatOptions?.(cfg);
9605
- const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) : null;
10133
+ const previousTimestamp = storePath && sessionApi?.readSessionUpdatedAt ? sessionApi.readSessionUpdatedAt({ storePath, sessionKey: routeSessionKey }) : null;
9606
10134
  const resolvedAttachmentResult = await resolveInboundAttachmentsForAgent({
9607
10135
  attachments: inbound.attachments,
9608
10136
  qqCfg,
@@ -9614,7 +10142,8 @@ async function dispatchToAgent(params) {
9614
10142
  to: target.to,
9615
10143
  text: buildVoiceASRFallbackReply(resolvedAttachmentResult.asrErrorMessage),
9616
10144
  replyToId: inbound.messageId,
9617
- replyEventId: inbound.eventId
10145
+ replyEventId: inbound.eventId,
10146
+ accountId: outboundAccountId
9618
10147
  });
9619
10148
  if (fallback.error) {
9620
10149
  logger.error(`sendText ASR fallback failed: ${fallback.error}`);
@@ -9629,6 +10158,39 @@ async function dispatchToAgent(params) {
9629
10158
  if (localImageCount > 0) {
9630
10159
  logger.info(`prepared ${localImageCount} local image attachment(s) for agent`);
9631
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
+ }
9632
10194
  const rawBody = buildInboundContentWithAttachments({
9633
10195
  content: inbound.content,
9634
10196
  attachments: resolvedAttachments
@@ -9654,40 +10216,48 @@ async function dispatchToAgent(params) {
9654
10216
  }) : rawBody;
9655
10217
  const inboundCtx = buildInboundContext({
9656
10218
  event: inbound,
9657
- sessionKey: route.sessionKey,
9658
- accountId: route.accountId ?? accountId,
10219
+ sessionKey: routeSessionKey,
10220
+ accountId: outboundAccountId,
9659
10221
  body: inboundBody,
9660
10222
  rawBody,
9661
10223
  commandBody: rawBody
9662
10224
  });
9663
10225
  const finalizeInboundContext = replyApi?.finalizeInboundContext;
9664
10226
  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;
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
+ });
9677
10246
  }
10247
+ finalCtx.BodyForAgent = appendCronHiddenPrompt(agentBody);
9678
10248
  }
9679
10249
  if (storePath && sessionApi?.recordInboundSession) {
9680
10250
  try {
9681
- const mainSessionKeyRaw = route?.mainSessionKey;
9682
- 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;
9683
10253
  const isGroup = inbound.type === "group" || inbound.type === "channel";
9684
10254
  const updateLastRoute = !isGroup ? {
9685
10255
  sessionKey: mainSessionKey ?? route.sessionKey,
9686
10256
  channel: "qqbot",
9687
- to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${inbound.senderId}`,
9688
- accountId: route.accountId ?? accountId
10257
+ to: stableTo,
10258
+ accountId: outboundAccountId
9689
10259
  } : void 0;
9690
- 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;
9691
10261
  await sessionApi.recordInboundSession({
9692
10262
  storePath,
9693
10263
  sessionKey: recordSessionKey,
@@ -9711,7 +10281,7 @@ async function dispatchToAgent(params) {
9711
10281
  const tableMode = textApi?.resolveMarkdownTableMode?.({
9712
10282
  cfg,
9713
10283
  channel: "qqbot",
9714
- accountId: route.accountId ?? accountId
10284
+ accountId: outboundAccountId
9715
10285
  });
9716
10286
  const resolvedTableMode = tableMode ?? "bullets";
9717
10287
  const chunkText = (text) => {
@@ -9736,27 +10306,16 @@ async function dispatchToAgent(params) {
9736
10306
  bufferedC2CMarkdownMediaSeen.add(next);
9737
10307
  bufferedC2CMarkdownMediaUrls.push(next);
9738
10308
  };
9739
- const flushBufferedC2CMarkdownReply = async () => {
9740
- if (!useC2CMarkdownTransport || bufferedC2CMarkdownTexts.length === 0 && bufferedC2CMarkdownMediaUrls.length === 0) {
9741
- bufferedC2CMarkdownTexts = [];
9742
- bufferedC2CMarkdownMediaUrls = [];
9743
- bufferedC2CMarkdownMediaSeen.clear();
9744
- return;
9745
- }
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);
10309
+ const sendC2CMarkdownTransportPayload = async (params2) => {
10310
+ const normalizedText = normalizeQQBotRenderedMarkdown(params2.text);
10311
+ const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(params2.mediaUrls);
9753
10312
  const finalMarkdownText = await normalizeQQBotMarkdownImages({
9754
- text: normalizedCombinedText,
10313
+ text: normalizedText,
9755
10314
  appendImageUrls: markdownImageUrls
9756
10315
  });
9757
10316
  const textReplyRefs = resolveQQBotTextReplyRefs({
9758
10317
  to: target.to,
9759
- text: finalMarkdownText || normalizedCombinedText,
10318
+ text: finalMarkdownText || normalizedText,
9760
10319
  markdownSupport,
9761
10320
  c2cMarkdownDeliveryMode,
9762
10321
  replyToId: inbound.messageId,
@@ -9765,7 +10324,7 @@ async function dispatchToAgent(params) {
9765
10324
  const textSegments = finalMarkdownText ? [finalMarkdownText] : [];
9766
10325
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
9767
10326
  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")}`
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")}`
9769
10328
  );
9770
10329
  await sendQQBotMediaWithFallback({
9771
10330
  qqCfg,
@@ -9773,6 +10332,7 @@ async function dispatchToAgent(params) {
9773
10332
  mediaQueue,
9774
10333
  replyToId: textReplyRefs.replyToId,
9775
10334
  replyEventId: textReplyRefs.replyEventId,
10335
+ accountId: outboundAccountId,
9776
10336
  logger,
9777
10337
  onDelivered: () => {
9778
10338
  markReplyDelivered();
@@ -9790,25 +10350,44 @@ async function dispatchToAgent(params) {
9790
10350
  for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
9791
10351
  const chunk = chunks[chunkIndex] ?? "";
9792
10352
  logger.info(
9793
- `delivery=${deliveryLabel} segment=${segmentIndex + 1}/${textSegments.length} chunk=${chunkIndex + 1}/${chunks.length} preview=${formatQQBotOutboundPreview(chunk)}`
10353
+ `delivery=${deliveryLabel} segment=${segmentIndex + 1}/${textSegments.length} chunk=${chunkIndex + 1}/${chunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
9794
10354
  );
9795
10355
  const result = await qqbotOutbound.sendText({
9796
10356
  cfg: { channels: { qqbot: qqCfg } },
9797
10357
  to: target.to,
9798
10358
  text: chunk,
9799
10359
  replyToId: textReplyRefs.replyToId,
9800
- replyEventId: textReplyRefs.replyEventId
10360
+ replyEventId: textReplyRefs.replyEventId,
10361
+ accountId: outboundAccountId
9801
10362
  });
9802
10363
  if (result.error) {
9803
- logger.error(`send buffered QQ markdown reply failed: ${result.error}`);
10364
+ logger.error(`send QQ markdown reply failed: ${result.error}`);
9804
10365
  markGroupMessageInterfaceBlocked(result.error);
9805
10366
  } else {
9806
- logger.info(`sent buffered QQ markdown reply (len=${chunk.length})`);
10367
+ logger.info(`sent QQ markdown reply (phase=${params2.phase}, len=${chunk.length})`);
9807
10368
  markReplyDelivered();
9808
10369
  }
9809
10370
  }
9810
10371
  }
9811
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
+ };
9812
10391
  const deliver = async (payload, info) => {
9813
10392
  const typed = payload;
9814
10393
  const extractedTextMedia = extractQQBotReplyMedia({
@@ -9840,12 +10419,21 @@ async function dispatchToAgent(params) {
9840
10419
  const suppressText = deliveryDecision.suppressText || suppressEchoText;
9841
10420
  const textToSend = suppressText ? "" : cleanedText;
9842
10421
  if (useC2CMarkdownTransport) {
9843
- if (textToSend) {
9844
- bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
9845
- }
9846
- for (const url of mediaQueue) {
9847
- bufferC2CMarkdownMedia(url);
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;
9848
10431
  }
10432
+ await sendC2CMarkdownTransportPayload({
10433
+ text: textToSend,
10434
+ mediaUrls: mediaQueue,
10435
+ phase: "immediate"
10436
+ });
9849
10437
  return;
9850
10438
  }
9851
10439
  if (textToSend) {
@@ -9865,7 +10453,8 @@ async function dispatchToAgent(params) {
9865
10453
  to: target.to,
9866
10454
  text: chunk,
9867
10455
  replyToId: textReplyRefs.replyToId,
9868
- replyEventId: textReplyRefs.replyEventId
10456
+ replyEventId: textReplyRefs.replyEventId,
10457
+ accountId: outboundAccountId
9869
10458
  });
9870
10459
  if (result.error) {
9871
10460
  logger.error(`sendText failed: ${result.error}`);
@@ -9881,6 +10470,7 @@ async function dispatchToAgent(params) {
9881
10470
  mediaQueue,
9882
10471
  replyToId: inbound.messageId,
9883
10472
  replyEventId: inbound.eventId,
10473
+ accountId: outboundAccountId,
9884
10474
  logger,
9885
10475
  onDelivered: () => {
9886
10476
  markReplyDelivered();
@@ -9952,7 +10542,8 @@ async function dispatchToAgent(params) {
9952
10542
  to: target.to,
9953
10543
  text: noReplyFallback,
9954
10544
  replyToId: inbound.messageId,
9955
- replyEventId: inbound.eventId
10545
+ replyEventId: inbound.eventId,
10546
+ accountId: outboundAccountId
9956
10547
  });
9957
10548
  if (fallbackResult.error) {
9958
10549
  logger.error(`sendText no-reply fallback failed: ${fallbackResult.error}`);
@@ -10057,9 +10648,19 @@ async function handleQQBotDispatch(params) {
10057
10648
  accountId,
10058
10649
  peer: { kind: target.peerKind, id: target.peerId }
10059
10650
  });
10060
- const queueKey = buildSessionDispatchQueueKey(route);
10651
+ const effectiveSessionKey = resolveQQBotEffectiveSessionKey({
10652
+ inbound,
10653
+ route,
10654
+ accountId
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);
10061
10662
  if (sessionDispatchQueue.has(queueKey)) {
10062
- logger.info(`session busy; queueing inbound dispatch sessionKey=${route.sessionKey}`);
10663
+ logger.info(`session busy; queueing inbound dispatch sessionKey=${resolveQQBotRouteSessionKey(resolvedRoute)}`);
10063
10664
  }
10064
10665
  await runSerializedSessionDispatch(
10065
10666
  queueKey,
@@ -10069,7 +10670,7 @@ async function handleQQBotDispatch(params) {
10069
10670
  qqCfg,
10070
10671
  accountId,
10071
10672
  logger,
10072
- route
10673
+ route: resolvedRoute
10073
10674
  })
10074
10675
  );
10075
10676
  }
@@ -10381,7 +10982,7 @@ var qqbotPlugin = {
10381
10982
  ...meta
10382
10983
  },
10383
10984
  capabilities: {
10384
- chatTypes: ["direct", "channel"],
10985
+ chatTypes: ["direct", "group", "channel"],
10385
10986
  media: true,
10386
10987
  reactions: false,
10387
10988
  threads: false,