feat(components): 实现 chatGPT 流式接口

feat(components): 实现 chatGPT 流式接口
This commit is contained in:
ronger 2023-05-27 06:54:41 +08:00 committed by GitHub
commit 0a205ca1f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 18 deletions

View File

@ -63,7 +63,7 @@ export default {
email: 'mailto:support@rymcu.com', email: 'mailto:support@rymcu.com',
aboutMe: '/article/115', aboutMe: '/article/115',
github: 'https://github.com/rymcu', github: 'https://github.com/rymcu',
gitee: 'https://gitee.com/rymcu-community', gitee: 'https://gitee.com/rymcu',
currencyRule: '/rules/currency', currencyRule: '/rules/currency',
wangBeiUrl: 'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=36012302000055', wangBeiUrl: 'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=36012302000055',
wangBei: '赣公网安备 36012302000055号', wangBei: '赣公网安备 36012302000055号',

View File

@ -57,7 +57,8 @@ export default {
{src: '~/plugins/extend'}, {src: '~/plugins/extend'},
{src: '~/plugins/axios'}, {src: '~/plugins/axios'},
{src: '~/plugins/element-ui'}, {src: '~/plugins/element-ui'},
{src: '~/plugins/vditor', ssr: false} {src: '~/plugins/vditor', ssr: false},
{src: '~/plugins/vue-sse'}
// {src: '~/plugins/vue-cropper', ssr: false} // {src: '~/plugins/vue-cropper', ssr: false}
], ],
/* /*
@ -106,7 +107,8 @@ export default {
}, },
autoLogout: false autoLogout: false
} }
} },
plugins: [{src: '~/plugins/axios', ssr: true}]
}, },
axios: { axios: {
proxy: true // 开启proxy proxy: true // 开启proxy

View File

@ -17,22 +17,19 @@
"@nuxtjs/auth-next": "^5.0.0-1667386184.dfbbb54", "@nuxtjs/auth-next": "^5.0.0-1667386184.dfbbb54",
"@nuxtjs/axios": "^5.13.1", "@nuxtjs/axios": "^5.13.1",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",
"core-js": "^2.6.12",
"defu": "^5.0.1",
"echarts": "^4.9.0", "echarts": "^4.9.0",
"element-ui": "^2.15.12", "element-ui": "^2.15.12",
"express": "^4.18.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"net": "^1.0.2",
"nuxt": "^2.15.8", "nuxt": "^2.15.8",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"save-svg-as-png": "^1.4.17", "save-svg-as-png": "^1.4.17",
"simple-icons": "^6.23.0", "simple-icons": "^6.23.0",
"sockjs-client": "^1.6.1",
"stompjs": "^2.3.3",
"vditor": "^3.8.18", "vditor": "^3.8.18",
"vue-cropperjs": "^4.2.0", "vue-cropperjs": "^4.2.0",
"vuejs-avataaars": "^4.0.1", "vue-sse": "^2.5.2",
"core-js": "^2.6.12", "vuejs-avataaars": "^4.0.1"
"defu": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.0", "@commitlint/cli": "^17.4.0",

View File

@ -8,6 +8,16 @@
<el-button type="primary" :loading="loading" @click="send" plain>发送</el-button> <el-button type="primary" :loading="loading" @click="send" plain>发送</el-button>
</el-col> </el-col>
<el-col style="margin-top: 2rem;" id="messagesContent"> <el-col style="margin-top: 2rem;" id="messagesContent">
<el-col v-if="message">
<el-col :span="2">
<el-avatar :src="to.avatarUrl"></el-avatar>
</el-col>
<el-col :span="22" style="text-align: left;">
<div class="to-message">
<div v-html="message"></div>
</div>
</el-col>
</el-col>
<el-col v-for="message in messages" :key="message.dataId"> <el-col v-for="message in messages" :key="message.dataId">
<el-col v-if="message.from === user.account"> <el-col v-if="message.from === user.account">
<el-col :span="22" style="text-align: right;"> <el-col :span="22" style="text-align: right;">
@ -39,6 +49,7 @@
import Vue from 'vue'; import Vue from 'vue';
import {mapState} from 'vuex'; import {mapState} from 'vuex';
import apiConfig from '~/config/api.config'; import apiConfig from '~/config/api.config';
import 'vditor/dist/css/content-theme/light.css';
export default { export default {
name: "Chat", name: "Chat",
@ -61,7 +72,10 @@ export default {
isShow: true, isShow: true,
loading: false, loading: false,
to: {}, to: {},
messages: [] messages: [],
vueSse: null,
customEvents: null,
message: ''
} }
}, },
watch: { watch: {
@ -159,6 +173,7 @@ export default {
}, },
async send() { async send() {
let _ts = this; let _ts = this;
_ts.message = '';
const message = { const message = {
to: _ts.to.account, to: _ts.to.account,
from: _ts.user.account, from: _ts.user.account,
@ -173,18 +188,59 @@ export default {
_ts.contentEditor.setValue('') _ts.contentEditor.setValue('')
_ts.$axios.$post('/api/openai/chat', { _ts.$axios.$post('/api/openai/chat', {
message: message.content message: message.content
}).then(res => { }).then(async res => {
const html = await Vue.Vditor.md2html(_ts.message);
_ts.messages.push({ _ts.messages.push({
to: _ts.user.account, to: _ts.user.account,
from: _ts.to.account, from: _ts.to.account,
dataType: 1, dataType: 1,
dataId: new Date().getTime(), dataId: new Date().getTime(),
content: res[0].message.content content: html
}); });
_ts.messages.sort((a, b) => { _ts.messages.sort((a, b) => {
return b.dataId - a.dataId; return b.dataId - a.dataId;
}); });
_ts.message = '';
}); });
},
close() {
let _ts = this;
//SSE
_ts.vueSse.disconnect();
_ts.$axios.$get(`/api/sse/close/${_ts.user.idUser}`);
},
init() {
// vue-sse
let _ts = this;
let vueSse = _ts.vueSse;
// message
vueSse.on('message', this.handleMessage);
// customEvents
vueSse.once('customEvents', this.handleCustomEvents);
// ononceoff
// once
//PR
// connect Promise
vueSse
.connect()
.then((sse) => {
console.log("We're connected!", sse);
})
.catch((err) => console.error('Failed make initial connection:', err));
},
//message
handleMessage(res) {
if (typeof res !== "undefined") {
this.message += res;
}
},
//handleCustomEvents
handleCustomEvents(res) {
console.log('customEvents22:', res)
let { data } = res;
console.info('customEvents:', data);
this.customEvents = data;
} }
}, },
async mounted() { async mounted() {
@ -215,6 +271,12 @@ export default {
content: '伟大的"坦格利安家族的风暴降生丹妮莉丝 · 铁王座的合法继承人 · 安达尔人和先民的合法女王 · 七国的守护者 · 草海上的卡丽熙 · 不焚者 · 解放者 · 傲之追猎者 · 悠米"为你服务' content: '伟大的"坦格利安家族的风暴降生丹妮莉丝 · 铁王座的合法继承人 · 安达尔人和先民的合法女王 · 七国的守护者 · 草海上的卡丽熙 · 不焚者 · 解放者 · 傲之追猎者 · 悠米"为你服务'
} }
_ts.messages.push(message); _ts.messages.push(message);
_ts.vueSse = _ts.$sse.create({
url: `/api/sse/subscribe/${_ts.user.idUser}`,
format: 'json',
withCredentials: true
});
this.init();
} }
if (!_ts.initEditor) { if (!_ts.initEditor) {
@ -230,6 +292,14 @@ export default {
}); });
}, 500); }, 500);
} }
},
sse: {
//
// beforeDestroy
cleanup: true,
},
beforeDestroy() {
this.close();
} }
} }
</script> </script>

15
plugins/vue-sse.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue'
import VueSSE from 'vue-sse';
// using defaults
Vue.use(VueSSE);
// OR specify custom defaults (described below)
Vue.use(VueSSE, {
format: 'json', //数据格式
url: '/', //路径
withCredentials: true, //标识为open // withCredentials should be set after "open" for Safari and Chrome (< 19 ?)
forcePolyfill:false, //强制使用原生SSE使用另一个库event-source-polyfill
polyfill: true, //支持旧版浏览器
polyfillOptions:null, //配置参数
});