ef-compiler.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. // EasyFlow 编译器 - 工作流任务解析和执行器(主机,单文件 ≤500 行)
  2. // ========== 入口说明:外部通过 require 本模块后,使用下方 module.exports 导出的 5 个方法作为调用入口 ==========
  3. const path = require('path')
  4. const fs = require('fs')
  5. // --- 配置:根目录 config.js(与 Electron 共用);projectRoot 优先取 config.projectRoot ---
  6. const defaultRoot = path.resolve(__dirname, '..', '..')
  7. const configPath = process.env.STATIC_ROOT
  8. ? path.join(path.dirname(path.resolve(process.env.STATIC_ROOT)), 'config.js')
  9. : path.join(defaultRoot, 'config.js')
  10. let projectRoot = path.dirname(path.resolve(configPath))
  11. let adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
  12. let nodeExePath = 'node'
  13. if (fs.existsSync(configPath)) {
  14. try {
  15. const cfg = require(configPath)
  16. if (cfg.projectRoot && fs.existsSync(cfg.projectRoot)) projectRoot = cfg.projectRoot
  17. nodeExePath = cfg.nodejsPath
  18. ? (path.isAbsolute(cfg.nodejsPath) ? cfg.nodejsPath : path.join(projectRoot, cfg.nodejsPath))
  19. : (process.env.STATIC_ROOT ? path.join(projectRoot, 'node', process.platform === 'win32' ? 'node.exe' : 'node') : 'node')
  20. adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js')
  21. if (process.env.STATIC_ROOT) {
  22. const unpacked = path.join(projectRoot, 'resources', 'app.asar.unpacked', 'nodejs', 'adb', 'adb-interact.js')
  23. if (fs.existsSync(unpacked)) adbInteractPath = unpacked
  24. }
  25. } catch (e) {}
  26. }
  27. const funcDir = path.join(__dirname, 'actions', 'fun')
  28. const DEFAULT_STEP_INTERVAL = 1000
  29. const DEFAULT_SCROLL_DISTANCE = 100
  30. const compilerConfig = { projectRoot, funcDir, adbInteractPath, nodeExePath, DEFAULT_STEP_INTERVAL, DEFAULT_SCROLL_DISTANCE }
  31. // --- 依赖 ---
  32. const setParser = require('./actions/set-parser.js')
  33. const expressionEvaluator = require('./expression-evaluator.js')
  34. const runtimeApi = require('./runtime-api.js')
  35. const workflowJsonParser = require('./workflow-json-parser.js')
  36. const sequenceRunner = require('./sequence-runner.js')
  37. const actions = require('./actions/fun/fun-parser.js')
  38. // --- 共享状态(须早于 electronAPI:匹配 tmp 依赖当前工作流目录)---
  39. const state = {
  40. variableContext: {},
  41. variableContextInitialized: false,
  42. globalStepCounter: 0,
  43. currentWorkflowFolderPath: null,
  44. declaredVariableNames: [], // workflow.variables 的 key,用于校验 inVars/outVars 引用是否已声明
  45. }
  46. // --- 功能模块(fun 目录)与运行时 API ---
  47. const imgCenterLocation = require('./actions/fun/img/img-center-point-location.js')
  48. const { readTextFile } = require('./actions/fun/IO/read-txt.js')
  49. const { writeTextFile } = require('./actions/fun/IO/save-txt.js')
  50. /** 按图点击等:临时目录落在「当前流程文件夹/tmp/」;调用方也可传第三参覆盖 */
  51. async function matchImageAndGetCoordinate (device, imagePath, folderPathFromCaller) {
  52. const fp = folderPathFromCaller != null && folderPathFromCaller !== ''
  53. ? folderPathFromCaller
  54. : state.currentWorkflowFolderPath
  55. return imgCenterLocation.matchImageAndGetCoordinate(device, imagePath, fp)
  56. }
  57. const electronAPI = runtimeApi.createElectronAPI({ matchImageAndGetCoordinate, readTextFile, writeTextFile }, compilerConfig)
  58. // --- 从各组件抽出的工具方法(供本文件与 ctx 使用)---
  59. const extractVarName = setParser.extractVarName
  60. const replaceVariablesInString = setParser.replaceVariablesInString
  61. const resolveValue = setParser.resolveValue
  62. const parseDelayString = setParser.parseDelayString
  63. const calculateWaitTime = setParser.calculateWaitTime
  64. const evaluateCondition = expressionEvaluator.evaluateCondition
  65. const evaluateExpression = expressionEvaluator.evaluateExpression
  66. const getActionName = workflowJsonParser.getActionName
  67. // --- 日志与变量输出 ---
  68. async function logMessage(message, folderPath = null) {
  69. try {
  70. const targetFolderPath = folderPath || state.currentWorkflowFolderPath
  71. if (targetFolderPath && electronAPI.appendLog) await electronAPI.appendLog(targetFolderPath, message)
  72. } catch (err) {}
  73. }
  74. async function logOutVars(action, variableContext, folderPath = null) {
  75. if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length === 0) return
  76. const outVarsInfo = action.outVars.map((varName) => {
  77. const varNameClean = extractVarName(varName)
  78. const value = variableContext[varNameClean]
  79. let displayValue = value
  80. if (typeof value === 'string' && value.length > 100) displayValue = value.substring(0, 100) + '...'
  81. return `${varNameClean}: ${JSON.stringify(displayValue)}`
  82. })
  83. }
  84. // --- 对外入口 1:解析整份工作流 ---
  85. function parseWorkflow(workflow) {
  86. const loaderState = {
  87. variableContext: state.variableContext,
  88. getInitialized: () => state.variableContextInitialized,
  89. setInitialized: (v) => { state.variableContextInitialized = v },
  90. }
  91. const result = workflowJsonParser.parseWorkflow(workflow, loaderState)
  92. state.declaredVariableNames = workflow && typeof workflow.variables === 'object' ? Object.keys(workflow.variables) : []
  93. return result
  94. }
  95. // --- 对外入口 2:仅解析动作列表 ---
  96. function parseActions(actions) {
  97. const loaderState = {
  98. variableContext: state.variableContext,
  99. getInitialized: () => state.variableContextInitialized,
  100. setInitialized: () => {},
  101. }
  102. return workflowJsonParser.parseActions(actions, loaderState)
  103. }
  104. // --- 对外入口 3:执行单条动作 ---
  105. async function executeAction(action, device, folderPath, resolution) {
  106. const ctx = {
  107. variableContext: state.variableContext,
  108. compilerConfig,
  109. electronAPI,
  110. extractVarName,
  111. replaceVariablesInString,
  112. resolveValue,
  113. evaluateCondition,
  114. evaluateExpression,
  115. getActionName,
  116. logMessage,
  117. logOutVars,
  118. parseDelayString,
  119. calculateWaitTime,
  120. DEFAULT_SCROLL_DISTANCE,
  121. registry: workflowJsonParser.registry,
  122. executeAction: workflowJsonParser.executeAction,
  123. }
  124. try {
  125. return await actions.runAction(action, device, folderPath, resolution, ctx)
  126. } catch (error) {
  127. const now = new Date()
  128. 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')}`
  129. const errDetail = error && (error.message || error.stack || String(error)) || 'unknown'
  130. const errorMsg = `[ef-compiler] [ERROR] Action failed: ${errDetail} [time: ${timeStr}]`
  131. await logMessage(errorMsg, folderPath).catch(() => {})
  132. return { success: false, error: errDetail }
  133. }
  134. }
  135. // --- 对外入口 4:按顺序执行动作序列(含 schedule/if/for/while 等)---
  136. async function executeActionSequence(actions, device, folderPath, resolution, stepInterval = DEFAULT_STEP_INTERVAL, onStepComplete = null, shouldStop = null, depth = 0) {
  137. const ctx = {
  138. executeAction,
  139. logMessage,
  140. evaluateCondition,
  141. getActionName,
  142. parseDelayString,
  143. calculateWaitTime,
  144. replaceVariablesInString,
  145. state,
  146. DEFAULT_STEP_INTERVAL,
  147. }
  148. return sequenceRunner.executeActionSequence(actions, device, folderPath, resolution, stepInterval, onStepComplete, shouldStop, depth, ctx)
  149. }
  150. // --- 编译/执行入口:对外暴露的 API ---
  151. module.exports = {
  152. parseWorkflow,
  153. parseActions,
  154. executeAction,
  155. executeActionSequence,
  156. }