auto-run-controller.js 24 KB

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