feat(telegram): register command menu and menu button on bot start

Add setMyCommands and setChatMenuButton to the Telegram Bot API client
and register a 12-command menu with a commands-type menu button when a
bot runtime connects. Menu setup failures are logged as warnings and do
not block startup.

Co-authored-by: xmanrui <841206367@qq.com>
This commit is contained in:
bitxeno 2026-08-21 22:11:04 +08:00
parent 7fbb459e00
commit 60e00bbca9
4 changed files with 284 additions and 132 deletions

View file

@ -16,6 +16,18 @@ export function validTelegramToken(value) {
return typeof value === 'string' && /^\d{5,20}:[A-Za-z0-9_-]{20,}$/.test(value.trim());
}
const TELEGRAM_COMMAND_NAME = /^[a-z0-9_]{1,32}$/;
function validBotCommand(value) {
return Boolean(value)
&& typeof value === 'object' && !Array.isArray(value)
&& typeof value.command === 'string' && TELEGRAM_COMMAND_NAME.test(value.command)
&& typeof value.description === 'string' && value.description.length >= 1
&& value.description.length <= 256;
}
export const COMMANDS_MENU_BUTTON = Object.freeze({ type: 'commands' });
export class TelegramApi {
#token;
#fetch;
@ -113,6 +125,25 @@ export class TelegramApi {
}, { signal });
}
async setMyCommands({ commands, scope, languageCode, signal } = {}) {
if (!Array.isArray(commands) || commands.length === 0
|| commands.some((command) => !validBotCommand(command))) {
throw new TypeError('Telegram bot commands are invalid');
}
return this.#call('setMyCommands', {
commands,
...(scope ? { scope } : {}),
...(cleanString(languageCode) ? { language_code: cleanString(languageCode) } : {}),
}, { signal });
}
async setChatMenuButton({ menuButton = COMMANDS_MENU_BUTTON, signal } = {}) {
if (!menuButton || typeof menuButton !== 'object' || Array.isArray(menuButton)) {
throw new TypeError('Telegram menu button is invalid');
}
return this.#call('setChatMenuButton', { menu_button: menuButton }, { signal });
}
async #call(method, payload, { signal, timeoutMs = 15_000 } = {}) {
const url = new URL(this.#baseUrl);
url.pathname = `${url.pathname.replace(/\/$/, '')}/bot${this.#token}/${method}`;

View file

@ -1,11 +1,26 @@
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
import { TelegramApi } from './telegram-api.mjs';
import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
import {
TELEGRAM_ACCESS_MODES,
normalizeTelegramAccessPolicy,
} from './config-store.mjs';
export const TELEGRAM_COMMAND_MENU = Object.freeze([
{ command: 'new', description: '开启一个全新会话' },
{ command: 'compact', description: '压缩当前会话的较早上下文' },
{ command: 'workspace', description: '切换工作区' },
{ command: 'workspacelist', description: '列出工作区绝对路径' },
{ command: 'sessionlist', description: '列出会话 ID 和标题' },
{ command: 'session', description: '将当前聊天绑定到指定会话' },
{ command: 'models', description: '按序号列出所有可用模型' },
{ command: 'model', description: '查看或切换当前会话模型' },
{ command: 'stop', description: '停止当前任务' },
{ command: 'steer', description: '纠偏当前任务' },
{ command: 'status', description: '检查连接状态' },
{ command: 'help', description: '显示帮助' },
]);
function escaped(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
@ -290,6 +305,15 @@ export class TelegramRuntime {
error.code = 'webhook-configured';
throw error;
}
try {
await api.setMyCommands({ commands: TELEGRAM_COMMAND_MENU, signal: controller.signal });
await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
} catch (error) {
this.#logger.warn?.(
`[dsh-im:telegram] bot ${this.#config.botId} command menu setup failed:`,
error,
);
}
const client = new TelegramBotClient({ api, signal: controller.signal });
this.#bridge = new TelegramHarnessBridge({
bot: client,