feat: 修改sse调用

This commit is contained in:
rd
2025-08-25 11:49:58 +08:00
parent db9b53821b
commit 13670acc9a
6 changed files with 189 additions and 117 deletions

View File

@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@arco-design/web-vue": "^2.42.0", "@arco-design/web-vue": "^2.42.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@vueuse/core": "^9.12.0", "@vueuse/core": "^9.12.0",
"ali-oss": "^6.17.1", "ali-oss": "^6.17.1",
@ -22,7 +23,6 @@
"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",

16
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
'@arco-design/web-vue': '@arco-design/web-vue':
specifier: ^2.42.0 specifier: ^2.42.0
version: 2.42.0(vue@3.5.18(typescript@4.9.5)) version: 2.42.0(vue@3.5.18(typescript@4.9.5))
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
'@types/nprogress': '@types/nprogress':
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.3 version: 0.2.3
@ -41,9 +44,6 @@ 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
@ -627,6 +627,9 @@ packages:
'@jridgewell/trace-mapping@0.3.17': '@jridgewell/trace-mapping@0.3.17':
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
@ -2378,9 +2381,6 @@ 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'}
@ -6120,6 +6120,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.0 '@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14 '@jridgewell/sourcemap-codec': 1.4.14
'@microsoft/fetch-event-source@2.0.1': {}
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
dependencies: dependencies:
eslint-scope: 5.1.1 eslint-scope: 5.1.1
@ -8594,8 +8596,6 @@ 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

@ -28,9 +28,9 @@ export const baseUrl = 'http://192.168.40.41:8001';
export const getHeaders = () => { export const getHeaders = () => {
const store = useEnterpriseStore(); const store = useEnterpriseStore();
return { return {
Authorization: glsWithCatch('accessToken'), 'Authorization': glsWithCatch('accessToken'),
'enterprise-id': store.enterpriseInfo?.id, 'enterprise-id': store.enterpriseInfo?.id,
Accept: 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
}; };

View File

@ -4,7 +4,7 @@ import { useUserStore } from '@/stores/modules/user';
import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage'; import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage';
interface EnterpriseInfo { interface EnterpriseInfo {
id: number; id: number | string;
name: string; name: string;
update_name_quota: number; update_name_quota: number;
used_update_name_count: number; used_update_name_count: number;

78
src/utils/querySSE.ts Normal file
View File

@ -0,0 +1,78 @@
import { fetchEventSource } from '@microsoft/fetch-event-source';
import type { EventSourceMessage } from '@microsoft/fetch-event-source';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { glsWithCatch } from '@/utils/stroage';
const customHost = 'http://localhost:3000';
const DEFAULT_SSE_URL = `${customHost}/agent/input`;
const SSE_HEADERS = {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Accept: 'text/event-stream',
};
interface SSEConfig {
headers?: Record<string, string | number>;
method?: string;
body?: any;
handleMessage?: (data: any) => void;
handleError?: (err: any) => number | null | undefined | void;
handleClose?: () => void;
handleOpen?: (response: Response) => Promise<void>;
}
/**
* 创建服务器发送事件SSE连接
* @param config SSE 配置
* @param url 可选的自定义 URL
*/
export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise<void> => {
const {
body = undefined,
headers = {},
method = 'get',
handleMessage,
handleError,
handleOpen,
handleClose,
} = config;
const store = useEnterpriseStore();
fetchEventSource(url, {
method,
// credentials: 'include',
headers: {
...SSE_HEADERS,
Authorization: glsWithCatch('accessToken'),
'enterprise-id': store.enterpriseInfo?.id?.toString(),
...headers,
},
body,
openWhenHidden: true, // 用户切换到另一个页面后仍能保持SSE连接
onmessage(event: EventSourceMessage) {
if (event.data) {
try {
const parsedData = JSON.parse(event.data);
handleMessage?.({ ...event, data: parsedData });
} catch (error) {
console.error('Error parsing SSE message:', error);
handleError(new Error('Failed to parse SSE message'));
}
}
},
onerror(error: Error) {
console.error('SSE error:', error);
handleError?.(error);
},
onclose() {
console.log('SSE connection closed');
handleClose?.();
},
async onopen(response: Response) {
// console.log('onopen', response);
handleOpen?.(response);
},
});
};

View File

@ -12,8 +12,8 @@ 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 { 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';
import querySSE from '@/utils/querySSE';
const QUESTION_ROLE = 'question'; const QUESTION_ROLE = 'question';
const ANSWER_ROLE = 'text'; const ANSWER_ROLE = 'text';
@ -28,7 +28,6 @@ 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('');
@ -163,27 +162,7 @@ export default {
}; };
const initSse = () => { const initSse = () => {
// 先清理可能存在的旧连接 const handleStart = () => {
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({ conversationList.value.push({
role: ANSWER_ROLE, role: ANSWER_ROLE,
content: ( content: (
@ -193,11 +172,9 @@ export default {
</div> </div>
), ),
}); });
}); };
eventSource.value.addEventListener('node_update', function (event) { const handleNodeUpdate = (data) => {
console.log('Node updated:', event.data);
const data = JSON.parse(event.data);
switch (data.status) { switch (data.status) {
case 'running': case 'running':
conversationList.value.push({ conversationList.value.push({
@ -209,14 +186,14 @@ export default {
conversationList.value.push({ conversationList.value.push({
content: data.output, content: data.output,
role: ANSWER_ROLE, role: ANSWER_ROLE,
messageRender: (content) => <div v-html={md.render(content)}></div>,
}); });
break; break;
} }
}); };
eventSource.value.addEventListener('final_result', function (event) { const handleFinalResult = (data) => {
showRightView.value = true; showRightView.value = true;
const data = JSON.parse(event.data);
const _files = data.output?.files; const _files = data.output?.files;
rightViewContent.value = _files[0]?.content || ''; rightViewContent.value = _files[0]?.content || '';
@ -245,20 +222,49 @@ export default {
); );
}, },
}); });
}); };
eventSource.value.addEventListener('end', function (event) { const handleEnd = () => {
generateLoading.value = false; generateLoading.value = false;
closeSse(); };
}); const handleError = () => {
// 处理错误事件
eventSource.value.onerror = function (error) {
console.error('EventSource error:', error);
generateLoading.value = false; generateLoading.value = false;
message.error('连接服务器失败'); message.error('连接服务器失败');
closeSse();
}; };
try {
const url = `http://localhost:3000/agent/input?content=${searchValue.value}&session_id=${conversationId.value}&agent_id=${chatStore.agentInfo?.agent_id}`;
searchValue.value = '';
querySSE(
{
handleMessage(parsedData) {
const { event, data } = parsedData;
switch (event) {
case 'start':
handleStart();
break;
case 'node_update':
handleNodeUpdate(data);
break;
case 'final_result':
handleFinalResult(data);
break;
case 'end':
handleEnd();
break;
case 'error':
handleError();
break;
default:
break;
}
},
async handleOpen(response) {
console.log('onopen', response);
},
},
url,
);
} catch (error) { } catch (error) {
console.error('Failed to initialize SSE:', error); console.error('Failed to initialize SSE:', error);
message.error('初始化连接失败'); message.error('初始化连接失败');
@ -266,24 +272,12 @@ export default {
} }
}; };
const closeSse = () => {
if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
// 确保加载状态被重置
generateLoading.value = false;
};
onMounted(() => { onMounted(() => {
searchValue.value = chatStore.searchValue; searchValue.value = chatStore.searchValue;
chatStore.clearSearchValue(); chatStore.clearSearchValue();
searchValue.value && initSse(); searchValue.value && initSse();
}); });
onUnmounted(() => {
closeSse();
});
return () => ( return () => (
<div class="conversation-detail-wrap w-full h-full flex"> <div class="conversation-detail-wrap w-full h-full flex">