// EasyFlow 编译器 - 工作流任务解析和执行器(主机,单文件 ≤500 行) // ========== 入口说明:外部通过 require 本模块后,使用下方 module.exports 导出的 5 个方法作为调用入口 ========== const path = require('path') const fs = require('fs') // --- 配置:projectRoot = config 文件所在目录的上级(config 在 <根>/configs/config.js)--- const defaultRoot = path.resolve(__dirname, '..', '..') const configPath = process.env.STATIC_ROOT ? path.join(path.dirname(process.env.STATIC_ROOT), 'configs', 'config.js') : path.join(defaultRoot, 'configs', 'config.js') let projectRoot = path.dirname(path.dirname(path.resolve(configPath))) let adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js') let nodeExePath = 'node' if (fs.existsSync(configPath)) { try { const cfg = require(configPath) nodeExePath = cfg.nodejsPath ? (path.isAbsolute(cfg.nodejsPath) ? cfg.nodejsPath : path.join(projectRoot, cfg.nodejsPath)) : (process.env.STATIC_ROOT ? path.join(projectRoot, 'node', process.platform === 'win32' ? 'node.exe' : 'node') : 'node') adbInteractPath = path.join(projectRoot, 'nodejs', 'adb', 'adb-interact.js') if (process.env.STATIC_ROOT) { const unpacked = path.join(projectRoot, 'resources', 'app.asar.unpacked', 'nodejs', 'adb', 'adb-interact.js') if (fs.existsSync(unpacked)) adbInteractPath = unpacked } } catch (e) {} } const funcDir = path.join(__dirname, 'fun') const DEFAULT_STEP_INTERVAL = 1000 const DEFAULT_SCROLL_DISTANCE = 100 const compilerConfig = { projectRoot, funcDir, adbInteractPath, nodeExePath, DEFAULT_STEP_INTERVAL, DEFAULT_SCROLL_DISTANCE } // --- 依赖 --- const setParser = require('./components/actions/set-parser.js') const expressionEvaluator = require('./components/expression-evaluator.js') const runtimeApi = require('./components/runtime-api.js') const workflowJsonParser = require('./components/workflow-json-parser.js') const sequenceRunner = require('./components/sequence-runner.js') const actions = require('./components/actions/fun-parser.js') // --- 功能模块(fun 目录)与运行时 API --- const { matchImageAndGetCoordinate } = require('./fun/img-center-point-location.js') const { readTextFile } = require('./fun/read-txt.js') const { writeTextFile } = require('./fun/save-txt.js') const electronAPI = runtimeApi.createElectronAPI({ matchImageAndGetCoordinate, readTextFile, writeTextFile }, compilerConfig) // --- 共享状态(变量上下文、步骤计数、当前工作流目录等)--- const state = { variableContext: {}, variableContextInitialized: false, globalStepCounter: 0, currentWorkflowFolderPath: null, } // --- 从各组件抽出的工具方法(供本文件与 ctx 使用)--- const extractVarName = setParser.extractVarName const replaceVariablesInString = setParser.replaceVariablesInString const resolveValue = setParser.resolveValue const parseDelayString = setParser.parseDelayString const calculateWaitTime = setParser.calculateWaitTime const evaluateCondition = expressionEvaluator.evaluateCondition const evaluateExpression = expressionEvaluator.evaluateExpression const getActionName = workflowJsonParser.getActionName // --- 日志与变量输出 --- async function logMessage(message, folderPath = null) { try { const targetFolderPath = folderPath || state.currentWorkflowFolderPath if (targetFolderPath && electronAPI.appendLog) await electronAPI.appendLog(targetFolderPath, message) } catch (err) {} } async function logOutVars(action, variableContext, folderPath = null) { if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length === 0) return const outVarsInfo = action.outVars.map((varName) => { const varNameClean = extractVarName(varName) const value = variableContext[varNameClean] let displayValue = value if (typeof value === 'string' && value.length > 100) displayValue = value.substring(0, 100) + '...' return `${varNameClean}: ${JSON.stringify(displayValue)}` }) } // --- 对外入口 1:解析整份工作流 --- function parseWorkflow(workflow) { const loaderState = { variableContext: state.variableContext, getInitialized: () => state.variableContextInitialized, setInitialized: (v) => { state.variableContextInitialized = v }, } return workflowJsonParser.parseWorkflow(workflow, loaderState) } // --- 对外入口 2:仅解析动作列表 --- function parseActions(actions) { const loaderState = { variableContext: state.variableContext, getInitialized: () => state.variableContextInitialized, setInitialized: () => {}, } return workflowJsonParser.parseActions(actions, loaderState) } // --- 对外入口 3:执行单条动作 --- async function executeAction(action, device, folderPath, resolution) { const ctx = { variableContext: state.variableContext, compilerConfig, electronAPI, extractVarName, replaceVariablesInString, resolveValue, evaluateCondition, evaluateExpression, getActionName, logMessage, logOutVars, parseDelayString, calculateWaitTime, DEFAULT_SCROLL_DISTANCE, registry: workflowJsonParser.registry, executeAction: workflowJsonParser.executeAction, } try { return await actions.runAction(action, device, folderPath, resolution, ctx) } catch (error) { 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')}` const errDetail = error && (error.message || error.stack || String(error)) || 'unknown' const errorMsg = `[ef-compiler] [ERROR] Action failed: ${errDetail} [time: ${timeStr}]` await logMessage(errorMsg, folderPath).catch(() => {}) return { success: false, error: errDetail } } } // --- 对外入口 4:按顺序执行动作序列(含 schedule/if/for/while 等)--- async function executeActionSequence(actions, device, folderPath, resolution, stepInterval = DEFAULT_STEP_INTERVAL, onStepComplete = null, shouldStop = null, depth = 0) { const ctx = { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state, DEFAULT_STEP_INTERVAL, } return sequenceRunner.executeActionSequence(actions, device, folderPath, resolution, stepInterval, onStepComplete, shouldStop, depth, ctx) } // --- 编译/执行入口:对外暴露的 API --- module.exports = { parseWorkflow, parseActions, executeAction, executeActionSequence, }