Commit b4dd2f2d authored by 水玉婷's avatar 水玉婷
Browse files

feat:把sse以及模版抽离

parent 08a063fa
This diff is collapsed.
<!-- 语音模版 -->
<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
...@@ -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消息样式
......
/**
* 音频模板工具类
* 用于生成音频消息的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
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
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
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment