feat: 小程序剧本、个人信息页、AI 运行时及 TTS 服务优化

This commit is contained in:
2026-05-26 20:50:20 +08:00
parent a51d225897
commit d09f600751
8 changed files with 1089 additions and 226 deletions
+45 -8
View File
@@ -44,6 +44,33 @@ const parseSseFrame = (frame) => {
}
}
const findOverlapLength = (current, next) => {
const max = Math.min(current.length, next.length)
for (let size = max; size > 0; size--) {
if (current.slice(-size) === next.slice(0, size)) return size
}
return 0
}
const mergeStreamOutput = (current, chunk) => {
const next = String(chunk || '')
if (!next) return { output: current, delta: '' }
if (!current) return { output: next, delta: next }
if (next === current) return { output: current, delta: '' }
if (next.length >= 16 && current.endsWith(next)) return { output: current, delta: '' }
if (next.startsWith(current)) {
return { output: next, delta: next.slice(current.length) }
}
const currentIndex = next.length > current.length ? next.indexOf(current) : -1
if (currentIndex >= 0) {
return { output: next, delta: next.slice(currentIndex + current.length) }
}
const overlap = findOverlapLength(current, next)
if (overlap < 8) return { output: current + next, delta: next }
const delta = next.slice(overlap)
return { output: current + delta, delta }
}
const queryRuntimeResult = (requestId) => {
return new Promise((resolve, reject) => {
uni.request({
@@ -113,6 +140,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
let requestTask
let recoveryTimer
let recoveryPromise
let streamStarted = false
const clearRecoveryTimer = () => {
if (recoveryTimer) {
@@ -130,6 +158,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
const completeFromRecoveredOutput = async () => {
if (closed) return
if (streamStarted || output.trim()) return
try {
const recoveredOutput = await recoverOnce()
if (closed) return
@@ -149,7 +178,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
clearRecoveryTimer()
recoveryTimer = setTimeout(() => {
completeFromRecoveredOutput()
}, 8000)
}, 25000)
}
const finishRecovered = (message, event) => {
@@ -172,7 +201,6 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
const recoverOrFail = async (message, event) => {
if (closed) return
if (finishRecovered(message, event)) return
try {
const recoveredOutput = await recoverOnce()
if (closed) return
@@ -182,6 +210,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
resolve({ output })
} catch (recoverError) {
if (closed) return
if (finishRecovered(message || recoverError.message, event)) return
const finalMessage = message || recoverError.message || 'AI 生成结果暂时没有返回'
closed = true
clearRecoveryTimer()
@@ -196,10 +225,7 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
const failWithoutRecovery = (message, event) => {
if (closed) return
closed = true
clearRecoveryTimer()
onError?.(message, event)
reject(new Error(message))
recoverOrFail(message, event)
}
const finishWithOutputOrRecover = async () => {
@@ -247,6 +273,8 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
requestTask?.onChunkReceived?.((res) => {
try {
streamStarted = true
clearRecoveryTimer()
consumeText(decodeChunk(res.data), failStream)
} catch (error) {
failStream(error.message || 'AI流式请求失败')
@@ -262,11 +290,20 @@ export const streamAiScene = ({ sceneCode, inputs = {}, onStart, onDelta, onDone
const event = parseSseFrame(frame)
if (!event) return
if (event.type === 'start') {
streamStarted = true
clearRecoveryTimer()
onStart?.(event)
} else if (event.type === 'delta') {
output += event.content || ''
onDelta?.(event.content || '', output, event)
streamStarted = true
clearRecoveryTimer()
const merged = mergeStreamOutput(output, event.content)
output = merged.output
if (merged.delta) {
onDelta?.(merged.delta, output, event)
}
} else if (event.type === 'done') {
streamStarted = true
clearRecoveryTimer()
onDone?.(event, output)
} else if (event.type === 'error') {
const message = event.message || event.code || 'AI流式请求失败'
+12 -4
View File
@@ -3,6 +3,8 @@ import { getEnvValue } from '../config/env.js'
const DEFAULT_SOURCE_TYPE = 'epic_script'
const DEFAULT_VOICE = 'default_zh_female'
const DEFAULT_SPEECH_RATE = 0.92
const DEFAULT_EMOTION = 'story'
const normalizeAudioUrl = (task) => {
if (!task?.audioUrl || /^https?:\/\//.test(task.audioUrl)) {
@@ -25,9 +27,12 @@ const normalizeResponse = (response) => {
export const createTtsTask = ({
sourceType = DEFAULT_SOURCE_TYPE,
sourceId,
voice = DEFAULT_VOICE
voice = DEFAULT_VOICE,
speechRate = DEFAULT_SPEECH_RATE,
pitch = 0,
emotion = DEFAULT_EMOTION
}) => {
return post('/tts/tasks', { sourceType, sourceId, voice }).then(normalizeResponse)
return post('/tts/tasks', { sourceType, sourceId, voice, speechRate, pitch, emotion }).then(normalizeResponse)
}
export const getTtsTask = (id) => {
@@ -37,9 +42,12 @@ export const getTtsTask = (id) => {
export const getTtsTaskBySource = ({
sourceType = DEFAULT_SOURCE_TYPE,
sourceId,
voice = DEFAULT_VOICE
voice = DEFAULT_VOICE,
speechRate = DEFAULT_SPEECH_RATE,
pitch = 0,
emotion = DEFAULT_EMOTION
}) => {
return get('/tts/tasks/by-source', { sourceType, sourceId, voice }).then(normalizeResponse)
return get('/tts/tasks/by-source', { sourceType, sourceId, voice, speechRate, pitch, emotion }).then(normalizeResponse)
}
export default {