import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron' import { Worker } from 'worker_threads' import { join } from 'path' import { autoUpdater } from 'electron-updater' import { readFile, writeFile, mkdir } from 'fs/promises' import { existsSync } from 'fs' import { ConfigService } from './services/config' import { dbPathService } from './services/dbPathService' import { wcdbService } from './services/wcdbService' import { chatService } from './services/chatService' import { imageDecryptService } from './services/imageDecryptService' import { imagePreloadService } from './services/imagePreloadService' import { analyticsService } from './services/analyticsService' import { groupAnalyticsService } from './services/groupAnalyticsService' import { annualReportService } from './services/annualReportService' import { exportService, ExportOptions } from './services/exportService' import { KeyService } from './services/keyService' // 配置自动更新 autoUpdater.autoDownload = false autoUpdater.autoInstallOnAppQuit = true autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载 const AUTO_UPDATE_ENABLED = process.env.AUTO_UPDATE_ENABLED === 'true' || process.env.AUTO_UPDATE_ENABLED === '1' || (process.env.AUTO_UPDATE_ENABLED == null && !process.env.VITE_DEV_SERVER_URL) // 单例服务 let configService: ConfigService | null = null // 协议窗口实例 let agreementWindow: BrowserWindow | null = null let onboardingWindow: BrowserWindow | null = null const keyService = new KeyService() let mainWindowReady = false let shouldShowMain = true function createWindow(options: { autoShow?: boolean } = {}) { // 获取图标路径 - 打包后在 resources 目录 const { autoShow = true } = options const isDev = !!process.env.VITE_DEV_SERVER_URL const iconPath = isDev ? join(__dirname, '../public/icon.ico') : join(process.resourcesPath, 'icon.ico') const win = new BrowserWindow({ width: 1400, height: 900, minWidth: 1000, minHeight: 700, icon: iconPath, webPreferences: { preload: join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, titleBarStyle: 'hidden', titleBarOverlay: { color: '#00000000', symbolColor: '#1a1a1a', height: 40 }, show: false }) // 窗口准备好后显示 win.once('ready-to-show', () => { mainWindowReady = true if (autoShow || shouldShowMain) { win.show() } }) // 开发环境加载 vite 服务器 if (process.env.VITE_DEV_SERVER_URL) { win.loadURL(process.env.VITE_DEV_SERVER_URL) // 开发环境下按 F12 或 Ctrl+Shift+I 打开开发者工具 win.webContents.on('before-input-event', (event, input) => { if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) { if (win.webContents.isDevToolsOpened()) { win.webContents.closeDevTools() } else { win.webContents.openDevTools() } event.preventDefault() } }) } else { win.loadFile(join(__dirname, '../dist/index.html')) } return win } /** * 创建用户协议窗口 */ function createAgreementWindow() { // 如果已存在,聚焦 if (agreementWindow && !agreementWindow.isDestroyed()) { agreementWindow.focus() return agreementWindow } const isDev = !!process.env.VITE_DEV_SERVER_URL const iconPath = isDev ? join(__dirname, '../public/icon.ico') : join(process.resourcesPath, 'icon.ico') const isDark = nativeTheme.shouldUseDarkColors agreementWindow = new BrowserWindow({ width: 700, height: 600, minWidth: 500, minHeight: 400, icon: iconPath, webPreferences: { preload: join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, titleBarStyle: 'hidden', titleBarOverlay: { color: '#00000000', symbolColor: isDark ? '#FFFFFF' : '#333333', height: 32 }, show: false, backgroundColor: isDark ? '#1A1A1A' : '#FFFFFF' }) agreementWindow.once('ready-to-show', () => { agreementWindow?.show() }) if (process.env.VITE_DEV_SERVER_URL) { agreementWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/agreement-window`) } else { agreementWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/agreement-window' }) } agreementWindow.on('closed', () => { agreementWindow = null }) return agreementWindow } /** * 创建首次引导窗口 */ function createOnboardingWindow() { if (onboardingWindow && !onboardingWindow.isDestroyed()) { onboardingWindow.focus() return onboardingWindow } const isDev = !!process.env.VITE_DEV_SERVER_URL const iconPath = isDev ? join(__dirname, '../public/icon.ico') : join(process.resourcesPath, 'icon.ico') onboardingWindow = new BrowserWindow({ width: 1100, height: 720, minWidth: 900, minHeight: 600, frame: false, transparent: true, backgroundColor: '#00000000', hasShadow: false, icon: iconPath, webPreferences: { preload: join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, show: false }) onboardingWindow.once('ready-to-show', () => { onboardingWindow?.show() }) if (process.env.VITE_DEV_SERVER_URL) { onboardingWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/onboarding-window`) } else { onboardingWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/onboarding-window' }) } onboardingWindow.on('closed', () => { onboardingWindow = null }) return onboardingWindow } function showMainWindow() { shouldShowMain = true if (mainWindowReady) { mainWindow?.show() } } // 注册 IPC 处理器 function registerIpcHandlers() { // 配置相关 ipcMain.handle('config:get', async (_, key: string) => { return configService?.get(key as any) }) ipcMain.handle('config:set', async (_, key: string, value: any) => { return configService?.set(key as any, value) }) ipcMain.handle('config:clear', async () => { configService?.clear() return true }) // 文件对话框 ipcMain.handle('dialog:openFile', async (_, options) => { const { dialog } = await import('electron') return dialog.showOpenDialog(options) }) ipcMain.handle('dialog:openDirectory', async (_, options) => { const { dialog } = await import('electron') return dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'], ...options }) }) ipcMain.handle('dialog:saveFile', async (_, options) => { const { dialog } = await import('electron') return dialog.showSaveDialog(options) }) ipcMain.handle('shell:openPath', async (_, path: string) => { const { shell } = await import('electron') return shell.openPath(path) }) ipcMain.handle('shell:openExternal', async (_, url: string) => { const { shell } = await import('electron') return shell.openExternal(url) }) ipcMain.handle('app:getDownloadsPath', async () => { return app.getPath('downloads') }) ipcMain.handle('app:getVersion', async () => { return app.getVersion() }) ipcMain.handle('log:getPath', async () => { return join(app.getPath('userData'), 'logs', 'wcdb.log') }) ipcMain.handle('log:read', async () => { try { const logPath = join(app.getPath('userData'), 'logs', 'wcdb.log') const content = await readFile(logPath, 'utf8') return { success: true, content } } catch (e) { return { success: false, error: String(e) } } }) ipcMain.handle('app:checkForUpdates', async () => { if (!AUTO_UPDATE_ENABLED) { return { hasUpdate: false } } try { const result = await autoUpdater.checkForUpdates() if (result && result.updateInfo) { const currentVersion = app.getVersion() const latestVersion = result.updateInfo.version if (latestVersion !== currentVersion) { return { hasUpdate: true, version: latestVersion, releaseNotes: result.updateInfo.releaseNotes as string || '' } } } return { hasUpdate: false } } catch (error) { console.error('检查更新失败:', error) return { hasUpdate: false } } }) ipcMain.handle('app:downloadAndInstall', async (event) => { if (!AUTO_UPDATE_ENABLED) { throw new Error('自动更新已暂时禁用') } const win = BrowserWindow.fromWebContents(event.sender) // 监听下载进度 autoUpdater.on('download-progress', (progress) => { win?.webContents.send('app:downloadProgress', progress.percent) }) // 下载完成后自动安装 autoUpdater.on('update-downloaded', () => { autoUpdater.quitAndInstall(false, true) }) try { await autoUpdater.downloadUpdate() } catch (error) { console.error('下载更新失败:', error) throw error } }) // 窗口控制 ipcMain.on('window:minimize', (event) => { BrowserWindow.fromWebContents(event.sender)?.minimize() }) ipcMain.on('window:maximize', (event) => { const win = BrowserWindow.fromWebContents(event.sender) if (win?.isMaximized()) { win.unmaximize() } else { win?.maximize() } }) ipcMain.on('window:close', (event) => { BrowserWindow.fromWebContents(event.sender)?.close() }) // 更新窗口控件主题色 ipcMain.on('window:setTitleBarOverlay', (event, options: { symbolColor: string }) => { const win = BrowserWindow.fromWebContents(event.sender) if (win) { try { win.setTitleBarOverlay({ color: '#00000000', symbolColor: options.symbolColor, height: 40 }) } catch (error) { console.warn('TitleBarOverlay not enabled for this window:', error) } } }) // 数据库路径相关 ipcMain.handle('dbpath:autoDetect', async () => { return dbPathService.autoDetect() }) ipcMain.handle('dbpath:scanWxids', async (_, rootPath: string) => { return dbPathService.scanWxids(rootPath) }) ipcMain.handle('dbpath:getDefault', async () => { return dbPathService.getDefaultPath() }) // WCDB 数据库相关 ipcMain.handle('wcdb:testConnection', async (_, dbPath: string, hexKey: string, wxid: string) => { return wcdbService.testConnection(dbPath, hexKey, wxid) }) ipcMain.handle('wcdb:open', async (_, dbPath: string, hexKey: string, wxid: string) => { return wcdbService.open(dbPath, hexKey, wxid) }) ipcMain.handle('wcdb:close', async () => { wcdbService.close() return true }) // 聊天相关 ipcMain.handle('chat:connect', async () => { return chatService.connect() }) ipcMain.handle('chat:getSessions', async () => { return chatService.getSessions() }) ipcMain.handle('chat:enrichSessionsContactInfo', async (_, usernames: string[]) => { return chatService.enrichSessionsContactInfo(usernames) }) ipcMain.handle('chat:getMessages', async (_, sessionId: string, offset?: number, limit?: number) => { return chatService.getMessages(sessionId, offset, limit) }) ipcMain.handle('chat:getLatestMessages', async (_, sessionId: string, limit?: number) => { return chatService.getLatestMessages(sessionId, limit) }) ipcMain.handle('chat:getContact', async (_, username: string) => { return chatService.getContact(username) }) ipcMain.handle('chat:getContactAvatar', async (_, username: string) => { return chatService.getContactAvatar(username) }) ipcMain.handle('chat:getCachedMessages', async (_, sessionId: string) => { return chatService.getCachedSessionMessages(sessionId) }) ipcMain.handle('chat:getMyAvatarUrl', async () => { return chatService.getMyAvatarUrl() }) ipcMain.handle('chat:downloadEmoji', async (_, cdnUrl: string, md5?: string) => { return chatService.downloadEmoji(cdnUrl, md5) }) ipcMain.handle('chat:close', async () => { chatService.close() return true }) ipcMain.handle('chat:getSessionDetail', async (_, sessionId: string) => { return chatService.getSessionDetail(sessionId) }) ipcMain.handle('chat:getImageData', async (_, sessionId: string, msgId: string) => { return chatService.getImageData(sessionId, msgId) }) ipcMain.handle('chat:getVoiceData', async (_, sessionId: string, msgId: string) => { return chatService.getVoiceData(sessionId, msgId) }) ipcMain.handle('chat:getMessageById', async (_, sessionId: string, localId: number) => { return chatService.getMessageById(sessionId, localId) }) // 私聊克隆 ipcMain.handle('image:decrypt', async (_, payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => { return imageDecryptService.decryptImage(payload) }) ipcMain.handle('image:resolveCache', async (_, payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }) => { return imageDecryptService.resolveCachedImage(payload) }) ipcMain.handle('image:preload', async (_, payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>) => { imagePreloadService.enqueue(payloads || []) return true }) // 导出相关 ipcMain.handle('export:exportSessions', async (_, sessionIds: string[], outputDir: string, options: ExportOptions) => { return exportService.exportSessions(sessionIds, outputDir, options) }) ipcMain.handle('export:exportSession', async (_, sessionId: string, outputPath: string, options: ExportOptions) => { return exportService.exportSessionToChatLab(sessionId, outputPath, options) }) // 数据分析相关 ipcMain.handle('analytics:getOverallStatistics', async (_, force?: boolean) => { return analyticsService.getOverallStatistics(force) }) ipcMain.handle('analytics:getContactRankings', async (_, limit?: number) => { return analyticsService.getContactRankings(limit) }) ipcMain.handle('analytics:getTimeDistribution', async () => { return analyticsService.getTimeDistribution() }) // 群聊分析相关 ipcMain.handle('groupAnalytics:getGroupChats', async () => { return groupAnalyticsService.getGroupChats() }) ipcMain.handle('groupAnalytics:getGroupMembers', async (_, chatroomId: string) => { return groupAnalyticsService.getGroupMembers(chatroomId) }) ipcMain.handle('groupAnalytics:getGroupMessageRanking', async (_, chatroomId: string, limit?: number, startTime?: number, endTime?: number) => { return groupAnalyticsService.getGroupMessageRanking(chatroomId, limit, startTime, endTime) }) ipcMain.handle('groupAnalytics:getGroupActiveHours', async (_, chatroomId: string, startTime?: number, endTime?: number) => { return groupAnalyticsService.getGroupActiveHours(chatroomId, startTime, endTime) }) ipcMain.handle('groupAnalytics:getGroupMediaStats', async (_, chatroomId: string, startTime?: number, endTime?: number) => { return groupAnalyticsService.getGroupMediaStats(chatroomId, startTime, endTime) }) // 打开协议窗口 ipcMain.handle('window:openAgreementWindow', async () => { createAgreementWindow() return true }) // 完成引导,关闭引导窗口并显示主窗口 ipcMain.handle('window:completeOnboarding', async () => { try { configService?.set('onboardingDone', true) } catch (e) { console.error('保存引导完成状态失败:', e) } if (onboardingWindow && !onboardingWindow.isDestroyed()) { onboardingWindow.close() } showMainWindow() return true }) // 重新打开首次引导窗口,并隐藏主窗口 ipcMain.handle('window:openOnboardingWindow', async () => { shouldShowMain = false if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.hide() } createOnboardingWindow() return true }) // 年度报告相关 ipcMain.handle('annualReport:getAvailableYears', async () => { const cfg = configService || new ConfigService() configService = cfg return annualReportService.getAvailableYears({ dbPath: cfg.get('dbPath'), decryptKey: cfg.get('decryptKey'), wxid: cfg.get('myWxid') }) }) ipcMain.handle('annualReport:generateReport', async (_, year: number) => { const cfg = configService || new ConfigService() configService = cfg const dbPath = cfg.get('dbPath') const decryptKey = cfg.get('decryptKey') const wxid = cfg.get('myWxid') const logEnabled = cfg.get('logEnabled') const resourcesPath = app.isPackaged ? join(process.resourcesPath, 'resources') : join(app.getAppPath(), 'resources') const userDataPath = app.getPath('userData') const workerPath = join(__dirname, 'annualReportWorker.js') return await new Promise((resolve) => { const worker = new Worker(workerPath, { workerData: { year, dbPath, decryptKey, myWxid: wxid, resourcesPath, userDataPath, logEnabled } }) const cleanup = () => { worker.removeAllListeners() } worker.on('message', (msg: any) => { if (msg && msg.type === 'annualReport:progress') { for (const win of BrowserWindow.getAllWindows()) { if (!win.isDestroyed()) { win.webContents.send('annualReport:progress', msg.data) } } return } if (msg && (msg.type === 'annualReport:result' || msg.type === 'done')) { cleanup() void worker.terminate() resolve(msg.data ?? msg.result) return } if (msg && (msg.type === 'annualReport:error' || msg.type === 'error')) { cleanup() void worker.terminate() resolve({ success: false, error: msg.error || '年度报告生成失败' }) } }) worker.on('error', (err) => { cleanup() resolve({ success: false, error: String(err) }) }) worker.on('exit', (code) => { if (code !== 0) { cleanup() resolve({ success: false, error: `年度报告线程异常退出: ${code}` }) } }) }) }) ipcMain.handle('annualReport:exportImages', async (_, payload: { baseDir: string; folderName: string; images: Array<{ name: string; dataUrl: string }> }) => { try { const { baseDir, folderName, images } = payload if (!baseDir || !folderName || !Array.isArray(images) || images.length === 0) { return { success: false, error: '导出参数无效' } } let targetDir = join(baseDir, folderName) if (existsSync(targetDir)) { let idx = 2 while (existsSync(`${targetDir}_${idx}`)) idx++ targetDir = `${targetDir}_${idx}` } await mkdir(targetDir, { recursive: true }) for (const img of images) { const dataUrl = img.dataUrl || '' const commaIndex = dataUrl.indexOf(',') if (commaIndex <= 0) continue const base64 = dataUrl.slice(commaIndex + 1) const buffer = Buffer.from(base64, 'base64') const filePath = join(targetDir, img.name) await writeFile(filePath, buffer) } return { success: true, dir: targetDir } } catch (e) { return { success: false, error: String(e) } } }) // 密钥获取 ipcMain.handle('key:autoGetDbKey', async (event) => { return keyService.autoGetDbKey(60_000, (message, level) => { event.sender.send('key:dbKeyStatus', { message, level }) }) }) ipcMain.handle('key:autoGetImageKey', async (event, manualDir?: string) => { return keyService.autoGetImageKey(manualDir, (message) => { event.sender.send('key:imageKeyStatus', { message }) }) }) } // 主窗口引用 let mainWindow: BrowserWindow | null = null // 启动时自动检测更新 function checkForUpdatesOnStartup() { if (!AUTO_UPDATE_ENABLED) return // 开发环境不检测更新 if (process.env.VITE_DEV_SERVER_URL) return // 延迟3秒检测,等待窗口完全加载 setTimeout(async () => { try { const result = await autoUpdater.checkForUpdates() if (result && result.updateInfo) { const currentVersion = app.getVersion() const latestVersion = result.updateInfo.version if (latestVersion !== currentVersion && mainWindow) { // 通知渲染进程有新版本 mainWindow.webContents.send('app:updateAvailable', { version: latestVersion, releaseNotes: result.updateInfo.releaseNotes || '' }) } } } catch (error) { console.error('启动时检查更新失败:', error) } }, 3000) } app.whenReady().then(() => { configService = new ConfigService() const candidateResources = app.isPackaged ? join(process.resourcesPath, 'resources') : join(app.getAppPath(), 'resources') const fallbackResources = join(process.cwd(), 'resources') const resourcesPath = existsSync(candidateResources) ? candidateResources : fallbackResources const userDataPath = app.getPath('userData') wcdbService.setPaths(resourcesPath, userDataPath) wcdbService.setLogEnabled(configService.get('logEnabled') === true) registerIpcHandlers() const onboardingDone = configService.get('onboardingDone') shouldShowMain = onboardingDone === true mainWindow = createWindow({ autoShow: shouldShowMain }) if (!onboardingDone) { createOnboardingWindow() } // 启动时检测更新 checkForUpdatesOnStartup() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createWindow() } }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } })