diff --git a/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java index 9bd6753..e5e95af 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java @@ -80,12 +80,17 @@ public class AiChatServiceImpl implements AiChatService { private AiConfigService aiConfigService; private static final String DEFAULT_USER_ID = "emotion-museum-user"; - + // 使用场景常量 private static final String USAGE_SCENARIO_CHAT = "chat"; private static final String USAGE_SCENARIO_SUMMARY = "summary"; private static final String DEFAULT_ENVIRONMENT = "production"; + /** + * Coze工作流配置键 - 聊天场景 + */ + private static final String COZE_CHAT_CONFIG_KEY = "coze.chat.default"; + // API 相关常量 private static final String CONTENT_KEY = "content"; private static final String ROLE_KEY = "role"; @@ -744,23 +749,98 @@ public class AiChatServiceImpl implements AiChatService { /** * 发送消息到Coze AI(带messageId) + * 使用通用工作流调用方式,通过配置键获取AI配置 + * + * @param conversationId 会话ID + * @param messageId 消息ID + * @param userMessage 用户消息 + * @param userId 用户ID + * @return AI回复内容 */ private String sendMessageWithMessageId(String conversationId, String messageId, String userMessage, String userId) { log.info("发送消息到Coze AI: conversationId={}, messageId={}, userId={}", conversationId, messageId, userId); - // 创建API调用记录(包含messageId) - CozeApiCall apiCall = createApiCallRecord(conversationId, messageId, userMessage, userId, "chat"); + // 1. 获取AI配置 + AiConfig config = aiConfigService.getByConfigKey(COZE_CHAT_CONFIG_KEY); + if (config == null) { + log.error("未找到聊天场景的AI配置或配置已禁用: configKey={}", COZE_CHAT_CONFIG_KEY); + return "抱歉,AI服务暂时不可用,请稍后再试。"; + } + + // 2. 创建API调用记录(包含conversationId和messageId) + CozeApiCall apiCall = createChatWorkflowApiCallRecord(config, conversationId, messageId, userMessage, userId); try { - return executeCozeApiCall(apiCall, conversationId, userMessage, userId); + // 3. 构建工作流请求参数 + Map parameters = new HashMap<>(); + parameters.put("input", userMessage); + parameters.put("user_id", userId); + + // 4. 构建工作流请求体 + Map requestBody = buildWorkflowRequest(config, parameters, userId); + + // 5. 执行工作流调用(带API调用记录) + return executeWorkflowCallWithRecord(config, requestBody, COZE_CHAT_CONFIG_KEY, userId, apiCall); + } catch (Exception e) { - log.error("发送消息失败", e); + log.error("发送消息失败: conversationId={}, messageId={}, error={}", + conversationId, messageId, e.getMessage(), e); updateApiCallFailure(apiCall, e.getMessage()); return "抱歉,AI服务暂时不可用,请稍后再试。"; } } + /** + * 创建聊天工作流API调用记录(包含conversationId和messageId) + * + * @param config AI配置 + * @param conversationId 会话ID + * @param messageId 消息ID + * @param userMessage 用户消息 + * @param userId 用户ID + * @return API调用记录 + */ + private CozeApiCall createChatWorkflowApiCallRecord(AiConfig config, String conversationId, + String messageId, String userMessage, String userId) { + CozeApiCall apiCall = CozeApiCall.builder() + .id(snowflakeIdGenerator.nextIdAsString()) + .conversationId(conversationId) + .messageId(messageId) + .workflowId(config.getWorkflowId()) + .botId(config.getBotId()) + .userId(userId) + .requestType("workflow") + .userMessage(userMessage) + .userMessageType("text") + .status("pending") + .startTime(LocalDateTime.now()) + .traceId(UUID.randomUUID().toString().replace("-", "")) + .metadata(JSON.toJSONString(Map.of("configKey", COZE_CHAT_CONFIG_KEY))) + .createBy(userId) + .updateBy(userId) + .build(); + + // 获取客户端信息 + try { + HttpServletRequest request = getCurrentRequest(); + if (request != null) { + apiCall.setClientIp(getClientIp(request)); + apiCall.setUserAgent(request.getHeader("User-Agent")); + apiCall.setSessionId(request.getSession().getId()); + } + } catch (Exception e) { + log.warn("获取客户端信息失败: {}", e.getMessage()); + } + + // 保存API调用记录 + cozeApiCallService.save(apiCall); + log.info("创建聊天工作流API调用记录: id={}, conversationId={}, messageId={}, workflowId={}, traceId={}", + apiCall.getId(), conversationId, messageId, config.getWorkflowId(), apiCall.getTraceId()); + + return apiCall; + } + /** * 执行Coze API调用的公共逻辑 */ diff --git a/web/src/stores/chat.ts b/web/src/stores/chat.ts index c582e7c..e5dfe19 100644 --- a/web/src/stores/chat.ts +++ b/web/src/stores/chat.ts @@ -59,6 +59,9 @@ export const useChatStore = defineStore('chat', () => { ) }) + // 已处理的消息ID集合 - 用于防止重复处理 + const processedMessageIds = ref>(new Set()) + // 处理消息队列 - 批量处理消息以避免竞态条件 const processMessageQueue = async () => { if (isProcessingQueue.value || messageQueue.value.length === 0) { @@ -70,11 +73,24 @@ export const useChatStore = defineStore('chat', () => { // 一次性取出所有待处理消息 const batch = messageQueue.value.splice(0, messageQueue.value.length) - // 批量添加消息到数组 - const newMessages = batch.map(item => item.message) - messages.value.push(...newMessages) + // 过滤掉已存在的消息(基于ID去重) + const newMessages = batch + .map(item => item.message) + .filter(msg => { + // 检查是否已存在于当前消息列表中 + const exists = messages.value.some(m => m.id === msg.id) + if (exists) { + console.log(`⚠️ 消息已存在,跳过: ${msg.id}`) + } + return !exists + }) - console.log(`✅ 消息队列处理完成,添加了 ${newMessages.length} 条消息,当前总数: ${messages.value.length}`) + if (newMessages.length > 0) { + messages.value.push(...newMessages) + console.log(`✅ 消息队列处理完成,添加了 ${newMessages.length} 条消息,当前总数: ${messages.value.length}`) + } else { + console.log(`⏭️ 队列中的消息都已存在,无需添加`) + } } finally { isProcessingQueue.value = false } @@ -82,6 +98,15 @@ export const useChatStore = defineStore('chat', () => { // 将消息加入队列而不是直接添加 const queueMessage = (message: ChatMessage) => { + // 先检查是否已处理过该消息ID + if (processedMessageIds.value.has(message.id)) { + console.log(`⚠️ 消息已处理过,跳过队列: ${message.id}`) + return + } + + // 标记消息ID为已处理 + processedMessageIds.value.add(message.id) + messageQueue.value.push({ message, timestamp: Date.now() @@ -90,11 +115,11 @@ export const useChatStore = defineStore('chat', () => { } // 添加消息 - 现在返回消息但不直接添加到数组 - const addMessage = (message: Omit) => { + const addMessage = (message: Omit & { id?: string, timestamp?: string }) => { const newMessage: ChatMessage = { ...message, - id: Date.now().toString(), - timestamp: new Date().toISOString(), + id: message.id || Date.now().toString(), // 优先使用传入的ID + timestamp: message.timestamp || new Date().toISOString(), // 优先使用传入的时间戳 status: message.type === 'user' ? 'sending' : 'sent' } // 将消息加入队列而不是直接添加 @@ -228,12 +253,17 @@ export const useChatStore = defineStore('chat', () => { // 如果需要过滤特定会话的消息,可以在这里添加过滤逻辑 // const sessionMessages = sortedMessages.filter(msg => msg.sessionId === sessionId) + // 清空并重新设置已处理消息ID集合 + processedMessageIds.value.clear() + sortedMessages.forEach(msg => processedMessageIds.value.add(msg.id)) + messages.value = sortedMessages - console.log('📨 会话消息加载完成,消息数量:', messages.value.length) + console.log('📨 会话消息加载完成,消息数量:', messages.value.length, '已处理ID数量:', processedMessageIds.value.size) } catch (error) { console.error('❌ 加载会话消息失败:', error) messages.value = [] + processedMessageIds.value.clear() } } @@ -261,6 +291,9 @@ export const useChatStore = defineStore('chat', () => { // 清空消息 const clearMessages = () => { messages.value = [] + // 同时清空已处理消息ID集合,避免后续加载时被错误过滤 + processedMessageIds.value.clear() + console.log('✅ 消息和已处理ID集合已清空') } // 搜索消息:支持本地搜索和远程搜索 @@ -390,31 +423,47 @@ export const useChatStore = defineStore('chat', () => { // 使用优化的排序和去重方法 const sortedMessages = MessageService.sortAndDeduplicateMessages(chatMessages) + // 清空并重新设置已处理消息ID集合 + processedMessageIds.value.clear() + sortedMessages.forEach(msg => processedMessageIds.value.add(msg.id)) + // 直接设置消息数组(初始加载时不使用队列) messages.value = sortedMessages - console.log('📝 最近消息已加载并排序,消息总数:', messages.value.length) + console.log('📝 最近消息已加载并排序,消息总数:', messages.value.length, '已处理ID数量:', processedMessageIds.value.size) return chatMessages } catch (error) { console.error('❌ 加载最近消息失败:', error) messages.value = [] + processedMessageIds.value.clear() return [] } } // 添加AI回复消息(使用队列处理) - const addAiReplyMessages = async (content: string) => { + const addAiReplyMessages = async (wsMessage: WebSocketMessage) => { // 停止输入状态 isTyping.value = false - // 添加AI消息到队列 + // 使用后端提供的messageId,确保去重能正常工作 + const messageId = wsMessage.messageId || Date.now().toString() + + // 检查是否已处理过该消息(基于后端messageId去重) + if (processedMessageIds.value.has(messageId)) { + console.log(`⚠️ AI消息已处理过,跳过: ${messageId}`) + return + } + + // 添加AI消息到队列,使用后端的messageId const aiMessage = addMessage({ - content: content.trim(), + id: messageId, + content: wsMessage.content.trim(), type: 'ai', - conversationId: currentSession.value?.id + conversationId: wsMessage.conversationId || currentSession.value?.id, + timestamp: wsMessage.createTime || new Date().toISOString() }) - console.log('✅ AI消息已加入队列,消息ID:', aiMessage.id) + console.log('✅ AI消息已加入队列,消息ID:', aiMessage.id, '(后端ID:', wsMessage.messageId, ')') // 立即处理队列 await processMessageQueue() @@ -422,13 +471,13 @@ export const useChatStore = defineStore('chat', () => { // WebSocket消息处理 - 使用队列处理所有消息 let handleWebSocketMessage = async (wsMessage: WebSocketMessage) => { - console.log('收到WebSocket消息:', wsMessage.type, wsMessage.senderType) + console.log('收到WebSocket消息:', wsMessage.type, wsMessage.senderType, '消息ID:', wsMessage.messageId) switch (wsMessage.type) { case 'TEXT': if (wsMessage.senderType === 'AI') { - // AI回复消息 - 加入队列处理 - await addAiReplyMessages(wsMessage.content) + // AI回复消息 - 传递完整的wsMessage以获取后端messageId + await addAiReplyMessages(wsMessage) } break @@ -443,8 +492,9 @@ export const useChatStore = defineStore('chat', () => { break case 'ERROR': - // 错误消息 - 加入队列处理 + // 错误消息 - 加入队列处理,使用后端messageId addMessage({ + id: wsMessage.messageId || Date.now().toString(), content: wsMessage.content, type: 'ai', sessionId: currentSession.value?.id @@ -453,8 +503,9 @@ export const useChatStore = defineStore('chat', () => { break case 'SYSTEM': - // 系统消息 - 加入队列处理 + // 系统消息 - 加入队列处理,使用后端messageId addMessage({ + id: wsMessage.messageId || Date.now().toString(), content: wsMessage.content, type: 'ai', sessionId: currentSession.value?.id