@webex/contact-center 3.12.0-next.13 → 3.12.0-next.130
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 +884 -0
- package/AGENTS.md +94 -0
- package/ai-docs/ARCHITECTURE.md +168 -0
- package/ai-docs/CONTRACTS.md +53 -0
- package/ai-docs/GETTING_STARTED.md +168 -0
- package/ai-docs/GLOSSARY.md +43 -0
- package/ai-docs/README.md +138 -0
- package/ai-docs/REVIEW_CHECKLIST.md +41 -0
- package/ai-docs/RULES.md +444 -0
- package/ai-docs/SECURITY.md +52 -0
- package/ai-docs/SERVICE_STATE.md +52 -0
- package/ai-docs/SPEC_INDEX.md +65 -0
- package/ai-docs/adr/0001-spec-source-policy.md +55 -0
- package/ai-docs/adr/README.md +8 -0
- package/ai-docs/adr/_adr-template.md +31 -0
- package/ai-docs/contact-center-spec.md +382 -0
- package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
- package/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
- package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
- package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
- package/ai-docs/patterns/event-driven-patterns.md +485 -0
- package/ai-docs/patterns/testing-patterns.md +480 -0
- package/ai-docs/patterns/typescript-patterns.md +365 -0
- package/ai-docs/templates/README.md +102 -0
- package/ai-docs/templates/documentation/create-agents-md.md +240 -0
- package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
- package/ai-docs/templates/existing-service/bug-fix.md +254 -0
- package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
- package/ai-docs/templates/new-method/00-master.md +80 -0
- package/ai-docs/templates/new-method/01-requirements.md +232 -0
- package/ai-docs/templates/new-method/02-implementation.md +295 -0
- package/ai-docs/templates/new-method/03-tests.md +201 -0
- package/ai-docs/templates/new-method/04-validation.md +141 -0
- package/ai-docs/templates/new-service/00-master.md +109 -0
- package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
- package/ai-docs/templates/new-service/02-code-generation.md +346 -0
- package/ai-docs/templates/new-service/03-integration.md +178 -0
- package/ai-docs/templates/new-service/04-test-generation.md +205 -0
- package/ai-docs/templates/new-service/05-validation.md +145 -0
- package/dist/cc.js +971 -42
- package/dist/cc.js.map +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +31 -3
- package/dist/constants.js.map +1 -1
- package/dist/index.js +45 -15
- 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 +205 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +48 -4
- 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/ApiAiAssistant.js +269 -9
- package/dist/services/ApiAiAssistant.js.map +1 -1
- 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/UserPreference.js +427 -0
- package/dist/services/UserPreference.js.map +1 -0
- 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 +11 -4
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +41 -7
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +51 -0
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +57 -12
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +111 -32
- package/dist/services/core/Utils.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/core/websocket/WebSocketManager.js +2 -1
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/core/websocket/types.js.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +754 -0
- package/dist/services/task/Task.js.map +1 -0
- package/dist/services/task/TaskFactory.js +49 -0
- package/dist/services/task/TaskFactory.js.map +1 -0
- package/dist/services/task/TaskManager.js +1172 -521
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +211 -27
- package/dist/services/task/TaskUtils.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 +12 -2
- 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/dialer.js +78 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/digital/Digital.js +78 -0
- package/dist/services/task/digital/Digital.js.map +1 -0
- package/dist/services/task/state-machine/TaskStateMachine.js +982 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +572 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +161 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +379 -0
- package/dist/services/task/state-machine/guards.js.map +1 -0
- package/dist/services/task/state-machine/index.js +53 -0
- package/dist/services/task/state-machine/index.js.map +1 -0
- package/dist/services/task/state-machine/types.js +54 -0
- package/dist/services/task/state-machine/types.js.map +1 -0
- package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
- package/dist/services/task/taskDataNormalizer.js +99 -0
- package/dist/services/task/taskDataNormalizer.js.map +1 -0
- package/dist/services/task/types.js +228 -5
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1485 -0
- package/dist/services/task/voice/Voice.js.map +1 -0
- package/dist/services/task/voice/WebRTC.js +152 -0
- package/dist/services/task/voice/WebRTC.js.map +1 -0
- 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 +193 -6
- package/dist/types/config.d.ts +13 -0
- package/dist/types/constants.d.ts +29 -1
- package/dist/types/index.d.ts +35 -8
- package/dist/types/metrics/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +43 -1
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
- package/dist/types/services/ApiAiAssistant.d.ts +56 -5
- package/dist/types/services/EntryPoint.d.ts +7 -5
- package/dist/types/services/Queue.d.ts +5 -3
- package/dist/types/services/UserPreference.d.ts +118 -0
- 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 +34 -5
- package/dist/types/services/config/types.d.ts +197 -12
- package/dist/types/services/core/Err.d.ts +6 -0
- package/dist/types/services/core/Utils.d.ts +35 -13
- package/dist/types/services/core/WebexRequest.d.ts +1 -0
- package/dist/types/services/core/types.d.ts +2 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
- package/dist/types/services/core/websocket/types.d.ts +1 -1
- package/dist/types/services/index.d.ts +1 -1
- package/dist/types/services/task/Task.d.ts +175 -0
- package/dist/types/services/task/TaskFactory.d.ts +13 -0
- package/dist/types/services/task/TaskUtils.d.ts +48 -2
- package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
- package/dist/types/services/task/constants.d.ts +10 -0
- package/dist/types/services/task/contact.d.ts +4 -0
- package/dist/types/services/task/dialer.d.ts +30 -0
- package/dist/types/services/task/digital/Digital.d.ts +22 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
- package/dist/types/services/task/state-machine/actions.d.ts +10 -0
- package/dist/types/services/task/state-machine/constants.d.ts +107 -0
- package/dist/types/services/task/state-machine/guards.d.ts +103 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +277 -0
- package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
- package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
- package/dist/types/services/task/types.d.ts +680 -66
- package/dist/types/services/task/voice/Voice.d.ts +227 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
- 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 +205 -9
- package/dist/types/utils/PageCache.d.ts +20 -3
- package/dist/types/webex.d.ts +1 -0
- package/dist/types.js +162 -4
- 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 +22 -2
- package/dist/webex.js.map +1 -1
- package/package.json +16 -12
- package/src/cc.ts +1289 -112
- package/src/config.ts +13 -0
- package/src/constants.ts +29 -1
- package/src/index.ts +36 -5
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/AGENTS.md +350 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
- package/src/metrics/ai-docs/metrics-spec.md +921 -0
- package/src/metrics/behavioral-events.ts +212 -0
- package/src/metrics/constants.ts +52 -4
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +239 -0
- package/src/services/ApiAiAssistant.ts +356 -9
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/UserPreference.ts +509 -0
- package/src/services/WebexCrossClientService.ts +293 -0
- package/src/services/WxAppTelephonyMercurySync.ts +142 -0
- package/src/services/agent/ai-docs/AGENTS.md +240 -0
- package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
- package/src/services/agent/ai-docs/agent-spec.md +505 -0
- package/src/services/ai-docs/AGENTS.md +386 -0
- package/src/services/ai-docs/services-spec.md +505 -0
- package/src/services/config/Util.ts +12 -3
- package/src/services/config/ai-docs/AGENTS.md +255 -0
- package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
- package/src/services/config/ai-docs/config-spec.md +686 -0
- package/src/services/config/constants.ts +46 -6
- package/src/services/config/index.ts +65 -0
- package/src/services/config/types.ts +197 -13
- package/src/services/core/Err.ts +3 -0
- package/src/services/core/Utils.ts +127 -37
- package/src/services/core/WebexRequest.ts +3 -1
- package/src/services/core/ai-docs/AGENTS.md +381 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
- package/src/services/core/ai-docs/core-spec.md +787 -0
- package/src/services/core/aqm-reqs.ts +30 -15
- package/src/services/core/types.ts +2 -0
- package/src/services/core/websocket/WebSocketManager.ts +2 -0
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +1 -1
- package/src/services/task/Task.ts +908 -0
- package/src/services/task/TaskFactory.ts +60 -0
- package/src/services/task/TaskManager.ts +1419 -608
- package/src/services/task/TaskUtils.ts +303 -25
- package/src/services/task/WebexCallingUtils.ts +136 -0
- package/src/services/task/ai-docs/AGENTS.md +457 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
- package/src/services/task/ai-docs/task-spec.md +1474 -0
- package/src/services/task/constants.ts +10 -0
- package/src/services/task/contact.ts +30 -0
- package/src/services/task/dialer.ts +80 -0
- package/src/services/task/digital/Digital.ts +97 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
- package/src/services/task/state-machine/actions.ts +741 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
- package/src/services/task/state-machine/constants.ts +172 -0
- package/src/services/task/state-machine/guards.ts +442 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +258 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +819 -70
- package/src/services/task/voice/Voice.ts +1851 -0
- package/src/services/task/voice/WebRTC.ts +191 -0
- 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 +262 -11
- package/src/utils/AGENTS.md +289 -0
- package/src/utils/PageCache.ts +38 -5
- package/src/utils/ai-docs/utils-spec.md +391 -0
- package/src/webex.js +10 -0
- package/test/unit/spec/cc.ts +2210 -119
- package/test/unit/spec/logger-proxy.ts +70 -0
- 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/ApiAiAssistant.ts +311 -21
- package/test/unit/spec/services/EntryPoint.ts +87 -40
- package/test/unit/spec/services/Queue.ts +123 -12
- package/test/unit/spec/services/UserPreference.ts +401 -0
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- 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 +129 -0
- package/test/unit/spec/services/config/index.ts +97 -29
- package/test/unit/spec/services/core/Utils.ts +425 -8
- 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/core/websocket/WebSocketManager.ts +66 -40
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +745 -0
- package/test/unit/spec/services/task/TaskFactory.ts +99 -0
- package/test/unit/spec/services/task/TaskManager.ts +2518 -1494
- package/test/unit/spec/services/task/TaskUtils.ts +235 -0
- 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/dialer.ts +190 -0
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +2061 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
- package/test/unit/spec/wellnessSampleUtils.ts +288 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
- package/dist/services/task/index.js +0 -1525
- package/dist/services/task/index.js.map +0 -1
- package/dist/types/services/task/index.d.ts +0 -650
- package/src/services/task/index.ts +0 -1801
- package/test/unit/spec/services/task/index.ts +0 -2184
|
@@ -3,25 +3,61 @@ import {ICall, LINE_EVENTS} from '@webex/calling';
|
|
|
3
3
|
import {WebSocketManager} from '../core/websocket/WebSocketManager';
|
|
4
4
|
import routingContact from './contact';
|
|
5
5
|
import WebCallingService from '../WebCallingService';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MEDIA_CHANNEL,
|
|
8
|
+
TASK_EVENTS,
|
|
9
|
+
TaskData,
|
|
10
|
+
TaskId,
|
|
11
|
+
ITask,
|
|
12
|
+
WebSocketPayload,
|
|
13
|
+
WebSocketMessage,
|
|
14
|
+
TaskEventActions,
|
|
15
|
+
EventContext,
|
|
16
|
+
} from './types';
|
|
7
17
|
import {TASK_MANAGER_FILE} from '../../constants';
|
|
8
18
|
import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants';
|
|
9
|
-
import {CC_EVENTS,
|
|
10
|
-
import {
|
|
19
|
+
import {CC_EVENTS, WrapupData} from '../config/types';
|
|
20
|
+
import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types';
|
|
11
21
|
import LoggerProxy from '../../logger-proxy';
|
|
12
|
-
import Task from '.';
|
|
13
|
-
import MetricsManager from '../../metrics/MetricsManager';
|
|
14
|
-
import {METRIC_EVENT_NAMES} from '../../metrics/constants';
|
|
15
22
|
import {
|
|
16
|
-
checkParticipantNotInInteraction,
|
|
17
23
|
getIsConferenceInProgress,
|
|
24
|
+
isCampaignPreviewTask,
|
|
18
25
|
isCampaignPreviewReservation,
|
|
19
|
-
isParticipantInMainInteraction,
|
|
20
|
-
isPrimary,
|
|
21
26
|
isSecondaryEpDnAgent,
|
|
22
27
|
shouldAutoAnswerTask,
|
|
23
28
|
} from './TaskUtils';
|
|
24
|
-
import
|
|
29
|
+
import TaskFactory from './TaskFactory';
|
|
30
|
+
import AnswerCallOnWebexService from '../AnswerCallOnWebexService';
|
|
31
|
+
import {getWebexCallingDeviceDetailsForAgent} from './WebexCallingUtils';
|
|
32
|
+
import WebRTC from './voice/WebRTC';
|
|
33
|
+
import {TaskEvent, TaskState, type TaskEventPayload} from './state-machine';
|
|
34
|
+
import {MEDIA_TYPE_MAIN_CALL} from './state-machine/constants';
|
|
35
|
+
import {normalizeTaskData} from './taskDataNormalizer';
|
|
36
|
+
import {ApiAIAssistant} from '../ApiAiAssistant';
|
|
37
|
+
|
|
38
|
+
const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
|
|
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
|
+
|
|
45
|
+
const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
|
|
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
|
+
]);
|
|
25
61
|
|
|
26
62
|
/** @internal */
|
|
27
63
|
export default class TaskManager extends EventEmitter {
|
|
@@ -35,11 +71,14 @@ export default class TaskManager extends EventEmitter {
|
|
|
35
71
|
private taskCollection: Record<TaskId, ITask>;
|
|
36
72
|
private webCallingService: WebCallingService;
|
|
37
73
|
private webSocketManager: WebSocketManager;
|
|
38
|
-
private
|
|
39
|
-
|
|
74
|
+
private rtdWebSocketManager: WebSocketManager;
|
|
75
|
+
// eslint-disable-next-line no-use-before-define
|
|
76
|
+
private static taskManager: TaskManager;
|
|
77
|
+
private configFlags?: ConfigFlags;
|
|
40
78
|
private wrapupData: WrapupData;
|
|
41
79
|
private agentId: string;
|
|
42
80
|
private webRtcEnabled: boolean;
|
|
81
|
+
private answerCallOnWebexService?: AnswerCallOnWebexService;
|
|
43
82
|
private apiAIAssistant?: ApiAIAssistant;
|
|
44
83
|
/**
|
|
45
84
|
* @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
|
|
@@ -50,47 +89,28 @@ export default class TaskManager extends EventEmitter {
|
|
|
50
89
|
apiAIAssistant: ApiAIAssistant,
|
|
51
90
|
contact: ReturnType<typeof routingContact>,
|
|
52
91
|
webCallingService: WebCallingService,
|
|
53
|
-
webSocketManager: WebSocketManager
|
|
92
|
+
webSocketManager: WebSocketManager,
|
|
93
|
+
rtdWebSocketManager: WebSocketManager
|
|
54
94
|
) {
|
|
55
95
|
super();
|
|
56
96
|
this.apiAIAssistant = apiAIAssistant;
|
|
57
97
|
this.contact = contact;
|
|
58
|
-
this.taskCollection = {};
|
|
59
98
|
this.webCallingService = webCallingService;
|
|
60
99
|
this.webSocketManager = webSocketManager;
|
|
61
|
-
this.
|
|
100
|
+
this.rtdWebSocketManager = rtdWebSocketManager;
|
|
101
|
+
this.taskCollection = {};
|
|
102
|
+
this.webRtcEnabled = false;
|
|
103
|
+
|
|
62
104
|
this.registerTaskListeners();
|
|
63
105
|
this.registerIncomingCallEvent();
|
|
64
106
|
}
|
|
65
107
|
|
|
66
|
-
public setWrapupData(wrapupData: WrapupData) {
|
|
67
|
-
this.wrapupData = wrapupData;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
public setAgentId(agentId: string) {
|
|
71
|
-
this.agentId = agentId;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Gets the current agent ID
|
|
76
|
-
* @returns {string} The agent ID set for this task manager instance
|
|
77
|
-
* @public
|
|
78
|
-
*/
|
|
79
|
-
public getAgentId(): string {
|
|
80
|
-
return this.agentId;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public setWebRtcEnabled(webRtcEnabled: boolean) {
|
|
84
|
-
this.webRtcEnabled = webRtcEnabled;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
108
|
public handleRealtimeWebsocketEvent(event: string) {
|
|
88
109
|
try {
|
|
89
110
|
const payload = JSON.parse(event);
|
|
90
111
|
|
|
91
|
-
const eventType = payload?.type || payload?.data?.notifType;
|
|
92
112
|
const interactionId = payload?.data?.data?.conversationId;
|
|
93
|
-
if (!
|
|
113
|
+
if (!interactionId) return;
|
|
94
114
|
|
|
95
115
|
const task = this.taskCollection[interactionId];
|
|
96
116
|
if (!task) {
|
|
@@ -103,7 +123,12 @@ export default class TaskManager extends EventEmitter {
|
|
|
103
123
|
return;
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
|
|
126
|
+
switch (payload.type) {
|
|
127
|
+
case CC_EVENTS.REAL_TIME_TRANSCRIPTION:
|
|
128
|
+
case CC_EVENTS.SUGGESTED_RESPONSE:
|
|
129
|
+
task.emit(payload.type, payload.data);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
107
132
|
} catch (error) {
|
|
108
133
|
LoggerProxy.error('Failed to parse RTD WebSocket message', {
|
|
109
134
|
module: TASK_MANAGER_FILE,
|
|
@@ -113,6 +138,44 @@ export default class TaskManager extends EventEmitter {
|
|
|
113
138
|
}
|
|
114
139
|
}
|
|
115
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Set config flags for task creation
|
|
143
|
+
*/
|
|
144
|
+
public setConfigFlags(configFlags: ConfigFlags) {
|
|
145
|
+
this.configFlags = configFlags;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set wrapup configuration data
|
|
150
|
+
*/
|
|
151
|
+
public setWrapupData(wrapupData: WrapupData) {
|
|
152
|
+
this.wrapupData = wrapupData;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Set agent ID for task operations
|
|
157
|
+
*/
|
|
158
|
+
public setAgentId(agentId: string) {
|
|
159
|
+
this.agentId = agentId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gets the current agent ID
|
|
164
|
+
* @returns {string} The agent ID set for this task manager instance
|
|
165
|
+
* @public
|
|
166
|
+
*/
|
|
167
|
+
public getAgentId(): string {
|
|
168
|
+
return this.agentId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public setWebRtcEnabled(webRtcEnabled: boolean) {
|
|
172
|
+
this.webRtcEnabled = webRtcEnabled;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public setAnswerCallOnWebexService(answerCallOnWebexService: AnswerCallOnWebexService) {
|
|
176
|
+
this.answerCallOnWebexService = answerCallOnWebexService;
|
|
177
|
+
}
|
|
178
|
+
|
|
116
179
|
private handleIncomingWebCall = (call: ICall) => {
|
|
117
180
|
const currentTask = Object.values(this.taskCollection).find(
|
|
118
181
|
(task) =>
|
|
@@ -126,7 +189,15 @@ export default class TaskManager extends EventEmitter {
|
|
|
126
189
|
method: METHODS.HANDLE_INCOMING_WEB_CALL,
|
|
127
190
|
interactionId: currentTask.data.interactionId,
|
|
128
191
|
});
|
|
129
|
-
|
|
192
|
+
|
|
193
|
+
// Send TASK_INCOMING to state machine - it will emit on the task object
|
|
194
|
+
const eventPayload = TaskManager.mapEventToTaskStateMachineEvent(
|
|
195
|
+
CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
196
|
+
currentTask.data
|
|
197
|
+
);
|
|
198
|
+
if (eventPayload && currentTask) {
|
|
199
|
+
currentTask.sendStateMachineEvent(eventPayload);
|
|
200
|
+
}
|
|
130
201
|
}
|
|
131
202
|
this.call = call;
|
|
132
203
|
};
|
|
@@ -139,553 +210,1249 @@ export default class TaskManager extends EventEmitter {
|
|
|
139
210
|
this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
|
|
140
211
|
}
|
|
141
212
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
LoggerProxy.log(`Got AGENT_CONTACT: Task already exists in collection`, {
|
|
163
|
-
module: TASK_MANAGER_FILE,
|
|
164
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
165
|
-
interactionId: payload.data.interactionId,
|
|
166
|
-
});
|
|
167
|
-
break;
|
|
168
|
-
} else if (!this.taskCollection[payload.data.interactionId]) {
|
|
169
|
-
// Case2 : Task is not present in taskCollection
|
|
170
|
-
LoggerProxy.log(`Got AGENT_CONTACT : Creating new task in taskManager`, {
|
|
171
|
-
module: TASK_MANAGER_FILE,
|
|
172
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
173
|
-
interactionId: payload.data.interactionId,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Check if auto-answer should happen for this task
|
|
177
|
-
const shouldAutoAnswer = shouldAutoAnswerTask(
|
|
178
|
-
payload.data,
|
|
179
|
-
this.agentId,
|
|
180
|
-
this.webCallingService.loginOption,
|
|
181
|
-
this.webRtcEnabled
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
task = new Task(
|
|
185
|
-
this.contact,
|
|
186
|
-
this.webCallingService,
|
|
187
|
-
{
|
|
188
|
-
...payload.data,
|
|
189
|
-
wrapUpRequired:
|
|
190
|
-
payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
|
|
191
|
-
isConferenceInProgress: getIsConferenceInProgress(payload.data),
|
|
192
|
-
isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
|
|
193
|
-
},
|
|
194
|
-
this.wrapupData,
|
|
195
|
-
this.agentId
|
|
196
|
-
);
|
|
197
|
-
this.taskCollection[payload.data.interactionId] = task;
|
|
198
|
-
// Condition 1: The state is=new i.e it is a incoming task
|
|
199
|
-
if (payload.data.interaction.state === 'new') {
|
|
200
|
-
LoggerProxy.log(
|
|
201
|
-
`Got AGENT_CONTACT for a task with state=new, sending TASK_INCOMING event`,
|
|
202
|
-
{
|
|
203
|
-
module: TASK_MANAGER_FILE,
|
|
204
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
205
|
-
interactionId: payload.data.interactionId,
|
|
206
|
-
}
|
|
207
|
-
);
|
|
208
|
-
this.emit(TASK_EVENTS.TASK_INCOMING, task);
|
|
209
|
-
} else {
|
|
210
|
-
// Condition 2: The state is anything else i.e the task was connected
|
|
211
|
-
LoggerProxy.log(
|
|
212
|
-
`Got AGENT_CONTACT for a task with state=${payload.data.interaction.state}, sending TASK_HYDRATE event`,
|
|
213
|
-
{
|
|
214
|
-
module: TASK_MANAGER_FILE,
|
|
215
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
216
|
-
interactionId: payload.data.interactionId,
|
|
217
|
-
}
|
|
218
|
-
);
|
|
219
|
-
this.emit(TASK_EVENTS.TASK_HYDRATE, task);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
|
|
224
|
-
case CC_EVENTS.AGENT_CONTACT_RESERVED: {
|
|
225
|
-
// Check if auto-answer should happen for this task
|
|
226
|
-
const shouldAutoAnswerReserved = shouldAutoAnswerTask(
|
|
227
|
-
payload.data,
|
|
228
|
-
this.agentId,
|
|
229
|
-
this.webCallingService.loginOption,
|
|
230
|
-
this.webRtcEnabled
|
|
231
|
-
);
|
|
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
|
+
}
|
|
232
233
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
task = this.updateTaskData(task, payload.data);
|
|
381
|
-
task.emit(TASK_EVENTS.TASK_HOLD, task);
|
|
382
|
-
break;
|
|
383
|
-
case CC_EVENTS.AGENT_CONTACT_UNHELD:
|
|
384
|
-
// As soon as the main interaction is unheld, we need to emit TASK_RESUME
|
|
385
|
-
task = this.updateTaskData(task, payload.data);
|
|
386
|
-
task.emit(TASK_EVENTS.TASK_RESUME, task);
|
|
387
|
-
break;
|
|
388
|
-
case CC_EVENTS.AGENT_VTEAM_TRANSFERRED:
|
|
389
|
-
task = this.updateTaskData(task, {
|
|
390
|
-
...payload.data,
|
|
391
|
-
wrapUpRequired: true,
|
|
392
|
-
});
|
|
393
|
-
task.emit(TASK_EVENTS.TASK_END, task);
|
|
394
|
-
break;
|
|
395
|
-
case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
|
|
396
|
-
task = this.updateTaskData(task, payload.data);
|
|
397
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, task);
|
|
398
|
-
break;
|
|
399
|
-
case CC_EVENTS.AGENT_CONSULT_CREATED:
|
|
400
|
-
// Received when self agent initiates a consult
|
|
401
|
-
task = this.updateTaskData(task, {
|
|
402
|
-
...payload.data,
|
|
403
|
-
isConsulted: false, // This ensures that the task consult status is always reset
|
|
404
|
-
});
|
|
405
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_CREATED, task);
|
|
406
|
-
break;
|
|
407
|
-
case CC_EVENTS.AGENT_OFFER_CONSULT:
|
|
408
|
-
// Received when other agent sends us a consult offer
|
|
409
|
-
task = this.updateTaskData(task, {
|
|
410
|
-
...payload.data,
|
|
411
|
-
isConsulted: true, // This ensures that the task is marked as us being requested for a consult
|
|
412
|
-
});
|
|
413
|
-
task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);
|
|
414
|
-
|
|
415
|
-
// Handle auto-answer for consult offer
|
|
416
|
-
this.handleAutoAnswer(task);
|
|
417
|
-
break;
|
|
418
|
-
case CC_EVENTS.AGENT_CONSULTING:
|
|
419
|
-
// Received when agent is in an active consult state
|
|
420
|
-
// TODO: Check if we can use backend consult state instead of isConsulted
|
|
421
|
-
task = this.updateTaskData(task, payload.data);
|
|
422
|
-
if (task.data.isConsulted) {
|
|
423
|
-
// Fire only if you are the agent who received the consult request
|
|
424
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, task);
|
|
425
|
-
} else {
|
|
426
|
-
// Fire only if you are the agent who initiated the consult
|
|
427
|
-
task.emit(TASK_EVENTS.TASK_CONSULTING, task);
|
|
428
|
-
}
|
|
429
|
-
break;
|
|
430
|
-
case CC_EVENTS.AGENT_CONSULT_FAILED:
|
|
431
|
-
// This can only be received by the agent who initiated the consult.
|
|
432
|
-
// We need not emit any event here since this will be result of promise
|
|
433
|
-
task = this.updateTaskData(task, payload.data);
|
|
434
|
-
break;
|
|
435
|
-
case CC_EVENTS.AGENT_CONSULT_ENDED:
|
|
436
|
-
task = this.updateTaskData(task, payload.data);
|
|
437
|
-
if (task.data.isConsulted) {
|
|
438
|
-
// This will be the end state of the task as soon as we end the consult in case of
|
|
439
|
-
// us being offered a consult
|
|
440
|
-
this.removeTaskFromCollection(task);
|
|
441
|
-
}
|
|
442
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_END, task);
|
|
443
|
-
break;
|
|
444
|
-
case CC_EVENTS.AGENT_CTQ_CANCELLED:
|
|
445
|
-
// This event is received when the consult using queue is cancelled using API
|
|
446
|
-
task = this.updateTaskData(task, payload.data);
|
|
447
|
-
task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, task);
|
|
448
|
-
break;
|
|
449
|
-
case CC_EVENTS.AGENT_WRAPUP:
|
|
450
|
-
task = this.updateTaskData(task, {...payload.data, wrapUpRequired: true});
|
|
451
|
-
task.emit(TASK_EVENTS.TASK_END, task);
|
|
452
|
-
break;
|
|
453
|
-
case CC_EVENTS.AGENT_WRAPPEDUP:
|
|
454
|
-
task.cancelAutoWrapupTimer();
|
|
455
|
-
this.removeTaskFromCollection(task);
|
|
456
|
-
task.emit(TASK_EVENTS.TASK_WRAPPEDUP, task);
|
|
457
|
-
break;
|
|
458
|
-
case CC_EVENTS.CONTACT_RECORDING_PAUSED:
|
|
459
|
-
task = this.updateTaskData(task, payload.data);
|
|
460
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_PAUSED, task);
|
|
461
|
-
break;
|
|
462
|
-
case CC_EVENTS.CONTACT_RECORDING_PAUSE_FAILED:
|
|
463
|
-
task = this.updateTaskData(task, payload.data);
|
|
464
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED, task);
|
|
465
|
-
break;
|
|
466
|
-
case CC_EVENTS.CONTACT_RECORDING_RESUMED:
|
|
467
|
-
task = this.updateTaskData(task, payload.data);
|
|
468
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_RESUMED, task);
|
|
469
|
-
break;
|
|
470
|
-
case CC_EVENTS.CONTACT_RECORDING_RESUME_FAILED:
|
|
471
|
-
task = this.updateTaskData(task, payload.data);
|
|
472
|
-
task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
|
|
473
|
-
break;
|
|
474
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
|
|
475
|
-
// Conference is being established - update task state and emit establishing event
|
|
476
|
-
task = this.updateTaskData(task, payload.data);
|
|
477
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, task);
|
|
478
|
-
break;
|
|
479
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
|
|
480
|
-
// Conference started successfully - update task state and emit event
|
|
481
|
-
task = this.updateTaskData(task, payload.data);
|
|
482
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
|
|
483
|
-
break;
|
|
484
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
|
|
485
|
-
// Conference failed - update task state and emit failure event
|
|
486
|
-
task = this.updateTaskData(task, payload.data);
|
|
487
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_FAILED, task);
|
|
488
|
-
break;
|
|
489
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
|
|
490
|
-
// Conference ended - update task state and emit event
|
|
491
|
-
task = this.updateTaskData(task, payload.data);
|
|
492
|
-
if (
|
|
493
|
-
!task ||
|
|
494
|
-
isPrimary(task, this.agentId) ||
|
|
495
|
-
isParticipantInMainInteraction(task, this.agentId)
|
|
496
|
-
) {
|
|
497
|
-
LoggerProxy.log('Primary or main interaction participant leaving conference');
|
|
498
|
-
} else {
|
|
499
|
-
this.removeTaskFromCollection(task);
|
|
500
|
-
}
|
|
501
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
|
|
502
|
-
break;
|
|
503
|
-
case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
|
|
504
|
-
task = this.updateTaskData(task, {
|
|
505
|
-
...payload.data,
|
|
506
|
-
isConferenceInProgress: getIsConferenceInProgress(payload.data),
|
|
507
|
-
});
|
|
508
|
-
task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
|
|
512
|
-
// Conference ended - update task state and emit event
|
|
513
|
-
|
|
514
|
-
task = this.updateTaskData(task, {
|
|
515
|
-
...payload.data,
|
|
516
|
-
isConferenceInProgress: getIsConferenceInProgress(payload.data),
|
|
517
|
-
});
|
|
518
|
-
if (checkParticipantNotInInteraction(task, this.agentId)) {
|
|
519
|
-
if (
|
|
520
|
-
isParticipantInMainInteraction(task, this.agentId) ||
|
|
521
|
-
isPrimary(task, this.agentId)
|
|
522
|
-
) {
|
|
523
|
-
LoggerProxy.log('Primary or main interaction participant leaving conference');
|
|
524
|
-
} else {
|
|
525
|
-
this.removeTaskFromCollection(task);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED:
|
|
532
|
-
// Conference exit failed - update task state and emit failure event
|
|
533
|
-
task = this.updateTaskData(task, payload.data);
|
|
534
|
-
task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, task);
|
|
535
|
-
break;
|
|
536
|
-
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_END_FAILED:
|
|
537
|
-
// Conference end failed - update task state with error details and emit failure event
|
|
538
|
-
task = this.updateTaskData(task, payload.data);
|
|
539
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, task);
|
|
540
|
-
break;
|
|
541
|
-
case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
|
|
542
|
-
// Conference was transferred - update task state and emit transfer success event
|
|
543
|
-
// Note: Backend should provide hasLeft and wrapUpRequired status
|
|
544
|
-
task = this.updateTaskData(task, payload.data);
|
|
545
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, task);
|
|
546
|
-
break;
|
|
547
|
-
case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
|
|
548
|
-
// Conference transfer failed - update task state with error details and emit failure event
|
|
549
|
-
task = this.updateTaskData(task, payload.data);
|
|
550
|
-
task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
|
|
551
|
-
break;
|
|
552
|
-
case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
|
|
553
|
-
// Post-call activity for participant - update task state with activity details
|
|
554
|
-
task = this.updateTaskData(task, payload.data);
|
|
555
|
-
task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
|
|
556
|
-
break;
|
|
557
|
-
case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: {
|
|
558
|
-
// Campaign preview contact offered to agent
|
|
559
|
-
// Create a task in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED
|
|
560
|
-
// after acceptPreviewContact) can find and update it.
|
|
561
|
-
// Emit TASK_CAMPAIGN_PREVIEW_RESERVATION instead of TASK_INCOMING so the call
|
|
562
|
-
// does not ring out to the customer before the agent explicitly accepts the preview contact.
|
|
563
|
-
LoggerProxy.log('Campaign preview reservation received', {
|
|
564
|
-
module: TASK_MANAGER_FILE,
|
|
565
|
-
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
566
|
-
interactionId: payload.data.interactionId,
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
if (!this.taskCollection[payload.data.interactionId]) {
|
|
570
|
-
task = new Task(
|
|
571
|
-
this.contact,
|
|
572
|
-
this.webCallingService,
|
|
573
|
-
{
|
|
574
|
-
...payload.data,
|
|
575
|
-
wrapUpRequired: false,
|
|
576
|
-
isConferenceInProgress: false,
|
|
577
|
-
isAutoAnswering: false,
|
|
578
|
-
},
|
|
579
|
-
this.wrapupData,
|
|
580
|
-
this.agentId
|
|
581
|
-
);
|
|
582
|
-
this.taskCollection[payload.data.interactionId] = task;
|
|
583
|
-
} else {
|
|
584
|
-
task = this.updateTaskData(task, payload.data);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, task);
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
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
|
+
}
|
|
590
381
|
|
|
591
|
-
|
|
592
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Map WebSocket CC_EVENTS to state machine TaskEvent
|
|
384
|
+
* @param ccEvent - The CC_EVENT type from WebSocket
|
|
385
|
+
* @param payload - The event payload
|
|
386
|
+
* @param agentId - Optional agent ID for state detection (needed for HYDRATE)
|
|
387
|
+
* @param task - Optional task instance for state-aware CONTACT_ENDED mapping
|
|
388
|
+
* @returns TaskEventPayload for state machine or null if no mapping
|
|
389
|
+
*/
|
|
390
|
+
private static mapEventToTaskStateMachineEvent(
|
|
391
|
+
ccEvent: CC_EVENTS,
|
|
392
|
+
payload: WebSocketPayload,
|
|
393
|
+
agentId?: string,
|
|
394
|
+
task?: ITask,
|
|
395
|
+
enableWxBetterTogether?: boolean
|
|
396
|
+
): TaskEventPayload | null {
|
|
397
|
+
const mediaResourceId =
|
|
398
|
+
payload.mediaResourceId ||
|
|
399
|
+
payload.interaction?.media?.[payload.interactionId]?.mediaResourceId;
|
|
400
|
+
|
|
401
|
+
switch (ccEvent) {
|
|
402
|
+
// CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
|
|
403
|
+
case CC_EVENTS.AGENT_CONTACT_RESERVED: // AgentContactReserved -> TASK_INCOMING
|
|
404
|
+
if (isCampaignPreviewTask(payload)) {
|
|
405
|
+
return {
|
|
406
|
+
type: TaskEvent.TASK_INCOMING,
|
|
407
|
+
taskData: payload,
|
|
408
|
+
isCampaignReservationAccept: true,
|
|
409
|
+
};
|
|
593
410
|
}
|
|
594
|
-
|
|
595
|
-
|
|
411
|
+
|
|
412
|
+
return {type: TaskEvent.TASK_INCOMING, taskData: payload};
|
|
413
|
+
|
|
414
|
+
case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: // -> TASK_INCOMING (campaign branch via guard)
|
|
415
|
+
return {
|
|
416
|
+
type: TaskEvent.TASK_INCOMING,
|
|
417
|
+
taskData: payload,
|
|
418
|
+
isCampaignReservationAccept: true,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
case CC_EVENTS.AGENT_OFFER_CONTACT: // AgentOfferContact -> TASK_OFFERED
|
|
422
|
+
return {type: TaskEvent.TASK_OFFERED, taskData: payload};
|
|
423
|
+
|
|
424
|
+
case CC_EVENTS.AGENT_CONTACT: // AgentContact -> HYDRATE
|
|
425
|
+
// Include agentId for state detection (e.g., checking isWrapUp in participant data)
|
|
426
|
+
return {type: TaskEvent.HYDRATE, taskData: payload, agentId};
|
|
427
|
+
|
|
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};
|
|
596
436
|
}
|
|
597
437
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
task?.data?.interactionId;
|
|
438
|
+
return {type: TaskEvent.CONTACT_UPDATED, taskData: payload};
|
|
439
|
+
case CC_EVENTS.CONTACT_OWNER_CHANGED:
|
|
440
|
+
return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
|
|
602
441
|
|
|
603
|
-
|
|
604
|
-
|
|
442
|
+
case CC_EVENTS.AGENT_OFFER_CONSULT: // AgentOfferConsult -> OFFER_CONSULT
|
|
443
|
+
return {
|
|
444
|
+
type: TaskEvent.OFFER_CONSULT,
|
|
445
|
+
taskData: {...payload, isConsulted: true},
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
case CC_EVENTS.AGENT_CONTACT_ASSIGNED: // AgentContactAssigned -> ASSIGN
|
|
449
|
+
return {type: TaskEvent.ASSIGN, taskData: payload};
|
|
450
|
+
|
|
451
|
+
case CC_EVENTS.AGENT_CONTACT_HELD:
|
|
452
|
+
return {
|
|
453
|
+
type: TaskEvent.HOLD_SUCCESS,
|
|
454
|
+
mediaResourceId: mediaResourceId || '',
|
|
455
|
+
taskData: payload,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
case CC_EVENTS.AGENT_CONTACT_UNHELD:
|
|
459
|
+
return {
|
|
460
|
+
type: TaskEvent.UNHOLD_SUCCESS,
|
|
461
|
+
mediaResourceId: mediaResourceId || '',
|
|
462
|
+
taskData: payload,
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
case CC_EVENTS.AGENT_CONSULT_CREATED:
|
|
466
|
+
return {
|
|
467
|
+
type: TaskEvent.CONSULT_CREATED,
|
|
468
|
+
taskData: {...payload, isConsulted: false},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
case CC_EVENTS.AGENT_CONSULTING: // AgentConsulting -> CONSULTING_ACTIVE
|
|
472
|
+
// use context to figure out if it's the initiator or receiver using consultInitiator from context
|
|
473
|
+
return {
|
|
474
|
+
type: TaskEvent.CONSULTING_ACTIVE,
|
|
475
|
+
consultDestinationAgentJoined: true,
|
|
476
|
+
taskData: payload,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
case CC_EVENTS.AGENT_CONSULT_ENDED: // AgentConsultEnded -> CONSULT_END
|
|
480
|
+
return {type: TaskEvent.CONSULT_END, taskData: payload};
|
|
481
|
+
|
|
482
|
+
case CC_EVENTS.AGENT_CONSULT_FAILED:
|
|
483
|
+
case CC_EVENTS.AGENT_CTQ_FAILED:
|
|
484
|
+
return {type: TaskEvent.CONSULT_FAILED, reason: payload.reason, taskData: payload};
|
|
485
|
+
|
|
486
|
+
case CC_EVENTS.AGENT_CTQ_CANCELLED:
|
|
487
|
+
return {type: TaskEvent.CTQ_CANCEL, taskData: payload};
|
|
488
|
+
|
|
489
|
+
case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
|
|
490
|
+
return {type: TaskEvent.CTQ_CANCEL_FAILED, taskData: payload};
|
|
491
|
+
|
|
492
|
+
case CC_EVENTS.AGENT_BLIND_TRANSFERRED: // AgentBlindTransferred -> TRANSFER_SUCCESS
|
|
493
|
+
case CC_EVENTS.AGENT_CONSULT_TRANSFERRED: // AgentConsultTransferred -> TRANSFER_SUCCESS
|
|
494
|
+
case CC_EVENTS.AGENT_VTEAM_TRANSFERRED: // AgentVTeamTransferred -> TRANSFER_SUCCESS
|
|
495
|
+
return {
|
|
496
|
+
type: TaskEvent.TRANSFER_SUCCESS,
|
|
497
|
+
taskData: payload,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
case CC_EVENTS.AGENT_WRAPUP:
|
|
501
|
+
return {type: TaskEvent.TASK_WRAPUP, taskData: {...payload, wrapUpRequired: true}};
|
|
502
|
+
case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
|
|
503
|
+
return null; // Add WRAPUP if needed
|
|
504
|
+
|
|
505
|
+
case CC_EVENTS.AGENT_BLIND_TRANSFER_FAILED:
|
|
506
|
+
case CC_EVENTS.AGENT_VTEAM_TRANSFER_FAILED:
|
|
507
|
+
case CC_EVENTS.AGENT_CONSULT_TRANSFER_FAILED:
|
|
508
|
+
case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
|
|
509
|
+
return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
|
|
510
|
+
|
|
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
|
+
};
|
|
605
534
|
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
type: TaskEvent.CONTACT_ENDED,
|
|
538
|
+
taskData: {
|
|
539
|
+
...payload,
|
|
540
|
+
wrapUpRequired,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
606
543
|
}
|
|
607
|
-
|
|
544
|
+
|
|
545
|
+
case CC_EVENTS.AGENT_INVITE_FAILED:
|
|
546
|
+
return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
|
|
547
|
+
|
|
548
|
+
case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
|
|
549
|
+
return {type: TaskEvent.ASSIGN_FAILED, reason: payload.reason};
|
|
550
|
+
|
|
551
|
+
case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
|
|
552
|
+
return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
|
|
553
|
+
|
|
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
|
+
}
|
|
571
|
+
|
|
572
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
|
|
573
|
+
return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
|
|
574
|
+
|
|
575
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
|
|
576
|
+
return {type: TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED, taskData: payload};
|
|
577
|
+
|
|
578
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
|
|
579
|
+
return {type: TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED, taskData: payload};
|
|
580
|
+
|
|
581
|
+
case CC_EVENTS.CONTACT_RECORDING_STARTED:
|
|
582
|
+
return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
|
|
583
|
+
|
|
584
|
+
case CC_EVENTS.CONTACT_RECORDING_PAUSED:
|
|
585
|
+
return {type: TaskEvent.PAUSE_RECORDING, taskData: payload};
|
|
586
|
+
|
|
587
|
+
case CC_EVENTS.CONTACT_RECORDING_RESUMED:
|
|
588
|
+
return {type: TaskEvent.RESUME_RECORDING, taskData: payload};
|
|
589
|
+
|
|
590
|
+
case CC_EVENTS.AGENT_WRAPPEDUP:
|
|
591
|
+
return {type: TaskEvent.WRAPUP_COMPLETE, taskData: payload};
|
|
592
|
+
|
|
593
|
+
// Conference events - these trigger state machine transition to CONFERENCING
|
|
594
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
|
|
595
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
|
|
596
|
+
case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE:
|
|
597
|
+
return {type: TaskEvent.CONFERENCE_START, taskData: payload};
|
|
598
|
+
|
|
599
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
|
|
600
|
+
return {type: TaskEvent.CONFERENCE_FAILED, reason: payload.reason, taskData: payload};
|
|
601
|
+
|
|
602
|
+
case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
|
|
603
|
+
return {type: TaskEvent.CONFERENCE_END, taskData: payload};
|
|
604
|
+
|
|
605
|
+
case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE:
|
|
606
|
+
return {
|
|
607
|
+
type: TaskEvent.PARTICIPANT_LEAVE,
|
|
608
|
+
taskData: payload,
|
|
609
|
+
participantId: payload?.participantId,
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
|
|
613
|
+
return {type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: payload};
|
|
614
|
+
|
|
615
|
+
default:
|
|
616
|
+
// Not all events need state machine mapping
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
608
619
|
}
|
|
609
620
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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;
|
|
613
638
|
}
|
|
614
639
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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;
|
|
620
676
|
}
|
|
621
677
|
|
|
678
|
+
const interactionWrapUp = payload.interaction?.state === 'wrapUp';
|
|
679
|
+
|
|
680
|
+
return wrapUpRequired || interactionWrapUp || wxAppAnswerPending;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Register WebSocket message listeners for task events
|
|
685
|
+
*
|
|
686
|
+
* Main entry point that orchestrates event processing through a clear pipeline:
|
|
687
|
+
* 1. Parse and validate incoming WebSocket messages
|
|
688
|
+
* 2. Prepare event context with task and state machine mappings
|
|
689
|
+
* 3. Handle task lifecycle (creation, updates, collection management)
|
|
690
|
+
* 4. Send events to state machine (task-level transitions/emissions)
|
|
691
|
+
* 5. Cleanup is triggered via task events emitted by the state machine
|
|
692
|
+
*
|
|
693
|
+
* This architecture separates concerns:
|
|
694
|
+
* - TaskManager: Manages task collection lifecycle and operational concerns
|
|
695
|
+
* - State Machine: Manages individual task state and event emissions
|
|
696
|
+
*/
|
|
697
|
+
private registerTaskListeners() {
|
|
698
|
+
this.webSocketManager.on('message', (event) => {
|
|
699
|
+
// Step 1: Parse and validate the message
|
|
700
|
+
const message = TaskManager.parseWebSocketMessage(event);
|
|
701
|
+
if (!message) return;
|
|
702
|
+
|
|
703
|
+
// Step 2: Prepare event context
|
|
704
|
+
const eventContext = this.prepareEventContext(message);
|
|
705
|
+
if (!eventContext) return;
|
|
706
|
+
|
|
707
|
+
const actions = this.handleTaskLifecycleEvent(eventContext);
|
|
708
|
+
|
|
709
|
+
const {task} = actions;
|
|
710
|
+
if (!task) return;
|
|
711
|
+
|
|
712
|
+
const {payload, stateMachineEvent} = eventContext;
|
|
713
|
+
|
|
714
|
+
// Always keep task.data updated (even for mapped events) so consumers relying
|
|
715
|
+
// on TaskManager-managed task instances see the latest payload.
|
|
716
|
+
if (payload) {
|
|
717
|
+
this.updateTaskData(task, payload);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Send event to state machine - this will trigger all TASK_EVENTS emissions
|
|
721
|
+
// including TASK_INCOMING which is now handled via the state machine callbacks
|
|
722
|
+
if (stateMachineEvent) {
|
|
723
|
+
task.sendStateMachineEvent(stateMachineEvent);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Emit TASK_POST_CALL_ACTIVITY for ParticipantPostCallActivity events so
|
|
727
|
+
// consumers (Widgets) can detect the interaction state change to post_call.
|
|
728
|
+
if (eventContext.eventType === CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY) {
|
|
729
|
+
task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Send transcript start/stop events for relevant CC events
|
|
733
|
+
this.requestRealTimeTranscripts(eventContext.eventType, payload.interactionId);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Parse and validate WebSocket message
|
|
739
|
+
* @returns Parsed message or null if invalid/keepalive
|
|
740
|
+
*/
|
|
741
|
+
private static parseWebSocketMessage(event: string): WebSocketMessage | null {
|
|
622
742
|
try {
|
|
623
|
-
const
|
|
624
|
-
|
|
743
|
+
const payload = JSON.parse(event) as WebSocketMessage;
|
|
744
|
+
|
|
745
|
+
// Filter out keepalive messages
|
|
746
|
+
if (payload?.keepalive === 'true' || payload?.keepalive === true) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Normalize task data if present
|
|
751
|
+
if (payload?.data?.interaction) {
|
|
752
|
+
payload.data = normalizeTaskData(payload.data);
|
|
753
|
+
}
|
|
625
754
|
|
|
626
|
-
return
|
|
755
|
+
return payload;
|
|
627
756
|
} catch (error) {
|
|
628
|
-
LoggerProxy.error(
|
|
757
|
+
LoggerProxy.error('Failed to parse WebSocket message', {
|
|
629
758
|
module: TASK_MANAGER_FILE,
|
|
630
|
-
method:
|
|
631
|
-
|
|
759
|
+
method: 'parseWebSocketMessage',
|
|
760
|
+
error,
|
|
632
761
|
});
|
|
633
762
|
|
|
634
|
-
return
|
|
763
|
+
return null;
|
|
635
764
|
}
|
|
636
765
|
}
|
|
637
766
|
|
|
638
767
|
/**
|
|
639
|
-
*
|
|
640
|
-
* @
|
|
641
|
-
* @param taskData - The task data from the event payload
|
|
642
|
-
* @returns Updated or newly created task
|
|
643
|
-
* @private
|
|
768
|
+
* Prepare context for event processing
|
|
769
|
+
* @returns Event context or null if event type is invalid
|
|
644
770
|
*/
|
|
645
|
-
private
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
771
|
+
private prepareEventContext(message: WebSocketMessage): EventContext | null {
|
|
772
|
+
const eventType = message.data?.type || message.type;
|
|
773
|
+
|
|
774
|
+
if (!eventType || !isCcEvent(eventType)) {
|
|
775
|
+
return null;
|
|
649
776
|
}
|
|
650
777
|
|
|
651
|
-
|
|
652
|
-
|
|
778
|
+
const interactionId = message.data.interactionId;
|
|
779
|
+
const reservationInteractionId = message.data.reservationInteractionId;
|
|
780
|
+
let task = this.taskCollection[interactionId];
|
|
781
|
+
let ownerChangeResolvedByRelatedInteraction = false;
|
|
782
|
+
|
|
783
|
+
// When a campaign preview contact is accepted, the assigned event may arrive
|
|
784
|
+
// with a new interactionId while the task is stored under the original
|
|
785
|
+
// reservationInteractionId. Fall back to that key so the task is found.
|
|
786
|
+
if (!task && reservationInteractionId) {
|
|
787
|
+
task = this.taskCollection[reservationInteractionId];
|
|
788
|
+
if (task) {
|
|
789
|
+
// Re-key the task under the new interaction ID and remove the old entry
|
|
790
|
+
delete this.taskCollection[reservationInteractionId];
|
|
791
|
+
this.taskCollection[interactionId] = task;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
796
|
+
const incomingShowsCurrentAgentPromotion = Boolean(
|
|
797
|
+
this.agentId &&
|
|
798
|
+
message.data.interaction?.owner === this.agentId &&
|
|
799
|
+
TaskManager.isParticipantActiveOnMainInteraction(message.data, this.agentId)
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
task = this.findUniqueTaskByRelatedInteraction(
|
|
803
|
+
message.data,
|
|
804
|
+
(candidate) => {
|
|
805
|
+
const currentAgentId = this.agentId || candidate.data?.agentId;
|
|
806
|
+
|
|
807
|
+
return Boolean(
|
|
808
|
+
currentAgentId &&
|
|
809
|
+
(TaskManager.isParticipantActiveOnMainInteraction(candidate.data, currentAgentId) ||
|
|
810
|
+
incomingShowsCurrentAgentPromotion)
|
|
811
|
+
);
|
|
812
|
+
},
|
|
813
|
+
{includeCollectionKeys: true, includeMainCallMediaKeys: true}
|
|
814
|
+
);
|
|
815
|
+
ownerChangeResolvedByRelatedInteraction = Boolean(task);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
let ownerChangeMainInteractionId: string | undefined;
|
|
819
|
+
if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
820
|
+
const taskMainInteractionId = task.data?.interaction?.mainInteractionId;
|
|
821
|
+
const payloadMainInteractionId = message.data.interaction?.mainInteractionId;
|
|
822
|
+
const hasConflictingMainInteractionIds = Boolean(
|
|
823
|
+
taskMainInteractionId &&
|
|
824
|
+
payloadMainInteractionId &&
|
|
825
|
+
taskMainInteractionId !== payloadMainInteractionId
|
|
826
|
+
);
|
|
827
|
+
// A promoted child task can retain stale embedded IDs. Prefer the
|
|
828
|
+
// authoritative incoming main-call identity before local snapshot IDs.
|
|
829
|
+
const stableMainInteractionId =
|
|
830
|
+
payloadMainInteractionId ||
|
|
831
|
+
TaskManager.getMainCallMediaId(message.data.interaction) ||
|
|
832
|
+
TaskManager.getMainCallMediaId(task.data?.interaction) ||
|
|
833
|
+
taskMainInteractionId ||
|
|
834
|
+
message.data.interaction?.interactionId ||
|
|
835
|
+
task.data?.interaction?.interactionId ||
|
|
836
|
+
task.data?.interactionId;
|
|
837
|
+
const taskAtStableKey = stableMainInteractionId
|
|
838
|
+
? this.taskCollection[stableMainInteractionId]
|
|
839
|
+
: undefined;
|
|
840
|
+
|
|
841
|
+
if (
|
|
842
|
+
!stableMainInteractionId ||
|
|
843
|
+
hasConflictingMainInteractionIds ||
|
|
844
|
+
(taskAtStableKey && taskAtStableKey !== task)
|
|
845
|
+
) {
|
|
846
|
+
LoggerProxy.warn('Unable to safely correlate owner change to the main interaction task', {
|
|
847
|
+
module: TASK_MANAGER_FILE,
|
|
848
|
+
method: 'prepareEventContext',
|
|
849
|
+
interactionId:
|
|
850
|
+
payloadMainInteractionId || taskMainInteractionId || message.data.interactionId,
|
|
851
|
+
});
|
|
852
|
+
task = undefined;
|
|
853
|
+
} else if (ownerChangeResolvedByRelatedInteraction) {
|
|
854
|
+
ownerChangeMainInteractionId = stableMainInteractionId;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
|
|
859
|
+
task = this.findUniqueTaskByRelatedInteraction(message.data);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const wasConsultedTask = Boolean(task?.data?.isConsulted);
|
|
863
|
+
const computeWrapUpRequired = () => {
|
|
864
|
+
if (message.data.wrapUpRequired !== undefined) {
|
|
865
|
+
return message.data.wrapUpRequired;
|
|
866
|
+
}
|
|
867
|
+
if (message.data.isConsulted !== undefined) {
|
|
868
|
+
return !message.data.isConsulted;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return !wasConsultedTask;
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
let adjustedPayload: WebSocketPayload =
|
|
875
|
+
eventType === CC_EVENTS.AGENT_CONSULT_TRANSFERRED ||
|
|
876
|
+
eventType === CC_EVENTS.AGENT_BLIND_TRANSFERRED ||
|
|
877
|
+
eventType === CC_EVENTS.AGENT_VTEAM_TRANSFERRED
|
|
878
|
+
? {
|
|
879
|
+
...message.data,
|
|
880
|
+
wrapUpRequired: computeWrapUpRequired(),
|
|
881
|
+
}
|
|
882
|
+
: message.data;
|
|
883
|
+
|
|
884
|
+
if (task && ownerChangeMainInteractionId) {
|
|
885
|
+
adjustedPayload = {
|
|
886
|
+
...adjustedPayload,
|
|
887
|
+
interactionId: ownerChangeMainInteractionId,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (task && eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
|
|
892
|
+
adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (WRAP_UP_STAMP_EVENTS.has(eventType)) {
|
|
896
|
+
adjustedPayload = TaskManager.stampWrapUpRequiredForEvent(
|
|
897
|
+
eventType,
|
|
898
|
+
adjustedPayload,
|
|
899
|
+
this.agentId,
|
|
900
|
+
task
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
|
|
905
|
+
eventType,
|
|
906
|
+
adjustedPayload,
|
|
907
|
+
this.agentId,
|
|
908
|
+
task,
|
|
909
|
+
this.configFlags?.enableWxBetterTogether
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
LoggerProxy.info(`Handling task event ${eventType}`, {
|
|
913
|
+
module: TASK_MANAGER_FILE,
|
|
914
|
+
method: 'prepareEventContext',
|
|
915
|
+
interactionId,
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
eventType,
|
|
920
|
+
payload: adjustedPayload,
|
|
921
|
+
task,
|
|
922
|
+
stateMachineEvent,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private static isParticipantActiveOnMainInteraction(
|
|
927
|
+
taskData: TaskData | WebSocketPayload | undefined,
|
|
928
|
+
participantId: string
|
|
929
|
+
): boolean {
|
|
930
|
+
const interaction = taskData?.interaction;
|
|
931
|
+
const participant = interaction?.participants?.[participantId];
|
|
932
|
+
if (!participant || participant.hasLeft === true) return false;
|
|
933
|
+
|
|
934
|
+
return Object.values(interaction?.media ?? {}).some(
|
|
935
|
+
(media) =>
|
|
936
|
+
media?.mType === MEDIA_TYPE_MAIN_CALL && media.participants?.includes(participantId)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private static getMainCallMediaId(
|
|
941
|
+
interaction: TaskData['interaction'] | undefined
|
|
942
|
+
): string | undefined {
|
|
943
|
+
return Object.entries(interaction?.media ?? {}).find(
|
|
944
|
+
([, media]) => media?.mType === MEDIA_TYPE_MAIN_CALL
|
|
945
|
+
)?.[0];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private static getStableMainInteractionId(payload: WebSocketPayload): string | undefined {
|
|
949
|
+
return (
|
|
950
|
+
payload.interaction?.mainInteractionId ||
|
|
951
|
+
TaskManager.getMainCallMediaId(payload.interaction) ||
|
|
952
|
+
payload.interaction?.interactionId ||
|
|
953
|
+
payload.interactionId
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private canRecoverTaskFromContactOwnerChanged(payload: WebSocketPayload): boolean {
|
|
958
|
+
const interaction = payload.interaction;
|
|
959
|
+
const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
|
|
960
|
+
|
|
961
|
+
return Boolean(
|
|
962
|
+
this.agentId &&
|
|
963
|
+
stableInteractionId &&
|
|
964
|
+
interaction?.mediaType === MEDIA_CHANNEL.TELEPHONY &&
|
|
965
|
+
interaction.isTerminated !== true &&
|
|
966
|
+
interaction.owner === this.agentId &&
|
|
967
|
+
TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* ParticipantLeftConference may arrive after an owner update while still carrying
|
|
973
|
+
* the departed owner. Keep the confirmed owner only when the new roster proves that
|
|
974
|
+
* owner is active on mainCall and identifies the incoming owner as departed.
|
|
975
|
+
*/
|
|
976
|
+
private static preserveConfirmedOwner(task: ITask, payload: WebSocketPayload): WebSocketPayload {
|
|
977
|
+
const confirmedOwner = task.data?.interaction?.owner;
|
|
978
|
+
const incomingOwner = payload.interaction?.owner;
|
|
979
|
+
if (!confirmedOwner || !incomingOwner || confirmedOwner === incomingOwner) {
|
|
980
|
+
return payload;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const confirmedOwnerIsActive = TaskManager.isParticipantActiveOnMainInteraction(
|
|
984
|
+
payload,
|
|
985
|
+
confirmedOwner
|
|
986
|
+
);
|
|
987
|
+
const incomingOwnerIsExplicitlyDeparted =
|
|
988
|
+
payload.participantId === incomingOwner ||
|
|
989
|
+
payload.interaction?.participants?.[incomingOwner]?.hasLeft === true;
|
|
990
|
+
|
|
991
|
+
if (!confirmedOwnerIsActive || !incomingOwnerIsExplicitlyDeparted) {
|
|
992
|
+
return payload;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return {
|
|
996
|
+
...payload,
|
|
997
|
+
owner: confirmedOwner,
|
|
998
|
+
interaction: {...payload.interaction, owner: confirmedOwner},
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Resolve lifecycle events that are emitted for the main interaction while the
|
|
1004
|
+
* local task can still be indexed by a child consult interaction. Exact task
|
|
1005
|
+
* keys are handled before this fallback. Multiple aliases of the same task are
|
|
1006
|
+
* treated as one candidate; genuinely ambiguous matches are intentionally ignored.
|
|
1007
|
+
*/
|
|
1008
|
+
private findUniqueTaskByRelatedInteraction(
|
|
1009
|
+
payload: WebSocketPayload,
|
|
1010
|
+
candidateFilter: (task: ITask) => boolean = () => true,
|
|
1011
|
+
options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
|
|
1012
|
+
): ITask | undefined {
|
|
1013
|
+
const candidates = this.findRelatedTasks(payload, options).filter(candidateFilter);
|
|
1014
|
+
|
|
1015
|
+
if (candidates.length === 1) {
|
|
1016
|
+
return candidates[0];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (candidates.length > 1) {
|
|
1020
|
+
LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
|
|
653
1021
|
module: TASK_MANAGER_FILE,
|
|
654
|
-
method:
|
|
655
|
-
interactionId:
|
|
1022
|
+
method: 'findUniqueTaskByRelatedInteraction',
|
|
1023
|
+
interactionId:
|
|
1024
|
+
payload.interaction?.mainInteractionId ||
|
|
1025
|
+
payload.interaction?.parentInteractionId ||
|
|
1026
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId ||
|
|
1027
|
+
payload.interactionId,
|
|
656
1028
|
});
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
private findRelatedTasks(
|
|
1035
|
+
payload: WebSocketPayload,
|
|
1036
|
+
options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
|
|
1037
|
+
): ITask[] {
|
|
1038
|
+
const {includeCollectionKeys = false, includeMainCallMediaKeys = false} = options;
|
|
1039
|
+
const payloadInteractionIds = new Set(
|
|
1040
|
+
[
|
|
1041
|
+
payload.interactionId,
|
|
1042
|
+
payload.interaction?.interactionId,
|
|
1043
|
+
payload.interaction?.mainInteractionId,
|
|
1044
|
+
payload.interaction?.parentInteractionId,
|
|
1045
|
+
payload.interaction?.callProcessingDetails?.parentInteractionId,
|
|
1046
|
+
...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : []),
|
|
1047
|
+
].filter((relatedInteractionId): relatedInteractionId is string =>
|
|
1048
|
+
Boolean(relatedInteractionId)
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
if (payloadInteractionIds.size === 0) return [];
|
|
1052
|
+
|
|
1053
|
+
return [
|
|
1054
|
+
...new Set(
|
|
1055
|
+
Object.entries(this.taskCollection)
|
|
1056
|
+
.filter(([taskId, candidate]) => {
|
|
1057
|
+
const taskInteraction = candidate?.data?.interaction;
|
|
1058
|
+
const candidateInteractionIds = [
|
|
1059
|
+
...(includeCollectionKeys ? [taskId] : []),
|
|
1060
|
+
candidate?.data?.interactionId,
|
|
1061
|
+
taskInteraction?.interactionId,
|
|
1062
|
+
taskInteraction?.mainInteractionId,
|
|
1063
|
+
taskInteraction?.parentInteractionId,
|
|
1064
|
+
taskInteraction?.callProcessingDetails?.parentInteractionId,
|
|
1065
|
+
...(includeMainCallMediaKeys
|
|
1066
|
+
? [TaskManager.getMainCallMediaId(taskInteraction)]
|
|
1067
|
+
: []),
|
|
1068
|
+
];
|
|
1069
|
+
|
|
1070
|
+
return candidateInteractionIds.some(
|
|
1071
|
+
(relatedInteractionId) =>
|
|
1072
|
+
Boolean(relatedInteractionId) && payloadInteractionIds.has(relatedInteractionId)
|
|
1073
|
+
);
|
|
1074
|
+
})
|
|
1075
|
+
.map(([, candidate]) => candidate)
|
|
1076
|
+
),
|
|
1077
|
+
];
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Handle task lifecycle events and determine required actions
|
|
1082
|
+
*
|
|
1083
|
+
* Delegates to specific event handlers based on event type. Each handler
|
|
1084
|
+
* is responsible for TaskManager-level concerns:
|
|
1085
|
+
* - Task creation and collection management
|
|
1086
|
+
* - Metrics tracking
|
|
1087
|
+
* - Resource cleanup decisions
|
|
1088
|
+
*
|
|
1089
|
+
* Note: Task-level state transitions and event emissions are handled by
|
|
1090
|
+
* the task state machine via sendStateMachineEvent()
|
|
1091
|
+
*/
|
|
1092
|
+
private handleTaskLifecycleEvent(context: EventContext): TaskEventActions {
|
|
1093
|
+
const {eventType} = context;
|
|
1094
|
+
|
|
1095
|
+
switch (eventType) {
|
|
1096
|
+
case CC_EVENTS.AGENT_CONTACT_RESERVED:
|
|
1097
|
+
return this.handleContactReserved(context);
|
|
1098
|
+
|
|
1099
|
+
case CC_EVENTS.AGENT_CONTACT:
|
|
1100
|
+
return this.handleAgentContact(context);
|
|
1101
|
+
|
|
1102
|
+
case CC_EVENTS.CONTACT_MERGED:
|
|
1103
|
+
return this.handleContactMergedEvent(context);
|
|
1104
|
+
|
|
1105
|
+
case CC_EVENTS.CONTACT_OWNER_CHANGED:
|
|
1106
|
+
return this.handleContactOwnerChanged(context);
|
|
1107
|
+
|
|
1108
|
+
case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
|
|
1109
|
+
return this.handleCampaignPreviewReservation(context);
|
|
1110
|
+
|
|
1111
|
+
case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
|
|
1112
|
+
return this.handleCampaignContactUpdated(context);
|
|
1113
|
+
|
|
1114
|
+
default:
|
|
1115
|
+
return {task: context.task};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Recover the promoted agent's active voice task when local task state was lost.
|
|
1121
|
+
* Existing related tasks are never replaced: an unresolved or ambiguous relation
|
|
1122
|
+
* is safer to ignore than to create a duplicate task.
|
|
1123
|
+
*/
|
|
1124
|
+
private handleContactOwnerChanged(context: EventContext): TaskEventActions {
|
|
1125
|
+
if (context.task) {
|
|
1126
|
+
return {task: context.task};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const {payload} = context;
|
|
1130
|
+
if (
|
|
1131
|
+
this.findRelatedTasks(payload, {
|
|
1132
|
+
includeCollectionKeys: true,
|
|
1133
|
+
includeMainCallMediaKeys: true,
|
|
1134
|
+
}).length > 0 ||
|
|
1135
|
+
!this.canRecoverTaskFromContactOwnerChanged(payload)
|
|
1136
|
+
) {
|
|
1137
|
+
return {};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
|
|
1141
|
+
if (!stableInteractionId || this.taskCollection[stableInteractionId]) {
|
|
1142
|
+
return {};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const normalizedPayload: WebSocketPayload =
|
|
1146
|
+
payload.interactionId === stableInteractionId
|
|
1147
|
+
? payload
|
|
1148
|
+
: {...payload, interactionId: stableInteractionId};
|
|
1149
|
+
const taskData: TaskData = {
|
|
1150
|
+
...normalizedPayload,
|
|
1151
|
+
owner: normalizedPayload.interaction.owner,
|
|
1152
|
+
wrapUpRequired: normalizedPayload.interaction.participants?.[this.agentId]?.isWrapUp || false,
|
|
1153
|
+
isConferenceInProgress: getIsConferenceInProgress(normalizedPayload),
|
|
1154
|
+
isConsulted: false,
|
|
1155
|
+
isAutoAnswering: false,
|
|
1156
|
+
};
|
|
1157
|
+
const task = TaskFactory.createTask(
|
|
1158
|
+
this.contact,
|
|
1159
|
+
this.webCallingService,
|
|
1160
|
+
taskData,
|
|
1161
|
+
this.configFlags,
|
|
1162
|
+
this.wrapupData,
|
|
1163
|
+
this.agentId,
|
|
1164
|
+
this.answerCallOnWebexService
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
this.taskCollection[stableInteractionId] = task;
|
|
1168
|
+
|
|
1169
|
+
// Restore the actor before installing listeners so the internal hydrate does
|
|
1170
|
+
// not leak as a second public hydrate or as an incoming-task notification.
|
|
1171
|
+
task.sendStateMachineEvent({
|
|
1172
|
+
type: TaskEvent.HYDRATE,
|
|
1173
|
+
taskData,
|
|
1174
|
+
agentId: this.agentId,
|
|
1175
|
+
} as TaskEventPayload);
|
|
1176
|
+
|
|
1177
|
+
this.setupTaskListeners(task);
|
|
1178
|
+
context.payload = taskData;
|
|
1179
|
+
context.stateMachineEvent = {
|
|
1180
|
+
type: TaskEvent.CONTACT_OWNER_CHANGED,
|
|
1181
|
+
taskData,
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
return {task};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private handleCampaignContactUpdated(context: EventContext) {
|
|
1188
|
+
const {payload} = context;
|
|
1189
|
+
let {task} = context;
|
|
1190
|
+
// CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
|
|
1191
|
+
// Update the task data and emit an event so consumers can react to the updated contact.
|
|
1192
|
+
// Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
|
|
1193
|
+
if (task) {
|
|
1194
|
+
// Carry forward campaign preview fields from existing task data since the updated
|
|
1195
|
+
// contact payload may not include them, and reconcileData would delete them.
|
|
1196
|
+
const existingCpd = task.data?.interaction?.callProcessingDetails;
|
|
1197
|
+
const updatedData: TaskData = {...payload};
|
|
1198
|
+
|
|
1199
|
+
if (existingCpd) {
|
|
1200
|
+
const campaignFields = {
|
|
1201
|
+
...(existingCpd.campaignPreviewAutoAction && {
|
|
1202
|
+
campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
|
|
1203
|
+
}),
|
|
1204
|
+
...(existingCpd.campaignPreviewOfferTimeout && {
|
|
1205
|
+
campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
|
|
1206
|
+
}),
|
|
1207
|
+
...(existingCpd.campaignPreviewSkipDisabled && {
|
|
1208
|
+
campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
|
|
1209
|
+
}),
|
|
1210
|
+
...(existingCpd.campaignPreviewRemoveDisabled && {
|
|
1211
|
+
campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
|
|
1212
|
+
}),
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
if (!updatedData.interaction) {
|
|
1216
|
+
updatedData.interaction = {} as typeof updatedData.interaction;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
updatedData.interaction = {
|
|
1220
|
+
...updatedData.interaction,
|
|
1221
|
+
callProcessingDetails: {
|
|
1222
|
+
...campaignFields,
|
|
1223
|
+
...(updatedData.interaction.callProcessingDetails || {}),
|
|
1224
|
+
} as typeof existingCpd,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
|
|
662
1229
|
module: TASK_MANAGER_FILE,
|
|
663
1230
|
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
664
|
-
interactionId:
|
|
1231
|
+
interactionId: payload.interactionId,
|
|
1232
|
+
data: {
|
|
1233
|
+
hasCpd: !!updatedData.interaction?.callProcessingDetails,
|
|
1234
|
+
autoAction: updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
|
|
1235
|
+
skipDisabled: updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
|
|
1236
|
+
removeDisabled:
|
|
1237
|
+
updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
|
|
1238
|
+
},
|
|
665
1239
|
});
|
|
666
1240
|
|
|
667
|
-
task =
|
|
1241
|
+
task = this.updateTaskData(task, updatedData);
|
|
1242
|
+
task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return {task};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Creates or updates a task for campaign preview reservation.
|
|
1250
|
+
* TASK_CAMPAIGN_PREVIEW_RESERVATION is emitted by the state machine (campaign TASK_INCOMING branch).
|
|
1251
|
+
*/
|
|
1252
|
+
private handleCampaignPreviewReservation(context: EventContext): TaskEventActions {
|
|
1253
|
+
const {payload} = context;
|
|
1254
|
+
let {task} = context;
|
|
1255
|
+
|
|
1256
|
+
LoggerProxy.log('Campaign preview reservation received', {
|
|
1257
|
+
module: TASK_MANAGER_FILE,
|
|
1258
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
1259
|
+
interactionId: payload.interactionId,
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
if (!task) {
|
|
1263
|
+
task = TaskFactory.createTask(
|
|
668
1264
|
this.contact,
|
|
669
1265
|
this.webCallingService,
|
|
670
1266
|
{
|
|
671
|
-
...
|
|
672
|
-
wrapUpRequired:
|
|
673
|
-
isConferenceInProgress:
|
|
1267
|
+
...payload,
|
|
1268
|
+
wrapUpRequired: false,
|
|
1269
|
+
isConferenceInProgress: false,
|
|
1270
|
+
isAutoAnswering: false,
|
|
674
1271
|
},
|
|
1272
|
+
this.configFlags,
|
|
675
1273
|
this.wrapupData,
|
|
676
|
-
this.agentId
|
|
1274
|
+
this.agentId,
|
|
1275
|
+
this.answerCallOnWebexService
|
|
677
1276
|
);
|
|
678
|
-
this.
|
|
1277
|
+
this.setupTaskListeners(task);
|
|
1278
|
+
this.taskCollection[payload.interactionId] = task;
|
|
1279
|
+
} else {
|
|
1280
|
+
task = this.updateTaskData(task, payload);
|
|
679
1281
|
}
|
|
680
1282
|
|
|
681
|
-
|
|
1283
|
+
return {task};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Handle AGENT_CONTACT_RESERVED event
|
|
1288
|
+
* Creates a new task; state machine event is sent during processing
|
|
1289
|
+
*/
|
|
1290
|
+
private handleContactReserved(context: EventContext): TaskEventActions {
|
|
1291
|
+
const {payload} = context;
|
|
1292
|
+
const isConsultedTask =
|
|
1293
|
+
payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
|
|
1294
|
+
const shouldAutoAnswer = shouldAutoAnswerTask(
|
|
1295
|
+
payload,
|
|
1296
|
+
this.agentId,
|
|
1297
|
+
this.webCallingService.loginOption,
|
|
1298
|
+
this.webRtcEnabled
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
const taskData: TaskData = {
|
|
1302
|
+
...payload,
|
|
1303
|
+
isConsulted: isConsultedTask,
|
|
1304
|
+
isAutoAnswering: shouldAutoAnswer,
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const task = TaskFactory.createTask(
|
|
1308
|
+
this.contact,
|
|
1309
|
+
this.webCallingService,
|
|
1310
|
+
taskData,
|
|
1311
|
+
this.configFlags,
|
|
1312
|
+
this.wrapupData,
|
|
1313
|
+
this.agentId,
|
|
1314
|
+
this.answerCallOnWebexService
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
this.setupTaskListeners(task);
|
|
1318
|
+
this.taskCollection[payload.interactionId] = task;
|
|
1319
|
+
|
|
1320
|
+
return {task};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Handle AGENT_CONTACT event
|
|
1325
|
+
* Re-creates task if missing (multi-session scenario)
|
|
1326
|
+
*/
|
|
1327
|
+
private handleAgentContact(context: EventContext): TaskEventActions {
|
|
1328
|
+
let {task} = context;
|
|
1329
|
+
const {payload} = context;
|
|
1330
|
+
|
|
1331
|
+
if (!task) {
|
|
1332
|
+
const isConsultedTask =
|
|
1333
|
+
payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
|
|
1334
|
+
const shouldAutoAnswer = shouldAutoAnswerTask(
|
|
1335
|
+
payload,
|
|
1336
|
+
this.agentId,
|
|
1337
|
+
this.webCallingService.loginOption,
|
|
1338
|
+
this.webRtcEnabled
|
|
1339
|
+
);
|
|
1340
|
+
const taskData: TaskData = {
|
|
1341
|
+
...payload,
|
|
1342
|
+
isConsulted: isConsultedTask,
|
|
1343
|
+
wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
|
|
1344
|
+
isConferenceInProgress: getIsConferenceInProgress(payload),
|
|
1345
|
+
isAutoAnswering: shouldAutoAnswer,
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
task = TaskFactory.createTask(
|
|
1349
|
+
this.contact,
|
|
1350
|
+
this.webCallingService,
|
|
1351
|
+
taskData,
|
|
1352
|
+
this.configFlags,
|
|
1353
|
+
this.wrapupData,
|
|
1354
|
+
this.agentId,
|
|
1355
|
+
this.answerCallOnWebexService
|
|
1356
|
+
);
|
|
1357
|
+
this.setupTaskListeners(task);
|
|
1358
|
+
this.taskCollection[payload.interactionId] = task;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return {task};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
private updateTaskData(task: ITask, taskData: TaskData): ITask {
|
|
1365
|
+
if (!task) {
|
|
1366
|
+
throw new Error('Task not found for update');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const snapshot = task.stateMachineService?.getSnapshot?.();
|
|
1370
|
+
const isConsultingFlow =
|
|
1371
|
+
snapshot?.value === 'CONSULTING' || taskData.interaction?.state === 'consulting';
|
|
1372
|
+
|
|
1373
|
+
const updateTaskData = isConsultingFlow
|
|
1374
|
+
? {
|
|
1375
|
+
...taskData,
|
|
1376
|
+
destAgentId: taskData.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null,
|
|
1377
|
+
destinationType:
|
|
1378
|
+
taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null,
|
|
1379
|
+
}
|
|
1380
|
+
: taskData;
|
|
1381
|
+
|
|
1382
|
+
task.updateTaskData(updateTaskData);
|
|
1383
|
+
|
|
1384
|
+
// A main-interaction lifecycle event can re-key a child consult task. Remove
|
|
1385
|
+
// any aliases first so consumers never observe duplicate task entries.
|
|
1386
|
+
Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
|
|
1387
|
+
if (candidate === task && taskId !== taskData.interactionId) {
|
|
1388
|
+
delete this.taskCollection[taskId];
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
this.taskCollection[taskData.interactionId] = task;
|
|
682
1392
|
|
|
683
1393
|
return task;
|
|
684
1394
|
}
|
|
685
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Setup listeners for task events that need to be bubbled up to TaskManager
|
|
1398
|
+
* This replaces the previous callback injection pattern
|
|
1399
|
+
*/
|
|
1400
|
+
private setupTaskListeners(task: ITask): void {
|
|
1401
|
+
// Listen for TASK_INCOMING and re-emit so webex.cc can notify consumers
|
|
1402
|
+
task.on(TASK_EVENTS.TASK_INCOMING, (t: ITask) => {
|
|
1403
|
+
LoggerProxy.log(`Task incoming event received`, {
|
|
1404
|
+
module: TASK_MANAGER_FILE,
|
|
1405
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
1406
|
+
interactionId: t.data?.interactionId,
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
this.emit(TASK_EVENTS.TASK_INCOMING, t);
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
task.on(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, (t: ITask) => {
|
|
1413
|
+
LoggerProxy.log(`Campaign preview reservation event received`, {
|
|
1414
|
+
module: TASK_MANAGER_FILE,
|
|
1415
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
1416
|
+
interactionId: t.data?.interactionId,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, t);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
// Listen for TASK_HYDRATE on the task and re-emit on TaskManager
|
|
1423
|
+
task.on(TASK_EVENTS.TASK_HYDRATE, (t: ITask) => {
|
|
1424
|
+
// Task data is already updated by the task itself before emitting
|
|
1425
|
+
this.emit(TASK_EVENTS.TASK_HYDRATE, t);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
task.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, (t: ITask) => {
|
|
1429
|
+
this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, t);
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// Listen for internal cleanup signal emitted by the state machine
|
|
1433
|
+
task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
|
|
1434
|
+
this.handleTaskCleanup(t);
|
|
1435
|
+
if (options?.removeFromCollection) {
|
|
1436
|
+
const taskIsStillCollected = Object.values(this.taskCollection).some(
|
|
1437
|
+
(candidate) => candidate === t
|
|
1438
|
+
);
|
|
1439
|
+
if (taskIsStillCollected) {
|
|
1440
|
+
this.removeTaskFromCollection(t);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
686
1446
|
private removeTaskFromCollection(task: ITask) {
|
|
1447
|
+
if (typeof task.cancelAutoWrapupTimer === 'function') {
|
|
1448
|
+
task.cancelAutoWrapupTimer();
|
|
1449
|
+
}
|
|
687
1450
|
if (task?.data?.interactionId) {
|
|
688
|
-
|
|
1451
|
+
Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
|
|
1452
|
+
if (candidate === task) {
|
|
1453
|
+
delete this.taskCollection[taskId];
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
689
1456
|
LoggerProxy.info(`Task removed from collection`, {
|
|
690
1457
|
module: TASK_MANAGER_FILE,
|
|
691
1458
|
method: METHODS.REMOVE_TASK_FROM_COLLECTION,
|
|
@@ -695,69 +1462,73 @@ export default class TaskManager extends EventEmitter {
|
|
|
695
1462
|
}
|
|
696
1463
|
|
|
697
1464
|
/**
|
|
698
|
-
* Handles
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* 2. Agent-initiated WebRTC outdial calls
|
|
703
|
-
* 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
|
|
704
|
-
*
|
|
705
|
-
* @param task - The task to auto-answer
|
|
1465
|
+
* Handles CONTACT_MERGED event logic
|
|
1466
|
+
* @param task - The task to process
|
|
1467
|
+
* @param taskData - The task data from the event payload
|
|
1468
|
+
* @returns Updated or newly created task
|
|
706
1469
|
* @private
|
|
707
1470
|
*/
|
|
708
|
-
private
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
1471
|
+
private handleContactMergedEvent(context: EventContext): TaskEventActions {
|
|
1472
|
+
const {payload} = context;
|
|
1473
|
+
let task = context.task;
|
|
712
1474
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
interactionId: task.data.interactionId,
|
|
717
|
-
});
|
|
1475
|
+
if (payload.childInteractionId && this.taskCollection[payload.childInteractionId]) {
|
|
1476
|
+
this.removeTaskFromCollection(this.taskCollection[payload.childInteractionId]);
|
|
1477
|
+
}
|
|
718
1478
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
LoggerProxy.info(`Task auto-answered successfully`, {
|
|
1479
|
+
if (task) {
|
|
1480
|
+
LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
|
|
722
1481
|
module: TASK_MANAGER_FILE,
|
|
723
|
-
method:
|
|
724
|
-
interactionId:
|
|
1482
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
1483
|
+
interactionId: payload.interactionId,
|
|
725
1484
|
});
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
taskId: task.data.interactionId,
|
|
732
|
-
mediaType: task.data.interaction.mediaType,
|
|
733
|
-
isAutoAnswered: true,
|
|
734
|
-
},
|
|
735
|
-
['behavioral', 'operational']
|
|
736
|
-
);
|
|
737
|
-
// Emit task:autoAnswered event for widgets/UI to react
|
|
738
|
-
task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
739
|
-
} catch (error) {
|
|
740
|
-
// Reset isAutoAnswering flag on failure
|
|
741
|
-
task.updateTaskData({...task.data, isAutoAnswering: false});
|
|
742
|
-
LoggerProxy.error(`Failed to auto-answer task`, {
|
|
1485
|
+
// update the task data
|
|
1486
|
+
this.updateTaskData(task, payload);
|
|
1487
|
+
} else {
|
|
1488
|
+
// Case2 : Task is not present in taskCollection
|
|
1489
|
+
LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
|
|
743
1490
|
module: TASK_MANAGER_FILE,
|
|
744
|
-
method:
|
|
745
|
-
interactionId:
|
|
746
|
-
error,
|
|
1491
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
1492
|
+
interactionId: payload.interactionId,
|
|
747
1493
|
});
|
|
748
1494
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1495
|
+
const taskData: TaskData = {
|
|
1496
|
+
...payload,
|
|
1497
|
+
wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
|
|
1498
|
+
isConferenceInProgress: getIsConferenceInProgress(payload),
|
|
1499
|
+
isConsulted: false,
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
task = TaskFactory.createTask(
|
|
1503
|
+
this.contact,
|
|
1504
|
+
this.webCallingService,
|
|
1505
|
+
taskData,
|
|
1506
|
+
this.configFlags,
|
|
1507
|
+
this.wrapupData,
|
|
1508
|
+
this.agentId,
|
|
1509
|
+
this.answerCallOnWebexService
|
|
759
1510
|
);
|
|
1511
|
+
this.taskCollection[payload.interactionId] = task;
|
|
1512
|
+
|
|
1513
|
+
// Transition the new task out of IDLE immediately so UI controls are
|
|
1514
|
+
// computed before TASK_MERGED is emitted. This handles the race where
|
|
1515
|
+
// AgentContactAssigned arrives before ContactMerged and gets dropped.
|
|
1516
|
+
// Send HYDRATE before setupTaskListeners so the emitTaskHydrate action
|
|
1517
|
+
// doesn't bubble up to the Widget (avoids duplicate listener registration).
|
|
1518
|
+
task.sendStateMachineEvent({
|
|
1519
|
+
type: TaskEvent.HYDRATE,
|
|
1520
|
+
taskData,
|
|
1521
|
+
agentId: this.agentId,
|
|
1522
|
+
} as TaskEventPayload);
|
|
1523
|
+
|
|
1524
|
+
this.setupTaskListeners(task);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (task) {
|
|
1528
|
+
this.emit(TASK_EVENTS.TASK_MERGED, task);
|
|
760
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
return {task};
|
|
761
1532
|
}
|
|
762
1533
|
|
|
763
1534
|
/**
|
|
@@ -769,7 +1540,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
769
1540
|
// Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
|
|
770
1541
|
if (
|
|
771
1542
|
this.webCallingService.loginOption === LoginOption.BROWSER &&
|
|
772
|
-
task.data.interaction.mediaType ===
|
|
1543
|
+
task.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY &&
|
|
1544
|
+
task instanceof WebRTC
|
|
773
1545
|
) {
|
|
774
1546
|
task.unregisterWebCallListeners();
|
|
775
1547
|
this.webCallingService.cleanUpCall();
|
|
@@ -797,12 +1569,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
797
1569
|
*/
|
|
798
1570
|
private requestRealTimeTranscripts(eventType: string, interactionId: string): void {
|
|
799
1571
|
const action = TRANSCRIPT_EVENT_MAP[eventType];
|
|
800
|
-
if (
|
|
801
|
-
|
|
802
|
-
!this.apiAIAssistant ||
|
|
803
|
-
this.apiAIAssistant.aiFeature?.realtimeTranscripts?.enable === false
|
|
804
|
-
)
|
|
805
|
-
return;
|
|
1572
|
+
if (!action || !this.apiAIAssistant) return;
|
|
1573
|
+
if (this.configFlags?.aiFeature?.realtimeTranscripts?.enable !== true) return;
|
|
806
1574
|
|
|
807
1575
|
this.apiAIAssistant
|
|
808
1576
|
.sendEvent(
|
|
@@ -810,12 +1578,12 @@ export default class TaskManager extends EventEmitter {
|
|
|
810
1578
|
interactionId,
|
|
811
1579
|
AIAssistantEventType.CUSTOM_EVENT,
|
|
812
1580
|
AIAssistantEventName.GET_TRANSCRIPTS,
|
|
813
|
-
action
|
|
1581
|
+
{action}
|
|
814
1582
|
)
|
|
815
1583
|
.catch((error) => {
|
|
816
1584
|
LoggerProxy.error(`Failed to send transcript ${action} event`, {
|
|
817
1585
|
module: TASK_MANAGER_FILE,
|
|
818
|
-
method:
|
|
1586
|
+
method: METHODS.REQUEST_REAL_TIME_TRANSCRIPTS,
|
|
819
1587
|
interactionId,
|
|
820
1588
|
error,
|
|
821
1589
|
});
|
|
@@ -826,28 +1594,71 @@ export default class TaskManager extends EventEmitter {
|
|
|
826
1594
|
return this.taskCollection[taskId];
|
|
827
1595
|
}
|
|
828
1596
|
|
|
1597
|
+
public getAllTasks(): Record<TaskId, ITask> {
|
|
1598
|
+
return {...this.taskCollection};
|
|
1599
|
+
}
|
|
1600
|
+
|
|
829
1601
|
/**
|
|
830
|
-
*
|
|
1602
|
+
* Propagate runtime enableWxBetterTogether changes to config flags and active tasks.
|
|
831
1603
|
*/
|
|
832
|
-
public
|
|
833
|
-
|
|
834
|
-
|
|
1604
|
+
public applyEnableWxBetterTogether(enabled: boolean): void {
|
|
1605
|
+
if (this.configFlags) {
|
|
1606
|
+
this.configFlags = {...this.configFlags, enableWxBetterTogether: enabled};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1610
|
+
task.setEnableWxBetterTogether(enabled);
|
|
1611
|
+
if (enabled) {
|
|
1612
|
+
TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Propagate wxApp mute state from Mercury sync to active voice tasks.
|
|
1619
|
+
*/
|
|
1620
|
+
public applyWxAppMuteStateFromSync(callId: string, muted: boolean): void {
|
|
1621
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1622
|
+
task.applyWxAppMuteStateFromSync(callId, muted);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Re-seeds wxApp mute state from GET telephony/calls/{callId} for one task.
|
|
1628
|
+
*/
|
|
1629
|
+
public static syncWxAppMuteFromCallDetailsForTask(task: ITask): void {
|
|
1630
|
+
const sync = task.syncWxAppMuteFromCallDetails?.bind(task);
|
|
1631
|
+
if (typeof sync === 'function') {
|
|
1632
|
+
sync().catch(() => undefined);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Re-seeds wxApp mute for all engaged voice tasks (reload / runtime enable backfill).
|
|
1638
|
+
*/
|
|
1639
|
+
public syncWxAppMuteFromCallDetailsForAllTasks(): void {
|
|
1640
|
+
Object.values(this.taskCollection).forEach((task) => {
|
|
1641
|
+
TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
835
1644
|
|
|
836
1645
|
public static getTaskManager(
|
|
837
1646
|
apiAIAssistant: ApiAIAssistant,
|
|
838
1647
|
contact: ReturnType<typeof routingContact>,
|
|
839
1648
|
webCallingService: WebCallingService,
|
|
840
|
-
webSocketManager: WebSocketManager
|
|
1649
|
+
webSocketManager: WebSocketManager,
|
|
1650
|
+
rtdWebSocketManager?: WebSocketManager
|
|
841
1651
|
): TaskManager {
|
|
842
1652
|
if (!TaskManager.taskManager) {
|
|
843
1653
|
TaskManager.taskManager = new TaskManager(
|
|
844
1654
|
apiAIAssistant,
|
|
845
1655
|
contact,
|
|
846
1656
|
webCallingService,
|
|
847
|
-
webSocketManager
|
|
1657
|
+
webSocketManager,
|
|
1658
|
+
rtdWebSocketManager
|
|
848
1659
|
);
|
|
849
1660
|
}
|
|
850
1661
|
|
|
851
|
-
return
|
|
1662
|
+
return TaskManager.taskManager;
|
|
852
1663
|
}
|
|
853
1664
|
}
|