如何创建呼叫处理脚本

介绍

呼叫处理脚本是 V20 的一项强大新功能。它允许您捕获呼叫并使用标准 C# 代码进行处理,从而为您提供无限的可能性来分析呼叫并应用自定义逻辑。以下是一些示例:

  • 分析来电显示并分配特定客服人员
  • 根据来电显示进行客户查找并相应路由。
  • 检查时间和日期,并据此处理来电。
  • 检查日期并根据日期播放提示音。

调用呼叫处理脚本

编写脚本之前,您必须考虑脚本的触发位置和触发方式。您可以为其配置拨号代码,或者通过分配 DID 号码或根据特定变量将呼叫转发到脚本。

在Update 2 中,您将能够对每个进入 SIP 中继的呼入呼叫触发呼叫!

确定脚本的触发方式后,您需要筛选呼叫并编写呼叫处理逻辑。

呼叫处理 API 概述

该 API 由三个核心方法组成,这些方法会将呼叫传递给新的目的地。

方法组 Task<CallControlResult> RouteToAsync(this ActiveConnection ac, <Destination>)

此方法创建一条路由,该路由绑定到指定的连接 ac

  • ac - 由 RoutePoint(路由点)拥有的活动连接。当新目的地应答时,其连接将替换 ac。(涉及 RoutePoint 的参与)。
  • 脚本应处理任务失败。脚本可以简单地调用 MyCall.Return 来终止其与主叫方的连接。
  • 脚本可以根据需要尝试建立尽可能多的路由,但第一个被应答的路由将取消所有其他路由,并将替换 RoutePoint 在呼叫中的参与(将终止 RoutePoint 的呼叫)。
  • RouteToAsync 可以在 RoutePoint 连接的任何状态下执行。因此,路由点可以在与主叫方通信时执行后台路由(播放提示音、处理 DTMF 信号等)。
  • 当任务成功时,呼叫将从 RoutePoint 撤回(MyCall 被断开),新目的地继续处理呼叫。

方法组 Task<CallControlResult> DivertAsync(this ActiveConnection ac,<Destination>)

方法将呼叫转移到新目的地,而不在 RoutePoint 上建立(应答)连接(即在 RoutePoint 振铃)。

  • 如果呼叫已与 RoutePoint 建立,该方法将失败,应改用 RouteToAsync 或 ReplaceWithAsync。
  • 由 RoutePoint 拥有的活动连接(处于振铃状态)将被新目的地替换,并且 RoutePoint 将从呼叫中断开。
  • 如果路由点不需要与主叫方交互,此方法非常有用。
  • 如果任务失败,脚本可以继续处理与主叫方的连接。
  • 当任务成功时,呼叫将从 RoutePoint 撤回(MyCall 连接终止),新目的地开始呼叫处理。RoutePoint 脚本切换到整理模式,需要完成其自身的任务。

方法组 Task<CallControlResult> ReplaceWithAsync(this ActiveConnection ac,<Destination>)

也称为“盲转”法。

  • 仅在已连接模式下允许(RoutePoint 已接听呼叫并与用户交互)。
  • 主叫方将被置于保持状态。
  • 如果目的地不可达,任务将失败。
  • 任务失败后,脚本可以继续处理呼叫(如果主叫方仍与 RoutePoint 保持连接)。
  • 当任务成功时,脚本连接(ICallHandler.MyCall)终止,脚本与呼叫处理分离(新参与者将接管主叫方)。

示例呼叫处理脚本

此示例演示了如何创建自定义路由点以及如何对其进行编程:

  1. C# 代码提供给 RoutePoint 的基本结构
  2. TCX.PBXAPI.CallControlAPI 扩展方法在 ActiveConnection (ICall) 接口上的基本用法
  3. CallFlowScriptingCore 提供的 MyCall 对象的基本操作
  4. 配置(PBX 参数)的基本操作
  5. 使用 ActiveConnection 对象的 RouteToAsync 扩展方法

RoutePoint 功能示例:

  • 路由点只接受通过话机(分机)使用盲转发送的呼叫。直接呼叫被拒绝。
  • 任何分机都可以同时将不限数量的呼叫转接至此 RoutePoint(每个呼叫单独处理)。
  • 15 秒后,呼叫将直接返回到转接方(分机)(不应用转发)。如果返回的呼叫在 15 秒内未被应答,则呼叫取消并在 15 秒后再次尝试。
  • 主叫方听到的保持音乐与 PBX 上为“停泊”功能配置的音乐相同。

如何运行

  • 创建一个具有任意号码的路由点(RoutingPoint),例如 #101,并将 RoutePoint.ScriptCode 属性设置为下面的文本(当前 UI 仍不允许使用自定义(手写代码)创建路由点,它需要某种 zip 文件,但对于简单脚本来说这并不是必需的)。
  • RoutePoint 应出现在相应列表中(目前是“CFD 应用程序”),并带有绿点(此代码编译不应失败)。
  • 然后:
  • 如果有任何分机将其呼叫转接到号码 #101,该呼叫将在 15 秒后被返回。
  • 主叫方将听到为“停车”功能配置的保持音乐。(脚本使用此设置,但可以修改代码为主叫方生成其他内容)。
  • 如果返回的呼叫未被应答,RoutePoint 将在上一次尝试后 15 秒再次尝试(并一直重复),直到主叫方挂断呼叫,或者原始转接方应答(或其呼叫被接听)。

代码注释

  • “脚本化”对象的代码基于(使用、实现和/或继承)
  • CallFlow namespace
  • CallFlow.ICall
  • CallFlow.ICallHandler
  • CallFlow.ICallHandlerEx
  • CallFlow.ScriptBase<T>
  • “脚本对象”类必须继承CallFlow.ScriptBase<T>并根据实例需要实现所有抽象方法。
  • ScriptingHost 使用 ICallHandler.Start 方法执行呼叫处理程序时,该对象开始运行。
  • 脚本必须以 ICall.Return 显式结束。
  • ICall.Start 方法的推荐实现是 async void,它运行分离的任务(必须捕获所有异常)。
  • 脚本实现必须仅控制由脚本主机公开的 MyCall 对象。这是呼叫脚本会话的唯一主体。
  • 当 MyCall(路由点在呼叫中的参与)结束时,脚本实现应进行整理并结束其会话。
  • 呼叫流程脚本不是监视系统配置或任何外部资源的方式。
  • 它不是监视系统中所有呼叫的方式。
  • 它只是一个 RoutePoint 逻辑,可以与其他呼叫流程集成。
  • 换句话说:脚本处理与 RoutePoint 连接的其中一个呼叫,但绝不发起新的呼叫。
  • 路由 API 封装在静态类 TCX.PBXAPI.CallControlAPI 中,该类公开了以下对象的扩展方法:
  • TCX.Configuration.ActiveCannection
  • TCX.Configuration.DN
  • TCX.Configuration.RegistrarRecord

示例代码

#nullable disable

using CallFlow;

using System;

using System.Threading;

using System.Threading.Tasks;

using TCX.Configuration;

using TCX.PBXAPI;

namespace dummy

{

    public class ParkingRoutePointSample : ScriptBase<ParkingRoutePointSample>

    {

        async Task<CallControlResult> ProcessAutoPickup(RoutePoint sp, DestinationStruct returnTo, CancellationToken token)

        {

            while (true)

                try

                {

                    return await Task.Delay(TimeSpan.FromSeconds(15), token).ContinueWith(x =>

                    {

                        MyCall.Trace("{0} - automatic redirection of the call from {1}.{2} to '{3}'", MyCall.DN, MyCall.Caller?.CallerID, MyCall.Caller?.DN, returnTo);

                        return MyCall.RouteToAsync(new RouteRequest

                        {

                            RouteTarget = returnTo,

                            TimeOut = TimeSpan.FromSeconds(15) //will ring until failure

                        }

                        );

                    }

                    , TaskContinuationOptions.NotOnCanceled).Unwrap();

                }

                catch (OperationFailed ex)

                {

                    MyCall.Trace("Automatic redirection failed: {0}", ex.TheResult);

                    MyCall.Trace("Continue hold call from {0}({1}) on {2}", MyCall.Caller?.CallerID, MyCall.Caller?.DN, MyCall.DN);

                    continue;

                }

        }

        PhoneSystem ps = null;  

        /// <summary>

        ///

        /// </summary>

        public override async void Start()

        {

            await Task.Run(async () =>

            {

                try

                {

                    MyCall.Debug($"Script start delay: {DateTime.UtcNow - MyCall.LastChangeStatus}");

                    MyCall.Debug($"Incoming connection {MyCall}");

                    ps = MyCall.PS as PhoneSystem;

                    CallControlResult lastresult = null;

                    DN referredBy = null;

                    RoutePoint thisPark = null;

                    string callerID = "";

                    DN callerDN = null;

                    bool scriptCompleted = true;

                    try

                    {

                        referredBy = MyCall.ReferredByDN?.GetFullSnapshot() as Extension;

                        thisPark = MyCall.DN?.Clone() as RoutePoint;

                        callerID = MyCall.Caller?.CallerID;

                        callerDN = MyCall.Caller?.DN?.Clone() as DN;

                        MyCall.Trace(

                            "Parked call from {0}({1}) on {2}", callerID, callerDN, thisPark

                        );

                        if (referredBy == null)

                        {

                            MyCall.Trace("{0} rejects call from {1}. Reason: No referrer specified", thisPark, callerDN);

                            return;

                        }

                        var cancelationToken = new CancellationTokenSource();

                        MyCall.OnTerminated += () =>

                        {

                            cancelationToken.Cancel();

                        };

                        lastresult = await MyCall.AssureMedia().ContinueWith(

                            x =>

                            {

                                if(!string.IsNullOrWhiteSpace(ps.GetParameterValue("PARK_MOH_SOURCE")))

                                    MyCall.SetBackgroundAudio(true, new string[] { ps.GetParameterValue("PARK_MOH_SOURCE") });

                                else

                                    MyCall.SetBackgroundAudio(true, new string[] { ps.GetParameterValue("MUSICONHOLDFILE") });

                                return ProcessAutoPickup(thisPark, new DestinationStruct(referredBy), cancelationToken.Token);

                            }, TaskContinuationOptions.OnlyOnRanToCompletion).Unwrap();

                    }

                    catch (PBXIsNotConnected ex)

                    {

                        MyCall.Error($"Call control API is not available:\n{ex}");

                        scriptCompleted = false;

                    }

                    catch (TaskCanceledException)

                    {

                        MyCall.Trace($"Call was disconnected from parking place");

                    }

                    catch (Exception ex)

                    {

                        MyCall.Error($"Parking failure:\n{ex}");

                        scriptCompleted = false;

                    }

                    finally

                    {

                        try

                        {

                            MyCall.Info("Call from {0}({1}) parked by {2} on {3} finished with result={4}", callerID, callerDN, referredBy, thisPark, lastresult?.ToString() ?? "terminated");

                        }

                        catch (Exception ex)

                        {

                            MyCall.Error($"SharedParkingFlow finalize exception {ex}");

                        }

                        MyCall.Return(scriptCompleted);

                    }

                }

                catch

                {

                    MyCall.Return(false);

                }

            });

        }

    }

}

另请参阅

最后更新时间

本文件最后更新日期为2024年3月5日。

https://www.3cx.cn/docs/manual/call-processing-script/