mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
perf(export): persist session list stats across app restarts
This commit is contained in:
@@ -461,6 +461,8 @@ const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(
|
|||||||
const createExportDiagTraceId = (): string => `export-card-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
const createExportDiagTraceId = (): string => `export-card-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||||
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
||||||
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
||||||
|
const EXPORT_SESSION_MESSAGE_COUNT_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
||||||
|
const EXPORT_SESSION_CONTENT_METRIC_CACHE_STALE_MS = 12 * 60 * 60 * 1000
|
||||||
const EXPORT_AVATAR_ENRICH_BATCH_SIZE = 80
|
const EXPORT_AVATAR_ENRICH_BATCH_SIZE = 80
|
||||||
const CONTACTS_LIST_VIRTUAL_ROW_HEIGHT = 76
|
const CONTACTS_LIST_VIRTUAL_ROW_HEIGHT = 76
|
||||||
const CONTACTS_LIST_VIRTUAL_OVERSCAN = 10
|
const CONTACTS_LIST_VIRTUAL_OVERSCAN = 10
|
||||||
@@ -637,6 +639,14 @@ const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeTimestampToMs = (value: unknown): number => {
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return 0
|
||||||
|
if (value < 1000000000000) {
|
||||||
|
return Math.floor(value * 1000)
|
||||||
|
}
|
||||||
|
return Math.floor(value)
|
||||||
|
}
|
||||||
|
|
||||||
const toContactMapFromCaches = (
|
const toContactMapFromCaches = (
|
||||||
contacts: configService.ContactsListCacheContact[],
|
contacts: configService.ContactsListCacheContact[],
|
||||||
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
|
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
|
||||||
@@ -1551,7 +1561,11 @@ function ExportPage() {
|
|||||||
const loadSessionContentStats = useCallback(async (
|
const loadSessionContentStats = useCallback(async (
|
||||||
sourceSessions: SessionRow[],
|
sourceSessions: SessionRow[],
|
||||||
priorityTab: ConversationTab,
|
priorityTab: ConversationTab,
|
||||||
resolvedMessageCounts?: Record<string, number>
|
resolvedMessageCounts?: Record<string, number>,
|
||||||
|
options?: {
|
||||||
|
scopeKey?: string
|
||||||
|
seededMetrics?: Record<string, SessionContentMetric>
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
const requestId = sessionContentStatsRequestIdRef.current + 1
|
const requestId = sessionContentStatsRequestIdRef.current + 1
|
||||||
sessionContentStatsRequestIdRef.current = requestId
|
sessionContentStatsRequestIdRef.current = requestId
|
||||||
@@ -1603,6 +1617,33 @@ function ExportPage() {
|
|||||||
|
|
||||||
const total = orderedSessionIds.length
|
const total = orderedSessionIds.length
|
||||||
const processedSessionIds = new Set<string>()
|
const processedSessionIds = new Set<string>()
|
||||||
|
const mergedMetrics: Record<string, configService.ExportSessionContentMetricCacheEntry> = {}
|
||||||
|
for (const [sessionId, metricRaw] of Object.entries(options?.seededMetrics || {})) {
|
||||||
|
const metric: configService.ExportSessionContentMetricCacheEntry = {}
|
||||||
|
const totalMessages = normalizeMessageCount(metricRaw.totalMessages)
|
||||||
|
const voiceMessages = normalizeMessageCount(metricRaw.voiceMessages)
|
||||||
|
const imageMessages = normalizeMessageCount(metricRaw.imageMessages)
|
||||||
|
const videoMessages = normalizeMessageCount(metricRaw.videoMessages)
|
||||||
|
const emojiMessages = normalizeMessageCount(metricRaw.emojiMessages)
|
||||||
|
if (typeof totalMessages === 'number') metric.totalMessages = totalMessages
|
||||||
|
if (typeof voiceMessages === 'number') metric.voiceMessages = voiceMessages
|
||||||
|
if (typeof imageMessages === 'number') metric.imageMessages = imageMessages
|
||||||
|
if (typeof videoMessages === 'number') metric.videoMessages = videoMessages
|
||||||
|
if (typeof emojiMessages === 'number') metric.emojiMessages = emojiMessages
|
||||||
|
if (Object.keys(metric).length > 0) {
|
||||||
|
mergedMetrics[sessionId] = metric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [sessionId, countRaw] of Object.entries(resolvedMessageCounts || {})) {
|
||||||
|
const count = normalizeMessageCount(countRaw)
|
||||||
|
if (typeof count !== 'number') continue
|
||||||
|
mergedMetrics[sessionId] = {
|
||||||
|
...(mergedMetrics[sessionId] || {}),
|
||||||
|
totalMessages: count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const markChunkProcessed = (chunk: string[]) => {
|
const markChunkProcessed = (chunk: string[]) => {
|
||||||
for (const sessionId of chunk) {
|
for (const sessionId of chunk) {
|
||||||
processedSessionIds.add(sessionId)
|
processedSessionIds.add(sessionId)
|
||||||
@@ -1624,6 +1665,25 @@ function ExportPage() {
|
|||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
if (result?.success && result.data) {
|
if (result?.success && result.data) {
|
||||||
mergeSessionContentMetrics(result.data as Record<string, SessionExportMetric | undefined>)
|
mergeSessionContentMetrics(result.data as Record<string, SessionExportMetric | undefined>)
|
||||||
|
for (const [sessionId, metricRaw] of Object.entries(result.data as Record<string, SessionExportMetric | undefined>)) {
|
||||||
|
if (!metricRaw) continue
|
||||||
|
const metric: configService.ExportSessionContentMetricCacheEntry = {}
|
||||||
|
const totalMessages = normalizeMessageCount(metricRaw.totalMessages)
|
||||||
|
const voiceMessages = normalizeMessageCount(metricRaw.voiceMessages)
|
||||||
|
const imageMessages = normalizeMessageCount(metricRaw.imageMessages)
|
||||||
|
const videoMessages = normalizeMessageCount(metricRaw.videoMessages)
|
||||||
|
const emojiMessages = normalizeMessageCount(metricRaw.emojiMessages)
|
||||||
|
if (typeof totalMessages === 'number') metric.totalMessages = totalMessages
|
||||||
|
if (typeof voiceMessages === 'number') metric.voiceMessages = voiceMessages
|
||||||
|
if (typeof imageMessages === 'number') metric.imageMessages = imageMessages
|
||||||
|
if (typeof videoMessages === 'number') metric.videoMessages = videoMessages
|
||||||
|
if (typeof emojiMessages === 'number') metric.emojiMessages = emojiMessages
|
||||||
|
if (Object.keys(metric).length === 0) continue
|
||||||
|
mergedMetrics[sessionId] = {
|
||||||
|
...(mergedMetrics[sessionId] || {}),
|
||||||
|
...metric
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
markChunkProcessed(chunk)
|
markChunkProcessed(chunk)
|
||||||
}
|
}
|
||||||
@@ -1664,13 +1724,24 @@ function ExportPage() {
|
|||||||
if (!isStale()) {
|
if (!isStale()) {
|
||||||
setSessionContentStatsProgress({ completed: processedSessionIds.size, total })
|
setSessionContentStatsProgress({ completed: processedSessionIds.size, total })
|
||||||
setIsLoadingSessionContentStats(false)
|
setIsLoadingSessionContentStats(false)
|
||||||
|
if (options?.scopeKey && Object.keys(mergedMetrics).length > 0) {
|
||||||
|
try {
|
||||||
|
await configService.setExportSessionContentMetricCache(options.scopeKey, mergedMetrics)
|
||||||
|
} catch (cacheError) {
|
||||||
|
console.error('写入导出页会话媒体统计缓存失败:', cacheError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [mergeSessionContentMetrics])
|
}, [mergeSessionContentMetrics])
|
||||||
|
|
||||||
const loadSessionMessageCounts = useCallback(async (
|
const loadSessionMessageCounts = useCallback(async (
|
||||||
sourceSessions: SessionRow[],
|
sourceSessions: SessionRow[],
|
||||||
priorityTab: ConversationTab
|
priorityTab: ConversationTab,
|
||||||
|
options?: {
|
||||||
|
scopeKey?: string
|
||||||
|
seededCounts?: Record<string, number>
|
||||||
|
}
|
||||||
): Promise<Record<string, number>> => {
|
): Promise<Record<string, number>> => {
|
||||||
const requestId = sessionCountRequestIdRef.current + 1
|
const requestId = sessionCountRequestIdRef.current + 1
|
||||||
sessionCountRequestIdRef.current = requestId
|
sessionCountRequestIdRef.current = requestId
|
||||||
@@ -1684,11 +1755,19 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
const accumulatedCounts: Record<string, number> = { ...seededHintCounts }
|
const seededPersistentCounts = Object.entries(options?.seededCounts || {}).reduce<Record<string, number>>((acc, [sessionId, countRaw]) => {
|
||||||
setSessionMessageCounts(seededHintCounts)
|
const nextCount = normalizeMessageCount(countRaw)
|
||||||
if (Object.keys(seededHintCounts).length > 0) {
|
if (typeof nextCount === 'number') {
|
||||||
|
acc[sessionId] = nextCount
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
const seededCounts = { ...seededHintCounts, ...seededPersistentCounts }
|
||||||
|
const accumulatedCounts: Record<string, number> = { ...seededCounts }
|
||||||
|
setSessionMessageCounts(seededCounts)
|
||||||
|
if (Object.keys(seededCounts).length > 0) {
|
||||||
mergeSessionContentMetrics(
|
mergeSessionContentMetrics(
|
||||||
Object.entries(seededHintCounts).reduce<Record<string, SessionContentMetric>>((acc, [sessionId, count]) => {
|
Object.entries(seededCounts).reduce<Record<string, SessionContentMetric>>((acc, [sessionId, count]) => {
|
||||||
acc[sessionId] = { totalMessages: count }
|
acc[sessionId] = { totalMessages: count }
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
@@ -1700,11 +1779,20 @@ function ExportPage() {
|
|||||||
return { ...accumulatedCounts }
|
return { ...accumulatedCounts }
|
||||||
}
|
}
|
||||||
|
|
||||||
const prioritizedSessionIds = exportableSessions
|
const pendingSessions = exportableSessions.filter((session) => {
|
||||||
|
const nextCount = normalizeMessageCount(accumulatedCounts[session.username])
|
||||||
|
return typeof nextCount !== 'number'
|
||||||
|
})
|
||||||
|
if (pendingSessions.length === 0) {
|
||||||
|
setIsLoadingSessionCounts(false)
|
||||||
|
return { ...accumulatedCounts }
|
||||||
|
}
|
||||||
|
|
||||||
|
const prioritizedSessionIds = pendingSessions
|
||||||
.filter(session => session.kind === priorityTab)
|
.filter(session => session.kind === priorityTab)
|
||||||
.map(session => session.username)
|
.map(session => session.username)
|
||||||
const prioritizedSet = new Set(prioritizedSessionIds)
|
const prioritizedSet = new Set(prioritizedSessionIds)
|
||||||
const remainingSessionIds = exportableSessions
|
const remainingSessionIds = pendingSessions
|
||||||
.filter(session => !prioritizedSet.has(session.username))
|
.filter(session => !prioritizedSet.has(session.username))
|
||||||
.map(session => session.username)
|
.map(session => session.username)
|
||||||
|
|
||||||
@@ -1752,6 +1840,13 @@ function ExportPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!isStale()) {
|
if (!isStale()) {
|
||||||
setIsLoadingSessionCounts(false)
|
setIsLoadingSessionCounts(false)
|
||||||
|
if (options?.scopeKey && Object.keys(accumulatedCounts).length > 0) {
|
||||||
|
try {
|
||||||
|
await configService.setExportSessionMessageCountCache(options.scopeKey, accumulatedCounts)
|
||||||
|
} catch (cacheError) {
|
||||||
|
console.error('写入导出页会话总消息缓存失败:', cacheError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ...accumulatedCounts }
|
return { ...accumulatedCounts }
|
||||||
@@ -1777,11 +1872,21 @@ function ExportPage() {
|
|||||||
const scopeKey = await ensureExportCacheScope()
|
const scopeKey = await ensureExportCacheScope()
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
|
||||||
|
const [
|
||||||
|
cachedContactsPayload,
|
||||||
|
cachedMessageCountsPayload,
|
||||||
|
cachedContentMetricsPayload
|
||||||
|
] = await Promise.all([
|
||||||
|
loadContactsCaches(scopeKey),
|
||||||
|
configService.getExportSessionMessageCountCache(scopeKey),
|
||||||
|
configService.getExportSessionContentMetricCache(scopeKey)
|
||||||
|
])
|
||||||
|
if (isStale()) return
|
||||||
|
|
||||||
const {
|
const {
|
||||||
contactsItem: cachedContactsItem,
|
contactsItem: cachedContactsItem,
|
||||||
avatarItem: cachedAvatarItem
|
avatarItem: cachedAvatarItem
|
||||||
} = await loadContactsCaches(scopeKey)
|
} = cachedContactsPayload
|
||||||
if (isStale()) return
|
|
||||||
|
|
||||||
const cachedContacts = cachedContactsItem?.contacts || []
|
const cachedContacts = cachedContactsItem?.contacts || []
|
||||||
const cachedAvatarEntries = cachedAvatarItem?.avatars || {}
|
const cachedAvatarEntries = cachedAvatarItem?.avatars || {}
|
||||||
@@ -1808,14 +1913,110 @@ function ExportPage() {
|
|||||||
if (sessionsResult.success && sessionsResult.sessions) {
|
if (sessionsResult.success && sessionsResult.sessions) {
|
||||||
const rawSessions = sessionsResult.sessions
|
const rawSessions = sessionsResult.sessions
|
||||||
const baseSessions = toSessionRowsWithContacts(rawSessions, cachedContactMap)
|
const baseSessions = toSessionRowsWithContacts(rawSessions, cachedContactMap)
|
||||||
|
const exportableSessionIds = baseSessions
|
||||||
|
.filter((session) => session.hasSession)
|
||||||
|
.map((session) => session.username)
|
||||||
|
const exportableSessionIdSet = new Set(exportableSessionIds)
|
||||||
|
|
||||||
|
const cachedMessageCounts = Object.entries(cachedMessageCountsPayload?.counts || {}).reduce<Record<string, number>>((acc, [sessionId, countRaw]) => {
|
||||||
|
if (!exportableSessionIdSet.has(sessionId)) return acc
|
||||||
|
const nextCount = normalizeMessageCount(countRaw)
|
||||||
|
if (typeof nextCount === 'number') {
|
||||||
|
acc[sessionId] = nextCount
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const cachedContentMetrics = Object.entries(cachedContentMetricsPayload?.metrics || {}).reduce<Record<string, SessionContentMetric>>((acc, [sessionId, metricRaw]) => {
|
||||||
|
if (!exportableSessionIdSet.has(sessionId)) return acc
|
||||||
|
if (!metricRaw || typeof metricRaw !== 'object') return acc
|
||||||
|
const metric: SessionContentMetric = {}
|
||||||
|
const totalMessages = normalizeMessageCount(metricRaw.totalMessages)
|
||||||
|
const voiceMessages = normalizeMessageCount(metricRaw.voiceMessages)
|
||||||
|
const imageMessages = normalizeMessageCount(metricRaw.imageMessages)
|
||||||
|
const videoMessages = normalizeMessageCount(metricRaw.videoMessages)
|
||||||
|
const emojiMessages = normalizeMessageCount(metricRaw.emojiMessages)
|
||||||
|
if (typeof totalMessages === 'number') metric.totalMessages = totalMessages
|
||||||
|
if (typeof voiceMessages === 'number') metric.voiceMessages = voiceMessages
|
||||||
|
if (typeof imageMessages === 'number') metric.imageMessages = imageMessages
|
||||||
|
if (typeof videoMessages === 'number') metric.videoMessages = videoMessages
|
||||||
|
if (typeof emojiMessages === 'number') metric.emojiMessages = emojiMessages
|
||||||
|
if (Object.keys(metric).length > 0) {
|
||||||
|
acc[sessionId] = metric
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const cachedCountAsMetrics = Object.entries(cachedMessageCounts).reduce<Record<string, SessionContentMetric>>((acc, [sessionId, count]) => {
|
||||||
|
if (typeof normalizeMessageCount(cachedContentMetrics[sessionId]?.totalMessages) === 'number') return acc
|
||||||
|
acc[sessionId] = { totalMessages: count }
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const latestSessionActivityMs = baseSessions.reduce((maxTs, session) => {
|
||||||
|
const activityTs = normalizeTimestampToMs(session.sortTimestamp || session.lastTimestamp || 0)
|
||||||
|
return Math.max(maxTs, activityTs)
|
||||||
|
}, 0)
|
||||||
|
const messageCountCacheUpdatedAt = normalizeTimestampToMs(cachedMessageCountsPayload?.updatedAt || 0)
|
||||||
|
const contentMetricCacheUpdatedAt = normalizeTimestampToMs(cachedContentMetricsPayload?.updatedAt || 0)
|
||||||
|
const isMessageCountCacheFresh = (
|
||||||
|
messageCountCacheUpdatedAt > 0 &&
|
||||||
|
Date.now() - messageCountCacheUpdatedAt <= EXPORT_SESSION_MESSAGE_COUNT_CACHE_STALE_MS &&
|
||||||
|
latestSessionActivityMs <= messageCountCacheUpdatedAt
|
||||||
|
)
|
||||||
|
const isContentMetricCacheFresh = (
|
||||||
|
contentMetricCacheUpdatedAt > 0 &&
|
||||||
|
Date.now() - contentMetricCacheUpdatedAt <= EXPORT_SESSION_CONTENT_METRIC_CACHE_STALE_MS &&
|
||||||
|
latestSessionActivityMs <= contentMetricCacheUpdatedAt
|
||||||
|
)
|
||||||
|
const hasMessageCountCoverage = exportableSessionIds.every((sessionId) => (
|
||||||
|
typeof normalizeMessageCount(cachedMessageCounts[sessionId]) === 'number'
|
||||||
|
))
|
||||||
|
const hasContentMetricCoverage = exportableSessionIds.every((sessionId) => {
|
||||||
|
const metric = cachedContentMetrics[sessionId]
|
||||||
|
if (!metric) return false
|
||||||
|
return (
|
||||||
|
typeof normalizeMessageCount(metric.imageMessages) === 'number' &&
|
||||||
|
typeof normalizeMessageCount(metric.voiceMessages) === 'number' &&
|
||||||
|
typeof normalizeMessageCount(metric.emojiMessages) === 'number' &&
|
||||||
|
typeof normalizeMessageCount(metric.videoMessages) === 'number'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
if (Object.keys(cachedMessageCounts).length > 0) {
|
||||||
|
setSessionMessageCounts(cachedMessageCounts)
|
||||||
|
}
|
||||||
|
if (Object.keys(cachedContentMetrics).length > 0) {
|
||||||
|
mergeSessionContentMetrics(cachedContentMetrics)
|
||||||
|
}
|
||||||
|
if (Object.keys(cachedCountAsMetrics).length > 0) {
|
||||||
|
mergeSessionContentMetrics(cachedCountAsMetrics)
|
||||||
|
}
|
||||||
setSessions(baseSessions)
|
setSessions(baseSessions)
|
||||||
sessionsHydratedAtRef.current = Date.now()
|
sessionsHydratedAtRef.current = Date.now()
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const resolvedMessageCounts = await loadSessionMessageCounts(baseSessions, activeTabRef.current)
|
let resolvedMessageCounts = { ...cachedMessageCounts }
|
||||||
|
const shouldRefreshMessageCounts = !isMessageCountCacheFresh || !hasMessageCountCoverage
|
||||||
|
if (shouldRefreshMessageCounts) {
|
||||||
|
resolvedMessageCounts = await loadSessionMessageCounts(baseSessions, activeTabRef.current, {
|
||||||
|
scopeKey,
|
||||||
|
seededCounts: cachedMessageCounts
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setIsLoadingSessionCounts(false)
|
||||||
|
}
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
await loadSessionContentStats(baseSessions, activeTabRef.current, resolvedMessageCounts)
|
const shouldRefreshContentStats = !isContentMetricCacheFresh || !hasContentMetricCoverage
|
||||||
|
if (shouldRefreshContentStats) {
|
||||||
|
await loadSessionContentStats(baseSessions, activeTabRef.current, resolvedMessageCounts, {
|
||||||
|
scopeKey,
|
||||||
|
seededMetrics: cachedContentMetrics
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setIsLoadingSessionContentStats(false)
|
||||||
|
setSessionContentStatsProgress({ completed: 0, total: 0 })
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
setSessionDataSource(cachedContacts.length > 0 ? 'cache' : 'network')
|
setSessionDataSource(cachedContacts.length > 0 ? 'cache' : 'network')
|
||||||
if (cachedContacts.length === 0) {
|
if (cachedContacts.length === 0) {
|
||||||
@@ -2005,7 +2206,7 @@ function ExportPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!isStale()) setIsLoading(false)
|
if (!isStale()) setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [ensureExportCacheScope, loadContactsCaches, loadSessionContentStats, loadSessionMessageCounts, syncContactTypeCounts])
|
}, [ensureExportCacheScope, loadContactsCaches, loadSessionContentStats, loadSessionMessageCounts, mergeSessionContentMetrics, syncContactTypeCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExportRoute) return
|
if (!isExportRoute) return
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const CONFIG_KEYS = {
|
|||||||
EXPORT_LAST_CONTENT_RUN_MAP: 'exportLastContentRunMap',
|
EXPORT_LAST_CONTENT_RUN_MAP: 'exportLastContentRunMap',
|
||||||
EXPORT_LAST_SNS_POST_COUNT: 'exportLastSnsPostCount',
|
EXPORT_LAST_SNS_POST_COUNT: 'exportLastSnsPostCount',
|
||||||
EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap',
|
EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap',
|
||||||
|
EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap',
|
||||||
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
|
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
|
||||||
SNS_PAGE_CACHE_MAP: 'snsPageCacheMap',
|
SNS_PAGE_CACHE_MAP: 'snsPageCacheMap',
|
||||||
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
|
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
|
||||||
@@ -460,6 +461,19 @@ export interface ExportSessionMessageCountCacheItem {
|
|||||||
counts: Record<string, number>
|
counts: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportSessionContentMetricCacheEntry {
|
||||||
|
totalMessages?: number
|
||||||
|
voiceMessages?: number
|
||||||
|
imageMessages?: number
|
||||||
|
videoMessages?: number
|
||||||
|
emojiMessages?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportSessionContentMetricCacheItem {
|
||||||
|
updatedAt: number
|
||||||
|
metrics: Record<string, ExportSessionContentMetricCacheEntry>
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExportSnsStatsCacheItem {
|
export interface ExportSnsStatsCacheItem {
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
totalPosts: number
|
totalPosts: number
|
||||||
@@ -550,6 +564,88 @@ export async function setExportSessionMessageCountCache(scopeKey: string, counts
|
|||||||
await config.set(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP, map)
|
await config.set(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExportSessionContentMetricCache(scopeKey: string): Promise<ExportSessionContentMetricCacheItem | null> {
|
||||||
|
if (!scopeKey) return null
|
||||||
|
const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_CONTENT_METRIC_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 rawMetrics = (rawItem as Record<string, unknown>).metrics
|
||||||
|
if (!rawMetrics || typeof rawMetrics !== 'object') return null
|
||||||
|
|
||||||
|
const metrics: Record<string, ExportSessionContentMetricCacheEntry> = {}
|
||||||
|
for (const [sessionId, rawMetric] of Object.entries(rawMetrics as Record<string, unknown>)) {
|
||||||
|
if (!rawMetric || typeof rawMetric !== 'object') continue
|
||||||
|
const source = rawMetric as Record<string, unknown>
|
||||||
|
const metric: ExportSessionContentMetricCacheEntry = {}
|
||||||
|
if (typeof source.totalMessages === 'number' && Number.isFinite(source.totalMessages) && source.totalMessages >= 0) {
|
||||||
|
metric.totalMessages = Math.floor(source.totalMessages)
|
||||||
|
}
|
||||||
|
if (typeof source.voiceMessages === 'number' && Number.isFinite(source.voiceMessages) && source.voiceMessages >= 0) {
|
||||||
|
metric.voiceMessages = Math.floor(source.voiceMessages)
|
||||||
|
}
|
||||||
|
if (typeof source.imageMessages === 'number' && Number.isFinite(source.imageMessages) && source.imageMessages >= 0) {
|
||||||
|
metric.imageMessages = Math.floor(source.imageMessages)
|
||||||
|
}
|
||||||
|
if (typeof source.videoMessages === 'number' && Number.isFinite(source.videoMessages) && source.videoMessages >= 0) {
|
||||||
|
metric.videoMessages = Math.floor(source.videoMessages)
|
||||||
|
}
|
||||||
|
if (typeof source.emojiMessages === 'number' && Number.isFinite(source.emojiMessages) && source.emojiMessages >= 0) {
|
||||||
|
metric.emojiMessages = Math.floor(source.emojiMessages)
|
||||||
|
}
|
||||||
|
if (Object.keys(metric).length === 0) continue
|
||||||
|
metrics[sessionId] = metric
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0,
|
||||||
|
metrics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setExportSessionContentMetricCache(
|
||||||
|
scopeKey: string,
|
||||||
|
metrics: Record<string, ExportSessionContentMetricCacheEntry>
|
||||||
|
): Promise<void> {
|
||||||
|
if (!scopeKey) return
|
||||||
|
const current = await config.get(CONFIG_KEYS.EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP)
|
||||||
|
const map = current && typeof current === 'object'
|
||||||
|
? { ...(current as Record<string, unknown>) }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const normalized: Record<string, ExportSessionContentMetricCacheEntry> = {}
|
||||||
|
for (const [sessionId, rawMetric] of Object.entries(metrics || {})) {
|
||||||
|
if (!rawMetric || typeof rawMetric !== 'object') continue
|
||||||
|
const metric: ExportSessionContentMetricCacheEntry = {}
|
||||||
|
if (typeof rawMetric.totalMessages === 'number' && Number.isFinite(rawMetric.totalMessages) && rawMetric.totalMessages >= 0) {
|
||||||
|
metric.totalMessages = Math.floor(rawMetric.totalMessages)
|
||||||
|
}
|
||||||
|
if (typeof rawMetric.voiceMessages === 'number' && Number.isFinite(rawMetric.voiceMessages) && rawMetric.voiceMessages >= 0) {
|
||||||
|
metric.voiceMessages = Math.floor(rawMetric.voiceMessages)
|
||||||
|
}
|
||||||
|
if (typeof rawMetric.imageMessages === 'number' && Number.isFinite(rawMetric.imageMessages) && rawMetric.imageMessages >= 0) {
|
||||||
|
metric.imageMessages = Math.floor(rawMetric.imageMessages)
|
||||||
|
}
|
||||||
|
if (typeof rawMetric.videoMessages === 'number' && Number.isFinite(rawMetric.videoMessages) && rawMetric.videoMessages >= 0) {
|
||||||
|
metric.videoMessages = Math.floor(rawMetric.videoMessages)
|
||||||
|
}
|
||||||
|
if (typeof rawMetric.emojiMessages === 'number' && Number.isFinite(rawMetric.emojiMessages) && rawMetric.emojiMessages >= 0) {
|
||||||
|
metric.emojiMessages = Math.floor(rawMetric.emojiMessages)
|
||||||
|
}
|
||||||
|
if (Object.keys(metric).length === 0) continue
|
||||||
|
normalized[sessionId] = metric
|
||||||
|
}
|
||||||
|
|
||||||
|
map[scopeKey] = {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
metrics: normalized
|
||||||
|
}
|
||||||
|
await config.set(CONFIG_KEYS.EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP, map)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getExportSnsStatsCache(scopeKey: string): Promise<ExportSnsStatsCacheItem | null> {
|
export async function getExportSnsStatsCache(scopeKey: string): Promise<ExportSnsStatsCacheItem | null> {
|
||||||
if (!scopeKey) return null
|
if (!scopeKey) return null
|
||||||
const value = await config.get(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP)
|
const value = await config.get(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP)
|
||||||
|
|||||||
Reference in New Issue
Block a user