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

@94ai/softphone

v5.0.12

Published

94 Intelligent Technology Co., Ltd softphone SDK

Readme

前言

94ai软电话前端SDK,框架无关(支持vue,react,angular等)

项目参考demo: https://gitee.com/softphone-demo(有权限问题,具体找94智能开发同事拉入团队组织即可)

本地调试: yarn add sip.js@portal:../SIP修改v2.js

安装

94智能sdk目前暂不在公网公布sdk源码,如有需要,可以联系94智能开发同事加入团队,具体步骤:

获取加入团队邀请链接

  1. 向94智能开发同事申请到【加入到团队的连接】,如: https://account-devops.aliyun.com/account/invite?sign=ab965d478dbed5dad3cc145a5a5b406c&next_url=https%3A%2F%2Fpackages.aliyun.com%3ForgId%3D644f755a97d94d909e43534c

  2. 进入上述链接后,进到阿里云登录页

  • 如果你是主账号,直接登录即可:

    img.png

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

    img_1.png

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

    img_2.png

阿里的数字安全码

登录过程会提示你填写 验证虚拟MFA设备数字安全码

1.如果以前使用过阿里云产品并绑定过

可以直接在【阿里云app】或使用【虚拟MFA验证小程序】获取【数字安全码】填写即可,如下:

  • 下载阿里云app

1.jpg

  • 定位首页的mfa图标

2.jpg

  • 找到对应账号的数字安全码

3.jpg

2.如果以前没有绑定过

  • 如下面选择 虚拟 MFA 设备

    img_5.png

  • 然后在手机安装【阿里云app】或使用【小程序虚拟MFA验证】,然后扫码绑定设备

    img_6.png

  • 在设备添加账号点确定

    img_7.png

  • 之后会每隔一小段时间会刷新获取到最新的【数字安全码】

    img_18.png

3.如果使用小程序

在微信搜索mfa二次验证如下,具体可以百度下:

20230501-205150.jpg

4.设置昵称申请加入

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

img_4.png

img_7.png

查看个人账号信息

等待审核通过后,刷新页面,点击进入企业

img_8.png

1.选择角色

选择作为研发者,开始工作

img_9.png

2.进入制品仓库

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

Snipaste_2023-05-01_21-22-33.png

3.查看账号密码

选择设置查看个人账号信息

Snipaste_2023-05-01_21-24-27.png

Snipaste_2023-05-01_21-26-20.png

设置私库源

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.配置个人账号信息

  1. 把对应rc文件username替换成你的账号名称

  2. 如果你使用的是yarn1,npm或pnpm,则把你的账号密码做base64加密之后替换掉对应rc文件password,如在这里在线base64加密获取base64加密的密码

img_10.png

  1. 如果你使用的是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包使用

// Example:
// 1.softphone.ts
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;
}

// transport层配置
export interface TransportOptions {
  /** websocket协商地址, server和wsServers 必须二选一*/
  server?: string
  /** 多个地址开启负载均衡模式 */
  wsServers?: string | string[] | {
    /** websocket协商地址 */
    ws_uri: string,
    /** 权重 */
    weight: number
  }[]
  /**
   * websocke初始化连接等待超时时间
   * @default 5
   */
  connectionTimeout?: number
  /**
   * transport层客户端保活最大重连尝试次数
   * @default 3
   */
  maxReconnectionAttempts?: number
  /**
   * transport层客户端保活重连动作执行间隔时间,单位秒,同UserAgentOptions.reconnectionDelay
   * @default 4
   */
  reconnectionTimeout?: number
  /**
   * transport层The time (Number) in seconds to wait in between CLRF keepAlive sequences are sent.
   * @default 0
   */
  keepAliveInterval?: number
  /**
   * transport层The time (Number) in seconds to debounce sending CLRF keepAlive sequences by
   * @default 10
   */
  keepAliveDebounce?: number
  /**
   * transport层If true, messages sent and received by the transport are logged.
   * @default false
   */
  traceSip?: boolean
}
// sip层配置
export interface UserAgentOptions {
  /**
   * 通过openApi获取坐席账号分机密码
   * @default ''
   */
  authorizationPassword: string,
  /**
   * 通过openApi获取坐席账号分机用户名
   * @default ''
   */
  authorizationUsername: string,
  /**
   * 指纹,唯一标志,用来排查线路故障,默认随机指纹,可选
   * @default createRandomToken(12) + ".invalid"
   */
  viaHost?: string,
  /**
   * sip服务地址,必填,需要服务可达的地址
   * @default new URI("sip", "anonymous." + createRandomToken(6), "anonymous.invalid") })
   */
  uri: URI,
  /**
   * sip日志查看等级,一般情况下生产开error,开发用debugger
   * @default 'log'
   */
  logLevel?: 'debug' | 'log' | 'warn' | 'error',
  /**
   * 一般设置同authorizationUsername,相当于MicroSIP的显示名称
   * @default createRandomToken(8)
   */
  contactName?: string
  /**
   * 签入来电后多长时间不执行接听会话自动结束会话,单位秒
   * @default 60
   */
  noAnswerTimeout?: number,
  /**
   * transport层协议配置
   */
  transportOptions: TransportOptions,
  /**
   * sip层 - 重连尝试间隔,为了兼容sipjs,同reconnectionInterval
   * @default 3
   */
  reconnectionDelay?: number,
  /**
   * sip层 - 重连尝试间隔,单位秒
   * @default 3
   */
  reconnectionInterval?: number,
  /**
   * sip层 - 重连失败最大尝试次数
   * @default 100
   */
  reconnectionAttempts?: number,
  /**
   * sip层 - 重连成功后尝试重新注册检间隔,单位秒
   * @default 3
   */
  registerInterval?: number,
  /**
   * sip层 - 重连成功最大尝试注册次数
   * @default 3
   */
  registerAttempts?: number
  /**
   * sip层 - ping动作间隔,单位秒
   * @default 8
   */
  optionsPingInterval?: number
  /**
   * sip层 - ping最大失败尝试次数后开始重连,防止网络抖动引起非必要重连
   * @default 3
   */
  optionsPingAttempts?: number
  /**
   * sip层 - 自定义通讯header
   */
  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()) // 👈 得到softphone代理对象
    }
    return this.#userAgentManager
  }
  public getUserAgentManagerSync = (config?: UserAgentOptions) => {
    if (!this.#userAgentManager) {
      this.#userAgentManager = UserAgentFactory.getUserAgentManager(config) // 👈 得到softphone代理对象
    }
    return this.#userAgentManager
  }
  /**
   * 通过openApi获取分机信息
   */
  public async initSeatsInfo = (): Promise<UserAgentOptions> => {
    const result = await getSeatsInfo()
    if (result.code === 200) {
      const {
        extPassword, // 分机密码
        extensionNumber, // 分机号
        wsRegisterAddress, // socket地址
        wsProtocol // socket协议
      } = 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(
        /** config */
        {
          /** 当软电话状态变化时会实时刷新这个方法 */
          refresh: (path: keyof typeof userAgentStatusDefault, value: boolean) => {

          }
        },
        /** event */
        {
          /** 当有外呼过来 */
          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()

// 2.App.vue
import commonSoftphone from './softphone.ts'
onBeforeMount(async () => {
  await commonSoftphone.getUserAgentManager()
})

// 3. softphone.vue
// <button @click="connect">签入</button>
import commonSoftphone from './softphone.ts'
// 签入
const connect = async () => {
  await commonSoftphone.connect()
}

2.直接在JavaScript中使用

  1. 获取umd文件
  • 确保安装了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
  1. 在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>
/**
 *  1.确保你的企业 【坐席外呼依赖】配置项 是【标识】 (默认是【登录】,如果不确定联系94运维在管理后台查看以及配置)
 *  2. 使用 【https://94ai.yuque.com/staff-kqoz0c/xed39g/obnqst?singleDoc# 《SG-九四智能API开放文档(全)》 密码:tvgm】 3.1 获取坐席信息,以及3.2 修改坐席状态接口,保证坐席状态在线
 *  3. 点击连接服务器
 *  4. 在决策外呼一个任务,确保你的坐席账号在任务配置的坐席组里
 *  5. 电话过来后会触发签入注册好的onInvite,这个时候就可以接听,挂断,忽略,转人工,静音等等
 *  6. 《SG-九四智能API开放文档(全)》该文档获取授权调用接口的问题找 航航同学
 */

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`
  /** 接口获取开始 */
  /** 通过接口获取  https://94ai.yuque.com/staff-kqoz0c/xed39g/obnqst?singleDoc# 《SG-九四智能API开放文档(全)》 密码:tvgm 👈 不知道怎么调接口找 航航同学 */
  /** 3.1坐席相关接口查询坐席信息 */
  const extensionNumber = '9288'
  const extPassword = 'zjh13542240708'
  const wsProtocol = 'wss'
  const wsRegisterAddress = 's28.94ai.com:7443'
  const sipServerHost = `sip:${extensionNumber}@${wsRegisterAddress}`
  const wsAddress = `${wsProtocol}://${wsRegisterAddress}`
  /** 3.2修改坐席账号状态 */
  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 // 👈 sdk

  /**
   * 开启电话响铃
   */
  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(
            { // config
              refresh (path, value) { // 当软电话状态变化时会实时刷新这个方法
                console.log(path, value)
              }
            },
            { // event
              onInvite (invitation) { // 当有外呼过来
                syncSipMessage(invitation.incomingInviteRequest.message.headers)
                phoneRings() // 播放模拟来电响铃
                ignore.disabled = false
                answer.disabled = false
                localSoundDisconnection.disabled = false
                remoteSoundDisconnection.disabled = false
                invitation.stateChange.addListener((state) => { // 一旦接听(执行accept),监听会话的生命周期
                  switch (state) {
                    case SessionState.Initial:
                      break
                    case SessionState.Establishing:
                      break
                    case SessionState.Established: // session建立后就可拿到webrtc的各种基础api,如userAgentManager.getPeerConnection(),如userAgentManager.getSenders()等等
                      const mediaElement1 = getMedia('remoteAudio')
                      mediaElement1.srcObject = userAgentManager.getStream() // 获取流
                      mediaElement1.play() // 把softphone流导入到audio接入用户语音
                      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') // 获取audio dom
                      mediaElement2.srcObject = null
                      mediaElement2.pause() // 释放audio
                      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 // 先监听任务需要 按*号转人工 才可以让用户听到坐席声音
  /**
   * sip协议通讯
   */
  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>

  1. tip 获取fingerprintjs: https://unpkg.com/@fingerprintjs/[email protected]/dist/fp.umd.min.js
  2. const phoneRing = './antique_phone.mp3' // 👈 铃声自己当以一个音频即可
  3. ￳直接双击打开的html的demo打电话这个模拟不了线上域名部署环境可能出现的问题,可以实现看各种接听挂断签入转人工等效果,file协议和localhost默认在浏览器安全域名内。注意线上环境浏览器是有限制要https协议的,http是使用不了浏览器webrtc的api的,要配置下浏览器安全上下文域名白名单

签入

import { playMedia, getMedia, SessionState } from '@94ai/softphone'
import { ref } from 'vue'
import popNotice from './usePopNotice.ts'  // 通知工具,见下面demo说明

const userAgentStatus = ref({ // 视图层响应式
  connectStatus: false, // 软电话是否已签入
  registerStatus: false, // 软电话是否已注册
  invitatingStatus: false, // 软电话是否正拨出
  incomingStatus: false, // 软电话是否正来电
  answerStatus: false, // 软电话是否正接听
  reconnectStatus: false, // 软电话是否正重连
})

userAgentManager.prepareUserAgent(
        { // config
          refresh(path, value) { // 当软电话状态变化时会实时刷新这个方法
            userAgentStatus[path] = value // 外部想要响应状态可以实时更新外部的userAgentStatus
          }
        },
        { // event
          onInvite(invitation) { // 当有外呼过来
            playMedia('localAudio') // 播放模拟来电响铃
            popNotice.popNotification('来电了') // 提示通知   来电了
            invitation.stateChange.addListener((state) => { // 一旦接听(执行accept),监听会话的生命周期
              switch (state) {
                case SessionState.Initial:
                  break
                case SessionState.Establishing:
                  break
                case SessionState.Established: // session建立后就可拿到webrtc的各种基础api,如userAgentManager.getPeerConnection(),如userAgentManager.getSenders()等等
                  const mediaElement1 = getMedia('remoteAudio')
                  mediaElement1.srcObject = userAgentManager.getStream() // 获取流
                  mediaElement1.play() // 把softphone流导入到audio接入用户语音
                  break
                case SessionState.Terminating:
                case SessionState.Terminated: // 在挂断电话时候会执行
                  const mediaElement2 = getMedia('remoteAudio') // 获取audio dom
                  mediaElement2.srcObject = null
                  mediaElement2.pause() // 释放audio
                  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() // 👈 通过webrtc的方式控制 本地音频流 推送到 远端
}

不传输本地声音到远端

const muteLocalAudio = () => {
  userAgentManager.muteLocalAudio() // 👈 通过webrtc的方式控制 本地音频流 禁用推送
}

接听远端声音

const unMuteRemoteAudio = () => {
  userAgentManager.unMuteRemoteAudio() // 👈 通过webrtc的方式控制 是否接受 远端音频流
} 

不接听远端声音

const muteRemoteAudio = () => {
  userAgentManager.muteRemoteAudio() // 👈 通过webrtc的方式控制 是否接受 远端音频流
}

获取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智能决策系统 额外扩展了 【监听中】,【整理中】,【小休中】等状态