perf(export): remove private relation stats and avatar backfill overhead

This commit is contained in:
tisonhuang
2026-03-04 13:51:55 +08:00
parent 38a023d0b6
commit b5eb8be15e
2 changed files with 4 additions and 307 deletions

View File

@@ -2722,128 +2722,6 @@ class ExportService {
return result
}
private async collectPrivateMutualGroupStats(
privateWxid: string,
myWxid: string
): Promise<{
totalGroups: number
totalMessagesByMe: number
totalMessagesByPeer: number
totalMessagesCombined: number
groups: Array<{
wxid: string
displayName: string
myMessageCount: number
peerMessageCount: number
totalMessageCount: number
}>
}> {
const normalizedPrivateWxid = String(privateWxid || '').trim()
const normalizedMyWxid = String(myWxid || '').trim()
if (!normalizedPrivateWxid || !normalizedMyWxid) {
return {
totalGroups: 0,
totalMessagesByMe: 0,
totalMessagesByPeer: 0,
totalMessagesCombined: 0,
groups: []
}
}
const sessionsResult = await wcdbService.getSessions()
if (!sessionsResult.success || !sessionsResult.sessions) {
return {
totalGroups: 0,
totalMessagesByMe: 0,
totalMessagesByPeer: 0,
totalMessagesCombined: 0,
groups: []
}
}
const groupIds = Array.from(
new Set(
(sessionsResult.sessions as Array<Record<string, any>>)
.map((row) => String(row.username || row.user_name || row.userName || '').trim())
.filter((username) => username.endsWith('@chatroom'))
)
)
if (groupIds.length === 0) {
return {
totalGroups: 0,
totalMessagesByMe: 0,
totalMessagesByPeer: 0,
totalMessagesCombined: 0,
groups: []
}
}
const mutualGroups = await parallelLimit(groupIds, 4, async (groupId) => {
const membersResult = await wcdbService.getGroupMembers(groupId)
if (!membersResult.success || !membersResult.members || membersResult.members.length === 0) {
return null
}
let hasMe = false
let hasPeer = false
for (const member of membersResult.members) {
const memberWxid = this.extractGroupMemberUsername(member)
if (!memberWxid) continue
if (!hasMe && this.isSameWxid(memberWxid, normalizedMyWxid)) {
hasMe = true
}
if (!hasPeer && this.isSameWxid(memberWxid, normalizedPrivateWxid)) {
hasPeer = true
}
if (hasMe && hasPeer) break
}
if (!hasMe || !hasPeer) return null
const [groupInfo, groupStatsResult] = await Promise.all([
this.getContactInfo(groupId),
wcdbService.getGroupStats(groupId, 0, 0)
])
const senderCountMap = groupStatsResult.success && groupStatsResult.data
? this.extractGroupSenderCountMap(groupStatsResult.data, groupId)
: new Map<string, number>()
const myMessageCount = this.sumSenderCountsByIdentity(senderCountMap, normalizedMyWxid)
const peerMessageCount = this.sumSenderCountsByIdentity(senderCountMap, normalizedPrivateWxid)
const totalMessageCount = myMessageCount + peerMessageCount
return {
wxid: groupId,
displayName: groupInfo.displayName || groupId,
myMessageCount,
peerMessageCount,
totalMessageCount
}
})
const groups = mutualGroups
.filter((item): item is {
wxid: string
displayName: string
myMessageCount: number
peerMessageCount: number
totalMessageCount: number
} => Boolean(item))
.sort((a, b) => {
if (b.totalMessageCount !== a.totalMessageCount) return b.totalMessageCount - a.totalMessageCount
return a.displayName.localeCompare(b.displayName, 'zh-CN')
})
const totalMessagesByMe = groups.reduce((sum, item) => sum + item.myMessageCount, 0)
const totalMessagesByPeer = groups.reduce((sum, item) => sum + item.peerMessageCount, 0)
return {
totalGroups: groups.length,
totalMessagesByMe,
totalMessagesByPeer,
totalMessagesCombined: totalMessagesByMe + totalMessagesByPeer,
groups
}
}
private resolveAvatarFile(avatarUrl?: string): { data?: Buffer; sourcePath?: string; sourceUrl?: string; ext: string; mime?: string } | null {
if (!avatarUrl) return null
if (avatarUrl.startsWith('data:')) {
@@ -3998,19 +3876,6 @@ class ExportService {
const arkmeSession: any = {
...sessionPayload
}
let privateMutualGroups: {
totalGroups: number
totalMessagesByMe: number
totalMessagesByPeer: number
totalMessagesCombined: number
groups: Array<{
wxid: string
displayName: string
myMessageCount: number
peerMessageCount: number
totalMessageCount: number
}>
} | undefined
let groupMembers: Array<{
wxid: string
displayName: string
@@ -4082,8 +3947,6 @@ class ExportService {
if (b.messageCount !== a.messageCount) return b.messageCount - a.messageCount
return String(a.displayName || a.wxid).localeCompare(String(b.displayName || b.wxid), 'zh-CN')
})
} else if (!sessionId.startsWith('gh_')) {
privateMutualGroups = await this.collectPrivateMutualGroupStats(sessionId, cleanedMyWxid)
}
const arkmeExport: any = {
@@ -4095,9 +3958,6 @@ class ExportService {
senders,
messages: compactMessages
}
if (privateMutualGroups) {
arkmeExport.privateMutualGroups = privateMutualGroups
}
if (groupMembers) {
arkmeExport.groupMembers = groupMembers
}

View File

@@ -863,12 +863,6 @@ function ExportPage() {
const [contactsLoadIssue, setContactsLoadIssue] = useState<ContactsLoadIssue | null>(null)
const [showContactsDiagnostics, setShowContactsDiagnostics] = useState(false)
const [contactsDiagnosticTick, setContactsDiagnosticTick] = useState(Date.now())
const [contactsAvatarEnrichProgress, setContactsAvatarEnrichProgress] = useState({
loaded: 0,
total: 0,
running: false,
tab: null as ConversationTab | null
})
const [showSessionDetailPanel, setShowSessionDetailPanel] = useState(false)
const [sessionDetail, setSessionDetail] = useState<SessionDetail | null>(null)
const [isLoadingSessionDetail, setIsLoadingSessionDetail] = useState(false)
@@ -941,8 +935,6 @@ function ExportPage() {
const preselectAppliedRef = useRef(false)
const exportCacheScopeRef = useRef('default')
const exportCacheScopeReadyRef = useRef(false)
const activeTabRef = useRef<ConversationTab>('private')
const contactsDataRef = useRef<ContactInfo[]>([])
const contactsLoadVersionRef = useRef(0)
const contactsLoadAttemptRef = useRef(0)
const contactsLoadTimeoutTimerRef = useRef<number | null>(null)
@@ -1059,128 +1051,8 @@ function ExportPage() {
contactsLoadTimeoutMsRef.current = contactsLoadTimeoutMs
}, [contactsLoadTimeoutMs])
useEffect(() => {
activeTabRef.current = activeTab
}, [activeTab])
useEffect(() => {
contactsDataRef.current = contactsList
}, [contactsList])
const applyEnrichedContactsToList = useCallback((enrichedMap: Record<string, { displayName?: string; avatarUrl?: string }>) => {
if (!enrichedMap || Object.keys(enrichedMap).length === 0) return
setContactsList(prev => {
let changed = false
const next = prev.map(contact => {
const enriched = enrichedMap[contact.username]
if (!enriched) return contact
const displayName = enriched.displayName || contact.displayName
const avatarUrl = enriched.avatarUrl || contact.avatarUrl
if (displayName === contact.displayName && avatarUrl === contact.avatarUrl) {
return contact
}
changed = true
return {
...contact,
displayName,
avatarUrl
}
})
return changed ? next : prev
})
}, [])
const enrichContactsListInBackground = useCallback(async (
sourceContacts: ContactInfo[],
loadVersion: number,
scopeKey: string,
targetTab: ConversationTab
) => {
const sourceByUsername = new Map<string, ContactInfo>()
for (const contact of sourceContacts) {
if (!contact.username) continue
sourceByUsername.set(contact.username, contact)
}
const usernames = Array.from(new Set(sourceContacts
.filter(contact => matchesContactTab(contact, targetTab))
.map(contact => contact.username)
.filter(Boolean)
.filter((username) => {
const currentContact = sourceByUsername.get(username)
return Boolean(currentContact && !currentContact.avatarUrl)
})))
const total = usernames.length
setContactsAvatarEnrichProgress({
loaded: 0,
total,
running: total > 0,
tab: targetTab
})
if (total === 0) return
for (let i = 0; i < total; i += EXPORT_AVATAR_ENRICH_BATCH_SIZE) {
if (contactsLoadVersionRef.current !== loadVersion) return
const batch = usernames.slice(i, i + EXPORT_AVATAR_ENRICH_BATCH_SIZE)
if (batch.length === 0) continue
try {
const avatarResult = await withTimeout(
window.electronAPI.chat.enrichSessionsContactInfo(batch, {
skipDisplayName: true,
onlyMissingAvatar: true
}),
CONTACT_ENRICH_TIMEOUT_MS
)
if (contactsLoadVersionRef.current !== loadVersion) return
if (avatarResult?.success && avatarResult.contacts) {
applyEnrichedContactsToList(avatarResult.contacts)
for (const [username, enriched] of Object.entries(avatarResult.contacts)) {
const prev = sourceByUsername.get(username)
if (!prev) continue
sourceByUsername.set(username, {
...prev,
displayName: enriched.displayName || prev.displayName,
avatarUrl: enriched.avatarUrl || prev.avatarUrl
})
}
}
const batchContacts = batch
.map(username => sourceByUsername.get(username))
.filter((contact): contact is ContactInfo => Boolean(contact))
const upsertResult = upsertAvatarCacheFromContacts(
contactsAvatarCacheRef.current,
batchContacts,
{ markCheckedUsernames: batch }
)
contactsAvatarCacheRef.current = upsertResult.avatarEntries
if (upsertResult.updatedAt) {
setAvatarCacheUpdatedAt(upsertResult.updatedAt)
}
} catch (error) {
console.error('导出页分批补全头像失败:', error)
}
const loaded = Math.min(i + batch.length, total)
setContactsAvatarEnrichProgress({
loaded,
total,
running: loaded < total,
tab: targetTab
})
await new Promise(resolve => setTimeout(resolve, 0))
}
void configService.setContactsAvatarCache(scopeKey, contactsAvatarCacheRef.current).catch((error) => {
console.error('写入导出页头像缓存失败:', error)
})
}, [applyEnrichedContactsToList])
const loadContactsList = useCallback(async (options?: { scopeKey?: string }) => {
const scopeKey = options?.scopeKey || await ensureExportCacheScope()
const targetTab = activeTabRef.current
const loadVersion = contactsLoadVersionRef.current + 1
contactsLoadVersionRef.current = loadVersion
contactsLoadAttemptRef.current += 1
@@ -1214,13 +1086,6 @@ function ExportPage() {
contactsLoadTimeoutTimerRef.current = timeoutTimerId
setIsContactsListLoading(true)
setContactsAvatarEnrichProgress({
loaded: 0,
total: 0,
running: false,
tab: null
})
try {
const contactsResult = await window.electronAPI.chat.getContacts()
if (contactsLoadVersionRef.current !== loadVersion) return
@@ -1266,7 +1131,6 @@ function ExportPage() {
).catch((error) => {
console.error('写入导出页通讯录缓存失败:', error)
})
void enrichContactsListInBackground(contactsWithAvatarCache, loadVersion, scopeKey, targetTab)
return
}
@@ -1301,7 +1165,7 @@ function ExportPage() {
setIsContactsListLoading(false)
}
}
}, [ensureExportCacheScope, enrichContactsListInBackground, syncContactTypeCounts])
}, [ensureExportCacheScope, syncContactTypeCounts])
useEffect(() => {
if (!isExportRoute) return
@@ -1341,29 +1205,9 @@ function ExportPage() {
}
}, [isExportRoute, ensureExportCacheScope, loadContactsList, syncContactTypeCounts])
useEffect(() => {
if (!isExportRoute || isContactsListLoading || contactsDataRef.current.length === 0) return
let cancelled = false
const loadVersion = contactsLoadVersionRef.current
void (async () => {
const scopeKey = await ensureExportCacheScope()
if (cancelled || contactsLoadVersionRef.current !== loadVersion) return
await enrichContactsListInBackground(contactsDataRef.current, loadVersion, scopeKey, activeTab)
})()
return () => {
cancelled = true
}
}, [activeTab, ensureExportCacheScope, enrichContactsListInBackground, isContactsListLoading, isExportRoute])
useEffect(() => {
if (isExportRoute) return
contactsLoadVersionRef.current += 1
setContactsAvatarEnrichProgress({
loaded: 0,
total: 0,
running: false,
tab: null
})
}, [isExportRoute])
useEffect(() => {
@@ -3203,8 +3047,6 @@ function ExportPage() {
contact.avatarUrl ? count + 1 : count
), 0)
}, [contactsList])
const isCurrentTabAvatarEnrichRunning = contactsAvatarEnrichProgress.running && contactsAvatarEnrichProgress.tab === activeTab
useEffect(() => {
if (!contactsListRef.current) return
contactsListRef.current.scrollTop = 0
@@ -3938,20 +3780,15 @@ function ExportPage() {
{avatarCacheUpdatedAtLabel ? ` · 更新于 ${avatarCacheUpdatedAtLabel}` : ''}
</span>
)}
{(isContactsListLoading || isCurrentTabAvatarEnrichRunning) && contactsList.length > 0 && (
{isContactsListLoading && contactsList.length > 0 && (
<span className="meta-item syncing">...</span>
)}
{isCurrentTabAvatarEnrichRunning && (
<span className="meta-item syncing">
{contactsAvatarEnrichProgress.loaded}/{contactsAvatarEnrichProgress.total}
</span>
)}
</div>
{contactsList.length > 0 && (isContactsListLoading || isCurrentTabAvatarEnrichRunning) && (
{contactsList.length > 0 && isContactsListLoading && (
<div className="table-stage-hint">
<Loader2 size={14} className="spin" />
{isContactsListLoading ? '联系人列表同步中…' : '正在补充头像…'}
</div>
)}