UNPKG

umbot

Version:

Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф

1,038 lines (1,037 loc) 44.1 kB
"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;