feat: 添加事件推送处理

- 当前不处理任何事件推送
This commit is contained in:
2025-12-26 04:55:51 +00:00
parent ac4cea4658
commit 545d1e687f
3 changed files with 184 additions and 98 deletions

View File

@ -51,6 +51,14 @@ export default defineEndpoint({
let replyContent; let replyContent;
let reply; 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') { if (MsgType !== 'text') {
replyContent = "暂不支持该消息类型"; replyContent = "暂不支持该消息类型";
} else { } else {

View File

@ -31,3 +31,24 @@ export function assertWechatReceivedMessageType(value: unknown, fieldName: strin
throw new Error(`Expected ${fieldName} to be one of ${WECHAT_RECEIVED_MESSAGE_TYPES.join(", ")}, but got ${typeof value === 'string' ? value : typeof value}`); 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}`);
}
}

View File

@ -1,10 +1,12 @@
import { XMLParser } from "fast-xml-parser"; import { XMLParser } from "fast-xml-parser";
import getRawBody from "raw-body"; import getRawBody from "raw-body";
import type { Request } from "express"; 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 type { WechatEncryptMessage } from "../types/encrypted-message";
import { WechatXmlObject, NormalizedWechatXml } from "../types/wechat-xml"; 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对象 * 从原始XML字符串解析为微信XML对象
@ -197,114 +199,169 @@ export async function parseWechatEncryptMessage(req: Request): Promise<WechatEnc
* // } * // }
* ``` * ```
*/ */
export async function parseDecryptedXML(raw: string): Promise<WechatReceivedMessage> { export async function parseDecryptedXML(raw: string): Promise<WechatReceivedMessage | WechatReceivedEvent> {
// const parser = new XMLParser({
// ignoreAttributes: false,
// parseTagValue: false,
// cdataPropName: '__cdata',
// trimValues: true,
// });
// const result = parser.parse(raw);
// const xml = result.xml as WechatXmlObject;
const xml = parseRawXml(raw); const xml = parseRawXml(raw);
// 将可能存在的 __cdata 提取 // 将可能存在的 __cdata 提取
const normalized = normalizeWechatXml(xml); const normalized = normalizeWechatXml(xml);
const msgType = normalized.MsgType; const msgType = normalized.MsgType;
assertString(msgType, 'MsgType'); let eventType: string | undefined;
assertWechatReceivedMessageType(msgType, 'MsgType');
// 基础字段类型断言 // 基础字段类型断言
assertString(normalized.ToUserName, 'ToUserName'); assertString(normalized.ToUserName, 'ToUserName');
assertString(normalized.FromUserName, 'FromUserName'); assertString(normalized.FromUserName, 'FromUserName');
assertString(normalized.CreateTime, 'CreateTime'); assertString(normalized.CreateTime, 'CreateTime');
assertString(normalized.MsgId, 'MsgId'); assertString(msgType, 'MsgType');
const baseFields: Omit<WechatReceivedBaseMessage, 'MsgType'> = { if (msgType === 'event') {
ToUserName: normalized.ToUserName, assertString(normalized.Event, 'Event');
FromUserName: normalized.FromUserName, eventType = normalized.Event;
CreateTime: Number(normalized.CreateTime), assertWechatReceivedEventType(eventType, 'Event');
MsgId: normalized.MsgId,
MsgDataId: normalized.MsgDataId,
Idx: normalized.Idx,
};
// 根据 MsgType 进行具体消息类型的构建 const baseFields: Omit<WechatReceivedEvent, 'MsgType' | 'Event'> = {
switch (msgType) { ToUserName: normalized.ToUserName,
case "text": FromUserName: normalized.FromUserName,
assertString(normalized.Content, 'Content'); CreateTime: Number(normalized.CreateTime),
return { };
...baseFields,
MsgType: "text", // 根据 Event 进行具体事件类型的构建
Content: normalized.Content, switch (eventType) {
} satisfies WechatReceivedTextMessage; case "subscribe":
case "image": return {
assertString(normalized.PicUrl, 'PicUrl'); ...baseFields,
assertString(normalized.MediaId, 'MediaId'); MsgType: "event",
return { Event: "subscribe",
...baseFields, EventKey: normalized.EventKey,
MsgType: "image", Ticket: normalized.Ticket,
PicUrl: normalized.PicUrl, } satisfies WechatReceivedEvent;
MediaId: normalized.MediaId, case "SCAN":
} satisfies WechatReceivedImageMessage; return {
case "voice": ...baseFields,
assertString(normalized.MediaId, 'MediaId'); MsgType: "event",
assertString(normalized.Format, 'Format'); Event: "SCAN",
assertString(normalized.MediaId16K, 'MediaId16K'); EventKey: normalized.EventKey ?? '',
return { Ticket: normalized.Ticket ?? '',
...baseFields, } satisfies WechatReceivedEvent;
MsgType: "voice", case "LOCATION":
MediaId: normalized.MediaId, assertString(normalized.Latitude, 'Latitude');
Format: normalized.Format, assertString(normalized.Longitude, 'Longitude');
MediaId16K: normalized.MediaId16K, assertString(normalized.Precision, 'Precision');
} satisfies WechatReceivedVoiceMessage; return {
case "video": ...baseFields,
assertString(normalized.MediaId, 'MediaId'); MsgType: "event",
assertString(normalized.ThumbMediaId, 'ThumbMediaId'); Event: "LOCATION",
return { Latitude: normalized.Latitude,
...baseFields, Longitude: normalized.Longitude,
MsgType: "video", Precision: normalized.Precision,
MediaId: normalized.MediaId, } satisfies WechatReceivedEvent;
ThumbMediaId: normalized.ThumbMediaId, case "CLICK":
} satisfies WechatReceivedVideoMessage; assertString(normalized.EventKey, 'EventKey');
case "shortvideo": return {
assertString(normalized.MediaId, 'MediaId'); ...baseFields,
assertString(normalized.ThumbMediaId, 'ThumbMediaId'); MsgType: "event",
return { Event: "CLICK",
...baseFields, EventKey: normalized.EventKey,
MsgType: "shortvideo", } satisfies WechatReceivedEvent;
MediaId: normalized.MediaId, case "VIEW":
ThumbMediaId: normalized.ThumbMediaId, assertString(normalized.EventKey, 'EventKey');
} satisfies WechatReceivedVideoMessage; return {
case "location": ...baseFields,
assertString(normalized.Location_X, 'Location_X'); MsgType: "event",
assertString(normalized.Location_Y, 'Location_Y'); Event: "VIEW",
assertString(normalized.Scale, 'Scale'); EventKey: normalized.EventKey,
assertString(normalized.Label, 'Label'); } satisfies WechatReceivedEvent;
return { default:
...baseFields, const _exhaustiveCheck: never = eventType;
MsgType: "location", throw new Error(`Unsupported Event type: ${_exhaustiveCheck}`);
Location_X: normalized.Location_X, }
Location_Y: normalized.Location_Y, } else {
Scale: normalized.Scale, assertWechatReceivedMessageType(msgType, 'MsgType');
Label: normalized.Label, assertString(normalized.MsgId, 'MsgId');
} satisfies WechatReceivedLocationMessage;
case "link": const baseFields: Omit<WechatReceivedBaseMessage, 'MsgType'> = {
assertString(normalized.Title, 'Title'); ToUserName: normalized.ToUserName,
assertString(normalized.Description, 'Description'); FromUserName: normalized.FromUserName,
assertString(normalized.Url, 'Url'); CreateTime: Number(normalized.CreateTime),
return { MsgId: normalized.MsgId,
...baseFields, MsgDataId: normalized.MsgDataId,
MsgType: "link", Idx: normalized.Idx,
Title: normalized.Title, };
Description: normalized.Description,
Url: normalized.Url, // 根据 MsgType 进行具体消息类型的构建
} satisfies WechatReceivedLinkMessage; switch (msgType) {
default: case "text":
const _exhaustiveCheck: never = msgType; assertString(normalized.Content, 'Content');
throw new Error(`Unsupported MsgType: ${_exhaustiveCheck}`); 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}`);
}
} }
} }