const { app, BrowserWindow, Tray, Menu, Notification, ipcMain, shell, nativeImage } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const http = require('http'); const isWin = process.platform === 'win32'; const isSmoke = process.argv.includes('--smoke'); const minimized = process.argv.includes('--minimized'); let win = null; let tray = null; let coreProc = null; let corePort = 0; let coreToken = ''; let localJsonTimer = null; let quitting = false; const dataDir = path.join(app.getPath('userData'), 'data'); const localJsonPath = path.join(dataDir, 'local.json'); const coreBin = isWin ? 'agent-core.exe' : 'agent-core'; const coreExe = app.isPackaged ? path.join(process.resourcesPath, coreBin) : path.join(__dirname, '..', 'dist', coreBin); function startCore() { if (!fs.existsSync(coreExe)) { console.error('[core] binary not found: ' + coreExe); win?.webContents.send('core-error', '核心程序缺失:' + coreExe); return; } fs.mkdirSync(dataDir, { recursive: true }); try { coreProc?.kill(); } catch (e) { /* ignore */ } corePort = 0; coreToken = ''; try { fs.rmSync(localJsonPath, { force: true }); } catch (e) { /* ignore */ } coreProc = spawn(coreExe, ['-dir', dataDir], { windowsHide: true }); coreProc.on('error', (err) => { console.error('[core] process error: ' + err.message); win?.webContents.send('core-error', '核心程序启动失败:' + err.message); }); coreProc.on('exit', (code) => { coreProc = null; win?.webContents.send('core-exit', code); }); pollLocalJson(); } function pollLocalJson() { if (localJsonTimer) clearInterval(localJsonTimer); localJsonTimer = setInterval(() => { try { const info = JSON.parse(fs.readFileSync(localJsonPath, 'utf8')); if (info.port && info.token) { corePort = info.port; coreToken = info.token; clearInterval(localJsonTimer); win?.webContents.send('core-ready'); } } catch (e) { /* 核心尚未就绪 */ } }, 800); } function coreRequest(method, p, body) { return new Promise((resolve, reject) => { if (!corePort || !coreToken) { reject(new Error('核心未就绪')); return; } const data = body ? JSON.stringify(body) : null; const req = http.request({ host: '127.0.0.1', port: corePort, path: p, method: method, headers: { 'X-Agent-Token': coreToken, 'Content-Type': 'application/json', 'Content-Length': data ? Buffer.byteLength(data) : 0, }, }, (res) => { let raw = ''; res.on('data', (c) => { raw += c; }); res.on('end', () => { try { resolve(JSON.parse(raw)); } catch (e) { resolve({ raw }); } }); }); req.on('error', reject); req.setTimeout(8000, () => { req.destroy(new Error('核心请求超时')); }); if (data) req.write(data); req.end(); }); } // ---- IPC(渲染进程 → 核心/系统) ---- ipcMain.handle('core:status', () => coreRequest('GET', '/status')); ipcMain.handle('core:pair', (e, code, name) => coreRequest('POST', '/pair', { code, name })); ipcMain.handle('core:challenges', () => coreRequest('GET', '/challenges')); ipcMain.handle('core:solve', (e, id, value) => coreRequest('POST', '/challenges/' + encodeURIComponent(id), { value })); ipcMain.handle('core:get-config', () => readConfig()); ipcMain.handle('core:save-config', (e, cfg) => { writeConfig(cfg); startCore(); return true; }); ipcMain.handle('core:restart', () => { startCore(); return true; }); ipcMain.handle('app:open-data-dir', () => shell.openPath(dataDir)); ipcMain.handle('app:open-web', (e, url) => shell.openExternal(url)); ipcMain.handle('app:notify', (e, title, body) => { if (Notification.isSupported()) { new Notification({ title: String(title || 'EveryPublish'), body: String(body || '') }).show(); } return true; }); ipcMain.handle('app:set-autostart', (e, on) => { app.setLoginItemSettings({ openAtLogin: !!on, openAsHidden: true, args: ['--minimized'] }); return app.getLoginItemSettings().openAtLogin; }); ipcMain.handle('app:get-autostart', () => app.getLoginItemSettings().openAtLogin); ipcMain.handle('app:quit', () => { quitting = true; app.quit(); }); function readConfig() { const defaults = { server: 'http://127.0.0.1:8090', webUrl: 'http://127.0.0.1:3003', fakeAll: false, }; try { return { ...defaults, ...JSON.parse(fs.readFileSync(path.join(dataDir, 'config.json'), 'utf8')) }; } catch (e) { return defaults; } } function writeConfig(cfg) { fs.mkdirSync(dataDir, { recursive: true }); fs.writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify({ ...readConfig(), ...cfg }, null, 2), { mode: 0o600 }); } function createTray() { let img = nativeImage.createFromPath(path.join(__dirname, 'icons', 'app.png')); if (img.isEmpty()) img = nativeImage.createEmpty(); tray = new Tray(img); tray.setToolTip('EveryPublish 客户端'); tray.setContextMenu(Menu.buildFromTemplate([ { label: '打开主窗口', click: showWin }, { type: 'separator' }, { label: '退出', click: () => { quitting = true; app.quit(); } }, ])); tray.on('click', showWin); } function showWin() { if (win) { win.show(); win.focus(); } } function createWindow() { win = new BrowserWindow({ width: 980, height: 660, minWidth: 780, minHeight: 540, title: 'EveryPublish 客户端', autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(path.join(__dirname, 'renderer', 'index.html')); win.on('close', (e) => { if (!quitting) { e.preventDefault(); win.hide(); } }); } app.whenReady().then(() => { app.setAppUserModelId('com.everypublish.client'); if (isSmoke) { // 冒烟模式:启动 core → 等就绪 → 查状态 → 退出(无窗口) runSmoke(); return; } createTray(); createWindow(); if (minimized) win?.hide(); startCore(); }); async function runSmoke() { startCore(); const deadline = Date.now() + 30000; while (!coreToken && Date.now() < deadline) { await new Promise((r) => setTimeout(r, 500)); } if (!coreToken) { console.log('SMOKE_RESULT ' + JSON.stringify({ error: 'core 未就绪' })); app.exit(1); return; } try { const status = await coreRequest('GET', '/status'); console.log('SMOKE_RESULT ' + JSON.stringify(status)); app.exit(0); } catch (e) { console.log('SMOKE_RESULT ' + JSON.stringify({ error: String(e) })); app.exit(1); } } app.on('before-quit', () => { quitting = true; if (localJsonTimer) clearInterval(localJsonTimer); try { coreProc?.kill(); } catch (e) { /* ignore */ } }); app.on('window-all-closed', () => { /* 驻留托盘不退出 */ }); app.on('activate', showWin);