diff --git a/background.js b/background.js index 58ecaea..a0115fb 100644 --- a/background.js +++ b/background.js @@ -39,6 +39,8 @@ const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback'; const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run'; const AUTO_RUN_DELAY_MIN_MINUTES = 1; const AUTO_RUN_DELAY_MAX_MINUTES = 1440; +const AUTO_RUN_RETRY_DELAY_MS = 3000; +const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3; const AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS = 0; const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600; const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds']; @@ -60,7 +62,7 @@ const PERSISTED_SETTING_DEFAULTS = { sub2apiPassword: '', sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, customPassword: '', - autoRunSkipFailures: false, + autoRunSkipFailures: true, autoRunFallbackThreadIntervalMinutes: 0, autoRunDelayEnabled: false, autoRunDelayMinutes: 30, @@ -107,6 +109,7 @@ const DEFAULT_STATE = { autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。 autoRunTotalRuns: 1, // 自动运行计划总轮数。 autoRunAttemptRun: 0, // 当前轮次的重试序号。 + autoRunRoundSummaries: [], // 自动运行轮次摘要。 scheduledAutoRunAt: null, // 自动运行计划启动时间戳。 scheduledAutoRunPlan: null, // 自动运行计划参数快照。 signupVerificationRequestedAt: null, @@ -195,7 +198,7 @@ function normalizeScheduledAutoRunPlan(plan) { return { totalRuns: normalizeRunCount(plan.totalRuns), - autoRunSkipFailures: Boolean(plan.autoRunSkipFailures), + autoRunSkipFailures: true, mode: plan.mode === 'continue' ? 'continue' : 'restart', }; } @@ -2388,8 +2391,8 @@ async function launchScheduledAutoRun(trigger = 'alarm') { : '倒计时结束,自动运行开始执行。', 'info' ); - autoRunLoop(plan.totalRuns, { - autoRunSkipFailures: plan.autoRunSkipFailures, + startAutoRunLoop(plan.totalRuns, { + autoRunSkipFailures: true, mode: plan.mode, }); return true; @@ -2701,10 +2704,10 @@ async function handleMessage(message, sender) { throw new Error('已有自动运行倒计时计划,请先取消或立即开始。'); } const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1); - const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures); + const autoRunSkipFailures = true; const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart'; await setState({ autoRunSkipFailures }); - autoRunLoop(totalRuns, { autoRunSkipFailures, mode }); // fire-and-forget + startAutoRunLoop(totalRuns, { autoRunSkipFailures, mode }); return { ok: true }; } @@ -2713,7 +2716,7 @@ async function handleMessage(message, sender) { const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1); return await scheduleAutoRun(totalRuns, { delayMinutes: message.payload?.delayMinutes, - autoRunSkipFailures: Boolean(message.payload?.autoRunSkipFailures), + autoRunSkipFailures: true, mode: message.payload?.mode, }); } @@ -2740,7 +2743,11 @@ async function handleMessage(message, sender) { if (message.payload.email) { await setEmailState(message.payload.email); } - resumeAutoRun(); // fire-and-forget + resumeAutoRun().catch((error) => { + handleAutoRunLoopUnhandledError(error).catch((handlerError) => { + console.error(LOG_PREFIX, 'Failed to finalize resume error:', handlerError); + }); + }); return { ok: true }; } @@ -3429,7 +3436,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) { } // Outer loop: keep retrying until the target number of successful runs is reached. -async function autoRunLoop(totalRuns, options = {}) { +async function legacyAutoRunLoop(totalRuns, options = {}) { if (autoRunActive) { await addLog('自动运行已在进行中', 'warn'); return; @@ -3661,7 +3668,7 @@ async function waitForResume() { }); } -async function resumeAutoRun() { +async function legacyResumeAutoRun() { throwIfStopped(); const state = await getState(); if (!state.email) { @@ -3698,6 +3705,427 @@ async function resumeAutoRun() { return true; } +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) => { + return 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 handleAutoRunLoopUnhandledError(error) { + console.error(LOG_PREFIX, 'Auto run loop crashed:', error); + if (!isStopError(error)) { + await addLog(`自动运行异常终止:${getErrorMessage(error) || '未知错误'}`, 'error'); + } + + autoRunActive = false; + await broadcastAutoRunStatus('stopped', { + currentRun: autoRunCurrentRun, + totalRuns: autoRunTotalRuns, + attemptRun: autoRunAttemptRun, + }); + clearStopRequest(); +} + +function startAutoRunLoop(totalRuns, options = {}) { + autoRunLoop(totalRuns, options).catch((error) => { + handleAutoRunLoopUnhandledError(error).catch((handlerError) => { + console.error(LOG_PREFIX, 'Failed to finalize auto run error:', handlerError); + }); + }); +} + +async function autoRunLoop(totalRuns, options = {}) { + if (autoRunActive) { + await addLog('自动运行已在进行中', 'warn'); + return; + } + + clearStopRequest(); + autoRunActive = true; + autoRunTotalRuns = totalRuns; + autoRunCurrentRun = 0; + autoRunAttemptRun = 0; + const autoRunSkipFailures = true; + 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; + 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'; + + await setState({ + autoRunSkipFailures, + autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), + ...getAutoRunStatusPayload(initialPhase, { + currentRun: continueCurrentOnFirstAttempt ? resumeCurrentRun : 0, + totalRuns, + attemptRun: continueCurrentOnFirstAttempt ? resumeAttemptRun : 0, + }), + }); + + for (let targetRun = resumeCurrentRun; targetRun <= totalRuns; targetRun += 1) { + const roundSummary = roundSummaries[targetRun - 1]; + let attemptRun = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun ? resumeAttemptRun : 1; + let reuseExistingProgress = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun; + + while (attemptRun <= AUTO_RUN_MAX_RETRIES_PER_ROUND + 1) { + 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); + 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; + } + + try { + 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 addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); + await broadcastAutoRunStatus('stopped', { + currentRun: targetRun, + totalRuns, + attemptRun, + }); + break; + } + + const reason = getErrorMessage(err); + roundSummary.failureReasons.push(reason); + const canRetry = attemptRun <= AUTO_RUN_MAX_RETRIES_PER_ROUND; + + 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 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 addLog(`第 ${targetRun}/${totalRuns} 轮最终失败:${reason}`, 'error'); + await addLog(`第 ${targetRun}/${totalRuns} 轮已达到 ${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试上限,继续下一轮。`, 'warn'); + cancelPendingCommands('当前轮已达到重试上限。'); + await broadcastStopToContentScripts(); + forceFreshTabsNextRun = true; + break; + } finally { + reuseExistingProgress = false; + continueCurrentOnFirstAttempt = false; + } + } + + if (stoppedEarly) { + break; + } + } + + await setState({ + autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), + }); + await logAutoRunFinalSummary(totalRuns, roundSummaries); + + if (stopRequested || stoppedEarly) { + await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮 ===`, 'warn'); + await broadcastAutoRunStatus('stopped', { + currentRun: autoRunCurrentRun, + totalRuns: autoRunTotalRuns, + attemptRun: autoRunAttemptRun, + }); + } else { + await addLog(`=== 全部 ${autoRunTotalRuns} 轮已执行完成,成功 ${successfulRuns} 轮 ===`, 'ok'); + await broadcastAutoRunStatus('complete', { + currentRun: autoRunTotalRuns, + totalRuns: autoRunTotalRuns, + attemptRun: autoRunAttemptRun, + }); + } + autoRunActive = false; + await setState({ + autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), + ...getAutoRunStatusPayload(stopRequested || stoppedEarly ? 'stopped' : 'complete', { + currentRun: stopRequested || stoppedEarly ? autoRunCurrentRun : autoRunTotalRuns, + totalRuns: autoRunTotalRuns, + attemptRun: autoRunAttemptRun, + }), + }); + clearStopRequest(); +} + +async function resumeAutoRun() { + throwIfStopped(); + const state = await getState(); + if (!state.email) { + await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error'); + return false; + } + + const resumedInMemory = await resumeAutoRunIfWaitingForEmail({ silent: true }); + if (resumedInMemory) { + return true; + } + + if (!isAutoRunPausedState(state)) { + return false; + } + + if (autoRunActive) { + return false; + } + + const totalRuns = state.autoRunTotalRuns || 1; + const currentRun = state.autoRunCurrentRun || 1; + const attemptRun = state.autoRunAttemptRun || 1; + + await addLog('检测到自动流程暂停上下文已丢失,正在从当前进度恢复自动运行...', 'warn'); + startAutoRunLoop(totalRuns, { + autoRunSkipFailures: true, + mode: 'continue', + resumeCurrentRun: currentRun, + resumeAttemptRun: attemptRun, + resumeRoundSummaries: state.autoRunRoundSummaries, + }); + return true; +} + // ============================================================ // Step 1: Get OAuth Link // ============================================================ diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 934a126..debdce5 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -198,15 +198,16 @@
- 兜底 + 自动重试
-