yzx пре 1 месец
родитељ
комит
cb08030614
22 измењених фајлова са 628 додато и 127 уклоњено
  1. 17 0
      pom.xml
  2. 26 19
      src/main/java/com/telerobot/fs/acd/CallHandler.java
  3. 51 31
      src/main/java/com/telerobot/fs/acd/InboundGroupHandler.java
  4. 4 15
      src/main/java/com/telerobot/fs/acd/InboundGroupHandlerList.java
  5. 2 2
      src/main/java/com/telerobot/fs/controller/InboundCallController.java
  6. 68 5
      src/main/java/com/telerobot/fs/controller/ReloadParams.java
  7. 2 2
      src/main/java/com/telerobot/fs/entity/pojo/AgentStatus.java
  8. 21 7
      src/main/java/com/telerobot/fs/global/CdrPush.java
  9. 11 2
      src/main/java/com/telerobot/fs/outbound/batchcall/CallTask.java
  10. 17 13
      src/main/java/com/telerobot/fs/robot/RobotBase.java
  11. 75 17
      src/main/java/com/telerobot/fs/robot/RobotChat.java
  12. 1 1
      src/main/java/com/telerobot/fs/robot/TransferToAgent.java
  13. 21 4
      src/main/java/com/telerobot/fs/robot/impl/DeepSeekChat.java
  14. 246 0
      src/main/java/com/telerobot/fs/robot/impl/TencentChat.java
  15. 6 2
      src/main/java/com/telerobot/fs/utils/OkHttpClientUtil.java
  16. 15 0
      src/main/java/com/telerobot/fs/wshandle/MessageHandlerEngineList.java
  17. 1 0
      src/main/java/com/telerobot/fs/wshandle/SessionManager.java
  18. 11 0
      src/main/java/com/telerobot/fs/wshandle/SwitchChannel.java
  19. 9 0
      src/main/java/com/telerobot/fs/wshandle/impl/AgentCc.java
  20. 21 4
      src/main/java/com/telerobot/fs/wshandle/impl/CallListener.java
  21. 2 2
      src/main/resources/application-238.properties
  22. 1 1
      src/main/resources/application.properties

+ 17 - 0
pom.xml

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

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

@@ -286,12 +286,12 @@ public class CallHandler {
 			CallHandler task = entry.getValue();
 			long currentTime = System.currentTimeMillis();
 			long timePassed = currentTime - task.inboundDetail.getTransferTime() + 200;
-			boolean transferExpired = (task.inboundDetail.getTransferTime() > 0L) && (timePassed > transferAgentTimeOut * 1000);
+			boolean transferTimeout = (task.inboundDetail.getTransferTime() > 0L) && (timePassed > transferAgentTimeOut * 1000);
 			boolean transferredSucceed = task.inboundDetail.getTransferredSucceed();
 			boolean hangup = task.inboundDetail.getHangup();
 			// 通话未挂机且未被应答
 			boolean notAnsweredAndNotHangup = !transferredSucceed && !hangup;
-			boolean callExtensionNoAnswer = task.transferring && transferExpired && notAnsweredAndNotHangup;
+			boolean callExtensionNoAnswer = task.transferring && transferTimeout && notAnsweredAndNotHangup;
 			boolean transferExtensionFailed =  task.transferFailed;
 			if(transferExtensionFailed || callExtensionNoAnswer){
 				log.warn("{} Put the transfer-failed call back to the queue:{}", task.uuid, JSON.toJSONString(task.inboundDetail));
@@ -441,15 +441,20 @@ public class CallHandler {
 				if(null != engine){
                     //发送弹屏消息
 					engine.sendReplyToAgent(new MessageResponse(RespStatus.NEW_INBOUND_CALL, "new inbound call", inboundDetail));
-
-					JSONObject jsonObject = new JSONObject();
-					jsonObject.put("status", AgentStatus.lockStatus.getIndex());
-					jsonObject.put("text", AgentStatus.lockStatus.getText());
-					// 座席置忙
-					engine.sendReplyToAgent(new MessageResponse(RespStatus.STATUS_CHANGED, "status: busy", jsonObject));
                     transferring = true;
 					playOpNumOnTransferring();
 
+					if(inboundDetail.getHangup()){
+						log.warn("{} The customer has hung-up. The transfer request is abandoned.", uuid);
+						MessageHandlerEngineList.sendReplyToAgent(
+								agent.getOpNum(),
+								new  MessageResponse(RespStatus.CALLER_HANGUP, "customer has benn hangup.")
+						);
+						ThreadUtil.sleep(50);
+						resetAgentStatus();
+						return;
+					}
+
 					CallApi callApi = ((CallApi)engine.getMessageHandleByName("call"));
 					if(null != callApi) {
 						String displayNumber = hideInboundNumber ?
@@ -464,6 +469,7 @@ public class CallHandler {
 						customerChannel.setPhoneNumber(inboundDetail.getCaller());
 						customerChannel.setChannelState(ChanneState.BRIDGED);
 						customerChannel.setFlag(ChannelFlag.HOLD_CALL);
+						customerChannel.setInboundDetail(inboundDetail);
 						if(!StringUtils.isNullOrEmpty(satisfSurveyIvrId)){
 							customerChannel.setFlag(ChannelFlag.SATISFACTION_SURVEY_REQUIRED);
 							EslConnectionUtil.sendExecuteCommand(
@@ -491,6 +497,18 @@ public class CallHandler {
 		});
 	}
 
+	private void resetAgentStatus(){
+		if(inboundDetail.getManualAnsweredTime() == 0L && agentSessionEntity != null){
+			// extension not answered, we must reset agent status.
+			AppContextProvider.getBean(SysService.class).resetAgentBusyLockTimeEx(
+					agentSessionEntity.getOpNum(),
+					System.currentTimeMillis() - 5000
+			);
+			AgentCc.setAgentStatus(AgentStatus.free, agentSessionEntity.getOpNum());
+
+		}
+	}
+
 	private void playOpNumOnTransferring(){
 		if (playOpNum) {
 			// 播放为当前通话服务的客服人员工号
@@ -709,17 +727,6 @@ public class CallHandler {
 			}
 		}
 
-		private void resetAgentStatus(){
-			if(inboundDetail.getManualAnsweredTime() == 0L && agentSessionEntity != null){
-				// extension not answered, we must reset agent status.
-				AppContextProvider.getBean(SysService.class).resetAgentBusyLockTimeEx(
-						agentSessionEntity.getOpNum(),
-						System.currentTimeMillis() - 5000
-				);
-				AgentCc.setAgentStatus(AgentStatus.free, agentSessionEntity.getOpNum());
-			}
-		}
-
 		private void breakWaitMusic(){
 			boolean getAllowSignal = getPlayKeepMusicSignal();
 			if(getAllowSignal) {

+ 51 - 31
src/main/java/com/telerobot/fs/acd/InboundGroupHandler.java

@@ -2,11 +2,14 @@ package com.telerobot.fs.acd;
 
 import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.config.AppContextProvider;
+import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.pojo.AgentStatus;
 import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.utils.CommonUtils;
+import com.telerobot.fs.utils.ThreadPoolCreator;
 import com.telerobot.fs.utils.ThreadUtil;
 import com.telerobot.fs.wshandle.*;
+import com.telerobot.fs.wshandle.impl.AgentCc;
 import com.telerobot.fs.wshandle.impl.InboundMonitorDataPull;
 import lombok.SneakyThrows;
 import org.slf4j.Logger;
@@ -15,6 +18,7 @@ import org.slf4j.LoggerFactory;
 import java.util.List;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.ThreadPoolExecutor;
 
 /**
  * Call queue handler class — a handler object is created for each group
@@ -25,7 +29,9 @@ public class InboundGroupHandler {
 
     private final static Logger log = LoggerFactory.getLogger(InboundGroupHandler.class);
 
-    /** 存放当前业务组的外呼电话 **/
+    /**
+	 * store call sessions for current groupId
+	 ***/
 	private ArrayBlockingQueue<CallHandler> inboundCallQueue = new ArrayBlockingQueue<>(600, false);
 
 	private Semaphore semaphore = new Semaphore(0);
@@ -34,6 +40,16 @@ public class InboundGroupHandler {
         return inboundCallQueue.size();
     }
 
+	private static int inboundGroupHandlerThreadPoolSize = Integer.parseInt(
+			SystemConfig.getValue("inbound-group-handler-thread-pool-size", "20")
+	);
+	private  static ThreadPoolExecutor assignCallThreadPool = ThreadPoolCreator.create(
+			inboundGroupHandlerThreadPoolSize,
+			"inbound-call-group-assign-thread",
+			365*24,
+			inboundGroupHandlerThreadPoolSize * 2
+	);
+
 	/**
 	 * Calculate and retrieve the number of people ahead of the current customer in the queue
 	 * @param current
@@ -50,24 +66,25 @@ public class InboundGroupHandler {
 		return count;
 	}
 
-    /** 业务组信息,这里使用学校的固话作为groupId  **/
+    /**
+	 * business groupId
+	 ***/
 	private String groupId = null;
 
-	/** 业务组信息 **/
 	public String getGroupId() {
 		return groupId;
 	}
 
-	/** 根据groupId创建呼出电话处理对象  **/
+
 	public InboundGroupHandler(String _groupId) {
-		this.groupId = _groupId;
+		this.groupId = _groupId.trim();
         activeCallHandler();
 	}
 
 	private boolean disposed = false;
 
     /**
-     * 添加一个话务请求到话务队列中
+     * add a call session to queue
      * @param callDetailInfo
      * @return
      */
@@ -79,7 +96,7 @@ public class InboundGroupHandler {
 			if(!inboundCallQueue.contains(callDetailInfo)) {
 				this.inboundCallQueue.put(callDetailInfo);
 			}else{
-				String errorTips = "严重错误,添加了重复的排队对象到队列中, addCallToQueue(CallHandler)";
+				String errorTips = "error, repeated call session, skip it. addCallToQueue(CallHandler)";
 				log.error("{} {}",  callDetailInfo.getTraceId(), errorTips);
 			}
             semaphore.release();
@@ -91,13 +108,13 @@ public class InboundGroupHandler {
     }
 
 	/**
-	 * 添加一个话务请求到话务队列中
+	 *  add a call session to a queue of specific groupId
 	 * @param callDetailInfo
 	 * @return
 	 */
 	public static boolean addCallToQueue(CallHandler callDetailInfo, String skillGroupId) {
 		InboundGroupHandler groupHandler = InboundGroupHandlerList.getInstance().
-				getCallHandlerBySkillGroupId(skillGroupId);
+				getCallHandlerBySkillGroupId(skillGroupId.trim());
 		assert null != groupHandler;
 		return groupHandler.addCallToQueue(callDetailInfo);
 	}
@@ -105,7 +122,7 @@ public class InboundGroupHandler {
 
 
 	private void assignCall() {
-		log.info("assignCall thread started :" + this.groupId.toString());
+		log.info("assignCall thread started groupId={}.", this.groupId);
 		while (!disposed) {
 			try {
                 semaphore.acquire();
@@ -133,15 +150,17 @@ public class InboundGroupHandler {
 							if (engine != null) {
 								SessionEntity session = engine.getSessionInfo();
 								if (session != null && session.tryLock()) {
-									log.info("{} An free agent is successfully obtained. opnum={}, extnum={}, lastHangupTime={}, agent groupId={}, groupId of call session={}.",
-											call.getTraceId(), agent.getOpNum(), agent.getExtNum(), agent.getLastHangupTime(), agent.getGroupId(), call.getInboundDetail().getGroupId()
+									log.info("{} An free agent is successfully obtained. opnum={}, extnum={}, sessionId={}, lastHangupTime={}, agent groupId={}, groupId of call session={}.",
+											call.getTraceId(), agent.getOpNum(), agent.getExtNum(), agent.getSessionId(),
+											agent.getLastHangupTime(), agent.getGroupId(), call.getInboundDetail().getGroupId()
 									);
-									log.info("{} Set the busy lock status of an agent. uerId={}, extNum={}",
+									log.info("{} Set the busy lock status of an agent. uerId={}, extNum={}, sessionId={}.",
 											call.getTraceId(),
 											agent.getOpNum(),
-											agent.getExtNum()
+											agent.getExtNum(),
+											agent.getSessionId()
 									);
-									AppContextProvider.getBean(SysService.class).setAgentStatusWithBusyLock(agent.getOpNum(), AgentStatus.incall.getIndex());
+									AgentCc.setAgentStatus(AgentStatus.lockStatus, agent.getOpNum());
 									agentFound = agent;
 									break;
 								}
@@ -191,22 +210,23 @@ public class InboundGroupHandler {
 	}
 
     private void activeCallHandler() {
-	    // 启动线程;
-        new Thread(new Runnable() {
-            @Override
-            public void run() {
-                assignCall();
-            }
-        }, "distributeCall").start();
-
-		// 启动线程;
-		new Thread(new Runnable() {
-			@SneakyThrows
-			@Override
-			public void run() {
-				sendAcdQueueInfoToGroup();
-			}
-		}, "sendAcdQueueToGroup").start();
+		assignCallThreadPool.execute(
+				new Runnable() {
+					@Override
+					public void run() {
+						assignCall();
+					}
+				}
+		);
+
+		assignCallThreadPool.execute(
+				new Runnable() {
+					@Override
+					public void run() {
+						sendAcdQueueInfoToGroup();
+					}
+				}
+		);
     }
 
     private volatile boolean sendQueueEmptyInfo = true;

+ 4 - 15
src/main/java/com/telerobot/fs/acd/InboundGroupHandlerList.java

@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
 
 /***
  *  Stores call queue handling objects for all business groups.
@@ -18,21 +19,9 @@ public class InboundGroupHandlerList {
 	public static InboundGroupHandlerList getInstance() {
 		return INSTANCE;
 	}
-	private List<InboundGroupHandler> callHandlerList = new ArrayList<>(30);
+	private ConcurrentHashMap<String, InboundGroupHandler>  callHandlerList= new ConcurrentHashMap<>(20);
 	private InboundGroupHandler findHandlerByGroupId(String groupId) {
-		InboundGroupHandler destHandler = null;
-		for (int i = 0; i <= callHandlerList.size() - 1; i++) {
-			try {
-				InboundGroupHandler myHandler = callHandlerList.get(i);
-				if (myHandler.getGroupId().equals(groupId)) {
-					destHandler = myHandler;
-					break;
-				}
-			} catch (Throwable ex) {
-				log.error(ex.toString());
-			}
-		}
-		return destHandler;
+		 return callHandlerList.get(groupId.trim());
 	}
 	
 	/***
@@ -46,7 +35,7 @@ public class InboundGroupHandlerList {
 				destHandler = findHandlerByGroupId(groupId);
 				if (destHandler == null) {
 					destHandler = new InboundGroupHandler(groupId);
-					this.callHandlerList.add(destHandler);
+					this.callHandlerList.put(groupId.trim(),  destHandler);
 				}
 			}
 		}

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

@@ -106,8 +106,8 @@ public class InboundCallController {
 				mainThreadPool.getCompletedTaskCount(),
 				mainThreadPool.getCorePoolSize()
 		);
-		logger.info("RECV NEW INBOUND CALL, uuid:{}, caller:{}, mediaPort:{}, recordTime: {}, remoteVideoPort:{}",
-				uuid, caller, mediaPort, loadTestUuid, remoteVideoPort);
+		logger.info("RECV NEW INBOUND CALL, uuid:{}, caller:{}, callee:{}, mediaPort:{}, recordTime: {}, remoteVideoPort:{}",
+				uuid, caller, callee,mediaPort, loadTestUuid, remoteVideoPort);
 		logger.info("uuid: {}, currentThreadPoolInfo: {}", uuid, currentThreadPoolInfo);
 		int maxPoolSize =  mainThreadPool.getCorePoolSize();
 		if(mainThreadPool.getActiveCount() >=  maxPoolSize){

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

@@ -6,6 +6,7 @@ import com.auth0.jwt.algorithms.Algorithm;
 import com.telerobot.fs.config.AppContextProvider;
 import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.dto.FreeswitchNodeInfo;
+import com.telerobot.fs.entity.pojo.AgentStatus;
 import com.telerobot.fs.service.CallTaskService;
 import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.tts.aliyun.CosyVoiceDemo;
@@ -13,12 +14,18 @@ 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.SessionEntity;
+import com.telerobot.fs.wshandle.impl.AgentCc;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 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;
 import java.util.*;
@@ -206,15 +213,71 @@ public class ReloadParams {
 		return "success";
 	}
 
-	@RequestMapping("/getAgentBusyStatusSubList")
+	@RequestMapping("/getAgent")
 	@ResponseBody
-	public String getAgentBusyStatusSubList(HttpServletRequest request,Map<String,Object> model) throws InstantiationException, IllegalAccessException, InterruptedException {
-		String list = SystemConfig.getValue("agent-busy-status", "");
+	public String getAgentStatus(HttpServletRequest request,Map<String,Object> model) throws InstantiationException, IllegalAccessException, InterruptedException {
+		String clientIP = request.getRemoteAddr();
+		if(!"127.0.0.1".equalsIgnoreCase(clientIP)){
+			return  "forbidden, only 127.0.0.1 allowed.";
+		}
+		String groupId = request.getParameter("groupId");
 
-		if(!StringUtils.isNullOrEmpty(list)){
-			String[] tmpArray = list.split("\\|");
+		List<SessionEntity> agentList = AppContextProvider.getBean(SysService.class).getFreeUserList(groupId);
+		int agentNum =  agentList.size();
+		if(agentNum > 0) {
+			SessionEntity agentFound = null;
+			for (SessionEntity agent : agentList) {
+				MessageHandlerEngine engine = MessageHandlerEngineList.
+						getInstance().getMsgHandlerEngine(agent.getSessionId());
+				if (engine != null) {
+					SessionEntity session = engine.getSessionInfo();
+					if (session != null && session.tryLock()) {
+						logger.info(" An free agent is successfully obtained. opnum={}, extnum={}, lastHangupTime={}, agent groupId={}.",
+								agent.getOpNum(), agent.getExtNum(), agent.getLastHangupTime(), agent.getGroupId()
+						);
+						logger.info("Set the busy lock status of an agent. uerId={}, extNum={}",
+								agent.getOpNum(),
+								agent.getExtNum()
+						);
+						AgentCc.setAgentStatus(AgentStatus.lockStatus, agent.getOpNum());
+						agentFound = agent;
+						break;
+					}
+				}else{
+					return "No acd agents are online.";
+				}
+			}
+			if(agentFound != null){
+				return "Get a free agent successfully. " + JSON.toJSONString(agentFound);
+			}else{
+				return "No free acd agents.";
+			}
+		}
 
+		return "success";
+	}
+
+	@RequestMapping("/tryLockAgent")
+	@ResponseBody
+	public String tryLockAgent(HttpServletRequest request,Map<String,Object> model) throws InstantiationException, IllegalAccessException, InterruptedException {
+		String clientIP = request.getRemoteAddr();
+		if (!"127.0.0.1".equalsIgnoreCase(clientIP)) {
+			return "forbidden, only 127.0.0.1 allowed.";
 		}
+		String opNum = request.getParameter("opnum");
+		MessageHandlerEngine engine = MessageHandlerEngineList.
+				getInstance().getMsgHandlerEngineByOpNum(opNum);
+
+		if (engine != null) {
+			SessionEntity session = engine.getSessionInfo();
+			if (session != null && session.tryLock()) {
+				AgentCc.setAgentStatus(AgentStatus.lockStatus, session.getOpNum());
+				return "Lock agent successfully. " +  JSON.toJSONString(session);
+			} else {
+				return "tryLock acd agent failed.";
+			}
+		}
+
 		return "success";
 	}
 }

+ 2 - 2
src/main/java/com/telerobot/fs/entity/pojo/AgentStatus.java

@@ -55,7 +55,7 @@ public enum AgentStatus {
     /**
      * 坐席预占(呼入来电锁定)
      */
-    lockStatus("lockStatus", "预占", 4);
+    lockStatus("lockStatus", "预占", 7);
 
 
 
@@ -67,7 +67,7 @@ public enum AgentStatus {
     /**
      *  状态描述
      */
-    private String text;
+    private String text = "";
 
 
     /**

+ 21 - 7
src/main/java/com/telerobot/fs/global/CdrPush.java

@@ -27,34 +27,46 @@ public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
     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"));
+    }
 
-
-    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;
+         }
          if(cdrQueue.add(cdr)){
              semaphore.release(1);
              return true;
+         }else{
+             logger.error("{} cdr-push queue is full. Cant not process new requests. cdr json={}",
+                     cdr.getUuid(), JSON.toJSONString(cdr)
+             );
          }
          return  false;
     }
 
-    private void postCdr(CdrDetail cdr){
+    private boolean postCdr(CdrDetail cdr){
         try {
             String url = SystemConfig.getValue("post_cdr_url");
             if (StringUtils.isNullOrEmpty(url)) {
-                logger.error("没有配置业务系统接收话单的参数 post_cdr_url.");
-                return;
+                logger.error("post_cdr_url  has not been configured yet.");
+                return false;
             }
             String cdrData = JSON.toJSONString(cdr);
             String response = OkHttpClientUtil.postCdr(url, cdrData);
             logger.info("{} postCdr: {}, request url {} , response: {}", cdr.getUuid(),  cdrData, url, response);
             if ("success".equalsIgnoreCase(response)) {
                 logger.info("{} post cdr succeed.", cdr.getUuid());
+                return true;
             } else {
                 logger.error("{} post cdr failed: cdr data={}", cdr.getUuid(), cdrData);
             }
         }catch (Throwable e){
-            logger.error("postCdr error: {} {}", e.toString(), CommonUtils.getStackTraceString(e.getStackTrace()));
+            logger.error("postCdr failed: {} {}", e.toString(), CommonUtils.getStackTraceString(e.getStackTrace()));
         }
+        return false;
     }
 
     @Override
@@ -69,7 +81,9 @@ public class CdrPush implements ApplicationListener<ApplicationReadyEvent> {
                            semaphore.acquire();
                            CdrDetail cdrDetail = cdrQueue.poll();
                            if (null != cdrDetail) {
-                               postCdr(cdrDetail);
+                               if(!postCdr(cdrDetail)){
+                                   addCdrToQueue(cdrDetail);
+                               }
                            }
                            Thread.sleep(10);
                        }

+ 11 - 2
src/main/java/com/telerobot/fs/outbound/batchcall/CallTask.java

@@ -12,6 +12,8 @@ import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.dao.CallTaskEntity;
 import com.telerobot.fs.entity.dao.CustmInfoEntity;
 import com.telerobot.fs.entity.dto.EmptyNumberDetectionConfig;
+import com.telerobot.fs.entity.po.CdrDetail;
+import com.telerobot.fs.global.CdrPush;
 import com.telerobot.fs.ivr.IvrEngine;
 import com.telerobot.fs.outbound.CallConfig;
 import com.telerobot.fs.robot.RobotChat;
@@ -357,7 +359,7 @@ public class CallTask implements Runnable {
 				if (phoneInfo.getConnectedTime() > 0L) {
 					long callDuring = eventTime - phoneInfo.getConnectedTime();
 					phoneInfo.setTimeLen((int) (callDuring));
-				}else{
+				} else {
 					log.info("{} try to save emptyNumberDetectionText.", getTraceId());
 					setRecognitionText();
 					saveEmptyNumberDetection(emptyNumberDetectionText.toString());
@@ -377,13 +379,20 @@ public class CallTask implements Runnable {
 					}
 				}
 
-				if(emptyNumberDetectionCode > 0){
+				if (emptyNumberDetectionCode > 0) {
 					phoneInfo.setCallstatus(emptyNumberDetectionCode);
 				}
 
 				phoneInfo.setCallEndTime(System.currentTimeMillis());
 				phoneInfo.setHangupCause(hangupCause);
 				ScheduledScanTask.addToSQLQueue(phoneInfo);
+
+				// push cdr
+				CdrDetail cdrRecord = new CdrDetail();
+				cdrRecord.setUuid(phoneInfo.getUuid());
+				cdrRecord.setCdrType("outbound");
+				cdrRecord.setCdrBody(JSON.toJSONString(phoneInfo));
+				CdrPush.addCdrToQueue(cdrRecord);
 			} else if (EventNames.CHANNEL_PROGRESS_MEDIA.equalsIgnoreCase(eventName)) {
 				log.info("{} Ringing event received: {}", getTraceId(), eventName);
 				// empty-number-detection-enabled

+ 17 - 13
src/main/java/com/telerobot/fs/robot/RobotBase.java

@@ -71,11 +71,15 @@ public abstract class RobotBase implements IEslEventListener {
              playBackFinishedSignal.release();
          }
     }
-    protected void waitForPlayBackFinished(){
+    protected void waitForPlayBackFinished(int...waiTimeoutMills){
         try {
             playBackFinishedSignalReleased = false;
+            int waitMills = 181000;
+            if(waiTimeoutMills.length > 0){
+                waitMills = waiTimeoutMills[0];
+            }
             try {
-                playBackFinishedSignal.tryAcquire(1, 181000, TimeUnit.MILLISECONDS);
+                playBackFinishedSignal.tryAcquire(1, waitMills, TimeUnit.MILLISECONDS);
             }catch (Throwable e){
             }
         }catch (Throwable e){
@@ -117,6 +121,8 @@ public abstract class RobotBase implements IEslEventListener {
      */
     protected volatile boolean transferToAgent = false;
 
+    protected volatile boolean transferToAgentExecuted = false;
+
     /**
      *  动态切换语音识别方式;
      */
@@ -181,7 +187,7 @@ public abstract class RobotBase implements IEslEventListener {
                    throwable.toString(),
                    CommonUtils.getStackTraceString(throwable.getStackTrace())  );
            CommonUtils.setHangupCauseDetail(callDetail, HangupCause.SYSTEM_INTERNAL_ERROR, "details:" + errorMsg );
-           hangupAndCloseConn();
+           hangupAndCloseConn("cant-not-create-chatRobot-object");
         }
     }
 
@@ -369,15 +375,13 @@ public abstract class RobotBase implements IEslEventListener {
     /**
      *  发送挂机指令; 释放计数器; 关闭Esl连接;
      */
-    public void hangupAndCloseConn(boolean...killcall){
-        if(killcall.length == 0) {
-            EslConnectionUtil.sendExecuteCommand(
-                    "hangup",
-                    "callCenter-mandatory-hangup",
-                    this.uuid,
-                    this.eslConnectionPool
-            );
-        }
+    public void hangupAndCloseConn(String reason) {
+        EslConnectionUtil.sendExecuteCommand(
+                "hangup",
+                reason,
+                this.uuid,
+                this.eslConnectionPool
+        );
         releaseThreadNum();
     }
 
@@ -515,7 +519,7 @@ public abstract class RobotBase implements IEslEventListener {
                 iterator.remove();
                 if (task.uuid.length() != 0) {
                     logger.info("{} The call is abnormal, The customer has not spoken for a long time and is about to end the call.", task.uuid);
-                    task.hangupAndCloseConn();
+                    task.hangupAndCloseConn("customer-has-not-spoken-for-a-long-time");
                 }
                 task.processFsMsg(task.generateHangupEvent("doMonitor-Longtime-No-Speak"));
             }

+ 75 - 17
src/main/java/com/telerobot/fs/robot/RobotChat.java

@@ -117,7 +117,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(1500);
+            ThreadUtil.sleep(200);
             if (isHangup) {
                 return;
             }
@@ -135,7 +135,7 @@ public class RobotChat extends RobotBase {
                          HangupCause.TTS_ACCOUNT_INFO_INCORRECT,
                          "error msg:" + errMsg
                  );
-                hangupAndCloseConn();
+                hangupAndCloseConn("AliyunTTSWebApi-getToken-error");
                 return;
             }
         }
@@ -224,7 +224,7 @@ public class RobotChat extends RobotBase {
             if(recvHangupSignal){
                  logger.info("{} The hang signal was received in the previous interaction process, and the call is about to hang up.",
                          getTraceId());
-                hangupAndCloseConn();
+                hangupAndCloseConn("recvHangupSignal");
              }
         }else if (EventNames.DTMF.equalsIgnoreCase(eventName)) {
             // get the dtmf key to check if its value is the same as
@@ -234,8 +234,12 @@ public class RobotChat extends RobotBase {
              String transferManualDigit = chatRobot.getAccount().transferManualDigit;
              if(transferManualDigit.equalsIgnoreCase(digit)){
                  logger.info("{} DTMF digit equals transferManualDigit.", getTraceId());
-                 transferToAgent = true;
+
                  if (recvPlayBackEndEvent || getAllowInterrupt()) {
+
+                     if(!setTransferState()){
+                         return;
+                     }
                      logger.info("{} The digit-key during call have successfully activated the condition " +
                                         "for transferring to a human operator. recvPlayBackEndEvent={}, getAllowInterrupt()={} ",
                                  getTraceId(), recvPlayBackEndEvent, getAllowInterrupt()
@@ -244,6 +248,22 @@ public class RobotChat extends RobotBase {
                          interruptRobotSpeech();
                          releasePlayBackFinishedSignal();
                          ThreadUtil.sleep(100);
+                         // wait for tts closed
+                         int step = 50;
+                         int maxWaitMills = 2000;
+                         int counter = 0;
+                         logger.info("{} wait for tts channel closed.", getTraceId());
+                         while (!ttsChannelClosed && !isHangup && counter <= maxWaitMills) {
+                             ThreadUtil.sleep(step);
+                             counter += step;
+                         }
+                         if(!ttsChannelClosed){
+                             ttsChannelClosed = true;
+                             chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
+                             logger.warn("{}  We haven't received the event of the TTS channel being closed within two seconds, we consider it to have been closed.  .", getTraceId());
+                         }else{
+                             logger.info("{}  tts channel is closed.", getTraceId());
+                         }
                      }
 
                      releaseSignal();
@@ -303,6 +323,12 @@ public class RobotChat extends RobotBase {
                logger.info("{}  TtsChannelClosed = true.", getTraceId());
                ttsChannelClosed = true;
            }
+           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);
+            }
 
             if ("NetworkError".equalsIgnoreCase(event)) {
                 CommonUtils.setHangupCauseDetail(
@@ -310,7 +336,8 @@ public class RobotChat extends RobotBase {
                         HangupCause.TTS_SERVER_CONNECTED_FAILED,
                         headers.get("Error-Details")
                 );
-                hangupAndCloseConn();
+                logger.info("{} recv NetworkError event, hangup call session.", getTraceId());
+                hangupAndCloseConn("Asr-TTs-NetworkError");
             }
         }
         else if ("CUSTOM".equalsIgnoreCase(eventName) && (
@@ -328,15 +355,15 @@ public class RobotChat extends RobotBase {
                         HangupCause.ASR_SERVER_CONNECTED_FAILED,
                         asrResponse
                 );
-                hangupAndCloseConn();
+                hangupAndCloseConn("Asr-Tts-NetworkError");
                 return;
             }
 
 
             lastTalkTime = System.currentTimeMillis();
 
-            if (isHangup || interactiveParam.checkInHangupState()) {
-                logger.info("{} Session is going to be hangup, drop asr result: {}", getTraceId(), asrResponse);
+            if (isHangup || interactiveParam.checkInHangupState() ||  transferToAgent) {
+                logger.info("{} Session is going to be hangup or is already being transferred to human operator, drop asr result: {}", getTraceId(), asrResponse);
                 return;
             }
 
@@ -407,6 +434,17 @@ public class RobotChat extends RobotBase {
         }
     }
 
+    private boolean setTransferState() {
+        synchronized (uuid.intern()) {
+            if (transferToAgent) {
+                logger.info("{} transferring to a human operator is already being handled. skip...", getTraceId());
+                return false;
+            }
+            transferToAgent = true;
+            return true;
+        }
+    }
+
     protected void interruptRobotSpeech(){
         logger.info("{} send uuid_break command to FreeSWITCH.", uuid);
         EslConnectionUtil.sendSyncApiCommand("uuid_break", uuid + " all");
@@ -542,6 +580,9 @@ public class RobotChat extends RobotBase {
      * interactWithRobot
      **/
     private void interactWithRobot() {
+        if (checkCallSession()) {
+            return;
+        }
         interactiveParam.setAllowInterrupt(0);
         recvPlayBackEndEvent = false;
         firstSpeak = false;
@@ -590,6 +631,9 @@ public class RobotChat extends RobotBase {
                     logger.error("{} llm api error, retry to send question to chatRobot: {}", getTraceId(), question);
                     aiphoneRes = chatRobot.talkWithAiAgent(question, kbQueryExecuted);
                     Llm_max_try_counter.incrementAndGet();
+                    if (checkCallSession()) {
+                        return;
+                    }
                 }
                 Llm_max_try_counter.set(0);
                 kbQueryExecuted = false;
@@ -605,7 +649,7 @@ public class RobotChat extends RobotBase {
                                 HangupCause.LLM_API_SERVER_ERROR,
                                 String.format("The large model failed to access successfully despite more than %d connection attempts.", LLM_MAX_TRY)
                         );
-                        hangupAndCloseConn();
+                        hangupAndCloseConn("reach-llm-max-try-error");
                         return;
                     }
                 }
@@ -652,7 +696,14 @@ public class RobotChat extends RobotBase {
                         }
                     }
 
+                    if (checkCallSession()) {
+                        return;
+                    }
+
                     if (aiphoneRes.getTransferToAgent() == 1) {
+                        if(!setTransferState()){
+                            return;
+                        }
                         doTransferToManualAgent(body);
                         return;
                     }
@@ -662,11 +713,15 @@ public class RobotChat extends RobotBase {
                             chatRobot.sendTtsRequest(chatRobot.getAccount().hangupTips);
                         }
                         chatRobot.closeTts();
-                        acquire(9000);
+                        waitForPlayBackFinished(11000);
+                        long startTimeTick = System.currentTimeMillis();
                         while (!ttsChannelClosed && !isHangup) {
                             ThreadUtil.sleep(1000);
+                            if(System.currentTimeMillis() - startTimeTick > 11000){
+                                break;
+                            }
                         }
-                        hangupAndCloseConn();
+                        hangupAndCloseConn("system-hangup");
                         return;
                     }
                 }
@@ -680,7 +735,7 @@ public class RobotChat extends RobotBase {
                         HangupCause.SYSTEM_INTERNAL_ERROR,
                         String.format("server error: %s", e.toString())
                 );
-                hangupAndCloseConn();
+                hangupAndCloseConn(HangupCause.SYSTEM_INTERNAL_ERROR.getCode());
                 return;
             }
 
@@ -711,17 +766,20 @@ public class RobotChat extends RobotBase {
         }
     }
 
-    private void doTransferToManualAgent(String audioTipsText){
-        transferToAgent = true;
+    private synchronized void doTransferToManualAgent(String audioTipsText){
+        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 = "";
         if(!StringUtils.isEmpty(audioTipsText) && audioTipsText.contains(LlmToolRequest.TRANSFER_TO_TEL)){
             if(!TransferToAgent.TRANSFER_TO_GATEWAY.equalsIgnoreCase(chatRobot.getAccount().aiTransferType)){
                 logger.error("{} instruction `{}`  is only applicable when an external gateway is used to transfer to a manual agent.",
                         uuid, LlmToolRequest.TRANSFER_TO_TEL);
-                hangupAndCloseConn();
+                hangupAndCloseConn("llm-instruction-error");
                 return;
             }
             List<String> matches = RegExp.GetMatchFromStringByRegExp(audioTipsText, LlmToolRequest.TRANSFER_TO_TEL_REGEXP);
@@ -744,7 +802,7 @@ public class RobotChat extends RobotBase {
             logger.info("{} Try to play tts  transferToAgentTips {}", getTraceId(), tips);
             chatRobot.sendTtsRequest(tips);
             chatRobot.closeTts();
-            waitForPlayBackFinished();
+            waitForPlayBackFinished(6000);
             // wait for tips playback finished
         }
 

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

@@ -88,7 +88,7 @@ public class TransferToAgent {
             callDetail.setGroupId(groupId);
             CallHandler callHandler = new CallHandler(callDetail);
             callHandler.setSatisfSurveyIvrId(satisfSurveyIvrId);
-            if (InboundGroupHandler.addCallToQueue(callHandler, callDetail.getGroupId())) {
+            if (InboundGroupHandler.addCallToQueue(callHandler, groupId)) {
                 logger.info("{} Successfully add call to acd queue, groupId={}.", callDetail.getUuid(), callDetail.getGroupId());
             }
         }else  if(transferType.equalsIgnoreCase(TRANSFER_TO_GATEWAY)) {

+ 21 - 4
src/main/java/com/telerobot/fs/robot/impl/DeepSeekChat.java

@@ -1,5 +1,6 @@
 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;
@@ -12,10 +13,7 @@ import com.telerobot.fs.entity.pojo.LlmToolRequest;
 import com.telerobot.fs.robot.AbstractChatRobot;
 import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.utils.CommonUtils;
-import okhttp3.MediaType;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
+import okhttp3.*;
 import okio.BufferedSource;
 import org.apache.commons.lang.StringUtils;
 import org.apache.http.HttpStatus;
@@ -132,7 +130,26 @@ 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(),

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

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

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

@@ -78,9 +78,13 @@ public class OkHttpClientUtil {
      */
     public static String postCdr(String url, String data){
 
-        RequestBody requestBody = new FormBody.Builder().add("cdr", data).build();;
+        RequestBody requestBody = RequestBody.create(
+                MediaType.parse("application/json; charset=utf-8"),
+                data
+        );
+//        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();

+ 15 - 0
src/main/java/com/telerobot/fs/wshandle/MessageHandlerEngineList.java

@@ -6,6 +6,7 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import com.telerobot.fs.utils.StringUtils;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -87,6 +88,20 @@ public class MessageHandlerEngineList {
 		return null;
 	}
 
+	/**
+	 *  send ws message to specific acd agent
+	 * @param opNum user code of acd agent
+	 * @param msg
+	 */
+	public static boolean sendReplyToAgent(String opNum, MessageResponse msg) {
+		MessageHandlerEngine engine = getInstance().getMsgHandlerEngineByOpNum(opNum);
+		if(engine != null){
+			engine.sendReplyToAgent(msg);
+			return true;
+		}
+		return  false;
+	}
+
 	/**
 	 * 通过分机号extNum获取MessageHandlerEngine对象
 	 *

+ 1 - 0
src/main/java/com/telerobot/fs/wshandle/SessionManager.java

@@ -201,6 +201,7 @@ public class SessionManager {
 							getMsgHandlerEngineByOpNum(session.getOpNum());
 					if (null != engine) {
 						if (engine.getSessionInfo() != null) {
+							logger.info("unLock acd agent extNum={}, opNum={}.", session.getExtNum(), session.getOpNum());
 							engine.getSessionInfo().unLock();
 						}
 					}

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

@@ -3,6 +3,7 @@ package com.telerobot.fs.wshandle;
 import com.telerobot.fs.config.CallConfig;
 import com.telerobot.fs.entity.bo.ChanneState;
 import com.telerobot.fs.entity.bo.ChannelFlag;
+import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.dto.CallMonitorInfo;
 import com.telerobot.fs.entity.dto.GatewayConfig;
 import com.telerobot.fs.wshandle.impl.*;
@@ -84,6 +85,8 @@ public class SwitchChannel {
 
     private volatile GatewayConfig gatewayConfig = null;
 
+    private volatile InboundDetail inboundDetail = null;
+
     /**
      * 挂机的sip状态码
      */
@@ -253,6 +256,14 @@ public class SwitchChannel {
         this.gatewayConfig = gatewayConfig;
     }
 
+    public InboundDetail getInboundDetail() {
+        return inboundDetail;
+    }
+
+    public void setInboundDetail(InboundDetail inboundDetail) {
+        this.inboundDetail = inboundDetail;
+    }
+
     /**
      *  获取完整的录音/录像的路径;
      * @return

+ 9 - 0
src/main/java/com/telerobot/fs/wshandle/impl/AgentCc.java

@@ -64,10 +64,17 @@ public class AgentCc extends MsgHandlerBase {
      */
     protected void setStatus(int status){
         int affectRow = sysService.setAgentStatus(getSessionInfo().getOpNum(), status);
+        String sessionId = getSessionInfo().getSessionId();
+        logger.info("try to set acd agent status={}, extNum={}, opNum={}, sessionId={}.",
+                status, getSessionInfo().getExtNum(), getSessionInfo().getOpNum(), sessionId);
         if(status == AgentStatus.free.getIndex()) {
             this.getSessionInfo().unLock();
+            logger.info("unLock acd agent extNum={}, opNum={}, sessionId={}.",
+                    getSessionInfo().getExtNum(), getSessionInfo().getOpNum(), sessionId);
         }
         if (affectRow == 1) {
+            logger.info("successfully set acd agent status={}, extNum={}, opNum={}, sessionId={}.",
+                    status, getSessionInfo().getExtNum(), getSessionInfo().getOpNum(), sessionId);
             AgentStatus agentStatus = AgentStatus.getItemByValue(status);
             String description = "cant not get description";
             if (null != agentStatus) {
@@ -78,6 +85,8 @@ public class AgentCc extends MsgHandlerBase {
             jsonObject.put("text", agentStatus.getText());
             sendReplyToAgent(new MessageResponse(RespStatus.STATUS_CHANGED, "agent status: " + description, jsonObject));
         } else {
+            logger.info("Failed to set acd agent status={}, extNum={}, opNum={}, sessionId={}.",
+                    status, getSessionInfo().getExtNum(), getSessionInfo().getOpNum(), sessionId);
             sendReplyToAgent(new MessageResponse(RespStatus.SERVER_ERROR, "update agent status error."));
         }
     }

+ 21 - 4
src/main/java/com/telerobot/fs/wshandle/impl/CallListener.java

@@ -123,8 +123,7 @@ public class CallListener implements IEslEventListener {
 		String uniqueId = headers.get("Unique-ID");
 		String eventName = headers.get("Event-Name");
 
-		if (EventNames.CHANNEL_ANSWER.equalsIgnoreCase(eventName)) {
-
+        if (EventNames.CHANNEL_ANSWER.equalsIgnoreCase(eventName)) {
 			logger.info("{} recv CHANNEL_ANSWER event.  uniqueId={}", getTraceId(), uniqueId);
 			if (uniqueId.equalsIgnoreCase(customerChannel.getUuid())) {
 				customerChannel.setAnsweredTime(System.currentTimeMillis());
@@ -167,6 +166,9 @@ public class CallListener implements IEslEventListener {
 						// audio call convert to video call scenario.
 						jsonObject.put("callType", agentChannel.getCallType());
 					}
+
+					AgentCc.setAgentStatus(AgentStatus.incall, callApiObject.getSessionInfo().getOpNum());
+                    ThreadUtil.sleep(10);
 					logger.info("{} agentChannel is connected,call confirmed.", getTraceId());
 					callApiObject.sendReplyToAgent(
 							new MessageResponse(RespStatus.CALLER_ANSWERED, "call connected.", jsonObject)
@@ -291,6 +293,8 @@ public class CallListener implements IEslEventListener {
 					agentChannel.getRecvMediaHook().onRecvMedia(headers, getTraceId());
 					agentChannel.setRecvMediaHook(null);
 				}
+
+                checkInboundCustomerCallSession();
 			}
 
 		} else
@@ -321,8 +325,10 @@ public class CallListener implements IEslEventListener {
 					agentChannel.setBridgeCallAfterPark(false);
 					customerChannel.setChannelState(ChanneState.PARKED);
 					agentChannel.setChannelState(ChanneState.PARKED);
-					logger.info("{} onPark event occurred, try to bridge call ... ", getTraceId());
-					callApiObject.bridgeCall(agentChannel, customerChannel);
+					if(checkInboundCustomerCallSession()) {
+						logger.info("{} onPark event occurred, try to bridge call ... ", getTraceId());
+						callApiObject.bridgeCall(agentChannel, customerChannel);
+					}
 				}
 
 				if(agentChannel.getParkHook() != null) {
@@ -333,6 +339,17 @@ public class CallListener implements IEslEventListener {
 		}
 	}
 
+	private boolean checkInboundCustomerCallSession() {
+		if(customerChannel.getInboundDetail() != null){
+			if(customerChannel.getInboundDetail().getHangup()){
+				logger.warn("{} The customer has hung-up. The transfer request is abandoned.", customerChannel.getUuid());
+				EslConnectionUtil.sendExecuteCommand("hangup", "Customer-Hangup", agentChannel.getUuid());
+				return false;
+			}
+		}
+		return true;
+	}
+
 	@Override
 	public void backgroundJobResultReceived(String addr, EslEvent event) {
 		EslConnectionUtil.getDefaultEslConnectionPool().getDefaultEslConn().removeListener(this.backgroundJobUuid);

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

@@ -1,6 +1,6 @@
-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.url=jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
 spring.datasource.username=root
-spring.datasource.password=tydic202x888
+spring.datasource.password=easycallcenter365
 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=238
+spring.profiles.active=pro