umbot
Version:
Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф
1,038 lines (1,037 loc) • 44.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BotController = void 0;
/**
* Модуль контроллера - основной компонент для обработки бизнес-логики вашего приложения
*/
const components_1 = require("../components");
const utils_1 = require("../utils");
const core_1 = require("../core");
const isPromise_1 = require("../utils/isPromise");
/*
* magick
* Если напрямую использовать переменные из другого модуля(например FALLBACK_COMMAND), то производительность может проседать.
* За счет данного хака мы решаем эту проблемы добавляя локальную глобальную переменную, благодаря чему v8 не нужно делать доп расчеты
*/
const DEFAULT_FALLBACK_COMMAND = core_1.FALLBACK_COMMAND;
const DEFAULT_HELP_INTENT_NAME = core_1.HELP_INTENT_NAME;
const DEFAULT_WELCOME_INTENT_NAME = core_1.WELCOME_INTENT_NAME;
/**
* Контроллер приложения – главный класс для реализации единой бизнес-логики вашего приложения. Именно в этом классе обрабатывается вся логика вашего приложения, которая потом передается в саму платформу, будь то голосовой навык для Алисы, либо чат-бот для 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 – основной класс приложения, управляющий адаптерами и контроллерами.
*/
class BotController {
#buttons;
#card;
#nlu;
#sound;
/**
* Текст, который будет отображен пользователю.
* Основной способ коммуникации с пользователем, так как именно этот текст пользователь увидит в интерфейсе.
*
* @example
* ```ts
* this.text = 'Привет! Чем могу помочь?';
* ```
*/
text = '';
/**
* Текст, который пользователь может услышать.
* Для голосовых платформ, озвучка будет произведена силами самой платформы, для не голосовых платформ, поведение зависит непосредственно от реализации адаптера.
* Так для некоторых стандартных адаптеров, в случае заполнения поля и указания токена yandex SpeechKit, будет отправлен запрос на преобразование текста в аудиофайл, после чего аудиофайл будет отправлен пользователю.
*
* @example
* ```ts
* this.tts = 'Привет! Я голосовой ассистент.';
* ```
*/
tts = null;
/**
* Уникальный идентификатор пользователя.
*
* @example
* ```ts
* this.userId = 'user_123'; // Telegram (string)
* this.userId = 123456789; // VK (number)
* this.userId = null; // не удалось получить информацию
* ```
*/
userId = null;
/**
* Пользовательский токен авторизации.
* Используется для авторизованных запросов (например, в Алисе).
*
* @example
* ```ts
* this.userToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
* ```
*/
userToken = null;
/**
* Дополнительная информация о пользователе.
*
* @example
* ```ts
* this.userMeta = {
* timezone: 'Europe/Moscow',
* locale: 'ru-RU'
* };
* ```
*/
userMeta = null;
/**
* ID сообщения.
* Используется для определения начала нового диалога.
*
* @example
* ```ts
* this.messageId = 12345;
* ```
*/
messageId = null;
/**
* Запрос пользователя в нижнем регистре.
*
* @example
* ```ts
* this.userCommand = 'привет мир';
* ```
*/
userCommand = null;
/**
* Оригинальный запрос пользователя.
* Текст запроса без изменений, включая регистр и знаки препинания.
*
* @example
* ```ts
* this.originalUserCommand = 'Привет, мир!';
* ```
*/
originalUserCommand = null;
/**
* Дополнительные параметры запроса.
* Может содержать любые дополнительные данные полученные от платформы.
*
* @example
* ```ts
* this.payload = {
* source: 'mobile',
* version: '1.0'
* };
* ```
*/
payload = null;
/**
* Пользовательские данные, который были сохранены.
*
* @example
* ```ts
* this.userData = {
* name: 'John',
* preferences: {
* language: 'ru'
* }
* };
* ```
*/
userData = {};
/**
* Флаг необходимости авторизации.
* Определяет, требуется ли авторизация пользователя или нет.
*
* @example
* ```ts
* this.isAuth = true; // требуется авторизация
* ```
*/
isAuth = false;
/**
* Пользовательские событий.
* Содержит информацию об авторизации или оценке.
*
* @see IUserEvent
* @example
* ```ts
* this.userEvents = {
* auth: { status: true },
* rating: { status: true, value: 5 }
* };
* ```
*/
userEvents = 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 = null;
/**
* Определяет, с колонки пользователь запустил приложение или с устройства с экраном.
*
* @example
* ```ts
* this.isScreen = true; // экран доступен
* ```
*/
isScreen = false;
/**
* Флаг, определяющий необходимость завершения диалога. Актуально когда необходимо принудительно завершить диалог с пользователем.
* Поддержка работы флага зависит от платформы.
*
* @example
* ```ts
* this.isEnd = true; // завершить диалог
* ```
*/
isEnd = false;
/**
* Флаг, указывающий, что ответ уже отправлен через API и не требуется автоматическая отправка. Как правило, данный флаг стоит использовать для платформ, которые не ждут ответ в виде возвращаемого содержимого, а ожидают что к самой платформе будет отправлен запрос. Например, чат-бот для Telegram, для отображения результата пользователю, отправляется запрос к платформе с нужным содержимым.
*
* @remarks
* Если указано true, значит все необходимые запросы уже отправлены в логике приложения, и дополнительно пользователю ничего отправлять не нужно.
*
* @example
* ```ts
* this.skipAutoReply = true; // запросы уже отправлены
* ```
*/
skipAutoReply = false;
/**
* Полученный запрос от платформы.
* Содержит оригинальный объект запроса.
*
* @example
* ```ts
* this.requestObject = {
* command: 'start',
* payload: { source: 'mobile' }
* };
* ```
*/
requestObject = null;
/**
* Название текущего интента.
* Определяет следующий шаг диалога.
*
* @example
* ```ts
* this.thisIntentName = 'help';
* ```
*/
thisIntentName = null;
/**
* Эмоция для голосового ответа.
* Используется для платформ, которые поддерживают данное поведение.
*
* @example
* ```ts
* this.emotion = 'good';
* ```
*/
emotion = null;
/**
* Стиль обращения к пользователю.
* Определяет формальность общения, используется для платформ, которые поддерживают данное поведение.
*
* @remarks
* Возможные значения:
* - 'official': официальное обращение
* - 'no_official': неофициальное обращение
* - null: стиль не определен
*
* @example
* ```ts
* this.appeal = 'official'; // официальное обращение
* ```
*/
appeal = null;
/**
* Флаг отправки запроса на оценку.
* Определяет, нужно ли запросить оценку у пользователя.
* Используется для платформ, которые поддерживают данное поведение.
*
* @example
* ```ts
* this.isSendRating = true; // запросить оценку
* ```
*/
isSendRating = false;
/**
* Название предыдущего интента/команды, полученное из `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 = null;
/**
* Контекст приложения.
*/
appContext;
/**
* Платформа от которой был получен запрос.
*/
appType = null;
/**
* Дополнительные опции платформы.
* ⚠️ Внутреннее свойство. Заполняется адаптером платформы.
* Не предназначено для прямого использования в пользовательском коде.
*/
platformOptions = {};
#getCustomRegExp;
/**
* Создает новый экземпляр контроллера.
* Инициализирует все необходимые компоненты.
*/
constructor(appContext) {
// Для корректности выставляем контекст по умолчанию.
this.appContext = appContext || new core_1.AppContext();
this.#getCustomRegExp = this.appContext.command.getCustomRegExp();
}
/**
* Компонент для отображения различных кнопок пользователю.
* Позволяет создавать интерактивные элементы управления в приложении.
*
* @remarks
* ## 🎯 ТИПИЧНОЕ ИСПОЛЬЗОВАНИЕ:
* - Навигация по меню
* - Быстрые ответы (Да/Нет)
* - Выбор из вариантов
* - Быстрое действие/команда
*
*
* @see Buttons
* @example
* ```ts
* this.buttons
* .addBtn('Помощь')
* .addBtn('Выход');
* ```
*/
get buttons() {
if (!this.#buttons) {
this.#buttons = new components_1.Buttons(this.appContext);
}
return this.#buttons;
}
/**
* Флаг возвращающий информацию о том, были ли инициализированы кнопки или нет
* @returns
*/
isButtonsInit() {
return !!this.#buttons;
}
/**
* Компонент для отображения карточек пользователю.
* Позволяет создавать визуальные элементы с изображениями и текстом.
* Также при указании нескольких изображений, они автоматически преобразуются в карточку.
*
* @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() {
if (!this.#card) {
this.#card = new components_1.Card(this.appContext);
}
return this.#card;
}
/**
* Флаг возвращающий информацию о том, были ли инициализированы карточки или нет
* @returns
*/
isCardInit() {
return !!this.#card;
}
/**
* Компонент для работы со звуками.
* Позволяет добавлять звуковые эффекты и музыку. Используется вместе с tts.
*
* @see Sound
*/
get sound() {
if (!this.#sound) {
this.#sound = new components_1.Sound();
}
return this.#sound;
}
/**
* Флаг возвращающий информацию о том, были ли инициализированы звуки или нет
* @returns
*/
isSoundInit() {
return !!this.#sound;
}
/**
* Обработанный NLU (Natural Language Understanding).
* Содержит результаты обработки естественного языка, как правило, данные заполняются самой платформой.
*
* @see Nlu
*/
get nlu() {
if (!this.#nlu) {
this.#nlu = new components_1.Nlu();
}
return this.#nlu;
}
/**
* Флаг возвращающий информацию о том, были ли инициализирован nlu или нет
* @returns
*/
isNluInit() {
return !!this.#nlu;
}
/**
* Устанавливает контекст приложения.
* @param appContext
*/
setAppContext(appContext) {
if (appContext) {
this.appContext = appContext;
this.#getCustomRegExp = this.appContext.command.getCustomRegExp();
if (this.#buttons) {
this.#buttons.setAppContext(appContext);
}
if (this.#card) {
this.#card.setAppContext(appContext);
}
}
return this;
}
/**
* Очищает все временные данные необходимые для отправки ответа.
*/
clearStoreData() {
if (this.#buttons) {
this.buttons.clear();
}
if (this.#card) {
this.card.clear();
}
if (this.isNluInit()) {
this.nlu.setNlu({});
}
this.text = '';
this.tts = null;
this.userId = null;
this.userToken = null;
this.userMeta = null;
this.messageId = null;
this.userCommand = null;
this.originalUserCommand = null;
this.payload = null;
this.userData = {};
this.isAuth = false;
this.userEvents = null;
this.state = null;
this.isScreen = false;
this.isEnd = false;
this.skipAutoReply = false;
this.requestObject = null;
this.oldIntentName = null;
this.thisIntentName = null;
this.emotion = null;
this.appeal = null;
this.isSendRating = false;
}
/**
* Возвращает список всех зарегистрированных интентов.
*
* @returns {IAppIntent[]} Массив интентов
*/
_intents() {
return this.appContext?.platformParams.intents || [];
}
/**
* Находит нужный интент по тексту запроса.
*
* @param {string | null} text - Текст запроса
* @returns {string | null} Название интента или null
*/
_getIntent(text) {
if (!text) {
return null;
}
const start = this.#getStartMetric();
const intents = this._intents();
for (let i = 0; i < intents.length; i++) {
const intent = intents[i];
if (utils_1.Text.isSayText(intent.slots || [], text, intent.is_pattern, false, this.#getCustomRegExp)) {
if (this.appContext.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_INTENT, performance.now() - start, {
intent,
status: true,
});
}
return intent.name;
}
}
if (this.appContext.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_INTENT, performance.now() - start, {
status: false,
});
}
return null;
}
/**
* Запуск кастомной обработки команд.
* @param startTimer
* @private
*/
#sendCustomCommandResolver(startTimer) {
if (this.appContext.command.customCommandResolver) {
const res = this.appContext.command.customCommandResolver(this.userCommand, this.appContext.commands);
const cb = (result) => {
const command = result ? this.appContext.commands.get(res) : null;
if (result && command) {
const res = this.#commandExecute(result, command);
if (res) {
return res
.then(() => {
if (this.appContext?.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_COMMAND, performance.now() - startTimer, {
result,
status: true,
});
}
})
.catch((error) => {
this.appContext.logError(`BotController: Произошла ошибка во время обработки команды "${result}". Текст ошибки: "${error}"`, {
error,
});
});
}
if (this.appContext?.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_COMMAND, performance.now() - startTimer, {
result,
status: true,
});
}
}
else if (this.appContext?.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_COMMAND, performance.now() - startTimer, {
status: false,
});
}
};
if ((0, isPromise_1.isPromise)(res)) {
return res.then(cb);
}
return cb(res);
}
return null;
}
#commandCb(commandName, command, start) {
if (!command) {
return;
}
const ex = this.#commandExecute(commandName, command);
if (ex) {
return ex
.then(() => {
if (this.appContext?.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_COMMAND, performance.now() - start, {
commandName,
status: true,
});
}
this._actionMetric(commandName, true);
})
.catch((err) => {
this.appContext.logError(`BotController: Произошла ошибка во время обработки команды "${commandName}". Текст ошибки: "${err}"`, {
err,
});
});
}
if (this.appContext?.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_COMMAND, performance.now() - start, {
commandName,
status: true,
});
}
this._actionMetric(commandName, true);
}
#getStartMetric() {
return this.appContext.usedMetric ? performance.now() : 0;
}
#getExactCommand(start) {
const tCommandName = this.appContext.command.getExactMatchCommand(this.userCommand);
if (tCommandName) {
const command = this.appContext.commands.get(tCommandName);
if (command) {
return this.#commandCb(tCommandName, command, start);
}
}
return null;
}
/**
* Извлекает нужную команду из запроса.
*
* @returns {string | null} найденная команда или null если не удалось найти команду
*/
_getCommand() {
if (!this.userCommand || !this.appContext.commands) {
return null;
}
const start = this.#getStartMetric();
if (this.appContext.command.customCommandResolver) {
return this.#sendCustomCommandResolver(start);
}
const exactCommand = this.#getExactCommand(start);
if (exactCommand !== null) {
return exactCommand;
}
let contCount = 0;
const useDirectRegExp = this.appContext.commands.size < 500;
for (const [commandName, command] of this.appContext.commands) {
if (commandName === DEFAULT_FALLBACK_COMMAND || !command || contCount !== 0) {
if (contCount) {
contCount--;
}
continue;
}
if (!command.slots || command.slots.length === 0) {
continue;
}
if (command.isPattern) {
const groups = this.appContext.regexpGroup.get(commandName);
if (groups) {
contCount = groups.commands.length - 1;
const groupRes = this.#searchCommandsInGroup(groups, this.userCommand, start);
if (groupRes !== null) {
return groupRes;
}
continue;
}
}
if (utils_1.Text.isSayText(command.regExp || command.slots, this.userCommand, command.isPattern, command.isRegExpString || useDirectRegExp, this.#getCustomRegExp)) {
return this.#commandCb(commandName, command, start);
}
}
if (this.appContext.usedMetric) {
this.appContext.logMetric(core_1.EMetric.GET_COMMAND, performance.now() - start, {
status: false,
});
}
return null;
}
#searchCommandsInGroup(groups, userCommand, startTimer) {
if (!groups.regExp) {
return null;
}
const reg = (0, utils_1.isRegex)(groups.regExp)
? groups.regExp
: (0, utils_1.getRegExp)(groups.regExp, 'ium', this.#getCustomRegExp);
const match = reg.exec(userCommand);
if (match) {
// Находим первую совпавшую подгруппу (index в массиве parts)
for (const key in match.groups) {
if (match.groups[key] !== undefined) {
const commandName = groups.commands[+key.slice(1)];
if (commandName && this.appContext.commands.has(commandName)) {
return this.#commandCb(commandName, this.appContext.commands.get(commandName), startTimer);
}
}
}
}
return null; //continue;
}
/**
* Выполнение команды.
* @param commandName
* @param command
*/
#commandExecute(commandName, command) {
const errorCb = (e) => {
this.appContext.logError(`BotController: Произошла ошибка во время обработки команды "${commandName}". Текст ошибки: "${e}"`, {
e,
});
this.text = 'Не удалось выполнить команду. Попробуйте ещё раз.';
};
try {
if (command) {
const res = command?.cb?.(this.userCommand, this);
if ((0, isPromise_1.isPromise)(res)) {
return res
.then((result) => {
if (result) {
this.text = result;
}
})
.catch(errorCb);
}
if (res) {
this.text = res;
}
}
}
catch (e) {
errorCb(e);
}
}
/**
* Запуск обработки пользовательских команд с учетом метрик.
* @param commandName
* @param isCommand
* @param isStep
*/
_actionMetric(commandName, isCommand = false, isStep = false) {
const start = this.appContext?.usedMetric ? performance.now() : 0;
this.action(commandName, isCommand, isStep);
if (this.appContext?.usedMetric) {
this.appContext.logMetric(core_1.EMetric.ACTION, performance.now() - start, {
commandName,
platform: this.appType,
isCommand,
});
}
}
/**
* Обработка зарегистрированных шагов.
* @private
*/
#stepResolver() {
if (this.appContext.steps.size) {
const intents = this.nlu.getIntents();
let step = this.oldIntentName ? this.appContext.steps.get(this.oldIntentName) : null;
if (!step && intents) {
for (const intent in intents) {
if (this.appContext.steps.has(intent)) {
step = this.appContext.steps.get(intent);
}
}
}
if (step) {
const res = step.cb(this);
if (res) {
return res
.then(() => {
this._actionMetric(step.stepName, false, true);
})
.catch((error) => {
this.appContext.logError(`BotController: Произошла ошибка во время обработки шага "${step.stepName}". Текст ошибки: "${error}"`, {
error,
});
});
}
else if (res === false) {
// Если передали false, значит хотят чтобы шаг не выполнялся, и дальше пошла логика с обработкой команд.
// Как правило, нужно в случаях, когда был записан какой-то шал, и диалог открыли заново. В таком случае сам шаг отрабатывать не нужно.
return null;
}
this._actionMetric(step.stepName, false, true);
return;
}
}
return null;
}
/**
* Основной метод обработки запроса, вызываемый автоматически фреймворком.
*
* @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() {
const stepResult = this.#stepResolver();
if (stepResult !== null) {
return stepResult;
}
const commandResult = this._getCommand();
if (commandResult === null) {
let intent = this._getIntent(this.userCommand);
const fallbackCommand = this.appContext?.commands.get(DEFAULT_FALLBACK_COMMAND);
if (!intent && fallbackCommand) {
const res = this.#commandExecute(DEFAULT_FALLBACK_COMMAND, fallbackCommand);
this._actionMetric(DEFAULT_FALLBACK_COMMAND, true);
return res;
}
else {
// if (
// intent === null &&
// this.originalUserCommand &&
// this.userCommand !== this.originalUserCommand
// ) {
// // Защита на случай, если сам запроса не был найден, но на самом деле должен был отработать.
// // По хорошему стоит пересмотреть эту механику, и возможно удалить ее.
// intent = this._getIntent(this.originalUserCommand.toLowerCase());
// }
if (intent === null && this.messageId === 0) {
intent = DEFAULT_WELCOME_INTENT_NAME;
}
let command;
/*
* Для стандартных действий параметры заполняются автоматически.
* Есть возможность переопределить их в action() по названию действия
*/
switch (intent) {
case DEFAULT_WELCOME_INTENT_NAME:
command = this.appContext.commands.get(DEFAULT_WELCOME_INTENT_NAME);
if (command) {
return this.#commandCb(DEFAULT_WELCOME_INTENT_NAME, command, this.#getStartMetric());
}
else {
this.text = utils_1.Text.getText(this.appContext.platformParams.welcome_text);
}
break;
case DEFAULT_HELP_INTENT_NAME:
command = this.appContext.commands.get(DEFAULT_HELP_INTENT_NAME);
if (command) {
return this.#commandCb(DEFAULT_HELP_INTENT_NAME, command, this.#getStartMetric());
}
else {
this.text = utils_1.Text.getText(this.appContext.platformParams.help_text);
}
break;
}
this._actionMetric(intent);
}
}
else {
return commandResult;
}
}
}
exports.BotController = BotController;