main.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { app, BrowserWindow, session, ipcMain } from 'electron';
  2. import { fileURLToPath } from 'url';
  3. import path from 'path';
  4. import { exec } from 'child_process';
  5. import { promisify } from 'util';
  6. const execAsync = promisify(exec);
  7. const __filename = fileURLToPath(import.meta.url);
  8. const __dirname = path.dirname(__filename);
  9. const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
  10. // 设置内容安全策略(CSP),防止 XSS 攻击
  11. function setContentSecurityPolicy() {
  12. session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  13. const csp = isDev
  14. ? "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:;"
  15. : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:;";
  16. const responseHeaders = Object.assign({}, details.responseHeaders);
  17. responseHeaders['Content-Security-Policy'] = [csp];
  18. callback({ responseHeaders });
  19. });
  20. }
  21. // 创建主窗口,根据环境加载不同内容源
  22. function createWindow() {
  23. const mainWindow = new BrowserWindow({
  24. width: 1200,
  25. height: 800,
  26. webPreferences: {
  27. preload: path.join(__dirname, 'preload.cjs'),
  28. nodeIntegration: false,
  29. contextIsolation: true
  30. }
  31. });
  32. if (isDev) {
  33. mainWindow.loadURL('http://localhost:5173');
  34. mainWindow.webContents.openDevTools();
  35. } else {
  36. mainWindow.loadFile(path.join(__dirname, 'dist/index.html'));
  37. }
  38. }
  39. // 获取已连接的 ADB 设备列表
  40. async function getADBDevices() {
  41. try {
  42. const { stdout } = await execAsync('adb devices');
  43. const lines = stdout.split('\n').slice(1);
  44. const devices = [];
  45. for (const line of lines) {
  46. const parts = line.trim().split(/\s+/);
  47. if (parts.length >= 2 && parts[0] && parts[1] === 'device') {
  48. devices.push({
  49. id: parts[0],
  50. status: parts[1]
  51. });
  52. }
  53. }
  54. return devices;
  55. } catch (error) {
  56. console.error('获取设备列表失败:', error);
  57. return [];
  58. }
  59. }
  60. // IPC 处理程序:获取 ADB 设备列表
  61. ipcMain.handle('get-adb-devices', async () => {
  62. return await getADBDevices();
  63. });
  64. // IPC 处理程序:连接 ADB 设备
  65. ipcMain.handle('connect-adb-device', async (event, ipPort) => {
  66. try {
  67. await execAsync(`adb connect ${ipPort}`);
  68. return { success: true };
  69. } catch (error) {
  70. console.error('连接设备失败:', error);
  71. return { success: false, error: error.message };
  72. }
  73. });
  74. // IPC 处理程序:获取设备分辨率
  75. ipcMain.handle('get-device-resolution', async (event, ipPort) => {
  76. if (!ipPort) {
  77. return { success: false, error: '缺少设备 ID' };
  78. }
  79. try {
  80. // 使用 wm size 命令获取设备分辨率
  81. const { stdout } = await execAsync(`adb -s ${ipPort} shell wm size`);
  82. // 输出格式通常是: "Physical size: 1080x2400" 或 "1080x2400"
  83. const match = stdout.match(/(\d+)x(\d+)/);
  84. if (match) {
  85. return {
  86. success: true,
  87. width: parseInt(match[1], 10),
  88. height: parseInt(match[2], 10)
  89. };
  90. }
  91. // 如果解析失败,返回默认值
  92. return { success: true, width: 1280, height: 2400 };
  93. } catch (error) {
  94. console.error('获取设备分辨率失败:', error);
  95. // 返回默认值
  96. return { success: true, width: 1280, height: 2400 };
  97. }
  98. });
  99. // IPC 处理程序:抓取设备截屏(返回 base64 PNG)
  100. ipcMain.handle('capture-screenshot', async (event, ipPort) => {
  101. if (!ipPort) {
  102. return { success: false, error: '缺少设备 ID' };
  103. }
  104. try {
  105. const { stdout } = await execAsync(`adb -s ${ipPort} exec-out screencap -p`, {
  106. encoding: 'buffer',
  107. maxBuffer: 25 * 1024 * 1024,
  108. });
  109. return { success: true, data: stdout.toString('base64') };
  110. } catch (error) {
  111. console.error('截屏失败:', error);
  112. return { success: false, error: error.message };
  113. }
  114. });
  115. // IPC 处理程序:发送 tap 事件到设备
  116. ipcMain.handle('send-tap', async (event, ipPort, x, y) => {
  117. if (!ipPort) {
  118. return { success: false, error: '缺少设备 ID' };
  119. }
  120. if (typeof x !== 'number' || typeof y !== 'number') {
  121. return { success: false, error: '坐标必须是数字' };
  122. }
  123. try {
  124. const command = `adb -s ${ipPort} shell input tap ${x} ${y}`;
  125. await execAsync(command, {
  126. timeout: 5000,
  127. maxBuffer: 1024 * 1024
  128. });
  129. return { success: true };
  130. } catch (error) {
  131. console.error('Tap 失败:', error.message);
  132. return { success: false, error: error.message };
  133. }
  134. });
  135. // IPC 处理程序:发送 swipe 事件到设备
  136. ipcMain.handle('send-swipe', async (event, ipPort, x1, y1, x2, y2, duration = 300) => {
  137. if (!ipPort) {
  138. return { success: false, error: '缺少设备 ID' };
  139. }
  140. if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number') {
  141. return { success: false, error: '坐标必须是数字' };
  142. }
  143. try {
  144. const command = `adb -s ${ipPort} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`;
  145. await execAsync(command, {
  146. timeout: 5000,
  147. maxBuffer: 1024 * 1024
  148. });
  149. return { success: true };
  150. } catch (error) {
  151. console.error('Swipe 失败:', error.message);
  152. return { success: false, error: error.message };
  153. }
  154. });
  155. // 应用启动逻辑:设置 CSP、创建窗口、监听激活事件
  156. app.whenReady().then(() => {
  157. setContentSecurityPolicy();
  158. createWindow();
  159. app.on('activate', () => {
  160. if (BrowserWindow.getAllWindows().length === 0) {
  161. createWindow();
  162. }
  163. });
  164. });
  165. // 应用关闭逻辑:macOS 保持运行,其他平台退出
  166. app.on('window-all-closed', () => {
  167. if (process.platform !== 'darwin') {
  168. app.quit();
  169. }
  170. });