= {}): Bot {
...(rich_message.skip_entity_detection === true ? { skip_entity_detection: true } : {}),
...richParams,
};
- const text = rich_message.markdown ?? rich_message.html ?? "";
+ const text = Array.isArray(rich_message.blocks)
+ ? rich_message.blocks
+ .map((block) => {
+ const blockText = (block as { text?: unknown }).text;
+ return typeof blockText === "string" ? blockText : "";
+ })
+ .join("\n")
+ : (rich_message.markdown ?? rich_message.html ?? "");
const replyParameters = sendParams.reply_parameters;
if (
replyParameters &&
@@ -1407,7 +1419,7 @@ describe("deliverReplies", () => {
};
const richMessage = raw.sendRichMessage.mock.calls[0]?.[0]?.rich_message;
expect(richMessage).toEqual({
- html: oauthProfileText,
+ blocks: [{ type: "paragraph", text: oauthProfileText }],
skip_entity_detection: true,
});
});
diff --git a/extensions/telegram/src/draft-stream.test.ts b/extensions/telegram/src/draft-stream.test.ts
index aab94c9a362f..f7842be55daf 100644
--- a/extensions/telegram/src/draft-stream.test.ts
+++ b/extensions/telegram/src/draft-stream.test.ts
@@ -986,16 +986,13 @@ describe("createTelegramDraftStream", () => {
});
});
- it("sends caller-provided rich previews through standard text transport", async () => {
+ it("sends caller-provided HTML previews through standard text transport", async () => {
const api = createMockDraftApi();
const stream = createDraftStream(api);
stream.updatePreview({
- text: "Shelling\n\n`đ ď¸ Exec`",
- richMessage: {
- html: "Shelling\nđ ď¸ Exec",
- skip_entity_detection: true,
- },
+ text: "Shelling\nđ ď¸ Exec",
+ parseMode: "HTML",
});
await stream.flush();
@@ -1005,11 +1002,8 @@ describe("createTelegramDraftStream", () => {
expect(api.raw.sendRichMessage).not.toHaveBeenCalled();
stream.updatePreview({
- text: "Shelling\n\n`đ ď¸ Exec`\n⢠_Checking files_",
- richMessage: {
- html: "Shelling\nđ ď¸ Exec\nChecking files",
- skip_entity_detection: true,
- },
+ text: "Shelling\nđ ď¸ Exec\nChecking files",
+ parseMode: "HTML",
});
await stream.flush();
@@ -1022,16 +1016,13 @@ describe("createTelegramDraftStream", () => {
expect(api.raw.editMessageText).not.toHaveBeenCalled();
});
- it("sends marked progress rich previews through HTML text transport", async () => {
+ it("sends marked progress HTML previews through HTML text transport", async () => {
const api = createMockDraftApi();
const stream = createDraftStream(api);
stream.updatePreview({
- text: "Shelling\n\nđ ď¸ Exec",
- richMessage: {
- html: "Shelling
đ ď¸ Exec",
- skip_entity_detection: true,
- },
+ text: "Shelling\nđ ď¸ Exec",
+ parseMode: "HTML",
});
await stream.flush();
@@ -1041,11 +1032,8 @@ describe("createTelegramDraftStream", () => {
expect(api.raw.sendRichMessage).not.toHaveBeenCalled();
stream.updatePreview({
- text: "Shelling\n\nđ ď¸ Exec\n⢠Checking files",
- richMessage: {
- html: "Shelling
đ ď¸ Exec
Update Checking files",
- skip_entity_detection: true,
- },
+ text: "Shelling\nđ ď¸ Exec\nUpdate Checking files",
+ parseMode: "HTML",
});
await stream.flush();
@@ -1058,7 +1046,7 @@ describe("createTelegramDraftStream", () => {
expect(api.raw.editMessageText).not.toHaveBeenCalled();
});
- it("falls back to plain preview text when rich preview HTML parsing fails", async () => {
+ it("falls back to plain preview text when HTML parsing fails", async () => {
const api = createMockDraftApi();
api.sendMessage
.mockRejectedValueOnce(new Error("can't parse entities: unsupported tag"))
@@ -1066,11 +1054,8 @@ describe("createTelegramDraftStream", () => {
const stream = createDraftStream(api);
stream.updatePreview({
- text: "Shelling <&>\n\nđ ď¸ Exec",
- richMessage: {
- html: "Shelling <&>\nđ ď¸ Exec",
- skip_entity_detection: true,
- },
+ text: "Shelling <&>\nđ ď¸ Exec",
+ parseMode: "HTML",
});
await stream.flush();
@@ -1080,10 +1065,10 @@ describe("createTelegramDraftStream", () => {
"Shelling <&>\nđ ď¸ Exec",
{ parse_mode: "HTML" },
);
- expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "Shelling <&>\n\nđ ď¸ Exec", {});
+ expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "Shelling <&>\nđ ď¸ Exec", {});
expect(stream.currentMessageSnapshot?.()).toEqual({
- text: "Shelling <&>\n\nđ ď¸ Exec",
- sourceText: "Shelling <&>\n\nđ ď¸ Exec",
+ text: "Shelling <&>\nđ ď¸ Exec",
+ sourceText: "Shelling <&>\nđ ď¸ Exec",
sourceTextMode: "html",
});
@@ -1091,8 +1076,8 @@ describe("createTelegramDraftStream", () => {
.mockRejectedValueOnce(new Error("can't parse entities: unsupported tag"))
.mockResolvedValueOnce(true);
stream.updatePreview({
- text: "Done <&>",
- richMessage: { html: "Done <&>" },
+ text: "Done <&>",
+ parseMode: "HTML",
});
await stream.flush();
@@ -1111,37 +1096,29 @@ describe("createTelegramDraftStream", () => {
const api = createMockDraftApi();
const stream = createDraftStream(api, { richMessages: true });
- stream.updatePreview({
- text: "Plan",
- richMessage: { html: "Plan
" },
- });
+ stream.update("## Plan\n\n| A |\n| --- |\n| x |");
await stream.flush();
- expect(api.raw.sendRichMessage).toHaveBeenCalledWith({
- chat_id: 123,
- rich_message: {
- html: "Plan
",
- },
- });
+ expect(api.raw.sendRichMessage).toHaveBeenCalledTimes(1);
+ const first = api.raw.sendRichMessage.mock.calls[0]?.[0] as {
+ rich_message?: TelegramInputRichMessage;
+ };
+ expect(first?.rich_message?.blocks?.some((block) => block.type === "heading")).toBe(true);
+ expect(first?.rich_message?.blocks?.some((block) => block.type === "table")).toBe(true);
expect(api.sendMessage).not.toHaveBeenCalled();
- stream.updatePreview({
- text: "Plan updated",
- richMessage: { html: "Plan updated
" },
- });
+ stream.update("## Plan updated\n\n| B |\n| --- |\n| y |");
await stream.flush();
- expect(api.raw.editMessageText).toHaveBeenCalledWith({
- chat_id: 123,
- message_id: 17,
- rich_message: {
- html: "Plan updated
",
- },
- });
+ expect(api.raw.editMessageText).toHaveBeenCalledTimes(1);
+ const edit = api.raw.editMessageText.mock.calls[0]?.[0] as {
+ rich_message?: TelegramInputRichMessage;
+ };
+ expect(edit?.rich_message?.blocks?.some((block) => block.type === "heading")).toBe(true);
expect(api.editMessageText).not.toHaveBeenCalled();
});
- it("uses table-aware plain text when rich preview fallback sends", async () => {
+ it("uses plain text when rich preview fallback sends", async () => {
const api = createMockDraftApi();
api.raw.sendRichMessage.mockRejectedValueOnce(
new Error("400: Bad Request: RICH_MESSAGE_URL_INVALID"),
@@ -1149,27 +1126,16 @@ describe("createTelegramDraftStream", () => {
const warn = vi.fn();
const stream = createDraftStream(api, { richMessages: true, warn });
- stream.updatePreview({
- text: "Plan",
- richMessage: {
- html: "| Rank | Model | Score |
| 4 | Claude Opus | 78.16% |
",
- },
- });
+ stream.update("| Rank | Model |\n| --- | --- |\n| 4 | Claude Opus |");
await stream.flush();
- expect(api.sendMessage).toHaveBeenCalledWith(
- 123,
- "Rank | Model | Score\n4 | Claude Opus | 78.16%",
- {},
- );
+ expect(api.sendMessage).toHaveBeenCalledTimes(1);
+ const plain = api.sendMessage.mock.calls[0]?.[1] ?? "";
+ expect(plain).toContain("Rank");
+ expect(plain).toContain("Claude Opus");
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("rich-degrade=plain-fallback:rich-entity-invalid"),
);
- expect(stream.currentMessageSnapshot?.()).toEqual({
- text: "Rank | Model | Score\n4 | Claude Opus | 78.16%",
- sourceText: "Rank | Model | Score\n4 | Claude Opus | 78.16%",
- sourceTextMode: "html",
- });
});
it("skips rich entity detection for draft text with provider-prefixed email addresses", async () => {
@@ -1184,19 +1150,19 @@ describe("createTelegramDraftStream", () => {
expect(api.raw.sendRichMessage).toHaveBeenCalledWith({
chat_id: 123,
rich_message: {
- html: oauthProfileText,
+ blocks: [{ type: "paragraph", text: oauthProfileText }],
skip_entity_detection: true,
},
});
});
- it("keeps rich preview html out of plain preview gating", async () => {
+ it("keeps short rich previews out of plain preview gating", async () => {
const api = createMockDraftApi();
const stream = createDraftStream(api, { richMessages: true, minInitialChars: 10 });
stream.updatePreview({
text: "Plan",
- richMessage: { html: "Plan
" },
+ richMessage: { blocks: [{ type: "heading", text: "Plan", size: 2 }] },
});
await stream.flush();
@@ -1215,8 +1181,13 @@ describe("createTelegramDraftStream", () => {
const calls = api.raw.sendRichMessage.mock.calls as unknown[][];
const params = calls[0]?.[0] as { rich_message?: TelegramInputRichMessage } | undefined;
const richMessage = params?.rich_message;
- expect(richMessage?.html).toContain("paragraph 499");
- expect(richMessage?.html).not.toContain("paragraph 500");
+ const plain = (richMessage?.blocks ?? [])
+ .map((block) =>
+ block.type === "paragraph" && typeof block.text === "string" ? block.text : "",
+ )
+ .join("\n");
+ expect(plain).toContain("paragraph 499");
+ expect(plain).not.toContain("paragraph 500");
});
it("clamps rendered previews to the text-message limit", async () => {
@@ -1364,7 +1335,9 @@ describe("createTelegramDraftStream", () => {
"```",
].join("\n");
const stream = createDraftStream(api, {
- maxChars: 55,
+ // Plain code body is shorter than HTML-wrapped rich text; keep the limit
+ // under the pre body so pagination still splits across messages.
+ maxChars: 30,
richMessages: true,
onRetainedPage: onSupersededPreview,
});
@@ -1374,18 +1347,22 @@ describe("createTelegramDraftStream", () => {
const pages = api.raw.sendRichMessage.mock.calls.map((call) => {
const params = call[0] as { rich_message?: TelegramInputRichMessage };
- return params.rich_message?.html ?? "";
+ return params.rich_message?.blocks ?? [];
});
expect(pages.length).toBeGreaterThan(1);
+ expect(pages.every((blocks) => blocks.every((block) => block.type === "pre"))).toBe(true);
expect(
- pages.every((page) => /^[\s\S]*<\/code><\/pre>$/u.test(page)),
+ pages.every((blocks) =>
+ blocks.some((block) => block.type === "pre" && block.language === "ts"),
+ ),
).toBe(true);
const fullRichMessage = buildTelegramRichMarkdown(text);
- if (!fullRichMessage.html) {
- throw new Error("expected rendered Telegram rich HTML");
- }
- expect(pages.map(telegramHtmlToPlainTextFallback).join("")).toBe(
- telegramHtmlToPlainTextFallback(fullRichMessage.html),
+ expect(
+ pages
+ .flatMap((blocks) => blocks.map((block) => (block.type === "pre" ? block.text : "")))
+ .join(""),
+ ).toBe(
+ fullRichMessage.blocks.map((block) => (block.type === "pre" ? block.text : "")).join(""),
);
expect(onSupersededPreview).toHaveBeenCalledTimes(pages.length - 1);
});
@@ -1400,13 +1377,16 @@ describe("createTelegramDraftStream", () => {
const pages = api.raw.sendRichMessage.mock.calls.map((call) => {
const params = call[0] as { rich_message?: TelegramInputRichMessage };
- return params.rich_message?.html ?? "";
+ return params.rich_message?.blocks ?? [];
});
expect(pages.length).toBeGreaterThan(1);
- expect(pages.every((page) => /^[\s\S]*<\/code><\/pre>$/u.test(page))).toBe(true);
- expect(pages.map(telegramHtmlToPlainTextFallback).join("").replace(/\n$/u, "")).toBe(
- " ".repeat(80),
- );
+ expect(pages.every((blocks) => blocks.every((block) => block.type === "pre"))).toBe(true);
+ expect(
+ pages
+ .flatMap((blocks) => blocks.map((block) => (block.type === "pre" ? block.text : "")))
+ .join("")
+ .replace(/\n$/u, ""),
+ ).toBe(" ".repeat(80));
});
it("keeps non-final overflow in one editable preview", async () => {
diff --git a/extensions/telegram/src/draft-stream.ts b/extensions/telegram/src/draft-stream.ts
index ca4f609e3268..91e3cd95ab3c 100644
--- a/extensions/telegram/src/draft-stream.ts
+++ b/extensions/telegram/src/draft-stream.ts
@@ -11,10 +11,8 @@ import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helper
import {
escapeTelegramHtml,
markdownToTelegramChunks,
- renderTelegramHtmlText,
splitTelegramHtmlChunks,
telegramHtmlToPlainTextFallback,
- type TelegramRichHtmlDegradationReason,
} from "./format.js";
import {
isRecoverableTelegramNetworkError,
@@ -27,11 +25,14 @@ import {
import { TELEGRAM_TEXT_CHUNK_LIMIT } from "./outbound-adapter.js";
import { normalizeTelegramReplyToMessageId } from "./outbound-params.js";
import {
- buildTelegramRichHtmlPlan,
+ inputRichBlocksToPlainText,
+ splitTelegramRichBlocks,
+ type TelegramRichBlocksDegradationReason,
+} from "./rich-blocks.js";
+import {
+ buildTelegramRichBlocksPlan,
buildTelegramRichMarkdownPlan,
getTelegramRichRawApi,
- splitTelegramRichMarkdownChunks,
- splitTelegramRichMessageTextChunks,
TELEGRAM_RICH_TEXT_LIMIT,
type TelegramInputRichMessage,
} from "./rich-message.js";
@@ -39,7 +40,7 @@ import {
buildTelegramPlainFallbackPlan,
isTelegramHtmlParseError,
splitTelegramPlainTextChunks,
- warnTelegramRichHtmlDegradations,
+ warnTelegramRichBlocksDegradations,
} from "./rich-plain-fallback.js";
const DEFAULT_THROTTLE_MS = 1000;
@@ -112,8 +113,8 @@ export type TelegramDraftPreview = {
type PlannedTelegramDraftPage = TelegramDraftMessageSnapshot & {
sourceTextMode: "html" | "markdown";
fullSourceText?: string;
- richMessage?: Extract;
- degradationReasons?: readonly TelegramRichHtmlDegradationReason[];
+ richMessage?: TelegramInputRichMessage;
+ degradationReasons?: readonly TelegramRichBlocksDegradationReason[];
};
type RetainedTelegramDraftPage = {
@@ -131,49 +132,61 @@ function telegramRichHtmlToParseModeHtml(html: string): string {
return html.replace(/
/giu, "\n");
}
-function buildTelegramDraftRichPlan(preview: TelegramDraftPreview) {
- const options = preview.richMessage
- ? { skipEntityDetection: preview.richMessage.skip_entity_detection === true }
- : undefined;
- if (preview.richMessage?.html !== undefined) {
- return buildTelegramRichHtmlPlan(preview.richMessage.html, options);
- }
- return buildTelegramRichMarkdownPlan(preview.richMessage?.markdown ?? preview.text, options);
-}
-
function planTelegramDraftPages(
preview: TelegramDraftPreview,
maxChars: number,
richMessages: boolean,
): PlannedTelegramDraftPage[] {
if (richMessages) {
- const previews = preview.richMessage
- ? [preview]
- : splitTelegramRichMarkdownChunks(preview.text, Number.MAX_SAFE_INTEGER, "length").map(
- (text) => ({ text }),
- );
- const pages: PlannedTelegramDraftPage[] = [];
- for (const richPreview of previews) {
- const plan = buildTelegramDraftRichPlan(richPreview);
- const planPages: PlannedTelegramDraftPage[] = splitTelegramRichMessageTextChunks({
- text: plan.richMessage.html,
+ const previewRich = preview.richMessage;
+ if (previewRich) {
+ const skipEntityDetection = previewRich.skip_entity_detection === true;
+ return splitTelegramRichBlocks(previewRich.blocks, {
textLimit: maxChars,
- textMode: "html",
- chunkMode: "length",
- skipEntityDetection: plan.richMessage.skip_entity_detection === true,
- }).map((page) => ({
+ }).map((blocks) => {
+ const plainText = inputRichBlocksToPlainText(blocks);
+ return {
+ text: plainText,
+ sourceText: plainText,
+ sourceTextMode: "markdown" as const,
+ richMessage: {
+ blocks,
+ ...(skipEntityDetection ? { skip_entity_detection: true } : {}),
+ },
+ };
+ });
+ }
+ const plan = buildTelegramRichMarkdownPlan(preview.text);
+ // Every page carries the plan's document-level skip flag: the render already
+ // committed to that linkify decision, so per-page re-derivation would leave
+ // unprotected file refs in pages without the skip trigger.
+ const planSkip = plan.richMessage.skip_entity_detection === true;
+ const pages = splitTelegramRichBlocks(plan.richMessage.blocks, {
+ textLimit: maxChars,
+ }).map((blocks, index) => {
+ const page = buildTelegramRichBlocksPlan(blocks, { skipEntityDetection: planSkip });
+ const planned: PlannedTelegramDraftPage = {
text: page.plainText,
- sourceText: page.text,
- sourceTextMode: page.textMode,
- richMessage: {
- html: page.text,
- ...(page.skipEntityDetection ? { skip_entity_detection: true } : {}),
- },
- }));
- if (planPages[0] && plan.degradationReasons.length > 0) {
- planPages[0].degradationReasons = plan.degradationReasons;
+ sourceText: page.plainText,
+ sourceTextMode: "markdown",
+ richMessage: page.richMessage,
+ };
+ if (index === 0 && plan.degradationReasons.length > 0) {
+ planned.degradationReasons = plan.degradationReasons;
}
- pages.push(...planPages);
+ return planned;
+ });
+ if (pages.length === 0 && preview.text.trim()) {
+ // Mirror the durable funnel: markdown that projects to zero blocks
+ // (link definitions only) still previews as readable source text.
+ return [
+ {
+ text: preview.text,
+ sourceText: preview.text,
+ sourceTextMode: "markdown",
+ richMessage: { blocks: [{ type: "paragraph", text: preview.text }] },
+ },
+ ];
}
return pages;
}
@@ -188,13 +201,10 @@ function planTelegramDraftPages(
sourceTextMode: "html",
}));
}
- const htmlText = preview.richMessage?.html
- ? telegramRichHtmlToParseModeHtml(preview.richMessage.html)
- : preview.richMessage?.markdown
- ? renderTelegramHtmlText(preview.richMessage.markdown)
- : preview.parseMode === "HTML"
- ? telegramRichHtmlToParseModeHtml(preview.text)
- : undefined;
+ // Non-rich path: progress drafts may still pass parseMode HTML text.
+ // Blocks-only richMessage is ignored here â richMessages must be enabled.
+ const htmlText =
+ preview.parseMode === "HTML" ? telegramRichHtmlToParseModeHtml(preview.text) : undefined;
if (htmlText === undefined) {
return splitTelegramPlainTextChunks(preview.text, maxChars)
.map((chunk, index) => (index === 0 ? chunk.trimEnd() : chunk.trim()))
@@ -205,9 +215,7 @@ function planTelegramDraftPages(
sourceTextMode: "markdown",
}));
}
- const plainText = preview.richMessage
- ? preview.text
- : telegramHtmlToPlainTextFallback(preview.text);
+ const plainText = telegramHtmlToPlainTextFallback(preview.text);
const htmlPages = splitTelegramHtmlChunks(htmlText, maxChars);
return htmlPages.map((sourceText) => ({
text: htmlPages.length === 1 ? plainText : telegramHtmlToPlainTextFallback(sourceText),
@@ -309,7 +317,7 @@ export function createTelegramDraftStream(params: {
sendMessageParams: ReturnType,
) => {
if (page.richMessage) {
- warnTelegramRichHtmlDegradations({
+ warnTelegramRichBlocksDegradations({
context: "stream preview",
reasons: page.degradationReasons ?? [],
warn: (message) => params.warn?.(message),
@@ -325,7 +333,7 @@ export function createTelegramDraftStream(params: {
};
} catch (err) {
const fallbackPlan = buildTelegramPlainFallbackPlan({
- html: page.richMessage.html,
+ plainText: page.text,
err,
context: "stream preview",
warn: (message) => params.warn?.(message),
@@ -372,7 +380,7 @@ export function createTelegramDraftStream(params: {
streamVisibleSinceMs ??= Date.now();
let acceptedSnapshot: TelegramDraftMessageSnapshot = page;
if (page.richMessage) {
- warnTelegramRichHtmlDegradations({
+ warnTelegramRichBlocksDegradations({
context: "stream preview edit",
reasons: page.degradationReasons ?? [],
warn: (message) => params.warn?.(message),
@@ -385,7 +393,7 @@ export function createTelegramDraftStream(params: {
});
} catch (err) {
const fallbackPlan = buildTelegramPlainFallbackPlan({
- html: page.richMessage.html,
+ plainText: page.text,
err,
context: "stream preview edit",
warn: (message) => params.warn?.(message),
diff --git a/extensions/telegram/src/format-html.ts b/extensions/telegram/src/format-html.ts
index a8abb79badaf..a4764c2906ae 100644
--- a/extensions/telegram/src/format-html.ts
+++ b/extensions/telegram/src/format-html.ts
@@ -1,9 +1,16 @@
const TELEGRAM_HTML_ENTITY_PATTERN = /&(#[xX][0-9A-Fa-f]+|#\d+|amp|lt|gt|quot|apos);/g;
-const TELEGRAM_RICH_BLOCK_HTML_TAGS = new Set([
+
+// Structural tags that force a line boundary when projecting HTML to plain text
+// (assistant transcript protection). Block-counting helpers for rich HTML are gone.
+const TELEGRAM_LINE_BREAK_STRUCTURAL_TAGS = new Set([
"aside",
"audio",
"blockquote",
+ "caption",
+ "col",
+ "colgroup",
"details",
+ "figcaption",
"figure",
"footer",
"h1",
@@ -18,7 +25,13 @@ const TELEGRAM_RICH_BLOCK_HTML_TAGS = new Set([
"ol",
"p",
"pre",
+ "summary",
"table",
+ "tbody",
+ "td",
+ "tfoot",
+ "th",
+ "thead",
"tg-collage",
"tg-map",
"tg-math-block",
@@ -28,31 +41,11 @@ const TELEGRAM_RICH_BLOCK_HTML_TAGS = new Set([
"video",
]);
-// Includes table/figure/details children omitted from the block-counting set.
-const TELEGRAM_RICH_LINE_BREAK_STRUCTURAL_TAGS: ReadonlySet = new Set([
- ...TELEGRAM_RICH_BLOCK_HTML_TAGS,
- "caption",
- "col",
- "colgroup",
- "figcaption",
- "summary",
- "tbody",
- "td",
- "tfoot",
- "th",
- "thead",
-]);
-
-function isNamedAnchor(rawTag: string, tagName: string): boolean {
- return tagName === "a" && /\sname="[^"]+"/i.test(rawTag);
-}
-
-export function isTelegramRichBlockHtmlTag(rawTag: string, tagName: string): boolean {
- return TELEGRAM_RICH_BLOCK_HTML_TAGS.has(tagName) || isNamedAnchor(rawTag, tagName);
-}
-
export function isTelegramRichLineBreakStructuralTag(rawTag: string, tagName: string): boolean {
- return TELEGRAM_RICH_LINE_BREAK_STRUCTURAL_TAGS.has(tagName) || isNamedAnchor(rawTag, tagName);
+ return (
+ TELEGRAM_LINE_BREAK_STRUCTURAL_TAGS.has(tagName) ||
+ (tagName === "a" && /\sname="[^"]+"/i.test(rawTag))
+ );
}
function isValidTelegramHtmlEntityCodePoint(codePoint: number): boolean {
diff --git a/extensions/telegram/src/format.ts b/extensions/telegram/src/format.ts
index c39293f3de89..d036d9c6c806 100644
--- a/extensions/telegram/src/format.ts
+++ b/extensions/telegram/src/format.ts
@@ -1,30 +1,20 @@
import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-contracts";
// Telegram helper module supports format behavior.
-import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
FILE_REF_EXTENSIONS_WITH_TLD,
isAutoLinkedFileRef,
markdownToIR,
- markdownToIRWithMeta,
type MarkdownLinkSpan,
type MarkdownIR,
- type MarkdownTableCell,
- type MarkdownTableMeta,
renderMarkdownIRChunksWithinLimit,
- sliceMarkdownIR,
tokenizeHtmlTags,
} from "openclaw/plugin-sdk/text-chunking";
import {
protectTelegramAssistantTranscriptRoleHeaders,
TELEGRAM_ASSISTANT_TRANSCRIPT_PREFIX,
} from "./format-assistant-transcript.js";
-import {
- decodeTelegramHtmlEntities,
- findTelegramHtmlEntityEnd,
- isTelegramRichBlockHtmlTag,
- isTelegramRichLineBreakStructuralTag,
-} from "./format-html.js";
+import { decodeTelegramHtmlEntities, findTelegramHtmlEntityEnd } from "./format-html.js";
import { renderTelegramMarkdownIR } from "./format-render.js";
export type TelegramFormattedChunk = {
@@ -32,15 +22,6 @@ export type TelegramFormattedChunk = {
text: string;
};
-const TELEGRAM_RICH_NESTING_LIMIT = 16;
-
-export type TelegramRichHtmlDegradationReason = "table-ascii";
-
-type TelegramOutboundRichHtmlNormalization = {
- html: string;
- degradationReasons: readonly TelegramRichHtmlDegradationReason[];
-};
-
export function escapeTelegramHtml(text: string): string {
return text.replace(/&/g, "&").replace(//g, ">");
}
@@ -204,23 +185,11 @@ const TELEGRAM_HTML_ANCHOR_PATTERN =
/]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a\s*>/gi;
const TELEGRAM_HTML_BREAK_PATTERN = /
/gi;
const TELEGRAM_HTML_TAG_PATTERN = /<[^>]*>/g;
-const TELEGRAM_RICH_MEDIA_BLOCK_PATTERN =
- /[^\S\r\n]*(?:]*>[\s\S]*?<\/figure>|]*>[\s\S]*?<\/tg-collage>|]*>[\s\S]*?<\/tg-slideshow>|
]*\bsrc="https?:\/\/[^"]+"[^>]*\/?>|