import { ipcMain } from 'electron'; import { exec } from 'child_process'; import { promisify } from 'util'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { getCachedAdbPath } from '../config.js'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // 转义文字中的特殊字符 function escapeText(text) { return 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, '\\"'); // 转义 " } // 设置剪贴板内容(尝试多种方法) async function setClipboard(adbPath, ipPort, text) { // 转义文本中的单引号 const escapedText = text.replace(/'/g, "'\\''"); // 方法1: 尝试使用 Clipper 应用(最常用) const clipperCmd = `${adbPath} -s ${ipPort} shell "am broadcast -a clipper.set -e text '${escapedText}'"`; try { await execAsync(clipperCmd, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 等待剪贴板设置完成 await new Promise(resolve => setTimeout(resolve, 300)); return; // 成功,直接返回 } catch (clipperError) { // Clipper 方法失败,尝试其他方法 } // 方法2: 尝试使用 Termux 应用 const termuxCmd = `${adbPath} -s ${ipPort} shell "echo -n '${escapedText}' | termux-clipboard-set"`; try { await execAsync(termuxCmd, { timeout: 3000, maxBuffer: 1024 * 1024 }); await new Promise(resolve => setTimeout(resolve, 300)); return; // 成功,直接返回 } catch (termuxError) { // Termux 方法失败,尝试 service call 方法 } // 方法3: 尝试使用 service call clipboard (Android 10+,可能需要 root) // 将文本写入临时文件 const tempFile = `/data/local/tmp/clip_${Date.now()}.txt`; const writeCmd = `${adbPath} -s ${ipPort} shell "echo -n '${escapedText}' > ${tempFile}"`; try { await execAsync(writeCmd, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 尝试使用 service call clipboard 设置(Android 10+) // 注意:这个方法可能需要 root 权限或特定 Android 版本 const serviceCallCmd = `${adbPath} -s ${ipPort} shell "service call clipboard 2 i32 1 i32 0 < ${tempFile}"`; await execAsync(serviceCallCmd, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 清理临时文件 await execAsync(`${adbPath} -s ${ipPort} shell rm ${tempFile}`, { timeout: 2000, maxBuffer: 1024 * 1024 }).catch(() => {}); // 忽略清理错误 await new Promise(resolve => setTimeout(resolve, 300)); return; // 成功,直接返回 } catch (serviceError) { // service call 方法也失败,清理临时文件 await execAsync(`${adbPath} -s ${ipPort} shell rm ${tempFile}`, { timeout: 2000, maxBuffer: 1024 * 1024 }).catch(() => {}); // 忽略清理错误 } // 所有方法都失败,抛出错误 throw new Error('所有剪贴板设置方法都失败。请确保设备已安装 Clipper 或 Termux 应用。'); } // 执行粘贴操作 async function performPaste(adbPath, ipPort) { await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_PASTE`, { timeout: 3000, maxBuffer: 1024 * 1024 }); } // 获取当前默认输入法 async function getCurrentInputMethod(adbPath, ipPort) { try { const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell settings get secure default_input_method`, { timeout: 3000, maxBuffer: 1024 * 1024 }); return stdout.trim(); } catch (error) { return null; } } // 安装 ADBKeyboard APK async function installADBKeyboard(adbPath, ipPort) { try { const apkPath = join(__dirname, '..', 'static', 'ADBKeyboard.apk'); const command = `${adbPath} -s ${ipPort} install "${apkPath}"`; const { stdout, stderr } = await execAsync(command, { timeout: 30000, // 安装可能需要较长时间 maxBuffer: 1024 * 1024 }); // 检查安装结果 if (stdout.includes('Success') || stdout.includes('success')) { // 等待安装完成 await new Promise(resolve => setTimeout(resolve, 1000)); return true; } else if (stdout.includes('already installed') || stdout.includes('INSTALL_FAILED_ALREADY_EXISTS')) { return true; } else { return false; } } catch (error) { return false; } } // 检查 ADBKeyBoard 是否已安装 async function isADBKeyboardInstalled(adbPath, ipPort) { try { const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell pm list packages | grep adbkeyboard`, { timeout: 3000, maxBuffer: 1024 * 1024 }); return stdout.trim().length > 0; } catch (error) { return false; } } // 检查并启用 ADBKeyBoard 输入法,返回之前的输入法 async function ensureADBKeyBoardEnabled(adbPath, ipPort) { let previousIME = null; try { // 保存当前输入法 previousIME = await getCurrentInputMethod(adbPath, ipPort); // 先启用 ADBKeyBoard await execAsync(`${adbPath} -s ${ipPort} shell ime enable com.android.adbkeyboard/.AdbIME`, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 设置为默认输入法 await execAsync(`${adbPath} -s ${ipPort} shell ime set com.android.adbkeyboard/.AdbIME`, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 等待设置完成 await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { // 如果启用失败,检查是否是未安装的问题 const errorMsg = error.message.toLowerCase(); if (errorMsg.includes('unknown input method') || errorMsg.includes('cannot be enabled')) { // 可能是未安装,尝试安装 const isInstalled = await isADBKeyboardInstalled(adbPath, ipPort); if (!isInstalled) { const installSuccess = await installADBKeyboard(adbPath, ipPort); if (installSuccess) { // 安装成功后,重试启用 try { await execAsync(`${adbPath} -s ${ipPort} shell ime enable com.android.adbkeyboard/.AdbIME`, { timeout: 3000, maxBuffer: 1024 * 1024 }); await execAsync(`${adbPath} -s ${ipPort} shell ime set com.android.adbkeyboard/.AdbIME`, { timeout: 3000, maxBuffer: 1024 * 1024 }); await new Promise(resolve => setTimeout(resolve, 200)); } catch (retryError) { // 安装后启用 ADBKeyBoard 仍然失败 } } } } } return previousIME; } // 恢复之前的输入法 async function restoreInputMethod(adbPath, ipPort, previousIME) { if (!previousIME) { return; } try { // 如果之前的输入法不是 ADBKeyBoard,则恢复 if (previousIME !== 'com.android.adbkeyboard/.AdbIME') { await execAsync(`${adbPath} -s ${ipPort} shell ime set ${previousIME}`, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 等待恢复完成 await new Promise(resolve => setTimeout(resolve, 200)); } } catch (error) { // 恢复输入法失败 } } // 使用 ADBKeyBoard 输入法发送文本(支持中文,不需要root) // 参考教程: https://github.com/senzhk/ADBKeyBoard // previousIME: 之前的输入法(用于恢复),如果为 null 则会在函数内部获取 async function sendTextViaADBKeyBoard(adbPath, ipPort, text, previousIME = null) { if (!text || text.length === 0) { return; } // 如果是第一次调用(previousIME 为 null),保存并启用 ADBKeyBoard let shouldRestore = false; if (previousIME === null) { previousIME = await ensureADBKeyBoardEnabled(adbPath, ipPort); shouldRestore = true; } try { // ADBKeyBoard 对单次发送的文本长度有限制,如果文本太长需要分段发送 const MAX_CHUNK_SIZE = 100; // 每次最多发送100个字符 // 如果文本长度超过限制,分段发送 if (text.length > MAX_CHUNK_SIZE) { const chunks = []; for (let i = 0; i < text.length; i += MAX_CHUNK_SIZE) { chunks.push(text.slice(i, i + MAX_CHUNK_SIZE)); } // 分段发送时,传递 previousIME,避免重复保存和恢复 for (let i = 0; i < chunks.length; i++) { await sendTextViaADBKeyBoard(adbPath, ipPort, chunks[i], previousIME); // 每段之间稍作延迟 if (i < chunks.length - 1) { await new Promise(resolve => setTimeout(resolve, 150)); } } return; } // 方法1: 优先使用 Base64 编码方式(最可靠,可以处理所有特殊字符和中文) try { // 将文本转换为 Base64 const base64Text = Buffer.from(text, 'utf8').toString('base64'); // 使用 ADB_INPUT_B64 广播发送 Base64 编码的文本 // 使用双引号包裹 Base64 字符串(与 CMD 测试一致) const b64Command = `${adbPath} -s ${ipPort} shell am broadcast -a ADB_INPUT_B64 --es msg "${base64Text}"`; const { stdout, stderr } = await execAsync(b64Command, { timeout: 5000, maxBuffer: 1024 * 1024 }); // 检查是否有错误输出(忽略正常的 Broadcasting 信息) if (stderr && stderr.trim() && !stderr.includes('Broadcasting') && !stderr.includes('broadcast')) { throw new Error(stderr); } // 等待输入完成(ADBKeyBoard 需要时间处理) await new Promise(resolve => setTimeout(resolve, 400)); return; // 成功,直接返回 } catch (b64Error) { // ADB_INPUT_B64 方法失败,尝试 ADB_INPUT_TEXT } // 方法2: 使用直接文本方式(如果 Base64 失败) try { // 对于 ADB_INPUT_TEXT,需要转义双引号 // 使用双引号包裹整个文本,内部双引号需要转义 const escapedText = text.replace(/"/g, '\\"'); // 使用 ADB_INPUT_TEXT 广播发送文本 // 使用双引号包裹,内部双引号已转义 const textCommand = `${adbPath} -s ${ipPort} shell am broadcast -a ADB_INPUT_TEXT --es msg "${escapedText}"`; const { stdout, stderr } = await execAsync(textCommand, { timeout: 5000, maxBuffer: 1024 * 1024 }); // 检查是否有错误输出 if (stderr && stderr.trim() && !stderr.includes('Broadcasting') && !stderr.includes('broadcast')) { throw new Error(stderr); } // 等待输入完成 await new Promise(resolve => setTimeout(resolve, 400)); } catch (textError) { // 如果两种方式都失败,抛出错误 throw new Error(`ADBKeyBoard 输入失败: ${textError.message}。请确保已安装并启用 ADBKeyBoard 输入法。`); } } finally { // 只在第一次调用时(shouldRestore 为 true)恢复之前的输入法 if (shouldRestore) { await restoreInputMethod(adbPath, ipPort, previousIME); } } } // 发送文字到设备(优先使用最简单可靠的方法) // 参考最佳实践:https://github.com/senzhk/ADBKeyBoard export async function sendText(ipPort, text) { if (!ipPort) { return { success: false, error: '缺少设备 ID' }; } if (typeof text !== 'string') { return { success: false, error: '文字必须是字符串' }; } // 空文本直接返回成功 if (text.trim().length === 0) { return { success: true }; } try { const adbPath = getCachedAdbPath(); // 检查是否包含非ASCII字符(如中文、emoji等) const hasNonASCII = /[^\x00-\x7F]/.test(text); // 方法1: 如果是纯ASCII字符且不含换行,优先使用 input text(最快最可靠,不需要任何应用) if (!hasNonASCII && !text.includes('\n')) { try { const escapedText = escapeText(text); const command = `${adbPath} -s ${ipPort} shell input text "${escapedText}"`; await execAsync(command, { timeout: 5000, maxBuffer: 1024 * 1024 }); return { success: true }; } catch (error) { // input text 方法失败,继续尝试其他方法 } } // 方法2: 优先尝试使用 ADBKeyBoard(支持中文、emoji、多行文本,不需要root) // ADBKeyBoard 是最可靠的中文输入方法,推荐使用 try { if (text.includes('\n')) { // 多行文本:逐行发送 const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i] || lines[i] === '') { // 即使是空行也发送(保持格式) await sendTextViaADBKeyBoard(adbPath, ipPort, lines[i]); } // 发送换行(除了最后一行) if (i < lines.length - 1) { await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 等待换行完成 await new Promise(resolve => setTimeout(resolve, 200)); } } } else { // 单行文本 await sendTextViaADBKeyBoard(adbPath, ipPort, text); } return { success: true }; } catch (adbKeyboardError) { // 检查错误信息,如果是未安装相关的错误,尝试自动安装 const errorMsg = adbKeyboardError.message.toLowerCase(); if (errorMsg.includes('adbkeyboard') || errorMsg.includes('未安装') || errorMsg.includes('unknown input method')) { const isInstalled = await isADBKeyboardInstalled(adbPath, ipPort); if (!isInstalled) { const installSuccess = await installADBKeyboard(adbPath, ipPort); if (installSuccess) { // 安装成功后,重试发送文字 try { if (text.includes('\n')) { const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i] || lines[i] === '') { await sendTextViaADBKeyBoard(adbPath, ipPort, lines[i]); } if (i < lines.length - 1) { await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, { timeout: 3000, maxBuffer: 1024 * 1024 }); await new Promise(resolve => setTimeout(resolve, 200)); } } } else { await sendTextViaADBKeyBoard(adbPath, ipPort, text); } return { success: true }; } catch (retryError) { // 安装后重试 ADBKeyBoard 仍然失败 } } } } // 如果 ADBKeyBoard 不可用,且是纯 ASCII,尝试回退到 input text if (!hasNonASCII) { try { if (text.includes('\n')) { // 多行文本:逐行发送 const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i]) { const escapedLine = escapeText(lines[i]); 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: 3000, maxBuffer: 1024 * 1024 }); await new Promise(resolve => setTimeout(resolve, 200)); } } } else { const escapedText = escapeText(text); await execAsync(`${adbPath} -s ${ipPort} shell input text "${escapedText}"`, { timeout: 5000, maxBuffer: 1024 * 1024 }); } return { success: true }; } catch (fallbackError) { // input text 回退方法也失败 } } // 如果失败,继续尝试剪贴板方式 } // 方法3: 使用剪贴板方式(需要安装 Clipper 或 Termux 应用,不需要root) // 这是最后的备选方案 try { if (text.includes('\n')) { // 多行文本:逐行处理 const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i] || lines[i] === '') { // 设置当前行到剪贴板 await setClipboard(adbPath, ipPort, lines[i]); // 执行粘贴操作 await performPaste(adbPath, ipPort); } // 如果不是最后一行,发送换行 if (i < lines.length - 1) { await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, { timeout: 3000, maxBuffer: 1024 * 1024 }); // 等待换行完成 await new Promise(resolve => setTimeout(resolve, 200)); } } } else { // 单行文本 // 设置剪贴板 await setClipboard(adbPath, ipPort, text); // 执行粘贴操作 await performPaste(adbPath, ipPort); } return { success: true }; } catch (clipboardError) { // 所有方法都失败 return { success: false, error: `输入失败:${hasNonASCII ? '对于中文输入,请安装并启用 ADBKeyBoard 输入法(推荐,无需root)。或者安装 Clipper/Termux 应用。' : '请检查设备连接和输入法设置。'}` }; } } catch (error) { return { success: false, error: error.message }; } } // 发送按键事件到设备 export async function sendKeyEvent(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) { return { success: false, error: error.message }; } } // 注册 IPC 处理器 export function registerIpcHandlers() { // IPC 处理程序:发送文字到设备 ipcMain.handle('send-text', async (event, ipPort, text) => { return await sendText(ipPort, text); }); // IPC 处理程序:发送按键事件到设备 ipcMain.handle('send-key-event', async (event, ipPort, keyCode) => { return await sendKeyEvent(ipPort, keyCode); }); }