main.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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. import { existsSync, readFileSync } from 'fs';
  7. const execAsync = promisify(exec);
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
  11. // 读取配置文件
  12. function loadConfig() {
  13. try {
  14. const configPath = path.join(__dirname, 'config.js');
  15. if (existsSync(configPath)) {
  16. const configContent = readFileSync(configPath, 'utf-8');
  17. // 解析 JSON(移除可能的注释和尾随逗号)
  18. const jsonContent = configContent.replace(/\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
  19. return JSON.parse(jsonContent);
  20. }
  21. } catch (error) {
  22. console.warn('Failed to load config.js:', error.message);
  23. }
  24. return null;
  25. }
  26. // 查找 ADB 可执行文件路径
  27. function getAdbPath() {
  28. // 首先尝试从配置文件读取
  29. const config = loadConfig();
  30. if (config && config['adb-path']) {
  31. const configAdbPath = path.join(config['adb-path'], 'adb.exe');
  32. if (existsSync(configAdbPath)) {
  33. console.log('Using ADB path from config.js:', configAdbPath);
  34. return configAdbPath;
  35. }
  36. // 如果配置的路径不存在,尝试直接使用配置的路径(可能已经是完整路径)
  37. if (existsSync(config['adb-path'])) {
  38. console.log('Using ADB path from config.js:', config['adb-path']);
  39. return config['adb-path'];
  40. }
  41. }
  42. // 如果配置文件没有或路径不存在,使用常见 ADB 安装位置(按优先级排序)
  43. const possiblePaths = [
  44. path.join(process.env.LOCALAPPDATA || '', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
  45. path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
  46. path.join(process.env.ProgramFiles || '', 'Android', 'android-sdk', 'platform-tools', 'adb.exe'),
  47. path.join(process.env['ProgramFiles(x86)'] || '', 'Android', 'android-sdk', 'platform-tools', 'adb.exe'),
  48. ];
  49. // 检查常见位置
  50. for (const adbPath of possiblePaths) {
  51. if (existsSync(adbPath)) {
  52. return adbPath;
  53. }
  54. }
  55. // 如果都找不到,尝试使用 PATH 中的 adb
  56. return 'adb';
  57. }
  58. // 缓存 ADB 路径
  59. let adbPathCache = null;
  60. function getCachedAdbPath() {
  61. if (!adbPathCache) {
  62. adbPathCache = getAdbPath();
  63. }
  64. return adbPathCache;
  65. }
  66. // 设置内容安全策略(CSP),防止 XSS 攻击
  67. function setContentSecurityPolicy() {
  68. session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  69. const csp = isDev
  70. ? "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:;"
  71. : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:;";
  72. const responseHeaders = Object.assign({}, details.responseHeaders);
  73. responseHeaders['Content-Security-Policy'] = [csp];
  74. callback({ responseHeaders });
  75. });
  76. }
  77. // 全局主窗口引用,用于发送实时事件
  78. let mainWindow = null;
  79. // 创建主窗口,根据环境加载不同内容源
  80. function createWindow() {
  81. mainWindow = new BrowserWindow({
  82. width: 1200,
  83. height: 800,
  84. webPreferences: {
  85. preload: path.join(__dirname, 'preload.cjs'),
  86. nodeIntegration: false,
  87. contextIsolation: true
  88. }
  89. });
  90. if (isDev) {
  91. mainWindow.loadURL('http://localhost:5173');
  92. mainWindow.webContents.openDevTools();
  93. } else {
  94. mainWindow.loadFile(path.join(__dirname, 'dist/index.html'));
  95. }
  96. }
  97. // 获取已连接的 ADB 设备列表
  98. async function getADBDevices() {
  99. try {
  100. const adbPath = getCachedAdbPath();
  101. const { stdout } = await execAsync(`${adbPath} devices`);
  102. const lines = stdout.split('\n').slice(1);
  103. const devices = [];
  104. for (const line of lines) {
  105. const parts = line.trim().split(/\s+/);
  106. if (parts.length >= 2 && parts[0] && parts[1] === 'device') {
  107. devices.push({
  108. id: parts[0],
  109. status: parts[1]
  110. });
  111. }
  112. }
  113. return devices;
  114. } catch (error) {
  115. console.error('获取设备列表失败:', error);
  116. return [];
  117. }
  118. }
  119. // 网络扫描:从 192.168.0.1 开始扫描网段,尝试连接设备(实时推送结果)
  120. async function scanNetworkDevices(event) {
  121. const adbPath = getCachedAdbPath();
  122. const baseIP = '192.168.0';
  123. const port = 5555;
  124. const maxConcurrent = 20; // 限制并发数,避免过载
  125. const connectTimeout = 1500; // 连接超时时间(毫秒)
  126. // 生成 IP 地址列表
  127. const ipList = [];
  128. for (let i = 1; i <= 255; i++) {
  129. ipList.push(`${baseIP}.${i}`);
  130. }
  131. const foundDevices = new Set(); // 使用 Set 避免重复
  132. // 分批并发扫描
  133. for (let i = 0; i < ipList.length; i += maxConcurrent) {
  134. const batch = ipList.slice(i, i + maxConcurrent);
  135. const promises = batch.map(async (ip) => {
  136. const ipPort = `${ip}:${port}`;
  137. try {
  138. // 尝试连接设备(超时时间短,避免等待太久)
  139. await execAsync(`${adbPath} connect ${ipPort}`, {
  140. timeout: connectTimeout,
  141. maxBuffer: 1024 * 1024
  142. });
  143. // 连接后稍等片刻,检查设备是否真的连接成功
  144. await new Promise(resolve => setTimeout(resolve, 300));
  145. // 检查设备是否在列表中
  146. try {
  147. const { stdout } = await execAsync(`${adbPath} devices`, {
  148. timeout: 2000,
  149. maxBuffer: 1024 * 1024
  150. });
  151. const lines = stdout.split('\n').slice(1);
  152. for (const line of lines) {
  153. const parts = line.trim().split(/\s+/);
  154. if (parts.length >= 2 && parts[0] === ipPort && parts[1] === 'device') {
  155. // 发现设备,实时推送
  156. if (!foundDevices.has(ipPort)) {
  157. foundDevices.add(ipPort);
  158. const device = {
  159. id: ipPort,
  160. status: 'device'
  161. };
  162. // 实时发送发现的设备
  163. if (event && event.sender) {
  164. event.sender.send('device-found', device);
  165. } else if (mainWindow) {
  166. mainWindow.webContents.send('device-found', device);
  167. }
  168. console.log('发现设备:', ipPort);
  169. }
  170. return ipPort;
  171. }
  172. }
  173. } catch (checkError) {
  174. // 检查失败,忽略
  175. }
  176. } catch (error) {
  177. // 连接失败是正常的,忽略错误
  178. return null;
  179. }
  180. return null;
  181. });
  182. await Promise.all(promises);
  183. }
  184. // 返回所有发现的设备
  185. return Array.from(foundDevices).map(ipPort => ({
  186. id: ipPort,
  187. status: 'device'
  188. }));
  189. }
  190. // IPC 处理程序:获取 ADB 路径配置
  191. ipcMain.handle('get-adb-path-config', async () => {
  192. const config = loadConfig();
  193. return config ? config['adb-path'] : null;
  194. });
  195. // IPC 处理程序:获取 ADB 设备列表
  196. ipcMain.handle('get-adb-devices', async () => {
  197. return await getADBDevices();
  198. });
  199. // IPC 处理程序:扫描网络设备(支持实时推送)
  200. ipcMain.handle('scan-adb-devices', async (event) => {
  201. try {
  202. const devices = await scanNetworkDevices(event);
  203. return devices;
  204. } catch (error) {
  205. console.error('网络扫描失败:', error);
  206. return [];
  207. }
  208. });
  209. // IPC 处理程序:连接 ADB 设备
  210. ipcMain.handle('connect-adb-device', async (event, ipPort) => {
  211. try {
  212. const adbPath = getCachedAdbPath();
  213. await execAsync(`${adbPath} connect ${ipPort}`);
  214. return { success: true };
  215. } catch (error) {
  216. console.error('连接设备失败:', error);
  217. return { success: false, error: error.message };
  218. }
  219. });
  220. // IPC 处理程序:获取设备分辨率
  221. ipcMain.handle('get-device-resolution', async (event, ipPort) => {
  222. if (!ipPort) {
  223. return { success: false, error: '缺少设备 ID' };
  224. }
  225. try {
  226. // 使用 wm size 命令获取设备分辨率
  227. const adbPath = getCachedAdbPath();
  228. const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell wm size`);
  229. // 输出格式通常是: "Physical size: 1080x2400" 或 "1080x2400"
  230. const match = stdout.match(/(\d+)x(\d+)/);
  231. if (match) {
  232. return {
  233. success: true,
  234. width: parseInt(match[1], 10),
  235. height: parseInt(match[2], 10)
  236. };
  237. }
  238. // 如果解析失败,返回默认值
  239. return { success: true, width: 1280, height: 2400 };
  240. } catch (error) {
  241. console.error('获取设备分辨率失败:', error);
  242. // 返回默认值
  243. return { success: true, width: 1280, height: 2400 };
  244. }
  245. });
  246. // IPC 处理程序:抓取设备截屏(返回 base64 PNG)
  247. ipcMain.handle('capture-screenshot', async (event, ipPort) => {
  248. if (!ipPort) {
  249. return { success: false, error: '缺少设备 ID' };
  250. }
  251. try {
  252. const adbPath = getCachedAdbPath();
  253. const { stdout } = await execAsync(`${adbPath} -s ${ipPort} exec-out screencap -p`, {
  254. encoding: 'buffer',
  255. maxBuffer: 25 * 1024 * 1024,
  256. });
  257. return { success: true, data: stdout.toString('base64') };
  258. } catch (error) {
  259. console.error('截屏失败:', error);
  260. return { success: false, error: error.message };
  261. }
  262. });
  263. // IPC 处理程序:发送 tap 事件到设备
  264. ipcMain.handle('send-tap', async (event, ipPort, x, y) => {
  265. if (!ipPort) {
  266. return { success: false, error: '缺少设备 ID' };
  267. }
  268. if (typeof x !== 'number' || typeof y !== 'number') {
  269. return { success: false, error: '坐标必须是数字' };
  270. }
  271. try {
  272. const adbPath = getCachedAdbPath();
  273. const command = `${adbPath} -s ${ipPort} shell input tap ${x} ${y}`;
  274. await execAsync(command, {
  275. timeout: 5000,
  276. maxBuffer: 1024 * 1024
  277. });
  278. return { success: true };
  279. } catch (error) {
  280. console.error('Tap 失败:', error.message);
  281. return { success: false, error: error.message };
  282. }
  283. });
  284. // IPC 处理程序:发送 swipe 事件到设备
  285. ipcMain.handle('send-swipe', async (event, ipPort, x1, y1, x2, y2, duration = 300) => {
  286. if (!ipPort) {
  287. return { success: false, error: '缺少设备 ID' };
  288. }
  289. if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number') {
  290. return { success: false, error: '坐标必须是数字' };
  291. }
  292. try {
  293. const adbPath = getCachedAdbPath();
  294. const command = `${adbPath} -s ${ipPort} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`;
  295. await execAsync(command, {
  296. timeout: 5000,
  297. maxBuffer: 1024 * 1024
  298. });
  299. return { success: true };
  300. } catch (error) {
  301. console.error('Swipe 失败:', error.message);
  302. return { success: false, error: error.message };
  303. }
  304. });
  305. // IPC 处理程序:发送文字到设备
  306. ipcMain.handle('send-text', async (event, ipPort, text) => {
  307. if (!ipPort) {
  308. return { success: false, error: '缺少设备 ID' };
  309. }
  310. if (typeof text !== 'string') {
  311. return { success: false, error: '文字必须是字符串' };
  312. }
  313. try {
  314. const adbPath = getCachedAdbPath();
  315. // ADB input text 需要转义特殊字符
  316. // 使用单引号包裹,并转义单引号
  317. const escapedText = text
  318. .replace(/\\/g, '\\\\') // 先转义反斜杠
  319. .replace(/'/g, "'\\''") // 转义单引号
  320. .replace(/ /g, '%s') // 空格转换为 %s
  321. .replace(/&/g, '\\&') // 转义 &
  322. .replace(/</g, '\\<') // 转义 <
  323. .replace(/>/g, '\\>') // 转义 >
  324. .replace(/\(/g, '\\(') // 转义 (
  325. .replace(/\)/g, '\\)') // 转义 )
  326. .replace(/;/g, '\\;') // 转义 ;
  327. .replace(/\|/g, '\\|') // 转义 |
  328. .replace(/\*/g, '\\*') // 转义 *
  329. .replace(/\?/g, '\\?') // 转义 ?
  330. .replace(/`/g, '\\`') // 转义 `
  331. .replace(/\$/g, '\\$') // 转义 $
  332. .replace(/"/g, '\\"'); // 转义 "
  333. // 对于换行,使用 KEYCODE_ENTER
  334. if (text.includes('\n')) {
  335. // 如果有换行,分段发送
  336. const lines = text.split('\n');
  337. for (let i = 0; i < lines.length; i++) {
  338. if (lines[i]) {
  339. const escapedLine = lines[i]
  340. .replace(/\\/g, '\\\\')
  341. .replace(/'/g, "'\\''")
  342. .replace(/ /g, '%s')
  343. .replace(/&/g, '\\&')
  344. .replace(/</g, '\\<')
  345. .replace(/>/g, '\\>')
  346. .replace(/\(/g, '\\(')
  347. .replace(/\)/g, '\\)')
  348. .replace(/;/g, '\\;')
  349. .replace(/\|/g, '\\|')
  350. .replace(/\*/g, '\\*')
  351. .replace(/\?/g, '\\?')
  352. .replace(/`/g, '\\`')
  353. .replace(/\$/g, '\\$')
  354. .replace(/"/g, '\\"');
  355. await execAsync(`${adbPath} -s ${ipPort} shell input text "${escapedLine}"`, {
  356. timeout: 5000,
  357. maxBuffer: 1024 * 1024
  358. });
  359. }
  360. // 发送换行(除了最后一行)
  361. if (i < lines.length - 1) {
  362. await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
  363. timeout: 5000,
  364. maxBuffer: 1024 * 1024
  365. });
  366. }
  367. }
  368. } else {
  369. // 没有换行,直接发送
  370. const command = `${adbPath} -s ${ipPort} shell input text "${escapedText}"`;
  371. await execAsync(command, {
  372. timeout: 5000,
  373. maxBuffer: 1024 * 1024
  374. });
  375. }
  376. return { success: true };
  377. } catch (error) {
  378. console.error('发送文字失败:', error.message);
  379. return { success: false, error: error.message };
  380. }
  381. });
  382. // IPC 处理程序:发送按键事件到设备
  383. ipcMain.handle('send-key-event', async (event, ipPort, keyCode) => {
  384. if (!ipPort) {
  385. return { success: false, error: '缺少设备 ID' };
  386. }
  387. if (typeof keyCode !== 'string') {
  388. return { success: false, error: '按键代码必须是字符串' };
  389. }
  390. try {
  391. const adbPath = getCachedAdbPath();
  392. const command = `${adbPath} -s ${ipPort} shell input keyevent ${keyCode}`;
  393. await execAsync(command, {
  394. timeout: 5000,
  395. maxBuffer: 1024 * 1024
  396. });
  397. return { success: true };
  398. } catch (error) {
  399. console.error('发送按键失败:', error.message);
  400. return { success: false, error: error.message };
  401. }
  402. });
  403. // 应用启动逻辑:设置 CSP、创建窗口、监听激活事件
  404. app.whenReady().then(() => {
  405. setContentSecurityPolicy();
  406. createWindow();
  407. app.on('activate', () => {
  408. if (BrowserWindow.getAllWindows().length === 0) {
  409. createWindow();
  410. }
  411. });
  412. });
  413. // 应用关闭逻辑:macOS 保持运行,其他平台退出
  414. app.on('window-all-closed', () => {
  415. if (process.platform !== 'darwin') {
  416. app.quit();
  417. }
  418. });