mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
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:
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
Reference in New Issue
Block a user