feat: parse streamvideo/gift cards and heuristic local_type 2/8

This commit is contained in:
wuyouMaster
2026-09-24 20:06:16 +08:00
parent 7864cf001f
commit e69c3626d6
2 changed files with 171 additions and 0 deletions
+117
View File
@@ -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))
+54
View File
@@ -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 = [