Задача:
1) Есть группа операторов Call-центра, нужно сделать возможность после разговора, оценивать звонок.
2) Максимальная интеграция с FreePBX.
Решение:
В Интернете много вариантов, все они ориентированы на чистый Asterisk.
Примеры:
https://wiki.merionet.ru/ip-telephoniya/55/ocenka-raboty-operatora-posle-zvonka-na-asterisk/
https://habr.com/ru/post/147122/
http://asterisk-service.com/blog/asterisk-1/post/2-0-53
Моя же реализация заключается в том, что все настройки можно делать в Web-интерфейсе FreePBX.
Реализация тестировалась и работает на:
— версии Asterisk 1.8.10.1
— FreePBX версии 12.0.76.6
— PHP версии 5.3.10-1ubuntu3.26
— библиотека PHPAGI версии 2.20 2010/09/30 02:21:00 (качаем тут: phpagi.zip)
Первым делом нам нужно определить как генерируется постановка вызова в очередь. Выполняем на сервере Asterisk:
nano /etc/asterisk/extensions_additional.conf
Ищем контекст с названием [ext-queues]. Внутри должны быть строки вида
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | [ext-queues] include => ext-queues-custom exten => 11,1,Macro(user-callerid,) exten => 11,n,Answer exten => 11,n,Macro(blkvm-set,reset) exten => 11,n,ExecIf($["${REGEX("(M[(]auto-blkvm[)])" ${DIAL_OPTIONS})}" != "1"]?Set(_DIAL_OPTIONS=${DIAL_OPTIONS}M(auto-blkvm))) exten => 11,n,Set(__NODEST=${EXTEN}) exten => 11,n,Set(QCIDPP=${IF($[${LEN(${VQ_CIDPP})}>0]?${VQ_CIDPP}: )}) exten => 11,n,Set(VQ_CIDPP=) exten => 11,n,ExecIf($["${QCIDPP}"!=""]?Macro(prepend-cid,${QCIDPP})) exten => 11,n,Set(QAINFO=${IF($[${LEN(${VQ_AINFO})}>0]?${VQ_AINFO}: )}) exten => 11,n,Set(VQ_AINFO=) exten => 11,n,ExecIf($["${QAINFO}"!=""]?Set(__ALERT_INFO=${QAINFO})) exten => 11,n,Set(QJOINMSG=${IF($[${LEN(${VQ_JOINMSG})}>0]?${IF($["${VQ_JOINMSG}"!="0"]?${VQ_JOINMSG}: )}:custom/zednannja-vstanovleno)}) exten => 11,n,Set(VQ_JOINMSG=) exten => 11,n,Set(QRINGOPTS=r) exten => 11,n,Set(QRETRY=${IF($[${LEN(${VQ_RETRY})}>0]?${VQ_RETRY}:n)}) exten => 11,n,Set(VQ_RETRY=) exten => 11,n(qoptions),Set(QOPTIONS=${IF($[${LEN(${VQ_OPTIONS})}>0]?${VQ_OPTIONS}:t)}${QCANCELMISSED}${QRINGOPTS}${QRETRY}) exten => 11,n,Set(VQ_OPTIONS=) exten => 11,n(qgosub),Set(QGOSUB=${IF($[${LEN(${VQ_GOSUB})}>0]?${VQ_GOSUB}:${QGOSUB})}) exten => 11,n,Set(VQ_GOSUB=) exten => 11,n(qagi),Set(QAGI=${IF($[${LEN(${VQ_AGI})}>0]?${VQ_AGI}:${QAGI})}) exten => 11,n,Set(VQ_AGI=) exten => 11,n(qrule),Set(QRULE=${IF($[${LEN(${VQ_RULE})}>0]?${IF($["${VQ_RULE}"!="0"]?${VQ_RULE}: )}:${QRULE})}) exten => 11,n,Set(VQ_RULE=) exten => 11,n(qposition),Set(QPOSITION=${IF($[${LEN(${VQ_POSITION})}>0]?${VQ_POSITION}:${QPOSITION})}) exten => 11,n,Set(VQ_POSITION=) exten => 11,n,Gosub(sub-record-check,s,1(q,11,dontcare)) exten => 11,n,ExecIf($["${QJOINMSG}"!=""]?Playback(${QJOINMSG}, )) exten => 11,n,QueueLog(11,${UNIQUEID},NONE,DID,${FROM_DID}) exten => 11,n,Set(QAANNOUNCE=${IF($[${LEN(${VQ_AANNOUNCE})}>0]?${IF($["${VQ_AANNOUNCE}"!="0"]?${VQ_AANNOUNCE}: )}: )}) exten => 11,n,Set(VQ_AANNOUNCE=) exten => 11,n,Set(QMOH=${IF($["${VQ_MOH}"!=""]?${VQ_MOH}: )}) exten => 11,n,Set(VQ_MOH=) exten => 11,n,ExecIf($["${QMOH}"!=""]?Set(__MOHCLASS=${QMOH})) exten => 11,n,ExecIf($["${MOHCLASS}"!=""]?Set(CHANNEL(musicclass)=${MOHCLASS})) exten => 11,n,Set(QMAXWAIT=${IF($[${LEN(${VQ_MAXWAIT})}>0]?${VQ_MAXWAIT}: )}) exten => 11,n,Set(VQ_MAXWAIT=) exten => 11,n,Set(QUEUENUM=11) exten => 11,n,Set(QUEUEJOINTIME=${EPOCH}) exten => 11,n(qcall),Queue(11,${QOPTIONS},,${QAANNOUNCE},${QMAXWAIT},${QAGI},,${QGOSUB},${QRULE},${QPOSITION}) exten => 11,n,Macro(blkvm-clr,) exten => 11,n,Gosub(sub-record-cancel,s,1()) exten => 11,n,Set(__NODEST=) exten => 11,n,Set(_QUEUE_PRIO=0) exten => 11,n,Set(QRINGOPTS=) exten => 11,n,Set(QDEST=${VQ_DEST}) exten => 11,n,Set(VQ_DEST=) exten => 11,n(gotodest),GotoIf($["${QDEST}"=""]?ext-group,505,1:${CUT(QDEST,^,1)},${CUT(QDEST,^,2)},${CUT(QDEST,^,3)}) |
здесь нас интересует строка: exten => 11,n(qcall),Queue(11,${QOPTIONS},,${QAANNOUNCE},${QMAXWAIT},${QAGI},,${QGOSUB},${QRULE},${QPOSITION})
Приложение Queue() генерирует постановку вызова в очередь. Как видим с примера — приложению передаются параметры, которые были сгенерированные перед выполнением приложения Queue(). Смотрим в документацию: https://asterisk-pbx.ru/wiki/asterisk/app/queue
Как видим по документации, если при выполнении приложения Queue(), добавить в аргумент «options» параметр «c», то диаплан продолжит свое выполнения, когда вызываемый (оператор) положил трубку. В нашем случае — диаплан перейдет на пункт «Fail Over Destination» в FreePBX.
По контексту определяем, что если перед поступлением звонка в очередь, мы установим значение «c» переменной VQ_OPTIONS, то приложение Queue() згенерирует постановку вызова в очередь с этим параметом.
Приступаем к выполнению
nano /etc/asterisk/extensions_custom.conf
Создаем собственный контекст с установкой значения переменной VQ_OPTIONS:
1 2 3 4 | [queue-call-begin] exten => s,1,Set(VQ_OPTIONS=tc) ;на первом приоритете, мы устанавливаем значение переменной. Параметром "t" - я разрешаю оператору переводить звонки exten => s,n,System(php /var/www/call/call.php incall ${UNIQUEID} ${CALLERID(num)} ${CHANNEL} &) ;запуск фонового php скрипта на выполнения exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван |
Также создаем контексты с оценками
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [assessment-1] exten => s,1,Set(__OcenkaOper=1) ;выполняем необходимое действие exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван [assessment-2] exten => s,1,Set(__OcenkaOper=2) ;выполняем необходимое действие exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван [assessment-3] exten => s,1,Set(__OcenkaOper=3) ;выполняем необходимое действие exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван [assessment-4] exten => s,1,Set(__OcenkaOper=4) ;выполняем необходимое действие exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван [assessment-5] exten => s,1,Set(__OcenkaOper=5) ;выполняем необходимое действие exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван |
После exten => s,1,Set(__OcenkaOper=X)
можем выполнять еще команды. Например отправить сообщение в телеграмм:
exten => s,n,System(curl -s -X POST "https://api.telegram.org/bot545454443:AAAAAAAAAAAAAAAABBBBBBBBBBBCCC/sendmessage" -F chat_id="20202020" -F text="Оставлена оценка оператору: 5" &)
Или можно запустить php скрипт, который возвращает значение. Это значение присвоим переменной:
exten => s,n,Set(CallId=${SHELL(php /var/www/call/call.php inCallRegister ${CALLERID(num)})})
Логично, что если звонок не состоялся (оператор не взял трубку), то предлагать оценивать вызов не нужно. Для этого, создадим еще один контекст:
1 2 3 4 5 | [queue-call-end] exten => s,1,GotoIf($[${QUEUESTATUS}=TIMEOUT]?timeout:answered); очередь по завершению получает переменную QUEUESTATUS, от значения которого мы можем определить был ли принят звонок exten => s,n(timeout),Goto(app-announcement-18,s,1) ; если звонок не был принят, направим его на app-announcement-18,s,1 - это приветствие, в котором говорим, что сейчас все операторы заняты и мы ему перезвоним. После, терминируем звонок (hangup) - обязательно. exten => s,n(answered),Noop(return to queue-call-end) ; если статус звонка отличается от "TIMEOUT", сделаем вывод, что звонок состоялся, и переходим на следующий приоритет exten => s,n,Return ;возвращаемся на предыдущий контекст, откуда был вызван |
Сохраняем изменения в файле /etc/asterisk/extensions_custom.conf
и выходим с редактора nano — Ctrl+o, Ctrl+x.
Следующим этапом нам нужно зафиксировать созданные контексты в FreePBX.
Заходим в FreePBX → АДМИНИСТРАТОР (Admin) → Дополнительные назначения (Custom Destination)
Добавляем созданные ранее назначения:
Для примера опишу добавление контекста [queue-call-begin]
— в поле Спецназначение: указываем «queue-call-begin,s,1»
— в поле Описание: указываем «queue-call-begin» — название понятное Вам
— в поле Примечания: указываем описание для назначения понятное Вам
— Обязательно ставим галочку «Return», после чего появится выпадающий список дальнейших назначений — направляем в очередь (можно в другое место).
По аналогии, добавляем остальные назначения:
[assessment-1]
Спецназначение: assessment-1,s,1
Return: Приветствие → «Спасибо за оценку» → Терминовать звонок (Положить трубку).
[assessment-2]
Спецназначение: assessment-2,s,1
Return: Приветствие → «Спасибо за оценку» → Терминовать звонок (Положить трубку).
[assessment-3]
Спецназначение: assessment-3,s,1
Return: Приветствие → «Спасибо за оценку» → Терминовать звонок (Положить трубку).
[assessment-4]
Спецназначение: assessment-4,s,1
Return: Приветствие → «Спасибо за оценку» → Терминовать звонок (Положить трубку).
[assessment-5]
Спецназначение: assessment-5,s,1
Return: Приветствие → «Спасибо за оценку» → Терминовать звонок (Положить трубку).
[queue-call-end]
Спецназначение: queue-call-end,s,1
Return: IVR → «Оцените работу оператора».
Настройка Очереди:
Все настройки делаем на свое усмотрение, но в «Назначение при неответе», устанавливаем «Дополнительные назначения» → «queue-call-end».
Настройка IVR «Оцените работу оператора»:
Применяем настройки. В терминале сервера можно выполнить asterisk -rx "dialplan reload"
Что у нас получается на этот момент:
Поступает звонок. Первым делом — направляем его на «Дополнительные назначения» → «queue-call-begin», где присваиваем переменной VQ_OPTIONS значение «tc» (параметр c — продолжение выполнения диалплана после того, как оператор положил трубку). Далее звонок поступает в очередь, где генерируются вызовы агентам (операторам). Оператор поднял трубку → разговор → положил трубку. Звонок попадает в «Назначение при неответе» → «Дополнительные назначения» → «queue-call-end», где мы проверяем был ли отвечен звонок. Если звонок не был принят (TIMEOUT), направляем его на Приветствие app-announcement-18 (у Вас будет другое, ищите в nano /etc/asterisk/extensions_additional.conf).Если статус звонка (QUEUESTATUS) отличается от «TIMEOUT», продолжаем выполнение диалплана, где попадаем в Return Дополнительного назначения «queue-call-begin» → IVR «Оцените работу оператора». Здесь мы озвучиваем запись «Поставьте оценку от 1-го до 5-ти», ждем ввода. Если клиент ничего не ввел, либо ввел неверно, Терминируем звонок (Положить трубку). Если нажал одну с оценок, переходим в «Дополнительные назначение» → «assessment-X», где присваиваем переменной OcenkaOper определенное значение, и возвращаемся в поле Return Дополнительного назначения «assessment-X» → Приветствие «Спасибо за оценку». Приветствие «Спасибо за оценку» воспроизводит запись о благодарности за оценку и Терминирует звонок (Положить трубку).
Посмотреть логи прохождения звонка можно командой asterisk -rvvvv
Теперь нам нужно следить за звонком. Создаем каталог «call» в /var/www/, внутри него создаем php файл с именем call.php. Также внутри каталога call создаем каталог src и в корень его кладем файлы библиотеки PHPAGI
nano /var/www/call/call.php
Вставляем код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | <?php require('/var/www/call/src/phpagi.php'); /* следующие строки - подключение к asterisk AMI, доступы посмотреть можно здесь: /etc/asterisk/manager.conf, либо создать дополнительный доступ: https://wiki.merionet.ru/ip-telephoniya/13/nastrojka-ami-v-freepbx-13/ */ $service_port = "5038"; $address = "127.0.0.1"; $user_ami = "admin"; $pass_ami = "admin"; $token_telegram='44444444444:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-UaaUUUU'; $chat_id_telegram='222222222222'; $responsible=NULL; $stat=false; //статус звонка (начат разговор=true, или нет=false). принимает значение true\false $ocenka=NULL; //переменная, хранит в себе поставленную оценку $Uniqueid1='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $Uniqueid2='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $Uniqueid='0'; //Глобальная переменная, которая НЕ будет изменятся, она статическая на все время работы скрипта (перевод звонка и т.п.) $UniqueID='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $CallerID1='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $CallerID2='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $Channel1='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $Channel2='0'; //Глобальная переменная, которая будет изменятся по циклу работы скрипта (перевод звонка, и т.п.) $i=true; //Глобальная переменная, которая определяет цикл работы скрипта. true = продолжает работать скрипт, false = скрипт закончит работу $redirekt=false; //Статус перевода звонка $per=false; //временный статус перевода звонка function telegram_message($TOKEN='',$CHAT_ID='',$MESSAGE=''){ $telegram_data = array ( 'text' => $MESSAGE, 'chat_id' => $CHAT_ID, 'parse_mode' => 'html' ); file_get_contents("https://api.telegram.org/bot".$TOKEN."/sendMessage?".http_build_query($telegram_data)); } function dump_incall($ecode,$data,$server,$port) { //telegram_message($GLOBALS["token_telegram"],$GLOBALS["chat_id_telegram"],'перевод звонка '.json_encode($data)); if(((($data['Event']=='VarSet')&&($data['Value']=='tr'))&&($data['Variable']=='ARG2'))&&(($data['Channel']==$GLOBALS["Channel1"])||($data['Channel']==$GLOBALS["Channel2"]))) $GLOBALS["per"]=true; //определяем перевод звонка if(((($data['Event']=='VarSet')&&($GLOBALS["stat"]==true))&&($data['Variable']=='ARG3'))&&(($data['Channel']==$GLOBALS["Channel1"])||($data['Channel']==$GLOBALS["Channel2"]))){ $GLOBALS["per"]=false; $GLOBALS["UniqueID"]=$data['Uniqueid']; $GLOBALS["stat"]=false; $GLOBALS["redirekt"]=true; //telegram_message($GLOBALS["token_telegram"],$GLOBALS["chat_id_telegram"],'перевод звонка '.json_encode($data)); } if(($data['Event']=='Newstate')&&($GLOBALS["CallerIdNum"]==$data['ConnectedLineNum'])) if(($data['ChannelStateDesc']=='Up')&&(preg_match("/^SIP/",$data['Channel']))) $GLOBALS["UniqueID"]=$data['Uniqueid']; if(($GLOBALS["UniqueID"]==$data['Uniqueid2'])||($GLOBALS["UniqueID"]==$data['Uniqueid1'])||($GLOBALS["UniqueID"]==$data['Uniqueid'])||($GLOBALS["UniqueID"]==$data['UniqueID'])){ if(($data['Event']=='Bridge')&&($data['Bridgestate']=='Link')){ //поднята трубка if($GLOBALS["stat"]==false){ $GLOBALS["stat"]=true; $GLOBALS["responsible"]=$data['CallerID2']; //определяем ответственного за звонок $GLOBALS["Uniqueid2"]=$data['Uniqueid2']; $GLOBALS["Uniqueid1"]=$data['Uniqueid1']; $GLOBALS["Channel1"]=$data['Channel1']; $GLOBALS["Channel2"]=$data['Channel2']; //telegram_message($GLOBALS["token_telegram"],$GLOBALS["chat_id_telegram"],'Поднята трубка. начат разговор '.json_encode($data)); } } if((($data['Event']=='Dial')&&($data['SubEvent']=='Begin'))&&($GLOBALS["redirekt"]==true)){ $GLOBALS["responsible"]=$data['Dialstring']; //telegram_message($GLOBALS["token_telegram"],$GLOBALS["chat_id_telegram"],'зафиксирован перевод звонка. Новые данные: CallerIDNum='.$data['CallerIDNum'].' Dialstring='.$data['Dialstring'].' '.json_encode($data)); $GLOBALS["redirekt"]=false; $GLOBALS["Channel1"]=$data['Channel']; $GLOBALS["Channel2"]=$data['Destination']; $GLOBALS["UniqueID"]=$data['UniqueID']; $GLOBALS["Uniqueid1"]=$data['UniqueID']; $GLOBALS["Uniqueid2"]=$data['DestUniqueID']; } } if($GLOBALS["Uniqueid"]==$data['Uniqueid']) if($data['Event']=='Hangup') { telegram_message($GLOBALS["token_telegram"],$GLOBALS["chat_id_telegram"],'Завершена работа скрипта. выход из цыкла '.json_encode($data)); $GLOBALS["i"]=false; } } if (isset($argv[1])) { $status=$argv[1]; } else echo "Введите параметры!"; if($status=='incall'){ if ((isset($argv[2])&&isset($argv[3]))&&isset($argv[4])) { $Uniqueid=$argv[2]; $CallerIdNum=$argv[3]; $Channel=$argv[4]; $manager = new AGI_AsteriskManager(); $manager->connect($address, $user_ami, $pass_ami); //telegram_message($token_telegram,$chat_id_telegram,'Входящий звонок. id='.$Uniqueid.' caller='.$CallerIdNum.' channel='.$Channel); while($i){ $manager->Events('on'); $manager->wait_response(true); $manager->add_event_handler('*', 'dump_incall'); if(!$ocenka) { $tmpOc=$manager->send_request('GetVar',array('Variable' => 'OcenkaOper', 'Channel' => $Channel)); if(isset($tmpOc['Value']) && ($tmpOc['Value']!='')){ $ocenka=$tmpOc['Value']; //telegram_message($token_telegram,$chat_id_telegram,'ocenka='.json_encode($tmpOc)); } } } $manager->disconnect(); if(isset($ocenka)) { if($ocenka=='5') $smile="\xF0\x9F\x98\x8D"; if($ocenka=='4') $smile="\xF0\x9F\x98\x83"; if($ocenka=='3') $smile="\xF0\x9F\x98\x8F"; if($ocenka=='2') $smile="\xF0\x9F\x98\x96"; if($ocenka=='1') $smile="\xF0\x9F\x98\xA1"; telegram_message($token_telegram,$chat_id_telegram,$smile." Оценен входящий звонок от ".$CallerIdNum.". Ответственный оператор ".$responsible.". Оставлена оценка ".$ocenka); } //telegram_message($token_telegram,$chat_id_telegram,'Завершение скрипта по номеру канала '.$Uniqueid.'. ответственный='.$responsible); } else echo "Введите параметры!"; } ?> |
Вносим свои изменения, сохраняем и выходим: CTRL+o, CTRL+x.
Что делает скрипт? Он запускается в контексте [queue-call-begin], и следит за звонком до самого его завершения. Он определяет кто поднят трубку, на кого звонок был перенапрвален, и была ли оставлена оценка. После чего — отправляет сообщение в телеграм. Вместо телеграма, можно отправлять оценку куда угодно (SQL, 1С, Битрикс24, и т.д.)
Делаем тестовый звонок, проверяем работоспособность, запускаем в продакшн.
Alex
Здравствуйте,
я настроил по вашему мануалу, но у меня не работает.
Я не получаю ответ в $tmpOc. В телеграме говорит $tmpOc={«Response»:»Error»,»Message»:»No such channel»}
Я проверял через телнет, у меня ответ приходит.
В чем может быть причина?
adminblog
1) Скажите какая у Вас версия Asterisk-а и FreePBX-а?
2) Вы создали контексты в файле /etc/asterisk/extensions_custom.conf? А именно [assessment-1],[assessment-2]…. и т.д.?
3) После завершения разговора, Вам запрашивает поставить оценку?
Alex
1. Версия Asterisk 13.32.0
2. Да конечно создал контексты.
3. Да, запрашивает поставить оценку.
А после всего он просто ввесит в процессах
root@freepbx /var/www/call # ps -aux | grep call
asterisk 8921 1.4 0.9 478428 18960 ? S 13:44 0:00 php /var/www/call/call1.php incall 1587714242.4 7272500404 SIP/out_oktell-00000001
adminblog
Скрипт разрабатывался под версию Астериска 1.8.10.1, к сожалению сейчас нет возможности проверить на Вашей версии.
Alex
Нашел проблему.
В скрипте call.php переместите
$manager->disconnect();
после
//telegram_message($token_telegram,$chat_id_telegram,’Завершение скрипта по номеру канала ‘.$Uniqueid.’. ответственный=’.$responsible);
По тестам показывает, что после закрытия соединение не передается параметр оцененка