Jelajahi Sumber

重构AI对话

tsurumure 6 bulan lalu
induk
melakukan
882c154d9f
19 mengubah file dengan 494 tambahan dan 176 penghapusan
  1. 1 1
      db/__ds_chat.sql
  2. 8 10
      db/ai_chat.sql
  3. 2 1
      db/ai_chat_history.sql
  4. 11 2
      src/main/java/com/backendsys/modules/ai/chat/controller/ChatController.java
  5. 9 0
      src/main/java/com/backendsys/modules/ai/chat/dao/ChatDao.java
  6. 9 0
      src/main/java/com/backendsys/modules/ai/chat/dao/ChatHistoryDao.java
  7. 24 5
      src/main/java/com/backendsys/modules/ai/chat/entity/Chat.java
  8. 28 0
      src/main/java/com/backendsys/modules/ai/chat/entity/ChatHistory.java
  9. 13 0
      src/main/java/com/backendsys/modules/ai/chat/service/ChatHistoryService.java
  10. 9 1
      src/main/java/com/backendsys/modules/ai/chat/service/ChatService.java
  11. 35 0
      src/main/java/com/backendsys/modules/ai/chat/service/impl/ChatHistoryServiceImpl.java
  12. 147 2
      src/main/java/com/backendsys/modules/ai/chat/service/impl/ChatServiceImpl.java
  13. 8 2
      src/main/java/com/backendsys/modules/sdk/deepseek/controller/DeepSeekController.java
  14. 12 0
      src/main/java/com/backendsys/modules/sdk/deepseek/entity/DSResponse.java
  15. 5 1
      src/main/java/com/backendsys/modules/sdk/deepseek/service/DeepSeekClient.java
  16. 148 148
      src/main/java/com/backendsys/modules/sdk/deepseek/service/impl/DeepSeekClientImpl.java
  17. 2 2
      src/main/java/com/backendsys/modules/sdk/douyincloud/tos/service/DouyinTosService.java
  18. 1 1
      src/main/java/com/backendsys/modules/sdk/douyincloud/tos/service/impl/DouyinTosServiceImpl.java
  19. 22 0
      src/main/java/com/backendsys/modules/sse/utils/SseUtil.java

+ 1 - 1
db/__ds_chat.sql

@@ -14,7 +14,7 @@
 #     `user_nickname` VARCHAR(255) COMMENT '用户名',
 #     `role` VARCHAR(255) COMMENT '对话角色 (user, assistant, system, tool)',
 #     `content` VARCHAR(5000) NOT NULL COMMENT '对话内容',
-#     `content_type` VARCHAR(255) NOT NULL COMMENT '对话内容类型 (LOADING: 思考中, THINK: 思考, REPLY: 回复)',
+#     `content_type` VARCHAR(255) NOT NULL COMMENT '对话内容类型 (THINK: 思考, REPLY: 回复)',
 #     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 #     INDEX `idx_history_code` (`history_code`),
 #     INDEX `idx_user_id` (`user_id`),

+ 8 - 10
db/ai_chat.sql

@@ -9,12 +9,16 @@ CREATE TABLE `ai_chat` (
     PRIMARY KEY (`id`),
     `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `history_code` VARCHAR(36) NOT NULL COMMENT '对话历史记录ID',
+    `model` VARCHAR(255) COMMENT '模型 (HUNYUAN-混元, DEEPSEEK)',
+    `model_version` VARCHAR(255) COMMENT '模型版本 (deepseek-chat: 对话模型, deepseek-reasoner: 推理模型)',
+    `role` VARCHAR(255) COMMENT '对话角色 ("user", "assistant")',
+    `content_type` VARCHAR(255) COMMENT '对话内容类型 (THINK: 思考, REPLY: 回复)',
+    `content` VARCHAR(5000) NOT NULL COMMENT '对话内容',
+    `duration` BIGINT COMMENT '耗时',
     `user_id` BIGINT COMMENT '用户ID',
-    `user_nickname` VARCHAR(255) COMMENT '用户名',
-    `user_avatar` VARCHAR(255) COMMENT '用户头像',
+#     `user_nickname` VARCHAR(255) COMMENT '用户名',
+#     `user_avatar` VARCHAR(255) COMMENT '用户头像',
     `robot_code` VARCHAR(255) COMMENT '机器人CODE',
-    `role` VARCHAR(255) NOT NULL COMMENT '对话角色 ("user", "assistant")',
-    `content` VARCHAR(2500) NOT NULL COMMENT '对话内容',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX `idx_history_code` (`history_code`),
@@ -22,9 +26,3 @@ CREATE TABLE `ai_chat` (
     INDEX `idx_robot_code` (`robot_code`)
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI对话表';
 
-# INSERT INTO ai_chat(history_code, user_id, user_nickname, robot_code, content) VALUES
-#     ('5670822e-f0f7-4635-b766-c9c61edef0fa', 1, '超人', null, '生成一条关于制作蛋糕的文案,适用于家用,用料是纯天然无添加剂,主打绿色健康美味的理念'),
-#     ('5670822e-f0f7-4635-b766-c9c61edef0fa', null, null, '102348821', '在家也能享受绿色健康美味!用纯天然无添加剂的食材,带给你和家人最纯净的美味。从选购新鲜食材到烘焙出炉,每一步都充满温馨与乐趣。让我们一起回归自然,享受绿色健康的蛋糕时光!'),
-#     ('3e69922c-f0e7-4f82-905d-0b375e09409c', 1, '超人', null, '你好'),
-#     ('3e69922c-f0e7-4f82-905d-0b375e09409c', null, null, '102348822', '你好,朋友')
-# ;

+ 2 - 1
db/ai_chat_history.sql

@@ -11,9 +11,10 @@ CREATE TABLE `ai_chat_history` (
     `history_code` VARCHAR(36) NOT NULL COMMENT '对话历史记录ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
     `robot_code` VARCHAR(255) COMMENT '机器人CODE',
-    `last_content` VARCHAR(255) NOT NULL COMMENT '最后一次的对话内容',
+    `last_prompt` VARCHAR(255) NOT NULL COMMENT '最后一次的对话内容',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    UNIQUE KEY `uk_history_code` (`history_code`),
     INDEX `idx_user_id` (`user_id`),
     INDEX `idx_robot_code` (`robot_code`)
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI对话历史记录表';

+ 11 - 2
src/main/java/com/backendsys/modules/ai/chat/controller/ChatController.java

@@ -3,9 +3,11 @@ package com.backendsys.modules.ai.chat.controller;
 import com.backendsys.modules.ai.chat.entity.Chat;
 import com.backendsys.modules.ai.chat.service.ChatService;
 import com.backendsys.modules.common.utils.Result;
+import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
@@ -17,11 +19,18 @@ public class ChatController {
     @Autowired
     private ChatService chatService;
 
+    @Operation(summary = "发起对话")
     @PreAuthorize("@sr.hasPermission('31')")
     @PostMapping("/api/ai/chat/sendChat")
     public Result sendChat(@Validated(Chat.Create.class) @RequestBody Chat chat) {
-        chatService.sendChat(chat);
-        return Result.success();
+        return Result.success().put("data", chatService.sendChat(chat));
+    }
+
+    @Operation(summary = "获取我的对话列表")
+    @PreAuthorize("@sr.hasPermission('31')")
+    @GetMapping("/api/ai/chat/getChatList")
+    public Result getChatList(@Validated(Chat.Detail.class) Chat chat) {
+        return Result.success().put("data", chatService.selectChatList(chat.getHistory_code()));
     }
 
 }

+ 9 - 0
src/main/java/com/backendsys/modules/ai/chat/dao/ChatDao.java

@@ -0,0 +1,9 @@
+package com.backendsys.modules.ai.chat.dao;
+
+import com.backendsys.modules.ai.chat.entity.Chat;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ChatDao extends BaseMapper<Chat> {
+}

+ 9 - 0
src/main/java/com/backendsys/modules/ai/chat/dao/ChatHistoryDao.java

@@ -0,0 +1,9 @@
+package com.backendsys.modules.ai.chat.dao;
+
+import com.backendsys.modules.ai.chat.entity.ChatHistory;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ChatHistoryDao extends BaseMapper<ChatHistory> {
+}

+ 24 - 5
src/main/java/com/backendsys/modules/ai/chat/entity/Chat.java

@@ -1,27 +1,46 @@
 package com.backendsys.modules.ai.chat.entity;
 
 import com.backendsys.entity.validator.RangeStringArray;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
+@TableName("ai_chat")
 public class Chat {
 
+    public static interface Detail{}
     public static interface Create{}
     public static interface Update{}
     public static interface Delete{}
 
-    @RangeStringArray(message="对话角色取值有误,范围应是(HUNYUAN, DEEPSEEK)", value = {"HUNYUAN", "DEEPSEEK"}, groups = { Create.class, Update.class })
-    private String type;
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    @Size(max = 36, message = "对话历史记录ID长度不超过 {max} 字符", groups = { Create.class })
+    @NotEmpty(message = "history_code 不能为空", groups = { Detail.class })
+    private String history_code;
 
+    @RangeStringArray(message="模型取值有误,范围应是(HUNYUAN, DEEPSEEK)", value = {"HUNYUAN", "DEEPSEEK"}, groups = { Create.class, Update.class })
     private String model;
+    private String model_version;
 
-    @Size(max = 36, message = "对话历史记录ID长度不超过 {max} 字符", groups = { Create.class })
-    private String history_code;
+    private Long user_id;
+//    private String user_nickname;
+//    private String user_avatar;
+    private String robot_code;
+
+    private String role;    // 对话角色 ("user", "assistant")
 
     @Size(max = 5000, message = "对话内容长度不超过 {max} 字符", groups = { Create.class })
     @NotEmpty(message = "对话内容不能为空", groups = { Create.class })
-    private String prompt;
+    private String content;
+    private String content_type;    // 对话内容类型 (THINK: 思考, REPLY: 回复)
+    private Long duration;
+
+    private String create_time;
+    private String update_time;
 
 }

+ 28 - 0
src/main/java/com/backendsys/modules/ai/chat/entity/ChatHistory.java

@@ -0,0 +1,28 @@
+package com.backendsys.modules.ai.chat.entity;
+
+import com.backendsys.entity.validator.RangeStringArray;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+@TableName("ai_chat_history")
+public class ChatHistory {
+
+    public static interface Create{}
+    public static interface Update{}
+    public static interface Delete{}
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String history_code;
+    private Long user_id;
+    private String robot_code;
+    private String last_prompt;
+    private String create_time;
+    private String update_time;
+
+}

+ 13 - 0
src/main/java/com/backendsys/modules/ai/chat/service/ChatHistoryService.java

@@ -0,0 +1,13 @@
+package com.backendsys.modules.ai.chat.service;
+
+import com.backendsys.modules.ai.chat.entity.Chat;
+import com.backendsys.utils.response.PageEntity;
+
+import java.util.Map;
+
+public interface ChatHistoryService {
+
+    // 获取我的对话历史列表
+    PageEntity selectChatHistoryist(String history_code);
+
+}

+ 9 - 1
src/main/java/com/backendsys/modules/ai/chat/service/ChatService.java

@@ -1,9 +1,17 @@
 package com.backendsys.modules.ai.chat.service;
 
 import com.backendsys.modules.ai.chat.entity.Chat;
+import com.backendsys.utils.response.PageEntity;
+
+import java.util.List;
+import java.util.Map;
 
 public interface ChatService {
 
-    void sendChat(Chat chat);
+    // 发起对话
+    Map<String, Object> sendChat(Chat chat);
+
+    // 获取我的对话列表
+    PageEntity selectChatList(String history_code);
 
 }

+ 35 - 0
src/main/java/com/backendsys/modules/ai/chat/service/impl/ChatHistoryServiceImpl.java

@@ -0,0 +1,35 @@
+package com.backendsys.modules.ai.chat.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.backendsys.exception.CustException;
+import com.backendsys.modules.ai.chat.dao.ChatDao;
+import com.backendsys.modules.ai.chat.dao.ChatHistoryDao;
+import com.backendsys.modules.ai.chat.entity.Chat;
+import com.backendsys.modules.ai.chat.entity.ChatHistory;
+import com.backendsys.modules.ai.chat.service.ChatHistoryService;
+import com.backendsys.modules.ai.chat.service.ChatService;
+import com.backendsys.modules.common.config.security.utils.SecurityUtil;
+import com.backendsys.modules.sdk.deepseek.entity.DSResponse;
+import com.backendsys.modules.sdk.deepseek.service.DeepSeekClient;
+import com.backendsys.utils.response.PageEntity;
+import com.backendsys.utils.response.PageInfoResult;
+import com.backendsys.utils.v2.PageUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+@Service
+public class ChatHistoryServiceImpl implements ChatHistoryService {
+
+
+    @Override
+    public PageEntity selectChatHistoryist(String history_code) {
+        return null;
+    }
+}

+ 147 - 2
src/main/java/com/backendsys/modules/ai/chat/service/impl/ChatServiceImpl.java

@@ -1,27 +1,172 @@
 package com.backendsys.modules.ai.chat.service.impl;
 
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.backendsys.exception.CustException;
+import com.backendsys.modules.ai.chat.dao.ChatDao;
+import com.backendsys.modules.ai.chat.dao.ChatHistoryDao;
 import com.backendsys.modules.ai.chat.entity.Chat;
+import com.backendsys.modules.ai.chat.entity.ChatHistory;
 import com.backendsys.modules.ai.chat.service.ChatService;
+import com.backendsys.modules.common.config.security.utils.SecurityUtil;
+import com.backendsys.modules.sdk.deepseek.entity.DSRequestMessage;
+import com.backendsys.modules.sdk.deepseek.entity.DSResponse;
 import com.backendsys.modules.sdk.deepseek.service.DeepSeekClient;
+import com.backendsys.utils.response.PageEntity;
+import com.backendsys.utils.response.PageInfoResult;
+import com.backendsys.utils.v2.PageUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
 @Service
 public class ChatServiceImpl implements ChatService {
 
     @Autowired
     private DeepSeekClient deepSeekClient;
 
+    @Autowired
+    private ChatDao chatDao;
+    @Autowired
+    private ChatHistoryDao chatHistoryDao;
+
     /**
      * 发起对话
+     * - model: (HUNYUAN-混元, DEEPSEEK)
      */
     @Override
-    public void sendChat(Chat chat) {
+    public Map<String, Object> sendChat(Chat chat) {
+
+        Long user_id = SecurityUtil.getUserId();
+        chat.setUser_id(user_id);
+        chat.setRole("user");
+
+        String model = chat.getModel();
+        String model_version = chat.getModel_version();
+        String prompt = chat.getContent();
+
+        // -- 是否首次创建 (如果历史记录code为空,即是首次创建) ---------------
+        Boolean is_create = false;
+        String history_code = chat.getHistory_code();
+        if (StrUtil.isEmpty(history_code)) {
+            is_create = true;
+            // 首次创建,生成随机的 UUID
+            history_code = UUID.randomUUID().toString();
+            chat.setHistory_code(history_code);
+        }
+
+        // -- 对话历史记录 -----------------------------------------------
+        if (is_create) {
+            // 首次创建
+            ChatHistory historyEntity = new ChatHistory();
+            historyEntity.setHistory_code(history_code);
+            historyEntity.setUser_id(user_id);
+            historyEntity.setLast_prompt(prompt);
+
+            // [创建]
+            chatHistoryDao.insert(historyEntity);
+        } else {
+            // 查询历史对话是否存在
+            LambdaQueryWrapper<ChatHistory> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(ChatHistory::getHistory_code, history_code);
+            ChatHistory historyDetail = chatHistoryDao.selectOne(wrapper);
+            if (historyDetail == null) throw new CustException("history_code 不存在,请重新创建");
+
+            // [更新] 最后一次提交的内容
+            historyDetail.setLast_prompt(prompt);
+            chatHistoryDao.updateById(historyDetail);
+        }
+
+        // -- 对话 -----------------------------------------------------
+
+        // 再记录一下耗时
+
+        // ------------------------------------------------------------
+        if ("DEEPSEEK".equals(model)) {
+            // 创建一个 CompletableFuture 来执行异步任务
+            String finalHistory_code = history_code;
+            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+
+                try {
+
+                    // [DB] 查询对话历史记录 (按时间升序排序,最新的在最后面) (用于对话上下文)
+                    LambdaQueryWrapper<Chat> wrapper = new LambdaQueryWrapper<>();
+                    wrapper.eq(Chat::getHistory_code, finalHistory_code);
+                    wrapper.orderByDesc(Chat::getCreate_time);
+                    List<Chat> chatList = chatDao.selectList(wrapper);
 
+                    // [DeepSeek] 发起对话
+                    DSResponse dsResponse = deepSeekClient.chatCompletion(model_version, prompt, chatList);
 
+                    // [DB][提问]
+                    Chat userChat = new Chat();
+                    BeanUtil.copyProperties(chat, userChat);
+                    chatDao.insert(userChat);
 
-        // deepSeekClient.chatCompletion(param.getModel(), param.getPrompt(), SecurityUtil.getUserId());
+                    // [DB][思考] THINK
+                    String reasoningContent = dsResponse.getReasoning_content();
+                    if (StrUtil.isNotEmpty(reasoningContent)) {
+                        //
+                        Chat thinkChat = new Chat();
+                        BeanUtil.copyProperties(chat, thinkChat);
+                        //
+                        thinkChat.setRole("assistant");
+                        thinkChat.setContent_type("THINK");
+                        thinkChat.setContent(reasoningContent);
+                        thinkChat.setDuration(dsResponse.getReasoning_duration());
+                        chatDao.insert(thinkChat);
+                    }
 
+                    // [DB][回复] REPLY
+                    String content = dsResponse.getContent();
+                    if (StrUtil.isNotEmpty(content)) {
+                        //
+                        Chat replyChat = new Chat();
+                        BeanUtil.copyProperties(chat, replyChat);
+                        //
+                        replyChat.setRole("assistant");
+                        replyChat.setContent_type("REPLY");
+                        replyChat.setContent(content);
+                        replyChat.setDuration(dsResponse.getContent_duration());
+                        chatDao.insert(replyChat);
+                    }
+
+                } catch (Exception e) {
+                    System.out.println(e.getMessage());
+                }
+
+            });
+        }
+        // ------------------------------------------------------------
+
+        return Map.of("history_code", history_code);
+
+    }
+
+
+    /**
+     * 获取我的对话列表 (升序, 最新的在最后面出现)
+     */
+    @Override
+    public PageEntity selectChatList(String history_code) {
+
+        ChatHistory chatHistory = chatHistoryDao.selectOne(new LambdaQueryWrapper<ChatHistory>().eq(ChatHistory::getHistory_code, history_code));
+        if (chatHistory == null) throw new CustException("history_code 不存在");
+
+        PageUtils.startPage();  // 分页
+        LambdaQueryWrapper<Chat> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Chat::getHistory_code, history_code);
+        wrapper.orderByAsc(Chat::getCreate_time);
+        List<Chat> list = chatDao.selectList(wrapper);
+
+        return new PageInfoResult(list).toEntity();
     }
 
+
 }

+ 8 - 2
src/main/java/com/backendsys/modules/sdk/deepseek/controller/DeepSeekController.java

@@ -16,6 +16,8 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.concurrent.CompletableFuture;
+
 @Validated
 @RestController
 @Tag(name = "DeepSeek")
@@ -37,8 +39,12 @@ public class DeepSeekController {
     @PreAuthorize("@sr.hasPermission('101')")
     @PostMapping("/api/deepSeek/chat")
     public Result chat(@Validated @RequestBody DSParam param) {
-        deepSeekClient.chatCompletion(param.getModel(), param.getPrompt(), SecurityUtil.getUserId());
-        return Result.success();
+
+//        // 创建一个 CompletableFuture 来执行异步任务
+//        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+//            deepSeekClient.chatCompletion(param.getModel(), param.getPrompt());
+//        });
+        return Result.success().put("data", deepSeekClient.chatCompletion(param.getModel(), param.getPrompt(), null));
     }
 
     /**

+ 12 - 0
src/main/java/com/backendsys/modules/sdk/deepseek/entity/DSResponse.java

@@ -0,0 +1,12 @@
+package com.backendsys.modules.sdk.deepseek.entity;
+
+import lombok.Data;
+
+@Data
+public class DSResponse {
+
+    private String reasoning_content;
+    private Long reasoning_duration;
+    private String content;
+    private Long content_duration;
+}

+ 5 - 1
src/main/java/com/backendsys/modules/sdk/deepseek/service/DeepSeekClient.java

@@ -1,10 +1,14 @@
 package com.backendsys.modules.sdk.deepseek.service;
 
 import cn.hutool.json.JSONArray;
+import com.backendsys.modules.ai.chat.entity.Chat;
+import com.backendsys.modules.sdk.deepseek.entity.DSResponse;
+
+import java.util.List;
 
 public interface DeepSeekClient {
 
-    void chatCompletion(String prompt, String model, Long user_id);
+    DSResponse chatCompletion(String model, String prompt, List<Chat> chatList);
 
     JSONArray getModels();
 

+ 148 - 148
src/main/java/com/backendsys/modules/sdk/deepseek/service/impl/DeepSeekClientImpl.java

@@ -5,9 +5,12 @@ import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.backendsys.exception.CustException;
+import com.backendsys.modules.ai.chat.dao.ChatDao;
+import com.backendsys.modules.ai.chat.entity.Chat;
 import com.backendsys.modules.sdk.deepseek.entity.DSContent;
 import com.backendsys.modules.sdk.deepseek.entity.DSRequest;
 import com.backendsys.modules.sdk.deepseek.entity.DSRequestMessage;
+import com.backendsys.modules.sdk.deepseek.entity.DSResponse;
 import com.backendsys.modules.sdk.deepseek.service.DeepSeekClient;
 import com.backendsys.modules.common.config.security.utils.SecurityUtil;
 import com.backendsys.modules.sse.entity.SseResponse;
@@ -29,6 +32,7 @@ import org.springframework.stereotype.Service;
 import java.io.*;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
@@ -45,205 +49,203 @@ public class DeepSeekClientImpl implements DeepSeekClient {
     private String API_URL;
     @Value("${deepseek.api-key}")
     private String API_KEY;
-    @Value("${spring.application.name}")
-    private String APPLICATION_NAME;
     @Autowired
     private SseUtil sseUtil;
 
-    private final ObjectMapper objectMapper = new ObjectMapper();
-
-
     /**
      * 对话 (文档:https://api-docs.deepseek.com/zh-cn/api/create-chat-completion)
      *
      * @param prompt
      */
     @Override
-    public void chatCompletion(String model, String prompt, Long user_id) {
+    public DSResponse chatCompletion(String model, String prompt, List<Chat> chatList) {
 
-        String emitterKey = APPLICATION_NAME + "-userid-" + Convert.toStr(user_id);
+        DSResponse dsResponse = new DSResponse();
+        try {
+            System.out.println("向模型: " + model + " 提问: " + prompt);
 
-        // 创建一个 CompletableFuture 来执行异步任务
-        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+            // 【多轮对话】
+            // https://api-docs.deepseek.com/zh-cn/guides/multi_round_chat
 
-            try {
-                System.out.println("向模型: " + model + " 提问: " + prompt);
+            // - 接口做最大问答限制 (1-字数限制, 2-次数限制)
 
-                // 【多轮对话】
-                // https://api-docs.deepseek.com/zh-cn/guides/multi_round_chat
 
-                // - 接口做最大问答限制 (1-字数限制, 2-次数限制)
+            List<DSRequestMessage> messages = new ArrayList<>();
 
-                List<DSRequestMessage> messages = new ArrayList<>();
-//                messages.add(new DSRequestMessage("user", "你是谁"));
-//                messages.add(new DSRequestMessage("assistant", "xxx"));
-//                messages.add(new DSRequestMessage("user", "你是什么大模型"));
-//                messages.add(new DSRequestMessage("assistant", "xxx"));
-                messages.add(new DSRequestMessage("user", prompt));
+            // 加入上下文历史对话
+            if (chatList != null && !chatList.isEmpty()) {
+                chatList.stream().forEach(chat -> {
+                    if (!"THINK".equals(chat.getContent_type())) {
+                        messages.add(new DSRequestMessage(chat.getRole(), chat.getContent()));
+                    }
+                });
+                // 反转列表
+                Collections.reverse(messages);
+            }
 
-                // 构建请求体
-                DSRequest body = new DSRequest();
-                // model: (deepseek-chat: 对话模型, deepseek-reasoner: 推理模型)
-                body.setModel(model);
-                body.setMessages(messages);
-                body.setStream(true);
+            messages.add(0, new DSRequestMessage("user", prompt));
 
-                // 记录请求开始时间
-                long allStartTime = System.currentTimeMillis();
 
-                // 调用 Deepseek API
-                try (CloseableHttpClient client = HttpClients.createDefault()) {
+            System.out.println("------------------");
+            System.out.println("messages:");
+            System.out.println(messages);
+            System.out.println("------------------");
 
-                    HttpPost request = new HttpPost(API_URL + "/chat/completions");
-                    request.setHeader("Content-Type", "application/json");
-                    request.setHeader("Authorization", "Bearer " + API_KEY);
+            // 构建请求体
+            DSRequest body = new DSRequest();
 
-                    String requestBody = objectMapper.writeValueAsString(body);
-                    request.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
+            // model: (deepseek-chat: 对话模型, deepseek-reasoner: 推理模型)
+            body.setModel(model);
+            body.setMessages(messages);
+            body.setStream(true);
 
-                    try (
-                            CloseableHttpResponse response = client.execute(request);
-                            BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))
-                    ) {
+            // 记录请求开始时间
+            long allStartTime = System.currentTimeMillis();
 
+            // 调用 Deepseek API
+            try (CloseableHttpClient client = HttpClients.createDefault()) {
 
-                        long apiDuration = System.currentTimeMillis() - allStartTime;   // 接口耗时
+                HttpPost request = new HttpPost(API_URL + "/chat/completions");
+                request.setHeader("Content-Type", "application/json");
+                request.setHeader("Authorization", "Bearer " + API_KEY);
 
-                        long thinkStartTime = 0L;   // 开始思考
-                        long thinkDuration = 0L;    // 思考耗时
-                        Boolean isThinking = false;
+                ObjectMapper objectMapper = new ObjectMapper();
+                String requestBody = objectMapper.writeValueAsString(body);
+                request.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
 
-                        System.out.println("API 调用耗时: " + apiDuration + " 毫秒");
-                        System.out.println("-------------------- 开始流式回答: --------------------");
+                try (
+                    CloseableHttpResponse response = client.execute(request);
+                    BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))
+                ) {
 
-                        // [SSE] 发送消息
-                        DSContent initContent = new DSContent();
-                        initContent.setContent_type("loading");
-                        initContent.setContent("正在思考");
-                        sseUtil.send(emitterKey, (new SseResponse(SseResponseEnum.DEEPSEEK, initContent)).toJsonStr());
+                    long apiDuration = System.currentTimeMillis() - allStartTime;   // 接口耗时
 
-                        StringBuilder allContent = new StringBuilder();
-                        StringBuilder allReasoning = new StringBuilder();
+                    long thinkStartTime = 0L;   // 开始思考
+                    long reasoningContentDuration = 0L;    // 思考耗时
+                    Boolean isThinking = false;
 
-                        String line;
-                        while ((line = reader.readLine()) != null) {
+                    System.out.println("API 调用耗时: " + apiDuration + " 毫秒");
+                    System.out.println("-------------------- 开始流式回答: --------------------");
 
-                            // System.out.println(line);
-                            /*
-                                ---------------------- line ----------------------
-                                data: {
-                                    "id":"6fc7b0e0-6ba1-4050-b7f7-924dd3559102", "object":"chat.completion.chunk",
-                                    "created":1740550788, "model":"deepseek-chat", "system_fingerprint":"fp_3a5770e1b4_prod0225",
-                                    "choices":[
-                                        {
-                                            "index":0,
-                                            "delta":{ "content": "我是", "reasoning_content":null },
-                                            "logprobs":null, "finish_reason":null
-                                        }
-                                    ]
-                                }
-                                --------------------------------------------------
-                             */
-                            // System.out.println(line);
-
-                            if (line.startsWith("data: ")) {
-                                String jsonData = line.substring(6);
-                                if ("[DONE]".equals(jsonData)) break;
-                                JsonNode node = objectMapper.readTree(jsonData);
-                                JsonNode delta = node.path("choices").path(0).path("delta");
-
-                                // [推理] Think (仅 deepseek-reasoner: 推理模型 支持)
-                                String reasoning_content = delta.path("reasoning_content").asText("");
-                                if (!reasoning_content.isEmpty()) {
-
-                                    // 开始思考
-                                    if (!isThinking) {
-                                        isThinking = true;
-                                        thinkStartTime = System.currentTimeMillis();
-                                    }
+                    // [SSE] 发送消息
+                    DSContent initContent = new DSContent();
+                    initContent.setContent_type("loading");
+                    initContent.setContent("正在思考");
+                    sseUtil.send(new SseResponse(SseResponseEnum.DEEPSEEK, initContent).toJsonStr());
 
-                                    System.out.println("reasoning_content: " + reasoning_content);
+                    StringBuilder allContent = new StringBuilder();
+                    StringBuilder allReasoningContent = new StringBuilder();
 
-                                    // [SSE] 发送消息
-                                    DSContent dsContent = new DSContent();
-                                    dsContent.setContent_type("think");
-                                    dsContent.setContent(reasoning_content);
-                                    sseUtil.send(emitterKey, (new SseResponse(SseResponseEnum.DEEPSEEK, dsContent)).toJsonStr());
+                    String line;
+                    while ((line = reader.readLine()) != null) {
 
-                                    // 收集推理内容
-                                    allReasoning.append(reasoning_content);
+                        // System.out.println(line);
+                        /*
+                            ---------------------- line ----------------------
+                            data: {
+                                "id":"6fc7b0e0-6ba1-4050-b7f7-924dd3559102", "object":"chat.completion.chunk",
+                                "created":1740550788, "model":"deepseek-chat", "system_fingerprint":"fp_3a5770e1b4_prod0225",
+                                "choices":[
+                                    {
+                                        "index":0,
+                                        "delta":{ "content": "我是", "reasoning_content":null },
+                                        "logprobs":null, "finish_reason":null
+                                    }
+                                ]
+                            }
+                            --------------------------------------------------
+                         */
+
+                        if (line.startsWith("data: ")) {
+                            String jsonData = line.substring(6);
+                            if ("[DONE]".equals(jsonData)) break;
+                            JsonNode node = objectMapper.readTree(jsonData);
+                            JsonNode delta = node.path("choices").path(0).path("delta");
+
+                            // [推理] Think (仅 deepseek-reasoner: 推理模型 支持)
+                            String reasoning_content = delta.path("reasoning_content").asText("");
+                            if (!reasoning_content.isEmpty()) {
+
+                                // 开始思考
+                                if (!isThinking) {
+                                    isThinking = true;
+                                    thinkStartTime = System.currentTimeMillis();
                                 }
 
-                                // [回答] Reply
-                                String content = delta.path("content").asText("");
-                                if (!content.isEmpty()) {
+                                System.out.println("reasoning_content: " + reasoning_content);
 
-                                    // 停止思考,并计算思考耗时
-                                    if (isThinking) {
-                                        isThinking = false;
-                                        thinkDuration = thinkStartTime - allStartTime;
-                                        System.out.println("推理耗时: " + thinkDuration + "毫秒");
-                                        System.out.println("-----------------------------------------------");
-                                    }
+                                // [SSE] 发送消息
+                                DSContent dsContent = new DSContent();
+                                dsContent.setContent_type("think");
+                                dsContent.setContent(reasoning_content);
+                                sseUtil.send(new SseResponse(SseResponseEnum.DEEPSEEK, dsContent).toJsonStr());
 
-                                    System.out.println("content: " + content);
+                                // 收集推理内容
+                                allReasoningContent.append(reasoning_content);
+                            }
 
-                                    // [SSE] 发送消息
-                                    DSContent dsContent = new DSContent();
-                                    dsContent.setContent_type("reply");
-                                    dsContent.setContent(content);
-                                    sseUtil.send(emitterKey, (new SseResponse(SseResponseEnum.DEEPSEEK, dsContent)).toJsonStr());
+                            // [回答] Reply
+                            String content = delta.path("content").asText("");
+                            if (!content.isEmpty()) {
 
-                                    // 收集回答内容
-                                    allContent.append(content);
+                                // 停止思考,并计算思考耗时
+                                if (isThinking) {
+                                    isThinking = false;
+                                    reasoningContentDuration = thinkStartTime - allStartTime;
+                                    System.out.println("推理耗时: " + reasoningContentDuration + "毫秒");
+                                    System.out.println("-----------------------------------------------");
                                 }
 
+                                System.out.println("content: " + content);
+
+                                // [SSE] 发送消息
+                                DSContent dsContent = new DSContent();
+                                dsContent.setContent_type("reply");
+                                dsContent.setContent(content);
+                                sseUtil.send(new SseResponse(SseResponseEnum.DEEPSEEK, dsContent).toJsonStr());
 
+                                // 收集回答内容
+                                allContent.append(content);
                             }
-                        }
 
-//                        // 将 AI 的回复添加到对话历史
-//                        Map<String, String> aiMessage = new HashMap<>();
-//                        aiMessage.put("role", "assistant");
-//                        aiMessage.put("content", aiResponse.toString());
-//                        messages.add(aiMessage);
-//
-//                        // 更新会话状态
-//                        sessionHistory.put(userId, messages);
-//
-//                        log.info("流式回答结束, 问题: {}", question);
-//                        System.out.println(que);
-//                        emitter.complete();
-
-                        System.out.println("-------------------- 结束流式回答. --------------------");
-                        long durationOutput = System.currentTimeMillis() - allStartTime;
-
-                        System.out.println("全部推理: " + allReasoning);
-                        System.out.println("全部回答: " + allContent);
-                        System.out.println("总输出耗时: " + durationOutput + " 毫秒");
 
+                        }
                     }
-                } catch (Exception e) {
-                    String dataStr = (new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr();
-                    sseUtil.send(emitterKey, dataStr);
-                    System.out.println(e.getMessage());
+
+                    System.out.println("-------------------- 结束流式回答. --------------------");
+                    long contentDuration = System.currentTimeMillis() - allStartTime;
+
+                    System.out.println("全部推理: " + allReasoningContent);
+                    System.out.println("全部回答: " + allContent);
+                    System.out.println("总输出耗时: " + contentDuration + " 毫秒");
+
+
+
+                    dsResponse.setReasoning_content(allReasoningContent.toString());
+                    dsResponse.setReasoning_duration(reasoningContentDuration);
+                    dsResponse.setContent(allContent.toString());
+                    dsResponse.setContent_duration(contentDuration);
+                    return dsResponse;
+
                 }
             } catch (Exception e) {
-                String dataStr = (new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr();
-                sseUtil.send(emitterKey, dataStr);
+                sseUtil.send((new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr());
                 System.out.println(e.getMessage());
+                dsResponse.setContent(e.getMessage());
+                return dsResponse;
             }
-
-        });
+        } catch (Exception e) {
+            sseUtil.send((new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr());
+            System.out.println(e.getMessage());
+            dsResponse.setContent(e.getMessage());
+            return dsResponse;
+        }
 
     }
 
     @Override
     public JSONArray getModels() {
 
-        Long userId = SecurityUtil.getUserId();
-        String emitterKey = APPLICATION_NAME + "-userid-" + Convert.toStr(userId);
-
         // 调用 Deepseek API
         try (CloseableHttpClient client = HttpClients.createDefault()) {
 
@@ -267,13 +269,11 @@ public class DeepSeekClientImpl implements DeepSeekClient {
                 }
 
             } catch (Exception e) {
-                String dataStr = (new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr();
-                sseUtil.send(emitterKey, dataStr);
+                sseUtil.send((new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr());
                 throw new CustException(e.getMessage());
             }
         } catch (Exception e) {
-            String dataStr = (new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr();
-            sseUtil.send(emitterKey, dataStr);
+            sseUtil.send((new SseResponse(SseResponseEnum.DEEPSEEK, e.getMessage())).toJsonStr());
             throw new CustException(e.getMessage());
         }
 

+ 2 - 2
src/main/java/com/backendsys/modules/sdk/douyincloud/tos/service/DouyinTosService.java

@@ -11,10 +11,10 @@ import java.io.IOException;
  */
 public interface DouyinTosService {
 
-    // [抖音云COS] 上传对象
+    // [抖音云TOS] 上传对象
     SysFileResult putObject(MultipartFile multipartFile, String object_key) throws IOException;
 
-    // [抖音云COS] 删除对象
+    // [抖音云TOS] 删除对象
     void deleteObject(String object_key) throws IOException;
 
 }

+ 1 - 1
src/main/java/com/backendsys/modules/sdk/douyincloud/tos/service/impl/DouyinTosServiceImpl.java

@@ -99,7 +99,7 @@ public class DouyinTosServiceImpl implements DouyinTosService {
     }
 
     /**
-     * [抖音云COS] 上传对象
+     * [抖音云TOS] 上传对象
      */
     @Override
     public SysFileResult putObject(MultipartFile multipartFile, String object_key) {

+ 22 - 0
src/main/java/com/backendsys/modules/sse/utils/SseUtil.java

@@ -2,9 +2,11 @@ package com.backendsys.modules.sse.utils;
 
 import cn.hutool.core.convert.Convert;
 import cn.hutool.json.JSONUtil;
+import com.backendsys.modules.common.config.security.utils.SecurityUtil;
 import com.backendsys.modules.sse.emitter.SseEmitterManager;
 import com.backendsys.modules.sse.entity.SseResponse;
 import com.backendsys.modules.sse.entity.SseResponseEnum;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
@@ -14,6 +16,11 @@ import java.util.concurrent.ConcurrentHashMap;
 @Component
 public class SseUtil {
 
+    @Value("${spring.application.name}")
+    private String APPLICATION_NAME;
+
+    private Long user_id = SecurityUtil.getUserId();
+
     // [SSE] 发送消息 (单个)
     public void send(String emitterKey, Object data) {
         SseEmitterManager manager = SseEmitterManager.getInstance();
@@ -27,6 +34,21 @@ public class SseUtil {
             }
         }
     }
+
+    // [SSE] 发送消息 (单个) (自己)
+    public void send(Object data) {
+        SseEmitterManager manager = SseEmitterManager.getInstance();
+        SseEmitterUTF8 emitter = manager.getEmitter(APPLICATION_NAME + "-userid-" + Convert.toStr(user_id));
+        if (emitter != null) {
+            try {
+                emitter.send(SseEmitter.event().data(data));
+            } catch (IOException e) {
+                System.out.println(e.getMessage());
+                manager.removeEmitter(emitter);
+            }
+        }
+    }
+
     // [SSE] 发送消息 (全部)
     public void sendToAll(Object data) {
         SseEmitterManager manager = SseEmitterManager.getInstance();