feat: 修改sse调用
This commit is contained in:
@ -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
16
pnpm-lock.yaml
generated
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
78
src/utils/querySSE.ts
Normal 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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,102 +162,109 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initSse = () => {
|
const initSse = () => {
|
||||||
// 先清理可能存在的旧连接
|
const handleStart = () => {
|
||||||
if (eventSource.value) {
|
conversationList.value.push({
|
||||||
eventSource.value.close();
|
role: ANSWER_ROLE,
|
||||||
eventSource.value = null;
|
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>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 构建SSE连接URL
|
const handleNodeUpdate = (data) => {
|
||||||
const url = new URL('http://localhost:3000/agent/input');
|
switch (data.status) {
|
||||||
url.searchParams.append('content', searchValue.value || '测试');
|
case 'running':
|
||||||
url.searchParams.append('session_id', conversationId.value as string);
|
conversationList.value.push({
|
||||||
url.searchParams.append('agent_id', chatStore.agentInfo?.agent_id || '67890');
|
content: data.message,
|
||||||
|
role: ANSWER_ROLE,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'success':
|
||||||
|
conversationList.value.push({
|
||||||
|
content: data.output,
|
||||||
|
role: ANSWER_ROLE,
|
||||||
|
messageRender: (content) => <div v-html={md.render(content)}></div>,
|
||||||
|
});
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
generateLoading.value = false;
|
||||||
|
};
|
||||||
|
const handleError = () => {
|
||||||
|
generateLoading.value = false;
|
||||||
|
message.error('连接服务器失败');
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eventSource.value = new EventSourcePolyfill(url.toString(), {
|
const url = `http://localhost:3000/agent/input?content=${searchValue.value}&session_id=${conversationId.value}&agent_id=${chatStore.agentInfo?.agent_id}`;
|
||||||
headers: { ...getHeaders(), Accept: 'text/event-stream' },
|
|
||||||
});
|
|
||||||
|
|
||||||
searchValue.value = '';
|
searchValue.value = '';
|
||||||
|
querySSE(
|
||||||
// 任务开始
|
{
|
||||||
eventSource.value.addEventListener('start', function () {
|
handleMessage(parsedData) {
|
||||||
conversationList.value.push({
|
const { event, data } = parsedData;
|
||||||
role: ANSWER_ROLE,
|
switch (event) {
|
||||||
content: (
|
case 'start':
|
||||||
<div class="flex items-center ">
|
handleStart();
|
||||||
<span class="font-family-medium color-#211F24 text-14px font-400 lh-22px mr-4px">智能思考</span>
|
break;
|
||||||
<icon-caret-up size={16} class="color-#211F24" />
|
case 'node_update':
|
||||||
</div>
|
handleNodeUpdate(data);
|
||||||
),
|
break;
|
||||||
});
|
case 'final_result':
|
||||||
});
|
handleFinalResult(data);
|
||||||
|
break;
|
||||||
eventSource.value.addEventListener('node_update', function (event) {
|
case 'end':
|
||||||
console.log('Node updated:', event.data);
|
handleEnd();
|
||||||
const data = JSON.parse(event.data);
|
break;
|
||||||
switch (data.status) {
|
case 'error':
|
||||||
case 'running':
|
handleError();
|
||||||
conversationList.value.push({
|
break;
|
||||||
content: data.message,
|
default:
|
||||||
role: ANSWER_ROLE,
|
break;
|
||||||
});
|
}
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
async handleOpen(response) {
|
||||||
});
|
console.log('onopen', response);
|
||||||
|
},
|
||||||
eventSource.value.addEventListener('end', function (event) {
|
},
|
||||||
generateLoading.value = false;
|
url,
|
||||||
closeSse();
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// 处理错误事件
|
|
||||||
eventSource.value.onerror = function (error) {
|
|
||||||
console.error('EventSource error:', error);
|
|
||||||
generateLoading.value = false;
|
|
||||||
message.error('连接服务器失败');
|
|
||||||
closeSse();
|
|
||||||
};
|
|
||||||
} 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user