adb-parser.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /**
  2. * 语句:adb / keyevent / scroll / swipe / locate / click / press / input / ocr 统一入口
  3. * type===adb 时按 method 分发到 adb/*.js;其余 type 在本文件内执行,公共逻辑见 utils.js
  4. */
  5. const { calculateSwipeCoordinates } = require('./utils.js')
  6. const { assertStrictKeys } = require('../../../action-schema.js')
  7. const types = ['adb', 'keyevent', 'scroll', 'swipe', 'locate', 'click', 'press', 'input', 'ocr']
  8. const METHOD_HANDLERS = {
  9. input: require('./input.js'),
  10. click: require('./click.js'),
  11. locate: require('./locate.js'),
  12. swipe: require('./swipe.js'),
  13. scroll: require('./scroll.js'),
  14. keyevent: require('./keyevent.js'),
  15. press: require('./press.js'),
  16. 'string-press': require('./string-press.js'),
  17. 'send-img-to-device': require('./send-img-to-device.js'),
  18. }
  19. /* ========== 解析入口 ========== */
  20. function parse (action, parseContext) {
  21. const path = parseContext.actionPath || 'adb'
  22. const type = action.type || 'adb'
  23. const { extractVarName, resolveValue } = parseContext
  24. const variableContext = parseContext.variableContext || {}
  25. if (type === 'adb') {
  26. assertStrictKeys(action, ['type', 'method', 'inVars', 'outVars', 'target', 'value', 'variable', 'clear'], path)
  27. if (action.method == null || String(action.method).trim() === '') {
  28. throw new Error(`${path}: adb 须包含 method`)
  29. }
  30. if (!Array.isArray(action.inVars)) {
  31. throw new Error(`${path}: adb 须包含 inVars 数组`)
  32. }
  33. if (!Array.isArray(action.outVars)) {
  34. throw new Error(`${path}: adb 须包含 outVars 数组`)
  35. }
  36. return {
  37. type: 'adb',
  38. method: action.method,
  39. inVars: action.inVars.map((v) => extractVarName(v)),
  40. outVars: action.outVars.map((v) => extractVarName(v)),
  41. target: action.target,
  42. value: action.value,
  43. variable: action.variable,
  44. clear: action.clear === true,
  45. }
  46. }
  47. if (type === 'locate' || type === 'click') {
  48. assertStrictKeys(action, ['type', 'method', 'target', 'variable'], path)
  49. return {
  50. type,
  51. method: action.method,
  52. target: action.target,
  53. variable: action.variable,
  54. }
  55. }
  56. if (type === 'input') {
  57. assertStrictKeys(action, ['type', 'value', 'target', 'clear'], path)
  58. if (action.value === undefined || action.value === null || action.value === '') {
  59. throw new Error(`${path}: input 须包含 value`)
  60. }
  61. return {
  62. type: 'input',
  63. value: action.value,
  64. target: action.target,
  65. clear: action.clear === true,
  66. }
  67. }
  68. if (type === 'press') {
  69. assertStrictKeys(action, ['type', 'value'], path)
  70. if (action.value === undefined || action.value === null || action.value === '') {
  71. throw new Error(`${path}: press 须包含 value`)
  72. }
  73. return { type: 'press', value: action.value }
  74. }
  75. if (type === 'ocr') {
  76. assertStrictKeys(action, ['type', 'method', 'area', 'avatar', 'variable'], path)
  77. return {
  78. type: 'ocr',
  79. method: action.method,
  80. area: action.area,
  81. avatar: action.avatar != null ? resolveValue(action.avatar, variableContext) : undefined,
  82. variable: action.variable,
  83. }
  84. }
  85. if (type === 'keyevent') {
  86. assertStrictKeys(action, ['type', 'inVars'], path)
  87. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  88. throw new Error(`${path}: keyevent 须使用 inVars 且至少 1 项为键码`)
  89. }
  90. return {
  91. type: 'keyevent',
  92. inVars: action.inVars.map((v) => extractVarName(v)),
  93. }
  94. }
  95. if (type === 'scroll') {
  96. assertStrictKeys(action, ['type', 'value'], path)
  97. if (action.value === undefined || action.value === null || action.value === '') {
  98. throw new Error(`${path}: scroll 须包含 value(方向)`)
  99. }
  100. return { type: 'scroll', value: action.value }
  101. }
  102. if (type === 'swipe') {
  103. assertStrictKeys(action, ['type', 'value'], path)
  104. if (action.value === undefined || action.value === null || action.value === '') {
  105. throw new Error(`${path}: swipe 须包含 value`)
  106. }
  107. return { type: 'swipe', value: action.value }
  108. }
  109. throw new Error(`${path}: 未知的 adb-parser type: ${type}`)
  110. }
  111. /* ========== 执行入口 ========== */
  112. async function execute(action, ctx) {
  113. const {
  114. device,
  115. folderPath,
  116. resolution,
  117. variableContext,
  118. api,
  119. extractVarName,
  120. resolveValue,
  121. logOutVars,
  122. DEFAULT_SCROLL_DISTANCE = 100,
  123. } = ctx
  124. /* --- type: locate --- */
  125. if (action.type === 'locate') {
  126. const method = action.method || 'image'
  127. let position = null
  128. if (method === 'image') {
  129. const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}`
  130. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  131. const matchResult = await api.matchImageAndGetCoordinate(device, imagePath, folderPath)
  132. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` }
  133. position = matchResult.clickPosition
  134. } else if (method === 'text') {
  135. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  136. const matchResult = await api.findTextAndGetCoordinate(device, action.target)
  137. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` }
  138. position = matchResult.clickPosition
  139. } else if (method === 'coordinate') {
  140. position = Array.isArray(action.target) ? { x: action.target[0], y: action.target[1] } : action.target
  141. }
  142. if (action.variable && position) variableContext[action.variable] = position
  143. return { success: true, result: position }
  144. }
  145. /* --- type: click --- */
  146. if (action.type === 'click') {
  147. const method = action.method || 'position'
  148. let position = null
  149. if (method === 'position') position = resolveValue(action.target, variableContext)
  150. else if (method === 'image') {
  151. const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}`
  152. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  153. const matchResult = await api.matchImageAndGetCoordinate(device, imagePath, folderPath)
  154. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` }
  155. position = matchResult.clickPosition
  156. } else if (method === 'text') {
  157. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  158. const matchResult = await api.findTextAndGetCoordinate(device, action.target)
  159. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` }
  160. position = matchResult.clickPosition
  161. }
  162. if (!position?.x || !position?.y) return { success: false, error: '无法获取点击位置' }
  163. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  164. const tapResult = await api.sendTap(device, position.x, position.y)
  165. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error != null ? tapResult.error : 'unknown'}` }
  166. return { success: true }
  167. }
  168. /* --- type: press --- */
  169. if (action.type === 'press') {
  170. const imagePath = `${folderPath}/resources/${action.value}`
  171. if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' }
  172. const matchResult = await api.matchImageAndGetCoordinate(device, imagePath, folderPath)
  173. if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` }
  174. const { x, y } = matchResult.clickPosition
  175. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  176. const tapResult = await api.sendTap(device, x, y)
  177. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error != null ? tapResult.error : 'unknown'}` }
  178. return { success: true }
  179. }
  180. /* --- type: input --- */
  181. if (action.type === 'input') {
  182. let inputValue = resolveValue(action.value, variableContext)
  183. if (!inputValue && action.target) {
  184. const resolvedTarget = resolveValue(action.target, variableContext)
  185. if (resolvedTarget !== action.target || !action.target.includes(' ')) inputValue = resolvedTarget
  186. }
  187. if (!inputValue) return { success: false, error: '输入内容为空' }
  188. if (!api?.sendText) return { success: false, error: '输入 API 不可用' }
  189. if (action.clear) {
  190. for (let i = 0; i < 200; i++) {
  191. const clearResult = await api.sendKeyEvent(device, '67')
  192. if (!clearResult.success) break
  193. await new Promise((r) => setTimeout(r, 10))
  194. }
  195. await new Promise((r) => setTimeout(r, 200))
  196. }
  197. const textResult = await api.sendText(device, inputValue)
  198. if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error != null ? textResult.error : 'unknown'}` }
  199. return { success: true }
  200. }
  201. /* --- type: ocr --- */
  202. if (action.type === 'ocr') {
  203. if (!api?.ocrLastMessage) return { success: false, error: 'OCR API 不可用' }
  204. const method = action.method || 'full-screen'
  205. let avatarPath = null
  206. if (method === 'by-avatar' && action.avatar) {
  207. const avatarName = resolveValue(action.avatar, variableContext)
  208. if (avatarName) {
  209. const folderName = folderPath.split(/[/\\]/).pop()
  210. avatarPath = `${folderName}/${avatarName}`
  211. }
  212. }
  213. const ocrResult = await api.ocrLastMessage(device, method, avatarPath, action.area, folderPath)
  214. if (!ocrResult.success) return { success: false, error: `OCR识别失败: ${ocrResult.error != null ? ocrResult.error : 'unknown'}` }
  215. if (action.variable) variableContext[action.variable] = ocrResult.text || ''
  216. return { success: true, text: ocrResult.text, position: ocrResult.position }
  217. }
  218. /* --- type: keyevent --- */
  219. if (action.type === 'keyevent') {
  220. const inVars = action.inVars || []
  221. const keyVar = extractVarName(inVars[0])
  222. const keyCode = variableContext[keyVar] != null && variableContext[keyVar] !== ''
  223. ? variableContext[keyVar]
  224. : keyVar
  225. if (!keyCode && keyCode !== 0) return { success: false, error: 'keyevent 操作缺少按键代码参数' }
  226. if (keyCode === 'KEYCODE_BACK') keyCode = '4'
  227. const keyResult = api.sendSystemKey(device, String(keyCode))
  228. if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error != null ? keyResult.error : 'unknown'}` }
  229. return { success: true }
  230. }
  231. /* --- type: scroll --- */
  232. if (action.type === 'scroll') {
  233. if (!api.sendScroll) return { success: false, error: '滚动 API 不可用' }
  234. const direction = action.value
  235. const r = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500)
  236. if (!r.success) return { success: false, error: `滚动失败: ${r.error != null ? r.error : 'unknown'}` }
  237. return { success: true }
  238. }
  239. /* --- type: swipe --- */
  240. if (action.type === 'swipe') {
  241. if (!api.sendSwipe) return { success: false, error: '滑动 API 不可用' }
  242. const { x1, y1, x2, y2 } = calculateSwipeCoordinates(action.value, resolution.width, resolution.height)
  243. const r = await api.sendSwipe(device, x1, y1, x2, y2, 300)
  244. if (!r.success) return { success: false, error: `滑动失败: ${r.error != null ? r.error : 'unknown'}` }
  245. return { success: true }
  246. }
  247. /* --- type: adb 按 method 分发到 adb/*.js --- */
  248. const method = action.method
  249. if (!method) return { success: false, error: 'adb 操作缺少 method 参数' }
  250. const handler = METHOD_HANDLERS[method]
  251. if (!handler || !handler.run) return { success: false, error: `未知的 adb method: ${method}` }
  252. return handler.run(action, ctx)
  253. }
  254. module.exports = { types, parse, execute }