@94ai/softphone 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ import type { UserAgentDelegate } from 'sip.js/lib/api/user-agent-delegate'
2
+ import type { UserAgentOptions as SipUserAgentOptions } from 'sip.js/lib/api/user-agent-options'
3
+ import { URI } from 'sip.js'
4
+ import { InvitationRejectOptions } from 'sip.js/lib/api/invitation-reject-options'
5
+ import { SessionByeOptions } from 'sip.js/lib/api/session-bye-options'
6
+ import { RegistererOptions } from 'sip.js/lib/api/registerer-options'
7
+ import { RegistererRegisterOptions } from 'sip.js/lib/api/registerer-register-options'
8
+ import { InviterInviteOptions } from 'sip.js/lib/api/inviter-invite-options'
9
+ import { SessionInfoOptions } from 'sip.js/lib/api/session-info-options'
10
+ import { InvitationAcceptOptions } from 'sip.js/lib/api/invitation-accept-options'
11
+
12
+ export type InviterInviteOptionsExtend = InviterInviteOptions & {extraHeaders?: string[]}
13
+ export type SessionInfoOptionsExtend = SessionInfoOptions & {extraHeaders?: string[]}
14
+ export type InvitationAcceptOptionsExtend = InvitationAcceptOptions & {onAck?: Function, onAckTimeout?: Function }
15
+
16
+ /**
17
+ * 挂断配置
18
+ */
19
+ export type HandUpInviteOption = {
20
+ /** 当会话尚在建立中,配置挂断请求 */
21
+ rejectOptions?: InvitationRejectOptions,
22
+ /** 当会话已建立,配置挂断请求 */
23
+ byeOptions?: SessionByeOptions
24
+ /** sip自定义头 */
25
+ extraHeaders?: string[]
26
+ /** 是否开启响应返回检测 */
27
+ scoutResponse?: boolean
28
+ }
29
+
30
+ /**
31
+ * 注册配置
32
+ */
33
+ export type RegisterOptions = {
34
+ /** sip自定义头 */
35
+ extraHeaders?: string[]
36
+ /** new Registerer Options */
37
+ registererOptions?: RegistererOptions
38
+ /** Registerer Instance register Options */
39
+ registererRegisterOptions?: RegistererRegisterOptions
40
+ }
41
+
42
+ /**
43
+ * 签入配置
44
+ */
45
+ export type PrepareUserAgentOptions = {
46
+ /** sip服务地址,可以用于切换签入服务器 */
47
+ uri?: URI,
48
+ /** 分机号,可以用于切换签入服务器 */
49
+ authorizationUsername?: string,
50
+ /** 分机密码,可以用于切换签入服务器 */
51
+ authorizationPassword?: string,
52
+ /** transport层协议配置,可以用于切换签入服务器 */
53
+ transportOptions?: TransportOptions,
54
+ /** 一般设置同authorizationUsername,相当于MicroSIP的显示名称,可以用于切换签入服务器 */
55
+ contactName?: string
56
+ /** sip自定义头 */
57
+ extraHeaders?: string[]
58
+ /** 当软电话状态变化时会实时刷新这个方法 */
59
+ refresh?: (path: UserAgentStatusKey, value: boolean) => void,
60
+ /** new Registerer Options */
61
+ registererOptions?: RegistererOptions
62
+ /** Registerer Instance register Options */
63
+ registererRegisterOptions?: RegistererRegisterOptions
64
+ agentId?: number
65
+ agentTag?: string
66
+ appKey?: string
67
+ appSecret?: string
68
+ openBaseUrl?: string
69
+ sg?: '1' | '0'
70
+ sgOpen?: '1' | '0'
71
+ }
72
+
73
+ /**
74
+ * 软电话代理实例状态
75
+ */
76
+ export type UserAgentStatus = {
77
+ /** 软电话是否已签入 */
78
+ connectStatus: boolean,
79
+ /** 软电话是否已注册 */
80
+ registerStatus: boolean,
81
+ /** 软电话是否正拨出 */
82
+ invitatingStatus: boolean,
83
+ /** 软电话是否正来电 */
84
+ incomingStatus: boolean,
85
+ /** 软电话是否正接听 */
86
+ answerStatus: boolean
87
+ /**
88
+ * 软电话是否正在重连
89
+ * 网络断掉,服务器重启,宕机等引起服务不可达时软电话代理会尝试重新连接并签入
90
+ * 这个时候用户拨出等动作可以通过此状态做拦截,通知用户软电话服务器可能正重启或断网等导致服务不可用
91
+ * */
92
+ reconnectStatus: boolean
93
+ }
94
+ export type UserAgentDelegateKey = keyof UserAgentDelegate
95
+ export type UserAgentStatusKey = 'connectStatus' | 'registerStatus' | 'invitatingStatus' | 'incomingStatus' | 'answerStatus'
96
+
97
+ /**
98
+ * transport层配置
99
+ */
100
+ export interface TransportOptions {
101
+ /** websocket协商地址, server和wsServers 必须二选一*/
102
+ server?: string
103
+ /** 多个地址开启负载均衡模式 */
104
+ wsServers?: string | string[] | {
105
+ /** websocket协商地址 */
106
+ ws_uri: string,
107
+ /** 权重 */
108
+ weight: number
109
+ }[]
110
+ /**
111
+ * websocke初始化连接等待超时时间
112
+ * @default 5
113
+ */
114
+ connectionTimeout?: number
115
+ /**
116
+ * transport层客户端保活最大重连尝试次数
117
+ * @default 3
118
+ */
119
+ maxReconnectionAttempts?: number
120
+ /**
121
+ * transport层客户端保活重连动作执行间隔时间,单位秒,同UserAgentOptionsSDK.reconnectionDelay
122
+ * @default 4
123
+ */
124
+ reconnectionTimeout?: number
125
+ /**
126
+ * transport层The time (Number) in seconds to wait in between CLRF keepAlive sequences are sent.
127
+ * @default 0
128
+ */
129
+ keepAliveInterval?: number
130
+ /**
131
+ * transport层The time (Number) in seconds to debounce sending CLRF keepAlive sequences by
132
+ * @default 10
133
+ */
134
+ keepAliveDebounce?: number
135
+ /**
136
+ * transport层If true, messages sent and received by the transport are logged.
137
+ * @default false
138
+ */
139
+ traceSip?: boolean
140
+ }
141
+
142
+ /**
143
+ * sip层配置
144
+ */
145
+ export interface UserAgentOptionsSDK {
146
+ /**
147
+ * 通过openApi获取坐席账号分机密码
148
+ * @default ''
149
+ */
150
+ authorizationPassword: string,
151
+ /**
152
+ * 通过openApi获取坐席账号分机用户名
153
+ * @default ''
154
+ */
155
+ authorizationUsername: string,
156
+ /**
157
+ * 指纹,唯一标志,用来排查线路故障,默认随机指纹,可选
158
+ * @default createRandomToken(12) + ".invalid"
159
+ */
160
+ viaHost?: string,
161
+ /**
162
+ * sip服务地址,必填,需要服务可达的地址
163
+ * @default new URI("sip", "anonymous." + createRandomToken(6), "anonymous.invalid") })
164
+ */
165
+ uri: URI,
166
+ /**
167
+ * sip日志查看等级,一般情况下生产开error,开发用debugger
168
+ * @default 'log'
169
+ */
170
+ logLevel?: 'debug' | 'log' | 'warn' | 'error',
171
+ /**
172
+ * 一般设置同authorizationUsername,相当于MicroSIP的显示名称
173
+ * @default createRandomToken(8)
174
+ */
175
+ contactName?: string
176
+ /**
177
+ * 签入来电后多长时间不执行接听会话自动结束会话,单位秒
178
+ * @default 60
179
+ */
180
+ noAnswerTimeout?: number,
181
+ /**
182
+ * sip层 - 重连尝试间隔,为了兼容sipjs,同reconnectionInterval
183
+ * @deprecated
184
+ * @default 100
185
+ */
186
+ reconnectionDelay?: number,
187
+ /**
188
+ * transport层协议配置
189
+ */
190
+ transportOptions: TransportOptions,
191
+ /**
192
+ * sip层 - 重连尝试间隔,单位秒
193
+ * @default 100
194
+ */
195
+ reconnectionInterval?: number,
196
+ /**
197
+ * sip层 - 重连失败最大尝试次数
198
+ * @default
199
+ */
200
+ reconnectionAttempts?: number,
201
+ /**
202
+ * sip层 - 重连成功后尝试重新注册检间隔,单位秒
203
+ * @default 3
204
+ */
205
+ registerInterval?: number,
206
+ /**
207
+ * sip层 - 重连成功最大尝试注册次数
208
+ * @default 3
209
+ */
210
+ registerAttempts?: number
211
+ /**
212
+ * sip层 - ping动作间隔,单位秒
213
+ * @default 8
214
+ */
215
+ optionsPingInterval?: number
216
+ /**
217
+ * sip层 - ping最大失败尝试次数后开始重连,防止网络抖动引起非必要重连
218
+ * @default 3
219
+ */
220
+ optionsPingAttempts?: number
221
+ /**
222
+ * sip层 - 自定义通讯header
223
+ */
224
+ sipHeaders?: Array<string>
225
+ appKey?: string
226
+ appSecret?: string
227
+ agentTag?: string
228
+ agentId?: number
229
+ openBaseUrl?: string
230
+ sg?: '1' | '0'
231
+ sgOpen?: '1' | '0'
232
+ token: string,
233
+ tokenTimestamp: string | number,
234
+ tokenExpirationTime: number,
235
+ sign: string,
236
+ timestamp: string | number,
237
+ signExpirationTime: number,
238
+ signCheck?: Function
239
+ tokenCheck?: Function
240
+ signOverdued?: Function
241
+ tokenOverdued?: Function
242
+ openXhrIntercept?: Function
243
+ gatewayXhrIntercept?: Function
244
+ refreshChatErrorCallback?: Function
245
+ refreshSpeekVolumn?: Function
246
+ refreshRequirementCheck?: Function
247
+ refreshChat?: Function
248
+ enableChatInfoPush?: boolean
249
+ enableVolumnTrack?: boolean
250
+ }
251
+
252
+ export type UserAgentOptions = SipUserAgentOptions & UserAgentOptionsSDK
253
+
254
+ export type ajaxOption = {
255
+ type: 'GET' | 'POST',
256
+ url: string,
257
+ data: Record<string, any>,
258
+ contentType: string,
259
+ }
260
+
261
+ export enum CallType {
262
+ '坐席-人工外呼' = 1001,
263
+ '坐席-AI 外呼-不转人工' = 1002,
264
+ '坐席-AI 外呼-接通转人工' = 1003,
265
+ '坐席-AI 外呼-智能转人工' = 1004,
266
+ '批量-预测外呼' = 2001,
267
+ '批量-AI 外呼-不转人工' = 2002,
268
+ '批量-AI 外呼-接通转人工' = 2003,
269
+ '批量-AI 外呼-智能转人工' = 2004,
270
+ '批量-语音通知' = 2005,
271
+ }
272
+
273
+ export interface PushData {
274
+ /**
275
+ * 是否停止轮询
276
+ */
277
+ stop: boolean
278
+ /**
279
+ * 任务 ID
280
+ */
281
+ taskId: string
282
+ /**
283
+ * 分机号
284
+ */
285
+ extensionNumber: string
286
+ /**
287
+ * 外呼 ID
288
+ */
289
+ callid: string
290
+ /**
291
+ * 外呼 ID
292
+ */
293
+ callId: string
294
+ /**
295
+ * 外呼类型
296
+ */
297
+ callType: CallType
298
+ /**
299
+ * 意向标签
300
+ */
301
+ intentTag: string
302
+ /**
303
+ * 外呼号码
304
+ */
305
+ number: string
306
+ /**
307
+ * 号码 ID
308
+ */
309
+ numberMD5: string
310
+ /**
311
+ * 分配坐席 ID
312
+ */
313
+ agentId: number
314
+ /**
315
+ * 坐席标签
316
+ */
317
+ agentTag: string
318
+ /**
319
+ * 用户标签
320
+ */
321
+ tag: string
322
+ /**
323
+ * AI 话术 ID
324
+ */
325
+ templateId: string
326
+ chats: {
327
+ /**
328
+ * 说话内容
329
+ */
330
+ "content": string,
331
+ /**
332
+ * 说话时间
333
+ */
334
+ "createTime": string,
335
+ /**
336
+ * 说话号码
337
+ */
338
+ "fromNumber": string
339
+ }[]
340
+ [index:string]:any
341
+ }
342
+
343
+
@@ -0,0 +1,301 @@
1
+ import {
2
+ Inviter,
3
+ SessionState,
4
+ UserAgent
5
+ } from 'sip.js'
6
+ import { ajaxOption } from '../types'
7
+
8
+ /**
9
+ * 获取音频标签
10
+ * @param id Element id
11
+ */
12
+ export function getMedia (id: string): HTMLAudioElement | undefined {
13
+ return document.getElementById(id) as HTMLAudioElement
14
+ }
15
+
16
+ /**
17
+ * 释放 mediaStream
18
+ * @param stream
19
+ */
20
+ export function stopStreamTracks (stream: any) {
21
+ if (!stream || !stream.getTracks) {
22
+ return
23
+ }
24
+ try {
25
+ const tracks = stream.getTracks()
26
+ tracks.forEach((it: any) => {
27
+ try {
28
+ it.stop()
29
+ } catch (errMsg) {
30
+ // debugger;
31
+ }
32
+ })
33
+ } catch (errMsg) {
34
+ // debugger;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 获取设备权限
40
+ * @param constraints
41
+ */
42
+ export function getDevicePermission (constraints: any) {
43
+ return navigator.mediaDevices
44
+ .getUserMedia(constraints)
45
+ .then(stream => {
46
+ if (stream) {
47
+ stopStreamTracks(stream)
48
+ return true
49
+ }
50
+ return Promise.reject(new Error('EmptyStreamError'))
51
+ })
52
+ .catch(errMsg => {
53
+ if (errMsg && errMsg.name === 'NotAllowedError') {
54
+ return false
55
+ }
56
+ return Promise.reject(errMsg)
57
+ })
58
+ }
59
+
60
+ /**
61
+ * 请求设备权限
62
+ */
63
+ export function requestMicroPhonePermission () {
64
+ try {
65
+ if (navigator.userAgent.indexOf('Firefox') === -1) {
66
+ return getDevicePermission({ video: false, audio: true }).catch(() => true)
67
+ }
68
+ } catch (e) {
69
+ console.log(e)
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 来电振铃
75
+ * @param id Element id
76
+ */
77
+ export function playMedia (id: string): void {
78
+ const localAudioDom = getMedia(id)
79
+ if (localAudioDom) {
80
+ localAudioDom.currentTime = 0
81
+ localAudioDom.play()
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 关闭振铃
87
+ * @param id Element id
88
+ */
89
+ export function pauseMedia (id: string): void {
90
+ const localAudioDom = getMedia(id)
91
+ if (localAudioDom) {
92
+ localAudioDom.currentTime = 0
93
+ localAudioDom.pause()
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 挂断通话
99
+ * @param id Element id
100
+ */
101
+ export function cleanupMedia (id: string): void {
102
+ const mediaElement = getMedia(id)
103
+ if (mediaElement) {
104
+ mediaElement.srcObject = null
105
+ mediaElement.pause()
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 发送
111
+ */
112
+ export const onSendCall = (userAgent: InstanceType<typeof UserAgent>, target: any) => {
113
+ const inviter = new Inviter(userAgent, target)
114
+ inviter.stateChange.addListener((state) => {
115
+ switch (state) {
116
+ case SessionState.Initial:
117
+ break
118
+ case SessionState.Establishing:
119
+ break
120
+ case SessionState.Established:
121
+ // setupMedia(inviter);
122
+ break
123
+ case SessionState.Terminating:
124
+ // fall through
125
+ case SessionState.Terminated:
126
+ cleanupMedia('remoteAudio')
127
+ break
128
+ default:
129
+ throw new Error('Unknown session state.')
130
+ }
131
+ })
132
+ inviter.invite()
133
+ }
134
+
135
+ /**
136
+ * 获取时间字符串
137
+ * @param { Date }} time 需要转换的时间对象
138
+ * @return xx:xx:xx
139
+ */
140
+ export const getTimes = (time: Date) => {
141
+ return time.getHours().toString().padStart(2, '0') + ':' + time.getMinutes().toString().padStart(2, '0') + ':' + time.getSeconds().toString().padStart(2, '0')
142
+ }
143
+ /**
144
+ * 获取0点时间
145
+ * @return Date
146
+ */
147
+ export const getZeorTime = () => {
148
+ return new Date(new Date(new Date().toLocaleDateString()).getTime())
149
+ }
150
+ /**
151
+ * 加1s时间
152
+ * @param { Date } date 需要操作的时间对象
153
+ * @return Date
154
+ */
155
+ export const accumulateSec = (date: Date) => {
156
+ return new Date(date.setSeconds(date.getSeconds() + 1))
157
+ }
158
+
159
+ /**
160
+ * 计时器
161
+ * @param { Function } refresh 回调
162
+ * @return Function 销毁计时器
163
+ */
164
+ export function accumulationTimer (refresh: Function) {
165
+ const date = getZeorTime()
166
+ refresh(getTimes(date))
167
+ const calc = () => {
168
+ const addtime = accumulateSec(date)
169
+ const times = getTimes(addtime)
170
+ refresh(times)
171
+ }
172
+ let timer = setInterval(calc, 1000);
173
+ return () => {
174
+ clearInterval(timer)
175
+ timer = null
176
+ }
177
+ }
178
+
179
+ /**
180
+ * 转换queryParams
181
+ * @param url
182
+ */
183
+ export function getQueryObject (location = window.location) {
184
+ const hashUrl = location.hash
185
+ const searchUrl = location.search
186
+ const obj:Record<string, string> = {}
187
+ const reg = /([^?&=]+)=([^?&=]*)/g
188
+ const hash = hashUrl.substring(hashUrl.indexOf('?') + 1)
189
+ const search = searchUrl.substring(1)
190
+ // @ts-ignore
191
+ hash.replace(reg, (rs, $1, $2) => {
192
+ const name = decodeURIComponent($1)
193
+ let val = decodeURIComponent($2)
194
+ val = String(val)
195
+ obj[name] = val
196
+ return rs
197
+ })
198
+ // @ts-ignore
199
+ search.replace(reg, (rs, $1, $2) => {
200
+ const name = decodeURIComponent($1)
201
+ let val = decodeURIComponent($2)
202
+ val = String(val)
203
+ obj[name] = val
204
+ return rs
205
+ })
206
+ return obj
207
+ }
208
+
209
+ export function getTop () {
210
+ if (getQueryObject(location).cross === '1') {
211
+ return window
212
+ }
213
+ return top
214
+ }
215
+
216
+ export function params(json: Record<string, string>){
217
+ let paramArr = []
218
+ for (let p in json) {
219
+ paramArr.push(p + '=' + json[p])
220
+ }
221
+ return paramArr.join('&')
222
+ }
223
+
224
+ export function isDomainOrIP (str: string) {
225
+ if (!str) {
226
+ return false
227
+ }
228
+ const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/
229
+ const domainPattern = /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$/
230
+ if (ipPattern.test(str)) {
231
+ return true
232
+ }
233
+ if (str.slice(0, 2) === '//') {
234
+ return domainPattern.test(str.slice(2, str.length))
235
+ }
236
+ if (str.slice(0, 7) === 'http://') {
237
+ return domainPattern.test(str.slice(7, str.length))
238
+ }
239
+ if (str.slice(0, 8) === 'https://') {
240
+ return domainPattern.test(str.slice(8, str.length))
241
+ }
242
+ return domainPattern.test(str)
243
+ }
244
+
245
+ export function getAjax(resolve: Function, reject: Function, param: Partial<ajaxOption>) {
246
+ // @ts-ignore
247
+ const xhr = window.XMLHttpRequest ? new window.XMLHttpRequest() : window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP') : undefined // 兼容IE6 IE5
248
+ if (!xhr) {
249
+ reject(new Error('Your Browser does not support ajax'))
250
+ }
251
+ xhr.onreadystatechange = function () {
252
+ // 0:请求初始化,对象刚刚创建
253
+ // 1:服务器已连接
254
+ // 2:已发送,send发放已调用
255
+ // 3:已接收,此时只接收了响应(response)头部分
256
+ // 4:已接收,此时接收响应(response)体信息
257
+ if (xhr.readyState == 4) { // 每当 readyState 状态值发生改变时会,就会触发 onreadystatechange 事件,对应着每个状态值就会被触发五次。当状态值为 4 时表示网络请求响应完毕,就可以获取返回的值。
258
+ const res = xhr.response || xhr.responseText
259
+ // 1XX:信息类,表示收到web浏览器请求,正在进一步处理中
260
+ // 2XX:成功,表示用户请求被正确接收
261
+ // 3XX:重定向,表示请求没成功,需要客户采取进一步的动作(304表示请求的资源未修改,可以直接使用浏览器的缓存版本)
262
+ // 4XX:客户端错误,表示客户端提交的请求有错误,如:404 Not Found,意味着请求中所引用的文档不存在
263
+ // 5XX:服务器错误,表示服务器不能完成对请求的处理
264
+ if (xhr.status == 200) {
265
+ //接收返回的数据类型
266
+ const type = xhr.getResponseHeader('Content-Type');
267
+ if (type.indexOf('json') != -1){ //json格式
268
+ resolve(JSON.parse(xhr.responseText))
269
+ } else if(type.indexOf('xml') != -1){ //xml格式
270
+ resolve(xhr.responseXML);
271
+ } else{
272
+ resolve(xhr.responseText); //普通格式
273
+ }
274
+ } else {
275
+ reject(res)
276
+ }
277
+ }
278
+ }
279
+ const {
280
+ url,
281
+ data,
282
+ type,
283
+ contentType,
284
+ } = param
285
+ const ajaxType = type || 'POST' //判断data是否为空
286
+ let ajaxUrl = url
287
+ let ajaxData = null
288
+ if (data) {
289
+ if(ajaxType === 'GET'){
290
+ ajaxUrl = ajaxUrl + '?'+ params(data);
291
+ } else if (ajaxType === 'POST') {
292
+ ajaxData = JSON.stringify(data)
293
+ }
294
+ }
295
+ xhr.open(ajaxType, ajaxUrl, true)
296
+ return {
297
+ xhr,
298
+ ajaxData,
299
+ contentType
300
+ }
301
+ }