惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
博客园 - 叶小钗
Hugging Face - Blog
Hugging Face - Blog
博客园 - 司徒正美
Last Week in AI
Last Week in AI
罗磊的独立博客
量子位
Jina AI
Jina AI
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
美团技术团队
雷峰网
雷峰网
爱范儿
爱范儿
S
SegmentFault 最新的问题
小众软件
小众软件
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(telegram): gate rich messages behind opt-in (#93279) ...
NianJiuZst · 2026-06-16 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -418,14 +418,28 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"

418418

</Accordion>

419419
420420

<Accordion title="Rich message formatting">

421-

Outbound text uses Telegram rich messages.

421+

Outbound text uses standard Telegram HTML messages by default so replies remain readable across current Telegram clients.

422+
423+

Set `channels.telegram.richMessages: true` to opt into Bot API 10.1 rich messages:

424+
425+

```json5

426+

{

427+

channels: {

428+

telegram: {

429+

richMessages: true,

430+

},

431+

},

432+

}

433+

```

422434
423435

- Markdown text is rendered through OpenClaw's Markdown IR and sent as Telegram rich HTML.

424436

- Explicit rich HTML payloads preserve supported Bot API 10.1 tags such as headings, tables, details, rich media, and formulas.

425437

- Media captions still use Telegram HTML captions because rich messages do not replace captions.

426438
427439

This keeps model text away from Telegram Rich Markdown sigils, so currency like `$400-600K` is not parsed as math. Long rich text is split automatically across Telegram's rich text and rich block limits. Tables over Telegram's column limit are sent as code blocks.

428440
441+

Rich messages require compatible Telegram clients. Some current Desktop, Web, Android, and third-party clients display accepted rich messages as unsupported, so keep this option disabled unless every client used with the bot can render them.

442+
429443

Link previews are enabled by default. `channels.telegram.linkPreview: false` skips automatic entity detection for rich text.

430444
431445

</Accordion>

@@ -1081,7 +1095,7 @@ Primary reference: [Configuration reference - Telegram](/gateway/config-channels

10811095

- command/menu: `commands.native`, `commands.nativeSkills`, `customCommands`

10821096

- threading/replies: `replyToMode`

10831097

- streaming: `streaming` (preview), `streaming.preview.toolProgress`, `blockStreaming`

1084-

- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`

1098+

- formatting/delivery: `textChunkLimit`, `chunkMode`, `richMessages`, `linkPreview`, `responsePrefix`

10851099

- media/network: `mediaMaxMb`, `mediaGroupFlushMs`, `timeoutSeconds`, `pollingStallThresholdMs`, `retry`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy`

10861100

- custom API root: `apiRoot` (Bot API root only; do not include `/bot<TOKEN>`)

10871101

- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`

Original file line numberDiff line numberDiff line change

@@ -49,6 +49,7 @@ import {

4949

resolveTelegramOutboundClientTimeoutFloorSeconds,

5050

} from "./client-fetch.js";

5151

import { resolveTelegramTransport } from "./fetch.js";

52+

import { TELEGRAM_TEXT_CHUNK_LIMIT } from "./outbound-adapter.js";

5253

import { stringifyTelegramRawUpdateForLog } from "./raw-update-log.js";

5354

import { TELEGRAM_RICH_TEXT_LIMIT } from "./rich-message.js";

5455

import { createTelegramSendChatActionHandler } from "./sendchataction-401-backoff.js";

@@ -290,11 +291,13 @@ export function createTelegramBotCore(

290291

DEFAULT_GROUP_HISTORY_LIMIT,

291292

);

292293

const groupHistories = new Map<string, HistoryEntry[]>();

294+

const telegramTextLimit =

295+

telegramCfg.richMessages === true ? TELEGRAM_RICH_TEXT_LIMIT : TELEGRAM_TEXT_CHUNK_LIMIT;

293296

const textLimit = Math.min(

294297

resolveTextChunkLimit(cfg, "telegram", account.accountId, {

295-

fallbackLimit: TELEGRAM_RICH_TEXT_LIMIT,

298+

fallbackLimit: telegramTextLimit,

296299

}),

297-

TELEGRAM_RICH_TEXT_LIMIT,

300+

telegramTextLimit,

298301

);

299302

const dmPolicy = telegramCfg.dmPolicy ?? "pairing";

300303

const allowFrom = opts.allowFrom ?? telegramCfg.allowFrom;

Original file line numberDiff line numberDiff line change

@@ -649,6 +649,61 @@ describe("dispatchTelegramMessage draft streaming", () => {

649649

expect(draftStream.clear).toHaveBeenCalledTimes(1);

650650

});

651651
652+

it("renders default draft previews with standard Telegram HTML", async () => {

653+

const draftStream = createDraftStream();

654+

createTelegramDraftStream.mockReturnValue(draftStream);

655+

dispatchReplyWithBufferedBlockDispatcher.mockImplementation(

656+

async ({ dispatcherOptions, replyOptions }) => {

657+

await replyOptions?.onPartialReply?.({ text: "# Heading" });

658+

await dispatcherOptions.deliver({ text: "# Heading" }, { kind: "final" });

659+

return { queuedFinal: true };

660+

},

661+

);

662+

deliverReplies.mockResolvedValue({ delivered: true });

663+
664+

await dispatchWithContext({ context: createContext() });

665+
666+

const params = expectDraftStreamParams({});

667+

const renderText = params.renderText as ((text: string) => Record<string, unknown>) | undefined;

668+

expect(renderText?.("# Heading")).toEqual({

669+

text: "Heading",

670+

parseMode: "HTML",

671+

});

672+

});

673+
674+

it("renders rich draft previews only when enabled", async () => {

675+

resolveMarkdownTableMode.mockReturnValueOnce("block");

676+

const draftStream = createDraftStream();

677+

createTelegramDraftStream.mockReturnValue(draftStream);

678+

dispatchReplyWithBufferedBlockDispatcher.mockImplementation(

679+

async ({ dispatcherOptions, replyOptions }) => {

680+

await replyOptions?.onPartialReply?.({

681+

text: "| A | B |\n| --- | --- |\n| 1 | 2 |",

682+

});

683+

await dispatcherOptions.deliver(

684+

{ text: "| A | B |\n| --- | --- |\n| 1 | 2 |" },

685+

{ kind: "final" },

686+

);

687+

return { queuedFinal: true };

688+

},

689+

);

690+

deliverReplies.mockResolvedValue({ delivered: true });

691+
692+

await dispatchWithContext({

693+

context: createContext(),

694+

telegramCfg: { richMessages: true },

695+

});

696+
697+

const params = expectDraftStreamParams({ richMessages: true });

698+

const renderText = params.renderText as ((text: string) => Record<string, unknown>) | undefined;

699+

const preview = renderText?.("| A | B |\n| --- | --- |\n| 1 | 2 |");

700+

expect(preview?.richMessage).toEqual(

701+

expect.objectContaining({

702+

html: expect.stringContaining("<table>"),

703+

}),

704+

);

705+

});

706+
652707

it("recovers forum thread context from a topic-scoped session key", async () => {

653708

const recordInboundSession = vi.fn(async () => undefined);

654709

const oldHistoryKey = "-1003774691294:topic:1";

@@ -1521,7 +1576,7 @@ describe("dispatchTelegramMessage draft streaming", () => {

15211576

telegramCfg: { streaming: { mode: "partial" } },

15221577

});

15231578
1524-

expectDraftStreamParams({ maxChars: 4096 });

1579+

expectDraftStreamParams({ maxChars: 4000 });

15251580

});

15261581
15271582

it("streams text-only finals into the answer message", async () => {

Original file line numberDiff line numberDiff line change

@@ -107,6 +107,7 @@ import {

107107

shouldSuppressTelegramError,

108108

} from "./error-policy.js";

109109

import { shouldSuppressLocalTelegramExecApprovalPrompt } from "./exec-approvals.js";

110+

import { renderTelegramHtmlText } from "./format.js";

110111

import { includesRecentTelegramGroupHistoryContext } from "./group-history-context.js";

111112

import { beginTelegramInboundEventDeliveryCorrelation } from "./inbound-event-delivery.js";

112113

import {

@@ -116,6 +117,7 @@ import {

116117

type LaneDeliveryResult,

117118

type LaneName,

118119

} from "./lane-delivery.js";

120+

import { TELEGRAM_TEXT_CHUNK_LIMIT } from "./outbound-adapter.js";

119121

import { recordOutboundMessageForPromptContext } from "./outbound-message-context.js";

120122

import {

121123

createTelegramReasoningStepState,

@@ -891,20 +893,29 @@ export const dispatchTelegramMessage = async ({

891893

const draftMaxChars =

892894

streamMode === "block"

893895

? Math.min(resolveTelegramDraftStreamingChunking(cfg, route.accountId).maxChars, textLimit)

894-

: Math.min(textLimit, TELEGRAM_RICH_TEXT_LIMIT);

896+

: Math.min(

897+

textLimit,

898+

telegramCfg.richMessages === true ? TELEGRAM_RICH_TEXT_LIMIT : TELEGRAM_TEXT_CHUNK_LIMIT,

899+

);

895900

const tableMode = resolveMarkdownTableMode({

896901

cfg,

897902

channel: "telegram",

898903

accountId: route.accountId,

899-

supportsBlockTables: true,

900-

});

901-

const renderStreamText = (text: string) => ({

902-

text,

903-

richMessage: buildTelegramRichMarkdown(text, {

904-

tableMode,

905-

skipEntityDetection: telegramCfg.linkPreview === false,

906-

}),

904+

supportsBlockTables: telegramCfg.richMessages === true,

907905

});

906+

const renderStreamText = (text: string): TelegramDraftPreview =>

907+

telegramCfg.richMessages === true

908+

? {

909+

text,

910+

richMessage: buildTelegramRichMarkdown(text, {

911+

tableMode,

912+

skipEntityDetection: telegramCfg.linkPreview === false,

913+

}),

914+

}

915+

: {

916+

text: renderTelegramHtmlText(text, { tableMode }),

917+

parseMode: "HTML",

918+

};

908919

const accountBlockStreamingEnabled =

909920

resolveChannelStreamingBlockEnabled(telegramCfg) ??

910921

cfg.agents?.defaults?.blockStreamingDefault === "on";

@@ -988,6 +999,7 @@ export const dispatchTelegramMessage = async ({

988999

maxChars: draftMaxChars,

9891000

thread: threadSpec,

9901001

replyToMessageId: draftReplyToMessageId,

1002+

richMessages: telegramCfg.richMessages,

9911003

minInitialChars: draftMinInitialChars,

9921004

renderText: renderStreamText,

9931005

onSupersededPreview: (superseded) => {

@@ -1507,6 +1519,7 @@ export const dispatchTelegramMessage = async ({

15071519

thread: threadSpec,

15081520

tableMode,

15091521

chunkMode,

1522+

richMessages: telegramCfg.richMessages,

15101523

linkPreview: telegramCfg.linkPreview,

15111524

replyQuoteMessageId,

15121525

replyQuoteText,

Original file line numberDiff line numberDiff line change

@@ -703,6 +703,25 @@ describe("registerTelegramNativeCommands", () => {

703703

expect(replyAt(deliverParams).isError).toBe(true);

704704

});

705705
706+

it("uses rich messages for plugin command replies when enabled", async () => {

707+

const { handler } = registerPlugCommand({

708+

cfg: {

709+

channels: {

710+

telegram: {

711+

richMessages: true,

712+

},

713+

},

714+

},

715+

registerOverrides: {

716+

telegramCfg: { richMessages: true } as TelegramAccountConfig,

717+

},

718+

});

719+
720+

await handler(createPrivateCommandContext());

721+
722+

expect(firstDeliverRepliesParams().richMessages).toBe(true);

723+

});

724+
706725

it("forwards topic-scoped binding context to Telegram plugin commands", async () => {

707726

const { handler } = registerPlugCommand();

708727
Original file line numberDiff line numberDiff line change

@@ -973,6 +973,7 @@ export const registerTelegramNativeCommands = ({

973973

tableMode: ReturnType<typeof resolveMarkdownTableMode>;

974974

chunkMode: TelegramChunkMode;

975975

linkPreview?: boolean;

976+

richMessages?: boolean;

976977

}) => ({

977978

cfg: params.cfg,

978979

chatId: String(params.chatId),

@@ -992,6 +993,7 @@ export const registerTelegramNativeCommands = ({

992993

tableMode: params.tableMode,

993994

chunkMode: params.chunkMode,

994995

linkPreview: params.linkPreview,

996+

richMessages: params.richMessages,

995997

});

996998

const resolveCommandTargetSessionKey = (params: {

997999

runtimeCfg: OpenClawConfig;

@@ -1209,6 +1211,7 @@ export const registerTelegramNativeCommands = ({

12091211

tableMode,

12101212

chunkMode,

12111213

linkPreview: runtimeTelegramCfg.linkPreview,

1214+

richMessages: runtimeTelegramCfg.richMessages,

12121215

});

12131216

let topicName: string | undefined;

12141217

if (isForum && resolvedThreadId != null) {

@@ -1431,6 +1434,7 @@ export const registerTelegramNativeCommands = ({

14311434

tableMode,

14321435

chunkMode,

14331436

linkPreview: runtimeTelegramCfg.linkPreview,

1437+

richMessages: runtimeTelegramCfg.richMessages,

14341438

});

14351439

const from = isGroup ? buildTelegramGroupFrom(chatId, threadSpec.id) : `telegram:${chatId}`;

14361440

const to = `telegram:${chatId}`;