workcase web ai聊天
This commit is contained in:
@@ -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-已归档
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
// ====================== 消息管理 ======================
|
// ====================== 消息管理 ======================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|
||||||
// ====================== 智能体聊天管理 ======================
|
// ====================== 智能体聊天管理 ======================
|
||||||
|
|
||||||
|
|||||||
@@ -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 = "更新聊天室")
|
||||||
|
|||||||
@@ -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}`
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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'
|
||||||
// ============================================
|
// ============================================
|
||||||
// 类型定义
|
// 类型定义
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user