2025-12-29 12:49:23 +08:00
|
|
|
|
import { request, uploadFile } from '../base'
|
2025-12-23 15:57:11 +08:00
|
|
|
|
import type { ResultDomain } from '../../types'
|
|
|
|
|
|
import type {
|
|
|
|
|
|
TbChat,
|
|
|
|
|
|
TbChatMessage,
|
|
|
|
|
|
CreateChatParam,
|
2025-12-29 18:40:26 +08:00
|
|
|
|
ChatPrepareData,
|
2025-12-23 15:57:11 +08:00
|
|
|
|
StopChatParam,
|
|
|
|
|
|
CommentMessageParam,
|
|
|
|
|
|
ChatListParam,
|
|
|
|
|
|
ChatMessageListParam,
|
|
|
|
|
|
SSECallbacks,
|
|
|
|
|
|
SSETask,
|
2025-12-29 12:49:23 +08:00
|
|
|
|
SSEMessageData,
|
|
|
|
|
|
DifyFileInfo
|
2025-12-23 15:57:11 +08:00
|
|
|
|
} 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 必传
|
|
|
|
|
|
*/
|
2025-12-29 18:40:26 +08:00
|
|
|
|
prepareChatMessageSession(param: ChatPrepareData): Promise<ResultDomain<string>> {
|
2025-12-23 15:57:11 +08:00
|
|
|
|
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 })
|
2025-12-29 12:49:23 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ====================== 文件上传 ======================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 上传文件用于对话(图文多模态)
|
|
|
|
|
|
* @param filePath 文件临时路径
|
|
|
|
|
|
* @param agentId 智能体ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
uploadFileForChat(filePath: string, agentId: string): Promise<ResultDomain<DifyFileInfo>> {
|
|
|
|
|
|
return uploadFile<DifyFileInfo>({
|
|
|
|
|
|
url: `${this.baseUrl}/file/upload`,
|
|
|
|
|
|
filePath: filePath,
|
|
|
|
|
|
name: 'file',
|
|
|
|
|
|
formData: { agentId: agentId }
|
|
|
|
|
|
})
|
2025-12-23 15:57:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|