diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/config/AiConfig.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/config/AiConfig.java deleted file mode 100644 index 8d5a4f47..00000000 --- a/api/ai-agent/src/main/java/cn/qihangerp/erp/config/AiConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.qihangerp.erp.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import cn.qihangerp.erp.serviceImpl.ConversationHistoryManager; -import cn.qihangerp.erp.serviceImpl.SessionManager; - -/** - * AI相关配置类 - */ -@Configuration -public class AiConfig { - - /** - * 会话管理服务Bean - */ - @Bean - public SessionManager sessionManager() { - return new SessionManager(); - } - - /** - * 对话历史管理服务Bean - */ - @Bean - public ConversationHistoryManager conversationHistoryManager() { - return new ConversationHistoryManager(); - } -} \ No newline at end of file diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java index bc464b1f..3c303dd4 100644 --- a/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java @@ -123,7 +123,7 @@ public class SseController { log.info("用户 {} 的会话ID: {}", userId, sessionId); // 添加用户消息到对话历史 - conversationHistoryManager.addMessage(sessionId, "user", message); + conversationHistoryManager.addMessage(userId, sessionId, "user", message); } // 获取对话历史 @@ -138,7 +138,7 @@ public class SseController { // 如果有会话ID,添加AI回复到对话历史 if (sessionId != null) { - conversationHistoryManager.addMessage(sessionId, "assistant", response); + conversationHistoryManager.addMessage(userId, sessionId, "assistant", response); } // 检查响应是否已经是JSON格式(以{开头) @@ -160,6 +160,16 @@ public class SseController { return "消息发送成功"; } catch (Exception e) { log.error("消息处理失败: {}", e.getMessage()); + try { + // 发送错误信息到前端 + String errorMessage = e.getMessage(); + String jsonError = String.format("{\"error\": \"%s\"}", errorMessage.replace("\"", "\\\"").replace("\n", "\\n")); + emitter.send(SseEmitter.event() + .name("error") + .data(jsonError)); + } catch (IOException ex) { + log.error("发送错误信息失败: {}", ex.getMessage()); + } emitters.remove(clientId); clientUserIdMap.remove(clientId); return "消息发送失败: " + e.getMessage(); diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/ConversationHistoryManager.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/ConversationHistoryManager.java index 8908fb6b..b7a49271 100644 --- a/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/ConversationHistoryManager.java +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/ConversationHistoryManager.java @@ -1,17 +1,22 @@ package cn.qihangerp.erp.serviceImpl; +import cn.qihangerp.service.IAiConversationHistoryService; +import cn.qihangerp.model.entity.AiConversationHistory; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; /** * 对话历史管理服务,用于保存和管理用户的对话历史 */ +@Component +@AllArgsConstructor public class ConversationHistoryManager { - private static final Map> sessionHistoryMap = new ConcurrentHashMap<>(); - private static final AtomicLong messageIdCounter = new AtomicLong(0); + + private final IAiConversationHistoryService aiConversationHistoryService; /** * 消息实体类 @@ -22,8 +27,15 @@ public class ConversationHistoryManager { private String content; private long timestamp; + public Message(long id, String role, String content, long timestamp) { + this.id = id; + this.role = role; + this.content = content; + this.timestamp = timestamp; + } + public Message(String role, String content) { - this.id = messageIdCounter.incrementAndGet(); + this.id = 0; this.role = role; this.content = content; this.timestamp = System.currentTimeMillis(); @@ -56,8 +68,22 @@ public class ConversationHistoryManager { if (sessionId == null) { return; } - sessionHistoryMap.computeIfAbsent(sessionId, k -> new ArrayList<>()) - .add(new Message(role, content)); + // 这里简化处理,暂时不传入userId,实际使用时需要从会话中获取 + aiConversationHistoryService.saveMessage(0L, sessionId, role, content); + } + + /** + * 添加消息到对话历史(带用户ID) + * @param userId 用户ID + * @param sessionId 会话ID + * @param role 角色 + * @param content 消息内容 + */ + public void addMessage(Long userId, String sessionId, String role, String content) { + if (sessionId == null) { + return; + } + aiConversationHistoryService.saveMessage(userId, sessionId, role, content); } /** @@ -69,7 +95,10 @@ public class ConversationHistoryManager { if (sessionId == null) { return new ArrayList<>(); } - return sessionHistoryMap.getOrDefault(sessionId, new ArrayList<>()); + List historyList = aiConversationHistoryService.getBySessionId(sessionId); + return historyList.stream() + .map(history -> new Message(history.getId(), history.getRole(), history.getContent(), history.getTimestamp())) + .collect(Collectors.toList()); } /** @@ -82,9 +111,13 @@ public class ConversationHistoryManager { if (sessionId == null) { return new ArrayList<>(); } - List history = sessionHistoryMap.getOrDefault(sessionId, new ArrayList<>()); - int startIndex = Math.max(0, history.size() - limit); - return history.subList(startIndex, history.size()); + List historyList = aiConversationHistoryService.getRecentBySessionId(sessionId, limit); + // 反转列表,使时间戳从早到晚排序 + List messages = historyList.stream() + .map(history -> new Message(history.getId(), history.getRole(), history.getContent(), history.getTimestamp())) + .collect(Collectors.toList()); + java.util.Collections.reverse(messages); + return messages; } /** @@ -93,7 +126,7 @@ public class ConversationHistoryManager { */ public void clearConversationHistory(String sessionId) { if (sessionId != null) { - sessionHistoryMap.remove(sessionId); + aiConversationHistoryService.deleteBySessionId(sessionId); } } @@ -106,7 +139,7 @@ public class ConversationHistoryManager { if (sessionId == null) { return 0; } - List history = sessionHistoryMap.get(sessionId); - return history != null ? history.size() : 0; + List historyList = aiConversationHistoryService.getBySessionId(sessionId); + return historyList.size(); } } \ No newline at end of file diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/SessionManager.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/SessionManager.java index a90857a9..f6d43171 100644 --- a/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/SessionManager.java +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/SessionManager.java @@ -3,10 +3,12 @@ package cn.qihangerp.erp.serviceImpl; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.UUID; +import org.springframework.stereotype.Component; /** * 会话管理服务,用于管理用户的会话ID */ +@Component public class SessionManager { private static final Map userIdSessionMap = new ConcurrentHashMap<>(); private static final Map sessionUserIdMap = new ConcurrentHashMap<>(); diff --git a/mapper/src/main/java/cn/qihangerp/mapper/AiConversationHistoryMapper.java b/mapper/src/main/java/cn/qihangerp/mapper/AiConversationHistoryMapper.java new file mode 100644 index 00000000..d75b2cc6 --- /dev/null +++ b/mapper/src/main/java/cn/qihangerp/mapper/AiConversationHistoryMapper.java @@ -0,0 +1,43 @@ +package cn.qihangerp.mapper; + +import cn.qihangerp.model.entity.AiConversationHistory; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * AI聊天历史Mapper + */ +@Mapper +public interface AiConversationHistoryMapper extends BaseMapper { + /** + * 根据会话ID获取聊天历史 + * @param sessionId 会话ID + * @return 聊天历史列表 + */ + List selectBySessionId(@Param("sessionId") String sessionId); + + /** + * 根据会话ID获取最近的聊天历史 + * @param sessionId 会话ID + * @param limit 限制数量 + * @return 最近的聊天历史列表 + */ + List selectRecentBySessionId(@Param("sessionId") String sessionId, @Param("limit") int limit); + + /** + * 根据用户ID获取会话ID列表 + * @param userId 用户ID + * @return 会话ID列表 + */ + List selectSessionIdsByUserId(@Param("userId") Long userId); + + /** + * 根据会话ID删除聊天历史 + * @param sessionId 会话ID + * @return 删除的记录数 + */ + int deleteBySessionId(@Param("sessionId") String sessionId); +} \ No newline at end of file diff --git a/mapper/src/main/resources/mapper/AiConversationHistoryMapper.xml b/mapper/src/main/resources/mapper/AiConversationHistoryMapper.xml new file mode 100644 index 00000000..8941b96a --- /dev/null +++ b/mapper/src/main/resources/mapper/AiConversationHistoryMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + id, user_id, session_id, role, content, timestamp, create_time, update_time + + + + + + + + + + delete from ai_conversation_history + where session_id = #{sessionId} + + \ No newline at end of file diff --git a/mapper/src/main/resources/sql/ai_conversation_history.sql b/mapper/src/main/resources/sql/ai_conversation_history.sql new file mode 100644 index 00000000..4df013c2 --- /dev/null +++ b/mapper/src/main/resources/sql/ai_conversation_history.sql @@ -0,0 +1,14 @@ +CREATE TABLE `ai_conversation_history` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id,自增', + `user_id` bigint(20) NOT NULL COMMENT '用户id', + `session_id` varchar(36) NOT NULL COMMENT '会话id', + `role` varchar(20) NOT NULL COMMENT '角色:user或assistant', + `content` text NOT NULL COMMENT '消息内容', + `timestamp` bigint(20) NOT NULL COMMENT '消息时间戳', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_session_id` (`session_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_timestamp` (`timestamp`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI聊天历史表'; \ No newline at end of file diff --git a/model/src/main/java/cn/qihangerp/model/entity/AiConversationHistory.java b/model/src/main/java/cn/qihangerp/model/entity/AiConversationHistory.java new file mode 100644 index 00000000..86779e2e --- /dev/null +++ b/model/src/main/java/cn/qihangerp/model/entity/AiConversationHistory.java @@ -0,0 +1,62 @@ +package cn.qihangerp.model.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * AI聊天历史表 + * @TableName ai_conversation_history + */ +@TableName(value = "ai_conversation_history") +@Data +public class AiConversationHistory implements Serializable { + /** + * 主键id,自增 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户id + */ + private Long userId; + + /** + * 会话id + */ + private String sessionId; + + /** + * 角色:user或assistant + */ + private String role; + + /** + * 消息内容 + */ + private String content; + + /** + * 消息时间戳 + */ + private Long timestamp; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/service/src/main/java/cn/qihangerp/service/IAiConversationHistoryService.java b/service/src/main/java/cn/qihangerp/service/IAiConversationHistoryService.java new file mode 100644 index 00000000..d8c5a66b --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/IAiConversationHistoryService.java @@ -0,0 +1,50 @@ +package cn.qihangerp.service; + +import cn.qihangerp.model.entity.AiConversationHistory; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** + * AI聊天历史Service + */ +public interface IAiConversationHistoryService extends IService { + /** + * 根据会话ID获取聊天历史 + * @param sessionId 会话ID + * @return 聊天历史列表 + */ + List getBySessionId(String sessionId); + + /** + * 根据会话ID获取最近的聊天历史 + * @param sessionId 会话ID + * @param limit 限制数量 + * @return 最近的聊天历史列表 + */ + List getRecentBySessionId(String sessionId, int limit); + + /** + * 根据用户ID获取会话ID列表 + * @param userId 用户ID + * @return 会话ID列表 + */ + List getSessionIdsByUserId(Long userId); + + /** + * 根据会话ID删除聊天历史 + * @param sessionId 会话ID + * @return 删除的记录数 + */ + int deleteBySessionId(String sessionId); + + /** + * 保存聊天消息 + * @param userId 用户ID + * @param sessionId 会话ID + * @param role 角色 + * @param content 内容 + * @return 保存的消息 + */ + AiConversationHistory saveMessage(Long userId, String sessionId, String role, String content); +} \ No newline at end of file diff --git a/service/src/main/java/cn/qihangerp/service/impl/AiConversationHistoryServiceImpl.java b/service/src/main/java/cn/qihangerp/service/impl/AiConversationHistoryServiceImpl.java new file mode 100644 index 00000000..94608d69 --- /dev/null +++ b/service/src/main/java/cn/qihangerp/service/impl/AiConversationHistoryServiceImpl.java @@ -0,0 +1,55 @@ +package cn.qihangerp.service.impl; + +import cn.qihangerp.mapper.AiConversationHistoryMapper; +import cn.qihangerp.model.entity.AiConversationHistory; +import cn.qihangerp.service.IAiConversationHistoryService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +/** + * AI聊天历史Service实现 + */ +@Service +@AllArgsConstructor +public class AiConversationHistoryServiceImpl extends ServiceImpl implements IAiConversationHistoryService { + + private final AiConversationHistoryMapper aiConversationHistoryMapper; + + @Override + public List getBySessionId(String sessionId) { + return aiConversationHistoryMapper.selectBySessionId(sessionId); + } + + @Override + public List getRecentBySessionId(String sessionId, int limit) { + return aiConversationHistoryMapper.selectRecentBySessionId(sessionId, limit); + } + + @Override + public List getSessionIdsByUserId(Long userId) { + return aiConversationHistoryMapper.selectSessionIdsByUserId(userId); + } + + @Override + public int deleteBySessionId(String sessionId) { + return aiConversationHistoryMapper.deleteBySessionId(sessionId); + } + + @Override + public AiConversationHistory saveMessage(Long userId, String sessionId, String role, String content) { + AiConversationHistory history = new AiConversationHistory(); + history.setUserId(userId); + history.setSessionId(sessionId); + history.setRole(role); + history.setContent(content); + history.setTimestamp(System.currentTimeMillis()); + history.setCreateTime(new Date()); + history.setUpdateTime(new Date()); + save(history); + return history; + } +} \ No newline at end of file diff --git a/vue/src/views/index.vue b/vue/src/views/index.vue index b68a453b..b3555d17 100644 --- a/vue/src/views/index.vue +++ b/vue/src/views/index.vue @@ -269,7 +269,33 @@ export default { console.log('收到心跳:', event.data); }); - // 监听错误 + // 监听后端发送的错误信息 + this.sse.addEventListener('error', (event) => { + console.log('收到错误信息:', event.data); + try { + // 解析错误信息 + const errorData = JSON.parse(event.data); + if (errorData.error) { + // 移除正在思考的消息 + if (this.isLoading) { + this.messages = this.messages.filter(msg => !msg.isLoading); + this.isLoading = false; + } + // 显示错误信息 + this.messages.push({ + content: `错误: ${errorData.error}`, + time: this.formatTime(new Date()), + isMe: false, + avatar: '' + }); + this.scrollToBottom(); + } + } catch (e) { + console.error('解析错误信息失败:', e); + } + }); + + // 监听连接错误 this.sse.onerror = (error) => { console.error('SSE连接错误:', error); this.isSseConnected = false;