feat(components): 实现 chatGPT 流式接口
feat(components): 实现 chatGPT 流式接口
This commit is contained in:
commit
0a205ca1f4
@ -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号',
|
||||||
|
@ -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}
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
@ -99,14 +100,15 @@ export default {
|
|||||||
autoFetch: false
|
autoFetch: false
|
||||||
},
|
},
|
||||||
endpoints: {
|
endpoints: {
|
||||||
login: { url: '/api/auth/login', method: 'post' },
|
login: {url: '/api/auth/login', method: 'post'},
|
||||||
logout: { url: '/api/auth/logout', method: 'post' },
|
logout: {url: '/api/auth/logout', method: 'post'},
|
||||||
refresh: { url: '/api/auth/refresh-token', method: 'post' },
|
refresh: {url: '/api/auth/refresh-token', method: 'post'},
|
||||||
user: { url: '/api/auth/user', method: 'get' }
|
user: {url: '/api/auth/user', method: 'get'}
|
||||||
},
|
},
|
||||||
autoLogout: false
|
autoLogout: false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
plugins: [{src: '~/plugins/axios', ssr: true}]
|
||||||
},
|
},
|
||||||
axios: {
|
axios: {
|
||||||
proxy: true // 开启proxy
|
proxy: true // 开启proxy
|
||||||
@ -139,7 +141,7 @@ export default {
|
|||||||
config.module.rules[2].use[0].options.plugins = ['lodash']
|
config.module.rules[2].use[0].options.plugins = ['lodash']
|
||||||
},
|
},
|
||||||
babel: {
|
babel: {
|
||||||
presets({ envName }) {
|
presets({envName}) {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'@nuxt/babel-preset-app',
|
'@nuxt/babel-preset-app',
|
||||||
|
11
package.json
11
package.json
@ -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",
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
//里面的 on、once、off 是用了发布订阅模式,
|
||||||
|
//源码 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
15
plugins/vue-sse.js
Normal 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, //配置参数
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user