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:
parent
9917a3fb77
commit
9c1786bdd6
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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+.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user