adb-parser.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /**
  2. * 语句:adb / keyevent / scroll / swipe / locate / click / press / input / ocr 统一入口,直接集成 adb 调用
  3. */
  4. const types = ['adb', 'keyevent', 'scroll', 'swipe', 'locate', 'click', 'press', 'input', 'ocr']
  5. /* ========== 滑动坐标计算 ========== */
  6. function calculateSwipeCoordinates(direction, width, height) {
  7. const margin = 0.15
  8. const swipeDistance = 0.7
  9. let x1, y1, x2, y2
  10. switch (direction) {
  11. case 'up-down':
  12. x1 = x2 = Math.round(width / 2)
  13. y1 = Math.round(height * margin)
  14. y2 = Math.round(height * (margin + swipeDistance))
  15. break
  16. case 'down-up':
  17. x1 = x2 = Math.round(width / 2)
  18. y1 = Math.round(height * (margin + swipeDistance))
  19. y2 = Math.round(height * margin)
  20. break
  21. case 'left-right':
  22. y1 = y2 = Math.round(height / 2)
  23. x1 = Math.round(width * margin)
  24. x2 = Math.round(width * (margin + swipeDistance))
  25. break
  26. case 'right-left':
  27. y1 = y2 = Math.round(height / 2)
  28. x1 = Math.round(width * (margin + swipeDistance))
  29. x2 = Math.round(width * margin)
  30. break
  31. default:
  32. throw new Error(`未知的滑动方向: ${direction}`)
  33. }
  34. return { x1, y1, x2, y2 }
  35. }
  36. /* ========== 解析入口(按 type 解析自身字段:inVars/outVars/method 等)========== */
  37. function parse(action, parseContext) {
  38. const type = action.type || 'adb'
  39. const { extractVarName, resolveValue } = parseContext
  40. const variableContext = parseContext.variableContext || {}
  41. const parsed = Object.assign({}, action, { type })
  42. if (type === 'adb') {
  43. parsed.method = action.method
  44. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
  45. parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
  46. parsed.target = action.target
  47. parsed.value = action.value
  48. parsed.variable = action.variable
  49. parsed.clear = action.clear || false
  50. return parsed
  51. }
  52. if (type === 'locate' || type === 'click') {
  53. return parsed
  54. }
  55. if (type === 'input') {
  56. parsed.clear = action.clear || false
  57. return parsed
  58. }
  59. if (type === 'ocr') {
  60. parsed.area = action.area
  61. parsed.avatar = resolveValue(action.avatar, variableContext)
  62. return parsed
  63. }
  64. return parsed
  65. }
  66. /* ========== 执行入口 ========== */
  67. async function execute(action, ctx) {
  68. const {
  69. device,
  70. folderPath,
  71. resolution,
  72. variableContext,
  73. api,
  74. extractVarName,
  75. resolveValue,
  76. logOutVars,
  77. DEFAULT_SCROLL_DISTANCE = 100,
  78. } = ctx
  79. /* --- type: locate 定位(image/text/coordinate)--- */
  80. if (action.type === 'locate') {
  81. const method = action.method || 'image'
  82. let position = null
  83. if (method === 'image') {
  84. const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}`
  85. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  86. const matchResult = await api.matchImageAndGetCoordinate(device, imagePath)
  87. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
  88. position = matchResult.clickPosition
  89. } else if (method === 'text') {
  90. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  91. const matchResult = await api.findTextAndGetCoordinate(device, action.target)
  92. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
  93. position = matchResult.clickPosition
  94. } else if (method === 'coordinate') {
  95. position = Array.isArray(action.target) ? { x: action.target[0], y: action.target[1] } : action.target
  96. }
  97. if (action.variable && position) variableContext[action.variable] = position
  98. return { success: true, result: position }
  99. }
  100. /* --- type: click 点击(position/image/text)--- */
  101. if (action.type === 'click') {
  102. const method = action.method || 'position'
  103. let position = null
  104. if (method === 'position') position = resolveValue(action.target, variableContext)
  105. else if (method === 'image') {
  106. const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}`
  107. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  108. const matchResult = await api.matchImageAndGetCoordinate(device, imagePath)
  109. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
  110. position = matchResult.clickPosition
  111. } else if (method === 'text') {
  112. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  113. const matchResult = await api.findTextAndGetCoordinate(device, action.target)
  114. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
  115. position = matchResult.clickPosition
  116. }
  117. if (!position?.x || !position?.y) return { success: false, error: '无法获取点击位置' }
  118. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  119. const tapResult = await api.sendTap(device, position.x, position.y)
  120. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  121. return { success: true }
  122. }
  123. /* --- type: press 按图点击 --- */
  124. if (action.type === 'press') {
  125. const imagePath = `${folderPath}/resources/${action.value}`
  126. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  127. const matchResult = await api.matchImageAndGetCoordinate(device, imagePath)
  128. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
  129. const { x, y } = matchResult.clickPosition
  130. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  131. const tapResult = await api.sendTap(device, x, y)
  132. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  133. return { success: true }
  134. }
  135. /* --- type: input 输入文本 --- */
  136. if (action.type === 'input') {
  137. let inputValue = resolveValue(action.value, variableContext)
  138. if (!inputValue && action.target) {
  139. const resolvedTarget = resolveValue(action.target, variableContext)
  140. if (resolvedTarget !== action.target || !action.target.includes(' ')) inputValue = resolvedTarget
  141. }
  142. if (!inputValue) return { success: false, error: '输入内容为空' }
  143. if (!api?.sendText) return { success: false, error: '输入 API 不可用' }
  144. if (action.clear) {
  145. for (let i = 0; i < 200; i++) {
  146. const clearResult = await api.sendKeyEvent(device, '67')
  147. if (!clearResult.success) break
  148. await new Promise((r) => setTimeout(r, 10))
  149. }
  150. await new Promise((r) => setTimeout(r, 200))
  151. }
  152. const textResult = await api.sendText(device, inputValue)
  153. if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error}` }
  154. return { success: true }
  155. }
  156. /* --- type: ocr 文字识别 --- */
  157. if (action.type === 'ocr') {
  158. if (!api?.ocrLastMessage) return { success: false, error: 'OCR API 不可用' }
  159. const method = action.method || 'full-screen'
  160. let avatarPath = null
  161. if (method === 'by-avatar' && action.avatar) {
  162. const avatarName = resolveValue(action.avatar, variableContext)
  163. if (avatarName) {
  164. const folderName = folderPath.split(/[/\\]/).pop()
  165. avatarPath = `${folderName}/${avatarName}`
  166. }
  167. }
  168. const ocrResult = await api.ocrLastMessage(device, method, avatarPath, action.area, folderPath)
  169. if (!ocrResult.success) return { success: false, error: `OCR识别失败: ${ocrResult.error}` }
  170. if (action.variable) variableContext[action.variable] = ocrResult.text || ''
  171. return { success: true, text: ocrResult.text, position: ocrResult.position }
  172. }
  173. /* --- type: keyevent 按键 --- */
  174. if (action.type === 'keyevent') {
  175. let keyCode = null
  176. const inVars = action.inVars || []
  177. if (inVars.length > 0) {
  178. const keyVar = extractVarName(inVars[0])
  179. keyCode = variableContext[keyVar] || keyVar
  180. } else if (action.value) {
  181. keyCode = resolveValue(action.value, variableContext)
  182. }
  183. if (!keyCode) return { success: false, error: 'keyevent 操作缺少按键代码参数' }
  184. if (keyCode === 'KEYCODE_BACK') keyCode = '4'
  185. const keyResult = api.sendSystemKey(device, String(keyCode))
  186. if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error}` }
  187. return { success: true }
  188. }
  189. /* --- type: scroll 滚动 --- */
  190. if (action.type === 'scroll') {
  191. if (!api.sendScroll) return { success: false, error: '滚动 API 不可用' }
  192. const direction = action.value
  193. const r = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500)
  194. if (!r.success) return { success: false, error: `滚动失败: ${r.error}` }
  195. return { success: true }
  196. }
  197. /* --- type: swipe 滑动 --- */
  198. if (action.type === 'swipe') {
  199. if (!api.sendSwipe) return { success: false, error: '滑动 API 不可用' }
  200. const { x1, y1, x2, y2 } = calculateSwipeCoordinates(action.value, resolution.width, resolution.height)
  201. const r = await api.sendSwipe(device, x1, y1, x2, y2, 300)
  202. if (!r.success) return { success: false, error: `滑动失败: ${r.error}` }
  203. return { success: true }
  204. }
  205. /* --- type: adb 按 method 分发(inVars/outVars)--- */
  206. const method = action.method
  207. if (!method) return { success: false, error: 'adb 操作缺少 method 参数' }
  208. const inVars = action.inVars || []
  209. const outVars = action.outVars || []
  210. switch (method) {
  211. case 'input': {
  212. let inputValue = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null
  213. if (!inputValue && action.value) inputValue = resolveValue(action.value, variableContext)
  214. if (!inputValue) return { success: false, error: 'input 操作缺少输入内容' }
  215. if (action.clear) {
  216. for (let i = 0; i < 200; i++) {
  217. const clearResult = await api.sendKeyEvent(device, '67')
  218. if (!clearResult.success) break
  219. await new Promise((r) => setTimeout(r, 10))
  220. }
  221. await new Promise((r) => setTimeout(r, 200))
  222. }
  223. if (!api?.sendText) return { success: false, error: '输入 API 不可用' }
  224. const textResult = await api.sendText(device, String(inputValue))
  225. if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error}` }
  226. return { success: true }
  227. }
  228. case 'click': {
  229. let position = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null
  230. if (!position && action.target) position = resolveValue(action.target, variableContext)
  231. if (!position) return { success: false, error: 'click 操作缺少位置参数' }
  232. if (typeof position === 'string') {
  233. if (position === '') return { success: false, error: 'click 操作缺少位置参数(位置变量为空)' }
  234. try {
  235. position = JSON.parse(position)
  236. } catch (e) {
  237. const parts = position.split(',')
  238. if (parts.length === 2) {
  239. const x = parseFloat(parts[0].trim())
  240. const y = parseFloat(parts[1].trim())
  241. if (!isNaN(x) && !isNaN(y)) position = { x: Math.round(x), y: Math.round(y) }
  242. else return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` }
  243. } else return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` }
  244. }
  245. }
  246. if (Array.isArray(position) && position.length >= 2) position = { x: position[0], y: position[1] }
  247. if (position?.topLeft && position?.bottomRight) {
  248. position = {
  249. x: Math.round((position.topLeft.x + position.bottomRight.x) / 2),
  250. y: Math.round((position.topLeft.y + position.bottomRight.y) / 2),
  251. }
  252. }
  253. if (!position || typeof position !== 'object' || position.x === undefined || position.y === undefined)
  254. return { success: false, error: 'click 操作的位置格式错误,需要 {x, y} 对象' }
  255. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  256. const tapResult = await api.sendTap(device, position.x, position.y)
  257. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  258. return { success: true }
  259. }
  260. case 'locate': {
  261. const locateMethod = action.method || action.targetMethod || 'image'
  262. let position = null
  263. if (locateMethod === 'image') {
  264. let imagePath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.target
  265. if (!imagePath) return { success: false, error: 'locate 操作(image)缺少图片路径' }
  266. const fullPath = imagePath.startsWith('/') || imagePath.includes(':') ? imagePath : `${folderPath}/resources/${imagePath}`
  267. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  268. const matchResult = await api.matchImageAndGetCoordinate(device, fullPath)
  269. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
  270. position = matchResult.clickPosition
  271. } else if (locateMethod === 'text') {
  272. const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.target
  273. if (!targetText) return { success: false, error: 'locate 操作(text)缺少文字内容' }
  274. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  275. const matchResult = await api.findTextAndGetCoordinate(device, targetText)
  276. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
  277. position = matchResult.clickPosition
  278. } else if (locateMethod === 'coordinate') {
  279. const coord = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : resolveValue(action.target, variableContext)
  280. if (!coord) return { success: false, error: 'locate 操作(coordinate)缺少坐标' }
  281. position = Array.isArray(coord) ? { x: coord[0], y: coord[1] } : coord
  282. }
  283. if (outVars.length > 0) {
  284. variableContext[extractVarName(outVars[0])] = position
  285. await logOutVars(action, variableContext, folderPath)
  286. } else if (action.variable) variableContext[action.variable] = position
  287. return { success: true, result: position }
  288. }
  289. case 'swipe': {
  290. let direction = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext)
  291. if (!direction) return { success: false, error: 'swipe 操作缺少方向参数' }
  292. let x1, y1, x2, y2
  293. if (inVars.length >= 3) {
  294. const start = variableContext[extractVarName(inVars[1])]
  295. const end = variableContext[extractVarName(inVars[2])]
  296. if (start && end) {
  297. x1 = start.x ?? start[0]
  298. y1 = start.y ?? start[1]
  299. x2 = end.x ?? end[0]
  300. y2 = end.y ?? end[1]
  301. }
  302. }
  303. if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) {
  304. const coords = calculateSwipeCoordinates(direction, resolution.width, resolution.height)
  305. x1 = coords.x1
  306. y1 = coords.y1
  307. x2 = coords.x2
  308. y2 = coords.y2
  309. }
  310. if (!api?.sendSwipe) return { success: false, error: '滑动 API 不可用' }
  311. const swipeResult = await api.sendSwipe(device, x1, y1, x2, y2, 300)
  312. if (!swipeResult.success) return { success: false, error: `滑动失败: ${swipeResult.error}` }
  313. return { success: true }
  314. }
  315. case 'scroll': {
  316. const direction = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext)
  317. if (!direction) return { success: false, error: 'scroll 操作缺少方向参数' }
  318. if (!api?.sendScroll) return { success: false, error: '滚动 API 不可用' }
  319. const scrollResult = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500)
  320. if (!scrollResult.success) return { success: false, error: `滚动失败: ${scrollResult.error}` }
  321. return { success: true }
  322. }
  323. case 'keyevent': {
  324. let keyCode = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext)
  325. if (!keyCode) return { success: false, error: 'keyevent 操作缺少按键代码参数' }
  326. if (keyCode === 'KEYCODE_BACK') keyCode = '4'
  327. const keyResult = api.sendSystemKey(device, String(keyCode))
  328. if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error}` }
  329. return { success: true }
  330. }
  331. case 'press': {
  332. const imagePath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.value
  333. if (!imagePath) return { success: false, error: 'press 操作缺少图片路径' }
  334. const fullPath = imagePath.startsWith('/') || imagePath.includes(':') ? imagePath : `${folderPath}/${imagePath}`
  335. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  336. const matchResult = await api.matchImageAndGetCoordinate(device, fullPath)
  337. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` }
  338. const { x, y } = matchResult.clickPosition
  339. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  340. const tapResult = await api.sendTap(device, x, y)
  341. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  342. return { success: true }
  343. }
  344. case 'string-press': {
  345. const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.value
  346. if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' }
  347. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  348. const matchResult = await api.findTextAndGetCoordinate(device, targetText)
  349. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
  350. const { x, y } = matchResult.clickPosition
  351. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  352. const tapResult = await api.sendTap(device, x, y)
  353. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  354. return { success: true }
  355. }
  356. default:
  357. return { success: false, error: `未知的 adb method: ${method}` }
  358. }
  359. }
  360. /* ========== 导出 ========== */
  361. module.exports = { types, parse, execute }