mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:24:33 +00:00
fix(telegram): fallback to plain text when threaded markdown renders empty
Minimal fix path for Telegram empty-text failures in threaded replies. - fallback to plain text when formatted htmlText is empty - retry plain text on parse/empty-text API errors - add focused regression test for threaded mode case Related: #25091 Supersedes alternative fix path in #17629 if maintainers prefer minimal scope.
This commit is contained in:
@@ -244,6 +244,40 @@ describe("deliverReplies", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to plain text when markdown renders to empty HTML in threaded mode", async () => {
|
||||||
|
const runtime = { error: vi.fn(), log: vi.fn() };
|
||||||
|
const sendMessage = vi.fn(async (_chatId: string, text: string) => {
|
||||||
|
if (text === "") {
|
||||||
|
throw new Error("400: Bad Request: message text is empty");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message_id: 6,
|
||||||
|
chat: { id: "123" },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const bot = { api: { sendMessage } } as unknown as Bot;
|
||||||
|
|
||||||
|
await deliverReplies({
|
||||||
|
replies: [{ text: ">" }],
|
||||||
|
chatId: "123",
|
||||||
|
token: "tok",
|
||||||
|
runtime,
|
||||||
|
bot,
|
||||||
|
replyToMode: "off",
|
||||||
|
textLimit: 4000,
|
||||||
|
thread: { id: 42, scope: "forum" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendMessage).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
">",
|
||||||
|
expect.objectContaining({
|
||||||
|
message_thread_id: 42,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses reply_to_message_id when quote text is provided", async () => {
|
it("uses reply_to_message_id when quote text is provided", async () => {
|
||||||
const runtime = createRuntime();
|
const runtime = createRuntime();
|
||||||
const sendMessage = vi.fn().mockResolvedValue({
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const TELEGRAM_MEDIA_SSRF_POLICY = {
|
|||||||
allowedHostnames: ["api.telegram.org"],
|
allowedHostnames: ["api.telegram.org"],
|
||||||
allowRfc2544BenchmarkRange: true,
|
allowRfc2544BenchmarkRange: true,
|
||||||
};
|
};
|
||||||
|
const EMPTY_TEXT_ERR_RE = /message text is empty/i;
|
||||||
|
|
||||||
export async function deliverReplies(params: {
|
export async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
@@ -553,6 +554,30 @@ async function sendTelegramText(
|
|||||||
const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
|
const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
|
||||||
const textMode = opts?.textMode ?? "markdown";
|
const textMode = opts?.textMode ?? "markdown";
|
||||||
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
||||||
|
const fallbackText = opts?.plainText ?? text;
|
||||||
|
const hasFallbackText = fallbackText.trim().length > 0;
|
||||||
|
const sendPlainFallback = async () => {
|
||||||
|
if (!hasFallbackText) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const res = await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
runtime,
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(chatId, fallbackText, {
|
||||||
|
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
||||||
|
...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}),
|
||||||
|
...baseParams,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return res.message_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown can occasionally render to empty HTML (for example syntax-only chunks).
|
||||||
|
// Telegram rejects those sends, so fall back to plain text early.
|
||||||
|
if (!htmlText.trim()) {
|
||||||
|
return await sendPlainFallback();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await withTelegramApiErrorLogging({
|
const res = await withTelegramApiErrorLogging({
|
||||||
operation: "sendMessage",
|
operation: "sendMessage",
|
||||||
@@ -570,21 +595,9 @@ async function sendTelegramText(
|
|||||||
return res.message_id;
|
return res.message_id;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errText = formatErrorMessage(err);
|
const errText = formatErrorMessage(err);
|
||||||
if (PARSE_ERR_RE.test(errText)) {
|
if (PARSE_ERR_RE.test(errText) || EMPTY_TEXT_ERR_RE.test(errText)) {
|
||||||
runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`);
|
runtime.log?.(`telegram formatted send failed; retrying without formatting: ${errText}`);
|
||||||
const fallbackText = opts?.plainText ?? text;
|
return await sendPlainFallback();
|
||||||
const res = await withTelegramApiErrorLogging({
|
|
||||||
operation: "sendMessage",
|
|
||||||
runtime,
|
|
||||||
fn: () =>
|
|
||||||
bot.api.sendMessage(chatId, fallbackText, {
|
|
||||||
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
|
||||||
...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}),
|
|
||||||
...baseParams,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
runtime.log?.(`telegram sendMessage ok chat=${chatId} message=${res.message_id} (plain)`);
|
|
||||||
return res.message_id;
|
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user