@yinshan-tech/webrtc-react
v0.8.6
Published
React UI for @yinshan-tech/webrtc-sdk Softphone: button, call widget, hooks (no antd)
Readme
@yinshan-tech/webrtc-react
@yinshan-tech/webrtc-sdk 的 React UI(当前 0.8.6)。无 antd。 催收逾期、电销案件仍由宿主注入。电话壳文案(中/英/印尼)在本库,宿主传 locale。后台入口是 SoftphoneHost + createCallCenterSoftphone,不要手写一套 Provider / toast / 接线。
规范见 docs/conventions.md。
安装
npm i @yinshan-tech/webrtc-react@^0.8 @yinshan-tech/webrtc-sdk@^0.8本仓 peerDependencies 要 webrtc-sdk@^0.8.9(predictive-task / predictive-occupancy SoftphoneEvent)。只克隆本仓即可 npm i / npm publish,不要把 file:../webrtc-sdk 写进对外文档。
不 bump 本仓库版本,除非明确要求。
只在既不挂 CallWidget / OutboundModeShell、也不 ensureCallWidgetHost 时才手动:
import '@yinshan-tech/webrtc-react/styles.css';表面二选一
CallWidget 与 OutboundModeShell 不要同时挂。OutboundCallButton 的 autoMountWidget 默认 false。
| 表面 | 何时用 |
|------|--------|
| OutboundModeShell | 点呼 + 渐进 / 预测工作台(ap-admin 现网) |
| CallWidget | 只要一张浮动通话卡片 |
最小用法(CallWidget)
import { createSoftphone } from '@yinshan-tech/webrtc-sdk';
import {
SoftphoneProvider,
OutboundCallButton,
CallWidget,
useSoftphoneConnect,
} from '@yinshan-tech/webrtc-react';
const phone = createSoftphone({ /* endpoints + adapters,见 webrtc-sdk README */ });
function Boot({ children }: { children: React.ReactNode }) {
useSoftphoneConnect();
return <>{children}</>;
}
<SoftphoneProvider phone={phone}>
<Boot>
<OutboundCallButton
dialPayload={{ phoneNumber: '6281...' }}
displayName="Customer"
/>
<CallWidget locale="id-ID" />
</Boot>
</SoftphoneProvider>OutboundCallButton 禁用 = origin 锁(本 origin 活 SIP 或渐进 running)或 本标签弹窗还在(running / paused / 完成倒计时)。完成倒计时不占 origin 锁。HTTP 进行中也会 busy。点呼前用 phone.isReadyForOutbound(),不要用 getRegistration() === 'registered' 跳过 connect()。connect() 抛错也走 onResult。宿主可在 start() 未完成时卸按钮(点呼后立刻关选号弹窗);库不在卸载后 setState。
后台最少对接
点呼-only 新后台:SoftphoneHost + createCallCenterSoftphone。库不建坐席、不拼媒体域名、不拉案件、不打开 tab。宿主实现 seat / media / HTTP / 案件 fetch / 开 tab。
渐进 hooks(useProgressiveSession / useProgressiveOverlay)与 OutboundModeShell 分开。壳继续只做 chrome + renderBusinessInfo。不要把 fetch 或开 tab 写进壳。
import {
SoftphoneHost,
OutboundModeShell,
createCallCenterSoftphone,
createSoftphoneHttp,
} from '@yinshan-tech/webrtc-react';
const http = createSoftphoneHttp({
getHeaders: () => ({
'x-auth-token': token() ?? '',
// 多产品宿主在这里带 x-product-code,不要写进库
}),
});
function createPhone() {
return createCallCenterSoftphone({
callCenterPrefix: '/CALL_CENTER',
media: hostMedia(), // controlWsUrl / sipDomain / sipWsUrl
manualDialUrl: '/CALL_CENTER_BIZ/outbound/manual/dial',
getAgentId: () => currentUser.id,
getSeatConfig: (agentId) => hostSeat(agentId),
getAuthToken: () => token(),
http,
});
}
<SoftphoneHost
locale="id-ID"
toast={hostToast}
enabled={seatReady} // false → 只渲染 children,不 REGISTER
createPhone={createPhone} // 单 UA;多 UA 用 rules + pathname
callSurface={
<OutboundModeShell
mode="manual"
locale="id-ID"
renderBusinessInfo={({ uiContext }) => hostCaseCard(uiContext)}
/>
}
>
{children}
</SoftphoneHost>渐进(宿主自己挂 hooks,不进壳):
import { useProgressiveSession, useProgressiveOverlay } from '@yinshan-tech/webrtc-react';
useProgressiveSession({
onCustomer: (ctx) => hostOpenTab(ctx), // 路径由宿主拼
canOpen: (ctx) => hostCanOpen(ctx),
identityKey: (ctx) => hostIdentityKey(ctx),
});
const overlay = useProgressiveOverlay({
fetch: (ctx) => hostFetchCase(ctx), // 案件 HTTP 留宿主
keyOf: (ctx) => hostOverlayKey(ctx),
});enabled === false或(有rules且无匹配)→ 只渲染 children,不建 Softphone。何时 REGISTER 是宿主产品行为。rules.match为函数或字符串前缀。库内零宿主路径字面量。createCallCenterSoftphone用callCenterPrefix拼 hangup / make-call / 渐进 pause|resume|cancel|agent-view。创建 URL 是可选的outboundTaskCreateUrl(与manualDialUrl同类,宿主全路径),不要从 prefix 拼。可选parseCreatedTaskId工厂一次注入;确定调createOutboundTask,库调用钩子挂载。不写默认 Hive 解析。现网若不调这些方法,不会打对应 URL。默认mergeDialSession = mergeTopLevelDialSession,runtime: 'tab-local'。- HTTP 头全部来自
getHeaders()。库不写死x-auth-token。 - waiting 页路径由宿主拼;库只认
isWaitingHostId(progressive-waiting或predictive-waiting)。 - 列表页注册态用
useRegistrationOrUnavailable(Host 未挂 phone 时返回'unavailable',不 throw)。外呼能否打用useSoftphoneReady/phone.isReadyForOutbound(),不要把=== 'registered'当门闩。 - 点呼互斥用
useClickCallDisabled({ checkedIn, predictiveCompleting })(origin 锁 + 任务running|paused+ 本标签完成倒计时 + 可选预测签入/15s)。未传checkedIn默认 false,不灰点呼。按钮仍由宿主画。 - 点呼成功用
bindDialHangupCallId(phone, result, parse?)。默认parseHangupCallId;ys 传入读providerCallId的 parse。催记开窗留宿主。
不要 Host、只要一张卡时,用上面的 CallWidget 最小用法。
OutboundModeShell
manual | progressive | predictive 共用 chrome。业务内容走槽位,不要把逾期金额写进本库。
<OutboundModeShell
mode="manual"
locale="id-ID"
formatPartyLine={(ctx) => String(ctx?.customerName ?? '')}
renderBusinessInfo={({ uiContext }) => (
<div>{String(uiContext?.overdueAmount ?? '')}</div>
)}
/>- 层:壳
createPortal到document.body(与CallWidget同一层)。默认--wro-modal-z为 1100,压过 antd 业务 Modal(1000)。宿主业务弹窗若更高,给壳传zIndex。onRequestCancelTask的确认框是对壳本身的决定,必须 高于壳(antdModal.confirm默认 1000 会被盖住,宿主自己设zIndex,例如 1200)。只改数字、不 portal,压不过#root里的 stacking context。 - 文案:
locale选库内包(zh-CN/en-US/id-ID)。未知 locale 与未传时用en-US,不读navigator.language。texts只覆盖单条。催收业务词不要放进本库。 - Toast:
SoftphoneConnectToast+SoftphoneNotifyBridge吃宿主toast适配器(无 antd)。connect 只报终态;lifecycle notify 忽略。点呼 / 外呼任务 / 挂断 HTTP 失败优先notify.message(后端信封或响应体原文);SDK 英文兜底(Call failed, please retry等)仍走三语 copy。客户端锁(origin_busy/already_in_call)和媒体保活失败(media_failed)只用 copy。预测签入错误predictive_tab_busy/predictive_align_timeout/predictive_session_replaced/predictive_sip_active/predictive_enter_in_flight/sip_unregister_timeout走库 copy。 - 点呼密度:宿主决定。催收可传
formatManualRemoteNumber/formatPartyLine(姓名 + 掩码号 + 角色);电销只传姓名、不传号码 formatter。 - 渐进:pause / resume / cancel、状态行、到期倒计时。
startOutboundTask后订阅outbound-task。两通之间 SIPended不拆壳。状态行无具体相位时不再写「等待接通中」(空则—)。暂停且当前腿仍dialing用pausedStillDialing(已暂停 · 当前号码仍在拨打);仅暂停、或暂停后的waiting_next,仍只「已暂停」。活 SIP(呼叫中 / 通话中)仍优先;failed不压waiting_next。未接通 interval 与已接通挂断一样,间隔 > 0 显示{n}s。案件 overlay / 开 tab 走 hooks,不要把fetch焊进壳。resumeawait phone.resumeOutboundTask(),按钮 busy;失败走 SDK notify。hasActiveCall || isOriginBusy()时灰掉(他标签点呼也禁)。宿主 Hive 创建走 SDKtryHoldOriginLock,打开选号弹窗不占锁。渐进开始不要用registered灰掉;恢复中可点,由 SDKcreateOutboundTask等就绪。 - 预测式任务壳:
mode="predictive"仅在predictiveCheckedIn时由宿主设置;签出立刻回manual。PredictiveTaskBody是真按钮(签出 / 暂停 / 就绪),不包ProgressiveTaskBody、无进度条。号码 / 姓名 /renderBusinessInfo一直画;挂断后不读stash.predictive,值走默认-(非活相位不把 endedcall传给 formatter,避免号码回落)。签入后默认画静音 / 挂断(与渐进同套callControls,未允许时禁用),不要闸成「有通话才渲染」。静音仅phase==='active'(canMuteCall)。挂断启用canHangup || phase==='active'(canHangupPredictiveCall),未桥接 / 振铃禁用,不要套渐进的ringing可挂。状态行走resolvePredictiveStatusLine(15s → 活 SIP → WRAPUP TTL → 暂停空闲 → 五句就绪空闲 → 对齐中);空闲不得画—。wrap-up 倒计时是截止时刻,迟到 view / occupancy 不得把剩余秒数调大。X / 签出走onRequestCancelTask(宿主确认后再session.leave());15s 到期走onPredictiveCheckout(直接leave())。不要cancelOutboundTask。
槽位
| 槽 | 用途 |
|----|------|
| renderBusinessInfo | 宿主案件卡片(逾期等) |
| formatPartyLine / formatManualPartyLine / formatProgressivePartyLine | 对方身份行 |
| formatManualRemoteNumber | 点呼号码展示 |
| onRequestCancelTask | 渐进:宿主确认后再 cancelOutboundTask。预测:X / 签出确认后再 session.leave() |
| onPredictiveCheckout / onPredictivePause / onPredictiveReady | 预测:15s 到期直接 leave() / 暂停 / 就绪 |
| onBlockedDismiss | 通话中点关闭 |
导出
Chrome: SoftphoneProvider、OutboundCallButton、CallWidget、OutboundModeShell、useSoftphoneConnect、useCallSnapshot、useRegistration、useRegistrationOrUnavailable、useSoftphoneReady、useSoftphoneNotify、ensureCallWidgetHost、resolveSoftphoneTexts、resolveNotifyMessage、SoftphoneConnectToast、SoftphoneNotifyBridge。
接线: SoftphoneHost、createCallCenterSoftphone、createSoftphoneHttp、callCenterTaskEndpoints、mergeTopLevelDialSession、parseHangupCallId、bindDialHangupCallId、resolveSoftphoneAssets、shouldDisableClickCall、shouldDisableProgressiveStart、shouldDisableProgressiveResume、useClickCallDisabled。
渐进槽(hooks,不是壳): flattenUiContext、useProgressiveSession、useProgressiveOverlay、isWaitingHostId、waitingHostKind。
预测式签入: createPredictiveSipSession、resolvePredictiveStatusLine、shouldStartPredictiveTerminalCountdown、PREDICTIVE_WAITING_CUSTOMER_ID。
const session = createPredictiveSipSession({
softphone: phone,
defaultMedia: hostMedia(), // A/B,createSoftphone 默认仍连这套
predictiveMedia: hostPredictive(), // C;不得与默认 SIP LB 相同
http,
statusUrl: `${prefix}/predictive/agent/status`,
pauseUrl: `${prefix}/predictive/agent/pause`,
readyUrl: `${prefix}/predictive/agent/ready`,
checkoutUrl: `${prefix}/predictive/agent/checkout`,
getExtNo: () => seat.account,
getExtNoCallId: () => phone.getRegisteredCallId(),
});
await session.enter(); // 不因 Redis checkedIn 拒绝;本标签有渐进则不切 C。ok 才算预测 REGISTER + Redis 对齐
await session.pause(); // 只改 AVAIL;SIP 留 C;预测锁不放。通话中可暂停
await session.ready(); // 本标签活 SIP 允许就绪;他标签 origin 忙或本标签渐进则拒
await session.leave(); // hasActiveCall → already_in_call;否则 void POST checkout(keepalive,不等、不读 body)立刻 rollbackToDefault + release
session.dispose(); // Host 卸载必须调用:release 锁;仍签入则同样 fire-and-forget checkoutenter() 迁册只换 SIP(默认域 UNREGISTER → 预测域 REGISTER),不拆控制面 WS。失败(unregister 超时 / REG 失败 / align 超时)rollback 回默认 FS,宿主不得进等待页。已签入后的 SIP/控制面断线走现有保活(当前预测域),只有 leave() 才切回默认。idempotent 只表示本标签已持锁,不是签入成功。
leave() / pagehide 禁止 if (!httpOk) return。失败不 toast。签出后宿主留在当前 URL,不要 history.replace。pagehide 只在 enter 成功后绑定。
后台对接:createCallCenterSoftphone({ predictiveMedia }) 会挂 session + view poll。列表用 usePredictiveSnapshot().enter(),成功后由宿主 history.push 等待页。壳传 bindPredictive:pause / ready / 15s leave / modeSession 由库填。签入中全屏 wro-aligning-mask 挡点击。定时 GET /predictive/agent/view 是上一发(成功或失败)结束后再等 3s,不要墙钟 setInterval;签入 / 回前台 / 任务事件 / 振铃接通对账 / 挂断后 1s 仍立刻拉。
预测 FS 唯一入口是宿主手动 enter()。库不因等待页 URL / F5 / registered 自动 enter。库不写产品路由,不开催收详情 tab,不弹催记。
rollbackToDefault 传 continueAfterUnregisterTimeout:迁册超时不改端点;回默认超时仍切回并 REGISTER。
两把锁:PredictiveCheckInLock(同 origin + 同分机,enter→leave 一直持有,暂停不放)与 origin 锁(就绪占、暂停且无坐席 SIP 则放,reason 仍是 'task')。他标签渐进 paused 且无活 SIP 时允许 enter;他标签活 SIP(即使渐进已暂停)拒绝。第二页再 enter → predictive_tab_busy。
