diff --git a/extensions/telegram/src/bot-message-context.body.test.ts b/extensions/telegram/src/bot-message-context.body.test.ts index e9f63f6b5f09..c0785b4b2a4a 100644 --- a/extensions/telegram/src/bot-message-context.body.test.ts +++ b/extensions/telegram/src/bot-message-context.body.test.ts @@ -73,6 +73,50 @@ function transcribeCallContext(index = 0): Record { } describe("resolveTelegramInboundBody", () => { + it("delivers rich-message-only updates as a sanitized placeholder", async () => { + const result = await resolveTelegramBody({ + msg: { + message_id: 0, + date: 1_700_000_000, + chat: { id: 42, type: "private", first_name: "Pat" }, + from: { id: 42, first_name: "Pat" }, + rich_message: { blocks: [{ type: "paragraph" }] }, + } as never, + }); + + expect(result?.rawBody).toBe("[unsupported Telegram rich_message received]"); + expect(result?.bodyText).toBe("[unsupported Telegram rich_message received]"); + }); + + it("keeps rich-message placeholders quiet in requireMention groups", async () => { + const logger = { info: vi.fn() }; + const result = await resolveTelegramBody({ + cfg: { + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: ["\\btelegram\\b"] } }, + } as never, + msg: { + message_id: 1, + date: 1_700_000_001, + chat: { id: -1001234567890, type: "supergroup", title: "Test Group" }, + from: { id: 42, first_name: "Pat" }, + rich_message: { blocks: [{ type: "paragraph" }] }, + } as never, + isGroup: true, + chatId: -1001234567890, + senderId: "42", + groupConfig: { requireMention: true } as never, + requireMention: true, + logger, + }); + + expect(logger.info).toHaveBeenCalledWith( + { chatId: -1001234567890, reason: "no-mention" }, + "skipping group message", + ); + expect(result).toBeNull(); + }); + it("renders Telegram text entities before building the agent body", async () => { const result = await resolveTelegramBody({ msg: { diff --git a/extensions/telegram/src/bot-message-context.body.ts b/extensions/telegram/src/bot-message-context.body.ts index 4a3b2ef3cf02..747857ceba14 100644 --- a/extensions/telegram/src/bot-message-context.body.ts +++ b/extensions/telegram/src/bot-message-context.body.ts @@ -41,6 +41,7 @@ import { hasBotMention, renderTelegramTextEntities, resolveTelegramPrimaryMedia, + resolveTelegramRichMessagePlaceholder, } from "./bot/body-helpers.js"; import { buildTelegramGroupPeerId, buildTelegramInboundOriginTarget } from "./bot/helpers.js"; import type { TelegramContext } from "./bot/types.js"; @@ -239,7 +240,7 @@ export async function resolveTelegramInboundBody(params: { const hasUserText = Boolean(rawText || locationText); let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim(); if (!rawBody) { - rawBody = placeholder; + rawBody = resolveTelegramRichMessagePlaceholder(msg) ?? placeholder; } if (!rawBody && allMedia.length === 0) { return null; diff --git a/extensions/telegram/src/bot/body-helpers.ts b/extensions/telegram/src/bot/body-helpers.ts index 142c74b7f536..282c99a5f57f 100644 --- a/extensions/telegram/src/bot/body-helpers.ts +++ b/extensions/telegram/src/bot/body-helpers.ts @@ -93,6 +93,22 @@ export function buildSenderLabel(msg: Message, senderId?: number | string) { export type TelegramTextEntity = NonNullable[number]; +const TELEGRAM_RICH_MESSAGE_PLACEHOLDER = "[unsupported Telegram rich_message received]"; + +type TelegramTextMessage = Pick & { + rich_message?: unknown; +}; + +function hasTelegramRichMessage(value: unknown): boolean { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function resolveTelegramRichMessagePlaceholder( + msg: TelegramTextMessage, +): string | undefined { + return hasTelegramRichMessage(msg.rich_message) ? TELEGRAM_RICH_MESSAGE_PLACEHOLDER : undefined; +} + export function isBinaryContent(text: string): boolean { for (let i = 0; i < text.length; i++) { const code = text.charCodeAt(i); @@ -108,9 +124,7 @@ export function resolveTelegramTextContent(text: unknown, caption?: unknown): st return isBinaryContent(raw) ? "" : raw; } -export function getTelegramTextParts( - msg: Pick, -): { +export function getTelegramTextParts(msg: TelegramTextMessage): { text: string; entities: TelegramTextEntity[]; } { diff --git a/extensions/telegram/src/bot/helpers.test.ts b/extensions/telegram/src/bot/helpers.test.ts index 89aaef8ae516..004f351c7828 100644 --- a/extensions/telegram/src/bot/helpers.test.ts +++ b/extensions/telegram/src/bot/helpers.test.ts @@ -507,6 +507,24 @@ describe("describeReplyTarget", () => { expect(result?.kind).toBe("reply"); }); + it("describes rich-message-only reply targets with a sanitized placeholder", () => { + const result = describeReplyTarget({ + message_id: 2, + date: 1000, + chat: { id: 1, type: "private" }, + reply_to_message: { + message_id: 1, + date: 900, + chat: { id: 1, type: "private" }, + rich_message: { blocks: [{ type: "paragraph" }] }, + from: { id: 42, first_name: "Alice", is_bot: false }, + }, + } as any); + + expect(result?.body).toBe("[unsupported Telegram rich_message received]"); + expect(result?.quoteSourceText).toBeUndefined(); + }); + it("drops binary reply captions with no safe fallback", () => { const result = describeReplyTarget({ message_id: 2, @@ -710,6 +728,23 @@ describe("isBinaryContent", () => { }); describe("getTelegramTextParts — binary caption filtering (#66647)", () => { + it("keeps rich-message-only updates out of canonical text", () => { + const result = getTelegramTextParts({ + rich_message: { blocks: [{ type: "paragraph" }] }, + }); + + expect(result).toEqual({ text: "", entities: [] }); + }); + + it("keeps normal text when Telegram also supplies a rich message", () => { + const result = getTelegramTextParts({ + text: "normal text", + rich_message: { blocks: [{ type: "paragraph" }] }, + }); + + expect(result).toEqual({ text: "normal text", entities: [] }); + }); + it("strips binary caption content to prevent token explosion", () => { const binaryCaption = "PK\x03\x04\x14\x00\x08binary-ebook-data"; const result = getTelegramTextParts({ diff --git a/extensions/telegram/src/bot/helpers.ts b/extensions/telegram/src/bot/helpers.ts index 4f26114d5ed8..17e55534d894 100644 --- a/extensions/telegram/src/bot/helpers.ts +++ b/extensions/telegram/src/bot/helpers.ts @@ -40,6 +40,7 @@ import { renderTelegramTextEntities, resolveTelegramTextContent, resolveTelegramMediaPlaceholder, + resolveTelegramRichMessagePlaceholder, type TelegramForwardedContext, type TelegramTextEntity, } from "./body-helpers.js"; @@ -56,6 +57,7 @@ export { normalizeForwardedContext, renderTelegramTextEntities, resolveTelegramMediaPlaceholder, + resolveTelegramRichMessagePlaceholder, }; const TELEGRAM_GENERAL_TOPIC_ID = 1; @@ -619,11 +621,12 @@ export function describeReplyTarget(msg: Message): TelegramReplyTarget | null { : replyLike && typeof replyLike.caption === "string" ? replyLike.caption : undefined; - const safeReplyText = resolveTelegramTextContent(rawReplyText); - const replyTextParts = replyLike && safeReplyText ? getTelegramTextParts(replyLike) : undefined; + const replyTextParts = replyLike ? getTelegramTextParts(replyLike) : undefined; + const safeReplyText = replyTextParts?.text ?? ""; let filteredReplyText = false; if (!body && replyLike) { - const replyBody = safeReplyText.trim(); + const replyBody = + safeReplyText.trim() || resolveTelegramRichMessagePlaceholder(replyLike) || ""; filteredReplyText = hadUnsafeTelegramText(rawReplyText, replyBody); body = replyBody; if (!body) { diff --git a/extensions/telegram/src/message-cache.test.ts b/extensions/telegram/src/message-cache.test.ts index 35d411b12e12..3f2309c60870 100644 --- a/extensions/telegram/src/message-cache.test.ts +++ b/extensions/telegram/src/message-cache.test.ts @@ -557,6 +557,49 @@ describe("telegram message cache", () => { expect(recent.map((entry) => entry.messageId)).toEqual(["42", "43"]); }); + it("preserves rich-message placeholders in subsequent conversation context", async () => { + const cache = createTelegramMessageCache(); + const chat = { id: 7, type: "private", first_name: "Nora" } as const; + await cache.record({ + accountId: "default", + chatId: 7, + msg: { + chat, + message_id: 45, + date: 1736380745, + rich_message: { blocks: [{ type: "paragraph" }] }, + from: { id: 1, is_bot: false, first_name: "Nora" }, + } as Message, + }); + await cache.record({ + accountId: "default", + chatId: 7, + msg: { + chat, + message_id: 46, + date: 1736380746, + text: "What did I just send?", + from: { id: 1, is_bot: false, first_name: "Nora" }, + } as Message, + }); + + const context = await buildTelegramConversationContext({ + cache, + accountId: "default", + chatId: 7, + messageId: "46", + replyChainNodes: [], + recentLimit: 10, + replyTargetWindowSize: 2, + }); + + expect(context).toHaveLength(1); + expect(context[0]?.node).toMatchObject({ + messageId: "45", + body: "[unsupported Telegram rich_message received]", + }); + }); + it("returns nearby messages around a stale reply target", async () => { const cache = createTelegramMessageCache(); for (const id of [100, 101, 102, 200, 201]) { diff --git a/extensions/telegram/src/message-cache.ts b/extensions/telegram/src/message-cache.ts index 66387ac3d898..2132020463c6 100644 --- a/extensions/telegram/src/message-cache.ts +++ b/extensions/telegram/src/message-cache.ts @@ -7,7 +7,10 @@ import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { resolveTelegramPrimaryMedia } from "./bot/body-helpers.js"; +import { + resolveTelegramPrimaryMedia, + resolveTelegramRichMessagePlaceholder, +} from "./bot/body-helpers.js"; import { buildSenderName, extractTelegramLocation, @@ -151,7 +154,9 @@ function resolveMessageBody(msg: Message): string | undefined { if (location) { return formatLocationText(location); } - return resolveTelegramPrimaryMedia(msg)?.placeholder; + return ( + resolveTelegramRichMessagePlaceholder(msg) ?? resolveTelegramPrimaryMedia(msg)?.placeholder + ); } function resolveMediaType(placeholder?: string): string | undefined {