feat: 微信信息验证 & echo功能

This commit is contained in:
2025-10-31 05:04:38 +00:00
parent ef07c35825
commit d8074ea6d9
7 changed files with 420 additions and 15 deletions

147
package-lock.json generated
View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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 = `<xml>
<ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[${replyContent}]]></Content>
</xml>`;
const encryptedReply = cryptoUtil.encrypt(reply);
// 再次签名
const replyArr = [token, timestamp, nonce, encryptedReply].sort();
const replySig = crypto.createHash('sha1').update(replyArr.join('')).digest('hex');
const responseXml = `<xml>
<Encrypt><![CDATA[${encryptedReply}]]></Encrypt>
<MsgSignature><![CDATA[${replySig}]]></MsgSignature>
<TimeStamp>${timestamp}</TimeStamp>
<Nonce><![CDATA[${nonce}]]></Nonce>
</xml>`;
res.set('Content-Type', 'application/xml');
return res.send(responseXml);
})
},
});

View File

@ -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;
/** 消息id64位整型 */
MsgId: string;
/** 消息数据id,消息来自文章时存在 */
MsgDataId?: string;
/** 多图文时的文章索引从1开始消息来自文章时存在 */
Idx?: string;
}
export type WechatMessage = WechatTextMessage;

17
src/utils/verification.ts Normal file
View File

@ -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
}

59
src/utils/wechatCrypto.ts Normal file
View File

@ -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');
}
}

103
src/utils/xml-parser.ts Normal file
View File

@ -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<WechatEncryptMessage> {
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<string, unknown>;
// 将可能存在的 __cdata 提取
const normalized: Record<string, string> = {};
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<WechatMessage> {
const parser = new XMLParser({
ignoreAttributes: false,
cdataPropName: '__cdata',
});
const result = parser.parse(raw.toString());
const xml = result.xml as Record<string, unknown>;
console.log("Parsed XML:", xml);
// 将可能存在的 __cdata 提取
const normalized: Record<string, string> = {};
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<WechatMessage> {
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<string, unknown>;
console.log("Parsed XML:", xml);
// 将可能存在的 __cdata 提取
const normalized: Record<string, string> = {};
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;
}