提交 514360da 编写于 作者: DCloud_JSON's avatar DCloud_JSON

1.2.0

上级 d1ebe1f7
## 1.2.0(2023-06-14)
- 【重要】支持通过 uni-ai 计费网关发起调用 [详情参考](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#api)
- 更新 大语言模型provider的值默认为:azure
## 1.1.5(2023-06-13) ## 1.1.5(2023-06-13)
- 修复 提供给AI做出总结的内容,多包含了最后一次提问的内容 - 修复 提供给AI做出总结的内容,多包含了最后一次提问的内容
## 1.1.4(2023-06-13) ## 1.1.4(2023-06-13)
......
...@@ -11,7 +11,6 @@ class Task { ...@@ -11,7 +11,6 @@ class Task {
complete complete
} }
} }
invoke(callbackName, ...args) { invoke(callbackName, ...args) {
if (this.status !== 0) { if (this.status !== 0) {
// console.log('此任务已被终止'); // console.log('此任务已被终止');
......
...@@ -55,11 +55,9 @@ ...@@ -55,11 +55,9 @@
currentModel:'' currentModel:''
}; };
}, },
mounted() {
this.currentModel = uni.getStorageSync('uni-ai-chat-llmModel')
},
methods: { methods: {
open(callback){ open(callback){
this.currentModel = uni.getStorageSync('uni-ai-chat-llmModel')
confirmCallback = callback confirmCallback = callback
this.$refs.popup.open('center') this.$refs.popup.open('center')
}, },
......
...@@ -39,10 +39,6 @@ ...@@ -39,10 +39,6 @@
</template> </template>
</view> </view>
</view> </view>
<!-- <uni-icons v-if="isLastMsg && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
@click="msg.state == -100 ? retriesSendMsg() : ''" :color="msg.state===0?'#999':'#d22'"
:type="msgStateIcon(msg)" class="msgStateIcon">
</uni-icons> -->
</view> </view>
</view> </view>
</template> </template>
...@@ -56,10 +52,6 @@ ...@@ -56,10 +52,6 @@
adpid adpid
} = config } = config
import {
msgList
} from '@/pages/chat/msgList.js';
// 引入markdown-it库 // 引入markdown-it库
import MarkdownIt from '@/lib/markdown-it.min.js'; import MarkdownIt from '@/lib/markdown-it.min.js';
...@@ -126,18 +118,13 @@ ...@@ -126,18 +118,13 @@
}) })
export default { export default {
name: "msg", name: "uni-ai-msg",
data() { data() {
return { return {
// 悬浮的复制按钮的左边距 // 悬浮的复制按钮的左边距
left: "-100px", left: "-100px",
// 悬浮的复制按钮的上边距 // 悬浮的复制按钮的上边距
top: "-100px", top: "-100px",
msg: {
content: "",
isDelete:false
},
msgIndexList: 0,
adpid, adpid,
showMoreMenu:false showMoreMenu:false
}; };
...@@ -145,7 +132,6 @@ ...@@ -145,7 +132,6 @@
mounted() { mounted() {
}, },
created() { created() {
this.msg = msgList[this.msgIndex]
}, },
props: { props: {
// 是否显示鼠标闪烁的效果 // 是否显示鼠标闪烁的效果
...@@ -155,24 +141,30 @@ ...@@ -155,24 +141,30 @@
return false return false
} }
}, },
msgIndex: {
type: Number,
default () {
return false
}
},
isLastMsg: { isLastMsg: {
type: Boolean, type: Boolean,
default () { default () {
return false return false
} }
} },
msg: {
type: Object,
default () {
return {
content: "",
isDelete:false
}
}
},
}, },
computed: { computed: {
msgContent() { msgContent() {
return this.msg.content return this.msg.content
}, },
nodes() { nodes() {
if(!this.msgContent){
return //处理特殊情况,比如网络异常导致的响应的 content 的值为空
}
let htmlString = '' let htmlString = ''
// 修改转换结果的htmlString值 用于正确给界面增加鼠标闪烁的效果 // 修改转换结果的htmlString值 用于正确给界面增加鼠标闪烁的效果
// 判断markdown中代码块标识符的数量是否为偶数 // 判断markdown中代码块标识符的数量是否为偶数
...@@ -200,27 +192,6 @@ ...@@ -200,27 +192,6 @@
} }
}, },
methods: { methods: {
// 根据消息状态返回对应的图标
msgStateIcon(msg) {
switch (msg.state) {
case 0:
// 发送中
return ''
break;
case -100:
// 发送失败
return 'refresh-filled'
break;
case -200:
// 禁止发送(内容不合法)
return 'info-filled'
break;
default:
// 默认不返回任何图标
return false
break;
}
},
trOnclick(e){ trOnclick(e){
console.log(e); console.log(e);
let {attrs} = e.detail.node let {attrs} = e.detail.node
...@@ -262,8 +233,11 @@ ...@@ -262,8 +233,11 @@
}, },
// 删除消息 // 删除消息
removeMsg(){ removeMsg(){
this.$emit('removeMsg',this.msgIndex) this.$emit('removeMsg')
this.showMoreMenu = false this.showMoreMenu = false
},
onAdClose(e){
this.$emit('onAdClose',e)
} }
} }
} }
...@@ -292,14 +266,6 @@ ...@@ -292,14 +266,6 @@
padding: 0 15px; padding: 0 15px;
padding-bottom: 15px; padding-bottom: 15px;
} }
.msgStateIcon {
position: relative;
top: -5px;
right: 1px;
align-self: center;
}
.avatar { .avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
...@@ -332,7 +298,8 @@ ...@@ -332,7 +298,8 @@
word-break: break-all; word-break: break-all;
user-select: text; user-select: text;
cursor: text; cursor: text;
/* #endif */ /* #endif */
flex-direction: column;
} }
.menu-box { .menu-box {
......
{ {
"id": "uni-ai-chat", "id": "uni-ai-chat",
"name": "uni-ai-chat", "name": "uni-ai-chat",
"version": "1.1.5", "version": "1.2.0",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体", "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
......
...@@ -3,18 +3,18 @@ ...@@ -3,18 +3,18 @@
<!-- #ifdef H5 --> <!-- #ifdef H5 -->
<view v-if="isWidescreen" class="header">uni-ai-chat</view> <view v-if="isWidescreen" class="header">uni-ai-chat</view>
<!-- #endif --> <!-- #endif -->
<text class="noData" v-if="msgLength === 0">没有对话记录</text> <text class="noData" v-if="msgList.length === 0">没有对话记录</text>
<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true"> <scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
<uni-ai-msg ref="msg" v-for="(msgIndex,index) in msgLength" :key="index" :msgIndex="index" <uni-ai-msg ref="msg" v-for="(msg,index) in msgList" :key="index" :msg="msg"
@retriesSendMsg="retriesSendMsg" @changeAnswer="changeAnswer" @retriesSendMsg="retriesSendMsg" @changeAnswer="changeAnswer" @onAdClose="onAdClose"
:show-cursor="index == msgLength - 1 && msgLength%2 === 0 && sseIndex" :show-cursor="index == msgList.length - 1 && msgList.length%2 === 0 && sseIndex"
:isLastMsg="index == visibleMsgLength - 1" @removeMsg="removeMsg"></uni-ai-msg> :isLastMsg="index == msgList.length - 1" @removeMsg="removeMsg(index)"></uni-ai-msg>
<template v-if="msgLength%2 !== 0"> <template v-if="msgList.length%2 !== 0">
<view v-if="lastMsgState == -100" class="retries-box"> <view v-if="lastMsgState == -100" class="retries-box">
<text>消息发送失败</text> <text>消息发送失败</text>
<uni-icons @click="retriesSendMsg" color="#d22" type="refresh-filled" class="retries-icon"></uni-icons> <uni-icons @click="retriesSendMsg" color="#d22" type="refresh-filled" class="retries-icon"></uni-icons>
</view> </view>
<view class="tip-ai-ing" v-else-if="msgLength"> <view class="tip-ai-ing" v-else-if="msgList.length">
<text>uni-ai正在思考中...</text> <text>uni-ai正在思考中...</text>
<view v-if="NODE_ENV == 'development' && !enableStream"> <view v-if="NODE_ENV == 'development' && !enableStream">
如需提速,请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html" 如需提速,请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html"
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
:placeholder="placeholderText" :maxlength="-1" :adjust-position="false" :placeholder="placeholderText" :maxlength="-1" :adjust-position="false"
:disable-default-padding="false" placeholder-class="input-placeholder"></textarea> :disable-default-padding="false" placeholder-class="input-placeholder"></textarea>
</view> </view>
<view class="send-btn-box" :title="(msgLength && msgLength%2 !== 0) ? 'ai正在回复中不能发送':''"> <view class="send-btn-box" :title="(msgList.length && msgList.length%2 !== 0) ? 'ai正在回复中不能发送':''">
<!-- #ifdef H5 --> <!-- #ifdef H5 -->
<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text> <text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<!-- #endif --> <!-- #endif -->
...@@ -65,10 +65,6 @@ ...@@ -65,10 +65,6 @@
// 引入配置文件 // 引入配置文件
import config from '@/config.js'; import config from '@/config.js';
import {
msgList
} from '@/pages/chat/msgList.js';
// 导入uniCloud云对象task模块 // 导入uniCloud云对象task模块
import uniCoTask from '@/common/unicloud-co-task.js'; import uniCoTask from '@/common/unicloud-co-task.js';
// 收集所有执行云对象的任务列表 // 收集所有执行云对象的任务列表
...@@ -89,14 +85,13 @@ ...@@ -89,14 +85,13 @@
let sseChannel = false; let sseChannel = false;
// 键盘的shift键是否被按下 // 键盘的shift键是否被按下
let shiftKeyPressed = false let shiftKeyPressed = false
export default { export default {
data() { data() {
return { return {
// 使聊天窗口滚动到指定元素id的值 // 使聊天窗口滚动到指定元素id的值
scrollIntoView: "", scrollIntoView: "",
// 消息长度(个数)
msgLength: 0,
// 消息列表数据 // 消息列表数据
msgList: [], msgList: [],
// 输入框的消息内容 // 输入框的消息内容
...@@ -110,8 +105,7 @@ ...@@ -110,8 +105,7 @@
// 广告位id // 广告位id
adpid, adpid,
llmModel: false, llmModel: false,
keyboardHeight: 0, keyboardHeight: 0
visibleMsgLength:0
} }
}, },
computed: { computed: {
...@@ -122,8 +116,8 @@ ...@@ -122,8 +116,8 @@
return true return true
} }
// 如果消息列表长度为奇数,则禁用输入框 // 如果消息列表长度为奇数,则禁用输入框
return !!(this.msgLength && this.msgLength % 2 !== 0) return !!(this.msgList.length && this.msgList.length % 2 !== 0)
}, },
// 输入框占位符文本 // 输入框占位符文本
placeholderText() { placeholderText() {
// #ifdef H5 // #ifdef H5
...@@ -152,16 +146,7 @@ ...@@ -152,16 +146,7 @@
// 监听msgList变化,将其存储到本地缓存中 // 监听msgList变化,将其存储到本地缓存中
watch: { watch: {
msgList: { msgList: {
handler(msgList) { handler(msgList) {
let msgLength = msgList.length
if (msgLength != this.msgLength) {
this.msgLength = msgLength
this.$nextTick(() => {
this.updateLastMsg(msgList[msgLength - 1])
})
}
msgList = msgList.filter(i => i.isDelete !== true)
this.visibleMsgLength = msgList.length
// 将msgList存储到本地缓存中 // 将msgList存储到本地缓存中
uni.setStorage({ uni.setStorage({
"key": "uni-ai-msg", "key": "uni-ai-msg",
...@@ -227,11 +212,7 @@ ...@@ -227,11 +212,7 @@
// } // }
// 获得历史对话记录 // 获得历史对话记录
let _msgList = uni.getStorageSync('uni-ai-msg') || []; this.msgList = uni.getStorageSync('uni-ai-msg') || [];
if (_msgList.length) {
msgList.push(..._msgList)
}
this.msgList = msgList
// 获得之前设置的llmModel // 获得之前设置的llmModel
this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel') this.llmModel = uni.getStorageSync('uni-ai-chat-llmModel')
...@@ -302,7 +283,6 @@ ...@@ -302,7 +283,6 @@
this.showLastMsg() this.showLastMsg()
}) })
} }
} }
// #endif // #endif
...@@ -396,7 +376,8 @@ ...@@ -396,7 +376,8 @@
// 解构出score字段的值,如果没有则默认为undefined // 解构出score字段的值,如果没有则默认为undefined
let { let {
score score
} = res.result.data[0] || {} } = res.result.data[0] || {}
console.log('score',score);
if (score > 0 || i > 5) { if (score > 0 || i > 5) {
// 清除轮询定时器 // 清除轮询定时器
clearInterval(myIntive) clearInterval(myIntive)
...@@ -438,30 +419,24 @@ ...@@ -438,30 +419,24 @@
//删除旧的回答 //删除旧的回答
this.msgList.pop() this.msgList.pop()
// 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问 this.updateLastMsg({
this.updateLastMsg({ // 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问
illegal: false illegal: false,
// 多设备登录时其他设备看广告后点击重新回答,insufficientScore应当设置为 false
insufficientScore:false
}) })
this.send() this.send()
}, },
removeMsg(index) { removeMsg(index) {
// #ifdef VUE3 // 如果问题还在回答中需要先关闭
this.msgList[index].isDelete = true if (this.sseIndex) {
if (this.msgList[index].isAi && this.msgList[index - 1]) { this.closeSseChannel()
this.msgList[index - 1].isDelete = true }
} else if (this.msgList[index + 1]) {
this.msgList[index + 1].isDelete = true if (this.msgList[index].isAi) {
} index -= 1
// #endif }
this.msgList.splice(index,2)
// #ifdef VUE2
this.$set(msgList[index], "isDelete", true)
if (msgList[index].isAi && msgList[index - 1]) {
this.$set(msgList[index - 1], "isDelete", true)
} else if (msgList[index + 1]) {
this.$set(msgList[index + 1], "isDelete", true)
}
// #endif
}, },
async beforeSendMsg() { async beforeSendMsg() {
if (this.inputBoxDisabled) { if (this.inputBoxDisabled) {
...@@ -550,11 +525,16 @@ ...@@ -550,11 +525,16 @@
}) })
this.send() // 发送消息 this.send() // 发送消息
}, },
async send() { async send() {
// 流式响应和云对象的请求状态
let state = {
sse:0,
co:0
}
let messages = [] let messages = []
// 复制一份,消息列表数据 // 复制一份,消息列表数据
let msgs = JSON.parse(JSON.stringify(msgList)).filter(i => i.isDelete !== true) let msgs = JSON.parse(JSON.stringify(this.msgList)).filter(i => i.isDelete !== true)
// 带总结的消息 index // 带总结的消息 index
let findIndex = [...msgs].reverse().findIndex(item => item.summarize) let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
// console.log('findIndex', findIndex) // console.log('findIndex', findIndex)
...@@ -563,13 +543,13 @@ ...@@ -563,13 +543,13 @@
// console.log('aiSummaryIndex', aiSummaryIndex) // console.log('aiSummaryIndex', aiSummaryIndex)
// 将带总结的消息的 内容 更换成 总结 // 将带总结的消息的 内容 更换成 总结
msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize
// 拿最后一条带直接的消息作为与ai对话的msg body // 拿最后一条带直接的消息作为与ai对话的msg body
msgs = msgs.splice(aiSummaryIndex, msgs.length - 1) msgs = msgs.splice(aiSummaryIndex)
} else { } else {
// 如果未总结过就直接从末尾拿10条 // 如果未总结过就直接从末尾拿10条
msgs = msgs.splice(-10) msgs = msgs.splice(-10)
} }
// 过滤涉敏问题 // 过滤涉敏问题
msgs = msgs.filter(msg => !msg.illegal) msgs = msgs.filter(msg => !msg.illegal)
...@@ -592,7 +572,7 @@ ...@@ -592,7 +572,7 @@
// 检查是否开通uni-push;决定是否启用enableStream // 检查是否开通uni-push;决定是否启用enableStream
await this.checkIsOpenPush() await this.checkIsOpenPush()
// console.log('this.enableStream',this.enableStream); // console.log('this.enableStream',this.enableStream);
// 判断是否开启了流式响应模式 // 判断是否开启了流式响应模式
if (this.enableStream) { if (this.enableStream) {
...@@ -625,37 +605,18 @@ ...@@ -625,37 +605,18 @@
// 监听end事件,如果云端执行end时传了message,会在客户端end事件内收到传递的消息 // 监听end事件,如果云端执行end时传了message,会在客户端end事件内收到传递的消息
sseChannel.on('end', (e) => { sseChannel.on('end', (e) => {
// console.log('on end', e); console.log('sse 结束',e)
// 如果e存在且包含summarize或insufficientScore属性 state.sse = 1
if (e) { if(state.sse === 1 && state.co === 1){
// 如果e包含summarize属性 // console.error('通过 sse end 结束',state);
if (e.summarize) { //当两个都结束时
// 设置总结 sseChannel.close()
this.setSummarize(e.summarize) // 结束流式响应 将流式响应计数值 设置为 0
}else{ this.sseIndex = 0
// 更新最后一条消息 state = {sse:0,co:0}
this.updateLastMsg(lastMsg => { }else{
// 如果e包含illegal属性 // console.log(1,state);
if (e.illegal) { }
// 将最后一条消息的illegal属性更新为e的illegal属性
lastMsg.illegal = e.illegal
lastMsg.content = "内容涉及敏感"
// 倒数第二条(用户发问内容)也需要设置illegal的值
this.msgList[this.msgList.length - 2].illegal = e.illegal
}
// 如果e包含insufficientScore属性
else if (e.insufficientScore) {
// 将最后一条消息的insufficientScore属性更新为e的insufficientScore属性
lastMsg.insufficientScore = e.insufficientScore
}
})
}
}
// 结束流式响应 将流式响应计数值 设置为 0
this.sseIndex = 0
// 滚动窗口以显示最新的一条消息
this.showLastMsg()
}) })
await sseChannel.open() // 等待通道开启 await sseChannel.open() // 等待通道开启
} }
...@@ -673,67 +634,84 @@ ...@@ -673,67 +634,84 @@
customUI: true customUI: true
}, },
success: res => { success: res => {
// console.log(111,res); console.log("success",res);
if (!sseChannel) { if (!res.data) {
if (!res.data) { return
return }
} // 更新最后一条消息的状态为100(发送成功)
// 更新最后一条消息的状态为100(发送成功) this.updateLastMsg({
this.updateLastMsg({ state: 100
state: 100 })
})
// console.log(res, res.reply); let {
let { "reply": content,
"reply": content, summarize,
summarize, insufficientScore,
insufficientScore, illegal
illegal } = res.data
} = res.data
if (illegal) { // 特殊处理 - start
// 如果返回的数据包含illegal属性,就更新最后一条消息的illegal属性为true if(this.enableStream == false && !content){
this.updateLastMsg({ illegal = true
illegal: true content = "内容涉及敏感"
}) }
} // 特殊处理 - end
// 将从云端接收到的消息添加到消息列表中
this.msgList.push({ if (illegal) {
// 添加消息创建时间 // 如果返回的数据包含illegal属性,就更新最后一条消息(用户输入的问题)的illegal属性为true
create_time: Date.now(), this.updateLastMsg({
// 标记消息为来自AI机器人 // 添加消息涉敏标记
isAi: true, illegal: true
// 添加消息内容
content,
// 添加消息分数不足标记
insufficientScore,
// 添加消息涉敏标记
illegal
}) })
// 如果回调包含总结的内容,就设置总结 }
if(summarize){
this.setSummarize(summarize)
} // 非流式模式 或者流式模式,但列表还没有数据且已经进入异常的情况下
// 滚动窗口以显示最新的一条消息 if (this.enableStream == false || this.sseIndex == 0 && (illegal || insufficientScore)) {
this.$nextTick(() => { // 将从云端接收到的消息添加到消息列表中
this.showLastMsg() this.msgList.push({
}) // 消息创建时间
} else { create_time: Date.now(),
// 处理 sseChannel没结束 云函数提前结束的情况 // 标记消息为来自AI机器人
sseChannel.close() isAi: true,
this.sseIndex = 0 // 消息内容
content,
// 消息是否涉敏标记
illegal,
// 本地对话是否因积分不足而终止
insufficientScore
})
}
// console.log(res, res.reply);
// 如果回调包含总结的内容,就设置总结
if(summarize){
console.log(' 拿到总结',summarize);
this.setSummarize(summarize)
} }
},
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);
}
}
// 滚动窗口以显示最新的一条消息
this.$nextTick(() => {
this.showLastMsg()
})
}, },
fail: e => { fail: e => {
console.log(e); console.error(e);
// 获取消息列表长度
let l = this.msgList.length
// console.log(l,this.msgList[l-1]);
// 如果最后一条消息的来源是人工智能机器人 就将流式响应计数值设置为0
if (l && sseChannel && this.msgList[l - 1].isAi) {
sseChannel.close()
this.sseIndex = 0
}
// 更新最后一条消息的状态为-100(发送失败) // 更新最后一条消息的状态为-100(发送失败)
this.updateLastMsg({ this.updateLastMsg({
state: -100 state: -100
...@@ -783,7 +761,7 @@ ...@@ -783,7 +761,7 @@
// 关闭ssh请求 // 关闭ssh请求
this.closeSseChannel() this.closeSseChannel()
// 将消息列表清空 // 将消息列表清空
this.msgList.splice(0, this.msgLength); this.msgList.splice(0, this.msgList.length);
} }
} }
}); });
......
export const msgList = []
...@@ -26,7 +26,7 @@ async function nextFn(data) { ...@@ -26,7 +26,7 @@ async function nextFn(data) {
pluginId: 'uni-ai-chat' pluginId: 'uni-ai-chat'
}).config() }).config()
console.log('uniAiChatConfig',uniAiChatConfig); console.log('uniAiChatConfig',uniAiChatConfig);
if(!uniAiChatConfig || !uniAiChatConfig.ad || !uniAiChatConfig.earnedScore.ad){ if(!uniAiChatConfig || !uniAiChatConfig.earnedScore || !uniAiChatConfig.earnedScore.ad){
throw new Error('请先完成uni-ai-chat的广告奖励配置') throw new Error('请先完成uni-ai-chat的广告奖励配置')
} }
......
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj // 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129 // jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
// 引入utils模块中的safeRequire和checkContentSecurityEnable函数 // 引入utils模块中的safeRequire和checkContentSecurityEnable函数
const {safeRequire, checkContentSecurityEnable} = require('./utils') const {
// 引入uni-config-center模块,并创建config对象 safeRequire,
const createConfig = safeRequire('uni-config-center') checkContentSecurityEnable
const config = createConfig({ } = require('./utils')
pluginId: 'uni-ai-chat' // 引入uni-config-center模块,并创建config对象
}).config() const createConfig = safeRequire('uni-config-center')
// 引入uniCloud.database()方法,并创建db对象 const config = createConfig({
const db = uniCloud.database(); pluginId: 'uni-ai-chat'
// 创建userscollection对象 }).config()
const userscollection = db.collection('uni-id-users') // 引入uniCloud.database()方法,并创建db对象
// 引入uni-id-common模块 const db = uniCloud.database();
const uniIdCommon = require('uni-id-common') // 创建userscollection对象
const userscollection = db.collection('uni-id-users')
// 引入uni-id-common模块
module.exports = { const uniIdCommon = require('uni-id-common')
_before:async function() {
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑
module.exports = {
// 判断否调用量本云对象的send方法 _before: async function() {
if(this.getMethodName() == 'send'){ // 这里是云函数的前置方法,你可以在这里加入你需要逻辑
// 从配置中心获取是否需要销毁积分
if(config.spentScore){ // 判断否调用量本云对象的send方法
if (this.getMethodName() == 'send') {
/*先校验token(用户身份令牌)是否有效,并获得用户的_id*/ // 从配置中心获取是否需要销毁积分
// 获取客户端信息 if (config.spentScore) {
this.clientInfo = this.getClientInfo()
// console.log(this.clientInfo); /*先校验token(用户身份令牌)是否有效,并获得用户的_id*/
// 获取客户端信息
// 定义uni-id公共模块对象 this.clientInfo = this.getClientInfo()
this.uniIdCommon = uniIdCommon.createInstance({ // console.log(this.clientInfo);
clientInfo: this.clientInfo
}) // 定义uni-id公共模块对象
// 校验token(用户身份令牌)是否有效,并获得用户的_id this.uniIdCommon = uniIdCommon.createInstance({
let res = await this.uniIdCommon.checkToken(this.clientInfo.uniIdToken) clientInfo: this.clientInfo
if (res.errCode) { })
// 如果token校验出错,则抛出错误 // 校验token(用户身份令牌)是否有效,并获得用户的_id
throw res let res = await this.uniIdCommon.checkToken(this.clientInfo.uniIdToken)
}else{ if (res.errCode) {
// 通过token校验则,拿去用户id // 如果token校验出错,则抛出错误
this.current_uid = res.uid throw res
} } else {
/* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */ // 通过token校验则,拿去用户id
let {data:[{score}]} = await userscollection.doc(this.current_uid).field({'score':1}).get() this.current_uid = res.uid
console.log('score----',score); }
// 如果积分余额小于与uni-ai对话一次所需消耗的积分数 即 积分不足 则抛出错误提醒客户端 /* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */
if(score < config.spentScore){ let {
throw "insufficientScore" data: [{
} score
// 扣除对应的积分值 }]
await userscollection.doc(this.current_uid) } = await userscollection.doc(this.current_uid).field({
.update({ 'score': 1
score:db.command.inc(-1 * config.spentScore) }).get()
}) // 如果积分余额小于与uni-ai对话一次所需消耗的积分数 即 积分不足 则抛出错误提醒客户端
} if (score < config.spentScore) {
throw "insufficientScore"
// 从配置中心获取内容安全配置 }
// console.log('config.contentSecurity',config.contentSecurity); // 扣除对应的积分值
await userscollection.doc(this.current_uid)
.update({
score: db.command.inc(-1 * config.spentScore)
})
}
// 从配置中心获取内容安全配置
// console.log('config.contentSecurity',config.contentSecurity);
if (config.contentSecurity) { if (config.contentSecurity) {
// 引入uni-sec-check模块 const uniIdconfig = createConfig({
const UniSecCheck = safeRequire('uni-sec-check') pluginId: 'uni-id'
// 创建uniSecCheck对象 }).config()
const uniSecCheck = new UniSecCheck({ // console.log('uniIdconfig',uniIdconfig);
provider: 'mp-weixin',
requestId: this.getUniCloudRequestId() try{
}) let {appid,appsecret} = uniIdconfig["mp-weixin"].oauth.weixin
// 定义文本安全检测函数 if(!appid || !appsecret){
this.textSecCheck = async (content)=>{ throw '启用内容安全,但未配置微信小程序的appid、appsecret,详情参考:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#config'
// 获取sseChannel
let {sseChannel} = this.getParams()[0]||{}
// 如果存在sseChannel,则抛出错误
if(sseChannel){
throw {
errSubject: 'uni-ai-chat',
errCode: "sec-check",
errMsg: "流式响应模式,内容安全识别功能无效"
}
}
// 检测文本
const checkRes = await uniSecCheck.textSecCheck({
// 文本内容,不可超过500KB
content,
// 微信小程序端 开放的唯一用户标识符
// openid,
// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
scene:4,
// 接口版本号,可选1或2,但1的检测能力很弱 支持微信登录的项目,微信小程序端 可改用模式2 详情:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#%E4%BD%BF%E7%94%A8%E5%89%8D%E5%BF%85%E7%9C%8B
version:1
})
console.log('checkRes检测文本',checkRes);
// 如果检测到风险内容,则抛出错误
if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
console.error({
errCode: checkRes.errCode,
errMsg: '文字存在风险',
result: checkRes.result
});
throw "uni-sec-check:illegalData"
// 如果检测出错,则抛出错误
} else if (checkRes.errCode) {
console.log(`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`);
console.error({
errCode: checkRes.errCode,
errMsg: checkRes.errMsg,
result: checkRes.result
});
throw "uni-sec-check:illegalData"
} }
}catch(e){
throw "启用内容安全,但未配置微信小程序的appid、appsecret,详情参考:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#config"
} }
// 获取messages参数
let {messages} = this.getParams()[0]||{"messages":[]}
// 将messages中的content拼接成字符串
let contentString = messages.map(i=>i.content).join(' ')
console.log('contentString',contentString);
// 对contentString进行文本安全检测
await this.textSecCheck(contentString)
}
}
},
async _after(error, result) {
// 打印错误和结果
// console.log('_after',{error,result});
// 如果有错误
if(error){
// 如果是内容安全检测错误
if(error.errCode == 60004 || error == "uni-sec-check:illegalData" ) {
// 返回一个包含敏感内容提示和标记的响应体
return {
"data": {
"reply": "内容涉及敏感",
"illegal":true
},
"errCode": 0
}
}
// 其他符合响应体规范的错误,直接返回
else if(error.errCode && error.errMsg) {
return error
}
// 如果是积分不足错误
else if(error == 'insufficientScore'){
// 设置回复内容
let reply = "积分不足,请看完激励视频广告后再试"
// 获取sseChannel // 获取sseChannel
let {sseChannel} = this.getParams()[0]||{} let {
// 如果存在sseChannel sseChannel
if(sseChannel){ } = this.getParams()[0] || {}
// 反序列化sseChannel // 如果存在sseChannel,则抛出错误
const channel = uniCloud.deserializeSSEChannel(sseChannel) if (sseChannel) {
// 向sseChannel写入回复内容 throw {
await channel.write(reply) errSubject: 'uni-ai-chat',
// 结束sseChannel errCode: "sec-check",
await channel.end({ errMsg: "流式响应模式,内容安全识别功能无效"
"insufficientScore":true
})
}else{
// 如果不存在sseChannel 返回一个包含回复内容和标记的响应体
return {
"data": {
reply,
"insufficientScore":true
},
"errCode": 0
} }
} }
}else{
// 如果是其他错误 // 引入uni-sec-check模块
throw error // 直接抛出异常 const UniSecCheck = safeRequire('uni-sec-check')
} // 创建uniSecCheck对象
} const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin',
// 如果是send方法且开启了内容安全检测 requestId: this.getUniCloudRequestId()
if (this.getMethodName() == 'send' && config.contentSecurity) { })
try{ // 定义文本安全检测函数
// 对回复内容进行文本安全检测 this.textSecCheck = async (content) => {
await this.textSecCheck(result.data.reply) // 检测文本
}catch(e){ const checkRes = await uniSecCheck.textSecCheck({
// 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体 // 文本内容,不可超过500KB
content,
// 微信小程序端 开放的唯一用户标识符
// openid,
// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
scene: 4,
// 接口版本号,可选1或2,但1的检测能力很弱 支持微信登录的项目,微信小程序端 可改用模式2 详情:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#%E4%BD%BF%E7%94%A8%E5%89%8D%E5%BF%85%E7%9C%8B
version: 1
})
// console.log('checkRes检测文本', checkRes);
// 如果检测到风险内容,则抛出错误
if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
console.error({
errCode: checkRes.errCode,
errMsg: '文字存在风险',
result: checkRes.result
});
throw "uni-sec-check:illegalData"
// 如果检测出错,则抛出错误
} else if (checkRes.errCode) {
console.log(
`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`
);
console.error({
errCode: checkRes.errCode,
errMsg: checkRes.errMsg,
result: checkRes.result
});
throw "uni-sec-check:illegalData"
}
}
// 获取messages参数
let {
messages
} = this.getParams()[0] || {
"messages": []
}
// 将messages中的content拼接成字符串
let contentString = messages.map(i => i.content).join(' ')
console.log('contentString', contentString);
// 对contentString进行文本安全检测
await this.textSecCheck(contentString)
}
}
},
async _after(error, result) {
// 打印错误和结果
// console.log('_after',{error,result});
// 如果有错误
if (error) {
// 如果是内容安全检测错误
if (error.errCode == 60004 || error == "uni-sec-check:illegalData") {
// 返回一个包含敏感内容提示和标记的响应体
return {
"data": {
"reply": "内容涉及敏感",
"illegal": true
},
"errCode": 0
}
}
// 其他符合响应体规范的错误,直接返回
else if (error.errCode && error.errMsg) {
return error
}
// 如果是积分不足错误
else if (error == 'insufficientScore') {
// 设置回复内容
let reply = "积分不足,请看完激励视频广告后再试"
return { return {
"data": { "data": {
"reply": "内容涉及敏感", reply,
"illegal":true "insufficientScore": true
}, },
"errCode": 0 "errCode": 0
} }
} } else {
} // 如果是其他错误
// 返回处理后的结果 throw error // 直接抛出异常
return result }
}, }
// 发送消息
async send({ // 如果是send方法且开启了内容安全检测
// 消息内容 if (this.getMethodName() == 'send' && config.contentSecurity) {
messages, try {
// sse渠道对象 // 对回复内容进行文本安全检测
sseChannel, await this.textSecCheck(result.data.reply)
// 语言模型 } catch (e) {
llmModel // 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体
}) { return {
"data": {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 "reply": "内容涉及敏感",
// messages = [{ "illegal": true
// role: 'user', },
// content: 'uni-app是什么,20个字以内进行说明' "errCode": 0
// }] }
}
// 校验客户端提交的参数 }
// 检查消息是否符合规范 // 返回处理后的结果
let res = checkMessages(messages) return result
if (res.errCode) { },
throw new Error(res.errMsg) // 发送消息
} async send({
// 消息内容
messages,
// sse渠道对象
sseChannel,
// 语言模型
llmModel
}) {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
// messages = [{
// role: 'user',
// content: 'uni-app是什么,20个字以内进行说明'
// }]
// 校验客户端提交的消息参数是否符合规范
let res = checkMessages(messages)
if (res.errCode) {
throw new Error(res.errMsg)
}
// 向uni-ai发送消息 // 向uni-ai发送消息
// 调用chatCompletion函数,传入messages、sseChannel、llm参数 let promiseTaskList = []
let {llm,chatCompletionOptions} = config let promiseTask = new Promise((resolve, reject) => {
// 如果客户端传了llmModel 就覆盖配置的model chatCompletion(messages)
if(llmModel){ .then((res) => {
if(llmModel.includes('gpt-') && (llm && llm.provider != "openai")){ console.log(' 获取到问题的回答,res.reply:',res.reply); //非sse 时才有值,sse 时消息直接发往客户端了
throw new Error('错误:LLM的provider不是openai,但model却选了'+llmModel+';请参考文档:https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion 中model参数的说明') resolve({
} type:"reply",
chatCompletionOptions.model = llmModel data:res.reply
} });
// 计算消息总长度,判断是否需要总结
// console.log('messages',messages);
let lastTimeMessages = messages.slice(0,-1)
// console.log('lastTimeMessages',lastTimeMessages);
let needSummarize = lastTimeMessages.map(i => i.content).join('').length > 800,
// 总结的内容默认为 false 表示没有内容或者暂未拿到
summarizeData = false,
//成功拿到总结内容的回调函数列表
getSummarizeCallbackList = []
console.log('needSummarize',needSummarize);
if (needSummarize) {
// 获取总结
let replySummarize = getSummarize(lastTimeMessages)
.then((replySummarize)=>{
// console.log('replySummarize1',replySummarize);
summarizeData = replySummarize
getSummarizeCallbackList.forEach(fun=>fun())
}) })
.catch((error)=>{ .catch((error) => {
// 抛出错误 reject(error)
throw error
}) })
} });
promiseTaskList.push(promiseTask)
return await chatCompletion({
messages, //消息内容 // 拿到最后一次对话的消息内容(去掉最后一次,还没得到答案的提问)
sseChannel, //sse渠道对象 let lastTimeMessages = messages.slice(0, -1)
llm // 判断是否需要总结 (根据消息总长度是否大于800)
}) if (lastTimeMessages.map(i => i.content).join('').length > 800) {
// 获取总结
// chatCompletion函数:对话完成 lastTimeMessages.push({
async function chatCompletion({
// 消息列表
messages,
// 是否需要总结
summarize = false,
// sse渠道对象
sseChannel = false,
// 语言模型
llm
}) {
// console.log({llm,chatCompletionOptions});
// 获取语言模型管理器
const llmManager = uniCloud.ai.getLLMManager(llm)
// 调用chatCompletion方法,传入参数
// console.log('______messages',messages);
let res = await llmManager.chatCompletion({
...chatCompletionOptions,
messages,
stream: sseChannel !== false
})
// 如果存在sseChannel
if (sseChannel) {
let reply = ""
return new Promise((resolve, reject) => {
// 反序列化sseChannel
const channel = uniCloud.deserializeSSEChannel(sseChannel)
// 判断如果是open-ai按字返回,否则按行返回
if(llm && llm.provider && llm.provider == "openai"){
// 按字返回
res.on('message', async (message) => {
reply += message
await channel.write(message)
// console.log('---message----', message)
})
}else{
// 按行返回
res.on('line', async (line) => {
if(reply.length){
line = " \n\n " + line
}
reply += line
await channel.write(line)
// console.log('---line----', line)
})
}
// 结束返回
res.on('end', async () => {
// console.log('---end----',reply)
// 将回复内容添加到消息列表中
messages.push({
"content": reply,
"role": "assistant"
})
// 计算消息总长度
let totalTokens = messages.map(i => i.content).join('').length;
// console.log('totalTokens',totalTokens);
// 判断:是否有‘总结’需要带上
if(needSummarize){
if(!summarizeData){
// 如果需要等待
await new Promise((reject,resolve)=>{
getSummarizeCallbackList.push(reject)
})
// console.log('等到了总结',summarizeData);
} else{
// console.log('直接拿到总结',summarizeData);
}
// 结束sseChannel并返回总结
await channel.end({
"summarize": summarizeData
})
}else{
// 结束sseChannel
await channel.end()
}
// 返回处理结果
resolve({
errCode: 0
})
})
// 返回错误
res.on('error',async (error) => {
// 特殊处理 uni-ai默认服务商检测到内容涉及敏感的错误
if(error.errCode == "60004"){
await channel.write("内容涉及敏感")
// 结束sseChannel并返回 illegal:true 表示内容涉及敏感
await channel.end({
illegal: true
})
return resolve({
errCode: 0
})
}
console.error('---error----', error)
reject(error)
})
})
} else {
// 如果 不是正在总结
if (summarize == false) {
// 将回复内容添加到消息列表中
messages.push({
"content": res.reply,
"role": "assistant"
})
// 判断:是否有‘总结’需要带上
if(needSummarize){
if(!summarizeData){
// 如果需要等待
await new Promise((reject,resolve)=>{
getSummarizeCallbackList.push(reject)
})
// console.log('等到了总结',summarizeData);
}else{
// console.log('直接拿到总结',summarizeData);
}
res.summarize = summarizeData
}
}
// 如果存在错误
if(res.errCode){
// 抛出错误
throw res
}
// 返回处理结果
return {
data:res,
errCode: 0
}
}
}
//获总结
async function getSummarize(messages) {
let _messages = [...messages]
_messages.push({
"content": "请简要总结上述全部对话", "content": "请简要总结上述全部对话",
"role": "user" "role": "user"
}) })
// 调用chatCompletion函数,传入messages、summarize、stream、sseChannel参数 let promiseTask = new Promise((resolve, reject) => {
let res = await chatCompletion({ chatCompletion(lastTimeMessages,false).then((res) => {
// 消息内容 console.log('获取到总结,res:',res);
messages:_messages, resolve({
// 是否需要总结 type:"summarize",
summarize: true, data:res.reply
// 是否需要流式返回 });
stream: false, })
// sse渠道对象 .catch((error) => {
sseChannel: false , reject(error)
// 大语言模型配置 })
llm });
}) promiseTaskList.push(promiseTask)
//故意延迟看看,总结比答案晚,是否成功进入等的逻辑
//function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); }
// await sleep(10000)
// console.log('getSummarize',res);
// 返回总结的文字内容
return res.data.reply
} }
let promiseAllRes = await Promise.all(promiseTaskList)
console.log('Promise.all promiseRes',promiseAllRes);
res = {
data:{},
errCode:0
}
promiseAllRes.forEach(item=>{
switch (item.type){
case 'summarize':
res.data.summarize = item.data
break;
case 'reply':
res.data.reply = item.data
break;
default:
break;
}
})
return res
// chatCompletion函数:对话完成
/** /**
* 校验消息内容是否符合规范 * 校验消息内容是否符合规范
* @param {Array} messages - 消息列表 * @param {Array} messages - 消息列表
* @returns {Object} - 返回校验结果 * @param {Boolean} stream - 是否启用流式响应
*/ * @returns {Promise} - 返回结果
function checkMessages(messages) { */
try { function chatCompletion(messages,stream = true) {
// 如果messages未定义 // 从uni-config-center config获取 调用chatCompletion函数,传入messages、sseChannel、llm参数
if (messages === undefined) { let {
// 抛出异常 llm,
throw "messages为必传参数" chatCompletionOptions
// 如果messages不是数组 } = config
} else if (!Array.isArray(messages)) { // 如果客户端传了llmModel 就覆盖配置的model
// 抛出异常 if (llmModel) {
throw "参数messages的值类型必须是[object,object...]" if (llmModel.includes('gpt-') && (llm && llm.provider != "openai")) {
} else { throw new Error('错误:LLM的provider不是openai,但model却选了' + llmModel + ';请参考文档:https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion 中model参数的说明')
// 否则 遍历messages
messages.forEach(item => {
// 如果item不是对象
if (typeof item != 'object') {
// 抛出异常
throw "参数messages的值类型必须是[object,object...]"
}
// 定义itemRoleArr数组
let itemRoleArr = ["assistant", "user", "system"]
// 如果item的role属性不在itemRoleArr数组中
if (!itemRoleArr.includes(item.role)) {
// 抛出异常
throw "参数messages[{role}]的值只能是:" + itemRoleArr.join('')
}
// 如果item的content属性不是字符串
if (typeof item.content != 'string') {
// 抛出异常
throw "参数messages[{content}]的值类型必须是字符串"
}
})
}
// 返回校验结果
return {
errCode: 0,
}
// 捕获异常
} catch (errMsg) {
// 返回异常信息
return {
errSubject: 'ai-demo',
errCode: 'param-error',
errMsg
} }
chatCompletionOptions.model = llmModel
} }
}
} // console.log({llm,chatCompletionOptions});
// 获取语言模型管理器
const llmManager = uniCloud.ai.getLLMManager(llm)
// 调用chatCompletion方法,传入参数
// console.log('______messages',messages);
return llmManager.chatCompletion({
...chatCompletionOptions,
messages,
stream:stream && sseChannel !== false,
sseChannel
})
}
/**
* 校验消息内容是否符合规范
* @param {Array} messages - 消息列表
* @returns {Object} - 返回校验结果
*/
function checkMessages(messages) {
try {
// 如果messages未定义
if (messages === undefined) {
// 抛出异常
throw "messages为必传参数"
// 如果messages不是数组
} else if (!Array.isArray(messages)) {
// 抛出异常
throw "参数messages的值类型必须是[object,object...]"
} else {
// 否则 遍历messages
messages.forEach(item => {
// 如果item不是对象
if (typeof item != 'object') {
// 抛出异常
throw "参数messages的值类型必须是[object,object...]"
}
// 定义itemRoleArr数组
let itemRoleArr = ["assistant", "user", "system"]
// 如果item的role属性不在itemRoleArr数组中
if (!itemRoleArr.includes(item.role)) {
// 抛出异常
throw "参数messages[{role}]的值只能是:" + itemRoleArr.join('')
}
// 如果item的content属性不是字符串
if (typeof item.content != 'string') {
// 抛出异常
throw "参数messages[{content}]的值类型必须是字符串"
}
})
}
// 返回校验结果
return {
errCode: 0,
}
// 捕获异常
} catch (errMsg) {
// 返回异常信息
return {
errSubject: 'ai-demo',
errCode: 'param-error',
errMsg
}
}
}
}
} }
\ No newline at end of file
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
"ad":3, "ad":3,
"price":3 "price":3
}, },
"llm":{}, "llm":{
"provider": "azure"
},
"chatCompletionOptions":{ "chatCompletionOptions":{
"tokensToGenerate":512 "tokensToGenerate":512
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册