fix: filter telegram native command names (#1558) (thanks @Glucksberg)

This commit is contained in:
Peter Steinberger 2026-01-24 06:19:19 +00:00
parent c2940adc80
commit 185ffca274
4 changed files with 70 additions and 3 deletions

View File

@ -551,6 +551,7 @@ Notes:
- Commands are registered globally and work across all channels
- Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
- Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
- Telegram native commands only allow `a-z0-9_` (max 32 chars). Use underscores (not hyphens) if you want a plugin command to appear in Telegrams native command list.
- Reserved command names (like `help`, `status`, `reset`, etc.) cannot be overridden by plugins
- Duplicate command registration across plugins will fail with a diagnostic error

View File

@ -8,6 +8,7 @@ import {
listChatCommandsForConfig,
listNativeCommandSpecs,
listNativeCommandSpecsForConfig,
normalizeNativeCommandSpecsForSurface,
normalizeCommandBody,
parseCommandArgs,
resolveCommandArgMenu,
@ -45,6 +46,20 @@ describe("commands registry", () => {
expect(specs.find((spec) => spec.name === "compact")).toBeFalsy();
});
it("normalizes telegram native command specs", () => {
const specs = [
{ name: "OK", description: "Ok", acceptsArgs: false },
{ name: "bad-name", description: "Bad", acceptsArgs: false },
{ name: "fine_name", description: "Fine", acceptsArgs: false },
{ name: "ok", description: "Dup", acceptsArgs: false },
];
const normalized = normalizeNativeCommandSpecsForSurface({
surface: "telegram",
specs,
});
expect(normalized.map((spec) => spec.name)).toEqual(["ok", "fine_name"]);
});
it("filters commands based on config flags", () => {
const disabled = listChatCommandsForConfig({
commands: { config: false, debug: false },

View File

@ -4,6 +4,10 @@ import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.d
import { getPluginCommandSpecs } from "../plugins/commands.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import {
normalizeTelegramCommandName,
TELEGRAM_COMMAND_NAME_PATTERN,
} from "../config/telegram-custom-commands.js";
import type {
ChatCommandDefinition,
CommandArgChoiceContext,
@ -143,6 +147,37 @@ export function listNativeCommandSpecsForConfig(
return extras.length > 0 ? [...base, ...extras] : base;
}
function normalizeNativeCommandNameForSurface(name: string, surface: string): string | null {
const trimmed = name.trim();
if (!trimmed) return null;
if (surface === "telegram") {
const normalized = normalizeTelegramCommandName(trimmed);
if (!normalized) return null;
if (!TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) return null;
return normalized;
}
return trimmed;
}
export function normalizeNativeCommandSpecsForSurface(params: {
surface: string;
specs: NativeCommandSpec[];
}): NativeCommandSpec[] {
const surface = params.surface.toLowerCase();
if (!surface) return params.specs;
const normalized: NativeCommandSpec[] = [];
const seen = new Set<string>();
for (const spec of params.specs) {
const normalizedName = normalizeNativeCommandNameForSurface(spec.name, surface);
if (!normalizedName) continue;
const key = normalizedName.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
normalized.push(normalizedName === spec.name ? spec : { ...spec, name: normalizedName });
}
return normalized;
}
export function findCommandByNativeName(name: string): ChatCommandDefinition | undefined {
const normalized = name.trim().toLowerCase();
return getChatCommands().find(

View File

@ -6,6 +6,7 @@ import {
findCommandByNativeName,
listNativeCommandSpecs,
listNativeCommandSpecsForConfig,
normalizeNativeCommandSpecsForSurface,
parseCommandArgs,
resolveCommandArgMenu,
} from "../auto-reply/commands-registry.js";
@ -84,13 +85,28 @@ export const registerTelegramNativeCommands = ({
}: RegisterTelegramNativeCommandsParams) => {
const skillCommands =
nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : [];
const nativeCommands = nativeEnabled
const rawNativeCommands = nativeEnabled
? listNativeCommandSpecsForConfig(cfg, { skillCommands })
: [];
const nativeCommands = normalizeNativeCommandSpecsForSurface({
surface: "telegram",
specs: rawNativeCommands,
});
const reservedCommands = new Set(
listNativeCommandSpecs().map((command) => command.name.toLowerCase()),
normalizeNativeCommandSpecsForSurface({
surface: "telegram",
specs: listNativeCommandSpecs(),
}).map((command) => command.name.toLowerCase()),
);
for (const command of skillCommands) {
const reservedSkillSpecs = normalizeNativeCommandSpecsForSurface({
surface: "telegram",
specs: skillCommands.map((command) => ({
name: command.name,
description: command.description,
acceptsArgs: true,
})),
});
for (const command of reservedSkillSpecs) {
reservedCommands.add(command.name.toLowerCase());
}
const customResolution = resolveTelegramCustomCommands({