Commit 7db07d6f by weiyudumei

fix: 修复历史记录自动提交问题,历史记录只隐藏UI不执行自动提交,只有流式返回的新记录才执行自动提交

parent 5b082fd7
...@@ -14,111 +14,111 @@ export function fetchStreamResponse(url: string, body: Record<string, any>, onMe ...@@ -14,111 +14,111 @@ export function fetchStreamResponse(url: string, body: Record<string, any>, onMe
let dataMsgBuffer = '' let dataMsgBuffer = ''
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
function processMessage(reader: any) { function processMessage(reader: any) {
reader.read().then((content: any) => { reader.read().then((content: any) => {
// 检查是否已被中止 // 检查是否已被中止
if (signal?.aborted) { if (signal?.aborted) {
resolve() // 中止时也 resolve,表示流已结束 resolve() // 中止时也 resolve,表示流已结束
return return
} }
buffer += decoder.decode(content.value, { stream: !content.done }) buffer += decoder.decode(content.value, { stream: !content.done })
const lines = buffer.split('\n') const lines = buffer.split('\n')
buffer = lines.pop() as string buffer = lines.pop() as string
lines.forEach((line) => { lines.forEach((line) => {
if (line === '') { // 读取到空行,一个数据块发送完成 if (line === '') { // 读取到空行,一个数据块发送完成
try { try {
onMessage({
type: 'DATA',
content: JSON.parse(dataMsgBuffer),
})
}
catch {
// 解析失败时静默处理,避免报错
}
dataMsgBuffer = ''
return
}
const [type] = line.split(':', 1)
const content = line.substring(type.length + 1)
if (type === 'data') { // 数据块没有收到空行之前放入buffer中
// SSE 格式中,data: 后面的内容前面可能有一个空格,trim 可以安全处理
dataMsgBuffer += content.trim()
}
else if (type === '' && content !== '') { // 服务端发送的注释,用于保证链接不断开
onMessage({ onMessage({
type: 'COMMENT', type: 'DATA',
content: content.trim(), content: JSON.parse(dataMsgBuffer),
}) })
} }
else { catch {
onMessage({ // 解析失败时静默处理,避免报错
type,
content: content.trim(),
})
} }
}) dataMsgBuffer = ''
if (!content.done) { return
processMessage(reader) }
const [type] = line.split(':', 1)
const content = line.substring(type.length + 1)
if (type === 'data') { // 数据块没有收到空行之前放入buffer中
// SSE 格式中,data: 后面的内容前面可能有一个空格,trim 可以安全处理
dataMsgBuffer += content.trim()
}
else if (type === '' && content !== '') { // 服务端发送的注释,用于保证链接不断开
onMessage({
type: 'COMMENT',
content: content.trim(),
})
} }
else { else {
onMessage({
type,
content: content.trim(),
})
}
})
if (!content.done) {
processMessage(reader)
}
else {
// 流结束,resolve Promise(不再发送 END 消息) // 流结束,resolve Promise(不再发送 END 消息)
resolve() resolve()
} }
}).catch((error: unknown) => { }).catch((error: unknown) => {
// 如果是 AbortError,resolve 而不是 reject(因为这是主动取消) // 如果是 AbortError,resolve 而不是 reject(因为这是主动取消)
if (error instanceof Error && error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {
resolve() resolve()
return return
} }
onMessage({ onMessage({
type: 'ERROR', type: 'ERROR',
content: error, content: error,
})
reject(error) // 其他错误 reject Promise
}) })
} reject(error) // 其他错误 reject Promise
const tokenStr = window.localStorage.getItem('__TOKEN__') || '""' })
let token = '' }
try { const tokenStr = window.localStorage.getItem('__TOKEN__') || '""'
token = JSON.parse(tokenStr) let token = ''
} try {
catch { token = JSON.parse(tokenStr)
token = '' }
} catch {
token = ''
}
fetch(url, { fetch(url, {
headers: { headers: {
'accept': 'text/event-stream', 'accept': 'text/event-stream',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Token': token, 'X-Token': token,
}, },
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
signal, signal,
})
.then((response) => {
return response.body?.getReader()
}) })
.then((response) => { .then((reader) => {
return response.body?.getReader()
})
.then((reader) => {
if (!reader) { if (!reader) {
reject(new Error('Failed to get reader from response')) reject(new Error('Failed to get reader from response'))
return return
} }
return processMessage(reader) return processMessage(reader)
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
// 如果是 AbortError,resolve 而不是 reject(因为这是主动取消) // 如果是 AbortError,resolve 而不是 reject(因为这是主动取消)
if (error instanceof Error && error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {
resolve() resolve()
return return
} }
onMessage({ onMessage({
type: 'ERROR', type: 'ERROR',
content: error, content: error,
}) })
reject(error) reject(error)
}) })
}) })
} }
/** /**
......
...@@ -949,19 +949,19 @@ export const Chat: React.FC = () => { ...@@ -949,19 +949,19 @@ export const Chat: React.FC = () => {
> >
<div className={styles.inter}> <div className={styles.inter}>
{allItems.map((record, index) => { {allItems.map((record, index) => {
// 如果历史记录中 firstQaFlag 为 true,则不展示该条 chatItem
if (record.role === 'ai' && record.answerList?.[0]?.firstQaFlag === true) {
return null
}
const recordId = record.answerList?.[0]?.recordId || record.groupId const recordId = record.answerList?.[0]?.recordId || record.groupId
const uniqueKey = recordId const uniqueKey = recordId
? `${record.role}-${recordId}` ? `${record.role}-${recordId}`
: `${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}` : `${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}`
// 如果历史记录中 firstQaFlag 为 true,隐藏 UI 但保留组件渲染(确保自动提交逻辑能执行)
const shouldHideHistoryItem = record.role === 'ai' && record.answerList?.[0]?.firstQaFlag === true
return ( return (
<div <div
className="w-full chatItem mx-auto" className="w-full chatItem mx-auto"
key={uniqueKey} key={uniqueKey}
style={shouldHideHistoryItem ? { visibility: 'hidden', height: 0, overflow: 'hidden', margin: 0, padding: 0 } : undefined}
> >
{record.role === 'system' && <ChatWelcome toolName={currentToolName} />} {record.role === 'system' && <ChatWelcome toolName={currentToolName} />}
{record.role === 'user' && <ChatItemUser record={record} />} {record.role === 'user' && <ChatItemUser record={record} />}
......
...@@ -60,7 +60,7 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({ ...@@ -60,7 +60,7 @@ export const CardWorkflowForm: React.FC<CardWorkflowFormProps> = ({
} }
}, [attachment, answer.firstQaFlag, searchParams, textFields]) }, [attachment, answer.firstQaFlag, searchParams, textFields])
// 如果 firstQaFlag 为 true,自动提交 // 如果 firstQaFlag 为 true,自动提交(仅对流式返回的新记录,历史记录不会渲染此组件)
useEffect(() => { useEffect(() => {
// 检查 firstQaFlag 是否从 false/undefined 变为 true(首次变为 true) // 检查 firstQaFlag 是否从 false/undefined 变为 true(首次变为 true)
const isFirstQaFlagChangedToTrue = answer.firstQaFlag === true && lastFirstQaFlagRef.current !== true const isFirstQaFlagChangedToTrue = answer.firstQaFlag === true && lastFirstQaFlagRef.current !== true
......
...@@ -60,8 +60,9 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, ...@@ -60,8 +60,9 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex,
// 当前默认始终展示点赞 / 点踩 / 复制等操作区(是否展示由下层基于卡片类型自行决定) // 当前默认始终展示点赞 / 点踩 / 复制等操作区(是否展示由下层基于卡片类型自行决定)
const shouldHideOperate = false const shouldHideOperate = false
// 如果 firstQaFlag 为 true 且存在 workflow 卡片,隐藏整个 chatItem(表单会自动提交,用户不需要看到这次的结果) // 如果 firstQaFlag 为 true 且存在 workflow 卡片,隐藏整个 chatItem(用户不需要看到这次的结果)
// 但保留组件的渲染,确保 CardWorkflowForm 的自动提交逻辑能够执行 // 注意:历史记录(isShow === true)不会渲染 CardWorkflowForm,不会自动提交
// 只有流式返回的新记录(isShow === false)才会渲染 CardWorkflowForm 并执行自动提交
const hasWorkflowCard = (item.cardList || []).some(attachment => attachment.type === 'card-workflow') const hasWorkflowCard = (item.cardList || []).some(attachment => attachment.type === 'card-workflow')
const shouldHideChatItem = item.firstQaFlag === true && hasWorkflowCard const shouldHideChatItem = item.firstQaFlag === true && hasWorkflowCard
......
...@@ -202,7 +202,9 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer ...@@ -202,7 +202,9 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ isLastAnswer
</MarkdownDetail> </MarkdownDetail>
</div> </div>
)} )}
{!isTyping {/* ChatAnswerParser 用于流式返回的新记录(isShow === false),即使 firstQaFlag === true 也要渲染 ChatAnswerAttachment,确保自动提交逻辑能执行
如果 firstQaFlag 为 true 且存在 workflow 卡片,即使 isTyping 为 true 也要渲染 ChatAnswerAttachment */}
{((!isTyping) || (answer.firstQaFlag === true && (answer.cardList || []).some(attachment => attachment.type === 'card-workflow')))
&& answer.cardList && answer.cardList
&& answer.cardList?.length !== 0 && answer.cardList?.length !== 0
&& ( && (
......
...@@ -28,7 +28,9 @@ export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLa ...@@ -28,7 +28,9 @@ export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer, isLa
</MarkdownDetail> </MarkdownDetail>
</div> </div>
)} )}
{answer.cardList && answer.cardList?.length !== 0 && ( {/* 历史记录(isShow === true)且 firstQaFlag === true 时,不渲染 ChatAnswerAttachment(不需要自动提交)
流式返回的新记录(isShow === false)即使 firstQaFlag === true 也要渲染,确保自动提交逻辑能执行 */}
{answer.cardList && answer.cardList?.length !== 0 && !(answer.isShow === true && answer.firstQaFlag === true) && (
<ChatAnswerAttachment <ChatAnswerAttachment
onSubmitQuestion={onSubmitQuestion} onSubmitQuestion={onSubmitQuestion}
isLastAnswer={isLastAnswer} isLastAnswer={isLastAnswer}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment