提交 8b19d05b 编写于 作者: DCloud_JSON's avatar DCloud_JSON

1.2.2

上级 ccbbf5f3
## 1.2.2(2023-06-16)
- 修复 部分情况下,通过 uni-ai 计费网关发起调用,“服务商接口抛出错误”会导致界面卡住的问题
- 修复 部分情况下,客户端关闭广告事件触发多次的问题
- 修复 AI返回表格格式的文档,没有边框线的问题
## 1.2.1(2023-06-14)
- 更新 大语言模型provider的值默认为:minimax
## 1.2.0(2023-06-14)
......
......@@ -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;
......
......@@ -11,17 +11,7 @@
<view v-if="msg.isAi" class="rich-text-box" :class="{'show-cursor':showCursor}" ref="rich-text-box">
<rich-text v-if="nodes&&nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick"></rich-text>
</view>
<view v-else>
{{msgContent}}
</view>
<view v-if="isLastMsg && adpid && msg.insufficientScore">
<text style="color: red;">
默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效)
位置:/components/uni-ai-msg/uni-ai-msg.vue 第28行,或全局搜索 uni-ad-rewarded-video
</text>
<!-- <uni-ad-rewarded-video :adpid="adpid" @onAdClose="onAdClose"></uni-ad-rewarded-video> -->
</view>
<view v-else>{{msgContent}}</view>
<view class="menu-box" :class='{"menu-box-ai":msg.isAi}'>
<text v-if="isLastMsg && msg.isAi" title="换一个答案" @click="changeAnswer" class="pointer change-answer"></text>
<view @click="showMoreMenu = !showMoreMenu" class="more-icon-box">
......@@ -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)
}
}
}
}
</script>
......
{
"id": "uni-ai-chat",
"name": "uni-ai-chat",
"version": "1.2.1",
"version": "1.2.2",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js",
"scripts": {
......
......@@ -5,14 +5,13 @@
<!-- #endif -->
<text class="noData" v-if="msgList.length === 0">没有对话记录</text>
<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
<uni-ai-msg ref="msg" v-for="(msg,index) in msgList" :key="index" :msg="msg"
@retriesSendMsg="retriesSendMsg" @changeAnswer="changeAnswer" @onAdClose="onAdClose"
<uni-ai-msg ref="msg" v-for="(msg,index) in msgList" :key="index" :msg="msg" @changeAnswer="changeAnswer"
:show-cursor="index == msgList.length - 1 && msgList.length%2 === 0 && sseIndex"
:isLastMsg="index == msgList.length - 1" @removeMsg="removeMsg(index)"></uni-ai-msg>
<template v-if="msgList.length%2 !== 0">
<view v-if="lastMsgState == -100" class="retries-box">
<view v-if="requestState == -100" class="retries-box">
<text>消息发送失败</text>
<uni-icons @click="retriesSendMsg" color="#d22" type="refresh-filled" class="retries-icon"></uni-icons>
<uni-icons @click="send" color="#d22" type="refresh-filled" class="retries-icon"></uni-icons>
</view>
<view class="tip-ai-ing" v-else-if="msgList.length">
<text>uni-ai正在思考中...</text>
......@@ -22,7 +21,14 @@
</view>
</view>
</template>
<view @click="closeSseChannel" class="stop-responding" v-if="enableStream && sseIndex"> ▣ 停止响应</view>
<view v-if="adpid" class="open-ad-btn-box">
<!-- <text style="color: red;">
默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效)
位置:/pages/chat/chat.vue 第30行,或全局搜索 uni-ad-rewarded-video
</text> -->
<uni-ad-rewarded-video v-show="insufficientScore" :adpid="adpid" @onAdClose="onAdClose"></uni-ad-rewarded-video>
</view>
<view @click="closeSseChannel" class="stop-responding" v-if="sseIndex"> ▣ 停止响应</view>
<view id="last-msg-item" style="height: 1px;"></view>
</scroll-view>
......@@ -52,7 +58,7 @@
<!-- #ifdef H5 -->
<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<!-- #endif -->
<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send"
<button @click="beforeSend" :disabled="inputBoxDisabled || !content" class="send"
type="primary">发送</button>
</view>
</view>
......@@ -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 {
......
......@@ -174,10 +174,9 @@ module.exports = {
// 如果是积分不足错误
else if (error == 'insufficientScore') {
// 设置回复内容
let reply = "积分不足,请看完激励视频广告后再试"
return {
"data": {
reply,
"reply":"积分不足,请看完激励视频广告后再试",
"insufficientScore": true
},
"errCode": 0
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册