yzx před 1 dnem
rodič
revize
aaff7ce440
51 změnil soubory, kde provedl 1202 přidání a 450 odebrání
  1. 42 29
      docs/ccPhoneBarSocket.js
  2. 36 1
      docs/phone-bar-ex.html
  3. 0 17
      pom.xml
  4. 6 1
      sql-scripts/clear.txt
  5. 303 4
      sql-scripts/easycallcenter365.sql
  6. 45 45
      src/main/java/com/telerobot/fs/acd/CallHandler.java
  7. 52 0
      src/main/java/com/telerobot/fs/config/AudioUtils.java
  8. 4 0
      src/main/java/com/telerobot/fs/controller/InboundCallController.java
  9. 2 2
      src/main/java/com/telerobot/fs/controller/ReloadParams.java
  10. 4 0
      src/main/java/com/telerobot/fs/entity/bo/ChannelFlag.java
  11. 19 0
      src/main/java/com/telerobot/fs/entity/bo/InboundDetail.java
  12. 36 0
      src/main/java/com/telerobot/fs/entity/dao/CallTaskEntity.java
  13. 4 0
      src/main/java/com/telerobot/fs/entity/dto/InboundConfig.java
  14. 1 1
      src/main/java/com/telerobot/fs/entity/dto/LlmAiphoneRes.java
  15. 27 1
      src/main/java/com/telerobot/fs/entity/dto/llm/AccountBaseEntity.java
  16. 4 1
      src/main/java/com/telerobot/fs/entity/pojo/LlmToolRequest.java
  17. 1 0
      src/main/java/com/telerobot/fs/entity/pojo/TtsProvider.java
  18. 3 30
      src/main/java/com/telerobot/fs/global/CdrPush.java
  19. 4 1
      src/main/java/com/telerobot/fs/mybatis/dao/SysDao.java
  20. 11 2
      src/main/java/com/telerobot/fs/mybatis/dao/SysDaoImpl.java
  21. 5 1
      src/main/java/com/telerobot/fs/mybatis/persistence/SysMapper.java
  22. 6 2
      src/main/java/com/telerobot/fs/outbound/batchcall/BatchTaskManager.java
  23. 5 0
      src/main/java/com/telerobot/fs/outbound/batchcall/CallTask.java
  24. 8 1
      src/main/java/com/telerobot/fs/robot/AbstractChatRobot.java
  25. 35 5
      src/main/java/com/telerobot/fs/robot/RobotBase.java
  26. 118 48
      src/main/java/com/telerobot/fs/robot/RobotChat.java
  27. 21 2
      src/main/java/com/telerobot/fs/robot/impl/Coze.java
  28. 6 24
      src/main/java/com/telerobot/fs/robot/impl/DeepSeekChat.java
  29. 2 1
      src/main/java/com/telerobot/fs/robot/impl/Dify.java
  30. 2 0
      src/main/java/com/telerobot/fs/robot/impl/JiutianChat.java
  31. 1 0
      src/main/java/com/telerobot/fs/robot/impl/JiutianWorkflow.java
  32. 115 141
      src/main/java/com/telerobot/fs/robot/impl/LocalLlmChat.java
  33. 19 3
      src/main/java/com/telerobot/fs/robot/impl/LocalNlpChat.java
  34. 2 1
      src/main/java/com/telerobot/fs/robot/impl/MaxKB.java
  35. 1 0
      src/main/java/com/telerobot/fs/service/AsrResultListener.java
  36. 1 0
      src/main/java/com/telerobot/fs/service/InboundDetailService.java
  37. 8 0
      src/main/java/com/telerobot/fs/service/SysService.java
  38. 58 22
      src/main/java/com/telerobot/fs/utils/CommonUtils.java
  39. 11 5
      src/main/java/com/telerobot/fs/utils/FileUtil.java
  40. 27 22
      src/main/java/com/telerobot/fs/utils/FileUtils.java
  41. 2 6
      src/main/java/com/telerobot/fs/utils/OkHttpClientUtil.java
  42. 6 0
      src/main/java/com/telerobot/fs/wshandle/RespStatus.java
  43. 15 3
      src/main/java/com/telerobot/fs/wshandle/SessionManager.java
  44. 10 0
      src/main/java/com/telerobot/fs/wshandle/SwitchChannel.java
  45. 29 17
      src/main/java/com/telerobot/fs/wshandle/impl/CallApi.java
  46. 38 1
      src/main/java/com/telerobot/fs/wshandle/impl/CallListener.java
  47. 8 0
      src/main/java/com/telerobot/fs/wshandle/nettyserver/WebSocketServer.java
  48. 8 4
      src/main/java/com/telerobot/fs/wshandle/nettyserver/WebSocketServerHandler.java
  49. 2 2
      src/main/resources/application-238.properties
  50. 1 1
      src/main/resources/application.properties
  51. 28 3
      src/main/resources/com/telerobot/fs/mybatis/persistence/SysMapper.xml

+ 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 = {

+ 0 - 17
pom.xml

@@ -222,23 +222,6 @@
             <version>8.16</version>
         </dependency>
 
-
-<!--        &lt;!&ndash; FreeSWITCH Java ESL 核心依赖 &ndash;&gt;-->
-<!--        <dependency>-->
-<!--            <groupId>org.freeswitch.esl.client</groupId>-->
-<!--            <artifactId>org.freeswitch.esl.client</artifactId>-->
-<!--            <version>0.9.2</version>-->
-<!--        </dependency>-->
-
-
-        <dependency>
-            <groupId>cn.hutool</groupId>
-            <artifactId>hutool-all</artifactId>
-            <version>5.8.31</version>
-        </dependency>
-
-
-
     </dependencies>
 
     <build>

+ 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  

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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);

+ 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;
 	}

+ 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";
 

+ 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 - 30
src/main/java/com/telerobot/fs/global/CdrPush.java

@@ -5,7 +5,6 @@ import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.po.CdrDetail;
 import com.telerobot.fs.entity.po.CdrEntity;
-import com.telerobot.fs.mybatis.dao.SysDao;
 import com.telerobot.fs.utils.CommonUtils;
 import com.telerobot.fs.utils.DateUtils;
 import com.telerobot.fs.utils.OkHttpClientUtil;
@@ -13,7 +12,6 @@ import com.telerobot.fs.utils.StringUtils;
 import lombok.SneakyThrows;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.annotation.DependsOn;
@@ -26,17 +24,14 @@ import java.util.concurrent.Semaphore;
 @DependsOn("appContextProvider")
 public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
 
-    @Autowired
-    SysDao sysDao;
-
     private static Logger logger =  LoggerFactory.getLogger(CdrPush.class);
     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;
@@ -55,13 +50,6 @@ public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
     private boolean postCdr(CdrDetail cdr){
         try {
             String url = SystemConfig.getValue("post_cdr_url");
-            //如果有自定义回调地址 替换回调地址为自定义回调地址 否则默认回调
-            String callBackUrl = getCallBackUrlByUuid(cdr.getUuid());
-            logger.info("测试日志cdrUUid:{} ,callBackUrlByUuid: {}", cdr.getUuid(), callBackUrl);
-            if(org.apache.commons.lang3.StringUtils.isNotBlank(callBackUrl)){
-                url = callBackUrl;
-            }
-            logger.info("测试日志最后请求的url:{}",url);
             if (StringUtils.isNullOrEmpty(url)) {
                 logger.error("post_cdr_url  has not been configured yet.");
                 return false;
@@ -87,7 +75,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,26 +82,12 @@ 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();
     }
-
-    private String getCallBackUrlByUuid(String uuid){
-        String userBizJson = sysDao.getUserBizJson(uuid);
-        logger.info("测试日志userBizJson:{},查询uuid:{}", userBizJson,uuid);
-        if(org.apache.commons.lang3.StringUtils.isNotBlank(userBizJson)){
-            JSONObject obj = JSONObject.parseObject(userBizJson);
-            if(null != obj && !obj.isEmpty() && obj.containsKey("callBackUrl")){
-                return obj.getString("callBackUrl");
-            }
-        }
-        return null;
-    }
 }

+ 4 - 1
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;
@@ -69,6 +70,8 @@ public interface SysDao {
 
 	List<LlmKb> getKbListByCatId(int catId);
 
-	String getUserBizJson(String uuid);
 
+	int updateExtension(CcExtNum ccExtNum);
+
+	List<CcExtNum> selectAllExtensions();
 }

+ 11 - 2
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;
@@ -137,7 +138,15 @@ public class SysDaoImpl implements SysDao {
 	}
 
 	@Override
-	public String getUserBizJson(String uuid){
-		return mapper.getUserBizJson(uuid);
+	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;
@@ -84,5 +85,8 @@ public interface SysMapper {
 
     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(1500);
             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(),

+ 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);

+ 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()));
             }
         }
 

+ 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 - 22
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);
 	}
@@ -109,24 +142,6 @@ public class CommonUtils<T>  {
         return true;
 	}
 
-	/**
-	 *  校验客户端 http 请求的 token
-	 * @param request
-	 * @return
-	 */
-	public static String validateHttpHeaderToken(HttpServletRequest request,  HttpServletResponse response) {
-		String sysToken = SystemConfig.getValue("call-center-api-token", "");
-		String token = request.getHeader("Authorization");
-		// remove start string: "Bearer "
-		if (!StringUtils.isEmpty(token) && token.length() > 7) {
-			token = token.substring(7);
-		}
-		if (!sysToken.equals(token)) {
-			response.setStatus(400);
-			return "{ \"code\": 400, \"msg\" : \"validate token error.\" }";
-		}
-		return "";
-	}
 
 	public static Map<String, String> parseUrlQueryString(String queryString) throws UnsupportedEncodingException {
 		Map<String, String> queryPairs = new HashMap<>(16);
@@ -287,7 +302,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();

+ 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;
+
     /**
      * 多人电话会议,重复的被叫;
      */

+ 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

+ 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 - 3
src/main/resources/com/telerobot/fs/mybatis/persistence/SysMapper.xml

@@ -222,7 +222,32 @@
 		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>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů