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

feat:把sse以及模版抽离

parent 08a063fa
...@@ -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 = defineProps({ 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>
......
<!-- 语音模版 -->
<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