首页 > 其他分享 >Twilio-和-Flybase-实时通信高级教程-全-

Twilio-和-Flybase-实时通信高级教程-全-

时间:2024-10-05 17:17:01浏览次数:1  
标签:function 教程 app SP Twilio var Flybase 我们

Twilio 和 Flybase 实时通信高级教程(全)

原文:Real-Time Twilio and Flybase

协议:CC BY-NC-SA 4.0

一、构建实时短信呼叫中心

我们将为我们的第一个项目建立一个方便的短信呼叫中心。

此呼叫中心将处理来自用户的传入 SMS 消息;它不会处理语音,只是短信。我们将在其他几章中讨论语音。

你想知道 Flybase 的一个美好之处吗?它很容易与其他服务集成。

在这一章中,我们将一起使用 Flybase 和 Twilio 来构建一个实时短信呼叫中心。

这可以用作客户帮助台,客户发送文本消息请求帮助,代理从他们的 web 浏览器发送回复。

实际的电话工作将由 Twilio 处理,Flybase 将存储数据并实时显示聊天内容。我们将使用 Node.js 来发送和接收文本消息,并使用 HTML 前端来处理实际的聊天。

设置

我们将使用一些工具来构建这个应用。在继续之前,您需要设置好这些:

如果你还没有,现在就注册( https://app.flybase.io/signup )一个免费的 Flybase 帐户,然后创建一个新的应用。您将在呼叫中心使用您的应用。

入门指南

我们首先需要设置我们的 Node.js 应用。

除了 Twilio 和 Flybase 模块,我们将使用 express 框架( http://expressjs.com/ )来设置我们的 Node web 服务器,以接收来自 Twilio 的 POST 请求,因此我们需要安装 Express 包。我们还将使用 body-parser 模块,所以我们也将安装它。

让我们创建我们的package.json文件:

javascript
{
      "name": "sms-contact-center",
      "version": "0.0.1",
      "description": "SMS Contact Center powered by Flybase, Twilio and Node.js",
      "main": "app.js",
      "repository": "https://github.com/flybase/sms-contact",
      "scripts": {
              "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [
            "twilio",
            "Flybase",
            "sms"
      ],
      "author": "Roger Stringer",
      "license": "MIT",
      "dependencies": {
            "twilio": "~1.6.0",
            "ejs": "~0.8.5",
            "express": "~3.4.8",
            "flybase": "~1.7.8",
            "node-buzz": "~1.1.0",
            "moment": "~2.5.1",
            "less-middleware": "~0.2.1-beta",
            "body-parser" : "~1.4.2",
            "method-override" : "~2.0.2"
      },
      "engines": {
            "node": "0.10.26"
      }
}

保存该文件,并从终端运行以下命令:

javascript
npm install

这将创建一个包含我们想要使用的所有模块的node_modules文件夹。

让我们设置我们的文件夹结构;创建一个名为views的文件夹。这是我们将保持前端的地方。

现在,创建一个名为“public”的文件夹这将托管我们的静态文件。在该文件夹中,创建一个css文件夹和一个js文件夹;我们稍后将回到这些。

在 app.js 文件的开头,我们需要 require express 并将其初始化为一个名为 app 的变量。

我们还将使用 bodyParser 中间件( https://github.com/expressjs/body-parser )来方便地使用我们将在 POST 请求中获得的数据。

创建一个名为app.js的新文件,并需要 twilio、express 和 flybase 包:

javascript
var express = require('express');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var twilio = require('twilio');
var  path = require('path');

var app = express();
app.set('views', path.join(process.cwd(), 'views'));
app.set('view engine', 'ejs');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(__dirname + '/public'));

var port = process.env.PORT || 8080; // set our port

var client = twilio('ACCOUNTSID', 'AUTHTOKEN');
var twilio_number = 'YOUR-NUMBER';

var api_key = "YOUR-API-KEY";
var appname = "YOUR-FLYBASE-APP";
var collection = "smscontact";
var messagesRef = require('flybase').init(appname, collection, api_key);

// backend routes =========================

ACCOUNTSIDAUTHTOKENYOUR-NUMBER替换为您将使用的 Twilio 凭证和 Twilio 帐户中的电话号码。

然后,用您的 Flybase API 键替换YOUR-API-KEYYOUR-flybase-APPsmscontact,并创建一个集合来使用。如果您还没有创建收藏,当您第一次保存数据时,系统会自动为您创建一个收藏,因此如果您愿意,您可以将收藏名称设置为smscontact

Flybase 使用集合来组织应用中的数据,因此一个应用可以有几个集合。如果您熟悉关系数据库,这相当于一个表。

这是我们应用的开始。接下来,我们将告诉它当新的短信进来和代理回复短信时该做什么。

发送和接收文本

Twilio 使用 webhooks ( https://en.wikipedia.org/wiki/Webhook )来让您的服务器知道何时有消息或电话进入我们的应用。我们需要设置一个端点,我们可以告诉 Twilio 将它用于消息传递 webhook。

我们将为/message添加一个用一些 TwiML (Twilio 标记语言, www.twilio.com/docs/api/twiml )响应的路由。TwiML 是一组基本指令,当你收到来电或短信时,你可以用它来告诉 Twilio 该做什么。我们的代码将如下所示:

javascript
app.post('/message', function (request, response) {
      var d = new Date();
      var date = d.toLocaleString();

      messagesRef.push({
            sid: request.param('MessageSid'),
            type:'text',
            direction: "inbound",
            tstamp: date,
            fromNumber:request.param('From'),
            textMessage:request.param('Body'),
            fromCity:request.param('FromCity'),
            fromState:request.param('FromState'),
            fromCountry:request.param('FromCountry')
      });

      var resp = new twilio.TwimlResponse();
      resp.message('Thanks for the message, an agent will get back to you shortly.');
      response.writeHead(200, {
            'Content-Type':'text/xml'
      });
      response.end(resp.toString());
});

这将监听任何传入的短信,并将它们存储在你的 Flybase 应用中。

一旦收到消息,我们使用 Twilio Node 库初始化一个新的TwimlResponse。然后我们使用消息关键字( www.twilio.com/docs/api/twiml/sms/message )来设置我们想要用什么来响应消息。在这种情况下,我们只需说“感谢您的消息,代理人会尽快回复您。”然后,我们将响应的内容类型设置为text/xml,并发送我们构建的 TwimlResponse 的字符串表示。

每当客户向我们设置的电话号码发送消息时,它都会向他们发送响应并将消息存储在 Flybase 中。如果代理正在监视客户端,那么他们会立即看到消息,并可以发送回复。

现在,让我们添加一条名为/reply的路线。当我们的代理想要回复一条消息时,我们将通过 AJAX 调用它:

javascript
app.post('/reply', function (request, response) {
      var d = new Date();
      var date = d.toLocaleString();

      messagesRef.push({
            type:'text',
            direction: "outbound",
            tstamp: date,
            fromNumber:request.param('From'),
            textMessage:request.param('Body'),
            fromCity:'',
            fromState:'',
            fromCountry:''
      });

      client.sendMessage( {
            to:request.param('To'),
            from:twilio_number,
            body:request.param('Body')
      }, function( err, data ) {
            console.log( data.body );
      });
});

这将在我们的 Flybase 应用中存储回复作为出站回复,并将消息发送给客户。

最后,让我们设置我们的服务器监听端口8080,并告诉它当我们从浏览器中查看它时该做什么:

javascript
// frontend routes =========================

// route to handle all angular requests
app.get('*', function(req, res) {
    res.render('home', {
        apikey:api_key,
        appname:appname,
        collection:collection
    });
});

var server = app.listen(port, function() {
      console.log('Listening on port %d', server.address().port);
});

现在我们已经构建了我们的服务器,我们需要告诉 Twilio 使用这个消息 URL 作为我们的消息请求 URL。

向您的 Twilio 号码发送短信,您应该会收到回复。如果你不知道,看看 Twilio 应用监视器( www.twilio.com/user/account/developer-tools/app-monitor )来帮助确定哪里出了问题。

这是我们呼叫中心的后端部分;它监听传入的短信,将它们存储在我们的 Flybase 应用中,然后在代理回复时发送回复。

现在,我们需要构建我们的代理系统,在这里代理可以观察收到的消息并回复它们。

我们现在就来建造它。

客户

我们让 Node.js 应用监听要发送和接收的消息。现在让我们设置我们的客户端,这是代理将从他们的 web 浏览器中看到的内容。

当有消息进来时,我们会显示一个显示消息的聊天框,然后发送回复。

首先,让我们创建视图。在/views文件夹中,创建名为home.ejs的文件:

  HTML
<!doctype html>
<html>
<head>
      <link href='http://fonts.googleapis.com/css?family=Lato:400,300italic,400italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
      <link rel="stylesheet" type="text/css" href="http://angular-ui.github.com/ng-grid/css/ng-grid.css" />
      <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
      <link rel="stylesheet" type="text/css" href="/css/style.css">

      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
      <script src="https://cdn.flybase.io/flybase.js?latest=1"></script>
      <script src="/js/chat.js"></script>

      <title>SMS Contact Center, powered by Flybase and Twilio</title>
</head>
<body>
      <div class='container'>
            <br />
            <div class="well">
                  <p class='homefont'>Welcome to your SMS Contact Center</p>

                  <p class='homefont'>This call center is the start of a modern day call center.</p>

                  <p class='homefont'>Take a look around and give us a try.</p>
            </div>
            <hr/>
            <h3>Incoming messages</h3>
            <div id="templateContainer"></div>
      </div>
      <script>
            $(function(){
// initializes our Flybase object
                   var flybaseRef = new flybase("<%= apikey %>", "<%= appname %>", "<%= collection %>");
// start our chatManager.
                   var myChatManager = new chatManager( flybaseRef );
                   myChatManager.updateChats();
            });
      </script>
</body>
</html>

这个文件将作为我们的 HTML 文件,我们使用 EJS,所以我们可以传递我们的 Flybase 设置,而不必在多个地方配置它们。EJS 可以方便地将模板功能添加到 Node.js 应用中。

现在,让我们创建我们的 CSS。在我们之前创建的/public/css文件夹中,创建一个名为 style.css 的新文件:

css
body{font-size:12pt;font-family:helvetica}
.chatWindow{float:left;margin:20px;border:1px solid #000;width:300px;background:#e5e5e5;border-radius:5px}
.chatName{margin-bottom:10px;background:#666;color:#fff;padding:4px}
.messages{padding:4px}
.message_outbound{color:blue;text-align:right}
.tstamp{font-size:9px;padding:2px;margin-bottom:10px;border-bottom:1px dotted #666;color:#666}
.error{color:red;text-align:center}
.messageForm textarea{float:left;width:220px;margin:5px}

最后,我们希望设置我们的应用的大脑。我们把最大的文件留到了最后。

public/js 文件夹中,新建一个名为 chat.js 的文件:

chat.js
var chatManager = function(flybaseRef) {
      this.flybaseRef = flybaseRef;
};

chatManager.prototype = {
      chats: [],
      getChat: function(fromNumber) {
            var foundChat = null;
            for (c = 0; c < this.chats.length; c++) {
                  if (this.chats[c].from == fromNumber) {
                        foundChat = this.chats[c];
                  }
            }

            if (foundChat == null) {
                  foundChat = new chat( this.flybaseRef );
                  foundChat.init(fromNumber);
                  foundChat.displayTemplate();
                  this.chats.push(foundChat);
            }
            return foundChat;
      },
      updateChats: function() {
            var _this = this;
            this.flybaseRef.once('value', function (data) {
                  data.forEach( function(message){
                        var row = message.value();
                        _this.getChat( row.fromNumber ).addMessage(
                              row.textMessage,
                              row.tstamp,
                              row.direction
                        );
                  });
            });
            this.flybaseRef.on('added', function (data) {
                  var row = data.value();
                  _this.getChat( row.fromNumber ).addMessage(
                        row.textMessage,
                        row.tstamp,
                        row.direction
                  );
            });
      }
};

var chat = function(flybaseRef) {
      this.flybaseRef = flybaseRef;
};
chat.prototype = {
      init: function(name) {
            this.from = name;
            this.chatName = 'chat-' + this.from;
            this.buttonName = 'submit-' + this.from;
            this.textName = 'reply-' + this.from;
      },
      replyMessage: function(message) {
            var _this = this;
            $.ajax({
                  type: "POST",
                  url: "/reply",
                  data: {
                        'To': this.from,
                        'Body': message,
                        'From': this.from
                  },
                  dataType: "json",
                  success: function(data) {
                        // your message was sent
                  }
            });
      },
      displayTemplate: function() {
            var content = '<div class="chatName">Chat with ' + this.from + '</div> \
            <div class="messages" id="' + this.chatName + '"></div> \
            <div class="messageForm"><textarea id="' + this.textName + '"></textarea><button id="' + this.buttonName + '">Reply</button></div> \
        </div>';

            content = '<div class="chatWindow" id="' + this.tmplName + '">' + content + '</div>';

            $('#templateContainer').append(content);
            var _this = this;

            $('#' + this.buttonName).click(function() {
                  _this.replyMessage($('#' + _this.textName).val());
                  $('#' + _this.textName).val('');
            });
      },
      addMessage: function(message, tstamp, direction) {
            $('#' + this.chatName).append("<div class='message_" + direction + "'>" + message + "<div class='tstamp'>" + tstamp + "</div></div>");
      }
};

我们的 chatManage 类是这样设置的,当它加载时,它首先使用value事件触发器获取一个保存的文本消息列表,并按照发送它们的电话号码显示它们。

我们将所有往来于同一号码的消息视为一个会话,因此对于每个聊天会话,我们会看到一个显示代理和客户之间消息的框,以及一个用于发送新消息的文本框。

然后,我们使用added事件触发器监听任何新消息,然后将它们显示在适当的聊天框中。

chat类告诉我们的应用如何显示聊天框,以及如何处理发送新的回复。

在这种情况下,当代理发送消息时,我们将它发布到我们的后端/reply路由,在那里它被保存到我们的 Flybase 应用,然后作为文本消息发送给客户。

我们还存储消息来自的方向,或者是inbound或者是outbound。这样,我们可以设计每条消息的样式,使其看起来与您在手机上查看聊天记录时相似。客户的文本将出现在左侧,代理的回复将出现在右侧。

现在让我们启动我们的应用:

javascript
node app.js

我们已经告诉我们的应用在端口 8080 上运行,所以如果你去你的网络浏览器并输入http://localhost:8080/,你应该看到你的呼叫中心。

顺便说一句,如果你在本地运行这个,你需要确保在进入下一步之前已经运行了 ngrok。如果你以前没有用过 ngrok ( https://ngrok.com/ ),Twilio 的凯文·维尼里已经整理了一个很棒的教程( www.twilio.com/blog/2013/10/test-your-webhooks-locally-with-ngrok.html )来帮助你入门。

摘要

我们做到了!既然你已经构建了一个简单的短信呼叫中心应用( https://github.com/flybaseio/sms-contact ),那么你就有机会用它来创造一些东西了。

接下这个项目,继续做下去。一些想法是,你实际上可以完全删除/reply AJAX 调用,而是创建一个outbound文本队列来存储消息,然后向出站集合添加一个added侦听器,该侦听器既可以向客户发送回复,又可以将其添加到message集合中,这样它就会出现在聊天窗口中。

这将消除对 AJAX 调用的需要,并且在一次向几个客户发送多个回复的情况下增加一些队列支持。

二、构建一个实时博客工具

参加活动时,博客直播非常方便;能够在博客上实时报道最新发布的新产品,这并没有什么坏处。

我们将使用 Node.js、Flybase 和 Twilio 构建一个简单的博客应用。

这个特殊的实时博客解决方案是为一个事件设置的。这意味着我们指定的电话号码上的所有帖子都会显示出来。

您可以在此基础上允许多个事件,但这比我们今天要深入探讨的要多一点。

设置

我们将使用一些工具来构建这个应用。在继续之前,您需要设置 Twilio、Flybase 和 Node.js。如果你还没有,现在就注册( https://app.flybase.io/signup )一个免费的 Flybase 帐户,然后创建一个新的应用。你将把你的应用用于你的实时博客应用。

入门指南

我们首先需要设置 Node.js 应用。除了 Twilio 和 Flybase 模块,我们将使用 express 框架( http://expressjs.com/ )来设置我们的 Node web 服务器,以接收来自 Twilio 的 POST 请求,因此我们需要安装 Express 包。我们还将使用 body-parser 模块,所以我们也将安装它。

让我们创建我们的package.json文件:

json
{
      "name": "live-blog",
      "version": "0.0.1",
      "description": "Live Blog App powered by Flybase, Twilio and Node.js",
      "main": "app.js",
      "repository": "https://github.com/flybase/live-blog",
      "scripts": {
              "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [
            "Twilio",
            "Flybase",
            "sms"
      ],
      "author": "Roger Stringer",
      "license": "MIT",
      "dependencies": {
            "Twilio": "~1.6.0",
            "ejs": "~0.8.5",
            "express": "~3.4.8",
            "flybase": "~1.7.8",
            "node-buzz": "~1.1.0",
            "moment": "~2.5.1",
            "less-middleware": "~0.2.1-beta",
            "body-parser" : "~1.4.2",
            "method-override" : "~2.0.2"
      },
      "engines": {
            "node": "0.10.26"
      }
}

保存该文件,并从终端运行以下命令:

npm install

这将创建一个包含我们想要使用的所有模块的node_modules文件夹。

我们要创建的第一个文件是config.js;这将保存我们的配置信息:

javascript
module.exports = {
    // Twilio API keys
    Twilio: {   // Where you will put your Twilio Credentials
        sid: "ACCOUNTSID",
        token: "AUTHTOKEN",
        from_number: "YOUR-NUMBER"
    },
    flybase: {  // Where you will put your flybase credentials
            api_key: "YOUR-API-KEY",
            app_name: "YOUR-flybase-APP"
    },
    un: 'admin',
    pw: 'password'
};

该文件用于我们的配置。我们可以在任何时候通过引用文件和调用键来访问这里的任何东西。例如,为了获得我们的 Flybase API 密钥,我们将调用

javascript
var config = require('./config');
console.log( config.flybase.api_key );

ACCOUNTSIDAUTHTOKENYOUR-NUMBER替换为您将使用的 Twilio 凭证和 Twilio 帐户中的电话号码。

然后,用您要使用的 Flybase API 密钥替换YOUR-API-KEYYOUR-flybase-APP

在我们的 app.js 文件的开始,我们需要 require express 并将其初始化为一个名为 app 的变量。我们还将使用 bodyParser 中间件( https://github.com/expressjs/body-parser )来方便地使用我们将在 POST 请求中获得的数据。

创建一个名为app.js的新文件,并需要 twilio、express 和 flybase 包:

javascript

var express = require('express');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var path = require('path');
var config = require('./config');

var app = express();
app.set('views', path.join(process.cwd(), 'views'));
app.set('view engine', 'ejs');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(__dirname + '/public')); // set the static files location /public/img will be /img for users

var port = process.env.PORT || 8080; // set our port

var Twilio = require('Twilio');
var client = Twilio(config.Twilio.sid, config.Twilio.token );

var flybase = require('flybase');
var postsRef = flybase.init(config.flybase.app_name, "posts", config.flybase.api_key);

Flybase 使用集合来组织应用中的数据,因此一个应用可以有几个集合。如果您熟悉关系数据库,这相当于一个表。

我们将为我们的项目使用一个集合,我们称之为posts

让我们建立我们的文件夹结构。创建一个名为views的文件夹;这是我们将保持前端的地方。

views文件夹中创建一个名为index.ejs的文件:

HTML

<!doctype html>
<html>
<head>
      <link href='http://fonts.googleapis.com/css?family=Lato:400,300italic,400italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
      <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
      <script src="https://cdn.flybase.io/flybase.js?latest=1"></script>
      <title>Live Blog, powered by Flybase and Twilio</title>
</head>
<body>
      <div class='container'>
            <div class="row">
                  <div class="col-md-4">
                        <div id="postsDiv"></div>
                  </div>
      </div>
      <script>
            $(function(){
                  var postsRef = new flybase("<%= apikey %>", "<%= appname %>", "posts");
                  postsRef.once('value', function (data) {
                        console.log( "we found " + data.count() + " posts");
                        data.forEach( function(post){
                              displayPost(post.value() );
                        });
                  });

                  postsRef.on('added', function (data) {
                        var post = data.value();
                        displayPost(post);
                  });

                  function displayPost(post) {
                        $('<div/>')
                              .attr("id",post._id)
                              .text(post.textMessage)
                              .appendTo( $('#postsDiv') );
                        $('#postsDiv')[0].scrollTop = $('#postsDiv')[0].scrollHeight;
                  }
            });
      </script>
</body>
</html>

这将在我们的应用中监听我们的posts收藏中的任何新帖子,然后在发生时在屏幕上输出。现在,让我们设置我们的 Twilio 监听器。

设置我们的 Twilio 监听器

Twilio 使用 webhooks ( https://en.wikipedia.org/wiki/Webhook )来让您的服务器知道何时有消息或电话进入我们的应用。我们需要设置一个端点,我们可以告诉 Twilio 将它用于消息传递 webhook。

我们将为用一些 TwiML ( www.twilio.com/docs/api/twiml )响应的/消息添加一条路由。TwiML 是一组基本指令,当你收到来电或短信时,你可以用它来告诉 Twilio 该做什么。我们的代码将如下所示:

javascript
// backend routes

// listen for incoming sms messages
app.post('/message', function (request, response) {
      var d = new Date();
      var date = d.toLocaleString();

      var postBody = request.param('Body');

      var numMedia = parseInt( request.param('NumMedia') );
      var i;
      if (numMedia > 0) {
            for (i = 0; i < numMedia; i++) {
                  var mediaUrl = request.param('MediaUrl' + i);
                  postBody += '<br /><img src="' + mediaUrl + '" />';
            }
      }

      postsRef.push({
            sid: request.param('MessageSid'),
            type:'text',
            tstamp: date,
            fromNumber:request.param('From'),
            textMessage:postBody,
            fromCity:request.param('FromCity'),
            fromState:request.param('FromState'),
            fromCountry:request.param('FromCountry')
      });

      var resp = new Twilio.TwimlResponse();
      resp.message('Post received');
      response.writeHead(200, {  // 200 is the limit for the title of the blog
            'Content-Type':'text/xml'
      });
      response.end(resp.toString());
});

这将把我们的消息存储在我们的posts收藏中。它将检查是否有任何图像附加到消息中,如果有,它将把它们附加到消息正文中。

最后,让我们设置我们的服务器监听端口8080,并告诉它当我们从浏览器中查看它时该做什么:

javascript
// frontend routes

app.get('*', function(req, res) {
      res.render('index', {
            apikey:config.flybase.api_key,
            appname:config.flybase.app_name,
      });
});

var server = app.listen(port, function() {
      console.log('Listening on port %d', server.address().port);
});

然后,您可以从您的终端调用node app.js,它将加载应用。

向您的 Twilio 号码发送短信,您应该会收到回复。如果你不知道,看看 Twilio 应用监视器( www.twilio.com/user/account/developer-tools/app-monitor )来帮助确定哪里出了问题。

摘要

我们现在已经建立了一个基本的实时博客工具。没有什么太花哨的,但你可以从这里开始,并在此基础上做一些更花哨的东西。直播博客对于参加活动来说很方便。您可以设置它,让与会者在参加会议时发布实时帖子,或者在新闻发布会上发布最新消息。我们稍后将在此基础上添加一个“Ionic”应用来处理帖子。

三、构建实时群聊应用

去年 11 月,我和几个同事参加了一个会议,我们希望让每个人都了解最新情况,并有组织地跟踪我们的计划。

我们建立了一个群聊系统,让其中一个成员发送短信,其他人都能收到,如果有人回复,我们都能看到回复。

这很方便,今天,我将向您展示如何构建一个类似的 web 应用。该应用将由一个简单的控制面板和一个后端组成,在控制面板上,你可以管理谁是一个组的一部分,后端将处理传入和传出的文本消息,并将它们路由到适当的组成员。

你还可以从网站上的一个页面实时发送和接收消息,当你可能没有带手机,但想给小组发消息时,反之亦然。

佐料

在继续之前,您需要设置好这些。

我们将使用 Flybase ( http://flybase.io/ )来处理 app 的数据存储和实时方面,Twilio ( www.twilio.com/ )来处理实际的短信工作,Node.js 用于系统本身。

我们将为一个单独的组构建这个特定的应用,但是将它扩展到多个组并不困难。

最后,我们将把这个应用作为一个免费的应用托管在 Heroku 上( https://heroku.com/ ,一个方便的托管平台,可以让你的项目快速启动和运行,尤其是在与 Flybase 和 Twilio 结合使用时)。

Node.js 将是我们应用的后端部分;这是我们为 Twilio 构建监听器的地方,无论我们何时发送或接收文本消息,我们都可以与之对话。

Flybase 是一个实时应用平台,将成为我们应用的首选数据存储。它将用于管理谁是一个组的成员,并存储传入和传出的消息以及它们来自谁。如果你还没有,现在就注册( https://app.flybase.io/signup )一个免费的 Flybase 帐户,然后在你的仪表盘内创建一个新的应用。你将在你的群聊系统中使用此应用。

Twilio 是我们一直以来都很方便的电话 API,它让我们可以构建像群聊应用甚至呼叫中心这样的服务。还没有 Twilio 帐户吗?免费报名( www.twilio.com/try-twilio )。

入门指南

我们首先需要设置 Node.js 应用。

除了 Twilio 和 Flybase 模块,我们将使用 express 框架( http://expressjs.com/ )来设置我们的 Node web 服务器,以接收来自 Twilio 的 POST 请求,因此我们需要安装 Express 包。我们还将使用 body-parser 模块,所以我们也将安装它。

让我们创建我们的package.json文件:

javascript
{
  "name": "group-chat",
  "version": "0.0.1",
  "description": "SMS Group Chat powered by Flybase, Twilio and Node.js",
  "main": "app.js",
  "repository": "https://github.com/flybaseio/group-chat",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "twilio",
    "flybase",
    "sms"
  ],
  "author": "Roger Stringer",
  "license": "MIT",
  "dependencies": {
    "body-parser": "~1.16.0",
    "ejs": "~2.5.5",
    "express": "~4.14.0",
    "flybase": "¹.8.2",
    "less-middleware": "~2.2.0",
    "method-override": "~2.3.7",
    "moment": "~2.17.1",
    "node-buzz": "~1.1.0",
    "twilio": "~2.11.1"
  }
}

保存该文件,并从终端运行以下命令:

javascript
npm install

这将创建一个包含我们想要使用的所有模块的node_modules文件夹。

让我们设置我们的文件夹结构,并创建一个名为views的文件夹。这是我们将保持前端的地方。

现在,创建一个名为public的文件夹。这将托管我们的静态文件。在这个文件夹中,创建一个css文件夹和一个js文件夹。我们稍后将回到这些。

我们要创建的第一个文件是config.js;这将保存我们的配置信息:

javascript
module.exports = {
    // Twilio API keys
    twilio: {
        sid: "ACCOUNTSID",
        token: "AUTHTOKEN",
        from_number: "YOUR-NUMBER"
    },
    flybase: {
            api_key: "YOUR-API-KEY",
            app_name: "YOUR-FLYBASE-APP"
    },
    un: 'admin',
    pw: 'password'
};

该文件用于我们的配置。我们可以在任何时候通过引用文件和调用键来访问这里的任何东西。例如,为了获得我们的 Flybase API 密钥,我们将调用

javascript
var config = require('./config');
console.log( config.flybase.api_key );

ACCOUNTSIDAUTHTOKENYOUR-NUMBER替换为您将使用的 Twilio 凭证和 Twilio 帐户中的电话号码。

然后,用您的 Flybase API 密钥替换YOUR-API-KEYYOUR-FLYBASE-APP

在我们的app.js文件的开头,我们需要 require express 并将其初始化为一个名为 app 的变量。我们还将使用 bodyParser 中间件( https://github.com/expressjs/body-parser )来方便地使用我们将在 POST 请求中获得的数据。

创建一个名为app.js的新文件,并需要 twilio、express 和 flybase 包:

javascript
var express = require('express');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var path = require('path');
var config = require('./config');

var app = express();
app.set('views', path.join(process.cwd(), 'views'));
app.set('view engine', 'ejs');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded( extended: true }));
app.use(express.static(__dirname + '/public')); // set the static files location /public/img will be /img for users

var port = process.env.PORT || 8080; // set our port

var twilio = require('twilio');
var client = twilio(config.twilio.sid, config.twilio.token );

var flybase = require('flybase');
var messagesRef = flybase.init(config.flybase.app_name, "messages", config.flybase.api_key);
var groupRef = flybase.init(config.flybase.app_name, "groups", config.flybase.api_key);

Flybase 使用集合来组织应用中的数据,因此一个应用可以有几个集合。如果您熟悉关系数据库,这相当于一个表。

我们将为我们的项目使用两个集合:一个包含messages,另一个包含groups。考虑到这一点,我们为我们的 Flybase 应用创建了两个不同的引用,一个用于消息,一个用于我们的组。这是我们应用的开始。接下来,我们将构建 web 界面来管理组成员,并允许发送和接收消息。

之后,我们将构建我们的 Twilio 界面,您将拥有一个有趣的应用来玩。

发送和接收文本

我们需要添加一些东西来发送和接收文本。我们的第一步是为 Twilio 添加一个监听器。

Twilio 使用 webhooks ( https://en.wikipedia.org/wiki/Webhook )来让您的服务器知道何时有消息或电话进入我们的应用。我们需要设置一个端点,我们可以告诉 Twilio 将它用于消息传递 webhook。

我们将为/message添加一条路由,它用一些 TwiML ( www.twilio.com/docs/api/twiml )进行响应。TwiML 是一组基本指令,当你收到来电或短信时,你可以用它来告诉 Twilio 该做什么。我们的代码将如下所示:

javascript
// listen for incoming sms messages
app.post('/message', function (request, response) {

      groupRef.where( {"memberNumber":request.param('From')} ).limit(1).on( "value", function ( data ){
            if( data.count() ){
                  data.forEach( function( snapshot ){
                        var member = snapshot.value();
                        messagesRef.push({
                              sid: request.param('MessageSid'),
                              type:'text',
                              tstamp: new Date().toLocaleString(),
                              fromName:member.memberName,
                              fromNumber:request.param('From'),
                              message:request.param('Body'),
                              media:"",
                              fromCity:request.param('FromCity'),
                              fromState:request.param('FromState'),
                              fromCountry:request.param('FromCountry'),
                              groupNumber:request.param('To')
                        });
                  });
            }
      });

      var numMedia = parseInt( request.param('NumMedia') );
      if (numMedia > 0) {
            for (i = 0; i < numMedia; i++) {
                  var mediaUrl = request.param('MediaUrl' + i);
                  groupRef.where( {"memberNumber":request.param('From')} ).limit(1).on( "value", function ( data ){
                        if( data.count() ){
                              data.forEach( function( snapshot ){
                                    var member = snapshot.value();
                                    messagesRef.push({
                                          sid: request.param('MessageSid'),
                                          type:'text',
                                          tstamp: new Date().toLocaleString(),
                                          fromName:member.memberName,
                                          fromNumber:request.param('From'),
                                          message:"",
                                          media:mediaUrl,
                                          fromCity:request.param('FromCity'),
                                          fromState:request.param('FromState'),
                                          fromCountry:request.param('FromCountry'),
                                          groupNumber:request.param('To')
                                    });
                              });
                        }
                  });
            }
      }
      var resp = new twilio.TwimlResponse();
      resp.message('Message received.');
      response.writeHead(200, {
            'Content-Type':'text/xml'
      });
      response.end(resp.toString());
});

这将监听任何传入的短信,并将它们存储在你的 Flybase 应用中,特别是在messages集合中。

作为存储消息的一部分,我们执行一个查找来找到与发送消息的电话号码相同的groups成员。然后,我们使用这个查找来验证该成员是否是组的一部分,并获取该成员的名称。

如果找不到成员,则不会发送任何消息。

一旦收到消息,我们使用 Twilio Node 库初始化一个新的 TwimlResponse 。然后我们使用消息关键字( www.twilio.com/docs/api/twiml/sms/message )来设置我们想要用什么来响应消息。在这种情况下,我们只说“消息已收到。”

然后,我们将响应的内容类型设置为text/xml,并发送我们构建的 TwimlResponse 的字符串表示。

倾听变化

作为我们的app.js代码的一部分,我们还想添加一些异步监听器来监听我们的 Flybase 应用的变化:

javascript
// when a new message is added to the Flybase app, send it via Twilio...
messagesRef.on("added", function (data ){
      var snapshot = data.value();
      sendMessage(
            snapshot.groupNumber,
            snapshot.fromName,
            snapshot.fromNumber,
            snapshot.message,
            snapshot.media || ""
      );
});

groupRef.on("added", function ( data ){
      var snapshot = data.value();
      var msg = snapshot.memberName + ' has joined the group';
      messagesRef.push({
            sid: "",
            type:'',
            tstamp: new Date().toLocaleString(),
            fromName:"Admin",
            fromNumber:"",
            message:msg,
            media:"",
            fromCity:"",
            fromState:"",
            fromCountry:"",
            groupNumber:snapshot.groupNumber
      });
});

groupRef.on("removed", function ( data ){
      var snapshot = data.value();
      var msg = snapshot.memberName + ' has left the group';
      //      send broadcast that a group member has been removed
      messagesRef.push({
            sid: "",
            type:'',
            tstamp: new Date().toLocaleString(),
            fromName:"Admin",
            fromNumber:"",
            message:msg,
            media:"",
            fromCity:"",
            fromState:"",
            fromCountry:"",
            groupNumber:snapshot.groupNumber
      });

});

//      broadcast a message to the group
function sendMessage( group_number, from_name, from_number, message, media ){
      var msg = from_name + ": " + message;
      groupRef.where( {"memberNumber":{"$not":from_number}} ).on( "value", function ( data ){
            if( data.count() ){
                  data.forEach( function( snapshot ){
                        var member = snapshot.value();
                        var msgObj = {
                              to:member.memberNumber,
                              from:group_number,
                              body:msg
                        };
                        if( media !== "" ){
                              msgObj.mediaUrl = media;
                        }
                        client.sendMessage( msgObj, function( err, data ) {});
                  });
            }
      });
}

我们设置了三个异步侦听器,一个用于 messages 集合,它侦听任何被“添加”到其中的消息,当它收到新消息的通知时,调用我们的 sendMessage 函数将消息发送给组中的其他成员。

另外两个异步监听器用于我们的集合:第一个监听器监听任何加入到组中的新成员,然后发送成员已经加入组的通知。

最后一个监听器将监听从组中“移除”的任何成员,并发送成员已经离开组的通知。

最后,我们的 sendMessage 函数用于向其他组成员发送消息;它将执行查询以返回该组的所有成员,不包括发送消息的人,并将消息发送给每个成员。

消息将以成员姓名后跟消息的格式显示:

John: How about pizza after work?

最后,让我们设置我们的服务器监听端口8080,并告诉它当我们从浏览器中查看它时该做什么:

javascript
// frontend routes =========================

// Create basic  middleware used to authenticate all admin requests
var auth = express.basicAuth(config.un, config.pw);

// route to handle all frontend requests with a password to protect unauthorized access....
app.get('*', auth, function(req, res) {
      res.render('index', {
            api_key:config.flybase.api_key,
            app_name:config.flybase.app_name,
            group_number:config.twilio.from_number
      });
});

var server = app.listen(port, function() {
      console.log('Listening on port %d', server.address().port);
});

这是我们群聊应用的后端部分。它会监听收到的短信,将其存储在我们的 Flybase 应用中,然后发送给小组的其他成员。

现在,我们需要建立我们的控制面板,管理员可以管理组成员,也可以发送和接收消息。

管理您的群组

我们将构建一个简单的 web 界面来管理我们的组成员。

我们为小组成员存储的数据将由以下三部分数据组成:

  • -群组电话号码(我们存储在"Getting Started"部分的 twilio_number 变量中的 Twilio 号码)

  • -成员姓名

  • -会员电话号码

我们还将显示一个基本的聊天框,让我们的管理员发送消息,并查看正在发送的消息。

首先,让我们创建视图。在/views文件夹中,创建名为index.ejs的文件:

`

HTML
<!doctype html>
<html>
<head>
      <link href='//fonts.googleapis.com/css?family=Lato:400,300italic,400italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
      <link rel="stylesheet" type="text/css" href="//angular-ui.github.com/ng-grid/css/ng-grid.css" />
      <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
      <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet">
      <link rel="stylesheet" type="text/css" href="/css/style.css">

      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
      <script src="https://cdn.flybase.io/flybase.js?20150217"></script>
      <script src="https://cdn.flybase.io/libs/phone.js"></script>
      <script src="/js/group.js"></script>

      <title>Group Chat, powered by Flybase and Twilio</title>
</head>
<body>
      <div class='container'>
            <div class="row">
                  <div class="col-md-6">
                        <h3>Group Members</h3>
                        <div id="group_wrapper"></div>
                        <hr />
                        <h2>Add new member</h2>
                        <div class="well">
                              <form id="group_form" method="post" accept-charset="utf-8" class="form-inline">
                                    <div class="form-group">
                                          <div class="input-group">
                                                <div class="input-group-addon"><i class="fa fa-pencil"></i></div>
                                                <input type="text" class="form-control" id="name" name="name" placeholder="name">
                                          </div>
                                    </div>
                                    <div class="form-group">
                                          <div class="input-group">
                                                <div class="input-group-addon"><i class="fa fa-mobile"></i></div>
                                                <input type="tel" class="form-control" id="phone" name="phone" placeholder="+11112223333"/>
                                          </div>
                                    </div>
                                    <button type="submit" class="btn btn-primary">Save</button>
                              </form>
                        </div>
                  </div>
                  <div class="col-md-4 col-md-offset-1">
                        <div id="chatBox" class='chat'>
                              <header>Chat Log</header>
                              <ul id='messagesDiv' class='chat-messages'></ul>
                              <footer>
                                    <form id="msg_form" method="post" accept-charset="utf-8" class="form-inline">
                                          <input type="text" id="messageInput" placeholder="Type a message..." />
                                    </form>
                              </footer>
                        </div>
                  </div>
      </div>
      <script>
            $(function(){
//                  initialize our Flybase object
                  var myGroupManager = new groupManager( "<%= api_key %>", "<%= app_name %>", "<?%= group_number %>");
                  myGroupManager.start();
            });
      </script>
</body>
</html>

这将显示我们的控制面板,它将分为两个窗格,左侧用于查看群组成员,右侧用于查看聊天日志。

在页面底部,我们正在初始化我们的groupManager类。我们将很快创建该文件。

接下来,让我们创建我们的样式表。在public/css文件夹中,创建名为style.css的文件:

css
body{font-size:12pt;font-family:helvetica}
.chatWindow{float:left;margin:20px;border:1px solid #000;width:300px;background:#e5e5e5;border-radius:5px}
.chatName{margin-bottom:10px;background:#666;color:#fff;padding:4px}
.messages{padding:4px}
.message_outbound{color:blue;text-align:right}
.tstamp{font-size:9px;padding:2px;margin-bottom:10px;border-bottom:1px dotted #666;color:#666}
.error{color:red;text-align:center}
.messageForm textarea{float:left;width:220px;margin:5px}
#phone{width:140px;}
#chatBox{background-color: #f8f8f8;background: rgb(229, 228, 228);margin:10px;}
.hide {display: none; }
.chat {font-family: "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;border-radius: 3px;-webkit-box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.2);box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.2);background-color: #dfe3ea;border: 1px solid #CCC;overflow: auto;padding: 0px;font-size: 18px;line-height: 22px;color: #666; }
.chat header {background-color: #EEE;background: -webkit-gradient(linear, left top, left bottom, from(#EEEEEE), to(#DDDDDD));background: -webkit-linear-gradient(top, #EEEEEE, #DDDDDD);background: linear-gradient(top, #EEEEEE, #DDDDDD);-webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.9), 0px 1px 2px rgba(0, 0, 0, 0.1);box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.9), 0px 1px 2px rgba(0, 0, 0, 0.1);border-radius: 3px 3px 0px 0px;border-bottom: 1px solid #CCC;line-height: 24px;font-size: 12px;text-align: center;color: #999; }
.chat input {-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;-webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.2);box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.2);border-radius: 3px;padding: 0px 10px;height: 30px;font-size: 18px;width: 100%;font-weight: normal;outline: none; }
.chat .chat-toolbar {background-color: #FFF;padding: 10px;position: relative;border-bottom: 1px solid #CCC; }
.chat .chat-toolbar label {text-transform: uppercase;line-height: 32px;font-size: 14px;color: #999;position: absolute;top: 10px;left: 20px;z-index: 1; }
.chat .chat-toolbar input {-webkit-box-shadow: none;box-shadow: none;border: 1px solid #FFF;padding-left: 100px;color: #999; }
.chat .chat-toolbar input:active, .chat .chat-toolbar input:focus {color: #1d9dff;border: 1px solid #FFF; }
.chat ul {list-style: none;margin: 0px;padding: 20px;height: 200px;overflow: auto; }
.chat ul li {margin-bottom: 10px;line-height: 24px; }
.chat ul li:last-child {margin: 0px; }
.chat ul .chat-username {margin-right: 10px; }
.chat footer {display: block;padding: 10px; }
.chat footer input {border: 1px solid #ced3db;height: 40px; width:75%;}

现在,让我们来看看我们系统的大脑。在public/js文件夹中,我们将创建一个名为group.js的文件:

javascript
var groupManager = function(api_key, app_name, group_number) {
//      store the group number
        this.group_number = group_number;
//      reference to our messages collection...
        this.messagesRef = new Flybase(api_key, app_name, "messages");

//      reference to our group collection...
        this.groupRef = new Flybase(api_key, app_name, "groups");

        this.group_members = [];
};

这是我们 groupManager 课程的第一部分。到目前为止,我们已经告诉它启动两个 Flybase 引用,一个名为messagesRef,一个名为groupRef。我们还将我们的组号存储为一个名为group_number的变量。

现在,让我们开始行动:

javascript
groupManager.prototype.start = function(){
      var _this = this;

// list group members if any
      this.groupRef.on("value", function( data ){
            if( data.count() ){
                  data.forEach( function( snapshot ){
                        var member = snapshot.value();
                        _this.group_members[member._id] = member;
                  });
            }
            _this.displayGroup();
      });

// listen for new members being added
      this.groupRef.on("added", function( snapshot ){
            var member = snapshot.value();
            _this.group_members[member._id] = member;
            _this.displayGroup();
      });

// save new group member to our app
      $("#group_form").submit( function(e){
            e.preventDefault();
            var member = {
                  'groupNumber': _this.group_number,
                  'memberName': $("#name").val(),
                  'memberNumber': clean_phone( $("#phone").val() )
            };
            _this.groupRef.push( member );
            $("#name").val('');
            $("#phone").val('');
            return false;
      });

// listen for members being removed
      $('div').on('click','a.delete', function(e){
            var _id = e.target.id;
            _this.groupRef.remove(_id);
            return false;
      });

      this.groupRef.on("removed", function( snapshot ){
            var member = snapshot.value();
            _this.group_members[member._id] = undefined;
            _this.displayGroup();
      });

// list any existing chat message
      this.messagesRef.on('value', function (data) {
            if( data.count() ){
                  data.forEach( function(message){
                        _this.displayChatMessage(message.value() );
                  });
            }
      });
// listen for incoming chat messages
      this.messagesRef.on('added', function (data) {
            var message = data.value();
            _this.displayChatMessage( message );
      });

// listen for outgoing chat messages
      $('#msg_form').submit( function(e){
            e.preventDefault();
            var message = {
                        "tstamp": new Date().toLocaleString(),
                        "fromName": "Admin",
                        "fromNumber": "",
                        "message": $('#messageInput').val(),
                        "fromCity": "",
                        "fromState": "",
                        "fromCountry": "",
                        "groupNumber": _this.group_number
            }
            _this.messagesRef.push( message );
            $('#messageInput').val('');
            return false;
      });
};

我们的函数设置了异步监听器,以及通过按下delete按钮删除表单提交和成员的监听器。

如果添加了一个组成员,那么该成员将被添加到集合中,并且会向该组的其他成员发送一个通知。群组成员列表也将显示新成员。

如果一个人被删除,他们的名字将从列表中消失,一条消息将被发送给其余的组成员。

我们的 groupManager 类的另一面是我们程序的实际聊天面。当管理员键入一条消息时,它将被发送给其他组成员。同时,当另一个群组成员发送消息时,管理员会在聊天框中看到该消息。

我们还剩下两个功能:一个显示一个组的所有成员,另一个显示聊天消息。

对于我们的组,我们将信息存储在名为 group_members 的类范围变量中。这使我们能够在收到相关通知时快速添加、更新或删除成员:

javascript
// Display group members
groupManager.prototype.displayGroup = function(){
      $('#group_wrapper').html('');
      for (var i in this.group_members ) {
            var member = this.group_members[i];
            if( member !== undefined ){
                  var html = '';
                  html = '<span>'+member.memberName+' ( ' + member.memberNumber + ' )</span> <a href="#delete" class="delete" id="' + member._id+'">[remove]</a>';
                  $('<div/>').prepend( html ).appendTo($('#group_wrapper'));
            }
      }
};

我们的最后一个功能显示收到的每条聊天消息:

javascript
// Display chat messages
groupManager.prototype.displayChatMessage = function( message ){
      var _this = this;
      var msg = message.message;
      if( message.media !== "" ){
            msg += '<br /><img src="' + message.media + '" />';
      }
      $('<li/>')
            .attr("id",message._id)
            .html(msg)
            .prepend(
                  $("<strong class='example-chat-username' />").text(message.fromName+': ')
                  ).appendTo( $('#messagesDiv') );
      $('#messagesDiv')[0].scrollTop = $('#messagesDiv')[0].scrollHeight;
};

最后一件事是启动我们的应用:

javascript
node app.js

我们已经告诉我们的应用在端口 8080 上运行,所以如果你在网络浏览器上键入http://localhost:8080/,你应该会看到你的群聊。

在 Heroku 上举办

Heroku 非常适合让服务器配置变得简单而轻松。我们可以更快地构建,并担心对我们重要的事情,而不是试图配置我们自己的服务器。这与我们在 Flybase 的理念完美契合,让我们可以快速地构建东西。让我们看看如何在几秒钟内将我们的群聊应用部署到 Heroku。

请前往 http://heroku.com 创建您的免费账户。仪表板非常简单和用户友好。

接下来,您需要安装 Heroku Toolbelt。Heroku Toolbelt 将让我们访问 Heroku 命令行实用程序。Heroku Toolbelt 程序有不同的操作系统,您可以从以下链接下载:

安装工具带后,我们将可以使用 heroku 命令。

现在,您需要执行以下操作:

  1. 在您创建群组聊天应用的文件夹中,创建一个新的 git 存储库

  2. 登录 Heroku

  3. 在 Heroku 中创建应用

  4. 将您的群组聊天储存库推送到 Heroku

  5. 告诉 Heroku 创建一个 dyno(一个 worker,用来响应 web 请求)

  6. heroku open在新的自定义网址打开网页浏览器

就这样。您的应用现在正在 Heroku 上运行。

将您的群聊指定给 Twilio 中的一个电话号码

现在,我们想回到 Twilio 帐户,打开我们用来发送消息的电话号码。

当你在 Heroku 上创建你的应用时,你可以给它一个唯一的 URL。例如,让我们说

https://my-group-chat.herokuapp.com/

我们通过短信接收信息的网址现在将变为 https://my-group-chat.herokuapp.com/message

现在向您的 Twilio 号码发送短信,您应该会收到回复。如果你不知道,看看 Twilio 应用监视器( www.twilio.com/user/account/developer-tools/app-monitor )来帮助确定哪里出了问题。

摘要

我们用 Flybase ( http://flybase.io )和 Twilio ( http://twilio.com )搭建了一个实时群聊 app。

这个群聊应用甚至可以处理传入的媒体(图片、Word 文档、视频等。)并将其重新发送给组中的其他人。

你可以在 GitHub ( https://github.com/flybaseio/group-chat )找到我们的群聊应用。

这个应用可以供一群人进行对话。这在参加活动时会很方便。

你可以用它来通知与会者即将到来的会谈。例如,一个会议可以将他们的与会者添加到一个组中,然后在谈话开始、午餐时间或紧急情况时发送广播。

四、创建“点击呼叫”呼叫中心

本章涵盖了 Flybase 和 Twilio 的一些有趣的领域。我们将建立一个“点击呼叫”呼叫中心,访问者可以点击页面上的产品,开始与另一个网页上的代理进行 Twilio 客户呼叫。为此,我们将使用 Flybase 的自定义事件。

本教程基于去年 Twilio 发表的一篇关于使用 Pusher、Twilio 和 Python 构建一个类似系统的文章( www.twilio.com/blog/2014/07/creating-a-click-to-call-service-with-twilio-client-pusher-and-python.html ),但我们的会简单一点,因为涉及的系统更少。

Flybase 的自定义事件

Flybase 为开发人员提供了许多好用的工具。在本教程中,我们将使用 Flybase 的自定义事件来构建一个自定义的“点击呼叫”呼叫中心。

什么是自定义事件?你知道预约事件( http://flybase.io/docs/web/guide/reading-data.html ),如valueaddedchangedonlineremoved,但我们也有定制事件。自定义事件可以方便地在设备、用户、不同集合甚至同一应用的不同部分之间传递消息或数据。

下面是自定义事件侦听器的一个基本示例:

javascript
flybase.on("custom_event", function(message) {
      console.log( message );
});

flybase.trigger("custom_event", "Hi")

当您想要在设备之间传递数据,但不一定需要将数据保存在任何地方时,应该使用自定义事件。在这种情况下,它更像一个信号服务器,让设备知道正在发生的事情。

什么是点击呼叫系统?

“点击呼叫”允许客户点击一个链接,开始与人进行浏览器内语音通话。在建立语音通话的同时,有关客户的上下文信息(例如他们正在查看的商品或他们的姓名/兴趣/脸书喜欢的东西)被传递给处理通话的人,然后该人可以提供高度个性化的体验。客户不需要告诉他们自己的名字或他们感兴趣的产品/服务:点击呼叫为你做了这一切。它摆脱了呼叫中心令人讨厌的部分,让你继续做对你来说重要的事情。

必要的工具

在本章中,我们将使用以下工具:

  • Twilio 让我们可以构建像短信应用甚至呼叫中心这样的服务。

  • Flybase 将用于管理谁是一个组的成员,并存储传入和传出的消息以及它们来自谁。

  • Node.js 将是我们应用的后端部分;这是我们为 Twilio 构建监听器的地方,无论我们何时发送或接收文本消息,我们都可以与之对话。

设置 Twilio 客户端

让我们从创建一个 TwiML 应用开始,这个应用可以重复用于 TwiML 配置,可以应用于 Twilio 电话号码或 TwiML 应用。每个 TwiML 应用都有一个唯一的 SID,我们用它来为 Twilio 客户端生成安全令牌。

转到你的账户的应用页面( www.twilio.com/user/account/apps ),点击“创建 TwiML 应用”创建一个新的应用

我们称我们的 TwiML 应用为“点击呼叫演示”您需要将语音请求 URL 链接到您网站上的 URL。我们将单击 Save,这将为 TwiML 应用生成一个 SID,我们稍后将使用它,所以请将它放在手边。

入门指南

我们首先需要设置我们的 Node.js app。

除了 Twilio 和 Flybase 模块,我们将使用 express 框架( http://expressjs.com/ )来设置我们的 Node web 服务器,以接收来自 Twilio 的 POST 请求,因此我们需要安装 Express 包。我们还将使用主体解析器模块,所以我们也将安装它。

让我们创建我们的 package.json 文件:

```javascript
{
      "name": "call-ads",
      "version": "0.0.1",
      "description": "Click-to-call call Center powered by Flybase, Twilio and Node.js",
      "main": "app.js",
      "repository": "https://github.com/flybaseio/call-ads",
      "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [
            "twilio",
            "data mcfly",
            "flybase",
            "twilio",
            "sms"
      ],
      "author": "Roger Stringer",
      "license": "MIT",
      "dependencies": {
            "twilio": "~1.6.0",
          "ejs": "~0.8.5",
            "express": "~3.4.8",
            "flybase": "~1.5.2",
            "node-buzz": "~1.1.0",
            "moment": "~2.5.1",
            "less-middleware": "~0.2.1-beta",
            "body-parser" : "~1.4.2",
            "method-override" : "~2.0.2"
      },
      "engines": {
            "node": "0.12"
      }
}
```js

保存该文件,并从终端运行以下命令:

npm install

这将创建一个“node_modules”文件夹,其中包含我们想要使用的所有模块。

让我们设置我们的文件夹结构,并创建一个名为“views”的文件夹这是我们将保持前端的地方。现在,创建一个名为“public”的文件夹这将托管我们的静态文件。在该文件夹中,创建一个“图像”文件夹;我们将在本章的后面回到这个问题上来。我们要创建的第一个文件是 config.js 。这将保存我们的配置信息:

javascript
module.exports = {
      // Twilio API keys
      twilio: {
            sid: "ACCOUNT-SID",
            token: "AUTH-TOKEN",
            appid: 'YOUR-TWILIO-APP-ID'
      },
      //      Flybase settings
      flybase: {
            api_key: "YOUR-API-KEY",
            app_name: "YOUR-FLYBASE-APP"
      },
      //      Username and password for admin section.
      un: 'admin',
      pw: 'password'
};

该文件用于我们的配置。我们可以在任何时候通过引用文件和调用键来访问这里的任何东西。例如,为了获得我们的 Flybase API 密钥,我们将调用

var config = require('./config');
console.log( config.flybase.api_key );

ACCOUNTSIDAUTHTOKENYOUR-Twilio-APP-IDYOUR-NUMBER 替换为您将使用的 TWILIO 帐户中的 TWILIO 凭证和电话号码。

显示了 YOUR-TWILIO-APP-ID 的占位符的 appid 变量是存储您在上一步中创建的 SID 的地方。接下来,将 YOUR-API-KEYYOUR-FLYBASE-APP 替换为要使用的 Flybase API Key。

最后, unpw 变量是您存储用户名和密码的地方,当您通过“/cc”路径访问您的控制面板时会用到它们。

在我们的 app.js 文件的开头,我们需要 require express 并将其初始化为一个名为 app 的变量。我们还将使用 bodyParser 中间件( https://github.com/expressjs/body-parser )来方便地使用我们将在 POST 请求中获得的数据。

创建一个名为 app.js 的新文件,并需要 twilio、express 和 flybase 包:

var express = require('express');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var path = require('path');
var config = require('./config');

var app = express();
app.set('views', path.join(process.cwd(), 'views'));
app.set('view engine', 'ejs');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({      extended: true      }));
app.use(express.static(__dirname + '/public')); // set the static files location /public/img will be /img for users

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 leadsRef = flybase.init(config.flybase.app_name, "leads", config.flybase.api_key);

Flybase 使用集合来组织应用中的数据,因此一个应用可以有几个集合。如果您熟悉关系数据库,这相当于一个表。我们将为我们的项目使用一个名为 leads 的集合。

自定义事件同样链接到我们所连接的集合,所以如果我们建立了到leads的 Flybase 连接,那么我们将监听 leads 集合中的所有事件,无论是否保留。

这是我们应用的开始。接下来,我们将构建 web 界面来管理组成员,并允许发送和接收消息。之后,我们将构建我们的 Twilio 界面,您将有一个有趣的应用可以玩:

javascript

//      listen for incoming sms messages
app.post('/voice', function (req, res) {
      leadsRef.trigger("new-caller", {
            item: req.param('item'),
            name:req.param('name')
      });

      res.writeHead(200, {
            'Content-Type':'text/xml'
      });

      var resp = new twilio.TwimlResponse();
      resp.dial(function() {
            this.client('Admin');
      });

      res.type('text/xml');
      res.end( resp.toString() );
});

当我们收到对/voice路线的新POST请求时,我们将其存储在我们的 Flybase 应用中的 new-caller 事件中,然后将该调用连接到我们的 admin 用户,在本例中我们称之为“Admin”。

最后,我们设置我们的前端路由,/cc/,然后告诉我们的服务器监听端口5000,并告诉它当我们从浏览器查看它时该做什么:

javascript
var auth = express.basicAuth(config.un, config.pw);

// route to handle all frontend requests, with a password to protect unauthorized access....
app.get('/cc', auth, function(req, res) {
      var capability = new twilio.Capability( config.twilio.sid, config.twilio.token );
      capability.allowClientIncoming( 'Admin' );
      capability.allowClientOutgoing( config.twilio.appid );
    var token = capability.generate();

      res.render('cc', {
            token:token,
            api_key:config.flybase.api_key,
            app_name:config.flybase.app_name
      });
});

app.get('/', 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.render('index', {
            call_token: token,
            client_name: client_name
      });
});

var server = app.listen(port, function() {
      console.log('Listening on port %d', server.address().port);
});

/cc/路由都调用 Twilio 为 Twilio 客户端创建功能令牌。这些让网页发出和接收呼叫。

我们做了一件事,在主页上显示跟踪。如果您访问附加了一个?client=myname变量的页面,那么客户端的名称就会改变。这是为了演示传递上下文信息。

设置模板

我们现在需要构建我们的模板文件。会有两个: index.ejscc.ejs 。我们会将它们存储在views文件夹中。

首先,我们来设置一下 index.ejs :

HTML
<!DOCTYPE html>
<html>
<head>
      <title>Fly Shop</title>
      <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css">
      <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet">

      <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
      <script type="text/javascript" src="//static.twilio.com/libs/twiliojs/1.2/twilio.min.js"></script>
      <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.0/js/bootstrap.min.js"></script>
</head>
<body>
      <div class="container">
            <div class="row">
                  <div class="col-md-6">
                        <h1>Fly Shop</h1>
                  </div>
            </div>
            <div class="well">
                  <h4 class="text-center">Click an ad to purchase now!</h4>
            </div>
            <div class="row">
                  <div class="col-md-4">
                        <a onclick="call('Apple LCD TV');">
                        <div class="panel panel-default">
                              <div class="panel-heading"><h4>55" Apple LCD TV</h4></div>
                              <div class="panel-body text-center">
                                    <img srcimg/apple1.png">
                              </div>
                        </div></a>
                  </div>
                  <div class="col-md-4">
                        <a onclick="call('Apple iPad');">
                        <div class="panel panel-default">
                              <div class="panel-heading"><h4>Apple iPad</h4></div>
                              <div class="panel-body text-center">
                                    <img srcimg/apple2.png">
                              </div>
                        </div></a>
                  </div>
                  <div class="col-md-4">
                        <a onclick="call('MacBook Pro');">
                        <div class="panel panel-default">
                              <div class="panel-heading"><h4>MacBook Pro</h4></div>
                              <div class="panel-body text-center">
                                    <img srcimg/apple6.png">
                              </div>
                        </div></a>
                  </div>
            </div>
            <div class="well" style="display:none;" id="hangupbox">
                  <a onClick="hangup();" class="btn btn-primary" id="hangup">Hang up</a>
            </div>
      </div>
      <script type="text/javascript">
            var myname = '';
            Twilio.Device.setup("<%=call_token%>");
            function call(item_of_choice) {
                  params = {"item": item_of_choice, "name": "<%= client_name %>"};
                  Twilio.Device.connect(params);
                  $("#hangupbox").show();
            }
            function hangup() {
                  Twilio.Device.disconnectAll();
                  $("#hangupbox").hide();
            }
      </script>
</body>
</html>

这将显示样品产品,让游客点击一个。当他们这样做时,它将开始呼叫代理。

这些图片在“公共/图片”文件夹中,只是一些随机的产品图片。你可以把它们换成任何你想要的真实图像。这只是让你知道它是如何工作的。

这个页面的实际工作在 JavaScript 中,它接受模块 call_tokenclient_name 以及用户感兴趣谈论的选定项目,并开始浏览器电话呼叫。

现在,让我们设置 cc.ejs ,这是代理控制面板:

<!DOCTYPE html>
<html>
<head>
      <title>Control Center</title>
      <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css">
      <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet">

      <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
      <script type="text/javascript" src="//static.twilio.com/libs/twiliojs/1.2/twilio.min.js"></script>
</head>
<body>
      <div class="container">
            <div class="well">
                  <h1>Incoming calls</h1>
            </div>
            <br />
            <div class="well">
                  <div class="list-group">
                        <div class="list-group-item">
                              <h4 class="list-group-item-heading warning"></h4>
                        </div>
                  </div>
            </div>
      </div>
      <script src="https://cdn.flybase.io/flybase.js?20150817"></script>
      <script>
            $(function(){
                  var leadsRef = new Flybase( "<%= api_key %>", "<%= app_name %>", "leads");
                  leadsRef.on("new-caller", function( call ) {
                        $('.warning').val( call.name + ' wants a ' + call.item );
                  });

                  Twilio.Device.setup("<%= token %>");
                  Twilio.Device.incoming(function (conn) {
                        // accept the incoming connection and start two-way audio
                        conn.accept();
                  });

                  function hangup() {
                        Twilio.Device.disconnectAll();
                  }
            });
      </script>
</body>
</html>

这看起来有点像“索引”文件。不同之处在于,当访问者点击开始呼叫时,代理可以查看。屏幕上会出现一个提示,电话会被接听。

在我们的 app.js 文件中,我们还设置了一个基本密码,这样只有代理可以访问它。

最后一件事,让我们启动我们的应用:

node app.js

我们已经告诉我们的应用在端口5000上运行,所以如果你去你的网络浏览器并输入http://localhost:5000/,你应该看到你的呼叫中心广告页面,如果你去http://localhost:5000/cc,你应该看到你实际的呼叫中心,等待网站访问者的呼叫。点击主页上的广告将触发呼叫中心的呼叫。

如果你在本地运行这个,你会希望在进入下一步之前确保你已经运行了 ngrok 。如果你以前没有使用过 ngrok ( https://ngrok.com/ ),Twilio 的凯文·维尼里整理了一个很棒的教程( www.twilio.com/blog/2013/10/test-your-webhooks-locally-with-ngrok.html )来帮助你入门。

摘要

我们已经使用 Flybase ( http://flybase.io )和 Twilio ( http://twilio.com )构建了一个实时点击呼叫呼叫中心应用。这是一个非常基本的实现,旨在向您展示使用 Twilio 客户端和 Flybase 的实时“点击呼叫”服务的可能性。您可以使用这个项目和开放源代码( https://github.com/flybaseio/call-ads )来扩展教程并开始构建您自己的应用。

这里有一些想法,让你的大脑思考如何通过“点击呼叫”来使用这样的实时信息:

  • -拨打电话时调出来电者信息,以加快通话速度。

  • -从呼叫者处收集位置数据,自动确定他们的地理位置。

  • -允许多个代理。甚至可以考虑使用 Flybase 来存储来电队列,并将客户连接到第一个可用的代理。

五、构建 Salesforce 支持的呼叫中心

在电话领域,自动呼叫分配(ACD)系统是一种根据客户的选择、客户的电话号码、所选的系统呼入线路或一天中处理呼叫的时间将呼入分配给特定代理组的系统。我们也称之为呼叫中心。

几年前,Twilio 的 Charles Oppenheimer ( https://github.com/choppen5 )使用 Twilio 客户端和 Ruby 构建了一个 Salesforce 嵌入式 ACD ( https://github.com/choppen5/client-acd )的演示。在这方面,查尔斯功不可没。

我们只是将 Charles 的演示转换成 Node.js,并使用 Flybase 驱动的后端来处理调用的分发,而不是原来的 Ruby/Mongo 系统。结果是一个更干净的呼叫中心,易于修改和集成到其他 CRM 中。

必要的工具

  • Flybase.io ( https://flybase.io/ )作为我们的后端,处理存储数据、传递事件和我们的调用队列。

  • Twilio 客户端( www.twilio.com/webrtc ),一个给 Twilio 的 WebRTC 接口。在我们的演示中,我们使用 JavaScript 库,该库为我们提供了一个 API 并连接到 Twilio,以便在我们的 Salesforce 浏览器中通过 WebRTC 接收呼叫。Twilio Client 还让我们能够通过软电话控制通话。

  • Heroku 将作为我们的网络主机,但你可以在任何你喜欢的地方托管你的呼叫中心。

  • Salesforce Open CTI ( https://developer.salesforce.com/page/Open_CTI )是一个开放的 API,允许第三方 CTI 供应商将电话渠道连接到 Salesforce CRM 界面。在我们的演示中,我们使用开放式 CTI 来容纳我们的软电话并驱动点击拨号/文本功能。由于开放式 CTI 的设计,该演示不需要插件或安装软件。

实际的 Salesforce 集成是可选的,您可以轻松地将您的软电话插入到另一个 CRM 中。本教程的第二部分实际上将使用 Flybase 构建一个简单的 CRM,并将 softphone 作为一个小部件包含在内。

入门指南

你可以在这里找到完整源代码: https://github.com/flybaseio/callcenter

首先,让我们设置我们的 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",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "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 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 = '';

     //     searches for an agent who has been set to Ready for the longest time and connects 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 a 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": "DeQueuing"}).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 the 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: "DeQueuing" }, 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 参考:

    • callsRef连接到我们的 calls 表,并处理来电信息的存储和检索。
    • agentsRef连接到我们的agents表,并为代理处理存储和检索信息。

我们处理的第一个后端任务是检查我们的 Twilio 队列,以检索 queueId 或我们的调用队列,否则如果它不存在,就创建它。如果我们的呼叫中心没有可用的代理,我们使用这个队列来存储来电,这些来电将留在队列中,直到有代理可用。

然后,我们为两个事件设置事件侦听器:

    • agent-removed:当代理注销时,我们更新他们的用户记录,将他们设置为not ready
    • get-ready-agents:只返回当前设置为Ready的代理数量。

然后我们有了实际的 URI 端点:

    • /dial是一个由 Twilio 处理的POST请求,用于在代理的 web 浏览器和电话号码之间发出呼叫。
    • /voice是一个POST请求,处理来自电话号码的来电。其工作原理是找到其状态设置为Ready时间最长的代理,并将其分配给呼叫。如果代理不是Ready,那么我们将调用者放在一个队列中,稍后检查它。
    • /handDialCallStatus 是在呼叫结束时调用的POST请求。它检查呼叫是否被应答,并根据从 Twilio 返回的DialCallStatus,要么将呼叫者放回队列并使代理脱离Ready状态,要么挂断呼叫,因为它认为呼叫已经完成。
    • /token是通过前端的 AJAX 调用调用的GET请求,用于在代理登录时向代理分配 Twilio 客户端功能令牌。
    • /getconfig是一个GET请求,也是通过 AJAX 调用从客户端调用的,它返回呼叫中心的 Flybase 设置,供软电话在前端使用。
    • /是一个GET请求,它显示软电话并根据?client查询字符串为客户机分配一个名称。

呼叫中心使用三种主干功能来处理各种目的:

    • getLongestIdle 是一个函数,用于检查状态设置为Ready或“ 出列 ”的代理,并返回该代理的客户端名称。在没有找到代理的情况下,我们返回 false,并将调用者放入队列中。“出列”是一种特殊的状态,当代理可用时,我们将在代码末尾设置这种状态。
    • update_agent 将获取代理的 ID,并用新信息更新他们在 Flybase 数据库中的帐户,例如通话时的状态更新、离线等。
    • update_call 的使用方式与update_agent相同,但用于追踪通话。

最后,我们有一个名为 checkQueue 的队列处理函数,它在应用加载 1.5 秒后被调用,然后每 3 秒执行一个简单的任务:

  1. 它进入一个循环,返回呼叫队列中的所有呼叫者。

  2. 如果有呼叫者等待连接到代理,那么它将通过按readyTime字段排序来寻找其状态设置为Ready的代理以及谁已经Ready最长。

  3. 如果代理是Ready,那么我们将该代理的状态设置为出列,并通过调用我们的dqueueurl将队列中的Front处的调用者连接到该代理。

  4. 如果没有代理Ready或者队列中没有调用者,那么我们设置一个超时,在 3 秒钟内再次调用该函数,并返回到“checkQueue”循环的步骤 1。

我们接下来要创建一个名为“app”的文件夹,然后在该文件夹中创建一个名为config.js的文件:

module.exports = {
     // Twilio API keys
     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 settings
     flybase: {
          api_key: "YOUR-API-KEY",
          app_name: "YOUR-FLYBASE-APP"
     }
};

更新此文件以包含您的 Twilio 信息和您的 Flybase 信息。

对于 Twilio 信息,您需要在您的 Twilio 帐户中创建一个 TwiML 应用。创建应用,并在/dial将其POST发布到您的呼叫中心网站。

此外,在 Twilio 中创建一个新的电话号码,并将该电话号码POST发送到您的呼叫中心网站/voice

有一个名为queueName的变量,它是您希望呼叫中心使用的队列的名称,还有一个名为dqueueurl的变量,它是您的网站的 URL,后面附有/voice。因为 Twilio 需要一个绝对 URL,所以您将需要它来执行出队任务。

软电话

app文件夹中,创建两个文件夹:

  1. views

  2. public

在“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="*">&lowast;</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-container -->

          <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 Caller ID "></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><!-- /softphone -->
</body>
</html>

这是我们的索引文件,它处理我们的软电话的输出,供座席用来接听和拨打电话。

public文件夹中,创建一个名为“css”的文件夹,并包含以下两个文件:

“dialer.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;
}

最后,我们要设置我们的软电话前端代码。

软电话前端代码

public中创建一个名为js的文件夹,并添加softphone.js

这段代码基于 Charles 编写的原始softphone.js代码,但是我已经直接在前端添加了 Flybase 查询,然后设置了事件监听器:

```javascript
$(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 the 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 the 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("disconnecting...");
          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 inbound 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);
          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);
     }
});
```js

设置好软电话后,我们向后端发出三个 AJAX 调用:

  1. /getconfig返回我们的飞行基地信息,并启用我们的agentsRefcallsRef变量。一旦agentsRef从 Flybase 返回isReady,我们就触发对startWebSocket函数的调用。isReady是一个函数,当我们在执行其他操作之前等待直到我们的连接已经建立时,我们可以使用这个函数。

  2. 我们将代理的名字传递给它,它返回一个 Twilio 能力令牌,让代理发出和接收呼叫。

  3. /getCallerId返回呼出电话号码供通话使用。

我们使用startWebSocket函数(基于原始函数)来设置三个事件监听器,并将代理的状态更新为LogginIn以及它们上线的时间。

在稍后的 Twilio 客户端代码中,一旦 Twilio 客户端连接建立,我们就将代理设置为Ready:

Twilio.Device.ready(function (device) {
     sforce.interaction.cti.enableClickToDial();
     sforce.interaction.cti.onClickToDial(startCall);
     var adNag = function() {
          SP.functions.ready();
     };
     setTimeout(adNag, 1500);
});

我们将从后端监听agents-readyin-queue事件,以告知软电话更新显示,显示设置为Ready并等待呼叫的代理人数,以及排队等待代理的呼叫者人数。

最后,我们将使用onDisconnect事件在代理由于某种原因离线时触发agent-removed触发器,比如关闭浏览器、注销等。

您还会注意到我们的 update_agent 函数在这个文件中的克隆。使用 Flybase 的一个好处是,我们可以从前端或后端处理数据库更新,这样我们就可以做很多以前做不到的事情。

softphone.js文件的其余部分实际上和以前一样。它与 Twilio 客户端就传入和传出呼叫进行对话,如果您在 Salesforce 中显示您的软电话,它或者从?client查询字符串中获取客户端名称,或者从 Salesforce 中获取。

您可能还注意到我们使用了新的承诺( http://blog.flybase.io/2016/02/02/promises-lookups/ )功能:

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" );
               });
          });
     }
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 Toolbelt ( https://toolbelt.heroku.com/ )。

创建一个名为“Profile”的文件,包括

web: node app.js

现在,运行以下命令:

  1. git init

  2. 登录 Heroku

  3. 在 Heroku 中创建应用

  4. git add --all .将您的所有新文件添加到存储库中

  5. git commit -am 'first commit'将文件存储在回购内

  6. 将您的 git 库推送到 Heroku

  7. heroku open在新的自定义网址打开浏览器

呼叫中心现在正在工作。您可以在 URL 的末尾添加?client=ANYNAMEYOUWANT,它会将您设置为代理。

配置 Salesforce(可选)

这一步是可选的。呼叫中心没有 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">http://YOURWEBSITE.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>

更改适当的信息以指向您的网站,然后按照以下步骤操作:

  1. 转到➤呼叫中心创建:

    -导入呼叫中心,包括配置,TwilioAdapter.xml。导入后,将参数 CTI Adapter URL 更改为在第一步中创建的 Heroku URL:https:/<insert yourherokuappurl.

    -将您自己添加到“管理呼叫中心用户”下的呼叫中心➤添加更多用户➤查找。

  2. 现在,您应该在 Contact 选项卡下看到一个 CTI 适配器。但是,您希望对所有 CTI 呼叫使用 Service Cloud 控制台(这可以防止可能挂断呼叫的浏览器刷新)。

  3. 创建服务云控制台

    -转到设置➤创建➤应用➤新。

    -为应用类型选择“控制台”。

    -为其命名,如“Twilio ACD”

    -接受默认徽标。

    -对于选项卡,向您的服务云控制台添加一些选项卡,如联系人、个案等。

    -接受步骤 5“选择记录显示方式”的默认值

    -将可见性设置为全部(对于开发组织)。

    -您现在已经创建了一个应用!您将在应用下拉列表中看到您的控制台,例如,“Twilio ACD”

  4. 配置屏幕弹出:

    -您可以在设置➤呼叫中心➤(您的呼叫中心)➤软电话布局中配置弹出屏幕响应,例如弹出搜索屏幕。

这些步骤是从 Charles 的原始帖子中借用的,因为它们没有改变。

摘要

现在,您已经有了一个工作的实时呼叫中心 ACD 系统,它可以独立使用(作为一个单独的软电话),也可以在 Salesforce 等 CRM 中使用,或者在完全围绕它构建的 CRM 中使用,这将在第二部分中介绍。如果您对最初的客户端 acd 非常熟悉,那么除了在 Node 中重写并使用 Flybase 作为后端/信号系统之外,没有太多变化,这就是本章的计划,因为我想演示如何在呼叫中心中使用 Flybase,这一直是各种项目的首选。

只是提醒一下,你可以在这里找到完整的源代码: https://github.com/flybaseio/callcenter

六、发送每日短信提醒

在这一章中,我将向你展示如何使用 Node.js、Flybase 和 Twilio 来编写你自己的每日短信提醒应用。

必要的工具

  • Twilio 发送和接收短信

  • 存储订阅我们服务的用户

  • Node.js 构建在 Chrome 的 JavaScript 运行时之上,轻松构建快速、可扩展的网络应用

使用 Cron 安排 SMS 消息

首先,我们需要安装几个 npm 包。我们将使用 twilio 包( https://github.com/twilio/twilio-node )来发送文本消息,并且我们将使用 cron 包( https://github.com/ncb000gt/node-cron )来安排我们想要发送文本消息的时间。您可以通过运行以下命令来安装它们:

npm install twilio
npm install cron

创建一个名为 app.js 的新文件,并需要 twilio 和 cron 包:

var twilio = require('twilio'),
client = twilio('ACCOUNTSID', 'AUTHTOKEN'),
cronJob = require('cron').CronJob;

让我们写一些每天下午 6 点发送短信的代码:

var textJob = new cronJob( '0 18 * * *', function(){
  client.sendMessage( { to:'YOURPHONENUMBER', from:'YOURTWILIONUMBER', body:'Hello! Hope you're having a good day!' }, function( err, data ) {});
}, null, true);

您可能想知道我们作为 cronJob 的第一个参数传递的字符串是什么。这是一种特定于 Cron 的格式,允许我们定义希望该作业启动的时间和频率。

在这种情况下,每天 18 小时的 0 分钟。这篇文章( www.nncron.ru/help/EN/working/cron-format.htm )很好地打破了 Cron 格式。

在对 cronJob 的回调中,我们使用 Twilio 客户端库发送消息。我们传递收件人和发件人号码以及我们想要发送的消息正文。

运行这段代码,等待你的短信。如果是上午 10 点,你可能不想等 8 个小时来看看你的代码是否工作。只需更新 Cron 格式,在更早的时间发送即可。给你个提示。要在上午 10:13 发送,您可以使用以下格式:“13 10 * * *”。

你现在有了这个应用的基本版本,但你很可能不想每天只给自己发一条消息。如果你有,那么恭喜你!你们都完了!对于我们其余的人,我们可以做一些小的代码修改,让这个发送到多个电话号码。

首先,让我们添加一个名为 numbers 的新变量,它包含我们要向其发送消息的电话号码:

var numbers = ['YOURPHONENUMBER', 'YOURFRIENDSPHONENUMBER'];

然后,让我们更新 textJob 中的代码,循环遍历这些电话号码并向它们发送消息:

for( var i = 0; i &lt; numbers.length; i++ ) {
  client.sendMessage( { to:numbers[i], from:'YOURTWILIONUMBER', body:'Hello! Hope you’re having a good day.'}, function( err, data ) {
    console.log( data.body );
  });
}

接收短信

现在,我们在需要的时间向不同的号码发送短信,让我们更新代码,以了解用户何时向我们的应用发送短信。Twilio 使用 webhooks ( https://en.wikipedia.org/wiki/Webhook )来让您的服务器知道何时有消息或电话进入我们的应用。我们需要设置一个端点,我们可以告诉 Twilio 将它用于消息传递 webhook。

我们将使用 express 框架( http://expressjs.com/ )来设置我们的 Node web 服务器,以接收来自 Twilio 的 POST 请求,因此我们需要安装 Express 包。我们还将使用 body-parser 模块,所以我们也要安装它:

npm install express
npm install body-parser

在 app.js 文件的开头,我们需要 require express 并将其初始化为一个名为 app 的变量。我们还将使用 bodyParser 中间件( https://github.com/expressjs/body-parser )来方便地使用我们将在 POST 请求中获得的数据:

var express = require('express'),
bodyParser = require('body-parser'),
app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: true
}));

我们将为/message 添加一个路由,用一些 TwiML ( www.twilio.com/docs/api/twiml )进行响应。TwiML 是一组基本指令,当你收到来电或短信时,你可以用它来告诉 Twilio 该做什么。我们的代码将如下所示:

app.post('/message', function (req, res) {
  var resp = new twilio.TwimlResponse();
  resp.message('Thanks for subscribing!');
  res.writeHead(200, {
    'Content-Type':'text/xml'
  });
  res.end(resp.toString());
});

我们使用 Twilio Node 库来初始化一个新的 TwimlResponse。然后我们使用消息动词( www.twilio.com/docs/api/twiml/sms/message )来设置我们想要用什么来响应消息。在这种情况下,我们只会说“感谢订阅!”然后,我们将响应的内容类型设置为text/xml,并发送我们构建的 TwimlResponse 的字符串表示。

最后,让我们将服务器设置为侦听端口 3000:

var server = app.listen(3000, function() {
  console.log('Listening on port %d', server.address().port);
});

现在让我们启动我们的应用:

node app.js

现在我们已经运行了服务器,我们需要告诉 Twilio 使用这个消息 URL 作为我们的消息请求 URL:

![](https://gitee.com/OpenDocCN/vkdoc-node-zh/raw/master/docs/rt-twilio-flybase/img/EDpe7a4_f17kekwXJmzaPj53kvW913UZHr-lEvlKP588mR5jHzIzUd7g48GSzkSzz5INNI9sh3Mygtmstiz4YmCuFznnTSlWpZV0bEFXjjnlU8mZzHR_SL-7nyEHWTmolw)

向您的 Twilio 号码发送短信,您应该会收到回复。如果你不知道,看看 Twilio 应用监视器( www.twilio.com/user/account/developer-tools/app-monitor )来帮助确定哪里出了问题。

在 Flybase 中保存用户

我们设置了一个脚本,每天在同一时间发送短信,我们让用户能够在我们的应用中发送短信。只剩下最后一件事要做了。当用户向我们的应用发送文本时,我们需要保存他们的信息。我们将使用 Flybase ( www.flybase.io/ )作为我们的数据存储,因此我们需要安装 Flybase Node 模块:

npm install flybase

现在我们已经安装了 Flybase 模块,让我们在 app.js 文件的顶部要求并初始化它:

var api_key = "{YOUR-API-KEY}";
var db = "dailysms";
var collection = "users";

var usersRef = require('flybase').init(db, collection, api_key);

当你注册一个 Flybase 帐户时,他们会为你的帐户提供一个 API 密钥。请务必更新此代码,用此键替换{YOUR-API-KEY}

从 Flybase 内部,创建一个名为dailysms的新应用。

因为我们将从 Flybase 中提取电话号码,所以我们希望将 numbers 变量更新为一个空数组,然后用数据库中的信息填充它。

Flybase 是一个实时数据库,它是围绕订阅事件而构建的,而不是按需阅读。我们将订阅两个事件:首先,我们希望检索所有现有电话号码的列表,然后我们希望在添加新用户时得到通知:

var numbers = [];
usersRef.on('value', function(snapshot) {
      snapshot.forEach( function( rec ){
           numbers.push( rec.value().phonenumber );
           console.log( 'Added number ' + rec.value().phonenumber );
      });
});

usersRef.on('added', function(snapshot) {
     numbers.push( snapshot.value().phonenumber );
     console.log( 'Added number ' + snapshot.value().phonenumber );
});

现在,我们需要添加用户到我们的数据库时,他们在订阅文本。让我们重新审视一下我们的消息路径,以进行更新:

```javascript
app.post('/message', function (req, res) {
     var resp = new twilio.TwimlResponse();
     if( req.body.Body.trim().toLowerCase() === 'subscribe' ) {
          var fromNum = req.body.From;
          if(numbers.indexOf(fromNum) !== -1) {
               resp.message('You already subscribed!');
          } else {
               resp.message('Thank you, you are now subscribed. Reply "STOP" to stop receiving updates.');
               usersRef.push({phonenumber:fromNum});
          }
     } else {
          resp.message('Welcome to Daily Updates. Text "Subscribe" receive updates.');
     }
     res.writeHead(200, {
          'Content-Type':'text/xml'
     });
     res.end(resp.toString());
});

```js

当 Twilio 消息 webhook 向您的服务器触发一个新的 POST 请求时,我们会在请求参数( [`www.twilio.com/docs/api/twiml/sms/twilio_request#request-parameters`](http://www.twilio.com/docs/api/twiml/sms/twilio_request%2523request-parameters) )中包含关于消息的信息。

我们将使用 Body 参数来检查用户发送的文本内容,使用 From 参数来确定用户发送的文本数量。如果他们已经输入了单词“subscribe ”,并且他们还不在我们的数据库中,我们将使用我们的 Flybase 引用上的 push 函数来添加他们。

我们的应用现已准备就绪。让我们运行并尝试一下:

node app.js


## 摘要

我们做到了!现在你已经建立了一个简单的每日短信提醒应用,这是你自定义每日信息的机会。

# 七、构建实时呼叫跟踪仪表板

本章将向您展示如何实现实时呼叫跟踪仪表板。

我们将分两部分完成这项工作:第一部分是一个简单的 Node.js 文件,它接受来自 Twilio 的来电,然后将信息存储在 Flybase 应用中,第二部分是仪表板本身。

我们将显示两个统计数据,传入的 Twilio 电话号码和电话的始发城市。您可以稍后在此基础上进一步构建。

使用我们最初的仪表板,我们传递事件,实际上不存储任何信息。这一次,我们将存储信息,以便以后检索。

## 后端

让我们构建您的仪表板的后端部分。

首先,让我们设置我们的“package.json”文件:

```js
{
  "name": "call-tracking",
  "version": "1.0.0",
  "description": "Example app demonstrating how to do call tracking with Twilio and Flybase",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/flybaseio/call-tracking.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/flybaseio/call-tracking/issues"
  },
  "homepage": "https://github.com/flybaseio/call-tracking#readme",
  "dependencies": {
    "body-parser": "¹.15.2",
    "compression": "¹.6.2",
    "cors": "².8.1",
    "ejs": "².5.2",
    "express": "⁴.14.0",
    "flybase": "¹.7.8",
    "method-override": "².3.6",
    "serve-static": "¹.11.1"
  }
}

现在,让我们设置“index.js”文件作为后端运行:

var http = require('http');
var express = require('express');
var bodyParser = require('body-parser');
var flybase = require('flybase');
var path = require('path');

var cors = require('cors');
var compression = require('compression');
var serveStatic = require('serve-static');

var app = express();
app.set('view engine', 'ejs');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({     extended: true     }));
app.use(express.static( path.join(__dirname, 'public')));

var port = process.env.PORT || 5000; // set our port

var flybaseRef = flybase.init('YOUR-FLYBASE-APP-NAME', "calltracking", 'YOUR-FLYBASE-API-KEY');

//     backend

app.post('/call', function(req, res) {
     flybaseRef.push({
          time: Date.now()/1000,
          number: req.body.To,
          city: req.body.FromCity
     }).then( function( rec ){
          res.type('text/xml');
          res.render('twiml', { message: 'Your call has been recorded!' })
     }, function(err){
          res.type('text/xml');
          console.log(error);
          res.render('twiml', { message: 'Sorry, an error happened.' });
     });
});

现在,让我们添加前端处理程序。这只是同一个“index.js”文件的一部分:

```javascript
// frontend

function setCustomCacheControl(res, path) {
     if (serveStatic.mime.lookup(path) === 'text/html') {
          // Custom Cache-Control for HTML files
          res.setHeader('Cache-Control', 'public, max-age=0')
     }
}

app.use(compression());

app.use(serveStatic(__dirname + '/dashboard', {
     maxAge: '1d',
     setHeaders: setCustomCacheControl,
     'index': ['index.html'],
     fallthrough: true
}));

var server = http.createServer(app);
server.listen(process.env.PORT || 3000, function() {
     console.log('Express server started.');
});
```js

我在这里使用了 serve-static 模块,因为如果我们希望的话,仪表板可以是独立的,所以它只是静态提供的标准 HTML 页面,所以我们会告诉我们的应用显示dashboard文件夹中的任何文件。

最后,我们需要创建一个名为views的文件夹,并添加一个名为twiml.ejs的小文件:

<Response>
     <Say><%= message %></Say>
</Response>

这用于返回我们对传入呼叫的 TwiML (Twilio 标记语言)响应。你可以进一步玩这个,让它做一些事情,如将呼叫连接到另一个号码等等,但对于这个应用,我们只需要记录和跟踪。

前端

我们希望这个仪表板能够在任何地方运行,所以我们只需包含dashboard文件夹,并设置我们的 Node 应用来静态地为其提供服务。你可以上传dashboard文件夹到任何你想上传的地方,让它运行并显示你的电话追踪数据。

创建一个名为dashboard的文件夹。现在,在dashboard文件夹中创建一个名为index.html的文件:

<!doctype html>
<html>
     <head>
          <title>Call Tracking On the Fly</title>
          <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
          <link href="https://cdnjs.cloudflare.com/ajax/libs/epoch/0.5.2/epoch.min.css" rel="stylesheet" />
          <link href="dashboard.css" rel="stylesheet" />
     </head>
     <body>
          <div class="navbar-nav navbar-inverse navbar-fixed-top">
                    <div class="container">
                    <div class="navbar-header">
                         <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                              <span class="icon-bar"></span>
                              <span class="icon-bar"></span>
                              <span class="icon-bar"></span>
                         </button>
                         <a class="navbar-brand" href="index.html">
                              Call Tracking Dashboard
                         </a>
                    </div>
                         <div class="navbar-collapse collapse">
                              <ul class="nav navbar-nav">
                                   <li class="active">
                                        <!-- <a href="index.html">
                                             <i class="icon-home icon-white"></i> Home
                                        </a> -->
                                   </li>
                              </ul>
                         </div><!--/.nav-collapse -->
                    </div>
          </div>

          <div class="container">

               <div class="row">

                    <div class="col-sm-12 col-lg-12">
                         <article class="widget">
                              <div class="widget-inner">

                                   <header>
                                        <h1>Calls</h1>
                                   </header>

                                   <section class="widget-body">
                                        <div id="calls" class="epoch" style="height: 200px;"></div>
                                   </section>

                              </div><!-- .widget-inner -->

                         </article>
                    </div>

               </div>

               <div class="row">

                    <div class="col-sm-6 col-lg-6">
                         <article class="widget">
                              <div class="widget-inner">

                                   <header>
                                        <h1>Incoming Number</h1>
                                   </header>

                                   <section class="widget-body">
                                        <div id="numbers" class="epoch" style="height: 200px;"></div>
                                   </section>

                              </div><!-- .widget-inner -->

                         </article>
                    </div>

                    <div class="col-sm-6 col-lg-6">
                         <article class="widget">
                              <div class="widget-inner">

                                   <header>
                                        <h1>City</h1>
                                   </header>

                                   <section class="widget-body">
                                        <div id="cities" class="epoch" style="height: 200px;"></div>
                                   </section>

                              </div><!-- .widget-inner -->

                         </article>
                    </div>
               </div>

          </div>

     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/epoch/0.5.2/epoch.min.js"></script>
     <script src="https://cdn.flybase.io/flybase.js"></script>
     <script src="dashboard.js"></script>
</body>
</html>

接下来,您将创建一个名为dashboard.js的文件:

$( function() {
     var calls = $('#calls').epoch( {
          type: 'time.area', axes: ['left', 'bottom', 'right'],
          data: [ { values: [ { time: Date.now()/1000, y: 0 } ] } ]
     } );
     var numbers = $( '#numbers' ).epoch( { type: 'bar' } );
     var cities = $( '#cities' ).epoch( { type: 'bar' } );
     var stats = {
          cities: {},
          numbers: {}
     };

     var dashboard = new Flybase("YOUR-FLYBASE-API-KEY", "calltracking", "stats");

     dashboard.once('value', function (data) {
          updateStats( data );
     });

     dashboard.on( 'added', function (data ){
          updateStats( data );
     });

     function updateStats( data ){
          //     process the new data...
          data.forEach( function( snapshot ){
               var row = snapshot.value();

               calls.push( [ { time: row.time, y: 1 } ] );

               var cityCount = stats.cities[ row.city ] || 0;
               stats.cities[ row.city ] = ++cityCount;

               var numberCount = stats.numbers[ row.number ] || 0;
               stats.numbers[ row.number ] = ++numberCount;
          });

          var citiesData = [];
          for( var city in stats.cities ) {
               citiesData.push( { x: city, y: stats.cities[ city ] } );
          }
          cities.update( [ { values: citiesData } ] );

          var numbersData = [];
          for( var number in stats.numbers ) {
               numbersData.push( { x: number, y: stats.numbers[ number ] } );
          }
          numbers.update( [ { values: numbersData } ] );

     }
});

这是我们仪表板的大脑;它处理所有的电话,并显示在仪表板上。

最后,我们来添加一些 CSS。

创建一个名为“dashboard.css”的文件,并添加以下内容:

body {
  font: 400 0.95em/1 "Proxima Nova", Helvetica,sans-serif;
  font-size: .875em;
  background-color: #f0f0f0;

  padding-top: 90px;
}

.widget {
  -webkit-box-shadow: #f0f0f0 0 0 8px;
  -moz-box-shadow: #f0f0f0 0 0 8px;
  box-shadow: #f0f0f0 0 0 8px;
  background-color: #f0f0f0;

  margin-bottom: 30px;
}

.widget h1 {
  font-size: 1.0em;
  margin: 0 0 .4em;
  font-weight: bold;
}

.widget .widget-inner>header, .widget .widget-inner>footer {
  font-size: 12px;
  text-shadow: 1px 1px #0e0e0e;
}

.widget .widget-inner>header {
  background-color: #272727;
  text-transform: uppercase;
  padding: 16px 12px 16px 26px;
  font-weight: 700;
}

.widget .widget-inner {
  border: solid 1px #e5e5e5;
  background-color: #fff;
}

.widget .widget-inner>header {
  background-color: #f5f5f5;
}

.widget .widget-inner>header h1 {
  color: #8b8b8b;
  text-shadow: 1px 1px #fff;
  margin-bottom: 0;
}

.widget .widget-body {
  color: #666;

  height: 225px
}

.widget .widget-body {
  padding: 16px;
  color: #d3d4d4;
  font-family: Helvetica, Arial, sans-serif;
  z-index: 1;
}

.widget .widget-inner>footer {
  color: #8b8b8b;
  background-color: #f5f5f5;
  text-shadow: 1px 1px #fff;
}

.dash-unit {
  margin-bottom: 30px;
  padding-bottom: 10px;
  border: 1px solid #e5e5e5;
  /*background-image: url('../img/sep-half.png');*/
  background-color: #f5f5f5;
  color: #8b8b8b;
  height: 290px;
  text-align: center;
}

.dash-unit dtitle {
  font-size: 11px;
  text-transform: uppercase;
  margin: 8px;
  padding: 0px;
  height: inherit;
}

.dash-unit hr {
  border: 0;
  border-top: 1px solid #151515;
  border-top-style: dashed;
  margin-top: 3px;
}

摘要

你可以在任何地方运行它。您只需将您想要追踪的 Twilio 电话号码指向您添加到该网站的 URL,并以/call作为终点。在 GitHub 可以看到完整的代码库: https://github.com/flybaseio/call-tracking

标签:function,教程,app,SP,Twilio,var,Flybase,我们
From: https://www.cnblogs.com/apachecn/p/18448031

相关文章

  • 面向-MongoDB-开发者的-CosmosDB-教程-全-
    面向MongoDB开发者的CosmosDB教程(全)原文:CosmosDBforMongoDBdevelopers协议:CCBY-NC-SA4.0一、为什么是NoSQL?自从上学以来,我们大多数人都被教导要组织信息,这样它就可以用表格的形式来表示。但并不是所有的信息都能遵循这种结构,因此存在NULL值。NULL值表示没有信息......
  • 树莓派数据科学教程-全-
    树莓派数据科学教程(全)原文:DataSciencewithRaspberryPi协议:CCBY-NC-SA4.0一、数据科学导论数据是关于主题的文字、数字和描述形式的信息的集合。考虑下面的陈述:“狗有四条腿,1.5米高,有棕色的毛。”这一陈述具有关于狗的三种不同类型的信息(即,数据)。数据“四”和“1.5......
  • ROS基础入门——实操教程
    ROS基础入门——实操教程前言本教程实操为主,少说书。可供参考的文档中详细的记录了ROS的实操和理论,只是过于详细繁杂了,看得脑壳疼,于是做了这个笔记。RubyRose,放在这里相当合理本文初编辑于2024年10月4日CSDN主页:https://blog.csdn.net/rvdgdsva博客园主页......
  • ROS基础入门——实操教程3C
    合集-Ubuntu强化学习合集(3)1.命令行gcc-v和g++-v输出版本不一致09-272.crypt.h:Nosuchfileordirectory报错处理09-283.ROS基础入门——实操教程10-04收起ROS基础入门——实操教程前言本教程实操为主,少说书。可供参考的文档中详细的记录了ROS的实操和理论,只是过于详细......
  • 图片无损放大编辑PhotoZoom Pro 9.0.2多版本软件安装包下载含安装教程
    PhotoZoomPro9.0.2多版本软件是一款非常流行的图像放大软件,它可以让你将低分辨率的图像放大到高分辨率的尺寸,同时保持高质量的图像细节和清晰度。PhotoZoomPro9.0.2多版本软件采用了一种称为S-Spline技术的算法,这是一种能够保持图像细节的高级插值算法。它可以将原始图像分成小......
  • ROS基础入门——实操教程
    ROS基础入门——实操教程前言本教程实操为主,少说书。可供参考的文档中详细的记录了ROS的实操和理论,只是过于详细繁杂了,看得脑壳疼,于是做了这个笔记。RubyRose,放在这里相当合理前言:本文初编辑于2024年10月24日CSDN主页:https://blog.csdn.net/rvdgdsva博客园主页:https://......
  • 帝国CMS7.2/7.5移动端/手机端/多终端访问设置图文教程
    ​随着PC互联网与移动互联网的不断融合、以及各类移动访问终端增加,网站移动互联越来越重要了,所以帝国CMS7.2/7.5版本在原来版本的多访问终端功能基础上,做出更多的改进,让网站多种移动访问端制作更加方便。下面我们来讲解帝国CMS7.2版本的“多终端访问功能”使用:新增网站访问端步......
  • 这可能是最全的输入法教程了
    输入法是我们离不开的软件,如果要评选用户最常使用的工具类应用,输入法一定名列前茅。由于输入法实在太常用了,我们往往会忽略它:我用自带的输入法就行,打几个字,用的着琢磨吗?实则不然,输入法软件有很多,也有很多进阶用法,这也是我写这系列教程的原因——让你打字速度更快,体验更好。‍......
  • 帝国CMS7.2 手机网站使用教程
    下面为详细操作步骤:一、设置所有访问端统一的访问地址后台>系统>系统参数设置:网站地址:这个地址一定不要加上域名,比如设置为:/(默认安装后也是没有加上域名,如果自行修改过,请把域名去掉。)附件地址:这个地址必须加域名,比如设置为:http://www.5300.cn/d/file/ 或者给附件目录/d/file/绑......
  • pbootcms教程—设置的会话目录创建失败!
    当你在宝塔面板上部署PBootCMS模板时,可能会遇到“设置的会话目录创建失败”的问题。这通常是因为文件权限或目录权限设置不当导致的。以下是一步一步的解决方法:解决方法登录宝塔面板打开浏览器,输入宝塔面板的地址(通常是http://你的服务器IP:8888),并登录宝塔面板。找到对......