@openclaw-china/qqbot 2026.3.16 → 2026.3.19

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
@@ -4228,6 +4228,11 @@ var displayAliasesSchema = external_exports.record(
4228
4228
  ).optional();
4229
4229
  var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
4230
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;
4235
+ var DEFAULT_QQBOT_C2C_MARKDOWN_SAFE_CHUNK_BYTE_LIMIT = 1200;
4231
4236
  var QQBotAccountSchema = external_exports.object({
4232
4237
  name: external_exports.string().optional(),
4233
4238
  enabled: external_exports.boolean().optional(),
@@ -4243,6 +4248,14 @@ var QQBotAccountSchema = external_exports.object({
4243
4248
  markdownSupport: external_exports.boolean().optional().default(true),
4244
4249
  c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4245
4250
  c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
4251
+ c2cMarkdownSafeChunkByteLimit: external_exports.number().int().positive().optional(),
4252
+ typingHeartbeatMode: QQBotTypingHeartbeatModeSchema,
4253
+ typingHeartbeatIntervalMs: external_exports.number().int().positive().optional().default(
4254
+ DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS
4255
+ ),
4256
+ typingInputSeconds: external_exports.number().int().positive().optional().default(
4257
+ DEFAULT_QQBOT_TYPING_INPUT_SECONDS
4258
+ ),
4246
4259
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4247
4260
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4248
4261
  requireMention: external_exports.boolean().optional().default(true),
@@ -4292,6 +4305,19 @@ function resolveInboundMediaKeepDays(config) {
4292
4305
  function resolveQQBotAutoSendLocalPathMedia(config) {
4293
4306
  return config?.autoSendLocalPathMedia ?? true;
4294
4307
  }
4308
+ function resolveQQBotC2CMarkdownSafeChunkByteLimit(config) {
4309
+ const value = config?.c2cMarkdownSafeChunkByteLimit;
4310
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
4311
+ }
4312
+ function resolveQQBotTypingHeartbeatMode(config) {
4313
+ return config?.typingHeartbeatMode ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE;
4314
+ }
4315
+ function resolveQQBotTypingHeartbeatIntervalMs(config) {
4316
+ return config?.typingHeartbeatIntervalMs ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS;
4317
+ }
4318
+ function resolveQQBotTypingInputSeconds(config) {
4319
+ return config?.typingInputSeconds ?? DEFAULT_QQBOT_TYPING_INPUT_SECONDS;
4320
+ }
4295
4321
  function resolveInboundMediaTempDir() {
4296
4322
  return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
4297
4323
  }
@@ -6548,6 +6574,7 @@ var CHANNEL_ORDER = [
6548
6574
  "wecom",
6549
6575
  "wecom-app",
6550
6576
  "wecom-kf",
6577
+ "wechat-mp",
6551
6578
  "feishu-china"
6552
6579
  ];
6553
6580
  var CHANNEL_DISPLAY_LABELS = {
@@ -6556,6 +6583,7 @@ var CHANNEL_DISPLAY_LABELS = {
6556
6583
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
6557
6584
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
6558
6585
  "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
6586
+ "wechat-mp": "WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09",
6559
6587
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
6560
6588
  };
6561
6589
  var CHANNEL_GUIDE_LINKS = {
@@ -6564,6 +6592,7 @@ var CHANNEL_GUIDE_LINKS = {
6564
6592
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
6565
6593
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
6566
6594
  "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
6595
+ "wechat-mp": `${GUIDES_BASE}/wechat-mp/configuration.md`,
6567
6596
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
6568
6597
  };
6569
6598
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -6773,7 +6802,9 @@ function isChannelConfigured(cfg, channelId) {
6773
6802
  case "wecom-app":
6774
6803
  return hasTokenPair(channelCfg);
6775
6804
  case "wecom-kf":
6776
- return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6805
+ return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
6806
+ case "wechat-mp":
6807
+ return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.token);
6777
6808
  default:
6778
6809
  return false;
6779
6810
  }
@@ -7034,6 +7065,15 @@ async function configureWecomKf(prompter, cfg) {
7034
7065
  section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
7035
7066
  showGuideLink("wecom-kf");
7036
7067
  const existing = getChannelConfig(cfg, "wecom-kf");
7068
+ Ve(
7069
+ [
7070
+ "\u5411\u5BFC\u987A\u5E8F\uFF1AwebhookPath / token / encodingAESKey / corpId / open_kfid / corpSecret",
7071
+ "\u57FA\u7840\u5FC5\u586B\uFF1AcorpId / token / encodingAESKey / open_kfid",
7072
+ "corpSecret \u4F1A\u4F5C\u4E3A\u6700\u540E\u4E00\u4E2A\u53C2\u6570\u8BE2\u95EE\uFF1B\u9996\u6B21\u63A5\u5165\u53EF\u5148\u7559\u7A7A\uFF0C\u5F85\u56DE\u8C03 URL \u6821\u9A8C\u901A\u8FC7\u5E76\u70B9\u51FB\u201C\u5F00\u59CB\u4F7F\u7528\u201D\u540E\u518D\u8865",
7073
+ "webhookPath \u9ED8\u8BA4\u503C\uFF1A/wecom-kf"
7074
+ ].join("\n"),
7075
+ "\u53C2\u6570\u8BF4\u660E"
7076
+ );
7037
7077
  const webhookPath = await prompter.askText({
7038
7078
  label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
7039
7079
  defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
@@ -7054,19 +7094,14 @@ async function configureWecomKf(prompter, cfg) {
7054
7094
  defaultValue: toTrimmedString2(existing.corpId),
7055
7095
  required: true
7056
7096
  });
7057
- const corpSecret = await prompter.askSecret({
7058
- label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
7059
- existingValue: toTrimmedString2(existing.corpSecret),
7060
- required: true
7061
- });
7062
7097
  const openKfId = await prompter.askText({
7063
7098
  label: "open_kfid",
7064
7099
  defaultValue: toTrimmedString2(existing.openKfId),
7065
7100
  required: true
7066
7101
  });
7067
- const welcomeText = await prompter.askText({
7068
- label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7069
- defaultValue: toTrimmedString2(existing.welcomeText),
7102
+ const corpSecret = await prompter.askSecret({
7103
+ label: "\u5FAE\u4FE1\u5BA2\u670D Secret\uFF08\u6700\u540E\u586B\u5199\uFF1B\u9996\u6B21\u63A5\u5165\u53EF\u5148\u7559\u7A7A\uFF09",
7104
+ existingValue: toTrimmedString2(existing.corpSecret),
7070
7105
  required: false
7071
7106
  });
7072
7107
  return mergeChannelConfig(cfg, "wecom-kf", {
@@ -7074,8 +7109,72 @@ async function configureWecomKf(prompter, cfg) {
7074
7109
  token,
7075
7110
  encodingAESKey,
7076
7111
  corpId,
7077
- corpSecret,
7078
7112
  openKfId,
7113
+ corpSecret: corpSecret || void 0
7114
+ });
7115
+ }
7116
+ async function configureWechatMp(prompter, cfg) {
7117
+ section("\u914D\u7F6E WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09");
7118
+ showGuideLink("wechat-mp");
7119
+ const existing = getChannelConfig(cfg, "wechat-mp");
7120
+ const webhookPath = await prompter.askText({
7121
+ label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wechat-mp\uFF09",
7122
+ defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wechat-mp",
7123
+ required: true
7124
+ });
7125
+ const appId = await prompter.askText({
7126
+ label: "\u516C\u4F17\u53F7 appId",
7127
+ defaultValue: toTrimmedString2(existing.appId),
7128
+ required: true
7129
+ });
7130
+ const appSecret = await prompter.askSecret({
7131
+ label: "\u516C\u4F17\u53F7 appSecret\uFF08\u4E3B\u52A8\u53D1\u9001\u9700\u8981\uFF09",
7132
+ existingValue: toTrimmedString2(existing.appSecret),
7133
+ required: false
7134
+ });
7135
+ const token = await prompter.askSecret({
7136
+ label: "\u670D\u52A1\u5668\u914D\u7F6E token",
7137
+ existingValue: toTrimmedString2(existing.token),
7138
+ required: true
7139
+ });
7140
+ const messageMode = await prompter.askSelect(
7141
+ "\u6D88\u606F\u52A0\u89E3\u5BC6\u6A21\u5F0F",
7142
+ [
7143
+ { value: "plain", label: "plain\uFF08\u660E\u6587\uFF09" },
7144
+ { value: "safe", label: "safe\uFF08\u5B89\u5168\u6A21\u5F0F\uFF09" },
7145
+ { value: "compat", label: "compat\uFF08\u517C\u5BB9\u6A21\u5F0F\uFF09" }
7146
+ ],
7147
+ toTrimmedString2(existing.messageMode) ?? "safe"
7148
+ );
7149
+ let encodingAESKey = toTrimmedString2(existing.encodingAESKey);
7150
+ if (messageMode !== "plain") {
7151
+ encodingAESKey = await prompter.askSecret({
7152
+ label: "EncodingAESKey\uFF08safe/compat \u5FC5\u586B\uFF09",
7153
+ existingValue: encodingAESKey,
7154
+ required: true
7155
+ });
7156
+ }
7157
+ const replyMode = await prompter.askSelect(
7158
+ "\u56DE\u590D\u6A21\u5F0F",
7159
+ [
7160
+ { value: "passive", label: "passive\uFF085 \u79D2\u5185\u88AB\u52A8\u56DE\u590D\uFF09" },
7161
+ { value: "active", label: "active\uFF08\u5BA2\u670D\u6D88\u606F\u4E3B\u52A8\u53D1\u9001\uFF09" }
7162
+ ],
7163
+ toTrimmedString2(existing.replyMode) ?? "passive"
7164
+ );
7165
+ const welcomeText = await prompter.askText({
7166
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7167
+ defaultValue: toTrimmedString2(existing.welcomeText),
7168
+ required: false
7169
+ });
7170
+ return mergeChannelConfig(cfg, "wechat-mp", {
7171
+ webhookPath,
7172
+ appId,
7173
+ appSecret: appSecret || void 0,
7174
+ token,
7175
+ encodingAESKey: messageMode === "plain" ? void 0 : encodingAESKey,
7176
+ messageMode,
7177
+ replyMode,
7079
7178
  welcomeText: welcomeText || void 0
7080
7179
  });
7081
7180
  }
@@ -7137,6 +7236,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
7137
7236
  return configureWecomApp(prompter, cfg);
7138
7237
  case "wecom-kf":
7139
7238
  return configureWecomKf(prompter, cfg);
7239
+ case "wechat-mp":
7240
+ return configureWechatMp(prompter, cfg);
7140
7241
  case "qqbot":
7141
7242
  return configureQQBot(prompter, cfg);
7142
7243
  default:
@@ -7278,6 +7379,7 @@ var SUPPORTED_CHANNELS = [
7278
7379
  "wecom",
7279
7380
  "wecom-app",
7280
7381
  "wecom-kf",
7382
+ "wechat-mp",
7281
7383
  "qqbot"
7282
7384
  ];
7283
7385
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -9623,6 +9725,37 @@ function startLongTaskNoticeTimer(params) {
9623
9725
  }
9624
9726
  };
9625
9727
  }
9728
+ function startQQBotTypingHeartbeat(params) {
9729
+ const { intervalMs, renew, shouldRenew } = params;
9730
+ let stopped = false;
9731
+ let timer = null;
9732
+ let renewalInFlight = false;
9733
+ const clear = () => {
9734
+ if (!timer) return;
9735
+ clearInterval(timer);
9736
+ timer = null;
9737
+ };
9738
+ const stop = () => {
9739
+ if (stopped) return;
9740
+ stopped = true;
9741
+ clear();
9742
+ };
9743
+ if (intervalMs > 0) {
9744
+ timer = setInterval(() => {
9745
+ if (stopped || renewalInFlight) return;
9746
+ if (shouldRenew && !shouldRenew()) return;
9747
+ renewalInFlight = true;
9748
+ void renew().catch(() => void 0).finally(() => {
9749
+ renewalInFlight = false;
9750
+ });
9751
+ }, intervalMs);
9752
+ timer.unref?.();
9753
+ }
9754
+ return {
9755
+ stop,
9756
+ dispose: stop
9757
+ };
9758
+ }
9626
9759
  function isHttpUrl3(value) {
9627
9760
  return /^https?:\/\//i.test(value);
9628
9761
  }
@@ -10085,7 +10218,7 @@ function extractLocalMediaFromText(params) {
10085
10218
  const MARKDOWN_LINKED_IMAGE_RE2 = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g;
10086
10219
  const MARKDOWN_IMAGE_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
10087
10220
  const MARKDOWN_LINK_RE2 = /\[([^\]]*)\]\(([^)]+)\)/g;
10088
- 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;
10221
+ const BARE_LOCAL_MEDIA_PATH_RE = /`?((?:\/(?:tmp|var|private|Users|home|root)\/[^\s`'",)]+|\/mnt\/[A-Za-z]\/[^\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;
10089
10222
  const collectLocalRichMedia = (rawValue, allowedTypes) => {
10090
10223
  const candidate = stripTitleFromUrl(rawValue.trim());
10091
10224
  if (!candidate || !isLocalReference(candidate)) {
@@ -10194,6 +10327,10 @@ var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[
10194
10327
  var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
10195
10328
  var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
10196
10329
  var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
10330
+ var QQBOT_MARKDOWN_SOFT_LIMIT_THRESHOLD = 128;
10331
+ var QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MIN = 16;
10332
+ var QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MAX = 320;
10333
+ var QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_RATIO = 0.18;
10197
10334
  function extractFinalBlocks(text) {
10198
10335
  const matches = Array.from(text.matchAll(FINAL_BLOCK_RE));
10199
10336
  if (matches.length === 0) return void 0;
@@ -10333,6 +10470,72 @@ function appendQQBotBufferedText(bufferedTexts, nextText) {
10333
10470
  }
10334
10471
  return [...bufferedTexts, normalized];
10335
10472
  }
10473
+ function resolveQQBotLastNonEmptyLine(text) {
10474
+ return text.split("\n").map((line) => line.trimEnd()).reverse().find((line) => line.trim().length > 0);
10475
+ }
10476
+ function resolveQQBotFirstNonEmptyLine(text) {
10477
+ return text.split("\n").map((line) => line.trimStart()).find((line) => line.trim().length > 0);
10478
+ }
10479
+ function resolveQQBotTrailingMarkdownTableColumnCount(text) {
10480
+ const lines = text.split("\n");
10481
+ for (let index = Math.max(0, lines.length - 2); index >= 0; index -= 1) {
10482
+ if (!isQQBotMarkdownTableStart(lines, index)) {
10483
+ continue;
10484
+ }
10485
+ const trailingLines = lines.slice(index + 2).filter((line) => line.trim().length > 0);
10486
+ if (trailingLines.length === 0 || trailingLines.every((line) => line.includes("|"))) {
10487
+ return parseQQBotMarkdownTableRowCells(lines[index] ?? "").length;
10488
+ }
10489
+ }
10490
+ return void 0;
10491
+ }
10492
+ function mergeQQBotBufferedTextSegments(current, next) {
10493
+ const currentTrimmed = current.trimEnd();
10494
+ const nextTrimmed = next.trimStart();
10495
+ if (!currentTrimmed) return nextTrimmed;
10496
+ if (!nextTrimmed) return currentTrimmed;
10497
+ if (currentTrimmed === nextTrimmed || currentTrimmed.includes(nextTrimmed)) {
10498
+ return currentTrimmed;
10499
+ }
10500
+ if (nextTrimmed.includes(currentTrimmed)) {
10501
+ return nextTrimmed;
10502
+ }
10503
+ const lastLine = resolveQQBotLastNonEmptyLine(currentTrimmed) ?? "";
10504
+ const firstLine = resolveQQBotFirstNonEmptyLine(nextTrimmed) ?? "";
10505
+ const trailingTableColumnCount = resolveQQBotTrailingMarkdownTableColumnCount(currentTrimmed);
10506
+ const lastLineLooksLikeTable = lastLine.includes("|");
10507
+ const firstLineLooksLikeTable = firstLine.startsWith("|");
10508
+ const firstLineContainsTableCell = firstLine.includes("|");
10509
+ const expectedPipeCount = typeof trailingTableColumnCount === "number" && trailingTableColumnCount > 0 ? trailingTableColumnCount + 1 : void 0;
10510
+ const lastLinePipeCount = (lastLine.match(/\|/g) ?? []).length;
10511
+ const firstLinePipeCount = (firstLine.match(/\|/g) ?? []).length;
10512
+ const sameRowJoiner = typeof expectedPipeCount === "number" && lastLinePipeCount + firstLinePipeCount < expectedPipeCount ? " | " : " ";
10513
+ const lastLineEndsInsideTableRow = Boolean(
10514
+ trailingTableColumnCount && lastLineLooksLikeTable && (!lastLine.trimEnd().endsWith("|") || typeof expectedPipeCount === "number" && lastLinePipeCount < expectedPipeCount)
10515
+ );
10516
+ if (lastLineEndsInsideTableRow && nextTrimmed.includes("|")) {
10517
+ return `${currentTrimmed}${sameRowJoiner}${nextTrimmed}`;
10518
+ }
10519
+ if (firstLineLooksLikeTable) {
10520
+ if (lastLineLooksLikeTable || hasQQBotMarkdownTable(currentTrimmed)) {
10521
+ return `${currentTrimmed}
10522
+ ${nextTrimmed}`;
10523
+ }
10524
+ }
10525
+ if (trailingTableColumnCount && firstLineContainsTableCell) {
10526
+ return `${currentTrimmed}${sameRowJoiner}${nextTrimmed}`;
10527
+ }
10528
+ return joinQQBotMarkdownPieces([currentTrimmed, nextTrimmed]);
10529
+ }
10530
+ function combineQQBotBufferedText(bufferedTexts) {
10531
+ return bufferedTexts.reduce((combined, segment) => {
10532
+ const normalized = segment.trim();
10533
+ if (!normalized) {
10534
+ return combined;
10535
+ }
10536
+ return mergeQQBotBufferedTextSegments(combined, normalized);
10537
+ }, "");
10538
+ }
10336
10539
  function normalizeQQBotRenderedMarkdown(text) {
10337
10540
  if (!text.trim()) return "";
10338
10541
  let next = text.trim();
@@ -10552,6 +10755,25 @@ function parseQQBotMarkdownBlocks(text) {
10552
10755
  function hasQQBotBoundaryGuard(text) {
10553
10756
  return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
10554
10757
  }
10758
+ function measureQQBotUtf8Length(text) {
10759
+ return Buffer.byteLength(text, "utf8");
10760
+ }
10761
+ function findQQBotIndexWithinUtf8Limit(text, limit) {
10762
+ if (limit <= 0 || !text) {
10763
+ return 0;
10764
+ }
10765
+ let totalBytes = 0;
10766
+ let index = 0;
10767
+ for (const char of text) {
10768
+ const charBytes = measureQQBotUtf8Length(char);
10769
+ if (totalBytes + charBytes > limit) {
10770
+ break;
10771
+ }
10772
+ totalBytes += charBytes;
10773
+ index += char.length;
10774
+ }
10775
+ return index;
10776
+ }
10555
10777
  function isQQBotSafeMarkdownBoundary(text, index) {
10556
10778
  const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
10557
10779
  const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
@@ -10560,13 +10782,14 @@ function isQQBotSafeMarkdownBoundary(text, index) {
10560
10782
  return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
10561
10783
  }
10562
10784
  function findQQBotRegexBoundary(text, limit, pattern) {
10563
- const scopedText = text.slice(0, Math.min(limit + 1, text.length));
10785
+ const scopedIndex = findQQBotIndexWithinUtf8Limit(text, limit);
10786
+ const scopedText = text.slice(0, scopedIndex);
10564
10787
  const regex = new RegExp(pattern.source, pattern.flags);
10565
10788
  let match = regex.exec(scopedText);
10566
10789
  let lastBoundary;
10567
10790
  while (match) {
10568
10791
  const boundary = match.index + match[0].length;
10569
- if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10792
+ if (boundary > 0 && measureQQBotUtf8Length(text.slice(0, boundary)) <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10570
10793
  lastBoundary = boundary;
10571
10794
  }
10572
10795
  match = regex.exec(scopedText);
@@ -10574,13 +10797,14 @@ function findQQBotRegexBoundary(text, limit, pattern) {
10574
10797
  return lastBoundary;
10575
10798
  }
10576
10799
  function findQQBotFallbackBoundary(text, limit) {
10577
- const minIndex = Math.max(1, limit - 120);
10578
- for (let index = limit; index >= minIndex; index -= 1) {
10800
+ const maxIndex = findQQBotIndexWithinUtf8Limit(text, limit);
10801
+ const minIndex = Math.max(1, maxIndex - 120);
10802
+ for (let index = maxIndex; index >= minIndex; index -= 1) {
10579
10803
  if (isQQBotSafeMarkdownBoundary(text, index)) {
10580
10804
  return index;
10581
10805
  }
10582
10806
  }
10583
- return limit;
10807
+ return maxIndex;
10584
10808
  }
10585
10809
  function findQQBotSafeSplitIndex(text, limit) {
10586
10810
  const boundaryPatterns = [
@@ -10599,14 +10823,15 @@ function findQQBotSafeSplitIndex(text, limit) {
10599
10823
  return findQQBotFallbackBoundary(text, limit);
10600
10824
  }
10601
10825
  function splitQQBotHardText(text, limit) {
10602
- if (limit <= 0 || text.length <= limit) {
10826
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10603
10827
  return [text];
10604
10828
  }
10605
10829
  const chunks = [];
10606
10830
  let remaining = text;
10607
- while (remaining.length > limit) {
10608
- chunks.push(remaining.slice(0, limit));
10609
- remaining = remaining.slice(limit);
10831
+ while (measureQQBotUtf8Length(remaining) > limit) {
10832
+ const nextIndex = Math.max(1, findQQBotIndexWithinUtf8Limit(remaining, limit));
10833
+ chunks.push(remaining.slice(0, nextIndex));
10834
+ remaining = remaining.slice(nextIndex);
10610
10835
  }
10611
10836
  if (remaining) {
10612
10837
  chunks.push(remaining);
@@ -10614,14 +10839,14 @@ function splitQQBotHardText(text, limit) {
10614
10839
  return chunks;
10615
10840
  }
10616
10841
  function splitQQBotTextSafely(text, limit, options) {
10617
- if (limit <= 0 || text.length <= limit) {
10842
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10618
10843
  return [text];
10619
10844
  }
10620
10845
  const trimLeading = options?.trimLeading ?? true;
10621
10846
  const trimTrailing = options?.trimTrailing ?? true;
10622
10847
  const chunks = [];
10623
10848
  let remaining = text;
10624
- while (remaining.length > limit) {
10849
+ while (measureQQBotUtf8Length(remaining) > limit) {
10625
10850
  const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
10626
10851
  let nextChunk = remaining.slice(0, splitIndex);
10627
10852
  let nextRemaining = remaining.slice(splitIndex);
@@ -10632,7 +10857,8 @@ function splitQQBotTextSafely(text, limit, options) {
10632
10857
  nextRemaining = nextRemaining.trimStart();
10633
10858
  }
10634
10859
  if (!nextChunk) {
10635
- const hardChunk = remaining.slice(0, limit);
10860
+ const hardChunkIndex = Math.max(1, findQQBotIndexWithinUtf8Limit(remaining, limit));
10861
+ const hardChunk = remaining.slice(0, hardChunkIndex);
10636
10862
  chunks.push(hardChunk);
10637
10863
  remaining = remaining.slice(hardChunk.length);
10638
10864
  continue;
@@ -10647,7 +10873,7 @@ function splitQQBotTextSafely(text, limit, options) {
10647
10873
  return chunks;
10648
10874
  }
10649
10875
  function splitQQBotMarkdownLineBlock(text, limit) {
10650
- if (limit <= 0 || text.length <= limit) {
10876
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10651
10877
  return [text];
10652
10878
  }
10653
10879
  const lines = text.split("\n");
@@ -10666,12 +10892,12 @@ function splitQQBotMarkdownLineBlock(text, limit) {
10666
10892
  for (const line of lines) {
10667
10893
  const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
10668
10894
  ${line}` : line;
10669
- if (candidate.length <= limit) {
10895
+ if (measureQQBotUtf8Length(candidate) <= limit) {
10670
10896
  currentLines.push(line);
10671
10897
  continue;
10672
10898
  }
10673
10899
  flushCurrent();
10674
- if (line.length <= limit) {
10900
+ if (measureQQBotUtf8Length(line) <= limit) {
10675
10901
  currentLines.push(line);
10676
10902
  continue;
10677
10903
  }
@@ -10687,16 +10913,133 @@ ${line}` : line;
10687
10913
  flushCurrent();
10688
10914
  return chunks;
10689
10915
  }
10916
+ function parseQQBotMarkdownTableRowCells(row, columnCount) {
10917
+ const trimmed = row.trim();
10918
+ const inner = trimmed.replace(/^\|\s*/, "").replace(/\s*\|$/, "");
10919
+ const cells = inner.split("|").map((cell) => cell.trim());
10920
+ if (typeof columnCount !== "number" || !Number.isFinite(columnCount) || columnCount <= 0) {
10921
+ return cells;
10922
+ }
10923
+ if (cells.length >= columnCount) {
10924
+ return cells.slice(0, columnCount);
10925
+ }
10926
+ return [...cells, ...Array.from({ length: columnCount - cells.length }, () => "")];
10927
+ }
10928
+ function renderQQBotMarkdownTableRow(cells) {
10929
+ return `| ${cells.join(" | ")} |`;
10930
+ }
10931
+ function splitQQBotMarkdownTableRowByCells(params) {
10932
+ const { row, columnCount, limit } = params;
10933
+ const normalizedCells = parseQQBotMarkdownTableRowCells(row, columnCount);
10934
+ const normalizedRow = renderQQBotMarkdownTableRow(normalizedCells);
10935
+ if (measureQQBotUtf8Length(normalizedRow) <= limit) {
10936
+ return [normalizedRow];
10937
+ }
10938
+ const anchorColumnCount = Math.min(
10939
+ Math.max(1, params.anchorColumnCount ?? 2),
10940
+ columnCount
10941
+ );
10942
+ const anchorCells = normalizedCells.map(
10943
+ (cell, index) => index < anchorColumnCount ? cell : ""
10944
+ );
10945
+ const chunks = [];
10946
+ let currentCells = [...anchorCells];
10947
+ let hasContent = false;
10948
+ const flushCurrent = () => {
10949
+ if (!hasContent) {
10950
+ return;
10951
+ }
10952
+ chunks.push(renderQQBotMarkdownTableRow(currentCells));
10953
+ currentCells = [...anchorCells];
10954
+ hasContent = false;
10955
+ };
10956
+ for (let index = anchorColumnCount; index < columnCount; index += 1) {
10957
+ const cell = normalizedCells[index] ?? "";
10958
+ if (!cell) {
10959
+ continue;
10960
+ }
10961
+ const candidateCells = [...currentCells];
10962
+ candidateCells[index] = cell;
10963
+ if (measureQQBotUtf8Length(renderQQBotMarkdownTableRow(candidateCells)) <= limit) {
10964
+ currentCells[index] = cell;
10965
+ hasContent = true;
10966
+ continue;
10967
+ }
10968
+ if (hasContent) {
10969
+ flushCurrent();
10970
+ const nextCandidateCells = [...currentCells];
10971
+ nextCandidateCells[index] = cell;
10972
+ if (measureQQBotUtf8Length(renderQQBotMarkdownTableRow(nextCandidateCells)) <= limit) {
10973
+ currentCells[index] = cell;
10974
+ hasContent = true;
10975
+ continue;
10976
+ }
10977
+ }
10978
+ const emptyRowBytes = measureQQBotUtf8Length(renderQQBotMarkdownTableRow(currentCells));
10979
+ const availableCellBytes = Math.max(1, limit - emptyRowBytes);
10980
+ for (const cellPiece of splitQQBotTextSafely(cell, availableCellBytes, {
10981
+ trimLeading: false,
10982
+ trimTrailing: false
10983
+ })) {
10984
+ const pieceCells = [...anchorCells];
10985
+ pieceCells[index] = cellPiece;
10986
+ chunks.push(renderQQBotMarkdownTableRow(pieceCells));
10987
+ }
10988
+ currentCells = [...anchorCells];
10989
+ hasContent = false;
10990
+ }
10991
+ flushCurrent();
10992
+ return chunks.length > 0 ? chunks : splitQQBotTextSafely(normalizedRow, limit);
10993
+ }
10994
+ function resolveQQBotMarkdownTableBlockLimit(params) {
10995
+ const { header, separator, rows, limit } = params;
10996
+ if (limit <= 512) {
10997
+ return limit;
10998
+ }
10999
+ const columnCount = parseQQBotMarkdownTableRowCells(header).length;
11000
+ if (columnCount < 8) {
11001
+ return limit;
11002
+ }
11003
+ const tablePrefixBytes = measureQQBotUtf8Length(`${header}
11004
+ ${separator}`);
11005
+ const extraColumnCount = Math.max(0, columnCount - 7);
11006
+ const columnPenalty = Math.min(240, extraColumnCount * 55);
11007
+ const headerPenalty = Math.min(120, Math.floor(tablePrefixBytes * 0.25));
11008
+ const reducedLimit = limit - columnPenalty - headerPenalty;
11009
+ const minTableLimit = Math.max(tablePrefixBytes + 64, Math.floor(limit * 0.45));
11010
+ const maxSingleRowRequirement = Math.min(
11011
+ limit,
11012
+ rows.reduce((max, row) => {
11013
+ const normalizedRow = renderQQBotMarkdownTableRow(
11014
+ parseQQBotMarkdownTableRowCells(row, columnCount)
11015
+ );
11016
+ return Math.max(max, measureQQBotUtf8Length(`${header}
11017
+ ${separator}
11018
+ ${normalizedRow}`));
11019
+ }, tablePrefixBytes)
11020
+ );
11021
+ return Math.min(
11022
+ limit,
11023
+ Math.max(maxSingleRowRequirement, minTableLimit, Math.min(limit, reducedLimit))
11024
+ );
11025
+ }
10690
11026
  function splitQQBotMarkdownTableBlock(text, limit) {
10691
- if (limit <= 0 || text.length <= limit) {
11027
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10692
11028
  return [text];
10693
11029
  }
10694
11030
  const lines = text.split("\n");
10695
11031
  const header = lines[0] ?? "";
10696
11032
  const separator = lines[1] ?? "";
10697
11033
  const rows = lines.slice(2);
11034
+ const packingLimit = resolveQQBotMarkdownTableBlockLimit({
11035
+ header,
11036
+ separator,
11037
+ rows,
11038
+ limit
11039
+ });
10698
11040
  const tablePrefix = `${header}
10699
11041
  ${separator}`;
11042
+ const columnCount = parseQQBotMarkdownTableRowCells(header).length;
10700
11043
  const chunks = [];
10701
11044
  let currentRows = [];
10702
11045
  const flushCurrent = () => {
@@ -10712,20 +11055,21 @@ ${currentRows.join("\n")}`);
10712
11055
  ${currentRows.join("\n")}
10713
11056
  ${row}` : `${tablePrefix}
10714
11057
  ${row}`;
10715
- if (candidate.length <= limit) {
11058
+ if (measureQQBotUtf8Length(candidate) <= packingLimit) {
10716
11059
  currentRows.push(row);
10717
11060
  continue;
10718
11061
  }
10719
11062
  flushCurrent();
10720
- if (`${tablePrefix}
10721
- ${row}`.length <= limit) {
11063
+ if (measureQQBotUtf8Length(`${tablePrefix}
11064
+ ${row}`) <= limit) {
10722
11065
  currentRows.push(row);
10723
11066
  continue;
10724
11067
  }
10725
- const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
10726
- for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
10727
- trimLeading: false,
10728
- trimTrailing: false
11068
+ const maxRowLength = Math.max(16, limit - measureQQBotUtf8Length(tablePrefix) - 1);
11069
+ for (const rowPiece of splitQQBotMarkdownTableRowByCells({
11070
+ row,
11071
+ columnCount,
11072
+ limit: maxRowLength
10729
11073
  })) {
10730
11074
  chunks.push(`${tablePrefix}
10731
11075
  ${rowPiece}`);
@@ -10735,7 +11079,7 @@ ${rowPiece}`);
10735
11079
  return chunks.length > 0 ? chunks : [text];
10736
11080
  }
10737
11081
  function splitQQBotMarkdownCodeFence(text, limit) {
10738
- if (limit <= 0 || text.length <= limit) {
11082
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10739
11083
  return [text];
10740
11084
  }
10741
11085
  const lines = text.split("\n");
@@ -10744,7 +11088,7 @@ function splitQQBotMarkdownCodeFence(text, limit) {
10744
11088
  const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
10745
11089
  const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
10746
11090
  const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
10747
- const fixedOverhead = openingLine.length + closingLine.length + 2;
11091
+ const fixedOverhead = measureQQBotUtf8Length(openingLine) + measureQQBotUtf8Length(closingLine) + 2;
10748
11092
  const availableLineLength = Math.max(1, limit - fixedOverhead);
10749
11093
  const chunks = [];
10750
11094
  let currentCodeLines = [];
@@ -10764,14 +11108,14 @@ ${codeLine}
10764
11108
  ${closingLine}` : `${openingLine}
10765
11109
  ${codeLine}
10766
11110
  ${closingLine}`;
10767
- if (candidate.length <= limit) {
11111
+ if (measureQQBotUtf8Length(candidate) <= limit) {
10768
11112
  currentCodeLines.push(codeLine);
10769
11113
  continue;
10770
11114
  }
10771
11115
  flushCurrent();
10772
- if (`${openingLine}
11116
+ if (measureQQBotUtf8Length(`${openingLine}
10773
11117
  ${codeLine}
10774
- ${closingLine}`.length <= limit) {
11118
+ ${closingLine}`) <= limit) {
10775
11119
  currentCodeLines.push(codeLine);
10776
11120
  continue;
10777
11121
  }
@@ -10785,7 +11129,7 @@ ${closingLine}`);
10785
11129
  return chunks.length > 0 ? chunks : [text];
10786
11130
  }
10787
11131
  function splitQQBotMarkdownBlock(block, limit) {
10788
- if (limit <= 0 || block.text.length <= limit) {
11132
+ if (limit <= 0 || measureQQBotUtf8Length(block.text) <= limit) {
10789
11133
  return [block.text];
10790
11134
  }
10791
11135
  switch (block.kind) {
@@ -10806,11 +11150,93 @@ function splitQQBotMarkdownBlock(block, limit) {
10806
11150
  return [block.text];
10807
11151
  }
10808
11152
  }
10809
- function chunkQQBotStructuredMarkdown(text, limit) {
11153
+ function resolveQQBotStructuredMarkdownSoftLimit(limit, safeChunkByteLimit) {
11154
+ if (limit <= QQBOT_MARKDOWN_SOFT_LIMIT_THRESHOLD) {
11155
+ return limit;
11156
+ }
11157
+ const configuredSafeLimit = typeof safeChunkByteLimit === "number" && Number.isFinite(safeChunkByteLimit) && safeChunkByteLimit > 0 ? Math.floor(safeChunkByteLimit) : void 0;
11158
+ if (configuredSafeLimit) {
11159
+ return Math.min(limit, configuredSafeLimit);
11160
+ }
11161
+ const reservedLength = Math.min(
11162
+ QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MAX,
11163
+ Math.max(
11164
+ QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MIN,
11165
+ Math.floor(limit * QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_RATIO)
11166
+ )
11167
+ );
11168
+ const softLimit = limit - reservedLength;
11169
+ const boundedSoftLimit = Math.min(
11170
+ softLimit > 0 ? softLimit : limit,
11171
+ DEFAULT_QQBOT_C2C_MARKDOWN_SAFE_CHUNK_BYTE_LIMIT
11172
+ );
11173
+ return boundedSoftLimit > 0 ? boundedSoftLimit : limit;
11174
+ }
11175
+ function maybePrefixQQBotContinuationPiece(params) {
11176
+ const prefix = params.prefix?.trim();
11177
+ if (!prefix) {
11178
+ return params.piece;
11179
+ }
11180
+ const prefixedPiece = joinQQBotMarkdownPieces([prefix, params.piece]);
11181
+ return measureQQBotUtf8Length(prefixedPiece) <= params.limit ? prefixedPiece : params.piece;
11182
+ }
11183
+ function resolveQQBotMarkdownLeadPiece(blocks, index, limit) {
11184
+ const block = blocks[index];
11185
+ if (!block) {
11186
+ return void 0;
11187
+ }
11188
+ if (block.kind === "heading") {
11189
+ const nextBlock = blocks[index + 1];
11190
+ if (nextBlock && nextBlock.kind !== "thematic-break") {
11191
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
11192
+ const firstBodyPiece = nextPieces[0];
11193
+ if (firstBodyPiece) {
11194
+ const pairedText = joinQQBotMarkdownPieces([block.text, firstBodyPiece]);
11195
+ if (measureQQBotUtf8Length(pairedText) <= limit) {
11196
+ return pairedText;
11197
+ }
11198
+ }
11199
+ }
11200
+ }
11201
+ return splitQQBotMarkdownBlock(block, limit).map((piece) => piece.trim()).find(Boolean);
11202
+ }
11203
+ function shouldQQBotCarryThematicBreakToNextBlock(params) {
11204
+ const block = params.blocks[params.index];
11205
+ if (!block || block.kind !== "thematic-break") {
11206
+ return false;
11207
+ }
11208
+ if (params.currentPieces.length === 0) {
11209
+ return true;
11210
+ }
11211
+ const withBreak = joinQQBotMarkdownPieces([...params.currentPieces, block.text]);
11212
+ if (measureQQBotUtf8Length(withBreak) > params.limit) {
11213
+ return true;
11214
+ }
11215
+ const nextLeadPiece = resolveQQBotMarkdownLeadPiece(
11216
+ params.blocks,
11217
+ params.index + 1,
11218
+ params.limit
11219
+ );
11220
+ if (!nextLeadPiece) {
11221
+ return false;
11222
+ }
11223
+ const prefixedLeadPiece = joinQQBotMarkdownPieces([block.text, nextLeadPiece]);
11224
+ if (measureQQBotUtf8Length(prefixedLeadPiece) > params.limit) {
11225
+ return false;
11226
+ }
11227
+ const sectionCandidate = joinQQBotMarkdownPieces([
11228
+ ...params.currentPieces,
11229
+ block.text,
11230
+ nextLeadPiece
11231
+ ]);
11232
+ return measureQQBotUtf8Length(sectionCandidate) > params.limit;
11233
+ }
11234
+ function chunkQQBotStructuredMarkdown(text, limit, safeChunkByteLimit) {
10810
11235
  const blocks = parseQQBotMarkdownBlocks(text);
10811
11236
  if (blocks.length === 0 || limit <= 0) {
10812
11237
  return [text.trim()];
10813
11238
  }
11239
+ const chunkLimit = resolveQQBotStructuredMarkdownSoftLimit(limit, safeChunkByteLimit);
10814
11240
  const chunks = [];
10815
11241
  let currentPieces = [];
10816
11242
  let pendingPrefixPieces = [];
@@ -10828,14 +11254,14 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10828
11254
  if (!piece) {
10829
11255
  return;
10830
11256
  }
10831
- const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
11257
+ const pieces = measureQQBotUtf8Length(piece) > chunkLimit ? splitQQBotTextSafely(piece, chunkLimit) : [piece];
10832
11258
  for (const nextPiece of pieces) {
10833
11259
  const normalizedPiece = nextPiece.trim();
10834
11260
  if (!normalizedPiece) {
10835
11261
  continue;
10836
11262
  }
10837
11263
  const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
10838
- if (currentPieces.length === 0 || candidate.length <= limit) {
11264
+ if (currentPieces.length === 0 || measureQQBotUtf8Length(candidate) <= chunkLimit) {
10839
11265
  currentPieces.push(normalizedPiece);
10840
11266
  continue;
10841
11267
  }
@@ -10857,9 +11283,19 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10857
11283
  continue;
10858
11284
  }
10859
11285
  if (block.kind === "thematic-break") {
11286
+ if (shouldQQBotCarryThematicBreakToNextBlock({
11287
+ blocks,
11288
+ index,
11289
+ currentPieces,
11290
+ limit: chunkLimit
11291
+ })) {
11292
+ flushCurrent();
11293
+ pendingPrefixPieces.push(block.text);
11294
+ continue;
11295
+ }
10860
11296
  if (currentPieces.length > 0) {
10861
11297
  const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
10862
- if (candidate.length <= limit) {
11298
+ if (measureQQBotUtf8Length(candidate) <= chunkLimit) {
10863
11299
  currentPieces.push(block.text);
10864
11300
  continue;
10865
11301
  }
@@ -10872,7 +11308,7 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10872
11308
  const headingText = consumePendingPrefix(block.text);
10873
11309
  const nextBlock = blocks[index + 1];
10874
11310
  if (nextBlock && nextBlock.kind !== "thematic-break") {
10875
- const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
11311
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, chunkLimit);
10876
11312
  const firstBodyPiece = nextPieces[0];
10877
11313
  if (firstBodyPiece) {
10878
11314
  const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
@@ -10881,19 +11317,33 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10881
11317
  headingText,
10882
11318
  firstBodyPiece
10883
11319
  ]);
10884
- if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
11320
+ if (measureQQBotUtf8Length(pairedText) <= chunkLimit && (currentPieces.length === 0 || measureQQBotUtf8Length(pairedCandidate) <= chunkLimit)) {
10885
11321
  currentPieces.push(headingText, firstBodyPiece);
10886
11322
  for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10887
- appendPiece(nextPieces[pieceIndex] ?? "");
11323
+ const nextPiece = nextPieces[pieceIndex] ?? "";
11324
+ appendPiece(
11325
+ nextBlock.kind === "table" ? maybePrefixQQBotContinuationPiece({
11326
+ prefix: headingText,
11327
+ piece: nextPiece,
11328
+ limit: chunkLimit
11329
+ }) : nextPiece
11330
+ );
10888
11331
  }
10889
11332
  index += 1;
10890
11333
  continue;
10891
11334
  }
10892
- if (currentPieces.length > 0 && pairedText.length <= limit) {
11335
+ if (currentPieces.length > 0 && measureQQBotUtf8Length(pairedText) <= chunkLimit) {
10893
11336
  flushCurrent();
10894
11337
  currentPieces.push(headingText, firstBodyPiece);
10895
11338
  for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10896
- appendPiece(nextPieces[pieceIndex] ?? "");
11339
+ const nextPiece = nextPieces[pieceIndex] ?? "";
11340
+ appendPiece(
11341
+ nextBlock.kind === "table" ? maybePrefixQQBotContinuationPiece({
11342
+ prefix: headingText,
11343
+ piece: nextPiece,
11344
+ limit: chunkLimit
11345
+ }) : nextPiece
11346
+ );
10897
11347
  }
10898
11348
  index += 1;
10899
11349
  continue;
@@ -10904,16 +11354,29 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10904
11354
  continue;
10905
11355
  }
10906
11356
  const blockText = consumePendingPrefix(block.text);
10907
- for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
11357
+ for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, chunkLimit)) {
10908
11358
  appendPiece(piece);
10909
11359
  }
10910
11360
  }
10911
11361
  if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
10912
11362
  const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
10913
- if (trailingCandidate.length <= limit) {
11363
+ if (measureQQBotUtf8Length(trailingCandidate) <= chunkLimit) {
10914
11364
  currentPieces.push(...pendingPrefixPieces);
11365
+ pendingPrefixPieces = [];
10915
11366
  }
10916
11367
  }
11368
+ if (pendingPrefixPieces.length > 0 && chunks.length > 0) {
11369
+ const trailingPrefix = joinQQBotMarkdownPieces(pendingPrefixPieces);
11370
+ const lastChunk = chunks[chunks.length - 1] ?? "";
11371
+ const trailingCandidate = joinQQBotMarkdownPieces([lastChunk, trailingPrefix]);
11372
+ if (measureQQBotUtf8Length(trailingCandidate) <= chunkLimit) {
11373
+ chunks[chunks.length - 1] = trailingCandidate;
11374
+ pendingPrefixPieces = [];
11375
+ }
11376
+ }
11377
+ if (pendingPrefixPieces.length > 0) {
11378
+ currentPieces.push(joinQQBotMarkdownPieces(pendingPrefixPieces));
11379
+ }
10917
11380
  flushCurrent();
10918
11381
  return chunks.length > 0 ? chunks : [text.trim()];
10919
11382
  }
@@ -10940,7 +11403,7 @@ function chunkC2CMarkdownText(params) {
10940
11403
  if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
10941
11404
  return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10942
11405
  }
10943
- return chunkQQBotStructuredMarkdown(normalized, params.limit);
11406
+ return chunkQQBotStructuredMarkdown(normalized, params.limit, params.safeChunkByteLimit);
10944
11407
  }
10945
11408
  async function sendQQBotMediaWithFallback(params) {
10946
11409
  const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
@@ -11029,6 +11492,9 @@ async function dispatchToAgent(params) {
11029
11492
  };
11030
11493
  const target = resolveChatTarget(inbound);
11031
11494
  const outboundAccountId = route.accountId ?? accountId;
11495
+ const typingHeartbeatMode = resolveQQBotTypingHeartbeatMode(qqCfg);
11496
+ const typingHeartbeatIntervalMs = resolveQQBotTypingHeartbeatIntervalMs(qqCfg);
11497
+ const typingInputSeconds = resolveQQBotTypingInputSeconds(qqCfg);
11032
11498
  let typingRefIdx;
11033
11499
  if (inbound.c2cOpenid && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11034
11500
  const typing = await qqbotOutbound.sendTyping({
@@ -11036,7 +11502,7 @@ async function dispatchToAgent(params) {
11036
11502
  to: `user:${inbound.c2cOpenid}`,
11037
11503
  replyToId: inbound.messageId,
11038
11504
  replyEventId: inbound.eventId,
11039
- inputSecond: 60,
11505
+ inputSecond: typingInputSeconds,
11040
11506
  accountId: outboundAccountId
11041
11507
  });
11042
11508
  if (typing.error) {
@@ -11052,10 +11518,15 @@ async function dispatchToAgent(params) {
11052
11518
  }
11053
11519
  let replyDelivered = false;
11054
11520
  let groupMessageInterfaceBlocked = false;
11521
+ let lastVisibleOutboundAt = Date.now();
11522
+ let typingHeartbeat = null;
11055
11523
  const markReplyDelivered = () => {
11056
11524
  replyDelivered = true;
11057
11525
  longTaskNotice.markReplyDelivered();
11058
11526
  };
11527
+ const markVisibleOutboundStarted = () => {
11528
+ lastVisibleOutboundAt = Date.now();
11529
+ };
11059
11530
  const markGroupMessageInterfaceBlocked = (error) => {
11060
11531
  if (!isQQBotGroupMessageInterfaceBlocked(error)) return;
11061
11532
  if (!groupMessageInterfaceBlocked) {
@@ -11063,11 +11534,40 @@ async function dispatchToAgent(params) {
11063
11534
  }
11064
11535
  groupMessageInterfaceBlocked = true;
11065
11536
  };
11537
+ if (inbound.c2cOpenid && typingHeartbeatMode !== "none" && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11538
+ typingHeartbeat = startQQBotTypingHeartbeat({
11539
+ intervalMs: typingHeartbeatIntervalMs,
11540
+ shouldRenew: () => {
11541
+ if (shouldSuppressVisibleReplies()) {
11542
+ return false;
11543
+ }
11544
+ if (typingHeartbeatMode === "always") {
11545
+ return true;
11546
+ }
11547
+ return Date.now() - lastVisibleOutboundAt >= typingHeartbeatIntervalMs;
11548
+ },
11549
+ renew: async () => {
11550
+ try {
11551
+ const typing = await qqbotOutbound.sendTyping({
11552
+ cfg: { channels: { qqbot: qqCfg } },
11553
+ to: `user:${inbound.c2cOpenid}`,
11554
+ replyToId: inbound.messageId,
11555
+ replyEventId: inbound.eventId,
11556
+ inputSecond: typingInputSeconds,
11557
+ accountId: outboundAccountId
11558
+ });
11559
+ void typing;
11560
+ } catch {
11561
+ }
11562
+ }
11563
+ });
11564
+ }
11066
11565
  const longTaskNotice = startLongTaskNoticeTimer({
11067
11566
  delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
11068
11567
  logger,
11069
11568
  sendNotice: async () => {
11070
11569
  if (groupMessageInterfaceBlocked || isFastAbortCommand || shouldSuppressVisibleReplies()) return;
11570
+ markVisibleOutboundStarted();
11071
11571
  const result = await qqbotOutbound.sendText({
11072
11572
  cfg: { channels: { qqbot: qqCfg } },
11073
11573
  to: target.to,
@@ -11080,7 +11580,7 @@ async function dispatchToAgent(params) {
11080
11580
  logger.warn(`send long-task notice failed: ${result.error}`);
11081
11581
  markGroupMessageInterfaceBlocked(result.error);
11082
11582
  } else {
11083
- replyDelivered = true;
11583
+ markReplyDelivered();
11084
11584
  }
11085
11585
  }
11086
11586
  });
@@ -11104,6 +11604,7 @@ async function dispatchToAgent(params) {
11104
11604
  if (shouldSuppressVisibleReplies()) {
11105
11605
  return;
11106
11606
  }
11607
+ markVisibleOutboundStarted();
11107
11608
  const fallback = await qqbotOutbound.sendText({
11108
11609
  cfg: { channels: { qqbot: qqCfg } },
11109
11610
  to: target.to,
@@ -11116,7 +11617,7 @@ async function dispatchToAgent(params) {
11116
11617
  logger.error(`sendText ASR fallback failed: ${fallback.error}`);
11117
11618
  markGroupMessageInterfaceBlocked(fallback.error);
11118
11619
  } else {
11119
- replyDelivered = true;
11620
+ markReplyDelivered();
11120
11621
  }
11121
11622
  return;
11122
11623
  }
@@ -11278,6 +11779,7 @@ async function dispatchToAgent(params) {
11278
11779
  const markdownSupport = qqCfg.markdownSupport ?? true;
11279
11780
  const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
11280
11781
  const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11782
+ const c2cMarkdownSafeChunkByteLimit = resolveQQBotC2CMarkdownSafeChunkByteLimit(qqCfg);
11281
11783
  const isC2CTarget = isQQBotC2CTarget(target.to);
11282
11784
  const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
11283
11785
  let bufferedC2CMarkdownTexts = [];
@@ -11315,13 +11817,17 @@ async function dispatchToAgent(params) {
11315
11817
  text: finalMarkdownText,
11316
11818
  limit,
11317
11819
  strategy: c2cMarkdownChunkStrategy,
11820
+ safeChunkByteLimit: c2cMarkdownSafeChunkByteLimit,
11318
11821
  fallbackChunkText: chunkText
11319
11822
  }) : [];
11320
11823
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
11321
11824
  logger.info(
11322
- `delivery=${deliveryLabel} to=${target.to} chunks=${textChunks.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")} chunkStrategy=${c2cMarkdownChunkStrategy}`
11825
+ `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} safeChunkByteLimit=${String(c2cMarkdownSafeChunkByteLimit ?? "auto")}`
11323
11826
  );
11324
11827
  if (!shouldSuppressVisibleReplies()) {
11828
+ if (mediaQueue.length > 0) {
11829
+ markVisibleOutboundStarted();
11830
+ }
11325
11831
  await sendQQBotMediaWithFallback({
11326
11832
  qqCfg,
11327
11833
  to: target.to,
@@ -11350,6 +11856,7 @@ async function dispatchToAgent(params) {
11350
11856
  logger.info(
11351
11857
  `delivery=${deliveryLabel} segment=1/1 chunk=${chunkIndex + 1}/${textChunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
11352
11858
  );
11859
+ markVisibleOutboundStarted();
11353
11860
  const result = await qqbotOutbound.sendText({
11354
11861
  cfg: { channels: { qqbot: qqCfg } },
11355
11862
  to: target.to,
@@ -11380,7 +11887,7 @@ async function dispatchToAgent(params) {
11380
11887
  bufferedC2CMarkdownMediaSeen.clear();
11381
11888
  return;
11382
11889
  }
11383
- const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
11890
+ const combinedText = combineQQBotBufferedText(bufferedC2CMarkdownTexts);
11384
11891
  const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
11385
11892
  bufferedC2CMarkdownTexts = [];
11386
11893
  bufferedC2CMarkdownMediaUrls = [];
@@ -11426,7 +11933,7 @@ async function dispatchToAgent(params) {
11426
11933
  const textToSend = suppressText ? "" : cleanedText;
11427
11934
  if (useC2CMarkdownTransport) {
11428
11935
  const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
11429
- const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
11936
+ const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && (hasBufferedC2CMarkdownReply() || looksLikeStructuredMarkdown(textToSend));
11430
11937
  if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
11431
11938
  if (textToSend) {
11432
11939
  bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
@@ -11464,6 +11971,7 @@ async function dispatchToAgent(params) {
11464
11971
  if (shouldSuppressVisibleReplies()) {
11465
11972
  return;
11466
11973
  }
11974
+ markVisibleOutboundStarted();
11467
11975
  const result = await qqbotOutbound.sendText({
11468
11976
  cfg: { channels: { qqbot: qqCfg } },
11469
11977
  to: target.to,
@@ -11483,6 +11991,9 @@ async function dispatchToAgent(params) {
11483
11991
  if (shouldSuppressVisibleReplies()) {
11484
11992
  return;
11485
11993
  }
11994
+ if (mediaQueue.length > 0) {
11995
+ markVisibleOutboundStarted();
11996
+ }
11486
11997
  await sendQQBotMediaWithFallback({
11487
11998
  qqCfg,
11488
11999
  to: target.to,
@@ -11587,6 +12098,7 @@ async function dispatchToAgent(params) {
11587
12098
  });
11588
12099
  if (noReplyFallback && !groupMessageInterfaceBlocked && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11589
12100
  logger.info("no visible reply generated for group mention; sending fallback text");
12101
+ markVisibleOutboundStarted();
11590
12102
  const fallbackResult = await qqbotOutbound.sendText({
11591
12103
  cfg: { channels: { qqbot: qqCfg } },
11592
12104
  to: target.to,
@@ -11603,6 +12115,7 @@ async function dispatchToAgent(params) {
11603
12115
  }
11604
12116
  }
11605
12117
  } finally {
12118
+ typingHeartbeat?.dispose();
11606
12119
  longTaskNotice.dispose();
11607
12120
  try {
11608
12121
  await pruneInboundMediaDir({
@@ -12205,6 +12718,7 @@ var qqbotPlugin = {
12205
12718
  type: "string",
12206
12719
  enum: ["markdown-block", "length"]
12207
12720
  },
12721
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12208
12722
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12209
12723
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12210
12724
  requireMention: { type: "boolean" },
@@ -12258,6 +12772,7 @@ var qqbotPlugin = {
12258
12772
  type: "string",
12259
12773
  enum: ["markdown-block", "length"]
12260
12774
  },
12775
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12261
12776
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12262
12777
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12263
12778
  requireMention: { type: "boolean" },
@@ -12470,6 +12985,7 @@ var plugin = {
12470
12985
  type: "string",
12471
12986
  enum: ["markdown-block", "length"]
12472
12987
  },
12988
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12473
12989
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12474
12990
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12475
12991
  requireMention: { type: "boolean" },
@@ -12523,6 +13039,7 @@ var plugin = {
12523
13039
  type: "string",
12524
13040
  enum: ["markdown-block", "length"]
12525
13041
  },
13042
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12526
13043
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12527
13044
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12528
13045
  requireMention: { type: "boolean" },