mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 01:31:35 +00:00
fix(telegram): deliver content instead of throwing when tag overhead fills chunk (#102999)
* fix(telegram): deliver content instead of throwing when tag overhead fills chunk When HTML tag overhead (open + close tags) exceeds the chunk limit, appendText threw an error causing complete message delivery failure. Instead, force the text into the chunk so the message is still delivered even if the chunk slightly exceeds the limit. Closes #102910 * fix(telegram): strip rich formatting when tag overhead fills chunk When HTML tag overhead fills an empty chunk with no room for text, strip rich formatting and retry as plain text instead of delivering an oversized chunk or throwing. Orphan close tags from the stripped formatting are skipped to keep the output well-formed and within the chunk limit. This preserves the existing plain-text fallback paths in send.ts and rich-message.ts. Closes #102910 * fix(telegram): preserve delivery when HTML tag overhead overflows --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -635,8 +635,19 @@ describe("markdownToTelegramHtml", () => {
|
||||
expect(containsLoneSurrogate(output)).toBe(false);
|
||||
});
|
||||
|
||||
it("fails loudly when tag overhead leaves no room for text", () => {
|
||||
expect(() => splitTelegramHtmlChunks("<b><i><u>x</u></i></b>", 10)).toThrow(/tag overhead/i);
|
||||
it("delivers content as plain text when tag overhead fills the chunk", () => {
|
||||
const chunks = splitTelegramHtmlChunks("<b><i><u>x</u></i></b>", 10);
|
||||
expect(chunks).toHaveLength(1);
|
||||
expect(chunks[0]).toBe("x");
|
||||
});
|
||||
|
||||
it("keeps later formatting balanced after dropping an oversized tag scope", () => {
|
||||
const oversizedLink = `<a href="https://example.com/${"x".repeat(40)}">first</a>`;
|
||||
const chunks = splitTelegramHtmlChunks(`${oversizedLink}<b>second</b>`, 20);
|
||||
|
||||
expect(chunks).toEqual(["first<b>second</b>"]);
|
||||
expect(chunks.every((chunk) => chunk.length <= 20)).toBe(true);
|
||||
expect(telegramHtmlToPlainTextFallback(chunks.join(""))).toBe("firstsecond");
|
||||
});
|
||||
|
||||
it("does not split an astral char across the chunk boundary", () => {
|
||||
|
||||
@@ -1496,6 +1496,7 @@ export function splitTelegramHtmlChunks(
|
||||
|
||||
const chunks: string[] = [];
|
||||
const openTags: TelegramHtmlTag[] = [];
|
||||
const suppressedTagNames: string[] = [];
|
||||
let current = "";
|
||||
let currentBlockCount = 0;
|
||||
let currentMediaCount = 0;
|
||||
@@ -1523,9 +1524,13 @@ export function splitTelegramHtmlChunks(
|
||||
normalizedLimit - current.length - buildTelegramHtmlCloseSuffixLength(openTags);
|
||||
if (available <= 0) {
|
||||
if (!chunkHasPayload) {
|
||||
throw new Error(
|
||||
`Telegram HTML chunk limit exceeded by tag overhead (limit=${normalizedLimit})`,
|
||||
);
|
||||
// Preserve the matching closes separately when tag overhead alone
|
||||
// fills a chunk. Dropping only this active scope keeps later tags
|
||||
// balanced while the affected text degrades to plain HTML content.
|
||||
suppressedTagNames.push(...openTags.map((tag) => tag.name));
|
||||
openTags.length = 0;
|
||||
resetCurrent();
|
||||
continue;
|
||||
}
|
||||
flushCurrent();
|
||||
continue;
|
||||
@@ -1590,7 +1595,12 @@ export function splitTelegramHtmlChunks(
|
||||
}
|
||||
}
|
||||
|
||||
current += rawTag;
|
||||
const closesOpenTag = isClosing && openTags.some((tag) => tag.name === tagName);
|
||||
const closesSuppressedTag =
|
||||
isClosing && !closesOpenTag && popLastTagName(suppressedTagNames, tagName);
|
||||
if (!closesSuppressedTag) {
|
||||
current += rawTag;
|
||||
}
|
||||
if (isSelfClosing) {
|
||||
chunkHasPayload = true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
|
||||
import { sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
// Telegram tests cover telegram outbound plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { splitTelegramHtmlChunks } from "./format.js";
|
||||
import { telegramOutbound } from "./outbound-adapter.js";
|
||||
import { clearTelegramRuntime } from "./runtime.js";
|
||||
@@ -56,6 +57,33 @@ describe("telegramPlugin outbound", () => {
|
||||
expect(telegramOutbound.chunker?.(text, 4000)).toEqual([text]);
|
||||
});
|
||||
|
||||
it("delivers bounded HTML through the shared payload path when tag overhead overflows", async () => {
|
||||
clearTelegramRuntime();
|
||||
const oversizedLink = `<a href="https://example.com/${"x".repeat(4_000)}">first</a>`;
|
||||
const text = `${oversizedLink}<b>second</b>`;
|
||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "tg-1", chatId: "12345" });
|
||||
|
||||
await sendTextMediaPayload({
|
||||
channel: "telegram",
|
||||
ctx: {
|
||||
cfg: {},
|
||||
to: "12345",
|
||||
text: "",
|
||||
payload: { text },
|
||||
formatting: { parseMode: "HTML" },
|
||||
deps: { sendTelegram },
|
||||
},
|
||||
adapter: telegramOutbound,
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(1);
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
"12345",
|
||||
"first<b>second</b>",
|
||||
expect.objectContaining({ textMode: "html" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps astral characters whole at positive configured chunk limits", () => {
|
||||
clearTelegramRuntime();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user