diff --git a/changelog.md b/changelog.md index dbf167d1a1e167214dcb8bccd96de6ca26902e96..b8961803f8a88fd57a322a157cdaf2aa8e84135c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,5 @@ +## 1.0.5(2023-05-11) +- 新增代码注释 ## 1.0.4(2023-05-10) - 修复 uni-ai默认服务商,检测到内容涉及违规后 内容会返回一部分或卡住的问题(HBuilderX3.8.2起或连接云端云函数支持) - 修复 web-pc端当回车键敲得较快时 发送的内容不正确的问题 diff --git a/components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue b/components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue index a04da3030190d45ec45297e9293f2ec423677e15..d1468e59e9eb35b1f387c4ef9f738a03bc0122a1 100644 --- a/components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue +++ b/components/uni-ad-rewarded-video/uni-ad-rewarded-video.vue @@ -9,25 +9,26 @@ - \ No newline at end of file diff --git a/package.json b/package.json index b21a36c01d11b6afd92f8c1713c908b6e5c012ff..5de8494d7dd5f8fe9e4dd3e8d14a87048555efca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "id": "uni-ai-chat", "name": "uni-ai-chat", - "version": "1.0.3", + "version": "1.0.5", "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体", "main": "main.js", "scripts": { diff --git a/pages/chat/chat.vue b/pages/chat/chat.vue index 0db3b112a62e5d2d84beb57977bdf9b57ba2628d..253442bccaaa605b6ea16ab456bf08df7d9fbdbe 100644 --- a/pages/chat/chat.vue +++ b/pages/chat/chat.vue @@ -1,830 +1,975 @@ - - - - - \ No newline at end of file diff --git a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js index 1c49590401fa4e225dbb3fc322962a0e9d281c6b..cb754b1da20eccfe495bb1b92a1b5e235203de45 100644 --- a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js +++ b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js @@ -1,25 +1,25 @@ -// 云对象教程: 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 +// 引入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 = { + + +module.exports = { _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(config.spentScore){ @@ -33,7 +33,8 @@ module.exports = { this.uniIdCommon = uniIdCommon.createInstance({ 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) { // 如果token校验出错,则抛出错误 throw res @@ -44,9 +45,12 @@ module.exports = { /* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */ let {data:[{score}]} = await userscollection.doc(this.current_uid).field({'score':1}).get() console.log('score----',score); - if(score == 0 || score < 0){ //并发的情况下可能花超过 + // 如果积分数小于等于0 则抛出错误提醒客户端 + // 注意需要判断小于0 因为特殊的情况下可能花超过 + if(score == 0 || score < 0){ throw "insufficientScore" } + // 扣除对应的积分值 await userscollection.doc(this.current_uid) .update({ score:db.command.inc(-1 * config.spentScore) @@ -56,13 +60,18 @@ module.exports = { // 从配置中心获取内容安全配置 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', @@ -72,12 +81,17 @@ module.exports = { } // 检测文本 const checkRes = await uniSecCheck.textSecCheck({ + // 文本内容,不可超过500KB content, + // 微信小程序端 开放的唯一用户标识符 // openid, + // 场景值(1 资料;2 评论;3 论坛;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); + // 如果检测到风险内容,则抛出错误 if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { console.error({ errCode: checkRes.errCode, @@ -85,7 +99,8 @@ module.exports = { result: checkRes.result }); throw "uni-sec-check:illegalData" - } else if (checkRes.errCode) { + // 如果检测出错,则抛出错误 + } else if (checkRes.errCode) { console.log(`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`); console.error({ errCode: checkRes.errCode, @@ -96,40 +111,57 @@ module.exports = { } } + // 获取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 { + // 打印错误和结果 + 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) { + }else if(error.errCode && error.errMsg) { + // 如果是符合响应体规范的错误 // 符合响应体规范的错误,直接返回 return error } - else if(error == 'insufficientScore'){ - let reply = "积分不足,请看完激励视频广告后再试" - let {sseChannel} = this.getParams()[0]||{} - if(sseChannel){ - const channel = uniCloud.deserializeSSEChannel(sseChannel) - await channel.write(reply) - await channel.end({ + // 如果是积分不足错误 + 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{ - return { + + }else{ + // 如果不存在sseChannel 返回一个包含回复内容和标记的响应体 + return { "data": { reply, "insufficientScore":true @@ -137,15 +169,19 @@ module.exports = { "errCode": 0 } } - }else{ - throw error // 直接抛出异常 + }else{ + // 如果是其他错误 + throw error // 直接抛出异常 } } - - if (this.getMethodName() == 'send' && config.contentSecurity) { + + // 如果是send方法且开启了内容安全检测 + if (this.getMethodName() == 'send' && config.contentSecurity) { try{ - await this.textSecCheck(result.data.reply) - }catch(e){ + // 对回复内容进行文本安全检测 + await this.textSecCheck(result.data.reply) + }catch(e){ + // 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体 return { "data": { "reply": "内容涉及敏感", @@ -155,157 +191,223 @@ module.exports = { } } } + // 返回处理后的结果 return result }, - async send({ - messages, - sseChannel - }) { - // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 - // messages = [{ - // role: 'user', - // content: 'uni-app是什么,20个字以内进行说明' - // }] - + + + // 发送消息 + async send({ + // 消息内容 + messages, + // sse渠道对象 + sseChannel + }) { + + // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 + // messages = [{ + // role: 'user', + // content: 'uni-app是什么,20个字以内进行说明' + // }] + // 校验客户端提交的参数 - let res = checkMessages(messages) - if (res.errCode) { - throw new Error(res.errMsg) - } - + // 检查消息是否符合规范 + let res = checkMessages(messages) + if (res.errCode) { + throw new Error(res.errMsg) + } + // 向uni-ai发送消息 + // 调用chatCompletion函数,传入messages、sseChannel、llm参数 let {llm,chatCompletionOptions} = config return await chatCompletion({ messages, //消息内容 sseChannel, //sse渠道对象 llm - }) - async function chatCompletion({ - messages, - summarize = false, - sseChannel = false, - llm + }) + + // chatCompletion函数:对话完成 + async function chatCompletion({ + // 消息列表 + 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({ - ...chatCompletionOptions, - messages, - stream: sseChannel !== false - }) - - if (sseChannel) { - let reply = "" - return new Promise((resolve, reject) => { + ...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) => { await channel.write(reply? ("\n\n " + line) : line) reply += 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 (!summarize && totalTokens > 500) { - let replySummarize = await getSummarize(messages) - // console.log('replySummarize',replySummarize) - await channel.end({ - summarize: replySummarize - }) - } else { - await channel.end() - } - resolve({ - errCode: 0 - }) - }) - res.on('error', (err) => { - console.error('---error----', err) - reject(err) - }) - }) - } else { - if (summarize == false) { - messages.push({ - "content": res.reply, - "role": "assistant" - }) - let totalTokens = messages.map(i => i.content).join('').length; - if (totalTokens > 500) { - let replySummarize = await getSummarize(messages) - res.summarize = replySummarize - } + // 结束返回 + 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); + // 如果不需要总结且消息总长度超过500 + if (!summarize && totalTokens > 500) { + // 获取总结 + let replySummarize = await getSummarize(messages) + // console.log('replySummarize',replySummarize) + // 结束sseChannel并返回总结 + await channel.end({ + summarize: replySummarize + }) + } else { + // 结束sseChannel + await channel.end() + } + // 返回处理结果 + resolve({ + errCode: 0 + }) + }) + // 返回错误 + res.on('error', (err) => { + console.error('---error----', err) + reject(err) + }) + }) + } 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){ + // 抛出错误 throw res } + // 返回处理结果 return { data:res, errCode: 0 - } - } - } - - //获总结 - async function getSummarize(messages) { - messages.push({ - "content": "请简要总结上述全部对话", - "role": "user" - }) - // 获取总结不需要再总结summarize和stream - let res = await chatCompletion({ - messages, - summarize: true, - stream: false, - sseChannel: false - }) - return res.reply - } - - function checkMessages(messages) { - try { - if (messages === undefined) { - throw "messages为必传参数" - } else if (!Array.isArray(messages)) { - throw "参数messages的值类型必须是[object,object...]" - } else { - messages.forEach(item => { - if (typeof item != 'object') { - throw "参数messages的值类型必须是[object,object...]" - } - let itemRoleArr = ["assistant", "user", "system"] - if (!itemRoleArr.includes(item.role)) { - throw "参数messages[{role}]的值只能是:" + itemRoleArr.join('或') - } - if (typeof item.content != 'string') { - throw "参数messages[{content}]的值类型必须是字符串" - } - }) - } - return { - errCode: 0, - } - } catch (errMsg) { - return { - errSubject: 'ai-demo', - errCode: 'param-error', - errMsg - } - } - } - } + } + } + } + + + //获总结 + async function getSummarize(messages) { + messages.push({ + "content": "请简要总结上述全部对话", + "role": "user" + }) + // 调用chatCompletion函数,传入messages、summarize、stream、sseChannel参数 + let res = await chatCompletion({ + // 消息内容 + messages, + // 是否需要总结 + summarize: true, + // 是否需要流式返回 + stream: false, + // sse渠道对象 + sseChannel: false + }) + // 返回总结的文字内容 + return res.reply + } + + + /** + * 校验消息内容是否符合规范 + * @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