Files
urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts
2026-01-12 10:47:44 +08:00

255 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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: ChatPrepareData): 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 {
// 兼容微信小程序真机环境(不支持 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<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 })
},
// ====================== 文件上传 ======================
/**
* 上传文件用于对话(图文多模态)
* @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 }
})
}
}