umbot
Version:
Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф
854 lines (853 loc) • 37.7 kB
TypeScript
/**
* Модуль контроллера - основной компонент для обработки бизнес-логики вашего приложения
*/
import { Buttons, Card, Sound, Nlu } from '../components';
import { AppContext, IAppIntent, TAppType } from '../core';
/**
* Тип статуса операции
* Определяет результат выполнения операции
*
* @remarks
* Возможные значения:
* - true: операция выполнена успешно
* - false: операция завершилась с ошибкой
* - null: операция не выполнялась
*
* @example
* ```ts
* const status: TStatus = true; // операция успешна
* const status: TStatus = false; // операция с ошибкой
* const status: TStatus = null; // операция не выполнялась
* ```
*/
export type TStatus = true | false | null;
/**
* Интерфейс событий пользователя
* Содержит информацию о различных действиях пользователя в приложении
*
* @remarks
* События включают:
* - Авторизацию пользователя
* - Оценку приложения
*
* @example
* ```ts
* const userEvent: IUserEvent = {
* auth: { // Событие авторизации
* status: true // пользователь успешно авторизовался
* },
* rating: { // Событие с оценкой
* status: true,
* value: 5 // пользователь поставил оценку 5
* }
* };
* ```
*/
export interface IUserEvent {
/**
* Информация об авторизации пользователя.
* Содержит статус авторизации и дополнительные данные
*/
auth?: {
/**
* Статус авторизации
* @remarks
* - true: авторизация успешна
* - false: авторизация не удалась
* - null: авторизация не выполнялась
*/
status: TStatus;
};
/**
* Информация об оценке приложения пользователем.
* Содержит статус оценки и её значение
*/
rating?: {
/**
* Статус выставления оценки
* @remarks
* - true: оценка выставлена
* - false: пользователь отказался от оценки
* - null: оценка не запрашивалась
*/
status: TStatus;
/**
* Числовое значение выставленной оценки
* @remarks
* Обычно от 1 до 5
*/
value?: number;
};
}
/**
* Интерфейс для пользовательских данных
* Расширяемый интерфейс для любых дополнительных данных, которые будут сохранены в БД/Локальное хранилище
*
* @remarks
* Базовое поле:
* - oldIntentName: название предыдущего интента. Актуально для случаев, когда в приложении есть какая-то последовательность действий.
* Также данное значение можно использовать при регистрации обработчика на шаг(bot.step('...', ()=>{...})).
*
* Дополнительные поля могут быть добавлены через:
* 1. Расширение интерфейса (extends)
* 2. Индексную сигнатуру [key: string]: unknown. Не рекомендуется к использованию, так как в таком случае, теряются преимущества ts
*
* @example
* ```ts
* // Способ 1: Расширение интерфейса
* interface MyUserData extends IUserData {
* name: string;
* preferences: {
* language: string;
* theme: string;
* };
* }
*
* // Способ 2: Использование индексной сигнатуры
* interface DynamicUserData extends IUserData {
* [key: string]: unknown;
* }
*
* const userData: MyUserData = {
* oldIntentName: 'greeting',
* name: 'John',
* preferences: {
* language: 'ru',
* theme: 'dark'
* }
* };
*
* const dynamicData: DynamicUserData = {
* oldIntentName: 'greeting',
* customField1: 'value1',
* customField2: 42,
* customObject: {
* nested: true
* }
* };
* ```
*/
export interface IUserData {
/**
* Название предыдущего интента.
* Используется для отслеживания контекста диалога, и реализации механизма для последовательного прохождения сценария приложения.
*
* @example
* ```ts
* this.userData.oldIntentName = 'greeting';
* ```
*/
oldIntentName?: string | null;
/**
* Дополнительные пользовательские данные.
* Может содержать любые поля, специфичные для приложения
* Важно! Если вы хотите чтобы поле было удалено, то необходимо в значение передавать null. Подобное связано с ограничением Алисы.
*/
[key: string]: unknown;
}
/**
* Интерфейс для пользовательских данных, хранящихся в локальном хранилище
* Расширяемый интерфейс для любых дополнительных данных, которые будут сохранены в Локальное хранилище
*/
export interface IPlatformData {
/**
* Дополнительные данные.
* Может содержать любые поля, специфичные для приложения
*/
[key: string]: unknown;
}
/**
* Дополнительные опции для платформ
*/
export interface IPlatformOptions {
/**
* Текст ошибки
*/
error?: string;
/**
* Время начало обработки запроса
*/
timeStart?: number;
/**
* Флаг говорящий о том, что результат выполнения приложения бы получен при обработке запроса
*/
sendInInit?: string | object | null;
/**
* Поле куда должны сохраниться пользовательские данные
*/
stateName?: string;
/**
* Флаг, говорящий о том, что в приложении может использоваться локальное хранилище платформы
*/
isState?: boolean;
/**
* Флаг, говорящий о том, что приложение использует локальное хранилище платформы
*/
usedLocalStorage?: boolean;
/**
* Информация о сессии пользователя
*/
session?: object;
/**
* Идентификатор приложения
*/
appId?: string;
/**
* ID callback-запроса
*/
callbackQueryId?: string;
}
/**
* Контроллер приложения – главный класс для реализации единой бизнес-логики вашего приложения. Именно в этом классе обрабатывается вся логика вашего приложения, которая потом передается в саму платформу, будь то голосовой навык для Алисы, либо чат-бот для VK.
*
* Этот класс связывает входящие запросы от пользователя с вашей бизнес‑логикой.
* Вы наследуетесь от `BotController`, переопределяете метод {@link action} и получаете доступ ко всем инструментам:
* кнопкам, карточкам, состоянию диалога, пользовательским данным, NLU и многому другому.
*
* Адаптеры платформ (например, `AlisaAdapter`) автоматически наполняют контроллер данными,
* вызывают внутренний метод `{@link run}` (он вызывает ваш `action()`), а затем формируют ответ на основе заполненных вами полей (`text`, `buttons`, `card` и т.д.).
*
* **Ключевая особенность:** вся логика вашего голосового навыка или бота описывается в одном месте – в методе `action()`.
* Фреймворк сам позаботится о маршрутизации: команды, интенты, шаги диалога – всё придёт в `action` с соответствующим флагом.
*
* @remarks
* Основные возможности:
* - Обработка пользовательских команд и интентов
* - Управление состоянием диалога
* - Работа с UI компонентами (кнопки, карточки)
* - Управление пользовательскими данными
*
* @example
* ```ts
* import { BotController } from 'umbot';
* // Определение пользовательских данных
* interface MyUserData extends IUserData {
* score: number;
* level: number;
* preferences: {
* language: string;
* theme: string;
* };
* }
*
* class MyController extends BotController<MyUserData> {
* public action(intentName: string | null): void {
* try {
* // Обработка приветствия
* if (intentName === WELCOME_INTENT_NAME) {
* // Текстовый ответ
* this.text = 'Привет! Чем могу помочь?';
*
* // Добавление кнопок
* this.buttons
* .addBtn('Помощь')
* .addBtn('Выход');
*
* // Добавление карточки
* this.card
* .addImage('xxx', 'Добро пожаловать!', 'Выберите действие:')
* .addButton('Начать игру')
*
* // Установка пользовательских данных
* this.userData = {
* score: 0,
* level: 1,
* preferences: {
* language: 'ru',
* theme: 'light'
* }
* };
* return;
* }
*
* // Обработка команды помощи
* if (intentName === HELP_INTENT_NAME) {
* this.text = 'Я могу помочь вам с...';
* this.buttons.addBtn('Назад');
* return;
* }
*
* // Обработка пользовательских событий
* if (this.userEvents?.auth?.status) {
* this.text = 'Вы успешно авторизовались!';
* }
*
* // Обработка оценки
* if (this.userEvents?.rating?.status) {
* const rating = this.userEvents.rating.value;
* this.text = `Спасибо за оценку ${rating}!`;
* }
*
* } catch (error) {
* // Обработка ошибки
* this.text = 'Произошла ошибка. Попробуйте позже.';
* }
* }
* }
* ```
* @see {@link action} – переопределите этот метод, чтобы добавить свою логику.
* @see Bot – основной класс приложения, управляющий адаптерами и контроллерами.
*/
export declare abstract class BotController<TUserData extends IUserData = IUserData, TPlatformState extends IPlatformData = IPlatformData> {
#private;
/**
* Текст, который будет отображен пользователю.
* Основной способ коммуникации с пользователем, так как именно этот текст пользователь увидит в интерфейсе.
*
* @example
* ```ts
* this.text = 'Привет! Чем могу помочь?';
* ```
*/
text: string;
/**
* Текст, который пользователь может услышать.
* Для голосовых платформ, озвучка будет произведена силами самой платформы, для не голосовых платформ, поведение зависит непосредственно от реализации адаптера.
* Так для некоторых стандартных адаптеров, в случае заполнения поля и указания токена yandex SpeechKit, будет отправлен запрос на преобразование текста в аудиофайл, после чего аудиофайл будет отправлен пользователю.
*
* @example
* ```ts
* this.tts = 'Привет! Я голосовой ассистент.';
* ```
*/
tts: string | null;
/**
* Уникальный идентификатор пользователя.
*
* @example
* ```ts
* this.userId = 'user_123'; // Telegram (string)
* this.userId = 123456789; // VK (number)
* this.userId = null; // не удалось получить информацию
* ```
*/
userId: string | number | null;
/**
* Пользовательский токен авторизации.
* Используется для авторизованных запросов (например, в Алисе).
*
* @example
* ```ts
* this.userToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
* ```
*/
userToken: string | null;
/**
* Дополнительная информация о пользователе.
*
* @example
* ```ts
* this.userMeta = {
* timezone: 'Europe/Moscow',
* locale: 'ru-RU'
* };
* ```
*/
userMeta: unknown | null;
/**
* ID сообщения.
* Используется для определения начала нового диалога.
*
* @example
* ```ts
* this.messageId = 12345;
* ```
*/
messageId: number | string | null;
/**
* Запрос пользователя в нижнем регистре.
*
* @example
* ```ts
* this.userCommand = 'привет мир';
* ```
*/
userCommand: string | null;
/**
* Оригинальный запрос пользователя.
* Текст запроса без изменений, включая регистр и знаки препинания.
*
* @example
* ```ts
* this.originalUserCommand = 'Привет, мир!';
* ```
*/
originalUserCommand: string | null;
/**
* Дополнительные параметры запроса.
* Может содержать любые дополнительные данные полученные от платформы.
*
* @example
* ```ts
* this.payload = {
* source: 'mobile',
* version: '1.0'
* };
* ```
*/
payload: Record<string, unknown> | string | null | undefined;
/**
* Пользовательские данные, который были сохранены.
*
* @example
* ```ts
* this.userData = {
* name: 'John',
* preferences: {
* language: 'ru'
* }
* };
* ```
*/
userData: TUserData;
/**
* Флаг необходимости авторизации.
* Определяет, требуется ли авторизация пользователя или нет.
*
* @example
* ```ts
* this.isAuth = true; // требуется авторизация
* ```
*/
isAuth: boolean;
/**
* Пользовательские событий.
* Содержит информацию об авторизации или оценке.
*
* @see IUserEvent
* @example
* ```ts
* this.userEvents = {
* auth: { status: true },
* rating: { status: true, value: 5 }
* };
* ```
*/
userEvents: IUserEvent | null;
/**
* Пользовательское локальное хранилище.
* Используется для временного хранения данных, специфичных для текущего диалога.
* Работает только при включённой опции `isLocalStorage: true` в конфигурации.
* bot.setAppConfig({
* isLocalStorage: true,
* });
*
* **Правила синхронизации с базой данных (если подключена):**
* - Если заполнены и `userData`, и `state`:
* - `userData` сохраняется в БД,
* - `state` сохраняется в локальное хранилище платформы.
* - Если заполнен только `userData` (а `state` пуст или равен `userData`):
* - данные сохраняются только в БД (состояние платформы не обновляется).
* - Если заполнен только `state`:
* - данные сохраняются только в локальное хранилище.
*
* **При загрузке данных:**
* 1. Если есть данные из локального хранилища и из БД, они попадают соответственно в `state` и `userData`.
* 2. Если есть только данные из хранилища, они копируются и в `userData`, и в `state` (для удобства).
* 3. Если есть только данные из БД, они записываются в `userData`.
*
* @see {@link userData} — для постоянного хранения данных пользователя.
*
* @example
* ```ts
* this.state = {
* lastIntent: 'greeting',
* step: 1
* };
* ```
*/
state: TPlatformState | null;
/**
* Определяет, с колонки пользователь запустил приложение или с устройства с экраном.
*
* @example
* ```ts
* this.isScreen = true; // экран доступен
* ```
*/
isScreen: boolean;
/**
* Флаг, определяющий необходимость завершения диалога. Актуально когда необходимо принудительно завершить диалог с пользователем.
* Поддержка работы флага зависит от платформы.
*
* @example
* ```ts
* this.isEnd = true; // завершить диалог
* ```
*/
isEnd: boolean;
/**
* Флаг, указывающий, что ответ уже отправлен через API и не требуется автоматическая отправка. Как правило, данный флаг стоит использовать для платформ, которые не ждут ответ в виде возвращаемого содержимого, а ожидают что к самой платформе будет отправлен запрос. Например, чат-бот для Telegram, для отображения результата пользователю, отправляется запрос к платформе с нужным содержимым.
*
* @remarks
* Если указано true, значит все необходимые запросы уже отправлены в логике приложения, и дополнительно пользователю ничего отправлять не нужно.
*
* @example
* ```ts
* this.skipAutoReply = true; // запросы уже отправлены
* ```
*/
skipAutoReply: boolean;
/**
* Полученный запрос от платформы.
* Содержит оригинальный объект запроса.
*
* @example
* ```ts
* this.requestObject = {
* command: 'start',
* payload: { source: 'mobile' }
* };
* ```
*/
requestObject: Record<string, unknown> | string | unknown | null;
/**
* Название текущего интента.
* Определяет следующий шаг диалога.
*
* @example
* ```ts
* this.thisIntentName = 'help';
* ```
*/
thisIntentName: string | null;
/**
* Эмоция для голосового ответа.
* Используется для платформ, которые поддерживают данное поведение.
*
* @example
* ```ts
* this.emotion = 'good';
* ```
*/
emotion: string | null;
/**
* Стиль обращения к пользователю.
* Определяет формальность общения, используется для платформ, которые поддерживают данное поведение.
*
* @remarks
* Возможные значения:
* - 'official': официальное обращение
* - 'no_official': неофициальное обращение
* - null: стиль не определен
*
* @example
* ```ts
* this.appeal = 'official'; // официальное обращение
* ```
*/
appeal: 'official' | 'no_official' | null;
/**
* Флаг отправки запроса на оценку.
* Определяет, нужно ли запросить оценку у пользователя.
* Используется для платформ, которые поддерживают данное поведение.
*
* @example
* ```ts
* this.isSendRating = true; // запросить оценку
* ```
*/
isSendRating: boolean;
/**
* Название предыдущего интента/команды, полученное из `userData.oldIntentName`.
* Используется для отслеживания контекста диалога.
*
* @remarks
* **КАК ЭТО РАБОТАЕТ:**
* 1. В конце обработки каждого запроса, `this.thisIntentName` сохраняется в `userData.oldIntentName`
* 2. При следующем запросе это значение копируется в `this.oldIntentName`
* 3. Используется для определения, с какого шага продолжить диалог
*
* **ТИПИЧНОЕ ИСПОЛЬЗОВАНИЕ:**
* - Возврат к предыдущему шагу
* - Многошаговые формы ("вернуться назад")
* - Диалоги с контекстом
* - Использование в шагах `bot.addStep()`
*
* @example
* ```ts
* // Пример: Многошаговая регистрация
* class RegistrationBot extends BotController {
* public action(intentName: string | null): void {
* // Определяем на каком шаге находимся
* const previousStep = this.oldIntentName;
*
* if (previousStep === 'enter_name') {
* // Пользователь только что ввел имя, спрашиваем email
* this.userData.name = this.userCommand;
* this.text = 'Отлично! Теперь введите ваш email:';
* this.thisIntentName = 'enter_email'; // Сохранится для следующего шага
* } else if (previousStep === 'enter_email') {
* // Пользователь ввел email, завершаем регистрацию
* this.userData.email = this.userCommand;
* this.text = 'Регистрация завершена!';
* }
* }
* }
*
* // Пример: Кнопка "Назад"
* if (intentName === 'back') {
* // Возвращаемся к предыдущему шагу
* switch(this.oldIntentName) {
* case 'product_list':
* this.text = 'Выберите категорию:';
* break;
* case 'category_list':
* this.text = 'Добро пожаловать!';
* break;
* }
* }
* ```
*/
oldIntentName: string | null;
/**
* Контекст приложения.
*/
appContext: AppContext;
/**
* Платформа от которой был получен запрос.
*/
appType: TAppType | null;
/**
* Дополнительные опции платформы.
* ⚠️ Внутреннее свойство. Заполняется адаптером платформы.
* Не предназначено для прямого использования в пользовательском коде.
*/
platformOptions: IPlatformOptions;
/**
* Создает новый экземпляр контроллера.
* Инициализирует все необходимые компоненты.
*/
constructor(appContext?: AppContext);
/**
* Компонент для отображения различных кнопок пользователю.
* Позволяет создавать интерактивные элементы управления в приложении.
*
* @remarks
* ## 🎯 ТИПИЧНОЕ ИСПОЛЬЗОВАНИЕ:
* - Навигация по меню
* - Быстрые ответы (Да/Нет)
* - Выбор из вариантов
* - Быстрое действие/команда
*
*
* @see Buttons
* @example
* ```ts
* this.buttons
* .addBtn('Помощь')
* .addBtn('Выход');
* ```
*/
get buttons(): Buttons;
/**
* Флаг возвращающий информацию о том, были ли инициализированы кнопки или нет
* @returns
*/
isButtonsInit(): boolean;
/**
* Компонент для отображения карточек пользователю.
* Позволяет создавать визуальные элементы с изображениями и текстом.
* Также при указании нескольких изображений, они автоматически преобразуются в карточку.
*
* @remarks
* ## 🎯 КОГДА ИСПОЛЬЗОВАТЬ:
* - Каталог товаров/услуг
* - Галерея изображений
* - Карточки статей/новостей
* - Навигация
*
* @see Card
* @example
```ts
* // КАТАЛОГ ТОВАРОВ (интернет-магазин):
* this.text = 'Популярные товары:';
* this.card
* .addImage(
* 'http://localhost/iphone.jpg',
* 'iPhone 15 Pro',
* '99 990 ₽\nЭкран 6.1", процессор A17 Pro'
* )
* .addButton('Купить')
*
* .addImage(
* 'http://localhost/macbook.jpg',
* 'MacBook Air M2',
* '124 990 ₽\n13.6", 8ГБ RAM, 256ГБ SSD'
* )
* .addButton('Купить');
*
* // ГАЛЕРЕЯ ФОТОГРАФИЙ:
* this.text = 'Наши работы:';
* this.card
* .addImage('photo1.jpg', 'Свадьба', 'Иван и Мария')
* .addImage('photo2.jpg', 'Выпускной', 'Школа №123')
* .addImage('photo3.jpg', 'Корпоратив', 'Компания "Рога и копыта"');
*
* // КАРТОЧКИ НОВОСТЕЙ:
* this.card
* .addImage(
* 'news1.jpg',
* 'Новое обновление',
* 'Добавлена оплата картой и доставка',
* {
* title: 'Перейти',
* url: 'http://localhost/news/1'
* }
* )
* ```
*/
get card(): Card;
/**
* Флаг возвращающий информацию о том, были ли инициализированы карточки или нет
* @returns
*/
isCardInit(): boolean;
/**
* Компонент для работы со звуками.
* Позволяет добавлять звуковые эффекты и музыку. Используется вместе с tts.
*
* @see Sound
*/
get sound(): Sound;
/**
* Флаг возвращающий информацию о том, были ли инициализированы звуки или нет
* @returns
*/
isSoundInit(): boolean;
/**
* Обработанный NLU (Natural Language Understanding).
* Содержит результаты обработки естественного языка, как правило, данные заполняются самой платформой.
*
* @see Nlu
*/
get nlu(): Nlu;
/**
* Флаг возвращающий информацию о том, были ли инициализирован nlu или нет
* @returns
*/
isNluInit(): boolean;
/**
* Устанавливает контекст приложения.
* @param appContext
*/
setAppContext(appContext: AppContext): this;
/**
* Очищает все временные данные необходимые для отправки ответа.
*/
clearStoreData(): void;
/**
* Возвращает список всех зарегистрированных интентов.
*
* @returns {IAppIntent[]} Массив интентов
*/
protected _intents(): IAppIntent[];
/**
* Находит нужный интент по тексту запроса.
*
* @param {string | null} text - Текст запроса
* @returns {string | null} Название интента или null
*/
protected _getIntent(text: string | null): string | null;
/**
* Извлекает нужную команду из запроса.
*
* @returns {string | null} найденная команда или null если не удалось найти команду
*/
protected _getCommand(): void | null | Promise<void>;
/**
* Основной метод, в котором вы реализуете логику вашего голосового навыка или бота.
*
* Этот метод вызывается фреймворком автоматически после того, как запрос пользователя
* был распознан как команда, интент или шаг диалога.
* В параметр `intentName` передаётся имя команды.
* Флаги `isCommand` и `isStep` позволяют различить источник вызова.
* Используется для более глубокой логики приложения, например можно использовать в качестве логирования, если все обработчики реализованы через команды.
* Либо использовать в качестве обработки команд, что не рекомендуется, так как из-за подобного подхода, размер метода может быть большим.
*
* Метод необходимо обязательно реализовать в дочерних классах.
*
* @param {string | null} intentName - Название интента или команды
* @param {boolean} [isCommand=false] - Флаг, указывающий что это команда
* @param {boolean} [isStep=false] - Флаг, указывающий что это шаг
*
* @example
* ```ts
* // Пример с обработкой интентов
* class MyController extends BotController {
* public action(intentName: string | null): void {
* if (intentName === 'greeting') {
* this.text = 'Привет!';
* } else if (intentName === 'help') {
* this.text = 'Помощь:';
* this.buttons.addBtn('Назад');
* }
* }
* }
*
* // Пример с логированием
* class MyController extends BotController {
* public action(intentName: string | null, isCommand?: boolean, isStep?: boolean): void {
* console.log(`Прошли по ${isCommand ? 'команде' : isStep ? 'шагу' : 'интенту'} с именем: ${intentName}`);
* }
* }
* ```
*/
abstract action(intentName: string | null, isCommand?: boolean, isStep?: boolean): void;
/**
* Запуск обработки пользовательских команд с учетом метрик.
* @param commandName
* @param isCommand
* @param isStep
*/
protected _actionMetric(commandName: string | null, isCommand?: boolean, isStep?: boolean): void;
/**
* Основной метод обработки запроса, вызываемый автоматически фреймворком.
*
* @remarks
* **КРАТКИЙ ОБЗОР РАБОТЫ:**
* 1. Пользователь отправляет сообщение → платформа → Bot.run()
* 2. `run()` определяет тип запроса (команда/интент/шаг)
* 3. Вызывается ваш метод `action()` с результатом
* 4. Вы заполняете поля ответа (`text`, `buttons`, `card`)
* 5. Bot отправляет ответ пользователю
*
* **ЧТО НЕ НУЖНО ДЕЛАТЬ:**
* - ❌ Не вызывайте `run()` вручную в своем коде
* - ❌ Не переопределяйте этот метод
* - ✅ Переопределяйте только метод `action()` для своей логики
*
* **ПОСЛЕДОВАТЕЛЬНОСТЬ ОБРАБОТКИ ВНУТРИ run():**
* ```
* run()
* ├── Шаг 1: Проверяет есть ли активный шаг
* │ → Если есть → вызывает action(stepName, false, true)
* ├── Шаг 2: Ищет команду
* │ → Если нашел → вызывает action(commandName, true, false)
* ├── Шаг 3: Ищет интент
* │ → Если нашел → вызывает action(intentName, false, false)
* └── Шаг 4: Если ничего не нашел → Fallback команда
* ```
*
* @example
* ```ts
* // ВАШ КОД (контроллер):
* class MyController extends BotController {
* public action(intentName: string | null): void {
* // Ваша логика здесь
* this.text = "Ответ пользователю";
* }
* }
*
* // КОД ФРЕЙМВОРКА (не ваш):
* // Когда приходит запрос от пользователя:
* const controller = new MyController();
* {...}; // Наполняет контроллер данными. Как правило, этим занимается адаптер платформы
* await controller.run(); // Автоматически вызывает ваш action()
* const response = ...; // Адаптер формирует ответ в зависимости от состояния контроллера
* ```
*
* @returns {void | Promise<void>} Может быть асинхронным
*/
run(): void | Promise<void>;
}