feat(mini-program): 短篇小说 SSE 流式服务
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
import { getApiBaseUrl, getAuthHeader } from './request.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短篇小说外部服务 API 封装
|
||||||
|
* 通过后端 SSE 代理调用外部 short-novel-service
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首次发起短篇小说生成(SSE 流式)
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string} params.query - 用户心愿文本
|
||||||
|
* @param {Function} params.onEvent - 事件回调 (event) => void,event 包含 type、session_id、payload
|
||||||
|
* @param {Function} params.onError - 错误回调
|
||||||
|
* @returns {Object} uni.request 任务对象(用于 abort)
|
||||||
|
*/
|
||||||
|
export const startNovelStream = ({ query, onEvent, onError }) => {
|
||||||
|
const task = uni.request({
|
||||||
|
url: `${getApiBaseUrl()}/shortNovel/stream`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { query },
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
...getAuthHeader()
|
||||||
|
},
|
||||||
|
enableChunked: true,
|
||||||
|
timeout: 300000,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
onError?.(res.data?.message || '请求失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof res.data === 'string' && res.data) {
|
||||||
|
consumeSseText(res.data, onEvent, onError)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
onError?.(error.errMsg || '网络请求失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
task?.onChunkReceived?.((res) => {
|
||||||
|
try {
|
||||||
|
const text = decodeChunk(res.data)
|
||||||
|
consumeSseText(text, onEvent, onError)
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error.message || '流式解析失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后续轮次(澄清回答、大纲确认/修改、重试)
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string} params.sessionId - 外部服务的 session_id
|
||||||
|
* @param {string} params.action - answer_clarification | confirm_outline | modify_outline | retry
|
||||||
|
* @param {Object} params.payload - 操作载荷
|
||||||
|
* @param {Function} params.onEvent
|
||||||
|
* @param {Function} params.onError
|
||||||
|
* @returns {Object} uni.request 任务对象
|
||||||
|
*/
|
||||||
|
export const followupStream = ({ sessionId, action, payload, onEvent, onError }) => {
|
||||||
|
const task = uni.request({
|
||||||
|
url: `${getApiBaseUrl()}/shortNovel/followup`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { sessionId, action, payload },
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
...getAuthHeader()
|
||||||
|
},
|
||||||
|
enableChunked: true,
|
||||||
|
timeout: 300000,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
onError?.(res.data?.message || '请求失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof res.data === 'string' && res.data) {
|
||||||
|
consumeSseText(res.data, onEvent, onError)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
onError?.(error.errMsg || '网络请求失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
task?.onChunkReceived?.((res) => {
|
||||||
|
try {
|
||||||
|
const text = decodeChunk(res.data)
|
||||||
|
consumeSseText(text, onEvent, onError)
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error.message || '流式解析失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解码 chunk 数据
|
||||||
|
*/
|
||||||
|
function decodeChunk(chunk) {
|
||||||
|
if (typeof chunk === 'string') return chunk
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
return decoder.decode(new Uint8Array(chunk))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 SSE 文本
|
||||||
|
* 外部服务事件格式:data: {"type":"status","session_id":"xxx","payload":{...}}
|
||||||
|
*/
|
||||||
|
function consumeSseText(text, onEvent, onError) {
|
||||||
|
const lines = text.split(/\r?\n/)
|
||||||
|
let dataBuffer = ''
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data:')) {
|
||||||
|
dataBuffer += line.slice(5).trim()
|
||||||
|
} else if (line === '' && dataBuffer) {
|
||||||
|
const dataStr = dataBuffer
|
||||||
|
dataBuffer = ''
|
||||||
|
if (dataStr === '[DONE]') return
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(dataStr)
|
||||||
|
onEvent?.(event)
|
||||||
|
} catch (e) {
|
||||||
|
onError?.(`SSE 解析失败: ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user