const { logActionError } = require('./actions/echo-parser.js') const variableParser = require('./variable-parser.js') /** * 执行操作序列(schedule/if/for/while + 普通步骤) * 单文件 ≤500 行。ctx: executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, state * state: variableContext, globalStepCounter, currentWorkflowFolderPath, variableContextInitialized */ async function executeActionSequence( actions, device, folderPath, resolution, stepInterval, onStepComplete, shouldStop, depth, ctx ) { const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state } = ctx const variableContext = state.variableContext const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000 if (depth === 0) { state.globalStepCounter = 0 state.variableContextInitialized = false state.currentWorkflowFolderPath = folderPath } let completedSteps = 0 const interval = stepInterval ?? DEFAULT_STEP_INTERVAL for (let i = 0; i < actions.length; i++) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } const action = actions[i] if (action.type === 'schedule') { const condition = action.condition || {} let intervalStr = condition.interval || '0s' if (replaceVariablesInString && typeof intervalStr === 'string') { intervalStr = replaceVariablesInString(intervalStr, variableContext) } let repeat = condition.repeat !== undefined ? condition.repeat : 1 if (typeof repeat === 'string' && variableContext) { const varName = repeat.replace(/^\{|\}$/g, '').trim() const resolved = variableContext[varName] repeat = resolved !== undefined && resolved !== null && resolved !== '' ? (parseInt(resolved, 10) || 1) : 1 } const actionsToExecute = action.interval || [] const intervalMs = parseDelayString(intervalStr) || 0 const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1) let iteration = 0 while (iteration < maxIterations) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } iteration++ if (iteration > 1 && intervalMs > 0) { let remainingTime = intervalMs const countdownInterval = 100 while (remainingTime > 0) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } const waitTime = Math.min(countdownInterval, remainingTime) await new Promise(resolve => setTimeout(resolve, waitTime)) remainingTime -= waitTime } } if (actionsToExecute.length > 0) { const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!result.success) return result completedSteps += result.completedSteps || 0 } } continue } if (action.type === 'if') { const conditionResult = evaluateCondition(action.condition, variableContext) const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || []) if (actionsToExecute.length > 0) { const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!result.success) return result completedSteps += result.completedSteps || 0 } continue } if (action.type === 'for') { if (action.times != null) { let count = action.times if (typeof count === 'string') { const varName = count.replace(/^\{|\}$/g, '').trim() count = Math.max(0, parseInt(variableContext[varName], 10) || 0) } else { count = Math.max(0, parseInt(count, 10) || 0) } const timesVarKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null const originalDeclaredForTimes = state.declaredVariableNames if (timesVarKey) state.declaredVariableNames = (originalDeclaredForTimes || []).concat(timesVarKey) for (let i = 0; i < count; i++) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = i if (action.body && action.body.length > 0) { const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!result.success) return result completedSteps += result.completedSteps || 0 } } if (timesVarKey) state.declaredVariableNames = originalDeclaredForTimes } else { const items = Array.isArray(action.items) ? action.items : [] const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null const originalDeclared = state.declaredVariableNames const forLocals = [indexKey, variableKey].filter(Boolean) if (forLocals.length > 0) state.declaredVariableNames = (originalDeclared || []).concat(forLocals) for (let i = 0; i < items.length; i++) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (indexKey !== null) variableContext[indexKey] = i if (variableKey !== null) variableContext[variableKey] = items[i] if (action.body && action.body.length > 0) { const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!result.success) return result completedSteps += result.completedSteps || 0 } } if (forLocals.length > 0) state.declaredVariableNames = originalDeclared } continue } if (action.type === 'while') { while (evaluateCondition(action.condition, variableContext)) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (action.body && action.body.length > 0) { const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!result.success) return result completedSteps += result.completedSteps || 0 } } continue } if (action.type === 'try') { const tryActions = action.try || action.body || [] const successActions = action.success || [] const failActions = action.fail || action.catch || [] const result = tryActions.length > 0 ? await executeActionSequence(tryActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) : { success: true, completedSteps: 0 } if (result.success && successActions.length > 0) { const successResult = await executeActionSequence(successActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!successResult.success) return successResult completedSteps += (result.completedSteps || 0) + (successResult.completedSteps || 0) } else if (result.success) { completedSteps += result.completedSteps || 0 } else { const errMsg = (result.error != null && result.error !== '') ? String(result.error) : 'Unknown error' const timeStr = new Date().toISOString().replace('T', ' ').slice(0, 19) await logMessage(`[sequence-runner] [try failed] ${timeStr} ${errMsg}`, folderPath).catch(() => {}) if (failActions.length > 0) { const failResult = await executeActionSequence(failActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx) if (!failResult.success) return failResult completedSteps += (result.completedSteps || 0) + (failResult.completedSteps || 0) } else { return result } } continue } const times = action.times || 1 if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, 0) const waitTime = calculateWaitTime(action.data, action.delay) if (waitTime > 0) { let remainingTime = waitTime const countdownInterval = 100 const stepName = getActionName(action) while (remainingTime > 0) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (onStepComplete) onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0) const waitTimeChunk = Math.min(countdownInterval, remainingTime) await new Promise(resolve => setTimeout(resolve, waitTimeChunk)) remainingTime -= waitTimeChunk } } for (let t = 0; t < times; t++) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1) state.globalStepCounter++ const typeName = getActionName(action) // inVars/outVars 引用的变量必须在 workflow.variables 中声明,否则写 log 并失败 if ((action.inVars && action.inVars.length > 0) || (action.outVars && action.outVars.length > 0)) { const declared = state.declaredVariableNames || [] const validation = variableParser.validateInOutVars(action, declared) if (!validation.valid) { const errMsg = `inVars/outVars 引用了未在 variables 中声明的变量: ${validation.undeclared.join(', ')}` await logActionError(action, { success: false, error: errMsg }, { getActionName, logMessage, folderPath }).catch(() => {}) return { success: false, error: errMsg, completedSteps: i } } } const result = await executeAction(action, device, folderPath, resolution) if (result.success && result.skipped) { /* 步骤跳过不写 log */ } // 统一由 echo-parser.logActionError 打印结点报错,各结点只需 return { success: false, error } 即可 if (!result.success) { await logActionError(action, result, { getActionName, logMessage, folderPath }).catch(() => {}) const errDetail = result.error != null && result.error !== '' ? String(result.error) : 'unknown' return { success: false, error: errDetail, completedSteps: i } } if (t < times - 1) await new Promise(resolve => setTimeout(resolve, 500)) } completedSteps++ if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, times) if (i < actions.length - 1) { let remainingTime = interval const countdownInterval = 100 const nextStepName = getActionName(actions[i + 1]) const nextTimes = actions[i + 1].times || 1 while (remainingTime > 0) { if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps } if (onStepComplete) onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0) const waitTime = Math.min(countdownInterval, remainingTime) await new Promise(resolve => setTimeout(resolve, waitTime)) remainingTime -= waitTime } } } return { success: true, completedSteps } } module.exports = { executeActionSequence }