ai对话
This commit is contained in:
194
urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts
Normal file
194
urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { request } from '../base'
|
||||
import type { ResultDomain } from '../../types'
|
||||
import type {
|
||||
TbChat,
|
||||
TbChatMessage,
|
||||
CreateChatParam,
|
||||
PrepareChatParam,
|
||||
StopChatParam,
|
||||
CommentMessageParam,
|
||||
ChatListParam,
|
||||
ChatMessageListParam,
|
||||
SSECallbacks,
|
||||
SSETask,
|
||||
SSEMessageData
|
||||
} from '../../types/ai/aiChat'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare const uni: {
|
||||
getStorageSync: (key: string) => any
|
||||
request: (options: any) => any
|
||||
}
|
||||
|
||||
// API 基础配置
|
||||
const BASE_URL = 'http://localhost:8180'
|
||||
|
||||
/**
|
||||
* @description AI对话相关接口(直接调用ai模块)
|
||||
* @filename aiChat.ts
|
||||
* @author cascade
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-23
|
||||
*/
|
||||
export const aiChatAPI = {
|
||||
baseUrl: '/urban-lifeline/ai/chat',
|
||||
|
||||
// ====================== AI对话管理 ======================
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
* @param param agentId、userId、title 必传
|
||||
*/
|
||||
createChat(param: CreateChatParam): Promise<ResultDomain<TbChat>> {
|
||||
return request<TbChat>({ url: `${this.baseUrl}/conversation`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新对话
|
||||
* @param chat agentId、userId、title、userType 必传
|
||||
*/
|
||||
updateChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
return request<TbChat>({ url: `${this.baseUrl}/conversation`, method: 'PUT', data: chat })
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询对话列表
|
||||
* @param param agentId 必传
|
||||
*/
|
||||
getChatList(param: ChatListParam): Promise<ResultDomain<TbChat[]>> {
|
||||
return request<TbChat[]>({ url: `${this.baseUrl}/conversations`, method: 'GET', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取对话消息列表
|
||||
* @param param agentId、chatId、userId 必传
|
||||
*/
|
||||
getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
|
||||
return request<TbChatMessage[]>({ url: `${this.baseUrl}/messages`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 准备流式对话会话
|
||||
* @param param agentId、chatId、query、userId 必传
|
||||
*/
|
||||
prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
|
||||
return request<string>({ url: `${this.baseUrl}/stream/prepare`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 流式对话(SSE)- 返回EventSource URL
|
||||
*/
|
||||
getStreamUrl(sessionId: string): string {
|
||||
return `${this.baseUrl}/stream?sessionId=${sessionId}`
|
||||
},
|
||||
|
||||
/**
|
||||
* 建立SSE流式对话连接
|
||||
* @param sessionId 会话ID(必传)
|
||||
* @param callbacks 回调函数
|
||||
* @returns SSETask 可用于中止请求
|
||||
*/
|
||||
streamChat(sessionId: string, callbacks: SSECallbacks): SSETask {
|
||||
const url = `${BASE_URL}${this.baseUrl}/stream?sessionId=${sessionId}`
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
|
||||
const requestTask = uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Accept': 'text/event-stream',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
enableChunked: true,
|
||||
success: (res: any) => {
|
||||
console.log('SSE请求完成:', res)
|
||||
// 处理非200状态码
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('SSE请求状态码异常:', res.statusCode)
|
||||
let errorMsg = '抱歉,服务暂时不可用,请稍后重试。'
|
||||
if (res.statusCode === 401) {
|
||||
errorMsg = '登录已过期,请重新登录。'
|
||||
} else if (res.statusCode === 403) {
|
||||
errorMsg = '无权限访问,请联系管理员。'
|
||||
} else if (res.statusCode === 404) {
|
||||
errorMsg = '会话不存在或已过期,请重新发起对话。'
|
||||
} else if (res.statusCode >= 500) {
|
||||
errorMsg = '服务器异常,请稍后重试。'
|
||||
}
|
||||
callbacks.onError?.(errorMsg)
|
||||
}
|
||||
callbacks.onComplete?.()
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('SSE请求失败:', err)
|
||||
callbacks.onError?.('网络连接失败,请稍后重试。')
|
||||
callbacks.onComplete?.()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听分块数据
|
||||
requestTask.onChunkReceived((res: any) => {
|
||||
try {
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const text = decoder.decode(new Uint8Array(res.data))
|
||||
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
if (dataStr && dataStr !== '[DONE]') {
|
||||
try {
|
||||
const data: SSEMessageData = JSON.parse(dataStr)
|
||||
const event = data.event
|
||||
|
||||
if (event === 'message' || event === 'agent_message') {
|
||||
callbacks.onMessage?.(data)
|
||||
} else if (event === 'message_end') {
|
||||
callbacks.onEnd?.(data.task_id || '')
|
||||
} else if (event === 'error' || data.message) {
|
||||
// 解析错误消息,提取友好提示
|
||||
let errorMsg = data.message || '发生错误,请稍后重试。'
|
||||
// 处理嵌套的 JSON 错误信息
|
||||
if (errorMsg.includes('invalid_param')) {
|
||||
errorMsg = '请求参数错误,请稍后重试。'
|
||||
} else if (errorMsg.includes('rate_limit')) {
|
||||
errorMsg = '请求过于频繁,请稍后再试。'
|
||||
} else if (errorMsg.includes('quota_exceeded')) {
|
||||
errorMsg = 'AI 服务额度已用完,请联系管理员。'
|
||||
}
|
||||
callbacks.onError?.(errorMsg)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('解析SSE数据失败:', dataStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理分块数据失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
abort: () => {
|
||||
requestTask.abort()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止对话
|
||||
* @param param taskId、agentId、userId 必传
|
||||
*/
|
||||
stopChat(param: StopChatParam): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/stop`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 评价对话消息
|
||||
* @param param agentId、chatId、messageId、comment、userId 必传
|
||||
*/
|
||||
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/comment`, method: 'POST', data: param })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { aiChatAPI } from './aiChat'
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./base"
|
||||
export * from "./sys"
|
||||
export * from "./workcase"
|
||||
export * from "./workcase"
|
||||
export * from "./ai"
|
||||
@@ -11,26 +11,30 @@ import type {
|
||||
ChatMemberVO,
|
||||
ChatRoomMessageVO,
|
||||
CustomerServiceVO
|
||||
} from '../../types/workcase/chatRoom'
|
||||
} from '../../types/workcase'
|
||||
import type {
|
||||
TbChat,
|
||||
TbChatMessage,
|
||||
CreateChatParam,
|
||||
PrepareChatParam,
|
||||
StopChatParam,
|
||||
CommentMessageParam,
|
||||
ChatListParam,
|
||||
ChatMessageListParam,
|
||||
SSECallbacks,
|
||||
SSETask,
|
||||
SSEMessageData
|
||||
} from '../../types/ai/aiChat'
|
||||
|
||||
// AI对话相关类型(简化版)
|
||||
interface TbChat {
|
||||
chatId?: string
|
||||
userId?: string
|
||||
title?: string
|
||||
status?: string
|
||||
}
|
||||
interface TbChatMessage {
|
||||
messageId?: string
|
||||
chatId?: string
|
||||
content?: string
|
||||
role?: string
|
||||
}
|
||||
interface ChatPrepareData {
|
||||
chatId?: string
|
||||
message?: string
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare const uni: {
|
||||
getStorageSync: (key: string) => any
|
||||
request: (options: any) => any
|
||||
}
|
||||
|
||||
// API 基础配置
|
||||
const BASE_URL = 'http://localhost:8180'
|
||||
|
||||
/**
|
||||
* @description 工单对话相关接口
|
||||
* @filename workcaseChat.ts
|
||||
@@ -45,9 +49,10 @@ export const workcaseChatAPI = {
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
* @param param agentId和userId必传
|
||||
*/
|
||||
createChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
return request<TbChat>({ url: this.baseUrl, method: 'POST', data: chat })
|
||||
createChat(param: CreateChatParam): Promise<ResultDomain<TbChat>> {
|
||||
return request<TbChat>({ url: this.baseUrl, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -59,23 +64,26 @@ export const workcaseChatAPI = {
|
||||
|
||||
/**
|
||||
* 查询对话列表
|
||||
* @param param userId必传
|
||||
*/
|
||||
getChatList(filter: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
return request<TbChat>({ url: `${this.baseUrl}/list`, method: 'POST', data: filter })
|
||||
getChatList(param: ChatListParam): Promise<ResultDomain<TbChat[]>> {
|
||||
return request<TbChat[]>({ url: `${this.baseUrl}/list`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取对话消息列表
|
||||
* @param param chatId必传
|
||||
*/
|
||||
getChatMessageList(filter: TbChat): Promise<ResultDomain<TbChatMessage>> {
|
||||
return request<TbChatMessage>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: filter })
|
||||
getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
|
||||
return request<TbChatMessage[]>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 准备对话会话
|
||||
* 准备流式对话会话
|
||||
* @param param chatId和message必传
|
||||
*/
|
||||
prepareChatMessageSession(prepareData: ChatPrepareData): Promise<ResultDomain<string>> {
|
||||
return request<string>({ url: `${this.baseUrl}/prepare`, method: 'POST', data: prepareData })
|
||||
prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
|
||||
return request<string>({ url: `${this.baseUrl}/prepare`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -86,17 +94,103 @@ export const workcaseChatAPI = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止对话
|
||||
* 建立SSE流式对话连接
|
||||
* @param sessionId 会话ID(必传)
|
||||
* @param callbacks 回调函数
|
||||
* @returns SSETask 可用于中止请求
|
||||
*/
|
||||
stopChat(filter: TbChat, taskId: string): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/stop/${taskId}`, method: 'POST', data: filter })
|
||||
streamChat(sessionId: string, callbacks: SSECallbacks): SSETask {
|
||||
const url = `${BASE_URL}${this.baseUrl}/stream/${sessionId}`
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
|
||||
const requestTask = uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Accept': 'text/event-stream',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
enableChunked: true,
|
||||
success: (res: any) => {
|
||||
console.log('SSE请求完成:', res)
|
||||
// 处理非200状态码
|
||||
if (res.statusCode !== 200) {
|
||||
console.error('SSE请求状态码异常:', res.statusCode)
|
||||
let errorMsg = '抱歉,服务暂时不可用,请稍后重试。'
|
||||
if (res.statusCode === 401) {
|
||||
errorMsg = '登录已过期,请重新登录。'
|
||||
} else if (res.statusCode === 403) {
|
||||
errorMsg = '无权限访问,请联系管理员。'
|
||||
} else if (res.statusCode === 404) {
|
||||
errorMsg = '会话不存在或已过期,请重新发起对话。'
|
||||
} else if (res.statusCode >= 500) {
|
||||
errorMsg = '服务器异常,请稍后重试。'
|
||||
}
|
||||
callbacks.onError?.(errorMsg)
|
||||
}
|
||||
callbacks.onComplete?.()
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('SSE请求失败:', err)
|
||||
callbacks.onError?.('网络连接失败,请稍后重试。')
|
||||
callbacks.onComplete?.()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听分块数据
|
||||
requestTask.onChunkReceived((res: any) => {
|
||||
try {
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const text = decoder.decode(new Uint8Array(res.data))
|
||||
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
if (dataStr && dataStr !== '[DONE]') {
|
||||
try {
|
||||
const data: SSEMessageData = JSON.parse(dataStr)
|
||||
const event = data.event
|
||||
|
||||
if (event === 'message' || event === 'agent_message') {
|
||||
callbacks.onMessage?.(data)
|
||||
} else if (event === 'message_end') {
|
||||
callbacks.onEnd?.(data.task_id || '')
|
||||
} else if (event === 'error') {
|
||||
callbacks.onError?.(data.message || '发生错误,请稍后重试。')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('解析SSE数据失败:', dataStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理分块数据失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
abort: () => {
|
||||
requestTask.abort()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 评论对话消息
|
||||
* 停止对话
|
||||
* @param param taskId, agentId, userId必传
|
||||
*/
|
||||
commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${messageId}&comment=${comment}`, method: 'POST', data: filter })
|
||||
stopChat(param: StopChatParam): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/stop/${param.taskId}`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
/**
|
||||
* 评价对话消息
|
||||
* @param param agentId, chatId, messageId, comment, userId必传
|
||||
*/
|
||||
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
|
||||
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${param.messageId}&comment=${param.comment}`, method: 'POST', data: param })
|
||||
},
|
||||
|
||||
// ====================== 对话分析 ======================
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const AGENT_ID = '17664699513920001'
|
||||
@@ -308,6 +308,10 @@
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 12px;
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bubble .message-text {
|
||||
@@ -460,3 +464,43 @@
|
||||
font-size: 18px;
|
||||
color: #4b87ff;
|
||||
}
|
||||
|
||||
// 打字指示器动画
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
animation: typing-bounce 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes typing-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,13 @@
|
||||
<text class="avatar-text">AI</text>
|
||||
</view>
|
||||
<view class="message-bubble bot-bubble">
|
||||
<text class="message-text">{{item.content}}</text>
|
||||
<!-- 加载动画:内容为空时显示 -->
|
||||
<view class="typing-indicator" v-if="!item.content && isTyping">
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
</view>
|
||||
<text class="message-text" v-else>{{item.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="message-time">{{item.time}}</text>
|
||||
@@ -112,9 +118,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
|
||||
import { guestAPI, workcaseChatAPI } from '@/api'
|
||||
import { guestAPI, aiChatAPI } from '@/api'
|
||||
import type { TbWorkcaseDTO } from '@/types'
|
||||
|
||||
import { AGENT_ID } from '@/config'
|
||||
// 前端消息展示类型
|
||||
interface ChatMessageItem {
|
||||
type: 'user' | 'bot'
|
||||
@@ -122,7 +128,7 @@
|
||||
time: string
|
||||
actions?: string[] | null
|
||||
}
|
||||
|
||||
const agentId = AGENT_ID
|
||||
// 响应式数据
|
||||
const messages = ref<ChatMessageItem[]>([])
|
||||
const inputText = ref<string>('')
|
||||
@@ -141,7 +147,7 @@
|
||||
userId: ''
|
||||
})
|
||||
const isMockMode = ref(true) // 开发环境mock模式
|
||||
|
||||
const userType = ref(false)
|
||||
// AI 对话相关
|
||||
const chatId = ref<string>('') // 当前会话ID
|
||||
const currentTaskId = ref<string>('') // 当前任务ID(用于停止)
|
||||
@@ -161,9 +167,9 @@
|
||||
// 开发环境:使用mock数据
|
||||
if (isMockMode.value) {
|
||||
userInfo.value = {
|
||||
wechatId: '17857100375',
|
||||
username: '测试用户',
|
||||
phone: '17857100375',
|
||||
wechatId: '17857100377',
|
||||
username: '访客用户',
|
||||
phone: '17857100377',
|
||||
userId: ''
|
||||
}
|
||||
await doIdentify()
|
||||
@@ -173,12 +179,12 @@
|
||||
// 切换mock用户(开发调试用)
|
||||
function switchMockUser() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['员工 (17857100375)', '访客 (17857100376)'],
|
||||
itemList: ['员工 (17857100375)', '访客 (17857100377)'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
|
||||
} else {
|
||||
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376', userId: '' }
|
||||
userInfo.value = { wechatId: '17857100377', username: '访客用户', phone: '17857100377', userId: '' }
|
||||
}
|
||||
doIdentify()
|
||||
}
|
||||
@@ -198,10 +204,16 @@
|
||||
const loginDomain = res.data
|
||||
uni.setStorageSync('token', loginDomain.token || '')
|
||||
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
|
||||
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
|
||||
uni.setStorageSync('wechatId', userInfo.value.wechatId)
|
||||
userInfo.value.userId = loginDomain.user?.userId || ''
|
||||
console.log('identify成功:', loginDomain)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
if(loginDomain.user.status == 'guest') {
|
||||
userType.value = false
|
||||
} else {
|
||||
userType.value = true
|
||||
}
|
||||
} else {
|
||||
console.error('identify失败:', res.message)
|
||||
}
|
||||
@@ -275,9 +287,11 @@
|
||||
try {
|
||||
// 如果没有会话ID,先创建会话
|
||||
if (!chatId.value) {
|
||||
const createRes = await workcaseChatAPI.createChat({
|
||||
const createRes = await aiChatAPI.createChat({
|
||||
title: '智能助手对话',
|
||||
userId: userInfo.value.userId || userInfo.value.wechatId
|
||||
userId: userInfo.value.userId || userInfo.value.wechatId,
|
||||
agentId: agentId,
|
||||
userType: userType.value
|
||||
})
|
||||
if (createRes.success && createRes.data) {
|
||||
chatId.value = createRes.data.chatId || ''
|
||||
@@ -288,22 +302,24 @@
|
||||
}
|
||||
|
||||
// 准备流式对话
|
||||
const prepareRes = await workcaseChatAPI.prepareChatMessageSession({
|
||||
const prepareRes = await aiChatAPI.prepareChatMessageSession({
|
||||
chatId: chatId.value,
|
||||
message: query
|
||||
query: query,
|
||||
agentId: agentId,
|
||||
userType: userType.value,
|
||||
userId: userInfo.value.userId
|
||||
})
|
||||
if (!prepareRes.success || !prepareRes.data) {
|
||||
throw new Error(prepareRes.message || '准备对话失败')
|
||||
}
|
||||
const sessionId = prepareRes.data
|
||||
console.log('准备流式对话成功:', sessionId)
|
||||
|
||||
// 添加空的AI消息占位
|
||||
const messageIndex = messages.value.length
|
||||
addMessage('bot', '')
|
||||
|
||||
// 建立SSE连接
|
||||
streamChat(sessionId, messageIndex)
|
||||
startStreamChat(sessionId, messageIndex)
|
||||
} catch (error : any) {
|
||||
console.error('AI聊天失败:', error)
|
||||
isTyping.value = false
|
||||
@@ -312,66 +328,30 @@
|
||||
}
|
||||
|
||||
// SSE 流式对话
|
||||
function streamChat(sessionId : string, messageIndex : number) {
|
||||
const url = `http://localhost:8180${workcaseChatAPI.getStreamUrl(sessionId)}`
|
||||
console.log('建立SSE连接:', url)
|
||||
function startStreamChat(sessionId : string, messageIndex : number) {
|
||||
console.log('建立SSE连接, sessionId:', sessionId)
|
||||
|
||||
const requestTask = uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: { 'Accept': 'text/event-stream' },
|
||||
enableChunked: true,
|
||||
success: (res : any) => {
|
||||
console.log('SSE请求完成:', res)
|
||||
isTyping.value = false
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('SSE请求失败:', err)
|
||||
isTyping.value = false
|
||||
messages.value[messageIndex].content = '抱歉,网络连接失败,请稍后重试。'
|
||||
}
|
||||
})
|
||||
|
||||
// 监听分块数据
|
||||
requestTask.onChunkReceived((res : any) => {
|
||||
try {
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const text = decoder.decode(new Uint8Array(res.data))
|
||||
console.log('收到分块数据:', text)
|
||||
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
if (dataStr && dataStr !== '[DONE]') {
|
||||
try {
|
||||
const data = JSON.parse(dataStr)
|
||||
const event = data.event
|
||||
|
||||
if (event === 'message' || event === 'agent_message') {
|
||||
if (data.answer) {
|
||||
messages.value[messageIndex].content += data.answer
|
||||
}
|
||||
} else if (event === 'message_end') {
|
||||
isTyping.value = false
|
||||
if (data.task_id) {
|
||||
currentTaskId.value = data.task_id
|
||||
}
|
||||
} else if (event === 'error') {
|
||||
console.error('SSE错误:', data.message)
|
||||
isTyping.value = false
|
||||
messages.value[messageIndex].content = data.message || '抱歉,发生错误,请稍后重试。'
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('解析SSE数据失败:', dataStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
aiChatAPI.streamChat(sessionId, {
|
||||
onMessage: (data) => {
|
||||
if (data.answer) {
|
||||
messages.value[messageIndex].content += data.answer
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理分块数据失败:', e)
|
||||
},
|
||||
onEnd: (taskId) => {
|
||||
isTyping.value = false
|
||||
if (taskId) {
|
||||
currentTaskId.value = taskId
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('SSE错误:', error)
|
||||
isTyping.value = false
|
||||
messages.value[messageIndex].content = error
|
||||
},
|
||||
onComplete: () => {
|
||||
isTyping.value = false
|
||||
}
|
||||
nextTick(() => scrollToBottom())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -491,7 +471,11 @@
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
scrollTop.value = 999999
|
||||
// 先重置再设置大值,确保值变化触发滚动
|
||||
scrollTop.value = scrollTop.value + 1
|
||||
nextTick(() => {
|
||||
scrollTop.value = 999999
|
||||
})
|
||||
}
|
||||
|
||||
// 联系人工客服
|
||||
|
||||
@@ -107,4 +107,122 @@ export interface CommentMessageParams {
|
||||
messageId: string
|
||||
comment: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
// ==================== 请求参数类型(必传校验) ====================
|
||||
|
||||
/**
|
||||
* 创建对话参数
|
||||
*/
|
||||
export interface CreateChatParam {
|
||||
/** 智能体ID(必传) */
|
||||
agentId: string
|
||||
/** 用户ID(必传) */
|
||||
userId: string
|
||||
/** 用户类型 */
|
||||
userType: boolean
|
||||
/** 对话标题 */
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备流式对话参数
|
||||
*/
|
||||
export interface PrepareChatParam {
|
||||
/** 对话ID(必传) */
|
||||
chatId: string
|
||||
/** 用户问题(必传) */
|
||||
query: string
|
||||
/** 智能体ID */
|
||||
agentId: string
|
||||
userType: boolean
|
||||
/** 用户ID */
|
||||
userId?: string
|
||||
/** 用户类型 */
|
||||
/** 文件列表 */
|
||||
files?: DifyFileInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话参数
|
||||
*/
|
||||
export interface StopChatParam {
|
||||
/** 任务ID(必传) */
|
||||
taskId: string
|
||||
/** 智能体ID(必传) */
|
||||
agentId: string
|
||||
/** 用户ID(必传) */
|
||||
userId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 评价消息参数
|
||||
*/
|
||||
export interface CommentMessageParam {
|
||||
/** 智能体ID(必传) */
|
||||
agentId: string
|
||||
/** 对话ID(必传) */
|
||||
chatId: string
|
||||
/** 消息ID(必传) */
|
||||
messageId: string
|
||||
/** 评价内容(必传) */
|
||||
comment: string
|
||||
/** 用户ID(必传) */
|
||||
userId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询对话列表参数
|
||||
*/
|
||||
export interface ChatListParam {
|
||||
/** 用户ID(必传) */
|
||||
userId: string
|
||||
/** 智能体ID */
|
||||
agentId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询对话消息列表参数
|
||||
*/
|
||||
export interface ChatMessageListParam {
|
||||
/** 对话ID(必传) */
|
||||
chatId: string
|
||||
}
|
||||
|
||||
// ==================== SSE 流式对话类型 ====================
|
||||
|
||||
/**
|
||||
* SSE 消息事件数据
|
||||
*/
|
||||
export interface SSEMessageData {
|
||||
/** 事件类型 */
|
||||
event?: string
|
||||
/** 回答内容 */
|
||||
answer?: string
|
||||
/** 任务ID */
|
||||
task_id?: string
|
||||
/** 错误消息 */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE 回调函数
|
||||
*/
|
||||
export interface SSECallbacks {
|
||||
/** 收到消息 */
|
||||
onMessage?: (data: SSEMessageData) => void
|
||||
/** 消息结束 */
|
||||
onEnd?: (taskId: string) => void
|
||||
/** 发生错误 */
|
||||
onError?: (error: string) => void
|
||||
/** 请求完成(无论成功失败) */
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE 请求任务对象
|
||||
*/
|
||||
export interface SSETask {
|
||||
/** 停止请求 */
|
||||
abort: () => void
|
||||
}
|
||||
Reference in New Issue
Block a user