gpt对话的流式输出

文章介绍了如何在后端使用fetchAPI发送POST、GET、PUT请求,并处理流式输出,确保前端能够接收到流式数据。同时提及了如何使用localStorage存储JWT令牌,以及处理数据流和前端剪贴板复制功能的实现。
摘要由CSDN通过智能技术生成

注意:后端和大模型返回的内容也要是流式输出的,不然前端做了流式输出也没用。

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请求。它接受两个参数:urldata。函数首先将传入的数据(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 函数来追加内容,并在完成后调用 requestChatTipsgetHistory 函数来获取相关的推荐问题和历史记录。

如果请求过程中出现错误,它会被捕获并输出到控制台。最后,代码包含一个 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('无法复制文本')
      }
    );
  }

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值