/** * 语句:adb / keyevent / scroll / swipe / locate / click / press / input / ocr 统一入口 * type===adb 时按 method 分发到 adb/*.js;其余 type 在本文件内执行,公共逻辑见 utils.js */ const { calculateSwipeCoordinates } = require('./utils.js') const { assertStrictKeys } = require('../../../action-schema.js') const types = ['adb', 'keyevent', 'scroll', 'swipe', 'locate', 'click', 'press', 'input', 'ocr'] const METHOD_HANDLERS = { input: require('./input.js'), click: require('./click.js'), locate: require('./locate.js'), swipe: require('./swipe.js'), scroll: require('./scroll.js'), keyevent: require('./keyevent.js'), press: require('./press.js'), 'string-press': require('./string-press.js'), 'send-img-to-device': require('./send-img-to-device.js'), } /* ========== 解析入口 ========== */ function parse (action, parseContext) { const path = parseContext.actionPath || 'adb' const type = action.type || 'adb' const { extractVarName, resolveValue } = parseContext const variableContext = parseContext.variableContext || {} if (type === 'adb') { assertStrictKeys(action, ['type', 'method', 'inVars', 'outVars', 'target', 'value', 'variable', 'clear'], path) if (action.method == null || String(action.method).trim() === '') { throw new Error(`${path}: adb 须包含 method`) } if (!Array.isArray(action.inVars)) { throw new Error(`${path}: adb 须包含 inVars 数组`) } if (!Array.isArray(action.outVars)) { throw new Error(`${path}: adb 须包含 outVars 数组`) } return { type: 'adb', method: action.method, inVars: action.inVars.map((v) => extractVarName(v)), outVars: action.outVars.map((v) => extractVarName(v)), target: action.target, value: action.value, variable: action.variable, clear: action.clear === true, } } if (type === 'locate' || type === 'click') { assertStrictKeys(action, ['type', 'method', 'target', 'variable'], path) return { type, method: action.method, target: action.target, variable: action.variable, } } if (type === 'input') { assertStrictKeys(action, ['type', 'value', 'target', 'clear'], path) if (action.value === undefined || action.value === null || action.value === '') { throw new Error(`${path}: input 须包含 value`) } return { type: 'input', value: action.value, target: action.target, clear: action.clear === true, } } if (type === 'press') { assertStrictKeys(action, ['type', 'value'], path) if (action.value === undefined || action.value === null || action.value === '') { throw new Error(`${path}: press 须包含 value`) } return { type: 'press', value: action.value } } if (type === 'ocr') { assertStrictKeys(action, ['type', 'method', 'area', 'avatar', 'variable'], path) return { type: 'ocr', method: action.method, area: action.area, avatar: action.avatar != null ? resolveValue(action.avatar, variableContext) : undefined, variable: action.variable, } } if (type === 'keyevent') { assertStrictKeys(action, ['type', 'inVars'], path) if (!Array.isArray(action.inVars) || action.inVars.length < 1) { throw new Error(`${path}: keyevent 须使用 inVars 且至少 1 项为键码`) } return { type: 'keyevent', inVars: action.inVars.map((v) => extractVarName(v)), } } if (type === 'scroll') { assertStrictKeys(action, ['type', 'value'], path) if (action.value === undefined || action.value === null || action.value === '') { throw new Error(`${path}: scroll 须包含 value(方向)`) } return { type: 'scroll', value: action.value } } if (type === 'swipe') { assertStrictKeys(action, ['type', 'value'], path) if (action.value === undefined || action.value === null || action.value === '') { throw new Error(`${path}: swipe 须包含 value`) } return { type: 'swipe', value: action.value } } throw new Error(`${path}: 未知的 adb-parser type: ${type}`) } /* ========== 执行入口 ========== */ async function execute(action, ctx) { const { device, folderPath, resolution, variableContext, api, extractVarName, resolveValue, logOutVars, DEFAULT_SCROLL_DISTANCE = 100, } = ctx /* --- type: locate --- */ 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, folderPath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` } 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 != null ? matchResult.error : 'unknown'}` } 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 --- */ 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, folderPath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` } 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 != null ? matchResult.error : 'unknown'}` } 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 != null ? tapResult.error : 'unknown'}` } 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, folderPath) if (!matchResult.success) return { success: false, error: `图像匹配失败: ${matchResult.error != null ? matchResult.error : 'unknown'}` } 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 != null ? tapResult.error : 'unknown'}` } 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 != null ? textResult.error : 'unknown'}` } 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 != null ? ocrResult.error : 'unknown'}` } if (action.variable) variableContext[action.variable] = ocrResult.text || '' return { success: true, text: ocrResult.text, position: ocrResult.position } } /* --- type: keyevent --- */ if (action.type === 'keyevent') { const inVars = action.inVars || [] const keyVar = extractVarName(inVars[0]) const keyCode = variableContext[keyVar] != null && variableContext[keyVar] !== '' ? variableContext[keyVar] : keyVar if (!keyCode && keyCode !== 0) 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 != null ? keyResult.error : 'unknown'}` } 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 != null ? r.error : 'unknown'}` } 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 != null ? r.error : 'unknown'}` } return { success: true } } /* --- type: adb 按 method 分发到 adb/*.js --- */ const method = action.method if (!method) return { success: false, error: 'adb 操作缺少 method 参数' } const handler = METHOD_HANDLERS[method] if (!handler || !handler.run) return { success: false, error: `未知的 adb method: ${method}` } return handler.run(action, ctx) } module.exports = { types, parse, execute }