diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e319f631f..4de7dd56eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -196,6 +196,7 @@ Docs: https://docs.openclaw.ai - Agents/gateway config guidance: stop exposing `config.schema` through the agent `gateway` tool, remove prompt/docs guidance that told agents to call it, and keep agents on `config.get` plus `config.patch`/`config.apply` for config changes. (#7382) thanks @kakuteki. - Agents/failover: classify periodic provider limit exhaustion text (for example `Weekly/Monthly Limit Exhausted`) as `rate_limit` while keeping explicit `402 Payment Required` variants in billing, so failover continues without misclassifying billing-wrapped quota errors. (#33813) thanks @zhouhe-xydt. - Mattermost/interactive button callbacks: allow external callback base URLs and stop requiring loopback-origin requests so button clicks work when Mattermost reaches the gateway over Tailscale, LAN, or a reverse proxy. (#37543) thanks @mukhtharcm. +- Telegram/Discord media upload caps: make outbound uploads honor channel `mediaMaxMb` config, raise Telegram's default media cap to 100MB, and remove MIME fallback limits that kept some Telegram uploads at 16MB. Thanks @vincentkoc. - Skills/nano-banana-pro resolution override: respect explicit `--resolution` values during image editing and only auto-detect output size from input images when the flag is omitted. (#36880) Thanks @shuofengzhang and @vincentkoc. ## 2026.3.2 diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 86e80430f7b..8266cf4c26e 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1194,6 +1194,7 @@ High-signal Discord fields: - delivery: `textChunkLimit`, `chunkMode`, `maxLinesPerMessage` - streaming: `streaming` (legacy alias: `streamMode`), `draftChunk`, `blockStreaming`, `blockStreamingCoalesce` - media/retry: `mediaMaxMb`, `retry` + - `mediaMaxMb` caps outbound Discord uploads (default: `8MB`) - actions: `actions.*` - presence: `activity`, `status`, `activityType`, `activityUrl` - UI: `ui.components.accentColor` diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 817ae1d51d4..e975db4c357 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -724,7 +724,7 @@ curl "https://api.telegram.org/bot/getUpdates" - `channels.telegram.textChunkLimit` default is 4000. - `channels.telegram.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting. - - `channels.telegram.mediaMaxMb` (default 5) caps inbound Telegram media download/processing size. + - `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound Telegram media size. - `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies). - group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables. - DM history controls: @@ -873,7 +873,7 @@ Primary reference: - `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. - `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true). - `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). In DMs, `partial` uses native `sendMessageDraft` when available. -- `channels.telegram.mediaMaxMb`: inbound Telegram media download/processing cap (MB). +- `channels.telegram.mediaMaxMb`: inbound/outbound Telegram media cap (MB, default: 100). - `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter). - `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled. - `channels.telegram.network.dnsResultOrder`: override DNS result order (`ipv4first` or `verbatim`). Defaults to `ipv4first` on Node 22+. diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index e204be9b7b5..30559b5d55d 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -183,7 +183,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat streaming: "partial", // off | partial | block | progress (default: off) actions: { reactions: true, sendMessage: true }, reactionNotifications: "own", // off | own | all - mediaMaxMb: 5, + mediaMaxMb: 100, retry: { attempts: 3, minDelayMs: 400, diff --git a/src/discord/send.outbound.ts b/src/discord/send.outbound.ts index 533d4060ed5..8234291e7ed 100644 --- a/src/discord/send.outbound.ts +++ b/src/discord/send.outbound.ts @@ -145,6 +145,10 @@ export async function sendMessageDiscord( accountId: accountInfo.accountId, }); const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId); + const mediaMaxBytes = + typeof accountInfo.config.mediaMaxMb === "number" + ? accountInfo.config.mediaMaxMb * 1024 * 1024 + : 8 * 1024 * 1024; const textWithTables = convertMarkdownTables(text ?? "", tableMode); const textWithMentions = rewriteDiscordKnownMentions(textWithTables, { accountId: accountInfo.accountId, @@ -211,6 +215,7 @@ export async function sendMessageDiscord( mediaCaption ?? "", opts.mediaUrl, opts.mediaLocalRoots, + mediaMaxBytes, undefined, request, accountInfo.config.maxLinesPerMessage, @@ -271,6 +276,7 @@ export async function sendMessageDiscord( textWithMentions, opts.mediaUrl, opts.mediaLocalRoots, + mediaMaxBytes, opts.replyTo, request, accountInfo.config.maxLinesPerMessage, diff --git a/src/discord/send.sends-basic-channel-messages.test.ts b/src/discord/send.sends-basic-channel-messages.test.ts index 6241fce7996..58b8e3799b7 100644 --- a/src/discord/send.sends-basic-channel-messages.test.ts +++ b/src/discord/send.sends-basic-channel-messages.test.ts @@ -1,5 +1,6 @@ import { ChannelType, PermissionFlagsBits, Routes } from "discord-api-types/v10"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { loadWebMedia } from "../web/media.js"; import { __resetDiscordDirectoryCacheForTest, rememberDiscordDirectoryUser, @@ -265,6 +266,33 @@ describe("sendMessageDiscord", () => { }), }), ); + expect(loadWebMedia).toHaveBeenCalledWith( + "file:///tmp/photo.jpg", + expect.objectContaining({ maxBytes: 8 * 1024 * 1024 }), + ); + }); + + it("uses configured discord mediaMaxMb for uploads", async () => { + const { rest, postMock } = makeDiscordRest(); + postMock.mockResolvedValue({ id: "msg", channel_id: "789" }); + + await sendMessageDiscord("channel:789", "photo", { + rest, + token: "t", + mediaUrl: "file:///tmp/photo.jpg", + cfg: { + channels: { + discord: { + mediaMaxMb: 32, + }, + }, + }, + }); + + expect(loadWebMedia).toHaveBeenCalledWith( + "file:///tmp/photo.jpg", + expect.objectContaining({ maxBytes: 32 * 1024 * 1024 }), + ); }); it("sends media with empty text without content field", async () => { diff --git a/src/discord/send.shared.ts b/src/discord/send.shared.ts index fddc276fccf..a90f0ffe01f 100644 --- a/src/discord/send.shared.ts +++ b/src/discord/send.shared.ts @@ -415,6 +415,7 @@ async function sendDiscordMedia( text: string, mediaUrl: string, mediaLocalRoots: readonly string[] | undefined, + maxBytes: number | undefined, replyTo: string | undefined, request: DiscordRequest, maxLinesPerMessage?: number, @@ -423,7 +424,10 @@ async function sendDiscordMedia( chunkMode?: ChunkMode, silent?: boolean, ) { - const media = await loadWebMedia(mediaUrl, buildOutboundMediaLoadOptions({ mediaLocalRoots })); + const media = await loadWebMedia( + mediaUrl, + buildOutboundMediaLoadOptions({ maxBytes, mediaLocalRoots }), + ); const chunks = text ? buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode }) : []; const caption = chunks[0] ?? ""; const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined; diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index 723db7ae508..9549fe71986 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -262,7 +262,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { }); const useAccessGroups = cfg.commands?.useAccessGroups !== false; const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions"; - const mediaMaxBytes = (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024; + const mediaMaxBytes = (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 100) * 1024 * 1024; const logger = getChildLogger({ module: "telegram-auto-reply" }); const streamMode = resolveTelegramStreamMode(telegramCfg); const resolveGroupPolicy = (chatId: string | number) => diff --git a/src/telegram/send.test.ts b/src/telegram/send.test.ts index 78a28cd3920..59c98ea3a96 100644 --- a/src/telegram/send.test.ts +++ b/src/telegram/send.test.ts @@ -1149,6 +1149,69 @@ describe("sendMessageTelegram", () => { }); expect(res.messageId).toBe("59"); }); + + it("defaults outbound media uploads to 100MB", async () => { + const chatId = "123"; + const sendPhoto = vi.fn().mockResolvedValue({ + message_id: 60, + chat: { id: chatId }, + }); + const api = { sendPhoto } as unknown as { + sendPhoto: typeof sendPhoto; + }; + + mockLoadedMedia({ + buffer: Buffer.from("fake-image"), + contentType: "image/jpeg", + fileName: "photo.jpg", + }); + + await sendMessageTelegram(chatId, "photo", { + token: "tok", + api, + mediaUrl: "https://example.com/photo.jpg", + }); + + expect(loadWebMedia).toHaveBeenCalledWith( + "https://example.com/photo.jpg", + expect.objectContaining({ maxBytes: 100 * 1024 * 1024 }), + ); + }); + + it("uses configured telegram mediaMaxMb for outbound uploads", async () => { + const chatId = "123"; + const sendPhoto = vi.fn().mockResolvedValue({ + message_id: 61, + chat: { id: chatId }, + }); + const api = { sendPhoto } as unknown as { + sendPhoto: typeof sendPhoto; + }; + loadConfig.mockReturnValue({ + channels: { + telegram: { + mediaMaxMb: 42, + }, + }, + }); + + mockLoadedMedia({ + buffer: Buffer.from("fake-image"), + contentType: "image/jpeg", + fileName: "photo.jpg", + }); + + await sendMessageTelegram(chatId, "photo", { + token: "tok", + api, + mediaUrl: "https://example.com/photo.jpg", + }); + + expect(loadWebMedia).toHaveBeenCalledWith( + "https://example.com/photo.jpg", + expect.objectContaining({ maxBytes: 42 * 1024 * 1024 }), + ); + }); }); describe("reactMessageTelegram", () => { diff --git a/src/telegram/send.ts b/src/telegram/send.ts index b04bd792529..61292f66608 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -473,6 +473,9 @@ export async function sendMessageTelegram( verbose: opts.verbose, }); const mediaUrl = opts.mediaUrl?.trim(); + const mediaMaxBytes = + opts.maxBytes ?? + (typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb : 100) * 1024 * 1024; const replyMarkup = buildInlineKeyboard(opts.buttons); const threadParams = buildTelegramThreadReplyParams({ @@ -563,7 +566,7 @@ export async function sendMessageTelegram( const media = await loadWebMedia( mediaUrl, buildOutboundMediaLoadOptions({ - maxBytes: opts.maxBytes, + maxBytes: mediaMaxBytes, mediaLocalRoots: opts.mediaLocalRoots, }), );