import { getSseConfigV1, getCompletionsV2, deleteChatItemByConversationId, // getAnalyzeKeywordsV1, getAnalyzeKeywordsChangeV1 } from '@/api/chat/index' import { VUE_APP_API_URL } from '@/config' import settings from '@/config/baseSetting.js' import { formatAiMsgContent } from '../utils/aiChat' import cookie from '@/utils/store/cookie' import { handleLoginFailure } from '@/utils' const plugin = requirePlugin('WechatSI') const removeMarkdown = require('remove-markdown') export const chatMixins = { data() { return { token: cookie.get('login_status'), userInfo: cookie.get('userInfo'), webUrl: this.$VUE_APP_RESOURCES_URL, bottomTools: Object.freeze([ { icon: 'icon-50', text: '我要送礼', type: 'prompt', prompt: '我要送礼,请给我推荐一些平台的礼品', url: '' }, { icon: 'icon-51', text: '订单查询', type: 'prompt', prompt: '订单查询', url: '' }, { icon: 'icon-52', text: '拍照识图', type: 'link', prompt: '', url: '/aiChat/views/imageRecognition' } ]), scrollData: {}, scrollTop: 0, isScrollToBottom: false, // 状态栏高度 statusBarHeight: 20, fileHttpStr: Object.freeze(VUE_APP_API_URL + settings.sysPrefix), keyboardHeight: 0, optionsFrom: '', onFocus: false, loading: false, chatNumber: '', infoResData: {}, chatMessageList: [ /* { type: -2, isError: false, showCursor: false, conversationId: '', prompt: '', nodes: '-------------监听 WebSocket 接受到服务器的消息事件', content: '-------------监听 WebSocket 接受到服务器的消息事件', listQuestion: [ '监听 WebSocket 接受到服务器的消息事件', '监听 WebSocket 接受到服务器的消息事件', '监听 WebSocket 接受到服务器的消息事件' ] } */ ], errorMsg: Object.freeze('您提的问题未能理解,云灵思想跑偏啦,重新整理一下问题或者重新提问,让云灵再试试!'), audioAyy: [], isPlayAudio: false, audioContext: null, audioPlayId: '' } }, computed: { chatMessageListLength() { return this.chatMessageList.length } }, onPageScroll(e) { this.scrollData = e const scrollTop = e.scrollTop this.scrollTop = scrollTop const query = wx.createSelectorQuery().in(this) query.select('#fixTbabarBody').boundingClientRect(function(rect) { const contentHeight = rect.height const windowInfo = wx.getWindowInfo() const windowHeight = windowInfo.windowHeight if (scrollTop + windowHeight + 30 >= contentHeight) { this.isScrollToBottom = true } else { this.isScrollToBottom = false } }.bind(this)).exec() }, onUnload() { this.audioStopHandle() this.closeWsFn() }, created() { const _this = this uni.getSystemInfo({ success: (e) => { let statusBar = 0 // #ifdef MP-WEIXIN statusBar = e.statusBarHeight const custom = uni.getMenuButtonBoundingClientRect() _this.rightDistance = e.windowWidth - custom.left + 10 // #endif // 状态栏高度 _this.statusBarHeight = statusBar } }) _this.createdCallbak() }, methods: { createdCallbak() { console.log('页面初始话回调') }, inputFocusHandle(value) { this.onFocus = value === 1 this.scrollToBottomHandle() }, historyViewHandle() { console.log(this.$refs.historyView) this.$refs.historyView.showView() }, copyContentHandle(data) { uni.setClipboardData({ data, success: () => { uni.showToast({ title: '已复制' }) } }) }, copyMoreInfoHandle(item) { uni.setClipboardData({ data: JSON.stringify(item.showMoreInfo), success: () => { uni.showToast({ title: '已复制' }) } }) }, shareHandle() { wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'], success: (res) => { console.log(res) }, fail: err => { console.log(err) } }) }, scrollToBottomHandle() { uni.pageScrollTo({ duration: 100, selector: '#bottom_postion' }) this.isScrollToBottom = true }, // 重新生成 regenerateHandle(item) { this.audioStopHandle() deleteChatItemByConversationId(item.conversationId).then(res => { const { success } = res if (success) { this.chatMessageList.splice(this.chatMessageListLength - 1, 1) this.streamReq({ prompt: item.prompt, init: 1 }) } }) }, // 建议提问 suggestionItemClickHandle(question) { this.streamReq({ prompt: question, init: 0 }) }, // 大家都在问 topicItemClickHandle(question) { this.streamReq({ prompt: question, init: 0 }) }, streamReq(params) { const _this = this // 需要校验登录状态 if (!_this.token) { handleLoginFailure() return } const prompt = params.prompt if (params.init !== 1) { _this.chatMessageList.push({ prompt, type: -1 }) } _this.$nextTick(() => { _this.scrollToBottomHandle() }) _this.chatMessageList.push({ content: '', nodes: '', isError: false, showCursor: false, conversationId: '', prompt, listQuestion: [], type: -2, showMoreInfo: {} }) _this.loading = true _this.showCursor = false getSseConfigV1({ 'chatNumber': _this.chatNumber, prompt }).then(resConfig => { const conversationId = resConfig.data.conversationId _this.chatNumber = resConfig.data.chatNumber || '' _this.chatMessageList[_this.chatMessageListLength - 1].showCursor = true _this.chatMessageList[_this.chatMessageListLength - 1].conversationId = conversationId if (conversationId) { _this.initWebSocket(conversationId, () => { getCompletionsV2({ conversationId, ws: true, isNoNeedPublicErrorNotification: 1 }).then(sseRes => { console.log(sseRes) // _this.closeWsFn() }).catch(err => { console.log(err) _this.chatErrorSetContent() }) }) /* getAnalyzeKeywordsV1({ 'chatNumber': _this.chatNumber, conversationId, prompt }).then(keywordRes => { if (keywordRes.success) { const keywordResData = keywordRes.data const show = (keywordResData.searchArtworks || keywordResData.searchDiscount || keywordResData.searchGoods || keywordResData.searchLandmark || keywordResData.searchTowns || keywordResData.searchTravelGuide ) && (keywordResData.resultItems && keywordResData.resultItems.length > 0) _this.chatMessageList[_this.chatMessageListLength - 1].showMoreInfo = keywordResData _this.chatMessageList[_this.chatMessageListLength - 1].showMoreInfo.show = show // 展示类型 _this.chatMessageList[_this.chatMessageListLength - 1].showMoreInfo.showType = '' if (show && keywordResData.resultItems) { keywordResData.resultItems.map(resItem => { _this.chatMessageList[_this.chatMessageListLength - 1].showMoreInfo.showType = resItem.type }) } console.log(_this.chatMessageList[_this.chatMessageListLength - 1]) } }) */ } else { _this.chatErrorSetContent() } }).catch(err => { console.log(err) _this.chatErrorSetContent() }) }, analyzeKeywordsChangeHandle(conversationId, showMoreInfo) { getAnalyzeKeywordsChangeV1({ conversationId, ...showMoreInfo }).then(res => { const { success, data } = res if (success) { let resultItems = [] let idx = -1 this.chatMessageList.map((item, index) => { if (item.conversationId === conversationId) { resultItems = data.resultItems || [] idx = index } }) if (idx > -1) { this.$set(this.chatMessageList[idx].showMoreInfo, 'resultItems', resultItems) this.$forceUpdate() } } }) }, productItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pages/shop/GoodsCon/index', query: { id: item.id } }) }, hotelItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pagesInn/inn/innHome', query: { id: item.id } }) }, ichItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pkg_product/views/heritage/details', query: { id: item.id } }) }, qianxianItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pkg_product/views/famousQianxianShop', query: { id: item.id } }) }, prizeItemClickHandle() { this.audioStopHandle() this.$yrouter.push({ path: '/pkg-video/views/lottery' }) }, chatErrorSetContent() { this.chatMessageList[this.chatMessageListLength - 1].isError = true this.chatMessageList[this.chatMessageListLength - 1].content = this.errorMsg this.chatMessageList[this.chatMessageListLength - 1].nodes = this.errorMsg this.closeWsFn() }, closeWsFn() { this.loading = false if (this.chatMessageListLength) { this.chatMessageList[this.chatMessageListLength - 1].showCursor = false } uni.closeSocket({ success: () => { console.log('-----关闭连接') } }) this.$nextTick(() => { this.scrollToBottomHandle() }) }, initWebSocket(conversationId, cb) { const _this = this console.log('------------------创建连接') uni.connectSocket({ url: VUE_APP_API_URL.replace('https', 'wss') + settings.sysPrefix + `v1/chat/websocket/${_this.userInfo.unionId}-${conversationId}`, header: { 'content-type': 'application/json' }, timeout: 10 * 1000, success: () => { console.log('------------WebSocket初始化成功') }, fail: (err) => { uni.showModal({ showCancel: false, content: err }) } }) uni.onSocketMessage(res => { _this.websocketOnmessage(res) }) uni.onSocketOpen(() => { cb && cb() _this.websocketOnopen() }) uni.onSocketError(err => { _this.websocketOnerror(err) }) }, websocketOnmessage(res) { console.log('-------------监听 WebSocket 接受到服务器的消息事件') const resData = JSON.parse(res.data) if (resData) { const message = resData.message if (message && this.chatMessageListLength > 1) { const mesContent = message.content this.chatMessageList[this.chatMessageListLength - 1].nodes = formatAiMsgContent(mesContent) this.chatMessageList[this.chatMessageListLength - 1].content = mesContent this.chatMessageList[this.chatMessageListLength - 1].listQuestion = message.listQuestion || [] this.$forceUpdate() this.$nextTick(() => { this.scrollToBottomHandle() if (message.finish) { this.closeWsFn() } }) } } }, websocketOnopen() { console.log('------------------监听 WebSocket 连接打开事件') // this.websocketSend(JSON.stringify({ msg: '1' })) }, websocketOnerror(e) { console.log('-----------------监听 WebSocket 错误事件') console.log(e) }, websocketSend(msg) { // 数据发送 try { uni.sendSocketMessage({ data: msg, success: () => { console.log('-----------发送消息成功了') } }) } catch (err) { console.log('send failed (' + err.code + ')') } }, audioStopHandle() { // Fix bug#4049 this.audioAyy = [] if (this.audioContext) { this.audioContext.stop() this.audioContext.destroy() // 强制销毁释放资源 this.audioContext = null } this.isPlayAudio = false // 强制关闭微信小程序背景音频 if (typeof wx !== 'undefined' && wx.getBackgroundAudioManager) { wx.getBackgroundAudioManager().stop() } }, ttsHandle(item) { this.audioAyy = [] if (this.audioContext) { this.audioContext.stop() // this.audioContext.destroy() } // 如果正在播放,点击当前播放的则认为是暂停 if (this.isPlayAudio && item.conversationId === this.audioPlayId) { this.isPlayAudio = false this.audioPlayId = '' return } this.isPlayAudio = false this.audioPlayId = item.conversationId const flag = 1 if (flag === 1) { console.log(removeMarkdown(item.content).replace(/[\n\t\s]/g, '')) uni.showLoading({ title: '合成中...' }) this.splitAndSynthesize(removeMarkdown(item.content).replace(/[\n\t\s]/g, '')) } else { // 调试数据 this.audioAyy = [ 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749075_2f71887cc48d9b3753c39d119862d4bb&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749077_0970508786c5e3edc3a68a5eb9de048c&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749079_e77cec52cc077ba981892887c3df4cdf&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749080_02db4235771f5561cdb73c06d8c67e53&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749082_534e455c5d6ac5e9a4192ecf9952851a&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749083_78c4a2fecad538561f81c48dc4ecd645&filekey=871027724&source=miniapp_plugin' ] this.playNextChunk() } }, splitAndSynthesize(text) { const _this = this if (text) { const textLength = text.length const chunkSize = 100 const chunk = text.slice(0, textLength > chunkSize ? chunkSize : textLength) plugin.textToSpeech({ // 语言 lang: 'zh_CN', tts: true, // 要转换的文字 content: chunk, success: function(res) { console.log("语音文件路径:", res.filename) _this.audioAyy.push(res.filename) // 首次就立即播放,增强体验 if (!_this.isPlayAudio) { _this.playNextChunk() } if (textLength > chunkSize) { // 递归调用,播放一定要按照文本分割顺序 _this.splitAndSynthesize(text.substr(chunkSize, textLength)) } else { // 最后一条合成,没有正在播放则立即播放 if (!_this.isPlayAudio) { _this.playNextChunk() } } uni.hideLoading() }, fail: function(err) { console.log("转换失败:", err) uni.hideLoading() } }) } else { if (!_this.isPlayAudio) { _this.playNextChunk() } uni.hideLoading() } }, playNextChunk() { if (this.audioAyy.length > 0 && this.audioPlayId) { const src = this.audioAyy.shift() this.isPlayAudio = true const audioContext = uni.createInnerAudioContext({ // 是否使用 WebAudio 作为底层音频驱动,默认关闭。对于短音频、播放频繁的音频建议开启此选项,开启后将获得更优的性能表现。由于开启此选项后也会带来一定的内存增长,因此对于长音频建议关闭此选项 useWebAudioImplement: false }) this.audioContext = audioContext audioContext.src = src audioContext.onEnded(() => { audioContext.destroy() this.playNextChunk() }) audioContext.play() console.log('------------src:' + src) } else { console.log('------------end') this.isPlayAudio = false } }, bottomToolClickHandle(item) { if (item.type === 'prompt') { this.streamReq({ prompt: item.prompt, init: 0 }) } if (item.type === 'link') { uni.navigateTo({ url: item.url }) } } } }