// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
// 引入utils模块中的safeRequire和checkContentSecurityEnable函数
const {safeRequire} = require('./utils')
// 引入uni-config-center模块,并创建config对象
const createConfig = safeRequire('uni-config-center')
const config = createConfig({
pluginId: 'uni-ai-chat'
}).config()
// 引入uni-id-common模块
const uniIdCommon = require('uni-id-common')
module.exports = {
_before:async function() {
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑
// 判断否调用量本云对象的send方法
if(this.getMethodName() == 'send'){
// 从配置中心获取内容安全配置
// console.log('config.contentSecurity',config.contentSecurity);
if (config.contentSecurity) {
// 引入uni-sec-check模块
const UniSecCheck = safeRequire('uni-sec-check')
// 创建uniSecCheck对象
const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin',
requestId: this.getUniCloudRequestId()
})
// 定义文本安全检测函数
this.textSecCheck = async (content)=>{
// 获取sseChannel
let {sseChannel} = this.getParams()[0]||{}
// 如果存在sseChannel,则抛出错误
if(sseChannel){
throw {
errSubject: 'uni-ai-chat',
errCode: "sec-check",
errMsg: "流式响应模式,内容安全识别功能无效"
}
}
// 检测文本
const checkRes = await uniSecCheck.textSecCheck({
// 文本内容,不可超过500KB
content,
// 微信小程序端 开放的唯一用户标识符
// openid,
// 场景值(1 资料;2 评论;3 论坛;4 社交日志)
scene:1,
// 接口版本号,可选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:2
})
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"
}
}
// 获取messages参数
let {messages} = this.getParams()[0]||{"messages":[]}
// 将messages中的content拼接成字符串
let contentString = messages.map(i=>i.content).join(' ')
console.log('contentString',contentString);
// 对contentString进行文本安全检测
await this.textSecCheck(contentString)
}
}
},
async _after(error, result) {
// 打印错误和结果
// console.log('_after',{error,result});
// 如果有错误
if(error){
// 如果是内容安全检测错误
if(error.errCode == 60004 || error == "uni-sec-check:illegalData" ) {
// 返回一个包含敏感内容提示和标记的响应体
return {
"data": {
"reply": "内容涉及敏感",
"illegal":true
},
"errCode": 0
}
}
// 其他符合响应体规范的错误,直接返回
else if(error.errCode && error.errMsg) {
return error
}else{
// 如果是其他错误
throw error // 直接抛出异常
}
}
// 如果是send方法且开启了内容安全检测
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,
// sse渠道对象
sseChannel
}) {
// 校验客户端提交的参数
// 检查消息是否符合规范
let res = checkMessages(messages)
if (res.errCode) {
throw new Error(res.errMsg)
}
// 向uni-ai发送消息
// 调用chatCompletion函数,传入messages、sseChannel、llm参数
let {llm,chatCompletionOptions} = config
return await chatCompletion({
messages, //消息内容
sseChannel, //sse渠道对象
llm
})
// chatCompletion函数:对话完成
async function chatCompletion({
// 消息列表
messages,
// 是否需要总结
summarize = false,
// sse渠道对象
sseChannel = false,
// 语言模型
llm
}) {
// 获取语言模型管理器
const llmManager = uniCloud.ai.getLLMManager(llm)
// 调用chatCompletion方法,传入参数
let res = await llmManager.chatCompletion({
...chatCompletionOptions,
messages,
stream: sseChannel !== false
})
// 如果存在sseChannel
if (sseChannel) {
let reply = ""
return new Promise((resolve, reject) => {
// 反序列化sseChannel
const channel = uniCloud.deserializeSSEChannel(sseChannel)
// 按字返回
res.on('message', async (message) => {
// console.log('---message----', message)
reply += message
await channel.write(message)
})
// 结束返回
res.on('end', async () => {
console.log('---end----结束返回',reply)
// 将回复内容添加到消息列表中
messages.push({
"content": reply,
"role": "assistant"
})
await channel.end()
// 返回处理结果
resolve({
errCode: 0
})
})
// 返回错误
res.on('error',async (error) => {
console.error('---error----', error)
// 特殊处理 uni-ai默认服务商检测到内容涉及敏感的错误
if(error.errCode == "60004"){
await channel.write("内容涉及敏感")
// 结束sseChannel并返回 illegal:true 表示内容涉及敏感
await channel.end({
illegal: true
})
return resolve({
errCode: 0
})
}
reject(error)
})
})
} else {
console.log('不存在sseChannel');
// 如果不需要总结
if (summarize == false) {
// 将回复内容添加到消息列表中
messages.push({
"content": res.reply,
"role": "assistant"
})
}
// 如果存在错误
if(res.errCode){
// 抛出错误
throw res
}
// 返回处理结果
return {
data:res,
errCode: 0
}
}
}
/**
* 校验消息内容是否符合规范
* @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
}
}
}
}
}
// 初始化sse通道
let sseChannel = false
// 引入markdown-it库
import MarkdownIt from '@/lib/markdown-it.min.js';
// hljs是由 Highlight.js 经兼容性修改后的文件,请勿直接升级。否则会造成uni-app-vue3-Android下有兼容问题
import hljs from "@/lib/highlight/highlight-uni.min.js";
const md = new MarkdownIt({
breaks: true, // 设置为true,即可实现冒号、分号、句号换行的效果
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code class="hljs '+lang+'">' +
hljs.highlight(lang, str, true).value +
'</code></pre>';
} catch (__) {}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
},
html: true
})
export default {
data(){
return{
// 记录流式响应次数
sseIndex: 0,
lastMsg:{
content:'',
markdownHtml:null
}
}
},
methods:{
async setGPT(msg){
sseChannel = new uniCloud.SSEChannel() // 创建消息通道
sseChannel.on('message', message => {
// 监听message事件
console.log('on message', message)
this.lastMsg.content += message
let html = md.render(this.lastMsg.content)
this.lastMsg.markdownHtml = html
// console.log(this.lastMsg.markdownHtml);
// 让流式响应计数值递增
this.sseIndex++
})
sseChannel.on('end', message => {
// 监听end事件,如果云端执行end时传了message,会在客户端end事件内收到传递的消息
console.log('on end', message)
// 结束流式响应 将流式响应计数值 设置为 0
this.sseIndex = 0
})
await sseChannel.open() // 等待通道开启
const chatgpt = uniCloud.importObject('hszy-chatgpt', {
customUI: true
})
chatgpt
.send({
messages: msg,
sseChannel
})
.then(res => {
console.log(res)
if (!sseChannel) {
if (!res.data) {
return
}
console.log(res, res.reply)
} else {
// 处理 sseChannel没结束 云函数提前结束的情况
sseChannel.close()
this.sseIndex = 0
}
})
.catch(() => {
sseChannel.close()
this.sseIndex = 0
})
}
}
}
从uni-ai的基础上,写了一个minxins,用来实现一问一答,SSEChannel总是没有对完话就执行
res.on('end', async () => {
console.log('---end----结束返回',reply),
两次会复现一次,百分之五十的概率
4 个回复
最佳回复
DCloud_uniCloud_WYQ
经排查,此问题是由于ai总结耗时导致了云函数执行超时未返回。uni-ai-chat已发布新版(1.1.1)优化此问题
m***@163.com (作者)
响应一半直接中断执行res.on('end'),和请求时,system里面填充的提示词太多有关联吗?
DCloud_uniCloud_WYQ
AI响应多少个字的时候触发的end,只有云端调用了end,客户端才会触发end事件
m***@163.com (作者)
之前这个问题“响应一半直接中断执行res.on('end')”,改了下tokensToGenerate,已经解决了,但又遇到了新的问题,在云端服务器,日志里面可以看到,响应的全文chatgpt都已经全部展示了,但向客户端推送了一半就停止了,直接去执行了函数的res,可看下图
2023-06-09 14:36
m***@163.com (作者)
给客户端返回了一半数据