yzx 1 день назад
Родитель
Сommit
857bcaa1e4

+ 3 - 0
.vscode/settings.json

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

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

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

+ 18 - 0
firewalld的说明.txt

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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