auto-run-controller.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. (function attachBackgroundAutoRunController(root, factory) {
  2. root.MultiPageBackgroundAutoRunController = factory();
  3. })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundAutoRunControllerModule() {
  4. function createAutoRunController(deps = {}) {
  5. const {
  6. addLog,
  7. AUTO_RUN_MAX_RETRIES_PER_ROUND,
  8. AUTO_RUN_RETRY_DELAY_MS,
  9. AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
  10. AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
  11. broadcastAutoRunStatus,
  12. broadcastStopToContentScripts,
  13. cancelPendingCommands,
  14. clearStopRequest,
  15. getAutoRunStatusPayload,
  16. getErrorMessage,
  17. getFirstUnfinishedStep,
  18. getPendingAutoRunTimerPlan,
  19. getRunningSteps,
  20. getState,
  21. hasSavedProgress,
  22. isRestartCurrentAttemptError,
  23. isStopError,
  24. launchAutoRunTimerPlan,
  25. normalizeAutoRunFallbackThreadIntervalMinutes,
  26. persistAutoRunTimerPlan,
  27. resetState,
  28. runAutoSequenceFromStep,
  29. runtime,
  30. setState,
  31. sleepWithStop,
  32. waitForRunningStepsToFinish,
  33. } = deps;
  34. function createAutoRunRoundSummary(round) {
  35. return {
  36. round,
  37. status: 'pending',
  38. attempts: 0,
  39. failureReasons: [],
  40. finalFailureReason: '',
  41. };
  42. }
  43. function normalizeAutoRunRoundSummary(summary, round) {
  44. const base = createAutoRunRoundSummary(round);
  45. if (!summary || typeof summary !== 'object') {
  46. return base;
  47. }
  48. const status = String(summary.status || '').trim().toLowerCase();
  49. return {
  50. round,
  51. status: ['pending', 'success', 'failed'].includes(status) ? status : base.status,
  52. attempts: Math.max(0, Math.floor(Number(summary.attempts) || 0)),
  53. failureReasons: Array.isArray(summary.failureReasons)
  54. ? summary.failureReasons.map((item) => String(item || '').trim()).filter(Boolean)
  55. : [],
  56. finalFailureReason: String(summary.finalFailureReason || '').trim(),
  57. };
  58. }
  59. function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) {
  60. return Array.from({ length: totalRuns }, (_, index) => normalizeAutoRunRoundSummary(rawSummaries[index], index + 1));
  61. }
  62. function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) {
  63. return buildAutoRunRoundSummaries(totalRuns, roundSummaries).map((summary) => ({
  64. ...summary,
  65. failureReasons: [...summary.failureReasons],
  66. }));
  67. }
  68. function getAutoRunRoundRetryCount(summary) {
  69. return Math.max(0, Number(summary?.attempts || 0) - 1);
  70. }
  71. function formatAutoRunFailureReasons(reasons = []) {
  72. if (!Array.isArray(reasons) || !reasons.length) {
  73. return '未知错误';
  74. }
  75. const counts = new Map();
  76. for (const reason of reasons) {
  77. const normalized = String(reason || '').trim() || '未知错误';
  78. counts.set(normalized, (counts.get(normalized) || 0) + 1);
  79. }
  80. return Array.from(counts.entries())
  81. .map(([reason, count]) => (count > 1 ? `${reason}(${count}次)` : reason))
  82. .join(';');
  83. }
  84. async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
  85. const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
  86. const successRounds = summaries.filter((item) => item.status === 'success');
  87. const failedRounds = summaries.filter((item) => item.status === 'failed');
  88. const pendingRounds = summaries.filter((item) => item.status === 'pending');
  89. await addLog('=== 自动运行汇总 ===', failedRounds.length ? 'warn' : 'ok');
  90. await addLog(
  91. `总轮数:${totalRuns};成功:${successRounds.length};失败:${failedRounds.length};未完成:${pendingRounds.length}`,
  92. failedRounds.length ? 'warn' : 'ok'
  93. );
  94. if (successRounds.length) {
  95. await addLog(
  96. `成功轮次:${successRounds
  97. .map((item) => `第 ${item.round} 轮(重试 ${getAutoRunRoundRetryCount(item)} 次)`)
  98. .join(';')}`,
  99. 'ok'
  100. );
  101. }
  102. if (failedRounds.length) {
  103. await addLog(
  104. `失败轮次:${failedRounds
  105. .map((item) => {
  106. const retryCount = getAutoRunRoundRetryCount(item);
  107. const finalReason = item.finalFailureReason || item.failureReasons[item.failureReasons.length - 1] || '未知错误';
  108. const reasonSummary = formatAutoRunFailureReasons(item.failureReasons);
  109. return `第 ${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason};失败记录:${reasonSummary})`;
  110. })
  111. .join(';')}`,
  112. 'error'
  113. );
  114. }
  115. if (pendingRounds.length) {
  116. await addLog(
  117. `未完成轮次:${pendingRounds.map((item) => `第 ${item.round} 轮`).join(';')}`,
  118. 'warn'
  119. );
  120. }
  121. }
  122. async function skipAutoRunCountdown() {
  123. const state = await getState();
  124. const plan = getPendingAutoRunTimerPlan(state);
  125. if (!plan || state.autoRunPhase !== 'waiting_interval') {
  126. return false;
  127. }
  128. return launchAutoRunTimerPlan('manual', {
  129. expectedKinds: [
  130. AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
  131. AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
  132. ],
  133. });
  134. }
  135. async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options = {}) {
  136. const { autoRunSkipFailures = false, roundSummaries = [] } = options;
  137. if (totalRuns <= 1 || targetRun >= totalRuns) {
  138. return false;
  139. }
  140. const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
  141. (await getState()).autoRunFallbackThreadIntervalMinutes
  142. );
  143. if (fallbackThreadIntervalMinutes <= 0) {
  144. return false;
  145. }
  146. const currentRuntime = runtime.get();
  147. const statusLabel = roundSummary?.status === 'failed' ? '失败' : '完成';
  148. await addLog(
  149. `线程间隔:第 ${targetRun}/${totalRuns} 轮已${statusLabel},等待 ${fallbackThreadIntervalMinutes} 分钟后开始下一轮。`,
  150. 'info'
  151. );
  152. await persistAutoRunTimerPlan({
  153. kind: AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
  154. fireAt: Date.now() + fallbackThreadIntervalMinutes * 60 * 1000,
  155. currentRun: targetRun,
  156. totalRuns,
  157. attemptRun: currentRuntime.autoRunAttemptRun,
  158. autoRunSkipFailures,
  159. roundSummaries,
  160. countdownTitle: '线程间隔中',
  161. countdownNote: `第 ${Math.min(targetRun + 1, totalRuns)}/${totalRuns} 轮即将开始`,
  162. }, {
  163. autoRunSkipFailures,
  164. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  165. });
  166. runtime.set({ autoRunActive: false });
  167. return true;
  168. }
  169. async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options = {}) {
  170. const { autoRunSkipFailures = false, roundSummaries = [] } = options;
  171. const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
  172. (await getState()).autoRunFallbackThreadIntervalMinutes
  173. );
  174. if (fallbackThreadIntervalMinutes <= 0) {
  175. return false;
  176. }
  177. await addLog(
  178. `线程间隔:等待 ${fallbackThreadIntervalMinutes} 分钟后开始第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试。`,
  179. 'info'
  180. );
  181. await persistAutoRunTimerPlan({
  182. kind: AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
  183. fireAt: Date.now() + fallbackThreadIntervalMinutes * 60 * 1000,
  184. currentRun: targetRun,
  185. totalRuns,
  186. attemptRun: nextAttemptRun,
  187. autoRunSkipFailures,
  188. roundSummaries,
  189. countdownTitle: '线程间隔中',
  190. countdownNote: `第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试即将开始`,
  191. }, {
  192. autoRunSkipFailures,
  193. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  194. });
  195. runtime.set({ autoRunActive: false });
  196. return true;
  197. }
  198. async function handleAutoRunLoopUnhandledError(error) {
  199. const currentRuntime = runtime.get();
  200. console.error('Auto run loop crashed:', error);
  201. if (!isStopError(error)) {
  202. await addLog(`自动运行异常终止:${getErrorMessage(error) || '未知错误'}`, 'error');
  203. }
  204. runtime.set({ autoRunActive: false });
  205. await broadcastAutoRunStatus('stopped', {
  206. currentRun: currentRuntime.autoRunCurrentRun,
  207. totalRuns: currentRuntime.autoRunTotalRuns,
  208. attemptRun: currentRuntime.autoRunAttemptRun,
  209. }, {
  210. autoRunTimerPlan: null,
  211. scheduledAutoRunPlan: null,
  212. });
  213. clearStopRequest();
  214. }
  215. function startAutoRunLoop(totalRuns, options = {}) {
  216. autoRunLoop(totalRuns, options).catch((error) => {
  217. handleAutoRunLoopUnhandledError(error).catch(() => {});
  218. });
  219. }
  220. async function autoRunLoop(totalRuns, options = {}) {
  221. let currentRuntime = runtime.get();
  222. if (currentRuntime.autoRunActive) {
  223. await addLog('自动运行已在进行中', 'warn');
  224. return;
  225. }
  226. clearStopRequest();
  227. runtime.set({
  228. autoRunActive: true,
  229. autoRunTotalRuns: totalRuns,
  230. autoRunCurrentRun: 0,
  231. autoRunAttemptRun: 0,
  232. });
  233. currentRuntime = runtime.get();
  234. const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
  235. const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
  236. const resumeCurrentRun = Number.isInteger(options.resumeCurrentRun) && options.resumeCurrentRun > 0
  237. ? Math.min(totalRuns, options.resumeCurrentRun)
  238. : 1;
  239. const resumeAttemptRun = Number.isInteger(options.resumeAttemptRun) && options.resumeAttemptRun > 0
  240. ? Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, options.resumeAttemptRun)
  241. : 1;
  242. let continueCurrentOnFirstAttempt = initialMode === 'continue';
  243. let forceFreshTabsNextRun = false;
  244. let stoppedEarly = false;
  245. let parkedByTimer = false;
  246. const roundSummaries = buildAutoRunRoundSummaries(totalRuns, options.resumeRoundSummaries);
  247. if (continueCurrentOnFirstAttempt && resumeCurrentRun > 1) {
  248. for (let round = 1; round < resumeCurrentRun; round += 1) {
  249. const summary = roundSummaries[round - 1];
  250. if (summary.status === 'pending') {
  251. summary.status = 'success';
  252. if (!summary.attempts) {
  253. summary.attempts = 1;
  254. }
  255. }
  256. }
  257. }
  258. let successfulRuns = roundSummaries.filter((item) => item.status === 'success').length;
  259. const initialState = await getState();
  260. const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
  261. ? 'waiting_step'
  262. : 'running';
  263. const showResumePosition = continueCurrentOnFirstAttempt || resumeCurrentRun > 1 || resumeAttemptRun > 1;
  264. await setState({
  265. autoRunSkipFailures,
  266. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  267. ...getAutoRunStatusPayload(initialPhase, {
  268. currentRun: showResumePosition ? resumeCurrentRun : 0,
  269. totalRuns,
  270. attemptRun: showResumePosition ? resumeAttemptRun : 0,
  271. }),
  272. });
  273. for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) {
  274. const roundSummary = roundSummaries[targetRun - 1];
  275. const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
  276. let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1;
  277. let reuseExistingProgress = resumingCurrentRound;
  278. const maxAttemptsForRound = autoRunSkipFailures
  279. ? AUTO_RUN_MAX_RETRIES_PER_ROUND + 1
  280. : Math.max(1, attemptRun);
  281. while (attemptRun <= maxAttemptsForRound) {
  282. runtime.set({
  283. autoRunCurrentRun: targetRun,
  284. autoRunAttemptRun: attemptRun,
  285. });
  286. roundSummary.attempts = attemptRun;
  287. let startStep = 1;
  288. let useExistingProgress = false;
  289. if (reuseExistingProgress) {
  290. let currentState = await getState();
  291. if (getRunningSteps(currentState.stepStatuses).length) {
  292. currentState = await waitForRunningStepsToFinish({
  293. currentRun: targetRun,
  294. totalRuns,
  295. attemptRun,
  296. });
  297. }
  298. const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
  299. if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
  300. startStep = resumeStep;
  301. useExistingProgress = true;
  302. } else if (hasSavedProgress(currentState.stepStatuses)) {
  303. await addLog('检测到当前流程已处理完成,本轮将改为从步骤 1 重新开始。', 'info');
  304. }
  305. }
  306. if (!useExistingProgress) {
  307. const prevState = await getState();
  308. const keepSettings = {
  309. vpsUrl: prevState.vpsUrl,
  310. vpsPassword: prevState.vpsPassword,
  311. customPassword: prevState.customPassword,
  312. autoRunSkipFailures: prevState.autoRunSkipFailures,
  313. autoRunFallbackThreadIntervalMinutes: prevState.autoRunFallbackThreadIntervalMinutes,
  314. autoRunDelayEnabled: prevState.autoRunDelayEnabled,
  315. autoRunDelayMinutes: prevState.autoRunDelayMinutes,
  316. autoStepDelaySeconds: prevState.autoStepDelaySeconds,
  317. mailProvider: prevState.mailProvider,
  318. emailGenerator: prevState.emailGenerator,
  319. emailPrefix: prevState.emailPrefix,
  320. inbucketHost: prevState.inbucketHost,
  321. inbucketMailbox: prevState.inbucketMailbox,
  322. cloudflareDomain: prevState.cloudflareDomain,
  323. cloudflareDomains: prevState.cloudflareDomains,
  324. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  325. tabRegistry: {},
  326. sourceLastUrls: {},
  327. ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
  328. };
  329. await resetState();
  330. await setState(keepSettings);
  331. deps.chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => { });
  332. await sleepWithStop(500);
  333. } else {
  334. await setState({
  335. autoRunSkipFailures,
  336. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  337. ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
  338. });
  339. }
  340. if (forceFreshTabsNextRun) {
  341. await addLog(`上一轮尝试已放弃,当前开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试。`, 'warn');
  342. forceFreshTabsNextRun = false;
  343. }
  344. try {
  345. deps.throwIfStopped();
  346. await broadcastAutoRunStatus('running', {
  347. currentRun: targetRun,
  348. totalRuns,
  349. attemptRun,
  350. });
  351. await runAutoSequenceFromStep(startStep, {
  352. targetRun,
  353. totalRuns,
  354. attemptRuns: attemptRun,
  355. continued: useExistingProgress,
  356. });
  357. roundSummary.status = 'success';
  358. roundSummary.finalFailureReason = '';
  359. successfulRuns += 1;
  360. await setState({
  361. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  362. });
  363. await addLog(`=== 第 ${targetRun}/${totalRuns} 轮完成(第 ${attemptRun} 次尝试成功)===`, 'ok');
  364. break;
  365. } catch (err) {
  366. if (isStopError(err)) {
  367. stoppedEarly = true;
  368. await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
  369. await broadcastAutoRunStatus('stopped', {
  370. currentRun: targetRun,
  371. totalRuns,
  372. attemptRun,
  373. });
  374. break;
  375. }
  376. const reason = getErrorMessage(err);
  377. roundSummary.failureReasons.push(reason);
  378. const canRetry = autoRunSkipFailures && attemptRun < maxAttemptsForRound;
  379. await setState({
  380. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  381. });
  382. if (canRetry) {
  383. const retryIndex = attemptRun;
  384. if (isRestartCurrentAttemptError(err)) {
  385. await addLog(`第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试需要整轮重开:${reason}`, 'warn');
  386. } else {
  387. await addLog(`第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试失败:${reason}`, 'error');
  388. }
  389. cancelPendingCommands('当前尝试已放弃。');
  390. await broadcastStopToContentScripts();
  391. await broadcastAutoRunStatus('retrying', {
  392. currentRun: targetRun,
  393. totalRuns,
  394. attemptRun,
  395. });
  396. forceFreshTabsNextRun = true;
  397. await addLog(
  398. `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
  399. 'warn'
  400. );
  401. try {
  402. await sleepWithStop(AUTO_RUN_RETRY_DELAY_MS);
  403. } catch (sleepError) {
  404. if (isStopError(sleepError)) {
  405. stoppedEarly = true;
  406. await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
  407. await broadcastAutoRunStatus('stopped', {
  408. currentRun: targetRun,
  409. totalRuns,
  410. attemptRun,
  411. });
  412. break;
  413. }
  414. throw sleepError;
  415. }
  416. try {
  417. const parkedForRetry = await waitBeforeAutoRunRetry(targetRun, totalRuns, attemptRun + 1, {
  418. autoRunSkipFailures,
  419. roundSummaries,
  420. });
  421. if (parkedForRetry) {
  422. parkedByTimer = true;
  423. break;
  424. }
  425. } catch (sleepError) {
  426. if (isStopError(sleepError)) {
  427. stoppedEarly = true;
  428. await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
  429. await broadcastAutoRunStatus('stopped', {
  430. currentRun: targetRun,
  431. totalRuns,
  432. attemptRun,
  433. });
  434. break;
  435. }
  436. throw sleepError;
  437. }
  438. attemptRun += 1;
  439. reuseExistingProgress = false;
  440. continue;
  441. }
  442. roundSummary.status = 'failed';
  443. roundSummary.finalFailureReason = reason;
  444. await setState({
  445. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  446. });
  447. if (!autoRunSkipFailures) {
  448. cancelPendingCommands('当前轮执行失败。');
  449. await broadcastStopToContentScripts();
  450. await addLog('自动重试未开启,自动运行将在当前失败后停止。', 'warn');
  451. stoppedEarly = true;
  452. await broadcastAutoRunStatus('stopped', {
  453. currentRun: targetRun,
  454. totalRuns,
  455. attemptRun,
  456. });
  457. break;
  458. }
  459. await addLog(`第 ${targetRun}/${totalRuns} 轮最终失败:${reason}`, 'error');
  460. await addLog(
  461. targetRun < totalRuns
  462. ? `第 ${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,继续下一轮。`
  463. : `第 ${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,本次自动运行结束。`,
  464. 'warn'
  465. );
  466. cancelPendingCommands('当前轮已达到重试上限。');
  467. await broadcastStopToContentScripts();
  468. forceFreshTabsNextRun = true;
  469. break;
  470. } finally {
  471. reuseExistingProgress = false;
  472. continueCurrentOnFirstAttempt = false;
  473. }
  474. }
  475. if (stoppedEarly || parkedByTimer) {
  476. break;
  477. }
  478. try {
  479. const parkedForNextRound = await waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, {
  480. autoRunSkipFailures,
  481. roundSummaries,
  482. });
  483. if (parkedForNextRound) {
  484. parkedByTimer = true;
  485. break;
  486. }
  487. } catch (sleepError) {
  488. if (isStopError(sleepError)) {
  489. stoppedEarly = true;
  490. await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
  491. await broadcastAutoRunStatus('stopped', {
  492. currentRun: targetRun,
  493. totalRuns,
  494. attemptRun: runtime.get().autoRunAttemptRun,
  495. });
  496. break;
  497. }
  498. throw sleepError;
  499. }
  500. }
  501. if (parkedByTimer) {
  502. runtime.set({ autoRunActive: false });
  503. clearStopRequest();
  504. return;
  505. }
  506. await setState({
  507. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  508. });
  509. await logAutoRunFinalSummary(totalRuns, roundSummaries);
  510. const finalRuntime = runtime.get();
  511. if (deps.getStopRequested() || stoppedEarly) {
  512. await addLog(`=== 已停止,完成 ${successfulRuns}/${finalRuntime.autoRunTotalRuns} 轮 ===`, 'warn');
  513. await broadcastAutoRunStatus('stopped', {
  514. currentRun: finalRuntime.autoRunCurrentRun,
  515. totalRuns: finalRuntime.autoRunTotalRuns,
  516. attemptRun: finalRuntime.autoRunAttemptRun,
  517. });
  518. } else {
  519. await addLog(`=== 全部 ${finalRuntime.autoRunTotalRuns} 轮已执行完成,成功 ${successfulRuns} 轮 ===`, 'ok');
  520. await broadcastAutoRunStatus('complete', {
  521. currentRun: finalRuntime.autoRunTotalRuns,
  522. totalRuns: finalRuntime.autoRunTotalRuns,
  523. attemptRun: finalRuntime.autoRunAttemptRun,
  524. });
  525. }
  526. runtime.set({ autoRunActive: false });
  527. const afterRuntime = runtime.get();
  528. await setState({
  529. autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
  530. autoRunTimerPlan: null,
  531. scheduledAutoRunPlan: null,
  532. ...getAutoRunStatusPayload(deps.getStopRequested() || stoppedEarly ? 'stopped' : 'complete', {
  533. currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns,
  534. totalRuns: afterRuntime.autoRunTotalRuns,
  535. attemptRun: afterRuntime.autoRunAttemptRun,
  536. }),
  537. });
  538. clearStopRequest();
  539. }
  540. return {
  541. autoRunLoop,
  542. buildAutoRunRoundSummaries,
  543. createAutoRunRoundSummary,
  544. formatAutoRunFailureReasons,
  545. getAutoRunRoundRetryCount,
  546. handleAutoRunLoopUnhandledError,
  547. logAutoRunFinalSummary,
  548. normalizeAutoRunRoundSummary,
  549. serializeAutoRunRoundSummaries,
  550. skipAutoRunCountdown,
  551. startAutoRunLoop,
  552. waitBetweenAutoRunRounds,
  553. waitBeforeAutoRunRetry,
  554. };
  555. }
  556. return {
  557. createAutoRunController,
  558. };
  559. });