mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 12:34:35 +08:00
feat: map transfer/hb status labels into export and chat cards
This commit is contained in:
@@ -1172,13 +1172,37 @@ const renderExportScript = (name: string): string => `
|
||||
}
|
||||
const renderPaymentContent = (data, kind) => {
|
||||
const isTransfer = kind === 'transfer'
|
||||
const pay = (isTransfer ? data.transfer : data.pay) || {}
|
||||
const amount = pay.amountText
|
||||
const memo = pay.payMemo
|
||||
const statusText = isTransfer ? pay.transferStatusText : pay.redPacketStatusText
|
||||
const meta = [amount, memo ? '备注:' + memo : '', statusText]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
const title = isTransfer
|
||||
? (data.title || '微信转账')
|
||||
: (pay.sendTitle || pay.receiveTitle || data.title || '微信红包')
|
||||
const blurb =
|
||||
data.description ||
|
||||
data.des ||
|
||||
(isTransfer ? '转账消息' : (pay.sceneText || '恭喜发财,大吉大利'))
|
||||
// 状态/金额/备注始终单独露出,避免被消息原文 des 盖住。
|
||||
const description = meta
|
||||
? (blurb && blurb !== meta ? blurb + ' · ' + meta : meta)
|
||||
: blurb
|
||||
const footerBits = isTransfer
|
||||
? [pay.paySubtype === '3' ? '收款' : pay.paySubtype === '1' || pay.paySubtype === '4' ? '转账' : '',
|
||||
pay.transferId ? '单号 ' + pay.transferId : '']
|
||||
: [pay.hbType ? '类型 ' + pay.hbType : '', pay.sendId ? 'sendid ' + pay.sendId : '']
|
||||
return '<div class="structured-content payment-content ' + (isTransfer ? 'transfer' : 'red-packet') +
|
||||
'" data-rich-kind="' + (isTransfer ? 'transfer' : 'redPacket') + '">' +
|
||||
'<div class="structured-kicker">' + (isTransfer ? '微信转账' : '微信红包') + '</div>' +
|
||||
'<div class="structured-title">' + displayText(data.title || (isTransfer ? '微信转账' : '微信红包')) + '</div>' +
|
||||
'<div class="structured-description">' +
|
||||
displayText(data.description || data.des || (isTransfer ? '转账消息' : '恭喜发财,大吉大利')) +
|
||||
'</div></div>'
|
||||
'<div class="structured-title">' + displayText(title) + '</div>' +
|
||||
'<div class="structured-description">' + displayText(description) + '</div>' +
|
||||
(footerBits.filter(Boolean).length
|
||||
? '<div class="structured-footer">' + displayText(footerBits.filter(Boolean).join(' · ')) + '</div>'
|
||||
: '') +
|
||||
'</div>'
|
||||
}
|
||||
const renderVoipContent = (data) => {
|
||||
const title = Number(data.roomType) === 1 ? '视频通话' : '语音通话'
|
||||
|
||||
+103
-5
@@ -1,3 +1,5 @@
|
||||
import { describeRedPacketStatus, describeTransferStatus } from '../shared/payment-status'
|
||||
|
||||
type TextContent = { type: 'text'; content: string }
|
||||
type VoiceContent = { type: 'voice'; duration?: number }
|
||||
type LocationContent = {
|
||||
@@ -22,6 +24,40 @@ type ShareContent = {
|
||||
appname?: string
|
||||
typeVal?: string
|
||||
articles?: ShareArticle[]
|
||||
transfer?: TransferPaymentInfo
|
||||
}
|
||||
|
||||
type TransferPaymentInfo = {
|
||||
paySubtype?: string
|
||||
amountText?: string
|
||||
transcationId?: string
|
||||
transferId?: string
|
||||
invalidTime?: string
|
||||
beginTransferTime?: string
|
||||
effectiveDate?: string
|
||||
payMemo?: string
|
||||
receiverUsername?: string
|
||||
payerUsername?: string
|
||||
transferStatus?: string
|
||||
/** `transfer_status` 展示文案(只读)。 */
|
||||
transferStatusText?: string
|
||||
}
|
||||
|
||||
type RedPacketPaymentInfo = {
|
||||
templateId?: string
|
||||
receiveTitle?: string
|
||||
sendTitle?: string
|
||||
sceneText?: string
|
||||
senderDes?: string
|
||||
receiverDes?: string
|
||||
iconUrl?: string
|
||||
nativeUrl?: string
|
||||
sendId?: string
|
||||
hbType?: string
|
||||
hbStatus?: string
|
||||
receiveStatus?: string
|
||||
/** `hb_status` / `receive_status` 展示文案(只读)。 */
|
||||
redPacketStatusText?: string
|
||||
}
|
||||
type ForwardedMessageItem = {
|
||||
messageType: number
|
||||
@@ -51,6 +87,7 @@ type RedPacketContent = {
|
||||
title: string
|
||||
description?: string
|
||||
url?: string
|
||||
pay?: RedPacketPaymentInfo
|
||||
}
|
||||
type VoipContent = { type: 'voip'; duration?: number; status: string; roomType?: number }
|
||||
type ImageContent = {
|
||||
@@ -522,12 +559,25 @@ function parseShareMessage(content: string): ParsedContent {
|
||||
}
|
||||
}
|
||||
|
||||
if (appMsgType === '2001') {
|
||||
if (appMsgType === '2001' || /mmpayhb|receivehongbao|wxpay:\/\/c2cbizmessagehandler\/hongbao/i.test(content)) {
|
||||
const pay = parseWcpayInfo(content)
|
||||
const url = decodeXmlUrl(extractXmlValue(content, 'url')) || undefined
|
||||
return {
|
||||
type: 'redPacket',
|
||||
title: extractXmlValue(content, 'title') || '微信红包',
|
||||
description: extractXmlValue(content, 'des') || '恭喜发财,大吉大利',
|
||||
url: decodeXmlUrl(extractXmlValue(content, 'url')) || undefined
|
||||
title:
|
||||
pay.sendTitle ||
|
||||
pay.receiveTitle ||
|
||||
extractXmlValue(content, 'title') ||
|
||||
'微信红包',
|
||||
description:
|
||||
extractXmlValue(content, 'des') ||
|
||||
pay.sceneText ||
|
||||
'恭喜发财,大吉大利',
|
||||
url,
|
||||
pay: {
|
||||
...pay,
|
||||
sendId: pay.sendId || extractSendIdFromUrl(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +607,55 @@ function parseShareMessage(content: string): ParsedContent {
|
||||
url,
|
||||
appname,
|
||||
typeVal,
|
||||
articles: articles.length > 1 ? articles : undefined
|
||||
articles: articles.length > 1 ? articles : undefined,
|
||||
transfer: typeVal === '2000' ? parseWcpayInfo(content) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 只读解析 `<wcpayinfo>`(转账 / 红包展示字段;不做支付)。 */
|
||||
function parseWcpayInfo(content: string): TransferPaymentInfo & RedPacketPaymentInfo {
|
||||
const block = /<wcpayinfo>([\s\S]*?)<\/wcpayinfo>/i.exec(content)?.[1] || content
|
||||
const val = (tag: string): string | undefined => {
|
||||
const raw = extractXmlValue(block, tag)
|
||||
return raw ? decodeXmlEntities(raw) || undefined : undefined
|
||||
}
|
||||
const transferStatus = val('transfer_status')
|
||||
const hbStatus = val('hb_status')
|
||||
const receiveStatus = val('receive_status')
|
||||
return {
|
||||
paySubtype: val('paysubtype'),
|
||||
amountText: val('feedesc'),
|
||||
transcationId: val('transcationid'),
|
||||
transferId: val('transferid'),
|
||||
invalidTime: val('invalidtime'),
|
||||
beginTransferTime: val('begintransfertime'),
|
||||
effectiveDate: val('effectivedate'),
|
||||
payMemo: val('pay_memo') || undefined,
|
||||
receiverUsername: val('receiver_username'),
|
||||
payerUsername: val('payer_username'),
|
||||
transferStatus,
|
||||
transferStatusText: describeTransferStatus(transferStatus),
|
||||
templateId: val('templateid'),
|
||||
receiveTitle: val('receivertitle'),
|
||||
sendTitle: val('sendertitle'),
|
||||
sceneText: val('scenetext'),
|
||||
senderDes: val('senderdes'),
|
||||
receiverDes: val('receiverdes'),
|
||||
iconUrl: decodeXmlUrl(val('iconurl') || '') || undefined,
|
||||
nativeUrl: val('nativeurl'),
|
||||
hbType: val('hb_type'),
|
||||
hbStatus,
|
||||
receiveStatus,
|
||||
redPacketStatusText: describeRedPacketStatus(hbStatus, receiveStatus)
|
||||
}
|
||||
}
|
||||
|
||||
function extractSendIdFromUrl(url?: string): string | undefined {
|
||||
if (!url) return undefined
|
||||
try {
|
||||
return new URL(url).searchParams.get('sendid') || undefined
|
||||
} catch {
|
||||
return /sendid=(\d+)/i.exec(url)?.[1]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+39
-1
@@ -155,6 +155,37 @@ import type {
|
||||
TextToSpeechSettingsResult
|
||||
} from '../shared/text-to-speech'
|
||||
|
||||
export type TransferPaymentInfo = {
|
||||
paySubtype?: string
|
||||
amountText?: string
|
||||
transcationId?: string
|
||||
transferId?: string
|
||||
invalidTime?: string
|
||||
beginTransferTime?: string
|
||||
effectiveDate?: string
|
||||
payMemo?: string
|
||||
receiverUsername?: string
|
||||
payerUsername?: string
|
||||
transferStatus?: string
|
||||
transferStatusText?: string
|
||||
}
|
||||
|
||||
export type RedPacketPaymentInfo = {
|
||||
templateId?: string
|
||||
receiveTitle?: string
|
||||
sendTitle?: string
|
||||
sceneText?: string
|
||||
senderDes?: string
|
||||
receiverDes?: string
|
||||
iconUrl?: string
|
||||
nativeUrl?: string
|
||||
sendId?: string
|
||||
hbType?: string
|
||||
hbStatus?: string
|
||||
receiveStatus?: string
|
||||
redPacketStatusText?: string
|
||||
}
|
||||
|
||||
export type ParsedContent =
|
||||
| { type: 'text'; content: string }
|
||||
| { type: 'voice'; duration?: number }
|
||||
@@ -167,6 +198,7 @@ export type ParsedContent =
|
||||
url: string
|
||||
appname?: string
|
||||
typeVal?: string
|
||||
transfer?: TransferPaymentInfo
|
||||
}
|
||||
| {
|
||||
type: 'miniProgram'
|
||||
@@ -178,7 +210,13 @@ export type ParsedContent =
|
||||
thumbDatName?: string
|
||||
thumbDataUrl?: string
|
||||
}
|
||||
| { type: 'redPacket'; title: string; description?: string; url?: string }
|
||||
| {
|
||||
type: 'redPacket'
|
||||
title: string
|
||||
description?: string
|
||||
url?: string
|
||||
pay?: RedPacketPaymentInfo
|
||||
}
|
||||
| { type: 'voip'; duration?: number; status: string; roomType?: number }
|
||||
| { type: 'image'; md5?: string; datName?: string; aeskey?: string; encrypVer?: number }
|
||||
| {
|
||||
|
||||
@@ -158,7 +158,22 @@ function CardBubble({ data }: { data: Extract<ParsedContent, { type: 'card' }> }
|
||||
}
|
||||
|
||||
function ShareBubble({ data }: { data: Extract<ParsedContent, { type: 'share' }> }): JSX.Element {
|
||||
const { title, des, url, appname, articles } = data
|
||||
const { title, des, url, appname, articles, transfer, typeVal } = data
|
||||
|
||||
if (String(typeVal) === '2000' || transfer) {
|
||||
const pay = transfer || {}
|
||||
const meta = [pay.amountText, pay.payMemo ? `备注:${pay.payMemo}` : '', pay.transferStatusText]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
return (
|
||||
<div className="transfer-message">
|
||||
<div className="transfer-kicker">微信转账</div>
|
||||
<div className="transfer-title">{title || '微信转账'}</div>
|
||||
<div className="transfer-meta">{meta || des || '转账消息'}</div>
|
||||
{pay.transferId ? <div className="transfer-footer">单号 {pay.transferId}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (articles?.length) {
|
||||
return (
|
||||
@@ -254,6 +269,8 @@ function RedPacketBubble({
|
||||
const handleClick = (): void => {
|
||||
if (data.url) window.open(data.url, '_blank')
|
||||
}
|
||||
const statusText = data.pay?.redPacketStatusText
|
||||
const blurb = data.description || data.pay?.sceneText || '恭喜发财,大吉大利'
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -266,7 +283,7 @@ function RedPacketBubble({
|
||||
</span>
|
||||
<span className="red-packet-copy">
|
||||
<strong>{data.title}</strong>
|
||||
<small>{data.description || '恭喜发财,大吉大利'}</small>
|
||||
<small>{statusText ? `${blurb} · ${statusText}` : blurb}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="red-packet-footer">微信红包</div>
|
||||
|
||||
@@ -364,6 +364,43 @@
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.transfer-message {
|
||||
width: min(280px, 48vw);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: 6px;
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-primary);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.transfer-kicker {
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.transfer-title {
|
||||
margin-top: 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.transfer-meta {
|
||||
margin-top: 6px;
|
||||
color: var(--wxex-text-primary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.transfer-footer {
|
||||
margin-top: 8px;
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.red-packet-message {
|
||||
width: min(280px, 48vw);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 转账 / 红包状态文案(只读展示)。
|
||||
* 文案对齐微信 UI 簇(待收款/已收款/…、待领取/已领取/…)。
|
||||
* 数字枚举为推断对齐;未知值原样回退。
|
||||
*/
|
||||
|
||||
const TRANSFER_STATUS_TEXT: Record<string, string> = {
|
||||
'0': '待收款',
|
||||
'1': '已收款,待入账',
|
||||
'2': '已收款',
|
||||
'3': '已退还',
|
||||
'4': '过期未收款,已退还',
|
||||
'5': '你已退还',
|
||||
'6': '已过期',
|
||||
'7': '此笔转账已被撤回',
|
||||
paid: '已收款',
|
||||
received: '已收款',
|
||||
pending: '待收款',
|
||||
unreceived: '待收款',
|
||||
refunded: '已退还',
|
||||
expired: '过期未收款,已退还',
|
||||
withdrawn: '此笔转账已被撤回',
|
||||
revoked: '此笔转账已被撤回',
|
||||
confirmed: '朋友已确认收款',
|
||||
accepted: '已被接收',
|
||||
accepting: '已被接收'
|
||||
}
|
||||
|
||||
const RED_PACKET_STATUS_TEXT: Record<string, string> = {
|
||||
'0': '待领取',
|
||||
'1': '已领取',
|
||||
'2': '已被领完',
|
||||
'3': '已过期',
|
||||
'4': '已被领取',
|
||||
received: '已领取',
|
||||
claimed: '已领取',
|
||||
expired: '已过期',
|
||||
exhausted: '已被领完',
|
||||
pending: '待领取',
|
||||
unclaimed: '待领取'
|
||||
}
|
||||
|
||||
const TRANSFER_STATUS_KEYWORDS: Array<[RegExp, string]> = [
|
||||
[/撤回|withdraw|revoke/i, '此笔转账已被撤回'],
|
||||
[/过期未收款|expired/i, '过期未收款,已退还'],
|
||||
[/已退还|退还|refund/i, '已退还'],
|
||||
[/待入账|pending.?credit/i, '已收款,待入账'],
|
||||
[/已收款|已收|paid|received/i, '已收款'],
|
||||
[/已被接收|accepted|accepting/i, '已被接收'],
|
||||
[/确认收款|confirmed/i, '朋友已确认收款'],
|
||||
[/待收款|请收款|pending|unreceived/i, '待收款'],
|
||||
[/已过期|expired/i, '已过期']
|
||||
]
|
||||
|
||||
const RED_PACKET_STATUS_KEYWORDS: Array<[RegExp, string]> = [
|
||||
[/领完|exhausted/i, '已被领完'],
|
||||
[/已被领取|已领取|received|claimed/i, '已领取'],
|
||||
[/过期|expired/i, '已过期'],
|
||||
[/待领取|未领取|pending|unclaimed/i, '待领取']
|
||||
]
|
||||
|
||||
function describeByMap(
|
||||
raw: string | number | undefined,
|
||||
table: Record<string, string>,
|
||||
keywords: Array<[RegExp, string]>
|
||||
): string | undefined {
|
||||
if (raw === undefined || raw === null || raw === '') return undefined
|
||||
const text = String(raw).trim()
|
||||
if (!text) return undefined
|
||||
const direct = table[text.toLowerCase()] || table[text]
|
||||
if (direct) return direct
|
||||
for (const [re, label] of keywords) {
|
||||
if (re.test(text)) return label
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/** `transfer_status` → 展示文案(未知原样返回)。 */
|
||||
export function describeTransferStatus(raw?: string | number): string | undefined {
|
||||
return describeByMap(raw, TRANSFER_STATUS_TEXT, TRANSFER_STATUS_KEYWORDS)
|
||||
}
|
||||
|
||||
/** 红包 `hb_status` / `receive_status` → 展示文案。 */
|
||||
export function describeRedPacketStatus(
|
||||
hbStatus?: string | number,
|
||||
receiveStatus?: string | number
|
||||
): string | undefined {
|
||||
return (
|
||||
describeByMap(hbStatus, RED_PACKET_STATUS_TEXT, RED_PACKET_STATUS_KEYWORDS) ||
|
||||
describeByMap(receiveStatus, RED_PACKET_STATUS_TEXT, RED_PACKET_STATUS_KEYWORDS)
|
||||
)
|
||||
}
|
||||
|
||||
/** `paysubtype` 仅作辅助标签(真机见过 1/3/4;语义未在 dylib 钉死)。 */
|
||||
export function describePaySubtype(raw?: string | number): string | undefined {
|
||||
if (raw === undefined || raw === null || raw === '') return undefined
|
||||
const text = String(raw).trim()
|
||||
const table: Record<string, string> = {
|
||||
'1': '转账',
|
||||
'3': '收款',
|
||||
'4': '转账'
|
||||
}
|
||||
return table[text] || text
|
||||
}
|
||||
|
||||
export const TRANSFER_STATUS_LABELS = TRANSFER_STATUS_TEXT
|
||||
export const RED_PACKET_STATUS_LABELS = RED_PACKET_STATUS_TEXT
|
||||
@@ -97,6 +97,43 @@ type ShareContent = {
|
||||
appname?: string
|
||||
typeVal?: string
|
||||
articles?: ShareArticle[]
|
||||
/** `wcpayinfo` when typeVal === '2000' (微信转账). Display/export only. */
|
||||
transfer?: TransferPaymentInfo
|
||||
}
|
||||
|
||||
/** 转账 `wcpayinfo` 字段(只读展示;不含支付操作)。 */
|
||||
export type TransferPaymentInfo = {
|
||||
paySubtype?: string
|
||||
amountText?: string
|
||||
transcationId?: string
|
||||
transferId?: string
|
||||
invalidTime?: string
|
||||
beginTransferTime?: string
|
||||
effectiveDate?: string
|
||||
payMemo?: string
|
||||
receiverUsername?: string
|
||||
payerUsername?: string
|
||||
transferStatus?: string
|
||||
/** `transfer_status` 展示文案(只读)。 */
|
||||
transferStatusText?: string
|
||||
}
|
||||
|
||||
/** 红包 `wcpayinfo` 字段(只读展示)。 */
|
||||
export type RedPacketPaymentInfo = {
|
||||
templateId?: string
|
||||
receiveTitle?: string
|
||||
sendTitle?: string
|
||||
sceneText?: string
|
||||
senderDes?: string
|
||||
receiverDes?: string
|
||||
iconUrl?: string
|
||||
nativeUrl?: string
|
||||
sendId?: string
|
||||
hbType?: string
|
||||
hbStatus?: string
|
||||
receiveStatus?: string
|
||||
/** 展示文案(只读)。 */
|
||||
redPacketStatusText?: string
|
||||
}
|
||||
export type ForwardedMessageItem = {
|
||||
messageType: number
|
||||
@@ -126,6 +163,8 @@ type RedPacketContent = {
|
||||
title: string
|
||||
description?: string
|
||||
url?: string
|
||||
/** `wcpayinfo` 展示字段(只读)。 */
|
||||
pay?: RedPacketPaymentInfo
|
||||
}
|
||||
type VoipContent = { type: 'voip'; duration?: number; status: string; roomType?: number }
|
||||
type ImageContent = {
|
||||
|
||||
Reference in New Issue
Block a user