From d8074ea6d9f423969610ec8159604329ca9a09ea Mon Sep 17 00:00:00 2001
From: R2m1liA <15258427350@163.com>
Date: Fri, 31 Oct 2025 05:04:38 +0000
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BE=AE=E4=BF=A1=E4=BF=A1=E6=81=AF?=
=?UTF-8?q?=E9=AA=8C=E8=AF=81=20&=20echo=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package-lock.json | 147 ++++++++++++++++++++++++++++++++----
package.json | 4 +
src/index.ts | 75 +++++++++++++++++-
src/types/wechat-message.ts | 30 ++++++++
src/utils/verification.ts | 17 +++++
src/utils/wechatCrypto.ts | 59 +++++++++++++++
src/utils/xml-parser.ts | 103 +++++++++++++++++++++++++
7 files changed, 420 insertions(+), 15 deletions(-)
create mode 100644 src/types/wechat-message.ts
create mode 100644 src/utils/verification.ts
create mode 100644 src/utils/wechatCrypto.ts
create mode 100644 src/utils/xml-parser.ts
diff --git a/package-lock.json b/package-lock.json
index 55f716b..0faf13b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,10 @@
"": {
"name": "directus-extension-wechat-service",
"version": "1.0.0",
+ "dependencies": {
+ "fast-xml-parser": "^5.3.0",
+ "raw-body": "^3.0.1"
+ },
"devDependencies": {
"@directus/extensions-sdk": "16.0.2",
"@types/node": "^24.9.1",
@@ -1787,7 +1791,6 @@
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1872,6 +1875,7 @@
"integrity": "sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@unhead/schema": "1.11.20",
"@unhead/shared": "1.11.20"
@@ -1886,6 +1890,7 @@
"integrity": "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"hookable": "^5.5.3",
"zhead": "^2.2.4"
@@ -1900,6 +1905,7 @@
"integrity": "sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@unhead/schema": "1.11.20",
"packrup": "^0.1.2"
@@ -2004,7 +2010,8 @@
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@vue/reactivity": {
"version": "3.5.18",
@@ -2182,7 +2189,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -2204,6 +2210,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -2691,6 +2706,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -2865,7 +2889,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -2962,6 +2985,24 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "node_modules/fast-xml-parser": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.0.tgz",
+ "integrity": "sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^2.1.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -3266,7 +3307,24 @@
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
},
"node_modules/human-signals": {
"version": "8.0.1",
@@ -3282,7 +3340,6 @@
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -3335,6 +3392,12 @@
"node": ">=4"
}
},
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
"node_modules/inquirer": {
"version": "12.9.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.0.tgz",
@@ -3546,7 +3609,6 @@
"integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"colorette": "2.0.19",
"commander": "^10.0.0",
@@ -3919,6 +3981,7 @@
"integrity": "sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
@@ -4035,6 +4098,7 @@
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
@@ -4059,6 +4123,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
@@ -4099,7 +4164,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -4701,6 +4765,21 @@
"safe-buffer": "^5.1.0"
}
},
+ "node_modules/raw-body": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.7.0",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -4788,7 +4867,6 @@
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -4946,7 +5024,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
"license": "MIT"
},
"node_modules/sax": {
@@ -4979,6 +5056,12 @@
"randombytes": "^2.1.0"
}
},
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -5066,6 +5149,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/stdin-discarder": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
@@ -5126,6 +5218,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strnum": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
+ "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/stylehacks": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz",
@@ -5255,6 +5359,15 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -5281,7 +5394,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5303,6 +5415,7 @@
"integrity": "sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@unhead/dom": "1.11.20",
"@unhead/schema": "1.11.20",
@@ -5336,6 +5449,15 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/unplugin-utils": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz",
@@ -5397,7 +5519,6 @@
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -5473,7 +5594,6 @@
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.18",
"@vue/compiler-sfc": "3.5.18",
@@ -5598,6 +5718,7 @@
"integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
diff --git a/package.json b/package.json
index 4707b03..fe6591c 100644
--- a/package.json
+++ b/package.json
@@ -28,5 +28,9 @@
"@directus/extensions-sdk": "16.0.2",
"@types/node": "^24.9.1",
"typescript": "^5.9.3"
+ },
+ "dependencies": {
+ "fast-xml-parser": "^5.3.0",
+ "raw-body": "^3.0.1"
}
}
diff --git a/src/index.ts b/src/index.ts
index 5109dd7..e8340da 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,79 @@
import { defineEndpoint } from "@directus/extensions-sdk";
+import { parseStringXML, parseWechatEncrypt, parseWechatMessage } from "./utils/xml-parser";
+import { verifyEncrypt, verifySignature } from "./utils/verification";
+import crypto from 'crypto'
+import { WechatCrypto } from "./utils/wechatCrypto";
export default defineEndpoint({
id: "wechat-service",
- handler: (router) => {
- router.get("/", (_req, res) => res.send("Hello, Wechat!"));
+ handler: (router, { env }) => {
+ router.get('/', async (req, res) => {
+ const { signature, timestamp, nonce, echostr } = req.query;
+
+ const token = env.WECHAT_TOKEN;
+
+ if (verifySignature(token as string, signature as string, timestamp as string, nonce as string)) {
+ return res.send(echostr);
+ }
+ return res.status(403).send('Invalid signature');
+ });
+
+ router.post("/echo", async (req, res) => {
+ const jsonData = await parseWechatMessage(req);
+ return res.json({ received: jsonData });
+ });
+
+ router.post("/", async (req, res) => {
+ // 验证Encrypt签名
+ const { timestamp, nonce, msg_signature } = req.query;
+ const token = env.WECHAT_TOKEN;
+
+ const encryptData = await parseWechatEncrypt(req);
+ const encrypt = encryptData.Encrypt;
+
+ if (!verifyEncrypt(token as string, encrypt as string, timestamp as string, nonce as string, msg_signature as string)) {
+ console.error("Invalid Signature");
+ return res.status(403).send('Invalid Signature');
+ }
+
+ // 处理加密信息体
+ const encodingAESKey = env.WECHAT_AESKEY;
+ const appId = env.WECHAT_APPID;
+ const cryptoUtil = new WechatCrypto(encodingAESKey, appId);
+ const decryptXml = cryptoUtil.decrypt(encrypt);
+ const msg = await parseStringXML(decryptXml);
+ let replyContent;
+ if (msg.MsgType !== 'text') {
+ replyContent = "暂不支持该消息类型";
+ } else {
+ replyContent = `已收到你的加密消息:${msg.Content}`;
+
+ }
+
+ // 回复消息(示例:文本)
+ const reply = `
+
+
+ ${Date.now()}
+
+
+ `;
+
+ const encryptedReply = cryptoUtil.encrypt(reply);
+
+ // 再次签名
+ const replyArr = [token, timestamp, nonce, encryptedReply].sort();
+ const replySig = crypto.createHash('sha1').update(replyArr.join('')).digest('hex');
+
+ const responseXml = `
+
+
+ ${timestamp}
+
+ `;
+
+ res.set('Content-Type', 'application/xml');
+ return res.send(responseXml);
+ })
},
});
diff --git a/src/types/wechat-message.ts b/src/types/wechat-message.ts
new file mode 100644
index 0000000..4d7fba8
--- /dev/null
+++ b/src/types/wechat-message.ts
@@ -0,0 +1,30 @@
+export interface WechatEncryptMessage {
+ ToUserName: string;
+ Encrypt: string;
+}
+
+export interface WechatBaseMessage {
+ /** 接收方微信号 */
+ ToUserName: string;
+ /** 发送方微信号 */
+ FromUserName: string;
+ /** 消息创建时间 */
+ CreateTime: number;
+ /** 消息类型 */
+ MsgType: string;
+}
+
+export interface WechatTextMessage extends WechatBaseMessage {
+ /** 文本消息类型 */
+ MsgType: "text";
+ /** 文本消息内容 */
+ Content: string;
+ /** 消息id,64位整型 */
+ MsgId: string;
+ /** 消息数据id,消息来自文章时存在 */
+ MsgDataId?: string;
+ /** 多图文时的文章索引,从1开始,消息来自文章时存在 */
+ Idx?: string;
+}
+
+export type WechatMessage = WechatTextMessage;
\ No newline at end of file
diff --git a/src/utils/verification.ts b/src/utils/verification.ts
new file mode 100644
index 0000000..4a05806
--- /dev/null
+++ b/src/utils/verification.ts
@@ -0,0 +1,17 @@
+import crypto from 'crypto';
+
+export function verifySignature(token: string, signature: string, timestamp: string, nonce: string): boolean {
+ const tempArray = [token, timestamp, nonce].sort();
+ const str = tempArray.join('');
+ const sha1 = crypto.createHash('sha1');
+ const hash = sha1.update(str).digest('hex');
+ return hash === signature
+}
+
+export function verifyEncrypt(token: string, encrypt: string, timestamp: string, nonce: string, msg_signature: string): boolean {
+ const tempArray = [token, timestamp, nonce, encrypt].sort();
+ const str = tempArray.join('');
+ const sha1 = crypto.createHash('sha1');
+ const hash = sha1.update(str).digest('hex');
+ return hash === msg_signature
+}
\ No newline at end of file
diff --git a/src/utils/wechatCrypto.ts b/src/utils/wechatCrypto.ts
new file mode 100644
index 0000000..6fd9b6f
--- /dev/null
+++ b/src/utils/wechatCrypto.ts
@@ -0,0 +1,59 @@
+import crypto from 'crypto';
+
+/**
+ * 微信AES加解密工具
+ */
+export class WechatCrypto {
+ AESKey: Buffer;
+ iv: Buffer;
+ AppId: string;
+
+ constructor(encodingAESKey: string, appId: string) {
+ this.AESKey = Buffer.from(encodingAESKey + '=', 'base64');
+ this.iv = this.AESKey.subarray(0, 16);
+ this.AppId = appId;
+ }
+
+ /** 信息体解密 */
+ decrypt(encrypt: string) {
+ const TmpMsg = Buffer.from(encrypt, 'base64');
+ const decipher = crypto.createDecipheriv('aes-256-cbc', this.AESKey, this.iv);
+ decipher.setAutoPadding(false);
+ const decrypted = Buffer.concat([
+ decipher.update(TmpMsg),
+ decipher.final(),
+ ]);
+
+ let pad = decrypted[decrypted.length - 1]!;
+ if (pad < 1 || pad > 32) pad = 0;
+ const content = decrypted.subarray(16, decrypted.length - pad);
+ const msgLength = content.readUInt32BE(0);
+ const msg = content.subarray(4, 4 + msgLength).toString('utf-8');
+ const appId = content.subarray(4 + msgLength).toString('utf-8');
+
+ if (appId !== this.AppId) {
+ throw new Error('AppID mismatch');
+ }
+
+ return msg;
+ }
+
+ /** 信息体加密 */
+ encrypt(replyMsg: string) {
+ const random16 = crypto.randomBytes(16);
+ const msg = Buffer.from(replyMsg);
+ const msgLength = Buffer.alloc(4);
+ msgLength.writeUInt32BE(msg.length, 0);
+ const appIdBuffer = Buffer.from(this.AppId);
+
+ const raw = Buffer.concat([random16, msgLength, msg, appIdBuffer]);
+ const padLen = 32 - (raw.length % 32);
+ const pad = Buffer.alloc(padLen, padLen);
+ const content = Buffer.concat([raw, pad]);
+
+ const cipher = crypto.createCipheriv('aes-256-cbc', this.AESKey, this.iv);
+ cipher.setAutoPadding(false);
+ const encrypted = Buffer.concat([cipher.update(content), cipher.final()]);
+ return encrypted.toString('base64');
+ }
+}
\ No newline at end of file
diff --git a/src/utils/xml-parser.ts b/src/utils/xml-parser.ts
new file mode 100644
index 0000000..d3085f0
--- /dev/null
+++ b/src/utils/xml-parser.ts
@@ -0,0 +1,103 @@
+import { XMLParser } from "fast-xml-parser";
+import getRawBody from "raw-body";
+import type { Request, Response } from "express";
+import { WechatEncryptMessage, WechatMessage } from "../types/wechat-message";
+
+export async function parseWechatEncrypt(req: Request): Promise {
+ const raw = await getRawBody(req, { encoding: "utf-8" });
+
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ cdataPropName: '__cdata'
+ })
+ const result = parser.parse(raw.toString());
+
+ const xml = result.xml as Record;
+
+ // 将可能存在的 __cdata 提取
+ const normalized: Record = {};
+ for (const key in xml) {
+ const value = xml[key];
+ if (typeof value === 'object' && value && '__cdata' in value) {
+ normalized[key] = (value as { __cdata: string }).__cdata;
+ } else {
+ normalized[key] = value as string;
+ }
+ }
+
+ return {
+ ToUserName: normalized.ToUserName,
+ ...normalized,
+ } as WechatEncryptMessage;
+}
+
+export async function parseStringXML(raw: string): Promise {
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ cdataPropName: '__cdata',
+ });
+ const result = parser.parse(raw.toString());
+
+ const xml = result.xml as Record;
+
+ console.log("Parsed XML:", xml);
+
+ // 将可能存在的 __cdata 提取
+ const normalized: Record = {};
+ for (const key in xml) {
+ const value = xml[key];
+ if (typeof value === 'object' && value && '__cdata' in value) {
+ normalized[key] = (value as { __cdata: string }).__cdata;
+ } else {
+ normalized[key] = value as string;
+ }
+ }
+
+ console.log(normalized.CreateTime)
+
+ return {
+ ToUserName: normalized.ToUserName,
+ FromUserName: normalized.FromUserName,
+ CreateTime: Number(normalized.CreateTime),
+ MsgType: normalized.MsgType,
+ ...normalized,
+ } as WechatMessage;
+}
+
+export async function parseWechatMessage(
+ req: Request,
+): Promise {
+ const raw = await getRawBody(req, { encoding: "utf-8" });
+
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ cdataPropName: '__cdata',
+ });
+ const result = parser.parse(raw.toString());
+
+ const xml = result.xml as Record;
+
+ console.log("Parsed XML:", xml);
+
+ // 将可能存在的 __cdata 提取
+ const normalized: Record = {};
+ for (const key in xml) {
+ const value = xml[key];
+ if (typeof value === 'object' && value && '__cdata' in value) {
+ normalized[key] = (value as { __cdata: string }).__cdata;
+ } else {
+ normalized[key] = value as string;
+ }
+ }
+
+ console.log(normalized.CreateTime)
+
+ return {
+ ToUserName: normalized.ToUserName,
+ FromUserName: normalized.FromUserName,
+ CreateTime: Number(normalized.CreateTime),
+ MsgType: normalized.MsgType,
+ ...normalized,
+ } as WechatMessage;
+
+}