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
b4dd2f2d
Commit
b4dd2f2d
authored
Nov 28, 2025
by
水玉婷
Browse files
feat:把sse以及模版抽离
parent
08a063fa
Changes
6
Expand all
Hide whitespace changes
Inline
Side-by-side
src/views/components/AiChat.vue
View file @
b4dd2f2d
This diff is collapsed.
Click to expand it.
src/views/components/AudioPlayer.vue
0 → 100644
View file @
b4dd2f2d
<!-- 语音模版 -->
<
template
>
<div
class=
"audio-message"
:data-audio-id=
"audioId"
>
<div
class=
"audio-player"
:data-audio-src=
"src"
@
click=
"handleAudioPlay"
>
<div
class=
"audio-wave"
>
<div
class=
"wave-bar"
></div>
<div
class=
"wave-bar"
></div>
<div
class=
"wave-bar"
></div>
<div
class=
"wave-bar"
></div>
<div
class=
"wave-bar"
></div>
</div>
<div
class=
"audio-duration"
>
{{
durationTime
||
'
0
'
}}
"
</div>
</div>
<audio
:id=
"audioId"
:src=
"src"
preload=
"metadata"
style=
"display: none;"
@
ended=
"handleAudioEnded"
@
pause=
"handleAudioPause"
></audio>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
onMounted
,
watch
}
from
'
vue
'
interface
Props
{
src
:
string
durationTime
?:
string
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
durationTime
:
'
0
'
})
const
audioId
=
ref
(
`audio-
${
Date
.
now
()}
-
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
)
const
isPlaying
=
ref
(
false
)
const
audioElement
=
ref
<
HTMLAudioElement
|
null
>
(
null
)
// 暂停其他正在播放的音频
const
pauseAllOtherAudios
=
(
currentAudio
:
HTMLAudioElement
)
=>
{
document
.
querySelectorAll
(
'
audio
'
).
forEach
(
audio
=>
{
if
(
audio
!==
currentAudio
&&
!
audio
.
paused
)
{
audio
.
pause
()
// 移除播放状态
const
audioMessage
=
audio
.
closest
(
'
.audio-message
'
)
const
audioPlayer
=
audioMessage
?.
querySelector
(
'
.audio-player
'
)
audioPlayer
?.
classList
.
remove
(
'
playing
'
)
}
})
}
// 处理音频播放
const
handleAudioPlay
=
()
=>
{
if
(
!
audioElement
.
value
)
return
if
(
audioElement
.
value
.
paused
)
{
// 暂停其他正在播放的音频
pauseAllOtherAudios
(
audioElement
.
value
)
audioElement
.
value
.
play
().
catch
(
error
=>
{
console
.
error
(
'
播放音频失败:
'
,
error
)
})
isPlaying
.
value
=
true
}
else
{
audioElement
.
value
.
pause
()
isPlaying
.
value
=
false
}
}
// 处理音频播放结束
const
handleAudioEnded
=
()
=>
{
isPlaying
.
value
=
false
}
// 处理音频暂停
const
handleAudioPause
=
()
=>
{
isPlaying
.
value
=
false
}
onMounted
(()
=>
{
audioElement
.
value
=
document
.
getElementById
(
audioId
.
value
)
as
HTMLAudioElement
// 监听音频播放事件
if
(
audioElement
.
value
)
{
audioElement
.
value
.
addEventListener
(
'
play
'
,
()
=>
{
isPlaying
.
value
=
true
})
}
})
// 监听播放状态变化,更新 UI
watch
(
isPlaying
,
(
newVal
)
=>
{
const
playerElement
=
document
.
querySelector
(
`[data-audio-id="
${
audioId
.
value
}
"] .audio-player`
)
if
(
playerElement
)
{
if
(
newVal
)
{
playerElement
.
classList
.
add
(
'
playing
'
)
}
else
{
playerElement
.
classList
.
remove
(
'
playing
'
)
}
}
})
</
script
>
<
style
lang=
"less"
scoped
>
.audio-message {
display: inline-block;
width: -webkit-fill-available;
audio {
display: none; // 隐藏原生音频控件
}
.audio-player {
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
box-sizing: border-box;
padding: 4px 12px;
border-radius: 20px;
color:#fff;
&.playing {
.audio-wave .wave-bar {
animation: waveAnimation 1.2s ease-in-out infinite;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
&:nth-child(4) {
animation-delay: 0.6s;
}
&:nth-child(5) {
animation-delay: 0.8s;
}
}
}
.audio-wave {
display: flex;
align-items: center;
gap: 2px;
margin-right: 8px;
.wave-bar {
width: 2px;
height: 12px;
background-color: #fff;
border-radius: 1px;
transition: all 0.3s ease;
&:nth-child(1) {
height: 4px;
}
&:nth-child(2) {
height: 8px;
}
&:nth-child(3) {
height: 12px;
}
&:nth-child(4) {
height: 8px;
}
&:nth-child(5) {
height: 4px;
}
}
}
.audio-duration {
font-size: 12px;
color: #fff;
min-width: 30px;
text-align: center;
white-space: nowrap;
}
}
}
@keyframes waveAnimation {
0%, 100% {
transform: scaleY(0.3);
opacity: 0.5;
}
50% {
transform: scaleY(1);
opacity: 1;
}
}
</
style
>
\ No newline at end of file
src/views/components/style.less
View file @
b4dd2f2d
...
@@ -892,108 +892,7 @@ li {
...
@@ -892,108 +892,7 @@ li {
}
}
}
}
// 音频消息样式 - 简化版本,移除audio-icon
:deep(.audio-message) {
display: inline-block;
width: -webkit-fill-available;
audio {
display: none; // 隐藏原生音频控件
}
.audio-player {
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
box-sizing: border-box;
padding: 4px 12px;
border-radius: 20px;
&.playing {
.audio-wave .wave-bar {
animation: waveAnimation 1.2s ease-in-out infinite;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
&:nth-child(4) {
animation-delay: 0.6s;
}
&:nth-child(5) {
animation-delay: 0.8s;
}
}
}
.audio-wave {
display: flex;
align-items: center;
gap: 2px;
margin-right: 8px;
.wave-bar {
width: 2px;
height: 12px;
background: #ffffff; // 白色波形条
border-radius: 1px;
transition: all 0.3s ease;
&:nth-child(1) {
height: 4px;
}
&:nth-child(2) {
height: 8px;
}
&:nth-child(3) {
height: 12px;
}
&:nth-child(4) {
height: 8px;
}
&:nth-child(5) {
height: 4px;
}
}
}
.audio-duration {
font-size: 12px;
color: #ffffff; // 白色时长文字
min-width: 30px;
text-align: center;
}
}
}
@keyframes waveAnimation {
0%,
100% {
transform: scaleY(0.3);
opacity: 0.5;
}
50% {
transform: scaleY(1);
opacity: 1;
}
}
// =============================================
// =============================================
// Markdown消息样式
// Markdown消息样式
...
...
src/views/components/utils/audioTemplate.ts
deleted
100644 → 0
View file @
08a063fa
/**
* 音频模板工具类
* 用于生成音频消息的HTML模板和音频播放器管理
*/
/**
* 音频消息模板 - 简化版本,移除audio-icon
* @param audioData 音频数据对象
* @returns 音频消息的HTML字符串
*/
export
const
audioTemplate
=
(
audioData
:
any
):
string
=>
{
const
{
audioUrl
,
audioBlob
,
durationTime
}
=
audioData
;
let
src
=
audioUrl
;
// 如果提供了Blob对象,创建对象URL
if
(
audioBlob
&&
!
audioUrl
)
{
src
=
URL
.
createObjectURL
(
audioBlob
);
}
// 生成唯一ID用于音频播放器
const
audioId
=
`audio_
${
Date
.
now
()}
_
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
;
return
`<div class="audio-message" data-audio-id="
${
audioId
}
">
<div class="audio-player" data-audio-src="
${
src
}
">
<div class="audio-wave">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
<div class="audio-duration">
${
durationTime
+
'
"
'
||
'
0"
'
}
</div>
</div>
<audio id="
${
audioId
}
" src="
${
src
}
" preload="metadata" style="display: none;"></audio>
</div>`
;
};
/**
* 初始化音频播放器
* 设置音频播放器的事件监听
*/
export
const
initAudioPlayers
=
():
void
=>
{
const
audioPlayers
=
document
.
querySelectorAll
(
'
.audio-player
'
);
audioPlayers
.
forEach
((
player
)
=>
{
// 检查是否已经绑定过事件监听器
if
(
player
.
hasAttribute
(
'
data-audio-bound
'
))
{
return
;
// 如果已经绑定过,跳过
}
// 标记为已绑定
player
.
setAttribute
(
'
data-audio-bound
'
,
'
true
'
);
const
audioMessage
=
player
.
closest
(
'
.audio-message
'
);
const
audioId
=
audioMessage
?.
getAttribute
(
'
data-audio-id
'
);
const
audioElement
=
audioId
?
document
.
getElementById
(
audioId
)
:
null
;
if
(
!
audioElement
)
{
console
.
warn
(
'
未找到音频元素,audioId:
'
,
audioId
);
return
;
}
// 音频播放结束,重置状态
audioElement
.
addEventListener
(
'
ended
'
,
()
=>
{
player
.
classList
.
remove
(
'
playing
'
);
});
// 设置播放/暂停事件
player
.
addEventListener
(
'
click
'
,
(
e
)
=>
{
e
.
stopPropagation
();
if
(
audioElement
.
paused
)
{
// 暂停其他正在播放的音频
pauseAllOtherAudios
(
audioElement
);
audioElement
.
play
().
catch
(
error
=>
{
console
.
error
(
'
播放音频失败:
'
,
error
);
});
player
.
classList
.
add
(
'
playing
'
);
}
else
{
audioElement
.
pause
();
player
.
classList
.
remove
(
'
playing
'
);
}
});
// 音频播放事件
audioElement
.
addEventListener
(
'
play
'
,
()
=>
{
player
.
classList
.
add
(
'
playing
'
);
});
// 音频暂停事件
audioElement
.
addEventListener
(
'
pause
'
,
()
=>
{
player
.
classList
.
remove
(
'
playing
'
);
});
});
};
/**
* 暂停所有其他正在播放的音频
* @param currentAudio 当前正在播放的音频元素
*/
export
const
pauseAllOtherAudios
=
(
currentAudio
:
HTMLAudioElement
):
void
=>
{
const
allAudios
=
document
.
querySelectorAll
(
'
audio
'
);
allAudios
.
forEach
((
audio
)
=>
{
if
(
audio
!==
currentAudio
&&
!
audio
.
paused
)
{
audio
.
pause
();
const
player
=
audio
.
closest
(
'
.audio-message
'
)?.
querySelector
(
'
.audio-player
'
);
if
(
player
)
{
player
.
classList
.
remove
(
'
playing
'
);
}
}
});
};
/**
* 简化的音频模板函数(兼容旧版本)
* @param audioData 音频数据对象
* @returns 音频消息的HTML字符串
*/
export
const
audio
=
(
audioData
:
any
):
string
=>
{
return
audioTemplate
(
audioData
);
};
// 默认导出对象
export
default
{
audioTemplate
,
audio
,
initAudioPlayers
,
pauseAllOtherAudios
};
\ No newline at end of file
src/views/components/utils/contentTemplateService.ts
0 → 100644
View file @
b4dd2f2d
import
{
tableTemplate
}
from
'
./tableTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./markdownTemplate
'
;
// 内容模板类型定义
export
interface
ContentTemplates
{
text
:
(
content
:
string
)
=>
string
;
thinking
:
(
content
:
string
)
=>
string
;
error
:
(
content
:
string
)
=>
string
;
table
:
(
tableData
:
any
)
=>
string
;
markdown
:
(
content
:
any
)
=>
string
;
option
:
(
optionData
:
any
)
=>
string
;
iframe
:
(
iframeData
:
any
)
=>
string
;
}
// 消息块类型定义
export
interface
MessageBlock
{
content
:
string
;
thinkContent
?:
string
;
hasThinkBox
:
boolean
;
thinkBoxExpanded
:
boolean
;
chartData
?:
any
;
chartType
?:
number
|
string
;
audioData
?:
{
src
:
string
;
durationTime
:
string
;
};
}
// 消息类型定义
export
interface
Message
{
messageType
:
'
received
'
|
'
sent
'
;
avatar
:
string
;
recordId
:
string
;
promptTokens
:
number
;
completionTokens
:
number
;
totalTokens
:
number
;
date
:
string
;
customClass
?:
string
;
contentBlocks
:
MessageBlock
[];
}
// SSE数据类型定义
export
interface
SSEData
{
message
:
any
;
status
:
number
|
string
;
type
:
number
|
string
;
}
// 模板处理结果
export
interface
TemplateProcessResult
{
updatedResponse
:
Message
|
null
;
updatedIsThinking
:
boolean
;
updatedBlockIndex
:
number
;
recordId
:
string
;
promptTokens
:
number
;
completionTokens
:
number
;
totalTokens
:
number
;
newDialogSessionId
:
string
;
}
// 内容模板服务类
export
class
ContentTemplateService
{
private
templates
:
ContentTemplates
;
constructor
()
{
this
.
templates
=
this
.
createTemplates
();
}
// 创建内容模板生成器
private
createTemplates
():
ContentTemplates
{
return
{
// 普通文本
text
:
(
content
:
string
)
=>
{
return
`<div class="message-text">
${
content
}
</div>`
;
},
// 思考过程
thinking
:
(
content
:
string
)
=>
{
const
formattedContent
=
content
.
split
(
'
\n
'
)
.
map
((
line
)
=>
`<div class="think-line">
${
line
}
</div>`
)
.
join
(
''
);
return
`<div class="think-content">
${
formattedContent
}
</div>`
;
},
// 错误信息
error
:
(
content
:
string
)
=>
{
return
`<div class="message-error">
${
content
}
</div>`
;
},
// 表格模板 - 使用独立的表格模板工具
table
:
(
tableData
:
any
)
=>
{
return
tableTemplate
(
tableData
);
},
// Markdown模板 - 使用独立的markdown模板工具
markdown
:
(
content
:
any
)
=>
{
return
markdownTemplate
(
content
);
},
// 选项数据模板 - 纯渲染,不允许点击
option
:
(
optionData
:
any
)
=>
{
const
{
tips
,
options
}
=
optionData
;
// 生成选项列表HTML - 序号和标题直接放在一行
const
optionsHtml
=
options
?.
map
((
item
:
any
,
index
:
number
)
=>
{
const
{
title
,
url
}
=
item
;
return
`
<div class="option-item">
<div class="option-content">
<span class="option-number-title">
${
index
+
1
}
.
${
title
}
</span>
<span class="option-url">(
${
url
}
)</span>
</div>
</div>
`
;
}).
join
(
''
)
||
''
;
return
`
<div class="message-options">
${
tips
?
`<div class="options-tips">
${
tips
}
</div>`
:
''
}
<div class="options-list">
${
optionsHtml
}
</div>
</div>
`
;
},
// 简化的iframe模板 - 移除全屏功能,设置宽高100%固定
iframe
:
(
iframeData
:
any
)
=>
{
const
{
tips
,
title
,
url
}
=
iframeData
||
{};
console
.
log
(
'
iframeData
'
,
iframeData
);
return
`<div class="message-iframe iframe-loading">
<!-- 加载状态 -->
<div class="iframe-loading">
<div class="loading-spinner"></div>
<div class="loading-text">正在加载内容...</div>
<div class="loading-progress">
<div class="progress-bar"></div>
</div>
</div>
<!-- iframe容器 -->
<div class="iframe-tips">
${
tips
||
''
}
</div>
<div class="iframe-title">
${
title
||
''
}
</div>
<iframe
src="
${
url
}
"
width="100%"
height="100%"
frameborder="0"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
scrolling="no"
style="overflow: hidden;"
onload="this.parentElement.classList.add('iframe-loaded'); this.parentElement.classList.remove('iframe-loading');"
onerror="console.error('iframe加载失败:', this.src)"
></iframe>
</div>`
;
}
};
}
// 获取模板对象
public
getTemplates
():
ContentTemplates
{
return
this
.
templates
;
}
// 公共模板生成方法
public
generateTextTemplate
(
content
:
string
):
string
{
return
this
.
templates
.
text
(
content
);
}
public
generateThinkingTemplate
(
content
:
string
):
string
{
return
this
.
templates
.
thinking
(
content
);
}
public
generateErrorTemplate
(
content
:
string
):
string
{
return
this
.
templates
.
error
(
content
);
}
// 处理SSE消息的核心方法
public
processSSEMessage
(
data
:
SSEData
,
currentResponse
:
Message
|
null
,
isThinking
:
boolean
,
currentBlockIndex
:
number
,
isHistoryData
=
false
,
templateTools
?:
{
tableTemplate
:
(
tableData
:
any
)
=>
string
;
markdownTemplate
:
(
content
:
any
)
=>
string
;
isLastBlockMarkdown
:
(
blocks
:
MessageBlock
[])
=>
boolean
;
getLastMarkdownBlockIndex
:
(
blocks
:
MessageBlock
[])
=>
number
;
mergeMarkdownContent
:
(
existing
:
string
,
newContent
:
string
)
=>
string
;
}
):
TemplateProcessResult
{
let
messageContent
=
data
.
message
||
''
;
const
contentStatus
=
data
.
status
;
const
contentType
=
data
.
type
;
let
updatedResponse
=
currentResponse
;
let
updatedIsThinking
=
isThinking
;
let
updatedBlockIndex
=
currentBlockIndex
;
let
recordId
=
''
;
let
promptTokens
=
0
;
let
completionTokens
=
0
;
let
totalTokens
=
0
;
let
newDialogSessionId
=
''
;
// 根据是否为历史数据设置默认展开状态
const
defaultThinkBoxExpanded
=
!
isHistoryData
;
switch
(
contentStatus
)
{
case
-
1
:
// 错误信息
if
(
updatedResponse
)
{
updatedResponse
.
contentBlocks
.
push
({
content
:
this
.
templates
.
error
(
messageContent
||
'
出错了~~
'
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
break
;
case
3
:
// 图表数据
if
(
updatedResponse
)
{
switch
(
contentType
)
{
case
2
:
// 表格数据
const
{
rows
}
=
messageContent
;
// 表格数据处理
updatedResponse
.
contentBlocks
.
push
({
content
:
templateTools
?.
tableTemplate
?
templateTools
.
tableTemplate
(
rows
)
:
this
.
templates
.
table
(
rows
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
// 图表数据处理
updatedResponse
.
contentBlocks
.
push
({
content
:
''
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
chartData
:
messageContent
,
chartType
:
3
,
});
break
;
case
3
:
// 选项数据
const
{
tips
,
options
}
=
messageContent
;
if
(
options
?.
length
)
{
if
(
options
?.
length
===
1
)
{
// 走iframe
updatedResponse
.
contentBlocks
.
push
({
content
:
this
.
templates
.
iframe
({
...
options
[
0
],
tips
:
tips
||
''
,
}),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
else
{
updatedResponse
.
contentBlocks
.
push
({
content
:
this
.
templates
.
option
(
messageContent
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
}
break
;
case
4
:
// MD格式
if
(
updatedResponse
)
{
const
markdownContent
=
templateTools
?.
markdownTemplate
?
templateTools
.
markdownTemplate
(
messageContent
||
''
)
:
this
.
templates
.
markdown
(
messageContent
||
''
);
// 检查最后一个块是否是markdown块
const
isLastMarkdown
=
templateTools
?.
isLastBlockMarkdown
?
templateTools
.
isLastBlockMarkdown
(
updatedResponse
.
contentBlocks
)
:
isLastBlockMarkdown
(
updatedResponse
.
contentBlocks
);
if
(
isLastMarkdown
)
{
// 合并到现有的markdown块
const
lastMarkdownIndex
=
templateTools
?.
getLastMarkdownBlockIndex
?
templateTools
.
getLastMarkdownBlockIndex
(
updatedResponse
.
contentBlocks
)
:
getLastMarkdownBlockIndex
(
updatedResponse
.
contentBlocks
);
if
(
lastMarkdownIndex
!==
-
1
)
{
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
=
templateTools
?.
mergeMarkdownContent
?
templateTools
.
mergeMarkdownContent
(
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
,
markdownContent
)
:
mergeMarkdownContent
(
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
,
markdownContent
);
}
}
else
{
// 创建新的markdown块
updatedResponse
.
contentBlocks
.
push
({
content
:
markdownContent
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
}
break
;
default
:
// 默认处理
updatedResponse
.
contentBlocks
.
push
({
content
:
this
.
templates
.
text
(
messageContent
||
''
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
break
;
}
}
break
;
case
10
:
// 思考开始
updatedIsThinking
=
true
;
if
(
updatedBlockIndex
===
-
1
&&
updatedResponse
)
{
updatedBlockIndex
=
updatedResponse
.
contentBlocks
.
length
;
updatedResponse
.
contentBlocks
.
push
({
content
:
''
,
thinkContent
:
`
${
messageContent
}
`
,
hasThinkBox
:
true
,
thinkBoxExpanded
:
defaultThinkBoxExpanded
,
});
}
else
if
(
updatedResponse
&&
updatedResponse
.
contentBlocks
[
updatedBlockIndex
])
{
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
thinkContent
+=
``
;
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
hasThinkBox
=
true
;
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
thinkBoxExpanded
=
defaultThinkBoxExpanded
;
}
break
;
case
11
:
// 思考结束
if
(
updatedResponse
&&
updatedBlockIndex
!==
-
1
&&
updatedResponse
.
contentBlocks
[
updatedBlockIndex
])
{
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
thinkContent
+=
``
;
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
hasThinkBox
=
true
;
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
thinkBoxExpanded
=
defaultThinkBoxExpanded
;
}
updatedIsThinking
=
false
;
break
;
case
20
:
// 初始连接回传信息
if
(
updatedResponse
)
{
updatedResponse
.
contentBlocks
.
push
({
content
:
this
.
templates
.
text
(
messageContent
?.
words
||
''
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
newDialogSessionId
=
messageContent
?.
dialogSessionId
||
''
;
break
;
case
21
:
// 重连成功正回传信息
newDialogSessionId
=
messageContent
?.
dialogSessionId
||
''
;
break
;
case
29
:
// 当前会话结束
recordId
=
messageContent
?.
recordId
||
''
;
promptTokens
=
messageContent
?.
promptTokens
||
0
;
completionTokens
=
messageContent
?.
completionTokens
||
0
;
totalTokens
=
messageContent
?.
totalTokens
||
0
;
// 只有实时对话才在29时折叠思考框,历史数据不受影响
if
(
!
isHistoryData
&&
updatedResponse
&&
updatedResponse
.
contentBlocks
.
length
>
0
)
{
updatedResponse
.
contentBlocks
.
forEach
((
block
)
=>
{
if
(
block
.
hasThinkBox
)
{
block
.
thinkBoxExpanded
=
false
;
}
});
}
updatedIsThinking
=
false
;
updatedBlockIndex
=
-
1
;
break
;
default
:
// 普通内容
if
(
updatedIsThinking
&&
updatedResponse
)
{
if
(
updatedBlockIndex
!==
-
1
&&
updatedResponse
.
contentBlocks
[
updatedBlockIndex
])
{
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
thinkContent
+=
`\n
${
messageContent
}
`
;
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
hasThinkBox
=
true
;
updatedResponse
.
contentBlocks
[
updatedBlockIndex
].
thinkBoxExpanded
=
defaultThinkBoxExpanded
;
}
}
else
if
(
updatedResponse
)
{
updatedBlockIndex
=
updatedResponse
.
contentBlocks
.
length
;
updatedResponse
.
contentBlocks
.
push
({
content
:
this
.
templates
.
text
(
messageContent
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
break
;
}
return
{
updatedResponse
,
updatedIsThinking
,
updatedBlockIndex
,
recordId
,
promptTokens
,
completionTokens
,
totalTokens
,
newDialogSessionId
,
};
}
// 处理历史记录数据
public
processHistoryData
(
dataArray
:
any
[],
templateTools
?:
{
tableTemplate
:
(
tableData
:
any
)
=>
string
;
markdownTemplate
:
(
content
:
any
)
=>
string
;
isLastBlockMarkdown
:
(
blocks
:
MessageBlock
[])
=>
boolean
;
getLastMarkdownBlockIndex
:
(
blocks
:
MessageBlock
[])
=>
number
;
mergeMarkdownContent
:
(
existing
:
string
,
newContent
:
string
)
=>
string
;
}
):
Message
[]
{
const
result
:
Message
[]
=
[];
dataArray
.
forEach
((
data
)
=>
{
let
date
=
new
Date
(
data
.
startTime
).
toLocaleTimeString
();
// 处理问题消息
if
(
data
.
question
||
data
.
audioPath
)
{
// 创建基础消息结构
const
message
=
{
messageType
:
'
sent
'
as
const
,
avatar
:
'
我
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
,
contentBlocks
:
[]
as
MessageBlock
[]
};
// 使用switch语句处理不同类型的消息
switch
(
true
)
{
case
!!
data
.
audioPath
:
// 音频消息
message
.
contentBlocks
.
push
({
audioData
:
{
src
:
data
.
audioPath
,
durationTime
:
data
.
audioTime
||
'
0"
'
},
content
:
''
,
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
});
break
;
case
!!
data
.
question
:
// 文本消息
message
.
contentBlocks
.
push
({
content
:
this
.
templates
.
text
(
data
.
question
),
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
});
break
;
default
:
// 其他类型的消息(未来扩展)
break
;
}
result
.
push
(
message
);
}
// 处理AI回答消息
if
(
data
.
answerInfoList
&&
Array
.
isArray
(
data
.
answerInfoList
))
{
const
aiMessage
:
Message
=
{
messageType
:
'
received
'
,
avatar
:
'
AI
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
contentBlocks
:
[],
date
,
};
let
currentThinkingMode
=
false
;
let
currentBlockIdx
=
-
1
;
// 历史数据处理,isHistoryData设为true,思考框折叠
data
.
answerInfoList
.
forEach
((
answer
)
=>
{
const
sseData
:
SSEData
=
{
message
:
answer
.
message
||
''
,
status
:
answer
.
status
||
0
,
type
:
answer
.
type
||
''
,
};
const
processResult
=
this
.
processSSEMessage
(
sseData
,
aiMessage
,
currentThinkingMode
,
currentBlockIdx
,
true
,
templateTools
);
currentThinkingMode
=
processResult
.
updatedIsThinking
;
currentBlockIdx
=
processResult
.
updatedBlockIndex
;
aiMessage
.
recordId
=
processResult
.
recordId
;
aiMessage
.
promptTokens
=
processResult
.
promptTokens
;
aiMessage
.
completionTokens
=
processResult
.
completionTokens
;
aiMessage
.
totalTokens
=
processResult
.
totalTokens
;
});
if
(
aiMessage
.
contentBlocks
.
length
>
0
)
{
result
.
push
(
aiMessage
);
}
}
});
return
result
;
}
}
// 创建内容模板服务实例的工厂函数
export
function
createContentTemplateService
():
ContentTemplateService
{
return
new
ContentTemplateService
();
}
export
default
ContentTemplateService
;
\ No newline at end of file
src/views/components/utils/sseService.ts
0 → 100644
View file @
b4dd2f2d
import
{
EventSourcePolyfill
}
from
'
event-source-polyfill
'
;
import
{
ref
,
Ref
}
from
'
vue
'
;
// SSE数据类型定义
export
interface
SSEData
{
message
:
any
;
status
:
number
|
string
;
type
:
number
|
string
;
}
// SSE服务配置
export
interface
SSEServiceConfig
{
apiBaseUrl
:
string
;
appCode
:
string
;
token
:
string
;
params
:
{
stage
?:
string
;
appId
?:
string
;
};
}
// SSE事件处理器
export
interface
SSEHandlers
{
onMessage
?:
(
data
:
SSEData
)
=>
void
;
onError
?:
(
error
:
any
)
=>
void
;
onOpen
?:
(
event
:
any
)
=>
void
;
onReconnect
?:
(
newDialogSessionId
:
string
)
=>
void
;
}
// SSE服务类 - 专注于SSE连接管理
export
class
SSEService
{
private
eventSource
:
EventSourcePolyfill
|
null
=
null
;
private
config
:
SSEServiceConfig
;
private
handlers
:
SSEHandlers
;
private
isReconnecting
:
Ref
<
boolean
>
=
ref
(
false
);
private
timeArr
:
NodeJS
.
Timeout
[]
=
[];
constructor
(
config
:
SSEServiceConfig
,
handlers
:
SSEHandlers
=
{})
{
this
.
config
=
config
;
this
.
handlers
=
handlers
;
}
// 初始化SSE连接
public
initSSE
(
dialogSessionId
:
string
):
void
{
try
{
const
url
=
`
${
this
.
config
.
apiBaseUrl
}
/aiService/sse/join/
${
this
.
config
.
params
?.
stage
||
''
}
?app-id=
${
this
.
config
.
params
?.
appId
||
''
}
&dialog-session-id=
${
dialogSessionId
||
''
}
`
;
console
.
log
(
'
初始化SSE连接,dialogSessionId:
'
,
dialogSessionId
);
this
.
eventSource
=
new
EventSourcePolyfill
(
url
,
{
headers
:
{
Token
:
this
.
config
.
token
||
''
,
'
x-session-id
'
:
this
.
config
.
token
||
''
,
'
x-app-code
'
:
this
.
config
.
appCode
||
''
,
},
withCredentials
:
true
,
connectionTimeout
:
30000
,
});
this
.
eventSource
.
onopen
=
(
event
)
=>
{
console
.
log
(
'
SSE连接已建立
'
,
event
);
if
(
this
.
handlers
.
onOpen
)
{
this
.
handlers
.
onOpen
(
event
);
}
};
this
.
eventSource
.
addEventListener
(
'
message
'
,
(
event
)
=>
{
try
{
console
.
log
(
'
Received message:
'
,
event
);
const
data
:
SSEData
=
JSON
.
parse
(
event
.
data
);
// 只传递原始数据,模板处理在外部进行
if
(
this
.
handlers
.
onMessage
)
{
this
.
handlers
.
onMessage
(
data
);
}
}
catch
(
error
)
{
console
.
error
(
'
处理SSE消息时出错:
'
,
error
);
}
});
this
.
eventSource
.
onerror
=
(
error
)
=>
{
console
.
error
(
'
SSE error:
'
,
error
);
if
(
this
.
handlers
.
onError
)
{
this
.
handlers
.
onError
(
error
);
}
this
.
closeSSE
();
// 添加错误重连逻辑
if
(
!
this
.
isReconnecting
.
value
)
{
setTimeout
(()
=>
{
if
(
dialogSessionId
)
{
this
.
reconnectSSE
(
dialogSessionId
);
}
},
3000
);
}
};
}
catch
(
err
)
{
console
.
error
(
'
初始化SSE连接失败:
'
,
err
);
}
}
// 重新连接SSE
public
reconnectSSE
(
newDialogSessionId
:
string
):
void
{
if
(
this
.
isReconnecting
.
value
)
{
console
.
log
(
'
正在重连中,跳过重复重连
'
);
return
;
}
this
.
isReconnecting
.
value
=
true
;
console
.
log
(
'
开始重连SSE,新的dialogSessionId:
'
,
newDialogSessionId
);
this
.
closeSSE
();
const
reconnectTimeout
=
setTimeout
(()
=>
{
this
.
initSSE
(
newDialogSessionId
);
setTimeout
(()
=>
{
this
.
isReconnecting
.
value
=
false
;
},
2000
);
},
500
);
this
.
timeArr
.
push
(
reconnectTimeout
);
if
(
this
.
handlers
.
onReconnect
)
{
this
.
handlers
.
onReconnect
(
newDialogSessionId
);
}
}
// 关闭SSE连接
public
closeSSE
():
void
{
if
(
this
.
eventSource
)
{
try
{
this
.
eventSource
.
close
();
this
.
eventSource
=
null
;
}
catch
(
err
)
{
console
.
warn
(
'
关闭SSE连接时出错:
'
,
err
);
}
}
// 清理所有定时器
this
.
timeArr
.
forEach
(
timeout
=>
{
clearTimeout
(
timeout
);
});
this
.
timeArr
=
[];
}
// 获取重连状态
public
getIsReconnecting
():
boolean
{
return
this
.
isReconnecting
.
value
;
}
// 获取当前SSE连接状态
public
getConnectionState
():
number
{
if
(
!
this
.
eventSource
)
{
return
0
;
// 未连接
}
return
this
.
eventSource
.
readyState
;
}
// 清理资源
public
destroy
():
void
{
this
.
closeSSE
();
this
.
timeArr
.
forEach
((
item
)
=>
{
clearTimeout
(
item
);
});
this
.
timeArr
=
[];
}
}
// 创建SSE服务实例的工厂函数
export
function
createSSEService
(
config
:
SSEServiceConfig
,
handlers
:
SSEHandlers
=
{}):
SSEService
{
return
new
SSEService
(
config
,
handlers
);
}
export
default
SSEService
;
\ No newline at end of file
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