UNPKG

umbot

Version:

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

298 lines (297 loc) 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AlisaAdapter = void 0; const index_1 = require("../../../index"); const Base_1 = require("../Base/Base"); const Button_1 = require("./Button"); const Card_1 = require("./Card"); const Sound_1 = require("./Sound"); const constants_1 = require("./constants"); const utils_1 = require("../Base/utils"); /** * Адаптер, обеспечивающий полную поддержку платформы Яндекс.Диалоги (Алиса). Позволяет разрабатывать навыки для Алисы на TypeScript с использованием всего функционала платформы: от обработки голосовых запросов до работы с карточками и кнопками. * * Подключение адаптера не требует изменения существующей бизнес-логики: после интеграции все команды и обработчики, написанные для umbot, автоматически становятся доступны для Алисы. * Единый интерфейс позволяет одновременно использовать одну бизнес-логику для нескольких платформ (Telegram, VK, Алиса и др.) без дублирования кода. * * Этот адаптер автоматически обрабатывает входящие webhook`и от Алисы, * преобразует их в унифицированный формат фреймворка и формирует ответ, * совместимый с требованиями платформы. Подключается одной строкой и * не мешает работе других адаптеров (например, для Telegram или VK). * * Поддерживает: * - голосовые и текстовые запросы; * - сохранение состояния (user/application/session); * - карточки, кнопки, TTS-эффекты; * - health-check (`ping` → `pong`); * * Подключается как любой другой адаптер: `bot.use(new AlisaAdapter(token))`. * Несколько адаптеров могут работать одновременно — система сама выберет подходящий * на основе заголовков и структуры входящего запроса. * @example * ```ts * // Простейший навык для Алисы, который отвечает на приветствие * import { Bot } from 'umbot'; * import { AlisaAdapter } from 'umbot/plugins'; * * const bot = new Bot() * .use(new AlisaAdapter('YOUR_OAUTH_TOKEN')) // Подключаем адаптер для Алисы * .addCommand('start', ['привет'], (_text, ctx) => { * ctx.text = 'Привет! Я твой первый навык для Алисы'; * }); * * bot.start('localhost', 3000); * ``` * * @see {@link https://yandex.ru/dev/dialogs/alice/doc/protocol-docpage/ | Протокол Яндекс.Диалогов} * @see Bot * @see BotController * @see BasePlatform */ class AlisaAdapter extends Base_1.BasePlatform { platformName = constants_1.T_ALISA; init(appContext) { super.init(appContext); if (this._token) { appContext.appConfig.tokens[this.platformName].token = this._token; } } isPlatformOnQuery(query, _headers) { if (!query) { this.appContext?.logWarn(`AlisaAdapter.isPlatformOnQuery(): ${Base_1.EMPTY_QUERY_ERROR}`); return false; } if (query.request && query.version && query.session) { if (query.meta?.client_id?.includes('yandex.searchplugin')) { return true; } else if (query.session.application?.application_id) { return (query.session.application?.application_id !== query.session.application?.application_id.toLowerCase()); } else { this.appContext?.logWarn('AlisaAdapter.isPlatformOnQuery(): Не удалось однозначно определить платформу (Алиса/Маруся). Используется fallback на Алису.'); return true; } } return false; } /** * Инициализирует команду пользователя от Алисы. * Обрабатывает различные типы запросов и сохраняет команду в контроллере * @param request Объект запроса от пользователя * @param controller Контроллер приложения */ #initUserCommand(request, controller) { (0, utils_1.initUserCommand)(request, controller); } /** * Устанавливает идентификатор пользователя. * Определяет ID пользователя из сессии или приложения */ #setUserId(controller, session) { if (this.appContext && session) { controller.platformOptions.isState = false; if (this.appContext.platformParams.isAuthUser && session.user?.user_id !== undefined) { controller.userId = session.user.user_id; controller.platformOptions.isState = true; controller.userToken = session.user.access_token || null; } else { if (session.application?.application_id === undefined) { controller.userId = session.user_id; } else { controller.userId = session.application.application_id; } } } } /** * Устанавливает состояние приложения. * Определяет тип хранилища и сохраняет состояние в контроллере * @param controller Объект состояния из запроса * @param state Объект состояния из запроса */ #setState(controller, state) { if (state.user) { controller.state = state.user; controller.platformOptions.stateName = 'user_state_update'; } else if (state.application) { controller.state = state.application; controller.platformOptions.stateName = 'application_state'; } else if (state.session) { controller.state = state.session; controller.platformOptions.stateName = 'session_state'; } } setQueryData(query, controller) { if (this.appContext) { if (query) { if (query.session === undefined && query.request === undefined) { if (query.account_linking_complete_event) { controller.userEvents = { auth: { status: true, }, }; return true; } controller.platformOptions.error = 'AlisaAdapter.setQueryData(): Переданы некорректные данные для авторизации!'; return false; } controller.requestObject = query; this.#initUserCommand(query.request, controller); this.#setUserId(controller, query.session); if (query.request.nlu) { controller.nlu.setNlu(query.request.nlu); } controller.userMeta = query.meta || {}; controller.messageId = query.session.message_id; if (query.state !== undefined) { this.#setState(controller, query.state); } controller.platformOptions.appId = query.session.skill_id; controller.isScreen = controller.userMeta.interfaces.screen !== undefined; /* * Раз в какое-то время Яндекс отправляет запрос ping, для проверки корректности работы навыка. * @see (https://yandex.ru/dev/dialogs/alice/doc/health-check-docpage/) Смотри тут */ if (!query.request.command && query.request.original_utterance === 'ping') { controller.text = 'pong'; controller.platformOptions.sendInInit = { version: constants_1.VERSION, response: { text: 'pong', }, }; } return true; } else { controller.platformOptions.error = `AlisaAdapter.setQueryData(): ${Base_1.EMPTY_QUERY_ERROR}`; } } else { console.error(`AlisaAdapter.setQueryData(): ${Base_1.EMPTY_CONTEXT_ERROR}`); } return false; } /** * Формирует ответ для пользователя. * Собирает текст, TTS, карточки и кнопки в единый объект ответа * @returns {Promise<IAlisaResponse>} Объект ответа для Алисы */ async _getResponse(controller) { const response = { text: index_1.Text.resize(controller.text, 1024), tts: index_1.Text.resize(controller.tts, 1024), end_session: controller.isEnd, }; if (controller.isScreen) { if (controller.isCardInit() && controller.card.images.length) { response.card = (await controller.card.getCards(Card_1.cardProcessing, controller)); if (!response.card) { response.card = undefined; } } if (controller.isButtonsInit()) { const fn = (buttons) => { return (0, Button_1.buttonProcessing)(buttons, false, this.appContext); }; response.buttons = controller.buttons.getButtons(fn); } else { response.buttons = []; } } return response; } async getContent(controller, stateData) { const result = { version: constants_1.VERSION, }; if (controller.isAuth && controller.userToken === null) { result.start_account_linking = {}; } else { const resTts = this._initTTS(controller); if (resTts) { await resTts; } result.response = await this._getResponse(controller); } if (controller.platformOptions.stateName && stateData) { result[controller.platformOptions.stateName] = stateData; } this._timeLimitLog(controller); return result; } /** * Использует кастомную обработку TTS через {@link soundProcessing} для вставки * звуковых эффектов в соответствии с требованиями Алисы. */ async soundProcessing(controller) { if (controller.isSoundInit()) { // eslint-disable-next-line require-atomic-updates controller.tts = await controller.sound.getSounds(controller.tts, Sound_1.soundProcessing, controller); } } /** * Возвращает состояние из `request.state` (user/application/session). * Тип состояния определяется настройками навыка в Яндекс.Диалогах. */ getLocalStorage(controller) { return controller.state; } /** * Проверяет, присутствует ли сохранённое состояние в запросе. * * @param controller - контроллер с бизнес-логикой приложения * @returns `true`, если состояние есть (не null) */ isLocalStorage(controller) { return controller.state !== null; } /** * Алиса не поддерживает push-сообщения, поэтому всегда возвращает `false`. */ send() { return false; } getQueryExample(query, userId, count, state) { return { meta: { locale: 'ru-Ru', timezone: 'UTC', client_id: 'yandex.searchplugin_local', interfaces: { payments: null, account_linking: null, }, }, session: { message_id: count, session_id: 'local', skill_id: 'local_test', user_id: userId, new: count === 0, }, request: { command: query.toLowerCase(), original_utterance: query, nlu: {}, type: 'SimpleUtterance', }, state: { session: state, }, version: '1.0', }; } } exports.AlisaAdapter = AlisaAdapter;