В мире телефонии автоматизированная система распределения вызовов (ACD) – это система, которая распределяет входящие вызовы между определенной группой агентов на основе выбора клиента, номера телефона клиента, выбранной входящей линии в систему или времени. дня звонок был обработан. Мы также называем это колл-центром.
Пару лет назад Чарльз Оппенгеймер из Twilio создал демонстрацию встраиваемого ACD Salesforce с использованием Twilio Client и Ruby.
Большое спасибо Чарльзу за то, что он поделился этой демонстрацией. Мы просто взяли эту демонстрацию и преобразовали ее в Node.js с бэкендом на базе Flybase для распределения вызовов, а не в исходной системе Ruby/Mongo. В результате получился более чистый колл-центр, который легко модифицировать и интегрировать в другие CRM. .
Что будем использовать:
- Flybase.io в качестве нашей серверной части, обрабатывающей хранение данных, передачу событий и наши очереди вызовов.
- Клиент Twilio — это WebRTC-интерфейс к Twilio. В нашей демонстрации мы используем библиотеку javascript, которая дает нам API и подключение к Twilio для получения вызова в нашем браузере Salesforce, доставляющего вызов через WebRTC. Клиент Twilio также дает нам возможность управлять звонком через наш программный телефон.
- Heroku будет использоваться в качестве нашего веб-хостинга, но вы можете разместить свой колл-центр где угодно.
- Salesforce Open CTI — это открытый API, позволяющий сторонним поставщикам CTI подключать телефонные каналы к интерфейсу Salesforce CRM. В нашей демонстрации мы используем Open CTI для размещения нашего программного телефона и управления функцией набора номера/текста нажатием кнопки. Демонстрация не требует плагинов или установленного программного обеспечения благодаря дизайну Open CTI. Подробнее см. в Руководстве разработчика.
Фактическая интеграция отдела продаж не является обязательной, и вы можете легко вставить свой программный телефон в другую CRM. Часть 2 этого руководства фактически использует flybase для создания простой CRM с программным телефоном, включенным в качестве виджета.
Начиная
Вы можете найти полный исходный код здесь.
Во-первых, давайте настроим наше приложение node.js.
Создайте package.json:
{ "name": "callcenter", "version": "0.0.1", "description": "Client ACD powered by Flybase, Twilio and Node.js", "main": "app.js", "repository": "https://github.com/flybaseio/callcenter", keywords": [ "twilio", "data mcfly", "flybase", "twilio", "sms" ], "author": "Roger Stringer", "license": "MIT", "dependencies": { "body-parser": "~1.4.2", "ejs": "~0.8.5", "express": "~3.4.8", "flybase": "1.7.2", "less-middleware": "~0.2.1-beta", "method-override": "~2.0.2", "moment": "~2.5.1", "node-buzz": "~1.1.0", "twilio": "~1.6.0" }, "engines": { "node": "0.12" } }
Это сообщит нашему колл-центру, какие модули мы хотим установить для нашего приложения node.
Теперь мы хотим создать наш файл app.js для обработки всей нашей серверной работы:
var express = require('express'); var bodyParser = require('body-parser'); var methodOverride = require('method-override'); var path = require('path'); var q = require('q'); var bluebird = require('bluebird'); var config = require( path.join(__dirname, 'app', 'config') ); var app = express(); app.set('views', path.join(__dirname, 'app', 'views')); app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static( path.join(__dirname, 'app', 'public'))); var port = process.env.PORT || 5000; // set our port var twilio = require('twilio'); var client = twilio(config.twilio.sid, config.twilio.token); var flybase = require('flybase'); var callsRef = flybase.init(config.flybase.app_name, "calls", config.flybase.api_key); var agentsRef = flybase.init(config.flybase.app_name, "agents", config.flybase.api_key); var queueid = ''; var good2go = false; // backend routes ========================================================= client.queues.list(function(err, data) { var to_go = data.queues.length; data.queues.forEach(function(queue) { if( queue.friendlyName === config.twilio.queueName ){ queueid = queue.sid; console.log( "Queueid = #" + queueid + " for #" + config.twilio.queueName ); good2go = true; } to_go--; if( to_go == 0 ){ if( queueid === '' ){ client.queues.create({ friendlyName: config.twilio.queueName }, function(err, queue) { queueid = queue.sid; }); } } }); }); // listen for events via Flybase... // if an agent gets disconnected then we log them off... agentsRef.on('agent-removed', function (data) { var data = JSON.parse( data ); console.log( data.username + " has left the building"); update_agent(data.username,{ status: 'LoggedOut' }); }); // return number of agents with status set to Ready agentsRef.on('get-ready-agents', function (data) { var adNag = function() { agentsRef.where({"status": 'Ready'}).on('value',function( rec ){ console.log( rec.count() + ' agents are Ready' ); if( rec.count() ){ agentsRef.trigger('agents-ready', rec.count() ); }else{ agentsRef.trigger('agents-ready', "0" ); } }); }; setTimeout(adNag, 1500); }); // listen for outgoing calls app.post('/dial', function (req, res) { var phonenumber = req.param('PhoneNumber'); var dial_id = config.twilio.fromNumber; if( typeof req.param('CallerID') !== 'undefined' ){ var dial_id = req.param('CallerID'); } var twiml = new twilio.TwimlResponse(); twiml.dial(phonenumber, { callerId:dial_id }); console.log("Response text for /dial post = #", twiml.toString()); res.writeHead(200, { 'Content-Type':'text/xml' }); res.end( twiml.toString() ); }); // listen for incoming calls app.post('/voice', function (req, res) { var queuename = config.twilio.queueName; var sid = req.param('CallSid'); var callerid = req.param('Caller'); var addtoq = 0; var dialqueue = ''; var client_name = ''; // search for agent who has been set to `Ready` for the longest time and connect them to the caller... getlongestidle(true, function( bestclient ){ if( bestclient ){ console.log("Routing incoming voice call to best agent = #", bestclient); var client_name = bestclient; }else{ console.log( 'no agent was found, adding caller to #', config.twilio.queueName ); var dialqueue = queuename; addtoq = 1; } var twiml = new twilio.TwimlResponse(); if( addtoq ){ twiml.say("Please wait for the next available agent",{ voice:'woman' }).enqueue(config.twilio.queueName); }else{ twiml.dial({ 'timeout':'10', 'action':'/handledialcallstatus', 'callerId':callerid }, function(node) { this.client( client_name ); }); update_call(sid, { 'sid': sid, 'agent': client_name, 'status': 'ringing' }); } console.log("Response text for /voice post = #", twiml.toString()); res.writeHead(200, { 'Content-Type':'text/xml' }); res.end( twiml.toString() ); }); }); app.post('/handledialcallstatus', function (req, res) { var sid = req.param('CallSid'); var twiml = new twilio.TwimlResponse(); if( req.param('DialCallStatus') == 'no-answer' ){ callsRef.where({"sid": sid}).on('value',function( rec ){ if( rec.count() !== null ){ var sidinfo = rec.first().value(); if( sidinfo ){ var agent = sidinfo.agent; update_agent(agent, { 'status': 'missed' }); } // Change agent status for agents that missed calls } // redirect and try to get new agent... twiml.redirect('/voice'); }); }else{ twiml.hangup(); } console.log("Response text for /handledialcallstatus post = #", twiml.toString()); res.writeHead(200, { 'Content-Type':'text/xml' }); res.end( twiml.toString() ); }); // assign a twilio call token to the agent app.get('/token', function(req, res) { var client_name = "anonymous"; if( typeof req.param("client") !== "undefined" ){ client_name = req.param("client"); } var capability = new twilio.Capability( config.twilio.sid, config.twilio.token ); capability.allowClientIncoming( client_name ); capability.allowClientOutgoing( config.twilio.appid ); var token = capability.generate(); res.end(token); }); // return flybase info to the softphone... app.get('/getconfig', function(req, res) { res.json({ app_name: config.flybase.app_name, api_key: config.flybase.api_key }); }); // return a phone number app.get('/getcallerid', function(req, res) { var client_name = "anonymous"; if( typeof req.param("from") !== "undefined" ){ client_name = req.param("from"); } res.end( config.twilio.fromNumber ); }); app.post('/track', function(req, res) { }); app.get('/', function(req, res) { var client_name = "anonymous"; if( typeof req.param("client") !== "undefined" ){ client_name = req.param("client"); } res.render('index', { client_name: client_name, anycallerid: 'none' }); }); var server = app.listen(port, function() { console.log('Listening on port %d', server.address().port); }); // various functions ========================================================= // find the caller who's been `Ready` the longest function getlongestidle( callrouting, callback ){ if( callrouting ){ agentsRef.where({"status": "DeQueing"}).orderBy( {"readytime":-1} ).on('value').then(function( data ){ var agent = data.first().value(); callback( agent.client ); },function(err){ agentsRef.where({"status": "Ready"}).orderBy( {"readytime":-1} ).on('value').then(function( data ){ var agent = data.first().value(); callback( agent.client ); },function(err){ callback( false ); }); }); }else{ agentsRef.where({"status": "Ready"}).orderBy( {"readytime":-1} ).on('value').then(function( data ){ var agent = data.first().value(); callback( agent.client ); },function(err){ callback( false ); }); } } // check if user exists and if they do then we update, otherwise we insert... function update_agent(client, data, cb){ var d = new Date(); var date = d.toLocaleString(); var callback = cb || null; agentsRef.where({"client": client}).once('value').then( function( rec ){ var agent = rec.first().value(); for( var i in data ){ agent[i] = data[i]; } agentsRef.push(agent, function(resp) { console.log( "agent updated" ); if( callback !== null ){ callback(); } }); },function(err){ data.client = client; agentsRef.push(data, function(resp) { console.log( "agent inserted" ); if( callback !== null ){ callback(); } }); }); } function update_call(sid, data){ var d = new Date(); var date = d.toLocaleString(); callsRef.where({"sid": sid}).on('value').then( function( rec ){ var call = rec.first().value(); for( var i in data ){ call[i] = data[i]; } callsRef.push(call, function(resp) { console.log( "call updated" ); }); },function(err){ data.sid = sid; callsRef.push(data, function(resp) { console.log( "call inserted" ); }); }); } // call queue handling ========================================================= var qsum = 0; var checkQueue = function() { qsum += 1; var qsize = 0; var readyagents = 0; var qname = config.twilio.queueName; client.queues(queueid).get(function(err, queue) { qsize = queue.currentSize; console.log( 'There are #' + qsize + ' callers in the queue (' + queueid + ')' ); if( qsize > 0 ){ agentsRef.where({"status": "Ready"}).orderBy( {"readytime":-1} ).on('value').then(function( agents ){ var readyagents = agents.count(); var bestclient = agents.first().value(); console.log("Found best client - routing to #" + bestclient.client + " - setting agent to DeQueuing status so they aren't sent another call from the queue"); update_agent(bestclient.client, {status: "DeQueing" }, function(){ console.log('redirecting call now!'); client.queues(queueid).members("Front").update({ url: config.twilio.dqueueurl, method: "POST" }, function(err, member) { // console.log(member.position); }); }); },function(err){ console.log("No Ready agents during queue poll #" + qsum); }); agentsRef.trigger('agents-ready', readyagents ); agentsRef.trigger('in-queue', qsize ); // restart the check checking setTimeout(checkQueue, 3000); }else{ // restart the check checking console.log("No callers found during queue poll #" + qsum); setTimeout(checkQueue, 3000); } }); }; setTimeout(checkQueue, 1500);
В этом файле много чего происходит. Сначала нам потребуются наши различные библиотеки и настроен экспресс, затем мы приступим к нашей фактической работе.
Вы заметите, что мы настроили ссылки на Flybase:
- callRef подключается к нашей таблице вызовов и обрабатывает хранение и получение информации о входящих вызовах.
- agentRef подключается к нашей таблице агентов и обрабатывает хранение и извлечение информации для агентов.
Первая серверная задача, которую мы обрабатываем, — это проверка наших очередей twilio для извлечения идентификатора очереди или нашей очереди вызовов или же ее создания, если она не существует. Мы используем эту очередь для хранения входящих вызовов, если в нашем колл-центре нет доступных агентов, и они остаются в очереди, пока не появится агент.
Затем мы настраиваем прослушиватели событий для двух событий:
- удаленный агент: когда агент выходит из системы, мы обновляем его запись пользователя, чтобы сделать его не готовым
- get-ready-agents: просто возвращает количество агентов, которые в данный момент находятся в состоянии готовности.
Затем у нас есть наши фактические конечные точки URI:
- /dial — это запрос POST, который обрабатывается Twilio для совершения исходящих вызовов между веб-браузером агента и номером телефона.
- /voice — это POST-запрос, который обрабатывает входящие звонки с телефонных номеров. Это работает путем поиска агента, состояние которого было установлено на «Готов» в течение длительного времени, и назначения его на вызов. Если агент не готов, мы помещаем вызывающего абонента в очередь и проверяем его позже.
- /handdialcallstatus — это POST-запрос, который вызывается после завершения вызова. Он проверяет, был ли ответ на вызов, нет ли ответа, и в зависимости от DialCallStatus, возвращенного из Twilio, либо помещает вызывающего абонента обратно в очередь и выводит агента из состояния готовности, либо завершает вызов, поскольку предполагается, что вызов выполнен. .
- /token — это запрос GET, вызываемый через вызов ajax на внешнем интерфейсе, чтобы назначить токен возможностей клиента Twilio агенту, когда они вошли в систему.
- /getconfig — это запрос GET, вызываемый также через вызов ajax от клиента, который возвращает настройки Flybase колл-центра для использования софтфоном во внешнем интерфейсе.
- / — это запрос GET, который отображает программный телефон и присваивает имя клиенту на основе строки запроса ?client.
У нас есть три основные функции, которые используются колл-центром для решения различных задач:
- getlongestidle — это функция, которая проверяет наличие агента со статусом Ready или DeQueing и возвращает имя клиента этого агента. Если агенты не найдены, мы возвращаем false и помещаем вызывающего абонента в свою очередь. DeQueing — это особый статус, который мы установим в конце нашего кода, когда агент станет доступным.
- update_agent возьмет идентификатор агента и обновит его учетную запись в базе данных Flybase новой информацией, такой как обновления статуса при вызове, переходе в автономный режим и т. д.
- update_call используется так же, как update_agent, но для отслеживания звонков.
Наконец, у нас есть обработка очереди, это функция, называемая checkQueue, которая вызывается через 1,5 секунды после загрузки приложения, а затем каждые 3 секунды и выполняет простую задачу:
- Он входит в цикл, чтобы вернуть всех вызывающих абонентов в очереди вызовов.
- Если есть вызывающие абоненты, ожидающие подключения к агентам, он будет искать агента со статусом, установленным на Готов, и который был готов дольше всех, путем сортировки по полю готовности.
- Если агент готов, то мы устанавливаем статус этого агента на DeQueing и подключаем вызывающего абонента в начале очереди к этому агенту, вызывая наш dqueueurl.
- Если нет агентов в состоянии Готов или в очереди нет вызывающих абонентов, то мы устанавливаем тайм-аут для повторного вызова функции через 3 секунды и возвращаемся к шагу 1 цикла checkQueue.
Затем мы хотим создать папку с именем app, а затем внутри этой папки создать файл с именем config.js:
module.exports = { twilio: { sid: "ACCOUNT-SID", token: "AUTH-TOKEN", appid: 'APP-ID', fromNumber : "TWILIO-NUMBER", welcome : "Thank you for calling.", hangup : false, queueName: "cnacd", dqueueurl:"http://yourwebsite.com/voice" },flybase: { api_key: "YOUR-API-KEY", app_name: "YOUR-FLYBASE-APP" };
Обновите этот файл, чтобы он содержал информацию о Twilio и информацию о Flybase.
Для получения информации о Twilio вам необходимо создать приложение Twiml в своей учетной записи Twilio, создать приложение и отправить его на веб-сайт вашего колл-центра по адресу /dial.
Кроме того, создайте новый номер телефона в Twilio и отправьте этот номер телефона на веб-сайт вашего колл-центра по адресу /voice.
Существует переменная с именем queueName, которая представляет собой имя очереди, которую вы хотите использовать в своем колл-центре, а также переменная с именем dqueueurl, это URL-адрес вашего веб-сайта с добавленным к нему /voice. Это понадобится вам для задачи исключения из очереди, поскольку Twilio требует абсолютный URL-адрес.
софтфон
Внутри папки приложения создайте две папки:
Теперь внутри public создайте файл с именем index.html:
<!DOCTYPE html> <html> <head> <title>Twilio Softphone</title> <script type="text/javascript" src="https://static.twilio.com/libs/twiliojs/1.2/twilio.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script> <script src="https://na15.salesforce.com/support/api/31.0/interaction.js"></script> <script src="https://na15.salesforce.com/support/console/31.0/integration.js"></script> <script src="https://cdn.flybase.io/flybase.js"></script> <script type="text/javascript" src="/js/softphone.js"></script> <link rel="stylesheet" type="text/css" href="/css/dialer.css"> </head> <body> <div id="client_name" hidden="true"><%= client_name %></div> <div id="softphone" class="softphone"> <div id="agent-status-controls" class="clearfix"> <button class="agent-status ready">Ready</button> <button class="agent-status not-ready">Not Ready</button> <div class="agent-status active">Call In-Progress</div> </div><!-- /agent-status --> <div id="agent-status"> <p></p> </div><!-- /agent-status --> <div class="divider"></div> <div id="number-entry"> <input placeholder="+1 (555) 555-5555"></input> <div class="incoming-call-status">Incoming Call</div> </div><!-- /number-entry --> <div id="dialer"> <div id="dialer-container"> <div class="numpad-container"> <div class="number" value="1">1</div><div class="number" value="2">2</div><div class="number" value="3">3</div><div class="number" value="4">4</div><div class="number" value="5">5</div><div class="number" value="6">6</div><div class="number" value="7">7</div><div class="number" value="8">8</div><div class="number" value="9">9</div><div class="number ast" value="*">∗</div><div class="number" value="0">0</div><div class="number" value="#">#</div> </div><!-- /numpad-container --> </div><!-- /dialer-container --> </div><!-- /dialer --> <div id="action-button-container"> <div id="action-buttons"> <button class="call">Call</button> <button class="answer">Answer</button> <button class="hangup">Hangup</button> <button class="mute">Mute</button><button class="hold">Hold</button><button class="unhold">UnHold</button> </div><!-- /action-buttons --> </div><!---action-button-containe --> <div id="call-data"> <h3>Caller info</h3> <ul class="name"><strong>Name: </strong><span class="caller-name"></span></ul> <ul class="phone_number"><strong>Number: </strong><span class="caller-number"></span></ul> <ul class="queue"><strong>Queue: </strong><span class="caller-queue"></span></ul> <ul class="message"><strong>Message: </strong><span class="caller-message"></span></ul> </div><!-- /call-data --> <div id="callerid-entry" style="display:<%= anycallerid %>"> <input placeholder="Change your callerid"></input> </div><!-- /number-entry --> <div id="team-status"> <div class="agents-status"><div class="agents-num">-</div>Agents</div> <div class="queues-status"><div class="queues-num">-</div>In-Queue</div> </div><!-- /team-status --> <div class="powered-by"> <img src="/images/poweredby.png" /> </div> </div><!-- /softphone --> </body> </html>
Это наш индексный файл, который обрабатывает выходные данные нашего программного телефона, которые агенты могут использовать для приема и совершения вызовов.
Внутри общей папки создайте папку с именем css и включите следующие два файла:
номеронабиратель.css:
/* reset css */ article,aside,details,figcaption,figure,footer,header,hgroup,hr,menu,nav,section{display:block}a,hr{padding:0}abbr,address,article,aside,audio,b,blockquote,body,canvas,caption,cite,code,dd,del,details,dfn,div,dl,dt,em,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,p,pre,q,samp,section,small,span,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,ul,var,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:0 0}ins,mark{background-color:#ff9;color:#000}body{line-height:1}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}a{margin:0;font-size:100%;vertical-align:baseline;background:0 0}ins{text-decoration:none}mark{font-style:italic;font-weight:700}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{height:1px;border:0;border-top:1px solid #ccc;margin:1em 0}input,select{vertical-align:middle} .clearfix:before, .clearfix:after { content: " "; display: table; } .clearfix:after { clear: both; } .clearfix { *zoom: 1; } *, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } body { font-family: "Helvetica", Arial, sans-serif; background-color: white; } #softphone { width: 175px; margin: 10px auto 0px; } #agent-status-controls { margin: 10px 0 20px; position: relative; } .agent-status { border: none; padding: 6px 10px; background-image: linear-gradient(bottom, #ddd 20%, #eee 72%); background-image: -o-linear-gradient(bottom, #ddd 20%, #eee 72%); background-image: -moz-linear-gradient(bottom, #ddd 20%, #eee 72%); background-image: -webkit-linear-gradient(bottom, #ddd 20%, #eee 72%); background-image: -ms-linear-gradient(bottom, #ddd 20%, #eee 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #ddd), color-stop(0.72, #eee)); color: #333; text-shadow: 0px -1px 0px rgba(255, 255, 255, 0.3); box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 0.4); cursor: pointer; text-align: center; } button.agent-status { display: inline-block; float: left; width: 50%; margin: 0; -webkit-appearance: none; -moz-appearance: none; appearance: none; } @-webkit-keyframes pulse { 0% {background-color: #EA6045;} 50% {background-color: #e54a23;} 100% {background-color: #EA6045;} } div.agent-status { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; font-size: 12px; line-height: 12px; background-image: none; background-color: #EA6045; -webkit-animation: pulse 1s infinite alternate; color: #fff; text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.2); border-radius: 2px; } .agent-status:active, .agent-status:focus { outline: none; } .agent-status[disabled] { box-shadow: inset 0px 0px 15px rgba(0, 0, 0, 0.6); opacity: 0.8; text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.4); } .agent-status.ready { border-radius: 2px 0 0 2px; } .agent-status.ready[disabled] { background-image: linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -o-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -moz-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -webkit-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -ms-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #7eac20), color-stop(0.72, #91c500)); color: #f5f5f5; } .agent-status.not-ready { border-radius: 0 2px 2px 0; } .agent-status.not-ready[disabled] { background-image: linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -o-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -moz-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -webkit-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -ms-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #e64118), color-stop(0.72, #e54a23)); color: #f5f5f5; } #dialer { border: solid 1px #ddd; border-width: 0 0 0 1px; -webkit-transition: opacity 1s; transition: opacity 1s; } input { border: solid 1px #ddd; border-bottom-color: #d5d5d5; border-radius: 2px 2px 0 0; font-size: 16px; width: 100%; padding: 14px 5px; display: block; text-align: center; margin: 0; position: relative; z-index: 100; -webkit-transition: border-color 1s; transition: border-color 1s; } #number-entry { position: relative; height: 48px; } .incoming input { border: solid 1px red; } .incoming #dialer { opacity: 0.25; } .softphone .incoming-call-status { position: absolute; display: none; top: 100%; left: 0; right: 0; background: red; color: #fff; font-size: 16px; padding: 6px 0; text-align: center; width: 100%; z-index: 200; border-radius: 0 0 2px 2px; opacity: 0; -webkit-transition: opacity 1s; transition: opacity 1s; } .incoming .incoming-call-status { display: block; opacity: 1; } .number { color: #555; font-weight: 300; cursor: pointer; display: inline-block; height: 38px; line-height: 38px; font-size: 21px; width: 33.333333333%; background-image: linear-gradient(bottom, #e9e9e9 20%, #e5e5e5 72%); background-image: -o-linear-gradient(bottom, #e9e9e9 20%, #e5e5e5 72%); background-image: -moz-linear-gradient(bottom, #e9e9e9 20%, #e5e5e5 72%); background-image: -webkit-linear-gradient(bottom, #e9e9e9 20%, #e5e5e5 72%); background-image: -ms-linear-gradient(bottom, #e9e9e9 20%, #e5e5e5 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #e9e9e9), color-stop(0.72, #e5e5e5)); text-shadow: 0px 1px 0px #f5f5f5; filter: dropshadow(color=#f5f5f5, offx=0, offy=1); text-align: center; box-shadow: inset 1px 0px 0px rgba(255, 255, 255, 0.4), inset -1px 0px 0px rgba(0, 0, 0, 0.1), inset 0px 1px 0px #f5f5f5, inset 0 -1px 0px #d6d6d6; } .number.ast { font-size: 33px; line-height: 32px; vertical-align: -1px; } .number:hover { background-image: linear-gradient(bottom, #f5f5f5 20%, #f0f0f0 72%); background-image: -o-linear-gradient(bottom, #f5f5f5 20%, #f0f0f0 72%); background-image: -moz-linear-gradient(bottom, #f5f5f5 20%, #f0f0f0 72%); background-image: -webkit-linear-gradient(bottom, #f5f5f5 20%, #f0f0f0 72%); background-image: -ms-linear-gradient(bottom, #f5f5f5 20%, #f0f0f0 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #f5f5f5), color-stop(0.72, #f0f0f0)); } .number:active { box-shadow: inset 1px 0px 0px rgba(255, 255, 255, 0.4), inset -1px 0px 0px rgba(0, 0, 0, 0.1), inset 0px 1px 0px #f5f5f5, inset 0 -1px 0px #d6d6d6, inset 0px 0px 5px 2px rgba(0, 0, 0, 0.15); } #action-buttons button { -webkit-appearance: none; -moz-appearance: none; appearance: none; display: inline-block; border: none; margin: 0; cursor: pointer; } #action-buttons .call { color: #f5f5f5; width: 100%; font-size: 18px; padding: 8px 0; text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.3); margin: 0; background-image: linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -o-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -moz-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -webkit-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -ms-linear-gradient(bottom, #7eac20 20%, #91c500 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #7eac20), color-stop(0.72, #91c500)); border-radius: 0 0 2px 2px; } #action-buttons .answer, #action-buttons .hangup { color: #f5f5f5; width: 100%; font-size: 18px; padding: 8px 0; text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.4); margin: 0; background-image: linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -o-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -moz-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -webkit-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -ms-linear-gradient(bottom, #e64118 20%, #e54a23 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #e64118), color-stop(0.72, #e54a23)); border-radius: 0 0 2px 2px; } #action-buttons .hold, #action-buttons .unhold, #action-buttons .mute { color: #444; width: 50%; font-size: 14px; padding: 12px 0; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.3); margin: 0; background-image: linear-gradient(bottom, #bbb 20%, #ccc 72%); background-image: -o-linear-gradient(bottom, #bbb 20%, #ccc 72%); background-image: -moz-linear-gradient(bottom, #bbb 20%, #ccc 72%); background-image: -webkit-linear-gradient(bottom, #bbb 20%, #ccc 72%); background-image: -ms-linear-gradient(bottom, #bbb 20%, #ccc 72%); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, #bbb), color-stop(0.72, #ccc)); box-shadow: inset 1px 0px 0px rgba(255, 255, 255, 0.4), inset -1px 0px 0px rgba(0, 0, 0, 0.1); } .mute { border-radius: 0 0 0 2px; } .hold, .unhold { border-radius: 0 2px 0 0; } #team-status .agents-status, #team-status .queues-status { display: inline-block; width: 45%; margin: 0; font-size: 14px; text-align: center; padding: 12px 0 16px; border-bottom: solid 1px #e5e5e5; } #team-status [class*="num"] { font-size: 32px; font-weight: bold; margin-bottom: 6px; } #call-data { display: none; } .powered-by { text-align: right; padding: 10px 0; } img { width: 100px; }
Наконец, мы хотим настроить внешний код нашего софтфона.
Создайте папку внутри общедоступной с именем js и добавьте softphone.js:
// Page loaded $(function() { // ** Application container ** // window.SP = {} // Global state SP.state = {}; SP.agentsRef = {}; SP.callsRef = {}; SP.agent = {}; SP.state.callNumber = null; SP.state.calltype = ""; SP.username = $('#client_name').text(); SP.currentCall = null; //instance variable for tracking current connection SP.requestedHold = false; //set if agent requested hold button SP.functions = {}; // Get a Twilio Client name and register with Twilio SP.functions.getTwilioClientName = function(sfdcResponse) { sforce.interaction.runApex('UserInfo', 'getUserName', '' , SP.functions.registerTwilioClient); } SP.functions.registerTwilioClient = function(response) { console.log("Registering with client name: " + response.result); // Twilio does not accept special characters in Client names var useresult = response.result; useresult = useresult.replace("@", "AT"); useresult = useresult.replace(".", "DOT"); SP.username = useresult; console.log("useresult = " + useresult); $.get("/getconfig", {"client":SP.username}, function (data) { if( typeof data.api_key !== 'undefined' ){ // agents... SP.agentsRef = new Flybase( data.api_key, data.app_name, 'agents'); SP.agentsRef.isReady( function(){ SP.functions.startWebSocket(); }); // calls... SP.callsRef = new Flybase( data.api_key, data.app_name, 'calls'); }else{ console.log( "umm yeah, something's broken. Please fix it"); } }); $.get("/token", {"client":SP.username}, function (token) { Twilio.Device.setup(token, {debug: true}); }); $.get("/getcallerid", { "from":SP.username}, function(data) { $("#callerid-entry > input").val(data); }); } SP.functions.startWebSocket = function() { // ** Agent Presence Stuff ** // console.log(".startWebSocket..."); var d = new Date(); var date = d.toLocaleString(); // look up or add agent: SP.functions.update_agent(SP.username,{ status: 'LoggingIn', readytime: date }); SP.agentsRef.on('agents-ready', function (data) { $("#team-status .agents-num").text( data ); }); SP.agentsRef.on('in-queue', function (data) { $("#team-status .queues-num").text( data); }); SP.agentsRef.onDisconnect( function(){ // if agent gets disconnected for any reason, then we want to kick them offline... SP.agentsRef.trigger('agent-removed',{username: SP.username}); }); } // update or insert agent.. don't keep re-adding same agent.. SP.functions.update_agent = function(client, data){ var d = new Date(); var date = d.toLocaleString(); SP.agentsRef.where({"client": client}).once('value').then(function( rec ){ var agent = rec.first().value(); for( var i in data ){ agent[i] = data[i]; } SP.agent = agent; SP.agentsRef.push(agent, function(resp) { console.log( "agent updated" ); }); }, function(err){ data.client = client; SP.agent = data; SP.agentsRef.push(data, function(resp) { console.log( "agent inserted" ); }); }); } // ** UI Widgets ** // // Hook up numpad to input field $("div.number").bind('click',function(){ //$("#number-entry > input").val($("#number-entry > input").val()+$(this).attr('Value')); //pass key without conn to a function SP.functions.handleKeyEntry($(this).attr('Value')); }); SP.functions.handleKeyEntry = function (key) { if (SP.currentCall != null) { console.log("sending DTMF" + key); SP.currentCall.sendDigits(key); } else { $("#number-entry > input").val($("#number-entry > input").val()+key); } } //called when agent is not on a call SP.functions.setIdleState = function() { $("#action-buttons > .call").show(); $("#action-buttons > .answer").hide(); $("#action-buttons > .mute").hide(); $("#action-buttons > .hold").hide(); $("#action-buttons > .unhold").hide(); $("#action-buttons > .hangup").hide(); $('div.agent-status').hide(); $("#number-entry > input").val(""); } SP.functions.setRingState = function () { $("#action-buttons > .answer").show(); $("#action-buttons > .call").hide(); $("#action-buttons > .mute").hide(); $("#action-buttons > .hold").hide(); $("#action-buttons > .unhold").hide(); $("#action-buttons > .hangup").hide(); } SP.functions.setOnCallState = function() { $("#action-buttons > .answer").hide(); $("#action-buttons > .call").hide(); $("#action-buttons > .mute").show(); //can not hold outbound calls, so disable this if (SP.calltype == "Inbound") { $("#action-buttons > .hold").show(); } $("#action-buttons > .hangup").show(); $('div.agent-status').show(); } // Hide caller info SP.functions.hideCallData = function() { $("#call-data").hide(); } SP.functions.hideCallData(); SP.functions.setIdleState(); // Show caller info SP.functions.showCallData = function(callData) { $("#call-data > ul").hide(); $(".caller-name").text(callData.callerName); $(".caller-number").text(callData.callerNumber); $(".caller-queue").text(callData.callerQueue); $(".caller-message").text(callData.callerMessage); if (callData.callerName) { $("#call-data > ul.name").show(); } if (callData.callerNumber) { $("#call-data > ul.phone_number").show(); } if (callData.callerQueue) { $("#call-data > ul.queue").show(); } if (callData.callerMessage) { $("#call-data > ul.message").show(); } $("#call-data").slideDown(400); } // Attach answer button to an incoming connection object SP.functions.attachAnswerButton = function(conn) { $("#action-buttons > button.answer").click(function() { conn.accept(); }).removeClass('inactive').addClass("active"); } SP.functions.detachAnswerButton = function() { $("#action-buttons > button.answer").unbind().removeClass('active').addClass("inactive"); } SP.functions.attachMuteButton = function(conn) { $("#action-buttons > button.mute").click(function() { conn.mute(); SP.functions.attachUnMute(conn); }).removeClass('inactive').addClass("active").text("Mute"); } SP.functions.attachUnMute = function(conn) { $("#action-buttons > button.mute").click(function() { conn.unmute(); SP.functions.attachMuteButton(conn); }).removeClass('inactive').addClass("active").text("UnMute"); } SP.functions.detachMuteButton = function() { $("#action-buttons > button.mute").unbind().removeClass('active').addClass("inactive"); } SP.functions.attachHoldButton = function(conn) { $("#action-buttons > button.hold").click(function() { console.dir(conn); SP.requestedHold = true; //can't hold outbound calls from Twilio client $.post("/request_hold", { "from":SP.username, "callsid":conn.parameters.CallSid, "calltype":SP.calltype }, function(data) { //Todo: handle errors //Todo: change status in future SP.functions.attachUnHold(conn, data); }); }).removeClass('inactive').addClass("active").text("Hold"); } SP.functions.attachUnHold = function(conn, holdid) { $("#action-buttons > button.unhold").click(function() { //do ajax request to hold for the conn.id $.post("/request_unhold", { "from":SP.username, "callsid":holdid }, function(data) { //Todo: handle errors //Todo: change status in future //SP.functions.attachHoldButton(conn); }); }).removeClass('inactive').addClass("active").text("UnHold").show(); } SP.functions.detachHoldButtons = function() { $("#action-buttons > button.unhold").unbind().removeClass('active').addClass("inactive"); $("#action-buttons > button.hold").unbind().removeClass('active').addClass("inactive"); } SP.functions.updateAgentStatusText = function(statusCategory, statusText, inboundCall) { if (statusCategory == "ready") { $("#agent-status-controls > button.ready").prop("disabled",true); $("#agent-status-controls > button.not-ready").prop("disabled",false); $("#agent-status").removeClass(); $("#agent-status").addClass("ready"); $('#softphone').removeClass('incoming'); } if (statusCategory == "notReady") { $("#agent-status-controls > button.ready").prop("disabled",false); $("#agent-status-controls > button.not-ready").prop("disabled",true); $("#agent-status").removeClass(); $("#agent-status").addClass("not-ready"); $('#softphone').removeClass('incoming'); } if (statusCategory == "onCall") { $("#agent-status-controls > button.ready").prop("disabled",true); $("#agent-status-controls > button.not-ready").prop("disabled",true); $("#agent-status").removeClass(); $("#agent-status").addClass("on-call"); $('#softphone').removeClass('incoming'); } if (inboundCall == true) { //alert("call from " + statusText); $('#softphone').addClass('incoming'); $("#number-entry > input").val(statusText); } //$("#agent-status > p").text(statusText); } // Call button will make an outbound call (click to dial) to the number entered $("#action-buttons > button.call").click( function( ) { params = {"PhoneNumber": $("#number-entry > input").val(), "CallerId": $("#callerid-entry > input").val()}; Twilio.Device.connect(params); }); // Hang up button will hang up any active calls $("#action-buttons > button.hangup").click( function( ) { Twilio.Device.disconnectAll(); }); // Wire the ready / not ready buttons up to the server-side status change functions $("#agent-status-controls > button.ready").click( function( ) { $("#agent-status-controls > button.ready").prop("disabled",true); $("#agent-status-controls > button.not-ready").prop("disabled",false); SP.functions.ready(); }); $("#agent-status-controls > button.not-ready").click( function( ) { $("#agent-status-controls > button.ready").prop("disabled",false); $("#agent-status-controls > button.not-ready").prop("disabled",true); SP.functions.notReady(); }); $("#agent-status-controls > button.userinfo").click( function( ) { }); // ** Twilio Client Stuff ** // // first register outside of sfdc if ( window.self === window.top ) { console.log("Not in an iframe, assume we are using default client"); var defaultclient = {} defaultclient.result = SP.username; SP.functions.registerTwilioClient(defaultclient); } else{ console.log("In an iframe, assume it is Salesforce"); sforce.interaction.isInConsole(SP.functions.getTwilioClientName); } //this will only be called inside of salesforce Twilio.Device.ready(function (device) { sforce.interaction.cti.enableClickToDial(); sforce.interaction.cti.onClickToDial(startCall); var adNag = function() { SP.functions.ready(); }; setTimeout(adNag, 1500); }); Twilio.Device.offline(function (device) { //make a new status call.. something like.. disconnected instead of notReady ? sforce.interaction.cti.disableClickToDial(); SP.functions.notReady(); SP.functions.hideCallData(); }); /* Report any errors on the screen */ Twilio.Device.error(function (error) { SP.functions.updateAgentStatusText("ready", error.message); SP.functions.hideCallData(); }); /* Log a message when a call disconnects. */ Twilio.Device.disconnect(function (conn) { console.log("disconnectiong..."); SP.functions.updateAgentStatusText("ready", "Call ended"); SP.state.callNumber = null; // deactivate answer button SP.functions.detachAnswerButton(); SP.functions.detachMuteButton(); SP.functions.detachHoldButtons(); SP.functions.setIdleState(); SP.currentCall = null; // return to waiting state SP.functions.hideCallData(); SP.functions.ready(); //sforce.interaction.getPageInfo(saveLog); }); Twilio.Device.connect(function (conn) { console.dir(conn); var status = ""; var callNum = null; if (conn.parameters.From) { callNum = conn.parameters.From; status = "Call From: " + callNum; SP.calltype = "Inbound"; } else { status = "Outbound call"; SP.calltype = "Outbound"; } console.dir(conn); SP.functions.updateAgentStatusText("onCall", status); SP.functions.setOnCallState(); SP.functions.detachAnswerButton(); SP.currentCall = conn; SP.functions.attachMuteButton(conn); SP.functions.attachHoldButton(conn, SP.calltype); //send status info SP.functions.update_agent(SP.username,{ status: 'OnCall' }); }); /* Listen for incoming connections */ Twilio.Device.incoming(function (conn) { // Update agent status sforce.interaction.setVisible(true); //pop up CTI console SP.functions.updateAgentStatusText("ready", ( conn.parameters.From), true); // Enable answer button and attach to incoming call SP.functions.attachAnswerButton(conn); SP.functions.setRingState(); if (SP.requestedHold == true) { //auto answer SP.requestedHold = false; $("#action-buttons > button.answer").click(); } var inboundnum = cleanInboundTwilioNumber(conn.parameters.From); var sid = conn.parameters.CallSid var result = ""; //sfdc screenpop fields are specific to new contact screenpop sforce.interaction.searchAndScreenPop(inboundnum, 'con10=' + inboundnum + '&con12=' + inboundnum + '&name_firstcon2=' + name,'inbound'); }); Twilio.Device.cancel(function(conn) { console.log(conn.parameters.From); // who canceled the call SP.functions.detachAnswerButton(); SP.functions.detachHoldButtons(); SP.functions.hideCallData(); SP.functions.notReady(); SP.functions.setIdleState(); $(".number").unbind(); SP.currentCall = null; //SP.functions.updateStatus(); }); $("#callerid-entry > input").change( function() { $.post("/setcallerid", { "from":SP.username, "callerid": $("#callerid-entry > input").val() }); }); // Set server-side status to ready / not-ready SP.functions.notReady = function() { SP.functions.update_agent(SP.username,{ status: 'NotReady' }); SP.agentsRef.trigger('get-ready-agents',{username: SP.username}); SP.functions.updateStatus(); } SP.functions.ready = function() { SP.functions.update_agent(SP.username,{ status: 'Ready' }); SP.agentsRef.trigger('get-ready-agents',{username: SP.username}); SP.functions.updateStatus(); } // Check the status on the server and update the agent status dialog accordingly SP.functions.updateStatus = function() { var data = SP.agent.status; if (data == "NotReady" || data == "Missed") { SP.functions.updateAgentStatusText("notReady", "Not Ready") } if (data == "Ready") { SP.functions.updateAgentStatusText("ready", "Ready") } } /******** GENERAL FUNCTIONS for SFDC ***********************/ function cleanInboundTwilioNumber(number) { //twilio inabound calls are passed with +1 (number). SFDC only stores return number.replace('+1',''); } function cleanFormatting(number) { //changes a SFDC formatted US number, which would be 415-555-1212 return number.replace(' ','').replace('-','').replace('(','').replace(')','').replace('+',''); } function startCall(response) { //called onClick2dial sforce.interaction.setVisible(true); //pop up CTI console var result = JSON.parse(response.result); var cleanednumber = cleanFormatting(result.number); //alert("cleanednumber = " + cleanednumber); params = {"PhoneNumber": cleanednumber, "CallerId": $("#callerid-entry > input").val()}; Twilio.Device.connect(params); } var saveLogcallback = function (response) { if (response.result) { console.log("saveLog result =" + response.result); } else { console.log("saveLog error = " + response.error); } }; function saveLog(response) { console.log("saving log result, response:"); var result = JSON.parse(response.result); console.log(response.result); var timeStamp = new Date().toString(); timeStamp = timeStamp.substring(0, timeStamp.lastIndexOf(':') + 3); var currentDate = new Date(); var currentDay = currentDate.getDate(); var currentMonth = currentDate.getMonth()+1; var currentYear = currentDate.getFullYear(); var dueDate = currentYear + '-' + currentMonth + '-' + currentDay; var saveParams = 'Subject=' + SP.calltype +' Call on ' + timeStamp; saveParams += '&Status=completed'; saveParams += '&CallType=' + SP.calltype; //should change this to reflect actual inbound or outbound saveParams += '&Activitydate=' + dueDate; saveParams += '&Phone=' + SP.state.callNumber; //we need to get this from.. somewhere saveParams += '&Description=' + "test description"; console.log("About to parse result.."); var result = JSON.parse(response.result); var objectidsubstr = result.objectId.substr(0,3); // object id 00Q means a lead.. adding this to support logging on leads as well as contacts. if(objectidsubstr == '003' || objectidsubstr == '00Q') { saveParams += '&whoId=' + result.objectId; } else { saveParams += '&whatId=' + result.objectId; } console.log("save params = " + saveParams); sforce.interaction.saveLog('Task', saveParams, saveLogcallback); } });
Этот код основан на исходном коде softphone.js, который написал Чарльз, но я добавил запросы Flybase непосредственно во внешний интерфейс, а затем настроил прослушиватели событий.
После того, как мы настроили наш софтфон, мы делаем три ajax-вызова нашему бэкенду:
- /getconfig, чтобы вернуть нашу информацию о Flybase и включить наши переменные AgentRef и CallsRef. Как только agentRef возвращает isReady из Flybase, мы инициируем вызов нашей функции startWebSocket. isReady — это функция, которую мы можем использовать с клиентом Flybase, когда мы ждем, пока наше соединение не будет установлено, прежде чем выполнять другие действия.
- /token, которому мы передаем имя агента и возвращаем токен возможностей twilio, чтобы позволить агенту совершать и принимать вызовы.
- /getcallerid, чтобы вернуть исходящий номер телефона для вызова.
Мы используем функцию startWebSocket (которая была основана на оригинале), чтобы настроить три прослушивателя событий и обновить статус агента как LogginIn, а также время, когда он подключился к сети.
Позже в клиентском коде Twilio мы устанавливаем агенту состояние «Готово» после установки их клиентского соединения Twilio:
Twilio.Device.ready(function (device) { sforce.interaction.cti.enableClickToDial(); sforce.interaction.cti.onClickToDial(startCall); var adNag = function() { SP.functions.ready(); }; setTimeout(adNag, 1500); });
Мы собираемся прослушивать события «Готовность агентов» и «Очередь» от нашего бэкэнда, чтобы указать программному телефону обновить дисплей, чтобы показать количество агентов, которые находятся в состоянии «Готов» и ожидают вызова, а затем количество вызывающих абонентов, которые находятся в состоянии готовности. очередь в ожидании агента.
Наконец, мы собираемся использовать событие onDisconnect для срабатывания триггера удаления агента, когда агент отключается по какой-то причине, такой как закрытие браузера, выход из системы и т. д.
Вы также заметите в этом файле клон нашей функции update_agent. Одна из приятных особенностей использования Flybase заключается в том, что мы можем обрабатывать обновления нашей базы данных как из внешнего, так и из внутреннего интерфейса, что позволяет нам делать многое, что мы не могли раньше.
Остальная часть файла softphone.js на самом деле такая же, как и раньше, он взаимодействует с клиентом Twilio при входящих и исходящих вызовах и либо получает имя клиента из строки запроса ?client, либо получает его из Salesforce. если вы отображаете свой программный телефон в Salesforce.
Вы также можете заметить, что мы используем нашу новую функциональность Обещания:
SP.functions.update_agent = function(client, data){ var d = new Date(); var date = d.toLocaleString(); SP.agentsRef.where({"client": client}).once('value').then(function( rec ){ var agent = rec.first().value(); for( var i in data ){ agent[i] = data[i]; } SP.agent = agent; SP.agentsRef.push(agent, function(resp) { console.log( "agent updated" ); }); }, function(err){ data.client = client; SP.agent = data; SP.agentsRef.push(data, function(resp) { console.log( "agent inserted" ); }); }); }
В update_agent мы используем обещания, чтобы либо вернуть существующую запись агента, чтобы мы могли ее обновить, либо создать новую запись группы.
Развертывание в Heroku (необязательно)
Этот шаг необязателен, и вы можете развернуть его где угодно.
Вам понадобится учетная запись Heroku, а также установленный Пояс инструментов Heroku.
Создайте файл с именем Procfile и включите в него:
Теперь выполните следующее:
- git инициировать
- Войдите в Heroku, чтобы войти в Heroku.
- heroku create для создания приложения в Heroku.
- git добавить — все . чтобы добавить все ваши новые файлы в репозиторий.
- git commit -am ‘first commit’ для сохранения файлов внутри репо.
- git push heroku master, чтобы отправить ваш репозиторий git в Heroku.
- heroku open, чтобы открыть браузер по вашему новому пользовательскому URL-адресу.
Колл-центр теперь работает, вы можете добавить ?client=ANYNAMEYOUWANT в конец URL-адреса, и он настроит вас в качестве агента.
Настройка Salesforce (необязательно)
Этот шаг является необязательным, колл-центр работает без отдела продаж, и во второй части мы создадим базовую CRM, в которую вы также сможете ее интегрировать.
Эта часть на самом деле довольно проста. Сначала создайте файл с именем TwilioAdapter.xml:
<?xml version="1.0" encoding="UTF-8" ?> <callCenter> <section sortOrder="0" name="reqGeneralInfo" label="General Information"> <item sortOrder="0" name="reqInternalName" label="InternalName">DemoAdapter</item> <item sortOrder="1" name="reqDisplayName" label="Display Name">Demo Call Center Adapter</item> <item sortOrder="2" name="reqAdapterUrl" label="CTI Adapter URL">https://macbook.ngrok.com</item> <item sortOrder="3" name="reqUseApi" label="Use CTI API">true</item> <item sortOrder="4" name="reqSoftphoneHeight" label="Softphone Height">400</item> <item sortOrder="5" name="reqSoftphoneWidth" label="Softphone Width">300</item> </section> <section sortOrder="1" name="reqDialingOptions" label="Dialing Options"> <item sortOrder="0" name="reqOutsidePrefix" label="Outside Prefix">9</item> <item sortOrder="1" name="reqLongDistPrefix" label="Long Distance Prefix">1</item> <item sortOrder="2" name="reqInternationalPrefix" label="International Prefix">01</item> </section> </callCenter>
Измените соответствующую информацию, чтобы она указывала на ваш веб-сайт, а затем выполните следующие действия:
- Перейти в Колл-центры › Создать
- Импорт включенной конфигурации колл-центра, TwilioAdapter.xml — после импорта измените параметр URL-адрес CTI-адаптера на URL-адрес Heroku, созданный на первых шагах https:/‹insert yourherokuappurl
- добавить себя в колл-центр в разделе «Управление пользователями колл-центра» › Добавить других пользователей › (найти)
- Теперь вы должны увидеть адаптер CTI на вкладках «Контакты». Однако вы хотите использовать Service Cloud Console для всех вызовов cti (что предотвращает обновления браузера, которые могут привести к зависанию вызовов).
- Создание облачной консоли службы
- Настройка › Создать › Приложения › Создать
- Выберите «Консоль» для типа приложения
- дайте ему имя, например «Twilio ACD»
- Принять значение по умолчанию для логотипа
- Для вкладок добавьте несколько вкладок в консоль Service Cloud, например «Контакты», «Обращения».
- принять значение по умолчанию для шага 5 «выбрать способ отображения записей»
- Установить видимость для всех (для организаций разработчиков)
- Вы создали приложение! Вы увидите свою консоль в раскрывающемся списке приложений, например «Twilio ACD».
- Настройка всплывающих окон
- вы можете настроить ответ на всплывающее окно, например всплывающее окно поиска, в Настройка › Центры обработки вызовов › (ваш центр обработки вызовов) -> Раскладка программного телефона.
Эти шаги были заимствованы из оригинального поста Чарльза, поскольку они не изменились.
Заканчивать
Теперь у вас есть работающая система ACD колл-центра в режиме реального времени, которую можно использовать автономно (как одиночный программный телефон), в CRM, такой как Salesforce, или в CRM, полностью построенной вокруг нее, что мы сделаем в следующем посте. .
Если вы вообще знакомы с оригинальным client-acd, то мало что изменилось, кроме того, что он был переписан в узле и использовал Flybase в качестве бэкэнда/сигнальной системы, и это был план с этим постом, поскольку я хотел продемонстрировать, как Flybase может использоваться в колл-центре, и этот всегда использовался для различных проектов.
Напоминаем, что вы можете найти полный исходный код здесь.
Первоначально опубликовано на blog.flybase.io 17 февраля 2016 г.