@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.11
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 +7 -5
- package/ai-docs/ARCHITECTURE.md +1 -1
- package/ai-docs/CONTRACTS.md +8 -3
- 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 +544 -12
- 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/MetricsManager.js +16 -0
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +104 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +18 -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 +202 -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 +222 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +108 -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 +490 -21
- 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 +162 -53
- 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 +7 -10
- 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 +456 -15
- 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 +354 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
- package/dist/services/wxAppDiagnosticLogging.js +122 -0
- package/dist/services/wxAppDiagnosticLogging.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 +78 -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/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +17 -0
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -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 +32 -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 +304 -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 +52 -9
- package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -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 +8 -8
- package/src/cc.ts +657 -12
- package/src/config.ts +7 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +2 -0
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/metrics-spec.md +78 -10
- package/src/metrics/behavioral-events.ts +106 -0
- package/src/metrics/constants.ts +20 -0
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +239 -0
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/WebexCrossClientService.ts +293 -0
- package/src/services/WxAppTelephonyMercurySync.ts +142 -0
- package/src/services/agent/ai-docs/agent-spec.md +1 -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 +716 -17
- 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 +156 -3
- 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 +235 -81
- package/src/services/task/state-machine/actions.ts +5 -2
- package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
- package/src/services/task/state-machine/guards.ts +7 -10
- 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 +595 -14
- package/src/services/task/voice/WebRTC.ts +5 -1
- package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
- package/src/services/wxAppDiagnosticLogging.ts +174 -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 +1522 -19
- package/test/unit/spec/metrics/MetricsManager.ts +25 -0
- package/test/unit/spec/metrics/behavioral-events.ts +60 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -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 +467 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -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 +274 -6
- package/test/unit/spec/services/task/TaskFactory.ts +39 -2
- package/test/unit/spec/services/task/TaskManager.ts +1717 -9
- 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 +1485 -109
- 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 +1412 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -27,15 +27,38 @@ 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
|
-
import {TaskEvent, type TaskEventPayload} from './state-machine';
|
|
33
|
+
import {TaskEvent, TaskState, type TaskEventPayload} from './state-machine';
|
|
34
|
+
import {MEDIA_TYPE_MAIN_CALL} from './state-machine/constants';
|
|
32
35
|
import {normalizeTaskData} from './taskDataNormalizer';
|
|
33
36
|
import {ApiAIAssistant} from '../ApiAiAssistant';
|
|
34
37
|
|
|
35
38
|
const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
|
|
36
39
|
|
|
40
|
+
const MAIN_INTERACTION_CORRELATED_EVENTS = new Set<CC_EVENTS>([
|
|
41
|
+
CC_EVENTS.AGENT_CONSULT_ENDED,
|
|
42
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
43
|
+
]);
|
|
44
|
+
|
|
37
45
|
const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
|
|
38
46
|
|
|
47
|
+
const WRAP_UP_STAMP_EVENTS = new Set<CC_EVENTS>([
|
|
48
|
+
CC_EVENTS.AGENT_WRAPUP,
|
|
49
|
+
CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED,
|
|
50
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
51
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const PARTICIPANT_LEAVE_WRAP_STATES = new Set<TaskState>([
|
|
55
|
+
TaskState.HELD,
|
|
56
|
+
TaskState.RESUME_INITIATING,
|
|
57
|
+
TaskState.CONSULTING,
|
|
58
|
+
TaskState.CONSULT_INITIATING,
|
|
59
|
+
TaskState.CONFERENCING,
|
|
60
|
+
]);
|
|
61
|
+
|
|
39
62
|
/** @internal */
|
|
40
63
|
export default class TaskManager extends EventEmitter {
|
|
41
64
|
private call: ICall;
|
|
@@ -55,6 +78,7 @@ export default class TaskManager extends EventEmitter {
|
|
|
55
78
|
private wrapupData: WrapupData;
|
|
56
79
|
private agentId: string;
|
|
57
80
|
private webRtcEnabled: boolean;
|
|
81
|
+
private answerCallOnWebexService?: AnswerCallOnWebexService;
|
|
58
82
|
private apiAIAssistant?: ApiAIAssistant;
|
|
59
83
|
/**
|
|
60
84
|
* @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
|
|
@@ -148,6 +172,10 @@ export default class TaskManager extends EventEmitter {
|
|
|
148
172
|
this.webRtcEnabled = webRtcEnabled;
|
|
149
173
|
}
|
|
150
174
|
|
|
175
|
+
public setAnswerCallOnWebexService(answerCallOnWebexService: AnswerCallOnWebexService) {
|
|
176
|
+
this.answerCallOnWebexService = answerCallOnWebexService;
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
private handleIncomingWebCall = (call: ICall) => {
|
|
152
180
|
const currentTask = Object.values(this.taskCollection).find(
|
|
153
181
|
(task) =>
|
|
@@ -182,17 +210,189 @@ export default class TaskManager extends EventEmitter {
|
|
|
182
210
|
this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
|
|
183
211
|
}
|
|
184
212
|
|
|
213
|
+
/**
|
|
214
|
+
* True when this agent left on the current payload.
|
|
215
|
+
* A named other participantId is never self-leave (even if the participants
|
|
216
|
+
* map is partial). Missing-map fallback is EP-DN only: participantId absent
|
|
217
|
+
* and self omitted now, with previous-task evidence when a prior map exists.
|
|
218
|
+
*/
|
|
219
|
+
private static didSelfLeaveConference(
|
|
220
|
+
payload: WebSocketPayload,
|
|
221
|
+
agentId?: string,
|
|
222
|
+
task?: ITask
|
|
223
|
+
): boolean {
|
|
224
|
+
if (!agentId) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (payload.participantId === agentId) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (payload.participantId && payload.participantId !== agentId) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const participants = payload.interaction?.participants;
|
|
235
|
+
if (!participants || agentId in participants) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const previousParticipants = task?.data?.interaction?.participants;
|
|
240
|
+
if (previousParticipants && !(agentId in previousParticipants)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Consulted non-owner: Voice exitConference / shouldWrapUpForThisAgent stays
|
|
249
|
+
* false unless an explicit wrap-up signal includes this agent.
|
|
250
|
+
*/
|
|
251
|
+
private static isConsultedNonOwner(
|
|
252
|
+
payload: WebSocketPayload,
|
|
253
|
+
agentId?: string,
|
|
254
|
+
task?: ITask
|
|
255
|
+
): boolean {
|
|
256
|
+
const isConsulted = payload.isConsulted ?? task?.data?.isConsulted;
|
|
257
|
+
if (isConsulted !== true) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const owner = payload.interaction?.owner ?? task?.data?.interaction?.owner;
|
|
261
|
+
|
|
262
|
+
return Boolean(agentId && owner && owner !== agentId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private static hasNonemptyPendingWrapUp(payload: WebSocketPayload): boolean {
|
|
266
|
+
return Array.isArray(payload.agentsPendingWrapUp) && payload.agentsPendingWrapUp.length > 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Transfer initiator fallback: widgets set transferConferenceRequested on the
|
|
271
|
+
* actor; desktop success payloads may include consultingAgentId. Only used
|
|
272
|
+
* when agentsPendingWrapUp is empty or absent.
|
|
273
|
+
*/
|
|
274
|
+
private static isSelfConferenceTransferInitiator(
|
|
275
|
+
payload: WebSocketPayload,
|
|
276
|
+
agentId?: string,
|
|
277
|
+
task?: ITask
|
|
278
|
+
): boolean {
|
|
279
|
+
const snapshotContext = task?.stateMachineService?.getSnapshot?.()?.context as
|
|
280
|
+
| {transferConferenceRequested?: boolean}
|
|
281
|
+
| undefined;
|
|
282
|
+
if (snapshotContext?.transferConferenceRequested === true) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return Boolean(agentId && payload.consultingAgentId === agentId);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Explicit wrap-up signals only (no owner / isConsulted fallback).
|
|
291
|
+
* Used for events that remaining conference agents also receive.
|
|
292
|
+
* nonempty agentsPendingWrapUp is authoritative, then wrapUpRequired / isWrapUp.
|
|
293
|
+
*/
|
|
294
|
+
private static wrapUpRequiredFromExplicitSignals(
|
|
295
|
+
payload: WebSocketPayload,
|
|
296
|
+
agentId?: string
|
|
297
|
+
): boolean {
|
|
298
|
+
const pending = payload.agentsPendingWrapUp;
|
|
299
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
300
|
+
return Boolean(agentId && pending.includes(agentId));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (payload.wrapUpRequired === true) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (!agentId) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return payload.interaction?.participants?.[agentId]?.isWrapUp === true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Stamp wrapUpRequired once at ingress (prepareEventContext) so updateTaskData
|
|
315
|
+
* and the mapper see the same flag. Remaining agents stay false.
|
|
316
|
+
*/
|
|
317
|
+
private static stampWrapUpRequiredForEvent(
|
|
318
|
+
eventType: CC_EVENTS,
|
|
319
|
+
payload: WebSocketPayload,
|
|
320
|
+
agentId?: string,
|
|
321
|
+
task?: ITask
|
|
322
|
+
): WebSocketPayload {
|
|
323
|
+
if (!WRAP_UP_STAMP_EVENTS.has(eventType)) {
|
|
324
|
+
return payload;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (eventType === CC_EVENTS.AGENT_WRAPUP) {
|
|
328
|
+
return {...payload, wrapUpRequired: true};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (eventType === CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED) {
|
|
332
|
+
return {
|
|
333
|
+
...payload,
|
|
334
|
+
wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
|
|
339
|
+
const actorState = task?.stateMachineService?.getSnapshot?.()?.value;
|
|
340
|
+
if (actorState && !PARTICIPANT_LEAVE_WRAP_STATES.has(actorState as TaskState)) {
|
|
341
|
+
return payload;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const left = TaskManager.didSelfLeaveConference(payload, agentId, task);
|
|
345
|
+
if (!left) {
|
|
346
|
+
return {
|
|
347
|
+
...payload,
|
|
348
|
+
wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (TaskManager.hasNonemptyPendingWrapUp(payload)) {
|
|
352
|
+
return {
|
|
353
|
+
...payload,
|
|
354
|
+
wrapUpRequired: Boolean(agentId && payload.agentsPendingWrapUp?.includes(agentId)),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
...payload,
|
|
360
|
+
wrapUpRequired:
|
|
361
|
+
TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId) ||
|
|
362
|
+
!TaskManager.isConsultedNonOwner(payload, agentId, task),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// AGENT_CONFERENCE_TRANSFERRED — pending list wins; initiator only if empty/absent
|
|
367
|
+
if (TaskManager.hasNonemptyPendingWrapUp(payload)) {
|
|
368
|
+
return {
|
|
369
|
+
...payload,
|
|
370
|
+
wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
...payload,
|
|
376
|
+
wrapUpRequired:
|
|
377
|
+
TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId) ||
|
|
378
|
+
TaskManager.isSelfConferenceTransferInitiator(payload, agentId, task),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
185
382
|
/**
|
|
186
383
|
* Map WebSocket CC_EVENTS to state machine TaskEvent
|
|
187
384
|
* @param ccEvent - The CC_EVENT type from WebSocket
|
|
188
385
|
* @param payload - The event payload
|
|
189
386
|
* @param agentId - Optional agent ID for state detection (needed for HYDRATE)
|
|
387
|
+
* @param task - Optional task instance for state-aware CONTACT_ENDED mapping
|
|
190
388
|
* @returns TaskEventPayload for state machine or null if no mapping
|
|
191
389
|
*/
|
|
192
390
|
private static mapEventToTaskStateMachineEvent(
|
|
193
391
|
ccEvent: CC_EVENTS,
|
|
194
392
|
payload: WebSocketPayload,
|
|
195
|
-
agentId?: string
|
|
393
|
+
agentId?: string,
|
|
394
|
+
task?: ITask,
|
|
395
|
+
enableWxBetterTogether?: boolean
|
|
196
396
|
): TaskEventPayload | null {
|
|
197
397
|
const mediaResourceId =
|
|
198
398
|
payload.mediaResourceId ||
|
|
@@ -226,6 +426,15 @@ export default class TaskManager extends EventEmitter {
|
|
|
226
426
|
return {type: TaskEvent.HYDRATE, taskData: payload, agentId};
|
|
227
427
|
|
|
228
428
|
case CC_EVENTS.CONTACT_UPDATED:
|
|
429
|
+
if (
|
|
430
|
+
task &&
|
|
431
|
+
typeof payload.interaction?.owner === 'string' &&
|
|
432
|
+
payload.interaction.owner.trim().length > 0 &&
|
|
433
|
+
payload.interaction.owner !== task.data?.interaction?.owner
|
|
434
|
+
) {
|
|
435
|
+
return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
|
|
436
|
+
}
|
|
437
|
+
|
|
229
438
|
return {type: TaskEvent.CONTACT_UPDATED, taskData: payload};
|
|
230
439
|
case CC_EVENTS.CONTACT_OWNER_CHANGED:
|
|
231
440
|
return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
|
|
@@ -299,16 +508,39 @@ export default class TaskManager extends EventEmitter {
|
|
|
299
508
|
case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
|
|
300
509
|
return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
|
|
301
510
|
|
|
302
|
-
case CC_EVENTS.CONTACT_ENDED:
|
|
511
|
+
case CC_EVENTS.CONTACT_ENDED: {
|
|
512
|
+
const wrapUpRequired = isCampaignPreviewTask(payload)
|
|
513
|
+
? false
|
|
514
|
+
: payload.agentsPendingWrapUp?.includes(agentId || '') || false;
|
|
515
|
+
|
|
516
|
+
// Post-accept wxApp outdial agent-end delivers ContactEnded instead of
|
|
517
|
+
// AgentOutboundFailed. Normalize to OUTBOUND_FAILED so state machine
|
|
518
|
+
// enters WRAPPING_UP and emits TASK_OUTDIAL_FAILED (AGENT_ENDS).
|
|
519
|
+
// Pre-accept cancelTask decline stays CONTACT_ENDED → TERMINATED (no wrapup).
|
|
520
|
+
if (
|
|
521
|
+
TaskManager.isAgentTerminatedOutdialWrapup(
|
|
522
|
+
payload,
|
|
523
|
+
wrapUpRequired,
|
|
524
|
+
task,
|
|
525
|
+
agentId,
|
|
526
|
+
enableWxBetterTogether
|
|
527
|
+
)
|
|
528
|
+
) {
|
|
529
|
+
return {
|
|
530
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
531
|
+
reason: 'AGENT_ENDS',
|
|
532
|
+
taskData: {...payload, wrapUpRequired: true},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
303
536
|
return {
|
|
304
537
|
type: TaskEvent.CONTACT_ENDED,
|
|
305
538
|
taskData: {
|
|
306
539
|
...payload,
|
|
307
|
-
wrapUpRequired
|
|
308
|
-
? false
|
|
309
|
-
: payload.agentsPendingWrapUp?.includes(agentId || '') || false,
|
|
540
|
+
wrapUpRequired,
|
|
310
541
|
},
|
|
311
542
|
};
|
|
543
|
+
}
|
|
312
544
|
|
|
313
545
|
case CC_EVENTS.AGENT_INVITE_FAILED:
|
|
314
546
|
return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
|
|
@@ -319,8 +551,23 @@ export default class TaskManager extends EventEmitter {
|
|
|
319
551
|
case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
|
|
320
552
|
return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
|
|
321
553
|
|
|
322
|
-
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
|
|
323
|
-
|
|
554
|
+
case CC_EVENTS.AGENT_OUTBOUND_FAILED: {
|
|
555
|
+
const isAgentTerminatedOutdial =
|
|
556
|
+
payload.interaction?.outboundType === 'OUTDIAL' &&
|
|
557
|
+
(payload.terminatingParty === 'Agent' || payload.reason === 'AGENT_ENDS');
|
|
558
|
+
|
|
559
|
+
const suppressOutdialFailedPopup =
|
|
560
|
+
isAgentTerminatedOutdial &&
|
|
561
|
+
!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether);
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
565
|
+
taskData: suppressOutdialFailedPopup
|
|
566
|
+
? {...payload, suppressOutdialFailedPopup: true}
|
|
567
|
+
: payload,
|
|
568
|
+
reason: payload.reason,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
324
571
|
|
|
325
572
|
case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
|
|
326
573
|
return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
|
|
@@ -371,6 +618,68 @@ export default class TaskManager extends EventEmitter {
|
|
|
371
618
|
}
|
|
372
619
|
}
|
|
373
620
|
|
|
621
|
+
/**
|
|
622
|
+
* True when the current agent's task is a wxApp-managed outdial (thick-client telephony).
|
|
623
|
+
*/
|
|
624
|
+
private static isWxAppManagedOutdialTask(
|
|
625
|
+
agentId: string | undefined,
|
|
626
|
+
payload: WebSocketPayload,
|
|
627
|
+
task: ITask | undefined,
|
|
628
|
+
enableWxBetterTogether?: boolean
|
|
629
|
+
): boolean {
|
|
630
|
+
const wxAppEnabled =
|
|
631
|
+
(task as {uiControlConfig?: {enableWxBetterTogether?: boolean}})?.uiControlConfig
|
|
632
|
+
?.enableWxBetterTogether ??
|
|
633
|
+
enableWxBetterTogether ??
|
|
634
|
+
false;
|
|
635
|
+
|
|
636
|
+
if (!wxAppEnabled) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const effectiveAgentId = agentId ?? task?.data?.agentId;
|
|
641
|
+
const participants = payload.interaction?.participants ?? task?.data?.interaction?.participants;
|
|
642
|
+
const details = getWebexCallingDeviceDetailsForAgent(effectiveAgentId, participants);
|
|
643
|
+
|
|
644
|
+
return details?.deviceType === 'wxApp';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* True when ContactEnded represents an agent-terminated wxApp outdial that should
|
|
649
|
+
* enter wrapup with an outdial-failed popup (legacy AgentOutboundFailed parity).
|
|
650
|
+
*/
|
|
651
|
+
private static isAgentTerminatedOutdialWrapup(
|
|
652
|
+
payload: WebSocketPayload,
|
|
653
|
+
wrapUpRequired: boolean,
|
|
654
|
+
task?: ITask,
|
|
655
|
+
agentId?: string,
|
|
656
|
+
enableWxBetterTogether?: boolean
|
|
657
|
+
): boolean {
|
|
658
|
+
const isOutdial = payload.interaction?.outboundType === 'OUTDIAL';
|
|
659
|
+
const agentTerminated = payload.terminatingParty === 'Agent';
|
|
660
|
+
|
|
661
|
+
if (!isOutdial || !agentTerminated) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether)) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const wxAppAnswerPending =
|
|
670
|
+
(task as {uiControlConfig?: {wxAppAnswerPending?: boolean}})?.uiControlConfig
|
|
671
|
+
?.wxAppAnswerPending ?? false;
|
|
672
|
+
|
|
673
|
+
// Pre-accept decline: decline() clears wxAppAnswerPending; backend may still send state wrapUp.
|
|
674
|
+
if (!wxAppAnswerPending && !wrapUpRequired) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const interactionWrapUp = payload.interaction?.state === 'wrapUp';
|
|
679
|
+
|
|
680
|
+
return wrapUpRequired || interactionWrapUp || wxAppAnswerPending;
|
|
681
|
+
}
|
|
682
|
+
|
|
374
683
|
/**
|
|
375
684
|
* Register WebSocket message listeners for task events
|
|
376
685
|
*
|
|
@@ -482,6 +791,14 @@ export default class TaskManager extends EventEmitter {
|
|
|
482
791
|
}
|
|
483
792
|
}
|
|
484
793
|
|
|
794
|
+
if (!task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
795
|
+
task = this.findTaskForContactOwnerChange(message.data);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
|
|
799
|
+
task = this.findUniqueTaskByRelatedInteraction(message.data);
|
|
800
|
+
}
|
|
801
|
+
|
|
485
802
|
const wasConsultedTask = Boolean(task?.data?.isConsulted);
|
|
486
803
|
const computeWrapUpRequired = () => {
|
|
487
804
|
if (message.data.wrapUpRequired !== undefined) {
|
|
@@ -494,7 +811,7 @@ export default class TaskManager extends EventEmitter {
|
|
|
494
811
|
return !wasConsultedTask;
|
|
495
812
|
};
|
|
496
813
|
|
|
497
|
-
|
|
814
|
+
let adjustedPayload: WebSocketPayload =
|
|
498
815
|
eventType === CC_EVENTS.AGENT_CONSULT_TRANSFERRED ||
|
|
499
816
|
eventType === CC_EVENTS.AGENT_BLIND_TRANSFERRED ||
|
|
500
817
|
eventType === CC_EVENTS.AGENT_VTEAM_TRANSFERRED
|
|
@@ -504,10 +821,29 @@ export default class TaskManager extends EventEmitter {
|
|
|
504
821
|
}
|
|
505
822
|
: message.data;
|
|
506
823
|
|
|
824
|
+
if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
825
|
+
adjustedPayload = TaskManager.preserveMainTaskIdentity(task, adjustedPayload);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (task && eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
|
|
829
|
+
adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (WRAP_UP_STAMP_EVENTS.has(eventType)) {
|
|
833
|
+
adjustedPayload = TaskManager.stampWrapUpRequiredForEvent(
|
|
834
|
+
eventType,
|
|
835
|
+
adjustedPayload,
|
|
836
|
+
this.agentId,
|
|
837
|
+
task
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
507
841
|
const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
|
|
508
842
|
eventType,
|
|
509
843
|
adjustedPayload,
|
|
510
|
-
this.agentId
|
|
844
|
+
this.agentId,
|
|
845
|
+
task,
|
|
846
|
+
this.configFlags?.enableWxBetterTogether
|
|
511
847
|
);
|
|
512
848
|
|
|
513
849
|
LoggerProxy.info(`Handling task event ${eventType}`, {
|
|
@@ -524,6 +860,236 @@ export default class TaskManager extends EventEmitter {
|
|
|
524
860
|
};
|
|
525
861
|
}
|
|
526
862
|
|
|
863
|
+
/**
|
|
864
|
+
* ContactOwnerChanged can identify the promoted agent's child interaction while
|
|
865
|
+
* the surviving task is stored under the main interaction. Accept one related
|
|
866
|
+
* task when its existing snapshot or the authoritative incoming promotion proves
|
|
867
|
+
* current-agent main-call membership, while excluding non-promoted and ambiguous matches.
|
|
868
|
+
*/
|
|
869
|
+
private findTaskForContactOwnerChange(payload: WebSocketPayload): ITask | undefined {
|
|
870
|
+
const relatedCandidates = this.findRelatedTasks(payload, {
|
|
871
|
+
includeCollectionKeys: true,
|
|
872
|
+
includeMainCallMediaKeys: true,
|
|
873
|
+
});
|
|
874
|
+
const incomingShowsCurrentAgentPromotion =
|
|
875
|
+
this.isCurrentAgentPromotedOnIncomingMainCall(payload);
|
|
876
|
+
const eligibleCandidates = relatedCandidates.filter(
|
|
877
|
+
(candidate) =>
|
|
878
|
+
this.isCurrentAgentActiveOnMainInteraction(candidate.data) ||
|
|
879
|
+
incomingShowsCurrentAgentPromotion
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
if (eligibleCandidates.length === 1) {
|
|
883
|
+
return eligibleCandidates[0];
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (eligibleCandidates.length > 1) {
|
|
887
|
+
LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
|
|
888
|
+
module: TASK_MANAGER_FILE,
|
|
889
|
+
method: 'findTaskForContactOwnerChange',
|
|
890
|
+
interactionId:
|
|
891
|
+
payload.interaction?.mainInteractionId ||
|
|
892
|
+
payload.interaction?.parentInteractionId ||
|
|
893
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId ||
|
|
894
|
+
payload.interactionId,
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
return undefined;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private isCurrentAgentActiveOnMainInteraction(taskData: TaskData | undefined): boolean {
|
|
904
|
+
const currentAgentId = this.agentId || taskData?.agentId;
|
|
905
|
+
if (!currentAgentId) return false;
|
|
906
|
+
|
|
907
|
+
return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private static isParticipantActiveOnMainInteraction(
|
|
911
|
+
taskData: TaskData | WebSocketPayload | undefined,
|
|
912
|
+
participantId: string
|
|
913
|
+
): boolean {
|
|
914
|
+
const interaction = taskData?.interaction;
|
|
915
|
+
const participant = interaction?.participants?.[participantId];
|
|
916
|
+
if (!participant || participant.hasLeft === true) return false;
|
|
917
|
+
|
|
918
|
+
return Object.values(interaction?.media ?? {}).some(
|
|
919
|
+
(media) =>
|
|
920
|
+
media?.mType === MEDIA_TYPE_MAIN_CALL && media.participants?.includes(participantId)
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private isCurrentAgentPromotedOnIncomingMainCall(payload: WebSocketPayload): boolean {
|
|
925
|
+
return Boolean(
|
|
926
|
+
this.agentId &&
|
|
927
|
+
payload.interaction?.owner === this.agentId &&
|
|
928
|
+
TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private static getMainCallMediaId(
|
|
933
|
+
interaction: TaskData['interaction'] | undefined
|
|
934
|
+
): string | undefined {
|
|
935
|
+
return Object.entries(interaction?.media ?? {}).find(
|
|
936
|
+
([, media]) => media?.mType === MEDIA_TYPE_MAIN_CALL
|
|
937
|
+
)?.[0];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private static getStableMainInteractionId(payload: WebSocketPayload): string | undefined {
|
|
941
|
+
return (
|
|
942
|
+
payload.interaction?.mainInteractionId ||
|
|
943
|
+
TaskManager.getMainCallMediaId(payload.interaction) ||
|
|
944
|
+
payload.interaction?.interactionId ||
|
|
945
|
+
payload.interactionId
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private canRecoverTaskFromContactOwnerChanged(payload: WebSocketPayload): boolean {
|
|
950
|
+
const interaction = payload.interaction;
|
|
951
|
+
const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
|
|
952
|
+
|
|
953
|
+
return Boolean(
|
|
954
|
+
this.agentId &&
|
|
955
|
+
stableInteractionId &&
|
|
956
|
+
interaction?.mediaType === MEDIA_CHANNEL.TELEPHONY &&
|
|
957
|
+
interaction.isTerminated !== true &&
|
|
958
|
+
interaction.owner === this.agentId &&
|
|
959
|
+
TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
|
|
964
|
+
private static preserveMainTaskIdentity(
|
|
965
|
+
task: ITask,
|
|
966
|
+
payload: WebSocketPayload
|
|
967
|
+
): WebSocketPayload {
|
|
968
|
+
const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
|
|
969
|
+
const payloadMainInteractionId = payload.interaction?.mainInteractionId;
|
|
970
|
+
const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
|
|
971
|
+
const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
|
|
972
|
+
const stableInteractionId =
|
|
973
|
+
currentMainInteractionId ||
|
|
974
|
+
payloadMainInteractionId ||
|
|
975
|
+
currentMainMediaId ||
|
|
976
|
+
payloadMainMediaId ||
|
|
977
|
+
payload.interaction?.interactionId ||
|
|
978
|
+
task.data?.interaction?.interactionId ||
|
|
979
|
+
task.data?.interactionId;
|
|
980
|
+
|
|
981
|
+
if (!stableInteractionId || stableInteractionId === payload.interactionId) {
|
|
982
|
+
return payload;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return {...payload, interactionId: stableInteractionId};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* ParticipantLeftConference may arrive after an owner update while still carrying
|
|
990
|
+
* the departed owner. Keep the confirmed owner only when the new roster proves that
|
|
991
|
+
* owner is active on mainCall and identifies the incoming owner as departed.
|
|
992
|
+
*/
|
|
993
|
+
private static preserveConfirmedOwner(task: ITask, payload: WebSocketPayload): WebSocketPayload {
|
|
994
|
+
const confirmedOwner = task.data?.interaction?.owner;
|
|
995
|
+
const incomingOwner = payload.interaction?.owner;
|
|
996
|
+
if (!confirmedOwner || !incomingOwner || confirmedOwner === incomingOwner) {
|
|
997
|
+
return payload;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const confirmedOwnerIsActive = TaskManager.isParticipantActiveOnMainInteraction(
|
|
1001
|
+
payload,
|
|
1002
|
+
confirmedOwner
|
|
1003
|
+
);
|
|
1004
|
+
const incomingOwnerIsExplicitlyDeparted =
|
|
1005
|
+
payload.participantId === incomingOwner ||
|
|
1006
|
+
payload.interaction?.participants?.[incomingOwner]?.hasLeft === true;
|
|
1007
|
+
|
|
1008
|
+
if (!confirmedOwnerIsActive || !incomingOwnerIsExplicitlyDeparted) {
|
|
1009
|
+
return payload;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
...payload,
|
|
1014
|
+
owner: confirmedOwner,
|
|
1015
|
+
interaction: {...payload.interaction, owner: confirmedOwner},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Resolve lifecycle events that are emitted for the main interaction while the
|
|
1021
|
+
* local task can still be indexed by a child consult interaction. Exact task
|
|
1022
|
+
* keys are handled before this fallback. Multiple aliases of the same task are
|
|
1023
|
+
* treated as one candidate; genuinely ambiguous matches are intentionally ignored.
|
|
1024
|
+
*/
|
|
1025
|
+
private findUniqueTaskByRelatedInteraction(
|
|
1026
|
+
payload: WebSocketPayload,
|
|
1027
|
+
candidateFilter: (task: ITask) => boolean = () => true
|
|
1028
|
+
): ITask | undefined {
|
|
1029
|
+
const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
|
|
1030
|
+
|
|
1031
|
+
if (candidates.length === 1) {
|
|
1032
|
+
return candidates[0];
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (candidates.length > 1) {
|
|
1036
|
+
LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
|
|
1037
|
+
module: TASK_MANAGER_FILE,
|
|
1038
|
+
method: 'findUniqueTaskByRelatedInteraction',
|
|
1039
|
+
interactionId:
|
|
1040
|
+
payload.interaction?.mainInteractionId ||
|
|
1041
|
+
payload.interaction?.parentInteractionId ||
|
|
1042
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId ||
|
|
1043
|
+
payload.interactionId,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private findRelatedTasks(
|
|
1051
|
+
payload: WebSocketPayload,
|
|
1052
|
+
options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
|
|
1053
|
+
): ITask[] {
|
|
1054
|
+
const {includeCollectionKeys = false, includeMainCallMediaKeys = false} = options;
|
|
1055
|
+
const payloadInteractionIds = new Set(
|
|
1056
|
+
[
|
|
1057
|
+
payload.interactionId,
|
|
1058
|
+
payload.interaction?.interactionId,
|
|
1059
|
+
payload.interaction?.mainInteractionId,
|
|
1060
|
+
payload.interaction?.parentInteractionId,
|
|
1061
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId,
|
|
1062
|
+
...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : []),
|
|
1063
|
+
].filter((interactionId): interactionId is string => Boolean(interactionId))
|
|
1064
|
+
);
|
|
1065
|
+
if (payloadInteractionIds.size === 0) return [];
|
|
1066
|
+
|
|
1067
|
+
return [
|
|
1068
|
+
...new Set(
|
|
1069
|
+
Object.entries(this.taskCollection)
|
|
1070
|
+
.filter(([taskId, candidate]) => {
|
|
1071
|
+
const taskInteraction = candidate?.data?.interaction;
|
|
1072
|
+
const candidateInteractionIds = [
|
|
1073
|
+
...(includeCollectionKeys ? [taskId] : []),
|
|
1074
|
+
candidate?.data?.interactionId,
|
|
1075
|
+
taskInteraction?.interactionId,
|
|
1076
|
+
taskInteraction?.mainInteractionId,
|
|
1077
|
+
taskInteraction?.parentInteractionId,
|
|
1078
|
+
taskInteraction?.callProcessingDetails?.parentInteractionId,
|
|
1079
|
+
...(includeMainCallMediaKeys
|
|
1080
|
+
? [TaskManager.getMainCallMediaId(taskInteraction)]
|
|
1081
|
+
: []),
|
|
1082
|
+
];
|
|
1083
|
+
|
|
1084
|
+
return candidateInteractionIds.some(
|
|
1085
|
+
(interactionId) => Boolean(interactionId) && payloadInteractionIds.has(interactionId)
|
|
1086
|
+
);
|
|
1087
|
+
})
|
|
1088
|
+
.map(([, candidate]) => candidate)
|
|
1089
|
+
),
|
|
1090
|
+
];
|
|
1091
|
+
}
|
|
1092
|
+
|
|
527
1093
|
/**
|
|
528
1094
|
* Handle task lifecycle events and determine required actions
|
|
529
1095
|
*
|
|
@@ -549,6 +1115,9 @@ export default class TaskManager extends EventEmitter {
|
|
|
549
1115
|
case CC_EVENTS.CONTACT_MERGED:
|
|
550
1116
|
return this.handleContactMergedEvent(context);
|
|
551
1117
|
|
|
1118
|
+
case CC_EVENTS.CONTACT_OWNER_CHANGED:
|
|
1119
|
+
return this.handleContactOwnerChanged(context);
|
|
1120
|
+
|
|
552
1121
|
case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
|
|
553
1122
|
return this.handleCampaignPreviewReservation(context);
|
|
554
1123
|
|
|
@@ -560,6 +1129,74 @@ export default class TaskManager extends EventEmitter {
|
|
|
560
1129
|
}
|
|
561
1130
|
}
|
|
562
1131
|
|
|
1132
|
+
/**
|
|
1133
|
+
* Recover the promoted agent's active voice task when local task state was lost.
|
|
1134
|
+
* Existing related tasks are never replaced: an unresolved or ambiguous relation
|
|
1135
|
+
* is safer to ignore than to create a duplicate task.
|
|
1136
|
+
*/
|
|
1137
|
+
private handleContactOwnerChanged(context: EventContext): TaskEventActions {
|
|
1138
|
+
if (context.task) {
|
|
1139
|
+
return {task: context.task};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const {payload} = context;
|
|
1143
|
+
if (
|
|
1144
|
+
this.findRelatedTasks(payload, {
|
|
1145
|
+
includeCollectionKeys: true,
|
|
1146
|
+
includeMainCallMediaKeys: true,
|
|
1147
|
+
}).length > 0 ||
|
|
1148
|
+
!this.canRecoverTaskFromContactOwnerChanged(payload)
|
|
1149
|
+
) {
|
|
1150
|
+
return {};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
|
|
1154
|
+
if (!stableInteractionId || this.taskCollection[stableInteractionId]) {
|
|
1155
|
+
return {};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const normalizedPayload: WebSocketPayload =
|
|
1159
|
+
payload.interactionId === stableInteractionId
|
|
1160
|
+
? payload
|
|
1161
|
+
: {...payload, interactionId: stableInteractionId};
|
|
1162
|
+
const taskData: TaskData = {
|
|
1163
|
+
...normalizedPayload,
|
|
1164
|
+
owner: normalizedPayload.interaction.owner,
|
|
1165
|
+
wrapUpRequired: normalizedPayload.interaction.participants?.[this.agentId]?.isWrapUp || false,
|
|
1166
|
+
isConferenceInProgress: getIsConferenceInProgress(normalizedPayload),
|
|
1167
|
+
isConsulted: false,
|
|
1168
|
+
isAutoAnswering: false,
|
|
1169
|
+
};
|
|
1170
|
+
const task = TaskFactory.createTask(
|
|
1171
|
+
this.contact,
|
|
1172
|
+
this.webCallingService,
|
|
1173
|
+
taskData,
|
|
1174
|
+
this.configFlags,
|
|
1175
|
+
this.wrapupData,
|
|
1176
|
+
this.agentId,
|
|
1177
|
+
this.answerCallOnWebexService
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
this.taskCollection[stableInteractionId] = task;
|
|
1181
|
+
|
|
1182
|
+
// Restore the actor before installing listeners so the internal hydrate does
|
|
1183
|
+
// not leak as a second public hydrate or as an incoming-task notification.
|
|
1184
|
+
task.sendStateMachineEvent({
|
|
1185
|
+
type: TaskEvent.HYDRATE,
|
|
1186
|
+
taskData,
|
|
1187
|
+
agentId: this.agentId,
|
|
1188
|
+
} as TaskEventPayload);
|
|
1189
|
+
|
|
1190
|
+
this.setupTaskListeners(task);
|
|
1191
|
+
context.payload = taskData;
|
|
1192
|
+
context.stateMachineEvent = {
|
|
1193
|
+
type: TaskEvent.CONTACT_OWNER_CHANGED,
|
|
1194
|
+
taskData,
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
return {task};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
563
1200
|
private handleCampaignContactUpdated(context: EventContext) {
|
|
564
1201
|
const {payload} = context;
|
|
565
1202
|
let {task} = context;
|
|
@@ -647,7 +1284,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
647
1284
|
},
|
|
648
1285
|
this.configFlags,
|
|
649
1286
|
this.wrapupData,
|
|
650
|
-
this.agentId
|
|
1287
|
+
this.agentId,
|
|
1288
|
+
this.answerCallOnWebexService
|
|
651
1289
|
);
|
|
652
1290
|
this.setupTaskListeners(task);
|
|
653
1291
|
this.taskCollection[payload.interactionId] = task;
|
|
@@ -685,7 +1323,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
685
1323
|
taskData,
|
|
686
1324
|
this.configFlags,
|
|
687
1325
|
this.wrapupData,
|
|
688
|
-
this.agentId
|
|
1326
|
+
this.agentId,
|
|
1327
|
+
this.answerCallOnWebexService
|
|
689
1328
|
);
|
|
690
1329
|
|
|
691
1330
|
this.setupTaskListeners(task);
|
|
@@ -725,7 +1364,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
725
1364
|
taskData,
|
|
726
1365
|
this.configFlags,
|
|
727
1366
|
this.wrapupData,
|
|
728
|
-
this.agentId
|
|
1367
|
+
this.agentId,
|
|
1368
|
+
this.answerCallOnWebexService
|
|
729
1369
|
);
|
|
730
1370
|
this.setupTaskListeners(task);
|
|
731
1371
|
this.taskCollection[payload.interactionId] = task;
|
|
@@ -753,6 +1393,14 @@ export default class TaskManager extends EventEmitter {
|
|
|
753
1393
|
: taskData;
|
|
754
1394
|
|
|
755
1395
|
task.updateTaskData(updateTaskData);
|
|
1396
|
+
|
|
1397
|
+
// A main-interaction lifecycle event can re-key a child consult task. Remove
|
|
1398
|
+
// any aliases first so consumers never observe duplicate task entries.
|
|
1399
|
+
Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
|
|
1400
|
+
if (candidate === task && taskId !== taskData.interactionId) {
|
|
1401
|
+
delete this.taskCollection[taskId];
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
756
1404
|
this.taskCollection[taskData.interactionId] = task;
|
|
757
1405
|
|
|
758
1406
|
return task;
|
|
@@ -798,8 +1446,10 @@ export default class TaskManager extends EventEmitter {
|
|
|
798
1446
|
task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
|
|
799
1447
|
this.handleTaskCleanup(t);
|
|
800
1448
|
if (options?.removeFromCollection) {
|
|
801
|
-
const
|
|
802
|
-
|
|
1449
|
+
const taskIsStillCollected = Object.values(this.taskCollection).some(
|
|
1450
|
+
(candidate) => candidate === t
|
|
1451
|
+
);
|
|
1452
|
+
if (taskIsStillCollected) {
|
|
803
1453
|
this.removeTaskFromCollection(t);
|
|
804
1454
|
}
|
|
805
1455
|
}
|
|
@@ -811,7 +1461,11 @@ export default class TaskManager extends EventEmitter {
|
|
|
811
1461
|
task.cancelAutoWrapupTimer();
|
|
812
1462
|
}
|
|
813
1463
|
if (task?.data?.interactionId) {
|
|
814
|
-
|
|
1464
|
+
Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
|
|
1465
|
+
if (candidate === task) {
|
|
1466
|
+
delete this.taskCollection[taskId];
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
815
1469
|
LoggerProxy.info(`Task removed from collection`, {
|
|
816
1470
|
module: TASK_MANAGER_FILE,
|
|
817
1471
|
method: METHODS.REMOVE_TASK_FROM_COLLECTION,
|
|
@@ -864,7 +1518,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
864
1518
|
taskData,
|
|
865
1519
|
this.configFlags,
|
|
866
1520
|
this.wrapupData,
|
|
867
|
-
this.agentId
|
|
1521
|
+
this.agentId,
|
|
1522
|
+
this.answerCallOnWebexService
|
|
868
1523
|
);
|
|
869
1524
|
this.taskCollection[payload.interactionId] = task;
|
|
870
1525
|
|
|
@@ -956,6 +1611,50 @@ export default class TaskManager extends EventEmitter {
|
|
|
956
1611
|
return {...this.taskCollection};
|
|
957
1612
|
}
|
|
958
1613
|
|
|
1614
|
+
/**
|
|
1615
|
+
* Propagate runtime enableWxBetterTogether changes to config flags and active tasks.
|
|
1616
|
+
*/
|
|
1617
|
+
public applyEnableWxBetterTogether(enabled: boolean): void {
|
|
1618
|
+
if (this.configFlags) {
|
|
1619
|
+
this.configFlags = {...this.configFlags, enableWxBetterTogether: enabled};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1623
|
+
task.setEnableWxBetterTogether(enabled);
|
|
1624
|
+
if (enabled) {
|
|
1625
|
+
TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Propagate wxApp mute state from Mercury sync to active voice tasks.
|
|
1632
|
+
*/
|
|
1633
|
+
public applyWxAppMuteStateFromSync(callId: string, muted: boolean): void {
|
|
1634
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1635
|
+
task.applyWxAppMuteStateFromSync(callId, muted);
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Re-seeds wxApp mute state from GET telephony/calls/{callId} for one task.
|
|
1641
|
+
*/
|
|
1642
|
+
public static syncWxAppMuteFromCallDetailsForTask(task: ITask): void {
|
|
1643
|
+
const sync = task.syncWxAppMuteFromCallDetails?.bind(task);
|
|
1644
|
+
if (typeof sync === 'function') {
|
|
1645
|
+
sync().catch(() => undefined);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Re-seeds wxApp mute for all engaged voice tasks (reload / runtime enable backfill).
|
|
1651
|
+
*/
|
|
1652
|
+
public syncWxAppMuteFromCallDetailsForAllTasks(): void {
|
|
1653
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1654
|
+
TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
|
|
959
1658
|
public static getTaskManager(
|
|
960
1659
|
apiAIAssistant: ApiAIAssistant,
|
|
961
1660
|
contact: ReturnType<typeof routingContact>,
|