From ffc52eee076c4b1f410e27397a7f246254b54c7b Mon Sep 17 00:00:00 2001 From: DCloud__JSON Date: Tue, 25 Apr 2023 21:09:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=AE=89=E5=85=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.md | 2 + manifest.json | 2 +- package-lock.json | 118 ++++++ package.json | 79 +++- pages/chat/chat.vue | 168 ++------ .../cloudfunctions/uni-ai-chat/index.obj.js | 385 +++++++++++------- .../cloudfunctions/uni-ai-chat/package.json | 25 +- .../cloudfunctions/uni-ai-chat/utils.js | 15 + .../uni-config-center/uni-ai-chat/config.json | 3 + .../uni-config-center/uni-id/config.json | 101 +++++ uni_modules/uni-link/changelog.md | 17 + .../uni-link/components/uni-link/uni-link.vue | 128 ++++++ uni_modules/uni-link/package.json | 87 ++++ uni_modules/uni-link/readme.md | 11 + .../uni-open-bridge-common/changelog.md | 23 ++ .../uni-open-bridge-common/package.json | 84 ++++ uni_modules/uni-open-bridge-common/readme.md | 5 + .../uni-open-bridge-common/bridge-error.js | 26 ++ .../common/uni-open-bridge-common/config.js | 124 ++++++ .../common/uni-open-bridge-common/consts.js | 30 ++ .../common/uni-open-bridge-common/index.js | 317 ++++++++++++++ .../uni-open-bridge-common/package.json | 15 + .../common/uni-open-bridge-common/storage.js | 111 +++++ .../uni-open-bridge-common/uni-cloud-cache.js | 324 +++++++++++++++ .../uni-open-bridge-common/validator.js | 31 ++ .../uni-open-bridge-common/weixin-server.js | 203 +++++++++ .../database/opendb-open-data.schema.json | 19 + uni_modules/uni-open-bridge/changelog.md | 2 + uni_modules/uni-open-bridge/package.json | 85 ++++ uni_modules/uni-open-bridge/readme.md | 5 + .../cloudfunctions/uni-open-bridge/basic.js | 131 ++++++ .../cloudfunctions/uni-open-bridge/bridge.js | 126 ++++++ .../cloudfunctions/uni-open-bridge/config.js | 153 +++++++ .../uni-open-bridge/index.obj.js | 94 +++++ .../uni-open-bridge/index.task.js | 83 ++++ .../uni-open-bridge/package.json | 24 ++ .../uni-open-bridge/task-weixin.js | 80 ++++ uni_modules/uni-sec-check/changelog.md | 12 + uni_modules/uni-sec-check/package.json | 81 ++++ uni_modules/uni-sec-check/readme.md | 7 + .../common/uni-sec-check/index.js | 132 ++++++ .../common/uni-sec-check/package.json | 16 + .../uni-sec-check/platform/mp-weixin/index.js | 111 +++++ .../platform/mp-weixin/protocol.js | 93 +++++ .../platform/mp-weixin/wx-api.js | 84 ++++ .../common/uni-sec-check/utils/cache.js | 44 ++ .../uni-sec-check/utils/case-transform.js | 61 +++ .../common/uni-sec-check/utils/client-info.js | 15 + .../common/uni-sec-check/utils/create-api.js | 53 +++ .../common/uni-sec-check/utils/error.js | 34 ++ .../common/uni-sec-check/utils/form-data.js | 66 +++ .../common/uni-sec-check/utils/index.js | 12 + .../common/uni-sec-check/utils/request.js | 18 + .../uni-sec-check/utils/resolve-file.js | 86 ++++ .../common/uni-sec-check/utils/type.js | 14 + .../common/uni-sec-check/utils/utils.js | 33 ++ 56 files changed, 3921 insertions(+), 287 deletions(-) create mode 100644 package-lock.json create mode 100644 uniCloud-aliyun/cloudfunctions/uni-ai-chat/utils.js create mode 100644 uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-ai-chat/config.json create mode 100644 uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json create mode 100644 uni_modules/uni-link/changelog.md create mode 100644 uni_modules/uni-link/components/uni-link/uni-link.vue create mode 100644 uni_modules/uni-link/package.json create mode 100644 uni_modules/uni-link/readme.md create mode 100644 uni_modules/uni-open-bridge-common/changelog.md create mode 100644 uni_modules/uni-open-bridge-common/package.json create mode 100644 uni_modules/uni-open-bridge-common/readme.md create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/bridge-error.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/config.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/consts.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/index.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/package.json create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/storage.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/uni-cloud-cache.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/validator.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/weixin-server.js create mode 100644 uni_modules/uni-open-bridge-common/uniCloud/database/opendb-open-data.schema.json create mode 100644 uni_modules/uni-open-bridge/changelog.md create mode 100644 uni_modules/uni-open-bridge/package.json create mode 100644 uni_modules/uni-open-bridge/readme.md create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/basic.js create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/bridge.js create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/config.js create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.obj.js create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.task.js create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/package.json create mode 100644 uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/task-weixin.js create mode 100644 uni_modules/uni-sec-check/changelog.md create mode 100644 uni_modules/uni-sec-check/package.json create mode 100644 uni_modules/uni-sec-check/readme.md create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/index.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/package.json create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/index.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/protocol.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/wx-api.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/cache.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/case-transform.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/client-info.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/create-api.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/error.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/form-data.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/index.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/request.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/resolve-file.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/type.js create mode 100644 uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/utils.js diff --git a/changelog.md b/changelog.md index bd00d25..501f633 100644 --- a/changelog.md +++ b/changelog.md @@ -1,2 +1,4 @@ +## 1.0.1(2023-04-25) +不再提供stream设置ui,自动根据是否开通并启用uni-push设置 ## 1.0.0(2023-04-24) 1.0.0 diff --git a/manifest.json b/manifest.json index dcecf5f..0891842 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name" : "uni-ai-chat", - "appid" : "__UNI__44E0E31", + "appid" : "__UNI__8F14B14", "description" : "", "versionName" : "1.0.0", "versionCode" : "100", diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1fc9f7a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,118 @@ +{ + "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==" + } + } +} diff --git a/package.json b/package.json index fe6a9eb..e6fc53d 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "id": "uni-ai-chat", "name": "uni-ai-chat", "version": "1.0.0", - "description": "云端一体uni-ai示例项目", + "description": "基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "repository": {}, + "repository": "https://github.com/dcloudio/uni-ui", "keywords": [ "uni-ai-chat" ], @@ -35,7 +35,80 @@ "data": "无", "permissions": "无" }, - "npmurl": "", + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", "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 diff --git a/pages/chat/chat.vue b/pages/chat/chat.vue index 7868475..30268a0 100644 --- a/pages/chat/chat.vue +++ b/pages/chat/chat.vue @@ -21,6 +21,12 @@ :type="msgStateIcon(msg)" class="msgStateIcon"> + + + uni-ai正在思考中... + + 如需提速,请开通 + @@ -30,16 +36,8 @@ - - - 流式响应 - - : - - - - + @@ -54,15 +52,6 @@ - - - - 流式响应 - - : - - - @@ -75,7 +64,7 @@ msgList: [], content: "", sseIndex: 0, - stream:false, + stream:true, isWidescreen:false } }, @@ -84,13 +73,7 @@ if (this.sseIndex !== 0) { return true } - - let length = this.msgList.length - if (length) { - return !this.msgList[length - 1].isAi - } else { - return false - } + return !!(this.msgList.length && this.msgList.length%2 !== 0) }, placeholderText() { if (this.inputBoxDisabled) { @@ -101,6 +84,9 @@ // #endif return '请输入要发给uni-ai的内容' } + }, + NODE_ENV(){ + return process.env.NODE_ENV } }, watch: { @@ -111,7 +97,7 @@ deep:true } }, - async mounted() { + async mounted() { // for (let i = 0; i < 15; i++) { // this.msgList.push({ // isAi: i % 2 == true, @@ -150,46 +136,7 @@ } // #endif - // 添加惰性函数,检查是否开通push - 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 = ()=>{} - } - }) - } + // 添加惰性函数,检查是否开通uni-push;决定是否启用stream // #ifdef H5 uni.createMediaQueryObserver(this).observe({ @@ -200,19 +147,27 @@ // #endif }, methods: { + //检查是否开通uni-push;决定是否启用stream + async checkIsOpenPush(){ + try{ + await uni.getPushClientId() + this.checkIsOpenPush = ()=>{} + }catch(err){ + this.stream = false + } + }, // updateLastMsg(){ // }, - changeStream(e){ - this.changeStream.check() - - // console.log('e',e.detail.value); - this.stream = e.detail.value - }, - retriesSendMsg() { + async retriesSendMsg() { + // 检查是否开通uni-push;决定是否启用stream + await this.checkIsOpenPush() this.send() }, - beforeSendMsg() { + async beforeSendMsg() { + // 检查是否开通uni-push;决定是否启用stream + await this.checkIsOpenPush() + if(!this.content){ return uni.showToast({ title: '内容不能为空', @@ -319,8 +274,8 @@ // console.log(res, res.reply); this.msgList.push({ isAi: true, - content: res.reply, - summarize: res.summarize, + content: res.data.reply, + summarize: res.data.summarize, create_time: Date.now() }) this.showLastMsg() @@ -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 - } + } } } @@ -543,10 +479,10 @@ .create_time { font-size: 12px; - padding: 5px; + padding: 5px 0; padding-top: 0; color: #aaa; - justify-content: center; + text-align: center; width:750rpx; /* #ifdef MP */ display: flex; @@ -607,22 +543,17 @@ justify-content: center; } - #set-stream{ - z-index: 999; - padding:0 5px; - justify-content: center; + .tip-ai-ing { align-items: center; - position: fixed; - bottom: 50px; - right: 0; - } - #set-stream .title{ - font-size: 12px; - position: relative; - left: 10rpx; + flex-direction: column; + font-size: 14px; + color: #919396; + padding: 15px 0; } - #set-stream switch{ - transform: scale(0.5); + + .uni-link { + margin-left: 5px; + line-height: 20px; } /* #ifdef H5 */ @@ -686,9 +617,6 @@ margin-top: 15px; justify-content: center; } - .create_time{ - display: flex; - } .textarea-box, .textarea, @@ -729,14 +657,6 @@ height: 15px; } - .set-stream .title { - font-size: 12px; - } - .set-stream switch { - transform: scale(0.6); - position: relative; - left: -5px; - } .textarea-box,.textarea-box *{ // border: 1px solid #000; diff --git a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js index 6d4f3a8..7e9f95e 100644 --- a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js +++ b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/index.obj.js @@ -1,165 +1,254 @@ // 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj -// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129 -module.exports = { - _before: function() { - // 这里是云函数的前置方法,你可以在这里加入你需要逻辑,比如:拦截用户必须登录才能访问等 - /* - 例如:使用uni-id-pages(链接地址:https://ext.dcloud.net.cn/plugin?id=8577)搭建账户体系。 - 然后再使用uni-id-common的uniIdCommon.checkToken判断用户端身份,验证不通过你可以直接`throw new Error(“token无效”)`抛出异常拦截访问。 - 如果验证通过了可以获得用户id,可以记录每一个用户id的调用次数来限制,调用多少次后必须充值(推荐用uni-pay,下载地址:https://ext.dcloud.net.cn/plugin?id=1835) - 或者看一个激励视频广告(详情:https://uniapp.dcloud.net.cn/uni-ad/ad-rewarded-video.html)后才能继续使用 - *** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 *** - */ - }, - async send({messages,SSEChannel}) { - // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 - // messages = [{ - // role: 'user', - // content: 'uni-app是什么,20个字以内进行说明' - // }] +// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129 +const {safeRequire, checkContentSecurityEnable} = require('./utils') +const createConfig = safeRequire('uni-config-center') +const config = createConfig({ + pluginId: 'uni-ai-chat' +}).config() - // 校验客户端提交的参数 - let res = checkMessages(messages) - if (res.errCode) { - throw new Error(res.errMsg) - } +module.exports = { + _before:async function() { + // 这里是云函数的前置方法,你可以在这里加入你需要逻辑,比如:拦截用户必须登录才能访问等 + /* + 例如:使用uni-id-pages(链接地址:https://ext.dcloud.net.cn/plugin?id=8577)搭建账户体系。 + 然后再使用uni-id-common的uniIdCommon.checkToken判断用户端身份,验证不通过你可以直接`throw new Error(“token无效”)`抛出异常拦截访问。 + 如果验证通过了可以获得用户id,可以记录每一个用户id的调用次数来限制,调用多少次后必须充值(推荐用uni-pay,下载地址:https://ext.dcloud.net.cn/plugin?id=1835) + 或者看一个激励视频广告(详情:https://uniapp.dcloud.net.cn/uni-ad/ad-rewarded-video.html)后才能继续使用 + *** 激励视频是造富神器。行业经常出现几个人的团队,月收入百万的奇迹。 *** + */ - // 向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 (this.getMethodName() == 'send' && config.contentSecurity) { + const UniSecCheck = safeRequire('uni-sec-check') + const uniSecCheck = new UniSecCheck({ + provider: 'mp-weixin', + requestId: this.getUniCloudRequestId() }) - - 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) - }) + this.textSecCheck = async (content)=>{ + let {SSEChannel} = this.getParams()[0]||{} + if(SSEChannel){ + return console.log('提示:流式响应模式,内容安全识别功能无效'); + } + // 检测文本 + const checkRes = await uniSecCheck.textSecCheck({ + content, + // openid, + scene:4, + version:1 //后续:支持微信登录后,微信小程序端 改用模式2 详情:https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check.html#%E4%BD%BF%E7%94%A8%E5%89%8D%E5%BF%85%E7%9C%8B }) - } 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 + console.log('checkRes检测文本',checkRes); + if (checkRes.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { + throw { + isSecCheck:true, + errCode: checkRes.errCode, + errMsg: '文字存在风险', + result: checkRes.result + } + } else if (checkRes.errCode) { + 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 // 直接抛出异常 } } - //获总结 - 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 + if (this.getMethodName() == 'send' && config.contentSecurity) { + try{ + await this.textSecCheck(result.data.reply) + }catch(e){ + return { + "data": { + "reply": "内容涉及敏感" + }, + "errCode": 0 + } + } } - - 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 result + }, + async send({ + messages, + SSEChannel + }) { + // 初次调试时,可不从客户端获取数据,直接使用下面写死在云函数里的数据 + // messages = [{ + // role: 'user', + // content: 'uni-app是什么,20个字以内进行说明' + // }] + + // 校验客户端提交的参数 + let res = checkMessages(messages) + if (res.errCode) { + throw new Error(res.errMsg) + } + + // 向uni-ai发送消息 + 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 { - errCode: 0, + if(res.errCode){ + throw res } - } catch (errMsg) { return { - errSubject: 'ai-demo', - errCode: 'param-error', - errMsg - } - } + data:res, + errCode: 0 + } + } + } + + //获总结 + async function getSummarize(messages) { + messages.push({ + "content": "请简要总结上述全部对话", + "role": "user" + }) + // 获取总结不需要再总结summarize和stream + let res = await chatCompletion({ + messages, + summarize: true, + stream: false, + SSEChannel: false + }) + return res.reply + } + + function checkMessages(messages) { + try { + if (messages === undefined) { + throw "messages为必传参数" + } else if (!Array.isArray(messages)) { + throw "参数messages的值类型必须是[object,object...]" + } else { + messages.forEach(item => { + if (typeof item != 'object') { + throw "参数messages的值类型必须是[object,object...]" + } + let itemRoleArr = ["assistant", "user", "system"] + if (!itemRoleArr.includes(item.role)) { + throw "参数messages[{role}]的值只能是:" + itemRoleArr.join('或') + } + if (typeof item.content != 'string') { + throw "参数messages[{content}]的值类型必须是字符串" + } + }) + } + return { + errCode: 0, + } + } catch (errMsg) { + return { + errSubject: 'ai-demo', + errCode: 'param-error', + errMsg + } + } } } } \ No newline at end of file diff --git a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/package.json b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/package.json index c35a21b..ddc8889 100644 --- a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/package.json +++ b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/package.json @@ -1,12 +1,15 @@ -{ - "name": "uni-ai-chat", - "dependencies": {}, - "extensions": { - "uni-cloud-jql": {}, - "uni-cloud-ai": {}, - "uni-cloud-push": {} - }, - "cloudfunction-config": { - "timeout": 60 - } +{ + "name": "uni-ai-chat", + "dependencies": { + "uni-config-center": "file:../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center", + "uni-sec-check": "file:../../../uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check" + }, + "extensions": { + "uni-cloud-jql": {}, + "uni-cloud-push": {}, + "uni-cloud-ai": {} + }, + "cloudfunction-config": { + "timeout": 60 + } } \ No newline at end of file diff --git a/uniCloud-aliyun/cloudfunctions/uni-ai-chat/utils.js b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/utils.js new file mode 100644 index 0000000..78ac2e6 --- /dev/null +++ b/uniCloud-aliyun/cloudfunctions/uni-ai-chat/utils.js @@ -0,0 +1,15 @@ +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) +} diff --git a/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-ai-chat/config.json b/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-ai-chat/config.json new file mode 100644 index 0000000..e19363a --- /dev/null +++ b/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-ai-chat/config.json @@ -0,0 +1,3 @@ +{ + "contentSecurity":false +} \ No newline at end of file diff --git a/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json b/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json new file mode 100644 index 0000000..6d59722 --- /dev/null +++ b/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json @@ -0,0 +1,101 @@ +{ + "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 diff --git a/uni_modules/uni-link/changelog.md b/uni_modules/uni-link/changelog.md new file mode 100644 index 0000000..7f43317 --- /dev/null +++ b/uni_modules/uni-link/changelog.md @@ -0,0 +1,17 @@ +## 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目录规范 diff --git a/uni_modules/uni-link/components/uni-link/uni-link.vue b/uni_modules/uni-link/components/uni-link/uni-link.vue new file mode 100644 index 0000000..8d72003 --- /dev/null +++ b/uni_modules/uni-link/components/uni-link/uni-link.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/uni_modules/uni-link/package.json b/uni_modules/uni-link/package.json new file mode 100644 index 0000000..3e99471 --- /dev/null +++ b/uni_modules/uni-link/package.json @@ -0,0 +1,87 @@ +{ + "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 diff --git a/uni_modules/uni-link/readme.md b/uni_modules/uni-link/readme.md new file mode 100644 index 0000000..d40fdca --- /dev/null +++ b/uni_modules/uni-link/readme.md @@ -0,0 +1,11 @@ + + +## 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 diff --git a/uni_modules/uni-open-bridge-common/changelog.md b/uni_modules/uni-open-bridge-common/changelog.md new file mode 100644 index 0000000..dbafb6f --- /dev/null +++ b/uni_modules/uni-open-bridge-common/changelog.md @@ -0,0 +1,23 @@ +## 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) +- 首次发布 diff --git a/uni_modules/uni-open-bridge-common/package.json b/uni_modules/uni-open-bridge-common/package.json new file mode 100644 index 0000000..e2f7064 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/package.json @@ -0,0 +1,84 @@ +{ + "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 diff --git a/uni_modules/uni-open-bridge-common/readme.md b/uni_modules/uni-open-bridge-common/readme.md new file mode 100644 index 0000000..de28c36 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/readme.md @@ -0,0 +1,5 @@ +# 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) diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/bridge-error.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/bridge-error.js new file mode 100644 index 0000000..b6cfe66 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/bridge-error.js @@ -0,0 +1,26 @@ +'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 +} diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/config.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/config.js new file mode 100644 index 0000000..063c746 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/config.js @@ -0,0 +1,124 @@ +'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 diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/consts.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/consts.js new file mode 100644 index 0000000..d89a5c1 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/consts.js @@ -0,0 +1,30 @@ +'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 +} diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/index.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/index.js new file mode 100644 index 0000000..a8dba4b --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/index.js @@ -0,0 +1,317 @@ +'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 +} diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/package.json b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/package.json new file mode 100644 index 0000000..a017b49 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/package.json @@ -0,0 +1,15 @@ +{ + "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 diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/storage.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/storage.js new file mode 100644 index 0000000..61e2fb3 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/storage.js @@ -0,0 +1,111 @@ +'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 +}; diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/uni-cloud-cache.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/uni-cloud-cache.js new file mode 100644 index 0000000..bde572e --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/uni-cloud-cache.js @@ -0,0 +1,324 @@ +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 +} diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/validator.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/validator.js new file mode 100644 index 0000000..231dc8b --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/validator.js @@ -0,0 +1,31 @@ +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 +} diff --git a/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/weixin-server.js b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/weixin-server.js new file mode 100644 index 0000000..87da133 --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common/weixin-server.js @@ -0,0 +1,203 @@ +'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 +} diff --git a/uni_modules/uni-open-bridge-common/uniCloud/database/opendb-open-data.schema.json b/uni_modules/uni-open-bridge-common/uniCloud/database/opendb-open-data.schema.json new file mode 100644 index 0000000..e7774df --- /dev/null +++ b/uni_modules/uni-open-bridge-common/uniCloud/database/opendb-open-data.schema.json @@ -0,0 +1,19 @@ +// 文档教程: 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": "过期时间" + } + } +} diff --git a/uni_modules/uni-open-bridge/changelog.md b/uni_modules/uni-open-bridge/changelog.md new file mode 100644 index 0000000..9fc6238 --- /dev/null +++ b/uni_modules/uni-open-bridge/changelog.md @@ -0,0 +1,2 @@ +## 1.0.1(2022-08-22) +- 首次发布 diff --git a/uni_modules/uni-open-bridge/package.json b/uni_modules/uni-open-bridge/package.json new file mode 100644 index 0000000..6680d63 --- /dev/null +++ b/uni_modules/uni-open-bridge/package.json @@ -0,0 +1,85 @@ +{ + "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 diff --git a/uni_modules/uni-open-bridge/readme.md b/uni_modules/uni-open-bridge/readme.md new file mode 100644 index 0000000..c2e7066 --- /dev/null +++ b/uni_modules/uni-open-bridge/readme.md @@ -0,0 +1,5 @@ +# 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) diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/basic.js b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/basic.js new file mode 100644 index 0000000..4774175 --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/basic.js @@ -0,0 +1,131 @@ +'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 +}; diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/bridge.js b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/bridge.js new file mode 100644 index 0000000..f14de56 --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/bridge.js @@ -0,0 +1,126 @@ +'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; diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/config.js b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/config.js new file mode 100644 index 0000000..773353a --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/config.js @@ -0,0 +1,153 @@ +'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 +}; diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.obj.js b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.obj.js new file mode 100644 index 0000000..cc45586 --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.obj.js @@ -0,0 +1,94 @@ +'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) + } +} diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.task.js b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.task.js new file mode 100644 index 0000000..3db5253 --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/index.task.js @@ -0,0 +1,83 @@ +'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; diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/package.json b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/package.json new file mode 100644 index 0000000..84e0e22 --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/package.json @@ -0,0 +1,24 @@ +{ + "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 diff --git a/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/task-weixin.js b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/task-weixin.js new file mode 100644 index 0000000..ddd984c --- /dev/null +++ b/uni_modules/uni-open-bridge/uniCloud/cloudfunctions/uni-open-bridge/task-weixin.js @@ -0,0 +1,80 @@ +'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 +} diff --git a/uni_modules/uni-sec-check/changelog.md b/uni_modules/uni-sec-check/changelog.md new file mode 100644 index 0000000..76186b3 --- /dev/null +++ b/uni_modules/uni-sec-check/changelog.md @@ -0,0 +1,12 @@ +## 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) +- 支持微信小程序提供的内容安全接口 diff --git a/uni_modules/uni-sec-check/package.json b/uni_modules/uni-sec-check/package.json new file mode 100644 index 0000000..a70a126 --- /dev/null +++ b/uni_modules/uni-sec-check/package.json @@ -0,0 +1,81 @@ +{ + "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" + } + } + } + } +} diff --git a/uni_modules/uni-sec-check/readme.md b/uni_modules/uni-sec-check/readme.md new file mode 100644 index 0000000..e22abf6 --- /dev/null +++ b/uni_modules/uni-sec-check/readme.md @@ -0,0 +1,7 @@ +# uni-sec-check + +开发业务时时常遇到需要向用户发送一些通知,如欠费通知、会员到期通知等等。 + +uni-subcribe-msg模块可以方便开发者快速接入小程序订阅消息和微信公众号模板消息。 + +文档链接 [https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check](https://uniapp.dcloud.net.cn/uniCloud/uni-sec-check) diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/index.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/index.js new file mode 100644 index 0000000..f890455 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/index.js @@ -0,0 +1,132 @@ +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 diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/package.json b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/package.json new file mode 100644 index 0000000..400a685 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/package.json @@ -0,0 +1,16 @@ +{ + "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 diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/index.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/index.js new file mode 100644 index 0000000..bc0a42b --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/index.js @@ -0,0 +1,111 @@ +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 diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/protocol.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/protocol.js new file mode 100644 index 0000000..144d436 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/protocol.js @@ -0,0 +1,93 @@ +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 + } + } +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/wx-api.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/wx-api.js new file mode 100644 index 0000000..4bc2045 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/platform/mp-weixin/wx-api.js @@ -0,0 +1,84 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/cache.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/cache.js new file mode 100644 index 0000000..11d59ad --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/cache.js @@ -0,0 +1,44 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/case-transform.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/case-transform.js new file mode 100644 index 0000000..ff381d6 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/case-transform.js @@ -0,0 +1,61 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/client-info.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/client-info.js new file mode 100644 index 0000000..49dfd33 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/client-info.js @@ -0,0 +1,15 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/create-api.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/create-api.js new file mode 100644 index 0000000..b463590 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/create-api.js @@ -0,0 +1,53 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/error.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/error.js new file mode 100644 index 0000000..360aee5 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/error.js @@ -0,0 +1,34 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/form-data.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/form-data.js new file mode 100644 index 0000000..e87d715 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/form-data.js @@ -0,0 +1,66 @@ +// 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 diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/index.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/index.js new file mode 100644 index 0000000..6ee01ff --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/index.js @@ -0,0 +1,12 @@ +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') +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/request.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/request.js new file mode 100644 index 0000000..31940c8 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/request.js @@ -0,0 +1,18 @@ +async function request({ + method = 'GET', + url, + content, + headers, + dataType = 'json' +} = {}) { + return await uniCloud.httpclient.request(url, { + method, + headers, + content, + dataType + }) +} + +module.exports = { + request +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/resolve-file.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/resolve-file.js new file mode 100644 index 0000000..9e87607 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/resolve-file.js @@ -0,0 +1,86 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/type.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/type.js new file mode 100644 index 0000000..4b3eb3d --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/type.js @@ -0,0 +1,14 @@ +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 +} diff --git a/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/utils.js b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/utils.js new file mode 100644 index 0000000..9cbb404 --- /dev/null +++ b/uni_modules/uni-sec-check/uniCloud/cloudfunctions/common/uni-sec-check/utils/utils.js @@ -0,0 +1,33 @@ +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 +} -- GitLab