info
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
|
||||
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
|
||||
const {safeRequire, checkContentSecurityEnable} = require('./utils')
|
||||
const createConfig = safeRequire('uni-config-center')
|
||||
const config = createConfig({
|
||||
pluginId: 'uni-ai-chat'
|
||||
}).config()
|
||||
const db = uniCloud.database();
|
||||
// const userscollection = db.collection('uni-id-users')
|
||||
// const uniIdCommon = require('uni-id-common')
|
||||
|
||||
|
||||
function getCurrentDateTimestamp (date = Date.now(), targetTimezone = 8) {
|
||||
const oneHour = 60 * 60 * 1000
|
||||
return parseInt((date + targetTimezone * oneHour) / (24 * oneHour)) * (24 * oneHour) - targetTimezone * oneHour
|
||||
}
|
||||
|
||||
async function checkLimit () {
|
||||
const ipLimit = 5 // 每个IP每日限制10次
|
||||
const userLimit = 2 // 每个用户(token)每小时限制3次
|
||||
|
||||
const aiChatLimitDB = db.collection('ai-chat-limit')
|
||||
const {clientIP} = this.getClientInfo()
|
||||
|
||||
const res = await aiChatLimitDB.where({
|
||||
ip: clientIP
|
||||
}).get()
|
||||
|
||||
const result = res.data && res.data[0] || {}
|
||||
const today = getCurrentDateTimestamp() + (24 * 60 * 60 * 1000)
|
||||
|
||||
if (result.send_count >= ipLimit && Date.now() < today) {
|
||||
throw {
|
||||
errCode: 'limit',
|
||||
errMsg: "今日 uni-ai-chat 对话已达上限,请明天尝试对话"
|
||||
}
|
||||
}
|
||||
|
||||
const currentUserToken = this.getUniIdToken()
|
||||
const userRecord = (result.tokens || []).find(token => currentUserToken === token.token)
|
||||
const oneHour = Date.now() - (60 * 60 * 1000)
|
||||
|
||||
if (userRecord && userRecord.send_count >= userLimit && userRecord.last_send_date >= oneHour) {
|
||||
throw {
|
||||
errCode: 'limit',
|
||||
errMsg: `每位登录用户一小时可对话${userLimit}次,当前已达上限,请一小时后尝试对话`
|
||||
}
|
||||
}
|
||||
|
||||
if (result._id) {
|
||||
await aiChatLimitDB.doc(result._id).update({
|
||||
send_count: result.send_count >= ipLimit ? 1: db.command.inc(1),
|
||||
tokens: db.command.push(!userRecord ? [
|
||||
{
|
||||
token: currentUserToken,
|
||||
send_count: 1,
|
||||
last_send_date: Date.now()
|
||||
}
|
||||
]: []),
|
||||
last_send_date: Date.now()
|
||||
})
|
||||
|
||||
userRecord && await aiChatLimitDB.where({
|
||||
'tokens.token': currentUserToken,
|
||||
}).update({
|
||||
'tokens.$.send_count': userRecord.send_count >= userLimit ? 1: db.command.inc(1),
|
||||
'tokens.$.last_send_date': Date.now()
|
||||
})
|
||||
} else {
|
||||
await aiChatLimitDB.add({
|
||||
ip: clientIP,
|
||||
send_count: 1,
|
||||
tokens: [
|
||||
{
|
||||
token: currentUserToken,
|
||||
send_count: 1,
|
||||
last_send_date: Date.now()
|
||||
}
|
||||
],
|
||||
last_send_date: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)后才能继续使用
|
||||
*** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 ***
|
||||
*/
|
||||
|
||||
// ::TODO 演示站用
|
||||
// await checkLimit.call(this)
|
||||
|
||||
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
|
||||
// })
|
||||
// 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);
|
||||
// if(score == 0 || score < 0){ //并发的情况下可能花超过
|
||||
// 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) {
|
||||
const UniSecCheck = safeRequire('uni-sec-check')
|
||||
const uniSecCheck = new UniSecCheck({
|
||||
provider: 'mp-weixin',
|
||||
requestId: this.getUniCloudRequestId()
|
||||
})
|
||||
this.textSecCheck = async (content) => {
|
||||
let {sseChannel} = this.getParams()[0] || {}
|
||||
if (sseChannel) {
|
||||
throw {
|
||||
errSubject: 'uni-ai-chat',
|
||||
errCode: "sec-check",
|
||||
errMsg: "流式响应模式,内容安全识别功能无效"
|
||||
}
|
||||
}
|
||||
// 检测文本
|
||||
const checkRes = await uniSecCheck.textSecCheck({
|
||||
content,
|
||||
// openid,
|
||||
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
|
||||
})
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
let {messages} = this.getParams()[0] || {"messages": []}
|
||||
let contentString = messages.map(i => i.content).join(' ')
|
||||
console.log('contentString', contentString);
|
||||
await this.textSecCheck(contentString)
|
||||
}
|
||||
}
|
||||
},
|
||||
async _after(error, result) {
|
||||
console.log('_after', {error, result});
|
||||
if (error) {
|
||||
if (error.errCode && error.errMsg) {
|
||||
// 符合响应体规范的错误,直接返回
|
||||
return error
|
||||
} else if (error == "uni-sec-check:illegalData") {
|
||||
return {
|
||||
"data": {
|
||||
"reply": "内容涉及敏感",
|
||||
"illegal": true
|
||||
},
|
||||
"errCode": 0
|
||||
}
|
||||
} 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({
|
||||
"insufficientScore": true
|
||||
})
|
||||
|
||||
} else {
|
||||
return {
|
||||
"data": {
|
||||
reply,
|
||||
"insufficientScore": true
|
||||
},
|
||||
"errCode": 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw error // 直接抛出异常
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
sseChannel
|
||||
}) {
|
||||
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
|
||||
// messages = [{
|
||||
// role: 'user',
|
||||
// content: 'uni-app是什么,20个字以内进行说明'
|
||||
// }]
|
||||
|
||||
// 校验客户端提交的参数
|
||||
let res = checkMessages(messages)
|
||||
if (res.errCode) {
|
||||
throw new Error(res.errMsg)
|
||||
}
|
||||
|
||||
// 向uni-ai发送消息
|
||||
let {llm, chatCompletionOptions} = config
|
||||
return await chatCompletion({
|
||||
messages, //消息内容
|
||||
sseChannel, //sse渠道对象
|
||||
llm
|
||||
})
|
||||
|
||||
async function chatCompletion({
|
||||
messages,
|
||||
summarize = false,
|
||||
sseChannel = false,
|
||||
llm
|
||||
}) {
|
||||
const llmManager = uniCloud.ai.getLLMManager(llm)
|
||||
let res = await llmManager.chatCompletion({
|
||||
...chatCompletionOptions,
|
||||
messages,
|
||||
stream: sseChannel !== false,
|
||||
sseChannel
|
||||
})
|
||||
|
||||
if (sseChannel) {
|
||||
let reply = ""
|
||||
return new Promise((resolve, reject) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "uni-ai-chat",
|
||||
"dependencies": {
|
||||
"uni-sec-check": "file:..\/..\/..\/..\/uni-sec-check\/uniCloud\/cloudfunctions\/common\/uni-sec-check",
|
||||
"uni-config-center": "file:..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center"
|
||||
},
|
||||
"extensions": {
|
||||
"uni-cloud-jql": {},
|
||||
"uni-cloud-push": {},
|
||||
"uni-cloud-ai": {}
|
||||
},
|
||||
"cloudfunction-config": {
|
||||
"timeout": 60
|
||||
},
|
||||
"origin-plugin-dev-name": "uni-cms",
|
||||
"origin-plugin-version": "1.0.20",
|
||||
"plugin-dev-name": "uni-cms",
|
||||
"plugin-version": "1.0.20"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
exports.safeRequire = function (module) {
|
||||
try {
|
||||
return require(module)
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
console.log(e);
|
||||
throw new Error(`${module} 公共模块不存在,请在 uni-ai-chat 目录右击"管理公共模块或扩展依赖"添加 ${module} 模块`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.checkContentSecurityEnable = function (config, field) {
|
||||
// 1. 从配置中心获取配置
|
||||
return config && config.allowCheckType && config.allowCheckType.includes(field)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 引入自定义模块
|
||||
const {safeRequire, checkContentSecurityEnable} = require('./utils')
|
||||
// 引入uni-config-center模块
|
||||
const createConfig = safeRequire('uni-config-center')
|
||||
// 获取uni-cms插件的配置
|
||||
const config = createConfig({
|
||||
pluginId: 'uni-cms'
|
||||
}).config()
|
||||
|
||||
|
||||
module.exports = {
|
||||
_before: function () {
|
||||
// 从配置中心获取内容安全配置
|
||||
|
||||
// 如果内容安全配置存在
|
||||
if (config.contentSecurity) {
|
||||
// 引入内容安全检测模块
|
||||
const UniSecCheck = safeRequire('uni-sec-check')
|
||||
// 实例化内容安全检测模块
|
||||
this.uniSecCheck = new UniSecCheck({
|
||||
provider: 'mp-weixin', // 使用微信小程序的内容安全检测
|
||||
requestId: this.getUniCloudRequestId() // 请求ID为当前云函数请求ID
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async checkImageSec (image) {
|
||||
// 检查内容安全是否开启
|
||||
// 如果内容安全未开启,则直接返回一个对象,包含错误码为0。表示检测跳过
|
||||
if (!checkContentSecurityEnable(config.contentSecurity, 'image')) {
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
}
|
||||
// 调用内容安全检测模块进行图片检测
|
||||
const res = await this.uniSecCheck.imgSecCheck({
|
||||
image, // 图片url
|
||||
scene: 1, // 表示检测场景为资料场景
|
||||
version: 1 // 表示使用内容安全检测API的版本为1
|
||||
})
|
||||
|
||||
// 如果图片违规,删除图片并抛出异常
|
||||
if (res.errCode === this.uniSecCheck.ErrorCode.RISK_CONTENT) {
|
||||
await uniCloud.deleteFile({
|
||||
fileList: [image]
|
||||
})
|
||||
|
||||
throw {
|
||||
errCode: 'uni-cms-risk-content',
|
||||
errMsg: '图片违规已删除'
|
||||
}
|
||||
} else if (res.errCode !== 0) {
|
||||
// 如果内容安全检测异常,抛出异常并打印错误信息
|
||||
console.error(res)
|
||||
throw new Error('内容安全检测异常:' + res.errCode)
|
||||
}
|
||||
|
||||
return {
|
||||
errCode: 0
|
||||
}
|
||||
},
|
||||
|
||||
_after: function (error, result) {
|
||||
if (error) {
|
||||
return {
|
||||
errCode: error.errCode || -1,
|
||||
errMsg: error.errMsg || error.message
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "uni-cms-co",
|
||||
"dependencies": {
|
||||
"uni-config-center": "file:..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center",
|
||||
"uni-sec-check": "file:..\/..\/..\/..\/uni-sec-check\/uniCloud\/cloudfunctions\/common\/uni-sec-check"
|
||||
},
|
||||
"extensions": {
|
||||
"uni-cloud-jql": {}
|
||||
},
|
||||
"cloudfunction-config": {
|
||||
"timeout": 60,
|
||||
"runtime": "Nodejs12"
|
||||
},
|
||||
"origin-plugin-dev-name": "uni-cms",
|
||||
"origin-plugin-version": "1.0.20",
|
||||
"plugin-dev-name": "uni-cms",
|
||||
"plugin-version": "1.0.20"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 为模块添加安全的 require 方法
|
||||
exports.safeRequire = function (module) {
|
||||
try {
|
||||
return require(module)
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
// 如果模块不存在,抛出错误提示用户添加模块
|
||||
throw new Error(`${module} 公共模块不存在,请在 uni-cms-co 目录右击"管理公共模块或扩展依赖"添加 ${module} 模块`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查内容安全是否启用
|
||||
exports.checkContentSecurityEnable = function (config, field) {
|
||||
// 1. 从配置中心获取配置
|
||||
return config && config.allowCheckType && config.allowCheckType.includes(field)
|
||||
}
|
||||
Reference in New Issue
Block a user