通过内网穿透配置微信公众号被动回复

312 阅读1分钟

最开始想用chatGTP做微信公众号的自动回复;因为个人号不能做微信认证,只能实现被动回复并不能主动发消息给用户

被动回复必须5秒累做出回复,chatGTP的API国内不能直接访问,只能通过代理访问,所以会超时

1、通过koa搭建一个本地服务

  • 初始化项目

npm init test -y

  • 项目依赖

npm i koa crypto koa-logger koa-router xml2js superagent koa-bodyparser dayjs

  • 目录结构

3.png

  • index.js
const koa = require("koa2");
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser');
var app = new koa();
const routers = require('./routes')

//设置允许跨域访问该服务.
app.use(async (ctx, next)=> {
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    if (ctx.method == 'OPTIONS') {
        ctx.body = 200; 
    } else {
        await next();
    }
});
// 公众号请求的参数是xml 这里解析
app.use(bodyParser({
    enableTypes: ['text', 'xml'],
}))


app.use(logger())

// logger
app.use(async (ctx, next) => {
    const start = new Date()
    await next()
    const ms = new Date() - start
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// routes
app.use(routers.routes()).use(routers.allowedMethods())

//监听3000端口
app.listen(80);

  • routes

    • index.js
      // 动态引入router
      const fs = require('fs')
    
      const Router = require('koa-router')
      const router = new Router()
    
      fs.readdirSync(__dirname).forEach(file => {
        if(file !== 'index.js'){
          let r = require('./' + file)
          router.use(r.routes())
        }
      })
    
      module.exports = router
    
    • wx.js
      // 这里可以封装成中间件 这里就省略了
      const router = require("koa-router")();
      const xml2js = require("xml2js");
      const dayjs = require("dayjs");
      const config = require("../config");
      const WXMsgCrypto = require("../utils/WXMsgCrypto");
      const chatGtp = require("../utils/chatGtp");
      const auth = require("../utils/auth");
      // 这里是验证
      router.get("/", function (ctx, next) {
        const res = auth(ctx.request.query);
        ctx.body = res;
      });
    
      // 这里是用户给微信公众号发送消息 参数需要解密 解密后是xml
      let inow = false
      const wx = new WXMsgCrypto(config.token, config.EncodingAESKey, config.appid);
      router.post("/", async function (ctx, next) {
        console.log("来了来了");
        const data = ctx.request.body;
        const parser = new xml2js.Parser({ explicitArray: false, ignoreAttrs: true });
        const jsonData = await parser.parseStringPromise(data);
        const xml = await wx.decrypt(jsonData.xml.Encrypt);
        const msg = await parser.parseStringPromise(xml.message);
        console.log(msg, "-----", xml, JSON.stringify(msg));
        // 这里是链接chatGTP 问点比较简单的可以回复 复杂点会超过五秒断开链接
        // 如果是企业认证这里可以直接默认回复,然后等chatGTP返回成功后在通过公众号客服功能回复
        try {
          if(inow) return
          inow = true
          const res = await chatGtp(msg.xml.Content);
          // 回复也是xml格式
          let xml = `<xml>
          <ToUserName><![CDATA[oO-x10Xky1zv7bJeqLAN2OeawgqA]]></ToUserName>
          <FromUserName><![CDATA[gh_d32ad421b149]]></FromUserName>
          <CreateTime>${dayjs().unix()}</CreateTime>
          <MsgType><![CDATA[text]]></MsgType>
          <Content><![CDATA[${res[0].message.content}]]></Content>
        </xml>`;
          inow = false
          ctx.body = xml;
        } catch {
          inow = false
          console.log("失败");
        }
      });
    
      module.exports = router;
    
    
  • config index.js

module.exports = {
   token: `这里的值对应服务器对应的token`,
   appid: `公众号ID`,
   EncodingAESKey: `服务器配置的EncodingAESKey`
}
  • utils

    • auth.js
      const crypto = require("crypto");
      const config = require('../config')
    
      const auth = (req) => {
        //1.获取微信服务器Get请求的参数 signature、timestamp、nonce、echostr
        var signature = req.signature, //微信加密签名
          timestamp = req.timestamp, //时间戳
          nonce = req.nonce, //随机数
          echostr = req.echostr; //随机字符串
    
        //2.将token、timestamp、nonce三个参数进行字典序排序
        var array = [config.token, timestamp, nonce];
        array.sort();
    
        //3.将三个参数字符串拼接成一个字符串进行sha1加密
        var tempStr = array.join("");
        const hashCode = crypto.createHash("sha1"); //创建加密类型
        var resultCode = hashCode.update(tempStr, "utf8").digest("hex"); //对传入的字符串进行加密
    
        //4.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
        return resultCode === signature ? echostr : "mismatch";
      };
    
      module.exports = auth;
    
    • chatGtp.js
      const superagent = require("superagent");
    
      const send = (content) => {
        console.log(content, 'content')
        // https://www.openai-proxy.com/
        return new Promise((resolve, reject) => {
          superagent
            .post("https://api.openai-proxy.com/v1/chat/completions")
            .set(
              "Authorization",
              "Bearer 这里是自己openai key"
            )
            .send({
              model: "gpt-3.5-turbo",
              messages: [{ role: "user", content }],
            })
            .then(res => {
              const data = JSON.parse(res.text);
              console.log(' 返回了', data.choices)
              resolve(data.choices)   
            }).catch(err => {
              reject(err)
            })
        })
      };
      // send('德玛西亚').then(res => console.log('成功', res)).catch(err => console.log('失败'))
      module.exports = send;
    
    • WXMsgCrypto.js
      var crypto = require('crypto')
      class PKCS7 {
        /**
         * 删除补位
         * @param {String} text 解密后的明文
         */
        decode(text) {
          let pad = text[text.length - 1]
          if (pad < 1 || pad > 32) {
            pad = 0
          }
          return text.slice(0, text.length - pad)
        }
        /**
         * 填充补位
         * @param {String} text 需要进行填充补位的明文
         */
        encode(text) {
          const blockSize = 32
          const textLength = text.length
          // 计算需要填充的位数
          const amountToPad = blockSize - (textLength % blockSize)
          const result = Buffer.alloc(amountToPad)
          result.fill(amountToPad)
          return Buffer.concat([text, result])
        }
      }
    
      /**
       * 微信公众号消息加解密
       * 官方文档(写的非常之烂):https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Message_Encryption/Technical_Plan.html
       */
      class WXMsgCrypto {
        /**
         * 以下信息在公众号 - 开发 - 基本配置
         * @param {String} token          令牌(Token)
         * @param {String} encodingAESKey 消息加解密密钥
         * @param {String} appId          公众号的AppId
         */
        constructor(token, encodingAESKey, appId) {
          if (!token || !encodingAESKey || !appId) {
            throw new Error('please check arguments')
          }
          this.token = token
          this.appId = appId
    
          let AESKey = Buffer.from(encodingAESKey + '=', 'base64')
          if (AESKey.length !== 32) {
            throw new Error('encodingAESKey invalid')
          }
          this.key = AESKey
          this.iv = AESKey.slice(0, 16)
          this.pkcs7 = new PKCS7()
        }
        /**
         * 获取签名
         * @param {String} timestamp    时间戳
         * @param {String} nonce        随机数
         * @param {String} encrypt      加密后的文本
         */
        getSignature(timestamp, nonce, encrypt) {
          const sha = crypto.createHash('sha1')
          const arr = [this.token, timestamp, nonce, encrypt].sort()
          sha.update(arr.join(''))
          return sha.digest('hex')
        }
        /**
         * 对密文进行解密
         * @param {String} text    待解密的密文
         */
        decrypt(text) {
          // 创建解密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节
          const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv)
          decipher.setAutoPadding(false)
    
          let deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()])
    
          deciphered = this.pkcs7.decode(deciphered)
          // 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID]
          // 去除16位随机数
          const content = deciphered.slice(16)
          const length = content.slice(0, 4).readUInt32BE(0)
    
          return {
            message: content.slice(4, length + 4).toString(),
            appId: content.slice(length + 4).toString()
          }
        }
        /**
         * 对明文进行加密
         * 算法:Base64_Encode(AES_Encrypt[random(16B) + msg_len(4B) + msg + $appId])
         * @param {String} text    待加密明文文本
         */
        encrypt(text) {
          // 16B 随机字符串
          const randomString = crypto.pseudoRandomBytes(16)
    
          const msg = Buffer.from(text)
          // 获取4B的内容长度的网络字节序
          const msgLength = Buffer.alloc(4)
          msgLength.writeUInt32BE(msg.length, 0)
    
          const id = Buffer.from(this.appId)
    
          const bufMsg = Buffer.concat([randomString, msgLength, msg, id])
    
          // 对明文进行补位操作
          const encoded = this.pkcs7.encode(bufMsg)
    
          // 创建加密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节
          const cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv)
          cipher.setAutoPadding(false)
    
          const cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()])
    
          return cipheredMsg.toString('base64')
        }
      }
    
      module.exports = WXMsgCrypto
    
    
  • 启动服务

在packge.json配置或者在node ./src/index.js

2、通过cpolar实现内网穿透 cpolar http [端口]

1.png

3、配置微信公众号

2.png

有问题评论互相学习交流