import { app, BrowserWindow, session, ipcMain } from 'electron'; import { fileURLToPath } from 'url'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import { existsSync, readFileSync } from 'fs'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; // 读取配置文件 function loadConfig() { try { const configPath = path.join(__dirname, 'config.js'); if (existsSync(configPath)) { const configContent = readFileSync(configPath, 'utf-8'); // 解析 JSON(移除可能的注释和尾随逗号) const jsonContent = configContent.replace(/\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1'); return JSON.parse(jsonContent); } } catch (error) { console.warn('Failed to load config.js:', error.message); } return null; } // 查找 ADB 可执行文件路径 function getAdbPath() { // 首先尝试从配置文件读取 const config = loadConfig(); if (config && config['adb-path']) { const configAdbPath = path.join(config['adb-path'], 'adb.exe'); if (existsSync(configAdbPath)) { console.log('Using ADB path from config.js:', configAdbPath); return configAdbPath; } // 如果配置的路径不存在,尝试直接使用配置的路径(可能已经是完整路径) if (existsSync(config['adb-path'])) { console.log('Using ADB path from config.js:', config['adb-path']); return config['adb-path']; } } // 如果配置文件没有或路径不存在,使用常见 ADB 安装位置(按优先级排序) const possiblePaths = [ path.join(process.env.LOCALAPPDATA || '', 'Android', 'Sdk', 'platform-tools', 'adb.exe'), path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Android', 'Sdk', 'platform-tools', 'adb.exe'), path.join(process.env.ProgramFiles || '', 'Android', 'android-sdk', 'platform-tools', 'adb.exe'), path.join(process.env['ProgramFiles(x86)'] || '', 'Android', 'android-sdk', 'platform-tools', 'adb.exe'), ]; // 检查常见位置 for (const adbPath of possiblePaths) { if (existsSync(adbPath)) { return adbPath; } } // 如果都找不到,尝试使用 PATH 中的 adb return 'adb'; } // 缓存 ADB 路径 let adbPathCache = null; function getCachedAdbPath() { if (!adbPathCache) { adbPathCache = getAdbPath(); } return adbPathCache; } // 设置内容安全策略(CSP),防止 XSS 攻击 function setContentSecurityPolicy() { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { const csp = isDev ? "default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:*; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:*; img-src 'self' data: https: blob:; font-src 'self' data:; worker-src 'self' blob:;" : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:;"; const responseHeaders = Object.assign({}, details.responseHeaders); responseHeaders['Content-Security-Policy'] = [csp]; callback({ responseHeaders }); }); } // 全局主窗口引用,用于发送实时事件 let mainWindow = null; // 创建主窗口,根据环境加载不同内容源 function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.cjs'), nodeIntegration: false, contextIsolation: true } }); // 禁用窗口关闭确认对话框,直接关闭 mainWindow.on('close', (event) => { // 不阻止关闭事件,直接关闭窗口 // 如果需要清理资源,可以在这里添加 }); // 禁用 webContents 的 beforeunload 确认对话框 mainWindow.webContents.on('will-prevent-unload', (event) => { // 不阻止卸载,直接关闭 // 注意:这里不需要调用 event.preventDefault(),因为我们要允许关闭 }); // 禁用 beforeunload 事件(前端页面可能触发的确认对话框) mainWindow.webContents.setWindowOpenHandler(() => { return { action: 'allow' }; }); // 禁用所有可能阻止关闭的事件 mainWindow.webContents.on('beforeunload', (event) => { // 不阻止卸载 }); if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, 'dist/index.html')); } } // 获取已连接的 ADB 设备列表 async function getADBDevices() { try { const adbPath = getCachedAdbPath(); const { stdout } = await execAsync(`${adbPath} devices`); const lines = stdout.split('\n').slice(1); const devices = []; for (const line of lines) { const parts = line.trim().split(/\s+/); if (parts.length >= 2 && parts[0] && parts[1] === 'device') { devices.push({ id: parts[0], status: parts[1] }); } } return devices; } catch (error) { console.error('获取设备列表失败:', error); return []; } } // 网络扫描:从 192.168.0.1 开始扫描网段,尝试连接设备(实时推送结果) async function scanNetworkDevices(event) { const adbPath = getCachedAdbPath(); const baseIP = '192.168.0'; const port = 5555; const maxConcurrent = 20; // 限制并发数,避免过载 const connectTimeout = 1500; // 连接超时时间(毫秒) // 生成 IP 地址列表 const ipList = []; for (let i = 1; i <= 255; i++) { ipList.push(`${baseIP}.${i}`); } const foundDevices = new Set(); // 使用 Set 避免重复 // 分批并发扫描 for (let i = 0; i < ipList.length; i += maxConcurrent) { const batch = ipList.slice(i, i + maxConcurrent); const promises = batch.map(async (ip) => { const ipPort = `${ip}:${port}`; try { // 先尝试连接设备 await execAsync(`${adbPath} connect ${ipPort}`, { timeout: connectTimeout, maxBuffer: 1024 * 1024 }); // 连接后稍等片刻,让设备注册到 ADB 服务器 await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { // 连接失败是正常的,继续检查设备列表 } // 直接使用 adb -s IP:PORT devices 命令检查该设备是否在列表中 try { const { stdout } = await execAsync(`${adbPath} -s ${ipPort} devices`, { timeout: 2000, maxBuffer: 1024 * 1024 }); // 检查输出中是否包含 IP:PORT,如果有则说明设备存在 if (stdout.includes(ipPort)) { // 发现设备,实时推送 if (!foundDevices.has(ipPort)) { foundDevices.add(ipPort); const device = { id: ipPort, status: 'device' }; // 实时发送发现的设备 if (event && event.sender) { event.sender.send('device-found', device); } else if (mainWindow) { mainWindow.webContents.send('device-found', device); } console.log('发现设备:', ipPort); } return ipPort; } } catch (checkError) { // 检查失败,忽略 } return null; }); await Promise.all(promises); } // 返回所有发现的设备 return Array.from(foundDevices).map(ipPort => ({ id: ipPort, status: 'device' })); } // IPC 处理程序:获取 ADB 路径配置 ipcMain.handle('get-adb-path-config', async () => { const config = loadConfig(); return config ? config['adb-path'] : null; }); // IPC 处理程序:获取 ADB 设备列表 ipcMain.handle('get-adb-devices', async () => { return await getADBDevices(); }); // IPC 处理程序:扫描网络设备(支持实时推送) ipcMain.handle('scan-adb-devices', async (event) => { try { const devices = await scanNetworkDevices(event); return devices; } catch (error) { console.error('网络扫描失败:', error); return []; } }); // IPC 处理程序:连接 ADB 设备 ipcMain.handle('connect-adb-device', async (event, ipPort) => { try { const adbPath = getCachedAdbPath(); await execAsync(`${adbPath} connect ${ipPort}`); return { success: true }; } catch (error) { console.error('连接设备失败:', error); return { success: false, error: error.message }; } }); // IPC 处理程序:获取设备分辨率 ipcMain.handle('get-device-resolution', async (event, ipPort) => { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } try { // 使用 wm size 命令获取设备分辨率 const adbPath = getCachedAdbPath(); const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell wm size`); // 输出格式通常是: "Physical size: 1080x2400" 或 "1080x2400" const match = stdout.match(/(\d+)x(\d+)/); if (match) { return { success: true, width: parseInt(match[1], 10), height: parseInt(match[2], 10) }; } // 如果解析失败,返回默认值 return { success: true, width: 1280, height: 2400 }; } catch (error) { console.error('获取设备分辨率失败:', error); // 返回默认值 return { success: true, width: 1280, height: 2400 }; } }); // IPC 处理程序:抓取设备截屏(返回 base64 PNG/JPEG) ipcMain.handle('capture-screenshot', async (event, ipPort, options = {}) => { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } try { const adbPath = getCachedAdbPath(); // 从选项或默认值获取参数 const format = options.format || 'png'; // 'png' 或 'jpeg' const quality = options.quality || 80; // JPEG 质量 1-100 const scale = options.scale || 1.0; // 缩放比例 0.1-1.0 // 构建 screencap 命令 let command = `${adbPath} -s ${ipPort} exec-out screencap`; // 根据格式选择参数 if (format === 'jpeg') { // JPEG 格式(更小,延迟更低) command += ` -j ${quality}`; } else { // PNG 格式(默认) command += ' -p'; } // 如果缩放比例不是 1.0,需要通过 shell 命令处理 // 注意:screencap 本身不支持缩放,需要通过其他方式实现 // 这里先实现基本功能,缩放可以在后续优化 const { stdout } = await execAsync(command, { encoding: 'buffer', maxBuffer: 25 * 1024 * 1024, }); return { success: true, data: stdout.toString('base64') }; } catch (error) { console.error('截屏失败:', error); return { success: false, error: error.message }; } }); // IPC 处理程序:发送 tap 事件到设备 ipcMain.handle('send-tap', async (event, ipPort, x, y) => { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } if (typeof x !== 'number' || typeof y !== 'number') { return { success: false, error: '坐标必须是数字' }; } try { const adbPath = getCachedAdbPath(); const command = `${adbPath} -s ${ipPort} shell input tap ${x} ${y}`; await execAsync(command, { timeout: 5000, maxBuffer: 1024 * 1024 }); return { success: true }; } catch (error) { console.error('Tap 失败:', error.message); return { success: false, error: error.message }; } }); // IPC 处理程序:发送 swipe 事件到设备 ipcMain.handle('send-swipe', async (event, ipPort, x1, y1, x2, y2, duration = 300) => { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number') { return { success: false, error: '坐标必须是数字' }; } try { const adbPath = getCachedAdbPath(); const command = `${adbPath} -s ${ipPort} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`; await execAsync(command, { timeout: 5000, maxBuffer: 1024 * 1024 }); return { success: true }; } catch (error) { console.error('Swipe 失败:', error.message); return { success: false, error: error.message }; } }); // IPC 处理程序:发送文字到设备 ipcMain.handle('send-text', async (event, ipPort, text) => { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } if (typeof text !== 'string') { return { success: false, error: '文字必须是字符串' }; } try { const adbPath = getCachedAdbPath(); // ADB input text 需要转义特殊字符 // 使用单引号包裹,并转义单引号 const escapedText = text .replace(/\\/g, '\\\\') // 先转义反斜杠 .replace(/'/g, "'\\''") // 转义单引号 .replace(/ /g, '%s') // 空格转换为 %s .replace(/&/g, '\\&') // 转义 & .replace(//g, '\\>') // 转义 > .replace(/\(/g, '\\(') // 转义 ( .replace(/\)/g, '\\)') // 转义 ) .replace(/;/g, '\\;') // 转义 ; .replace(/\|/g, '\\|') // 转义 | .replace(/\*/g, '\\*') // 转义 * .replace(/\?/g, '\\?') // 转义 ? .replace(/`/g, '\\`') // 转义 ` .replace(/\$/g, '\\$') // 转义 $ .replace(/"/g, '\\"'); // 转义 " // 对于换行,使用 KEYCODE_ENTER if (text.includes('\n')) { // 如果有换行,分段发送 const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i]) { const escapedLine = lines[i] .replace(/\\/g, '\\\\') .replace(/'/g, "'\\''") .replace(/ /g, '%s') .replace(/&/g, '\\&') .replace(//g, '\\>') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/;/g, '\\;') .replace(/\|/g, '\\|') .replace(/\*/g, '\\*') .replace(/\?/g, '\\?') .replace(/`/g, '\\`') .replace(/\$/g, '\\$') .replace(/"/g, '\\"'); await execAsync(`${adbPath} -s ${ipPort} shell input text "${escapedLine}"`, { timeout: 5000, maxBuffer: 1024 * 1024 }); } // 发送换行(除了最后一行) if (i < lines.length - 1) { await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, { timeout: 5000, maxBuffer: 1024 * 1024 }); } } } else { // 没有换行,直接发送 const command = `${adbPath} -s ${ipPort} shell input text "${escapedText}"`; await execAsync(command, { timeout: 5000, maxBuffer: 1024 * 1024 }); } return { success: true }; } catch (error) { console.error('发送文字失败:', error.message); return { success: false, error: error.message }; } }); // IPC 处理程序:发送按键事件到设备 ipcMain.handle('send-key-event', async (event, ipPort, keyCode) => { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } if (typeof keyCode !== 'string') { return { success: false, error: '按键代码必须是字符串' }; } try { const adbPath = getCachedAdbPath(); const command = `${adbPath} -s ${ipPort} shell input keyevent ${keyCode}`; await execAsync(command, { timeout: 5000, maxBuffer: 1024 * 1024 }); return { success: true }; } catch (error) { console.error('发送按键失败:', error.message); return { success: false, error: error.message }; } }); // 应用启动逻辑:设置 CSP、创建窗口、监听激活事件 app.whenReady().then(() => { setContentSecurityPolicy(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // 禁用 before-quit 事件的确认对话框 app.on('before-quit', (event) => { // 不阻止退出,直接退出应用 // 如果需要清理资源,可以在这里添加 }); }); // 应用关闭逻辑:macOS 保持运行,其他平台退出 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { // 直接退出,不询问 app.exit(0); } });