技术栈 : react + js + ant - design (本人用的是react , vue也可以实现 , 在最后有重点方法的vue版)
实现效果如下 :
1. 我专门封装了一个fetch.js来处理流式数据 , 流式数据返回如下
import { FETCH_DIFY_API_REQUEST } from '@/utils/fetch.js'
// 流式内容返回
const postChatMessage = params => {
return FETCH_DIFY_API_REQUEST.post(`/chat-messages`, params, {
responseType: 'stream',
signal: params?.signal
})
}
export { postChatMessage }
-------- 以下为 @/utils/fetch.js 的内容 .env.VITE_DIFY_API_URL就需自己配置了 --------------
import Cookies from 'js-cookie'
class FetchService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
/**
* 发起 GET 请求
* @param {string} path - API 路径
* @param {object} [options] - 可选配置项
* @returns {Promise<Response>}
*/
get (path, params, options = {}) {
return this.Fetch(`${this.baseUrl}${path}`, params, { method: 'GET', ...options }
);
}
/**
* 发起 POST 请求
* @param {string} path - API 节点路径
* @param {object} [options] - 可选配置项
* @returns {Promise<Response>}
*/
post (path, params, options = {}) {
return this.Fetch(`${this.baseUrl}${path}`, params, { method: 'POST', ...options });
}
/**
* 发起 PUT 请求
* @param {string} path - API 路径
* @param {object} [options] - 可选配置项
* @returns {Promise<Response>}
*/
put (path, params, options = {}) {
return this.Fetch(`${this.baseUrl}${path}`, params, { method: 'PUT', ...options, });
}
/**
* 发起 DELETE 请求
* @param {string} path - API 路径
* @param {object} [options] - 可选配置项
* @returns {Promise<Response>}
*/
delete (path, params, options = {}) {
return this.Fetch(`${this.baseUrl}${path}`, params, { method: 'DELETE', ...options });
}
/**
* 发起通用的 fetch 请求
* @param {string} url - 请求 URL
* @param {object} [options] - 可选配置项
* @returns {Promise<Response>}
*/
async Fetch (url, params, options = {}) {
const response = await fetch(url,
{
body: JSON.stringify(params),
headers: {
Authorization: "Bearer " + Cookies.get('token'),
"Content-Type": "application/json",
},
...options,
}
);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response;
}
}
const FETCH_DIFY_API_REQUEST = new FetchService(import.meta.env.VITE_DIFY_API_URL);
export { FETCH_DIFY_API_REQUEST }
2. 导入接口和获取流式数据内容 方法如下
const [chatList, setChatList] = useState([])
const [conversationId, setConversationId] = useState('')
const [isStep, setIsStep] = useState(false)
const [isSelect, setIsSelect] = useState(false)
const [referInfo, setReferInfo] = useState({ referList: [] })
const [userId, setUserId] = useState('user-123') // 假设的用户ID
const [disabled, setDisabled] = useState(false)
const [loading, setLoading] = useState(false)
const [abortController, setAbortController] = useState(null)
// 点击发送的事件,该方法大同小异也可以转成适用于vue的方法
const sendHandle = async () => {
try {
if (!inputValue.trim()) return
setDisabled(true)
if (abortController) {
abortController.abort() // 如果已有请求,先取消它
}
// 创建一个新的 AbortController 实例
const newAbortController = new AbortController()
setAbortController(newAbortController)
if (chatList.some(item => item.loading)) {
message.warning('正在搜索,请稍等...')
return
}
const userObj = { role: 'user', loading: false, content: inputValue }
const aiObj = {
role: 'assistant',
loading: true,
content: '',
finished: false,
contenteditable: false
}
setChatList(prevChatList => [...prevChatList, userObj, aiObj])
setLoading(true) // 设置加载状态为 true
const refs =
referInfo.referList.map(item => item.content).join(';\n') || ''
const params = {
inputs: {},
query: isStep ? inputValue + '大纲' : inputValue,
refs: isSelect ? refs : '',
conversation_id: conversationId,
response_mode: 'streaming', // blocking, streaming
user: userId
}
const response = await postChatMessage({
...params,
signal: newAbortController.signal
})
setChatList(prevChatList => {
const newChatList = [...prevChatList]
newChatList[newChatList.length - 1].loading = false
return newChatList
})
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let result = true
let contenteditable = false
while (result) {
const { done, value } = await reader.read()
if (done) {
setDisabled(false)
setLoading(false) // 设置加载状态为 false
setChatList(prevChatList => {
const newChatList = [...prevChatList]
newChatList[newChatList.length - 1].finished = true
return newChatList
})
break
}
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (let line of lines) {
if (line.startsWith('data: ')) {
if (isJsonString(line.substring(6))) {
const jsonData = JSON.parse(line.substring(6))
if (jsonData.event === 'error') {
setLoading(false) // 设置加载状态为 false
setChatList(prevChatList => {
const newChatList = [...prevChatList]
newChatList[newChatList.length - 1].content =
'没有找到您想要的答案,请您稍后再试哦~~~'
newChatList[newChatList.length - 1].loading = false
newChatList[newChatList.length - 1].finished = true
return newChatList
})
return
}
if (jsonData.answer === 'outline') {
contenteditable = true
}
setConversationId(jsonData.conversation_id)
setChatList(prevChatList => {
const newChatList = [...prevChatList]
newChatList[newChatList.length - 1].contenteditable =
contenteditable
let content =
(jsonData.answer &&
jsonData.answer.replace(/\n+/g, '<br>')) ||
''
if (content.indexOf('outline') > -1) {
content = content.replace('outline', '')
}
newChatList[newChatList.length - 1].content += content
return newChatList
})
}
}
}
setInputValue('') // 清空输入框
}
} catch (error) {
setDisabled(false)
setLoading(false) // 设置加载状态为 false
message.error(error.message)
setChatList(prevChatList => {
const newChatList = [...prevChatList]
newChatList[newChatList.length - 1].content =
'没有找到您想要的答案,请您稍后再试哦~~~'
newChatList[newChatList.length - 1].loading = false
newChatList[newChatList.length - 1].finished = true
return newChatList
})
}
}
// 取消请求的事件
const handleCancelClick = () => {
if (abortController) {
abortController.abort() // 取消请求
setAbortController(null) // 清除控制器
}
setLoading(!loading)
}
2.1 以下是重点方法sendHandle的vue版本适用的方法
const sendHandle = async () => {
try {
if (!form.prompt) return;
disabled.value = true;
Cookies.set('token', chatToken);
const isLoading = chatList.value.some(item => item.loading);
if (isLoading) return proxy.$message.warning('正在搜索,请稍等...');
let userObj = { role: 'user', loading: false, content: form.prompt };
let aiObj = { role: 'assistant', loading: true, content: '', finished: false, contenteditable: false };
chatList.value.push(userObj, aiObj);
const refs = referInfo.value.referList.map(item => item.content).join(';\n') || '';
const params = {
inputs: {},
"query": isStep.value ? form.prompt + '大纲' : form.prompt,
"refs": isSelect.value ? refs : '',
"conversation_id": conversation_id.value,
"response_mode": "streaming",//blocking, streaming
"user": userId
}
const response = await postChatMessage(params);
chatList.value[chatList.value.length - 1].loading = false;
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let result = true;
let contenteditable = false;
while (result) {
const { done, value } = await reader.read();
if (done) {
result = false;
disabled.value = false;
chatList.value[chatList.value.length - 1].finished = true;
break;
}
const chunk = decoder.decode(value);
// 处理以 data: 开头的数据
const lines = chunk.split('\n');
for (let line of lines) {
if (line.startsWith('data: ')) {
if (isJsonString(line.substring(6))) {
const jsonData = JSON.parse(line.substring(6));
// console.log(jsonData);
if (jsonData.event == 'error') {
chatList.value[chatList.value.length - 1].content = '没有找到您想要的答案,请您稍后再试哦~~~';
chatList.value[chatList.value.length - 1].loading = false;
chatList.value[chatList.value.length - 1].finished = true;
return
}
if (jsonData.answer == "outline") {
contenteditable = true;
}
conversation_id.value = jsonData.conversation_id;
chatList.value[chatList.value.length - 1].contenteditable = contenteditable;
let content = jsonData.answer && jsonData.answer.replace(/\n+/g, '<br>') || '';
if (content.indexOf('outline') > -1) {
content = content.replace('outline', '')
}
// console.log(content);
chatList.value[chatList.value.length - 1].content += content;
}
}
}
}
} catch (error) {
// console.log(error);
disabled.value = false;
proxy.$message.error(error.message);
chatList.value[chatList.value.length - 1].content = '没有找到您想要的答案,请您稍后再试哦~~~';
chatList.value[chatList.value.length - 1].loading = false;
chatList.value[chatList.value.length - 1].finished = true;
}
}
总结 :
流式数据处理 + 地址配好token加上 + 重点方法 = 成功处理流式数据
优化项 :
中止请求 (文中有) + 样式的优化调整