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>
This commit is contained in:
Dirk
2026-06-16 08:42:48 +08:00
committed by GitHub
parent 2365a137d8
commit ee3b7eb7c0
7 changed files with 154 additions and 9 deletions
@@ -73,6 +73,50 @@ function transcribeCallContext(index = 0): Record<string, unknown> {
}
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: {
@@ -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;
+17 -3
View File
@@ -93,6 +93,22 @@ export function buildSenderLabel(msg: Message, senderId?: number | string) {
export type TelegramTextEntity = NonNullable<Message["entities"]>[number];
const TELEGRAM_RICH_MESSAGE_PLACEHOLDER = "[unsupported Telegram rich_message received]";
type TelegramTextMessage = Pick<Message, "text" | "caption" | "entities" | "caption_entities"> & {
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<Message, "text" | "caption" | "entities" | "caption_entities">,
): {
export function getTelegramTextParts(msg: TelegramTextMessage): {
text: string;
entities: TelegramTextEntity[];
} {
@@ -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({
+6 -3
View File
@@ -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) {
@@ -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]) {
+7 -2
View File
@@ -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 {