pet/utils/sse.js

259 lines
6.0 KiB
JavaScript
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.

/**
* SSE (Server-Sent Events) 流式响应工具类
* 提供通用的SSE数据流处理功能
*/
/**
* 创建SSE流式请求
* @param {Object} options 配置选项
* @param {string} options.url 请求URL
* @param {string} options.method 请求方法默认POST
* @param {Object} options.data 请求数据
* @param {Object} options.headers 请求头会自动添加SSE相关头部
* @param {Function} options.onChunk 接收到数据块时的回调 (chunk) => {}
* @param {Function} options.onComplete 流式响应完成时的回调 (result) => {}
* @param {Function} options.onError 发生错误时的回调 (error) => {}
* @param {string} options.doneMarker 结束标记,默认为 '[DONE]'
* @returns {Promise} 返回Promiseresolve时返回完整结果
*/
export function createSSERequest(options) {
const {
url,
method = 'POST',
data = {},
headers = {},
onChunk,
onComplete,
onError,
doneMarker = '[DONE]'
} = options
return new Promise((resolve, reject) => {
// 构建完整的请求头
const requestHeaders = {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Authorization': `Bearer ${uni.getStorageSync('token') || ''}`,
...headers
}
// 累积的响应数据
let accumulatedData = {
content: '',
metadata: {}
}
// 发起uni.request请求
const requestTask = uni.request({
url,
method,
data,
header: requestHeaders,
responseType: 'text',
success: (res) => {
try {
// 处理SSE数据流
const lines = res.data.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6).trim()
// 检查是否为结束标记
if (data === doneMarker) {
// 流式响应结束
const result = {
data: {
message: accumulatedData.content,
...accumulatedData.metadata
}
}
// 调用完成回调
if (onComplete) {
onComplete(result)
}
resolve(result)
return
}
try {
const parsed = JSON.parse(data)
// 处理内容数据
if (parsed.content) {
accumulatedData.content += parsed.content
// 调用数据块回调
if (onChunk) {
onChunk({
content: parsed.content,
totalContent: accumulatedData.content,
chunk: parsed
})
}
}
// 更新元数据
if (parsed.isSensitive !== undefined) {
accumulatedData.metadata.isSensitive = parsed.isSensitive
}
if (parsed.tokenCount !== undefined) {
accumulatedData.metadata.tokenCount = parsed.tokenCount
}
if (parsed.responseTime !== undefined) {
accumulatedData.metadata.responseTime = parsed.responseTime
}
// 保存其他元数据
Object.keys(parsed).forEach(key => {
if (!['content', 'isSensitive', 'tokenCount', 'responseTime'].includes(key)) {
accumulatedData.metadata[key] = parsed[key]
}
})
} catch (parseError) {
console.warn('解析SSE数据失败:', parseError, data)
// 继续处理其他数据,不中断流程
}
}
}
// 如果没有收到结束标记,也认为完成
const result = {
data: {
message: accumulatedData.content,
...accumulatedData.metadata
}
}
if (onComplete) {
onComplete(result)
}
resolve(result)
} catch (error) {
console.error('处理SSE响应失败:', error)
if (onError) {
onError(error)
}
reject(error)
}
},
fail: (error) => {
console.error('SSE请求失败:', error)
if (onError) {
onError(error)
}
reject(error)
}
})
// 返回请求任务,允许外部取消请求
return requestTask
})
}
/**
* 创建带有默认配置的SSE请求
* @param {string} endpoint API端点路径
* @param {Object} data 请求数据
* @param {Object} callbacks 回调函数 {onChunk, onComplete, onError}
* @returns {Promise} SSE请求Promise
*/
export function createDefaultSSERequest(endpoint, data, callbacks = {}) {
const baseURL = uni.$u?.http?.config?.baseURL || ''
return createSSERequest({
url: `${baseURL}${endpoint}`,
method: 'POST',
data,
...callbacks
})
}
/**
* 专用于宠物助手的SSE请求
* @param {Object} data 请求数据
* @param {Object} callbacks 回调函数
* @returns {Promise} SSE请求Promise
*/
export function createPetAssistantSSE(data, callbacks = {}) {
return createDefaultSSERequest('/pet/user/assistant/stream-ask', data, callbacks)
}
/**
* SSE请求状态枚举
*/
export const SSE_STATUS = {
PENDING: 'pending',
STREAMING: 'streaming',
COMPLETED: 'completed',
ERROR: 'error',
CANCELLED: 'cancelled'
}
/**
* 创建可取消的SSE请求
* @param {Object} options 配置选项
* @returns {Object} 返回包含promise和cancel方法的对象
*/
export function createCancellableSSE(options) {
let requestTask = null
let status = SSE_STATUS.PENDING
const promise = new Promise((resolve, reject) => {
const enhancedOptions = {
...options,
onChunk: (chunk) => {
status = SSE_STATUS.STREAMING
if (options.onChunk) {
options.onChunk(chunk)
}
},
onComplete: (result) => {
status = SSE_STATUS.COMPLETED
if (options.onComplete) {
options.onComplete(result)
}
resolve(result)
},
onError: (error) => {
status = SSE_STATUS.ERROR
if (options.onError) {
options.onError(error)
}
reject(error)
}
}
requestTask = createSSERequest(enhancedOptions)
})
return {
promise,
cancel: () => {
if (requestTask && status === SSE_STATUS.STREAMING) {
requestTask.abort?.()
status = SSE_STATUS.CANCELLED
}
},
getStatus: () => status
}
}
export default {
createSSERequest,
createDefaultSSERequest,
createPetAssistantSSE,
createCancellableSSE,
SSE_STATUS
}