mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 15:14:45 +00:00
fix(telegram): cache startup bot info
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/sessions: estimate context usage from local/OpenAI-compatible transcripts when provider usage telemetry is missing, so status no longer shows empty usage for real local-model sessions. Fixes #73990. (#82317) Thanks @giodl73-repo.
|
||||
- Agents/sessions: preserve fresh post-compaction token snapshots across stale usage updates, preventing repeated auto-compaction after every message. Fixes #82576. (#82578) Thanks @njuboy11.
|
||||
- Agents/OpenAI Responses: log redacted diagnostics for detail-less `response.failed` events while preserving failed response ids, so operators can correlate provider-side failures. Fixes #82558.
|
||||
- Telegram: cache successful startup bot identity by account and token fingerprint for up to 24 hours, so restarts can skip redundant `getMe` probes during Telegram API slow periods without permanently pinning renamed bots. Refs #82525.
|
||||
- Gateway/sessions: discard stale metadata when recreating dead main session rows, so replacement sessions do not inherit old labels or transcript paths.
|
||||
- Codex app-server: mark native context compaction completion events as successful, preventing false "Compaction incomplete" notices after successful Codex-managed compaction. Fixes #82470. (#81593) Thanks @Kyzcreig.
|
||||
- Codex app-server: keep long-running turns alive while current-turn approvals, user input, dynamic tools, and notifications make progress, and carry that progress into the outer run timeout. (#82601) Thanks @100yenadmin.
|
||||
|
||||
@@ -76,6 +76,7 @@ openclaw pairing approve telegram <CODE>
|
||||
|
||||
<Note>
|
||||
Token resolution order is account-aware. In practice, config values win over env fallback, and `TELEGRAM_BOT_TOKEN` only applies to the default account.
|
||||
After a successful startup, OpenClaw caches the bot identity in the state directory for up to 24 hours so restarts can avoid an extra Telegram `getMe` call; changing or removing the token clears that cache.
|
||||
</Note>
|
||||
|
||||
## Telegram side settings
|
||||
|
||||
130
extensions/telegram/src/bot-info-cache.test.ts
Normal file
130
extensions/telegram/src/bot-info-cache.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
deleteCachedTelegramBotInfo,
|
||||
readCachedTelegramBotInfo,
|
||||
resolveTelegramBotInfoCachePath,
|
||||
TELEGRAM_BOT_INFO_CACHE_MAX_AGE_MS,
|
||||
writeCachedTelegramBotInfo,
|
||||
} from "./bot-info-cache.js";
|
||||
import type { TelegramBotInfo } from "./bot-info.js";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
const botInfo: TelegramBotInfo = {
|
||||
id: 123456,
|
||||
is_bot: true,
|
||||
first_name: "OpenClaw",
|
||||
username: "openclaw_bot",
|
||||
can_join_groups: true,
|
||||
can_read_all_group_messages: false,
|
||||
can_manage_bots: false,
|
||||
supports_inline_queries: false,
|
||||
can_connect_to_business: false,
|
||||
has_main_web_app: false,
|
||||
has_topics_enabled: false,
|
||||
allows_users_to_create_topics: false,
|
||||
};
|
||||
|
||||
async function useTempStateDir(): Promise<NodeJS.ProcessEnv> {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-tg-bot-info-"));
|
||||
tempRoots.push(stateDir);
|
||||
return { ...process.env, OPENCLAW_STATE_DIR: stateDir };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
await Promise.all(
|
||||
tempRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Telegram bot info cache", () => {
|
||||
it("reads botInfo for the same account and bot token", async () => {
|
||||
const env = await useTempStateDir();
|
||||
|
||||
await writeCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:secret",
|
||||
botInfo,
|
||||
env,
|
||||
});
|
||||
|
||||
await expect(
|
||||
readCachedTelegramBotInfo({ accountId: "ops", botToken: "123456:secret", env }),
|
||||
).resolves.toMatchObject({ botInfo });
|
||||
});
|
||||
|
||||
it("ignores botInfo written for a different token fingerprint", async () => {
|
||||
const env = await useTempStateDir();
|
||||
|
||||
await writeCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:old-secret",
|
||||
botInfo,
|
||||
env,
|
||||
});
|
||||
|
||||
await expect(
|
||||
readCachedTelegramBotInfo({ accountId: "ops", botToken: "123456:new-secret", env }),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("treats stale botInfo as a cache miss", async () => {
|
||||
const env = await useTempStateDir();
|
||||
|
||||
await writeCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:secret",
|
||||
botInfo,
|
||||
env,
|
||||
});
|
||||
|
||||
await expect(
|
||||
readCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:secret",
|
||||
env,
|
||||
now: new Date(Date.now() + TELEGRAM_BOT_INFO_CACHE_MAX_AGE_MS + 1),
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("deletes cached botInfo for an account", async () => {
|
||||
const env = await useTempStateDir();
|
||||
|
||||
await writeCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:secret",
|
||||
botInfo,
|
||||
env,
|
||||
});
|
||||
await deleteCachedTelegramBotInfo({ accountId: "ops", env });
|
||||
|
||||
await expect(
|
||||
readCachedTelegramBotInfo({ accountId: "ops", botToken: "123456:secret", env }),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("treats malformed persisted botInfo as a cache miss", async () => {
|
||||
const env = await useTempStateDir();
|
||||
const filePath = resolveTelegramBotInfoCachePath("ops", env);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
tokenFingerprint: "not-the-token",
|
||||
fetchedAt: new Date().toISOString(),
|
||||
botInfo: { id: 123456, is_bot: true },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(
|
||||
readCachedTelegramBotInfo({ accountId: "ops", botToken: "123456:secret", env }),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
135
extensions/telegram/src/bot-info-cache.ts
Normal file
135
extensions/telegram/src/bot-info-cache.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { normalizeTelegramBotInfo, type TelegramBotInfo } from "./bot-info.js";
|
||||
import { fingerprintTelegramBotToken } from "./token-fingerprint.js";
|
||||
|
||||
const STORE_VERSION = 1;
|
||||
export const TELEGRAM_BOT_INFO_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
type TelegramBotInfoCacheState = {
|
||||
version: number;
|
||||
tokenFingerprint: string;
|
||||
fetchedAt: string;
|
||||
botInfo: TelegramBotInfo;
|
||||
};
|
||||
|
||||
export type CachedTelegramBotInfo = {
|
||||
botInfo: TelegramBotInfo;
|
||||
fetchedAt: string;
|
||||
};
|
||||
|
||||
function normalizeAccountId(accountId?: string) {
|
||||
const trimmed = accountId?.trim();
|
||||
if (!trimmed) {
|
||||
return "default";
|
||||
}
|
||||
return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
|
||||
}
|
||||
|
||||
function fingerprintFromToken(botToken?: string): string | null {
|
||||
const trimmed = botToken?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
return fingerprintTelegramBotToken(trimmed);
|
||||
}
|
||||
|
||||
export function resolveTelegramBotInfoCachePath(
|
||||
accountId?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
const stateDir = resolveStateDir(env, os.homedir);
|
||||
return path.join(stateDir, "telegram", `bot-info-${normalizeAccountId(accountId)}.json`);
|
||||
}
|
||||
|
||||
function parseCachedTelegramBotInfo(value: unknown): TelegramBotInfoCacheState | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const state = value as {
|
||||
version?: unknown;
|
||||
tokenFingerprint?: unknown;
|
||||
fetchedAt?: unknown;
|
||||
botInfo?: unknown;
|
||||
};
|
||||
if (
|
||||
state.version !== STORE_VERSION ||
|
||||
typeof state.tokenFingerprint !== "string" ||
|
||||
typeof state.fetchedAt !== "string" ||
|
||||
Number.isNaN(Date.parse(state.fetchedAt))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const botInfo = normalizeTelegramBotInfo(state.botInfo);
|
||||
if (!botInfo) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
version: STORE_VERSION,
|
||||
tokenFingerprint: state.tokenFingerprint,
|
||||
fetchedAt: state.fetchedAt,
|
||||
botInfo,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readCachedTelegramBotInfo(params: {
|
||||
accountId?: string;
|
||||
botToken?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
now?: Date;
|
||||
}): Promise<CachedTelegramBotInfo | null> {
|
||||
const tokenFingerprint = fingerprintFromToken(params.botToken);
|
||||
if (!tokenFingerprint) {
|
||||
return null;
|
||||
}
|
||||
const filePath = resolveTelegramBotInfoCachePath(params.accountId, params.env);
|
||||
const { value } = await readJsonFileWithFallback<unknown>(filePath, null);
|
||||
const parsed = parseCachedTelegramBotInfo(value);
|
||||
if (!parsed || parsed.tokenFingerprint !== tokenFingerprint) {
|
||||
return null;
|
||||
}
|
||||
const fetchedAtMs = Date.parse(parsed.fetchedAt);
|
||||
const nowMs = params.now?.getTime() ?? Date.now();
|
||||
if (nowMs - fetchedAtMs > TELEGRAM_BOT_INFO_CACHE_MAX_AGE_MS) {
|
||||
return null;
|
||||
}
|
||||
return { botInfo: parsed.botInfo, fetchedAt: parsed.fetchedAt };
|
||||
}
|
||||
|
||||
export async function writeCachedTelegramBotInfo(params: {
|
||||
accountId?: string;
|
||||
botToken: string;
|
||||
botInfo: TelegramBotInfo;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
const tokenFingerprint = fingerprintFromToken(params.botToken);
|
||||
if (!tokenFingerprint) {
|
||||
return;
|
||||
}
|
||||
const filePath = resolveTelegramBotInfoCachePath(params.accountId, params.env);
|
||||
const payload: TelegramBotInfoCacheState = {
|
||||
version: STORE_VERSION,
|
||||
tokenFingerprint,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
botInfo: params.botInfo,
|
||||
};
|
||||
await writeJsonFileAtomically(filePath, payload);
|
||||
}
|
||||
|
||||
export async function deleteCachedTelegramBotInfo(params: {
|
||||
accountId?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
const filePath = resolveTelegramBotInfoCachePath(params.accountId, params.env);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (err) {
|
||||
if ((err as { code?: string }).code === "ENOENT") {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,38 @@ export type TelegramBotInfo = {
|
||||
has_topics_enabled: boolean;
|
||||
allows_users_to_create_topics: boolean;
|
||||
};
|
||||
|
||||
function normalizeBoolean(value: unknown): boolean | null {
|
||||
return typeof value === "boolean" ? value : null;
|
||||
}
|
||||
|
||||
export function normalizeTelegramBotInfo(value: unknown): TelegramBotInfo | undefined {
|
||||
if (!value || typeof value !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const bot = value as Record<string, unknown>;
|
||||
if (
|
||||
typeof bot.id !== "number" ||
|
||||
bot.is_bot !== true ||
|
||||
typeof bot.first_name !== "string" ||
|
||||
typeof bot.username !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: bot.id,
|
||||
is_bot: true,
|
||||
first_name: bot.first_name,
|
||||
username: bot.username,
|
||||
...(typeof bot.last_name === "string" ? { last_name: bot.last_name } : {}),
|
||||
...(typeof bot.language_code === "string" ? { language_code: bot.language_code } : {}),
|
||||
can_join_groups: normalizeBoolean(bot.can_join_groups) ?? false,
|
||||
can_read_all_group_messages: normalizeBoolean(bot.can_read_all_group_messages) ?? false,
|
||||
can_manage_bots: normalizeBoolean(bot.can_manage_bots) ?? false,
|
||||
supports_inline_queries: normalizeBoolean(bot.supports_inline_queries) ?? false,
|
||||
can_connect_to_business: normalizeBoolean(bot.can_connect_to_business) ?? false,
|
||||
has_main_web_app: normalizeBoolean(bot.has_main_web_app) ?? false,
|
||||
has_topics_enabled: normalizeBoolean(bot.has_topics_enabled) ?? false,
|
||||
allows_users_to_create_topics: normalizeBoolean(bot.allows_users_to_create_topics) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
createPluginRuntimeMock,
|
||||
createStartAccountContext,
|
||||
} from "openclaw/plugin-sdk/channel-test-helpers";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { readCachedTelegramBotInfo, writeCachedTelegramBotInfo } from "./bot-info-cache.js";
|
||||
import type { TelegramBotInfo } from "./bot-info.js";
|
||||
import { telegramPlugin } from "./channel.js";
|
||||
import type { TelegramMonitorFn } from "./monitor.types.js";
|
||||
import {
|
||||
@@ -18,6 +23,29 @@ import { resetTelegramStartupProbeLimiterForTests } from "./startup-probe-limite
|
||||
const probeTelegram = vi.fn();
|
||||
const monitorTelegramProvider = vi.fn();
|
||||
const sendMessageTelegram = vi.fn();
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
const startupBotInfo: TelegramBotInfo = {
|
||||
id: 123456,
|
||||
is_bot: true,
|
||||
first_name: "OpenClaw",
|
||||
username: "openclaw_bot",
|
||||
can_join_groups: true,
|
||||
can_read_all_group_messages: false,
|
||||
can_manage_bots: false,
|
||||
supports_inline_queries: false,
|
||||
can_connect_to_business: false,
|
||||
has_main_web_app: false,
|
||||
has_topics_enabled: false,
|
||||
allows_users_to_create_topics: false,
|
||||
};
|
||||
|
||||
async function useTempStateDir(): Promise<string> {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-tg-channel-"));
|
||||
tempRoots.push(stateDir);
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
return stateDir;
|
||||
}
|
||||
|
||||
function installTelegramRuntime() {
|
||||
const runtime = createPluginRuntimeMock();
|
||||
@@ -112,18 +140,22 @@ async function waitForCondition(check: () => boolean, message: string, attempts
|
||||
if (check()) {
|
||||
return;
|
||||
}
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
clearTelegramRuntime();
|
||||
resetTelegramPollingLeasesForTests();
|
||||
resetTelegramStartupProbeLimiterForTests();
|
||||
probeTelegram.mockReset();
|
||||
monitorTelegramProvider.mockReset();
|
||||
sendMessageTelegram.mockReset();
|
||||
vi.unstubAllEnvs();
|
||||
await Promise.all(
|
||||
tempRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("telegramPlugin gateway startup", () => {
|
||||
@@ -200,37 +232,67 @@ describe("telegramPlugin gateway startup", () => {
|
||||
|
||||
it("passes successful startup probe botInfo into the polling monitor", async () => {
|
||||
installTelegramRuntime();
|
||||
const botInfo = {
|
||||
id: 123456,
|
||||
is_bot: true,
|
||||
first_name: "OpenClaw",
|
||||
username: "openclaw_bot",
|
||||
can_join_groups: true,
|
||||
can_read_all_group_messages: false,
|
||||
can_manage_bots: false,
|
||||
supports_inline_queries: false,
|
||||
can_connect_to_business: false,
|
||||
has_main_web_app: false,
|
||||
has_topics_enabled: false,
|
||||
allows_users_to_create_topics: false,
|
||||
} as const;
|
||||
probeTelegram.mockResolvedValue({
|
||||
ok: true,
|
||||
status: null,
|
||||
error: null,
|
||||
elapsedMs: 12,
|
||||
bot: {
|
||||
id: botInfo.id,
|
||||
username: botInfo.username,
|
||||
id: startupBotInfo.id,
|
||||
username: startupBotInfo.username,
|
||||
},
|
||||
botInfo,
|
||||
botInfo: startupBotInfo,
|
||||
});
|
||||
monitorTelegramProvider.mockResolvedValue(undefined);
|
||||
|
||||
const { task } = startTelegramAccount();
|
||||
|
||||
await expect(task).resolves.toBeUndefined();
|
||||
expect(latestMonitorOptions().botInfo).toBe(botInfo);
|
||||
expect(latestMonitorOptions().botInfo).toBe(startupBotInfo);
|
||||
});
|
||||
|
||||
it("caches successful startup probe botInfo for later restarts", async () => {
|
||||
await useTempStateDir();
|
||||
installTelegramRuntime();
|
||||
probeTelegram.mockResolvedValue({
|
||||
ok: true,
|
||||
status: null,
|
||||
error: null,
|
||||
elapsedMs: 12,
|
||||
bot: {
|
||||
id: startupBotInfo.id,
|
||||
username: startupBotInfo.username,
|
||||
},
|
||||
botInfo: startupBotInfo,
|
||||
});
|
||||
monitorTelegramProvider.mockResolvedValue(undefined);
|
||||
|
||||
const { task } = startTelegramAccount("ops");
|
||||
|
||||
await expect(task).resolves.toBeUndefined();
|
||||
await expect(
|
||||
readCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:bad-token",
|
||||
}),
|
||||
).resolves.toMatchObject({ botInfo: startupBotInfo });
|
||||
});
|
||||
|
||||
it("uses cached startup botInfo without calling getMe", async () => {
|
||||
await useTempStateDir();
|
||||
installTelegramRuntime();
|
||||
await writeCachedTelegramBotInfo({
|
||||
accountId: "ops",
|
||||
botToken: "123456:bad-token",
|
||||
botInfo: startupBotInfo,
|
||||
});
|
||||
monitorTelegramProvider.mockResolvedValue(undefined);
|
||||
|
||||
const { task } = startTelegramAccount("ops");
|
||||
|
||||
await expect(task).resolves.toBeUndefined();
|
||||
expect(probeTelegram).not.toHaveBeenCalled();
|
||||
expect(latestMonitorOptions().botInfo).toEqual(startupBotInfo);
|
||||
});
|
||||
|
||||
it("honors higher per-account timeoutSeconds for startup probe", async () => {
|
||||
|
||||
@@ -40,6 +40,11 @@ import { resolveTelegramAutoThreadId } from "./action-threading.js";
|
||||
import { lookupTelegramChatId } from "./api-fetch.js";
|
||||
import { telegramApprovalCapability } from "./approval-native.js";
|
||||
import * as auditModule from "./audit.js";
|
||||
import {
|
||||
deleteCachedTelegramBotInfo,
|
||||
readCachedTelegramBotInfo,
|
||||
writeCachedTelegramBotInfo,
|
||||
} from "./bot-info-cache.js";
|
||||
import type { TelegramBotInfo } from "./bot-info.js";
|
||||
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
|
||||
import { telegramMessageActions as telegramMessageActionsImpl } from "./channel-actions.js";
|
||||
@@ -109,6 +114,48 @@ function resolveTelegramProbe() {
|
||||
);
|
||||
}
|
||||
|
||||
async function readStartupBotInfoCache(params: {
|
||||
accountId: string;
|
||||
token: string;
|
||||
log?: { debug?: (message: string) => void };
|
||||
}): Promise<TelegramBotInfo | undefined> {
|
||||
try {
|
||||
const cached = await readCachedTelegramBotInfo({
|
||||
accountId: params.accountId,
|
||||
botToken: params.token,
|
||||
});
|
||||
return cached?.botInfo;
|
||||
} catch (err) {
|
||||
if (getTelegramRuntime().logging.shouldLogVerbose()) {
|
||||
params.log?.debug?.(`[${params.accountId}] bot info cache read failed: ${String(err)}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeStartupBotInfoCache(params: {
|
||||
accountId: string;
|
||||
token: string;
|
||||
botInfo: TelegramBotInfo;
|
||||
log?: { debug?: (message: string) => void };
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await writeCachedTelegramBotInfo({
|
||||
accountId: params.accountId,
|
||||
botToken: params.token,
|
||||
botInfo: params.botInfo,
|
||||
});
|
||||
} catch (err) {
|
||||
if (getTelegramRuntime().logging.shouldLogVerbose()) {
|
||||
params.log?.debug?.(`[${params.accountId}] bot info cache write failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStartupBotInfoCache(accountId: string): Promise<void> {
|
||||
await deleteCachedTelegramBotInfo({ accountId }).catch(() => undefined);
|
||||
}
|
||||
|
||||
function resolveTelegramAuditCollector() {
|
||||
return (
|
||||
getOptionalTelegramRuntime()?.channel?.telegram?.collectTelegramUnmentionedGroupIds ??
|
||||
@@ -741,12 +788,18 @@ export const telegramPlugin = createChatChannelPlugin({
|
||||
const nextToken = resolveTelegramAccount({ cfg: nextCfg, accountId }).token.trim();
|
||||
if (previousToken !== nextToken) {
|
||||
const { deleteTelegramUpdateOffset } = await loadTelegramUpdateOffsetRuntime();
|
||||
await deleteTelegramUpdateOffset({ accountId });
|
||||
await Promise.all([
|
||||
deleteTelegramUpdateOffset({ accountId }),
|
||||
deleteStartupBotInfoCache(accountId),
|
||||
]);
|
||||
}
|
||||
},
|
||||
onAccountRemoved: async ({ accountId }) => {
|
||||
const { deleteTelegramUpdateOffset } = await loadTelegramUpdateOffsetRuntime();
|
||||
await deleteTelegramUpdateOffset({ accountId });
|
||||
await Promise.all([
|
||||
deleteTelegramUpdateOffset({ accountId }),
|
||||
deleteStartupBotInfoCache(accountId),
|
||||
]);
|
||||
},
|
||||
},
|
||||
heartbeat: {
|
||||
@@ -897,34 +950,53 @@ export const telegramPlugin = createChatChannelPlugin({
|
||||
let telegramBotLabel = "";
|
||||
let unauthorizedTokenReason: string | null = null;
|
||||
let botInfo: TelegramBotInfo | undefined;
|
||||
try {
|
||||
const probe = await withTelegramStartupProbeSlot(ctx.abortSignal, () =>
|
||||
resolveTelegramProbe()(
|
||||
token,
|
||||
resolveTelegramStartupProbeTimeoutMs(account.config.timeoutSeconds),
|
||||
{
|
||||
const cachedBotInfo = await readStartupBotInfoCache({
|
||||
accountId: account.accountId,
|
||||
token,
|
||||
log: ctx.log,
|
||||
});
|
||||
if (cachedBotInfo) {
|
||||
botInfo = cachedBotInfo;
|
||||
telegramBotLabel = ` (@${cachedBotInfo.username})`;
|
||||
} else {
|
||||
try {
|
||||
const probe = await withTelegramStartupProbeSlot(ctx.abortSignal, () =>
|
||||
resolveTelegramProbe()(
|
||||
token,
|
||||
resolveTelegramStartupProbeTimeoutMs(account.config.timeoutSeconds),
|
||||
{
|
||||
accountId: account.accountId,
|
||||
proxyUrl: account.config.proxy,
|
||||
network: account.config.network,
|
||||
apiRoot: account.config.apiRoot,
|
||||
includeWebhookInfo: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
const username = probe.ok ? probe.bot?.username?.trim() : null;
|
||||
if (username) {
|
||||
telegramBotLabel = ` (@${username})`;
|
||||
}
|
||||
botInfo = probe.ok ? probe.botInfo : undefined;
|
||||
if (probe.ok && probe.botInfo) {
|
||||
await writeStartupBotInfoCache({
|
||||
accountId: account.accountId,
|
||||
proxyUrl: account.config.proxy,
|
||||
network: account.config.network,
|
||||
apiRoot: account.config.apiRoot,
|
||||
includeWebhookInfo: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
const username = probe.ok ? probe.bot?.username?.trim() : null;
|
||||
if (username) {
|
||||
telegramBotLabel = ` (@${username})`;
|
||||
}
|
||||
botInfo = probe.ok ? probe.botInfo : undefined;
|
||||
if (!probe.ok && probe.status === 401) {
|
||||
unauthorizedTokenReason = formatTelegramUnauthorizedTokenError(account);
|
||||
}
|
||||
} catch (err) {
|
||||
if (ctx.abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
if (getTelegramRuntime().logging.shouldLogVerbose()) {
|
||||
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
|
||||
token,
|
||||
botInfo: probe.botInfo,
|
||||
log: ctx.log,
|
||||
});
|
||||
}
|
||||
if (!probe.ok && probe.status === 401) {
|
||||
await deleteStartupBotInfoCache(account.accountId);
|
||||
unauthorizedTokenReason = formatTelegramUnauthorizedTokenError(account);
|
||||
}
|
||||
} catch (err) {
|
||||
if (ctx.abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
if (getTelegramRuntime().logging.shouldLogVerbose()) {
|
||||
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unauthorizedTokenReason) {
|
||||
@@ -1020,6 +1092,9 @@ export const telegramPlugin = createChatChannelPlugin({
|
||||
afterWrite: { mode: "auto" },
|
||||
});
|
||||
}
|
||||
if (cleared || loggedOut) {
|
||||
await deleteStartupBotInfoCache(accountId);
|
||||
}
|
||||
return { cleared, envToken: Boolean(envToken), loggedOut };
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { fetchWithTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { TelegramBotInfo } from "./bot-info.js";
|
||||
import { normalizeTelegramBotInfo, type TelegramBotInfo } from "./bot-info.js";
|
||||
import {
|
||||
resolveTelegramApiBase,
|
||||
resolveTelegramTransport,
|
||||
@@ -114,37 +114,6 @@ function normalizeBoolean(value: unknown): boolean | null {
|
||||
return typeof value === "boolean" ? value : null;
|
||||
}
|
||||
|
||||
function normalizeTelegramBotInfo(value: unknown): TelegramBotInfo | undefined {
|
||||
if (!value || typeof value !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const bot = value as Record<string, unknown>;
|
||||
if (
|
||||
typeof bot.id !== "number" ||
|
||||
bot.is_bot !== true ||
|
||||
typeof bot.first_name !== "string" ||
|
||||
typeof bot.username !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: bot.id,
|
||||
is_bot: true,
|
||||
first_name: bot.first_name,
|
||||
username: bot.username,
|
||||
...(typeof bot.last_name === "string" ? { last_name: bot.last_name } : {}),
|
||||
...(typeof bot.language_code === "string" ? { language_code: bot.language_code } : {}),
|
||||
can_join_groups: normalizeBoolean(bot.can_join_groups) ?? false,
|
||||
can_read_all_group_messages: normalizeBoolean(bot.can_read_all_group_messages) ?? false,
|
||||
can_manage_bots: normalizeBoolean(bot.can_manage_bots) ?? false,
|
||||
supports_inline_queries: normalizeBoolean(bot.supports_inline_queries) ?? false,
|
||||
can_connect_to_business: normalizeBoolean(bot.can_connect_to_business) ?? false,
|
||||
has_main_web_app: normalizeBoolean(bot.has_main_web_app) ?? false,
|
||||
has_topics_enabled: normalizeBoolean(bot.has_topics_enabled) ?? false,
|
||||
allows_users_to_create_topics: normalizeBoolean(bot.allows_users_to_create_topics) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function probeTelegram(
|
||||
token: string,
|
||||
timeoutMs: number,
|
||||
|
||||
Reference in New Issue
Block a user