feat(mini-program): 短篇小说 SSE 流式服务

This commit is contained in:
2026-07-19 12:36:06 +08:00
parent 00f17d2f47
commit ab13ac258b
+134
View File
@@ -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) => voidevent 包含 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}`)
}
}
}
}