botium-connector-voip 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import ws from 'ws';
|
|
2
2
|
import lodash from 'lodash';
|
|
3
3
|
import axios from 'axios';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import https from 'https';
|
|
4
6
|
import debug$4 from 'debug';
|
|
5
|
-
import path from 'path';
|
|
6
7
|
import musicMetadata from 'music-metadata';
|
|
7
8
|
|
|
8
9
|
const debug$3 = debug$4('botium-connector-voip');
|
|
@@ -24,6 +25,9 @@ const Capabilities = {
|
|
|
24
25
|
VOIP_TTS_BODY: 'VOIP_TTS_BODY',
|
|
25
26
|
VOIP_TTS_HEADERS: 'VOIP_TTS_HEADERS',
|
|
26
27
|
VOIP_TTS_TIMEOUT: 'VOIP_TTS_TIMEOUT',
|
|
28
|
+
VOIP_TTS_CACHE_ENABLE: 'VOIP_TTS_CACHE_ENABLE',
|
|
29
|
+
VOIP_TTS_CACHE_SIZE: 'VOIP_TTS_CACHE_SIZE',
|
|
30
|
+
VOIP_TTS_PREFETCH_ENABLE: 'VOIP_TTS_PREFETCH_ENABLE',
|
|
27
31
|
VOIP_WORKER_URL: 'VOIP_WORKER_URL',
|
|
28
32
|
VOIP_WORKER_APIKEY: 'VOIP_WORKER_APIKEY',
|
|
29
33
|
VOIP_SIP_POOL_CALLER_ENABLE: 'VOIP_SIP_POOL_CALLER_ENABLE',
|
|
@@ -57,6 +61,9 @@ const Defaults = {
|
|
|
57
61
|
VOIP_STT_TIMEOUT: 10000,
|
|
58
62
|
VOIP_TTS_METHOD: 'GET',
|
|
59
63
|
VOIP_TTS_TIMEOUT: 10000,
|
|
64
|
+
VOIP_TTS_CACHE_ENABLE: true,
|
|
65
|
+
VOIP_TTS_CACHE_SIZE: 50,
|
|
66
|
+
VOIP_TTS_PREFETCH_ENABLE: true,
|
|
60
67
|
VOIP_STT_MESSAGE_HANDLING: 'ORIGINAL',
|
|
61
68
|
VOIP_STT_MESSAGE_HANDLING_TIMEOUT: 2500,
|
|
62
69
|
VOIP_STT_MESSAGE_HANDLING_DELIMITER: '. ',
|
|
@@ -71,6 +78,12 @@ const Defaults = {
|
|
|
71
78
|
VOIP_USE_GLOBAL_VOIP_WORKER: false,
|
|
72
79
|
VOIP_SIP_PROTOCOL: 'TCP'
|
|
73
80
|
};
|
|
81
|
+
const TTS_HTTP_AGENT = new http.Agent({
|
|
82
|
+
keepAlive: true
|
|
83
|
+
});
|
|
84
|
+
const TTS_HTTPS_AGENT = new https.Agent({
|
|
85
|
+
keepAlive: true
|
|
86
|
+
});
|
|
74
87
|
class BotiumConnectorVoip {
|
|
75
88
|
constructor({
|
|
76
89
|
queueBotSays,
|
|
@@ -84,9 +97,18 @@ class BotiumConnectorVoip {
|
|
|
84
97
|
this.sentencesBuilding = 0;
|
|
85
98
|
this.sentencesFinal = 0;
|
|
86
99
|
this.sentenceBuilding = false;
|
|
100
|
+
this.ttsCache = new Map();
|
|
101
|
+
this.ttsCacheEnabled = false;
|
|
102
|
+
this.ttsCacheMaxEntries = 0;
|
|
103
|
+
this.ttsPrefetchEnabled = false;
|
|
104
|
+
|
|
105
|
+
// For debugging latency between incoming STT (bot says) and outgoing audio (sendAudio)
|
|
106
|
+
this._lastBotSaysQueuedAt = null;
|
|
107
|
+
this._lastBotSaysText = null;
|
|
87
108
|
}
|
|
88
109
|
async Validate() {
|
|
89
110
|
debug$3('Validate called');
|
|
111
|
+
this.caps = Object.assign({}, Defaults, this.caps);
|
|
90
112
|
debug$3(this.caps.VOIP_STT_MESSAGE_HANDLING);
|
|
91
113
|
if (this.caps.VOIP_TTS_URL) {
|
|
92
114
|
this.axiosTtsParams = {
|
|
@@ -94,7 +116,9 @@ class BotiumConnectorVoip {
|
|
|
94
116
|
params: this._getParams(Capabilities.VOIP_TTS_PARAMS),
|
|
95
117
|
method: this.caps.VOIP_TTS_METHOD,
|
|
96
118
|
timeout: this.caps.VOIP_TTS_TIMEOUT,
|
|
97
|
-
headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS)
|
|
119
|
+
headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS),
|
|
120
|
+
httpAgent: TTS_HTTP_AGENT,
|
|
121
|
+
httpsAgent: TTS_HTTPS_AGENT
|
|
98
122
|
};
|
|
99
123
|
try {
|
|
100
124
|
const {
|
|
@@ -112,7 +136,10 @@ class BotiumConnectorVoip {
|
|
|
112
136
|
throw new Error(`Checking TTS Status failed - ${this._getAxiosErrOutput(err)}`);
|
|
113
137
|
}
|
|
114
138
|
}
|
|
115
|
-
|
|
139
|
+
const cacheSize = parseInt(this.caps[Capabilities.VOIP_TTS_CACHE_SIZE], 10);
|
|
140
|
+
this.ttsCacheEnabled = !!this.caps[Capabilities.VOIP_TTS_CACHE_ENABLE];
|
|
141
|
+
this.ttsCacheMaxEntries = lodash.isFinite(cacheSize) && cacheSize > 0 ? cacheSize : 0;
|
|
142
|
+
this.ttsPrefetchEnabled = !!this.caps[Capabilities.VOIP_TTS_PREFETCH_ENABLE];
|
|
116
143
|
}
|
|
117
144
|
async Start() {
|
|
118
145
|
debug$3('Start called');
|
|
@@ -122,19 +149,31 @@ class BotiumConnectorVoip {
|
|
|
122
149
|
this.end = false;
|
|
123
150
|
this.connected = false;
|
|
124
151
|
this.convoStep = null;
|
|
152
|
+
this._lastBotSaysQueuedAt = null;
|
|
153
|
+
this._lastBotSaysText = null;
|
|
154
|
+
if (this.ttsCache) {
|
|
155
|
+
this.ttsCache.clear();
|
|
156
|
+
}
|
|
125
157
|
const sendBotMsg = botMsg => {
|
|
158
|
+
this._lastBotSaysQueuedAt = Date.now();
|
|
159
|
+
this._lastBotSaysText = botMsg && botMsg.messageText ? String(botMsg.messageText) : null;
|
|
126
160
|
setTimeout(() => this.queueBotSays(botMsg), 0);
|
|
127
161
|
};
|
|
128
162
|
const joinBotMsg = (botMsgs, joinLastPrevMsg) => {
|
|
129
163
|
const botMsg = {};
|
|
130
164
|
botMsg.messageText = botMsgs.map(m => m.messageText).join(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_DELIMITER] || '');
|
|
131
165
|
botMsg.sourceData = botMsgs.map(m => m.sourceData);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
const firstStart = lodash.get(botMsgs, '[0].sourceData.data.start', null);
|
|
167
|
+
const lastEnd = lodash.get(botMsgs, `[${botMsgs.length - 1}].sourceData.data.end`, null);
|
|
168
|
+
const prevEnd = lodash.get(joinLastPrevMsg, 'sourceData.data.end', null);
|
|
169
|
+
// joinLastPrevMsg can be null (first joined message) OR incomplete (in case JOIN timeout fired before we got any final STT info).
|
|
170
|
+
// In those cases, fall back to "start-of-first-chunk" semantics (same as previous behaviour for nil joinLastPrevMsg).
|
|
171
|
+
if (lodash.isFinite(firstStart) && lodash.isFinite(lastEnd)) {
|
|
172
|
+
botMsg.sourceData[0].silenceDuration = lodash.isFinite(prevEnd) ? firstStart - prevEnd : firstStart;
|
|
173
|
+
botMsg.sourceData[0].voiceDuration = lastEnd - firstStart;
|
|
135
174
|
} else {
|
|
136
|
-
botMsg.sourceData[0].silenceDuration =
|
|
137
|
-
botMsg.sourceData[0].voiceDuration =
|
|
175
|
+
botMsg.sourceData[0].silenceDuration = lodash.isFinite(firstStart) ? firstStart : null;
|
|
176
|
+
botMsg.sourceData[0].voiceDuration = lodash.isFinite(firstStart) && lodash.isFinite(lastEnd) ? lastEnd - firstStart : null;
|
|
138
177
|
}
|
|
139
178
|
return botMsg;
|
|
140
179
|
};
|
|
@@ -163,12 +202,14 @@ class BotiumConnectorVoip {
|
|
|
163
202
|
const connect = async retryIndex => {
|
|
164
203
|
retryIndex = retryIndex || 0;
|
|
165
204
|
try {
|
|
205
|
+
const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL].replace('wss', 'https').replace('ws', 'http');
|
|
206
|
+
const baseUrl = workerUrl.endsWith('/') ? workerUrl.slice(0, -1) : workerUrl;
|
|
166
207
|
const res = await axios({
|
|
167
208
|
method: 'post',
|
|
168
209
|
data: {
|
|
169
210
|
API_KEY: this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_APIKEY : this.caps[Capabilities.VOIP_WORKER_APIKEY]
|
|
170
211
|
},
|
|
171
|
-
url:
|
|
212
|
+
url: `${baseUrl}/initCall`
|
|
172
213
|
});
|
|
173
214
|
if (res) {
|
|
174
215
|
data = res.data;
|
|
@@ -222,6 +263,35 @@ class BotiumConnectorVoip {
|
|
|
222
263
|
}
|
|
223
264
|
this.eventEmitter.on('CONVO_STEP_NEXT', (container, convoStep) => {
|
|
224
265
|
this.convoStep = convoStep;
|
|
266
|
+
this._maybePrefetchTts(convoStep);
|
|
267
|
+
// For PSST: send join silence duration per step to VOIP worker (controls PSST silence trigger)
|
|
268
|
+
try {
|
|
269
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' && this.ws && this.ws.readyState === ws.OPEN) {
|
|
270
|
+
const joinLogicHook = this._getJoinLogicHook(convoStep);
|
|
271
|
+
let silenceMs = null;
|
|
272
|
+
if (joinLogicHook && joinLogicHook.args && joinLogicHook.args.length > 0) {
|
|
273
|
+
silenceMs = parseInt(joinLogicHook.args[0], 10);
|
|
274
|
+
}
|
|
275
|
+
// Fallback to global timeout if no per-step hook is set
|
|
276
|
+
if (!lodash.isFinite(silenceMs) || silenceMs <= 0) {
|
|
277
|
+
silenceMs = parseInt(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT], 10);
|
|
278
|
+
}
|
|
279
|
+
// PSST: treat like JOIN, but subtract 500ms from timeout
|
|
280
|
+
if (lodash.isFinite(silenceMs) && silenceMs > 0) {
|
|
281
|
+
silenceMs = Math.max(0, silenceMs - 500);
|
|
282
|
+
}
|
|
283
|
+
if (lodash.isFinite(silenceMs) && silenceMs > 0 && this.sessionId) {
|
|
284
|
+
debug$3(`PSST: sending silenceDurationMs=${silenceMs} for sessionId=${this.sessionId}`);
|
|
285
|
+
this.ws.send(JSON.stringify({
|
|
286
|
+
METHOD: 'setSttSilenceDuration',
|
|
287
|
+
sessionId: this.sessionId,
|
|
288
|
+
silenceDurationMs: silenceMs
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
debug$3(`Failed sending PSST silence duration to VOIP worker: ${err.message || err}`);
|
|
294
|
+
}
|
|
225
295
|
});
|
|
226
296
|
this.silence = null;
|
|
227
297
|
this.msgCount = 0;
|
|
@@ -230,6 +300,18 @@ class BotiumConnectorVoip {
|
|
|
230
300
|
this.silenceTimeout = null;
|
|
231
301
|
this.wsOpened = true;
|
|
232
302
|
debug$3(`Websocket connection to ${wsEndpoint} opened.`);
|
|
303
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
304
|
+
const isPsst = sttHandling === 'PSST';
|
|
305
|
+
const sttLegacy = true;
|
|
306
|
+
let sttUrl = this.caps[Capabilities.VOIP_STT_URL_STREAM];
|
|
307
|
+
if (isPsst && lodash.isString(sttUrl)) {
|
|
308
|
+
const replaced = sttUrl.replace('/api/sttstream/', '/api/stt/');
|
|
309
|
+
if (replaced !== sttUrl) {
|
|
310
|
+
sttUrl = replaced;
|
|
311
|
+
} else {
|
|
312
|
+
debug$3(`PSST: Could not derive /api/stt url from ${sttUrl}, using as-is`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
233
315
|
const request = {
|
|
234
316
|
METHOD: 'initCall',
|
|
235
317
|
SIP_CALLER_AUTO: this.caps[Capabilities.VOIP_SIP_POOL_CALLER_ENABLE],
|
|
@@ -250,8 +332,9 @@ class BotiumConnectorVoip {
|
|
|
250
332
|
ICE_TURN_PASSWORD: this.caps[Capabilities.VOIP_ICE_TURN_PASSWORD],
|
|
251
333
|
ICE_TURN_PROTOCOL: this.caps[Capabilities.VOIP_ICE_TURN_PROTOCOL] || 'TCP',
|
|
252
334
|
MIN_SILENCE_DURATION: this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_ENABLE] ? this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT] : null,
|
|
335
|
+
STT_LEGACY: sttLegacy,
|
|
253
336
|
STT_CONFIG: {
|
|
254
|
-
stt_url:
|
|
337
|
+
stt_url: sttUrl,
|
|
255
338
|
stt_params: this.caps[Capabilities.VOIP_STT_PARAMS_STREAM],
|
|
256
339
|
stt_body: this.caps[Capabilities.VOIP_STT_BODY_STREAM] || null
|
|
257
340
|
},
|
|
@@ -272,9 +355,88 @@ class BotiumConnectorVoip {
|
|
|
272
355
|
});
|
|
273
356
|
this.ws.on('message', async data => {
|
|
274
357
|
const parsedData = JSON.parse(data);
|
|
358
|
+
// Allow fullRecord delivery even if Stop() was already called.
|
|
359
|
+
// Otherwise the recording can be dropped if the worker streams it late (common on hangup).
|
|
360
|
+
if (this.stopCalled) {
|
|
361
|
+
const allowedTypes = ['fullRecord', 'fullRecordStart', 'fullRecordChunk', 'fullRecordEnd', 'error'];
|
|
362
|
+
if (!parsedData || !parsedData.type || !allowedTypes.includes(parsedData.type)) {
|
|
363
|
+
debug$3(`${this.sessionId} - Stop already called, ignoring incoming message`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
275
367
|
const parsedDataLog = lodash.cloneDeep(parsedData);
|
|
276
|
-
|
|
368
|
+
// Sanitize all potential base64 fields for logging (don't dump huge buffers)
|
|
369
|
+
const sanitizeBase64Fields = (obj, prefix = '') => {
|
|
370
|
+
if (!obj || typeof obj !== 'object') return;
|
|
371
|
+
for (const key of Object.keys(obj)) {
|
|
372
|
+
const val = obj[key];
|
|
373
|
+
if (typeof val === 'string' && val.length > 500) {
|
|
374
|
+
obj[key] = `<base64:${val.length}chars>`;
|
|
375
|
+
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
376
|
+
sanitizeBase64Fields(val, `${prefix}${key}.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
sanitizeBase64Fields(parsedDataLog);
|
|
277
381
|
debug$3(JSON.stringify(parsedDataLog, null, 2));
|
|
382
|
+
const _extractFullRecordBase64 = pd => {
|
|
383
|
+
if (!pd) return null;
|
|
384
|
+
// Different VOIP workers may put the payload in various fields - search all string fields
|
|
385
|
+
// First try known field names
|
|
386
|
+
const knownFields = [pd.fullRecord, pd.full_record, pd.data?.fullRecord, pd.data?.full_record, pd.data?.b64_buffer, pd.data?.base64, pd.data?.full_record_buffer, pd.data?.buffer, pd.buffer, pd.base64, pd.audio, pd.audioData, pd.data?.audio, pd.data?.audioData];
|
|
387
|
+
for (const val of knownFields) {
|
|
388
|
+
if (typeof val === 'string' && val.length > 100) return val;
|
|
389
|
+
}
|
|
390
|
+
// Fallback: find any large string field (likely base64)
|
|
391
|
+
const findLargeString = (obj, depth = 0) => {
|
|
392
|
+
if (!obj || typeof obj !== 'object' || depth > 3) return null;
|
|
393
|
+
for (const key of Object.keys(obj)) {
|
|
394
|
+
const val = obj[key];
|
|
395
|
+
if (typeof val === 'string' && val.length > 1000) {
|
|
396
|
+
debug$3(`${this.sessionId} - Found base64 in field '${key}' (len=${val.length})`);
|
|
397
|
+
return val;
|
|
398
|
+
}
|
|
399
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
400
|
+
const found = findLargeString(val, depth + 1);
|
|
401
|
+
if (found) return found;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
};
|
|
406
|
+
return findLargeString(pd);
|
|
407
|
+
};
|
|
408
|
+
const emitFullRecordAttachment = base64 => {
|
|
409
|
+
if (!base64 || typeof base64 !== 'string' || base64.length === 0) {
|
|
410
|
+
// Log available fields (length only) to help diagnose worker payload shape without dumping base64
|
|
411
|
+
try {
|
|
412
|
+
const candidates = {
|
|
413
|
+
fullRecord: typeof parsedData?.fullRecord === 'string' ? parsedData.fullRecord.length : null,
|
|
414
|
+
full_record: typeof parsedData?.full_record === 'string' ? parsedData.full_record.length : null,
|
|
415
|
+
data_fullRecord: typeof parsedData?.data?.fullRecord === 'string' ? parsedData.data.fullRecord.length : null,
|
|
416
|
+
data_full_record: typeof parsedData?.data?.full_record === 'string' ? parsedData.data.full_record.length : null,
|
|
417
|
+
data_b64_buffer: typeof parsedData?.data?.b64_buffer === 'string' ? parsedData.data.b64_buffer.length : null,
|
|
418
|
+
data_base64: typeof parsedData?.data?.base64 === 'string' ? parsedData.data.base64.length : null,
|
|
419
|
+
data_full_record_buffer: typeof parsedData?.data?.full_record_buffer === 'string' ? parsedData.data.full_record_buffer.length : null,
|
|
420
|
+
data_buffer: typeof parsedData?.data?.buffer === 'string' ? parsedData.data.buffer.length : null
|
|
421
|
+
};
|
|
422
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit. CandidateLengths=${JSON.stringify(candidates)}`);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit (diag failed: ${err.message || err})`);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
debug$3(`${this.sessionId} - Emitting MESSAGE_ATTACHMENT full_record.wav (base64Len=${base64.length}, stopCalled=${!!this.stopCalled})`);
|
|
429
|
+
this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
|
|
430
|
+
name: 'full_record.wav',
|
|
431
|
+
mimeType: 'audio/wav',
|
|
432
|
+
base64,
|
|
433
|
+
// Session context from capabilities for correlation
|
|
434
|
+
sessionContext: {
|
|
435
|
+
testSessionId: this.caps.VOIP_TEST_SESSION_ID || null,
|
|
436
|
+
testSessionJobId: this.caps.VOIP_TEST_SESSION_JOB_ID || null
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
};
|
|
278
440
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
279
441
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
280
442
|
}
|
|
@@ -310,6 +472,22 @@ class BotiumConnectorVoip {
|
|
|
310
472
|
reject(new Error(`Error: ${parsedData.message}`));
|
|
311
473
|
sendBotMsg(new Error(`Error: ${parsedData.message}`));
|
|
312
474
|
}
|
|
475
|
+
|
|
476
|
+
// Full record streaming support:
|
|
477
|
+
// - some VOIP workers send the recording in chunks and an end marker
|
|
478
|
+
if (parsedData && parsedData.type === 'fullRecordStart') {
|
|
479
|
+
this.fullRecord = '';
|
|
480
|
+
}
|
|
481
|
+
if (parsedData && parsedData.type === 'fullRecordChunk') {
|
|
482
|
+
const chunk = _extractFullRecordBase64(parsedData);
|
|
483
|
+
if (chunk) this.fullRecord = (this.fullRecord || '') + chunk;
|
|
484
|
+
}
|
|
485
|
+
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
486
|
+
const tail = _extractFullRecordBase64(parsedData);
|
|
487
|
+
if (tail) this.fullRecord = (this.fullRecord || '') + tail;
|
|
488
|
+
emitFullRecordAttachment(this.fullRecord || tail);
|
|
489
|
+
this.end = true;
|
|
490
|
+
}
|
|
313
491
|
if (parsedData && parsedData.type === 'silence') {
|
|
314
492
|
if (lodash.isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
315
493
|
if (lodash.isNil(this._getJoinLogicHook(this.convoStep)) && parsedData.data.silence.length > 0) {
|
|
@@ -319,11 +497,8 @@ class BotiumConnectorVoip {
|
|
|
319
497
|
}
|
|
320
498
|
}
|
|
321
499
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
mimeType: 'audio/wav',
|
|
325
|
-
base64: parsedData.fullRecord
|
|
326
|
-
});
|
|
500
|
+
// Non-chunked full record
|
|
501
|
+
emitFullRecordAttachment(_extractFullRecordBase64(parsedData));
|
|
327
502
|
this.end = true;
|
|
328
503
|
}
|
|
329
504
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
@@ -337,16 +512,29 @@ class BotiumConnectorVoip {
|
|
|
337
512
|
}
|
|
338
513
|
}
|
|
339
514
|
this.firstSttInfoReceived = true;
|
|
340
|
-
if (this.prevData) {
|
|
515
|
+
if (this.prevData && this.prevData.data && !lodash.isNil(this.prevData.data.end)) {
|
|
341
516
|
if (!lodash.isNil(parsedData.data.start)) {
|
|
342
517
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
343
518
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
344
|
-
const
|
|
519
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
520
|
+
const isPsst = sttHandling === 'PSST';
|
|
521
|
+
const isJoinMethod = sttHandling === 'JOIN' || isPsst;
|
|
522
|
+
const toJoinTimeoutMs = ms => {
|
|
523
|
+
const parsed = parseInt(ms, 10);
|
|
524
|
+
if (!lodash.isFinite(parsed) || parsed <= 0) return null;
|
|
525
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
526
|
+
};
|
|
345
527
|
let matched = false;
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
528
|
+
if (!lodash.isNil(joinLogicHook) && isJoinMethod) {
|
|
529
|
+
const joinTimeoutMs = toJoinTimeoutMs(joinLogicHook && joinLogicHook.args && joinLogicHook.args[0]);
|
|
530
|
+
if (lodash.isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
531
|
+
matched = true;
|
|
532
|
+
}
|
|
533
|
+
} else if (lodash.isNil(joinLogicHook) && isJoinMethod) {
|
|
534
|
+
const joinTimeoutMs = toJoinTimeoutMs(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
|
|
535
|
+
if (lodash.isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
536
|
+
matched = true;
|
|
537
|
+
}
|
|
350
538
|
}
|
|
351
539
|
if (matched && this.botMsgs.length > 0) {
|
|
352
540
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
@@ -361,6 +549,7 @@ class BotiumConnectorVoip {
|
|
|
361
549
|
const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
|
|
362
550
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
|
|
363
551
|
debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
|
|
552
|
+
// ORIGINAL: emit final message immediately, unless join hooks are active.
|
|
364
553
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash.isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
365
554
|
let botMsg = {
|
|
366
555
|
messageText: parsedData.data.message
|
|
@@ -375,7 +564,9 @@ class BotiumConnectorVoip {
|
|
|
375
564
|
this.firstMsg = false;
|
|
376
565
|
} else {
|
|
377
566
|
const sourceData = parsedData;
|
|
378
|
-
|
|
567
|
+
const start = lodash.get(parsedData, 'data.start', null);
|
|
568
|
+
const prevEnd = lodash.get(this.prevData, 'data.end', null);
|
|
569
|
+
sourceData.silenceDuration = lodash.isFinite(start) && lodash.isFinite(prevEnd) ? start - prevEnd : lodash.isFinite(start) ? start : null;
|
|
379
570
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
380
571
|
botMsg = Object.assign({}, botMsg, {
|
|
381
572
|
sourceData
|
|
@@ -402,7 +593,9 @@ class BotiumConnectorVoip {
|
|
|
402
593
|
this.firstMsg = false;
|
|
403
594
|
} else {
|
|
404
595
|
const sourceData = parsedData;
|
|
405
|
-
|
|
596
|
+
const start = lodash.get(parsedData, 'data.start', null);
|
|
597
|
+
const prevEnd = lodash.get(this.prevData, 'data.end', null);
|
|
598
|
+
sourceData.silenceDuration = lodash.isFinite(start) && lodash.isFinite(prevEnd) ? start - prevEnd : lodash.isFinite(start) ? start : null;
|
|
406
599
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
407
600
|
botMsg = Object.assign({}, botMsg, {
|
|
408
601
|
sourceData
|
|
@@ -416,7 +609,7 @@ class BotiumConnectorVoip {
|
|
|
416
609
|
}
|
|
417
610
|
this.botMsgs = [];
|
|
418
611
|
}
|
|
419
|
-
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'CONCAT' || !lodash.isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
612
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'CONCAT' || !lodash.isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
420
613
|
const botMsg = {
|
|
421
614
|
messageText: parsedData.data.message,
|
|
422
615
|
sourceData: parsedData
|
|
@@ -424,15 +617,27 @@ class BotiumConnectorVoip {
|
|
|
424
617
|
this.prevData = parsedData;
|
|
425
618
|
if (successfulConfidenceScore) {
|
|
426
619
|
this.botMsgs.push(botMsg);
|
|
620
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
621
|
+
const isPsst = sttHandling === 'PSST';
|
|
622
|
+
const toJoinTimeoutMs = ms => {
|
|
623
|
+
const parsed = parseInt(ms, 10);
|
|
624
|
+
if (!lodash.isFinite(parsed) || parsed <= 0) return null;
|
|
625
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
626
|
+
};
|
|
627
|
+
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
628
|
+
let joinTimeoutMs = toJoinTimeoutMs(lodash.get(joinLogicHook, 'args[0]'));
|
|
629
|
+
if (!lodash.isFinite(joinTimeoutMs) || joinTimeoutMs <= 0) {
|
|
630
|
+
joinTimeoutMs = toJoinTimeoutMs(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
|
|
631
|
+
}
|
|
427
632
|
this.silenceTimeout = setTimeout(() => {
|
|
428
633
|
if (this.botMsgs.length > 0) {
|
|
429
|
-
debug$3('Silence Duration Timeout (PSST):',
|
|
634
|
+
debug$3('Silence Duration Timeout (JOIN/PSST):', joinTimeoutMs, 'ms');
|
|
430
635
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
431
636
|
this.firstMsg = false;
|
|
432
637
|
this.joinLastPrevMsg = this.botMsgs[this.botMsgs.length - 1];
|
|
433
638
|
this.botMsgs = [];
|
|
434
639
|
}
|
|
435
|
-
},
|
|
640
|
+
}, joinTimeoutMs || 0);
|
|
436
641
|
}
|
|
437
642
|
}
|
|
438
643
|
}
|
|
@@ -444,7 +649,24 @@ class BotiumConnectorVoip {
|
|
|
444
649
|
}
|
|
445
650
|
async UserSays(msg) {
|
|
446
651
|
debug$3('UserSays called');
|
|
447
|
-
|
|
652
|
+
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
653
|
+
try {
|
|
654
|
+
const safeLog = {
|
|
655
|
+
messageText: msg && msg.messageText ? String(msg.messageText).substring(0, 200) : undefined,
|
|
656
|
+
buttons: msg && Array.isArray(msg.buttons) ? msg.buttons.length : 0,
|
|
657
|
+
hasMedia: !!(msg && msg.media && msg.media.length > 0),
|
|
658
|
+
mediaMimeType: msg && msg.media && msg.media[0] ? msg.media[0].mimeType : undefined,
|
|
659
|
+
mediaSize: msg && msg.media && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined,
|
|
660
|
+
attachments: msg && Array.isArray(msg.attachments) ? msg.attachments.map(a => ({
|
|
661
|
+
name: a && (a.name || a.title),
|
|
662
|
+
mimeType: a && a.mimeType,
|
|
663
|
+
base64Len: a && a.base64 ? a.base64.length : 0
|
|
664
|
+
})) : []
|
|
665
|
+
};
|
|
666
|
+
debug$3(safeLog);
|
|
667
|
+
} catch (err) {
|
|
668
|
+
debug$3(`UserSays: failed to build safe log: ${err.message || err}`);
|
|
669
|
+
}
|
|
448
670
|
if (!msg.attachments) {
|
|
449
671
|
msg.attachments = [];
|
|
450
672
|
}
|
|
@@ -459,23 +681,32 @@ class BotiumConnectorVoip {
|
|
|
459
681
|
});
|
|
460
682
|
this.ws.send(request);
|
|
461
683
|
} else if (msg && msg.messageText) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
684
|
+
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
685
|
+
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
686
|
+
if (dtmfMatch && dtmfMatch[1]) {
|
|
687
|
+
const digits = dtmfMatch[1];
|
|
688
|
+
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
689
|
+
const request = JSON.stringify({
|
|
690
|
+
METHOD: 'sendDtmf',
|
|
691
|
+
digits,
|
|
692
|
+
sessionId: this.sessionId
|
|
693
|
+
});
|
|
694
|
+
this.ws.send(request);
|
|
695
|
+
return resolve();
|
|
696
|
+
}
|
|
697
|
+
if (!this.axiosTtsParams) {
|
|
698
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
699
|
+
return reject(new Error('TTS not configured, only audio input supported'));
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
703
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
473
704
|
msg.sourceData = ttsRequest;
|
|
474
|
-
let
|
|
705
|
+
let ttsResult = null;
|
|
475
706
|
try {
|
|
476
|
-
|
|
707
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
477
708
|
} catch (err) {
|
|
478
|
-
reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
709
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
479
710
|
}
|
|
480
711
|
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
481
712
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
@@ -486,22 +717,28 @@ class BotiumConnectorVoip {
|
|
|
486
717
|
apiKey
|
|
487
718
|
});
|
|
488
719
|
}
|
|
489
|
-
if (Buffer.isBuffer(
|
|
490
|
-
duration =
|
|
720
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
721
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
722
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
491
723
|
const request = JSON.stringify({
|
|
492
724
|
METHOD: 'sendAudio',
|
|
493
725
|
PESQ: false,
|
|
494
726
|
sessionId: this.sessionId,
|
|
495
|
-
b64_buffer:
|
|
727
|
+
b64_buffer: b64Buffer
|
|
496
728
|
});
|
|
729
|
+
if (this._lastBotSaysQueuedAt) {
|
|
730
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
731
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
732
|
+
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
733
|
+
}
|
|
497
734
|
msg.attachments.push({
|
|
498
735
|
name: 'tts.wav',
|
|
499
736
|
mimeType: 'audio/wav',
|
|
500
|
-
base64:
|
|
737
|
+
base64: b64Buffer
|
|
501
738
|
});
|
|
502
739
|
this.ws.send(request);
|
|
503
740
|
} else {
|
|
504
|
-
reject(new Error(
|
|
741
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
505
742
|
}
|
|
506
743
|
}
|
|
507
744
|
}
|
|
@@ -511,6 +748,11 @@ class BotiumConnectorVoip {
|
|
|
511
748
|
sessionId: this.sessionId,
|
|
512
749
|
b64_buffer: msg.media[0].buffer.toString('base64')
|
|
513
750
|
});
|
|
751
|
+
if (this._lastBotSaysQueuedAt) {
|
|
752
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
753
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
754
|
+
debug$3(`Latency (bot says -> sendAudio/MEDIA): ${latencyMs} ms (last bot: ${botText})`);
|
|
755
|
+
}
|
|
514
756
|
this.ws.send(request);
|
|
515
757
|
msg.attachments.push({
|
|
516
758
|
name: msg.media[0].mediaUri,
|
|
@@ -535,6 +777,11 @@ class BotiumConnectorVoip {
|
|
|
535
777
|
}
|
|
536
778
|
async Stop() {
|
|
537
779
|
debug$3(`${this.sessionId} - Stop called`);
|
|
780
|
+
this.stopCalled = true;
|
|
781
|
+
if (this.silenceTimeout) {
|
|
782
|
+
clearTimeout(this.silenceTimeout);
|
|
783
|
+
this.silenceTimeout = null;
|
|
784
|
+
}
|
|
538
785
|
if (this.ws && this.ws.readyState !== ws.CLOSED) {
|
|
539
786
|
const request = JSON.stringify({
|
|
540
787
|
METHOD: 'stopCall',
|
|
@@ -542,9 +789,12 @@ class BotiumConnectorVoip {
|
|
|
542
789
|
});
|
|
543
790
|
this.ws.send(request);
|
|
544
791
|
await new Promise(resolve => {
|
|
545
|
-
setTimeout(resolve, 100000);
|
|
546
|
-
setInterval(() => {
|
|
792
|
+
const stopTimeout = setTimeout(resolve, 100000);
|
|
793
|
+
this._stopWaitInterval = setInterval(() => {
|
|
547
794
|
if (this.end) {
|
|
795
|
+
clearTimeout(stopTimeout);
|
|
796
|
+
clearInterval(this._stopWaitInterval);
|
|
797
|
+
this._stopWaitInterval = null;
|
|
548
798
|
this.wsOpened = false;
|
|
549
799
|
this.ws = null;
|
|
550
800
|
this.end = false;
|
|
@@ -562,6 +812,126 @@ class BotiumConnectorVoip {
|
|
|
562
812
|
this.firstSttInfoReceived = false;
|
|
563
813
|
}
|
|
564
814
|
}
|
|
815
|
+
_isTtsCacheEnabled() {
|
|
816
|
+
return this.ttsCacheEnabled && this.ttsCacheMaxEntries > 0;
|
|
817
|
+
}
|
|
818
|
+
_touchTtsCache(cacheKey, entry) {
|
|
819
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
820
|
+
this.ttsCache.delete(cacheKey);
|
|
821
|
+
this.ttsCache.set(cacheKey, entry);
|
|
822
|
+
}
|
|
823
|
+
_setTtsCacheEntry(cacheKey, entry) {
|
|
824
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
825
|
+
this.ttsCache.set(cacheKey, entry);
|
|
826
|
+
this._trimTtsCache();
|
|
827
|
+
}
|
|
828
|
+
_deleteTtsCacheEntry(cacheKey) {
|
|
829
|
+
if (this.ttsCache) {
|
|
830
|
+
this.ttsCache.delete(cacheKey);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
_trimTtsCache() {
|
|
834
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
835
|
+
while (this.ttsCache.size > this.ttsCacheMaxEntries) {
|
|
836
|
+
const oldestKey = this.ttsCache.keys().next().value;
|
|
837
|
+
this.ttsCache.delete(oldestKey);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
_shouldPrefetchTtsText(text) {
|
|
841
|
+
if (!lodash.isString(text)) return false;
|
|
842
|
+
if (!text.trim()) return false;
|
|
843
|
+
return !/\$[A-Za-z]\w*/.test(text);
|
|
844
|
+
}
|
|
845
|
+
_maybePrefetchTts(convoStep) {
|
|
846
|
+
if (!this.ttsPrefetchEnabled || !this._isTtsCacheEnabled()) return;
|
|
847
|
+
if (!convoStep || convoStep.sender !== 'me') return;
|
|
848
|
+
if (!this._shouldPrefetchTtsText(convoStep.messageText)) return;
|
|
849
|
+
const ttsRequest = this._buildTtsRequest(convoStep.messageText);
|
|
850
|
+
if (!ttsRequest) return;
|
|
851
|
+
this._getTtsAudio(ttsRequest, convoStep.messageText).catch(err => {
|
|
852
|
+
debug$3(`TTS prefetch failed - ${this._getAxiosErrOutput(err)}`);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
_buildTtsRequest(text) {
|
|
856
|
+
if (!this.axiosTtsParams) return null;
|
|
857
|
+
return {
|
|
858
|
+
...this.axiosTtsParams,
|
|
859
|
+
params: {
|
|
860
|
+
...(this.axiosTtsParams.params || {}),
|
|
861
|
+
text
|
|
862
|
+
},
|
|
863
|
+
data: this._getBody(Capabilities.VOIP_TTS_BODY),
|
|
864
|
+
responseType: 'arraybuffer'
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
_getTtsCacheKey(ttsRequest) {
|
|
868
|
+
const cacheKeyObj = {
|
|
869
|
+
url: ttsRequest.url,
|
|
870
|
+
method: ttsRequest.method,
|
|
871
|
+
params: ttsRequest.params,
|
|
872
|
+
data: ttsRequest.data
|
|
873
|
+
};
|
|
874
|
+
try {
|
|
875
|
+
return JSON.stringify(cacheKeyObj);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
return `${ttsRequest.url}|${ttsRequest.method}|${lodash.get(ttsRequest, 'params.text', '')}`;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async _fetchTts(ttsRequest, text) {
|
|
881
|
+
const ttsStart = Date.now();
|
|
882
|
+
const ttsResponse = await axios(ttsRequest);
|
|
883
|
+
const ttsMs = Date.now() - ttsStart;
|
|
884
|
+
const textLen = lodash.isString(text) ? text.length : 0;
|
|
885
|
+
debug$3(`TTS response ${ttsMs} ms (chars: ${textLen})`);
|
|
886
|
+
if (!ttsResponse || !Buffer.isBuffer(ttsResponse.data)) {
|
|
887
|
+
throw new Error(`TTS failed, response is: ${this._getAxiosShortenedOutput(ttsResponse && ttsResponse.data)}`);
|
|
888
|
+
}
|
|
889
|
+
const durationHeader = ttsResponse.headers && ttsResponse.headers['content-duration'];
|
|
890
|
+
return {
|
|
891
|
+
buffer: ttsResponse.data,
|
|
892
|
+
duration: durationHeader
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
async _getTtsAudio(ttsRequest, text) {
|
|
896
|
+
if (!ttsRequest) throw new Error('TTS request not configured');
|
|
897
|
+
const cacheKey = this._getTtsCacheKey(ttsRequest);
|
|
898
|
+
if (this._isTtsCacheEnabled()) {
|
|
899
|
+
const cached = this.ttsCache.get(cacheKey);
|
|
900
|
+
if (cached) {
|
|
901
|
+
if (cached.state === 'ready') {
|
|
902
|
+
this._touchTtsCache(cacheKey, cached);
|
|
903
|
+
debug$3(`TTS cache hit (chars: ${lodash.isString(text) ? text.length : 0})`);
|
|
904
|
+
return {
|
|
905
|
+
buffer: cached.buffer,
|
|
906
|
+
duration: cached.duration
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (cached.state === 'pending') {
|
|
910
|
+
return cached.promise;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const fetchPromise = this._fetchTts(ttsRequest, text).then(result => {
|
|
915
|
+
if (this._isTtsCacheEnabled()) {
|
|
916
|
+
this._setTtsCacheEntry(cacheKey, {
|
|
917
|
+
state: 'ready',
|
|
918
|
+
buffer: result.buffer,
|
|
919
|
+
duration: result.duration
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return result;
|
|
923
|
+
}).catch(err => {
|
|
924
|
+
this._deleteTtsCacheEntry(cacheKey);
|
|
925
|
+
throw err;
|
|
926
|
+
});
|
|
927
|
+
if (this._isTtsCacheEnabled()) {
|
|
928
|
+
this._setTtsCacheEntry(cacheKey, {
|
|
929
|
+
state: 'pending',
|
|
930
|
+
promise: fetchPromise
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return fetchPromise;
|
|
934
|
+
}
|
|
565
935
|
_getParams(capParams) {
|
|
566
936
|
if (this.caps[capParams]) {
|
|
567
937
|
if (lodash.isString(this.caps[capParams])) return JSON.parse(this.caps[capParams]);else return this.caps[capParams];
|