openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
543 lines (542 loc) • 16.8 kB
JavaScript
import { S as TextStyle, _ as sendZaloTypingEvent, f as sendZaloDeliveredEvent, g as sendZaloTextMessage, h as sendZaloSeenEvent, m as sendZaloReaction, p as sendZaloLink, x as createZalouserSendReceipt } from "./zalo-js-B1zE_Wyq.js";
//#region extensions/zalouser/src/text-styles.ts
const ESCAPE_SENTINEL_START = "";
const ESCAPE_SENTINEL_END = "";
const TAG_STYLE_MAP = {
red: TextStyle.Red,
orange: TextStyle.Orange,
yellow: TextStyle.Yellow,
green: TextStyle.Green,
small: null,
big: TextStyle.Big,
underline: TextStyle.Underline
};
const INLINE_MARKERS = [
{
pattern: /`([^`\n]+)`/g,
extractText: (match) => match[0],
literal: true
},
{
pattern: /\\([*_~#\\{}>+\-`])/g,
extractText: (match) => match[1],
literal: true
},
{
pattern: new RegExp(`\\{(${Object.keys(TAG_STYLE_MAP).join("|")})\\}(.+?)\\{/\\1\\}`, "g"),
extractText: (match) => match[2],
resolveStyles: (match) => {
const style = TAG_STYLE_MAP[match[1]];
return style ? [style] : [];
}
},
{
pattern: /(?<!\*)\*\*\*(?=\S)([^\n]*?\S)(?<!\*)\*\*\*(?!\*)/g,
extractText: (match) => match[1],
resolveStyles: () => [TextStyle.Bold, TextStyle.Italic]
},
{
pattern: /(?<!\*)\*\*(?![\s*])([^\n]*?\S)(?<!\*)\*\*(?!\*)/g,
extractText: (match) => match[1],
resolveStyles: () => [TextStyle.Bold]
},
{
pattern: /(?<![\w_])__(?![\s_])([^\n]*?\S)(?<!_)__(?![\w_])/g,
extractText: (match) => match[1],
resolveStyles: () => [TextStyle.Bold]
},
{
pattern: /(?<!~)~~(?=\S)([^\n]*?\S)(?<!~)~~(?!~)/g,
extractText: (match) => match[1],
resolveStyles: () => [TextStyle.StrikeThrough]
},
{
pattern: /(?<!\*)\*(?![\s*])([^\n]*?\S)(?<!\*)\*(?!\*)/g,
extractText: (match) => match[1],
resolveStyles: () => [TextStyle.Italic]
},
{
pattern: /(?<![\w_])_(?![\s_])([^\n]*?\S)(?<!_)_(?![\w_])/g,
extractText: (match) => match[1],
resolveStyles: () => [TextStyle.Italic]
}
];
function parseZalouserTextStyles(input) {
const allStyles = [];
const escapeMap = [];
const lines = input.replace(/\r\n?/g, "\n").split("\n");
const lineStyles = [];
const processedLines = [];
let activeFence = null;
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const rawLine = lines[lineIndex];
const { text: unquotedLine, indent: baseIndent } = stripQuotePrefix(rawLine);
if (activeFence) {
const codeLine = activeFence.quoteIndent > 0 ? stripQuotePrefix(rawLine, activeFence.quoteIndent).text : rawLine;
if (isClosingFence(codeLine, activeFence)) {
activeFence = null;
continue;
}
processedLines.push(escapeLiteralText(normalizeCodeBlockLeadingWhitespace(stripCodeFenceIndent(codeLine, activeFence.indent)), escapeMap));
continue;
}
const line = unquotedLine;
const openingFence = resolveOpeningFence(rawLine);
if (openingFence) {
const fenceLine = openingFence.quoteIndent > 0 ? unquotedLine : rawLine;
if (!hasClosingFence(lines, lineIndex + 1, openingFence)) {
processedLines.push(escapeLiteralText(fenceLine, escapeMap));
activeFence = openingFence;
continue;
}
activeFence = openingFence;
continue;
}
const outputLineIndex = processedLines.length;
if (isIndentedCodeBlockLine(line)) {
if (baseIndent > 0) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: baseIndent
});
processedLines.push(escapeLiteralText(normalizeCodeBlockLeadingWhitespace(line), escapeMap));
continue;
}
const { text: markdownLine, size: markdownPadding } = stripOptionalMarkdownPadding(line);
const headingMatch = markdownLine.match(/^(#{1,4})\s(.*)$/);
if (headingMatch) {
const depth = headingMatch[1].length;
lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Bold
});
if (depth === 1) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Big
});
if (baseIndent > 0) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: baseIndent
});
processedLines.push(headingMatch[2]);
continue;
}
const indentMatch = markdownLine.match(/^(\s+)(.*)$/);
let indentLevel = 0;
let content = markdownLine;
if (indentMatch) {
indentLevel = clampIndent(indentMatch[1].length);
content = indentMatch[2];
}
const totalIndent = Math.min(5, baseIndent + indentLevel);
if (/^[-*+]\s\[[ xX]\]\s/.test(content)) {
if (totalIndent > 0) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: totalIndent
});
processedLines.push(content);
continue;
}
const orderedListMatch = content.match(/^(\d+)\.\s(.*)$/);
if (orderedListMatch) {
if (totalIndent > 0) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: totalIndent
});
lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.OrderedList
});
processedLines.push(orderedListMatch[2]);
continue;
}
const unorderedListMatch = content.match(/^[-*+]\s(.*)$/);
if (unorderedListMatch) {
if (totalIndent > 0) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: totalIndent
});
lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.UnorderedList
});
processedLines.push(unorderedListMatch[1]);
continue;
}
if (markdownPadding > 0) {
if (baseIndent > 0) lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: baseIndent
});
processedLines.push(line);
continue;
}
if (totalIndent > 0) {
lineStyles.push({
lineIndex: outputLineIndex,
style: TextStyle.Indent,
indentSize: totalIndent
});
processedLines.push(content);
continue;
}
processedLines.push(line);
}
const segments = parseInlineSegments(processedLines.join("\n"));
let plainText = "";
for (const segment of segments) {
const start = plainText.length;
plainText += segment.text;
for (const style of segment.styles) allStyles.push({
start,
len: segment.text.length,
st: style
});
}
if (escapeMap.length > 0) {
const escapeRegex = new RegExp(`${ESCAPE_SENTINEL_START}(\\d+)${ESCAPE_SENTINEL_END}`, "g");
const shifts = [];
let cumulativeDelta = 0;
for (const match of plainText.matchAll(escapeRegex)) {
const escapeIndex = Number.parseInt(match[1], 10);
cumulativeDelta += match[0].length - escapeMap[escapeIndex].length;
shifts.push({
pos: (match.index ?? 0) + match[0].length,
delta: cumulativeDelta
});
}
for (const style of allStyles) {
let startDelta = 0;
let endDelta = 0;
const end = style.start + style.len;
for (const shift of shifts) {
if (shift.pos <= style.start) startDelta = shift.delta;
if (shift.pos <= end) endDelta = shift.delta;
}
style.start -= startDelta;
style.len -= endDelta - startDelta;
}
plainText = plainText.replace(escapeRegex, (_match, index) => escapeMap[Number.parseInt(index, 10)]);
}
const finalLines = plainText.split("\n");
let offset = 0;
for (let lineIndex = 0; lineIndex < finalLines.length; lineIndex += 1) {
const lineLength = finalLines[lineIndex].length;
if (lineLength > 0) for (const lineStyle of lineStyles) {
if (lineStyle.lineIndex !== lineIndex) continue;
if (lineStyle.style === TextStyle.Indent) allStyles.push({
start: offset,
len: lineLength,
st: TextStyle.Indent,
indentSize: lineStyle.indentSize
});
else allStyles.push({
start: offset,
len: lineLength,
st: lineStyle.style
});
}
offset += lineLength + 1;
}
return {
text: plainText,
styles: allStyles
};
}
function clampIndent(spaceCount) {
return Math.min(5, Math.max(1, Math.floor(spaceCount / 2)));
}
function stripOptionalMarkdownPadding(line) {
const match = line.match(/^( {1,3})(?=\S)/);
if (!match) return {
text: line,
size: 0
};
return {
text: line.slice(match[1].length),
size: match[1].length
};
}
function hasClosingFence(lines, startIndex, fence) {
for (let index = startIndex; index < lines.length; index += 1) if (isClosingFence(fence.quoteIndent > 0 ? stripQuotePrefix(lines[index], fence.quoteIndent).text : lines[index], fence)) return true;
return false;
}
function resolveOpeningFence(line) {
const directFence = parseFenceMarker(line);
if (directFence) return {
...directFence,
quoteIndent: 0
};
const quoted = stripQuotePrefix(line);
if (quoted.indent === 0) return null;
const quotedFence = parseFenceMarker(quoted.text);
if (!quotedFence) return null;
return {
...quotedFence,
quoteIndent: quoted.indent
};
}
function stripQuotePrefix(line, maxDepth = Number.POSITIVE_INFINITY) {
let cursor = 0;
while (cursor < line.length && cursor < 3 && line[cursor] === " ") cursor += 1;
let removedDepth = 0;
let consumedCursor = cursor;
while (removedDepth < maxDepth && consumedCursor < line.length && line[consumedCursor] === ">") {
removedDepth += 1;
consumedCursor += 1;
if (line[consumedCursor] === " ") consumedCursor += 1;
}
if (removedDepth === 0) return {
text: line,
indent: 0
};
return {
text: line.slice(consumedCursor),
indent: Math.min(5, removedDepth)
};
}
function parseFenceMarker(line) {
const match = line.match(/^([ ]{0,3})(`{3,}|~{3,})(.*)$/);
if (!match) return null;
const marker = match[2];
const char = marker[0];
if (char !== "`" && char !== "~") return null;
return {
char,
length: marker.length,
indent: match[1].length
};
}
function isClosingFence(line, fence) {
const match = line.match(/^([ ]{0,3})(`{3,}|~{3,})[ \t]*$/);
if (!match) return false;
return match[2][0] === fence.char && match[2].length >= fence.length;
}
function escapeLiteralText(input, escapeMap) {
return input.replace(/[\\*_~{}`]/g, (ch) => {
const index = escapeMap.length;
escapeMap.push(ch);
return `\x01${index}\x02`;
});
}
function parseInlineSegments(text, inheritedStyles = []) {
const segments = [];
let cursor = 0;
while (cursor < text.length) {
const nextMatch = findNextInlineMatch(text, cursor);
if (!nextMatch) {
pushSegment(segments, text.slice(cursor), inheritedStyles);
break;
}
if (nextMatch.match.index > cursor) pushSegment(segments, text.slice(cursor, nextMatch.match.index), inheritedStyles);
const combinedStyles = [...inheritedStyles, ...nextMatch.styles];
if (nextMatch.marker.literal) pushSegment(segments, nextMatch.text, combinedStyles);
else segments.push(...parseInlineSegments(nextMatch.text, combinedStyles));
cursor = nextMatch.match.index + nextMatch.match[0].length;
}
return segments;
}
function findNextInlineMatch(text, startIndex) {
let bestMatch = null;
for (const [priority, marker] of INLINE_MARKERS.entries()) {
const regex = new RegExp(marker.pattern.source, marker.pattern.flags);
regex.lastIndex = startIndex;
const match = regex.exec(text);
if (!match) continue;
if (bestMatch && (match.index > bestMatch.match.index || match.index === bestMatch.match.index && priority > bestMatch.priority)) continue;
bestMatch = {
match,
marker,
text: marker.extractText(match),
styles: marker.resolveStyles?.(match) ?? [],
priority
};
}
return bestMatch;
}
function pushSegment(segments, text, styles) {
if (!text) return;
const lastSegment = segments.at(-1);
if (lastSegment && sameStyles(lastSegment.styles, styles)) {
lastSegment.text += text;
return;
}
segments.push({
text,
styles: [...styles]
});
}
function sameStyles(left, right) {
return left.length === right.length && left.every((style, index) => style === right[index]);
}
function normalizeCodeBlockLeadingWhitespace(line) {
return line.replace(/^[ \t]+/, (leadingWhitespace) => leadingWhitespace.replace(/\t/g, "\xA0\xA0\xA0\xA0").replace(/ /g, "\xA0"));
}
function isIndentedCodeBlockLine(line) {
return /^(?: {4,}|\t)/.test(line);
}
function stripCodeFenceIndent(line, indent) {
let consumed = 0;
let cursor = 0;
while (cursor < line.length && consumed < indent && line[cursor] === " ") {
cursor += 1;
consumed += 1;
}
return line.slice(cursor);
}
//#endregion
//#region extensions/zalouser/src/send.ts
const ZALO_TEXT_LIMIT = 2e3;
const DEFAULT_TEXT_CHUNK_MODE = "length";
async function sendMessageZalouser(threadId, text, options = {}) {
const prepared = options.textMode === "markdown" ? parseZalouserTextStyles(text) : {
text,
styles: options.textStyles
};
const textChunkLimit = options.textChunkLimit ?? ZALO_TEXT_LIMIT;
const chunks = splitStyledText(prepared.text, (prepared.styles?.length ?? 0) > 0 ? prepared.styles : void 0, textChunkLimit, options.textChunkMode);
let lastResult = null;
for (const [index, chunk] of chunks.entries()) {
const chunkOptions = index === 0 ? {
...options,
textStyles: chunk.styles
} : {
...options,
caption: void 0,
mediaLocalRoots: void 0,
mediaUrl: void 0,
textStyles: chunk.styles
};
const result = await sendZaloTextMessage(threadId, chunk.text, chunkOptions);
if (!result.ok) return result;
lastResult = result;
}
return lastResult ?? {
ok: false,
error: "No message content provided",
receipt: createZalouserSendReceipt({
threadId,
kind: "text"
})
};
}
async function sendImageZalouser(threadId, imageUrl, options = {}) {
return await sendMessageZalouser(threadId, options.caption ?? "", {
...options,
caption: void 0,
mediaUrl: imageUrl
});
}
async function sendLinkZalouser(threadId, url, options = {}) {
return await sendZaloLink(threadId, url, options);
}
async function sendTypingZalouser(threadId, options = {}) {
await sendZaloTypingEvent(threadId, options);
}
async function sendReactionZalouser(params) {
const result = await sendZaloReaction({
profile: params.profile,
threadId: params.threadId,
isGroup: params.isGroup,
msgId: params.msgId,
cliMsgId: params.cliMsgId,
emoji: params.emoji,
remove: params.remove
});
return {
ok: result.ok,
error: result.error,
receipt: createZalouserSendReceipt({
threadId: params.threadId,
kind: "unknown"
})
};
}
async function sendDeliveredZalouser(params) {
await sendZaloDeliveredEvent(params);
}
async function sendSeenZalouser(params) {
await sendZaloSeenEvent(params);
}
function splitStyledText(text, styles, limit, mode) {
if (text.length === 0) return [{
text,
styles: void 0
}];
const chunks = [];
for (const range of splitTextRanges(text, limit, mode ?? DEFAULT_TEXT_CHUNK_MODE)) {
const { start, end } = range;
chunks.push({
text: text.slice(start, end),
styles: sliceTextStyles(styles, start, end)
});
}
return chunks;
}
function sliceTextStyles(styles, start, end) {
if (!styles || styles.length === 0) return;
const chunkStyles = styles.map((style) => {
const overlapStart = Math.max(style.start, start);
const overlapEnd = Math.min(style.start + style.len, end);
if (overlapEnd <= overlapStart) return null;
if (style.st === TextStyle.Indent) return {
start: overlapStart - start,
len: overlapEnd - overlapStart,
st: style.st,
indentSize: style.indentSize
};
return {
start: overlapStart - start,
len: overlapEnd - overlapStart,
st: style.st
};
}).filter((style) => style !== null);
return chunkStyles.length > 0 ? chunkStyles : void 0;
}
function splitTextRanges(text, limit, mode) {
if (mode === "newline") return splitTextRangesByPreferredBreaks(text, limit);
const ranges = [];
for (let start = 0; start < text.length; start += limit) ranges.push({
start,
end: Math.min(text.length, start + limit)
});
return ranges;
}
function splitTextRangesByPreferredBreaks(text, limit) {
const ranges = [];
let start = 0;
while (start < text.length) {
const maxEnd = Math.min(text.length, start + limit);
let end = maxEnd;
if (maxEnd < text.length) end = findParagraphBreak(text, start, maxEnd) ?? findLastBreak(text, "\n", start, maxEnd) ?? findLastWhitespaceBreak(text, start, maxEnd) ?? maxEnd;
if (end <= start) end = maxEnd;
ranges.push({
start,
end
});
start = end;
}
return ranges;
}
function findParagraphBreak(text, start, end) {
const matches = text.slice(start, end).matchAll(/\n[\t ]*\n+/g);
let lastMatch;
for (const match of matches) lastMatch = match;
if (!lastMatch || lastMatch.index === void 0) return;
return start + lastMatch.index + lastMatch[0].length;
}
function findLastBreak(text, marker, start, end) {
const index = text.lastIndexOf(marker, end - 1);
if (index < start) return;
return index + marker.length;
}
function findLastWhitespaceBreak(text, start, end) {
for (let index = end - 1; index > start; index -= 1) if (/\s/.test(text[index])) return index + 1;
}
//#endregion
export { sendReactionZalouser as a, sendMessageZalouser as i, sendImageZalouser as n, sendSeenZalouser as o, sendLinkZalouser as r, sendTypingZalouser as s, sendDeliveredZalouser as t };