fix(telegram): degrade command sync on BOT_COMMANDS_TOO_MUCH

When Telegram rejects native command registration for excessive commands, progressively retry with fewer commands instead of hard-failing startup.

Made-with: Cursor
This commit is contained in:
SidQin-cyber
2026-02-26 20:22:10 +08:00
parent 46eba86b45
commit a02c40483e
2 changed files with 92 additions and 5 deletions

View File

@@ -86,4 +86,42 @@ describe("bot-native-command-menu", () => {
expect(callOrder).toEqual(["delete", "set"]);
});
it("retries with fewer commands on BOT_COMMANDS_TOO_MUCH", async () => {
const deleteMyCommands = vi.fn(async () => undefined);
const setMyCommands = vi
.fn()
.mockRejectedValueOnce(new Error("400: Bad Request: BOT_COMMANDS_TOO_MUCH"))
.mockResolvedValue(undefined);
const runtimeLog = vi.fn();
syncTelegramMenuCommands({
bot: {
api: {
deleteMyCommands,
setMyCommands,
},
} as unknown as Parameters<typeof syncTelegramMenuCommands>[0]["bot"],
runtime: {
log: runtimeLog,
error: vi.fn(),
exit: vi.fn(),
} as Parameters<typeof syncTelegramMenuCommands>[0]["runtime"],
commandsToRegister: Array.from({ length: 100 }, (_, i) => ({
command: `cmd_${i}`,
description: `Command ${i}`,
})),
});
await vi.waitFor(() => {
expect(setMyCommands).toHaveBeenCalledTimes(2);
});
const firstPayload = setMyCommands.mock.calls[0]?.[0] as Array<unknown>;
const secondPayload = setMyCommands.mock.calls[1]?.[0] as Array<unknown>;
expect(firstPayload).toHaveLength(100);
expect(secondPayload).toHaveLength(80);
expect(runtimeLog).toHaveBeenCalledWith(
"Telegram rejected 100 commands (BOT_COMMANDS_TOO_MUCH); retrying with 80.",
);
});
});

View File

@@ -7,6 +7,7 @@ import type { RuntimeEnv } from "../runtime.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
export const TELEGRAM_MAX_COMMANDS = 100;
const TELEGRAM_COMMAND_RETRY_RATIO = 0.8;
export type TelegramMenuCommand = {
command: string;
@@ -18,6 +19,31 @@ type TelegramPluginCommandSpec = {
description: string;
};
function isBotCommandsTooMuchError(err: unknown): boolean {
if (!err) {
return false;
}
const pattern = /\bBOT_COMMANDS_TOO_MUCH\b/i;
if (typeof err === "string") {
return pattern.test(err);
}
if (err instanceof Error) {
if (pattern.test(err.message)) {
return true;
}
}
if (typeof err === "object") {
const maybe = err as { description?: unknown; message?: unknown };
if (typeof maybe.description === "string" && pattern.test(maybe.description)) {
return true;
}
if (typeof maybe.message === "string" && pattern.test(maybe.message)) {
return true;
}
}
return false;
}
export function buildPluginTelegramMenuCommands(params: {
specs: TelegramPluginCommandSpec[];
existingCommands: Set<string>;
@@ -93,11 +119,34 @@ export function syncTelegramMenuCommands(params: {
return;
}
await withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands(commandsToRegister),
});
let retryCommands = commandsToRegister;
while (retryCommands.length > 0) {
try {
await withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands(retryCommands),
});
return;
} catch (err) {
if (!isBotCommandsTooMuchError(err)) {
throw err;
}
const nextCount = Math.floor(retryCommands.length * TELEGRAM_COMMAND_RETRY_RATIO);
const reducedCount =
nextCount < retryCommands.length ? nextCount : retryCommands.length - 1;
if (reducedCount <= 0) {
runtime.error?.(
"Telegram rejected native command registration (BOT_COMMANDS_TOO_MUCH); leaving menu empty. Reduce commands or disable channels.telegram.commands.native.",
);
return;
}
runtime.log?.(
`Telegram rejected ${retryCommands.length} commands (BOT_COMMANDS_TOO_MUCH); retrying with ${reducedCount}.`,
);
retryCommands = retryCommands.slice(0, reducedCount);
}
}
};
void sync().catch((err) => {