// main.js const { app, Tray, Menu, dialog, BrowserWindow, ipcMain, nativeTheme } = require('electron'); const { autoUpdater } = require('electron-updater'); const path = require('path'); const fs = require('fs'); const { spawn, spawnSync, exec } = require('child_process'); const log = require('electron-log'); Object.assign(console, log.functions); const ALLOW_UNSIGNED = process.env.YOLO_ALLOW_UNSIGNED === '1'; let installTriggered = false; // NEW: 防止重复触发安装 // ===== 全局状态 ===== let tray = null; let managedChild = null; let quitting = false; let updateWin = null; let pythonStarted = false; // NEW: 已启动 tkpage? let updaterKicked = false; // NEW: 是否已触发检查 // ===== tkpage 路径 ===== function resolveExePath() { const devExe = path.resolve(__dirname, 'resources', 'python', 'tkpage.exe'); const prodExe = path.join(process.resourcesPath, 'python', 'tkpage.exe'); return app.isPackaged ? prodExe : devExe; } // ===== 启动 tkpage(只会启动一次) ===== function launchPythonIfNeeded() { if (pythonStarted) return; const exe = resolveExePath(); const cwd = path.dirname(exe); console.info('[tkpage exe]', exe); if (!fs.existsSync(exe)) { dialog.showErrorBox('启动失败', `未找到可执行文件:\n${exe}`); return; } pythonStarted = true; managedChild = spawn(exe, [], { cwd, windowsHide: false, detached: false, stdio: 'ignore', }); managedChild.on('exit', (code, signal) => { console.info(`[tkpage] exit code=${code} signal=${signal}`); if (!quitting) { quitting = true; app.quit(); } }); managedChild.on('close', (code, signal) => { console.info(`[tkpage] close code=${code} signal=${signal}`); if (!quitting) { quitting = true; app.quit(); } }); } // ===== 结束 tkpage ===== function killPythonGUI(killSelf = true) { if (managedChild && managedChild.pid) { try { process.kill(managedChild.pid); } catch (e) { console.warn('[kill] managed kill error:', e?.message || e); } finally { managedChild = null; } } try { spawnSync('taskkill', ['/f', '/t', '/im', 'tkpage.exe'], { windowsHide: true }); } catch { } if (killSelf) { try { exec('taskkill /IM YOLO助手.exe /F'); } catch { } } } // ===== 托盘 ===== function createTray() { const iconPath = path.join(__dirname, 'assets', 'icon.ico'); tray = new Tray(iconPath); const menu = Menu.buildFromTemplate([ { label: '检查更新', click: () => startUpdateCheck(/*force*/true) }, { type: 'separator' }, { label: '显示更新窗口', click: () => { if (updateWin) { updateWin.show(); updateWin.focus(); } else { createUpdateWindow(); } } }, { type: 'separator' }, { label: '重启 Python GUI', click: () => { killPythonGUI(); pythonStarted = false; launchPythonIfNeeded(); } }, { type: 'separator' }, { label: '退出', click: () => { quitting = true; killPythonGUI(); app.quit(); } }, ]); tray.setToolTip('YOLO'); tray.setContextMenu(menu); } // ===== 更新窗口 ===== function createUpdateWindow() { if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); return; } updateWin = new BrowserWindow({ width: 640, height: 420, resizable: false, fullscreenable: false, maximizable: false, autoHideMenuBar: true, show: true, center: true, backgroundColor: nativeTheme.shouldUseDarkColors ? '#0b1220' : '#ffffff', icon: path.join(__dirname, 'assets', 'icon.ico'), webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, } }); updateWin.on('close', (e) => { e.preventDefault(); updateWin.hide(); }); updateWin.loadFile(path.join(__dirname, 'index.html')).catch(err => console.error('[updateWin] load error', err)); } // ===== Updater 事件桥接到渲染进程,并控制 tkpage 启动时机 ===== function wireUpdaterIpc() { if (ALLOW_UNSIGNED) { console.warn('[updater] verifyUpdateCodeSignature disabled (YOLO_ALLOW_UNSIGNED=1)'); autoUpdater.verifyUpdateCodeSignature = async () => null; } autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = false; const send = (ch, data) => { if (updateWin && !updateWin.isDestroyed()) updateWin.webContents.send(ch, data); }; autoUpdater.on('checking-for-update', () => { console.info('[updater] Checking'); tray?.setToolTip('YOLO - 正在检查更新…'); send('update:status', { status: 'checking' }); }); autoUpdater.on('update-available', (info) => { console.info('[updater] Available', info?.version); tray?.setToolTip(`YOLO - 检测到新版本 v${info?.version},开始下载…`); send('update:status', { status: 'available', version: info?.version, notes: info?.releaseNotes || '' }); // 此时不启动 tkpage,等待下载完成 }); autoUpdater.on('update-not-available', (info) => { console.info('[updater] None'); tray?.setToolTip('YOLO - 已是最新版本'); send('update:status', { status: 'none', currentVersion: app.getVersion(), info }); // ✅ 没有更新,立刻启动 tkpage launchPythonIfNeeded(); // 可隐藏更新窗口(让应用留在托盘+tkpage前台) setTimeout(() => updateWin?.hide(), 1200); }); autoUpdater.on('download-progress', (p) => { const percent = Math.max(0, Math.min(100, p?.percent || 0)); const transferred = Math.floor((p?.transferred || 0) / 1024 / 1024); const total = Math.floor((p?.total || 0) / 1024 / 1024); const speed = Math.floor((p?.bytesPerSecond || 0) / 1024 / 1024); tray?.setToolTip(`YOLO - 正在下载更新:${percent.toFixed(1)}%`); send('update:progress', { percent, transferred, total, speed }); }); autoUpdater.on('update-downloaded', (info) => { console.info('[updater] Downloaded', info?.version); tray?.setToolTip('YOLO - 更新已下载,等待安装…'); // 仅通知渲染进程显示“立即安装”按钮;不自动安装 send('update:status', { status: 'downloaded', version: info?.version }); // 可选择把更新窗口确保前置 if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); } }); autoUpdater.on('error', (err) => { const msg = err?.message || String(err); console.error('[updater] error', msg); tray?.setToolTip('YOLO - 更新失败'); dialog.showErrorBox('自动更新失败', msg); send('update:status', { status: 'error', message: msg }); // ⚠️ 出错:不阻断主流程,直接启动 tkpage launchPythonIfNeeded(); }); // 渲染侧手动触发 ipcMain.on('updater:check', () => startUpdateCheck(/*force*/true)); ipcMain.on('updater:install-now', () => { if (installTriggered) return; installTriggered = true; console.info('[updater] quitAndInstall requested by renderer'); tray?.setToolTip('YOLO - 正在安装更新…'); quitting = true; try { killPythonGUI(false); } catch (e) { console.warn('[updater] killPythonGUI error:', e?.message || e); } setTimeout(() => { try { autoUpdater.quitAndInstall(false, true); } catch (e) { installTriggered = false; console.error('[updater] quitAndInstall error:', e?.message || e); dialog.showErrorBox('安装失败', String(e?.message || e)); } }, 300); }); } // ===== 触发更新检查(仅触发一次,托盘点“检查更新”可强制再触发) ===== function startUpdateCheck(force = false) { if (updaterKicked && !force) return; updaterKicked = true; // 显示更新窗口(可选:你也可以只在有更新时显示) if (!updateWin || updateWin.isDestroyed()) createUpdateWindow(); else { updateWin.show(); updateWin.focus(); } autoUpdater.checkForUpdates().catch(() => { /* 错误会在 on('error') 里处理 */ }); } // ===== 单例 & 生命周期 ===== process.on('unhandledRejection', (r) => console.error('[unhandledRejection]', r)); process.on('uncaughtException', (e) => console.error('[uncaughtException]', e)); const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { app.quit(); } else { app.on('second-instance', () => { if (updateWin) { updateWin.show(); updateWin.focus(); } }); app.whenReady().then(() => { if (process.platform === 'win32') { app.setAppUserModelId('com.yolozs.newPage'); } createTray(); createUpdateWindow(); wireUpdaterIpc(); // ✅ 启动时:只检查更新,不启动 tkpage。无更新 → 启动;有更新 → 下载并重启安装。 startUpdateCheck(); }); app.on('before-quit', () => { quitting = true; killPythonGUI(true); }); app.on('window-all-closed', () => { /* 后台常驻 */ }); }