WebSocket 呼叫状态订阅接口

适用于:来电弹屏、通话状态实时展示、坐席忙闲展示、排队看板。 读者:将 OA/CRM 等系统与呼叫中心集成的开发人员与 AI 编程工具。

1. 概述

呼叫中心通过 WebSocket 向订阅方主动推送 JSON 格式的实时事件:通话状态(振铃/接通/挂机)、分机忙闲变化、客户排队变化等。订阅以分机号为单位:订阅了某分机,就会收到与该分机相关的事件推送。

服务端实现为 Netty,WS 路径固定为 /websocket。

端点地址说明
WS(明文)ws://{cti_host}:7397/websocket默认端口 7397,以现场配置为准
WSS(加密)wss://{cti_host}:7399/websocket需配置证书

{cti_host} 为呼叫中心(CTI)服务器 IP 或域名。

2. 订阅与退订协议

连接建立后,客户端发送文本帧 JSON 进行订阅:

{
  "method": "SUBSCRIBE",
  "from": "801",
  "to": "801",
  "expires": 3600
}
字段类型必填说明
methodString是固定 SUBSCRIBE
fromString是发起订阅的标识,通常填自己的分机号;服务端在订阅链路不校验该字段
toString是被订阅的分机号,必须是系统中存在的分机,否则订阅被忽略(无回执)
expiresNumber是> 0 表示订阅;<= 0(demo 中传 0)表示退订,退订按 from 匹配

要点:

  • 没有订阅应答报文。订阅是否生效以随后收到的初始状态推送为准(见下节)。
  • expires 正值即可,服务端不按它超时;保活依赖客户端周期性重发 SUBSCRIBE(见 §5)。
  • 一个连接可以多次发送 SUBSCRIBE 订阅多个分机(例如监管场景订阅一组坐席)。
  • 当前版本订阅链路无身份鉴权,能连上端口即可订阅任意分机。生产环境请通过网络层(防火墙/安全组)限制 7397/7399 端口的访问来源。

3. 订阅成功后的初始推送

订阅生效后,服务端立即按以下顺序推送 4 类当前状态,之后进入事件推送模式:

  1. presence —— 该分机当前忙闲状态
  2. dialog —— 该分机当前通话(若正在通话中,state 为 talking 并带 duration)
  3. extinfo —— 该分机的档案信息
  4. queue —— 当前排队情况

4. 推送消息类型(sceenShowType)

所有推送都是 JSON 文本帧,用 sceenShowType 字段区分类型(字段名拼写即如此,注意不是 screenShowType):

4.1 dialog —— 通话状态通知(核心)

一次通话从建立到结束,id 保持不变;客户端可据此把同一通话的多条消息串联起来,并可在挂机后用该 id 下载录音(见 recording-download)。

{
  "id": "1b304a1f-1196-482d-964e-212420a401c9@192.168.1.82",
  "caller": "13512340001",
  "callee": "801",
  "state": "talking",
  "direction": "incoming",
  "group": "3003",
  "origcallee": "3003",
  "sceenShowType": "dialog",
  "duration": 65
}
字段类型说明
idString通话唯一标识(callid),同一通话内不变;挂机后可用于查询呼叫记录/下载录音
callerString主叫号码
calleeString被叫号码
stateStringcalling 尝试呼叫(主叫侧外呼时先推)→ ringing 振铃 → talking 已接通 → hungup 已挂机;canceled 振铃中取消(未接通即结束)
directionStringincoming 被叫侧视角(来电)/ outgoing 主叫侧视角(去电)
durationNumber仅 talking 时出现,接通时长(秒)
groupString可选,话务组号码
origcalleeString可选,原始被叫(客户拨打的热线号/分组号),用于区分客户从哪条线打入
ivrTraceString可选,经过的 IVR 节点路径
ivrInputString可选,客户在 IVR 中的按键
bizDataString可选,转接等操作携带的业务数据(JSON 字符串)
businessIdString可选,保留字段
remoteString可选,真实对端号码
senderString仅 hungup 时出现,挂机的对端号码
sceenShowTypeString固定 dialog(或 batchcall:xxx,见 4.5)

判断来电/去电的常用方式:订阅自己的分机 801,若 caller === "801" 为去电(对端号码取 callee),否则为来电(对端号码取 caller)。

一次典型来电的消息序列:

ringing  (caller=135xxxxxxxx, callee=801)   ← 来电振铃,此时弹屏
talking  (duration=0)                        ← 坐席接听
talking  (duration=65)                       ← 通话中(时长随状态更新推送)
hungup   (sender=135xxxxxxxx)                ← 挂机,通话结束

4.2 presence —— 分机忙闲变化通知

{
  "extnum": "801",
  "state": "busy",
  "sceenShowType": "presence"
}

state 取值(与置忙置闲接口的取值一致,unkown 拼写即如此):online 在线 / busy 置忙 / reducing 减少来电 / unkown 未知。

4.3 queue —— 客户排队变化通知

排队变化会广播给所有订阅者:

{
  "sceenShowType": "queue",
  "content": [
    {
      "group": "3003",
      "count": 2,
      "queue": [
        {
          "caller": "13512340001",
          "callee": "4001234567",
          "waitingtime": 3,
          "target": "3003",
          "origcallee": "4001234567",
          "viplevel": 5
        }
      ]
    }
  ]
}
字段说明
content.group话务组号码
content.count该组当前排队人数
content.queue.caller排队客户号码
content.queue.waitingtime已等待秒数
content.queue.viplevelVIP 插队等级

content 为空数组表示当前无排队。

4.4 extinfo —— 分机档案信息

订阅建立时推送一次:

{
  "sceenShowType": "extinfo",
  "extnum": "801",
  "name": "张三",
  "worknum": "1001",
  "group": "售后组,白班"
}

4.5 batchcall:{任务号码id} —— 批量外呼弹屏通知

批量外呼任务转人工时,该通话的 dialog 消息中 sceenShowType 会被置为 batchcall:<任务号码id>(前缀 batchcall:),消息体结构同 4.1。普通集成可按前缀识别,或直接忽略。

4.6 webphone —— 网页电话(WebRTC)故障通知

{
  "sceenShowType": "webphone",
  "extnum": "801",
  "error": "timeout"
}

仅使用网页电话(WebRTC 软电话)时可能出现,普通 CTI 集成可忽略。

5. 保活与重连

事项说明
服务端心跳7397 明文端口无服务端心跳(不主动断空闲连接)。保活做法:客户端每 20 秒重发一次 SUBSCRIBE(官方示例即如此)。
WSS 心跳7399 加密端口:服务端 30 秒读空闲时发文本帧 ping,客户端可回 pong;客户端也可主动发 ping,服务端回 pong。连续 10 次读空闲服务端关闭连接。
重连断线重连完全由客户端负责,建议用指数退避或定时重连(参考 reconnecting-websocket.js 的做法)。
订阅关系连接断开后订阅关系即失效;重新连接后需重新发送 SUBSCRIBE。

6. 关联接口:置忙置闲(SET_EXT_PRESENCE_STATE)

订阅能收到 presence 变化,对应的状态设置接口(例如坐席弹屏录单时置忙、保存后置闲)。完整契约(含 reducing 语义、查询接口 GET_EXT_PRESENCE_STATE、幂等与多写入口说明)见 ext-presence:

GET http://{cti_host}:12121/bridge/callctrl?extnum=801&state=busy&reason=LeaveSeat&opt=SET_EXT_PRESENCE_STATE
参数必填说明
extnum是分机号
state是online 示闲 / busy 置忙 / reducing 减少来电(不区分大小写;其他取值返回 400)
reason否置忙原因,供对接系统展示
opt是固定 SET_EXT_PRESENCE_STATE

响应体为文本业务码(注意 HTTP 状态码恒为 200,下同):200 成功(幂等重试亦为 200)/ 400 参数错误 / 404 分机不存在。

7. 最小可用 JS 客户端示例

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<script>
const CTI_WS_URL = "ws://192.168.1.80:7397/websocket"; // 改成现场 CTI 地址
const MY_EXTNUM  = "801";                               // 改成要订阅的分机

let ws;
let keepaliveTimer;

function connect() {
  ws = new WebSocket(CTI_WS_URL);

  ws.onopen = function () {
    subscribe();
    // 7397 端口无服务端心跳:每 20 秒重发订阅保活
    if (keepaliveTimer) clearInterval(keepaliveTimer);
    keepaliveTimer = setInterval(subscribe, 20 * 1000);
  };

  ws.onmessage = function (event) {
    const msg = JSON.parse(event.data);
    switch (msg.sceenShowType) {
      case "dialog":      onCallState(msg); break;   // 通话状态(核心)
      case "presence":    console.log("忙闲:", msg.state);   break;
      case "queue":       console.log("排队:", msg.content); break;
      case "extinfo":     console.log("分机:", msg.name);    break;
      default: break;                                  // webphone / batchcall:xxx
    }
  };

  ws.onclose = function () {
    if (keepaliveTimer) clearInterval(keepaliveTimer);
    setTimeout(connect, 3000);                        // 断线 3 秒后重连
  };
}

function subscribe() {
  ws.send(JSON.stringify({
    method: "SUBSCRIBE",
    from: MY_EXTNUM,
    to: MY_EXTNUM,
    expires: 3600
  }));
}

function onCallState(msg) {
  // 同一通话的 ringing/talking/hungup 消息 id 相同
  if (msg.state === "ringing" && msg.caller !== MY_EXTNUM) {
    // 来电弹屏:打开客户资料页等
    console.log("来电弹屏:", msg.caller, "callid=", msg.id);
  }
  if (msg.state === "hungup") {
    // 通话结束,可用 msg.id 下载录音:
    // http://{cti_host}:12121/Oms/FileDownServlet?callid=<URL编码后的id>
  }
}

connect();
</script>
</body>
</html>

官方完整示例(含炫酷弹屏 UI)随产品部署在 tomcat/webapps/demo/callstate/(index.html + js/main.js + js/call.state.api.js),可直接参考。

8. 常见问题排查

现象原因与处理
连接立即断开端口/路径不对:必须是 7397(或 7399)端口 + /websocket 路径,不是 12121
连上但收不到任何消息to 填的分机不存在,订阅被静默忽略;检查分机号
只收到 presence/extinfo,通话时无 dialog订阅的分机与实际通话的分机不一致;或客户端把 sceenShowType 判断写错(注意拼写 sceenShowType)
一段时间后不再收到推送连接已被中间网络设备静默断开:实现定时重发 SUBSCRIBE + onclose 重连
弹屏重复触发未按 id 去重:同一通话会推多条消息,应用 id 区分不同通话
\n\n\n","html",[44,1636,1637,1652,1661,1694,1703,1712,1737,1757,1763,1774,1783,1787,1802,1826,1830,1850,1859,1864,1890,1921,1927,1932,1956,1988,2007,2040,2087,2132,2178,2193,2198,2203,2208,2226,2247,2270,2275,2280,2285,2297,2321,2337,2350,2362,2372,2383,2388,2393,2409,2415,2456,2462,2509,2515,2540,2546,2552,2557,2562,2567,2576,2586,2595],{"__ignoreMap":120},[124,1638,1639,1642,1646,1649],{"class":126,"line":127},[124,1640,1641],{"class":130},"\n",[124,1653,1654,1657,1659],{"class":126,"line":134},[124,1655,1656],{"class":130},"<",[124,1658,1634],{"class":1644},[124,1660,1651],{"class":130},[124,1662,1663,1665,1668,1671,1674,1677,1680,1682,1685,1687,1690,1692],{"class":126,"line":162},[124,1664,1656],{"class":130},[124,1666,1667],{"class":1644},"head",[124,1669,1670],{"class":130},"><",[124,1672,1673],{"class":1644},"meta",[124,1675,1676],{"class":140}," charset",[124,1678,1679],{"class":130},"=",[124,1681,144],{"class":130},[124,1683,1684],{"class":153},"utf-8",[124,1686,144],{"class":130},[124,1688,1689],{"class":130},">\n",[124,2553,2555],{"class":126,"line":2554},57,[124,2556,2514],{"class":130},[124,2558,2560],{"class":126,"line":2559},58,[124,2561,222],{"class":130},[124,2563,2565],{"class":126,"line":2564},59,[124,2566,1762],{"emptyLinePlaceholder":1761},[124,2568,2570,2572,2574],{"class":126,"line":2569},60,[124,2571,2257],{"class":1794},[124,2573,1798],{"class":1719},[124,2575,1773],{"class":130},[124,2577,2579,2582,2584],{"class":126,"line":2578},61,[124,2580,2581],{"class":130},"