فهرست منبع

坐席外呼逻辑开发,待调试

刘威 6 ماه پیش
والد
کامیت
4f5dffd3ad

+ 7 - 1
src/core/callcenter/acd.py

@@ -1,11 +1,17 @@
 #!/usr/bin/env python3
 # encoding:utf-8
 from src.core.callcenter.model import CallInfo
+from apscheduler.schedulers.background import BackgroundScheduler
 
 
 class AcdService:
     def __init__(self):
-        pass
+        scheduler = BackgroundScheduler()
+        scheduler.add_job(self.try_transfer_agent, 'interval', seconds=5)
+        scheduler.start()
 
     def transfer_to_agent(self, call_info: CallInfo, device_id, service_id):
         pass
+
+    def try_transfer_agent(self):
+        pass

+ 3 - 3
src/core/callcenter/call.py

@@ -30,7 +30,7 @@ class CallService:
                              caller=request.caller, called=request.called, direction=Direction.INBOUND,
                              caller_display=request.caller_display, called_display=request.called_display,
                              call_type=request.call_type, call_time=now, follow_data=request.follow_data,
-                             uuid1=request.uuid1, uuid2=request.uuid2)
+                             uuid1=request.uuid1, uuid2=request.uuid2, saas_id=saasId)
         device_info = DeviceInfo(device_id=device_id, call_time=now, call_id=call_id, device_type=DeviceType.AGENT,
                                  agent_key=agent.agent_number)
         call_info.device_list.append(device_info)
@@ -42,8 +42,8 @@ class CallService:
     def hold(self, call_info: CallInfo, device_id):
         devices = call_info.device_list
         devices.remove(device_id)
-        self.client.bridge_break(list.get(0))
-        self.client.hold_play(list.get(0), HOLD_MUSIC_PATH)
+        self.client.bridge_break(devices.get(0))
+        self.client.hold_play(devices.get(0), HOLD_MUSIC_PATH)
 
     def transfer(self, call_info: CallInfo, agent_number, service_id):
         agent = Cache.get_agent_info(call_info.agent_key)

+ 15 - 2
src/core/callcenter/constant.py

@@ -1,6 +1,8 @@
 #!/usr/bin/env python3
 # encoding:utf-8
 
+from src.core.callcenter.model import CallInfo
+
 saasId = "mdj"
 
 UTF_8 = "UTF-8"
@@ -38,7 +40,8 @@ READY_TIMES = "readyTimes"
 #服务次数
 SEREVICE_TIMES = "serviceTimes"
 
-HOLD_MUSIC_PATH = '/'
+HOLD_MUSIC_PATH = '/tmp/hold.wav'
+BASE_RECORD_PATH = '/freeswitch/record/'
 
 EMPTY = ""
 DEFAULT_KEY = ""
@@ -71,4 +74,14 @@ AGENT_INFO = "AGENT_INFO:"
 ADMIN_TOKEN = "ADMIN_TOKEN:"
 ADMIN_INFO = "ADMIN_INFO:"
 
-CALL_INFO = "CALL_INFO:"
+CALL_INFO = "CALL_INFO:"
+
+
+def format_time_millis(time_millis, pattern='%Y%m%d'):
+    from datetime import datetime
+    dt = datetime.utcfromtimestamp(time_millis)
+    return dt.strftime(pattern)
+
+
+def get_record_prefix(call: CallInfo):
+    return BASE_RECORD_PATH + call.call_type + '/' + call.saas_id + '/' + call.caller + '/' + format_time_millis(call.call_time)

+ 43 - 1
src/core/callcenter/enumeration.py

@@ -4,6 +4,20 @@
 from enum import Enum
 
 
+class AnswerFlag(Enum):
+    INIT = (0, "均未拨通")
+    USER_ANSWER = (1, "用户接通")
+    ROBOT_ANSWER = (2, "机器人接通")
+    USER_AND_ROBOT_BRIDGE = (3, "用户与机器人bridge")
+    TRANSFER_TO_AGENT = (4, "开始转接到坐席")
+    AGENT_ANSWER = (5, "坐席接通")
+    USER_AND_AGENT_BRIDGE = (6, "用户与坐席bridge")
+
+    def __init__(self, code, description):
+        self.code = code
+        self.description = description
+
+
 class DeviceType(Enum):
     AGENT = (1, "坐席")
     CUSTOMER = (2, "客户")
@@ -72,7 +86,7 @@ class NextType(Enum):
         self.description = description
 
 
-class CallCauseEnum(Enum):
+class CallCause(Enum):
     DEFAULT = (0, "默认")
     RESTART = (2, "服务重启")
     CALL_TIMEOUT = (3, "呼叫超时")
@@ -93,3 +107,31 @@ class CallCauseEnum(Enum):
     def __init__(self, code, description):
         self.code = code
         self.description = description
+
+
+class CdrType(Enum):
+
+    INBOUND = (1, "呼入")
+    OUTBOUND = (2, "外呼")
+    INTERNAL_CALL = (3, "内呼")
+    TRANSFER = (4, "转接")
+    CONSULT = (5, "咨询")
+    LISTENER = (6, "监听")
+    INTRUSION = (7, "强插")
+    WHISPER = (8, "耳语")
+    ROBOT_LISTENER = (9, "机器人质检监听")
+
+    def __init__(self, code, description):
+        self.code = code
+        self.description = description
+
+
+class HangupDir:
+
+    HOST_HANGUP = (1, "主叫挂断")
+    CUSTOMER_HANGUP = (2, "被叫挂断")
+    PLATFORM_HANGUP = (3, "平台挂机")
+
+    def __init__(self, code, description):
+        self.code = code
+        self.description = description

+ 4 - 4
src/core/callcenter/esl/client.py

@@ -16,7 +16,7 @@ from src.core.callcenter.esl.constant.esl_constant import BRIDGE_VARIABLES, BRID
 import src.core.callcenter.esl.utils.esl_event_util as EslEventUtil
 import src.core.callcenter.esl.handler as event_handler
 from src.core.callcenter.esl.constant.sip_header_constant import sipHeaderHoldMusic
-from src.core.callcenter.enumeration import CallCauseEnum
+from src.core.callcenter.enumeration import CallCause
 from src.core.callcenter.esl.handler.default_esl_event_handler import DefaultEslEventHandler
 
 
@@ -190,14 +190,14 @@ class InboundClient:
         """应答"""
         self.con.bgapi('uuid_phone_event', device_id + ' talk')
 
-    def hangup_call(self, call_id, device_id, case_enum=CallCauseEnum.DEFAULT):
+    def hangup_call(self, call_id, device_id, case_enum=CallCause.DEFAULT):
         """挂机"""
         msg = ESL.ESLevent("sendmsg", device_id)
         msg.addHeader("call-command", EXECUTE)
         msg.addHeader("execute-app-name", HANGUP)
         msg.addHeader("execute-app-arg", NORMAL_CLEARING)
         self.logger.info("hangup_call挂机 hangup call: {}, device: {}, ctiCauseEnum:{}", call_id, device_id, case_enum)
-        self.send_args(device_id, SET, EslEventUtil.SIP_H_P_WDH_HANGUP_CAUSE + "=" + case_enum.description)
+        self.send_args(device_id, SET, EslEventUtil.SIP_H_P_LIBRA_HANGUP_CAUSE + "=" + case_enum.description)
         self.con.sendEvent(msg)
 
     def broadcast(self, uuid, path, smf):
@@ -345,7 +345,7 @@ class InboundClient:
         """播放超时主动挂机"""
         pass
 
-    def listen(self, device_id1, device_id2, aleg, bleg):
+    def listen(self, device_id1, device_id2, aleg=True, bleg=True):
         """监听"""
         if aleg:
             self.send_args(device_id1, SET, "eavesdrop_bridge_aleg=true")

+ 62 - 9
src/core/callcenter/esl/handler/channel_answer_handler.py

@@ -1,8 +1,10 @@
 #!/usr/bin/env python3
 # encoding:utf-8
 
+import time
+from src.core.callcenter.constant import saasId, get_record_prefix
 import src.core.callcenter.cache as Cache
-from src.core.callcenter.enumeration import NextType
+from src.core.callcenter.enumeration import NextType, AnswerFlag, Direction, DeviceType
 from src.core.callcenter.esl.annotation import EslEventName
 import src.core.callcenter.esl.utils.esl_event_util as EslEventUtil
 from src.core.callcenter.esl.constant.event_names import CHANNEL_ANSWER
@@ -35,24 +37,75 @@ class ChannelAnswerHandler(EslEventHandler):
 
         if NextType.NEXT_CALL_OTHER == next_command.next_type:
             self.call_other(call_info, device_info)
-        elif NextType.NEXT_TRANSFER_CALL == next_command.next_type:
-            self.transfer_call(call_info, next_command, event)
         elif NextType.NEXT_CALL_BRIDGE == next_command.next_type:
             self.call_bridge(call_info, device_info, next_command, event)
+        elif NextType.NEXT_TRANSFER_CALL == next_command.next_type:
+            self.transfer_call(call_info, next_command, event)
         elif NextType.NEXT_LISTEN_CALL == next_command.next_type:
             self.listen(call_info, device_info, next_command, event)
         else:
-            self.logger.warn("can not match command :%s, callId:%s", next_command.next_type, call_id)
+            self.logger.warn("can not match command :%s, callId :%s", next_command.next_type, call_id)
         Cache.add_call_info(call_info)
 
     def call_other(self, call: CallInfo, device: DeviceInfo):
-        pass
+        call_id = call.call_id
+        device_id = device.device_id
 
-    def transfer_call(self, call: CallInfo, next_command: NextCommand, event):
-        pass
+        # 启用录音, 生产时候打开
+        # record = get_record_prefix(call) + '/' + call_id + '.wav'
+        # self.inbound_client.record(device.device_id, 'start', record, 0)
+        # device.record = record
+        # device.record_start_time = device.answer_time
+
+        call.direction = Direction.OUTBOUND
+        call.answer_flag = AnswerFlag.AGENT_ANSWER
+
+        new_device_id = 'D' + self.snowflake.next_id()
+        call.device_list.append(new_device_id)
+        called = call.called
+
+        self.logger.info("呼另外一侧电话: callId: %s, display:%s, called:%s, deviceId: %s ",
+                         call_id, call.called_display, called, device_id)
+        route_gateway = Cache.get_route_gateway(saasId)
+        if not route_gateway:
+            self.logger.warn("callId:%s routeGetway error, called:%s", call_id, called)
+            self.inbound_client.hangup_call(call_id, device_id)
+            return
+
+        now = lambda: int(round(time.time() * 1000))
+        new_device = DeviceInfo(device_id=new_device_id, call_id=call_id, agent_key=call.agent_key,
+                                called=called, display=call.called_display, caller=call.called_display,
+                                call_time=now, device_type=DeviceType.CUSTOMER)
+        call.next_commands.append(NextCommand(device_id=device_id, next_type=NextType.NEXT_CALL_BRIDGE, next_value=new_device_id))
+        call.device_info_map[new_device_id] = new_device
+        Cache.add_call_info(call)
+
+        self.client.make_call(route_gateway, call.caller, called, call_id, new_device_id)
 
     def call_bridge(self, call: CallInfo, device: DeviceInfo, next_command: NextCommand, event):
-        pass
+        self.logger.info("开始桥接电话: callId:%s, caller:%s, called:%s, device1:%s, device2:%s", call.call_id,
+                         call.caller, call.called, next_command.device_id, next_command.next_value)
+        device1 = call.device_info_map.get(next_command.device_id)
+        device2 = call.device_info_map.get(next_command.next_value)
+
+        if not device1.bridge_time:
+            device1.bridge_time = EslEventUtil.getEventDateTimestamp(event)
+        if not device2.bridge_time:
+            device2.bridge_time = EslEventUtil.getEventDateTimestamp(event)
+        Cache.add_call_info(call)
+
+        self.inbound_client.bridge_call(call.call_id, next_command.device_id, next_command.next_value)
+
+    def transfer_call(self, call: CallInfo, next_command: NextCommand, event):
+        # 转接电话 deviceInfo为被转接设备
+        from_device_id = next_command.device_id
+        device_id = EslEventUtil.getDeviceId(event)
+        call.next_commands.append(NextCommand(device_id, NextType.NEXT_TRANSFER_SUCCESS, call.device_list[1]))
+        self.logger.info("转接电话中 callId:%s, from:%s, to:%s ", call.call_id, from_device_id, device_id)
+        self.inbound_client.transfer_call(device_id, next_command.next_value)
 
     def listen(self, call: CallInfo, device: DeviceInfo, next_command: NextCommand, event):
-        pass
+        device_id = EslEventUtil.getDeviceId(event)
+        self.logger.info("开始监听 callId:%s, deviceId:%s, nextCommandDeviceId:%s", call.call_id, device_id, next_command.next_value)
+        self.inbound_client.listen(device_id, next_command.next_value)
+

+ 101 - 1
src/core/callcenter/esl/handler/channel_hangup_handler.py

@@ -1,9 +1,16 @@
 #!/usr/bin/env python3
 # encoding:utf-8
 
+import src.core.callcenter.cache as Cache
+from src.core.callcenter.acd import AcdService
+from src.core.callcenter.call import CallService
+from src.core.callcenter.enumeration import CallType, DeviceType, AnswerFlag, NextType, CdrType, HangupDir, \
+    CallCause
 from src.core.callcenter.esl.annotation import EslEventName
+import src.core.callcenter.esl.utils.esl_event_util as EslEventUtil
 from src.core.callcenter.esl.constant.event_names import CHANNEL_HANGUP
 from src.core.callcenter.esl.handler.esl_event_handler import EslEventHandler
+from src.core.callcenter.model import CallInfo, DeviceInfo, NextCommand
 
 
 @EslEventName(CHANNEL_HANGUP)
@@ -11,6 +18,99 @@ class ChannelHangupHandler(EslEventHandler):
 
     def __init__(self, inbound):
         super().__init__(inbound)
+        self.acd_service = AcdService()
+        self.call_service = CallService()
 
     def handle(self, address, event, coreUUID):
-        pass
+        call_id = EslEventUtil.getCallId(event)
+        call = Cache.get_call_info(call_id)
+        if not call:
+            self.logger.info("call:{} is null", call_id)
+            return
+        device_id = EslEventUtil.getDeviceId(event)
+        device = call.device_info_map.get(device_id)
+        if not device:
+            self.logger.info("device:{} is null", device_id)
+            return
+
+        count = len(call.device_list)
+        call.device_list.remove(device_id)
+
+        cause = EslEventUtil.getCallHangupCause(event)
+        caller = EslEventUtil.getCallerCallerIdNumber(event)
+        called = EslEventUtil.getCallerDestinationNumber(event)
+        sip_status = EslEventUtil.getSipStatus(event)
+        sip_protocol = EslEventUtil.getSipProtocol(event)
+        rtp_use_codec = EslEventUtil.getRtpUseCodec(event)
+        channel_name = EslEventUtil.getCallerChannelName(event)
+        timestamp = EslEventUtil.getEventDateTimestamp(event)
+        hangup_cause = EslEventUtil.getVariableSipHPLIBRAHangupCause(event)
+        hangup_reason = EslEventUtil.getLIBRAHangupReason(event)
+
+        device.hangup_cause = cause
+        device.sip_protocol = sip_protocol
+        device.sip_status = sip_status
+        device.channel_name = channel_name
+        device.end_time = timestamp
+
+        # 计算通话时长
+        if device.answer_time:
+            device.talk_time = device.end_time - device.answer_time
+        else:
+            device.ring_start_time = device.end_time
+        # 计算录音时长
+        if device.record_start_time:
+            device.record_time = device.end_time - device.record_start_time
+        call.device_info_map[device.device_id] = device
+
+        # 如果是转人工
+        if 'transferToAgent' == hangup_reason and DeviceType.ROBOT == device.device_type:
+            call.answer_flag = AnswerFlag.TRANSFER_TO_AGENT
+            service_id = EslEventUtil.getLIBRAServiceId(event)
+            Cache.add_call_info(call)
+            self.acd_service.transfer_to_agent(call, device, service_id)
+            return
+
+        # 如果有下一步
+        next_command = call.next_commands[0] if len(call.next_commands) > 0 else None
+        if next_command:
+            self.next_cmd(call, device, next_command, cause)
+            return
+
+        # 一般情况下,挂断其他所有设备
+        if device.cdr_type <= 4 and not call.end_time:
+            call.end_time = device.end_time
+            self.call_service.hangup_all(call, CallCause.HANGUP_EVENT)
+
+        # 判断挂机方向 && 更新缓存
+        self.hangup_dir(call, device, cause)
+        Cache.add_call_info(call)
+
+    def next_cmd(self, call: CallInfo, device: DeviceInfo, next_command: NextCommand, cause):
+        # 呼入转到坐席,坐席拒接和坐席sip呼不通的时候,都需要再次转回来到技能组排队。
+        if NextType.NEXT_CALL_BRIDGE == next_command.next_type or NextType.NEXT_LISTEN_CALL == next_command.next_type:
+            pass
+        elif NextType.NEXT_TRANSFER_CALL:
+            pass
+        else:
+            pass
+
+        if not next_command or NextType.NEXT_HANGUP != next_command.next_type:
+            call.next_commands.remove(next_command)
+
+        # 判断挂机方向 && 更新缓存
+        self.hangup_dir(call, device, cause)
+        Cache.add_call_info(call)
+
+    def hangup_dir(self, call:CallInfo, device:DeviceInfo, cause):
+        if call.hangup_dir or device.cdr_type > CdrType.CONSULT.code:
+            self.logger.info("hangup_dir::hangup_dir :%s, cdr_type :%s", call.hangup_dir, device.cdr_type)
+            return
+        if DeviceType.AGENT == device.device_type:
+            call.hangup_dir = HangupDir.HOST_HANGUP.code
+        elif DeviceType.CUSTOMER == device.device_type:
+            call.hangup_dir = HangupDir.CUSTOMER_HANGUP.code
+
+        if not call.end_time:
+            call.end_time = device.end_time
+        self.logger.info("hangup_dir::callId: %s, direction:%s, hangupDir:%s, cause:%s, deviceId: %s", call.call_id, call.direction, call.hangup_dir, cause, device.device_id)

+ 6 - 6
src/core/callcenter/esl/utils/esl_event_util.py

@@ -120,7 +120,7 @@ def getUniqueId(e):
     return e.getHeader(UNIQUE_ID)
 
 
-def getVariableSipHPWdhCalled(e):
+def getVariableSipHPLIBRACalled(e):
     return e.getHeader(VARIABLE_SIP_H_P_LIBRA_CALLED)
 
 
@@ -132,11 +132,11 @@ def getVariableSipHXCallSequenceId(e):
     return e.getHeader(VARIABLE_SIP_H_X_CALL_SEQUENCE_ID)
 
 
-def getVariableSipHPWdhCtiFlowId(e):
+def getVariableSipHPLIBRACtiFlowId(e):
     return e.getHeader(VARIABLE_SIP_H_P_LIBRA_CTI_FLOW_ID)
 
 
-def getVariableSipHPWdhCallSequenceId(e):
+def getVariableSipHPLIBRACallSequenceId(e):
     return e.getHeader(VARIABLE_SIP_H_P_LIBRA_CALL_SEQUENCE_ID)
 
 
@@ -388,11 +388,11 @@ def getDeviceId(e):
     return e.getHeader(VARIABLE_SIP_LIBRA_DEVICE_ID)
 
 
-def getWDHHangupReason(e):
+def getLIBRAHangupReason(e):
     return e.getHeader(VARIABLE_SIP_WHD_HANGUP_REASON)
 
 
-def getWDHServiceId(e):
+def getLIBRAServiceId(e):
     return e.getHeader(VARIABLE_SIP_WHD_SERVICE_ID)
 
 
@@ -444,7 +444,7 @@ def getDetectedTone(e):
     return e.getHeader(DETECTED_TONE)
 
 
-def getVariableSipHPWdhHangupCause(e):
+def getVariableSipHPLIBRAHangupCause(e):
     return e.getHeader(VARIABLE_SIP_H_P_LIBRA_HANGUP_CAUSE)
 
 

+ 2 - 2
src/core/callcenter/model.py

@@ -126,7 +126,7 @@ class AgentInfo:
 
 
 class CallInfo:
-    def __init__(self, core_uuid=None, call_id=None, conference=None, company_id=None, group_id=None,
+    def __init__(self, core_uuid=None, call_id=None, conference=None, saas_id=None, group_id=None,
                  hidden_customer=0, caller_display=None, caller=None, called_display=None, called=None,
                  number_location=None, agent_key=None, agent_name=None, login_type=None, ivr_id=None, task_id=None,
                  media_host=None, cti_host=None, client_host=None, record=None, record2=None, record_time=None,
@@ -138,7 +138,7 @@ class CallInfo:
         self.core_uuid = core_uuid  # 通话唯一标识
         self.call_id = call_id  # 通话唯一标识
         self.conference = conference  # 会议号
-        self.company_id = company_id  # 企业id
+        self.saas_id = saas_id  # 企业id
         self.group_id = group_id  # 所在技能组id
         self.hidden_customer = hidden_customer  # 隐藏客户号码(0:不隐藏;1:隐藏)
         self.caller_display = caller_display  # 主叫显号