[codex] Fix Telegram rich local Markdown link hrefs (#94118)

Merged via squash.

Prepared head SHA: 42c738978f
Co-authored-by: dankarization <26330178+dankarization@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Daniil Karpenko
2026-06-18 21:15:39 +04:00
committed by GitHub
parent 3d6840bc59
commit 0922ee9955
3 changed files with 60 additions and 0 deletions
+22
View File
@@ -206,6 +206,28 @@ describe("markdownToTelegramHtml", () => {
).toBe('<a href="https://example.com">docs</a>');
});
it("keeps unsupported markdown link hrefs as visible text in rich HTML", () => {
expect(
markdownToTelegramRichHtml(
"[scripts/yougile.py](/home/dankar/.openclaw/workspace-yougile/scripts/yougile.py#L41)",
),
).toBe("<code>scripts/yougile.py</code>");
expect(markdownToTelegramRichHtml("[config](./openclaw.json)")).toBe("config");
expect(markdownToTelegramRichHtml("[docs](https://example.com/docs)")).toBe(
'<a href="https://example.com/docs">docs</a>',
);
expect(markdownToTelegramRichHtml("[user](tg://user?id=123)")).toBe(
'<a href="tg://user?id=123">user</a>',
);
expect(markdownToTelegramRichHtml("[support](mailto:user@example.com)")).toBe(
'<a href="mailto:user@example.com">support</a>',
);
expect(markdownToTelegramRichHtml("[call](tel:+123456789)")).toBe(
'<a href="tel:+123456789">call</a>',
);
expect(markdownToTelegramRichHtml("[back](#top)")).toBe('<a href="#top">back</a>');
});
it("preserves Markdown heading levels in rich HTML", () => {
expect(markdownToTelegramRichHtml("# Title\n\n### Detail")).toBe(
"<h1>Title</h1>\n\n<h3>Detail</h3>",
+9
View File
@@ -32,6 +32,10 @@ function escapeHtmlAttr(text: string): string {
return escapeHtml(text).replace(/"/g, "&quot;");
}
function isTelegramRichLinkHref(href: string): boolean {
return /^(?:https?:\/\/|tg:\/\/|mailto:|tel:|#)/i.test(href);
}
/**
* File extensions that share TLDs and commonly appear in code/documentation.
* These are wrapped in <code> tags to prevent Telegram from generating
@@ -51,6 +55,11 @@ function buildTelegramLink(link: MarkdownLinkSpan, text: string) {
if (link.start === link.end) {
return null;
}
// Telegram rich links reject local or relative hrefs; keep the label visible
// instead of letting one unsupported link drop the whole message.
if (!isTelegramRichLinkHref(href)) {
return null;
}
// Suppress auto-linkified file references (e.g. README.md → http://README.md)
const label = text.slice(link.start, link.end);
if (isAutoLinkedFileRef(href, label)) {
+29
View File
@@ -1044,6 +1044,35 @@ describe("sendMessageTelegram", () => {
expect(botRawApi.sendRichMessage.mock.calls[0]?.[0]?.rich_message.html).toBe(markdown);
});
it.each([
{
name: "local path",
markdown:
"See [scripts/yougile.py](/home/user/.openclaw/workspace/scripts/yougile.py#L41) and [docs](https://example.com/docs)",
rejectedAnchor: '<a href="/home',
visibleLabel: "<code>scripts/yougile.py</code>",
},
{
name: "relative path",
markdown: "Edit [config](./openclaw.json) or see [docs](https://example.com/docs)",
rejectedAnchor: '<a href="./',
visibleLabel: "config",
},
])("keeps rich delivery when a markdown link targets a $name", async (testCase) => {
botApi.sendMessage.mockResolvedValue({ message_id: 48, chat: { id: "123" } });
await sendMessageTelegram("123", testCase.markdown, {
cfg: { channels: { telegram: { richMessages: true } } },
token: "tok",
});
expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
const richHtml = String(botRawApi.sendRichMessage.mock.calls[0]?.[0]?.rich_message.html ?? "");
expect(richHtml).not.toContain(testCase.rejectedAnchor);
expect(richHtml).toContain(testCase.visibleLabel);
expect(richHtml).toContain('<a href="https://example.com/docs">docs</a>');
});
it("renders complex markdown into HTML text", async () => {
botApi.sendMessage.mockResolvedValue({ message_id: 46, chat: { id: "123" } });
const markdown = [