|
|
@@ -0,0 +1,629 @@
|
|
|
+(function attachBackgroundAutoRunController(root, factory) {
|
|
|
+ root.MultiPageBackgroundAutoRunController = factory();
|
|
|
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundAutoRunControllerModule() {
|
|
|
+ function createAutoRunController(deps = {}) {
|
|
|
+ const {
|
|
|
+ addLog,
|
|
|
+ appendAccountRunRecord,
|
|
|
+ AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
|
|
+ AUTO_RUN_RETRY_DELAY_MS,
|
|
|
+ AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
|
|
+ AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
|
|
|
+ broadcastAutoRunStatus,
|
|
|
+ broadcastStopToContentScripts,
|
|
|
+ cancelPendingCommands,
|
|
|
+ clearStopRequest,
|
|
|
+ getAutoRunStatusPayload,
|
|
|
+ getErrorMessage,
|
|
|
+ getFirstUnfinishedStep,
|
|
|
+ getPendingAutoRunTimerPlan,
|
|
|
+ getRunningSteps,
|
|
|
+ getState,
|
|
|
+ hasSavedProgress,
|
|
|
+ isRestartCurrentAttemptError,
|
|
|
+ isStopError,
|
|
|
+ launchAutoRunTimerPlan,
|
|
|
+ normalizeAutoRunFallbackThreadIntervalMinutes,
|
|
|
+ persistAutoRunTimerPlan,
|
|
|
+ resetState,
|
|
|
+ runAutoSequenceFromStep,
|
|
|
+ runtime,
|
|
|
+ setState,
|
|
|
+ sleepWithStop,
|
|
|
+ waitForRunningStepsToFinish,
|
|
|
+ } = deps;
|
|
|
+
|
|
|
+ function createAutoRunRoundSummary(round) {
|
|
|
+ return {
|
|
|
+ round,
|
|
|
+ status: 'pending',
|
|
|
+ attempts: 0,
|
|
|
+ failureReasons: [],
|
|
|
+ finalFailureReason: '',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeAutoRunRoundSummary(summary, round) {
|
|
|
+ const base = createAutoRunRoundSummary(round);
|
|
|
+ if (!summary || typeof summary !== 'object') {
|
|
|
+ return base;
|
|
|
+ }
|
|
|
+
|
|
|
+ const status = String(summary.status || '').trim().toLowerCase();
|
|
|
+ return {
|
|
|
+ round,
|
|
|
+ status: ['pending', 'success', 'failed'].includes(status) ? status : base.status,
|
|
|
+ attempts: Math.max(0, Math.floor(Number(summary.attempts) || 0)),
|
|
|
+ failureReasons: Array.isArray(summary.failureReasons)
|
|
|
+ ? summary.failureReasons.map((item) => String(item || '').trim()).filter(Boolean)
|
|
|
+ : [],
|
|
|
+ finalFailureReason: String(summary.finalFailureReason || '').trim(),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) {
|
|
|
+ return Array.from({ length: totalRuns }, (_, index) => normalizeAutoRunRoundSummary(rawSummaries[index], index + 1));
|
|
|
+ }
|
|
|
+
|
|
|
+ function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) {
|
|
|
+ return buildAutoRunRoundSummaries(totalRuns, roundSummaries).map((summary) => ({
|
|
|
+ ...summary,
|
|
|
+ failureReasons: [...summary.failureReasons],
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ function getAutoRunRoundRetryCount(summary) {
|
|
|
+ return Math.max(0, Number(summary?.attempts || 0) - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatAutoRunFailureReasons(reasons = []) {
|
|
|
+ if (!Array.isArray(reasons) || !reasons.length) {
|
|
|
+ return '未知错误';
|
|
|
+ }
|
|
|
+
|
|
|
+ const counts = new Map();
|
|
|
+ for (const reason of reasons) {
|
|
|
+ const normalized = String(reason || '').trim() || '未知错误';
|
|
|
+ counts.set(normalized, (counts.get(normalized) || 0) + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Array.from(counts.entries())
|
|
|
+ .map(([reason, count]) => (count > 1 ? `${reason}(${count}次)` : reason))
|
|
|
+ .join(';');
|
|
|
+ }
|
|
|
+
|
|
|
+ async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
|
|
|
+ const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
|
|
|
+ const successRounds = summaries.filter((item) => item.status === 'success');
|
|
|
+ const failedRounds = summaries.filter((item) => item.status === 'failed');
|
|
|
+ const pendingRounds = summaries.filter((item) => item.status === 'pending');
|
|
|
+
|
|
|
+ await addLog('=== 自动运行汇总 ===', failedRounds.length ? 'warn' : 'ok');
|
|
|
+ await addLog(
|
|
|
+ `总轮数:${totalRuns};成功:${successRounds.length};失败:${failedRounds.length};未完成:${pendingRounds.length}`,
|
|
|
+ failedRounds.length ? 'warn' : 'ok'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (successRounds.length) {
|
|
|
+ await addLog(
|
|
|
+ `成功轮次:${successRounds
|
|
|
+ .map((item) => `第 ${item.round} 轮(重试 ${getAutoRunRoundRetryCount(item)} 次)`)
|
|
|
+ .join(';')}`,
|
|
|
+ 'ok'
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (failedRounds.length) {
|
|
|
+ await addLog(
|
|
|
+ `失败轮次:${failedRounds
|
|
|
+ .map((item) => {
|
|
|
+ const retryCount = getAutoRunRoundRetryCount(item);
|
|
|
+ const finalReason = item.finalFailureReason || item.failureReasons[item.failureReasons.length - 1] || '未知错误';
|
|
|
+ const reasonSummary = formatAutoRunFailureReasons(item.failureReasons);
|
|
|
+ return `第 ${item.round} 轮(重试 ${retryCount} 次,最终原因:${finalReason};失败记录:${reasonSummary})`;
|
|
|
+ })
|
|
|
+ .join(';')}`,
|
|
|
+ 'error'
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pendingRounds.length) {
|
|
|
+ await addLog(
|
|
|
+ `未完成轮次:${pendingRounds.map((item) => `第 ${item.round} 轮`).join(';')}`,
|
|
|
+ 'warn'
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function skipAutoRunCountdown() {
|
|
|
+ const state = await getState();
|
|
|
+ const plan = getPendingAutoRunTimerPlan(state);
|
|
|
+ if (!plan || state.autoRunPhase !== 'waiting_interval') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return launchAutoRunTimerPlan('manual', {
|
|
|
+ expectedKinds: [
|
|
|
+ AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
|
|
|
+ AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, options = {}) {
|
|
|
+ const { autoRunSkipFailures = false, roundSummaries = [] } = options;
|
|
|
+ if (totalRuns <= 1 || targetRun >= totalRuns) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
|
|
|
+ (await getState()).autoRunFallbackThreadIntervalMinutes
|
|
|
+ );
|
|
|
+ if (fallbackThreadIntervalMinutes <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentRuntime = runtime.get();
|
|
|
+ const statusLabel = roundSummary?.status === 'failed' ? '失败' : '完成';
|
|
|
+ await addLog(
|
|
|
+ `线程间隔:第 ${targetRun}/${totalRuns} 轮已${statusLabel},等待 ${fallbackThreadIntervalMinutes} 分钟后开始下一轮。`,
|
|
|
+ 'info'
|
|
|
+ );
|
|
|
+ await persistAutoRunTimerPlan({
|
|
|
+ kind: AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
|
|
|
+ fireAt: Date.now() + fallbackThreadIntervalMinutes * 60 * 1000,
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun: currentRuntime.autoRunAttemptRun,
|
|
|
+ autoRunSkipFailures,
|
|
|
+ roundSummaries,
|
|
|
+ countdownTitle: '线程间隔中',
|
|
|
+ countdownNote: `第 ${Math.min(targetRun + 1, totalRuns)}/${totalRuns} 轮即将开始`,
|
|
|
+ }, {
|
|
|
+ autoRunSkipFailures,
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ });
|
|
|
+ runtime.set({ autoRunActive: false });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun, options = {}) {
|
|
|
+ const { autoRunSkipFailures = false, roundSummaries = [] } = options;
|
|
|
+ const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
|
|
|
+ (await getState()).autoRunFallbackThreadIntervalMinutes
|
|
|
+ );
|
|
|
+ if (fallbackThreadIntervalMinutes <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ await addLog(
|
|
|
+ `线程间隔:等待 ${fallbackThreadIntervalMinutes} 分钟后开始第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试。`,
|
|
|
+ 'info'
|
|
|
+ );
|
|
|
+ await persistAutoRunTimerPlan({
|
|
|
+ kind: AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
|
|
+ fireAt: Date.now() + fallbackThreadIntervalMinutes * 60 * 1000,
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun: nextAttemptRun,
|
|
|
+ autoRunSkipFailures,
|
|
|
+ roundSummaries,
|
|
|
+ countdownTitle: '线程间隔中',
|
|
|
+ countdownNote: `第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试即将开始`,
|
|
|
+ }, {
|
|
|
+ autoRunSkipFailures,
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ });
|
|
|
+ runtime.set({ autoRunActive: false });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleAutoRunLoopUnhandledError(error) {
|
|
|
+ const currentRuntime = runtime.get();
|
|
|
+ console.error('Auto run loop crashed:', error);
|
|
|
+ if (!isStopError(error)) {
|
|
|
+ await addLog(`自动运行异常终止:${getErrorMessage(error) || '未知错误'}`, 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ runtime.set({ autoRunActive: false });
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: currentRuntime.autoRunCurrentRun,
|
|
|
+ totalRuns: currentRuntime.autoRunTotalRuns,
|
|
|
+ attemptRun: currentRuntime.autoRunAttemptRun,
|
|
|
+ }, {
|
|
|
+ autoRunTimerPlan: null,
|
|
|
+ scheduledAutoRunPlan: null,
|
|
|
+ });
|
|
|
+ clearStopRequest();
|
|
|
+ }
|
|
|
+
|
|
|
+ function startAutoRunLoop(totalRuns, options = {}) {
|
|
|
+ autoRunLoop(totalRuns, options).catch((error) => {
|
|
|
+ handleAutoRunLoopUnhandledError(error).catch(() => {});
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async function autoRunLoop(totalRuns, options = {}) {
|
|
|
+ let currentRuntime = runtime.get();
|
|
|
+ if (currentRuntime.autoRunActive) {
|
|
|
+ await addLog('自动运行已在进行中', 'warn');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ clearStopRequest();
|
|
|
+ runtime.set({
|
|
|
+ autoRunActive: true,
|
|
|
+ autoRunTotalRuns: totalRuns,
|
|
|
+ autoRunCurrentRun: 0,
|
|
|
+ autoRunAttemptRun: 0,
|
|
|
+ });
|
|
|
+ currentRuntime = runtime.get();
|
|
|
+
|
|
|
+ const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
|
|
+ const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
|
|
|
+ const resumeCurrentRun = Number.isInteger(options.resumeCurrentRun) && options.resumeCurrentRun > 0
|
|
|
+ ? Math.min(totalRuns, options.resumeCurrentRun)
|
|
|
+ : 1;
|
|
|
+ const resumeAttemptRun = Number.isInteger(options.resumeAttemptRun) && options.resumeAttemptRun > 0
|
|
|
+ ? Math.min(AUTO_RUN_MAX_RETRIES_PER_ROUND + 1, options.resumeAttemptRun)
|
|
|
+ : 1;
|
|
|
+ let continueCurrentOnFirstAttempt = initialMode === 'continue';
|
|
|
+ let forceFreshTabsNextRun = false;
|
|
|
+ let stoppedEarly = false;
|
|
|
+ let parkedByTimer = false;
|
|
|
+ const roundSummaries = buildAutoRunRoundSummaries(totalRuns, options.resumeRoundSummaries);
|
|
|
+
|
|
|
+ if (continueCurrentOnFirstAttempt && resumeCurrentRun > 1) {
|
|
|
+ for (let round = 1; round < resumeCurrentRun; round += 1) {
|
|
|
+ const summary = roundSummaries[round - 1];
|
|
|
+ if (summary.status === 'pending') {
|
|
|
+ summary.status = 'success';
|
|
|
+ if (!summary.attempts) {
|
|
|
+ summary.attempts = 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let successfulRuns = roundSummaries.filter((item) => item.status === 'success').length;
|
|
|
+ const initialState = await getState();
|
|
|
+ const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
|
|
|
+ ? 'waiting_step'
|
|
|
+ : 'running';
|
|
|
+ const showResumePosition = continueCurrentOnFirstAttempt || resumeCurrentRun > 1 || resumeAttemptRun > 1;
|
|
|
+
|
|
|
+ await setState({
|
|
|
+ autoRunSkipFailures,
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ ...getAutoRunStatusPayload(initialPhase, {
|
|
|
+ currentRun: showResumePosition ? resumeCurrentRun : 0,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun: showResumePosition ? resumeAttemptRun : 0,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+
|
|
|
+ for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) {
|
|
|
+ const roundSummary = roundSummaries[targetRun - 1];
|
|
|
+ let roundRecordAppended = false;
|
|
|
+ const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun;
|
|
|
+ let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1;
|
|
|
+ let reuseExistingProgress = resumingCurrentRound;
|
|
|
+ const maxAttemptsForRound = autoRunSkipFailures
|
|
|
+ ? AUTO_RUN_MAX_RETRIES_PER_ROUND + 1
|
|
|
+ : Math.max(1, attemptRun);
|
|
|
+
|
|
|
+ while (attemptRun <= maxAttemptsForRound) {
|
|
|
+ runtime.set({
|
|
|
+ autoRunCurrentRun: targetRun,
|
|
|
+ autoRunAttemptRun: attemptRun,
|
|
|
+ });
|
|
|
+ roundSummary.attempts = attemptRun;
|
|
|
+ let startStep = 1;
|
|
|
+ let useExistingProgress = false;
|
|
|
+
|
|
|
+ if (reuseExistingProgress) {
|
|
|
+ let currentState = await getState();
|
|
|
+ if (getRunningSteps(currentState.stepStatuses).length) {
|
|
|
+ currentState = await waitForRunningStepsToFinish({
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
|
|
|
+ if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
|
|
|
+ startStep = resumeStep;
|
|
|
+ useExistingProgress = true;
|
|
|
+ } else if (hasSavedProgress(currentState.stepStatuses)) {
|
|
|
+ await addLog('检测到当前流程已处理完成,本轮将改为从步骤 1 重新开始。', 'info');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!useExistingProgress) {
|
|
|
+ const prevState = await getState();
|
|
|
+ const keepSettings = {
|
|
|
+ vpsUrl: prevState.vpsUrl,
|
|
|
+ vpsPassword: prevState.vpsPassword,
|
|
|
+ customPassword: prevState.customPassword,
|
|
|
+ autoRunSkipFailures: prevState.autoRunSkipFailures,
|
|
|
+ autoRunFallbackThreadIntervalMinutes: prevState.autoRunFallbackThreadIntervalMinutes,
|
|
|
+ autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
|
|
+ autoRunDelayMinutes: prevState.autoRunDelayMinutes,
|
|
|
+ autoStepDelaySeconds: prevState.autoStepDelaySeconds,
|
|
|
+ mailProvider: prevState.mailProvider,
|
|
|
+ emailGenerator: prevState.emailGenerator,
|
|
|
+ emailPrefix: prevState.emailPrefix,
|
|
|
+ inbucketHost: prevState.inbucketHost,
|
|
|
+ inbucketMailbox: prevState.inbucketMailbox,
|
|
|
+ cloudflareDomain: prevState.cloudflareDomain,
|
|
|
+ cloudflareDomains: prevState.cloudflareDomains,
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ tabRegistry: {},
|
|
|
+ sourceLastUrls: {},
|
|
|
+ ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
|
|
|
+ };
|
|
|
+ await resetState();
|
|
|
+ await setState(keepSettings);
|
|
|
+ deps.chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => { });
|
|
|
+ await sleepWithStop(500);
|
|
|
+ } else {
|
|
|
+ await setState({
|
|
|
+ autoRunSkipFailures,
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (forceFreshTabsNextRun) {
|
|
|
+ await addLog(`上一轮尝试已放弃,当前开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试。`, 'warn');
|
|
|
+ forceFreshTabsNextRun = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const appendRoundRecordIfNeeded = async (status, reason = '') => {
|
|
|
+ if (roundRecordAppended) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof appendAccountRunRecord !== 'function') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const record = await appendAccountRunRecord(status, null, reason);
|
|
|
+ if (record) {
|
|
|
+ roundRecordAppended = true;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ deps.throwIfStopped();
|
|
|
+ await broadcastAutoRunStatus('running', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+
|
|
|
+ await runAutoSequenceFromStep(startStep, {
|
|
|
+ targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRuns: attemptRun,
|
|
|
+ continued: useExistingProgress,
|
|
|
+ });
|
|
|
+
|
|
|
+ roundSummary.status = 'success';
|
|
|
+ roundSummary.finalFailureReason = '';
|
|
|
+ successfulRuns += 1;
|
|
|
+ await setState({
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ });
|
|
|
+ await addLog(`=== 第 ${targetRun}/${totalRuns} 轮完成(第 ${attemptRun} 次尝试成功)===`, 'ok');
|
|
|
+ break;
|
|
|
+ } catch (err) {
|
|
|
+ if (isStopError(err)) {
|
|
|
+ stoppedEarly = true;
|
|
|
+ await appendRoundRecordIfNeeded('stopped', getErrorMessage(err));
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ const reason = getErrorMessage(err);
|
|
|
+ roundSummary.failureReasons.push(reason);
|
|
|
+ const canRetry = autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
|
|
+
|
|
|
+ await setState({
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ });
|
|
|
+
|
|
|
+ if (canRetry) {
|
|
|
+ const retryIndex = attemptRun;
|
|
|
+ if (isRestartCurrentAttemptError(err)) {
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试需要整轮重开:${reason}`, 'warn');
|
|
|
+ } else {
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮第 ${attemptRun} 次尝试失败:${reason}`, 'error');
|
|
|
+ }
|
|
|
+ cancelPendingCommands('当前尝试已放弃。');
|
|
|
+ await broadcastStopToContentScripts();
|
|
|
+ await broadcastAutoRunStatus('retrying', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+ forceFreshTabsNextRun = true;
|
|
|
+ await addLog(
|
|
|
+ `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`,
|
|
|
+ 'warn'
|
|
|
+ );
|
|
|
+ try {
|
|
|
+ await sleepWithStop(AUTO_RUN_RETRY_DELAY_MS);
|
|
|
+ } catch (sleepError) {
|
|
|
+ if (isStopError(sleepError)) {
|
|
|
+ stoppedEarly = true;
|
|
|
+ await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError));
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ throw sleepError;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const parkedForRetry = await waitBeforeAutoRunRetry(targetRun, totalRuns, attemptRun + 1, {
|
|
|
+ autoRunSkipFailures,
|
|
|
+ roundSummaries,
|
|
|
+ });
|
|
|
+ if (parkedForRetry) {
|
|
|
+ parkedByTimer = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (sleepError) {
|
|
|
+ if (isStopError(sleepError)) {
|
|
|
+ stoppedEarly = true;
|
|
|
+ await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError));
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ throw sleepError;
|
|
|
+ }
|
|
|
+ attemptRun += 1;
|
|
|
+ reuseExistingProgress = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ roundSummary.status = 'failed';
|
|
|
+ roundSummary.finalFailureReason = reason;
|
|
|
+ await setState({
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ });
|
|
|
+ await appendRoundRecordIfNeeded('failed', reason);
|
|
|
+ if (!autoRunSkipFailures) {
|
|
|
+ cancelPendingCommands('当前轮执行失败。');
|
|
|
+ await broadcastStopToContentScripts();
|
|
|
+ await addLog('自动重试未开启,自动运行将在当前失败后停止。', 'warn');
|
|
|
+ stoppedEarly = true;
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮最终失败:${reason}`, 'error');
|
|
|
+ await addLog(
|
|
|
+ targetRun < totalRuns
|
|
|
+ ? `第 ${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,继续下一轮。`
|
|
|
+ : `第 ${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,本次自动运行结束。`,
|
|
|
+ 'warn'
|
|
|
+ );
|
|
|
+ cancelPendingCommands('当前轮已达到重试上限。');
|
|
|
+ await broadcastStopToContentScripts();
|
|
|
+ forceFreshTabsNextRun = true;
|
|
|
+ break;
|
|
|
+ } finally {
|
|
|
+ reuseExistingProgress = false;
|
|
|
+ continueCurrentOnFirstAttempt = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (stoppedEarly || parkedByTimer) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const parkedForNextRound = await waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary, {
|
|
|
+ autoRunSkipFailures,
|
|
|
+ roundSummaries,
|
|
|
+ });
|
|
|
+ if (parkedForNextRound) {
|
|
|
+ parkedByTimer = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (sleepError) {
|
|
|
+ if (isStopError(sleepError)) {
|
|
|
+ stoppedEarly = true;
|
|
|
+ await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: targetRun,
|
|
|
+ totalRuns,
|
|
|
+ attemptRun: runtime.get().autoRunAttemptRun,
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ throw sleepError;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (parkedByTimer) {
|
|
|
+ runtime.set({ autoRunActive: false });
|
|
|
+ clearStopRequest();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await setState({
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ });
|
|
|
+ await logAutoRunFinalSummary(totalRuns, roundSummaries);
|
|
|
+
|
|
|
+ const finalRuntime = runtime.get();
|
|
|
+ if (deps.getStopRequested() || stoppedEarly) {
|
|
|
+ await addLog(`=== 已停止,完成 ${successfulRuns}/${finalRuntime.autoRunTotalRuns} 轮 ===`, 'warn');
|
|
|
+ await broadcastAutoRunStatus('stopped', {
|
|
|
+ currentRun: finalRuntime.autoRunCurrentRun,
|
|
|
+ totalRuns: finalRuntime.autoRunTotalRuns,
|
|
|
+ attemptRun: finalRuntime.autoRunAttemptRun,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ await addLog(`=== 全部 ${finalRuntime.autoRunTotalRuns} 轮已执行完成,成功 ${successfulRuns} 轮 ===`, 'ok');
|
|
|
+ await broadcastAutoRunStatus('complete', {
|
|
|
+ currentRun: finalRuntime.autoRunTotalRuns,
|
|
|
+ totalRuns: finalRuntime.autoRunTotalRuns,
|
|
|
+ attemptRun: finalRuntime.autoRunAttemptRun,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ runtime.set({ autoRunActive: false });
|
|
|
+ const afterRuntime = runtime.get();
|
|
|
+ await setState({
|
|
|
+ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
|
|
+ autoRunTimerPlan: null,
|
|
|
+ scheduledAutoRunPlan: null,
|
|
|
+ ...getAutoRunStatusPayload(deps.getStopRequested() || stoppedEarly ? 'stopped' : 'complete', {
|
|
|
+ currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns,
|
|
|
+ totalRuns: afterRuntime.autoRunTotalRuns,
|
|
|
+ attemptRun: afterRuntime.autoRunAttemptRun,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ clearStopRequest();
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ autoRunLoop,
|
|
|
+ buildAutoRunRoundSummaries,
|
|
|
+ createAutoRunRoundSummary,
|
|
|
+ formatAutoRunFailureReasons,
|
|
|
+ getAutoRunRoundRetryCount,
|
|
|
+ handleAutoRunLoopUnhandledError,
|
|
|
+ logAutoRunFinalSummary,
|
|
|
+ normalizeAutoRunRoundSummary,
|
|
|
+ serializeAutoRunRoundSummaries,
|
|
|
+ skipAutoRunCountdown,
|
|
|
+ startAutoRunLoop,
|
|
|
+ waitBetweenAutoRunRounds,
|
|
|
+ waitBeforeAutoRunRetry,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ createAutoRunController,
|
|
|
+ };
|
|
|
+});
|