提交 ffc52eee 编写于 作者: DCloud_JSON's avatar DCloud_JSON

新增支持内容安全识别

上级 6f692bae
## 1.0.1(2023-04-25)
不再提供stream设置ui,自动根据是否开通并启用uni-push设置
## 1.0.0(2023-04-24) ## 1.0.0(2023-04-24)
1.0.0 1.0.0
{ {
"name" : "uni-ai-chat", "name" : "uni-ai-chat",
"appid" : "__UNI__44E0E31", "appid" : "__UNI__8F14B14",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",
......
{
"name": "uni-ai-chat",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "uni-ai-chat",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"highlight.js": "^11.7.0",
"markdown-it": "^13.0.1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"engines": {
"node": ">=0.12"
}
},
"node_modules/highlight.js": {
"version": "11.7.0",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.7.0.tgz",
"integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/markdown-it": {
"version": "13.0.1",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.1.tgz",
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
}
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"entities": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
},
"highlight.js": {
"version": "11.7.0",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.7.0.tgz",
"integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ=="
},
"linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"markdown-it": {
"version": "13.0.1",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.1.tgz",
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
"requires": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
}
}
}
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
"id": "uni-ai-chat", "id": "uni-ai-chat",
"name": "uni-ai-chat", "name": "uni-ai-chat",
"version": "1.0.0", "version": "1.0.0",
"description": "云端一体uni-ai示例项目", "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": {}, "repository": "https://github.com/dcloudio/uni-ui",
"keywords": [ "keywords": [
"uni-ai-chat" "uni-ai-chat"
], ],
...@@ -35,7 +35,80 @@ ...@@ -35,7 +35,80 @@
"data": "无", "data": "无",
"permissions": "无" "permissions": "无"
}, },
"npmurl": "", "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "unicloud-template-project" "type": "unicloud-template-project"
},
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
} }
} }
\ No newline at end of file
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
:type="msgStateIcon(msg)" class="msgStateIcon"> :type="msgStateIcon(msg)" class="msgStateIcon">
</uni-icons> </uni-icons>
</view> </view>
</view>
<view class="tip-ai-ing" v-if="msgList.length && msgList.length%2 !== 0">
<text>uni-ai正在思考中...</text>
<view v-if="NODE_ENV == 'development' && !stream">
如需提速,请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html" text="[流式响应]"></uni-link>
</view>
</view> </view>
<view id="last-msg-item"></view> <view id="last-msg-item"></view>
</scroll-view> </scroll-view>
...@@ -30,16 +36,8 @@ ...@@ -30,16 +36,8 @@
<view class="trash menu-item"> <view class="trash menu-item">
<image @click="clear" src="@/static/remove.png" mode="heightFix"></image> <image @click="clear" src="@/static/remove.png" mode="heightFix"></image>
</view> </view>
<view class="set-stream menu-item">
<view class="title">
<text>流式响应</text>
<uni-icons @click="toStreamMD" class="help" type="help"></uni-icons>
<text>:</text>
</view>
<switch :checked="stream" @change="changeStream" />
</view>
</view> </view>
<view class="foot-box-content"> <view class="foot-box-content">
<view v-if="!isWidescreen" class="trash"> <view v-if="!isWidescreen" class="trash">
<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons> <uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons>
...@@ -54,15 +52,6 @@ ...@@ -54,15 +52,6 @@
</view> </view>
</view> </view>
</view> </view>
<view v-if="!isWidescreen" id="set-stream">
<view class="title">
<text>流式响应</text>
<uni-icons @click="toStreamMD" class="help" type="help"></uni-icons>
<text>:</text>
</view>
<switch :checked="stream" @change="changeStream" />
</view>
</view> </view>
</view> </view>
</template> </template>
...@@ -75,7 +64,7 @@ ...@@ -75,7 +64,7 @@
msgList: [], msgList: [],
content: "", content: "",
sseIndex: 0, sseIndex: 0,
stream:false, stream:true,
isWidescreen:false isWidescreen:false
} }
}, },
...@@ -84,13 +73,7 @@ ...@@ -84,13 +73,7 @@
if (this.sseIndex !== 0) { if (this.sseIndex !== 0) {
return true return true
} }
return !!(this.msgList.length && this.msgList.length%2 !== 0)
let length = this.msgList.length
if (length) {
return !this.msgList[length - 1].isAi
} else {
return false
}
}, },
placeholderText() { placeholderText() {
if (this.inputBoxDisabled) { if (this.inputBoxDisabled) {
...@@ -101,6 +84,9 @@ ...@@ -101,6 +84,9 @@
// #endif // #endif
return '请输入要发给uni-ai的内容' return '请输入要发给uni-ai的内容'
} }
},
NODE_ENV(){
return process.env.NODE_ENV
} }
}, },
watch: { watch: {
...@@ -111,7 +97,7 @@ ...@@ -111,7 +97,7 @@
deep:true deep:true
} }
}, },
async mounted() { async mounted() {
// for (let i = 0; i < 15; i++) { // for (let i = 0; i < 15; i++) {
// this.msgList.push({ // this.msgList.push({
// isAi: i % 2 == true, // isAi: i % 2 == true,
...@@ -150,46 +136,7 @@ ...@@ -150,46 +136,7 @@
} }
// #endif // #endif
// 添加惰性函数,检查是否开通push // 添加惰性函数,检查是否开通uni-push;决定是否启用stream
this.changeStream.check = async ()=>{
uni.getPushClientId({
fail:()=> {
this.stream = false
uni.showModal({
content: '应用暂未开通uni-push。不支持此功能',
confirmText:"查看详情",
complete(e) {
if(!e.confirm){
return
}
let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#heed"
// #ifndef H5
uni.setClipboardData({
data:url,
showToast:false,
success() {
uni.showToast({
title: '已复制文档链接,请到浏览器粘贴浏览',
icon: 'none',
duration:5000
});
}
})
// #endif
// #ifdef H5
window.open(url)
// #endif
}
});
console.log('你暂未开通uni-push。不支持此功能。详情:https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9');
},
success:(e)=>{
console.log('success',e);
this.changeStream.check = ()=>{}
}
})
}
// #ifdef H5 // #ifdef H5
uni.createMediaQueryObserver(this).observe({ uni.createMediaQueryObserver(this).observe({
...@@ -200,19 +147,27 @@ ...@@ -200,19 +147,27 @@
// #endif // #endif
}, },
methods: { methods: {
//检查是否开通uni-push;决定是否启用stream
async checkIsOpenPush(){
try{
await uni.getPushClientId()
this.checkIsOpenPush = ()=>{}
}catch(err){
this.stream = false
}
},
// updateLastMsg(){ // updateLastMsg(){
// }, // },
changeStream(e){ async retriesSendMsg() {
this.changeStream.check() // 检查是否开通uni-push;决定是否启用stream
await this.checkIsOpenPush()
// console.log('e',e.detail.value);
this.stream = e.detail.value
},
retriesSendMsg() {
this.send() this.send()
}, },
beforeSendMsg() { async beforeSendMsg() {
// 检查是否开通uni-push;决定是否启用stream
await this.checkIsOpenPush()
if(!this.content){ if(!this.content){
return uni.showToast({ return uni.showToast({
title: '内容不能为空', title: '内容不能为空',
...@@ -319,8 +274,8 @@ ...@@ -319,8 +274,8 @@
// console.log(res, res.reply); // console.log(res, res.reply);
this.msgList.push({ this.msgList.push({
isAi: true, isAi: true,
content: res.reply, content: res.data.reply,
summarize: res.summarize, summarize: res.data.summarize,
create_time: Date.now() create_time: Date.now()
}) })
this.showLastMsg() this.showLastMsg()
...@@ -376,26 +331,7 @@ ...@@ -376,26 +331,7 @@
} }
} }
}); });
}, }
toStreamMD(){
let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream"
// #ifndef H5
uni.setClipboardData({
data:url,
showToast:false,
success() {
uni.showToast({
title: '已复制文档链接,请到浏览器粘贴浏览',
icon: 'none',
duration:5000
});
}
})
// #endif
// #ifdef H5
window.open(url)
// #endif
}
} }
} }
</script> </script>
...@@ -543,10 +479,10 @@ ...@@ -543,10 +479,10 @@
.create_time { .create_time {
font-size: 12px; font-size: 12px;
padding: 5px; padding: 5px 0;
padding-top: 0; padding-top: 0;
color: #aaa; color: #aaa;
justify-content: center; text-align: center;
width:750rpx; width:750rpx;
/* #ifdef MP */ /* #ifdef MP */
display: flex; display: flex;
...@@ -607,22 +543,17 @@ ...@@ -607,22 +543,17 @@
justify-content: center; justify-content: center;
} }
#set-stream{ .tip-ai-ing {
z-index: 999;
padding:0 5px;
justify-content: center;
align-items: center; align-items: center;
position: fixed; flex-direction: column;
bottom: 50px; font-size: 14px;
right: 0; color: #919396;
} padding: 15px 0;
#set-stream .title{
font-size: 12px;
position: relative;
left: 10rpx;
} }
#set-stream switch{
transform: scale(0.5); .uni-link {
margin-left: 5px;
line-height: 20px;
} }
/* #ifdef H5 */ /* #ifdef H5 */
...@@ -686,9 +617,6 @@ ...@@ -686,9 +617,6 @@
margin-top: 15px; margin-top: 15px;
justify-content: center; justify-content: center;
} }
.create_time{
display: flex;
}
.textarea-box, .textarea-box,
.textarea, .textarea,
...@@ -729,14 +657,6 @@ ...@@ -729,14 +657,6 @@
height: 15px; height: 15px;
} }
.set-stream .title {
font-size: 12px;
}
.set-stream switch {
transform: scale(0.6);
position: relative;
left: -5px;
}
.textarea-box,.textarea-box *{ .textarea-box,.textarea-box *{
// border: 1px solid #000; // border: 1px solid #000;
......
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj // 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129 // jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
module.exports = { const {safeRequire, checkContentSecurityEnable} = require('./utils')
_before: function() { const createConfig = safeRequire('uni-config-center')
// 这里是云函数的前置方法,你可以在这里加入你需要逻辑,比如:拦截用户必须登录才能访问等 const config = createConfig({
/* pluginId: 'uni-ai-chat'
例如:使用uni-id-pages(链接地址:https://ext.dcloud.net.cn/plugin?id=8577)搭建账户体系。 }).config()
然后再使用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)后才能继续使用
*** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 ***
*/
},
async send({messages,SSEChannel}) {
// 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
// messages = [{
// role: 'user',
// content: 'uni-app是什么,20个字以内进行说明'
// }]
// 校验客户端提交的参数 module.exports = {
let res = checkMessages(messages) _before:async function() {
if (res.errCode) { // 这里是云函数的前置方法,你可以在这里加入你需要逻辑,比如:拦截用户必须登录才能访问等
throw new Error(res.errMsg) /*
} 例如:使用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)后才能继续使用
*** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 ***
*/
// 向uni-ai发送消息 // 从配置中心获取内容安全配置
return await chatCompletion({ if (this.getMethodName() == 'send' && config.contentSecurity) {
messages, //消息内容 const UniSecCheck = safeRequire('uni-sec-check')
SSEChannel, //sse渠道对象 const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin',
// 以下参数参考:https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#get-llm-manager requestId: this.getUniCloudRequestId()
// provider:"minimax",//llm服务商,目前支持openai、baidu、minimax。不指定时由uni-ai自动分配
// apiKey:"",//llm服务商的apiKey,如不填则使用uni-ai的key。如指定openai和baidu则必填
// accessToken:"",//llm服务商的accessToken。目前百度文心一言是必填
// proxy:""//可有效连接openai服务器的、可被uniCloud云函数连接的代理服务器地址。格式为IP或域名,域名不包含http前缀,协议层面仅支持https。配置为openai时必填
})
async function chatCompletion({
messages,
summarize = false,
SSEChannel = false,
provider,
apiKey,
accessToken,
proxy
}) {
const llmManager = uniCloud.ai.getLLMManager({
provider,
apiKey,
accessToken,
proxy
})
let res = await llmManager.chatCompletion({
messages,
tokensToGenerate: 512,
stream: SSEChannel !== false
}) })
this.textSecCheck = async (content)=>{
if (SSEChannel) { let {SSEChannel} = this.getParams()[0]||{}
let reply = "" if(SSEChannel){
return new Promise((resolve, reject) => { return console.log('提示:流式响应模式,内容安全识别功能无效');
const channel = uniCloud.deserializeSSEChannel(SSEChannel) }
res.on('message', async (message) => { // 检测文本
// await channel.write(message) const checkRes = await uniSecCheck.textSecCheck({
// console.log('---message----', message) content,
}) // openid,
res.on('line', async (line) => { scene:4,
reply += line 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
await channel.write(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 { console.log('checkRes检测文本',checkRes);
if(summarize == false){ if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
messages.push({ throw {
"content": res.reply, isSecCheck:true,
"role": "assistant" errCode: checkRes.errCode,
}) errMsg: '文字存在风险',
let totalTokens = messages.map(i => i.content).join('').length; result: checkRes.result
if (totalTokens > 500) { }
let replySummarize = await getSummarize(messages) } else if (checkRes.errCode) {
res.summarize = replySummarize console.log(`其他原因导致此文件未完成自动审核(错误码:${checkRes.errCode},错误信息:${checkRes.errMsg}),需要人工审核`);
throw {
isSecCheck:true,
errCode: checkRes.errCode,
errMsg: checkRes.errMsg,
result: checkRes.result
} }
} }
return res }
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) {
if(error){
if(error.isSecCheck ) {
return {
"data": {
"reply": "内容涉及敏感"
},
"errCode": 0
}
}else{
throw error // 直接抛出异常
} }
} }
//获总结 if (this.getMethodName() == 'send' && config.contentSecurity) {
async function getSummarize(messages) { try{
messages.push({ await this.textSecCheck(result.data.reply)
"content": "请简要总结上述全部对话", }catch(e){
"role": "user" return {
}) "data": {
// 获取总结不需要再总结summarize和stream "reply": "内容涉及敏感"
let res = await chatCompletion({ },
messages, "errCode": 0
summarize: true, }
stream: false, }
SSEChannel:false
})
return res.reply
} }
return result
function checkMessages(messages) { },
try { async send({
if (messages === undefined) { messages,
throw "messages为必传参数" SSEChannel
} else if (!Array.isArray(messages)) { }) {
throw "参数messages的值类型必须是[object,object...]" // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据
} else { // messages = [{
messages.forEach(item => { // role: 'user',
if (typeof item != 'object') { // content: 'uni-app是什么,20个字以内进行说明'
throw "参数messages的值类型必须是[object,object...]" // }]
}
let itemRoleArr = ["assistant", "user", "system"] // 校验客户端提交的参数
if (!itemRoleArr.includes(item.role)) { let res = checkMessages(messages)
throw "参数messages[{role}]的值只能是:" + itemRoleArr.join('') if (res.errCode) {
} throw new Error(res.errMsg)
if (typeof item.content != 'string') { }
throw "参数messages[{content}]的值类型必须是字符串"
} // 向uni-ai发送消息
}) return await chatCompletion({
messages, //消息内容
SSEChannel, //sse渠道对象
// 以下参数参考:https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#get-llm-manager
// provider:"minimax",//llm服务商,目前支持openai、baidu、minimax。不指定时由uni-ai自动分配
// apiKey:"",//llm服务商的apiKey,如不填则使用uni-ai的key。如指定openai和baidu则必填
// accessToken:"",//llm服务商的accessToken。目前百度文心一言是必填
// proxy:""//可有效连接openai服务器的、可被uniCloud云函数连接的代理服务器地址。格式为IP或域名,域名不包含http前缀,协议层面仅支持https。配置为openai时必填
})
async function chatCompletion({
messages,
summarize = false,
SSEChannel = false,
provider,
apiKey,
accessToken,
proxy
}) {
const llmManager = uniCloud.ai.getLLMManager({
provider,
apiKey,
accessToken,
proxy
})
let res = await llmManager.chatCompletion({
messages,
tokensToGenerate: 512,
stream: SSEChannel !== false
})
if (SSEChannel) {
let reply = ""
return new Promise((resolve, reject) => {
const channel = uniCloud.deserializeSSEChannel(SSEChannel)
res.on('message', async (message) => {
// await channel.write(message)
// console.log('---message----', message)
})
res.on('line', async (line) => {
reply += line
await channel.write(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
}
} }
return { if(res.errCode){
errCode: 0, throw res
} }
} catch (errMsg) {
return { return {
errSubject: 'ai-demo', data:res,
errCode: 'param-error', errCode: 0
errMsg }
} }
} }
//获总结
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
}
}
} }
} }
} }
\ No newline at end of file
{ {
"name": "uni-ai-chat", "name": "uni-ai-chat",
"dependencies": {}, "dependencies": {
"extensions": { "uni-config-center": "file:../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center",
"uni-cloud-jql": {}, "uni-sec-check": "file:../../../uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check"
"uni-cloud-ai": {}, },
"uni-cloud-push": {} "extensions": {
}, "uni-cloud-jql": {},
"cloudfunction-config": { "uni-cloud-push": {},
"timeout": 60 "uni-cloud-ai": {}
} },
"cloudfunction-config": {
"timeout": 60
}
} }
\ No newline at end of file
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)
}
{
"passwordSecret": [{
"type": "hmac-sha256",
"version": 1
}],
"passwordStrength": "medium",
"tokenSecret": "",
"requestAuthSecret": "",
"tokenExpiresIn": 7200,
"tokenExpiresThreshold": 3600,
"passwordErrorLimit": 6,
"passwordErrorRetryTime": 3600,
"autoSetInviteCode": false,
"forceInviteCode": false,
"idCardCertifyLimit": 1,
"realNameCertifyLimit": 5,
"sensitiveInfoEncryptSecret": "",
"frvNeedAlivePhoto": false,
"userRegisterDefaultRole": [],
"app": {
"tokenExpiresIn": 2592000,
"tokenExpiresThreshold": 864000,
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
},
"qq": {
"appid": "",
"appsecret": ""
},
"apple": {
"bundleId": ""
}
}
},
"web": {
"tokenExpiresIn": 7200,
"tokenExpiresThreshold": 3600,
"oauth": {
"weixin-h5": {
"appid": "",
"appsecret": ""
},
"weixin-web": {
"appid": "",
"appsecret": ""
}
}
},
"mp-weixin": {
"tokenExpiresIn": 259200,
"tokenExpiresThreshold": 86400,
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-qq": {
"tokenExpiresIn": 259200,
"tokenExpiresThreshold": 86400,
"oauth": {
"qq": {
"appid": "",
"appsecret": ""
}
}
},
"mp-alipay": {
"tokenExpiresIn": 259200,
"tokenExpiresThreshold": 86400,
"oauth": {
"alipay": {
"appid": "",
"privateKey": "",
"keyType": "PKCS8"
}
}
},
"service": {
"sms": {
"name": "",
"codeExpiresIn": 180,
"smsKey": "",
"smsSecret": "",
"scene": {
"bind-mobile-by-sms": {
"templateId": "",
"codeExpiresIn": 240
}
}
},
"univerify": {
"appid": "",
"apiKey": "",
"apiSecret": ""
}
}
}
\ No newline at end of file
## 1.0.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-link](https://uniapp.dcloud.io/component/uniui/uni-link)
## 1.1.7(2021-11-08)
## 0.0.7(2021-09-03)
- 修复 在 nvue 下不显示的 bug
## 0.0.6(2021-07-30)
- 新增 支持自定义插槽
## 0.0.5(2021-06-21)
- 新增 download 属性,H5平台下载文件名
## 0.0.4(2021-05-12)
- 新增 组件示例地址
## 0.0.3(2021-03-09)
- 新增 href 属性支持 tel:|mailto:
## 0.0.2(2021-02-05)
- 调整为uni_modules目录规范
<template>
<a v-if="isShowA" class="uni-link" :href="href"
:class="{'uni-link--withline':showUnderLine===true||showUnderLine==='true'}"
:style="{color,fontSize:fontSize+'px'}" :download="download">
<slot>{{text}}</slot>
</a>
<!-- #ifndef APP-NVUE -->
<text v-else class="uni-link" :class="{'uni-link--withline':showUnderLine===true||showUnderLine==='true'}"
:style="{color,fontSize:fontSize+'px'}" @click="openURL">
<slot>{{text}}</slot>
</text>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<text v-else class="uni-link" :class="{'uni-link--withline':showUnderLine===true||showUnderLine==='true'}"
:style="{color,fontSize:fontSize+'px'}" @click="openURL">
{{text}}
</text>
<!-- #endif -->
</template>
<script>
/**
* Link 外部网页超链接组件
* @description uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打开新网页
* @tutorial https://ext.dcloud.net.cn/plugin?id=1182
* @property {String} href 点击后打开的外部网页url
* @property {String} text 显示的文字
* @property {String} downlaod H5平台下载文件名
* @property {Boolean} showUnderLine 是否显示下划线
* @property {String} copyTips 在小程序端复制链接时显示的提示语
* @property {String} color 链接文字颜色
* @property {String} fontSize 链接文字大小
* @example * <uni-link href="https://ext.dcloud.net.cn" text="https://ext.dcloud.net.cn"></uni-link>
*/
export default {
name: 'uniLink',
props: {
href: {
type: String,
default: ''
},
text: {
type: String,
default: ''
},
download: {
type: String,
default: ''
},
showUnderLine: {
type: [Boolean, String],
default: true
},
copyTips: {
type: String,
default: '已自动复制网址,请在手机浏览器里粘贴该网址'
},
color: {
type: String,
default: '#999999'
},
fontSize: {
type: [Number, String],
default: 14
}
},
computed: {
isShowA() {
// #ifdef H5
this._isH5 = true;
// #endif
if ((this.isMail() || this.isTel()) && this._isH5 === true) {
return true;
}
return false;
}
},
created() {
this._isH5 = null;
},
methods: {
isMail() {
return this.href.startsWith('mailto:');
},
isTel() {
return this.href.startsWith('tel:');
},
openURL() {
// #ifdef APP-PLUS
if (this.isTel()) {
this.makePhoneCall(this.href.replace('tel:', ''));
} else {
plus.runtime.openURL(this.href);
}
// #endif
// #ifdef H5
window.open(this.href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: this.href
});
uni.showModal({
content: this.copyTips,
showCancel: false
});
// #endif
},
makePhoneCall(phoneNumber) {
uni.makePhoneCall({
phoneNumber
})
}
}
}
</script>
<style>
/* #ifndef APP-NVUE */
.uni-link {
cursor: pointer;
}
/* #endif */
.uni-link--withline {
text-decoration: underline;
}
</style>
{
"id": "uni-link",
"displayName": "uni-link 超链接",
"version": "1.0.0",
"description": "uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打",
"keywords": [
"uni-ui",
"uniui",
"link",
"超链接",
""
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}
\ No newline at end of file
## Link 链接
> **组件名:uni-link**
> 代码块: `uLink`
uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打开新网页。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-link)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839
\ No newline at end of file
## 1.1.5(2023-03-27)
- 修复 微信小程序平台 某些情况下 encrypt_key 插入错误的问题
## 1.1.4(2023-03-13)
- 修复 平台 weixin-web
## 1.1.3(2023-03-13)
- 新增 支持旧版本 uni-id 配置
- 新增 支持平台 weixin-app|qq-mp|qq-app
## 1.1.2(2023-02-28)
- 新增 config 配置错误提示语
## 1.1.1(2023-02-28)
- 新增 支持 provider 参数,和 platform 保持一致
## 1.1.0(2023-02-27)
- 重要更新 调整数据库key格式,兼容旧版本API,如果开发者通过手动拼接key查询数据库需要修改现有逻辑
+ 原格式: uni-id:[dcloudAppid]:[platform]:[openid]:[access-token|user-access-token|session-key|encrypt-key-version|ticket]
+ 新格式: uni-id:[provider]:[appid]:[openid]:[access-token|user-access-token|session-key|encrypt-key-version|ticket]
## 1.0.4(2022-09-21)
- 新增 支持使用阿里云固定IP获取微信公众号H5凭据 access_token、ticket,开发者需要在微信公众平台配置阿里云固定IP,[固定IP详情](https://uniapp.dcloud.net.cn/uniCloud/cf-functions.html#aliyun-eip)
## 1.0.3(2022-09-06)
- 修复 过期时间问题,容错 AccessToken 默认 fallback 逻辑,当微信服务器没有返回过期时间时设置为2小时后过期
## 1.0.2(2022-09-02)
- 新增 依赖数据表schema opendb-open-data
## 1.0.0(2022-08-22)
- 首次发布
{
"id": "uni-open-bridge-common",
"displayName": "uni-open-bridge-common",
"version": "1.1.5",
"description": "统一接管微信等三方平台认证凭据",
"keywords": [
"uni-open-bridge-common",
"access_token",
"session_key",
"ticket"
],
"repository": "",
"engines": {
"HBuilderX": "^3.5.2"
},
"dcloudext": {
"type": "unicloud-template-function",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
\ No newline at end of file
# uni-open-bridge-common
`uni-open-bridge-common` 是统一接管微信等三方平台认证凭据(包括但不限于`access_token``session_key``encrypt_key``ticket`)的开源库。
文档链接 [https://uniapp.dcloud.net.cn/uniCloud/uni-open-bridge#common](https://uniapp.dcloud.net.cn/uniCloud/uni-open-bridge#common)
'use strict';
class BridgeError extends Error {
constructor(code, message) {
super(message)
this._code = code
}
get code() {
return this._code
}
get errCode() {
return this._code
}
get errMsg() {
return this.message
}
}
module.exports = {
BridgeError
}
'use strict';
const {
ProviderType
} = require('./consts.js')
const configCenter = require('uni-config-center')
// 多维数据为兼容uni-id以前版本配置
const OauthConfig = {
'weixin-app': [
['app', 'oauth', 'weixin'],
['app-plus', 'oauth', 'weixin']
],
'weixin-mp': [
['mp-weixin', 'oauth', 'weixin']
],
'weixin-h5': [
['web', 'oauth', 'weixin-h5'],
['h5-weixin', 'oauth', 'weixin'],
['h5', 'oauth', 'weixin']
],
'weixin-web': [
['web', 'oauth', 'weixin-web']
],
'qq-app': [
['app', 'oauth', 'qq'],
['app-plus', 'oauth', 'qq']
],
'qq-mp': [
['mp-qq', 'oauth', 'qq']
]
}
const Support_Platforms = [
ProviderType.WEIXIN_MP,
ProviderType.WEIXIN_H5,
ProviderType.WEIXIN_APP,
ProviderType.WEIXIN_WEB,
ProviderType.QQ_MP,
ProviderType.QQ_APP
]
class ConfigBase {
constructor() {
const uniIdConfigCenter = configCenter({
pluginId: 'uni-id'
})
this._uniIdConfig = uniIdConfigCenter.config()
}
getAppConfig(appid) {
if (Array.isArray(this._uniIdConfig)) {
return this._uniIdConfig.find((item) => {
return (item.dcloudAppid === appid)
})
}
return this._uniIdConfig
}
}
class AppConfig extends ConfigBase {
constructor() {
super()
}
get(appid, platform) {
if (!this.isSupport(platform)) {
return null
}
let appConfig = this.getAppConfig(appid)
if (!appConfig) {
return null
}
return this.getOauthConfig(appConfig, platform)
}
isSupport(platformName) {
return (Support_Platforms.indexOf(platformName) >= 0)
}
getOauthConfig(appConfig, platformName) {
let treePath = OauthConfig[platformName]
let node = this.findNode(appConfig, treePath)
if (node && node.appid && node.appsecret) {
return {
appid: node.appid,
secret: node.appsecret
}
}
return null
}
findNode(treeNode, arrayPath) {
let node = treeNode
for (let treePath of arrayPath) {
for (let name of treePath) {
const currentNode = node[name]
if (currentNode) {
node = currentNode
} else {
node = null
break
}
}
if (node === null) {
node = treeNode
} else {
break
}
}
return node
}
}
module.exports = {
AppConfig
};
\ No newline at end of file
'use strict';
const TAG = "UNI_OPEN_BRIDGE"
const HTTP_STATUS = {
SUCCESS: 200
}
const ProviderType = {
WEIXIN_MP: 'weixin-mp',
WEIXIN_H5: 'weixin-h5',
WEIXIN_APP: 'weixin-app',
WEIXIN_WEB: 'weixin-web',
QQ_MP: 'qq-mp',
QQ_APP: 'qq-app'
}
// old
const PlatformType = ProviderType
const ErrorCodeType = {
SYSTEM_ERROR: TAG + "_SYSTEM_ERROR"
}
module.exports = {
HTTP_STATUS,
ProviderType,
PlatformType,
ErrorCodeType
}
'use strict';
const {
PlatformType,
ProviderType,
ErrorCodeType
} = require('./consts.js')
const {
AppConfig
} = require('./config.js')
const {
Storage
} = require('./storage.js')
const {
BridgeError
} = require('./bridge-error.js')
const {
WeixinServer
} = require('./weixin-server.js')
const appConfig = new AppConfig()
class AccessToken extends Storage {
constructor() {
super('access-token', ['provider', 'appid'])
}
async update(key) {
super.update(key)
const result = await this.getByWeixinServer(key)
return this.set(key, result.value, result.duration)
}
async fallback(key) {
return this.getByWeixinServer(key)
}
async getByWeixinServer(key) {
const oauthConfig = appConfig.get(key.dcloudAppid, key.provider)
let methodName
if (key.provider === ProviderType.WEIXIN_MP) {
methodName = 'GetMPAccessTokenData'
} else if (key.provider === ProviderType.WEIXIN_H5) {
methodName = 'GetH5AccessTokenData'
} else {
throw new BridgeError(ErrorCodeType.SYSTEM_ERROR, "provider invalid")
}
const responseData = await WeixinServer[methodName](oauthConfig)
const duration = responseData.expires_in || (60 * 60 * 2)
delete responseData.expires_in
return {
value: responseData,
duration
}
}
}
class UserAccessToken extends Storage {
constructor() {
super('user-access-token', ['provider', 'appid', 'openid'])
}
}
class SessionKey extends Storage {
constructor() {
super('session-key', ['provider', 'appid', 'openid'])
}
}
class Encryptkey extends Storage {
constructor() {
super('encrypt-key', ['provider', 'appid', 'openid'])
}
async update(key) {
super.update(key)
const result = await this.getByWeixinServer(key)
return this.set(key, result.value, result.duration)
}
getKeyString(key) {
return `${super.getKeyString(key)}-${key.version}`
}
getExpiresIn(value) {
if (value <= 0) {
return 60
}
return value
}
async fallback(key) {
return this.getByWeixinServer(key)
}
async getByWeixinServer(key) {
const accessToken = await Factory.Get(AccessToken, key)
const userSession = await Factory.Get(SessionKey, key)
const responseData = await WeixinServer.GetUserEncryptKeyData({
openid: key.openid,
access_token: accessToken.access_token,
session_key: userSession.session_key
})
const keyInfo = responseData.key_info_list.find((item) => {
return item.version === key.version
})
if (!keyInfo) {
throw new BridgeError(ErrorCodeType.SYSTEM_ERROR, 'key version invalid')
}
const value = {
encrypt_key: keyInfo.encrypt_key,
iv: keyInfo.iv
}
return {
value,
duration: keyInfo.expire_in
}
}
}
class Ticket extends Storage {
constructor() {
super('ticket', ['provider', 'appid'])
}
async update(key) {
super.update(key)
const result = await this.getByWeixinServer(key)
return this.set(key, result.value, result.duration)
}
async fallback(key) {
return this.getByWeixinServer(key)
}
async getByWeixinServer(key) {
const accessToken = await Factory.Get(AccessToken, {
dcloudAppid: key.dcloudAppid,
provider: ProviderType.WEIXIN_H5
})
const responseData = await WeixinServer.GetH5TicketData(accessToken)
const duration = responseData.expires_in || (60 * 60 * 2)
delete responseData.expires_in
delete responseData.errcode
delete responseData.errmsg
return {
value: responseData,
duration
}
}
}
const Factory = {
async Get(T, key, fallback) {
Factory.FixOldKey(key)
return Factory.MakeUnique(T).get(key, fallback)
},
async Set(T, key, value, expiresIn) {
Factory.FixOldKey(key)
return Factory.MakeUnique(T).set(key, value, expiresIn)
},
async Remove(T, key) {
Factory.FixOldKey(key)
return Factory.MakeUnique(T).remove(key)
},
async Update(T, key) {
Factory.FixOldKey(key)
return Factory.MakeUnique(T).update(key)
},
FixOldKey(key) {
if (!key.provider) {
key.provider = key.platform
}
const configData = appConfig.get(key.dcloudAppid, key.provider)
if (!configData) {
throw new BridgeError(ErrorCodeType.SYSTEM_ERROR, 'appid or provider invalid')
}
key.appid = configData.appid
},
MakeUnique(T) {
return new T()
}
}
// exports
async function getAccessToken(key, fallback) {
return Factory.Get(AccessToken, key, fallback)
}
async function setAccessToken(key, value, expiresIn) {
return Factory.Set(AccessToken, key, value, expiresIn)
}
async function removeAccessToken(key) {
return Factory.Remove(AccessToken, key)
}
async function updateAccessToken(key) {
return Factory.Update(AccessToken, key)
}
async function getUserAccessToken(key, fallback) {
return Factory.Get(UserAccessToken, key, fallback)
}
async function setUserAccessToken(key, value, expiresIn) {
return Factory.Set(UserAccessToken, key, value, expiresIn)
}
async function removeUserAccessToken(key) {
return Factory.Remove(UserAccessToken, key)
}
async function getSessionKey(key, fallback) {
return Factory.Get(SessionKey, key, fallback)
}
async function setSessionKey(key, value, expiresIn) {
return Factory.Set(SessionKey, key, value, expiresIn)
}
async function removeSessionKey(key) {
return Factory.Remove(SessionKey, key)
}
async function getEncryptKey(key, fallback) {
return Factory.Get(Encryptkey, key, fallback)
}
async function setEncryptKey(key, value, expiresIn) {
return Factory.Set(Encryptkey, key, value, expiresIn)
}
async function removeEncryptKey(key) {
return Factory.Remove(Encryptkey, key)
}
async function updateEncryptKey(key) {
return Factory.Update(Encryptkey, key)
}
async function getTicket(key, fallback) {
return Factory.Get(Ticket, key, fallback)
}
async function setTicket(key, value, expiresIn) {
return Factory.Set(Ticket, key, value, expiresIn)
}
async function removeTicket(key) {
return Factory.Remove(Ticket, key)
}
async function updateTicket(key) {
return Factory.Update(Ticket, key)
}
module.exports = {
getAccessToken,
setAccessToken,
removeAccessToken,
updateAccessToken,
getUserAccessToken,
setUserAccessToken,
removeUserAccessToken,
getSessionKey,
setSessionKey,
removeSessionKey,
getEncryptKey,
setEncryptKey,
removeEncryptKey,
updateEncryptKey,
getTicket,
setTicket,
removeTicket,
updateTicket,
ProviderType,
PlatformType,
WeixinServer,
ErrorCodeType
}
{
"name": "uni-open-bridge-common",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
\ No newline at end of file
'use strict';
const {
Validator
} = require('./validator.js')
const {
CacheKeyCascade
} = require('./uni-cloud-cache.js')
const {
BridgeError
} = require('./bridge-error.js')
class Storage {
constructor(type, keys) {
this._type = type || null
this._keys = keys || []
}
async get(key, fallback) {
this.validateKey(key)
const result = await this.create(key, fallback).get()
return result.value
}
async set(key, value, expiresIn) {
this.validateKey(key)
this.validateValue(value)
const expires_in = this.getExpiresIn(expiresIn)
if (expires_in !== 0) {
await this.create(key).set(this.getValue(value), expires_in)
}
}
async remove(key) {
this.validateKey(key)
await this.create(key).remove()
}
// virtual
async update(key) {
this.validateKey(key)
}
async ttl(key) {
this.validateKey(key)
// 后续考虑支持
}
async fallback(key) {}
getKeyString(key) {
const keyArray = [Storage.Prefix]
this._keys.forEach((name) => {
keyArray.push(key[name])
})
keyArray.push(this._type)
return keyArray.join(':')
}
getValue(value) {
return value
}
getExpiresIn(value) {
if (value !== undefined) {
return value
}
return -1
}
validateKey(key) {
Validator.Key(this._keys, key)
}
validateValue(value) {
Validator.Value(value)
}
create(key, fallback) {
const keyString = this.getKeyString(key)
const options = {
layers: [{
type: 'database',
key: keyString
}, {
type: 'redis',
key: keyString
}]
}
const _this = this
return new CacheKeyCascade({
...options,
fallback: async function() {
if (fallback) {
return fallback(key)
} else if (_this.fallback) {
return _this.fallback(key)
}
}
})
}
}
Storage.Prefix = "uni-id"
module.exports = {
Storage
};
const db = uniCloud.database()
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
}
const validator = {
key: function(value) {
const err = new Error('Invalid key')
if (typeof value !== 'string') {
throw err
}
const valueTrim = value.trim()
if (!valueTrim || valueTrim !== value) {
throw err
}
},
value: function(value) {
// 仅作简单校验
const type = getType(value)
const validValueType = ['null', 'number', 'string', 'array', 'object']
if (validValueType.indexOf(type) === -1) {
throw new Error('Invalid value type')
}
},
duration: function(value) {
const err = new Error('Invalid duration')
if (value === undefined) {
return
}
if (typeof value !== 'number' || value === 0) {
throw err
}
}
}
/**
* 入库时 expired 为过期时间对应的时间戳,永不过期用-1表示
* 返回结果时 与redis对齐,-1表示永不过期,-2表示已过期或不存在
*/
class DatabaseCache {
constructor({
collection = 'opendb-open-data'
} = {}) {
this.type = 'db'
this.collection = db.collection(collection)
}
_serializeValue(value) {
return value === undefined ? null : JSON.stringify(value)
}
_deserializeValue(value) {
return value ? JSON.parse(value) : value
}
async set(key, value, duration) {
validator.key(key)
validator.value(value)
validator.duration(duration)
value = this._serializeValue(value)
await this.collection.doc(key).set({
value,
expired: duration && duration !== -1 ? Date.now() + (duration * 1000) : -1
})
}
async _getWithDuration(key) {
const getKeyRes = await this.collection.doc(key).get()
const record = getKeyRes.data[0]
if (!record) {
return {
value: null,
duration: -2
}
}
const value = this._deserializeValue(record.value)
const expired = record.expired
if (expired === -1) {
return {
value,
duration: -1
}
}
const duration = expired - Date.now()
if (duration <= 0) {
await this.remove(key)
return {
value: null,
duration: -2
}
}
return {
value,
duration: Math.floor(duration / 1000)
}
}
async get(key, {
withDuration = true
} = {}) {
const result = await this._getWithDuration(key)
if (!withDuration) {
delete result.duration
}
return result
}
async remove(key) {
await this.collection.doc(key).remove()
}
}
class RedisCache {
constructor() {
this.type = 'redis'
this.redis = uniCloud.redis()
}
_serializeValue(value) {
return value === undefined ? null : JSON.stringify(value)
}
_deserializeValue(value) {
return value ? JSON.parse(value) : value
}
async set(key, value, duration) {
validator.key(key)
validator.value(value)
validator.duration(duration)
value = this._serializeValue(value)
if (!duration || duration === -1) {
await this.redis.set(key, value)
} else {
await this.redis.set(key, value, 'EX', duration)
}
}
async get(key, {
withDuration = false
} = {}) {
let value = await this.redis.get(key)
value = this._deserializeValue(value)
if (!withDuration) {
return {
value
}
}
const durationSecond = await this.redis.ttl(key)
let duration
switch (durationSecond) {
case -1:
duration = -1
break
case -2:
duration = -2
break
default:
duration = durationSecond
break
}
return {
value,
duration
}
}
async remove(key) {
await this.redis.del(key)
}
}
class Cache {
constructor({
type,
collection
} = {}) {
if (type === 'database') {
return new DatabaseCache({
collection
})
} else if (type === 'redis') {
return new RedisCache()
} else {
throw new Error('Invalid cache type')
}
}
}
class CacheKey {
constructor({
type,
collection,
cache,
key,
fallback
} = {}) {
this.cache = cache || new Cache({
type,
collection
})
this.key = key
this.fallback = fallback
}
async set(value, duration) {
await this.cache.set(this.key, value, duration)
}
async setWithSync(value, duration, syncMethod) {
await Promise.all([
this.set(this.key, value, duration),
syncMethod(value, duration)
])
}
async get() {
let {
value,
duration
} = await this.cache.get(this.key)
if (value !== null && value !== undefined) {
return {
value,
duration
}
}
if (!this.fallback) {
return {
value: null,
duration: -2
}
}
const fallbackResult = await this.fallback()
value = fallbackResult.value
duration = fallbackResult.duration
if (value !== null && duration !== undefined) {
await this.cache.set(this.key, value, duration)
}
return {
value,
duration
}
}
async remove() {
await this.cache.remove(this.key)
}
}
class CacheKeyCascade {
constructor({
layers, // [{cache, type, collection, key}] 从低级到高级排序,[DbCacheKey, RedisCacheKey]
fallback
} = {}) {
this.layers = layers
this.cacheLayers = []
let lastCacheKey
for (let i = 0; i < layers.length; i++) {
const {
type,
cache,
collection,
key
} = layers[i]
const lastCacheKeyTemp = lastCacheKey
try {
const currentCacheKey = new CacheKey({
type,
collection,
cache,
key,
fallback: i === 0 ? fallback : function() {
return lastCacheKeyTemp.get()
}
})
this.cacheLayers.push(currentCacheKey)
lastCacheKey = currentCacheKey
} catch (e) {}
}
this.highLevelCache = lastCacheKey
}
async set(value, duration) {
return Promise.all(
this.cacheLayers.map(item => {
return item.set(value, duration)
})
)
}
async setWithSync(value, duration, syncMethod) {
const setPromise = this.cacheLayers.map(item => {
return item.set(value, duration)
})
return Promise.all(
[
...setPromise,
syncMethod(value, duration)
]
)
}
async get() {
return this.highLevelCache.get()
}
async remove() {
await Promise.all(
this.cacheLayers.map(cacheKeyItem => {
return cacheKeyItem.remove()
})
)
}
}
module.exports = {
Cache,
DatabaseCache,
RedisCache,
CacheKey,
CacheKeyCascade
}
const Validator = {
Key(keyArray, parameters) {
for (let i = 0; i < keyArray.length; i++) {
const keyName = keyArray[i]
if (typeof parameters[keyName] !== 'string') {
Validator.ThrowNewError(`Invalid ${keyName}`)
}
if (parameters[keyName].length < 1) {
Validator.ThrowNewError(`Invalid ${keyName}`)
}
}
},
Value(value) {
if (value === undefined) {
Validator.ThrowNewError('Invalid Value')
}
if (typeof value !== 'object') {
Validator.ThrowNewError('Invalid Value Type')
}
},
ThrowNewError(message) {
throw new Error(message)
}
}
module.exports = {
Validator
}
'use strict';
const crypto = require('crypto')
const {
HTTP_STATUS
} = require('./consts.js')
const {
BridgeError
} = require('./bridge-error.js')
class WeixinServer {
constructor(options = {}) {
this._appid = options.appid
this._secret = options.secret
}
getAccessToken() {
return uniCloud.httpclient.request(WeixinServer.AccessToken_Url, {
dataType: 'json',
method: 'GET',
dataAsQueryString: true,
data: {
appid: this._appid,
secret: this._secret,
grant_type: "client_credential"
}
})
}
// 使用客户端获取的 code 从微信服务器换取 openid,code 仅可使用一次
codeToSession(code) {
return uniCloud.httpclient.request(WeixinServer.Code2Session_Url, {
dataType: 'json',
data: {
appid: this._appid,
secret: this._secret,
js_code: code,
grant_type: 'authorization_code'
}
})
}
getUserEncryptKey({
access_token,
openid,
session_key
}) {
console.log(access_token, openid, session_key);
const signature = crypto.createHmac('sha256', session_key).update('').digest('hex')
return uniCloud.httpclient.request(WeixinServer.User_Encrypt_Key_Url, {
dataType: 'json',
method: 'POST',
dataAsQueryString: true,
data: {
access_token,
openid: openid,
signature: signature,
sig_method: 'hmac_sha256'
}
})
}
getH5AccessToken() {
return uniCloud.httpclient.request(WeixinServer.AccessToken_H5_Url, {
dataType: 'json',
method: 'GET',
data: {
appid: this._appid,
secret: this._secret,
grant_type: "client_credential"
}
})
}
getH5Ticket(access_token) {
return uniCloud.httpclient.request(WeixinServer.Ticket_Url, {
dataType: 'json',
dataAsQueryString: true,
method: 'POST',
data: {
access_token
}
})
}
getH5AccessTokenForEip() {
return uniCloud.httpProxyForEip.postForm(WeixinServer.AccessToken_H5_Url, {
appid: this._appid,
secret: this._secret,
grant_type: "client_credential"
}, {
dataType: 'json'
})
}
getH5TicketForEip(access_token) {
return uniCloud.httpProxyForEip.postForm(WeixinServer.Ticket_Url, {
access_token
}, {
dataType: 'json',
dataAsQueryString: true
})
}
}
WeixinServer.AccessToken_Url = 'https://api.weixin.qq.com/cgi-bin/token'
WeixinServer.Code2Session_Url = 'https://api.weixin.qq.com/sns/jscode2session'
WeixinServer.User_Encrypt_Key_Url = 'https://api.weixin.qq.com/wxa/business/getuserencryptkey'
WeixinServer.AccessToken_H5_Url = 'https://api.weixin.qq.com/cgi-bin/token'
WeixinServer.Ticket_Url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi'
WeixinServer.GetMPAccessToken = function(options) {
return new WeixinServer(options).getAccessToken()
}
WeixinServer.GetCodeToSession = function(options) {
return new WeixinServer(options).codeToSession(options.code)
}
WeixinServer.GetUserEncryptKey = function(options) {
return new WeixinServer(options).getUserEncryptKey(options)
}
WeixinServer.GetH5AccessToken = function(options) {
return new WeixinServer(options).getH5AccessToken()
}
WeixinServer.GetH5Ticket = function(options) {
return new WeixinServer(options).getH5Ticket(options.access_token)
}
////////////////////////////////////////////////////////////////
function isAliyun() {
return (uniCloud.getCloudInfos()[0].provider === 'aliyun')
}
WeixinServer.GetResponseData = function(response) {
// console.log("WeixinServer::response", response)
if (!(response.status === HTTP_STATUS.SUCCESS || response.statusCodeValue === HTTP_STATUS.SUCCESS)) {
throw new BridgeError(response.status || response.statusCodeValue, response.status || response.statusCodeValue)
}
const responseData = response.data || response.body
if (responseData.errcode !== undefined && responseData.errcode !== 0) {
throw new BridgeError(responseData.errcode, responseData.errmsg)
}
return responseData
}
WeixinServer.GetMPAccessTokenData = async function(options) {
const response = await new WeixinServer(options).getAccessToken()
return WeixinServer.GetResponseData(response)
}
WeixinServer.GetCodeToSessionData = async function(options) {
const response = await new WeixinServer(options).codeToSession(options.code)
return WeixinServer.GetResponseData(response)
}
WeixinServer.GetUserEncryptKeyData = async function(options) {
const response = await new WeixinServer(options).getUserEncryptKey(options)
return WeixinServer.GetResponseData(response)
}
WeixinServer.GetH5AccessTokenData = async function(options) {
const ws = new WeixinServer(options)
let response
if (isAliyun()) {
response = await ws.getH5AccessTokenForEip()
if (typeof response === 'string') {
response = JSON.parse(response)
}
} else {
response = await ws.getH5AccessToken()
}
return WeixinServer.GetResponseData(response)
}
WeixinServer.GetH5TicketData = async function(options) {
const ws = new WeixinServer(options)
let response
if (isAliyun()) {
response = await ws.getH5TicketForEip(options.access_token)
if (typeof response === 'string') {
response = JSON.parse(response)
}
} else {
response = await ws.getH5Ticket(options.access_token)
}
return WeixinServer.GetResponseData(response)
}
module.exports = {
WeixinServer
}
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": ["_id", "value"],
"properties": {
"_id": {
"bsonType": "string",
"description": "key,格式:uni-id:[provider]:[appid]:[openid]:[access-token|user-access-token|session-key|encrypt-key-version|ticket]"
},
"value": {
"bsonType": "object",
"description": "字段_id对应的值"
},
"expired": {
"bsonType": "date",
"description": "过期时间"
}
}
}
## 1.0.1(2022-08-22)
- 首次发布
{
"id": "uni-open-bridge",
"displayName": "uni-open-bridge",
"version": "1.0.1",
"description": "uni-open-bridge 是统一接管微信等三方平台认证的开源库",
"keywords": [
"uni-open-bridge-common",
"access_token",
"session_key",
"ticket",
"微信"
],
"repository": "",
"engines": {
"HBuilderX": "^3.5.4"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-open-bridge-common"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
\ No newline at end of file
# uni-open-bridge
`uni-open-bridge` 是统一接管微信等三方平台认证凭据(包括但不限于`access_token``session_key``encrypt_key``ticket`)的开源库。
文档链接 [https://uniapp.dcloud.net.cn/uniCloud/uni-open-bridge](https://uniapp.dcloud.net.cn/uniCloud/uni-open-bridge)
'use strict';
class Command {
constructor() {
this._registered = {}
}
async execute(name, args) {
if (this._registered[name]) {
return await this._registered[name].execute(args)
}
}
canExecute(name, args) {
if (this._registered[name] && this._registered[name].canExecute) {
this._registered[name].canExecute(args)
}
return false
}
register(name, execute, canExecute) {
this._registered[name] = {
execute,
canExecute
}
}
}
class Task {
constructor(id) {
this._id = id || this._newTaskId()
this._state = Task.TASK_STATE.WAITING
}
async run() {}
async cancel() {}
async taskAction() {}
_newTaskId() {
let guid = ''
const format = 'xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx'
for (let i = 0; i < format.length; i++) {
if (format[i] === 'x') {
guid += (Math.random() * 16 | 0).toString(16)
} else {
guid += format[i]
}
}
return guid.toUpperCase()
}
get id() {
return this._id
}
set id(value) {
this._id = value
}
get state() {
return this._state
}
set state(value) {
this._state = value
}
}
Task.TASK_STATE = {
WAITING: "WAITING",
RESOLVING: "RESOLVING",
RESOLVED: "RESOLVED",
EXECUTING: "EXECUTING",
ERROR: "ERROR",
COMPLETED: "COMPLETED",
CANCELLING: "CANCELLING",
CANCELLED: "CANCELLED"
}
class TaskManager {
constructor() {
this._tasks = []
}
get tasks() {
return this._tasks
}
clear() {
this._tasks.length = 0
}
getTask(id) {
return this._tasks.find((item) => {
return (item.id == id)
})
}
addTask(task) {
this._tasks.push(task)
}
deleteTask(id) {
const index = this.findIndex(id)
if (index < 0) {
return
}
this._tasks[index].cancel()
if (index > -1) {
this._tasks.splice(index, 1)
}
}
findIndex(id) {
return this._tasks.findIndex((item) => {
return (item.id == id)
})
}
}
module.exports = {
Command,
Task,
TaskManager
};
'use strict';
const {
getAccessToken,
setAccessToken,
removeAccessToken,
getUserAccessToken,
setUserAccessToken,
removeUserAccessToken,
getSessionKey,
setSessionKey,
removeSessionKey,
getEncryptKey,
setEncryptKey,
removeEncryptKey,
getTicket,
setTicket,
removeTicket
} = require('uni-open-bridge-common')
const {
Command
} = require('./basic.js');
const {
OpenBridgeConfig
} = require('./config.js')
const openBridgeConfig = new OpenBridgeConfig()
const Commands = [
'getAccessToken',
'setAccessToken',
'removeAccessToken',
'getUserAccessToken',
'setUserAccessToken',
'removeUserAccessToken',
'getSessionKey',
'setSessionKey',
'removeSessionKey',
'getEncryptKey',
'setEncryptKey',
'removeEncryptKey',
'getTicket',
'setTicket',
'removeTicket'
]
class MainFrame extends Command {
constructor() {
super()
Commands.forEach((name) => {
this.register(name, this[name].bind(this))
})
}
async getAccessToken(parameters) {
return await getAccessToken(parameters)
}
async setAccessToken(parameters) {
return await setAccessToken(parameters, parameters.value, parameters.expiresIn)
}
async removeAccessToken(parameters) {
return await removeAccessToken(parameters)
}
async getUserAccessToken(parameters) {
return await getUserAccessToken(parameters)
}
async setUserAccessToken(parameters) {
return await setUserAccessToken(parameters, parameters.value, parameters.expiresIn)
}
async removeUserAccessToken(parameters) {
return await removeUserAccessToken(parameters)
}
async getSessionKey(parameters) {
return await getSessionKey(parameters, parameters.fallback || null)
}
async setSessionKey(parameters) {
return await setSessionKey(parameters, parameters.value, parameters.expiresIn)
}
async removeSessionKey(parameters) {
return await removeSessionKey(parameters)
}
async getEncryptKey(parameters) {
return await getEncryptKey(parameters, null)
}
async setEncryptKey(parameters) {
return await setEncryptKey(parameters, parameters.value, parameters.expiresIn)
}
async removeEncryptKey(parameters) {
return await removeEncryptKey(parameters)
}
async getTicket(parameters) {
return await getTicket(parameters, null)
}
async setTicket(parameters) {
return await setTicket(parameters, parameters.value, parameters.expiresIn)
}
async removeTicket(parameters) {
return await removeTicket(parameters)
}
checkIP(ip) {
return openBridgeConfig.inWhiteList(ip)
}
}
const mainFrame = new MainFrame();
module.exports = mainFrame;
'use strict';
const configCenter = require('uni-config-center')
class TaskConfig {
constructor(options) {
this._dcloudAppid = options.dcloudAppid
this._appid = options.appid
this._secret = options.secret
this._platform = options.platform
this._tasks = options.tasks
this._timeout = 1000 * 10
}
get dcloudAppid() {
return this._dcloudAppid
}
get appid() {
return this._appid
}
get secret() {
return this._secret
}
get platform() {
return this._platform
}
get tasks() {
return this._tasks
}
}
class ConfigBase {
constructor() {
const uniIdConfig = configCenter({
pluginId: 'uni-id'
})
const openBridgeConfig = configCenter({
pluginId: 'uni-open-bridge'
})
this._uniId = uniIdConfig.config()
this._openBridge = openBridgeConfig.config()
this._ready = true
}
getAppConfig(appid) {
if (Array.isArray(this._uniId)) {
return this._uniId.find((item) => {
return (item.dcloudAppid === appid)
})
}
if (this._uniId.dcloudAppid === appid) {
return this._uniId
}
return null
}
inWhiteList(ip) {
return (this.ipWhiteList.indexOf(ip) > -1)
}
get openBridge() {
return this._openBridge
}
get ipWhiteList() {
return this._openBridge.ipWhiteList
}
get ready() {
return this._ready
}
}
class OpenBridgeConfig extends ConfigBase {
constructor() {
super()
this._tasks = []
this.resolve()
}
get tasks() {
return this._tasks
}
resolve() {
this._tasks.length = 0
const appids = Object.keys(this.openBridge.schedule)
for (let i = 0; i < appids.length; i++) {
const appid = appids[i]
let appConfig = this.getAppConfig(appid)
if (appConfig != null) {
const schedule = this.openBridge.schedule[appid]
if (schedule) {
this.resolveSchedule(appid, appConfig, schedule)
}
}
}
}
resolveSchedule(dcloudAppid, appConfig, schedule) {
if (schedule.enable !== true) {
return
}
const schedulePlatforms = Object.keys(schedule)
for (let i = 0; i < schedulePlatforms.length; i++) {
const platformName = schedulePlatforms[i]
const scheduleTask = schedule[platformName]
if (!scheduleTask.enable) {
continue
}
if (!this.isSupport(platformName)) {
continue
}
this._tasks.push({
dcloudAppid: dcloudAppid,
platform: platformName,
tasks: scheduleTask.tasks
})
}
}
isSupport(platformName) {
return (OpenBridgeConfig.Support_Platforms.indexOf(platformName) >= 0)
}
}
OpenBridgeConfig.Support_Platforms = ['weixin-mp', 'weixin-h5']
module.exports = {
OpenBridgeConfig
};
'use strict';
const runTask = require('./index.task.js')
const command = require('./bridge.js')
async function executeCommand() {
const methodName = this.getMethodName()
const clientInfo = this.getClientInfo()
let parameters
if (clientInfo.source === 'http') {
const postData = this.getHttpInfo().body
if (!postData || postData.length < 4) {
throw new Error('Invalid parameter(s)::' + postData)
}
parameters = JSON.parse(postData)
} else if (clientInfo.source === 'function') {
const args = this.getParams()
parameters = args[0]
if (args.length === 2) {
parameters.value = args[1]
}
if (args.length === 3) {
parameters.expiresIn = args[2]
}
} else {
throw new Error('Invalid')
}
return await command.execute(methodName, parameters)
}
module.exports = {
async _timing() {
console.log('triggered by timing')
await runTask()
},
async _before() {
const clientInfo = this.getClientInfo()
if (clientInfo.source === 'http' && !command.checkIP(clientInfo.clientIP)) {
throw new Error('Invalid IP::' + clientInfo.clientIP)
}
},
/// AccessToken
async getAccessToken() {
return await executeCommand.call(this)
},
async setAccessToken() {
return await executeCommand.call(this)
},
async removeAccessToken() {
return await executeCommand.call(this)
},
/// UserAccessToken
async getUserAccessToken() {
return await executeCommand.call(this)
},
async setUserAccessToken() {
return await executeCommand.call(this)
},
async removeUserAccessToken() {
return await executeCommand.call(this)
},
/// SessionKey
async getSessionKey() {
return await executeCommand.call(this)
},
async setSessionKey() {
return await executeCommand.call(this)
},
async removeSessionKey() {
return await executeCommand.call(this)
},
/// EncryptKey
async getEncryptKey() {
return await executeCommand.call(this)
},
async setEncryptKey() {
return await executeCommand.call(this)
},
async removeEncryptKey() {
return await executeCommand.call(this)
},
/// Ticket
async getTicket() {
return await executeCommand.call(this)
},
async setTicket() {
return await executeCommand.call(this)
},
async removeTicket() {
return await executeCommand.call(this)
}
}
'use strict';
const {
OpenBridgeConfig
} = require('./config.js')
const {
TaskManager
} = require('./basic.js')
const {
TaskAccessTokenMP,
TaskAccessTokenH5,
TaskTicket
} = require('./task-weixin.js')
const TaskMapping = {
'weixin-mp': {
'accessToken': TaskAccessTokenMP
},
'weixin-h5': {
'accessToken': TaskAccessTokenH5,
'ticket': TaskTicket
}
}
class ScheduleManager extends TaskManager {
constructor() {
super()
}
async runAll() {
for (let i = 0; i < this.tasks.length; i++) {
const task = this.tasks[i]
try {
await task.run()
} catch (e) {
console.log("task.run::", e)
}
}
}
newTask(T, config) {
const newTask = new T(config)
super.addTask(newTask)
return newTask
}
}
ScheduleManager.instance = function() {
if (!ScheduleManager._instance) {
ScheduleManager._instance = new ScheduleManager()
}
return ScheduleManager._instance
}
async function main() {
const openBridgeConfig = new OpenBridgeConfig()
ScheduleManager.instance().clear()
for (let taskConfig of openBridgeConfig.tasks) {
let {
platform,
tasks
} = taskConfig
for (let taskName of tasks) {
const platformTask = TaskMapping[platform]
if (platformTask) {
ScheduleManager.instance().newTask(platformTask[taskName], taskConfig)
}
}
}
await ScheduleManager.instance().runAll()
}
module.exports = main;
{
"name": "uni-open-bridge",
"dependencies": {
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center",
"uni-open-bridge-common": "file:../../../../uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common"
},
"cloudfunction-config": {
"memorySize": 256,
"timeout": 60,
"triggers": [
{
"name": "uni-open-bridge",
"type": "timer",
"config": "0 0 * * * * *"
}
],
"path": "",
"runtime": "Nodejs8"
},
"extensions": {
"uni-cloud-jql": {},
"uni-cloud-redis": {}
}
}
\ No newline at end of file
'use strict';
const {
getAccessToken,
getTicket,
PlatformType
} = require('uni-open-bridge-common')
const {
Task
} = require('./basic.js')
class TaskAccessTokenMP extends Task {
constructor(config) {
super()
this._config = config || null
}
async run() {
const key = {
dcloudAppid: this._config.dcloudAppid,
platform: PlatformType.WEIXIN_MP
}
const result = await getAccessToken(key)
console.log("setAccessToken...", key, result)
}
}
TaskAccessTokenMP.ID = 'TaskAccessTokenMP'
class TaskAccessTokenH5 extends Task {
constructor(config) {
super()
this._config = config || null
}
async run() {
const key = {
dcloudAppid: this._config.dcloudAppid,
platform: PlatformType.WEIXIN_H5
}
const result = await getAccessToken(key)
console.log("setAccessToken...", key, result)
}
}
TaskAccessTokenH5.ID = 'TaskAccessTokenH5'
class TaskTicket extends Task {
constructor(config) {
super()
this._config = config || null
}
async run() {
const key = {
dcloudAppid: this._config.dcloudAppid,
platform: PlatformType.WEIXIN_H5
}
const result = await getTicket(key)
console.log("setTicket...", key, result)
}
}
TaskTicket.ID = 'TaskTicket'
module.exports = {
TaskAccessTokenMP,
TaskAccessTokenH5,
TaskTicket
}
## 2.0.1(2023-04-24)
- 优化示例运行体验,优化文档
## 2.0.0(2022-10-19)
- 升级 微信v2接口
- 支持 `uni-open-bridge` 管理微信认证凭证
- 增加 音视频检测接口`avSecCheck`
## 1.0.2(2021-09-16)
- 修复 textSecCheck接口错误码未格式化的Bug
## 1.0.1(2021-07-13)
- 为避免混淆contentSecCheck接口名调整为textSecCheck,旧接口依然支持
## 1.0.0(2021-06-25)
- 支持微信小程序提供的内容安全接口
{
"id": "uni-sec-check",
"displayName": "uni-sec-check",
"version": "2.0.1",
"description": "内容安全检测,包含图片和文字检测",
"keywords": [
"内容安全检查",
"图片鉴黄",
"违禁词",
"广告词",
"敏感词"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
# uni-sec-check
开发业务时时常遇到需要向用户发送一些通知,如欠费通知、会员到期通知等等。
uni-subcribe-msg模块可以方便开发者快速接入小程序订阅消息和微信公众号模板消息。
文档链接 [https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check](https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check)
const createConfig = require('uni-config-center')
const {
ErrorCode,
Cache,
createApi,
getClientInfo
} = require('./utils/index')
const MpWeixinService = require('./platform/mp-weixin/index')
const configCenter = createConfig({
pluginId: 'uni-sec-check'
})
const providerMap = {
'mp-weixin': MpWeixinService
}
class SecurityCheck {
constructor({
provider,
requestId
} = {}) {
if (!requestId) {
throw new Error('缺少 requestId 参数')
}
if (!provider || !providerMap[provider]) {
throw new Error(`请提供支持的provider参数,当前provider为:${provider}`)
}
this.provider = provider
const clientInfo = getClientInfo(requestId)
// 开发者提供的获取accessToken的接口,需要能返回{accessToken,expired}这种结构
this.service = createApi(providerMap[provider], {
clientInfo
})
this.ErrorCode = ErrorCode
}
/**
* 图片违规检测
* @param {String} image 图片地址
* @param {String} openid
* @param {Number} scene 1 资料;2 评论;3 论坛;4 社交日志
* @param {Number} version
* @return {Promise<*>}
*/
async imgSecCheck({
image,
openid,
scene,
version = 1
}) {
// 兼容内容安全V2
if (version === 2) {
if (!openid) throw new Error('openid required')
return this.service.mediaSecCheck({
mediaUrl: image,
mediaType: 2,
openid,
scene,
version
})
}
return this.service.imgSecCheck({
image
})
}
/**
* 文本内容违规检测
* @param {String} content 文本内容
* @deprecated
* @return {Promise<*>}
*/
async contentSecCheck({
content
}) {
console.warn('contentSecCheck接口已废弃,请使用textSecCheck')
return this.service.textSecCheck({
content
})
}
/**
* 文本内容违规检测
* @param {String} content
* @param {String} openid
* @param {Number} scene 1 资料;2 评论;3 论坛;4 社交日志
* @param {String} version
* @return {Promise<*>}
*/
async textSecCheck({
content,
openid,
scene,
version
}) {
return this.service.textSecCheck({
content,
openid,
scene,
version
})
}
/**
* 音视频内容违规检测
* @param {String} mediaUrl 媒体URL
* @param {String} openid
* @param {Number} scene 1 资料;2 评论;3 论坛;4 社交日志
* @param {String} version
* @return {Promise<*>}
*/
async avSecCheck ({
mediaUrl,
openid,
scene
}) {
return this.service.mediaSecCheck({
mediaUrl,
mediaType: 1,
openid,
scene
})
}
}
module.exports = SecurityCheck
{
"name": "uni-sec-check",
"version": "2.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center",
"uni-open-bridge-common": "file:../../../../../uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common"
}
}
\ No newline at end of file
const uobc = require('uni-open-bridge-common')
const {
requestWxApi
} = require('./wx-api')
const protocol = require('./protocol.js')
const {
FormData,
resolveFile
} = require('../../utils/index')
class WxOpenapi {
constructor({
clientInfo
} = {}) {
this.clientInfo = clientInfo
this._protocol = protocol
}
async getAccessToken () {
const params = {
dcloudAppid: this.clientInfo.appId,
platform: 'weixin-mp'
}
const result = await uobc.getAccessToken(params)
return result.access_token
}
async _requestWxApi(action, options) {
const accessToken = await this.getAccessToken()
if (!accessToken) throw new Error('缺少参数accessToken')
options.param = options.param || {}
options.param.accessToken = accessToken
return requestWxApi(action, options)
}
async imgSecCheck({
image
} = {}) {
if (!image) {
throw new Error('image required')
}
const {
filename,
contentType,
buffer
} = await resolveFile(image)
const form = new FormData()
form.append('img', buffer, {
filename,
contentType
})
return this._requestWxApi('imgSecCheck', {
content: form.getBuffer(),
headers: form.getHeaders()
})
}
async textSecCheck({
content,
openid,
scene = 1,
version = 1
}) {
if (!content) throw new Error('content required')
let params = {
content
}
// 兼容内容安全v2
if (version === 2) {
if (!openid) throw new Error('openid required')
params.version = version
params.openid = openid
params.scene = scene
}
return this._requestWxApi('contentSecCheck', {
data: params
})
}
async mediaSecCheck ({
mediaUrl,
mediaType,
version = 2,
openid,
scene = 1
}) {
if (!mediaUrl) throw new Error('mediaUrl required')
if (version === 2 && !openid) throw new Error('openid required')
return this._requestWxApi('mediaSecCheck', {
data: {
mediaUrl,
mediaType,
version,
openid,
scene
}
})
}
}
module.exports = WxOpenapi
const {
hasOwn,
ErrorCode
} = require('../../utils/index')
const errCodeMap = {
'-1': ErrorCode.SYSTEM_ERROR,
87014: ErrorCode.RISK_CONTENT,
40001: ErrorCode.INVALID_APPSECRET,
40004: ErrorCode.INVALID_MEDIA_TYPE,
40005: ErrorCode.INVALID_FILE_TYPE,
40006: ErrorCode.INVALID_MEDIA_SIZE,
40009: ErrorCode.INVALID_IMAGE_SIZE,
40010: ErrorCode.INVALID_AUDIO_SIZE,
40011: ErrorCode.INVALID_VIDEO_SIZE,
40013: ErrorCode.INVALID_APPID,
40014: ErrorCode.INVALID_ACCESS_TOKEN,
40033: ErrorCode.INVALID_REQUEST_URL,
40035: ErrorCode.INVALID_REQUEST_PARAM,
40038: ErrorCode.INVALID_REQUEST_FORMAT,
41011: ErrorCode.PARAM_REQUIRED,
42001: ErrorCode.ACCESS_TOKEN_EXPIRED,
44001: ErrorCode.EMPTY_MEDIA,
44002: ErrorCode.EMPTY_BODY,
44003: ErrorCode.EMPTY_IMAGE,
44004: ErrorCode.EMPTY_CONTENT,
45009: ErrorCode.INVOKE_OUT_OF_LIMIT
}
const labels = {
100: '正常',
10001: '广告',
20001: '时政',
20002: '色情',
20003: '辱骂',
20006: '违法犯罪',
20008: '欺诈',
20012: '低俗',
20013: '版权',
21000: '其他'
}
function parseErrCode(returnValue) {
if (returnValue.errCode === 0) {
return
}
returnValue.errMsg += ` errCode: ${returnValue.errCode}`
if (hasOwn(errCodeMap, returnValue.errCode)) {
returnValue.errCode = errCodeMap[returnValue.errCode]
} else {
returnValue.errCode = `uni-sec-check-mp-weixin-${returnValue.errCode}`
}
}
function filterValue (returnValue) {
delete returnValue.detail
if (returnValue.result) {
returnValue.result = {
suggest: returnValue.result.suggest,
label: labels[returnValue.result.label] || '未知'
}
if (returnValue.result.suggest !== 'pass') {
returnValue.errCode = ErrorCode.RISK_CONTENT
}
}
}
module.exports = {
imgSecCheck: {
returnValue: (returnValue) => {
parseErrCode(returnValue)
filterValue(returnValue)
return returnValue
}
},
mediaSecCheck: {
returnValue: (returnValue) => {
parseErrCode(returnValue)
filterValue(returnValue)
return returnValue
}
},
textSecCheck: {
returnValue: (returnValue) => {
parseErrCode(returnValue)
filterValue(returnValue)
return returnValue
}
}
}
const querystring = require('querystring')
const {
request,
snake2camelRecursive,
camel2snakeRecursive
} = require('../../utils/index')
const apiMap = {
getAccessToken: {
method: 'GET',
path: '/cgi-bin/token'
},
imgSecCheck: {
method: 'POST',
path: '/wxa/img_sec_check'
},
contentSecCheck: {
method: 'POST',
path: '/wxa/msg_sec_check'
},
mediaSecCheck: {
method: 'POST',
path: '/wxa/media_check_async'
}
}
const wxApiBase = 'https://api.weixin.qq.com'
function normalizeError({
errcode,
errmsg
} = {}) {
const error = new Error(errmsg)
error.code = errcode
throw error
}
function normalizeResult(result) {
result.errMsg = result.errmsg || ''
result.errCode = result.errcode || 0
delete result.errmsg
delete result.errcode
return snake2camelRecursive(result)
}
async function requestWxApi(action, {
param,
data,
content,
headers,
dataType = 'json'
} = {}) {
if (!apiMap[action]) {
throw new Error(`暂不支持${action}`)
}
let url = wxApiBase + apiMap[action].path
if (param) {
param = querystring.stringify(camel2snakeRecursive(param))
url = `${url}${url.indexOf('?') > -1 ? '&' : '?'}${param}`
}
if (data) {
data = camel2snakeRecursive(data)
}
const res = await request({
url,
method: apiMap[action].method,
content: content || JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
...headers
},
dataType
})
if (res.status >= 400) {
throw new Error(`请求微信服务错误,状态码:${res.status}`)
}
const result = res.data
return normalizeResult(result)
}
module.exports = {
requestWxApi
}
const db = uniCloud.database()
class Cache {
constructor({
type = 'db',
collection = "opendb-cloud-cache"
} = {}) {
this.type = type
this.collection = collection
}
async set(key, value, expired) {
if (typeof key !== 'string' || !key) {
return
};
!expired && (expired = 0)
await db.collection(this.collection).doc(key).set({
value,
expired
})
}
async get(key) {
const res = await db.collection(this.collection).doc(key).get()
const cache = res.data[0] || {
key,
value: undefined,
expired: -1
}
return cache
// 永不过期时expired需设置为0
// if (cache.expired && cache.expired < Date.now()) {
// return
// }
// return value
}
async remove(key) {
await db.collection(this.collection).doc(key).remove()
}
}
module.exports = {
Cache
}
const {
hasOwn
} = require('./utils')
const {
isPlainObject
} = require('./type')
const isSnakeCase = /_(\w)/g
const isCamelCase = /[A-Z]/g
function snake2camel(value) {
return value.replace(isSnakeCase, (_, c) => (c ? c.toUpperCase() : ''))
}
function camel2snake(value) {
return value.replace(isCamelCase, str => '_' + str.toLowerCase())
}
function parseObjectKeys(obj, type) {
let parserReg, parser
switch (type) {
case 'snake2camel':
parser = snake2camel
parserReg = isSnakeCase
break
case 'camel2snake':
parser = camel2snake
parserReg = isCamelCase
break
}
for (const key in obj) {
if (hasOwn(obj, key)) {
if (parserReg.test(key)) {
const keyCopy = parser(key)
obj[keyCopy] = obj[key]
delete obj[key]
if (isPlainObject(obj[keyCopy])) {
obj[keyCopy] = parseObjectKeys(obj[keyCopy], type)
} else if (Array.isArray(obj[keyCopy])) {
obj[keyCopy] = obj[keyCopy].map((item) => {
return parseObjectKeys(item, type)
})
}
}
}
}
return obj
}
function snake2camelRecursive(obj) {
return parseObjectKeys(obj, 'snake2camel')
}
function camel2snakeRecursive(obj) {
return parseObjectKeys(obj, 'camel2snake')
}
module.exports = {
snake2camelRecursive,
camel2snakeRecursive
}
function getClientInfo (requestId) {
if (!requestId) return undefined
const clientInfos = uniCloud.getClientInfos()
for (const clientInfo of clientInfos) {
if (clientInfo.requestId === requestId) {
return clientInfo
}
}
}
module.exports = {
getClientInfo
}
const {
isFn,
isPlainObject
} = require('./type')
const {
hasOwn
} = require('./utils')
// 注意:不进行递归处理
function parseParams(params = {}, rule) {
if (!rule || !params) {
return params
}
if (isPlainObject(rule)) {
for (const key in rule) {
const value = rule[key]
if (isFn(value)) {
// 通过function转化的默认不删除旧属性名
params[key] = value(params)
} else if (typeof value === 'string' && hasOwn(params, key)) {
// 直接转换属性名称的删除旧属性名
params[value] = params[key]
delete params[key]
}
}
} else if (isFn(rule)) {
params = rule(params)
}
return params
}
function createApi(ApiClass, options) {
const apiInstance = new ApiClass(options)
return new Proxy(apiInstance, {
get: function(obj, prop) {
if (typeof obj[prop] === 'function' && prop.indexOf('_') !== 0 && obj._protocol && obj._protocol[prop]) {
const protocol = obj._protocol[prop]
return async function(params) {
params = parseParams(params, protocol.args)
let result = await obj[prop](params)
result = parseParams(result, protocol.returnValue)
return result
}
} else {
return obj[prop]
}
}
})
}
module.exports = {
createApi
}
const ErrorCode = {
SYSTEM_ERROR: 'system-error',
RISK_CONTENT: 'risk-content',
INVALID_APPSECRET: 'invalid-appsecret',
INVALID_MEDIA_TYPE: 'invalid-media-type',
INVALID_FILE_TYPE: 'invalid-file-type',
INVALID_MEDIA_SIZE: 'invalid-media-size',
INVALID_IMAGE_SIZE: 'invalid-image-size',
INVALID_AUDIO_SIZE: 'invalid-audio-size',
INVALID_VIDEO_SIZE: 'invalid-video-size',
INVALID_APPID: 'invalid-appid',
INVALID_ACCESS_TOKEN: 'invalid-access-token',
INVALID_REQUEST_URL: 'invalid-request-url',
INVALID_REQUEST_PARAM: 'invalid-request-param',
INVALID_REQUEST_FORMAT: 'invalid-request-format',
PARAM_REQUIRED: 'param-required',
ACCESS_TOKEN_EXPIRED: 'access-token-expired',
EMPTY_MEDIA: 'empty-media',
EMPTY_BODY: 'empty-body',
EMPTY_IMAGE: 'empty-image',
EMPTY_CONTENT: 'empty-content',
INVOKE_OUT_OF_LIMIT: 'invoke-out-of-limit'
}
const errorCodePrefix = 'uni-sec-check'
for (let key in ErrorCode) {
ErrorCode[key] = `${errorCodePrefix}-${ErrorCode[key]}`
}
module.exports = {
ErrorCode
}
// author: wangyaqi@dcloud.io
// 搭配uniCloud.httpclient.request使用
// content: formData.getBuffer()
// header: formData.getHeaders(userHeaders)
class FormData {
constructor () {
this._boundary =
'------FormDataBaseBoundary' + Math.random().toString(36).substring(2)
this.dataList = []
}
_addData (data) {
// 优化 减少Buffer.concat执行次数
const lastData = this.dataList[this.dataList.length - 1]
if (typeof data === 'string' && typeof lastData === 'string') {
this.dataList[this.dataList.length - 1] = lastData + '\r\n' + data
} else {
this.dataList.push(data)
}
}
append (name, value, options) {
this._addData('--' + this._boundary)
let leading = `Content-Disposition: form-data; name="${name}"`
switch (Buffer.isBuffer(value)) {
case true:
if (!options.filename || !options.contentType) {
throw new Error('filename and contentType required')
}
leading += `; filename="${options.filename}"`
this._addData(leading)
this._addData(`Content-Type: ${options.contentType}`)
this._addData('')
this._addData(value)
break
default:
this._addData('')
this._addData(value)
}
}
getHeaders (options) {
const headers = {
'Content-Type': 'multipart/form-data; boundary=' + this._boundary
}
return Object.assign(headers, options)
}
getBuffer () {
let dataBuffer = Buffer.alloc(0)
this.dataList.forEach((item) => {
if (Buffer.isBuffer(item)) {
dataBuffer = Buffer.concat([dataBuffer, item])
} else {
dataBuffer = Buffer.concat([dataBuffer, Buffer.from('' + item)])
}
dataBuffer = Buffer.concat([dataBuffer, Buffer.from('\r\n')])
})
dataBuffer = Buffer.concat([dataBuffer, Buffer.from('--' + this._boundary + '--')])
return dataBuffer
}
}
module.exports = {
FormData
}
\ No newline at end of file
module.exports = {
...require('./case-transform'),
...require('./form-data'),
...require('./utils'),
...require('./request'),
...require('./resolve-file'),
...require('./cache'),
...require('./type'),
...require('./create-api'),
...require('./error'),
...require('./client-info')
}
async function request({
method = 'GET',
url,
content,
headers,
dataType = 'json'
} = {}) {
return await uniCloud.httpclient.request(url, {
method,
headers,
content,
dataType
})
}
module.exports = {
request
}
const {
getMimeType,
getExtension,
getFilename,
} = require('./utils')
async function resolveFilePath(file) {
const filename = getFilename(file)
const extension = filename.split('.').pop()
const contentType = getMimeType(extension)
if (!extension) {
throw new Error(`不支持的文件类型:${extension}`)
}
return new Promise((resolve, reject) => {
require('fs').readFile(file, (err, buffer) => {
if (err) {
reject(err)
return
}
resolve({
buffer,
contentType,
filename
})
})
})
}
async function resolveFileUrl(file) {
const res = await uniCloud.httpclient.request(file)
const contentType = res.headers['content-type'].split(';')[0]
const buffer = res.data
const extension = getExtension(contentType)
if (!extension) {
throw new Error('不支持的文件mime type:contentType')
}
const filename = Date.now() + '.' + extension
return {
buffer,
contentType,
filename
}
}
async function resolveFileId(file) {
const filename = getFilename(file)
const extension = filename.split('.').pop()
const contentType = getMimeType(extension)
if (!extension) {
throw new Error(`不支持的文件类型:${extension}`)
}
const {
fileContent: buffer
} = await uniCloud.downloadFile({
fileID: file
})
return {
buffer,
contentType,
filename
}
}
async function resolveFile(file) {
// 可以接收绝对路径、http://、cloudFileId
let fileType = 'path'
if (/^(?:https?:)/.test(file)) {
fileType = 'url'
} else if (/^cloud:/.test(file)) {
fileType = 'id'
}
switch (fileType) {
case 'path':
return resolveFilePath(file)
case 'url':
return resolveFileUrl(file)
case 'id':
return resolveFileId(file)
default:
break;
}
}
module.exports = {
resolveFile
}
const _toString = Object.prototype.toString
function isPlainObject(obj) {
return _toString.call(obj) === '[object Object]'
}
function isFn(fn) {
return typeof fn === 'function'
}
module.exports = {
isPlainObject,
isFn
}
const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
const extension2Type = {
image: 'image/*',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
}
function getMimeType(extension) {
return extension2Type[extension.toLowerCase()]
}
function getExtension(mimeType) {
return Object.keys(extension2Type).find(extension => extension2Type[extension] === mimeType)
}
function getFilename(path) {
return path.replace(/\\/g, '/').split('/').pop()
}
module.exports = {
getMimeType,
getExtension,
getFilename,
hasOwn
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册