auto-run-controller.js 30 KB

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