feat: sse连接,处理流式渲染

This commit is contained in:
rd
2025-08-23 11:53:27 +08:00
parent 063ce3df5e
commit 75874d88dc
6 changed files with 179 additions and 91 deletions

View File

@ -22,6 +22,7 @@
"dompurify": "^3.2.6", "dompurify": "^3.2.6",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-resize-detector": "^1.2.4", "element-resize-detector": "^1.2.4",
"event-source-polyfill": "^1.0.31",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^3.0.1", "jspdf": "^3.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

8
pnpm-lock.yaml generated
View File

@ -41,6 +41,9 @@ importers:
element-resize-detector: element-resize-detector:
specifier: ^1.2.4 specifier: ^1.2.4
version: 1.2.4 version: 1.2.4
event-source-polyfill:
specifier: ^1.0.31
version: 1.0.31
html2canvas: html2canvas:
specifier: ^1.4.1 specifier: ^1.4.1
version: 1.4.1 version: 1.4.1
@ -2375,6 +2378,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
event-source-polyfill@1.0.31:
resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==}
execa@5.1.1: execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -8588,6 +8594,8 @@ snapshots:
etag@1.8.1: {} etag@1.8.1: {}
event-source-polyfill@1.0.31: {}
execa@5.1.1: execa@5.1.1:
dependencies: dependencies:
cross-spawn: 7.0.3 cross-spawn: 7.0.3

View File

@ -24,9 +24,8 @@ export const deleteHistoryItem = (id: string) => {
}; };
export const baseUrl = 'http://192.168.40.41:8001'; export const baseUrl = 'http://192.168.40.41:8001';
const getHeaders = () => { export const getHeaders = () => {
const store = useEnterpriseStore(); const store = useEnterpriseStore();
return { return {
Authorization: glsWithCatch('accessToken'), Authorization: glsWithCatch('accessToken'),
@ -46,17 +45,6 @@ export const getAgentInfo = async () => {
return data; return data;
}; };
/**
* 指令输入
*/
export const getInputAgent = async (params: {}) => {
const { data } = await axios.get(`${baseUrl}/api/agent/input`, {
params,
headers: { ...getHeaders(), Accept: 'text/event-stream' },
});
return data;
};
/** /**
* 生成会话id * 生成会话id
*/ */

View File

@ -4,17 +4,21 @@ import { BubbleList } from '@/components/xt-chat/xt-bubble';
import SenderInput from '../sender-input/index.vue'; import SenderInput from '../sender-input/index.vue';
import { Typography } from 'ant-design-vue'; import { Typography } from 'ant-design-vue';
import RightView from './rightView.vue'; import RightView from './rightView.vue';
import type { Ref } from 'vue'; import TextOverTips from '@/components/text-over-tips/index.vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import markdownit from 'markdown-it'; import markdownit from 'markdown-it';
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { genRandomId } from '@/utils/tools'; import { genRandomId } from '@/utils/tools';
import { useChatStore } from '@/stores/modules/chat'; import { useChatStore } from '@/stores/modules/chat';
import { getHeaders } from '@/api/all/chat';
import { EventSourcePolyfill } from 'event-source-polyfill'; // 导入 event-source-polyfill
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types'; import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
const QUESTION_ROLE = 'question'; const QUESTION_ROLE = 'question';
const ANSWER_ROLE = 'text'; const ANSWER_ROLE = 'text';
const FILE_ROLE = 'file';
const THOUGHT_ROLE = 'thought'; // 新增思考过程角色常量
export default { export default {
setup(props, { emit, expose }) { setup(props, { emit, expose }) {
@ -24,6 +28,7 @@ export default {
const { copy } = useClipboard(); const { copy } = useClipboard();
const senderRef = ref(null); const senderRef = ref(null);
const eventSource = ref(null);
const rightViewRef = ref(null); const rightViewRef = ref(null);
const bubbleListRef = ref<any>(null); const bubbleListRef = ref<any>(null);
const searchValue = ref(''); const searchValue = ref('');
@ -80,78 +85,7 @@ export default {
}); });
currentAnswerId.value = tempId; currentAnswerId.value = tempId;
setTimeout(() => { initSse();
const content = `# 测试数据表格
## 用户信息表
| 表头1 | 表头2 |
|---|---|
| 内容1 | 内容2 |
## 项目统计表
| 项目名称 | 负责人 | 进度 | 状态 | 预算(万元) | 完成度 |
|----------|--------|------|------|------------|--------|
| 电商平台重构 | 张三 | 75% | 进行中 | 50 | 🟡 |
| 移动端APP | 李四 | 90% | 测试中 | 30 | 🟢 |
| 数据分析系统 | 王五 | 45% | 开发中 | 80 | 🟡 |
| 客户管理系统 | 赵六 | 100% | 已完成 | 25 | 🟢 |
| 营销自动化 | 钱七 | 30% | 规划中 | 60 | 🔴 |
## 销售数据
| 月份 | 销售额(万) | 订单数 | 客户数 | 增长率 | 备注 |
|------|------------|--------|--------|--------|------|
| 1月 | 120.5 | 156 | 89 | +12% | 春节促销 |
| 2月 | 98.3 | 134 | 76 | -18% | 淡季 |
| 3月 | 145.2 | 189 | 102 | +48% | 新品上市 |
| 4月 | 167.8 | 203 | 115 | +16% | 稳定增长 |
| 5月 | 189.6 | 234 | 128 | +13% | 持续增长 |
> 以上数据仅供参考,实际数据请以系统为准。
**注意事项:**
- 所有数据均为测试数据
- 表格支持Markdown格式渲染
- 可以包含表情符号和特殊字符`;
showRightView.value = true;
rightViewContent.value = '';
nextTick(() => {
rightViewContent.value = content;
});
searchValue.value = '';
conversationList.value.splice(tempIndex, 1, {
id: tempId,
loading: false,
role: ANSWER_ROLE,
content,
messageRender: (content) => (
<Typography>
<div v-html={md.render(content)}></div>
</Typography>
),
footer: ({ item }) => {
// 判断当前元素是否是最后一个非QUESTION_ROLE的元素
const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE);
const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id;
return (
<div class="flex items-center">
<Tooltip title="复制" onClick={() => onCopy(content)}>
<icon-copy size={16} class="mr-12px color-#737478 cursor-pointer" />
</Tooltip>
{isLastAnswer && (
<Tooltip title="重新生成" onClick={() => onRefresh(tempId, tempIndex)}>
<icon-refresh size={16} class="color-#737478 cursor-pointer" />
</Tooltip>
)}
</div>
);
},
});
}, 1000);
}; };
const handleCancel = () => { const handleCancel = () => {
@ -181,6 +115,39 @@ export default {
margin: '0 auto', margin: '0 auto',
}, },
}, },
[FILE_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
messageRender: (items) => {
return items.map((item) => (
<div class="file-card">
<icon-file class="w-17px h-20px mr-20px color-#6D4CFE" />
<div>
<TextOverTips
context={item.name}
class="font-family-medium color-#211F24 text-14px font-400 lh-22px mb-4px"
/>
<span class="color-#939499 font-family-regular text-12px font-400 lh-22px">创建时间08-04 12:40</span>
</div>
</div>
));
},
style: {
width: '600px',
margin: '0 auto',
},
},
// 新增思考过程角色配置
[THOUGHT_ROLE]: {
placement: 'start',
variant: 'borderless',
style: {
width: '600px',
margin: '0 auto',
},
},
[QUESTION_ROLE]: { [QUESTION_ROLE]: {
placement: 'end', placement: 'end',
shape: 'round', shape: 'round',
@ -191,16 +158,129 @@ export default {
}, },
}; };
const initSse = () => { const onDownload = (content) => {
console.log('initSse', { agentInfo: chatStore.agentInfo, searchValue: chatStore.searchValue }); console.log('onDownload', content);
}; };
const initSse = () => {
// 先清理可能存在的旧连接
if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
// 构建SSE连接URL
const url = new URL('http://localhost:3000/agent/input');
url.searchParams.append('content', searchValue.value || '测试');
url.searchParams.append('session_id', conversationId.value as string);
url.searchParams.append('agent_id', chatStore.agentInfo?.agent_id || '67890');
try {
eventSource.value = new EventSourcePolyfill(url.toString(), {
headers: { ...getHeaders(), Accept: 'text/event-stream' },
});
searchValue.value = '';
// 任务开始
eventSource.value.addEventListener('start', function () {
conversationList.value.push({
role: ANSWER_ROLE,
content: (
<div class="flex items-center ">
<span class="font-family-medium color-#211F24 text-14px font-400 lh-22px mr-4px">智能思考</span>
<icon-caret-up size={16} class="color-#211F24" />
</div>
),
});
});
eventSource.value.addEventListener('node_update', function (event) {
console.log('Node updated:', event.data);
const data = JSON.parse(event.data);
switch (data.status) {
case 'running':
conversationList.value.push({
content: data.message,
role: ANSWER_ROLE,
});
break;
case 'success':
conversationList.value.push({
content: data.output,
role: ANSWER_ROLE,
});
break;
}
});
eventSource.value.addEventListener('final_result', function (event) {
showRightView.value = true;
const data = JSON.parse(event.data);
const _files = data.output?.files;
rightViewContent.value = _files[0]?.content || '';
conversationList.value.push({
id: currentAnswerId.value,
role: FILE_ROLE,
content: _files,
footer: ({ item }) => {
const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE);
const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id;
return (
<div class="flex items-center">
<Tooltip title="下载" onClick={() => onDownload(rightViewContent.value)}>
<icon-download size={16} class="color-#737478 cursor-pointer" />
</Tooltip>
{isLastAnswer && (
<Tooltip
title="重新生成"
onClick={() => onRefresh(currentAnswerId.value, conversationList.value.length)}
>
<icon-refresh size={16} class="color-#737478 cursor-pointer" />
</Tooltip>
)}
</div>
);
},
});
});
eventSource.value.addEventListener('end', function (event) {
generateLoading.value = false;
closeSse();
});
// 处理错误事件
eventSource.value.onerror = function (error) {
console.error('EventSource error:', error);
generateLoading.value = false;
message.error('连接服务器失败');
closeSse();
};
} catch (error) {
console.error('Failed to initialize SSE:', error);
message.error('初始化连接失败');
generateLoading.value = false;
}
};
const closeSse = () => { const closeSse = () => {
console.log('closeSse'); if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
// 确保加载状态被重置
generateLoading.value = false;
}; };
onMounted(() => { onMounted(() => {
initSse(); searchValue.value = chatStore.searchValue;
chatStore.clearSearchValue();
searchValue.value && initSse();
}); });
onUnmounted(() => { onUnmounted(() => {
closeSse(); closeSse();
}); });

View File

@ -6,4 +6,15 @@
font-weight: 400; font-weight: 400;
line-height: 22px; line-height: 22px;
} }
:deep(.xt-bubble) {
.file-card {
border-radius: 12px;
border: 1px solid var(--Border-2, #e6e6e8);
background: linear-gradient(90deg, #f6f4ff 0%, #fff 100%);
padding: 13px 16px;
display: flex;
justify-content: center;
align-items: center;
}
}
} }

View File

@ -28,7 +28,7 @@ export default {
onMounted(() => { onMounted(() => {
getAgentData(); getAgentData();
}); });
return () => ( return () => (
<div class="chat-wrap rounded-12px w-full h-full"> <div class="chat-wrap rounded-12px w-full h-full">
{conversationId.value ? <ConversationDetail /> : <ConversationCreate />} {conversationId.value ? <ConversationDetail /> : <ConversationCreate />}