fix(export): 修复导出页文件消息数量始终显示为 0
This commit is contained in:
1 parent
50ae72b639
commit
3a593b2f7c
5 files changed
+108
-5
No files matched your search
@@ -13,6 +13,7 @@ import { wcdbService } from './wcdbService'
|
||||
import { MessageCacheService } from './messageCacheService'
|
||||
import { ContactCacheService, ContactCacheEntry } from './contactCacheService'
|
||||
import { SessionStatsCacheService, SessionStatsCacheEntry, SessionStatsCacheStats } from './sessionStatsCacheService'
|
||||
import { FILE_APP_LOCAL_TYPE_SET } from './export/constants'
|
||||
import { GroupMyMessageCountCacheService, GroupMyMessageCountCacheEntry } from './groupMyMessageCountCacheService'
|
||||
import { exportCardDiagnosticsService } from './exportCardDiagnosticsService'
|
||||
import { voiceTranscribeService } from './voiceTranscribeService'
|
||||
@@ -181,6 +182,8 @@ interface ExportSessionStats {
|
||||
imageMessages: number
|
||||
videoMessages: number
|
||||
emojiMessages: number
|
||||
/** 文件类消息数量(包含 49 / 34359738417 / 103079215153 / 25769803825 等 localType 变体) */
|
||||
fileMessages: number
|
||||
transferMessages: number
|
||||
redPacketMessages: number
|
||||
callMessages: number
|
||||
@@ -4233,6 +4236,7 @@ class ChatService {
|
||||
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,
|
||||
fileMessages: Number.isFinite(stats.fileMessages) ? Math.max(0, Math.floor(stats.fileMessages)) : 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
|
||||
@@ -4256,6 +4260,7 @@ class ChatService {
|
||||
imageMessages: stats.imageMessages,
|
||||
videoMessages: stats.videoMessages,
|
||||
emojiMessages: stats.emojiMessages,
|
||||
fileMessages: stats.fileMessages,
|
||||
transferMessages: stats.transferMessages,
|
||||
redPacketMessages: stats.redPacketMessages,
|
||||
callMessages: stats.callMessages,
|
||||
@@ -4511,11 +4516,13 @@ class ChatService {
|
||||
transferMessages: number
|
||||
redPacketMessages: number
|
||||
callMessages: number
|
||||
fileMessages: number
|
||||
}> {
|
||||
const counters = {
|
||||
transferMessages: 0,
|
||||
redPacketMessages: 0,
|
||||
callMessages: 0
|
||||
callMessages: 0,
|
||||
fileMessages: 0
|
||||
}
|
||||
|
||||
const cursorResult = await wcdbService.openMessageCursor(sessionId, 500, false, beginTimestamp, endTimestamp)
|
||||
@@ -4551,6 +4558,7 @@ class ChatService {
|
||||
const xmlType = this.extractType49XmlTypeForStats(content)
|
||||
if (xmlType === '2000') counters.transferMessages += 1
|
||||
if (xmlType === '2001') counters.redPacketMessages += 1
|
||||
if (xmlType === '6') counters.fileMessages += 1
|
||||
}
|
||||
|
||||
if (!batch.hasMore || rows.length === 0) break
|
||||
@@ -4562,6 +4570,52 @@ class ChatService {
|
||||
return counters
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 cursor 扫描精确统计指定会话中的文件消息数量。
|
||||
*
|
||||
* 背景:Native 聚合接口 `getSessionMessageTypeStats` 通常只把 localType === 49 且
|
||||
* appmsg type === 6 的消息计为文件;但微信 4.x 中文件消息还存在
|
||||
* 34359738417 / 103079215153 / 25769803825 等 localType 变体,Native 不会计入。
|
||||
* 该函数覆盖 FILE_APP_LOCAL_TYPE_SET 中所有文件变体,作为 Native 统计的兜底/修正。
|
||||
*/
|
||||
private async collectFileMessageCountByCursorScan(
|
||||
sessionId: string,
|
||||
beginTimestamp: number = 0,
|
||||
endTimestamp: number = 0
|
||||
): Promise<number> {
|
||||
let count = 0
|
||||
const fileLocalTypeSet = FILE_APP_LOCAL_TYPE_SET
|
||||
const cursorResult = await wcdbService.openMessageCursor(sessionId, 500, false, beginTimestamp, endTimestamp)
|
||||
if (!cursorResult.success || !cursorResult.cursor) {
|
||||
return count
|
||||
}
|
||||
|
||||
const cursor = cursorResult.cursor
|
||||
try {
|
||||
while (true) {
|
||||
const batch = await wcdbService.fetchMessageBatch(cursor)
|
||||
if (!batch.success) break
|
||||
const rows = Array.isArray(batch.rows) ? batch.rows as Record<string, any>[] : []
|
||||
for (const row of rows) {
|
||||
const localType = this.getRowInt(row, ['local_type'], 1)
|
||||
if (!fileLocalTypeSet.has(localType)) continue
|
||||
|
||||
const rawMessageContent = row.message_content
|
||||
const rawCompressContent = row.compress_content
|
||||
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||
const xmlType = this.extractType49XmlTypeForStats(content)
|
||||
if (xmlType === '6') count += 1
|
||||
}
|
||||
|
||||
if (!batch.hasMore || rows.length === 0) break
|
||||
}
|
||||
} finally {
|
||||
await wcdbService.closeMessageCursor(cursor)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
private async collectSessionExportStatsByCursorScan(
|
||||
sessionId: string,
|
||||
selfIdentitySet: Set<string>,
|
||||
@@ -4574,6 +4628,7 @@ class ChatService {
|
||||
imageMessages: 0,
|
||||
videoMessages: 0,
|
||||
emojiMessages: 0,
|
||||
fileMessages: 0,
|
||||
transferMessages: 0,
|
||||
redPacketMessages: 0,
|
||||
callMessages: 0
|
||||
@@ -4609,13 +4664,16 @@ class ChatService {
|
||||
if (localType === 50) stats.callMessages += 1
|
||||
if (localType === 8589934592049) stats.transferMessages += 1
|
||||
if (localType === 8594229559345) stats.redPacketMessages += 1
|
||||
if (localType === 49) {
|
||||
// 文件、转账、红包等共享 appmsg 类型的 localType 变体,需要解析 XML 进一步区分
|
||||
if (FILE_APP_LOCAL_TYPE_SET.has(localType)) {
|
||||
const rawMessageContent = row.message_content
|
||||
const rawCompressContent = row.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
|
||||
// appmsg type === 6 表示普通文件附件
|
||||
if (xmlType === '6') stats.fileMessages += 1
|
||||
}
|
||||
|
||||
const createTime = this.getRowInt(
|
||||
@@ -4679,6 +4737,7 @@ class ChatService {
|
||||
imageMessages: 0,
|
||||
videoMessages: 0,
|
||||
emojiMessages: 0,
|
||||
fileMessages: 0,
|
||||
transferMessages: 0,
|
||||
redPacketMessages: 0,
|
||||
callMessages: 0
|
||||
@@ -4700,6 +4759,7 @@ class ChatService {
|
||||
stats.imageMessages = Math.max(0, Math.floor(Number(data.image_messages || 0)))
|
||||
stats.videoMessages = Math.max(0, Math.floor(Number(data.video_messages || 0)))
|
||||
stats.emojiMessages = Math.max(0, Math.floor(Number(data.emoji_messages || 0)))
|
||||
stats.fileMessages = Math.max(0, Math.floor(Number(data.file_messages || 0)))
|
||||
stats.callMessages = Math.max(0, Math.floor(Number(data.call_messages || 0)))
|
||||
stats.transferMessages = Math.max(0, Math.floor(Number(data.transfer_messages || 0)))
|
||||
stats.redPacketMessages = Math.max(0, Math.floor(Number(data.red_packet_messages || 0)))
|
||||
@@ -4715,9 +4775,23 @@ class ChatService {
|
||||
stats.transferMessages = preciseCounters.transferMessages
|
||||
stats.redPacketMessages = preciseCounters.redPacketMessages
|
||||
stats.callMessages = preciseCounters.callMessages
|
||||
stats.fileMessages = preciseCounters.fileMessages
|
||||
} catch {
|
||||
// 保留 native 聚合结果作为兜底
|
||||
}
|
||||
} else {
|
||||
// Native 聚合可能不识别 25769803825 等文件消息变体,返回 0 但 key 存在。
|
||||
// 当 Native 返回的文件消息数为 0 时,通过轻量 cursor 扫描补充,避免漏统计。
|
||||
// 仅在 0 时兜底是为了兼顾性能:若 Native 已给出非 0 结果,通常已覆盖常见文件消息。
|
||||
const nativeFileMessages = Math.max(0, Math.floor(Number(data.file_messages || 0)))
|
||||
if (nativeFileMessages === 0) {
|
||||
try {
|
||||
const cursorFileMessages = await this.collectFileMessageCountByCursorScan(sessionId, beginTimestamp, endTimestamp)
|
||||
stats.fileMessages = Math.max(nativeFileMessages, cursorFileMessages)
|
||||
} catch {
|
||||
// 保留 native 聚合结果作为兜底
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
@@ -4742,6 +4816,7 @@ class ChatService {
|
||||
imageMessages: Math.max(0, Math.floor(Number(row?.image_messages || 0))),
|
||||
videoMessages: Math.max(0, Math.floor(Number(row?.video_messages || 0))),
|
||||
emojiMessages: Math.max(0, Math.floor(Number(row?.emoji_messages || 0))),
|
||||
fileMessages: Math.max(0, Math.floor(Number(row?.file_messages || 0))),
|
||||
callMessages: Math.max(0, Math.floor(Number(row?.call_messages || 0))),
|
||||
transferMessages: Math.max(0, Math.floor(Number(row?.transfer_messages || 0))),
|
||||
redPacketMessages: Math.max(0, Math.floor(Number(row?.red_packet_messages || 0)))
|
||||
@@ -4864,6 +4939,7 @@ class ChatService {
|
||||
imageMessages: 0,
|
||||
videoMessages: 0,
|
||||
emojiMessages: 0,
|
||||
fileMessages: 0,
|
||||
transferMessages: 0,
|
||||
redPacketMessages: 0,
|
||||
callMessages: 0
|
||||
@@ -5021,7 +5097,8 @@ class ChatService {
|
||||
|
||||
await this.forEachWithConcurrency(normalizedSessionIds, 3, async (sessionId) => {
|
||||
try {
|
||||
const stats = hasNativeBatchStats && nativeBatchStats[sessionId]
|
||||
const fromNativeBatch = hasNativeBatchStats && nativeBatchStats[sessionId]
|
||||
const stats = fromNativeBatch
|
||||
? { ...nativeBatchStats[sessionId] }
|
||||
: await this.collectSessionExportStats(
|
||||
sessionId,
|
||||
@@ -5030,6 +5107,16 @@ class ChatService {
|
||||
beginTimestamp,
|
||||
endTimestamp
|
||||
)
|
||||
// Native 批量统计可能不识别文件消息变体(如 25769803825),对 fileMessages 为 0 的会话补充 cursor 扫描。
|
||||
// 导出页同时加载多个会话时走批量路径,这里需要与单会话路径保持一致的兜底策略。
|
||||
if (fromNativeBatch && stats.fileMessages === 0) {
|
||||
try {
|
||||
const cursorFileMessages = await this.collectFileMessageCountByCursorScan(sessionId, beginTimestamp, endTimestamp)
|
||||
stats.fileMessages = Math.max(stats.fileMessages, cursorFileMessages)
|
||||
} catch {
|
||||
// 保留 native 批量结果作为兜底
|
||||
}
|
||||
}
|
||||
if (sessionId.endsWith('@chatroom')) {
|
||||
if (shouldLoadGroupMemberCount) {
|
||||
stats.groupMemberCount = typeof memberCountMap[sessionId] === 'number'
|
||||
|
||||
@@ -2,7 +2,8 @@ import { join, dirname } from 'path'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
||||
import { ConfigService } from './config'
|
||||
|
||||
const CACHE_VERSION = 2
|
||||
/** 缓存版本号。增加/修改 SessionStatsCacheStats 字段后必须提升,避免旧缓存被误用。 */
|
||||
const CACHE_VERSION = 4
|
||||
const MAX_SESSION_ENTRIES_PER_SCOPE = 2000
|
||||
const MAX_SCOPE_ENTRIES = 12
|
||||
|
||||
@@ -12,6 +13,8 @@ export interface SessionStatsCacheStats {
|
||||
imageMessages: number
|
||||
videoMessages: number
|
||||
emojiMessages: number
|
||||
/** 文件类消息数量(对应 ExportSessionStats.fileMessages) */
|
||||
fileMessages: number
|
||||
transferMessages: number
|
||||
redPacketMessages: number
|
||||
callMessages: number
|
||||
@@ -53,6 +56,7 @@ function normalizeStats(raw: unknown): SessionStatsCacheStats | null {
|
||||
const imageMessages = toNonNegativeInt(source.imageMessages)
|
||||
const videoMessages = toNonNegativeInt(source.videoMessages)
|
||||
const emojiMessages = toNonNegativeInt(source.emojiMessages)
|
||||
const fileMessages = toNonNegativeInt(source.fileMessages)
|
||||
const transferMessages = toNonNegativeInt(source.transferMessages)
|
||||
const redPacketMessages = toNonNegativeInt(source.redPacketMessages)
|
||||
const callMessages = toNonNegativeInt(source.callMessages)
|
||||
@@ -63,6 +67,7 @@ function normalizeStats(raw: unknown): SessionStatsCacheStats | null {
|
||||
imageMessages === undefined ||
|
||||
videoMessages === undefined ||
|
||||
emojiMessages === undefined ||
|
||||
fileMessages === undefined ||
|
||||
transferMessages === undefined ||
|
||||
redPacketMessages === undefined ||
|
||||
callMessages === undefined
|
||||
@@ -76,6 +81,7 @@ function normalizeStats(raw: unknown): SessionStatsCacheStats | null {
|
||||
imageMessages,
|
||||
videoMessages,
|
||||
emojiMessages,
|
||||
fileMessages,
|
||||
transferMessages,
|
||||
redPacketMessages,
|
||||
callMessages
|
||||
|
||||
@@ -56,7 +56,7 @@ export const useSessionMetrics = create<SessionMetricsState>((set, get) => ({
|
||||
imageMessages: sessionStat.imageMessages,
|
||||
videoMessages: sessionStat.videoMessages,
|
||||
emojiMessages: sessionStat.emojiMessages,
|
||||
fileMessages: 0 // Will need specific metric if available
|
||||
fileMessages: sessionStat.fileMessages ?? 0 // 文件消息数量,由后端 ExportSessionStats 返回
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,6 +931,8 @@ export interface ExportSessionContentMetricCacheEntry {
|
||||
imageMessages?: number
|
||||
videoMessages?: number
|
||||
emojiMessages?: number
|
||||
/** 文件类消息数量,与后端 ExportSessionStats.fileMessages 对齐 */
|
||||
fileMessages?: number
|
||||
firstTimestamp?: number
|
||||
lastTimestamp?: number
|
||||
}
|
||||
@@ -1099,6 +1101,9 @@ export async function getExportSessionContentMetricCache(scopeKey: string): Prom
|
||||
if (typeof source.emojiMessages === 'number' && Number.isFinite(source.emojiMessages) && source.emojiMessages >= 0) {
|
||||
metric.emojiMessages = Math.floor(source.emojiMessages)
|
||||
}
|
||||
if (typeof source.fileMessages === 'number' && Number.isFinite(source.fileMessages) && source.fileMessages >= 0) {
|
||||
metric.fileMessages = Math.floor(source.fileMessages)
|
||||
}
|
||||
if (typeof source.firstTimestamp === 'number' && Number.isFinite(source.firstTimestamp) && source.firstTimestamp > 0) {
|
||||
metric.firstTimestamp = Math.floor(source.firstTimestamp)
|
||||
}
|
||||
@@ -1144,6 +1149,9 @@ export async function setExportSessionContentMetricCache(
|
||||
if (typeof rawMetric.emojiMessages === 'number' && Number.isFinite(rawMetric.emojiMessages) && rawMetric.emojiMessages >= 0) {
|
||||
metric.emojiMessages = Math.floor(rawMetric.emojiMessages)
|
||||
}
|
||||
if (typeof rawMetric.fileMessages === 'number' && Number.isFinite(rawMetric.fileMessages) && rawMetric.fileMessages >= 0) {
|
||||
metric.fileMessages = Math.floor(rawMetric.fileMessages)
|
||||
}
|
||||
if (typeof rawMetric.firstTimestamp === 'number' && Number.isFinite(rawMetric.firstTimestamp) && rawMetric.firstTimestamp > 0) {
|
||||
metric.firstTimestamp = Math.floor(rawMetric.firstTimestamp)
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -644,6 +644,8 @@ export interface ElectronAPI {
|
||||
imageMessages: number
|
||||
videoMessages: number
|
||||
emojiMessages: number
|
||||
/** 文件类消息数量 */
|
||||
fileMessages: number
|
||||
transferMessages: number
|
||||
redPacketMessages: number
|
||||
callMessages: number
|
||||
|
||||
Reference in new issue
Block a user