Telegram/Discord: honor outbound mediaMaxMb uploads (#38065)

* Telegram: default media cap to 100MB

* Telegram: honor outbound mediaMaxMb

* Discord: add shared media upload cap

* Discord: pass mediaMaxMb to outbound sends

* Telegram: cover outbound media cap sends

* Discord: cover media upload cap config

* Docs: update Telegram media cap guide

* Docs: update Telegram config reference

* Changelog: note media upload cap fix

* Docs: note Discord upload cap behavior
This commit is contained in:
Vincent Koc 2026-03-06 10:53:06 -05:00 committed by GitHub
parent 9917a3fb77
commit 9c1786bdd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 112 additions and 6 deletions

View File

@ -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

View File

@ -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`

View File

@ -724,7 +724,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
<Accordion title="Limits, retry, and CLI targets">
- `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+.

View File

@ -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,

View File

@ -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,

View File

@ -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 () => {

View File

@ -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;

View File

@ -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) =>

View File

@ -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", () => {

View File

@ -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,
}),
);