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)
|
||||
})
|
||||
|
||||
ipcMain.handle('chat:getGroupMyMessageCountHint', async (_, chatroomId: string) => {
|
||||
return chatService.getGroupMyMessageCountHint(chatroomId)
|
||||
})
|
||||
|
||||
ipcMain.handle('chat:getImageData', async (_, sessionId: string, msgId: string) => {
|
||||
return chatService.getImageData(sessionId, msgId)
|
||||
})
|
||||
|
||||
@@ -165,6 +165,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
sessionIds: string[],
|
||||
options?: { includeRelations?: boolean; forceRefresh?: boolean; allowStaleCache?: boolean }
|
||||
) => 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),
|
||||
getVoiceData: (sessionId: string, msgId: string, createTime?: number, serverId?: string | number) =>
|
||||
ipcRenderer.invoke('chat:getVoiceData', sessionId, msgId, createTime, serverId),
|
||||
|
||||
@@ -13,6 +13,7 @@ import { wcdbService } from './wcdbService'
|
||||
import { MessageCacheService } from './messageCacheService'
|
||||
import { ContactCacheService, ContactCacheEntry } from './contactCacheService'
|
||||
import { SessionStatsCacheService, SessionStatsCacheEntry, SessionStatsCacheStats } from './sessionStatsCacheService'
|
||||
import { GroupMyMessageCountCacheService, GroupMyMessageCountCacheEntry } from './groupMyMessageCountCacheService'
|
||||
import {
|
||||
ExportContentScopeStatsEntry,
|
||||
ExportContentSessionStatsEntry,
|
||||
@@ -226,6 +227,7 @@ class ChatService {
|
||||
private readonly contactCacheService: ContactCacheService
|
||||
private readonly messageCacheService: MessageCacheService
|
||||
private readonly sessionStatsCacheService: SessionStatsCacheService
|
||||
private readonly groupMyMessageCountCacheService: GroupMyMessageCountCacheService
|
||||
private readonly exportContentStatsCacheService: ExportContentStatsCacheService
|
||||
private voiceWavCache: LRUCache<string, Buffer>
|
||||
private voiceTranscriptCache: LRUCache<string, string>
|
||||
@@ -265,6 +267,8 @@ class ChatService {
|
||||
private allGroupSessionIdsCache: { ids: string[]; updatedAt: number } | null = null
|
||||
private readonly sessionStatsCacheTtlMs = 10 * 60 * 1000
|
||||
private readonly allGroupSessionIdsCacheTtlMs = 5 * 60 * 1000
|
||||
private groupMyMessageCountCacheScope = ''
|
||||
private groupMyMessageCountMemoryCache = new Map<string, GroupMyMessageCountCacheEntry>()
|
||||
private exportContentStatsScope = ''
|
||||
private exportContentStatsMemory = new Map<string, ExportContentSessionStatsEntry>()
|
||||
private exportContentStatsScopeUpdatedAt = 0
|
||||
@@ -282,6 +286,7 @@ class ChatService {
|
||||
this.avatarCache = new Map(Object.entries(persisted))
|
||||
this.messageCacheService = new MessageCacheService(this.configService.getCacheBasePath())
|
||||
this.sessionStatsCacheService = new SessionStatsCacheService(this.configService.getCacheBasePath())
|
||||
this.groupMyMessageCountCacheService = new GroupMyMessageCountCacheService(this.configService.getCacheBasePath())
|
||||
this.exportContentStatsCacheService = new ExportContentStatsCacheService(this.configService.getCacheBasePath())
|
||||
// 初始化LRU缓存,限制大小防止内存泄漏
|
||||
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
|
||||
counts?: Record<string, number>
|
||||
error?: string
|
||||
@@ -923,28 +931,36 @@ class ChatService {
|
||||
return { success: true, counts: {} }
|
||||
}
|
||||
|
||||
const preferHintCache = options?.preferHintCache !== false
|
||||
const bypassSessionCache = options?.bypassSessionCache === true
|
||||
|
||||
this.refreshSessionMessageCountCacheScope()
|
||||
const counts: Record<string, number> = {}
|
||||
const now = Date.now()
|
||||
const pendingSessionIds: string[] = []
|
||||
|
||||
for (const sessionId of normalizedSessionIds) {
|
||||
const cached = this.sessionMessageCountCache.get(sessionId)
|
||||
if (cached && now - cached.updatedAt <= this.sessionMessageCountCacheTtlMs) {
|
||||
counts[sessionId] = cached.count
|
||||
continue
|
||||
if (!bypassSessionCache) {
|
||||
const cached = this.sessionMessageCountCache.get(sessionId)
|
||||
if (cached && now - cached.updatedAt <= this.sessionMessageCountCacheTtlMs) {
|
||||
counts[sessionId] = cached.count
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const hintCount = this.sessionMessageCountHintCache.get(sessionId)
|
||||
if (typeof hintCount === 'number' && Number.isFinite(hintCount) && hintCount >= 0) {
|
||||
counts[sessionId] = Math.floor(hintCount)
|
||||
this.sessionMessageCountCache.set(sessionId, {
|
||||
count: Math.floor(hintCount),
|
||||
updatedAt: now
|
||||
})
|
||||
} else {
|
||||
pendingSessionIds.push(sessionId)
|
||||
if (preferHintCache) {
|
||||
const hintCount = this.sessionMessageCountHintCache.get(sessionId)
|
||||
if (typeof hintCount === 'number' && Number.isFinite(hintCount) && hintCount >= 0) {
|
||||
counts[sessionId] = Math.floor(hintCount)
|
||||
this.sessionMessageCountCache.set(sessionId, {
|
||||
count: Math.floor(hintCount),
|
||||
updatedAt: now
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pendingSessionIds.push(sessionId)
|
||||
}
|
||||
|
||||
const batchSize = 320
|
||||
@@ -1618,6 +1634,7 @@ class ChatService {
|
||||
const scope = `${dbPath}::${myWxid}`
|
||||
if (scope === this.sessionMessageCountCacheScope) {
|
||||
this.refreshSessionStatsCacheScope(scope)
|
||||
this.refreshGroupMyMessageCountCacheScope(scope)
|
||||
this.refreshExportContentStatsScope(scope)
|
||||
return
|
||||
}
|
||||
@@ -1628,9 +1645,16 @@ class ChatService {
|
||||
this.sessionDetailExtraCache.clear()
|
||||
this.sessionStatusCache.clear()
|
||||
this.refreshSessionStatsCacheScope(scope)
|
||||
this.refreshGroupMyMessageCountCacheScope(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 {
|
||||
if (scope === this.exportContentStatsScope) return
|
||||
this.exportContentStatsScope = scope
|
||||
@@ -1718,7 +1742,7 @@ class ChatService {
|
||||
return {
|
||||
...entry,
|
||||
updatedAt: Date.now(),
|
||||
mediaReady: true
|
||||
mediaReady: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1783,9 +1807,18 @@ class ChatService {
|
||||
|
||||
if (targets.length > 0) {
|
||||
await this.forEachWithConcurrency(targets, 3, async (sessionId) => {
|
||||
const nextEntry = await this.collectExportContentEntry(sessionId)
|
||||
this.exportContentStatsMemory.set(sessionId, nextEntry)
|
||||
this.exportContentStatsDirtySessionIds.delete(sessionId)
|
||||
try {
|
||||
const nextEntry = await this.collectExportContentEntry(sessionId)
|
||||
this.exportContentStatsMemory.set(sessionId, nextEntry)
|
||||
if (nextEntry.mediaReady) {
|
||||
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.hasAny) {
|
||||
textSessions += 1
|
||||
} else if (this.isExportContentEntryDirty(sessionId)) {
|
||||
} else if (forceRefresh || this.isExportContentEntryDirty(sessionId)) {
|
||||
missingTextCountSessionIds.push(sessionId)
|
||||
}
|
||||
} else {
|
||||
@@ -1873,7 +1906,10 @@ class ChatService {
|
||||
}
|
||||
|
||||
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) {
|
||||
const now = Date.now()
|
||||
for (const sessionId of missingTextCountSessionIds) {
|
||||
@@ -1948,6 +1984,43 @@ class ChatService {
|
||||
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 {
|
||||
const normalized: SessionStatsCacheStats = {
|
||||
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 {
|
||||
const updatedAt = Date.now()
|
||||
const normalizedStats = this.toSessionStatsCacheStats(stats)
|
||||
const entry: SessionStatsCacheEntry = {
|
||||
updatedAt,
|
||||
includeRelations,
|
||||
stats: this.toSessionStatsCacheStats(stats)
|
||||
stats: normalizedStats
|
||||
}
|
||||
const scopedKey = this.buildScopedSessionStatsKey(sessionId)
|
||||
this.sessionStatsMemoryCache.set(scopedKey, 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
|
||||
}
|
||||
|
||||
@@ -2217,6 +2294,9 @@ class ChatService {
|
||||
|
||||
if (sessionId.endsWith('@chatroom')) {
|
||||
stats.groupActiveSpeakers = senderIdentities.size
|
||||
if (Number.isFinite(stats.groupMyMessages)) {
|
||||
this.setGroupMyMessageCountHintEntry(sessionId, stats.groupMyMessages as number)
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
@@ -4264,6 +4344,8 @@ class ChatService {
|
||||
this.sessionStatsPendingFull.clear()
|
||||
this.allGroupSessionIdsCache = null
|
||||
this.sessionStatsCacheService.clearAll()
|
||||
this.groupMyMessageCountMemoryCache.clear()
|
||||
this.groupMyMessageCountCacheService.clearAll()
|
||||
this.exportContentStatsMemory.clear()
|
||||
this.exportContentStatsDirtySessionIds.clear()
|
||||
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<{
|
||||
success: boolean
|
||||
data?: Record<string, ExportSessionStats>
|
||||
@@ -4743,12 +4870,18 @@ class ChatService {
|
||||
const now = Date.now()
|
||||
|
||||
for (const sessionId of normalizedSessionIds) {
|
||||
const groupMyMessagesHint = sessionId.endsWith('@chatroom')
|
||||
? this.getGroupMyMessageCountHintEntry(sessionId)
|
||||
: null
|
||||
if (!forceRefresh) {
|
||||
const cachedResult = this.getSessionStatsCacheEntry(sessionId)
|
||||
if (cachedResult && this.supportsRequestedRelation(cachedResult.entry, includeRelations)) {
|
||||
const stale = now - cachedResult.entry.updatedAt > this.sessionStatsCacheTtlMs
|
||||
if (!stale || allowStaleCache) {
|
||||
resultMap[sessionId] = this.fromSessionStatsCacheStats(cachedResult.entry.stats)
|
||||
if (groupMyMessagesHint && Number.isFinite(groupMyMessagesHint.entry.messageCount)) {
|
||||
resultMap[sessionId].groupMyMessages = groupMyMessagesHint.entry.messageCount
|
||||
}
|
||||
cacheMeta[sessionId] = {
|
||||
updatedAt: cachedResult.entry.updatedAt,
|
||||
stale,
|
||||
|
||||
@@ -875,6 +875,7 @@ class GroupAnalyticsService {
|
||||
])
|
||||
const groupNicknames = await this.getGroupNicknamesForRoom(chatroomId, nicknameCandidates)
|
||||
const myWxid = this.cleanAccountDirName(this.configService.get('myWxid') || '')
|
||||
let myGroupMessageCountHint: number | undefined
|
||||
|
||||
const data: GroupMembersPanelEntry[] = members
|
||||
.map((member) => {
|
||||
@@ -916,6 +917,17 @@ class GroupAnalyticsService {
|
||||
})
|
||||
.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) }
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user