input.js 21 KB

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