diff --git a/extensions/telegram/src/bot-core.ts b/extensions/telegram/src/bot-core.ts index d965a621623..40a3d7606b3 100644 --- a/extensions/telegram/src/bot-core.ts +++ b/extensions/telegram/src/bot-core.ts @@ -372,6 +372,7 @@ export function createTelegramBotCore( groupAllowFrom, replyToMode, textLimit, + mediaMaxBytes, useAccessGroups, nativeEnabled, nativeSkillsEnabled, diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index d530cb2d010..6c903b30d1e 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -1790,6 +1790,7 @@ describe("dispatchTelegramMessage draft streaming", () => { it("keeps streamed final text in place when late media arrives", async () => { const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); + const mediaMaxBytes = 50 * 1024 * 1024; dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { await replyOptions?.onPartialReply?.({ text: "Photo" }); @@ -1801,10 +1802,14 @@ describe("dispatchTelegramMessage draft streaming", () => { }, ); - await dispatchWithContext({ context: createContext() }); + await dispatchWithContext({ + context: createContext(), + telegramCfg: { mediaMaxMb: 50 }, + }); expect(answerDraftStream.clear).not.toHaveBeenCalled(); expect(answerDraftStream.update).toHaveBeenCalledWith("Photo"); + expectDeliverRepliesParams({ mediaMaxBytes }); expectDeliveredReply(0, { text: undefined, mediaUrl: "https://example.com/a.png" }); }); diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index b70f55f1ab8..4b02226641c 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -228,7 +228,7 @@ type DispatchTelegramMessageParams = { textLimit: number; telegramCfg: TelegramAccountConfig; telegramDeps?: TelegramBotDeps; - opts: Pick; + opts: Pick; }; type TelegramReasoningLevel = "off" | "on" | "stream"; @@ -1001,6 +1001,7 @@ export const dispatchTelegramMessage = async ({ runtime, bot, mediaLocalRoots, + mediaMaxBytes: (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 100) * 1024 * 1024, replyToMode, textLimit, thread: threadSpec, diff --git a/extensions/telegram/src/bot-native-commands.test.ts b/extensions/telegram/src/bot-native-commands.test.ts index e2171b80bf8..b45b9427efe 100644 --- a/extensions/telegram/src/bot-native-commands.test.ts +++ b/extensions/telegram/src/bot-native-commands.test.ts @@ -59,8 +59,8 @@ function registerPlugCommand(params: PlugCommandHarnessParams = {}) { registerTelegramNativeCommands({ ...createNativeCommandTestParams(params.cfg ?? {}, { bot: botHarness.bot, - ...params.registerOverrides, }), + ...params.registerOverrides, }); const handler = botHarness.commandHandlers.get("plug"); if (!handler) { @@ -371,6 +371,7 @@ describe("registerTelegramNativeCommands", () => { }); it("passes agent-scoped media roots for plugin command replies with media", async () => { + const mediaMaxBytes = 50 * 1024 * 1024; const cfg: OpenClawConfig = { agents: { list: [{ id: "main", default: true }, { id: "work" }], @@ -384,11 +385,15 @@ describe("registerTelegramNativeCommands", () => { text: "with media", mediaUrl: "/tmp/workspace-work/render.png", }, + registerOverrides: { + mediaMaxBytes, + } as Partial[0]>, }); await handler(createPrivateCommandContext()); const deliverParams = firstDeliverRepliesParams(); + expect(deliverParams.mediaMaxBytes).toBe(mediaMaxBytes); const mediaLocalRoots = deliverParams.mediaLocalRoots as Array | undefined; expect(mediaLocalRoots?.some((root) => /[\\/]\.openclaw[\\/]workspace-work$/.test(root))).toBe( true, diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 4a728f60707..8c84fa6c14f 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -467,6 +467,7 @@ export type RegisterTelegramNativeCommandsParams = { groupAllowFrom?: Array; replyToMode: ReplyToMode; textLimit: number; + mediaMaxBytes?: number; useAccessGroups: boolean; nativeEnabled: boolean; nativeSkillsEnabled: boolean; @@ -693,6 +694,7 @@ export const registerTelegramNativeCommands = ({ groupAllowFrom, replyToMode, textLimit, + mediaMaxBytes, useAccessGroups, nativeEnabled, nativeSkillsEnabled, @@ -933,6 +935,7 @@ export const registerTelegramNativeCommands = ({ runtime, bot, mediaLocalRoots: params.mediaLocalRoots, + mediaMaxBytes, replyToMode, textLimit, thread: params.threadSpec, diff --git a/extensions/telegram/src/bot/delivery.replies.ts b/extensions/telegram/src/bot/delivery.replies.ts index fd81df2563f..434885c556a 100644 --- a/extensions/telegram/src/bot/delivery.replies.ts +++ b/extensions/telegram/src/bot/delivery.replies.ts @@ -331,6 +331,7 @@ async function deliverMediaReply(params: { thread?: TelegramThreadSpec | null; tableMode?: MarkdownTableMode; mediaLocalRoots?: readonly string[]; + mediaMaxBytes?: number; chunkText: ChunkTextFn; mediaLoader: typeof loadWebMedia; onVoiceRecording?: () => Promise | void; @@ -353,7 +354,10 @@ async function deliverMediaReply(params: { const isFirstMedia = first; const media = await params.mediaLoader( mediaUrl, - buildOutboundMediaLoadOptions({ mediaLocalRoots: params.mediaLocalRoots }), + buildOutboundMediaLoadOptions({ + mediaLocalRoots: params.mediaLocalRoots, + maxBytes: params.mediaMaxBytes, + }), ); const kind = kindFromMime(media.contentType ?? undefined); const isGif = isGifMedia({ @@ -693,6 +697,7 @@ export async function deliverReplies(params: { runtime: RuntimeEnv; bot: Bot; mediaLocalRoots?: readonly string[]; + mediaMaxBytes?: number; replyToMode: ReplyToMode; textLimit: number; thread?: TelegramThreadSpec | null; @@ -879,6 +884,7 @@ export async function deliverReplies(params: { thread: params.thread, tableMode: params.tableMode, mediaLocalRoots: params.mediaLocalRoots, + mediaMaxBytes: params.mediaMaxBytes, chunkText, mediaLoader, onVoiceRecording: params.onVoiceRecording, diff --git a/extensions/telegram/src/bot/delivery.test.ts b/extensions/telegram/src/bot/delivery.test.ts index c4cce55a49b..2f79ad2df77 100644 --- a/extensions/telegram/src/bot/delivery.test.ts +++ b/extensions/telegram/src/bot/delivery.test.ts @@ -751,6 +751,33 @@ describe("deliverReplies", () => { }); }); + it("passes the configured media byte cap to media loading", async () => { + const runtime = createRuntime(); + const sendPhoto = vi.fn().mockResolvedValue({ + message_id: 13, + chat: { id: "123" }, + }); + const bot = createBot({ sendPhoto }); + const mediaMaxBytes = 50 * 1024 * 1024; + + mockMediaLoad("photo.jpg", "image/jpeg", "image"); + + await deliverReplies({ + replies: [{ mediaUrl: "https://example.com/photo.jpg" }], + chatId: "123", + token: "tok", + runtime, + bot, + replyToMode: "off", + textLimit: 4000, + mediaMaxBytes, + }); + + expect(loadWebMedia).toHaveBeenCalledWith("https://example.com/photo.jpg", { + maxBytes: mediaMaxBytes, + }); + }); + it("includes link_preview_options when linkPreview is false", async () => { const runtime = createRuntime(); const sendMessage = vi.fn().mockResolvedValue({