Files
WechatExplorer/src/main/services/settings-store.ts
T
Wxw-Gu ab3b3731b7 feat: 重构问问微信并完善知识库增量检索
统一问问微信与 Agent Hub 的查询链路
完善查询覆盖度、新鲜度和部分结果表达,避免索引滞后产生错误结论
基于会话实现真正的增量追新与历史补齐
支持后台同步、取消恢复、重启续传以及同步期间继续查询
优化知识库跨会话检索、同步状态、进度展示和侧栏布局
2026-09-11 17:44:48 +08:00

214 lines
6.7 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.
import { app } from 'electron'
import fs from 'fs-extra'
import path from 'path'
import os from 'os'
import type { TextToSpeechModel } from '../../shared/text-to-speech'
/**
* 把 V3 时代的 "...\\Documents\\WeChat Files" 路径重定向到
* "...\\Documents\\xwechat_files"(V4)。如果 xwechat_files 不存在则保留原值。
* 仅支持 WeChat 4.0:自动纠正用户机器上残留的旧路径。
*/
function redirectLegacyWeChatFilesToXwechat(candidate: string): string {
if (!candidate) return candidate
const normalized = candidate.replace(/[\\/]+$/, '')
const lowered = normalized.toLowerCase()
const legacyMarker = `${path.sep}wechat files`
if (!lowered.endsWith(legacyMarker)) return candidate
const redirected = `${normalized.slice(0, -legacyMarker.length)}${path.sep}xwechat_files`
if (fs.existsSync(redirected)) return redirected
return candidate
}
export interface AppSettings {
dbRoot: string
apiEnabled: boolean
apiHost: string
apiPort: number
imageKeyRoot: string
imageXorKey: string
imageAesKey: string
imageKeyFallbackDisabled: boolean
ffmpegPath: string
recallProtectionEnabled: boolean
debugEnabled: boolean
autoLogin: boolean
autoLoginPreferenceSet: boolean
appearanceTheme: 'system' | 'light' | 'dark'
compactMode: boolean
showStartupProgress: boolean
ttsSelectedVoiceId: string
ttsModel: TextToSpeechModel
/** Keep a running personal-WeChat OneBot process across app restarts. */
keepPersonalWechatProcess?: boolean
windowsWechatPort: string
/**
* Query Agent 是否为桌面「问问微信」与 Agent Hub 查询类问题的主路径。
* 默认开启;关闭后回退到 Legacy AI Search Pipeline(仅作 runtime regression 时的回退开关,
* 不在用户界面暴露实验性名称)。
*/
queryAgentEnabled: boolean
}
function getDefaultDbRoot(): string {
const home = os.homedir()
const candidates = getDefaultDbRootCandidates(home)
return candidates.find((candidate) => isUsableDbRoot(candidate)) || candidates[0]
}
function getDefaultDbRootCandidates(home: string): string[] {
if (process.platform !== 'win32') {
// macOS 仅支持 WeChat 4.0 路径(xwechat_files)
return [
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files')
]
}
// 仅支持 WeChat 4.0:剔除 V3 时代的 "WeChat Files" 目录,
// 只认 xwechat_files(含 Documents\ 和 AppData\Roaming\Tencent\ 两种合法位置)。
const candidates = [
path.join(home, 'Documents', 'xwechat_files'),
path.join(os.homedir(), 'AppData', 'Roaming', 'Tencent', 'xwechat_files')
]
return unique(candidates)
}
function unique(values: string[]): string[] {
return Array.from(new Set(values))
}
export function isUsableDbRoot(candidate?: string): boolean {
if (!candidate || !fs.existsSync(candidate)) return false
if (fs.existsSync(path.join(candidate, 'db_storage'))) return true
try {
return fs
.readdirSync(candidate)
.some((name) => fs.existsSync(path.join(candidate, name, 'db_storage')))
} catch {
return false
}
}
export function validateDbRoot(candidate?: string): { valid: boolean; error?: string } {
const root = String(candidate || '').trim()
if (!root) return { valid: false, error: '微信数据目录为空,请重新选择目录' }
if (!fs.existsSync(root)) {
return { valid: false, error: '微信数据目录不存在,请检查路径或重新选择目录' }
}
if (!isUsableDbRoot(root)) {
return {
valid: false,
error: '所选目录中未找到微信 4.x 数据库(db_storage),请选择 xwechat_files 或账号目录'
}
}
return { valid: true }
}
const defaultDbRoot = getDefaultDbRoot()
const DEFAULT_SETTINGS: AppSettings = {
dbRoot: defaultDbRoot,
apiEnabled: true,
apiHost: '127.0.0.1',
apiPort: 6131,
imageKeyRoot: defaultDbRoot,
imageXorKey: '',
imageAesKey: '',
imageKeyFallbackDisabled: false,
ffmpegPath: '',
recallProtectionEnabled: false,
debugEnabled: false,
autoLogin: ['1', 'true', 'yes', 'on'].includes(
String(import.meta.env.VITE_AUTO_LOGIN || '')
.trim()
.toLowerCase()
),
autoLoginPreferenceSet: false,
appearanceTheme: 'system',
compactMode: false,
showStartupProgress: true,
ttsSelectedVoiceId: '',
ttsModel: 's2.1-pro-free',
keepPersonalWechatProcess: false,
windowsWechatPort: '',
queryAgentEnabled: true
}
const SETTINGS_FILE = path.join(
process.env['WE_SETTINGS_DIR'] || app.getPath('userData'),
'settings.json'
)
let cache: AppSettings | null = null
function ensureDir(): void {
fs.ensureDirSync(path.dirname(SETTINGS_FILE))
}
export function loadSettings(): AppSettings {
if (cache) return cache
try {
if (fs.existsSync(SETTINGS_FILE)) {
const raw = fs.readJsonSync(SETTINGS_FILE) as Partial<AppSettings>
cache = { ...DEFAULT_SETTINGS, ...raw }
if (raw.autoLogin === undefined) {
const hasSavedDatabaseKey = fs.existsSync(
path.join(app.getPath('userData'), 'wechat-db-key.bin')
)
if (hasSavedDatabaseKey) cache.autoLogin = true
}
if (process.platform === 'win32' && !isUsableDbRoot(cache.dbRoot)) {
cache.dbRoot = getDefaultDbRoot()
}
// 同步:imageKeyRoot 必须跟随 dbRoot 更新,
// 否则自动获取会扫错目录(旧 bug:状态面板显示 D 盘,自动获取扫 C 盘)。
if (!cache.imageKeyRoot || !isUsableDbRoot(cache.imageKeyRoot)) {
cache.imageKeyRoot = cache.dbRoot
}
// V4-only 兜底:如果 imageKeyRoot 指向旧的 "WeChat Files"(V3 路径),
// 重定向到同一父目录下的 xwechat_files(V4)。
if (cache.imageKeyRoot) {
cache.imageKeyRoot = redirectLegacyWeChatFilesToXwechat(cache.imageKeyRoot)
}
if (cache.dbRoot) {
cache.dbRoot = redirectLegacyWeChatFilesToXwechat(cache.dbRoot)
}
return cache
}
} catch (error) {
console.warn('[Settings] failed to load, fallback to defaults:', error)
}
cache = { ...DEFAULT_SETTINGS }
return cache
}
export function saveSettings(next: AppSettings): AppSettings {
cache = { ...next }
try {
ensureDir()
fs.writeJsonSync(SETTINGS_FILE, cache, { spaces: 2 })
} catch (error) {
console.error('[Settings] failed to save:', error)
}
return cache
}
export function updateSettings(patch: Partial<AppSettings>): AppSettings {
return saveSettings({ ...loadSettings(), ...patch })
}
export function resetSettings(): AppSettings {
cache = null
try {
if (fs.existsSync(SETTINGS_FILE)) fs.unlinkSync(SETTINGS_FILE)
} catch {
// best effort
}
return loadSettings()
}
export function getSettingsPath(): string {
return SETTINGS_FILE
}