|
@@ -1,8 +1,13 @@
|
|
|
package com.zhentao.information.handler;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONException;
|
|
|
+import com.google.gson.Gson;
|
|
|
+import com.google.gson.JsonObject;
|
|
|
+import com.google.gson.JsonParser;
|
|
|
+import com.zhentao.Ai.dto.DeeseekRequest;
|
|
|
import com.zhentao.groups.MongoDB.pojo.GroupMessage;
|
|
|
-import com.zhentao.groups.MongoDB.pojo.Message;
|
|
|
+import com.zhentao.information.entity.Message;
|
|
|
import com.zhentao.information.entity.ChatMessage;
|
|
|
import com.zhentao.information.repository.ChatMessageRepository;
|
|
|
import com.zhentao.information.service.WebSocketService;
|
|
@@ -15,17 +20,34 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStreamReader;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.SocketTimeoutException;
|
|
|
+import java.net.URL;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
|
|
|
/**
|
|
|
* WebSocket消息处理器
|
|
|
- * 处理WebSocket连接、消息接收和发送
|
|
|
+ * 处理WebSocket连接、消息接收和发送,集成AI功能
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Component
|
|
|
@ChannelHandler.Sharable
|
|
|
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
|
|
|
|
|
|
+ private static final Gson gson = new Gson();
|
|
|
+ private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
|
|
|
+ private static final String API_KEY = "sk-df51dab7323441998d41f18494098ddc";
|
|
|
+ private static final String AI_USER_ID = "1933707308387405824";
|
|
|
+
|
|
|
@Resource
|
|
|
private ChatMessageRepository chatMessageRepository;
|
|
|
|
|
@@ -39,8 +61,23 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketF
|
|
|
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
|
|
|
String text = msg.text();
|
|
|
log.info("收到消息:{}", text);
|
|
|
+
|
|
|
+ // 检查消息是否为空或无效
|
|
|
+ if (text == null || text.trim().isEmpty()) {
|
|
|
+ log.warn("收到空消息或无效消息");
|
|
|
+ sendErrorMessage(ctx, "消息不能为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
- Message message = JSON.parseObject(text, Message.class);
|
|
|
+ // 尝试解析JSON消息
|
|
|
+ Message message = parseMessage(text);
|
|
|
+ if (message == null) {
|
|
|
+ log.error("消息解析失败,原始消息:{}", text);
|
|
|
+ sendErrorMessage(ctx, "消息格式错误");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
log.info("接收到的消息:{}", message);
|
|
|
|
|
|
// 如果是连接消息,处理token
|
|
@@ -73,33 +110,234 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketF
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 检查是否是发送给AI的消息
|
|
|
+ if (AI_USER_ID.equals(message.getToUserId())) {
|
|
|
+ handleAIMessage(message, ctx);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// 处理普通消息
|
|
|
handleMessage(message);
|
|
|
|
|
|
+ } catch (JSONException e) {
|
|
|
+ log.error("JSON解析失败,原始消息:{},错误:{}", text, e.getMessage());
|
|
|
+ sendErrorMessage(ctx, "消息格式错误:" + e.getMessage());
|
|
|
} catch (Exception e) {
|
|
|
- log.error("处理消息失败", e);
|
|
|
+ log.error("处理消息失败,原始消息:{},错误:{}", text, e.getMessage(), e);
|
|
|
// 发送错误消息给客户端
|
|
|
Message errorMessage = new Message();
|
|
|
errorMessage.setType("error");
|
|
|
- errorMessage.setContent("消息处理失败");
|
|
|
+ errorMessage.setContent("消息处理失败:" + e.getMessage());
|
|
|
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(errorMessage)));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 解析消息,增加错误处理
|
|
|
+ */
|
|
|
+ private Message parseMessage(String text) {
|
|
|
+ try {
|
|
|
+ // 首先验证JSON格式
|
|
|
+ if (!text.trim().startsWith("{")) {
|
|
|
+ log.error("消息不是有效的JSON格式,期望以{开头,实际:{}", text);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ Message message = JSON.parseObject(text, Message.class);
|
|
|
+
|
|
|
+ // 验证必要字段
|
|
|
+ if (message.getType() == null) {
|
|
|
+ log.warn("消息缺少type字段:{}", text);
|
|
|
+ }
|
|
|
+
|
|
|
+ return message;
|
|
|
+ } catch (JSONException e) {
|
|
|
+ log.error("JSON解析异常:{},原始消息:{}", e.getMessage(), text);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("消息解析异常:{},原始消息:{}", e.getMessage(), text);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将information包的Message转换为MongoDB包的Message
|
|
|
+ */
|
|
|
+ private com.zhentao.groups.MongoDB.pojo.Message convertToMongoMessage(Message message) {
|
|
|
+ com.zhentao.groups.MongoDB.pojo.Message mongoMessage = new com.zhentao.groups.MongoDB.pojo.Message();
|
|
|
+ mongoMessage.setType(message.getType());
|
|
|
+ mongoMessage.setFromUserId(message.getFromUserId());
|
|
|
+ mongoMessage.setToUserId(message.getToUserId());
|
|
|
+ mongoMessage.setGroupId(message.getGroupId());
|
|
|
+ mongoMessage.setContent(message.getContent());
|
|
|
+ mongoMessage.setTimestamp(message.getTimestamp());
|
|
|
+ mongoMessage.setFileUrl(message.getFileUrl());
|
|
|
+ mongoMessage.setFileName(message.getFileName());
|
|
|
+ mongoMessage.setFileType(message.getFileType());
|
|
|
+ mongoMessage.setFileSize(message.getFileSize());
|
|
|
+ mongoMessage.setAvatar(message.getAvatar());
|
|
|
+ return mongoMessage;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理AI消息
|
|
|
+ */
|
|
|
+ private void handleAIMessage(Message message, ChannelHandlerContext ctx) {
|
|
|
+ String question = message.getContent();
|
|
|
+ if (question == null || question.trim().isEmpty()) {
|
|
|
+ sendErrorMessage(ctx, "问题不能为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送AI开始处理的消息
|
|
|
+ Message aiStartMessage = new Message();
|
|
|
+ aiStartMessage.setType("ai_start");
|
|
|
+ aiStartMessage.setContent("AI正在思考中...");
|
|
|
+ aiStartMessage.setFromUserId(AI_USER_ID);
|
|
|
+ aiStartMessage.setToUserId(message.getFromUserId());
|
|
|
+ ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(aiStartMessage)));
|
|
|
+
|
|
|
+ // 使用线程池处理AI请求
|
|
|
+ ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
|
+ executor.submit(() -> processAIStreamingResponse(message, ctx));
|
|
|
+ executor.shutdown();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理AI流式响应
|
|
|
+ */
|
|
|
+ private void processAIStreamingResponse(Message originalMessage, ChannelHandlerContext ctx) {
|
|
|
+ HttpURLConnection connection = null;
|
|
|
+ try {
|
|
|
+ URL url = new URL(DEEPSEEK_API_URL);
|
|
|
+ connection = (HttpURLConnection) url.openConnection();
|
|
|
+ connection.setRequestMethod("POST");
|
|
|
+ connection.setRequestProperty("Content-Type", "application/json");
|
|
|
+ connection.setRequestProperty("Authorization", "Bearer " + API_KEY);
|
|
|
+ connection.setRequestProperty("Accept", "text/event-stream");
|
|
|
+ connection.setDoOutput(true);
|
|
|
+ connection.setDoInput(true);
|
|
|
+ connection.setUseCaches(false);
|
|
|
+ connection.setConnectTimeout(5000);
|
|
|
+ connection.setReadTimeout(30000);
|
|
|
+
|
|
|
+ // 发送请求体
|
|
|
+ try (OutputStream os = connection.getOutputStream()) {
|
|
|
+ os.write(buildAIRequestBody(originalMessage.getContent()).getBytes(StandardCharsets.UTF_8));
|
|
|
+ os.flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理流式响应
|
|
|
+ try (BufferedReader reader = new BufferedReader(
|
|
|
+ new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
|
|
+
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null && ctx.channel().isActive()) {
|
|
|
+ if (line.startsWith("data: ") && !line.equals("data: [DONE]")) {
|
|
|
+ String content = parseAIContent(line.substring(6));
|
|
|
+ if (content != null) {
|
|
|
+ // 发送AI流式响应
|
|
|
+ Message aiResponse = new Message();
|
|
|
+ aiResponse.setType("ai_stream");
|
|
|
+ aiResponse.setContent(content);
|
|
|
+ aiResponse.setFromUserId(AI_USER_ID);
|
|
|
+ aiResponse.setToUserId(originalMessage.getFromUserId());
|
|
|
+ ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(aiResponse)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送AI完成消息
|
|
|
+ Message aiCompleteMessage = new Message();
|
|
|
+ aiCompleteMessage.setType("ai_complete");
|
|
|
+ aiCompleteMessage.setContent("AI回答完成");
|
|
|
+ aiCompleteMessage.setFromUserId(AI_USER_ID);
|
|
|
+ aiCompleteMessage.setToUserId(originalMessage.getFromUserId());
|
|
|
+ ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(aiCompleteMessage)));
|
|
|
+
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
+ sendErrorMessage(ctx, "AI响应超时,请重试");
|
|
|
+ } catch (IOException e) {
|
|
|
+ sendErrorMessage(ctx, "AI网络错误: " + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ if (connection != null) connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建AI请求体
|
|
|
+ */
|
|
|
+ private String buildAIRequestBody(String question) {
|
|
|
+ List<DeeseekRequest.Message> messages = new ArrayList<>();
|
|
|
+ messages.add(DeeseekRequest.Message.builder()
|
|
|
+ .role("system")
|
|
|
+ .content("你是一个佳佳聊天小助手,请用中文回答")
|
|
|
+ .build());
|
|
|
+ messages.add(DeeseekRequest.Message.builder()
|
|
|
+ .role("user")
|
|
|
+ .content(question)
|
|
|
+ .build());
|
|
|
+
|
|
|
+ return gson.toJson(DeeseekRequest.builder()
|
|
|
+ .model("deepseek-chat")
|
|
|
+ .messages(messages)
|
|
|
+ .stream(true)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析AI响应内容
|
|
|
+ */
|
|
|
+ private String parseAIContent(String json) {
|
|
|
+ try {
|
|
|
+ JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
|
|
|
+ if (obj.has("choices")) {
|
|
|
+ JsonObject delta = obj.getAsJsonArray("choices")
|
|
|
+ .get(0).getAsJsonObject()
|
|
|
+ .getAsJsonObject("delta");
|
|
|
+ if (delta.has("content")) {
|
|
|
+ return delta.get("content").getAsString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送错误消息
|
|
|
+ */
|
|
|
+ private void sendErrorMessage(ChannelHandlerContext ctx, String message) {
|
|
|
+ try {
|
|
|
+ if (ctx.channel().isActive()) {
|
|
|
+ Message errorMessage = new Message();
|
|
|
+ errorMessage.setType("ai_error");
|
|
|
+ errorMessage.setContent(message);
|
|
|
+ ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(errorMessage)));
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发送错误消息失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 处理群聊消息
|
|
|
*/
|
|
|
private void handleGroupMessage(Message message) {
|
|
|
// 设置群聊消息类型
|
|
|
message.setType(message.getType() != null ? message.getType() : "group_chat");
|
|
|
+ // 转换为MongoDB Message类型
|
|
|
+ com.zhentao.groups.MongoDB.pojo.Message mongoMessage = convertToMongoMessage(message);
|
|
|
// 广播消息给群内所有成员
|
|
|
- boolean sent = webSocketService.handleGroupMessage(message);
|
|
|
+ boolean sent = webSocketService.handleGroupMessage(mongoMessage);
|
|
|
if (sent) {
|
|
|
// 发送消息确认
|
|
|
Message ackMessage = new Message();
|
|
|
ackMessage.setType("message_ack");
|
|
|
ackMessage.setContent("群消息已发送");
|
|
|
- webSocketService.sendMessageToUser(message.getFromUserId(), ackMessage);
|
|
|
+ webSocketService.sendMessageToUser(message.getFromUserId(), convertToMongoMessage(ackMessage));
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -131,22 +369,23 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketF
|
|
|
// 保存消息到MongoDB
|
|
|
chatMessageRepository.save(chatMessage);
|
|
|
|
|
|
- // 发送消息给接收者
|
|
|
- boolean sent = webSocketService.sendMessageToUser(message.getToUserId(), message);
|
|
|
+ // 转换为MongoDB Message类型并发送消息给接收者
|
|
|
+ com.zhentao.groups.MongoDB.pojo.Message mongoMessage = convertToMongoMessage(message);
|
|
|
+ boolean sent = webSocketService.sendMessageToUser(message.getToUserId(), mongoMessage);
|
|
|
if (sent) {
|
|
|
log.info("消息已发送给用户: {}, 内容: {}", message.getToUserId(), message.getContent());
|
|
|
// 发送消息确认给发送者
|
|
|
Message ackMessage = new Message();
|
|
|
ackMessage.setType("message_ack");
|
|
|
ackMessage.setContent("消息已发送");
|
|
|
- webSocketService.sendMessageToUser(message.getFromUserId(), ackMessage);
|
|
|
+ webSocketService.sendMessageToUser(message.getFromUserId(), convertToMongoMessage(ackMessage));
|
|
|
} else {
|
|
|
log.info("用户 {} 不在线,消息已保存到MongoDB", message.getToUserId());
|
|
|
// 发送消息未送达通知给发送者
|
|
|
Message offlineMessage = new Message();
|
|
|
offlineMessage.setType("message_offline");
|
|
|
offlineMessage.setContent("对方不在线,消息已保存");
|
|
|
- webSocketService.sendMessageToUser(message.getFromUserId(), offlineMessage);
|
|
|
+ webSocketService.sendMessageToUser(message.getFromUserId(), convertToMongoMessage(offlineMessage));
|
|
|
}
|
|
|
}
|
|
|
|