auto-run-controller.js 29 KB

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