From 545d1e687f0870aaf66c207c7c65904c9496e96f Mon Sep 17 00:00:00 2001 From: R2m1liA <15258427350@163.com> Date: Fri, 26 Dec 2025 04:55:51 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 当前不处理任何事件推送 --- src/index.ts | 8 ++ src/utils/assertUtils.ts | 21 +++ src/utils/wechatXmlParser.ts | 253 +++++++++++++++++++++-------------- 3 files changed, 184 insertions(+), 98 deletions(-) diff --git a/src/index.ts b/src/index.ts index ac94436..484bd21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,14 @@ export default defineEndpoint({ let replyContent; let reply; + if(MsgType === 'event') { + // 不处理事件推送,回复空内容以避免重试 + console.warn("Received event push, no reply sent."); + res.set('Content-Type', 'plain/text'); + res.send(''); + return; + } + if (MsgType !== 'text') { replyContent = "暂不支持该消息类型"; } else { diff --git a/src/utils/assertUtils.ts b/src/utils/assertUtils.ts index ea7fa96..50d63b7 100644 --- a/src/utils/assertUtils.ts +++ b/src/utils/assertUtils.ts @@ -30,4 +30,25 @@ export function assertWechatReceivedMessageType(value: unknown, fieldName: strin if (typeof value !== 'string' || !WECHAT_RECEIVED_MESSAGE_TYPES.includes(value as WechatReceivedMessageType)) { throw new Error(`Expected ${fieldName} to be one of ${WECHAT_RECEIVED_MESSAGE_TYPES.join(", ")}, but got ${typeof value === 'string' ? value : typeof value}`); } +} + +const WECHAT_RECEIVED_EVENT_TYPES = [ + "subscribe", + "SCAN", + "LOCATION", + "CLICK", + "VIEW" +] as const; + +export type WechatReceivedEventType = typeof WECHAT_RECEIVED_EVENT_TYPES[number]; + +/** 断言值为微信接收推送类型字面量 + * + * @param value 原始值 + * @param fieldName 字段名称 + */ +export function assertWechatReceivedEventType(value: unknown, fieldName: string): asserts value is WechatReceivedEventType { + if (typeof value !== 'string' || !WECHAT_RECEIVED_EVENT_TYPES.includes(value as WechatReceivedEventType)) { + throw new Error(`Expected ${fieldName} to be one of ${WECHAT_RECEIVED_EVENT_TYPES.join(", ")}, but got ${typeof value === 'string' ? value : typeof value}`); + } } \ No newline at end of file diff --git a/src/utils/wechatXmlParser.ts b/src/utils/wechatXmlParser.ts index 093a554..a3113f8 100644 --- a/src/utils/wechatXmlParser.ts +++ b/src/utils/wechatXmlParser.ts @@ -1,10 +1,12 @@ import { XMLParser } from "fast-xml-parser"; import getRawBody from "raw-body"; import type { Request } from "express"; -import type { WechatReceivedBaseMessage, WechatReceivedImageMessage, WechatReceivedLinkMessage, WechatReceivedLocationMessage, WechatReceivedMessage, WechatReceivedTextMessage, WechatReceivedVideoMessage, WechatReceivedVoiceMessage } from "../types/received-message"; +import type { WechatReceivedBaseMessage, WechatReceivedImageMessage, WechatReceivedLinkMessage, WechatReceivedLocationMessage, WechatReceivedMessage, WechatReceivedTextMessage, WechatReceivedVideoMessage, WechatReceivedVoiceMessage } from "../types/received-message"; import type { WechatEncryptMessage } from "../types/encrypted-message"; import { WechatXmlObject, NormalizedWechatXml } from "../types/wechat-xml"; -import { assertString, assertWechatReceivedMessageType } from "./assertUtils"; +import { assertString, assertWechatReceivedEventType, assertWechatReceivedMessageType } from "./assertUtils"; +import { WechatReceivedEvent } from "../types/received-event"; +import { assert } from "console"; /** * 从原始XML字符串解析为微信XML对象 @@ -197,114 +199,169 @@ export async function parseWechatEncryptMessage(req: Request): Promise { - // const parser = new XMLParser({ - // ignoreAttributes: false, - // parseTagValue: false, - // cdataPropName: '__cdata', - // trimValues: true, - // }); - // const result = parser.parse(raw); - - // const xml = result.xml as WechatXmlObject; - +export async function parseDecryptedXML(raw: string): Promise { const xml = parseRawXml(raw); // 将可能存在的 __cdata 提取 const normalized = normalizeWechatXml(xml); const msgType = normalized.MsgType; - assertString(msgType, 'MsgType'); - assertWechatReceivedMessageType(msgType, 'MsgType'); + let eventType: string | undefined; // 基础字段类型断言 assertString(normalized.ToUserName, 'ToUserName'); assertString(normalized.FromUserName, 'FromUserName'); assertString(normalized.CreateTime, 'CreateTime'); - assertString(normalized.MsgId, 'MsgId'); + assertString(msgType, 'MsgType'); - const baseFields: Omit = { - ToUserName: normalized.ToUserName, - FromUserName: normalized.FromUserName, - CreateTime: Number(normalized.CreateTime), - MsgId: normalized.MsgId, - MsgDataId: normalized.MsgDataId, - Idx: normalized.Idx, - }; + if (msgType === 'event') { + assertString(normalized.Event, 'Event'); + eventType = normalized.Event; + assertWechatReceivedEventType(eventType, 'Event'); - // 根据 MsgType 进行具体消息类型的构建 - switch (msgType) { - case "text": - assertString(normalized.Content, 'Content'); - return { - ...baseFields, - MsgType: "text", - Content: normalized.Content, - } satisfies WechatReceivedTextMessage; - case "image": - assertString(normalized.PicUrl, 'PicUrl'); - assertString(normalized.MediaId, 'MediaId'); - return { - ...baseFields, - MsgType: "image", - PicUrl: normalized.PicUrl, - MediaId: normalized.MediaId, - } satisfies WechatReceivedImageMessage; - case "voice": - assertString(normalized.MediaId, 'MediaId'); - assertString(normalized.Format, 'Format'); - assertString(normalized.MediaId16K, 'MediaId16K'); - return { - ...baseFields, - MsgType: "voice", - MediaId: normalized.MediaId, - Format: normalized.Format, - MediaId16K: normalized.MediaId16K, - } satisfies WechatReceivedVoiceMessage; - case "video": - assertString(normalized.MediaId, 'MediaId'); - assertString(normalized.ThumbMediaId, 'ThumbMediaId'); - return { - ...baseFields, - MsgType: "video", - MediaId: normalized.MediaId, - ThumbMediaId: normalized.ThumbMediaId, - } satisfies WechatReceivedVideoMessage; - case "shortvideo": - assertString(normalized.MediaId, 'MediaId'); - assertString(normalized.ThumbMediaId, 'ThumbMediaId'); - return { - ...baseFields, - MsgType: "shortvideo", - MediaId: normalized.MediaId, - ThumbMediaId: normalized.ThumbMediaId, - } satisfies WechatReceivedVideoMessage; - case "location": - assertString(normalized.Location_X, 'Location_X'); - assertString(normalized.Location_Y, 'Location_Y'); - assertString(normalized.Scale, 'Scale'); - assertString(normalized.Label, 'Label'); - return { - ...baseFields, - MsgType: "location", - Location_X: normalized.Location_X, - Location_Y: normalized.Location_Y, - Scale: normalized.Scale, - Label: normalized.Label, - } satisfies WechatReceivedLocationMessage; - case "link": - assertString(normalized.Title, 'Title'); - assertString(normalized.Description, 'Description'); - assertString(normalized.Url, 'Url'); - return { - ...baseFields, - MsgType: "link", - Title: normalized.Title, - Description: normalized.Description, - Url: normalized.Url, - } satisfies WechatReceivedLinkMessage; - default: - const _exhaustiveCheck: never = msgType; - throw new Error(`Unsupported MsgType: ${_exhaustiveCheck}`); + const baseFields: Omit = { + ToUserName: normalized.ToUserName, + FromUserName: normalized.FromUserName, + CreateTime: Number(normalized.CreateTime), + }; + + // 根据 Event 进行具体事件类型的构建 + switch (eventType) { + case "subscribe": + return { + ...baseFields, + MsgType: "event", + Event: "subscribe", + EventKey: normalized.EventKey, + Ticket: normalized.Ticket, + } satisfies WechatReceivedEvent; + case "SCAN": + return { + ...baseFields, + MsgType: "event", + Event: "SCAN", + EventKey: normalized.EventKey ?? '', + Ticket: normalized.Ticket ?? '', + } satisfies WechatReceivedEvent; + case "LOCATION": + assertString(normalized.Latitude, 'Latitude'); + assertString(normalized.Longitude, 'Longitude'); + assertString(normalized.Precision, 'Precision'); + return { + ...baseFields, + MsgType: "event", + Event: "LOCATION", + Latitude: normalized.Latitude, + Longitude: normalized.Longitude, + Precision: normalized.Precision, + } satisfies WechatReceivedEvent; + case "CLICK": + assertString(normalized.EventKey, 'EventKey'); + return { + ...baseFields, + MsgType: "event", + Event: "CLICK", + EventKey: normalized.EventKey, + } satisfies WechatReceivedEvent; + case "VIEW": + assertString(normalized.EventKey, 'EventKey'); + return { + ...baseFields, + MsgType: "event", + Event: "VIEW", + EventKey: normalized.EventKey, + } satisfies WechatReceivedEvent; + default: + const _exhaustiveCheck: never = eventType; + throw new Error(`Unsupported Event type: ${_exhaustiveCheck}`); + } + } else { + assertWechatReceivedMessageType(msgType, 'MsgType'); + assertString(normalized.MsgId, 'MsgId'); + + const baseFields: Omit = { + ToUserName: normalized.ToUserName, + FromUserName: normalized.FromUserName, + CreateTime: Number(normalized.CreateTime), + MsgId: normalized.MsgId, + MsgDataId: normalized.MsgDataId, + Idx: normalized.Idx, + }; + + // 根据 MsgType 进行具体消息类型的构建 + switch (msgType) { + case "text": + assertString(normalized.Content, 'Content'); + return { + ...baseFields, + MsgType: "text", + Content: normalized.Content, + } satisfies WechatReceivedTextMessage; + case "image": + assertString(normalized.PicUrl, 'PicUrl'); + assertString(normalized.MediaId, 'MediaId'); + return { + ...baseFields, + MsgType: "image", + PicUrl: normalized.PicUrl, + MediaId: normalized.MediaId, + } satisfies WechatReceivedImageMessage; + case "voice": + assertString(normalized.MediaId, 'MediaId'); + assertString(normalized.Format, 'Format'); + assertString(normalized.MediaId16K, 'MediaId16K'); + return { + ...baseFields, + MsgType: "voice", + MediaId: normalized.MediaId, + Format: normalized.Format, + MediaId16K: normalized.MediaId16K, + } satisfies WechatReceivedVoiceMessage; + case "video": + assertString(normalized.MediaId, 'MediaId'); + assertString(normalized.ThumbMediaId, 'ThumbMediaId'); + return { + ...baseFields, + MsgType: "video", + MediaId: normalized.MediaId, + ThumbMediaId: normalized.ThumbMediaId, + } satisfies WechatReceivedVideoMessage; + case "shortvideo": + assertString(normalized.MediaId, 'MediaId'); + assertString(normalized.ThumbMediaId, 'ThumbMediaId'); + return { + ...baseFields, + MsgType: "shortvideo", + MediaId: normalized.MediaId, + ThumbMediaId: normalized.ThumbMediaId, + } satisfies WechatReceivedVideoMessage; + case "location": + assertString(normalized.Location_X, 'Location_X'); + assertString(normalized.Location_Y, 'Location_Y'); + assertString(normalized.Scale, 'Scale'); + assertString(normalized.Label, 'Label'); + return { + ...baseFields, + MsgType: "location", + Location_X: normalized.Location_X, + Location_Y: normalized.Location_Y, + Scale: normalized.Scale, + Label: normalized.Label, + } satisfies WechatReceivedLocationMessage; + case "link": + assertString(normalized.Title, 'Title'); + assertString(normalized.Description, 'Description'); + assertString(normalized.Url, 'Url'); + return { + ...baseFields, + MsgType: "link", + Title: normalized.Title, + Description: normalized.Description, + Url: normalized.Url, + } satisfies WechatReceivedLinkMessage; + default: + const _exhaustiveCheck: never = msgType; + throw new Error(`Unsupported MsgType: ${_exhaustiveCheck}`); + } } } \ No newline at end of file