注意:后端和大模型返回的内容也要是流式输出的,不然前端做了流式输出也没用。
1、封装 fetch请求
// 创建fetch请求的封装函数 (流式输出)
export async function fetchAPI(url, data) {
// 构造请求体
const requestBody = JSON.stringify(data);
const token = localStorage.getItem("token");
// 发送POST请求
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`, // 添加JWT头部
},
body: requestBody,
};
try {
return await fetch(url, options);
} catch (error) {
console.error("请求失败: ", error);
}
}
定义了一个名为 fetchAPI
的异步函数,用于发送POST请求。它接受两个参数:url
和 data
。函数首先将传入的数据(data
)转换为JSON格式,然后从本地存储(localStorage
)中获取一个名为 "token" 的值。这个token通常用于验证用户身份。请求的配置(options
)包括设置请求方法为POST,设置请求头(包括内容类型和授权头部),以及将转换后的数据作为请求体。最后,函数使用 fetch
API发送请求,并处理任何可能发生的错误。
2、formData的请求方式
//formData的请求方式
export async function fetchAPIs (url, data) {
const token = localStorage.getItem("token");
// 发送POST请求
const options = {
method: "POST",
headers: {
Authorization: `Bearer ${token}`, // 添加JWT头部
},
body: data,
};
try {
let datas = null
const { status } =await fetch(url, options)
if (status !== 200) {
ElMessage.error('网络错误')
return
}
return fetch(url, options).then((response) => response.json())
.then((res) => {
if (res.code == 200) {
datas=res
}else if(res.code == 7001||res.code ==7005||res.code ==7001||res.code ==7002){
ElMessage.error('请重新登录')
// useUserStore.removeUser()
router.push('/login')
}else{
ElMessage.error(res.msg)
}
return datas;
})
} catch (error) {
console.error("请求失败: ", error);
}
}
//get、PUT、Post的请求方式
//get的请求方式
export async function fetchAPIGets (url) {
const token = localStorage.getItem("token");
// 发送GET请求
const options = {
method: "GET",
headers: {
// Authorization: `Bearer ${token}`, // 添加JWT头部
},
};
try {
let datas = null
const { status } =await fetch(url, options)
if (status !== 200) {
ElMessage.error('网络错误')
return
}
return fetch(url, options).then((response) => response.json())
.then((res) => {
if (res.code == 200) {
datas=res
}else if(res.code == 7001||res.code ==7005||res.code ==7001||res.code ==7002){
ElMessage.error('请重新登录')
// useUserStore.removeUser()
router.push('/login')
}else{
ElMessage.error(res.msg)
}
return datas;
})
// return await fetch(url, options);
} catch (error) {
console.error("请求失败: ", error);
}
}
//PUT的请求方式
export async function fetchAPIPuts (url, file) {
// 发送PUT请求
const options = {
method: 'PUT',
headers: {
'Content-Type': ''
},
body: file // 这里的file是你要上传的文件对象
};
try {
const res = await fetch(url, options)
return res
} catch (error) {
console.error("请求失败: ", error);
}
}
//Post的请求方式
export async function fetchPostAPI (url, data) {
const requestBody = JSON.stringify(data);
const token = localStorage.getItem("token");
// 发送POST请求
const options = {
method: "POST",
headers: {
Authorization: `Bearer ${token}`, // 添加JWT头部
},
body: requestBody,
};
try {
let datas = null
const { status } =await fetch(url, options)
if (status !== 200) {
ElMessage.error('网络错误')
return
}
return fetch(url, options).then((response) => response.json())
.then((res) => {
if (res.code == 200) {
datas=res
}else if(res.code == 7001||res.code ==7005||res.code ==7001||res.code ==7002){
ElMessage.error('请重新登录')
// useUserStore.removeUser()
router.push('/login')
}else{
ElMessage.error(res.msg)
}
return datas;
})
} catch (error) {
console.error("请求失败: ", error);
}
}
发送消息的代码:
import { fetchAPI, fetchPostAPI } from '@/utils/https.js'
import { getUUID } from '@/utils/index.js'
const messageList = ref([])
const currentSession = ref(getUUID())
//滚动到最底部
const scrollToBottom = () => {
nextTick(() => {
if (innerRef.value.clientHeight > 200) {
chatListContainer.value.setScrollTop(innerRef.value.clientHeight)
}
})
};
//获取问题提示
const getTipsList = (content) => {
let list = content.split(/\n/);
// 遍历数组
list.forEach((item, index) => {
console.log(item);
item = item.trim().replace(/^\d+\、/, '');
list[index] = item;
// 判断是否为空
if (item.trim() === "") {
// 删除数组中当前项
list.splice(index, 1);
}
});
// 返回列表最后3项
if (list.length > 3) {
return list.slice(-3)
}
return list
}
const appendTipsContent = (content) => {
tipsContent.value = content;
nextTick(() => scrollToBottom())
};
//获取问题提示 相关问题推荐
const requestChatTips = async (content) => {
let messages = content;
if (messages.length > 4) {
messages = messages.slice(-4);
}
const { body, status } = await fetchAPI('/api/gaa/chat/recommendedQuestions', { messages });
if (body) {
const reader = body.getReader();
readStream(reader, status,
(data) => {
appendTipsContent(data);
},
() => {
tipsList.value = getTipsList(tipsContent.value);
}
);
}
}
// 读取数据流
const readStream = async (reader, status, appendContent, callback) => {
let partialLine = "";
while (true) {
const { value, done } = await reader.read();
if (done) {
//延时500毫秒,防止数据流过快,导致数据丢失
await new Promise((resolve) => setTimeout(resolve, 0));
callback()
break;
}
const decodedText = decoder.decode(value, { stream: true });
if (status !== 200) {
// const json = JSON.parse(decodedText);
const json = decodedText; // start with "data: "
const content = json.error ? json.error : decodedText;
appendContent(content);
return;
}
partialLine = partialLine + decodedText;
appendContent(partialLine);
}
appendContent(partialLine);
};
//插入对话记录中
const appendLastMessageContent = (content) => {
messageList.value[messageList.value.length - 1].content = content;
input.value = ''
nextTick(() => scrollToBottom())
}
//对话接口
const aiMultiNoticeApi = async () => {
console.log(currentSession.value);
try {
const { body, status } = await fetchAPI('/api/gaa/chat/newChat', {
dialogueId: messagesObj.value ? messagesObj.value.dialougeId : currentSession.value,//对话id,新对话用currentSession.value,旧对话messagesObj.value.dialougeId
userId: userStore.user.userId, //用户id
messages: messageList.value,//对话记录
type: 0,//类型(可省略)
});
if (body) {
const reader = body.getReader();
messageList.value.push({ role: "assistant", content: "" });
readStream(reader, status,
(data) => {
appendLastMessageContent(data);
},
() => {
// 启动推荐问题的请求 相关问题推荐
requestChatTips(messageList.value)
getHistort()// 获取历史记录
});
}
} catch (error) {
console.log(error);
} finally {
//isTalking.value = false;
}
}
这段代码定义了一个名为 aiMultiNoticeApi
的异步函数,用于处理与聊天相关的API请求。函数首先输出当前会话的值,然后尝试发送一个POST请求,使用之前定义的 fetchAPI
函数。请求的URL是 /api/gaa/chat/newChat
,传递的数据包括对话ID(dialogueId
),用户ID(userId
),对话记录(messages
),以及类型(type
),其中类型是可选的。
如果请求成功并返回数据(body
),代码将使用 body.getReader()
读取响应流。随后,将一个空的助手角色消息添加到消息列表中,并处理读取的数据流。处理过程中,将调用 appendLastMessageContent
函数来追加内容,并在完成后调用 requestChatTips
和 getHistory
函数来获取相关的推荐问题和历史记录。
如果请求过程中出现错误,它会被捕获并输出到控制台。最后,代码包含一个 finally
块,可能用于更新聊天状态,但具体代码已被省略。
// 读取数据流
const readStream = async (reader, status, appendContent, callback) => {
let partialLine = "";
while (true) {
const { value, done } = await reader.read();
if (done) {
//延时500毫秒,防止数据流过快,导致数据丢失
await new Promise((resolve) => setTimeout(resolve, 0));
callback()
break;
}
const decodedText = decoder.decode(value, { stream: true });
if (status !== 200) {
// const json = JSON.parse(decodedText);
const json = decodedText; // start with "data: "
const content = json.error ? json.error : decodedText;
appendContent(content);
return;
}
partialLine = partialLine + decodedText;
appendContent(partialLine);
}
appendContent(partialLine);
};
定义了一个名为 readStream
的异步函数,用于处理数据流。函数接收四个参数:reader
(用于读取流的Reader对象),status
(HTTP响应状态码),appendContent
(一个函数,用于将解码后的文本追加到某个容器中),和callback
(处理完数据流后执行的回调函数)。
函数使用 while
循环不断读取 reader
中的数据。如果 done
为真,意味着数据已经读取完毕,此时执行 callback
函数并退出循环。如果 status
不是200,意味着请求出现问题,函数会处理错误信息并提前返回。
在正常情况下,函数会连续读取数据并通过 decoder.decode
将其解码,然后使用 appendContent
函数将解码后的文本追加处理。这个过程适用于处理大数据量或分块发送的响应,例如流式API响应。
一键复制功能:注意:要在带有https域名下才能使用navigator.clipboard.writeText否则不会复制出带有格式的文本。
//一键复制
const copy = (val) => {
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard 向剪贴板写文本
return navigator.clipboard.writeText(val)
.then(() => {
ElMessage.success('文本已复制到剪切板')
}).catch(() => {
ElMessage.warning('无法复制文本')
});
} else {
// 创建text area
const textArea = document.createElement("textarea");
textArea.value = JSON.stringify(val);
// 使text area不在viewport,同时设置不可见
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
new Promise((resolve, reject) => {
// 执行复制命令并移除文本框
document.execCommand("copy") ? resolve() : reject(new Error("出错了"));
textArea.remove();
}).then(
() => {
ElMessage.success('文本已复制到剪切板')
},
() => {
ElMessage.warning('无法复制文本')
}
);
}
}