fix: 撤回 typewriter 改动,novel 改用直接累加 delta 保证流式输出

之前用 useTypewriterStream.writer.push + 同步读 visibleText 有时序问题:
writer 的 visibleText 由内部 setInterval 异步推进,push 后立即同步读得到空串,
气泡一直空白。

撤回 typewriter 改动,恢复 novel_delta 直接 lastNovel.content += delta,
SSE 本身就是流式推送,每次到达即显示。

保留 confirmOutline / modifyOutline 的 pendingNextResponse = true 修复(loading)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 20:43:11 +08:00
parent 6afec090e4
commit 9b496bb42a
+5 -28
View File
@@ -290,8 +290,6 @@ const currentVersionMessageId = ref('')
const novelSessionId = ref('') const novelSessionId = ref('')
const novelFullText = ref('') const novelFullText = ref('')
const novelOutline = ref(null) const novelOutline = ref(null)
// novel 消息 id → typewriter writer 实例映射(用于真正的逐字流式输出)
const novelWriters = new Map()
const clarificationCard = ref(null) const clarificationCard = ref(null)
const currentStreamTask = ref(null) const currentStreamTask = ref(null)
const answeringClarification = ref(false) const answeringClarification = ref(false)
@@ -1707,27 +1705,16 @@ const handleShortNovelEvent = (event) => {
generationStatus.value = 'idle' generationStatus.value = 'idle'
pendingNextResponse.value = false pendingNextResponse.value = false
break break
case 'novel_start': { case 'novel_start':
const novelMsg = addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true }) addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true })
// 为这条 novel 消息创建独立的 typewriter writer,实现逐字流式输出
novelWriters.set(novelMsg.id, useTypewriterStream({ interval: 32, step: 1 }))
generationStatus.value = 'streaming' generationStatus.value = 'streaming'
pendingNextResponse.value = false pendingNextResponse.value = false
break break
}
case 'novel_delta': { case 'novel_delta': {
// 往最后一条 pending novel 消息推送 delta 到 typewriter writer // 往最后一条 pending novel 消息直接累加 delta,保证流式显示
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending) const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
if (lastNovel) { if (lastNovel) {
const writer = novelWriters.get(lastNovel.id) lastNovel.content += payload.delta || ''
const delta = payload.delta || ''
if (writer) {
writer.push((writer.targetText?.value || '') + delta)
lastNovel.content = writer.visibleText.value
// writer 的 visibleText 是 reactive,需用 watch;但这里直接读取值即可
} else {
lastNovel.content += delta
}
} }
keepResultAtBottom() keepResultAtBottom()
break break
@@ -1736,17 +1723,7 @@ const handleShortNovelEvent = (event) => {
// 标记最后一条 novel 消息完成 // 标记最后一条 novel 消息完成
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel') const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel')
if (lastNovel) { if (lastNovel) {
const writer = novelWriters.get(lastNovel.id) if (payload.full_text) lastNovel.content = payload.full_text
const finalText = payload.full_text || ''
if (writer) {
writer.finish(finalText)
// finish 后 typewriter 会逐字排空到 targetText,等排空完成再写回 content
// 但为了持久化和立即显示,这里直接同步设值
lastNovel.content = finalText
novelWriters.delete(lastNovel.id)
} else if (finalText) {
lastNovel.content = finalText
}
lastNovel.pending = false lastNovel.pending = false
} }
scriptId.value = payload.scriptId || '' scriptId.value = payload.scriptId || ''