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

添加注释

上级 2713e2a2
...@@ -9,25 +9,26 @@ ...@@ -9,25 +9,26 @@
</view> </view>
</template> </template>
<script> <script>
export default { export default {
name: "uni-ad-rewarded-video", name: "uni-ad-rewarded-video",
data() { data() {
return {} return {}
}, },
props: { props: {
adpid: { // 广告位唯一标识
type: String, adpid: {
default(){ type: String,
return '1053355918' default(){
} return ''
}, }
},
}, },
computed: { computed: {
// 回调URL // 回调URL
urlCallback() { urlCallback() {
let uid = uniCloud.getCurrentUserInfo().uid // 拿到当前用户的id值
let uid = uniCloud.getCurrentUserInfo().uid
return { return {
userId: uid, userId: uid,
extra: JSON.stringify({ extra: JSON.stringify({
...@@ -38,7 +39,7 @@ ...@@ -38,7 +39,7 @@
} }
}, },
}, },
async mounted() { async mounted() {
}, },
methods: { methods: {
callAd() { callAd() {
...@@ -46,46 +47,46 @@ ...@@ -46,46 +47,46 @@
// 如果在浏览器中,则提示需在App或小程序中操作 // 如果在浏览器中,则提示需在App或小程序中操作
return uni.showModal({ return uni.showModal({
content: '浏览器不支持广告播放, 请在App或小程序中操作', content: '浏览器不支持广告播放, 请在App或小程序中操作',
showCancel: false, showCancel: false,
confirmText:"知道了" confirmText:"知道了"
}) })
// #endif // #endif
// 如果用户状态无效,则提示需要登录 // 如果用户状态无效,则提示需要登录
if (uniCloud.getCurrentUserInfo().tokenExpired < Date.now()) { if (uniCloud.getCurrentUserInfo().tokenExpired < Date.now()) {
uni.showModal({ uni.showModal({
content: '请登录后操作', content: '请登录后操作',
success: ({ success: ({
confirm confirm
}) => { }) => {
if(confirm){ if(confirm){
// 登录跳转URL 请根据实际情况修改 // 登录跳转URL 请根据实际情况修改
// 获取当前页面信息 // 获取当前页面信息
let pages = getCurrentPages() let pages = getCurrentPages()
let currentPage = pages[pages.length - 1] let currentPage = pages[pages.length - 1]
let url = '/uni_modules/uni-id-pages/pages/login/login-withoutpwd' + let url = '/uni_modules/uni-id-pages/pages/login/login-withoutpwd' +
(currentPage.route ? ('?uniIdRedirectUrl=' + currentPage.route) : '') (currentPage.route ? ('?uniIdRedirectUrl=' + currentPage.route) : '')
uni.navigateTo({ uni.navigateTo({
url, url,
complete(e) { complete(e) {
console.log(e); console.log(e);
} }
}); });
} }
} }
}) })
return return
} }
// 显示广告 // 显示广告
this.$refs.rewardedVideo.show() this.$refs.rewardedVideo.show()
}, },
onAdLoad(e) { onAdLoad(e) {
console.log('onAdLoad', e) console.log('onAdLoad', e)
}, },
onAdClose(e) { onAdClose(e) {
console.log('onAdClose', e) console.log('onAdClose', e)
this.$emit('onAdClose',e) this.$emit('onAdClose',e) // 触发onAdClose事件
}, },
onAdError(e) { onAdError(e) {
console.log('onAdError', e) console.log('onAdError', e)
......
...@@ -12,18 +12,23 @@ ...@@ -12,18 +12,23 @@
</template> </template>
<script> <script>
import MarkdownIt from '@/lib/markdown-it.min.js'; // 引入markdown-it库
import MarkdownIt from '@/lib/markdown-it.min.js';
// hljs是由 Highlight.js 经兼容性修改后的文件,请勿直接升级。否则会造成uni-app-vue3-Android下有兼容问题
import hljs from "@/lib/highlight/highlight-uni.min.js"; // hljs是由 Highlight.js 经兼容性修改后的文件,请勿直接升级。否则会造成uni-app-vue3-Android下有兼容问题
import hljs from "@/lib/highlight/highlight-uni.min.js";
import parseHtml from '@/lib/html-parser.js';
// 引入html-parser.js库
import parseHtml from '@/lib/html-parser.js';
// console.log('hljs--',hljs); // console.log('hljs--',hljs);
// console.log('hljs--',hljs.getLanguage('js')); // console.log('hljs--',hljs.getLanguage('js'));
// 初始化 MarkdownIt库
const markdownIt = MarkdownIt({ const markdownIt = MarkdownIt({
// 在源码中启用 HTML 标签
html: true, html: true,
// 如果结果以 <pre ... 开头内部包装器则会跳过
highlight: function(str, lang) { highlight: function(str, lang) {
// if (lang && hljs.getLanguage(lang)) { // if (lang && hljs.getLanguage(lang)) {
// console.error('lang', lang) // console.error('lang', lang)
...@@ -49,7 +54,9 @@ ...@@ -49,7 +54,9 @@
name: "msg", name: "msg",
data() { data() {
return { return {
// 悬浮的复制按钮的左边距
left: "-100px", left: "-100px",
// 悬浮的复制按钮的上边距
top: "-100px" top: "-100px"
}; };
}, },
...@@ -58,32 +65,37 @@ ...@@ -58,32 +65,37 @@
// web端限制不选中文字时出现系统右键菜单 // web端限制不选中文字时出现系统右键菜单
let richTextBox = this.$refs['rich-text-box'] let richTextBox = this.$refs['rich-text-box']
if (richTextBox) { if (richTextBox) {
// 监听鼠标右键事件
richTextBox.$el.addEventListener('contextmenu', (e) => { richTextBox.$el.addEventListener('contextmenu', (e) => {
// 判断是否选中了文字内容,如果没有则限制系统默认行为(禁止弹出右键菜单)
if (!document.getSelection().toString()) { if (!document.getSelection().toString()) {
// console.log(e); // console.log(e);
// 设置悬浮的复制按钮的坐标值
this.top = e.y + 'px' this.top = e.y + 'px'
this.left = e.x + 'px' this.left = e.x + 'px'
// console.log(e.x); // console.log(e.x);
// console.log(e.y); // console.log(e.y);
// 禁止系统默认行为(禁止弹出右键菜单)
e.preventDefault() e.preventDefault()
} }
}) })
} }
// 监听全局点击事件,隐藏悬浮的复制按钮的坐标
document.addEventListener('click', () => { document.addEventListener('click', () => {
this.left = "-100px" this.left = "-100px"
}) })
// #endif // #endif
}, },
props: { props: {
// 传入的markdown内容
md: { md: {
type: String, type: String,
default () { default () {
return '' return ''
} }
}, },
// 是否显示鼠标闪烁的效果
showCursor: { showCursor: {
type: [Boolean, Number], type: [Boolean, Number],
default () { default () {
...@@ -92,26 +104,31 @@ ...@@ -92,26 +104,31 @@
} }
}, },
computed: { computed: {
// 修改转换结果的html值 用于正确给界面增加鼠标闪烁的效果
html() { html() {
if(this.md.split("```").length%2){ // 判断markdown中代码块标识符的数量是否为偶数
return markdownIt.render(this.md + ' \n <span class="cursor">|</span>'); if(this.md.split("```").length%2){
}else{ return markdownIt.render(this.md + ' \n <span class="cursor">|</span>');
return markdownIt.render(this.md) + ' \n <span class="cursor">|</span>'; }else{
return markdownIt.render(this.md) + ' \n <span class="cursor">|</span>';
} }
}, },
nodes() { nodes() {
// return this.html // return this.html
// console.log('parseHtml(this.html)',parseHtml(this.html)); // console.log('parseHtml(this.html)',parseHtml(this.html));
// HTML String 类型转换 避免内部转换导致的性能下降。
return parseHtml(this.html) return parseHtml(this.html)
} }
}, },
methods: { methods: {
// #ifdef H5 // #ifdef H5
// 复制文本内容到系统剪切板
copy() { copy() {
uni.setClipboardData({ uni.setClipboardData({
data: this.md, data: this.md,
showToast: false, showToast: false,
}) })
// 设置悬浮的复制按钮的坐标值,使其隐藏
this.left = "-100px" this.left = "-100px"
} }
// #endif // #endif
...@@ -120,7 +137,7 @@ ...@@ -120,7 +137,7 @@
</script> </script>
<style lang="scss"> <style lang="scss">
/* #ifndef VUE3 && APP-PLUS */ /* #ifndef VUE3 && APP-PLUS */
@import "@/components/uni-ai-msg/uni-ai-msg.scss"; @import "@/components/uni-ai-msg/uni-ai-msg.scss";
/* #endif */ /* #endif */
</style> </style>
\ No newline at end of file
{ {
"id": "uni-ai-chat", "id": "uni-ai-chat",
"name": "uni-ai-chat", "name": "uni-ai-chat",
"version": "1.0.3", "version": "1.0.4",
"description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体", "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
......
<template> <template>
<view class="page"> <view class="page">
<view class="container"> <view class="container">
<view v-if="isWidescreen" class="header">uni-ai-chat</view> <view v-if="isWidescreen" class="header">uni-ai-chat</view>
<text class="noData" v-if="msgList.length === 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">
<view v-for="(msg,index) in msgList" class="msg-item" :key="index"> <view v-for="(msg,index) in msgList" class="msg-item" :key="index">
<view class="create_time-box"> <view class="create_time-box">
<uni-dateformat class="create_time" :date="msg.create_time" format="MM/dd hh:mm:ss"></uni-dateformat> <uni-dateformat class="create_time" :date="msg.create_time" format="MM/dd hh:mm:ss"></uni-dateformat>
</view> </view>
<view :class="{reverse:!msg.isAi}"> <view :class="{reverse:!msg.isAi}">
<view class="userInfo"> <view class="userInfo">
<image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image> <image class="avatar" :src="msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'" mode="widthFix"></image>
</view> </view>
<view class="content"> <view class="content">
<!-- <text class="copy" @click="copy">复制</text> --> <!-- <text class="copy" @click="copy">复制</text> -->
<uni-ai-msg :md="msg.content" :show-cursor="index == msgList.length-1 && msg.isAi && sseIndex"></uni-ai-msg> <uni-ai-msg :md="msg.content" :show-cursor="index == msgList.length-1 && msg.isAi && sseIndex"></uni-ai-msg>
<view v-if="index == msgList.length-1 && adpid && msg.insufficientScore"> <view v-if="index == msgList.length-1 && adpid && msg.insufficientScore">
<uni-ad-rewarded-video :adpid="adpid" @onAdClose="onAdClose"></uni-ad-rewarded-video> <uni-ad-rewarded-video :adpid="adpid" @onAdClose="onAdClose"></uni-ad-rewarded-video>
</view> </view>
</view> </view>
<uni-icons v-if="index == msgList.length-1 && !msg.isAi && msg.state != 100 && msgStateIcon(msg)" <uni-icons v-if="index == msgList.length-1 && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
@click="msg.state == -100 ? retriesSendMsg() : ''" :color="msg.state===0?'#999':'#d22'" @click="msg.state == -100 ? retriesSendMsg() : ''" :color="msg.state===0?'#999':'#d22'"
:type="msgStateIcon(msg)" class="msgStateIcon"> :type="msgStateIcon(msg)" class="msgStateIcon">
</uni-icons> </uni-icons>
</view> </view>
</view> </view>
<view class="tip-ai-ing" v-if="msgList.length && msgList.length%2 !== 0"> <view class="tip-ai-ing" v-if="msgList.length && msgList.length%2 !== 0">
<text>uni-ai正在思考中...</text> <text>uni-ai正在思考中...</text>
<view v-if="NODE_ENV == 'development' && !stream"> <view v-if="NODE_ENV == 'development' && !enableStream">
如需提速,请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html" text="[流式响应]"></uni-link> 如需提速,请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html" text="[流式响应]"></uni-link>
</view> </view>
</view> </view>
<view id="last-msg-item"></view> <view id="last-msg-item"></view>
</scroll-view> </scroll-view>
<view class="foot-box"> <view class="foot-box">
<view class="menu" v-if="isWidescreen"> <view class="menu" v-if="isWidescreen">
<view class="trash menu-item"> <view class="trash menu-item">
<image @click="clear" src="@/static/remove.png" mode="heightFix"></image> <image @click="clear" src="@/static/remove.png" mode="heightFix"></image>
</view> </view>
</view> </view>
<view class="foot-box-content"> <view class="foot-box-content">
<view v-if="!isWidescreen" class="trash"> <view v-if="!isWidescreen" class="trash">
<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons> <uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons>
</view> </view>
<view class="textarea-box"> <view class="textarea-box">
<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen" :disabled="inputBoxDisabled" <textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
:placeholder="placeholderText" :maxlength="-1" placeholder-class="input-placeholder"></textarea> @keyup.shift="onKeyup('shift')" @keydown.shift="onKeydown('shift')" @keydown.enter="onKeydown('enter')"
</view> :disabled="inputBoxDisabled" :placeholder="placeholderText" :maxlength="-1"
<view class="send-btn-box"> placeholder-class="input-placeholder"></textarea>
<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text> </view>
<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send" type="primary">发送</button> <view class="send-btn-box">
</view> <text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
</view> <button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send" type="primary">发送</button>
</view> </view>
</view> </view>
</view> </view>
</template> </view>
</view>
<script> </template>
import config from '@/config.js';
const {adpid} = config <script>
let sseChannel = false; // 引入配置文件
// 是否通过回调,当用户点击清空后应当跳过前一次请求的回调 import config from '@/config.js';
// 获取广告id
const {
adpid
} = config
// 初始化sse通道
let sseChannel = false;
// 是否通过回调,当用户点击清空后应当跳过前一次请求的回调
let skip_callback = false; let skip_callback = false;
export default {
data() { // 键盘的shift键是否被按下
return { let shiftKeyPressed = false
scrollIntoView: "", export default {
msgList: [], data() {
content: "", return {
sseIndex: 0, // 使聊天窗口滚动到指定元素id的值
stream:true, scrollIntoView: "",
isWidescreen:false, // 消息列表数据
adpid msgList: [],
} // 输入框的消息内容
}, content: "",
computed: { // 记录流式响应次数
inputBoxDisabled() { sseIndex: 0,
if (this.sseIndex !== 0) { // 是否启用流式响应模式
return true enableStream: true,
} // 当前屏幕是否为宽屏
return !!(this.msgList.length && this.msgList.length%2 !== 0) isWidescreen: false,
}, // 广告位id
placeholderText() { adpid
if (this.inputBoxDisabled) { }
return 'uni-ai正在回复中' },
} else { computed: {
// #ifdef H5 // 输入框是否禁用
return window.innerWidth > 960 ? '请输入内容,ctrl + enter 发送' : '请输入要发给uni-ai的内容' inputBoxDisabled() {
// #endif // 如果正在等待流式响应,则禁用输入框
return '请输入要发给uni-ai的内容' if (this.sseIndex !== 0) {
} return true
}, }
NODE_ENV(){ // 如果消息列表长度为奇数,则禁用输入框
return process.env.NODE_ENV return !!(this.msgList.length && this.msgList.length % 2 !== 0)
} },
}, // 输入框占位符文本
watch: { placeholderText() {
msgList:{ // 如果输入框被禁用,则显示“uni-ai正在回复中”
handler(msgList) { if (this.inputBoxDisabled) {
uni.setStorageSync('uni-ai-msg', msgList) return 'uni-ai正在回复中'
}, } else {
deep:true // #ifdef H5
} // 如果屏幕宽度大于960,则显示“请输入内容,ctrl + enter 发送”,否则显示“请输入要发给uni-ai的内容”
}, return window.innerWidth > 960 ? '请输入内容,ctrl + enter 发送' : '请输入要发给uni-ai的内容'
async mounted() { // #endif
if(this.adpid && uniCloud.getCurrentUserInfo().tokenExpired > Date.now()){ return '请输入要发给uni-ai的内容'
let db = uniCloud.databaseForJQL(); }
let res = await db.collection("uni-id-users") },
.where({ // 获取当前环境
"_id":uniCloud.getCurrentUserInfo().uid NODE_ENV() {
}) return process.env.NODE_ENV
.field('score') }
.get() },
console.log('当前用户有多少积分:',res.data[0] && res.data[0].score); // 监听msgList变化,将其存储到本地缓存中
} watch: {
msgList: {
handler(msgList) {
// let {score} = res.result.data[0] || {} // 将msgList存储到本地缓存中
// console.log('score',score); uni.setStorageSync('uni-ai-msg', msgList)
},
// 深度监听msgList变化
// for (let i = 0; i < 15; i++) { deep: true
// this.msgList.push({ }
// isAi: i % 2 == true, },
// content: "1-" + i async mounted() {
// }) // 如果存在广告位id且用户token未过期
// } if (this.adpid && uniCloud.getCurrentUserInfo().tokenExpired > Date.now()) {
// 查询当前用户的积分
this.msgList = uni.getStorageSync('uni-ai-msg') || [] // 获取数据库对象
let db = uniCloud.databaseForJQL();
// 获取uni-id-users集合
// 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。 let res = await db.collection("uni-id-users")
let length = this.msgList.length // 查询条件
if(length){ .where({
let lastMsg = this.msgList[length - 1] // 当前用户id
if(!lastMsg.isAi){ "_id": uniCloud.getCurrentUserInfo().uid
this.retriesSendMsg() })
} // 返回score字段
} .field('score')
// 执行查询
.get()
// this.msgList.pop() // 输出当前用户积分
// console.log('this.msgList', this.msgList); console.log('当前用户有多少积分:', res.data[0] && res.data[0].score);
this.showLastMsg() }
// #ifdef H5 // for (let i = 0; i < 15; i++) {
//获得消息输入框对象 // this.msgList.push({
let adjunctKeydown = false // isAi: i % 2 == true,
const textareaDom = document.querySelector('.textarea-box textarea'); // content: "1-" + i
if (textareaDom) { // })
//键盘按下时 // }
textareaDom.onkeydown = e => {
// console.log('onkeydown', e.keyCode) this.msgList = uni.getStorageSync('uni-ai-msg') || []
if ([16, 17, 18, 93].includes(e.keyCode)) {
//按下了shift ctrl alt windows键
adjunctKeydown = true; // 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。
} let length = this.msgList.length
if (e.keyCode == 13 && !adjunctKeydown) { if (length) {
setTimeout(()=> { let lastMsg = this.msgList[length - 1]
this.beforeSendMsg(); if (!lastMsg.isAi) {
}, 100); this.retriesSendMsg()
} }
}; }
textareaDom.onkeyup = e => {
//松开adjunct键
if ([16, 17, 18, 93].includes(e.keyCode)) { // this.msgList.pop()
adjunctKeydown = false; // console.log('this.msgList', this.msgList);
}
}; // 在dom渲染完毕后 使聊天窗口滚动到最后一条消息
} this.$nextTick(() => {
// #endif this.showLastMsg()
})
// 添加惰性函数,检查是否开通uni-push;决定是否启用stream
// #ifdef H5
// #ifdef H5 // 监听屏幕宽度变化,判断是否为宽屏 并设置isWidescreen的值
uni.createMediaQueryObserver(this).observe({ uni.createMediaQueryObserver(this).observe({
minWidth: 650, minWidth: 650,
}, matches => { }, matches => {
this.isWidescreen = matches; this.isWidescreen = matches;
}) })
// #endif // #endif
}, },
// onUnload() {
// if(sseChannel){
// console.log('onUnload','sseChannel.close()');
// sseChannel.close()
// }
// },
methods: { methods: {
//检查是否开通uni-push;决定是否启用stream onKeydown(keyname){
async checkIsOpenPush(){ if(keyname == 'shift'){
try{ //按下了shift键
await uni.getPushClientId() shiftKeyPressed = true;
this.checkIsOpenPush = ()=>{}
}catch(err){
this.stream = false
}
},
updateLastMsg(param){
let length = this.msgList.length
if(length === 0){
return
}
let lastMsg = this.msgList[length - 1]
if(typeof param == 'function'){
let callback = param;
callback(lastMsg)
}else{
const [data,cover = false] = arguments
if(cover){
lastMsg = data
}else{
lastMsg = Object.assign(lastMsg,data)
}
}
this.msgList.splice(length - 1, 1, lastMsg)
},
onAdClose(e){
console.log('onAdClose e.detail.isEnded',e.detail.isEnded);
if(e.detail.isEnded){
//5次轮训查结果
let i = 0;
uni.showLoading({mask: true})
let myIntive = setInterval(async e => {
i++;
const db = uniCloud.database();
let res = await db.collection("uni-id-users")
.where('"_id" == $cloudEnv_uid')
.field('score')
.get()
let {score} = res.result.data[0] || {}
// console.log("1111score----------*--",score);
if(score>0 || i > 5){
// console.log("22222score----------*--",score);
clearInterval(myIntive)
uni.hideLoading()
if(score>0){
// console.log("3333score----------*--",score);
this.msgList.pop()
this.$nextTick(()=>{
this.retriesSendMsg()
uni.showToast({
title: '积分余额:'+score,
icon: 'none'
});
})
}
}
}, 2000);
}
},
async retriesSendMsg() {
// 检查是否开通uni-push;决定是否启用stream
await this.checkIsOpenPush()
this.updateLastMsg({state:0})
this.send()
},
async beforeSendMsg() {
// 如果开启了广告位需要登录
if(this.adpid){
let token = uni.getStorageSync('uni_id_token')
if(!token){
return uni.showModal({
content: '启用激励视频,客户端需登录并启用安全网络',
showCancel: false,
confirmText:"查看详情",
complete() {
let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#ad"
// #ifndef H5
uni.setClipboardData({
data:url,
showToast:false,
success() {
uni.showToast({
title: '已复制文档链接,请到浏览器粘贴浏览',
icon: 'none',
duration:5000
});
}
})
// #endif
// #ifdef H5
window.open(url)
// #endif
}
});
}
}
// 检查是否开通uni-push;决定是否启用stream
await this.checkIsOpenPush()
if(!this.content){
return uni.showToast({
title: '内容不能为空',
icon: 'none'
});
} }
// 按下了回车 且 之前没按下 shift
this.msgList.push({ if (keyname == 'enter' && !shiftKeyPressed) {
isAi: false, this.$nextTick(()=>{
content: this.content, this.beforeSendMsg();
state: 0,
create_time: Date.now()
})
this.showLastMsg()
// 清空文本内容
this.$nextTick(() => {
this.content = ''
})
this.send()
},
async send() {
let messages = []
// 复制一份,消息列表数据
let msgs = JSON.parse(JSON.stringify(this.msgList))
// 带总结的消息 index
let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
// console.log('findIndex', findIndex)
if (findIndex != -1) {
let aiSummaryIndex = msgs.length - findIndex - 1
// console.log('aiSummaryIndex', aiSummaryIndex)
msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize
// 拿最后一条带直接的消息作为与ai对话的msg body
msgs = msgs.splice(aiSummaryIndex, msgs.length - 1)
} else {
// 如果未总结过就直接从末尾拿10条
msgs = msgs.splice(-10)
}
// 过滤涉敏问题
msgs = msgs.filter(msg=>!msg.illegal)
messages = msgs.map(item => {
let role = "user"
if (item.isAi) {
role = item.summarize ? 'system' : 'assistant'
}
return {
content: item.content,
role
}
})
console.log('send to ai messages:', messages);
if(this.stream){
sseChannel = new uniCloud.SSEChannel() // 创建消息通道
// console.log('sseChannel',sseChannel);
sseChannel.on('message', (message) => { // 监听message事件
// console.log('on message', message);
if (this.sseIndex === 0) {
this.msgList.push({
isAi: true,
content: message,
create_time: Date.now()
})
this.showLastMsg()
} else {
this.updateLastMsg(lastMsg=>{
lastMsg.content += message
})
this.showLastMsg()
}
this.sseIndex++
})
sseChannel.on('end', (e) => { // 监听end事件,如果云端执行end时传了message,会在客户端end事件内收到传递的消息
// console.log('on end', e);
if(e && (e.summarize || e.insufficientScore)){
this.updateLastMsg(lastMsg=>{
if(e.summarize){
lastMsg.summarize = e.summarize
}else if (e.insufficientScore){
lastMsg.insufficientScore
}
})
}
this.sseIndex = 0
this.showLastMsg()
}) })
await sseChannel.open() // 等待通道开启
} }
skip_callback = false
const uniAiChat = uniCloud.importObject("uni-ai-chat",{
customUI:true
})
uniAiChat.send({
messages,
sseChannel
})
.then(res => {
console.log(111,res);
this.updateLastMsg({state:100})
if (res.data) {
// console.log(res, res.reply);
// 判断长度,防止请求未返回时,历史对话已被清空。引起对话顺序错误 导致 对话输入框卡住
if(!skip_callback){
let {"reply":content,summarize,insufficientScore,illegal} = res.data
if(illegal){
this.updateLastMsg({illegal:true})
}
this.msgList.push({
create_time: Date.now(),
isAi: true,
content,
summarize,
insufficientScore,
illegal
})
this.showLastMsg()
}else{
console.log('用户点击了清空按钮,跳过前一次请求的回调',res.data.reply);
}
}
})
.catch(e => {
console.log(e);
let l = this.msgList.length
console.log(l,this.msgList[l-1]);
if(l && sseChannel && this.msgList[l-1].isAi){
this.sseIndex = 0
}
this.updateLastMsg({state:-100})
uni.showModal({
content: JSON.stringify(e.message),
showCancel: false
});
})
}, },
showLastMsg() { onKeyup(keyname){
this.$nextTick(() => { if(keyname == 'shift'){
this.scrollIntoView = "last-msg-item" //按下了shift键
this.$nextTick(() => { shiftKeyPressed = false;
this.scrollIntoView = ""
})
})
},
msgStateIcon(msg) {
switch (msg.state) {
case 0:
// 发送中
return 'spinner-cycle'
break;
case -100:
// 发送失败
return 'refresh-filled'
break;
case -200:
// 禁止发送(内容不合法)
return 'info-filled'
break;
default:
return false
break;
} }
}, },
clear() { // 此(惰性)函数,检查是否开通uni-push;决定是否启用enableStream
uni.showModal({ async checkIsOpenPush() {
title: "确认要清空聊天记录?", try {
content: '本操作不可撤销', // 获取推送客户端id
complete: (e) => { await uni.getPushClientId()
if (e.confirm) { // 如果获取成功,则将checkIsOpenPush函数重写为一个空函数
if(sseChannel){ this.checkIsOpenPush = () => {}
sseChannel.close() } catch (err) {
} // 如果获取失败,则将enableStream设置为false
skip_callback = true this.enableStream = false
this.sseIndex = 0 }
this.msgList = [] },
} // 更新最后一条消息
} updateLastMsg(param) {
}); let length = this.msgList.length
} if (length === 0) {
} return
} }
</script> let lastMsg = this.msgList[length - 1]
<style lang="scss"> // 如果param是函数,则将最后一条消息作为参数传入该函数
/* #ifdef VUE3 && APP-PLUS */ if (typeof param == 'function') {
@import "@/components/uni-ai-msg/uni-ai-msg.scss"; let callback = param;
/* #endif */ callback(lastMsg)
} else {
/* #ifndef APP-NVUE */ // 否则,将参数解构为data和cover两个变量
view, const [data, cover = false] = arguments
textarea, if (cover) {
button, lastMsg = data
.page } else {
{ lastMsg = Object.assign(lastMsg, data)
display: flex; }
box-sizing: border-box; }
} this.msgList.splice(length - 1, 1, lastMsg)
/* #endif */ },
// 广告关闭事件
onAdClose(e) {
/* #ifndef APP-NVUE */ console.log('onAdClose e.detail.isEnded', e.detail.isEnded);
page, if (e.detail.isEnded) {
/* #endif */ //5次轮训查结果
.page, let i = 0;
.container { uni.showLoading({
background-color: #efefef; mask: true
})
/* #ifdef APP-NVUE */ let myIntive = setInterval(async e => {
flex: 1; i++;
/* #endif */ // 获取云数据库实例
const db = uniCloud.database();
/* #ifndef APP-NVUE */ // 获取uni-id-users集合
height: 100vh; let res = await db.collection("uni-id-users")
/* #endif */ // 查询条件为_id等于当前用户id
.where('"_id" == $cloudEnv_uid')
/* #ifdef H5 */ // 只返回score字段
height: calc(100vh - 44px); .field('score')
/* #endif */ // 执行查询
.get()
flex-direction: column; // 解构出score字段的值,如果没有则默认为undefined
align-items: center; let {
justify-content: center; score
} } = res.result.data[0] || {}
if (score > 0 || i > 5) {
/* #ifndef APP-NVUE */ // 清除轮询定时器
.container { clearInterval(myIntive)
background-color: #FAFAFA; // 隐藏加载提示
} uni.hideLoading()
/* #endif */ if (score > 0) {
// 移除最后一条消息
.foot-box { this.msgList.pop()
width: 750rpx; this.$nextTick(() => {
display: flex; // 重发消息
flex-direction: column; this.retriesSendMsg()
padding: 10px 0px; uni.showToast({
background-color: #FFF; title: '积分余额:' + score,
} icon: 'none'
.foot-box-content{ });
justify-content: space-around; })
} }
}
.textarea-box { }, 2000);
padding: 8px 10px; }
background-color: #f9f9f9; },
border-radius: 5px; async retriesSendMsg() {
} // 检查是否开通uni-push;决定是否启用enableStream
await this.checkIsOpenPush()
.textarea-box .textarea { // 更新最后一条消息的状态为0 表示消息正在发送中
max-height: 100px; this.updateLastMsg({
font-size: 14px; state: 0
/* #ifndef APP-NVUE */ })
overflow: auto; // 发送消息
/* #endif */ this.send()
width: 450rpx; },
} async beforeSendMsg() {
// 如果开启了广告位需要登录
/* #ifdef H5 */ if (this.adpid) {
/*隐藏滚动条*/ // 获取本地缓存的token
.textarea-box .textarea::-webkit-scrollbar { let token = uni.getStorageSync('uni_id_token')
width: 0; // 如果token不存在
} if (!token) {
/* #endif */ // 弹出提示框
return uni.showModal({
.input-placeholder { // 提示内容
color: #bbb; content: '启用激励视频,客户端需登录并启用安全网络',
} // 不显示取消按钮
showCancel: false,
.trash, // 确认按钮文本
.send { confirmText: "查看详情",
width: 50px; // 弹框关闭后执行的回调函数
height: 30px; complete() {
justify-content: center; // 文档链接
align-items: center; let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#ad"
flex-shrink: 0; // #ifndef H5
} // 将文档链接复制到剪贴板
uni.setClipboardData({
.trash { // 复制的内容
width: 30rpx; data: url,
margin-left: 10rpx; // 不显示提示框
} showToast: false,
// 复制成功后的回调函数
.send { success() {
color: #FFF; // 弹出提示框
border-radius: 4px; uni.showToast({
display: flex; // 提示内容
margin: 0; title: '已复制文档链接,请到浏览器粘贴浏览',
padding: 0; // 不显示图标
font-size: 14px; icon: 'none',
margin-right: 20rpx; // 提示框持续时间
} duration: 5000
});
/* #ifndef APP-NVUE */ }
.send::after { })
display: none; // #endif
}
/* #endif */ // #ifdef H5
// 在新窗口打开文档链接
window.open(url)
.msg-list { // #endif
flex: 1; }
height: 1px; });
width: 750rpx; }
} }
.userInfo {
flex-direction: column; // 检查是否开通uni-push;决定是否启用enableStream
} await this.checkIsOpenPush()
.msg-item { // 如果内容为空
position: relative; if (!this.content) {
width: 750rpx; // 弹出提示框
flex-direction: column; return uni.showToast({
padding: 0 15px; // 提示内容
padding-bottom: 15px; title: '内容不能为空',
} // 不显示图标
icon: 'none'
.msgStateIcon { });
position: relative; }
top: 5px;
right: 5px; // 将用户输入的消息添加到消息列表中
align-self: center; this.msgList.push({
} // 标记为非人工智能机器人,即:为用户发送的消息
isAi: false,
.avatar { // 消息内容
width: 40px; content: this.content,
height: 40px; // 消息状态为0,表示正在发送中
border-radius: 2px; state: 0,
} // 消息创建时间
create_time: Date.now()
.create_time { })
font-size: 12px;
padding: 5px 0; // 展示最后一条消息
padding-top: 0; this.showLastMsg()
color: #aaa; // dom加载完成后 清空文本内容
text-align: center; console.log(999999)
width:750rpx; this.$nextTick(() => {
/* #ifdef MP */ this.content = ''
display: flex; console.log(101010)
/* #endif */ })
justify-content: center; this.send() // 发送消息
} },
async send() {
.content { let messages = []
/* #ifndef APP-NVUE */ // 复制一份,消息列表数据
max-width: 550rpx; let msgs = JSON.parse(JSON.stringify(this.msgList))
/* #endif */ // 带总结的消息 index
background-color: #FFF; let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
border-radius: 5px; // console.log('findIndex', findIndex)
padding: 12px 10px; if (findIndex != -1) {
margin-left: 10px; let aiSummaryIndex = msgs.length - findIndex - 1
/* #ifndef APP-NVUE */ // console.log('aiSummaryIndex', aiSummaryIndex)
word-break: break-all; // 将带总结的消息的 内容 更换成 总结
user-select: text; msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize
cursor: text; // 拿最后一条带直接的消息作为与ai对话的msg body
/* #endif */ msgs = msgs.splice(aiSummaryIndex, msgs.length - 1)
} } else {
// 如果未总结过就直接从末尾拿10条
/* #ifndef APP-NVUE */ msgs = msgs.splice(-10)
.content { }
display: inline;
} // 过滤涉敏问题
msgs = msgs.filter(msg => !msg.illegal)
.content ::v-deep rich-text{
max-width: 550rpx; // 根据数据内容设置角色
overflow: auto; messages = msgs.map(item => {
} // 角色默认为用户
/* #endif */ let role = "user"
// 如果是ai再根据 是否有总结 来设置角色为 system 还是 assistant
/* #ifdef H5 */ if (item.isAi) {
.content * { role = item.summarize ? 'system' : 'assistant'
display: inline; }
} return {
/* #endif */ content: item.content,
role
.reverse { }
flex-direction: row-reverse; })
}
// 在控制台输出 向ai机器人发送的完整消息内容
.reverse .content { console.log('send to ai messages:', messages);
margin-left: 0;
margin-right: 10px; // 判断是否开启了流式响应模式
} if (this.enableStream) {
// 创建消息通道
.reverse-align { sseChannel = new uniCloud.SSEChannel()
align-items: flex-end; // console.log('sseChannel',sseChannel);
}
// 监听message事件
.noData { sseChannel.on('message', (message) => {
margin-top: 15px; // console.log('on message', message);
text-align: center; // 将从云端接收到的消息添加到消息列表中
width: 750rpx;
color: #aaa; // 如果之前未添加过就添加,否则就执行更新最后一条消息
font-size: 12px; if (this.sseIndex === 0) {
justify-content: center; this.msgList.push({
} isAi: true,
content: message,
.tip-ai-ing { create_time: Date.now()
align-items: center; })
flex-direction: column; this.showLastMsg()
font-size: 14px; } else {
color: #919396; this.updateLastMsg(lastMsg => {
padding: 15px 0; lastMsg.content += message
} })
this.showLastMsg()
.uni-link { }
margin-left: 5px; // 让流式响应计数值递增
line-height: 20px; this.sseIndex++
} })
/* #ifdef H5 */ // 监听end事件,如果云端执行end时传了message,会在客户端end事件内收到传递的消息
@media screen and (min-width:650px){ sseChannel.on('end', (e) => {
.foot-box{ // console.log('on end', e);
border-top: solid 1px #dde0e2; // 如果e存在且包含summarize或insufficientScore属性
} if (e && (e.summarize || e.insufficientScore)) {
.page{ // 更新最后一条消息
width: 100vw; this.updateLastMsg(lastMsg => {
flex-direction: row; // 如果e包含summarize属性
} if (e.summarize) {
.page * { // 将最后一条消息的summarize属性更新为e的summarize属性
max-width: 950px; lastMsg.summarize = e.summarize
} // 如果e包含insufficientScore属性
} else if (e.insufficientScore) {
.container, { // 将最后一条消息的insufficientScore属性更新为e的insufficientScore属性
box-shadow: 0 0 5px #e0e1e7; lastMsg.insufficientScore
margin-top: 44px; }
border-radius: 10px; })
overflow: hidden; }
}
// 结束流式响应 将流式响应计数值 设置为 0
.container .header{ this.sseIndex = 0
height: 44px; // 滚动窗口以显示最新的一条消息
line-height: 44px; this.showLastMsg()
border-bottom: 1px solid #F0F0F0; })
width: 100vw; await sseChannel.open() // 等待通道开启
justify-content: center; }
font-weight: 500;
} // 重置skip_callback为false,以便下一次请求可以正常回调
skip_callback = false
.content { // 导入uni-ai-chat模块,并设置customUI为true
background-color: #f9f9f9; const uniAiChat = uniCloud.importObject("uni-ai-chat", {
position: relative; customUI: true
max-width: 90%; })
}
// .copy { // 发送消息给ai机器人
// color: #888888; uniAiChat.send({
// position: absolute; messages, // 消息列表
// right: 8px; sseChannel // 消息通道
// top: 8px; })
// font-size: 12px; .then(res => {
// cursor:pointer; // console.log(111,res);
// }
// .copy :hover{ // 更新最后一条消息的状态为100(发送成功)
// color: #4b9e5f; this.updateLastMsg({
// } state: 100
})
.foot-box, if (res.data) {
.foot-box-content, // console.log(res, res.reply);
.msg-list, // 判断是否要跳过本次回调,防止请求未返回时,历史对话已被清空。引起对话顺序错误 导致 对话输入框卡住
.msg-item, if (!skip_callback) {
// .create_time, let {"reply": content,summarize,insufficientScore,illegal} = res.data
.noData, if (illegal) {
.textarea-box, // 如果返回的数据包含illegal属性,就更新最后一条消息的illegal属性为true
.textarea, this.updateLastMsg({illegal: true})
textarea-box { }
width: 100% !important; // 将从云端接收到的消息添加到消息列表中
} this.msgList.push({
// 添加消息创建时间
.create_time-box { create_time: Date.now(),
margin-top: 15px; // 标记消息为来自AI机器人
justify-content: center; isAi: true,
} // 添加消息内容
content,
.textarea-box, // 添加消息总结
.textarea, summarize,
textarea, // 添加消息分数不足标记
textarea-box { insufficientScore,
height: 120px; // 添加消息涉敏标记
} illegal
})
.container, // 滚动窗口以显示最新的一条消息
.foot-box, this.showLastMsg()
.textarea-box { } else {
background-color: #FFF; console.log('用户点击了清空按钮,跳过前一次请求的回调', res.data.reply);
} }
}
.foot-box-content{ })
flex-direction: column; .catch(e => {
justify-content: center; console.log(e);
align-items: flex-end; // 获取消息列表长度
padding-bottom: 0; let l = this.msgList.length
} // console.log(l,this.msgList[l-1]);
.menu { // 如果最后一条消息的来源是人工智能机器人 就将流式响应计数值设置为0
padding:0 10px; if (l && sseChannel && this.msgList[l - 1].isAi) {
} this.sseIndex = 0
.menu-item{ }
height:20px;
justify-content: center; // 更新最后一条消息的状态为-100(发送失败)
align-items: center; this.updateLastMsg({
align-content: center; state: -100
display: flex; })
margin-right: 10px; // 弹框提示用户错误原因
cursor: pointer; uni.showModal({
} content: JSON.stringify(e.message),
.trash { showCancel: false
opacity: 0.8; });
} })
.trash image{ },
height: 15px; // 滚动窗口以显示最新的一条消息
} showLastMsg() {
// 等待DOM更新
this.$nextTick(() => {
.textarea-box,.textarea-box *{ // 将scrollIntoView属性设置为"last-msg-item",以便滚动窗口到最后一条消息
// border: 1px solid #000; this.scrollIntoView = "last-msg-item"
} // 等待DOM更新,即:滚动完成
this.$nextTick(() => {
.send-btn-box .send-btn-tip{ // 将scrollIntoView属性设置为空,以便下次设置滚动条位置可被监听
color: #919396; this.scrollIntoView = ""
margin-right: 8px; })
font-size: 12px; })
line-height: 28px; },
} // 根据消息状态返回对应的图标
} msgStateIcon(msg) {
/* #endif */ switch (msg.state) {
case 0:
// 发送中
return 'spinner-cycle'
break;
case -100:
// 发送失败
return 'refresh-filled'
break;
case -200:
// 禁止发送(内容不合法)
return 'info-filled'
break;
default:
// 默认不返回任何图标
return false
break;
}
},
// 清空消息列表
clear() {
// 弹出确认清空聊天记录的提示框
uni.showModal({
title: "确认要清空聊天记录?",
content: '本操作不可撤销',
complete: (e) => {
// 如果用户确认清空聊天记录
if (e.confirm) {
// 如果存在消息通道,就关闭消息通道
if (sseChannel) {
sseChannel.close()
}
// 将skip_callback设置为true,以便下一次请求可以正常回调
skip_callback = true
// 将流式响应计数值归零
this.sseIndex = 0
// 将消息列表清空
this.msgList = []
}
}
});
}
}
}
</script>
<style lang="scss">
/* #ifdef VUE3 && APP-PLUS */
@import "@/components/uni-ai-msg/uni-ai-msg.scss";
/* #endif */
/* #ifndef APP-NVUE */
view,
textarea,
button,
.page {
display: flex;
box-sizing: border-box;
}
/* #endif */
/* #ifndef APP-NVUE */
page,
/* #endif */
.page,
.container {
background-color: #efefef;
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
height: 100vh;
/* #endif */
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
}
/* #ifndef APP-NVUE */
.container {
background-color: #FAFAFA;
}
/* #endif */
.foot-box {
width: 750rpx;
display: flex;
flex-direction: column;
padding: 10px 0px;
background-color: #FFF;
}
.foot-box-content {
justify-content: space-around;
}
.textarea-box {
padding: 8px 10px;
background-color: #f9f9f9;
border-radius: 5px;
}
.textarea-box .textarea {
max-height: 100px;
font-size: 14px;
/* #ifndef APP-NVUE */
overflow: auto;
/* #endif */
width: 450rpx;
}
/* #ifdef H5 */
/*隐藏滚动条*/
.textarea-box .textarea::-webkit-scrollbar {
width: 0;
}
/* #endif */
.input-placeholder {
color: #bbb;
}
.trash,
.send {
width: 50px;
height: 30px;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.trash {
width: 30rpx;
margin-left: 10rpx;
}
.send {
color: #FFF;
border-radius: 4px;
display: flex;
margin: 0;
padding: 0;
font-size: 14px;
margin-right: 20rpx;
}
/* #ifndef APP-NVUE */
.send::after {
display: none;
}
/* #endif */
.msg-list {
flex: 1;
height: 1px;
width: 750rpx;
}
.userInfo {
flex-direction: column;
}
.msg-item {
position: relative;
width: 750rpx;
flex-direction: column;
padding: 0 15px;
padding-bottom: 15px;
}
.msgStateIcon {
position: relative;
top: 5px;
right: 5px;
align-self: center;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 2px;
}
.create_time {
font-size: 12px;
padding: 5px 0;
padding-top: 0;
color: #aaa;
text-align: center;
width: 750rpx;
/* #ifdef MP */
display: flex;
/* #endif */
justify-content: center;
}
.content {
/* #ifndef APP-NVUE */
max-width: 550rpx;
/* #endif */
background-color: #FFF;
border-radius: 5px;
padding: 12px 10px;
margin-left: 10px;
/* #ifndef APP-NVUE */
word-break: break-all;
user-select: text;
cursor: text;
/* #endif */
}
/* #ifndef APP-NVUE */
.content {
display: inline;
}
.content ::v-deep rich-text {
max-width: 550rpx;
overflow: auto;
}
/* #endif */
/* #ifdef H5 */
.content * {
display: inline;
}
/* #endif */
.reverse {
flex-direction: row-reverse;
}
.reverse .content {
margin-left: 0;
margin-right: 10px;
}
.reverse-align {
align-items: flex-end;
}
.noData {
margin-top: 15px;
text-align: center;
width: 750rpx;
color: #aaa;
font-size: 12px;
justify-content: center;
}
.tip-ai-ing {
align-items: center;
flex-direction: column;
font-size: 14px;
color: #919396;
padding: 15px 0;
}
.uni-link {
margin-left: 5px;
line-height: 20px;
}
/* #ifdef H5 */
@media screen and (min-width:650px) {
.foot-box {
border-top: solid 1px #dde0e2;
}
.page {
width: 100vw;
flex-direction: row;
}
.page * {
max-width: 950px;
}
.container,
{
box-shadow: 0 0 5px #e0e1e7;
margin-top: 44px;
border-radius: 10px;
overflow: hidden;
}
.container .header {
height: 44px;
line-height: 44px;
border-bottom: 1px solid #F0F0F0;
width: 100vw;
justify-content: center;
font-weight: 500;
}
.content {
background-color: #f9f9f9;
position: relative;
max-width: 90%;
}
// .copy {
// color: #888888;
// position: absolute;
// right: 8px;
// top: 8px;
// font-size: 12px;
// cursor:pointer;
// }
// .copy :hover{
// color: #4b9e5f;
// }
.foot-box,
.foot-box-content,
.msg-list,
.msg-item,
// .create_time,
.noData,
.textarea-box,
.textarea,
textarea-box {
width: 100% !important;
}
.create_time-box {
margin-top: 15px;
justify-content: center;
}
.textarea-box,
.textarea,
textarea,
textarea-box {
height: 120px;
}
.container,
.foot-box,
.textarea-box {
background-color: #FFF;
}
.foot-box-content {
flex-direction: column;
justify-content: center;
align-items: flex-end;
padding-bottom: 0;
}
.menu {
padding: 0 10px;
}
.menu-item {
height: 20px;
justify-content: center;
align-items: center;
align-content: center;
display: flex;
margin-right: 10px;
cursor: pointer;
}
.trash {
opacity: 0.8;
}
.trash image {
height: 15px;
}
.textarea-box,
.textarea-box * {
// border: 1px solid #000;
}
.send-btn-box .send-btn-tip {
color: #919396;
margin-right: 8px;
font-size: 12px;
line-height: 28px;
}
}
/* #endif */
</style> </style>
\ No newline at end of file
// 云对象教程: 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函数
const {safeRequire, checkContentSecurityEnable} = require('./utils') const {safeRequire, checkContentSecurityEnable} = require('./utils')
// 引入uni-config-center模块,并创建config对象
const createConfig = safeRequire('uni-config-center') const createConfig = safeRequire('uni-config-center')
const config = createConfig({ const config = createConfig({
pluginId: 'uni-ai-chat' pluginId: 'uni-ai-chat'
}).config() }).config()
// 引入uniCloud.database()方法,并创建db对象
const db = uniCloud.database(); const db = uniCloud.database();
// 创建userscollection对象
const userscollection = db.collection('uni-id-users') const userscollection = db.collection('uni-id-users')
// 引入uni-id-common模块
const uniIdCommon = require('uni-id-common') const uniIdCommon = require('uni-id-common')
module.exports = {
module.exports = {
_before:async function() { _before:async function() {
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑,比如: // 这里是云函数的前置方法,你可以在这里加入你需要逻辑
/*
例如:使用uni-id-pages(链接地址:https://ext.dcloud.net.cn/plugin?id=8577)搭建账户体系。
然后再使用uni-id-common的uniIdCommon.checkToken判断用户端身份,验证不通过你可以直接`throw new Error(“token无效”)`抛出异常拦截访问。
如果验证通过了可以获得用户id,可以记录每一个用户id的调用次数来限制,调用多少次后必须充值(推荐用uni-pay,下载地址:https://ext.dcloud.net.cn/plugin?id=1835)
或者看一个激励视频广告(详情:https://uniapp.dcloud.net.cn/uni-ad/ad-rewarded-video.html)后才能继续使用
*** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 ***
*/
// 判断否调用量本云对象的send方法
if(this.getMethodName() == 'send'){ if(this.getMethodName() == 'send'){
// 从配置中心获取是否需要销毁积分 // 从配置中心获取是否需要销毁积分
if(config.spentScore){ if(config.spentScore){
...@@ -33,7 +33,8 @@ module.exports = { ...@@ -33,7 +33,8 @@ module.exports = {
this.uniIdCommon = uniIdCommon.createInstance({ this.uniIdCommon = uniIdCommon.createInstance({
clientInfo: this.clientInfo clientInfo: this.clientInfo
}) })
let res = await this.uniIdCommon.checkToken(this.clientInfo.uniIdToken) // 校验token(用户身份令牌)是否有效,并获得用户的_id
let res = await this.uniIdCommon.checkToken(this.clientInfo.uniIdToken)
if (res.errCode) { if (res.errCode) {
// 如果token校验出错,则抛出错误 // 如果token校验出错,则抛出错误
throw res throw res
...@@ -44,9 +45,12 @@ module.exports = { ...@@ -44,9 +45,12 @@ module.exports = {
/* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */ /* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */
let {data:[{score}]} = await userscollection.doc(this.current_uid).field({'score':1}).get() let {data:[{score}]} = await userscollection.doc(this.current_uid).field({'score':1}).get()
console.log('score----',score); console.log('score----',score);
if(score == 0 || score < 0){ //并发的情况下可能花超过 // 如果积分数小于等于0 则抛出错误提醒客户端
// 注意需要判断小于0 因为特殊的情况下可能花超过
if(score == 0 || score < 0){
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)
...@@ -56,13 +60,18 @@ module.exports = { ...@@ -56,13 +60,18 @@ module.exports = {
// 从配置中心获取内容安全配置 // 从配置中心获取内容安全配置
console.log('config.contentSecurity',config.contentSecurity); console.log('config.contentSecurity',config.contentSecurity);
if (config.contentSecurity) { if (config.contentSecurity) {
// 引入uni-sec-check模块
const UniSecCheck = safeRequire('uni-sec-check') const UniSecCheck = safeRequire('uni-sec-check')
// 创建uniSecCheck对象
const uniSecCheck = new UniSecCheck({ const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin', provider: 'mp-weixin',
requestId: this.getUniCloudRequestId() requestId: this.getUniCloudRequestId()
}) })
// 定义文本安全检测函数
this.textSecCheck = async (content)=>{ this.textSecCheck = async (content)=>{
// 获取sseChannel
let {sseChannel} = this.getParams()[0]||{} let {sseChannel} = this.getParams()[0]||{}
// 如果存在sseChannel,则抛出错误
if(sseChannel){ if(sseChannel){
throw { throw {
errSubject: 'uni-ai-chat', errSubject: 'uni-ai-chat',
...@@ -72,12 +81,17 @@ module.exports = { ...@@ -72,12 +81,17 @@ module.exports = {
} }
// 检测文本 // 检测文本
const checkRes = await uniSecCheck.textSecCheck({ const checkRes = await uniSecCheck.textSecCheck({
// 文本内容,不可超过500KB
content, content,
// 微信小程序端 开放的唯一用户标识符
// openid, // openid,
// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
scene:4, scene:4,
version: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
}) })
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({
errCode: checkRes.errCode, errCode: checkRes.errCode,
...@@ -85,7 +99,8 @@ module.exports = { ...@@ -85,7 +99,8 @@ module.exports = {
result: checkRes.result result: checkRes.result
}); });
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,
...@@ -96,40 +111,57 @@ module.exports = { ...@@ -96,40 +111,57 @@ module.exports = {
} }
} }
// 获取messages参数
let {messages} = this.getParams()[0]||{"messages":[]} let {messages} = this.getParams()[0]||{"messages":[]}
// 将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进行文本安全检测
await this.textSecCheck(contentString) await this.textSecCheck(contentString)
} }
} }
}, },
async _after(error, result) { async _after(error, result) {
console.log('_after',{error,result}); // 打印错误和结果
if(error){ console.log('_after',{error,result});
if(error.errCode == "60004" || error == "uni-sec-check:illegalData" ) { // 如果有错误
return { if(error){
// 如果是内容安全检测错误
if(error.errCode == "60004" || error == "uni-sec-check:illegalData" ) {
// 返回一个包含敏感内容提示和标记的响应体
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'){ // 如果是积分不足错误
let reply = "积分不足,请看完激励视频广告后再试" else if(error == 'insufficientScore'){
let {sseChannel} = this.getParams()[0]||{} // 设置回复内容
if(sseChannel){ let reply = "积分不足,请看完激励视频广告后再试"
const channel = uniCloud.deserializeSSEChannel(sseChannel) // 获取sseChannel
await channel.write(reply) let {sseChannel} = this.getParams()[0]||{}
await channel.end({ // 如果存在sseChannel
if(sseChannel){
// 反序列化sseChannel
const channel = uniCloud.deserializeSSEChannel(sseChannel)
// 向sseChannel写入回复内容
await channel.write(reply)
// 结束sseChannel
await channel.end({
"insufficientScore":true "insufficientScore":true
}) })
}else{ }else{
return { // 如果不存在sseChannel 返回一个包含回复内容和标记的响应体
return {
"data": { "data": {
reply, reply,
"insufficientScore":true "insufficientScore":true
...@@ -137,15 +169,19 @@ module.exports = { ...@@ -137,15 +169,19 @@ module.exports = {
"errCode": 0 "errCode": 0
} }
} }
}else{ }else{
throw error // 直接抛出异常 // 如果是其他错误
throw error // 直接抛出异常
} }
} }
if (this.getMethodName() == 'send' && config.contentSecurity) { // 如果是send方法且开启了内容安全检测
if (this.getMethodName() == 'send' && config.contentSecurity) {
try{ try{
await this.textSecCheck(result.data.reply) // 对回复内容进行文本安全检测
}catch(e){ await this.textSecCheck(result.data.reply)
}catch(e){
// 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体
return { return {
"data": { "data": {
"reply": "内容涉及敏感", "reply": "内容涉及敏感",
...@@ -155,157 +191,223 @@ module.exports = { ...@@ -155,157 +191,223 @@ module.exports = {
} }
} }
} }
// 返回处理后的结果
return result return result
}, },
async send({
messages,
sseChannel // 发送消息
}) { async send({
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 // 消息内容
// messages = [{ messages,
// role: 'user', // sse渠道对象
// content: 'uni-app是什么,20个字以内进行说明' sseChannel
// }] }) {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
// messages = [{
// role: 'user',
// content: 'uni-app是什么,20个字以内进行说明'
// }]
// 校验客户端提交的参数 // 校验客户端提交的参数
let res = checkMessages(messages) // 检查消息是否符合规范
if (res.errCode) { let res = checkMessages(messages)
throw new Error(res.errMsg) if (res.errCode) {
} throw new Error(res.errMsg)
}
// 向uni-ai发送消息 // 向uni-ai发送消息
// 调用chatCompletion函数,传入messages、sseChannel、llm参数
let {llm,chatCompletionOptions} = config let {llm,chatCompletionOptions} = config
return await chatCompletion({ return await chatCompletion({
messages, //消息内容 messages, //消息内容
sseChannel, //sse渠道对象 sseChannel, //sse渠道对象
llm llm
}) })
async function chatCompletion({
messages, // chatCompletion函数:对话完成
summarize = false, async function chatCompletion({
sseChannel = false, // 消息列表
llm messages,
// 是否需要总结
summarize = false,
// sse渠道对象
sseChannel = false,
// 语言模型
llm
}) { }) {
const llmManager = uniCloud.ai.getLLMManager(llm) // 获取语言模型管理器
const llmManager = uniCloud.ai.getLLMManager(llm)
// 调用chatCompletion方法,传入参数
let res = await llmManager.chatCompletion({ let res = await llmManager.chatCompletion({
...chatCompletionOptions, ...chatCompletionOptions,
messages, messages,
stream: sseChannel !== false stream: sseChannel !== false
}) })
if (sseChannel) { // 如果存在sseChannel
let reply = "" if (sseChannel) {
return new Promise((resolve, reject) => { let reply = ""
return new Promise((resolve, reject) => {
// 反序列化sseChannel
const channel = uniCloud.deserializeSSEChannel(sseChannel) const channel = uniCloud.deserializeSSEChannel(sseChannel)
// 判断如果是open-ai按字返回,否则按行返回 // 判断如果是open-ai按字返回,否则按行返回
if(llm && llm.provider && llm.provider == "openai"){ if(llm && llm.provider && llm.provider == "openai"){
// 按字返回
res.on('message', async (message) => { res.on('message', async (message) => {
reply += message reply += message
await channel.write(message) await channel.write(message)
// console.log('---message----', message) // console.log('---message----', message)
}) })
}else{ }else{
// 按行返回
res.on('line', async (line) => { res.on('line', async (line) => {
await channel.write(reply? ("\n\n " + line) : line) await channel.write(reply? ("\n\n " + line) : line)
reply += line reply += line
// console.log('---line----', line) // console.log('---line----', line)
}) })
} }
res.on('end', async () => { // 结束返回
// console.log('---end----',reply) res.on('end', async () => {
messages.push({ // console.log('---end----',reply)
"content": reply, // 将回复内容添加到消息列表中
"role": "assistant" messages.push({
}) "content": reply,
"role": "assistant"
let totalTokens = messages.map(i => i.content).join('').length; })
// console.log('totalTokens',totalTokens);
if (!summarize && totalTokens > 500) { // 计算消息总长度
let replySummarize = await getSummarize(messages) let totalTokens = messages.map(i => i.content).join('').length;
// console.log('replySummarize',replySummarize) // console.log('totalTokens',totalTokens);
await channel.end({ // 如果不需要总结且消息总长度超过500
summarize: replySummarize if (!summarize && totalTokens > 500) {
}) // 获取总结
} else { let replySummarize = await getSummarize(messages)
await channel.end() // console.log('replySummarize',replySummarize)
} // 结束sseChannel并返回总结
resolve({ await channel.end({
errCode: 0 summarize: replySummarize
}) })
}) } else {
res.on('error', (err) => { // 结束sseChannel
console.error('---error----', err) await channel.end()
reject(err) }
}) // 返回处理结果
}) resolve({
} else { errCode: 0
if (summarize == false) { })
messages.push({ })
"content": res.reply, // 返回错误
"role": "assistant" res.on('error', (err) => {
}) console.error('---error----', err)
let totalTokens = messages.map(i => i.content).join('').length; reject(err)
if (totalTokens > 500) { })
let replySummarize = await getSummarize(messages) })
res.summarize = replySummarize } else {
} // 如果不需要总结
if (summarize == false) {
// 将回复内容添加到消息列表中
messages.push({
"content": res.reply,
"role": "assistant"
})
// 计算消息总长度
let totalTokens = messages.map(i => i.content).join('').length;
// 如果消息总长度超过500
if (totalTokens > 500) {
// 获取总结
let replySummarize = await getSummarize(messages)
// 将总结添加到返回结果中
res.summarize = replySummarize
}
} }
// 如果存在错误
if(res.errCode){ if(res.errCode){
// 抛出错误
throw res throw res
} }
// 返回处理结果
return { return {
data:res, data:res,
errCode: 0 errCode: 0
} }
} }
} }
//获总结
async function getSummarize(messages) { //获总结
messages.push({ async function getSummarize(messages) {
"content": "请简要总结上述全部对话", messages.push({
"role": "user" "content": "请简要总结上述全部对话",
}) "role": "user"
// 获取总结不需要再总结summarize和stream })
let res = await chatCompletion({ // 调用chatCompletion函数,传入messages、summarize、stream、sseChannel参数
messages, let res = await chatCompletion({
summarize: true, // 消息内容
stream: false, messages,
sseChannel: false // 是否需要总结
}) summarize: true,
return res.reply // 是否需要流式返回
} stream: false,
// sse渠道对象
function checkMessages(messages) { sseChannel: false
try { })
if (messages === undefined) { // 返回总结的文字内容
throw "messages为必传参数" return res.reply
} else if (!Array.isArray(messages)) { }
throw "参数messages的值类型必须是[object,object...]"
} else {
messages.forEach(item => { /**
if (typeof item != 'object') { * 校验消息内容是否符合规范
throw "参数messages的值类型必须是[object,object...]" * @param {Array} messages - 消息列表
} * @returns {Object} - 返回校验结果
let itemRoleArr = ["assistant", "user", "system"] */
if (!itemRoleArr.includes(item.role)) { function checkMessages(messages) {
throw "参数messages[{role}]的值只能是:" + itemRoleArr.join('') try {
} // 如果messages未定义
if (typeof item.content != 'string') { if (messages === undefined) {
throw "参数messages[{content}]的值类型必须是字符串" // 抛出异常
} throw "messages为必传参数"
}) // 如果messages不是数组
} } else if (!Array.isArray(messages)) {
return { // 抛出异常
errCode: 0, throw "参数messages的值类型必须是[object,object...]"
} } else {
} catch (errMsg) { // 否则 遍历messages
return { messages.forEach(item => {
errSubject: 'ai-demo', // 如果item不是对象
errCode: 'param-error', if (typeof item != 'object') {
errMsg // 抛出异常
} 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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册