sequence-runner.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. const { logActionError } = require('./actions/echo-parser.js')
  2. /**
  3. * 执行操作序列(schedule/if/for/while + 普通步骤)
  4. * 单文件 ≤500 行。ctx: executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, state
  5. * state: variableContext, globalStepCounter, currentWorkflowFolderPath, variableContextInitialized
  6. */
  7. async function executeActionSequence(
  8. actions,
  9. device,
  10. folderPath,
  11. resolution,
  12. stepInterval,
  13. onStepComplete,
  14. shouldStop,
  15. depth,
  16. ctx
  17. ) {
  18. const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state } = ctx
  19. const variableContext = state.variableContext
  20. const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000
  21. if (depth === 0) {
  22. state.globalStepCounter = 0
  23. state.variableContextInitialized = false
  24. state.currentWorkflowFolderPath = folderPath
  25. }
  26. let completedSteps = 0
  27. const interval = stepInterval ?? DEFAULT_STEP_INTERVAL
  28. for (let i = 0; i < actions.length; i++) {
  29. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  30. const action = actions[i]
  31. if (action.type === 'schedule') {
  32. const condition = action.condition || {}
  33. let intervalStr = condition.interval || '0s'
  34. if (replaceVariablesInString && typeof intervalStr === 'string') {
  35. intervalStr = replaceVariablesInString(intervalStr, variableContext)
  36. }
  37. let repeat = condition.repeat !== undefined ? condition.repeat : 1
  38. if (typeof repeat === 'string' && variableContext) {
  39. const varName = repeat.replace(/^\{|\}$/g, '').trim()
  40. const resolved = variableContext[varName]
  41. repeat = resolved !== undefined && resolved !== null && resolved !== '' ? (parseInt(resolved, 10) || 1) : 1
  42. }
  43. const actionsToExecute = action.interval || []
  44. const intervalMs = parseDelayString(intervalStr) || 0
  45. const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1)
  46. let iteration = 0
  47. while (iteration < maxIterations) {
  48. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  49. iteration++
  50. if (iteration > 1 && intervalMs > 0) {
  51. let remainingTime = intervalMs
  52. const countdownInterval = 100
  53. while (remainingTime > 0) {
  54. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  55. const waitTime = Math.min(countdownInterval, remainingTime)
  56. await new Promise(resolve => setTimeout(resolve, waitTime))
  57. remainingTime -= waitTime
  58. }
  59. }
  60. if (actionsToExecute.length > 0) {
  61. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  62. if (!result.success) return result
  63. completedSteps += result.completedSteps || 0
  64. }
  65. }
  66. continue
  67. }
  68. if (action.type === 'if') {
  69. const conditionResult = evaluateCondition(action.condition, variableContext)
  70. const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || [])
  71. if (actionsToExecute.length > 0) {
  72. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  73. if (!result.success) return result
  74. completedSteps += result.completedSteps || 0
  75. }
  76. continue
  77. }
  78. if (action.type === 'for') {
  79. if (action.times != null) {
  80. let count = action.times
  81. if (typeof count === 'string') {
  82. const varName = count.replace(/^\{|\}$/g, '').trim()
  83. count = Math.max(0, parseInt(variableContext[varName], 10) || 0)
  84. } else {
  85. count = Math.max(0, parseInt(count, 10) || 0)
  86. }
  87. for (let i = 0; i < count; i++) {
  88. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  89. if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = i
  90. if (action.body && action.body.length > 0) {
  91. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  92. if (!result.success) return result
  93. completedSteps += result.completedSteps || 0
  94. }
  95. }
  96. } else {
  97. const items = Array.isArray(action.items) ? action.items : []
  98. const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null
  99. const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
  100. for (let i = 0; i < items.length; i++) {
  101. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  102. if (indexKey !== null) variableContext[indexKey] = i
  103. if (variableKey !== null) variableContext[variableKey] = items[i]
  104. if (action.body && action.body.length > 0) {
  105. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  106. if (!result.success) return result
  107. completedSteps += result.completedSteps || 0
  108. }
  109. }
  110. }
  111. continue
  112. }
  113. if (action.type === 'while') {
  114. while (evaluateCondition(action.condition, variableContext)) {
  115. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  116. if (action.body && action.body.length > 0) {
  117. const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  118. if (!result.success) return result
  119. completedSteps += result.completedSteps || 0
  120. }
  121. }
  122. continue
  123. }
  124. if (action.type === 'try') {
  125. const tryActions = action.try || action.body || []
  126. const successActions = action.success || []
  127. const failActions = action.fail || action.catch || []
  128. const result = tryActions.length > 0
  129. ? await executeActionSequence(tryActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  130. : { success: true, completedSteps: 0 }
  131. if (result.success && successActions.length > 0) {
  132. const successResult = await executeActionSequence(successActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  133. if (!successResult.success) return successResult
  134. completedSteps += (result.completedSteps || 0) + (successResult.completedSteps || 0)
  135. } else if (result.success) {
  136. completedSteps += result.completedSteps || 0
  137. } else {
  138. const errMsg = (result.error != null && result.error !== '') ? String(result.error) : 'Unknown error'
  139. const timeStr = new Date().toISOString().replace('T', ' ').slice(0, 19)
  140. await logMessage(`[sequence-runner] [try failed] ${timeStr} ${errMsg}`, folderPath).catch(() => {})
  141. if (failActions.length > 0) {
  142. const failResult = await executeActionSequence(failActions, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
  143. if (!failResult.success) return failResult
  144. completedSteps += (result.completedSteps || 0) + (failResult.completedSteps || 0)
  145. } else {
  146. return result
  147. }
  148. }
  149. continue
  150. }
  151. const times = action.times || 1
  152. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, 0)
  153. const waitTime = calculateWaitTime(action.data, action.delay)
  154. if (waitTime > 0) {
  155. let remainingTime = waitTime
  156. const countdownInterval = 100
  157. const stepName = getActionName(action)
  158. while (remainingTime > 0) {
  159. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  160. if (onStepComplete) onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0)
  161. const waitTimeChunk = Math.min(countdownInterval, remainingTime)
  162. await new Promise(resolve => setTimeout(resolve, waitTimeChunk))
  163. remainingTime -= waitTimeChunk
  164. }
  165. }
  166. for (let t = 0; t < times; t++) {
  167. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  168. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1)
  169. state.globalStepCounter++
  170. const typeName = getActionName(action)
  171. const result = await executeAction(action, device, folderPath, resolution)
  172. if (result.success && result.skipped) { /* 步骤跳过不写 log */ }
  173. // 统一由 echo-parser.logActionError 打印结点报错,各结点只需 return { success: false, error } 即可
  174. if (!result.success) {
  175. await logActionError(action, result, { getActionName, logMessage, folderPath }).catch(() => {})
  176. const errDetail = result.error != null && result.error !== '' ? String(result.error) : 'unknown'
  177. return { success: false, error: errDetail, completedSteps: i }
  178. }
  179. if (t < times - 1) await new Promise(resolve => setTimeout(resolve, 500))
  180. }
  181. completedSteps++
  182. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, times)
  183. if (i < actions.length - 1) {
  184. let remainingTime = interval
  185. const countdownInterval = 100
  186. const nextStepName = getActionName(actions[i + 1])
  187. const nextTimes = actions[i + 1].times || 1
  188. while (remainingTime > 0) {
  189. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  190. if (onStepComplete) onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0)
  191. const waitTime = Math.min(countdownInterval, remainingTime)
  192. await new Promise(resolve => setTimeout(resolve, waitTime))
  193. remainingTime -= waitTime
  194. }
  195. }
  196. }
  197. return { success: true, completedSteps }
  198. }
  199. module.exports = { executeActionSequence }