@openclaw-china/qqbot 2026.3.12 → 2026.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4223,12 +4223,17 @@ var optionalCoercedString = external_exports.preprocess(
4223
4223
  (value) => toTrimmedString(value),
4224
4224
  external_exports.string().min(1).optional()
4225
4225
  );
4226
+ var displayAliasesSchema = external_exports.record(
4227
+ external_exports.preprocess((value) => toTrimmedString(value), external_exports.string().min(1))
4228
+ ).optional();
4226
4229
  var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
4230
+ var QQBotC2CMarkdownChunkStrategySchema = external_exports.enum(["markdown-block", "length"]).optional().default("markdown-block");
4227
4231
  var QQBotAccountSchema = external_exports.object({
4228
4232
  name: external_exports.string().optional(),
4229
4233
  enabled: external_exports.boolean().optional(),
4230
4234
  appId: optionalCoercedString,
4231
4235
  clientSecret: optionalCoercedString,
4236
+ displayAliases: displayAliasesSchema,
4232
4237
  asr: external_exports.object({
4233
4238
  enabled: external_exports.boolean().optional().default(false),
4234
4239
  appId: optionalCoercedString,
@@ -4237,6 +4242,7 @@ var QQBotAccountSchema = external_exports.object({
4237
4242
  }).optional(),
4238
4243
  markdownSupport: external_exports.boolean().optional().default(true),
4239
4244
  c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4245
+ c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
4240
4246
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4241
4247
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4242
4248
  requireMention: external_exports.boolean().optional().default(true),
@@ -4261,6 +4267,21 @@ QQBotAccountSchema.extend({
4261
4267
  var DEFAULT_INBOUND_MEDIA_DIR = join(homedir(), ".openclaw", "media", "qqbot", "inbound");
4262
4268
  var DEFAULT_INBOUND_MEDIA_KEEP_DAYS = 7;
4263
4269
  var DEFAULT_INBOUND_MEDIA_TEMP_DIR = join(tmpdir(), "qqbot-media");
4270
+ function normalizeDisplayAliasesMap(raw) {
4271
+ if (!raw || typeof raw !== "object") {
4272
+ return {};
4273
+ }
4274
+ const aliases = {};
4275
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
4276
+ const key = rawKey.trim();
4277
+ const value = toTrimmedString(rawValue);
4278
+ if (!key || !value) {
4279
+ continue;
4280
+ }
4281
+ aliases[key] = value;
4282
+ }
4283
+ return aliases;
4284
+ }
4264
4285
  function resolveInboundMediaDir(config) {
4265
4286
  return String(config?.inboundMedia?.dir ?? "").trim() || DEFAULT_INBOUND_MEDIA_DIR;
4266
4287
  }
@@ -4301,7 +4322,15 @@ function mergeQQBotAccountConfig(cfg, accountId) {
4301
4322
  const base = cfg.channels?.qqbot ?? {};
4302
4323
  const { accounts: _ignored, defaultAccount: _ignored2, ...baseConfig } = base;
4303
4324
  const account = resolveAccountConfig(cfg, accountId) ?? {};
4304
- return { ...baseConfig, ...account };
4325
+ const mergedDisplayAliases = {
4326
+ ...normalizeDisplayAliasesMap(baseConfig.displayAliases),
4327
+ ...normalizeDisplayAliasesMap(account.displayAliases)
4328
+ };
4329
+ return {
4330
+ ...baseConfig,
4331
+ ...account,
4332
+ ...Object.keys(mergedDisplayAliases).length > 0 ? { displayAliases: mergedDisplayAliases } : {}
4333
+ };
4305
4334
  }
4306
4335
  function resolveQQBotCredentials(config) {
4307
4336
  const appId = toTrimmedString(config?.appId);
@@ -6518,6 +6547,7 @@ var CHANNEL_ORDER = [
6518
6547
  "qqbot",
6519
6548
  "wecom",
6520
6549
  "wecom-app",
6550
+ "wecom-kf",
6521
6551
  "feishu-china"
6522
6552
  ];
6523
6553
  var CHANNEL_DISPLAY_LABELS = {
@@ -6525,6 +6555,7 @@ var CHANNEL_DISPLAY_LABELS = {
6525
6555
  "feishu-china": "Feishu\uFF08\u98DE\u4E66\uFF09",
6526
6556
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
6527
6557
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
6558
+ "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
6528
6559
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
6529
6560
  };
6530
6561
  var CHANNEL_GUIDE_LINKS = {
@@ -6532,6 +6563,7 @@ var CHANNEL_GUIDE_LINKS = {
6532
6563
  "feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
6533
6564
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
6534
6565
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
6566
+ "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
6535
6567
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
6536
6568
  };
6537
6569
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -6740,6 +6772,8 @@ function isChannelConfigured(cfg, channelId) {
6740
6772
  return hasWecomWsCredentialPair(channelCfg);
6741
6773
  case "wecom-app":
6742
6774
  return hasTokenPair(channelCfg);
6775
+ case "wecom-kf":
6776
+ return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6743
6777
  default:
6744
6778
  return false;
6745
6779
  }
@@ -6996,6 +7030,55 @@ async function configureWecomApp(prompter, cfg) {
6996
7030
  patch.asr = asr;
6997
7031
  return mergeChannelConfig(cfg, "wecom-app", patch);
6998
7032
  }
7033
+ async function configureWecomKf(prompter, cfg) {
7034
+ section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
7035
+ showGuideLink("wecom-kf");
7036
+ const existing = getChannelConfig(cfg, "wecom-kf");
7037
+ const webhookPath = await prompter.askText({
7038
+ label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
7039
+ defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
7040
+ required: true
7041
+ });
7042
+ const token = await prompter.askSecret({
7043
+ label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 Token",
7044
+ existingValue: toTrimmedString2(existing.token),
7045
+ required: true
7046
+ });
7047
+ const encodingAESKey = await prompter.askSecret({
7048
+ label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 EncodingAESKey",
7049
+ existingValue: toTrimmedString2(existing.encodingAESKey),
7050
+ required: true
7051
+ });
7052
+ const corpId = await prompter.askText({
7053
+ label: "corpId",
7054
+ defaultValue: toTrimmedString2(existing.corpId),
7055
+ required: true
7056
+ });
7057
+ const corpSecret = await prompter.askSecret({
7058
+ label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
7059
+ existingValue: toTrimmedString2(existing.corpSecret),
7060
+ required: true
7061
+ });
7062
+ const openKfId = await prompter.askText({
7063
+ label: "open_kfid",
7064
+ defaultValue: toTrimmedString2(existing.openKfId),
7065
+ required: true
7066
+ });
7067
+ const welcomeText = await prompter.askText({
7068
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7069
+ defaultValue: toTrimmedString2(existing.welcomeText),
7070
+ required: false
7071
+ });
7072
+ return mergeChannelConfig(cfg, "wecom-kf", {
7073
+ webhookPath,
7074
+ token,
7075
+ encodingAESKey,
7076
+ corpId,
7077
+ corpSecret,
7078
+ openKfId,
7079
+ welcomeText: welcomeText || void 0
7080
+ });
7081
+ }
6999
7082
  async function configureQQBot(prompter, cfg) {
7000
7083
  section("\u914D\u7F6E QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09");
7001
7084
  showGuideLink("qqbot");
@@ -7052,6 +7135,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
7052
7135
  return configureWecom(prompter, cfg);
7053
7136
  case "wecom-app":
7054
7137
  return configureWecomApp(prompter, cfg);
7138
+ case "wecom-kf":
7139
+ return configureWecomKf(prompter, cfg);
7055
7140
  case "qqbot":
7056
7141
  return configureQQBot(prompter, cfg);
7057
7142
  default:
@@ -7192,6 +7277,7 @@ var SUPPORTED_CHANNELS = [
7192
7277
  "feishu-china",
7193
7278
  "wecom",
7194
7279
  "wecom-app",
7280
+ "wecom-kf",
7195
7281
  "qqbot"
7196
7282
  ];
7197
7283
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -8015,21 +8101,22 @@ async function readMediaWithConfig(source, options) {
8015
8101
 
8016
8102
  // src/outbound.ts
8017
8103
  function stripPrefix(value, prefix) {
8018
- return value.startsWith(prefix) ? value.slice(prefix.length) : value;
8104
+ return value.slice(0, prefix.length).toLowerCase() === prefix ? value.slice(prefix.length) : value;
8019
8105
  }
8020
8106
  function parseTarget(to) {
8021
8107
  let raw = to.trim();
8022
8108
  raw = stripPrefix(raw, "qqbot:");
8023
- if (raw.startsWith("group:")) {
8109
+ const normalizedRaw = raw.toLowerCase();
8110
+ if (normalizedRaw.startsWith("group:")) {
8024
8111
  return { kind: "group", id: raw.slice("group:".length) };
8025
8112
  }
8026
- if (raw.startsWith("channel:")) {
8113
+ if (normalizedRaw.startsWith("channel:")) {
8027
8114
  return { kind: "channel", id: raw.slice("channel:".length) };
8028
8115
  }
8029
- if (raw.startsWith("user:")) {
8116
+ if (normalizedRaw.startsWith("user:")) {
8030
8117
  return { kind: "c2c", id: raw.slice("user:".length) };
8031
8118
  }
8032
- if (raw.startsWith("c2c:")) {
8119
+ if (normalizedRaw.startsWith("c2c:")) {
8033
8120
  return { kind: "c2c", id: raw.slice("c2c:".length) };
8034
8121
  }
8035
8122
  return { kind: "c2c", id: raw };
@@ -9077,6 +9164,51 @@ function getQQBotRuntime() {
9077
9164
  return runtime;
9078
9165
  }
9079
9166
  var sessionDispatchQueue = /* @__PURE__ */ new Map();
9167
+ var QQBOT_ABORT_TRIGGERS = /* @__PURE__ */ new Set([
9168
+ "stop",
9169
+ "esc",
9170
+ "abort",
9171
+ "wait",
9172
+ "exit",
9173
+ "interrupt",
9174
+ "detente",
9175
+ "deten",
9176
+ "det\xE9n",
9177
+ "arrete",
9178
+ "arr\xEAte",
9179
+ "\u505C\u6B62",
9180
+ "\u3084\u3081\u3066",
9181
+ "\u6B62\u3081\u3066",
9182
+ "\u0930\u0941\u0915\u094B",
9183
+ "\u062A\u0648\u0642\u0641",
9184
+ "\u0441\u0442\u043E\u043F",
9185
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C",
9186
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438",
9187
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C",
9188
+ "\u043F\u0440\u0435\u043A\u0440\u0430\u0442\u0438",
9189
+ "halt",
9190
+ "anhalten",
9191
+ "aufh\xF6ren",
9192
+ "hoer auf",
9193
+ "stopp",
9194
+ "pare",
9195
+ "stop openclaw",
9196
+ "openclaw stop",
9197
+ "stop action",
9198
+ "stop current action",
9199
+ "stop run",
9200
+ "stop current run",
9201
+ "stop agent",
9202
+ "stop the agent",
9203
+ "stop don't do anything",
9204
+ "stop dont do anything",
9205
+ "stop do not do anything",
9206
+ "stop doing anything",
9207
+ "do not do that",
9208
+ "please stop",
9209
+ "stop please"
9210
+ ]);
9211
+ var QQBOT_ABORT_TRAILING_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
9080
9212
  function resolveQQBotRouteSessionKey(route) {
9081
9213
  const effectiveSessionKey = route.effectiveSessionKey?.trim();
9082
9214
  if (effectiveSessionKey) {
@@ -9088,6 +9220,100 @@ function buildSessionDispatchQueueKey(route) {
9088
9220
  const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
9089
9221
  return `${accountId}:${resolveQQBotRouteSessionKey(route)}`;
9090
9222
  }
9223
+ function createSessionDispatchState() {
9224
+ return {
9225
+ queue: [],
9226
+ processing: false,
9227
+ immediateActiveCount: 0,
9228
+ waiters: [],
9229
+ abortGeneration: 0
9230
+ };
9231
+ }
9232
+ function getSessionDispatchState(queueKey) {
9233
+ const existing = sessionDispatchQueue.get(queueKey);
9234
+ if (existing) {
9235
+ return existing;
9236
+ }
9237
+ const created = createSessionDispatchState();
9238
+ sessionDispatchQueue.set(queueKey, created);
9239
+ return created;
9240
+ }
9241
+ function signalSessionDispatchState(state) {
9242
+ const waiters = state.waiters.splice(0, state.waiters.length);
9243
+ for (const resolve3 of waiters) {
9244
+ resolve3();
9245
+ }
9246
+ }
9247
+ function waitForSessionDispatchState(state) {
9248
+ return new Promise((resolve3) => {
9249
+ state.waiters.push(resolve3);
9250
+ });
9251
+ }
9252
+ function cleanupSessionDispatchState(queueKey, state) {
9253
+ if (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0 || state.waiters.length > 0) {
9254
+ return;
9255
+ }
9256
+ if (sessionDispatchQueue.get(queueKey) === state) {
9257
+ sessionDispatchQueue.delete(queueKey);
9258
+ }
9259
+ }
9260
+ function hasSessionDispatchBacklog(queueKey) {
9261
+ const state = sessionDispatchQueue.get(queueKey);
9262
+ return Boolean(
9263
+ state && (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0)
9264
+ );
9265
+ }
9266
+ async function processSerializedSessionDispatchQueue(queueKey) {
9267
+ const state = sessionDispatchQueue.get(queueKey);
9268
+ if (!state || state.processing) {
9269
+ return;
9270
+ }
9271
+ state.processing = true;
9272
+ try {
9273
+ for (; ; ) {
9274
+ if (state.immediateActiveCount > 0) {
9275
+ await waitForSessionDispatchState(state);
9276
+ continue;
9277
+ }
9278
+ const next = state.queue.shift();
9279
+ if (!next) {
9280
+ break;
9281
+ }
9282
+ try {
9283
+ await next.task();
9284
+ next.resolve();
9285
+ } catch (err) {
9286
+ next.reject(err);
9287
+ }
9288
+ }
9289
+ } finally {
9290
+ state.processing = false;
9291
+ if (state.queue.length > 0) {
9292
+ void processSerializedSessionDispatchQueue(queueKey);
9293
+ return;
9294
+ }
9295
+ cleanupSessionDispatchState(queueKey, state);
9296
+ }
9297
+ }
9298
+ function dropQueuedSessionDispatches(queueKey) {
9299
+ const state = sessionDispatchQueue.get(queueKey);
9300
+ if (!state || state.queue.length === 0) {
9301
+ return 0;
9302
+ }
9303
+ const dropped = state.queue.splice(0, state.queue.length);
9304
+ for (const item of dropped) {
9305
+ item.resolve();
9306
+ }
9307
+ signalSessionDispatchState(state);
9308
+ cleanupSessionDispatchState(queueKey, state);
9309
+ return dropped.length;
9310
+ }
9311
+ function markSessionDispatchAbort(queueKey) {
9312
+ const state = getSessionDispatchState(queueKey);
9313
+ state.abortGeneration += 1;
9314
+ signalSessionDispatchState(state);
9315
+ return state.abortGeneration;
9316
+ }
9091
9317
  function normalizeQQBotSessionKeyPart(value) {
9092
9318
  const trimmed = value.trim();
9093
9319
  return trimmed ? trimmed.toLowerCase() : "unknown";
@@ -9123,22 +9349,163 @@ function normalizeQQBotReplyTarget(value) {
9123
9349
  return /^(user|group|channel):/i.test(trimmed) ? trimmed : void 0;
9124
9350
  }
9125
9351
  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);
9352
+ const state = getSessionDispatchState(queueKey);
9353
+ return new Promise((resolve3, reject) => {
9354
+ let settled = false;
9355
+ state.queue.push({
9356
+ task: async () => {
9357
+ if (settled) {
9358
+ return;
9359
+ }
9360
+ try {
9361
+ const result = await task();
9362
+ if (!settled) {
9363
+ settled = true;
9364
+ resolve3(result);
9365
+ }
9366
+ } catch (err) {
9367
+ if (!settled) {
9368
+ settled = true;
9369
+ reject(err);
9370
+ }
9371
+ }
9372
+ },
9373
+ resolve: () => {
9374
+ if (settled) {
9375
+ return;
9376
+ }
9377
+ settled = true;
9378
+ resolve3(void 0);
9379
+ },
9380
+ reject: (err) => {
9381
+ if (settled) {
9382
+ return;
9383
+ }
9384
+ settled = true;
9385
+ reject(err);
9386
+ }
9387
+ });
9388
+ signalSessionDispatchState(state);
9389
+ void processSerializedSessionDispatchQueue(queueKey);
9390
+ });
9391
+ }
9392
+ async function runImmediateSessionDispatch(queueKey, task) {
9393
+ const state = getSessionDispatchState(queueKey);
9394
+ state.immediateActiveCount += 1;
9395
+ signalSessionDispatchState(state);
9130
9396
  try {
9131
- return await run;
9397
+ return await task();
9132
9398
  } finally {
9133
- if (sessionDispatchQueue.get(queueKey) === cleanup) {
9134
- sessionDispatchQueue.delete(queueKey);
9399
+ state.immediateActiveCount = Math.max(0, state.immediateActiveCount - 1);
9400
+ signalSessionDispatchState(state);
9401
+ if (state.queue.length > 0) {
9402
+ void processSerializedSessionDispatchQueue(queueKey);
9135
9403
  }
9404
+ cleanupSessionDispatchState(queueKey, state);
9136
9405
  }
9137
9406
  }
9407
+ function normalizeQQBotAbortTriggerText(text) {
9408
+ return text.trim().toLowerCase().replace(/[’`]/g, "'").replace(/\s+/g, " ").replace(QQBOT_ABORT_TRAILING_PUNCTUATION_RE, "").trim();
9409
+ }
9410
+ function isQQBotAbortTrigger(text) {
9411
+ if (!text) {
9412
+ return false;
9413
+ }
9414
+ return QQBOT_ABORT_TRIGGERS.has(normalizeQQBotAbortTriggerText(text));
9415
+ }
9416
+ function isQQBotFastAbortCommandText(text) {
9417
+ if (!text) {
9418
+ return false;
9419
+ }
9420
+ const normalized = text.trim();
9421
+ if (!normalized) {
9422
+ return false;
9423
+ }
9424
+ const lower = normalized.toLowerCase();
9425
+ return lower === "/stop" || normalizeQQBotAbortTriggerText(lower) === "/stop" || isQQBotAbortTrigger(lower);
9426
+ }
9138
9427
  function toString(value) {
9139
9428
  if (typeof value === "string" && value.trim()) return value;
9140
9429
  return void 0;
9141
9430
  }
9431
+ function asRecord(value) {
9432
+ if (!value || typeof value !== "object") {
9433
+ return void 0;
9434
+ }
9435
+ return value;
9436
+ }
9437
+ function normalizeQQBotDisplayAliasesMap(raw) {
9438
+ if (!raw || typeof raw !== "object") {
9439
+ return {};
9440
+ }
9441
+ const aliases = {};
9442
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
9443
+ const key = rawKey.trim();
9444
+ const value = toString(rawValue);
9445
+ if (!key || !value) {
9446
+ continue;
9447
+ }
9448
+ aliases[key] = value;
9449
+ }
9450
+ return aliases;
9451
+ }
9452
+ function resolveQQBotDisplayAliasMaps(cfg, accountId) {
9453
+ const qqbot = cfg?.channels?.qqbot;
9454
+ return {
9455
+ globalAliases: normalizeQQBotDisplayAliasesMap(qqbot?.displayAliases),
9456
+ accountAliases: normalizeQQBotDisplayAliasesMap(qqbot?.accounts?.[accountId]?.displayAliases)
9457
+ };
9458
+ }
9459
+ function resolveQQBotSenderName(params) {
9460
+ const { inbound, cfg, accountId } = params;
9461
+ const stableId = inbound.c2cOpenid?.trim() || inbound.senderId.trim();
9462
+ const { globalAliases, accountAliases } = resolveQQBotDisplayAliasMaps(cfg, accountId);
9463
+ if (inbound.type === "direct") {
9464
+ const knownTarget = stableId ? getKnownQQBotTarget({ accountId, target: `user:${stableId}` }) : void 0;
9465
+ const knownTargetDisplayName = knownTarget?.displayName?.trim();
9466
+ if (knownTargetDisplayName) {
9467
+ return {
9468
+ displayName: knownTargetDisplayName,
9469
+ persistentDisplayName: knownTargetDisplayName,
9470
+ source: "known-target",
9471
+ knownTargetDisplayName
9472
+ };
9473
+ }
9474
+ const aliasKeys = [...new Set([`user:${stableId}`, stableId, inbound.senderId.trim()].filter(Boolean))];
9475
+ for (const aliasKey of aliasKeys) {
9476
+ const alias = accountAliases[aliasKey];
9477
+ if (alias) {
9478
+ return {
9479
+ displayName: alias,
9480
+ persistentDisplayName: alias,
9481
+ source: "account-alias",
9482
+ matchedAliasKey: aliasKey
9483
+ };
9484
+ }
9485
+ }
9486
+ for (const aliasKey of aliasKeys) {
9487
+ const alias = globalAliases[aliasKey];
9488
+ if (alias) {
9489
+ return {
9490
+ displayName: alias,
9491
+ persistentDisplayName: alias,
9492
+ source: "global-alias",
9493
+ matchedAliasKey: aliasKey
9494
+ };
9495
+ }
9496
+ }
9497
+ }
9498
+ return {
9499
+ displayName: stableId,
9500
+ source: "stable-id"
9501
+ };
9502
+ }
9503
+ function logQQBotSenderNameResolution(params) {
9504
+ const { logger, inbound, accountId, resolution } = params;
9505
+ logger.debug?.(
9506
+ `[display-name] accountId=${accountId} type=${inbound.type} senderId=${inbound.senderId} knownTarget=${resolution.knownTargetDisplayName ?? "-"} alias=${resolution.matchedAliasKey ?? "-"} final=${JSON.stringify(resolution.displayName)} source=${resolution.source}`
9507
+ );
9508
+ }
9142
9509
  function toNumber2(value) {
9143
9510
  if (typeof value === "number" && Number.isFinite(value)) return value;
9144
9511
  if (typeof value === "string") {
@@ -9522,14 +9889,13 @@ function parseC2CMessage(data, fallbackEventId) {
9522
9889
  const id = toString(payload.id);
9523
9890
  const eventId = resolveEventId(payload, fallbackEventId);
9524
9891
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9525
- const author = payload.author ?? {};
9892
+ const author = asRecord(payload.author) ?? {};
9526
9893
  const senderId = toString(author.user_openid);
9527
9894
  if (!text && attachments.length === 0 || !id || !senderId) return null;
9528
9895
  return {
9529
9896
  type: "direct",
9530
9897
  senderId,
9531
9898
  c2cOpenid: senderId,
9532
- senderName: toString(author.username),
9533
9899
  content: text,
9534
9900
  attachments: attachments.length > 0 ? attachments : void 0,
9535
9901
  messageId: id,
@@ -9546,13 +9912,12 @@ function parseGroupMessage(data, fallbackEventId) {
9546
9912
  const eventId = resolveEventId(payload, fallbackEventId);
9547
9913
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9548
9914
  const groupOpenid = toString(payload.group_openid);
9549
- const author = payload.author ?? {};
9915
+ const author = asRecord(payload.author) ?? {};
9550
9916
  const senderId = toString(author.member_openid);
9551
9917
  if (!text && attachments.length === 0 || !id || !senderId || !groupOpenid) return null;
9552
9918
  return {
9553
9919
  type: "group",
9554
9920
  senderId,
9555
- senderName: toString(author.nickname) ?? toString(author.username),
9556
9921
  content: text,
9557
9922
  attachments: attachments.length > 0 ? attachments : void 0,
9558
9923
  messageId: id,
@@ -9570,13 +9935,12 @@ function parseChannelMessage(data, fallbackEventId) {
9570
9935
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9571
9936
  const channelId = toString(payload.channel_id);
9572
9937
  const guildId = toString(payload.guild_id);
9573
- const author = payload.author ?? {};
9938
+ const author = asRecord(payload.author) ?? {};
9574
9939
  const senderId = toString(author.id);
9575
9940
  if (!text && attachments.length === 0 || !id || !senderId || !channelId) return null;
9576
9941
  return {
9577
9942
  type: "channel",
9578
9943
  senderId,
9579
- senderName: toString(author.username),
9580
9944
  content: text,
9581
9945
  attachments: attachments.length > 0 ? attachments : void 0,
9582
9946
  messageId: id,
@@ -9594,13 +9958,12 @@ function parseDirectMessage(data, fallbackEventId) {
9594
9958
  const eventId = resolveEventId(payload, fallbackEventId);
9595
9959
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9596
9960
  const guildId = toString(payload.guild_id);
9597
- const author = payload.author ?? {};
9961
+ const author = asRecord(payload.author) ?? {};
9598
9962
  const senderId = toString(author.id);
9599
9963
  if (!text && attachments.length === 0 || !id || !senderId) return null;
9600
9964
  return {
9601
9965
  type: "direct",
9602
9966
  senderId,
9603
- senderName: toString(author.username),
9604
9967
  content: text,
9605
9968
  attachments: attachments.length > 0 ? attachments : void 0,
9606
9969
  messageId: id,
@@ -9675,7 +10038,7 @@ function resolveEnvelopeFrom(event) {
9675
10038
  return event.senderName?.trim() || event.senderId;
9676
10039
  }
9677
10040
  function resolveKnownQQBotTargetFromInbound(params) {
9678
- const { inbound, accountId } = params;
10041
+ const { inbound, accountId, persistentDisplayName } = params;
9679
10042
  if (inbound.type === "direct") {
9680
10043
  if (!inbound.c2cOpenid?.trim()) {
9681
10044
  return void 0;
@@ -9684,7 +10047,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9684
10047
  accountId,
9685
10048
  kind: "user",
9686
10049
  target: `user:${inbound.c2cOpenid}`,
9687
- displayName: inbound.senderName,
10050
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9688
10051
  sourceChatType: "direct",
9689
10052
  firstSeenAt: inbound.timestamp,
9690
10053
  lastSeenAt: inbound.timestamp
@@ -9695,7 +10058,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9695
10058
  accountId,
9696
10059
  kind: "group",
9697
10060
  target: `group:${inbound.groupOpenid}`,
9698
- displayName: inbound.senderName,
10061
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9699
10062
  sourceChatType: "group",
9700
10063
  firstSeenAt: inbound.timestamp,
9701
10064
  lastSeenAt: inbound.timestamp
@@ -9706,7 +10069,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9706
10069
  accountId,
9707
10070
  kind: "channel",
9708
10071
  target: `channel:${inbound.channelId}`,
9709
- displayName: inbound.senderName,
10072
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9710
10073
  sourceChatType: "channel",
9711
10074
  firstSeenAt: inbound.timestamp,
9712
10075
  lastSeenAt: inbound.timestamp
@@ -9821,6 +10184,14 @@ var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_v
9821
10184
  var VOICE_EMOTION_TAG_RE = /\[(?:happy|excited|calm|sad|angry|frustrated|softly|whispers|loudly|cheerfully|deadpan|sarcastically|laughs|sighs|chuckles|gasps|pause|slowly|rushed|hesitates|playfully|warmly|gently)\]/gi;
9822
10185
  var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
9823
10186
  var MARKDOWN_TABLE_SEPARATOR_RE = /^\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?)?\|?$/;
10187
+ var MARKDOWN_THEMATIC_BREAK_RE = /^\s{0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/;
10188
+ var MARKDOWN_ATX_HEADING_RE = /^\s{0,3}#{1,6}\s+\S/;
10189
+ var MARKDOWN_BLOCKQUOTE_RE = /^\s{0,3}>\s?/;
10190
+ var MARKDOWN_FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
10191
+ var MARKDOWN_LIST_ITEM_RE = /^\s*(?:[-+*]|\d+\.)\s+/;
10192
+ var MARKDOWN_LIST_CONTINUATION_RE = /^\s{2,}\S/;
10193
+ var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[^~\n]+~~|\*[^*\n]+\*)/;
10194
+ var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
9824
10195
  var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
9825
10196
  var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
9826
10197
  function extractFinalBlocks(text) {
@@ -9889,8 +10260,9 @@ function evaluateReplyFinalOnlyDelivery(params) {
9889
10260
  }
9890
10261
  function isQQBotC2CTarget(to) {
9891
10262
  const trimmed = to.trim();
9892
- const raw = trimmed.startsWith("qqbot:") ? trimmed.slice("qqbot:".length) : trimmed;
9893
- return !raw.startsWith("group:") && !raw.startsWith("channel:");
10263
+ const raw = trimmed.slice(0, "qqbot:".length).toLowerCase() === "qqbot:" ? trimmed.slice("qqbot:".length) : trimmed;
10264
+ const normalizedRaw = raw.toLowerCase();
10265
+ return !normalizedRaw.startsWith("group:") && !normalizedRaw.startsWith("channel:");
9894
10266
  }
9895
10267
  function splitQQBotMarkdownTransportMediaUrls(mediaUrls) {
9896
10268
  const markdownImageUrls = [];
@@ -9992,10 +10364,592 @@ function normalizeQQBotRenderedMarkdown(text) {
9992
10364
  );
9993
10365
  return changed ? next.trim() : text.trim();
9994
10366
  }
10367
+ function normalizeQQBotMarkdownSegment(text) {
10368
+ return text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
10369
+ }
10370
+ function isBlankQQBotMarkdownLine(line) {
10371
+ return line.trim().length === 0;
10372
+ }
10373
+ function resolveQQBotFenceDelimiter(line) {
10374
+ const match = line.match(MARKDOWN_FENCE_RE);
10375
+ return match?.[1];
10376
+ }
10377
+ function isQQBotFenceClosingLine(line, delimiter) {
10378
+ const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10379
+ const closingRe = new RegExp(`^\\s*${escapedDelimiter}${delimiter[0]}*\\s*$`);
10380
+ return closingRe.test(line);
10381
+ }
10382
+ function joinQQBotMarkdownPieces(parts) {
10383
+ return parts.filter(Boolean).join("\n\n").trim();
10384
+ }
10385
+ function isQQBotMarkdownTableStart(lines, index) {
10386
+ const header = lines[index]?.trim() ?? "";
10387
+ const separator = lines[index + 1]?.trim() ?? "";
10388
+ return Boolean(header.includes("|") && MARKDOWN_TABLE_SEPARATOR_RE.test(separator));
10389
+ }
10390
+ function collectQQBotFencedCodeBlock(lines, startIndex) {
10391
+ const openingLine = lines[startIndex] ?? "";
10392
+ const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
10393
+ let index = startIndex + 1;
10394
+ while (index < lines.length) {
10395
+ if (isQQBotFenceClosingLine(lines[index] ?? "", delimiter)) {
10396
+ index += 1;
10397
+ break;
10398
+ }
10399
+ index += 1;
10400
+ }
10401
+ return {
10402
+ block: {
10403
+ kind: "code",
10404
+ text: lines.slice(startIndex, index).join("\n").trimEnd()
10405
+ },
10406
+ nextIndex: index
10407
+ };
10408
+ }
10409
+ function collectQQBotMarkdownTableBlock(lines, startIndex) {
10410
+ let index = startIndex + 2;
10411
+ while (index < lines.length) {
10412
+ const line = lines[index] ?? "";
10413
+ if (isBlankQQBotMarkdownLine(line) || !line.includes("|")) {
10414
+ break;
10415
+ }
10416
+ index += 1;
10417
+ }
10418
+ return {
10419
+ block: {
10420
+ kind: "table",
10421
+ text: lines.slice(startIndex, index).join("\n").trimEnd()
10422
+ },
10423
+ nextIndex: index
10424
+ };
10425
+ }
10426
+ function collectQQBotBlockquoteBlock(lines, startIndex) {
10427
+ const collected = [];
10428
+ let index = startIndex;
10429
+ while (index < lines.length) {
10430
+ const line = lines[index] ?? "";
10431
+ if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
10432
+ collected.push(line);
10433
+ index += 1;
10434
+ continue;
10435
+ }
10436
+ if (isBlankQQBotMarkdownLine(line) && index + 1 < lines.length && MARKDOWN_BLOCKQUOTE_RE.test(lines[index + 1] ?? "")) {
10437
+ collected.push(line);
10438
+ index += 1;
10439
+ continue;
10440
+ }
10441
+ break;
10442
+ }
10443
+ return {
10444
+ block: {
10445
+ kind: "blockquote",
10446
+ text: collected.join("\n").trimEnd()
10447
+ },
10448
+ nextIndex: index
10449
+ };
10450
+ }
10451
+ function collectQQBotListBlock(lines, startIndex) {
10452
+ const collected = [];
10453
+ let index = startIndex;
10454
+ while (index < lines.length) {
10455
+ const line = lines[index] ?? "";
10456
+ if (isBlankQQBotMarkdownLine(line)) {
10457
+ break;
10458
+ }
10459
+ if (MARKDOWN_FENCE_RE.test(line) || MARKDOWN_BLOCKQUOTE_RE.test(line) || MARKDOWN_ATX_HEADING_RE.test(line) || MARKDOWN_THEMATIC_BREAK_RE.test(line) || isQQBotMarkdownTableStart(lines, index)) {
10460
+ break;
10461
+ }
10462
+ if (collected.length > 0 && !MARKDOWN_LIST_ITEM_RE.test(line) && !MARKDOWN_LIST_CONTINUATION_RE.test(line)) {
10463
+ collected.push(line);
10464
+ index += 1;
10465
+ continue;
10466
+ }
10467
+ collected.push(line);
10468
+ index += 1;
10469
+ }
10470
+ return {
10471
+ block: {
10472
+ kind: "list",
10473
+ text: collected.join("\n").trimEnd()
10474
+ },
10475
+ nextIndex: index
10476
+ };
10477
+ }
10478
+ function collectQQBotParagraphBlock(lines, startIndex) {
10479
+ const collected = [];
10480
+ let index = startIndex;
10481
+ while (index < lines.length) {
10482
+ const line = lines[index] ?? "";
10483
+ if (isBlankQQBotMarkdownLine(line)) {
10484
+ break;
10485
+ }
10486
+ if (collected.length > 0 && (MARKDOWN_FENCE_RE.test(line) || MARKDOWN_BLOCKQUOTE_RE.test(line) || MARKDOWN_ATX_HEADING_RE.test(line) || MARKDOWN_THEMATIC_BREAK_RE.test(line) || MARKDOWN_LIST_ITEM_RE.test(line) || isQQBotMarkdownTableStart(lines, index))) {
10487
+ break;
10488
+ }
10489
+ collected.push(line);
10490
+ index += 1;
10491
+ }
10492
+ return {
10493
+ block: {
10494
+ kind: "paragraph",
10495
+ text: collected.join("\n").trimEnd()
10496
+ },
10497
+ nextIndex: index
10498
+ };
10499
+ }
10500
+ function parseQQBotMarkdownBlocks(text) {
10501
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
10502
+ const blocks = [];
10503
+ let index = 0;
10504
+ while (index < lines.length) {
10505
+ while (index < lines.length && isBlankQQBotMarkdownLine(lines[index] ?? "")) {
10506
+ index += 1;
10507
+ }
10508
+ if (index >= lines.length) {
10509
+ break;
10510
+ }
10511
+ const line = lines[index] ?? "";
10512
+ if (MARKDOWN_FENCE_RE.test(line)) {
10513
+ const result2 = collectQQBotFencedCodeBlock(lines, index);
10514
+ blocks.push(result2.block);
10515
+ index = result2.nextIndex;
10516
+ continue;
10517
+ }
10518
+ if (isQQBotMarkdownTableStart(lines, index)) {
10519
+ const result2 = collectQQBotMarkdownTableBlock(lines, index);
10520
+ blocks.push(result2.block);
10521
+ index = result2.nextIndex;
10522
+ continue;
10523
+ }
10524
+ if (MARKDOWN_THEMATIC_BREAK_RE.test(line)) {
10525
+ blocks.push({ kind: "thematic-break", text: line.trim() });
10526
+ index += 1;
10527
+ continue;
10528
+ }
10529
+ if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
10530
+ const result2 = collectQQBotBlockquoteBlock(lines, index);
10531
+ blocks.push(result2.block);
10532
+ index = result2.nextIndex;
10533
+ continue;
10534
+ }
10535
+ if (MARKDOWN_ATX_HEADING_RE.test(line)) {
10536
+ blocks.push({ kind: "heading", text: line.trimEnd() });
10537
+ index += 1;
10538
+ continue;
10539
+ }
10540
+ if (MARKDOWN_LIST_ITEM_RE.test(line)) {
10541
+ const result2 = collectQQBotListBlock(lines, index);
10542
+ blocks.push(result2.block);
10543
+ index = result2.nextIndex;
10544
+ continue;
10545
+ }
10546
+ const result = collectQQBotParagraphBlock(lines, index);
10547
+ blocks.push(result.block);
10548
+ index = result.nextIndex;
10549
+ }
10550
+ return blocks;
10551
+ }
10552
+ function hasQQBotBoundaryGuard(text) {
10553
+ return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
10554
+ }
10555
+ function isQQBotSafeMarkdownBoundary(text, index) {
10556
+ const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
10557
+ const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
10558
+ const leftEdge = left.slice(-1);
10559
+ const rightEdge = right.slice(0, 1);
10560
+ return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
10561
+ }
10562
+ function findQQBotRegexBoundary(text, limit, pattern) {
10563
+ const scopedText = text.slice(0, Math.min(limit + 1, text.length));
10564
+ const regex = new RegExp(pattern.source, pattern.flags);
10565
+ let match = regex.exec(scopedText);
10566
+ let lastBoundary;
10567
+ while (match) {
10568
+ const boundary = match.index + match[0].length;
10569
+ if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10570
+ lastBoundary = boundary;
10571
+ }
10572
+ match = regex.exec(scopedText);
10573
+ }
10574
+ return lastBoundary;
10575
+ }
10576
+ function findQQBotFallbackBoundary(text, limit) {
10577
+ const minIndex = Math.max(1, limit - 120);
10578
+ for (let index = limit; index >= minIndex; index -= 1) {
10579
+ if (isQQBotSafeMarkdownBoundary(text, index)) {
10580
+ return index;
10581
+ }
10582
+ }
10583
+ return limit;
10584
+ }
10585
+ function findQQBotSafeSplitIndex(text, limit) {
10586
+ const boundaryPatterns = [
10587
+ /\n\n+/g,
10588
+ /\n/g,
10589
+ /[。!?.!?;;::](?:\s+|$)/g,
10590
+ /[,,](?:\s+|$)/g,
10591
+ /\s+/g
10592
+ ];
10593
+ for (const pattern of boundaryPatterns) {
10594
+ const boundary = findQQBotRegexBoundary(text, limit, pattern);
10595
+ if (boundary && boundary > 0) {
10596
+ return boundary;
10597
+ }
10598
+ }
10599
+ return findQQBotFallbackBoundary(text, limit);
10600
+ }
10601
+ function splitQQBotHardText(text, limit) {
10602
+ if (limit <= 0 || text.length <= limit) {
10603
+ return [text];
10604
+ }
10605
+ const chunks = [];
10606
+ let remaining = text;
10607
+ while (remaining.length > limit) {
10608
+ chunks.push(remaining.slice(0, limit));
10609
+ remaining = remaining.slice(limit);
10610
+ }
10611
+ if (remaining) {
10612
+ chunks.push(remaining);
10613
+ }
10614
+ return chunks;
10615
+ }
10616
+ function splitQQBotTextSafely(text, limit, options) {
10617
+ if (limit <= 0 || text.length <= limit) {
10618
+ return [text];
10619
+ }
10620
+ const trimLeading = options?.trimLeading ?? true;
10621
+ const trimTrailing = options?.trimTrailing ?? true;
10622
+ const chunks = [];
10623
+ let remaining = text;
10624
+ while (remaining.length > limit) {
10625
+ const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
10626
+ let nextChunk = remaining.slice(0, splitIndex);
10627
+ let nextRemaining = remaining.slice(splitIndex);
10628
+ if (trimTrailing) {
10629
+ nextChunk = nextChunk.trimEnd();
10630
+ }
10631
+ if (trimLeading) {
10632
+ nextRemaining = nextRemaining.trimStart();
10633
+ }
10634
+ if (!nextChunk) {
10635
+ const hardChunk = remaining.slice(0, limit);
10636
+ chunks.push(hardChunk);
10637
+ remaining = remaining.slice(hardChunk.length);
10638
+ continue;
10639
+ }
10640
+ chunks.push(nextChunk);
10641
+ remaining = nextRemaining;
10642
+ }
10643
+ const finalChunk = trimTrailing ? remaining.trimEnd() : remaining;
10644
+ if (finalChunk) {
10645
+ chunks.push(finalChunk);
10646
+ }
10647
+ return chunks;
10648
+ }
10649
+ function splitQQBotMarkdownLineBlock(text, limit) {
10650
+ if (limit <= 0 || text.length <= limit) {
10651
+ return [text];
10652
+ }
10653
+ const lines = text.split("\n");
10654
+ const chunks = [];
10655
+ let currentLines = [];
10656
+ const flushCurrent = () => {
10657
+ if (currentLines.length === 0) {
10658
+ return;
10659
+ }
10660
+ const chunk = currentLines.join("\n").trimEnd();
10661
+ if (chunk) {
10662
+ chunks.push(chunk);
10663
+ }
10664
+ currentLines = [];
10665
+ };
10666
+ for (const line of lines) {
10667
+ const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
10668
+ ${line}` : line;
10669
+ if (candidate.length <= limit) {
10670
+ currentLines.push(line);
10671
+ continue;
10672
+ }
10673
+ flushCurrent();
10674
+ if (line.length <= limit) {
10675
+ currentLines.push(line);
10676
+ continue;
10677
+ }
10678
+ for (const piece of splitQQBotTextSafely(line, limit, {
10679
+ trimLeading: false,
10680
+ trimTrailing: false
10681
+ })) {
10682
+ if (piece) {
10683
+ chunks.push(piece);
10684
+ }
10685
+ }
10686
+ }
10687
+ flushCurrent();
10688
+ return chunks;
10689
+ }
10690
+ function splitQQBotMarkdownTableBlock(text, limit) {
10691
+ if (limit <= 0 || text.length <= limit) {
10692
+ return [text];
10693
+ }
10694
+ const lines = text.split("\n");
10695
+ const header = lines[0] ?? "";
10696
+ const separator = lines[1] ?? "";
10697
+ const rows = lines.slice(2);
10698
+ const tablePrefix = `${header}
10699
+ ${separator}`;
10700
+ const chunks = [];
10701
+ let currentRows = [];
10702
+ const flushCurrent = () => {
10703
+ if (currentRows.length === 0) {
10704
+ return;
10705
+ }
10706
+ chunks.push(`${tablePrefix}
10707
+ ${currentRows.join("\n")}`);
10708
+ currentRows = [];
10709
+ };
10710
+ for (const row of rows) {
10711
+ const candidate = currentRows.length > 0 ? `${tablePrefix}
10712
+ ${currentRows.join("\n")}
10713
+ ${row}` : `${tablePrefix}
10714
+ ${row}`;
10715
+ if (candidate.length <= limit) {
10716
+ currentRows.push(row);
10717
+ continue;
10718
+ }
10719
+ flushCurrent();
10720
+ if (`${tablePrefix}
10721
+ ${row}`.length <= limit) {
10722
+ currentRows.push(row);
10723
+ continue;
10724
+ }
10725
+ const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
10726
+ for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
10727
+ trimLeading: false,
10728
+ trimTrailing: false
10729
+ })) {
10730
+ chunks.push(`${tablePrefix}
10731
+ ${rowPiece}`);
10732
+ }
10733
+ }
10734
+ flushCurrent();
10735
+ return chunks.length > 0 ? chunks : [text];
10736
+ }
10737
+ function splitQQBotMarkdownCodeFence(text, limit) {
10738
+ if (limit <= 0 || text.length <= limit) {
10739
+ return [text];
10740
+ }
10741
+ const lines = text.split("\n");
10742
+ const openingLine = lines[0] ?? "```";
10743
+ const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
10744
+ const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
10745
+ const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
10746
+ const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
10747
+ const fixedOverhead = openingLine.length + closingLine.length + 2;
10748
+ const availableLineLength = Math.max(1, limit - fixedOverhead);
10749
+ const chunks = [];
10750
+ let currentCodeLines = [];
10751
+ const flushCurrent = () => {
10752
+ if (currentCodeLines.length === 0) {
10753
+ return;
10754
+ }
10755
+ chunks.push(`${openingLine}
10756
+ ${currentCodeLines.join("\n")}
10757
+ ${closingLine}`);
10758
+ currentCodeLines = [];
10759
+ };
10760
+ for (const codeLine of codeLines) {
10761
+ const candidate = currentCodeLines.length > 0 ? `${openingLine}
10762
+ ${currentCodeLines.join("\n")}
10763
+ ${codeLine}
10764
+ ${closingLine}` : `${openingLine}
10765
+ ${codeLine}
10766
+ ${closingLine}`;
10767
+ if (candidate.length <= limit) {
10768
+ currentCodeLines.push(codeLine);
10769
+ continue;
10770
+ }
10771
+ flushCurrent();
10772
+ if (`${openingLine}
10773
+ ${codeLine}
10774
+ ${closingLine}`.length <= limit) {
10775
+ currentCodeLines.push(codeLine);
10776
+ continue;
10777
+ }
10778
+ for (const linePiece of splitQQBotHardText(codeLine, availableLineLength)) {
10779
+ chunks.push(`${openingLine}
10780
+ ${linePiece}
10781
+ ${closingLine}`);
10782
+ }
10783
+ }
10784
+ flushCurrent();
10785
+ return chunks.length > 0 ? chunks : [text];
10786
+ }
10787
+ function splitQQBotMarkdownBlock(block, limit) {
10788
+ if (limit <= 0 || block.text.length <= limit) {
10789
+ return [block.text];
10790
+ }
10791
+ switch (block.kind) {
10792
+ case "table":
10793
+ return splitQQBotMarkdownTableBlock(block.text, limit);
10794
+ case "code":
10795
+ return splitQQBotMarkdownCodeFence(block.text, limit);
10796
+ case "blockquote":
10797
+ return splitQQBotMarkdownLineBlock(block.text, limit);
10798
+ case "list":
10799
+ return splitQQBotMarkdownLineBlock(block.text, limit);
10800
+ case "paragraph":
10801
+ case "heading":
10802
+ return splitQQBotTextSafely(block.text, limit);
10803
+ case "thematic-break":
10804
+ return [block.text];
10805
+ default:
10806
+ return [block.text];
10807
+ }
10808
+ }
10809
+ function chunkQQBotStructuredMarkdown(text, limit) {
10810
+ const blocks = parseQQBotMarkdownBlocks(text);
10811
+ if (blocks.length === 0 || limit <= 0) {
10812
+ return [text.trim()];
10813
+ }
10814
+ const chunks = [];
10815
+ let currentPieces = [];
10816
+ let pendingPrefixPieces = [];
10817
+ const flushCurrent = () => {
10818
+ if (currentPieces.length === 0) {
10819
+ return;
10820
+ }
10821
+ const chunk = joinQQBotMarkdownPieces(currentPieces);
10822
+ if (chunk) {
10823
+ chunks.push(chunk);
10824
+ }
10825
+ currentPieces = [];
10826
+ };
10827
+ const appendPiece = (piece) => {
10828
+ if (!piece) {
10829
+ return;
10830
+ }
10831
+ const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
10832
+ for (const nextPiece of pieces) {
10833
+ const normalizedPiece = nextPiece.trim();
10834
+ if (!normalizedPiece) {
10835
+ continue;
10836
+ }
10837
+ const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
10838
+ if (currentPieces.length === 0 || candidate.length <= limit) {
10839
+ currentPieces.push(normalizedPiece);
10840
+ continue;
10841
+ }
10842
+ flushCurrent();
10843
+ currentPieces.push(normalizedPiece);
10844
+ }
10845
+ };
10846
+ const consumePendingPrefix = (piece) => {
10847
+ if (pendingPrefixPieces.length === 0) {
10848
+ return piece;
10849
+ }
10850
+ const prefixed = joinQQBotMarkdownPieces([...pendingPrefixPieces, piece]);
10851
+ pendingPrefixPieces = [];
10852
+ return prefixed;
10853
+ };
10854
+ for (let index = 0; index < blocks.length; index += 1) {
10855
+ const block = blocks[index];
10856
+ if (!block) {
10857
+ continue;
10858
+ }
10859
+ if (block.kind === "thematic-break") {
10860
+ if (currentPieces.length > 0) {
10861
+ const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
10862
+ if (candidate.length <= limit) {
10863
+ currentPieces.push(block.text);
10864
+ continue;
10865
+ }
10866
+ flushCurrent();
10867
+ }
10868
+ pendingPrefixPieces.push(block.text);
10869
+ continue;
10870
+ }
10871
+ if (block.kind === "heading") {
10872
+ const headingText = consumePendingPrefix(block.text);
10873
+ const nextBlock = blocks[index + 1];
10874
+ if (nextBlock && nextBlock.kind !== "thematic-break") {
10875
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
10876
+ const firstBodyPiece = nextPieces[0];
10877
+ if (firstBodyPiece) {
10878
+ const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
10879
+ const pairedCandidate = joinQQBotMarkdownPieces([
10880
+ ...currentPieces,
10881
+ headingText,
10882
+ firstBodyPiece
10883
+ ]);
10884
+ if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
10885
+ currentPieces.push(headingText, firstBodyPiece);
10886
+ for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10887
+ appendPiece(nextPieces[pieceIndex] ?? "");
10888
+ }
10889
+ index += 1;
10890
+ continue;
10891
+ }
10892
+ if (currentPieces.length > 0 && pairedText.length <= limit) {
10893
+ flushCurrent();
10894
+ currentPieces.push(headingText, firstBodyPiece);
10895
+ for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10896
+ appendPiece(nextPieces[pieceIndex] ?? "");
10897
+ }
10898
+ index += 1;
10899
+ continue;
10900
+ }
10901
+ }
10902
+ }
10903
+ appendPiece(headingText);
10904
+ continue;
10905
+ }
10906
+ const blockText = consumePendingPrefix(block.text);
10907
+ for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
10908
+ appendPiece(piece);
10909
+ }
10910
+ }
10911
+ if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
10912
+ const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
10913
+ if (trailingCandidate.length <= limit) {
10914
+ currentPieces.push(...pendingPrefixPieces);
10915
+ }
10916
+ }
10917
+ flushCurrent();
10918
+ return chunks.length > 0 ? chunks : [text.trim()];
10919
+ }
10920
+ function looksLikeStructuredMarkdown(text) {
10921
+ const normalized = normalizeQQBotMarkdownSegment(text);
10922
+ if (!normalized) {
10923
+ return false;
10924
+ }
10925
+ const lines = normalized.split("\n");
10926
+ if (hasQQBotMarkdownTable(normalized)) {
10927
+ return true;
10928
+ }
10929
+ return normalized.includes("\n\n") || lines.some((line) => MARKDOWN_ATX_HEADING_RE.test(line)) || lines.some((line) => MARKDOWN_BLOCKQUOTE_RE.test(line)) || lines.some((line) => MARKDOWN_FENCE_RE.test(line)) || lines.some((line) => MARKDOWN_THEMATIC_BREAK_RE.test(line)) || lines.some((line) => MARKDOWN_LIST_ITEM_RE.test(line)) || MARKDOWN_INLINE_STRUCTURE_RE.test(normalized);
10930
+ }
10931
+ function chunkC2CMarkdownText(params) {
10932
+ const normalized = params.text.trim();
10933
+ if (!normalized) {
10934
+ return [];
10935
+ }
10936
+ const strategy = params.strategy ?? "markdown-block";
10937
+ if (strategy === "length") {
10938
+ return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10939
+ }
10940
+ if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
10941
+ return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10942
+ }
10943
+ return chunkQQBotStructuredMarkdown(normalized, params.limit);
10944
+ }
9995
10945
  async function sendQQBotMediaWithFallback(params) {
9996
10946
  const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
9997
10947
  const outbound = params.outbound ?? qqbotOutbound;
10948
+ const shouldContinue = params.shouldContinue ?? (() => true);
9998
10949
  for (const mediaUrl of mediaQueue) {
10950
+ if (!shouldContinue()) {
10951
+ return;
10952
+ }
9999
10953
  const result = await outbound.sendMedia({
10000
10954
  cfg: { channels: { qqbot: qqCfg } },
10001
10955
  to,
@@ -10011,6 +10965,9 @@ async function sendQQBotMediaWithFallback(params) {
10011
10965
  if (!fallback) {
10012
10966
  continue;
10013
10967
  }
10968
+ if (!shouldContinue()) {
10969
+ return;
10970
+ }
10014
10971
  const fallbackResult = await outbound.sendText({
10015
10972
  cfg: { channels: { qqbot: qqCfg } },
10016
10973
  to,
@@ -10063,10 +11020,17 @@ async function dispatchToAgent(params) {
10063
11020
  const { inbound, cfg, qqCfg, accountId, logger, route } = params;
10064
11021
  const runtime2 = getQQBotRuntime();
10065
11022
  const routeSessionKey = resolveQQBotRouteSessionKey(route);
11023
+ const queueKey = buildSessionDispatchQueueKey(route);
11024
+ const isFastAbortCommand = isQQBotFastAbortCommandText(inbound.content);
11025
+ const dispatchAbortGeneration = getSessionDispatchState(queueKey).abortGeneration;
11026
+ const shouldSuppressVisibleReplies = () => {
11027
+ const currentAbortGeneration = sessionDispatchQueue.get(queueKey)?.abortGeneration ?? dispatchAbortGeneration;
11028
+ return currentAbortGeneration !== dispatchAbortGeneration;
11029
+ };
10066
11030
  const target = resolveChatTarget(inbound);
10067
11031
  const outboundAccountId = route.accountId ?? accountId;
10068
11032
  let typingRefIdx;
10069
- if (inbound.c2cOpenid) {
11033
+ if (inbound.c2cOpenid && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
10070
11034
  const typing = await qqbotOutbound.sendTyping({
10071
11035
  cfg: { channels: { qqbot: qqCfg } },
10072
11036
  to: `user:${inbound.c2cOpenid}`,
@@ -10103,7 +11067,7 @@ async function dispatchToAgent(params) {
10103
11067
  delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
10104
11068
  logger,
10105
11069
  sendNotice: async () => {
10106
- if (groupMessageInterfaceBlocked) return;
11070
+ if (groupMessageInterfaceBlocked || isFastAbortCommand || shouldSuppressVisibleReplies()) return;
10107
11071
  const result = await qqbotOutbound.sendText({
10108
11072
  cfg: { channels: { qqbot: qqCfg } },
10109
11073
  to: target.to,
@@ -10137,6 +11101,9 @@ async function dispatchToAgent(params) {
10137
11101
  logger
10138
11102
  });
10139
11103
  if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
11104
+ if (shouldSuppressVisibleReplies()) {
11105
+ return;
11106
+ }
10140
11107
  const fallback = await qqbotOutbound.sendText({
10141
11108
  cfg: { channels: { qqbot: qqCfg } },
10142
11109
  to: target.to,
@@ -10246,29 +11213,43 @@ async function dispatchToAgent(params) {
10246
11213
  }
10247
11214
  finalCtx.BodyForAgent = appendCronHiddenPrompt(agentBody);
10248
11215
  }
10249
- if (storePath && sessionApi?.recordInboundSession) {
10250
- try {
10251
- const mainSessionKeyRaw = route.mainSessionKey;
10252
- const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw.trim() : void 0;
10253
- const isGroup = inbound.type === "group" || inbound.type === "channel";
10254
- const updateLastRoute = !isGroup ? {
10255
- sessionKey: mainSessionKey ?? route.sessionKey,
10256
- channel: "qqbot",
10257
- to: stableTo,
10258
- accountId: outboundAccountId
10259
- } : void 0;
10260
- const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : routeSessionKey;
10261
- await sessionApi.recordInboundSession({
10262
- storePath,
10263
- sessionKey: recordSessionKey,
10264
- ctx: finalCtx,
10265
- updateLastRoute,
10266
- onRecordError: (err) => {
10267
- logger.warn(`failed to record inbound session: ${String(err)}`);
10268
- }
10269
- });
10270
- } catch (err) {
10271
- logger.warn(`failed to record inbound session: ${String(err)}`);
11216
+ if (storePath) {
11217
+ const mainSessionKeyRaw = route.mainSessionKey;
11218
+ const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw.trim() : void 0;
11219
+ const isGroup = inbound.type === "group" || inbound.type === "channel";
11220
+ const updateLastRoute = !isGroup ? {
11221
+ sessionKey: mainSessionKey ?? route.sessionKey,
11222
+ channel: "qqbot",
11223
+ to: stableTo,
11224
+ accountId: outboundAccountId
11225
+ } : void 0;
11226
+ const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : routeSessionKey;
11227
+ if (sessionApi?.recordInboundSession) {
11228
+ try {
11229
+ await sessionApi.recordInboundSession({
11230
+ storePath,
11231
+ sessionKey: recordSessionKey,
11232
+ ctx: finalCtx,
11233
+ updateLastRoute,
11234
+ onRecordError: (err) => {
11235
+ logger.warn(`failed to record inbound session: ${String(err)}`);
11236
+ }
11237
+ });
11238
+ } catch (err) {
11239
+ logger.warn(`failed to record inbound session: ${String(err)}`);
11240
+ }
11241
+ }
11242
+ if (sessionApi?.recordSessionMetaFromInbound) {
11243
+ try {
11244
+ await sessionApi.recordSessionMetaFromInbound({
11245
+ storePath,
11246
+ sessionKey: recordSessionKey,
11247
+ ctx: finalCtx,
11248
+ createIfMissing: true
11249
+ });
11250
+ } catch (err) {
11251
+ logger.warn(`failed to record inbound session meta: ${String(err)}`);
11252
+ }
10272
11253
  }
10273
11254
  }
10274
11255
  const textApi = runtime2.channel?.text;
@@ -10296,10 +11277,13 @@ async function dispatchToAgent(params) {
10296
11277
  const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
10297
11278
  const markdownSupport = qqCfg.markdownSupport ?? true;
10298
11279
  const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
10299
- const useC2CMarkdownTransport = markdownSupport && isQQBotC2CTarget(target.to);
11280
+ const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11281
+ const isC2CTarget = isQQBotC2CTarget(target.to);
11282
+ const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
10300
11283
  let bufferedC2CMarkdownTexts = [];
10301
11284
  let bufferedC2CMarkdownMediaUrls = [];
10302
11285
  const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
11286
+ const hasBufferedC2CMarkdownReply = () => bufferedC2CMarkdownTexts.length > 0 || bufferedC2CMarkdownMediaUrls.length > 0;
10303
11287
  const bufferC2CMarkdownMedia = (url) => {
10304
11288
  const next = url?.trim();
10305
11289
  if (!next || bufferedC2CMarkdownMediaSeen.has(next)) return;
@@ -10307,12 +11291,18 @@ async function dispatchToAgent(params) {
10307
11291
  bufferedC2CMarkdownMediaUrls.push(next);
10308
11292
  };
10309
11293
  const sendC2CMarkdownTransportPayload = async (params2) => {
11294
+ if (shouldSuppressVisibleReplies()) {
11295
+ return;
11296
+ }
10310
11297
  const normalizedText = normalizeQQBotRenderedMarkdown(params2.text);
10311
11298
  const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(params2.mediaUrls);
10312
11299
  const finalMarkdownText = await normalizeQQBotMarkdownImages({
10313
11300
  text: normalizedText,
10314
11301
  appendImageUrls: markdownImageUrls
10315
11302
  });
11303
+ if (shouldSuppressVisibleReplies()) {
11304
+ return;
11305
+ }
10316
11306
  const textReplyRefs = resolveQQBotTextReplyRefs({
10317
11307
  to: target.to,
10318
11308
  text: finalMarkdownText || normalizedText,
@@ -10321,52 +11311,59 @@ async function dispatchToAgent(params) {
10321
11311
  replyToId: inbound.messageId,
10322
11312
  replyEventId: inbound.eventId
10323
11313
  });
10324
- const textSegments = finalMarkdownText ? [finalMarkdownText] : [];
11314
+ const textChunks = finalMarkdownText ? chunkC2CMarkdownText({
11315
+ text: finalMarkdownText,
11316
+ limit,
11317
+ strategy: c2cMarkdownChunkStrategy,
11318
+ fallbackChunkText: chunkText
11319
+ }) : [];
10325
11320
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
10326
11321
  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")}`
11322
+ `delivery=${deliveryLabel} to=${target.to} chunks=${textChunks.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")} chunkStrategy=${c2cMarkdownChunkStrategy}`
10328
11323
  );
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
- });
11324
+ if (!shouldSuppressVisibleReplies()) {
11325
+ await sendQQBotMediaWithFallback({
11326
+ qqCfg,
11327
+ to: target.to,
11328
+ mediaQueue,
11329
+ replyToId: textReplyRefs.replyToId,
11330
+ replyEventId: textReplyRefs.replyEventId,
11331
+ accountId: outboundAccountId,
11332
+ logger,
11333
+ onDelivered: () => {
11334
+ markReplyDelivered();
11335
+ },
11336
+ onError: (error) => {
11337
+ markGroupMessageInterfaceBlocked(error);
11338
+ },
11339
+ shouldContinue: () => !shouldSuppressVisibleReplies()
11340
+ });
11341
+ }
10344
11342
  if (!finalMarkdownText) {
10345
11343
  return;
10346
11344
  }
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
- }
11345
+ for (let chunkIndex = 0; chunkIndex < textChunks.length; chunkIndex += 1) {
11346
+ if (shouldSuppressVisibleReplies()) {
11347
+ return;
11348
+ }
11349
+ const chunk = textChunks[chunkIndex] ?? "";
11350
+ logger.info(
11351
+ `delivery=${deliveryLabel} segment=1/1 chunk=${chunkIndex + 1}/${textChunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
11352
+ );
11353
+ const result = await qqbotOutbound.sendText({
11354
+ cfg: { channels: { qqbot: qqCfg } },
11355
+ to: target.to,
11356
+ text: chunk,
11357
+ replyToId: textReplyRefs.replyToId,
11358
+ replyEventId: textReplyRefs.replyEventId,
11359
+ accountId: outboundAccountId
11360
+ });
11361
+ if (result.error) {
11362
+ logger.error(`send QQ markdown reply failed: ${result.error}`);
11363
+ markGroupMessageInterfaceBlocked(result.error);
11364
+ } else {
11365
+ logger.info(`sent QQ markdown reply (phase=${params2.phase}, len=${chunk.length})`);
11366
+ markReplyDelivered();
10370
11367
  }
10371
11368
  }
10372
11369
  };
@@ -10377,6 +11374,12 @@ async function dispatchToAgent(params) {
10377
11374
  bufferedC2CMarkdownMediaSeen.clear();
10378
11375
  return;
10379
11376
  }
11377
+ if (shouldSuppressVisibleReplies()) {
11378
+ bufferedC2CMarkdownTexts = [];
11379
+ bufferedC2CMarkdownMediaUrls = [];
11380
+ bufferedC2CMarkdownMediaSeen.clear();
11381
+ return;
11382
+ }
10380
11383
  const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
10381
11384
  const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
10382
11385
  bufferedC2CMarkdownTexts = [];
@@ -10389,6 +11392,9 @@ async function dispatchToAgent(params) {
10389
11392
  });
10390
11393
  };
10391
11394
  const deliver = async (payload, info) => {
11395
+ if (shouldSuppressVisibleReplies()) {
11396
+ return;
11397
+ }
10392
11398
  const typed = payload;
10393
11399
  const extractedTextMedia = extractQQBotReplyMedia({
10394
11400
  text: typed?.text ?? "",
@@ -10420,7 +11426,8 @@ async function dispatchToAgent(params) {
10420
11426
  const textToSend = suppressText ? "" : cleanedText;
10421
11427
  if (useC2CMarkdownTransport) {
10422
11428
  const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
10423
- if (shouldBufferFinalOnlyPayload) {
11429
+ const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
11430
+ if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
10424
11431
  if (textToSend) {
10425
11432
  bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
10426
11433
  }
@@ -10429,6 +11436,12 @@ async function dispatchToAgent(params) {
10429
11436
  }
10430
11437
  return;
10431
11438
  }
11439
+ if (hasBufferedC2CMarkdownReply()) {
11440
+ await flushBufferedC2CMarkdownReply();
11441
+ if (shouldSuppressVisibleReplies()) {
11442
+ return;
11443
+ }
11444
+ }
10432
11445
  await sendC2CMarkdownTransportPayload({
10433
11446
  text: textToSend,
10434
11447
  mediaUrls: mediaQueue,
@@ -10448,6 +11461,9 @@ async function dispatchToAgent(params) {
10448
11461
  });
10449
11462
  const chunks = chunkText(converted);
10450
11463
  for (const chunk of chunks) {
11464
+ if (shouldSuppressVisibleReplies()) {
11465
+ return;
11466
+ }
10451
11467
  const result = await qqbotOutbound.sendText({
10452
11468
  cfg: { channels: { qqbot: qqCfg } },
10453
11469
  to: target.to,
@@ -10464,6 +11480,9 @@ async function dispatchToAgent(params) {
10464
11480
  }
10465
11481
  }
10466
11482
  }
11483
+ if (shouldSuppressVisibleReplies()) {
11484
+ return;
11485
+ }
10467
11486
  await sendQQBotMediaWithFallback({
10468
11487
  qqCfg,
10469
11488
  to: target.to,
@@ -10477,12 +11496,38 @@ async function dispatchToAgent(params) {
10477
11496
  },
10478
11497
  onError: (error) => {
10479
11498
  markGroupMessageInterfaceBlocked(error);
10480
- }
11499
+ },
11500
+ shouldContinue: () => !shouldSuppressVisibleReplies()
10481
11501
  });
10482
11502
  };
10483
11503
  const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
11504
+ const dispatchDirect = replyApi.dispatchReplyWithDispatcher;
10484
11505
  const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
10485
- if (dispatchBuffered) {
11506
+ const streamingReplyOptions = isC2CTarget && !replyFinalOnly ? {
11507
+ disableBlockStreaming: false
11508
+ } : void 0;
11509
+ if (isC2CTarget && !replyFinalOnly && dispatchDirect) {
11510
+ logger.debug(`[dispatch] mode=direct session=${routeSessionKey} to=${target.to}`);
11511
+ await dispatchDirect({
11512
+ ctx: finalCtx,
11513
+ cfg,
11514
+ dispatcherOptions: {
11515
+ deliver,
11516
+ humanDelay,
11517
+ onError: (err, info) => {
11518
+ logger.error(`${info.kind} reply failed: ${String(err)}`);
11519
+ },
11520
+ onSkip: (_payload, info) => {
11521
+ if (info.reason !== "silent") {
11522
+ logger.info(`reply skipped: ${info.reason}`);
11523
+ }
11524
+ }
11525
+ },
11526
+ replyOptions: streamingReplyOptions
11527
+ });
11528
+ await flushBufferedC2CMarkdownReply();
11529
+ } else if (dispatchBuffered) {
11530
+ logger.debug(`[dispatch] mode=buffered session=${routeSessionKey} to=${target.to}`);
10486
11531
  await dispatchBuffered({
10487
11532
  ctx: finalCtx,
10488
11533
  cfg,
@@ -10497,10 +11542,12 @@ async function dispatchToAgent(params) {
10497
11542
  logger.info(`reply skipped: ${info.reason}`);
10498
11543
  }
10499
11544
  }
10500
- }
11545
+ },
11546
+ replyOptions: streamingReplyOptions
10501
11547
  });
10502
11548
  await flushBufferedC2CMarkdownReply();
10503
11549
  } else {
11550
+ logger.debug(`[dispatch] mode=legacy session=${routeSessionKey} to=${target.to}`);
10504
11551
  const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
10505
11552
  deliver,
10506
11553
  humanDelay,
@@ -10526,7 +11573,10 @@ async function dispatchToAgent(params) {
10526
11573
  ctx: finalCtx,
10527
11574
  cfg,
10528
11575
  dispatcher: dispatcherResult.dispatcher,
10529
- replyOptions: dispatcherResult.replyOptions
11576
+ replyOptions: {
11577
+ ...typeof dispatcherResult.replyOptions === "object" && dispatcherResult.replyOptions ? dispatcherResult.replyOptions : {},
11578
+ ...streamingReplyOptions ?? {}
11579
+ }
10530
11580
  });
10531
11581
  dispatcherResult.markDispatchIdle?.();
10532
11582
  await flushBufferedC2CMarkdownReply();
@@ -10535,7 +11585,7 @@ async function dispatchToAgent(params) {
10535
11585
  inbound,
10536
11586
  replyDelivered
10537
11587
  });
10538
- if (noReplyFallback && !groupMessageInterfaceBlocked) {
11588
+ if (noReplyFallback && !groupMessageInterfaceBlocked && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
10539
11589
  logger.info("no visible reply generated for group mention; sending fallback text");
10540
11590
  const fallbackResult = await qqbotOutbound.sendText({
10541
11591
  cfg: { channels: { qqbot: qqCfg } },
@@ -10609,18 +11659,39 @@ async function handleQQBotDispatch(params) {
10609
11659
  logger.info("qqbot disabled, ignoring inbound message");
10610
11660
  return;
10611
11661
  }
10612
- const content = inbound.content.trim();
11662
+ const senderNameResolution = resolveQQBotSenderName({
11663
+ inbound,
11664
+ cfg: params.cfg,
11665
+ accountId
11666
+ });
11667
+ const resolvedInbound = {
11668
+ ...inbound,
11669
+ senderName: senderNameResolution.displayName
11670
+ };
11671
+ logQQBotSenderNameResolution({
11672
+ logger,
11673
+ inbound,
11674
+ accountId,
11675
+ resolution: senderNameResolution
11676
+ });
11677
+ const content = resolvedInbound.content.trim();
10613
11678
  const inboundLogContent = sanitizeInboundLogText(
10614
11679
  resolveInboundLogContent({
10615
11680
  content,
10616
- attachments: inbound.attachments
11681
+ attachments: resolvedInbound.attachments
10617
11682
  })
10618
11683
  );
10619
- logger.info(`[inbound-user] accountId=${accountId} senderId=${inbound.senderId} content=${inboundLogContent}`);
10620
- if (!shouldHandleMessage(inbound, qqCfg, logger)) {
11684
+ logger.info(
11685
+ `[inbound-user] accountId=${accountId} senderId=${resolvedInbound.senderId} senderName=${JSON.stringify(resolvedInbound.senderName ?? resolvedInbound.senderId)} content=${inboundLogContent}`
11686
+ );
11687
+ if (!shouldHandleMessage(resolvedInbound, qqCfg, logger)) {
10621
11688
  return;
10622
11689
  }
10623
- const knownTarget = resolveKnownQQBotTargetFromInbound({ inbound, accountId });
11690
+ const knownTarget = resolveKnownQQBotTargetFromInbound({
11691
+ inbound: resolvedInbound,
11692
+ accountId,
11693
+ persistentDisplayName: senderNameResolution.persistentDisplayName
11694
+ });
10624
11695
  if (knownTarget) {
10625
11696
  try {
10626
11697
  upsertKnownQQBotTarget({ target: knownTarget });
@@ -10628,7 +11699,7 @@ async function handleQQBotDispatch(params) {
10628
11699
  logger.warn(`failed to record known qqbot target: ${String(err)}`);
10629
11700
  }
10630
11701
  }
10631
- const attachmentCount = inbound.attachments?.length ?? 0;
11702
+ const attachmentCount = resolvedInbound.attachments?.length ?? 0;
10632
11703
  if (attachmentCount > 0) {
10633
11704
  logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
10634
11705
  }
@@ -10641,7 +11712,7 @@ async function handleQQBotDispatch(params) {
10641
11712
  logger.warn("routing API not available");
10642
11713
  return;
10643
11714
  }
10644
- const target = resolveChatTarget(inbound);
11715
+ const target = resolveChatTarget(resolvedInbound);
10645
11716
  const route = routing({
10646
11717
  cfg: params.cfg,
10647
11718
  channel: "qqbot",
@@ -10649,7 +11720,7 @@ async function handleQQBotDispatch(params) {
10649
11720
  peer: { kind: target.peerKind, id: target.peerId }
10650
11721
  });
10651
11722
  const effectiveSessionKey = resolveQQBotEffectiveSessionKey({
10652
- inbound,
11723
+ inbound: resolvedInbound,
10653
11724
  route,
10654
11725
  accountId
10655
11726
  });
@@ -10659,13 +11730,36 @@ async function handleQQBotDispatch(params) {
10659
11730
  effectiveSessionKey
10660
11731
  };
10661
11732
  const queueKey = buildSessionDispatchQueueKey(resolvedRoute);
10662
- if (sessionDispatchQueue.has(queueKey)) {
11733
+ if (isQQBotFastAbortCommandText(content)) {
11734
+ const routeSessionKey = resolveQQBotRouteSessionKey(resolvedRoute);
11735
+ markSessionDispatchAbort(queueKey);
11736
+ const droppedCount = dropQueuedSessionDispatches(queueKey);
11737
+ logger.info(
11738
+ `session fast-abort command detected; executing immediately sessionKey=${routeSessionKey}`
11739
+ );
11740
+ logger.info(
11741
+ `session fast-abort command dropped ${droppedCount} queued messages sessionKey=${routeSessionKey}`
11742
+ );
11743
+ await runImmediateSessionDispatch(
11744
+ queueKey,
11745
+ async () => dispatchToAgent({
11746
+ inbound: { ...resolvedInbound, content },
11747
+ cfg: params.cfg,
11748
+ qqCfg,
11749
+ accountId,
11750
+ logger,
11751
+ route: resolvedRoute
11752
+ })
11753
+ );
11754
+ return;
11755
+ }
11756
+ if (hasSessionDispatchBacklog(queueKey)) {
10663
11757
  logger.info(`session busy; queueing inbound dispatch sessionKey=${resolveQQBotRouteSessionKey(resolvedRoute)}`);
10664
11758
  }
10665
11759
  await runSerializedSessionDispatch(
10666
11760
  queueKey,
10667
11761
  async () => dispatchToAgent({
10668
- inbound: { ...inbound, content },
11762
+ inbound: { ...resolvedInbound, content },
10669
11763
  cfg: params.cfg,
10670
11764
  qqCfg,
10671
11765
  accountId,
@@ -10693,6 +11787,10 @@ function formatGatewayConnectError(err) {
10693
11787
  }
10694
11788
  return String(err);
10695
11789
  }
11790
+ function isConnectionIdle(conn) {
11791
+ if (!conn) return true;
11792
+ return !conn.socket && !conn.promise && !conn.connecting;
11793
+ }
10696
11794
  var activeConnections = /* @__PURE__ */ new Map();
10697
11795
  function getOrCreateConnection(accountId) {
10698
11796
  let conn = activeConnections.get(accountId);
@@ -10722,7 +11820,10 @@ function clearTimers(conn) {
10722
11820
  conn.reconnectTimer = null;
10723
11821
  }
10724
11822
  }
10725
- function cleanupSocket(conn) {
11823
+ function cleanupSocket(conn, expectedSocket) {
11824
+ if (expectedSocket && conn.socket !== expectedSocket) {
11825
+ return false;
11826
+ }
10726
11827
  clearTimers(conn);
10727
11828
  if (conn.socket) {
10728
11829
  try {
@@ -10733,6 +11834,7 @@ function cleanupSocket(conn) {
10733
11834
  }
10734
11835
  conn.socket = null;
10735
11836
  }
11837
+ return true;
10736
11838
  }
10737
11839
  async function monitorQQBotProvider(opts = {}) {
10738
11840
  const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID, setStatus } = opts;
@@ -10740,11 +11842,16 @@ async function monitorQQBotProvider(opts = {}) {
10740
11842
  log: runtime2?.log,
10741
11843
  error: runtime2?.error
10742
11844
  });
10743
- const conn = getOrCreateConnection(accountId);
10744
- if (conn.socket) {
10745
- if (conn.promise) {
10746
- return conn.promise;
10747
- }
11845
+ const existingConn = activeConnections.get(accountId);
11846
+ if (!existingConn) ; else if (isConnectionIdle(existingConn)) {
11847
+ activeConnections.delete(accountId);
11848
+ }
11849
+ const conn = activeConnections.get(accountId);
11850
+ const existingPromise = conn?.promise;
11851
+ if (existingPromise) {
11852
+ return existingPromise;
11853
+ }
11854
+ if (conn?.socket) {
10748
11855
  throw new Error(`QQBot monitor state invalid for account ${accountId}: active socket without promise`);
10749
11856
  }
10750
11857
  const qqCfg = config ? mergeQQBotAccountConfig(config, accountId) : void 0;
@@ -10754,18 +11861,20 @@ async function monitorQQBotProvider(opts = {}) {
10754
11861
  if (!qqCfg.appId || !qqCfg.clientSecret) {
10755
11862
  throw new Error(`QQBot not configured for account ${accountId} (missing appId or clientSecret)`);
10756
11863
  }
10757
- conn.promise = new Promise((resolve3, reject) => {
11864
+ const nextConn = conn ?? getOrCreateConnection(accountId);
11865
+ nextConn.promise = new Promise((resolve3, reject) => {
10758
11866
  let stopped = false;
10759
11867
  const finish = (err) => {
10760
11868
  if (stopped) return;
10761
11869
  stopped = true;
10762
11870
  abortSignal?.removeEventListener("abort", onAbort);
10763
- cleanupSocket(conn);
10764
- conn.sessionId = null;
10765
- conn.lastSeq = null;
10766
- conn.promise = null;
10767
- conn.stop = null;
10768
- conn.reconnectAttempt = 0;
11871
+ cleanupSocket(nextConn);
11872
+ nextConn.connecting = false;
11873
+ nextConn.sessionId = null;
11874
+ nextConn.lastSeq = null;
11875
+ nextConn.promise = null;
11876
+ nextConn.stop = null;
11877
+ nextConn.reconnectAttempt = 0;
10769
11878
  activeConnections.delete(accountId);
10770
11879
  {
10771
11880
  resolve3();
@@ -10775,33 +11884,33 @@ async function monitorQQBotProvider(opts = {}) {
10775
11884
  logger.info("abort signal received, stopping gateway");
10776
11885
  finish();
10777
11886
  };
10778
- conn.stop = () => {
11887
+ nextConn.stop = () => {
10779
11888
  logger.info("stop requested");
10780
11889
  finish();
10781
11890
  };
10782
11891
  const scheduleReconnect = (reason) => {
10783
11892
  if (stopped) return;
10784
- if (conn.reconnectTimer) return;
10785
- const delay = RECONNECT_DELAYS_MS[Math.min(conn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
10786
- conn.reconnectAttempt += 1;
11893
+ if (nextConn.reconnectTimer) return;
11894
+ const delay = RECONNECT_DELAYS_MS[Math.min(nextConn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
11895
+ nextConn.reconnectAttempt += 1;
10787
11896
  logger.warn(`[reconnect] ${reason}; retry in ${delay}ms`);
10788
- conn.reconnectTimer = setTimeout(() => {
10789
- conn.reconnectTimer = null;
11897
+ nextConn.reconnectTimer = setTimeout(() => {
11898
+ nextConn.reconnectTimer = null;
10790
11899
  void connect();
10791
11900
  }, delay);
10792
11901
  };
10793
11902
  const startHeartbeat = (intervalMs) => {
10794
- if (conn.heartbeatTimer) {
10795
- clearInterval(conn.heartbeatTimer);
11903
+ if (nextConn.heartbeatTimer) {
11904
+ clearInterval(nextConn.heartbeatTimer);
10796
11905
  }
10797
- conn.heartbeatTimer = setInterval(() => {
10798
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
10799
- const payload = JSON.stringify({ op: 1, d: conn.lastSeq });
10800
- conn.socket.send(payload);
11906
+ nextConn.heartbeatTimer = setInterval(() => {
11907
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
11908
+ const payload = JSON.stringify({ op: 1, d: nextConn.lastSeq });
11909
+ nextConn.socket.send(payload);
10801
11910
  }, intervalMs);
10802
11911
  };
10803
11912
  const sendIdentify = (token) => {
10804
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
11913
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
10805
11914
  const payload = {
10806
11915
  op: 2,
10807
11916
  d: {
@@ -10810,10 +11919,10 @@ async function monitorQQBotProvider(opts = {}) {
10810
11919
  shard: [0, 1]
10811
11920
  }
10812
11921
  };
10813
- conn.socket.send(JSON.stringify(payload));
11922
+ nextConn.socket.send(JSON.stringify(payload));
10814
11923
  };
10815
11924
  const sendResume = (token, session, seq) => {
10816
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
11925
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
10817
11926
  const payload = {
10818
11927
  op: 6,
10819
11928
  d: {
@@ -10822,11 +11931,14 @@ async function monitorQQBotProvider(opts = {}) {
10822
11931
  seq
10823
11932
  }
10824
11933
  };
10825
- conn.socket.send(JSON.stringify(payload));
11934
+ nextConn.socket.send(JSON.stringify(payload));
10826
11935
  };
10827
- const handleGatewayPayload = async (payload) => {
11936
+ const handleGatewayPayload = async (payload, activeSocket) => {
11937
+ if (stopped || nextConn.socket !== activeSocket) {
11938
+ return;
11939
+ }
10828
11940
  if (typeof payload.s === "number") {
10829
- conn.lastSeq = payload.s;
11941
+ nextConn.lastSeq = payload.s;
10830
11942
  }
10831
11943
  switch (payload.op) {
10832
11944
  case 10: {
@@ -10834,8 +11946,11 @@ async function monitorQQBotProvider(opts = {}) {
10834
11946
  const interval = hello?.heartbeat_interval ?? 3e4;
10835
11947
  startHeartbeat(interval);
10836
11948
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
10837
- if (conn.sessionId && typeof conn.lastSeq === "number") {
10838
- sendResume(token, conn.sessionId, conn.lastSeq);
11949
+ if (stopped || nextConn.socket !== activeSocket) {
11950
+ return;
11951
+ }
11952
+ if (nextConn.sessionId && typeof nextConn.lastSeq === "number") {
11953
+ sendResume(token, nextConn.sessionId, nextConn.lastSeq);
10839
11954
  } else {
10840
11955
  sendIdentify(token);
10841
11956
  }
@@ -10845,14 +11960,18 @@ async function monitorQQBotProvider(opts = {}) {
10845
11960
  setStatus?.({ lastEventAt: Date.now() });
10846
11961
  return;
10847
11962
  case 7:
10848
- cleanupSocket(conn);
11963
+ if (!cleanupSocket(nextConn, activeSocket)) {
11964
+ return;
11965
+ }
10849
11966
  scheduleReconnect("server requested reconnect");
10850
11967
  return;
10851
11968
  case 9:
10852
- conn.sessionId = null;
10853
- conn.lastSeq = null;
11969
+ nextConn.sessionId = null;
11970
+ nextConn.lastSeq = null;
10854
11971
  clearTokenCache(qqCfg.appId);
10855
- cleanupSocket(conn);
11972
+ if (!cleanupSocket(nextConn, activeSocket)) {
11973
+ return;
11974
+ }
10856
11975
  scheduleReconnect("invalid session");
10857
11976
  return;
10858
11977
  case 0: {
@@ -10860,14 +11979,14 @@ async function monitorQQBotProvider(opts = {}) {
10860
11979
  if (eventType === "READY") {
10861
11980
  const ready = payload.d;
10862
11981
  if (ready?.session_id) {
10863
- conn.sessionId = ready.session_id;
11982
+ nextConn.sessionId = ready.session_id;
10864
11983
  }
10865
- conn.reconnectAttempt = 0;
11984
+ nextConn.reconnectAttempt = 0;
10866
11985
  logger.info("gateway ready");
10867
11986
  return;
10868
11987
  }
10869
11988
  if (eventType === "RESUMED") {
10870
- conn.reconnectAttempt = 0;
11989
+ nextConn.reconnectAttempt = 0;
10871
11990
  logger.info("gateway resumed");
10872
11991
  return;
10873
11992
  }
@@ -10888,15 +12007,21 @@ async function monitorQQBotProvider(opts = {}) {
10888
12007
  }
10889
12008
  };
10890
12009
  const connect = async () => {
10891
- if (stopped || conn.connecting) return;
10892
- conn.connecting = true;
12010
+ if (stopped || nextConn.connecting) return;
12011
+ nextConn.connecting = true;
10893
12012
  try {
10894
- cleanupSocket(conn);
12013
+ cleanupSocket(nextConn);
10895
12014
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
12015
+ if (stopped) return;
10896
12016
  const gatewayUrl = await getGatewayUrl(token);
12017
+ if (stopped) return;
10897
12018
  logger.info(`connecting gateway: ${gatewayUrl}`);
10898
12019
  const ws = new WebSocket(gatewayUrl);
10899
- conn.socket = ws;
12020
+ nextConn.socket = ws;
12021
+ if (stopped) {
12022
+ cleanupSocket(nextConn, ws);
12023
+ return;
12024
+ }
10900
12025
  ws.on("open", () => {
10901
12026
  logger.info("gateway socket opened");
10902
12027
  });
@@ -10909,24 +12034,29 @@ async function monitorQQBotProvider(opts = {}) {
10909
12034
  logger.warn(`failed to parse gateway payload: ${String(err)}`);
10910
12035
  return;
10911
12036
  }
10912
- void handleGatewayPayload(payload).catch((err) => {
12037
+ void handleGatewayPayload(payload, ws).catch((err) => {
10913
12038
  logger.error(`gateway dispatch error: ${String(err)}`);
10914
12039
  });
10915
12040
  });
10916
12041
  ws.on("close", (code, reason) => {
12042
+ if (!cleanupSocket(nextConn, ws)) {
12043
+ return;
12044
+ }
10917
12045
  logger.warn(`gateway socket closed (${code}) ${String(reason)}`);
10918
- cleanupSocket(conn);
10919
12046
  scheduleReconnect("socket closed");
10920
12047
  });
10921
12048
  ws.on("error", (err) => {
12049
+ if (stopped || nextConn.socket !== ws) {
12050
+ return;
12051
+ }
10922
12052
  logger.error(`gateway socket error: ${String(err)}`);
10923
12053
  });
10924
12054
  } catch (err) {
10925
12055
  logger.error(`gateway connect failed: ${formatGatewayConnectError(err)}`);
10926
- cleanupSocket(conn);
12056
+ cleanupSocket(nextConn);
10927
12057
  scheduleReconnect("connect failed");
10928
12058
  } finally {
10929
- conn.connecting = false;
12059
+ nextConn.connecting = false;
10930
12060
  }
10931
12061
  };
10932
12062
  if (abortSignal?.aborted) {
@@ -10936,7 +12066,7 @@ async function monitorQQBotProvider(opts = {}) {
10936
12066
  abortSignal?.addEventListener("abort", onAbort, { once: true });
10937
12067
  void connect();
10938
12068
  });
10939
- return conn.promise;
12069
+ return nextConn.promise;
10940
12070
  }
10941
12071
  function stopQQBotMonitorForAccount(accountId = DEFAULT_ACCOUNT_ID) {
10942
12072
  const conn = activeConnections.get(accountId);
@@ -10973,7 +12103,8 @@ function resolveQQBotAccount(params) {
10973
12103
  configured,
10974
12104
  appId: credentials?.appId,
10975
12105
  markdownSupport: merged.markdownSupport ?? true,
10976
- c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only"
12106
+ c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only",
12107
+ c2cMarkdownChunkStrategy: merged.c2cMarkdownChunkStrategy ?? "markdown-block"
10977
12108
  };
10978
12109
  }
10979
12110
  var qqbotPlugin = {
@@ -11051,6 +12182,10 @@ var qqbotPlugin = {
11051
12182
  defaultAccount: { type: "string" },
11052
12183
  appId: { type: ["string", "number"] },
11053
12184
  clientSecret: { type: "string" },
12185
+ displayAliases: {
12186
+ type: "object",
12187
+ additionalProperties: { type: "string" }
12188
+ },
11054
12189
  asr: {
11055
12190
  type: "object",
11056
12191
  additionalProperties: false,
@@ -11066,6 +12201,10 @@ var qqbotPlugin = {
11066
12201
  type: "string",
11067
12202
  enum: ["passive", "proactive-table-only", "proactive-all"]
11068
12203
  },
12204
+ c2cMarkdownChunkStrategy: {
12205
+ type: "string",
12206
+ enum: ["markdown-block", "length"]
12207
+ },
11069
12208
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11070
12209
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11071
12210
  requireMention: { type: "boolean" },
@@ -11096,6 +12235,10 @@ var qqbotPlugin = {
11096
12235
  enabled: { type: "boolean" },
11097
12236
  appId: { type: ["string", "number"] },
11098
12237
  clientSecret: { type: "string" },
12238
+ displayAliases: {
12239
+ type: "object",
12240
+ additionalProperties: { type: "string" }
12241
+ },
11099
12242
  asr: {
11100
12243
  type: "object",
11101
12244
  additionalProperties: false,
@@ -11111,6 +12254,10 @@ var qqbotPlugin = {
11111
12254
  type: "string",
11112
12255
  enum: ["passive", "proactive-table-only", "proactive-all"]
11113
12256
  },
12257
+ c2cMarkdownChunkStrategy: {
12258
+ type: "string",
12259
+ enum: ["markdown-block", "length"]
12260
+ },
11114
12261
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11115
12262
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11116
12263
  requireMention: { type: "boolean" },
@@ -11262,7 +12409,9 @@ var qqbotPlugin = {
11262
12409
  ctx.log?.info(`[qqbot] starting gateway for account ${ctx.accountId}`);
11263
12410
  if (ctx.runtime) {
11264
12411
  const candidate = ctx.runtime;
11265
- if (candidate.channel?.routing?.resolveAgentRoute && candidate.channel?.reply?.dispatchReplyFromConfig) {
12412
+ const hasRouting = Boolean(candidate.channel?.routing?.resolveAgentRoute);
12413
+ const hasReply = Boolean(candidate.channel?.reply?.dispatchReplyWithDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyFromConfig);
12414
+ if (hasRouting && hasReply) {
11266
12415
  setQQBotRuntime(ctx.runtime);
11267
12416
  }
11268
12417
  }
@@ -11298,6 +12447,10 @@ var plugin = {
11298
12447
  defaultAccount: { type: "string" },
11299
12448
  appId: { type: ["string", "number"] },
11300
12449
  clientSecret: { type: "string" },
12450
+ displayAliases: {
12451
+ type: "object",
12452
+ additionalProperties: { type: "string" }
12453
+ },
11301
12454
  asr: {
11302
12455
  type: "object",
11303
12456
  additionalProperties: false,
@@ -11313,6 +12466,10 @@ var plugin = {
11313
12466
  type: "string",
11314
12467
  enum: ["passive", "proactive-table-only", "proactive-all"]
11315
12468
  },
12469
+ c2cMarkdownChunkStrategy: {
12470
+ type: "string",
12471
+ enum: ["markdown-block", "length"]
12472
+ },
11316
12473
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11317
12474
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11318
12475
  requireMention: { type: "boolean" },
@@ -11343,6 +12500,10 @@ var plugin = {
11343
12500
  enabled: { type: "boolean" },
11344
12501
  appId: { type: ["string", "number"] },
11345
12502
  clientSecret: { type: "string" },
12503
+ displayAliases: {
12504
+ type: "object",
12505
+ additionalProperties: { type: "string" }
12506
+ },
11346
12507
  asr: {
11347
12508
  type: "object",
11348
12509
  additionalProperties: false,
@@ -11358,6 +12519,10 @@ var plugin = {
11358
12519
  type: "string",
11359
12520
  enum: ["passive", "proactive-table-only", "proactive-all"]
11360
12521
  },
12522
+ c2cMarkdownChunkStrategy: {
12523
+ type: "string",
12524
+ enum: ["markdown-block", "length"]
12525
+ },
11361
12526
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11362
12527
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11363
12528
  requireMention: { type: "boolean" },