@openclaw-china/qqbot 2026.3.12 → 2026.3.18

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,21 @@ 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");
4231
+ var QQBotTypingHeartbeatModeSchema = external_exports.enum(["none", "idle", "always"]).optional().default("idle");
4232
+ var DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE = "idle";
4233
+ var DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS = 5e3;
4234
+ var DEFAULT_QQBOT_TYPING_INPUT_SECONDS = 60;
4227
4235
  var QQBotAccountSchema = external_exports.object({
4228
4236
  name: external_exports.string().optional(),
4229
4237
  enabled: external_exports.boolean().optional(),
4230
4238
  appId: optionalCoercedString,
4231
4239
  clientSecret: optionalCoercedString,
4240
+ displayAliases: displayAliasesSchema,
4232
4241
  asr: external_exports.object({
4233
4242
  enabled: external_exports.boolean().optional().default(false),
4234
4243
  appId: optionalCoercedString,
@@ -4237,6 +4246,14 @@ var QQBotAccountSchema = external_exports.object({
4237
4246
  }).optional(),
4238
4247
  markdownSupport: external_exports.boolean().optional().default(true),
4239
4248
  c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4249
+ c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
4250
+ typingHeartbeatMode: QQBotTypingHeartbeatModeSchema,
4251
+ typingHeartbeatIntervalMs: external_exports.number().int().positive().optional().default(
4252
+ DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS
4253
+ ),
4254
+ typingInputSeconds: external_exports.number().int().positive().optional().default(
4255
+ DEFAULT_QQBOT_TYPING_INPUT_SECONDS
4256
+ ),
4240
4257
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4241
4258
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4242
4259
  requireMention: external_exports.boolean().optional().default(true),
@@ -4261,6 +4278,21 @@ QQBotAccountSchema.extend({
4261
4278
  var DEFAULT_INBOUND_MEDIA_DIR = join(homedir(), ".openclaw", "media", "qqbot", "inbound");
4262
4279
  var DEFAULT_INBOUND_MEDIA_KEEP_DAYS = 7;
4263
4280
  var DEFAULT_INBOUND_MEDIA_TEMP_DIR = join(tmpdir(), "qqbot-media");
4281
+ function normalizeDisplayAliasesMap(raw) {
4282
+ if (!raw || typeof raw !== "object") {
4283
+ return {};
4284
+ }
4285
+ const aliases = {};
4286
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
4287
+ const key = rawKey.trim();
4288
+ const value = toTrimmedString(rawValue);
4289
+ if (!key || !value) {
4290
+ continue;
4291
+ }
4292
+ aliases[key] = value;
4293
+ }
4294
+ return aliases;
4295
+ }
4264
4296
  function resolveInboundMediaDir(config) {
4265
4297
  return String(config?.inboundMedia?.dir ?? "").trim() || DEFAULT_INBOUND_MEDIA_DIR;
4266
4298
  }
@@ -4271,6 +4303,15 @@ function resolveInboundMediaKeepDays(config) {
4271
4303
  function resolveQQBotAutoSendLocalPathMedia(config) {
4272
4304
  return config?.autoSendLocalPathMedia ?? true;
4273
4305
  }
4306
+ function resolveQQBotTypingHeartbeatMode(config) {
4307
+ return config?.typingHeartbeatMode ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE;
4308
+ }
4309
+ function resolveQQBotTypingHeartbeatIntervalMs(config) {
4310
+ return config?.typingHeartbeatIntervalMs ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS;
4311
+ }
4312
+ function resolveQQBotTypingInputSeconds(config) {
4313
+ return config?.typingInputSeconds ?? DEFAULT_QQBOT_TYPING_INPUT_SECONDS;
4314
+ }
4274
4315
  function resolveInboundMediaTempDir() {
4275
4316
  return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
4276
4317
  }
@@ -4301,7 +4342,15 @@ function mergeQQBotAccountConfig(cfg, accountId) {
4301
4342
  const base = cfg.channels?.qqbot ?? {};
4302
4343
  const { accounts: _ignored, defaultAccount: _ignored2, ...baseConfig } = base;
4303
4344
  const account = resolveAccountConfig(cfg, accountId) ?? {};
4304
- return { ...baseConfig, ...account };
4345
+ const mergedDisplayAliases = {
4346
+ ...normalizeDisplayAliasesMap(baseConfig.displayAliases),
4347
+ ...normalizeDisplayAliasesMap(account.displayAliases)
4348
+ };
4349
+ return {
4350
+ ...baseConfig,
4351
+ ...account,
4352
+ ...Object.keys(mergedDisplayAliases).length > 0 ? { displayAliases: mergedDisplayAliases } : {}
4353
+ };
4305
4354
  }
4306
4355
  function resolveQQBotCredentials(config) {
4307
4356
  const appId = toTrimmedString(config?.appId);
@@ -6518,6 +6567,7 @@ var CHANNEL_ORDER = [
6518
6567
  "qqbot",
6519
6568
  "wecom",
6520
6569
  "wecom-app",
6570
+ "wecom-kf",
6521
6571
  "feishu-china"
6522
6572
  ];
6523
6573
  var CHANNEL_DISPLAY_LABELS = {
@@ -6525,6 +6575,7 @@ var CHANNEL_DISPLAY_LABELS = {
6525
6575
  "feishu-china": "Feishu\uFF08\u98DE\u4E66\uFF09",
6526
6576
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
6527
6577
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
6578
+ "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
6528
6579
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
6529
6580
  };
6530
6581
  var CHANNEL_GUIDE_LINKS = {
@@ -6532,6 +6583,7 @@ var CHANNEL_GUIDE_LINKS = {
6532
6583
  "feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
6533
6584
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
6534
6585
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
6586
+ "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
6535
6587
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
6536
6588
  };
6537
6589
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -6740,6 +6792,8 @@ function isChannelConfigured(cfg, channelId) {
6740
6792
  return hasWecomWsCredentialPair(channelCfg);
6741
6793
  case "wecom-app":
6742
6794
  return hasTokenPair(channelCfg);
6795
+ case "wecom-kf":
6796
+ return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6743
6797
  default:
6744
6798
  return false;
6745
6799
  }
@@ -6996,6 +7050,55 @@ async function configureWecomApp(prompter, cfg) {
6996
7050
  patch.asr = asr;
6997
7051
  return mergeChannelConfig(cfg, "wecom-app", patch);
6998
7052
  }
7053
+ async function configureWecomKf(prompter, cfg) {
7054
+ section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
7055
+ showGuideLink("wecom-kf");
7056
+ const existing = getChannelConfig(cfg, "wecom-kf");
7057
+ const webhookPath = await prompter.askText({
7058
+ label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
7059
+ defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
7060
+ required: true
7061
+ });
7062
+ const token = await prompter.askSecret({
7063
+ label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 Token",
7064
+ existingValue: toTrimmedString2(existing.token),
7065
+ required: true
7066
+ });
7067
+ const encodingAESKey = await prompter.askSecret({
7068
+ label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 EncodingAESKey",
7069
+ existingValue: toTrimmedString2(existing.encodingAESKey),
7070
+ required: true
7071
+ });
7072
+ const corpId = await prompter.askText({
7073
+ label: "corpId",
7074
+ defaultValue: toTrimmedString2(existing.corpId),
7075
+ required: true
7076
+ });
7077
+ const corpSecret = await prompter.askSecret({
7078
+ label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
7079
+ existingValue: toTrimmedString2(existing.corpSecret),
7080
+ required: true
7081
+ });
7082
+ const openKfId = await prompter.askText({
7083
+ label: "open_kfid",
7084
+ defaultValue: toTrimmedString2(existing.openKfId),
7085
+ required: true
7086
+ });
7087
+ const welcomeText = await prompter.askText({
7088
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7089
+ defaultValue: toTrimmedString2(existing.welcomeText),
7090
+ required: false
7091
+ });
7092
+ return mergeChannelConfig(cfg, "wecom-kf", {
7093
+ webhookPath,
7094
+ token,
7095
+ encodingAESKey,
7096
+ corpId,
7097
+ corpSecret,
7098
+ openKfId,
7099
+ welcomeText: welcomeText || void 0
7100
+ });
7101
+ }
6999
7102
  async function configureQQBot(prompter, cfg) {
7000
7103
  section("\u914D\u7F6E QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09");
7001
7104
  showGuideLink("qqbot");
@@ -7052,6 +7155,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
7052
7155
  return configureWecom(prompter, cfg);
7053
7156
  case "wecom-app":
7054
7157
  return configureWecomApp(prompter, cfg);
7158
+ case "wecom-kf":
7159
+ return configureWecomKf(prompter, cfg);
7055
7160
  case "qqbot":
7056
7161
  return configureQQBot(prompter, cfg);
7057
7162
  default:
@@ -7192,6 +7297,7 @@ var SUPPORTED_CHANNELS = [
7192
7297
  "feishu-china",
7193
7298
  "wecom",
7194
7299
  "wecom-app",
7300
+ "wecom-kf",
7195
7301
  "qqbot"
7196
7302
  ];
7197
7303
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -8015,21 +8121,22 @@ async function readMediaWithConfig(source, options) {
8015
8121
 
8016
8122
  // src/outbound.ts
8017
8123
  function stripPrefix(value, prefix) {
8018
- return value.startsWith(prefix) ? value.slice(prefix.length) : value;
8124
+ return value.slice(0, prefix.length).toLowerCase() === prefix ? value.slice(prefix.length) : value;
8019
8125
  }
8020
8126
  function parseTarget(to) {
8021
8127
  let raw = to.trim();
8022
8128
  raw = stripPrefix(raw, "qqbot:");
8023
- if (raw.startsWith("group:")) {
8129
+ const normalizedRaw = raw.toLowerCase();
8130
+ if (normalizedRaw.startsWith("group:")) {
8024
8131
  return { kind: "group", id: raw.slice("group:".length) };
8025
8132
  }
8026
- if (raw.startsWith("channel:")) {
8133
+ if (normalizedRaw.startsWith("channel:")) {
8027
8134
  return { kind: "channel", id: raw.slice("channel:".length) };
8028
8135
  }
8029
- if (raw.startsWith("user:")) {
8136
+ if (normalizedRaw.startsWith("user:")) {
8030
8137
  return { kind: "c2c", id: raw.slice("user:".length) };
8031
8138
  }
8032
- if (raw.startsWith("c2c:")) {
8139
+ if (normalizedRaw.startsWith("c2c:")) {
8033
8140
  return { kind: "c2c", id: raw.slice("c2c:".length) };
8034
8141
  }
8035
8142
  return { kind: "c2c", id: raw };
@@ -9077,6 +9184,51 @@ function getQQBotRuntime() {
9077
9184
  return runtime;
9078
9185
  }
9079
9186
  var sessionDispatchQueue = /* @__PURE__ */ new Map();
9187
+ var QQBOT_ABORT_TRIGGERS = /* @__PURE__ */ new Set([
9188
+ "stop",
9189
+ "esc",
9190
+ "abort",
9191
+ "wait",
9192
+ "exit",
9193
+ "interrupt",
9194
+ "detente",
9195
+ "deten",
9196
+ "det\xE9n",
9197
+ "arrete",
9198
+ "arr\xEAte",
9199
+ "\u505C\u6B62",
9200
+ "\u3084\u3081\u3066",
9201
+ "\u6B62\u3081\u3066",
9202
+ "\u0930\u0941\u0915\u094B",
9203
+ "\u062A\u0648\u0642\u0641",
9204
+ "\u0441\u0442\u043E\u043F",
9205
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C",
9206
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438",
9207
+ "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C",
9208
+ "\u043F\u0440\u0435\u043A\u0440\u0430\u0442\u0438",
9209
+ "halt",
9210
+ "anhalten",
9211
+ "aufh\xF6ren",
9212
+ "hoer auf",
9213
+ "stopp",
9214
+ "pare",
9215
+ "stop openclaw",
9216
+ "openclaw stop",
9217
+ "stop action",
9218
+ "stop current action",
9219
+ "stop run",
9220
+ "stop current run",
9221
+ "stop agent",
9222
+ "stop the agent",
9223
+ "stop don't do anything",
9224
+ "stop dont do anything",
9225
+ "stop do not do anything",
9226
+ "stop doing anything",
9227
+ "do not do that",
9228
+ "please stop",
9229
+ "stop please"
9230
+ ]);
9231
+ var QQBOT_ABORT_TRAILING_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
9080
9232
  function resolveQQBotRouteSessionKey(route) {
9081
9233
  const effectiveSessionKey = route.effectiveSessionKey?.trim();
9082
9234
  if (effectiveSessionKey) {
@@ -9088,6 +9240,100 @@ function buildSessionDispatchQueueKey(route) {
9088
9240
  const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
9089
9241
  return `${accountId}:${resolveQQBotRouteSessionKey(route)}`;
9090
9242
  }
9243
+ function createSessionDispatchState() {
9244
+ return {
9245
+ queue: [],
9246
+ processing: false,
9247
+ immediateActiveCount: 0,
9248
+ waiters: [],
9249
+ abortGeneration: 0
9250
+ };
9251
+ }
9252
+ function getSessionDispatchState(queueKey) {
9253
+ const existing = sessionDispatchQueue.get(queueKey);
9254
+ if (existing) {
9255
+ return existing;
9256
+ }
9257
+ const created = createSessionDispatchState();
9258
+ sessionDispatchQueue.set(queueKey, created);
9259
+ return created;
9260
+ }
9261
+ function signalSessionDispatchState(state) {
9262
+ const waiters = state.waiters.splice(0, state.waiters.length);
9263
+ for (const resolve3 of waiters) {
9264
+ resolve3();
9265
+ }
9266
+ }
9267
+ function waitForSessionDispatchState(state) {
9268
+ return new Promise((resolve3) => {
9269
+ state.waiters.push(resolve3);
9270
+ });
9271
+ }
9272
+ function cleanupSessionDispatchState(queueKey, state) {
9273
+ if (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0 || state.waiters.length > 0) {
9274
+ return;
9275
+ }
9276
+ if (sessionDispatchQueue.get(queueKey) === state) {
9277
+ sessionDispatchQueue.delete(queueKey);
9278
+ }
9279
+ }
9280
+ function hasSessionDispatchBacklog(queueKey) {
9281
+ const state = sessionDispatchQueue.get(queueKey);
9282
+ return Boolean(
9283
+ state && (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0)
9284
+ );
9285
+ }
9286
+ async function processSerializedSessionDispatchQueue(queueKey) {
9287
+ const state = sessionDispatchQueue.get(queueKey);
9288
+ if (!state || state.processing) {
9289
+ return;
9290
+ }
9291
+ state.processing = true;
9292
+ try {
9293
+ for (; ; ) {
9294
+ if (state.immediateActiveCount > 0) {
9295
+ await waitForSessionDispatchState(state);
9296
+ continue;
9297
+ }
9298
+ const next = state.queue.shift();
9299
+ if (!next) {
9300
+ break;
9301
+ }
9302
+ try {
9303
+ await next.task();
9304
+ next.resolve();
9305
+ } catch (err) {
9306
+ next.reject(err);
9307
+ }
9308
+ }
9309
+ } finally {
9310
+ state.processing = false;
9311
+ if (state.queue.length > 0) {
9312
+ void processSerializedSessionDispatchQueue(queueKey);
9313
+ return;
9314
+ }
9315
+ cleanupSessionDispatchState(queueKey, state);
9316
+ }
9317
+ }
9318
+ function dropQueuedSessionDispatches(queueKey) {
9319
+ const state = sessionDispatchQueue.get(queueKey);
9320
+ if (!state || state.queue.length === 0) {
9321
+ return 0;
9322
+ }
9323
+ const dropped = state.queue.splice(0, state.queue.length);
9324
+ for (const item of dropped) {
9325
+ item.resolve();
9326
+ }
9327
+ signalSessionDispatchState(state);
9328
+ cleanupSessionDispatchState(queueKey, state);
9329
+ return dropped.length;
9330
+ }
9331
+ function markSessionDispatchAbort(queueKey) {
9332
+ const state = getSessionDispatchState(queueKey);
9333
+ state.abortGeneration += 1;
9334
+ signalSessionDispatchState(state);
9335
+ return state.abortGeneration;
9336
+ }
9091
9337
  function normalizeQQBotSessionKeyPart(value) {
9092
9338
  const trimmed = value.trim();
9093
9339
  return trimmed ? trimmed.toLowerCase() : "unknown";
@@ -9123,22 +9369,163 @@ function normalizeQQBotReplyTarget(value) {
9123
9369
  return /^(user|group|channel):/i.test(trimmed) ? trimmed : void 0;
9124
9370
  }
9125
9371
  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);
9372
+ const state = getSessionDispatchState(queueKey);
9373
+ return new Promise((resolve3, reject) => {
9374
+ let settled = false;
9375
+ state.queue.push({
9376
+ task: async () => {
9377
+ if (settled) {
9378
+ return;
9379
+ }
9380
+ try {
9381
+ const result = await task();
9382
+ if (!settled) {
9383
+ settled = true;
9384
+ resolve3(result);
9385
+ }
9386
+ } catch (err) {
9387
+ if (!settled) {
9388
+ settled = true;
9389
+ reject(err);
9390
+ }
9391
+ }
9392
+ },
9393
+ resolve: () => {
9394
+ if (settled) {
9395
+ return;
9396
+ }
9397
+ settled = true;
9398
+ resolve3(void 0);
9399
+ },
9400
+ reject: (err) => {
9401
+ if (settled) {
9402
+ return;
9403
+ }
9404
+ settled = true;
9405
+ reject(err);
9406
+ }
9407
+ });
9408
+ signalSessionDispatchState(state);
9409
+ void processSerializedSessionDispatchQueue(queueKey);
9410
+ });
9411
+ }
9412
+ async function runImmediateSessionDispatch(queueKey, task) {
9413
+ const state = getSessionDispatchState(queueKey);
9414
+ state.immediateActiveCount += 1;
9415
+ signalSessionDispatchState(state);
9130
9416
  try {
9131
- return await run;
9417
+ return await task();
9132
9418
  } finally {
9133
- if (sessionDispatchQueue.get(queueKey) === cleanup) {
9134
- sessionDispatchQueue.delete(queueKey);
9419
+ state.immediateActiveCount = Math.max(0, state.immediateActiveCount - 1);
9420
+ signalSessionDispatchState(state);
9421
+ if (state.queue.length > 0) {
9422
+ void processSerializedSessionDispatchQueue(queueKey);
9135
9423
  }
9424
+ cleanupSessionDispatchState(queueKey, state);
9425
+ }
9426
+ }
9427
+ function normalizeQQBotAbortTriggerText(text) {
9428
+ return text.trim().toLowerCase().replace(/[’`]/g, "'").replace(/\s+/g, " ").replace(QQBOT_ABORT_TRAILING_PUNCTUATION_RE, "").trim();
9429
+ }
9430
+ function isQQBotAbortTrigger(text) {
9431
+ if (!text) {
9432
+ return false;
9136
9433
  }
9434
+ return QQBOT_ABORT_TRIGGERS.has(normalizeQQBotAbortTriggerText(text));
9435
+ }
9436
+ function isQQBotFastAbortCommandText(text) {
9437
+ if (!text) {
9438
+ return false;
9439
+ }
9440
+ const normalized = text.trim();
9441
+ if (!normalized) {
9442
+ return false;
9443
+ }
9444
+ const lower = normalized.toLowerCase();
9445
+ return lower === "/stop" || normalizeQQBotAbortTriggerText(lower) === "/stop" || isQQBotAbortTrigger(lower);
9137
9446
  }
9138
9447
  function toString(value) {
9139
9448
  if (typeof value === "string" && value.trim()) return value;
9140
9449
  return void 0;
9141
9450
  }
9451
+ function asRecord(value) {
9452
+ if (!value || typeof value !== "object") {
9453
+ return void 0;
9454
+ }
9455
+ return value;
9456
+ }
9457
+ function normalizeQQBotDisplayAliasesMap(raw) {
9458
+ if (!raw || typeof raw !== "object") {
9459
+ return {};
9460
+ }
9461
+ const aliases = {};
9462
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
9463
+ const key = rawKey.trim();
9464
+ const value = toString(rawValue);
9465
+ if (!key || !value) {
9466
+ continue;
9467
+ }
9468
+ aliases[key] = value;
9469
+ }
9470
+ return aliases;
9471
+ }
9472
+ function resolveQQBotDisplayAliasMaps(cfg, accountId) {
9473
+ const qqbot = cfg?.channels?.qqbot;
9474
+ return {
9475
+ globalAliases: normalizeQQBotDisplayAliasesMap(qqbot?.displayAliases),
9476
+ accountAliases: normalizeQQBotDisplayAliasesMap(qqbot?.accounts?.[accountId]?.displayAliases)
9477
+ };
9478
+ }
9479
+ function resolveQQBotSenderName(params) {
9480
+ const { inbound, cfg, accountId } = params;
9481
+ const stableId = inbound.c2cOpenid?.trim() || inbound.senderId.trim();
9482
+ const { globalAliases, accountAliases } = resolveQQBotDisplayAliasMaps(cfg, accountId);
9483
+ if (inbound.type === "direct") {
9484
+ const knownTarget = stableId ? getKnownQQBotTarget({ accountId, target: `user:${stableId}` }) : void 0;
9485
+ const knownTargetDisplayName = knownTarget?.displayName?.trim();
9486
+ if (knownTargetDisplayName) {
9487
+ return {
9488
+ displayName: knownTargetDisplayName,
9489
+ persistentDisplayName: knownTargetDisplayName,
9490
+ source: "known-target",
9491
+ knownTargetDisplayName
9492
+ };
9493
+ }
9494
+ const aliasKeys = [...new Set([`user:${stableId}`, stableId, inbound.senderId.trim()].filter(Boolean))];
9495
+ for (const aliasKey of aliasKeys) {
9496
+ const alias = accountAliases[aliasKey];
9497
+ if (alias) {
9498
+ return {
9499
+ displayName: alias,
9500
+ persistentDisplayName: alias,
9501
+ source: "account-alias",
9502
+ matchedAliasKey: aliasKey
9503
+ };
9504
+ }
9505
+ }
9506
+ for (const aliasKey of aliasKeys) {
9507
+ const alias = globalAliases[aliasKey];
9508
+ if (alias) {
9509
+ return {
9510
+ displayName: alias,
9511
+ persistentDisplayName: alias,
9512
+ source: "global-alias",
9513
+ matchedAliasKey: aliasKey
9514
+ };
9515
+ }
9516
+ }
9517
+ }
9518
+ return {
9519
+ displayName: stableId,
9520
+ source: "stable-id"
9521
+ };
9522
+ }
9523
+ function logQQBotSenderNameResolution(params) {
9524
+ const { logger, inbound, accountId, resolution } = params;
9525
+ logger.debug?.(
9526
+ `[display-name] accountId=${accountId} type=${inbound.type} senderId=${inbound.senderId} knownTarget=${resolution.knownTargetDisplayName ?? "-"} alias=${resolution.matchedAliasKey ?? "-"} final=${JSON.stringify(resolution.displayName)} source=${resolution.source}`
9527
+ );
9528
+ }
9142
9529
  function toNumber2(value) {
9143
9530
  if (typeof value === "number" && Number.isFinite(value)) return value;
9144
9531
  if (typeof value === "string") {
@@ -9256,6 +9643,37 @@ function startLongTaskNoticeTimer(params) {
9256
9643
  }
9257
9644
  };
9258
9645
  }
9646
+ function startQQBotTypingHeartbeat(params) {
9647
+ const { intervalMs, renew, shouldRenew } = params;
9648
+ let stopped = false;
9649
+ let timer = null;
9650
+ let renewalInFlight = false;
9651
+ const clear = () => {
9652
+ if (!timer) return;
9653
+ clearInterval(timer);
9654
+ timer = null;
9655
+ };
9656
+ const stop = () => {
9657
+ if (stopped) return;
9658
+ stopped = true;
9659
+ clear();
9660
+ };
9661
+ if (intervalMs > 0) {
9662
+ timer = setInterval(() => {
9663
+ if (stopped || renewalInFlight) return;
9664
+ if (shouldRenew && !shouldRenew()) return;
9665
+ renewalInFlight = true;
9666
+ void renew().catch(() => void 0).finally(() => {
9667
+ renewalInFlight = false;
9668
+ });
9669
+ }, intervalMs);
9670
+ timer.unref?.();
9671
+ }
9672
+ return {
9673
+ stop,
9674
+ dispose: stop
9675
+ };
9676
+ }
9259
9677
  function isHttpUrl3(value) {
9260
9678
  return /^https?:\/\//i.test(value);
9261
9679
  }
@@ -9522,14 +9940,13 @@ function parseC2CMessage(data, fallbackEventId) {
9522
9940
  const id = toString(payload.id);
9523
9941
  const eventId = resolveEventId(payload, fallbackEventId);
9524
9942
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9525
- const author = payload.author ?? {};
9943
+ const author = asRecord(payload.author) ?? {};
9526
9944
  const senderId = toString(author.user_openid);
9527
9945
  if (!text && attachments.length === 0 || !id || !senderId) return null;
9528
9946
  return {
9529
9947
  type: "direct",
9530
9948
  senderId,
9531
9949
  c2cOpenid: senderId,
9532
- senderName: toString(author.username),
9533
9950
  content: text,
9534
9951
  attachments: attachments.length > 0 ? attachments : void 0,
9535
9952
  messageId: id,
@@ -9546,13 +9963,12 @@ function parseGroupMessage(data, fallbackEventId) {
9546
9963
  const eventId = resolveEventId(payload, fallbackEventId);
9547
9964
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9548
9965
  const groupOpenid = toString(payload.group_openid);
9549
- const author = payload.author ?? {};
9966
+ const author = asRecord(payload.author) ?? {};
9550
9967
  const senderId = toString(author.member_openid);
9551
9968
  if (!text && attachments.length === 0 || !id || !senderId || !groupOpenid) return null;
9552
9969
  return {
9553
9970
  type: "group",
9554
9971
  senderId,
9555
- senderName: toString(author.nickname) ?? toString(author.username),
9556
9972
  content: text,
9557
9973
  attachments: attachments.length > 0 ? attachments : void 0,
9558
9974
  messageId: id,
@@ -9570,13 +9986,12 @@ function parseChannelMessage(data, fallbackEventId) {
9570
9986
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9571
9987
  const channelId = toString(payload.channel_id);
9572
9988
  const guildId = toString(payload.guild_id);
9573
- const author = payload.author ?? {};
9989
+ const author = asRecord(payload.author) ?? {};
9574
9990
  const senderId = toString(author.id);
9575
9991
  if (!text && attachments.length === 0 || !id || !senderId || !channelId) return null;
9576
9992
  return {
9577
9993
  type: "channel",
9578
9994
  senderId,
9579
- senderName: toString(author.username),
9580
9995
  content: text,
9581
9996
  attachments: attachments.length > 0 ? attachments : void 0,
9582
9997
  messageId: id,
@@ -9594,13 +10009,12 @@ function parseDirectMessage(data, fallbackEventId) {
9594
10009
  const eventId = resolveEventId(payload, fallbackEventId);
9595
10010
  const timestamp = toNumber2(payload.timestamp) ?? Date.now();
9596
10011
  const guildId = toString(payload.guild_id);
9597
- const author = payload.author ?? {};
10012
+ const author = asRecord(payload.author) ?? {};
9598
10013
  const senderId = toString(author.id);
9599
10014
  if (!text && attachments.length === 0 || !id || !senderId) return null;
9600
10015
  return {
9601
10016
  type: "direct",
9602
10017
  senderId,
9603
- senderName: toString(author.username),
9604
10018
  content: text,
9605
10019
  attachments: attachments.length > 0 ? attachments : void 0,
9606
10020
  messageId: id,
@@ -9675,7 +10089,7 @@ function resolveEnvelopeFrom(event) {
9675
10089
  return event.senderName?.trim() || event.senderId;
9676
10090
  }
9677
10091
  function resolveKnownQQBotTargetFromInbound(params) {
9678
- const { inbound, accountId } = params;
10092
+ const { inbound, accountId, persistentDisplayName } = params;
9679
10093
  if (inbound.type === "direct") {
9680
10094
  if (!inbound.c2cOpenid?.trim()) {
9681
10095
  return void 0;
@@ -9684,7 +10098,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9684
10098
  accountId,
9685
10099
  kind: "user",
9686
10100
  target: `user:${inbound.c2cOpenid}`,
9687
- displayName: inbound.senderName,
10101
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9688
10102
  sourceChatType: "direct",
9689
10103
  firstSeenAt: inbound.timestamp,
9690
10104
  lastSeenAt: inbound.timestamp
@@ -9695,7 +10109,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9695
10109
  accountId,
9696
10110
  kind: "group",
9697
10111
  target: `group:${inbound.groupOpenid}`,
9698
- displayName: inbound.senderName,
10112
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9699
10113
  sourceChatType: "group",
9700
10114
  firstSeenAt: inbound.timestamp,
9701
10115
  lastSeenAt: inbound.timestamp
@@ -9706,7 +10120,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
9706
10120
  accountId,
9707
10121
  kind: "channel",
9708
10122
  target: `channel:${inbound.channelId}`,
9709
- displayName: inbound.senderName,
10123
+ ...persistentDisplayName ? { displayName: persistentDisplayName } : {},
9710
10124
  sourceChatType: "channel",
9711
10125
  firstSeenAt: inbound.timestamp,
9712
10126
  lastSeenAt: inbound.timestamp
@@ -9821,6 +10235,14 @@ var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_v
9821
10235
  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
10236
  var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
9823
10237
  var MARKDOWN_TABLE_SEPARATOR_RE = /^\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?)?\|?$/;
10238
+ var MARKDOWN_THEMATIC_BREAK_RE = /^\s{0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/;
10239
+ var MARKDOWN_ATX_HEADING_RE = /^\s{0,3}#{1,6}\s+\S/;
10240
+ var MARKDOWN_BLOCKQUOTE_RE = /^\s{0,3}>\s?/;
10241
+ var MARKDOWN_FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
10242
+ var MARKDOWN_LIST_ITEM_RE = /^\s*(?:[-+*]|\d+\.)\s+/;
10243
+ var MARKDOWN_LIST_CONTINUATION_RE = /^\s{2,}\S/;
10244
+ var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[^~\n]+~~|\*[^*\n]+\*)/;
10245
+ var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
9824
10246
  var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
9825
10247
  var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
9826
10248
  function extractFinalBlocks(text) {
@@ -9889,8 +10311,9 @@ function evaluateReplyFinalOnlyDelivery(params) {
9889
10311
  }
9890
10312
  function isQQBotC2CTarget(to) {
9891
10313
  const trimmed = to.trim();
9892
- const raw = trimmed.startsWith("qqbot:") ? trimmed.slice("qqbot:".length) : trimmed;
9893
- return !raw.startsWith("group:") && !raw.startsWith("channel:");
10314
+ const raw = trimmed.slice(0, "qqbot:".length).toLowerCase() === "qqbot:" ? trimmed.slice("qqbot:".length) : trimmed;
10315
+ const normalizedRaw = raw.toLowerCase();
10316
+ return !normalizedRaw.startsWith("group:") && !normalizedRaw.startsWith("channel:");
9894
10317
  }
9895
10318
  function splitQQBotMarkdownTransportMediaUrls(mediaUrls) {
9896
10319
  const markdownImageUrls = [];
@@ -9992,10 +10415,592 @@ function normalizeQQBotRenderedMarkdown(text) {
9992
10415
  );
9993
10416
  return changed ? next.trim() : text.trim();
9994
10417
  }
10418
+ function normalizeQQBotMarkdownSegment(text) {
10419
+ return text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
10420
+ }
10421
+ function isBlankQQBotMarkdownLine(line) {
10422
+ return line.trim().length === 0;
10423
+ }
10424
+ function resolveQQBotFenceDelimiter(line) {
10425
+ const match = line.match(MARKDOWN_FENCE_RE);
10426
+ return match?.[1];
10427
+ }
10428
+ function isQQBotFenceClosingLine(line, delimiter) {
10429
+ const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10430
+ const closingRe = new RegExp(`^\\s*${escapedDelimiter}${delimiter[0]}*\\s*$`);
10431
+ return closingRe.test(line);
10432
+ }
10433
+ function joinQQBotMarkdownPieces(parts) {
10434
+ return parts.filter(Boolean).join("\n\n").trim();
10435
+ }
10436
+ function isQQBotMarkdownTableStart(lines, index) {
10437
+ const header = lines[index]?.trim() ?? "";
10438
+ const separator = lines[index + 1]?.trim() ?? "";
10439
+ return Boolean(header.includes("|") && MARKDOWN_TABLE_SEPARATOR_RE.test(separator));
10440
+ }
10441
+ function collectQQBotFencedCodeBlock(lines, startIndex) {
10442
+ const openingLine = lines[startIndex] ?? "";
10443
+ const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
10444
+ let index = startIndex + 1;
10445
+ while (index < lines.length) {
10446
+ if (isQQBotFenceClosingLine(lines[index] ?? "", delimiter)) {
10447
+ index += 1;
10448
+ break;
10449
+ }
10450
+ index += 1;
10451
+ }
10452
+ return {
10453
+ block: {
10454
+ kind: "code",
10455
+ text: lines.slice(startIndex, index).join("\n").trimEnd()
10456
+ },
10457
+ nextIndex: index
10458
+ };
10459
+ }
10460
+ function collectQQBotMarkdownTableBlock(lines, startIndex) {
10461
+ let index = startIndex + 2;
10462
+ while (index < lines.length) {
10463
+ const line = lines[index] ?? "";
10464
+ if (isBlankQQBotMarkdownLine(line) || !line.includes("|")) {
10465
+ break;
10466
+ }
10467
+ index += 1;
10468
+ }
10469
+ return {
10470
+ block: {
10471
+ kind: "table",
10472
+ text: lines.slice(startIndex, index).join("\n").trimEnd()
10473
+ },
10474
+ nextIndex: index
10475
+ };
10476
+ }
10477
+ function collectQQBotBlockquoteBlock(lines, startIndex) {
10478
+ const collected = [];
10479
+ let index = startIndex;
10480
+ while (index < lines.length) {
10481
+ const line = lines[index] ?? "";
10482
+ if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
10483
+ collected.push(line);
10484
+ index += 1;
10485
+ continue;
10486
+ }
10487
+ if (isBlankQQBotMarkdownLine(line) && index + 1 < lines.length && MARKDOWN_BLOCKQUOTE_RE.test(lines[index + 1] ?? "")) {
10488
+ collected.push(line);
10489
+ index += 1;
10490
+ continue;
10491
+ }
10492
+ break;
10493
+ }
10494
+ return {
10495
+ block: {
10496
+ kind: "blockquote",
10497
+ text: collected.join("\n").trimEnd()
10498
+ },
10499
+ nextIndex: index
10500
+ };
10501
+ }
10502
+ function collectQQBotListBlock(lines, startIndex) {
10503
+ const collected = [];
10504
+ let index = startIndex;
10505
+ while (index < lines.length) {
10506
+ const line = lines[index] ?? "";
10507
+ if (isBlankQQBotMarkdownLine(line)) {
10508
+ break;
10509
+ }
10510
+ 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)) {
10511
+ break;
10512
+ }
10513
+ if (collected.length > 0 && !MARKDOWN_LIST_ITEM_RE.test(line) && !MARKDOWN_LIST_CONTINUATION_RE.test(line)) {
10514
+ collected.push(line);
10515
+ index += 1;
10516
+ continue;
10517
+ }
10518
+ collected.push(line);
10519
+ index += 1;
10520
+ }
10521
+ return {
10522
+ block: {
10523
+ kind: "list",
10524
+ text: collected.join("\n").trimEnd()
10525
+ },
10526
+ nextIndex: index
10527
+ };
10528
+ }
10529
+ function collectQQBotParagraphBlock(lines, startIndex) {
10530
+ const collected = [];
10531
+ let index = startIndex;
10532
+ while (index < lines.length) {
10533
+ const line = lines[index] ?? "";
10534
+ if (isBlankQQBotMarkdownLine(line)) {
10535
+ break;
10536
+ }
10537
+ 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))) {
10538
+ break;
10539
+ }
10540
+ collected.push(line);
10541
+ index += 1;
10542
+ }
10543
+ return {
10544
+ block: {
10545
+ kind: "paragraph",
10546
+ text: collected.join("\n").trimEnd()
10547
+ },
10548
+ nextIndex: index
10549
+ };
10550
+ }
10551
+ function parseQQBotMarkdownBlocks(text) {
10552
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
10553
+ const blocks = [];
10554
+ let index = 0;
10555
+ while (index < lines.length) {
10556
+ while (index < lines.length && isBlankQQBotMarkdownLine(lines[index] ?? "")) {
10557
+ index += 1;
10558
+ }
10559
+ if (index >= lines.length) {
10560
+ break;
10561
+ }
10562
+ const line = lines[index] ?? "";
10563
+ if (MARKDOWN_FENCE_RE.test(line)) {
10564
+ const result2 = collectQQBotFencedCodeBlock(lines, index);
10565
+ blocks.push(result2.block);
10566
+ index = result2.nextIndex;
10567
+ continue;
10568
+ }
10569
+ if (isQQBotMarkdownTableStart(lines, index)) {
10570
+ const result2 = collectQQBotMarkdownTableBlock(lines, index);
10571
+ blocks.push(result2.block);
10572
+ index = result2.nextIndex;
10573
+ continue;
10574
+ }
10575
+ if (MARKDOWN_THEMATIC_BREAK_RE.test(line)) {
10576
+ blocks.push({ kind: "thematic-break", text: line.trim() });
10577
+ index += 1;
10578
+ continue;
10579
+ }
10580
+ if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
10581
+ const result2 = collectQQBotBlockquoteBlock(lines, index);
10582
+ blocks.push(result2.block);
10583
+ index = result2.nextIndex;
10584
+ continue;
10585
+ }
10586
+ if (MARKDOWN_ATX_HEADING_RE.test(line)) {
10587
+ blocks.push({ kind: "heading", text: line.trimEnd() });
10588
+ index += 1;
10589
+ continue;
10590
+ }
10591
+ if (MARKDOWN_LIST_ITEM_RE.test(line)) {
10592
+ const result2 = collectQQBotListBlock(lines, index);
10593
+ blocks.push(result2.block);
10594
+ index = result2.nextIndex;
10595
+ continue;
10596
+ }
10597
+ const result = collectQQBotParagraphBlock(lines, index);
10598
+ blocks.push(result.block);
10599
+ index = result.nextIndex;
10600
+ }
10601
+ return blocks;
10602
+ }
10603
+ function hasQQBotBoundaryGuard(text) {
10604
+ return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
10605
+ }
10606
+ function isQQBotSafeMarkdownBoundary(text, index) {
10607
+ const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
10608
+ const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
10609
+ const leftEdge = left.slice(-1);
10610
+ const rightEdge = right.slice(0, 1);
10611
+ return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
10612
+ }
10613
+ function findQQBotRegexBoundary(text, limit, pattern) {
10614
+ const scopedText = text.slice(0, Math.min(limit + 1, text.length));
10615
+ const regex = new RegExp(pattern.source, pattern.flags);
10616
+ let match = regex.exec(scopedText);
10617
+ let lastBoundary;
10618
+ while (match) {
10619
+ const boundary = match.index + match[0].length;
10620
+ if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10621
+ lastBoundary = boundary;
10622
+ }
10623
+ match = regex.exec(scopedText);
10624
+ }
10625
+ return lastBoundary;
10626
+ }
10627
+ function findQQBotFallbackBoundary(text, limit) {
10628
+ const minIndex = Math.max(1, limit - 120);
10629
+ for (let index = limit; index >= minIndex; index -= 1) {
10630
+ if (isQQBotSafeMarkdownBoundary(text, index)) {
10631
+ return index;
10632
+ }
10633
+ }
10634
+ return limit;
10635
+ }
10636
+ function findQQBotSafeSplitIndex(text, limit) {
10637
+ const boundaryPatterns = [
10638
+ /\n\n+/g,
10639
+ /\n/g,
10640
+ /[。!?.!?;;::](?:\s+|$)/g,
10641
+ /[,,](?:\s+|$)/g,
10642
+ /\s+/g
10643
+ ];
10644
+ for (const pattern of boundaryPatterns) {
10645
+ const boundary = findQQBotRegexBoundary(text, limit, pattern);
10646
+ if (boundary && boundary > 0) {
10647
+ return boundary;
10648
+ }
10649
+ }
10650
+ return findQQBotFallbackBoundary(text, limit);
10651
+ }
10652
+ function splitQQBotHardText(text, limit) {
10653
+ if (limit <= 0 || text.length <= limit) {
10654
+ return [text];
10655
+ }
10656
+ const chunks = [];
10657
+ let remaining = text;
10658
+ while (remaining.length > limit) {
10659
+ chunks.push(remaining.slice(0, limit));
10660
+ remaining = remaining.slice(limit);
10661
+ }
10662
+ if (remaining) {
10663
+ chunks.push(remaining);
10664
+ }
10665
+ return chunks;
10666
+ }
10667
+ function splitQQBotTextSafely(text, limit, options) {
10668
+ if (limit <= 0 || text.length <= limit) {
10669
+ return [text];
10670
+ }
10671
+ const trimLeading = options?.trimLeading ?? true;
10672
+ const trimTrailing = options?.trimTrailing ?? true;
10673
+ const chunks = [];
10674
+ let remaining = text;
10675
+ while (remaining.length > limit) {
10676
+ const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
10677
+ let nextChunk = remaining.slice(0, splitIndex);
10678
+ let nextRemaining = remaining.slice(splitIndex);
10679
+ if (trimTrailing) {
10680
+ nextChunk = nextChunk.trimEnd();
10681
+ }
10682
+ if (trimLeading) {
10683
+ nextRemaining = nextRemaining.trimStart();
10684
+ }
10685
+ if (!nextChunk) {
10686
+ const hardChunk = remaining.slice(0, limit);
10687
+ chunks.push(hardChunk);
10688
+ remaining = remaining.slice(hardChunk.length);
10689
+ continue;
10690
+ }
10691
+ chunks.push(nextChunk);
10692
+ remaining = nextRemaining;
10693
+ }
10694
+ const finalChunk = trimTrailing ? remaining.trimEnd() : remaining;
10695
+ if (finalChunk) {
10696
+ chunks.push(finalChunk);
10697
+ }
10698
+ return chunks;
10699
+ }
10700
+ function splitQQBotMarkdownLineBlock(text, limit) {
10701
+ if (limit <= 0 || text.length <= limit) {
10702
+ return [text];
10703
+ }
10704
+ const lines = text.split("\n");
10705
+ const chunks = [];
10706
+ let currentLines = [];
10707
+ const flushCurrent = () => {
10708
+ if (currentLines.length === 0) {
10709
+ return;
10710
+ }
10711
+ const chunk = currentLines.join("\n").trimEnd();
10712
+ if (chunk) {
10713
+ chunks.push(chunk);
10714
+ }
10715
+ currentLines = [];
10716
+ };
10717
+ for (const line of lines) {
10718
+ const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
10719
+ ${line}` : line;
10720
+ if (candidate.length <= limit) {
10721
+ currentLines.push(line);
10722
+ continue;
10723
+ }
10724
+ flushCurrent();
10725
+ if (line.length <= limit) {
10726
+ currentLines.push(line);
10727
+ continue;
10728
+ }
10729
+ for (const piece of splitQQBotTextSafely(line, limit, {
10730
+ trimLeading: false,
10731
+ trimTrailing: false
10732
+ })) {
10733
+ if (piece) {
10734
+ chunks.push(piece);
10735
+ }
10736
+ }
10737
+ }
10738
+ flushCurrent();
10739
+ return chunks;
10740
+ }
10741
+ function splitQQBotMarkdownTableBlock(text, limit) {
10742
+ if (limit <= 0 || text.length <= limit) {
10743
+ return [text];
10744
+ }
10745
+ const lines = text.split("\n");
10746
+ const header = lines[0] ?? "";
10747
+ const separator = lines[1] ?? "";
10748
+ const rows = lines.slice(2);
10749
+ const tablePrefix = `${header}
10750
+ ${separator}`;
10751
+ const chunks = [];
10752
+ let currentRows = [];
10753
+ const flushCurrent = () => {
10754
+ if (currentRows.length === 0) {
10755
+ return;
10756
+ }
10757
+ chunks.push(`${tablePrefix}
10758
+ ${currentRows.join("\n")}`);
10759
+ currentRows = [];
10760
+ };
10761
+ for (const row of rows) {
10762
+ const candidate = currentRows.length > 0 ? `${tablePrefix}
10763
+ ${currentRows.join("\n")}
10764
+ ${row}` : `${tablePrefix}
10765
+ ${row}`;
10766
+ if (candidate.length <= limit) {
10767
+ currentRows.push(row);
10768
+ continue;
10769
+ }
10770
+ flushCurrent();
10771
+ if (`${tablePrefix}
10772
+ ${row}`.length <= limit) {
10773
+ currentRows.push(row);
10774
+ continue;
10775
+ }
10776
+ const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
10777
+ for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
10778
+ trimLeading: false,
10779
+ trimTrailing: false
10780
+ })) {
10781
+ chunks.push(`${tablePrefix}
10782
+ ${rowPiece}`);
10783
+ }
10784
+ }
10785
+ flushCurrent();
10786
+ return chunks.length > 0 ? chunks : [text];
10787
+ }
10788
+ function splitQQBotMarkdownCodeFence(text, limit) {
10789
+ if (limit <= 0 || text.length <= limit) {
10790
+ return [text];
10791
+ }
10792
+ const lines = text.split("\n");
10793
+ const openingLine = lines[0] ?? "```";
10794
+ const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
10795
+ const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
10796
+ const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
10797
+ const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
10798
+ const fixedOverhead = openingLine.length + closingLine.length + 2;
10799
+ const availableLineLength = Math.max(1, limit - fixedOverhead);
10800
+ const chunks = [];
10801
+ let currentCodeLines = [];
10802
+ const flushCurrent = () => {
10803
+ if (currentCodeLines.length === 0) {
10804
+ return;
10805
+ }
10806
+ chunks.push(`${openingLine}
10807
+ ${currentCodeLines.join("\n")}
10808
+ ${closingLine}`);
10809
+ currentCodeLines = [];
10810
+ };
10811
+ for (const codeLine of codeLines) {
10812
+ const candidate = currentCodeLines.length > 0 ? `${openingLine}
10813
+ ${currentCodeLines.join("\n")}
10814
+ ${codeLine}
10815
+ ${closingLine}` : `${openingLine}
10816
+ ${codeLine}
10817
+ ${closingLine}`;
10818
+ if (candidate.length <= limit) {
10819
+ currentCodeLines.push(codeLine);
10820
+ continue;
10821
+ }
10822
+ flushCurrent();
10823
+ if (`${openingLine}
10824
+ ${codeLine}
10825
+ ${closingLine}`.length <= limit) {
10826
+ currentCodeLines.push(codeLine);
10827
+ continue;
10828
+ }
10829
+ for (const linePiece of splitQQBotHardText(codeLine, availableLineLength)) {
10830
+ chunks.push(`${openingLine}
10831
+ ${linePiece}
10832
+ ${closingLine}`);
10833
+ }
10834
+ }
10835
+ flushCurrent();
10836
+ return chunks.length > 0 ? chunks : [text];
10837
+ }
10838
+ function splitQQBotMarkdownBlock(block, limit) {
10839
+ if (limit <= 0 || block.text.length <= limit) {
10840
+ return [block.text];
10841
+ }
10842
+ switch (block.kind) {
10843
+ case "table":
10844
+ return splitQQBotMarkdownTableBlock(block.text, limit);
10845
+ case "code":
10846
+ return splitQQBotMarkdownCodeFence(block.text, limit);
10847
+ case "blockquote":
10848
+ return splitQQBotMarkdownLineBlock(block.text, limit);
10849
+ case "list":
10850
+ return splitQQBotMarkdownLineBlock(block.text, limit);
10851
+ case "paragraph":
10852
+ case "heading":
10853
+ return splitQQBotTextSafely(block.text, limit);
10854
+ case "thematic-break":
10855
+ return [block.text];
10856
+ default:
10857
+ return [block.text];
10858
+ }
10859
+ }
10860
+ function chunkQQBotStructuredMarkdown(text, limit) {
10861
+ const blocks = parseQQBotMarkdownBlocks(text);
10862
+ if (blocks.length === 0 || limit <= 0) {
10863
+ return [text.trim()];
10864
+ }
10865
+ const chunks = [];
10866
+ let currentPieces = [];
10867
+ let pendingPrefixPieces = [];
10868
+ const flushCurrent = () => {
10869
+ if (currentPieces.length === 0) {
10870
+ return;
10871
+ }
10872
+ const chunk = joinQQBotMarkdownPieces(currentPieces);
10873
+ if (chunk) {
10874
+ chunks.push(chunk);
10875
+ }
10876
+ currentPieces = [];
10877
+ };
10878
+ const appendPiece = (piece) => {
10879
+ if (!piece) {
10880
+ return;
10881
+ }
10882
+ const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
10883
+ for (const nextPiece of pieces) {
10884
+ const normalizedPiece = nextPiece.trim();
10885
+ if (!normalizedPiece) {
10886
+ continue;
10887
+ }
10888
+ const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
10889
+ if (currentPieces.length === 0 || candidate.length <= limit) {
10890
+ currentPieces.push(normalizedPiece);
10891
+ continue;
10892
+ }
10893
+ flushCurrent();
10894
+ currentPieces.push(normalizedPiece);
10895
+ }
10896
+ };
10897
+ const consumePendingPrefix = (piece) => {
10898
+ if (pendingPrefixPieces.length === 0) {
10899
+ return piece;
10900
+ }
10901
+ const prefixed = joinQQBotMarkdownPieces([...pendingPrefixPieces, piece]);
10902
+ pendingPrefixPieces = [];
10903
+ return prefixed;
10904
+ };
10905
+ for (let index = 0; index < blocks.length; index += 1) {
10906
+ const block = blocks[index];
10907
+ if (!block) {
10908
+ continue;
10909
+ }
10910
+ if (block.kind === "thematic-break") {
10911
+ if (currentPieces.length > 0) {
10912
+ const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
10913
+ if (candidate.length <= limit) {
10914
+ currentPieces.push(block.text);
10915
+ continue;
10916
+ }
10917
+ flushCurrent();
10918
+ }
10919
+ pendingPrefixPieces.push(block.text);
10920
+ continue;
10921
+ }
10922
+ if (block.kind === "heading") {
10923
+ const headingText = consumePendingPrefix(block.text);
10924
+ const nextBlock = blocks[index + 1];
10925
+ if (nextBlock && nextBlock.kind !== "thematic-break") {
10926
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
10927
+ const firstBodyPiece = nextPieces[0];
10928
+ if (firstBodyPiece) {
10929
+ const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
10930
+ const pairedCandidate = joinQQBotMarkdownPieces([
10931
+ ...currentPieces,
10932
+ headingText,
10933
+ firstBodyPiece
10934
+ ]);
10935
+ if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
10936
+ currentPieces.push(headingText, firstBodyPiece);
10937
+ for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10938
+ appendPiece(nextPieces[pieceIndex] ?? "");
10939
+ }
10940
+ index += 1;
10941
+ continue;
10942
+ }
10943
+ if (currentPieces.length > 0 && pairedText.length <= limit) {
10944
+ flushCurrent();
10945
+ currentPieces.push(headingText, firstBodyPiece);
10946
+ for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10947
+ appendPiece(nextPieces[pieceIndex] ?? "");
10948
+ }
10949
+ index += 1;
10950
+ continue;
10951
+ }
10952
+ }
10953
+ }
10954
+ appendPiece(headingText);
10955
+ continue;
10956
+ }
10957
+ const blockText = consumePendingPrefix(block.text);
10958
+ for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
10959
+ appendPiece(piece);
10960
+ }
10961
+ }
10962
+ if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
10963
+ const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
10964
+ if (trailingCandidate.length <= limit) {
10965
+ currentPieces.push(...pendingPrefixPieces);
10966
+ }
10967
+ }
10968
+ flushCurrent();
10969
+ return chunks.length > 0 ? chunks : [text.trim()];
10970
+ }
10971
+ function looksLikeStructuredMarkdown(text) {
10972
+ const normalized = normalizeQQBotMarkdownSegment(text);
10973
+ if (!normalized) {
10974
+ return false;
10975
+ }
10976
+ const lines = normalized.split("\n");
10977
+ if (hasQQBotMarkdownTable(normalized)) {
10978
+ return true;
10979
+ }
10980
+ 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);
10981
+ }
10982
+ function chunkC2CMarkdownText(params) {
10983
+ const normalized = params.text.trim();
10984
+ if (!normalized) {
10985
+ return [];
10986
+ }
10987
+ const strategy = params.strategy ?? "markdown-block";
10988
+ if (strategy === "length") {
10989
+ return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10990
+ }
10991
+ if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
10992
+ return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10993
+ }
10994
+ return chunkQQBotStructuredMarkdown(normalized, params.limit);
10995
+ }
9995
10996
  async function sendQQBotMediaWithFallback(params) {
9996
10997
  const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
9997
10998
  const outbound = params.outbound ?? qqbotOutbound;
10999
+ const shouldContinue = params.shouldContinue ?? (() => true);
9998
11000
  for (const mediaUrl of mediaQueue) {
11001
+ if (!shouldContinue()) {
11002
+ return;
11003
+ }
9999
11004
  const result = await outbound.sendMedia({
10000
11005
  cfg: { channels: { qqbot: qqCfg } },
10001
11006
  to,
@@ -10011,6 +11016,9 @@ async function sendQQBotMediaWithFallback(params) {
10011
11016
  if (!fallback) {
10012
11017
  continue;
10013
11018
  }
11019
+ if (!shouldContinue()) {
11020
+ return;
11021
+ }
10014
11022
  const fallbackResult = await outbound.sendText({
10015
11023
  cfg: { channels: { qqbot: qqCfg } },
10016
11024
  to,
@@ -10063,16 +11071,26 @@ async function dispatchToAgent(params) {
10063
11071
  const { inbound, cfg, qqCfg, accountId, logger, route } = params;
10064
11072
  const runtime2 = getQQBotRuntime();
10065
11073
  const routeSessionKey = resolveQQBotRouteSessionKey(route);
11074
+ const queueKey = buildSessionDispatchQueueKey(route);
11075
+ const isFastAbortCommand = isQQBotFastAbortCommandText(inbound.content);
11076
+ const dispatchAbortGeneration = getSessionDispatchState(queueKey).abortGeneration;
11077
+ const shouldSuppressVisibleReplies = () => {
11078
+ const currentAbortGeneration = sessionDispatchQueue.get(queueKey)?.abortGeneration ?? dispatchAbortGeneration;
11079
+ return currentAbortGeneration !== dispatchAbortGeneration;
11080
+ };
10066
11081
  const target = resolveChatTarget(inbound);
10067
11082
  const outboundAccountId = route.accountId ?? accountId;
11083
+ const typingHeartbeatMode = resolveQQBotTypingHeartbeatMode(qqCfg);
11084
+ const typingHeartbeatIntervalMs = resolveQQBotTypingHeartbeatIntervalMs(qqCfg);
11085
+ const typingInputSeconds = resolveQQBotTypingInputSeconds(qqCfg);
10068
11086
  let typingRefIdx;
10069
- if (inbound.c2cOpenid) {
11087
+ if (inbound.c2cOpenid && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
10070
11088
  const typing = await qqbotOutbound.sendTyping({
10071
11089
  cfg: { channels: { qqbot: qqCfg } },
10072
11090
  to: `user:${inbound.c2cOpenid}`,
10073
11091
  replyToId: inbound.messageId,
10074
11092
  replyEventId: inbound.eventId,
10075
- inputSecond: 60,
11093
+ inputSecond: typingInputSeconds,
10076
11094
  accountId: outboundAccountId
10077
11095
  });
10078
11096
  if (typing.error) {
@@ -10088,10 +11106,15 @@ async function dispatchToAgent(params) {
10088
11106
  }
10089
11107
  let replyDelivered = false;
10090
11108
  let groupMessageInterfaceBlocked = false;
11109
+ let lastVisibleOutboundAt = Date.now();
11110
+ let typingHeartbeat = null;
10091
11111
  const markReplyDelivered = () => {
10092
11112
  replyDelivered = true;
10093
11113
  longTaskNotice.markReplyDelivered();
10094
11114
  };
11115
+ const markVisibleOutboundStarted = () => {
11116
+ lastVisibleOutboundAt = Date.now();
11117
+ };
10095
11118
  const markGroupMessageInterfaceBlocked = (error) => {
10096
11119
  if (!isQQBotGroupMessageInterfaceBlocked(error)) return;
10097
11120
  if (!groupMessageInterfaceBlocked) {
@@ -10099,11 +11122,40 @@ async function dispatchToAgent(params) {
10099
11122
  }
10100
11123
  groupMessageInterfaceBlocked = true;
10101
11124
  };
11125
+ if (inbound.c2cOpenid && typingHeartbeatMode !== "none" && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11126
+ typingHeartbeat = startQQBotTypingHeartbeat({
11127
+ intervalMs: typingHeartbeatIntervalMs,
11128
+ shouldRenew: () => {
11129
+ if (shouldSuppressVisibleReplies()) {
11130
+ return false;
11131
+ }
11132
+ if (typingHeartbeatMode === "always") {
11133
+ return true;
11134
+ }
11135
+ return Date.now() - lastVisibleOutboundAt >= typingHeartbeatIntervalMs;
11136
+ },
11137
+ renew: async () => {
11138
+ try {
11139
+ const typing = await qqbotOutbound.sendTyping({
11140
+ cfg: { channels: { qqbot: qqCfg } },
11141
+ to: `user:${inbound.c2cOpenid}`,
11142
+ replyToId: inbound.messageId,
11143
+ replyEventId: inbound.eventId,
11144
+ inputSecond: typingInputSeconds,
11145
+ accountId: outboundAccountId
11146
+ });
11147
+ void typing;
11148
+ } catch {
11149
+ }
11150
+ }
11151
+ });
11152
+ }
10102
11153
  const longTaskNotice = startLongTaskNoticeTimer({
10103
11154
  delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
10104
11155
  logger,
10105
11156
  sendNotice: async () => {
10106
- if (groupMessageInterfaceBlocked) return;
11157
+ if (groupMessageInterfaceBlocked || isFastAbortCommand || shouldSuppressVisibleReplies()) return;
11158
+ markVisibleOutboundStarted();
10107
11159
  const result = await qqbotOutbound.sendText({
10108
11160
  cfg: { channels: { qqbot: qqCfg } },
10109
11161
  to: target.to,
@@ -10116,7 +11168,7 @@ async function dispatchToAgent(params) {
10116
11168
  logger.warn(`send long-task notice failed: ${result.error}`);
10117
11169
  markGroupMessageInterfaceBlocked(result.error);
10118
11170
  } else {
10119
- replyDelivered = true;
11171
+ markReplyDelivered();
10120
11172
  }
10121
11173
  }
10122
11174
  });
@@ -10137,6 +11189,10 @@ async function dispatchToAgent(params) {
10137
11189
  logger
10138
11190
  });
10139
11191
  if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
11192
+ if (shouldSuppressVisibleReplies()) {
11193
+ return;
11194
+ }
11195
+ markVisibleOutboundStarted();
10140
11196
  const fallback = await qqbotOutbound.sendText({
10141
11197
  cfg: { channels: { qqbot: qqCfg } },
10142
11198
  to: target.to,
@@ -10149,7 +11205,7 @@ async function dispatchToAgent(params) {
10149
11205
  logger.error(`sendText ASR fallback failed: ${fallback.error}`);
10150
11206
  markGroupMessageInterfaceBlocked(fallback.error);
10151
11207
  } else {
10152
- replyDelivered = true;
11208
+ markReplyDelivered();
10153
11209
  }
10154
11210
  return;
10155
11211
  }
@@ -10246,29 +11302,43 @@ async function dispatchToAgent(params) {
10246
11302
  }
10247
11303
  finalCtx.BodyForAgent = appendCronHiddenPrompt(agentBody);
10248
11304
  }
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)}`);
11305
+ if (storePath) {
11306
+ const mainSessionKeyRaw = route.mainSessionKey;
11307
+ const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw.trim() : void 0;
11308
+ const isGroup = inbound.type === "group" || inbound.type === "channel";
11309
+ const updateLastRoute = !isGroup ? {
11310
+ sessionKey: mainSessionKey ?? route.sessionKey,
11311
+ channel: "qqbot",
11312
+ to: stableTo,
11313
+ accountId: outboundAccountId
11314
+ } : void 0;
11315
+ const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : routeSessionKey;
11316
+ if (sessionApi?.recordInboundSession) {
11317
+ try {
11318
+ await sessionApi.recordInboundSession({
11319
+ storePath,
11320
+ sessionKey: recordSessionKey,
11321
+ ctx: finalCtx,
11322
+ updateLastRoute,
11323
+ onRecordError: (err) => {
11324
+ logger.warn(`failed to record inbound session: ${String(err)}`);
11325
+ }
11326
+ });
11327
+ } catch (err) {
11328
+ logger.warn(`failed to record inbound session: ${String(err)}`);
11329
+ }
11330
+ }
11331
+ if (sessionApi?.recordSessionMetaFromInbound) {
11332
+ try {
11333
+ await sessionApi.recordSessionMetaFromInbound({
11334
+ storePath,
11335
+ sessionKey: recordSessionKey,
11336
+ ctx: finalCtx,
11337
+ createIfMissing: true
11338
+ });
11339
+ } catch (err) {
11340
+ logger.warn(`failed to record inbound session meta: ${String(err)}`);
11341
+ }
10272
11342
  }
10273
11343
  }
10274
11344
  const textApi = runtime2.channel?.text;
@@ -10296,10 +11366,13 @@ async function dispatchToAgent(params) {
10296
11366
  const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
10297
11367
  const markdownSupport = qqCfg.markdownSupport ?? true;
10298
11368
  const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
10299
- const useC2CMarkdownTransport = markdownSupport && isQQBotC2CTarget(target.to);
11369
+ const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11370
+ const isC2CTarget = isQQBotC2CTarget(target.to);
11371
+ const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
10300
11372
  let bufferedC2CMarkdownTexts = [];
10301
11373
  let bufferedC2CMarkdownMediaUrls = [];
10302
11374
  const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
11375
+ const hasBufferedC2CMarkdownReply = () => bufferedC2CMarkdownTexts.length > 0 || bufferedC2CMarkdownMediaUrls.length > 0;
10303
11376
  const bufferC2CMarkdownMedia = (url) => {
10304
11377
  const next = url?.trim();
10305
11378
  if (!next || bufferedC2CMarkdownMediaSeen.has(next)) return;
@@ -10307,12 +11380,18 @@ async function dispatchToAgent(params) {
10307
11380
  bufferedC2CMarkdownMediaUrls.push(next);
10308
11381
  };
10309
11382
  const sendC2CMarkdownTransportPayload = async (params2) => {
11383
+ if (shouldSuppressVisibleReplies()) {
11384
+ return;
11385
+ }
10310
11386
  const normalizedText = normalizeQQBotRenderedMarkdown(params2.text);
10311
11387
  const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(params2.mediaUrls);
10312
11388
  const finalMarkdownText = await normalizeQQBotMarkdownImages({
10313
11389
  text: normalizedText,
10314
11390
  appendImageUrls: markdownImageUrls
10315
11391
  });
11392
+ if (shouldSuppressVisibleReplies()) {
11393
+ return;
11394
+ }
10316
11395
  const textReplyRefs = resolveQQBotTextReplyRefs({
10317
11396
  to: target.to,
10318
11397
  text: finalMarkdownText || normalizedText,
@@ -10321,52 +11400,63 @@ async function dispatchToAgent(params) {
10321
11400
  replyToId: inbound.messageId,
10322
11401
  replyEventId: inbound.eventId
10323
11402
  });
10324
- const textSegments = finalMarkdownText ? [finalMarkdownText] : [];
11403
+ const textChunks = finalMarkdownText ? chunkC2CMarkdownText({
11404
+ text: finalMarkdownText,
11405
+ limit,
11406
+ strategy: c2cMarkdownChunkStrategy,
11407
+ fallbackChunkText: chunkText
11408
+ }) : [];
10325
11409
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
10326
11410
  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")}`
11411
+ `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
11412
  );
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);
11413
+ if (!shouldSuppressVisibleReplies()) {
11414
+ if (mediaQueue.length > 0) {
11415
+ markVisibleOutboundStarted();
10342
11416
  }
10343
- });
11417
+ await sendQQBotMediaWithFallback({
11418
+ qqCfg,
11419
+ to: target.to,
11420
+ mediaQueue,
11421
+ replyToId: textReplyRefs.replyToId,
11422
+ replyEventId: textReplyRefs.replyEventId,
11423
+ accountId: outboundAccountId,
11424
+ logger,
11425
+ onDelivered: () => {
11426
+ markReplyDelivered();
11427
+ },
11428
+ onError: (error) => {
11429
+ markGroupMessageInterfaceBlocked(error);
11430
+ },
11431
+ shouldContinue: () => !shouldSuppressVisibleReplies()
11432
+ });
11433
+ }
10344
11434
  if (!finalMarkdownText) {
10345
11435
  return;
10346
11436
  }
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
- }
11437
+ for (let chunkIndex = 0; chunkIndex < textChunks.length; chunkIndex += 1) {
11438
+ if (shouldSuppressVisibleReplies()) {
11439
+ return;
11440
+ }
11441
+ const chunk = textChunks[chunkIndex] ?? "";
11442
+ logger.info(
11443
+ `delivery=${deliveryLabel} segment=1/1 chunk=${chunkIndex + 1}/${textChunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
11444
+ );
11445
+ markVisibleOutboundStarted();
11446
+ const result = await qqbotOutbound.sendText({
11447
+ cfg: { channels: { qqbot: qqCfg } },
11448
+ to: target.to,
11449
+ text: chunk,
11450
+ replyToId: textReplyRefs.replyToId,
11451
+ replyEventId: textReplyRefs.replyEventId,
11452
+ accountId: outboundAccountId
11453
+ });
11454
+ if (result.error) {
11455
+ logger.error(`send QQ markdown reply failed: ${result.error}`);
11456
+ markGroupMessageInterfaceBlocked(result.error);
11457
+ } else {
11458
+ logger.info(`sent QQ markdown reply (phase=${params2.phase}, len=${chunk.length})`);
11459
+ markReplyDelivered();
10370
11460
  }
10371
11461
  }
10372
11462
  };
@@ -10377,6 +11467,12 @@ async function dispatchToAgent(params) {
10377
11467
  bufferedC2CMarkdownMediaSeen.clear();
10378
11468
  return;
10379
11469
  }
11470
+ if (shouldSuppressVisibleReplies()) {
11471
+ bufferedC2CMarkdownTexts = [];
11472
+ bufferedC2CMarkdownMediaUrls = [];
11473
+ bufferedC2CMarkdownMediaSeen.clear();
11474
+ return;
11475
+ }
10380
11476
  const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
10381
11477
  const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
10382
11478
  bufferedC2CMarkdownTexts = [];
@@ -10389,6 +11485,9 @@ async function dispatchToAgent(params) {
10389
11485
  });
10390
11486
  };
10391
11487
  const deliver = async (payload, info) => {
11488
+ if (shouldSuppressVisibleReplies()) {
11489
+ return;
11490
+ }
10392
11491
  const typed = payload;
10393
11492
  const extractedTextMedia = extractQQBotReplyMedia({
10394
11493
  text: typed?.text ?? "",
@@ -10420,7 +11519,8 @@ async function dispatchToAgent(params) {
10420
11519
  const textToSend = suppressText ? "" : cleanedText;
10421
11520
  if (useC2CMarkdownTransport) {
10422
11521
  const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
10423
- if (shouldBufferFinalOnlyPayload) {
11522
+ const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
11523
+ if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
10424
11524
  if (textToSend) {
10425
11525
  bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
10426
11526
  }
@@ -10429,6 +11529,12 @@ async function dispatchToAgent(params) {
10429
11529
  }
10430
11530
  return;
10431
11531
  }
11532
+ if (hasBufferedC2CMarkdownReply()) {
11533
+ await flushBufferedC2CMarkdownReply();
11534
+ if (shouldSuppressVisibleReplies()) {
11535
+ return;
11536
+ }
11537
+ }
10432
11538
  await sendC2CMarkdownTransportPayload({
10433
11539
  text: textToSend,
10434
11540
  mediaUrls: mediaQueue,
@@ -10448,6 +11554,10 @@ async function dispatchToAgent(params) {
10448
11554
  });
10449
11555
  const chunks = chunkText(converted);
10450
11556
  for (const chunk of chunks) {
11557
+ if (shouldSuppressVisibleReplies()) {
11558
+ return;
11559
+ }
11560
+ markVisibleOutboundStarted();
10451
11561
  const result = await qqbotOutbound.sendText({
10452
11562
  cfg: { channels: { qqbot: qqCfg } },
10453
11563
  to: target.to,
@@ -10464,6 +11574,12 @@ async function dispatchToAgent(params) {
10464
11574
  }
10465
11575
  }
10466
11576
  }
11577
+ if (shouldSuppressVisibleReplies()) {
11578
+ return;
11579
+ }
11580
+ if (mediaQueue.length > 0) {
11581
+ markVisibleOutboundStarted();
11582
+ }
10467
11583
  await sendQQBotMediaWithFallback({
10468
11584
  qqCfg,
10469
11585
  to: target.to,
@@ -10477,12 +11593,38 @@ async function dispatchToAgent(params) {
10477
11593
  },
10478
11594
  onError: (error) => {
10479
11595
  markGroupMessageInterfaceBlocked(error);
10480
- }
11596
+ },
11597
+ shouldContinue: () => !shouldSuppressVisibleReplies()
10481
11598
  });
10482
11599
  };
10483
11600
  const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
11601
+ const dispatchDirect = replyApi.dispatchReplyWithDispatcher;
10484
11602
  const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
10485
- if (dispatchBuffered) {
11603
+ const streamingReplyOptions = isC2CTarget && !replyFinalOnly ? {
11604
+ disableBlockStreaming: false
11605
+ } : void 0;
11606
+ if (isC2CTarget && !replyFinalOnly && dispatchDirect) {
11607
+ logger.debug(`[dispatch] mode=direct session=${routeSessionKey} to=${target.to}`);
11608
+ await dispatchDirect({
11609
+ ctx: finalCtx,
11610
+ cfg,
11611
+ dispatcherOptions: {
11612
+ deliver,
11613
+ humanDelay,
11614
+ onError: (err, info) => {
11615
+ logger.error(`${info.kind} reply failed: ${String(err)}`);
11616
+ },
11617
+ onSkip: (_payload, info) => {
11618
+ if (info.reason !== "silent") {
11619
+ logger.info(`reply skipped: ${info.reason}`);
11620
+ }
11621
+ }
11622
+ },
11623
+ replyOptions: streamingReplyOptions
11624
+ });
11625
+ await flushBufferedC2CMarkdownReply();
11626
+ } else if (dispatchBuffered) {
11627
+ logger.debug(`[dispatch] mode=buffered session=${routeSessionKey} to=${target.to}`);
10486
11628
  await dispatchBuffered({
10487
11629
  ctx: finalCtx,
10488
11630
  cfg,
@@ -10497,10 +11639,12 @@ async function dispatchToAgent(params) {
10497
11639
  logger.info(`reply skipped: ${info.reason}`);
10498
11640
  }
10499
11641
  }
10500
- }
11642
+ },
11643
+ replyOptions: streamingReplyOptions
10501
11644
  });
10502
11645
  await flushBufferedC2CMarkdownReply();
10503
11646
  } else {
11647
+ logger.debug(`[dispatch] mode=legacy session=${routeSessionKey} to=${target.to}`);
10504
11648
  const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
10505
11649
  deliver,
10506
11650
  humanDelay,
@@ -10526,7 +11670,10 @@ async function dispatchToAgent(params) {
10526
11670
  ctx: finalCtx,
10527
11671
  cfg,
10528
11672
  dispatcher: dispatcherResult.dispatcher,
10529
- replyOptions: dispatcherResult.replyOptions
11673
+ replyOptions: {
11674
+ ...typeof dispatcherResult.replyOptions === "object" && dispatcherResult.replyOptions ? dispatcherResult.replyOptions : {},
11675
+ ...streamingReplyOptions ?? {}
11676
+ }
10530
11677
  });
10531
11678
  dispatcherResult.markDispatchIdle?.();
10532
11679
  await flushBufferedC2CMarkdownReply();
@@ -10535,8 +11682,9 @@ async function dispatchToAgent(params) {
10535
11682
  inbound,
10536
11683
  replyDelivered
10537
11684
  });
10538
- if (noReplyFallback && !groupMessageInterfaceBlocked) {
11685
+ if (noReplyFallback && !groupMessageInterfaceBlocked && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
10539
11686
  logger.info("no visible reply generated for group mention; sending fallback text");
11687
+ markVisibleOutboundStarted();
10540
11688
  const fallbackResult = await qqbotOutbound.sendText({
10541
11689
  cfg: { channels: { qqbot: qqCfg } },
10542
11690
  to: target.to,
@@ -10553,6 +11701,7 @@ async function dispatchToAgent(params) {
10553
11701
  }
10554
11702
  }
10555
11703
  } finally {
11704
+ typingHeartbeat?.dispose();
10556
11705
  longTaskNotice.dispose();
10557
11706
  try {
10558
11707
  await pruneInboundMediaDir({
@@ -10609,18 +11758,39 @@ async function handleQQBotDispatch(params) {
10609
11758
  logger.info("qqbot disabled, ignoring inbound message");
10610
11759
  return;
10611
11760
  }
10612
- const content = inbound.content.trim();
11761
+ const senderNameResolution = resolveQQBotSenderName({
11762
+ inbound,
11763
+ cfg: params.cfg,
11764
+ accountId
11765
+ });
11766
+ const resolvedInbound = {
11767
+ ...inbound,
11768
+ senderName: senderNameResolution.displayName
11769
+ };
11770
+ logQQBotSenderNameResolution({
11771
+ logger,
11772
+ inbound,
11773
+ accountId,
11774
+ resolution: senderNameResolution
11775
+ });
11776
+ const content = resolvedInbound.content.trim();
10613
11777
  const inboundLogContent = sanitizeInboundLogText(
10614
11778
  resolveInboundLogContent({
10615
11779
  content,
10616
- attachments: inbound.attachments
11780
+ attachments: resolvedInbound.attachments
10617
11781
  })
10618
11782
  );
10619
- logger.info(`[inbound-user] accountId=${accountId} senderId=${inbound.senderId} content=${inboundLogContent}`);
10620
- if (!shouldHandleMessage(inbound, qqCfg, logger)) {
11783
+ logger.info(
11784
+ `[inbound-user] accountId=${accountId} senderId=${resolvedInbound.senderId} senderName=${JSON.stringify(resolvedInbound.senderName ?? resolvedInbound.senderId)} content=${inboundLogContent}`
11785
+ );
11786
+ if (!shouldHandleMessage(resolvedInbound, qqCfg, logger)) {
10621
11787
  return;
10622
11788
  }
10623
- const knownTarget = resolveKnownQQBotTargetFromInbound({ inbound, accountId });
11789
+ const knownTarget = resolveKnownQQBotTargetFromInbound({
11790
+ inbound: resolvedInbound,
11791
+ accountId,
11792
+ persistentDisplayName: senderNameResolution.persistentDisplayName
11793
+ });
10624
11794
  if (knownTarget) {
10625
11795
  try {
10626
11796
  upsertKnownQQBotTarget({ target: knownTarget });
@@ -10628,7 +11798,7 @@ async function handleQQBotDispatch(params) {
10628
11798
  logger.warn(`failed to record known qqbot target: ${String(err)}`);
10629
11799
  }
10630
11800
  }
10631
- const attachmentCount = inbound.attachments?.length ?? 0;
11801
+ const attachmentCount = resolvedInbound.attachments?.length ?? 0;
10632
11802
  if (attachmentCount > 0) {
10633
11803
  logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
10634
11804
  }
@@ -10641,7 +11811,7 @@ async function handleQQBotDispatch(params) {
10641
11811
  logger.warn("routing API not available");
10642
11812
  return;
10643
11813
  }
10644
- const target = resolveChatTarget(inbound);
11814
+ const target = resolveChatTarget(resolvedInbound);
10645
11815
  const route = routing({
10646
11816
  cfg: params.cfg,
10647
11817
  channel: "qqbot",
@@ -10649,7 +11819,7 @@ async function handleQQBotDispatch(params) {
10649
11819
  peer: { kind: target.peerKind, id: target.peerId }
10650
11820
  });
10651
11821
  const effectiveSessionKey = resolveQQBotEffectiveSessionKey({
10652
- inbound,
11822
+ inbound: resolvedInbound,
10653
11823
  route,
10654
11824
  accountId
10655
11825
  });
@@ -10659,13 +11829,36 @@ async function handleQQBotDispatch(params) {
10659
11829
  effectiveSessionKey
10660
11830
  };
10661
11831
  const queueKey = buildSessionDispatchQueueKey(resolvedRoute);
10662
- if (sessionDispatchQueue.has(queueKey)) {
11832
+ if (isQQBotFastAbortCommandText(content)) {
11833
+ const routeSessionKey = resolveQQBotRouteSessionKey(resolvedRoute);
11834
+ markSessionDispatchAbort(queueKey);
11835
+ const droppedCount = dropQueuedSessionDispatches(queueKey);
11836
+ logger.info(
11837
+ `session fast-abort command detected; executing immediately sessionKey=${routeSessionKey}`
11838
+ );
11839
+ logger.info(
11840
+ `session fast-abort command dropped ${droppedCount} queued messages sessionKey=${routeSessionKey}`
11841
+ );
11842
+ await runImmediateSessionDispatch(
11843
+ queueKey,
11844
+ async () => dispatchToAgent({
11845
+ inbound: { ...resolvedInbound, content },
11846
+ cfg: params.cfg,
11847
+ qqCfg,
11848
+ accountId,
11849
+ logger,
11850
+ route: resolvedRoute
11851
+ })
11852
+ );
11853
+ return;
11854
+ }
11855
+ if (hasSessionDispatchBacklog(queueKey)) {
10663
11856
  logger.info(`session busy; queueing inbound dispatch sessionKey=${resolveQQBotRouteSessionKey(resolvedRoute)}`);
10664
11857
  }
10665
11858
  await runSerializedSessionDispatch(
10666
11859
  queueKey,
10667
11860
  async () => dispatchToAgent({
10668
- inbound: { ...inbound, content },
11861
+ inbound: { ...resolvedInbound, content },
10669
11862
  cfg: params.cfg,
10670
11863
  qqCfg,
10671
11864
  accountId,
@@ -10693,6 +11886,10 @@ function formatGatewayConnectError(err) {
10693
11886
  }
10694
11887
  return String(err);
10695
11888
  }
11889
+ function isConnectionIdle(conn) {
11890
+ if (!conn) return true;
11891
+ return !conn.socket && !conn.promise && !conn.connecting;
11892
+ }
10696
11893
  var activeConnections = /* @__PURE__ */ new Map();
10697
11894
  function getOrCreateConnection(accountId) {
10698
11895
  let conn = activeConnections.get(accountId);
@@ -10722,7 +11919,10 @@ function clearTimers(conn) {
10722
11919
  conn.reconnectTimer = null;
10723
11920
  }
10724
11921
  }
10725
- function cleanupSocket(conn) {
11922
+ function cleanupSocket(conn, expectedSocket) {
11923
+ if (expectedSocket && conn.socket !== expectedSocket) {
11924
+ return false;
11925
+ }
10726
11926
  clearTimers(conn);
10727
11927
  if (conn.socket) {
10728
11928
  try {
@@ -10733,6 +11933,7 @@ function cleanupSocket(conn) {
10733
11933
  }
10734
11934
  conn.socket = null;
10735
11935
  }
11936
+ return true;
10736
11937
  }
10737
11938
  async function monitorQQBotProvider(opts = {}) {
10738
11939
  const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID, setStatus } = opts;
@@ -10740,11 +11941,16 @@ async function monitorQQBotProvider(opts = {}) {
10740
11941
  log: runtime2?.log,
10741
11942
  error: runtime2?.error
10742
11943
  });
10743
- const conn = getOrCreateConnection(accountId);
10744
- if (conn.socket) {
10745
- if (conn.promise) {
10746
- return conn.promise;
10747
- }
11944
+ const existingConn = activeConnections.get(accountId);
11945
+ if (!existingConn) ; else if (isConnectionIdle(existingConn)) {
11946
+ activeConnections.delete(accountId);
11947
+ }
11948
+ const conn = activeConnections.get(accountId);
11949
+ const existingPromise = conn?.promise;
11950
+ if (existingPromise) {
11951
+ return existingPromise;
11952
+ }
11953
+ if (conn?.socket) {
10748
11954
  throw new Error(`QQBot monitor state invalid for account ${accountId}: active socket without promise`);
10749
11955
  }
10750
11956
  const qqCfg = config ? mergeQQBotAccountConfig(config, accountId) : void 0;
@@ -10754,18 +11960,20 @@ async function monitorQQBotProvider(opts = {}) {
10754
11960
  if (!qqCfg.appId || !qqCfg.clientSecret) {
10755
11961
  throw new Error(`QQBot not configured for account ${accountId} (missing appId or clientSecret)`);
10756
11962
  }
10757
- conn.promise = new Promise((resolve3, reject) => {
11963
+ const nextConn = conn ?? getOrCreateConnection(accountId);
11964
+ nextConn.promise = new Promise((resolve3, reject) => {
10758
11965
  let stopped = false;
10759
11966
  const finish = (err) => {
10760
11967
  if (stopped) return;
10761
11968
  stopped = true;
10762
11969
  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;
11970
+ cleanupSocket(nextConn);
11971
+ nextConn.connecting = false;
11972
+ nextConn.sessionId = null;
11973
+ nextConn.lastSeq = null;
11974
+ nextConn.promise = null;
11975
+ nextConn.stop = null;
11976
+ nextConn.reconnectAttempt = 0;
10769
11977
  activeConnections.delete(accountId);
10770
11978
  {
10771
11979
  resolve3();
@@ -10775,33 +11983,33 @@ async function monitorQQBotProvider(opts = {}) {
10775
11983
  logger.info("abort signal received, stopping gateway");
10776
11984
  finish();
10777
11985
  };
10778
- conn.stop = () => {
11986
+ nextConn.stop = () => {
10779
11987
  logger.info("stop requested");
10780
11988
  finish();
10781
11989
  };
10782
11990
  const scheduleReconnect = (reason) => {
10783
11991
  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;
11992
+ if (nextConn.reconnectTimer) return;
11993
+ const delay = RECONNECT_DELAYS_MS[Math.min(nextConn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
11994
+ nextConn.reconnectAttempt += 1;
10787
11995
  logger.warn(`[reconnect] ${reason}; retry in ${delay}ms`);
10788
- conn.reconnectTimer = setTimeout(() => {
10789
- conn.reconnectTimer = null;
11996
+ nextConn.reconnectTimer = setTimeout(() => {
11997
+ nextConn.reconnectTimer = null;
10790
11998
  void connect();
10791
11999
  }, delay);
10792
12000
  };
10793
12001
  const startHeartbeat = (intervalMs) => {
10794
- if (conn.heartbeatTimer) {
10795
- clearInterval(conn.heartbeatTimer);
12002
+ if (nextConn.heartbeatTimer) {
12003
+ clearInterval(nextConn.heartbeatTimer);
10796
12004
  }
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);
12005
+ nextConn.heartbeatTimer = setInterval(() => {
12006
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
12007
+ const payload = JSON.stringify({ op: 1, d: nextConn.lastSeq });
12008
+ nextConn.socket.send(payload);
10801
12009
  }, intervalMs);
10802
12010
  };
10803
12011
  const sendIdentify = (token) => {
10804
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
12012
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
10805
12013
  const payload = {
10806
12014
  op: 2,
10807
12015
  d: {
@@ -10810,10 +12018,10 @@ async function monitorQQBotProvider(opts = {}) {
10810
12018
  shard: [0, 1]
10811
12019
  }
10812
12020
  };
10813
- conn.socket.send(JSON.stringify(payload));
12021
+ nextConn.socket.send(JSON.stringify(payload));
10814
12022
  };
10815
12023
  const sendResume = (token, session, seq) => {
10816
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) return;
12024
+ if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
10817
12025
  const payload = {
10818
12026
  op: 6,
10819
12027
  d: {
@@ -10822,11 +12030,14 @@ async function monitorQQBotProvider(opts = {}) {
10822
12030
  seq
10823
12031
  }
10824
12032
  };
10825
- conn.socket.send(JSON.stringify(payload));
12033
+ nextConn.socket.send(JSON.stringify(payload));
10826
12034
  };
10827
- const handleGatewayPayload = async (payload) => {
12035
+ const handleGatewayPayload = async (payload, activeSocket) => {
12036
+ if (stopped || nextConn.socket !== activeSocket) {
12037
+ return;
12038
+ }
10828
12039
  if (typeof payload.s === "number") {
10829
- conn.lastSeq = payload.s;
12040
+ nextConn.lastSeq = payload.s;
10830
12041
  }
10831
12042
  switch (payload.op) {
10832
12043
  case 10: {
@@ -10834,8 +12045,11 @@ async function monitorQQBotProvider(opts = {}) {
10834
12045
  const interval = hello?.heartbeat_interval ?? 3e4;
10835
12046
  startHeartbeat(interval);
10836
12047
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
10837
- if (conn.sessionId && typeof conn.lastSeq === "number") {
10838
- sendResume(token, conn.sessionId, conn.lastSeq);
12048
+ if (stopped || nextConn.socket !== activeSocket) {
12049
+ return;
12050
+ }
12051
+ if (nextConn.sessionId && typeof nextConn.lastSeq === "number") {
12052
+ sendResume(token, nextConn.sessionId, nextConn.lastSeq);
10839
12053
  } else {
10840
12054
  sendIdentify(token);
10841
12055
  }
@@ -10845,14 +12059,18 @@ async function monitorQQBotProvider(opts = {}) {
10845
12059
  setStatus?.({ lastEventAt: Date.now() });
10846
12060
  return;
10847
12061
  case 7:
10848
- cleanupSocket(conn);
12062
+ if (!cleanupSocket(nextConn, activeSocket)) {
12063
+ return;
12064
+ }
10849
12065
  scheduleReconnect("server requested reconnect");
10850
12066
  return;
10851
12067
  case 9:
10852
- conn.sessionId = null;
10853
- conn.lastSeq = null;
12068
+ nextConn.sessionId = null;
12069
+ nextConn.lastSeq = null;
10854
12070
  clearTokenCache(qqCfg.appId);
10855
- cleanupSocket(conn);
12071
+ if (!cleanupSocket(nextConn, activeSocket)) {
12072
+ return;
12073
+ }
10856
12074
  scheduleReconnect("invalid session");
10857
12075
  return;
10858
12076
  case 0: {
@@ -10860,14 +12078,14 @@ async function monitorQQBotProvider(opts = {}) {
10860
12078
  if (eventType === "READY") {
10861
12079
  const ready = payload.d;
10862
12080
  if (ready?.session_id) {
10863
- conn.sessionId = ready.session_id;
12081
+ nextConn.sessionId = ready.session_id;
10864
12082
  }
10865
- conn.reconnectAttempt = 0;
12083
+ nextConn.reconnectAttempt = 0;
10866
12084
  logger.info("gateway ready");
10867
12085
  return;
10868
12086
  }
10869
12087
  if (eventType === "RESUMED") {
10870
- conn.reconnectAttempt = 0;
12088
+ nextConn.reconnectAttempt = 0;
10871
12089
  logger.info("gateway resumed");
10872
12090
  return;
10873
12091
  }
@@ -10888,15 +12106,21 @@ async function monitorQQBotProvider(opts = {}) {
10888
12106
  }
10889
12107
  };
10890
12108
  const connect = async () => {
10891
- if (stopped || conn.connecting) return;
10892
- conn.connecting = true;
12109
+ if (stopped || nextConn.connecting) return;
12110
+ nextConn.connecting = true;
10893
12111
  try {
10894
- cleanupSocket(conn);
12112
+ cleanupSocket(nextConn);
10895
12113
  const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
12114
+ if (stopped) return;
10896
12115
  const gatewayUrl = await getGatewayUrl(token);
12116
+ if (stopped) return;
10897
12117
  logger.info(`connecting gateway: ${gatewayUrl}`);
10898
12118
  const ws = new WebSocket(gatewayUrl);
10899
- conn.socket = ws;
12119
+ nextConn.socket = ws;
12120
+ if (stopped) {
12121
+ cleanupSocket(nextConn, ws);
12122
+ return;
12123
+ }
10900
12124
  ws.on("open", () => {
10901
12125
  logger.info("gateway socket opened");
10902
12126
  });
@@ -10909,24 +12133,29 @@ async function monitorQQBotProvider(opts = {}) {
10909
12133
  logger.warn(`failed to parse gateway payload: ${String(err)}`);
10910
12134
  return;
10911
12135
  }
10912
- void handleGatewayPayload(payload).catch((err) => {
12136
+ void handleGatewayPayload(payload, ws).catch((err) => {
10913
12137
  logger.error(`gateway dispatch error: ${String(err)}`);
10914
12138
  });
10915
12139
  });
10916
12140
  ws.on("close", (code, reason) => {
12141
+ if (!cleanupSocket(nextConn, ws)) {
12142
+ return;
12143
+ }
10917
12144
  logger.warn(`gateway socket closed (${code}) ${String(reason)}`);
10918
- cleanupSocket(conn);
10919
12145
  scheduleReconnect("socket closed");
10920
12146
  });
10921
12147
  ws.on("error", (err) => {
12148
+ if (stopped || nextConn.socket !== ws) {
12149
+ return;
12150
+ }
10922
12151
  logger.error(`gateway socket error: ${String(err)}`);
10923
12152
  });
10924
12153
  } catch (err) {
10925
12154
  logger.error(`gateway connect failed: ${formatGatewayConnectError(err)}`);
10926
- cleanupSocket(conn);
12155
+ cleanupSocket(nextConn);
10927
12156
  scheduleReconnect("connect failed");
10928
12157
  } finally {
10929
- conn.connecting = false;
12158
+ nextConn.connecting = false;
10930
12159
  }
10931
12160
  };
10932
12161
  if (abortSignal?.aborted) {
@@ -10936,7 +12165,7 @@ async function monitorQQBotProvider(opts = {}) {
10936
12165
  abortSignal?.addEventListener("abort", onAbort, { once: true });
10937
12166
  void connect();
10938
12167
  });
10939
- return conn.promise;
12168
+ return nextConn.promise;
10940
12169
  }
10941
12170
  function stopQQBotMonitorForAccount(accountId = DEFAULT_ACCOUNT_ID) {
10942
12171
  const conn = activeConnections.get(accountId);
@@ -10973,7 +12202,8 @@ function resolveQQBotAccount(params) {
10973
12202
  configured,
10974
12203
  appId: credentials?.appId,
10975
12204
  markdownSupport: merged.markdownSupport ?? true,
10976
- c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only"
12205
+ c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only",
12206
+ c2cMarkdownChunkStrategy: merged.c2cMarkdownChunkStrategy ?? "markdown-block"
10977
12207
  };
10978
12208
  }
10979
12209
  var qqbotPlugin = {
@@ -11051,6 +12281,10 @@ var qqbotPlugin = {
11051
12281
  defaultAccount: { type: "string" },
11052
12282
  appId: { type: ["string", "number"] },
11053
12283
  clientSecret: { type: "string" },
12284
+ displayAliases: {
12285
+ type: "object",
12286
+ additionalProperties: { type: "string" }
12287
+ },
11054
12288
  asr: {
11055
12289
  type: "object",
11056
12290
  additionalProperties: false,
@@ -11066,6 +12300,10 @@ var qqbotPlugin = {
11066
12300
  type: "string",
11067
12301
  enum: ["passive", "proactive-table-only", "proactive-all"]
11068
12302
  },
12303
+ c2cMarkdownChunkStrategy: {
12304
+ type: "string",
12305
+ enum: ["markdown-block", "length"]
12306
+ },
11069
12307
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11070
12308
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11071
12309
  requireMention: { type: "boolean" },
@@ -11096,6 +12334,10 @@ var qqbotPlugin = {
11096
12334
  enabled: { type: "boolean" },
11097
12335
  appId: { type: ["string", "number"] },
11098
12336
  clientSecret: { type: "string" },
12337
+ displayAliases: {
12338
+ type: "object",
12339
+ additionalProperties: { type: "string" }
12340
+ },
11099
12341
  asr: {
11100
12342
  type: "object",
11101
12343
  additionalProperties: false,
@@ -11111,6 +12353,10 @@ var qqbotPlugin = {
11111
12353
  type: "string",
11112
12354
  enum: ["passive", "proactive-table-only", "proactive-all"]
11113
12355
  },
12356
+ c2cMarkdownChunkStrategy: {
12357
+ type: "string",
12358
+ enum: ["markdown-block", "length"]
12359
+ },
11114
12360
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11115
12361
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11116
12362
  requireMention: { type: "boolean" },
@@ -11262,7 +12508,9 @@ var qqbotPlugin = {
11262
12508
  ctx.log?.info(`[qqbot] starting gateway for account ${ctx.accountId}`);
11263
12509
  if (ctx.runtime) {
11264
12510
  const candidate = ctx.runtime;
11265
- if (candidate.channel?.routing?.resolveAgentRoute && candidate.channel?.reply?.dispatchReplyFromConfig) {
12511
+ const hasRouting = Boolean(candidate.channel?.routing?.resolveAgentRoute);
12512
+ const hasReply = Boolean(candidate.channel?.reply?.dispatchReplyWithDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyFromConfig);
12513
+ if (hasRouting && hasReply) {
11266
12514
  setQQBotRuntime(ctx.runtime);
11267
12515
  }
11268
12516
  }
@@ -11298,6 +12546,10 @@ var plugin = {
11298
12546
  defaultAccount: { type: "string" },
11299
12547
  appId: { type: ["string", "number"] },
11300
12548
  clientSecret: { type: "string" },
12549
+ displayAliases: {
12550
+ type: "object",
12551
+ additionalProperties: { type: "string" }
12552
+ },
11301
12553
  asr: {
11302
12554
  type: "object",
11303
12555
  additionalProperties: false,
@@ -11313,6 +12565,10 @@ var plugin = {
11313
12565
  type: "string",
11314
12566
  enum: ["passive", "proactive-table-only", "proactive-all"]
11315
12567
  },
12568
+ c2cMarkdownChunkStrategy: {
12569
+ type: "string",
12570
+ enum: ["markdown-block", "length"]
12571
+ },
11316
12572
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11317
12573
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11318
12574
  requireMention: { type: "boolean" },
@@ -11343,6 +12599,10 @@ var plugin = {
11343
12599
  enabled: { type: "boolean" },
11344
12600
  appId: { type: ["string", "number"] },
11345
12601
  clientSecret: { type: "string" },
12602
+ displayAliases: {
12603
+ type: "object",
12604
+ additionalProperties: { type: "string" }
12605
+ },
11346
12606
  asr: {
11347
12607
  type: "object",
11348
12608
  additionalProperties: false,
@@ -11358,6 +12618,10 @@ var plugin = {
11358
12618
  type: "string",
11359
12619
  enum: ["passive", "proactive-table-only", "proactive-all"]
11360
12620
  },
12621
+ c2cMarkdownChunkStrategy: {
12622
+ type: "string",
12623
+ enum: ["markdown-block", "length"]
12624
+ },
11361
12625
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
11362
12626
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
11363
12627
  requireMention: { type: "boolean" },