window中有一个叫EventSource的构造函数。一个EventSource实例会对服务器开启一个持久化的连接,以text/event-stream格式发送事件,此连接会一直保持开启直到通过调用EventSource.close()关闭。但使用EventSource时只能把参数加到url后面,而且也不能像fetch请求那样设置header等参数。借助fetch-event-source这个库就可以像发起fetch请求一样发起服务器单向通信请求。
对接AI的api,返回的数据是流式的,后台不好转,前端项目vue3
使用 @microsoft/fetch-event-source,实现fetchEvent 的post传送
fetch-event-source使用
await fetchEventSource(url, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
"Accept": "text/event-stream",
"Authorization": `Bearer XXXX`,
"Transfer-Encoding": "chunked",
},
body: JSON.stringify(params),
signal: cancelTokenSourceRef.current.signal,
onopen(response) { // 成功建立连接
},
onmessage(msg) { // 接收信息
// 这里接收数据处理业务逻辑
const result = JSON.parse(msg.data);
if (result.event === "message") {
}else if (result.event === "message_end"){
console.log("回答完毕");}
}, // 接收信息
onerror(err) {
console.error("发生错误", err);
throw err;
},
onclose(res) {
console.log("关闭链接", res);
},
openWhenHidden: true,
});
接下来直接展示代码吧
- 下载 @microsoft/fetch-event-source
npm install @microsoft/fetch-event-source
- 项目引用
import { fetchEventSource } from "@microsoft/fetch-event-source";
- 中断会话的用处
// 用于中断会话,中断时调用cancelTokenSourceRef.current.abort();
const cancelTokenSourceRef = ref(new AbortController());
cancelTokenSourceRef.current = new AbortController();
- 设置重试次数,有时候是网络原因,所以要再次请求看看
let retryDelay = 1000; // 初始重试延迟(毫秒)
const maxRetryDelay = 30000; // 最大重试延迟(毫秒)
const maxRetries = 3; // 最大重试次数
完整代码
const cancelTokenSourceRef = ref(new AbortController());
cancelTokenSourceRef.current = new AbortController();
const attemptFetch = async (retryCount = 0) => {
try {
var params = {
inputs: {},
query: text || prompt.value,
response_mode: "streaming",
conversation_id: conversation_id.value, // 选填)会话 ID
user: loginUser.value.nickname || "abc-123", // 用户标识
auto_generate_name:true // 选填)自动生成标题
};
// console.log('difyConfig------------params',params);
await fetchEventSource(`/difyChatApi/v1${difyConfig.value.path}`, {
method: "POST",
headers: {
// "Content-Type": "application/json",
"Content-Type": "application/json; charset=utf-8",
"Accept": "text/event-stream",
"Authorization": `Bearer ${difyConfig.value.apikey}`,
"Transfer-Encoding": "chunked",
},
body: JSON.stringify(params),
signal: cancelTokenSourceRef.current.signal,
onopen(response) {
if (response.ok) {
console.log("成功建立连接");
connect_success = false;
// 重新链接的时候 char重置为0
typeWriterObj.charIndex = 0;
typeWriterObj.timer = null;
typeWriterObj.flag = false;
retryDelay = 1000; // 重置重试延迟
} else {
chatData.value[chatData.value.length - 1].isLoading = false;
chatData.value[chatData.value.length - 1].content = "连接失败。ε(┬┬﹏┬┬)3";
enableInput();
throw new Error("连接失败");
}
},
onmessage(msg) {
console.log('Received message:');
if (msg.data) {
// connect_success来判断是否是第一次的信息加载
if (!connect_success) {
console.log('接收消息---onmessage');
connect_success = true;
}
const result = JSON.parse(msg.data);
if (result.event === "message") {
conversation_id.value = result.conversation_id;
session_id.value = result.message_id;
console.log("流");
} else if (result.event === "message_end") {
// 回复
console.log("回答完毕");
}
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
}
},
onerror(err) {
console.error("发生错误", err);
throw err;
},
onclose(res) {
console.log("关闭链接", res);
// lineBuffer.value = ""
// disableInput(true);
},
openWhenHidden: true,
});
} catch (err) {
console.log("错误是什么?", err);
if (retryCount < maxRetries) {
console.log(`尝试重连... (重试次数: ${retryCount + 1})`);
retryTime.value = setTimeout(() => {
attemptFetch(retryCount + 1);
}, retryDelay);
retryDelay = Math.min(retryDelay * 2, maxRetryDelay); // 指数退避
} else {
console.error("达到最大重试次数,停止重试");
}
}
};
attemptFetch();
};