umbot
    Preparing search index...

    Интеграция с платформами

    Фреймворк umbot обеспечивает единое API для разработки голосовых навыков и чат-ботов на всех ведущих российских и международных платформах.

    Возможность umbot Jovo SaluteJS Нативный SDK
    Алиса + Маруся + Сбер ✅ ❌ ⚠️ Сбер Требуется ручная маршрутизация и дублирование логики
    Единая бизнес-логика ✅ ✅ ❌ ❌
    Поддержка Telegram / VK / Viber ✅ ⚠️ Частично ❌ Требуется ручная маршрутизация и дублирование логики
    TypeScript «из коробки» ✅ ✅ ✅ ⚠️ Зависит от sdk

    Сильная сторона umbot — полный российский стек голосовых ассистентов (Алиса, Сбер SmartApp, Маруся) в одном коде. SaluteJS — нативный SDK экосистемы Сбера (Салют), поэтому Сбер для него родная платформа, но мультиплатформенность (Алиса, Маруся, чат-боты) в нём не поддерживается. Jovo сфокусирован на мультиплатформенных чат-ботах (из тройки Telegram / VK / Viber у него есть Telegram и Viber, VK — нет) и не интегрирован с российскими голосовыми платформами. Нативные SDK (telegraf, alice-sdk, vk-io) ориентированы на одну платформу и требуют дублирования логики при мультиплатформенности. Актуальные списки поддерживаемых платформ см. в их официальных документациях.

    Платформа Идентификатор Статус
    Яндекс.Алиса alisa ✅ Протокол навыков целиком
    Маруся marusia ⚠️ Только существующие навыки: VK закрыла создание новых (20.12.2024)
    Сбер SmartApp smart_app ✅ Протокол SmartApp API
    Telegram telegram ✅ Базовый набор (остальное API — через controller.api / TelegramRequest)
    VK vk ✅ Базовый набор (остальное API — через controller.api / VkRequest)
    MAX max_app ✅ Базовый набор (остальное API — через controller.api / MaxRequest)
    Viber viber ✅ Базовый набор. Новые боты Viber — только на коммерческих условиях
    Любая другая платформа ... ✅ Через адаптеры

    Что входит в базовый набор мессенджеров по каждой платформе — в разделах ниже и в «Сравнении контрактов платформ».

    Выбор платформы происходит автоматически в зависимости от запроса, который пришел в приложение, главное не забыть подключить адаптеры для платформ. Также есть возможность явно указать какая именно платформа используется:

    const bot = new Bot('max_app');
    
    • HTTPS с валидным SSL-сертификатом
    • Стабильное время ответа (рекомендуется < 3 секунд)
    • Поддержка webhook URL
    • Node.js 20.19+ и TypeScript 5+
    import { Bot } from 'umbot';
    import { fullPlatforms } from 'umbot/plugins';

    const bot = new Bot();
    bot.use(fullPlatforms); // Подключаем все доступные платформы
    bot.setPlatformParams({
    // Параметры платформы
    welcome_text: 'Привет!', // Текст приветствия
    help_text: 'Я умею...', // Текст помощи
    intents: [],
    });
    bot.setAppConfig({
    // Общие параметры
    json: './data', // Директория для JSON данных
    error_log: './logs', // Директория для логов
    isLocalStorage: true, // Использование локального хранилища
    });
    bot.start('localhost', 3000); // Запуск приложения

    Bot сам определяет, от какой платформы пришёл запрос — по телу запроса и заголовкам. Вам ничего настраивать не нужно: один webhook-эндпоинт принимает запросы от всех платформ.

    Если авто-определение не справляется (редкий случай, обычно при проксировании через свой шлюз), его можно переопределить:

    bot.setPlatformResolver((query, headers, detect) => {
    // detect() запускает стандартное авто-определение
    if (headers?.['x-my-routing'] === 'alice') return 'alisa';
    return detect ? detect(query, headers) : null;
    });

    У каждой платформы свои лимиты на длину текста, число кнопок, размер карточки и state. Адаптеры приводят ответ к допустимому виду сами, поэтому код остаётся одинаковым для всех платформ:

    • Кнопки сверх лимита отбрасываются с предупреждением в лог. Лимиты адаптеров: Алиса, Маруся и VK — 10 кнопок, SmartApp — 8, Viber — 6, MAX — 30, Telegram — 40. Лишние кнопки в ряду (buttons.row()) переносятся на следующую строку.
    • Текст длиннее лимита обрезается: Алиса и Маруся — 1024 символа, Telegram и VK — 4096, MAX — 4000, Viber — 7000, SmartApp — 250 в «пузыре».
    • Payload кнопки больше лимита — кнопка пропускается с предупреждением; данные не обрезаются и не переписываются.
    • State больше лимита (Алиса — 1 КБ, Маруся — 3584 байта) не отправляется, ошибка пишется в лог.
    • Устройство без экрана (колонка) — кнопки и карточки не отправляются.

    Обрезать бизнес-логику фреймворк не может: время ответа голосовым платформам — ваша зона ответственности. Фреймворк пишет предупреждение после 2 с обработки и ошибку после 2,9 с; медиа загружайте заранее через Preload.

    По умолчанию платформа сама присылает запрос на ваш HTTPS-адрес — вебхук (bot.start(), webhookHandle, webhookEvent). Telegram, VK и MAX умеют ещё и отдавать обновления по запросу: bot.startPolling() запускает long polling (getUpdates у Telegram, Bots Long Poll у VK, GET /updates у MAX), и публичный адрес не нужен — удобно для локальной разработки и серверов без HTTPS.

    bot.use(new TelegramAdapter(process.env.TELEGRAM_TOKEN));
    await bot.startPolling(); // выполняется после bot.stopPolling(), bot.close() или SIGINT/SIGTERM
    • Обновление проходит тот же конвейер, что и вебхук (middleware, команды, очередь пользователя), кроме проверки подписи: оно получено от API по токену бота. Обновления одной пачки выполняются параллельно, не больше 32 одновременно; обновления одного пользователя — по очереди, в порядке пачки.
    • IP клиента у polling нет: ipFilter с rejectWithoutIp: true отклонит все обновления. Для бота на polling ipFilter не нужен — запросы к платформе делает сам бот.
    • Ошибка сети или 5xx — повтор с паузой от 1 до 30 секунд. Неверный токен или активный вебхук у Telegram (ответ 409) останавливают polling этой платформы с причиной в логе.
    • Telegram: polling не работает, пока у бота зарегистрирован вебхук (ответ 409). Вебхук не снимается молча — токен может принадлежать production-боту. Возьмите для разработки другой токен или снимите вебхук явно опцией new TelegramAdapter(token, { telegram_delete_webhook: true }): адаптер вызовет deleteWebhook при первом запросе и запишет предупреждение в лог. В режиме polling ответ всегда уходит через API: опция telegram_webhook_reply не действует.
    • VK: нужен токен сообщества и включённый Long Poll API («Работа с API» → «Long Poll API») с нужными типами событий. Секрет Callback API (VK_SECRET_KEY) для polling не нужен. Имена авторов сообщений пачки загружаются одним запросом users.get.
    • MAX рекомендует polling для разработки и тестов, в продакшене — вебхук (POST /subscriptions). По документации MAX первый запрос без marker отдаёт только последнее накопившееся событие: сообщения, пришедшие до запуска бота, кроме последнего, не обрабатываются.
    • Можно совмещать: например, bot.start() для Алисы и bot.startPolling({ platforms: ['telegram'] }) для Telegram.

    Алиса, Маруся, SmartApp и Viber работают только через вебхук: для проверки на локальной машине нужен туннель (ngrok и аналоги, см. getting-started). Без сети логику можно проверить в консоли через BotTest (umbot/test).

    Как фреймворк обрабатывает поток вебхуков:

    • Запросы одного пользователя выполняются по очереди (ключ — платформа + userId). Двойное нажатие кнопки или несколько соединений вебхука больше не приводят к тому, что два обработчика читают одни и те же userData и сохраняется только последний. Запросы разных пользователей идут параллельно. Если предыдущий запрос пользователя обрабатывается дольше 10 секунд, следующий начинается, не дожидаясь его (у Алисы, Маруси и SmartApp — не дольше половины оставшегося срока ответа). Очередь живёт в памяти процесса: при нескольких репликах направляйте запросы одного пользователя на одну реплику.
    • Повторные доставки не обрабатываются дважды. Telegram, VK, MAX и Viber повторяют доставку, если не получили ответ 2xx вовремя (у MAX — 30 секунд). Фреймворк помнит принятые доставки час (до 10 000 в памяти процесса) и на повтор отвечает 200 ok, не запуская логику. Ключ включает хэш тела запроса, поэтому поддельный запрос с угаданным update_id не заблокирует настоящий апдейт. Повтор, пришедший во время обработки исходного запроса, ждёт его исхода (до 30 секунд); если исходный упал с ошибкой сервера (500), повтор обрабатывается заново. Не дедуплицируются события, ответ на которые несёт содержимое: Telegram в режиме telegram_webhook_reply, confirmation у VK, webhook и conversation_started у Viber. Дедупликация работает только для запросов через webhookHandle / webhookEvent (bot.run() её не выполняет).
    • Аккаунт разработчика в Яндекс.Диалоги
    • HTTPS endpoint для webhook
    • Время ответа < 3 секунд
    1. Создайте навык в консоли Яндекс.Диалоги
    2. Получите OAuth токен в Яндекс.OAuth если он необходим. Токен нужен для загрузки аудио или изображений.
    3. Настройте параметры в коде:
    bot.setPlatformParams({
    isAuthUser: true, // Для работы с авторизацией
    intents: [],
    });
    bot.use(new AlisaAdapter('YOUR_OAUTH_TOKEN')); // Способ 1: токен в конструкторе (приоритет выше)
    // bot.setAppConfig({ // Способ 2: токен в конфиге (альтернатива, если не передан в конструкторе)
    // tokens: {
    // alisa: {
    // token: 'YOUR_OAUTH_TOKEN',
    // },
    // },
    // });

    Токен можно не указывать в коде: переменная окружения ALISA_TOKEN подхватывается автоматически (без настройки env в конфиге). Старое имя YANDEX_TOKEN сохранено для обратной совместимости — если заданы обе переменные, приоритет у ALISA_TOKEN.

    • Поддержка авторизации пользователей
    • Локальное хранилище данных
    • Встроенная система синтеза речи
    • Поддержка карточек и галерей
    class AlisaController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    // Проверка авторизации
    if (!this.userToken) {
    this.isAuth = true;
    this.text = 'Для продолжения необходима авторизация';
    return;
    }

    // Работа с авторизованным пользователем
    this.text = `Привет, ${this.nlu.getUserName()?.first_name || 'пользователь'}!`;
    this.tts = 'Привет! Рад вас видеть снова!';

    // Добавление карточки
    this.card.addImage('image_token', 'Добро пожаловать', 'Описание', 'Кнопка');

    // Добавление кнопок
    this.buttons.addBtn('Помощь').addBtn('Начать игру');
    }
    }
    }
    • Бот, созданный через @BotFather
    • HTTPS webhook URL
    • Поддержка Telegram Bot API
    1. Получите токен у @BotFather

    2. Быстрый путь для шагов 2–3: npx umbot webhook telegram https://ваш-домен/webhook в папке проекта. Команда берёт TELEGRAM_TOKEN из .env, генерирует секрет, регистрирует вебхук с ним и сохраняет TELEGRAM_WEBHOOK_SECRET в .env — фреймворк подхватит его сам. Вручную: сгенерируйте секрет вебхука (одна и та же строка понадобится в двух местах):

      node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"
      
    3. Зарегистрируйте webhook, передав секрет в secret_token:

      curl "https://api.telegram.org/bot<ТОКЕН>/setWebhook" \
      -d "url=https://ваш-домен/webhook" \
      -d "secret_token=<СЕКРЕТ>"
    4. Настройте параметры в коде (секрет — тот же, что в setWebhook):

    bot.use(new TelegramAdapter('YOUR_BOT_TOKEN')); // Способ 1: токен в конструкторе (приоритет выше)
    // bot.setAppConfig({ // Способ 2: токен в конфиге (альтернатива)
    // tokens: {
    // telegram: {
    // token: 'YOUR_BOT_TOKEN',
    // webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET, // тот же секрет, что в setWebhook
    // },
    // },
    // });

    Проверка подлинности запросов. Задайте appConfig.tokens.telegram.webhookSecret — адаптер будет проверять заголовок x-telegram-bot-api-secret-token и отклонять запросы не от Telegram (401 до выполнения логики). Без webhookSecret адаптер принимает любой запрос с полем update_id — любой, кто узнает URL вебхука, сможет слать сообщения от имени любого пользователя; это допустимо только для локальной отладки. Подробнее — в configuration.md → Проверка подписи вебхука.

    • Богатый набор UI элементов
    • Поддержка файлов и медиа
    • Inline кнопки и клавиатура
    class TelegramController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Привет! Я Telegram бот на umbot';

    // Добавление inline кнопок
    this.buttons
    .addBtn('Веб-сайт', 'http://localhost')
    .addBtn('Помощь', null, { command: 'help' });

    // Отправка изображения
    this.card.addImage('image_url', ' ', 'Описание изображения');
    }
    }
    }
    • Группа ВКонтакте
    • Права администратора группы
    • Включены сообщения сообщества
    1. Создайте группу ВКонтакте
    2. Получите ключ доступа в настройках группы (управление сообществом → работа с API; портал для разработчиков — dev.vk.com)
    3. Настройте Callback API и включите «Секретный ключ» в его настройках (без этого проверять подпись нечем — см. примечание ниже)
    4. Настройте параметры в коде:
    bot.use(
    new VkAdapter('YOUR_BOT_TOKEN', {
    vk_confirmation_token: 'YOUR_CONFIRMATION_TOKEN',
    vk_secret_key: 'YOUR_SECRET_KEY', // тот же «Секретный ключ», что включён в настройках группы
    vk_api_version: '5.199',
    }),
    ); // Способ 1: токен и опции в конструкторе (приоритет выше)
    // bot.setAppConfig({ // Способ 2: токен в конфиге (альтернатива)
    // tokens: {
    // vk: {
    // token: 'YOUR_BOT_TOKEN',
    // confirmation_token: 'YOUR_CONFIRMATION_TOKEN',
    // secret_key: 'YOUR_SECRET_KEY',
    // api_version: '5.199',
    // },
    // },
    // });

    Примечание: В конструкторе VkAdapter ключи передаются с префиксом vk_ (vk_confirmation_token, vk_secret_key, vk_api_version), а в appConfig.tokens.vk — без префикса (confirmation_token, secret_key, api_version). Оба формата валидны и фреймворком поддерживаются.

    Проверка подлинности запросов. VK присылает secret в теле каждого callback-запроса, когда в настройках группы включён «Секретный ключ»; адаптер сверяет его с secret_key константным по времени сравнением. Без secret_key адаптер принимает любой запрос с полями type + group_id — любой, кто узнает URL вебхука, сможет слать сообщения от имени любого пользователя. Если секрет в группе включить нельзя — ограничьте доступ через ipFilter (диапазоны IP VK Callback API).

    • Поддержка карусели сообщений
    • Клавиатура сообщений
    • Работа с вложениями
    • Интеграция с VK API
    class VKController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Привет! Я бот ВКонтакте';

    // Добавление клавиатуры
    this.buttons.addBtn('Меню').addBtn('Помощь').addBtn('О нас', 'https://vk.ru/group');

    // Отправка карусели
    this.card
    .addImage('photo_token_1', 'Товар 1', '100 руб.')
    .addImage('photo_token_2', 'Товар 2', '200 руб.');
    }
    }
    }
    • Требуется верифицированный профиль организации/ИП
    1. Перейдите в профиль вашей организации на платформе
    2. В разделе Чат-боты нажмите Создать
    3. Заполните данные в настройках бота (его карточке) и нажмите Создать
    4. Зарегистрируйте вебхук с секретом: npx umbot webhook max https://ваш-домен/webhook (MAX принимает только HTTPS на порту 443). Команда берёт MAX_TOKEN из .env, вызывает POST /subscriptions с сгенерированным секретом и сохраняет его в .env как MAX_WEBHOOK_SECRET — фреймворк подхватит его сам.
    bot.use(new MaxAdapter('YOUR_BOT_TOKEN', { secret: 'YOUR_WEBHOOK_SECRET' })); // Способ 1: токен + секрет вебхука
    // bot.setAppConfig({ // Способ 2: токен в конфиге (альтернатива)
    // tokens: {
    // max_app: {
    // token: 'YOUR_BOT_TOKEN',
    // webhookSecret: process.env.MAX_WEBHOOK_SECRET, // тот же секрет, что у подписки бота
    // },
    // },
    // });

    Проверка подлинности запросов. MAX передаёт секрет заголовком x-max-bot-api-secret. Задайте его вторым аргументом конструктора ({ secret: ... }) или в appConfig.tokens.max_app.webhookSecret — адаптер начнёт отклонять запросы с неверным заголовком (401). Без секрета адаптер принимает любой запрос с полями update_type + timestamp — любой, кто узнает URL вебхука, сможет слать сообщения от имени любого пользователя; допустимо только для локальной отладки. Подробнее — в configuration.md → Проверка подписи вебхука.

    • Поддержка карусели сообщений
    • Клавиатура сообщений
    • Работа с вложениями
    • Интеграция с MAX API
    class MaxController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Привет! Я бот в MAX';

    // Добавление клавиатуры
    this.buttons
    .addBtn('Меню')
    .addBtn('Помощь')
    .addBtn('О нас', 'https://dev.max.ru/docs/chatbots/bots-create');

    // Отправка карусели
    this.card
    .addImage('photo_token_1', 'Товар 1', '100 руб.')
    .addImage('photo_token_2', 'Товар 2', '200 руб.');
    }
    }
    }
    • Учётная запись бота, созданная в Viber Admin Panel
    • HTTPS webhook URL
    • Имя отправителя (sender), совпадающее с именем бота в Viber
    1. Создайте бота в Viber Admin Panel и скопируйте токен бота
    2. Укажите webhook URL на вашем сервере. Адаптер сам отвечает 200 на служебное событие webhook, которое Viber присылает при регистрации вебхука — без этого вебхук не зарегистрируется
    3. Настройте параметры в коде:
    bot.use(
    new ViberAdapter('YOUR_BOT_TOKEN', {
    viber_sender: 'YOUR_BOT_NAME', // обязательно: имя бота в Viber
    }),
    ); // Способ 1: токен и опции в конструкторе (приоритет выше)
    // bot.setAppConfig({ // Способ 2: токен в конфиге (альтернатива)
    // tokens: {
    // viber: {
    // token: 'YOUR_BOT_TOKEN',
    // sender: 'YOUR_BOT_NAME',
    // },
    // },
    // });

    Примечание: Подлинность запросов Viber подтверждает заголовком x-viber-content-signature — адаптер проверяет его автоматически. Формат API описан в документации Viber для разработчиков.

    • Текст сообщения до 7000 символов
    • Кнопки — rich-media (RichMedia): адаптер отправляет до 6 кнопок в текущей реализации адаптера (сетка Viber позволяет до 42: 6×7); Columns/Rows каждой кнопки задают её размер в сетке, а не число карточек
    • Звуки и TTS платформой не поддерживаются
    class ViberController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Привет! Я бот в Viber';

    // Добавление кнопок
    this.buttons.addBtn('Помощь').addBtn('О нас', 'https://example.com');
    }
    }
    }
    • Аккаунт разработчика VK
    • HTTPS endpoint
    • Поддержка протокола Маруси
    1. Создайте навык в консоли разработчика Маруси (документация по навыкам — vk.com/dev/marusia_skill_docs)
    2. Получите токен для загрузки медиа
    3. Настройте параметры:
    bot.use(new MarusiaAdapter('YOUR_MEDIA_TOKEN')); // Способ 1: токен загрузки медиа в конструкторе (приоритет выше)
    bot.setAppConfig({
    isLocalStorage: true,
    // tokens: { // Способ 2: токен в конфиге (альтернатива)
    // marusia: {
    // token: 'YOUR_MEDIA_TOKEN',
    // },
    // },
    });

    Токен нужен не только для картинок, но и для загрузки собственных звуков. С 3.1.0 MarusiaSound умеет загружать аудиофайлы в Марусю (marusia.getAudioUploadLink → upload → marusia.createAudio), поэтому кастомные звуки работают у обеих голосовых платформ — у Алисы и Маруси. Предзагрузка — через Preload.loadSounds(paths, [T_ALISA, T_MARUSIA]): токены звуков кэшируются в БД (как у Алисы), маршрут тот же, что и в контрактной сверке (раздел 6, «Исходящие API-запросы Маруси»). В обработчике достаточно работать с controller.sound — адаптер сам подберёт токен по пути к файлу.

    • Поддержка голосового ввода/вывода
    • Локальное хранилище
    • Карточки: BigImage ({type, image_id}) и ItemsList ({type, items: [{image_id}]}); image_id — integer. Заголовков, описаний и кнопок у карточек Маруси нет, типа ImageGallery в протоколе нет — галерея отправляется как ItemsList (до 7 изображений, у списка — до 5)
    • ⚠ С 20.12.2024 VK прекратил создание и поддержку пользовательских скиллов Маруси (документация протокола удалена с dev.vk.com; формат сверен по архивной копии).
    • Загрузка собственных звуков (через токен загрузки медиа, см. «Настройку» выше)
    • Health-check: на служебный ping фреймворк автоматически отвечает pong
    class MarusiaController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Привет! Я навык для Маруси';
    this.tts = 'Привет! Я готова помочь вам';

    // Добавление карточки
    this.card.addImage('image_token', 'Добро пожаловать', 'Выберите действие');

    // Добавление кнопок
    this.buttons.addBtn('Начать').addBtn('Помощь');
    }
    }
    }
    • Аккаунт разработчика Сбера
    • HTTPS endpoint
    • Поддержка SmartApp протокола
    1. Создайте приложение в портале разработчика Сбера
    2. Настройте параметры:
    bot.use(new SmartAppAdapter()); // Токен не нужен — аутентификация через Sber-экосистему
    bot.setAppConfig({
    isLocalStorage: true,
    });

    Почему нет токена? SmartApp использует встроенную аутентификацию платформы Сбербанка — приложение проходит проверку через экосистему Сбера при регистрации, отдельный API-токен не требуется.

    • Поддержка Canvas App
    • Встроенные сценарии
    • Богатый UI
    • Интеграция с экосистемой Сбера
    class SmartAppController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Привет! Я SmartApp на umbot';

    // Добавление карточки
    this.card.addImage('image_token', 'Добро пожаловать', 'Выберите действие');

    // Добавление кнопок
    this.buttons.addBtn('Начать').addBtn('Помощь');
    }
    }
    }
    const bot = new Bot();
    bot.use(new MyAdapter()); // Задаем кастомный адаптер
    // MyAdapter.ts
    import { BasePlatformAdapter, TContent } from 'umbot/plugins';
    import { BotController } from 'umbot';
    import { Text } from 'umbot';

    class MyAdapter extends BasePlatformAdapter {
    /**
    * Уникальное имя платформы
    */
    platformName: string = 'my_platform';

    /**
    * Возвращает признак того, соответствует ли запрос текущей платформе или нет
    * @param query - Тело запроса
    * @param headers - HTTP-заголовки
    */
    isPlatformOnQuery(query: unknown, headers?: Record<string, unknown>): boolean {
    const q = query as Record<string, unknown>;
    return !!(q.data && (q.data as Record<string, unknown>).messageCount !== undefined);
    }

    /**
    * Обработка полученного запроса. В данном методе необходимо настроить botController необходимыми данными
    * @param query - Запрос от платформы
    * @param controller - Контроллер приложения
    */
    setQueryData(query: unknown, controller: BotController): boolean | Promise<boolean> {
    if (!query) {
    // ошибки адаптера пишите через логгер контекста, а не в console напрямую
    controller.appContext.logError('MyAdapter.setQueryData(): отправлен пустой запрос');
    return false;
    }
    let content: Record<string, unknown>;
    if (typeof query === 'string') {
    content = JSON.parse(query);
    } else {
    content = query as Record<string, unknown>;
    }

    const data = content.data as Record<string, unknown> | undefined;

    controller.requestObject = content;
    controller.userId = content.userId as string;
    controller.userCommand = ((data?.text as string) || '').toLowerCase();
    controller.originalUserCommand = (data?.text as string) || '';
    controller.messageId = data?.messageCount as number;

    if (content.store) {
    controller.state = content.store as Record<string, unknown>;
    }

    controller.isScreen = false;

    return true;
    }

    /**
    * Возвращает результат, который будет отправлен платформе.
    * @param controller
    */
    getContent(controller: BotController): TContent {
    return {
    text: controller.text,
    tts: controller.tts,
    };
    }

    /**
    * Возвращает демо результат запроса, который будет приходить от платформы
    * @param query Запрос пользователя
    * @param userId Идентификатор пользователя
    * @param count Порядковый номер запроса
    * @param state Данные из локального хранилища
    */
    getQueryExample(
    query: string,
    userId: string,
    count: number,
    state: Record<string, unknown> | string,
    ): Record<string, unknown> {
    return {
    userId,
    data: {
    text: query.toLowerCase(),
    messageCount: count,
    },
    store: state,
    };
    }
    }
    Свойство Алиса Маруся SmartApp Telegram VK Viber Max
    Голосовая (TTS native) ✅ ✅ ✅ ❌ ❌ ❌ ❌
    Локальное хранилище ✅ ✅ ✅ (внешнее API) ❌ ❌ ❌ ❌
    Проактивная отправка (bot.send) ❌ ❌ ❌ ✅ ✅ ✅ ✅
    Загрузка изображений ✅ ✅ ❌ (URL) ✅ ✅ ❌ (URL) ✅
    Загрузка файлов звуков ✅ ✅ ❌ ✅ ✅ ❌ ✅
    Стандартные звуки (S_AUDIO_*) ✅ ✅ ❌ ❌ ❌ ❌ ❌
    Эффекты S_EFFECT_* ✅ ❌ ❌ ❌ ❌ ❌ ❌
    TTS через SpeechKit (speech_kit_token) ❌ ❌ ❌ ✅ ✅ ❌ ✅
    Проверка подписи webhook ❌ ❌ ❌ ✅* ✅* ✅ ✅*
    Эмоции / appeal ❌ ❌ ✅ ❌ ❌ ❌ ❌

    Где ❌ — фича не поддерживается платформой, фреймворк просто молча проигнорирует соответствующие поля в controller. Код не сломается.

    ⚠️ Про «Проверка подписи webhook»:

    • Алиса, SmartApp, Маруся — подписи запросов нет вообще: всё содержимое payload (включая user_id) контролирует отправитель. Не интерполируйте эти данные в URL или query без экранирования и не считайте такой запрос аутентифицированным.
    • Viber — подпись проверяется автоматически всегда (x-viber-content-signature).
    • Telegram — проверка включается заданием секрета (tokens.telegram.webhookSecret → заголовок x-telegram-bot-api-secret-token); без секрета проверка отключена.
    • VK — проверка включается только если задан tokens.vk.secret_key (сверяется с полем secret в теле запроса); без секрета — пропускается.
    • MAX — проверка включается заданием tokens.max_app.webhookSecret (заголовок x-max-bot-api-secret).

    ℹ️ Про звуки:

    • Голосовые платформы (Алиса, Маруся) подставляют звуки как <speaker audio="..."> в TTS.
    • Алиса и Маруся умеют загружать ваши аудиофайлы (хелперы getSoundInDB из Alisa/Sound и Marusia/Sound — внутри используют YandexSoundRequest / MarusiaRequest); у Маруси стандартные звуки подставляются из фиксированного набора marusia-sounds/*.
    • Чат-платформы (Telegram, VK, MAX) загружают аудиофайл и отправляют его как голосовое/аудио сообщение; текстовая часть TTS при заданном speech_kit_token синтезируется через Yandex SpeechKit.
    • Viber и SmartApp маркеры звуков из TTS вычищают (в Viber soundProcessing возвращает null).
    • Жёсткое время ответа. Фреймворк сам следит за временем обработки: при ответе дольше 2000 мс пишется предупреждение, дольше 2900 мс — ошибка. Таймауты самой платформы проверяйте в её актуальной документации. Используйте Preload для медиа.
    • Лимит state Алисы: 1 КБ. Если данные больше или не сериализуются, поле state не отправляется и прежнее состояние не очищается. Для больших данных используйте адаптер базы данных.
    • Пустой ответ не дополняется фреймворком (голосовые платформы). Пустой text допустим по документации, когда заполнен tts. Если разработчик оставил пустыми оба поля, umbot сохранит их как есть и запишет предупреждение: эмпирически такой ответ может приниматься, но документация Алисы не гарантирует этот сценарий. На чат-платформах (Telegram, VK, Viber, MAX) работает фолбэк: при пустом text и заполненном tts фреймворк подставляет tts (без звуковой SSML-разметки) как текст ответа.
    • isScreen = false на колонках. Кнопки и карточки не отображаются. Проверяйте this.isScreen перед this.card.addImage(...).
    • Health check (ping). Яндекс периодически шлёт ping. Фреймворк автоматически отвечает pong.
    • Событие авторизации. Завершение account linking (account_linking_complete_event) приходит как универсальное событие auth: bot.addEvent('auth', ...) — факт linking'а фиксируется в controller.userEvents.auth. Текстовые реплики пользователя — событие message.
    • Удаление полей. delete this.userData.foo не работает — платформа вернёт старое значение. Используйте this.userData.foo = null.
    • Лимиты. Текст и TTS — до 1024 символов, state — до 3584 байт, payload кнопки — до 4096 байт (превышения: state не отправляется, кнопка пропускается с предупреждением).
    • Health check (ping). Фреймворк автоматически отвечает pong на служебные запросы платформы.
    • Карточки. BigImage и ItemsList, элементы — только image_id (integer); галерея уходит как ItemsList.
    • Текст ответа не может быть пустым (в отличие от Алисы): при пустом text он берётся из tts без разметки.
    • Нет локального хранилища. При isLocalStorage: true без DB-адаптера userData хранится в памяти процесса (memorySession): шаги работают, но данные теряются при перезапуске и не разделяются между процессами и репликами. Для надёжного хранения подключите БД.
    • TTS через SpeechKit. Для озвучки нужен appConfig.tokens.telegram.speech_kit_token (или переменная окружения SPEECH_KIT_TOKEN — она раскладывается сразу на Telegram, VK и MAX). Без него controller.tts игнорируется.
    • Разметка выключена по умолчанию. parse_mode передаётся только при явном telegram_parse_mode. При включённом HTML/MarkdownV2 разработчик отвечает за экранирование динамических данных.
    • Проактивная отправка. bot.send(userId, text, T_TELEGRAM) работает (в отличие от голосовых платформ).
    • Групповые чаты. userId берётся из from.id (человек), а не из chat.id (группа) — один пользователь получает одну запись в БД и в группе, и в личке. Ответ доставляется в исходный чат (ID чата — platformOptions.requestData.telegram.chatId).
    • События. Адаптер распознаёт все типы апдейтов (медиа, callback, inline, message_edited, channel_post, my_chat_member и др.) — неизвестные служебные апдейты подтверждаются HTTP 200 без ответа. Не-текстовые апдейты ловятся событийным роутингом: bot.addEvent('photo' | 'voice' | 'callback' | 'inline' | 'message_edited' | 'channel_post', ...) (полный список типов — в api-reference.md, раздел «Событийный роутинг»).
    • Inline-кнопки без payload (options.inline). Кнопка без payload и url по умолчанию уходит обычной reply-клавиатурой. С опцией { inline: true } она показывается inline-кнопкой под сообщением, а нажатие приходит боту как текст кнопки: this.buttons.addBtn('Каталог', '', '', { inline: true }). Текст длиннее лимита callback_data (64 байта) передаётся служебным токеном #t<n> и восстанавливается адаптером из клавиатуры сообщения. На request_contact / request_location опция не действует — Telegram принимает их только в обычной клавиатуре. Проекты, сгенерированные через npx umbot create from-flow, выставляют опцию всем кнопкам Telegram.
    • Webhook-reply (opt-in). new TelegramAdapter('TOKEN', { telegram_webhook_reply: true }): простой текстовый ответ уходит телом webhook-ответа ({method: 'sendMessage', ...}) — Telegram выполнит его сам, экономится один исходящий POST на запрос. По образцу grammy: opt-in (по умолчанию выключено), не применяется к callback/inline-запросам и ответам с карточками/звуками — они уходят штатным путём. Учтите: ошибки отправки при этом недиагностируемы (Telegram подтверждает webhook раньше реального выполнения метода).
    import { Bot } from 'umbot';
    import { TelegramAdapter, T_FORMAT_MARKDOWN, escapeMarkdownV2 } from 'umbot/plugins';

    // Вариант 1: обычный текст без parse_mode
    const botPlain = new Bot().use(new TelegramAdapter('TOKEN'));

    // Вариант 2: Явно MarkdownV2 (фреймворк не экранирует — разработчик отвечает за валидность)
    const botMd = new Bot().use(
    new TelegramAdapter('TOKEN', {
    telegram_parse_mode: T_FORMAT_MARKDOWN,
    }),
    );

    // Вариант 3: текстовый ответ телом webhook без отдельного POST
    const botWebhookReply = new Bot().use(
    new TelegramAdapter('TOKEN', {
    telegram_webhook_reply: true,
    }),
    );

    // Безопасная вставка пользовательского ввода в MarkdownV2
    botMd.addCommand('whoami', ['кто я'], (_, ctx) => {
    const userName = escapeMarkdownV2(ctx.originalUserCommand ?? '');
    ctx.text = `*Вы написали:* ${userName}`;
    });
    • Два токена. Бот-токен + vk_confirmation_token (для подтверждения вебхука при первичной настройке). Если VK прислал запрос подтверждения, а confirmation_token не задан, адаптер отвечает ok без запуска бизнес-логики и пишет в лог, где задать токен.
    • Секретный ключ. Опционально: укажите vk_secret_key в конструкторе адаптера или VK_SECRET_KEY в .env для проверки подлинности каждого запроса от VK Callback API. Если секретный ключ включён в настройках группы, VK присылает поле secret в теле каждого события — адаптер сверяет его с сохранённым значением.
    • Нет локального хранилища. При isLocalStorage: true без DB-адаптера userData хранится в памяти процесса (memorySession): шаги работают, но данные теряются при перезапуске и не разделяются между процессами и репликами. Для надёжного хранения подключите БД.
    • Имя пользователя берётся из кэша. Результат users.get (имя для nlu.getUserName()) кэшируется в памяти процесса на 1 час (до 5000 записей; ошибки API не кэшируются). Отключить загрузку можно опцией адаптера new VkAdapter(token, { vk_load_user_info: false }) — тогда getUserName() вернёт null, зато на ответ уходит один запрос к VK вместо двух. Сбросить кэш (тесты, смена имени) — clearVkUserCache() из umbot/plugins.
    • Callback-кнопки подтверждаются через messages.sendMessageEventAnswer. На нажатие callback-кнопки (message_event) адаптер подтверждает событие (sendMessageEvent без event_data — у пользователя пропадает индикатор загрузки на кнопке), а ответ обработчика отправляет обычным сообщением (messages.send). Если бизнес-логика завершилась ошибкой, вместо сообщения показывается snackbar с текстом ошибки. Чтобы показать свой snackbar, вызовите controller.api.answerCallback(text). ID события хранится в platformOptions.requestData.vk.eventId (с fallback в platformOptions.eventId).
    • Payload callback-кнопок нормализуется. Строка 'buy' или JSON {"command":"buy"} в payload попадает в userCommand как buy и срабатывает как обычная команда — без ручного разбора requestObject.
    • Раскладка кнопок. buttons.row() завершает ряд (до 5 кнопок; location/vkpay/open_app занимают ряд целиком); кнопки с одинаковым options._group (строка или число) тоже встают в один ряд.
    • Цвет кнопок. options.color: 'primary' | 'secondary' | 'positive' | 'negative'.
    • Sender name обязателен. Должен совпадать с именем бота в Viber.
    • Версия API — 7 по умолчанию. Если пользователь не передал версию явно, адаптер отправляет min_api_version: 7 (VIBER_DEFAULT_API_VERSION). Версия 7 нужна для rich_media (карточек); на старых клиентах карточки не отобразятся.
    • Звуки не поддерживаются. Кастомные звуки не отправляются; controller.tts при пустом text уходит обычным текстом (без звуковой разметки), при заполненном text — не используется.
    • Нет локального хранилища. При isLocalStorage: true без DB-адаптера userData хранится в памяти процесса (memorySession): шаги работают, но данные теряются при перезапуске и не разделяются между процессами и репликами. Для надёжного хранения подключите БД.
    • Служебные события. Адаптер обрабатывает события subscribed/unsubscribed (логируются), delivered/seen/failed (подтверждаются без ошибки), conversation_started и событие webhook при регистрации вебхука (см. «Настройку» выше). Через событийный роутинг (bot.addEvent('start' | 'subscribed' | 'unsubscribed', ...)) на них можно навесить свою логику; типы медиа-сообщений пользователя доступны как photo/video/document/ contact/location/sticker (подробности — в controller.payload и requestObject).
    • Нет локального хранилища. При isLocalStorage: true без DB-адаптера userData хранится в памяти процесса (memorySession): шаги работают, но данные теряются при перезапуске и не разделяются между процессами и репликами. Для надёжного хранения подключите БД.
    • TTS через SpeechKit. Для озвучки нужен appConfig.tokens.max_app.speech_kit_token (или переменная окружения SPEECH_KIT_TOKEN).
    • Очередь отправки. MAX ограничивает отправку в один диалог — не чаще 1 сообщения в 500 мс (и не более 2 callback-ответов в секунду на диалог). MaxRequest ставит исходящие сообщения в очередь на диалог с интервалом 500 мс, поэтому быстрые повторные ответы не получают 429 от платформы. Внутренние таймеры очереди не блокируют выход процесса.
    • Вложения сразу после загрузки. MAX обрабатывает загруженный файл не мгновенно и на сообщение с ним может ответить ошибкой attachment.not.ready. MaxRequest повторяет такую отправку до 3 раз с паузами 0,5 / 1 / 2 с; если файл так и не готов, ошибка пишется в лог. Для часто используемых картинок и звуков загружайте файлы заранее (Preload) — тогда в ответе уходит уже готовый токен.
    • Лимиты сообщения. Текст до 4000 символов, до 12 вложений в сообщении (клавиатура считается вложением), клавиатура — до 30 рядов по 7 кнопок. Превышения обрезаются фреймворком с предупреждением в лог.
    • Групповые чаты и каналы. Если в webhook есть chat_id, ответ уходит в чат, а не в личный диалог (ID чата — в platformOptions.chatId).
    • Callback-кнопки. На нажатие адаптер подтверждает callback (POST /answers с пустым телом), а ответ обработчика отправляет новым сообщением — как в Telegram и VK. Если нужно, чтобы ответ заменял сообщение с нажатой кнопкой (так устроен message в POST /answers), включите опцию new MaxAdapter(token, { max_callback_edit_message: true }). Если обработчик сам вызвал controller.api.answerCallback(text), повторного подтверждения не будет.
    • API. Базовый URL — platform-api2.max.ru; авторизация заголовком Authorization: <token> (query-параметры платформа больше не поддерживает). Детальное сравнение контракта — в platform-contract-comparison.md.
    • Без токена. Аутентификация через Sber-экосистему.
    • Эмоции. controller.emotion = 'radost' (22 варианта).
    • Rating flow. controller.isSendRating = true запускает оценку навыка.
    • События. Запуск приложения (RUN_APP) приходит как событие start, завершение оценки — как rating (текстовые реплики — message): bot.addEvent('start' | 'rating', ...).

    Как видно из примеров выше, код контроллера для всех платформ выглядит практически одинаково.
    Вы пишете логику один раз, используя универсальные методы this.text, this.buttons, this.card и т.д.
    Фреймворк сам определяет, от какой платформы пришёл запрос, и автоматически преобразует ваш ответ в нужный формат.

    Вам не нужно вручную проверять this.appType и писать разный код для Алисы, Telegram или VK —
    адаптеры платформ сделают это за вас. Единственное исключение — редкие случаи, когда требуется
    платформозависимое поведение (например, генерация UTM-меток в ссылках). Для таких ситуаций вы всегда можете
    явно обратиться к this.appType и добавить дополнительную логику.

    Благодаря такому подходу вы можете сосредоточиться на бизнес-логике вашего приложения, а не на деталях реализации под каждую платформу. Один код — работает везде.

    Две кросс-платформенные возможности 3.1.0 закрывают то, что раньше требовало ручного разбора requestObject под каждую платформу. Полный справочник API (сигнатуры, примеры) — в api-reference.md; здесь — привязка к платформам.

    Не-текстовые апдейты (фото, голосовые, callback-кнопки, редактирование сообщений, старт, подписки) приходят как универсальные события: адаптер записывает тип в controller.eventType, обработчики bot.addEvent(eventType, handler) вызываются до шагов и команд. Каждый адаптер объявляет перечень поддерживаемых событий (supportedEvents) — bot.addEvent предупреждает об опечатке или событии, которого не поддерживает ни одна подключённая платформа:

    Платформа События (supportedEvents)
    Telegram message, photo, voice, video, document, location, contact, sticker, callback, inline, message_edited, channel_post
    VK message, callback
    MAX message, callback, start, message_edited
    Viber message, photo, video, document, contact, location, sticker, start, subscribed, unsubscribed
    Алиса message, auth
    Маруся message, auth
    SmartApp message, start, rating

    Кастомная платформа, унаследованная от BasePlatform, объявляет собственный supportedEvents (базовое значение — ['message']) и автоматически участвует в валидации.

    Унифицированный доступ к возможностям активной платформы: sendPhoto / sendDocument / sendAudio / sendVideo(файл, { caption }), answerCallback(text, showAlert?) и can(method) для проверки поддержки. Фасад ленивый — создаётся при первом обращении к ctx.api; на голосовых платформах (Алиса, SmartApp, Маруся) — null (их ответ формируется телом webhook; медиа отправляются через controller.card / controller.sound).

    Метод Telegram VK MAX Viber
    sendPhoto полный через штатный upload-flow /uploads null + warn
    sendDocument полный да /uploads null + warn
    sendAudio полный нет (null) /uploads null + warn
    sendVideo полный нет (null) /uploads null + warn
    answerCallback да (showAlert поддерживает только Telegram) show_snackbar POST /answers null + warn

    Viber возвращает can() === false для всех методов: его Bot API принимает медиа только по публичному URL с обязательным size — используйте controller.card / ViberRequest напрямую.

    Кастомная платформа и controller.api: фасад подключается сам через метод адаптера createApi(controller) (контракт IPlatformAdapter). Базовая реализация BasePlatform возвращает null (фасад недоступен), поэтому платформе с исходящими API-вызовами достаточно переопределить один метод — ядро узнает об этом без правок с его стороны:

    import { BasePlatformAdapter } from 'umbot/plugins';
    import type { BotController, IControllerApi } from 'umbot';

    class MyAdapter extends BasePlatformAdapter {
    // ...
    createApi(controller: BotController): IControllerApi | null {
    return makeMyApi(controller); // своя фабрика фасада
    }
    }

    Кроме подключения адаптеров по одному, есть наборы из umbot/plugins: voicePlatforms (Алиса, SmartApp, Маруся), botPlatforms (Telegram, VK, MAX, Viber) и fullPlatforms (все 7). Список всех адаптеров — adapters из umbot/plugins.

    Если вы используете Express, Fastify или любой другой HTTP-фреймворк — вы можете интегрировать umbot через метод webhookHandle.

    import express from 'express';
    import { Bot } from 'umbot';
    import { fullPlatforms } from 'umbot/plugins';

    const app = express();
    app.use(express.json({ type: '*/*' })); // важно для Алисы/Сбера

    // Инициализация приложения
    const bot = new Bot();
    bot.use(fullPlatforms);
    bot.setAppConfig({
    json: './data',
    error_log: './logs',
    isLocalStorage: true,
    env: 'local',
    });

    // Подключение webhook-обработчика
    app.post('/webhook', async (req, res) => {
    try {
    await bot.webhookHandle(req, res);
    } catch (err) {
    console.error('Webhook error:', err);
    res.status(500).send('Internal Server Error');
    }
    });

    app.listen(3000, () => {
    console.log('Сервер запущен на http://localhost:3000/webhook');
    });

    Начиная с версии 3.0.0, фреймворк поддерживает активную отправку сообщений — то есть навык (если поддерживает) или бот может инициировать диалог с пользователем без входящего запроса.

    ⚠️ Важно: не все платформы поддерживают эту функцию. Например, Алиса, SmartApp и Маруся не позволяют отправлять сообщения без запроса. Telegram, VK, Viber и MAX поддерживают отправку через bot.send() — реализация унаследована от базового адаптера (без собственных проверок в платформенных адаптерах): для Viber нужен валидный receiver (user_id пользователя), для MAX — инициированный диалог (user_id или chat_id). Поддержка функционала зависит от используемой платформы.

    import { T_TELEGRAM } from 'umbot/plugins';

    // Отправка сообщения пользователю в Telegram
    const result = await bot.send('123456789', 'Привет! Это рассылка.', T_TELEGRAM);
    • Храните токены в переменных окружения
    • Используйте HTTPS
    • Проверяйте подпись запросов
    • Валидируйте входящие данные
    • Используйте TypeScript
    • Следуйте принципам SOLID
    • Пишите тесты
    • Ведите документацию