В этом руководстве вы узнаете, как быстро создать мультиплатформенное приложение для голосовых навыков и чат‑ботов с помощью фреймворка umbot на TypeScript.
umbot - универсальный фреймворк для разработки голосовых навыков и чат‑ботов для множества платформ. Ключевые возможности:
npm install umbot
Базовый вариант (с использованием контроллера) Создадим контроллер, в котором опишем логику обработки команд.
import { Bot, BotController, WELCOME_INTENT_NAME } from 'umbot';
import { fullPlatforms } from 'umbot/plugins';
import { join } from 'node:path';
// Создаем контроллер с логикой навыка
class MyController extends BotController {
public action(intentName: string | null): void {
switch (intentName) {
case WELCOME_INTENT_NAME:
this.text = 'Привет! Я новый навык.';
this.buttons.addBtn('Помощь');
break;
case 'help':
this.text = 'Я умею отвечать на команды и показывать кнопки';
break;
default:
this.text = this.userCommand || 'Вы ничего не сказали';
break;
}
}
}
// Инициализируем приложение
const bot = new Bot();
// Подключаем все доступные платформы
// Если вам нужны только голосовые платформы, используйте voicePlatforms или конкретный адаптер, если нужна только одна платформа
bot.use(fullPlatforms);
// Настраиваем команды
bot.setPlatformParams({
intents: [
{
name: 'help',
slots: ['помощь', 'что ты умеешь'],
},
],
});
// Настраиваем параметры
bot.setAppConfig({
json: join(__dirname, 'data'),
error_log: join(__dirname, 'logs'),
isLocalStorage: true,
});
// Подключаем контроллер
bot.initBotController(MyController);
bot.start('localhost', 3000);
Минималистичный вариант (без контроллера)
Также можно совсем не создавать BotController и решить все задачи с помощью динамического добавления команд.
Обратите внимание на FALLBACK_COMMAND, обработчик будет выполнен в том случае, если не удалось найти нужную
команду. Вместо константы можно просто указать "*", что также равносильно заданию через константу.
import { Bot, BotController, FALLBACK_COMMAND, HELP_INTENT_NAME, WELCOME_INTENT_NAME } from 'umbot';
import { fullPlatforms } from 'umbot/plugins';
import { join } from 'node:path';
const bot = new Bot()
.use(fullPlatforms)
.setAppConfig({
json: join(__dirname, 'data'),
error_log: join(__dirname, 'logs'),
isLocalStorage: true,
})
.addCommand(WELCOME_INTENT_NAME, ['привет'], (_: string, bc: BotController) => {
bc.text = 'Привет! Я новый навык.';
bc.buttons.addBtn('Помощь');
})
.addCommand(HELP_INTENT_NAME, ['помощь'], (_: string, bc: BotController) => {
bc.text = 'Я умею отвечать на команды и показывать кнопки';
})
.addCommand(FALLBACK_COMMAND, [], (_: string, bc: BotController) => {
bc.text = bc.userCommand || 'Вы ничего не сказали';
})
.start('localhost', 3000);
Базовый класс, предоставляющий доступ к API ответа и состоянию.
this.text = 'Ответ пользователю'; // Текст ответа
this.tts = 'Текст для синтеза речи (если отличается от text)'; // TTS версия (опционально)
this.buttons
.addBtn('Простая кнопка')
.addBtn('Ссылка', 'http://localhost')
.addBtn('Кнопка с данными', null, {
action: 'custom',
value: 123,
});
this.card.addImage('image.jpg').setTitle('Заголовок').setDescription('Описание');
// Для TypeScript, объявите интерфейс и передайте его в BotController
interface IUserState {
counter?: number;
}
class MyController extends BotController<IUserState> {}
// Внутри controller.userData теперь знает про counter
this.userData.counter = 42;
// Прочитать данные
const counter = this.userData.counter ?? 0;
bot.setPlatformParams({
intents: [
{
name: 'start_game',
slots: ['начать игру', 'играть', 'старт'],
},
],
});
bot.addCommand('greeting', ['привет', 'здравствуй'], (_, controller) => {
controller.text = 'Здравствуйте!';
});
src/
├── controller/ # Контроллеры с логикой (Если нужно)
├── plugins/ # Дополнительные плагины (Если нужно)
├── utils/ # Вспомогательные функции (Если нужно)
├── config/ # Конфигурация (Если нужно)
└── index.ts # Точка входа
interface IGameState {
score: number;
level: number;
lastAction?: string;
}
class GameController extends BotController<IGameState> {
public action(intentName: string | null): void {
// Теперь this.userData типизирован как IGameState
this.userData.score = 100;
}
}
try {
// Ваша асинхронная логика (запрос к API, работа с БД и т.д.)
const result = await fetchExternalData();
this.text = `Успешно: ${result}`;
} catch (error) {
console.error('Ошибка:', error);
this.text = 'Извините, произошла ошибка';
}
// Проверка первого запуска
if (!this.userData.initialized) {
this.userData.initialized = true;
this.userData.score = 0;
}
// Сброс состояния — мутируйте, а не переприсваивайте
if (intentName === 'restart') {
Object.keys(this.userData).forEach((key) => delete this.userData[key]);
this.text = 'Игра начата заново';
}
Важно: не делайте
this.userData = {};— фреймворк хранит ссылку на объект и при полном переприсваивании отслеживание изменений может сломаться. Вместо этого мутируйте или удаляйте поля по одному.Нюанс Алисы: у локального хранилища Алисы
deleteи= undefinedне удаляют поле — при следующем запросе оно вернётся со старым значением. Для удаления поля на Алисе присваивайтеnull:this.userData.tempData = null.
import { BotTest } from 'umbot/test';
import { fullPlatforms } from 'umbot/plugins';
const bot = new BotTest();
bot.use(fullPlatforms);
// Запускает интерактивный режим в консоли: вы вводите фразы, приложение отвечает
bot.test();
Консольный режим BotTest не требует сети, но для проверки с реальной платформой нужен вебхук.
Платформы не умеют отправлять запросы на localhost — им нужен публичный HTTPS-адрес.
На время разработки поднимите туннель, который пробросит ваш локальный порт в интернет:
# ngrok
ngrok http 3000
# или cloudflared (Cloudflare Tunnel)
cloudflared tunnel --url http://localhost:3000
Инструмент выдаст публичный URL вида https://xxxx.ngrok-free.app. Запустите приложение
(bot.start('localhost', 3000)) и укажите этот URL в качестве вебхука в консоли разработчика
платформы. После отладки удалите URL и разверните приложение на сервере с HTTPS
(см. «Запуск в production»).
⚠️ URL туннеля временный и подходит только для разработки. Не оставляйте продакшн-вебхук указывать на туннель.
// В контроллере
console.log('Данные:', this.userData);
console.log('Команда:', this.userCommand);
// В конфигурации
bot.setAppConfig({
error_log: './logs',
});
bot.setAppMode('dev');
Для продакшн‑окружения используйте режим strict_prod, настройте webhook и включите проверку подписи вебхука.
bot.setAppMode('strict_prod'); // включает строгие проверки безопасности (блокирует ReDoS-регулярки)
bot.start('0.0.0.0', 8080); // запуск HTTP-сервера
Пока проверка подписи не включена, любой, кто узнает URL вашего вебхука, может отправлять боту
поддельные запросы от имени любого пользователя — в том числе обходить авторизацию по userId
и читать/перезаписывать чужие данные. URL вебхука утекает легко (логи, реестры доменов), поэтому
секрет нужно задать до первого продакшн-запроса:
bot.setAppConfig({
tokens: {
// Секрет задаётся с двух сторон: при регистрации вебхука у платформы
// (setWebhook у Telegram, настройки группы VK, подписка MAX) и здесь.
telegram: { webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET },
vk: { secret_key: process.env.VK_SECRET_KEY },
max_app: { webhookSecret: process.env.MAX_WEBHOOK_SECRET },
// Viber проверяет подпись автоматически по самому токену бота — ничего дополнительно задавать не нужно.
},
});
Что за что отвечает каждая платформа и как сгенерировать секрет — в
configuration.md → Проверка подписи вебхука.
У Алисы, Маруси и SmartApp подписи вебхука нет в принципе (ограничение платформ): не считайте
userId этих платформ аутентифицированной идентичностью.
Убедитесь, что всё выполнено:
strict_prod — включен через bot.setAppMode('strict_prod')webhookSecret (Telegram/MAX) или secret_key (VK); при старте в логе нет предупреждения «БЕЗ проверки подписи». Для Алисы/Маруси/SmartApp подписи нет — продумайте собственную верификацию чувствительных действийbot.setPlatformParams({ intents: [...] }). Учтите: переданный массив заменяет встроенные интенты welcome/help, поэтому либо добавьте их в свой список, либо задайте собственные слоты для приветствия и помощи.gitignorebot.use(rateLimiter()) для защиты от превышения лимитов платформbot.setAppConfig({ error_log: './logs' })Причина: Регистр. controller.userCommand автоматически приводится к нижнему регистру.
// ❌ Неправильно — слот с заглавной буквы
bot.addCommand('greet', ['Привет'], (_, bc) => {
bc.text = 'Привет!';
});
// ✅ Правильно — слот в нижнем регистре
bot.addCommand('greet', ['привет'], (_, bc) => {
bc.text = 'Привет!';
});
Причина: Не задан свой welcome_text в setPlatformParams — фреймворк отвечает placeholder-текстом по умолчанию.
// ❌ Не настроено — ответит стандартным текстом приветствия
bot.setPlatformParams({ intents: [] });
// ✅ Правильно — свой текст приветствия
bot.setPlatformParams({
welcome_text: 'Привет! Я могу помочь.',
intents: [],
});
Причина: userData без дженерика типизирован как IUserData с индексной сигнатурой [key: string]: unknown —
запись любого поля разрешена, но чтение в арифметике (+= 10) уже нет: значение имеет тип unknown.
// ❌ Неправильно — TypeScript не знает про score
bot.addCommand('play', ['играть'], (_, bc) => {
bc.userData.score += 10; // Ошибка!
});
// ✅ Правильно — аннотируем тип
bot.addCommand('play', ['играть'], (_, bc: BotController<MyData>) => {
bc.userData.score += 10; // OK
});
Причина: Не подключен DB-adapter и isLocalStorage: false.
import { MongoAdapter } from 'umbot/plugins';
// ❌ Неправильно — данные теряются
bot.setAppConfig({ isLocalStorage: false });
// ✅ Вариант 1: локальное хранилище (для голосовых платформ)
bot.setAppConfig({ isLocalStorage: true });
// ✅ Вариант 2: БД (для чат-ботов)
bot.use(new MongoAdapter({ host: '...', database: '...' }));
bot.setAppConfig({ isLocalStorage: false });
Причина: Используете BotController вместо BaseBotController. Автоматическая установка empty_text работает только через BaseBotController. Если вы наследуетесь от BotController напрямую, задайте this.text в action(). Адаптеры не придумывают ответ: Алиса и Маруся сохранят пустые поля и запишут предупреждение, а чат-платформы не станут отправлять недопустимое пустое сообщение.
Подробнее об этом механизме — в разделе «Порядок диспетчера» в GUIDE.md.
// Решение: вручную обрабатывайте default-case в action()
public action(intentName: string | null): void {
switch (intentName) {
case WELCOME_INTENT_NAME:
this.text = 'Привет!';
break;
default:
if (!this.text) this.text = 'Не поняла. Скажите "помощь".';
}
}
При использовании регулярных выражений в командах (addCommand(..., isPattern: true)) или интентах, фреймворк проверяет их на потенциальные ReDoS‑уязвимости.
⚠️ По умолчанию (appMode: 'dev') небезопасные RegExp всё равно регистрируются!
Это сделано для гибкости в разработке, но порой недопустимо в production.
✅ Рекомендация для production включить строгую проверку:
const bot = new Bot();
bot.setAppMode('strict_prod'); // ← обязательно включите!
При setAppMode('strict_prod') любая потенциально опасная RegExp будет отклонена при регистрации, а попытка её использовать вызовет ошибку в логах.
⚠️ Если вы используете slots с RegExp, убедитесь, что ваши выражения:
Достаточно создать адаптер для нужной платформы согласно документации по созданию адаптера платформы и после подключить его к приложению. Если все сделано верно, то при получении запроса от новой платформы, фреймворк корректно отработает запрос, и вернет данные в нужном для платформы виде.
Достаточно сохранить чувствительные данные в .env файл, передав путь к нему:
bot.setAppConfig({
env: './.env', // путь до файла
});
Пример содержимого .env файла:
TELEGRAM_TOKEN=your-telegram-token
VK_TOKEN=your-vk-token
VK_CONFIRMATION_TOKEN=your-vk-confirmation-token
VIBER_TOKEN=your-viber-token
ALISA_TOKEN=your-alisa-token
MARUSIA_TOKEN=your-marusia-token
MAX_TOKEN=your-max-token
SMARTAPP_TOKEN=your-smartapp-token
# Yandex SpeechKit — TTS для чат-платформ (Telegram/VK/Max);
# значение автоматически записывается в speech_kit_token всех трёх платформ.
# Рекомендуется API-ключ сервисного аккаунта (уходит как `Api-Key`); IAM-токен `t1.…`
# тоже принимается (уходит как `Bearer`), но живёт не больше 12 часов.
SPEECH_KIT_TOKEN=your-speechkit-api-key
# Подключение к MongoDB: host — полная connection string с протоколом
DB_HOST=mongodb://localhost:27017
DB_USER=user
DB_PASSWORD=password
DB_NAME=bot_db
YANDEX_TOKENдля Алисы устарел и сохранён только для обратной совместимости — используйтеALISA_TOKEN(при обоих заданных приоритет у него).SMARTAPP_TOKENнужна только для SmartApp (Сбер); при работе без этой платформы её можно не задавать.
⚠️ Не коммитьте
.envв git! Он уже добавлен в шаблонный.gitignoreпри генерации через CLI, но если создаёте файл вручную — проверьте, что он в исключениях.
Если все необходимые токены лежат в process.env, то можно в свойство env передать значение local.
bot.setAppConfig({
env: 'local', // Получить данные из process.env
});
Данные в this.userData автоматически сохраняются между сессиями. Выберите способ хранения через параметр
isLocalStorage:
bot.setAppConfig({
isLocalStorage: true, // Данные хранятся в локальном хранилище платформы (поддерживается голосовыми платформами)
// или
isLocalStorage: false, // данные хранятся в вашей БД (подключите адаптер: MongoAdapter и т.п.)
});
У чат-платформ (Telegram, VK, Max, Viber) локального хранилища нет: при
isLocalStorage: trueбез подключённого БД-адаптера их данные не сохранятся.
this.buttons.addBtn('Да').addBtn('Нет').addBtn('Не знаю');
this.card
.addImage('image1.jpg', 'Заголовок 1', 'Описание 1')
.addImage('image2.jpg', 'Заголовок 2', 'Описание 2')
.setTitle('Галерея изображений');
Больше вопросов и ответов можно найти в разделе FAQ.