@openclaw-china/qqbot 2026.3.18 → 2026.3.20

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
@@ -4232,6 +4232,7 @@ var QQBotTypingHeartbeatModeSchema = external_exports.enum(["none", "idle", "alw
4232
4232
  var DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE = "idle";
4233
4233
  var DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS = 5e3;
4234
4234
  var DEFAULT_QQBOT_TYPING_INPUT_SECONDS = 60;
4235
+ var DEFAULT_QQBOT_C2C_MARKDOWN_SAFE_CHUNK_BYTE_LIMIT = 1200;
4235
4236
  var QQBotAccountSchema = external_exports.object({
4236
4237
  name: external_exports.string().optional(),
4237
4238
  enabled: external_exports.boolean().optional(),
@@ -4247,6 +4248,7 @@ var QQBotAccountSchema = external_exports.object({
4247
4248
  markdownSupport: external_exports.boolean().optional().default(true),
4248
4249
  c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4249
4250
  c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
4251
+ c2cMarkdownSafeChunkByteLimit: external_exports.number().int().positive().optional(),
4250
4252
  typingHeartbeatMode: QQBotTypingHeartbeatModeSchema,
4251
4253
  typingHeartbeatIntervalMs: external_exports.number().int().positive().optional().default(
4252
4254
  DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS
@@ -4303,6 +4305,10 @@ function resolveInboundMediaKeepDays(config) {
4303
4305
  function resolveQQBotAutoSendLocalPathMedia(config) {
4304
4306
  return config?.autoSendLocalPathMedia ?? true;
4305
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
+ }
4306
4312
  function resolveQQBotTypingHeartbeatMode(config) {
4307
4313
  return config?.typingHeartbeatMode ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE;
4308
4314
  }
@@ -6568,6 +6574,7 @@ var CHANNEL_ORDER = [
6568
6574
  "wecom",
6569
6575
  "wecom-app",
6570
6576
  "wecom-kf",
6577
+ "wechat-mp",
6571
6578
  "feishu-china"
6572
6579
  ];
6573
6580
  var CHANNEL_DISPLAY_LABELS = {
@@ -6576,6 +6583,7 @@ var CHANNEL_DISPLAY_LABELS = {
6576
6583
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
6577
6584
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
6578
6585
  "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
6586
+ "wechat-mp": "WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09",
6579
6587
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
6580
6588
  };
6581
6589
  var CHANNEL_GUIDE_LINKS = {
@@ -6584,6 +6592,7 @@ var CHANNEL_GUIDE_LINKS = {
6584
6592
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
6585
6593
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
6586
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`,
6587
6596
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
6588
6597
  };
6589
6598
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -6793,7 +6802,9 @@ function isChannelConfigured(cfg, channelId) {
6793
6802
  case "wecom-app":
6794
6803
  return hasTokenPair(channelCfg);
6795
6804
  case "wecom-kf":
6796
- 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);
6797
6808
  default:
6798
6809
  return false;
6799
6810
  }
@@ -7054,6 +7065,15 @@ async function configureWecomKf(prompter, cfg) {
7054
7065
  section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
7055
7066
  showGuideLink("wecom-kf");
7056
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
+ );
7057
7077
  const webhookPath = await prompter.askText({
7058
7078
  label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
7059
7079
  defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
@@ -7074,19 +7094,14 @@ async function configureWecomKf(prompter, cfg) {
7074
7094
  defaultValue: toTrimmedString2(existing.corpId),
7075
7095
  required: true
7076
7096
  });
7077
- const corpSecret = await prompter.askSecret({
7078
- label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
7079
- existingValue: toTrimmedString2(existing.corpSecret),
7080
- required: true
7081
- });
7082
7097
  const openKfId = await prompter.askText({
7083
7098
  label: "open_kfid",
7084
7099
  defaultValue: toTrimmedString2(existing.openKfId),
7085
7100
  required: true
7086
7101
  });
7087
- const welcomeText = await prompter.askText({
7088
- label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7089
- 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),
7090
7105
  required: false
7091
7106
  });
7092
7107
  return mergeChannelConfig(cfg, "wecom-kf", {
@@ -7094,8 +7109,89 @@ async function configureWecomKf(prompter, cfg) {
7094
7109
  token,
7095
7110
  encodingAESKey,
7096
7111
  corpId,
7097
- corpSecret,
7098
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
+ let activeDeliveryMode;
7166
+ if (replyMode === "active") {
7167
+ activeDeliveryMode = await prompter.askSelect(
7168
+ "\u4E3B\u52A8\u53D1\u9001\u6A21\u5F0F\uFF08activeDeliveryMode\uFF09",
7169
+ [
7170
+ { value: "split", label: "split\uFF08\u9010\u5757\u53D1\u9001\uFF0C\u63A8\u8350\uFF09" },
7171
+ { value: "merged", label: "merged\uFF08\u5408\u5E76\u540E\u5355\u6B21\u53D1\u9001\uFF09" }
7172
+ ],
7173
+ toTrimmedString2(existing.activeDeliveryMode) ?? "split"
7174
+ );
7175
+ }
7176
+ const renderMarkdown = await prompter.askConfirm(
7177
+ "\u542F\u7528 Markdown \u6E32\u67D3\uFF08\u63A8\u8350\u5F00\u542F\uFF09",
7178
+ toBoolean(existing.renderMarkdown, true)
7179
+ );
7180
+ const welcomeText = await prompter.askText({
7181
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
7182
+ defaultValue: toTrimmedString2(existing.welcomeText),
7183
+ required: false
7184
+ });
7185
+ return mergeChannelConfig(cfg, "wechat-mp", {
7186
+ webhookPath,
7187
+ appId,
7188
+ appSecret: appSecret || void 0,
7189
+ token,
7190
+ encodingAESKey: messageMode === "plain" ? void 0 : encodingAESKey,
7191
+ messageMode,
7192
+ replyMode,
7193
+ activeDeliveryMode,
7194
+ renderMarkdown,
7099
7195
  welcomeText: welcomeText || void 0
7100
7196
  });
7101
7197
  }
@@ -7157,6 +7253,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
7157
7253
  return configureWecomApp(prompter, cfg);
7158
7254
  case "wecom-kf":
7159
7255
  return configureWecomKf(prompter, cfg);
7256
+ case "wechat-mp":
7257
+ return configureWechatMp(prompter, cfg);
7160
7258
  case "qqbot":
7161
7259
  return configureQQBot(prompter, cfg);
7162
7260
  default:
@@ -7298,6 +7396,7 @@ var SUPPORTED_CHANNELS = [
7298
7396
  "wecom",
7299
7397
  "wecom-app",
7300
7398
  "wecom-kf",
7399
+ "wechat-mp",
7301
7400
  "qqbot"
7302
7401
  ];
7303
7402
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -10136,7 +10235,7 @@ function extractLocalMediaFromText(params) {
10136
10235
  const MARKDOWN_LINKED_IMAGE_RE2 = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g;
10137
10236
  const MARKDOWN_IMAGE_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
10138
10237
  const MARKDOWN_LINK_RE2 = /\[([^\]]*)\]\(([^)]+)\)/g;
10139
- 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;
10238
+ 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;
10140
10239
  const collectLocalRichMedia = (rawValue, allowedTypes) => {
10141
10240
  const candidate = stripTitleFromUrl(rawValue.trim());
10142
10241
  if (!candidate || !isLocalReference(candidate)) {
@@ -10245,6 +10344,10 @@ var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[
10245
10344
  var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
10246
10345
  var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
10247
10346
  var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
10347
+ var QQBOT_MARKDOWN_SOFT_LIMIT_THRESHOLD = 128;
10348
+ var QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MIN = 16;
10349
+ var QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MAX = 320;
10350
+ var QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_RATIO = 0.18;
10248
10351
  function extractFinalBlocks(text) {
10249
10352
  const matches = Array.from(text.matchAll(FINAL_BLOCK_RE));
10250
10353
  if (matches.length === 0) return void 0;
@@ -10384,6 +10487,72 @@ function appendQQBotBufferedText(bufferedTexts, nextText) {
10384
10487
  }
10385
10488
  return [...bufferedTexts, normalized];
10386
10489
  }
10490
+ function resolveQQBotLastNonEmptyLine(text) {
10491
+ return text.split("\n").map((line) => line.trimEnd()).reverse().find((line) => line.trim().length > 0);
10492
+ }
10493
+ function resolveQQBotFirstNonEmptyLine(text) {
10494
+ return text.split("\n").map((line) => line.trimStart()).find((line) => line.trim().length > 0);
10495
+ }
10496
+ function resolveQQBotTrailingMarkdownTableColumnCount(text) {
10497
+ const lines = text.split("\n");
10498
+ for (let index = Math.max(0, lines.length - 2); index >= 0; index -= 1) {
10499
+ if (!isQQBotMarkdownTableStart(lines, index)) {
10500
+ continue;
10501
+ }
10502
+ const trailingLines = lines.slice(index + 2).filter((line) => line.trim().length > 0);
10503
+ if (trailingLines.length === 0 || trailingLines.every((line) => line.includes("|"))) {
10504
+ return parseQQBotMarkdownTableRowCells(lines[index] ?? "").length;
10505
+ }
10506
+ }
10507
+ return void 0;
10508
+ }
10509
+ function mergeQQBotBufferedTextSegments(current, next) {
10510
+ const currentTrimmed = current.trimEnd();
10511
+ const nextTrimmed = next.trimStart();
10512
+ if (!currentTrimmed) return nextTrimmed;
10513
+ if (!nextTrimmed) return currentTrimmed;
10514
+ if (currentTrimmed === nextTrimmed || currentTrimmed.includes(nextTrimmed)) {
10515
+ return currentTrimmed;
10516
+ }
10517
+ if (nextTrimmed.includes(currentTrimmed)) {
10518
+ return nextTrimmed;
10519
+ }
10520
+ const lastLine = resolveQQBotLastNonEmptyLine(currentTrimmed) ?? "";
10521
+ const firstLine = resolveQQBotFirstNonEmptyLine(nextTrimmed) ?? "";
10522
+ const trailingTableColumnCount = resolveQQBotTrailingMarkdownTableColumnCount(currentTrimmed);
10523
+ const lastLineLooksLikeTable = lastLine.includes("|");
10524
+ const firstLineLooksLikeTable = firstLine.startsWith("|");
10525
+ const firstLineContainsTableCell = firstLine.includes("|");
10526
+ const expectedPipeCount = typeof trailingTableColumnCount === "number" && trailingTableColumnCount > 0 ? trailingTableColumnCount + 1 : void 0;
10527
+ const lastLinePipeCount = (lastLine.match(/\|/g) ?? []).length;
10528
+ const firstLinePipeCount = (firstLine.match(/\|/g) ?? []).length;
10529
+ const sameRowJoiner = typeof expectedPipeCount === "number" && lastLinePipeCount + firstLinePipeCount < expectedPipeCount ? " | " : " ";
10530
+ const lastLineEndsInsideTableRow = Boolean(
10531
+ trailingTableColumnCount && lastLineLooksLikeTable && (!lastLine.trimEnd().endsWith("|") || typeof expectedPipeCount === "number" && lastLinePipeCount < expectedPipeCount)
10532
+ );
10533
+ if (lastLineEndsInsideTableRow && nextTrimmed.includes("|")) {
10534
+ return `${currentTrimmed}${sameRowJoiner}${nextTrimmed}`;
10535
+ }
10536
+ if (firstLineLooksLikeTable) {
10537
+ if (lastLineLooksLikeTable || hasQQBotMarkdownTable(currentTrimmed)) {
10538
+ return `${currentTrimmed}
10539
+ ${nextTrimmed}`;
10540
+ }
10541
+ }
10542
+ if (trailingTableColumnCount && firstLineContainsTableCell) {
10543
+ return `${currentTrimmed}${sameRowJoiner}${nextTrimmed}`;
10544
+ }
10545
+ return joinQQBotMarkdownPieces([currentTrimmed, nextTrimmed]);
10546
+ }
10547
+ function combineQQBotBufferedText(bufferedTexts) {
10548
+ return bufferedTexts.reduce((combined, segment) => {
10549
+ const normalized = segment.trim();
10550
+ if (!normalized) {
10551
+ return combined;
10552
+ }
10553
+ return mergeQQBotBufferedTextSegments(combined, normalized);
10554
+ }, "");
10555
+ }
10387
10556
  function normalizeQQBotRenderedMarkdown(text) {
10388
10557
  if (!text.trim()) return "";
10389
10558
  let next = text.trim();
@@ -10603,6 +10772,25 @@ function parseQQBotMarkdownBlocks(text) {
10603
10772
  function hasQQBotBoundaryGuard(text) {
10604
10773
  return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
10605
10774
  }
10775
+ function measureQQBotUtf8Length(text) {
10776
+ return Buffer.byteLength(text, "utf8");
10777
+ }
10778
+ function findQQBotIndexWithinUtf8Limit(text, limit) {
10779
+ if (limit <= 0 || !text) {
10780
+ return 0;
10781
+ }
10782
+ let totalBytes = 0;
10783
+ let index = 0;
10784
+ for (const char of text) {
10785
+ const charBytes = measureQQBotUtf8Length(char);
10786
+ if (totalBytes + charBytes > limit) {
10787
+ break;
10788
+ }
10789
+ totalBytes += charBytes;
10790
+ index += char.length;
10791
+ }
10792
+ return index;
10793
+ }
10606
10794
  function isQQBotSafeMarkdownBoundary(text, index) {
10607
10795
  const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
10608
10796
  const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
@@ -10611,13 +10799,14 @@ function isQQBotSafeMarkdownBoundary(text, index) {
10611
10799
  return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
10612
10800
  }
10613
10801
  function findQQBotRegexBoundary(text, limit, pattern) {
10614
- const scopedText = text.slice(0, Math.min(limit + 1, text.length));
10802
+ const scopedIndex = findQQBotIndexWithinUtf8Limit(text, limit);
10803
+ const scopedText = text.slice(0, scopedIndex);
10615
10804
  const regex = new RegExp(pattern.source, pattern.flags);
10616
10805
  let match = regex.exec(scopedText);
10617
10806
  let lastBoundary;
10618
10807
  while (match) {
10619
10808
  const boundary = match.index + match[0].length;
10620
- if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10809
+ if (boundary > 0 && measureQQBotUtf8Length(text.slice(0, boundary)) <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10621
10810
  lastBoundary = boundary;
10622
10811
  }
10623
10812
  match = regex.exec(scopedText);
@@ -10625,13 +10814,14 @@ function findQQBotRegexBoundary(text, limit, pattern) {
10625
10814
  return lastBoundary;
10626
10815
  }
10627
10816
  function findQQBotFallbackBoundary(text, limit) {
10628
- const minIndex = Math.max(1, limit - 120);
10629
- for (let index = limit; index >= minIndex; index -= 1) {
10817
+ const maxIndex = findQQBotIndexWithinUtf8Limit(text, limit);
10818
+ const minIndex = Math.max(1, maxIndex - 120);
10819
+ for (let index = maxIndex; index >= minIndex; index -= 1) {
10630
10820
  if (isQQBotSafeMarkdownBoundary(text, index)) {
10631
10821
  return index;
10632
10822
  }
10633
10823
  }
10634
- return limit;
10824
+ return maxIndex;
10635
10825
  }
10636
10826
  function findQQBotSafeSplitIndex(text, limit) {
10637
10827
  const boundaryPatterns = [
@@ -10650,14 +10840,15 @@ function findQQBotSafeSplitIndex(text, limit) {
10650
10840
  return findQQBotFallbackBoundary(text, limit);
10651
10841
  }
10652
10842
  function splitQQBotHardText(text, limit) {
10653
- if (limit <= 0 || text.length <= limit) {
10843
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10654
10844
  return [text];
10655
10845
  }
10656
10846
  const chunks = [];
10657
10847
  let remaining = text;
10658
- while (remaining.length > limit) {
10659
- chunks.push(remaining.slice(0, limit));
10660
- remaining = remaining.slice(limit);
10848
+ while (measureQQBotUtf8Length(remaining) > limit) {
10849
+ const nextIndex = Math.max(1, findQQBotIndexWithinUtf8Limit(remaining, limit));
10850
+ chunks.push(remaining.slice(0, nextIndex));
10851
+ remaining = remaining.slice(nextIndex);
10661
10852
  }
10662
10853
  if (remaining) {
10663
10854
  chunks.push(remaining);
@@ -10665,14 +10856,14 @@ function splitQQBotHardText(text, limit) {
10665
10856
  return chunks;
10666
10857
  }
10667
10858
  function splitQQBotTextSafely(text, limit, options) {
10668
- if (limit <= 0 || text.length <= limit) {
10859
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10669
10860
  return [text];
10670
10861
  }
10671
10862
  const trimLeading = options?.trimLeading ?? true;
10672
10863
  const trimTrailing = options?.trimTrailing ?? true;
10673
10864
  const chunks = [];
10674
10865
  let remaining = text;
10675
- while (remaining.length > limit) {
10866
+ while (measureQQBotUtf8Length(remaining) > limit) {
10676
10867
  const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
10677
10868
  let nextChunk = remaining.slice(0, splitIndex);
10678
10869
  let nextRemaining = remaining.slice(splitIndex);
@@ -10683,7 +10874,8 @@ function splitQQBotTextSafely(text, limit, options) {
10683
10874
  nextRemaining = nextRemaining.trimStart();
10684
10875
  }
10685
10876
  if (!nextChunk) {
10686
- const hardChunk = remaining.slice(0, limit);
10877
+ const hardChunkIndex = Math.max(1, findQQBotIndexWithinUtf8Limit(remaining, limit));
10878
+ const hardChunk = remaining.slice(0, hardChunkIndex);
10687
10879
  chunks.push(hardChunk);
10688
10880
  remaining = remaining.slice(hardChunk.length);
10689
10881
  continue;
@@ -10698,7 +10890,7 @@ function splitQQBotTextSafely(text, limit, options) {
10698
10890
  return chunks;
10699
10891
  }
10700
10892
  function splitQQBotMarkdownLineBlock(text, limit) {
10701
- if (limit <= 0 || text.length <= limit) {
10893
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10702
10894
  return [text];
10703
10895
  }
10704
10896
  const lines = text.split("\n");
@@ -10717,12 +10909,12 @@ function splitQQBotMarkdownLineBlock(text, limit) {
10717
10909
  for (const line of lines) {
10718
10910
  const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
10719
10911
  ${line}` : line;
10720
- if (candidate.length <= limit) {
10912
+ if (measureQQBotUtf8Length(candidate) <= limit) {
10721
10913
  currentLines.push(line);
10722
10914
  continue;
10723
10915
  }
10724
10916
  flushCurrent();
10725
- if (line.length <= limit) {
10917
+ if (measureQQBotUtf8Length(line) <= limit) {
10726
10918
  currentLines.push(line);
10727
10919
  continue;
10728
10920
  }
@@ -10738,16 +10930,133 @@ ${line}` : line;
10738
10930
  flushCurrent();
10739
10931
  return chunks;
10740
10932
  }
10933
+ function parseQQBotMarkdownTableRowCells(row, columnCount) {
10934
+ const trimmed = row.trim();
10935
+ const inner = trimmed.replace(/^\|\s*/, "").replace(/\s*\|$/, "");
10936
+ const cells = inner.split("|").map((cell) => cell.trim());
10937
+ if (typeof columnCount !== "number" || !Number.isFinite(columnCount) || columnCount <= 0) {
10938
+ return cells;
10939
+ }
10940
+ if (cells.length >= columnCount) {
10941
+ return cells.slice(0, columnCount);
10942
+ }
10943
+ return [...cells, ...Array.from({ length: columnCount - cells.length }, () => "")];
10944
+ }
10945
+ function renderQQBotMarkdownTableRow(cells) {
10946
+ return `| ${cells.join(" | ")} |`;
10947
+ }
10948
+ function splitQQBotMarkdownTableRowByCells(params) {
10949
+ const { row, columnCount, limit } = params;
10950
+ const normalizedCells = parseQQBotMarkdownTableRowCells(row, columnCount);
10951
+ const normalizedRow = renderQQBotMarkdownTableRow(normalizedCells);
10952
+ if (measureQQBotUtf8Length(normalizedRow) <= limit) {
10953
+ return [normalizedRow];
10954
+ }
10955
+ const anchorColumnCount = Math.min(
10956
+ Math.max(1, params.anchorColumnCount ?? 2),
10957
+ columnCount
10958
+ );
10959
+ const anchorCells = normalizedCells.map(
10960
+ (cell, index) => index < anchorColumnCount ? cell : ""
10961
+ );
10962
+ const chunks = [];
10963
+ let currentCells = [...anchorCells];
10964
+ let hasContent = false;
10965
+ const flushCurrent = () => {
10966
+ if (!hasContent) {
10967
+ return;
10968
+ }
10969
+ chunks.push(renderQQBotMarkdownTableRow(currentCells));
10970
+ currentCells = [...anchorCells];
10971
+ hasContent = false;
10972
+ };
10973
+ for (let index = anchorColumnCount; index < columnCount; index += 1) {
10974
+ const cell = normalizedCells[index] ?? "";
10975
+ if (!cell) {
10976
+ continue;
10977
+ }
10978
+ const candidateCells = [...currentCells];
10979
+ candidateCells[index] = cell;
10980
+ if (measureQQBotUtf8Length(renderQQBotMarkdownTableRow(candidateCells)) <= limit) {
10981
+ currentCells[index] = cell;
10982
+ hasContent = true;
10983
+ continue;
10984
+ }
10985
+ if (hasContent) {
10986
+ flushCurrent();
10987
+ const nextCandidateCells = [...currentCells];
10988
+ nextCandidateCells[index] = cell;
10989
+ if (measureQQBotUtf8Length(renderQQBotMarkdownTableRow(nextCandidateCells)) <= limit) {
10990
+ currentCells[index] = cell;
10991
+ hasContent = true;
10992
+ continue;
10993
+ }
10994
+ }
10995
+ const emptyRowBytes = measureQQBotUtf8Length(renderQQBotMarkdownTableRow(currentCells));
10996
+ const availableCellBytes = Math.max(1, limit - emptyRowBytes);
10997
+ for (const cellPiece of splitQQBotTextSafely(cell, availableCellBytes, {
10998
+ trimLeading: false,
10999
+ trimTrailing: false
11000
+ })) {
11001
+ const pieceCells = [...anchorCells];
11002
+ pieceCells[index] = cellPiece;
11003
+ chunks.push(renderQQBotMarkdownTableRow(pieceCells));
11004
+ }
11005
+ currentCells = [...anchorCells];
11006
+ hasContent = false;
11007
+ }
11008
+ flushCurrent();
11009
+ return chunks.length > 0 ? chunks : splitQQBotTextSafely(normalizedRow, limit);
11010
+ }
11011
+ function resolveQQBotMarkdownTableBlockLimit(params) {
11012
+ const { header, separator, rows, limit } = params;
11013
+ if (limit <= 512) {
11014
+ return limit;
11015
+ }
11016
+ const columnCount = parseQQBotMarkdownTableRowCells(header).length;
11017
+ if (columnCount < 8) {
11018
+ return limit;
11019
+ }
11020
+ const tablePrefixBytes = measureQQBotUtf8Length(`${header}
11021
+ ${separator}`);
11022
+ const extraColumnCount = Math.max(0, columnCount - 7);
11023
+ const columnPenalty = Math.min(240, extraColumnCount * 55);
11024
+ const headerPenalty = Math.min(120, Math.floor(tablePrefixBytes * 0.25));
11025
+ const reducedLimit = limit - columnPenalty - headerPenalty;
11026
+ const minTableLimit = Math.max(tablePrefixBytes + 64, Math.floor(limit * 0.45));
11027
+ const maxSingleRowRequirement = Math.min(
11028
+ limit,
11029
+ rows.reduce((max, row) => {
11030
+ const normalizedRow = renderQQBotMarkdownTableRow(
11031
+ parseQQBotMarkdownTableRowCells(row, columnCount)
11032
+ );
11033
+ return Math.max(max, measureQQBotUtf8Length(`${header}
11034
+ ${separator}
11035
+ ${normalizedRow}`));
11036
+ }, tablePrefixBytes)
11037
+ );
11038
+ return Math.min(
11039
+ limit,
11040
+ Math.max(maxSingleRowRequirement, minTableLimit, Math.min(limit, reducedLimit))
11041
+ );
11042
+ }
10741
11043
  function splitQQBotMarkdownTableBlock(text, limit) {
10742
- if (limit <= 0 || text.length <= limit) {
11044
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10743
11045
  return [text];
10744
11046
  }
10745
11047
  const lines = text.split("\n");
10746
11048
  const header = lines[0] ?? "";
10747
11049
  const separator = lines[1] ?? "";
10748
11050
  const rows = lines.slice(2);
11051
+ const packingLimit = resolveQQBotMarkdownTableBlockLimit({
11052
+ header,
11053
+ separator,
11054
+ rows,
11055
+ limit
11056
+ });
10749
11057
  const tablePrefix = `${header}
10750
11058
  ${separator}`;
11059
+ const columnCount = parseQQBotMarkdownTableRowCells(header).length;
10751
11060
  const chunks = [];
10752
11061
  let currentRows = [];
10753
11062
  const flushCurrent = () => {
@@ -10763,20 +11072,21 @@ ${currentRows.join("\n")}`);
10763
11072
  ${currentRows.join("\n")}
10764
11073
  ${row}` : `${tablePrefix}
10765
11074
  ${row}`;
10766
- if (candidate.length <= limit) {
11075
+ if (measureQQBotUtf8Length(candidate) <= packingLimit) {
10767
11076
  currentRows.push(row);
10768
11077
  continue;
10769
11078
  }
10770
11079
  flushCurrent();
10771
- if (`${tablePrefix}
10772
- ${row}`.length <= limit) {
11080
+ if (measureQQBotUtf8Length(`${tablePrefix}
11081
+ ${row}`) <= limit) {
10773
11082
  currentRows.push(row);
10774
11083
  continue;
10775
11084
  }
10776
- const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
10777
- for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
10778
- trimLeading: false,
10779
- trimTrailing: false
11085
+ const maxRowLength = Math.max(16, limit - measureQQBotUtf8Length(tablePrefix) - 1);
11086
+ for (const rowPiece of splitQQBotMarkdownTableRowByCells({
11087
+ row,
11088
+ columnCount,
11089
+ limit: maxRowLength
10780
11090
  })) {
10781
11091
  chunks.push(`${tablePrefix}
10782
11092
  ${rowPiece}`);
@@ -10786,7 +11096,7 @@ ${rowPiece}`);
10786
11096
  return chunks.length > 0 ? chunks : [text];
10787
11097
  }
10788
11098
  function splitQQBotMarkdownCodeFence(text, limit) {
10789
- if (limit <= 0 || text.length <= limit) {
11099
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10790
11100
  return [text];
10791
11101
  }
10792
11102
  const lines = text.split("\n");
@@ -10795,7 +11105,7 @@ function splitQQBotMarkdownCodeFence(text, limit) {
10795
11105
  const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
10796
11106
  const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
10797
11107
  const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
10798
- const fixedOverhead = openingLine.length + closingLine.length + 2;
11108
+ const fixedOverhead = measureQQBotUtf8Length(openingLine) + measureQQBotUtf8Length(closingLine) + 2;
10799
11109
  const availableLineLength = Math.max(1, limit - fixedOverhead);
10800
11110
  const chunks = [];
10801
11111
  let currentCodeLines = [];
@@ -10815,14 +11125,14 @@ ${codeLine}
10815
11125
  ${closingLine}` : `${openingLine}
10816
11126
  ${codeLine}
10817
11127
  ${closingLine}`;
10818
- if (candidate.length <= limit) {
11128
+ if (measureQQBotUtf8Length(candidate) <= limit) {
10819
11129
  currentCodeLines.push(codeLine);
10820
11130
  continue;
10821
11131
  }
10822
11132
  flushCurrent();
10823
- if (`${openingLine}
11133
+ if (measureQQBotUtf8Length(`${openingLine}
10824
11134
  ${codeLine}
10825
- ${closingLine}`.length <= limit) {
11135
+ ${closingLine}`) <= limit) {
10826
11136
  currentCodeLines.push(codeLine);
10827
11137
  continue;
10828
11138
  }
@@ -10836,7 +11146,7 @@ ${closingLine}`);
10836
11146
  return chunks.length > 0 ? chunks : [text];
10837
11147
  }
10838
11148
  function splitQQBotMarkdownBlock(block, limit) {
10839
- if (limit <= 0 || block.text.length <= limit) {
11149
+ if (limit <= 0 || measureQQBotUtf8Length(block.text) <= limit) {
10840
11150
  return [block.text];
10841
11151
  }
10842
11152
  switch (block.kind) {
@@ -10857,11 +11167,93 @@ function splitQQBotMarkdownBlock(block, limit) {
10857
11167
  return [block.text];
10858
11168
  }
10859
11169
  }
10860
- function chunkQQBotStructuredMarkdown(text, limit) {
11170
+ function resolveQQBotStructuredMarkdownSoftLimit(limit, safeChunkByteLimit) {
11171
+ if (limit <= QQBOT_MARKDOWN_SOFT_LIMIT_THRESHOLD) {
11172
+ return limit;
11173
+ }
11174
+ const configuredSafeLimit = typeof safeChunkByteLimit === "number" && Number.isFinite(safeChunkByteLimit) && safeChunkByteLimit > 0 ? Math.floor(safeChunkByteLimit) : void 0;
11175
+ if (configuredSafeLimit) {
11176
+ return Math.min(limit, configuredSafeLimit);
11177
+ }
11178
+ const reservedLength = Math.min(
11179
+ QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MAX,
11180
+ Math.max(
11181
+ QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_MIN,
11182
+ Math.floor(limit * QQBOT_MARKDOWN_SOFT_LIMIT_HEADROOM_RATIO)
11183
+ )
11184
+ );
11185
+ const softLimit = limit - reservedLength;
11186
+ const boundedSoftLimit = Math.min(
11187
+ softLimit > 0 ? softLimit : limit,
11188
+ DEFAULT_QQBOT_C2C_MARKDOWN_SAFE_CHUNK_BYTE_LIMIT
11189
+ );
11190
+ return boundedSoftLimit > 0 ? boundedSoftLimit : limit;
11191
+ }
11192
+ function maybePrefixQQBotContinuationPiece(params) {
11193
+ const prefix = params.prefix?.trim();
11194
+ if (!prefix) {
11195
+ return params.piece;
11196
+ }
11197
+ const prefixedPiece = joinQQBotMarkdownPieces([prefix, params.piece]);
11198
+ return measureQQBotUtf8Length(prefixedPiece) <= params.limit ? prefixedPiece : params.piece;
11199
+ }
11200
+ function resolveQQBotMarkdownLeadPiece(blocks, index, limit) {
11201
+ const block = blocks[index];
11202
+ if (!block) {
11203
+ return void 0;
11204
+ }
11205
+ if (block.kind === "heading") {
11206
+ const nextBlock = blocks[index + 1];
11207
+ if (nextBlock && nextBlock.kind !== "thematic-break") {
11208
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
11209
+ const firstBodyPiece = nextPieces[0];
11210
+ if (firstBodyPiece) {
11211
+ const pairedText = joinQQBotMarkdownPieces([block.text, firstBodyPiece]);
11212
+ if (measureQQBotUtf8Length(pairedText) <= limit) {
11213
+ return pairedText;
11214
+ }
11215
+ }
11216
+ }
11217
+ }
11218
+ return splitQQBotMarkdownBlock(block, limit).map((piece) => piece.trim()).find(Boolean);
11219
+ }
11220
+ function shouldQQBotCarryThematicBreakToNextBlock(params) {
11221
+ const block = params.blocks[params.index];
11222
+ if (!block || block.kind !== "thematic-break") {
11223
+ return false;
11224
+ }
11225
+ if (params.currentPieces.length === 0) {
11226
+ return true;
11227
+ }
11228
+ const withBreak = joinQQBotMarkdownPieces([...params.currentPieces, block.text]);
11229
+ if (measureQQBotUtf8Length(withBreak) > params.limit) {
11230
+ return true;
11231
+ }
11232
+ const nextLeadPiece = resolveQQBotMarkdownLeadPiece(
11233
+ params.blocks,
11234
+ params.index + 1,
11235
+ params.limit
11236
+ );
11237
+ if (!nextLeadPiece) {
11238
+ return false;
11239
+ }
11240
+ const prefixedLeadPiece = joinQQBotMarkdownPieces([block.text, nextLeadPiece]);
11241
+ if (measureQQBotUtf8Length(prefixedLeadPiece) > params.limit) {
11242
+ return false;
11243
+ }
11244
+ const sectionCandidate = joinQQBotMarkdownPieces([
11245
+ ...params.currentPieces,
11246
+ block.text,
11247
+ nextLeadPiece
11248
+ ]);
11249
+ return measureQQBotUtf8Length(sectionCandidate) > params.limit;
11250
+ }
11251
+ function chunkQQBotStructuredMarkdown(text, limit, safeChunkByteLimit) {
10861
11252
  const blocks = parseQQBotMarkdownBlocks(text);
10862
11253
  if (blocks.length === 0 || limit <= 0) {
10863
11254
  return [text.trim()];
10864
11255
  }
11256
+ const chunkLimit = resolveQQBotStructuredMarkdownSoftLimit(limit, safeChunkByteLimit);
10865
11257
  const chunks = [];
10866
11258
  let currentPieces = [];
10867
11259
  let pendingPrefixPieces = [];
@@ -10879,14 +11271,14 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10879
11271
  if (!piece) {
10880
11272
  return;
10881
11273
  }
10882
- const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
11274
+ const pieces = measureQQBotUtf8Length(piece) > chunkLimit ? splitQQBotTextSafely(piece, chunkLimit) : [piece];
10883
11275
  for (const nextPiece of pieces) {
10884
11276
  const normalizedPiece = nextPiece.trim();
10885
11277
  if (!normalizedPiece) {
10886
11278
  continue;
10887
11279
  }
10888
11280
  const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
10889
- if (currentPieces.length === 0 || candidate.length <= limit) {
11281
+ if (currentPieces.length === 0 || measureQQBotUtf8Length(candidate) <= chunkLimit) {
10890
11282
  currentPieces.push(normalizedPiece);
10891
11283
  continue;
10892
11284
  }
@@ -10908,9 +11300,19 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10908
11300
  continue;
10909
11301
  }
10910
11302
  if (block.kind === "thematic-break") {
11303
+ if (shouldQQBotCarryThematicBreakToNextBlock({
11304
+ blocks,
11305
+ index,
11306
+ currentPieces,
11307
+ limit: chunkLimit
11308
+ })) {
11309
+ flushCurrent();
11310
+ pendingPrefixPieces.push(block.text);
11311
+ continue;
11312
+ }
10911
11313
  if (currentPieces.length > 0) {
10912
11314
  const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
10913
- if (candidate.length <= limit) {
11315
+ if (measureQQBotUtf8Length(candidate) <= chunkLimit) {
10914
11316
  currentPieces.push(block.text);
10915
11317
  continue;
10916
11318
  }
@@ -10923,7 +11325,7 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10923
11325
  const headingText = consumePendingPrefix(block.text);
10924
11326
  const nextBlock = blocks[index + 1];
10925
11327
  if (nextBlock && nextBlock.kind !== "thematic-break") {
10926
- const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
11328
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, chunkLimit);
10927
11329
  const firstBodyPiece = nextPieces[0];
10928
11330
  if (firstBodyPiece) {
10929
11331
  const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
@@ -10932,19 +11334,33 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10932
11334
  headingText,
10933
11335
  firstBodyPiece
10934
11336
  ]);
10935
- if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
11337
+ if (measureQQBotUtf8Length(pairedText) <= chunkLimit && (currentPieces.length === 0 || measureQQBotUtf8Length(pairedCandidate) <= chunkLimit)) {
10936
11338
  currentPieces.push(headingText, firstBodyPiece);
10937
11339
  for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10938
- appendPiece(nextPieces[pieceIndex] ?? "");
11340
+ const nextPiece = nextPieces[pieceIndex] ?? "";
11341
+ appendPiece(
11342
+ nextBlock.kind === "table" ? maybePrefixQQBotContinuationPiece({
11343
+ prefix: headingText,
11344
+ piece: nextPiece,
11345
+ limit: chunkLimit
11346
+ }) : nextPiece
11347
+ );
10939
11348
  }
10940
11349
  index += 1;
10941
11350
  continue;
10942
11351
  }
10943
- if (currentPieces.length > 0 && pairedText.length <= limit) {
11352
+ if (currentPieces.length > 0 && measureQQBotUtf8Length(pairedText) <= chunkLimit) {
10944
11353
  flushCurrent();
10945
11354
  currentPieces.push(headingText, firstBodyPiece);
10946
11355
  for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10947
- appendPiece(nextPieces[pieceIndex] ?? "");
11356
+ const nextPiece = nextPieces[pieceIndex] ?? "";
11357
+ appendPiece(
11358
+ nextBlock.kind === "table" ? maybePrefixQQBotContinuationPiece({
11359
+ prefix: headingText,
11360
+ piece: nextPiece,
11361
+ limit: chunkLimit
11362
+ }) : nextPiece
11363
+ );
10948
11364
  }
10949
11365
  index += 1;
10950
11366
  continue;
@@ -10955,16 +11371,29 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10955
11371
  continue;
10956
11372
  }
10957
11373
  const blockText = consumePendingPrefix(block.text);
10958
- for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
11374
+ for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, chunkLimit)) {
10959
11375
  appendPiece(piece);
10960
11376
  }
10961
11377
  }
10962
11378
  if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
10963
11379
  const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
10964
- if (trailingCandidate.length <= limit) {
11380
+ if (measureQQBotUtf8Length(trailingCandidate) <= chunkLimit) {
10965
11381
  currentPieces.push(...pendingPrefixPieces);
11382
+ pendingPrefixPieces = [];
11383
+ }
11384
+ }
11385
+ if (pendingPrefixPieces.length > 0 && chunks.length > 0) {
11386
+ const trailingPrefix = joinQQBotMarkdownPieces(pendingPrefixPieces);
11387
+ const lastChunk = chunks[chunks.length - 1] ?? "";
11388
+ const trailingCandidate = joinQQBotMarkdownPieces([lastChunk, trailingPrefix]);
11389
+ if (measureQQBotUtf8Length(trailingCandidate) <= chunkLimit) {
11390
+ chunks[chunks.length - 1] = trailingCandidate;
11391
+ pendingPrefixPieces = [];
10966
11392
  }
10967
11393
  }
11394
+ if (pendingPrefixPieces.length > 0) {
11395
+ currentPieces.push(joinQQBotMarkdownPieces(pendingPrefixPieces));
11396
+ }
10968
11397
  flushCurrent();
10969
11398
  return chunks.length > 0 ? chunks : [text.trim()];
10970
11399
  }
@@ -10991,7 +11420,7 @@ function chunkC2CMarkdownText(params) {
10991
11420
  if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
10992
11421
  return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10993
11422
  }
10994
- return chunkQQBotStructuredMarkdown(normalized, params.limit);
11423
+ return chunkQQBotStructuredMarkdown(normalized, params.limit, params.safeChunkByteLimit);
10995
11424
  }
10996
11425
  async function sendQQBotMediaWithFallback(params) {
10997
11426
  const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
@@ -11367,6 +11796,7 @@ async function dispatchToAgent(params) {
11367
11796
  const markdownSupport = qqCfg.markdownSupport ?? true;
11368
11797
  const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
11369
11798
  const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11799
+ const c2cMarkdownSafeChunkByteLimit = resolveQQBotC2CMarkdownSafeChunkByteLimit(qqCfg);
11370
11800
  const isC2CTarget = isQQBotC2CTarget(target.to);
11371
11801
  const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
11372
11802
  let bufferedC2CMarkdownTexts = [];
@@ -11404,11 +11834,12 @@ async function dispatchToAgent(params) {
11404
11834
  text: finalMarkdownText,
11405
11835
  limit,
11406
11836
  strategy: c2cMarkdownChunkStrategy,
11837
+ safeChunkByteLimit: c2cMarkdownSafeChunkByteLimit,
11407
11838
  fallbackChunkText: chunkText
11408
11839
  }) : [];
11409
11840
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
11410
11841
  logger.info(
11411
- `delivery=${deliveryLabel} to=${target.to} chunks=${textChunks.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")} chunkStrategy=${c2cMarkdownChunkStrategy}`
11842
+ `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")}`
11412
11843
  );
11413
11844
  if (!shouldSuppressVisibleReplies()) {
11414
11845
  if (mediaQueue.length > 0) {
@@ -11473,7 +11904,7 @@ async function dispatchToAgent(params) {
11473
11904
  bufferedC2CMarkdownMediaSeen.clear();
11474
11905
  return;
11475
11906
  }
11476
- const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
11907
+ const combinedText = combineQQBotBufferedText(bufferedC2CMarkdownTexts);
11477
11908
  const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
11478
11909
  bufferedC2CMarkdownTexts = [];
11479
11910
  bufferedC2CMarkdownMediaUrls = [];
@@ -11519,7 +11950,7 @@ async function dispatchToAgent(params) {
11519
11950
  const textToSend = suppressText ? "" : cleanedText;
11520
11951
  if (useC2CMarkdownTransport) {
11521
11952
  const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
11522
- const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
11953
+ const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && (hasBufferedC2CMarkdownReply() || looksLikeStructuredMarkdown(textToSend));
11523
11954
  if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
11524
11955
  if (textToSend) {
11525
11956
  bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
@@ -12304,6 +12735,7 @@ var qqbotPlugin = {
12304
12735
  type: "string",
12305
12736
  enum: ["markdown-block", "length"]
12306
12737
  },
12738
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12307
12739
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12308
12740
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12309
12741
  requireMention: { type: "boolean" },
@@ -12357,6 +12789,7 @@ var qqbotPlugin = {
12357
12789
  type: "string",
12358
12790
  enum: ["markdown-block", "length"]
12359
12791
  },
12792
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12360
12793
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12361
12794
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12362
12795
  requireMention: { type: "boolean" },
@@ -12569,6 +13002,7 @@ var plugin = {
12569
13002
  type: "string",
12570
13003
  enum: ["markdown-block", "length"]
12571
13004
  },
13005
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12572
13006
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12573
13007
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12574
13008
  requireMention: { type: "boolean" },
@@ -12622,6 +13056,7 @@ var plugin = {
12622
13056
  type: "string",
12623
13057
  enum: ["markdown-block", "length"]
12624
13058
  },
13059
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12625
13060
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12626
13061
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12627
13062
  requireMention: { type: "boolean" },