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
Hide whitespace changes
Inline
Side-by-side
src/views/components/AiChat.vue
View file @
b4dd2f2d
...
@@ -42,6 +42,13 @@
...
@@ -42,6 +42,13 @@
<ChartComponent
:chart-data=
"item.chartData"
:chart-type=
"item.chartType || 3"
<ChartComponent
:chart-data=
"item.chartData"
:chart-type=
"item.chartType || 3"
:title=
"item.chartData.title || '图表数据'"
/>
:title=
"item.chartData.title || '图表数据'"
/>
</div>
</div>
<!-- 音频内容块 -->
<template
v-else-if=
"item.audioData"
>
<AudioPlayer
:src=
"item.audioData.src"
:duration-time=
"item.audioData.durationTime"
/>
</
template
>
<!-- 普通内容块 -->
<!-- 普通内容块 -->
<div
v-else
v-html=
"item.content"
class=
"message-inner-box"
></div>
<div
v-else
v-html=
"item.content"
class=
"message-inner-box"
></div>
<!-- 思考过程框 -->
<!-- 思考过程框 -->
...
@@ -50,7 +57,7 @@
...
@@ -50,7 +57,7 @@
item.thinkBoxExpanded ? '▲ 收起思考过程' : '▼ 展开思考过程'
item.thinkBoxExpanded ? '▲ 收起思考过程' : '▼ 展开思考过程'
}}
</div>
}}
</div>
<div
v-if=
"item.thinkBoxExpanded"
class=
"think-box-content"
<div
v-if=
"item.thinkBoxExpanded"
class=
"think-box-content"
v-html=
"
contentTemplates.thinking
(item.thinkContent || '')"
></div>
v-html=
"
templateService.generateThinkingTemplate
(item.thinkContent || '')"
></div>
</div>
</div>
</template>
</template>
</div>
</div>
...
@@ -84,187 +91,58 @@
...
@@ -84,187 +91,58 @@
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
EventSourcePolyfill
}
from
'
event-source-polyfill
'
;
import
{
ref
,
onMounted
,
onBeforeUnmount
,
nextTick
,
watch
}
from
'
vue
'
;
import
{
ref
,
onMounted
,
onBeforeUnmount
,
nextTick
,
watch
}
from
'
vue
'
;
import
dayjs
from
'
dayjs
'
;
import
dayjs
from
'
dayjs
'
;
import
{
post
,
get
}
from
'
@/utils/axios.js
'
;
// 导入axios的post方法
import
{
post
,
get
}
from
'
@/utils/axios.js
'
;
// 导入axios的post方法
import
{
tableTemplate
}
from
'
./utils/tableTemplate
'
;
import
{
tableTemplate
}
from
'
./utils/tableTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./utils/markdownTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./utils/markdownTemplate
'
;
import
{
audioTemplate
,
initAudioPlayers
}
from
'
./utils/audioTemplate
'
;
import
{
SendOutlined
,
UserOutlined
}
from
'
@ant-design/icons-vue
'
;
import
{
SendOutlined
,
UserOutlined
}
from
'
@ant-design/icons-vue
'
;
import
defaultAvatar
from
'
@/assets/logo.png
'
;
import
defaultAvatar
from
'
@/assets/logo.png
'
;
import
ChartComponent
from
'
./ChartComponent.vue
'
;
// 导入独立的图表组件
import
ChartComponent
from
'
./ChartComponent.vue
'
;
// 导入独立的图表组件
import
VoiceRecognition
from
'
./VoiceRecognition.vue
'
;
// 导入语音识别组件
import
VoiceRecognition
from
'
./VoiceRecognition.vue
'
;
// 导入语音识别组件
import
AudioPlayer
from
'
./AudioPlayer.vue
'
;
// 导入音频播放器组件
import
{
createSSEService
,
type
SSEData
}
from
'
./utils/sseService
'
;
// 导入SSE服务
import
{
createContentTemplateService
,
type
Message
}
from
'
./utils/contentTemplateService
'
;
// 导入模板服务
// 定义组件属性
// 定义组件属性
接口
const
props
=
define
Props
(
{
interface
Props
{
// 对话会话ID
// 对话会话ID
dialogSessionId
:
{
dialogSessionId
?:
string
type
:
String
,
default
:
''
},
// API基础URL
// API基础URL
apiBaseUrl
:
{
apiBaseUrl
?:
string
type
:
String
,
default
:
import
.
meta
.
env
.
VITE_API_BASE_PATH
||
'
/pedapi
'
},
// 应用代码
// 应用代码
appCode
:
{
appCode
?:
string
type
:
String
,
token
?:
string
default
:
import
.
meta
.
env
.
VITE_APP_CODE
||
'
ped.qywx
'
logoUrl
?:
string
},
token
:
{
type
:
String
,
default
:
''
},
logoUrl
:
{
type
:
String
,
default
:
''
},
// 对话详情数据
// 对话详情数据
detailData
:
{
detailData
?:
Record
<
string
,
any
>
type
:
Object
,
onMessageSend
?:
(
message
:
string
|
Blob
)
=>
Promise
<
void
>
default
:
()
=>
({})
onGetChatRecord
?:
Function
},
customClass
?:
string
onMessageSend
:
{
params
?:
{
type
:
Function
,
appId
?:
string
default
:
undefined
stage
?:
string
},
[
key
:
string
]:
any
onGetChatRecord
:
{
type
:
Function
,
default
:
undefined
},
customClass
:
{
type
:
String
,
default
:
''
},
params
:
{
type
:
Object
,
default
:
()
=>
({
appId
:
'
app-test
'
,
stage
:
'
wechat-demo
'
,
})
}
}
});
// 内容模板生成器 - 简化版本,表格功能已抽离
const
contentTemplates
=
{
// 普通文本
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>`
;
},
// 音频消息模板
audio
:
(
audioData
:
any
)
=>
{
return
audioTemplate
(
audioData
);
},
};
// 定义消息类型 - 更新接口添加图表相关字段
interface
Message
{
messageType
:
'
received
'
|
'
sent
'
;
avatar
:
string
;
recordId
:
string
;
promptTokens
:
number
;
completionTokens
:
number
;
totalTokens
:
number
;
date
:
string
;
customClass
?:
string
;
contentBlocks
:
{
content
:
string
;
thinkContent
?:
string
;
hasThinkBox
:
boolean
;
thinkBoxExpanded
:
boolean
;
chartData
?:
any
;
// 添加图表数据字段
chartType
?:
number
|
string
;
// 添加图表类型字段
}[];
}
}
interface
SSEData
{
// 定义组件属性
message
:
any
;
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
status
:
number
|
string
;
dialogSessionId
:
''
,
type
:
number
|
string
;
apiBaseUrl
:
import
.
meta
.
env
.
VITE_API_BASE_PATH
||
'
/pedapi
'
,
}
appCode
:
import
.
meta
.
env
.
VITE_APP_CODE
||
'
ped.qywx
'
,
token
:
''
,
logoUrl
:
''
,
detailData
:
()
=>
({}),
onMessageSend
:
undefined
,
onGetChatRecord
:
undefined
,
customClass
:
''
,
params
:
()
=>
({
appId
:
'
app-test
'
,
stage
:
'
wechat-demo
'
,
})
});
interface
ChatParams
{
stage
?:
string
;
appId
?:
string
;
}
// 响应式数据
// 响应式数据
const
messageText
=
ref
(
''
);
const
messageText
=
ref
(
''
);
...
@@ -274,22 +152,193 @@ const textarea = ref<HTMLTextAreaElement>();
...
@@ -274,22 +152,193 @@ const textarea = ref<HTMLTextAreaElement>();
const
loading
=
ref
(
false
);
const
loading
=
ref
(
false
);
const
currentAIResponse
=
ref
<
Message
|
null
>
(
null
);
const
currentAIResponse
=
ref
<
Message
|
null
>
(
null
);
const
isAIResponding
=
ref
(
false
);
const
isAIResponding
=
ref
(
false
);
const
eventSource
=
ref
<
EventSourcePolyfill
|
null
>
(
null
);
const
dialogSessionId
=
ref
(
props
.
dialogSessionId
||
''
);
const
dialogSessionId
=
ref
(
props
.
dialogSessionId
||
''
);
const
isInThinkingMode
=
ref
(
false
);
const
isInThinkingMode
=
ref
(
false
);
const
currentBlockIndex
=
ref
(
-
1
);
const
currentBlockIndex
=
ref
(
-
1
);
const
isReconnecting
=
ref
(
false
);
const
timeArr
=
ref
([]);
const
hasStartedConversation
=
ref
(
false
);
// 添加对话开始状态
const
hasStartedConversation
=
ref
(
false
);
// 添加对话开始状态
// SSE消息处理函数
const
handleSSEMessage
=
(
data
:
SSEData
)
=>
{
try
{
console
.
log
(
'
Received SSE message:
'
,
data
);
// 创建AI响应消息的辅助函数
const
createAIResponse
=
()
=>
{
isAIResponding
.
value
=
true
;
currentAIResponse
.
value
=
{
messageType
:
'
received
'
,
avatar
:
'
AI
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[],
};
messages
.
value
.
push
(
currentAIResponse
.
value
);
};
// 对于type21消息,先检查是否有实际内容再决定是否创建新消息
if
(
data
.
status
===
21
)
{
const
messageContent
=
data
.
message
||
''
;
const
hasContent
=
messageContent
?.
words
&&
messageContent
.
words
.
trim
()
!==
''
;
// 只有有实际内容时才创建新消息
if
(
!
isAIResponding
.
value
&&
hasContent
)
{
createAIResponse
();
}
}
else
{
// 其他消息类型保持原有逻辑
if
(
!
isAIResponding
.
value
)
{
createAIResponse
();
}
}
// 使用模板服务处理消息
const
result
=
templateService
.
processSSEMessage
(
data
,
currentAIResponse
.
value
,
isInThinkingMode
.
value
,
currentBlockIndex
.
value
,
false
,
{
tableTemplate
,
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
);
currentAIResponse
.
value
=
result
.
updatedResponse
;
isInThinkingMode
.
value
=
result
.
updatedIsThinking
;
currentBlockIndex
.
value
=
result
.
updatedBlockIndex
;
// 如果type21没有内容,需要清理可能创建的空白消息
if
(
data
.
status
===
21
&&
currentAIResponse
.
value
&&
currentAIResponse
.
value
.
contentBlocks
.
length
===
0
)
{
// 移除空白消息
const
lastIndex
=
messages
.
value
.
length
-
1
;
if
(
lastIndex
>=
0
&&
messages
.
value
[
lastIndex
]
===
currentAIResponse
.
value
)
{
messages
.
value
.
splice
(
lastIndex
,
1
);
currentAIResponse
.
value
=
null
;
isAIResponding
.
value
=
false
;
}
}
if
(
result
.
recordId
&&
currentAIResponse
.
value
)
{
currentAIResponse
.
value
.
recordId
=
result
.
recordId
;
currentAIResponse
.
value
.
promptTokens
=
result
.
promptTokens
;
currentAIResponse
.
value
.
completionTokens
=
result
.
completionTokens
;
currentAIResponse
.
value
.
totalTokens
=
result
.
totalTokens
;
}
if
(
result
.
newDialogSessionId
)
{
console
.
log
(
'
收到新的 dialogSessionId:
'
,
result
.
newDialogSessionId
);
dialogSessionId
.
value
=
result
.
newDialogSessionId
;
}
nextTick
(()
=>
{
scrollToBottom
();
});
}
catch
(
error
)
{
console
.
error
(
'
处理SSE消息时出错:
'
,
error
);
}
};
// 创建SSE服务实例
const
sseService
=
createSSEService
({
apiBaseUrl
:
props
.
apiBaseUrl
,
appCode
:
props
.
appCode
,
token
:
props
.
token
,
params
:
props
.
params
},
{
onMessage
:
handleSSEMessage
,
onError
:
(
error
)
=>
{
console
.
error
(
'
SSE error:
'
,
error
);
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
closeSSE
();
},
onOpen
:
(
event
)
=>
{
console
.
log
(
'
SSE连接已建立
'
,
event
);
},
onReconnect
:
(
newDialogSessionId
)
=>
{
console
.
log
(
'
SSE重连成功,新的dialogSessionId:
'
,
newDialogSessionId
);
dialogSessionId
.
value
=
newDialogSessionId
;
}
});
// 创建模板服务实例
const
templateService
=
createContentTemplateService
();
// 语音事件处理函数 - 修改为接收服务器返回的URL
// 语音事件处理函数 - 修改为接收服务器返回的URL
const
handleVoiceAudio
=
(
audioUrl
:
string
,
audioBlob
?:
Blob
,
durationTime
?:
number
)
=>
{
const
handleVoiceAudio
=
(
audioUrl
:
string
,
audioBlob
?:
Blob
,
durationTime
?:
number
)
=>
{
console
.
log
(
'
收到音频URL:
'
,
audioUrl
);
// 直接使用统一的sendMessage函数发送音频消息
sendMessage
(
'
audio
'
,
{
audioUrl
,
durationTime
});
};
const
handleVoiceError
=
(
error
:
string
)
=>
{
console
.
error
(
'
语音识别错误:
'
,
error
);
// 可以添加错误提示
};
// 开始对话函数 - 修改为在发送消息时调用
const
startConversation
=
()
=>
{
if
(
!
hasStartedConversation
.
value
)
{
console
.
log
(
'
开始对话,初始化SSE连接
'
);
initSSE
();
// 只在第一次发送消息时初始化SSE
hasStartedConversation
.
value
=
true
;
}
};
// 定义消息类型
type
MessageType
=
'
text
'
|
'
audio
'
|
'
image
'
|
'
file
'
|
'
video
'
;
// 定义消息参数接口
interface
MessageParams
{
message
?:
string
;
audioUrl
?:
string
;
durationTime
?:
number
;
}
// 验证消息参数的辅助函数
const
validateMessageParams
=
(
type
:
MessageType
,
params
:
MessageParams
):
boolean
=>
{
const
{
message
,
audioUrl
}
=
params
;
const
messageContent
=
message
||
messageText
.
value
.
trim
();
switch
(
type
)
{
case
'
text
'
:
return
!!
messageContent
;
case
'
audio
'
:
return
!!
audioUrl
;
default
:
return
false
;
}
};
// 统一发送消息函数
const
sendMessage
=
async
(
type
:
MessageType
=
'
text
'
,
params
:
MessageParams
=
{})
=>
{
loading
.
value
=
true
;
const
{
message
,
audioUrl
,
durationTime
}
=
params
;
const
messageContent
=
message
||
messageText
.
value
.
trim
();
// 统一验证消息参数
if
(
!
validateMessageParams
(
type
,
params
))
{
loading
.
value
=
false
;
return
;
}
// 开始对话
// 开始对话
startConversation
();
startConversation
();
// 添加音频消息到聊天记录
isAIResponding
.
value
=
false
;
messages
.
value
.
push
({
isInThinkingMode
.
value
=
false
;
currentAIResponse
.
value
=
null
;
// 推送消息到聊天记录
const
messageData
=
{
messageType
:
'
sent
'
,
messageType
:
'
sent
'
,
avatar
:
'
我
'
,
avatar
:
'
我
'
,
recordId
:
''
,
recordId
:
''
,
...
@@ -297,528 +346,107 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob, durationTime?: num
...
@@ -297,528 +346,107 @@ const handleVoiceAudio = (audioUrl: string, audioBlob?: Blob, durationTime?: num
completionTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
totalTokens
:
0
,
date
:
dayjs
().
format
(
'
HH:mm
'
),
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[
contentBlocks
:
[]
as
any
[],
{
};
content
:
contentTemplates
.
audio
({
audioUrl
,
audioBlob
,
durationTime
}),
switch
(
type
)
{
case
'
text
'
:
messageData
.
contentBlocks
.
push
({
content
:
templateService
.
generateTextTemplate
(
messageContent
),
thinkContent
:
''
,
thinkContent
:
''
,
hasThinkBox
:
false
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
thinkBoxExpanded
:
false
,
});
// 重置文本输入框
if
(
textarea
.
value
)
{
textarea
.
value
.
style
.
height
=
'
50px
'
;
}
}
],
messageText
.
value
=
''
;
})
;
break
;
// 如果有音频Blob,直接发送到服务器
case
'
audio
'
:
if
(
audioUrl
)
{
messageData
.
contentBlocks
.
push
({
sendAudioMessage
(
audioUrl
,
durationTime
);
audioData
:
{
src
:
audioUrl
,
durationTime
:
durationTime
},
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
});
break
;
default
:
loading
.
value
=
false
;
return
;
}
}
// 滚动到底部
nextTick
(()
=>
{
scrollToBottom
();
});
};
const
handleVoiceError
=
(
error
:
string
)
=>
{
messages
.
value
.
push
(
messageData
);
console
.
error
(
'
语音识别错误:
'
,
error
);
// 可以添加错误提示
};
// 发送音频消息 - 简化逻辑,与sendMessage保持一致
await
nextTick
();
const
sendAudioMessage
=
async
(
audioUrl
:
string
,
durationTime
?:
number
)
=>
{
scrollToBottom
();
loading
.
value
=
true
;
try
{
try
{
// 开始对话
startConversation
();
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
currentAIResponse
.
value
=
null
;
// 调用外部传入的消息发送函数
// 调用外部传入的消息发送函数
if
(
props
.
onMessageSend
)
{
if
(
props
.
onMessageSend
)
{
console
.
log
(
'
调用外部音频发送函数
'
);
const
sendContent
=
type
===
'
audio
'
?
audioUrl
!
:
messageContent
;
await
props
.
onMessageSend
(
audioUrl
);
console
.
log
(
`调用外部发送函数`
,
sendContent
);
await
props
.
onMessageSend
(
sendContent
);
}
else
{
}
else
{
// 默认的API调用逻辑 - 使用与sendMessage相同的逻辑,只是参数不同
// 默认的API调用逻辑
const
response
=
await
post
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
{
console
.
log
(
`默认API调用逻辑`
,
dialogSessionId
.
value
);
const
requestData
=
type
===
'
audio
'
?
{
questionLocalAudioFilePath
:
audioUrl
,
questionLocalAudioFilePath
:
audioUrl
,
audioDuration
:
durationTime
,
audioDuration
:
durationTime
,
...
props
.
params
,
...
props
.
params
,
},
{
}
:
{
headers
:
{
question
:
messageContent
,
Token
:
props
.
token
||
''
,
...
props
.
params
,
'
x-session-id
'
:
props
.
token
||
''
,
};
'
x-app-code
'
:
props
.
appCode
||
''
,
}
});
const
data
=
response
.
data
;
if
(
data
.
code
===
0
)
{
console
.
log
(
'
音频发送成功
'
);
}
}
}
catch
(
e
)
{
console
.
error
(
'
发送音频消息失败:
'
,
e
);
}
finally
{
loading
.
value
=
false
;
}
};
// 开始对话函数 - 修改为在发送消息时调用
const
startConversation
=
()
=>
{
hasStartedConversation
.
value
=
true
;
};
// 发送消息
const
sendMessage
=
async
()
=>
{
loading
.
value
=
true
;
const
message
=
messageText
.
value
.
trim
();
if
(
message
)
{
// 开始对话
startConversation
();
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
currentAIResponse
.
value
=
null
;
messages
.
value
.
push
({
const
response
=
await
post
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
messageType
:
'
sent
'
,
requestData
,
avatar
:
'
我
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[
{
{
content
:
contentTemplates
.
text
(
message
),
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
}
],
});
if
(
textarea
.
value
)
{
textarea
.
value
.
style
.
height
=
'
50px
'
;
}
await
nextTick
();
scrollToBottom
();
try
{
messageText
.
value
=
''
;
// 调用外部传入的消息发送函数
if
(
props
.
onMessageSend
)
{
console
.
log
(
'
调用外部消息发送函数
'
,
message
);
await
props
.
onMessageSend
(
message
);
}
else
{
// 默认的API调用逻辑
console
.
log
(
'
默认API调用逻辑
'
,
dialogSessionId
);
const
response
=
await
post
(
`
${
props
.
apiBaseUrl
}
/aiService/ask/app/
${
props
.
params
?.
appId
}
`
,
{
question
:
message
,
...
props
.
params
,
},
{
headers
:
{
headers
:
{
Token
:
props
.
token
||
''
,
Token
:
props
.
token
||
''
,
'
x-session-id
'
:
props
.
token
||
''
,
'
x-session-id
'
:
props
.
token
||
''
,
'
x-app-code
'
:
props
.
appCode
||
''
,
'
x-app-code
'
:
props
.
appCode
||
''
,
}
}
});
const
data
=
response
.
data
;
if
(
data
.
code
===
0
)
{
loading
.
value
=
false
;
}
}
);
const
data
=
response
.
data
;
if
(
data
.
code
===
0
)
{
console
.
log
(
`发送成功`
);
}
}
}
catch
(
e
)
{
console
.
error
(
'
发送消息失败:
'
,
e
);
}
finally
{
loading
.
value
=
false
;
}
}
}
catch
(
e
)
{
console
.
error
(
`发送失败:`
,
e
);
}
finally
{
loading
.
value
=
false
;
}
}
};
};
// 处理SSE消息的核心方法 - 添加图表类型处理
// 发送音频消息的快捷函数(保持向后兼容)
const
processSSEMessage
=
(
const
sendAudioMessage
=
async
(
audioUrl
:
string
,
durationTime
?:
number
)
=>
{
data
:
SSEData
,
await
sendMessage
(
'
audio
'
,
{
audioUrl
,
durationTime
});
currentResponse
:
Message
|
null
,
isThinking
:
boolean
,
currentBlockIndex
:
number
,
isHistoryData
=
false
,
)
=>
{
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
:
contentTemplates
.
error
(
messageContent
||
'
出错了~~
'
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
break
;
case
3
:
// 图表数据
if
(
updatedResponse
)
{
switch
(
contentType
)
{
case
2
:
// 表格数据
const
{
rows
}
=
messageContent
;
// 表格数据处理
updatedResponse
.
contentBlocks
.
push
({
content
:
contentTemplates
.
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
:
contentTemplates
.
iframe
({
...
options
[
0
],
tips
:
tips
||
''
,
}),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
else
{
updatedResponse
.
contentBlocks
.
push
({
content
:
contentTemplates
.
option
(
messageContent
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
}
break
;
case
4
:
// MD格式
if
(
updatedResponse
)
{
const
markdownContent
=
contentTemplates
.
markdown
(
messageContent
||
''
);
// 检查最后一个块是否是markdown块
if
(
isLastBlockMarkdown
(
updatedResponse
.
contentBlocks
))
{
// 合并到现有的markdown块
const
lastMarkdownIndex
=
getLastMarkdownBlockIndex
(
updatedResponse
.
contentBlocks
);
if
(
lastMarkdownIndex
!==
-
1
)
{
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
=
mergeMarkdownContent
(
updatedResponse
.
contentBlocks
[
lastMarkdownIndex
].
content
,
markdownContent
);
}
}
else
{
// 创建新的markdown块
updatedResponse
.
contentBlocks
.
push
({
content
:
markdownContent
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
}
break
;
default
:
// 默认处理
updatedResponse
.
contentBlocks
.
push
({
content
:
contentTemplates
.
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
:
contentTemplates
.
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
:
contentTemplates
.
text
(
messageContent
),
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
});
}
break
;
}
return
{
updatedResponse
,
updatedIsThinking
,
updatedBlockIndex
,
recordId
,
promptTokens
,
completionTokens
,
totalTokens
,
newDialogSessionId
,
};
};
};
// 重新连接SSE
(添加重连间隔控制)
// 重新连接SSE
const
reconnectSSE
=
(
newDialogSessionId
:
string
)
=>
{
const
reconnectSSE
=
(
newDialogSessionId
:
string
)
=>
{
if
(
isReconnecting
.
value
)
{
console
.
log
(
'
正在重连中,跳过重复重连
'
);
return
;
}
isReconnecting
.
value
=
true
;
console
.
log
(
'
开始重连SSE,新的dialogSessionId:
'
,
newDialogSessionId
);
console
.
log
(
'
开始重连SSE,新的dialogSessionId:
'
,
newDialogSessionId
);
closeSSE
();
dialogSessionId
.
value
=
newDialogSessionId
;
dialogSessionId
.
value
=
newDialogSessionId
;
// 添加重连间隔控制,避免频繁重连
sseService
.
reconnectSSE
(
newDialogSessionId
);
const
reconnectTimeout
=
setTimeout
(()
=>
{
initSSE
();
// 重连完成后重置标志
setTimeout
(()
=>
{
isReconnecting
.
value
=
false
;
},
2000
);
// 延长重连间隔
},
500
);
// 增加重连延迟
timeArr
.
value
.
push
(
reconnectTimeout
);
};
};
// 关闭SSE连接
(增强清理逻辑)
// 关闭SSE连接
const
closeSSE
=
()
=>
{
const
closeSSE
=
()
=>
{
if
(
eventSource
.
value
)
{
sseService
.
closeSSE
();
try
{
eventSource
.
value
.
close
();
eventSource
.
value
=
null
;
}
catch
(
err
)
{
console
.
warn
(
'
关闭SSE连接时出错:
'
,
err
);
}
}
// 清理所有定时器
timeArr
.
value
.
forEach
(
timeout
=>
{
clearTimeout
(
timeout
);
});
timeArr
.
value
=
[];
};
};
// 初始化SSE连接
(添加错误边界)
// 初始化SSE连接
const
initSSE
=
()
=>
{
const
initSSE
=
()
=>
{
try
{
sseService
.
initSSE
(
dialogSessionId
.
value
);
const
url
=
`
${
props
.
apiBaseUrl
}
/aiService/sse/join/
${
props
.
params
?.
stage
||
''
}
?app-id=
${
props
.
params
?.
appId
||
''
}
&dialog-session-id=
${
dialogSessionId
.
value
||
''
}
`
;
console
.
log
(
'
初始化SSE连接,dialogSessionId:
'
,
dialogSessionId
.
value
);
eventSource
.
value
=
new
EventSourcePolyfill
(
url
,
{
headers
:
{
Token
:
props
.
token
||
''
,
'
x-session-id
'
:
props
.
token
||
''
,
'
x-app-code
'
:
props
.
appCode
||
''
,
},
withCredentials
:
true
,
connectionTimeout
:
30000
,
// 缩短超时时间
});
eventSource
.
value
.
onopen
=
(
event
)
=>
{
console
.
log
(
'
SSE连接已建立
'
,
event
);
};
eventSource
.
value
.
addEventListener
(
'
message
'
,
async
(
event
)
=>
{
try
{
console
.
log
(
'
Received message:
'
,
event
);
const
data
:
SSEData
=
JSON
.
parse
(
event
.
data
);
// 对于type21消息,先检查是否有实际内容再决定是否创建新消息
if
(
data
.
status
===
21
)
{
const
messageContent
=
data
.
message
||
''
;
const
hasContent
=
messageContent
?.
words
&&
messageContent
.
words
.
trim
()
!==
''
;
// 只有有实际内容时才创建新消息
if
(
!
isAIResponding
.
value
&&
hasContent
)
{
isAIResponding
.
value
=
true
;
currentAIResponse
.
value
=
{
messageType
:
'
received
'
,
avatar
:
'
AI
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[],
};
messages
.
value
.
push
(
currentAIResponse
.
value
);
}
}
else
{
// 其他消息类型保持原有逻辑
if
(
!
isAIResponding
.
value
)
{
isAIResponding
.
value
=
true
;
currentAIResponse
.
value
=
{
messageType
:
'
received
'
,
avatar
:
'
AI
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
:
dayjs
().
format
(
'
HH:mm
'
),
contentBlocks
:
[],
};
messages
.
value
.
push
(
currentAIResponse
.
value
);
}
}
// 实时消息处理,isHistoryData设为false,新会话思考框展开
const
result
=
processSSEMessage
(
data
,
currentAIResponse
.
value
,
isInThinkingMode
.
value
,
currentBlockIndex
.
value
,
false
,
);
currentAIResponse
.
value
=
result
.
updatedResponse
;
isInThinkingMode
.
value
=
result
.
updatedIsThinking
;
currentBlockIndex
.
value
=
result
.
updatedBlockIndex
;
// 如果type21没有内容,需要清理可能创建的空白消息
if
(
data
.
status
===
21
&&
currentAIResponse
.
value
&&
currentAIResponse
.
value
.
contentBlocks
.
length
===
0
)
{
// 移除空白消息
const
lastIndex
=
messages
.
value
.
length
-
1
;
if
(
lastIndex
>=
0
&&
messages
.
value
[
lastIndex
]
===
currentAIResponse
.
value
)
{
messages
.
value
.
splice
(
lastIndex
,
1
);
currentAIResponse
.
value
=
null
;
isAIResponding
.
value
=
false
;
}
}
if
(
result
.
recordId
&&
currentAIResponse
.
value
)
{
currentAIResponse
.
value
.
recordId
=
result
.
recordId
;
currentAIResponse
.
value
.
promptTokens
=
result
.
promptTokens
;
currentAIResponse
.
value
.
completionTokens
=
result
.
completionTokens
;
currentAIResponse
.
value
.
totalTokens
=
result
.
totalTokens
;
}
if
(
result
.
newDialogSessionId
)
{
console
.
log
(
'
收到新的 dialogSessionId:
'
,
result
.
newDialogSessionId
);
dialogSessionId
.
value
=
result
.
newDialogSessionId
;
}
await
nextTick
();
scrollToBottom
();
}
catch
(
error
)
{
console
.
error
(
'
处理SSE消息时出错:
'
,
error
);
}
});
eventSource
.
value
.
onerror
=
(
error
)
=>
{
console
.
error
(
'
SSE error:
'
,
error
);
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
closeSSE
();
// 添加错误重连逻辑
if
(
!
isReconnecting
.
value
)
{
setTimeout
(()
=>
{
if
(
dialogSessionId
.
value
)
{
reconnectSSE
(
dialogSessionId
.
value
);
}
},
3000
);
}
};
}
catch
(
err
)
{
console
.
error
(
'
初始化SSE连接失败:
'
,
err
);
}
};
};
// 组件卸载时清理所有资源
// 组件卸载时清理所有资源
...
@@ -826,92 +454,18 @@ onBeforeUnmount(() => {
...
@@ -826,92 +454,18 @@ onBeforeUnmount(() => {
closeSSE
();
closeSSE
();
isAIResponding
.
value
=
false
;
isAIResponding
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
isInThinkingMode
.
value
=
false
;
sseService
.
destroy
();
});
});
// 处理历史记录数据
// 处理历史记录数据
const
processHistoryData
=
(
dataArray
:
any
[])
=>
{
const
processHistoryData
=
(
dataArray
:
any
[])
=>
{
const
result
:
Message
[]
=
[];
return
templateService
.
processHistoryData
(
dataArray
,
{
dataArray
.
forEach
((
data
)
=>
{
tableTemplate
,
let
date
=
dayjs
(
data
.
startTime
).
format
(
'
YYYY-MM-DD HH:mm:ss
'
);
markdownTemplate
,
// 处理问题消息
isLastBlockMarkdown
,
if
(
data
.
question
||
data
.
audioPath
)
{
getLastMarkdownBlockIndex
,
let
questionContent
=
''
;
mergeMarkdownContent
// 检查是否为音频消息
if
(
data
.
audioPath
)
{
// 处理音频消息
questionContent
=
contentTemplates
.
audio
({
audioUrl
:
data
.
audioPath
,
durationTime
:
data
.
audioTime
||
'
0"
'
,
});
}
else
{
// 处理文本消息
questionContent
=
contentTemplates
.
text
(
data
.
question
);
}
result
.
push
({
messageType
:
'
sent
'
,
avatar
:
'
我
'
,
recordId
:
''
,
promptTokens
:
0
,
completionTokens
:
0
,
totalTokens
:
0
,
date
,
contentBlocks
:
[
{
content
:
questionContent
,
thinkContent
:
''
,
hasThinkBox
:
false
,
thinkBoxExpanded
:
false
,
},
],
});
}
// 处理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
=
processSSEMessage
(
sseData
,
aiMessage
,
currentThinkingMode
,
currentBlockIdx
,
true
,
);
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
;
};
};
// 获取历史会话消息
// 获取历史会话消息
...
@@ -979,7 +533,6 @@ defineExpose({
...
@@ -979,7 +533,6 @@ defineExpose({
reconnectSSE
,
// 重新连接SSE
reconnectSSE
,
// 重新连接SSE
// 消息处理方法
// 消息处理方法
processSSEMessage
,
// 处理SSE消息
processHistoryData
,
// 处理历史记录数据
processHistoryData
,
// 处理历史记录数据
// 其他实用方法
// 其他实用方法
...
@@ -994,37 +547,17 @@ defineExpose({
...
@@ -994,37 +547,17 @@ defineExpose({
isInThinkingMode
// 是否在思考模式
isInThinkingMode
// 是否在思考模式
});
});
// 初始化音频播放器
const
initAudioPlayersWrapper
=
()
=>
{
// 监听消息变化,为新的音频消息添加事件监听
watch
(
messages
,
()
=>
{
nextTick
(()
=>
{
initAudioPlayers
();
});
},
{
deep
:
true
});
};
// 生命周期
// 生命周期
onMounted
(()
=>
{
onMounted
(()
=>
{
console
.
log
(
'
组件挂载,初始 dialogSessionId:
'
,
props
.
dialogSessionId
);
console
.
log
(
'
组件挂载,初始 dialogSessionId:
'
,
props
.
dialogSessionId
);
initSSE
();
scrollToBottom
();
scrollToBottom
();
if
(
props
.
dialogSessionId
)
{
if
(
props
.
dialogSessionId
)
{
getChatRecord
(
props
.
dialogSessionId
);
getChatRecord
(
props
.
dialogSessionId
);
}
}
// 初始化音频播放器事件监听
initAudioPlayersWrapper
();
});
});
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
closeSSE
();
closeSSE
();
// 清除重连超时
timeArr
.
value
.
forEach
((
item
)
=>
{
clearTimeout
(
item
);
});
});
});
</
script
>
</
script
>
<
style
lang=
"less"
scoped
>
<
style
lang=
"less"
scoped
>
...
...
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