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

1.2.0

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