fix:澄清问答配对逻辑修复(按 messageOrder 升序匹配)

问题: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。
This commit is contained in:
2026-07-26 17:07:22 +08:00
parent 7d19fa5898
commit ed67739a47
2 changed files with 112 additions and 44 deletions
@@ -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 => { messages.forEach(m => {
if (m.type === 'clarification_question') { if (m.type === 'clarification_question') {
let card = {} const cardMsg = cardMessagesByQuestionId.get(m.id)
try { card = JSON.parse(m.content || '{}') } catch (e) { card = {} } if (cardMsg) result.push(cardMsg)
result.push({ id: m.id, role: 'assistant', kind: 'card', card, answer: '', submitted: true })
} else if (m.type === 'clarification_answer') { } else if (m.type === 'clarification_answer') {
const lastCard = [...result].reverse().find(x => x.kind === 'card') // 已配对的 answer 由 question 那一步负责渲染;这里跳过
if (lastCard) { if (answerAssigned.has(m.id)) return
// answer 存用户提交的 value,渲染时通过 card.options 映射成中文 label // 未配对的 answer 展示为独立 user 文本
lastCard.answer = m.content || '' result.push({ id: m.id, role: 'user', kind: 'text', content: m.content || '' })
lastCard.selectedValue = m.content || ''
} else {
// 无对应 question,作为独立 user 消息
result.push({ id: m.id, role: 'user', kind: 'text', content: m.content || '' })
}
} else if (m.type === 'outline') { } else if (m.type === 'outline') {
let outline = {} let outline = {}
try { outline = JSON.parse(m.content || '{}') } catch (e) { 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') const hasNovel = result.some(m => m.kind === 'novel')
if (!hasNovel && scriptData?.plotJson?.fullContent) { if (!hasNovel && scriptData?.plotJson?.fullContent) {
result.push({ result.push({
+62 -30
View File
@@ -1489,43 +1489,75 @@ const openScriptChat = async (payload = {}) => {
// 从后端 messages 重建统一对话消息流,复刻生成时的 chat-bubble 展示 // 从后端 messages 重建统一对话消息流,复刻生成时的 chat-bubble 展示
// 不再重复 push wishTheme/novelContentmessages 中已包含) // 不再重复 push wishTheme/novelContentmessages 中已包含)
const messagesList = messages.value || [] 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 => { messagesList.forEach(m => {
if (m.type === 'clarification_question') { if (m.type === 'clarification_question') {
let card = {} const cardMsg = cardMessagesByQuestionId.get(m.id)
try { card = JSON.parse(m.content || '{}') } catch (e) { card = {} } 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({ resultMessages.value.push({
id: m.id || createMessageId(), id: m.id || createMessageId(),
role: 'assistant', role: 'user',
kind: 'card', kind: 'text',
content: '', content: m.content || '',
card,
answer: '',
pending: false, pending: false,
time: formatMessageTime(), time: formatMessageTime(),
submitted: true, submitted: false,
confirmed: true confirmed: false
}) })
} else if (m.type === 'clarification_answer') {
// 把答案合并到上一条 card 消息:answer 存用户原始 valueselectedValue 也存 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') { } else if (m.type === 'outline') {
let outline = {} let outline = {}
try { outline = JSON.parse(m.content || '{}') } catch (e) { outline = {} } try { outline = JSON.parse(m.content || '{}') } catch (e) { outline = {} }