workcase web ai聊天

This commit is contained in:
2025-12-23 16:56:22 +08:00
parent e75b2f3bab
commit 9dea8f3b2a
12 changed files with 430 additions and 88 deletions

View File

@@ -30,7 +30,7 @@ DROP TABLE IF EXISTS workcase.tb_chat_room CASCADE;
CREATE TABLE workcase.tb_chat_room( CREATE TABLE workcase.tb_chat_room(
optsn VARCHAR(50) NOT NULL, -- 流水号 optsn VARCHAR(50) NOT NULL, -- 流水号
room_id VARCHAR(50) NOT NULL, -- 聊天室ID room_id VARCHAR(50) NOT NULL, -- 聊天室ID
workcase_id VARCHAR(50) NOT NULL, -- 关联工单ID workcase_id VARCHAR(50) DEFAULT NULL, -- 关联工单ID
room_name VARCHAR(200) NOT NULL, -- 聊天室名称(如:工单#12345的客服支持 room_name VARCHAR(200) NOT NULL, -- 聊天室名称(如:工单#12345的客服支持
room_type VARCHAR(20) NOT NULL DEFAULT 'workcase', -- 聊天室类型workcase-工单客服 room_type VARCHAR(20) NOT NULL DEFAULT 'workcase', -- 聊天室类型workcase-工单客服
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 状态active-活跃 closed-已关闭 archived-已归档 status VARCHAR(20) NOT NULL DEFAULT 'active', -- 状态active-活跃 closed-已关闭 archived-已归档

View File

@@ -16,6 +16,8 @@ import org.xyzh.api.ai.service.AgentChatService;
import org.xyzh.common.auth.utils.LoginUtil; import org.xyzh.common.auth.utils.LoginUtil;
import org.xyzh.common.core.domain.LoginDomain; import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.utils.NonUtils; import org.xyzh.common.utils.NonUtils;
import org.xyzh.common.utils.validation.ValidationParam; import org.xyzh.common.utils.validation.ValidationParam;
import org.xyzh.common.utils.validation.ValidationResult; import org.xyzh.common.utils.validation.ValidationResult;
@@ -128,7 +130,7 @@ public class ChatController {
* @author yslg * @author yslg
* @since 2025-12-17 * @since 2025-12-17
*/ */
@GetMapping("/conversations") @PostMapping("/conversation/list")
public ResultDomain<TbChat> getChatList(@RequestBody TbChat filter, @RequestHeader("Authorization") String token) { public ResultDomain<TbChat> getChatList(@RequestBody TbChat filter, @RequestHeader("Authorization") String token) {
log.info("获取会话列表: agentId={}", filter.getAgentId()); log.info("获取会话列表: agentId={}", filter.getAgentId());
@@ -142,6 +144,26 @@ public class ChatController {
return chatService.getChatList(filter); return chatService.getChatList(filter);
} }
/**
* @description 分页获取对话列表
* @param pageRequest 分页请求参数
* @author yslg
* @since 2025-12-17
*/
@PostMapping("/conversation/page")
public ResultDomain<PageDomain<TbChat>> getChatPage(@RequestBody PageRequest<TbChat> pageRequest, @RequestHeader("Authorization") String token) {
log.info("分页获取会话列表: agentId={}", pageRequest.getFilter().getAgentId());
pageRequest.getFilter().setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
pageRequest.getFilter().setUserType(true);
}
}
return chatService.getChatPage(pageRequest);
}
// ====================== 消息管理 ====================== // ====================== 消息管理 ======================
/** /**

View File

@@ -22,6 +22,9 @@ import org.xyzh.api.ai.dto.TbChatMessage;
import org.xyzh.api.ai.service.AgentChatService; import org.xyzh.api.ai.service.AgentChatService;
import org.xyzh.api.ai.service.AgentService; import org.xyzh.api.ai.service.AgentService;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.redis.service.RedisService; import org.xyzh.common.redis.service.RedisService;
import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.id.IdUtil;
import org.xyzh.common.auth.utils.LoginUtil; import org.xyzh.common.auth.utils.LoginUtil;
@@ -200,6 +203,30 @@ public class AgentChatServiceImpl implements AgentChatService {
return ResultDomain.success("查询成功", chatList); return ResultDomain.success("查询成功", chatList);
} }
@Override
public ResultDomain<PageDomain<TbChat>> getChatPage(PageRequest<TbChat> pageRequest) {
TbChat filter = pageRequest.getFilter();
// 判断agent是否是outer来客才需要校验
if (!filter.getUserType() && !isOuterAgent(filter.getAgentId())) {
return ResultDomain.<PageDomain<TbChat>>failure("智能体不可用");
}
// 获取用户ID
String userId = getUserIdByType(filter);
if (userId == null) {
return ResultDomain.<PageDomain<TbChat>>failure("用户信息获取失败");
}
filter.setUserId(userId);
// 分页查询
PageParam pageParam = pageRequest.getPageParam();
List<TbChat> chatList = chatMapper.selectChatPage(filter, pageParam);
long total = chatMapper.countChats(filter);
pageParam.setTotal((int) total);
PageDomain<TbChat> pageDomain = new PageDomain<>(pageParam, chatList);
return ResultDomain.<PageDomain<TbChat>>success("查询成功", pageDomain);
}
// ====================== 智能体聊天管理 ====================== // ====================== 智能体聊天管理 ======================
@Override @Override

View File

@@ -5,6 +5,8 @@ import org.xyzh.api.ai.dto.ChatPrepareData;
import org.xyzh.api.ai.dto.TbChat; import org.xyzh.api.ai.dto.TbChat;
import org.xyzh.api.ai.dto.TbChatMessage; import org.xyzh.api.ai.dto.TbChatMessage;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageRequest;
public interface AgentChatService { public interface AgentChatService {
@@ -41,6 +43,13 @@ public interface AgentChatService {
*/ */
ResultDomain<TbChat> getChatList(TbChat filter); ResultDomain<TbChat> getChatList(TbChat filter);
/**
* 分页获取会话列表
* @param pageRequest 分页请求参数
* @return 分页会话列表
*/
ResultDomain<PageDomain<TbChat>> getChatPage(PageRequest<TbChat> pageRequest);
// ====================== 智能体聊天管理 ====================== // ====================== 智能体聊天管理 ======================

View File

@@ -68,7 +68,12 @@ public class WorkcaseChatContorller {
if (!vr.isValid()) { if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors()); return ResultDomain.failure(vr.getAllErrors());
} }
chatRoom.setCreator(chatRoom.getGuestId());
try {
return chatRoomService.createChatRoom(chatRoom); return chatRoomService.createChatRoom(chatRoom);
} catch (Exception e) {
return ResultDomain.failure(e.getMessage());
}
} }
@Operation(summary = "更新聊天室") @Operation(summary = "更新聊天室")

View File

@@ -1,4 +1,5 @@
import { api } from '@/api/index' import { api } from '@/api/index'
import { API_BASE_URL } from '@/config'
import type { ResultDomain } from '@/types' import type { ResultDomain } from '@/types'
import type { TbChat, TbChatMessage, ChatPrepareData, StopChatParam, CommentMessageParam, DifyFileInfo } from '@/types/ai' import type { TbChat, TbChatMessage, ChatPrepareData, StopChatParam, CommentMessageParam, DifyFileInfo } from '@/types/ai'
@@ -37,7 +38,7 @@ export const aiChatAPI = {
* @param chat 会话信息 * @param chat 会话信息
*/ */
async deleteChat(chat: TbChat): Promise<ResultDomain<TbChat>> { async deleteChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
const response = await api.delete<TbChat>(`${this.baseUrl}/conversation`, { data: chat }) const response = await api.delete<TbChat>(`${this.baseUrl}/conversation`, chat )
return response.data return response.data
}, },
@@ -46,7 +47,7 @@ export const aiChatAPI = {
* @param filter 筛选条件 * @param filter 筛选条件
*/ */
async getChatList(filter: TbChat): Promise<ResultDomain<TbChat>> { async getChatList(filter: TbChat): Promise<ResultDomain<TbChat>> {
const response = await api.get<TbChat>(`${this.baseUrl}/conversations`, { data: filter }) const response = await api.post<TbChat>(`${this.baseUrl}/conversation/list`, filter)
return response.data return response.data
}, },
@@ -77,7 +78,7 @@ export const aiChatAPI = {
* @param sessionId 会话ID * @param sessionId 会话ID
*/ */
getStreamChatUrl(sessionId: string): string { getStreamChatUrl(sessionId: string): string {
return `${this.baseUrl}/stream?sessionId=${sessionId}` return `${API_BASE_URL}${this.baseUrl}/stream?sessionId=${sessionId}`
}, },
/** /**

View File

@@ -22,7 +22,7 @@
* Base64 编码的 32 字节密钥256 位) * Base64 编码的 32 字节密钥256 位)
*/ */
export const AES_SECRET_KEY = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=' // Base64 编码,解码后是 "12345678901234567890123456789012" (32字节) export const AES_SECRET_KEY = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=' // Base64 编码,解码后是 "12345678901234567890123456789012" (32字节)
export const AGENT_ID = '17664699513920001'
// ============================================ // ============================================
// 类型定义 // 类型定义
// ============================================ // ============================================

View File

@@ -108,8 +108,15 @@ declare module 'shared/types' {
TbChatMessage, TbChatMessage,
DifyFileInfo, DifyFileInfo,
ChatPrepareData, ChatPrepareData,
StopChatParams, CreateChatParam,
CommentMessageParams PrepareChatParam,
StopChatParam,
CommentMessageParam,
ChatListParam,
ChatMessageListParam,
SSEMessageData,
SSECallbacks,
SSETask
} from '../../../shared/src/types/ai' } from '../../../shared/src/types/ai'
// 重新导出 menu // 重新导出 menu

View File

@@ -365,6 +365,20 @@ $brand-color-hover: #004488;
margin: 0 auto; margin: 0 auto;
padding: 24px 16px; padding: 24px 16px;
.chat-header {
text-align: center;
padding: 16px 0 24px;
border-bottom: 1px solid #f1f5f9;
margin-bottom: 24px;
.chat-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
}
.message-row { .message-row {
display: flex; display: flex;
gap: 12px; gap: 12px;
@@ -421,6 +435,31 @@ $brand-color-hover: #004488;
word-wrap: break-word; word-wrap: break-word;
} }
.typing-cursor {
display: inline-block;
animation: blink 0.8s infinite;
color: $brand-color;
font-weight: bold;
}
.loading-dots {
display: flex;
gap: 4px;
padding: 4px 0;
span {
width: 6px;
height: 6px;
background: #94a3b8;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
&:nth-child(3) { animation-delay: 0s; }
}
}
.message-time { .message-time {
font-size: 12px; font-size: 12px;
color: #94a3b8; color: #94a3b8;
@@ -429,6 +468,16 @@ $brand-color-hover: #004488;
} }
} }
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
// ==================== 快捷命令栏 ==================== // ==================== 快捷命令栏 ====================
.quick-bar { .quick-bar {
padding: 8px 16px; padding: 8px 16px;

View File

@@ -8,10 +8,10 @@
<div class="history-icons"> <div class="history-icons">
<button <button
v-for="conv in chatHistory.slice(0, 8)" v-for="conv in chatHistory.slice(0, 8)"
:key="conv.id" :key="conv.chatId"
@click="loadChat(conv.id); toggleHistory()" @click="loadChat(conv.chatId!); toggleHistory()"
class="history-icon-btn" class="history-icon-btn"
:class="{ active: currentChatId === conv.id }" :class="{ active: currentChatId === conv.chatId }"
:title="conv.title" :title="conv.title"
> >
<MessageCircle :size="16" /> <MessageCircle :size="16" />
@@ -52,19 +52,19 @@
</div> </div>
<div <div
v-for="conv in chatHistory" v-for="conv in chatHistory"
:key="conv.id" :key="conv.chatId"
@click="loadChat(conv.id)" @click="loadChat(conv.chatId!)"
class="conversation-item" class="conversation-item"
:class="{ active: currentChatId === conv.id }" :class="{ active: currentChatId === conv.chatId }"
> >
<MessageCircle :size="16" class="conv-icon" /> <MessageCircle :size="16" class="conv-icon" />
<div class="conv-info"> <div class="conv-info">
<div class="conv-title">{{ conv.title }}</div> <div class="conv-title">{{ conv.title || '新对话' }}</div>
<div class="conv-time">{{ conv.time }}</div> <div class="conv-time">{{ formatTime(conv.createTime) }}</div>
</div> </div>
<button <button
class="delete-btn" class="delete-btn"
@click.stop="deleteConversation(conv.id)" @click.stop="deleteConversation(conv.chatId!)"
title="删除" title="删除"
> >
<Trash2 :size="14" /> <Trash2 :size="14" />
@@ -104,9 +104,12 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 消息列表 --> <!-- 消息列表 -->
<div v-else class="messages-list"> <div v-else class="messages-list">
<!-- 对话标题 -->
<div v-if="currentChatTitle" class="chat-header">
<h2 class="chat-title">{{ currentChatTitle }}</h2>
</div>
<div <div
v-for="msg in messages" v-for="msg in messages"
:key="msg.id" :key="msg.id"
@@ -121,7 +124,13 @@
<!-- 消息内容 --> <!-- 消息内容 -->
<div class="message-bubble" :class="msg.role"> <div class="message-bubble" :class="msg.role">
<p class="message-text">{{ msg.text }}</p> <p class="message-text">
{{ msg.text }}
<span v-if="isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1" class="typing-cursor">|</span>
</p>
<div v-if="!msg.text && isStreaming && msg.role === 'assistant'" class="loading-dots">
<span></span><span></span><span></span>
</div>
<div class="message-time">{{ msg.time }}</div> <div class="message-time">{{ msg.time }}</div>
</div> </div>
</div> </div>
@@ -185,7 +194,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick } from 'vue' import { ref, nextTick, onMounted, computed } from 'vue'
import { import {
Plus, Plus,
MessageCircle, MessageCircle,
@@ -202,38 +211,53 @@ import {
User, User,
Headphones Headphones
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { aiChatAPI, agentAPI } from 'shared/api/ai'
import type {
TbChat,
TbChatMessage,
TbAgent,
PrepareChatParam,
SSEMessageData,
DifyFileInfo
} from 'shared/types'
import { AGENT_ID } from '@/config'
interface Message { // 显示用消息接口
id: number interface DisplayMessage {
id: string
role: 'user' | 'assistant' role: 'user' | 'assistant'
text: string text: string
time: string time: string
messageId?: string
} }
interface Conversation { // 用户信息TODO: 从实际用户store获取
id: number const userId = computed(()=>{
title: string return JSON.parse(localStorage.getItem("loginDomain")||'{}').user.userId;
time: string })
} const userType = ref<boolean>(true) // 系统内部人员
// 智能体信息
const currentAgent = ref<TbAgent | null>(null)
const agentId = AGENT_ID
// 历史对话展开状态 // 历史对话展开状态
const isHistoryOpen = ref(false) const isHistoryOpen = ref(false)
// 当前选中的对话ID // 当前选中的对话ID
const currentChatId = ref<number | null>(null) const currentChatId = ref<string | null>(null)
// 历史对话列表 // 历史对话列表
const chatHistory = ref<Conversation[]>([ const chatHistory = ref<TbChat[]>([])
{ id: 1, title: '发电机故障咨询', time: '今天 10:30' }, const currentChatTitle = ref<string>('')
{ id: 2, title: '设备维保规范查询', time: '今天 09:15' },
{ id: 3, title: '配件更换流程', time: '昨天 16:42' },
{ id: 4, title: 'TH-500GF参数查询', time: '昨天 14:20' },
{ id: 5, title: '巡检报告模板', time: '12月10日' },
{ id: 6, title: '客户投诉处理流程', time: '12月09日' }
])
// 聊天消息列表 // 聊天消息列表
const messages = ref<Message[]>([]) const messages = ref<DisplayMessage[]>([])
// 流式对话状态
const isStreaming = ref(false)
const currentTaskId = ref<string>('')
const eventSource = ref<EventSource | null>(null)
// 输入框文本 // 输入框文本
const inputText = ref('') const inputText = ref('')
@@ -256,33 +280,71 @@ const toggleHistory = () => {
} }
// 开始新对话 // 开始新对话
const startNewChat = () => { const startNewChat = async () => {
currentChatId.value = null currentChatId.value = null
currentChatTitle.value = ''
messages.value = [] messages.value = []
inputText.value = '' inputText.value = ''
// 创建新会话
if (agentId && userId.value) {
try {
const result = await aiChatAPI.createChat({
agentId: agentId,
userId: userId.value,
userType: userType.value
})
if (result.success && result.data) {
currentChatId.value = result.data.chatId || null
}
} catch (error) {
console.error('创建会话失败:', error)
}
}
} }
// 加载历史对话 // 加载历史对话
const loadChat = (chatId: number) => { const loadChat = async (chatId: string) => {
currentChatId.value = chatId currentChatId.value = chatId
// 模拟加载历史对话 messages.value = []
messages.value = [
{ // 设置当前对话标题
id: 1, const chat = chatHistory.value.find((c: TbChat) => c.chatId === chatId)
role: 'assistant', currentChatTitle.value = chat?.title || '新对话'
text: '您好,欢迎使用泰豪智能客服,请问有什么可以帮您的?',
time: '10:00' try {
const result = await aiChatAPI.getMessageList({ chatId })
if (result.success && result.dataList) {
const messageList = Array.isArray(result.dataList) ? result.dataList : [result.dataList]
messages.value = messageList.map((msg: TbChatMessage) => ({
id: msg.messageId || String(Date.now()),
role: msg.role === 'user' ? 'user' : 'assistant',
text: msg.content || '',
time: formatTime(msg.createTime),
messageId: msg.messageId
} as DisplayMessage))
} }
] } catch (error) {
console.error('加载对话消息失败:', error)
}
scrollToBottom()
} }
// 删除对话 // 删除对话
const deleteConversation = (convId: number) => { const deleteConversation = async (chatId: string) => {
chatHistory.value = chatHistory.value.filter(c => c.id !== convId) try {
if (currentChatId.value === convId) { const result = await aiChatAPI.deleteChat({ chatId })
if (result.success) {
chatHistory.value = chatHistory.value.filter((c: TbChat) => c.chatId !== chatId)
if (currentChatId.value === chatId) {
startNewChat() startNewChat()
} }
} }
} catch (error) {
console.error('删除对话失败:', error)
}
}
// 滚动到底部 // 滚动到底部
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -295,17 +357,21 @@ const scrollToBottom = () => {
// 发送消息 // 发送消息
const sendMessage = async () => { const sendMessage = async () => {
if (!inputText.value.trim()) return if (!inputText.value.trim() || isStreaming.value) return
if (!agentId) {
console.error('未选择智能体')
return
}
const userMessage: Message = { const query = inputText.value.trim()
id: Date.now(), const userMessage: DisplayMessage = {
id: String(Date.now()),
role: 'user', role: 'user',
text: inputText.value.trim(), text: query,
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
} }
messages.value.push(userMessage) messages.value.push(userMessage)
const query = inputText.value
inputText.value = '' inputText.value = ''
// 重置输入框高度 // 重置输入框高度
@@ -315,29 +381,151 @@ const sendMessage = async () => {
scrollToBottom() scrollToBottom()
// 模拟 AI 回复 // 如果没有当前会话,先创建
setTimeout(() => { if (!currentChatId.value) {
const assistantMessage: Message = { try {
id: Date.now() + 1, const createResult = await aiChatAPI.createChat({
agentId: agentId,
userId: userId.value,
userType: userType.value,
title: query.slice(0, 20) + (query.length > 20 ? '...' : '')
})
if (createResult.success && createResult.data) {
currentChatId.value = createResult.data.chatId || null
chatHistory.value.unshift(createResult.data)
}
} catch (error) {
console.error('创建会话失败:', error)
return
}
}
// 准备流式对话参数
const prepareParam: PrepareChatParam = {
chatId: currentChatId.value!,
query: query,
agentId: agentId,
userType: userType.value,
userId: userId.value
}
try {
// 准备流式对话
const prepareResult = await aiChatAPI.prepareStreamChat(prepareParam)
if (!prepareResult.success || !prepareResult.data) {
throw new Error('准备流式对话失败')
}
const sessionId = prepareResult.data
// 创建AI回复消息占位
const assistantMessage: DisplayMessage = {
id: String(Date.now() + 1),
role: 'assistant', role: 'assistant',
text: `感谢您的咨询!关于"${query}",我正在为您查询相关信息...\n\n这是一个模拟回复。在实际应用中这里会连接到后端AI服务获取真实回答。`, text: '',
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
} }
messages.value.push(assistantMessage) messages.value.push(assistantMessage)
// 保存到对话列表 // 开始流式对话
if (!currentChatId.value) { isStreaming.value = true
const newConv: Conversation = { eventSource.value = aiChatAPI.createStreamChat(sessionId)
id: Date.now(),
title: query.slice(0, 20) + (query.length > 20 ? '...' : ''), const es = eventSource.value
time: '刚刚' if (!es) return
}
chatHistory.value.unshift(newConv) es.onmessage = (event) => {
currentChatId.value = newConv.id try {
console.log('SSE原始数据:', event.data)
const data = JSON.parse(event.data)
console.log('SSE解析数据:', data)
if (data.task_id) {
currentTaskId.value = data.task_id
} }
// 支持多种事件格式
if (data.event === 'message' && data.answer) {
// 更新最后一条消息
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg && lastMsg.role === 'assistant') {
lastMsg.text += data.answer
}
scrollToBottom() scrollToBottom()
}, 1000) } else if (data.event === 'message_end' || data.event === 'workflow_finished') {
closeEventSource()
} else if (data.event === 'error') {
console.error('SSE错误:', data.message)
closeEventSource()
}
} catch (e) {
console.error('解析SSE数据失败:', e, event.data)
}
}
es.onerror = () => {
closeEventSource()
}
} catch (error) {
console.error('发送消息失败:', error)
isStreaming.value = false
}
}
// 关闭EventSource
const closeEventSource = () => {
if (eventSource.value) {
eventSource.value.close()
eventSource.value = null
}
isStreaming.value = false
currentTaskId.value = ''
}
// 停止对话
const stopChat = async () => {
if (currentTaskId.value && agentId && userId.value) {
try {
await aiChatAPI.stopChat({
taskId: currentTaskId.value,
agentId: agentId,
userId: userId.value
})
} catch (error) {
console.error('停止对话失败:', error)
}
}
closeEventSource()
}
// 加载对话列表
const loadChatHistory = async () => {
if (!userId.value) return
try {
const result = await aiChatAPI.getChatList({
agentId: agentId
})
if (result.success && result.dataList) {
chatHistory.value = result.dataList
}
} catch (error) {
console.error('加载对话列表失败:', error)
}
}
// 格式化时间
const formatTime = (time?: string | number): string => {
if (!time) return ''
const date = new Date(time)
const now = new Date()
const isToday = date.toDateString() === now.toDateString()
if (isToday) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
} }
// 处理快捷命令 // 处理快捷命令
@@ -362,6 +550,19 @@ const handleKeyDown = (e: KeyboardEvent) => {
sendMessage() sendMessage()
} }
} }
// 组件挂载
onMounted(async () => {
// TODO: 根据路由参数或配置获取智能体ID
// 示例:从路由获取 agentId
// const route = useRoute()
// if (route.query.agentId) {
// await loadAgent(route.query.agentId as string)
// }
// 加载对话历史
await loadChatHistory()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -145,7 +145,7 @@ const userId = ref(localStorage.getItem('userId') || '')
// 搜索文本 // 搜索文本
const searchText = ref('') const searchText = ref('')
const userType = true //web端固定这个
// 加载状态 // 加载状态
const loading = ref(false) const loading = ref(false)
const messageLoading = ref(false) const messageLoading = ref(false)

View File

@@ -118,7 +118,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue' import { ref, nextTick, onMounted } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue' import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import { guestAPI, aiChatAPI } from '@/api' import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
import type { TbWorkcaseDTO } from '@/types' import type { TbWorkcaseDTO } from '@/types'
import { AGENT_ID } from '@/config' import { AGENT_ID } from '@/config'
// 前端消息展示类型 // 前端消息展示类型
@@ -478,22 +478,43 @@
}) })
} }
// 联系人工客服 // 联系人工客服 - 创建聊天室并进入
function contactHuman() { async function contactHuman() {
uni.showModal({ uni.showLoading({ title: '正在连接客服...' })
title: '联系人工客服', try {
content: '客服电话400-123-4567\n工作时间9:00-18:00\n\n是否拨打电话', // 创建聊天室
confirmText: '拨打', const res = await workcaseChatAPI.createChatRoom({
cancelText: '取消', guestId: userInfo.value.userId || userInfo.value.wechatId,
success: (res) => { guestName: userInfo.value.username || '访客',
if (res.confirm) { roomName: `${userInfo.value.username || '访客'}的咨询`,
uni.makePhoneCall({ roomType: 'guest',
phoneNumber: '400-123-4567' status: 'active',
aiSessionId: chatId.value || ''
})
uni.hideLoading()
if (res.success && res.data) {
const roomId = res.data.roomId
console.log('创建聊天室成功:', roomId)
// 跳转到聊天室页面
uni.navigateTo({
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${roomId}&roomName=${encodeURIComponent(res.data.roomName || '人工客服')}`
})
} else {
uni.showToast({
title: res.message || '连接客服失败',
icon: 'none'
}) })
} }
} } catch (error: any) {
uni.hideLoading()
console.error('创建聊天室失败:', error)
uni.showToast({
title: '连接客服失败,请稍后重试',
icon: 'none'
}) })
} }
}
// 处理快速问题 // 处理快速问题
async function handleQuickQuestion(question : string) { async function handleQuickQuestion(question : string) {