@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.3
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.
- package/.sdd/manifest.json +5 -4
- package/ai-docs/ARCHITECTURE.md +1 -1
- package/ai-docs/CONTRACTS.md +5 -1
- package/ai-docs/SECURITY.md +1 -1
- package/ai-docs/contact-center-spec.md +22 -4
- package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
- package/dist/cc.js +440 -10
- package/dist/cc.js.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +10 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/behavioral-events.js +13 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +2 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AddressBook.js +18 -15
- package/dist/services/AddressBook.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +174 -0
- package/dist/services/AnswerCallOnWebexService.js.map +1 -0
- package/dist/services/EntryPoint.js +46 -68
- package/dist/services/EntryPoint.js.map +1 -1
- package/dist/services/Queue.js +27 -22
- package/dist/services/Queue.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +171 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +93 -0
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
- package/dist/services/config/Util.js +3 -0
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +10 -6
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/types.js +4 -0
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/WebexRequest.js +6 -2
- package/dist/services/core/WebexRequest.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +28 -14
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/types.js.map +1 -1
- package/dist/services/task/Task.js +73 -7
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskFactory.js +8 -4
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +176 -20
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/WebexCallingUtils.js +70 -0
- package/dist/services/task/WebexCallingUtils.js.map +1 -0
- package/dist/services/task/constants.js +4 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/contact.js +29 -0
- package/dist/services/task/contact.js.map +1 -1
- package/dist/services/task/digital/Digital.js +3 -2
- package/dist/services/task/digital/Digital.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +6 -1
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +38 -11
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/types.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/types.js +16 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +350 -14
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/WebRTC.js +4 -1
- package/dist/services/task/voice/WebRTC.js.map +1 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
- package/dist/services/wxAppTelephonyUtils.js +19 -0
- package/dist/services/wxAppTelephonyUtils.js.map +1 -0
- package/dist/types/cc.d.ts +77 -4
- package/dist/types/config.d.ts +7 -0
- package/dist/types/constants.d.ts +8 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/metrics/constants.d.ts +2 -0
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
- package/dist/types/services/EntryPoint.d.ts +7 -5
- package/dist/types/services/Queue.d.ts +5 -3
- package/dist/types/services/WebexCrossClientService.d.ts +28 -0
- package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
- package/dist/types/services/config/constants.d.ts +8 -5
- package/dist/types/services/config/types.d.ts +17 -4
- package/dist/types/services/core/Err.d.ts +2 -0
- package/dist/types/services/core/WebexRequest.d.ts +1 -0
- package/dist/types/services/core/types.d.ts +2 -0
- package/dist/types/services/task/Task.d.ts +20 -2
- package/dist/types/services/task/TaskFactory.d.ts +2 -1
- package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
- package/dist/types/services/task/constants.d.ts +3 -0
- package/dist/types/services/task/contact.d.ts +4 -0
- package/dist/types/services/task/digital/Digital.d.ts +2 -2
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
- package/dist/types/services/task/state-machine/guards.d.ts +6 -5
- package/dist/types/services/task/state-machine/types.d.ts +9 -1
- package/dist/types/services/task/types.d.ts +81 -4
- package/dist/types/services/task/voice/Voice.d.ts +48 -9
- package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
- package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
- package/dist/types/types.d.ts +14 -8
- package/dist/types/utils/PageCache.d.ts +20 -3
- package/dist/types.js +7 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/PageCache.js +19 -5
- package/dist/utils/PageCache.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/cc.ts +506 -9
- package/src/config.ts +7 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +2 -0
- package/src/metrics/ai-docs/metrics-spec.md +9 -3
- package/src/metrics/behavioral-events.ts +14 -0
- package/src/metrics/constants.ts +2 -0
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +206 -0
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/WebexCrossClientService.ts +212 -0
- package/src/services/WxAppTelephonyMercurySync.ts +115 -0
- package/src/services/ai-docs/AGENTS.md +10 -10
- package/src/services/ai-docs/services-spec.md +6 -1
- package/src/services/config/Util.ts +3 -0
- package/src/services/config/ai-docs/AGENTS.md +1 -1
- package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
- package/src/services/config/ai-docs/config-spec.md +6 -0
- package/src/services/config/constants.ts +10 -6
- package/src/services/config/types.ts +16 -4
- package/src/services/core/Err.ts +1 -0
- package/src/services/core/WebexRequest.ts +3 -1
- package/src/services/core/ai-docs/core-spec.md +5 -1
- package/src/services/core/aqm-reqs.ts +30 -15
- package/src/services/core/types.ts +2 -0
- package/src/services/task/Task.ts +79 -8
- package/src/services/task/TaskFactory.ts +8 -3
- package/src/services/task/TaskManager.ts +254 -15
- package/src/services/task/WebexCallingUtils.ts +136 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
- package/src/services/task/ai-docs/task-spec.md +117 -2
- package/src/services/task/constants.ts +3 -0
- package/src/services/task/contact.ts +30 -0
- package/src/services/task/digital/Digital.ts +4 -2
- package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
- package/src/services/task/state-machine/actions.ts +5 -2
- package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +46 -22
- package/src/services/task/state-machine/guards.ts +64 -11
- package/src/services/task/state-machine/types.ts +16 -1
- package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
- package/src/services/task/types.ts +114 -4
- package/src/services/task/voice/Voice.ts +462 -12
- package/src/services/task/voice/WebRTC.ts +5 -1
- package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
- package/src/services/wxAppTelephonyUtils.ts +14 -0
- package/src/types.ts +33 -9
- package/src/utils/AGENTS.md +21 -10
- package/src/utils/PageCache.ts +38 -5
- package/src/utils/ai-docs/utils-spec.md +21 -11
- package/test/unit/spec/cc.ts +1274 -0
- package/test/unit/spec/metrics/behavioral-events.ts +18 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
- package/test/unit/spec/services/EntryPoint.ts +87 -40
- package/test/unit/spec/services/Queue.ts +123 -12
- package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
- package/test/unit/spec/services/config/Util.ts +85 -0
- package/test/unit/spec/services/core/WebexRequest.ts +3 -1
- package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
- package/test/unit/spec/services/task/Task.ts +200 -0
- package/test/unit/spec/services/task/TaskFactory.ts +39 -2
- package/test/unit/spec/services/task/TaskManager.ts +653 -1
- package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
- package/test/unit/spec/services/task/contact.ts +33 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
- package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
- package/test/unit/spec/services/task/voice/Voice.ts +874 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
shouldAutoAnswerTask,
|
|
28
28
|
} from './TaskUtils';
|
|
29
29
|
import TaskFactory from './TaskFactory';
|
|
30
|
+
import AnswerCallOnWebexService from '../AnswerCallOnWebexService';
|
|
31
|
+
import {getWebexCallingDeviceDetailsForAgent} from './WebexCallingUtils';
|
|
30
32
|
import WebRTC from './voice/WebRTC';
|
|
31
33
|
import {TaskEvent, type TaskEventPayload} from './state-machine';
|
|
32
34
|
import {normalizeTaskData} from './taskDataNormalizer';
|
|
@@ -34,6 +36,11 @@ import {ApiAIAssistant} from '../ApiAiAssistant';
|
|
|
34
36
|
|
|
35
37
|
const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
|
|
36
38
|
|
|
39
|
+
const MAIN_INTERACTION_CORRELATED_EVENTS = new Set<CC_EVENTS>([
|
|
40
|
+
CC_EVENTS.AGENT_CONSULT_ENDED,
|
|
41
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
42
|
+
]);
|
|
43
|
+
|
|
37
44
|
const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
|
|
38
45
|
|
|
39
46
|
/** @internal */
|
|
@@ -55,6 +62,7 @@ export default class TaskManager extends EventEmitter {
|
|
|
55
62
|
private wrapupData: WrapupData;
|
|
56
63
|
private agentId: string;
|
|
57
64
|
private webRtcEnabled: boolean;
|
|
65
|
+
private answerCallOnWebexService?: AnswerCallOnWebexService;
|
|
58
66
|
private apiAIAssistant?: ApiAIAssistant;
|
|
59
67
|
/**
|
|
60
68
|
* @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
|
|
@@ -148,6 +156,10 @@ export default class TaskManager extends EventEmitter {
|
|
|
148
156
|
this.webRtcEnabled = webRtcEnabled;
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
public setAnswerCallOnWebexService(answerCallOnWebexService: AnswerCallOnWebexService) {
|
|
160
|
+
this.answerCallOnWebexService = answerCallOnWebexService;
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
private handleIncomingWebCall = (call: ICall) => {
|
|
152
164
|
const currentTask = Object.values(this.taskCollection).find(
|
|
153
165
|
(task) =>
|
|
@@ -187,12 +199,15 @@ export default class TaskManager extends EventEmitter {
|
|
|
187
199
|
* @param ccEvent - The CC_EVENT type from WebSocket
|
|
188
200
|
* @param payload - The event payload
|
|
189
201
|
* @param agentId - Optional agent ID for state detection (needed for HYDRATE)
|
|
202
|
+
* @param task - Optional task instance for state-aware CONTACT_ENDED mapping
|
|
190
203
|
* @returns TaskEventPayload for state machine or null if no mapping
|
|
191
204
|
*/
|
|
192
205
|
private static mapEventToTaskStateMachineEvent(
|
|
193
206
|
ccEvent: CC_EVENTS,
|
|
194
207
|
payload: WebSocketPayload,
|
|
195
|
-
agentId?: string
|
|
208
|
+
agentId?: string,
|
|
209
|
+
task?: ITask,
|
|
210
|
+
enableWxBetterTogether?: boolean
|
|
196
211
|
): TaskEventPayload | null {
|
|
197
212
|
const mediaResourceId =
|
|
198
213
|
payload.mediaResourceId ||
|
|
@@ -299,16 +314,39 @@ export default class TaskManager extends EventEmitter {
|
|
|
299
314
|
case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
|
|
300
315
|
return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
|
|
301
316
|
|
|
302
|
-
case CC_EVENTS.CONTACT_ENDED:
|
|
317
|
+
case CC_EVENTS.CONTACT_ENDED: {
|
|
318
|
+
const wrapUpRequired = isCampaignPreviewTask(payload)
|
|
319
|
+
? false
|
|
320
|
+
: payload.agentsPendingWrapUp?.includes(agentId || '') || false;
|
|
321
|
+
|
|
322
|
+
// Post-accept wxApp outdial agent-end delivers ContactEnded instead of
|
|
323
|
+
// AgentOutboundFailed. Normalize to OUTBOUND_FAILED so state machine
|
|
324
|
+
// enters WRAPPING_UP and emits TASK_OUTDIAL_FAILED (AGENT_ENDS).
|
|
325
|
+
// Pre-accept cancelTask decline stays CONTACT_ENDED → TERMINATED (no wrapup).
|
|
326
|
+
if (
|
|
327
|
+
TaskManager.isAgentTerminatedOutdialWrapup(
|
|
328
|
+
payload,
|
|
329
|
+
wrapUpRequired,
|
|
330
|
+
task,
|
|
331
|
+
agentId,
|
|
332
|
+
enableWxBetterTogether
|
|
333
|
+
)
|
|
334
|
+
) {
|
|
335
|
+
return {
|
|
336
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
337
|
+
reason: 'AGENT_ENDS',
|
|
338
|
+
taskData: {...payload, wrapUpRequired: true},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
303
342
|
return {
|
|
304
343
|
type: TaskEvent.CONTACT_ENDED,
|
|
305
344
|
taskData: {
|
|
306
345
|
...payload,
|
|
307
|
-
wrapUpRequired
|
|
308
|
-
? false
|
|
309
|
-
: payload.agentsPendingWrapUp?.includes(agentId || '') || false,
|
|
346
|
+
wrapUpRequired,
|
|
310
347
|
},
|
|
311
348
|
};
|
|
349
|
+
}
|
|
312
350
|
|
|
313
351
|
case CC_EVENTS.AGENT_INVITE_FAILED:
|
|
314
352
|
return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
|
|
@@ -319,8 +357,23 @@ export default class TaskManager extends EventEmitter {
|
|
|
319
357
|
case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
|
|
320
358
|
return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
|
|
321
359
|
|
|
322
|
-
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
|
|
323
|
-
|
|
360
|
+
case CC_EVENTS.AGENT_OUTBOUND_FAILED: {
|
|
361
|
+
const isAgentTerminatedOutdial =
|
|
362
|
+
payload.interaction?.outboundType === 'OUTDIAL' &&
|
|
363
|
+
(payload.terminatingParty === 'Agent' || payload.reason === 'AGENT_ENDS');
|
|
364
|
+
|
|
365
|
+
const suppressOutdialFailedPopup =
|
|
366
|
+
isAgentTerminatedOutdial &&
|
|
367
|
+
!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
371
|
+
taskData: suppressOutdialFailedPopup
|
|
372
|
+
? {...payload, suppressOutdialFailedPopup: true}
|
|
373
|
+
: payload,
|
|
374
|
+
reason: payload.reason,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
324
377
|
|
|
325
378
|
case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
|
|
326
379
|
return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
|
|
@@ -371,6 +424,68 @@ export default class TaskManager extends EventEmitter {
|
|
|
371
424
|
}
|
|
372
425
|
}
|
|
373
426
|
|
|
427
|
+
/**
|
|
428
|
+
* True when the current agent's task is a wxApp-managed outdial (thick-client telephony).
|
|
429
|
+
*/
|
|
430
|
+
private static isWxAppManagedOutdialTask(
|
|
431
|
+
agentId: string | undefined,
|
|
432
|
+
payload: WebSocketPayload,
|
|
433
|
+
task: ITask | undefined,
|
|
434
|
+
enableWxBetterTogether?: boolean
|
|
435
|
+
): boolean {
|
|
436
|
+
const wxAppEnabled =
|
|
437
|
+
(task as {uiControlConfig?: {enableWxBetterTogether?: boolean}})?.uiControlConfig
|
|
438
|
+
?.enableWxBetterTogether ??
|
|
439
|
+
enableWxBetterTogether ??
|
|
440
|
+
false;
|
|
441
|
+
|
|
442
|
+
if (!wxAppEnabled) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const effectiveAgentId = agentId ?? task?.data?.agentId;
|
|
447
|
+
const participants = payload.interaction?.participants ?? task?.data?.interaction?.participants;
|
|
448
|
+
const details = getWebexCallingDeviceDetailsForAgent(effectiveAgentId, participants);
|
|
449
|
+
|
|
450
|
+
return details?.deviceType === 'wxApp';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* True when ContactEnded represents an agent-terminated wxApp outdial that should
|
|
455
|
+
* enter wrapup with an outdial-failed popup (legacy AgentOutboundFailed parity).
|
|
456
|
+
*/
|
|
457
|
+
private static isAgentTerminatedOutdialWrapup(
|
|
458
|
+
payload: WebSocketPayload,
|
|
459
|
+
wrapUpRequired: boolean,
|
|
460
|
+
task?: ITask,
|
|
461
|
+
agentId?: string,
|
|
462
|
+
enableWxBetterTogether?: boolean
|
|
463
|
+
): boolean {
|
|
464
|
+
const isOutdial = payload.interaction?.outboundType === 'OUTDIAL';
|
|
465
|
+
const agentTerminated = payload.terminatingParty === 'Agent';
|
|
466
|
+
|
|
467
|
+
if (!isOutdial || !agentTerminated) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const wxAppAnswerPending =
|
|
476
|
+
(task as {uiControlConfig?: {wxAppAnswerPending?: boolean}})?.uiControlConfig
|
|
477
|
+
?.wxAppAnswerPending ?? false;
|
|
478
|
+
|
|
479
|
+
// Pre-accept decline: decline() clears wxAppAnswerPending; backend may still send state wrapUp.
|
|
480
|
+
if (!wxAppAnswerPending && !wrapUpRequired) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const interactionWrapUp = payload.interaction?.state === 'wrapUp';
|
|
485
|
+
|
|
486
|
+
return wrapUpRequired || interactionWrapUp || wxAppAnswerPending;
|
|
487
|
+
}
|
|
488
|
+
|
|
374
489
|
/**
|
|
375
490
|
* Register WebSocket message listeners for task events
|
|
376
491
|
*
|
|
@@ -482,6 +597,10 @@ export default class TaskManager extends EventEmitter {
|
|
|
482
597
|
}
|
|
483
598
|
}
|
|
484
599
|
|
|
600
|
+
if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
|
|
601
|
+
task = this.findUniqueTaskByRelatedInteraction(message.data);
|
|
602
|
+
}
|
|
603
|
+
|
|
485
604
|
const wasConsultedTask = Boolean(task?.data?.isConsulted);
|
|
486
605
|
const computeWrapUpRequired = () => {
|
|
487
606
|
if (message.data.wrapUpRequired !== undefined) {
|
|
@@ -507,7 +626,9 @@ export default class TaskManager extends EventEmitter {
|
|
|
507
626
|
const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
|
|
508
627
|
eventType,
|
|
509
628
|
adjustedPayload,
|
|
510
|
-
this.agentId
|
|
629
|
+
this.agentId,
|
|
630
|
+
task,
|
|
631
|
+
this.configFlags?.enableWxBetterTogether
|
|
511
632
|
);
|
|
512
633
|
|
|
513
634
|
LoggerProxy.info(`Handling task event ${eventType}`, {
|
|
@@ -524,6 +645,62 @@ export default class TaskManager extends EventEmitter {
|
|
|
524
645
|
};
|
|
525
646
|
}
|
|
526
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Resolve lifecycle events that are emitted for the main interaction while the
|
|
650
|
+
* local task can still be indexed by a child consult interaction. Exact task
|
|
651
|
+
* keys are handled before this fallback. Multiple aliases of the same task are
|
|
652
|
+
* treated as one candidate; genuinely ambiguous matches are intentionally ignored.
|
|
653
|
+
*/
|
|
654
|
+
private findUniqueTaskByRelatedInteraction(payload: WebSocketPayload): ITask | undefined {
|
|
655
|
+
const payloadInteractionIds = new Set(
|
|
656
|
+
[
|
|
657
|
+
payload.interactionId,
|
|
658
|
+
payload.interaction?.interactionId,
|
|
659
|
+
payload.interaction?.mainInteractionId,
|
|
660
|
+
payload.interaction?.parentInteractionId,
|
|
661
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId,
|
|
662
|
+
].filter((interactionId): interactionId is string => Boolean(interactionId))
|
|
663
|
+
);
|
|
664
|
+
if (payloadInteractionIds.size === 0) return undefined;
|
|
665
|
+
|
|
666
|
+
const candidates = [
|
|
667
|
+
...new Set(
|
|
668
|
+
Object.values(this.taskCollection).filter((candidate) => {
|
|
669
|
+
const taskInteraction = candidate?.data?.interaction;
|
|
670
|
+
const candidateInteractionIds = [
|
|
671
|
+
candidate?.data?.interactionId,
|
|
672
|
+
taskInteraction?.interactionId,
|
|
673
|
+
taskInteraction?.mainInteractionId,
|
|
674
|
+
taskInteraction?.parentInteractionId,
|
|
675
|
+
taskInteraction?.callProcessingDetails?.parentInteractionId,
|
|
676
|
+
];
|
|
677
|
+
|
|
678
|
+
return candidateInteractionIds.some(
|
|
679
|
+
(interactionId) => Boolean(interactionId) && payloadInteractionIds.has(interactionId)
|
|
680
|
+
);
|
|
681
|
+
})
|
|
682
|
+
),
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
if (candidates.length === 1) {
|
|
686
|
+
return candidates[0];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (candidates.length > 1) {
|
|
690
|
+
LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
|
|
691
|
+
module: TASK_MANAGER_FILE,
|
|
692
|
+
method: 'findUniqueTaskByRelatedInteraction',
|
|
693
|
+
interactionId:
|
|
694
|
+
payload.interaction?.mainInteractionId ||
|
|
695
|
+
payload.interaction?.parentInteractionId ||
|
|
696
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId ||
|
|
697
|
+
payload.interactionId,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
|
|
527
704
|
/**
|
|
528
705
|
* Handle task lifecycle events and determine required actions
|
|
529
706
|
*
|
|
@@ -647,7 +824,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
647
824
|
},
|
|
648
825
|
this.configFlags,
|
|
649
826
|
this.wrapupData,
|
|
650
|
-
this.agentId
|
|
827
|
+
this.agentId,
|
|
828
|
+
this.answerCallOnWebexService
|
|
651
829
|
);
|
|
652
830
|
this.setupTaskListeners(task);
|
|
653
831
|
this.taskCollection[payload.interactionId] = task;
|
|
@@ -685,7 +863,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
685
863
|
taskData,
|
|
686
864
|
this.configFlags,
|
|
687
865
|
this.wrapupData,
|
|
688
|
-
this.agentId
|
|
866
|
+
this.agentId,
|
|
867
|
+
this.answerCallOnWebexService
|
|
689
868
|
);
|
|
690
869
|
|
|
691
870
|
this.setupTaskListeners(task);
|
|
@@ -725,7 +904,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
725
904
|
taskData,
|
|
726
905
|
this.configFlags,
|
|
727
906
|
this.wrapupData,
|
|
728
|
-
this.agentId
|
|
907
|
+
this.agentId,
|
|
908
|
+
this.answerCallOnWebexService
|
|
729
909
|
);
|
|
730
910
|
this.setupTaskListeners(task);
|
|
731
911
|
this.taskCollection[payload.interactionId] = task;
|
|
@@ -753,6 +933,14 @@ export default class TaskManager extends EventEmitter {
|
|
|
753
933
|
: taskData;
|
|
754
934
|
|
|
755
935
|
task.updateTaskData(updateTaskData);
|
|
936
|
+
|
|
937
|
+
// A main-interaction lifecycle event can re-key a child consult task. Remove
|
|
938
|
+
// any aliases first so consumers never observe duplicate task entries.
|
|
939
|
+
Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
|
|
940
|
+
if (candidate === task && taskId !== taskData.interactionId) {
|
|
941
|
+
delete this.taskCollection[taskId];
|
|
942
|
+
}
|
|
943
|
+
});
|
|
756
944
|
this.taskCollection[taskData.interactionId] = task;
|
|
757
945
|
|
|
758
946
|
return task;
|
|
@@ -798,8 +986,10 @@ export default class TaskManager extends EventEmitter {
|
|
|
798
986
|
task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
|
|
799
987
|
this.handleTaskCleanup(t);
|
|
800
988
|
if (options?.removeFromCollection) {
|
|
801
|
-
const
|
|
802
|
-
|
|
989
|
+
const taskIsStillCollected = Object.values(this.taskCollection).some(
|
|
990
|
+
(candidate) => candidate === t
|
|
991
|
+
);
|
|
992
|
+
if (taskIsStillCollected) {
|
|
803
993
|
this.removeTaskFromCollection(t);
|
|
804
994
|
}
|
|
805
995
|
}
|
|
@@ -811,7 +1001,11 @@ export default class TaskManager extends EventEmitter {
|
|
|
811
1001
|
task.cancelAutoWrapupTimer();
|
|
812
1002
|
}
|
|
813
1003
|
if (task?.data?.interactionId) {
|
|
814
|
-
|
|
1004
|
+
Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
|
|
1005
|
+
if (candidate === task) {
|
|
1006
|
+
delete this.taskCollection[taskId];
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
815
1009
|
LoggerProxy.info(`Task removed from collection`, {
|
|
816
1010
|
module: TASK_MANAGER_FILE,
|
|
817
1011
|
method: METHODS.REMOVE_TASK_FROM_COLLECTION,
|
|
@@ -864,7 +1058,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
864
1058
|
taskData,
|
|
865
1059
|
this.configFlags,
|
|
866
1060
|
this.wrapupData,
|
|
867
|
-
this.agentId
|
|
1061
|
+
this.agentId,
|
|
1062
|
+
this.answerCallOnWebexService
|
|
868
1063
|
);
|
|
869
1064
|
this.taskCollection[payload.interactionId] = task;
|
|
870
1065
|
|
|
@@ -956,6 +1151,50 @@ export default class TaskManager extends EventEmitter {
|
|
|
956
1151
|
return {...this.taskCollection};
|
|
957
1152
|
}
|
|
958
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Propagate runtime enableWxBetterTogether changes to config flags and active tasks.
|
|
1156
|
+
*/
|
|
1157
|
+
public applyEnableWxBetterTogether(enabled: boolean): void {
|
|
1158
|
+
if (this.configFlags) {
|
|
1159
|
+
this.configFlags = {...this.configFlags, enableWxBetterTogether: enabled};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1163
|
+
task.setEnableWxBetterTogether(enabled);
|
|
1164
|
+
if (enabled) {
|
|
1165
|
+
TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Propagate wxApp mute state from Mercury sync to active voice tasks.
|
|
1172
|
+
*/
|
|
1173
|
+
public applyWxAppMuteStateFromSync(callId: string, muted: boolean): void {
|
|
1174
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1175
|
+
task.applyWxAppMuteStateFromSync(callId, muted);
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Re-seeds wxApp mute state from GET telephony/calls/{callId} for one task.
|
|
1181
|
+
*/
|
|
1182
|
+
public static syncWxAppMuteFromCallDetailsForTask(task: ITask): void {
|
|
1183
|
+
const sync = task.syncWxAppMuteFromCallDetails?.bind(task);
|
|
1184
|
+
if (typeof sync === 'function') {
|
|
1185
|
+
sync().catch(() => undefined);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Re-seeds wxApp mute for all engaged voice tasks (reload / runtime enable backfill).
|
|
1191
|
+
*/
|
|
1192
|
+
public syncWxAppMuteFromCallDetailsForAllTasks(): void {
|
|
1193
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1194
|
+
TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
959
1198
|
public static getTaskManager(
|
|
960
1199
|
apiAIAssistant: ApiAIAssistant,
|
|
961
1200
|
contact: ReturnType<typeof routingContact>,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {TaskState} from './state-machine/constants';
|
|
2
|
+
|
|
3
|
+
export type WebexCallingDeviceDetails = {
|
|
4
|
+
deviceType: string;
|
|
5
|
+
deviceId: string;
|
|
6
|
+
deviceCallId: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type WebexCallingParticipant = {
|
|
10
|
+
id?: unknown;
|
|
11
|
+
deviceType?: unknown;
|
|
12
|
+
deviceCallId?: unknown;
|
|
13
|
+
deviceId?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const normalizeId = (id: unknown): string | undefined => {
|
|
17
|
+
if (typeof id !== 'string') {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalizedId = id.trim();
|
|
22
|
+
|
|
23
|
+
return normalizedId === '' ? undefined : normalizedId;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const isParticipantsById = (participants: unknown): participants is Record<string, unknown> =>
|
|
27
|
+
Boolean(participants) &&
|
|
28
|
+
typeof participants === 'object' &&
|
|
29
|
+
!Array.isArray(participants) &&
|
|
30
|
+
!(participants instanceof Map);
|
|
31
|
+
|
|
32
|
+
const hasWebexCallingDeviceDetails = (
|
|
33
|
+
participant: unknown
|
|
34
|
+
): participant is WebexCallingParticipant &
|
|
35
|
+
Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'> => {
|
|
36
|
+
const participantDetails = participant as WebexCallingParticipant | undefined;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
typeof participantDetails?.deviceCallId === 'string' &&
|
|
40
|
+
participantDetails.deviceCallId.trim() !== '' &&
|
|
41
|
+
typeof participantDetails?.deviceId === 'string' &&
|
|
42
|
+
participantDetails.deviceId.trim() !== ''
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const isWebexCallingParticipantForAgent = (
|
|
47
|
+
participant: unknown,
|
|
48
|
+
agentId: string
|
|
49
|
+
): participant is WebexCallingParticipant &
|
|
50
|
+
Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'> => {
|
|
51
|
+
const participantDetails = participant as WebexCallingParticipant | undefined;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
normalizeId(participantDetails?.id) === agentId && hasWebexCallingDeviceDetails(participant)
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getWebexCallingDeviceDetails = (
|
|
59
|
+
participant: WebexCallingParticipant &
|
|
60
|
+
Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'>
|
|
61
|
+
): WebexCallingDeviceDetails => ({
|
|
62
|
+
deviceType: typeof participant.deviceType === 'string' ? participant.deviceType : '',
|
|
63
|
+
deviceId: participant.deviceId,
|
|
64
|
+
deviceCallId: participant.deviceCallId,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function getWebexCallingDeviceDetailsForAgent(
|
|
68
|
+
agentId: string | undefined,
|
|
69
|
+
participants: unknown
|
|
70
|
+
): WebexCallingDeviceDetails | undefined {
|
|
71
|
+
const normalizedAgentId = normalizeId(agentId);
|
|
72
|
+
if (!normalizedAgentId || !isParticipantsById(participants)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const agentParticipant = participants[normalizedAgentId];
|
|
77
|
+
const matchingAgentParticipant = hasWebexCallingDeviceDetails(agentParticipant)
|
|
78
|
+
? agentParticipant
|
|
79
|
+
: Object.values(participants).find((participant) =>
|
|
80
|
+
isWebexCallingParticipantForAgent(participant, normalizedAgentId)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!matchingAgentParticipant || !hasWebexCallingDeviceDetails(matchingAgentParticipant)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return getWebexCallingDeviceDetails(matchingAgentParticipant);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isWebexCallingCallForAgent(
|
|
91
|
+
agentId: string | undefined,
|
|
92
|
+
participants: unknown
|
|
93
|
+
): boolean {
|
|
94
|
+
return Boolean(getWebexCallingDeviceDetailsForAgent(agentId, participants));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Matches wxAppVoiceMethods.getWebexCallingCallId engaged semantics for uiControls. */
|
|
98
|
+
export function isWxAppEngagedForControls(
|
|
99
|
+
enableWxBetterTogether: boolean,
|
|
100
|
+
agentId: string | undefined,
|
|
101
|
+
participants: unknown,
|
|
102
|
+
state: TaskState
|
|
103
|
+
): boolean {
|
|
104
|
+
if (!enableWxBetterTogether) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
!state ||
|
|
110
|
+
state === TaskState.OFFERED ||
|
|
111
|
+
state === TaskState.IDLE ||
|
|
112
|
+
state === TaskState.WRAPPING_UP ||
|
|
113
|
+
state === TaskState.TERMINATED ||
|
|
114
|
+
state === TaskState.COMPLETED
|
|
115
|
+
) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const details = getWebexCallingDeviceDetailsForAgent(agentId, participants);
|
|
120
|
+
|
|
121
|
+
return details?.deviceType === 'wxApp';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function decodedLineOwnerId(lineOwnerId?: string): string | undefined {
|
|
125
|
+
if (!lineOwnerId) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const decoded = atob(lineOwnerId);
|
|
131
|
+
|
|
132
|
+
return decoded.split('/').pop();
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
- `state?: SnapshotFrom<TaskStateMachine>`
|
|
36
36
|
- `autoWrapup?: AutoWrapup`
|
|
37
37
|
- `uiControls: TaskUIControls` (getter)
|
|
38
|
+
- includes ordered, action-specific `consultTransferDestinations` computed from Task profile configuration and live interaction data
|
|
38
39
|
|
|
39
40
|
**Methods**
|
|
40
41
|
|