sequence-runner.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. const { logActionError } = require('./actions/echo-parser.js')
  2. const variableParser = require('./variable-parser.js')
  3. /**
  4. * 执行操作序列(schedule/if/for/while + 普通步骤)
  5. * 规则:除 schedule 的定时间隔外,均为同步串行——上一步 await 结束才执行下一步;任一步失败立即 return,后续步骤不执行。
  6. * 根级 execute 数组之间可有 stepInterval;for/if/while/try 内部及 schedule 单次 tick 内步骤间隔为 0。
  7. */
  8. function isOk(r) {
  9. return r && r.success === true
  10. }
  11. async function executeActionSequence(
  12. actions,
  13. device,
  14. folderPath,
  15. resolution,
  16. stepInterval,
  17. onStepComplete,
  18. shouldStop,
  19. depth,
  20. ctx
  21. ) {
  22. const { executeAction, logMessage, evaluateCondition, getActionName, parseDelayString, calculateWaitTime, replaceVariablesInString, state } = ctx
  23. const variableContext = state.variableContext
  24. const DEFAULT_STEP_INTERVAL = ctx.DEFAULT_STEP_INTERVAL ?? 1000
  25. /** 嵌套体内步骤紧接执行;仅根级兄弟结点之间用 stepInterval */
  26. const innerInterval = 0
  27. if (depth === 0) {
  28. state.globalStepCounter = 0
  29. state.variableContextInitialized = false
  30. state.currentWorkflowFolderPath = folderPath
  31. }
  32. let completedSteps = 0
  33. const interval = stepInterval ?? DEFAULT_STEP_INTERVAL
  34. for (let i = 0; i < actions.length; i++) {
  35. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  36. const action = actions[i]
  37. if (action.type === 'schedule') {
  38. const condition = action.condition || {}
  39. let intervalStr = condition.interval || '0s'
  40. if (replaceVariablesInString && typeof intervalStr === 'string') {
  41. intervalStr = replaceVariablesInString(intervalStr, variableContext)
  42. }
  43. let repeat = condition.repeat !== undefined ? condition.repeat : 1
  44. if (typeof repeat === 'string' && variableContext) {
  45. const varName = repeat.replace(/^\{|\}$/g, '').trim()
  46. const resolved = variableContext[varName]
  47. repeat = resolved !== undefined && resolved !== null && resolved !== '' ? (parseInt(resolved, 10) || 1) : 1
  48. }
  49. const actionsToExecute = action.interval || []
  50. const intervalMs = parseDelayString(intervalStr) || 0
  51. const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1)
  52. let iteration = 0
  53. while (iteration < maxIterations) {
  54. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  55. iteration++
  56. if (iteration > 1 && intervalMs > 0) {
  57. let remainingTime = intervalMs
  58. const countdownInterval = 100
  59. while (remainingTime > 0) {
  60. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  61. const waitTime = Math.min(countdownInterval, remainingTime)
  62. await new Promise(resolve => setTimeout(resolve, waitTime))
  63. remainingTime -= waitTime
  64. }
  65. }
  66. if (actionsToExecute.length > 0) {
  67. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  68. if (!isOk(result)) {
  69. return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps }
  70. }
  71. completedSteps += result.completedSteps || 0
  72. }
  73. }
  74. continue
  75. }
  76. if (action.type === 'if') {
  77. const conditionResult = evaluateCondition(action.condition, variableContext)
  78. const actionsToExecute = conditionResult ? (action.then || []) : (action.else || [])
  79. if (actionsToExecute.length > 0) {
  80. const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  81. if (!isOk(result)) {
  82. return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps }
  83. }
  84. completedSteps += result.completedSteps || 0
  85. }
  86. continue
  87. }
  88. if (action.type === 'for') {
  89. if (action.times != null) {
  90. let count = action.times
  91. if (typeof count === 'string') {
  92. const varName = count.replace(/^\{|\}$/g, '').trim()
  93. count = Math.max(0, parseInt(variableContext[varName], 10) || 0)
  94. } else {
  95. count = Math.max(0, parseInt(count, 10) || 0)
  96. }
  97. const timesVarKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
  98. const originalDeclaredForTimes = state.declaredVariableNames
  99. if (timesVarKey) state.declaredVariableNames = (originalDeclaredForTimes || []).concat(timesVarKey)
  100. for (let i = 0; i < count; i++) {
  101. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  102. if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = i
  103. if (action.body && action.body.length > 0) {
  104. const result = await executeActionSequence(action.body, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  105. if (!isOk(result)) {
  106. return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps }
  107. }
  108. completedSteps += result.completedSteps || 0
  109. }
  110. }
  111. if (timesVarKey) state.declaredVariableNames = originalDeclaredForTimes
  112. } else {
  113. let items = action.array
  114. if (typeof items === 'string' && items.startsWith('{') && items.endsWith('}')) {
  115. const varName = items.slice(1, -1).trim()
  116. items = variableContext[varName]
  117. }
  118. items = Array.isArray(items) ? items : []
  119. const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null
  120. const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
  121. const originalDeclared = state.declaredVariableNames
  122. const forLocals = [indexKey, variableKey].filter(Boolean)
  123. if (forLocals.length > 0) state.declaredVariableNames = (originalDeclared || []).concat(forLocals)
  124. // 每次进入 for 都先重置索引变量,避免沿用上一个 for 循环的 idx 等(多个 for 共用同一 variableContext)
  125. if (indexKey !== null) variableContext[indexKey] = 0
  126. for (let i = 0; i < items.length; i++) {
  127. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  128. if (indexKey !== null) variableContext[indexKey] = i
  129. if (variableKey !== null) variableContext[variableKey] = items[i]
  130. if (action.body && action.body.length > 0) {
  131. const result = await executeActionSequence(action.body, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  132. if (!isOk(result)) {
  133. return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps }
  134. }
  135. completedSteps += result.completedSteps || 0
  136. }
  137. }
  138. if (forLocals.length > 0) state.declaredVariableNames = originalDeclared
  139. }
  140. continue
  141. }
  142. if (action.type === 'while') {
  143. while (evaluateCondition(action.condition, variableContext)) {
  144. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  145. if (action.body && action.body.length > 0) {
  146. const result = await executeActionSequence(action.body, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  147. if (!isOk(result)) {
  148. return { success: false, error: (result && result.error != null) ? String(result.error) : 'failed', completedSteps: result && result.completedSteps != null ? result.completedSteps : completedSteps }
  149. }
  150. completedSteps += result.completedSteps || 0
  151. }
  152. }
  153. continue
  154. }
  155. if (action.type === 'try') {
  156. const tryActions = action.try || []
  157. const successActions = action.success || []
  158. const failActions = action.fail || []
  159. /** try 主路径失败后:即使 fail 分支执行成功,默认仍向上返回失败,避免 for 进入下一轮或继续执行后续兄弟步骤。需继续时请设 continueAfterFail: true */
  160. const continueAfterFail = action.continueAfterFail === true
  161. const result = tryActions.length > 0
  162. ? await executeActionSequence(tryActions, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  163. : { success: true, completedSteps: 0 }
  164. if (isOk(result) && successActions.length > 0) {
  165. const successResult = await executeActionSequence(successActions, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  166. if (!isOk(successResult)) {
  167. return { success: false, error: (successResult && successResult.error != null) ? String(successResult.error) : 'failed', completedSteps }
  168. }
  169. completedSteps += (result.completedSteps || 0) + (successResult.completedSteps || 0)
  170. } else if (isOk(result)) {
  171. completedSteps += result.completedSteps || 0
  172. } else {
  173. const errMsg = (result.error != null && result.error !== '') ? String(result.error) : 'Unknown error'
  174. const timeStr = new Date().toISOString().replace('T', ' ').slice(0, 19)
  175. await logMessage(`[sequence-runner] [try failed] ${timeStr} ${errMsg}`, folderPath).catch(() => {})
  176. if (failActions.length > 0) {
  177. const failResult = await executeActionSequence(failActions, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
  178. if (!isOk(failResult)) return failResult
  179. completedSteps += (result.completedSteps || 0) + (failResult.completedSteps || 0)
  180. if (!continueAfterFail) {
  181. return { success: false, error: errMsg, completedSteps }
  182. }
  183. } else {
  184. return result
  185. }
  186. }
  187. continue
  188. }
  189. const times = action.times || 1
  190. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, 0)
  191. const waitTime = calculateWaitTime(action.data, action.delay)
  192. if (waitTime > 0) {
  193. let remainingTime = waitTime
  194. const countdownInterval = 100
  195. const stepName = getActionName(action)
  196. while (remainingTime > 0) {
  197. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  198. if (onStepComplete) onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0)
  199. const waitTimeChunk = Math.min(countdownInterval, remainingTime)
  200. await new Promise(resolve => setTimeout(resolve, waitTimeChunk))
  201. remainingTime -= waitTimeChunk
  202. }
  203. }
  204. for (let t = 0; t < times; t++) {
  205. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  206. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, t + 1)
  207. state.globalStepCounter++
  208. const typeName = getActionName(action)
  209. // inVars/outVars 引用的变量必须在 workflow.variables 中声明,否则写 log 并失败
  210. if ((action.inVars && action.inVars.length > 0) || (action.outVars && action.outVars.length > 0)) {
  211. const declared = state.declaredVariableNames || []
  212. const validation = variableParser.validateInOutVars(action, declared)
  213. if (!validation.valid) {
  214. const errMsg = `inVars/outVars 引用了未在 variables 中声明的变量: ${validation.undeclared.join(', ')}`
  215. await logActionError(action, { success: false, error: errMsg }, { getActionName, logMessage, folderPath }).catch(() => {})
  216. return { success: false, error: errMsg, completedSteps: i }
  217. }
  218. }
  219. const result = await executeAction(action, device, folderPath, resolution)
  220. if (isOk(result) && result.skipped) { /* 步骤跳过不写 log */ }
  221. // 统一由 echo-parser.logActionError 打印结点报错,各结点只需 return { success: false, error } 即可
  222. if (!isOk(result)) {
  223. await logActionError(action, result, { getActionName, logMessage, folderPath }).catch(() => {})
  224. const errDetail = result && result.error != null && result.error !== '' ? String(result.error) : 'unknown'
  225. return { success: false, error: errDetail, completedSteps: i }
  226. }
  227. if (t < times - 1) await new Promise(resolve => setTimeout(resolve, 500))
  228. }
  229. completedSteps++
  230. if (onStepComplete) onStepComplete(i + 1, actions.length, getActionName(action), 0, times, times)
  231. if (i < actions.length - 1) {
  232. let remainingTime = interval
  233. const countdownInterval = 100
  234. const nextStepName = getActionName(actions[i + 1])
  235. const nextTimes = actions[i + 1].times || 1
  236. while (remainingTime > 0) {
  237. if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
  238. if (onStepComplete) onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0)
  239. const waitTime = Math.min(countdownInterval, remainingTime)
  240. await new Promise(resolve => setTimeout(resolve, waitTime))
  241. remainingTime -= waitTime
  242. }
  243. }
  244. }
  245. return { success: true, completedSteps }
  246. }
  247. module.exports = { executeActionSequence }