npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yinshan-tech/webrtc-sdk

v0.8.10

Published

Call-center Softphone SDK: JsSIP registration, control WS, dial HTTP, call state (no UI)

Readme

@yinshan-tech/webrtc-sdk

银杉 call-center / FreeSWITCH 的无头 Softphone(当前 0.8.10)。给各国管理后台(ap-admin、ys-web-admin、hc-admin 等)和各部门(催收 / 电销 / 客服)复用。

本库不是催收库、也不是电销库。 国家域名、坐席接口、拨号 HTTP 业务体、案件字段都由宿主注入。用户可见 locale 与后台接线入口(SoftphoneHost / createCallCenterSoftphone)在 webrtc-react;本库 messages 保持英文,且仍不解析案件 / overdue 字段。库内的点呼 / 渐进式 / 预测式是呼叫中心产品模式。

UI 请用 @yinshan-tech/webrtc-react,或自己订阅事件画界面。

规范见 docs/conventions.md。改架构 / 公开 API / 外呼行为时必须同步改 README 与 conventions。

分层

宿主(国家后台)  域名、seat、manualDialUrl、uiContext 业务字段、传 locale、催收文案、开 tab、案件 HTTP
webrtc-react     后台入口 SoftphoneHost + 电话壳三语包 + 渐进 hooks(无 antd)
webrtc-sdk       SIP REGISTER、control WS、CallSnapshot、OutboundTask(不解析案件字段)
call-center      同一套 HTTP/WS;各国域名不同
FreeSWITCH       媒体

安装

npm i @yinshan-tech/webrtc-sdk@^0.8

npm test / npm publish 只读本仓(会 build)。催收 / 电销契约扫描在各宿主仓库。宿主本地联调可以用 file:,不要把旁路仓写进本库测试。不要擅自 bump package.json 版本。

快速开始

import { createSoftphone } from '@yinshan-tech/webrtc-sdk';

const phone = createSoftphone({
  endpoints: {
    controlWsUrl: 'wss://call-center.example/ws',
    sipDomain: 'freeswitch.example',
    sipWsUrl: 'wss://ws-freeswitch.example',
    manualDialUrl: '/CALL_CENTER_BIZ/outbound/manual/dial', // 宿主业务拨号,不是催收路径
    hangupCallUrl: '/CALL_CENTER/call/hangup',
    outboundTaskPauseUrl: '/CALL_CENTER/progressive/task/pause/{taskId}',
    outboundTaskResumeUrl: '/CALL_CENTER/progressive/task/resume/{taskId}',
    outboundTaskCancelUrl: '/CALL_CENTER/progressive/task/cancel/{taskId}',
    outboundTaskAgentViewUrl: '/CALL_CENTER/progressive/task/{taskId}/agent-view',
  },
  adapters: {
    getAuthToken: () => localStorage.getItem('token'),
    http: async ({ url, method, data, keepalive }) => {
      const res = await myRequest(url, { method, data, keepalive });
      return { success: res.success, message: res.message, data: res.data };
    },
    getAgentId: () => currentUser.id,
    getSeatConfig: async (agentId) => ({
      agentId: String(agentId),
      account: '1001',
      password: 'sip-password',
    }),
    isProduction: () => process.env.NODE_ENV === 'production',
  },
  media: {
    turn: {
      hosts: ['coturn.example'],
      username: 'turn-user',
      credential: 'turn-cred',
    },
  },
  runtime: 'tab-local',
});

await phone.connect();

const result = await phone.dial({
  dialPayload: { phoneNumber: '628123456789', orderId: 'opaque-host-id' },
  displayName: 'Customer A',
  remoteNumber: '0812****789',
  uiContext: { contactRole: 'SELF' },
});

phone.subscribe((ev) => {
  if (ev.type === 'call') console.log(ev.call?.phase);
  if (ev.type === 'outbound-task') console.log(ev.task?.status);
  if (ev.type === 'origin-lock') console.log('originBusy', ev.busy);
});

phone.setMuted(true);
await phone.hangup();
await phone.destroy();

dialPayload / uiContext 对 SDK 不透明。宿主解析 result.http.data

evaluateHttpEnvelope:2xx 仍看业务 success / code。非 2xx 先取信封 messagesuccess === falsecode !== 200,且 message 非空);否则 toast 响应体 trim(最长 HTTP_ERROR_BODY_TOAST_MAX 300 字 + );空 body 才 HTTP {status}。该 message 写入 notify.message。agent-view 单次失败不 toast。

点呼

  • isReadyForOutbound() 时先再踢一轮 REGISTER;仍失败返回 not_connected,不发 HTTP。registered 字符串不够。
  • 挂断许可:收到 CUSTOMER_CHANNEL_PROGRESS_MEDIA 后默认 500ms 可挂(allowHangupAfterProgressMediaMs);后端一直不放行则默认 10s 强制放行(forceAllowHangupMs)。
  • HangupSourcetransport-down(显式 hangup 仍可用)。控制面或 SIP 断开且本地仍在 dialing|ringing|active|ending内部挂断、相位不变、 notify;只 console.log。不自动 pause / resume 渐进、不自动重拨。
  • 本标签已在 dialing|ringing|active|ending,或本标签渐进任务 running|paused,或本标签 reserve-only hold:already_in_call(不发 notify)。
  • 同 origin 其他标签已占用(live SIP、渐进 running、reserve-only hold、或预测就绪):origin_busy,不发拨号 HTTP。
  • 两把锁:origin 锁 = 活 SIP 或任务 running 或 reserve-only hold 或 predictiveReady(跨标签)。本标签再加弹窗锁:paused 不占 origin 锁,但本标签 dial() 仍拒绝。完成倒计时不占 origin 锁(壳层禁按钮)。就绪占用 reason 复用 'task',不要加 'predictive'
  • 锁按 浏览器 origin,不按分机。催收标签与电销标签(两个分机)也不能同时打。
  • 实现:BroadcastChannel + localStorage 心跳(TTL 约 8s),崩溃后超时释放。
  • UI 用 phone.isOriginBusy() 和事件 origin-lockOutboundCallButton 再叠加本标签弹窗锁。

渐进式任务

宿主注入 outboundTaskCreateUrl(与 manualDialUrl 同类,全路径)和可选 adapters.parseCreatedTaskId(工厂一次注入,只收 http.data)。确定时:createOutboundTask({ payload }) 先占 origin 锁再 POST 不透明 payload;只判信封成败,不读 Hive 字段。有钩子则调宿主解析:有 id 则内部 startOutboundTask,解不出 / 抛错 / 挂不上则放锁并 notify outbound_task_failed。无钩子则不 attach、reserved 仍占着,交回 http

startOutboundTask({ taskId }) 仍是纯挂载。本标签活 SIP 或已有 running|paused 任务时直接 return,不覆盖。他标签占 origin 锁则 origin_busy。本标签 reserved hold 时允许 attach;attach 后 running 接管占用并清 reserved。

  • pause / resume / cancel:POST 宿主注入的 {taskId} URL 模板。pause / cancel / hangup 不是占坑请求,不要先 hold。pause HTTP 成功后:ui.phase === 'dialing' 保留(当前腿还在拨);waiting_next 清掉以免倒计时下一通。resume 仍清 ui 并立刻 poll。
  • resumeOutboundTask:本标签活 SIP 或 origin 忙(含他标签点呼)时发 origin_busy(现有 originBusy 文案),不发 resume HTTP。空闲则先 reserved hold 再 POST;HTTP 失败释放 reserved。
  • agent-view:GET,query extNo + SIP REGISTER Call-IDgetRegisteredCallId())。定时是上一发结束后再等 3s(尾间隔),不是墙钟 setIntervalrunning 持续拉,直到任务终态。SIP active 不再一律停:未收到 BRIDGING 且 hangup id 或 uiContext 仍空时(needsLiveReconcile)最多再拉 10 次(AGENT_VIEW_LIVE_RECONCILE_MAX)。pause HTTP 成功后 paused 最多再拉 60s(AGENT_VIEW_PAUSED_POLL_MAX_MS)。单次 poll 失败不 toast、仍续下一发定时。挂断后 1s 补拉一次;resume 立刻拉一次;二者不重置 3s。未成功暂停的 paused(无本地 timestamp)不靠 3s 定时续命。
  • DIALING 的 agent-view 可带顶层 callId + 不透明 businessData(不写进 ui,也不拷进 uiContext)。WAITING_NEXT / IDLE 不应带上一通。
  • BRIDGING 是 hangup / uiContext 的权威:写入后立 bridgingBound。agent-view 只填空;绑定后不再覆盖。SIP 刚进 ringing / active 且仍需对账时立刻 GET 一次。
  • WS:PROGRESSIVE_CALL_CUSTOMER_AGENT_BRIDGINGbusinessData → 不透明 uiContext)、PROGRESSIVE_TASK_PROGRESSPROGRESSIVE_TASK_AGENT_VIEWPROGRESSIVE_TASK_COMPLETED
  • 到期 EXPIRED:无 live SIP 则结束任务;通话中不挂断,挂断后再退出。
  • 关页 / destroy 必须 cancel 任务(含 paused)。同一分机多标签都会 REGISTER,后端用 REGISTER Call-ID 绑标签;不 cancel 则无法再开新任务。
  • pagehide 一律 ua.unregister()(点呼和渐进同样生效,不是预测式专用)。hangup + cancel 渐进后再 void ua.unregister()beforeunload 只做离开确认:活通话 hasOutboundTask predictiveReady。bfcache 回前台靠现成 kick REGISTER,不要另写一套恢复。

预测式

control WS 会订阅 PREDICTIVE_*PREDICTIVE_CALL_CUSTOMER_AGENT_BRIDGING 与渐进 BRIDGING 同一条路径:绑 callId + 不透明 businessDataapplyProgressiveBridging),立 bridgingBoundPREDICTIVE_TASK_COMPLETED → SoftphoneEvent { type: 'predictive-task'; taskId; taskStatus }PREDICTIVE_AGENT_OCCUPANCY{ type: 'predictive-occupancy'; occupancy; wrapUpRemainingSeconds?; availability? }不要把 COMPLETED 挂进 OutboundTask;宿主用 predictive-task 立刻再 GET view,禁止把事件名当签出扳机。

坐席签入预测FS 用 switchSipEndpoints + setPredictiveReady + PredictiveCheckInLock(headless 编排在 webrtc-react createPredictiveSipSession):

  • 当前 SIP 等于预测媒体时,无 phoneNumberdial / 创建渐进 / resume → predictive_sip_active(必须先于 origin 锁)。带 phoneNumber 的 ops simulate 放行。
  • 就绪占 origin 锁(reason 'task');暂停且本页无坐席 SIP 则放锁。预测任务拨客户不占这个坐席。
  • 签入标签锁按 extNo,暂停不释放。不要因 Redis checkedIn 拒 enter。
  • pagehide 三种模式都 unregister。pagehide / leave 另发 keepalive HTTP checkout,不拆控制面、不 cancel 预测任务。

多标签

每个 createSoftphone() + connect() 独立 REGISTER(runtime: 'tab-local')。FreeSWITCH 允许多注册。点呼互斥见上文 origin 锁。未来 shared runtime 可共享一路 REGISTER,不改 dial / hangup 签名。

公开 API

| 方法 | 作用 | |------|------| | createSoftphone(config) | 工厂,非全局单例 | | connect() | 坐席 + SIP + control WS | | dial(...) | 点呼 | | patchCallUiContext | 替换当前通话不透明 UI 元数据 | | hangup / setMuted / unlockAudio | 挂断 / 静音 / 手势解锁铃声 | | subscribe | registration / call / outbound-task / origin-lock / predictive-task / predictive-occupancy / notify | | createOutboundTask | 占锁后 POST 创建 URL;有 parseCreatedTaskId 则内部挂载 | | startOutboundTask / pause / resume / cancel | 渐进挂载 / 控制 | | hasActiveCall | 本标签 live SIP | | isOriginBusy | 本 origin 任一标签占用(活 SIP / 任务 running / reserved / 预测就绪) | | tryHoldOriginLock / releaseOriginLock | 占用原语(create/resume 内部用);有 parseCreatedTaskId 时库在解不出 / 挂不上时自己 release;reserved 时本标签 dial() 拒绝,startOutboundTask 仍可 attach | | setPredictiveReady | 预测就绪位;内部 syncOriginLock。不要只 tryHold | | getSipMedia / switchSipEndpoints | 当前 SIP 媒体;迁册只改 sip 域名,不改 controlWsUrl,不 cleanSDK。旧域 UNREGISTER 后再重建 JsSIP。重建前摘旧 UA 监听。迁册超时不改端点;rollback 传 continueAfterUnregisterTimeout 仍切回默认并 REGISTER | | isReadyForOutbound | 本连接控制面 session + SIP 通 + REGISTER 200(不含 SIP 下行 15s 新鲜度) | | getRegisteredCallId | SIP REGISTER Call-ID | | setHangupCallId / getHangupCallId | 平台通话 id(挂断 HTTP)。getter 只读内存;未绑定或已清为 null。不要把该 id 写进 uiContext | | destroy | 清理;会 hangup + cancel 任务 |

ICE / WebRTC PC failed 保持当前通话相位(不打成 failed,不放 origin 锁),notify media_failed(英文默认 Media failed, please hang up)。真正挂断 / destroy / TTL 才释放。普通 SIP session failed 仍走 failed 并放锁。

点呼在拨号 HTTP 成功后播本地 ringbackUrl,直到运营商 183 或客户接听 / 挂断。坐席自动接听的 agent INVITE 不是 停铃条件。

connect()getSeatConfig,有分机才 SIP REGISTER。waitForWsSessionId 超时必须 reject connect(),失败要 cleanSDK,避免死 promise / 双 UA。控制面断开必须作废旧 wsSessionIdWebSocket.OPEN 不是活着:online 直接换控制面 socket;pageshow / 回前台在网络可用且(长休眠则先 settle 1500ms)后发 PING 探活 2s,任意下行则不拆,零下行且在线才 forceReplaceclose() 后 1s 无 onclose 必须丢掉旧引用再建。服务端 30s HEARTBEAT 保留,客户端 40s watchdog,PONG/HEARTBEAT 都 reset。已注册后传输断开 → connecting(不是 idle),在当前 UA 内再 REGISTER;connect() 不得为恢复新建 UA。新 wsSessionId 或 SIP connected 立刻 REGISTER,不要 sleep;SIP 断开取消 in-flight 并清 isRegistered / sipRegisterPendingua.register() 前再检运输层。恢复 REGISTER:registered 事件或「已注册 + 本轮 SIP 200」即成功。恢复时 2s 无 SIP 下行则拆 SIP WS;运输标 up 但下行过期 先拆。connect() 等 transport 离开 READY(JsSIP disconnect() 不会自己再连)。kick 时 SIP 未 up 必须先拉起 SIP WS;点击路径已 isReadyForOutbound() 不要再置 needsRegister。外呼就绪看 isReadyForOutbound()(控制面 session + SIP 通 + REGISTER 200),不能只看 registered,也不因 SIP 静默 15s 判未就绪。回前台:未就绪才 kick;已就绪须隐藏 ≥30s 且 SIP 下行过期才探活 REGISTER。短切前台不 kick。不要每次回前台拆 SIP。同 wsSessionId 且 Contact 已带当前值不要再 ua.register()pageshow / 标签可见 / online 先确认控制面活,400ms debounce,不 pause 任务。控制面 CONNECTING 超时从 connect() 起算。两端 ready 仍失败满 5 次 → failed + 15s 慢重试(不停)。外呼时若未就绪再踢一轮,失败则不发 HTTP。活通话传输断开 → 不 hangup、不 notify,只 console.log,相位不变,不自动 pause 渐进。首次 REGISTER 401 是 Digest 握手,JsSIP 内部重发,registrationFailednotify.code 等于 SoftphoneError.code。connect 进行中的 REGISTER_FAILED 不把 registration 打成 failed。

渐进 agent-view 必须带 taskId 才改当前任务;cancelled 会清本地任务。关页 pagehide(含 bfcache persisted)一律 hangup + cancel 渐进 + ua.unregister()。任务 completed 不 hangup 仍在通话的 SIP。

构建 / 测试

npm test

只克隆本仓即可。不要在 scripts/run-tests.mjs 里读 webrtc-react / ap-admin

从 0.3 迁移

| 0.3 | 0.8 | |-----|-----| | createWebRTCKit | createSoftphone | | getReviewerId | getAgentId | | startOutboundCall | phone.dial | | hold / unhold | setMuted |