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; + +}