const { app, globalShortcut, BrowserWindow, net, dialog, ipcMain } = require('electron'); const { startSSE } = require('./js/sse-server'); const { createBurstBroadcaster } = require('./js/burst-broadcast'); const mq = require('./js/rabbitmq-service'); const axios = require('axios'); const os = require('os'); const fsp = require('node:fs/promises') const { fileURLToPath } = require('node:url') const path = require('path'); const USER_ROOT = path.resolve(os.homedir()); const { autoUpdater } = require('electron-updater') const fs = require('fs') const log = require('electron-log') Object.assign(console, log.functions) // 存储 SSE 服务器的引用 let sseServer = null; // ✅ 本地 HTTPS 白名单(按你的实际地址改端口) const LOCAL_HTTPS_WHITELIST = [ 'https://192.168.1.218:34567', 'https://192.168.1.231:34567', 'https://127.0.0.1:34567', 'https://localhost:34567' ] let userData = { tenantId: null, userId: null } const { exec, spawn, execFile } = require('child_process'); // 如果你用 exec 启动 scrcpy,保留此行 // app.commandLine.appendSwitch('remote-debugging-port', '9222'); //远程控制台端口F12 app.commandLine.appendSwitch('enable-precise-memory-info') app.commandLine.appendSwitch('js-flags', '--expose-gc --max-old-space-size=4096') // 以 MB 为单位,建议设置 2048-4096 app.commandLine.appendSwitch('enable-experimental-web-platform-features'); app.commandLine.appendSwitch('enable-features', 'WebCodecs,MediaStreamInsertableStreams'); // === 更新门闩:更新进行时,禁止跳转业务页面 === let updateInProgress = false; // 有更新或正在下载 -> true let updateDownloaded = false; // 已下载完成(通常会随即重启安装) let pendingNavs = []; // 被延迟的跳转请求(函数队列) let installingNow = false; // 正在静默安装(用户点击“立即重启安装”之后) // --- 持久化兼容模式开关(仅影响本机) --- const compatFile = path.join(app.getPath('userData'), 'compat.json'); function readCompat() { try { return JSON.parse(fs.readFileSync(compatFile, 'utf8')); } catch { return {}; } } function writeCompat(obj) { try { fs.writeFileSync(compatFile, JSON.stringify(obj, null, 2)); } catch { } } // 支持三种触发:环境变量、命令行、持久化标记 const SAFE_MODE = process.env.YOLO_SAFE_MODE === '1' || process.argv.includes('--safe-mode') || readCompat().safeMode === true; // (可选)允许命令行清除持久化标记:--clear-safe-mode if (process.argv.includes('--clear-safe-mode')) { const c = readCompat(); delete c.safeMode; writeCompat(c); console.log('[兼容性] 已清除安全模式标志'); } // === 兼容模式等价于 bat 里的那些开关(不含日志) === if (SAFE_MODE) { app.disableHardwareAcceleration(); // --disable-gpu app.commandLine.appendSwitch('disable-gpu'); app.commandLine.appendSwitch('in-process-gpu'); // --in-process-gpu app.commandLine.appendSwitch('disable-direct-composition'); // --disable-direct-composition // 如需更稳再关沙箱:默认开着更安全;若你确实要关,则解除注释 // app.commandLine.appendSwitch('no-sandbox'); // --no-sandbox console.log('[安全模式] 已启用'); } function flushPendingNavs() { const fns = pendingNavs.slice(); pendingNavs = []; for (const fn of fns) { try { fn(); } catch (e) { console.warn('[导航] 延迟运行错误:', e); } } } function safeLoadURL(win, url, onFail) { if (!win || win.isDestroyed()) return; win.loadURL(url).catch(err => { console.warn('[加载url 失败]', err); onFail?.(err); }); } // 进程异常处理 process.on('uncaughtException', (error) => { console.error('uncaughtException未捕获异常:', error); }); process.on('unhandledRejection', (reason, p) => { console.error('[unhandledRejection未捕获异常]', reason); }); // 渲染崩溃 → 持久化 safeMode=true(下次自动以兼容模式启动) app.on('web-contents-created', (_, contents) => { contents.on('render-process-gone', (_event, details) => { console.error('⚠️ 渲染崩溃:', details); if (['launch-failed', 'abnorm al-exit'].includes(details.reason)) { const c = readCompat(); c.safeMode = true; writeCompat(c); console.warn('[兼容性] 已设置安全模式为true(已持久化),下次启动时将自动使用安全模式'); } }); }); //(可选)更广义的子进程异常也触发降级 app.on('child-process-gone', (_e, details) => { if ((details.type === 'GPU' || details.type === 'Renderer') && ['launch-failed', 'abnormal-exit'].includes(details.reason)) { const c = readCompat(); c.safeMode = true; writeCompat(c); console.warn('[兼容性] 已设置安全模式为true(已持久化),下次启动时将自动使用安全模式'); } }); //本地https 证书跳过校验 app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { // console.warn('[certificate-error]', url, error) // 只放行你自己的本地 HTTPS if (LOCAL_HTTPS_WHITELIST.some(prefix => url.startsWith(prefix))) { event.preventDefault() // 告诉 Electron:我来处理这个错误 callback(true) // ✅ 忽略证书错误,照样加载 } else { // 其他地址还是正常校验证书,防止误放行 callback(false) } }) app.on('ready', () => { console.log('[App Ready] 当前版本:', app.getVersion()); // 配置日志记录(可选) const log = require('electron-log'); autoUpdater.logger = log; autoUpdater.logger.transports.file.level = 'info'; // 启用静默更新(关键设置) autoUpdater.autoDownload = true; // 自动下载更新 autoUpdater.autoInstallOnAppQuit = false; // 我们手动调用安装(静默) // 监听事件(用于控制流程或调试,可保留日志) autoUpdater.on('checking-for-update', () => { console.log('[autoUpdater] 正在检查更新...'); }); autoUpdater.on('update-available', async (info) => { updateInProgress = true; // ⛔ 开始阻塞导航 console.log(`[autoUpdater] 检测到新版本: ${info.version},开始自动下载...`); await killIOSAI() broadcast('update:available', info); }); let lastSend = 0; autoUpdater.on('update-not-available', () => { console.log('[autoUpdater] 当前已是最新版本'); updateInProgress = false; // ✅ 解锁 flushPendingNavs(); // 立刻执行被延迟的导航 }); autoUpdater.on('download-progress', (p) => { updateInProgress = true; // ⛔ 持续阻塞 // 节流 150ms,避免刷屏 const now = Date.now(); if (now - lastSend > 150) { lastSend = now; broadcast('update:progress', { percent: p.percent, // 0~100 bytesPerSecond: p.bytesPerSecond, // B/s transferred: p.transferred, // 已下载字节 total: p.total // 总字节 }); } // 维持你原来的任务栏进度 const win = BrowserWindow.getAllWindows()[0]; if (win) win.setProgressBar(p.percent / 100); }); autoUpdater.on('update-downloaded', (info) => { updateInProgress = true; // 已下载,通常马上安装并退出 updateDownloaded = true; console.log('[autoUpdater] 更新下载完成'); broadcast('update:downloaded', info); // 如果想“用户点按钮再安装”,注释掉下一行,交给渲染进程触发 // autoUpdater.quitAndInstall(false, true); }); autoUpdater.on('error', (err) => { console.error('[autoUpdater] 错误:', err); broadcast('update:error', { message: String(err) }); updateInProgress = false; // ✅ 出错也解锁(按需可保持阻塞) flushPendingNavs(); }); // 可选:提供渲染端调用的安装入口 // ✅ 渲染端主动触发静默安装(不显示 UI) ipcMain.handle('update:quitAndInstallNow', async () => { try { installingNow = true; // 进入“静默安装中”阶段 // 安装前做一次业务清理:但避免误杀自身或阻塞 await killIOSAI(); } catch (e) { console.warn('[更新] 预安装清理警告:', e); } // electron-updater 实际上传给 NSIS /S 静默参数,第二个 true 表示强制重启后运行新版本 autoUpdater.quitAndInstall(false, true); }); ipcMain.handle('update:checkNow', () => { return autoUpdater.checkForUpdates(); }); // 启动检查更新(不弹窗) autoUpdater.checkForUpdates(); }); // 应用程序生命周期管理 app.whenReady().then(() => { setInterval(dumpAllMem, 5000); //循环查看内存情况 // 再创建窗口 createWindow(); // macOS 特有:激活时若无打开窗口再创建 app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); // globalShortcut.register('F12', () => toggleDevtools(BrowserWindow.getFocusedWindow())); globalShortcut.register('Control+Shift+i', () => toggleDevtools(BrowserWindow.getFocusedWindow())); // globalShortcut.register('Control+Alt+m', () => toggleDevtools(BrowserWindow.getFocusedWindow())); }); app.on('will-quit', () => globalShortcut.unregisterAll()); // 仅在“不是更新安装中”时,做你的退出清理 app.on('before-quit', async () => { if (!installingNow) { await killIOSAI(); } else { console.log('[退出前] 跳过 killIOSAI 因为 installingNow = true'); } }); function toggleDevtools(win) { if (!win) return; const wc = win.webContents; if (wc.isDevToolsOpened()) wc.closeDevTools(); else wc.openDevTools({ mode: 'detach' }); // 独立窗口 } // 窗口全部关闭时,退出或保留后台进程 app.on('window-all-closed', async () => { await killIOSAI(); if (process.platform !== 'darwin') { app.quit(); } }); // 再加几道保险(进程异常/信号也关) process.on('exit', killIOSAI); process.on('SIGINT', killIOSAI); process.on('SIGTERM', killIOSAI); function toMB(v) { if (!v || v <= 0) return 0; // 某些平台字段是 bytes、也有历史 Electron 返回 KB 的情况,做兜底 // 这里按优先:bytes->MB,其次 KB->MB,最后假定已经是MB // const bytesToMB = v / (1024 * 1024); // if (bytesToMB > 1) return Number(bytesToMB.toFixed(1)); // 看起来像 bytes const kbToMB = v / 1024; if (kbToMB > 1) return Number(kbToMB.toFixed(1)); // 看起来像 KB return Number(v.toFixed(1)); // 当作 MB } function dumpAllMem() { try { const metrics = app.getAppMetrics(); const report = metrics.map(m => { const mem = m.memory || {}; const workingSetMB = toMB(mem.workingSetSize ?? mem.workingSet ?? 0); const privateMB = toMB(mem.privateBytes ?? mem.private ?? 0); const sharedMB = toMB(mem.shared ?? 0); return { pid: m.pid, type: m.type, workingSetMB, privateMB, sharedMB }; }); // console.log('>Tab workingSetMB=', report[3].workingSetMB); // // 渲染进程类型在不同版本可能是 "Renderer" 或 "Tab" // const isRenderer = (t) => t === 'Renderer' || t === 'Tab'; // const renderers = report.filter(r => isRenderer(r.type)); // const gpu = report.filter(r => r.type.toLowerCase().includes('gpu')); // const sum = (arr, k) => arr.reduce((a, b) => a + (b[k] || 0), 0).toFixed(1); // console.log('> [MEM] SUM Renderer workingSetMB=', sum(renderers, 'workingSetMB')); // if (gpu.length) console.log('> [MEM] GPU=', gpu); } catch (e) { console.warn('获取应用指标错误:', e); } } // 在函数外定义计数器(或者放在函数内部,用闭包封装) let consumeCount = 0; async function setupMQConsumerAndPublisher(emitMessage, tenantId) { const queueNames = [ `q.tenant.${tenantId}`, `b.tenant.${tenantId}`, // 新增队列 ]; for (const qName of queueNames) { await mq.startConsumer( qName, async (msg) => { const payload = msg.json ?? msg.text; // 原始业务数据 consumeCount++; // 所有队列共用计数器 // 标记来源类型:普通队列 / burst 队列 const isBurstQueue = qName.startsWith('b.'); const meta = isBurstQueue ? 2 : 1; console.log( `[MQ消费] [${qName}]`, payload?.hostsId, payload?.country, '共' + consumeCount + '条数据' ); // ⚠️ 关键:在原有 payload 的基础上,增加 _mqMeta 字段 // 这样你之前前端用 payload.hostsId / payload.country 的地方完全不用改 const wrapped = { ...payload, _mqMeta: meta }; // 广播到前端 emitMessage(wrapped); // 成功返回会在 mq 客户端内部自动 ack }, { prefetch: 1, requeueOnError: false, durable: true, assertQueue: true } ); } // 供渲染进程发送消息到队列(保持原来的 q.tenant.* 不变) ipcMain.removeHandler('mq-send'); ipcMain.handle('mq-send', async (_event, user) => { console.log('消息已发送', user); await mq.publishToQueue(`q.tenant.${tenantId}`, user); return { ok: true }; }); } /** * 创建 BrowserWindow,并在加载 http://localhost:8080/ 失败时启动定时检测,成功后自动 reload */ function createWindow() { const appVersion = app.getVersion(); // 获取 package.json 中的 version const win = new BrowserWindow({ width: 1920, height: 1080, title: `YOLO(AI助手ios) v${appVersion}`, // 设置窗口标题 frame: true, fullscreenable: false, webPreferences: { preload: path.join(__dirname, 'js', 'preload.js'), contextIsolation: true, nodeIntegration: false, // 推荐:默认 true;只有 SAFE_MODE 才 false(等价 --no-sandbox) sandbox: SAFE_MODE ? false : true, webSecurity: true, backgroundThrottling: false, enableRemoteModule: false, offscreen: false, // 兼容模式更稳:关实验特性 experimentalFeatures: SAFE_MODE ? false : true, autoplayPolicy: 'no-user-gesture-required', devTools: true, } }); win.setMenu(null); // 禁用菜单栏F12 win.maximize(); // ✅ 把渲染进程的 console.* 打进 main 的日志 win.webContents.on('console-message', (event, level, message, line, sourceId) => { // level: 0=log,1=warn,2=error,3=info,4=debug if (level == 1) { console.log( `[页面日志] ${message}` ); } }); // 自动判断环境使用不同的页面地址 const isProd = app.isPackaged; // const targetURL = isProd ? 'https://iosaitest.yolozs.com' : 'http://192.168.1.128:8080'; const targetURL = isProd ? 'https://iosai.yolozs.com' : 'http://192.168.1.128:8080'; console.log('[页面加载] 使用地址:', targetURL); let retryIntervalId = null; // 启动定时重试逻辑 function scheduleRetry() { const intervalMs = 5000; // 每 5 秒检测一次,可根据需求调整 console.log(`[Auto-Reconnect] 启动定时检测,每 ${intervalMs / 1000}s 重新检测服务`); } // 统一的“尝试跳转”函数:更新中 -> 延迟;否则直接跳 const tryNavigate = (reasonTag = '') => { const ts = Date.now() // 或者用前端版本号 const go = () => safeLoadURL(win, `${targetURL}?t=${ts}`, () => { if (!retryIntervalId) scheduleRetry(); }); if (updateInProgress) { console.log(`[Nav] 阻塞跳转(${reasonTag}):正在更新中,等待解锁`); pendingNavs.push(go); // 等待 autoUpdater 事件解锁后 flush 执行 return; } go(); }; // 监听前端请求文件选择 ipcMain.handle('select-apk-file', async () => { const result = await dialog.showOpenDialog({ title: '选择 APK 文件', filters: [{ name: 'APK', extensions: ['apk'] }], properties: ['openFile'] }) if (result.canceled || result.filePaths.length === 0) { return null } return result.filePaths[0] }) // 监听前端请求文件选择 ipcMain.handle('select-file', async () => { const result = await dialog.showOpenDialog({ title: '选择文件', // filters: [{ name: 'APK', extensions: ['apk'] }], properties: ['openFile'] }) if (result.canceled || result.filePaths.length === 0) { return null } return result.filePaths[0] }) // 接收渲染进程请求执行 GC ipcMain.handle('manual-gc', () => { if (global.gc) { global.gc(); console.log('🧹 手动触发 GC 成功'); } else { console.warn('⚠️ global.gc 不存在,请确认是否添加了 --expose-gc'); } }); // 小工具:异步 sleep const sleep = (ms) => new Promise(r => setTimeout(r, ms)); /** * 前端调用:一直检测,直到检测到 iproxy.exe 才返回 * 用法: * const res = await window.electron.ipcRenderer.invoke('iproxy:waitUntilRunning', { intervalMs: 2000, exeName: 'iproxy.exe' }) * if (res.running) { /* 决定是否跳转 *\/ } */ ipcMain.handle('isiproxy', async (_event, opts = {}) => { startIOSAIExecutable(); //启动iosAi服务 const { intervalMs = 2000, exeName = 'iproxy.exe', maxWaitMs = 0 } = opts; // 非 Windows 平台直接认为通过(按需改) if (process.platform !== 'win32') return { running: true, platform: process.platform }; const start = Date.now(); while (true) { const ok = await isProcessRunningWin(exeName); if (ok) return { running: true }; if (maxWaitMs > 0 && (Date.now() - start) >= maxWaitMs) { return { running: false, timeout: true }; } await sleep(intervalMs); } }); // 仅检查固定路径:C:\Users\Administrator\IOSAI\aiConfig.json ipcMain.handle('file-exists', async () => { try { // ★ 固定路径(如需改用户,改这里即可) const fixedPath = `${USER_ROOT}\\IOSAI\\aiConfig.json`; // 用 stat 判断是否存在 & 获取信息 const stat = await fsp.stat(fixedPath).catch(err => { if (err && err.code === 'ENOENT') return null; // 不存在 throw err; // 其他错误抛出 }); if (!stat) { return { ok: true, exists: false, path: fixedPath }; } return { ok: true, exists: true, path: fixedPath, isFile: stat.isFile(), isDirectory: stat.isDirectory(), size: stat.size, mtimeMs: stat.mtimeMs }; } catch (e) { return { ok: false, error: e.message || String(e) }; } }); // // 判断文件存不存在 // ipcMain.handle('file-exists', async (_evt, targetPath, baseDir /* 可选 */) => { // try { // const full = normalizePath(targetPath, baseDir) // const stat = await fsp.stat(full).catch(err => { // if (err && err.code === 'ENOENT') return null // throw err // }) // if (!stat) { // return { ok: true, exists: false, path: full } // } // return { // ok: true, // exists: true, // path: full, // isFile: stat.isFile(), // isDirectory: stat.isDirectory(), // size: stat.size, // mtimeMs: stat.mtimeMs // } // } catch (e) { // return { ok: false, error: e.message || String(e) } // } // }) // 监听加载失败事件 win.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (isMainFrame) { console.warn(`[did-fail-load] 加载失败(${errorCode}): ${errorDescription},URL: ${validatedURL}`); if (!retryIntervalId) { scheduleRetry(); } } }); // 监听加载完成事件:一旦成功加载,清除重试定时器 win.webContents.on('did-finish-load', () => { const currentURL = win.webContents.getURL(); console.log(`[did-finish-load] 加载完成,当前 URL: ${currentURL}`) if (retryIntervalId) { clearInterval(retryIntervalId); retryIntervalId = null; } }); // === 首次加载:先上“加载页” === const loadingFile = path.join(__dirname, 'waiting.html'); const hasLoading = fs.existsSync(loadingFile); if (process.platform === 'win32') { if (hasLoading) { win.loadFile(loadingFile).catch(err => { console.warn('[加载页 loadFile 失败,退回内联HTML]', err); // 兜底:内联一个极简页面 win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('