Files
urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts

255 lines
9.5 KiB
TypeScript
Raw Normal View History

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'
2026-01-09 12:17:21 +08:00
import { BASE_URL } from '../../config'
2025-12-23 15:57:11 +08:00
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
}
2026-01-12 10:47:44 +08:00
/**
* 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
}
2025-12-23 15:57:11 +08:00
/**
* @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 agentIduserIdtitle
*/
createChat(param: CreateChatParam): Promise<ResultDomain<TbChat>> {
return request<TbChat>({ url: `${this.baseUrl}/conversation`, method: 'POST', data: param })
},
/**
*
* @param chat agentIduserIdtitleuserType
*/
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 agentIdchatIduserId
*/
getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
return request<TbChatMessage[]>({ url: `${this.baseUrl}/messages`, method: 'POST', data: param })
},
/**
*
* @param param agentIdchatIdqueryuserId
*/
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 {
2026-01-12 10:47:44 +08:00
// 兼容微信小程序真机环境(不支持 TextDecoder
const text = arrayBufferToString(res.data)
2025-12-23 15:57:11 +08:00
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 taskIdagentIduserId
*/
stopChat(param: StopChatParam): Promise<ResultDomain<boolean>> {
return request<boolean>({ url: `${this.baseUrl}/stop`, method: 'POST', data: param })
},
/**
*
* @param param agentIdchatIdmessageIdcommentuserId
*/
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
}
}