/** * 语句:adb / keyevent / scroll / swipe / locate / click / press / input / ocr 统一入口,直接集成 adb 调用 */ const types = ['adb', 'keyevent', 'scroll', 'swipe', 'locate', 'click', 'press', 'input', 'ocr'] /* ========== 滑动坐标计算 ========== */ function calculateSwipeCoordinates(direction, width, height) { const margin = 0.15 const swipeDistance = 0.7 let x1, y1, x2, y2 switch (direction) { case 'up-down': x1 = x2 = Math.round(width / 2) y1 = Math.round(height * margin) y2 = Math.round(height * (margin + swipeDistance)) break case 'down-up': x1 = x2 = Math.round(width / 2) y1 = Math.round(height * (margin + swipeDistance)) y2 = Math.round(height * margin) break case 'left-right': y1 = y2 = Math.round(height / 2) x1 = Math.round(width * margin) x2 = Math.round(width * (margin + swipeDistance)) break case 'right-left': y1 = y2 = Math.round(height / 2) x1 = Math.round(width * (margin + swipeDistance)) x2 = Math.round(width * margin) break default: throw new Error(`未知的滑动方向: ${direction}`) } return { x1, y1, x2, y2 } } /* ========== 解析入口(按 type 解析自身字段:inVars/outVars/method 等)========== */ function parse(action, parseContext) { const type = action.type || 'adb' const { extractVarName, resolveValue } = parseContext const variableContext = parseContext.variableContext || {} const parsed = Object.assign({}, action, { type }) if (type === 'adb') { parsed.method = action.method parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : [] parsed.target = action.target parsed.value = action.value parsed.variable = action.variable parsed.clear = action.clear || false return parsed } if (type === 'locate' || type === 'click') { return parsed } if (type === 'input') { parsed.clear = action.clear || false return parsed } if (type === 'ocr') { parsed.area = action.area parsed.avatar = resolveValue(action.avatar, variableContext) return parsed } return parsed } /* ========== 执行入口 ========== */ async function execute(action, ctx) { const { device, folderPath, resolution, variableContext, api, extractVarName, resolveValue, logOutVars, DEFAULT_SCROLL_DISTANCE = 100, } = ctx /* --- type: locate 定位(image/text/coordinate)--- */ if (action.type === 'locate') { const method = action.method || 'image' let position = null if (method === 'image') { const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}` if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' } const matchResult = await api.matchImageAndGetCoordinate(device, imagePath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` } position = matchResult.clickPosition } else if (method === 'text') { if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' } const matchResult = await api.findTextAndGetCoordinate(device, action.target) if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` } position = matchResult.clickPosition } else if (method === 'coordinate') { position = Array.isArray(action.target) ? { x: action.target[0], y: action.target[1] } : action.target } if (action.variable && position) variableContext[action.variable] = position return { success: true, result: position } } /* --- type: click 点击(position/image/text)--- */ if (action.type === 'click') { const method = action.method || 'position' let position = null if (method === 'position') position = resolveValue(action.target, variableContext) else if (method === 'image') { const imagePath = action.target.startsWith('/') || action.target.includes(':') ? action.target : `${folderPath}/${action.target}` if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' } const matchResult = await api.matchImageAndGetCoordinate(device, imagePath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` } position = matchResult.clickPosition } else if (method === 'text') { if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' } const matchResult = await api.findTextAndGetCoordinate(device, action.target) if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` } position = matchResult.clickPosition } if (!position?.x || !position?.y) return { success: false, error: '无法获取点击位置' } if (!api?.sendTap) return { success: false, error: '点击 API 不可用' } const tapResult = await api.sendTap(device, position.x, position.y) if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` } return { success: true } } /* --- type: press 按图点击 --- */ if (action.type === 'press') { const imagePath = `${folderPath}/resources/${action.value}` if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' } const matchResult = await api.matchImageAndGetCoordinate(device, imagePath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` } const { x, y } = matchResult.clickPosition if (!api?.sendTap) return { success: false, error: '点击 API 不可用' } const tapResult = await api.sendTap(device, x, y) if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` } return { success: true } } /* --- type: input 输入文本 --- */ if (action.type === 'input') { let inputValue = resolveValue(action.value, variableContext) if (!inputValue && action.target) { const resolvedTarget = resolveValue(action.target, variableContext) if (resolvedTarget !== action.target || !action.target.includes(' ')) inputValue = resolvedTarget } if (!inputValue) return { success: false, error: '输入内容为空' } if (!api?.sendText) return { success: false, error: '输入 API 不可用' } if (action.clear) { for (let i = 0; i < 200; i++) { const clearResult = await api.sendKeyEvent(device, '67') if (!clearResult.success) break await new Promise((r) => setTimeout(r, 10)) } await new Promise((r) => setTimeout(r, 200)) } const textResult = await api.sendText(device, inputValue) if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error}` } return { success: true } } /* --- type: ocr 文字识别 --- */ if (action.type === 'ocr') { if (!api?.ocrLastMessage) return { success: false, error: 'OCR API 不可用' } const method = action.method || 'full-screen' let avatarPath = null if (method === 'by-avatar' && action.avatar) { const avatarName = resolveValue(action.avatar, variableContext) if (avatarName) { const folderName = folderPath.split(/[/\\]/).pop() avatarPath = `${folderName}/${avatarName}` } } const ocrResult = await api.ocrLastMessage(device, method, avatarPath, action.area, folderPath) if (!ocrResult.success) return { success: false, error: `OCR识别失败: ${ocrResult.error}` } if (action.variable) variableContext[action.variable] = ocrResult.text || '' return { success: true, text: ocrResult.text, position: ocrResult.position } } /* --- type: keyevent 按键 --- */ if (action.type === 'keyevent') { let keyCode = null const inVars = action.inVars || [] if (inVars.length > 0) { const keyVar = extractVarName(inVars[0]) keyCode = variableContext[keyVar] || keyVar } else if (action.value) { keyCode = resolveValue(action.value, variableContext) } if (!keyCode) return { success: false, error: 'keyevent 操作缺少按键代码参数' } if (keyCode === 'KEYCODE_BACK') keyCode = '4' const keyResult = api.sendSystemKey(device, String(keyCode)) if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error}` } return { success: true } } /* --- type: scroll 滚动 --- */ if (action.type === 'scroll') { if (!api.sendScroll) return { success: false, error: '滚动 API 不可用' } const direction = action.value const r = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500) if (!r.success) return { success: false, error: `滚动失败: ${r.error}` } return { success: true } } /* --- type: swipe 滑动 --- */ if (action.type === 'swipe') { if (!api.sendSwipe) return { success: false, error: '滑动 API 不可用' } const { x1, y1, x2, y2 } = calculateSwipeCoordinates(action.value, resolution.width, resolution.height) const r = await api.sendSwipe(device, x1, y1, x2, y2, 300) if (!r.success) return { success: false, error: `滑动失败: ${r.error}` } return { success: true } } /* --- type: adb 按 method 分发(inVars/outVars)--- */ const method = action.method if (!method) return { success: false, error: 'adb 操作缺少 method 参数' } const inVars = action.inVars || [] const outVars = action.outVars || [] switch (method) { case 'input': { let inputValue = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null if (!inputValue && action.value) inputValue = resolveValue(action.value, variableContext) if (!inputValue) return { success: false, error: 'input 操作缺少输入内容' } if (action.clear) { for (let i = 0; i < 200; i++) { const clearResult = await api.sendKeyEvent(device, '67') if (!clearResult.success) break await new Promise((r) => setTimeout(r, 10)) } await new Promise((r) => setTimeout(r, 200)) } if (!api?.sendText) return { success: false, error: '输入 API 不可用' } const textResult = await api.sendText(device, String(inputValue)) if (!textResult.success) return { success: false, error: `输入失败: ${textResult.error}` } return { success: true } } case 'click': { let position = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null if (!position && action.target) position = resolveValue(action.target, variableContext) if (!position) return { success: false, error: 'click 操作缺少位置参数' } if (typeof position === 'string') { if (position === '') return { success: false, error: 'click 操作缺少位置参数(位置变量为空)' } try { position = JSON.parse(position) } catch (e) { const parts = position.split(',') if (parts.length === 2) { const x = parseFloat(parts[0].trim()) const y = parseFloat(parts[1].trim()) if (!isNaN(x) && !isNaN(y)) position = { x: Math.round(x), y: Math.round(y) } else return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` } } else return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` } } } if (Array.isArray(position) && position.length >= 2) position = { x: position[0], y: position[1] } if (position?.topLeft && position?.bottomRight) { position = { x: Math.round((position.topLeft.x + position.bottomRight.x) / 2), y: Math.round((position.topLeft.y + position.bottomRight.y) / 2), } } if (!position || typeof position !== 'object' || position.x === undefined || position.y === undefined) return { success: false, error: 'click 操作的位置格式错误,需要 {x, y} 对象' } if (!api?.sendTap) return { success: false, error: '点击 API 不可用' } const tapResult = await api.sendTap(device, position.x, position.y) if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` } return { success: true } } case 'locate': { const locateMethod = action.method || action.targetMethod || 'image' let position = null if (locateMethod === 'image') { let imagePath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.target if (!imagePath) return { success: false, error: 'locate 操作(image)缺少图片路径' } const fullPath = imagePath.startsWith('/') || imagePath.includes(':') ? imagePath : `${folderPath}/resources/${imagePath}` if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' } const matchResult = await api.matchImageAndGetCoordinate(device, fullPath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` } position = matchResult.clickPosition } else if (locateMethod === 'text') { const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.target if (!targetText) return { success: false, error: 'locate 操作(text)缺少文字内容' } if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' } const matchResult = await api.findTextAndGetCoordinate(device, targetText) if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` } position = matchResult.clickPosition } else if (locateMethod === 'coordinate') { const coord = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : resolveValue(action.target, variableContext) if (!coord) return { success: false, error: 'locate 操作(coordinate)缺少坐标' } position = Array.isArray(coord) ? { x: coord[0], y: coord[1] } : coord } if (outVars.length > 0) { variableContext[extractVarName(outVars[0])] = position await logOutVars(action, variableContext, folderPath) } else if (action.variable) variableContext[action.variable] = position return { success: true, result: position } } case 'swipe': { let direction = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext) if (!direction) return { success: false, error: 'swipe 操作缺少方向参数' } let x1, y1, x2, y2 if (inVars.length >= 3) { const start = variableContext[extractVarName(inVars[1])] const end = variableContext[extractVarName(inVars[2])] if (start && end) { x1 = start.x ?? start[0] y1 = start.y ?? start[1] x2 = end.x ?? end[0] y2 = end.y ?? end[1] } } if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) { const coords = calculateSwipeCoordinates(direction, resolution.width, resolution.height) x1 = coords.x1 y1 = coords.y1 x2 = coords.x2 y2 = coords.y2 } if (!api?.sendSwipe) return { success: false, error: '滑动 API 不可用' } const swipeResult = await api.sendSwipe(device, x1, y1, x2, y2, 300) if (!swipeResult.success) return { success: false, error: `滑动失败: ${swipeResult.error}` } return { success: true } } case 'scroll': { const direction = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext) if (!direction) return { success: false, error: 'scroll 操作缺少方向参数' } if (!api?.sendScroll) return { success: false, error: '滚动 API 不可用' } const scrollResult = await api.sendScroll(device, direction, resolution.width, resolution.height, DEFAULT_SCROLL_DISTANCE, 500) if (!scrollResult.success) return { success: false, error: `滚动失败: ${scrollResult.error}` } return { success: true } } case 'keyevent': { let keyCode = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : resolveValue(action.value, variableContext) if (!keyCode) return { success: false, error: 'keyevent 操作缺少按键代码参数' } if (keyCode === 'KEYCODE_BACK') keyCode = '4' const keyResult = api.sendSystemKey(device, String(keyCode)) if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error}` } return { success: true } } case 'press': { const imagePath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.value if (!imagePath) return { success: false, error: 'press 操作缺少图片路径' } const fullPath = imagePath.startsWith('/') || imagePath.includes(':') ? imagePath : `${folderPath}/${imagePath}` if (!api?.matchImageAndGetCoordinate) return { success: false, error: '图像匹配 API 不可用' } const matchResult = await api.matchImageAndGetCoordinate(device, fullPath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error}` } const { x, y } = matchResult.clickPosition if (!api?.sendTap) return { success: false, error: '点击 API 不可用' } const tapResult = await api.sendTap(device, x, y) if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` } return { success: true } } case 'string-press': { const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.value if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' } if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' } const matchResult = await api.findTextAndGetCoordinate(device, targetText) if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` } const { x, y } = matchResult.clickPosition if (!api?.sendTap) return { success: false, error: '点击 API 不可用' } const tapResult = await api.sendTap(device, x, y) if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` } return { success: true } } default: return { success: false, error: `未知的 adb method: ${method}` } } } /* ========== 导出 ========== */ module.exports = { types, parse, execute }