소스 검색

外呼流程开发

刘威 5 달 전
부모
커밋
f5bb815df2

+ 4 - 4
src/core/callcenter/agent.py

@@ -90,11 +90,11 @@ class AgentOperService:
         return self._push_event_for_checkout(agent, req.scene)
 
     def busy(self, req: AgentActionRequest):
-        agent = _get_agent(req.saas_id, req.agent_number, req.out_id)
+        agent = _get_agent(req.saas_id, req.agent_id)
         if not agent or agent.agent_state == AgentState.DISABLE.code:
             raise BizException(BizErrorCode.AGENT_DISABLE_NOT_ALLOW_OPERATE)
 
-        agent_monitor = _get_agent_monitor(req.saas_id, req.agent_number)
+        agent_monitor = _get_agent_monitor(req.saas_id, agent.agent_num)
         if not agent_monitor or agent_monitor.check_state == AgentCheck.OUT.code:
             raise BizException(BizErrorCode.AGENT_CHECK_OUT_NOT_ALLOW_OPERATE)
 
@@ -679,10 +679,10 @@ class AgentStateService:
         return idle_agents[0].get_phone_num()
 
 
-def _get_agent(saas_id, agent_number, out_id):
+def _get_agent(saas_id, agent_id=None, agent_number=None, out_id=None):
     agent = Agent.query.filter(
         Agent.saas_id == saas_id,
-        or_(Agent.out_id == out_id, Agent.agent_num == agent_number)
+        or_(Agent.out_id == agent_id, Agent.out_id == out_id, Agent.agent_num == agent_number)
     ).first()
     return agent
 

+ 187 - 4
src/core/callcenter/api.py

@@ -5,18 +5,23 @@ import json
 from urllib.parse import urlparse
 from typing import Dict, Any, Optional
 
-from src.core.callcenter.enumeration import CallType
+from src.core.callcenter.exception import SipUriSyntaxException
+from src.core.datasource import SIP_SERVER
+from src.core.callcenter.esl.constant.esl_constant import SPLIT, SIP_HEADER
+from src.core.callcenter.enumeration import CallType, DeviceType
 
 
-class MakeCallRequest:
+class AgentCallRequest:
     """
     呼叫请求对象
     """
 
-    def __init__(self, saas_id, call_type: CallType, caller, called, caller_display="", called_display="",
+    def __init__(self, saas_id, agent_id, call_type: CallType, caller, called, caller_display="", called_display="",
                  uuid1=None, uuid2=None, follow_data: Dict[str, Any] = {}):
         # 租户id
         self.saas_id = saas_id
+        # 坐席号
+        self.agent_id = agent_id
         # 呼叫类型
         self.call_type: CallType = call_type
         # 主叫,如果没有传,则使用坐席号码
@@ -89,7 +94,7 @@ class AgentActionRequest:
     坐席操作
     """
 
-    def __init__(self, saas_id, agent_id, agent_number, out_id, identity_type, scene='manual'):
+    def __init__(self, saas_id, agent_id, agent_number=None, out_id=None, identity_type=None, scene='manual'):
         self.saas_id =saas_id
         # 坐席工号
         self.agent_id = agent_id
@@ -189,6 +194,184 @@ class CheckInCallRequest:
         self.agent_number = agent_number
 
 
+class MakeCallContext:
+
+    def __init__(self, 
+                 pbx_server=SIP_SERVER,
+                 route_gateway_name="gateway-fxo",
+                 display: Optional[str] = None,
+                 caller: Optional[str] = None,
+                 called: Optional[str] = None,
+                 call_id: Optional[str] = None,
+                 device_id: Optional[str] = None,
+                 eavesdrop: Optional[str] = None,
+                 device_type: Optional[int] = None,
+                 timeout: Optional[int] = 30,
+                 originate_timeout: Optional[int] = 30,
+                 sip_header_map: Optional[Dict[str, str]] = [],
+                 called_prefix: Optional[str] = "",
+                 service_id: Optional[str] = None,
+                 call_type: Optional[int] = None):
+        # fs 地址
+        self.pbx_server = pbx_server
+        # 线路名(非必传)
+        self.route_gateway_name = route_gateway_name
+        # 外显号(必传)
+        self.display = display
+        # 该腿的呼叫者(必传)
+        self.caller = caller
+        # 该腿的被呼叫者(必传)
+        self.called = called
+        # 该腿的callId (必传)
+        self.call_id = call_id
+        # 该腿的uuid(必传)
+        self.device_id = device_id
+        # 是否是监听腿 监听腿必填true,其它不填
+        self.eavesdrop = eavesdrop
+        # 当前腿的类型(必填 客户 坐席 监听者 机器人)
+        self.device_type = device_type
+        # 超时挂断
+        self.timeout = timeout
+        # 超时挂断(后面可以考虑删掉)
+        self.originate_timeout = originate_timeout
+        # 额外的sip头添加(非必填)
+        self.sip_header_map = sip_header_map or {}
+        # 被叫前缀 (用户腿必填)
+        self.called_prefix = called_prefix
+        # 任务id(机器人外呼必填)
+        self.service_id = service_id
+        # 呼叫类型(必填)
+        self.call_type = call_type
+
+    def get_sip_header(self) -> str:
+        headers = [
+            f"{SIP_HEADER}contact_user={self.display}",
+            "ring_asr=true",
+            f"origination_caller_id_number={self.display}",
+            f"origination_caller_id_name={self.display}",
+            f"origination_uuid={self.device_id}",
+            "absolute_codec_string=^^:PCMU:PCMA,",
+        ]
+
+        if self.originate_timeout is not None:
+            headers.append(f"originate_timeout={self.originate_timeout}")
+
+        headers += [
+            "RECORD_STEREO=true",
+            "RECORD_APPEND=true",
+            f"{SIP_HEADER}call_id={self.call_id}",
+            f"{SIP_HEADER}caller={self.caller}",
+            f"{SIP_HEADER}called={self.called}",
+            f"{SIP_HEADER}device_id={self.device_id}",
+            f"{SIP_HEADER}device_type={self.device_type}",
+        ]
+
+        if self.eavesdrop:
+            headers.append(f"{SIP_HEADER}eavesdrop={self.eavesdrop}")
+        if self.service_id:
+            headers.append(f"{SIP_HEADER}service_id={self.service_id}")
+
+        # if self.device_type == DeviceType.CUSTOMER:
+        #     headers += [
+        #         "RECORD_STEREO_SWAP=true"
+        #     ]
+        # else:
+        #     headers += [
+        #         "RECORD_STEREO_SWAP=false",
+        #         "continue_on_fail=true"
+        #     ]
+
+        if self.sip_header_map:
+            headers.extend([f"{SIP_HEADER}{k}={v}" for k, v in self.sip_header_map.items()])
+
+        return SPLIT.join(headers)
+
+    def get_called(self) -> str:
+        if self.called_prefix and self.device_type == DeviceType.CUSTOMER:
+            return f"{self.called_prefix}{self.called}"
+        return self.called
+
+
+class SipURI:
+    DEFAULT_PORT = -1
+    SIP_SCHEME = "sip"
+    SCHEME_SEPARATOR = ':'
+
+    def __init__(self, sip_uri: str):
+        self.string_representation = sip_uri
+        self.userinfo = None
+        self.host = None
+        self.port = self.DEFAULT_PORT
+        self.uri_parameters = {}
+
+        scheme = f"{self.SIP_SCHEME}{self.SCHEME_SEPARATOR}"
+        if not sip_uri.startswith(scheme):
+            raise SipUriSyntaxException(f"SIP URI must start with {scheme}")
+
+        buf = sip_uri[len(scheme):]
+        at_pos = buf.find("@")
+
+        if at_pos == 0:
+            raise SipUriSyntaxException("userinfo cannot start with a '@'")
+        if at_pos > 0:
+            self.userinfo = buf[:at_pos]
+            buf = buf[at_pos + 1:]
+
+        end_hostport = buf.find(";")
+        if end_hostport == 0:
+            raise SipUriSyntaxException("hostport not present or it cannot start with ';'")
+        if end_hostport < 0:
+            end_hostport = len(buf)
+
+        hostport = buf[:end_hostport]
+        buf = buf[end_hostport:]
+
+        colon_pos = hostport.find(":")
+        if colon_pos > -1:
+            if colon_pos == len(hostport) - 1:
+                raise SipUriSyntaxException("hostport cannot terminate with a ':'")
+            self.port = int(hostport[colon_pos + 1:])
+            self.host = hostport[:colon_pos]
+        else:
+            self.host = hostport
+
+        if buf == ";":
+            buf = ""
+
+        while buf:
+            if buf[0] == ";":
+                buf = buf[1:]
+
+            next_semicolon = buf.find(";")
+            if next_semicolon < 0:
+                next_semicolon = len(buf)
+
+            next_equals = buf.find("=")
+            if next_equals < 0 or next_equals > next_semicolon:
+                next_equals = next_semicolon
+
+            key = buf[:next_equals]
+            value = buf[next_equals + 1:next_semicolon] if next_equals < next_semicolon else ""
+            self.uri_parameters[key] = value
+
+            buf = buf[next_semicolon:]
+
+    def __str__(self):
+        return self.string_representation
+
+    def get_host(self):
+        return self.host
+
+    def get_port(self):
+        return self.port
+
+    def get_uri_parameters(self):
+        return self.uri_parameters
+
+    def get_userinfo(self):
+        return self.userinfo
+
+
 class AgentInfo:
     def __init__(self, sass_id=None, agent_number=None, realm=None, sip_server=None,
                  call_id=None, device_id=None, real_device_id=None, line_id=None, fs_user=None, domain=None,

+ 23 - 12
src/core/callcenter/call.py

@@ -6,8 +6,9 @@ import time
 import src.core.callcenter.cache as Cache
 from src.core.callcenter.constant import saasId, HOLD_MUSIC_PATH
 from src.core.callcenter.enumeration import CallCause, Direction, NextType, DeviceType, CdrType
-from src.core.callcenter.api import MakeCallRequest, AgentInfo, CallInfo, HangupCallRequest, CheckInCallRequest, \
-    DeviceInfo, NextCommand
+from src.core.callcenter.api import AgentCallRequest, AgentInfo, CallInfo, HangupCallRequest, CheckInCallRequest, \
+    DeviceInfo, NextCommand, MakeCallContext
+from src.core.callcenter.esl.constant.sip_header_constant import sipHeaderServiceId
 from src.core.callcenter.snowflake import Snowflake
 
 
@@ -18,14 +19,14 @@ class CallService:
         self.logger = logger
         self.snowflake = Snowflake(worker_id=1, data_center_id=1)
 
-    def call(self, request: MakeCallRequest, agent: AgentInfo):
+    def call(self, request: AgentCallRequest):
         call_id = 'C' + self.snowflake.next_id()
         device_id = 'D' + self.snowflake.next_id()
         now = lambda: int(round(time.time() * 1000))
 
-        route_gateway = Cache.get_route_gateway(saasId)
+        agent = Cache.get_agent_info(request.agent_id)
         call_info = CallInfo(call_id=call_id, agent_key=agent.agent_number, cti_host=agent.sip_server,
-                             caller=request.caller, called=request.called, direction=Direction.INBOUND,
+                             caller=agent.agent_number, 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, saas_id=saasId)
@@ -35,7 +36,12 @@ class CallService:
         call_info.next_commands.append(NextCommand(device_id, NextType.NEXT_CALL_OTHER))
         call_info.device_info_map[device_id] = device_info
         Cache.add_call_info(call_info)
-        self.client.make_call(route_gateway, request.called, request.caller, call_id, device_id)
+
+        context = MakeCallContext(display=request.called, caller=request.called, called=request.caller,
+                                  call_id=call_id, device_id=device_id, device_type=device_info.device_type,
+                                  call_type=call_info.call_type)
+
+        self.client.make_call_new(context)
         return call_id
 
     def hold(self, call_info: CallInfo, device_id):
@@ -54,18 +60,23 @@ class CallService:
         device_id = 'D' + self.snowflake.next_id()
         now = lambda: int(round(time.time() * 1000))
 
-        device = DeviceInfo(device_id=device_id, caller=caller, display=caller, called=agent_number, call_id=call_id,
-                            call_time=now, cdr_type=CdrType.TRANSFER.code, device_type=DeviceType.AGENT.code)
+        device_info = DeviceInfo(device_id=device_id, caller=caller, display=caller, called=agent_number, call_id=call_id,
+                            call_time=now, cdr_type=CdrType.TRANSFER.code, device_type=DeviceType.AGENT)
         call_info.device_list.append(device_id)
+
         call_info.caller = agent_number
-        call_info.device_info_map[device_id] = device
-        call_info.next_commands.append(NextCommand(device.device_id, NextType.NEXT_TRANSFER_CALL, call_info.device_list[0]))
+        call_info.device_info_map[device_id] = device_info
+        call_info.next_commands.append(NextCommand(device_info.device_id, NextType.NEXT_TRANSFER_CALL, call_info.device_list[0]))
         call_info.agent_key = agent_number
         # agent.sip_server
-        route_gateway = Cache.get_route_gateway(saasId)
         Cache.add_call_info(call_info)
         Cache.add_agent_info(agent=agent, call_id=call_id, device_id=device_id)
-        self.client.make_call(route_gateway, caller, agent_number, call_id, device_id)
+
+        sip_header_map = {sipHeaderServiceId: service_id}
+        context = MakeCallContext(display=call_info.called, caller=call_info.called, called=agent_number,
+                                  call_id=call_id, device_id=device_id, device_type=device_info.device_type,
+                                  call_type=call_info.call_type, service_id=service_id, sip_header_map=sip_header_map)
+        self.inbound_client.make_call_new(context)
 
     def hangup(self, request: HangupCallRequest):
         call_info = Cache.get_call_info(request.call_id)

+ 21 - 2
src/core/callcenter/esl/client.py

@@ -12,14 +12,15 @@ import threading
 import traceback
 import concurrent.futures
 import src.core.callcenter.cache as Cache
+from src.core.callcenter.api import MakeCallContext
 from src.core.callcenter.constant import SK, EMPTY
 from src.core.callcenter.esl.constant.esl_constant import BRIDGE_VARIABLES, BRIDGE, HANGUP, NORMAL_CLEARING, SIP_HEADER, SPACE, SPLIT, SOFIA, \
     ORIGINATE, PARK, SET, EAVESDROP, SMF_ALEG, EXECUTE, PLAYBACK, PAUSE, TRANSFER, UUID_TRANSFER, UUID_BROADCAST, UUID_BREAK, UUID_HOLD, \
     UUID_RECORD, UUID_SETVAR, UUID_GETVAR
 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 CallCause
+from src.core.callcenter.esl.constant.sip_header_constant import sipHeaderHoldMusic, profile1, profile2
+from src.core.callcenter.enumeration import CallCause, DeviceType
 from src.core.callcenter.esl.handler.default_esl_event_handler import DefaultEslEventHandler
 from src.core.datasource import SERVE_HOST
 from src.core.voip.constant import *
@@ -112,6 +113,24 @@ class InboundClient:
         except:
             traceback.print_exc()
 
+    def make_call_new(self, context: MakeCallContext):
+        called = context.get_called()
+        params = {'gateway': context.route_gateway_name, 'called': called, 'realm': context.pbx_server}
+
+        builder = [
+            '{', context.get_sip_header(), '}'
+        ]
+
+        if context.device_type == DeviceType.CUSTOMER:
+            profile = self.expression(profile1, params)
+            builder.append(f"{SOFIA}{SK}{profile}{SK}{called}{PARK}")
+        else:
+            profile = self.expression(profile2, params)
+            builder.append(f"{SOFIA}{SK}{profile}{SK}{called}{PARK}")
+        cmd = "".join(builder)
+        self.logger.info(cmd)
+        # self.con.bgapi(ORIGINATE, cmd)
+
     def make_call(self, route_gateway, display, called, call_id, device_id, timeout=30, originate_timeout=30, *sip_headers):
 
         # called = f"{called}{AT}{route_gateway.media_host}{CO}{route_gateway.media_port}"

+ 1 - 1
src/core/callcenter/esl/constant/sip_header_constant.py

@@ -17,4 +17,4 @@ sipHeaderDomain = "P-LIBRA-domain"
 sipHeaderCtiFlowId = "P-LIBRA-CtiFlowId"
 
 sipHeaderClueOutId = "X-EXTID"
-sipHeaderHoldMusic = "P-LIBRA-need-hold-music"
+sipHeaderHoldMusic = "P-LIBRA-need-hold-music"

+ 5 - 7
src/core/callcenter/esl/handler/channel_answer_handler.py

@@ -9,7 +9,7 @@ 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
 from src.core.callcenter.esl.handler.esl_event_handler import EslEventHandler
-from src.core.callcenter.api import CallInfo, DeviceInfo, NextCommand
+from src.core.callcenter.api import CallInfo, DeviceInfo, NextCommand, MakeCallContext
 
 
 @EslEventName(CHANNEL_ANSWER)
@@ -66,11 +66,6 @@ class ChannelAnswerHandler(EslEventHandler):
 
         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,
@@ -81,7 +76,10 @@ class ChannelAnswerHandler(EslEventHandler):
         Cache.add_call_info(call)
         Cache.add_agent_info(call_info=call, call_id=call_id, device_id=device_id)
 
-        self.client.make_call(route_gateway, call.caller, called, call_id, new_device_id)
+        context = MakeCallContext(display=call.caller, caller=call.caller, called=called,
+                                  call_id=call_id, device_id=device_id, device_type=device.device_type,
+                                  call_type=call.call_type)
+        self.inbound_client.make_call_new(context)
 
     def call_bridge(self, call: CallInfo, device: DeviceInfo, next_command: NextCommand, event):
         self.logger.info("开始桥接电话: callId:%s, caller:%s, called:%s, device1:%s, device2:%s", call.call_id,

+ 7 - 0
src/core/callcenter/exception.py

@@ -12,6 +12,13 @@ class BizException(Exception):
         self.status_code = status_code
 
 
+class SipUriSyntaxException(Exception):
+    def __init__(self, message, status_code=400):
+        super().__init__(message)
+        self.message = message
+        self.status_code = status_code
+
+
 @app.errorhandler(BizException)
 def handle_biz_exception(error):
     return error_response(msg=str(error), http_code=200)

+ 4 - 5
src/core/callcenter/views.py

@@ -14,7 +14,7 @@ from src.core.callcenter.esl.client import InboundClient, OutboundClient
 from flask import Flask, request, render_template_string
 
 from src.core.callcenter.call import CallService
-from src.core.callcenter.api import MakeCallRequest, AgentInfo, AgentActionRequest, HangupCallRequest, \
+from src.core.callcenter.api import AgentCallRequest, AgentActionRequest, HangupCallRequest, \
     HumanServiceQueryRequest
 # from src.core.voip.bot import BotAgent
 
@@ -22,7 +22,7 @@ from src.core.callcenter.api import MakeCallRequest, AgentInfo, AgentActionReque
 # agent = BotAgent(app.logger)
 agent = None
 inbound_client = InboundClient(agent, app.logger)
-outbound_client = OutboundClient(agent, app.logger)
+# outbound_client = OutboundClient(agent, app.logger)
 call_service = CallService(inbound_client, app.logger)
 agent_service = AgentService(inbound_client, app.logger)
 agent_oper_service = AgentOperService(inbound_client, app.logger)
@@ -143,9 +143,8 @@ def manual_call():
     # ext?: object
     # callId: string
     data = request.json()
-    agent = Cache.get_agent_info(data.get('agentId'))
-    req = MakeCallRequest(saas_id=data.get('vccId'), call_type=CallType.OUTBOUND_CALL, caller=agent.agent_number, called=data.get('called'), follow_data=data.get('ext'))
-    res = call_service.call(req, agent)
+    req = AgentCallRequest(saas_id=data.get('saas_id'), call_type=CallType.OUTBOUND_CALL, called=data.get('called'), follow_data=data.get('ext'))
+    res = call_service.call(req)
     return success_response(res)