| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- 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, '\\$') // 转义 $
- .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);
- });
- }
|