最开始想用chatGTP做微信公众号的自动回复;因为个人号不能做微信认证,只能实现被动回复并不能主动发消息给用户
被动回复必须5秒累做出回复,chatGTP的API国内不能直接访问,只能通过代理访问,所以会超时
1、通过koa搭建一个本地服务
- 初始化项目
npm init test -y
- 项目依赖
npm i koa crypto koa-logger koa-router xml2js superagent koa-bodyparser dayjs
- 目录结构
- 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 [端口]
3、配置微信公众号
有问题评论互相学习交流