openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
186 lines (185 loc) • 7.05 kB
JavaScript
import { D as resolveIntegerOption } from "./number-coercion-CJQ8TR--.js";
import { i as chunkMarkdownTextWithMode } from "./chunk-Cpb8JO1x.js";
import "./number-runtime-DBLVDypr.js";
import "./reply-chunking-C9pgDFVG.js";
//#region extensions/discord/src/chunk.ts
const DEFAULT_MAX_CHARS = 2e3;
const DEFAULT_MAX_LINES = 17;
const FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
const CJK_PUNCTUATION_BREAK_AFTER_RE = /[、。,.!?;:)]}〉》」』】〕〗〙]/u;
function resolveDiscordChunkLimit(value, fallback) {
return resolveIntegerOption(value, fallback, { min: 1 });
}
function countLines(text) {
if (!text) return 0;
return text.split("\n").length;
}
function parseFenceLine(line) {
const match = line.match(FENCE_RE);
if (!match) return null;
const indent = match[1] ?? "";
const marker = match[2] ?? "";
return {
indent,
markerChar: marker[0] ?? "`",
markerLen: marker.length,
openLine: line
};
}
function closeFenceLine(openFence) {
return `${openFence.indent}${openFence.markerChar.repeat(openFence.markerLen)}`;
}
function closeFenceIfNeeded(text, openFence) {
if (!openFence) return text;
const closeLine = closeFenceLine(openFence);
if (!text) return closeLine;
if (!text.endsWith("\n")) return `${text}\n${closeLine}`;
return `${text}${closeLine}`;
}
function isHighSurrogate(code) {
return code >= 55296 && code <= 56319;
}
function isLowSurrogate(code) {
return code >= 56320 && code <= 57343;
}
function clampToCodePointBoundary(text, index) {
const boundary = Math.min(Math.max(0, index), text.length);
if (boundary <= 0 || boundary >= text.length) return boundary;
const previous = text.charCodeAt(boundary - 1);
const next = text.charCodeAt(boundary);
if (isHighSurrogate(previous) && isLowSurrogate(next)) return boundary > 1 ? boundary - 1 : boundary + 1;
return boundary;
}
function findWhitespaceBreak(window) {
for (let i = window.length - 1; i >= 0; i--) if (/\s/.test(window[i])) return i;
return -1;
}
function findCjkPunctuationBreak(window) {
for (let end = window.length; end > 0;) {
const start = isLowSurrogate(window.charCodeAt(end - 1)) && end > 1 ? end - 2 : end - 1;
const char = window.slice(start, end);
if (start > 0 && CJK_PUNCTUATION_BREAK_AFTER_RE.test(char)) return end;
end = start;
}
return -1;
}
function splitLongLine(line, maxChars, opts) {
const limit = resolveDiscordChunkLimit(maxChars, DEFAULT_MAX_CHARS);
if (line.length <= limit) return [line];
const out = [];
let remaining = line;
while (remaining.length > limit) {
if (opts.preserveWhitespace) {
const breakIdx = clampToCodePointBoundary(remaining, limit);
out.push(remaining.slice(0, breakIdx));
remaining = remaining.slice(breakIdx);
continue;
}
const window = remaining.slice(0, limit);
let breakIdx = findWhitespaceBreak(window);
if (breakIdx <= 0) breakIdx = findCjkPunctuationBreak(window);
if (breakIdx <= 0) breakIdx = clampToCodePointBoundary(remaining, limit);
out.push(remaining.slice(0, breakIdx));
remaining = remaining.slice(breakIdx);
}
if (remaining.length) out.push(remaining);
return out;
}
/**
* Chunks outbound Discord text by both character count and (soft) line count,
* while keeping fenced code blocks balanced across chunks.
*/
function chunkDiscordText(text, opts = {}) {
const maxChars = resolveDiscordChunkLimit(opts.maxChars, DEFAULT_MAX_CHARS);
const maxLines = resolveDiscordChunkLimit(opts.maxLines, DEFAULT_MAX_LINES);
const body = text ?? "";
if (!body) return [];
if (body.length <= maxChars && countLines(body) <= maxLines) return [body];
const lines = body.split("\n");
const chunks = [];
let current = "";
let currentLines = 0;
let openFence = null;
const flush = () => {
if (!current) return;
const payload = closeFenceIfNeeded(current, openFence);
if (payload.trim().length) chunks.push(payload);
current = "";
currentLines = 0;
if (openFence) {
current = openFence.openLine;
currentLines = 1;
}
};
for (const originalLine of lines) {
const fenceInfo = parseFenceLine(originalLine);
const wasInsideFence = openFence !== null;
let nextOpenFence = openFence;
if (fenceInfo) {
if (!openFence) nextOpenFence = fenceInfo;
else if (openFence.markerChar === fenceInfo.markerChar && fenceInfo.markerLen >= openFence.markerLen) nextOpenFence = null;
}
const reserveChars = nextOpenFence ? closeFenceLine(nextOpenFence).length + 1 : 0;
const reserveLines = nextOpenFence ? 1 : 0;
const effectiveMaxChars = maxChars - reserveChars;
const effectiveMaxLines = maxLines - reserveLines;
const charLimit = effectiveMaxChars > 0 ? effectiveMaxChars : maxChars;
const lineLimit = effectiveMaxLines > 0 ? effectiveMaxLines : maxLines;
const prefixLen = current.length > 0 ? current.length + 1 : 0;
const segments = splitLongLine(originalLine, Math.max(1, charLimit - prefixLen), { preserveWhitespace: wasInsideFence });
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
const segment = segments[segIndex];
const isLineContinuation = segIndex > 0;
const addition = `${isLineContinuation ? "" : current.length > 0 ? "\n" : ""}${segment}`;
const nextLen = current.length + addition.length;
const nextLines = currentLines + (isLineContinuation ? 0 : 1);
if ((nextLen > charLimit || nextLines > lineLimit) && current.length > 0) flush();
if (current.length > 0) {
current += addition;
if (!isLineContinuation) currentLines += 1;
} else {
current = segment;
currentLines = 1;
}
}
openFence = nextOpenFence;
}
if (current.length) {
const payload = closeFenceIfNeeded(current, openFence);
if (payload.trim().length) chunks.push(payload);
}
return rebalanceReasoningItalics(text, chunks);
}
function chunkDiscordTextWithMode(text, opts) {
if ((opts.chunkMode ?? "length") !== "newline") return chunkDiscordText(text, opts);
const lineChunks = chunkMarkdownTextWithMode(text, resolveDiscordChunkLimit(opts.maxChars, DEFAULT_MAX_CHARS), "newline");
const chunks = [];
for (const line of lineChunks) {
const nested = chunkDiscordText(line, opts);
if (!nested.length && line) {
chunks.push(line);
continue;
}
chunks.push(...nested);
}
return chunks;
}
function rebalanceReasoningItalics(source, chunks) {
if (chunks.length <= 1) return chunks;
if (!(/^(?:Reasoning:|Thinking\.{0,3})\n+_/u.test(source) && source.trimEnd().endsWith("_"))) return chunks;
const adjusted = [...chunks];
for (let i = 0; i < adjusted.length; i++) {
const isLast = i === adjusted.length - 1;
const current = adjusted[i];
if (!current.trimEnd().endsWith("_")) adjusted[i] = `${current}_`;
if (isLast) break;
const next = adjusted[i + 1];
const leadingWhitespaceLen = next.length - next.trimStart().length;
const leadingWhitespace = next.slice(0, leadingWhitespaceLen);
const nextBody = next.slice(leadingWhitespaceLen);
if (!nextBody.startsWith("_")) adjusted[i + 1] = `${leadingWhitespace}_${nextBody}`;
}
return adjusted;
}
//#endregion
export { chunkDiscordTextWithMode as t };