CCBar WebPhone SDK
@16x/webphone-sdk 是浏览器端 WebPhone SDK。客户只需提供服务端取票接口,并在前端配置 createTokenSessionProvider。SDK 会在内部获取坐席账号、创建会话、登录 SIP、刷新凭证并请求坐席状态。
安装
npm install @16x/webphone-sdk
公开包地址:npmjs。
最简接入
1. 服务端实现取票接口
浏览器通过客户自己的 POST /get-token 接口取短期 fs token。服务端从已登录用户的会话中取得分机号,使用服务端环境变量中的 APP Key 和 security(APP Secret)签名调用 POST /openapi/v1/token/fs,然后返回上游的 token 和 expires。不要接受浏览器提交的 APP Key、security 或任意分机号。
取票请求与签名示例见会话服务指南。
2. 创建客户端并连接
import { CCBarClient, createTokenSessionProvider } from '@16x/webphone-sdk';
const client = new CCBarClient({
platform: 'web',
sessionProvider: createTokenSessionProvider({
// 请向商务经理或对接人获取 HTTP API 网关地址
host: 'https://api.example.com',
// 请向商务经理或对接人获取完整的 SIP WebSocket 地址
sipWsUrl: 'wss://sip.example.com/api/fs/sip-ws',
// 这是客户需要实现的唯一服务端方法
getToken: async () => {
const response = await fetch('/get-token', {
method: 'POST',
credentials: 'include',
});
if (!response.ok) throw new Error('获取 token 失败');
return response.json();
},
}),
});
await client.initialize();
await client.connect();
host 用于 SDK 内部请求坐席账号和状态接口;sipWsUrl 是完整的 SIP WebSocket 地址。getToken 可返回 { token, expires },其中 expires 是秒数;也可返回 { token, expiresAt }(Unix 秒)或 { token, expiresAtMs }(Unix 毫秒)。SDK 按字段名解释单位,不会根据数值大小猜测秒或毫秒。
坐席账号响应中的 expiresIn / expires_in 是账号数据刷新间隔(秒),不代表当前 SIP WebSocket 必须断开重建。刷新时 SDK 会更新 SIP 账号密码及 JsSIP 自动重连使用的最新 token;当前 WSS 保持连接,后续底层重连会使用新凭证。
请向商务经理或对接人获取 HTTP API 网关地址 host 和完整的 SIP WebSocket 地址 sipWsUrl。host 用于坐席账号/状态接口;sipWsUrl 应包含 wss:// 协议和服务端提供的路径。另请获取 APP Key、security(仅供客户服务端实现 getToken,不要放进浏览器代码)以及话单查询/推送接口资料;话单由客户业务系统另行集成,SDK 不提供查询或推送能力。
3. 拨打、控制和挂断
const call = await client.dial({ destination: '9196' });
await call.mute();
await call.unmute();
await call.hold();
await call.resume();
await call.hangup();
4. 传递 userData 并关联话单
拨打时可以通过 userdata 传递业务关联信息。SDK 会把它放进 SIP INVITE 请求的 X-User-Data header,供平台侧读取:
const userdata = encodeURIComponent(JSON.stringify({
orderId: 'ORDER-20261003-001',
source: 'crm',
}));
const call = await client.dial({
destination: '9196',
userdata,
});
userdata 必须是字符串;未传、空字符串或纯空白不会发送该 header。值只支持可见 ASCII,不能包含换行、制表符或未编码的中文;传入不合规内容会以 CALL_INVALID_USERDATA 拒绝拨打。像上例一样先对 JSON 做 encodeURIComponent(),平台侧收到 X-User-Data 后再 decodeURIComponent() 和解析即可。不要放入密码、token 等敏感信息,因为 SIP header 可能经过或记录在平台网络设备中。
SDK 负责把 userdata 透传到 SIP 请求。若要让它出现在话单推送或话单查询结果中,平台服务端还需要读取 X-User-Data,将其解析并保存到话单字段;请与平台侧确认字段映射和推送、查询接口是否都返回该字段。SDK 不会直接写入或查询话单。
5. 查看连接状态、坐席状态和拨打结果
通过 client.on() 监听连接状态。连接注册成功和连接状态是不同事件:
const offConnection = client.on('connection.stateChanged', ({ state }) => {
// offline | connecting | connected | reconnecting | failed
console.log('SIP connection:', state);
});
const offRegistered = client.on('connection.registered', () => {
console.log('SIP registered');
});
调用 setAgentStatus() 修改坐席状态;agent.statusChanged 在状态接口成功后发出:
const offAgentStatus = client.on('agent.statusChanged', ({ status }) => {
console.log('Agent status:', status); // available | break | offline
});
await client.setAgentStatus('available');
坐席状态表示坐席在业务平台上的工作状态,不表示 SIP WebSocket 是否已连接,也不表示当前通话状态:
| 坐席状态 | 含义 |
|---|---|
available | 坐席空闲/就绪;是否能接听新呼叫仍由平台的队列和路由策略决定。 |
break | 坐席暂时休息或暂停接听,平台侧对应 On Break。 |
offline | 坐席在业务平台登出,平台侧对应 Logged Out。它本身不关闭 SIP 连接;如需断开连接,还要调用 client.disconnect()。 |
因此,client.setAgentStatus('offline') 和 client.disconnect() 是两个独立操作:前者更新平台坐席状态,后者断开 SDK 的 SIP 连接。反过来,连接掉线也不一定会自动把坐席状态改为 offline。
client.dial() 成功返回 Call 对象不代表通话已接通。监听 call.stateChanged 并检查 to 是否为 active;结束或失败分别通过 call.ended、call.failed 通知:
const offCallState = client.on('call.stateChanged', ({ callId, from, to }) => {
console.log(`Call ${callId}: ${from} -> ${to}`);
// new | dialing | ringing | connecting | active | held | ended | failed
});
const offCallEnded = client.on('call.ended', ({ callId, reason }) => {
console.log('Call ended:', callId, reason);
});
const offCallFailed = client.on('call.failed', ({ callId, error }) => {
showCallError(error.code);
});
拨号发起阶段的错误会让 dial() 拒绝;接通后的通话错误通过 call.failed 事件报告。npm SDK 与 CCBar Script 使用相同的字符串 code 和可读 message。若后端返回数字错误码,SDK 将其保留在可选的 platformCode 中:
import { CCBarError } from '@16x/webphone-sdk';
try {
await client.dial({ destination: '9196' });
} catch (error) {
if (error instanceof CCBarError) {
showCallError(error.message);
reportSafeError({ code: error.code, errno: error.errno, source: error.source, platformCode: error.platformCode });
} else {
showCallError('拨号失败,请稍后重试。');
}
}
客户代码应使用 error.code 做程序判断、将 error.message 显示给用户;platformCode 仅用于排查后端响应,不应作为 SDK 业务分支的依据。
每个错误还带数字编号 errno 和归属 source,用来判断该找谁排查。errno 首位即归属:
| 首位 | source | 含义 |
|---|---|---|
| 1 | client | 本机或调用方(参数、麦克风、浏览器能力等) |
| 2 | platform | 平台接口与注册;有平台业务码时为 2000 + 业务码,否则有 HTTP 错误状态时为 2000 + 状态码 |
| 3 | network | 网络(断网、超时、信令连接中断) |
| 4 | line | 线路(4000 + SIP 码,如 503 → 4503) |
| 5 | callee | 被叫(5000 + SIP 码,如忙线 486 → 5486、拒接 603 → 5603) |
| 6 | platform | 平台拦截了呼叫(欠费、黑名单、频次、线路未分配等),编号与话单结束码一致(如 6033) |
呼叫失败时还可能带 sipCode(SIP 响应码)、q850(失败响应中的 Q.850 原因值)和 platformFailCode(平台拦截结束码原值)。平台拦截的 code 为 PLATFORM_REJECTED(3.2.13 新增,此前报为 CALL_REJECTED),message 会给出具体原因,例如「平台拦截了本次呼叫:欠费(编号 6033)」。其余 code 取值不变。错误码清单和处理建议见 错误码说明。
通话状态含义如下。ended 和 failed 都是终态;一个 Call 到达终态后不会再恢复到其他状态。
| 状态 | 含义 |
|---|---|
new | SDK 已创建 Call 对象,呼叫尚未开始处理;通常是很短暂的初始状态。 |
dialing | SDK 已发起呼叫,正在等待对端响应。 |
ringing | 对端正在振铃,或呼叫流程已进入振铃/早期进展阶段。 |
connecting | 呼叫已应答,正在完成媒体协商或建立音频连接;此时还不能当作通话已接通。 |
active | 通话已接通,可以进行通话控制。 |
held | 通话已保持;调用 call.resume() 可恢复到 active。 |
ended | 通话已正常结束、被挂断或被拒接。 |
failed | 呼叫建立或通话过程中发生失败;查看 call.failed 事件中的错误码。 |
client.on() 返回取消监听函数。页面或组件卸载时调用对应的 off...(),或在客户端销毁时统一清理监听器。
网络和安全要求
- 页面须运行在 HTTPS 或 localhost,并取得浏览器麦克风权限。
- 网关须允许页面的 Origin 和客户出口 IP 调用坐席账号与状态接口;这两项与客户自己的 get-token 接口是分开的,请在上线前确认 CORS 和 IP 白名单。
- APP Key 和 security 只保存在服务端。浏览器运行时会持有短期 fs token 和 SIP 凭证;不要记录含 token 的 WSS URL、Authorization 请求头或 SIP 密码。
- 按客户系统的登录态调用 get-token,避免浏览器自行指定分机号。
连接生命周期
建议在系统账号登录成功或进入拨打列表页时调用 client.connect()。退出账号或离开拨打列表页时,按需先调用 client.setAgentStatus('offline'),再调用 client.disconnect()。客户端实例不再使用时调用 client.dispose() 释放监听器、定时器、连接和媒体资源。重复调用正在连接或已连接的 connect() 会复用当前连接。
常用 API
client.initialize()、client.connect()、client.disconnect()、client.dispose()client.dial({ destination, userdata? })client.setAgentStatus('available' | 'break' | 'offline')call.mute()、call.unmute()、call.hold()、call.resume()、call.hangup()client.on(event, handler)、client.getDiagnostics()
详细 API 和完整事件说明见客户端 API 文档;服务端取票签名示例见会话服务指南。
开发与验证
npm run lint
npm run typecheck
npm test
npm run pack:check
License
Copyright 16X. All rights reserved.
Private customer distribution only. No redistribution or sublicensing without written permission.
Live acceptance (maintainers)
Real-call acceptance is opt-in and runs outside npm test. Copy scripts/live-call-acceptance.env.example to a local .env file, fill it with a dedicated test account, and start a Chromium-based browser with remote debugging enabled at CCBAR_LIVE_CDP_URL. The runner requests microphone permission through CDP and uses only routes 9196 and 9199.
npm run test:live
The runner verifies token signing, account lookup, SIP registration, seat status, repeated connect(), disconnect/reconnect, call controls, credential failures, delayed/lost HTTP requests, browser network loss, and malformed/unreachable STUN behavior. Repeated-connect assertions compare SDK token, account, session, connect, and registration-event counts; they do not claim server-side REGISTER packet counts. It measures dial-to-active time against CC_DIAL_ACTIVE_SLO_MS; each sample must meet the threshold. JSON output contains scenario, stage, status codes, elapsed times, and SDK resource counts; it omits credentials, tokens, raw errors, and WebSocket URLs. Live tests require the gateway Origin/IP allowlists, browser microphone permission, reachable WSS, and both test routes.