diff --git a/package.json b/package.json index a555e52..d5f56e1 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@arco-design/web-vue": "^2.42.0", + "@microsoft/fetch-event-source": "^2.0.1", "@types/nprogress": "^0.2.0", "@vueuse/core": "^9.12.0", "ali-oss": "^6.17.1", @@ -22,7 +23,6 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9071d24..37f87db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@arco-design/web-vue': specifier: ^2.42.0 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': specifier: ^0.2.0 version: 0.2.3 @@ -41,9 +44,6 @@ 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 @@ -627,6 +627,9 @@ packages: '@jridgewell/trace-mapping@0.3.17': 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': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -2378,9 +2381,6 @@ 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'} @@ -6120,6 +6120,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + '@microsoft/fetch-event-source@2.0.1': {} + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -8594,8 +8596,6 @@ snapshots: etag@1.8.1: {} - event-source-polyfill@1.0.31: {} - execa@5.1.1: dependencies: cross-spawn: 7.0.3 diff --git a/src/api/all/chat.ts b/src/api/all/chat.ts index 345ee54..cea1188 100644 --- a/src/api/all/chat.ts +++ b/src/api/all/chat.ts @@ -28,9 +28,9 @@ export const baseUrl = 'http://192.168.40.41:8001'; export const getHeaders = () => { const store = useEnterpriseStore(); return { - Authorization: glsWithCatch('accessToken'), + 'Authorization': glsWithCatch('accessToken'), 'enterprise-id': store.enterpriseInfo?.id, - Accept: 'application/json', + 'Accept': 'application/json', 'Content-Type': 'application/json', }; }; diff --git a/src/stores/modules/enterprise/index.ts b/src/stores/modules/enterprise/index.ts index b245cb8..1517998 100644 --- a/src/stores/modules/enterprise/index.ts +++ b/src/stores/modules/enterprise/index.ts @@ -4,7 +4,7 @@ import { useUserStore } from '@/stores/modules/user'; import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage'; interface EnterpriseInfo { - id: number; + id: number | string; name: string; update_name_quota: number; used_update_name_count: number; diff --git a/src/utils/querySSE.ts b/src/utils/querySSE.ts new file mode 100644 index 0000000..e9cb27a --- /dev/null +++ b/src/utils/querySSE.ts @@ -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; + method?: string; + body?: any; + handleMessage?: (data: any) => void; + handleError?: (err: any) => number | null | undefined | void; + handleClose?: () => void; + handleOpen?: (response: Response) => Promise; +} + +/** + * 创建服务器发送事件(SSE)连接 + * @param config SSE 配置 + * @param url 可选的自定义 URL + */ +export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise => { + 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); + }, + }); +}; diff --git a/src/views/home/components/conversation-detail/index.vue b/src/views/home/components/conversation-detail/index.vue index 4ead331..91c586a 100644 --- a/src/views/home/components/conversation-detail/index.vue +++ b/src/views/home/components/conversation-detail/index.vue @@ -12,8 +12,8 @@ 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'; +import querySSE from '@/utils/querySSE'; const QUESTION_ROLE = 'question'; const ANSWER_ROLE = 'text'; @@ -28,7 +28,6 @@ export default { const { copy } = useClipboard(); const senderRef = ref(null); - const eventSource = ref(null); const rightViewRef = ref(null); const bubbleListRef = ref(null); const searchValue = ref(''); @@ -163,102 +162,109 @@ export default { }; const initSse = () => { - // 先清理可能存在的旧连接 - if (eventSource.value) { - eventSource.value.close(); - eventSource.value = null; - } + const handleStart = () => { + conversationList.value.push({ + role: ANSWER_ROLE, + content: ( +
+ 智能思考 + +
+ ), + }); + }; - // 构建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'); + const handleNodeUpdate = (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, + messageRender: (content) =>
, + }); + break; + } + }; + + const handleFinalResult = (data) => { + showRightView.value = true; + 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 ( +
+ onDownload(rightViewContent.value)}> + + + {isLastAnswer && ( + onRefresh(currentAnswerId.value, conversationList.value.length)} + > + + + )} +
+ ); + }, + }); + }; + + const handleEnd = () => { + generateLoading.value = false; + }; + const handleError = () => { + generateLoading.value = false; + message.error('连接服务器失败'); + }; try { - eventSource.value = new EventSourcePolyfill(url.toString(), { - headers: { ...getHeaders(), Accept: 'text/event-stream' }, - }); - + const url = `http://localhost:3000/agent/input?content=${searchValue.value}&session_id=${conversationId.value}&agent_id=${chatStore.agentInfo?.agent_id}`; searchValue.value = ''; - - // 任务开始 - eventSource.value.addEventListener('start', function () { - conversationList.value.push({ - role: ANSWER_ROLE, - content: ( -
- 智能思考 - -
- ), - }); - }); - - 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 ( -
- onDownload(rightViewContent.value)}> - - - {isLastAnswer && ( - onRefresh(currentAnswerId.value, conversationList.value.length)} - > - - - )} -
- ); + 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; + } }, - }); - }); - - 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(); - }; + async handleOpen(response) { + console.log('onopen', response); + }, + }, + url, + ); } catch (error) { console.error('Failed to initialize SSE:', error); message.error('初始化连接失败'); @@ -266,24 +272,12 @@ export default { } }; - const closeSse = () => { - if (eventSource.value) { - eventSource.value.close(); - eventSource.value = null; - } - // 确保加载状态被重置 - generateLoading.value = false; - }; - onMounted(() => { searchValue.value = chatStore.searchValue; chatStore.clearSearchValue(); searchValue.value && initSse(); }); - onUnmounted(() => { - closeSse(); - }); return () => (