mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:21:35 +00:00
Telegram: remove @ts-nocheck from bot.ts, fix duplicate error handler, harden sticker caching (#9077)
* Telegram: remove @ts-nocheck from bot.ts and bot-message-dispatch.ts - bot/types.ts: TelegramContext.me uses UserFromGetMe (Grammy) instead of manual inline type - bot.ts: remove 6 unsafe casts (as any, as unknown, as object), use Grammy types directly - bot.ts: remove dead message_thread_id access on reactions (not in Telegram Bot API) - bot.ts: remove resolveThreadSessionKeys import (no longer needed for reactions) - bot-message-dispatch.ts: replace ': any' with DispatchTelegramMessageParams type - bot-message-dispatch.ts: add sticker.fileId guard before cache access - bot.test.ts: update reaction tests, remove dead DM thread-reaction test * Telegram: remove duplicate bot.catch handler (only the last one runs in Grammy) * Telegram: remove @ts-nocheck from bot.ts, fix duplicate error handler, harden sticker caching (#9077)
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
- Telegram: remove `@ts-nocheck` from `bot.ts`, fix duplicate `bot.catch` error handler (Grammy overrides), remove dead reaction `message_thread_id` routing, harden sticker cache guard. (#9077)
|
||||||
- Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan.
|
- Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan.
|
||||||
- Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.
|
- Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.
|
||||||
- Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.
|
- Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const dispatchTelegramMessage = async ({
|
|||||||
// Handle uncached stickers: get a dedicated vision description before dispatch
|
// Handle uncached stickers: get a dedicated vision description before dispatch
|
||||||
// This ensures we cache a raw description rather than a conversational response
|
// This ensures we cache a raw description rather than a conversational response
|
||||||
const sticker = ctxPayload.Sticker;
|
const sticker = ctxPayload.Sticker;
|
||||||
if (sticker?.fileUniqueId && ctxPayload.MediaPath) {
|
if (sticker?.fileId && sticker.fileUniqueId && ctxPayload.MediaPath) {
|
||||||
const agentDir = resolveAgentDir(cfg, route.agentId);
|
const agentDir = resolveAgentDir(cfg, route.agentId);
|
||||||
const stickerSupportsVision = await resolveStickerVisionSupport(cfg, route.agentId);
|
const stickerSupportsVision = await resolveStickerVisionSupport(cfg, route.agentId);
|
||||||
let description = sticker.cachedDescription ?? null;
|
let description = sticker.cachedDescription ?? null;
|
||||||
|
|||||||
@@ -2876,7 +2876,7 @@ describe("createTelegramBot", () => {
|
|||||||
expect(enqueueSystemEvent).not.toHaveBeenCalled();
|
expect(enqueueSystemEvent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses correct session key for forum group reactions with topic", async () => {
|
it("routes forum group reactions to the general topic (thread id not available on reactions)", async () => {
|
||||||
onSpy.mockReset();
|
onSpy.mockReset();
|
||||||
enqueueSystemEvent.mockReset();
|
enqueueSystemEvent.mockReset();
|
||||||
|
|
||||||
@@ -2891,12 +2891,13 @@ describe("createTelegramBot", () => {
|
|||||||
ctx: Record<string, unknown>,
|
ctx: Record<string, unknown>,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
|
// MessageReactionUpdated does not include message_thread_id in the Bot API,
|
||||||
|
// so forum reactions always route to the general topic (1).
|
||||||
await handler({
|
await handler({
|
||||||
update: { update_id: 505 },
|
update: { update_id: 505 },
|
||||||
messageReaction: {
|
messageReaction: {
|
||||||
chat: { id: 5678, type: "supergroup", is_forum: true },
|
chat: { id: 5678, type: "supergroup", is_forum: true },
|
||||||
message_id: 100,
|
message_id: 100,
|
||||||
message_thread_id: 42,
|
|
||||||
user: { id: 10, first_name: "Bob", username: "bob_user" },
|
user: { id: 10, first_name: "Bob", username: "bob_user" },
|
||||||
date: 1736380800,
|
date: 1736380800,
|
||||||
old_reaction: [],
|
old_reaction: [],
|
||||||
@@ -2908,7 +2909,7 @@ describe("createTelegramBot", () => {
|
|||||||
expect(enqueueSystemEvent).toHaveBeenCalledWith(
|
expect(enqueueSystemEvent).toHaveBeenCalledWith(
|
||||||
"Telegram reaction added: 🔥 by Bob (@bob_user) on msg 100",
|
"Telegram reaction added: 🔥 by Bob (@bob_user) on msg 100",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
sessionKey: expect.stringContaining("telegram:group:5678:topic:42"),
|
sessionKey: expect.stringContaining("telegram:group:5678:topic:1"),
|
||||||
contextKey: expect.stringContaining("telegram:reaction:add:5678:100:10"),
|
contextKey: expect.stringContaining("telegram:reaction:add:5678:100:10"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -2991,41 +2992,4 @@ describe("createTelegramBot", () => {
|
|||||||
const sessionKey = enqueueSystemEvent.mock.calls[0][1].sessionKey;
|
const sessionKey = enqueueSystemEvent.mock.calls[0][1].sessionKey;
|
||||||
expect(sessionKey).not.toContain(":topic:");
|
expect(sessionKey).not.toContain(":topic:");
|
||||||
});
|
});
|
||||||
it("uses thread session key for dm reactions with topic id", async () => {
|
|
||||||
onSpy.mockReset();
|
|
||||||
enqueueSystemEvent.mockReset();
|
|
||||||
|
|
||||||
loadConfig.mockReturnValue({
|
|
||||||
channels: {
|
|
||||||
telegram: { dmPolicy: "open", reactionNotifications: "all" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createTelegramBot({ token: "tok" });
|
|
||||||
const handler = getOnHandler("message_reaction") as (
|
|
||||||
ctx: Record<string, unknown>,
|
|
||||||
) => Promise<void>;
|
|
||||||
|
|
||||||
await handler({
|
|
||||||
update: { update_id: 508 },
|
|
||||||
messageReaction: {
|
|
||||||
chat: { id: 1234, type: "private" },
|
|
||||||
message_id: 300,
|
|
||||||
message_thread_id: 42,
|
|
||||||
user: { id: 12, first_name: "Dana" },
|
|
||||||
date: 1736380800,
|
|
||||||
old_reaction: [],
|
|
||||||
new_reaction: [{ type: "emoji", emoji: "🔥" }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(enqueueSystemEvent).toHaveBeenCalledTimes(1);
|
|
||||||
expect(enqueueSystemEvent).toHaveBeenCalledWith(
|
|
||||||
"Telegram reaction added: 🔥 by Dana on msg 300",
|
|
||||||
expect.objectContaining({
|
|
||||||
sessionKey: expect.stringContaining(":thread:42"),
|
|
||||||
contextKey: expect.stringContaining("telegram:reaction:add:1234:300:12"),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { ApiClientOptions } from "grammy";
|
import type { ApiClientOptions } from "grammy";
|
||||||
// @ts-nocheck
|
|
||||||
import { sequentialize } from "@grammyjs/runner";
|
import { sequentialize } from "@grammyjs/runner";
|
||||||
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
||||||
import { type Message, ReactionTypeEmoji } from "@grammyjs/types";
|
import { type Message, type UserFromGetMe, ReactionTypeEmoji } from "@grammyjs/types";
|
||||||
import { Bot, webhookCallback } from "grammy";
|
import { Bot, webhookCallback } from "grammy";
|
||||||
import type { OpenClawConfig, ReplyToMode } from "../config/config.js";
|
import type { OpenClawConfig, ReplyToMode } from "../config/config.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -28,7 +27,6 @@ import { enqueueSystemEvent } from "../infra/system-events.js";
|
|||||||
import { getChildLogger } from "../logging.js";
|
import { getChildLogger } from "../logging.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||||
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
|
||||||
import { resolveTelegramAccount } from "./accounts.js";
|
import { resolveTelegramAccount } from "./accounts.js";
|
||||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import { registerTelegramHandlers } from "./bot-handlers.js";
|
import { registerTelegramHandlers } from "./bot-handlers.js";
|
||||||
@@ -67,6 +65,7 @@ export type TelegramBotOptions = {
|
|||||||
|
|
||||||
export function getTelegramSequentialKey(ctx: {
|
export function getTelegramSequentialKey(ctx: {
|
||||||
chat?: { id?: number };
|
chat?: { id?: number };
|
||||||
|
me?: UserFromGetMe;
|
||||||
message?: Message;
|
message?: Message;
|
||||||
update?: {
|
update?: {
|
||||||
message?: Message;
|
message?: Message;
|
||||||
@@ -87,7 +86,7 @@ export function getTelegramSequentialKey(ctx: {
|
|||||||
ctx.update?.callback_query?.message;
|
ctx.update?.callback_query?.message;
|
||||||
const chatId = msg?.chat?.id ?? ctx.chat?.id;
|
const chatId = msg?.chat?.id ?? ctx.chat?.id;
|
||||||
const rawText = msg?.text ?? msg?.caption;
|
const rawText = msg?.text ?? msg?.caption;
|
||||||
const botUsername = (ctx as { me?: { username?: string } }).me?.username;
|
const botUsername = ctx.me?.username;
|
||||||
if (
|
if (
|
||||||
rawText &&
|
rawText &&
|
||||||
isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined)
|
isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined)
|
||||||
@@ -99,7 +98,7 @@ export function getTelegramSequentialKey(ctx: {
|
|||||||
}
|
}
|
||||||
const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
|
const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
|
||||||
const messageThreadId = msg?.message_thread_id;
|
const messageThreadId = msg?.message_thread_id;
|
||||||
const isForum = (msg?.chat as { is_forum?: boolean } | undefined)?.is_forum;
|
const isForum = msg?.chat?.is_forum;
|
||||||
const threadId = isGroup
|
const threadId = isGroup
|
||||||
? resolveTelegramForumThreadId({ isForum, messageThreadId })
|
? resolveTelegramForumThreadId({ isForum, messageThreadId })
|
||||||
: messageThreadId;
|
: messageThreadId;
|
||||||
@@ -135,9 +134,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const client: ApiClientOptions | undefined =
|
const client: ApiClientOptions | undefined =
|
||||||
shouldProvideFetch || timeoutSeconds
|
shouldProvideFetch || timeoutSeconds
|
||||||
? {
|
? {
|
||||||
...(shouldProvideFetch && fetchImpl
|
...(shouldProvideFetch && fetchImpl ? { fetch: fetchImpl } : {}),
|
||||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
|
||||||
: {}),
|
|
||||||
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -145,14 +142,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const bot = new Bot(opts.token, client ? { client } : undefined);
|
const bot = new Bot(opts.token, client ? { client } : undefined);
|
||||||
bot.api.config.use(apiThrottler());
|
bot.api.config.use(apiThrottler());
|
||||||
bot.use(sequentialize(getTelegramSequentialKey));
|
bot.use(sequentialize(getTelegramSequentialKey));
|
||||||
bot.catch((err) => {
|
|
||||||
runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Catch all errors from bot middleware to prevent unhandled rejections
|
// Catch all errors from bot middleware to prevent unhandled rejections
|
||||||
bot.catch((err) => {
|
bot.catch((err) => {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
|
||||||
runtime.error?.(danger(`telegram bot error: ${message}`));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const recentUpdates = createTelegramUpdateDedupe();
|
const recentUpdates = createTelegramUpdateDedupe();
|
||||||
@@ -203,11 +195,10 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
const obj = value as object;
|
if (seen.has(value)) {
|
||||||
if (seen.has(obj)) {
|
|
||||||
return "[Circular]";
|
return "[Circular]";
|
||||||
}
|
}
|
||||||
seen.add(obj);
|
seen.add(value);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
@@ -267,9 +258,8 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
const streamMode = resolveTelegramStreamMode(telegramCfg);
|
const streamMode = resolveTelegramStreamMode(telegramCfg);
|
||||||
let botHasTopicsEnabled: boolean | undefined;
|
let botHasTopicsEnabled: boolean | undefined;
|
||||||
const resolveBotTopicsEnabled = async (ctx?: TelegramContext) => {
|
const resolveBotTopicsEnabled = async (ctx?: TelegramContext) => {
|
||||||
const fromCtx = ctx?.me as { has_topics_enabled?: boolean } | undefined;
|
if (typeof ctx?.me?.has_topics_enabled === "boolean") {
|
||||||
if (typeof fromCtx?.has_topics_enabled === "boolean") {
|
botHasTopicsEnabled = ctx.me.has_topics_enabled;
|
||||||
botHasTopicsEnabled = fromCtx.has_topics_enabled;
|
|
||||||
return botHasTopicsEnabled;
|
return botHasTopicsEnabled;
|
||||||
}
|
}
|
||||||
if (typeof botHasTopicsEnabled === "boolean") {
|
if (typeof botHasTopicsEnabled === "boolean") {
|
||||||
@@ -280,11 +270,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
return botHasTopicsEnabled;
|
return botHasTopicsEnabled;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const me = (await withTelegramApiErrorLogging({
|
const me = await withTelegramApiErrorLogging({
|
||||||
operation: "getMe",
|
operation: "getMe",
|
||||||
runtime,
|
runtime,
|
||||||
fn: () => bot.api.getMe(),
|
fn: () => bot.api.getMe(),
|
||||||
})) as { has_topics_enabled?: boolean };
|
});
|
||||||
botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
|
botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`telegram getMe failed: ${String(err)}`);
|
logVerbose(`telegram getMe failed: ${String(err)}`);
|
||||||
@@ -445,18 +435,14 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
}
|
}
|
||||||
senderLabel = senderLabel || "unknown";
|
senderLabel = senderLabel || "unknown";
|
||||||
|
|
||||||
// Extract forum thread info (similar to message processing)
|
// Reactions target a specific message_id; the Telegram Bot API does not include
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// message_thread_id on MessageReactionUpdated, so we route to the chat-level
|
||||||
const messageThreadId = (reaction as any).message_thread_id;
|
// session (forum topic routing is not available for reactions).
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
|
||||||
const isForum = (reaction.chat as any).is_forum === true;
|
|
||||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
|
||||||
isForum,
|
|
||||||
messageThreadId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Resolve agent route for session
|
|
||||||
const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
|
const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
|
||||||
|
const isForum = reaction.chat.is_forum === true;
|
||||||
|
const resolvedThreadId = isForum
|
||||||
|
? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined })
|
||||||
|
: undefined;
|
||||||
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
||||||
const route = resolveAgentRoute({
|
const route = resolveAgentRoute({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -464,14 +450,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
peer: { kind: isGroup ? "group" : "dm", id: peerId },
|
peer: { kind: isGroup ? "group" : "dm", id: peerId },
|
||||||
});
|
});
|
||||||
const baseSessionKey = route.sessionKey;
|
const sessionKey = route.sessionKey;
|
||||||
// DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
|
|
||||||
const dmThreadId = !isGroup ? messageThreadId : undefined;
|
|
||||||
const threadKeys =
|
|
||||||
dmThreadId != null
|
|
||||||
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
|
|
||||||
: null;
|
|
||||||
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
|
|
||||||
|
|
||||||
// Enqueue system event for each added reaction
|
// Enqueue system event for each added reaction
|
||||||
for (const r of addedReactions) {
|
for (const r of addedReactions) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Message } from "@grammyjs/types";
|
import type { Message, UserFromGetMe } from "@grammyjs/types";
|
||||||
|
|
||||||
/** App-specific stream mode for Telegram draft streaming. */
|
/** App-specific stream mode for Telegram draft streaming. */
|
||||||
export type TelegramStreamMode = "off" | "partial" | "block";
|
export type TelegramStreamMode = "off" | "partial" | "block";
|
||||||
@@ -10,7 +10,7 @@ export type TelegramStreamMode = "off" | "partial" | "block";
|
|||||||
*/
|
*/
|
||||||
export type TelegramContext = {
|
export type TelegramContext = {
|
||||||
message: Message;
|
message: Message;
|
||||||
me?: { id?: number; username?: string };
|
me?: UserFromGetMe;
|
||||||
getFile: () => Promise<{ file_path?: string }>;
|
getFile: () => Promise<{ file_path?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user