input.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import { ipcMain } from 'electron';
  2. import { exec } from 'child_process';
  3. import { promisify } from 'util';
  4. import { join, dirname } from 'path';
  5. import { fileURLToPath } from 'url';
  6. import { getCachedAdbPath } from '../config.js';
  7. const execAsync = promisify(exec);
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = dirname(__filename);
  10. // 转义文字中的特殊字符
  11. function escapeText(text) {
  12. return text
  13. .replace(/\\/g, '\\\\') // 先转义反斜杠
  14. .replace(/'/g, "'\\''") // 转义单引号
  15. .replace(/ /g, '%s') // 空格转换为 %s
  16. .replace(/&/g, '\\&') // 转义 &
  17. .replace(/</g, '\\<') // 转义 <
  18. .replace(/>/g, '\\>') // 转义 >
  19. .replace(/\(/g, '\\(') // 转义 (
  20. .replace(/\)/g, '\\)') // 转义 )
  21. .replace(/;/g, '\\;') // 转义 ;
  22. .replace(/\|/g, '\\|') // 转义 |
  23. .replace(/\*/g, '\\*') // 转义 *
  24. .replace(/\?/g, '\\?') // 转义 ?
  25. .replace(/`/g, '\\`') // 转义 `
  26. .replace(/\$/g, '\\$') // 转义 $
  27. .replace(/"/g, '\\"'); // 转义 "
  28. }
  29. // 设置剪贴板内容(尝试多种方法)
  30. async function setClipboard(adbPath, ipPort, text) {
  31. // 转义文本中的单引号
  32. const escapedText = text.replace(/'/g, "'\\''");
  33. // 方法1: 尝试使用 Clipper 应用(最常用)
  34. const clipperCmd = `${adbPath} -s ${ipPort} shell "am broadcast -a clipper.set -e text '${escapedText}'"`;
  35. try {
  36. await execAsync(clipperCmd, {
  37. timeout: 3000,
  38. maxBuffer: 1024 * 1024
  39. });
  40. // 等待剪贴板设置完成
  41. await new Promise(resolve => setTimeout(resolve, 300));
  42. return; // 成功,直接返回
  43. } catch (clipperError) {
  44. // Clipper 方法失败,尝试其他方法
  45. }
  46. // 方法2: 尝试使用 Termux 应用
  47. const termuxCmd = `${adbPath} -s ${ipPort} shell "echo -n '${escapedText}' | termux-clipboard-set"`;
  48. try {
  49. await execAsync(termuxCmd, {
  50. timeout: 3000,
  51. maxBuffer: 1024 * 1024
  52. });
  53. await new Promise(resolve => setTimeout(resolve, 300));
  54. return; // 成功,直接返回
  55. } catch (termuxError) {
  56. // Termux 方法失败,尝试 service call 方法
  57. }
  58. // 方法3: 尝试使用 service call clipboard (Android 10+,可能需要 root)
  59. // 将文本写入临时文件
  60. const tempFile = `/data/local/tmp/clip_${Date.now()}.txt`;
  61. const writeCmd = `${adbPath} -s ${ipPort} shell "echo -n '${escapedText}' > ${tempFile}"`;
  62. try {
  63. await execAsync(writeCmd, {
  64. timeout: 3000,
  65. maxBuffer: 1024 * 1024
  66. });
  67. // 尝试使用 service call clipboard 设置(Android 10+)
  68. // 注意:这个方法可能需要 root 权限或特定 Android 版本
  69. const serviceCallCmd = `${adbPath} -s ${ipPort} shell "service call clipboard 2 i32 1 i32 0 < ${tempFile}"`;
  70. await execAsync(serviceCallCmd, {
  71. timeout: 3000,
  72. maxBuffer: 1024 * 1024
  73. });
  74. // 清理临时文件
  75. await execAsync(`${adbPath} -s ${ipPort} shell rm ${tempFile}`, {
  76. timeout: 2000,
  77. maxBuffer: 1024 * 1024
  78. }).catch(() => {}); // 忽略清理错误
  79. await new Promise(resolve => setTimeout(resolve, 300));
  80. return; // 成功,直接返回
  81. } catch (serviceError) {
  82. // service call 方法也失败,清理临时文件
  83. await execAsync(`${adbPath} -s ${ipPort} shell rm ${tempFile}`, {
  84. timeout: 2000,
  85. maxBuffer: 1024 * 1024
  86. }).catch(() => {}); // 忽略清理错误
  87. }
  88. // 所有方法都失败,抛出错误
  89. throw new Error('所有剪贴板设置方法都失败。请确保设备已安装 Clipper 或 Termux 应用。');
  90. }
  91. // 执行粘贴操作
  92. async function performPaste(adbPath, ipPort) {
  93. await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_PASTE`, {
  94. timeout: 3000,
  95. maxBuffer: 1024 * 1024
  96. });
  97. }
  98. // 获取当前默认输入法
  99. async function getCurrentInputMethod(adbPath, ipPort) {
  100. try {
  101. const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell settings get secure default_input_method`, {
  102. timeout: 3000,
  103. maxBuffer: 1024 * 1024
  104. });
  105. return stdout.trim();
  106. } catch (error) {
  107. return null;
  108. }
  109. }
  110. // 安装 ADBKeyboard APK
  111. async function installADBKeyboard(adbPath, ipPort) {
  112. try {
  113. const apkPath = join(__dirname, '..', 'static', 'ADBKeyboard.apk');
  114. const command = `${adbPath} -s ${ipPort} install "${apkPath}"`;
  115. const { stdout, stderr } = await execAsync(command, {
  116. timeout: 30000, // 安装可能需要较长时间
  117. maxBuffer: 1024 * 1024
  118. });
  119. // 检查安装结果
  120. if (stdout.includes('Success') || stdout.includes('success')) {
  121. // 等待安装完成
  122. await new Promise(resolve => setTimeout(resolve, 1000));
  123. return true;
  124. } else if (stdout.includes('already installed') || stdout.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
  125. return true;
  126. } else {
  127. return false;
  128. }
  129. } catch (error) {
  130. return false;
  131. }
  132. }
  133. // 检查 ADBKeyBoard 是否已安装
  134. async function isADBKeyboardInstalled(adbPath, ipPort) {
  135. try {
  136. const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell pm list packages | grep adbkeyboard`, {
  137. timeout: 3000,
  138. maxBuffer: 1024 * 1024
  139. });
  140. return stdout.trim().length > 0;
  141. } catch (error) {
  142. return false;
  143. }
  144. }
  145. // 检查并启用 ADBKeyBoard 输入法,返回之前的输入法
  146. async function ensureADBKeyBoardEnabled(adbPath, ipPort) {
  147. let previousIME = null;
  148. try {
  149. // 保存当前输入法
  150. previousIME = await getCurrentInputMethod(adbPath, ipPort);
  151. // 先启用 ADBKeyBoard
  152. await execAsync(`${adbPath} -s ${ipPort} shell ime enable com.android.adbkeyboard/.AdbIME`, {
  153. timeout: 3000,
  154. maxBuffer: 1024 * 1024
  155. });
  156. // 设置为默认输入法
  157. await execAsync(`${adbPath} -s ${ipPort} shell ime set com.android.adbkeyboard/.AdbIME`, {
  158. timeout: 3000,
  159. maxBuffer: 1024 * 1024
  160. });
  161. // 等待设置完成
  162. await new Promise(resolve => setTimeout(resolve, 200));
  163. } catch (error) {
  164. // 如果启用失败,检查是否是未安装的问题
  165. const errorMsg = error.message.toLowerCase();
  166. if (errorMsg.includes('unknown input method') || errorMsg.includes('cannot be enabled')) {
  167. // 可能是未安装,尝试安装
  168. const isInstalled = await isADBKeyboardInstalled(adbPath, ipPort);
  169. if (!isInstalled) {
  170. const installSuccess = await installADBKeyboard(adbPath, ipPort);
  171. if (installSuccess) {
  172. // 安装成功后,重试启用
  173. try {
  174. await execAsync(`${adbPath} -s ${ipPort} shell ime enable com.android.adbkeyboard/.AdbIME`, {
  175. timeout: 3000,
  176. maxBuffer: 1024 * 1024
  177. });
  178. await execAsync(`${adbPath} -s ${ipPort} shell ime set com.android.adbkeyboard/.AdbIME`, {
  179. timeout: 3000,
  180. maxBuffer: 1024 * 1024
  181. });
  182. await new Promise(resolve => setTimeout(resolve, 200));
  183. } catch (retryError) {
  184. // 安装后启用 ADBKeyBoard 仍然失败
  185. }
  186. }
  187. }
  188. }
  189. }
  190. return previousIME;
  191. }
  192. // 恢复之前的输入法
  193. async function restoreInputMethod(adbPath, ipPort, previousIME) {
  194. if (!previousIME) {
  195. return;
  196. }
  197. try {
  198. // 如果之前的输入法不是 ADBKeyBoard,则恢复
  199. if (previousIME !== 'com.android.adbkeyboard/.AdbIME') {
  200. await execAsync(`${adbPath} -s ${ipPort} shell ime set ${previousIME}`, {
  201. timeout: 3000,
  202. maxBuffer: 1024 * 1024
  203. });
  204. // 等待恢复完成
  205. await new Promise(resolve => setTimeout(resolve, 200));
  206. }
  207. } catch (error) {
  208. // 恢复输入法失败
  209. }
  210. }
  211. // 使用 ADBKeyBoard 输入法发送文本(支持中文,不需要root)
  212. // 参考教程: https://github.com/senzhk/ADBKeyBoard
  213. // previousIME: 之前的输入法(用于恢复),如果为 null 则会在函数内部获取
  214. async function sendTextViaADBKeyBoard(adbPath, ipPort, text, previousIME = null) {
  215. if (!text || text.length === 0) {
  216. return;
  217. }
  218. // 如果是第一次调用(previousIME 为 null),保存并启用 ADBKeyBoard
  219. let shouldRestore = false;
  220. if (previousIME === null) {
  221. previousIME = await ensureADBKeyBoardEnabled(adbPath, ipPort);
  222. shouldRestore = true;
  223. }
  224. try {
  225. // ADBKeyBoard 对单次发送的文本长度有限制,如果文本太长需要分段发送
  226. const MAX_CHUNK_SIZE = 100; // 每次最多发送100个字符
  227. // 如果文本长度超过限制,分段发送
  228. if (text.length > MAX_CHUNK_SIZE) {
  229. const chunks = [];
  230. for (let i = 0; i < text.length; i += MAX_CHUNK_SIZE) {
  231. chunks.push(text.slice(i, i + MAX_CHUNK_SIZE));
  232. }
  233. // 分段发送时,传递 previousIME,避免重复保存和恢复
  234. for (let i = 0; i < chunks.length; i++) {
  235. await sendTextViaADBKeyBoard(adbPath, ipPort, chunks[i], previousIME);
  236. // 每段之间稍作延迟
  237. if (i < chunks.length - 1) {
  238. await new Promise(resolve => setTimeout(resolve, 150));
  239. }
  240. }
  241. return;
  242. }
  243. // 方法1: 优先使用 Base64 编码方式(最可靠,可以处理所有特殊字符和中文)
  244. try {
  245. // 将文本转换为 Base64
  246. const base64Text = Buffer.from(text, 'utf8').toString('base64');
  247. // 使用 ADB_INPUT_B64 广播发送 Base64 编码的文本
  248. // 使用双引号包裹 Base64 字符串(与 CMD 测试一致)
  249. const b64Command = `${adbPath} -s ${ipPort} shell am broadcast -a ADB_INPUT_B64 --es msg "${base64Text}"`;
  250. const { stdout, stderr } = await execAsync(b64Command, {
  251. timeout: 5000,
  252. maxBuffer: 1024 * 1024
  253. });
  254. // 检查是否有错误输出(忽略正常的 Broadcasting 信息)
  255. if (stderr && stderr.trim() && !stderr.includes('Broadcasting') && !stderr.includes('broadcast')) {
  256. throw new Error(stderr);
  257. }
  258. // 等待输入完成(ADBKeyBoard 需要时间处理)
  259. await new Promise(resolve => setTimeout(resolve, 400));
  260. return; // 成功,直接返回
  261. } catch (b64Error) {
  262. // ADB_INPUT_B64 方法失败,尝试 ADB_INPUT_TEXT
  263. }
  264. // 方法2: 使用直接文本方式(如果 Base64 失败)
  265. try {
  266. // 对于 ADB_INPUT_TEXT,需要转义双引号
  267. // 使用双引号包裹整个文本,内部双引号需要转义
  268. const escapedText = text.replace(/"/g, '\\"');
  269. // 使用 ADB_INPUT_TEXT 广播发送文本
  270. // 使用双引号包裹,内部双引号已转义
  271. const textCommand = `${adbPath} -s ${ipPort} shell am broadcast -a ADB_INPUT_TEXT --es msg "${escapedText}"`;
  272. const { stdout, stderr } = await execAsync(textCommand, {
  273. timeout: 5000,
  274. maxBuffer: 1024 * 1024
  275. });
  276. // 检查是否有错误输出
  277. if (stderr && stderr.trim() && !stderr.includes('Broadcasting') && !stderr.includes('broadcast')) {
  278. throw new Error(stderr);
  279. }
  280. // 等待输入完成
  281. await new Promise(resolve => setTimeout(resolve, 400));
  282. } catch (textError) {
  283. // 如果两种方式都失败,抛出错误
  284. throw new Error(`ADBKeyBoard 输入失败: ${textError.message}。请确保已安装并启用 ADBKeyBoard 输入法。`);
  285. }
  286. } finally {
  287. // 只在第一次调用时(shouldRestore 为 true)恢复之前的输入法
  288. if (shouldRestore) {
  289. await restoreInputMethod(adbPath, ipPort, previousIME);
  290. }
  291. }
  292. }
  293. // 发送文字到设备(优先使用最简单可靠的方法)
  294. // 参考最佳实践:https://github.com/senzhk/ADBKeyBoard
  295. export async function sendText(ipPort, text) {
  296. if (!ipPort) {
  297. return { success: false, error: '缺少设备 ID' };
  298. }
  299. if (typeof text !== 'string') {
  300. return { success: false, error: '文字必须是字符串' };
  301. }
  302. // 空文本直接返回成功
  303. if (text.trim().length === 0) {
  304. return { success: true };
  305. }
  306. try {
  307. const adbPath = getCachedAdbPath();
  308. // 检查是否包含非ASCII字符(如中文、emoji等)
  309. const hasNonASCII = /[^\x00-\x7F]/.test(text);
  310. // 方法1: 如果是纯ASCII字符且不含换行,优先使用 input text(最快最可靠,不需要任何应用)
  311. if (!hasNonASCII && !text.includes('\n')) {
  312. try {
  313. const escapedText = escapeText(text);
  314. const command = `${adbPath} -s ${ipPort} shell input text "${escapedText}"`;
  315. await execAsync(command, {
  316. timeout: 5000,
  317. maxBuffer: 1024 * 1024
  318. });
  319. return { success: true };
  320. } catch (error) {
  321. // input text 方法失败,继续尝试其他方法
  322. }
  323. }
  324. // 方法2: 优先尝试使用 ADBKeyBoard(支持中文、emoji、多行文本,不需要root)
  325. // ADBKeyBoard 是最可靠的中文输入方法,推荐使用
  326. try {
  327. if (text.includes('\n')) {
  328. // 多行文本:逐行发送
  329. const lines = text.split('\n');
  330. for (let i = 0; i < lines.length; i++) {
  331. if (lines[i] || lines[i] === '') {
  332. // 即使是空行也发送(保持格式)
  333. await sendTextViaADBKeyBoard(adbPath, ipPort, lines[i]);
  334. }
  335. // 发送换行(除了最后一行)
  336. if (i < lines.length - 1) {
  337. await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
  338. timeout: 3000,
  339. maxBuffer: 1024 * 1024
  340. });
  341. // 等待换行完成
  342. await new Promise(resolve => setTimeout(resolve, 200));
  343. }
  344. }
  345. } else {
  346. // 单行文本
  347. await sendTextViaADBKeyBoard(adbPath, ipPort, text);
  348. }
  349. return { success: true };
  350. } catch (adbKeyboardError) {
  351. // 检查错误信息,如果是未安装相关的错误,尝试自动安装
  352. const errorMsg = adbKeyboardError.message.toLowerCase();
  353. if (errorMsg.includes('adbkeyboard') || errorMsg.includes('未安装') || errorMsg.includes('unknown input method')) {
  354. const isInstalled = await isADBKeyboardInstalled(adbPath, ipPort);
  355. if (!isInstalled) {
  356. const installSuccess = await installADBKeyboard(adbPath, ipPort);
  357. if (installSuccess) {
  358. // 安装成功后,重试发送文字
  359. try {
  360. if (text.includes('\n')) {
  361. const lines = text.split('\n');
  362. for (let i = 0; i < lines.length; i++) {
  363. if (lines[i] || lines[i] === '') {
  364. await sendTextViaADBKeyBoard(adbPath, ipPort, lines[i]);
  365. }
  366. if (i < lines.length - 1) {
  367. await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
  368. timeout: 3000,
  369. maxBuffer: 1024 * 1024
  370. });
  371. await new Promise(resolve => setTimeout(resolve, 200));
  372. }
  373. }
  374. } else {
  375. await sendTextViaADBKeyBoard(adbPath, ipPort, text);
  376. }
  377. return { success: true };
  378. } catch (retryError) {
  379. // 安装后重试 ADBKeyBoard 仍然失败
  380. }
  381. }
  382. }
  383. }
  384. // 如果 ADBKeyBoard 不可用,且是纯 ASCII,尝试回退到 input text
  385. if (!hasNonASCII) {
  386. try {
  387. if (text.includes('\n')) {
  388. // 多行文本:逐行发送
  389. const lines = text.split('\n');
  390. for (let i = 0; i < lines.length; i++) {
  391. if (lines[i]) {
  392. const escapedLine = escapeText(lines[i]);
  393. await execAsync(`${adbPath} -s ${ipPort} shell input text "${escapedLine}"`, {
  394. timeout: 5000,
  395. maxBuffer: 1024 * 1024
  396. });
  397. }
  398. // 发送换行(除了最后一行)
  399. if (i < lines.length - 1) {
  400. await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
  401. timeout: 3000,
  402. maxBuffer: 1024 * 1024
  403. });
  404. await new Promise(resolve => setTimeout(resolve, 200));
  405. }
  406. }
  407. } else {
  408. const escapedText = escapeText(text);
  409. await execAsync(`${adbPath} -s ${ipPort} shell input text "${escapedText}"`, {
  410. timeout: 5000,
  411. maxBuffer: 1024 * 1024
  412. });
  413. }
  414. return { success: true };
  415. } catch (fallbackError) {
  416. // input text 回退方法也失败
  417. }
  418. }
  419. // 如果失败,继续尝试剪贴板方式
  420. }
  421. // 方法3: 使用剪贴板方式(需要安装 Clipper 或 Termux 应用,不需要root)
  422. // 这是最后的备选方案
  423. try {
  424. if (text.includes('\n')) {
  425. // 多行文本:逐行处理
  426. const lines = text.split('\n');
  427. for (let i = 0; i < lines.length; i++) {
  428. if (lines[i] || lines[i] === '') {
  429. // 设置当前行到剪贴板
  430. await setClipboard(adbPath, ipPort, lines[i]);
  431. // 执行粘贴操作
  432. await performPaste(adbPath, ipPort);
  433. }
  434. // 如果不是最后一行,发送换行
  435. if (i < lines.length - 1) {
  436. await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
  437. timeout: 3000,
  438. maxBuffer: 1024 * 1024
  439. });
  440. // 等待换行完成
  441. await new Promise(resolve => setTimeout(resolve, 200));
  442. }
  443. }
  444. } else {
  445. // 单行文本
  446. // 设置剪贴板
  447. await setClipboard(adbPath, ipPort, text);
  448. // 执行粘贴操作
  449. await performPaste(adbPath, ipPort);
  450. }
  451. return { success: true };
  452. } catch (clipboardError) {
  453. // 所有方法都失败
  454. return {
  455. success: false,
  456. error: `输入失败:${hasNonASCII ? '对于中文输入,请安装并启用 ADBKeyBoard 输入法(推荐,无需root)。或者安装 Clipper/Termux 应用。' : '请检查设备连接和输入法设置。'}`
  457. };
  458. }
  459. } catch (error) {
  460. return { success: false, error: error.message };
  461. }
  462. }
  463. // 发送按键事件到设备
  464. export async function sendKeyEvent(ipPort, keyCode) {
  465. if (!ipPort) {
  466. return { success: false, error: '缺少设备 ID' };
  467. }
  468. if (typeof keyCode !== 'string') {
  469. return { success: false, error: '按键代码必须是字符串' };
  470. }
  471. try {
  472. const adbPath = getCachedAdbPath();
  473. const command = `${adbPath} -s ${ipPort} shell input keyevent ${keyCode}`;
  474. await execAsync(command, {
  475. timeout: 5000,
  476. maxBuffer: 1024 * 1024
  477. });
  478. return { success: true };
  479. } catch (error) {
  480. return { success: false, error: error.message };
  481. }
  482. }
  483. // 注册 IPC 处理器
  484. export function registerIpcHandlers() {
  485. // IPC 处理程序:发送文字到设备
  486. ipcMain.handle('send-text', async (event, ipPort, text) => {
  487. return await sendText(ipPort, text);
  488. });
  489. // IPC 处理程序:发送按键事件到设备
  490. ipcMain.handle('send-key-event', async (event, ipPort, keyCode) => {
  491. return await sendKeyEvent(ipPort, keyCode);
  492. });
  493. }