From 8b19d05b056dc08b46d6c021dfecbb1e25204108 Mon Sep 17 00:00:00 2001 From: DCloud__JSON Date: Fri, 16 Jun 2023 20:34:18 +0800 Subject: [PATCH] 1.2.2 --- changelog.md | 4 + components/uni-ai-msg/uni-ai-msg.scss | 11 + components/uni-ai-msg/uni-ai-msg.vue | 20 +- package.json | 2 +- pages/chat/chat.vue | 315 +++++++++++------- .../cloudfunctions/uni-ai-chat/index.obj.js | 3 +- 6 files changed, 205 insertions(+), 150 deletions(-) diff --git a/changelog.md b/changelog.md index 96c3dcc..755772b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +## 1.2.2(2023-06-16) +- 修复 部分情况下,通过 uni-ai 计费网关发起调用,“服务商接口抛出错误”会导致界面卡住的问题 +- 修复 部分情况下,客户端关闭广告事件触发多次的问题 +- 修复 AI返回表格格式的文档,没有边框线的问题 ## 1.2.1(2023-06-14) - 更新 大语言模型provider的值默认为:minimax ## 1.2.0(2023-06-14) diff --git a/components/uni-ai-msg/uni-ai-msg.scss b/components/uni-ai-msg/uni-ai-msg.scss index 229b783..4449ff8 100644 --- a/components/uni-ai-msg/uni-ai-msg.scss +++ b/components/uni-ai-msg/uni-ai-msg.scss @@ -9,6 +9,17 @@ margin: 5px 0; overflow: auto; } + +.rich-text-box ::v-deep table { + border-spacing: 0; +} + +.rich-text-box ::v-deep th, +.rich-text-box ::v-deep td +{ + border: 1px solid #666; + padding:3px 5px; +} .cursor { display: none; diff --git a/components/uni-ai-msg/uni-ai-msg.vue b/components/uni-ai-msg/uni-ai-msg.vue index bead518..2b524c6 100644 --- a/components/uni-ai-msg/uni-ai-msg.vue +++ b/components/uni-ai-msg/uni-ai-msg.vue @@ -11,17 +11,7 @@ - - {{msgContent}} - - - - - 默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效) - 位置:/components/uni-ai-msg/uni-ai-msg.vue 第28行,或全局搜索 uni-ad-rewarded-video - - - + {{msgContent}} @@ -214,9 +204,6 @@ changeAnswer(){ this.$emit('changeAnswer') }, - retriesSendMsg(){ - this.$emit('retriesSendMsg') - }, // 复制文本内容到系统剪切板 copy() { uni.setClipboardData({ @@ -235,10 +222,7 @@ removeMsg(){ this.$emit('removeMsg') this.showMoreMenu = false - }, - onAdClose(e){ - this.$emit('onAdClose',e) - } + } } } diff --git a/package.json b/package.json index c5253ab..e6d15f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "id": "uni-ai-chat", "name": "uni-ai-chat", - "version": "1.2.1", + "version": "1.2.2", "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体", "main": "main.js", "scripts": { diff --git a/pages/chat/chat.vue b/pages/chat/chat.vue index 5f70070..8374044 100644 --- a/pages/chat/chat.vue +++ b/pages/chat/chat.vue @@ -5,14 +5,13 @@ 没有对话记录 - - ▣ 停止响应 + + + + + ▣ 停止响应 @@ -52,7 +58,7 @@ ↵ 发送 / shift + ↵ 换行 - @@ -93,7 +99,11 @@ // 使聊天窗口滚动到指定元素id的值 scrollIntoView: "", // 消息列表数据 - msgList: [], + msgList: [], + // 通讯请求状态 + requestState:0,//0发送中 100发送成功 -100发送失败 + // 本地对话是否因积分不足而终止 + insufficientScore:false, // 输入框的消息内容 content: "", // 记录流式响应次数 @@ -130,15 +140,6 @@ NODE_ENV() { return process.env.NODE_ENV }, - //最后一条消息的状态 - lastMsgState() { - let mLength = this.msgList.length - if (mLength) { - return this.msgList[mLength - 1].state - } else { - return false - } - }, footBoxPaddingBottom() { return (this.keyboardHeight || 10) + 'px' } @@ -155,6 +156,12 @@ }, // 深度监听msgList变化 deep: true + }, + insufficientScore(insufficientScore){ + uni.setStorage({ + "key": "uni-ai-chat-insufficientScore", + "data": insufficientScore + }) }, llmModel(llmModel) { let title = 'uni-ai-chat' @@ -215,14 +222,17 @@ this.msgList = uni.getStorageSync('uni-ai-msg') || []; // 获得之前设置的llmModel - this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel') + this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel') + + // 获得之前设置的uni-ai-chat-insufficientScore + this.insufficientScore = uni.getStorageSync('uni-ai-chat-insufficientScore') || false // 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。 let length = this.msgList.length if (length) { let lastMsg = this.msgList[length - 1] if (!lastMsg.isAi) { - this.retriesSendMsg() + this.send() } } @@ -250,7 +260,7 @@ e.preventDefault() // 执行发送 setTimeout(() => { - this.beforeSendMsg(); + this.beforeSend(); }, 300) } }; @@ -338,20 +348,6 @@ } this.msgList.splice(length - 1, 1, lastMsg) }, - setSummarize(summarizeData) { - // console.log('setSummarize'); - let length = this.msgList.length; - let index = length - 2 - // 从 v1.1.0起,总结可能是总结倒数第二条之前的内容 - if (index < 0) { - index = 0 - } - - let msg = this.msgList[index] - msg.summarize = summarizeData - this.msgList.splice(index, 1, msg) - // console.log('setSummarize this.msgList',this.msgList); - }, // 广告关闭事件 onAdClose(e) { console.log('onAdClose e.detail.isEnded', e.detail.isEnded); @@ -383,12 +379,13 @@ clearInterval(myIntive) // 隐藏加载提示 uni.hideLoading() - if (score > 0) { + if (score > 0) { + this.insufficientScore = false // 移除最后一条消息 this.msgList.pop() this.$nextTick(() => { // 重发消息 - this.retriesSendMsg() + this.send() uni.showToast({ title: '积分余额:' + score, icon: 'none' @@ -399,34 +396,23 @@ }, 2000); } }, - async retriesSendMsg() { - // 检查是否开通uni-push;决定是否启用enableStream - await this.checkIsOpenPush() - // 更新最后一条消息的状态为0 表示消息正在发送中 - this.updateLastMsg({ - state: 0 - }) - // 发送消息 - this.send() - }, // 换一个答案 async changeAnswer() { // 如果问题还在回答中需要先关闭 if (this.sseIndex) { this.closeSseChannel() } - //删除旧的回答 this.msgList.pop() - this.updateLastMsg({ // 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问 illegal: false, - // 多设备登录时其他设备看广告后点击重新回答,insufficientScore应当设置为 false - insufficientScore:false - }) + }) + // 多设备登录时其他设备看广告后点击重新回答,insufficientScore应当设置为 false + this.insufficientScore = false this.send() - }, + }, + //当消息涉及敏感 removeMsg(index) { // 如果问题还在回答中需要先关闭 if (this.sseIndex) { @@ -438,7 +424,7 @@ } this.msgList.splice(index,2) }, - async beforeSendMsg() { + async beforeSend() { if (this.inputBoxDisabled) { return uni.showToast({ title: 'ai正在回复中不能发送', @@ -511,8 +497,6 @@ isAi: false, // 消息内容 content: this.content, - // 消息状态为0,表示正在发送中 - state: 0, // 消息创建时间 create_time: Date.now() }) @@ -524,17 +508,18 @@ this.content = '' }) this.send() // 发送消息 - }, + }, async send() { - // 流式响应和云对象的请求状态 - let state = { - sse:0, - co:0 - } - + // 请求状态归零 + this.requestState = 0 + // 防止重复发起,关闭之前的 + uniCoTaskList.clear() + // 清除旧的afterChatCompletion(如果存在) + if(this.afterChatCompletion && this.afterChatCompletion.clear) this.afterChatCompletion.clear() + let messages = [] // 复制一份,消息列表数据 - let msgs = JSON.parse(JSON.stringify(this.msgList)).filter(i => i.isDelete !== true) + let msgs = JSON.parse(JSON.stringify(this.msgList)) // 带总结的消息 index let findIndex = [...msgs].reverse().findIndex(item => item.summarize) // console.log('findIndex', findIndex) @@ -549,10 +534,8 @@ // 如果未总结过就直接从末尾拿10条 msgs = msgs.splice(-10) } - // 过滤涉敏问题 msgs = msgs.filter(msg => !msg.illegal) - // 根据数据内容设置角色 messages = msgs.map(item => { // 角色默认为用户 @@ -574,12 +557,13 @@ await this.checkIsOpenPush() // console.log('this.enableStream',this.enableStream); + // 流式响应和云对象的请求结束回调函数 + let sseEnd,requestEnd; // 判断是否开启了流式响应模式 if (this.enableStream) { // 创建消息通道 sseChannel = new uniCloud.SSEChannel() // console.log('sseChannel',sseChannel); - // 监听message事件 sseChannel.on('message', (message) => { // console.log('on message', message); @@ -592,13 +576,12 @@ content: message, create_time: Date.now() }) - this.showLastMsg() } else { this.updateLastMsg(lastMsg => { lastMsg.content += message }) - this.showLastMsg() - } + } + this.showLastMsg() // 让流式响应计数值递增 this.sseIndex++ }) @@ -606,21 +589,85 @@ // 监听end事件,如果云端执行end时传了message,会在客户端end事件内收到传递的消息 sseChannel.on('end', (e) => { console.log('sse 结束',e) - state.sse = 1 - if(state.sse === 1 && state.co === 1){ - // console.error('通过 sse end 结束',state); + if(e && typeof e == 'object' && e.errCode){ + let setLastAiMsgContent = (content)=>{ + // console.log(content); + // 如果最后一项不是ai的就添加,否则就执行更新最后一条消息 + if (this.sseIndex === 0) { + this.msgList.push({ + isAi: true, + content, + create_time: Date.now() + }) + } else { + this.updateLastMsg(lastMsg => { + lastMsg.content += content + }) + } + this.showLastMsg() + } + if(e.errCode == 60004){ + //服务商检测到AI输出了敏感内容 + let length = this.msgList.length + //如果最后一项不是ai,就创建一项 + if(length % 2){ + this.msgList.push({ + isAi: true, + content:"内容涉及敏感", + illegal:true, + create_time: Date.now() + }) + length += 1 + } + // 更新倒数第2项 用户提的问题 + this.msgList[length - 2].illegal = true + // 更新倒数第1项 ai 回答的内容 + this.msgList[length - 1].illegal = true + this.msgList[length - 1].content = "内容涉及敏感" + + }else{ + setLastAiMsgContent(e.errMsg) + } + } + sseEnd() + }) + await sseChannel.open(); // 等待通道开启 + + // 等待对话完成(云函数请求完成,sse 执行了 end)之后 + (function fnSelf(that){ + fnSelf.clear = ()=>{ + // console.log('do fnSelf.clear'); + if(fnSelf.clear.sse){ + // console.log('fnSelf.clear.sse();') + fnSelf.clear.sse(); + } + if(fnSelf.clear.request){ + // console.log('fnSelf.clear.request();') + fnSelf.clear.request(); + } + } + Promise.all([ + new Promise((resolve,reject)=>{ + sseEnd = resolve; + fnSelf.clear.sse = reject; + }), + new Promise((resolve,reject)=>{ + requestEnd = resolve; + fnSelf.clear.request = reject; + }) + ]).then((e)=>{ + // console.log('sseEnd && requestEnd'); //当两个都结束时 sseChannel.close() // 结束流式响应 将流式响应计数值 设置为 0 - this.sseIndex = 0 - state = {sse:0,co:0} - }else{ - // console.log(1,state); - } - }) - await sseChannel.open() // 等待通道开启 - } - + that.sseIndex = 0 + }).catch((err)=>{ + // console.log('afterChatCompletion is close',err); + }) + that.afterChatCompletion = fnSelf + })(this) + } + // 导入uni-ai-chat模块,并设置customUI为true let task = uniCoTask({ coName: "uni-ai-chat", @@ -633,41 +680,29 @@ config: { customUI: true }, - success: res => { - console.log("success",res); - if (!res.data) { - return - } - // 更新最后一条消息的状态为100(发送成功) - this.updateLastMsg({ - state: 100 - }) + success: res => { + // 更新 通讯状态为100(发送成功) + this.requestState = 100 + // console.log("success",res); + if (!res.data) return let { - "reply": content, + reply, summarize, insufficientScore, illegal } = res.data // 特殊处理 - start - if(this.enableStream == false && !content){ + if(this.enableStream == false && !reply){ + //服务商检测到AI输出了敏感内容 illegal = true - content = "内容涉及敏感" + reply = "内容涉及敏感" } // 特殊处理 - end - - if (illegal) { - // 如果返回的数据包含illegal属性,就更新最后一条消息(用户输入的问题)的illegal属性为true - this.updateLastMsg({ - // 添加消息涉敏标记 - illegal: true - }) - } - - + // 非流式模式 或者流式模式,但列表还没有数据且已经进入异常的情况下 - if (this.enableStream == false || this.sseIndex == 0 && (illegal || insufficientScore)) { + if (this.enableStream == false || this.sseIndex == 0 && (illegal||insufficientScore)) { // 将从云端接收到的消息添加到消息列表中 this.msgList.push({ // 消息创建时间 @@ -675,36 +710,50 @@ // 标记消息为来自AI机器人 isAi: true, // 消息内容 - content, + content:reply, // 消息是否涉敏标记 - illegal, - // 本地对话是否因积分不足而终止 - insufficientScore + illegal }) + } + + if(insufficientScore){ + // 积分不足 + this.insufficientScore = true } - // console.log(res, res.reply); - // 如果回调包含总结的内容,就设置总结 if(summarize){ console.log(' 拿到总结',summarize); - this.setSummarize(summarize) + // 总结的内容是上一轮对话的 + // console.log('setSummarize'); + let index = this.msgList.length; + // 如果最后一项是ai就往前退2项,否则退一项(流式响应的时候,回答可能晚于总结) + if(index%2 === 0){ + index -= 2 + }else{ + index -= 1 + } + // 假如第一次提问就需要总结 + if (index < 0) { + index = 0 + } + let msg = this.msgList[index] + msg.summarize = summarize + this.msgList.splice(index, 1, msg) + // console.log('setSummarize this.msgList',this.msgList); + } + if (illegal) { + console.error('内容涉及敏感'); + this.updateLastMsg({ + // 添加消息涉敏标记 + illegal: true + }) } }, complete:e=>{ - // console.log('complete:',e); - if (sseChannel) { - state.co = 1 - if(state.sse === 1 && state.co === 1){ - // console.error('通过 co complete 结束'); - //当两个都结束时 - sseChannel.close() - // 结束流式响应 将流式响应计数值 设置为 0 - this.sseIndex = 0 - state = {sse:0,co:0} - }else{ - // console.log(2,state); - } + if (this.enableStream) { + requestEnd() } + // console.log('complete:',e); // 滚动窗口以显示最新的一条消息 this.$nextTick(() => { this.showLastMsg() @@ -712,15 +761,17 @@ }, fail: e => { console.error(e); - // 更新最后一条消息的状态为-100(发送失败) - this.updateLastMsg({ - state: -100 - }) + // 更新 通讯状态为-100(发送失败) + this.requestState = -100 // 弹框提示用户错误原因 uni.showModal({ content: JSON.stringify(e.message), showCancel: false - }); + }); + // 如果启用流式,云函数出错了,sse 也应当被终止 + if (this.enableStream) { + sseEnd() + } } }) uniCoTaskList.push(task) @@ -728,7 +779,8 @@ closeSseChannel() { // 如果存在消息通道,就关闭消息通道 if (sseChannel) { - sseChannel.close() + sseChannel.close() + // 设置为 false 防止重复调用closeSseChannel时出错 sseChannel = false } // 清空历史网络请求(调用云对象)任务 @@ -922,6 +974,11 @@ color: #aaa; font-size: 12px; justify-content: center; + } + + .open-ad-btn-box{ + justify-content: center; + margin: 10px 0; } .tip-ai-ing { diff --git a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js index cc5f531..6357ed8 100644 --- a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js +++ b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js @@ -174,10 +174,9 @@ module.exports = { // 如果是积分不足错误 else if (error == 'insufficientScore') { // 设置回复内容 - let reply = "积分不足,请看完激励视频广告后再试" return { "data": { - reply, + "reply":"积分不足,请看完激励视频广告后再试", "insufficientScore": true }, "errCode": 0 -- GitLab