feat: sse连接,处理流式渲染
This commit is contained in:
@ -22,6 +22,7 @@
|
||||
"dompurify": "^3.2.6",
|
||||
"echarts": "^5.6.0",
|
||||
"element-resize-detector": "^1.2.4",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -41,6 +41,9 @@ importers:
|
||||
element-resize-detector:
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
event-source-polyfill:
|
||||
specifier: ^1.0.31
|
||||
version: 1.0.31
|
||||
html2canvas:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1
|
||||
@ -2375,6 +2378,9 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
event-source-polyfill@1.0.31:
|
||||
resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==}
|
||||
|
||||
execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
@ -8588,6 +8594,8 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
event-source-polyfill@1.0.31: {}
|
||||
|
||||
execa@5.1.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
|
||||
@ -24,9 +24,8 @@ export const deleteHistoryItem = (id: string) => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const baseUrl = 'http://192.168.40.41:8001';
|
||||
const getHeaders = () => {
|
||||
export const getHeaders = () => {
|
||||
const store = useEnterpriseStore();
|
||||
return {
|
||||
Authorization: glsWithCatch('accessToken'),
|
||||
@ -46,17 +45,6 @@ export const getAgentInfo = async () => {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -4,17 +4,21 @@ import { BubbleList } from '@/components/xt-chat/xt-bubble';
|
||||
import SenderInput from '../sender-input/index.vue';
|
||||
import { Typography } from 'ant-design-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 markdownit from 'markdown-it';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { genRandomId } from '@/utils/tools';
|
||||
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';
|
||||
|
||||
const QUESTION_ROLE = 'question';
|
||||
const ANSWER_ROLE = 'text';
|
||||
const FILE_ROLE = 'file';
|
||||
const THOUGHT_ROLE = 'thought'; // 新增思考过程角色常量
|
||||
|
||||
export default {
|
||||
setup(props, { emit, expose }) {
|
||||
@ -24,6 +28,7 @@ export default {
|
||||
const { copy } = useClipboard();
|
||||
|
||||
const senderRef = ref(null);
|
||||
const eventSource = ref(null);
|
||||
const rightViewRef = ref(null);
|
||||
const bubbleListRef = ref<any>(null);
|
||||
const searchValue = ref('');
|
||||
@ -80,78 +85,7 @@ export default {
|
||||
});
|
||||
currentAnswerId.value = tempId;
|
||||
|
||||
setTimeout(() => {
|
||||
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);
|
||||
initSse();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@ -181,6 +115,39 @@ export default {
|
||||
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]: {
|
||||
placement: 'end',
|
||||
shape: 'round',
|
||||
@ -191,16 +158,129 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
const initSse = () => {
|
||||
console.log('initSse', { agentInfo: chatStore.agentInfo, searchValue: chatStore.searchValue });
|
||||
const onDownload = (content) => {
|
||||
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 = () => {
|
||||
console.log('closeSse');
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close();
|
||||
eventSource.value = null;
|
||||
}
|
||||
// 确保加载状态被重置
|
||||
generateLoading.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initSse();
|
||||
searchValue.value = chatStore.searchValue;
|
||||
chatStore.clearSearchValue();
|
||||
searchValue.value && initSse();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
closeSse();
|
||||
});
|
||||
|
||||
@ -6,4 +6,15 @@
|
||||
font-weight: 400;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export default {
|
||||
onMounted(() => {
|
||||
getAgentData();
|
||||
});
|
||||
|
||||
|
||||
return () => (
|
||||
<div class="chat-wrap rounded-12px w-full h-full">
|
||||
{conversationId.value ? <ConversationDetail /> : <ConversationCreate />}
|
||||
|
||||
Reference in New Issue
Block a user