fix(telegram): keep streamed final pages on word boundaries (#104608)

* fix(telegram): align draft final pagination

* chore: drop release-owned changelog entry

* style(telegram): format pagination parity test
This commit is contained in:
Peter Steinberger
2026-07-11 11:44:09 -07:00
committed by GitHub
parent 473df17bd7
commit fe3884a7ad
5 changed files with 58 additions and 5 deletions

File diff suppressed because one or more lines are too long

View File

@@ -726,6 +726,10 @@ describe("dispatchTelegramMessage draft streaming", () => {
expect(renderText?.("# Heading")).toEqual({
text: "Heading",
parseMode: "HTML",
markdownSource: {
text: "# Heading",
tableMode: "preserve",
},
});
});

View File

@@ -948,6 +948,7 @@ export const dispatchTelegramMessage = async ({
: {
text: renderTelegramHtmlText(text, { tableMode }),
parseMode: "HTML",
markdownSource: { text, tableMode },
};
const accountBlockStreamingEnabled =
resolveChannelStreamingBlockEnabled(telegramCfg) ??

View File

@@ -2,7 +2,11 @@
import type { Bot } from "grammy";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTelegramDraftStream } from "./draft-stream.js";
import { renderTelegramHtmlText, telegramHtmlToPlainTextFallback } from "./format.js";
import {
markdownToTelegramChunks,
renderTelegramHtmlText,
telegramHtmlToPlainTextFallback,
} from "./format.js";
import { buildTelegramRichMarkdown, type TelegramInputRichMessage } from "./rich-message.js";
type TelegramDraftStreamParams = Parameters<typeof createTelegramDraftStream>[0];
@@ -1278,6 +1282,34 @@ describe("createTelegramDraftStream", () => {
});
});
it("shares durable Markdown chunk boundaries with final draft pagination", async () => {
const api = createMockDraftApi();
const text = Array.from(
{ length: 12 },
(_, index) =>
`**${String(index).padStart(2, "0")}** overflow pagination line with filler text`,
).join("\n");
const maxChars = 80;
const expectedChunks = markdownToTelegramChunks(text, maxChars);
const stream = createDraftStream(api, {
maxChars,
renderText: (value) => ({
text: renderTelegramHtmlText(value),
parseMode: "HTML",
markdownSource: { text: value },
}),
});
stream.update(text);
await stream.stop();
const pages = api.sendMessage.mock.calls.map((call) => call[1]);
expect(pages).toEqual(expectedChunks.map((chunk) => chunk.html));
expect(pages.map(telegramHtmlToPlainTextFallback)).toEqual(
expectedChunks.map((chunk) => chunk.text),
);
});
it("paginates one rendered rich-code plan without reparsing Markdown tails", async () => {
const api = createMockDraftApi();
const onSupersededPreview = vi.fn();

View File

@@ -4,12 +4,13 @@ import {
createFinalizableDraftStreamControlsForState,
takeMessageIdAfterStop,
} from "openclaw/plugin-sdk/channel-outbound";
import type { ReplyToMode } from "openclaw/plugin-sdk/config-contracts";
import type { MarkdownTableMode, ReplyToMode } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { isSingleUseReplyToMode } from "openclaw/plugin-sdk/reply-reference";
import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helpers.js";
import {
escapeTelegramHtml,
markdownToTelegramChunks,
renderTelegramHtmlText,
splitTelegramHtmlChunks,
telegramHtmlToPlainTextFallback,
@@ -102,6 +103,10 @@ export type TelegramDraftPreview = {
text: string;
parseMode?: "HTML";
richMessage?: TelegramInputRichMessage;
markdownSource?: {
text: string;
tableMode?: MarkdownTableMode;
};
};
type PlannedTelegramDraftPage = TelegramDraftMessageSnapshot & {
@@ -172,6 +177,17 @@ function planTelegramDraftPages(
}
return pages;
}
if (preview.markdownSource) {
// Keep streaming-final pagination on the durable send funnel's chunker;
// splitting pre-rendered HTML loses Markdown word and block boundaries.
return markdownToTelegramChunks(preview.markdownSource.text, maxChars, {
tableMode: preview.markdownSource.tableMode,
}).map((chunk) => ({
text: chunk.text,
sourceText: chunk.html,
sourceTextMode: "html",
}));
}
const htmlText = preview.richMessage?.html
? telegramRichHtmlToParseModeHtml(preview.richMessage.html)
: preview.richMessage?.markdown