mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix: tighten telegram media-group error handling
This commit is contained in:
@@ -288,6 +288,7 @@ Docs: https://docs.openclaw.ai
|
||||
- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky.
|
||||
- iOS/Signing: restore local auto-selected signing-team overrides during iOS project generation by wiring `.local-signing.xcconfig` into the active signing config and emitting `OPENCLAW_DEVELOPMENT_TEAM` in local signing setup. (#19993) Thanks @ngutman.
|
||||
- Telegram: unify message-like inbound handling so `message` and `channel_post` share the same dedupe/access/media pipeline and remain behaviorally consistent. (#20591) Thanks @obviyus.
|
||||
- Telegram: keep media-group processing resilient by skipping recoverable per-item download failures while still failing loud on non-recoverable media errors. (#20598) thanks @mcaxtr.
|
||||
- Telegram/Agents: gate exec/bash tool-failure warnings behind verbose mode so default Telegram replies stay clean while verbose sessions still surface diagnostics. (#20560) Thanks @obviyus.
|
||||
- Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`<chatId>:topic:<threadId>`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi.
|
||||
- Telegram/DM routing: prevent DM inbound origin metadata from leaking into main-session `lastRoute` updates and normalize DM `lastRoute.to` to provider-prefixed `telegram:<chatId>`. (#19491) thanks @guirguispierre.
|
||||
|
||||
@@ -20,6 +20,7 @@ import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
||||
import type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
|
||||
import { danger, logVerbose, warn } from "../globals.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
import { MediaFetchError } from "../media/fetch.js";
|
||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||
@@ -61,6 +62,15 @@ import {
|
||||
import { buildInlineKeyboard } from "./send.js";
|
||||
import { wasSentByBot } from "./sent-message-cache.js";
|
||||
|
||||
function isMediaSizeLimitError(err: unknown): boolean {
|
||||
const errMsg = String(err);
|
||||
return errMsg.includes("exceeds") && errMsg.includes("MB limit");
|
||||
}
|
||||
|
||||
function isRecoverableMediaGroupError(err: unknown): boolean {
|
||||
return err instanceof MediaFetchError || isMediaSizeLimitError(err);
|
||||
}
|
||||
|
||||
export const registerTelegramHandlers = ({
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -274,6 +284,9 @@ export const registerTelegramHandlers = ({
|
||||
try {
|
||||
media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
|
||||
} catch (mediaErr) {
|
||||
if (!isRecoverableMediaGroupError(mediaErr)) {
|
||||
throw mediaErr;
|
||||
}
|
||||
runtime.log?.(
|
||||
warn(`media group: skipping photo that failed to fetch: ${String(mediaErr)}`),
|
||||
);
|
||||
@@ -671,8 +684,7 @@ export const registerTelegramHandlers = ({
|
||||
try {
|
||||
media = await resolveMedia(ctx, mediaMaxBytes, opts.token, opts.proxyFetch);
|
||||
} catch (mediaErr) {
|
||||
const errMsg = String(mediaErr);
|
||||
if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) {
|
||||
if (isMediaSizeLimitError(mediaErr)) {
|
||||
if (sendOversizeWarning) {
|
||||
const limitMb = Math.round(mediaMaxBytes / (1024 * 1024));
|
||||
await withTelegramApiErrorLogging({
|
||||
@@ -684,7 +696,7 @@ export const registerTelegramHandlers = ({
|
||||
}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
logger.warn({ chatId, error: errMsg }, oversizeLogMessage);
|
||||
logger.warn({ chatId, error: String(mediaErr) }, oversizeLogMessage);
|
||||
return;
|
||||
}
|
||||
throw mediaErr;
|
||||
|
||||
@@ -1973,6 +1973,89 @@ describe("createTelegramBot", () => {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
it("drops the media group when a non-recoverable media error occurs", async () => {
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "open",
|
||||
groups: {
|
||||
"-100777111222": {
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async () =>
|
||||
new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
);
|
||||
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
try {
|
||||
createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS });
|
||||
const handler = getOnHandler("channel_post") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
const first = handler({
|
||||
channelPost: {
|
||||
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
|
||||
message_id: 501,
|
||||
caption: "fatal album",
|
||||
date: 1736380800,
|
||||
media_group_id: "fatal-album-1",
|
||||
photo: [{ file_id: "p1" }],
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ file_path: "photos/p1.jpg" }),
|
||||
});
|
||||
|
||||
const second = handler({
|
||||
channelPost: {
|
||||
chat: { id: -100777111222, type: "channel", title: "Wake Channel" },
|
||||
message_id: 502,
|
||||
date: 1736380801,
|
||||
media_group_id: "fatal-album-1",
|
||||
photo: [{ file_id: "p2" }],
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
await Promise.all([first, second]);
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
|
||||
const flushTimerCallIndex = setTimeoutSpy.mock.calls.findLastIndex(
|
||||
(call) => call[1] === TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs,
|
||||
);
|
||||
const flushTimer =
|
||||
flushTimerCallIndex >= 0
|
||||
? (setTimeoutSpy.mock.calls[flushTimerCallIndex]?.[0] as (() => unknown) | undefined)
|
||||
: undefined;
|
||||
// Cancel the real timer so it cannot fire a second time after we manually invoke it.
|
||||
if (flushTimerCallIndex >= 0) {
|
||||
clearTimeout(
|
||||
setTimeoutSpy.mock.results[flushTimerCallIndex]?.value as ReturnType<typeof setTimeout>,
|
||||
);
|
||||
}
|
||||
expect(flushTimer).toBeTypeOf("function");
|
||||
await flushTimer?.();
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
setTimeoutSpy.mockRestore();
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
it("dedupes duplicate message updates by update_id", async () => {
|
||||
onSpy.mockReset();
|
||||
replySpy.mockReset();
|
||||
|
||||
Reference in New Issue
Block a user