From ed67739a479351548c0b335883e8468ff350a929 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 26 Jul 2026 17:07:22 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E6=BE=84=E6=B8=85=E9=97=AE?= =?UTF-8?q?=E7=AD=94=E9=85=8D=E5=AF=B9=E9=80=BB=E8=BE=91=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=88=E6=8C=89=20messageOrder=20=E5=8D=87=E5=BA=8F=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:DB 中 clarification_answer 可能先于 question 存入(messageOrder 更小), 导致原「找最后一张 card」的匹配逻辑把 answer 错误渲染成独立 user 文本消息 (如截图中的「love」紫色气泡)。 修复: - rebuildResultMessages 改用 messageOrder 升序配对策略: 1. 先分别提取所有 clarification_question 和 clarification_answer,按 messageOrder 排序 2. 为每个 question 配对它之后第一个未分配的 answer 3. 遍历时 question 替换为含答案的 card 消息,已配对 answer 跳过,未配对 answer 作为独立 user 文本 影响: - ScriptView.vue(chat-bubble 流程视图) - ScriptDetailView.vue(详情页只读视图) 数据基础:后端 answer 仍只存 value(语义化),前端渲染时通过 options 映射 label。 --- .../src/pages/main/ScriptDetailView.vue | 64 ++++++++++--- mini-program/src/pages/main/ScriptView.vue | 92 +++++++++++++------ 2 files changed, 112 insertions(+), 44 deletions(-) diff --git a/mini-program/src/pages/main/ScriptDetailView.vue b/mini-program/src/pages/main/ScriptDetailView.vue index c4a83cf..3a659b8 100644 --- a/mini-program/src/pages/main/ScriptDetailView.vue +++ b/mini-program/src/pages/main/ScriptDetailView.vue @@ -143,22 +143,58 @@ const rebuildResultMessages = (scriptData, messages) => { }) } - // 2. 按 messageOrder 遍历 messages,转换为对应 kind + // 2. 澄清问答按 messageOrder 升序配对(DB 中 answer 可能先于 question 存入) + const clarificationQuestions = messages + .filter(m => m.type === 'clarification_question') + .sort((a, b) => (a.messageOrder || 0) - (b.messageOrder || 0)) + const clarificationAnswers = messages + .filter(m => m.type === 'clarification_answer') + .sort((a, b) => (a.messageOrder || 0) - (b.messageOrder || 0)) + + // 为每个 question 配对它之后第一个未分配的 answer(按 messageOrder 升序) + const answerAssigned = new Set() + const questionAnswerMap = new Map() + const unmatchedAnswers = [] + clarificationQuestions.forEach(q => { + const matched = clarificationAnswers.find(a => + !answerAssigned.has(a.id) && (a.messageOrder || 0) > (q.messageOrder || 0) + ) + if (matched) { + answerAssigned.add(matched.id) + questionAnswerMap.set(q.id, matched) + } + }) + clarificationAnswers.forEach(a => { + if (!answerAssigned.has(a.id)) unmatchedAnswers.push(a) + }) + + // 为每个 question 生成 card 消息(含配对答案) + const cardMessagesByQuestionId = new Map() + clarificationQuestions.forEach(q => { + let card = {} + try { card = JSON.parse(q.content || '{}') } catch (e) { card = {} } + const answer = questionAnswerMap.get(q.id) + cardMessagesByQuestionId.set(q.id, { + id: q.id, + role: 'assistant', + kind: 'card', + card, + answer: answer?.content || '', + selectedValue: answer?.content || '', + submitted: true + }) + }) + + // 3. 按 messageOrder 顺序遍历,构建统一消息流 messages.forEach(m => { if (m.type === 'clarification_question') { - let card = {} - try { card = JSON.parse(m.content || '{}') } catch (e) { card = {} } - result.push({ id: m.id, role: 'assistant', kind: 'card', card, answer: '', submitted: true }) + const cardMsg = cardMessagesByQuestionId.get(m.id) + if (cardMsg) result.push(cardMsg) } else if (m.type === 'clarification_answer') { - const lastCard = [...result].reverse().find(x => x.kind === 'card') - if (lastCard) { - // answer 存用户提交的 value,渲染时通过 card.options 映射成中文 label - lastCard.answer = m.content || '' - lastCard.selectedValue = m.content || '' - } else { - // 无对应 question,作为独立 user 消息 - result.push({ id: m.id, role: 'user', kind: 'text', content: m.content || '' }) - } + // 已配对的 answer 由 question 那一步负责渲染;这里跳过 + if (answerAssigned.has(m.id)) return + // 未配对的 answer 展示为独立 user 文本 + result.push({ id: m.id, role: 'user', kind: 'text', content: m.content || '' }) } else if (m.type === 'outline') { let outline = {} try { outline = JSON.parse(m.content || '{}') } catch (e) { outline = {} } @@ -172,7 +208,7 @@ const rebuildResultMessages = (scriptData, messages) => { } }) - // 3. 如果 messages 中没有 script 类型消息,fallback 到 plotJson.fullContent + // 4. 如果 messages 中没有 script 类型消息,fallback 到 plotJson.fullContent const hasNovel = result.some(m => m.kind === 'novel') if (!hasNovel && scriptData?.plotJson?.fullContent) { result.push({ diff --git a/mini-program/src/pages/main/ScriptView.vue b/mini-program/src/pages/main/ScriptView.vue index 2941906..94a836f 100644 --- a/mini-program/src/pages/main/ScriptView.vue +++ b/mini-program/src/pages/main/ScriptView.vue @@ -1489,43 +1489,75 @@ const openScriptChat = async (payload = {}) => { // 从后端 messages 重建统一对话消息流,复刻生成时的 chat-bubble 展示 // 不再重复 push wishTheme/novelContent(messages 中已包含) const messagesList = messages.value || [] + + // 第一步:先把所有澄清问答按 question-answer 配对(按 messageOrder 升序) + // 原因:DB 中 answer 可能先于 question 存入(messageOrder 更小),不能简单按数组顺序匹配「最后一张 card」 + // 策略:question 按 messageOrder 升序排序,每条 question 配对它之后第一个未分配的 answer + const clarificationQuestions = messagesList + .filter(m => m.type === 'clarification_question') + .sort((a, b) => (a.messageOrder || 0) - (b.messageOrder || 0)) + const clarificationAnswers = messagesList + .filter(m => m.type === 'clarification_answer') + .sort((a, b) => (a.messageOrder || 0) - (b.messageOrder || 0)) + + // 为每个 question 配对一个 answer(按 messageOrder 升序,第一个 > question.messageOrder 且未被占用的) + const answerAssigned = new Set() + const questionAnswerMap = new Map() + const unmatchedAnswers = [] + clarificationQuestions.forEach(q => { + const matched = clarificationAnswers.find(a => + !answerAssigned.has(a.id) && (a.messageOrder || 0) > (q.messageOrder || 0) + ) + if (matched) { + answerAssigned.add(matched.id) + questionAnswerMap.set(q.id, matched) + } + }) + // 未匹配的 answer(如 messageOrder 在所有 question 之前的预填充)按独立 user 消息展示 + clarificationAnswers.forEach(a => { + if (!answerAssigned.has(a.id)) unmatchedAnswers.push(a) + }) + + // 为每个 question 生成 card 消息(含配对答案) + const cardMessagesByQuestionId = new Map() + clarificationQuestions.forEach(q => { + let card = {} + try { card = JSON.parse(q.content || '{}') } catch (e) { card = {} } + const answer = questionAnswerMap.get(q.id) + cardMessagesByQuestionId.set(q.id, { + id: q.id || createMessageId(), + role: 'assistant', + kind: 'card', + content: '', + card, + answer: answer?.content || '', + selectedValue: answer?.content || '', + pending: false, + time: formatMessageTime(), + submitted: true, + confirmed: true + }) + }) + + // 第二步:按 messageOrder 顺序遍历消息,构建统一消息流 messagesList.forEach(m => { if (m.type === 'clarification_question') { - let card = {} - try { card = JSON.parse(m.content || '{}') } catch (e) { card = {} } + const cardMsg = cardMessagesByQuestionId.get(m.id) + if (cardMsg) resultMessages.value.push(cardMsg) + } else if (m.type === 'clarification_answer') { + // 已配对到 question 的 answer 由 question 那一步负责渲染;这里跳过 + if (answerAssigned.has(m.id)) return + // 未配对的 answer(如用户对未识别 question 的回答)展示为独立 user 文本 resultMessages.value.push({ id: m.id || createMessageId(), - role: 'assistant', - kind: 'card', - content: '', - card, - answer: '', + role: 'user', + kind: 'text', + content: m.content || '', pending: false, time: formatMessageTime(), - submitted: true, - confirmed: true + submitted: false, + confirmed: false }) - } else if (m.type === 'clarification_answer') { - // 把答案合并到上一条 card 消息:answer 存用户原始 value,selectedValue 也存 value 用于渲染时映射 label - const lastCard = [...resultMessages.value].reverse().find(x => x.kind === 'card') - if (lastCard) { - const rawValue = m.content || '' - lastCard.answer = rawValue // 原始 value(用于查找 label) - lastCard.selectedValue = rawValue // 选中的 value(渲染时通过 options 映射成 label) - lastCard.submitted = true - } else { - // 无对应 question,作为独立 user 消息 - resultMessages.value.push({ - id: m.id || createMessageId(), - role: 'user', - kind: 'text', - content: m.content || '', - pending: false, - time: formatMessageTime(), - submitted: false, - confirmed: false - }) - } } else if (m.type === 'outline') { let outline = {} try { outline = JSON.parse(m.content || '{}') } catch (e) { outline = {} }