umbot
Version:
Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф
298 lines (297 loc) • 13.7 kB
JavaScript
;
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;