Files
Moltbot/src/telegram/bot-native-commands.ts
Eugene 45b74fb56c fix(telegram): move network fallback to resolver-scoped dispatchers (#40740)
Merged via squash.

Prepared head SHA: a4456d48b42d6c588b2858831a2391d015260a9b
Co-authored-by: sircrumpet <4436535+sircrumpet@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-03-10 11:28:51 +05:30

900 lines
31 KiB
TypeScript

import type { Bot, Context } from "grammy";
import { ensureConfiguredAcpRouteReady } from "../acp/persistent-bindings.route.js";
import { resolveChunkMode } from "../auto-reply/chunk.js";
import { resolveCommandAuthorization } from "../auto-reply/command-auth.js";
import type { CommandArgs } from "../auto-reply/commands-registry.js";
import {
buildCommandTextFromArgs,
findCommandByNativeName,
listNativeCommandSpecs,
listNativeCommandSpecsForConfig,
parseCommandArgs,
resolveCommandArgMenu,
} from "../auto-reply/commands-registry.js";
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
import { resolveNativeCommandSessionTargets } from "../channels/native-command-session-targets.js";
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { recordInboundSessionMetaSafe } from "../channels/session-meta.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ChannelGroupPolicy } from "../config/group-policy.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import {
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
TELEGRAM_COMMAND_NAME_PATTERN,
} from "../config/telegram-custom-commands.js";
import type {
ReplyToMode,
TelegramAccountConfig,
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.js";
import { danger, logVerbose } from "../globals.js";
import { getChildLogger } from "../logging.js";
import { getAgentScopedMediaLocalRoots } from "../media/local-roots.js";
import {
executePluginCommand,
getPluginCommandSpecs,
matchPluginCommand,
} from "../plugins/commands.js";
import { resolveAgentRoute } from "../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js";
import type { TelegramMediaRef } from "./bot-message-context.js";
import {
buildCappedTelegramMenuCommands,
buildPluginTelegramMenuCommands,
syncTelegramMenuCommands,
} from "./bot-native-command-menu.js";
import { TelegramUpdateKeyContext } from "./bot-updates.js";
import { TelegramBotOptions } from "./bot.js";
import { deliverReplies } from "./bot/delivery.js";
import {
buildTelegramThreadParams,
buildSenderName,
buildTelegramGroupFrom,
resolveTelegramGroupAllowFromContext,
resolveTelegramThreadSpec,
} from "./bot/helpers.js";
import type { TelegramContext } from "./bot/types.js";
import { resolveTelegramConversationRoute } from "./conversation-route.js";
import { shouldSuppressLocalTelegramExecApprovalPrompt } from "./exec-approvals.js";
import {
evaluateTelegramGroupBaseAccess,
evaluateTelegramGroupPolicyAccess,
} from "./group-access.js";
import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js";
import { buildInlineKeyboard } from "./send.js";
const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
type TelegramNativeCommandContext = Context & { match?: string };
type TelegramCommandAuthResult = {
chatId: number;
isGroup: boolean;
isForum: boolean;
resolvedThreadId?: number;
senderId: string;
senderUsername: string;
groupConfig?: TelegramGroupConfig;
topicConfig?: TelegramTopicConfig;
commandAuthorized: boolean;
};
export type RegisterTelegramHandlerParams = {
cfg: OpenClawConfig;
accountId: string;
bot: Bot;
mediaMaxBytes: number;
opts: TelegramBotOptions;
telegramFetchImpl?: typeof fetch;
runtime: RuntimeEnv;
telegramCfg: TelegramAccountConfig;
allowFrom?: Array<string | number>;
groupAllowFrom?: Array<string | number>;
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
resolveTelegramGroupConfig: (
chatId: string | number,
messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
processMessage: (
ctx: TelegramContext,
allMedia: TelegramMediaRef[],
storeAllowFrom: string[],
options?: {
messageIdOverride?: string;
forceWasMentioned?: boolean;
},
replyMedia?: TelegramMediaRef[],
) => Promise<void>;
logger: ReturnType<typeof getChildLogger>;
};
type RegisterTelegramNativeCommandsParams = {
bot: Bot;
cfg: OpenClawConfig;
runtime: RuntimeEnv;
accountId: string;
telegramCfg: TelegramAccountConfig;
allowFrom?: Array<string | number>;
groupAllowFrom?: Array<string | number>;
replyToMode: ReplyToMode;
textLimit: number;
useAccessGroups: boolean;
nativeEnabled: boolean;
nativeSkillsEnabled: boolean;
nativeDisabledExplicit: boolean;
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
resolveTelegramGroupConfig: (
chatId: string | number,
messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
opts: { token: string };
};
async function resolveTelegramCommandAuth(params: {
msg: NonNullable<TelegramNativeCommandContext["message"]>;
bot: Bot;
cfg: OpenClawConfig;
accountId: string;
telegramCfg: TelegramAccountConfig;
allowFrom?: Array<string | number>;
groupAllowFrom?: Array<string | number>;
useAccessGroups: boolean;
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
resolveTelegramGroupConfig: (
chatId: string | number,
messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
requireAuth: boolean;
}): Promise<TelegramCommandAuthResult | null> {
const {
msg,
bot,
cfg,
accountId,
telegramCfg,
allowFrom,
groupAllowFrom,
useAccessGroups,
resolveGroupPolicy,
resolveTelegramGroupConfig,
requireAuth,
} = params;
const chatId = msg.chat.id;
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
const threadSpec = resolveTelegramThreadSpec({
isGroup,
isForum,
messageThreadId,
});
const threadParams = buildTelegramThreadParams(threadSpec) ?? {};
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
chatId,
accountId,
isGroup,
isForum,
messageThreadId,
groupAllowFrom,
resolveTelegramGroupConfig,
});
const {
resolvedThreadId,
dmThreadId,
storeAllowFrom,
groupConfig,
topicConfig,
groupAllowOverride,
effectiveGroupAllow,
hasGroupAllowOverride,
} = groupAllowContext;
// Use direct config dmPolicy override if available for DMs
const effectiveDmPolicy =
!isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? telegramCfg.dmPolicy ?? "pairing")
: (telegramCfg.dmPolicy ?? "pairing");
const requireTopic = (groupConfig as TelegramDirectConfig | undefined)?.requireTopic;
if (!isGroup && requireTopic === true && dmThreadId == null) {
logVerbose(`Blocked telegram command in DM ${chatId}: requireTopic=true but no topic present`);
return null;
}
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const senderId = msg.from?.id ? String(msg.from.id) : "";
const senderUsername = msg.from?.username ?? "";
const commandsAllowFrom = cfg.commands?.allowFrom;
const commandsAllowFromConfigured =
commandsAllowFrom != null &&
typeof commandsAllowFrom === "object" &&
(Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"]));
const commandsAllowFromAccess = commandsAllowFromConfigured
? resolveCommandAuthorization({
ctx: {
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
AccountId: accountId,
ChatType: isGroup ? "group" : "direct",
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,
},
cfg,
// commands.allowFrom is the only auth source when configured.
commandAuthorized: false,
})
: null;
const sendAuthMessage = async (text: string) => {
await withTelegramApiErrorLogging({
operation: "sendMessage",
fn: () => bot.api.sendMessage(chatId, text, threadParams),
});
return null;
};
const rejectNotAuthorized = async () => {
return await sendAuthMessage("You are not authorized to use this command.");
};
const baseAccess = evaluateTelegramGroupBaseAccess({
isGroup,
groupConfig,
topicConfig,
hasGroupAllowOverride,
effectiveGroupAllow,
senderId,
senderUsername,
enforceAllowOverride: requireAuth,
requireSenderForAllowOverride: true,
});
if (!baseAccess.allowed) {
if (baseAccess.reason === "group-disabled") {
return await sendAuthMessage("This group is disabled.");
}
if (baseAccess.reason === "topic-disabled") {
return await sendAuthMessage("This topic is disabled.");
}
return await rejectNotAuthorized();
}
const policyAccess = evaluateTelegramGroupPolicyAccess({
isGroup,
chatId,
cfg,
telegramCfg,
topicConfig,
groupConfig,
effectiveGroupAllow,
senderId,
senderUsername,
resolveGroupPolicy,
enforcePolicy: useAccessGroups,
useTopicAndGroupOverrides: false,
enforceAllowlistAuthorization: requireAuth && !commandsAllowFromConfigured,
allowEmptyAllowlistEntries: true,
requireSenderForAllowlistAuthorization: true,
checkChatAllowlist: useAccessGroups,
});
if (!policyAccess.allowed) {
if (policyAccess.reason === "group-policy-disabled") {
return await sendAuthMessage("Telegram group commands are disabled.");
}
if (
policyAccess.reason === "group-policy-allowlist-no-sender" ||
policyAccess.reason === "group-policy-allowlist-unauthorized"
) {
return await rejectNotAuthorized();
}
if (policyAccess.reason === "group-chat-not-allowed") {
return await sendAuthMessage("This group is not allowed.");
}
}
const dmAllow = normalizeDmAllowFromWithStore({
allowFrom: dmAllowFrom,
storeAllowFrom: isGroup ? [] : storeAllowFrom,
dmPolicy: effectiveDmPolicy,
});
const senderAllowed = isSenderAllowed({
allow: dmAllow,
senderId,
senderUsername,
});
const groupSenderAllowed = isGroup
? isSenderAllowed({ allow: effectiveGroupAllow, senderId, senderUsername })
: false;
const commandAuthorized = commandsAllowFromConfigured
? Boolean(commandsAllowFromAccess?.isAuthorizedSender)
: resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers: [
{ configured: dmAllow.hasEntries, allowed: senderAllowed },
...(isGroup
? [{ configured: effectiveGroupAllow.hasEntries, allowed: groupSenderAllowed }]
: []),
],
modeWhenAccessGroupsOff: "configured",
});
if (requireAuth && !commandAuthorized) {
return await rejectNotAuthorized();
}
return {
chatId,
isGroup,
isForum,
resolvedThreadId,
senderId,
senderUsername,
groupConfig,
topicConfig,
commandAuthorized,
};
}
export const registerTelegramNativeCommands = ({
bot,
cfg,
runtime,
accountId,
telegramCfg,
allowFrom,
groupAllowFrom,
replyToMode,
textLimit,
useAccessGroups,
nativeEnabled,
nativeSkillsEnabled,
nativeDisabledExplicit,
resolveGroupPolicy,
resolveTelegramGroupConfig,
shouldSkipUpdate,
opts,
}: RegisterTelegramNativeCommandsParams) => {
const boundRoute =
nativeEnabled && nativeSkillsEnabled
? resolveAgentRoute({ cfg, channel: "telegram", accountId })
: null;
if (nativeEnabled && nativeSkillsEnabled && !boundRoute) {
runtime.log?.(
"nativeSkillsEnabled is true but no agent route is bound for this Telegram account; skill commands will not appear in the native menu.",
);
}
const skillCommands =
nativeEnabled && nativeSkillsEnabled && boundRoute
? listSkillCommandsForAgents({ cfg, agentIds: [boundRoute.agentId] })
: [];
const nativeCommands = nativeEnabled
? listNativeCommandSpecsForConfig(cfg, {
skillCommands,
provider: "telegram",
})
: [];
const reservedCommands = new Set(
listNativeCommandSpecs().map((command) => normalizeTelegramCommandName(command.name)),
);
for (const command of skillCommands) {
reservedCommands.add(command.name.toLowerCase());
}
const customResolution = resolveTelegramCustomCommands({
commands: telegramCfg.customCommands,
reservedCommands,
});
for (const issue of customResolution.issues) {
runtime.error?.(danger(issue.message));
}
const customCommands = customResolution.commands;
const pluginCommandSpecs = getPluginCommandSpecs("telegram");
const existingCommands = new Set(
[
...nativeCommands.map((command) => normalizeTelegramCommandName(command.name)),
...customCommands.map((command) => command.command),
].map((command) => command.toLowerCase()),
);
const pluginCatalog = buildPluginTelegramMenuCommands({
specs: pluginCommandSpecs,
existingCommands,
});
for (const issue of pluginCatalog.issues) {
runtime.error?.(danger(issue));
}
const allCommandsFull: Array<{ command: string; description: string }> = [
...nativeCommands
.map((command) => {
const normalized = normalizeTelegramCommandName(command.name);
if (!TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
runtime.error?.(
danger(
`Native command "${command.name}" is invalid for Telegram (resolved to "${normalized}"). Skipping.`,
),
);
return null;
}
return {
command: normalized,
description: command.description,
};
})
.filter((cmd): cmd is { command: string; description: string } => cmd !== null),
...(nativeEnabled ? pluginCatalog.commands : []),
...customCommands,
];
const { commandsToRegister, totalCommands, maxCommands, overflowCount } =
buildCappedTelegramMenuCommands({
allCommands: allCommandsFull,
});
if (overflowCount > 0) {
runtime.log?.(
`Telegram limits bots to ${maxCommands} commands. ` +
`${totalCommands} configured; registering first ${maxCommands}. ` +
`Use channels.telegram.commands.native: false to disable, or reduce plugin/skill/custom commands.`,
);
}
// Telegram only limits the setMyCommands payload (menu entries).
// Keep hidden commands callable by registering handlers for the full catalog.
syncTelegramMenuCommands({
bot,
runtime,
commandsToRegister,
accountId,
botIdentity: opts.token,
});
const resolveCommandRuntimeContext = async (params: {
msg: NonNullable<TelegramNativeCommandContext["message"]>;
isGroup: boolean;
isForum: boolean;
resolvedThreadId?: number;
senderId?: string;
topicAgentId?: string;
}): Promise<{
chatId: number;
threadSpec: ReturnType<typeof resolveTelegramThreadSpec>;
route: ReturnType<typeof resolveTelegramConversationRoute>["route"];
mediaLocalRoots: readonly string[] | undefined;
tableMode: ReturnType<typeof resolveMarkdownTableMode>;
chunkMode: ReturnType<typeof resolveChunkMode>;
} | null> => {
const { msg, isGroup, isForum, resolvedThreadId, senderId, topicAgentId } = params;
const chatId = msg.chat.id;
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
const threadSpec = resolveTelegramThreadSpec({
isGroup,
isForum,
messageThreadId,
});
let { route, configuredBinding } = resolveTelegramConversationRoute({
cfg,
accountId,
chatId,
isGroup,
resolvedThreadId,
replyThreadId: threadSpec.id,
senderId,
topicAgentId,
});
if (configuredBinding) {
const ensured = await ensureConfiguredAcpRouteReady({
cfg,
configuredBinding,
});
if (!ensured.ok) {
logVerbose(
`telegram native command: configured ACP binding unavailable for topic ${configuredBinding.spec.conversationId}: ${ensured.error}`,
);
await withTelegramApiErrorLogging({
operation: "sendMessage",
runtime,
fn: () =>
bot.api.sendMessage(
chatId,
"Configured ACP binding is unavailable right now. Please try again.",
buildTelegramThreadParams(threadSpec) ?? {},
),
});
return null;
}
}
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
const tableMode = resolveMarkdownTableMode({
cfg,
channel: "telegram",
accountId: route.accountId,
});
const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId);
return { chatId, threadSpec, route, mediaLocalRoots, tableMode, chunkMode };
};
const buildCommandDeliveryBaseOptions = (params: {
chatId: string | number;
accountId: string;
sessionKeyForInternalHooks?: string;
mirrorIsGroup?: boolean;
mirrorGroupId?: string;
mediaLocalRoots?: readonly string[];
threadSpec: ReturnType<typeof resolveTelegramThreadSpec>;
tableMode: ReturnType<typeof resolveMarkdownTableMode>;
chunkMode: ReturnType<typeof resolveChunkMode>;
}) => ({
chatId: String(params.chatId),
accountId: params.accountId,
sessionKeyForInternalHooks: params.sessionKeyForInternalHooks,
mirrorIsGroup: params.mirrorIsGroup,
mirrorGroupId: params.mirrorGroupId,
token: opts.token,
runtime,
bot,
mediaLocalRoots: params.mediaLocalRoots,
replyToMode,
textLimit,
thread: params.threadSpec,
tableMode: params.tableMode,
chunkMode: params.chunkMode,
linkPreview: telegramCfg.linkPreview,
});
if (commandsToRegister.length > 0 || pluginCatalog.commands.length > 0) {
if (typeof (bot as unknown as { command?: unknown }).command !== "function") {
logVerbose("telegram: bot.command unavailable; skipping native handlers");
} else {
for (const command of nativeCommands) {
const normalizedCommandName = normalizeTelegramCommandName(command.name);
bot.command(normalizedCommandName, async (ctx: TelegramNativeCommandContext) => {
const msg = ctx.message;
if (!msg) {
return;
}
if (shouldSkipUpdate(ctx)) {
return;
}
const auth = await resolveTelegramCommandAuth({
msg,
bot,
cfg,
accountId,
telegramCfg,
allowFrom,
groupAllowFrom,
useAccessGroups,
resolveGroupPolicy,
resolveTelegramGroupConfig,
requireAuth: true,
});
if (!auth) {
return;
}
const {
chatId,
isGroup,
isForum,
resolvedThreadId,
senderId,
senderUsername,
groupConfig,
topicConfig,
commandAuthorized,
} = auth;
const runtimeContext = await resolveCommandRuntimeContext({
msg,
isGroup,
isForum,
resolvedThreadId,
senderId,
topicAgentId: topicConfig?.agentId,
});
if (!runtimeContext) {
return;
}
const { threadSpec, route, mediaLocalRoots, tableMode, chunkMode } = runtimeContext;
const threadParams = buildTelegramThreadParams(threadSpec) ?? {};
const commandDefinition = findCommandByNativeName(command.name, "telegram");
const rawText = ctx.match?.trim() ?? "";
const commandArgs = commandDefinition
? parseCommandArgs(commandDefinition, rawText)
: rawText
? ({ raw: rawText } satisfies CommandArgs)
: undefined;
const prompt = commandDefinition
? buildCommandTextFromArgs(commandDefinition, commandArgs)
: rawText
? `/${command.name} ${rawText}`
: `/${command.name}`;
const menu = commandDefinition
? resolveCommandArgMenu({
command: commandDefinition,
args: commandArgs,
cfg,
})
: null;
if (menu && commandDefinition) {
const title =
menu.title ??
`Choose ${menu.arg.description || menu.arg.name} for /${commandDefinition.nativeName ?? commandDefinition.key}.`;
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
for (let i = 0; i < menu.choices.length; i += 2) {
const slice = menu.choices.slice(i, i + 2);
rows.push(
slice.map((choice) => {
const args: CommandArgs = {
values: { [menu.arg.name]: choice.value },
};
return {
text: choice.label,
callback_data: buildCommandTextFromArgs(commandDefinition, args),
};
}),
);
}
const replyMarkup = buildInlineKeyboard(rows);
await withTelegramApiErrorLogging({
operation: "sendMessage",
runtime,
fn: () =>
bot.api.sendMessage(chatId, title, {
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
...threadParams,
}),
});
return;
}
const baseSessionKey = route.sessionKey;
// DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
const threadKeys =
dmThreadId != null
? resolveThreadSessionKeys({
baseSessionKey,
threadId: `${chatId}:${dmThreadId}`,
})
: null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
const { skillFilter, groupSystemPrompt } = resolveTelegramGroupPromptSettings({
groupConfig,
topicConfig,
});
const { sessionKey: commandSessionKey, commandTargetSessionKey } =
resolveNativeCommandSessionTargets({
agentId: route.agentId,
sessionPrefix: "telegram:slash",
userId: String(senderId || chatId),
targetSessionKey: sessionKey,
});
const deliveryBaseOptions = buildCommandDeliveryBaseOptions({
chatId,
accountId: route.accountId,
sessionKeyForInternalHooks: commandSessionKey,
mirrorIsGroup: isGroup,
mirrorGroupId: isGroup ? String(chatId) : undefined,
mediaLocalRoots,
threadSpec,
tableMode,
chunkMode,
});
const conversationLabel = isGroup
? msg.chat.title
? `${msg.chat.title} id:${chatId}`
: `group:${chatId}`
: (buildSenderName(msg) ?? String(senderId || chatId));
const ctxPayload = finalizeInboundContext({
Body: prompt,
BodyForAgent: prompt,
RawBody: prompt,
CommandBody: prompt,
CommandArgs: commandArgs,
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
To: `slash:${senderId || chatId}`,
ChatType: isGroup ? "group" : "direct",
ConversationLabel: conversationLabel,
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined,
SenderName: buildSenderName(msg),
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,
Surface: "telegram",
Provider: "telegram",
MessageSid: String(msg.message_id),
Timestamp: msg.date ? msg.date * 1000 : undefined,
WasMentioned: true,
CommandAuthorized: commandAuthorized,
CommandSource: "native" as const,
SessionKey: commandSessionKey,
AccountId: route.accountId,
CommandTargetSessionKey: commandTargetSessionKey,
MessageThreadId: threadSpec.id,
IsForum: isForum,
// Originating context for sub-agent announce routing
OriginatingChannel: "telegram" as const,
OriginatingTo: `telegram:${chatId}`,
});
await recordInboundSessionMetaSafe({
cfg,
agentId: route.agentId,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
onError: (err) =>
runtime.error?.(
danger(`telegram slash: failed updating session meta: ${String(err)}`),
),
});
const disableBlockStreaming =
typeof telegramCfg.blockStreaming === "boolean"
? !telegramCfg.blockStreaming
: undefined;
const deliveryState = {
delivered: false,
skippedNonSilent: 0,
};
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "telegram",
accountId: route.accountId,
});
await dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
...prefixOptions,
deliver: async (payload, _info) => {
if (
shouldSuppressLocalTelegramExecApprovalPrompt({
cfg,
accountId: route.accountId,
payload,
})
) {
deliveryState.delivered = true;
return;
}
const result = await deliverReplies({
replies: [payload],
...deliveryBaseOptions,
});
if (result.delivered) {
deliveryState.delivered = true;
}
},
onSkip: (_payload, info) => {
if (info.reason !== "silent") {
deliveryState.skippedNonSilent += 1;
}
},
onError: (err, info) => {
runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`));
},
},
replyOptions: {
skillFilter,
disableBlockStreaming,
onModelSelected,
},
});
if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {
await deliverReplies({
replies: [{ text: EMPTY_RESPONSE_FALLBACK }],
...deliveryBaseOptions,
});
}
});
}
for (const pluginCommand of pluginCatalog.commands) {
bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => {
const msg = ctx.message;
if (!msg) {
return;
}
if (shouldSkipUpdate(ctx)) {
return;
}
const chatId = msg.chat.id;
const rawText = ctx.match?.trim() ?? "";
const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`;
const match = matchPluginCommand(commandBody);
if (!match) {
await withTelegramApiErrorLogging({
operation: "sendMessage",
runtime,
fn: () => bot.api.sendMessage(chatId, "Command not found."),
});
return;
}
const auth = await resolveTelegramCommandAuth({
msg,
bot,
cfg,
accountId,
telegramCfg,
allowFrom,
groupAllowFrom,
useAccessGroups,
resolveGroupPolicy,
resolveTelegramGroupConfig,
requireAuth: match.command.requireAuth !== false,
});
if (!auth) {
return;
}
const { senderId, commandAuthorized, isGroup, isForum, resolvedThreadId } = auth;
const runtimeContext = await resolveCommandRuntimeContext({
msg,
isGroup,
isForum,
resolvedThreadId,
senderId,
topicAgentId: auth.topicConfig?.agentId,
});
if (!runtimeContext) {
return;
}
const { threadSpec, route, mediaLocalRoots, tableMode, chunkMode } = runtimeContext;
const deliveryBaseOptions = buildCommandDeliveryBaseOptions({
chatId,
accountId: route.accountId,
sessionKeyForInternalHooks: route.sessionKey,
mirrorIsGroup: isGroup,
mirrorGroupId: isGroup ? String(chatId) : undefined,
mediaLocalRoots,
threadSpec,
tableMode,
chunkMode,
});
const from = isGroup
? buildTelegramGroupFrom(chatId, threadSpec.id)
: `telegram:${chatId}`;
const to = `telegram:${chatId}`;
const result = await executePluginCommand({
command: match.command,
args: match.args,
senderId,
channel: "telegram",
isAuthorizedSender: commandAuthorized,
commandBody,
config: cfg,
from,
to,
accountId,
messageThreadId: threadSpec.id,
});
if (
!shouldSuppressLocalTelegramExecApprovalPrompt({
cfg,
accountId: route.accountId,
payload: result,
})
) {
await deliverReplies({
replies: [result],
...deliveryBaseOptions,
});
}
});
}
}
} else if (nativeDisabledExplicit) {
withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands([]),
}).catch(() => {});
}
};