mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 04:20:34 +08:00
统一问问微信与 Agent Hub 的查询链路 完善查询覆盖度、新鲜度和部分结果表达,避免索引滞后产生错误结论 基于会话实现真正的增量追新与历史补齐 支持后台同步、取消恢复、重启续传以及同步期间继续查询 优化知识库跨会话检索、同步状态、进度展示和侧栏布局
214 lines
6.7 KiB
TypeScript
214 lines
6.7 KiB
TypeScript
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
|
||
}
|