Files
tk-electron-ai/main.js
2025-12-01 20:04:52 +08:00

899 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: `YOLOAI助手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('<h3>正在等待 iproxy.exe 启动…</h3>'));
});
} else {
// 如果没有把加载页文件放到指定位置,给个兜底
win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('<h3>正在等待 iproxy.exe 启动…</h3>'));
}
// ✅ 不再自动检测 iproxy3 秒后直接进入业务页面
setTimeout(() => {
tryNavigate('delay-3s-autogo');
}, 3000);
// // 开始检测 iproxy检测到后再跳转 targetURL
// waitForIproxyAndNavigate(win, targetURL, {
// intervalMs: 2000,
// timeoutMs: 999999,
// exeName: 'iproxy.exe'
// }, tryNavigate);
} else {
// 非 Windows 平台直接进页面
tryNavigate('non-win-first');
}
//启动sse
sseServer = startSSE();
const emitMessage = createBurstBroadcaster(sseServer.broadcast, {
event: 'message', // 你前端监 n听的事件名
idleMs: 10_000,
startPayload: 'start',
startOnFirst: true // 如果不想程序启动后的第一条先发 start就设为 false
});
// 开启mq
ipcMain.handle('start-mq', async (event, tentId, id) => {
userData.tenantId = tentId;
userData.userId = id;
//启动mq
setupMQConsumerAndPublisher(emitMessage, tentId)
});
// 可选:开发阶段打开 DevTools F12
// win.webContents.openDevTools();
return win;
}
// —— 启动 iOSAI.exe ——
// 兼容开发/打包路径:优先同级目录,其次 resources再到源码目录
function resolveIOSAIPath() {
const candidates = [
// 打包后和应用同级目录app.exe 同目录)
path.join(path.dirname(process.execPath), 'iOSAI', 'IOSAI.exe'),
// 打包后:在 resources 目录(需要用 electron-builder 的 extraResources 拷贝进去)
path.join(process.resourcesPath || '', 'iOSAI', 'IOSAI.exe'),
// 开发环境:以当前文件目录为基准
path.join(__dirname, 'iOSAI', 'IOSAI.exe'),
// 兜底:以当前工作目录为基准
path.join(process.cwd(), 'iOSAI', 'IOSAI.exe'),
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) return p;
} catch (_) { }
}
return null;
}
let iosAIProcess = null; // 如果你后面想控制/关闭它,用得到;“只要打开就行”也可以不管
// function startIOSAIExecutable() {
// if (process.platform !== 'win32') {
// console.warn('[IOSAI] 当前不是 Windows跳过启动 IOSAI.exe');
// return;
// }
// const exePath = resolveIOSAIPath();
// if (!exePath) {
// console.error('[IOSAI] 未找到 IOSAI.exe请确认路径根目录/iOSAI/IOSAI.exe或已打包到 resources');
// return;
// }
// const exeDir = path.dirname(exePath);
// const exeFile = path.basename(exePath);
// // 打印确认:应为 ...\IOSAI.exe
// console.log('[IOSAI] 计划启动文件:', exePath);
// console.log('[IOSAI] 文件存在:', fs.existsSync(exePath));
// // 注意start 的第一个带引号参数是窗口标题,这里用 "" 占位
// // /D 指定工作目录;最后只传文件名,避免整条路径再被拆分
// // const cmd = `start "" /D "${exeDir}" "${exeFile}"`;
// const params = 'iosai';
// const cmd = `start "" /D "${exeDir}" "${exeFile}" ${params}`;
// try {
// exec(cmd, { cwd: exeDir, windowsHide: false }, (err) => {
// if (err) {
// console.error('[IOSAI] 启动失败:', err);
// } else {
// console.log('[IOSAI] 启动命令已执行(独立控制台窗口中运行)');
// }
// });
// } catch (e) {
// console.error('[IOSAI] 启动异常:', e);
// }
// }
// 启动 IOSAI.exe管理员权限
// 检查当前 Electron 是否已是管理员(高完整性)
function isElevated(cb) {
// 查询当前进程完整性级别High 表示管理员
exec('whoami /groups', { windowsHide: true }, (err, stdout) => {
if (err) return cb(false);
cb(/High Mandatory Level/i.test(stdout));
});
}
function psEscapeSingleQuotes(s) {
return s.replace(/'/g, "''");
}
function startIOSAIExecutable() {
if (process.platform !== 'win32') {
console.warn('[IOSAI] 当前不是 Windows跳过启动 IOSAI.exe');
return;
}
const exePath = resolveIOSAIPath();
if (!exePath || !fs.existsSync(exePath)) {
console.error('[IOSAI] 未找到 IOSAI.exe', exePath);
return;
}
const exeDir = path.dirname(exePath);
const args = ['iosai'];
isElevated((elev) => {
console.log('[IOSAI] 当前 Electron 是否管理员:', elev);
if (elev) {
const exeFile = path.basename(exePath);
// 已经是管理员:直接普通方式启动即可
const cmd = `start "" /D "${exeDir}" "${exeFile}" ${args[0]}`;
exec(cmd, { cwd: exeDir, windowsHide: false }, (err) => {
console.log(err)
if (err) console.error('[IOSAI] 启动失败:', err);
else console.log('[IOSAI] 已启动(父进程已是管理员,无需 UAC 弹窗)');
});
return;
}
// 未提权 → 用 PowerShell 提权
const exePathPS = psEscapeSingleQuotes(exePath);
const exeDirPS = psEscapeSingleQuotes(exeDir);
const argListPS = `@(${args.map(a => `'${psEscapeSingleQuotes(String(a))}'`).join(',')})`;
const psCommand = `Start-Process -FilePath '${exePathPS}' -ArgumentList ${argListPS} -WorkingDirectory '${exeDirPS}' -Verb RunAs -WindowStyle Normal -PassThru`;
console.log('[IOSAI] 提权启动命令(PS)', psCommand);
execFile(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psCommand],
{ windowsHide: true },
(err) => {
if (err) {
console.error('[IOSAI] 提权启动失败:', err);
} else {
console.log('[IOSAI] 已请求管理员启动(按系统策略可能不弹 UAC');
// 可选:用 tasklist 简单确认是否起来了
setTimeout(() => {
exec('tasklist /FI "IMAGENAME eq IOSAI.exe"', { windowsHide: true }, (e2, out) => {
console.log('[IOSAI] 进程检查:\n', out);
});
}, 1200);
}
}
);
});
}
async function killIOSAI() {
console.log('尝试关闭IOSAI', userData);
// console.log('axios', axios);
await axios.post('https://crawlclient.api.yolozs.com/api/user/aiChat-logout', { userId: userData.userId, tenantId: userData.tenantId }, {
headers: {
'Content-Type': 'application/json', // 设置请求头
'vvtoken': userData.tokenValue
}
}).then(response => {
console.log("发送登出请求成功")
}).catch(error => {
console.log("发送登出请求错误", error)
}).finally(error => {
// console.log("发送")
})
try {
if (iosAIProcess && iosAIProcess.pid) {
// 按 PID 杀,并带上 /T 杀掉它可能拉起的子进程
exec(`taskkill /PID ${iosAIProcess.pid} /T /F`, (err) => {
if (err) {
console.warn('[IOSAI] 按 PID 关闭失败,改用按进程名:', err.message);
// 兜底:按进程名杀(会杀掉系统里所有 IOSAI.exe若允许同名多实例请注意
exec('taskkill /IM IOSAI.exe /F');
}
});
} else {
// 没有 PID比如程序二次拉起后原 PID 退出),直接按进程名兜底
exec('taskkill /IM IOSAI.exe /F');
exec('taskkill /IM iproxy.exe /F');
exec('taskkill /IM pythonw.exe /F');
exec('taskkill /IM ios.exe /F');
}
} catch (_) { }
iosAIProcess = null;
}
const broadcast = (channel, payload) => {
BrowserWindow.getAllWindows().forEach(w => {
if (!w.isDestroyed()) w.webContents.send(channel, payload);
});
};
// —— 进程检测Windows 使用 tasklist 精确匹配 ——
// 返回 Promise<boolean>
function isProcessRunningWin(exeName) {
return new Promise((resolve) => {
if (process.platform !== 'win32') return resolve(true); // 非 Win 平台直接放行
// /FI 过滤进程名;/FO CSV 便于稳定解析;/NH 去表头
const cmd = `tasklist /FI "IMAGENAME eq ${exeName}" /FO CSV /NH`;
exec(cmd, { windowsHide: true }, (err, stdout) => {
if (err || !stdout) return resolve(false);
// 匹配形如:"iproxy.exe","xxxx","Console",...
const found = stdout.toLowerCase().includes(`"${exeName.toLowerCase()}"`);
resolve(found);
});
});
}
/**
* 等待 iproxy.exe 启动后跳转到目标 URL
* @param {BrowserWindow} win
* @param {string} targetURL
* @param {object} options
*/
function waitForIproxyAndNavigate(win, targetURL, options = {}, tryNavigate) {
const {
intervalMs = 2000, // 轮询间隔
timeoutMs = 999999, // 超时提示(不强制跳转)
exeName = 'iproxy.exe'
} = options;
let elapsed = 0;
// 加个不确定进度(任务栏)提示
win.setProgressBar(2, { mode: 'indeterminate' });
const timer = setInterval(async () => {
const ok = await isProcessRunningWin(exeName);
if (ok) {
clearInterval(timer);
win.setProgressBar(-1);
console.log(`[iproxy] 检测到 ${exeName} 已运行,进入页面:${targetURL}`);
// 检测到后再真正加载业务页面
tryNavigate('no-waiting-file')
return;
}
elapsed += intervalMs;
if (elapsed >= timeoutMs) {
// 仅提示,不强制退出。你也可以在此弹窗或修改窗口标题
console.warn(`[iproxy] 等待 ${exeName} 超时 ${timeoutMs / 1000}s仍未检测到进程。`);
win.setTitle('YOLOAI助手ios - 正在等待 iproxy.exe 启动…(可检查连接/杀软/权限)');
// 到时间后依旧保持加载页 + 继续轮询(如果想停止轮询,把 clearInterval 放这里)
// clearInterval(timer);
}
}, intervalMs);
}
/**
* 将传入路径标准化:
* - 支持 file:// URL
* - 相对路径会按 baseDir 解析(不传 baseDir 则按当前进程工作目录)
*/
function normalizePath(targetPath, baseDir) {
if (typeof targetPath !== 'string' || !targetPath.trim()) {
throw new Error('无效的路径参数')
}
// 支持 file://
if (targetPath.startsWith('file://')) {
return fileURLToPath(new URL(targetPath))
}
// 相对路径 → 解析到 baseDir
if (!path.isAbsolute(targetPath)) {
const base = baseDir && typeof baseDir === 'string' ? baseDir : process.cwd()
return path.resolve(base, targetPath)
}
return path.resolve(targetPath)
}