Files
WeFlow/src/services/config.ts
hicccc77 83f50cbaee fix: support configurable bind host for HTTP API and fix Windows sherpa-onnx PATH
- fix(#547): HTTP API server now supports configurable bind host (default 127.0.0.1)
  Docker/N8N users can set host to 0.0.0.0 in settings to allow container access.
  Adds httpApiHost config key, UI input in settings, and passes host through
  IPC chain (preload -> main -> httpService).

- fix(#546): Add Windows PATH injection for sherpa-onnx native module
  buildTranscribeWorkerEnv() now adds the sherpa-onnx-win-x64 directory to
  PATH on Windows, fixing 'Could not find sherpa-onnx-node' errors caused
  by missing DLL search path in forked worker processes.
2026-03-25 15:10:16 +08:00

1522 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 配置服务 - 封装 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_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS: 'exportDefaultImageDeepSearchOnMiss',
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',
HTTP_API_TOKEN: 'httpApiToken',
HTTP_API_ENABLED: 'httpApiEnabled',
HTTP_API_PORT: 'httpApiPort',
HTTP_API_HOST: 'httpApiHost',
MESSAGE_PUSH_ENABLED: 'messagePushEnabled',
WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior',
QUOTE_LAYOUT: 'quoteLayout',
// 词云
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'
export type QuoteLayout = 'quote-top' | 'quote-bottom'
const DEFAULT_EXPORT_MEDIA_CONFIG: ExportDefaultMediaConfig = {
images: true,
videos: true,
voices: true,
emojis: true
}
// 获取解密密钥
export async function getDecryptKey(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.DECRYPT_KEY)
return value as string | null
}
// 设置解密密钥
export async function setDecryptKey(key: string): Promise<void> {
await config.set(CONFIG_KEYS.DECRYPT_KEY, key)
}
// 获取数据库路径
export async function getDbPath(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.DB_PATH)
return value as string | null
}
// 获取api access_token
export async function getHttpApiToken(): Promise<string> {
const value = await config.get(CONFIG_KEYS.HTTP_API_TOKEN)
return (value as string) || ''
}
// 设置access_token
export async function setHttpApiToken(token: string): Promise<void> {
await config.set(CONFIG_KEYS.HTTP_API_TOKEN, token)
}
// 设置数据库路径
export async function setDbPath(path: string): Promise<void> {
await config.set(CONFIG_KEYS.DB_PATH, path)
}
// 获取当前用户 wxid
export async function getMyWxid(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.MY_WXID)
return value as string | null
}
// 设置当前用户 wxid
export async function setMyWxid(wxid: string): Promise<void> {
await config.set(CONFIG_KEYS.MY_WXID, wxid)
}
export async function getWxidConfigs(): Promise<Record<string, WxidConfig>> {
const value = await config.get(CONFIG_KEYS.WXID_CONFIGS)
if (value && typeof value === 'object') {
return value as Record<string, WxidConfig>
}
return {}
}
export async function getWxidConfig(wxid: string): Promise<WxidConfig | null> {
if (!wxid) return null
const configs = await getWxidConfigs()
return configs[wxid] || null
}
export async function setWxidConfig(wxid: string, configValue: WxidConfig): Promise<void> {
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<void> {
await config.set(CONFIG_KEYS.THEME, theme)
}
// 获取主题配色
export async function getThemeId(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.THEME_ID)
return (value as string) || null
}
// 设置主题配色
export async function setThemeId(themeId: string): Promise<void> {
await config.set(CONFIG_KEYS.THEME_ID, themeId)
}
// 获取上次打开的会话
export async function getLastSession(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.LAST_SESSION)
return value as string | null
}
// 设置上次打开的会话
export async function setLastSession(sessionId: string): Promise<void> {
await config.set(CONFIG_KEYS.LAST_SESSION, sessionId)
}
// 获取缓存路径
export async function getCachePath(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.CACHE_PATH)
return value as string | null
}
// 设置缓存路径
export async function setCachePath(path: string): Promise<void> {
await config.set(CONFIG_KEYS.CACHE_PATH, path)
}
// 获取导出路径
export async function getExportPath(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_PATH)
return value as string | null
}
// 设置导出路径
export async function setExportPath(path: string): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_PATH, path)
}
// 获取协议同意状态
export async function getAgreementAccepted(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AGREEMENT_ACCEPTED)
return value === true
}
// 设置协议同意状态
export async function setAgreementAccepted(accepted: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AGREEMENT_ACCEPTED, accepted)
}
// 获取日志开关
export async function getLogEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.LOG_ENABLED)
return value === true
}
// 设置日志开关
export async function setLogEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.LOG_ENABLED, enabled)
}
// 获取 LLM 模型路径
export async function getLlmModelPath(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.LLM_MODEL_PATH)
return (value as string) || null
}
// 设置 LLM 模型路径
export async function setLlmModelPath(path: string): Promise<void> {
await config.set(CONFIG_KEYS.LLM_MODEL_PATH, path)
}
// 获取 Whisper 模型名称
export async function getWhisperModelName(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.WHISPER_MODEL_NAME)
return (value as string) || null
}
// 设置 Whisper 模型名称
export async function setWhisperModelName(name: string): Promise<void> {
await config.set(CONFIG_KEYS.WHISPER_MODEL_NAME, name)
}
// 获取 Whisper 模型目录
export async function getWhisperModelDir(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.WHISPER_MODEL_DIR)
return (value as string) || null
}
// 设置 Whisper 模型目录
export async function setWhisperModelDir(dir: string): Promise<void> {
await config.set(CONFIG_KEYS.WHISPER_MODEL_DIR, dir)
}
// 获取 Whisper 下载源
export async function getWhisperDownloadSource(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.WHISPER_DOWNLOAD_SOURCE)
return (value as string) || null
}
// 设置 Whisper 下载源
export async function setWhisperDownloadSource(source: string): Promise<void> {
await config.set(CONFIG_KEYS.WHISPER_DOWNLOAD_SOURCE, source)
}
// 清除所有配置
export async function clearConfig(): Promise<void> {
await config.clear()
}
// 获取图片 XOR 密钥
export async function getImageXorKey(): Promise<number | null> {
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<void> {
await config.set(CONFIG_KEYS.IMAGE_XOR_KEY, key)
}
// 获取图片 AES 密钥
export async function getImageAesKey(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.IMAGE_AES_KEY)
return (value as string) || null
}
// 设置图片 AES 密钥
export async function setImageAesKey(key: string): Promise<void> {
await config.set(CONFIG_KEYS.IMAGE_AES_KEY, key)
}
// 获取是否完成首次配置引导
export async function getOnboardingDone(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.ONBOARDING_DONE)
return value === true
}
// 设置首次配置引导完成
export async function setOnboardingDone(done: boolean): Promise<void> {
await config.set(CONFIG_KEYS.ONBOARDING_DONE, done)
}
// 获取自动语音转文字开关
export async function getAutoTranscribeVoice(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AUTO_TRANSCRIBE_VOICE)
return value === true
}
// 设置自动语音转文字开关
export async function setAutoTranscribeVoice(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AUTO_TRANSCRIBE_VOICE, enabled)
}
// 获取语音转文字支持的语言列表
export async function getTranscribeLanguages(): Promise<string[]> {
const value = await config.get(CONFIG_KEYS.TRANSCRIBE_LANGUAGES)
// 默认只支持中文
return (value as string[]) || ['zh']
}
// 设置语音转文字支持的语言列表
export async function setTranscribeLanguages(languages: string[]): Promise<void> {
await config.set(CONFIG_KEYS.TRANSCRIBE_LANGUAGES, languages)
}
// 获取导出默认格式
export async function getExportDefaultFormat(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT)
return (value as string) || null
}
// 设置导出默认格式
export async function setExportDefaultFormat(format: string): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT, format)
}
// 获取导出默认头像设置
export async function getExportDefaultAvatars(): Promise<boolean | null> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_AVATARS, enabled)
}
// 获取导出默认时间范围
export async function getExportDefaultDateRange(): Promise<ExportDefaultDateRangeConfig | string | null> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE, range)
}
// 获取导出默认媒体设置
export async function getExportDefaultMedia(): Promise<ExportDefaultMediaConfig | null> {
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<Record<keyof ExportDefaultMediaConfig, unknown>>
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<void> {
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<boolean | null> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT, enabled)
}
// 获取导出默认 Excel 列模式
export async function getExportDefaultExcelCompactColumns(): Promise<boolean | null> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS, enabled)
}
// 获取导出默认 TXT 列配置
export async function getExportDefaultTxtColumns(): Promise<string[] | null> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS, columns)
}
// 获取导出默认并发数
export async function getExportDefaultConcurrency(): Promise<number | null> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
}
// 获取缺图时是否深度搜索(默认导出行为)
export async function getExportDefaultImageDeepSearchOnMiss(): Promise<boolean | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS)
if (typeof value === 'boolean') return value
return null
}
// 设置缺图时是否深度搜索(默认导出行为)
export async function setExportDefaultImageDeepSearchOnMiss(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_IMAGE_DEEP_SEARCH_ON_MISS, enabled)
}
export type ExportWriteLayout = 'A' | 'B' | 'C'
export async function getExportWriteLayout(): Promise<ExportWriteLayout> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_WRITE_LAYOUT, layout)
}
export async function getExportSessionNamePrefixEnabled(): Promise<boolean> {
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<void> {
await config.set(CONFIG_KEYS.EXPORT_SESSION_NAME_PREFIX_ENABLED, enabled)
}
export async function getExportLastSessionRunMap(): Promise<Record<string, number>> {
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<string, unknown>)
const map: Record<string, number> = {}
for (const [sessionId, raw] of entries) {
if (typeof raw === 'number' && Number.isFinite(raw)) {
map[sessionId] = raw
}
}
return map
}
export async function setExportLastSessionRunMap(map: Record<string, number>): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_LAST_SESSION_RUN_MAP, map)
}
export async function getExportLastContentRunMap(): Promise<Record<string, number>> {
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<string, unknown>)
const map: Record<string, number> = {}
for (const [key, raw] of entries) {
if (typeof raw === 'number' && Number.isFinite(raw)) {
map[key] = raw
}
}
return map
}
export async function setExportLastContentRunMap(map: Record<string, number>): Promise<void> {
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<Record<string, ExportSessionRecordEntry[]>> {
const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_RECORD_MAP)
if (!value || typeof value !== 'object') return {}
const map: Record<string, ExportSessionRecordEntry[]> = {}
const entries = Object.entries(value as Record<string, unknown>)
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<string, unknown>).exportTime)
const content = String((rawItem as Record<string, unknown>).content || '').trim()
const outputDir = String((rawItem as Record<string, unknown>).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<string, ExportSessionRecordEntry[]>): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_SESSION_RECORD_MAP, map)
}
export async function getExportLastSnsPostCount(): Promise<number> {
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<void> {
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<string, number>
}
export interface ExportSessionContentMetricCacheEntry {
totalMessages?: number
voiceMessages?: number
imageMessages?: number
videoMessages?: number
emojiMessages?: number
firstTimestamp?: number
lastTimestamp?: number
}
export interface ExportSessionContentMetricCacheItem {
updatedAt: number
metrics: Record<string, ExportSessionContentMetricCacheEntry>
}
export interface ExportSnsStatsCacheItem {
updatedAt: number
totalPosts: number
totalFriends: number
}
export interface ExportSnsUserPostCountsCacheItem {
updatedAt: number
counts: Record<string, number>
}
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<string, ExportSessionMutualFriendsCacheEntry>
}
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
alias?: string
labels?: string[]
detailDescription?: string
region?: 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<string, ContactsAvatarCacheEntry>
}
export async function getExportSessionMessageCountCache(scopeKey: string): Promise<ExportSessionMessageCountCacheItem | null> {
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<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const rawUpdatedAt = (rawItem as Record<string, unknown>).updatedAt
const rawCounts = (rawItem as Record<string, unknown>).counts
if (!rawCounts || typeof rawCounts !== 'object') return null
const counts: Record<string, number> = {}
for (const [sessionId, countRaw] of Object.entries(rawCounts as Record<string, unknown>)) {
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<string, number>): Promise<void> {
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<string, unknown>) }
: {}
const normalized: Record<string, number> = {}
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<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 (typeof source.firstTimestamp === 'number' && Number.isFinite(source.firstTimestamp) && source.firstTimestamp > 0) {
metric.firstTimestamp = Math.floor(source.firstTimestamp)
}
if (typeof source.lastTimestamp === 'number' && Number.isFinite(source.lastTimestamp) && source.lastTimestamp > 0) {
metric.lastTimestamp = Math.floor(source.lastTimestamp)
}
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 (typeof rawMetric.firstTimestamp === 'number' && Number.isFinite(rawMetric.firstTimestamp) && rawMetric.firstTimestamp > 0) {
metric.firstTimestamp = Math.floor(rawMetric.firstTimestamp)
}
if (typeof rawMetric.lastTimestamp === 'number' && Number.isFinite(rawMetric.lastTimestamp) && rawMetric.lastTimestamp > 0) {
metric.lastTimestamp = Math.floor(rawMetric.lastTimestamp)
}
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> {
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<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const raw = rawItem as Record<string, unknown>
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<void> {
if (!scopeKey) return
const current = await config.get(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP)
const map = current && typeof current === 'object'
? { ...(current as Record<string, unknown>) }
: {}
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<ExportSnsUserPostCountsCacheItem | null> {
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<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const raw = rawItem as Record<string, unknown>
const rawCounts = raw.counts
if (!rawCounts || typeof rawCounts !== 'object') return null
const counts: Record<string, number> = {}
for (const [rawUsername, rawCount] of Object.entries(rawCounts as Record<string, unknown>)) {
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<string, number>
): Promise<void> {
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<string, unknown>) }
: {}
const normalized: Record<string, number> = {}
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<string, unknown>
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<string, unknown>
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<ExportSessionMutualFriendsCacheItem | null> {
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<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, ExportSessionMutualFriendsCacheEntry> = {}
for (const [sessionIdRaw, metricRaw] of Object.entries(rawMetrics as Record<string, unknown>)) {
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<string, ExportSessionMutualFriendsCacheEntry>
): Promise<void> {
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<string, unknown>) }
: {}
const normalized: Record<string, ExportSessionMutualFriendsCacheEntry> = {}
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<void> {
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<string, unknown>) }
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<SnsPageCacheItem | null> {
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<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const raw = rawItem as Record<string, unknown>
const rawOverview = raw.overviewStats
const rawPosts = raw.posts
if (!rawOverview || typeof rawOverview !== 'object' || !Array.isArray(rawPosts)) return null
const overviewObj = rawOverview as Record<string, unknown>
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<void> {
if (!scopeKey) return
const current = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP)
const map = current && typeof current === 'object'
? { ...(current as Record<string, unknown>) }
: {}
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<number> {
const value = await config.get(CONFIG_KEYS.CONTACTS_LOAD_TIMEOUT_MS)
if (typeof value === 'number' && Number.isFinite(value) && value >= 1000 && value <= 60000) {
const normalized = Math.floor(value)
// 兼容历史默认值 3000ms自动提升到新的更稳妥阈值。
return normalized === 3000 ? 10000 : normalized
}
return 10000
}
// 设置通讯录加载超时阈值(毫秒)
export async function setContactsLoadTimeoutMs(timeoutMs: number): Promise<void> {
const normalized = Number.isFinite(timeoutMs)
? Math.min(60000, Math.max(1000, Math.floor(timeoutMs)))
: 10000
await config.set(CONFIG_KEYS.CONTACTS_LOAD_TIMEOUT_MS, normalized)
}
export async function getContactsListCache(scopeKey: string): Promise<ContactsListCacheItem | null> {
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<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const rawUpdatedAt = (rawItem as Record<string, unknown>).updatedAt
const rawContacts = (rawItem as Record<string, unknown>).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<string, unknown>
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,
alias: typeof item.alias === 'string' ? item.alias : undefined,
labels: Array.isArray(item.labels)
? Array.from(new Set(item.labels.map((label) => String(label || '').trim()).filter(Boolean)))
: undefined,
detailDescription: typeof item.detailDescription === 'string' ? (item.detailDescription.trim() || undefined) : undefined,
region: typeof item.region === 'string' ? (item.region.trim() || undefined) : 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<void> {
if (!scopeKey) return
const current = await config.get(CONFIG_KEYS.CONTACTS_LIST_CACHE_MAP)
const map = current && typeof current === 'object'
? { ...(current as Record<string, unknown>) }
: {}
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,
alias: contact?.alias ? String(contact.alias) : undefined,
labels: Array.isArray(contact?.labels)
? Array.from(new Set(contact.labels.map((label) => String(label || '').trim()).filter(Boolean)))
: undefined,
detailDescription: contact?.detailDescription ? (String(contact.detailDescription).trim() || undefined) : undefined,
region: contact?.region ? (String(contact.region).trim() || undefined) : 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<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> {
const value = await config.get(CONFIG_KEYS.AUTH_ENABLED)
return value === true
}
export async function setAuthEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AUTH_ENABLED, enabled)
}
export async function getAuthPassword(): Promise<string> {
const value = await config.get(CONFIG_KEYS.AUTH_PASSWORD)
return (value as string) || ''
}
export async function setAuthPassword(passwordHash: string): Promise<void> {
await config.set(CONFIG_KEYS.AUTH_PASSWORD, passwordHash)
}
export async function getAuthUseHello(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.AUTH_USE_HELLO)
return value === true
}
export async function setAuthUseHello(useHello: boolean): Promise<void> {
await config.set(CONFIG_KEYS.AUTH_USE_HELLO, useHello)
}
// === 更新相关 ===
// 获取被忽略的更新版本
export async function getIgnoredUpdateVersion(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.IGNORED_UPDATE_VERSION)
return (value as string) || null
}
// 设置被忽略的更新版本
export async function setIgnoredUpdateVersion(version: string): Promise<void> {
await config.set(CONFIG_KEYS.IGNORED_UPDATE_VERSION, version)
}
// 获取通知开关
export async function getNotificationEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.NOTIFICATION_ENABLED)
return value !== false // 默认为 true
}
// 设置通知开关
export async function setNotificationEnabled(enabled: boolean): Promise<void> {
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<void> {
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<void> {
await config.set(CONFIG_KEYS.NOTIFICATION_FILTER_MODE, mode)
}
// 获取通知过滤列表
export async function getNotificationFilterList(): Promise<string[]> {
const value = await config.get(CONFIG_KEYS.NOTIFICATION_FILTER_LIST)
return Array.isArray(value) ? value : []
}
// 设置通知过滤列表
export async function setNotificationFilterList(list: string[]): Promise<void> {
await config.set(CONFIG_KEYS.NOTIFICATION_FILTER_LIST, list)
}
export async function getMessagePushEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.MESSAGE_PUSH_ENABLED)
return value === true
}
export async function setMessagePushEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.MESSAGE_PUSH_ENABLED, enabled)
}
export async function getWindowCloseBehavior(): Promise<WindowCloseBehavior> {
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<void> {
await config.set(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR, behavior)
}
export async function getQuoteLayout(): Promise<QuoteLayout> {
const value = await config.get(CONFIG_KEYS.QUOTE_LAYOUT)
if (value === 'quote-bottom') return value
return 'quote-top'
}
export async function setQuoteLayout(layout: QuoteLayout): Promise<void> {
await config.set(CONFIG_KEYS.QUOTE_LAYOUT, layout)
}
// 获取词云排除词列表
export async function getWordCloudExcludeWords(): Promise<string[]> {
const value = await config.get(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS)
return Array.isArray(value) ? value : []
}
// 设置词云排除词列表
export async function setWordCloudExcludeWords(words: string[]): Promise<void> {
await config.set(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS, words)
}
// 获取数据收集同意状态
export async function getAnalyticsConsent(): Promise<boolean | null> {
const value = await config.get(CONFIG_KEYS.ANALYTICS_CONSENT)
if (typeof value === 'boolean') return value
return null
}
// 设置数据收集同意状态
export async function setAnalyticsConsent(consent: boolean): Promise<void> {
await config.set(CONFIG_KEYS.ANALYTICS_CONSENT, consent)
}
// 获取数据收集拒绝次数
export async function getAnalyticsDenyCount(): Promise<number> {
const value = await config.get(CONFIG_KEYS.ANALYTICS_DENY_COUNT)
return typeof value === 'number' ? value : 0
}
// 设置数据收集拒绝次数
export async function setAnalyticsDenyCount(count: number): Promise<void> {
await config.set(CONFIG_KEYS.ANALYTICS_DENY_COUNT, count)
}
// 获取 HTTP API 自动启动状态
export async function getHttpApiEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.HTTP_API_ENABLED)
return value === true
}
// 设置 HTTP API 自动启动状态
export async function setHttpApiEnabled(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.HTTP_API_ENABLED, enabled)
}
// 获取 HTTP API 端口
export async function getHttpApiPort(): Promise<number> {
const value = await config.get(CONFIG_KEYS.HTTP_API_PORT)
return typeof value === 'number' ? value : 5031
}
// 设置 HTTP API 端口
export async function setHttpApiPort(port: number): Promise<void> {
await config.set(CONFIG_KEYS.HTTP_API_PORT, port)
}
export async function getHttpApiHost(): Promise<string> {
const value = await config.get(CONFIG_KEYS.HTTP_API_HOST)
return typeof value === 'string' && value.trim() ? value.trim() : '127.0.0.1'
}
export async function setHttpApiHost(host: string): Promise<void> {
await config.set(CONFIG_KEYS.HTTP_API_HOST, host)
}