UNPKG

umbot

Version:

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

854 lines (853 loc) 37.7 kB
/** * Модуль контроллера - основной компонент для обработки бизнес-логики вашего приложения */ 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>; }