mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
feat: 设置功能
This commit is contained in:
@@ -2,6 +2,7 @@ import { app, shell } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import type { AppLogEntry } from '../shared/app-log'
|
||||
import { isPackagedRuntime } from './runtime-mode'
|
||||
|
||||
const MAX_LOG_BYTES = 5 * 1024 * 1024
|
||||
const REDACTED_KEY = /(?:api[-_]?key|authorization|token|secret|password|database[-_]?key)/i
|
||||
@@ -52,14 +53,14 @@ export class AppLogger {
|
||||
this.rotateIfNeeded()
|
||||
const record = {
|
||||
timestamp: new Date().toISOString(),
|
||||
mode: app.isPackaged ? 'packaged' : 'development',
|
||||
mode: isPackagedRuntime() ? 'packaged' : 'development',
|
||||
level: entry.level,
|
||||
scope: String(entry.scope || 'app').slice(0, 80),
|
||||
message: String(entry.message || '').slice(0, 500),
|
||||
details: sanitize(entry.details || {})
|
||||
}
|
||||
fs.appendFileSync(this.logPath, `${JSON.stringify(record)}\n`, { encoding: 'utf8' })
|
||||
if (!app.isPackaged) {
|
||||
if (!isPackagedRuntime()) {
|
||||
const method =
|
||||
entry.level === 'error'
|
||||
? console.error
|
||||
|
||||
@@ -76,6 +76,9 @@ import { installSafeConsole } from './safe-log'
|
||||
import { agentHubService } from './services/agent-hub-service'
|
||||
import { appLogger } from './app-logger'
|
||||
import type { AppLogEntry } from '../shared/app-log'
|
||||
import { appUpdateService } from './services/app-update-service'
|
||||
import { clearCache, getCacheSummary } from './services/cache-service'
|
||||
import type { CacheClearScope } from './services/cache-service'
|
||||
import { configureRecallArchive, RecallArchiveMonitor } from './services/recall-archive-service'
|
||||
import { VideoAssetService } from './video-asset-service'
|
||||
import { cancelExport, revealExport, runExport } from './export-service'
|
||||
@@ -349,6 +352,17 @@ app.whenReady().then(async () => {
|
||||
ipcMain.handle('app-log:write', (_, entry: AppLogEntry) => appLogger.write(entry))
|
||||
ipcMain.handle('app-log:getPath', () => appLogger.logPath)
|
||||
ipcMain.handle('app-log:reveal', () => appLogger.reveal())
|
||||
ipcMain.handle('app-update:getState', () => appUpdateService.getState())
|
||||
ipcMain.handle('app-update:check', () => appUpdateService.check())
|
||||
ipcMain.handle('app-update:download', () => appUpdateService.download())
|
||||
ipcMain.handle('app-update:install', () => appUpdateService.install())
|
||||
ipcMain.handle('cache:getSummary', () => getCacheSummary())
|
||||
ipcMain.handle('cache:clear', async (_, scope: CacheClearScope) => {
|
||||
const allowedScopes: CacheClearScope[] = ['bootstrap', 'electron', 'all']
|
||||
if (!allowedScopes.includes(scope)) return getCacheSummary()
|
||||
imageDecryptService = null
|
||||
return clearCache(scope)
|
||||
})
|
||||
|
||||
ipcMain.handle('db:init', async (_, key: string) => {
|
||||
if (dbInitInFlight) return dbInitInFlight
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { app } from 'electron'
|
||||
import { existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
export function isPackagedRuntime(): boolean {
|
||||
if (app.isPackaged) return true
|
||||
|
||||
return (
|
||||
existsSync(join(process.resourcesPath, 'app.asar')) &&
|
||||
existsSync(join(process.resourcesPath, 'app-update.yml'))
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
import type { AppSettings } from './settings-store'
|
||||
import { generateAgentGroupReport } from './agent-group-report-service'
|
||||
import { AIProviderService } from './ai-provider-service'
|
||||
import { isPackagedRuntime } from '../runtime-mode'
|
||||
import {
|
||||
getGroupSnapshot,
|
||||
isReady,
|
||||
@@ -68,7 +69,7 @@ const agentAIProvider = new AIProviderService()
|
||||
function resolveBundledBinary(
|
||||
resourceSegments: string[],
|
||||
executable: string,
|
||||
packaged = app.isPackaged,
|
||||
packaged = isPackagedRuntime(),
|
||||
platform = process.platform,
|
||||
arch = process.arch
|
||||
): string {
|
||||
@@ -80,7 +81,7 @@ function resolveBundledBinary(
|
||||
}
|
||||
|
||||
export function resolveWechatConnectorBinaryPath(
|
||||
packaged = app.isPackaged,
|
||||
packaged = isPackagedRuntime(),
|
||||
platform = process.platform,
|
||||
arch = process.arch
|
||||
): string {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { autoUpdater, type ProgressInfo } from 'electron-updater'
|
||||
import type { AppUpdateCheckResult, AppUpdateState } from '../../shared/app-update'
|
||||
import { isPackagedRuntime } from '../runtime-mode'
|
||||
|
||||
export class AppUpdateService {
|
||||
private state: AppUpdateState = {
|
||||
status: 'idle',
|
||||
currentVersion: app.getVersion()
|
||||
}
|
||||
|
||||
constructor() {
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.on('checking-for-update', () => this.setState({ status: 'checking' }))
|
||||
autoUpdater.on('update-available', (info) =>
|
||||
this.setState({ status: 'available', version: info.version, message: '发现新版本' })
|
||||
)
|
||||
autoUpdater.on('update-not-available', () =>
|
||||
this.setState({ status: 'not-available', message: '当前已是最新版本' })
|
||||
)
|
||||
autoUpdater.on('download-progress', (progress: ProgressInfo) =>
|
||||
this.setState({
|
||||
status: 'downloading',
|
||||
percent: progress.percent,
|
||||
transferred: progress.transferred,
|
||||
total: progress.total,
|
||||
bytesPerSecond: progress.bytesPerSecond
|
||||
})
|
||||
)
|
||||
autoUpdater.on('update-downloaded', (info) =>
|
||||
this.setState({
|
||||
status: 'downloaded',
|
||||
version: info.version,
|
||||
percent: 100,
|
||||
message: '更新已下载'
|
||||
})
|
||||
)
|
||||
autoUpdater.on('error', (error) =>
|
||||
this.setState({ status: 'error', message: error.message || '更新失败' })
|
||||
)
|
||||
}
|
||||
|
||||
getState(): AppUpdateState {
|
||||
return { ...this.state, currentVersion: app.getVersion() }
|
||||
}
|
||||
|
||||
async check(): Promise<AppUpdateCheckResult> {
|
||||
if (!isPackagedRuntime()) {
|
||||
const state = this.setState({
|
||||
status: 'unsupported',
|
||||
message: '开发模式不执行安装包更新,请在正式安装包中检查更新'
|
||||
})
|
||||
return { success: false, state }
|
||||
}
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result?.updateInfo.version) {
|
||||
this.setState({
|
||||
status: 'available',
|
||||
version: result.updateInfo.version,
|
||||
message: '发现新版本'
|
||||
})
|
||||
}
|
||||
return { success: true, state: this.getState() }
|
||||
} catch (error) {
|
||||
const state = this.setState({
|
||||
status: 'error',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return { success: false, state }
|
||||
}
|
||||
}
|
||||
|
||||
async download(): Promise<AppUpdateCheckResult> {
|
||||
if (!isPackagedRuntime()) {
|
||||
const state = this.setState({ status: 'unsupported', message: '开发模式不能下载更新' })
|
||||
return { success: false, state }
|
||||
}
|
||||
try {
|
||||
this.setState({ status: 'downloading', percent: 0 })
|
||||
await autoUpdater.downloadUpdate()
|
||||
return { success: true, state: this.getState() }
|
||||
} catch (error) {
|
||||
const state = this.setState({
|
||||
status: 'error',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return { success: false, state }
|
||||
}
|
||||
}
|
||||
|
||||
install(): { success: boolean; error?: string } {
|
||||
if (this.state.status !== 'downloaded') {
|
||||
return { success: false, error: '更新包尚未下载完成' }
|
||||
}
|
||||
autoUpdater.quitAndInstall()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
handleState(callback: (state: AppUpdateState) => void): () => void {
|
||||
this.listeners.add(callback)
|
||||
callback(this.getState())
|
||||
return () => this.listeners.delete(callback)
|
||||
}
|
||||
|
||||
private listeners = new Set<(state: AppUpdateState) => void>()
|
||||
|
||||
private setState(patch: Partial<AppUpdateState>): AppUpdateState {
|
||||
this.state = { ...this.state, ...patch, currentVersion: app.getVersion() }
|
||||
const state = this.getState()
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) window.webContents.send('app-update:state', state)
|
||||
}
|
||||
for (const listener of this.listeners) listener(state)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const appUpdateService = new AppUpdateService()
|
||||
@@ -369,6 +369,13 @@ export function flushBootstrapCacheWritesSync(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function clearBootstrapCache(): void {
|
||||
for (const timer of writeTimers.values()) clearTimeout(timer)
|
||||
writeTimers.clear()
|
||||
writeQueues.clear()
|
||||
memoryCache.clear()
|
||||
}
|
||||
|
||||
export function saveCachedMessages(
|
||||
accountRoot: string,
|
||||
userMd5: string,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { app, session } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { clearBootstrapCache } from './bootstrap-cache'
|
||||
import type { CacheClearScope, CacheSummary, CacheSummaryItem } from '../../shared/cache'
|
||||
|
||||
export type { CacheClearScope } from '../../shared/cache'
|
||||
|
||||
const BOOTSTRAP_CACHE_DIR = path.join(app.getPath('userData'), 'cache', 'bootstrap')
|
||||
|
||||
function inspectDirectory(directory: string): { sizeBytes: number; fileCount: number } {
|
||||
if (!fs.existsSync(directory)) return { sizeBytes: 0, fileCount: 0 }
|
||||
let sizeBytes = 0
|
||||
let fileCount = 0
|
||||
const visit = (current: string): void => {
|
||||
let entries: fs.Dirent[]
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const target = path.join(current, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
visit(target)
|
||||
} else if (entry.isFile()) {
|
||||
try {
|
||||
sizeBytes += fs.statSync(target).size
|
||||
fileCount += 1
|
||||
} catch {
|
||||
// A cache file can disappear while it is being inspected.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(directory)
|
||||
return { sizeBytes, fileCount }
|
||||
}
|
||||
|
||||
export function getCacheSummary(): CacheSummary {
|
||||
const bootstrap = inspectDirectory(BOOTSTRAP_CACHE_DIR)
|
||||
const electron = inspectDirectory(path.join(app.getPath('userData'), 'Cache'))
|
||||
const items: CacheSummaryItem[] = [
|
||||
{
|
||||
id: 'bootstrap',
|
||||
label: '启动与聊天缓存',
|
||||
description: '联系人、头像、群成员和最近聊天记录的本地副本。',
|
||||
...bootstrap
|
||||
},
|
||||
{
|
||||
id: 'electron',
|
||||
label: '应用临时缓存',
|
||||
description: 'Electron 页面资源缓存,清理后会自动重新生成。',
|
||||
...electron
|
||||
}
|
||||
]
|
||||
return {
|
||||
items,
|
||||
totalBytes: items.reduce((total, item) => total + item.sizeBytes, 0),
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache(scope: CacheClearScope): Promise<CacheSummary> {
|
||||
if (scope === 'bootstrap' || scope === 'all') {
|
||||
clearBootstrapCache()
|
||||
await fs.remove(BOOTSTRAP_CACHE_DIR)
|
||||
}
|
||||
if (scope === 'electron' || scope === 'all') {
|
||||
await session.defaultSession.clearCache()
|
||||
}
|
||||
return getCacheSummary()
|
||||
}
|
||||
@@ -32,6 +32,9 @@ export interface AppSettings {
|
||||
debugEnabled: boolean
|
||||
autoLogin: boolean
|
||||
autoLoginPreferenceSet: boolean
|
||||
appearanceTheme: 'system' | 'light' | 'dark'
|
||||
compactMode: boolean
|
||||
showStartupProgress: boolean
|
||||
}
|
||||
|
||||
function getDefaultDbRoot(): string {
|
||||
@@ -112,7 +115,10 @@ const DEFAULT_SETTINGS: AppSettings = {
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
),
|
||||
autoLoginPreferenceSet: false
|
||||
autoLoginPreferenceSet: false,
|
||||
appearanceTheme: 'system',
|
||||
compactMode: false,
|
||||
showStartupProgress: true
|
||||
}
|
||||
|
||||
const SETTINGS_FILE = path.join(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app, shell } from 'electron'
|
||||
import { existsSync, promises as fs } from 'fs'
|
||||
import { dirname, join } from 'path'
|
||||
import { isPackagedRuntime } from '../runtime-mode'
|
||||
|
||||
const SKILL_RELATIVE_PATH = join('skill', 'wechatexplorer-reader', 'SKILL.md')
|
||||
const GITHUB_URL =
|
||||
@@ -23,7 +24,7 @@ function getSkillCandidates(): { path: string; source: 'development' | 'bundled'
|
||||
join(dirname(app.getAppPath()), SKILL_RELATIVE_PATH),
|
||||
join(dirname(process.execPath), 'resources', SKILL_RELATIVE_PATH)
|
||||
]
|
||||
return app.isPackaged
|
||||
return isPackagedRuntime()
|
||||
? bundledPaths.map((path) => ({ path, source: 'bundled' as const }))
|
||||
: [
|
||||
{ path: developmentPath, source: 'development' as const },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
import { Wcdb4Client } from './wcdb4-client'
|
||||
import { isPackagedRuntime } from './runtime-mode'
|
||||
|
||||
export class VoiceService {
|
||||
private wcdb4Client: Wcdb4Client
|
||||
@@ -101,7 +102,7 @@ export class VoiceService {
|
||||
private async decodeSilkToPcm(silkData: Buffer, sampleRate: number): Promise<Buffer | null> {
|
||||
try {
|
||||
let wasmPath: string
|
||||
if (app.isPackaged) {
|
||||
if (isPackagedRuntime()) {
|
||||
wasmPath = join(
|
||||
process.resourcesPath,
|
||||
'app.asar.unpacked',
|
||||
|
||||
Reference in New Issue
Block a user