diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 1626ae1..bea63a0 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -146,6 +146,9 @@ interface ExportSessionStats { imageMessages: number videoMessages: number emojiMessages: number + transferMessages: number + redPacketMessages: number + callMessages: number firstTimestamp?: number lastTimestamp?: number privateMutualGroups?: number @@ -2102,7 +2105,10 @@ class ChatService { voiceMessages: Number.isFinite(stats.voiceMessages) ? Math.max(0, Math.floor(stats.voiceMessages)) : 0, imageMessages: Number.isFinite(stats.imageMessages) ? Math.max(0, Math.floor(stats.imageMessages)) : 0, videoMessages: Number.isFinite(stats.videoMessages) ? Math.max(0, Math.floor(stats.videoMessages)) : 0, - emojiMessages: Number.isFinite(stats.emojiMessages) ? Math.max(0, Math.floor(stats.emojiMessages)) : 0 + emojiMessages: Number.isFinite(stats.emojiMessages) ? Math.max(0, Math.floor(stats.emojiMessages)) : 0, + transferMessages: Number.isFinite(stats.transferMessages) ? Math.max(0, Math.floor(stats.transferMessages)) : 0, + redPacketMessages: Number.isFinite(stats.redPacketMessages) ? Math.max(0, Math.floor(stats.redPacketMessages)) : 0, + callMessages: Number.isFinite(stats.callMessages) ? Math.max(0, Math.floor(stats.callMessages)) : 0 } if (Number.isFinite(stats.firstTimestamp)) normalized.firstTimestamp = Math.max(0, Math.floor(stats.firstTimestamp as number)) @@ -2123,6 +2129,9 @@ class ChatService { imageMessages: stats.imageMessages, videoMessages: stats.videoMessages, emojiMessages: stats.emojiMessages, + transferMessages: stats.transferMessages, + redPacketMessages: stats.redPacketMessages, + callMessages: stats.callMessages, firstTimestamp: stats.firstTimestamp, lastTimestamp: stats.lastTimestamp, privateMutualGroups: stats.privateMutualGroups, @@ -2341,6 +2350,26 @@ class ChatService { return String(value || '').replace(/'/g, "''") } + private extractType49XmlTypeForStats(content: string): string { + if (!content) return '' + + const appmsgMatch = /([\s\S]*?)<\/appmsg>/i.exec(content) + if (appmsgMatch) { + const appmsgInner = appmsgMatch[1] + .replace(//gi, '') + .replace(//gi, '') + const typeMatch = /([\s\S]*?)<\/type>/i.exec(appmsgInner) + if (typeMatch) return String(typeMatch[1] || '').trim() + } + + return this.extractXmlValue(content, 'type') + } + + private buildXmlTypeLikeExpr(columnName: string, xmlType: '2000' | '2001'): string { + const colExpr = `LOWER(CAST(COALESCE(${this.quoteSqlIdentifier(columnName)}, '') AS TEXT))` + return `${colExpr} LIKE '%${xmlType}%'` + } + private async collectSessionExportStatsByCursorScan( sessionId: string, selfIdentitySet: Set @@ -2350,7 +2379,10 @@ class ChatService { voiceMessages: 0, imageMessages: 0, videoMessages: 0, - emojiMessages: 0 + emojiMessages: 0, + transferMessages: 0, + redPacketMessages: 0, + callMessages: 0 } if (sessionId.endsWith('@chatroom')) { stats.groupMyMessages = 0 @@ -2380,6 +2412,17 @@ class ChatService { if (localType === 3) stats.imageMessages += 1 if (localType === 43) stats.videoMessages += 1 if (localType === 47) stats.emojiMessages += 1 + if (localType === 50) stats.callMessages += 1 + if (localType === 8589934592049) stats.transferMessages += 1 + if (localType === 8594229559345) stats.redPacketMessages += 1 + if (localType === 49) { + const rawMessageContent = this.getRowField(row, ['message_content', 'messageContent', 'msg_content', 'msgContent', 'content', 'WCDB_CT_message_content']) + const rawCompressContent = this.getRowField(row, ['compress_content', 'compressContent', 'compressed_content', 'compressedContent', 'WCDB_CT_compress_content']) + const content = this.decodeMessageContent(rawMessageContent, rawCompressContent) + const xmlType = this.extractType49XmlTypeForStats(content) + if (xmlType === '2000') stats.transferMessages += 1 + if (xmlType === '2001') stats.redPacketMessages += 1 + } const createTime = this.getRowInt( row, @@ -2438,7 +2481,10 @@ class ChatService { voiceMessages: 0, imageMessages: 0, videoMessages: 0, - emojiMessages: 0 + emojiMessages: 0, + transferMessages: 0, + redPacketMessages: 0, + callMessages: 0 } if (sessionId.endsWith('@chatroom')) { stats.groupMyMessages = 0 @@ -2465,6 +2511,18 @@ class ChatService { const timeCol = this.pickFirstColumn(columnSet, ['create_time', 'createtime', 'msg_create_time', 'time']) const senderCol = this.pickFirstColumn(columnSet, ['sender_username', 'senderusername', 'sender']) const isSendCol = this.pickFirstColumn(columnSet, ['computed_is_send', 'computedissend', 'is_send', 'issend']) + const messageContentCol = this.pickFirstColumn(columnSet, ['message_content', 'messagecontent', 'msg_content', 'msgcontent', 'content']) + const compressContentCol = this.pickFirstColumn(columnSet, ['compress_content', 'compresscontent', 'compressed_content', 'compressedcontent']) + + const transferXmlConditions: string[] = [] + if (messageContentCol) transferXmlConditions.push(this.buildXmlTypeLikeExpr(messageContentCol, '2000')) + if (compressContentCol) transferXmlConditions.push(this.buildXmlTypeLikeExpr(compressContentCol, '2000')) + const transferXmlCond = transferXmlConditions.length > 0 ? `(${transferXmlConditions.join(' OR ')})` : '0' + + const redPacketXmlConditions: string[] = [] + if (messageContentCol) redPacketXmlConditions.push(this.buildXmlTypeLikeExpr(messageContentCol, '2001')) + if (compressContentCol) redPacketXmlConditions.push(this.buildXmlTypeLikeExpr(compressContentCol, '2001')) + const redPacketXmlCond = redPacketXmlConditions.length > 0 ? `(${redPacketXmlConditions.join(' OR ')})` : '0' const selectParts: string[] = [ 'COUNT(*) AS total_messages', @@ -2472,6 +2530,9 @@ class ChatService { typeCol ? `SUM(CASE WHEN ${this.quoteSqlIdentifier(typeCol)} = 3 THEN 1 ELSE 0 END) AS image_messages` : '0 AS image_messages', typeCol ? `SUM(CASE WHEN ${this.quoteSqlIdentifier(typeCol)} = 43 THEN 1 ELSE 0 END) AS video_messages` : '0 AS video_messages', typeCol ? `SUM(CASE WHEN ${this.quoteSqlIdentifier(typeCol)} = 47 THEN 1 ELSE 0 END) AS emoji_messages` : '0 AS emoji_messages', + typeCol ? `SUM(CASE WHEN ${this.quoteSqlIdentifier(typeCol)} = 50 THEN 1 ELSE 0 END) AS call_messages` : '0 AS call_messages', + typeCol ? `SUM(CASE WHEN ${this.quoteSqlIdentifier(typeCol)} = 8589934592049 THEN 1 WHEN ${this.quoteSqlIdentifier(typeCol)} = 49 AND ${transferXmlCond} THEN 1 ELSE 0 END) AS transfer_messages` : '0 AS transfer_messages', + typeCol ? `SUM(CASE WHEN ${this.quoteSqlIdentifier(typeCol)} = 8594229559345 THEN 1 WHEN ${this.quoteSqlIdentifier(typeCol)} = 49 AND ${redPacketXmlCond} THEN 1 ELSE 0 END) AS red_packet_messages` : '0 AS red_packet_messages', timeCol ? `MIN(${this.quoteSqlIdentifier(timeCol)}) AS first_timestamp` : 'NULL AS first_timestamp', timeCol ? `MAX(${this.quoteSqlIdentifier(timeCol)}) AS last_timestamp` : 'NULL AS last_timestamp' ] @@ -2509,6 +2570,9 @@ class ChatService { stats.imageMessages += this.getRowInt(aggregateRow, ['image_messages', 'imageMessages'], 0) stats.videoMessages += this.getRowInt(aggregateRow, ['video_messages', 'videoMessages'], 0) stats.emojiMessages += this.getRowInt(aggregateRow, ['emoji_messages', 'emojiMessages'], 0) + stats.callMessages += this.getRowInt(aggregateRow, ['call_messages', 'callMessages'], 0) + stats.transferMessages += this.getRowInt(aggregateRow, ['transfer_messages', 'transferMessages'], 0) + stats.redPacketMessages += this.getRowInt(aggregateRow, ['red_packet_messages', 'redPacketMessages'], 0) const firstTs = this.getRowInt(aggregateRow, ['first_timestamp', 'firstTimestamp'], 0) if (firstTs > 0 && (stats.firstTimestamp === undefined || firstTs < stats.firstTimestamp)) { @@ -2545,6 +2609,9 @@ class ChatService { stats.imageMessages += this.getRowInt(aggregateRow, ['image_messages', 'imageMessages'], 0) stats.videoMessages += this.getRowInt(aggregateRow, ['video_messages', 'videoMessages'], 0) stats.emojiMessages += this.getRowInt(aggregateRow, ['emoji_messages', 'emojiMessages'], 0) + stats.callMessages += this.getRowInt(aggregateRow, ['call_messages', 'callMessages'], 0) + stats.transferMessages += this.getRowInt(aggregateRow, ['transfer_messages', 'transferMessages'], 0) + stats.redPacketMessages += this.getRowInt(aggregateRow, ['red_packet_messages', 'redPacketMessages'], 0) const firstTs = this.getRowInt(aggregateRow, ['first_timestamp', 'firstTimestamp'], 0) if (firstTs > 0 && (stats.firstTimestamp === undefined || firstTs < stats.firstTimestamp)) { @@ -2640,7 +2707,10 @@ class ChatService { voiceMessages: 0, imageMessages: 0, videoMessages: 0, - emojiMessages: 0 + emojiMessages: 0, + transferMessages: 0, + redPacketMessages: 0, + callMessages: 0 } if (isGroup) { stats.groupMyMessages = 0 diff --git a/electron/services/sessionStatsCacheService.ts b/electron/services/sessionStatsCacheService.ts index 6af90f6..7115fd5 100644 --- a/electron/services/sessionStatsCacheService.ts +++ b/electron/services/sessionStatsCacheService.ts @@ -12,6 +12,9 @@ export interface SessionStatsCacheStats { imageMessages: number videoMessages: number emojiMessages: number + transferMessages: number + redPacketMessages: number + callMessages: number firstTimestamp?: number lastTimestamp?: number privateMutualGroups?: number @@ -50,6 +53,9 @@ function normalizeStats(raw: unknown): SessionStatsCacheStats | null { const imageMessages = toNonNegativeInt(source.imageMessages) const videoMessages = toNonNegativeInt(source.videoMessages) const emojiMessages = toNonNegativeInt(source.emojiMessages) + const transferMessages = toNonNegativeInt(source.transferMessages) ?? 0 + const redPacketMessages = toNonNegativeInt(source.redPacketMessages) ?? 0 + const callMessages = toNonNegativeInt(source.callMessages) ?? 0 if ( totalMessages === undefined || @@ -66,7 +72,10 @@ function normalizeStats(raw: unknown): SessionStatsCacheStats | null { voiceMessages, imageMessages, videoMessages, - emojiMessages + emojiMessages, + transferMessages, + redPacketMessages, + callMessages } const firstTimestamp = toNonNegativeInt(source.firstTimestamp) diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 2807565..b8b6def 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -215,6 +215,9 @@ interface SessionDetail { imageMessages?: number videoMessages?: number emojiMessages?: number + transferMessages?: number + redPacketMessages?: number + callMessages?: number privateMutualGroups?: number groupMemberCount?: number groupMyMessages?: number @@ -234,6 +237,9 @@ interface SessionExportMetric { imageMessages: number videoMessages: number emojiMessages: number + transferMessages: number + redPacketMessages: number + callMessages: number firstTimestamp?: number lastTimestamp?: number privateMutualGroups?: number @@ -787,6 +793,9 @@ function ChatPage(_props: ChatPageProps) { imageMessages: Number.isFinite(metric.imageMessages) ? metric.imageMessages : prev.imageMessages, videoMessages: Number.isFinite(metric.videoMessages) ? metric.videoMessages : prev.videoMessages, emojiMessages: Number.isFinite(metric.emojiMessages) ? metric.emojiMessages : prev.emojiMessages, + transferMessages: Number.isFinite(metric.transferMessages) ? metric.transferMessages : prev.transferMessages, + redPacketMessages: Number.isFinite(metric.redPacketMessages) ? metric.redPacketMessages : prev.redPacketMessages, + callMessages: Number.isFinite(metric.callMessages) ? metric.callMessages : prev.callMessages, groupMemberCount: Number.isFinite(metric.groupMemberCount) ? metric.groupMemberCount : prev.groupMemberCount, groupMyMessages: Number.isFinite(metric.groupMyMessages) ? metric.groupMyMessages : prev.groupMyMessages, groupActiveSpeakers: Number.isFinite(metric.groupActiveSpeakers) ? metric.groupActiveSpeakers : prev.groupActiveSpeakers, @@ -832,6 +841,9 @@ function ChatPage(_props: ChatPageProps) { imageMessages: sameSession ? prev?.imageMessages : undefined, videoMessages: sameSession ? prev?.videoMessages : undefined, emojiMessages: sameSession ? prev?.emojiMessages : undefined, + transferMessages: sameSession ? prev?.transferMessages : undefined, + redPacketMessages: sameSession ? prev?.redPacketMessages : undefined, + callMessages: sameSession ? prev?.callMessages : undefined, privateMutualGroups: sameSession ? prev?.privateMutualGroups : undefined, groupMemberCount: sameSession ? prev?.groupMemberCount : undefined, groupMyMessages: sameSession ? prev?.groupMyMessages : undefined, @@ -884,6 +896,9 @@ function ChatPage(_props: ChatPageProps) { imageMessages: prev?.imageMessages, videoMessages: prev?.videoMessages, emojiMessages: prev?.emojiMessages, + transferMessages: prev?.transferMessages, + redPacketMessages: prev?.redPacketMessages, + callMessages: prev?.callMessages, privateMutualGroups: prev?.privateMutualGroups, groupMemberCount: prev?.groupMemberCount, groupMyMessages: prev?.groupMyMessages, @@ -3712,6 +3727,30 @@ function ChatPage(_props: ChatPageProps) { : (isLoadingDetailExtra ? '统计中...' : '—')} +
+ 转账消息数 + + {Number.isFinite(sessionDetail.transferMessages) + ? (sessionDetail.transferMessages as number).toLocaleString() + : (isLoadingDetailExtra ? '统计中...' : '—')} + +
+
+ 红包消息数 + + {Number.isFinite(sessionDetail.redPacketMessages) + ? (sessionDetail.redPacketMessages as number).toLocaleString() + : (isLoadingDetailExtra ? '统计中...' : '—')} + +
+
+ 通话消息数 + + {Number.isFinite(sessionDetail.callMessages) + ? (sessionDetail.callMessages as number).toLocaleString() + : (isLoadingDetailExtra ? '统计中...' : '—')} + +
{sessionDetail.wxid.includes('@chatroom') ? ( <>
diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index f2f5657..6e76f45 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -506,6 +506,9 @@ interface SessionDetail { imageMessages?: number videoMessages?: number emojiMessages?: number + transferMessages?: number + redPacketMessages?: number + callMessages?: number privateMutualGroups?: number groupMemberCount?: number groupMyMessages?: number @@ -525,6 +528,9 @@ interface SessionExportMetric { imageMessages: number videoMessages: number emojiMessages: number + transferMessages: number + redPacketMessages: number + callMessages: number firstTimestamp?: number lastTimestamp?: number privateMutualGroups?: number @@ -540,6 +546,9 @@ interface SessionContentMetric { imageMessages?: number videoMessages?: number emojiMessages?: number + transferMessages?: number + redPacketMessages?: number + callMessages?: number } interface SessionContentStatsProgress { @@ -1458,13 +1467,19 @@ function ExportPage() { const imageMessages = normalizeMessageCount(metricRaw.imageMessages) const videoMessages = normalizeMessageCount(metricRaw.videoMessages) const emojiMessages = normalizeMessageCount(metricRaw.emojiMessages) + const transferMessages = normalizeMessageCount(metricRaw.transferMessages) + const redPacketMessages = normalizeMessageCount(metricRaw.redPacketMessages) + const callMessages = normalizeMessageCount(metricRaw.callMessages) if ( typeof totalMessages !== 'number' && typeof voiceMessages !== 'number' && typeof imageMessages !== 'number' && typeof videoMessages !== 'number' && - typeof emojiMessages !== 'number' + typeof emojiMessages !== 'number' && + typeof transferMessages !== 'number' && + typeof redPacketMessages !== 'number' && + typeof callMessages !== 'number' ) { continue } @@ -1474,7 +1489,10 @@ function ExportPage() { voiceMessages, imageMessages, videoMessages, - emojiMessages + emojiMessages, + transferMessages, + redPacketMessages, + callMessages } if (typeof totalMessages === 'number') { nextMessageCounts[sessionId] = totalMessages @@ -1505,14 +1523,20 @@ function ExportPage() { voiceMessages: typeof metric.voiceMessages === 'number' ? metric.voiceMessages : previous.voiceMessages, imageMessages: typeof metric.imageMessages === 'number' ? metric.imageMessages : previous.imageMessages, videoMessages: typeof metric.videoMessages === 'number' ? metric.videoMessages : previous.videoMessages, - emojiMessages: typeof metric.emojiMessages === 'number' ? metric.emojiMessages : previous.emojiMessages + emojiMessages: typeof metric.emojiMessages === 'number' ? metric.emojiMessages : previous.emojiMessages, + transferMessages: typeof metric.transferMessages === 'number' ? metric.transferMessages : previous.transferMessages, + redPacketMessages: typeof metric.redPacketMessages === 'number' ? metric.redPacketMessages : previous.redPacketMessages, + callMessages: typeof metric.callMessages === 'number' ? metric.callMessages : previous.callMessages } if ( previous.totalMessages === nextMetric.totalMessages && previous.voiceMessages === nextMetric.voiceMessages && previous.imageMessages === nextMetric.imageMessages && previous.videoMessages === nextMetric.videoMessages && - previous.emojiMessages === nextMetric.emojiMessages + previous.emojiMessages === nextMetric.emojiMessages && + previous.transferMessages === nextMetric.transferMessages && + previous.redPacketMessages === nextMetric.redPacketMessages && + previous.callMessages === nextMetric.callMessages ) { continue } @@ -3150,6 +3174,9 @@ function ExportPage() { imageMessages: Number.isFinite(metric.imageMessages) ? metric.imageMessages : prev.imageMessages, videoMessages: Number.isFinite(metric.videoMessages) ? metric.videoMessages : prev.videoMessages, emojiMessages: Number.isFinite(metric.emojiMessages) ? metric.emojiMessages : prev.emojiMessages, + transferMessages: Number.isFinite(metric.transferMessages) ? metric.transferMessages : prev.transferMessages, + redPacketMessages: Number.isFinite(metric.redPacketMessages) ? metric.redPacketMessages : prev.redPacketMessages, + callMessages: Number.isFinite(metric.callMessages) ? metric.callMessages : prev.callMessages, groupMemberCount: Number.isFinite(metric.groupMemberCount) ? metric.groupMemberCount : prev.groupMemberCount, groupMyMessages: Number.isFinite(metric.groupMyMessages) ? metric.groupMyMessages : prev.groupMyMessages, groupActiveSpeakers: Number.isFinite(metric.groupActiveSpeakers) ? metric.groupActiveSpeakers : prev.groupActiveSpeakers, @@ -3182,6 +3209,9 @@ function ExportPage() { const metricImage = normalizeMessageCount(cachedMetric?.imageMessages) const metricVideo = normalizeMessageCount(cachedMetric?.videoMessages) const metricEmoji = normalizeMessageCount(cachedMetric?.emojiMessages) + const metricTransfer = normalizeMessageCount(cachedMetric?.transferMessages) + const metricRedPacket = normalizeMessageCount(cachedMetric?.redPacketMessages) + const metricCall = normalizeMessageCount(cachedMetric?.callMessages) const hintedCount = typeof mappedSession?.messageCountHint === 'number' && Number.isFinite(mappedSession.messageCountHint) && mappedSession.messageCountHint >= 0 ? Math.floor(mappedSession.messageCountHint) : undefined @@ -3204,6 +3234,9 @@ function ExportPage() { imageMessages: metricImage ?? (sameSession ? prev?.imageMessages : undefined), videoMessages: metricVideo ?? (sameSession ? prev?.videoMessages : undefined), emojiMessages: metricEmoji ?? (sameSession ? prev?.emojiMessages : undefined), + transferMessages: metricTransfer ?? (sameSession ? prev?.transferMessages : undefined), + redPacketMessages: metricRedPacket ?? (sameSession ? prev?.redPacketMessages : undefined), + callMessages: metricCall ?? (sameSession ? prev?.callMessages : undefined), privateMutualGroups: sameSession ? prev?.privateMutualGroups : undefined, groupMemberCount: sameSession ? prev?.groupMemberCount : undefined, groupMyMessages: sameSession ? prev?.groupMyMessages : undefined, @@ -3236,6 +3269,9 @@ function ExportPage() { imageMessages: prev?.imageMessages, videoMessages: prev?.videoMessages, emojiMessages: prev?.emojiMessages, + transferMessages: prev?.transferMessages, + redPacketMessages: prev?.redPacketMessages, + callMessages: prev?.callMessages, privateMutualGroups: prev?.privateMutualGroups, groupMemberCount: prev?.groupMemberCount, groupMyMessages: prev?.groupMyMessages, @@ -4546,6 +4582,30 @@ function ExportPage() { : (isLoadingSessionDetailExtra ? '统计中...' : '—')}
+
+ 转账消息数 + + {Number.isFinite(sessionDetail.transferMessages) + ? (sessionDetail.transferMessages as number).toLocaleString() + : (isLoadingSessionDetailExtra ? '统计中...' : '—')} + +
+
+ 红包消息数 + + {Number.isFinite(sessionDetail.redPacketMessages) + ? (sessionDetail.redPacketMessages as number).toLocaleString() + : (isLoadingSessionDetailExtra ? '统计中...' : '—')} + +
+
+ 通话消息数 + + {Number.isFinite(sessionDetail.callMessages) + ? (sessionDetail.callMessages as number).toLocaleString() + : (isLoadingSessionDetailExtra ? '统计中...' : '—')} + +
{sessionDetail.wxid.includes('@chatroom') ? ( <>
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 5265c33..46d7c8a 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -251,6 +251,9 @@ export interface ElectronAPI { imageMessages: number videoMessages: number emojiMessages: number + transferMessages: number + redPacketMessages: number + callMessages: number firstTimestamp?: number lastTimestamp?: number privateMutualGroups?: number