// 配置服务 - 封装 Electron Store import { config } from './ipc' import type { ExportDefaultDateRangeConfig } from '../utils/exportDateRange' // 配置键名 export const CONFIG_KEYS = { DECRYPT_KEY: 'decryptKey', DB_PATH: 'dbPath', MY_WXID: 'myWxid', WXID_CONFIGS: 'wxidConfigs', THEME: 'theme', THEME_ID: 'themeId', LAST_SESSION: 'lastSession', WINDOW_BOUNDS: 'windowBounds', CACHE_PATH: 'cachePath', EXPORT_PATH: 'exportPath', AGREEMENT_ACCEPTED: 'agreementAccepted', LOG_ENABLED: 'logEnabled', ONBOARDING_DONE: 'onboardingDone', LLM_MODEL_PATH: 'llmModelPath', IMAGE_XOR_KEY: 'imageXorKey', IMAGE_AES_KEY: 'imageAesKey', WHISPER_MODEL_NAME: 'whisperModelName', WHISPER_MODEL_DIR: 'whisperModelDir', WHISPER_DOWNLOAD_SOURCE: 'whisperDownloadSource', AUTO_TRANSCRIBE_VOICE: 'autoTranscribeVoice', TRANSCRIBE_LANGUAGES: 'transcribeLanguages', EXPORT_DEFAULT_FORMAT: 'exportDefaultFormat', EXPORT_DEFAULT_AVATARS: 'exportDefaultAvatars', EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange', EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia', EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText', EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns', EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns', EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency', EXPORT_WRITE_LAYOUT: 'exportWriteLayout', EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled', EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap', EXPORT_LAST_CONTENT_RUN_MAP: 'exportLastContentRunMap', EXPORT_SESSION_RECORD_MAP: 'exportSessionRecordMap', EXPORT_LAST_SNS_POST_COUNT: 'exportLastSnsPostCount', EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap', EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap', EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap', EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP: 'exportSnsUserPostCountsCacheMap', EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP: 'exportSessionMutualFriendsCacheMap', SNS_PAGE_CACHE_MAP: 'snsPageCacheMap', CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs', CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap', CONTACTS_AVATAR_CACHE_MAP: 'contactsAvatarCacheMap', // 安全 AUTH_ENABLED: 'authEnabled', AUTH_PASSWORD: 'authPassword', AUTH_USE_HELLO: 'authUseHello', // 更新 IGNORED_UPDATE_VERSION: 'ignoredUpdateVersion', // 通知 NOTIFICATION_ENABLED: 'notificationEnabled', NOTIFICATION_POSITION: 'notificationPosition', NOTIFICATION_FILTER_MODE: 'notificationFilterMode', NOTIFICATION_FILTER_LIST: 'notificationFilterList', MESSAGE_PUSH_ENABLED: 'messagePushEnabled', WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior', // 词云 WORD_CLOUD_EXCLUDE_WORDS: 'wordCloudExcludeWords', // 数据收集 ANALYTICS_CONSENT: 'analyticsConsent', ANALYTICS_DENY_COUNT: 'analyticsDenyCount' } as const export interface WxidConfig { decryptKey?: string imageXorKey?: number imageAesKey?: string updatedAt?: number } export interface ExportDefaultMediaConfig { images: boolean videos: boolean voices: boolean emojis: boolean } export type WindowCloseBehavior = 'ask' | 'tray' | 'quit' const DEFAULT_EXPORT_MEDIA_CONFIG: ExportDefaultMediaConfig = { images: true, videos: true, voices: true, emojis: true } // 获取解密密钥 export async function getDecryptKey(): Promise { const value = await config.get(CONFIG_KEYS.DECRYPT_KEY) return value as string | null } // 设置解密密钥 export async function setDecryptKey(key: string): Promise { await config.set(CONFIG_KEYS.DECRYPT_KEY, key) } // 获取数据库路径 export async function getDbPath(): Promise { const value = await config.get(CONFIG_KEYS.DB_PATH) return value as string | null } // 设置数据库路径 export async function setDbPath(path: string): Promise { await config.set(CONFIG_KEYS.DB_PATH, path) } // 获取当前用户 wxid export async function getMyWxid(): Promise { const value = await config.get(CONFIG_KEYS.MY_WXID) return value as string | null } // 设置当前用户 wxid export async function setMyWxid(wxid: string): Promise { await config.set(CONFIG_KEYS.MY_WXID, wxid) } export async function getWxidConfigs(): Promise> { const value = await config.get(CONFIG_KEYS.WXID_CONFIGS) if (value && typeof value === 'object') { return value as Record } return {} } export async function getWxidConfig(wxid: string): Promise { if (!wxid) return null const configs = await getWxidConfigs() return configs[wxid] || null } export async function setWxidConfig(wxid: string, configValue: WxidConfig): Promise { if (!wxid) return const configs = await getWxidConfigs() const previous = configs[wxid] || {} configs[wxid] = { ...previous, ...configValue, updatedAt: Date.now() } await config.set(CONFIG_KEYS.WXID_CONFIGS, configs) } // 获取主题 export async function getTheme(): Promise<'light' | 'dark' | 'system'> { const value = await config.get(CONFIG_KEYS.THEME) return (value as 'light' | 'dark' | 'system') || 'light' } // 设置主题 export async function setTheme(theme: 'light' | 'dark' | 'system'): Promise { await config.set(CONFIG_KEYS.THEME, theme) } // 获取主题配色 export async function getThemeId(): Promise { const value = await config.get(CONFIG_KEYS.THEME_ID) return (value as string) || null } // 设置主题配色 export async function setThemeId(themeId: string): Promise { await config.set(CONFIG_KEYS.THEME_ID, themeId) } // 获取上次打开的会话 export async function getLastSession(): Promise { const value = await config.get(CONFIG_KEYS.LAST_SESSION) return value as string | null } // 设置上次打开的会话 export async function setLastSession(sessionId: string): Promise { await config.set(CONFIG_KEYS.LAST_SESSION, sessionId) } // 获取缓存路径 export async function getCachePath(): Promise { const value = await config.get(CONFIG_KEYS.CACHE_PATH) return value as string | null } // 设置缓存路径 export async function setCachePath(path: string): Promise { await config.set(CONFIG_KEYS.CACHE_PATH, path) } // 获取导出路径 export async function getExportPath(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_PATH) return value as string | null } // 设置导出路径 export async function setExportPath(path: string): Promise { await config.set(CONFIG_KEYS.EXPORT_PATH, path) } // 获取协议同意状态 export async function getAgreementAccepted(): Promise { const value = await config.get(CONFIG_KEYS.AGREEMENT_ACCEPTED) return value === true } // 设置协议同意状态 export async function setAgreementAccepted(accepted: boolean): Promise { await config.set(CONFIG_KEYS.AGREEMENT_ACCEPTED, accepted) } // 获取日志开关 export async function getLogEnabled(): Promise { const value = await config.get(CONFIG_KEYS.LOG_ENABLED) return value === true } // 设置日志开关 export async function setLogEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.LOG_ENABLED, enabled) } // 获取 LLM 模型路径 export async function getLlmModelPath(): Promise { const value = await config.get(CONFIG_KEYS.LLM_MODEL_PATH) return (value as string) || null } // 设置 LLM 模型路径 export async function setLlmModelPath(path: string): Promise { await config.set(CONFIG_KEYS.LLM_MODEL_PATH, path) } // 获取 Whisper 模型名称 export async function getWhisperModelName(): Promise { const value = await config.get(CONFIG_KEYS.WHISPER_MODEL_NAME) return (value as string) || null } // 设置 Whisper 模型名称 export async function setWhisperModelName(name: string): Promise { await config.set(CONFIG_KEYS.WHISPER_MODEL_NAME, name) } // 获取 Whisper 模型目录 export async function getWhisperModelDir(): Promise { const value = await config.get(CONFIG_KEYS.WHISPER_MODEL_DIR) return (value as string) || null } // 设置 Whisper 模型目录 export async function setWhisperModelDir(dir: string): Promise { await config.set(CONFIG_KEYS.WHISPER_MODEL_DIR, dir) } // 获取 Whisper 下载源 export async function getWhisperDownloadSource(): Promise { const value = await config.get(CONFIG_KEYS.WHISPER_DOWNLOAD_SOURCE) return (value as string) || null } // 设置 Whisper 下载源 export async function setWhisperDownloadSource(source: string): Promise { await config.set(CONFIG_KEYS.WHISPER_DOWNLOAD_SOURCE, source) } // 清除所有配置 export async function clearConfig(): Promise { await config.clear() } // 获取图片 XOR 密钥 export async function getImageXorKey(): Promise { const value = await config.get(CONFIG_KEYS.IMAGE_XOR_KEY) if (typeof value === 'number' && Number.isFinite(value)) return value return null } // 设置图片 XOR 密钥 export async function setImageXorKey(key: number): Promise { await config.set(CONFIG_KEYS.IMAGE_XOR_KEY, key) } // 获取图片 AES 密钥 export async function getImageAesKey(): Promise { const value = await config.get(CONFIG_KEYS.IMAGE_AES_KEY) return (value as string) || null } // 设置图片 AES 密钥 export async function setImageAesKey(key: string): Promise { await config.set(CONFIG_KEYS.IMAGE_AES_KEY, key) } // 获取是否完成首次配置引导 export async function getOnboardingDone(): Promise { const value = await config.get(CONFIG_KEYS.ONBOARDING_DONE) return value === true } // 设置首次配置引导完成 export async function setOnboardingDone(done: boolean): Promise { await config.set(CONFIG_KEYS.ONBOARDING_DONE, done) } // 获取自动语音转文字开关 export async function getAutoTranscribeVoice(): Promise { const value = await config.get(CONFIG_KEYS.AUTO_TRANSCRIBE_VOICE) return value === true } // 设置自动语音转文字开关 export async function setAutoTranscribeVoice(enabled: boolean): Promise { await config.set(CONFIG_KEYS.AUTO_TRANSCRIBE_VOICE, enabled) } // 获取语音转文字支持的语言列表 export async function getTranscribeLanguages(): Promise { const value = await config.get(CONFIG_KEYS.TRANSCRIBE_LANGUAGES) // 默认只支持中文 return (value as string[]) || ['zh'] } // 设置语音转文字支持的语言列表 export async function setTranscribeLanguages(languages: string[]): Promise { await config.set(CONFIG_KEYS.TRANSCRIBE_LANGUAGES, languages) } // 获取导出默认格式 export async function getExportDefaultFormat(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT) return (value as string) || null } // 设置导出默认格式 export async function setExportDefaultFormat(format: string): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT, format) } // 获取导出默认头像设置 export async function getExportDefaultAvatars(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_AVATARS) if (typeof value === 'boolean') return value return null } // 设置导出默认头像设置 export async function setExportDefaultAvatars(enabled: boolean): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_AVATARS, enabled) } // 获取导出默认时间范围 export async function getExportDefaultDateRange(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE) if (typeof value === 'string') return value if (value && typeof value === 'object') { return value as ExportDefaultDateRangeConfig } return null } // 设置导出默认时间范围 export async function setExportDefaultDateRange(range: ExportDefaultDateRangeConfig | string): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE, range) } // 获取导出默认媒体设置 export async function getExportDefaultMedia(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA) if (typeof value === 'boolean') { return { images: value, videos: value, voices: value, emojis: value } } if (value && typeof value === 'object') { const raw = value as Partial> return { images: typeof raw.images === 'boolean' ? raw.images : DEFAULT_EXPORT_MEDIA_CONFIG.images, videos: typeof raw.videos === 'boolean' ? raw.videos : DEFAULT_EXPORT_MEDIA_CONFIG.videos, voices: typeof raw.voices === 'boolean' ? raw.voices : DEFAULT_EXPORT_MEDIA_CONFIG.voices, emojis: typeof raw.emojis === 'boolean' ? raw.emojis : DEFAULT_EXPORT_MEDIA_CONFIG.emojis } } return null } // 设置导出默认媒体设置 export async function setExportDefaultMedia(media: ExportDefaultMediaConfig): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA, { images: media.images, videos: media.videos, voices: media.voices, emojis: media.emojis }) } // 获取导出默认语音转文字 export async function getExportDefaultVoiceAsText(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT) if (typeof value === 'boolean') return value return null } // 设置导出默认语音转文字 export async function setExportDefaultVoiceAsText(enabled: boolean): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT, enabled) } // 获取导出默认 Excel 列模式 export async function getExportDefaultExcelCompactColumns(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS) if (typeof value === 'boolean') return value return null } // 设置导出默认 Excel 列模式 export async function setExportDefaultExcelCompactColumns(enabled: boolean): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS, enabled) } // 获取导出默认 TXT 列配置 export async function getExportDefaultTxtColumns(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS) return Array.isArray(value) ? (value as string[]) : null } // 设置导出默认 TXT 列配置 export async function setExportDefaultTxtColumns(columns: string[]): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS, columns) } // 获取导出默认并发数 export async function getExportDefaultConcurrency(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY) if (typeof value === 'number' && Number.isFinite(value)) return value return null } // 设置导出默认并发数 export async function setExportDefaultConcurrency(concurrency: number): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency) } export type ExportWriteLayout = 'A' | 'B' | 'C' export async function getExportWriteLayout(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_WRITE_LAYOUT) if (value === 'A' || value === 'B' || value === 'C') return value return 'B' } export async function setExportWriteLayout(layout: ExportWriteLayout): Promise { await config.set(CONFIG_KEYS.EXPORT_WRITE_LAYOUT, layout) } export async function getExportSessionNamePrefixEnabled(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_NAME_PREFIX_ENABLED) if (typeof value === 'boolean') return value return true } export async function setExportSessionNamePrefixEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.EXPORT_SESSION_NAME_PREFIX_ENABLED, enabled) } export async function getExportLastSessionRunMap(): Promise> { const value = await config.get(CONFIG_KEYS.EXPORT_LAST_SESSION_RUN_MAP) if (!value || typeof value !== 'object') return {} const entries = Object.entries(value as Record) const map: Record = {} for (const [sessionId, raw] of entries) { if (typeof raw === 'number' && Number.isFinite(raw)) { map[sessionId] = raw } } return map } export async function setExportLastSessionRunMap(map: Record): Promise { await config.set(CONFIG_KEYS.EXPORT_LAST_SESSION_RUN_MAP, map) } export async function getExportLastContentRunMap(): Promise> { const value = await config.get(CONFIG_KEYS.EXPORT_LAST_CONTENT_RUN_MAP) if (!value || typeof value !== 'object') return {} const entries = Object.entries(value as Record) const map: Record = {} for (const [key, raw] of entries) { if (typeof raw === 'number' && Number.isFinite(raw)) { map[key] = raw } } return map } export async function setExportLastContentRunMap(map: Record): Promise { await config.set(CONFIG_KEYS.EXPORT_LAST_CONTENT_RUN_MAP, map) } export interface ExportSessionRecordEntry { exportTime: number content: string outputDir: string } export async function getExportSessionRecordMap(): Promise> { const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_RECORD_MAP) if (!value || typeof value !== 'object') return {} const map: Record = {} const entries = Object.entries(value as Record) for (const [sessionId, rawList] of entries) { if (!Array.isArray(rawList)) continue const normalizedList: ExportSessionRecordEntry[] = [] for (const rawItem of rawList) { if (!rawItem || typeof rawItem !== 'object') continue const exportTime = Number((rawItem as Record).exportTime) const content = String((rawItem as Record).content || '').trim() const outputDir = String((rawItem as Record).outputDir || '').trim() if (!Number.isFinite(exportTime) || exportTime <= 0) continue if (!content || !outputDir) continue normalizedList.push({ exportTime: Math.floor(exportTime), content, outputDir }) } if (normalizedList.length > 0) { map[sessionId] = normalizedList } } return map } export async function setExportSessionRecordMap(map: Record): Promise { await config.set(CONFIG_KEYS.EXPORT_SESSION_RECORD_MAP, map) } export async function getExportLastSnsPostCount(): Promise { const value = await config.get(CONFIG_KEYS.EXPORT_LAST_SNS_POST_COUNT) if (typeof value === 'number' && Number.isFinite(value) && value >= 0) { return Math.floor(value) } return 0 } export async function setExportLastSnsPostCount(count: number): Promise { const normalized = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0 await config.set(CONFIG_KEYS.EXPORT_LAST_SNS_POST_COUNT, normalized) } export interface ExportSessionMessageCountCacheItem { updatedAt: number counts: Record } export interface ExportSessionContentMetricCacheEntry { totalMessages?: number voiceMessages?: number imageMessages?: number videoMessages?: number emojiMessages?: number } export interface ExportSessionContentMetricCacheItem { updatedAt: number metrics: Record } export interface ExportSnsStatsCacheItem { updatedAt: number totalPosts: number totalFriends: number } export interface ExportSnsUserPostCountsCacheItem { updatedAt: number counts: Record } export type ExportSessionMutualFriendDirection = 'incoming' | 'outgoing' | 'bidirectional' export type ExportSessionMutualFriendBehavior = 'likes' | 'comments' | 'both' export interface ExportSessionMutualFriendCacheItem { name: string incomingLikeCount: number incomingCommentCount: number outgoingLikeCount: number outgoingCommentCount: number totalCount: number latestTime: number direction: ExportSessionMutualFriendDirection behavior: ExportSessionMutualFriendBehavior } export interface ExportSessionMutualFriendsCacheEntry { count: number items: ExportSessionMutualFriendCacheItem[] loadedPosts: number totalPosts: number | null computedAt: number } export interface ExportSessionMutualFriendsCacheItem { updatedAt: number metrics: Record } export interface SnsPageOverviewCache { totalPosts: number totalFriends: number myPosts: number | null earliestTime: number | null latestTime: number | null } export interface SnsPageCacheItem { updatedAt: number overviewStats: SnsPageOverviewCache posts: unknown[] } export interface ContactsListCacheContact { username: string displayName: string remark?: string nickname?: string type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } export interface ContactsListCacheItem { updatedAt: number contacts: ContactsListCacheContact[] } export interface ContactsAvatarCacheEntry { avatarUrl: string updatedAt: number checkedAt: number } export interface ContactsAvatarCacheItem { updatedAt: number avatars: Record } export async function getExportSessionMessageCountCache(scopeKey: string): Promise { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP) if (!value || typeof value !== 'object') return null const rawMap = value as Record const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const rawUpdatedAt = (rawItem as Record).updatedAt const rawCounts = (rawItem as Record).counts if (!rawCounts || typeof rawCounts !== 'object') return null const counts: Record = {} for (const [sessionId, countRaw] of Object.entries(rawCounts as Record)) { if (typeof countRaw === 'number' && Number.isFinite(countRaw) && countRaw >= 0) { counts[sessionId] = Math.floor(countRaw) } } return { updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0, counts } } export async function setExportSessionMessageCountCache(scopeKey: string, counts: Record): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} const normalized: Record = {} for (const [sessionId, countRaw] of Object.entries(counts || {})) { if (typeof countRaw === 'number' && Number.isFinite(countRaw) && countRaw >= 0) { normalized[sessionId] = Math.floor(countRaw) } } map[scopeKey] = { updatedAt: Date.now(), counts: normalized } await config.set(CONFIG_KEYS.EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP, map) } export async function getExportSessionContentMetricCache(scopeKey: string): Promise { 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 const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const rawUpdatedAt = (rawItem as Record).updatedAt const rawMetrics = (rawItem as Record).metrics if (!rawMetrics || typeof rawMetrics !== 'object') return null const metrics: Record = {} for (const [sessionId, rawMetric] of Object.entries(rawMetrics as Record)) { if (!rawMetric || typeof rawMetric !== 'object') continue const source = rawMetric as Record 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 ): Promise { 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) } : {} const normalized: Record = {} 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 { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP) if (!value || typeof value !== 'object') return null const rawMap = value as Record const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const raw = rawItem as Record const totalPosts = typeof raw.totalPosts === 'number' && Number.isFinite(raw.totalPosts) && raw.totalPosts >= 0 ? Math.floor(raw.totalPosts) : 0 const totalFriends = typeof raw.totalFriends === 'number' && Number.isFinite(raw.totalFriends) && raw.totalFriends >= 0 ? Math.floor(raw.totalFriends) : 0 const updatedAt = typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0 return { updatedAt, totalPosts, totalFriends } } export async function setExportSnsStatsCache( scopeKey: string, stats: { totalPosts: number; totalFriends: number } ): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} map[scopeKey] = { updatedAt: Date.now(), totalPosts: Number.isFinite(stats.totalPosts) ? Math.max(0, Math.floor(stats.totalPosts)) : 0, totalFriends: Number.isFinite(stats.totalFriends) ? Math.max(0, Math.floor(stats.totalFriends)) : 0 } await config.set(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP, map) } export async function getExportSnsUserPostCountsCache(scopeKey: string): Promise { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP) if (!value || typeof value !== 'object') return null const rawMap = value as Record const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const raw = rawItem as Record const rawCounts = raw.counts if (!rawCounts || typeof rawCounts !== 'object') return null const counts: Record = {} for (const [rawUsername, rawCount] of Object.entries(rawCounts as Record)) { const username = String(rawUsername || '').trim() if (!username) continue const valueNum = Number(rawCount) counts[username] = Number.isFinite(valueNum) ? Math.max(0, Math.floor(valueNum)) : 0 } const updatedAt = typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0 return { updatedAt, counts } } export async function setExportSnsUserPostCountsCache( scopeKey: string, counts: Record ): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} const normalized: Record = {} for (const [rawUsername, rawCount] of Object.entries(counts || {})) { const username = String(rawUsername || '').trim() if (!username) continue const valueNum = Number(rawCount) normalized[username] = Number.isFinite(valueNum) ? Math.max(0, Math.floor(valueNum)) : 0 } map[scopeKey] = { updatedAt: Date.now(), counts: normalized } await config.set(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP, map) } const normalizeMutualFriendDirection = (value: unknown): ExportSessionMutualFriendDirection | null => { if (value === 'incoming' || value === 'outgoing' || value === 'bidirectional') { return value } return null } const normalizeMutualFriendBehavior = (value: unknown): ExportSessionMutualFriendBehavior | null => { if (value === 'likes' || value === 'comments' || value === 'both') { return value } return null } const normalizeExportSessionMutualFriendsCacheEntry = (raw: unknown): ExportSessionMutualFriendsCacheEntry | null => { if (!raw || typeof raw !== 'object') return null const source = raw as Record const count = Number(source.count) const loadedPosts = Number(source.loadedPosts) const computedAt = Number(source.computedAt) const itemsRaw = Array.isArray(source.items) ? source.items : [] const totalPostsRaw = source.totalPosts const totalPosts = totalPostsRaw === null || totalPostsRaw === undefined ? null : Number(totalPostsRaw) if (!Number.isFinite(count) || count < 0 || !Number.isFinite(loadedPosts) || loadedPosts < 0 || !Number.isFinite(computedAt) || computedAt < 0) { return null } const items: ExportSessionMutualFriendCacheItem[] = [] for (const itemRaw of itemsRaw) { if (!itemRaw || typeof itemRaw !== 'object') continue const item = itemRaw as Record const name = String(item.name || '').trim() const direction = normalizeMutualFriendDirection(item.direction) const behavior = normalizeMutualFriendBehavior(item.behavior) const incomingLikeCount = Number(item.incomingLikeCount) const incomingCommentCount = Number(item.incomingCommentCount) const outgoingLikeCount = Number(item.outgoingLikeCount) const outgoingCommentCount = Number(item.outgoingCommentCount) const totalCount = Number(item.totalCount) const latestTime = Number(item.latestTime) if (!name || !direction || !behavior) continue if ( !Number.isFinite(incomingLikeCount) || incomingLikeCount < 0 || !Number.isFinite(incomingCommentCount) || incomingCommentCount < 0 || !Number.isFinite(outgoingLikeCount) || outgoingLikeCount < 0 || !Number.isFinite(outgoingCommentCount) || outgoingCommentCount < 0 || !Number.isFinite(totalCount) || totalCount < 0 || !Number.isFinite(latestTime) || latestTime < 0 ) { continue } items.push({ name, incomingLikeCount: Math.floor(incomingLikeCount), incomingCommentCount: Math.floor(incomingCommentCount), outgoingLikeCount: Math.floor(outgoingLikeCount), outgoingCommentCount: Math.floor(outgoingCommentCount), totalCount: Math.floor(totalCount), latestTime: Math.floor(latestTime), direction, behavior }) } return { count: Math.floor(count), items, loadedPosts: Math.floor(loadedPosts), totalPosts: totalPosts === null ? null : (Number.isFinite(totalPosts) && totalPosts >= 0 ? Math.floor(totalPosts) : null), computedAt: Math.floor(computedAt) } } export async function getExportSessionMutualFriendsCache(scopeKey: string): Promise { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP) if (!value || typeof value !== 'object') return null const rawMap = value as Record const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const rawUpdatedAt = (rawItem as Record).updatedAt const rawMetrics = (rawItem as Record).metrics if (!rawMetrics || typeof rawMetrics !== 'object') return null const metrics: Record = {} for (const [sessionIdRaw, metricRaw] of Object.entries(rawMetrics as Record)) { const sessionId = String(sessionIdRaw || '').trim() if (!sessionId) continue const metric = normalizeExportSessionMutualFriendsCacheEntry(metricRaw) if (!metric) continue metrics[sessionId] = metric } return { updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0, metrics } } export async function setExportSessionMutualFriendsCache( scopeKey: string, metrics: Record ): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} const normalized: Record = {} for (const [sessionIdRaw, metricRaw] of Object.entries(metrics || {})) { const sessionId = String(sessionIdRaw || '').trim() if (!sessionId) continue const metric = normalizeExportSessionMutualFriendsCacheEntry(metricRaw) if (!metric) continue normalized[sessionId] = metric } map[scopeKey] = { updatedAt: Date.now(), metrics: normalized } await config.set(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP, map) } export async function clearExportSessionMutualFriendsCache(scopeKey: string): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP) if (!current || typeof current !== 'object') return const map = { ...(current as Record) } if (!(scopeKey in map)) return delete map[scopeKey] await config.set(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP, map) } export async function getSnsPageCache(scopeKey: string): Promise { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP) if (!value || typeof value !== 'object') return null const rawMap = value as Record const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const raw = rawItem as Record const rawOverview = raw.overviewStats const rawPosts = raw.posts if (!rawOverview || typeof rawOverview !== 'object' || !Array.isArray(rawPosts)) return null const overviewObj = rawOverview as Record const normalizeNumber = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : 0) const normalizeNullableTimestamp = (v: unknown) => { if (v === null || v === undefined) return null if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v) return null } const normalizeNullableCount = (v: unknown) => { if (v === null || v === undefined) return null if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return Math.floor(v) return null } return { updatedAt: typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0, overviewStats: { totalPosts: Math.max(0, normalizeNumber(overviewObj.totalPosts)), totalFriends: Math.max(0, normalizeNumber(overviewObj.totalFriends)), myPosts: normalizeNullableCount(overviewObj.myPosts), earliestTime: normalizeNullableTimestamp(overviewObj.earliestTime), latestTime: normalizeNullableTimestamp(overviewObj.latestTime) }, posts: rawPosts } } export async function setSnsPageCache( scopeKey: string, payload: { overviewStats: SnsPageOverviewCache; posts: unknown[] } ): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} const normalizeNumber = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.floor(v)) : 0) const normalizeNullableTimestamp = (v: unknown) => { if (v === null || v === undefined) return null if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v) return null } const normalizeNullableCount = (v: unknown) => { if (v === null || v === undefined) return null if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return Math.floor(v) return null } map[scopeKey] = { updatedAt: Date.now(), overviewStats: { totalPosts: normalizeNumber(payload?.overviewStats?.totalPosts), totalFriends: normalizeNumber(payload?.overviewStats?.totalFriends), myPosts: normalizeNullableCount(payload?.overviewStats?.myPosts), earliestTime: normalizeNullableTimestamp(payload?.overviewStats?.earliestTime), latestTime: normalizeNullableTimestamp(payload?.overviewStats?.latestTime) }, posts: Array.isArray(payload?.posts) ? payload.posts : [] } await config.set(CONFIG_KEYS.SNS_PAGE_CACHE_MAP, map) } // 获取通讯录加载超时阈值(毫秒) export async function getContactsLoadTimeoutMs(): Promise { const value = await config.get(CONFIG_KEYS.CONTACTS_LOAD_TIMEOUT_MS) if (typeof value === 'number' && Number.isFinite(value) && value >= 1000 && value <= 60000) { return Math.floor(value) } return 3000 } // 设置通讯录加载超时阈值(毫秒) export async function setContactsLoadTimeoutMs(timeoutMs: number): Promise { const normalized = Number.isFinite(timeoutMs) ? Math.min(60000, Math.max(1000, Math.floor(timeoutMs))) : 3000 await config.set(CONFIG_KEYS.CONTACTS_LOAD_TIMEOUT_MS, normalized) } export async function getContactsListCache(scopeKey: string): Promise { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.CONTACTS_LIST_CACHE_MAP) if (!value || typeof value !== 'object') return null const rawMap = value as Record const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const rawUpdatedAt = (rawItem as Record).updatedAt const rawContacts = (rawItem as Record).contacts if (!Array.isArray(rawContacts)) return null const contacts: ContactsListCacheContact[] = [] for (const raw of rawContacts) { if (!raw || typeof raw !== 'object') continue const item = raw as Record const username = typeof item.username === 'string' ? item.username.trim() : '' if (!username) continue const displayName = typeof item.displayName === 'string' ? item.displayName : username const type = typeof item.type === 'string' ? item.type : 'other' contacts.push({ username, displayName, remark: typeof item.remark === 'string' ? item.remark : undefined, nickname: typeof item.nickname === 'string' ? item.nickname : undefined, type: (type === 'friend' || type === 'group' || type === 'official' || type === 'former_friend' || type === 'other') ? type : 'other' }) } return { updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0, contacts } } export async function setContactsListCache(scopeKey: string, contacts: ContactsListCacheContact[]): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.CONTACTS_LIST_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} const normalized: ContactsListCacheContact[] = [] for (const contact of contacts || []) { const username = String(contact?.username || '').trim() if (!username) continue const displayName = String(contact?.displayName || username) const type = contact?.type || 'other' if (type !== 'friend' && type !== 'group' && type !== 'official' && type !== 'former_friend' && type !== 'other') { continue } normalized.push({ username, displayName, remark: contact?.remark ? String(contact.remark) : undefined, nickname: contact?.nickname ? String(contact.nickname) : undefined, type }) } map[scopeKey] = { updatedAt: Date.now(), contacts: normalized } await config.set(CONFIG_KEYS.CONTACTS_LIST_CACHE_MAP, map) } export async function getContactsAvatarCache(scopeKey: string): Promise { 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 const rawItem = rawMap[scopeKey] if (!rawItem || typeof rawItem !== 'object') return null const rawUpdatedAt = (rawItem as Record).updatedAt const rawAvatars = (rawItem as Record).avatars if (!rawAvatars || typeof rawAvatars !== 'object') return null const avatars: Record = {} for (const [rawUsername, rawEntry] of Object.entries(rawAvatars as Record)) { 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 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 ): Promise { if (!scopeKey) return const current = await config.get(CONFIG_KEYS.CONTACTS_AVATAR_CACHE_MAP) const map = current && typeof current === 'object' ? { ...(current as Record) } : {} const normalized: Record = {} 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 { const value = await config.get(CONFIG_KEYS.AUTH_ENABLED) return value === true } export async function setAuthEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.AUTH_ENABLED, enabled) } export async function getAuthPassword(): Promise { const value = await config.get(CONFIG_KEYS.AUTH_PASSWORD) return (value as string) || '' } export async function setAuthPassword(passwordHash: string): Promise { await config.set(CONFIG_KEYS.AUTH_PASSWORD, passwordHash) } export async function getAuthUseHello(): Promise { const value = await config.get(CONFIG_KEYS.AUTH_USE_HELLO) return value === true } export async function setAuthUseHello(useHello: boolean): Promise { await config.set(CONFIG_KEYS.AUTH_USE_HELLO, useHello) } // === 更新相关 === // 获取被忽略的更新版本 export async function getIgnoredUpdateVersion(): Promise { const value = await config.get(CONFIG_KEYS.IGNORED_UPDATE_VERSION) return (value as string) || null } // 设置被忽略的更新版本 export async function setIgnoredUpdateVersion(version: string): Promise { await config.set(CONFIG_KEYS.IGNORED_UPDATE_VERSION, version) } // 获取通知开关 export async function getNotificationEnabled(): Promise { const value = await config.get(CONFIG_KEYS.NOTIFICATION_ENABLED) return value !== false // 默认为 true } // 设置通知开关 export async function setNotificationEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.NOTIFICATION_ENABLED, enabled) } // 获取通知位置 export async function getNotificationPosition(): Promise<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'> { const value = await config.get(CONFIG_KEYS.NOTIFICATION_POSITION) return (value as any) || 'top-right' } // 设置通知位置 export async function setNotificationPosition(position: string): Promise { await config.set(CONFIG_KEYS.NOTIFICATION_POSITION, position) } // 获取通知过滤模式 export async function getNotificationFilterMode(): Promise<'all' | 'whitelist' | 'blacklist'> { const value = await config.get(CONFIG_KEYS.NOTIFICATION_FILTER_MODE) return (value as any) || 'all' } // 设置通知过滤模式 export async function setNotificationFilterMode(mode: 'all' | 'whitelist' | 'blacklist'): Promise { await config.set(CONFIG_KEYS.NOTIFICATION_FILTER_MODE, mode) } // 获取通知过滤列表 export async function getNotificationFilterList(): Promise { const value = await config.get(CONFIG_KEYS.NOTIFICATION_FILTER_LIST) return Array.isArray(value) ? value : [] } // 设置通知过滤列表 export async function setNotificationFilterList(list: string[]): Promise { await config.set(CONFIG_KEYS.NOTIFICATION_FILTER_LIST, list) } export async function getMessagePushEnabled(): Promise { const value = await config.get(CONFIG_KEYS.MESSAGE_PUSH_ENABLED) return value === true } export async function setMessagePushEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.MESSAGE_PUSH_ENABLED, enabled) } export async function getWindowCloseBehavior(): Promise { const value = await config.get(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR) if (value === 'tray' || value === 'quit') return value return 'ask' } export async function setWindowCloseBehavior(behavior: WindowCloseBehavior): Promise { await config.set(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR, behavior) } // 获取词云排除词列表 export async function getWordCloudExcludeWords(): Promise { const value = await config.get(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS) return Array.isArray(value) ? value : [] } // 设置词云排除词列表 export async function setWordCloudExcludeWords(words: string[]): Promise { await config.set(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS, words) } // 获取数据收集同意状态 export async function getAnalyticsConsent(): Promise { const value = await config.get(CONFIG_KEYS.ANALYTICS_CONSENT) if (typeof value === 'boolean') return value return null } // 设置数据收集同意状态 export async function setAnalyticsConsent(consent: boolean): Promise { await config.set(CONFIG_KEYS.ANALYTICS_CONSENT, consent) } // 获取数据收集拒绝次数 export async function getAnalyticsDenyCount(): Promise { const value = await config.get(CONFIG_KEYS.ANALYTICS_DENY_COUNT) return typeof value === 'number' ? value : 0 } // 设置数据收集拒绝次数 export async function setAnalyticsDenyCount(count: number): Promise { await config.set(CONFIG_KEYS.ANALYTICS_DENY_COUNT, count) }