Quellcode durchsuchen

传图片到相册

yichael vor 2 Monaten
Ursprung
Commit
dc549c0739

+ 1 - 1
doc/ef-compiler新增结点说明.md

@@ -51,7 +51,7 @@ module.exports = { executeTestFun }  // 导出与注册表 execute 同名的函
 
 ### 2. 注册表一条
 
-在 `nodejs/ef-compiler/actions/fun-node-registry.js` 里添加:
+在 `nodejs/ef-compiler/actions/fun/fun-node-registry.js` 里添加:
 
 ```js
 {

+ 0 - 64
nodejs/adb/send-img-to-device.js

@@ -1,64 +0,0 @@
-#!/usr/bin/env node
-/**
- * 通过 ADB 将本地图片推送到手机并加入相册(1+ 等 Android 通用)
- * 用法: node send-img-to-device.js <本地图片路径> [deviceId]
- * 或 require 后调用: sendImageToDevice(localPath, deviceId) => { success, error }
- */
-
-const { spawnSync } = require('child_process')
-const path = require('path')
-const fs = require('fs')
-
-const configPath = process.env.STATIC_ROOT
-  ? path.join(path.dirname(process.env.STATIC_ROOT), 'configs', 'config.js')
-  : path.join(__dirname, '..', '..', 'configs', 'config.js')
-const projectRoot = path.dirname(path.dirname(path.resolve(configPath)))
-const config = fs.existsSync(configPath) ? require(configPath) : {}
-const adbPath = config.adbPath?.path
-  ? (path.isAbsolute(config.adbPath.path) ? config.adbPath.path : path.resolve(projectRoot, config.adbPath.path))
-  : path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
-
-/** 设备 DCIM 目录,相册会扫描此处 */
-const DEVICE_DCIM = '/sdcard/DCIM/'
-
-/**
- * 将本地图片推送到设备相册
- * @param {string} localPath - 本地图片绝对或相对路径
- * @param {string} [deviceId] - 设备 ID,如 192.168.42.129 或 192.168.42.129:5555
- * @returns {{ success: boolean, error?: string, devicePath?: string }}
- */
-function sendImageToDevice(localPath, deviceId = '') {
-  const resolved = path.resolve(localPath)
-  if (!fs.existsSync(resolved)) {
-    return { success: false, error: `本地文件不存在: ${resolved}` }
-  }
-  const basename = path.basename(resolved)
-  const deviceFile = DEVICE_DCIM + basename
-  const args = deviceId ? ['-s', deviceId, 'push', resolved, deviceFile] : ['push', resolved, deviceFile]
-  const push = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 30000 })
-  if (push.status !== 0) {
-    const err = (push.stderr || push.stdout || '').trim() || `adb push 退出码 ${push.status}`
-    return { success: false, error: err }
-  }
-  const scanArgs = deviceId ? ['-s', deviceId, 'shell', 'am', 'broadcast', '-a', 'android.intent.action.MEDIA_SCANNER_SCAN_FILE', '-d', `file://${deviceFile}`] : ['shell', 'am', 'broadcast', '-a', 'android.intent.action.MEDIA_SCANNER_SCAN_FILE', '-d', `file://${deviceFile}`]
-  spawnSync(adbPath, scanArgs, { encoding: 'utf-8', timeout: 5000 })
-  return { success: true, devicePath: deviceFile }
-}
-
-if (require.main === module) {
-  const localPath = process.argv[2]
-  const deviceId = process.argv[3] || ''
-  if (!localPath) {
-    console.error('用法: node send-img-to-device.js <本地图片路径> [deviceId]')
-    process.exit(1)
-  }
-  const result = sendImageToDevice(localPath, deviceId)
-  if (result.success) {
-    console.log(result.devicePath || 'ok')
-  } else {
-    console.error(result.error)
-    process.exit(1)
-  }
-}
-
-module.exports = { sendImageToDevice }

+ 0 - 385
nodejs/ef-compiler/actions/adb-parser.js

@@ -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 }

+ 208 - 0
nodejs/ef-compiler/actions/adb/adb-parser.js

@@ -0,0 +1,208 @@
+/**
+ * 语句:adb / keyevent / scroll / swipe / locate / click / press / input / ocr 统一入口
+ * type===adb 时按 method 分发到 adb/*.js;其余 type 在本文件内执行,公共逻辑见 utils.js
+ */
+
+const { calculateSwipeCoordinates } = require('./utils.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 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 --- */
+  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 --- */
+  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 分发到 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 }

+ 39 - 0
nodejs/ef-compiler/actions/adb/click.js

@@ -0,0 +1,39 @@
+/**
+ * adb method: click — 按坐标或变量位置点击
+ */
+async function run(action, ctx) {
+  const { device, variableContext, api, extractVarName, resolveValue } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 24 - 0
nodejs/ef-compiler/actions/adb/input.js

@@ -0,0 +1,24 @@
+/**
+ * adb method: input — 输入文本(支持 clear 清空)
+ */
+async function run(action, ctx) {
+  const { device, folderPath, variableContext, api, extractVarName, resolveValue } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 15 - 0
nodejs/ef-compiler/actions/adb/keyevent.js

@@ -0,0 +1,15 @@
+/**
+ * adb method: keyevent — 按键
+ */
+async function run(action, ctx) {
+  const { device, variableContext, api, extractVarName, resolveValue } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 37 - 0
nodejs/ef-compiler/actions/adb/locate.js

@@ -0,0 +1,37 @@
+/**
+ * adb method: locate — 定位(image/text/coordinate),结果写入 outVars 或 variable
+ */
+async function run(action, ctx) {
+  const { device, folderPath, variableContext, api, extractVarName, resolveValue, logOutVars } = ctx
+  const inVars = action.inVars || []
+  const outVars = action.outVars || []
+  const locateMethod = 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 }
+}
+
+module.exports = { run }

+ 20 - 0
nodejs/ef-compiler/actions/adb/press.js

@@ -0,0 +1,20 @@
+/**
+ * adb method: press — 按图点击(匹配图片位置后点击)
+ */
+async function run(action, ctx) {
+  const { device, folderPath, variableContext, api, extractVarName } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 15 - 0
nodejs/ef-compiler/actions/adb/scroll.js

@@ -0,0 +1,15 @@
+/**
+ * adb method: scroll — 滚动
+ */
+async function run(action, ctx) {
+  const { device, resolution, variableContext, api, extractVarName, resolveValue, DEFAULT_SCROLL_DISTANCE = 100 } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 67 - 0
nodejs/ef-compiler/actions/adb/send-img-to-device.js

@@ -0,0 +1,67 @@
+/**
+ * adb method: send-img-to-device — 将本地图片推送到手机并加入相册(1+ 等 Android 通用)
+ * inVars: [本地图片路径] 路径可为相对 folderPath 或绝对
+ */
+const { spawnSync } = require('child_process')
+const path = require('path')
+const fs = require('fs')
+
+const projectRoot = path.resolve(__dirname, '..', '..', '..', '..')
+const configPath = path.join(projectRoot, 'configs', 'config.js')
+const config = fs.existsSync(configPath) ? require(configPath) : {}
+const adbPath = config.adbPath?.path
+  ? (path.isAbsolute(config.adbPath.path) ? config.adbPath.path : path.resolve(projectRoot, config.adbPath.path))
+  : path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
+
+// Android 10+ 相册对 Pictures 目录更友好,1+ 等机型也常从此处读
+const DEVICE_PICTURES = '/sdcard/Pictures/'
+const DEVICE_DCIM = '/sdcard/DCIM/'
+
+/**
+ * 将本地图片推送到设备相册(供 run 与 CLI 复用)
+ * @param {string} localPath - 本地图片绝对路径
+ * @param {string} deviceId - 设备 ID
+ * @param {string} [adbExe] - 可选,默认用 config
+ */
+function doSendImageToDevice(localPath, deviceId, adbExe = adbPath) {
+  const resolved = path.resolve(localPath)
+  if (!fs.existsSync(resolved)) {
+    return { success: false, error: `本地文件不存在: ${resolved}` }
+  }
+  const basename = path.basename(resolved)
+  // 优先推送到 Pictures,Android 10+ 相册更容易识别
+  const deviceFile = DEVICE_PICTURES + basename
+  const localForAdb = resolved.replace(/\\/g, '/')
+  const args = deviceId ? ['-s', deviceId, 'push', localForAdb, deviceFile] : ['push', localForAdb, deviceFile]
+  const push = spawnSync(adbExe, args, { encoding: 'utf-8', timeout: 30000 })
+  if (push.status !== 0) {
+    const err = (push.stderr || push.stdout || '').trim() || `adb push 退出码 ${push.status}`
+    return { success: false, error: err }
+  }
+  // Android 10/11+ 需带 --receiver-include-background 扫描才易在相册中显示
+  const scan = (d) => {
+    const base = ['shell', 'am', 'broadcast', '-a', 'android.intent.action.MEDIA_SCANNER_SCAN_FILE', '-d', d, '--receiver-include-background']
+    const a = deviceId ? ['-s', deviceId, ...base] : base
+    spawnSync(adbExe, a, { encoding: 'utf-8', timeout: 8000 })
+  }
+  scan(`file://${deviceFile}`)
+  scan('file:///sdcard/Pictures')
+  scan('file:///sdcard/DCIM')
+  return { success: true, devicePath: deviceFile }
+}
+
+async function run(action, ctx) {
+  const { device, folderPath, variableContext, extractVarName, logMessage } = ctx
+  const inVars = action.inVars || []
+  let localPath = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || inVars[0]) : action.value
+  if (!localPath) return { success: false, error: 'send-img-to-device 操作缺少本地图片路径' }
+  if (!device || String(device).trim() === '') return { success: false, error: 'send-img-to-device 需要设备 ID,请确保流程已连接设备' }
+  const fullPath = localPath.startsWith('/') || (localPath.length >= 2 && localPath[1] === ':') ? localPath : path.resolve(folderPath, localPath)
+  if (!fs.existsSync(fullPath)) return { success: false, error: `本地文件不存在: ${fullPath}(流程目录: ${folderPath})` }
+  const result = doSendImageToDevice(fullPath, device, adbPath)
+  if (!result.success) return result
+  if (logMessage && folderPath) await logMessage(`[send-img-to-device] pushed: ${result.devicePath}`, folderPath).catch(() => {})
+  return { success: true, devicePath: result.devicePath }
+}
+
+module.exports = { run, doSendImageToDevice }

+ 19 - 0
nodejs/ef-compiler/actions/adb/string-press.js

@@ -0,0 +1,19 @@
+/**
+ * adb method: string-press — 按文字点击(OCR 匹配文字后点击)
+ */
+async function run(action, ctx) {
+  const { device, variableContext, api, extractVarName } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 35 - 0
nodejs/ef-compiler/actions/adb/swipe.js

@@ -0,0 +1,35 @@
+/**
+ * adb method: swipe — 滑动(方向或起止坐标)
+ */
+const { calculateSwipeCoordinates } = require('./utils.js')
+
+async function run(action, ctx) {
+  const { device, resolution, variableContext, api, extractVarName, resolveValue } = ctx
+  const inVars = action.inVars || []
+  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 }
+}
+
+module.exports = { run }

+ 35 - 0
nodejs/ef-compiler/actions/adb/utils.js

@@ -0,0 +1,35 @@
+/**
+ * adb 公共工具:滑动坐标计算等
+ */
+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 }
+}
+
+module.exports = { calculateSwipeCoordinates }

+ 0 - 0
nodejs/ef-compiler/actions/fun-node-registry.js → nodejs/ef-compiler/actions/fun/fun-node-registry.js


+ 5 - 11
nodejs/ef-compiler/actions/fun-parser.js → nodejs/ef-compiler/actions/fun/fun-parser.js

@@ -3,7 +3,7 @@
  * 简易结点由 fun-node-registry.js 配置,只需新建脚本 + 在注册表加一条即可。
  */
 const path = require('path')
-const variableParser = require('../variable-parser.js')
+const variableParser = require('../../variable-parser.js')
 const FUN_NODE_REGISTRY = require('./fun-node-registry.js')
 
 const LEGACY_FUN_TYPES = [
@@ -54,7 +54,7 @@ function parse(action, parseContext) {
 
   const regDef = REGISTRY_BY_TYPE.get(action.type)
   if (regDef) {
-    const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || path.join(__dirname, 'fun')
+    const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || __dirname
     if (regDef.customParse) {
       const script = getRegistryScript(funcDir, action.type)
       if (script && typeof script.parseNode === 'function') return script.parseNode(action, parseContext)
@@ -634,22 +634,16 @@ async function run(actionType, action, ctx, device, folderPath) {
         const mod = get(funcDir, regDef.category)
         const fn = mod[regDef.execute]
         if (!fn) {
-          const errMsg = `fun-parser: ${regDef.execute} not found`
-          if (logMessage) await logMessage(`[${actionType}] failed: ${errMsg}`, folderPath).catch(() => {})
-          return { success: false, error: errMsg }
+          return { success: false, error: `fun-parser: ${regDef.execute} not found` }
         }
         let result
         try {
           result = await fn(input)
         } catch (e) {
-          const errMsg = (e && (e.message || String(e))) || 'execute threw'
-          if (logMessage) await logMessage(`[${actionType}] failed: ${errMsg}`, folderPath).catch(() => {})
-          return { success: false, error: errMsg }
+          return { success: false, error: (e && (e.message || String(e))) || 'execute threw' }
         }
         if (!result || !result.success) {
-          const errMsg = (result && result.error) || 'execute failed'
-          if (logMessage) await logMessage(`[${actionType}] failed: ${errMsg}`, folderPath).catch(() => {})
-          return { success: false, error: errMsg }
+          return { success: false, error: (result && result.error) || 'execute failed' }
         }
         const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : null
         if (outputVarName && result != null) {

+ 1 - 1
nodejs/ef-compiler/ef-compiler.js

@@ -36,7 +36,7 @@ const expressionEvaluator = require('./expression-evaluator.js')
 const runtimeApi = require('./runtime-api.js')
 const workflowJsonParser = require('./workflow-json-parser.js')
 const sequenceRunner = require('./sequence-runner.js')
-const actions = require('./actions/fun-parser.js')
+const actions = require('./actions/fun/fun-parser.js')
 
 // --- 功能模块(fun 目录)与运行时 API ---
 const { matchImageAndGetCoordinate } = require('./actions/fun/img-center-point-location.js')

+ 1 - 0
nodejs/ef-compiler/sequence-runner.js

@@ -182,6 +182,7 @@ async function executeActionSequence(
 
       if (result.success && result.skipped) { /* 步骤跳过不写 log */ }
 
+      // 统一在此处将结点报错写入 log.txt,各结点只需 return { success: false, error } 即可,无需单独写 logMessage
       if (!result.success) {
         const now = new Date()
         const timeStr = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`

+ 3 - 3
nodejs/ef-compiler/workflow-json-parser.js

@@ -5,12 +5,12 @@
 const setParser = require('./actions/set-parser.js')
 const extractVarName = setParser.extractVarName
 const resolveValue = setParser.resolveValue
-const funNodeRegistry = require('./actions/fun-node-registry.js')
+const funNodeRegistry = require('./actions/fun/fun-node-registry.js')
 
 const actionModules = [
   require('./actions/delay-parser.js'),
   setParser,
-  require('./actions/adb-parser.js'),
+  require('./actions/adb/adb-parser.js'),
   require('./actions/echo-parser.js'),
   require('./actions/random-parser.js'),
   require('./actions/schedule-parser.js'),
@@ -18,7 +18,7 @@ const actionModules = [
   require('./actions/for-parser.js'),
   require('./actions/while-parser.js'),
   require('./actions/try-parser.js'),
-  require('./actions/fun-parser.js'),
+  require('./actions/fun/fun-parser.js'),
 ]
 
 const registry = {}

+ 3 - 5
static/process/CreateNote/process.json

@@ -8,12 +8,10 @@
   },
   "execute": 
   [
-    
     {
-      "type": "ai",
-      "method": "text2img",
-      "inVars": ["{imgPrompt}", "{model}", "{imgPath}"],
-      "outVars": []
+      "type": "adb",
+      "method": "send-img-to-device",
+      "inVars": ["./tmp/test.jpg"]
     }
   ]
 }

BIN
static/process/CreateNote/tmp/test.jpg