diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e0835d0b..57bcf70614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai - Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus. - Telegram: ignore `` placeholder lines when extracting `MEDIA:` tool-result paths, preventing false local-file reads and dropped replies. (#18510) Thanks @yinghaosang. - Telegram: skip retries when inbound media `getFile` fails with Telegram's 20MB limit and continue processing message text, avoiding dropped messages for oversized attachments. (#18531) Thanks @brandonwise. +- Telegram: clear stored polling offsets when bot tokens change or accounts are deleted, preventing stale offsets after token rotations. (#18233) - Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x. - Agents/Tools: deliver tool-result media even when verbose tool output is off so media attachments are not dropped. (#16679) - Discord: optimize reaction notification handling to skip unnecessary message fetches in `off`/`all`/`allowlist` modes, streamline reaction routing, and improve reaction emoji formatting. (#18248) Thanks @thewilloftheshadow and @victorGPT. diff --git a/src/commands/channels.add.test.ts b/src/commands/channels.add.test.ts new file mode 100644 index 0000000000..8cabd70039 --- /dev/null +++ b/src/commands/channels.add.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { setDefaultChannelPluginRegistryForTests } from "./channel-test-helpers.js"; +import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js"; + +const configMocks = vi.hoisted(() => ({ + readConfigFileSnapshot: vi.fn(), + writeConfigFile: vi.fn().mockResolvedValue(undefined), +})); + +const offsetMocks = vi.hoisted(() => ({ + deleteTelegramUpdateOffset: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readConfigFileSnapshot: configMocks.readConfigFileSnapshot, + writeConfigFile: configMocks.writeConfigFile, + }; +}); + +vi.mock("../telegram/update-offset-store.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + deleteTelegramUpdateOffset: offsetMocks.deleteTelegramUpdateOffset, + }; +}); + +import { channelsAddCommand } from "./channels.js"; + +const runtime = createTestRuntime(); + +describe("channelsAddCommand", () => { + beforeEach(() => { + configMocks.readConfigFileSnapshot.mockReset(); + configMocks.writeConfigFile.mockClear(); + offsetMocks.deleteTelegramUpdateOffset.mockClear(); + runtime.log.mockClear(); + runtime.error.mockClear(); + runtime.exit.mockClear(); + setDefaultChannelPluginRegistryForTests(); + }); + + it("clears telegram update offsets when the token changes", async () => { + configMocks.readConfigFileSnapshot.mockResolvedValue({ + ...baseConfigSnapshot, + config: { + channels: { + telegram: { botToken: "old-token", enabled: true }, + }, + }, + }); + + await channelsAddCommand( + { channel: "telegram", account: "default", token: "new-token" }, + runtime, + { hasFlags: true }, + ); + + expect(offsetMocks.deleteTelegramUpdateOffset).toHaveBeenCalledTimes(1); + expect(offsetMocks.deleteTelegramUpdateOffset).toHaveBeenCalledWith({ accountId: "default" }); + }); + + it("does not clear telegram update offsets when the token is unchanged", async () => { + configMocks.readConfigFileSnapshot.mockResolvedValue({ + ...baseConfigSnapshot, + config: { + channels: { + telegram: { botToken: "same-token", enabled: true }, + }, + }, + }); + + await channelsAddCommand( + { channel: "telegram", account: "default", token: "same-token" }, + runtime, + { hasFlags: true }, + ); + + expect(offsetMocks.deleteTelegramUpdateOffset).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index b1b2060632..c086433be5 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -1,13 +1,15 @@ +import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.js"; +import type { ChannelChoice } from "../onboard-types.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { listChannelPluginCatalogEntries } from "../../channels/plugins/catalog.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; -import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.js"; import { writeConfigFile, type OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; +import { resolveTelegramAccount } from "../../telegram/accounts.js"; +import { deleteTelegramUpdateOffset } from "../../telegram/update-offset-store.js"; import { createClackPrompter } from "../../wizard/clack-prompter.js"; import { setupChannels } from "../onboard-channels.js"; -import type { ChannelChoice } from "../onboard-types.js"; import { ensureOnboardingPluginInstalled, reloadOnboardingPluginRegistry, @@ -209,6 +211,11 @@ export async function channelsAddCommand( return; } + const previousTelegramToken = + channel === "telegram" + ? resolveTelegramAccount({ cfg: nextConfig, accountId }).token.trim() + : ""; + nextConfig = applyChannelAccountConfig({ cfg: nextConfig, channel, @@ -216,6 +223,14 @@ export async function channelsAddCommand( input, }); + if (channel === "telegram") { + const nextTelegramToken = resolveTelegramAccount({ cfg: nextConfig, accountId }).token.trim(); + if (previousTelegramToken !== nextTelegramToken) { + // Clear stale polling offsets after Telegram token rotation. + await deleteTelegramUpdateOffset({ accountId }); + } + } + await writeConfigFile(nextConfig); runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`); }