mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
feat: parse streamvideo/gift cards and heuristic local_type 2/8
This commit is contained in:
@@ -195,8 +195,12 @@ export function parseMessageContent(content: string, messageType: number): Parse
|
||||
switch (localType) {
|
||||
case 1:
|
||||
return { type: 'text', content: normalized }
|
||||
case 2:
|
||||
return parseLegacyType2(normalized)
|
||||
case 3:
|
||||
return parseImageMessage(normalized)
|
||||
case 8:
|
||||
return parseLegacyType8(normalized)
|
||||
case 17:
|
||||
return parseForwardBundle(normalized)
|
||||
case 37:
|
||||
@@ -559,6 +563,12 @@ function parseShareMessage(content: string): ParsedContent {
|
||||
if (looksLikeKefuCard(content)) {
|
||||
return parseKefuCard(content)
|
||||
}
|
||||
if (looksLikeStreamVideo(content)) {
|
||||
return parseStreamVideoCard(content)
|
||||
}
|
||||
if (looksLikeGiftCard(content)) {
|
||||
return parseGiftCard(content)
|
||||
}
|
||||
if (appMsgType === '19') {
|
||||
return parseForwardBundle(content)
|
||||
}
|
||||
@@ -643,6 +653,8 @@ function parseShareMessage(content: string): ParsedContent {
|
||||
if (looksLikeMusicShare(content)) return parseMusicShareCard(content)
|
||||
if (looksLikeSubscribeCard(content)) return parseSubscribeCard(content)
|
||||
if (looksLikeKefuCard(content)) return parseKefuCard(content)
|
||||
if (looksLikeStreamVideo(content)) return parseStreamVideoCard(content)
|
||||
if (looksLikeGiftCard(content)) return parseGiftCard(content)
|
||||
return { type: 'unknown', raw: content }
|
||||
}
|
||||
|
||||
@@ -804,6 +816,111 @@ function looksLikeKefuCard(content: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
/** 流视频 / 长视频卡(`streamvideotitle` / `finderMegaVideo`)。 */
|
||||
function looksLikeStreamVideo(content: string): boolean {
|
||||
return /streamvideotitle|streamvideoword|streamvideoweburl|streamvideothumb|finderMegaVideo|finderLiveInvite/i.test(
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼物 / 礼品卡(`csgift` / `giftcarditem`)。
|
||||
* 只读展示;**不**调用 `acceptgiftcard` / `preacceptgiftcard` / `getcardgiftinfo`。
|
||||
*/
|
||||
function looksLikeGiftCard(content: string): boolean {
|
||||
return /<csgift\b|<ecsgift\b|<giftcarditem\b|<giftcard\b|giftcarditem|acceptgiftcard/i.test(content)
|
||||
}
|
||||
|
||||
/** 流视频卡 → 只读 share。 */
|
||||
function parseStreamVideoCard(content: string): ParsedContent {
|
||||
const decoded = decodeXmlEntities(stripChatroomPrefix(content))
|
||||
const title =
|
||||
extractXmlValue(decoded, 'streamvideotitle') ||
|
||||
extractXmlValue(decoded, 'title') ||
|
||||
extractXmlValue(decoded, 'sourcetitle') ||
|
||||
'视频'
|
||||
const des =
|
||||
extractXmlValue(decoded, 'streamvideoword') ||
|
||||
extractXmlValue(decoded, 'des') ||
|
||||
extractXmlValue(decoded, 'contentdescshowtext') ||
|
||||
undefined
|
||||
const url =
|
||||
decodeXmlUrl(
|
||||
extractXmlValue(decoded, 'streamvideoweburl') ||
|
||||
extractXmlValue(decoded, 'url') ||
|
||||
extractXmlValue(decoded, 'weburl')
|
||||
) || ''
|
||||
return {
|
||||
type: 'share',
|
||||
title,
|
||||
des,
|
||||
url,
|
||||
appname: /finderMegaVideo|finderLiveInvite/i.test(content) ? '视频号' : '视频',
|
||||
typeVal: extractAppMsgType(content) || 'streamvideo'
|
||||
}
|
||||
}
|
||||
|
||||
/** 礼物 / 礼品卡 → 只读 share(无 title 时 system 文案)。 */
|
||||
function parseGiftCard(content: string): ParsedContent {
|
||||
const decoded = decodeXmlEntities(stripChatroomPrefix(content))
|
||||
const title =
|
||||
extractXmlValue(decoded, 'title') ||
|
||||
extractXmlValue(decoded, 'gifttitle') ||
|
||||
extractXmlValue(decoded, 'cardtitle') ||
|
||||
extractXmlValue(decoded, 'brandname')
|
||||
const des =
|
||||
extractXmlValue(decoded, 'des') ||
|
||||
extractXmlValue(decoded, 'giftwording') ||
|
||||
extractXmlValue(decoded, 'cardwording') ||
|
||||
extractXmlValue(decoded, 'description')
|
||||
const url = decodeXmlUrl(extractXmlValue(decoded, 'url') || extractXmlValue(decoded, 'cardurl') || '')
|
||||
if (!title && !des) {
|
||||
return {
|
||||
type: 'system',
|
||||
content: normalizeSystemText(decoded.replace(/<[^>]+>/g, ' ')) || '礼物卡',
|
||||
raw: content
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'share',
|
||||
title: title || '礼物卡',
|
||||
des: des || undefined,
|
||||
url: url || '',
|
||||
appname: extractXmlValue(decoded, 'brandname') || '礼物',
|
||||
typeVal: extractAppMsgType(content) || 'giftcard'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* local_type=2:真机 histogram 未采到;按内容形态尽量落到 system/text,
|
||||
* 否则保留 unknown(见 wechat-message-type-coverage.md)。
|
||||
*/
|
||||
function parseLegacyType2(content: string): ParsedContent {
|
||||
if (/<sysmsg\b|<patMsg\b|revoke/i.test(content)) {
|
||||
return parseSystemMessage(content)
|
||||
}
|
||||
if (!/<[a-zA-Z!]/.test(content)) {
|
||||
return { type: 'text', content }
|
||||
}
|
||||
return parseSystemMessage(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* local_type=8:社区多见于 GIF/大表情变体;无真机样本时按 sticker → image → system
|
||||
* 逐级尝试,避免直接黑块。
|
||||
*/
|
||||
function parseLegacyType8(content: string): ParsedContent {
|
||||
const sticker = parseStickerMessage(content)
|
||||
if (sticker.type === 'sticker' && sticker.md5) return sticker
|
||||
const image = parseImageMessage(content)
|
||||
if (image.type === 'image' && (image.md5 || image.datName)) return image
|
||||
if (/<sysmsg\b|<emoji\b|<msg\b/i.test(content)) {
|
||||
const system = parseSystemMessage(content)
|
||||
if (system.type === 'system' && system.content && system.content !== content) return system
|
||||
}
|
||||
return { type: 'unknown', raw: content, messageType: 8 }
|
||||
}
|
||||
|
||||
/** 音乐 / 听歌分享(`musicShareItem` / `ListenItem` / `songalbumurl`)→ 只读 share。 */
|
||||
function parseMusicShareCard(content: string): ParsedContent {
|
||||
const decoded = decodeXmlEntities(stripChatroomPrefix(content))
|
||||
|
||||
@@ -343,6 +343,60 @@ describe('message parser', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('parses stream video cards', () => {
|
||||
const xml = [
|
||||
'<msg><appmsg>',
|
||||
'<type>51</type>',
|
||||
'<streamvideotitle><![CDATA[户外长视频]]></streamvideotitle>',
|
||||
'<streamvideoword><![CDATA[点击播放]]></streamvideoword>',
|
||||
'<streamvideoweburl><![CDATA[https://finder.example/v/1]]></streamvideoweburl>',
|
||||
'<finderMegaVideo></finderMegaVideo>',
|
||||
'</appmsg></msg>'
|
||||
].join('')
|
||||
expect(parseMessageContent(xml, 49)).toMatchObject({
|
||||
type: 'share',
|
||||
title: '户外长视频',
|
||||
des: '点击播放',
|
||||
url: 'https://finder.example/v/1',
|
||||
appname: '视频号',
|
||||
typeVal: '51'
|
||||
})
|
||||
})
|
||||
|
||||
it('parses gift cards without enabling accept actions', () => {
|
||||
const xml = [
|
||||
'<msg><appmsg>',
|
||||
'<title><![CDATA[生日礼物卡]]></title>',
|
||||
'<des><![CDATA[送你一张礼物卡]]></des>',
|
||||
'<giftcarditem><brandname><![CDATA[示例品牌]]></brandname></giftcarditem>',
|
||||
'</appmsg></msg>'
|
||||
].join('')
|
||||
expect(parseMessageContent(xml, 49)).toMatchObject({
|
||||
type: 'share',
|
||||
title: '生日礼物卡',
|
||||
des: '送你一张礼物卡',
|
||||
appname: '示例品牌',
|
||||
typeVal: 'giftcard'
|
||||
})
|
||||
})
|
||||
|
||||
it('maps legacy local_type 2 and 8 without dropping content', () => {
|
||||
expect(parseMessageContent('plain status line', 2)).toEqual({
|
||||
type: 'text',
|
||||
content: 'plain status line'
|
||||
})
|
||||
const gifLike =
|
||||
'<msg><emoji md5="abcdefabcdefabcdefabcdefabcdefab" cdnurl="https://e.example/a.gif" /></msg>'
|
||||
expect(parseMessageContent(gifLike, 8)).toMatchObject({
|
||||
type: 'sticker',
|
||||
md5: 'abcdefabcdefabcdefabcdefabcdefab'
|
||||
})
|
||||
expect(parseMessageContent('opaque type8', 8)).toMatchObject({
|
||||
type: 'unknown',
|
||||
messageType: 8
|
||||
})
|
||||
})
|
||||
|
||||
it('parses transfer wcpayinfo fields (type 2000)', () => {
|
||||
// 2026-09 真机转账采样字段名(含微信原文 transcationid 拼写)
|
||||
const xml = [
|
||||
|
||||
Reference in New Issue
Block a user