Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
sdream-ai-fe
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
侯明涛
sdream-ai-fe
Commits
7db07d6f
Commit
7db07d6f
authored
Jan 23, 2026
by
weiyudumei
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 修复历史记录自动提交问题,历史记录只隐藏UI不执行自动提交,只有流式返回的新记录才执行自动提交
parent
5b082fd7
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
97 additions
and
92 deletions
+97
-92
src/api/chat.ts
+82
-82
src/pages/Chat/Chat.tsx
+5
-5
src/pages/Chat/components/ChatItem/CardWorkflowForm.tsx
+1
-1
src/pages/Chat/components/ChatItem/ChatAnswerBox.tsx
+3
-2
src/pages/Chat/components/ChatItem/ChatAnswerParser.tsx
+3
-1
src/pages/Chat/components/ChatItem/ChatAnswerShower.tsx
+3
-1
No files found.
src/api/chat.ts
View file @
7db07d6f
...
@@ -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
)
})
})
})
})
}
}
/**
/**
...
...
src/pages/Chat/Chat.tsx
View file @
7db07d6f
...
@@ -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
}
/>
}
...
...
src/pages/Chat/components/ChatItem/CardWorkflowForm.tsx
View file @
7db07d6f
...
@@ -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
...
...
src/pages/Chat/components/ChatItem/ChatAnswerBox.tsx
View file @
7db07d6f
...
@@ -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
...
...
src/pages/Chat/components/ChatItem/ChatAnswerParser.tsx
View file @
7db07d6f
...
@@ -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
&&
(
&&
(
...
...
src/pages/Chat/components/ChatItem/ChatAnswerShower.tsx
View file @
7db07d6f
...
@@ -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
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment