Files
2025-06-13 21:16:12 +08:00

389 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 云对象教程: 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: "今日 ai 对话已达上限,请明天尝试对话"
}
}
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
}
}
}
}
}