Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
水玉婷
ai-wechat
Commits
d7e7c0e3
Commit
d7e7c0e3
authored
Dec 23, 2025
by
水玉婷
Browse files
feat:添加重连机制
parent
c57963b0
Changes
4
Hide whitespace changes
Inline
Side-by-side
src/views/components/AiChat.vue
View file @
d7e7c0e3
<
template
>
<div
class=
"chat-container"
:class=
"props?.customClass"
>
<!-- 聊天头部 -->
<div
class=
"chat-header"
v-if=
"props.dialogSessionId || hasStartedConversation"
>
<div
class=
"chat-header"
v-if=
"props
?
.dialogSessionId || hasStartedConversation"
>
<div
class=
"header-avatar"
>
<img
:src=
"props.logoUrl || defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
<img
:src=
"props
?
.logoUrl || defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
</div>
<div
class=
"header-info"
>
<h2>
{{
props
.
dialogSessionId
?
props
?.
detailData
?.
title
||
'
继续对话
'
:
'
新建对话
'
}}
</h2>
<h2>
{{
props
?
.
dialogSessionId
?
props
?.
detailData
?.
title
||
'
继续对话
'
:
'
新建对话
'
}}
</h2>
</div>
</div>
<!-- 当没有dialogSessionId且未开始对话时显示介绍页面 -->
<div
class=
"chat-intro-center"
v-if=
"!props.dialogSessionId && !hasStartedConversation"
>
<div
class=
"chat-intro-center"
v-if=
"!props
?
.dialogSessionId && !hasStartedConversation"
>
<div
class=
"intro-content"
>
<img
:src=
"defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
<h3>
嗨,我是国械小智
</h3>
...
...
@@ -20,12 +20,12 @@
</div>
<!-- 消息区域 -->
<div
class=
"chat-messages"
ref=
"messagesContainer"
v-if=
"props.dialogSessionId || hasStartedConversation"
>
<div
class=
"chat-messages"
ref=
"messagesContainer"
v-if=
"props
?
.dialogSessionId || hasStartedConversation"
>
<div
v-for=
"(msg, index) in messages"
:key=
"index"
:class=
"['message', msg.messageType]"
>
<div
class=
"avatar-container"
>
<div
class=
"avatar"
>
<template
v-if=
"msg.messageType === 'received'"
>
<img
:src=
"props.logoUrl || defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
<img
:src=
"props
?
.logoUrl || defaultAvatar"
alt=
"avatar"
class=
"avatar-image"
/>
</
template
>
<
template
v-else
>
<user-outlined
/>
...
...
@@ -82,9 +82,9 @@
<div
class=
"chat-input"
>
<!-- 语音识别按钮 -->
<VoiceRecognition
ref=
"voiceRecognitionRef"
:disabled=
"loading"
:debug=
"true"
:token=
"props.token"
:appCode=
"props.appCode"
:apiBaseUrl=
"props.apiBaseUrl"
:token=
"props
?
.token"
:appCode=
"props
?
.appCode"
:apiBaseUrl=
"props
?
.apiBaseUrl"
@
audio=
"handleVoiceAudio"
@
error=
"handleVoiceError"
class=
"voice-recognition-wrapper"
/>
...
...
@@ -253,6 +253,48 @@ const handleSSEMessage = (data: SSEData) => {
}
};
// 添加网络状态模拟消息函数(带去重机制和样式区分)
const
lastNetworkStatusMessage
=
ref
<
{
type
:
string
,
message
:
string
,
timestamp
:
number
}
|
null
>
(
null
);
const
addNetworkStatusMessage
=
(
type
:
'
error
'
|
'
success
'
,
message
:
string
)
=>
{
const
now
=
Date
.
now
();
// 去重逻辑:相同类型和内容的消息在5秒内不重复显示
if
(
lastNetworkStatusMessage
.
value
&&
lastNetworkStatusMessage
.
value
.
type
===
type
&&
lastNetworkStatusMessage
.
value
.
message
===
message
&&
now
-
lastNetworkStatusMessage
.
value
.
timestamp
<
5000
)
{
console
.
log
(
`📡
${
type
===
'
error
'
?
'
❌
'
:
'
✅
'
}
跳过重复网络状态消息:`
,
message
);
return
;
}
lastNetworkStatusMessage
.
value
=
{
type
,
message
,
timestamp
:
now
};
const
statusMessage
:
Message
=
{
messageType
:
'
received
'
,
// 改回原来的receive类型
avatar
:
'
AI
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[{
content
:
type
===
'
error
'
?
templateService
.
generateErrorTemplate
(
message
)
:
templateService
.
generateSuccessTemplate
(
message
),
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
}]
};
messages
.
value
.
push
(
statusMessage
);
nextTick
(()
=>
{
scrollToBottom
();
});
console
.
log
(
`📡
${
type
===
'
error
'
?
'
❌
'
:
'
✅
'
}
网络状态消息:`
,
message
);
};
// 创建SSE服务实例
const
sseService
=
createSSEService
({
apiBaseUrl
:
props
.
apiBaseUrl
,
...
...
@@ -262,17 +304,55 @@ const sseService = createSSEService({
},
{
onMessage
:
handleSSEMessage
,
onError
:
(
error
)
=>
{
console
.
error
(
'
SSE
error
:
'
,
error
);
console
.
error
(
'
❌
SSE
连接错误
:
'
,
error
);
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
closeSSE
();
// 关键修改:SSE连接错误时也重置AI响应状态,确保重连后重新开始对话块
currentAIResponse
.
value
=
null
;
currentBlockIndex
.
value
=
-
1
;
// 优化:只在没有网络断开消息的情况下显示SSE错误消息
if
(
!
lastNetworkStatusMessage
.
value
||
lastNetworkStatusMessage
.
value
.
type
!==
'
error
'
)
{
addNetworkStatusMessage
(
'
error
'
,
'
服务连接异常,正在尝试重新连接...
'
);
}
console
.
log
(
'
🔄 等待自动重连...
'
);
// 不再手动关闭SSE,让重连逻辑自动处理
// closeSSE();
},
onOpen
:
(
event
)
=>
{
console
.
log
(
'
SSE连接已建立
'
,
event
);
console
.
log
(
'
✅ SSE连接已建立
'
,
event
);
// 只在真正需要时显示连接成功消息
// 避免在正常连接时也显示恢复消息
if
(
lastNetworkStatusMessage
.
value
?.
type
===
'
error
'
)
{
addNetworkStatusMessage
(
'
success
'
,
'
服务连接已恢复,可以正常对话了!
'
);
}
},
onReconnect
:
(
newDialogSessionId
)
=>
{
console
.
log
(
'
SSE重连成功,新的dialogSessionId:
'
,
newDialogSessionId
);
console
.
log
(
'
🔄
SSE重连成功,新的dialogSessionId:
'
,
newDialogSessionId
);
dialogSessionId
.
value
=
newDialogSessionId
;
// 优化:只在有错误消息的情况下显示重连成功消息
if
(
lastNetworkStatusMessage
.
value
?.
type
===
'
error
'
)
{
addNetworkStatusMessage
(
'
success
'
,
'
服务重连成功,对话已恢复!
'
);
}
},
onNetworkOffline
:
()
=>
{
console
.
log
(
'
📡 网络断开事件触发
'
);
// 网络断开时添加错误消息
addNetworkStatusMessage
(
'
error
'
,
'
网络连接已断开,正在尝试重新连接...
'
);
// 关键修改:网络断开时重置AI响应状态,确保网络恢复后重新开始对话块
console
.
log
(
'
💡 网络断开,重置AI响应状态,准备网络恢复时重新开始对话块
'
);
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
currentAIResponse
.
value
=
null
;
currentBlockIndex
.
value
=
-
1
;
},
onNetworkOnline
:
()
=>
{
console
.
log
(
'
🌐 网络恢复事件触发
'
);
// 优化:网络恢复事件只记录日志,不显示消息,避免与重连成功消息重复
// 真正的恢复消息由onReconnect处理
}
});
...
...
@@ -447,6 +527,7 @@ const closeSSE = () => {
// 初始化SSE连接
const
initSSE
=
()
=>
{
console
.
log
(
'
🔗 初始化SSE连接,当前会话ID:
'
,
dialogSessionId
.
value
||
'
空
'
);
sseService
.
initSSE
(
dialogSessionId
.
value
);
};
...
...
@@ -552,6 +633,13 @@ defineExpose({
// 生命周期
onMounted
(()
=>
{
console
.
log
(
'
组件挂载,初始 dialogSessionId:
'
,
props
.
dialogSessionId
);
// 添加防重复初始化检查
if
(
hasStartedConversation
.
value
)
{
console
.
log
(
'
⚠️ 检测到重复初始化,跳过SSE初始化
'
);
return
;
}
initSSE
();
scrollToBottom
();
if
(
props
.
dialogSessionId
)
{
...
...
src/views/components/style.less
View file @
d7e7c0e3
...
...
@@ -19,7 +19,6 @@
@gray-6: #666666;
@gray-7: #333333;
@success-color: #52c41a;
@success-hover: #46a51a; // 添加缺失的变量定义
@error-color: #f5222d;
@warning-color: #faad14;
...
...
@@ -300,6 +299,14 @@ li {
font-size: 14px;
}
// 成功消息样式
:deep(.message-success) {
line-height: 1.5;
color: @success-color;
white-space: pre-wrap;
font-size: 14px;
}
// 思考消息样式
:deep(.think-message) {
color: @gray-5;
...
...
src/views/components/utils/contentTemplateService.ts
View file @
d7e7c0e3
...
...
@@ -12,6 +12,7 @@ import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, merge
option
:
(
optionData
:
any
)
=>
string
;
iframe
:
(
iframeData
:
any
)
=>
string
;
tips
:
(
content
:
string
)
=>
string
;
success
:
(
content
:
string
)
=>
string
;
}
// 消息块类型定义
...
...
@@ -133,6 +134,11 @@ export class ContentTemplateService {
return
`<div class="message-error">
${
content
}
</div>`
;
},
// 成功信息
success
:
(
content
:
string
)
=>
{
return
`<div class="message-success">
${
content
}
</div>`
;
},
// 表格模板 - 使用独立的表格模板工具
table
:
(
tableData
:
any
)
=>
{
return
tableTemplate
(
tableData
);
...
...
@@ -224,6 +230,10 @@ export class ContentTemplateService {
return
this
.
templates
.
error
(
content
);
}
public
generateSuccessTemplate
(
content
:
string
):
string
{
return
this
.
templates
.
success
(
content
);
}
// 处理SSE消息的核心方法
...
...
src/views/components/utils/sseService.ts
View file @
d7e7c0e3
...
...
@@ -25,6 +25,8 @@ export interface SSEHandlers {
onError
?:
(
error
:
any
)
=>
void
;
onOpen
?:
(
event
:
any
)
=>
void
;
onReconnect
?:
(
newDialogSessionId
:
string
)
=>
void
;
onNetworkOffline
?:
()
=>
void
;
// 网络断开事件
onNetworkOnline
?:
()
=>
void
;
// 网络恢复事件
}
// SSE服务类 - 专注于SSE连接管理
...
...
@@ -34,15 +36,37 @@ export class SSEService {
private
handlers
:
SSEHandlers
;
private
isReconnecting
:
Ref
<
boolean
>
=
ref
(
false
);
private
timeArr
:
NodeJS
.
Timeout
[]
=
[];
private
reconnectAttempts
:
number
=
0
;
private
maxReconnectAttempts
:
number
=
5
;
private
currentDialogSessionId
:
string
=
''
;
private
connectionMonitorInterval
:
NodeJS
.
Timeout
|
null
=
null
;
// 心跳检测配置
private
readonly
HEARTBEAT_INTERVAL
=
30000
;
// 30秒检测一次
private
readonly
RECONNECT_DELAY_BASE
=
3000
;
// 3秒基础重连延迟
private
readonly
MAX_RECONNECT_DELAY
=
60000
;
// 最大重连延迟60秒
private
readonly
MAX_RECONNECT_ATTEMPTS
=
5
;
// 最大重连次数
constructor
(
config
:
SSEServiceConfig
,
handlers
:
SSEHandlers
=
{})
{
this
.
config
=
config
;
this
.
handlers
=
handlers
;
// 监听网络状态变化
if
(
typeof
window
!==
'
undefined
'
)
{
window
.
addEventListener
(
'
online
'
,
this
.
handleNetworkOnline
.
bind
(
this
));
window
.
addEventListener
(
'
offline
'
,
this
.
handleNetworkOffline
.
bind
(
this
));
}
}
// 初始化SSE连接
public
initSSE
(
dialogSessionId
:
string
):
void
{
try
{
this
.
currentDialogSessionId
=
dialogSessionId
;
this
.
reconnectAttempts
=
0
;
// 重置重连次数
console
.
log
(
'
🔗 开始建立SSE连接...
'
,
'
会话ID:
'
,
dialogSessionId
||
'
空
'
);
// 即使会话ID为空,也尝试建立连接
// 服务器可能会返回新的会话ID,或者允许空会话ID的连接
const
url
=
`
${
this
.
config
.
apiBaseUrl
}
/aiService/sse/join/
${
this
.
config
.
params
?.
stage
||
''
}
?app-id=
${
this
.
config
.
params
?.
appId
||
''
}
&dialog-session-id=
${
dialogSessionId
||
''
}
`
;
this
.
eventSource
=
new
EventSourcePolyfill
(
url
,
{
headers
:
{
...
...
@@ -52,10 +76,18 @@ export class SSEService {
},
withCredentials
:
true
,
connectionTimeout
:
60000
,
heartbeatTimeout
:
45000
,
// 心跳超时时间
});
this
.
eventSource
.
onopen
=
(
event
)
=>
{
// 移除这里的日志,只在外部处理器中打印
this
.
reconnectAttempts
=
0
;
// 连接成功时重置重连次数
this
.
isReconnecting
.
value
=
false
;
console
.
log
(
'
✅ SSE连接已建立
'
);
// 启动心跳检测
this
.
startHeartbeatCheck
();
if
(
this
.
handlers
.
onOpen
)
{
this
.
handlers
.
onOpen
(
event
);
}
...
...
@@ -65,6 +97,9 @@ export class SSEService {
try
{
const
data
:
SSEData
=
JSON
.
parse
(
event
.
data
);
// 重置重连次数,因为收到了有效消息
this
.
reconnectAttempts
=
0
;
// 只传递原始数据,模板处理在外部进行
if
(
this
.
handlers
.
onMessage
)
{
this
.
handlers
.
onMessage
(
data
);
...
...
@@ -75,20 +110,24 @@ export class SSEService {
});
this
.
eventSource
.
onerror
=
(
error
)
=>
{
console
.
error
(
'
SSE
error
:
'
,
error
);
console
.
error
(
'
❌
SSE
连接错误
:
'
,
error
);
if
(
this
.
handlers
.
onError
)
{
this
.
handlers
.
onError
(
error
);
}
this
.
closeSSE
();
// 添加错误重连逻辑
if
(
!
this
.
isReconnecting
.
value
)
{
// 简单重连逻辑
if
(
!
this
.
isReconnecting
.
value
&&
this
.
reconnectAttempts
<
this
.
MAX_RECONNECT_ATTEMPTS
)
{
this
.
reconnectAttempts
++
;
const
delay
=
this
.
calculateReconnectDelay
();
console
.
log
(
`🔄 SSE连接断开,将在
${
delay
}
ms 后尝试第
${
this
.
reconnectAttempts
}
次重连`
);
setTimeout
(()
=>
{
if
(
d
ialogSessionId
)
{
this
.
reconnectSSE
(
d
ialogSessionId
);
if
(
this
.
currentD
ialogSessionId
)
{
this
.
reconnectSSE
(
this
.
currentD
ialogSessionId
);
}
},
3000
);
},
delay
);
}
};
...
...
@@ -100,20 +139,32 @@ export class SSEService {
// 重新连接SSE
public
reconnectSSE
(
newDialogSessionId
:
string
):
void
{
if
(
this
.
isReconnecting
.
value
)
{
console
.
log
(
'
正在重连中,跳过重复重连
'
);
console
.
log
(
'
⏳
正在重连中,跳过重复重连
'
);
return
;
}
if
(
this
.
reconnectAttempts
>=
this
.
maxReconnectAttempts
)
{
console
.
error
(
'
⛔ 重连次数已达上限,停止重连
'
);
return
;
}
this
.
isReconnecting
.
value
=
true
;
console
.
log
(
'
开始重连SSE
,新的dialogSessionId
:
'
,
newDialogSessionId
);
console
.
log
(
`🔄
开始重连SSE
(第
${
this
.
reconnectAttempts
}
次)`
,
'
会话ID
:
'
,
newDialogSessionId
||
'
空
'
);
this
.
closeSSE
();
// 只关闭SSE连接,但不停止心跳检测
if
(
this
.
eventSource
)
{
try
{
this
.
eventSource
.
close
();
this
.
eventSource
=
null
;
}
catch
(
err
)
{
console
.
warn
(
'
关闭SSE连接时出错:
'
,
err
);
}
}
const
reconnectTimeout
=
setTimeout
(()
=>
{
this
.
initSSE
(
newDialogSessionId
);
setTimeout
(()
=>
{
this
.
isReconnecting
.
value
=
false
;
},
2000
);
},
500
);
// 重连状态会在连接成功后自动重置
},
100
);
this
.
timeArr
.
push
(
reconnectTimeout
);
...
...
@@ -133,11 +184,17 @@ export class SSEService {
}
}
// 停止心跳检测
this
.
stopHeartbeatCheck
();
// 清理所有定时器
this
.
timeArr
.
forEach
(
timeout
=>
{
clearTimeout
(
timeout
);
});
this
.
timeArr
=
[];
// 重置重连状态
this
.
isReconnecting
.
value
=
false
;
}
// 获取重连状态
...
...
@@ -153,6 +210,136 @@ export class SSEService {
return
this
.
eventSource
.
readyState
;
}
// 计算重连延迟(指数退避策略)
private
calculateReconnectDelay
():
number
{
// 指数退避:3s, 6s, 12s, 24s, 48s
return
Math
.
min
(
this
.
RECONNECT_DELAY_BASE
*
Math
.
pow
(
2
,
this
.
reconnectAttempts
-
1
),
this
.
MAX_RECONNECT_DELAY
);
}
// 处理网络恢复
private
handleNetworkOnline
():
void
{
console
.
log
(
'
🌐 网络已恢复,检查SSE连接状态
'
);
console
.
log
(
'
📊 当前状态 - 重连中:
'
,
this
.
isReconnecting
.
value
,
'
会话ID:
'
,
this
.
currentDialogSessionId
,
'
重连次数:
'
,
this
.
reconnectAttempts
);
// 检查当前连接状态
const
isConnected
=
this
.
eventSource
&&
this
.
eventSource
.
readyState
===
1
;
if
(
isConnected
)
{
console
.
log
(
'
✅ 网络恢复但SSE连接正常,无需重连
'
);
// 即使连接正常,也触发网络恢复回调,但只在真正需要时显示消息
if
(
this
.
handlers
.
onNetworkOnline
)
{
this
.
handlers
.
onNetworkOnline
();
}
return
;
}
// 如果不在重连中且连接异常,尝试重新连接
if
(
!
this
.
isReconnecting
.
value
)
{
// 无论之前是否有会话ID,都重新创建连接
console
.
log
(
'
🔄 网络恢复,创建新SSE会话
'
);
this
.
reconnectSSE
(
''
);
}
else
{
console
.
log
(
'
⚠️ 网络恢复但未触发重连 - 重连中:
'
,
this
.
isReconnecting
.
value
,
'
会话ID存在:
'
,
!!
this
.
currentDialogSessionId
);
console
.
log
(
'
💡 等待重连完成或心跳检测处理连接状态
'
);
}
// 触发网络恢复回调
if
(
this
.
handlers
.
onNetworkOnline
)
{
this
.
handlers
.
onNetworkOnline
();
}
}
// 处理网络断开
private
handleNetworkOffline
():
void
{
console
.
log
(
'
🌐 网络已断开,关闭SSE连接
'
);
// 保存当前的会话ID,保持会话连续性
console
.
log
(
'
💾 当前会话ID:
'
,
this
.
currentDialogSessionId
||
'
空
'
);
// 关闭SSE连接
if
(
this
.
eventSource
)
{
try
{
this
.
eventSource
.
close
();
this
.
eventSource
=
null
;
}
catch
(
err
)
{
console
.
warn
(
'
关闭SSE连接时出错:
'
,
err
);
}
}
// 停止心跳检测
this
.
stopHeartbeatCheck
();
// 重置重连状态和重连次数
this
.
isReconnecting
.
value
=
false
;
this
.
reconnectAttempts
=
0
;
// 关键修改:保持会话ID不变,但标记网络断开状态
// 这样网络恢复时可以继续使用同一个会话,但需要重新开始对话块
console
.
log
(
'
💡 网络断开,保持会话ID,准备网络恢复时重新开始对话块
'
);
// 网络断开时启动心跳检测,以便网络恢复时能立即发现
this
.
startHeartbeatCheck
();
// 触发网络断开回调
if
(
this
.
handlers
.
onNetworkOffline
)
{
this
.
handlers
.
onNetworkOffline
();
}
}
// 启动心跳检测
private
startHeartbeatCheck
():
void
{
// 停止之前的心跳检测
this
.
stopHeartbeatCheck
();
console
.
log
(
`💓 心跳检测已启动(
${
this
.
HEARTBEAT_INTERVAL
/
1000
}
秒间隔)`
);
// 使用固定间隔开始检测
this
.
connectionMonitorInterval
=
setInterval
(()
=>
{
this
.
performHeartbeatCheck
();
},
this
.
HEARTBEAT_INTERVAL
);
}
// 执行心跳检测
private
performHeartbeatCheck
():
void
{
// 检查网络状态
const
isOnline
=
typeof
window
!==
'
undefined
'
&&
window
.
navigator
.
onLine
;
if
(
!
isOnline
)
{
console
.
log
(
'
💓 SSE心跳检测 - 网络已断开,等待网络恢复
'
);
return
;
}
if
(
this
.
eventSource
&&
this
.
eventSource
.
readyState
===
1
)
{
// 连接正常,记录心跳
console
.
log
(
'
💓 SSE心跳检测正常 - 连接状态:
'
,
this
.
eventSource
.
readyState
);
}
else
{
console
.
log
(
'
💓 SSE心跳检测 - 连接状态:
'
,
this
.
eventSource
?
this
.
eventSource
.
readyState
:
'
未连接
'
);
// 如果未连接或连接异常,但网络正常,尝试重连
if
(
!
this
.
isReconnecting
.
value
)
{
if
(
this
.
currentDialogSessionId
)
{
console
.
log
(
'
💔 检测到SSE连接异常,尝试重连
'
);
this
.
reconnectSSE
(
this
.
currentDialogSessionId
);
}
else
{
console
.
log
(
'
💡 检测到网络已恢复但无会话ID,等待用户交互或网络恢复处理
'
);
// 不再自动触发重连,避免重复消息
// 网络恢复事件会由网络状态监听器自动处理
}
}
}
}
// 停止心跳检测
private
stopHeartbeatCheck
():
void
{
if
(
this
.
connectionMonitorInterval
)
{
clearInterval
(
this
.
connectionMonitorInterval
);
this
.
connectionMonitorInterval
=
null
;
console
.
log
(
'
💓 基础心跳检测已停止
'
);
}
}
// 清理资源
public
destroy
():
void
{
this
.
closeSSE
();
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new 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