import './preload-env' import { app, shell, BrowserWindow, ipcMain, nativeImage, clipboard, Menu, Tray, dialog } from 'electron' import { join } from 'path' import { existsSync } from 'fs' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import { WechatDb } from './wechat-db' import { bootstrapWcdbNative } from './wcdb4-client' import { VoiceService } from './voice-service' import { StickerService } from './sticker-service' import { parseMessageContent } from './message-parser' import { ImageDecryptService } from './image-decrypt-service' import { exportGroupReport } from './group-report-service' import { deleteGeneratedReport, listGeneratedReports, saveGeneratedReport } from './report-history-service' import type { GroupReportExportRequest } from '../shared/group-report' import type { SaveGeneratedReportRequest } from '../shared/report-history' import type { AIChatRequestOptions, AIProviderConfig, AIVisionTestRequest, LegacyAIConfig } from '../shared/ai-provider' import { DatabaseKeyStore } from './database-key-store' import { ImageKeyConfigService } from './services/image-key-config-service' import { AIProviderService } from './services/ai-provider-service' import { imageInsightService } from './services/image-insight-service' import type { ImageAnalysisRequest, ImageAnalysisResponse, ImageCandidate, ImageCandidateQuery, ImageInsight } from '../shared/image-insight' import { KeyServiceMac } from './key-service-mac' import { KeyService as KeyServiceWin } from './key-service-win' import * as chat from './services/chat-service' import { apiServer } from './http-server' import { skillResourceService } from './services/skill-resource-service' import { testLocalApiRequest } from './services/local-api-test-service' import { isWindowsWechatRunning } from './services/wechat-process-status' import { inspectImageDecryptionStatus, testImageDecryption } from './services/image-decryption-status-service' import type { SaveImageKeyRequest, TestImageDecryptionRequest } from '../shared/image-decryption' import { loadSettings, saveSettings, getSettingsPath, AppSettings } from './services/settings-store' import { getBootstrapCache, getCachedMessages, mergeBootstrapAvatars, mergeCachedContactAvatars, saveBootstrapContacts, saveBootstrapSelf, saveCachedMessages } from './services/bootstrap-cache' import { installSafeConsole } from './safe-log' import { agentHubService } from './services/agent-hub-service' import { appLogger } from './app-logger' import type { AppLogEntry } from '../shared/app-log' // electron-vite can close the child's stdout/stderr after spawning Electron. // Plain console.error then throws EPIPE on a closed pipe and crashes the IPC // handler. Wrap console.* before any other module logs anything. installSafeConsole() let voiceService: VoiceService | null = null let imageDecryptService: ImageDecryptService | null = null let stickerService: StickerService | null = null const databaseKeyStore = new DatabaseKeyStore() const imageKeyConfigService = new ImageKeyConfigService() const aiProviderService = new AIProviderService() const keyServiceMac = new KeyServiceMac() const keyServiceWin = new KeyServiceWin() let tray: Tray | null = null const packagedIconPath = join(process.resourcesPath, 'resources', 'icon.png') const appIconPath = existsSync(packagedIconPath) ? packagedIconPath : icon // WCDB's Windows runtime checks the host application name during wcdb_init. // Mirroring WeFlow's name unblocks the -1006 init failure on Windows. app.setName(process.platform === 'win32' ? 'WeFlow' : 'WechatExplorer') let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null = null const BUILD_MARK = 'wechat4-local-http-api-2026-07-03' const TRAY_MODE = process.argv.includes('--tray') || (process.env['WXE_TRAY'] || '').toString() === '1' function getConfiguredImageKeys(): { xorKey: string; aesKey: string } { const config = imageKeyConfigService.getConfig() return { xorKey: config.xorKey || '0x40', aesKey: config.aesKey || '' } } function createWindow(): void { // 鍒涘缓娴忚鍣ㄧ獥鍙? const mainWindow = new BrowserWindow({ width: 1400, height: 800, show: false, autoHideMenuBar: true, icon: appIconPath, webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false } }) mainWindow.on('ready-to-show', () => { mainWindow.show() }) mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) // 鍩轰簬 electron-vite cli 鐨勬覆鏌撳櫒 HMR // 鍔犺浇寮€鍙戠幆澧冪殑杩滅▼ URL 鎴栫敓浜х幆澧冪殑鏈湴 html 鏂囦欢 if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } } // 褰?Electron 瀹屾垚鍒濆鍖栧苟鍑嗗濂藉垱寤烘祻瑙堝櫒绐楀彛鏃讹紝灏嗚皟鐢ㄦ鏂规硶 // 鏌愪簺 API 鍙兘鍦ㄦ浜嬩欢鍙戠敓鍚庝娇鐢? app.whenReady().then(async () => { console.log(`WechatExplorer main build: ${BUILD_MARK}`) appLogger.write({ level: 'info', scope: 'lifecycle', message: 'WechatExplorer 启动', details: { build: BUILD_MARK, platform: process.platform, version: app.getVersion() } }) process.on('uncaughtException', (error) => { appLogger.write({ level: 'error', scope: 'main-process', message: error.message, details: { stack: error.stack } }) }) process.on('unhandledRejection', (reason) => { appLogger.write({ level: 'error', scope: 'main-process', message: reason instanceof Error ? reason.message : 'Promise 未处理拒绝', details: { stack: reason instanceof Error ? reason.stack : undefined, reason: reason instanceof Error ? undefined : String(reason) } }) }) // WCDB's Windows runtime returns -1006 if wcdb_init is called more than once // per process. Bootstrap native once here so any later Wcdb4Client instance // reuses the already-initialized library and skips wcdb_init. try { bootstrapWcdbNative() console.log('[WCDB4] bootstrap complete at whenReady top') } catch (bootstrapError) { console.error('[WCDB4] bootstrap failed at whenReady top:', bootstrapError) } // 涓虹獥鍙h缃簲鐢ㄧ▼搴忕敤鎴锋ā鍨?ID electronApp.setAppUserModelId('com.wechatexplorer.app') if (process.platform === 'darwin') app.dock?.setIcon(appIconPath) // 鍦ㄥ紑鍙戠幆澧冧腑榛樿鎸?F12 鎵撳紑鎴栧叧闂?DevTools // 鍦ㄧ敓浜х幆澧冧腑蹇界暐 CommandOrControl + R // 鍙傝 https://github.com/alex8088/electron-toolkit/tree/master/packages/utils app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window) }) // IPC test ipcMain.on('ping', () => console.log('pong')) 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('db:init', async (_, key: string) => { if (dbInitInFlight) return dbInitInFlight dbInitInFlight = (async () => { try { const trimmedKey = String(key || '').trim() console.log(`db:init build=${BUILD_MARK} keyLength=${trimmedKey.length}`) const settings = loadSettings() if ( chat.isReady() && chat.getCurrentKey().replace(/^0x/i, '').trim() === trimmedKey.replace(/^0x/i, '') && (!settings.dbRoot || chat.getCurrentAccountRoot() === settings.dbRoot) ) { console.log('[WCDB4] db:init reuse current connection') return { success: true, monitoring: true } } const nextWechatDb = await WechatDb.create(key, settings.dbRoot) const resolvedRoot = nextWechatDb.getWcdb4Client().getAccountRoot() if (resolvedRoot && resolvedRoot !== settings.dbRoot) { saveSettings({ ...settings, dbRoot: resolvedRoot }) } chat.setChatDb(nextWechatDb) const wcdb4Client = nextWechatDb.getWcdb4Client() voiceService = new VoiceService(wcdb4Client) stickerService = new StickerService(wcdb4Client) const monitoring = wcdb4Client.startMonitor((type, json) => { for (const window of BrowserWindow.getAllWindows()) { if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json }) } }) imageDecryptService = null return { success: true, monitoring } } catch (error) { console.error('Failed to init DB:', error) return { success: false, error: error instanceof Error ? error.message : String(error) } } finally { dbInitInFlight = null } })() return dbInitInFlight }) ipcMain.handle('key:getSavedDbKey', async () => databaseKeyStore.load()) ipcMain.handle('key:getEnvironment', async () => { const storage = await databaseKeyStore.getStatus() const self = chat.getSelfAccountInfo() return { platform: process.platform, autoDetectSupported: process.platform === 'win32', wechatRunning: await isWindowsWechatRunning(), accountIdentified: Boolean(self?.wxid), dbConnected: chat.isReady(), encryptionAvailable: storage.encryptionAvailable } }) ipcMain.handle('key:readClipboardDbKey', () => { try { return { success: true, value: clipboard.readText().trim() } } catch { return { success: false, error: '无法读取剪贴板' } } }) ipcMain.handle('key:pasteAndSaveDbKey', async () => { const clipboardKey = clipboard.readText().trim() return databaseKeyStore.save(clipboardKey) }) ipcMain.handle('key:saveDbKey', async (_, key: string) => databaseKeyStore.save(String(key || '')) ) ipcMain.handle('key:clearSavedDbKey', async () => databaseKeyStore.clear()) ipcMain.handle('key:autoGetDbKey', async (event, options?: { save?: boolean }) => { const onStatus = (message: string): void => { if (!event.sender.isDestroyed()) event.sender.send('key:dbKeyStatus', { message }) } const result = process.platform === 'win32' ? await keyServiceWin.autoGetDbKey(60_000, onStatus) : await keyServiceMac.autoGetDbKey(onStatus) if (!result.success || !result.key) return result if (options?.save === false) return result const saved = await databaseKeyStore.save(result.key) return { ...result, saved: saved.success, warning: saved.success ? undefined : saved.error } }) ipcMain.handle('key:autoGetImageKey', async (event, options?: { save?: boolean }) => { const settings = loadSettings() const self = chat.getSelfAccountInfo() const accountRoot = settings.imageKeyRoot || self?.accountRoot || settings.dbRoot const wxid = self?.wxid const onStatus = (message: string): void => { if (!event.sender.isDestroyed()) event.sender.send('key:imageKeyStatus', { message }) } const result = process.platform === 'win32' ? await keyServiceWin.autoGetImageKeyByMemoryScan(accountRoot, onStatus) : await keyServiceMac.autoGetImageKey(accountRoot, onStatus, wxid) if (!result.success || !result.aesKey) return result if (process.platform === 'win32') { onStatus('发现候选密钥,图片模板验证通过') } const imageXorKey = `0x${Number(result.xorKey ?? 0x40) .toString(16) .toUpperCase() .padStart(2, '0')}` const verified = result.verified ?? process.platform === 'win32' if (options?.save === false) { return { ...result, verified, imageXorKey, imageAesKey: result.aesKey } } const saved = imageKeyConfigService.save({ resourceRoot: accountRoot, xorKey: imageXorKey, aesKey: result.aesKey }) if (saved.success) imageDecryptService = null return { ...result, success: saved.success, error: saved.success ? undefined : saved.error, verified, imageXorKey, imageAesKey: result.aesKey, settings: imageKeyConfigService.getLegacySettingsView() } }) ipcMain.handle('image:getConfig', () => imageKeyConfigService.getConfig()) ipcMain.handle('image:getStatus', async () => inspectImageDecryptionStatus(imageKeyConfigService.getConfig()) ) ipcMain.handle('image:saveConfig', (_, request: SaveImageKeyRequest) => { const result = imageKeyConfigService.save(request) if (result.success) imageDecryptService = null return result }) ipcMain.handle('image:testConfig', (_, request: TestImageDecryptionRequest) => testImageDecryption(request) ) ipcMain.handle('image:clearConfig', () => { const result = imageKeyConfigService.clear() if (result.success) imageDecryptService = null return result }) ipcMain.handle('db:getBootstrapCache', () => { if (!chat.isReady()) return null return getBootstrapCache(chat.getCurrentAccountRoot()) }) ipcMain.handle( 'db:getCachedMessages', (_, userMd5: string, startTime?: number, endTime?: number) => { if (!chat.isReady()) return [] return getCachedMessages(chat.getCurrentAccountRoot(), userMd5, startTime, endTime) } ) ipcMain.handle('db:getContacts', (_, filter?: string) => { const accountRoot = chat.getCurrentAccountRoot() const contacts = accountRoot ? mergeCachedContactAvatars(accountRoot, chat.listContacts(filter)) : chat.listContacts(filter) if (!filter && chat.isReady() && accountRoot) { saveBootstrapContacts(accountRoot, contacts) } return contacts }) ipcMain.handle('db:getContactAvatars', (_, usernames: string[]) => { const avatars = chat.getContactAvatars(usernames) if (chat.isReady()) mergeBootstrapAvatars(chat.getCurrentAccountRoot(), avatars) return avatars }) ipcMain.handle( 'db:getMessages', (_, userMd5: string, startTime?: number, endTime?: number, options?: { limit?: number }) => { const messages = chat.listMessages(userMd5, startTime, endTime, options) if (chat.isReady()) { saveCachedMessages(chat.getCurrentAccountRoot(), userMd5, startTime, endTime, messages) } return messages } ) ipcMain.handle('db:getGroupSnapshot', (_, userMd5: string) => chat.getGroupSnapshot(userMd5)) ipcMain.handle('db:search', (_, keyword: string) => chat.searchMessages(keyword)) ipcMain.handle( 'ai:chat', async (_, messages: { role: string; content: string }[], options?: AIChatRequestOptions) => aiProviderService.chat(messages, options) ) ipcMain.handle('ai:listProviders', () => aiProviderService.list()) ipcMain.handle('ai:getRuntimeConfig', () => aiProviderService.getRuntimeConfig()) ipcMain.handle('ai:saveProvider', (_, provider: AIProviderConfig) => aiProviderService.save(provider) ) ipcMain.handle('ai:deleteProvider', (_, providerId: string) => aiProviderService.delete(providerId) ) ipcMain.handle('ai:setDefaultProvider', (_, providerId: string) => aiProviderService.setDefault(providerId) ) ipcMain.handle('ai:testProvider', (_, providerId: string) => aiProviderService.test(providerId)) ipcMain.handle('ai:testVision', (_, request: AIVisionTestRequest) => aiProviderService.testVision(request) ) ipcMain.handle('ai:migrateLegacy', (_, config: LegacyAIConfig) => aiProviderService.migrateLegacy(config) ) ipcMain.handle('copy-image', async (_, base64String) => { try { const image = nativeImage.createFromDataURL(base64String) clipboard.writeImage(image) return { success: true } } catch (error: unknown) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } } }) ipcMain.handle('report:export', async (_, request: GroupReportExportRequest) => { return exportGroupReport(request) }) ipcMain.handle('report:listGenerated', async () => { return listGeneratedReports() }) ipcMain.handle('report:saveGenerated', async (_, request: SaveGeneratedReportRequest) => { return saveGeneratedReport(request) }) ipcMain.handle('report:deleteGenerated', async (_, reportId: string) => { return deleteGeneratedReport(reportId) }) ipcMain.handle('report:reveal', async (_, filePath: string) => { try { shell.showItemInFolder(filePath) return { success: true } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } } }) ipcMain.handle( 'db:getVoiceData', async (_, sessionId: string, localId: number, createTime: number, svrId?: string | number) => { if (!voiceService) { return { success: false, error: 'VoiceService 未初始化' } } return voiceService.resolveVoice(sessionId, localId, createTime, svrId) } ) ipcMain.handle('db:parseMessage', async (_, content: string, messageType: number) => { return parseMessageContent(content, messageType) }) ipcMain.handle( 'db:getImage', async ( _, imageMd5?: string, imageDatNameOrThumb?: string | boolean, _sessionId?: string, options?: { force?: boolean } ) => { void _sessionId if (!imageDecryptService) { const { xorKey, aesKey } = getConfiguredImageKeys() if (!aesKey) { return { success: false, error: '未配置图片解密密钥' } } imageDecryptService = new ImageDecryptService( xorKey, aesKey, chat.getChatDb()?.getWcdb4Client() ) } const imageDatName = typeof imageDatNameOrThumb === 'string' ? imageDatNameOrThumb : undefined const force = options?.force === true let filePath = force ? imageDecryptService.findImageFile(imageMd5, imageDatName, { allowThumbnail: false }) : null if (!filePath) { filePath = imageDecryptService.findImageFile(imageMd5, imageDatName, { allowThumbnail: true }) } if (!filePath) { return { success: false, error: force ? '未找到原图或缩略图文件' : '未找到图片文件' } } const decrypted = imageDecryptService.decryptImageToBase64WithFallback(filePath, true) if (!decrypted) { return { success: false, error: '图片解密失败' } } return { success: true, data: decrypted.data, isThumb: imageDecryptService.isThumbnailFile(decrypted.filePath), filePath: decrypted.filePath } } ) // ============================================================ // AI 图片理解基础设施(ImageInsightService) // ============================================================ // 注入依赖(用闭包捕获当前 db:getImage 已经初始化过的 imageDecryptService) // 同时把 imageDecryptService 暴露到 globalThis,供 group-report-service 渲染时按 imageHash 取图 ;(globalThis as { __imageDecrypt?: typeof imageDecryptService }).__imageDecrypt = imageDecryptService imageInsightService.bind({ providerService: aiProviderService, decryptService: { findImageFile: (md5, datName, opts) => imageDecryptService?.findImageFile(md5, datName, opts) ?? null, decryptImageToBase64: (filePath) => imageDecryptService?.decryptImageToBase64(filePath) ?? null } }) /** 日报入口:取会话 Top N 热点图片 + 已缓存的 Insight */ ipcMain.handle( 'image:listCandidates', async ( _, query: ImageCandidateQuery ): Promise<{ success: boolean; candidates: ImageCandidate[]; error?: string }> => { console.log('[IPC] image:listCandidates query=%j', query) try { const inputs = (query as ImageCandidateQuery & { inputs?: unknown[] }).inputs || [] console.log('[IPC] image:listCandidates received %d inputs', inputs.length) const candidates = await imageInsightService.listTopHotImages(query, inputs as never) console.log('[IPC] image:listCandidates returned %d candidates', candidates.length) return { success: true, candidates } } catch (error) { console.warn('[IPC] image:listCandidates failed:', error) return { success: false, candidates: [], error: error instanceof Error ? error.message : String(error) } } } ) /** 单图分析:缓存命中即返回,未命中调 AI;失败不抛 */ ipcMain.handle( 'image:analyze', async (_, request: ImageAnalysisRequest): Promise => { console.log('[IPC] image:analyze hash=%s messageId=%s', request.imageHash, request.messageId) // 校验 provider 是否支持 vision const runtime = aiProviderService.getRuntimeConfig() if (!runtime.configured) { return { success: false, error: '尚未配置 AI Provider' } } const list = aiProviderService.list() const provider = list.providers.find((p) => p.id === runtime.providerId) const model = provider?.models.find((m) => m.id === runtime.model) if (!provider || !model) { return { success: false, error: '当前 AI 模型不存在' } } // Capability metadata is stored per machine. A model verified on macOS // may still be unmarked on Windows, so do not reject before making the // real multimodal request. The provider response remains authoritative. // request 来自 renderer,imageHash 是 md5(优先)或 sha256(...),dataUrl 在内部算出 // 这里直接调 service,dataUrl 由 renderer 通过 window.api.getImage 拿到再传进来 return imageInsightService.analyze(request) } ) /** 单图查询缓存 */ ipcMain.handle( 'image:getInsight', async (_, imageHash: string): Promise<{ success: boolean; insight?: ImageInsight }> => { const insight = imageInsightService.getInsight(imageHash) return { success: true, insight: insight || undefined } } ) /** 列出某会话所有已分析的 insights */ ipcMain.handle( 'image:listInsights', async ( _, sessionId: string, limit?: number ): Promise<{ success: boolean; insights: ImageInsight[] }> => { return { success: true, insights: imageInsightService.listBySession(sessionId, limit) } } ) ipcMain.handle('db:getSticker', async (_, cdnUrl?: string, md5?: string) => { if (!stickerService) { stickerService = new StickerService(chat.getChatDb()?.getWcdb4Client()) } return stickerService.resolveSticker(cdnUrl, md5) }) // -------- Settings & API service -------- ipcMain.handle('settings:get', () => ({ settings: imageKeyConfigService.getLegacySettingsView(), settingsPath: getSettingsPath() })) ipcMain.handle('settings:set', (_, patch: Partial) => { const before = loadSettings() const current = imageKeyConfigService.getConfig() const resourceRoot = patch.imageKeyRoot ?? before.imageKeyRoot const xorKey = patch.imageXorKey ?? current.xorKey ?? '0x40' const aesKey = patch.imageAesKey ?? current.aesKey ?? '' const includesImageKey = 'imageXorKey' in patch || 'imageAesKey' in patch if (includesImageKey) { saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' }) if (aesKey) imageKeyConfigService.save({ resourceRoot, xorKey, aesKey }) else imageKeyConfigService.clear() imageDecryptService = null } else { saveSettings({ ...before, ...patch, imageXorKey: '', imageAesKey: '' }) } return { settings: imageKeyConfigService.getLegacySettingsView(), settingsPath: getSettingsPath() } }) ipcMain.handle('settings:getSelf', () => { const info = chat.getSelfAccountInfo() if (!info) return { ready: false } if (chat.isReady()) saveBootstrapSelf(chat.getCurrentAccountRoot(), info) return { ready: true, info } }) ipcMain.handle('db:testConnection', (_, key: string, accountRoot?: string) => { return chat.testConnection(key, accountRoot) }) ipcMain.handle('db:reopenWithRoot', (_, accountRoot: string) => { const ok = chat.reopenWithRoot(accountRoot) if (!ok) return { success: false, error: '数据库未初始化或重新打开失败' } const info = chat.getSelfAccountInfo() return { success: true, info } }) ipcMain.handle('settings:selectDbRoot', async (event) => { const result = await dialog.showOpenDialog(BrowserWindow.fromWebContents(event.sender)!, { title: '选择微信数据库目录', defaultPath: loadSettings().dbRoot || undefined, properties: ['openDirectory'] }) return result.canceled ? { canceled: true } : { canceled: false, path: result.filePaths[0] } }) ipcMain.handle('settings:openAccountRoot', async () => { const accountRoot = chat.getCurrentAccountRoot() if (!accountRoot) return { success: false, error: '当前没有可打开的账号目录' } const error = await shell.openPath(accountRoot) return error ? { success: false, error } : { success: true } }) ipcMain.handle('db:disconnect', (_, options?: { closeNative?: boolean }) => { // 断开操作保持幂等:渲染进程可能已标记断开,或主进程连接已先行失效。 // 即使当前未就绪,也应让用户正常返回登录页。 if (options?.closeNative !== false && chat.isReady()) chat.setChatDb(null) return { success: true } }) ipcMain.handle('api:getStatus', () => apiServer.getState()) ipcMain.handle('api:start', async (_, host?: string, port?: number) => { const settings = loadSettings() const target = { host: host || settings.apiHost, port: port || settings.apiPort } if (host || port) saveSettings({ ...settings, ...target }) return apiServer.start(target.host, target.port) }) ipcMain.handle('api:stop', async () => apiServer.stop()) ipcMain.handle('api:toggle', async (_, enabled: boolean) => { const settings = saveSettings({ ...loadSettings(), apiEnabled: enabled }) if (enabled) { return apiServer.start(settings.apiHost, settings.apiPort) } return apiServer.stop() }) ipcMain.handle('api:skillStatus', () => skillResourceService.getStatus()) ipcMain.handle('api:readSkill', () => skillResourceService.read()) ipcMain.handle('api:revealSkill', () => skillResourceService.reveal()) ipcMain.handle('api:openSkillGithub', () => skillResourceService.openGithub()) ipcMain.handle('api:testLocalRequest', (_, request) => testLocalApiRequest(request)) ipcMain.handle('api:copyText', (_, text: unknown) => { if (typeof text !== 'string' || text.length > 1024 * 1024) { return { success: false, error: '复制内容无效或过大' } } try { clipboard.writeText(text) return { success: true } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } } }) ipcMain.handle('agent-hub:getStatus', () => agentHubService.getStatus()) ipcMain.handle('agent-hub:getLogs', () => agentHubService.getLogs()) ipcMain.handle('agent-hub:clearLogs', () => agentHubService.clearLogs()) ipcMain.handle('agent-hub:startLogin', () => agentHubService.startLogin()) ipcMain.handle('agent-hub:cancelLogin', () => agentHubService.cancelLogin()) ipcMain.handle('agent-hub:reconnect', () => agentHubService.reconnect()) ipcMain.handle('agent-hub:disconnect', () => agentHubService.disconnect()) ipcMain.handle('agent-hub:selectTestImage', async (event) => { const window = BrowserWindow.fromWebContents(event.sender) const result = await dialog.showOpenDialog(window!, { title: '选择要测试发送的图片', properties: ['openFile'], filters: [ { name: '图片', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'] }, { name: '所有文件', extensions: ['*'] } ] }) return result.canceled ? { canceled: true } : { canceled: false, path: result.filePaths[0] } }) createWindow() // 鍚姩鏈湴 HTTP API(鏍规嵁 settings.apiEnabled 鎺у埗) const settings = loadSettings() if (settings.apiEnabled) { await apiServer.start(settings.apiHost, settings.apiPort) } await agentHubService.start(settings) if (TRAY_MODE) { app.dock?.hide() setupTray() } app.on('activate', function () { // 鍦?macOS 涓婏紝褰撶偣鍑?dock 鍥炬爣涓旀病鏈夊叾浠栫獥鍙f墦寮€鏃讹紝 // 閫氬父浼氬湪搴旂敤绋嬪簭涓噸鏂板垱寤轰竴涓獥鍙c€? if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) // 褰撴墍鏈夌獥鍙e叧闂椂閫€鍑猴紝闄や簡 macOS銆傚湪閭i噷锛? // 搴旂敤绋嬪簭鍙婂叾鑿滃崟鏍忛€氬父浼氫繚鎸佹椿鍔ㄧ姸鎬侊紝鐩村埌鐢ㄦ埛 // 鏄惧紡浣跨敤 Cmd + Q 閫€鍑恒€? app.on('window-all-closed', () => { if (TRAY_MODE) return if (process.platform !== 'darwin') { app.quit() } }) app.on('before-quit', async () => { agentHubService.stop() chat.setChatDb(null) await apiServer.stop().catch(() => undefined) if (tray) { tray.destroy() tray = null } }) function showMainWindow(): void { if (TRAY_MODE) app.dock?.show().catch(() => undefined) const wins = BrowserWindow.getAllWindows() if (wins.length === 0) { createWindow() return } const win = wins[0] if (win.isMinimized()) win.restore() win.show() win.focus() } function buildTrayMenu(): Menu { return Menu.buildFromTemplate([ { label: '打开主窗口', click: () => showMainWindow() }, { label: 'API 状态', click: () => showMainWindow() }, { type: 'separator' }, { label: '退出 WechatExplorer', click: () => { tray?.destroy() tray = null app.quit() } } ]) } function setupTray(): void { if (tray) return try { const image = nativeImage.createFromPath(appIconPath) const traySize = process.platform === 'darwin' ? 20 : 24 const trayImage = image.isEmpty() ? nativeImage.createEmpty() : image.resize({ width: traySize, height: traySize, quality: 'best' }) tray = new Tray(trayImage) tray.setToolTip('WechatExplorer') tray.setContextMenu(buildTrayMenu()) tray.on('click', () => showMainWindow()) } catch (error) { console.warn('[Tray] Failed to create tray:', error) } }