mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(contacts): persist avatar cache with incremental refresh
This commit is contained in:
@@ -25,6 +25,7 @@ const SEARCH_DEBOUNCE_MS = 120
|
|||||||
const VIRTUAL_ROW_HEIGHT = 76
|
const VIRTUAL_ROW_HEIGHT = 76
|
||||||
const VIRTUAL_OVERSCAN = 10
|
const VIRTUAL_OVERSCAN = 10
|
||||||
const DEFAULT_CONTACTS_LOAD_TIMEOUT_MS = 3000
|
const DEFAULT_CONTACTS_LOAD_TIMEOUT_MS = 3000
|
||||||
|
const AVATAR_RECHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
interface ContactsLoadSession {
|
interface ContactsLoadSession {
|
||||||
requestId: string
|
requestId: string
|
||||||
@@ -91,8 +92,10 @@ function ContactsPage() {
|
|||||||
const [diagnosticTick, setDiagnosticTick] = useState(Date.now())
|
const [diagnosticTick, setDiagnosticTick] = useState(Date.now())
|
||||||
const [contactsDataSource, setContactsDataSource] = useState<ContactsDataSource>(null)
|
const [contactsDataSource, setContactsDataSource] = useState<ContactsDataSource>(null)
|
||||||
const [contactsUpdatedAt, setContactsUpdatedAt] = useState<number | null>(null)
|
const [contactsUpdatedAt, setContactsUpdatedAt] = useState<number | null>(null)
|
||||||
|
const [avatarCacheUpdatedAt, setAvatarCacheUpdatedAt] = useState<number | null>(null)
|
||||||
const contactsLoadTimeoutMsRef = useRef(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS)
|
const contactsLoadTimeoutMsRef = useRef(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS)
|
||||||
const contactsCacheScopeRef = useRef('default')
|
const contactsCacheScopeRef = useRef('default')
|
||||||
|
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
|
||||||
|
|
||||||
const ensureContactsCacheScope = useCallback(async () => {
|
const ensureContactsCacheScope = useCallback(async () => {
|
||||||
if (contactsCacheScopeRef.current !== 'default') {
|
if (contactsCacheScopeRef.current !== 'default') {
|
||||||
@@ -130,6 +133,85 @@ function ContactsPage() {
|
|||||||
contactsLoadTimeoutMsRef.current = contactsLoadTimeoutMs
|
contactsLoadTimeoutMsRef.current = contactsLoadTimeoutMs
|
||||||
}, [contactsLoadTimeoutMs])
|
}, [contactsLoadTimeoutMs])
|
||||||
|
|
||||||
|
const mergeAvatarCacheIntoContacts = useCallback((sourceContacts: ContactInfo[]): ContactInfo[] => {
|
||||||
|
const avatarCache = contactsAvatarCacheRef.current
|
||||||
|
if (!sourceContacts.length || Object.keys(avatarCache).length === 0) {
|
||||||
|
return sourceContacts
|
||||||
|
}
|
||||||
|
let changed = false
|
||||||
|
const merged = sourceContacts.map((contact) => {
|
||||||
|
const cachedAvatar = avatarCache[contact.username]?.avatarUrl
|
||||||
|
if (!cachedAvatar || contact.avatarUrl) {
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...contact,
|
||||||
|
avatarUrl: cachedAvatar
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return changed ? merged : sourceContacts
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const upsertAvatarCacheFromContacts = useCallback((
|
||||||
|
scopeKey: string,
|
||||||
|
sourceContacts: ContactInfo[],
|
||||||
|
options?: { prune?: boolean; markCheckedUsernames?: string[] }
|
||||||
|
) => {
|
||||||
|
if (!scopeKey) return
|
||||||
|
const nextCache = { ...contactsAvatarCacheRef.current }
|
||||||
|
const now = Date.now()
|
||||||
|
const markCheckedSet = new Set((options?.markCheckedUsernames || []).filter(Boolean))
|
||||||
|
const usernamesInSource = new Set<string>()
|
||||||
|
let changed = false
|
||||||
|
|
||||||
|
for (const contact of sourceContacts) {
|
||||||
|
const username = String(contact.username || '').trim()
|
||||||
|
if (!username) continue
|
||||||
|
usernamesInSource.add(username)
|
||||||
|
const prev = nextCache[username]
|
||||||
|
const avatarUrl = String(contact.avatarUrl || '').trim()
|
||||||
|
if (!avatarUrl) continue
|
||||||
|
const updatedAt = !prev || prev.avatarUrl !== avatarUrl ? now : prev.updatedAt
|
||||||
|
const checkedAt = markCheckedSet.has(username) ? now : (prev?.checkedAt || now)
|
||||||
|
if (!prev || prev.avatarUrl !== avatarUrl || prev.updatedAt !== updatedAt || prev.checkedAt !== checkedAt) {
|
||||||
|
nextCache[username] = {
|
||||||
|
avatarUrl,
|
||||||
|
updatedAt,
|
||||||
|
checkedAt
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const username of markCheckedSet) {
|
||||||
|
const prev = nextCache[username]
|
||||||
|
if (!prev) continue
|
||||||
|
if (prev.checkedAt !== now) {
|
||||||
|
nextCache[username] = {
|
||||||
|
...prev,
|
||||||
|
checkedAt: now
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.prune) {
|
||||||
|
for (const username of Object.keys(nextCache)) {
|
||||||
|
if (usernamesInSource.has(username)) continue
|
||||||
|
delete nextCache[username]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return
|
||||||
|
contactsAvatarCacheRef.current = nextCache
|
||||||
|
setAvatarCacheUpdatedAt(now)
|
||||||
|
void configService.setContactsAvatarCache(scopeKey, nextCache).catch((error) => {
|
||||||
|
console.error('写入通讯录头像缓存失败:', error)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const applyEnrichedContacts = useCallback((enrichedMap: Record<string, ContactEnrichInfo>) => {
|
const applyEnrichedContacts = useCallback((enrichedMap: Record<string, ContactEnrichInfo>) => {
|
||||||
if (!enrichedMap || Object.keys(enrichedMap).length === 0) return
|
if (!enrichedMap || Object.keys(enrichedMap).length === 0) return
|
||||||
|
|
||||||
@@ -170,8 +252,34 @@ function ContactsPage() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const enrichContactsInBackground = useCallback(async (sourceContacts: ContactInfo[], loadVersion: number) => {
|
const enrichContactsInBackground = useCallback(async (
|
||||||
const usernames = sourceContacts.map(contact => contact.username).filter(Boolean)
|
sourceContacts: ContactInfo[],
|
||||||
|
loadVersion: number,
|
||||||
|
scopeKey: string
|
||||||
|
) => {
|
||||||
|
const sourceByUsername = new Map<string, ContactInfo>()
|
||||||
|
for (const contact of sourceContacts) {
|
||||||
|
if (!contact.username) continue
|
||||||
|
sourceByUsername.set(contact.username, contact)
|
||||||
|
}
|
||||||
|
const now = Date.now()
|
||||||
|
const usernames = sourceContacts
|
||||||
|
.map(contact => contact.username)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((username) => {
|
||||||
|
const currentContact = sourceByUsername.get(username)
|
||||||
|
if (!currentContact) return false
|
||||||
|
const cacheEntry = contactsAvatarCacheRef.current[username]
|
||||||
|
if (!cacheEntry || !cacheEntry.avatarUrl) {
|
||||||
|
return !currentContact.avatarUrl
|
||||||
|
}
|
||||||
|
if (currentContact.avatarUrl && currentContact.avatarUrl !== cacheEntry.avatarUrl) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const checkedAt = cacheEntry.checkedAt || 0
|
||||||
|
return now - checkedAt >= AVATAR_RECHECK_INTERVAL_MS
|
||||||
|
})
|
||||||
|
|
||||||
const total = usernames.length
|
const total = usernames.length
|
||||||
setAvatarEnrichProgress({
|
setAvatarEnrichProgress({
|
||||||
loaded: 0,
|
loaded: 0,
|
||||||
@@ -190,7 +298,22 @@ function ContactsPage() {
|
|||||||
if (loadVersionRef.current !== loadVersion) return
|
if (loadVersionRef.current !== loadVersion) return
|
||||||
if (avatarResult.success && avatarResult.contacts) {
|
if (avatarResult.success && avatarResult.contacts) {
|
||||||
applyEnrichedContacts(avatarResult.contacts)
|
applyEnrichedContacts(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))
|
||||||
|
upsertAvatarCacheFromContacts(scopeKey, batchContacts, {
|
||||||
|
markCheckedUsernames: batch
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('分批补全头像失败:', e)
|
console.error('分批补全头像失败:', e)
|
||||||
}
|
}
|
||||||
@@ -204,7 +327,7 @@ function ContactsPage() {
|
|||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
}
|
}
|
||||||
}, [applyEnrichedContacts])
|
}, [applyEnrichedContacts, upsertAvatarCacheFromContacts])
|
||||||
|
|
||||||
// 加载通讯录
|
// 加载通讯录
|
||||||
const loadContacts = useCallback(async (options?: { scopeKey?: string }) => {
|
const loadContacts = useCallback(async (options?: { scopeKey?: string }) => {
|
||||||
@@ -256,21 +379,23 @@ function ContactsPage() {
|
|||||||
window.clearTimeout(loadTimeoutTimerRef.current)
|
window.clearTimeout(loadTimeoutTimerRef.current)
|
||||||
loadTimeoutTimerRef.current = null
|
loadTimeoutTimerRef.current = null
|
||||||
}
|
}
|
||||||
setContacts(contactsResult.contacts)
|
const contactsWithAvatarCache = mergeAvatarCacheIntoContacts(contactsResult.contacts)
|
||||||
syncContactTypeCounts(contactsResult.contacts)
|
setContacts(contactsWithAvatarCache)
|
||||||
|
syncContactTypeCounts(contactsWithAvatarCache)
|
||||||
setSelectedUsernames(new Set())
|
setSelectedUsernames(new Set())
|
||||||
setSelectedContact(prev => {
|
setSelectedContact(prev => {
|
||||||
if (!prev) return prev
|
if (!prev) return prev
|
||||||
return contactsResult.contacts!.find(contact => contact.username === prev.username) || null
|
return contactsWithAvatarCache.find(contact => contact.username === prev.username) || null
|
||||||
})
|
})
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
setContactsDataSource('network')
|
setContactsDataSource('network')
|
||||||
setContactsUpdatedAt(now)
|
setContactsUpdatedAt(now)
|
||||||
setLoadIssue(null)
|
setLoadIssue(null)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
upsertAvatarCacheFromContacts(scopeKey, contactsWithAvatarCache, { prune: true })
|
||||||
void configService.setContactsListCache(
|
void configService.setContactsListCache(
|
||||||
scopeKey,
|
scopeKey,
|
||||||
contactsResult.contacts.map(contact => ({
|
contactsWithAvatarCache.map(contact => ({
|
||||||
username: contact.username,
|
username: contact.username,
|
||||||
displayName: contact.displayName,
|
displayName: contact.displayName,
|
||||||
remark: contact.remark,
|
remark: contact.remark,
|
||||||
@@ -280,7 +405,7 @@ function ContactsPage() {
|
|||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
console.error('写入通讯录缓存失败:', error)
|
console.error('写入通讯录缓存失败:', error)
|
||||||
})
|
})
|
||||||
void enrichContactsInBackground(contactsResult.contacts, loadVersion)
|
void enrichContactsInBackground(contactsWithAvatarCache, loadVersion, scopeKey)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const elapsedMs = Date.now() - startedAt
|
const elapsedMs = Date.now() - startedAt
|
||||||
@@ -314,7 +439,13 @@ function ContactsPage() {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [ensureContactsCacheScope, enrichContactsInBackground, syncContactTypeCounts])
|
}, [
|
||||||
|
ensureContactsCacheScope,
|
||||||
|
enrichContactsInBackground,
|
||||||
|
mergeAvatarCacheIntoContacts,
|
||||||
|
syncContactTypeCounts,
|
||||||
|
upsertAvatarCacheFromContacts
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -322,11 +453,17 @@ function ContactsPage() {
|
|||||||
const scopeKey = await ensureContactsCacheScope()
|
const scopeKey = await ensureContactsCacheScope()
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
try {
|
try {
|
||||||
const cacheItem = await configService.getContactsListCache(scopeKey)
|
const [cacheItem, avatarCacheItem] = await Promise.all([
|
||||||
|
configService.getContactsListCache(scopeKey),
|
||||||
|
configService.getContactsAvatarCache(scopeKey)
|
||||||
|
])
|
||||||
|
const avatarCacheMap = avatarCacheItem?.avatars || {}
|
||||||
|
contactsAvatarCacheRef.current = avatarCacheMap
|
||||||
|
setAvatarCacheUpdatedAt(avatarCacheItem?.updatedAt || null)
|
||||||
if (!cancelled && cacheItem && Array.isArray(cacheItem.contacts) && cacheItem.contacts.length > 0) {
|
if (!cancelled && cacheItem && Array.isArray(cacheItem.contacts) && cacheItem.contacts.length > 0) {
|
||||||
const cachedContacts: ContactInfo[] = cacheItem.contacts.map(contact => ({
|
const cachedContacts: ContactInfo[] = cacheItem.contacts.map(contact => ({
|
||||||
...contact,
|
...contact,
|
||||||
avatarUrl: undefined
|
avatarUrl: avatarCacheMap[contact.username]?.avatarUrl
|
||||||
}))
|
}))
|
||||||
setContacts(cachedContacts)
|
setContacts(cachedContacts)
|
||||||
syncContactTypeCounts(cachedContacts)
|
syncContactTypeCounts(cachedContacts)
|
||||||
@@ -503,6 +640,17 @@ function ContactsPage() {
|
|||||||
return new Date(contactsUpdatedAt).toLocaleString()
|
return new Date(contactsUpdatedAt).toLocaleString()
|
||||||
}, [contactsUpdatedAt])
|
}, [contactsUpdatedAt])
|
||||||
|
|
||||||
|
const avatarCachedCount = useMemo(() => {
|
||||||
|
return contacts.reduce((count, contact) => (
|
||||||
|
contact.avatarUrl ? count + 1 : count
|
||||||
|
), 0)
|
||||||
|
}, [contacts])
|
||||||
|
|
||||||
|
const avatarCacheUpdatedAtLabel = useMemo(() => {
|
||||||
|
if (!avatarCacheUpdatedAt) return ''
|
||||||
|
return new Date(avatarCacheUpdatedAt).toLocaleString()
|
||||||
|
}, [avatarCacheUpdatedAt])
|
||||||
|
|
||||||
const toggleContactSelected = (username: string, checked: boolean) => {
|
const toggleContactSelected = (username: string, checked: boolean) => {
|
||||||
setSelectedUsernames(prev => {
|
setSelectedUsernames(prev => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
@@ -686,6 +834,12 @@ function ContactsPage() {
|
|||||||
{contactsDataSource === 'cache' ? '缓存' : '最新'} · 更新于 {contactsUpdatedAtLabel}
|
{contactsDataSource === 'cache' ? '缓存' : '最新'} · 更新于 {contactsUpdatedAtLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{contacts.length > 0 && (
|
||||||
|
<span className="contacts-cache-meta">
|
||||||
|
头像缓存 {avatarCachedCount}/{contacts.length}
|
||||||
|
{avatarCacheUpdatedAtLabel ? ` · 更新于 ${avatarCacheUpdatedAtLabel}` : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isLoading && contacts.length > 0 && (
|
{isLoading && contacts.length > 0 && (
|
||||||
<span className="contacts-cache-meta syncing">后台同步中...</span>
|
<span className="contacts-cache-meta syncing">后台同步中...</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const CONFIG_KEYS = {
|
|||||||
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
|
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
|
||||||
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
|
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
|
||||||
CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap',
|
CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap',
|
||||||
|
CONTACTS_AVATAR_CACHE_MAP: 'contactsAvatarCacheMap',
|
||||||
|
|
||||||
// 安全
|
// 安全
|
||||||
AUTH_ENABLED: 'authEnabled',
|
AUTH_ENABLED: 'authEnabled',
|
||||||
@@ -477,6 +478,17 @@ export interface ContactsListCacheItem {
|
|||||||
contacts: ContactsListCacheContact[]
|
contacts: ContactsListCacheContact[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContactsAvatarCacheEntry {
|
||||||
|
avatarUrl: string
|
||||||
|
updatedAt: number
|
||||||
|
checkedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactsAvatarCacheItem {
|
||||||
|
updatedAt: number
|
||||||
|
avatars: Record<string, ContactsAvatarCacheEntry>
|
||||||
|
}
|
||||||
|
|
||||||
export async function getExportSessionMessageCountCache(scopeKey: string): Promise<ExportSessionMessageCountCacheItem | null> {
|
export async function getExportSessionMessageCountCache(scopeKey: string): Promise<ExportSessionMessageCountCacheItem | null> {
|
||||||
if (!scopeKey) return null
|
if (!scopeKey) return null
|
||||||
const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP)
|
const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP)
|
||||||
@@ -650,6 +662,94 @@ export async function setContactsListCache(scopeKey: string, contacts: ContactsL
|
|||||||
await config.set(CONFIG_KEYS.CONTACTS_LIST_CACHE_MAP, map)
|
await config.set(CONFIG_KEYS.CONTACTS_LIST_CACHE_MAP, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getContactsAvatarCache(scopeKey: string): Promise<ContactsAvatarCacheItem | null> {
|
||||||
|
if (!scopeKey) return null
|
||||||
|
const value = await config.get(CONFIG_KEYS.CONTACTS_AVATAR_CACHE_MAP)
|
||||||
|
if (!value || typeof value !== 'object') return null
|
||||||
|
const rawMap = value as Record<string, unknown>
|
||||||
|
const rawItem = rawMap[scopeKey]
|
||||||
|
if (!rawItem || typeof rawItem !== 'object') return null
|
||||||
|
|
||||||
|
const rawUpdatedAt = (rawItem as Record<string, unknown>).updatedAt
|
||||||
|
const rawAvatars = (rawItem as Record<string, unknown>).avatars
|
||||||
|
if (!rawAvatars || typeof rawAvatars !== 'object') return null
|
||||||
|
|
||||||
|
const avatars: Record<string, ContactsAvatarCacheEntry> = {}
|
||||||
|
for (const [rawUsername, rawEntry] of Object.entries(rawAvatars as Record<string, unknown>)) {
|
||||||
|
const username = rawUsername.trim()
|
||||||
|
if (!username) continue
|
||||||
|
|
||||||
|
if (typeof rawEntry === 'string') {
|
||||||
|
const avatarUrl = rawEntry.trim()
|
||||||
|
if (!avatarUrl) continue
|
||||||
|
avatars[username] = {
|
||||||
|
avatarUrl,
|
||||||
|
updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0,
|
||||||
|
checkedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawEntry || typeof rawEntry !== 'object') continue
|
||||||
|
const entry = rawEntry as Record<string, unknown>
|
||||||
|
const avatarUrl = typeof entry.avatarUrl === 'string' ? entry.avatarUrl.trim() : ''
|
||||||
|
if (!avatarUrl) continue
|
||||||
|
const updatedAt = typeof entry.updatedAt === 'number' && Number.isFinite(entry.updatedAt)
|
||||||
|
? entry.updatedAt
|
||||||
|
: 0
|
||||||
|
const checkedAt = typeof entry.checkedAt === 'number' && Number.isFinite(entry.checkedAt)
|
||||||
|
? entry.checkedAt
|
||||||
|
: updatedAt
|
||||||
|
|
||||||
|
avatars[username] = {
|
||||||
|
avatarUrl,
|
||||||
|
updatedAt,
|
||||||
|
checkedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0,
|
||||||
|
avatars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setContactsAvatarCache(
|
||||||
|
scopeKey: string,
|
||||||
|
avatars: Record<string, ContactsAvatarCacheEntry>
|
||||||
|
): Promise<void> {
|
||||||
|
if (!scopeKey) return
|
||||||
|
const current = await config.get(CONFIG_KEYS.CONTACTS_AVATAR_CACHE_MAP)
|
||||||
|
const map = current && typeof current === 'object'
|
||||||
|
? { ...(current as Record<string, unknown>) }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const normalized: Record<string, ContactsAvatarCacheEntry> = {}
|
||||||
|
for (const [rawUsername, rawEntry] of Object.entries(avatars || {})) {
|
||||||
|
const username = String(rawUsername || '').trim()
|
||||||
|
if (!username || !rawEntry || typeof rawEntry !== 'object') continue
|
||||||
|
const avatarUrl = String(rawEntry.avatarUrl || '').trim()
|
||||||
|
if (!avatarUrl) continue
|
||||||
|
const updatedAt = Number.isFinite(rawEntry.updatedAt)
|
||||||
|
? Math.max(0, Math.floor(rawEntry.updatedAt))
|
||||||
|
: Date.now()
|
||||||
|
const checkedAt = Number.isFinite(rawEntry.checkedAt)
|
||||||
|
? Math.max(0, Math.floor(rawEntry.checkedAt))
|
||||||
|
: updatedAt
|
||||||
|
normalized[username] = {
|
||||||
|
avatarUrl,
|
||||||
|
updatedAt,
|
||||||
|
checkedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map[scopeKey] = {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
avatars: normalized
|
||||||
|
}
|
||||||
|
await config.set(CONFIG_KEYS.CONTACTS_AVATAR_CACHE_MAP, map)
|
||||||
|
}
|
||||||
|
|
||||||
// === 安全相关 ===
|
// === 安全相关 ===
|
||||||
|
|
||||||
export async function getAuthEnabled(): Promise<boolean> {
|
export async function getAuthEnabled(): Promise<boolean> {
|
||||||
|
|||||||
Reference in New Issue
Block a user