使用@microsoft/fetch-event-source发送post请求接收stream流数据实现chatGPT流式输出

使用@microsoft/fetch-event-source发送post请求接收stream流数据实现chatGPT流式输出

效果:


image.png

需求:实现一个类似于chatGPT回复功能和文字打印机效果,经过查询可以使用EventSource。
接口返回:


image.png
1.使用EventSource

官网地址:https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource
但是原生的EventSource 不能使用post方法,只能使用get方法,而且还不能自定义请求header,所以我们可以使用以下两种方式:

  1. event-source-polyfill 这个可以自定义请求头
    使用[event-source-polyfill可以参考前端怎么用 EventSource并配置请求头及加参数(流式数据)
  2. @microsoft/fetch-event-source 这个可以使用post请求,也可以自定义请求头功能强大,建议用这个
2.使用 @microsoft/fetch-event-source

// 下载依赖

npm i @microsoft/fetch-event-source

// 页面引入

import { fetchEventSource } from "@microsoft/fetch-event-source";

// 使用

async handleSearch() {
      const that = this
      that.ctrlAbout = new AbortController()
      // const outputDiv = document.getElementById('gpt-output')
      let innerText = ''
      // 发送 SSE 请求
      fetchEventSource('http://222.71.38.xxx:xxxx/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        signal: that.ctrlAbout.signal,
        body: JSON.stringify({
          model: 'o_prgpt',
          temperature: 0.9,
          presence_penalty: 0.1,
          max_tokens: 8192,
          messages: that.msgList,
          stream: true
        }),
        onmessage(ev) {
          // debugger
          // console.log(ev);
          if (ev.data === '[DONE]') {
            that.ctrlAbout.abort()
            return
          }
          // console.log(JSON.parse(ev.data));
          const res = JSON.parse(ev.data)
          // ev.data = JSON.parse(ev.data)
          // 实时接收数据,并将其添加到页面
          // outputDiv.innerText += res.choices[0].delta.content

          // // for (let i = 0; i < res.choices.length; i++) {
          innerText += res.choices[0].delta.content
          console.log('innerText', innerText)
          if (res.choices[0].delta.content) {
            // // }
            if (that.msgList.at(-1).role === 'assistant') {
              that.msgList.pop()
            }
            that.msgList.push({
              role: 'assistant',
              content: innerText
            })
            that.scrollToNew()
          }
        },
        onclose() {
          console.log('连接关闭')
        },
        onerror(err) {
          console.error('连接错误', err)
        }
      })
    }
3.全部代码
<template>
  <!-- <Header /> -->
  <div
    class="body"
    style="background-color: rgb(244, 245, 248)"
  >
    <header>
      <div class="cover">
        <!-- <img src="1.png" alt="" style="width: 100%; height: 100%" /> -->
      </div>
    </header>
    <main>
      <div class="container">
        <div class="right">
          <div class="top">AI问答</div>
          <div
            ref="chatContainer"
            class="chat"
          >
            <div
              v-for="(item, i) in msgList"
              :key="i"
              :class="item.role == 'user' ? 'rightMsg' : 'leftMsg'"
            >
              <img
                v-if="item.role == 'assistant'"
                src="../../assets/chat/AI.png"
                alt=""
              >
              <!-- <div
                id="gpt-output"
                class="msg"
              >{{ item.content }}</div> -->
              <div
                class="msg"
                v-html="item.content"
              />
              <div
                v-if="item.role == 'assistant'"
                class="caozuo"
              >
                <img
                  class="zuo"
                  src="../../assets/chat/updata.png"
                  alt=""
                >
                <img
                  class="zuo"
                  src="../../assets/chat/yuyin.png"
                  alt=""
                >
              </div>
              <img
                v-if="item.role == 'user'"
                src="../../assets/chat/me.png"
                alt=""
              >
            </div>
            <!--
            <div v-if="msgList.length >= 10" class="separator">
              -------------- 本AI问答仅显示最近10条对话 --------------
            </div> -->
          </div>
          <div class="bottom">
            <i
              class="el-icon-microphone"
              style="font-size: 22px;color: #000"
            />
            <el-input
              v-model="value"
              class="input"
              placeholder="请输入您想提问的内容"
            >
              <!-- <input
              v-model="value"
              placeholder="请输入您想提问的内容"
            > -->

            </el-input>
            <el-button @click="onSend">
              <!-- <img src="../assets/images/send.png" alt="发送" /> -->
              <i
                class="el-icon-s-promotion"
                style="font-size: 30px; color: #fff"
              />
            </el-button>
          </div>
        </div>
      </div>
    </main>
  </div>
  <!-- <foot /> -->
</template>

<script>
// import { completions } from '@/api/gpt'
import { fetchEventSource } from '@microsoft/fetch-event-source'
export default {
  name: 'Chat',
  // 局部注册的组件
  components: {},
  // 组件参数 接收来自父组件的数据
  props: {},
  data() {
    return {
      value: '',
      msgList: []
    }
  },
  computed: {},
  watch: {},
  created() { },
  mounted() { },
  methods: {
    // 等待DOM更新完成。自动滚动到最新发送的消息处
    scrollToNew() {
      this.$nextTick(() => {
        const chatContainer = document.querySelector('.chat')
        if (chatContainer) {
          chatContainer.scrollTop = chatContainer.scrollHeight
        }
      })
    },
    onSend() {
      // 发送用户输入的消息
      this.msgList.push({
        role: 'user',
        content: this.value
      })
      this.value = ''
      this.handleSearch()
    },
    async handleSearch() {
      const that = this
      that.ctrlAbout = new AbortController()
      // const outputDiv = document.getElementById('gpt-output')
      let innerText = ''
      // 发送 SSE 请求
      fetchEventSource('http://222.71.38.106:7008/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        signal: that.ctrlAbout.signal,
        body: JSON.stringify({
          model: 'o_prgpt',
          temperature: 0.9,
          presence_penalty: 0.1,
          max_tokens: 8192,
          messages: that.msgList,
          stream: true
        }),
        onmessage(ev) {
          // debugger
          // console.log(ev);
          if (ev.data === '[DONE]') {
            that.ctrlAbout.abort()
            return
          }
          // console.log(JSON.parse(ev.data));
          const res = JSON.parse(ev.data)
          // ev.data = JSON.parse(ev.data)
          // 实时接收数据,并将其添加到页面
          // outputDiv.innerText += res.choices[0].delta.content

          // // for (let i = 0; i < res.choices.length; i++) {
          innerText += res.choices[0].delta.content
          console.log('innerText', innerText)
          if (res.choices[0].delta.content) {
            // // }
            if (that.msgList.at(-1).role === 'assistant') {
              that.msgList.pop()
            }
            that.msgList.push({
              role: 'assistant',
              content: innerText
            })
            that.scrollToNew()
          }
        },
        onclose() {
          console.log('连接关闭')
        },
        onerror(err) {
          console.error('连接错误', err)
        }
      })
    }
  }
}
</script>
<style lang="scss">
.body {
  color: #fff;
  font-weight: 900;
  letter-spacing: 2px;
  width: 95%;
  height: 100%;
  background-size: 50%;
  display: flex;
  align-items: center;
  position: relative;
}
main {
  /* border: 1px solid red; */
  width: 100%;
  // width: 1200px;
  height: 720px;
  // margin: 100px auto;
  display: flex;
}
.cover {
  // position: absolute;
  // top: 0px;
  // z-index: 0;
  // height: 180px;
  // width: 1483px;
  // left: 50%;
  // margin-left: -754px;
  // overflow: hidden;
}
.body {
  :deep(.slick-slide) {
    text-align: center;
    height: 100%;
    line-height: 100%;
    background: #364d79;
    overflow: hidden;
  }

  :deep(.slick-arrow.custom-slick-arrow) {
    width: 25px;
    height: 25px;
    font-size: 25px;
    color: #fff;
    background-color: rgba(31, 45, 61, 0.11);
    transition: ease all 0.3s;
    opacity: 0.3;
    z-index: 1;
  }

  :deep(.slick-arrow.custom-slick-arrow:before) {
    display: none;
  }

  :deep(.slick-arrow.custom-slick-arrow:hover) {
    color: #fff;
    opacity: 0.5;
  }

  :deep(.slick-slide h3) {
    color: #fff;
  }
}

.container {
  z-index: 1;
  // border: solid 1px #bebebe;
  width: 100%;
  height: 100%;
  margin: -6px auto;
  display: flex;
  justify-content: center;

  .right {
    flex: 1;
    // border-radius: 10px;
    background-color: white;
    display: flex;
    flex-direction: column;
    height: 720px;
    .top {
      height: 70px;
      background-color: rgba(147, 213, 255, 0.764);
      width: 100%;
      font-size: 22px;
      text-align: center;
      line-height: 70px;
    }

    .chat {
      flex: 1;
      max-height: 580px;
      overflow-y: auto;
      padding: 10px;
      .leftMsg,
      .rightMsg {
        position: relative;
        display: flex;
        flex-direction: row;
        justify-content: start;
        align-items: center;
        margin: 10px;

        img {
          width: 40px;
          height: 40px;
          border-radius: 20px;
          overflow: hidden;
          object-fit: cover;
          margin: 0 10px;
        }

        .msg {
          display: inline-block;
          padding: 10px;
          word-wrap: anywhere;
          max-width: 75%;
          background-color: #364d79;
          border-radius: 10px;
        }
      }

      .rightMsg {
        justify-content: end;

        .msg {
          color: black;
          background-color: #dfdfdf;
        }
      }
    }

    .bottom {
      position: relative;
      height: 45px;
      display: flex;
      align-items: center;
      width: 80%;
      margin: 10px auto;
      // position: absolute;
      // bottom: 0px;
      // left: 10px;
      input {
        width: 90%;
        border: 1px solid rgb(171, 171, 171);
        border-right: none;
        height: 40px;
        color: black;
        text-indent: 2px;
        line-height: 40px;
        border-radius: 10px 0 0 10px;
      }

      button {
        cursor: pointer;
        width: 10%;
        border: none;
        outline: none;
        height: 45px;
        border-radius: 0 10px 10px 0;
        background: linear-gradient(
          to right,
          rgb(146, 197, 255),
          rgb(200, 134, 200)
        );
      }
      img {
        width: 20px;
        height: 20px;
      }
    }
  }
}
.separator {
  color: rgb(133, 132, 132);
  text-align: center;
  font-size: 15px;
  font-weight: normal;
}
.el-icon-microphone {
  position: absolute;
  top: 12px;
  left: 7px;
  z-index: 99;
  cursor: pointer;
}
.input {
  height: 43px;
}
:deep(.el-input__inner) {
  height: 43px !important;
  line-height: 43px;
}
.container .right .bottom input {
  width: 100%;
  height: 43px;
  padding-left: 30px;
}
.caozuo {
  position: absolute;
  left: 75px;
  top: -33px;
  // .zuo {
  img {
    width: 30px !important;
    height: 30px !important;
    border: none;
    border-radius: 0 !important;
    margin: 0 5px !important;
  }
  // }
}
table {
  width: 100%;
  border-collapse: collapse;
}
table,
th,
td {
  border: 1px solid black;
}
th,
td {
  padding: 8px;
  text-align: left;
}
</style>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容