Commit 94a7e78b by Liu

fix:kuan

parent dee25152
......@@ -22,6 +22,9 @@ import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
import { setIsAsking } from '@/store/chatSlice'
import SdreamLoading from '@/components/SdreamLoading'
// 记录已自动触发提问的会话,避免严格模式或多次渲染造成重复调用
const autoSubmittedConversationIds = new Set<string>()
function formatDateTime(date: Date) {
const pad = (num: number) => num.toString().padStart(2, '0')
const year = date.getFullYear()
......@@ -80,6 +83,8 @@ export const Chat: React.FC = () => {
const [hasHistoryRecord, setHasHistoryRecord] = useState(false)
const [historyItemsCount, setHistoryItemsCount] = useState(0)
const [historyTimestamp, setHistoryTimestamp] = useState('')
// 当历史记录需要追加到新消息之后时,用于确定分隔线插入位置(分隔线插在 historyDividerIndex 前一个元素之后)
const [historyDividerIndex, setHistoryDividerIndex] = useState<number | null>(null)
const dispatch = useAppDispatch()
const { shouldSendQuestion, currentToolId, conversations } = useAppSelector((state: RootState) => state.conversation)
const scrollableRef = useRef<HTMLDivElement | any>(null)
......@@ -95,6 +100,8 @@ export const Chat: React.FC = () => {
const isFromTactics = searchParams.get('from') === 'tactics'
const [showClearConfirm, setShowClearConfirm] = useState(false)
const [isClearingHistory, setIsClearingHistory] = useState(false)
// 是否需要在自动提问完成后再拼接历史记录
const shouldAppendHistoryAfterAutoRef = useRef(false)
// 当外部系统直接以 /chat/:id 链接进入(没有 location.state,且 URL 中也没有 toolId)时,
// 视为一次新的会话入口:重置为通用模式,清除历史遗留的工具模式状态
......@@ -197,9 +204,23 @@ export const Chat: React.FC = () => {
/** 处理正常stream的数据 */
const handleStreamMesageData = (msg: any, question: string) => {
const resolvedQuestion = question || msg?.content?.data?.question || ''
setAllItems((prevItems) => {
const newItems = [...prevItems] // 创建数组的浅拷贝
const lastIndex = newItems.length - 1
const prevIndex = lastIndex - 1
// 如果自动提问时后端返回了具体的问题,补全上一条用户问题
if (
resolvedQuestion
&& prevIndex >= 0
&& newItems[prevIndex].role === 'user'
&& !newItems[prevIndex].question
) {
newItems[prevIndex] = {
...newItems[prevIndex],
question: resolvedQuestion,
}
}
if (lastIndex >= 0) {
// 创建最后一项的新对象,合并现有数据和新的 answer
const originalAnswer = (newItems[lastIndex].answerList?.[0]?.answer || '') + msg.content.data.answer
......@@ -219,8 +240,8 @@ export const Chat: React.FC = () => {
],
}
// 只有当question不为空时才设置question字段
if (question) {
updatedItem.question = question
if (resolvedQuestion) {
updatedItem.question = resolvedQuestion
}
newItems[lastIndex] = updatedItem
}
......@@ -230,6 +251,7 @@ export const Chat: React.FC = () => {
/** 处理超过最大条数的数据 */
const handleChatMaxCount = (msg: any, question: string) => {
const resolvedQuestion = question || msg?.content?.data?.question || ''
// toast(t => (
// <div className="flex items-center">
// <p className="text-[14px]">⚠️ 超过最大轮数上限!请新建对话 👉🏻</p>
......@@ -275,8 +297,8 @@ export const Chat: React.FC = () => {
],
}
// 只有当question不为空时才设置question字段
if (question) {
updatedItem.question = question
if (resolvedQuestion) {
updatedItem.question = resolvedQuestion
}
newItems[lastIndex] = updatedItem
}
......@@ -284,144 +306,20 @@ export const Chat: React.FC = () => {
})
}
/** 提交问题 */
const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string, isAutoCall?: boolean) => {
const resolvedToolId = toolId ?? currentToolId ?? undefined
// 根据是否是自动调用设置不同的参数
const busiType = isAutoCall ? '01' : '02'
const recordType = isAutoCall ? 'A02' : 'A01'
// 停止之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const isNew = allItems.length <= 1
dispatch(setIsAsking(true))
// 检查token
await fetchCheckTokenApi()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
if (question) {
// 一次性添加用户问题和空的AI回答
setAllItems(prevItems => [
...prevItems,
{
role: 'user',
question,
} as ChatRecord,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
}
else {
// 自动调用时,只添加空的AI回答
setAllItems(prevItems => [
...prevItems,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController()
let fetchUrl = `/conversation/api/conversation/mobile/v1/submit_question_stream`
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
let proxy = ''
if (viteOutputObj === 'open') {
proxy = import.meta.env.MODE !== 'prod' ? '/api' : '/dev-sdream-api'
}
else {
proxy = import.meta.env.MODE === 'dev' ? '/api' : '/dev-sdream-api'
}
fetchUrl = proxy + fetchUrl
// 构建请求参数,包含路由参数
const requestParams: Record<string, any> = {
question,
conversationId: currentIdRef.current,
stream: true,
productCode,
toolId: resolvedToolId,
busiType,
recordType,
}
// 添加路由参数(如果存在)
if (taskId !== undefined) {
requestParams.taskId = taskId
}
if (version !== undefined) {
requestParams.version = version
}
if (pinBeginTime !== undefined) {
requestParams.pinBeginTime = pinBeginTime
}
if (pinEndTime !== undefined) {
requestParams.pinEndTime = pinEndTime
}
if (partOrAll !== undefined) {
requestParams.partOrAll = partOrAll
}
if (channel !== undefined) {
requestParams.channel = channel
}
if (channelName !== undefined) {
requestParams.channelName = channelName
}
fetchStreamResponse(
fetchUrl,
requestParams,
(msg) => {
// 检查是否已被取消
if (abortControllerRef.current?.signal.aborted) {
return
}
// 处理错误
if (msg?.type === 'ERROR') {
// 如果是 AbortError,不显示错误
if (msg.content?.name === 'AbortError') {
return
}
return
}
// 正常的stream数据
if (msg?.type === 'DATA' && msg?.content?.code === '00000000') {
handleStreamMesageData(msg, question)
}
if (msg?.type === 'DATA' && msg?.content?.code === '01010005') {
handleChatMaxCount(msg, question)
return
}
if (msg.type === 'END') {
if (isNew) {
setTimeout(() => {
dispatch(fetchConversations())
}, 2000)
}
}
},
abortControllerRef.current.signal,
)
}
/** 获取qa记录 */
const getUserQaRecordPage = useCallback(async (conversationId: string) => {
const getUserQaRecordPage = useCallback(async (
conversationId: string,
options?: { append?: boolean, showLoading?: boolean },
) => {
const append = options?.append ?? false
const showLoading = options?.showLoading ?? !append
if (showLoading)
setIsLoading(true)
setHasHistoryRecord(false)
setHistoryItemsCount(0)
setHistoryTimestamp('')
if (!append)
setHistoryDividerIndex(null)
try {
// 检测是否从收藏页返回
const fromCollect = location.state?.fromCollect
......@@ -447,9 +345,32 @@ export const Chat: React.FC = () => {
}
return item
})
setAllItems(processedMessages)
const messagesWithoutSystemForAppend = append
? processedMessages.filter((_, idx) => !(idx === 0 && processedMessages[idx].role === 'system'))
: processedMessages
if (append) {
if (!messagesWithoutSystemForAppend.length) {
setHistoryDividerIndex(null)
}
setAllItems((prev) => {
const baseLength = prev.length
const historyLength = messagesWithoutSystemForAppend.length
if (historyLength) {
// 分割线放在历史记录之后
setHistoryDividerIndex(baseLength + historyLength - 1)
}
return [
...prev,
...messagesWithoutSystemForAppend,
]
})
}
else {
setAllItems(messagesWithoutSystemForAppend)
}
setHasHistoryRecord(hasQaRecords)
setHistoryItemsCount(hasQaRecords ? processedMessages.length : 0)
setHistoryItemsCount(hasQaRecords ? messagesWithoutSystemForAppend.length : 0)
if (hasQaRecords) {
setHistoryTimestamp(formatDateTime(new Date()))
}
......@@ -566,10 +487,151 @@ export const Chat: React.FC = () => {
// 错误处理
}
finally {
if (showLoading)
setIsLoading(false)
}
}, [dispatch, currentToolId, conversations, location.state])
/** 提交问题 */
const handleSubmitQuestion = async (question: string, productCode?: string, toolId?: string, isAutoCall?: boolean) => {
const resolvedToolId = toolId ?? currentToolId ?? undefined
// 根据是否是自动调用设置不同的参数
const busiType = isAutoCall ? '01' : '02'
const recordType = isAutoCall ? 'A02' : 'A01'
// 停止之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const isNew = allItems.length <= 1
dispatch(setIsAsking(true))
// 检查token
await fetchCheckTokenApi()
// 如果是自动调用(question为空),只添加空的AI回答,不添加用户问题
// 自动调用也先插入一条用户问题占位,待后端返回真实问题后补全
setAllItems(prevItems => [
...prevItems,
{
role: 'user',
question,
} as ChatRecord,
{
role: 'ai',
answerList: [{ answer: '' }],
} as ChatRecord,
])
// 创建新的 AbortController
abortControllerRef.current = new AbortController()
let fetchUrl = `/conversation/api/conversation/mobile/v1/submit_question_stream`
const viteOutputObj = import.meta.env.VITE_OUTPUT_OBJ || 'open'
let proxy = ''
if (viteOutputObj === 'open') {
proxy = import.meta.env.MODE !== 'prod' ? '/api' : '/dev-sdream-api'
}
else {
proxy = import.meta.env.MODE === 'dev' ? '/api' : '/dev-sdream-api'
}
fetchUrl = proxy + fetchUrl
// 构建请求参数,包含路由参数
const requestParams: Record<string, any> = {
conversationId: currentIdRef.current,
stream: true,
productCode,
toolId: resolvedToolId,
busiType,
recordType,
}
// 自动调用不传 question;手动调用正常传递
if (!isAutoCall) {
requestParams.question = question
}
// 添加路由参数(如果存在)
if (taskId !== undefined) {
requestParams.taskId = taskId
}
if (version !== undefined) {
requestParams.version = version
}
if (pinBeginTime !== undefined) {
requestParams.pinBeginTime = pinBeginTime
}
if (pinEndTime !== undefined) {
requestParams.pinEndTime = pinEndTime
}
if (partOrAll !== undefined) {
requestParams.partOrAll = partOrAll
}
if (channel !== undefined) {
requestParams.channel = channel
}
if (channelName !== undefined) {
requestParams.channelName = channelName
}
fetchStreamResponse(
fetchUrl,
requestParams,
(msg) => {
// 检查是否已被取消
if (abortControllerRef.current?.signal.aborted) {
return
}
// 处理错误
if (msg?.type === 'ERROR') {
// 如果是 AbortError,不显示错误
if (msg.content?.name === 'AbortError') {
return
}
return
}
// 正常的stream数据
if (msg?.type === 'DATA' && msg?.content?.code === '00000000') {
handleStreamMesageData(msg, question)
}
if (msg?.type === 'DATA' && msg?.content?.code === '01010005') {
handleChatMaxCount(msg, question)
return
}
if (msg.type === 'END') {
if (isNew) {
setTimeout(() => {
dispatch(fetchConversations())
}, 2000)
}
// 自动提问完成后,再追加历史记录
if (isAutoCall && shouldAppendHistoryAfterAutoRef.current && currentIdRef.current) {
shouldAppendHistoryAfterAutoRef.current = false
getUserQaRecordPage(currentIdRef.current, { append: true, showLoading: false })
}
}
},
abortControllerRef.current.signal,
)
}
/**
* 战术入口的自动提问(进入页面和“重新分析”按钮公用)
* 保证每次都按自动模式调用,不携带 question 参数
*/
const triggerAutoSubmit = useCallback(() => {
if (!currentIdRef.current || isLoading)
return
// 自动调用后重新追加最新历史
shouldAppendHistoryAfterAutoRef.current = true
handleSubmitQuestion('', undefined, currentToolId, true)
}, [handleSubmitQuestion, isLoading, currentToolId])
/** 点击滚动到底部 */
const scrollToBottom = () => {
scrollableRef.current.scrollTo(scrollableRef.current.scrollHeight, { behavior: 'smooth' })
......@@ -586,9 +648,21 @@ export const Chat: React.FC = () => {
currentIdRef.current = id
lastSentQuestionRef.current = '' // 重置标记
hasAutoSubmittedRef.current = false // 重置自动提交标记
shouldAppendHistoryAfterAutoRef.current = isFromTactics
// 初始化为仅包含 system 的列表,等待自动提问或历史拉取
setAllItems([{ role: 'system' } as ChatRecord])
setHasHistoryRecord(false)
setHistoryItemsCount(0)
setHistoryTimestamp('')
setHistoryDividerIndex(null)
// 非战术入口:立即拉取历史(保持原行为)
if (!isFromTactics) {
getUserQaRecordPage(id)
}
}, [id])
}
}, [id, isFromTactics, getUserQaRecordPage, dispatch])
// 处理shouldSendQuestion的变化 - 自动发送问题
useEffect(() => {
......@@ -608,34 +682,37 @@ export const Chat: React.FC = () => {
}
}, [shouldSendQuestion, isLoading, currentToolId])
// 页面加载时自动调用submit接口
// 页面加载时自动调用 submit 接口(战术入口只触发一次)
useEffect(() => {
if (
currentIdRef.current
&& !isLoading
&& !hasAutoSubmittedRef.current
&& isFromTactics
) {
if (!isFromTactics)
return
const conversationId = currentIdRef.current
// 确保存在会话且未处于加载中
if (!conversationId || isLoading)
return
// 避免严格模式或多次渲染导致重复自动提交
if (autoSubmittedConversationIds.has(conversationId))
return
autoSubmittedConversationIds.add(conversationId)
hasAutoSubmittedRef.current = true
// 确保历史记录加载完成后再调用submit接口
setTimeout(() => {
handleSubmitQuestion('', undefined, currentToolId, true) // 自动调用,传递 isAutoCall = true
triggerAutoSubmit() // 自动调用,传递 isAutoCall = true
}, 100)
}
}, [isLoading, currentToolId, isFromTactics])
}, [isLoading, isFromTactics, triggerAutoSubmit])
// 监听“重新分析”事件,重新发起一次自动提问(仍使用路由参数)
useEffect(() => {
const handleReAnalyze = () => {
if (currentIdRef.current && !isLoading) {
handleSubmitQuestion('', undefined, currentToolId, true)
}
triggerAutoSubmit()
}
window.addEventListener('tacticsReAnalyze', handleReAnalyze as EventListener)
return () => {
window.removeEventListener('tacticsReAnalyze', handleReAnalyze as EventListener)
}
}, [isLoading, currentToolId])
}, [triggerAutoSubmit])
// 根据 currentToolId 获取对应的 toolName
useEffect(() => {
......@@ -775,9 +852,10 @@ export const Chat: React.FC = () => {
const uniqueKey = recordId
? `${record.role}-${recordId}`
: `${record.role}-${record.question || record.answerList?.[0]?.answer || ''}-${index}`
const shouldShowHistoryDivider = hasHistoryRecord
const shouldShowHistoryDivider = historyDividerIndex !== null
&& hasHistoryRecord
&& historyItemsCount > 0
&& index === historyItemsCount - 1
&& index === historyDividerIndex
const historyDividerText = `以上为历史分析数据 ${historyTimestamp || formatDateTime(new Date())}`
return (
<React.Fragment key={uniqueKey}>
......
......@@ -81,7 +81,9 @@ export const Home: React.FC = () => {
}
// 处理工具按钮点击
const requestIdRef = useRef(0) // 标记最新请求,避免旧响应覆盖
const _handleToolClick = useCallback(async (isToolBtn: boolean, toolId?: string, ignoreUrlToolId?: boolean) => {
const currentRequestId = ++requestIdRef.current
// 提质增效模式 / 数据助手 / 通用模式:都先清空数据,重新拉常见问题
setOtherQuestions((prev: any) => ({
...prev,
......@@ -115,7 +117,8 @@ export const Home: React.FC = () => {
const res = await fetchEfficiencyQuestionList({
toolId: sessionToolId || finalToolId,
})
if (res && res.data && res.data.questions) {
// 只接受当前最新的请求结果,避免旧请求覆盖新请求
if (currentRequestId === requestIdRef.current && res?.data?.questions) {
setOtherQuestions((prev: any) => ({
...prev,
content: res.data.questions || [],
......@@ -126,7 +129,9 @@ export const Home: React.FC = () => {
console.error('获取工具相关问题失败:', error)
}
finally {
setIsDataLoaded(true) // 无论成功失败都标记为已加载
// 仅在当前请求仍是最新时更新加载态,避免闪烁
if (currentRequestId === requestIdRef.current)
setIsDataLoaded(true)
}
}, [originalOtherQuestions, location.search])
......@@ -329,7 +334,7 @@ export const Home: React.FC = () => {
<div className="hidden sm:flex h-full flex-none ml-auto">
<div
className="w-full h-full bg-transparent box-border rounded-[24px]"
style={{ width: '420px', height: 'calc(100vh - 64px)', background: '#FFFFFF', padding: '0 30px' }}
style={{ height: 'calc(100vh - 64px)', background: '#FFFFFF', padding: '0 30px' }}
>
<Outlet />
</div>
......
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