Skip to content

Commit ee3b7eb

Browse files
fix(telegram): forward Bot API 10.1 rich_message content to agent (#93418)
* fix(telegram): surface unsupported inbound rich messages * fix(telegram): isolate rich message placeholders * fix(telegram): accept typed rich message inputs * fix(telegram): preserve rich message cache marker --------- Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
1 parent 2365a13 commit ee3b7eb

7 files changed

Lines changed: 154 additions & 9 deletions

File tree

extensions/telegram/src/bot-message-context.body.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,50 @@ function transcribeCallContext(index = 0): Record<string, unknown> {
7373
}
7474

7575
describe("resolveTelegramInboundBody", () => {
76+
it("delivers rich-message-only updates as a sanitized placeholder", async () => {
77+
const result = await resolveTelegramBody({
78+
msg: {
79+
message_id: 0,
80+
date: 1_700_000_000,
81+
chat: { id: 42, type: "private", first_name: "Pat" },
82+
from: { id: 42, first_name: "Pat" },
83+
rich_message: { blocks: [{ type: "paragraph" }] },
84+
} as never,
85+
});
86+
87+
expect(result?.rawBody).toBe("[unsupported Telegram rich_message received]");
88+
expect(result?.bodyText).toBe("[unsupported Telegram rich_message received]");
89+
});
90+
91+
it("keeps rich-message placeholders quiet in requireMention groups", async () => {
92+
const logger = { info: vi.fn() };
93+
const result = await resolveTelegramBody({
94+
cfg: {
95+
channels: { telegram: {} },
96+
messages: { groupChat: { mentionPatterns: ["\\btelegram\\b"] } },
97+
} as never,
98+
msg: {
99+
message_id: 1,
100+
date: 1_700_000_001,
101+
chat: { id: -1001234567890, type: "supergroup", title: "Test Group" },
102+
from: { id: 42, first_name: "Pat" },
103+
rich_message: { blocks: [{ type: "paragraph" }] },
104+
} as never,
105+
isGroup: true,
106+
chatId: -1001234567890,
107+
senderId: "42",
108+
groupConfig: { requireMention: true } as never,
109+
requireMention: true,
110+
logger,
111+
});
112+
113+
expect(logger.info).toHaveBeenCalledWith(
114+
{ chatId: -1001234567890, reason: "no-mention" },
115+
"skipping group message",
116+
);
117+
expect(result).toBeNull();
118+
});
119+
76120
it("renders Telegram text entities before building the agent body", async () => {
77121
const result = await resolveTelegramBody({
78122
msg: {

extensions/telegram/src/bot-message-context.body.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
hasBotMention,
4242
renderTelegramTextEntities,
4343
resolveTelegramPrimaryMedia,
44+
resolveTelegramRichMessagePlaceholder,
4445
} from "./bot/body-helpers.js";
4546
import { buildTelegramGroupPeerId, buildTelegramInboundOriginTarget } from "./bot/helpers.js";
4647
import type { TelegramContext } from "./bot/types.js";
@@ -239,7 +240,7 @@ export async function resolveTelegramInboundBody(params: {
239240
const hasUserText = Boolean(rawText || locationText);
240241
let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim();
241242
if (!rawBody) {
242-
rawBody = placeholder;
243+
rawBody = resolveTelegramRichMessagePlaceholder(msg) ?? placeholder;
243244
}
244245
if (!rawBody && allMedia.length === 0) {
245246
return null;

extensions/telegram/src/bot/body-helpers.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ export function buildSenderLabel(msg: Message, senderId?: number | string) {
9393

9494
export type TelegramTextEntity = NonNullable<Message["entities"]>[number];
9595

96+
const TELEGRAM_RICH_MESSAGE_PLACEHOLDER = "[unsupported Telegram rich_message received]";
97+
98+
type TelegramTextMessage = Pick<Message, "text" | "caption" | "entities" | "caption_entities"> & {
99+
rich_message?: unknown;
100+
};
101+
102+
function hasTelegramRichMessage(value: unknown): boolean {
103+
return typeof value === "object" && value !== null && !Array.isArray(value);
104+
}
105+
106+
export function resolveTelegramRichMessagePlaceholder(
107+
msg: TelegramTextMessage,
108+
): string | undefined {
109+
return hasTelegramRichMessage(msg.rich_message) ? TELEGRAM_RICH_MESSAGE_PLACEHOLDER : undefined;
110+
}
111+
96112
export function isBinaryContent(text: string): boolean {
97113
for (let i = 0; i < text.length; i++) {
98114
const code = text.charCodeAt(i);
@@ -108,9 +124,7 @@ export function resolveTelegramTextContent(text: unknown, caption?: unknown): st
108124
return isBinaryContent(raw) ? "" : raw;
109125
}
110126

111-
export function getTelegramTextParts(
112-
msg: Pick<Message, "text" | "caption" | "entities" | "caption_entities">,
113-
): {
127+
export function getTelegramTextParts(msg: TelegramTextMessage): {
114128
text: string;
115129
entities: TelegramTextEntity[];
116130
} {

extensions/telegram/src/bot/helpers.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,24 @@ describe("describeReplyTarget", () => {
507507
expect(result?.kind).toBe("reply");
508508
});
509509

510+
it("describes rich-message-only reply targets with a sanitized placeholder", () => {
511+
const result = describeReplyTarget({
512+
message_id: 2,
513+
date: 1000,
514+
chat: { id: 1, type: "private" },
515+
reply_to_message: {
516+
message_id: 1,
517+
date: 900,
518+
chat: { id: 1, type: "private" },
519+
rich_message: { blocks: [{ type: "paragraph" }] },
520+
from: { id: 42, first_name: "Alice", is_bot: false },
521+
},
522+
} as any);
523+
524+
expect(result?.body).toBe("[unsupported Telegram rich_message received]");
525+
expect(result?.quoteSourceText).toBeUndefined();
526+
});
527+
510528
it("drops binary reply captions with no safe fallback", () => {
511529
const result = describeReplyTarget({
512530
message_id: 2,
@@ -710,6 +728,23 @@ describe("isBinaryContent", () => {
710728
});
711729

712730
describe("getTelegramTextParts — binary caption filtering (#66647)", () => {
731+
it("keeps rich-message-only updates out of canonical text", () => {
732+
const result = getTelegramTextParts({
733+
rich_message: { blocks: [{ type: "paragraph" }] },
734+
});
735+
736+
expect(result).toEqual({ text: "", entities: [] });
737+
});
738+
739+
it("keeps normal text when Telegram also supplies a rich message", () => {
740+
const result = getTelegramTextParts({
741+
text: "normal text",
742+
rich_message: { blocks: [{ type: "paragraph" }] },
743+
});
744+
745+
expect(result).toEqual({ text: "normal text", entities: [] });
746+
});
747+
713748
it("strips binary caption content to prevent token explosion", () => {
714749
const binaryCaption = "PK\x03\x04\x14\x00\x08binary-ebook-data";
715750
const result = getTelegramTextParts({

extensions/telegram/src/bot/helpers.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
renderTelegramTextEntities,
4141
resolveTelegramTextContent,
4242
resolveTelegramMediaPlaceholder,
43+
resolveTelegramRichMessagePlaceholder,
4344
type TelegramForwardedContext,
4445
type TelegramTextEntity,
4546
} from "./body-helpers.js";
@@ -56,6 +57,7 @@ export {
5657
normalizeForwardedContext,
5758
renderTelegramTextEntities,
5859
resolveTelegramMediaPlaceholder,
60+
resolveTelegramRichMessagePlaceholder,
5961
};
6062

6163
const TELEGRAM_GENERAL_TOPIC_ID = 1;
@@ -619,11 +621,12 @@ export function describeReplyTarget(msg: Message): TelegramReplyTarget | null {
619621
: replyLike && typeof replyLike.caption === "string"
620622
? replyLike.caption
621623
: undefined;
622-
const safeReplyText = resolveTelegramTextContent(rawReplyText);
623-
const replyTextParts = replyLike && safeReplyText ? getTelegramTextParts(replyLike) : undefined;
624+
const replyTextParts = replyLike ? getTelegramTextParts(replyLike) : undefined;
625+
const safeReplyText = replyTextParts?.text ?? "";
624626
let filteredReplyText = false;
625627
if (!body && replyLike) {
626-
const replyBody = safeReplyText.trim();
628+
const replyBody =
629+
safeReplyText.trim() || resolveTelegramRichMessagePlaceholder(replyLike) || "";
627630
filteredReplyText = hadUnsafeTelegramText(rawReplyText, replyBody);
628631
body = replyBody;
629632
if (!body) {

extensions/telegram/src/message-cache.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,49 @@ describe("telegram message cache", () => {
557557
expect(recent.map((entry) => entry.messageId)).toEqual(["42", "43"]);
558558
});
559559

560+
it("preserves rich-message placeholders in subsequent conversation context", async () => {
561+
const cache = createTelegramMessageCache();
562+
const chat = { id: 7, type: "private", first_name: "Nora" } as const;
563+
await cache.record({
564+
accountId: "default",
565+
chatId: 7,
566+
msg: {
567+
chat,
568+
message_id: 45,
569+
date: 1736380745,
570+
rich_message: { blocks: [{ type: "paragraph" }] },
571+
from: { id: 1, is_bot: false, first_name: "Nora" },
572+
} as Message,
573+
});
574+
await cache.record({
575+
accountId: "default",
576+
chatId: 7,
577+
msg: {
578+
chat,
579+
message_id: 46,
580+
date: 1736380746,
581+
text: "What did I just send?",
582+
from: { id: 1, is_bot: false, first_name: "Nora" },
583+
} as Message,
584+
});
585+
586+
const context = await buildTelegramConversationContext({
587+
cache,
588+
accountId: "default",
589+
chatId: 7,
590+
messageId: "46",
591+
replyChainNodes: [],
592+
recentLimit: 10,
593+
replyTargetWindowSize: 2,
594+
});
595+
596+
expect(context).toHaveLength(1);
597+
expect(context[0]?.node).toMatchObject({
598+
messageId: "45",
599+
body: "[unsupported Telegram rich_message received]",
600+
});
601+
});
602+
560603
it("returns nearby messages around a stale reply target", async () => {
561604
const cache = createTelegramMessageCache();
562605
for (const id of [100, 101, 102, 200, 201]) {

extensions/telegram/src/message-cache.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
77
import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
88
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
99
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
10-
import { resolveTelegramPrimaryMedia } from "./bot/body-helpers.js";
10+
import {
11+
resolveTelegramPrimaryMedia,
12+
resolveTelegramRichMessagePlaceholder,
13+
} from "./bot/body-helpers.js";
1114
import {
1215
buildSenderName,
1316
extractTelegramLocation,
@@ -151,7 +154,9 @@ function resolveMessageBody(msg: Message): string | undefined {
151154
if (location) {
152155
return formatLocationText(location);
153156
}
154-
return resolveTelegramPrimaryMedia(msg)?.placeholder;
157+
return (
158+
resolveTelegramRichMessagePlaceholder(msg) ?? resolveTelegramPrimaryMedia(msg)?.placeholder
159+
);
155160
}
156161

157162
function resolveMediaType(placeholder?: string): string | undefined {

0 commit comments

Comments
 (0)