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
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| method | String | 是 | 固定 SUBSCRIBE |
| from | String | 是 | 发起订阅的标识,通常填自己的分机号;服务端在订阅链路不校验该字段 |
| to | String | 是 | 被订阅的分机号,必须是系统中存在的分机,否则订阅被忽略(无回执) |
| expires | Number | 是 | > 0 表示订阅;<= 0(demo 中传 0)表示退订,退订按 from 匹配 |
要点:
- 没有订阅应答报文。订阅是否生效以随后收到的初始状态推送为准(见下节)。
expires正值即可,服务端不按它超时;保活依赖客户端周期性重发 SUBSCRIBE(见 §5)。- 一个连接可以多次发送 SUBSCRIBE 订阅多个分机(例如监管场景订阅一组坐席)。
- 当前版本订阅链路无身份鉴权,能连上端口即可订阅任意分机。生产环境请通过网络层(防火墙/安全组)限制 7397/7399 端口的访问来源。
3. 订阅成功后的初始推送
订阅生效后,服务端立即按以下顺序推送 4 类当前状态,之后进入事件推送模式:
presence—— 该分机当前忙闲状态dialog—— 该分机当前通话(若正在通话中,state为talking并带duration)extinfo—— 该分机的档案信息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
}
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 通话唯一标识(callid),同一通话内不变;挂机后可用于查询呼叫记录/下载录音 |
| caller | String | 主叫号码 |
| callee | String | 被叫号码 |
| state | String | calling 尝试呼叫(主叫侧外呼时先推)→ ringing 振铃 → talking 已接通 → hungup 已挂机;canceled 振铃中取消(未接通即结束) |
| direction | String | incoming 被叫侧视角(来电)/ outgoing 主叫侧视角(去电) |
| duration | Number | 仅 talking 时出现,接通时长(秒) |
| group | String | 可选,话务组号码 |
| origcallee | String | 可选,原始被叫(客户拨打的热线号/分组号),用于区分客户从哪条线打入 |
| ivrTrace | String | 可选,经过的 IVR 节点路径 |
| ivrInput | String | 可选,客户在 IVR 中的按键 |
| bizData | String | 可选,转接等操作携带的业务数据(JSON 字符串) |
| businessId | String | 可选,保留字段 |
| remote | String | 可选,真实对端号码 |
| sender | String | 仅 hungup 时出现,挂机的对端号码 |
| sceenShowType | String | 固定 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.viplevel | VIP 插队等级 |
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 区分不同通话 |
