3 Achegas 97d9d6b9f8 ... ef707fb162

Autor SHA1 Mensaxe Data
  yzx ef707fb162 优化 hai 1 mes
  yzx 857bcaa1e4 优化 hai 1 mes
  yzx aaff7ce440 优化 hai 1 mes
Modificáronse 66 ficheiros con 2913 adicións e 387 borrados
  1. 3 0
      .vscode/settings.json
  2. 42 29
      docs/ccPhoneBarSocket.js
  3. 36 1
      docs/phone-bar-ex.html
  4. 39 0
      docs/zh-cn/mysql8/my.cnf
  5. 18 0
      firewalld的说明.txt
  6. 6 1
      sql-scripts/clear.txt
  7. 303 4
      sql-scripts/easycallcenter365.sql
  8. 45 45
      src/main/java/com/telerobot/fs/acd/CallHandler.java
  9. 52 0
      src/main/java/com/telerobot/fs/config/AudioUtils.java
  10. 91 0
      src/main/java/com/telerobot/fs/config/OriginateSessionErrorCode.java
  11. 45 0
      src/main/java/com/telerobot/fs/config/SipSessionStatusCode.java
  12. 4 0
      src/main/java/com/telerobot/fs/controller/InboundCallController.java
  13. 2 2
      src/main/java/com/telerobot/fs/controller/ReloadParams.java
  14. 4 0
      src/main/java/com/telerobot/fs/entity/bo/ChannelFlag.java
  15. 19 0
      src/main/java/com/telerobot/fs/entity/bo/InboundDetail.java
  16. 36 0
      src/main/java/com/telerobot/fs/entity/dao/CallTaskEntity.java
  17. 52 0
      src/main/java/com/telerobot/fs/entity/dao/CcExtNum.java
  18. 4 0
      src/main/java/com/telerobot/fs/entity/dto/InboundConfig.java
  19. 1 1
      src/main/java/com/telerobot/fs/entity/dto/LlmAiphoneRes.java
  20. 27 1
      src/main/java/com/telerobot/fs/entity/dto/llm/AccountBaseEntity.java
  21. 4 1
      src/main/java/com/telerobot/fs/entity/pojo/LlmToolRequest.java
  22. 26 0
      src/main/java/com/telerobot/fs/entity/pojo/TtsFileInfo.java
  23. 1 0
      src/main/java/com/telerobot/fs/entity/pojo/TtsProvider.java
  24. 3 6
      src/main/java/com/telerobot/fs/global/CdrPush.java
  25. 4 0
      src/main/java/com/telerobot/fs/mybatis/dao/SysDao.java
  26. 13 1
      src/main/java/com/telerobot/fs/mybatis/dao/SysDaoImpl.java
  27. 5 1
      src/main/java/com/telerobot/fs/mybatis/persistence/SysMapper.java
  28. 6 2
      src/main/java/com/telerobot/fs/outbound/batchcall/BatchTaskManager.java
  29. 5 0
      src/main/java/com/telerobot/fs/outbound/batchcall/CallTask.java
  30. 8 1
      src/main/java/com/telerobot/fs/robot/AbstractChatRobot.java
  31. 35 5
      src/main/java/com/telerobot/fs/robot/RobotBase.java
  32. 118 48
      src/main/java/com/telerobot/fs/robot/RobotChat.java
  33. 246 0
      src/main/java/com/telerobot/fs/robot/impl/ChatGPT.java
  34. 267 0
      src/main/java/com/telerobot/fs/robot/impl/ClaudeChat.java
  35. 21 2
      src/main/java/com/telerobot/fs/robot/impl/Coze.java
  36. 6 24
      src/main/java/com/telerobot/fs/robot/impl/DeepSeekChat.java
  37. 2 1
      src/main/java/com/telerobot/fs/robot/impl/Dify.java
  38. 2 0
      src/main/java/com/telerobot/fs/robot/impl/JiutianChat.java
  39. 1 0
      src/main/java/com/telerobot/fs/robot/impl/JiutianWorkflow.java
  40. 115 141
      src/main/java/com/telerobot/fs/robot/impl/LocalLlmChat.java
  41. 19 3
      src/main/java/com/telerobot/fs/robot/impl/LocalNlpChat.java
  42. 191 0
      src/main/java/com/telerobot/fs/robot/impl/LocalWavFile.java
  43. 72 0
      src/main/java/com/telerobot/fs/robot/impl/LocalWebApiTest.java
  44. 2 1
      src/main/java/com/telerobot/fs/robot/impl/MaxKB.java
  45. 256 0
      src/main/java/com/telerobot/fs/robot/impl/XingWenChat.java
  46. 1 0
      src/main/java/com/telerobot/fs/service/AsrResultListener.java
  47. 1 0
      src/main/java/com/telerobot/fs/service/InboundDetailService.java
  48. 8 0
      src/main/java/com/telerobot/fs/service/SysService.java
  49. 58 4
      src/main/java/com/telerobot/fs/utils/CommonUtils.java
  50. 11 5
      src/main/java/com/telerobot/fs/utils/FileUtil.java
  51. 27 22
      src/main/java/com/telerobot/fs/utils/FileUtils.java
  52. 2 6
      src/main/java/com/telerobot/fs/utils/OkHttpClientUtil.java
  53. 103 0
      src/main/java/com/telerobot/fs/utils/SipProfilesPortReader.java
  54. 66 0
      src/main/java/com/telerobot/fs/utils/SwitchRtpPortConfigReader.java
  55. 6 0
      src/main/java/com/telerobot/fs/wshandle/RespStatus.java
  56. 223 0
      src/main/java/com/telerobot/fs/wshandle/SecurityManager.java
  57. 15 3
      src/main/java/com/telerobot/fs/wshandle/SessionManager.java
  58. 10 0
      src/main/java/com/telerobot/fs/wshandle/SwitchChannel.java
  59. 11 0
      src/main/java/com/telerobot/fs/wshandle/firewalld-template.xml
  60. 29 17
      src/main/java/com/telerobot/fs/wshandle/impl/CallApi.java
  61. 38 1
      src/main/java/com/telerobot/fs/wshandle/impl/CallListener.java
  62. 8 0
      src/main/java/com/telerobot/fs/wshandle/nettyserver/WebSocketServer.java
  63. 8 4
      src/main/java/com/telerobot/fs/wshandle/nettyserver/WebSocketServerHandler.java
  64. 2 2
      src/main/resources/application-238.properties
  65. 1 1
      src/main/resources/application.properties
  66. 28 1
      src/main/resources/com/telerobot/fs/mybatis/persistence/SysMapper.xml

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "java.compile.nullAnalysis.mode": "automatic"
+}

+ 42 - 29
docs/ccPhoneBarSocket.js

@@ -580,9 +580,14 @@ function ccPhoneBarSocket() {
 		"fill_form" : 5,
 
 		/**
-		 * 电话会议中
+		 * in conference
 		 */
-		"conference"  :  6
+		"conference"  :  6,
+		
+		/**
+		 * acd agnet is in lockStatus
+		 */
+		"lockStatus"  :  7
 	};
 
 	//定义视频level-id
@@ -661,130 +666,132 @@ function ccPhoneBarSocket() {
 		/**
 		 * 收到转接的来电请求
 		 */
-		"transfer_call_recv" :  616,
+		"transfer_call_recv" :  "616",
 
 		/**
 		 * 锁定坐席失败
 		 */
-		"lock_agent_fail" :  617,
+		"lock_agent_fail" :  "617",
 
 		/**
 		 * 通话已经转接成功
 		 */
-		"transfer_call_success" :  618,
+		"transfer_call_success" :  "618",
 
 		/**
 		 * 产生asr语音识别结果
 		 */
-		"asr_result_generate" :  619,
+		"asr_result_generate" :  "619",
 
 		/**
 		 * ASR语音识别流程结束(坐席侧)
 		 */
-		"asr_process_end_agent" :  620,
+		"asr_process_end_agent" :  "620",
 
 		/**
 		 * ASR语音识别流程结束(客户侧)
 		 */
-		"asr_process_end_customer" :  621,
+		"asr_process_end_customer" :  "621",
 
-		"asr_process_started" : 622,
+		"asr_process_started" : "622",
 
 		/**
 		 * customer call session hold.
 		 */
-		"customer_channel_hold" : 623,
+		"customer_channel_hold" : "623",
 
 		/**
 		 * customer call session unHold.
 		 */
-		"customer_channel_unhold" : 624,
+		"customer_channel_unhold" : "624",
 
 		/**
 		 * customer call session on hold is hangup.
 		 */
-		"customer_on_hold_hangup" : 625,
+		"customer_on_hold_hangup" : "625",
 
-		"inner_consultation_request" : 626,
+		"inner_consultation_request" : "626",
 
 		/**
 		 * customer call session on call-wait.
 		 */
-		"customer_channel_call_wait" : 627,
+		"customer_channel_call_wait" : "627",
 
 		/**
 		 * customer call session off call-wait.
 		 */
-		"customer_channel_off_call_wait" : 628,
+		"customer_channel_off_call_wait" : "628",
 
 		/**
 		 * customer call session on call-wait is hangup.
 		 */
-		"customer_on_call_wait_hangup" : 629,
+		"customer_on_call_wait_hangup" : "629",
 
 		/**
 		 *  extension on line event
 		 */
-		"extension_on_line" : 630,
+		"extension_on_line" : "630",
 
 		/**
 		 * extension off line event
 		 */
-		"extension_off_line" : 631,
+		"extension_off_line" : "631",
 
         /**
          * Notify the agent that the call consultation has started.
          */
-        "inner_consultation_start" : 632,
+        "inner_consultation_start" : "632",
 
         /**
          *  Notify the agent that the call consultation has stopped.
          */
-        "inner_consultation_stop" : 633,
+        "inner_consultation_stop" : "633",
+		
+		"extension_cannot_connected" : "634",
 
 
 	    /**
 		* 多人电话会议,重复的被叫 ,
 		*/
-		"conference_repeat_callee"  :  660 ,
+		"conference_repeat_callee"  :  "660" ,
 
 		 /**
 		 * 多人电话会议,呼叫成员超时 ,
 		 */
-		 "CONFERENCE_CALL_MODERATOR_TIMEOUT"  :  661 ,
+		 "CONFERENCE_CALL_MODERATOR_TIMEOUT"  :  "661" ,
 
 		/**
 		 * 多人电话会议,成员接通 ,
 		 */
-		"CONFERENCE_MEMBER_ANSWERED"  :  662 ,
+		"CONFERENCE_MEMBER_ANSWERED"  :  "662" ,
 
 
 		/**
 		 * 多人电话会议,成员挂机 ,
 		 */
-		"CONFERENCE_MEMBER_HANGUP"  :  663 ,
+		"CONFERENCE_MEMBER_HANGUP"  :  "663" ,
 
 		/**
 		 * 多人电话会议,成员禁言成功 ,
 		 */
-		"CONFERENCE_MEMBER_MUTED_SUCCESS"  :  666 ,
+		"CONFERENCE_MEMBER_MUTED_SUCCESS"  :  "666" ,
 
 
 		/**
 		 * 多人电话会议,成员禁言失败 ,
 		 */
-		"CONFERENCE_MEMBER_MUTED_FAILED"  : 665  ,
+		"CONFERENCE_MEMBER_MUTED_FAILED"  : "665"  ,
 
 		/**
 		 * 多人电话会议,成员解除禁言成功 ,
 		 */
-		"CONFERENCE_MEMBER_UNMUTED_SUCCESS"  :  667 ,
+		"CONFERENCE_MEMBER_UNMUTED_SUCCESS"  :  "667" ,
 
 
 		/**
 		 * 多人电话会议,成员解除禁言失败 ,
 		 */
-		"CONFERENCE_MEMBER_UNMUTED_FAILED"  : 668  ,
+		"CONFERENCE_MEMBER_UNMUTED_FAILED"  : "668"  ,
 
 		/**
 		 * 多人电话会议,会议成员不存在,无法执行相关操作:
@@ -1150,7 +1157,11 @@ function ccPhoneBarSocket() {
 		"caller_answered":{ "code" : 600, msg:"分机已接通",
 			btn_text:[],
 			enabled_btn:['#resetStatus', '#hangUpBtn', '#transferBtn', '#holdBtn', '#consultationBtn']
-		},
+		},	
+        "lock_agent":{ "code" : 0, msg:"坐席已锁定",
+			btn_text:[],
+			enabled_btn:['#conferenceBtn']
+		},			
 		"caller_hangup":{ "code" : 601, msg:"分机已挂断",
 			btn_text:[],
 			enabled_btn:['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn' ]
@@ -1221,6 +1232,8 @@ function ccPhoneBarSocket() {
 		if(status_info.code === ccPhoneBarSocket.eventListWithTextInfo.status_changed.code){
 			if(msg.object.status === ccPhoneBarSocket.agentStatusEnum.free){
 				status_info = ccPhoneBarSocket.eventListWithTextInfo.free;
+			}else if(msg.object.status === ccPhoneBarSocket.agentStatusEnum.lockStatus){
+				status_info = ccPhoneBarSocket.eventListWithTextInfo.lock_agent;
 			}else{
 				status_info = ccPhoneBarSocket.eventListWithTextInfo.busy;
 			}

+ 36 - 1
docs/phone-bar-ex.html

@@ -129,6 +129,10 @@ window.onload = function(){
         _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.ws_disconnected.code);
         $("#transfer_area").hide();
     });
+	
+   _phoneBar.on(ccPhoneBarSocket.eventList.extension_cannot_connected, function(msg){
+	    console.log('extension cannot connected', msg);
+	});
 
     _phoneBar.on(ccPhoneBarSocket.eventList.OUTBOUND_START, function (msg) {
         console.log('outbound_start',msg);
@@ -334,7 +338,38 @@ window.onload = function(){
         onTransferToConferenceSuccess(msg);
     });
 
-    var _gatewayList = [{"gatewayAddr":"192.168.67.217:5080","callProfile":"internal","authUsername":"1002","callerNumber":"13195510173\r\n13195510174\r\n13195510188","updateTime":1769767068989,"calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"3","concurrency":2,"register":0}];
+    var _gatewayList = [
+        {
+            "gatewayAddr":"192.168.67.217:5080",
+            "callProfile":"internal",
+            "authUsername":"1002",
+            "callerNumber":"13195510173\r\n13195510174\r\n13195510188",
+            "updateTime":1769767068989,
+            "calleePrefix":"",
+            "priority":1,
+            "audioCodec":"pcma",
+            "uuid":"3",
+            "concurrency":2,
+            "register":0
+        }
+    ];
+    var _gatewayList2 = [
+        {"gatewayAddr":"192.168.31.252:111","callProfile":"external","callerNumber":"1004","updateTime":1772073618994,
+            "calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"2","concurrency":2,"register":0
+        },
+        {"gatewayAddr":"192.168.67.217:5080","callProfile":"internal","callerNumber":"13195510173\r\n13195510174\r\n13195510188",
+            "updateTime":1769767068989,"calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"3","concurrency":2,"register":0
+        },
+        {"gatewayAddr":"192.168.31.59:63406","callProfile":"external","callerNumber":"1004","updateTime":1772585206484,
+            "calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"4","concurrency":2,"register":0
+        },
+        {"gatewayAddr":"192.168.31.64:54354","callProfile":"external","callerNumber":"1005","updateTime":1771991512294,
+            "calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"5","concurrency":2,"register":0
+        },
+        {"gatewayAddr":"192.168.31.167:49424","callProfile":"external","callerNumber":"1007","updateTime":1770710040756,
+            "calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"6","concurrency":2,"register":0
+        }
+    ];
 
     // 电话工具条参数配置;
 	_callConfig = {

+ 39 - 0
docs/zh-cn/mysql8/my.cnf

@@ -0,0 +1,39 @@
+# For advice on how to change settings please see
+# http://dev.mysql.com/doc/refman/8.0/en/server-configuration-defaults.html
+
+[mysqld]
+#
+# Remove leading # and set to the amount of RAM for the most important data
+# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
+# innodb_buffer_pool_size = 128M
+#
+# Remove leading # to turn on a very important data integrity option: logging
+# changes to the binary log between backups.
+# log_bin
+#
+# Remove leading # to set options mainly useful for reporting servers.
+# The server defaults are faster for transactions and fast SELECTs.
+# Adjust sizes as needed, experiment to find the optimal values.
+# join_buffer_size = 128M
+# sort_buffer_size = 2M
+# read_rnd_buffer_size = 2M
+
+# Remove leading # to revert to previous value for default_authentication_plugin,
+# this will increase compatibility with older clients. For background, see:
+# https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_default_authentication_plugin
+# default-authentication-plugin=mysql_native_password
+skip-host-cache
+skip-name-resolve
+datadir=/var/lib/mysql
+socket=/var/run/mysqld/mysqld.sock
+secure-file-priv=/var/lib/mysql-files
+user=mysql
+default_authentication_plugin=mysql_native_password
+max_connections=1000
+lower_case_table_names=1
+
+pid-file=/var/run/mysqld/mysqld.pid
+[client]
+socket=/var/run/mysqld/mysqld.sock
+
+!includedir /etc/mysql/conf.d/

+ 18 - 0
firewalld的说明.txt

@@ -0,0 +1,18 @@
+新增firewalld的安全防护
+
+需要在宿主机上安装firewalld服务
+
+1. firewalld-template.xml 模板文件放入到目 /home/freeswitch/etc/freeswitch/autoload_configs
+
+2. 修改默认的 autoload_configs/acl.conf.xml文件,增加文件包含: 
+   <X-PRE-PROCESS cmd="include" data="inbound_acl.xml"/>
+   <X-PRE-PROCESS cmd="include" data="register_acl.xml"/>
+
+3. 所有的 profile 配置文件 
+    internal.xml 引用设置:
+    <param name="apply-inbound-acl" value="inbound_allow_list"/>
+	<param name="apply-register-acl" value="register_allow_list"/> 
+	
+	external.xml 引用设置:
+    <param name="apply-inbound-acl" value="inbound_allow_list"/>
+	

+ 6 - 1
sql-scripts/clear.txt

@@ -31,5 +31,10 @@ reset master;
 4. update version info:
    update sys_config set config_value='v20260217' where config_key='sys.version';
    
+5. delete all test-user accounts:  
+   DELETE from sys_user where user_name <> 'admin';
    
-5. 修改参数管理页面: event-socket-ip 为127.0.0.1  
+6. delete install_lock file:
+   rm -rf /home/freeswitch/etc/freeswitch/autoload_configs/install_lock.data    
+   
+7. 修改参数管理页面: event-socket-ip 为127.0.0.1  

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 303 - 4
sql-scripts/easycallcenter365.sql


+ 45 - 45
src/main/java/com/telerobot/fs/acd/CallHandler.java

@@ -3,6 +3,8 @@ package com.telerobot.fs.acd;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.config.AppContextProvider;
+import com.telerobot.fs.config.OriginateSessionErrorCode;
+import com.telerobot.fs.config.SipSessionStatusCode;
 import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.bo.ChanneState;
 import com.telerobot.fs.entity.bo.ChannelFlag;
@@ -78,17 +80,12 @@ public class CallHandler {
 		return inboundDetail;
 	}
 
-	/**
-	 * IVR ID for satisfaction survey
-	 */
-	private volatile String satisfSurveyIvrId = "";
-
 	public String getSatisfSurveyIvrId() {
-		return satisfSurveyIvrId;
+		return this.inboundDetail.getSatisfSurveyIvrId();
 	}
 
 	public void setSatisfSurveyIvrId(String satisfSurveyIvrId) {
-		this.satisfSurveyIvrId = satisfSurveyIvrId;
+		this.inboundDetail.setSatisfSurveyIvrId(satisfSurveyIvrId);
 	}
 
 	private static ThreadPoolExecutor transferCallThreadPool;
@@ -359,7 +356,6 @@ public class CallHandler {
 		if(queueNum == 0) {
 			return "";
 		}
-		queueNum = queueNum - 1;
 		StringBuilder stringBuilder = new StringBuilder();
 		stringBuilder.append("$${sounds_dir}/ivr/queue-wait-num-tips-start.wav");
 		stringBuilder.append(";");
@@ -470,7 +466,8 @@ public class CallHandler {
 						customerChannel.setChannelState(ChanneState.BRIDGED);
 						customerChannel.setFlag(ChannelFlag.HOLD_CALL);
 						customerChannel.setInboundDetail(inboundDetail);
-						if(!StringUtils.isNullOrEmpty(satisfSurveyIvrId)){
+						inboundDetail.setSwitchChannel(customerChannel);
+						if(!StringUtils.isNullOrEmpty(inboundDetail.getSatisfSurveyIvrId())){
 							customerChannel.setFlag(ChannelFlag.SATISFACTION_SURVEY_REQUIRED);
 							EslConnectionUtil.sendExecuteCommand(
 									"set",
@@ -659,53 +656,33 @@ public class CallHandler {
 					cdrDetail.setCdrBody(JSON.toJSONString(inboundDetail));
 					CdrPush.addCdrToQueue(cdrDetail);
 				}else {
-					String extnum = headers.get("variable_extnum");
-					saveAgentHangupTime(extnum);
-					log.info("{} extension is hangup, hangupCause: {}", uuid, hangupCause);
 					boolean answered = inboundDetail.getManualAnsweredTime() > 0L;
 					if (!answered) {
-						log.warn("{} The extension not answered the  call session. Extension {} is abnormal. Please pay attention to it!", uuid, extnum);
-					}
-					resetAgentStatus();
+						log.warn("{} The extension not answered the  call session. Extension {} is abnormal. Please pay attention to it!",
+								uuid, agentSessionEntity.getExtNum()
+						);
 
-					if (inboundDetail.getManualAnsweredTime() > 0L) {
-						if (!StringUtils.isNullOrEmpty(satisfSurveyIvrId)) {
-							try {
-								callerParkSemaphore.tryAcquire(2100L, TimeUnit.MILLISECONDS);
-							} catch (Throwable e) {
-							}
-							log.info("{} Try to start ivr process for satisfaction survey. ivrId={}.", inboundDetail.getUuid(), satisfSurveyIvrId);
-							AppContextProvider.getBean(IvrEngine.class).startIvrSession(inboundDetail, satisfSurveyIvrId);
+						String sipCode = headers.get("variable_sip_invite_failure_status");
+						int sipStatus = StringUtils.isNullOrEmpty(sipCode) ? 0 : Integer.parseInt(sipCode);
+						if(sipStatus != SipSessionStatusCode.USER_BUSY){
+							reportExtensionStatusToWsClient();
 						}
 					}
+
+					String extnum = headers.get("variable_extnum");
+					saveAgentHangupTime(extnum);
+					log.info("{} extension is hangup, hangupCause: {}", uuid, hangupCause);
+					resetAgentStatus();
 				}
 			} else if (EventNames.CHANNEL_PROGRESS_MEDIA.equalsIgnoreCase(eventName)) {
 				log.info("{} recv ringing event {}", uuid, eventName);
 			} else if (EventNames.CHANNEL_ANSWER.equalsIgnoreCase(eventName)) {
 				if (uniqueID.contains(BLEGSTR)) {
 					// 桥接通话
-					log.info("{} the extension {} has been connected, try to bridge session. {}",
+					log.info("{} the extension {} has been connected.",
 							uuid,
-							headers.get("variable_extnum"),
-							inboundDetail.getCaller()
-					);
-
-					EslMessage eslMessage = EslConnectionUtil.sendSyncApiCommand(
-							"uuid_bridge",
-							String.format("%s %s",uuid, uniqueID),
-							eslConnectionPool
+							agentSessionEntity.getExtNum()
 					);
-					boolean bridgeSucceed = false;
-					if(eslMessage.getBodyLines().size() > 0){
-						if(eslMessage.getBodyLines().get(0).contains("+OK")){
-							bridgeSucceed = true;
-						}
-					}
-					if(!bridgeSucceed){
-						log.error("{} call bridged failed: {}", uuid, JSON.toJSONString(eslMessage));
-					}else{
-						log.info("{} call bridged successfully: {}", uuid, JSON.toJSONString(eslMessage));
-					}
 
 					assert null != agentSessionEntity;
 					// 记录当前接听者;
@@ -715,7 +692,6 @@ public class CallHandler {
 					inboundDetail.setTransferredSucceed(true);
 
 					breakWaitMusic();
-
 					log.info("{} resetAgentBusyLockTime. uerId={}, extNum={}",
 							uuid,
 							agentSessionEntity.getOpNum(),
@@ -763,7 +739,14 @@ public class CallHandler {
 			});
 		}
 
-
+		String[] abnormalCodeList = new String[]{
+				OriginateSessionErrorCode.USER_NOT_REGISTERED,
+				OriginateSessionErrorCode.DESTINATION_OUT_OF_ORDER,
+				OriginateSessionErrorCode.NO_ANSWER,
+				OriginateSessionErrorCode.NO_USER_RESPONSE,
+				OriginateSessionErrorCode.RECOVERY_ON_TIMER_EXPIRE,
+				OriginateSessionErrorCode.INCOMPATIBLE_DESTINATION
+		};
 
 		@Override
 		public void backgroundJobResultReceived(String addr, EslEvent event) {
@@ -774,6 +757,16 @@ public class CallHandler {
 				transferFailed = true;
 				log.error("{} exception result got while connect extension,details:{}", uuid, event.toString());
 			}
+			if(eslStr.contains(OriginateSessionErrorCode.USER_NOT_REGISTERED)){
+				resetAgentStatus();
+			}
+
+			for (String caseStr : abnormalCodeList) {
+				if(eslStr.contains(caseStr)){
+					reportExtensionStatusToWsClient();
+					return;
+				}
+			}
 		}
 
 		@Override
@@ -782,6 +775,13 @@ public class CallHandler {
 		}
 	}
 
+	private void reportExtensionStatusToWsClient() {
+		MessageHandlerEngineList.sendReplyToAgent(agentSessionEntity.getOpNum(),
+				new MessageResponse(RespStatus.EXTENSION_CANNOT_CONNECTED, JSON.toJSONString(agentSessionEntity))
+		);
+	}
+
+
 	@Override
 	public boolean equals(Object o) {
 		if (this == o) {

+ 52 - 0
src/main/java/com/telerobot/fs/config/AudioUtils.java

@@ -1,5 +1,10 @@
 package com.telerobot.fs.config;
 
+import com.telerobot.fs.entity.pojo.TtsFileInfo;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import javax.sound.sampled.AudioFormat;
 import javax.sound.sampled.AudioInputStream;
 import javax.sound.sampled.AudioSystem;
@@ -7,6 +12,8 @@ import java.io.File;
 
 public class AudioUtils {
 
+    protected final static Logger logger = LoggerFactory.getLogger(AudioUtils.class);
+
     /**
      *  get Duration of a wav file.
      * @param  wavFilePath
@@ -26,6 +33,51 @@ public class AudioUtils {
         }
     }
 
+    /**
+     *  Concatenate multiple WAV file paths.
+     * @return
+     */
+    public static TtsFileInfo joinTtsFiles(String ttsFiles){
+        String traceId =  ThreadLocalTraceId.getInstance().getTraceId();
+        StringBuilder ttsFileUnion = new StringBuilder();
+        Long ttsFileTimeLenTmp = 0L;
+        int ttsFileNumber = 0;
+        if(!StringUtils.isEmpty(ttsFiles)){
+            if(ttsFiles.contains(";")) {
+                String[] fileArrs = ttsFiles.split(";");
+                ttsFileUnion.append("file_string://");
+                for (int i = 0; i <= fileArrs.length - 1; i++) {
+                    if (!new File(fileArrs[i]).exists()) {
+                        logger.error("{} The tts file {} does not exist and will be skipped. ", traceId, fileArrs[i]);
+                    }else{
+                        ttsFileNumber ++;
+                        if(i != fileArrs.length - 1){
+                            ttsFileUnion.append(fileArrs[i]).append("!");
+                        }else{
+                            ttsFileUnion.append(fileArrs[i]);
+                        }
+                        ttsFileTimeLenTmp += getWavFileDuration(fileArrs[i]);
+                    }
+                }
+            }else{
+                // only one single file
+                if (!new File(ttsFiles).exists()) {
+                    logger.error("{} The tts file {} does not exist and will be skipped. ", traceId, ttsFiles);
+                }else {
+                    ttsFileNumber ++;
+                    ttsFileUnion.append(ttsFiles);
+                    ttsFileTimeLenTmp += getWavFileDuration(ttsFiles);
+                }
+            }
+        }
+        if(ttsFileNumber > 0) {
+            logger.info("{} Multiple WAV files have been successfully concatenated. The total duration of the merged file is {} milliseconds ",
+                    traceId, ttsFileTimeLenTmp
+            );
+        }
+        return new TtsFileInfo(ttsFileUnion.toString(), ttsFileTimeLenTmp, ttsFileNumber);
+    }
+
     public static void main(String[] args) {
        long duration =  getWavFileDuration("C:\\Users\\zhaohai\\Downloads\\zh.wav");
        System.out.println(duration);

+ 91 - 0
src/main/java/com/telerobot/fs/config/OriginateSessionErrorCode.java

@@ -0,0 +1,91 @@
+package com.telerobot.fs.config;
+
+/**
+ * ErrorCode for originate session fails
+ * @author easycallcenter365
+ */
+public class OriginateSessionErrorCode {
+
+    public static final String NONE = "NONE";
+    public static final String UNALLOCATED_NUMBER = "UNALLOCATED_NUMBER";
+    public static final String NO_ROUTE_TRANSIT_NET = "NO_ROUTE_TRANSIT_NET";
+    public static final String NO_ROUTE_DESTINATION = "NO_ROUTE_DESTINATION";
+    public static final String CHANNEL_UNACCEPTABLE = "CHANNEL_UNACCEPTABLE";
+    public static final String CALL_AWARDED_DELIVERED = "CALL_AWARDED_DELIVERED";
+    public static final String NORMAL_CLEARING = "NORMAL_CLEARING";
+    public static final String USER_BUSY = "USER_BUSY";
+    public static final String NO_USER_RESPONSE = "NO_USER_RESPONSE";
+    public static final String NO_ANSWER = "NO_ANSWER";
+    public static final String SUBSCRIBER_ABSENT = "SUBSCRIBER_ABSENT";
+    public static final String CALL_REJECTED = "CALL_REJECTED";
+    public static final String NUMBER_CHANGED = "NUMBER_CHANGED";
+    public static final String REDIRECTION_TO_NEW_DESTINATION = "REDIRECTION_TO_NEW_DESTINATION";
+    public static final String EXCHANGE_ROUTING_ERROR = "EXCHANGE_ROUTING_ERROR";
+    public static final String DESTINATION_OUT_OF_ORDER = "DESTINATION_OUT_OF_ORDER";
+    public static final String INVALID_NUMBER_FORMAT = "INVALID_NUMBER_FORMAT";
+    public static final String FACILITY_REJECTED = "FACILITY_REJECTED";
+    public static final String RESPONSE_TO_STATUS_ENQUIRY = "RESPONSE_TO_STATUS_ENQUIRY";
+    public static final String NORMAL_UNSPECIFIED = "NORMAL_UNSPECIFIED";
+    public static final String NORMAL_CIRCUIT_CONGESTION = "NORMAL_CIRCUIT_CONGESTION";
+    public static final String NETWORK_OUT_OF_ORDER = "NETWORK_OUT_OF_ORDER";
+    public static final String NORMAL_TEMPORARY_FAILURE = "NORMAL_TEMPORARY_FAILURE";
+    public static final String SWITCH_CONGESTION = "SWITCH_CONGESTION";
+    public static final String ACCESS_INFO_DISCARDED = "ACCESS_INFO_DISCARDED";
+    public static final String REQUESTED_CHAN_UNAVAIL = "REQUESTED_CHAN_UNAVAIL";
+    public static final String PRE_EMPTED = "PRE_EMPTED";
+    public static final String FACILITY_NOT_SUBSCRIBED = "FACILITY_NOT_SUBSCRIBED";
+    public static final String OUTGOING_CALL_BARRED = "OUTGOING_CALL_BARRED";
+    public static final String INCOMING_CALL_BARRED = "INCOMING_CALL_BARRED";
+    public static final String BEARERCAPABILITY_NOTAUTH = "BEARERCAPABILITY_NOTAUTH";
+    public static final String BEARERCAPABILITY_NOTAVAIL = "BEARERCAPABILITY_NOTAVAIL";
+    public static final String SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE";
+    public static final String BEARERCAPABILITY_NOTIMPL = "BEARERCAPABILITY_NOTIMPL";
+    public static final String CHAN_NOT_IMPLEMENTED = "CHAN_NOT_IMPLEMENTED";
+    public static final String FACILITY_NOT_IMPLEMENTED = "FACILITY_NOT_IMPLEMENTED";
+    public static final String SERVICE_NOT_IMPLEMENTED = "SERVICE_NOT_IMPLEMENTED";
+    public static final String INVALID_CALL_REFERENCE = "INVALID_CALL_REFERENCE";
+    public static final String INCOMPATIBLE_DESTINATION = "INCOMPATIBLE_DESTINATION";
+    public static final String INVALID_MSG_UNSPECIFIED = "INVALID_MSG_UNSPECIFIED";
+    public static final String MANDATORY_IE_MISSING = "MANDATORY_IE_MISSING";
+    public static final String MESSAGE_TYPE_NONEXIST = "MESSAGE_TYPE_NONEXIST";
+    public static final String WRONG_MESSAGE = "WRONG_MESSAGE";
+    public static final String IE_NONEXIST = "IE_NONEXIST";
+    public static final String INVALID_IE_CONTENTS = "INVALID_IE_CONTENTS";
+    public static final String WRONG_CALL_STATE = "WRONG_CALL_STATE";
+    public static final String RECOVERY_ON_TIMER_EXPIRE = "RECOVERY_ON_TIMER_EXPIRE";
+    public static final String MANDATORY_IE_LENGTH_ERROR = "MANDATORY_IE_LENGTH_ERROR";
+    public static final String PROTOCOL_ERROR = "PROTOCOL_ERROR";
+    public static final String INTERWORKING = "INTERWORKING";
+    public static final String SUCCESS = "SUCCESS";
+    public static final String ORIGINATOR_CANCEL = "ORIGINATOR_CANCEL";
+    public static final String CRASH = "CRASH";
+    public static final String SYSTEM_SHUTDOWN = "SYSTEM_SHUTDOWN";
+    public static final String LOSE_RACE = "LOSE_RACE";
+    public static final String MANAGER_REQUEST = "MANAGER_REQUEST";
+    public static final String BLIND_TRANSFER = "BLIND_TRANSFER";
+    public static final String ATTENDED_TRANSFER = "ATTENDED_TRANSFER";
+    public static final String ALLOTTED_TIMEOUT = "ALLOTTED_TIMEOUT";
+    public static final String USER_CHALLENGE = "USER_CHALLENGE";
+    public static final String MEDIA_TIMEOUT = "MEDIA_TIMEOUT";
+    public static final String PICKED_OFF = "PICKED_OFF";
+    public static final String USER_NOT_REGISTERED = "USER_NOT_REGISTERED";
+    public static final String PROGRESS_TIMEOUT = "PROGRESS_TIMEOUT";
+    public static final String INVALID_GATEWAY = "INVALID_GATEWAY";
+    public static final String GATEWAY_DOWN = "GATEWAY_DOWN";
+    public static final String INVALID_URL = "INVALID_URL";
+    public static final String INVALID_PROFILE = "INVALID_PROFILE";
+    public static final String NO_PICKUP = "NO_PICKUP";
+    public static final String SRTP_READ_ERROR = "SRTP_READ_ERROR";
+    public static final String BOWOUT = "BOWOUT";
+    public static final String BUSY_EVERYWHERE = "BUSY_EVERYWHERE";
+    public static final String DECLINE = "DECLINE";
+    public static final String DOES_NOT_EXIST_ANYWHERE = "DOES_NOT_EXIST_ANYWHERE";
+    public static final String NOT_ACCEPTABLE = "NOT_ACCEPTABLE";
+    public static final String UNWANTED = "UNWANTED";
+    public static final String NO_IDENTITY = "NO_IDENTITY";
+    public static final String BAD_IDENTITY_INFO = "BAD_IDENTITY_INFO";
+    public static final String UNSUPPORTED_CERTIFICATE = "UNSUPPORTED_CERTIFICATE";
+    public static final String INVALID_IDENTITY = "INVALID_IDENTITY";
+    public static final String STALE_DATE = "STALE_DATE";
+    public static final String REJECT_ALL = "REJECT_ALL";
+}

+ 45 - 0
src/main/java/com/telerobot/fs/config/SipSessionStatusCode.java

@@ -0,0 +1,45 @@
+package com.telerobot.fs.config;
+
+/**
+ * Status code for sip call sessions.
+ * @author  easycallcenter365
+ */
+public class SipSessionStatusCode {
+
+    // 1xx - temp response
+
+    public static final int TRYING = 100;
+    public static final int RINGING = 180;
+    public static final int SESSION_PROGRESS = 183;
+
+    public static final int OK = 200;
+
+    // 3xx - redirect response
+
+    public static final int MOVED_TEMPORARILY = 302;
+    public static final int USE_PROXY = 305;
+
+    // 4xx - sip client error
+
+    public static final int BAD_REQUEST = 400;
+    public static final int UNAUTHORIZED = 401;
+    public static final int FORBIDDEN = 403;
+    public static final int NOT_FOUND = 404;
+
+
+    public static final int USER_BUSY = 486;
+    public static final int REQUEST_TERMINATED = 487;
+    public static final int NOT_ACCEPTABLE_HERE = 488;
+
+
+    // 5xx - sip server error
+
+    public static final int SERVER_INTERNAL_ERROR = 500;
+    public static final int SERVICE_UNAVAILABLE = 503;
+
+    // 6xx - global error
+
+    public static final int BUSY_EVERYWHERE = 600;
+    public static final int DECLINE = 603;
+    public static final int DOES_NOT_EXIST_ANYWHERE = 604;
+}

+ 4 - 0
src/main/java/com/telerobot/fs/controller/InboundCallController.java

@@ -189,6 +189,10 @@ public class InboundCallController {
 
 								account.voiceSource = inboundConfig.getVoiceSource();
                                 account.voiceCode = inboundConfig.getVoiceCode();
+								account.ttsModels = inboundConfig.getTtsModels();
+								account.ttsLanguageCode = inboundConfig.getTtsLanguageCode();
+								account.asrModels = inboundConfig.getAsrModels();
+								account.asrLanguageCode = inboundConfig.getAsrLanguageCode();
                                 account.asrProvider = inboundConfig.getAsrProvider();
 								account.aiTransferType = inboundConfig.getAiTransferType();
 								account.aiTransferData = inboundConfig.getAiTransferData();

+ 2 - 2
src/main/java/com/telerobot/fs/controller/ReloadParams.java

@@ -13,9 +13,9 @@ import com.telerobot.fs.tts.aliyun.CosyVoiceDemo;
 import com.telerobot.fs.utils.CommonUtils;
 import com.telerobot.fs.utils.DESUtil;
 import com.telerobot.fs.utils.DateUtils;
-import com.telerobot.fs.utils.StringUtils;
 import com.telerobot.fs.wshandle.MessageHandlerEngine;
 import com.telerobot.fs.wshandle.MessageHandlerEngineList;
+import com.telerobot.fs.wshandle.SecurityManager;
 import com.telerobot.fs.wshandle.SessionEntity;
 import com.telerobot.fs.wshandle.impl.AgentCc;
 import org.slf4j.Logger;
@@ -24,7 +24,6 @@ import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
-import sun.management.resources.agent;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
@@ -42,6 +41,7 @@ public class ReloadParams {
 	@ResponseBody
 	public String reload(HttpServletRequest request,Map<String,Object> model) throws InstantiationException, IllegalAccessException {
 		sysService.refreshParams();
+		SecurityManager.getInstance().reloadFirewallConfig();
 		return "success";
 	}
 

+ 4 - 0
src/main/java/com/telerobot/fs/entity/bo/ChannelFlag.java

@@ -30,6 +30,10 @@ public enum ChannelFlag {
      */
     SATISFACTION_SURVEY_REQUIRED("SATISFACTION_SURVEY_REQUIRED", 5),
 
+    /**
+     * on consultation state
+     */
+    ON_CONSULTATION("ON_CONSULTATION", 6),
 
     /**
      *  当前通话已收到振铃媒体

+ 19 - 0
src/main/java/com/telerobot/fs/entity/bo/InboundDetail.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.annotation.JSONField;
 import com.telerobot.fs.entity.dao.CustmInfoEntity;
 import com.telerobot.fs.utils.StringUtils;
+import com.telerobot.fs.wshandle.SwitchChannel;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -35,6 +36,8 @@ public class InboundDetail {
     private volatile String ivrDtmfDigits = "";
     private volatile String hangupCause = "";
     private volatile boolean startDtmfExecuted = false;
+    private SwitchChannel switchChannel;
+    private volatile String satisfSurveyIvrId = "";
 
     /**
      *  如果该字段不为零,则是视频通话,否则为音频通话
@@ -238,6 +241,14 @@ public class InboundDetail {
         this.manualAnsweredTimeLen = manualAnsweredTimeLen;
     }
 
+    public String getSatisfSurveyIvrId() {
+        return satisfSurveyIvrId;
+    }
+
+    public void setSatisfSurveyIvrId(String satisfSurveyIvrId) {
+        this.satisfSurveyIvrId = satisfSurveyIvrId;
+    }
+
     public boolean getStartDtmfExecuted() {
         return startDtmfExecuted;
     }
@@ -246,6 +257,14 @@ public class InboundDetail {
         this.startDtmfExecuted = startDtmfExecuted;
     }
 
+    public SwitchChannel getSwitchChannel() {
+        return switchChannel;
+    }
+
+    public void setSwitchChannel(SwitchChannel switchChannel) {
+        this.switchChannel = switchChannel;
+    }
+
     public void setIvrDtmfDigits(String ivrDtmfDigits) {
         this.ivrDtmfDigits = ivrDtmfDigits;
         if(getOutboundPhoneInfo() != null){

+ 36 - 0
src/main/java/com/telerobot/fs/entity/dao/CallTaskEntity.java

@@ -41,6 +41,10 @@ public class CallTaskEntity {
 	 */
 	private int playTimes;
 
+	private String asrLanguageCode;
+	private String ttsLanguageCode;
+	private String asrModels;
+	private String ttsModels;
 	private String voiceCode;
 	private String voiceSource;
 
@@ -260,6 +264,38 @@ public class CallTaskEntity {
 		return voiceSource;
 	}
 
+	public String getTtsModels() {
+		return ttsModels;
+	}
+
+	public void setTtsModels(String ttsModels) {
+		this.ttsModels = ttsModels;
+	}
+
+	public String getAsrLanguageCode() {
+		return asrLanguageCode;
+	}
+
+	public void setAsrLanguageCode(String asrLanguageCode) {
+		this.asrLanguageCode = asrLanguageCode;
+	}
+
+	public String getTtsLanguageCode() {
+		return ttsLanguageCode;
+	}
+
+	public void setTtsLanguageCode(String ttsLanguageCode) {
+		this.ttsLanguageCode = ttsLanguageCode;
+	}
+
+	public String getAsrModels() {
+		return asrModels;
+	}
+
+	public void setAsrModels(String asrModels) {
+		this.asrModels = asrModels;
+	}
+
 	public void setVoiceSource(String voiceSource) {
 		this.voiceSource = voiceSource;
 	}

+ 52 - 0
src/main/java/com/telerobot/fs/entity/dao/CcExtNum.java

@@ -0,0 +1,52 @@
+package com.telerobot.fs.entity.dao;
+
+public class CcExtNum {
+    /**
+     * 流水编号
+     */
+    private Integer extId;
+    /**
+     * 分机号
+     */
+    private String extNum;
+    /**
+     * 分机密码
+     */
+    private String extPass;
+    /**
+     * 所属员工/绑定关系
+     */
+    private String userCode;
+
+    public Integer getExtId() {
+        return extId;
+    }
+
+    public void setExtId(Integer extId) {
+        this.extId = extId;
+    }
+
+    public String getExtNum() {
+        return extNum;
+    }
+
+    public void setExtNum(String extNum) {
+        this.extNum = extNum;
+    }
+
+    public String getExtPass() {
+        return extPass;
+    }
+
+    public void setExtPass(String extPass) {
+        this.extPass = extPass;
+    }
+
+    public String getUserCode() {
+        return userCode;
+    }
+
+    public void setUserCode(String userCode) {
+        this.userCode = userCode;
+    }
+}

+ 4 - 0
src/main/java/com/telerobot/fs/entity/dto/InboundConfig.java

@@ -7,6 +7,10 @@ public class InboundConfig {
     private int id;
     private int llmAccountId;
     private String callee;
+    private String asrLanguageCode;
+    private String ttsLanguageCode;
+    private String asrModels;
+    private String ttsModels;
     private String voiceCode;
     private String voiceSource;
     private String asrProvider;

+ 1 - 1
src/main/java/com/telerobot/fs/entity/dto/LlmAiphoneRes.java

@@ -43,7 +43,7 @@ public class LlmAiphoneRes {
     /**
      * tts合成文件路径(预合成)
      */
-    private String ttsFilePath;
+    private String ttsFilePathList;
 
     /**
      * 流式合成文本

+ 27 - 1
src/main/java/com/telerobot/fs/entity/dto/llm/AccountBaseEntity.java

@@ -28,11 +28,15 @@ package com.telerobot.fs.entity.dto.llm;
      */
     public String openingRemarks;
 
-
     public String voiceSource;
 
     public String voiceCode;
 
+    public String asrLanguageCode;
+    public String ttsLanguageCode;
+    public String asrModels;
+    public String ttsModels;
+
     public String asrProvider;
 
     /**
@@ -76,4 +80,26 @@ package com.telerobot.fs.entity.dto.llm;
     public String transferManualDigit;
 
     public int kbCatId;
+
+
+
+    /**
+     * The voice prompt played when  transferring to agent tips string (wav file)
+     */
+    public String transferToAgentTipsWav;
+
+    /**
+     * The voice prompt played when call session hangup (wav file)
+     */
+    public String hangupTipsWav;
+
+    /**
+     * tips for No Voice (wav file)
+     */
+    public String customerNoVoiceTipsWav;
+
+    /**
+     * The opening remarks of a phone call (wav file)
+     */
+    public String openingRemarksWav;
 }

+ 4 - 1
src/main/java/com/telerobot/fs/entity/pojo/LlmToolRequest.java

@@ -3,12 +3,15 @@ package com.telerobot.fs.entity.pojo;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.config.SystemConfig;
+import com.telerobot.fs.utils.RegExp;
+
+import java.util.List;
 
 public class LlmToolRequest {
 
     public static final String TRANSFER_TO_AGENT = "transferToAgent";
     public static final String TRANSFER_TO_TEL = "transferToTel";
-    public static final String TRANSFER_TO_TEL_REGEXP = "transferToTel\\D{1,4}\\d{7,12}";
+    public static final String TRANSFER_TO_TEL_REGEXP = "transferToTel\\D{1,4}\\d{1,12}";
     public static final String HANGUP = "hangupCall";
     public static final String KB_QUERY = "kbQuery";
 

+ 26 - 0
src/main/java/com/telerobot/fs/entity/pojo/TtsFileInfo.java

@@ -0,0 +1,26 @@
+package com.telerobot.fs.entity.pojo;
+
+public class TtsFileInfo {
+    private String filesString;
+    private Long timeLength = 0L;
+    private int ttsFileNumber = 0;
+
+    public TtsFileInfo(String filesString, Long timeLength, int ttsFileNumber) {
+        this.filesString = filesString;
+        this.timeLength = timeLength;
+        this.ttsFileNumber = ttsFileNumber;
+    }
+
+    public String getFilesString() {
+        return filesString;
+    }
+
+    public int getTtsFileNumber() {
+        return ttsFileNumber;
+    }
+
+    public Long getTimeLength() {
+        return timeLength;
+    }
+
+}

+ 1 - 0
src/main/java/com/telerobot/fs/entity/pojo/TtsProvider.java

@@ -5,4 +5,5 @@ public class TtsProvider {
     public static final String DOUBAO = "doubao_vcl_tts";
     public static final String MICROSOFT = "microsoft_tts";
     public static final String CHINA_TELECOM = "chinatelecom_tts";
+
 }

+ 3 - 6
src/main/java/com/telerobot/fs/global/CdrPush.java

@@ -33,10 +33,10 @@ public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
     private static Semaphore semaphore = new Semaphore(9999);
     private static ArrayBlockingQueue<CdrDetail> cdrQueue = new ArrayBlockingQueue<>(9999);
     private static boolean checkPostCdrEnabled(){
-        return Boolean.parseBoolean(SystemConfig.getValue("post_cdr_enabled", "false"));
+        return Boolean.parseBoolean(SystemConfig.getValue("post_cdr_enabled", "true"));
     }
 
-    public static boolean   addCdrToQueue(CdrDetail cdr){
+    public static boolean addCdrToQueue(CdrDetail cdr){
          if(!checkPostCdrEnabled()){
              logger.info("{} cdr push is not enabled.", cdr.getUuid());
              return false;
@@ -87,7 +87,6 @@ public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
                @SneakyThrows
                @Override
                public void run() {
-                   try {
                        logger.info("CdrPush thread is now running...");
                        while (true) {
                            semaphore.acquire();
@@ -95,13 +94,11 @@ public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
                            if (null != cdrDetail) {
                                if(!postCdr(cdrDetail)){
                                    addCdrToQueue(cdrDetail);
+                                   Thread.sleep(100);
                                }
                            }
                            Thread.sleep(10);
                        }
-                   }catch (Throwable e){
-                       logger.error("postCdr main thread error: {} {}", e.toString(), CommonUtils.getStackTraceString(e.getStackTrace()));
-                   }
                }
            }).start();
     }

+ 4 - 0
src/main/java/com/telerobot/fs/mybatis/dao/SysDao.java

@@ -1,6 +1,7 @@
 package com.telerobot.fs.mybatis.dao;
 
 import com.telerobot.fs.entity.dao.BizGroup;
+import com.telerobot.fs.entity.dao.CcExtNum;
 import com.telerobot.fs.entity.dao.ExtPowerConfig;
 import com.telerobot.fs.entity.dao.LlmKb;
 import com.telerobot.fs.entity.dto.AgentEx;
@@ -71,4 +72,7 @@ public interface SysDao {
 
 	String getUserBizJson(String uuid);
 
+	int updateExtension(CcExtNum ccExtNum);
+
+	List<CcExtNum> selectAllExtensions();
 }

+ 13 - 1
src/main/java/com/telerobot/fs/mybatis/dao/SysDaoImpl.java

@@ -1,6 +1,7 @@
 package com.telerobot.fs.mybatis.dao;
 
 import com.telerobot.fs.entity.dao.BizGroup;
+import com.telerobot.fs.entity.dao.CcExtNum;
 import com.telerobot.fs.entity.dao.ExtPowerConfig;
 import com.telerobot.fs.entity.dao.LlmKb;
 import com.telerobot.fs.entity.dto.AgentEx;
@@ -135,9 +136,20 @@ public class SysDaoImpl implements SysDao {
 	public  List<LlmKb> getKbListByCatId(int catId){
          return mapper.getKbListByCatId(catId);
 	}
-
 	@Override
 	public String getUserBizJson(String uuid){
 		return mapper.getUserBizJson(uuid);
 	}
+	@Override
+	public int updateExtension(CcExtNum ccExtNum) {
+		if (ccExtNum == null || ccExtNum.getExtId() == null) {
+			return 0;
+		}
+		return mapper.updateExtension(ccExtNum);
+	}
+
+	@Override
+	public List<CcExtNum> selectAllExtensions() {
+		return mapper.selectAllExtensions();
+	}
 }

+ 5 - 1
src/main/java/com/telerobot/fs/mybatis/persistence/SysMapper.java

@@ -1,6 +1,7 @@
 package com.telerobot.fs.mybatis.persistence;
 
 import com.telerobot.fs.entity.dao.BizGroup;
+import com.telerobot.fs.entity.dao.CcExtNum;
 import com.telerobot.fs.entity.dao.ExtPowerConfig;
 import com.telerobot.fs.entity.dao.LlmKb;
 import com.telerobot.fs.entity.dto.AgentEx;
@@ -83,6 +84,9 @@ public interface SysMapper {
     int updateParam(@Param("code")String paramCode, @Param("value") String paramValue);
 
     List<LlmKb> getKbListByCatId(int catId);
-
 	String getUserBizJson(@Param("uuid") String uuid);
+
+	int updateExtension(CcExtNum ccExtNum);
+
+	List<CcExtNum> selectAllExtensions();
 }

+ 6 - 2
src/main/java/com/telerobot/fs/outbound/batchcall/BatchTaskManager.java

@@ -190,6 +190,10 @@ public class BatchTaskManager implements Runnable {
             }
             this.llmAccount.voiceSource = batchEntity.getVoiceSource();
             this.llmAccount.voiceCode = batchEntity.getVoiceCode();
+            this.llmAccount.ttsModels = batchEntity.getTtsModels();
+            this.llmAccount.asrModels = batchEntity.getAsrModels();
+            this.llmAccount.asrLanguageCode = batchEntity.getAsrLanguageCode();
+            this.llmAccount.ttsLanguageCode = batchEntity.getTtsLanguageCode();
 
             llmAccount.asrProvider = batchEntity.getAsrProvider();
             llmAccount.aiTransferType = batchEntity.getAiTransferType();
@@ -293,10 +297,10 @@ public class BatchTaskManager implements Runnable {
         for (SessionEntity session : agentList) {
             double passedSecs = (System.currentTimeMillis() - session.getStateChangeTime()) / 1000d;
             boolean free = avgRingTimeLen - (avgCallTalkTimeLen - avgFillFormTimeLen  - passedSecs) > 0;
-            log.info("预测座席坐席 {} 在 {} 秒后的状态是否空闲 {},当前座席状态{}, 当前状态已持续时间: {} 秒",
+            log.info("Predicted that agent {} will be free '{}'  in {} seconds. Current agent status is {}, and the current status has lasted for {} seconds.",
                     session.getOpNum(),
-                    avgRingTimeLen,
                     free,
+                    avgRingTimeLen,
                     AgentStatus.getItemByValue(session.getAgentStatus()).getName(),
                     passedSecs
             );

+ 5 - 0
src/main/java/com/telerobot/fs/outbound/batchcall/CallTask.java

@@ -720,6 +720,11 @@ public class CallTask implements Runnable {
 		}else if(batchEntity.getRegister() == 2) {
 			String authUsername = batchEntity.getAuthUsername();
 			String dynamicGateway = CommonUtils.getDynamicGatewayAddr(authUsername, getTraceId());
+			if(StringUtils.isEmpty(dynamicGateway)){
+				log.error("{} Failed to parse dynamic gateway address. ", getTraceId());
+				releaseThreadNum();
+				return;
+			}
 			log.info("{} successfully get dynamic gateway address : {}", getTraceId(), dynamicGateway);
 			// for dynamic gateway, we must use internal profile
 			callParameter = String.format("{%s%s}sofia/internal/%s%s@%s  &park()",

+ 8 - 1
src/main/java/com/telerobot/fs/robot/AbstractChatRobot.java

@@ -3,6 +3,7 @@ package com.telerobot.fs.robot;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.config.SystemConfig;
+import com.telerobot.fs.config.ThreadLocalTraceId;
 import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.dto.LlmAiphoneRes;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
@@ -83,7 +84,7 @@ public abstract class AbstractChatRobot implements IChatRobot {
             ",", ",",
             ";", ";",
             "。", ".",
-            "、",
+            "、", " ",
             "!", "!",
             ":", ":"
     };
@@ -133,6 +134,12 @@ public abstract class AbstractChatRobot implements IChatRobot {
 
     @Override
     public void sendTtsRequest(String textParam){
+        if(StringUtils.isEmpty(getAccount().voiceSource)){
+            logger.error("{} tts voiceSource is empty, can not process tts request. ",
+                       ThreadLocalTraceId.getInstance().getTraceId()
+            );
+            return;
+        }
         if(StringUtils.isEmpty(textParam)){
             return;
         }

+ 35 - 5
src/main/java/com/telerobot/fs/robot/RobotBase.java

@@ -1,5 +1,6 @@
 package com.telerobot.fs.robot;
 
+import com.telerobot.fs.config.AudioUtils;
 import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.bo.LlmConsumer;
@@ -7,6 +8,7 @@ import com.telerobot.fs.entity.bo.RobotInteractiveParam;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
 import com.telerobot.fs.entity.po.HangupCause;
 import com.telerobot.fs.entity.pojo.SpeechResultEntity;
+import com.telerobot.fs.entity.pojo.TtsFileInfo;
 import com.telerobot.fs.utils.CommonUtils;
 import com.telerobot.fs.utils.ThreadPoolCreator;
 import com.telerobot.fs.utils.ThreadUtil;
@@ -14,6 +16,7 @@ import link.thingscloud.freeswitch.esl.EslConnectionPool;
 import link.thingscloud.freeswitch.esl.EslConnectionUtil;
 import link.thingscloud.freeswitch.esl.IEslEventListener;
 import link.thingscloud.freeswitch.esl.constant.UuidKeys;
+import link.thingscloud.freeswitch.esl.util.CurrentTimeMillisClock;
 import org.apache.commons.lang.StringUtils;
 import org.dom4j.Document;
 import org.dom4j.Element;
@@ -27,6 +30,7 @@ import java.io.InputStream;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.LongAdder;
 
 public abstract class RobotBase implements IEslEventListener {
@@ -65,11 +69,16 @@ public abstract class RobotBase implements IEslEventListener {
         playBackStoppedSignalForLlmConcurrency.release();
     }
     private boolean playBackFinishedSignalReleased = false;
-    protected synchronized void releasePlayBackFinishedSignal(){
-         if(!playBackFinishedSignalReleased){
-             playBackFinishedSignalReleased = true;
-             playBackFinishedSignal.release();
-         }
+    protected void releasePlayBackFinishedSignal(){
+        String lockerKey = String.format("%s%s", uuid, "releasePlayBackFinishedSignal");
+        synchronized (lockerKey.intern()) {
+            if (!playBackFinishedSignalReleased) {
+                playBackFinishedSignalReleased = true;
+                playBackFinishedSignal.release();
+                logger.info("{} releasePlayBackFinishedSignal enter", getTraceId());
+            }
+        }
+        logger.info("{} releasePlayBackFinishedSignal exit", getTraceId());
     }
     protected void waitForPlayBackFinished(int...waiTimeoutMills){
         try {
@@ -134,6 +143,7 @@ public abstract class RobotBase implements IEslEventListener {
         this.currentAsrType = asrType;
     }
 
+
     /**
      * 接收到了挂机信号
      */
@@ -478,6 +488,26 @@ public abstract class RobotBase implements IEslEventListener {
         }
     }
 
+    protected void playSound(String tips){
+        String ttsProvider =  chatRobot.getAccount().voiceSource;
+        if(StringUtils.isEmpty(ttsProvider)) {
+            startPlayback(AudioUtils.joinTtsFiles(tips));
+        }else {
+            chatRobot.sendTtsRequest(tips);
+            chatRobot.closeTts();
+        }
+    }
+
+    public void startPlayback(TtsFileInfo ttsFileInfo) {
+        playbackStartTime = System.currentTimeMillis();
+        playbackEndTime = playbackStartTime + ttsFileInfo.getTimeLength();
+        EslConnectionUtil.sendExecuteCommand(
+                "playback",
+                "{playback_sleep_val=0}" + ttsFileInfo.getFilesString(),
+                 uuid
+        );
+        logger.info("{} Start to play '{}' wav file, {}", getTraceId(), ttsFileInfo.getTtsFileNumber(), ttsFileInfo.getFilesString());
+    }
 
     /**
      *  开启监视线程; 如果一个通话1分钟还收到客户讲话识别结果,则强制结束它;

+ 118 - 48
src/main/java/com/telerobot/fs/robot/RobotChat.java

@@ -3,10 +3,7 @@ package com.telerobot.fs.robot;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.acd.AcdSqlQueue;
-import com.telerobot.fs.config.AppContextProvider;
-import com.telerobot.fs.config.SystemConfig;
-import com.telerobot.fs.config.ThreadLocalTraceId;
-import com.telerobot.fs.config.UuidGenerator;
+import com.telerobot.fs.config.*;
 import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.bo.LlmConsumer;
 import com.telerobot.fs.entity.dao.LlmKb;
@@ -15,11 +12,9 @@ import com.telerobot.fs.entity.dto.LlmAiphoneRes;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
 import com.telerobot.fs.entity.po.CdrDetail;
 import com.telerobot.fs.entity.po.HangupCause;
-import com.telerobot.fs.entity.pojo.AsrProvider;
-import com.telerobot.fs.entity.pojo.LlmToolRequest;
-import com.telerobot.fs.entity.pojo.SpeechResultEntity;
-import com.telerobot.fs.entity.pojo.TtsProvider;
+import com.telerobot.fs.entity.pojo.*;
 import com.telerobot.fs.global.CdrPush;
+import com.telerobot.fs.robot.impl.LocalWebApiTest;
 import com.telerobot.fs.service.InboundDetailService;
 import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.tts.aliyun.AliyunTTSWebApi;
@@ -79,6 +74,9 @@ public class RobotChat extends RobotBase {
         chatRobot.setCallDetail(callDetail);
         chatRobot.setTtsProvider(llmAccountInfo.voiceSource);
         chatRobot.setTtsVoiceName(llmAccountInfo.voiceCode);
+        if(chatRobot instanceof LocalWebApiTest){
+            ((LocalWebApiTest)chatRobot).makeMockData();
+        }
 
         if(getAllowInterrupt() && ASR_TYPE_MRCP.equalsIgnoreCase(this.getAsrModelType())){
             logger.error("{} `robot-speech-interrupt-allowed`  is not effective in the mrcp speech recognition mode.", uuid);
@@ -117,7 +115,7 @@ public class RobotChat extends RobotBase {
             // In the outbound call scenario, solve the problem that the first few words of the first sentence
             // cannot be heard clearly, because it takes about 2 seconds for the customer to transfer from
             // the receiver to the headphones after answering the call.
-            ThreadUtil.sleep(200);
+            ThreadUtil.sleep(500);
             if (isHangup) {
                 return;
             }
@@ -126,6 +124,16 @@ public class RobotChat extends RobotBase {
         String ttsProvider =  chatRobot.getAccount().voiceSource;
         String asrProvider =  chatRobot.getAccount().asrProvider;
 
+        if(StringUtils.isEmpty(ttsProvider)){
+            logger.warn("{} No TTS voice is currently configured for the robot. Sound playback will be handled by playing pre-synthesized TTS files.",
+                    getTraceId());
+        }
+        if(StringUtils.isEmpty(asrProvider)){
+            logger.error("{} asrProvider cant not be null, please check your speech-to-text configuration. ", getTraceId());
+            hangupAndCloseConn("asrProvider-can-not-be-null");
+            return;
+        }
+
         if(ttsProvider.equalsIgnoreCase(TtsProvider.ALIYUN) || asrProvider.equalsIgnoreCase(AsrProvider.ALIYUN)) {
              if((!AliyunTTSWebApi.setAliyunTokenToFreeSWITCH(uuid))) {
                  String errMsg = "AliyunTTSWebApi getToken error!";
@@ -141,17 +149,34 @@ public class RobotChat extends RobotBase {
         }
 
         if(ttsProvider.equalsIgnoreCase(TtsProvider.DOUBAO)) {
-            logger.info("{}  Current tts provider is doubao!", getTraceId());
+            String ttsModels = chatRobot.getAccount().ttsModels;
+            logger.info("{}  Current tts provider is doubao, set ttsModels={}", getTraceId(), ttsModels);
         }
-
         if(ttsProvider.equalsIgnoreCase(TtsProvider.MICROSOFT)) {
             logger.info("{}  Current tts provider is microsoft!", getTraceId());
         }
-
         if(ttsProvider.equalsIgnoreCase(TtsProvider.CHINA_TELECOM)) {
             logger.info("{}  Current tts provider is china_telecom!", getTraceId());
         }
 
+        // set tts_models、asr_models、asr_language_code、tts_language_code
+        EslConnectionUtil.sendExecuteCommand("set",
+                "ecc365_tts_models=" + chatRobot.getAccount().ttsModels,
+                uuid
+        );
+        EslConnectionUtil.sendExecuteCommand("set",
+                "ecc365_asr_models=" + chatRobot.getAccount().asrModels,
+                uuid
+        );
+        EslConnectionUtil.sendExecuteCommand("set",
+                "ecc365_asr_language_code=" + chatRobot.getAccount().asrLanguageCode,
+                uuid
+        );
+        EslConnectionUtil.sendExecuteCommand("set",
+                "ecc365_tts_language_code=" + chatRobot.getAccount().ttsLanguageCode,
+                uuid
+        );
+
         EslMessage apiResponseMsg = EslConnectionUtil.sendSyncApiCommand(
                 "uuid_exists",
                 uniqueID,
@@ -198,27 +223,37 @@ public class RobotChat extends RobotBase {
                 logger.info("{} PLAYBACK_START event,  time cost = {} ms. ", getTraceId(), timeSpent);
             }
 
-            if(playbackFilePath != null && playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)){
-                releasePlayBackStartSignalForLlmConcurrency();
-                logger.info("{} recv PLAYBACK_START event for wav file {}. ", getTraceId(), playbackFilePath);
+            if(StringUtils.isNotEmpty(playbackFilePath)){
+                if(playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)) {
+                    releasePlayBackStartSignalForLlmConcurrency();
+                    logger.info("{} recv PLAYBACK_START event for wav file {}. ", getTraceId(), playbackFilePath);
+                }else{
+                    logger.info("{} The currently playing file is {} . ", getTraceId(), playbackFilePath);
+                }
             }
         }else if(EventNames.CHANNEL_PARK.equalsIgnoreCase(eventName))
         {
             logger.info("{} recv CHANNEL_PARK event. ", uuid);
         }
         else if (EventNames.PLAYBACK_STOP.equalsIgnoreCase(eventName)) {
-            if(playbackFilePath != null && playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)){
-                 releasePlayBackStoppedSignalForLlmConcurrency();
-                 logger.info("{} recv PLAYBACK_STOP event for wav file {}. ", getTraceId(), playbackFilePath);
-                 return;
+            if(StringUtils.isNotEmpty(playbackFilePath)) {
+                if (playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)) {
+                    releasePlayBackStoppedSignalForLlmConcurrency();
+                    logger.info("{} recv PLAYBACK_STOP event for wav file {}. ", getTraceId(), playbackFilePath);
+                }else {
+                    logger.info("{} The playback of the file {} has completed. ", getTraceId(), playbackFilePath);
+                    recvPlayBackEndEvent = true;
+                    playbackEndTime = System.currentTimeMillis();
+                    releasePlayBackFinishedSignal();
+                }
             }
 
             if(EventNames.PLAYBACK_STOP.equalsIgnoreCase(detail)) {
+                logger.info("{} ************ streaming tts playback finished.", getTraceId());
                 chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
                 recvPlayBackEndEvent = true;
                 playbackEndTime = System.currentTimeMillis();
                 releasePlayBackFinishedSignal();
-                logger.info("{} streaming tts playback finished.", getTraceId());
             }
 
             if(recvHangupSignal){
@@ -248,6 +283,9 @@ public class RobotChat extends RobotBase {
                          interruptRobotSpeech();
                          releasePlayBackFinishedSignal();
                          ThreadUtil.sleep(100);
+                         if(StringUtils.isEmpty(chatRobot.getAccount().voiceSource)) {
+                             ttsChannelClosed = true;
+                         }
                          // wait for tts closed
                          int step = 50;
                          int maxWaitMills = 2000;
@@ -322,13 +360,14 @@ public class RobotChat extends RobotBase {
                chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
                logger.info("{}  TtsChannelClosed = true.", getTraceId());
                ttsChannelClosed = true;
+               releasePlayBackFinishedSignal();
            }
            if("Speech-Open".equalsIgnoreCase(event)){
-                chatRobot.setTtsChannelState(TtsChannelState.OPENED);
-                chatRobot.flushTtsRequestQueue();
-                long timeSpent = System.currentTimeMillis() - playbackStartTime;
-                logger.info("{} Speech-Open event,  time cost = {} ms. ", getTraceId(), timeSpent);
-            }
+               chatRobot.setTtsChannelState(TtsChannelState.OPENED);
+               chatRobot.flushTtsRequestQueue();
+               long timeSpent = System.currentTimeMillis() - playbackStartTime;
+               logger.info("{} Speech-Open event,  time cost = {} ms. ", getTraceId(), timeSpent);
+           }
 
             if ("NetworkError".equalsIgnoreCase(event)) {
                 CommonUtils.setHangupCauseDetail(
@@ -387,7 +426,8 @@ public class RobotChat extends RobotBase {
                 );
                 if (recvPlayBackEndEvent || getAllowInterrupt()) {
                     if (!interactiveParam.checkInSpeaking()) {
-                        synchronized (getTraceId().intern()) {
+                        String lockerKey = String.format("%s%s", uuid, "checkInSpeaking");
+                        synchronized (lockerKey.intern()) {
                             if (!interactiveParam.checkInSpeaking()) {
                                 interactiveParam.setInSpeaking(true);
                                 // Main thread awakened to extend customer speaking time beyond 6 seconds.
@@ -435,7 +475,8 @@ public class RobotChat extends RobotBase {
     }
 
     private boolean setTransferState() {
-        synchronized (uuid.intern()) {
+        String lockerKey = String.format("%s%s", uuid, "setTransferState");
+        synchronized (lockerKey.intern()) {
             if (transferToAgent) {
                 logger.info("{} transferring to a human operator is already being handled. skip...", getTraceId());
                 return false;
@@ -576,6 +617,7 @@ public class RobotChat extends RobotBase {
     public void backgroundJobResultReceived(String addr, EslEvent event) {
     }
 
+
     /**
      * interactWithRobot
      **/
@@ -612,8 +654,7 @@ public class RobotChat extends RobotBase {
                     if (counter > MAX_CONSECUTIVE_NO_VOICE_NUMBER) {
                         logger.info("{} There has been no sound for {} consecutive times. Play hangupTips and then hangup call.",
                                 getTraceId(), MAX_CONSECUTIVE_NO_VOICE_NUMBER);
-                        chatRobot.sendTtsRequest(chatRobot.getAccount().hangupTips);
-                        chatRobot.closeTts();
+                        playSound(chatRobot.getAccount().hangupTips);
                         recvHangupSignal = true;
                         return;
                     }
@@ -641,8 +682,7 @@ public class RobotChat extends RobotBase {
                 if (aiphoneRes == null || aiphoneRes.getStatus_code() == 0) {
                     String tips = SystemConfig.getValue("llm-max-try-fail-tips", "");
                     if (!StringUtils.isEmpty(tips)) {
-                        chatRobot.sendTtsRequest(tips);
-                        chatRobot.closeTts();
+                        playSound(tips);
                     } else {
                         CommonUtils.setHangupCauseDetail(
                                 callDetail,
@@ -650,8 +690,8 @@ public class RobotChat extends RobotBase {
                                 String.format("The large model failed to access successfully despite more than %d connection attempts.", LLM_MAX_TRY)
                         );
                         hangupAndCloseConn("reach-llm-max-try-error");
-                        return;
                     }
+                    return;
                 }
 
                 talkRound.increment();
@@ -660,8 +700,8 @@ public class RobotChat extends RobotBase {
                         getTraceId(), spentCost, JSON.toJSONString(aiphoneRes)
                 );
 
-                if(aiphoneRes != null && aiphoneRes.getStatus_code() == 1) {
 
+                if(aiphoneRes.getStatus_code() == 1) {
                     ttsChannelClosed = false;
                     String body = aiphoneRes.getBody();
                     if(!StringUtils.isEmpty(body)){
@@ -704,17 +744,22 @@ public class RobotChat extends RobotBase {
                         if(!setTransferState()){
                             return;
                         }
-                        doTransferToManualAgent(body);
+                        doTransferToManualAgent(aiphoneRes);
                         return;
                     }
 
                     if (aiphoneRes.getClose_phone() == 1) {
                         if(StringUtils.isEmpty(body)){
-                            chatRobot.sendTtsRequest(chatRobot.getAccount().hangupTips);
+                            playSound(chatRobot.getAccount().hangupTips);
+                        }else{
+                            playResponse(aiphoneRes);
                         }
-                        chatRobot.closeTts();
                         waitForPlayBackFinished(11000);
                         long startTimeTick = System.currentTimeMillis();
+                        if(StringUtils.isEmpty(chatRobot.getAccount().voiceSource)){
+                            ttsChannelClosed = true;
+                            ThreadUtil.sleep(2000);
+                        }
                         while (!ttsChannelClosed && !isHangup) {
                             ThreadUtil.sleep(1000);
                             if(System.currentTimeMillis() - startTimeTick > 11000){
@@ -724,6 +769,9 @@ public class RobotChat extends RobotBase {
                         hangupAndCloseConn("system-hangup");
                         return;
                     }
+
+                    // play wav file
+                    playResponse(aiphoneRes);
                 }
 
             } catch (Throwable e) {
@@ -740,7 +788,7 @@ public class RobotChat extends RobotBase {
             }
 
 
-        if(aiphoneRes != null && aiphoneRes.getIfcan_interrupt() == 1) {
+        if(aiphoneRes.getIfcan_interrupt() == 1) {
             interactiveParam.setAllowInterrupt(1);
             logger.info("{} allowSpeechInterrupt={}", getTraceId(), 1);
         }
@@ -766,12 +814,29 @@ public class RobotChat extends RobotBase {
         }
     }
 
-    private synchronized void doTransferToManualAgent(String audioTipsText){
-        if(transferToAgentExecuted){
-            logger.warn("{} The call transfer to a human agent has already been processed.", getTraceId());
-            return;
+    private void playResponse(LlmAiphoneRes aiphoneRes){
+        String ttsFilePathList = aiphoneRes.getTtsFilePathList();
+        if(!StringUtils.isEmpty(ttsFilePathList)){
+            TtsFileInfo ttsFileInfo = AudioUtils.joinTtsFiles(ttsFilePathList);
+            logger.info("{} try to play wav file for text {}.", getTraceId(), aiphoneRes.getBody());
+            startPlayback(ttsFileInfo);
+        }
+    }
+
+    private  void doTransferToManualAgent(LlmAiphoneRes aiphoneRes){
+        String audioTipsText = null;
+        if(aiphoneRes != null) {
+            audioTipsText = aiphoneRes.getBody();
         }
-        transferToAgentExecuted = true;
+        String lockerKey = String.format("%s%s", uuid, "doTransferToManualAgent");
+        synchronized (lockerKey.intern()) {
+            if (transferToAgentExecuted) {
+                logger.warn("{} The call transfer to a human agent has already been processed.", getTraceId());
+                return;
+            }
+            transferToAgentExecuted = true;
+        }
+
         callDetail.setChatContent(chatRobot.getDialogues());
         // Replace the prompt words for manual transfer in the text with blank spaces.
         String transferToTel = "";
@@ -786,7 +851,7 @@ public class RobotChat extends RobotBase {
             for (String match : matches) {
                 audioTipsText = audioTipsText.replace(match, "");
 
-                List<String> tmp = RegExp.GetMatchFromStringByRegExp(match, "\\d{7,12}");
+                List<String> tmp = RegExp.GetMatchFromStringByRegExp(match, "\\d{1,12}");
                 transferToTel = tmp.get(0);
                 logger.info("{} Successfully retrieved transferToTel number {}", uuid, transferToTel);
 
@@ -797,13 +862,17 @@ public class RobotChat extends RobotBase {
             }
         }
 
-        if(StringUtils.isEmpty(audioTipsText)){
+        if(StringUtils.isEmpty(audioTipsText) || audioTipsText.equalsIgnoreCase(LlmToolRequest.TRANSFER_TO_AGENT)){
             String tips = chatRobot.getAccount().transferToAgentTips;
             logger.info("{} Try to play tts  transferToAgentTips {}", getTraceId(), tips);
-            chatRobot.sendTtsRequest(tips);
-            chatRobot.closeTts();
-            waitForPlayBackFinished(6000);
+            if(!StringUtils.isEmpty(tips)) {
+                playSound(tips);
+                waitForPlayBackFinished(9000);
+            }
             // wait for tips playback finished
+        }else {
+            playResponse(aiphoneRes);
+            waitForPlayBackFinished(9000);
         }
 
         // stop_asr 的顺序很重要,需要放在播放tts之后,否则不起作用;会被uuid_break清空指令;
@@ -863,7 +932,8 @@ public class RobotChat extends RobotBase {
         long startWaitTimeMills = System.currentTimeMillis();
         logger.info("{} wait for customer speaking  ...", getTraceId());
 
-        acquire(7000);
+        Integer maxWaitTimeCustomerSpeaking = Integer.parseInt(SystemConfig.getValue("max-wait-time-customer-speaking", "7000")) ;
+        acquire(maxWaitTimeCustomerSpeaking);
 
         logger.info("{} wait for customer speaking, time passed = {}ms.  ...",
                 getTraceId(),

+ 246 - 0
src/main/java/com/telerobot/fs/robot/impl/ChatGPT.java

@@ -0,0 +1,246 @@
+package com.telerobot.fs.robot.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.config.AppContextProvider;
+import com.telerobot.fs.entity.dao.LlmKb;
+import com.telerobot.fs.entity.dto.LlmAiphoneRes;
+import com.telerobot.fs.entity.dto.llm.LlmAccount;
+import com.telerobot.fs.entity.po.HangupCause;
+import com.telerobot.fs.entity.pojo.LlmToolRequest;
+import com.telerobot.fs.robot.AbstractChatRobot;
+import com.telerobot.fs.service.SysService;
+import com.telerobot.fs.utils.CommonUtils;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ChatGPT extends AbstractChatRobot {
+
+    @Override
+    public LlmAiphoneRes  talkWithAiAgent(String question, Boolean... withKbResponse) {
+        LlmAiphoneRes aiphoneRes = new LlmAiphoneRes();
+        aiphoneRes.setStatus_code(1);
+        aiphoneRes.setClose_phone(0);
+        aiphoneRes.setIfcan_interrupt(0);
+        if (firstRound) {
+            firstRound = false;
+
+            String llmTips = ((LlmAccount) getAccount()).getLlmTips();
+            int catId = llmAccountInfo.kbCatId;
+            String topicsVar = "${kbTopicList}";
+            if(catId != -1) {
+                if (llmTips.contains(topicsVar)) {
+                    List<LlmKb> kbList = AppContextProvider.getBean(SysService.class).getKbListByCatId(catId);
+                    StringBuilder topics = new StringBuilder();
+                    for (LlmKb llmKb : kbList) {
+                        topics.append("*").append(llmKb.getTitle()).append("\r\n");
+                    }
+                    topics.append("\r\n");
+                    llmTips = llmTips.replace(topicsVar, topics.toString());
+                    logger.info("{} topic list: {}", uuid, topics.toString());
+                } else {
+                    logger.warn("{} {} tag not found, the knowledge base function will be unavailable. ", uuid, topicsVar);
+                }
+            }else {
+                logger.warn("{} The current model {} doesn’t have a knowledge base linked to it. ", uuid, ((LlmAccount) getAccount()).getModelName());
+            }
+
+            String tips = llmTips  + "\r\n\r\n" + ((LlmAccount) getAccount()).getFaqContext();
+            JSONObject bizJson = new JSONObject();
+            if (null != callDetail && null != callDetail.getOutboundPhoneInfo() && StringUtils.isNotBlank(callDetail.getOutboundPhoneInfo().getBizJson())) {
+                tips += "\n bizJson:" + callDetail.getOutboundPhoneInfo().getBizJson();
+                bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
+            }
+            addDialogue(ROLE_SYSTEM, tips);
+
+            String openingRemarks = replaceParams(llmAccountInfo.openingRemarks, bizJson);
+
+            addDialogue(ROLE_ASSISTANT, openingRemarks);
+
+            ttsTextCache.add(openingRemarks);
+            sendToTts();
+            closeTts();
+
+            aiphoneRes.setBody(openingRemarks);
+            return aiphoneRes;
+        } else {
+            if (withKbResponse.length > 0 && !withKbResponse[0]) {
+                if (!StringUtils.isEmpty(question)) {
+                    addDialogue(ROLE_USER, question);
+                } else {
+                    addDialogue(ROLE_USER, "NO_VOICE");
+
+                    String noVoiceTips = llmAccountInfo.customerNoVoiceTips;
+                    addDialogue(ROLE_ASSISTANT, noVoiceTips);
+
+                    ttsTextCache.add(noVoiceTips);
+                    sendToTts();
+                    closeTts();
+
+                    aiphoneRes.setBody(noVoiceTips);
+                    return aiphoneRes;
+                }
+            }
+
+            try {
+                JSONObject response = sendStreamingRequest(aiphoneRes, llmRoundMessages);
+                if (null != response) {
+                    llmRoundMessages.add(response);
+                } else {
+                    aiphoneRes.setStatus_code(0);
+                }
+            } catch (Throwable throwable) {
+                aiphoneRes.setStatus_code(0);
+                logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
+            }
+            return aiphoneRes;
+        }
+    }
+
+
+    private  JSONObject sendStreamingRequest(LlmAiphoneRes aiphoneRes, List<JSONObject> messages) throws IOException {
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("model", ((LlmAccount)getAccount()).getModelName());
+        requestBody.put("stream", true);
+        // enable stream output
+
+
+        JSONArray messagesArray = new JSONArray();
+        messagesArray.addAll(messages);
+        requestBody.put("messages", messagesArray);
+
+        RequestBody body = RequestBody.create(
+                MediaType.parse("application/json"),
+                requestBody.toJSONString()
+        );
+
+        Request request = new Request.Builder()
+                .url(getAccount().serverUrl)
+                .post(body)
+                .addHeader("Authorization", "Bearer " + ((LlmAccount)getAccount()).getApiKey())
+                .build();
+
+        boolean recvData = false;
+        long startTime = System.currentTimeMillis();
+
+        try (Response response = CLIENT.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                logger.error("Model api error: http-code={}, msg={}, url={}",
+                        response.code(),
+                        response.message(),
+                        getAccount().serverUrl
+                );
+                if(response.code() == HttpStatus.SC_UNAUTHORIZED) {
+                   CommonUtils.setHangupCauseDetail(
+                           callDetail,
+                           HangupCause.LLM_API_KEY_INCORRECT,
+                           "http-status-code=" + response.code()
+                   );
+                  CommonUtils.hangupCallSession(uuid,  HangupCause.LLM_API_KEY_INCORRECT.getCode());
+                  return null;
+                }else{
+                    CommonUtils.setHangupCauseDetail(
+                            callDetail,
+                            HangupCause.LLM_API_SERVER_ERROR,
+                            "http-status-code=" + response.code()
+                    );
+                }
+
+                if(response.code() == HttpStatus.SC_UNAUTHORIZED || response.code() >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+                    throw new IOException("Unexpected code " + response);
+                }else{
+                    return null;
+                }
+            }
+
+            BufferedSource source = response.body().source();
+            StringBuilder responseBuilder = new StringBuilder();
+
+//                data: {"choices":[{"delta":{"content":"AI"},"index":0}]}
+//                data: {"choices":[{"delta":{"content":"像星辰"},"index":0}]}
+//                data: {"choices":[{"delta":{"content":"照亮未来"},"index":0}]}
+//                data: [DONE]
+            while (!source.exhausted()) {
+                String line = source.readUtf8Line();
+                try {
+//                    logger.info("{}-->{}", uuid, line);
+                    if (line != null && line.startsWith("data: ")) {
+                        String jsonData = line.substring(5).trim(); // 去掉 "data: " 前缀
+                        if (jsonData.equals("[DONE]")) {
+                            break; // 流式响应结束
+                        }
+
+                        JSONObject jsonResponse = JSON.parseObject(jsonData);
+                        JSONObject message = jsonResponse.getJSONArray("choices")
+                                .getJSONObject(0)
+                                .getJSONObject("delta"); // 注意:流式响应中消息在 "delta" 字段中
+
+                        if (null != message && message.containsKey("content")) {
+                            String speechContent = message.getString("content");
+                            logger.info("{} speechContent: {}", getTraceId(), speechContent);
+
+                            if (!StringUtils.isEmpty(speechContent)) {
+
+                                if (speechContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
+                                    aiphoneRes.setTransferToAgent(1);
+                                    logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
+                                }
+
+                                if (speechContent.contains(LlmToolRequest.HANGUP)) {
+                                    aiphoneRes.setClose_phone(1);
+                                    logger.info("{} `HANGUP` command detected. ", getTraceId());
+                                }
+
+                                if (!StringUtils.isEmpty(speechContent)) {
+                                    speechContent = speechContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
+                                            .replace(LlmToolRequest.HANGUP,"")
+                                            .replace("`","");
+                                    ttsTextCache.add(speechContent);
+                                    ttsTextLength += speechContent.length();
+                                    // 积攒足够的字数之后,才发送给tts,避免播放异常;
+                                    if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                        sendToTts();
+
+                                        if (!recvData) {
+                                            recvData = true;
+                                            long costTime = (System.currentTimeMillis() - startTime);
+                                            logger.info("http request cost time : {} ms.", costTime);
+                                            aiphoneRes.setCostTime(costTime);
+                                        }
+                                    }
+                                }
+                                responseBuilder.append(speechContent);
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.info("{}-->{}", uuid, line);
+                    logger.error("parse llm response error:{}", ExceptionUtils.getStackTrace(e));
+                }
+            }
+
+            String answer = responseBuilder.toString();
+            logger.info("{} recv llm response end flag. answer={}", this.uuid, answer);
+            if(ttsTextLength > 0){
+                sendToTts();
+            }
+            closeTts();
+
+            JSONObject finalResponse = new JSONObject();
+            finalResponse.put("role", "assistant");
+            finalResponse.put("content", answer);
+            aiphoneRes.setBody(answer);
+            return finalResponse;
+        }
+    }
+}

+ 267 - 0
src/main/java/com/telerobot/fs/robot/impl/ClaudeChat.java

@@ -0,0 +1,267 @@
+package com.telerobot.fs.robot.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.config.AppContextProvider;
+import com.telerobot.fs.entity.dao.LlmKb;
+import com.telerobot.fs.entity.dto.LlmAiphoneRes;
+import com.telerobot.fs.entity.dto.llm.LlmAccount;
+import com.telerobot.fs.entity.po.HangupCause;
+import com.telerobot.fs.entity.pojo.LlmToolRequest;
+import com.telerobot.fs.robot.AbstractChatRobot;
+import com.telerobot.fs.service.SysService;
+import com.telerobot.fs.utils.CommonUtils;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ClaudeChat extends AbstractChatRobot {
+
+    @Override
+    public LlmAiphoneRes  talkWithAiAgent(String question, Boolean... withKbResponse) {
+        LlmAiphoneRes aiphoneRes = new LlmAiphoneRes();
+        aiphoneRes.setStatus_code(1);
+        aiphoneRes.setClose_phone(0);
+        aiphoneRes.setIfcan_interrupt(0);
+        if (firstRound) {
+            firstRound = false;
+
+            String llmTips = ((LlmAccount) getAccount()).getLlmTips();
+            int catId = llmAccountInfo.kbCatId;
+            String topicsVar = "${kbTopicList}";
+            if(catId != -1) {
+                if (llmTips.contains(topicsVar)) {
+                    List<LlmKb> kbList = AppContextProvider.getBean(SysService.class).getKbListByCatId(catId);
+                    StringBuilder topics = new StringBuilder();
+                    for (LlmKb llmKb : kbList) {
+                        topics.append("*").append(llmKb.getTitle()).append("\r\n");
+                    }
+                    topics.append("\r\n");
+                    llmTips = llmTips.replace(topicsVar, topics.toString());
+                    logger.info("{} topic list: {}", uuid, topics.toString());
+                } else {
+                    logger.warn("{} {} tag not found, the knowledge base function will be unavailable. ", uuid, topicsVar);
+                }
+            }else {
+                logger.warn("{} The current model {} doesn’t have a knowledge base linked to it. ", uuid, ((LlmAccount) getAccount()).getModelName());
+            }
+
+            String tips = llmTips  + "\r\n\r\n" + ((LlmAccount) getAccount()).getFaqContext();
+            JSONObject bizJson = new JSONObject();
+            if (null != callDetail && null != callDetail.getOutboundPhoneInfo() && StringUtils.isNotBlank(callDetail.getOutboundPhoneInfo().getBizJson())) {
+                tips += "\n bizJson:" + callDetail.getOutboundPhoneInfo().getBizJson();
+                bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
+            }
+            addDialogue(ROLE_SYSTEM, tips);
+
+            String openingRemarks = replaceParams(llmAccountInfo.openingRemarks, bizJson);
+
+            addDialogue(ROLE_ASSISTANT, openingRemarks);
+
+            ttsTextCache.add(openingRemarks);
+            sendToTts();
+            closeTts();
+
+            aiphoneRes.setBody(openingRemarks);
+            return aiphoneRes;
+        } else {
+            if (withKbResponse.length > 0 && !withKbResponse[0]) {
+                if (!StringUtils.isEmpty(question)) {
+                    addDialogue(ROLE_USER, question);
+                } else {
+                    addDialogue(ROLE_USER, "NO_VOICE");
+
+                    String noVoiceTips = llmAccountInfo.customerNoVoiceTips;
+                    addDialogue(ROLE_ASSISTANT, noVoiceTips);
+
+                    ttsTextCache.add(noVoiceTips);
+                    sendToTts();
+                    closeTts();
+
+                    aiphoneRes.setBody(noVoiceTips);
+                    return aiphoneRes;
+                }
+            }
+
+            try {
+                JSONObject response = sendStreamingRequest(aiphoneRes, llmRoundMessages);
+                if (null != response) {
+                    llmRoundMessages.add(response);
+                } else {
+                    aiphoneRes.setStatus_code(0);
+                }
+            } catch (Throwable throwable) {
+                aiphoneRes.setStatus_code(0);
+                logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
+            }
+            return aiphoneRes;
+        }
+    }
+
+
+    private  JSONObject sendStreamingRequest(LlmAiphoneRes aiphoneRes, List<JSONObject> messages) throws IOException {
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("model", ((LlmAccount)getAccount()).getModelName());
+        requestBody.put("stream", true);
+        requestBody.put("max_tokens", 1024);
+        String systemTips = "";
+        JSONArray messagesArray = new JSONArray();
+        for (JSONObject _msg: messages) {
+            if (ROLE_SYSTEM.equals(_msg.getString("role"))) {
+                systemTips = _msg.getString("content");
+            } else {
+                JSONObject _msgObj = new JSONObject();
+                _msgObj.put("role", _msg.getString("role"));
+                _msgObj.put("content", _msg.getString("content"));
+                messagesArray.add(_msgObj);
+            }
+        }
+        requestBody.put("system", systemTips);
+        requestBody.put("messages", messagesArray);
+
+        RequestBody body = RequestBody.create(
+                MediaType.parse("application/json"),
+                requestBody.toJSONString()
+        );
+
+        Request request = new Request.Builder()
+                .url(getAccount().serverUrl)
+                .post(body)
+                .header("x-api-key", ((LlmAccount)getAccount()).getApiKey())
+                .header("anthropic-version", "2023-06-01")
+                .header("content-type", "application/json")
+                .build();
+
+        boolean recvData = false;
+        long startTime = System.currentTimeMillis();
+
+        try (Response response = CLIENT.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                logger.error("Model api error: http-code={}, msg={}, url={}",
+                        response.code(),
+                        response.message(),
+                        getAccount().serverUrl
+                );
+                if(response.code() == HttpStatus.SC_UNAUTHORIZED) {
+                   CommonUtils.setHangupCauseDetail(
+                           callDetail,
+                           HangupCause.LLM_API_KEY_INCORRECT,
+                           "http-status-code=" + response.code()
+                   );
+                  CommonUtils.hangupCallSession(uuid,  HangupCause.LLM_API_KEY_INCORRECT.getCode());
+                  logger.error("response body:{}", response.body().string());
+                  return null;
+                }else{
+                    CommonUtils.setHangupCauseDetail(
+                            callDetail,
+                            HangupCause.LLM_API_SERVER_ERROR,
+                            "http-status-code=" + response.code()
+                    );
+                }
+
+                if(response.code() == HttpStatus.SC_UNAUTHORIZED || response.code() >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+                    throw new IOException("Unexpected code " + response);
+                }else{
+                    logger.error("response body:{}", response.body().string());
+                    return null;
+                }
+            }
+
+            BufferedSource source = response.body().source();
+            StringBuilder responseBuilder = new StringBuilder();
+
+//                event: message_start
+//                data: {...}
+//
+//                event: content_block_delta
+//                data: {"delta":{"text":"人工"}}
+//
+//                event: content_block_delta
+//                data: {"delta":{"text":"智能"}}
+//
+//                event: message_stop
+//                data: {}
+            while (!source.exhausted()) {
+                String line = source.readUtf8Line();
+                try {
+//                    logger.info("{}-->{}", uuid, line);
+                    if (line != null && line.equals("event: message_stop")) {
+                        break; // 流式响应结束
+                    }
+                    if (line != null && line.startsWith("data: ")) {
+                        String jsonData = line.substring(5).trim(); // 去掉 "data: " 前缀
+                        if (jsonData.equals("{}")) {
+                            break; // 流式响应结束
+                        }
+
+                        JSONObject jsonResponse = JSON.parseObject(jsonData);
+                        JSONObject message = jsonResponse.getJSONObject("delta"); // 注意:流式响应中消息在 "delta" 字段中
+
+                        if (null != message && message.containsKey("text")) {
+                            String speechContent = message.getString("text");
+                            logger.info("{} speechContent: {}", getTraceId(), speechContent);
+
+                            if (!StringUtils.isEmpty(speechContent)) {
+
+                                if (speechContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
+                                    aiphoneRes.setTransferToAgent(1);
+                                    logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
+                                }
+
+                                if (speechContent.contains(LlmToolRequest.HANGUP)) {
+                                    aiphoneRes.setClose_phone(1);
+                                    logger.info("{} `HANGUP` command detected. ", getTraceId());
+                                }
+
+                                if (!StringUtils.isEmpty(speechContent)) {
+                                    speechContent = speechContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
+                                            .replace(LlmToolRequest.HANGUP,"")
+                                            .replace("`","");
+                                    ttsTextCache.add(speechContent);
+                                    ttsTextLength += speechContent.length();
+                                    // 积攒足够的字数之后,才发送给tts,避免播放异常;
+                                    if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                        sendToTts();
+
+                                        if (!recvData) {
+                                            recvData = true;
+                                            long costTime = (System.currentTimeMillis() - startTime);
+                                            logger.info("http request cost time : {} ms.", costTime);
+                                            aiphoneRes.setCostTime(costTime);
+                                        }
+                                    }
+                                }
+                                responseBuilder.append(speechContent);
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.info("{}-->{}", uuid, line);
+                    logger.error("parse llm response error:{}", ExceptionUtils.getStackTrace(e));
+                }
+            }
+
+            String answer = responseBuilder.toString();
+            logger.info("{} recv llm response end flag. answer={}", this.uuid, answer);
+            if(ttsTextLength > 0){
+                sendToTts();
+            }
+            closeTts();
+
+            JSONObject finalResponse = new JSONObject();
+            finalResponse.put("role", "assistant");
+            finalResponse.put("content", answer);
+            aiphoneRes.setBody(answer);
+            return finalResponse;
+        }
+    }
+}

+ 21 - 2
src/main/java/com/telerobot/fs/robot/impl/Coze.java

@@ -9,6 +9,7 @@ import com.coze.openapi.client.connversations.message.model.Message;
 import com.coze.openapi.service.auth.JWTOAuthClient;
 import com.coze.openapi.service.auth.TokenAuth;
 import com.coze.openapi.service.service.CozeAPI;
+import com.telerobot.fs.entity.dao.CustmInfoEntity;
 import com.telerobot.fs.entity.dto.LlmAiphoneRes;
 import com.telerobot.fs.entity.dto.llm.CozeAccount;
 import com.telerobot.fs.robot.AbstractChatRobot;
@@ -20,6 +21,8 @@ import org.apache.commons.lang3.StringUtils;
 
 import javax.websocket.OnError;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public class Coze  extends AbstractChatRobot {
@@ -109,7 +112,12 @@ public class Coze  extends AbstractChatRobot {
         if(firstRound) {
             firstRound = false;
 
-            String openingRemarks = llmAccountInfo.openingRemarks;
+            JSONObject bizJson = new JSONObject();
+            if (null != callDetail && null != callDetail.getOutboundPhoneInfo() && org.apache.commons.lang.StringUtils.isNotBlank( callDetail.getOutboundPhoneInfo().getBizJson())) {
+                bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
+            }
+            String openingRemarks = replaceParams(llmAccountInfo.openingRemarks, bizJson);
+
             addDialogue(ROLE_ASSISTANT, openingRemarks);
 
             ttsTextCache.add(openingRemarks);
@@ -132,6 +140,7 @@ public class Coze  extends AbstractChatRobot {
                 closeTts();
 
                 aiphoneRes.setBody(noVoiceTips);
+                return aiphoneRes;
             }
         }
 
@@ -152,7 +161,16 @@ public class Coze  extends AbstractChatRobot {
     private JSONObject sendStreamingRequest(LlmAiphoneRes aiphoneRes, String question, String cozeToken){
         JSONObject finalResponse = new JSONObject();
         finalResponse.put("role", "assistant");
-
+        Map<String, String> customVariables = new HashMap<>();
+        CustmInfoEntity custmInfoEntity= callDetail.getOutboundPhoneInfo();
+        if (null != custmInfoEntity) {
+            if (org.apache.commons.lang.StringUtils.isNotBlank(custmInfoEntity.getBizJson())) {
+                JSONObject bizJson = JSONObject.parseObject(custmInfoEntity.getBizJson());
+                for (String k: bizJson.keySet()) {
+                    customVariables.put(k, bizJson.getString(k));
+                }
+            }
+        }
         String url = getAccount().serverUrl;
         if(!url.endsWith("/")){
             url = url + "/";
@@ -177,6 +195,7 @@ public class Coze  extends AbstractChatRobot {
                 CreateChatReq.builder()
                         .botID(botID)
                         .userID(uuid)
+                        .customVariables(customVariables)
                         .messages(Collections.singletonList(Message.buildUserQuestionText(question)))
                         .build();
         if(!StringUtils.isEmpty(conversationId)) {

+ 6 - 24
src/main/java/com/telerobot/fs/robot/impl/DeepSeekChat.java

@@ -1,6 +1,5 @@
 package com.telerobot.fs.robot.impl;
 
-import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
@@ -13,7 +12,10 @@ import com.telerobot.fs.entity.pojo.LlmToolRequest;
 import com.telerobot.fs.robot.AbstractChatRobot;
 import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.utils.CommonUtils;
-import okhttp3.*;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
 import okio.BufferedSource;
 import org.apache.commons.lang.StringUtils;
 import org.apache.http.HttpStatus;
@@ -85,8 +87,7 @@ public class DeepSeekChat extends AbstractChatRobot {
                     closeTts();
 
                     aiphoneRes.setBody(noVoiceTips);
-
-                    return aiphoneRes;
+					return aiphoneRes;
                 }
             }
 
@@ -99,7 +100,7 @@ public class DeepSeekChat extends AbstractChatRobot {
                 }
             } catch (Throwable throwable) {
                 aiphoneRes.setStatus_code(0);
-                logger.error("{} talkWithAiAgent error: {}", uuid, CommonUtils.getStackTraceString(throwable.getStackTrace()));
+                logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
             }
             return aiphoneRes;
         }
@@ -132,26 +133,7 @@ public class DeepSeekChat extends AbstractChatRobot {
         long startTime = System.currentTimeMillis();
 
         try (Response response = CLIENT.newCall(request).execute()) {
-
             if (!response.isSuccessful()) {
-
-                ResponseBody responseBody = response.body();
-                String bodyStr = responseBody != null ? responseBody.string() : "空响应体";
-
-                // 2. 组装 完整日志(请求+响应全部信息)
-                String log = "\n======= OKHTTP 完整请求响应 =======\n"
-                        + "请求URL:" + response.request().url() + "\n"
-                        + "请求方法:" + response.request().method() + "\n"
-                        + "请求头:\n" + response.request().headers() + "\n"
-                        + "响应状态码:" + response.code() + "\n"
-                        + "响应头:\n" + response.headers() + "\n"
-                        + "响应体:\n" + bodyStr + "\n"
-                        + "====================================\n";
-
-                // 3. 用 error 级别打印(你要的)
-                logger.error(log);
-
-
                 logger.error("Model api error: http-code={}, msg={}, url={}",
                         response.code(),
                         response.message(),

+ 2 - 1
src/main/java/com/telerobot/fs/robot/impl/Dify.java

@@ -58,6 +58,7 @@ public class Dify extends AbstractChatRobot {
                 closeTts();
 
                 aiphoneRes.setBody(noVoiceTips);
+                return aiphoneRes;
             }
         }
 
@@ -71,7 +72,7 @@ public class Dify extends AbstractChatRobot {
                 }
             } catch (Throwable throwable) {
                 aiphoneRes.setStatus_code(0);
-                logger.error("{} talkWith Dify error: {}", uuid, CommonUtils.getStackTraceString(throwable.getStackTrace()));
+                logger.error("{} talkWith Dify error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
             }
         }
 

+ 2 - 0
src/main/java/com/telerobot/fs/robot/impl/JiutianChat.java

@@ -40,6 +40,7 @@ public class JiutianChat extends AbstractChatRobot {
             closeTts();
 
             aiphoneRes.setBody(openingRemarks);
+            return aiphoneRes;
         }else{
             if(!StringUtils.isEmpty(question)) {
                 addDialogue(ROLE_USER, question);
@@ -54,6 +55,7 @@ public class JiutianChat extends AbstractChatRobot {
                 closeTts();
 
                 aiphoneRes.setBody(noVoiceTips);
+                return aiphoneRes;
             }
         }
 

+ 1 - 0
src/main/java/com/telerobot/fs/robot/impl/JiutianWorkflow.java

@@ -55,6 +55,7 @@ public class JiutianWorkflow extends AbstractChatRobot {
                 closeTts();
 
                 aiphoneRes.setBody(noVoiceTips);
+                return aiphoneRes;
             }
         }
 

+ 115 - 141
src/main/java/com/telerobot/fs/robot/impl/LocalLlmChat.java

@@ -3,11 +3,15 @@ package com.telerobot.fs.robot.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.config.AppContextProvider;
 import com.telerobot.fs.entity.dao.CustmInfoEntity;
+import com.telerobot.fs.entity.dao.LlmKb;
 import com.telerobot.fs.entity.dto.LlmAiphoneRes;
 import com.telerobot.fs.entity.dto.llm.LlmAccount;
+import com.telerobot.fs.entity.po.HangupCause;
 import com.telerobot.fs.entity.pojo.LlmToolRequest;
 import com.telerobot.fs.robot.AbstractChatRobot;
+import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.utils.CommonUtils;
 import okhttp3.MediaType;
 import okhttp3.Request;
@@ -22,27 +26,33 @@ import java.util.List;
 
 public class LocalLlmChat extends AbstractChatRobot {
 
-//    public static final String HANGUP = "{<conversation_end>}";
-    public static final String HANGUP = "hangupCall";
-
     @Override
     public LlmAiphoneRes  talkWithAiAgent(String question, Boolean... withKbResponse) {
-        LlmAiphoneRes aiphoneRes = new  LlmAiphoneRes();
+        LlmAiphoneRes aiphoneRes = new LlmAiphoneRes();
         aiphoneRes.setStatus_code(1);
         aiphoneRes.setClose_phone(0);
         aiphoneRes.setIfcan_interrupt(0);
 
-        // 获取随路数据
-        JSONObject bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
-        logger.info("随路数据:{}", bizJson);
-        logger.info("大模型接口地址:{}", getAccount().serverUrl);
+        JSONObject bizJson = new JSONObject();
+        if (null != callDetail && null != callDetail.getOutboundPhoneInfo() && StringUtils.isNotBlank(callDetail.getOutboundPhoneInfo().getBizJson())) {
+            bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
+        }
 
-        if(firstRound) {
+        if (firstRound) {
             firstRound = false;
-//            String tips = ((LlmAccount)getAccount()).getLlmTips() + "\n" + ((LlmAccount)getAccount()).getFaqContext();
-//            addDialogue(ROLE_SYSTEM, tips);
 
-            String openingRemarks = bizJson.getString("welcomeMessage");
+            String llmTips = ((LlmAccount) getAccount()).getLlmTips();
+            String faqContent = ((LlmAccount) getAccount()).getFaqContext();
+
+            String tips = llmTips;
+            if (StringUtils.isNotBlank(faqContent)
+                    && !"-".equals(faqContent)) {
+                tips = tips + "\r\n\r\n" + faqContent;
+            }
+            addDialogue(ROLE_SYSTEM, tips);
+
+            String openingRemarks = replaceParams(llmAccountInfo.openingRemarks, bizJson);
+
             addDialogue(ROLE_ASSISTANT, openingRemarks);
 
             ttsTextCache.add(openingRemarks);
@@ -50,104 +60,67 @@ public class LocalLlmChat extends AbstractChatRobot {
             closeTts();
 
             aiphoneRes.setBody(openingRemarks);
-        }else{
-            if(!StringUtils.isEmpty(question)) {
-                addDialogue(ROLE_USER, question);
-            }else{
-                addDialogue(ROLE_USER, "NO_VOICE");
-
-                String noVoiceTips = llmAccountInfo.customerNoVoiceTips;
-                addDialogue(ROLE_ASSISTANT, noVoiceTips);
-
-                ttsTextCache.add(noVoiceTips);
-                sendToTts();
-                closeTts();
-
-                aiphoneRes.setBody(noVoiceTips);
+            return aiphoneRes;
+        } else {
+            if (withKbResponse.length > 0 && !withKbResponse[0]) {
+                if (!StringUtils.isEmpty(question)) {
+                    addDialogue(ROLE_USER, question);
+                } else {
+                    addDialogue(ROLE_USER, "NO_VOICE");
+
+                    String noVoiceTips = llmAccountInfo.customerNoVoiceTips;
+                    addDialogue(ROLE_ASSISTANT, noVoiceTips);
+
+                    ttsTextCache.add(noVoiceTips);
+                    sendToTts();
+                    closeTts();
+
+                    aiphoneRes.setBody(noVoiceTips);
+                    return aiphoneRes;
+                }
             }
-        }
 
-        if(!firstRound && !StringUtils.isEmpty(question)) {
             try {
-                JSONObject response = sendStreamingRequest(aiphoneRes, bizJson, question);
-                if(null != response) {
+                JSONObject response = sendStreamingRequest(aiphoneRes, llmRoundMessages, bizJson, question);
+                if (null != response) {
                     llmRoundMessages.add(response);
-                }else{
+                } else {
                     aiphoneRes.setStatus_code(0);
                 }
             } catch (Throwable throwable) {
                 aiphoneRes.setStatus_code(0);
-                logger.error("{} talkWithAiAgent error: {}", uuid, CommonUtils.getStackTraceString(throwable.getStackTrace()));
+                logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
             }
+            return aiphoneRes;
         }
-
-        return aiphoneRes;
     }
 
 
-
-    private  JSONObject sendStreamingRequest(LlmAiphoneRes aiphoneRes, JSONObject bizJson, String question) throws IOException {
+    private  JSONObject sendStreamingRequest(LlmAiphoneRes aiphoneRes, List<JSONObject> messages, JSONObject bizJson, String question) throws IOException {
         JSONObject requestBody = new JSONObject();
-        // 默认为 0 -2.0 到 2.0 之间的数字。正值根据文本目前的存在频率惩罚新标记,降低模型重复相同行的可能性。
-        requestBody.put("frequencyPenalty", 0.2);
-        // 修改指定标记出现在补全中的可能性。接受一个 JSON 对象,该对象将标记(由标记器指定的标记 ID)映射到相关的偏差值(-100 到 100)。
-        requestBody.put("logitBias", new JSONObject());
-        // 默认为 inf 在聊天补全中生成的最大标记数。
-        requestBody.put("maxTokens", 1024);
-        // 至今为止对话所包含的消息列表。
-        JSONObject message = new JSONObject();
-        message.put("content", question);
-        message.put("role", "user");
-        JSONArray paramsMessages = new JSONArray();
-        paramsMessages.add(message);
-        requestBody.put("messages", paramsMessages);
-        // 要使用的模型的 ID。有关哪些模型可与聊天 API 一起使用的详细信息,请参阅模型端点兼容性表。
-        // gpt-3.5-turbo
+        // 模型名称
         requestBody.put("model", ((LlmAccount)getAccount()).getModelName());
-        // 默认为 1 为每个输入消息生成多少个聊天补全选择。
-        requestBody.put("n", 1);
-        // -2.0 和 2.0 之间的数字。正值会根据到目前为止是否出现在文本中来惩罚新标记,从而增加模型谈论新主题的可能性。
-        requestBody.put("presencePenalty", 1.0);
-        // 指定模型必须输出的格式的对象。将 { "type": "json_object" } 启用 JSON 模式。
-        requestBody.put("responseFormat", "text");
-//        // 此功能处于测试阶段。如果指定,我们的系统将尽最大努力确定性地进行采样。
-//        requestBody.put("seen", 1);
-//        // 默认为 null 最多 4 个序列,API 将停止进一步生成标记。
-//        requestBody.put("stop", "1");
-        // 默认为 false 如果设置,则像在 ChatGPT 中一样会发送部分消息增量。
+        // 流式响应
         requestBody.put("stream", true);
-        // 使用什么采样温度,介于 0 和 2 之间。
-        requestBody.put("temperature", 1.0);
-        // 控制模型调用哪个函数(如果有的话)。
-        requestBody.put("toolChoice", "");
-        // 模型可以调用的一组工具列表。目前,只支持作为工具的函数。
-        requestBody.put("tools", new JSONArray());
-        // 一种替代温度采样的方法,称为核采样。
-        requestBody.put("topP", 0.95);
-        // 代表您的最终用户的唯一标识符,可以帮助 OpenAI 监控和检测滥用行为。
-        requestBody.put("user", "easycallcenter365");
-        // 通用变量
-        requestBody.put("variables", new JSONArray());
-        requestBody.put("outboundType", 1);
+        JSONArray messagesArray = new JSONArray();
+        messagesArray.addAll(messages);
+        // 对话上下文(包括客户最近说的一句话)
+        requestBody.put("messages", messagesArray);
+        // 随路数据(即客户信息)
+        requestBody.put("custInfo", bizJson);
+        // 客户刚刚说的话
         requestBody.put("question", question);
-        logger.info("请求参数:{}", requestBody.toJSONString());
+        // 本通电话的唯一标识
+        requestBody.put("uuid", uuid);
 
         RequestBody body = RequestBody.create(
                 MediaType.parse("application/json"),
                 requestBody.toJSONString()
         );
-
-        JSONObject bizParam = new JSONObject();
-        bizParam.put("question_chain_id", bizJson.getString("questionChainId"));
         Request request = new Request.Builder()
                 .url(getAccount().serverUrl)
                 .post(body)
-                .addHeader("Biz-Param", JSONObject.toJSONString(bizParam))
-                .addHeader("User-Agent", "Apifox/1.0.0 (https://apifox.com)")
-                .addHeader("Content-Type", "application/json")
-                .addHeader("Accept", "*/*")
-                // .addHeader("Host", "xgentapidev.health.techxgent.com")
-                .addHeader("Connection", "keep-alive")
+                .addHeader("Authorization", "Bearer " + ((LlmAccount)getAccount()).getApiKey())
                 .build();
 
         boolean recvData = false;
@@ -160,6 +133,22 @@ public class LocalLlmChat extends AbstractChatRobot {
                         response.message(),
                         getAccount().serverUrl
                 );
+                if(response.code() == HttpStatus.SC_UNAUTHORIZED) {
+                    CommonUtils.setHangupCauseDetail(
+                            callDetail,
+                            HangupCause.LLM_API_KEY_INCORRECT,
+                            "http-status-code=" + response.code()
+                    );
+                    CommonUtils.hangupCallSession(uuid,  HangupCause.LLM_API_KEY_INCORRECT.getCode());
+                    return null;
+                }else{
+                    CommonUtils.setHangupCauseDetail(
+                            callDetail,
+                            HangupCause.LLM_API_SERVER_ERROR,
+                            "http-status-code=" + response.code()
+                    );
+                }
+
                 if(response.code() == HttpStatus.SC_UNAUTHORIZED || response.code() >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
                     throw new IOException("Unexpected code " + response);
                 }else{
@@ -170,70 +159,57 @@ public class LocalLlmChat extends AbstractChatRobot {
             BufferedSource source = response.body().source();
             StringBuilder responseBuilder = new StringBuilder();
 
-            Integer completionTokens = 0; // 模型生成回复转换为 Token 后的长度。
-            Integer promptTokens = 0; // 用户的输入转换成 Token 后的长度。
-
             while (!source.exhausted()) {
-                // data:{"id":"chatcmpl-cd5eeb3e7d5f44ea9526f322a40b6","object":"chat.completion.chunk","created":"1763626633","model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{}}]}
-                // data:{"id":"chatcmpl-ed70d3e952dc40a7a8272249aed2e","object":"chat.completion.chunk","created":"1763604699","model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"content":"了解了,那我们就不打扰了,感谢您的接听,祝您生活愉快,再见。 "}}]}
-                // event:done
-                // data:hangupCall
                 String line = source.readUtf8Line();
-                logger.info("{}接收到的消息:{}", uuid, line);
-                if (line != null && line.startsWith("data:")) {
+                // data: {"choices":[{"delta":{"content":"xxxxxxx"}}]}
+                // data: [DONE]
+                if (line != null && line.startsWith("data: ")) {
                     String jsonData = line.substring(5).trim(); // 去掉 "data: " 前缀
-                    if (jsonData.equals("[DONE]") || !jsonData.startsWith("{")) {
-                        continue; // 流式响应结束
-                    }
-                    String speechContent = "";
-                    if (jsonData.equals(HANGUP)) {
-                        speechContent = speechContent + jsonData;
-                        logger.info("{}接收到了挂机指令!", uuid);
-                    } else {
-                        JSONObject jsonResponse = JSON.parseObject(jsonData);
-                        JSONObject choice = jsonResponse.getJSONArray("choices")
-                                .getJSONObject(0);
-//                        if ("stop".equals(choice.getString("finish_reason"))) {
-//                            break; // 流式响应结束
-//                        }
-                        JSONObject delta = choice.getJSONObject("delta"); // 注意:流式响应中消息在 "delta" 字段中
-
-                        if (delta.containsKey("content")) {
-                            speechContent = delta.getString("content");
-                        }
-                    }
-                    logger.info("{} speechContent: {}", getTraceId(), speechContent);
-                    if (!recvData) {
-                        recvData = true;
-                        long costTime = (System.currentTimeMillis() - startTime);
-                        logger.info("http request cost time : {} ms.", costTime);
-                        aiphoneRes.setCostTime(costTime);
+                    if (jsonData.equals("[DONE]")) {
+                        break; // 流式响应结束
                     }
 
-                    if (!StringUtils.isEmpty(speechContent)) {
+                    JSONObject jsonResponse = JSON.parseObject(jsonData);
+                    JSONObject message = jsonResponse.getJSONArray("choices")
+                            .getJSONObject(0)
+                            .getJSONObject("delta"); // 注意:流式响应中消息在 "delta" 字段中
 
-                        if (speechContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
-                            aiphoneRes.setTransferToAgent(1);
-                            logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
-                        }
-
-                        if (speechContent.contains(HANGUP)) {
-                            aiphoneRes.setClose_phone(1);
-                            logger.info("{} `HANGUP` command detected. ", getTraceId());
-                        }
+                    if (message.containsKey("content")) {
+                        String speechContent = message.getString("content");
+                        logger.info("{} speechContent: {}", getTraceId(), speechContent);
 
                         if (!StringUtils.isEmpty(speechContent)) {
-                            speechContent = speechContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
-                                    .replace(HANGUP,"")
-                                    .replace("`","");
-                            ttsTextCache.add(speechContent);
-                            ttsTextLength += speechContent.length();
-                            // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                            if (!StringUtils.isEmpty(speechContent) && ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
-                                sendToTts();
+
+                            if (speechContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
+                                aiphoneRes.setTransferToAgent(1);
+                                logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
+                            }
+
+                            if (speechContent.contains(LlmToolRequest.HANGUP)) {
+                                aiphoneRes.setClose_phone(1);
+                                logger.info("{} `HANGUP` command detected. ", getTraceId());
+                            }
+
+                            if (!StringUtils.isEmpty(speechContent)) {
+                                speechContent = speechContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
+                                        .replace(LlmToolRequest.HANGUP,"")
+                                        .replace("`","");
+                                ttsTextCache.add(speechContent);
+                                ttsTextLength += speechContent.length();
+                                // 积攒足够的字数之后,才发送给tts,避免播放异常;
+                                if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                    sendToTts();
+
+                                    if (!recvData) {
+                                        recvData = true;
+                                        long costTime = (System.currentTimeMillis() - startTime);
+                                        logger.info("http request cost time : {} ms.", costTime);
+                                        aiphoneRes.setCostTime(costTime);
+                                    }
+                                }
                             }
+                            responseBuilder.append(speechContent);
                         }
-                        responseBuilder.append(speechContent);
                     }
                 }
             }
@@ -248,8 +224,6 @@ public class LocalLlmChat extends AbstractChatRobot {
             JSONObject finalResponse = new JSONObject();
             finalResponse.put("role", "assistant");
             finalResponse.put("content", answer);
-            finalResponse.put("completionTokens", completionTokens); // 模型生成回复转换为 Token 后的长度。
-            finalResponse.put("promptTokens", promptTokens); // 用户的输入转换成 Token 后的长度。
             aiphoneRes.setBody(answer);
             return finalResponse;
         }

+ 19 - 3
src/main/java/com/telerobot/fs/robot/impl/LocalNlpChat.java

@@ -47,7 +47,7 @@ public class LocalNlpChat extends AbstractChatRobot {
             }
         } catch (Throwable throwable) {
             aiphoneRes.setStatus_code(0);
-            logger.error("{} talkWithAiAgent error: {}", uuid, CommonUtils.getStackTraceString(throwable.getStackTrace()));
+            logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
         }
 
         return aiphoneRes;
@@ -99,8 +99,24 @@ public class LocalNlpChat extends AbstractChatRobot {
                 chatContent = result.getJSONObject("result").getString("chatContent");
                 callCode = result.getJSONObject("result").getString("callCode");
                 String ttsContent = result.getJSONObject("result").getString("ttsContent");
-                ttsTextCache.add(ttsContent);
-                ttsTextLength += ttsContent.length();
+
+                if (chatContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
+                    aiphoneRes.setTransferToAgent(1);
+                    logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
+                }
+
+                if (chatContent.contains(LlmToolRequest.HANGUP)) {
+                    aiphoneRes.setClose_phone(1);
+                    logger.info("{} `HANGUP` command detected. ", getTraceId());
+                }
+
+                if (!StringUtils.isEmpty(chatContent)) {
+                    chatContent = chatContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
+                            .replace(LlmToolRequest.HANGUP,"")
+                            .replace("`","");
+                    ttsTextCache.add(ttsContent);
+                    ttsTextLength += ttsContent.length();
+                }
             }
 
             logger.info("{} recv llm response end flag. answer={}", this.uuid, chatContent);

+ 191 - 0
src/main/java/com/telerobot/fs/robot/impl/LocalWavFile.java

@@ -0,0 +1,191 @@
+package com.telerobot.fs.robot.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.entity.dto.LlmAiphoneRes;
+import com.telerobot.fs.entity.dto.llm.CozeAccount;
+import com.telerobot.fs.entity.dto.llm.LlmAccount;
+import com.telerobot.fs.entity.pojo.LlmToolRequest;
+import com.telerobot.fs.robot.AbstractChatRobot;
+import com.telerobot.fs.utils.CommonUtils;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.util.List;
+
+public class LocalWavFile extends AbstractChatRobot {
+
+    @Override
+    public LlmAiphoneRes  talkWithAiAgent(String question, Boolean... withKbResponse) {
+        LlmAiphoneRes aiphoneRes = new  LlmAiphoneRes();
+        aiphoneRes.setStatus_code(1);
+        aiphoneRes.setClose_phone(0);
+        aiphoneRes.setIfcan_interrupt(0);
+
+        // 获取随路数据
+        JSONObject bizJson = new JSONObject();
+        if (null != callDetail.getOutboundPhoneInfo()) {
+            if (null != callDetail.getOutboundPhoneInfo().getBizJson()) {
+                bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
+            }
+        }
+        logger.info("随路数据:{}", bizJson);
+        logger.info("模型接口地址:{}", getAccount().serverUrl);
+
+
+        if (firstRound) {
+            firstRound = false;
+
+            String llmTips = ((LlmAccount) getAccount()).getLlmTips();
+            String faqContent = ((LlmAccount) getAccount()).getFaqContext();
+
+            String tips = llmTips;
+            if (StringUtils.isNotBlank(faqContent)
+                    && !"-".equals(faqContent)) {
+                tips = tips + "\r\n\r\n" + faqContent;
+            }
+            addDialogue(ROLE_SYSTEM, tips);
+
+            String openingRemarks = replaceParams(llmAccountInfo.openingRemarks, bizJson);
+
+            addDialogue(ROLE_ASSISTANT, openingRemarks);
+
+            ttsTextCache.add(openingRemarks);
+
+            if (StringUtils.isNotBlank(llmAccountInfo.openingRemarksWav)) {
+                aiphoneRes.setTtsFilePathList(llmAccountInfo.openingRemarksWav);
+            }
+            if (StringUtils.isNotBlank(llmAccountInfo.transferToAgentTipsWav)) {
+                llmAccountInfo.transferToAgentTips = llmAccountInfo.transferToAgentTipsWav;
+            }
+            if (StringUtils.isNotBlank(llmAccountInfo.hangupTipsWav)) {
+                llmAccountInfo.hangupTips = llmAccountInfo.hangupTipsWav;
+            }
+            logger.info("{},openingRemarksWav:{}", this.uuid, aiphoneRes.getTtsFilePathList());
+            aiphoneRes.setBody(openingRemarks);
+
+            return aiphoneRes;
+        } else {
+
+            if (!StringUtils.isEmpty(question)) {
+                addDialogue(ROLE_USER, question);
+            } else {
+                addDialogue(ROLE_USER, "NO_VOICE");
+                String noVoiceTips = llmAccountInfo.customerNoVoiceTips;
+                addDialogue(ROLE_ASSISTANT, noVoiceTips);
+                ttsTextCache.add(noVoiceTips);
+                if (StringUtils.isNotBlank(llmAccountInfo.customerNoVoiceTipsWav)) {
+                    aiphoneRes.setTtsFilePathList(llmAccountInfo.customerNoVoiceTipsWav);
+                }
+                logger.info("{},customerNoVoiceTipsWav:{}", this.uuid, aiphoneRes.getTtsFilePathList());
+                aiphoneRes.setBody(noVoiceTips);
+
+                return aiphoneRes;
+            }
+
+            try {
+                JSONObject response = sendNoneStreamingRequest(aiphoneRes, llmRoundMessages, bizJson, question);
+                if (null != response) {
+                    llmRoundMessages.add(response);
+                } else {
+                    aiphoneRes.setStatus_code(0);
+                }
+            } catch (Throwable throwable) {
+                aiphoneRes.setStatus_code(0);
+                logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
+            }
+            return aiphoneRes;
+        }
+    }
+
+
+
+    private  JSONObject sendNoneStreamingRequest(LlmAiphoneRes aiphoneRes, List<JSONObject> messages, JSONObject bizJson, String question) throws IOException {
+        JSONObject requestBody = new JSONObject();
+        // 模型名称
+        requestBody.put("model", ((LlmAccount)getAccount()).getModelName());
+        // 流式响应
+        requestBody.put("stream", false);
+        JSONArray messagesArray = new JSONArray();
+        messagesArray.addAll(messages);
+        // 对话上下文(包括客户最近说的一句话)
+        requestBody.put("messages", messagesArray);
+        // 随路数据(即客户信息)
+        requestBody.put("custInfo", bizJson);
+        // 客户刚刚说的话
+        requestBody.put("question", question);
+        // 本通电话的唯一标识
+        requestBody.put("uuid", uuid);
+        logger.info("请求参数:{}", requestBody.toJSONString());
+
+        RequestBody body = RequestBody.create(
+                MediaType.parse("application/json"),
+                requestBody.toJSONString()
+        );
+
+        Request request = new Request.Builder()
+                .url(getAccount().serverUrl)
+                .post(body)
+                .addHeader("Content-Type", "application/json")
+                .addHeader("Accept", "*/*")
+                .addHeader("Connection", "keep-alive")
+                .addHeader("Authorization", "Bearer " + ((LlmAccount)getAccount()).getApiKey())
+                .build();
+
+        try (Response response = CLIENT.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                logger.error("Model api error: http-code={}, msg={}, url={}",
+                        response.code(),
+                        response.message(),
+                        getAccount().serverUrl
+                );
+            }
+
+            String chatContent = "";
+            // {"code":200, "data":{"choices":[{"delta":{"content":"xxxxxxx", "wavFilePath":"/home/Records/251224101457010001/20260113161253001914.wav"}}]}}
+            JSONObject result = JSONObject.parseObject(response.body().string());
+            logger.info("{} recv local response:{}", uuid, result);
+            String wavFiles = "";
+            if (result.getInteger("code") == 200 && null != result.getJSONObject("data")) {
+                JSONArray choices = result.getJSONObject("data").getJSONArray("choices");
+                if (null != choices && choices.size() > 0) {
+                    JSONObject delta = choices.getJSONObject(0).getJSONObject("delta");
+                    chatContent = delta.getString("content");
+                    wavFiles = delta.getString("wavFilePath");
+
+                    if (chatContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
+                        aiphoneRes.setTransferToAgent(1);
+                        logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
+                    }
+
+                    if (chatContent.contains(LlmToolRequest.HANGUP)) {
+                        aiphoneRes.setClose_phone(1);
+                        logger.info("{} `HANGUP` command detected. ", getTraceId());
+                    }
+
+                    if (!StringUtils.isEmpty(chatContent)) {
+                        chatContent = chatContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
+                                .replace(LlmToolRequest.HANGUP,"")
+                                .replace("`","");
+                        ttsTextCache.add(chatContent);
+                        ttsTextLength += chatContent.length();
+                    }
+                }
+            }
+
+            logger.info("{} recv llm response end flag. answer={}, wavFiles={}", this.uuid, chatContent, wavFiles);
+            aiphoneRes.setTtsFilePathList(wavFiles);
+
+            JSONObject finalResponse = new JSONObject();
+            finalResponse.put("role", "assistant");
+            finalResponse.put("content", chatContent);
+            aiphoneRes.setBody(chatContent);
+            return finalResponse;
+        }
+    }
+}

+ 72 - 0
src/main/java/com/telerobot/fs/robot/impl/LocalWebApiTest.java

@@ -0,0 +1,72 @@
+package com.telerobot.fs.robot.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.entity.dto.LlmAiphoneRes;
+import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
+import com.telerobot.fs.entity.dto.llm.CozeAccount;
+import com.telerobot.fs.entity.dto.llm.LlmAccount;
+import com.telerobot.fs.entity.pojo.LlmToolRequest;
+import com.telerobot.fs.robot.AbstractChatRobot;
+import com.telerobot.fs.utils.CommonUtils;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class LocalWebApiTest extends AbstractChatRobot {
+
+    static List<LlmAiphoneRes> testDataList = new ArrayList<>(10);
+    static {
+
+        String[] data = new String[]{
+                "您好,请问您是测试1本人是吗?| /home/Records/23/round-1.wav; ",
+                "这里是中信银行委托方,这个电话号码是测试1在中信银行办理业务时登记的号码,您是测试1吗? | /home/Records/23/round-2-1.wav;/home/Records/23/round-2-2.wav;/home/Records/23/round-2-3.wav",
+                "这里是中信银行委托方,这边主要是通知您一下,您在中信银行卡尾号6388的信用卡已错过到期环款日,当期账单账面欠款总额9097元全国统一核账时间在今天下午5点需要您在此之前处理进来,可以吧?| /home/Records/23/round-3-1.wav;/home/Records/23/round-3-2.wav;",
+                "您的账单现在已经过环款日了,能和我们说一下您是为什么还没处理欠款吗? | /home/Records/23/round-4.wav",
+                "好的,请您在今天下午5点之前至少处理您的最低还款额0元时间和资金都没问题,对吗?|/home/Records/23/round-5.wav",
+                "银行稍后安排工作人员查账,建议您尽快处理欠款,不打扰您了,再见。|/home/Records/23/round-6.wav"
+        };
+        for (String item : data) {
+            String[] array = item.split("\\|");
+            LlmAiphoneRes aiphoneRes = new LlmAiphoneRes();
+            aiphoneRes.setBody(array[0].trim());
+            aiphoneRes.setTtsFilePathList(array[1].trim());
+            aiphoneRes.setStatus_code(1);
+            aiphoneRes.setCostTime(1L);
+            testDataList.add(aiphoneRes);
+        }
+    }
+
+    private AtomicLong round = new AtomicLong(0);
+
+    public  void makeMockData() {
+        AccountBaseEntity llmAccount = getAccount();
+        llmAccount.voiceSource = "";
+        llmAccount.customerNoVoiceTips  = "/home/Records/23/no-voice.wav";
+        llmAccount.transferToAgentTips  = "/home/Records/23/transfer-tips.wav";
+        llmAccount.hangupTips = "/home/Records/23/hangup-tips.wav";
+    }
+
+    @Override
+    public LlmAiphoneRes  talkWithAiAgent(String question, Boolean... withKbResponse) {
+        int index = (int)round.getAndIncrement();
+        if(index == testDataList.size() - 1){
+            LlmAiphoneRes res = testDataList.get(index);
+            res.setTransferToAgent(1);
+            return res;
+        }else{
+            return testDataList.get(index);
+        }
+    }
+
+}

+ 2 - 1
src/main/java/com/telerobot/fs/robot/impl/MaxKB.java

@@ -54,6 +54,7 @@ public class MaxKB extends AbstractChatRobot {
                 closeTts();
 
                 aiphoneRes.setBody(noVoiceTips);
+                return aiphoneRes;
             }
         }
 
@@ -67,7 +68,7 @@ public class MaxKB extends AbstractChatRobot {
                 }
             } catch (Throwable throwable) {
                 aiphoneRes.setStatus_code(0);
-                logger.error("{} talkWith MaxKB error: {}", uuid, CommonUtils.getStackTraceString(throwable.getStackTrace()));
+                logger.error("{} talkWith MaxKB error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
             }
         }
 

+ 256 - 0
src/main/java/com/telerobot/fs/robot/impl/XingWenChat.java

@@ -0,0 +1,256 @@
+package com.telerobot.fs.robot.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.entity.dto.LlmAiphoneRes;
+import com.telerobot.fs.entity.dto.llm.LlmAccount;
+import com.telerobot.fs.entity.pojo.LlmToolRequest;
+import com.telerobot.fs.robot.AbstractChatRobot;
+import com.telerobot.fs.utils.CommonUtils;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okio.BufferedSource;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+
+public class XingWenChat extends AbstractChatRobot {
+
+//    public static final String HANGUP = "{<conversation_end>}";
+    public static final String HANGUP = "hangupCall";
+
+    @Override
+    public LlmAiphoneRes  talkWithAiAgent(String question, Boolean... withKbResponse) {
+        LlmAiphoneRes aiphoneRes = new  LlmAiphoneRes();
+        aiphoneRes.setStatus_code(1);
+        aiphoneRes.setClose_phone(0);
+        aiphoneRes.setIfcan_interrupt(0);
+
+        // 获取随路数据
+        JSONObject bizJson = JSONObject.parseObject(callDetail.getOutboundPhoneInfo().getBizJson());
+        logger.info("随路数据:{}", bizJson);
+        logger.info("大模型接口地址:{}", getAccount().serverUrl);
+
+        if(firstRound) {
+            firstRound = false;
+//            String tips = ((LlmAccount)getAccount()).getLlmTips() + "\n" + ((LlmAccount)getAccount()).getFaqContext();
+//            addDialogue(ROLE_SYSTEM, tips);
+
+            String openingRemarks = bizJson.getString("welcomeMessage");
+            addDialogue(ROLE_ASSISTANT, openingRemarks);
+
+            ttsTextCache.add(openingRemarks);
+            sendToTts();
+            closeTts();
+
+            aiphoneRes.setBody(openingRemarks);
+        }else{
+            if(!StringUtils.isEmpty(question)) {
+                addDialogue(ROLE_USER, question);
+            }else{
+                addDialogue(ROLE_USER, "NO_VOICE");
+
+                String noVoiceTips = llmAccountInfo.customerNoVoiceTips;
+                addDialogue(ROLE_ASSISTANT, noVoiceTips);
+
+                ttsTextCache.add(noVoiceTips);
+                sendToTts();
+                closeTts();
+
+                aiphoneRes.setBody(noVoiceTips);
+                return aiphoneRes;
+            }
+        }
+
+        if(!firstRound && !StringUtils.isEmpty(question)) {
+            try {
+                JSONObject response = sendStreamingRequest(aiphoneRes, bizJson, question);
+                if(null != response) {
+                    llmRoundMessages.add(response);
+                }else{
+                    aiphoneRes.setStatus_code(0);
+                }
+            } catch (Throwable throwable) {
+                aiphoneRes.setStatus_code(0);
+                logger.error("{} talkWithAiAgent error: {} \n {}", uuid, throwable.toString(), CommonUtils.getStackTraceString(throwable.getStackTrace()));
+            }
+        }
+
+        return aiphoneRes;
+    }
+
+
+
+    private  JSONObject sendStreamingRequest(LlmAiphoneRes aiphoneRes, JSONObject bizJson, String question) throws IOException {
+        JSONObject requestBody = new JSONObject();
+        // 默认为 0 -2.0 到 2.0 之间的数字。正值根据文本目前的存在频率惩罚新标记,降低模型重复相同行的可能性。
+        requestBody.put("frequencyPenalty", 0.2);
+        // 修改指定标记出现在补全中的可能性。接受一个 JSON 对象,该对象将标记(由标记器指定的标记 ID)映射到相关的偏差值(-100 到 100)。
+        requestBody.put("logitBias", new JSONObject());
+        // 默认为 inf 在聊天补全中生成的最大标记数。
+        requestBody.put("maxTokens", 1024);
+        // 至今为止对话所包含的消息列表。
+        JSONObject message = new JSONObject();
+        message.put("content", question);
+        message.put("role", "user");
+        JSONArray paramsMessages = new JSONArray();
+        paramsMessages.add(message);
+        requestBody.put("messages", paramsMessages);
+        // 要使用的模型的 ID。有关哪些模型可与聊天 API 一起使用的详细信息,请参阅模型端点兼容性表。
+        // gpt-3.5-turbo
+        requestBody.put("model", ((LlmAccount)getAccount()).getModelName());
+        // 默认为 1 为每个输入消息生成多少个聊天补全选择。
+        requestBody.put("n", 1);
+        // -2.0 和 2.0 之间的数字。正值会根据到目前为止是否出现在文本中来惩罚新标记,从而增加模型谈论新主题的可能性。
+        requestBody.put("presencePenalty", 1.0);
+        // 指定模型必须输出的格式的对象。将 { "type": "json_object" } 启用 JSON 模式。
+        requestBody.put("responseFormat", "text");
+//        // 此功能处于测试阶段。如果指定,我们的系统将尽最大努力确定性地进行采样。
+//        requestBody.put("seen", 1);
+//        // 默认为 null 最多 4 个序列,API 将停止进一步生成标记。
+//        requestBody.put("stop", "1");
+        // 默认为 false 如果设置,则像在 ChatGPT 中一样会发送部分消息增量。
+        requestBody.put("stream", true);
+        // 使用什么采样温度,介于 0 和 2 之间。
+        requestBody.put("temperature", 1.0);
+        // 控制模型调用哪个函数(如果有的话)。
+        requestBody.put("toolChoice", "");
+        // 模型可以调用的一组工具列表。目前,只支持作为工具的函数。
+        requestBody.put("tools", new JSONArray());
+        // 一种替代温度采样的方法,称为核采样。
+        requestBody.put("topP", 0.95);
+        // 代表您的最终用户的唯一标识符,可以帮助 OpenAI 监控和检测滥用行为。
+        requestBody.put("user", "easycallcenter365");
+        // 通用变量
+        requestBody.put("variables", new JSONArray());
+        requestBody.put("outboundType", 1);
+        requestBody.put("question", question);
+        logger.info("请求参数:{}", requestBody.toJSONString());
+
+        RequestBody body = RequestBody.create(
+                MediaType.parse("application/json"),
+                requestBody.toJSONString()
+        );
+
+        JSONObject bizParam = new JSONObject();
+        bizParam.put("question_chain_id", bizJson.getString("questionChainId"));
+        Request request = new Request.Builder()
+                .url(getAccount().serverUrl)
+                .post(body)
+                .addHeader("Biz-Param", JSONObject.toJSONString(bizParam))
+                .addHeader("User-Agent", "Apifox/1.0.0 (https://apifox.com)")
+                .addHeader("Content-Type", "application/json")
+                .addHeader("Accept", "*/*")
+                // .addHeader("Host", "xgentapidev.health.techxgent.com")
+                .addHeader("Connection", "keep-alive")
+                .build();
+
+        boolean recvData = false;
+        long startTime = System.currentTimeMillis();
+
+        try (Response response = CLIENT.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                logger.error("Model api error: http-code={}, msg={}, url={}",
+                        response.code(),
+                        response.message(),
+                        getAccount().serverUrl
+                );
+                if(response.code() == HttpStatus.SC_UNAUTHORIZED || response.code() >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+                    throw new IOException("Unexpected code " + response);
+                }else{
+                    return null;
+                }
+            }
+
+            BufferedSource source = response.body().source();
+            StringBuilder responseBuilder = new StringBuilder();
+
+            Integer completionTokens = 0; // 模型生成回复转换为 Token 后的长度。
+            Integer promptTokens = 0; // 用户的输入转换成 Token 后的长度。
+
+            while (!source.exhausted()) {
+                // data:{"id":"chatcmpl-cd5eeb3e7d5f44ea9526f322a40b6","object":"chat.completion.chunk","created":"1763626633","model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{}}]}
+                // data:{"id":"chatcmpl-ed70d3e952dc40a7a8272249aed2e","object":"chat.completion.chunk","created":"1763604699","model":"gpt-3.5-turbo","choices":[{"index":0,"delta":{"content":"了解了,那我们就不打扰了,感谢您的接听,祝您生活愉快,再见。 "}}]}
+                // event:done
+                // data:hangupCall
+                String line = source.readUtf8Line();
+                logger.info("{}接收到的消息:{}", uuid, line);
+                if (line != null && line.startsWith("data:")) {
+                    String jsonData = line.substring(5).trim(); // 去掉 "data:" 前缀
+                    if (jsonData.equals("[DONE]") || !jsonData.startsWith("{")) {
+                        continue; // 流式响应结束
+                    }
+                    String speechContent = "";
+                    if (jsonData.equals(HANGUP)) {
+                        speechContent = speechContent + jsonData;
+                        logger.info("{}接收到了挂机指令!", uuid);
+                    } else {
+                        JSONObject jsonResponse = JSON.parseObject(jsonData);
+                        JSONObject choice = jsonResponse.getJSONArray("choices")
+                                .getJSONObject(0);
+//                        if ("stop".equals(choice.getString("finish_reason"))) {
+//                            break; // 流式响应结束
+//                        }
+                        JSONObject delta = choice.getJSONObject("delta"); // 注意:流式响应中消息在 "delta" 字段中
+
+                        if (delta.containsKey("content")) {
+                            speechContent = delta.getString("content");
+                        }
+                    }
+                    logger.info("{} speechContent: {}", getTraceId(), speechContent);
+                    if (!recvData) {
+                        recvData = true;
+                        long costTime = (System.currentTimeMillis() - startTime);
+                        logger.info("http request cost time : {} ms.", costTime);
+                        aiphoneRes.setCostTime(costTime);
+                    }
+
+                    if (!StringUtils.isEmpty(speechContent)) {
+
+                        if (speechContent.contains(LlmToolRequest.TRANSFER_TO_AGENT)) {
+                            aiphoneRes.setTransferToAgent(1);
+                            logger.info("{} `TRANSFER_TO_AGENT` command detected. ", getTraceId());
+                        }
+
+                        if (speechContent.contains(HANGUP)) {
+                            aiphoneRes.setClose_phone(1);
+                            logger.info("{} `HANGUP` command detected. ", getTraceId());
+                        }
+
+                        if (!StringUtils.isEmpty(speechContent)) {
+                            speechContent = speechContent.replace(LlmToolRequest.TRANSFER_TO_AGENT,"")
+                                    .replace(HANGUP,"")
+                                    .replace("`","");
+                            ttsTextCache.add(speechContent);
+                            ttsTextLength += speechContent.length();
+                            // 积攒足够的字数之后,才发送给tts,避免播放异常;
+                            if (!StringUtils.isEmpty(speechContent) && ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
+                                sendToTts();
+                            }
+                        }
+                        responseBuilder.append(speechContent);
+                    }
+                }
+            }
+
+            String answer = responseBuilder.toString();
+            logger.info("{} recv llm response end flag. answer={}", this.uuid, answer);
+            if(ttsTextLength > 0){
+                sendToTts();
+            }
+            closeTts();
+
+            JSONObject finalResponse = new JSONObject();
+            finalResponse.put("role", "assistant");
+            finalResponse.put("content", answer);
+            finalResponse.put("completionTokens", completionTokens); // 模型生成回复转换为 Token 后的长度。
+            finalResponse.put("promptTokens", promptTokens); // 用户的输入转换成 Token 后的长度。
+            aiphoneRes.setBody(answer);
+            return finalResponse;
+        }
+    }
+}

+ 1 - 0
src/main/java/com/telerobot/fs/service/AsrResultListener.java

@@ -163,6 +163,7 @@ public class AsrResultListener implements ApplicationListener<ApplicationReadyEv
         String uuid = callMonitorInfo.getUuid();
         String asrProvider = SystemConfig.getValue("fs_call_asr_engine", "funasr");
         if(asrProvider.equalsIgnoreCase(AsrProvider.ALIYUN)) {
+            logger.info("{} Attempt to initiate the call speech transcription process.", callMonitorInfo.getUuidAgent());
             AlibabaTokenEntity token = AliyunTTSWebApi.getToken();
             if (token != null) {
                 logger.info("{} set FreeSWITCH channel variables, aliyun_tts_token={}, aliyun_tts_app_key={} ",

+ 1 - 0
src/main/java/com/telerobot/fs/service/InboundDetailService.java

@@ -62,6 +62,7 @@ public class InboundDetailService {
             inboundConfig.setId((Integer)map.get("id"));
             inboundConfig.setLlmAccountId((Integer)map.get("llm_account_id"));
             inboundConfig.setVoiceCode(map.get("voice_code").toString());
+            inboundConfig.setTtsModels(map.get("tts_models").toString());
             inboundConfig.setVoiceSource(map.get("voice_source").toString());
             inboundConfig.setAsrProvider(map.get("asr_provider").toString());
             inboundConfig.setServiceType(map.get("service_type").toString());

+ 8 - 0
src/main/java/com/telerobot/fs/service/SysService.java

@@ -3,6 +3,7 @@ package com.telerobot.fs.service;
 import com.telerobot.fs.config.AppContextProvider;
 import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.dao.BizGroup;
+import com.telerobot.fs.entity.dao.CcExtNum;
 import com.telerobot.fs.entity.dao.ExtPowerConfig;
 import com.telerobot.fs.entity.dao.LlmKb;
 import com.telerobot.fs.entity.dto.AgentEx;
@@ -177,4 +178,11 @@ public class SysService {
 		return sysDao.getKbListByCatId(catId);
 	}
 
+	public int updateExtension(CcExtNum ccExtNum) {
+		return sysDao.updateExtension(ccExtNum);
+	}
+
+	public List<CcExtNum> selectAllExtensions() {
+		return sysDao.selectAllExtensions();
+	}
 }

+ 58 - 4
src/main/java/com/telerobot/fs/utils/CommonUtils.java

@@ -19,13 +19,12 @@ import org.slf4j.LoggerFactory;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.net.URLDecoder;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -36,6 +35,40 @@ import java.util.zip.ZipOutputStream;
 public class CommonUtils<T>  {
 	private static final Logger logger = LoggerFactory.getLogger(CommonUtils.class);
 
+	public static String  execSystemCommand(String command) {
+		Runtime runtime = Runtime.getRuntime();
+		StringBuilder output = new StringBuilder();
+		try {
+			Process p = runtime.exec(command);
+			// 启动另一个进程来执行命令
+			BufferedInputStream in = new BufferedInputStream(p.getInputStream());
+			BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
+			String lineStr;
+			//获得命令执行后在控制台的输出信息
+			while ((lineStr = inBr.readLine()) != null) {
+				output.append(lineStr).append("\n");
+			}
+			if (p.waitFor(11, TimeUnit.SECONDS)) {
+				// p.exitValue()==0表示正常结束,1:非正常结束
+				if (p.exitValue() == 1) {
+					logger.error("exec system command error: {}", command);
+				}
+			}
+
+		} catch (Throwable e) {
+			logger.error("exec system command error: {} {} {}", command, e.toString(), CommonUtils.getStackTraceString(e.getStackTrace()));
+		}
+		return output.toString();
+	}
+
+	public static String getIpFromFullAddress(String clientFullAddr){
+		String ip = clientFullAddr;
+		if(clientFullAddr.contains(":")){
+			ip = clientFullAddr.substring(0, clientFullAddr.indexOf(":")) ;
+		};
+		return ip.replace("/","");
+	}
+
 	public static void hangupCallSession(String uuid, String reason){
 		EslConnectionUtil.sendExecuteCommand("hangup", reason, uuid);
 	}
@@ -287,7 +320,28 @@ public class CommonUtils<T>  {
 			return null;
 		}
 	}
-	
+
+	/**
+	 * String Scramble Tool Class
+	 * @param input
+	 * @return
+	 */
+	public static String shuffleString(String input) {
+		// 将字符串转换为字符数组以便随机交换
+		char[] chars = input.toCharArray();
+		Random random = new Random();
+
+		// Fisher-Yates洗牌算法
+		for (int i = chars.length - 1; i > 0; i--) {
+			int j = random.nextInt(i + 1); // 生成0到i之间的随机索引
+			// 交换字符
+			char temp = chars[i];
+			chars[i] = chars[j];
+			chars[j] = temp;
+		}
+		return new String(chars);
+	}
+
 	/**
 	 * 禁止jsp页面被客户端浏览器缓存
 	 * @param response

+ 11 - 5
src/main/java/com/telerobot/fs/utils/FileUtil.java

@@ -1,11 +1,15 @@
 package com.telerobot.fs.utils;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.*;
 
 /**
  * 文件操作工具类
  */
 public class FileUtil {
+	private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
 	public static boolean writeToLocal(String path, byte[] data) throws IOException {
 		File file = new File(path);
 		if (!file.exists()) {
@@ -54,14 +58,16 @@ public class FileUtil {
 		return out.toByteArray();
 	}
 	
-	public static void WriteStringToFile(String filePath, String content) {  
+	public static boolean WriteStringToFile(String filePath, String content) {
         try {  
             PrintWriter pw = new PrintWriter(new FileWriter(filePath));  
             pw.println(content);    
-            pw.close();  
-        } catch (IOException e) {  
-            e.printStackTrace();  
-        }  
+            pw.close();
+            return true;
+        } catch (Throwable e) {
+        	logger.error("Error! Failed to write file '{}' ! {} {}", filePath, e.toString(), CommonUtils.getStackTraceString(e.getStackTrace()));
+        }
+        return false;
     }  
 	
 	  public static void copyFile(File fromFile,File toFile) throws IOException{

+ 27 - 22
src/main/java/com/telerobot/fs/utils/FileUtils.java

@@ -1,11 +1,15 @@
 package com.telerobot.fs.utils;
 
 import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
 
 import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.core.io.FileSystemResource;
 import org.springframework.core.io.support.EncodedResource;
 import org.springframework.util.FileCopyUtils;
@@ -16,9 +20,10 @@ import javax.sound.sampled.AudioInputStream;
 import javax.sound.sampled.AudioSystem;
 
 public class FileUtils {
-
+	private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);
 	/**
 	 * 合并返回的录音文件为1个
+	 *
 	 * @param ttsFile
 	 * @param files
 	 * @return
@@ -37,10 +42,10 @@ public class FileUtils {
 				org.apache.commons.io.FileUtils.copyFile(firstFile, new File(ttsFile));
 			}
 		} catch (IOException e) {
-		   return false;
+			return false;
 		}
 		File out = new File(ttsFile);
-		if(!out.exists()){
+		if (!out.exists()) {
 			out.mkdirs();
 		}
 		try {
@@ -50,12 +55,12 @@ public class FileUtils {
 				ArrayList<AudioInputStream> aisList = new ArrayList<AudioInputStream>();
 				long frameLenght = 0L;
 				for (String file : fileArrs) {
-					if(StringUtils.isNotBlank(file)){
+					if (StringUtils.isNotBlank(file)) {
 						File f = new File(file);
 						int waitTtsTime = 0;
 						while (waitTtsTime < 10 && !f.exists()) {
 							ThreadUtil.sleep(100);
-							waitTtsTime ++;
+							waitTtsTime++;
 						}
 						AudioInputStream ais = AudioSystem.getAudioInputStream(f);
 						aisList.add(ais);
@@ -78,29 +83,29 @@ public class FileUtils {
 
 	public static String ReadFile(String filePath, String encoding) {
 		String content = "";
-		if(!new File(filePath).exists()) return "";
-		Resource res1 = new FileSystemResource(filePath); 
-		EncodedResource encRes = new EncodedResource(res1,encoding);
+		if (!new File(filePath).exists()) return "";
+		Resource res1 = new FileSystemResource(filePath);
+		EncodedResource encRes = new EncodedResource(res1, encoding);
 		try {
 			content = FileCopyUtils.copyToString(encRes.getReader());
 		} catch (Exception e) {
-			System.out.println("error reading file: " + filePath  + e.toString());
+			System.out.println("error reading file: " + filePath + e.toString());
 		}
 		return content;
 	}
-	
-	public static boolean WriteFile(String filePath, String content)
-	{
-		try
-		{
-		FileWriter fw = new FileWriter(filePath);
-		PrintWriter pw = new PrintWriter(fw);
-		pw.println(content);
-		pw.flush();
-		pw.close();
-		}
-		catch(Throwable ex)
-		{
+
+	public static boolean WriteFile(String filePath, String content) {
+		try {
+			OutputStreamWriter fw = new OutputStreamWriter(
+					new FileOutputStream(filePath),
+					StandardCharsets.UTF_8
+			);
+			PrintWriter pw = new PrintWriter(fw);
+			pw.println(content);
+			pw.flush();
+			pw.close();
+		} catch (Throwable ex) {
+            logger.error("Failed to write file {}, {}", filePath, CommonUtils.getStackTraceString(ex.getStackTrace()));
 			return false;
 		}
 		return true;

+ 2 - 6
src/main/java/com/telerobot/fs/utils/OkHttpClientUtil.java

@@ -78,13 +78,9 @@ public class OkHttpClientUtil {
      */
     public static String postCdr(String url, String data){
 
-        RequestBody requestBody = RequestBody.create(
-                MediaType.parse("application/json; charset=utf-8"),
-                data
-        );
-//        RequestBody requestBody = new FormBody.Builder().add("cdr", data).build();;
+        RequestBody requestBody = new FormBody.Builder().add("cdr", data).build();;
         Request.Builder  builder = new Request.Builder()
-                .post(requestBody)
+                    .post(requestBody)
                     .url(url);
 
         Request request = builder.build();

+ 103 - 0
src/main/java/com/telerobot/fs/utils/SipProfilesPortReader.java

@@ -0,0 +1,103 @@
+package com.telerobot.fs.utils;
+
+import link.thingscloud.freeswitch.esl.EslConnectionUtil;
+import link.thingscloud.freeswitch.esl.transport.message.EslMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.*;
+import javax.xml.parsers.*;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SipProfilesPortReader {
+    private static final Logger logger = LoggerFactory.getLogger(SipProfilesPortReader.class);
+    /**
+     * get  sip-ports of all sip-profile files in 'sip_profiles' directory
+     *
+     * @param dirPath sip_profiles directory
+     * @return sip-port
+     */
+    public static List<Integer> readSipPorts(String dirPath) {
+        List<Integer> sipPorts = new ArrayList<>();
+
+        File dir = new File(dirPath);
+        if (!dir.exists() || !dir.isDirectory()) {
+            logger.error("directory not exists , {}", dir.getName());
+            return sipPorts;
+        }
+
+        File[] files = dir.listFiles((d, name) -> name.endsWith(".xml"));
+        if (files == null) {
+            logger.error("No sip profiles found in directory  {} !", dir.getName());
+            return sipPorts;
+        }
+
+        for (File file : files) {
+            parseFile(file, sipPorts);
+        }
+
+        return sipPorts;
+    }
+
+    private static void parseFile(File file, List<Integer> sipPorts) {
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            factory.setIgnoringComments(true);
+            factory.setNamespaceAware(false);
+
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document doc = builder.parse(file);
+
+            NodeList paramList = doc.getElementsByTagName("param");
+
+            for (int i = 0; i < paramList.getLength(); i++) {
+                Element param = (Element) paramList.item(i);
+
+                String name = param.getAttribute("name");
+                if ("sip-port".equals(name)) {
+                    String value = param.getAttribute("value");
+                    int port = 0;
+                    if(!StringUtils.isNumeric(value)){
+                        EslMessage response = EslConnectionUtil.sendSyncApiCommand("eval", value);
+                        StringBuilder body = new StringBuilder();
+                        List<String> array = response.getBodyLines();
+                        for (String s : array) {
+                            body.append(s);
+                        }
+                        String actualValue = body.toString().trim();
+                        if(StringUtils.isNumeric(actualValue)) {
+                            logger.info("Dynamically calculate the actual value of the sip-port parameter '{}'  as  '{}'.", value, actualValue);
+                            port = Integer.parseInt(actualValue);
+                        }else{
+                            logger.error("Error dynamically calculating the actual value of the sip-port parameter! {} {}", value, actualValue);
+                        }
+                    }else{
+                        port = Integer.parseInt(value);
+                    }
+                    if(port > 0) {
+                        sipPorts.add(port);
+                    }
+                }
+            }
+
+        } catch (Throwable e) {
+            logger.error("sip profile {} read error , {}", file.getName(), CommonUtils.getStackTraceString(e.getStackTrace()));
+        }
+    }
+
+    public static void main(String[] args) {
+//        String path = "D:\\Program Files\\FreeSWITCH\\conf\\sip_profiles\\";
+//
+//        List<String> sipPorts = readSipPorts(path);
+//
+//        System.out.println("all sip-port:");
+//        for (String port : sipPorts) {
+//            System.out.println(port);
+//        }
+
+        String test = "/192.168.67.217:49952";
+        String clientIP = CommonUtils.getIpFromFullAddress(test);
+        System.out.println(clientIP);
+    }
+}

+ 66 - 0
src/main/java/com/telerobot/fs/utils/SwitchRtpPortConfigReader.java

@@ -0,0 +1,66 @@
+package com.telerobot.fs.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.*;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SwitchRtpPortConfigReader {
+    private static final Logger logger = LoggerFactory.getLogger(SwitchRtpPortConfigReader.class);
+
+    /**
+     *  read 'rtp-start-port' and 'rtp-end-port' parameter from switch.conf.xml
+     * @return rtp-start-port = arrayList.get(0); rtp-end-port = arrayList.get(1)
+     */
+    public static List<Integer> load(String filePath) {
+        int rtpStartPort = 0;
+        int rtpEndPort = 0;
+
+        List<Integer> resultList = new ArrayList<>();
+        try {
+            File file = new File(filePath);
+
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document doc = builder.parse(file);
+
+            NodeList paramList = doc.getElementsByTagName("param");
+
+            for (int i = 0; i < paramList.getLength(); i++) {
+                Node node = paramList.item(i);
+
+                if (node.getNodeType() == Node.ELEMENT_NODE) {
+                    Element element = (Element) node;
+
+                    String name = element.getAttribute("name");
+                    String value = element.getAttribute("value");
+
+                    if ("rtp-start-port".equals(name)) {
+                        rtpStartPort = Integer.parseInt(value);
+                    } else if ("rtp-end-port".equals(name)) {
+                        rtpEndPort = Integer.parseInt(value);
+                    }
+                }
+            }
+
+        } catch (Throwable e) {
+            logger.error("switch.conf.xml read error , {}", CommonUtils.getStackTraceString(e.getStackTrace()));
+        }
+        resultList.add(rtpStartPort);
+        resultList.add(rtpEndPort);
+        return  resultList;
+    }
+
+    public static void main(String[] args) {
+        SwitchRtpPortConfigReader reader = new SwitchRtpPortConfigReader();
+
+        List<Integer> resultList = reader.load("D:\\Program Files\\FreeSWITCH\\conf\\autoload_configs\\switch.conf.xml");
+
+        System.out.println("rtp-start-port = " + resultList.get(0));
+        System.out.println("rtp-end-port   = " + resultList.get(1));
+    }
+}

+ 6 - 0
src/main/java/com/telerobot/fs/wshandle/RespStatus.java

@@ -204,6 +204,12 @@ public class RespStatus {
      */
     public static final int INNER_CONSULTATION_STOP =  633;
 
+
+    /**
+     * extension cannot connected
+     */
+    public static final int EXTENSION_CANNOT_CONNECTED =  634;
+
     /**
      * 多人电话会议,重复的被叫;
      */

+ 223 - 0
src/main/java/com/telerobot/fs/wshandle/SecurityManager.java

@@ -0,0 +1,223 @@
+package com.telerobot.fs.wshandle;
+
+import com.alibaba.fastjson.JSON;
+import com.telerobot.fs.config.SystemConfig;
+import com.telerobot.fs.utils.*;
+import link.thingscloud.freeswitch.esl.EslConnectionUtil;
+import link.thingscloud.freeswitch.esl.transport.message.EslMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+
+/**
+ * Use the firewalld service and FreeSWITCH's built-in ACL mechanism
+ *  to ensure the security of the telephony system.
+ * @author easycallcenter365
+ */
+public class SecurityManager {
+    private static final Object SYNC_ROOT = new Object();
+    private static final Logger logger = LoggerFactory.getLogger(SecurityManager.class);
+    private static SecurityManager instance;
+    private SecurityManager() {
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    logger.info("The worker thread for dynamically adding IP whitelist entries to the firewalld has started.");
+                    while (true) {
+                        try {
+                            scheduledUpdateFirewall();
+                        } catch (Throwable e) {
+                            logger.error("SessionManager scheduledUpdateIptables error: {}, {}",
+                                    e.toString(),
+                                    CommonUtils.getStackTraceString(e.getStackTrace())
+                            );
+                        }
+                        ThreadUtil.sleep(5000);
+                    }
+                }
+            }).start();
+    }
+
+    public static SecurityManager getInstance() {
+        if (instance == null) {
+            synchronized (SYNC_ROOT) {
+                if (instance == null) {
+                    instance = new SecurityManager();
+                }
+            }
+        }
+        return instance;
+    }
+
+    private static final ArrayBlockingQueue<String> IP_ADDRESS = new ArrayBlockingQueue<>(1000);
+    private static boolean hasNewIpAddr = false;
+    /**
+     *  Add the specified IP address to the firewall whitelist.
+     * @param ip client ip
+     */
+    public void addClientIpToFirewallWhiteList(String ip){
+        if(!IP_ADDRESS.contains(ip)) {
+            synchronized (SYNC_ROOT) {
+                if (!IP_ADDRESS.contains(ip)) {
+                    IP_ADDRESS.add(ip);
+                    hasNewIpAddr = true;
+                    logger.info("Add the specified IP address to the firewall whitelist. ipAddress = {} .", ip);
+                }
+            }
+        }
+    }
+
+    public void reloadFirewallConfig(){
+        hasNewIpAddr = true;
+    }
+
+    /**
+     * create freeSWITCH acl File
+     * @param aclType inbound 、 register
+     * @allIpList
+     */
+    private boolean createFsAclFile(String aclType, List<String> allIpList){
+        String fsConfPath = SystemConfig.getValue("fs_conf_directory");
+        String paramName = String.format("fs-%s-acl-enabled", aclType);
+        if (Boolean.parseBoolean(SystemConfig.getValue(paramName, "true"))) {
+            logger.info("{}=true, use freeSWITCH's built-in ACL mechanism to ensure the security.", paramName);
+            String inboundAclFile = fsConfPath +  String.format("/autoload_configs/%s_acl.xml", aclType);
+            StringBuilder aclItemList = new StringBuilder();
+            aclItemList.append(String.format("<list name=\"%s_allow_list\" default=\"deny\">\n", aclType));
+            for (String ip : allIpList) {
+                aclItemList.append("  <node type=\"allow\" cidr=\"");
+                aclItemList.append(ip.trim());
+                aclItemList.append("/32\"/>\n");
+            }
+            aclItemList.append("</list>");
+            return FileUtils.WriteFile(inboundAclFile, aclItemList.toString());
+        }else{
+            logger.info("{}=false, freeSWITCH's built-in ACL mechanism is disabled.", paramName);
+        }
+        return false;
+    }
+
+    /**
+     *  Scheduled refresh of the firewall configuration file and restart of the firewall service.
+     */
+    private void scheduledUpdateFirewall() {
+        if(hasNewIpAddr) {
+            List<String> allIpAddress;
+            synchronized (SYNC_ROOT) {
+                 allIpAddress =  SessionManager.getInstance().getAllUserIpList();
+                IP_ADDRESS.clear();
+                IP_ADDRESS.addAll(allIpAddress);
+                hasNewIpAddr = false;
+            }
+
+            String fsConfPath = SystemConfig.getValue("fs_conf_directory");
+            List<Integer> fsPortList = SipProfilesPortReader.readSipPorts(fsConfPath + "/sip_profiles/");
+            if(fsPortList.size() ==0 ){
+                return;
+            }
+
+            List<Integer> rtpPortInfo = SwitchRtpPortConfigReader.load(fsConfPath + "/autoload_configs/switch.conf.xml");
+            if(rtpPortInfo.size() != 2){
+                return;
+            }
+
+            // get inbound white ip list
+            List<String> inboundAllowIpList = new ArrayList<>(10);
+            String whiteIPList = SystemConfig.getValue("fs-inbound-allow-ip-list", "");
+            if(!StringUtils.isNullOrEmpty(whiteIPList)){
+                String[] array = whiteIPList.split("\\r\\n");
+                for (String s : array) {
+                    inboundAllowIpList.add(s.trim());
+                }
+            }
+            allIpAddress.addAll(inboundAllowIpList);
+
+            // get inbound white ip list
+            List<String> registerAllowIpList = new ArrayList<>(10);
+            whiteIPList = SystemConfig.getValue("fs-register-allow-ip-list", "");
+            if(!StringUtils.isNullOrEmpty(whiteIPList)){
+                String[] array = whiteIPList.split("\\r\\n");
+                for (String s : array) {
+                    registerAllowIpList.add(s.trim());
+                }
+            }
+
+            if(allIpAddress.size() > 0 || inboundAllowIpList.size() > 0 ||  registerAllowIpList.size() > 0 ) {
+                if (Boolean.parseBoolean(SystemConfig.getValue("firewalld-enabled", "true"))) {
+                    logger.info("firewalld-enabled=true, use system firewalld mechanism to ensure the security.");
+                    StringBuilder sb = new StringBuilder();
+                    sb.append("<!-- The following rules is generated by easycallcenter365. Dot not edit it manually. --> \n ");
+                    int wsPort = Integer.parseInt(SystemConfig.getValue("ws-server-port", "1081"));
+                    sb.append(String.format("   <port port=\"%s\" protocol=\"tcp\"/>\n", String.valueOf(wsPort)));
+                    sb.append("   <service name=\"ssh\"/>\n");
+                    sb.append("   <service name=\"dhcpv6-client\"/>\n");
+                    for (String ip : allIpAddress) {
+                        for (int port : fsPortList) {
+                            sb.append("   <rule family=\"ipv4\">\n");
+                            sb.append("     <source address=\"");
+                            sb.append(ip);
+                            sb.append("\"/>\n");
+                            sb.append("     <port port=\"");
+                            sb.append(port);
+                            sb.append("\" protocol=\"udp\"/>\n");
+                            sb.append("     <accept/>\n");
+                            sb.append("   </rule>\n");
+                        }
+
+                        // append rtp-start-port and rtp-end-port
+                        sb.append("   <rule family=\"ipv4\">\n");
+                        sb.append("     <source address=\"");
+                        sb.append(ip);
+                        sb.append("\"/>\n");
+                        sb.append("     <port port=\"");
+                        sb.append(rtpPortInfo.get(0));
+                        sb.append("-");
+                        sb.append(rtpPortInfo.get(1));
+                        sb.append("\" protocol=\"udp\"/>\n");
+                        sb.append("     <accept/>\n");
+                        sb.append("   </rule>\n");
+                    }
+
+                    String firewallTemplate = fsConfPath + "/autoload_configs/firewalld-template.xml";
+                    String firewallConfig = SystemConfig.getValue("firewalld-config-path", "/etc/firewalld/zones/public.xml");
+                    String toReplacer = "<!--${office_ip_rule_list}-->";
+                    String fileContent = FileUtils.ReadFile(firewallTemplate, "utf-8");
+                    if (StringUtils.isNullOrEmpty(fileContent)) {
+                        logger.error("Unable to read the firewall template file :{}", firewallTemplate);
+                        return;
+                    }
+                    String writeContent = fileContent.replace(toReplacer, sb.toString());
+                    boolean success = FileUtils.WriteFile(firewallConfig, writeContent);
+                    if (success) {
+                        logger.info("Successfully updated the firewall configuration file. Preparing to restart the firewall service.");
+                        String restartCmd = SystemConfig.getValue("firewalld-restart-cmd", "/usr/bin/systemctl  restart firewalld");
+                        String response = CommonUtils.execSystemCommand(restartCmd);
+                        if (response.contains("Failed")) {
+                            logger.error("Got restart firewalld response:{}", response);
+                        } else {
+                            logger.info("Got restart firewalld response:{}", response);
+                        }
+                    }
+                }else{
+                    logger.info("firewalld-enabled=false, system firewall is disabled.");
+                }
+
+
+                boolean createInboundAclFileOk = createFsAclFile("inbound", allIpAddress);
+                allIpAddress.removeAll(inboundAllowIpList);
+
+                allIpAddress.addAll(registerAllowIpList);
+                boolean createRegisterAclFileOk = createFsAclFile("register", allIpAddress);
+                if(createInboundAclFileOk || createRegisterAclFileOk) {
+                    logger.info("Preparing to reload freeSWITCH configs.");
+                    EslMessage response = EslConnectionUtil.sendSyncApiCommand("reloadacl", "");
+                    logger.info("reloadacl response: {}", JSON.toJSONString(response));
+                }
+            }
+        }
+    }
+
+}

+ 15 - 3
src/main/java/com/telerobot/fs/wshandle/SessionManager.java

@@ -22,13 +22,25 @@ import java.util.concurrent.ConcurrentHashMap;
 public class SessionManager {
 	private static SessionManager instance;
 	private static final Object syncRoot = new Object();
-	private static final Logger logger = LoggerFactory.getLogger(MessageHandlerEngine.class);
+	private static final Logger logger = LoggerFactory.getLogger(SessionManager.class);
 	
 	/**
 	 * 保存所有客户端的Session会话信息的容器
 	 */
-	private Map<String, SessionEntity> sessionContainer = new ConcurrentHashMap<String, SessionEntity>();
-	
+	private Map<String, SessionEntity> sessionContainer = new ConcurrentHashMap<String, SessionEntity>(1000);
+
+	public List<String> getAllUserIpList(){
+		List<String> allIpAddress = new ArrayList<String>(200);
+		Iterator<Map.Entry<String, SessionEntity>> it = sessionContainer.entrySet().iterator();
+		while (it.hasNext()) {
+			Map.Entry<String, SessionEntity> entry = it.next();
+			String ip = entry.getValue().getClientIp();
+			if (!allIpAddress.contains(ip)) {
+				allIpAddress.add(ip);
+			}
+		}
+		return  allIpAddress;
+	}
 	
 	private SessionManager() {
 

+ 10 - 0
src/main/java/com/telerobot/fs/wshandle/SwitchChannel.java

@@ -87,6 +87,8 @@ public class SwitchChannel {
 
     private volatile InboundDetail inboundDetail = null;
 
+    private volatile String bizFieldValue;
+
     /**
      * 挂机的sip状态码
      */
@@ -264,6 +266,14 @@ public class SwitchChannel {
         this.inboundDetail = inboundDetail;
     }
 
+    public String getBizFieldValue() {
+        return bizFieldValue;
+    }
+
+    public void setBizFieldValue(String bizFieldValue) {
+        this.bizFieldValue = bizFieldValue;
+    }
+
     /**
      *  获取完整的录音/录像的路径;
      * @return

+ 11 - 0
src/main/java/com/telerobot/fs/wshandle/firewalld-template.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<zone>
+  <short>Public</short>
+  <description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
+  <service name="ssh"/>
+  <service name="dhcpv6-client"/>
+  <port port="1081" protocol="tcp"/>
+  <port port="8899" protocol="tcp"/>   
+<!--${office_ip_rule_list}-->
+  <forward/>
+</zone>

+ 29 - 17
src/main/java/com/telerobot/fs/wshandle/impl/CallApi.java

@@ -146,6 +146,7 @@ public class CallApi extends MsgHandlerBase {
             ));
             return;
         }
+
         // Use the callWait processor to keep the current customer call  on hold with background music.
         sendCustomerCallToCallWaitHandle();
 
@@ -171,26 +172,26 @@ public class CallApi extends MsgHandlerBase {
         String uuidInner = UuidGenerator.GetOneUuid();
         String uuidOuter = UuidGenerator.GetOneUuid();
 
-        SwitchChannel customerChannel = new SwitchChannel(uuidOuter, uuidInner, PhoneCallType.AUDIO_CALL, CallDirection.INBOUND);
+        SwitchChannel consultationChannel = new SwitchChannel(uuidOuter, uuidInner, PhoneCallType.AUDIO_CALL, CallDirection.INBOUND);
         SwitchChannel agentChannel = new SwitchChannel(uuidInner, uuidOuter, PhoneCallType.AUDIO_CALL, CallDirection.INBOUND);
 
-        customerChannel.setPhoneNumber(to);
-        customerChannel.setBridgeCallAfterPark(true);
-        customerChannel.setSendChannelStatusToWsClient(true);
+        consultationChannel.setPhoneNumber(to);
+        consultationChannel.setBridgeCallAfterPark(true);
+        consultationChannel.setSendChannelStatusToWsClient(true);
         agentChannel.setPhoneNumber(getExtNum());
         agentChannel.setBridgeCallAfterPark(false);
         agentChannel.setSendChannelStatusToWsClient(true);
 
-        this.connectExtension(agentChannel, customerChannel);
+        this.connectExtension(agentChannel, consultationChannel);
         EslConnectionPool connectionPool = EslConnectionUtil.getDefaultEslConnectionPool();
         connectionPool.getDefaultEslConn().addListener(agentChannel.getUuid(), listener);
-        connectionPool.getDefaultEslConn().addListener(customerChannel.getUuid(), listener);
+        connectionPool.getDefaultEslConn().addListener(consultationChannel.getUuid(), listener);
 
         if (agentChannel.getAnsweredTime() > 0) {
 
             if(transferType.equalsIgnoreCase("outer")){
-                customerChannel.setFlag(ChannelFlag.EXTERNAL_LINE);
-                this.bridgeAgentToExternalLineForConsultation(customerChannel, agentChannel, from, to);
+                consultationChannel.setFlag(ChannelFlag.EXTERNAL_LINE);
+                this.bridgeAgentToExternalLineForConsultation(consultationChannel, agentChannel, from, to);
                 return;
             }
 
@@ -205,7 +206,7 @@ public class CallApi extends MsgHandlerBase {
                     return;
                 }
 
-                customerChannel.setPhoneNumber(engine.getSessionInfo().getExtNum());
+                consultationChannel.setPhoneNumber(engine.getSessionInfo().getExtNum());
 
                 JSONObject jsonObject = new JSONObject();
                 jsonObject.put("status", AgentStatus.busy.getIndex());
@@ -242,7 +243,7 @@ public class CallApi extends MsgHandlerBase {
                     return;
                 }
 
-                customerChannel.setAnsweredHook(new IOnAnsweredHook() {
+                consultationChannel.setAnsweredHook(new IOnAnsweredHook() {
                     @Override
                     public void onAnswered(Map<String, String> eventHeaders, String traceId) {
                         String tips = "The call consultation has started.";
@@ -283,9 +284,9 @@ public class CallApi extends MsgHandlerBase {
                     }
                 });
 
-                customerChannel.setAnsweredTime(0L);
-                callApi.connectExtension(customerChannel, agentChannel);
-                if (customerChannel.getAnsweredTime() <= 0) {
+                consultationChannel.setAnsweredTime(0L);
+                callApi.connectExtension(consultationChannel, agentChannel);
+                if (consultationChannel.getAnsweredTime() <= 0) {
                     logger.warn("{} The person being consulted has no answer, terminate call session. ", getTraceId());
                     this.listener.endCall("Consultation-call-failed.");
                 }
@@ -1093,7 +1094,7 @@ public class CallApi extends MsgHandlerBase {
         return bridgeString;
     }
 
-
+    private static final String BIZ_FIELD_DEFAULT_VALUE = "NOT_SET";
 
 	/**
 	 *  开始通话:
@@ -1134,8 +1135,14 @@ public class CallApi extends MsgHandlerBase {
             gatewayListArgs = callArgs.getArgs().getString("gatewayList");
         }
 
-        List<GatewayConfig> gatewayList = JSON.parseObject(gatewayListArgs, new TypeReference<List<GatewayConfig>>() {
-        });
+        List<GatewayConfig> gatewayList = null;
+        try {
+            gatewayList = JSON.parseObject(gatewayListArgs, new TypeReference<List<GatewayConfig>>() {
+            });
+        }catch (Throwable e){
+            logger.error("{} parse gatewayList parameter error! {}  \n {}", getTraceId(), e.toString(),
+                    CommonUtils.getStackTraceString(e.getStackTrace()));
+        }
         logger.info("gatewayListArgs={}", gatewayListArgs);
         if (null == gatewayList || gatewayList.size() == 0) {
             Utils.processArgsError("gatewayList parameter error!", thisRef);
@@ -1149,7 +1156,9 @@ public class CallApi extends MsgHandlerBase {
             caseNo = phoneAndCaseInfo.split(";")[1];
             phone = phoneAndCaseInfo.split(";")[0];
         } else {
-            caseNo = "notSet";
+            // This is a custom business field parameter.
+            // It will be carried when sending messages via WebSocket.
+            caseNo = BIZ_FIELD_DEFAULT_VALUE;
             phone = phoneAndCaseInfo;
         }
 
@@ -1170,6 +1179,7 @@ public class CallApi extends MsgHandlerBase {
         SwitchChannel customerChannel = new SwitchChannel(uuidOuter, uuidInner, callType, CallDirection.OUTBOUND);
 
         agentChannel.setVideoLevel(videoLevel);
+        agentChannel.setBizFieldValue(caseNo);
         agentChannel.setPhoneNumber(this.getExtNum());
         agentChannel.setSendChannelStatusToWsClient(true);
         customerChannel.setVideoLevel(videoLevel);
@@ -1214,7 +1224,9 @@ public class CallApi extends MsgHandlerBase {
 
         JSONObject outboundStartEvent = new JSONObject();
         outboundStartEvent.put("uuid", agentChannel.getUuid());
+        outboundStartEvent.put("uuid_customer", customerChannel.getUuid());
         outboundStartEvent.put("destPhone", customerChannel.getPhoneNumber());
+        outboundStartEvent.put("biz_field_value", caseNo);
         this.sendReplyToAgent(
                 new MessageResponse(
                         RespStatus.OUTBOUND_START,

+ 38 - 1
src/main/java/com/telerobot/fs/wshandle/impl/CallListener.java

@@ -1,11 +1,14 @@
 package com.telerobot.fs.wshandle.impl;
 
 import com.alibaba.fastjson.JSONObject;
+import com.telerobot.fs.config.AppContextProvider;
 import com.telerobot.fs.entity.bo.ChanneState;
 import com.telerobot.fs.entity.bo.ChannelFlag;
 import com.telerobot.fs.entity.bo.ConferenceCommand;
+import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.pojo.AgentStatus;
 import com.telerobot.fs.global.BizThreadPoolForEsl;
+import com.telerobot.fs.ivr.IvrEngine;
 import com.telerobot.fs.utils.CommonUtils;
 import com.telerobot.fs.utils.DateUtils;
 import com.telerobot.fs.utils.StringUtils;
@@ -21,6 +24,7 @@ import org.slf4j.LoggerFactory;
 import java.util.Date;
 import java.util.Map;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
 
 public class CallListener implements IEslEventListener {
 	private static final Logger logger = LoggerFactory.getLogger(CallListener.class);
@@ -138,9 +142,14 @@ public class CallListener implements IEslEventListener {
 				if(customerChannel.getSendChannelStatusToWsClient()){
 					JSONObject jsonObject = new JSONObject();
 					jsonObject.put("uuid", agentChannel.getUuid());
+					jsonObject.put("uuid_customer", customerChannel.getUuid());
+					jsonObject.put("biz_field_value", agentChannel.getBizFieldValue());
 					if(customerChannel.getCallDirection().equalsIgnoreCase(CallDirection.OUTBOUND)){
 						jsonObject.put("callType", customerChannel.getCallType());
 					}
+					if(customerChannel.getInboundDetail() != null) {
+						jsonObject.put("callDetail", customerChannel.getInboundDetail());
+					}
 					logger.info("{} customerChannel is connected,call confirmed.", getTraceId());
 					callApiObject.sendReplyToAgent(
 							new MessageResponse(RespStatus.CALLEE_ANSWERED, "call connected.", jsonObject)
@@ -159,6 +168,11 @@ public class CallListener implements IEslEventListener {
 				if(agentChannel.getSendChannelStatusToWsClient()){
 					JSONObject jsonObject = new JSONObject();
 					jsonObject.put("uuid", agentChannel.getUuid());
+					jsonObject.put("uuid_customer", customerChannel.getUuid());
+					jsonObject.put("biz_field_value", agentChannel.getBizFieldValue());
+					if(customerChannel.getInboundDetail() != null) {
+						jsonObject.put("callDetail", customerChannel.getInboundDetail());
+					}
 					if(agentChannel.getCallDirection().equalsIgnoreCase(CallDirection.INBOUND)){
 						jsonObject.put("callType", agentChannel.getCallType());
 					}
@@ -250,7 +264,14 @@ public class CallListener implements IEslEventListener {
 						JSONObject jsonObject = new JSONObject();
 						String hangupSipCode = headers.get("variable_sip_invite_failure_status");
 						String hangupCause = headers.get("Hangup-Cause");
+						jsonObject.put("uuid", agentChannel.getUuid());
+						jsonObject.put("uuid_customer", customerChannel.getUuid());
+						jsonObject.put("biz_field_value", agentChannel.getBizFieldValue());
 						jsonObject.put("My-Hangup-Cause", hangupCause + ":" + hangupSipCode);
+						if(customerChannel.getInboundDetail() != null) {
+							jsonObject.put("callDetail", customerChannel.getInboundDetail());
+						}
+
 						callApiObject.sendReplyToAgent(
 								new MessageResponse(RespStatus.CALLER_HANGUP, "extension is hangup.", jsonObject)
 						);
@@ -260,7 +281,7 @@ public class CallListener implements IEslEventListener {
 						}
 					}
 
-					if(customerChannel.testFlag(ChannelFlag.SATISFACTION_SURVEY_REQUIRED)){
+					if(customerChannel.testFlag(ChannelFlag.SATISFACTION_SURVEY_REQUIRED)) {
 						logger.info("{} satisfaction survey is required for this session , we wait for this process.", getTraceId());
 						return;
 					}
@@ -320,6 +341,22 @@ public class CallListener implements IEslEventListener {
 					customerChannel.getParkHook().onPark(headers, getTraceId());
 					customerChannel.setParkHook(null);
                 }
+
+				if(customerChannel.testFlag(ChannelFlag.SATISFACTION_SURVEY_REQUIRED)){
+					logger.info("{} satisfaction survey is required for this session , we wait for this process.", getTraceId());
+					boolean channelHold = customerChannel.testFlag(ChannelFlag.HOLD_CALL);
+					InboundDetail inboundDetail = customerChannel.getInboundDetail();
+					if(inboundDetail != null) {
+						String satisfSurveyIvrId = inboundDetail.getSatisfSurveyIvrId();
+						if (inboundDetail.getManualAnsweredTime() > 0L && !channelHold) {
+							if (!StringUtils.isNullOrEmpty(satisfSurveyIvrId)) {
+								logger.info("{} Try to start ivr process for satisfaction survey. ivrId={}.",
+										inboundDetail.getUuid(), satisfSurveyIvrId);
+								AppContextProvider.getBean(IvrEngine.class).startIvrSession(inboundDetail, satisfSurveyIvrId);
+							}
+						}
+					}
+				}
 			}else if (uniqueId.equalsIgnoreCase(agentChannel.getUuid())) {
 				if(agentChannel.getBridgeCallAfterPark()){
 					agentChannel.setBridgeCallAfterPark(false);

+ 8 - 0
src/main/java/com/telerobot/fs/wshandle/nettyserver/WebSocketServer.java

@@ -3,6 +3,10 @@ package com.telerobot.fs.wshandle.nettyserver;
 import com.telerobot.fs.config.AppContextProvider;
 import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.mybatis.dao.SysDao;
+import com.telerobot.fs.service.SysService;
+import com.telerobot.fs.utils.CommonUtils;
+import com.telerobot.fs.utils.FileUtil;
+import com.telerobot.fs.utils.FileUtils;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.PooledByteBufAllocator;
 import io.netty.channel.*;
@@ -18,6 +22,8 @@ import org.springframework.context.annotation.DependsOn;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.PreDestroy;
+import java.io.File;
+import java.util.UUID;
 
 /**
  * 启动服务
@@ -94,6 +100,8 @@ public class WebSocketServer implements ApplicationListener<ApplicationReadyEven
 		this.port = port;
 	}
 
+
+
 	public void runWebsocketServer() {
 		try {
 			this.port = Integer.parseInt(SystemConfig.getValue("ws-server-port", "1081"));

+ 8 - 4
src/main/java/com/telerobot/fs/wshandle/nettyserver/WebSocketServerHandler.java

@@ -9,6 +9,7 @@ import com.telerobot.fs.utils.CommonUtils;
 import com.telerobot.fs.utils.StringUtils;
 import com.telerobot.fs.utils.ThreadUtil;
 import com.telerobot.fs.wshandle.*;
+import com.telerobot.fs.wshandle.SecurityManager;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFuture;
@@ -279,8 +280,10 @@ public class WebSocketServerHandler extends BaseWebSocketServerHandler {
 					jsonObject.put("groups", groups);
 					replyMsg.setStatus(200);
 					replyMsg.setObject(jsonObject);
+					String remoteAddr = ctx.channel().remoteAddress().toString();
+					String clientIP = CommonUtils.getIpFromFullAddress(remoteAddr);
 					SessionEntity sessionEntity = new SessionEntity();
-					sessionEntity.setClientIp(ctx.channel().remoteAddress().toString());
+					sessionEntity.setClientIp(clientIP);
 					sessionEntity.setExtNum(extnum);
 					sessionEntity.setOpNum(opnum);
 					sessionEntity.setSessionId(ctx.channel().id().asLongText());
@@ -289,11 +292,12 @@ public class WebSocketServerHandler extends BaseWebSocketServerHandler {
 					sessionEntity.setLoginTime(System.currentTimeMillis());
 					sessionEntity.setGroupId(groupId);
 					boolean addSessionOk = SessionManager.getInstance().add(sessionEntity); // 添加到会话管理
-					if (addSessionOk) {
-						logger.info("{} successfully add current session.", traceId);
-					} else {
+					if (!addSessionOk) {
 						logger.error("{} failed to add current session.", traceId);
+						return;
 					}
+					SecurityManager.getInstance().addClientIpToFirewallWhiteList(clientIP);
+					logger.info("{} successfully add current session.", traceId);
 					MessageHandlerEngine myEngine = new MessageHandlerEngine(ctx);
 					logger.info("{} successfully create MsgEngine for current user.", traceId);
 					if (!MessageHandlerEngineList.getInstance().add(myEngine)) {

+ 2 - 2
src/main/resources/application-238.properties

@@ -1,6 +1,6 @@
-spring.datasource.url=jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
+spring.datasource.url=jdbc:mysql://127.0.0.1:3306/easycallcenter365?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
 spring.datasource.username=root
-spring.datasource.password=easycallcenter365
+spring.datasource.password=tydic202x888
 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 log.path=/home/easycallcenter365/logs/easycallcenter365
 

+ 1 - 1
src/main/resources/application.properties

@@ -1 +1 @@
-spring.profiles.active=pro
+spring.profiles.active=uat

+ 28 - 1
src/main/resources/com/telerobot/fs/mybatis/persistence/SysMapper.xml

@@ -221,8 +221,35 @@
 		 from cc_agent_online where group_id=#{groupId} and agent_status in (4, 5)
 		order by lastHangupTime asc
 	</select>
-
 	<select id="getUserBizJson" resultType="java.lang.String">
 		select biz_json from cc_call_phone where uuid = #{uuid}
 	</select>
+
+	<resultMap id="ExtensionResultMap" type="com.telerobot.fs.entity.dao.CcExtNum">
+		<id column="ext_id" property="extId" jdbcType="INTEGER"/>
+		<result column="ext_num" property="extNum" jdbcType="VARCHAR"/>
+		<result column="ext_pass" property="extPass" jdbcType="VARCHAR"/>
+		<result column="user_code" property="userCode" jdbcType="VARCHAR"/>
+	</resultMap>
+
+	<update id="updateExtension" parameterType="com.telerobot.fs.entity.dao.CcExtNum">
+        UPDATE cc_ext_num
+        SET
+            ext_num = #{extNum, jdbcType=VARCHAR},
+            ext_pass = #{extPass, jdbcType=VARCHAR},
+            user_code = #{userCode, jdbcType=VARCHAR}
+        WHERE
+            ext_id = #{extId, jdbcType=INTEGER}
+    </update>
+
+	<select id="selectAllExtensions" resultMap="ExtensionResultMap">
+        SELECT
+            ext_id,
+            ext_num,
+            ext_pass,
+            user_code
+        FROM
+            cc_ext_num
+    </select>
+
 </mapper>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio