对话接口 ws改造
This commit is contained in:
@@ -93,4 +93,23 @@ easy-es:
|
|||||||
db-config:
|
db-config:
|
||||||
refresh-policy: immediate
|
refresh-policy: immediate
|
||||||
username: elastic
|
username: elastic
|
||||||
password: zkr2024@@.com
|
password: zkr2024@@.com
|
||||||
|
|
||||||
|
chat:
|
||||||
|
baseUrl: http://127.0.0.1:8082
|
||||||
|
chatUrl: /v1/chat/completions
|
||||||
|
chatDetailUrl: /core/chat/getPaginationRecords
|
||||||
|
chatHistoryUrl: /core/chat/getHistories
|
||||||
|
updateNameUrl: /core/chat/updateHistory
|
||||||
|
stickChatUrl: /core/chat/updateHistory
|
||||||
|
delChatUrl: /core/chat/delHistory
|
||||||
|
delAllChatUrl: /core/chat/clearHistories
|
||||||
|
guestUrl: /v1/chat/completions
|
||||||
|
praiseUrl: /core/chat/feedback/updateUserFeedback
|
||||||
|
appId: 67cd49095e947ae0ca7fadd8
|
||||||
|
apiKey: fastgpt-qMl63276wPZvKAxEkW77bur0sSJpmuC6Ngg9lzyEjufLhsBAurjT55j
|
||||||
|
model: qd-job-turbo
|
||||||
|
|
||||||
|
audioText:
|
||||||
|
asr: http://39.98.44.136:8001/asr/file
|
||||||
|
tts: http://39.98.44.136:19527/synthesize
|
||||||
@@ -6,7 +6,7 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:highgo://192.168.0.13:5866/highgo?useUnicode=true&characterEncoding=utf8¤tSchema=shz&stringtype=unspecified
|
url: jdbc:highgo://39.98.44.136:6022/highgo?useUnicode=true&characterEncoding=utf8¤tSchema=shz&stringtype=unspecified
|
||||||
#username: syssso
|
#username: syssso
|
||||||
username: sysdba
|
username: sysdba
|
||||||
password: SHZ2025@comzkr2
|
password: SHZ2025@comzkr2
|
||||||
@@ -63,9 +63,9 @@ spring:
|
|||||||
multi-statement-allow: true
|
multi-statement-allow: true
|
||||||
redis:
|
redis:
|
||||||
# 地址
|
# 地址
|
||||||
host: 127.0.0.1
|
host: 39.98.44.136
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 6379
|
port: 6018
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 0
|
database: 0
|
||||||
# 密码
|
# 密码
|
||||||
@@ -87,10 +87,30 @@ spring:
|
|||||||
easy-es:
|
easy-es:
|
||||||
enable: true
|
enable: true
|
||||||
banner: false
|
banner: false
|
||||||
address: 127.0.0.1:9200
|
address: 39.98.44.136:6023
|
||||||
global-config:
|
global-config:
|
||||||
process-index-mode: manual
|
process-index-mode: manual
|
||||||
db-config:
|
db-config:
|
||||||
refresh-policy: immediate
|
refresh-policy: immediate
|
||||||
username: elastic
|
username: elastic
|
||||||
password: shz2025@@.com
|
password: shz2025@@.com
|
||||||
|
|
||||||
|
#ai
|
||||||
|
chat:
|
||||||
|
baseUrl: http://39.98.44.136:8082
|
||||||
|
chatUrl: /v1/chat/completions
|
||||||
|
chatDetailUrl: /core/chat/getPaginationRecords
|
||||||
|
chatHistoryUrl: /core/chat/getHistories
|
||||||
|
updateNameUrl: /core/chat/updateHistory
|
||||||
|
stickChatUrl: /core/chat/updateHistory
|
||||||
|
delChatUrl: /core/chat/delHistory
|
||||||
|
delAllChatUrl: /core/chat/clearHistories
|
||||||
|
guestUrl: /v1/chat/completions
|
||||||
|
praiseUrl: /core/chat/feedback/updateUserFeedback
|
||||||
|
appId: 67cd49095e947ae0ca7fadd8
|
||||||
|
apiKey: fastgpt-qMl63276wPZvKAxEkW77bur0sSJpmuC6Ngg9lzyEjufLhsBAurjT55j
|
||||||
|
model: qd-job-turbo
|
||||||
|
|
||||||
|
audioText:
|
||||||
|
asr: http://39.98.44.136:8001/asr/file
|
||||||
|
tts: http://39.98.44.136:19527/synthesize
|
||||||
@@ -185,22 +185,4 @@ oauth:
|
|||||||
read-timeout: 30
|
read-timeout: 30
|
||||||
write-timeout: 30
|
write-timeout: 30
|
||||||
|
|
||||||
#ai
|
|
||||||
chat:
|
|
||||||
baseUrl: http://127.0.0.1:8082
|
|
||||||
chatUrl: /v1/chat/completions
|
|
||||||
chatDetailUrl: /core/chat/getPaginationRecords
|
|
||||||
chatHistoryUrl: /core/chat/getHistories
|
|
||||||
updateNameUrl: /core/chat/updateHistory
|
|
||||||
stickChatUrl: /core/chat/updateHistory
|
|
||||||
delChatUrl: /core/chat/delHistory
|
|
||||||
delAllChatUrl: /core/chat/clearHistories
|
|
||||||
guestUrl: /v1/chat/completions
|
|
||||||
praiseUrl: /core/chat/feedback/updateUserFeedback
|
|
||||||
appId: 67cd49095e947ae0ca7fadd8
|
|
||||||
apiKey: fastgpt-qMl63276wPZvKAxEkW77bur0sSJpmuC6Ngg9lzyEjufLhsBAurjT55j
|
|
||||||
model: qd-job-turbo
|
|
||||||
|
|
||||||
audioText:
|
|
||||||
asr: http://39.98.44.136:8001/asr/file
|
|
||||||
tts: http://39.98.44.136:19527/synthesize
|
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package com.ruoyi.cms.controller.app;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.ruoyi.cms.config.ChatClient;
|
||||||
|
import com.ruoyi.cms.config.ChatConfig;
|
||||||
|
import com.ruoyi.cms.domain.ai.AiChatHistory;
|
||||||
|
import com.ruoyi.cms.domain.chat.ChatRequest;
|
||||||
|
import com.ruoyi.cms.service.AiChatHistoryService;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.websocket.*;
|
||||||
|
import javax.websocket.server.PathParam;
|
||||||
|
import javax.websocket.server.ServerEndpoint;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket AI 聊天处理器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ServerEndpoint("/ws/chat/{userId}")
|
||||||
|
public class ChatWebSocketHandler {
|
||||||
|
|
||||||
|
// 存储所有连接的会话
|
||||||
|
private static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 由于 WebSocket 是多例的,需要通过 SpringUtils 获取 Bean
|
||||||
|
private ChatClient getChatClient() {
|
||||||
|
return SpringUtils.getBean(ChatClient.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatConfig getChatConfig() {
|
||||||
|
return SpringUtils.getBean(ChatConfig.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiChatHistoryService getAiChatHistoryService() {
|
||||||
|
return SpringUtils.getBean(AiChatHistoryService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnOpen
|
||||||
|
public void onOpen(Session session, @PathParam("userId") String userId) {
|
||||||
|
SESSIONS.put(userId + "_" + session.getId(), session);
|
||||||
|
log.info("WebSocket 连接建立,userId: {}, sessionId: {}", userId, session.getId());
|
||||||
|
sendMessage(session, buildResponse("connected", "连接成功", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClose
|
||||||
|
public void onClose(Session session, @PathParam("userId") String userId) {
|
||||||
|
SESSIONS.remove(userId + "_" + session.getId());
|
||||||
|
log.info("WebSocket 连接关闭,userId: {}, sessionId: {}", userId, session.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnError
|
||||||
|
public void onError(Session session, Throwable error, @PathParam("userId") String userId) {
|
||||||
|
log.error("WebSocket 发生错误,userId: {}, error: {}", userId, error.getMessage());
|
||||||
|
sendMessage(session, buildResponse("error", error.getMessage(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收客户端消息
|
||||||
|
* 消息格式:
|
||||||
|
* {
|
||||||
|
* "action": "chat", // chat: 聊天, history: 获取历史, detail: 获取详情, guest: 获取建议
|
||||||
|
* "data": "用户输入的内容",
|
||||||
|
* "sessionId": "会话ID",
|
||||||
|
* "dataId": "数据ID",
|
||||||
|
* "fileUrl": ["文件URL列表"]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@OnMessage
|
||||||
|
public void onMessage(String message, Session session, @PathParam("userId") String userId) {
|
||||||
|
log.info("收到消息,userId: {}, message: {}", userId, message);
|
||||||
|
try {
|
||||||
|
JSONObject json = JSONObject.parseObject(message);
|
||||||
|
String action = json.getString("action");
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(action)) {
|
||||||
|
action = "chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "chat":
|
||||||
|
handleChat(json, session, userId);
|
||||||
|
break;
|
||||||
|
case "history":
|
||||||
|
handleHistory(json, session);
|
||||||
|
break;
|
||||||
|
case "detail":
|
||||||
|
handleDetail(json, session);
|
||||||
|
break;
|
||||||
|
case "guest":
|
||||||
|
handleGuest(json, session);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendMessage(session, buildResponse("error", "未知的操作类型: " + action, null));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理消息失败: {}", e.getMessage(), e);
|
||||||
|
sendMessage(session, buildResponse("error", "处理消息失败: " + e.getMessage(), null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理聊天请求
|
||||||
|
*/
|
||||||
|
private void handleChat(JSONObject json, Session session, String userId) {
|
||||||
|
ChatRequest request = buildChatRequest(json, userId);
|
||||||
|
|
||||||
|
JSONObject contentObject = new JSONObject();
|
||||||
|
contentObject.put("content", request.getData());
|
||||||
|
contentObject.put("role", "user");
|
||||||
|
|
||||||
|
JSONArray array = getAiChatHistoryService().getChatHistoryData(request.getSessionId());
|
||||||
|
if (array == null || array.isEmpty()) {
|
||||||
|
array = new JSONArray();
|
||||||
|
}
|
||||||
|
array.add(contentObject);
|
||||||
|
request.setMessages(array);
|
||||||
|
|
||||||
|
List<String> answerList = new ArrayList<>();
|
||||||
|
long[] timeStart = {0};
|
||||||
|
|
||||||
|
getChatClient().sendStreamingChat(request, new ChatClient.StreamCallback() {
|
||||||
|
@Override
|
||||||
|
public void onData(String chunk) {
|
||||||
|
if (timeStart[0] == 0) {
|
||||||
|
timeStart[0] = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
String processedChunk = chunk.trim();
|
||||||
|
String content = parseChatChunk(processedChunk);
|
||||||
|
if (StringUtils.isNotEmpty(content)) {
|
||||||
|
answerList.add(content);
|
||||||
|
}
|
||||||
|
// 发送流式数据
|
||||||
|
sendMessage(session, buildResponse("data", null, processedChunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
// 保存聊天记录
|
||||||
|
saveChatHistory(request, userId, answerList, timeStart[0]);
|
||||||
|
sendMessage(session, buildResponse("complete", "对话完成", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable e) {
|
||||||
|
log.error("聊天请求失败: {}", e.getMessage());
|
||||||
|
sendMessage(session, buildResponse("error", e.getMessage(), null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理获取历史记录请求
|
||||||
|
*/
|
||||||
|
private void handleHistory(JSONObject json, Session session) {
|
||||||
|
AiChatHistory chatHistory = new AiChatHistory();
|
||||||
|
if (json.containsKey("userId")) {
|
||||||
|
chatHistory.setUserId(json.getLong("userId"));
|
||||||
|
}
|
||||||
|
JSONObject result = getAiChatHistoryService().getList(chatHistory);
|
||||||
|
sendMessage(session, buildResponse("history", null, result.toJSONString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理获取聊天详情请求
|
||||||
|
*/
|
||||||
|
private void handleDetail(JSONObject json, Session session) {
|
||||||
|
String sessionId = json.getString("sessionId");
|
||||||
|
JSONObject result = getAiChatHistoryService().getDetailList(sessionId);
|
||||||
|
sendMessage(session, buildResponse("detail", null, result.toJSONString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理获取建议请求
|
||||||
|
*/
|
||||||
|
private void handleGuest(JSONObject json, Session session) {
|
||||||
|
try {
|
||||||
|
ChatRequest request = new ChatRequest();
|
||||||
|
request.setSessionId(json.getString("sessionId"));
|
||||||
|
JSONArray array = getAiChatHistoryService().getChatHistoryData(request.getSessionId());
|
||||||
|
request.setMessages(array);
|
||||||
|
|
||||||
|
String result = getChatClient().sendChatGuest(request);
|
||||||
|
String[] strList = result.split("?");
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (String str : strList) {
|
||||||
|
if (StringUtils.isNotEmpty(str)) {
|
||||||
|
str = str + "?";
|
||||||
|
list.add(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendMessage(session, buildResponse("guest", null, JSONObject.toJSONString(list)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取建议失败: {}", e.getMessage());
|
||||||
|
sendMessage(session, buildResponse("error", "获取建议失败: " + e.getMessage(), null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 ChatRequest 对象
|
||||||
|
*/
|
||||||
|
private ChatRequest buildChatRequest(JSONObject json, String userId) {
|
||||||
|
ChatRequest request = new ChatRequest();
|
||||||
|
request.setData(json.getString("data"));
|
||||||
|
request.setSessionId(json.getString("sessionId"));
|
||||||
|
request.setDataId(json.getString("dataId"));
|
||||||
|
|
||||||
|
if (json.containsKey("fileUrl")) {
|
||||||
|
request.setFileUrl(json.getJSONArray("fileUrl").toJavaList(String.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.containsKey("userId") && json.getLong("userId") != 0) {
|
||||||
|
request.setUserId(json.getLong("userId"));
|
||||||
|
} else if (StringUtils.isNotEmpty(userId) && !"0".equals(userId)) {
|
||||||
|
request.setUserId(Long.parseLong(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存聊天记录
|
||||||
|
*/
|
||||||
|
private void saveChatHistory(ChatRequest request, String userId, List<String> answerList, long timeStart) {
|
||||||
|
long timeEnd = System.currentTimeMillis();
|
||||||
|
double duration = (timeEnd - timeStart) / 1000.0;
|
||||||
|
|
||||||
|
AiChatHistory chatHistory = new AiChatHistory();
|
||||||
|
chatHistory.setChatId(request.getSessionId());
|
||||||
|
|
||||||
|
Long uid = null;
|
||||||
|
if (request.getUserId() != 0) {
|
||||||
|
uid = request.getUserId();
|
||||||
|
} else if (StringUtils.isNotEmpty(userId) && !"0".equals(userId)) {
|
||||||
|
uid = Long.parseLong(userId);
|
||||||
|
}
|
||||||
|
chatHistory.setUserId(uid);
|
||||||
|
chatHistory.setAppId(getChatConfig().getAppId());
|
||||||
|
chatHistory.setDataId(request.getDataId());
|
||||||
|
chatHistory.setTitle(request.getData());
|
||||||
|
chatHistory.setAnswerStringList(answerList);
|
||||||
|
chatHistory.setDurationSeconds(duration);
|
||||||
|
|
||||||
|
getAiChatHistoryService().saveChatHistory(chatHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析流式数据
|
||||||
|
*/
|
||||||
|
private String parseChatChunk(String chunk) {
|
||||||
|
try {
|
||||||
|
String processed = chunk.trim();
|
||||||
|
if (processed.startsWith("data:")) {
|
||||||
|
processed = processed.substring("data:".length()).trim();
|
||||||
|
}
|
||||||
|
if (processed.isEmpty() || "[DONE]".equals(processed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject json = JSONObject.parseObject(processed);
|
||||||
|
String choices = json.getString("choices");
|
||||||
|
if (choices != null && !choices.trim().isEmpty()) {
|
||||||
|
JSONArray jsonArray = JSONArray.parseArray(choices);
|
||||||
|
json = JSONObject.parseObject(jsonArray.getString(0));
|
||||||
|
json = json.getJSONObject("delta");
|
||||||
|
return json.getString("content");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建响应消息
|
||||||
|
*/
|
||||||
|
private String buildResponse(String type, String message, String data) {
|
||||||
|
JSONObject response = new JSONObject();
|
||||||
|
response.put("type", type);
|
||||||
|
if (message != null) {
|
||||||
|
response.put("message", message);
|
||||||
|
}
|
||||||
|
if (data != null) {
|
||||||
|
response.put("data", data);
|
||||||
|
}
|
||||||
|
return response.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
private void sendMessage(Session session, String message) {
|
||||||
|
if (session != null && session.isOpen()) {
|
||||||
|
try {
|
||||||
|
synchronized (session) {
|
||||||
|
session.getBasicRemote().sendText(message);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送消息失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定用户发送消息
|
||||||
|
*/
|
||||||
|
public static void sendMessageToUser(String userId, String message) {
|
||||||
|
SESSIONS.forEach((key, session) -> {
|
||||||
|
if (key.startsWith(userId + "_") && session.isOpen()) {
|
||||||
|
try {
|
||||||
|
synchronized (session) {
|
||||||
|
session.getBasicRemote().sendText(message);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送消息给用户 {} 失败: {}", userId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ public class ESJobSearchImpl implements IESJobSearchService
|
|||||||
/**
|
/**
|
||||||
* 项目启动时,初始化索引及数据
|
* 项目启动时,初始化索引及数据
|
||||||
*/
|
*/
|
||||||
@PostConstruct
|
// @PostConstruct
|
||||||
public void init()
|
public void init()
|
||||||
{
|
{
|
||||||
boolean isLockAcquired = false;
|
boolean isLockAcquired = false;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public class SecurityConfig
|
|||||||
.authorizeHttpRequests((requests) -> {
|
.authorizeHttpRequests((requests) -> {
|
||||||
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
|
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
|
||||||
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
|
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
|
||||||
requests.antMatchers("/login", "/register", "/captchaImage","/app/login","/websocket/**","/speech-recognition","/speech-synthesis",
|
requests.antMatchers("/login", "/register", "/captchaImage","/app/login","/websocket/**","/ws/**","/speech-recognition","/speech-synthesis",
|
||||||
"/cms/company/listPage","/cms/appUser/noTmlist","/getTjmhToken","/getWwTjmhToken","/getWwTjmHlwToken",
|
"/cms/company/listPage","/cms/appUser/noTmlist","/getTjmhToken","/getWwTjmhToken","/getWwTjmHlwToken",
|
||||||
"/cms/notice/noticTotal","/cms/jobApply/zphApply","/cms/jobApply/zphApplyAgree").permitAll()
|
"/cms/notice/noticTotal","/cms/jobApply/zphApply","/cms/jobApply/zphApplyAgree").permitAll()
|
||||||
// 静态资源,可匿名访问
|
// 静态资源,可匿名访问
|
||||||
|
|||||||
Reference in New Issue
Block a user