| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- /**
- * 语句: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 }
|