Bladeren bron

会话记录保存实现

Zhangbw 2 maanden geleden
bovenliggende
commit
956a2a2e01

+ 0 - 244
src/ChatDialog.vue

@@ -1,244 +0,0 @@
-<template>
-  <div class="chat-dialog">
-    <div class="chat-header">
-      <el-button text @click="$emit('close')">
-        <el-icon><ArrowLeft /></el-icon>
-        返回
-      </el-button>
-    </div>
-
-    <div class="chat-content">
-      <div v-for="(message, index) in messages" :key="index" :class="['message', message.type === 'ai' ? 'ai-message' : 'user-message']">
-        <div class="message-text">
-          {{ message.text }}
-          <div v-if="message.hasQuickQuestions" class="quick-questions">
-            <div class="question-item">😴 我最近总是失眠,有什么办法可以解决吗?</div>
-            <div class="question-item">🛏️ 怎么改善晚上睡不着、早上睡不醒的问题?</div>
-            <div class="question-item">🚀 超过半小时才睡着是正常的吗?</div>
-          </div>
-          <el-icon v-if="message.type === 'user' && isAiSpeaking" class="pause-icon"><VideoPlay /></el-icon>
-        </div>
-      </div>
-    </div>
-
-    <div class="chat-footer">
-      <el-button circle class="emoji-btn">
-        <el-icon><ChatDotRound /></el-icon>
-      </el-button>
-
-      <el-button circle class="mic-btn" :class="{ recording: isRecording }" @click="handleMicClick">
-        <el-icon><Microphone /></el-icon>
-      </el-button>
-
-      <div class="input-placeholder">
-        <span v-if="isRecording" class="recording-text">{{ currentTranscription || '正在录音...' }}</span>
-      </div>
-
-      <el-button circle class="hangup-btn" @click="handleHangup">
-        <el-icon><PhoneFilled /></el-icon>
-      </el-button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, watch } from 'vue'
-import { ArrowLeft, ChatDotRound, Microphone, PhoneFilled, VideoPlay } from '@element-plus/icons-vue'
-import { useStreamChat } from './composables/useStreamChat'
-import { useVoiceRecognition } from './composables/useVoiceRecognition'
-
-const emit = defineEmits(['close'])
-
-const { displayText, conversationId, sendMessage, stopAudio } = useStreamChat()
-const { isRecording, currentTranscription, startRecording, stopRecording } = useVoiceRecognition()
-
-const messages = ref([
-  {
-    type: 'ai',
-    text: '你好,我是你的腾讯云音视频 AI 助手!你可以问我:',
-    hasQuickQuestions: true
-  }
-])
-
-const isAiSpeaking = ref(false)
-
-// 监听displayText变化,实时更新消息
-watch(displayText, (newText) => {
-  if (newText) {
-    const lastMessage = messages.value[messages.value.length - 1]
-    if (lastMessage && lastMessage.type === 'ai' && !lastMessage.hasQuickQuestions) {
-      lastMessage.text = newText
-    } else {
-      messages.value.push({
-        type: 'ai',
-        text: newText
-      })
-    }
-    isAiSpeaking.value = true
-  }
-})
-
-// 处理语音识别
-const handleMicClick = async () => {
-  if (isRecording.value) {
-    stopRecording()
-    if (currentTranscription.value) {
-      await handleSendMessage(currentTranscription.value)
-    }
-  } else {
-    await startRecording()
-  }
-}
-
-// 发送消息
-const handleSendMessage = async (text) => {
-  messages.value.push({
-    type: 'user',
-    text
-  })
-
-  try {
-    await sendMessage(text, null, '1', [], false)
-    isAiSpeaking.value = false
-  } catch (error) {
-    console.error('发送消息失败:', error)
-  }
-}
-
-// 挂断电话
-const handleHangup = () => {
-  stopAudio()
-  emit('close')
-}
-</script>
-
-<style scoped>
-.chat-dialog {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: #f5f5f5;
-  display: flex;
-  flex-direction: column;
-  z-index: 1000;
-}
-
-.chat-header {
-  padding: 16px;
-  background: white;
-  border-bottom: 1px solid #e5e7eb;
-}
-
-.chat-content {
-  flex: 1;
-  overflow-y: auto;
-  padding: 20px;
-}
-
-.message {
-  margin-bottom: 16px;
-}
-
-.ai-message .message-text {
-  background: white;
-  padding: 16px;
-  border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-  max-width: 80%;
-}
-
-.user-message {
-  display: flex;
-  justify-content: flex-end;
-}
-
-.user-message .message-text {
-  background: white;
-  padding: 16px;
-  border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-  max-width: 80%;
-  position: relative;
-}
-
-.quick-questions {
-  margin-top: 12px;
-}
-
-.question-item {
-  padding: 8px 0;
-  font-size: 14px;
-  color: #333;
-}
-
-.pause-icon {
-  position: absolute;
-  right: 12px;
-  bottom: 12px;
-  color: #3b82f6;
-}
-
-.chat-footer {
-  padding: 16px;
-  background: white;
-  border-top: 1px solid #e5e7eb;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.emoji-btn,
-.mic-btn {
-  width: 40px;
-  height: 40px;
-  border: none;
-  background: transparent;
-}
-
-.input-placeholder {
-  flex: 1;
-  height: 40px;
-  border-bottom: 2px dotted #d1d5db;
-  display: flex;
-  align-items: center;
-  padding: 0 12px;
-}
-
-.recording-text {
-  color: #ef4444;
-  font-size: 14px;
-}
-
-.mic-btn.recording {
-  background: #ef4444;
-  color: white;
-  animation: pulse 1.5s infinite;
-}
-
-@keyframes pulse {
-  0%, 100% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0.5;
-  }
-}
-
-.hangup-btn {
-  width: 56px;
-  height: 56px;
-  background: #ef4444;
-  border: none;
-  color: white;
-}
-
-.hangup-btn:hover {
-  background: #dc2626;
-}
-
-:deep(.el-button) {
-  font-size: 16px;
-}
-</style>

+ 25 - 9
src/CustomerService.vue

@@ -218,7 +218,7 @@ const chatContent = ref(null)
 const { isRecording, currentTranscription, tempTranscription, startRecording, stopRecording } = useVoiceRecognition()
 
 // 流式聊天
-const { displayText, conversationId: streamConversationId, sendMessage: sendStreamMessage, stopAudio: stopStreamAudio } = useStreamChat()
+const { displayText, conversationId: streamConversationId, sendMessage: sendStreamMessage, stopAudio: stopStreamAudio, resetConversation } = useStreamChat()
 
 // 当前播放的音频对象
 const currentAudio = ref(null)
@@ -427,19 +427,19 @@ watch(showChat, async (newVal) => {
       waveformInterval = null
     }
 
-    // 重置状态为初始值
-    isTextInput.value = false
-    isMicMuted.value = false
-    currentConversationId.value = null
-
     // 对话结束,将客服状态改回空闲中
-    if (selectedAgent.value) {
+    if (selectedAgent.value && currentConversationId.value) {
       try {
+        const sessionId = currentConversationId.value
         await fetch(`http://localhost:8080/talk/agent/${selectedAgent.value}/hangup`, {
           method: 'POST',
-          headers: getHeaders()
+          headers: getHeaders(),
+          body: JSON.stringify({
+            sessionId: sessionId,
+            chatHistory: chatHistory.value
+          })
         })
-        console.log('客服状态已恢复为空闲中')
+        console.log('客服状态已恢复为空闲中,会话ID:', sessionId)
 
         // 立即刷新客服列表,让用户看到状态变化
         await fetchAgents(true)
@@ -447,6 +447,10 @@ watch(showChat, async (newVal) => {
         console.error('更新客服状态失败:', error)
       }
     }
+
+    // 清空 conversationId,确保下次开始对话时是新的会话
+    currentConversationId.value = null
+    resetConversation()
   }
 })
 
@@ -686,6 +690,10 @@ const startChat = async () => {
     return
   }
 
+  // 清空 conversationId,确保每次开始对话时都是新的会话
+  currentConversationId.value = null
+  resetConversation()
+
   try {
     // 调用对话前端的开始对话接口(带并发控制)
     const response = await fetch(`http://localhost:8080/talk/agent/${selectedAgent.value}/start`, {
@@ -703,6 +711,14 @@ const startChat = async () => {
       return
     }
 
+    // 保存后端返回的 sessionId
+    if (result.sessionId) {
+      currentConversationId.value = result.sessionId
+      // 同步到 useStreamChat 中的 conversationId
+      streamConversationId.value = result.sessionId
+      console.log('保存临时 sessionId:', result.sessionId)
+    }
+
     // 更新客服的TTS配置
     await fetch(`http://localhost:8080/talk/agent/${selectedAgent.value}`, {
       method: 'PUT',

+ 8 - 1
src/composables/useStreamChat.js

@@ -215,10 +215,17 @@ export function useStreamChat() {
     currentRequestId++
   }
 
+  const resetConversation = () => {
+    conversationId.value = ''
+    displayText.value = ''
+    audioQueue.value = []
+  }
+
   return {
     displayText,
     conversationId,
     sendMessage,
-    stopAudio
+    stopAudio,
+    resetConversation
   }
 }

+ 3 - 1
target/classes/META-INF/mps/autoMapper

@@ -1,6 +1,8 @@
 org.dromara.talk.domain.bo.TalkSessionBo
 org.dromara.talk.domain.vo.TalkAgentVo
-org.dromara.talk.domain.vo.TalkSessionVo
 org.dromara.talk.domain.bo.TalkAgentBo
+org.dromara.system.domain.bo.SysUserBo
+org.dromara.system.domain.vo.SysUserVo
+org.dromara.talk.domain.vo.TalkSessionVo
 org.dromara.talk.domain.vo.TalkUserVo
 org.dromara.talk.domain.bo.TalkUserBo

+ 10 - 4
target/classes/META-INF/mps/mappers

@@ -1,9 +1,15 @@
-org.dromara.talk.domain.TalkUserToTalkUserVoMapper
-org.dromara.talk.domain.TalkSessionToTalkSessionVoMapper
 org.dromara.talk.domain.bo.TalkAgentBoToTalkAgentMapper
 org.dromara.talk.domain.vo.TalkSessionVoToTalkSessionMapper
+org.dromara.system.domain.vo.SysUserVoToSysUserMapper
 org.dromara.talk.domain.vo.TalkUserVoToTalkUserMapper
+org.dromara.talk.domain.bo.TalkUserBoToTalkUserMapper
+org.dromara.system.domain.SysUserToSysUserVoMapper
+org.dromara.talk.domain.TalkUserToTalkUserVoMapper
+org.dromara.talk.domain.TalkSessionToTalkSessionVoMapper
+org.dromara.system.domain.bo.SysUserBoToSysUserMapper
+org.dromara.common.log.event.OperLogEventToSysOperLogBoMapper
+org.dromara.system.domain.bo.SysOperLogBoToSysOperLogMapper
 org.dromara.talk.domain.vo.TalkAgentVoToTalkAgentMapper
 org.dromara.talk.domain.TalkAgentToTalkAgentVoMapper
-org.dromara.talk.domain.bo.TalkSessionBoToTalkSessionMapper
-org.dromara.talk.domain.bo.TalkUserBoToTalkUserMapper
+org.dromara.system.domain.bo.SysOperLogBoToOperLogEventMapper
+org.dromara.talk.domain.bo.TalkSessionBoToTalkSessionMapper