@openclaw-china/qqbot 2026.3.9 → 2026.3.11

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
@@ -1,9 +1,9 @@
1
1
  import * as os from 'os';
2
2
  import { homedir, tmpdir } from 'os';
3
3
  import * as path2 from 'path';
4
- import { join } from 'path';
4
+ import { join, dirname } from 'path';
5
5
  import * as fs3 from 'fs';
6
- import { existsSync } from 'fs';
6
+ import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
7
7
  import { fileURLToPath } from 'url';
8
8
  import * as fsPromises from 'fs/promises';
9
9
  import { createHmac } from 'crypto';
@@ -14,6 +14,7 @@ import 'util';
14
14
  import { createRequire } from 'module';
15
15
  import { execFileSync } from 'child_process';
16
16
  import WebSocket from 'ws';
17
+ import { Buffer as Buffer$1 } from 'buffer';
17
18
 
18
19
  var __create = Object.create;
19
20
  var __defProp = Object.defineProperty;
@@ -4222,6 +4223,7 @@ var optionalCoercedString = external_exports.preprocess(
4222
4223
  (value) => toTrimmedString(value),
4223
4224
  external_exports.string().min(1).optional()
4224
4225
  );
4226
+ var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
4225
4227
  var QQBotAccountSchema = external_exports.object({
4226
4228
  name: external_exports.string().optional(),
4227
4229
  enabled: external_exports.boolean().optional(),
@@ -4234,6 +4236,7 @@ var QQBotAccountSchema = external_exports.object({
4234
4236
  secretKey: optionalCoercedString
4235
4237
  }).optional(),
4236
4238
  markdownSupport: external_exports.boolean().optional().default(true),
4239
+ c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4237
4240
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4238
4241
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4239
4242
  requireMention: external_exports.boolean().optional().default(true),
@@ -4245,6 +4248,7 @@ var QQBotAccountSchema = external_exports.object({
4245
4248
  longTaskNoticeDelayMs: external_exports.number().int().min(0).optional().default(3e4),
4246
4249
  maxFileSizeMB: external_exports.number().positive().optional().default(100),
4247
4250
  mediaTimeoutMs: external_exports.number().int().positive().optional().default(3e4),
4251
+ autoSendLocalPathMedia: external_exports.boolean().optional().default(true),
4248
4252
  inboundMedia: external_exports.object({
4249
4253
  dir: external_exports.string().optional(),
4250
4254
  keepDays: external_exports.number().optional()
@@ -4264,6 +4268,9 @@ function resolveInboundMediaKeepDays(config) {
4264
4268
  const value = config?.inboundMedia?.keepDays;
4265
4269
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : DEFAULT_INBOUND_MEDIA_KEEP_DAYS;
4266
4270
  }
4271
+ function resolveQQBotAutoSendLocalPathMedia(config) {
4272
+ return config?.autoSendLocalPathMedia ?? true;
4273
+ }
4267
4274
  function resolveInboundMediaTempDir() {
4268
4275
  return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
4269
4276
  }
@@ -4316,6 +4323,156 @@ function resolveQQBotASRCredentials(config) {
4316
4323
  };
4317
4324
  }
4318
4325
 
4326
+ // src/onboarding.ts
4327
+ function isPromptCancelled(value) {
4328
+ return typeof value === "symbol";
4329
+ }
4330
+ function setQQBotCredentials(params) {
4331
+ const existing = params.cfg.channels?.qqbot ?? {};
4332
+ if (params.accountId === DEFAULT_ACCOUNT_ID) {
4333
+ return {
4334
+ ...params.cfg,
4335
+ channels: {
4336
+ ...params.cfg.channels,
4337
+ qqbot: {
4338
+ ...existing,
4339
+ enabled: true,
4340
+ appId: params.appId,
4341
+ clientSecret: params.clientSecret
4342
+ }
4343
+ }
4344
+ };
4345
+ }
4346
+ const accounts = existing.accounts ?? {};
4347
+ return {
4348
+ ...params.cfg,
4349
+ channels: {
4350
+ ...params.cfg.channels,
4351
+ qqbot: {
4352
+ ...existing,
4353
+ enabled: true,
4354
+ accounts: {
4355
+ ...accounts,
4356
+ [params.accountId]: {
4357
+ ...accounts[params.accountId],
4358
+ enabled: true,
4359
+ appId: params.appId,
4360
+ clientSecret: params.clientSecret
4361
+ }
4362
+ }
4363
+ }
4364
+ }
4365
+ };
4366
+ }
4367
+ async function noteQQBotCredentialHelp(prompter) {
4368
+ await prompter.note(
4369
+ [
4370
+ "1) \u6253\u5F00 QQ \u5F00\u653E\u5E73\u53F0 (https://q.qq.com/)",
4371
+ "2) \u521B\u5EFA\u673A\u5668\u4EBA\u5E94\u7528\uFF0C\u83B7\u53D6 AppID \u548C ClientSecret",
4372
+ "3) \u5728\u5F00\u53D1\u8BBE\u7F6E\u4E2D\u914D\u7F6E\u6C99\u7BB1\u6210\u5458\u6216\u6D4B\u8BD5\u7FA4",
4373
+ "4) \u914D\u7F6E\u5B8C\u6210\u540E\u53EF\u4F7F\u7528 openclaw gateway \u542F\u52A8\u8FDE\u63A5",
4374
+ "",
4375
+ '\u547D\u4EE4\u884C\u4E5F\u652F\u6301\uFF1Aopenclaw channels add --channel qqbot --token "AppID:ClientSecret"'
4376
+ ].join("\n"),
4377
+ "QQ Bot \u914D\u7F6E"
4378
+ );
4379
+ }
4380
+ function resolveOnboardingAccountId(params) {
4381
+ const override = params.accountOverrides?.qqbot?.trim();
4382
+ if (override) return override;
4383
+ const defaultAccountId = resolveDefaultQQBotAccountId(params.cfg);
4384
+ const accountIds = listQQBotAccountIds(params.cfg);
4385
+ if (!params.shouldPromptAccountIds || accountIds.length <= 1) {
4386
+ return defaultAccountId;
4387
+ }
4388
+ return params.prompter.select({
4389
+ message: "\u9009\u62E9\u8981\u914D\u7F6E\u7684 QQ Bot \u8D26\u6237",
4390
+ options: accountIds.map((accountId) => ({
4391
+ value: accountId,
4392
+ label: accountId === DEFAULT_ACCOUNT_ID ? "\u9ED8\u8BA4\u8D26\u6237" : accountId
4393
+ })),
4394
+ initialValue: defaultAccountId
4395
+ }).then((selected) => isPromptCancelled(selected) ? defaultAccountId : selected);
4396
+ }
4397
+ var qqbotOnboardingAdapter = {
4398
+ channel: "qqbot",
4399
+ getStatus: async (params) => {
4400
+ const accountIds = listQQBotAccountIds(params.cfg);
4401
+ const configuredAccountId = accountIds.find(
4402
+ (accountId) => Boolean(resolveQQBotCredentials(mergeQQBotAccountConfig(params.cfg, accountId)))
4403
+ );
4404
+ const configured = Boolean(configuredAccountId);
4405
+ const defaultAccountId = resolveDefaultQQBotAccountId(params.cfg);
4406
+ const statusLines = configured ? [
4407
+ configuredAccountId && configuredAccountId !== DEFAULT_ACCOUNT_ID ? `QQ Bot: \u5DF2\u914D\u7F6E (${configuredAccountId})` : `QQ Bot: \u5DF2\u914D\u7F6E${defaultAccountId !== DEFAULT_ACCOUNT_ID ? ` (default=${defaultAccountId})` : ""}`
4408
+ ] : ["QQ Bot: \u9700\u8981 AppID \u548C ClientSecret"];
4409
+ return {
4410
+ channel: "qqbot",
4411
+ configured,
4412
+ statusLines,
4413
+ selectionHint: configured ? "\u5DF2\u914D\u7F6E" : "\u9700\u8981 AppID \u548C ClientSecret",
4414
+ quickstartScore: configured ? 2 : 0
4415
+ };
4416
+ },
4417
+ configure: async (params) => {
4418
+ const accountId = await resolveOnboardingAccountId(params);
4419
+ const merged = mergeQQBotAccountConfig(params.cfg, accountId);
4420
+ const configured = Boolean(resolveQQBotCredentials(merged));
4421
+ let next = params.cfg;
4422
+ let appId = null;
4423
+ let clientSecret = null;
4424
+ if (!configured) {
4425
+ await noteQQBotCredentialHelp(params.prompter);
4426
+ } else {
4427
+ const keepCurrent = await params.prompter.confirm({
4428
+ message: accountId === DEFAULT_ACCOUNT_ID ? "QQ Bot \u51ED\u8BC1\u5DF2\u914D\u7F6E\uFF0C\u662F\u5426\u4FDD\u7559\u5F53\u524D\u914D\u7F6E\uFF1F" : `\u8D26\u6237 ${accountId} \u7684 QQ Bot \u51ED\u8BC1\u5DF2\u914D\u7F6E\uFF0C\u662F\u5426\u4FDD\u7559\u5F53\u524D\u914D\u7F6E\uFF1F`,
4429
+ initialValue: true
4430
+ });
4431
+ if (keepCurrent) {
4432
+ return { cfg: next, accountId };
4433
+ }
4434
+ }
4435
+ const nextAppId = await params.prompter.text({
4436
+ message: "\u8BF7\u8F93\u5165 QQ Bot AppID",
4437
+ placeholder: "\u4F8B\u5982: 102146862",
4438
+ initialValue: typeof merged.appId === "string" ? merged.appId : void 0,
4439
+ validate: (value) => String(value ?? "").trim() ? void 0 : "AppID \u4E0D\u80FD\u4E3A\u7A7A"
4440
+ });
4441
+ if (isPromptCancelled(nextAppId)) {
4442
+ return { cfg: next, accountId };
4443
+ }
4444
+ appId = String(nextAppId).trim();
4445
+ const nextClientSecret = await params.prompter.text({
4446
+ message: "\u8BF7\u8F93\u5165 QQ Bot ClientSecret",
4447
+ placeholder: "\u4F60\u7684 ClientSecret",
4448
+ validate: (value) => String(value ?? "").trim() ? void 0 : "ClientSecret \u4E0D\u80FD\u4E3A\u7A7A"
4449
+ });
4450
+ if (isPromptCancelled(nextClientSecret)) {
4451
+ return { cfg: next, accountId };
4452
+ }
4453
+ clientSecret = String(nextClientSecret).trim();
4454
+ if (appId && clientSecret) {
4455
+ next = setQQBotCredentials({
4456
+ cfg: next,
4457
+ accountId,
4458
+ appId,
4459
+ clientSecret
4460
+ });
4461
+ }
4462
+ return { cfg: next, accountId };
4463
+ },
4464
+ disable: (cfg) => ({
4465
+ ...cfg,
4466
+ channels: {
4467
+ ...cfg.channels,
4468
+ qqbot: {
4469
+ ...cfg.channels?.qqbot ?? {},
4470
+ enabled: false
4471
+ }
4472
+ }
4473
+ })
4474
+ };
4475
+
4319
4476
  // ../../packages/shared/src/logger/logger.ts
4320
4477
  function createLogger(prefix, opts) {
4321
4478
  const logFn = opts?.log ?? console.log;
@@ -4719,7 +4876,7 @@ function extractMediaFromText(text, options = {}) {
4719
4876
  const {
4720
4877
  removeFromText = true,
4721
4878
  checkExists = false,
4722
- existsSync: existsSync5,
4879
+ existsSync: existsSync6,
4723
4880
  parseMediaLines = false,
4724
4881
  parseMarkdownImages = true,
4725
4882
  parseHtmlImages = true,
@@ -4734,7 +4891,7 @@ function extractMediaFromText(text, options = {}) {
4734
4891
  const key = media.localPath || media.source;
4735
4892
  if (seenSources.has(key)) return false;
4736
4893
  if (checkExists && media.isLocal && media.localPath) {
4737
- const exists = existsSync5 ? existsSync5(media.localPath) : fs3.existsSync(media.localPath);
4894
+ const exists = existsSync6 ? existsSync6(media.localPath) : fs3.existsSync(media.localPath);
4738
4895
  if (!exists) return false;
4739
4896
  }
4740
4897
  seenSources.add(key);
@@ -6529,6 +6686,13 @@ function getChannelConfig(cfg, channelId) {
6529
6686
  const existing = channels[channelId];
6530
6687
  return isRecord(existing) ? existing : {};
6531
6688
  }
6689
+ function getGatewayAuthToken(cfg) {
6690
+ if (!isRecord(cfg.gateway)) {
6691
+ return void 0;
6692
+ }
6693
+ const auth = isRecord(cfg.gateway.auth) ? cfg.gateway.auth : void 0;
6694
+ return toTrimmedString2(auth?.token);
6695
+ }
6532
6696
  function getPreferredAccountConfig(channelCfg) {
6533
6697
  const accounts = channelCfg.accounts;
6534
6698
  if (!isRecord(accounts)) {
@@ -6690,11 +6854,22 @@ async function configureDingtalk(prompter, cfg) {
6690
6854
  "\u542F\u7528 AI Card \u6D41\u5F0F\u56DE\u590D\uFF08\u63A8\u8350\u5173\u95ED\uFF0C\u4F7F\u7528\u975E\u6D41\u5F0F\uFF09",
6691
6855
  toBoolean(existing.enableAICard, false)
6692
6856
  );
6693
- return mergeChannelConfig(cfg, "dingtalk", {
6857
+ const patch = {
6694
6858
  clientId,
6695
6859
  clientSecret,
6696
6860
  enableAICard
6697
- });
6861
+ };
6862
+ if (enableAICard) {
6863
+ const gatewayToken = await prompter.askSecret({
6864
+ label: "OpenClaw Gateway Token\uFF08\u6D41\u5F0F\u8F93\u51FA\u5FC5\u9700\uFF1B\u7559\u7A7A\u5219\u4F7F\u7528\u5168\u5C40 gateway.auth.token\uFF09",
6865
+ existingValue: toTrimmedString2(existing.gatewayToken) ?? getGatewayAuthToken(cfg),
6866
+ required: false
6867
+ });
6868
+ if (gatewayToken.trim()) {
6869
+ patch.gatewayToken = gatewayToken;
6870
+ }
6871
+ }
6872
+ return mergeChannelConfig(cfg, "dingtalk", patch);
6698
6873
  }
6699
6874
  async function configureFeishu(prompter, cfg) {
6700
6875
  section("\u914D\u7F6E Feishu\uFF08\u98DE\u4E66\uFF09");
@@ -7083,9 +7258,11 @@ function showChinaInstallHint(api) {
7083
7258
  var API_BASE = "https://api.sgroup.qq.com";
7084
7259
  var TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
7085
7260
  var MSG_SEQ_BASE = 1e6;
7261
+ var MAX_DUPLICATE_MSG_SEQ_RETRIES = 5;
7086
7262
  var tokenCacheMap = /* @__PURE__ */ new Map();
7087
7263
  var tokenPromiseMap = /* @__PURE__ */ new Map();
7088
7264
  var msgSeqMap = /* @__PURE__ */ new Map();
7265
+ var fallbackMsgSeq = 0;
7089
7266
  function toTrimmedString3(value) {
7090
7267
  if (value === void 0 || value === null) return void 0;
7091
7268
  const next = String(value).trim();
@@ -7105,7 +7282,10 @@ function sanitizeUploadFileName(fileName) {
7105
7282
  return normalized || "file";
7106
7283
  }
7107
7284
  function nextMsgSeq(sequenceKey) {
7108
- if (!sequenceKey) return MSG_SEQ_BASE + 1;
7285
+ if (!sequenceKey) {
7286
+ fallbackMsgSeq += 1;
7287
+ return MSG_SEQ_BASE + fallbackMsgSeq;
7288
+ }
7109
7289
  const current = msgSeqMap.get(sequenceKey) ?? 0;
7110
7290
  const next = current + 1;
7111
7291
  msgSeqMap.set(sequenceKey, next);
@@ -7117,6 +7297,46 @@ function nextMsgSeq(sequenceKey) {
7117
7297
  }
7118
7298
  return MSG_SEQ_BASE + next;
7119
7299
  }
7300
+ function resolveMsgSeqKey(messageId, eventId) {
7301
+ if (messageId) return `msg:${messageId}`;
7302
+ if (eventId) return `event:${eventId}`;
7303
+ return void 0;
7304
+ }
7305
+ function isDuplicateMsgSeqError(err) {
7306
+ if (!(err instanceof HttpError) || err.status !== 400) {
7307
+ return false;
7308
+ }
7309
+ const body = err.body?.trim();
7310
+ if (!body) {
7311
+ return false;
7312
+ }
7313
+ try {
7314
+ const parsed = JSON.parse(body);
7315
+ if (parsed.code === 40054005 || parsed.err_code === 40054005) {
7316
+ return true;
7317
+ }
7318
+ const message = typeof parsed.message === "string" ? parsed.message.toLowerCase() : "";
7319
+ return message.includes("msgseq") && (message.includes("\u53BB\u91CD") || message.includes("duplicate"));
7320
+ } catch {
7321
+ const lowered = body.toLowerCase();
7322
+ return lowered.includes("msgseq") && (lowered.includes("\u53BB\u91CD") || lowered.includes("duplicate"));
7323
+ }
7324
+ }
7325
+ async function postPassiveMessage(params) {
7326
+ let lastError;
7327
+ for (let attempt = 0; attempt <= MAX_DUPLICATE_MSG_SEQ_RETRIES; attempt += 1) {
7328
+ const msgSeq = nextMsgSeq(params.sequenceKey);
7329
+ try {
7330
+ return await apiPost(params.accessToken, params.path, params.buildBody(msgSeq), params.options);
7331
+ } catch (err) {
7332
+ lastError = err;
7333
+ if (!isDuplicateMsgSeqError(err) || attempt === MAX_DUPLICATE_MSG_SEQ_RETRIES) {
7334
+ throw err;
7335
+ }
7336
+ }
7337
+ }
7338
+ throw lastError;
7339
+ }
7120
7340
  function clearTokenCache(appId) {
7121
7341
  const normalizedAppId = toTrimmedString3(appId);
7122
7342
  if (normalizedAppId) {
@@ -7185,16 +7405,11 @@ async function getGatewayUrl(accessToken) {
7185
7405
  return data.url;
7186
7406
  }
7187
7407
  function buildMessageBody(params) {
7188
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7189
- const body = params.markdown ? {
7190
- markdown: { content: params.content },
7191
- msg_type: 2,
7192
- msg_seq: msgSeq
7193
- } : {
7408
+ const body = buildTextMessageBody({
7194
7409
  content: params.content,
7195
- msg_type: 0,
7196
- msg_seq: msgSeq
7197
- };
7410
+ markdown: params.markdown
7411
+ });
7412
+ body.msg_seq = params.msgSeq;
7198
7413
  if (params.messageId) {
7199
7414
  body.msg_id = params.messageId;
7200
7415
  } else if (params.eventId) {
@@ -7202,22 +7417,63 @@ function buildMessageBody(params) {
7202
7417
  }
7203
7418
  return body;
7204
7419
  }
7420
+ function buildTextMessageBody(params) {
7421
+ return params.markdown ? {
7422
+ markdown: { content: params.content },
7423
+ msg_type: 2
7424
+ } : {
7425
+ content: params.content,
7426
+ msg_type: 0
7427
+ };
7428
+ }
7429
+ function buildProactiveMessageBody(params) {
7430
+ if (!params.content.trim()) {
7431
+ throw new Error("QQBot proactive message content is empty");
7432
+ }
7433
+ return buildTextMessageBody(params);
7434
+ }
7205
7435
  async function sendC2CMessage(params) {
7206
- const body = buildMessageBody({
7436
+ return postPassiveMessage({
7437
+ accessToken: params.accessToken,
7438
+ path: `/v2/users/${params.openid}/messages`,
7439
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7440
+ options: { timeout: 15e3 },
7441
+ buildBody: (msgSeq) => buildMessageBody({
7442
+ content: params.content,
7443
+ messageId: params.messageId,
7444
+ eventId: params.eventId,
7445
+ markdown: params.markdown,
7446
+ msgSeq
7447
+ })
7448
+ });
7449
+ }
7450
+ async function sendGroupMessage(params) {
7451
+ return postPassiveMessage({
7452
+ accessToken: params.accessToken,
7453
+ path: `/v2/groups/${params.groupOpenid}/messages`,
7454
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7455
+ options: { timeout: 15e3 },
7456
+ buildBody: (msgSeq) => buildMessageBody({
7457
+ content: params.content,
7458
+ messageId: params.messageId,
7459
+ eventId: params.eventId,
7460
+ markdown: params.markdown,
7461
+ msgSeq
7462
+ })
7463
+ });
7464
+ }
7465
+ async function sendProactiveC2CMessage(params) {
7466
+ const body = buildProactiveMessageBody({
7207
7467
  content: params.content,
7208
- messageId: params.messageId,
7209
- eventId: params.eventId,
7210
7468
  markdown: params.markdown
7211
7469
  });
7212
7470
  return apiPost(params.accessToken, `/v2/users/${params.openid}/messages`, body, {
7213
7471
  timeout: 15e3
7214
7472
  });
7215
7473
  }
7216
- async function sendGroupMessage(params) {
7217
- const body = buildMessageBody({
7474
+ async function sendProactiveGroupMessage(params) {
7475
+ const body = buildProactiveMessageBody({
7218
7476
  content: params.content,
7219
- messageId: params.messageId,
7220
- eventId: params.eventId,
7221
7477
  markdown: params.markdown
7222
7478
  });
7223
7479
  return apiPost(params.accessToken, `/v2/groups/${params.groupOpenid}/messages`, body, {
@@ -7234,11 +7490,12 @@ async function sendChannelMessage(params) {
7234
7490
  });
7235
7491
  }
7236
7492
  async function sendC2CInputNotify(params) {
7237
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7238
- await apiPost(
7239
- params.accessToken,
7240
- `/v2/users/${params.openid}/messages`,
7241
- {
7493
+ await postPassiveMessage({
7494
+ accessToken: params.accessToken,
7495
+ path: `/v2/users/${params.openid}/messages`,
7496
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7497
+ options: { timeout: 15e3 },
7498
+ buildBody: (msgSeq) => ({
7242
7499
  msg_type: 6,
7243
7500
  input_notify: {
7244
7501
  input_type: 1,
@@ -7246,9 +7503,8 @@ async function sendC2CInputNotify(params) {
7246
7503
  },
7247
7504
  msg_seq: msgSeq,
7248
7505
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7249
- },
7250
- { timeout: 15e3 }
7251
- );
7506
+ })
7507
+ });
7252
7508
  }
7253
7509
  async function uploadC2CMedia(params) {
7254
7510
  const body = {
@@ -7289,34 +7545,34 @@ async function uploadGroupMedia(params) {
7289
7545
  });
7290
7546
  }
7291
7547
  async function sendC2CMediaMessage(params) {
7292
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7293
- return apiPost(
7294
- params.accessToken,
7295
- `/v2/users/${params.openid}/messages`,
7296
- {
7548
+ return postPassiveMessage({
7549
+ accessToken: params.accessToken,
7550
+ path: `/v2/users/${params.openid}/messages`,
7551
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7552
+ options: { timeout: 15e3 },
7553
+ buildBody: (msgSeq) => ({
7297
7554
  msg_type: 7,
7298
7555
  media: { file_info: params.fileInfo },
7299
7556
  msg_seq: msgSeq,
7300
7557
  ...params.content ? { content: params.content } : {},
7301
7558
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7302
- },
7303
- { timeout: 15e3 }
7304
- );
7559
+ })
7560
+ });
7305
7561
  }
7306
7562
  async function sendGroupMediaMessage(params) {
7307
- const msgSeq = nextMsgSeq(params.messageId ?? params.eventId);
7308
- return apiPost(
7309
- params.accessToken,
7310
- `/v2/groups/${params.groupOpenid}/messages`,
7311
- {
7563
+ return postPassiveMessage({
7564
+ accessToken: params.accessToken,
7565
+ path: `/v2/groups/${params.groupOpenid}/messages`,
7566
+ sequenceKey: resolveMsgSeqKey(params.messageId, params.eventId),
7567
+ options: { timeout: 15e3 },
7568
+ buildBody: (msgSeq) => ({
7312
7569
  msg_type: 7,
7313
7570
  media: { file_info: params.fileInfo },
7314
7571
  msg_seq: msgSeq,
7315
7572
  ...params.content ? { content: params.content } : {},
7316
7573
  ...params.messageId ? { msg_id: params.messageId } : params.eventId ? { event_id: params.eventId } : {}
7317
- },
7318
- { timeout: 15e3 }
7319
- );
7574
+ })
7575
+ });
7320
7576
  }
7321
7577
  var require2 = createRequire(import.meta.url);
7322
7578
  function resolveQQBotMediaFileType(fileName) {
@@ -7569,6 +7825,14 @@ function logEventIdFallback(params) {
7569
7825
  }
7570
7826
  console.info(detail);
7571
7827
  }
7828
+ function logQQBotOutboundDispatch(params) {
7829
+ const accountLabel = params.accountId?.trim() || DEFAULT_ACCOUNT_ID;
7830
+ const textLength = typeof params.text === "string" ? params.text.length : 0;
7831
+ const mediaLabel = params.mediaUrl ? ` media=${shortId(params.mediaUrl)}` : "";
7832
+ console.info(
7833
+ `[qqbot] outbound action=${params.action} api=${params.api} accountId=${accountLabel} target=${params.targetKind}:${shortId(params.targetId)} markdown=${params.markdown ? "yes" : "no"} replyToId=${params.replyToId ? "yes" : "no"} replyEventId=${params.replyEventId ? "yes" : "no"} textLen=${textLength}${mediaLabel}`
7834
+ );
7835
+ }
7572
7836
  function shouldRetryWithEventId(err) {
7573
7837
  const status = err instanceof HttpError ? err.status : void 0;
7574
7838
  let body = "";
@@ -7589,6 +7853,15 @@ function shouldRetryWithEventId(err) {
7589
7853
  function shouldSendTextAsFollowupForMedia(mediaUrl) {
7590
7854
  return detectMediaType(stripTitleFromUrl(mediaUrl)) === "file";
7591
7855
  }
7856
+ function buildPassiveReplyRefs(params) {
7857
+ if (params.replyToId) {
7858
+ return { messageId: params.replyToId };
7859
+ }
7860
+ if (params.replyEventId) {
7861
+ return { eventId: params.replyEventId };
7862
+ }
7863
+ return {};
7864
+ }
7592
7865
  var qqbotOutbound = {
7593
7866
  deliveryMode: "direct",
7594
7867
  textChunkLimit: 1500,
@@ -7606,13 +7879,42 @@ var qqbotOutbound = {
7606
7879
  const groupMarkdown = false;
7607
7880
  try {
7608
7881
  if (target.kind === "group") {
7882
+ if (!replyToId && !replyEventId) {
7883
+ logQQBotOutboundDispatch({
7884
+ action: "text",
7885
+ api: "sendProactiveGroupMessage",
7886
+ accountId,
7887
+ targetKind: target.kind,
7888
+ targetId: target.id,
7889
+ markdown,
7890
+ text
7891
+ });
7892
+ const result3 = await sendProactiveGroupMessage({
7893
+ accessToken,
7894
+ groupOpenid: target.id,
7895
+ content: text,
7896
+ markdown
7897
+ });
7898
+ return { channel: "qqbot", messageId: result3.id, timestamp: result3.timestamp };
7899
+ }
7609
7900
  let result2;
7610
7901
  try {
7902
+ logQQBotOutboundDispatch({
7903
+ action: "text",
7904
+ api: "sendGroupMessage",
7905
+ accountId,
7906
+ targetKind: target.kind,
7907
+ targetId: target.id,
7908
+ markdown: groupMarkdown,
7909
+ replyToId,
7910
+ replyEventId,
7911
+ text
7912
+ });
7611
7913
  result2 = await sendGroupMessage({
7612
7914
  accessToken,
7613
7915
  groupOpenid: target.id,
7614
7916
  content: text,
7615
- messageId: replyToId,
7917
+ ...buildPassiveReplyRefs({ replyToId, replyEventId }),
7616
7918
  markdown: groupMarkdown
7617
7919
  });
7618
7920
  } catch (err) {
@@ -7663,6 +7965,15 @@ var qqbotOutbound = {
7663
7965
  return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
7664
7966
  }
7665
7967
  if (target.kind === "channel") {
7968
+ logQQBotOutboundDispatch({
7969
+ action: "text",
7970
+ api: "sendChannelMessage",
7971
+ accountId,
7972
+ targetKind: target.kind,
7973
+ targetId: target.id,
7974
+ replyToId,
7975
+ text
7976
+ });
7666
7977
  const result2 = await sendChannelMessage({
7667
7978
  accessToken,
7668
7979
  channelId: target.id,
@@ -7671,13 +7982,42 @@ var qqbotOutbound = {
7671
7982
  });
7672
7983
  return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
7673
7984
  }
7985
+ if (!replyToId && !replyEventId) {
7986
+ logQQBotOutboundDispatch({
7987
+ action: "text",
7988
+ api: "sendProactiveC2CMessage",
7989
+ accountId,
7990
+ targetKind: target.kind,
7991
+ targetId: target.id,
7992
+ markdown,
7993
+ text
7994
+ });
7995
+ const result2 = await sendProactiveC2CMessage({
7996
+ accessToken,
7997
+ openid: target.id,
7998
+ content: text,
7999
+ markdown
8000
+ });
8001
+ return { channel: "qqbot", messageId: result2.id, timestamp: result2.timestamp };
8002
+ }
7674
8003
  let result;
7675
8004
  try {
8005
+ logQQBotOutboundDispatch({
8006
+ action: "text",
8007
+ api: "sendC2CMessage",
8008
+ accountId,
8009
+ targetKind: target.kind,
8010
+ targetId: target.id,
8011
+ markdown,
8012
+ replyToId,
8013
+ replyEventId,
8014
+ text
8015
+ });
7676
8016
  result = await sendC2CMessage({
7677
8017
  accessToken,
7678
8018
  openid: target.id,
7679
8019
  content: text,
7680
- messageId: replyToId,
8020
+ ...buildPassiveReplyRefs({ replyToId, replyEventId }),
7681
8021
  markdown
7682
8022
  });
7683
8023
  } catch (err) {
@@ -7755,6 +8095,17 @@ ${mediaUrl}` : mediaUrl;
7755
8095
  try {
7756
8096
  let result;
7757
8097
  try {
8098
+ logQQBotOutboundDispatch({
8099
+ action: "media",
8100
+ api: "sendFileQQBot",
8101
+ accountId,
8102
+ targetKind: target.kind,
8103
+ targetId: target.id,
8104
+ replyToId,
8105
+ replyEventId,
8106
+ text: sendTextAsFollowup ? void 0 : trimmedText,
8107
+ mediaUrl
8108
+ });
7758
8109
  result = await sendFileQQBot({
7759
8110
  cfg: qqCfg,
7760
8111
  target: { kind: target.kind, id: target.id },
@@ -7904,6 +8255,451 @@ ${mediaUrl}` : mediaUrl;
7904
8255
 
7905
8256
  // src/logger.ts
7906
8257
  createLogger("qqbot");
8258
+ var DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE = {
8259
+ width: 512,
8260
+ height: 512
8261
+ };
8262
+ var MARKDOWN_IMAGE_RE2 = /!\[([^\]]*)\]\(([^)\n]+)\)/g;
8263
+ var BARE_HTTP_IMAGE_URL_RE = /(?<![(\["'<])(https?:\/\/[^\s)"'<>]+\.(?:png|jpe?g|gif|webp)(?:\?[^\s)"'<>]*)?)/gi;
8264
+ var FENCED_CODE_BLOCK_RE = /(^|\n)(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g;
8265
+ function parsePngSize(buffer) {
8266
+ if (buffer.length < 24) return null;
8267
+ if (buffer[0] !== 137 || buffer[1] !== 80 || buffer[2] !== 78 || buffer[3] !== 71) {
8268
+ return null;
8269
+ }
8270
+ return {
8271
+ width: buffer.readUInt32BE(16),
8272
+ height: buffer.readUInt32BE(20)
8273
+ };
8274
+ }
8275
+ function parseJpegSize(buffer) {
8276
+ if (buffer.length < 4 || buffer[0] !== 255 || buffer[1] !== 216) {
8277
+ return null;
8278
+ }
8279
+ let offset = 2;
8280
+ while (offset < buffer.length - 9) {
8281
+ if (buffer[offset] !== 255) {
8282
+ offset += 1;
8283
+ continue;
8284
+ }
8285
+ const marker = buffer[offset + 1];
8286
+ if (marker === 192 || marker === 194) {
8287
+ return {
8288
+ height: buffer.readUInt16BE(offset + 5),
8289
+ width: buffer.readUInt16BE(offset + 7)
8290
+ };
8291
+ }
8292
+ if (offset + 3 >= buffer.length) {
8293
+ break;
8294
+ }
8295
+ const blockLength = buffer.readUInt16BE(offset + 2);
8296
+ offset += 2 + blockLength;
8297
+ }
8298
+ return null;
8299
+ }
8300
+ function parseGifSize(buffer) {
8301
+ if (buffer.length < 10) return null;
8302
+ const signature = buffer.toString("ascii", 0, 6);
8303
+ if (signature !== "GIF87a" && signature !== "GIF89a") {
8304
+ return null;
8305
+ }
8306
+ return {
8307
+ width: buffer.readUInt16LE(6),
8308
+ height: buffer.readUInt16LE(8)
8309
+ };
8310
+ }
8311
+ function parseWebpSize(buffer) {
8312
+ if (buffer.length < 30) return null;
8313
+ if (buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WEBP") {
8314
+ return null;
8315
+ }
8316
+ const chunkType = buffer.toString("ascii", 12, 16);
8317
+ if (chunkType === "VP8 " && buffer[23] === 157 && buffer[24] === 1 && buffer[25] === 42) {
8318
+ return {
8319
+ width: buffer.readUInt16LE(26) & 16383,
8320
+ height: buffer.readUInt16LE(28) & 16383
8321
+ };
8322
+ }
8323
+ if (chunkType === "VP8L" && buffer[20] === 47) {
8324
+ const bits = buffer.readUInt32LE(21);
8325
+ return {
8326
+ width: (bits & 16383) + 1,
8327
+ height: (bits >> 14 & 16383) + 1
8328
+ };
8329
+ }
8330
+ if (chunkType === "VP8X") {
8331
+ return {
8332
+ width: (buffer[24] | buffer[25] << 8 | buffer[26] << 16) + 1,
8333
+ height: (buffer[27] | buffer[28] << 8 | buffer[29] << 16) + 1
8334
+ };
8335
+ }
8336
+ return null;
8337
+ }
8338
+ function parseImageSize(buffer) {
8339
+ return parsePngSize(buffer) ?? parseJpegSize(buffer) ?? parseGifSize(buffer) ?? parseWebpSize(buffer);
8340
+ }
8341
+ function normalizeImageSize(size) {
8342
+ if (!size) return null;
8343
+ if (!Number.isFinite(size.width) || !Number.isFinite(size.height)) return null;
8344
+ if (size.width <= 0 || size.height <= 0) return null;
8345
+ return {
8346
+ width: Math.round(size.width),
8347
+ height: Math.round(size.height)
8348
+ };
8349
+ }
8350
+ function splitMarkdownImageDestination(rawDestination) {
8351
+ let next = rawDestination.trim();
8352
+ const whitespaceIndex = next.search(/\s/);
8353
+ if (whitespaceIndex >= 0) {
8354
+ next = next.slice(0, whitespaceIndex);
8355
+ }
8356
+ if (next.startsWith("<") && next.endsWith(">")) {
8357
+ next = next.slice(1, -1).trim();
8358
+ }
8359
+ return next;
8360
+ }
8361
+ function splitFencedCodeBlocks(text) {
8362
+ const segments = [];
8363
+ const re = new RegExp(FENCED_CODE_BLOCK_RE.source, FENCED_CODE_BLOCK_RE.flags);
8364
+ let lastIndex = 0;
8365
+ let match;
8366
+ while ((match = re.exec(text)) !== null) {
8367
+ const leading = match[1] ?? "";
8368
+ const codeStart = match.index + leading.length;
8369
+ if (codeStart > lastIndex) {
8370
+ segments.push({ kind: "text", value: text.slice(lastIndex, codeStart) });
8371
+ }
8372
+ segments.push({ kind: "code", value: text.slice(codeStart, re.lastIndex) });
8373
+ lastIndex = re.lastIndex;
8374
+ }
8375
+ if (lastIndex < text.length) {
8376
+ segments.push({ kind: "text", value: text.slice(lastIndex) });
8377
+ }
8378
+ return segments;
8379
+ }
8380
+ async function replaceAsync(input2, pattern, replacer) {
8381
+ const re = new RegExp(pattern.source, pattern.flags);
8382
+ let result = "";
8383
+ let lastIndex = 0;
8384
+ let match;
8385
+ while ((match = re.exec(input2)) !== null) {
8386
+ result += input2.slice(lastIndex, match.index);
8387
+ result += await replacer(match);
8388
+ lastIndex = match.index + match[0].length;
8389
+ if (match[0].length === 0) {
8390
+ re.lastIndex += 1;
8391
+ }
8392
+ }
8393
+ result += input2.slice(lastIndex);
8394
+ return result;
8395
+ }
8396
+ async function getResolvedImageSize(params) {
8397
+ const { url, cache, resolveImageSize } = params;
8398
+ const existing = cache.get(url);
8399
+ if (existing) {
8400
+ return existing;
8401
+ }
8402
+ const pending = resolveImageSize(url).then((size) => normalizeImageSize(size) ?? DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE).catch(() => DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE);
8403
+ cache.set(url, pending);
8404
+ return pending;
8405
+ }
8406
+ async function normalizeTextSegment(params) {
8407
+ const { text, seenImageUrls, imageSizeCache, resolveImageSize } = params;
8408
+ const withMarkdownImages = await replaceAsync(text, MARKDOWN_IMAGE_RE2, async (match) => {
8409
+ const fullMatch = match[0];
8410
+ const destination = splitMarkdownImageDestination(match[2] ?? "");
8411
+ if (!isQQBotHttpImageUrl(destination)) {
8412
+ return fullMatch;
8413
+ }
8414
+ seenImageUrls.add(destination);
8415
+ if (hasQQBotMarkdownImageSize(fullMatch)) {
8416
+ return fullMatch;
8417
+ }
8418
+ const size = await getResolvedImageSize({
8419
+ url: destination,
8420
+ cache: imageSizeCache,
8421
+ resolveImageSize
8422
+ });
8423
+ return formatQQBotMarkdownImage(destination, size);
8424
+ });
8425
+ return replaceAsync(withMarkdownImages, BARE_HTTP_IMAGE_URL_RE, async (match) => {
8426
+ const url = match[1] ?? match[0];
8427
+ if (!isQQBotHttpImageUrl(url)) {
8428
+ return match[0];
8429
+ }
8430
+ seenImageUrls.add(url);
8431
+ const size = await getResolvedImageSize({
8432
+ url,
8433
+ cache: imageSizeCache,
8434
+ resolveImageSize
8435
+ });
8436
+ return formatQQBotMarkdownImage(url, size);
8437
+ });
8438
+ }
8439
+ function isQQBotHttpImageUrl(url) {
8440
+ const trimmed = url.trim();
8441
+ if (!/^https?:\/\//i.test(trimmed)) {
8442
+ return false;
8443
+ }
8444
+ try {
8445
+ const parsed = new URL(trimmed);
8446
+ return /\.(?:png|jpe?g|gif|webp)$/i.test(parsed.pathname);
8447
+ } catch {
8448
+ return /\.(?:png|jpe?g|gif|webp)(?:\?[^\s)"'<>]*)?$/i.test(trimmed);
8449
+ }
8450
+ }
8451
+ async function getQQBotHttpImageSize(url, timeoutMs = 5e3) {
8452
+ if (!isQQBotHttpImageUrl(url)) {
8453
+ return null;
8454
+ }
8455
+ const controller = new AbortController();
8456
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
8457
+ try {
8458
+ const response = await fetch(url, {
8459
+ signal: controller.signal,
8460
+ headers: {
8461
+ Range: "bytes=0-65535",
8462
+ "User-Agent": "OpenClaw-QQBot-ImageSize/1.0"
8463
+ }
8464
+ });
8465
+ if (!(response.ok || response.status === 206)) {
8466
+ return null;
8467
+ }
8468
+ const buffer = Buffer$1.from(await response.arrayBuffer());
8469
+ return normalizeImageSize(parseImageSize(buffer));
8470
+ } catch {
8471
+ return null;
8472
+ } finally {
8473
+ clearTimeout(timeoutId);
8474
+ }
8475
+ }
8476
+ function formatQQBotMarkdownImage(url, size) {
8477
+ const resolved = normalizeImageSize(size) ?? DEFAULT_QQBOT_MARKDOWN_IMAGE_SIZE;
8478
+ return `![#${resolved.width}px #${resolved.height}px](${url})`;
8479
+ }
8480
+ function hasQQBotMarkdownImageSize(markdownImage) {
8481
+ return /!\[#\d+px\s+#\d+px\]\([^)]+\)/.test(markdownImage);
8482
+ }
8483
+ async function normalizeQQBotMarkdownImages(params) {
8484
+ const text = params.text ?? "";
8485
+ const appendImageUrls = params.appendImageUrls ?? [];
8486
+ const resolveImageSize = params.resolveImageSize ?? ((url) => getQQBotHttpImageSize(url, params.timeoutMs));
8487
+ const imageSizeCache = /* @__PURE__ */ new Map();
8488
+ const seenImageUrls = /* @__PURE__ */ new Set();
8489
+ const segments = splitFencedCodeBlocks(text);
8490
+ const normalizedSegments = [];
8491
+ for (const segment of segments) {
8492
+ if (segment.kind === "code") {
8493
+ normalizedSegments.push(segment.value);
8494
+ continue;
8495
+ }
8496
+ normalizedSegments.push(
8497
+ await normalizeTextSegment({
8498
+ text: segment.value,
8499
+ seenImageUrls,
8500
+ imageSizeCache,
8501
+ resolveImageSize
8502
+ })
8503
+ );
8504
+ }
8505
+ const appendedImages = [];
8506
+ for (const rawUrl of appendImageUrls) {
8507
+ const url = rawUrl.trim();
8508
+ if (!isQQBotHttpImageUrl(url) || seenImageUrls.has(url)) {
8509
+ continue;
8510
+ }
8511
+ seenImageUrls.add(url);
8512
+ const size = await getResolvedImageSize({
8513
+ url,
8514
+ cache: imageSizeCache,
8515
+ resolveImageSize
8516
+ });
8517
+ appendedImages.push(formatQQBotMarkdownImage(url, size));
8518
+ }
8519
+ const body = normalizedSegments.join("").trim();
8520
+ if (appendedImages.length === 0) {
8521
+ return body;
8522
+ }
8523
+ if (!body) {
8524
+ return appendedImages.join("\n");
8525
+ }
8526
+ return `${body}
8527
+
8528
+ ${appendedImages.join("\n")}`;
8529
+ }
8530
+ var DEFAULT_KNOWN_TARGETS_PATH = join(homedir(), ".openclaw", "data", "qqbot", "known-targets.json");
8531
+ function resolveKnownTargetsFilePath(options) {
8532
+ return options?.filePath?.trim() || DEFAULT_KNOWN_TARGETS_PATH;
8533
+ }
8534
+ function ensureKnownTargetsDir(filePath) {
8535
+ mkdirSync(dirname(filePath), { recursive: true });
8536
+ }
8537
+ function compareTargetsByLastSeenDesc(a, b) {
8538
+ if (b.lastSeenAt !== a.lastSeenAt) {
8539
+ return b.lastSeenAt - a.lastSeenAt;
8540
+ }
8541
+ return a.target.localeCompare(b.target);
8542
+ }
8543
+ function normalizeKnownQQBotTarget(target) {
8544
+ const accountId = target.accountId.trim() || DEFAULT_ACCOUNT_ID;
8545
+ const normalized = {
8546
+ accountId,
8547
+ kind: target.kind,
8548
+ target: target.target.trim(),
8549
+ sourceChatType: target.sourceChatType,
8550
+ firstSeenAt: Math.trunc(target.firstSeenAt),
8551
+ lastSeenAt: Math.trunc(target.lastSeenAt)
8552
+ };
8553
+ const displayName = target.displayName?.trim();
8554
+ if (displayName) {
8555
+ normalized.displayName = displayName;
8556
+ }
8557
+ return normalized;
8558
+ }
8559
+ function parseKnownTargets(raw, filePath) {
8560
+ const parsed = JSON.parse(raw);
8561
+ if (!Array.isArray(parsed)) {
8562
+ throw new Error(`Invalid known QQBot targets file: ${filePath}`);
8563
+ }
8564
+ return parsed.filter((entry) => {
8565
+ if (!entry || typeof entry !== "object") return false;
8566
+ const candidate = entry;
8567
+ return typeof candidate.accountId === "string" && typeof candidate.kind === "string" && typeof candidate.target === "string" && typeof candidate.sourceChatType === "string" && typeof candidate.firstSeenAt === "number" && typeof candidate.lastSeenAt === "number";
8568
+ }).map((entry) => normalizeKnownQQBotTarget(entry)).filter((entry) => entry.target.length > 0);
8569
+ }
8570
+ function readKnownTargets(options) {
8571
+ const filePath = resolveKnownTargetsFilePath(options);
8572
+ if (!existsSync(filePath)) {
8573
+ return [];
8574
+ }
8575
+ const raw = readFileSync(filePath, "utf8");
8576
+ if (!raw.trim()) {
8577
+ return [];
8578
+ }
8579
+ return parseKnownTargets(raw, filePath).sort(compareTargetsByLastSeenDesc);
8580
+ }
8581
+ function writeKnownTargets(targets, options) {
8582
+ const filePath = resolveKnownTargetsFilePath(options);
8583
+ if (targets.length === 0) {
8584
+ if (existsSync(filePath)) {
8585
+ rmSync(filePath, { force: true });
8586
+ }
8587
+ return;
8588
+ }
8589
+ ensureKnownTargetsDir(filePath);
8590
+ writeFileSync(filePath, `${JSON.stringify(targets, null, 2)}
8591
+ `, "utf8");
8592
+ }
8593
+ function upsertKnownQQBotTarget(params) {
8594
+ const next = normalizeKnownQQBotTarget(params.target);
8595
+ if (!next.target) {
8596
+ throw new Error("Known QQBot target requires a non-empty target");
8597
+ }
8598
+ const targets = readKnownTargets(params);
8599
+ const index = targets.findIndex(
8600
+ (entry) => entry.accountId === next.accountId && entry.target === next.target
8601
+ );
8602
+ if (index >= 0) {
8603
+ const existing = targets[index];
8604
+ targets[index] = {
8605
+ ...existing,
8606
+ kind: next.kind,
8607
+ sourceChatType: next.sourceChatType,
8608
+ displayName: next.displayName ?? existing.displayName,
8609
+ lastSeenAt: next.lastSeenAt
8610
+ };
8611
+ } else {
8612
+ targets.push(next);
8613
+ }
8614
+ targets.sort(compareTargetsByLastSeenDesc);
8615
+ writeKnownTargets(targets, params);
8616
+ return index >= 0 ? targets.find((entry) => entry.accountId === next.accountId && entry.target === next.target) : next;
8617
+ }
8618
+ function listKnownQQBotTargets(params = {}) {
8619
+ let targets = readKnownTargets(params);
8620
+ if (params.accountId?.trim()) {
8621
+ targets = targets.filter((entry) => entry.accountId === params.accountId?.trim());
8622
+ }
8623
+ if (params.kind) {
8624
+ targets = targets.filter((entry) => entry.kind === params.kind);
8625
+ }
8626
+ if (typeof params.limit === "number" && params.limit > 0) {
8627
+ targets = targets.slice(0, params.limit);
8628
+ }
8629
+ return targets;
8630
+ }
8631
+ function getKnownQQBotTarget(params) {
8632
+ const target = params.target.trim();
8633
+ if (!target) return void 0;
8634
+ const matches = readKnownTargets(params).filter((entry) => {
8635
+ if (entry.target !== target) return false;
8636
+ if (params.accountId?.trim()) {
8637
+ return entry.accountId === params.accountId.trim();
8638
+ }
8639
+ return true;
8640
+ });
8641
+ return matches[0];
8642
+ }
8643
+ function removeKnownQQBotTarget(params) {
8644
+ const target = params.target.trim();
8645
+ if (!target) return false;
8646
+ const before = readKnownTargets(params);
8647
+ const filtered = before.filter((entry) => {
8648
+ if (entry.target !== target) return true;
8649
+ if (params.accountId?.trim()) {
8650
+ return entry.accountId !== params.accountId.trim();
8651
+ }
8652
+ return false;
8653
+ });
8654
+ if (filtered.length === before.length) {
8655
+ return false;
8656
+ }
8657
+ writeKnownTargets(filtered, params);
8658
+ return true;
8659
+ }
8660
+ function clearKnownQQBotTargets(params = {}) {
8661
+ const before = readKnownTargets(params);
8662
+ const filtered = before.filter((entry) => {
8663
+ if (params.accountId?.trim() && entry.accountId !== params.accountId.trim()) {
8664
+ return true;
8665
+ }
8666
+ if (params.kind && entry.kind !== params.kind) {
8667
+ return true;
8668
+ }
8669
+ return false;
8670
+ });
8671
+ const removed = before.length - filtered.length;
8672
+ if (removed === 0) {
8673
+ return 0;
8674
+ }
8675
+ writeKnownTargets(filtered, params);
8676
+ return removed;
8677
+ }
8678
+ async function sendProactiveQQBotMessage(params) {
8679
+ const to = params.to.trim();
8680
+ if (!to) {
8681
+ return { channel: "qqbot", error: "to is required for proactive send" };
8682
+ }
8683
+ if (params.mediaUrl?.trim()) {
8684
+ return qqbotOutbound.sendMedia({
8685
+ cfg: params.cfg,
8686
+ to,
8687
+ mediaUrl: params.mediaUrl.trim(),
8688
+ text: params.text,
8689
+ accountId: params.accountId
8690
+ });
8691
+ }
8692
+ const text = params.text?.trim();
8693
+ if (!text) {
8694
+ return { channel: "qqbot", error: "text or mediaUrl is required for proactive send" };
8695
+ }
8696
+ return qqbotOutbound.sendText({
8697
+ cfg: params.cfg,
8698
+ to,
8699
+ text,
8700
+ accountId: params.accountId
8701
+ });
8702
+ }
7907
8703
 
7908
8704
  // src/runtime.ts
7909
8705
  var runtime = null;
@@ -7916,6 +8712,24 @@ function getQQBotRuntime() {
7916
8712
  }
7917
8713
  return runtime;
7918
8714
  }
8715
+ var sessionDispatchQueue = /* @__PURE__ */ new Map();
8716
+ function buildSessionDispatchQueueKey(route) {
8717
+ const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
8718
+ return `${accountId}:${route.sessionKey}`;
8719
+ }
8720
+ async function runSerializedSessionDispatch(queueKey, task) {
8721
+ const previous = sessionDispatchQueue.get(queueKey) ?? Promise.resolve();
8722
+ const run = previous.catch(() => void 0).then(task);
8723
+ const cleanup = run.then(() => void 0, () => void 0);
8724
+ sessionDispatchQueue.set(queueKey, cleanup);
8725
+ try {
8726
+ return await run;
8727
+ } finally {
8728
+ if (sessionDispatchQueue.get(queueKey) === cleanup) {
8729
+ sessionDispatchQueue.delete(queueKey);
8730
+ }
8731
+ }
8732
+ }
7919
8733
  function toString(value) {
7920
8734
  if (typeof value === "string" && value.trim()) return value;
7921
8735
  return void 0;
@@ -8341,26 +9155,100 @@ function resolveEnvelopeFrom(event) {
8341
9155
  }
8342
9156
  return event.senderName?.trim() || event.senderId;
8343
9157
  }
9158
+ function resolveKnownQQBotTargetFromInbound(params) {
9159
+ const { inbound, accountId } = params;
9160
+ if (inbound.type === "direct") {
9161
+ if (!inbound.c2cOpenid?.trim()) {
9162
+ return void 0;
9163
+ }
9164
+ return {
9165
+ accountId,
9166
+ kind: "user",
9167
+ target: `user:${inbound.c2cOpenid}`,
9168
+ displayName: inbound.senderName,
9169
+ sourceChatType: "direct",
9170
+ firstSeenAt: inbound.timestamp,
9171
+ lastSeenAt: inbound.timestamp
9172
+ };
9173
+ }
9174
+ if (inbound.type === "group" && inbound.groupOpenid?.trim()) {
9175
+ return {
9176
+ accountId,
9177
+ kind: "group",
9178
+ target: `group:${inbound.groupOpenid}`,
9179
+ displayName: inbound.senderName,
9180
+ sourceChatType: "group",
9181
+ firstSeenAt: inbound.timestamp,
9182
+ lastSeenAt: inbound.timestamp
9183
+ };
9184
+ }
9185
+ if (inbound.type === "channel" && inbound.channelId?.trim()) {
9186
+ return {
9187
+ accountId,
9188
+ kind: "channel",
9189
+ target: `channel:${inbound.channelId}`,
9190
+ displayName: inbound.senderName,
9191
+ sourceChatType: "channel",
9192
+ firstSeenAt: inbound.timestamp,
9193
+ lastSeenAt: inbound.timestamp
9194
+ };
9195
+ }
9196
+ return void 0;
9197
+ }
8344
9198
  function extractLocalMediaFromText(params) {
8345
9199
  const { text, logger } = params;
8346
- const result = extractMediaFromText(text, {
8347
- removeFromText: true,
8348
- checkExists: true,
8349
- existsSync: (p) => {
8350
- const exists = fs3.existsSync(p);
8351
- if (!exists) {
8352
- logger?.warn?.(`[media] local file not found: ${p}`);
8353
- }
8354
- return exists;
8355
- },
8356
- parseMediaLines: false,
8357
- parseMarkdownImages: true,
8358
- parseHtmlImages: false,
8359
- parseBarePaths: true,
8360
- parseMarkdownLinks: true
9200
+ const mediaUrls = [];
9201
+ const seenMedia = /* @__PURE__ */ new Set();
9202
+ let nextText = text;
9203
+ const MARKDOWN_LINKED_IMAGE_RE2 = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g;
9204
+ const MARKDOWN_IMAGE_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
9205
+ const MARKDOWN_LINK_RE2 = /\[([^\]]*)\]\(([^)]+)\)/g;
9206
+ const BARE_LOCAL_MEDIA_PATH_RE = /`?((?:\/(?:tmp|var|private|Users|home|root)\/[^\s`'",)]+|[A-Za-z]:[\\/][^\s`'",)]+)\.(?:png|jpg|jpeg|gif|bmp|webp|svg|ico|mp3|wav|ogg|m4a|amr|flac|aac|wma|mp4|mov|avi|mkv|webm|flv|wmv|m4v))`?/gi;
9207
+ const collectLocalRichMedia = (rawValue, allowedTypes) => {
9208
+ const candidate = stripTitleFromUrl(rawValue.trim());
9209
+ if (!candidate || !isLocalReference(candidate)) {
9210
+ return void 0;
9211
+ }
9212
+ if (!fs3.existsSync(candidate)) {
9213
+ logger?.warn?.(`[media] local file not found: ${candidate}`);
9214
+ return void 0;
9215
+ }
9216
+ const mediaType = detectMediaType(candidate);
9217
+ if (mediaType === "file") {
9218
+ return void 0;
9219
+ }
9220
+ if (allowedTypes && !allowedTypes.has(mediaType)) {
9221
+ return void 0;
9222
+ }
9223
+ if (seenMedia.has(candidate)) {
9224
+ return candidate;
9225
+ }
9226
+ seenMedia.add(candidate);
9227
+ mediaUrls.push(candidate);
9228
+ return candidate;
9229
+ };
9230
+ nextText = nextText.replace(MARKDOWN_LINKED_IMAGE_RE2, (fullMatch, _alt, rawPath) => {
9231
+ return collectLocalRichMedia(rawPath) ? "" : fullMatch;
8361
9232
  });
8362
- const mediaUrls = result.all.filter((m) => m.isLocal && typeof m.localPath === "string").filter((m) => m.type !== "file").map((m) => m.localPath);
8363
- return { text: result.text, mediaUrls };
9233
+ nextText = nextText.replace(MARKDOWN_IMAGE_RE3, (fullMatch, _alt, rawPath) => {
9234
+ return collectLocalRichMedia(rawPath) ? "" : fullMatch;
9235
+ });
9236
+ nextText = nextText.replace(MARKDOWN_LINK_RE2, (fullMatch, _label, rawPath) => {
9237
+ const mediaPath = collectLocalRichMedia(rawPath, /* @__PURE__ */ new Set(["audio", "video"]));
9238
+ if (!mediaPath) {
9239
+ return fullMatch;
9240
+ }
9241
+ return "";
9242
+ });
9243
+ nextText = nextText.replace(BARE_LOCAL_MEDIA_PATH_RE, (fullMatch, rawPath) => {
9244
+ return collectLocalRichMedia(rawPath) ? "" : fullMatch;
9245
+ });
9246
+ nextText = nextText.replace(/[ \t]+\n/g, "\n");
9247
+ nextText = nextText.replace(/\n{3,}/g, "\n\n");
9248
+ return {
9249
+ text: nextText.trim(),
9250
+ mediaUrls
9251
+ };
8364
9252
  }
8365
9253
  function extractMediaLinesFromText(params) {
8366
9254
  const { text, logger } = params;
@@ -8383,6 +9271,23 @@ function extractMediaLinesFromText(params) {
8383
9271
  const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
8384
9272
  return { text: result.text, mediaUrls };
8385
9273
  }
9274
+ function extractQQBotReplyMedia(params) {
9275
+ const mediaLineResult = extractMediaLinesFromText({
9276
+ text: params.text,
9277
+ logger: params.logger
9278
+ });
9279
+ if (!params.autoSendLocalPathMedia) {
9280
+ return mediaLineResult;
9281
+ }
9282
+ const localMediaResult = extractLocalMediaFromText({
9283
+ text: mediaLineResult.text,
9284
+ logger: params.logger
9285
+ });
9286
+ return {
9287
+ text: localMediaResult.text,
9288
+ mediaUrls: [.../* @__PURE__ */ new Set([...mediaLineResult.mediaUrls, ...localMediaResult.mediaUrls])]
9289
+ };
9290
+ }
8386
9291
  function buildMediaFallbackText(mediaUrl) {
8387
9292
  if (!/^https?:\/\//i.test(mediaUrl)) {
8388
9293
  return void 0;
@@ -8396,6 +9301,9 @@ var FILE_PLACEHOLDER_RE = /\[文件:\s*[^\]\n]+\]/g;
8396
9301
  var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_voice|tts(?::text)?|\/tts(?::text)?)\s*\]\]/gi;
8397
9302
  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;
8398
9303
  var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
9304
+ var MARKDOWN_TABLE_SEPARATOR_RE = /^\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?)?\|?$/;
9305
+ var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
9306
+ var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
8399
9307
  function extractFinalBlocks(text) {
8400
9308
  const matches = Array.from(text.matchAll(FINAL_BLOCK_RE));
8401
9309
  if (matches.length === 0) return void 0;
@@ -8420,6 +9328,14 @@ function sanitizeQQBotOutboundText(rawText) {
8420
9328
  if (/^NO_REPLY$/i.test(next)) return "";
8421
9329
  return next;
8422
9330
  }
9331
+ function formatQQBotOutboundPreview(text, maxLength = 240) {
9332
+ const normalized = text.replace(/\r\n/g, "\n").trim();
9333
+ if (!normalized) {
9334
+ return '""';
9335
+ }
9336
+ const preview = normalized.length > maxLength ? `${normalized.slice(0, Math.max(0, maxLength - 3))}...` : normalized;
9337
+ return JSON.stringify(preview);
9338
+ }
8423
9339
  function shouldSuppressQQBotTextWhenMediaPresent(rawText, sanitizedText) {
8424
9340
  const raw = rawText.trim();
8425
9341
  if (!raw) return false;
@@ -8452,6 +9368,111 @@ function evaluateReplyFinalOnlyDelivery(params) {
8452
9368
  }
8453
9369
  return { skipDelivery: true, suppressText: false };
8454
9370
  }
9371
+ function isQQBotC2CTarget(to) {
9372
+ const trimmed = to.trim();
9373
+ const raw = trimmed.startsWith("qqbot:") ? trimmed.slice("qqbot:".length) : trimmed;
9374
+ return !raw.startsWith("group:") && !raw.startsWith("channel:");
9375
+ }
9376
+ function splitQQBotMarkdownTransportMediaUrls(mediaUrls) {
9377
+ const markdownImageUrls = [];
9378
+ const mediaQueue = [];
9379
+ const seenMarkdownImages = /* @__PURE__ */ new Set();
9380
+ const seenMedia = /* @__PURE__ */ new Set();
9381
+ for (const rawUrl of mediaUrls) {
9382
+ const next = rawUrl.trim();
9383
+ if (!next) continue;
9384
+ if (isQQBotHttpImageUrl(next)) {
9385
+ if (seenMarkdownImages.has(next)) continue;
9386
+ seenMarkdownImages.add(next);
9387
+ markdownImageUrls.push(next);
9388
+ continue;
9389
+ }
9390
+ if (seenMedia.has(next)) continue;
9391
+ seenMedia.add(next);
9392
+ mediaQueue.push(next);
9393
+ }
9394
+ return { markdownImageUrls, mediaQueue };
9395
+ }
9396
+ function hasQQBotMarkdownTable(text) {
9397
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
9398
+ for (let index = 0; index < lines.length - 1; index += 1) {
9399
+ const header = lines[index]?.trim() ?? "";
9400
+ const separator = lines[index + 1]?.trim() ?? "";
9401
+ if (!header.includes("|") || !MARKDOWN_TABLE_SEPARATOR_RE.test(separator)) {
9402
+ continue;
9403
+ }
9404
+ const headerColumns = header.split("|").filter((column) => column.trim()).length;
9405
+ const separatorColumns = separator.split("|").filter((column) => column.trim()).length;
9406
+ if (headerColumns >= 2 && separatorColumns >= 2) {
9407
+ return true;
9408
+ }
9409
+ }
9410
+ return false;
9411
+ }
9412
+ function resolveQQBotTextReplyRefs(params) {
9413
+ const mode = params.c2cMarkdownDeliveryMode ?? "proactive-table-only";
9414
+ const forceProactive = params.markdownSupport && isQQBotC2CTarget(params.to) && (mode === "proactive-all" || mode === "proactive-table-only" && hasQQBotMarkdownTable(params.text));
9415
+ if (!forceProactive) {
9416
+ return {
9417
+ forceProactive: false,
9418
+ replyToId: params.replyToId,
9419
+ replyEventId: params.replyEventId
9420
+ };
9421
+ }
9422
+ return {
9423
+ forceProactive: true,
9424
+ replyToId: void 0,
9425
+ replyEventId: void 0
9426
+ };
9427
+ }
9428
+ function appendQQBotBufferedText(bufferedTexts, nextText) {
9429
+ const normalized = nextText.trim();
9430
+ if (!normalized) return bufferedTexts;
9431
+ if (bufferedTexts.length === 0) return [normalized];
9432
+ const currentCombined = bufferedTexts.join("\n\n");
9433
+ if (currentCombined === normalized || currentCombined.includes(normalized)) {
9434
+ return bufferedTexts;
9435
+ }
9436
+ if (normalized.includes(currentCombined)) {
9437
+ return [normalized];
9438
+ }
9439
+ const last = bufferedTexts[bufferedTexts.length - 1];
9440
+ if (last === normalized) {
9441
+ return bufferedTexts;
9442
+ }
9443
+ return [...bufferedTexts, normalized];
9444
+ }
9445
+ function normalizeQQBotRenderedMarkdown(text) {
9446
+ if (!text.trim()) return "";
9447
+ let next = text.trim();
9448
+ let changed = false;
9449
+ next = next.replace(
9450
+ EXPLICIT_MARKDOWN_FENCE_RE,
9451
+ (block, leadingLineBreak, _fence, inner) => {
9452
+ const normalizedInner = inner.trim();
9453
+ if (!normalizedInner) {
9454
+ return block;
9455
+ }
9456
+ changed = true;
9457
+ return `${leadingLineBreak}${normalizedInner}`;
9458
+ }
9459
+ );
9460
+ next = next.replace(
9461
+ GENERIC_MARKDOWN_FENCE_RE,
9462
+ (block, leadingLineBreak, _fence, inner) => {
9463
+ const normalizedInner = inner.trim();
9464
+ if (!normalizedInner) {
9465
+ return block;
9466
+ }
9467
+ if (!hasQQBotMarkdownTable(normalizedInner)) {
9468
+ return block;
9469
+ }
9470
+ changed = true;
9471
+ return `${leadingLineBreak}${normalizedInner}`;
9472
+ }
9473
+ );
9474
+ return changed ? next.trim() : text.trim();
9475
+ }
8455
9476
  async function sendQQBotMediaWithFallback(params) {
8456
9477
  const { qqCfg, to, mediaQueue, replyToId, replyEventId, logger, onDelivered, onError } = params;
8457
9478
  const outbound = params.outbound ?? qqbotOutbound;
@@ -8518,13 +9539,8 @@ function buildInboundContext(params) {
8518
9539
  };
8519
9540
  }
8520
9541
  async function dispatchToAgent(params) {
8521
- const { inbound, cfg, qqCfg, accountId, logger } = params;
9542
+ const { inbound, cfg, qqCfg, accountId, logger, route } = params;
8522
9543
  const runtime2 = getQQBotRuntime();
8523
- const routing = runtime2.channel?.routing?.resolveAgentRoute;
8524
- if (!routing) {
8525
- logger.warn("routing API not available");
8526
- return;
8527
- }
8528
9544
  const target = resolveChatTarget(inbound);
8529
9545
  if (inbound.c2cOpenid) {
8530
9546
  const typing = await qqbotOutbound.sendTyping({
@@ -8538,12 +9554,6 @@ async function dispatchToAgent(params) {
8538
9554
  logger.warn(`sendTyping failed: ${typing.error}`);
8539
9555
  }
8540
9556
  }
8541
- const route = routing({
8542
- cfg,
8543
- channel: "qqbot",
8544
- accountId,
8545
- peer: { kind: target.peerKind, id: target.peerId }
8546
- });
8547
9557
  const replyApi = runtime2.channel?.reply;
8548
9558
  if (!replyApi) {
8549
9559
  logger.warn("reply API not available");
@@ -8714,17 +9724,99 @@ async function dispatchToAgent(params) {
8714
9724
  return [text];
8715
9725
  };
8716
9726
  const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
9727
+ const markdownSupport = qqCfg.markdownSupport ?? true;
9728
+ const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
9729
+ const useC2CMarkdownTransport = markdownSupport && isQQBotC2CTarget(target.to);
9730
+ let bufferedC2CMarkdownTexts = [];
9731
+ let bufferedC2CMarkdownMediaUrls = [];
9732
+ const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
9733
+ const bufferC2CMarkdownMedia = (url) => {
9734
+ const next = url?.trim();
9735
+ if (!next || bufferedC2CMarkdownMediaSeen.has(next)) return;
9736
+ bufferedC2CMarkdownMediaSeen.add(next);
9737
+ bufferedC2CMarkdownMediaUrls.push(next);
9738
+ };
9739
+ const flushBufferedC2CMarkdownReply = async () => {
9740
+ if (!useC2CMarkdownTransport || bufferedC2CMarkdownTexts.length === 0 && bufferedC2CMarkdownMediaUrls.length === 0) {
9741
+ bufferedC2CMarkdownTexts = [];
9742
+ bufferedC2CMarkdownMediaUrls = [];
9743
+ bufferedC2CMarkdownMediaSeen.clear();
9744
+ return;
9745
+ }
9746
+ const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
9747
+ const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
9748
+ bufferedC2CMarkdownTexts = [];
9749
+ bufferedC2CMarkdownMediaUrls = [];
9750
+ bufferedC2CMarkdownMediaSeen.clear();
9751
+ const normalizedCombinedText = normalizeQQBotRenderedMarkdown(combinedText);
9752
+ const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(combinedMediaUrls);
9753
+ const finalMarkdownText = await normalizeQQBotMarkdownImages({
9754
+ text: normalizedCombinedText,
9755
+ appendImageUrls: markdownImageUrls
9756
+ });
9757
+ const textReplyRefs = resolveQQBotTextReplyRefs({
9758
+ to: target.to,
9759
+ text: finalMarkdownText || normalizedCombinedText,
9760
+ markdownSupport,
9761
+ c2cMarkdownDeliveryMode,
9762
+ replyToId: inbound.messageId,
9763
+ replyEventId: inbound.eventId
9764
+ });
9765
+ const textSegments = finalMarkdownText ? [finalMarkdownText] : [];
9766
+ const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
9767
+ logger.info(
9768
+ `delivery=${deliveryLabel} to=${target.to} segments=${textSegments.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")}`
9769
+ );
9770
+ await sendQQBotMediaWithFallback({
9771
+ qqCfg,
9772
+ to: target.to,
9773
+ mediaQueue,
9774
+ replyToId: textReplyRefs.replyToId,
9775
+ replyEventId: textReplyRefs.replyEventId,
9776
+ logger,
9777
+ onDelivered: () => {
9778
+ markReplyDelivered();
9779
+ },
9780
+ onError: (error) => {
9781
+ markGroupMessageInterfaceBlocked(error);
9782
+ }
9783
+ });
9784
+ if (!finalMarkdownText) {
9785
+ return;
9786
+ }
9787
+ for (let segmentIndex = 0; segmentIndex < textSegments.length; segmentIndex += 1) {
9788
+ const segment = textSegments[segmentIndex] ?? "";
9789
+ const chunks = chunkText(segment);
9790
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
9791
+ const chunk = chunks[chunkIndex] ?? "";
9792
+ logger.info(
9793
+ `delivery=${deliveryLabel} segment=${segmentIndex + 1}/${textSegments.length} chunk=${chunkIndex + 1}/${chunks.length} preview=${formatQQBotOutboundPreview(chunk)}`
9794
+ );
9795
+ const result = await qqbotOutbound.sendText({
9796
+ cfg: { channels: { qqbot: qqCfg } },
9797
+ to: target.to,
9798
+ text: chunk,
9799
+ replyToId: textReplyRefs.replyToId,
9800
+ replyEventId: textReplyRefs.replyEventId
9801
+ });
9802
+ if (result.error) {
9803
+ logger.error(`send buffered QQ markdown reply failed: ${result.error}`);
9804
+ markGroupMessageInterfaceBlocked(result.error);
9805
+ } else {
9806
+ logger.info(`sent buffered QQ markdown reply (len=${chunk.length})`);
9807
+ markReplyDelivered();
9808
+ }
9809
+ }
9810
+ }
9811
+ };
8717
9812
  const deliver = async (payload, info) => {
8718
9813
  const typed = payload;
8719
- const mediaLineResult = extractMediaLinesFromText({
9814
+ const extractedTextMedia = extractQQBotReplyMedia({
8720
9815
  text: typed?.text ?? "",
8721
- logger
8722
- });
8723
- const localMediaResult = extractLocalMediaFromText({
8724
- text: mediaLineResult.text,
8725
- logger
9816
+ logger,
9817
+ autoSendLocalPathMedia: resolveQQBotAutoSendLocalPathMedia(qqCfg)
8726
9818
  });
8727
- const cleanedText = sanitizeQQBotOutboundText(localMediaResult.text);
9819
+ const cleanedText = sanitizeQQBotOutboundText(extractedTextMedia.text);
8728
9820
  const payloadMediaUrls = Array.isArray(typed?.mediaUrls) ? typed?.mediaUrls : typed?.mediaUrl ? [typed.mediaUrl] : [];
8729
9821
  const mediaQueue = [];
8730
9822
  const seenMedia = /* @__PURE__ */ new Set();
@@ -8736,8 +9828,7 @@ async function dispatchToAgent(params) {
8736
9828
  mediaQueue.push(next);
8737
9829
  };
8738
9830
  for (const url of payloadMediaUrls) addMedia(url);
8739
- for (const url of mediaLineResult.mediaUrls) addMedia(url);
8740
- for (const url of localMediaResult.mediaUrls) addMedia(url);
9831
+ for (const url of extractedTextMedia.mediaUrls) addMedia(url);
8741
9832
  const deliveryDecision = evaluateReplyFinalOnlyDelivery({
8742
9833
  replyFinalOnly,
8743
9834
  kind: info?.kind,
@@ -8745,19 +9836,36 @@ async function dispatchToAgent(params) {
8745
9836
  sanitizedText: cleanedText
8746
9837
  });
8747
9838
  if (deliveryDecision.skipDelivery) return;
8748
- const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(localMediaResult.text, cleanedText);
9839
+ const suppressEchoText = mediaQueue.length > 0 && shouldSuppressQQBotTextWhenMediaPresent(extractedTextMedia.text, cleanedText);
8749
9840
  const suppressText = deliveryDecision.suppressText || suppressEchoText;
8750
9841
  const textToSend = suppressText ? "" : cleanedText;
9842
+ if (useC2CMarkdownTransport) {
9843
+ if (textToSend) {
9844
+ bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
9845
+ }
9846
+ for (const url of mediaQueue) {
9847
+ bufferC2CMarkdownMedia(url);
9848
+ }
9849
+ return;
9850
+ }
8751
9851
  if (textToSend) {
8752
9852
  const converted = textApi?.convertMarkdownTables ? textApi.convertMarkdownTables(textToSend, resolvedTableMode) : textToSend;
9853
+ const textReplyRefs = resolveQQBotTextReplyRefs({
9854
+ to: target.to,
9855
+ text: converted,
9856
+ markdownSupport,
9857
+ c2cMarkdownDeliveryMode,
9858
+ replyToId: inbound.messageId,
9859
+ replyEventId: inbound.eventId
9860
+ });
8753
9861
  const chunks = chunkText(converted);
8754
9862
  for (const chunk of chunks) {
8755
9863
  const result = await qqbotOutbound.sendText({
8756
9864
  cfg: { channels: { qqbot: qqCfg } },
8757
9865
  to: target.to,
8758
9866
  text: chunk,
8759
- replyToId: inbound.messageId,
8760
- replyEventId: inbound.eventId
9867
+ replyToId: textReplyRefs.replyToId,
9868
+ replyEventId: textReplyRefs.replyEventId
8761
9869
  });
8762
9870
  if (result.error) {
8763
9871
  logger.error(`sendText failed: ${result.error}`);
@@ -8801,6 +9909,7 @@ async function dispatchToAgent(params) {
8801
9909
  }
8802
9910
  }
8803
9911
  });
9912
+ await flushBufferedC2CMarkdownReply();
8804
9913
  } else {
8805
9914
  const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
8806
9915
  deliver,
@@ -8830,6 +9939,7 @@ async function dispatchToAgent(params) {
8830
9939
  replyOptions: dispatcherResult.replyOptions
8831
9940
  });
8832
9941
  dispatcherResult.markDispatchIdle?.();
9942
+ await flushBufferedC2CMarkdownReply();
8833
9943
  }
8834
9944
  const noReplyFallback = resolveQQBotNoReplyFallback({
8835
9945
  inbound,
@@ -8919,6 +10029,14 @@ async function handleQQBotDispatch(params) {
8919
10029
  if (!shouldHandleMessage(inbound, qqCfg, logger)) {
8920
10030
  return;
8921
10031
  }
10032
+ const knownTarget = resolveKnownQQBotTargetFromInbound({ inbound, accountId });
10033
+ if (knownTarget) {
10034
+ try {
10035
+ upsertKnownQQBotTarget({ target: knownTarget });
10036
+ } catch (err) {
10037
+ logger.warn(`failed to record known qqbot target: ${String(err)}`);
10038
+ }
10039
+ }
8922
10040
  const attachmentCount = inbound.attachments?.length ?? 0;
8923
10041
  if (attachmentCount > 0) {
8924
10042
  logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
@@ -8926,13 +10044,34 @@ async function handleQQBotDispatch(params) {
8926
10044
  if (!content && attachmentCount === 0) {
8927
10045
  return;
8928
10046
  }
8929
- await dispatchToAgent({
8930
- inbound: { ...inbound, content },
10047
+ const runtime2 = getQQBotRuntime();
10048
+ const routing = runtime2.channel?.routing?.resolveAgentRoute;
10049
+ if (!routing) {
10050
+ logger.warn("routing API not available");
10051
+ return;
10052
+ }
10053
+ const target = resolveChatTarget(inbound);
10054
+ const route = routing({
8931
10055
  cfg: params.cfg,
8932
- qqCfg,
10056
+ channel: "qqbot",
8933
10057
  accountId,
8934
- logger
10058
+ peer: { kind: target.peerKind, id: target.peerId }
8935
10059
  });
10060
+ const queueKey = buildSessionDispatchQueueKey(route);
10061
+ if (sessionDispatchQueue.has(queueKey)) {
10062
+ logger.info(`session busy; queueing inbound dispatch sessionKey=${route.sessionKey}`);
10063
+ }
10064
+ await runSerializedSessionDispatch(
10065
+ queueKey,
10066
+ async () => dispatchToAgent({
10067
+ inbound: { ...inbound, content },
10068
+ cfg: params.cfg,
10069
+ qqCfg,
10070
+ accountId,
10071
+ logger,
10072
+ route
10073
+ })
10074
+ );
8936
10075
  }
8937
10076
 
8938
10077
  // src/monitor.ts
@@ -9232,7 +10371,8 @@ function resolveQQBotAccount(params) {
9232
10371
  enabled,
9233
10372
  configured,
9234
10373
  appId: credentials?.appId,
9235
- markdownSupport: merged.markdownSupport ?? true
10374
+ markdownSupport: merged.markdownSupport ?? true,
10375
+ c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only"
9236
10376
  };
9237
10377
  }
9238
10378
  var qqbotPlugin = {
@@ -9248,7 +10388,8 @@ var qqbotPlugin = {
9248
10388
  edit: false,
9249
10389
  reply: true,
9250
10390
  polls: false,
9251
- blockStreaming: false
10391
+ blockStreaming: false,
10392
+ activeSend: true
9252
10393
  },
9253
10394
  messaging: {
9254
10395
  normalizeTarget: (raw) => {
@@ -9320,6 +10461,10 @@ var qqbotPlugin = {
9320
10461
  }
9321
10462
  },
9322
10463
  markdownSupport: { type: "boolean" },
10464
+ c2cMarkdownDeliveryMode: {
10465
+ type: "string",
10466
+ enum: ["passive", "proactive-table-only", "proactive-all"]
10467
+ },
9323
10468
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9324
10469
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9325
10470
  requireMention: { type: "boolean" },
@@ -9331,6 +10476,7 @@ var qqbotPlugin = {
9331
10476
  longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
9332
10477
  maxFileSizeMB: { type: "number" },
9333
10478
  mediaTimeoutMs: { type: "number" },
10479
+ autoSendLocalPathMedia: { type: "boolean" },
9334
10480
  inboundMedia: {
9335
10481
  type: "object",
9336
10482
  additionalProperties: false,
@@ -9360,6 +10506,10 @@ var qqbotPlugin = {
9360
10506
  }
9361
10507
  },
9362
10508
  markdownSupport: { type: "boolean" },
10509
+ c2cMarkdownDeliveryMode: {
10510
+ type: "string",
10511
+ enum: ["passive", "proactive-table-only", "proactive-all"]
10512
+ },
9363
10513
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9364
10514
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9365
10515
  requireMention: { type: "boolean" },
@@ -9371,6 +10521,7 @@ var qqbotPlugin = {
9371
10521
  longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
9372
10522
  maxFileSizeMB: { type: "number" },
9373
10523
  mediaTimeoutMs: { type: "number" },
10524
+ autoSendLocalPathMedia: { type: "boolean" },
9374
10525
  inboundMedia: {
9375
10526
  type: "object",
9376
10527
  additionalProperties: false,
@@ -9386,6 +10537,7 @@ var qqbotPlugin = {
9386
10537
  }
9387
10538
  },
9388
10539
  reload: { configPrefixes: ["channels.qqbot"] },
10540
+ onboarding: qqbotOnboardingAdapter,
9389
10541
  config: {
9390
10542
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
9391
10543
  resolveAccount: (cfg, accountId) => resolveQQBotAccount({ cfg, accountId }),
@@ -9556,6 +10708,10 @@ var plugin = {
9556
10708
  }
9557
10709
  },
9558
10710
  markdownSupport: { type: "boolean" },
10711
+ c2cMarkdownDeliveryMode: {
10712
+ type: "string",
10713
+ enum: ["passive", "proactive-table-only", "proactive-all"]
10714
+ },
9559
10715
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9560
10716
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9561
10717
  requireMention: { type: "boolean" },
@@ -9567,6 +10723,7 @@ var plugin = {
9567
10723
  longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
9568
10724
  maxFileSizeMB: { type: "number" },
9569
10725
  mediaTimeoutMs: { type: "number" },
10726
+ autoSendLocalPathMedia: { type: "boolean" },
9570
10727
  inboundMedia: {
9571
10728
  type: "object",
9572
10729
  additionalProperties: false,
@@ -9596,6 +10753,10 @@ var plugin = {
9596
10753
  }
9597
10754
  },
9598
10755
  markdownSupport: { type: "boolean" },
10756
+ c2cMarkdownDeliveryMode: {
10757
+ type: "string",
10758
+ enum: ["passive", "proactive-table-only", "proactive-all"]
10759
+ },
9599
10760
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
9600
10761
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
9601
10762
  requireMention: { type: "boolean" },
@@ -9607,6 +10768,7 @@ var plugin = {
9607
10768
  longTaskNoticeDelayMs: { type: "integer", minimum: 0 },
9608
10769
  maxFileSizeMB: { type: "number" },
9609
10770
  mediaTimeoutMs: { type: "number" },
10771
+ autoSendLocalPathMedia: { type: "boolean" },
9610
10772
  inboundMedia: {
9611
10773
  type: "object",
9612
10774
  additionalProperties: false,
@@ -9631,6 +10793,6 @@ var plugin = {
9631
10793
  };
9632
10794
  var index_default = plugin;
9633
10795
 
9634
- export { DEFAULT_ACCOUNT_ID, index_default as default, getQQBotRuntime, qqbotPlugin, setQQBotRuntime };
10796
+ export { DEFAULT_ACCOUNT_ID, clearKnownQQBotTargets, index_default as default, getKnownQQBotTarget, getQQBotRuntime, listKnownQQBotTargets, qqbotPlugin, removeKnownQQBotTarget, sendProactiveQQBotMessage, setQQBotRuntime };
9635
10797
  //# sourceMappingURL=index.js.map
9636
10798
  //# sourceMappingURL=index.js.map