mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix: chunk telegram markdown sends
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Config: log the "newer OpenClaw" version warning once per process instead of once per config snapshot read. (#75927) Thanks @romneyda.
|
||||
- Telegram/message actions: treat benign delete-message 400s as no-op warnings instead of runtime errors, so stale or already-removed messages do not create noisy delete failures. Fixes #73726. Thanks @Avicennasis.
|
||||
- Telegram: split long default markdown sends and media follow-up text into safe HTML chunks, so outbound messages over Telegram's limit no longer fail as one oversized Bot API request. Fixes #75868. Thanks @zhengsx.
|
||||
- Gateway/chat history: merge Claude CLI transcript imports for Anthropic-routed sessions that still have a Claude CLI binding, so local chat history does not hide CLI JSONL turns. Fixes #75850. Thanks @alfredjbclaw.
|
||||
- Media: trim serialized JSON suffixes after local `MEDIA:` directive file extensions, so generated-image metadata cannot pollute the parsed media path and cause false `ENOENT` delivery failures. Fixes #75182. Thanks @TnzGit and @hclsys.
|
||||
- Cron: make scheduler reload schedule comparison tolerate malformed persisted jobs, so one bad cron entry no longer aborts the whole tick. Fixes #75886. Thanks @samfox-ai.
|
||||
|
||||
@@ -843,6 +843,46 @@ describe("sendMessageTelegram", () => {
|
||||
expect(res.messageId).toBe("71");
|
||||
});
|
||||
|
||||
it("chunks long default markdown media follow-up text", async () => {
|
||||
const chatId = "123";
|
||||
const longText = `**${"A".repeat(5000)}**`;
|
||||
|
||||
const sendPhoto = vi.fn().mockResolvedValue({
|
||||
message_id: 72,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const sendMessage = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ message_id: 73, chat: { id: chatId } })
|
||||
.mockResolvedValueOnce({ message_id: 74, chat: { id: chatId } });
|
||||
const api = { sendPhoto, sendMessage } as unknown as {
|
||||
sendPhoto: typeof sendPhoto;
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
mockLoadedMedia({
|
||||
buffer: Buffer.from("fake-image"),
|
||||
contentType: "image/jpeg",
|
||||
fileName: "photo.jpg",
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, longText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
});
|
||||
|
||||
expect(sendPhoto).toHaveBeenCalledWith(chatId, expect.anything(), {
|
||||
caption: undefined,
|
||||
});
|
||||
expect(sendMessage).toHaveBeenCalledTimes(2);
|
||||
expect(sendMessage.mock.calls.every((call) => call[2]?.parse_mode === "HTML")).toBe(true);
|
||||
expect(sendMessage.mock.calls.every((call) => String(call[1] ?? "").length <= 4000)).toBe(true);
|
||||
expect(sendMessage.mock.calls.map((call) => String(call[1] ?? "")).join("")).toContain("<b>");
|
||||
expect(res.messageId).toBe("74");
|
||||
});
|
||||
|
||||
it("uses caption when text is within 1024 char limit", async () => {
|
||||
const chatId = "123";
|
||||
const shortText = "B".repeat(1024);
|
||||
@@ -1898,6 +1938,41 @@ describe("sendMessageTelegram", () => {
|
||||
expect(res.messageId).toBe("91");
|
||||
});
|
||||
|
||||
it("chunks long default markdown text and keeps buttons on the last chunk only", async () => {
|
||||
const chatId = "123";
|
||||
const markdownText = `**${"A".repeat(5000)}**`;
|
||||
|
||||
const sendMessage = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ message_id: 90, chat: { id: chatId } })
|
||||
.mockResolvedValueOnce({ message_id: 91, chat: { id: chatId } });
|
||||
const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };
|
||||
|
||||
const res = await sendMessageTelegram(chatId, markdownText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
buttons: [[{ text: "OK", callback_data: "ok" }]],
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(2);
|
||||
const firstCall = sendMessage.mock.calls[0];
|
||||
const secondCall = sendMessage.mock.calls[1];
|
||||
expect(firstCall).toBeDefined();
|
||||
expect(secondCall).toBeDefined();
|
||||
expect(String(firstCall[1] ?? "").length).toBeLessThanOrEqual(4000);
|
||||
expect(String(secondCall[1] ?? "").length).toBeLessThanOrEqual(4000);
|
||||
expect(firstCall[2]?.parse_mode).toBe("HTML");
|
||||
expect(secondCall[2]?.parse_mode).toBe("HTML");
|
||||
expect(String(firstCall[1] ?? "")).toMatch(/^<b>[\s\S]*<\/b>$/);
|
||||
expect(String(secondCall[1] ?? "")).toMatch(/^<b>[\s\S]*<\/b>$/);
|
||||
expect(firstCall[2]?.reply_markup).toBeUndefined();
|
||||
expect(secondCall[2]?.reply_markup).toEqual({
|
||||
inline_keyboard: [[{ text: "OK", callback_data: "ok" }]],
|
||||
});
|
||||
expect(res.messageId).toBe("91");
|
||||
});
|
||||
|
||||
it("preserves caller plain-text fallback across chunked html parse retries", async () => {
|
||||
const chatId = "123";
|
||||
const htmlText = `<b>${"A".repeat(5000)}</b>`;
|
||||
|
||||
@@ -720,10 +720,11 @@ export async function sendMessageTelegram(
|
||||
};
|
||||
|
||||
const buildChunkedTextPlan = (rawText: string, context: string): TelegramTextChunk[] => {
|
||||
const htmlText = renderHtmlText(rawText);
|
||||
const fallbackText = opts.plainText ?? rawText;
|
||||
let htmlChunks: string[];
|
||||
try {
|
||||
htmlChunks = splitTelegramHtmlChunks(rawText, 4000);
|
||||
htmlChunks = splitTelegramHtmlChunks(htmlText, 4000);
|
||||
} catch (error) {
|
||||
logVerbose(
|
||||
`telegram ${context} failed HTML chunk planning, retrying as plain text: ${formatErrorMessage(
|
||||
@@ -951,14 +952,7 @@ export async function sendMessageTelegram(
|
||||
// If text was too long for a caption, send it as a separate follow-up message.
|
||||
// Use HTML conversion so markdown renders like captions.
|
||||
if (needsSeparateText && followUpText) {
|
||||
if (textMode === "html") {
|
||||
const textResult = await sendChunkedText(followUpText, "text follow-up send");
|
||||
return { messageId: textResult.messageId, chatId: resolvedChatId };
|
||||
}
|
||||
const textResult = await sendTelegramTextChunks(
|
||||
[{ plainText: followUpText, htmlText: renderHtmlText(followUpText) }],
|
||||
"text follow-up send",
|
||||
);
|
||||
const textResult = await sendChunkedText(followUpText, "text follow-up send");
|
||||
return { messageId: textResult.messageId, chatId: resolvedChatId };
|
||||
}
|
||||
|
||||
@@ -968,15 +962,7 @@ export async function sendMessageTelegram(
|
||||
if (!text || !text.trim()) {
|
||||
throw new Error("Message must be non-empty for Telegram sends");
|
||||
}
|
||||
let textResult: { messageId: string; chatId: string };
|
||||
if (textMode === "html") {
|
||||
textResult = await sendChunkedText(text, "text send");
|
||||
} else {
|
||||
textResult = await sendTelegramTextChunks(
|
||||
[{ plainText: opts.plainText ?? text, htmlText: renderHtmlText(text) }],
|
||||
"text send",
|
||||
);
|
||||
}
|
||||
const textResult = await sendChunkedText(text, "text send");
|
||||
recordChannelActivity({
|
||||
channel: "telegram",
|
||||
accountId: account.accountId,
|
||||
|
||||
Reference in New Issue
Block a user