mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 09:30:59 +00:00
fix(telegram): scope command-sync hash cache by bot identity (#32059)
This commit is contained in:
@@ -100,6 +100,8 @@ describe("bot-native-command-menu", () => {
|
||||
} as unknown as Parameters<typeof syncTelegramMenuCommands>[0]["bot"],
|
||||
runtime: {} as Parameters<typeof syncTelegramMenuCommands>[0]["runtime"],
|
||||
commandsToRegister: [{ command: "cmd", description: "Command" }],
|
||||
accountId: `test-delete-${Date.now()}`,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
@@ -143,6 +145,7 @@ describe("bot-native-command-menu", () => {
|
||||
>[0]["runtime"],
|
||||
commandsToRegister: commands,
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
@@ -159,6 +162,7 @@ describe("bot-native-command-menu", () => {
|
||||
>[0]["runtime"],
|
||||
commandsToRegister: commands,
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
@@ -169,6 +173,41 @@ describe("bot-native-command-menu", () => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not reuse cached hash across different bot identities", async () => {
|
||||
const deleteMyCommands = vi.fn(async () => undefined);
|
||||
const setMyCommands = vi.fn(async () => undefined);
|
||||
const runtimeLog = vi.fn();
|
||||
const accountId = `test-bot-identity-${Date.now()}`;
|
||||
const commands = [{ command: "same", description: "Same" }];
|
||||
|
||||
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: commands,
|
||||
accountId,
|
||||
botIdentity: "token-bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(1));
|
||||
|
||||
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: commands,
|
||||
accountId,
|
||||
botIdentity: "token-bot-b",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
expect(runtimeLog).not.toHaveBeenCalledWith("telegram: command menu unchanged; skipping sync");
|
||||
});
|
||||
|
||||
it("retries with fewer commands on BOT_COMMANDS_TOO_MUCH", async () => {
|
||||
const deleteMyCommands = vi.fn(async () => undefined);
|
||||
const setMyCommands = vi
|
||||
@@ -193,6 +232,8 @@ describe("bot-native-command-menu", () => {
|
||||
command: `cmd_${i}`,
|
||||
description: `Command ${i}`,
|
||||
})),
|
||||
accountId: `test-retry-${Date.now()}`,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
|
||||
@@ -112,25 +112,38 @@ export function hashCommandList(commands: TelegramMenuCommand[]): string {
|
||||
return createHash("sha256").update(JSON.stringify(sorted)).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
function resolveCommandHashPath(accountId?: string): string {
|
||||
const stateDir = resolveStateDir(process.env, os.homedir);
|
||||
const normalized = accountId?.trim().replace(/[^a-z0-9._-]+/gi, "_") || "default";
|
||||
return path.join(stateDir, "telegram", `command-hash-${normalized}.txt`);
|
||||
function hashBotIdentity(botIdentity?: string): string {
|
||||
const normalized = botIdentity?.trim();
|
||||
if (!normalized) {
|
||||
return "no-bot";
|
||||
}
|
||||
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
async function readCachedCommandHash(accountId?: string): Promise<string | null> {
|
||||
function resolveCommandHashPath(accountId?: string, botIdentity?: string): string {
|
||||
const stateDir = resolveStateDir(process.env, os.homedir);
|
||||
const normalizedAccount = accountId?.trim().replace(/[^a-z0-9._-]+/gi, "_") || "default";
|
||||
const botHash = hashBotIdentity(botIdentity);
|
||||
return path.join(stateDir, "telegram", `command-hash-${normalizedAccount}-${botHash}.txt`);
|
||||
}
|
||||
|
||||
async function readCachedCommandHash(
|
||||
accountId?: string,
|
||||
botIdentity?: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
return (await fs.readFile(resolveCommandHashPath(accountId), "utf-8")).trim();
|
||||
return (await fs.readFile(resolveCommandHashPath(accountId, botIdentity), "utf-8")).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeCachedCommandHash(accountId?: string, hash?: string): Promise<void> {
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
const filePath = resolveCommandHashPath(accountId);
|
||||
async function writeCachedCommandHash(
|
||||
accountId: string | undefined,
|
||||
botIdentity: string | undefined,
|
||||
hash: string,
|
||||
): Promise<void> {
|
||||
const filePath = resolveCommandHashPath(accountId, botIdentity);
|
||||
try {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, hash, "utf-8");
|
||||
@@ -145,15 +158,16 @@ export function syncTelegramMenuCommands(params: {
|
||||
runtime: RuntimeEnv;
|
||||
commandsToRegister: TelegramMenuCommand[];
|
||||
accountId?: string;
|
||||
botIdentity?: string;
|
||||
}): void {
|
||||
const { bot, runtime, commandsToRegister, accountId } = params;
|
||||
const { bot, runtime, commandsToRegister, accountId, botIdentity } = params;
|
||||
const sync = async () => {
|
||||
// Skip sync if the command list hasn't changed since the last successful
|
||||
// sync. This prevents hitting Telegram's 429 rate limit when the gateway
|
||||
// is restarted several times in quick succession.
|
||||
// See: openclaw/openclaw#32017
|
||||
const currentHash = hashCommandList(commandsToRegister);
|
||||
const cachedHash = await readCachedCommandHash(accountId);
|
||||
const cachedHash = await readCachedCommandHash(accountId, botIdentity);
|
||||
if (cachedHash === currentHash) {
|
||||
runtime.log?.("telegram: command menu unchanged; skipping sync");
|
||||
return;
|
||||
@@ -169,7 +183,7 @@ export function syncTelegramMenuCommands(params: {
|
||||
}
|
||||
|
||||
if (commandsToRegister.length === 0) {
|
||||
await writeCachedCommandHash(accountId, currentHash);
|
||||
await writeCachedCommandHash(accountId, botIdentity, currentHash);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,7 +195,7 @@ export function syncTelegramMenuCommands(params: {
|
||||
runtime,
|
||||
fn: () => bot.api.setMyCommands(retryCommands),
|
||||
});
|
||||
await writeCachedCommandHash(accountId, currentHash);
|
||||
await writeCachedCommandHash(accountId, botIdentity, currentHash);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (!isBotCommandsTooMuchError(err)) {
|
||||
|
||||
@@ -397,7 +397,13 @@ export const registerTelegramNativeCommands = ({
|
||||
}
|
||||
// Telegram only limits the setMyCommands payload (menu entries).
|
||||
// Keep hidden commands callable by registering handlers for the full catalog.
|
||||
syncTelegramMenuCommands({ bot, runtime, commandsToRegister, accountId });
|
||||
syncTelegramMenuCommands({
|
||||
bot,
|
||||
runtime,
|
||||
commandsToRegister,
|
||||
accountId,
|
||||
botIdentity: opts.token,
|
||||
});
|
||||
|
||||
const resolveCommandRuntimeContext = (params: {
|
||||
msg: NonNullable<TelegramNativeCommandContext["message"]>;
|
||||
|
||||
Reference in New Issue
Block a user