@94ai/softphone 5.0.10 → 5.0.12

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,465 @@
1
+ class SoftphoneManager {
2
+
3
+ id
4
+ timer
5
+
6
+ constructor() {
7
+ /**
8
+ * 隔离链接实例
9
+ */
10
+ this.id = generateUUID()
11
+ }
12
+
13
+ option = {
14
+ origin: 'ai-softphone',
15
+ ancestorOrigin: 'nf-softphone',
16
+ destinationOrigin: '*',
17
+
18
+ scrollview: undefined,
19
+ scrollingText: '正在滚动中...',
20
+
21
+ envMap: {
22
+ test: 'http://softphone.test.k8s.com',
23
+ dev: 'http://softphone.dev.k8s.com',
24
+ sg: 'https://seat.teleai.com',
25
+ gray: 'http://softphone.gray.94ai.com',
26
+ prod: 'https://seat.94ai.com',
27
+ },
28
+ env: 'prod',
29
+ selector: undefined,
30
+ el: undefined,
31
+
32
+ agentId: undefined,
33
+ agentTag: undefined,
34
+ appKey: undefined,
35
+ appSecret: undefined,
36
+
37
+ sgGateway: undefined,
38
+ sgBase: undefined,
39
+ sgOpen: undefined,
40
+ sgDomain: undefined,
41
+
42
+ timestamp: undefined,
43
+ sign: undefined,
44
+
45
+ extStatus: '0',
46
+
47
+ callShow: '1',
48
+ transferToLaborShow: '1',
49
+ restShow: '1',
50
+ networkDetectShow: '1',
51
+ muteShow: '1',
52
+ logShow: '1',
53
+ timingShow: '1',
54
+ signShow: '1',
55
+
56
+ instanceMap: {},
57
+
58
+ softphoneConnectCallBack: () => {console.log('softphone-connect')},
59
+ softphoneCallRefreshCallBack: () => {console.log('softphone-call-refresh')},
60
+ softphoneSeatStatusChangeCallBack: () => {console.log('softphone-seats-status-change')},
61
+ softphoneAcceptCallBack: () => {console.log('softphone-accept')},
62
+ softphoneIgnoreCallBack: () => {console.log('softphone-ignore')},
63
+ softphoneHangupCallBack: () => {console.log('softphone-hangup')},
64
+ softphoneSessionStateChangeCallBack: () => {console.log('softphone-session-state-change')},
65
+ softphoneIncomingCallBack: () => {console.log('softphone-incoming')},
66
+ softphoneSendDtmfCallBack: () => {console.log('softphone-send-dtmf')},
67
+ softphoneConnectRegisteredCallBack: () => {console.log('softphone-connect-registered')},
68
+ softphoneSignOverduedCallBack: (done) => {
69
+ done({
70
+ timestamp: undefined,
71
+ sign: undefined
72
+ })
73
+ },
74
+ }
75
+
76
+
77
+ loadCssCode = (code) => {
78
+ const styleId = 'nf-softphone'
79
+ if (!document.getElementById(styleId)) {
80
+ const style = document.createElement('style');
81
+ style.id = styleId
82
+ style.type = 'text/css';
83
+ // @ts-ignore
84
+ style.rel = 'stylesheet';
85
+ //for Chrome Firefox Opera Safari
86
+ style.appendChild(document.createTextNode(code));
87
+ //for IE
88
+ //style.styleSheet.cssText = code;
89
+ const head = document.getElementsByTagName('head')[0];
90
+ head.appendChild(style);
91
+ }
92
+ }
93
+
94
+ removeCssCode = () => {
95
+ const styleId = 'nf-softphone'
96
+ const css = document.getElementById(styleId)
97
+ if (css) {
98
+ if (css.parentNode) {
99
+ css.parentNode.removeChild(css);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 隔离软电话容器
106
+ */
107
+ getContextDomElement = (el) => {
108
+ return (this.option.selector ? (document.querySelector(this.option.selector) ?? document) : document).querySelector(el)
109
+ }
110
+ /**
111
+ * 隔离软电话容器内容
112
+ */
113
+ getSoftphoneDomElement = (el) => {
114
+ return this.getContextDomElement(this.option.el).querySelector(el) || this.getContextDomElement(el) || document.querySelector(el)
115
+ }
116
+
117
+ signOverdued = (data) => {
118
+ const iframe = this.getSoftphoneDomElement('.nf-softphone-iframe')
119
+ iframe?.contentWindow?.postMessage(JSON.stringify({
120
+ origin: 'nf-softphone',
121
+ ancestorOrigin: this.option.ancestorOrigin,
122
+ destinationOrigin: 'ai-softphone',
123
+ type: 'softphone-sign-overdued',
124
+ data
125
+ }), '*')
126
+ }
127
+
128
+ handlingCommunication = (e) => {
129
+ const iframe = this.getSoftphoneDomElement('.nf-softphone-iframe')
130
+ if (iframe) {
131
+ let data = {}
132
+ try {
133
+ data = JSON.parse(e.data)
134
+ } catch (e) {}
135
+ if ((data.ancestorOrigin === this.option.ancestorOrigin && data.origin !== this.option.ancestorOrigin) ||
136
+ (data.ancestorOrigin === this.option.origin && data.origin === this.option.origin)) {
137
+ if (data.destinationOrigin === '*' || data.destinationOrigin === this.option.destinationOrigin) {
138
+ if (data.type === 'softphone-expand') {
139
+ if (data.data === 'expand') {
140
+ iframe.setAttribute('style', 'position: fixed;z-index:999;');
141
+ } else if (data.data === 'shrink') {
142
+ iframe.removeAttribute('style');
143
+ }
144
+ return
145
+ }
146
+ if (data.type === 'softphone-connect') {
147
+ this.option.softphoneConnectCallBack?.(data)
148
+ } else if (data.type === 'softphone-call-refresh') {
149
+ this.option.softphoneCallRefreshCallBack?.(data)
150
+ } else if (data.type === 'softphone-seats-status-change') {
151
+ this.option.softphoneSeatStatusChangeCallBack?.(data)
152
+ } else if (data.type === 'softphone-accept') {
153
+ this.option.softphoneAcceptCallBack?.(data)
154
+ } else if (data.type === 'softphone-ignore') {
155
+ this.option.softphoneIgnoreCallBack?.(data)
156
+ } else if (data.type === 'softphone-hangup') {
157
+ this.option.softphoneHangupCallBack?.(data)
158
+ } else if (data.type === 'softphone-session-state-change') {
159
+ this.option.softphoneSessionStateChangeCallBack?.(data)
160
+ } else if (data.type === 'softphone-incoming') {
161
+ this.option.softphoneIncomingCallBack?.(data)
162
+ } else if (data.type === 'softphone-send-dtmf') {
163
+ this.option.softphoneSendDtmfCallBack?.(data)
164
+ } else if (data.type === 'softphone-connect-registered') {
165
+ this.option.softphoneConnectRegisteredCallBack?.(data)
166
+ } else if (data.type === 'softphone-sign-overdued') {
167
+ const done = (data) => {
168
+ const {
169
+ sign,
170
+ timestamp
171
+ } = data
172
+ this.signOverdued({
173
+ sign,
174
+ timestamp
175
+ })
176
+ }
177
+ this.option.softphoneSignOverduedCallBack?.(done)
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ handlingScroll = debounce(async () => {
185
+ const {
186
+ top,
187
+ left,
188
+ } = (await this.refreshStyle() || {})
189
+ if (top && left) {
190
+ (this.getSoftphoneDomElement('.nf-softphone-iframe')).contentWindow.postMessage(JSON.stringify({
191
+ type: 'softphone-scroll',
192
+ origin: this.option.origin,
193
+ ancestorOrigin: this.option.ancestorOrigin,
194
+ destinationOrigin: this.option.destinationOrigin,
195
+ data: {
196
+ paddingTop: top + 'px',
197
+ }
198
+ }), '*')
199
+ }
200
+ }, 500, false)
201
+
202
+ handlerInteractive = () => {
203
+ /**
204
+ * 实时处理层级
205
+ */
206
+ window.addEventListener('message', this.handlingCommunication, false);
207
+ if (this.option.scrollview) {
208
+ document.querySelector(this.option.scrollview)?.addEventListener('scroll', this.handlingScroll)
209
+ document.querySelector(this.option.scrollview)?.addEventListener('scroll', this.handlingScrollTransparent)
210
+ }
211
+
212
+ this.loadCssCode(`
213
+ .nf-transparent {
214
+ visibility: hidden;
215
+ }
216
+ .nf-softphone-text {
217
+ text-align: center;
218
+ line-height: 32px;
219
+ margin-top: -32px;
220
+ }
221
+ .nf-softphone-container {
222
+ margin-left: 10px;
223
+ height: 32px;
224
+ min-width: 600px;
225
+ }
226
+ .nf-softphone-iframe-container {
227
+ height: 32px;
228
+ width: 100%;
229
+ overflow: visible;
230
+ }
231
+ .nf-softphone-iframe {
232
+ width: 100vw;
233
+ height: 100vh;
234
+ top: 0;
235
+ left: 0;
236
+ }
237
+ `);
238
+ }
239
+
240
+ handlerDomRemove = () => {
241
+ Object.keys(this.option.instanceMap).forEach(key => {
242
+ const el = document.getElementById(key)
243
+ if (el && el.parentNode) {
244
+ el.parentNode.removeChild(el)
245
+ }
246
+ })
247
+ }
248
+
249
+ _containerScrolling
250
+
251
+ get containerScrolling() {
252
+ return this._containerScrolling
253
+ }
254
+
255
+ set containerScrolling(value) {
256
+ if (this.containerScrolling !== value) {
257
+ this._containerScrolling = value
258
+ const nfSoftphoneContainer = this.getSoftphoneDomElement('.nf-softphone-container')
259
+ const nfSoftphoneText = this.getSoftphoneDomElement('.nf-softphone-text')
260
+ nfSoftphoneContainer?.setAttribute('class', `nf-softphone-container ${value === true ? 'nf-transparent' : ''}`)
261
+ nfSoftphoneText?.setAttribute('style', `display: ${value === true ? 'block' : 'none'}`)
262
+ }
263
+ }
264
+
265
+ handlingScrollTransparent = () => {
266
+ this.containerScrolling = true
267
+ }
268
+
269
+ refreshStyle = async () => {
270
+ clearTimeout(this.timer)
271
+ this.timer = undefined
272
+ const nfSoftphoneContainer = this.getSoftphoneDomElement('.nf-softphone-container')
273
+ const nfSoftphoneIframeContainer = this.getSoftphoneDomElement('.nf-softphone-iframe-container')
274
+ if (nfSoftphoneContainer && nfSoftphoneIframeContainer) {
275
+ const containerStyle = nfSoftphoneContainer.getAttribute('style')
276
+ const iframeContainerStyle = nfSoftphoneIframeContainer.getAttribute('style')
277
+ nfSoftphoneContainer.removeAttribute('style')
278
+ nfSoftphoneIframeContainer.removeAttribute('style')
279
+ const leftMargin = getComputedStyle(nfSoftphoneContainer).marginLeft
280
+ const top = nfSoftphoneContainer.getBoundingClientRect().top
281
+ if (top <= 0) {
282
+ nfSoftphoneContainer.setAttribute('style', containerStyle)
283
+ nfSoftphoneIframeContainer.setAttribute('style', iframeContainerStyle)
284
+ return {
285
+ top: 0,
286
+ left: 0,
287
+ leftMargin
288
+ }
289
+ }
290
+ const left = nfSoftphoneContainer.getBoundingClientRect().left
291
+ if (left <= 0) {
292
+ nfSoftphoneContainer.setAttribute('style', containerStyle)
293
+ nfSoftphoneIframeContainer.setAttribute('style', iframeContainerStyle)
294
+ return {
295
+ top: 0,
296
+ left: 0,
297
+ leftMargin
298
+ }
299
+ }
300
+ nfSoftphoneContainer.setAttribute('style', `margin-top:${top}px;margin-left:${leftMargin};margin-bottom:${top}px;`)
301
+ nfSoftphoneIframeContainer.setAttribute('style', `margin-top:-${2 * top}px;margin-left:-${left}px;`)
302
+ this.timer = setTimeout(() => {
303
+ clearTimeout(this.timer)
304
+ this.timer = undefined
305
+ this.containerScrolling = false
306
+ }, 100)
307
+ return {
308
+ leftMargin,
309
+ top,
310
+ left
311
+ }
312
+ }
313
+ return {
314
+ leftMargin: 0,
315
+ top: 0,
316
+ left: 0
317
+ }
318
+ }
319
+
320
+ handlerDomGenerater = async () => {
321
+ const nfSoftphone = document.createElement('div');
322
+ const nfSoftphoneText = document.createElement('div');
323
+ nfSoftphoneText.innerText = this.option.scrollingText || '正在滚动中...'
324
+
325
+ const nfSoftphoneContainer = document.createElement('div');
326
+ const iframeContainer = document.createElement('div');
327
+ const iframe = document.createElement('iframe');
328
+
329
+ const uuid = this.id + '-container'
330
+ this.option.instanceMap[uuid] = nfSoftphone
331
+ nfSoftphone.setAttribute('id', uuid)
332
+ nfSoftphone.setAttribute('class', 'nf-softphone')
333
+ nfSoftphoneText.setAttribute('class', 'nf-softphone-text')
334
+ nfSoftphoneText.setAttribute('style', "display: none;")
335
+ nfSoftphoneContainer.setAttribute('class', "nf-softphone-container")
336
+ iframeContainer.setAttribute('class', "nf-softphone-iframe-container")
337
+ iframe.setAttribute('class', "nf-softphone-iframe")
338
+ iframe.setAttribute('src', "")
339
+ iframe.setAttribute('frameborder', "0")
340
+ iframe.setAttribute('scrolling', "no")
341
+ iframe.setAttribute('allow', "camera *; microphone *")
342
+ iframeContainer.appendChild(iframe)
343
+ nfSoftphoneContainer.appendChild(iframeContainer)
344
+ nfSoftphone.appendChild(nfSoftphoneContainer)
345
+ nfSoftphone.appendChild(nfSoftphoneText)
346
+ const destination = (this.option.el ? this.getSoftphoneDomElement(this.option.el) ?? document.body : document.body)
347
+ destination.appendChild(nfSoftphone)
348
+ await nextTick()
349
+ const {
350
+ top,
351
+ left,
352
+ } = await this.refreshStyle()
353
+ const {
354
+ envMap,
355
+ env,
356
+ agentTag,
357
+ agentId,
358
+ appSecret,
359
+ extStatus,
360
+ appKey,
361
+ ancestorOrigin,
362
+ destinationOrigin,
363
+ callShow,
364
+ transferToLaborShow,
365
+ restShow,
366
+ networkDetectShow,
367
+ muteShow,
368
+ logShow,
369
+ timingShow,
370
+ signShow,
371
+ sign,
372
+ timestamp,
373
+ sgGateway,
374
+ sgBase,
375
+ sgOpen,
376
+ sgDomain,
377
+ } = this.option
378
+ let url = `${ envMap?.[env] ?? env }/#/?cross=1&open=1`
379
+ url += agentTag ? `&agentTag=${agentTag}` : ''
380
+ url += agentId ? `&agentId=${agentId}` : ''
381
+ url += appSecret ? `&appSecret=${appSecret}` : ''
382
+ url += sign ? `&sign=${sign}` : ''
383
+ url += timestamp ? `&timestamp=${timestamp}` : ''
384
+ url += sgGateway ? `&sgGateway=${sgGateway}` : ''
385
+ url += sgBase ? `&sgBase=${sgBase}` : ''
386
+ url += sgOpen ? `&sgOpen=${sgOpen}` : ''
387
+ url += sgDomain ? `&sgDomain=${sgDomain}` : ''
388
+ url += `&appKey=${appKey}&extStatus=${extStatus}&paddingTop=${top}px&paddingLeft=${left}px&ancestorOrigin=${ancestorOrigin}&destinationOrigin=${destinationOrigin}`
389
+ url += `&callShow=${callShow}`
390
+ url += `&transferToLaborShow=${transferToLaborShow}`
391
+ url += `&restShow=${restShow}`
392
+ url += `&networkDetectShow=${networkDetectShow}`
393
+ url += `&muteShow=${muteShow}`
394
+ url += `&logShow=${logShow}`
395
+ url += `&timingShow=${timingShow}`
396
+ url += `&signShow=${signShow}`
397
+ iframe.src = url
398
+ }
399
+
400
+ initSoftphone = (option) => {
401
+ this.option = option ? { ...this.option, ...option } :this.option
402
+ this.handlerInteractive()
403
+ this.handlerDomGenerater()
404
+ return this
405
+ }
406
+
407
+ postMessageCommunicate = (type, status) => {
408
+ const iframe = this.getSoftphoneDomElement('.nf-softphone-iframe')
409
+ if (iframe) {
410
+ iframe.contentWindow.postMessage(JSON.stringify({
411
+ origin: 'nf-softphone',
412
+ ancestorOrigin: this.option.ancestorOrigin,
413
+ destinationOrigin: this.option.destinationOrigin,
414
+ type,
415
+ data: {
416
+ status,
417
+ type: 'manual'
418
+ }
419
+ }), '*')
420
+ }
421
+ }
422
+ connect = () => {
423
+ this.postMessageCommunicate('softphone-connect', true)
424
+ }
425
+
426
+
427
+ disconnect = () => {
428
+ this.postMessageCommunicate('softphone-connect', false)
429
+ }
430
+
431
+ seatsStatusChange = (status) => {
432
+ this.postMessageCommunicate('softphone-seats-status-change', status)
433
+ }
434
+
435
+ accept = () => {
436
+ this.postMessageCommunicate('softphone-accept')
437
+ }
438
+ ignore = () => {
439
+ this.postMessageCommunicate('softphone-ignore')
440
+ }
441
+ hangup = () => {
442
+ this.postMessageCommunicate('softphone-hangup')
443
+ }
444
+ sendDtmf = () => {
445
+ this.postMessageCommunicate('softphone-send-dtmf')
446
+ }
447
+
448
+ removeTimer = () => {
449
+ clearTimeout(this.timer)
450
+ this.timer = undefined
451
+ }
452
+
453
+ destroy = () => {
454
+ window.removeEventListener('message', this.handlingCommunication, false);
455
+ if (this.option.scrollview) {
456
+ document.querySelector(this.option.scrollview)?.removeEventListener('scroll', this.handlingScroll)
457
+ document.querySelector(this.option.scrollview)?.removeEventListener('scroll', this.handlingScrollTransparent)
458
+ }
459
+ this.removeCssCode()
460
+ this.handlerDomRemove()
461
+ this.removeTimer()
462
+ }
463
+ }
464
+
465
+ const softphoneManager = new SoftphoneManager()
@@ -0,0 +1,123 @@
1
+ function generateUUID() { // Public Domain/MIT
2
+ let d = Date.now();
3
+ if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
4
+ d += performance.now(); //use high-precision timer if available
5
+ }
6
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
7
+ const r = (d + Math.random() * 16) % 16 | 0;
8
+ d = Math.floor(d / 16);
9
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
10
+ });
11
+ }
12
+
13
+ function debounce(func, wait, immediate) {
14
+ let timeout, args, context, timestamp, result
15
+
16
+ const later = function() {
17
+ // 据上一次触发时间间隔
18
+ const last = +new Date() - timestamp
19
+
20
+ // 上次被包装函数被调用时间间隔last小于设定时间间隔wait
21
+ if (last < wait && last > 0) {
22
+ timeout = setTimeout(later, wait - last)
23
+ } else {
24
+ timeout = null
25
+ // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
26
+ if (!immediate) {
27
+ result = func.apply(context, args)
28
+ if (!timeout) context = args = null
29
+ }
30
+ }
31
+ }
32
+
33
+ return function(...args) {
34
+ // @ts-ignore
35
+ context = this
36
+ timestamp = +new Date()
37
+ const callNow = immediate && !timeout
38
+ // 如果延时不存在,重新设定延时
39
+ if (!timeout) timeout = setTimeout(later, wait)
40
+ if (callNow) {
41
+ result = func.apply(context, args)
42
+ // @ts-ignore
43
+ context = args = null
44
+ }
45
+
46
+ return result
47
+ }
48
+ }
49
+
50
+ // 用于存储传入 nextTick 的回调函数
51
+ const callbacks = [];
52
+ // 标记是否已经安排了回调函数的执行
53
+ let pending = false;
54
+
55
+ // 执行队列中的所有回调函数
56
+ function flushCallbacks() {
57
+ pending = false;
58
+ // 复制一份当前的回调队列,避免在执行过程中添加新的回调影响循环
59
+ const copies = callbacks.slice(0);
60
+ callbacks.length = 0;
61
+ // 依次执行每个回调函数,并处理可能出现的异常
62
+ for (let i = 0; i < copies.length; i++) {
63
+ try {
64
+ copies[i]();
65
+ } catch (e) {
66
+ console.error('Error in nextTick callback:', e);
67
+ }
68
+ }
69
+ }
70
+
71
+ // 根据浏览器支持情况选择合适的异步执行方式
72
+ let timerFunc;
73
+ if (typeof Promise !== 'undefined') {
74
+ // 如果支持 Promise,使用 Promise 的 then 方法创建微任务
75
+ const p = Promise.resolve();
76
+ timerFunc = () => {
77
+ p.then(flushCallbacks);
78
+ };
79
+ } else if (typeof MutationObserver !== 'undefined') {
80
+ // 如果支持 MutationObserver,使用它创建微任务
81
+ let counter = 1;
82
+ const observer = new MutationObserver(flushCallbacks);
83
+ const textNode = document.createTextNode(String(counter));
84
+ observer.observe(textNode, {
85
+ characterData: true
86
+ });
87
+ timerFunc = () => {
88
+ counter = (counter + 1) % 2;
89
+ textNode.data = String(counter);
90
+ };
91
+ } else if (typeof setImmediate !== 'undefined') {
92
+ // 如果支持 setImmediate,使用它创建宏任务
93
+ timerFunc = () => {
94
+ setImmediate(flushCallbacks);
95
+ };
96
+ } else {
97
+ // 最后使用 setTimeout 作为兜底方案创建宏任务
98
+ timerFunc = () => {
99
+ setTimeout(flushCallbacks, 0);
100
+ };
101
+ }
102
+ // 实现 nextTick 函数,支持传入回调函数或返回 Promise
103
+ async function nextTick(cb) {
104
+ return new Promise((resolve) => {
105
+ // 将回调函数和 resolve 函数封装成一个新的回调
106
+ callbacks.push(() => {
107
+ if (cb) {
108
+ try {
109
+ cb();
110
+ } catch (e) {
111
+ console.error('Error in nextTick callback:', e);
112
+ }
113
+ }
114
+ // 当回调执行完毕后,resolve Promise
115
+ resolve(true);
116
+ });
117
+ // 如果当前没有正在执行的回调安排,安排一次新的执行
118
+ if (!pending) {
119
+ pending = true;
120
+ timerFunc();
121
+ }
122
+ });
123
+ }