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);
48 49
				// 如果积分不足 则抛出错误提醒客户端
				if(score < config.spentScore){ 
50
					throw "insufficientScore"
51
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
52
				// 扣除对应的积分值
53 54
				await userscollection.doc(this.current_uid)
						.update({
DCloud_JSON's avatar
DCloud_JSON 已提交
55
							score:db.command.inc(-1 * config.spentScore)
56 57 58 59
						})
			}
			
			// 从配置中心获取内容安全配置
60
			console.log('config.contentSecurity',config.contentSecurity);
61
			if (config.contentSecurity) {
DCloud_JSON's avatar
DCloud_JSON 已提交
62
				// 引入uni-sec-check模块
63
				const UniSecCheck = safeRequire('uni-sec-check')
DCloud_JSON's avatar
DCloud_JSON 已提交
64
				// 创建uniSecCheck对象
65 66 67
				const uniSecCheck = new UniSecCheck({
					provider: 'mp-weixin',
					requestId: this.getUniCloudRequestId()
DCloud_JSON's avatar
DCloud_JSON 已提交
68
				})
DCloud_JSON's avatar
DCloud_JSON 已提交
69
				// 定义文本安全检测函数
70
				this.textSecCheck = async (content)=>{
DCloud_JSON's avatar
DCloud_JSON 已提交
71
					// 获取sseChannel
DCloud_JSON's avatar
DCloud_JSON 已提交
72
					let {sseChannel} = this.getParams()[0]||{}
DCloud_JSON's avatar
DCloud_JSON 已提交
73
					// 如果存在sseChannel,则抛出错误
DCloud_JSON's avatar
DCloud_JSON 已提交
74
					if(sseChannel){
75 76 77 78 79
						throw {
							errSubject: 'uni-ai-chat',
							errCode: "sec-check",
							errMsg: "流式响应模式,内容安全识别功能无效"
						}
DCloud_JSON's avatar
DCloud_JSON 已提交
80
					}
81 82
					// 检测文本
					const checkRes = await uniSecCheck.textSecCheck({
DCloud_JSON's avatar
DCloud_JSON 已提交
83
						// 文本内容,不可超过500KB
84
						content,
DCloud_JSON's avatar
DCloud_JSON 已提交
85
						// 微信小程序端 开放的唯一用户标识符
86
						// openid,
DCloud_JSON's avatar
DCloud_JSON 已提交
87
						// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
88
						scene:4,
DCloud_JSON's avatar
DCloud_JSON 已提交
89 90
						// 接口版本号,可选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 
91 92
					})
					console.log('checkRes检测文本',checkRes);
DCloud_JSON's avatar
DCloud_JSON 已提交
93
					// 如果检测到风险内容,则抛出错误
94 95 96 97 98 99
					if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
						console.error({
							errCode: checkRes.errCode,
							errMsg: '文字存在风险',
							result: checkRes.result
						});
DCloud_JSON's avatar
DCloud_JSON 已提交
100
						throw "uni-sec-check:illegalData"
DCloud_JSON's avatar
DCloud_JSON 已提交
101 102
						// 如果检测出错,则抛出错误
					} else if (checkRes.errCode) { 
103 104 105 106 107 108
						console.log(`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`);
						console.error({
							errCode: checkRes.errCode,
							errMsg: checkRes.errMsg,
							result: checkRes.result
						});
DCloud_JSON's avatar
DCloud_JSON 已提交
109
						throw "uni-sec-check:illegalData"
DCloud_JSON's avatar
DCloud_JSON 已提交
110 111
					}
				}
112
				
DCloud_JSON's avatar
DCloud_JSON 已提交
113
				// 获取messages参数
114
				let {messages} = this.getParams()[0]||{"messages":[]}
DCloud_JSON's avatar
DCloud_JSON 已提交
115
				// 将messages中的content拼接成字符串
116 117
				let contentString = messages.map(i=>i.content).join(' ')
				console.log('contentString',contentString);
DCloud_JSON's avatar
DCloud_JSON 已提交
118
				// 对contentString进行文本安全检测
119
				await this.textSecCheck(contentString)
DCloud_JSON's avatar
DCloud_JSON 已提交
120

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

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

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


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

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

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

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

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

			// 如果存在sseChannel
			if (sseChannel) {
250
				let reply = "" 
DCloud_JSON's avatar
DCloud_JSON 已提交
251 252
				return new Promise((resolve, reject) => {
					// 反序列化sseChannel
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
253 254 255
					const channel = uniCloud.deserializeSSEChannel(sseChannel)
					// 判断如果是open-ai按字返回,否则按行返回
					if(llm && llm.provider && llm.provider == "openai"){
DCloud_JSON's avatar
DCloud_JSON 已提交
256
						// 按字返回
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
257 258 259
						res.on('message', async (message) => {
							reply += message
							await channel.write(message)
DCloud_JSON's avatar
DCloud_JSON 已提交
260
							// console.log('---message----', message)
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
261 262
						})
					}else{
DCloud_JSON's avatar
DCloud_JSON 已提交
263
						// 按行返回
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
264
						res.on('line', async (line) => {
265 266 267
							if(reply.length){
								line = " \n\n " + line
							}
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
268
							reply += line
269
							await channel.write(line)
DCloud_JSON's avatar
DCloud_JSON 已提交
270
							// console.log('---line----', line)
DCloud_JSON's avatar
1.0.3  
DCloud_JSON 已提交
271 272
						})
					}
DCloud_JSON's avatar
DCloud_JSON 已提交
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 302 303
					// 结束返回
					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
						})
					})
					// 返回错误
304 305 306 307 308 309 310 311 312 313 314 315 316 317
					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 已提交
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
					})
				})
			} 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 已提交
337
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
338
				// 如果存在错误
DCloud_JSON's avatar
DCloud_JSON 已提交
339
				if(res.errCode){
DCloud_JSON's avatar
DCloud_JSON 已提交
340
					// 抛出错误
DCloud_JSON's avatar
DCloud_JSON 已提交
341
					throw res
DCloud_JSON's avatar
DCloud_JSON 已提交
342
				}
DCloud_JSON's avatar
DCloud_JSON 已提交
343
				// 返回处理结果
DCloud_JSON's avatar
DCloud_JSON 已提交
344
				return {
DCloud_JSON's avatar
DCloud_JSON 已提交
345 346
					data:res,
					errCode: 0
DCloud_JSON's avatar
DCloud_JSON 已提交
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 424 425
				}
			}
		}


		//获总结
		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 已提交
426
}