mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(export): improve count accuracy and include pending updates
This commit is contained in:
@@ -1061,6 +1061,10 @@ function registerIpcHandlers() {
|
|||||||
return chatService.getExportSessionStats(sessionIds, options)
|
return chatService.getExportSessionStats(sessionIds, options)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getGroupMyMessageCountHint', async (_, chatroomId: string) => {
|
||||||
|
return chatService.getGroupMyMessageCountHint(chatroomId)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('chat:getImageData', async (_, sessionId: string, msgId: string) => {
|
ipcMain.handle('chat:getImageData', async (_, sessionId: string, msgId: string) => {
|
||||||
return chatService.getImageData(sessionId, msgId)
|
return chatService.getImageData(sessionId, msgId)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
sessionIds: string[],
|
sessionIds: string[],
|
||||||
options?: { includeRelations?: boolean; forceRefresh?: boolean; allowStaleCache?: boolean }
|
options?: { includeRelations?: boolean; forceRefresh?: boolean; allowStaleCache?: boolean }
|
||||||
) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds, options),
|
) => ipcRenderer.invoke('chat:getExportSessionStats', sessionIds, options),
|
||||||
|
getGroupMyMessageCountHint: (chatroomId: string) =>
|
||||||
|
ipcRenderer.invoke('chat:getGroupMyMessageCountHint', chatroomId),
|
||||||
getImageData: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:getImageData', sessionId, msgId),
|
getImageData: (sessionId: string, msgId: string) => ipcRenderer.invoke('chat:getImageData', sessionId, msgId),
|
||||||
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>
|
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>
|
||||||
ipcRenderer.invoke('chat:getVoiceData', sessionId, msgId, createTime, serverId),
|
ipcRenderer.invoke('chat:getVoiceData', sessionId, msgId, createTime, serverId),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { wcdbService } from './wcdbService'
|
|||||||
import { MessageCacheService } from './messageCacheService'
|
import { MessageCacheService } from './messageCacheService'
|
||||||
import { ContactCacheService, ContactCacheEntry } from './contactCacheService'
|
import { ContactCacheService, ContactCacheEntry } from './contactCacheService'
|
||||||
import { SessionStatsCacheService, SessionStatsCacheEntry, SessionStatsCacheStats } from './sessionStatsCacheService'
|
import { SessionStatsCacheService, SessionStatsCacheEntry, SessionStatsCacheStats } from './sessionStatsCacheService'
|
||||||
|
import { GroupMyMessageCountCacheService, GroupMyMessageCountCacheEntry } from './groupMyMessageCountCacheService'
|
||||||
import {
|
import {
|
||||||
ExportContentScopeStatsEntry,
|
ExportContentScopeStatsEntry,
|
||||||
ExportContentSessionStatsEntry,
|
ExportContentSessionStatsEntry,
|
||||||
@@ -226,6 +227,7 @@ class ChatService {
|
|||||||
private readonly contactCacheService: ContactCacheService
|
private readonly contactCacheService: ContactCacheService
|
||||||
private readonly messageCacheService: MessageCacheService
|
private readonly messageCacheService: MessageCacheService
|
||||||
private readonly sessionStatsCacheService: SessionStatsCacheService
|
private readonly sessionStatsCacheService: SessionStatsCacheService
|
||||||
|
private readonly groupMyMessageCountCacheService: GroupMyMessageCountCacheService
|
||||||
private readonly exportContentStatsCacheService: ExportContentStatsCacheService
|
private readonly exportContentStatsCacheService: ExportContentStatsCacheService
|
||||||
private voiceWavCache: LRUCache<string, Buffer>
|
private voiceWavCache: LRUCache<string, Buffer>
|
||||||
private voiceTranscriptCache: LRUCache<string, string>
|
private voiceTranscriptCache: LRUCache<string, string>
|
||||||
@@ -265,6 +267,8 @@ class ChatService {
|
|||||||
private allGroupSessionIdsCache: { ids: string[]; updatedAt: number } | null = null
|
private allGroupSessionIdsCache: { ids: string[]; updatedAt: number } | null = null
|
||||||
private readonly sessionStatsCacheTtlMs = 10 * 60 * 1000
|
private readonly sessionStatsCacheTtlMs = 10 * 60 * 1000
|
||||||
private readonly allGroupSessionIdsCacheTtlMs = 5 * 60 * 1000
|
private readonly allGroupSessionIdsCacheTtlMs = 5 * 60 * 1000
|
||||||
|
private groupMyMessageCountCacheScope = ''
|
||||||
|
private groupMyMessageCountMemoryCache = new Map<string, GroupMyMessageCountCacheEntry>()
|
||||||
private exportContentStatsScope = ''
|
private exportContentStatsScope = ''
|
||||||
private exportContentStatsMemory = new Map<string, ExportContentSessionStatsEntry>()
|
private exportContentStatsMemory = new Map<string, ExportContentSessionStatsEntry>()
|
||||||
private exportContentStatsScopeUpdatedAt = 0
|
private exportContentStatsScopeUpdatedAt = 0
|
||||||
@@ -282,6 +286,7 @@ class ChatService {
|
|||||||
this.avatarCache = new Map(Object.entries(persisted))
|
this.avatarCache = new Map(Object.entries(persisted))
|
||||||
this.messageCacheService = new MessageCacheService(this.configService.getCacheBasePath())
|
this.messageCacheService = new MessageCacheService(this.configService.getCacheBasePath())
|
||||||
this.sessionStatsCacheService = new SessionStatsCacheService(this.configService.getCacheBasePath())
|
this.sessionStatsCacheService = new SessionStatsCacheService(this.configService.getCacheBasePath())
|
||||||
|
this.groupMyMessageCountCacheService = new GroupMyMessageCountCacheService(this.configService.getCacheBasePath())
|
||||||
this.exportContentStatsCacheService = new ExportContentStatsCacheService(this.configService.getCacheBasePath())
|
this.exportContentStatsCacheService = new ExportContentStatsCacheService(this.configService.getCacheBasePath())
|
||||||
// 初始化LRU缓存,限制大小防止内存泄漏
|
// 初始化LRU缓存,限制大小防止内存泄漏
|
||||||
this.voiceWavCache = new LRUCache(this.voiceWavCacheMaxEntries)
|
this.voiceWavCache = new LRUCache(this.voiceWavCacheMaxEntries)
|
||||||
@@ -901,7 +906,10 @@ class ChatService {
|
|||||||
/**
|
/**
|
||||||
* 批量获取会话消息总数(轻量接口,用于列表优先排序)
|
* 批量获取会话消息总数(轻量接口,用于列表优先排序)
|
||||||
*/
|
*/
|
||||||
async getSessionMessageCounts(sessionIds: string[]): Promise<{
|
async getSessionMessageCounts(
|
||||||
|
sessionIds: string[],
|
||||||
|
options?: { preferHintCache?: boolean; bypassSessionCache?: boolean }
|
||||||
|
): Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
counts?: Record<string, number>
|
counts?: Record<string, number>
|
||||||
error?: string
|
error?: string
|
||||||
@@ -923,18 +931,24 @@ class ChatService {
|
|||||||
return { success: true, counts: {} }
|
return { success: true, counts: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preferHintCache = options?.preferHintCache !== false
|
||||||
|
const bypassSessionCache = options?.bypassSessionCache === true
|
||||||
|
|
||||||
this.refreshSessionMessageCountCacheScope()
|
this.refreshSessionMessageCountCacheScope()
|
||||||
const counts: Record<string, number> = {}
|
const counts: Record<string, number> = {}
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const pendingSessionIds: string[] = []
|
const pendingSessionIds: string[] = []
|
||||||
|
|
||||||
for (const sessionId of normalizedSessionIds) {
|
for (const sessionId of normalizedSessionIds) {
|
||||||
|
if (!bypassSessionCache) {
|
||||||
const cached = this.sessionMessageCountCache.get(sessionId)
|
const cached = this.sessionMessageCountCache.get(sessionId)
|
||||||
if (cached && now - cached.updatedAt <= this.sessionMessageCountCacheTtlMs) {
|
if (cached && now - cached.updatedAt <= this.sessionMessageCountCacheTtlMs) {
|
||||||
counts[sessionId] = cached.count
|
counts[sessionId] = cached.count
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferHintCache) {
|
||||||
const hintCount = this.sessionMessageCountHintCache.get(sessionId)
|
const hintCount = this.sessionMessageCountHintCache.get(sessionId)
|
||||||
if (typeof hintCount === 'number' && Number.isFinite(hintCount) && hintCount >= 0) {
|
if (typeof hintCount === 'number' && Number.isFinite(hintCount) && hintCount >= 0) {
|
||||||
counts[sessionId] = Math.floor(hintCount)
|
counts[sessionId] = Math.floor(hintCount)
|
||||||
@@ -942,11 +956,13 @@ class ChatService {
|
|||||||
count: Math.floor(hintCount),
|
count: Math.floor(hintCount),
|
||||||
updatedAt: now
|
updatedAt: now
|
||||||
})
|
})
|
||||||
} else {
|
continue
|
||||||
pendingSessionIds.push(sessionId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pendingSessionIds.push(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
const batchSize = 320
|
const batchSize = 320
|
||||||
for (let i = 0; i < pendingSessionIds.length; i += batchSize) {
|
for (let i = 0; i < pendingSessionIds.length; i += batchSize) {
|
||||||
const batch = pendingSessionIds.slice(i, i + batchSize)
|
const batch = pendingSessionIds.slice(i, i + batchSize)
|
||||||
@@ -1618,6 +1634,7 @@ class ChatService {
|
|||||||
const scope = `${dbPath}::${myWxid}`
|
const scope = `${dbPath}::${myWxid}`
|
||||||
if (scope === this.sessionMessageCountCacheScope) {
|
if (scope === this.sessionMessageCountCacheScope) {
|
||||||
this.refreshSessionStatsCacheScope(scope)
|
this.refreshSessionStatsCacheScope(scope)
|
||||||
|
this.refreshGroupMyMessageCountCacheScope(scope)
|
||||||
this.refreshExportContentStatsScope(scope)
|
this.refreshExportContentStatsScope(scope)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1628,9 +1645,16 @@ class ChatService {
|
|||||||
this.sessionDetailExtraCache.clear()
|
this.sessionDetailExtraCache.clear()
|
||||||
this.sessionStatusCache.clear()
|
this.sessionStatusCache.clear()
|
||||||
this.refreshSessionStatsCacheScope(scope)
|
this.refreshSessionStatsCacheScope(scope)
|
||||||
|
this.refreshGroupMyMessageCountCacheScope(scope)
|
||||||
this.refreshExportContentStatsScope(scope)
|
this.refreshExportContentStatsScope(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private refreshGroupMyMessageCountCacheScope(scope: string): void {
|
||||||
|
if (scope === this.groupMyMessageCountCacheScope) return
|
||||||
|
this.groupMyMessageCountCacheScope = scope
|
||||||
|
this.groupMyMessageCountMemoryCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
private refreshExportContentStatsScope(scope: string): void {
|
private refreshExportContentStatsScope(scope: string): void {
|
||||||
if (scope === this.exportContentStatsScope) return
|
if (scope === this.exportContentStatsScope) return
|
||||||
this.exportContentStatsScope = scope
|
this.exportContentStatsScope = scope
|
||||||
@@ -1718,7 +1742,7 @@ class ChatService {
|
|||||||
return {
|
return {
|
||||||
...entry,
|
...entry,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
mediaReady: true
|
mediaReady: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1783,9 +1807,18 @@ class ChatService {
|
|||||||
|
|
||||||
if (targets.length > 0) {
|
if (targets.length > 0) {
|
||||||
await this.forEachWithConcurrency(targets, 3, async (sessionId) => {
|
await this.forEachWithConcurrency(targets, 3, async (sessionId) => {
|
||||||
|
try {
|
||||||
const nextEntry = await this.collectExportContentEntry(sessionId)
|
const nextEntry = await this.collectExportContentEntry(sessionId)
|
||||||
this.exportContentStatsMemory.set(sessionId, nextEntry)
|
this.exportContentStatsMemory.set(sessionId, nextEntry)
|
||||||
|
if (nextEntry.mediaReady) {
|
||||||
this.exportContentStatsDirtySessionIds.delete(sessionId)
|
this.exportContentStatsDirtySessionIds.delete(sessionId)
|
||||||
|
} else {
|
||||||
|
this.exportContentStatsDirtySessionIds.add(sessionId)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ChatService: 刷新导出内容会话统计失败:', sessionId, error)
|
||||||
|
this.exportContentStatsDirtySessionIds.add(sessionId)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1850,7 +1883,7 @@ class ChatService {
|
|||||||
if (entry) {
|
if (entry) {
|
||||||
if (entry.hasAny) {
|
if (entry.hasAny) {
|
||||||
textSessions += 1
|
textSessions += 1
|
||||||
} else if (this.isExportContentEntryDirty(sessionId)) {
|
} else if (forceRefresh || this.isExportContentEntryDirty(sessionId)) {
|
||||||
missingTextCountSessionIds.push(sessionId)
|
missingTextCountSessionIds.push(sessionId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1873,7 +1906,10 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (missingTextCountSessionIds.length > 0) {
|
if (missingTextCountSessionIds.length > 0) {
|
||||||
const textCountResult = await this.getSessionMessageCounts(missingTextCountSessionIds)
|
const textCountResult = await this.getSessionMessageCounts(missingTextCountSessionIds, {
|
||||||
|
preferHintCache: false,
|
||||||
|
bypassSessionCache: true
|
||||||
|
})
|
||||||
if (textCountResult.success && textCountResult.counts) {
|
if (textCountResult.success && textCountResult.counts) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
for (const sessionId of missingTextCountSessionIds) {
|
for (const sessionId of missingTextCountSessionIds) {
|
||||||
@@ -1948,6 +1984,43 @@ class ChatService {
|
|||||||
return `${this.sessionStatsCacheScope}::${sessionId}`
|
return `${this.sessionStatsCacheScope}::${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildScopedGroupMyMessageCountKey(chatroomId: string): string {
|
||||||
|
return `${this.groupMyMessageCountCacheScope}::${chatroomId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGroupMyMessageCountHintEntry(
|
||||||
|
chatroomId: string
|
||||||
|
): { entry: GroupMyMessageCountCacheEntry; source: 'memory' | 'disk' } | null {
|
||||||
|
const scopedKey = this.buildScopedGroupMyMessageCountKey(chatroomId)
|
||||||
|
const inMemory = this.groupMyMessageCountMemoryCache.get(scopedKey)
|
||||||
|
if (inMemory) {
|
||||||
|
return { entry: inMemory, source: 'memory' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = this.groupMyMessageCountCacheService.get(this.groupMyMessageCountCacheScope, chatroomId)
|
||||||
|
if (!persisted) return null
|
||||||
|
this.groupMyMessageCountMemoryCache.set(scopedKey, persisted)
|
||||||
|
return { entry: persisted, source: 'disk' }
|
||||||
|
}
|
||||||
|
|
||||||
|
private setGroupMyMessageCountHintEntry(chatroomId: string, messageCount: number, updatedAt?: number): number {
|
||||||
|
const nextCount = Number.isFinite(messageCount) ? Math.max(0, Math.floor(messageCount)) : 0
|
||||||
|
const nextUpdatedAt = Number.isFinite(updatedAt) ? Math.max(0, Math.floor(updatedAt as number)) : Date.now()
|
||||||
|
const scopedKey = this.buildScopedGroupMyMessageCountKey(chatroomId)
|
||||||
|
const existing = this.groupMyMessageCountMemoryCache.get(scopedKey)
|
||||||
|
if (existing && existing.updatedAt > nextUpdatedAt) {
|
||||||
|
return existing.updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: GroupMyMessageCountCacheEntry = {
|
||||||
|
updatedAt: nextUpdatedAt,
|
||||||
|
messageCount: nextCount
|
||||||
|
}
|
||||||
|
this.groupMyMessageCountMemoryCache.set(scopedKey, entry)
|
||||||
|
this.groupMyMessageCountCacheService.set(this.groupMyMessageCountCacheScope, chatroomId, entry)
|
||||||
|
return nextUpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
private toSessionStatsCacheStats(stats: ExportSessionStats): SessionStatsCacheStats {
|
private toSessionStatsCacheStats(stats: ExportSessionStats): SessionStatsCacheStats {
|
||||||
const normalized: SessionStatsCacheStats = {
|
const normalized: SessionStatsCacheStats = {
|
||||||
totalMessages: Number.isFinite(stats.totalMessages) ? Math.max(0, Math.floor(stats.totalMessages)) : 0,
|
totalMessages: Number.isFinite(stats.totalMessages) ? Math.max(0, Math.floor(stats.totalMessages)) : 0,
|
||||||
@@ -2005,14 +2078,18 @@ class ChatService {
|
|||||||
|
|
||||||
private setSessionStatsCacheEntry(sessionId: string, stats: ExportSessionStats, includeRelations: boolean): number {
|
private setSessionStatsCacheEntry(sessionId: string, stats: ExportSessionStats, includeRelations: boolean): number {
|
||||||
const updatedAt = Date.now()
|
const updatedAt = Date.now()
|
||||||
|
const normalizedStats = this.toSessionStatsCacheStats(stats)
|
||||||
const entry: SessionStatsCacheEntry = {
|
const entry: SessionStatsCacheEntry = {
|
||||||
updatedAt,
|
updatedAt,
|
||||||
includeRelations,
|
includeRelations,
|
||||||
stats: this.toSessionStatsCacheStats(stats)
|
stats: normalizedStats
|
||||||
}
|
}
|
||||||
const scopedKey = this.buildScopedSessionStatsKey(sessionId)
|
const scopedKey = this.buildScopedSessionStatsKey(sessionId)
|
||||||
this.sessionStatsMemoryCache.set(scopedKey, entry)
|
this.sessionStatsMemoryCache.set(scopedKey, entry)
|
||||||
this.sessionStatsCacheService.set(this.sessionStatsCacheScope, sessionId, entry)
|
this.sessionStatsCacheService.set(this.sessionStatsCacheScope, sessionId, entry)
|
||||||
|
if (sessionId.endsWith('@chatroom') && Number.isFinite(normalizedStats.groupMyMessages)) {
|
||||||
|
this.setGroupMyMessageCountHintEntry(sessionId, normalizedStats.groupMyMessages as number, updatedAt)
|
||||||
|
}
|
||||||
return updatedAt
|
return updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2217,6 +2294,9 @@ class ChatService {
|
|||||||
|
|
||||||
if (sessionId.endsWith('@chatroom')) {
|
if (sessionId.endsWith('@chatroom')) {
|
||||||
stats.groupActiveSpeakers = senderIdentities.size
|
stats.groupActiveSpeakers = senderIdentities.size
|
||||||
|
if (Number.isFinite(stats.groupMyMessages)) {
|
||||||
|
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
@@ -4264,6 +4344,8 @@ class ChatService {
|
|||||||
this.sessionStatsPendingFull.clear()
|
this.sessionStatsPendingFull.clear()
|
||||||
this.allGroupSessionIdsCache = null
|
this.allGroupSessionIdsCache = null
|
||||||
this.sessionStatsCacheService.clearAll()
|
this.sessionStatsCacheService.clearAll()
|
||||||
|
this.groupMyMessageCountMemoryCache.clear()
|
||||||
|
this.groupMyMessageCountCacheService.clearAll()
|
||||||
this.exportContentStatsMemory.clear()
|
this.exportContentStatsMemory.clear()
|
||||||
this.exportContentStatsDirtySessionIds.clear()
|
this.exportContentStatsDirtySessionIds.clear()
|
||||||
this.exportContentScopeSessionIdsCache = null
|
this.exportContentScopeSessionIdsCache = null
|
||||||
@@ -4707,6 +4789,51 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getGroupMyMessageCountHint(chatroomId: string): Promise<{
|
||||||
|
success: boolean
|
||||||
|
count?: number
|
||||||
|
updatedAt?: number
|
||||||
|
source?: 'memory' | 'disk'
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
this.refreshSessionMessageCountCacheScope()
|
||||||
|
const normalizedChatroomId = String(chatroomId || '').trim()
|
||||||
|
if (!normalizedChatroomId || !normalizedChatroomId.endsWith('@chatroom')) {
|
||||||
|
return { success: false, error: '群聊ID无效' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.getGroupMyMessageCountHintEntry(normalizedChatroomId)
|
||||||
|
if (!cached) return { success: true }
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: cached.entry.messageCount,
|
||||||
|
updatedAt: cached.entry.updatedAt,
|
||||||
|
source: cached.source
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGroupMyMessageCountHint(
|
||||||
|
chatroomId: string,
|
||||||
|
messageCount: number,
|
||||||
|
updatedAt?: number
|
||||||
|
): Promise<{ success: boolean; updatedAt?: number; error?: string }> {
|
||||||
|
try {
|
||||||
|
this.refreshSessionMessageCountCacheScope()
|
||||||
|
const normalizedChatroomId = String(chatroomId || '').trim()
|
||||||
|
if (!normalizedChatroomId || !normalizedChatroomId.endsWith('@chatroom')) {
|
||||||
|
return { success: false, error: '群聊ID无效' }
|
||||||
|
}
|
||||||
|
const savedAt = this.setGroupMyMessageCountHintEntry(normalizedChatroomId, messageCount, updatedAt)
|
||||||
|
return { success: true, updatedAt: savedAt }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getExportSessionStats(sessionIds: string[], options: ExportSessionStatsOptions = {}): Promise<{
|
async getExportSessionStats(sessionIds: string[], options: ExportSessionStatsOptions = {}): Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
data?: Record<string, ExportSessionStats>
|
data?: Record<string, ExportSessionStats>
|
||||||
@@ -4743,12 +4870,18 @@ class ChatService {
|
|||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
for (const sessionId of normalizedSessionIds) {
|
for (const sessionId of normalizedSessionIds) {
|
||||||
|
const groupMyMessagesHint = sessionId.endsWith('@chatroom')
|
||||||
|
? this.getGroupMyMessageCountHintEntry(sessionId)
|
||||||
|
: null
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
||||||
if (cachedResult && this.supportsRequestedRelation(cachedResult.entry, includeRelations)) {
|
if (cachedResult && this.supportsRequestedRelation(cachedResult.entry, includeRelations)) {
|
||||||
const stale = now - cachedResult.entry.updatedAt > this.sessionStatsCacheTtlMs
|
const stale = now - cachedResult.entry.updatedAt > this.sessionStatsCacheTtlMs
|
||||||
if (!stale || allowStaleCache) {
|
if (!stale || allowStaleCache) {
|
||||||
resultMap[sessionId] = this.fromSessionStatsCacheStats(cachedResult.entry.stats)
|
resultMap[sessionId] = this.fromSessionStatsCacheStats(cachedResult.entry.stats)
|
||||||
|
if (groupMyMessagesHint && Number.isFinite(groupMyMessagesHint.entry.messageCount)) {
|
||||||
|
resultMap[sessionId].groupMyMessages = groupMyMessagesHint.entry.messageCount
|
||||||
|
}
|
||||||
cacheMeta[sessionId] = {
|
cacheMeta[sessionId] = {
|
||||||
updatedAt: cachedResult.entry.updatedAt,
|
updatedAt: cachedResult.entry.updatedAt,
|
||||||
stale,
|
stale,
|
||||||
|
|||||||
@@ -875,6 +875,7 @@ class GroupAnalyticsService {
|
|||||||
])
|
])
|
||||||
const groupNicknames = await this.getGroupNicknamesForRoom(chatroomId, nicknameCandidates)
|
const groupNicknames = await this.getGroupNicknamesForRoom(chatroomId, nicknameCandidates)
|
||||||
const myWxid = this.cleanAccountDirName(this.configService.get('myWxid') || '')
|
const myWxid = this.cleanAccountDirName(this.configService.get('myWxid') || '')
|
||||||
|
let myGroupMessageCountHint: number | undefined
|
||||||
|
|
||||||
const data: GroupMembersPanelEntry[] = members
|
const data: GroupMembersPanelEntry[] = members
|
||||||
.map((member) => {
|
.map((member) => {
|
||||||
@@ -916,6 +917,17 @@ class GroupAnalyticsService {
|
|||||||
})
|
})
|
||||||
.filter((member): member is GroupMembersPanelEntry => Boolean(member))
|
.filter((member): member is GroupMembersPanelEntry => Boolean(member))
|
||||||
|
|
||||||
|
if (includeMessageCounts && myWxid) {
|
||||||
|
const selfEntry = data.find((member) => this.cleanAccountDirName(member.username) === myWxid)
|
||||||
|
if (selfEntry && Number.isFinite(selfEntry.messageCount)) {
|
||||||
|
myGroupMessageCountHint = Math.max(0, Math.floor(selfEntry.messageCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMessageCounts && Number.isFinite(myGroupMessageCountHint)) {
|
||||||
|
void chatService.setGroupMyMessageCountHint(chatroomId, myGroupMessageCountHint as number)
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, data: this.sortGroupMembersPanelEntries(data) }
|
return { success: true, data: this.sortGroupMembersPanelEntries(data) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
204
electron/services/groupMyMessageCountCacheService.ts
Normal file
204
electron/services/groupMyMessageCountCacheService.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { join, dirname } from 'path'
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
||||||
|
import { ConfigService } from './config'
|
||||||
|
|
||||||
|
const CACHE_VERSION = 1
|
||||||
|
const MAX_GROUP_ENTRIES_PER_SCOPE = 3000
|
||||||
|
const MAX_SCOPE_ENTRIES = 12
|
||||||
|
|
||||||
|
export interface GroupMyMessageCountCacheEntry {
|
||||||
|
updatedAt: number
|
||||||
|
messageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupMyMessageCountScopeMap {
|
||||||
|
[chatroomId: string]: GroupMyMessageCountCacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupMyMessageCountCacheStore {
|
||||||
|
version: number
|
||||||
|
scopes: Record<string, GroupMyMessageCountScopeMap>
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNonNegativeInt(value: unknown): number | undefined {
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value)) return undefined
|
||||||
|
return Math.max(0, Math.floor(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEntry(raw: unknown): GroupMyMessageCountCacheEntry | null {
|
||||||
|
if (!raw || typeof raw !== 'object') return null
|
||||||
|
const source = raw as Record<string, unknown>
|
||||||
|
const updatedAt = toNonNegativeInt(source.updatedAt)
|
||||||
|
const messageCount = toNonNegativeInt(source.messageCount)
|
||||||
|
if (updatedAt === undefined || messageCount === undefined) return null
|
||||||
|
return {
|
||||||
|
updatedAt,
|
||||||
|
messageCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupMyMessageCountCacheService {
|
||||||
|
private readonly cacheFilePath: string
|
||||||
|
private store: GroupMyMessageCountCacheStore = {
|
||||||
|
version: CACHE_VERSION,
|
||||||
|
scopes: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(cacheBasePath?: string) {
|
||||||
|
const basePath = cacheBasePath && cacheBasePath.trim().length > 0
|
||||||
|
? cacheBasePath
|
||||||
|
: ConfigService.getInstance().getCacheBasePath()
|
||||||
|
this.cacheFilePath = join(basePath, 'group-my-message-counts.json')
|
||||||
|
this.ensureCacheDir()
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCacheDir(): void {
|
||||||
|
const dir = dirname(this.cacheFilePath)
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private load(): void {
|
||||||
|
if (!existsSync(this.cacheFilePath)) return
|
||||||
|
try {
|
||||||
|
const raw = readFileSync(this.cacheFilePath, 'utf8')
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (!parsed || typeof parsed !== 'object') {
|
||||||
|
this.store = { version: CACHE_VERSION, scopes: {} }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parsed as Record<string, unknown>
|
||||||
|
const scopesRaw = payload.scopes
|
||||||
|
if (!scopesRaw || typeof scopesRaw !== 'object') {
|
||||||
|
this.store = { version: CACHE_VERSION, scopes: {} }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopes: Record<string, GroupMyMessageCountScopeMap> = {}
|
||||||
|
for (const [scopeKey, scopeValue] of Object.entries(scopesRaw as Record<string, unknown>)) {
|
||||||
|
if (!scopeValue || typeof scopeValue !== 'object') continue
|
||||||
|
const normalizedScope: GroupMyMessageCountScopeMap = {}
|
||||||
|
for (const [chatroomId, entryRaw] of Object.entries(scopeValue as Record<string, unknown>)) {
|
||||||
|
const entry = normalizeEntry(entryRaw)
|
||||||
|
if (!entry) continue
|
||||||
|
normalizedScope[chatroomId] = entry
|
||||||
|
}
|
||||||
|
if (Object.keys(normalizedScope).length > 0) {
|
||||||
|
scopes[scopeKey] = normalizedScope
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store = {
|
||||||
|
version: CACHE_VERSION,
|
||||||
|
scopes
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GroupMyMessageCountCacheService: 载入缓存失败', error)
|
||||||
|
this.store = { version: CACHE_VERSION, scopes: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(scopeKey: string, chatroomId: string): GroupMyMessageCountCacheEntry | undefined {
|
||||||
|
if (!scopeKey || !chatroomId) return undefined
|
||||||
|
const scope = this.store.scopes[scopeKey]
|
||||||
|
if (!scope) return undefined
|
||||||
|
const entry = normalizeEntry(scope[chatroomId])
|
||||||
|
if (!entry) {
|
||||||
|
delete scope[chatroomId]
|
||||||
|
if (Object.keys(scope).length === 0) {
|
||||||
|
delete this.store.scopes[scopeKey]
|
||||||
|
}
|
||||||
|
this.persist()
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
set(scopeKey: string, chatroomId: string, entry: GroupMyMessageCountCacheEntry): void {
|
||||||
|
if (!scopeKey || !chatroomId) return
|
||||||
|
const normalized = normalizeEntry(entry)
|
||||||
|
if (!normalized) return
|
||||||
|
|
||||||
|
if (!this.store.scopes[scopeKey]) {
|
||||||
|
this.store.scopes[scopeKey] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = this.store.scopes[scopeKey][chatroomId]
|
||||||
|
if (existing && existing.updatedAt > normalized.updatedAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.scopes[scopeKey][chatroomId] = normalized
|
||||||
|
this.trimScope(scopeKey)
|
||||||
|
this.trimScopes()
|
||||||
|
this.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(scopeKey: string, chatroomId: string): void {
|
||||||
|
if (!scopeKey || !chatroomId) return
|
||||||
|
const scope = this.store.scopes[scopeKey]
|
||||||
|
if (!scope) return
|
||||||
|
if (!(chatroomId in scope)) return
|
||||||
|
delete scope[chatroomId]
|
||||||
|
if (Object.keys(scope).length === 0) {
|
||||||
|
delete this.store.scopes[scopeKey]
|
||||||
|
}
|
||||||
|
this.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScope(scopeKey: string): void {
|
||||||
|
if (!scopeKey) return
|
||||||
|
if (!this.store.scopes[scopeKey]) return
|
||||||
|
delete this.store.scopes[scopeKey]
|
||||||
|
this.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAll(): void {
|
||||||
|
this.store = { version: CACHE_VERSION, scopes: {} }
|
||||||
|
try {
|
||||||
|
rmSync(this.cacheFilePath, { force: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GroupMyMessageCountCacheService: 清理缓存失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private trimScope(scopeKey: string): void {
|
||||||
|
const scope = this.store.scopes[scopeKey]
|
||||||
|
if (!scope) return
|
||||||
|
const entries = Object.entries(scope)
|
||||||
|
if (entries.length <= MAX_GROUP_ENTRIES_PER_SCOPE) return
|
||||||
|
entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
|
||||||
|
const trimmed: GroupMyMessageCountScopeMap = {}
|
||||||
|
for (const [chatroomId, entry] of entries.slice(0, MAX_GROUP_ENTRIES_PER_SCOPE)) {
|
||||||
|
trimmed[chatroomId] = entry
|
||||||
|
}
|
||||||
|
this.store.scopes[scopeKey] = trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
private trimScopes(): void {
|
||||||
|
const scopeEntries = Object.entries(this.store.scopes)
|
||||||
|
if (scopeEntries.length <= MAX_SCOPE_ENTRIES) return
|
||||||
|
scopeEntries.sort((a, b) => {
|
||||||
|
const aUpdatedAt = Math.max(...Object.values(a[1]).map((entry) => entry.updatedAt), 0)
|
||||||
|
const bUpdatedAt = Math.max(...Object.values(b[1]).map((entry) => entry.updatedAt), 0)
|
||||||
|
return bUpdatedAt - aUpdatedAt
|
||||||
|
})
|
||||||
|
|
||||||
|
const trimmedScopes: Record<string, GroupMyMessageCountScopeMap> = {}
|
||||||
|
for (const [scopeKey, scopeMap] of scopeEntries.slice(0, MAX_SCOPE_ENTRIES)) {
|
||||||
|
trimmedScopes[scopeKey] = scopeMap
|
||||||
|
}
|
||||||
|
this.store.scopes = trimmedScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
private persist(): void {
|
||||||
|
try {
|
||||||
|
writeFileSync(this.cacheFilePath, JSON.stringify(this.store), 'utf8')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GroupMyMessageCountCacheService: 保存缓存失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -848,6 +848,26 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setIsLoadingDetail(true)
|
setIsLoadingDetail(true)
|
||||||
setIsLoadingDetailExtra(true)
|
setIsLoadingDetailExtra(true)
|
||||||
|
|
||||||
|
if (normalizedSessionId.includes('@chatroom')) {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const hintResult = await window.electronAPI.chat.getGroupMyMessageCountHint(normalizedSessionId)
|
||||||
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
|
if (!hintResult.success || !Number.isFinite(hintResult.count)) return
|
||||||
|
const hintedMyCount = Math.max(0, Math.floor(hintResult.count as number))
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
if (!prev || prev.wxid !== normalizedSessionId) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
groupMyMessages: hintedMyCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// ignore hint errors
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.chat.getSessionDetailFast(normalizedSessionId)
|
const result = await window.electronAPI.chat.getSessionDetailFast(normalizedSessionId)
|
||||||
if (requestSeq !== detailRequestSeqRef.current) return
|
if (requestSeq !== detailRequestSeqRef.current) return
|
||||||
@@ -1074,6 +1094,57 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const normalizeWxidLikeIdentity = useCallback((value?: string): string => {
|
||||||
|
const trimmed = String(value || '').trim()
|
||||||
|
if (!trimmed) return ''
|
||||||
|
const lowered = trimmed.toLowerCase()
|
||||||
|
if (lowered.startsWith('wxid_')) {
|
||||||
|
const matched = lowered.match(/^(wxid_[^_]+)/i)
|
||||||
|
return matched ? matched[1].toLowerCase() : lowered
|
||||||
|
}
|
||||||
|
const suffixMatch = lowered.match(/^(.+)_([a-z0-9]{4})$/i)
|
||||||
|
return suffixMatch ? suffixMatch[1].toLowerCase() : lowered
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isSelfGroupMember = useCallback((memberUsername?: string): boolean => {
|
||||||
|
const selfRaw = String(myWxid || '').trim().toLowerCase()
|
||||||
|
const selfNormalized = normalizeWxidLikeIdentity(myWxid)
|
||||||
|
if (!selfRaw && !selfNormalized) return false
|
||||||
|
const memberRaw = String(memberUsername || '').trim().toLowerCase()
|
||||||
|
const memberNormalized = normalizeWxidLikeIdentity(memberUsername)
|
||||||
|
return Boolean(
|
||||||
|
(selfRaw && memberRaw && selfRaw === memberRaw) ||
|
||||||
|
(selfNormalized && memberNormalized && selfNormalized === memberNormalized)
|
||||||
|
)
|
||||||
|
}, [myWxid, normalizeWxidLikeIdentity])
|
||||||
|
|
||||||
|
const resolveMyGroupMessageCountFromMembers = useCallback((members: GroupPanelMember[]): number | undefined => {
|
||||||
|
if (!myWxid) return undefined
|
||||||
|
|
||||||
|
for (const member of members) {
|
||||||
|
if (!isSelfGroupMember(member.username)) continue
|
||||||
|
if (Number.isFinite(member.messageCount)) {
|
||||||
|
return Math.max(0, Math.floor(member.messageCount))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}, [isSelfGroupMember, myWxid])
|
||||||
|
|
||||||
|
const syncGroupMyMessagesFromMembers = useCallback((chatroomId: string, members: GroupPanelMember[]) => {
|
||||||
|
const myMessageCount = resolveMyGroupMessageCountFromMembers(members)
|
||||||
|
if (!Number.isFinite(myMessageCount)) return
|
||||||
|
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
if (!prev || prev.wxid !== chatroomId || !prev.wxid.includes('@chatroom')) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
groupMyMessages: myMessageCount as number
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [resolveMyGroupMessageCountFromMembers])
|
||||||
|
|
||||||
const updateGroupMembersPanelCache = useCallback((
|
const updateGroupMembersPanelCache = useCallback((
|
||||||
chatroomId: string,
|
chatroomId: string,
|
||||||
members: GroupPanelMember[],
|
members: GroupPanelMember[],
|
||||||
@@ -1093,6 +1164,47 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const syncGroupMembersMyCountFromDetail = useCallback((chatroomId: string, myMessageCount: number) => {
|
||||||
|
if (!chatroomId || !chatroomId.includes('@chatroom')) return
|
||||||
|
const normalizedCount = Number.isFinite(myMessageCount) ? Math.max(0, Math.floor(myMessageCount)) : 0
|
||||||
|
|
||||||
|
const patchMembers = (members: GroupPanelMember[]): { changed: boolean; members: GroupPanelMember[] } => {
|
||||||
|
if (!Array.isArray(members) || members.length === 0) {
|
||||||
|
return { changed: false, members }
|
||||||
|
}
|
||||||
|
let changed = false
|
||||||
|
const patched = members.map((member) => {
|
||||||
|
if (!isSelfGroupMember(member.username)) return member
|
||||||
|
if (member.messageCount === normalizedCount) return member
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
messageCount: normalizedCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!changed) return { changed: false, members }
|
||||||
|
return { changed: true, members: normalizeGroupPanelMembers(patched) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = groupMembersPanelCacheRef.current.get(chatroomId)
|
||||||
|
if (cached && cached.members.length > 0) {
|
||||||
|
const patchedCache = patchMembers(cached.members)
|
||||||
|
if (patchedCache.changed) {
|
||||||
|
updateGroupMembersPanelCache(chatroomId, patchedCache.members, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupPanelMembers((prev) => {
|
||||||
|
const patched = patchMembers(prev)
|
||||||
|
if (!patched.changed) return prev
|
||||||
|
return patched.members
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
isSelfGroupMember,
|
||||||
|
normalizeGroupPanelMembers,
|
||||||
|
updateGroupMembersPanelCache
|
||||||
|
])
|
||||||
|
|
||||||
const getGroupMembersPanelDataWithTimeout = useCallback(async (
|
const getGroupMembersPanelDataWithTimeout = useCallback(async (
|
||||||
chatroomId: string,
|
chatroomId: string,
|
||||||
options: { forceRefresh?: boolean; includeMessageCounts?: boolean },
|
options: { forceRefresh?: boolean; includeMessageCounts?: boolean },
|
||||||
@@ -1145,6 +1257,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
const membersWithCounts = normalizeGroupPanelMembers(countsResult.data as GroupPanelMember[])
|
const membersWithCounts = normalizeGroupPanelMembers(countsResult.data as GroupPanelMember[])
|
||||||
setGroupPanelMembers(membersWithCounts)
|
setGroupPanelMembers(membersWithCounts)
|
||||||
|
syncGroupMyMessagesFromMembers(chatroomId, membersWithCounts)
|
||||||
setGroupMembersError(null)
|
setGroupMembersError(null)
|
||||||
updateGroupMembersPanelCache(chatroomId, membersWithCounts, true)
|
updateGroupMembersPanelCache(chatroomId, membersWithCounts, true)
|
||||||
hasInitializedGroupMembersRef.current = true
|
hasInitializedGroupMembersRef.current = true
|
||||||
@@ -1161,6 +1274,9 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
if (cacheFresh && cached) {
|
if (cacheFresh && cached) {
|
||||||
setGroupPanelMembers(cached.members)
|
setGroupPanelMembers(cached.members)
|
||||||
|
if (cached.includeMessageCounts) {
|
||||||
|
syncGroupMyMessagesFromMembers(chatroomId, cached.members)
|
||||||
|
}
|
||||||
setGroupMembersError(null)
|
setGroupMembersError(null)
|
||||||
setGroupMembersLoadingHint('')
|
setGroupMembersLoadingHint('')
|
||||||
setIsLoadingGroupMembers(false)
|
setIsLoadingGroupMembers(false)
|
||||||
@@ -1176,6 +1292,9 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setGroupMembersError(null)
|
setGroupMembersError(null)
|
||||||
if (hasCachedMembers && cached) {
|
if (hasCachedMembers && cached) {
|
||||||
setGroupPanelMembers(cached.members)
|
setGroupPanelMembers(cached.members)
|
||||||
|
if (cached.includeMessageCounts) {
|
||||||
|
syncGroupMyMessagesFromMembers(chatroomId, cached.members)
|
||||||
|
}
|
||||||
setIsRefreshingGroupMembers(true)
|
setIsRefreshingGroupMembers(true)
|
||||||
setGroupMembersLoadingHint('')
|
setGroupMembersLoadingHint('')
|
||||||
setIsLoadingGroupMembers(false)
|
setIsLoadingGroupMembers(false)
|
||||||
@@ -1230,6 +1349,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
}, [
|
}, [
|
||||||
getGroupMembersPanelDataWithTimeout,
|
getGroupMembersPanelDataWithTimeout,
|
||||||
isGroupChatSession,
|
isGroupChatSession,
|
||||||
|
syncGroupMyMessagesFromMembers,
|
||||||
normalizeGroupPanelMembers,
|
normalizeGroupPanelMembers,
|
||||||
updateGroupMembersPanelCache
|
updateGroupMembersPanelCache
|
||||||
])
|
])
|
||||||
@@ -1267,6 +1387,13 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
void loadGroupMembersPanel(currentSessionId)
|
void loadGroupMembersPanel(currentSessionId)
|
||||||
}, [showGroupMembersPanel, currentSessionId, loadGroupMembersPanel, isGroupChatSession])
|
}, [showGroupMembersPanel, currentSessionId, loadGroupMembersPanel, isGroupChatSession])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const chatroomId = String(sessionDetail?.wxid || '').trim()
|
||||||
|
if (!chatroomId || !chatroomId.includes('@chatroom')) return
|
||||||
|
if (!Number.isFinite(sessionDetail?.groupMyMessages)) return
|
||||||
|
syncGroupMembersMyCountFromDetail(chatroomId, sessionDetail!.groupMyMessages as number)
|
||||||
|
}, [sessionDetail?.groupMyMessages, sessionDetail?.wxid, syncGroupMembersMyCountFromDetail])
|
||||||
|
|
||||||
// 复制字段值到剪贴板
|
// 复制字段值到剪贴板
|
||||||
const handleCopyField = useCallback(async (text: string, field: string) => {
|
const handleCopyField = useCallback(async (text: string, field: string) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -876,7 +876,6 @@ function ExportPage() {
|
|||||||
totalFriends: 0
|
totalFriends: 0
|
||||||
})
|
})
|
||||||
const [contentSessionCounts, setContentSessionCounts] = useState<ExportContentSessionCountsSummary>(defaultContentSessionCounts)
|
const [contentSessionCounts, setContentSessionCounts] = useState<ExportContentSessionCountsSummary>(defaultContentSessionCounts)
|
||||||
const [isContentSessionCountsLoading, setIsContentSessionCountsLoading] = useState(true)
|
|
||||||
const [hasSeededContentSessionCounts, setHasSeededContentSessionCounts] = useState(false)
|
const [hasSeededContentSessionCounts, setHasSeededContentSessionCounts] = useState(false)
|
||||||
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
||||||
const [nowTick, setNowTick] = useState(Date.now())
|
const [nowTick, setNowTick] = useState(Date.now())
|
||||||
@@ -904,6 +903,7 @@ function ExportPage() {
|
|||||||
const inProgressSessionIdsRef = useRef<string[]>([])
|
const inProgressSessionIdsRef = useRef<string[]>([])
|
||||||
const activeTaskCountRef = useRef(0)
|
const activeTaskCountRef = useRef(0)
|
||||||
const hasBaseConfigReadyRef = useRef(false)
|
const hasBaseConfigReadyRef = useRef(false)
|
||||||
|
const contentSessionCountsForceRetryRef = useRef(0)
|
||||||
|
|
||||||
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
const ensureExportCacheScope = useCallback(async (): Promise<string> => {
|
||||||
if (exportCacheScopeReadyRef.current) {
|
if (exportCacheScopeReadyRef.current) {
|
||||||
@@ -1413,9 +1413,6 @@ function ExportPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadContentSessionCounts = useCallback(async (options?: { silent?: boolean; forceRefresh?: boolean }) => {
|
const loadContentSessionCounts = useCallback(async (options?: { silent?: boolean; forceRefresh?: boolean }) => {
|
||||||
if (!options?.silent) {
|
|
||||||
setIsContentSessionCountsLoading(true)
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const result = await withTimeout(
|
const result = await withTimeout(
|
||||||
window.electronAPI.chat.getExportContentSessionCounts({
|
window.electronAPI.chat.getExportContentSessionCounts({
|
||||||
@@ -1437,14 +1434,23 @@ function ExportPage() {
|
|||||||
refreshing: result.data.refreshing === true
|
refreshing: result.data.refreshing === true
|
||||||
}
|
}
|
||||||
setContentSessionCounts(next)
|
setContentSessionCounts(next)
|
||||||
|
const looksLikeAllZero = next.totalSessions > 0 &&
|
||||||
|
next.textSessions === 0 &&
|
||||||
|
next.voiceSessions === 0 &&
|
||||||
|
next.imageSessions === 0 &&
|
||||||
|
next.videoSessions === 0 &&
|
||||||
|
next.emojiSessions === 0
|
||||||
|
|
||||||
|
if (looksLikeAllZero && contentSessionCountsForceRetryRef.current < 3) {
|
||||||
|
contentSessionCountsForceRetryRef.current += 1
|
||||||
|
void window.electronAPI.chat.refreshExportContentSessionCounts({ forceRefresh: true })
|
||||||
|
} else {
|
||||||
|
contentSessionCountsForceRetryRef.current = 0
|
||||||
setHasSeededContentSessionCounts(true)
|
setHasSeededContentSessionCounts(true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载导出内容会话统计失败:', error)
|
console.error('加载导出内容会话统计失败:', error)
|
||||||
} finally {
|
|
||||||
if (!options?.silent) {
|
|
||||||
setIsContentSessionCountsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -3205,7 +3211,7 @@ function ExportPage() {
|
|||||||
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog
|
||||||
const shouldShowMediaSection = !isContentScopeDialog
|
const shouldShowMediaSection = !isContentScopeDialog
|
||||||
const isTabCountComputing = isSharedTabCountsLoading && !isSharedTabCountsReady
|
const isTabCountComputing = isSharedTabCountsLoading && !isSharedTabCountsReady
|
||||||
const isSessionCardStatsLoading = isBaseConfigLoading || (isContentSessionCountsLoading && !hasSeededContentSessionCounts)
|
const isSessionCardStatsLoading = isBaseConfigLoading || !hasSeededContentSessionCounts
|
||||||
const isSessionCardStatsRefreshing = contentSessionCounts.refreshing || contentSessionCounts.pendingMediaSessions > 0
|
const isSessionCardStatsRefreshing = contentSessionCounts.refreshing || contentSessionCounts.pendingMediaSessions > 0
|
||||||
const isSnsCardStatsLoading = !hasSeededSnsStats
|
const isSnsCardStatsLoading = !hasSeededSnsStats
|
||||||
const taskRunningCount = tasks.filter(task => task.status === 'running').length
|
const taskRunningCount = tasks.filter(task => task.status === 'running').length
|
||||||
|
|||||||
7
src/types/electron.d.ts
vendored
7
src/types/electron.d.ts
vendored
@@ -240,6 +240,13 @@ export interface ElectronAPI {
|
|||||||
needsRefresh?: string[]
|
needsRefresh?: string[]
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
getGroupMyMessageCountHint: (chatroomId: string) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
count?: number
|
||||||
|
updatedAt?: number
|
||||||
|
source?: 'memory' | 'disk'
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
getImageData: (sessionId: string, msgId: string) => Promise<{ success: boolean; data?: string; error?: string }>
|
getImageData: (sessionId: string, msgId: string) => Promise<{ success: boolean; data?: string; error?: string }>
|
||||||
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) => Promise<{ success: boolean; data?: string; error?: string }>
|
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) => Promise<{ success: boolean; data?: string; error?: string }>
|
||||||
getAllVoiceMessages: (sessionId: string) => Promise<{ success: boolean; messages?: Message[]; error?: string }>
|
getAllVoiceMessages: (sessionId: string) => Promise<{ success: boolean; messages?: Message[]; error?: string }>
|
||||||
|
|||||||
Reference in New Issue
Block a user