团队博客

使用 XCC-SDK 搭建简单话务条

前言

XCC 是 XSwitch Call Control(XSwitch 呼叫控制)的缩写。

XSwitch 是一个电信级的 IP 电话软交换系统和综合实时音视频多媒体通信平台,本文主要面向的是使用 XCC 的前端开发人。

XCC_SDK

SDK 原则上使用 HTTPAPI 网关通信,可以扩展使用 WebSocketgRPC 等传输方式。

API 接口同时支持 HTTPWebSocket 两种方式。

业务类接口采用的是 HTTP 方式,消息订阅采用的是 WebSocket 方式。请求及返回数据都为 JSON 格式。

SDK 引入

SDK 支持 es5es6 语法和开发方式,具体可参考 SDK 使用代码示例 。

es5 方式可以直接在 HTML 文件中引入 JS,如:

<script type="text/javascript" src="static/js/index.min.js"></script>

es6 中,可以使用以下方法引入:

import { sys, Xcall } from "@xcall/js";

SDK 使用

本文采用的是 es5 的方式编写 demo, 但以下示例采用 es6 的方式:

import React, { useEffect } from "react";
import { sys, Xcall } from "@xcall/js";

// 初始session security为true 使用https/wss访问服务,全局唯一。
let session = null;

// 默认使用http通信, stream 为 true 时, 表示使用websocket通信
const stream: boolean = false;

let uc = null;

const App: FC = () => {
  // 订阅事件
  const subscribe = () => {
    session.subscribe({
      channels: ["event.agent.xx", "event.channel.xx"],
      handler: (event: any) => {
        console.log(event);
      },
    });
  };

  // 取消订阅
  const unSubscribe = () => {
    session.unsubscribe({
      channels: ["event.agent.xx", "event.agent.xx"],
    });
  };

  useEffect(() => {
    session = new Xcall({
      host: "127.0.0.1:9090",
      security: false,
      contentType: "application/json",
    });

    // 创建一个请求实例
    uc = session.create(sys.ucenter, "sys.ucenter", stream);

    return () => {
      unSubscribe();
    };
  }, []);

  // 刷新token
  const saveToken = (token: string): void => {
    if (!token) return;
    session.setToken(token).then((response) => {
      console.log(message);
      if (response.code === 200) {
        //  TODO
      }
    });
  };

  // 登陆
  const handleLogin = (): void => {
    // 具体参数以 sys.ILoginRequest 为准
    const params: sys.ILoginRequest = {
      username: "John",
      password: "1234",
      type: "account",
      dom: "xxx.com",
    };

    uc.login(params)
      .then((response: sys.LoginResponse): void => {
        const { account, code } = response;
        if (code !== 200) {
          console.log("Logon failed", response.message);
          return;
        }

        saveToken(account?.token);
      })
      .catch((reson: any) => {
        console.log(reson);
      });
  };

  return (
    <div className="App">
      <Button type="primary" onClick={handleLogin}>
        登陆
      </Button>
    </div>
  );
};

export default App;

实操

上述是如何引入 SDK 和使用 SDK,下面我们开始实操,搭建一个简易的话务条。

登陆系统获取基本信息

登陆系统后,我们需要获取到当前登陆用户的基本信息,包括用户 userid ,坐席分机号码 extensionNumber 等信息,以便我们后期使用。 使用示例如下:

<!DOCTYPE html>
<html>
  <head>
    <title>使用XCC-SDK搭建简单话务条demo</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <style>
      .btn-outline {
        display: flex;
        align-items: center;
        justify-content: center;
        border: 1px solid #f4f4f4;
        border-radius: 4px;
        color: limegreen;
      }
    </style>
    <script type="text/javascript" src="dist/index.min.js"></script>
    <link
      rel="stylesheet"
      href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.css"
    />
  </head>

  <body class="bg-light">
    <div class="container align-middle">
      <div class="row py-3 justify-content-center">
        <div class="col-12 col-md-4" id="loginCard">
          <div class="card">
            <div class="card-body">
              <h5>Connect</h5>
              <div class="form-group">
                <label for="dom"></label>
                <input
                  type="text"
                  class="form-control"
                  id="dom"
                  placeholder="Enter Dom"
                  onchange="saveInLocalStorage(event)"
                />
              </div>
              <div class="form-group">
                <label for="app">账户</label>
                <input
                  type="text"
                  class="form-control"
                  id="username"
                  placeholder="Enter Username"
                  onchange="saveInLocalStorage(event)"
                />
              </div>
              <div class="form-group">
                <label for="password">密码</label>
                <input
                  type="text"
                  class="form-control"
                  id="password"
                  placeholder="Enter your Password"
                  onchange="saveInLocalStorage(event)"
                />
              </div>
              <button
                id="btnConnect"
                class="btn btn-block btn-success"
                onclick="connect()"
              >
                登陆
              </button>
              <button
                id="btnDisconnect"
                class="btn btn-block btn-danger d-none"
                onclick="disconnect()"
              >
                退出登录
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <script type="text/javascript">
      let client;
      var currentCall = null;
      let token = null;
      let stream = false;
      let host = "dev.xswitch.cn:9090";

      // ucenter
      let ucenter;
      let currentUser = {};

      let outDialNumber = "";
      let extensionNumber = "";
      // xcc
      let XCC;
      let agents;

      let username = localStorage.getItem("relay.example.username") || "";
      let password = localStorage.getItem("relay.example.password") || "";
      let dom = localStorage.getItem("relay.example.dom") || "";

      // 关闭consloe.log
      logger.setLevel(5);

      /**
       * On document ready auto-fill the input values from the localStorage.
       */
      window.onload = function () {
        document.getElementById("dom").value = dom;
        document.getElementById("username").value = username;
        document.getElementById("password").value = password;
      };

      function saveInLocalStorage(e) {
        const key = e.target.id;
        window.localStorage.setItem(`relay.example.${key}`, e.target.value);
      }

      // 获取坐席信息
      function getAgents(uuid) {
        agents
          .get({
            uuid,
          })
          .then(function (response) {
            if (response.code != 200) {
              console.error(response.message);
              return;
            }

            currentAgent = response.agent || {};
            const state = getLabels(agentState, currentAgent.state);
            Status.innerText = state;

            // 获取坐席分机号码
            extensionNumber =
              currentAgent.agentCidNumber ||
              (currentUser.extensions.length ? currentUser.extensions[0] : "");
          });
      }

      /**
       * Connect with Relay creating a client and attaching all the event handler.
       */
      function connect() {
        // 初始client security为true 使用https/wss访问服务,全局唯一。
        client = new Xcall({
          host,
          security: false,
          contentType: "application/json",
        });

        // 创建ucenter请求实例
        ucenter = client.create(sys.ucenter, "sys.ucenter", stream);

        // 创建XCC请求实例
        XCC = client.create(xcc.Xcc, "xcc.Xcc", stream);

        // 坐席
        agents = client.create(xcc.agent, "xcc.agent", stream);

        const params = {
          username: localStorage.getItem("relay.example.username"),
          password: localStorage.getItem("relay.example.password"),
          type: "account",
          dom: localStorage.getItem("relay.example.dom"),
        };

        ucenter
          .login(params)
          .then(function (response) {
            console.log("userLogin", response);
            if (response.code != 200) {
              console.error(response.message);
              alert(response.message);
              return;
            }
            token = response.account.token;
            id = response.account.id;

            loginCard.classList.add("d-none");

            traffic.classList.remove("d-none");

            // 刷新token
            client.setToken(token);

            console.log("connent to ", client);

            queryCurrent();

            btnConnect.classList.add("d-none");
            btnDisconnect.classList.remove("d-none");
          })
          .catch(function (value) {
            console.log(value);
          });

        // Update UI on socket close
        client.on("xcall.error", function () {
          btnConnect.classList.remove("d-none");
          btnDisconnect.classList.add("d-none");

          window.token = null;
          token = null;
          console.log("socket.close", token);
        });
      }

      // 获取用户信息
      function queryCurrent() {
        ucenter.currentUser({}).then(function (response) {
          if (response.code != 200) {
            console.error(response.message);
            return;
          }

          currentUser = response.currentUser;

          if (currentUser.isAgent) {
            getAgents(currentUser.userid);
          }
        });
      }

      // 断开
      function disconnect() {
        XCC = null;
        ucenter = null;
        btnConnect.classList.remove("d-none");
        btnDisconnect.classList.add("d-none");

        window.token = null;
        token = null;
      }
    </script>
  </body>
</html>

subscribe 订阅事件/unsubscribe 取消订阅事件

用户登录之后,我们可以订阅事件用于监听 agent(坐席), channel(通话), queue(队列), message(消息) 等变化。

每个订阅事件都有一个唯一的 topicchannels 数组里面的就是需要订阅服务的 topic, 每个订阅事件订阅成功且有数据变化的时候,我们可以在 handler这个函数里面获取到每个订阅事件推送等消息。

下面是示例代码

  ...
  // currentUser 就是当前登陆用户信息
  const topicForAgent = `event.agent.${currentUser.userid}`;
  const topicForChannel = `event.channel.${currentUser.userid}`;
  const topicForAgentStatus = `event.agent.status.${currentUser.userid}`
  const sessionChannels = [topicForAgent, topicForChannel, topicForAgentStatus];
  // 订阅事件
  function subscribe() {
    client.subscribe({
      channels: sessionChannels,
      handler: (e) => {
        const data = e.body;
        switch (e.topic) {
          case topicForAgentStatus:
            // 监听坐席状态变化
            // TODO
            break;
          case topicForAgent:
            // TODO
            break;
          case topicForChannel:
            // TODO
            break;
          default:
            break;
        }
      },
    });
  }

  // 取消订阅
  function unSubscribe() {
    if (sessionChannels.length && client) {
      client.unsubscribe({
        channels: sessionChannels
      });
    }
  }

用户退出系统时需要调用 unSubscribe 方法, 以便取消订阅,节省资源。

  1. 话务条基本功能 话务条基本功能包括签入 签出 呼叫 挂机 静音 转接 等等,我们选用以下几种常见等操作。

先来一段 html 编写下基本功能的页面,样式可以忽略!

<div class="col-12 col-md-8 mt-2 mt-md-1 d-none" id="traffic">
  <div class="row">
    <div id="Status" class="px-3 ml-2 col-md-auto btn-outline"></div>
    <button
      id="Login"
      class="btn px-3 ml-2 col-md-auto btn-success"
      onClick="onXcc(this)"
    >
      签入
    </button>
    <button
      id="Logout"
      class="btn btn-primary px-3 ml-2 col-md-auto btn-danger d-none"
      onClick="onXcc(this)"
    >
      签出
    </button>
    <input
      type="tel"
      class="form-control px-3 ml-2 col-md-2"
      id="DestNumber"
      disabled
      onchange="handleNumberChange(event)"
    />
    <button
      id="Dial"
      class="btn btn-primary px-3 ml-2 col-md-auto"
      onClick="onXcc(this)"
      disabled
    >
      呼叫
    </button>
    <button
      id="Kill"
      class="btn btn-primary px-3 ml-2 col-md-auto btn-danger d-none"
      onClick="onXcc(this)"
    >
      挂机
    </button>
    <button
      id="Mute"
      class="btn btn-primary px-3 ml-2 col-md-auto"
      onClick="onXcc(this)"
      disabled
    >
      静音
    </button>
    <button
      id="unMute"
      class="btn btn-primary px-3 ml-2 col-md-auto d-none"
      onClick="onXcc(this)"
    >
      解除静音
    </button>
    <button
      id="Transfer"
      class="btn btn-primary px-3 ml-2 col-md-auto"
      onClick="onXcc(this)"
      disabled
    >
      转接
    </button>
  </div>
</div>

  • 空闲 坐席话务状态。
  • 签入 坐席签入按钮(点击该按钮,会变成 签出 )。
  • 输入框 填写需要呼叫的号码。
  • 呼叫 呼叫按钮(点击该按钮,会变成 挂机 )。
  • 静音 静音按钮(点击该按钮,会变成 解除静音 )。
  • 转接 通话转接按钮。

坐席签入

外呼

通话中

坐席未 签入 前,输入框 呼叫 静音转接 等操作不能生效。签入输入框呼叫处于可操作状态,其他按钮需要在通话中才可以操作状态,在实际开发中需要注意。

基本操作功能具体逻辑,代码如下:

  ...

  let outDialNumber = ''
  let extensionNumber = ''
  // xcc
  let XCC;
  let agents;

  // xcc callcenter
  let currentCallUUID = '';
  let currentNodeUUID = '';
  let currentPeerUUID = '';
  let currentSessionUuid = ''

  // 是否来电弹屏
  let isLayer = false;

  // 关闭consloe.log
  logger.setLevel(5);

  const agentState = [
    {
      label: '空闲',
      value: xcc.AgentState.AgentStateIdle,
    },
    {
      label: '空闲', // '就绪',
      value: xcc.AgentState.AgentStateWaiting,
    },
    {
      label: '接收中',
      value: xcc.AgentState.AgentStateReceiving,
    },
    {
      label: '队列通话中',
      value: xcc.AgentState.AgentStateInAQueueCall,
    },
    {
      label: '保留',
      value: xcc.AgentState.AgentStateReserved,
    },
    {
      label: '话后处理',
      value: xcc.AgentState.AgentStateAcw,
    },
    {
      label: '呼叫中',
      value: xcc.AgentState.AgentStateOutboundCall,
    },
    {
      label: '通话中',
      value: xcc.AgentState.AgentStateConnected,
    },
    {
      label: '未知',
      value: xcc.AgentState.AgentStateUnknown,
    },
  ];

  /**
   * xcc call center.
   */
  function onXcc(element) {
    console.log('call:' + element.id)
    if (token == null) {
      console.error("Failed to connect to server")
      return
    }
    switch (element.id) {
      case 'Login':
        XCC.login({
            extensionNumber: extensionNumber,
            stationtype: xcc.StationType.SIP
          })
          .then(function(res) {
            console.log(res, 'Login')
            if (res.code == 200) {
              Login.classList.add('d-none');
              Logout.classList.remove('d-none');
              Dial.removeAttribute('disabled');
              DestNumber.removeAttribute('disabled');
            }
          })
        break
      case 'Logout':
        XCC.logout({}).then(function(res) {
          console.log(res, 'Login')
          Logout.classList.add('d-none');
          Login.classList.remove('d-none');
          Dial.disabled = true;
          DestNumber.disabled = true;
          DestNumber.value = '';
        })
        break
      case 'Dial':
        XCC.dial({
            extensionNumber: extensionNumber,
            destNumber: outDialNumber
          })
          .then(function(res) {
            console.log(res, 'Dial')
            if (res.code == 200) {
              currentCallUUID = res.callUuid
              currentNodeUUID = res.nodeUuid
              currentPeerUUID = res.peerUuid
              currentSessionUuid = res.sessionUuid;
              Kill.classList.remove('d-none');
              Dial.classList.add('d-none');
              return
            }

            alert(res.message)
          })
          .catch(function(err) {
            console.log(err)
          })
        break
      case 'Kill':
        XCC.hangup({
          nodeUuid: currentCallUUID,
          callUuid: currentNodeUUID
        }).then(() => {
          if (res.code == 200) {
            Kill.classList.add('d-none');
            Dial.classList.remove('d-none');
            DestNumber.disabled = true;
          }
        })
        break
      case 'unMute':
        XCC.setMute2({
          nodeUuid: currentNodeUUID,
          callUuid: currentCallUUID,
          level: 0,
          direction: 'READ',
          flag: 'FIRST'
        }).then(function (res) {
          if (res.code != 200) {
            console.error(res.message)
            return
          }

          unMute.classList.add('d-none');
          Mute.classList.remove('d-none');
          console.log('解除坐席静音')
        })
        break;
      case 'Mute':
        XCC.setMute2({
          nodeUuid: currentNodeUUID,
          callUuid: currentCallUUID,
          level: 1,
          direction: 'READ',
          flag: 'FIRST'
        }).then(function (res) {
          if (res.code != 200) {
            console.error(res.message)
            return
          }
          Mute.classList.add('d-none');
          unMute.classList.remove('d-none');
          console.log('坐席静音')
        })
        break;
      case 'Transfer':
        const callUuid = currentPeerUUID || (callingInfo.params?.cc_side == 'agent' ? callingInfo.params
          ?.cc_member_session_uuid : callingInfo.uuid);
        const nodeUuid = currentNodeUUID || callingInfo.node_uuid;
        let originCidNumber = outDialNumber;
        // 呼出
        if (callingInfo.params?.xcc_is_call_out == 'true') {
          outDialNumber = callingInfo.params.xcc_origin_dest_number;
        }

        originCidNumber = outDialNumber || callingInfo.params.xcc_origin_cid_number;
        const calleeUuid = currentCallUUID || (callingInfo.params?.cc_side == 'agent' ? callingInfo.uuid :
          callingInfo.params?.cc_member_session_uuid);
        XCC.transferNew({
          nodeUuid: currentCallUUID,
          callUuid: currentNodeUUID,
          extensionNumber,
          calleeUuid,
          destNumber,
          nodeUuid,
          callUuid,
          stationtype: xcc.StationType.PSTN,
          destAgentUid,
          originCidNumber,
          ccQueue: callingInfo.params?.cc_queue,
          ccMemberUuid: callingInfo.params?.cc_member_uuid
        }).then(() => {
          if (res.code == 200) {
            Kill.classList.add('d-none');
            Dial.classList.remove('d-none');
          }
        })
        break
      default:
        console.log('default break on' + element.id)
        break
    }
  }

  // 获取呼叫号码
  function handleNumberChange(e) {
    outDialNumber = e.target.value
  }

  // 获取label
  function getLabels(arrys, val, text) {
    if (val === undefined) return '';
    const obj = arrys.find((item) => `${item.value}` === `${val}`);
    if (text) {
      return obj ? obj.text : '';
    }
    return obj ? obj.label : '';
  };

在话务条中,通话状态变化,坐席状态变化等都需要通过订阅事件来完成,我们改造下订阅事件:

  ...
  // 订阅事件
  function subscribe() {
    client.subscribe({
      channels: sessionChannels,
      handler: (e) => {
        const data = e.body;
        switch (e.topic) {
          case topicForAgentStatus:
            // TODO
            const state = getLabels(agentState, currentAgent.state);
            Status.innerText = state;
            break;
          case topicForAgent:
            // TODO
            break;
          case topicForChannel:
            // 来电弹屏
            if (data.state == "CALLING") {
              console.log('channel msg:', JSON.stringify(data, null, 2));
            }

            // 当前坐席端
            if (data.params.xcc_uid == currentUser.userid) {
              handleChannel(data)
            }
            break;
          default:
            break;
        }
      },
    });
  }

  // channel消息处理函数
  function handleChannel(data = {}) {
    // 呼入弹屏
    if (data.state == 'CALLING' && data.params?.xcc_is_call_out != "true") {
      if (!isLayer && !data.answered) {
        // data.params?.xcc_origin_cid_number
        console.log('来电号码:' + data.params?.xcc_origin_cid_number)

        isLayer = true;

        // 这里实现具体业务逻辑
        alert('来电号码:' + data.params?.xcc_origin_cid_number);
      }
    }

    // 是否通话中
    isCalling = data.uuid && data.state != 'DESTROY';

    if (isCalling) {
      callingInfo = data;
      // 可以设置转接/静音
      Transfer.removeAttribute('disabled');
      Mute.removeAttribute('disabled');
      // 通话中不能签出操作
      Logout.disabled = true;
      // 通话中不能输入
      DestNumber.disabled = true;
    } else {
      // 恢复话务条初始状态
      resetTraffic();
    }
  }

  // 恢复话务条初始状态
  function resetTraffic() {
    callingInfo = {};
    tempChannels = [];
    isLayer = false;
    currentCallUUID = '';
    currentNodeUUID = '';
    currentPeerUUID = '';
    currentSessionUuid = '';
    DestNumber.value = '';
    outDialNumber = '';
  }

至于 转接 强拆等功能和代码展示等 静音功能类似,在实际开发中需要开发者按找 XCC-SDK 提供的API 传入对应的且正确的参数,就可以实现具体的功能,至于业务逻辑开发者可按照自己的需求进一步扩展。

topicForChannel 这个 topic 我们可以处理 来话 时的具体业务逻辑。

划重点

每次通话结束后需要把上路通话的相关缓存数据还原,具体可以参考 handleChannel 函数。

完整 demo

以下是简单的 demo code,样式相对简单

<!DOCTYPE html>
<html>
  <head>
    <title>使用XCC-SDK搭建简单话务条demo</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <style>
      .btn-outline {
        display: flex;
        align-items: center;
        justify-content: center;
        border: 1px solid #f4f4f4;
        border-radius: 4px;
        color: limegreen;
      }
    </style>
    <script type="text/javascript" src="dist/index.min.js"></script>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.css" />
  </head>

  <body class="bg-light">
    <div class="container align-middle">
      <div class="row py-3 justify-content-center">
        <div class="col-12 col-md-4" id="loginCard">
          <div class="card">
            <div class="card-body">
              <h5>Connect</h5>
              <div class="form-group">
                <label for="dom"></label>
                <input type="text" class="form-control" id="dom" placeholder="Enter Dom"
                  onchange="saveInLocalStorage(event)">
              </div>
              <div class="form-group">
                <label for="app">账户</label>
                <input type="text" class="form-control" id="username" placeholder="Enter Username"
                  onchange="saveInLocalStorage(event)">
              </div>
              <div class="form-group">
                <label for="password">密码</label>
                <input type="text" class="form-control" id="password" placeholder="Enter your Password"
                  onchange="saveInLocalStorage(event)">
              </div>
              <button id="btnConnect" class="btn btn-block btn-success" onclick="connect()">登陆</button>
              <button id="btnDisconnect" class="btn btn-block btn-danger d-none"
                onclick="disconnect()">退出登录</button>
            </div>
          </div>
        </div>

        <div class="col-12 col-md-8 mt-2 mt-md-1 d-none" id="traffic">
          <div class="row">
            <div id="Status" class="px-3 ml-2 col-md-auto btn-outline"></div>
            <button id="Login" class="btn px-3 ml-2 col-md-auto btn-success"
              onClick="onXcc(this)">签入</button>
            <button id="Logout" class="btn btn-primary px-3 ml-2 col-md-auto btn-danger d-none"
              onClick="onXcc(this)">签出</button>
            <input type="tel" class="form-control px-3 ml-2 col-md-2" id="DestNumber" disabled onchange="handleNumberChange(event)">
            <button id="Dial" class="btn btn-primary px-3 ml-2 col-md-auto" onClick="onXcc(this)"
              disabled>呼叫</button>
            <button id="Kill" class="btn btn-primary px-3 ml-2 col-md-auto btn-danger d-none"
              onClick="onXcc(this)">挂机</button>
            <button id="Mute" class="btn btn-primary px-3 ml-2 col-md-auto"
              onClick="onXcc(this)" disabled>静音</button>
            <button id="unMute" class="btn btn-primary px-3 ml-2 col-md-auto d-none"
              onClick="onXcc(this)">解除静音</button>
            <button id="Transfer" class="btn btn-primary px-3 ml-2 col-md-auto"
              onClick="onXcc(this)" disabled>转接</button>
          </div>
        </div>
      </div>
  </div>
    <script type="text/javascript">
      let client;
      var currentCall = null;
      let token = null;
      let stream = false;
      let host = "dev.xswitch.cn:9090";

      // ucenter
      let ucenter;
      let currentUser = {}

      let sessionChannels = [];
      let topicForAgent = '';
      let topicForChannel = '';
      let topicForAgentStatus = '';

      let outDialNumber = ''
      let extensionNumber = ''
      // xcc
      let XCC;
      let agents;

      let username = localStorage.getItem('relay.example.username') || '';
      let password = localStorage.getItem('relay.example.password') || '';
      let dom = localStorage.getItem('relay.example.dom') || '';

      // xcc callcenter
      let currentCallUUID = '';
      let currentNodeUUID = '';
      let currentPeerUUID = '';
      let currentSessionUuid = ''

      // 是否来电弹屏
      let isLayer = false;

      // 关闭consloe.log
      logger.setLevel(5);

      const agentState = [
        {
          label: '空闲',
          value: xcc.AgentState.AgentStateIdle,
        },
        {
          label: '空闲', // '就绪',
          value: xcc.AgentState.AgentStateWaiting,
        },
        {
          label: '接收中',
          value: xcc.AgentState.AgentStateReceiving,
        },
        {
          label: '队列通话中',
          value: xcc.AgentState.AgentStateInAQueueCall,
        },
        {
          label: '保留',
          value: xcc.AgentState.AgentStateReserved,
        },
        {
          label: '话后处理',
          value: xcc.AgentState.AgentStateAcw,
        },
        {
          label: '呼叫中',
          value: xcc.AgentState.AgentStateOutboundCall,
        },
        {
          label: '通话中',
          value: xcc.AgentState.AgentStateConnected,
        },
        {
          label: '未知',
          value: xcc.AgentState.AgentStateUnknown,
        },
      ];

      /**
       * On document ready auto-fill the input values from the localStorage.
       */
      window.onload = (function() {
        document.getElementById('dom').value = dom;
        document.getElementById('username').value = username;
        document.getElementById('password').value = password;
      });

      function saveInLocalStorage(e) {
        const key = e.target.id;
        window.localStorage.setItem(`relay.example.${key}`, e.target.value)
      }

      // 获取坐席信息
      function getAgents(uuid) {
        agents.get({
          uuid
        }).then(function (response) {
          if (response.code != 200) {
            console.error(response.message);
            return;
          }

          currentAgent = response.agent || {};
          const state = getLabels(agentState, currentAgent.state);
          Status.innerText = state;

          // 获取坐席分机号码
          extensionNumber = currentAgent.agentCidNumber || (currentUser.extensions.length ? currentUser.extensions[0] : '')
        });
      }

      /**
       * xcc call center.
       */
      function onXcc(element) {
        console.log('call:' + element.id)
        if (token == null) {
          console.error("Failed to connect to server")
          return
        }
        switch (element.id) {
          case 'Login':
            XCC.login({
                extensionNumber: extensionNumber,
                stationtype: xcc.StationType.SIP
              })
              .then(function(res) {
                console.log(res, 'Login')
                if (res.code == 200) {
                  Login.classList.add('d-none');
                  Logout.classList.remove('d-none');
                  Dial.removeAttribute('disabled');
                  DestNumber.removeAttribute('disabled');
                }
              })
            break
          case 'Logout':
            XCC.logout({}).then(function(res) {
              console.log(res, 'Login')
              Logout.classList.add('d-none');
              Login.classList.remove('d-none');
              Dial.disabled = true;
              DestNumber.disabled = true;
              DestNumber.value = '';
            })
            break
          case 'Dial':
            XCC.dial({
                extensionNumber: extensionNumber,
                destNumber: outDialNumber
              })
              .then(function(res) {
                console.log(res, 'Dial')
                if (res.code == 200) {
                  currentCallUUID = res.callUuid
                  currentNodeUUID = res.nodeUuid
                  currentPeerUUID = res.peerUuid
                  currentSessionUuid = res.sessionUuid;
                  Kill.classList.remove('d-none');
                  Dial.classList.add('d-none');
                  return
                }

                alert(res.message)
              })
              .catch(function(err) {
                console.log(err)
              })
            break
          case 'Kill':
            XCC.hangup({
              nodeUuid: currentCallUUID,
              callUuid: currentNodeUUID
            }).then(() => {
              if (res.code == 200) {
                Kill.classList.add('d-none');
                Dial.classList.remove('d-none');
                DestNumber.disabled = true;
              }
            })
            break
          case 'unMute':
            XCC.setMute2({
              nodeUuid: currentNodeUUID,
              callUuid: currentCallUUID,
              level: 0,
              direction: 'READ',
              flag: 'FIRST'
            }).then(function (res) {
              if (res.code != 200) {
                console.error(res.message)
                return
              }

              unMute.classList.add('d-none');
              Mute.classList.remove('d-none');
              console.log('解除坐席静音')
            })
            break;
          case 'Mute':
            XCC.setMute2({
              nodeUuid: currentNodeUUID,
              callUuid: currentCallUUID,
              level: 1,
              direction: 'READ',
              flag: 'FIRST'
            }).then(function (res) {
              if (res.code != 200) {
                console.error(res.message)
                return
              }
              Mute.classList.add('d-none');
              unMute.classList.remove('d-none');
              console.log('坐席静音')
            })
            break;
          case 'Transfer':
            const callUuid = currentPeerUUID || (callingInfo.params?.cc_side == 'agent' ? callingInfo.params
              ?.cc_member_session_uuid : callingInfo.uuid);
            const nodeUuid = currentNodeUUID || callingInfo.node_uuid;
            let originCidNumber = outDialNumber;
            // 呼出
            if (callingInfo.params?.xcc_is_call_out == 'true') {
              outDialNumber = callingInfo.params.xcc_origin_dest_number;
            }

            originCidNumber = outDialNumber || callingInfo.params.xcc_origin_cid_number;
            const calleeUuid = currentCallUUID || (callingInfo.params?.cc_side == 'agent' ? callingInfo.uuid :
              callingInfo.params?.cc_member_session_uuid);
            XCC.transferNew({
              nodeUuid: currentCallUUID,
              callUuid: currentNodeUUID,
              extensionNumber,
              calleeUuid,
              destNumber,
              nodeUuid,
              callUuid,
              stationtype: xcc.StationType.PSTN,
              destAgentUid,
              originCidNumber,
              ccQueue: callingInfo.params?.cc_queue,
              ccMemberUuid: callingInfo.params?.cc_member_uuid
            }).then(() => {
              if (res.code == 200) {
                Kill.classList.add('d-none');
                Dial.classList.remove('d-none');
              }
            })
            break
          default:
            console.log('default break on' + element.id)
            break
        }
      }

      // 获取呼叫号码
      function handleNumberChange(e) {
        outDialNumber = e.target.value
      }

      /**
       * Connect with Relay creating a client and attaching all the event handler.
       */
      function connect() {
        // 初始client security为true 使用https/wss访问服务,全局唯一。
        client = new Xcall({
          host,
          security: false,
          contentType: 'application/json',
        });

        // 创建ucenter请求实例
        ucenter = client.create(sys.ucenter, 'sys.ucenter', stream);

        // 创建XCC请求实例
        XCC = client.create(xcc.Xcc, 'xcc.Xcc', stream);

        // 坐席
        agents = client.create(xcc.agent, 'xcc.agent', stream);

        const params = {
          username: localStorage.getItem('relay.example.username'),
          password: localStorage.getItem('relay.example.password'),
          type: 'account',
          dom: localStorage.getItem('relay.example.dom')
        };

        ucenter.login(params).then(function(response) {
          console.log("userLogin", response);
          if (response.code != 200) {
            console.error(response.message);
            alert(response.message)
            return;
          }
          token = response.account.token
          id = response.account.id;

          loginCard.classList.add('d-none');

          traffic.classList.remove('d-none');

          // 刷新token
          client.setToken(token);

          console.log('connent to ', client)

          queryCurrent();

          btnConnect.classList.add('d-none');
          btnDisconnect.classList.remove('d-none');

        }).catch(function(value) {
          console.log(value);
        });

        // Update UI on socket close
        client.on('xcall.error', function() {
          btnConnect.classList.remove('d-none');
          btnDisconnect.classList.add('d-none');

          window.token = null;
          token = null
          console.log('socket.close', token);
        });
      }

      // 获取用户
      function queryCurrent() {
        ucenter.currentUser({}).then(function (response) {
          if (response.code != 200) {
            console.error(response.message);
            return;
          }

          currentUser = response.currentUser;

          if (currentUser.isAgent) {
            topicForAgent = `event.agent.${currentUser.userid}`;
            topicForChannel = `event.channel.${currentUser.userid}`;
            topicForAgentStatus = `event.agent.status.${currentUser.userid}`
            sessionChannels = [topicForAgent, topicForChannel, topicForAgentStatus];
            // 订阅
            subscribe();

            getAgents(currentUser.userid)
          }
        });
      }

      // 断开
      function disconnect() {
        XCC = null;
        ucenter = null;
        btnConnect.classList.remove('d-none');
        btnDisconnect.classList.add('d-none');

        window.token = null;
        token = null;

        // 取消订阅
        unSubscribe();
      }

      // 订阅事件
      function subscribe() {
        client.subscribe({
          channels: sessionChannels,
          handler: (e) => {
            const data = e.body;
            switch (e.topic) {
              case topicForAgentStatus:
                // TODO
                const state = getLabels(agentState, currentAgent.state);
                Status.innerText = state;
                break;
              case topicForAgent:
                // TODO
                break;
              case topicForChannel:
                // 来电弹屏
                if (data.state == "CALLING") {
                  console.log('channel msg:', JSON.stringify(data, null, 2));
                }

                // 当前坐席端
                if (data.params.xcc_uid == currentUser.userid) {
                  handleChannel(data)
                }
                break;
              default:
                break;
            }
          },
        });
      }

      // 获取label
      function getLabels(arrys, val, text) {
        if (val === undefined) return '';
        const obj = arrys.find((item) => `${item.value}` === `${val}`);
        if (text) {
          return obj ? obj.text : '';
        }
        return obj ? obj.label : '';
      };

      // channel消息处理函数
      function handleChannel(data = {}) {
        // 呼入弹屏
        if (data.state == 'CALLING' && data.params?.xcc_is_call_out != "true") {
          if (!isLayer && !data.answered) {
            // data.params?.xcc_origin_cid_number
            console.log('来电号码:' + data.params?.xcc_origin_cid_number)

            isLayer = true;

            // 这里实现具体业务逻辑
            alert('来电号码:' + data.params?.xcc_origin_cid_number);
          }
        }

        // 是否通话中
        isCalling = data.uuid && data.state != 'DESTROY';

        if (isCalling) {
          callingInfo = data;
          // 可以设置转接/静音
          Transfer.removeAttribute('disabled');
          Mute.removeAttribute('disabled');
          // 通话中不能签出操作
          Logout.disabled = true;
          // 通话中不能输入
          DestNumber.disabled = true;
        } else {
          // 恢复话务条初始状态
          resetTraffic();
        }
      }

      // 恢复话务条初始状态
      function resetTraffic() {
        callingInfo = {};
        tempChannels = [];
        isLayer = false;
        currentCallUUID = '';
        currentNodeUUID = '';
        currentPeerUUID = '';
        currentSessionUuid = '';
        DestNumber.value = '';
        outDialNumber = '';
      }

      // 取消订阅
      function unSubscribe() {
        if (sessionChannels.length && client) {
          client.unsubscribe({
            channels: sessionChannels
          });
        }
      }
    </script>
  </body>
</html>

开发无止境,一切皆可能。

关于 SDK 获取以及具体 SDK 提供的 API 可以自行查阅文档---XCC-SDK, XCC-API

声音信号