import { request, uploadFile } from '../base' import type { ResultDomain } from '../../types' import { BASE_URL } from '../../config' import type { TbChat, TbChatMessage, CreateChatParam, ChatPrepareData, StopChatParam, CommentMessageParam, ChatListParam, ChatMessageListParam, SSECallbacks, SSETask, SSEMessageData, DifyFileInfo } from '../../types/ai/aiChat' /* eslint-disable @typescript-eslint/no-explicit-any */ declare const uni: { getStorageSync: (key: string) => any request: (options: any) => any } /** * ArrayBuffer 转字符串(兼容微信小程序真机环境) * 微信小程序真机不支持 TextDecoder,需要手动解码 UTF-8 */ function arrayBufferToString(buffer: ArrayBuffer): string { // 优先使用 TextDecoder(开发者工具和支持的环境) if (typeof TextDecoder !== 'undefined') { return new TextDecoder('utf-8').decode(new Uint8Array(buffer)) } // 微信小程序真机兼容方案:手动解码 UTF-8 const bytes = new Uint8Array(buffer) let result = '' let i = 0 while (i < bytes.length) { const byte1 = bytes[i++] if (byte1 < 0x80) { // 单字节字符 (0xxxxxxx) result += String.fromCharCode(byte1) } else if ((byte1 & 0xE0) === 0xC0) { // 双字节字符 (110xxxxx 10xxxxxx) const byte2 = bytes[i++] & 0x3F result += String.fromCharCode(((byte1 & 0x1F) << 6) | byte2) } else if ((byte1 & 0xF0) === 0xE0) { // 三字节字符 (1110xxxx 10xxxxxx 10xxxxxx) - 中文常用 const byte2 = bytes[i++] & 0x3F const byte3 = bytes[i++] & 0x3F result += String.fromCharCode(((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3) } else if ((byte1 & 0xF8) === 0xF0) { // 四字节字符 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) - emoji等 const byte2 = bytes[i++] & 0x3F const byte3 = bytes[i++] & 0x3F const byte4 = bytes[i++] & 0x3F const codePoint = ((byte1 & 0x07) << 18) | (byte2 << 12) | (byte3 << 6) | byte4 // 转换为 UTF-16 代理对 const surrogate = codePoint - 0x10000 result += String.fromCharCode(0xD800 + (surrogate >> 10), 0xDC00 + (surrogate & 0x3FF)) } } return result } /** * @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> { return request({ url: `${this.baseUrl}/conversation`, method: 'POST', data: param }) }, /** * 更新对话 * @param chat agentId、userId、title、userType 必传 */ updateChat(chat: TbChat): Promise> { return request({ url: `${this.baseUrl}/conversation`, method: 'PUT', data: chat }) }, /** * 查询对话列表 * @param param agentId 必传 */ getChatList(param: ChatListParam): Promise> { return request({ url: `${this.baseUrl}/conversations`, method: 'GET', data: param }) }, /** * 获取对话消息列表 * @param param agentId、chatId、userId 必传 */ getChatMessageList(param: ChatMessageListParam): Promise> { return request({ url: `${this.baseUrl}/messages`, method: 'POST', data: param }) }, /** * 准备流式对话会话 * @param param agentId、chatId、query、userId 必传 */ prepareChatMessageSession(param: ChatPrepareData): Promise> { return request({ 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 { // 兼容微信小程序真机环境(不支持 TextDecoder) const text = arrayBufferToString(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> { return request({ url: `${this.baseUrl}/stop`, method: 'POST', data: param }) }, /** * 评价对话消息 * @param param agentId、chatId、messageId、comment、userId 必传 */ commentChatMessage(param: CommentMessageParam): Promise> { return request({ url: `${this.baseUrl}/comment`, method: 'POST', data: param }) }, // ====================== 文件上传 ====================== /** * 上传文件用于对话(图文多模态) * @param filePath 文件临时路径 * @param agentId 智能体ID */ uploadFileForChat(filePath: string, agentId: string): Promise> { return uploadFile({ url: `${this.baseUrl}/file/upload`, filePath: filePath, name: 'file', formData: { agentId: agentId } }) } }