@openclaw-china/qqbot 2026.3.18 → 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
@@ -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,72 @@ 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
+ 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,
7099
7178
  welcomeText: welcomeText || void 0
7100
7179
  });
7101
7180
  }
@@ -7157,6 +7236,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
7157
7236
  return configureWecomApp(prompter, cfg);
7158
7237
  case "wecom-kf":
7159
7238
  return configureWecomKf(prompter, cfg);
7239
+ case "wechat-mp":
7240
+ return configureWechatMp(prompter, cfg);
7160
7241
  case "qqbot":
7161
7242
  return configureQQBot(prompter, cfg);
7162
7243
  default:
@@ -7298,6 +7379,7 @@ var SUPPORTED_CHANNELS = [
7298
7379
  "wecom",
7299
7380
  "wecom-app",
7300
7381
  "wecom-kf",
7382
+ "wechat-mp",
7301
7383
  "qqbot"
7302
7384
  ];
7303
7385
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -10136,7 +10218,7 @@ function extractLocalMediaFromText(params) {
10136
10218
  const MARKDOWN_LINKED_IMAGE_RE2 = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g;
10137
10219
  const MARKDOWN_IMAGE_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
10138
10220
  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;
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;
10140
10222
  const collectLocalRichMedia = (rawValue, allowedTypes) => {
10141
10223
  const candidate = stripTitleFromUrl(rawValue.trim());
10142
10224
  if (!candidate || !isLocalReference(candidate)) {
@@ -10245,6 +10327,10 @@ var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[
10245
10327
  var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
10246
10328
  var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
10247
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;
10248
10334
  function extractFinalBlocks(text) {
10249
10335
  const matches = Array.from(text.matchAll(FINAL_BLOCK_RE));
10250
10336
  if (matches.length === 0) return void 0;
@@ -10384,6 +10470,72 @@ function appendQQBotBufferedText(bufferedTexts, nextText) {
10384
10470
  }
10385
10471
  return [...bufferedTexts, normalized];
10386
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
+ }
10387
10539
  function normalizeQQBotRenderedMarkdown(text) {
10388
10540
  if (!text.trim()) return "";
10389
10541
  let next = text.trim();
@@ -10603,6 +10755,25 @@ function parseQQBotMarkdownBlocks(text) {
10603
10755
  function hasQQBotBoundaryGuard(text) {
10604
10756
  return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
10605
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
+ }
10606
10777
  function isQQBotSafeMarkdownBoundary(text, index) {
10607
10778
  const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
10608
10779
  const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
@@ -10611,13 +10782,14 @@ function isQQBotSafeMarkdownBoundary(text, index) {
10611
10782
  return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
10612
10783
  }
10613
10784
  function findQQBotRegexBoundary(text, limit, pattern) {
10614
- const scopedText = text.slice(0, Math.min(limit + 1, text.length));
10785
+ const scopedIndex = findQQBotIndexWithinUtf8Limit(text, limit);
10786
+ const scopedText = text.slice(0, scopedIndex);
10615
10787
  const regex = new RegExp(pattern.source, pattern.flags);
10616
10788
  let match = regex.exec(scopedText);
10617
10789
  let lastBoundary;
10618
10790
  while (match) {
10619
10791
  const boundary = match.index + match[0].length;
10620
- if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10792
+ if (boundary > 0 && measureQQBotUtf8Length(text.slice(0, boundary)) <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
10621
10793
  lastBoundary = boundary;
10622
10794
  }
10623
10795
  match = regex.exec(scopedText);
@@ -10625,13 +10797,14 @@ function findQQBotRegexBoundary(text, limit, pattern) {
10625
10797
  return lastBoundary;
10626
10798
  }
10627
10799
  function findQQBotFallbackBoundary(text, limit) {
10628
- const minIndex = Math.max(1, limit - 120);
10629
- 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) {
10630
10803
  if (isQQBotSafeMarkdownBoundary(text, index)) {
10631
10804
  return index;
10632
10805
  }
10633
10806
  }
10634
- return limit;
10807
+ return maxIndex;
10635
10808
  }
10636
10809
  function findQQBotSafeSplitIndex(text, limit) {
10637
10810
  const boundaryPatterns = [
@@ -10650,14 +10823,15 @@ function findQQBotSafeSplitIndex(text, limit) {
10650
10823
  return findQQBotFallbackBoundary(text, limit);
10651
10824
  }
10652
10825
  function splitQQBotHardText(text, limit) {
10653
- if (limit <= 0 || text.length <= limit) {
10826
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10654
10827
  return [text];
10655
10828
  }
10656
10829
  const chunks = [];
10657
10830
  let remaining = text;
10658
- while (remaining.length > limit) {
10659
- chunks.push(remaining.slice(0, limit));
10660
- 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);
10661
10835
  }
10662
10836
  if (remaining) {
10663
10837
  chunks.push(remaining);
@@ -10665,14 +10839,14 @@ function splitQQBotHardText(text, limit) {
10665
10839
  return chunks;
10666
10840
  }
10667
10841
  function splitQQBotTextSafely(text, limit, options) {
10668
- if (limit <= 0 || text.length <= limit) {
10842
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10669
10843
  return [text];
10670
10844
  }
10671
10845
  const trimLeading = options?.trimLeading ?? true;
10672
10846
  const trimTrailing = options?.trimTrailing ?? true;
10673
10847
  const chunks = [];
10674
10848
  let remaining = text;
10675
- while (remaining.length > limit) {
10849
+ while (measureQQBotUtf8Length(remaining) > limit) {
10676
10850
  const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
10677
10851
  let nextChunk = remaining.slice(0, splitIndex);
10678
10852
  let nextRemaining = remaining.slice(splitIndex);
@@ -10683,7 +10857,8 @@ function splitQQBotTextSafely(text, limit, options) {
10683
10857
  nextRemaining = nextRemaining.trimStart();
10684
10858
  }
10685
10859
  if (!nextChunk) {
10686
- const hardChunk = remaining.slice(0, limit);
10860
+ const hardChunkIndex = Math.max(1, findQQBotIndexWithinUtf8Limit(remaining, limit));
10861
+ const hardChunk = remaining.slice(0, hardChunkIndex);
10687
10862
  chunks.push(hardChunk);
10688
10863
  remaining = remaining.slice(hardChunk.length);
10689
10864
  continue;
@@ -10698,7 +10873,7 @@ function splitQQBotTextSafely(text, limit, options) {
10698
10873
  return chunks;
10699
10874
  }
10700
10875
  function splitQQBotMarkdownLineBlock(text, limit) {
10701
- if (limit <= 0 || text.length <= limit) {
10876
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10702
10877
  return [text];
10703
10878
  }
10704
10879
  const lines = text.split("\n");
@@ -10717,12 +10892,12 @@ function splitQQBotMarkdownLineBlock(text, limit) {
10717
10892
  for (const line of lines) {
10718
10893
  const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
10719
10894
  ${line}` : line;
10720
- if (candidate.length <= limit) {
10895
+ if (measureQQBotUtf8Length(candidate) <= limit) {
10721
10896
  currentLines.push(line);
10722
10897
  continue;
10723
10898
  }
10724
10899
  flushCurrent();
10725
- if (line.length <= limit) {
10900
+ if (measureQQBotUtf8Length(line) <= limit) {
10726
10901
  currentLines.push(line);
10727
10902
  continue;
10728
10903
  }
@@ -10738,16 +10913,133 @@ ${line}` : line;
10738
10913
  flushCurrent();
10739
10914
  return chunks;
10740
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
+ }
10741
11026
  function splitQQBotMarkdownTableBlock(text, limit) {
10742
- if (limit <= 0 || text.length <= limit) {
11027
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10743
11028
  return [text];
10744
11029
  }
10745
11030
  const lines = text.split("\n");
10746
11031
  const header = lines[0] ?? "";
10747
11032
  const separator = lines[1] ?? "";
10748
11033
  const rows = lines.slice(2);
11034
+ const packingLimit = resolveQQBotMarkdownTableBlockLimit({
11035
+ header,
11036
+ separator,
11037
+ rows,
11038
+ limit
11039
+ });
10749
11040
  const tablePrefix = `${header}
10750
11041
  ${separator}`;
11042
+ const columnCount = parseQQBotMarkdownTableRowCells(header).length;
10751
11043
  const chunks = [];
10752
11044
  let currentRows = [];
10753
11045
  const flushCurrent = () => {
@@ -10763,20 +11055,21 @@ ${currentRows.join("\n")}`);
10763
11055
  ${currentRows.join("\n")}
10764
11056
  ${row}` : `${tablePrefix}
10765
11057
  ${row}`;
10766
- if (candidate.length <= limit) {
11058
+ if (measureQQBotUtf8Length(candidate) <= packingLimit) {
10767
11059
  currentRows.push(row);
10768
11060
  continue;
10769
11061
  }
10770
11062
  flushCurrent();
10771
- if (`${tablePrefix}
10772
- ${row}`.length <= limit) {
11063
+ if (measureQQBotUtf8Length(`${tablePrefix}
11064
+ ${row}`) <= limit) {
10773
11065
  currentRows.push(row);
10774
11066
  continue;
10775
11067
  }
10776
- const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
10777
- for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
10778
- trimLeading: false,
10779
- 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
10780
11073
  })) {
10781
11074
  chunks.push(`${tablePrefix}
10782
11075
  ${rowPiece}`);
@@ -10786,7 +11079,7 @@ ${rowPiece}`);
10786
11079
  return chunks.length > 0 ? chunks : [text];
10787
11080
  }
10788
11081
  function splitQQBotMarkdownCodeFence(text, limit) {
10789
- if (limit <= 0 || text.length <= limit) {
11082
+ if (limit <= 0 || measureQQBotUtf8Length(text) <= limit) {
10790
11083
  return [text];
10791
11084
  }
10792
11085
  const lines = text.split("\n");
@@ -10795,7 +11088,7 @@ function splitQQBotMarkdownCodeFence(text, limit) {
10795
11088
  const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
10796
11089
  const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
10797
11090
  const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
10798
- const fixedOverhead = openingLine.length + closingLine.length + 2;
11091
+ const fixedOverhead = measureQQBotUtf8Length(openingLine) + measureQQBotUtf8Length(closingLine) + 2;
10799
11092
  const availableLineLength = Math.max(1, limit - fixedOverhead);
10800
11093
  const chunks = [];
10801
11094
  let currentCodeLines = [];
@@ -10815,14 +11108,14 @@ ${codeLine}
10815
11108
  ${closingLine}` : `${openingLine}
10816
11109
  ${codeLine}
10817
11110
  ${closingLine}`;
10818
- if (candidate.length <= limit) {
11111
+ if (measureQQBotUtf8Length(candidate) <= limit) {
10819
11112
  currentCodeLines.push(codeLine);
10820
11113
  continue;
10821
11114
  }
10822
11115
  flushCurrent();
10823
- if (`${openingLine}
11116
+ if (measureQQBotUtf8Length(`${openingLine}
10824
11117
  ${codeLine}
10825
- ${closingLine}`.length <= limit) {
11118
+ ${closingLine}`) <= limit) {
10826
11119
  currentCodeLines.push(codeLine);
10827
11120
  continue;
10828
11121
  }
@@ -10836,7 +11129,7 @@ ${closingLine}`);
10836
11129
  return chunks.length > 0 ? chunks : [text];
10837
11130
  }
10838
11131
  function splitQQBotMarkdownBlock(block, limit) {
10839
- if (limit <= 0 || block.text.length <= limit) {
11132
+ if (limit <= 0 || measureQQBotUtf8Length(block.text) <= limit) {
10840
11133
  return [block.text];
10841
11134
  }
10842
11135
  switch (block.kind) {
@@ -10857,11 +11150,93 @@ function splitQQBotMarkdownBlock(block, limit) {
10857
11150
  return [block.text];
10858
11151
  }
10859
11152
  }
10860
- 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) {
10861
11235
  const blocks = parseQQBotMarkdownBlocks(text);
10862
11236
  if (blocks.length === 0 || limit <= 0) {
10863
11237
  return [text.trim()];
10864
11238
  }
11239
+ const chunkLimit = resolveQQBotStructuredMarkdownSoftLimit(limit, safeChunkByteLimit);
10865
11240
  const chunks = [];
10866
11241
  let currentPieces = [];
10867
11242
  let pendingPrefixPieces = [];
@@ -10879,14 +11254,14 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10879
11254
  if (!piece) {
10880
11255
  return;
10881
11256
  }
10882
- const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
11257
+ const pieces = measureQQBotUtf8Length(piece) > chunkLimit ? splitQQBotTextSafely(piece, chunkLimit) : [piece];
10883
11258
  for (const nextPiece of pieces) {
10884
11259
  const normalizedPiece = nextPiece.trim();
10885
11260
  if (!normalizedPiece) {
10886
11261
  continue;
10887
11262
  }
10888
11263
  const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
10889
- if (currentPieces.length === 0 || candidate.length <= limit) {
11264
+ if (currentPieces.length === 0 || measureQQBotUtf8Length(candidate) <= chunkLimit) {
10890
11265
  currentPieces.push(normalizedPiece);
10891
11266
  continue;
10892
11267
  }
@@ -10908,9 +11283,19 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10908
11283
  continue;
10909
11284
  }
10910
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
+ }
10911
11296
  if (currentPieces.length > 0) {
10912
11297
  const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
10913
- if (candidate.length <= limit) {
11298
+ if (measureQQBotUtf8Length(candidate) <= chunkLimit) {
10914
11299
  currentPieces.push(block.text);
10915
11300
  continue;
10916
11301
  }
@@ -10923,7 +11308,7 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10923
11308
  const headingText = consumePendingPrefix(block.text);
10924
11309
  const nextBlock = blocks[index + 1];
10925
11310
  if (nextBlock && nextBlock.kind !== "thematic-break") {
10926
- const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
11311
+ const nextPieces = splitQQBotMarkdownBlock(nextBlock, chunkLimit);
10927
11312
  const firstBodyPiece = nextPieces[0];
10928
11313
  if (firstBodyPiece) {
10929
11314
  const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
@@ -10932,19 +11317,33 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10932
11317
  headingText,
10933
11318
  firstBodyPiece
10934
11319
  ]);
10935
- if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
11320
+ if (measureQQBotUtf8Length(pairedText) <= chunkLimit && (currentPieces.length === 0 || measureQQBotUtf8Length(pairedCandidate) <= chunkLimit)) {
10936
11321
  currentPieces.push(headingText, firstBodyPiece);
10937
11322
  for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10938
- 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
+ );
10939
11331
  }
10940
11332
  index += 1;
10941
11333
  continue;
10942
11334
  }
10943
- if (currentPieces.length > 0 && pairedText.length <= limit) {
11335
+ if (currentPieces.length > 0 && measureQQBotUtf8Length(pairedText) <= chunkLimit) {
10944
11336
  flushCurrent();
10945
11337
  currentPieces.push(headingText, firstBodyPiece);
10946
11338
  for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
10947
- 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
+ );
10948
11347
  }
10949
11348
  index += 1;
10950
11349
  continue;
@@ -10955,16 +11354,29 @@ function chunkQQBotStructuredMarkdown(text, limit) {
10955
11354
  continue;
10956
11355
  }
10957
11356
  const blockText = consumePendingPrefix(block.text);
10958
- for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
11357
+ for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, chunkLimit)) {
10959
11358
  appendPiece(piece);
10960
11359
  }
10961
11360
  }
10962
11361
  if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
10963
11362
  const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
10964
- if (trailingCandidate.length <= limit) {
11363
+ if (measureQQBotUtf8Length(trailingCandidate) <= chunkLimit) {
10965
11364
  currentPieces.push(...pendingPrefixPieces);
11365
+ pendingPrefixPieces = [];
11366
+ }
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 = [];
10966
11375
  }
10967
11376
  }
11377
+ if (pendingPrefixPieces.length > 0) {
11378
+ currentPieces.push(joinQQBotMarkdownPieces(pendingPrefixPieces));
11379
+ }
10968
11380
  flushCurrent();
10969
11381
  return chunks.length > 0 ? chunks : [text.trim()];
10970
11382
  }
@@ -10991,7 +11403,7 @@ function chunkC2CMarkdownText(params) {
10991
11403
  if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
10992
11404
  return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
10993
11405
  }
10994
- return chunkQQBotStructuredMarkdown(normalized, params.limit);
11406
+ return chunkQQBotStructuredMarkdown(normalized, params.limit, params.safeChunkByteLimit);
10995
11407
  }
10996
11408
  async function sendQQBotMediaWithFallback(params) {
10997
11409
  const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
@@ -11367,6 +11779,7 @@ async function dispatchToAgent(params) {
11367
11779
  const markdownSupport = qqCfg.markdownSupport ?? true;
11368
11780
  const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
11369
11781
  const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
11782
+ const c2cMarkdownSafeChunkByteLimit = resolveQQBotC2CMarkdownSafeChunkByteLimit(qqCfg);
11370
11783
  const isC2CTarget = isQQBotC2CTarget(target.to);
11371
11784
  const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
11372
11785
  let bufferedC2CMarkdownTexts = [];
@@ -11404,11 +11817,12 @@ async function dispatchToAgent(params) {
11404
11817
  text: finalMarkdownText,
11405
11818
  limit,
11406
11819
  strategy: c2cMarkdownChunkStrategy,
11820
+ safeChunkByteLimit: c2cMarkdownSafeChunkByteLimit,
11407
11821
  fallbackChunkText: chunkText
11408
11822
  }) : [];
11409
11823
  const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
11410
11824
  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}`
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")}`
11412
11826
  );
11413
11827
  if (!shouldSuppressVisibleReplies()) {
11414
11828
  if (mediaQueue.length > 0) {
@@ -11473,7 +11887,7 @@ async function dispatchToAgent(params) {
11473
11887
  bufferedC2CMarkdownMediaSeen.clear();
11474
11888
  return;
11475
11889
  }
11476
- const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
11890
+ const combinedText = combineQQBotBufferedText(bufferedC2CMarkdownTexts);
11477
11891
  const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
11478
11892
  bufferedC2CMarkdownTexts = [];
11479
11893
  bufferedC2CMarkdownMediaUrls = [];
@@ -11519,7 +11933,7 @@ async function dispatchToAgent(params) {
11519
11933
  const textToSend = suppressText ? "" : cleanedText;
11520
11934
  if (useC2CMarkdownTransport) {
11521
11935
  const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
11522
- const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
11936
+ const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && (hasBufferedC2CMarkdownReply() || looksLikeStructuredMarkdown(textToSend));
11523
11937
  if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
11524
11938
  if (textToSend) {
11525
11939
  bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
@@ -12304,6 +12718,7 @@ var qqbotPlugin = {
12304
12718
  type: "string",
12305
12719
  enum: ["markdown-block", "length"]
12306
12720
  },
12721
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12307
12722
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12308
12723
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12309
12724
  requireMention: { type: "boolean" },
@@ -12357,6 +12772,7 @@ var qqbotPlugin = {
12357
12772
  type: "string",
12358
12773
  enum: ["markdown-block", "length"]
12359
12774
  },
12775
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12360
12776
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12361
12777
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12362
12778
  requireMention: { type: "boolean" },
@@ -12569,6 +12985,7 @@ var plugin = {
12569
12985
  type: "string",
12570
12986
  enum: ["markdown-block", "length"]
12571
12987
  },
12988
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12572
12989
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12573
12990
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12574
12991
  requireMention: { type: "boolean" },
@@ -12622,6 +13039,7 @@ var plugin = {
12622
13039
  type: "string",
12623
13040
  enum: ["markdown-block", "length"]
12624
13041
  },
13042
+ c2cMarkdownSafeChunkByteLimit: { type: "integer", minimum: 1 },
12625
13043
  dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
12626
13044
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
12627
13045
  requireMention: { type: "boolean" },