From 6afec090e4ddf40b691de5e5a1976e85a385a3e0 Mon Sep 17 00:00:00 2001 From: Peanut Date: Tue, 21 Jul 2026 20:33:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=A7=E7=BA=B2=E7=A1=AE=E8=AE=A4/?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=97=B6=E6=98=BE=E7=A4=BA=20loading=20+=20n?= =?UTF-8?q?ovel=20=E6=B6=88=E6=81=AF=E4=BD=BF=E7=94=A8=20typewriter=20?= =?UTF-8?q?=E9=80=90=E5=AD=97=E6=B5=81=E5=BC=8F=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - confirmOutline / modifyOutline 中设置 pendingNextResponse.value = true 解决大纲确认/修改后等待响应期间无 loading 的问题 - novel_start 时为 novel 消息创建独立 typewriter writer 实例(novelWriters Map) - novel_delta 时通过 writer.push() 推送增量,逐字渲染 visibleText - novel_done 时 finish writer 并同步最终文本,清理 writer 实例 Co-Authored-By: Claude Opus 4.8 (1M context) --- mini-program/src/pages/main/ScriptView.vue | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/mini-program/src/pages/main/ScriptView.vue b/mini-program/src/pages/main/ScriptView.vue index f06e383..2101f84 100644 --- a/mini-program/src/pages/main/ScriptView.vue +++ b/mini-program/src/pages/main/ScriptView.vue @@ -290,6 +290,8 @@ const currentVersionMessageId = ref('') const novelSessionId = ref('') const novelFullText = ref('') const novelOutline = ref(null) +// novel 消息 id → typewriter writer 实例映射(用于真正的逐字流式输出) +const novelWriters = new Map() const clarificationCard = ref(null) const currentStreamTask = ref(null) const answeringClarification = ref(false) @@ -1705,16 +1707,27 @@ const handleShortNovelEvent = (event) => { generationStatus.value = 'idle' pendingNextResponse.value = false break - case 'novel_start': - addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true }) + case 'novel_start': { + const novelMsg = addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true }) + // 为这条 novel 消息创建独立的 typewriter writer,实现逐字流式输出 + novelWriters.set(novelMsg.id, useTypewriterStream({ interval: 32, step: 1 })) generationStatus.value = 'streaming' pendingNextResponse.value = false break + } case 'novel_delta': { - // 往最后一条 novel 消息追加 delta + // 往最后一条 pending novel 消息推送 delta 到 typewriter writer const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending) if (lastNovel) { - lastNovel.content += payload.delta || '' + const writer = novelWriters.get(lastNovel.id) + 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() break @@ -1723,7 +1736,17 @@ const handleShortNovelEvent = (event) => { // 标记最后一条 novel 消息完成 const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel') if (lastNovel) { - if (payload.full_text) lastNovel.content = payload.full_text + const writer = novelWriters.get(lastNovel.id) + 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 } scriptId.value = payload.scriptId || '' @@ -1775,6 +1798,7 @@ const submitClarification = (answer) => { const confirmOutline = (msg) => { if (msg) msg.confirmed = true + pendingNextResponse.value = true addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' }) currentStreamTask.value = followupStream({ sessionId: novelSessionId.value, @@ -1792,6 +1816,7 @@ const modifyOutline = (msg) => { return } if (msg) msg.confirmed = true + pendingNextResponse.value = true addResultMessage({ role: 'user', kind: 'text', content: feedback }) currentStreamTask.value = followupStream({ sessionId: novelSessionId.value,