Создание скрипта обработки вызовов

Введение

Скрипты обработки вызовов - новая мощная функция 3CX V20. Скрипты позволяют перехватывать звонки и обрабатывать их с помощью стандартного кода на C# - фактически предоставляя вам неограниченные возможности для анализа и применения индивидуальной логики. Создание скрипта обработки вызовов решает, например, такие задачи:

  • Анализ Caller ID вызывающего абонента и назначение ему конкретных операторов.
  • Поиск информации о клиенте на основе Caller ID и маршрутизация вызова к персональному менеджеру.
  • Проверка времени и даты и последующая обработка вызова.
  • Проверка даты и воспроизведение сообщения в зависимости от этой даты.

Вызов скрипта обработки вызовов

Прежде чем создавать скрипт, необходимо продумать, в каком месте и как он будет запускаться. Вы можете настроить для него диал-код или направить на скрипт вызовы. Для этого скрипту назначается DID, либо вызовы направляются на скрипт в соответствии со значением переменной.

В Update 2 появится возможность инициировать сценарий при каждом входящем вызове на SIP-транк!

После того как вы определили, как будет запускаться скрипт, необходимо фильтровать вызовы и написать логику обработки.

Обзор API обработки вызовов

API состоит из трех основных методов, которые переключают звонки на новое место назначения.

Группа методов Task<CallControlResult> RouteToAsync(this ActiveConnection ac, <Destination>)

Этот метод создает маршрут, который связан с указанным соединением ac

  • ac - активное соединение, принадлежащее Точке маршрутизации. Когда новое место назначения отвечает, его соединение заменяет ac. (участие Точки маршрутизации).
  • Скрипт должен обрабатывать сбои задач. Скрипт может просто вызвать MyCall.Return, чтобы завершить собственное соединение с вызывающим абонентом.
  • Скрипт может пытаться построить столько маршрутов, сколько ему требуется, но первый отвеченный маршрут отменит все остальные и заменит участие Точки маршрутизации в звонке (завершит звонок для Точки маршрутизации).
  • RouteToAsync может быть выполнен в любом состоянии соединения Точки маршрутизации. Таким образом, Точка маршрутизации может выполнять фоновую маршрутизацию, общаясь с вызывающим (воспроизведение подсказок, обработка DTMF и т.д.).
  • Когда задача успешно завершена, звонок отзывается из Точки маршрутизации (MyCall отключается), и новое место назначения продолжает обработку.

Группа методов Task<CallControlResult> DivertAsync(this ActiveConnection ac, <Destination>)

Этот метод перенаправляет звонок на новое место назначения без установления (ответа) соединения (вызов на Точке маршрутизации).

  • Если соединение уже установлено с точкой маршрутизации, метод не удастся, и следует использовать RouteToAsyc/ReplaceWithAsync.
  • Активное соединение (в состоянии вызова) принадлежащее Точке маршрутизации будет заменено новым местом назначения, и Точка маршрутизации будет отключена от звонка.
  • Этот метод полезен, если Точке маршрутизации не нужно взаимодействовать с вызывающим абонентом.
  • Если задача не удалась, скрипт может продолжать обрабатывать соединение с вызывающим абонентом.
  • Когда задача успешно завершена, звонок отзывается из Точки маршрутизации (соединение MyCall завершается), и новое место назначения начинает обработку звонка. Скрипт Точки маршрутизации переходит в режим завершения работы и должен завершить свою собственную задачу.

Группа методов Task<CallControlResult> ReplaceWithAsync(this ActiveConnection ac, <Destination>)

Также известен как метод "слепой перевод".

  • Разрешено только в режиме подключения (Точка маршрутизации приняла звонок и взаимодействует с пользователем).
  • Вызывающий абонент будет поставлен на удержание.
  • Задача не удастся, если место назначения недоступно.
  • Скрипт может продолжать обработку звонка после сбоя задачи (если вызывающий абонент все еще подключен к Точке маршрутизации).
  • Когда задача успешно завершена, соединение скрипта (ICallHandler.MyCall) завершается, и скрипт отключается от обработки звонка (новый участник берет на себя вызывающего абонента).

Пример скрипта обработки вызовов

Этот пример демонстрирует создание и программирование собственной Точки маршрутизации:

  1. Базовая структура кода C#, предоставленного Точке маршрутизации
  2. Базовое использование методов добавочного номера TCX.PBXAPI.CallControlAPI для интерфейса ActiveConnection (ICall)
  3. Базовая работа с объектом MyCall, предоставленным CallFlowScriptingCore
  4. Базовая работа с конфигурацией (параметры АТС)
  5. Использование метода расширения RouteToAsyc объекта ActiveConnection

Пример функциональности Точки маршрутизации:

  • Точка маршрутизации принимает только звонки, которые направляются с помощью слепого перевода с телефона (добавочный номер). Прямые звонки отклоняются.
  • Неограниченное количество звонков может быть перенаправлено на эту Точку маршрутизации любым добавочным номером одновременно (каждый звонок обрабатывается отдельно)
  • Звонок возвращается через 15 секунд отправителю (добавочному номеру) напрямую (без переадресации). Если возвращаемый звонок не отвечен в течение 15 секунд, звонок отменяется и повторяется снова через 15 секунд.
  • Вызывающий абонент слышит музыку на удержании, как это настроено для функции “Парковка вызова” на АТС.

Как запустить скрипт

  • Для создания Точки маршрутизации используйте любой номер, например, #101. Установите свойство RoutePoint.ScriptCode в соответствии с текстом ниже. Пока интерфейс пользователя не позволяет создавать Точку маршрутизации с пользовательским (написанным вручную) кодом - требуется какой-то zip-файл (который, в общем, не нужен для простого скрипта).
  • Точка маршрутизации должна появиться в соответствующем списке (на данный момент это "CFD application") с зеленым индикатором [компиляция этого кода не должна вызывать ошибок].
  • Затем:
  • Если любой абонент перенаправит свой вызов на номер #101, вызов будет возвращен обратно через 15 секунд.
  • Звонящий будет слышать музыку на удержании, как это настроено для функции "Парковка вызова". (Скрипт использует эту настройку, но код может быть изменен для генерации другого содержимого для звонящего).
  • Если возвращенный вызов не будет принят, Точка маршрутизации пробует снова (и снова) через 15 секунд после предыдущей попытки, пока звонящий не прекратит вызов или первоначальный абонент, перенаправивший вызов, не ответит (либо его вызов будет перехвачен).

Комментарии к коду:

  • Код "скриптового" объекта основан на использовании, реализации и/или наследовании:
  • Пространство имен CallFlow
  • CallFlow.ICall
  • CallFlow.ICallHandler
  • CallFlow.ICallHandlerEx
  • CallFlow.ScriptBase<T>
  • Класс "скриптового объекта" должен наследовать CallFlow.ScriptBase<T> и реализовывать все абстрактные методы, как требуется для его экземпляра.
  • Объект запускается, когда ScriptingHost выполняет обработчик вызова с использованием метода ICallHandler.Start.
  • Сценарий должен завершаться явно с помощью ICall.Return.
  • Предпочтительная реализация метода ICall.Start - "async void", который запускает отдельную задачу (должен перехватывать все исключения).
  • Реализация скрипта должна контролировать только объект MyCall, предоставленный Scripting Host. Это единственный объект сценария сессии вызова.
  • Когда MyCall (участие Точки маршрутизации в вызове) завершается, реализация скрипта должна завершиться и закончить свою сессию.
  • Скрипт потока вызова - это не способ мониторинга конфигурации системы или любых внешних ресурсов.
  • Это не способ мониторинга всех вызовов в системе.
  • Это просто логика Точки маршрутизации, которая может быть интегрирована с другими потоками вызовов.
  • Другими словами: скрипт обрабатывает один из вызовов, связанных с Точкой маршрутизации, но никогда не инициирует новый вызов.
  • API маршрутизации инкапсулирован в статический класс TCX.PBXAPI.CallControlAPI, который предоставляет методы расширения для:
  • TCX.Configuration.ActiveConnection
  • 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);

                }

            });

        }

    }

}

Дополнительная информация

Версия документа

Последнее обновление документа 5 марта 2024

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