ccPhoneBarSocket.js 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  1. /*
  2. * 呼叫中心电话工具条
  3. * author: easycallcenter365@126.com
  4. * 2025-04-08
  5. * 修改:增强断开清理,优化重连状态,修复isCalling死锁
  6. */
  7. import $ from './jquery-1.11.0.js';
  8. function _phoneBarObserver() {
  9. if (!(this instanceof _phoneBarObserver)) {
  10. return new _phoneBarObserver();
  11. }
  12. this.listeners = {};
  13. }
  14. _phoneBarObserver.prototype = {
  15. on: function(key, callback) { this.addListener(key, callback); },
  16. off: function(key, callback) { this.removeListener(key, callback); },
  17. addListener: function(key, callback) {
  18. if (!this.listeners[key]) this.listeners[key] = [];
  19. this.listeners[key].push(callback);
  20. },
  21. removeListener: function(key, callback) {
  22. if (this.listeners[key]) {
  23. if (callback) {
  24. const index = this.listeners[key].indexOf(callback);
  25. if (index > -1) this.listeners[key].splice(index, 1);
  26. } else {
  27. this.listeners[key] = [];
  28. }
  29. }
  30. },
  31. notifyAll: function(key, info) {
  32. var keys = [key];
  33. if (key != null && String(key) !== key) keys.push(String(key));
  34. keys.forEach(function(keyName) {
  35. if (keyName == null || !this.listeners[keyName]) return;
  36. for (var i = 0; i < this.listeners[keyName].length; i++) {
  37. if (typeof this.listeners[keyName][i] === 'function') {
  38. this.listeners[keyName][i](info);
  39. }
  40. }
  41. }, this);
  42. },
  43. make: function(o) {
  44. for (var i in this) {
  45. o[i] = this[i];
  46. o.listeners = {};
  47. }
  48. }
  49. };
  50. "use strict";
  51. function ccPhoneBarSocket() {
  52. var observer = new _phoneBarObserver();
  53. observer.make(this);
  54. var _cc = this;
  55. var ws = null;
  56. var wsuri = null;
  57. var _connectPending = false;
  58. var _sendQueue = [];
  59. var _kickedByOtherDevice = false;
  60. var _everConnected = false;
  61. var _reconnectSeq = 0;
  62. var _reconnectAttemptAt = 0;
  63. var _reconnect201RetryCount = 0;
  64. var _staleCloseTimer = null;
  65. var MAX_201_RETRY = 5;
  66. var CONNECT_RACE_WINDOW_MS = 12000;
  67. var STALE_CLOSE_DELAY_MS = 400;
  68. var RECOVERING_CALL_CLEAR_MS = 5000;
  69. var RETRY_201_BASE_MS = 400;
  70. var RETRY_201_STEP_MS = 300;
  71. var isConnected = false;
  72. var callConnected = false;
  73. var canSendVideoReInvite = false;
  74. this.subscribeAgentListStarted = false;
  75. this.iframe = null;
  76. this.isCalling = false; // 防止重复外呼
  77. this._intentionalDisconnect = false;
  78. this.resetOutboundLock = function() {
  79. this.isCalling = false;
  80. };
  81. this._activeCallUuid = null;
  82. this._recoveringCall = false;
  83. this._suppressSessionNotFound = false;
  84. this.setActiveCallUuid = function(uuid) {
  85. if (uuid) this._activeCallUuid = String(uuid);
  86. };
  87. this.getActiveCallUuid = function() {
  88. return this._activeCallUuid;
  89. };
  90. this.clearActiveCallUuid = function() {
  91. this._activeCallUuid = null;
  92. this._recoveringCall = false;
  93. };
  94. this._captureCallUuid = function(msg) {
  95. if (msg && msg.object && msg.object.uuid) {
  96. this.setActiveCallUuid(msg.object.uuid);
  97. }
  98. };
  99. this.restoreCallSession = function() {
  100. if (!this._activeCallUuid) return;
  101. if (!ws || ws.readyState !== WebSocket.OPEN) return;
  102. console.log('[IPCC] 重连后恢复本地通话状态 uuid=', this._activeCallUuid);
  103. this._recoveringCall = true;
  104. callConnected = true;
  105. };
  106. this.callConfig = {
  107. 'useDefaultUi': false,
  108. 'ipccServer': 'sip.ylrzcloud.com',
  109. 'enableWss': true,
  110. 'callCodec': 'pcma',
  111. 'enableHeartBeat': true,
  112. 'heartBeatIntervalSecs': 16,
  113. 'useSameAudioCodeForOutbound': true,
  114. 'loginToken': '',
  115. 'gatewayList': [{
  116. uuid: '01',
  117. updateTime: 1675953810492,
  118. gatewayAddr: '113.207.68.30:5543;external',
  119. callerNumber: '02386990208',
  120. calleePrefix: '',
  121. priority: 1,
  122. concurrency: 1000,
  123. register: false,
  124. audioCodec: 'g711'
  125. }],
  126. 'gatewayEncrypted': false,
  127. 'fsServer': '129.28.164.235:5060',
  128. 'groupId': '1',
  129. 'groups': null,
  130. 'agentList': null,
  131. 'phoneType': 'EyeBeam',
  132. 'webPhoneUrl': 'None',
  133. 'localHostProxyVersion': 'v20221130_1736',
  134. 'localHostProxyPort': 8888
  135. };
  136. this.insertAfter = function(newElement, targetElement) {
  137. var parent = targetElement.parentNode;
  138. if (parent.lastChild === targetElement) parent.appendChild(newElement);
  139. else parent.insertBefore(newElement, targetElement.nextSibling);
  140. };
  141. this.trim = function(str) { return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); };
  142. this.getIsConnected = function() { return isConnected; };
  143. this.getCallConnected = function() { return callConnected; };
  144. this.setCallConnected = function(value) { callConnected = value; };
  145. this.setCanSendVideoReInvite = function(value) { canSendVideoReInvite = value; };
  146. this.getCanSendVideoReInvite = function() { return canSendVideoReInvite; };
  147. this.heartbeatTimer = null;
  148. // 与 phone-bar-ex.html 一致:status 200 / 重连后重置心跳定时器
  149. this.setHeartbeat = function() {
  150. if (this.heartbeatTimer) {
  151. clearInterval(this.heartbeatTimer);
  152. this.heartbeatTimer = null;
  153. }
  154. if (!_cc.callConfig.enableHeartBeat) return;
  155. var intervalSec = _cc.callConfig.heartBeatIntervalSecs || 16;
  156. this.heartbeatTimer = setInterval(function() {
  157. if (!_cc.callConfig.enableHeartBeat) return;
  158. if (!_cc.getIsConnected()) return;
  159. if (!ws || ws.readyState !== WebSocket.OPEN) {
  160. console.warn('[heartbeat] WebSocket未连接,停止心跳');
  161. if (_cc.heartbeatTimer) {
  162. clearInterval(_cc.heartbeatTimer);
  163. _cc.heartbeatTimer = null;
  164. }
  165. return;
  166. }
  167. console.debug('[heartbeat] 发送心跳');
  168. try {
  169. _cc.sendMsg({ action: 'setHearBeat', body: '{}' });
  170. } catch (e) {
  171. console.error('[heartbeat] 发送心跳失败:', e);
  172. if (_cc.heartbeatTimer) {
  173. clearInterval(_cc.heartbeatTimer);
  174. _cc.heartbeatTimer = null;
  175. }
  176. }
  177. }, intervalSec * 1000);
  178. console.log('[heartbeat] 心跳已启动,间隔:', intervalSec, '秒');
  179. };
  180. this.loadScript = function(destUrl, callbackFunc) {
  181. var script = document.createElement("script");
  182. script.type = "text/javascript";
  183. script.src = destUrl;
  184. if (null != callbackFunc) script.onload = function() { callbackFunc(); };
  185. document.getElementsByTagName('head')[0].appendChild(script);
  186. };
  187. this.initConfig = function(config) {
  188. for (var element in config) this.callConfig[element] = config[element];
  189. var _loginToken = this.callConfig["loginToken"];
  190. if (typeof(_loginToken) == "undefined" || _loginToken === "") {
  191. alert("电话工具条:无法获取 loginToken!");
  192. return;
  193. }
  194. console.log("callConfig:", JSON.stringify(this.callConfig));
  195. if (_cc.callConfig.enableWss) {
  196. wsuri = 'wss://' + this.callConfig.ipccServer +
  197. '/call-center/websocketServer?' +
  198. '&loginToken=' + this.callConfig.loginToken;
  199. } else {
  200. wsuri = 'ws://129.28.164.235:1081/call-center/websocketServer?' +
  201. '&loginToken=' + this.callConfig.loginToken;
  202. }
  203. console.log("ipccServer url: " + wsuri);
  204. var ipccServerIpAddr = this.callConfig.ipccServer.split(":");
  205. if (this.callConfig.enableWss && this.checkIP(ipccServerIpAddr)) {
  206. var tipsError = "ERROR! 启用了wss之后,必须使用域名访问websocketServer! " + this.callConfig.ipccServer;
  207. console.log(tipsError);
  208. alert(tipsError);
  209. }
  210. if (this.callConfig.enableHeartBeat) this.setHeartbeat();
  211. if (_cc.callConfig["useDefaultUi"]) this.initPhoneBarUI();
  212. };
  213. this.checkIP = function(ip) {
  214. var re = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
  215. return re.test(ip);
  216. };
  217. this.disconnect = function() {
  218. if (callConnected) {
  219. console.warn('[disconnect] 通话进行中,拒绝断开 IPPC 连接');
  220. return;
  221. }
  222. _cc._performDisconnect();
  223. };
  224. /** 强制断开(清理/切线路时使用,忽略通话态) */
  225. this.forceDisconnect = function() {
  226. callConnected = false;
  227. _cc._activeCallUuid = null;
  228. _cc._recoveringCall = false;
  229. _cc._performDisconnect();
  230. };
  231. this._performDisconnect = function() {
  232. console.log('[disconnect] 开始断开IPCC连接');
  233. this._intentionalDisconnect = true;
  234. if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
  235. if (!ws || ws.readyState !== WebSocket.OPEN) {
  236. if (ws) {
  237. ws.onmessage = null;
  238. ws.onclose = null;
  239. ws.onopen = null;
  240. ws.onerror = null;
  241. ws.close();
  242. ws = null;
  243. }
  244. isConnected = false;
  245. _connectPending = false;
  246. this.isCalling = false;
  247. return;
  248. }
  249. try {
  250. ws.onmessage = null;
  251. ws.onclose = null;
  252. ws.onopen = null;
  253. ws.onerror = null;
  254. var cmdInfo = {};
  255. cmdInfo.action = "setAgentStatus";
  256. cmdInfo.body = { "cmd": "disconnect", "args": { "msg": "disconnection opt triggered by js client." } };
  257. _cc.sendMsg(cmdInfo);
  258. console.log('[disconnect] 已发送断开指令');
  259. } catch(e) { console.error('[disconnect] 发送断开指令失败:', e); }
  260. try { ws.close(); } catch(e) { console.error('[disconnect] 关闭WebSocket失败:', e); }
  261. ws = null;
  262. isConnected = false;
  263. _connectPending = false;
  264. this.isCalling = false;
  265. console.log('[disconnect] IPCC连接已完全断开');
  266. };
  267. /** 断开并等待 WebSocket 关闭,避免同 token 新连接与旧会话冲突导致 201 */
  268. this.disconnectAndWait = function(timeoutMs) {
  269. timeoutMs = timeoutMs || 2000;
  270. var _ccRef = this;
  271. return new Promise(function(resolve) {
  272. if (callConnected) {
  273. console.warn('[disconnectAndWait] 通话进行中,跳过断开');
  274. resolve(false);
  275. return;
  276. }
  277. var finished = false;
  278. function done() {
  279. if (finished) return;
  280. finished = true;
  281. _ccRef._intentionalDisconnect = true;
  282. _connectPending = false;
  283. isConnected = false;
  284. _ccRef.isCalling = false;
  285. resolve(true);
  286. }
  287. var timer = setTimeout(done, timeoutMs);
  288. if (_ccRef.heartbeatTimer) {
  289. clearInterval(_ccRef.heartbeatTimer);
  290. _ccRef.heartbeatTimer = null;
  291. }
  292. if (!ws) {
  293. clearTimeout(timer);
  294. done();
  295. return;
  296. }
  297. var closing = ws;
  298. var state = closing.readyState;
  299. if (state === WebSocket.CLOSED) {
  300. ws = null;
  301. clearTimeout(timer);
  302. done();
  303. return;
  304. }
  305. try {
  306. closing.onmessage = null;
  307. closing.onopen = null;
  308. closing.onerror = null;
  309. closing.onclose = function() {
  310. if (ws === closing) ws = null;
  311. clearTimeout(timer);
  312. done();
  313. };
  314. if (state === WebSocket.OPEN) {
  315. try {
  316. closing.send(JSON.stringify({
  317. action: "setAgentStatus",
  318. body: { cmd: "disconnect", args: { msg: "disconnection opt triggered by js client." } }
  319. }));
  320. } catch (e) {}
  321. }
  322. closing.close();
  323. } catch (e) {
  324. if (ws === closing) ws = null;
  325. clearTimeout(timer);
  326. done();
  327. }
  328. });
  329. };
  330. this.getExtNum = function() { return this.callConfig.extnum; };
  331. this.getOpNum = function() { return this.callConfig.opnum; };
  332. this.getGroupId = function() { return this.callConfig.groupId; };
  333. this.getGroups = function() { return JSON.parse(JSON.stringify(this.callConfig.groups)); };
  334. this.findAgentByOpNum = function(opnum) {
  335. if (_cc.callConfig.agentList == null) return null;
  336. for (var key in _cc.callConfig.agentList) {
  337. let item = _cc.callConfig.agentList[key];
  338. if (item["opnum"] === opnum) {
  339. item["_arrayIndex"] = key;
  340. return item;
  341. }
  342. }
  343. return null;
  344. };
  345. this.updateAgentList = function(agentList) {
  346. for (var key in agentList) {
  347. let item = agentList[key];
  348. var existItem = this.findAgentByOpNum(item["opnum"]);
  349. if (existItem != null) {
  350. var newStatus = item["agentStatus"];
  351. var oldStatus = existItem["agentStatus"];
  352. var logoutTime = item["logoutTime"];
  353. if (logoutTime > 0) {
  354. console.info("delete offline user", item["opnum"], "index=", key);
  355. _cc.callConfig.agentList.splice(existItem["_arrayIndex"], 1);
  356. continue;
  357. }
  358. if (newStatus != oldStatus) existItem["agentStatus"] = newStatus;
  359. } else {
  360. if (_cc.callConfig.agentList != null) _cc.callConfig.agentList.push(item);
  361. }
  362. }
  363. };
  364. function _isCurrentSocket(socket) {
  365. return ws === socket;
  366. }
  367. function _safeSendOnSocket(socket, jsonObject) {
  368. if (_kickedByOtherDevice) {
  369. console.warn('[sendMsg] 账号已在其他设备登录,拒绝发送');
  370. return false;
  371. }
  372. if (!socket) return false;
  373. var state = socket.readyState;
  374. if (state === WebSocket.OPEN) {
  375. try {
  376. socket.send(JSON.stringify(jsonObject));
  377. return true;
  378. } catch (e) {
  379. console.error('[sendMsg] 发送失败:', e);
  380. return false;
  381. }
  382. }
  383. if (state === WebSocket.CONNECTING && socket === ws) {
  384. _sendQueue.push(jsonObject);
  385. return true;
  386. }
  387. console.warn('[sendMsg] WebSocket 未就绪, readyState=', state);
  388. return false;
  389. }
  390. function _flushSendQueue(socket) {
  391. if (!_isCurrentSocket(socket) || socket.readyState !== WebSocket.OPEN) return;
  392. while (_sendQueue.length) {
  393. try {
  394. socket.send(JSON.stringify(_sendQueue.shift()));
  395. } catch (e) {
  396. console.error('[sendMsg] 刷新发送队列失败:', e);
  397. break;
  398. }
  399. }
  400. }
  401. this.resetKickedState = function() {
  402. _kickedByOtherDevice = false;
  403. _reconnect201RetryCount = 0;
  404. };
  405. this._handleKickedByOtherDevice = function(msg, sourceSocket) {
  406. if (sourceSocket && ws && ws !== sourceSocket) {
  407. if (isConnected && ws.readyState === WebSocket.OPEN) {
  408. console.warn('[IPCC] 已有有效连接,忽略过期连接的201');
  409. try {
  410. sourceSocket.onmessage = null;
  411. sourceSocket.onclose = null;
  412. sourceSocket.close();
  413. } catch (e) {}
  414. return;
  415. }
  416. }
  417. if (isConnected && ws && ws.readyState === WebSocket.OPEN && (!sourceSocket || sourceSocket === ws)) {
  418. console.error('[IPCC] 当前连接收到201,确认被踢下线');
  419. }
  420. console.error('[IPCC] 用户已在其他设备登录(201),停止重连');
  421. _kickedByOtherDevice = true;
  422. _connectPending = false;
  423. _cc._intentionalDisconnect = true;
  424. isConnected = false;
  425. _sendQueue = [];
  426. if (_cc.heartbeatTimer) { clearInterval(_cc.heartbeatTimer); _cc.heartbeatTimer = null; }
  427. _cc.notifyAll(ccPhoneBarSocket.eventList.user_login_on_other_device, msg);
  428. var closing = sourceSocket || ws;
  429. if (closing) {
  430. try {
  431. closing.onmessage = null;
  432. closing.onclose = null;
  433. closing.onopen = null;
  434. closing.onerror = null;
  435. closing.close();
  436. } catch (e) {}
  437. if (ws === closing) ws = null;
  438. }
  439. };
  440. function _scheduleOpenAfterStale(mySeq, delayMs) {
  441. if (_staleCloseTimer) clearTimeout(_staleCloseTimer);
  442. _staleCloseTimer = setTimeout(function() {
  443. _staleCloseTimer = null;
  444. if (mySeq !== _reconnectSeq || _kickedByOtherDevice) {
  445. _connectPending = false;
  446. return;
  447. }
  448. _createWebSocketConnection();
  449. }, delayMs);
  450. }
  451. this.connect = function() {
  452. console.log('[connect] 开始建立IPCC WebSocket连接');
  453. if (_kickedByOtherDevice) {
  454. console.warn('[connect] 已在其他设备登录,停止重连');
  455. return;
  456. }
  457. if (ws && ws.readyState === WebSocket.OPEN) {
  458. if (callConnected) {
  459. console.log('[connect] WebSocket 已连接且通话中,跳过重连');
  460. return;
  461. }
  462. console.log('[connect] WebSocket 已连接,无需重复连接');
  463. _connectPending = false;
  464. return;
  465. }
  466. if (ws && ws.readyState === WebSocket.CONNECTING) {
  467. console.log('[connect] WebSocket 正在连接中,请勿重复操作');
  468. return;
  469. }
  470. if (_connectPending) {
  471. console.log('[connect] 连接任务已排队,跳过重复 connect');
  472. return;
  473. }
  474. _connectPending = true;
  475. _reconnectAttemptAt = Date.now();
  476. _reconnectSeq++;
  477. var mySeq = _reconnectSeq;
  478. this._intentionalDisconnect = false;
  479. if (ws) {
  480. console.log('[connect] 检测到旧连接,等待关闭后再建连, readyState:', ws.readyState);
  481. var stale = ws;
  482. isConnected = false;
  483. _sendQueue = [];
  484. try {
  485. stale.onmessage = null;
  486. stale.onopen = null;
  487. stale.onerror = null;
  488. stale.onclose = function(evt) {
  489. if (ws === stale) ws = null;
  490. console.log('[connect] 旧连接已关闭, code=', evt && evt.code);
  491. if (mySeq !== _reconnectSeq) {
  492. _connectPending = false;
  493. return;
  494. }
  495. _scheduleOpenAfterStale(mySeq, STALE_CLOSE_DELAY_MS);
  496. };
  497. if (stale.readyState === WebSocket.OPEN || stale.readyState === WebSocket.CONNECTING) {
  498. stale.close();
  499. } else if (ws === stale) {
  500. ws = null;
  501. _scheduleOpenAfterStale(mySeq, 400);
  502. }
  503. } catch (e) {
  504. console.error('[connect] 关闭旧连接失败:', e);
  505. if (ws === stale) ws = null;
  506. _scheduleOpenAfterStale(mySeq, STALE_CLOSE_DELAY_MS);
  507. }
  508. return;
  509. }
  510. _scheduleOpenAfterStale(mySeq, 100);
  511. };
  512. this.reconnect = function() {
  513. this.connect();
  514. };
  515. function _createWebSocketConnection() {
  516. console.log('[connect] 开始创建新的WebSocket连接');
  517. if (!('WebSocket' in window)) {
  518. _connectPending = false;
  519. console.log('您的浏览器不支持 websocket,您无法使用本页面的功能!');
  520. return;
  521. }
  522. var socket = new WebSocket(wsuri);
  523. ws = socket;
  524. _sendQueue = [];
  525. socket.onmessage = function(evt) {
  526. if (!_isCurrentSocket(socket)) return;
  527. console.log("recv msg from websocket server: ", evt.data);
  528. var msg = JSON.parse(evt.data);
  529. console.log("parsed json data:", msg);
  530. var resp_status = msg["status"];
  531. switch (resp_status) {
  532. case 200:
  533. _everConnected = true;
  534. _connectPending = false;
  535. _reconnect201RetryCount = 0;
  536. isConnected = true;
  537. _cc.callConfig.extnum = msg.object["extnum"];
  538. _cc.callConfig.opnum = msg.object["opnum"];
  539. _cc.callConfig.groupId = msg.object["groupId"];
  540. _cc.callConfig.groups = msg.object["groups"];
  541. console.log('ipcc 连接成功.', 'connected_ipcc_server');
  542. _cc.notifyAll(ccPhoneBarSocket.eventList.ws_connected, msg);
  543. if (callConnected || _cc._activeCallUuid) {
  544. _cc.restoreCallSession();
  545. setTimeout(function() {
  546. if (_cc.getIsConnected()) _cc._recoveringCall = false;
  547. }, RECOVERING_CALL_CLEAR_MS);
  548. } else {
  549. _safeSendOnSocket(socket, {
  550. action: "setAgentStatus",
  551. body: { cmd: "setStatus", args: { status: ccPhoneBarSocket.agentStatusEnum.busy } }
  552. });
  553. }
  554. _cc.setHeartbeat();
  555. break;
  556. default:
  557. if (parseInt(resp_status) === 201) {
  558. if (isConnected && ws && ws !== socket && ws.readyState === WebSocket.OPEN) {
  559. console.warn('[IPCC] 有效连接已建立,忽略迟到的201');
  560. try { socket.close(); } catch (e) {}
  561. return;
  562. }
  563. var isConnectRace = (Date.now() - _reconnectAttemptAt < CONNECT_RACE_WINDOW_MS) &&
  564. _reconnect201RetryCount < MAX_201_RETRY;
  565. if (isConnectRace && _isCurrentSocket(socket)) {
  566. _reconnect201RetryCount++;
  567. console.warn('[IPCC] 连接竞态201,第', _reconnect201RetryCount, '次自动重试');
  568. _connectPending = false;
  569. isConnected = false;
  570. try {
  571. socket.onmessage = null;
  572. socket.onclose = null;
  573. socket.onopen = null;
  574. socket.onerror = null;
  575. socket.close();
  576. } catch (e) {}
  577. if (ws === socket) ws = null;
  578. setTimeout(function() {
  579. if (!_kickedByOtherDevice && !(isConnected && ws && ws.readyState === WebSocket.OPEN)) {
  580. _cc.connect();
  581. }
  582. }, RETRY_201_BASE_MS + _reconnect201RetryCount * RETRY_201_STEP_MS);
  583. return;
  584. }
  585. _cc._handleKickedByOtherDevice(msg, socket);
  586. return;
  587. }
  588. _cc._captureCallUuid(msg);
  589. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.asr_result_generate)) {
  590. _cc._recoveringCall = false;
  591. }
  592. var hangupCode = parseInt(resp_status);
  593. var isHangupEvent = hangupCode === parseInt(ccPhoneBarSocket.eventList.caller_hangup) ||
  594. hangupCode === parseInt(ccPhoneBarSocket.eventList.callee_hangup) ||
  595. hangupCode === parseInt(ccPhoneBarSocket.eventList.CONFERENCE_MODERATOR_HANGUP);
  596. if (isHangupEvent) {
  597. _cc.notifyAll(resp_status, msg);
  598. }
  599. if (parseInt(resp_status) === 400) {
  600. var errText = (msg["msg"] || "").toString();
  601. if (errText.indexOf("method not support") >= 0) {
  602. console.debug('[SDK] 服务端不支持的方法,已忽略:', errText);
  603. break;
  604. }
  605. if (errText.indexOf("通话不存在") >= 0 || errText.indexOf("不存在") >= 0) {
  606. if (_cc._suppressSessionNotFound || !_cc.getCallConnected()) {
  607. console.debug('[SDK] 挂机后会话已不存在,已忽略:', errText);
  608. } else {
  609. console.warn('[SDK] 服务端无通话会话:', errText);
  610. _cc.notifyAll('call_session_not_found', msg);
  611. }
  612. } else {
  613. _cc.isCalling = false;
  614. console.log('[SDK] 请求参数错误, status: 400', errText);
  615. }
  616. } else if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.server_error)) {
  617. _cc.isCalling = false;
  618. console.log('[SDK] 发生错误,已重置 isCalling = false, status:', resp_status);
  619. }
  620. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.caller_hangup) ||
  621. parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.CONFERENCE_MODERATOR_HANGUP)) {
  622. _cc.setCallConnected(false);
  623. _cc.setCanSendVideoReInvite(false);
  624. _cc.callConfig.agentList = null;
  625. _cc.unSubscribeAgentList();
  626. _cc.isCalling = false;
  627. _cc.clearActiveCallUuid();
  628. console.log("caller_hangup");
  629. }
  630. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.caller_answered)) {
  631. // 外呼:600=分机已接通,客户仍在振铃;桥接期需标记通话活跃,避免误判挂机
  632. _cc.setCallConnected(true);
  633. }
  634. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.callee_hangup)) {
  635. _cc.isCalling = false;
  636. _cc.setCallConnected(false);
  637. _cc.clearActiveCallUuid();
  638. }
  639. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.extension_off_line)) {
  640. _cc.isCalling = false;
  641. if (_cc.getCallConnected()) {
  642. _cc.setCallConnected(false);
  643. _cc.clearActiveCallUuid();
  644. }
  645. }
  646. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.callee_answered)) {
  647. _cc.setCallConnected(true);
  648. _cc._recoveringCall = false;
  649. console.log("callee_answered");
  650. }
  651. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.OUTBOUND_START)) {
  652. _cc.setCallConnected(true);
  653. }
  654. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.callee_busy)) {
  655. console.warn("callee_busy or rejected");
  656. _cc.isCalling = false;
  657. }
  658. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.new_inbound_call)) {
  659. _cc.setCallConnected(true);
  660. console.log("new_inbound_call");
  661. }
  662. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.caller_answered) ||
  663. parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.callee_answered)) {
  664. if (msg["object"]["callType"] === "audio") {
  665. _cc.setCanSendVideoReInvite(true);
  666. _cc.notifyAll(ccPhoneBarSocket.eventList.on_audio_call_connected, "audio call connected.");
  667. console.log("current callType is audio.");
  668. }
  669. if (msg["object"]["callType"] === "video") {
  670. _cc.setCanSendVideoReInvite(false);
  671. _cc.notifyAll(ccPhoneBarSocket.eventList.on_video_call_connected, "video call connected.");
  672. console.log("current callType is video.");
  673. }
  674. }
  675. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_hold)) _cc.changeUiOnHold();
  676. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_unhold)) _cc.changeUiOnUnHold();
  677. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_on_hold_hangup)) {
  678. _cc.changeUiOnUnHold();
  679. $("#holdBtn").removeClass('on');
  680. $("#callStatus").text("保持的通话已挂机.");
  681. }
  682. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_call_wait)) {
  683. $("#stopCallWait").show();
  684. $("#doConsultationBtn").hide();
  685. $("#callStatus").text("客户电话等待中.");
  686. _cc.showTransferAreaUI();
  687. }
  688. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_off_call_wait)) {
  689. $("#stopCallWait").hide();
  690. $("#transferCallWait").hide();
  691. _cc.hideTransferAreaUI();
  692. $("#callStatus").text("等待的电话已接回.");
  693. }
  694. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.inner_consultation_start)) {
  695. $("#callStatus").text("咨询已开始.");
  696. $("#transferCallWait").show();
  697. }
  698. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.inner_consultation_stop)) {
  699. $("#callStatus").text("咨询已结束.");
  700. $("#transferCallWait").hide();
  701. }
  702. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_on_call_wait_hangup)) {
  703. $("#stopCallWait").hide();
  704. $("#transferCallWait").hide();
  705. $("#callStatus").text("等待的客户已挂机.");
  706. }
  707. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.inner_consultation_start)) {
  708. $("#holdBtn").removeClass('on');
  709. $("#hangUpBtn").removeClass('on');
  710. $("#transferBtn").removeClass('on');
  711. }
  712. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.transfer_call_success)) {
  713. var extNum = msg["object"]["callee"];
  714. if (extNum === _cc.getExtNum()) {
  715. $("#holdBtn").addClass('on');
  716. $("#hangUpBtn").addClass('on');
  717. $("#transferBtn").addClass('on');
  718. }
  719. }
  720. if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.agent_status_data_changed)) {
  721. if (_cc.subscribeAgentListStarted) {
  722. if (_cc.callConfig.agentList == null) _cc.callConfig.agentList = JSON.parse(msg.object);
  723. else _cc.updateAgentList(JSON.parse(msg.object));
  724. }
  725. }
  726. if (!isHangupEvent) {
  727. _cc.notifyAll(resp_status, msg);
  728. }
  729. break;
  730. }
  731. };
  732. socket.onclose = function(evt) {
  733. if (!_isCurrentSocket(socket)) {
  734. console.log('[onclose] 忽略已作废连接的关闭事件');
  735. return;
  736. }
  737. console.log('[onclose] IPCC WebSocket关闭事件触发');
  738. console.log('[onclose] evt.code:', evt.code, ', evt.reason:', evt.reason, ', evt.wasClean:', evt.wasClean);
  739. const intentional = _cc._intentionalDisconnect || _kickedByOtherDevice;
  740. _cc._intentionalDisconnect = false;
  741. _connectPending = false;
  742. isConnected = false;
  743. _sendQueue = [];
  744. if (_cc.heartbeatTimer) { clearInterval(_cc.heartbeatTimer); _cc.heartbeatTimer = null; }
  745. ws = null;
  746. if (_kickedByOtherDevice) {
  747. console.log('[onclose] 其他设备登录导致断开');
  748. return;
  749. }
  750. if (!callConnected && !_cc._activeCallUuid) {
  751. _cc.isCalling = false;
  752. }
  753. if (intentional || (evt.wasClean && evt.code === 1000)) {
  754. console.log('[onclose] 主动/正常关闭,不触发异常断开事件');
  755. } else {
  756. console.warn('[onclose] 异常断开,通知前端');
  757. _cc.notifyAll(ccPhoneBarSocket.eventList.ws_disconnected, "ipccserver 连接断开.");
  758. }
  759. console.log('[onclose] 连接状态已更新');
  760. };
  761. socket.onerror = function(evt) {
  762. if (!_isCurrentSocket(socket)) return;
  763. console.warn('[onerror] IPCC WebSocket 错误', evt);
  764. };
  765. socket.onopen = function(evt) {
  766. if (!_isCurrentSocket(socket)) return;
  767. console.log('ipccserver websocket onopen...');
  768. _flushSendQueue(socket);
  769. };
  770. }
  771. this.sendMsg = function(jsonObject) {
  772. console.debug("ws.send:", jsonObject);
  773. return _safeSendOnSocket(ws, jsonObject);
  774. };
  775. this.changeUiOnHold = function() { $("#holdBtnLi").hide(); $("#unHoldBtnLi").show(); $("#unHoldBtn").addClass('on'); };
  776. this.changeUiOnUnHold = function() { $("#holdBtnLi").show(); $("#holdBtn").addClass('on'); $("#unHoldBtnLi").hide(); };
  777. this.hideTransferAreaUI = function() { var transferArea = document.getElementById("transfer_area"); if (transferArea) transferArea.style.display = "none"; };
  778. this.showTransferAreaUI = function() { var transferArea = document.getElementById("transfer_area"); if (transferArea) transferArea.style.display = "block"; };
  779. this.setStatusFree = function() { this.setStatus(ccPhoneBarSocket.agentStatusEnum.free); };
  780. this.setStatusBusy = function() { this.setStatus(ccPhoneBarSocket.agentStatusEnum.busy); };
  781. ccPhoneBarSocket.utils = {
  782. getQueryParam: function(paramName) {
  783. const url = window.location.href;
  784. const params = new URLSearchParams(new URL(url).search);
  785. return params.get(paramName);
  786. }
  787. };
  788. ccPhoneBarSocket.agentStatusEnum = {
  789. "justLogin": 1, "free": 2, "busy": 3, "busy_rest": 31,
  790. "busy_meeting": 32, "busy_training": 33, "incall": 4, "fill_form": 5, "conference": 6
  791. };
  792. ccPhoneBarSocket.videoLevels = {
  793. "Smooth": { "levelId": "42e00b", "description": "流畅" },
  794. "Smooth2": { "levelId": "42e00c", "description": "流畅+" },
  795. "Smooth3": { "levelId": "42e00d", "description": "流畅++" },
  796. "Clear": { "levelId": "42e014", "description": "清晰" },
  797. "Clear2": { "levelId": "42e015", "description": "清晰+" },
  798. "Clear3": { "levelId": "42e016", "description": "清晰++" },
  799. "HD": { "levelId": "42e01e", "description": "高清" },
  800. "HD2": { "levelId": "42e01f", "description": "高清+" }
  801. };
  802. ccPhoneBarSocket.eventList = {
  803. "on_audio_call_connected": "100", "on_video_call_connected": "101", "ws_connected": "200",
  804. "user_login_on_other_device": "201", "ws_disconnected": "202", "call_session_status_data_changed": "203",
  805. "agent_status_data_changed": "205", "request_args_error": "400", "unauthorized": "401",
  806. "server_error": "500", "server_error_audio_codec_not_match": "501", "caller_answered": "600",
  807. "caller_hangup": "601", "caller_busy": "602", "caller_not_login": "603", "caller_respond_timeout": "604",
  808. "callee_answered": "605", "callee_hangup": "606", "callee_ringing": "607", "status_changed": "608",
  809. "callee_busy": "609", "outbound_finished": "611", "PREDICTIVE_CALL_INBOUND": "612",
  810. "new_inbound_call": "613", "acd_group_queue_number": "615", "transfer_call_recv": 616,
  811. "lock_agent_fail": 617, "transfer_call_success": 618, "asr_result_generate": 619,
  812. "asr_process_end_agent": 620, "asr_process_end_customer": 621, "asr_process_started": 622,
  813. "customer_channel_hold": 623, "customer_channel_unhold": 624, "customer_on_hold_hangup": 625,
  814. "inner_consultation_request": 626, "customer_channel_call_wait": 627, "customer_channel_off_call_wait": 628,
  815. "customer_on_call_wait_hangup": 629, "extension_on_line": 630, "extension_off_line": 631,
  816. "inner_consultation_start": 632, "inner_consultation_stop": 633, "conference_repeat_callee": 660,
  817. "CONFERENCE_CALL_MODERATOR_TIMEOUT": 661, "CONFERENCE_MEMBER_ANSWERED": 662,
  818. "CONFERENCE_MEMBER_HANGUP": 663, "CONFERENCE_MEMBER_MUTED_SUCCESS": 666,
  819. "CONFERENCE_MEMBER_MUTED_FAILED": 665, "CONFERENCE_MEMBER_UNMUTED_SUCCESS": 667,
  820. "CONFERENCE_MEMBER_UNMUTED_FAILED": 668, "CONFERENCE_MEMBER_NOT_EXISTS": "669",
  821. "CONFERENCE_MODERATOR_RESET": "670", "CONFERENCE_MODERATOR_ANSWERED": "671",
  822. "CONFERENCE_MODERATOR_HANGUP": "672", "CONFERENCE_MEMBER_VMUTED_SUCCESS": "674",
  823. "CONFERENCE_MEMBER_UnVMUTED_SUCCESS": "676", "CONFERENCE_MEMBER_UnVMUTED_FAILED": "677",
  824. "CONFERENCE_TRANSFER_SUCCESS_FROM_EXISTED_CALL": "678", "OUTBOUND_START": "679"
  825. };
  826. this.createIframe = function(src) {
  827. var _iframe = document.createElement("iframe");
  828. _iframe.style.width = '0';
  829. _iframe.style.height = '0';
  830. _iframe.style.margin = '0';
  831. _iframe.style.padding = '0';
  832. _iframe.style.overflow = 'hidden';
  833. _iframe.style.border = 'none';
  834. _iframe.src = src;
  835. document.body.appendChild(_iframe);
  836. _cc.iframe = _iframe;
  837. };
  838. this.openSoftPhone = function() {
  839. var softPhoneUrl = "http://127.0.0.1:" + _cc.callConfig["localHostProxyPort"] +
  840. "/autoSetExtension?server=" + encodeURIComponent(_cc.callConfig["fsServer"].split(':')[0]) +
  841. "&port=" + _cc.callConfig["fsServer"].split(':')[1] +
  842. "&extnum=" + _cc.callConfig["extnum"] +
  843. "&pass=" + encodeURIComponent(_cc.callConfig["extPassword"]) +
  844. "&phoneType=" + _cc.callConfig["phoneType"] +
  845. "&version=" + encodeURIComponent(_cc.callConfig["localHostProxyVersion"]) +
  846. "&webPhoneUrl=" + encodeURIComponent(_cc.callConfig["webPhoneUrl"]);
  847. console.log("softPhoneUrl:", softPhoneUrl);
  848. _cc.iframe.src = softPhoneUrl;
  849. };
  850. this.setStatus = function(status) {
  851. this.sendMsg({
  852. action: "setAgentStatus",
  853. body: { cmd: "setStatus", args: { status: status } }
  854. });
  855. };
  856. this.logOff = function() {
  857. this.sendMsg({
  858. action: "setAgentStatus",
  859. body: { cmd: "disconnect", args: { cause: "disconnect request from js client." } }
  860. });
  861. };
  862. this.stopCallWaitBtnClickUI = function() {
  863. this.sendMsg({
  864. action: "callWait",
  865. body: { cmd: "stop", args: {} }
  866. });
  867. };
  868. this.transferCallWaitBtnClickUI = function() { this.callControl("transferCallWait", {}); };
  869. this.consultationBtnClickUI = function() {
  870. let transferType = "outer";
  871. let phoneNumber = $("#externalPhoneNumber").val().trim();
  872. if (phoneNumber === "") {
  873. transferType = "inner";
  874. var groupId = $("#transfer_to_groupIds").val();
  875. if ($.trim(groupId) == "") { alert("请选择业务组!"); $("#transfer_to_groupIds").focus(); return; }
  876. var member = $("#transfer_to_member").val();
  877. if ($.trim(member) == "") { alert("请选择要咨询的坐席成员!"); $("#transfer_to_member").focus(); return; }
  878. var selectText = $('#transfer_to_member option:selected').text();
  879. if (selectText.indexOf("空闲") == -1) { alert("请选择空闲的坐席成员!"); $("#transfer_to_member").focus(); return; }
  880. if (member == this.getOpNum()) { alert("不能咨询自己,请选择其他坐席成员!"); return; }
  881. phoneNumber = member;
  882. }
  883. this.callControl("consultation", { "to": phoneNumber, "transferType": transferType });
  884. };
  885. this.transferBtnClickUI = function() {
  886. let transferType = "outer";
  887. let phoneNumber = $("#externalPhoneNumber").val().trim();
  888. if (phoneNumber === "") {
  889. transferType = "inner";
  890. var groupId = $("#transfer_to_groupIds").val();
  891. if ($.trim(groupId) === "") { alert("请选择转接的业务组!"); $("#transfer_to_groupIds").focus(); return; }
  892. var member = $("#transfer_to_member").val();
  893. if ($.trim(member) === "") { alert("请选择转接的坐席成员!"); $("#transfer_to_member").focus(); return; }
  894. var selectText = $('#transfer_to_member option:selected').text();
  895. if (selectText.indexOf("空闲") === -1) { alert("请选择空闲的坐席成员!"); $("#transfer_to_member").focus(); return; }
  896. if (member === this.getOpNum()) { alert("不能转给自己,请选择其他坐席成员!"); return; }
  897. phoneNumber = member;
  898. }
  899. this.transferCall(phoneNumber, transferType);
  900. };
  901. this.transferCall = function(userCodeOrPhone, transferType) {
  902. if (transferType === "inner") {
  903. if (userCodeOrPhone !== this.getOpNum()) this.callControl("transferCall", { "to": userCodeOrPhone, "transferType": "inner" });
  904. else console.error("cant not transfer call to yourself.");
  905. } else {
  906. this.callControl("transferCall", { "to": userCodeOrPhone, "transferType": "outer" });
  907. }
  908. };
  909. this.hangup = function() {
  910. var args = {};
  911. if (_cc._activeCallUuid) args.uuid = _cc._activeCallUuid;
  912. _cc._suppressSessionNotFound = true;
  913. setTimeout(function() { _cc._suppressSessionNotFound = false; }, 4000);
  914. var sent = this.callControl("endSession", args);
  915. this.isCalling = false;
  916. if (sent !== false) {
  917. setTimeout(function() {
  918. _cc.setCallConnected(false);
  919. _cc.clearActiveCallUuid();
  920. }, 200);
  921. } else {
  922. _cc.setCallConnected(false);
  923. _cc.clearActiveCallUuid();
  924. }
  925. };
  926. this.callControl = function(action, argsObject) {
  927. if (!ws || ws.readyState !== WebSocket.OPEN) {
  928. console.warn('[callControl] WebSocket 未连接, cmd=', action);
  929. return false;
  930. }
  931. var sessionControl = {};
  932. sessionControl.action = "call";
  933. sessionControl.body = { "cmd": action, "args": argsObject || {} };
  934. return this.sendMsg(sessionControl);
  935. };
  936. this.checkCallConfirmed = function() {
  937. if (!_cc.getIsConnected()) { console.log('请先上线.'); return false; }
  938. if (!_cc.getCallConnected()) { console.log('当前没有通话.'); return false; }
  939. return true;
  940. };
  941. this.sendVideoFile = function(mp4FilePath) {
  942. if (!_cc.checkCallConfirmed()) return false;
  943. if (typeof(mp4FilePath) == "undefined" || mp4FilePath.trim().length === 0) { console.log("Parameter mp4FilePath is missing!"); return false; }
  944. this.callControl("playMp4File", { "mp4FilePath": mp4FilePath });
  945. return true;
  946. };
  947. this.reInviteVideoCall = function() {
  948. if (!_cc.checkCallConfirmed()) return false;
  949. if (!_cc.getCanSendVideoReInvite()) { console.log('cant not send video reInvite.'); return false; }
  950. this.callControl("reInviteVideo", {});
  951. return true;
  952. };
  953. this.call = function(phoneNumber, callType, videoLevel) {
  954. if (this.isCalling) {
  955. console.warn('[SDK] 正在呼叫中,请勿重复操作 (isCalling=true)');
  956. return false;
  957. }
  958. if (typeof(videoLevel) == "undefined" || videoLevel.trim().length === 0) {
  959. videoLevel = ccPhoneBarSocket.videoLevels.HD.levelId;
  960. console.log("auto default set videoLevel=", videoLevel);
  961. }
  962. if (typeof(callType) == "undefined" || callType.trim().length === 0) {
  963. callType = "audio";
  964. console.log("auto default set callType=", callType);
  965. }
  966. if (phoneNumber == null || phoneNumber.length === 0) { console.log('请输入外呼号码!'); return false; }
  967. if (phoneNumber.trim().length < 3) { alert('请输入正确格式的外呼号码!'); return false; }
  968. if (!_cc.getIsConnected()) { console.log('请先上线.'); return false; }
  969. let outboundInfo = {
  970. "gatewayList": _cc.callConfig.gatewayList,
  971. "destPhone": phoneNumber,
  972. "gatewayEncrypted": _cc.callConfig.gatewayEncrypted,
  973. "useSameAudioCodeForOutbound": _cc.callConfig.useSameAudioCodeForOutbound,
  974. "callType": callType,
  975. "videoLevel": videoLevel
  976. };
  977. this.callControl("startSession", outboundInfo);
  978. this.isCalling = true;
  979. return true;
  980. };
  981. this.callEx = function(phoneNumber) {
  982. if (phoneNumber == null || phoneNumber.length === 0) { console.log('请输入外呼号码!'); return; }
  983. if (!_cc.getIsConnected()) {
  984. _cc.connect();
  985. _cc.on(ccPhoneBarSocket.eventList.ws_connected, function() {
  986. _cc.off(ccPhoneBarSocket.eventList.ws_connected);
  987. _cc.call(phoneNumber);
  988. });
  989. return;
  990. }
  991. _cc.call(phoneNumber);
  992. };
  993. // 以下为UI工具条相关方法(仅当 useDefaultUi=true 时使用,不影响核心功能)
  994. ccPhoneBarSocket.findItemByCode = function(code) {
  995. for (var item in ccPhoneBarSocket.eventListWithTextInfo) {
  996. if (ccPhoneBarSocket.eventListWithTextInfo[item].code === code) return ccPhoneBarSocket.eventListWithTextInfo[item];
  997. }
  998. };
  999. ccPhoneBarSocket.eventListWithTextInfo = {
  1000. "ws_connected": { "code": 200, msg: "已签入", btn_text: [{ id: "#onLineBtn", name: "签出" }], enabled_btn: ['#setFree', '#callBtn', '#onLineBtn', '#consultationBtn'] },
  1001. "ws_disconnected": { "code": 202, msg: "服务器连接断开", btn_text: [{ id: "#onLineBtn", name: "签入" }], enabled_btn: ['#onLineBtn'] },
  1002. "user_login_on_other_device": { "code": 201, msg: "用户已在其他设备登录", btn_text: [{ id: "#onLineBtn", name: "签入" }], enabled_btn: ['#onLineBtn'] },
  1003. "request_args_error": { "code": 400, msg: "客户端请求参数错误", btn_text: [], enabled_btn: [] },
  1004. "server_error": { "code": 500, msg: "服务器内部错误", btn_text: [], enabled_btn: [] },
  1005. "caller_answered": { "code": 600, msg: "分机已接通", btn_text: [], enabled_btn: ['#resetStatus', '#hangUpBtn', '#transferBtn', '#holdBtn', '#consultationBtn'] },
  1006. "caller_hangup": { "code": 601, msg: "分机已挂断", btn_text: [], enabled_btn: ['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn'] },
  1007. "caller_busy": { "code": 602, msg: "分机忙,上一通电话未挂断", btn_text: [], enabled_btn: ['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn'] },
  1008. "caller_not_login": { "code": 603, msg: "分机未登录,请检查", btn_text: [], enabled_btn: ['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn'] },
  1009. "caller_respond_timeout": { "code": 604, msg: "分机未应答超时,请重新打开分机", btn_text: [], enabled_btn: ['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn'] },
  1010. "callee_answered": { "code": 605, msg: "被叫已接通", btn_text: [], enabled_btn: ['#resetStatus', '#hangUpBtn', '#transferBtn', '#holdBtn', '#consultationBtn'] },
  1011. "callee_hangup": { "code": 606, msg: "通话结束", btn_text: [], enabled_btn: ['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn'] },
  1012. "callee_ringing": { "code": 607, msg: "被叫振铃中", btn_text: [], enabled_btn: ['#resetStatus', '#hangUpBtn', '#transferBtn', '#consultationBtn'] },
  1013. "status_changed": { "code": 608, msg: "状态已改变", btn_text: [], enabled_btn: [] },
  1014. "free": { "code": 0, msg: "空闲中", btn_text: [], enabled_btn: ['#setBusy', '#onLineBtn', '#consultationBtn'] },
  1015. "busy": { "code": 1, msg: "忙碌", btn_text: [], enabled_btn: ['#setFree', '#onLineBtn', '#callBtn', '#consultationBtn'] },
  1016. "customer_channel_hold": { "code": 623, msg: "通话已保持.", btn_text: [], enabled_btn: ['#setFree', '#callBtn', '#unHoldBtn', '#consultationBtn'] },
  1017. "customer_channel_unhold": { "code": 624, msg: "通话已接回.", btn_text: [], enabled_btn: ['#hangUpBtn', '#holdBtn'] }
  1018. };
  1019. ccPhoneBarSocket.phone_buttons = ['#setFree', '#setBusy', '#callBtn', '#hangUpBtn', '#resetStatus', '#onLineBtn', '#transferBtn', '#holdBtn', '#unHoldBtn', '#consultationBtn'];
  1020. this.updatePhoneBar = function(msg, status_key) {
  1021. if (!_cc.callConfig.useDefaultUi) { console.log("callConfig.useDefaultUi = false , 已禁用默认ui工具条按钮."); return; }
  1022. if (msg) $("#callStatus").text(msg.msg);
  1023. var status_info = ccPhoneBarSocket.findItemByCode(status_key);
  1024. if (!status_info) return;
  1025. if (status_info.code === ccPhoneBarSocket.eventListWithTextInfo.status_changed.code) {
  1026. if (msg.object.status === ccPhoneBarSocket.agentStatusEnum.free) status_info = ccPhoneBarSocket.eventListWithTextInfo.free;
  1027. else status_info = ccPhoneBarSocket.eventListWithTextInfo.busy;
  1028. }
  1029. var msgSet = status_info.msg;
  1030. if (msgSet && msgSet.length > 0) $("#callStatus").text(msgSet);
  1031. var btn_text = status_info.btn_text;
  1032. var enabled_btn = status_info.enabled_btn;
  1033. if (btn_text) $.each(btn_text, function(i, d) { $(d.id).next().text(d.name); });
  1034. if (enabled_btn.length === 0) return;
  1035. var all_btn = ccPhoneBarSocket.phone_buttons;
  1036. for (var i in all_btn) {
  1037. if ($.inArray(all_btn[i], enabled_btn) < 0) $(all_btn[i]).removeClass('on');
  1038. else $(enabled_btn[$.inArray(all_btn[i], enabled_btn)]).addClass('on');
  1039. }
  1040. };
  1041. this.initPhoneBarUI = function() {
  1042. window.onbeforeunload = function() { if (!confirm('关闭网页将导致您无法接听电话,确定要关闭吗 ?')) return false; };
  1043. $("#unHoldBtnLi").hide();
  1044. if (!_cc.callConfig.useDefaultUi) { console.log("callConfig.useDefaultUi = false , 已禁用默认ui工具条按钮."); return; }
  1045. $('#conferenceBtn').on('click', function() {
  1046. if (!_cc.getIsConnected()) { console.log('请先上线.'); return; }
  1047. var confObjId = document.getElementById("conference_area");
  1048. if (confObjId.style.display === "block") confObjId.style.display = "none";
  1049. else confObjId.style.display = "block";
  1050. });
  1051. $('#callBtn').on('click', function() {
  1052. if ($(this).hasClass('on')) {
  1053. var destPhone = $.trim($("#ccphoneNumber").val());
  1054. var videoLevel = document.getElementById("videoLevelSelect").value;
  1055. var callType = document.forms[0].callType.value;
  1056. _cc.call(destPhone, callType, videoLevel);
  1057. }
  1058. });
  1059. $('#setFree').on('click', function() { if ($(this).hasClass('on')) _cc.setStatus(ccPhoneBarSocket.agentStatusEnum.free); });
  1060. $('#setBusy').on('click', function() { if ($(this).hasClass('on')) _cc.setStatus(ccPhoneBarSocket.agentStatusEnum.busy); });
  1061. $('#setBusySubList').on('change', function() { let itemValue = $('#setBusySubList').val(); _cc.setStatus(itemValue); });
  1062. $('#hangUpBtn').on('click', function() { if ($(this).hasClass('on')) _cc.hangup(); });
  1063. $('#holdBtn').on('click', function() { if ($(this).hasClass('on')) _cc.holdCall(); });
  1064. $('#unHoldBtn').on('click', function() { if ($(this).hasClass('on')) _cc.unHoldCall(); });
  1065. $("#doTransferBtn").hide();
  1066. function populateGroupIdOptions() {
  1067. var groups = _cc.callConfig.groups;
  1068. if (groups) {
  1069. var selectObj = document.getElementById("transfer_to_groupIds");
  1070. if (selectObj && selectObj.tagName.toLowerCase() === 'select') {
  1071. var firstOption = selectObj.options[0];
  1072. selectObj.innerHTML = '';
  1073. selectObj.add(firstOption);
  1074. for (var i = 0; i < groups.length; i++) {
  1075. var group = groups[i];
  1076. var option = document.createElement("option");
  1077. option.value = group.groupId;
  1078. option.text = group.bizGroupName;
  1079. selectObj.add(option);
  1080. }
  1081. if (groups.length > 0 && selectObj.options.length > 1) selectObj.selectedIndex = 1;
  1082. }
  1083. }
  1084. }
  1085. $('#transferBtn').on('click', function() {
  1086. if ($(this).hasClass('on')) {
  1087. if (!_cc.getIsConnected()) { console.log('请先上线.'); return; }
  1088. var transferArea = document.getElementById("transfer_area");
  1089. if (transferArea.style.display === "block") {
  1090. transferArea.style.display = "none";
  1091. _cc.unSubscribeAgentList();
  1092. $("#doTransferBtn").hide();
  1093. $("#doConsultationBtn").hide();
  1094. } else {
  1095. transferArea.style.display = "block";
  1096. populateGroupIdOptions();
  1097. $("#doTransferBtn").show();
  1098. $("#doConsultationBtn").hide();
  1099. }
  1100. }
  1101. });
  1102. $("#stopCallWait").hide();
  1103. $("#transferCallWait").hide();
  1104. $("#doConsultationBtn").hide();
  1105. $('#consultationBtn').on('click', function() {
  1106. if ($(this).hasClass('on')) {
  1107. if (!_cc.getIsConnected()) { console.log('请先上线.'); return; }
  1108. var transferArea = document.getElementById("transfer_area");
  1109. if (transferArea.style.display === "block") {
  1110. transferArea.style.display = "none";
  1111. _cc.unSubscribeAgentList();
  1112. $("#doConsultationBtn").hide();
  1113. $("#doTransferBtn").hide();
  1114. } else {
  1115. transferArea.style.display = "block";
  1116. populateGroupIdOptions();
  1117. $("#doConsultationBtn").show();
  1118. $("#doTransferBtn").hide();
  1119. }
  1120. }
  1121. });
  1122. $('#onLineBtn').on('click', function() {
  1123. if ($(this).hasClass('on')) {
  1124. if (_cc.getIsConnected()) _cc.disconnect();
  1125. else _cc.connect();
  1126. } else alert('当前不允许签出!');
  1127. });
  1128. $('#resetStatus').on('click', function() { window.onbeforeunload = null; location.reload(); });
  1129. $("#ccphoneNumber").keydown(function(e) {
  1130. if (e.which === 13) {
  1131. var destPhone = $.trim($("#ccphoneNumber").val());
  1132. var videoLevel = document.getElementById("videoLevelSelect").value;
  1133. var callType = document.forms[0].callType.value;
  1134. _cc.call(destPhone, callType, videoLevel);
  1135. return false;
  1136. }
  1137. });
  1138. $(document).keyup(function(e) {
  1139. if (e.which === 27) {
  1140. console.log('按下了ESC键, 即将发送挂机指令.');
  1141. if (_cc.getIsConnected()) {
  1142. if (_cc.callConfig["useDefaultUi"]) { if ($('#hangUpBtn').hasClass('on')) _cc.hangup(); }
  1143. else _cc.hangup();
  1144. }
  1145. }
  1146. });
  1147. };
  1148. this.holdCall = function() {
  1149. this.sendMsg({ action: "callHold", body: { cmd: "hold", args: {} } });
  1150. };
  1151. this.unHoldCall = function() {
  1152. this.sendMsg({ action: "callHold", body: { cmd: "unhold", args: {} } });
  1153. };
  1154. this.subscribeAgentList = function() {
  1155. this.sendMsg({ action: "pollAgentList", body: { cmd: "subscribe", args: {} } });
  1156. _cc.subscribeAgentListStarted = true;
  1157. };
  1158. this.unSubscribeAgentList = function() {
  1159. if (_cc.subscribeAgentListStarted) {
  1160. _cc.subscribeAgentListStarted = false;
  1161. _cc.callConfig.agentList = null;
  1162. this.sendMsg({ action: "pollAgentList", body: { cmd: "unSubscribe", args: {} } });
  1163. }
  1164. };
  1165. // 会议相关方法(保持不变)
  1166. this.conferenceStartBtnUI = function(customerName) {
  1167. var callType = document.getElementById("conf_call_type").value;
  1168. var confTemplate = document.getElementById("conf_template").value;
  1169. var layOut = document.getElementById("conf_layout").value;
  1170. _cc.setStatusBusy();
  1171. $("#callBtn").removeClass('on');
  1172. $("#setFree").removeClass('on');
  1173. $("#onLineBtn").removeClass('on');
  1174. document.getElementById("startConference").setAttribute("disabled", "true");
  1175. if (_cc.getCallConnected()) {
  1176. let tips = "是否把当前通话转换为会议 ?";
  1177. if (confirm(tips)) _cc.transferToConference(layOut, confTemplate, callType, customerName);
  1178. else document.getElementById("startConference").removeAttribute("disabled");
  1179. } else _cc.conferenceStart(layOut, confTemplate, callType);
  1180. };
  1181. this.conferenceAddMemberBtnUI = function(reInvite, memberPhoneParam, memberNameParam) {
  1182. var memberName = "", memberPhone = "", memberCallType = $.trim(document.getElementById("member_call_type").value), memberVideoLevel = $.trim(document.getElementById("member_video_level").value);
  1183. if (reInvite === 0) {
  1184. memberName = $("#member_name").val();
  1185. memberPhone = $("#member_phone").val();
  1186. if (memberName.length === 0 || $.trim(memberName) === "") { alert('请填写参会者姓名!'); return; }
  1187. if (memberPhone.length === 0 || $.trim(memberPhone) === "") { alert('请填写参会者手机号!'); return; }
  1188. memberName = $.trim(memberName);
  1189. memberPhone = $.trim(memberPhone);
  1190. var templateObj = document.getElementById("conf_member_template");
  1191. if (document.getElementById("conf_member_" + memberPhone) != null) { alert('会议成员已经存在,请不要重复添加!'); return; }
  1192. var memberHtmlItem = templateObj.innerHTML;
  1193. memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_name}", "gm"), memberName);
  1194. memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_phone}", "gm"), memberPhone);
  1195. memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_status}", "gm"), "即将呼叫");
  1196. var li = document.createElement("li");
  1197. li.setAttribute("id", "conf_member_" + memberPhone);
  1198. li.setAttribute("class", "conf_member_item_row");
  1199. li.innerHTML = memberHtmlItem;
  1200. _cc.insertAfter(li, templateObj);
  1201. $("#member_name").val('');
  1202. $("#member_phone").val('');
  1203. } else { memberName = memberNameParam; memberPhone = memberPhoneParam; }
  1204. var memberItemId = "#conf_member_" + memberPhone;
  1205. $(".conf_mute", $(memberItemId)).find("img").hide();
  1206. $(".conf_vmute", $(memberItemId)).find("img").hide();
  1207. $(".conf_re_invite", $(memberItemId)).hide();
  1208. $(".conf_status", $(memberItemId)).html("即将呼叫");
  1209. _cc.conferenceAddMembers([{ "name": memberName, "phone": memberPhone, "callType": memberCallType, "videoLevel": memberVideoLevel }]);
  1210. };
  1211. this.conferenceAddMemberFromExistCall = function(memberName, memberPhone) {
  1212. if (memberName.length === 0 || $.trim(memberName) === "") { alert('请填写参会者姓名!'); return; }
  1213. if (memberPhone.length === 0 || $.trim(memberPhone) === "") { alert('请填写参会者手机号!'); return; }
  1214. memberName = $.trim(memberName);
  1215. memberPhone = $.trim(memberPhone);
  1216. var templateObj = document.getElementById("conf_member_template");
  1217. if (document.getElementById("conf_member_" + memberPhone) != null) { alert('会议成员已经存在,请不要重复添加!'); return; }
  1218. var memberHtmlItem = templateObj.innerHTML;
  1219. memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_name}", "gm"), memberName);
  1220. memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_phone}", "gm"), memberPhone);
  1221. memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_status}", "gm"), "即将呼叫");
  1222. var li = document.createElement("li");
  1223. li.setAttribute("id", "conf_member_" + memberPhone);
  1224. li.setAttribute("class", "conf_member_item_row");
  1225. li.innerHTML = memberHtmlItem;
  1226. _cc.insertAfter(li, templateObj);
  1227. var memberItemId = "#conf_member_" + memberPhone;
  1228. $(".conf_mute", $(memberItemId)).find("img").show();
  1229. $(".conf_vmute", $(memberItemId)).find("img").show();
  1230. $(".conf_re_invite", $(memberItemId)).hide();
  1231. $(".conf_status", $(memberItemId)).html("通话中").css("color", "green");
  1232. };
  1233. this.transferToConference = function(layOut, confTemplate, callType, customerName) {
  1234. console.log("try to transferToConference: ", layOut, confTemplate, callType, customerName);
  1235. _cc.callControl("transferToConference", { "layOut": layOut, "callType": callType, "confTemplate": confTemplate, "customerName": customerName });
  1236. };
  1237. this.conferenceStart = function(layOut, confTemplate, callType) {
  1238. this.sendMsg({
  1239. action: "conference",
  1240. body: { method: "startconf", args: { layOut: layOut, callType: callType, confTemplate: confTemplate } }
  1241. });
  1242. };
  1243. this.conferenceVMuteMember = function(members) { this.conferenceControl("vmute", typeof members === "string" ? [{ "phone": members }] : members); };
  1244. this.conferenceUnVMuteMember = function(members) { this.conferenceControl("unvmute", typeof members === "string" ? [{ "phone": members }] : members); };
  1245. this.conferenceMuteMember = function(members) { this.conferenceControl("mute", typeof members === "string" ? [{ "phone": members }] : members); };
  1246. this.conferenceUnMuteMember = function(members) { this.conferenceControl("unmute", typeof members === "string" ? [{ "phone": members }] : members); };
  1247. this.conferenceAddMembers = function(members) { this.conferenceControl("add", members); };
  1248. this.conferenceRemoveMembers = function(members) {
  1249. if (typeof members === "string") {
  1250. var memberItemObj = $("#conf_member_" + members);
  1251. if ($(".conf_status", memberItemObj).text() === "通话中") this.conferenceControl("remove", [{ "phone": members }]);
  1252. memberItemObj.remove();
  1253. } else this.conferenceControl("remove", members);
  1254. };
  1255. this.conferenceEnd = function() { this.conferenceControl("endconf", []); };
  1256. this.conferenceControl = function(action, phoneList) {
  1257. this.sendMsg({
  1258. action: "conference",
  1259. body: { method: action, memberList: phoneList }
  1260. });
  1261. };
  1262. this.callMonitorDataPull = function() {
  1263. this.sendMsg({ action: "monitorData", body: {} });
  1264. };
  1265. this.inboundCallQueuePull = function() {
  1266. this.sendMsg({ action: "inboundMonitorData", body: {} });
  1267. };
  1268. this.monitorControl = function(action, argsObject) {
  1269. this.sendMsg({
  1270. action: "callMonitor",
  1271. body: { cmd: action, args: argsObject }
  1272. });
  1273. };
  1274. this.callMonitorStart = function(callId) {
  1275. if (callId == null || callId.length === 0) { console.log('请提供待监听电话的 callId !'); return; }
  1276. if (!_cc.getIsConnected()) { console.log('请先上线.'); return; }
  1277. this.monitorControl("startMonitoring", { 'callSpyId': callId });
  1278. };
  1279. this.callMonitorEnd = function() { if (!_cc.getIsConnected()) { console.log('请先上线.'); return; } this.monitorControl("endMonitoring", {}); };
  1280. }
  1281. export default ccPhoneBarSocket;