提交 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: { isLastMsg: {
type: Number, type: Boolean,
default () { default () {
return false return false
} }
}, },
isLastMsg: { msg: {
type: Boolean, type: Object,
default () { default () {
return false 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;
...@@ -333,6 +299,7 @@ ...@@ -333,6 +299,7 @@
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';
// 收集所有执行云对象的任务列表 // 收集所有执行云对象的任务列表
...@@ -90,13 +86,12 @@ ...@@ -90,13 +86,12 @@
// 键盘的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,7 +116,7 @@ ...@@ -122,7 +116,7 @@
return true return true
} }
// 如果消息列表长度为奇数,则禁用输入框 // 如果消息列表长度为奇数,则禁用输入框
return !!(this.msgLength && this.msgLength % 2 !== 0) return !!(this.msgList.length && this.msgList.length % 2 !== 0)
}, },
// 输入框占位符文本 // 输入框占位符文本
placeholderText() { placeholderText() {
...@@ -153,15 +147,6 @@ ...@@ -153,15 +147,6 @@
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
...@@ -397,6 +377,7 @@ ...@@ -397,6 +377,7 @@
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
} }
// #endif
// #ifdef VUE2 if (this.msgList[index].isAi) {
this.$set(msgList[index], "isDelete", true) index -= 1
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 this.msgList.splice(index,2)
}, },
async beforeSendMsg() { async beforeSendMsg() {
if (this.inputBoxDisabled) { if (this.inputBoxDisabled) {
...@@ -551,10 +526,15 @@ ...@@ -551,10 +526,15 @@
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)
...@@ -564,7 +544,7 @@ ...@@ -564,7 +544,7 @@
// 将带总结的消息的 内容 更换成 总结 // 将带总结的消息的 内容 更换成 总结
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)
...@@ -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)
}else{
// 更新最后一条消息
this.updateLastMsg(lastMsg => {
// 如果e包含illegal属性
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 // 结束流式响应 将流式响应计数值 设置为 0
this.sseIndex = 0 this.sseIndex = 0
// 滚动窗口以显示最新的一条消息 state = {sse:0,co:0}
this.showLastMsg() }else{
// console.log(1,state);
}
}) })
await sseChannel.open() // 等待通道开启 await sseChannel.open() // 等待通道开启
} }
...@@ -673,8 +634,7 @@ ...@@ -673,8 +634,7 @@
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
} }
...@@ -682,58 +642,76 @@ ...@@ -682,58 +642,76 @@
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
// 特殊处理 - start
if(this.enableStream == false && !content){
illegal = true
content = "内容涉及敏感"
}
// 特殊处理 - end
if (illegal) { if (illegal) {
// 如果返回的数据包含illegal属性,就更新最后一条消息的illegal属性为true // 如果返回的数据包含illegal属性,就更新最后一条消息(用户输入的问题)的illegal属性为true
this.updateLastMsg({ this.updateLastMsg({
// 添加消息涉敏标记
illegal: true illegal: true
}) })
} }
// 非流式模式 或者流式模式,但列表还没有数据且已经进入异常的情况下
if (this.enableStream == false || this.sseIndex == 0 && (illegal || insufficientScore)) {
// 将从云端接收到的消息添加到消息列表中 // 将从云端接收到的消息添加到消息列表中
this.msgList.push({ this.msgList.push({
// 添加消息创建时间 // 消息创建时间
create_time: Date.now(), create_time: Date.now(),
// 标记消息为来自AI机器人 // 标记消息为来自AI机器人
isAi: true, isAi: true,
// 添加消息内容 // 消息内容
content, content,
// 添加消息分数不足标记 // 消息是否涉敏标记
insufficientScore, illegal,
// 添加消息涉敏标记 // 本地对话是否因积分不足而终止
illegal insufficientScore
}) })
}
// console.log(res, res.reply);
// 如果回调包含总结的内容,就设置总结 // 如果回调包含总结的内容,就设置总结
if(summarize){ if(summarize){
console.log(' 拿到总结',summarize);
this.setSummarize(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.$nextTick(() => {
this.showLastMsg() this.showLastMsg()
}) })
} else {
// 处理 sseChannel没结束 云函数提前结束的情况
sseChannel.close()
this.sseIndex = 0
}
}, },
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 {
safeRequire,
checkContentSecurityEnable
} = require('./utils')
// 引入uni-config-center模块,并创建config对象 // 引入uni-config-center模块,并创建config对象
const createConfig = safeRequire('uni-config-center') const createConfig = safeRequire('uni-config-center')
const config = createConfig({ const config = createConfig({
...@@ -16,13 +19,13 @@ const uniIdCommon = require('uni-id-common') ...@@ -16,13 +19,13 @@ const uniIdCommon = require('uni-id-common')
module.exports = { module.exports = {
_before:async function() { _before: async function() {
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑 // 这里是云函数的前置方法,你可以在这里加入你需要逻辑
// 判断否调用量本云对象的send方法 // 判断否调用量本云对象的send方法
if(this.getMethodName() == 'send'){ if (this.getMethodName() == 'send') {
// 从配置中心获取是否需要销毁积分 // 从配置中心获取是否需要销毁积分
if(config.spentScore){ if (config.spentScore) {
/*先校验token(用户身份令牌)是否有效,并获得用户的_id*/ /*先校验token(用户身份令牌)是否有效,并获得用户的_id*/
// 获取客户端信息 // 获取客户端信息
...@@ -38,46 +41,68 @@ module.exports = { ...@@ -38,46 +41,68 @@ module.exports = {
if (res.errCode) { if (res.errCode) {
// 如果token校验出错,则抛出错误 // 如果token校验出错,则抛出错误
throw res throw res
}else{ } else {
// 通过token校验则,拿去用户id // 通过token校验则,拿去用户id
this.current_uid = res.uid this.current_uid = res.uid
} }
/* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */ /* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */
let {data:[{score}]} = await userscollection.doc(this.current_uid).field({'score':1}).get() let {
console.log('score----',score); data: [{
score
}]
} = await userscollection.doc(this.current_uid).field({
'score': 1
}).get()
// 如果积分余额小于与uni-ai对话一次所需消耗的积分数 即 积分不足 则抛出错误提醒客户端 // 如果积分余额小于与uni-ai对话一次所需消耗的积分数 即 积分不足 则抛出错误提醒客户端
if(score < config.spentScore){ if (score < config.spentScore) {
throw "insufficientScore" throw "insufficientScore"
} }
// 扣除对应的积分值 // 扣除对应的积分值
await userscollection.doc(this.current_uid) await userscollection.doc(this.current_uid)
.update({ .update({
score:db.command.inc(-1 * config.spentScore) score: db.command.inc(-1 * config.spentScore)
}) })
} }
// 从配置中心获取内容安全配置 // 从配置中心获取内容安全配置
// console.log('config.contentSecurity',config.contentSecurity); // 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'
}
}catch(e){
throw "启用内容安全,但未配置微信小程序的appid、appsecret,详情参考:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#config"
}
// 获取sseChannel // 获取sseChannel
let {sseChannel} = this.getParams()[0]||{} let {
sseChannel
} = this.getParams()[0] || {}
// 如果存在sseChannel,则抛出错误 // 如果存在sseChannel,则抛出错误
if(sseChannel){ if (sseChannel) {
throw { throw {
errSubject: 'uni-ai-chat', errSubject: 'uni-ai-chat',
errCode: "sec-check", errCode: "sec-check",
errMsg: "流式响应模式,内容安全识别功能无效" errMsg: "流式响应模式,内容安全识别功能无效"
} }
} }
// 引入uni-sec-check模块
const UniSecCheck = safeRequire('uni-sec-check')
// 创建uniSecCheck对象
const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin',
requestId: this.getUniCloudRequestId()
})
// 定义文本安全检测函数
this.textSecCheck = async (content) => {
// 检测文本 // 检测文本
const checkRes = await uniSecCheck.textSecCheck({ const checkRes = await uniSecCheck.textSecCheck({
// 文本内容,不可超过500KB // 文本内容,不可超过500KB
...@@ -85,11 +110,11 @@ module.exports = { ...@@ -85,11 +110,11 @@ module.exports = {
// 微信小程序端 开放的唯一用户标识符 // 微信小程序端 开放的唯一用户标识符
// openid, // openid,
// 场景值(1 资料;2 评论;3 论坛;4 社交日志) // 场景值(1 资料;2 评论;3 论坛;4 社交日志)
scene: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 // 接口版本号,可选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 version: 1
}) })
console.log('checkRes检测文本',checkRes); // console.log('checkRes检测文本', checkRes);
// 如果检测到风险内容,则抛出错误 // 如果检测到风险内容,则抛出错误
if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
console.error({ console.error({
...@@ -100,7 +125,9 @@ module.exports = { ...@@ -100,7 +125,9 @@ module.exports = {
throw "uni-sec-check:illegalData" throw "uni-sec-check:illegalData"
// 如果检测出错,则抛出错误 // 如果检测出错,则抛出错误
} else if (checkRes.errCode) { } else if (checkRes.errCode) {
console.log(`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`); console.log(
`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`
);
console.error({ console.error({
errCode: checkRes.errCode, errCode: checkRes.errCode,
errMsg: checkRes.errMsg, errMsg: checkRes.errMsg,
...@@ -111,10 +138,14 @@ module.exports = { ...@@ -111,10 +138,14 @@ module.exports = {
} }
// 获取messages参数 // 获取messages参数
let {messages} = this.getParams()[0]||{"messages":[]} let {
messages
} = this.getParams()[0] || {
"messages": []
}
// 将messages中的content拼接成字符串 // 将messages中的content拼接成字符串
let contentString = messages.map(i=>i.content).join(' ') let contentString = messages.map(i => i.content).join(' ')
console.log('contentString',contentString); console.log('contentString', contentString);
// 对contentString进行文本安全检测 // 对contentString进行文本安全检测
await this.textSecCheck(contentString) await this.textSecCheck(contentString)
} }
...@@ -124,49 +155,34 @@ module.exports = { ...@@ -124,49 +155,34 @@ module.exports = {
// 打印错误和结果 // 打印错误和结果
// console.log('_after',{error,result}); // console.log('_after',{error,result});
// 如果有错误 // 如果有错误
if(error){ if (error) {
// 如果是内容安全检测错误 // 如果是内容安全检测错误
if(error.errCode == 60004 || error == "uni-sec-check:illegalData" ) { if (error.errCode == 60004 || error == "uni-sec-check:illegalData") {
// 返回一个包含敏感内容提示和标记的响应体 // 返回一个包含敏感内容提示和标记的响应体
return { return {
"data": { "data": {
"reply": "内容涉及敏感", "reply": "内容涉及敏感",
"illegal":true "illegal": true
}, },
"errCode": 0 "errCode": 0
} }
} }
// 其他符合响应体规范的错误,直接返回 // 其他符合响应体规范的错误,直接返回
else if(error.errCode && error.errMsg) { else if (error.errCode && error.errMsg) {
return error return error
} }
// 如果是积分不足错误 // 如果是积分不足错误
else if(error == 'insufficientScore'){ else if (error == 'insufficientScore') {
// 设置回复内容 // 设置回复内容
let reply = "积分不足,请看完激励视频广告后再试" let reply = "积分不足,请看完激励视频广告后再试"
// 获取sseChannel
let {sseChannel} = this.getParams()[0]||{}
// 如果存在sseChannel
if(sseChannel){
// 反序列化sseChannel
const channel = uniCloud.deserializeSSEChannel(sseChannel)
// 向sseChannel写入回复内容
await channel.write(reply)
// 结束sseChannel
await channel.end({
"insufficientScore":true
})
}else{
// 如果不存在sseChannel 返回一个包含回复内容和标记的响应体
return { return {
"data": { "data": {
reply, reply,
"insufficientScore":true "insufficientScore": true
}, },
"errCode": 0 "errCode": 0
} }
} } else {
}else{
// 如果是其他错误 // 如果是其他错误
throw error // 直接抛出异常 throw error // 直接抛出异常
} }
...@@ -174,15 +190,15 @@ module.exports = { ...@@ -174,15 +190,15 @@ module.exports = {
// 如果是send方法且开启了内容安全检测 // 如果是send方法且开启了内容安全检测
if (this.getMethodName() == 'send' && config.contentSecurity) { if (this.getMethodName() == 'send' && config.contentSecurity) {
try{ try {
// 对回复内容进行文本安全检测 // 对回复内容进行文本安全检测
await this.textSecCheck(result.data.reply) await this.textSecCheck(result.data.reply)
}catch(e){ } catch (e) {
// 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体 // 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体
return { return {
"data": { "data": {
"reply": "内容涉及敏感", "reply": "内容涉及敏感",
"illegal":true "illegal": true
}, },
"errCode": 0 "errCode": 0
} }
...@@ -200,232 +216,115 @@ module.exports = { ...@@ -200,232 +216,115 @@ module.exports = {
// 语言模型 // 语言模型
llmModel llmModel
}) { }) {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
// messages = [{ // messages = [{
// role: 'user', // role: 'user',
// content: 'uni-app是什么,20个字以内进行说明' // content: 'uni-app是什么,20个字以内进行说明'
// }] // }]
// 校验客户端提交的参数 // 校验客户端提交的消息参数是否符合规范
// 检查消息是否符合规范
let res = checkMessages(messages) let res = checkMessages(messages)
if (res.errCode) { if (res.errCode) {
throw new Error(res.errMsg) 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)=>{
// 抛出错误
throw error
}) })
} .catch((error) => {
reject(error)
return await chatCompletion({
messages, //消息内容
sseChannel, //sse渠道对象
llm
}) })
});
promiseTaskList.push(promiseTask)
// chatCompletion函数:对话完成 // 拿到最后一次对话的消息内容(去掉最后一次,还没得到答案的提问)
async function chatCompletion({ let lastTimeMessages = messages.slice(0, -1)
// 消息列表 // 判断是否需要总结 (根据消息总长度是否大于800)
messages, if (lastTimeMessages.map(i => i.content).join('').length > 800) {
// 是否需要总结 // 获取总结
summarize = false, lastTimeMessages.push({
// sse渠道对象 "content": "请简要总结上述全部对话",
sseChannel = false, "role": "user"
// 语言模型
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
}) })
let promiseTask = new Promise((resolve, reject) => {
// 如果存在sseChannel chatCompletion(lastTimeMessages,false).then((res) => {
if (sseChannel) { console.log('获取到总结,res:',res);
let reply = "" resolve({
return new Promise((resolve, reject) => { type:"summarize",
// 反序列化sseChannel data:res.reply
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{ .catch((error) => {
// 按行返回 reject(error)
res.on('line', async (line) => {
if(reply.length){
line = " \n\n " + line
}
reply += line
await channel.write(line)
// console.log('---line----', line)
}) })
});
promiseTaskList.push(promiseTask)
} }
// 结束返回
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);
// 判断:是否有‘总结’需要带上 let promiseAllRes = await Promise.all(promiseTaskList)
if(needSummarize){ console.log('Promise.all promiseRes',promiseAllRes);
if(!summarizeData){ res = {
// 如果需要等待 data:{},
await new Promise((reject,resolve)=>{ errCode:0
getSummarizeCallbackList.push(reject) }
}) promiseAllRes.forEach(item=>{
// console.log('等到了总结',summarizeData); switch (item.type){
} else{ case 'summarize':
// console.log('直接拿到总结',summarizeData); res.data.summarize = item.data
break;
case 'reply':
res.data.reply = item.data
break;
default:
break;
} }
// 结束sseChannel并返回总结
await channel.end({
"summarize": summarizeData
}) })
}else{ return res
// 结束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"
})
// 判断:是否有‘总结’需要带上 // chatCompletion函数:对话完成
if(needSummarize){ /**
if(!summarizeData){ * 校验消息内容是否符合规范
// 如果需要等待 * @param {Array} messages - 消息列表
await new Promise((reject,resolve)=>{ * @param {Boolean} stream - 是否启用流式响应
getSummarizeCallbackList.push(reject) * @returns {Promise} - 返回结果
}) */
// console.log('等到了总结',summarizeData); function chatCompletion(messages,stream = true) {
}else{ // 从uni-config-center config获取 调用chatCompletion函数,传入messages、sseChannel、llm参数
// console.log('直接拿到总结',summarizeData); let {
} llm,
res.summarize = summarizeData chatCompletionOptions
} } = config
} // 如果客户端传了llmModel 就覆盖配置的model
// 如果存在错误 if (llmModel) {
if(res.errCode){ if (llmModel.includes('gpt-') && (llm && llm.provider != "openai")) {
// 抛出错误 throw new Error('错误:LLM的provider不是openai,但model却选了' + llmModel + ';请参考文档:https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion 中model参数的说明')
throw res
}
// 返回处理结果
return {
data:res,
errCode: 0
}
} }
chatCompletionOptions.model = llmModel
} }
// console.log({llm,chatCompletionOptions});
//获总结 // 获取语言模型管理器
async function getSummarize(messages) { const llmManager = uniCloud.ai.getLLMManager(llm)
let _messages = [...messages] // 调用chatCompletion方法,传入参数
_messages.push({ // console.log('______messages',messages);
"content": "请简要总结上述全部对话", return llmManager.chatCompletion({
"role": "user" ...chatCompletionOptions,
}) messages,
// 调用chatCompletion函数,传入messages、summarize、stream、sseChannel参数 stream:stream && sseChannel !== false,
let res = await chatCompletion({ sseChannel
// 消息内容
messages:_messages,
// 是否需要总结
summarize: true,
// 是否需要流式返回
stream: false,
// sse渠道对象
sseChannel: false ,
// 大语言模型配置
llm
}) })
//故意延迟看看,总结比答案晚,是否成功进入等的逻辑
//function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); }
// await sleep(10000)
// console.log('getSummarize',res);
// 返回总结的文字内容
return res.data.reply
} }
/** /**
* 校验消息内容是否符合规范 * 校验消息内容是否符合规范
* @param {Array} messages - 消息列表 * @param {Array} messages - 消息列表
......
...@@ -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.
先完成此消息的编辑!
想要评论请 注册