一、使用EventSOurce
EventSource
接口是 web 内容与服务器发送事件通信的接口。
const evtSource = new EventSource("服务器地址");
evtSource.onopen = (e) => {
console.log('建立链接',e)
};
evtSource.onmessage = (e) => {
console.log('接收消息',e)
};
evtSource.onerror= (error) => {
console.log('错误信息',error)
};
备注: 你可以在 Github 上找到完整的示例——使用 PHP 语言的简单 SSE 示例。
二、 使用@microsoft/fetch-event-source
1、安装
npm i @microsoft/fetch-event-source
2、使用
import { fetchEventSource } from '@microsoft/fetch-event-source';
const url = ref('/getMsg');
class RetriableError extends Error {}
class FatalError extends Error {}
const abortController = ref();
const eventSource = ref();
abortController.value = new AbortController();
const initFetchEventSource = (url) => {
eventSource.value = fetchEventSource(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
signal: abortController.value.signal,
asynconopen() {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return; // everything's good
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// client-side errors are usually non-retriable:
throw new FatalError();
} else {
throw new RetriableError();
}
},
onmessage(msg) {
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
// console.log('接收消息', msg);
if (msg.event === 'FatalError') {
throw new FatalError(msg.data);
}
if (msg.data.includes('DONE')) {
abortController.value.abort(); // 结束会话
} else {
let res = JSON.parse(msg.data);
console.log('接收消息',res)
}
},
onclose() {
console.log('关闭链接')
abortController.value.abort(); // 结束会话
},
onerror(err) {
console.log('抛出异常', err);
abortController.value.abort(); // 结束会话
}
});
使用POST请求
const initFetchEventSource = (url) => {
eventSource.value = fetchEventSource(url, {
method: 'POSt',
headers: {
'Content-Type': 'application/json',
'authorization': 'Bearer ' + getStorages(),
},
body: JSON.stringify({
msg: message.value,
modelId: detailObj.value?.id,
temperature: sliderList.value[0].num,
topPnum: sliderList.value[1].num,
promptId: currentPrompt.value?.id
}),
signal: abortController.value.signal,
asynconopen() {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return; // everything's good
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// client-side errors are usually non-retriable:
throw new FatalError();
} else {
throw new RetriableError();
}
},
onmessage(msg) {
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
// console.log('接收消息', msg);
if (msg.event === 'FatalError') {
throw new FatalError(msg.data);
}
if (msg.data.includes('DONE')) {
closeEventSource();
} else {
let res = JSON.parse(msg.data);
console.log('接收消息',res)
}
},
onclose() {
console.log('关闭链接')
abortController.value.abort(); // 结束会话
},
onerror(err) {
console.log('抛出异常', err);
abortController.value.abort(); // 结束会话
}
});
};
示例
1736940217165.png
1736928560753.png
回显回答,仿照Ai 打字
let index = 0;
messageObj.answer = '';
timer.value = setInterval(() => {
if (index < tempContent.value.length) {
messageObj.answer = `${messageObj.answer}${tempContent.value[index]}`;
index++;
scrollToBottom();
} else {
clearInterval(timer.value);
tempContent.value = '';
scrollToBottom();
closeEventSource();
isAnswer.value = false;
}
}, 80);
所有代码
import { require } from '@/utils';
import { ref } from 'vue';
import Prompt from './prompt.vue';
import { useUserStore } from '@/store';
import empty from '@/assets/images/models/empty.png';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { apiPath } from '@/config';
import { getStorages } from '@/utils';
const userStore = useUserStore();
const detailObj = ref();
const currentPrompt = ref();
const isAnswer = ref(false);
const contentRef = ref();
const conversations = ref([]);
const max = 5;
const abortController = ref();
abortController.value = new AbortController();
const tempContent = ref('');
const eventSource = ref();
const url = ref(`/getMsg`);
const timer = ref();
// 滑块
const handleInput = (id, value) => {};
const identy = ref(1);
const message = ref(null);
const setData = (obj = {}) => {
detailObj.value = obj;
};
defineExpose({ setData });
const addNewLine = (event) => {
console.log('event', event);
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
message.value += '\n';
} else {
return;
}
};
const onCloseAnswer = () => {
if (timer.value) {
clearInterval(timer.value);
}
closeEventSource();
isAnswer.value = false;
};
const initFetchEventSource = (url) => {
class RetriableError extends Error {}
class FatalError extends Error {}
// console.log('conversations.value', conversations.value);
let list = conversations.value.map((item) => item.list);
let realyList = [].concat(...list);
let msgList = [];
if (realyList.length > max) {
let length = realyList.length;
// console.log('length', length);
for (let i = 0; i < length; i++) {
// console.log('length', length, length - max + i);
if (i < max) {
msgList.push({
'role': 'user',
'content': realyList[length - max + i].question
});
msgList.push({
'role': 'system',
'content': realyList[length - max + i]?.answer || ''
});
} else {
break;
}
}
} else {
for (let i = 0; i < realyList.length; i++) {
msgList.push({
'role': 'user',
'content': realyList[i].question
});
msgList.push({
'role': 'system',
'content': realyList[i]?.answer || ''
});
}
}
// console.log('realyListlist==', realyList);
eventSource.value = fetchEventSource(url, {
method: 'POSt',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
msg: msgList,
modelId: detailObj.value?.id,
temperature: sliderList.value[0].num,
topPnum: sliderList.value[1].num,
promptId: currentPrompt.value?.id
}),
signal: abortController.value.signal,
// openWhenHidden: true,
async onopen(response) {
// if (response.ok && response.headers.get('content-type') === 'EventStreamContentType') {
// return; // everything's good
// } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// // client-side errors are usually non-retriable:
// console.log('response', response);
// throw new FatalError();
// } else {
// throw new RetriableError();
// }
},
onmessage(msg) {
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
// console.log('接收消息', msg);
if (msg.event === 'FatalError') {
throw new FatalError(msg.data);
}
if (msg.data.includes('DONE')) {
// console.log('sss');
let messageList = conversations.value[conversations.value.length - 1];
let messageObj = messageList.list[messageList.list.length - 1];
// console.log(' tempContent.value', tempContent.value);
let index = 0;
messageObj.answer = '';
timer.value = setInterval(() => {
if (index < tempContent.value.length) {
messageObj.answer = `${messageObj.answer}${tempContent.value[index]}`;
index++;
scrollToBottom();
} else {
clearInterval(timer.value);
tempContent.value = '';
scrollToBottom();
closeEventSource();
isAnswer.value = false;
}
}, 80);
} else {
let res = JSON.parse(msg.data);
let content = res.choices[0].delta.content;
//.replace(/\n/g, '<br/>');
tempContent.value = `${tempContent.value}${content}`;
}
},
onclose() {
closeEventSource();
throw new FatalError();
},
onerror(err) {
// console.log('err instanceof FatalError', err instanceof FatalError);
if (err instanceof FatalError) {
throw err; // rethrow to stop the operation
} else {
console.log('抛出异常', err);
closeEventSource();
isAnswer.value = false;
}
// eventSource.value.close();
}
}).catch((err) => {
console.log('err', err);
});
};
// 结束回答
const closeEventSource = () => {
if (abortController.value) {
abortController.value.abort(); // 结束会话
}
eventSource.value = null;
// return;
};
// 发送数据
const onClickSend = () => {
if (event.ctrlKey && event.key === 'Enter') {
return;
}
if (!message.value.trim() || isAnswer.value) {
return;
}
isAnswer.value = true;
let messageObj = conversations.value[conversations.value.length - 1];
let json = {
userName: userStore.nickname,
question: message.value,
prompt: currentPrompt.value,
answer: messageObj.answer || ''
};
// console.log('conversations.value', conversations.value.length);
messageObj.list.push(json);
nextTick(() => {
scrollToBottom();
});
// console.log('detailObj', detailObj.value, currentPrompt.value);
// console.log(conversations.value);
initFetchEventSource(url.value);
message.value = '';
};
const scrollToBottom = () => {
contentRef.value.scrollTo(0, contentRef.value.scrollHeight);
};
const drawer = ref(false);
const onClickPrompt = () => {
drawer.value = true;
};
// 使用模板
const promptUse = (value) => {
currentPrompt.value = value;
identy.value += 1;
let obj = {
promptName: currentPrompt.value.name,
promptContent: currentPrompt.value.content,
showPromptContent: false,
list: []
};
conversations.value.push(obj);
drawer.value = false;
};
const clearPrompt = () => {
currentPrompt.value = null;
let obj = {
promptName: '系统模型回答',
promptContent: '',
list: []
};
conversations.value.push(obj);
};
const updateDrawer = (value: boolean) => {
drawer.value = value;
};
const init = () => {
conversations.value = [];
// 物理添加 页面
conversations.value.push({
promptName: '系统模型回答',
promptContent: '',
showPromptContent: false,
list: []
});
};
onMounted(() => {
init();
});
onUnmounted(() => {
closeEventSource();
});