├── (XT[3C8SV0TO4~`T2A([N`7.png ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml ├── modules.xml ├── vcs.xml ├── workspace.xml └── 微信服务.iml ├── .prettierrc ├── .releaserc ├── 1557823227950.png ├── 2994169387.txt ├── 4ca97265e7616b13f73dbf03a93_p43_mk42.jpg ├── DATA.json ├── DATABASE.json ├── TKMU(RLCF%{PM]@E8]$291V.png ├── api ├── 2994169387.txt ├── index.html └── test.js ├── assets └── avatar │ ├── AbTDic.png │ ├── Hz6dGT.png │ ├── iJk7EN.png │ ├── rMbt4N.png │ ├── rRR5dz.png │ ├── sanwei │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 6.png │ ├── 7.png │ └── 8.png │ ├── tGfXc5.png │ ├── temp │ ├── 1.png │ ├── 2.png │ └── 3.png │ └── xuesong │ ├── 500_1.png │ ├── 500_2.png │ ├── 500_3.png │ ├── xs1.png │ ├── xs2.png │ ├── xs3.png │ └── xs4.png ├── bin └── run.js ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── project ├── DATA.json ├── assets │ └── avatar │ │ └── xuesong │ │ └── xs1.png ├── config.json ├── index.html ├── index.js ├── package.json └── 自行更改文件名称与内容验证微信.txt ├── readme.md ├── src ├── client │ ├── client.ts │ ├── index.html │ └── tsconfig.json ├── node │ ├── Activity │ │ └── Avatar.ts │ ├── Encrypt.ts │ ├── MessageParser │ │ └── index.ts │ ├── index.ts │ ├── plugins │ │ ├── ParsePlatFormMessagePlugin.ts │ │ ├── SelfWeChatPlugin.ts │ │ └── ThirdPartWeChatPlugins.ts │ ├── root.d.ts │ ├── server.ts │ ├── tsconfig.json │ └── util.ts └── util │ └── Log.ts ├── test.jpg ├── test ├── project.spec.ts └── project │ ├── DATA.json │ ├── assets │ └── avatar │ │ ├── sanwei │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 6.png │ │ ├── 7.png │ │ └── 8.png │ │ ├── xuesong │ │ ├── xs1.png │ │ ├── xs2.png │ │ ├── xs3.png │ │ └── xs4.png │ │ └── zYJ3hD.png │ ├── config.json │ ├── index.js │ └── package.json ├── tsconfig.base.json ├── util ├── Log.d.ts └── Log.js ├── vercel.json └── watch-bob.jpg /(XT[3C8SV0TO4~`T2A([N`7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/(XT[3C8SV0TO4~`T2A([N`7.png -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | tags: 9 | - v*.*.* 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 12 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 12 20 | cache: 'npm' 21 | registry-url: 'https://registry.npmjs.org' 22 | - run: npm install 23 | - run: npm run build 24 | - run: npm publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | .vercel 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 23 | 24 | 31 | 32 | 39 | 40 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 18 | 19 | 20 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 40 | 41 | 42 | 85 | 86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /project/index.js: -------------------------------------------------------------------------------- 1 | const { avatarPlugins } = require('wx-serve') 2 | 3 | async function test({ target, Content, FromUserName, root, rawContent }) { 4 | // 图片活动 5 | await avatarPlugins({ targetInfo: target, uid: FromUserName, content: Content, root, frameName: ['xs1'], dir: 'xuesong' }) 6 | } 7 | 8 | module.exports = test 9 | -------------------------------------------------------------------------------- /project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temp", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "description": "" 11 | } 12 | -------------------------------------------------------------------------------- /project/自行更改文件名称与内容验证微信.txt: -------------------------------------------------------------------------------- 1 | 自行更改文件名称与内容验证微信 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | 8 |

9 | 10 | **wx-serve** 是一个基于微信第三方公众号而搭建的框架,目的在于解决快速开发与部署微信服务,让开发者可以专注于插件的开发之中。 11 | 12 | ![](https://res.psy-1.com/Fvhwh76XUZjkdieubH-3ptkF9woy) 13 | 14 | #### 和我们平时的微信第三方搭建方式有什么区别? 15 | 1.通过npm命令行,开箱即用,无需额外代码 16 | 17 | 2.监听业务代码,实行热更新功能 18 | 19 | 3.线上服务启动后,只需要开发者关注业务代码本身,功能持续运行 20 | 21 | 4.插件式开发(希望开发者的插件可以共享起来) 22 | 23 | 5.记录微信数据的文件,可以随时通过磁盘文件的方式同步,这意味着线上环境的数据可以与测试环境的数据进行同步,不用担心过期丢失刷新的问题。 24 | 25 | #### 目前谁在用? 26 | ![](https://res.psy-1.com/FiEYEGfPNPXh0EiQ7hS2FepPVg6l) 27 | ![](https://res.psy-1.com/FvyvUnfID9IQyl0-dbtGGEv7-P2d) 28 | 29 | 刚拿出来共享,还需要完善的API还有很多。 30 | 31 | 32 | ## Installation 33 | ```shell script 34 | npm install wx-serve --save 35 | ``` 36 | 37 | ## Usgae 38 | 在使用该服务前,请先在微信开放平台上进行注册 39 | 40 | https://open.weixin.qq.com/ 41 | 42 | 注册成功后,配置参数,在域名上运行wx-serve即可,可以自动监听入口依赖文件的改变。 43 | 44 | (注意不会监听```config.json```的变动,如需改变,请重新开启服务) 45 | 46 | **step1创建模板**: 47 | ```shell script 48 | wx-serve create --appid yourAppid --url yourUrl 49 | ``` 50 | 51 | **step2修改config**: 52 | 53 | 创建模板后,修改```config.json```,根据微信配置输入appid,secret,encodingAESKey,token。 54 | 55 | **step3修改txt验证**: 56 | 57 | 修改文件```自行更改文件名称与内容验证微信```(该文件用于微信域名验证) 58 | 59 | **step4开启服务**: 60 | ```shell script 61 | wx-serve --port 3000 62 | ``` 63 | 64 | 微信公众号拥有者扫码打开localhost:3000,点击按钮进行授权。 65 | 66 | 微信公众号窗口发送任意信息,可以获得一张图片,服务运行成功。 67 | 68 | ## config.json 69 | ```json 70 | { 71 | "wechat": { 72 | "appid": "微信平台提供", 73 | "secret": "微信平台提供", 74 | "encodingAESKey": "微信平台提供", 75 | "token": "微信平台提供" 76 | }, 77 | "data": "./DATA.json", 78 | "input": "./index.js" 79 | } 80 | ``` 81 | 82 | ```wechat```: 微信配置提供 83 | 84 | ```data```: 储存运行状态信息的文件 85 | 86 | ```input```: 插件入口 87 | 88 | ## 插件入口参数 89 | ```typescript 90 | function input({ target, Content, FromUserName, root, rawContent }) { 91 | } 92 | ``` 93 | 94 | ```target```: 第三方平台信息 95 | 96 | ```Content```: 用户发送的消息内容 97 | 98 | ```FromUserName```: 用户平台id 99 | 100 | ```root```: 命令行运行路径 101 | 102 | ```rawContent```: 用户发送的消息内容XML 103 | 104 | ## API 105 | ### sendContent 106 | ```typescript 107 | function sendContent(toUser: any, content: any, serveAccessToken: any, type: 'voice' | 'video' | 'image' | 'text') { // type voice video image 108 | } 109 | ``` 110 | 111 | ```toUser```: 用户平台id (入口```FromUserName```参数) 112 | 113 | ```type```: 消息类型 114 | 115 | ```content```: 内容|媒体ID 116 | 117 | ```serveAccessToken```: 目标平台的serveAccessToken(入口```target.authorizer_access_token```参数可取) 118 | 119 | 120 | ## TODO 121 | [] 完善测试action 122 | 123 | [√] 消息API兼容 124 | 125 | [] 开源交友插件 126 | -------------------------------------------------------------------------------- /src/client/client.ts: -------------------------------------------------------------------------------- 1 | console.log('client ~') 2 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 晚安睡务局 6 | 37 | 38 | 39 | 40 | 41 | 42 | 85 | 91 | 92 | 277 |
278 |
279 | 280 | 281 |
282 |
283 |
284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist", 5 | "module": "esnext", 6 | "lib": ["ESNext", "DOM"] 7 | }, 8 | "include": ["./"] 9 | } 10 | -------------------------------------------------------------------------------- /src/node/Activity/Avatar.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { createCanvas, loadImage } from 'canvas' 3 | import { randomString } from '../util'; 4 | // @ts-ignore 5 | import request from 'request' 6 | import { getUserInfo, sendContent } from '../MessageParser' 7 | 8 | const fs = require('fs') 9 | const path = require("path") 10 | 11 | // 边框贴图渲染 12 | export async function parseBlockTypeAvatar({ root, frameName, userPicUrl = '', dir = '' }: { root?: any, frameName?: any, userPicUrl?: string, dir?: string } = {}) { 13 | const width = 512 14 | const height = 512 15 | const canvas = createCanvas(width, height) 16 | const ctx = canvas.getContext('2d') 17 | 18 | // 绘制头像 19 | await loadImage(userPicUrl.replace(/132$/, '0')).then((image: any) => { 20 | ctx.drawImage(image, 0, 0, width, height) 21 | }) 22 | 23 | const data = fs.readFileSync(path.join(root, dir + frameName)) 24 | // // 绘制叠加的框框 25 | await loadImage(data).then((image: any) => { 26 | ctx.drawImage(image, 0, 0, width, height) 27 | }) 28 | 29 | // todo 不要使用写进本地文件的方式 30 | const promise = new Promise((resolve) => { 31 | const hash = randomString(6) 32 | let done: boolean = false 33 | setTimeout(() => { 34 | if (!done) { 35 | resolve(undefined) 36 | } 37 | }, 5000) 38 | 39 | // @ts-ignore 40 | fs.writeFile(path.join(root, `${dir}${hash}.png`), canvas.toBuffer('image/jpeg', { quality: 1 }), (err: any) => { 41 | done = true 42 | if (err) { 43 | console.log(err) 44 | resolve(undefined) 45 | return 46 | } 47 | resolve(path.join(root, `${dir}${hash}.png`)) 48 | }) 49 | }).catch((e) => { 50 | console.log(e) 51 | }) 52 | 53 | return promise 54 | } 55 | 56 | async function avatarPlugins({ targetInfo, uid, frameName, root, dir, index = 0 }: any = {}) { 57 | console.log('图片渲染活动') 58 | let formData = { 59 | my_field: 'my_value', 60 | my_file: '' 61 | } 62 | 63 | // 获取用户信息头像 64 | const userInfo = await getUserInfo({ serveAccessToken: targetInfo.authorizer_access_token, uid, platFormName: targetInfo.name }) 65 | 66 | console.log(userInfo) 67 | 68 | let resultPath: any = '' 69 | if (userInfo && userInfo.picUrl) { 70 | console.log('userInfo') 71 | resultPath = await parseBlockTypeAvatar({ root, frameName: frameName[index] + '.png', userPicUrl: (userInfo || {}).picUrl, dir }) 72 | if (resultPath) { 73 | formData.my_file = fs.createReadStream(resultPath) 74 | } else { 75 | console.log('没有用户信息,不进行头像渲染') 76 | return 77 | } 78 | } 79 | 80 | if (global.__TEST__) { 81 | // 测试模式下 不需要上传图片并发送给用户 82 | return '' 83 | } 84 | 85 | // 上传图片 并发送 86 | request.post({url:`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${targetInfo.authorizer_access_token}&type=image`, formData: formData}, async function(err: any, httpResponse: any, body: any) { 87 | // 删除文件 免得占用内存 88 | if (resultPath) { 89 | fs.unlink(resultPath, function(err: any){ 90 | if(err){ 91 | throw err; 92 | } 93 | }) 94 | } 95 | 96 | if (err) { 97 | return console.error('upload failed: ', err) 98 | } 99 | 100 | if (JSON.parse(body).media_id) { 101 | // 发送消息给用户 102 | sendContent(uid, JSON.parse(body).media_id, targetInfo.authorizer_access_token, 'image') 103 | if (frameName.length > index + 1) { 104 | avatarPlugins({ userInfo, formData, targetInfo, uid, frameName, index: index + 1, root, dir }) 105 | } 106 | } 107 | }) 108 | } 109 | 110 | export default avatarPlugins 111 | -------------------------------------------------------------------------------- /src/node/Encrypt.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | var Encrypt = function (options: any) { 4 | this.checkParams(options) 5 | this.appId = options.appId 6 | this.encodingAESKey = options.encodingAESKey 7 | this.token = options.token 8 | this.aesKey = this.getAesKey(this.encodingAESKey) 9 | this.iv = this.getIv(this.aesKey) 10 | } 11 | 12 | Encrypt.prototype = { 13 | checkParams: function (options: any) { 14 | var keys = ['appId', 'encodingAESKey', 'token'] 15 | keys.forEach(function (key) { 16 | if (typeof options[key] !== 'string') { 17 | throw new Error('Encrypt constructor() params is all String') 18 | } 19 | }) 20 | }, 21 | getAesKey: function (encodingAESKey: any) { 22 | return Buffer.from(encodingAESKey + '=', 'base64') 23 | }, 24 | getIv: function (aesKey: any) { 25 | return aesKey.slice(0, 16) 26 | }, 27 | encode: function (xmlMsg: any) { 28 | if (typeof xmlMsg !== 'string') { 29 | throw new TypeError('encode() required a String!') 30 | } 31 | var random = crypto.randomFillSync(Buffer.alloc(16)) 32 | var buf = Buffer.from(xmlMsg) 33 | var msgLength = Buffer.alloc(4) 34 | msgLength.writeUInt32BE(buf.length, 0) 35 | var idBuf = Buffer.from(this.appId) 36 | var xmlBuf = Buffer.from(xmlMsg) 37 | var totalBuf = Buffer.concat([random, msgLength, xmlBuf, idBuf]) 38 | totalBuf = this.PKCS7Encode(totalBuf) 39 | var cipher = crypto.createCipheriv('aes-256-cbc', this.aesKey, this.iv) 40 | cipher.setAutoPadding(false) 41 | var encryptBuf = Buffer.concat([cipher.update(totalBuf), cipher.final()]) 42 | return encryptBuf.toString('base64') 43 | }, 44 | decode: function (encryptMsg: any) { 45 | if (typeof encryptMsg !== 'string') { 46 | throw new TypeError('decode() required a String!') 47 | } 48 | var decipher = crypto.createDecipheriv('aes-256-cbc', this.aesKey, this.iv) 49 | decipher.setAutoPadding(false) 50 | var decipherBuffer = Buffer.concat([decipher.update(encryptMsg, 'base64'), decipher.final()]) 51 | decipherBuffer = this.PKCS7Decode(decipherBuffer) 52 | var msgLength = decipherBuffer.slice(16, 20).readUInt32BE(0) 53 | var result = decipherBuffer.slice(20, msgLength + 20).toString() 54 | return result 55 | }, 56 | getSignature: function (data: any) { 57 | var str = ([ 58 | data.nonce, 59 | data.timestamp, 60 | 'LnrDkrmurYPe3yPgziYAdwaTuk2obn7s' 61 | ]).sort().join('') 62 | 63 | return crypto.createHash('sha1').update(str).digest('hex') 64 | }, 65 | verify: function (data: any) { 66 | var msg_signature = data.msg_signature 67 | var result = this.getSignature(data) 68 | return result === msg_signature 69 | }, 70 | PKCS7Decode: function (buf: any) { 71 | var len = buf[buf.length - 1] 72 | if (len < 1 || len > 32) { 73 | len = 0 74 | } 75 | return buf.slice(0, buf.length - len) 76 | }, 77 | PKCS7Encode: function (buf: any) { 78 | var blockSize = 32 79 | var len = buf.length 80 | var paddingLength = blockSize - (len % blockSize) 81 | var paddingBuffer = Buffer.alloc(paddingLength, paddingLength) 82 | return Buffer.concat([buf, paddingBuffer]) 83 | } 84 | } 85 | 86 | export default Encrypt 87 | -------------------------------------------------------------------------------- /src/node/MessageParser/index.ts: -------------------------------------------------------------------------------- 1 | import SuperAgent from 'superagent' 2 | import { userInfoCache, UserInfo } from '../server' 3 | 4 | // 发送媒体信息给用户 5 | export function sendContent( 6 | toUser: any, 7 | content: any, 8 | serveAccessToken: any, 9 | type: 'voice' | 'video' | 'image' | 'text' 10 | ) { 11 | // type voice video image 12 | return new Promise((resolve) => { 13 | const serviceData = { 14 | touser: toUser, 15 | msgtype: type, 16 | [type]: { 17 | media_id: content 18 | } 19 | } 20 | 21 | if (type === 'text') { 22 | serviceData[type] = { 23 | content 24 | } 25 | } 26 | 27 | console.log(serviceData) 28 | 29 | SuperAgent.post( 30 | `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${serveAccessToken}` 31 | ) 32 | .send(serviceData) 33 | .end((err, res) => { 34 | console.log('消息发送回调结果: ', err, res?.body) 35 | resolve(null) 36 | }) 37 | }) 38 | } 39 | 40 | // 获取用户信息 41 | export async function getUserInfo({ 42 | serveAccessToken, 43 | uid, 44 | platFormName 45 | }: { 46 | serveAccessToken: string 47 | uid: string 48 | platFormName: string 49 | }): Promise { 50 | return new Promise((resolve) => { 51 | if (global.__TEST__) { 52 | resolve({ 53 | name: 'foo', 54 | picUrl: 'https://res.psy-1.com/Fr0Rww96T0CK_lG0y36fm-IE75XD', 55 | openid: 'foo', 56 | sex: '1', 57 | all: {} 58 | }) 59 | return 60 | } 61 | let cache = userInfoCache.get(uid) 62 | if (cache) { 63 | resolve(cache) 64 | return 65 | } 66 | SuperAgent.get( 67 | `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${serveAccessToken}&openid=${uid}&lang=zh_CN` 68 | ).end((err, res) => { 69 | if (!res) { 70 | console.log('获取用户信息没有响应') 71 | return 72 | } 73 | if (res.body) { 74 | const data = { 75 | name: res.body.nickname, 76 | picUrl: res.body.headimgurl, 77 | openid: res.body.openid, 78 | sex: res.body.sex, 79 | all: res.body 80 | } 81 | if (res.body.openid) { 82 | userInfoCache.set(res.body.openid, data) 83 | } 84 | resolve(data) 85 | return 86 | } 87 | resolve(undefined) 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server' 2 | // import avatarPlugins from './Activity/Avatar' 3 | 4 | // export { avatarPlugins } 5 | -------------------------------------------------------------------------------- /src/node/plugins/ParsePlatFormMessagePlugin.ts: -------------------------------------------------------------------------------- 1 | // 处理第三方平台的信息 2 | import { Plugin } from '../server' 3 | import { getData } from '../util' 4 | import _Log from '../../util/Log' 5 | 6 | const madge = require('madge') 7 | const path = require('path') 8 | 9 | // 需插件引入 10 | // import avatarPlugins from '../Activity/Avatar' 11 | 12 | const ParsePlatFormMessagePlugin: Plugin = ({ 13 | app, 14 | Router, 15 | encrypt, 16 | root, 17 | DATA, 18 | input, 19 | watcher 20 | }) => { 21 | let inputMth: Function 22 | 23 | try { 24 | const Log = _Log(`热更新:`) 25 | watcher.on('change', (file) => { 26 | madge(path.join(root, input)).then( 27 | (res: { tree: Record }) => { 28 | if (Object.keys(res.tree).includes(path.relative(root, file))) { 29 | Log(`文件${file}改动,将重加载入口方法`) 30 | // 消息处理 31 | inputMth = require(path.join(root, input)) 32 | } 33 | } 34 | ) 35 | }) 36 | 37 | // 消息处理 38 | inputMth = require(path.join(root, input)) 39 | } catch (e) { 40 | console.log(e) 41 | } 42 | 43 | if (app) { 44 | app.use(async (ctx, next) => {}) 45 | } 46 | 47 | // 监听第三方平台信息 48 | Router.post(`/wechat_open_platform/:id/message`, async (ctx) => { 49 | const platFormId = ctx.params.id 50 | let target: any = {} 51 | 52 | for (let i = 0; i < DATA.thirdPart.length; i++) { 53 | const _target = DATA.thirdPart[i] 54 | if (_target.appid === platFormId) { 55 | target = _target 56 | break 57 | } 58 | } 59 | 60 | const { result } = await getData(ctx, encrypt) 61 | 62 | const Content = 63 | (/]*>\<\!\[CDATA\[([\s\S]*?)\]\]\><\/Content>/gm.exec( 64 | result 65 | ) || [])![1] 66 | const FromUserName = 67 | (/]*>\<\!\[CDATA\[([\s\S]*?)\]\]\><\/FromUserName>/gm.exec( 68 | result 69 | ) || [])![1] 70 | 71 | const Log = _Log(`收到来自${target.name}(${platFormId})的消息:`) 72 | 73 | Log(Content) 74 | Log(result) 75 | 76 | ctx.response.body = 'success' 77 | 78 | console.log(inputMth) 79 | 80 | // todo 消息插件 target content FromUserName 81 | if (inputMth && typeof inputMth === 'function') { 82 | inputMth({ target, Content, FromUserName, root, rawContent: result }) 83 | } 84 | 85 | // @ts-ignore 86 | if (inputMth?.default && typeof inputMth?.default === 'function') { 87 | inputMth({ target, Content, FromUserName, root, rawContent: result }) 88 | } 89 | }) 90 | } 91 | 92 | export default ParsePlatFormMessagePlugin 93 | -------------------------------------------------------------------------------- /src/node/plugins/SelfWeChatPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../server' 2 | import _Log from '../../util/Log' 3 | import SuperAgent from 'superagent' 4 | import { writeFile, getData } from '../util' 5 | 6 | export let EnctypeTicket = '' 7 | 8 | const Log = _Log('Message from 自身平台:') 9 | Log(`读取本地DATA文件,获取EnctypeTicket: ${EnctypeTicket}`) 10 | 11 | // 微信第三方自身授权 12 | const SelfWeChatPlugin: Plugin = ({ 13 | app, 14 | Router, 15 | root, 16 | encrypt, 17 | appid, 18 | secret, 19 | DATA 20 | }) => { 21 | if (app) { 22 | app.use(async (ctx, next) => {}) 23 | } 24 | 25 | EnctypeTicket = DATA && DATA.self && DATA.self.Encrypt 26 | 27 | // 每10分钟会有请求进来 28 | Router.post( 29 | '/wechat_open_platform/auth/callback', 30 | async (ctx: any, res: any) => { 31 | const { result: _EnctypeTicket, bodyXML } = await getData( 32 | ctx, 33 | encrypt, 34 | 'ComponentVerifyTicket' 35 | ) 36 | 37 | if (_EnctypeTicket) { 38 | EnctypeTicket = _EnctypeTicket 39 | 40 | // todo 抓获setter 41 | DATA.self.Encrypt = EnctypeTicket 42 | writeFile(root, DATA) 43 | 44 | Log(`微信端接收EnctypeTicket:${EnctypeTicket}`) 45 | ctx.response.body = 'success' 46 | } else { 47 | Log(`微信端接收EnctypeTicket异常: ${bodyXML}`) 48 | } 49 | } 50 | ) 51 | 52 | getSelfAccessComponentToken({ appid, root, secret, DATA }) 53 | 54 | refleash({ appid, root, DATA }) 55 | } 56 | 57 | // 获取自身平台的令牌 58 | export function getComponentAccessToken({ 59 | appid, 60 | secret, 61 | enctypeTicket 62 | }: Record = {}): Promise { 63 | const params = { 64 | component_appid: appid, 65 | component_appsecret: secret, 66 | component_verify_ticket: enctypeTicket 67 | } 68 | 69 | console.log(params) 70 | 71 | return new Promise((resolve) => { 72 | // 这个方法怎么不是返回promise? 73 | // todo 改写为request 74 | SuperAgent.post( 75 | `https://api.weixin.qq.com/cgi-bin/component/api_component_token` 76 | ) 77 | .send(params) 78 | .end((err, res) => { 79 | if (!res) { 80 | return 81 | } 82 | 83 | console.log(res.body) 84 | Log(`获取令牌access_token:${res.body.component_access_token}`) 85 | resolve(res.body.component_access_token) 86 | }) 87 | }) 88 | } 89 | 90 | // 获取预授权码 91 | // https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/pre_auth_code.html 92 | export async function getPreCode({ 93 | appid, 94 | access_token 95 | }: Record = {}) { 96 | return new Promise((resolve) => { 97 | const _URL = `https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=${access_token}` 98 | const _Params = { 99 | component_appid: appid 100 | } 101 | return SuperAgent.post(_URL) 102 | .send(_Params) 103 | .end((err, res) => { 104 | if (!res) { 105 | return '' 106 | resolve(null) 107 | } 108 | const code: string = res.body.pre_auth_code 109 | Log(`获取预授权码: ${code}`) 110 | resolve(code) 111 | return code 112 | }) 113 | }) 114 | } 115 | 116 | // 获取账号自身的AccessComponentToken 用于刷新 117 | function getSelfAccessComponentToken({ appid, root, secret, DATA }: any = {}) { 118 | const gapTime = new Date().getTime() - parseInt(DATA.self.update || 0) 119 | const time = 1000 * 60 * 50 120 | 121 | if (gapTime >= time) { 122 | const params = { 123 | component_appid: appid, 124 | component_appsecret: secret, 125 | component_verify_ticket: DATA.self.Encrypt 126 | } 127 | 128 | SuperAgent.post( 129 | `https://api.weixin.qq.com/cgi-bin/component/api_component_token` 130 | ) 131 | .send(params) 132 | .end((err, res) => { 133 | if (res) { 134 | if (res.body.component_access_token) { 135 | Log(`获取access_token:${res.body.component_access_token}`) 136 | DATA.self.component_access_token = res.body.component_access_token 137 | DATA.self.update = new Date().getTime() 138 | writeFile(root, DATA) 139 | } else { 140 | Log('获取失败,请查看异常提示') 141 | console.log(res.body, params) 142 | } 143 | } 144 | }) 145 | } 146 | 147 | // 每一小时请求一次 148 | setTimeout(() => { 149 | getSelfAccessComponentToken({ appid, root, secret, DATA }) 150 | }, 1000 * 60 * 20) 151 | } 152 | 153 | // 刷新机制 154 | // todo 删除 155 | function refleash({ appid, root, DATA }: any = {}) { 156 | DATA.thirdPart.forEach((v: any, index: number) => { 157 | const minTime = new Date().getTime() - parseInt(v.update) 158 | const time = 1000 * 60 * 60 159 | 160 | const params = { 161 | component_appid: appid, 162 | authorizer_appid: v.appid, // 授权方的appid 163 | authorizer_refresh_token: v.refresh_authorizer_refresh_token // 授权方的刷新令牌 164 | } 165 | 166 | if (v.appid && minTime >= time) { 167 | const target = DATA.thirdPart[index] 168 | Log(`刷新${target.name}的accessToken`) 169 | SuperAgent.post( 170 | `https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=${DATA.self.component_access_token}` 171 | ) 172 | .send(params) 173 | .end(async (err, res) => { 174 | if (res.body.authorizer_access_token) { 175 | target.update = new Date().getTime() 176 | target.authorizer_access_token = res.body.authorizer_access_token 177 | target.refresh_authorizer_refresh_token = 178 | res.body.authorizer_refresh_token 179 | writeFile(root, DATA) 180 | } else { 181 | Log(`${target.name}刷新后,没有数据,请查看异常提示`) 182 | console.log(res.body) 183 | } 184 | }) 185 | } 186 | }) 187 | 188 | // 1小时请求一次 189 | setTimeout(() => { 190 | refleash({ appid, root, DATA }) 191 | }, 1000 * 60 * 20) 192 | } 193 | 194 | export default SelfWeChatPlugin 195 | -------------------------------------------------------------------------------- /src/node/plugins/ThirdPartWeChatPlugins.ts: -------------------------------------------------------------------------------- 1 | import SuperAgent from 'superagent' 2 | import _Log from '../../util/Log' 3 | import { Plugin } from '../server' 4 | import { 5 | EnctypeTicket, 6 | getComponentAccessToken, 7 | getPreCode, 8 | } from './SelfWeChatPlugin' 9 | import { writeFile } from '../util' 10 | import { DataType } from '../server' 11 | 12 | const Log = _Log('Message from 第三方:') 13 | 14 | const ThirdPartWeChatPlugin: Plugin = ({ app, appid, secret, Router, root, DATA }) => { 15 | if (app) { 16 | app.use(async (ctx, next) => { 17 | 18 | }) 19 | } 20 | 21 | let ACCESS_TOKEN = '' 22 | 23 | // step1 发送第三方的预授权码 24 | Router.get('/wechat_open_platform/preauthcode', async (ctx: any, res: any) => { 25 | if (!EnctypeTicket) { 26 | Log(`EnctypeTicket(${EnctypeTicket})错误,发送预授权码失败`) 27 | ctx.response.body = 'error' 28 | return 29 | } 30 | 31 | ACCESS_TOKEN = await getComponentAccessToken({ 32 | appid, 33 | secret, 34 | enctypeTicket: EnctypeTicket 35 | }) 36 | const code = await getPreCode({ access_token: ACCESS_TOKEN, appid }) 37 | ctx.response.body = code 38 | }) 39 | 40 | // step2 接收从前端页面跳转发来的authorization_code 41 | Router.get(`/wechat_open_platform/submitac`, async (ctx: any, res: any) => { 42 | 43 | if (!ACCESS_TOKEN) { 44 | Log( 45 | `ACCESS_TOKEN(${ACCESS_TOKEN})令牌为空,需要获取自身平台的令牌,才可以进行授权。` 46 | ) 47 | ctx.response.body = 'error' 48 | return 49 | } 50 | 51 | Authorization(ctx.query.ac as string, ACCESS_TOKEN, appid, root!, DATA!) 52 | ctx.response.body = 'success' 53 | }) 54 | } 55 | 56 | function Authorization( 57 | authorization_code: string, 58 | ACCESS_TOKEN: string, 59 | appid: string, 60 | root: string, 61 | DATA: DataType 62 | ): Promise { 63 | Log(`授权开始,authorization_code: ${authorization_code}`) 64 | 65 | return new Promise((resolve) => { 66 | SuperAgent.post( 67 | `https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=${ACCESS_TOKEN}` 68 | ) 69 | .send({ 70 | component_appid: appid, 71 | authorization_code: authorization_code 72 | }) 73 | .end(async (err, res) => { 74 | if (!res) { 75 | return 76 | } 77 | 78 | if (res.body.hasOwnProperty('errcode')) { 79 | Log(`无效的authorization_code:${ACCESS_TOKEN}`) 80 | Log(`本次授权失败`) 81 | return 82 | } 83 | 84 | const AUTHORIZATION_INFO = res.body.authorization_info 85 | 86 | let authorizer_access_token = AUTHORIZATION_INFO.authorizer_access_token 87 | let refresh_authorizer_refresh_token = 88 | AUTHORIZATION_INFO.authorizer_refresh_token 89 | 90 | Log( 91 | `获取成功!\r\nauthorizer_access_token:${authorizer_access_token}\r\nrefresh_authorizer_refresh_token:${refresh_authorizer_refresh_token}` 92 | ) 93 | 94 | // 获取第三方平台的信息 95 | const platFormInfo: any = await new Promise((resolve) => { 96 | SuperAgent.post( 97 | `https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=${ACCESS_TOKEN}` 98 | ) 99 | .send({ 100 | component_appid: appid, 101 | authorizer_appid: AUTHORIZATION_INFO.authorizer_appid 102 | }) 103 | .then((err) => { 104 | if (!err) { 105 | return 106 | } 107 | resolve(err.body.authorizer_info) 108 | }) 109 | }) 110 | 111 | Log( 112 | `${platFormInfo.nick_name}第三方授权完成,将凭借refresh_authorizer_refresh_token,每一个小时刷新一次authorizer_access_token` 113 | ) 114 | 115 | // 写入&更新第三方平台的信息 116 | setupPlatFormData({ AUTHORIZATION_INFO, authorizer_access_token, refresh_authorizer_refresh_token, platFormInfo, root, DATA }) 117 | 118 | // 返回第三方平台的id 119 | resolve(AUTHORIZATION_INFO.authorizer_appid) 120 | }) 121 | }) 122 | } 123 | 124 | // todo 类型 125 | // 写入或更新第三方平台的信息 126 | function setupPlatFormData({ AUTHORIZATION_INFO, authorizer_access_token, refresh_authorizer_refresh_token, platFormInfo, root, DATA }: any = {}) { 127 | let targetIndex: null | number = null 128 | const thirdPlatForm = { 129 | appid: AUTHORIZATION_INFO.authorizer_appid, 130 | authorizer_access_token, 131 | refresh_authorizer_refresh_token, 132 | update: new Date().getTime(), 133 | create: new Date().getTime(), 134 | qrcode_url: platFormInfo.qrcode_url, 135 | name: platFormInfo.nick_name 136 | } 137 | 138 | for (let i = 0; i < DATA.thirdPart.length; i++) { 139 | const old = DATA.thirdPart[i] 140 | if (old.appid === AUTHORIZATION_INFO.authorizer_appid) { 141 | targetIndex = i 142 | thirdPlatForm.create = old.create 143 | break 144 | } 145 | } 146 | 147 | if (!targetIndex) { 148 | DATA.thirdPart.push(thirdPlatForm) 149 | } else { 150 | DATA.thirdPart[targetIndex] = thirdPlatForm 151 | } 152 | 153 | // todo 数据库保存平台的信息 与刷新token 154 | writeFile(root, DATA) 155 | } 156 | 157 | export default ThirdPartWeChatPlugin 158 | -------------------------------------------------------------------------------- /src/node/root.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare var __TEST__: boolean 7 | declare var __CONFIG__: { 8 | data: string // 数据文件 9 | input: string // 入口文件 10 | } 11 | -------------------------------------------------------------------------------- /src/node/server.ts: -------------------------------------------------------------------------------- 1 | import http, { Server } from 'http' 2 | import Koa from 'koa' 3 | import SelfWeChatPlugin from './plugins/SelfWeChatPlugin' 4 | import ThirdPartWeChatPlugins from './plugins/ThirdPartWeChatPlugins' 5 | import _Router from 'koa-router' 6 | import _BodyParser from 'koa-bodyparser' 7 | import Encrypt from './Encrypt' 8 | import chokidar, { FSWatcher } from 'chokidar' 9 | import ParsePlatFormMessagePlugins from './plugins/ParsePlatFormMessagePlugin'; 10 | 11 | // @ts-ignore 12 | import LRUCache from 'lru-cache' 13 | 14 | export interface UserInfo { 15 | name: string, 16 | picUrl: string, 17 | openid: string, 18 | sex: string, 19 | all: any 20 | } 21 | 22 | export const userInfoCache = new LRUCache({ 23 | max: 65535 24 | }) 25 | 26 | export type Plugin = (ctx: PluginContext) => void 27 | export const Router = new _Router() 28 | 29 | export let ROOT = '' 30 | 31 | export interface DataType { 32 | self: { 33 | Encrypt: string 34 | }, 35 | thirdPart: { 36 | appid: string 37 | authorizer_access_token: string 38 | refresh_authorizer_refresh_token: string 39 | update: number 40 | create: number 41 | qrcode_url: string 42 | name: string 43 | }[] 44 | } 45 | 46 | export interface ServerConfig { 47 | root?: string 48 | plugins?: Plugin[] 49 | appid?: string 50 | secret?: string 51 | encodingAESKey?: string 52 | token?: string 53 | DATA: DataType 54 | input: string 55 | } 56 | 57 | export interface PluginContext { 58 | encrypt: any 59 | appid: string 60 | secret: string 61 | app?: Koa 62 | type: 'koa' 63 | Router: typeof Router 64 | root?: string 65 | watcher: FSWatcher 66 | DATA: DataType 67 | input: string 68 | } 69 | 70 | export const internalPlugins: Plugin[] = [ 71 | SelfWeChatPlugin, 72 | ThirdPartWeChatPlugins, 73 | ParsePlatFormMessagePlugins 74 | ] 75 | 76 | export function createServer({ 77 | root = process.cwd(), 78 | appid = '', 79 | secret = '', 80 | plugins = [], 81 | encodingAESKey, 82 | token, 83 | DATA, 84 | input = 'index.js' 85 | }: ServerConfig): Server { 86 | ROOT = root 87 | const app = new Koa() 88 | const watcher = chokidar.watch(root, { 89 | ignored: [/node_modules/] 90 | }) 91 | 92 | app.use(Router.routes()) 93 | app.use(_BodyParser()) 94 | app.use(require('koa-static')(root)) 95 | 96 | const server = http.createServer(app.callback()) 97 | 98 | // @ts-ignore 99 | const encrypt = new Encrypt({ 100 | appId: appid, 101 | encodingAESKey, 102 | token 103 | }) 104 | 105 | ;[...internalPlugins, ...plugins].forEach((m) => 106 | m({ 107 | encrypt, 108 | appid, 109 | secret, 110 | app, 111 | type: 'koa', 112 | Router, 113 | root, 114 | watcher, 115 | DATA, 116 | input 117 | }) 118 | ) 119 | 120 | return server 121 | } 122 | 123 | process.on('uncaughtException', function(err){ 124 | console.log(err) 125 | }) 126 | -------------------------------------------------------------------------------- /src/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist", 5 | "module": "commonjs", 6 | "lib": ["ESNext"] 7 | }, 8 | "typeRoots": [ 9 | "./root.d.ts" 10 | ], 11 | "include": ["./"], 12 | "exclude": ["./__test__/*"] 13 | } 14 | -------------------------------------------------------------------------------- /src/node/util.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | 3 | const path = require('path') 4 | 5 | const getPostData = (ctx: any): Promise => { 6 | return new Promise(function (resolve, reject) { 7 | try { 8 | let str = '' 9 | ctx.req.on('data', function (data: any) { 10 | str += data; 11 | }) 12 | ctx.req.on('end', function () { 13 | resolve(str) 14 | }) 15 | } catch (e) { 16 | reject(e) 17 | } 18 | }) 19 | } 20 | 21 | const writeFile = (ROOT: string = process.cwd(), data: Record) => { 22 | let dataJSON = JSON.stringify(data) 23 | console.log(ROOT, global.__CONFIG__, '写入') 24 | fs.writeFile( 25 | path.join(ROOT, global.__CONFIG__.data), 26 | dataJSON 27 | ) 28 | } 29 | 30 | const getData = async (ctx: any, encrypt: any, tagName?: string): Promise<{ result: any, bodyXML: any }> => { 31 | const bodyXML: string = await getPostData(ctx) 32 | let result = '' 33 | let match: RegExpExecArray | null = null 34 | if (match = /]*>\<\!\[CDATA\[([\s\S]*?)\]\]\><\/Encrypt>/gm.exec(bodyXML)) { 35 | result = encrypt.decode(match[1]) 36 | if (tagName === 'ComponentVerifyTicket') { 37 | result = // (eval(`/<${tagName}\\b[^>]*>\\<\\!\\[CDATA\\[([\\s\\S]*?)\\]\\]\\><\\/${tagName}>/gm`)).exec(result)![1] 38 | (/]*>\<\!\[CDATA\[([\s\S]*?)\]\]\><\/ComponentVerifyTicket>/gm.exec(result) || [])![1] 39 | } 40 | } 41 | return { 42 | result, 43 | bodyXML 44 | } 45 | } 46 | 47 | export function randomString(len: number): string { 48 | len = len || 32; 49 | let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/ 50 | let maxPos = $chars.length; 51 | let pwd = ''; 52 | for (let i = 0; i < len; i++) { 53 | pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); 54 | } 55 | return pwd; 56 | } 57 | 58 | export { getPostData, writeFile, getData } 59 | -------------------------------------------------------------------------------- /src/util/Log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export default function Log(from: string) { 4 | return function _Log(msg: string) { 5 | console.log(chalk.blue(`\r\n${chalk.red(from)}\r\n${msg}`)) 6 | } 7 | } 8 | 9 | 10 | 11 | export function convertPlugins(_ctx: any, res: any, type: 'express' | 'koa') { 12 | let ctx: any = {} 13 | 14 | if (type === 'express') { 15 | ctx.request = ctx 16 | ctx.response = res 17 | } else { 18 | return _ctx 19 | } 20 | 21 | return ctx 22 | } 23 | 24 | export function WechatLog() { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test.jpg -------------------------------------------------------------------------------- /test/project.spec.ts: -------------------------------------------------------------------------------- 1 | import _axios from 'axios' 2 | 3 | const axios = _axios.create() 4 | 5 | const execa = require('execa') 6 | const path = require('path') 7 | 8 | const projectDir = path.join(__dirname, 'project') 9 | let server 10 | 11 | describe('hmr', () => { 12 | test('图片测试', () => { 13 | const activityFlow = require('../dist/node/Activity/Avatar.js').default 14 | activityFlow({ targetInfo: {}, uid: '123', content: '123', root: process.cwd(), frameName: ['xs1'], dir: 'xuesong' }) 15 | }) 16 | 17 | test('消息hmr测试', async () => { 18 | const port = 3007 19 | server = execa(path.resolve(__dirname, '../bin/run.js'), ['--port', port, '--test'], { 20 | cwd: projectDir 21 | }) 22 | axios.defaults.baseURL = `http://localhost:${port}` 23 | 24 | // 等待服务运行 25 | await new Promise((resolve) => { 26 | server.stdout.on('data', (data) => { 27 | if (data.toString().match('Running')) { 28 | resolve() 29 | } 30 | }) 31 | }) 32 | 33 | // 消息回调测试 34 | await axios.post('/wechat_open_platform/wx0ea308250417bd30/message', ` 35 | 36 | 39 | `, { headers: { 'Content-Type': 'text/xml' } }).then((res) => { 40 | expect(res.data).toEqual('success') 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/project/DATA.json: -------------------------------------------------------------------------------- 1 | {"self":{"Encrypt":"ticket@@@jo-pIncA347Zd5Sk7NmOUWnT3BIGtRmLf2LOKrp9QB9fl8uuPzYd-OESWQd4aW52BPgC7G5S2ZlsnTCFuLieKQ","update":1625916049282},"thirdPart":[{"appid":""},{"appid":"wx0ea308250417bd30","authorizer_access_token":"46_VRh3xzLPKiNGTCm3Db9NGLmxqlZPtnzwF5AKMQhDcZLP02VkU637_xOgYZ9kU6PXvjqubelCqBRqKLazhaPso0hZpcPPfQnBOL0p1KYxBPpU3Jlqj459VOkdrW7sLsFu7br2GvooLssQnSMPHHSiAGDXXI","refresh_authorizer_refresh_token":"refreshtoken@@@aoXF2D5-AonY6zK8ZTZ-vVA4UNyFBY0uJXCPYe-4M8I","update":1625057501899,"create":1624789173830,"qrcode_url":"http://mmbiz.qpic.cn/mmbiz_jpg/Y2LExAS5IQEafY1lqsfvx0DsibSTuSIg8VdU1bHJYs1UMJVg5YsIRyusWlic3QiaicKvwmpQic3JLliaachNqwTfDgibA/0","name":"单行线oneway"},{"appid":"wx436fcd3fdc77dc4f","authorizer_access_token":"46_yiI2MUgFmdSINGGTkDJjY0ExdaDPAaMSDiu3sHOIVaIW1e8lWCNUvBlZq2U36R4hxoUfLAJ5-QRPK44mBQPNUErS5cQloSsmsDh6aMn2X7gk4GYZfcsBZtvZq3vk8QlA-Dqe1iowRgleHezuLMKjAEDYZX","refresh_authorizer_refresh_token":"refreshtoken@@@a9LyquHlIXZBe0abXlPj8qydUvzvkmpLpVRfsEhSjl4","update":1625057501904,"create":1624815270396,"qrcode_url":"http://mmbiz.qpic.cn/mmbiz_jpg/8e89TsEShR5uIlYUYfibBmDWGdlNHuYVvRhdSDVcI9ZYDZiaSW1MyZLwZBrFTQLoMgB9czNb7j2Wz2v5Ht6u568w/0","name":"微读读"},{"appid":"wx85df74b62aad79ed","authorizer_access_token":"46_MqSWuwouHD3AF8QB_W2_qkskvV6Fc45J5VI7Z292QMtapqWbUm7Ts4_d-9AOqI9OP_LWH7ZkOMDdlK6jGNW98CkS36bG9opAvpwo3PBvSvz5piJBBEglbFmV1ANYeU08oay-ik1np02Fp1eqJIViAGDONA","refresh_authorizer_refresh_token":"refreshtoken@@@JFv0RMxhMtWp7rwD4ExJZRD_UDnQV32JvlUa0uAwmQE","update":1625057501886,"create":1624841939866,"qrcode_url":"http://mmbiz.qpic.cn/mmbiz/yolMztZrK8cHGMnvIZPDfClRibj9Og2GRqibc46Gm8bY4kCl4J5FpCmT9joh2yqMLnZklMBYbAU7sUiagleUbQ7iaw/0","name":"雪松控股"},{"appid":"wx7630866bd98a50de","authorizer_access_token":"46_15aFf8C9bga4utjWIa8NYX63IBMZG5enMtkqLjqD8Fzq9iWNQZiiRuw6c25hPIhqHnOs-z6TTaTl15iYRVdGCwvUngmT2lAlJAN2M3o6Q79cOVuF9_YrXHGs124gz1J9s0lwsRU6zlpfdOarMXMdAKDYJY","refresh_authorizer_refresh_token":"refreshtoken@@@1vMm3Gx0ebkeHCRMFxECAx7xLls1-hoXYktSZVq2p5w","update":1625057501961,"create":1624842809037,"qrcode_url":"http://mmbiz.qpic.cn/mmbiz_jpg/N4Yia8kulhG4sibL1PNxicoWKt50mOib2VgvicuZ7EaBQeOyZicr2tVWsllDG7lK7BjZOdEwM4MOLian2yNWUSib9mmkWQ/0","name":"三维家软件"}]} -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/1.png -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/10.png -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/2.png -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/3.png -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/6.png -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/7.png -------------------------------------------------------------------------------- /test/project/assets/avatar/sanwei/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/sanwei/8.png -------------------------------------------------------------------------------- /test/project/assets/avatar/xuesong/xs1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/xuesong/xs1.png -------------------------------------------------------------------------------- /test/project/assets/avatar/xuesong/xs2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/xuesong/xs2.png -------------------------------------------------------------------------------- /test/project/assets/avatar/xuesong/xs3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/xuesong/xs3.png -------------------------------------------------------------------------------- /test/project/assets/avatar/xuesong/xs4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/xuesong/xs4.png -------------------------------------------------------------------------------- /test/project/assets/avatar/zYJ3hD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/test/project/assets/avatar/zYJ3hD.png -------------------------------------------------------------------------------- /test/project/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wechat": { 3 | "appid": "", 4 | "secret": "", 5 | "encodingAESKey": "", 6 | "token": "" 7 | }, 8 | "data": "./DATA.json", 9 | "input": "./index.js" 10 | } 11 | -------------------------------------------------------------------------------- /test/project/index.js: -------------------------------------------------------------------------------- 1 | const avatarPlugins = require('../../dist/node/Activity/Avatar.js').default 2 | 3 | async function test({ target, Content, FromUserName, root }) { 4 | // 图片活动 5 | await avatarPlugins({ targetInfo: target, uid: FromUserName, content: Content, root, frameName: ['xs1'], dir: 'xuesong' }) 6 | } 7 | 8 | module.exports = test 9 | -------------------------------------------------------------------------------- /test/project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "allowJs": false, 9 | "allowSyntheticDefaultImports": true, 10 | "noUnusedLocals": true, 11 | "strictNullChecks": true, 12 | "noImplicitAny": true, 13 | "removeComments": false, 14 | 15 | "baseUrl": "./" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /util/Log.d.ts: -------------------------------------------------------------------------------- 1 | export default function Log(from: string): (msg: string) => void; 2 | export declare function convertPlugins(_ctx: any, res: any, type: 'express' | 'koa'): any; 3 | -------------------------------------------------------------------------------- /util/Log.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | export default function Log(from) { 3 | return function _Log(msg) { 4 | console.log(chalk.blue(`${chalk.red(from)}\r\n${msg}`)); 5 | }; 6 | } 7 | export function convertPlugins(_ctx, res, type) { 8 | let ctx = {}; 9 | if (type === 'express') { 10 | ctx.request = ctx; 11 | ctx.response = res; 12 | } 13 | else { 14 | return _ctx; 15 | } 16 | return ctx; 17 | } 18 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/api/(.*)", "destination": "/api" }] 3 | } 4 | -------------------------------------------------------------------------------- /watch-bob.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kingbultsea/wechat-server-tool/4c782186dd9cfc6f930f0d3770e00b4762ef5f5e/watch-bob.jpg --------------------------------------------------------------------------------