@salutejs/client
Version:
Модуль взаимодействия с виртуальным ассистентом
1,087 lines (1,080 loc) • 54.5 kB
JavaScript
import { d as createNanoEvents, b as __awaiter, c as __generator, _ as __assign, a as __rest } from './common-ba25e019.js';
import { b as createTransport, d as convertFieldValuesToString, e as createClient, D as DEFAULT_APP, i as isDefaultApp, B as BASIC_SMART_APP_COMMANDS_TYPES, g as getTime, p as promiseTimeout, f as getAnswerForRequestPermissions, S as STATE_UPDATE_TIMEOUT } from './client-ed11c12c.js';
import { M as MessageNames, d as createProtocol } from './sdk-7c3cb15e.js';
import { i as iosSilentModePatch, c as createVoicePlayer } from './voicePlayer-705b49ef.js';
var isAudioSupported = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext);
/**
* Возвращает новый инстанс AudioContext или ошибку
* @param options AudioContextOptions
* @returns AudioContext
*/
function createAudioContext(options) {
if (window.AudioContext) {
return new AudioContext(options);
}
if (window.webkitAudioContext) {
// eslint-disable-next-line new-cap
return new window.webkitAudioContext();
}
throw new Error('Audio not supported');
}
var _a = createNanoEvents(), on = _a.on, emit = _a.emit;
var audioContext;
/**
* При помощи вызова функции из аргумента, возвращает, готовый к воспроизведению звука, AudioContext.
* Всегда возвращает один и тот же AudioContext
* @param onReady Функция, в аргумент которой будет возвращен AudioContext
*/
var resolveAudioContext = function (onReady) {
if (!audioContext) {
var isSafari_1 = navigator.vendor.search('Apple') >= 0;
var context_1 = createAudioContext();
audioContext = {
context: context_1,
ready: !isSafari_1 && context_1.state === 'running',
on: on,
};
context_1.onstatechange = function () {
if (audioContext.context.state === 'running') {
audioContext.ready = true;
emit('ready');
}
};
/// Контекст может быть не готов для использования сразу после создания
/// Если попробовать что-то воспроизвести в этом контексте - звука не будет
if (!audioContext.ready) {
var handleClick_1 = function () {
document.removeEventListener('click', handleClick_1);
iosSilentModePatch.initialize();
if (isSafari_1) {
/// проигрываем тишину, т.к нужно что-то проиграть,
/// чтобы сафари разрешил воспроизводить звуки в любой момент в этом контексте
var oscillator = audioContext.context.createOscillator();
oscillator.frequency.value = 0;
oscillator.connect(audioContext.context.destination);
oscillator.start(0);
oscillator.stop(0.5);
}
if (audioContext.context.state === 'suspended') {
/// Developers who write games, WebRTC applications, or other websites that use the Web Audio API
/// should call context.resume() after the first user gesture (e.g. a click, or tap)
/// https://sites.google.com/a/chromium.org/dev/audio-video/autoplay
audioContext.context.resume();
}
};
/// чтобы сделать контекст готовым к использованию (воспроизведению звука),
/// необходимо событие от пользователя (только не touch)
document.addEventListener('click', handleClick_1);
}
}
if (audioContext.ready) {
onReady && onReady(audioContext.context, function () { return iosSilentModePatch.destroy(); });
}
else {
var unsubscribe_1 = on('ready', function () {
onReady(audioContext.context, function () { return iosSilentModePatch.destroy(); });
unsubscribe_1();
});
}
};
/**
* Понижает sample rate c inSampleRate до значения outSampleRate и преобразует Float32Array в ArrayBuffer
* @param buffer Аудио
* @param inSampleRate текущий sample rate
* @param outSampleRate требуемый sample rate
* @returns Аудио со значением sample rate = outSampleRate
*/
var downsampleBuffer = function (buffer, inSampleRate, outSampleRate) {
if (outSampleRate > inSampleRate) {
throw new Error('downsampling rate show be smaller than original sample rate');
}
var sampleRateRatio = inSampleRate / outSampleRate;
var newLength = Math.round(buffer.length / sampleRateRatio);
var result = new Int16Array(newLength);
var offsetResult = 0;
var offsetBuffer = 0;
while (offsetResult < result.length) {
var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
var accum = 0;
var count = 0;
for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
accum += buffer[i];
count++;
}
result[offsetResult] = Math.min(1, accum / count) * 0x7fff;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return result.buffer;
};
var TARGET_SAMPLE_RATE = 16000;
var IS_FIREFOX = typeof window !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
var IS_SAFARI = typeof window !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
var context;
var processor;
var analyser = null;
var destination;
/**
* Преобразует stream в чанки (кусочки), и передает их в cb,
* будет это делать, пока не будет вызвана функция остановки
* @param stream Аудио-поток
* @param cb callback, куда будут переданы чанки из потока
* @returns Функция, вызов которой остановит передачу чанков
*/
var createAudioRecorder = function (stream, cb, targetSampleRate, useAnalyser) {
return new Promise(function (resolve) {
var state = 'inactive';
var input;
var stop = function () {
if (state === 'inactive') {
return;
}
state = 'inactive';
stream.getTracks().forEach(function (track) {
track.stop();
});
input.disconnect();
};
var start = function () {
if (state !== 'inactive') {
throw new Error("Can't start not inactive recorder");
}
state = 'recording';
if (!context) {
context = createAudioContext({
// firefox не умеет выравнивать samplerate, будем делать это самостоятельно
sampleRate: IS_FIREFOX ? undefined : targetSampleRate,
});
}
input = context.createMediaStreamSource(stream);
if (!processor) {
processor = context.createScriptProcessor(2048, 1, 1);
}
if (!analyser && useAnalyser) {
analyser = context.createAnalyser();
analyser.fftSize = 1024;
}
var listener = function (e) {
var buffer = e.inputBuffer.getChannelData(0);
var data = downsampleBuffer(buffer, context.sampleRate, targetSampleRate);
var last = state === 'inactive';
// отсылаем только чанки где есть звук voiceData > 0, т.к.
// в safari первые несколько чанков со звуком пустые
if (!IS_SAFARI || new Uint8Array(data).some(function (voiceData) { return voiceData > 0; })) {
var analyserArray = null;
if (analyser) {
analyserArray = new Uint8Array(analyser.frequencyBinCount);
analyser === null || analyser === void 0 ? void 0 : analyser.getByteFrequencyData(analyserArray);
}
cb(data, analyserArray, last);
resolve(stop);
}
if (last) {
processor.removeEventListener('audioprocess', listener);
}
};
processor.addEventListener('audioprocess', listener);
input.connect(processor);
if (analyser) {
input.connect(analyser);
}
if (!destination) {
destination = context.createMediaStreamDestination();
}
processor.connect(destination);
};
start();
});
};
/**
* Запрашивает у браузера доступ к микрофону и резолвит Promise, если разрешение получено.
* После получения разрешения, чанки с голосом будут передаваться в cb - пока не будет вызвана функция из результата.
* @param cb Callback, куда будут передаваться чанки с голосом пользователя
* @returns Promise, который содержит функцию прерывающую слушание
*/
var createNavigatorAudioProvider = function (cb, useAnalyser, options) {
if (options === void 0) { options = {}; }
return navigator.mediaDevices
.getUserMedia({
audio: {
echoCancellation: options === null || options === void 0 ? void 0 : options.echoCancellation,
noiseSuppression: options === null || options === void 0 ? void 0 : options.noiseSuppression,
},
})
.then(function (stream) {
return createAudioRecorder(stream, cb, options.targetSampleRate || TARGET_SAMPLE_RATE, useAnalyser);
})
.catch(function (err) {
if (window.location.protocol === 'http:') {
throw new Error('Audio is supported only on a secure connection');
}
throw err;
});
};
/**
* Возвращает объект, позволяющий получать запись голоса пользователя и управлять ею.
* @param createAudioProvider Источник голоса
* @returns Api для запуска и остановки слушания
*/
var createVoiceListener = function (createAudioProvider) {
if (createAudioProvider === void 0) { createAudioProvider = createNavigatorAudioProvider; }
var _a = createNanoEvents(), emit = _a.emit, on = _a.on;
var stopRecord;
var status = 'stopped';
var cancelableToken = { current: false };
var stop = function () {
cancelableToken.current = true;
cancelableToken = { current: false };
status = 'stopped';
stopRecord === null || stopRecord === void 0 ? void 0 : stopRecord();
emit('status', 'stopped');
};
var listen = function (handleVoice) {
cancelableToken = { current: false };
var capturedToken = cancelableToken;
status = 'started';
emit('status', 'started');
return createAudioProvider(function (data, analyser, last) { return handleVoice(new Uint8Array(data), analyser, last); })
.then(function (recStop) {
stopRecord = recStop;
})
.then(function () {
if (capturedToken.current === true || status === 'stopped') {
stopRecord();
}
else {
status = 'listen';
emit('status', 'listen');
}
})
.catch(function (err) {
status = 'stopped';
emit('status', 'stopped');
throw err;
});
};
return {
listen: listen,
stop: stop,
on: on,
get status() {
return status;
},
};
};
/** Фильтр тишины */
var filterEmptyChunks = function (chunksOriginal) {
return chunksOriginal.reduce(function (acc, chunkOriginal) {
var chunk = chunkOriginal.filter(function (int) { return int; });
if (chunk.length) {
acc.push(chunk);
}
return acc;
}, []);
};
var createVoice = function (client, settings, emit,
/// пока onReady не вызван, треки не воспроизводятся
/// когда случится onReady, очередь треков начнет проигрываться
onReady) {
var useAnalyser = false;
var voicePlayer;
var listener = createVoiceListener(function (cb) { return createNavigatorAudioProvider(cb, useAnalyser); });
var subscriptions = [];
var appInfoDict = {};
var mesIdQueue = [];
/** в процессе инициализации слушания */
var isRecognizeInitializing = false;
/** проигрывается/не проигрывается озвучка */
var isPlaying = false;
/** id сообщения, после проигрывания которого, нужно активировать слушание */
var autolistenMessageId = null;
/** id сообщения со звуком, отправляемое в данный момент */
var currentVoiceMessageId = null;
/** стримит поток чанков. Если метода нет, то стриминг не идёт */
var streaming = null;
/** Уничтожает аудио-контекст */
var destroyAudioContext = null;
/** Останавливает слушание голоса, отправляет cancel. Возвращает true - если слушание было активно */
var stopVoice = function (sendCancel) {
if (sendCancel === void 0) { sendCancel = true; }
autolistenMessageId = null;
streaming = null;
if (sendCancel && currentVoiceMessageId) {
client.sendCancel(currentVoiceMessageId);
}
currentVoiceMessageId = null;
if (listener.status === 'listen') {
listener.stop();
return true;
}
return false;
};
/** Останавливает слушание и воспроизведение */
var stop = function () {
// здесь важен порядок остановки голоса
stopVoice();
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.stop();
};
var recognize = function (_a) {
var _b = _a === void 0 ? {} : _a, begin = _b.begin, messageName = _b.messageName, _c = _b.isAutoListening, isAutoListening = _c === void 0 ? false : _c;
return __awaiter(void 0, void 0, void 0, function () {
var unsubscribe_1;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
if (stopVoice()) {
return [2 /*return*/];
}
if (isPlaying) {
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.stop();
return [2 /*return*/];
}
if (settings.current.disableListening) {
return [2 /*return*/];
}
if (!(listener.status === 'stopped' && !isRecognizeInitializing)) return [3 /*break*/, 2];
isRecognizeInitializing = true;
unsubscribe_1 = listener.on('status', function () {
isRecognizeInitializing = false;
unsubscribe_1();
});
return [4 /*yield*/, client.init().catch(function (error) {
isRecognizeInitializing = false;
throw error;
})];
case 1:
_d.sent();
return [2 /*return*/, client.createVoiceStream(function (_a) {
var sendVoice = _a.sendVoice, messageId = _a.messageId;
begin === null || begin === void 0 ? void 0 : begin.forEach(function (chunk) { return sendVoice(new Uint8Array(chunk), false); });
currentVoiceMessageId = messageId;
return listener.listen(function (chunk, analyser, last) {
if (analyser) {
emit({ voiceAnalyser: { data: analyser } });
}
sendVoice(chunk, last, messageName);
});
}, {
source: {
sourceType: isAutoListening ? 'autoListening' : 'lavashar',
},
})];
case 2: return [2 /*return*/];
}
});
});
};
/**
* Стримит переданные чанки звука в VPS.
* При отсутствии last=true через 3 секунды тишины отправляет Cancel.
* Если было активно слушание или проигрывание – останавливает.
*
* @param chunks одноканальные, sampleRate: 16000
* @param last последние чанки этого стрима?
* @param messageName указать, если чанки для шазама
*/
var streamVoice = function (chunks, last, messageName) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
chunks = filterEmptyChunks(chunks);
if (streaming === null || streaming === void 0 ? void 0 : streaming(chunks, last)) {
return [2 /*return*/];
}
stopVoice();
if (isPlaying) {
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.stop();
}
if (!(!isRecognizeInitializing && chunks.length)) return [3 /*break*/, 2];
isRecognizeInitializing = true;
return [4 /*yield*/, client.init()];
case 1:
_a.sent();
return [2 /*return*/, client.createVoiceStream(function (_a) {
var messageId = _a.messageId, sendVoice = _a.sendVoice;
return __awaiter(void 0, void 0, void 0, function () {
var cancelTimeoutId;
return __generator(this, function (_b) {
cancelTimeoutId = -1;
isRecognizeInitializing = false;
currentVoiceMessageId = messageId;
streaming = function (ch, l) {
clearTimeout(cancelTimeoutId);
ch.forEach(function (chunk) { return sendVoice(new Uint8Array(chunk), l, messageName); });
if (l) {
streaming = null;
}
else {
cancelTimeoutId = setTimeout(function () {
if (streaming) {
stopVoice();
}
}, 3000);
}
return true;
};
streaming(chunks, last);
return [2 /*return*/];
});
});
}, {
source: {
sourceType: 'lavashar',
},
})];
case 2: return [2 /*return*/];
}
});
}); };
/**
* Отправляет готовые чанки звука в VPS.
* Чанки считаются завершёнными (сообщение отправляется с last=true).
* Если было активно слушание или проигрывание – останавливает.
*
* @param chunks одноканальные, sampleRate: 16000
* @param messageName указать, если чанки для шазама
*/
var sendVoice = function (chunks, messageName) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
chunks = filterEmptyChunks(chunks);
stopVoice();
if (isPlaying) {
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.stop();
}
if (!(!isRecognizeInitializing && chunks.length)) return [3 /*break*/, 2];
isRecognizeInitializing = true;
return [4 /*yield*/, client.init()];
case 1:
_a.sent();
return [2 /*return*/, client.createVoiceStream(function (_a) {
var messageId = _a.messageId, sendVoiceStream = _a.sendVoice;
isRecognizeInitializing = false;
currentVoiceMessageId = messageId;
chunks.forEach(function (chunk) { return sendVoiceStream(new Uint8Array(chunk), true, messageName); });
return Promise.resolve();
}, {
source: {
sourceType: 'lavashar',
},
})];
case 2: return [2 /*return*/];
}
});
}); };
/**
* Активирует слушание голоса.
* Если было активно слушание или проигрывание - останавливает, слушание в этом случае не активируется.
*
* @param begin одноканальные чанки, sampleRate: 16000 – будут отправлены перед голосом пользователя
*/
var listen = function (_a, isAutoListening) {
var _b = _a === void 0 ? {} : _a, begin = _b.begin;
return recognize({ begin: begin, isAutoListening: isAutoListening });
};
/**
* Активирует распознавание музыки.
* Если было активно слушание или проигрывание – останавливает
*/
var shazam = function () {
return recognize({ messageName: MessageNames.MTT, isAutoListening: false });
};
if (isAudioSupported) {
resolveAudioContext(function (context, destroy) {
/// создаем плеер только если поддерживается аудио
/// и только когда готов AudioContext
voicePlayer = createVoicePlayer(context, { startVoiceDelay: 1 });
destroyAudioContext = destroy;
// начало проигрывания озвучки
subscriptions.push(voicePlayer.on('play', function (mesId) {
isPlaying = true;
emit({ emotion: 'talk' });
emit({ tts: { status: 'start', messageId: Number(mesId), appInfo: appInfoDict[mesId] } });
}));
subscriptions.push(voicePlayer.on('stop', function (mesId) {
client.sendMute(Number(mesId));
}));
// окончание проигрывания озвучки
subscriptions.push(voicePlayer.on('end', function (mesId) {
isPlaying = false;
emit({ emotion: 'idle' });
emit({ tts: { status: 'stop', messageId: Number(mesId), appInfo: appInfoDict[mesId] } });
if (mesId === autolistenMessageId) {
listen().catch(function (error) {
// eslint-disable-next-line no-console
console.error(error);
});
}
// очистка сохраненных appInfo и messageId
var idx = 0;
do {
delete appInfoDict[mesIdQueue[0]];
} while (mesIdQueue[idx++] !== mesId && mesIdQueue.length > idx);
mesIdQueue.splice(0, idx);
}));
// оповещаем о готовности к воспроизведению звука
onReady === null || onReady === void 0 ? void 0 : onReady();
});
}
subscriptions.push(
// обработка входящей озвучки
client.on('voice', function (data, message) {
if (settings.current.disableDubbing) {
return;
}
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.append(data, message.messageId.toString(), message.last === 1);
}),
// статусы слушания речи
listener.on('status', function (status) {
emit({ listener: { status: status } });
if (status === 'listen') {
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.setActive(false);
emit({ emotion: 'listen' });
}
else if (status === 'stopped') {
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.setActive(!settings.current.disableDubbing);
emit({ asr: { text: '' }, emotion: 'idle' });
}
}),
// активация автослушания
client.on('systemMessage', function (systemMessage, originalMessage) {
var autoListening = systemMessage.auto_listening;
var messageId = originalMessage.messageId.toString();
if (typeof systemMessage.app_info !== 'undefined') {
appInfoDict[messageId] = systemMessage.app_info;
mesIdQueue.push(messageId);
}
if (autoListening) {
/// если озвучка включена - сохраняем mesId чтобы включить слушание после озвучки
/// если озвучка выключена - включаем слушание сразу
if (settings.current.disableDubbing === false) {
autolistenMessageId = messageId;
}
else {
listen({}, autoListening).catch(function (error) {
// eslint-disable-next-line no-console
console.error(error);
});
}
}
}), client.on('status', function (_a) {
var code = _a.code;
if (code < 0) {
stopVoice(false);
}
}), client.on('stt', function (_a, originalMessage) {
var _b;
var text = _a.text, response = _a.response;
var listening = listener.status === 'listen' && !settings.current.disableListening;
if (text) {
var last = originalMessage.last === 1;
if (last || listening) {
emit({
asr: {
mid: originalMessage.messageId,
text: text.data || '',
last: last,
},
});
}
if (last) {
stopVoice(false);
}
}
if (response) {
var decoderResultField = response.decoderResultField, errorResponse = response.errorResponse;
var last = !!(decoderResultField && (decoderResultField === null || decoderResultField === void 0 ? void 0 : decoderResultField.isFinal));
if ((last || listening) && ((_b = decoderResultField === null || decoderResultField === void 0 ? void 0 : decoderResultField.hypothesis) === null || _b === void 0 ? void 0 : _b.length)) {
emit({
asr: {
mid: originalMessage.messageId,
text: decoderResultField.hypothesis[0].normalizedText || '',
last: last,
},
});
}
if (last || errorResponse) {
stopVoice(false);
}
}
}), client.on('musicRecognition', function (response, originalMessage) {
var _a;
emit({ mtt: { response: response, mid: originalMessage.messageId } });
if (((_a = response.decoderResultField) === null || _a === void 0 ? void 0 : _a.isFinal) || response.errorResponse) {
stopVoice(false);
}
}), settings.on('change-request', function (nextSettings) {
var disableDubbing = nextSettings.disableDubbing, disableListening = nextSettings.disableListening;
/// Важен порядок обработки флагов слушания и озвучки —
/// сначала слушание, потом озвучка
disableListening && stopVoice();
// Такой вызов необходим, чтобы включая озвучку она тут же проигралась (при её наличии), и наоборот
settings.current.disableDubbing !== disableDubbing && (voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.setActive(!disableDubbing));
}));
return {
destroy: function () {
stopVoice();
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.setActive(false);
subscriptions.splice(0, subscriptions.length).map(function (unsubscribe) { return unsubscribe(); });
destroyAudioContext === null || destroyAudioContext === void 0 ? void 0 : destroyAudioContext();
},
listen: listen,
shazam: shazam,
sendVoice: sendVoice,
streamVoice: streamVoice,
stop: stop,
stopPlaying: function () {
voicePlayer === null || voicePlayer === void 0 ? void 0 : voicePlayer.stop();
},
toggleAnalyser: function (enable) {
useAnalyser = enable;
},
};
};
var createMutexedObject = function (initialObject) {
var _a = createNanoEvents(), on = _a.on, emit = _a.emit;
var object = __assign({}, initialObject);
var nextObject = {};
var mode = 'released';
var tryApply = function () {
if (mode === 'released') {
var prevObject_1 = object;
object = __assign(__assign({}, prevObject_1), nextObject);
var isObjectChanged = Object.keys(nextObject).some(function (name) { return nextObject[name] !== prevObject_1[name]; });
if (isObjectChanged) {
emit('changed', object, prevObject_1);
}
}
};
var lock = function () {
mode = 'locked';
};
var release = function () {
mode = 'released';
tryApply();
};
var change = function (setts) {
nextObject = __assign(__assign({}, nextObject), setts);
emit('change-request', setts);
tryApply();
};
var current = {};
Object.keys(initialObject).forEach(function (prop) {
Object.defineProperty(current, prop, {
get: function () {
return object[prop];
},
});
});
return {
on: on,
lock: lock,
release: release,
change: change,
current: current,
};
};
var createMutexSwitcher = function (_a, initialDeps) {
var lock = _a.lock, release = _a.release;
var deps = __assign({}, initialDeps);
return {
change: function (nextDeps) {
deps = __assign(__assign({}, deps), nextDeps);
if (Object.values(deps).every(function (dep) { return dep; })) {
release();
}
else {
lock();
}
},
};
};
var createAssistant = function (_a) {
var _b;
var getMeta = _a.getMeta, getInitialMeta = _a.getInitialMeta, getVoiceMeta = _a.getVoiceMeta, checkCertUrl = _a.checkCertUrl, configuration = __rest(_a, ["getMeta", "getInitialMeta", "getVoiceMeta", "checkCertUrl"]);
var _c = createNanoEvents(), on = _c.on, emit = _c.emit;
// default_character отправляется в мета при отправке InitialSettings
var defaultCharacter = 'sber';
// хеш [messageId]: requestId, где requestId - пользовательский ид экшена
var requestIdMap = {};
// mid для последнего отправленного/принятого сообщения (кроме server_action)
var lastMid = 0;
var subscriptions = [];
var backgroundApps = {};
var settings = createMutexedObject({
disableDubbing: configuration.settings.dubbing === -1,
disableListening: false,
sendTextAsSsml: false,
});
var settingsSwitcher = createMutexSwitcher(settings, { isListenerStopped: true, isVoicePlayerEnded: true });
// готов/не готов воспроизводить озвучку
var voiceReady = false;
// текущий апп
var app = { info: DEFAULT_APP };
var sdkMeta = { theme: 'dark' };
var metaProvider = function (additionalMeta) { return __awaiter(void 0, void 0, void 0, function () {
var appState, _a, current_app, getBackgroundAppsMeta, background_apps;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!(app !== null && app.info.frontendType === 'WEB_APP' && app.getState)) return [3 /*break*/, 2];
return [4 /*yield*/, promiseTimeout(app.getState(), STATE_UPDATE_TIMEOUT).catch(function () {
// eslint-disable-next-line no-console
console.error('App-state wasn`t resolved, timeout had been expired');
return undefined;
})];
case 1:
_a = _b.sent();
return [3 /*break*/, 3];
case 2:
_a = undefined;
_b.label = 3;
case 3:
appState = _a;
current_app = {
app_info: app.info,
state: appState || {},
};
getBackgroundAppsMeta = function () { return __awaiter(void 0, void 0, void 0, function () {
var apps, backgroundAppsIds, backgroundAppsMeta;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
apps = __assign({}, backgroundApps);
backgroundAppsIds = Object.keys(apps);
backgroundAppsMeta = [];
return [4 /*yield*/, Promise.all(backgroundAppsIds.map(function (applicationId) { return __awaiter(void 0, void 0, void 0, function () {
var _a, getState;
return __generator(this, function (_b) {
_a = apps[applicationId].getState, getState = _a === void 0 ? function () { return Promise.resolve({}); } : _a;
return [2 /*return*/, promiseTimeout(getState(), STATE_UPDATE_TIMEOUT).then(function (state) { return state; }, function () { return ({}); })];
});
}); })).then(function (results) {
results.forEach(function (appResult, index) {
var state = appResult;
var applicationId = backgroundAppsIds[index];
backgroundAppsMeta.push({
app_info: apps[applicationId].appInfo,
state: state,
});
});
})];
case 1:
_a.sent();
return [2 /*return*/, backgroundAppsMeta];
}
});
}); };
return [4 /*yield*/, getBackgroundAppsMeta()];
case 4:
background_apps = _b.sent();
return [2 /*return*/, convertFieldValuesToString(__assign(__assign(__assign(__assign({}, sdkMeta), { time: getTime(), current_app: current_app, background_apps: background_apps }), (additionalMeta || {})), (getMeta ? getMeta() : {})))];
}
});
}); };
var transport = createTransport({
createWS: (_b = configuration.fakeVps) === null || _b === void 0 ? void 0 : _b.createFakeWS,
checkCertUrl: checkCertUrl,
});
var protocol = createProtocol(transport, __assign(__assign({}, configuration), { getInitialMeta: typeof getInitialMeta !== 'undefined'
? function () {
return getInitialMeta().then(function (meta) {
return convertFieldValuesToString(__assign(__assign({}, meta), { default_character: defaultCharacter }));
});
}
: function () {
return convertFieldValuesToString({
default_character: defaultCharacter,
});
},
// пока голос не готов, выключаем озвучку
settings: __assign(__assign({}, configuration.settings), { dubbing: -1 }) }));
var client = createClient(protocol, metaProvider, {
getVoiceMeta: function () { return (getVoiceMeta ? convertFieldValuesToString(getVoiceMeta()) : {}); },
});
var voice = createVoice(client, settings, function (event) {
if (typeof event.tts !== 'undefined') {
emit('tts', event.tts);
settingsSwitcher.change({ isVoicePlayerEnded: event.tts.status === 'stop' });
return;
}
if (typeof event.listener !== 'undefined') {
settingsSwitcher.change({ isListenerStopped: event.listener.status === 'stopped' });
}
emit('assistant', event);
}, function () {
voiceReady = true;
if (!settings.current.disableDubbing) {
protocol.changeSettings({ dubbing: 1 });
}
});
/** завершает текущий апп */
var closeApp = function (closing) {
if (closing === void 0) { closing = app.info; }
// переключить на дефолтный апп
if (closing.applicationId === app.info.applicationId) {
/// выглядит как нарушение логики,
/// но с точки зрения апи - ок
/// иначе потребителю нужно знать про DEFAULT_APP
app = {
info: DEFAULT_APP,
};
}
if (!isDefaultApp(closing)) {
emit('app', { type: 'close', app: closing });
}
};
/** отправляет текст */
var sendText = function (text, shouldSendDisableDubbing, additionalMeta) {
if (shouldSendDisableDubbing === void 0) { shouldSendDisableDubbing = false; }
voice.stop();
return client.sendText(text, settings.current.sendTextAsSsml, shouldSendDisableDubbing, additionalMeta);
};
/** отправляет server_action */
var sendServerAction = function (serverAction, messageName, requestId, actionApp, mode) {
if (messageName === void 0) { messageName = 'SERVER_ACTION'; }
if (requestId === void 0) { requestId = undefined; }
if (actionApp === void 0) { actionApp = app.info; }
client.sendServerAction(serverAction, actionApp, messageName, mode).then(function (messageId) {
if (requestId && messageId) {
requestIdMap[messageId.toString()] = requestId;
}
});
};
/** отправляет ответ на запрос доступа к местоположению и пр. меты */
var sendMetaForPermissionRequest = function (requestMessageId, appInfo, items) { return __awaiter(void 0, void 0, void 0, function () {
var _a, props, data, meta;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, getAnswerForRequestPermissions(requestMessageId, appInfo, items)];
case 1:
_a = _b.sent(), props = __rest(_a.meta, []), data = __rest(_a, ["meta"]);
return [4 /*yield*/, metaProvider()];
case 2:
meta = _b.sent();
client.sendData(__assign({}, data), 'SERVER_ACTION', __assign(__assign({}, meta), convertFieldValuesToString(props)));
return [2 /*return*/];
}
});
}); };
subscriptions.push(protocol.on('ready', function () { return emit('vps', { type: 'ready' }); }));
// пока voicePlayer не доступен, включение озвучки не будет отправлено
subscriptions.push(settings.on('changed', function (nextSettings, prevSettings) {
if (nextSettings.disableDubbing !== prevSettings.disableDubbing) {
voiceReady && protocol.changeSettings({ dubbing: nextSettings.disableDubbing ? -1 : 1 });
}
}));
// при неудачном переподключении к сокету
subscriptions.push(transport.on('error', function (error) {
voice.stop();
protocol.clearQueue();
emit('vps', { type: 'error', error: error });
}));
// обработка исходящих сообщений
subscriptions.push(protocol.on('outcoming', function (message) {
if (message.text || message.voice) {
/// не прерываем множественные ответы для сервер-экшенов
/// прервем, если получим карточку или бабл в ответ
lastMid = message.messageId;
}
emit('vps', { type: 'outcoming', message: message });
}));
// обработка ошибок
subscriptions.push(protocol.on('error', function (error) {
emit('error', error);
}));
// оповещение о статусах
subscriptions.push(client.on('status', function (status, _a) {
var messageId = _a.messageId;
emit('status', status, messageId);
}));
// история на запрос GetHistoryRequest
subscriptions.push(client.on('history', function (history) {
emit('history', history);
}));
// обработка входящих команд, и событий аппа
subscriptions.push(client.on('systemMessage', function (systemMessage, originalMessage) {
if (originalMessage.messageName === 'ANSWER_TO_USER') {
var _a = systemMessage.answerId, answerId = _a === void 0 ? 0 : _a, activate_app_info = systemMessage.activate_app_info, _b = systemMessage.items, items = _b === void 0 ? [] : _b, mesAppInfo = systemMessage.app_info;
var isChatApp = mesAppInfo && mesAppInfo.frontendType === 'CHAT_APP';
var isDialog = mesAppInfo && mesAppInfo.frontendType === 'DIALOG';
var isAppChanged = mesAppInfo && mesAppInfo.applicationId !== app.info.applicationId;
if (
// DIALOG не попадает в current_app
!isDialog &&
isAppChanged &&
/// игнорируем activate_app_info для чатапов
/// по-умолчанию activate_app_info: true
(isChatApp || activate_app_info !== false)) {
emit('app', { type: 'run', app: mesAppInfo });
}
if (isDialog && isAppChanged && app.info.applicationId !== DEFAULT_APP.applicationId) {
emit('app', { type: 'run', app: DEFAULT_APP });
}
// cancel для множественных ответов
if (answerId >= 2 && lastMid > originalMessage.messageId) {
client.sendCancel(originalMessage.messageId);
}
// последним сообщением считаем, только если пришли карточки/баблы
if (lastMid < originalMessage.messageId && items.findIndex(function (_a) {
var bubble = _a.bubble, card = _a.card;
return bubble || card;
}) >= 0) {
lastMid = originalMessage.messageId;
}
if (items.length) {
var _loop_1 = function (i) {
var command = items[i].command;
if (typeof command !== 'undefined') {
window.setTimeout(function () { return emit('command', command); });
if (command.type === 'start_music_recognition') {
voice.shazam();
return { value: void 0 };
}
if (command.type === 'request_permissions' && mesAppInfo) {
sendMetaForPermissionRequest(originalMessage.messageId, mesAppInfo, command.permissions);
return { value: void 0 };
}
if (command.type === 'action') {
emit('actionCommand', {
type: 'command',
command: command,
appInfo: mesAppInfo,
});
}
if (mesAppInfo && BASIC_SMART_APP_COMMANDS_TYPES.includes(command.type)) {
// эмитим все команды, т.к бывают фоновые команды
emit('app', {
type: 'command',
command: __assign(__assign({}, command), { sdk_meta: {
mid: originalMessage.messageId.toString(),
requestId: requestIdMap[originalMessage.messageId.toString()],
} }),
app: mesAppInfo,
});
}
if (command.type === 'close_app' && !isDialog) {
closeApp(mesAppInfo);
}
}
};
for (var i = 0; i < items.length; i++) {
var state_1 = _loop_1(i);
if (typeof state_1 === "object")
return state_1.value;
}
}
emit('vps', { type: 'incoming', systemMessage: systemMessage, originalMessage: originalMessage });
}
}));
// прокидывает команды backgroundApp'ов в их подписчики
on('app', function (event) {
var _a;
if (event.type === 'command') {
var backgroundAppOnCommand = (_a = backgroundApps[event.app.applicationId]) === null || _a === void 0 ? void 0 : _a.commandsSubscribers;
if (Array.isArray(backgroundAppOnCommand)) {
backgroundAppOnCommand.forEach(function (onCommand) {
var _a;
onCommand(event.command, (_a = event.command.sdk_meta) === null || _a === void 0 ? void 0 : _a.mid);
});
}
}
});
/** уничтожает ассистент, очищает подписки */
var destroy = function () {
voice.destroy();
client.destroy();
protocol.destroy();
subscriptions.splice(0, subscriptions.length).map(function (unsubscribe) { return unsubscribe(); });
};
/** запускает ассистент (приветствие) */
var start = function (_a) {
var _b = _a === void 0 ? {} : _a, _c = _b.disableGreetings, disableGreetings = _c === void 0 ? false : _c, _d = _b.initPhrase, initPhrase = _d === void 0 ? undefined : _d, _e = _b.isFirstSession, isFirstSession = _e === void 0 ? false : _e;
return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_f) {
switch (_f.label) {
case 0:
if (!(!disableGreetings && isDefaultApp(app.info))) return [3 /*break*/, 2];
return [4 /*yield*/, client.sendOpenAssistant({ isFirstSession: isFirstSession })];
case 1:
_f.sent();
_f.label = 2;
case 2:
if (initPhrase) {
return [2 /*return*/, client
.sendText(initPhrase)
.then(function (messageId) { return (messageId ? client.waitForAnswer(messageId) : undefined); })];
}
return [2 /*return*/, undefined];
}
});
});
};
return {
get activeApp() {
return !isDefaultApp(app.info) ? app.info : null;
},
get settings() {
return Object.create(Object.getPrototypeOf(settings.current), Object.getOwnPropertyDescriptors(settings.current));
},
destroy: destroy,
closeApp: closeApp,
listen: voice.listen,
shazam: voice.shazam,
sendServerAction: sendServerAction,
getHistoryRequest: protocol.getHistoryRequest,
sendText: sendText,
sendVoice: voice.sendVoice,
streamVoice: voice.streamVoice,
start: start,
stop: function () {
voice.stop();
if (lastMid !== 0) {
client.sendCancel(lastMid);
}
setTimeout(function () {
protocol.clearQueue();
transport.close();
});
},
stopTts: voice.stopPlaying,
stopVoice: voice.stop,
emit: emit,
on: on,
changeConfiguration: protocol.changeConfiguration,
changeSettings: settings.change,
changeSdkMeta: function (nextSdkMeta) {
sdkMeta = __assign(__assign({}, sdkMeta), nextSdkMeta);
},
reconnect: protocol.reconnect,
get protocol() {
return protocol;
},
setActiveApp: function (info, getState) {
app = { info: info, getState: getState };
},
addBackgroundApp: function (_a) {
var appInfo = _a.appInfo, getState = _a.getState;
backgroundApps[appInfo.applicationId] = {
appInfo: appInfo,
getState: getState,
commandsSubscribers: [],
};
var remove = function () {
delete backgroundApps[appInfo.applicationId];
};
var onCommand = function (subscriber) {
var _a;
(_a = backgroundApps[appInfo.applicationId]) === null || _a === void 0 ? void 0 : _a.commandsSubscribers.push(subscriber);
return {
clearSubscribers: function () {
if (backgroundApps[appInfo.applicationId]) {
backgroundApps[appInfo.applicationId].commandsSubscribers = [];
}
},
};
};
var sendServerActionToBackgroundApp = function (serverAction, messageName, requestId) {
var _a;
if (messageName === void 0) { messageName = 'S