|
|
@@ -1,385 +0,0 @@
|
|
|
-/**
|
|
|
- * 语句: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 != 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 点击(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 != 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)
|
|
|
- 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') {
|
|
|
- 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 != 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 分发(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 != null ? textResult.error : 'unknown'}` }
|
|
|
- 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 != null ? tapResult.error : 'unknown'}` }
|
|
|
- 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 != null ? matchResult.error : 'unknown'}` }
|
|
|
- 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 != null ? matchResult.error : 'unknown'}` }
|
|
|
- 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 != null ? swipeResult.error : 'unknown'}` }
|
|
|
- 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 != null ? scrollResult.error : 'unknown'}` }
|
|
|
- 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 != null ? keyResult.error : 'unknown'}` }
|
|
|
- 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 != 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 }
|
|
|
- }
|
|
|
-
|
|
|
- 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 != 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 }
|
|
|
- }
|
|
|
-
|
|
|
- default:
|
|
|
- return { success: false, error: `未知的 adb method: ${method}` }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/* ========== 导出 ========== */
|
|
|
-module.exports = { types, parse, execute }
|