前端使用fetch-event-source实现AI对话
安装
npm install --save @microsoft/fetch-event-source
使用
import {fetchEventSource} from '@microsoft/fetch-event-source';data() {return {question: '',isLoading: false,currentIndex: '',thinkFlag: false,ctrlAbout: null,conversationId: null,answering: false}
},
methods: {sendQuestion(question) {if (this.isLoading || this.answering) return;if (window.EventSource) {this.question = '';this.currentIndex = this.dataList.length;this.isLoading = true;this.dataList.push({index: this.currentIndex,type: 'q',code: 200,info: question});this.scrollTop();let _that = this;let ix = 0;const params = {question: question, conversationId: this.conversationId};this.ctrlAbout = new AbortController();fetchEventSource(process.env.VUE_APP_BASE_API + '/ai/aiQuestion', {method: 'POST',headers: {[tokenName]: 'Bearer ' + getToken(),accept: 'application/json;charset=UTF-8,text/plain,*/*','Content-Type': 'application/json;charset=UTF-8', // 文本返回格式},body: JSON.stringify(params),signal: _that.ctrlAbout.signal,openWhenHidden: true, // 在浏览器标签页隐藏时保持与服务器的EventSource连接onopen(resp) {console.log('SSE连接成功...', resp)_that.isLoading = false;_that.thinkFlag = false;},onmessage(esm) {let esmData = esm.data;if (esmData && esmData !== '' && esmData !== 'event: ping') {// console.log('esmData', esmData)let respData = JSON.parse(esmData.substring(esm.data.indexOf(":") + 1, esmData.length));if (respData.event && respData.event === 'message') {if (ix < 1) {_that.answering = true;}if (!_that.answering) return;let respAnswer = respData.answer;console.log('respAnswer', respAnswer)if (respAnswer === '<think>') {_that.thinkFlag = true;respAnswer = '';} else if (respAnswer === '</think>') {_that.thinkFlag = false;respAnswer = '';}let isExist = _that.dataList.some(d => d.type === 'a' && d.index === _that.currentIndex);if (isExist) {_that.dataList.forEach(item => {if (item.type === 'a' && item.index === _that.currentIndex) {if (item.info.state === 0) {if (_that.thinkFlag) {item.info.text.think += respAnswer;} else {item.info.text.answer += respAnswer;}} else {if (_that.thinkFlag) {item.info = {state: 0,text: {think: respAnswer,answer: ''}}} else {item.info = {state: 0,text: {think: '',answer: respAnswer}}}}}})} else {if (_that.thinkFlag) {_that.dataList.push({index: _that.currentIndex,type: 'a',code: 200,info: {state: 0,showInfo: true,text: {think: respAnswer,answer: ''}}});} else {_that.dataList.push({index: _that.currentIndex,type: 'a',code: 200,info: {state: 0,showInfo: true,text: {think: '',answer: respAnswer}}});}}} else {_that.dataList.forEach(item => {if (item.type === 'a' && item.index === _that.currentIndex && item.info.state === 0) {item.info.state = 1;}})_that.answering = false;_that.ctrlAbout = null;_that.getHistorySession();}}_that.scrollTop();},onclose() {this.ctrlAbout = null;console.log('SSE已关闭...')},onerror(e) {console.log('SSE出现错误...', e)this.stopAnswer();this.$message.error('系统错误,请联系管理员进行处理!');throw e;}});} else {this.$message.warning('您的浏览器不支持SSE!');}},eventSourceClose() {if (this.ctrlAbout) {this.ctrlAbout.abort();this.ctrlAbout = null;aiQuestionByClose(this.taskId).then(resp => {console.info('SSE已中断...')})}},stopAnswer() {this.answering = false;this.eventSourceClose();this.dataList.forEach(item => {if (item.type === 'a' && item.index === this.currentIndex && item.info.state === 0) {item.info.state = 2;}});this.scrollTop();this.getHistorySession();},scrollTop() {this.$nextTick(() => {if (this.$refs.kqaWindow.scrollHeight) {this.$refs.kqaWindow.scrollTo({top: this.$refs.kqaWindow.scrollHeight,behavior: 'smooth'});}});},eventSourceClose() {if (this.ctrlAbout) {this.ctrlAbout.abort();this.ctrlAbout = null;aiQuestionByClose(this.taskId).then(resp => {console.info('SSE已中断...')})}},
}
参考文档
https://juejin.cn/post/7518783423273664547
fetchEventSource使用+源码解析 - 漫思 - 博客园
https://juejin.cn/post/7411047482652262415