|
@@ -1,490 +1,30 @@
|
|
|
-import { app, BrowserWindow, session, ipcMain } from 'electron';
|
|
|
|
|
|
|
+import { app, BrowserWindow, session } from 'electron';
|
|
|
import { fileURLToPath } from 'url';
|
|
import { fileURLToPath } from 'url';
|
|
|
import path from 'path';
|
|
import path from 'path';
|
|
|
-import { exec } from 'child_process';
|
|
|
|
|
-import { promisify } from 'util';
|
|
|
|
|
-import { existsSync, readFileSync } from 'fs';
|
|
|
|
|
-
|
|
|
|
|
-const execAsync = promisify(exec);
|
|
|
|
|
|
|
+import { setContentSecurityPolicy } from './main-js/security.js';
|
|
|
|
|
+import { createWindow } from './main-js/window-setup.js';
|
|
|
|
|
+import { registerIpcHandlers as registerDeviceManagerHandlers } from './main-js/adb/device-manager.js';
|
|
|
|
|
+import { registerIpcHandlers as registerDeviceInfoHandlers } from './main-js/adb/device-info.js';
|
|
|
|
|
+import { registerIpcHandlers as registerScreenshotHandlers } from './main-js/adb/screenshot.js';
|
|
|
|
|
+import { registerIpcHandlers as registerInputHandlers } from './main-js/adb/input.js';
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
const __dirname = path.dirname(__filename);
|
|
const __dirname = path.dirname(__filename);
|
|
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
|
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, '\\$') // 转义 $
|
|
|
|
|
- .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, '\\$')
|
|
|
|
|
- .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、创建窗口、监听激活事件
|
|
// 应用启动逻辑:设置 CSP、创建窗口、监听激活事件
|
|
|
app.whenReady().then(() => {
|
|
app.whenReady().then(() => {
|
|
|
- setContentSecurityPolicy();
|
|
|
|
|
|
|
+ setContentSecurityPolicy(isDev);
|
|
|
createWindow();
|
|
createWindow();
|
|
|
|
|
|
|
|
|
|
+ // 注册所有 ADB 相关的 IPC 处理器
|
|
|
|
|
+ registerDeviceManagerHandlers();
|
|
|
|
|
+ registerDeviceInfoHandlers();
|
|
|
|
|
+ registerScreenshotHandlers();
|
|
|
|
|
+ registerInputHandlers();
|
|
|
|
|
+
|
|
|
app.on('activate', () => {
|
|
app.on('activate', () => {
|
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
|
createWindow();
|
|
createWindow();
|