前言
94ai软电话前端SDK,框架无关(支持vue,react,angular等)
项目参考demo: https://gitee.com/softphone-demo(有权限问题,具体找94智能开发同事拉入团队组织即可)
本地调试:
yarn add sip.js@portal:../SIP修改v2.js
安装
94智能sdk目前暂不在公网公布sdk源码,如有需要,可以联系94智能开发同事加入团队,具体步骤:
获取加入团队邀请链接
-
如果你是主账号,直接登录即可:

-
如果你是RAM子账号,选择下面RAM用户登录:

进入RAM 用户登录页登录即可:

阿里的数字安全码
登录过程会提示你填写 验证虚拟MFA设备数字安全码
1.如果以前使用过阿里云产品并绑定过
可以直接在【阿里云app】或使用【虚拟MFA验证小程序】获取【数字安全码】填写即可,如下:



2.如果以前没有绑定过
3.如果使用小程序
在微信搜索mfa二次验证如下,具体可以百度下:

4.设置昵称申请加入
登录成功后会提示你加入团队,设置号昵称点加入


查看个人账号信息
等待审核通过后,刷新页面,点击进入企业

1.选择角色
选择作为研发者,开始工作

2.进入制品仓库
在工作台选择【制品仓库】进入

3.查看账号密码
选择设置查看个人账号信息


设置私库源
1.新建rc文件
在需要使用94智能sdk的项目根目录,新建一个rc文件:
- 如果你使用的npm或pnpm,新建.npmrc,内容如下:
@94ai:registry=https://packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/
//packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:username=username
//packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:_password=password
//packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:always-auth=true
- 如果你使用的yarn1,新建.yarnrc,内容如下:
"@94ai:registry" "https://packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/"
"//packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:username" "username"
"//packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:_password" "password"
"//packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:always-auth" true
- 如果你使用的yarn2~3,新建.yarnrc.yml,内容如下:
enableImmutableInstalls: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.1.1.cjs
#npmRegistryServer: "https://nexusdev.k8s.94ai.pro/repository/npm-group/"
npmScopes:
94ai:
npmRegistryServer: "https://packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/"
npmAlwaysAuth: true
npmAuthIdent: "username:password"
#unsafeHttpWhitelist:
# - 192.168.20.9
2.配置个人账号信息

- 如果你使用的是yarn2~3,则把你的账号密码直接替换掉对应rc文件
password
3.安装依赖
之后该项目只要是@94ai域的私包都会经过94智能私服抓取
其他包【包括@94ai域私包的子包(非@94ai域)】都会经由个人电脑配置的npm包registry源抓取
执行install命令即可成功安装@94ai域的所有私包,包括sdk,如
$ yarn add @94ai/softphone
# or
$ npm i @94ai/softphone
# or
$ pnpm add @94ai/softphone
使用
初始化
1.作为NPM包使用
import { UserAgent, UserAgentManager, URI } from '@94ai/softphone'
import FingerprintJS from '@fingerprintjs/fingerprintjs'
import useToast from '@/utils/useToast'
export function createRandomToken(size, base = 32) {
let token = "";
for (let i = 0; i < size; i++) {
const r = Math.floor(Math.random() * base);
token += r.toString(base);
}
return token;
}
export interface TransportOptions {
server?: string
wsServers?: string | string[] | {
ws_uri: string,
weight: number
}[]
connectionTimeout?: number
maxReconnectionAttempts?: number
reconnectionTimeout?: number
keepAliveInterval?: number
keepAliveDebounce?: number
traceSip?: boolean
}
export interface UserAgentOptions {
authorizationPassword: string,
authorizationUsername: string,
viaHost?: string,
uri: URI,
logLevel?: 'debug' | 'log' | 'warn' | 'error',
contactName?: string
noAnswerTimeout?: number,
transportOptions: TransportOptions,
reconnectionDelay?: number,
reconnectionInterval?: number,
reconnectionAttempts?: number,
registerInterval?: number,
registerAttempts?: number
optionsPingInterval?: number
optionsPingAttempts?: number
sipHeaders?: Array<string>;
}
export class SoftphoneManager {
static #getDeviceId: () => Promise<string>
#userAgentManager: InstanceType<typeof UserAgentManager>
static {
SoftphoneManager.#getDeviceId = (() => {
let visitorId = ''
return async (): Promise<string> => {
if (visitorId) return visitorId
const fp = await FingerprintJS.load()
const result = await fp.get()
visitorId = result.visitorId
return visitorId
}
})()
}
public async getUserAgentManager = () => {
if (!this.#userAgentManager) {
this.#userAgentManager = UserAgentFactory.getUserAgentManager(await this.initSeatsInfo())
}
return this.#userAgentManager
}
public getUserAgentManagerSync = (config?: UserAgentOptions) => {
if (!this.#userAgentManager) {
this.#userAgentManager = UserAgentFactory.getUserAgentManager(config)
}
return this.#userAgentManager
}
public async initSeatsInfo = (): Promise<UserAgentOptions> => {
const result = await getSeatsInfo()
if (result.code === 200) {
const {
extPassword,
extensionNumber,
wsRegisterAddress,
wsProtocol
} = result.data
return {
authorizationPassword: extPassword,
authorizationUsername: extensionNumber,
viaHost: `${await SoftphoneManager.#getDeviceId()}.sip`,
uri: UserAgent.makeURI(`sip:${extensionNumber}@${wsRegisterAddress}`),
logLevel: 'error',
transportOptions: {
server: `${wsProtocol}://${wsRegisterAddress}`
},
contactName: extensionNumber
}
}
useToast.showToast('获取分机信息失败')
throw new Error('获取分机信息失败')
}
public connect = async () => {
try {
await this.getUserAgentManagerSync().prepareUserAgent(
{
refresh: (path: keyof typeof userAgentStatusDefault, value: boolean) => {
}
},
{
onInvite: async (invitation: Invitation) => {
}
})
} catch (e) {
const errorInfo = e as Error
if (errorInfo.message === 'sip register fail with code 503' ||
errorInfo.message.indexOf('WebSocket closed') > -1 && errorInfo.message.indexOf('code: 1006') > -1) {
useToast.showToast('软电话服务器异常,签入失败')
} else {
useToast.showToast(errorInfo.message)
}
throw e
}
}
}
export default new SoftphoneManager()
import commonSoftphone from './softphone.ts'
onBeforeMount(async () => {
await commonSoftphone.getUserAgentManager()
})
import commonSoftphone from './softphone.ts'
const connect = async () => {
await commonSoftphone.connect()
}
2.直接在JavaScript中使用
- 确保安装了node,随手新建一个目录,打开cmd或bash。
- 设置@4ai域账号授权,依次执行执行如下,其中
:username=xxxx替换你的账号名字,:_password=xxxx替换你的账号密码在做base64加密后的结果
npm config set @94ai:registry=https://packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/
npm config set //packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:username=xxx
npm config set //packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:_password=xxx
npm config set //packages.aliyun.com/644f755a97d94d909e43534c/npm/npm-registry/:always-auth=true
- 最后执行
npx -y @94ai/softphone -- sdk-umd,之后在当前执行命令路径下即可获取到softphone.umd.min.js
- 在index.html引入
softphone.umd.min.js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<title>softphone</title>
<style>
#softphone-app {
text-align: center;
padding-top: 50px;
}
</style>
</head>
<body>
<div id="softphone-app">
<button id="connectToServer">连接服务器</button>
<br>
<br>
<button id="disconnect" disabled>断开连接服务器</button>
<br>
<br>
<button id="ignore" disabled>忽略</button>
<br>
<br>
<button id="answer" disabled>接听</button>
<br>
<br>
<button id="hangUp" disabled>挂断</button>
<br>
<br>
<button id="press" disabled>按*转人工</button>
<br>
<br>
<button id="localVoiceAccess" disabled>本地声音接入</button>
<br>
<br>
<button id="remoteSoundAccess" disabled>远程声音接入</button>
<br>
<br>
<button id="localSoundDisconnection" disabled>本地声音断开</button>
<br>
<br>
<button id="remoteSoundDisconnection" disabled>远程声音断开</button>
<br>
<br>
<audio
id="remoteAudio"
controls
style="display:none"
>
<p>Your browser doesn't support HTML5 audio - remoteAudio</p>
</audio>
<audio
id="localAudio"
style="display:none"
src="./antique_phone.mp3"
controls
loop
>
<p>Your browser doesn't support HTML5 audio - localAudio</p>
</audio>
</div>
<script src="../lib/softphone.umd.min.js"></script>
<script src="./fp.umd.min.js"></script>
<script>
const getDeviceId = (() => {
let visitorId = ''
return async () => {
if (visitorId) return visitorId
const fp = await FingerprintJS.load()
const result = await fp.get()
visitorId = result.visitorId
return visitorId
}
})()
document.addEventListener('DOMContentLoaded', async (event) => {
const phoneRing = './antique_phone.mp3'
const ignore = document.getElementById('ignore')
const answer = document.getElementById('answer')
const hangUp = document.getElementById('hangUp')
const connectToServer = document.getElementById('connectToServer')
const disconnect = document.getElementById('disconnect')
const press = document.getElementById('press')
const localVoiceAccess = document.getElementById('localVoiceAccess')
const remoteSoundAccess = document.getElementById('remoteSoundAccess')
const localSoundDisconnection = document.getElementById('localSoundDisconnection')
const remoteSoundDisconnection = document.getElementById('remoteSoundDisconnection')
const localAudio = document.getElementById('localAudio')
const remoteAudio = document.getElementById('remoteAudio')
const viaHost = `${await getDeviceId()}.sip`
const extensionNumber = '9288'
const extPassword = 'zjh13542240708'
const wsProtocol = 'wss'
const wsRegisterAddress = 's28.94ai.com:7443'
const sipServerHost = `sip:${extensionNumber}@${wsRegisterAddress}`
const wsAddress = `${wsProtocol}://${wsRegisterAddress}`
const request = {
post(url) {
console.log('写你的调用的openapi改坐席状态,要写好它')
}
}
const editSeats = async (params) => {
return request.post('/seats/editLineStatus')
}
await editSeats({ lineStatus: 1 })
const {
UserAgentFactory,
UserAgent,
playMedia,
pauseMedia,
SessionState,
getMedia
} = softphone
const phoneRings = () => {
remoteAudio.src = phoneRing
playMedia('localAudio')
}
const stopPhoneRings = () => {
remoteAudio.src = ''
pauseMedia('localAudio')
}
const userAgentManager = UserAgentFactory.getUserAgentManager({
authorizationPassword: extPassword,
authorizationUsername: extensionNumber,
viaHost,
uri: UserAgent.makeURI(sipServerHost),
logLevel: 'error',
transportOptions: {
server: wsAddress
},
contactName: extensionNumber
})
const connect = () => {
userAgentManager.prepareUserAgent(
{
refresh (path, value) {
console.log(path, value)
}
},
{
onInvite (invitation) {
syncSipMessage(invitation.incomingInviteRequest.message.headers)
phoneRings()
ignore.disabled = false
answer.disabled = false
localSoundDisconnection.disabled = false
remoteSoundDisconnection.disabled = false
invitation.stateChange.addListener((state) => {
switch (state) {
case SessionState.Initial:
break
case SessionState.Establishing:
break
case SessionState.Established:
const mediaElement1 = getMedia('remoteAudio')
mediaElement1.srcObject = userAgentManager.getStream()
mediaElement1.play()
hangUp.disabled = false
break
case SessionState.Terminating:
case SessionState.Terminated:
ignore.disabled = true
answer.disabled = true
hangUp.disabled = true
localSoundDisconnection.disabled = true
remoteSoundDisconnection.disabled = true
localVoiceAccess.disabled = true
remoteSoundAccess.disabled = true
const mediaElement2 = getMedia('remoteAudio')
mediaElement2.srcObject = null
mediaElement2.pause()
stopPhoneRings()
break
default:
throw new Error('Unknown session state.')
}
})
}
}
)
disconnect.disabled = false
connectToServer.disabled = true
ignore.disabled = true
answer.disabled = true
hangUp.disabled = true
localSoundDisconnection.disabled = true
remoteSoundDisconnection.disabled = true
localVoiceAccess.disabled = true
remoteSoundAccess.disabled = true
}
const disConnect = () => {
userAgentManager.dispose()
connectToServer.disabled = false
disconnect.disabled = true
ignore.disabled = true
answer.disabled = true
hangUp.disabled = true
localSoundDisconnection.disabled = true
remoteSoundDisconnection.disabled = true
localVoiceAccess.disabled = true
remoteSoundAccess.disabled = true
}
let monitorFirstAnswerAfter = true
const syncSipMessage = (headers) => {
monitorFirstAnswerAfter = !(String(headers['X-Aftertransferlabour'] && headers['X-Aftertransferlabour'][0].raw) === '1')
}
const sendStarDtmf = () => {
if (monitorFirstAnswerAfter) {
userAgentManager.sendStarDtmf()
press.disabled = true
}
}
const hangUpInvite = () => {
stopPhoneRings()
userAgentManager.hangUpInvite()
ignore.disabled = true
answer.disabled = true
hangUp.disabled = true
localSoundDisconnection.disabled = true
remoteSoundDisconnection.disabled = true
localVoiceAccess.disabled = true
remoteSoundAccess.disabled = true
}
const ignoreInvite = () => {
stopPhoneRings()
userAgentManager.ignoreInvite()
ignore.disabled = true
answer.disabled = true
hangUp.disabled = true
localSoundDisconnection.disabled = true
remoteSoundDisconnection.disabled = true
localVoiceAccess.disabled = true
remoteSoundAccess.disabled = true
}
const acceptInvite = () => {
stopPhoneRings()
userAgentManager.acceptInvite()
if (monitorFirstAnswerAfter) {
press.disabled = false
}
hangUp.disabled = false
answer.disabled = true
ignore.disabled = true
}
const unMuteLocalAudio = () => {
localVoiceAccess.disabled = true
localSoundDisconnection.disabled = false
userAgentManager.unMuteLocalAudio()
}
const muteLocalAudio = () => {
remoteSoundAccess.disabled = true
remoteSoundDisconnection.disabled = false
userAgentManager.muteLocalAudio()
}
const unMuteRemoteAudio = () => {
localSoundDisconnection.disabled = true
localVoiceAccess.disabled = false
userAgentManager.unMuteRemoteAudio()
}
const muteRemoteAudio = () => {
remoteSoundDisconnection.disabled = true
remoteSoundAccess.disabled = false
userAgentManager.muteRemoteAudio()
}
ignore.addEventListener('click', ignoreInvite)
answer.addEventListener('click', acceptInvite)
hangUp.addEventListener('click', hangUpInvite)
connectToServer.addEventListener('click', connect)
disconnect.addEventListener('click', disConnect)
press.addEventListener('click', sendStarDtmf)
localVoiceAccess.addEventListener('click', unMuteLocalAudio)
remoteSoundAccess.addEventListener('click', muteLocalAudio)
localSoundDisconnection.addEventListener('click', unMuteRemoteAudio)
remoteSoundDisconnection.addEventListener('click', muteRemoteAudio)
})
</script>
</body>
</html>
签入
import { playMedia, getMedia, SessionState } from '@94ai/softphone'
import { ref } from 'vue'
import popNotice from './usePopNotice.ts'
const userAgentStatus = ref({
connectStatus: false,
registerStatus: false,
invitatingStatus: false,
incomingStatus: false,
answerStatus: false,
reconnectStatus: false,
})
userAgentManager.prepareUserAgent(
{
refresh(path, value) {
userAgentStatus[path] = value
}
},
{
onInvite(invitation) {
playMedia('localAudio')
popNotice.popNotification('来电了')
invitation.stateChange.addListener((state) => {
switch (state) {
case SessionState.Initial:
break
case SessionState.Establishing:
break
case SessionState.Established:
const mediaElement1 = getMedia('remoteAudio')
mediaElement1.srcObject = userAgentManager.getStream()
mediaElement1.play()
break
case SessionState.Terminating:
case SessionState.Terminated:
const mediaElement2 = getMedia('remoteAudio')
mediaElement2.srcObject = null
mediaElement2.pause()
break
default:
throw new Error('Unknown session state.')
}
})
}
})
接听
import { refreshShowTime, pauseMedia } from '@94ai/softphone'
const acceptInvite = () => {
refreshShowTime()
pauseMedia('localAudio')
userAgentManager.acceptInvite()
}
忽略
import { pauseMedia } from '@94ai/softphone'
const ignoreInvite = () => {
pauseMedia('localAudio')
userAgentManager.ignoreInvite()
}
挂断
import { refreshShowTime } from '@94ai/softphone'
const hangUpInvite = () => {
refreshShowTime()
userAgentManager.hangUpInvite()
}
按*号转人工
import { refreshShowTime } from '@94ai/softphone'
const sendStarDtmf = () => {
refreshShowTime()
userAgentManager.sendStarDtmf()
}
销毁
import { refreshShowTime } from '@94ai/softphone'
const disconnect = () => {
refreshShowTime()
userAgentManager.dispose()
}
传输本地声音到远端
const unMuteLocalAudio = () => {
userAgentManager.unMuteLocalAudio()
}
不传输本地声音到远端
const muteLocalAudio = () => {
userAgentManager.muteLocalAudio()
}
接听远端声音
const unMuteRemoteAudio = () => {
userAgentManager.unMuteRemoteAudio()
}
不接听远端声音
const muteRemoteAudio = () => {
userAgentManager.muteRemoteAudio()
}
获取webrtc基础api
const senders = userAgentManager.getSenders()
const receivers = userAgentManager.getReceivers()
const peerConnection = userAgentManager.getPeerConnection()
获取代理用户相关sip协议通讯实例
const userAgent = userAgentManager.getUserAgent()
const sessionDescriptionHandler = userAgentManager.getSessionDescriptionHandler()
const currentInvitation = userAgentManager.getCurrentInvitation()
const currentInviter = userAgentManager.getCurrentInviter()
const localMixedMediaStream = userAgentManager.getStream()
查看代理用户目前状态
interface userAgentStatus {
connectStatus: boolean,
registerStatus: boolean,
invitatingStatus: boolean,
incomingStatus: boolean,
answerStatus: boolean,
reconnectStatus: boolean,
}
const userAgentStatus: userAgentStatus = userAgentManager.getUserAgentStatue()
软电话状态可以根据企业特定业务流程定制扩展额外的状态,如94智能决策系统 额外扩展了 【监听中】,【整理中】,【小休中】等状态