adb-interact.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env node
  2. const { execSync, spawnSync } = require('child_process')
  3. const path = require('path')
  4. const fs = require('fs')
  5. const configPath = process.env.STATIC_ROOT
  6. ? path.join(path.dirname(process.env.STATIC_ROOT), 'config.js')
  7. : path.join(__dirname, '..', '..', 'config.js')
  8. const projectRoot = path.dirname(path.resolve(configPath))
  9. const config = fs.existsSync(configPath) ? require(configPath) : {}
  10. /** 从 config 解析并返回 adb 可执行文件路径 */
  11. function getAdbPath() {
  12. const p = config.adbPath?.path
  13. if (p) return path.isAbsolute(p) ? p : path.resolve(projectRoot, p)
  14. return path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
  15. }
  16. /** 根据 deviceId 返回 adb -s 参数前缀,无设备时返回空串 */
  17. function getDeviceFlag(deviceId) {
  18. return deviceId && deviceId.includes(':') ? `-s ${deviceId} ` : ''
  19. }
  20. /** 执行 adb shell 命令 */
  21. function runShell(adbPath, deviceFlag, shellArgs, timeout = 1000) {
  22. const quoted = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
  23. execSync(`${quoted} ${deviceFlag}shell ${shellArgs}`, { encoding: 'utf-8', timeout })
  24. }
  25. /** 执行 adb shell 并返回标准输出 */
  26. function runShellWithOutput(adbPath, deviceFlag, shellArgs, timeout = 1000) {
  27. const quoted = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
  28. return execSync(`${quoted} ${deviceFlag}shell ${shellArgs}`, { encoding: 'utf-8', timeout })
  29. }
  30. /** 处理 tap:点击坐标(超时放宽以便 WiFi 连接时能完成) */
  31. function handleTap(adbPath, argv) {
  32. const coordX = argv[3]
  33. const coordY = argv[4]
  34. const deviceId = argv[5] || ''
  35. if (!coordX || !coordY) process.exit(1)
  36. const flag = getDeviceFlag(deviceId)
  37. runShell(adbPath, flag, `input tap ${coordX} ${coordY}`, 8000)
  38. }
  39. /** 根据方向与距离计算滑动的起止坐标(基于屏幕中心),返回 [startX, startY, endX, endY] */
  40. function getSwipeCoordsByDirection(direction, distance, centerX, centerY) {
  41. const half = Math.floor(distance / 2)
  42. switch (direction) {
  43. case 'up-down':
  44. return [centerX, centerY - half, centerX, centerY + half]
  45. case 'down-up':
  46. return [centerX, centerY + half, centerX, centerY - half]
  47. case 'left-right':
  48. return [centerX - half, centerY, centerX + half, centerY]
  49. case 'right-left':
  50. return [centerX + half, centerY, centerX - half, centerY]
  51. default:
  52. process.exit(1)
  53. }
  54. }
  55. /** 处理 swipe:按方向与距离从屏幕中心滑动 */
  56. function handleSwipe(adbPath, argv) {
  57. const direction = argv[3]
  58. const distance = parseInt(argv[4], 10)
  59. const duration = parseInt(argv[5], 10)
  60. const deviceId = argv[6] || ''
  61. if (!direction || !distance || !duration) process.exit(1)
  62. const flag = getDeviceFlag(deviceId)
  63. const sizeOut = runShellWithOutput(adbPath, flag, 'wm size', 1000)
  64. const sizeMatch = sizeOut.match(/(\d+)x(\d+)/)
  65. const centerX = Math.floor(parseInt(sizeMatch[1], 10) / 2)
  66. const centerY = Math.floor(parseInt(sizeMatch[2], 10) / 2)
  67. const [startX, startY, endX, endY] = getSwipeCoordsByDirection(direction, distance, centerX, centerY)
  68. runShell(adbPath, flag, `input swipe ${startX} ${startY} ${endX} ${endY} ${duration}`, 2000)
  69. }
  70. /** 处理 swipe-coords:按坐标滑动 */
  71. function handleSwipeCoords(adbPath, argv) {
  72. const x1 = argv[3]
  73. const y1 = argv[4]
  74. const x2 = argv[5]
  75. const y2 = argv[6]
  76. const duration = argv[7] || 300
  77. const deviceId = argv[8] || ''
  78. if (!x1 || !y1 || !x2 || !y2) process.exit(1)
  79. const flag = getDeviceFlag(deviceId)
  80. runShell(adbPath, flag, `input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`, 2000)
  81. }
  82. /** 处理 keyevent:按键 */
  83. function handleKeyevent(adbPath, argv) {
  84. const keyCode = argv[3]
  85. const deviceId = argv[4] || ''
  86. if (!keyCode) process.exit(1)
  87. const flag = getDeviceFlag(deviceId)
  88. runShell(adbPath, flag, `input keyevent ${keyCode}`, 1000)
  89. }
  90. /** 简单 input text(供 type:input 等调用;adb method:input 的完整逻辑在 ef-compiler/actions/adb/input.js) */
  91. function handleText(adbPath, argv) {
  92. const text = argv[3]
  93. const deviceId = argv[4] || ''
  94. if (text === undefined) process.exit(1)
  95. const str = String(text)
  96. const shellArgs = deviceId && deviceId.includes(':') ? ['-s', deviceId, 'shell', 'input', 'text', str] : ['shell', 'input', 'text', str]
  97. const r = spawnSync(adbPath, shellArgs, { encoding: 'utf-8', timeout: 5000 })
  98. if (r.status !== 0) {
  99. const err = new Error(r.stderr || r.stdout || 'input text failed')
  100. err.status = r.status
  101. throw err
  102. }
  103. }
  104. const action = process.argv[2]
  105. if (!action) process.exit(1)
  106. const adbPath = getAdbPath()
  107. switch (action) {
  108. case 'tap':
  109. handleTap(adbPath, process.argv)
  110. break
  111. case 'swipe':
  112. handleSwipe(adbPath, process.argv)
  113. break
  114. case 'swipe-coords':
  115. handleSwipeCoords(adbPath, process.argv)
  116. break
  117. case 'keyevent':
  118. handleKeyevent(adbPath, process.argv)
  119. break
  120. case 'text':
  121. handleText(adbPath, process.argv)
  122. break
  123. default:
  124. process.exit(1)
  125. }
  126. process.exit(0)