This commit is contained in:
c
2025-03-12 20:11:03 +08:00
commit 70b7c82c97
1178 changed files with 148185 additions and 0 deletions
@@ -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)
}