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