index.obj.js 12.4 KB
Newer Older
DCloud_JSON's avatar
DCloud_JSON 已提交
1
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
DCloud_JSON's avatar
DCloud_JSON 已提交
2
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
DCloud_JSON's avatar
DCloud_JSON 已提交
3
// 引入utils模块中的safeRequire和checkContentSecurityEnable函数
DCloud_JSON's avatar
DCloud_JSON 已提交
4
const {safeRequire, checkContentSecurityEnable} = require('./utils')
DCloud_JSON's avatar
DCloud_JSON 已提交
5
// 引入uni-config-center模块,并创建config对象
DCloud_JSON's avatar
DCloud_JSON 已提交
6 7 8 9
const createConfig = safeRequire('uni-config-center')
const config = createConfig({
	pluginId: 'uni-ai-chat'
}).config()
DCloud_JSON's avatar
DCloud_JSON 已提交
10
// 引入uniCloud.database()方法,并创建db对象
11
const db = uniCloud.database();
DCloud_JSON's avatar
DCloud_JSON 已提交
12
// 创建userscollection对象
13
const userscollection = db.collection('uni-id-users')
DCloud_JSON's avatar
DCloud_JSON 已提交
14
// 引入uni-id-common模块
15
const uniIdCommon = require('uni-id-common')
DCloud_JSON's avatar
DCloud_JSON 已提交
16 17 18


module.exports = {
19
	_before:async function() {
DCloud_JSON's avatar
DCloud_JSON 已提交
20
		// 这里是云函数的前置方法,你可以在这里加入你需要逻辑
21
	   
DCloud_JSON's avatar
DCloud_JSON 已提交
22
		// 判断否调用量本云对象的send方法
23 24
		if(this.getMethodName() == 'send'){
			// 从配置中心获取是否需要销毁积分
DCloud_JSON's avatar
DCloud_JSON 已提交
25
			if(config.spentScore){
26 27 28 29 30 31 32 33 34 35
				
			/*先校验token(用户身份令牌)是否有效,并获得用户的_id*/
				// 获取客户端信息
				this.clientInfo = this.getClientInfo()
				// console.log(this.clientInfo);
				
				// 定义uni-id公共模块对象
				this.uniIdCommon = uniIdCommon.createInstance({
					clientInfo: this.clientInfo
				})
DCloud_JSON's avatar
DCloud_JSON 已提交
36 37
				// 校验token(用户身份令牌)是否有效,并获得用户的_id
				let res = await this.uniIdCommon.checkToken(this.clientInfo.uniIdToken) 
38 39 40 41 42 43
				if (res.errCode) {
					// 如果token校验出错,则抛出错误
					throw res
				}else{
					// 通过token校验则,拿去用户id
					this.current_uid = res.uid
DCloud_JSON's avatar
DCloud_JSON 已提交
44
				}
45 46 47
			/* 判断剩余多少积分:拒绝对话、扣除配置的积分数 */
				let {data:[{score}]} = await userscollection.doc(this.current_uid).field({'score':1}).get()
				console.log('score----',score);
DCloud_JSON's avatar
DCloud_JSON 已提交
48 49 50
				// 如果积分数小于等于0 则抛出错误提醒客户端
				// 注意需要判断小于0 因为特殊的情况下可能花超过
				if(score == 0 || score < 0){ 
51
					throw "insufficientScore"
52
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
53
				// 扣除对应的积分值
54 55
				await userscollection.doc(this.current_uid)
						.update({
DCloud_JSON's avatar
DCloud_JSON 已提交
56
							score:db.command.inc(-1 * config.spentScore)
57 58 59 60
						})
			}
			
			// 从配置中心获取内容安全配置
61
			console.log('config.contentSecurity',config.contentSecurity);
62
			if (config.contentSecurity) {
DCloud_JSON's avatar
DCloud_JSON 已提交
63
				// 引入uni-sec-check模块
64
				const UniSecCheck = safeRequire('uni-sec-check')
DCloud_JSON's avatar
DCloud_JSON 已提交
65
				// 创建uniSecCheck对象
66 67 68
				const uniSecCheck = new UniSecCheck({
					provider: 'mp-weixin',
					requestId: this.getUniCloudRequestId()
DCloud_JSON's avatar
DCloud_JSON 已提交
69
				})
DCloud_JSON's avatar
DCloud_JSON 已提交
70
				// 定义文本安全检测函数
71
				this.textSecCheck = async (content)=>{
DCloud_JSON's avatar
DCloud_JSON 已提交
72
					// 获取sseChannel
DCloud_JSON's avatar
DCloud_JSON 已提交
73
					let {sseChannel} = this.getParams()[0]||{}
DCloud_JSON's avatar
DCloud_JSON 已提交
74
					// 如果存在sseChannel,则抛出错误
DCloud_JSON's avatar
DCloud_JSON 已提交
75
					if(sseChannel){
76 77 78 79 80
						throw {
							errSubject: 'uni-ai-chat',
							errCode: "sec-check",
							errMsg: "流式响应模式,内容安全识别功能无效"
						}
DCloud_JSON's avatar
DCloud_JSON 已提交
81
					}
82 83
					// 检测文本
					const checkRes = await uniSecCheck.textSecCheck({
DCloud_JSON's avatar
DCloud_JSON 已提交
84
						// 文本内容,不可超过500KB
85
						content,
DCloud_JSON's avatar
DCloud_JSON 已提交
86
						// 微信小程序端 开放的唯一用户标识符
87
						// openid,
DCloud_JSON's avatar
DCloud_JSON 已提交
88
						// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
89
						scene:4,
DCloud_JSON's avatar
DCloud_JSON 已提交
90 91
						// 接口版本号,可选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 
92 93
					})
					console.log('checkRes检测文本',checkRes);
DCloud_JSON's avatar
DCloud_JSON 已提交
94
					// 如果检测到风险内容,则抛出错误
95 96 97 98 99 100
					if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
						console.error({
							errCode: checkRes.errCode,
							errMsg: '文字存在风险',
							result: checkRes.result
						});
DCloud_JSON's avatar
DCloud_JSON 已提交
101
						throw "uni-sec-check:illegalData"
DCloud_JSON's avatar
DCloud_JSON 已提交
102 103
						// 如果检测出错,则抛出错误
					} else if (checkRes.errCode) { 
104 105 106 107 108 109
						console.log(`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`);
						console.error({
							errCode: checkRes.errCode,
							errMsg: checkRes.errMsg,
							result: checkRes.result
						});
DCloud_JSON's avatar
DCloud_JSON 已提交
110
						throw "uni-sec-check:illegalData"
DCloud_JSON's avatar
DCloud_JSON 已提交
111 112
					}
				}
113
				
DCloud_JSON's avatar
DCloud_JSON 已提交
114
				// 获取messages参数
115
				let {messages} = this.getParams()[0]||{"messages":[]}
DCloud_JSON's avatar
DCloud_JSON 已提交
116
				// 将messages中的content拼接成字符串
117 118
				let contentString = messages.map(i=>i.content).join(' ')
				console.log('contentString',contentString);
DCloud_JSON's avatar
DCloud_JSON 已提交
119
				// 对contentString进行文本安全检测
120
				await this.textSecCheck(contentString)
DCloud_JSON's avatar
DCloud_JSON 已提交
121

DCloud_JSON's avatar
DCloud_JSON 已提交
122
			}
123
		}
DCloud_JSON's avatar
DCloud_JSON 已提交
124 125
	},
	async _after(error, result) {
DCloud_JSON's avatar
DCloud_JSON 已提交
126 127 128 129
		// 打印错误和结果
		console.log('_after',{error,result}); 
		// 如果有错误
		if(error){ 
130 131 132 133
			if(error.errCode && error.errMsg) { 
				// 符合响应体规范的错误,直接返回
				return error
			}
DCloud_JSON's avatar
DCloud_JSON 已提交
134
			// 如果是内容安全检测错误
135
			else if(error == "uni-sec-check:illegalData" ) { 
DCloud_JSON's avatar
DCloud_JSON 已提交
136 137
				// 返回一个包含敏感内容提示和标记的响应体
				return { 
DCloud_JSON's avatar
DCloud_JSON 已提交
138
					"data": {
139 140
						"reply": "内容涉及敏感",
						"illegal":true
DCloud_JSON's avatar
DCloud_JSON 已提交
141 142 143
					},
					"errCode": 0
				}
144
			} 
DCloud_JSON's avatar
DCloud_JSON 已提交
145 146 147 148 149 150 151 152 153 154 155 156 157 158
			// 如果是积分不足错误
			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({ 
159
						"insufficientScore":true
160
					})
DCloud_JSON's avatar
DCloud_JSON 已提交
161 162 163 164

				}else{ 
					// 如果不存在sseChannel 返回一个包含回复内容和标记的响应体
					return { 
165 166
						"data": {
							reply,
167
							"insufficientScore":true
168 169 170 171
						},
						"errCode": 0
					}
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
172 173 174
			}else{ 
				// 如果是其他错误
				throw error  // 直接抛出异常
DCloud_JSON's avatar
DCloud_JSON 已提交
175 176
			}
		}
DCloud_JSON's avatar
DCloud_JSON 已提交
177 178 179

		// 如果是send方法且开启了内容安全检测
		if (this.getMethodName() == 'send' && config.contentSecurity) { 
DCloud_JSON's avatar
DCloud_JSON 已提交
180
			try{
DCloud_JSON's avatar
DCloud_JSON 已提交
181 182 183 184
				// 对回复内容进行文本安全检测
				await this.textSecCheck(result.data.reply) 
			}catch(e){ 
				// 如果检测到敏感内容 返回一个包含敏感内容提示和标记的响应体
DCloud_JSON's avatar
DCloud_JSON 已提交
185 186
				return {
					"data": {
187 188
						"reply": "内容涉及敏感",
						"illegal":true
DCloud_JSON's avatar
DCloud_JSON 已提交
189 190 191 192
					},
					"errCode": 0
				}
			}
DCloud_JSON's avatar
DCloud_JSON 已提交
193
		}
DCloud_JSON's avatar
DCloud_JSON 已提交
194
		// 返回处理后的结果
DCloud_JSON's avatar
DCloud_JSON 已提交
195 196
		return result
	},
DCloud_JSON's avatar
DCloud_JSON 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212


	// 发送消息
	async send({
		// 消息内容
		messages, 
		// sse渠道对象
		sseChannel 
	}) {

		// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
		// messages =  [{
		// 	role: 'user',
		// 	content: 'uni-app是什么,20个字以内进行说明'
		// }]

DCloud_JSON's avatar
DCloud_JSON 已提交
213
		// 校验客户端提交的参数
DCloud_JSON's avatar
DCloud_JSON 已提交
214 215 216 217 218 219
		// 检查消息是否符合规范
		let res = checkMessages(messages)
		if (res.errCode) {
			throw new Error(res.errMsg)
		}

220
		// 向uni-ai发送消息
DCloud_JSON's avatar
DCloud_JSON 已提交
221
		// 调用chatCompletion函数,传入messages、sseChannel、llm参数
222 223 224
		let {llm,chatCompletionOptions} = config
		return await chatCompletion({
			messages, //消息内容
DCloud_JSON's avatar
DCloud_JSON 已提交
225
			sseChannel, //sse渠道对象
226
			llm
DCloud_JSON's avatar
DCloud_JSON 已提交
227 228 229 230 231 232 233 234 235 236 237 238
		})

		// chatCompletion函数:对话完成
		async function chatCompletion({
			// 消息列表
			messages, 
			// 是否需要总结
			summarize = false, 
			// sse渠道对象
			sseChannel = false, 
			// 语言模型
			llm 
239
		}) {
DCloud_JSON's avatar
DCloud_JSON 已提交
240 241 242
			// 获取语言模型管理器
			const llmManager = uniCloud.ai.getLLMManager(llm)
			// 调用chatCompletion方法,传入参数
243
			let res = await llmManager.chatCompletion({
DCloud_JSON's avatar
DCloud_JSON 已提交
244 245 246 247 248 249 250 251 252 253
				...chatCompletionOptions,
				messages,
				stream: sseChannel !== false
			})

			// 如果存在sseChannel
			if (sseChannel) {
				let reply = ""
				return new Promise((resolve, reject) => {
					// 反序列化sseChannel
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
254 255 256
					const channel = uniCloud.deserializeSSEChannel(sseChannel)
					// 判断如果是open-ai按字返回,否则按行返回
					if(llm && llm.provider && llm.provider == "openai"){
DCloud_JSON's avatar
DCloud_JSON 已提交
257
						// 按字返回
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
258 259 260
						res.on('message', async (message) => {
							reply += message
							await channel.write(message)
DCloud_JSON's avatar
DCloud_JSON 已提交
261
							// console.log('---message----', message)
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
262 263
						})
					}else{
DCloud_JSON's avatar
DCloud_JSON 已提交
264
						// 按行返回
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
265 266 267
						res.on('line', async (line) => {
							await channel.write(reply? ("\n\n " + line) : line)
							reply += line
DCloud_JSON's avatar
DCloud_JSON 已提交
268
							// console.log('---line----', line)
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
269 270
						})
					}
DCloud_JSON's avatar
DCloud_JSON 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
					// 结束返回
					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
						})
					})
					// 返回错误
302 303 304 305 306 307 308 309 310 311 312 313 314 315
					res.on('error',async (error) => {
						// 特殊处理 uni-ai默认服务商检测到内容涉及敏感的错误
						if(error.errCode == "60004"){
							await channel.write("内容涉及敏感")
							// 结束sseChannel并返回 illegal:true 表示内容涉及敏感
							await channel.end({
								illegal: true
							})
							return resolve({
								errCode: 0
							})
						}
						console.error('---error----', error)
						reject(error)
DCloud_JSON's avatar
DCloud_JSON 已提交
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
					})
				})
			} 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
					}
DCloud_JSON's avatar
DCloud_JSON 已提交
335
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
336
				// 如果存在错误
DCloud_JSON's avatar
DCloud_JSON 已提交
337
				if(res.errCode){
DCloud_JSON's avatar
DCloud_JSON 已提交
338
					// 抛出错误
DCloud_JSON's avatar
DCloud_JSON 已提交
339
					throw res
DCloud_JSON's avatar
DCloud_JSON 已提交
340
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
341
				// 返回处理结果
DCloud_JSON's avatar
DCloud_JSON 已提交
342
				return {
DCloud_JSON's avatar
DCloud_JSON 已提交
343 344
					data:res,
					errCode: 0
DCloud_JSON's avatar
DCloud_JSON 已提交
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
				}
			}
		}


		//获总结
		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
				}
			}
		}
	}
DCloud_JSON's avatar
DCloud_JSON 已提交
424
}