diff --git a/background.js b/background.js index 9d623c6..b293560 100644 --- a/background.js +++ b/background.js @@ -118,6 +118,9 @@ const DEFAULT_STATE = { autoRunRoundSummaries: [], // 自动运行轮次摘要。 scheduledAutoRunAt: null, // 自动运行计划启动时间戳。 scheduledAutoRunPlan: null, // 自动运行计划参数快照。 + autoRunCountdownAt: null, + autoRunCountdownTitle: '', + autoRunCountdownNote: '', signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, currentHotmailAccountId: null, @@ -2337,7 +2340,20 @@ function getAutoRunStatusPayload(phase, payload = {}) { ? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null) : null; const scheduledAt = rawScheduledAt === null ? null : Number(rawScheduledAt); - const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_step' || phase === 'waiting_email' || phase === 'retrying'; + const rawCountdownAt = payload.countdownAt ?? payload.autoRunCountdownAt ?? null; + const countdownAt = rawCountdownAt === null ? null : Number(rawCountdownAt); + const countdownTitle = payload.countdownTitle === undefined + ? '' + : String(payload.countdownTitle || ''); + const countdownNote = payload.countdownNote === undefined + ? '' + : String(payload.countdownNote || ''); + const autoRunning = phase === 'scheduled' + || phase === 'running' + || phase === 'waiting_step' + || phase === 'waiting_email' + || phase === 'retrying' + || phase === 'waiting_interval'; return { autoRunning, @@ -2346,6 +2362,9 @@ function getAutoRunStatusPayload(phase, payload = {}) { autoRunTotalRuns: totalRuns, autoRunAttemptRun: attemptRun, scheduledAutoRunAt: Number.isFinite(scheduledAt) ? scheduledAt : null, + autoRunCountdownAt: Number.isFinite(countdownAt) ? countdownAt : null, + autoRunCountdownTitle: countdownTitle, + autoRunCountdownNote: countdownNote, }; } @@ -2353,12 +2372,16 @@ async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) { const rawScheduledAt = phase === 'scheduled' ? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null) : null; + const rawCountdownAt = payload.countdownAt ?? payload.autoRunCountdownAt ?? null; const statusPayload = { phase, currentRun: payload.currentRun ?? autoRunCurrentRun, totalRuns: payload.totalRuns ?? autoRunTotalRuns, attemptRun: payload.attemptRun ?? autoRunAttemptRun, scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt), + countdownAt: rawCountdownAt === null ? null : Number(rawCountdownAt), + countdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''), + countdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''), }; await setState({ @@ -2372,7 +2395,13 @@ async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) { } function isAutoRunLockedState(state) { - return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'waiting_step' || state.autoRunPhase === 'retrying'); + return Boolean(state.autoRunning) + && ( + state.autoRunPhase === 'running' + || state.autoRunPhase === 'waiting_step' + || state.autoRunPhase === 'retrying' + || state.autoRunPhase === 'waiting_interval' + ); } function isAutoRunPausedState(state) { @@ -2452,6 +2481,9 @@ async function scheduleAutoRun(totalRuns, options = {}) { totalRuns: plan.totalRuns, attemptRun: 0, scheduledAt, + countdownAt: scheduledAt, + countdownTitle: '已计划自动运行', + countdownNote: `计划于 ${formatAutoRunScheduleTime(scheduledAt)} 开始`, }, { autoRunSkipFailures: plan.autoRunSkipFailures, @@ -4025,6 +4057,18 @@ async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) { } } +async function sleepWithAutoRunCountdown(waitMs, payload = {}) { + if (!Number.isFinite(waitMs) || waitMs <= 0) { + return; + } + + await broadcastAutoRunStatus('waiting_interval', { + ...payload, + countdownAt: Date.now() + waitMs, + }); + await sleepWithStop(waitMs); +} + async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary) { if (totalRuns <= 1 || targetRun >= totalRuns) { return; @@ -4042,7 +4086,13 @@ async function waitBetweenAutoRunRounds(targetRun, totalRuns, roundSummary) { `线程间隔:第 ${targetRun}/${totalRuns} 轮已${statusLabel},等待 ${fallbackThreadIntervalMinutes} 分钟后开始下一轮。`, 'info' ); - await sleepWithStop(fallbackThreadIntervalMinutes * 60 * 1000); + await sleepWithAutoRunCountdown(fallbackThreadIntervalMinutes * 60 * 1000, { + currentRun: targetRun, + totalRuns, + attemptRun: autoRunAttemptRun, + countdownTitle: '线程间隔中', + countdownNote: `第 ${Math.min(targetRun + 1, totalRuns)}/${totalRuns} 轮即将开始`, + }); } async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun) { @@ -4057,7 +4107,13 @@ async function waitBeforeAutoRunRetry(targetRun, totalRuns, nextAttemptRun) { `线程间隔:等待 ${fallbackThreadIntervalMinutes} 分钟后开始第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试。`, 'info' ); - await sleepWithStop(fallbackThreadIntervalMinutes * 60 * 1000); + await sleepWithAutoRunCountdown(fallbackThreadIntervalMinutes * 60 * 1000, { + currentRun: targetRun, + totalRuns, + attemptRun: nextAttemptRun, + countdownTitle: '线程间隔中', + countdownNote: `第 ${targetRun}/${totalRuns} 轮第 ${nextAttemptRun} 次尝试即将开始`, + }); } async function handleAutoRunLoopUnhandledError(error) { diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 0ed7e32..6cc72da 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -153,6 +153,9 @@ let currentAutoRun = { totalRuns: 1, attemptRun: 0, scheduledAt: null, + countdownAt: null, + countdownTitle: '', + countdownNote: '', }; let settingsDirty = false; let settingsSaveInFlight = false; @@ -574,26 +577,45 @@ function syncLatestState(nextState) { }; } +function hasOwnStateValue(source, key) { + return Object.prototype.hasOwnProperty.call(source, key); +} + +function readAutoRunStateValue(source, keys, fallback) { + for (const key of keys) { + if (hasOwnStateValue(source, key)) { + return source[key]; + } + } + return fallback; +} + function syncAutoRunState(source = {}) { const phase = source.autoRunPhase ?? source.phase ?? currentAutoRun.phase; const autoRunning = source.autoRunning !== undefined ? Boolean(source.autoRunning) : (source.autoRunPhase !== undefined || source.phase !== undefined - ? ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase) + ? ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase) : currentAutoRun.autoRunning); currentAutoRun = { autoRunning, phase, - currentRun: source.autoRunCurrentRun ?? source.currentRun ?? currentAutoRun.currentRun, - totalRuns: source.autoRunTotalRuns ?? source.totalRuns ?? currentAutoRun.totalRuns, - attemptRun: source.autoRunAttemptRun ?? source.attemptRun ?? currentAutoRun.attemptRun, - scheduledAt: source.scheduledAutoRunAt ?? source.scheduledAt ?? currentAutoRun.scheduledAt, + currentRun: readAutoRunStateValue(source, ['autoRunCurrentRun', 'currentRun'], currentAutoRun.currentRun), + totalRuns: readAutoRunStateValue(source, ['autoRunTotalRuns', 'totalRuns'], currentAutoRun.totalRuns), + attemptRun: readAutoRunStateValue(source, ['autoRunAttemptRun', 'attemptRun'], currentAutoRun.attemptRun), + scheduledAt: readAutoRunStateValue(source, ['scheduledAutoRunAt', 'scheduledAt'], currentAutoRun.scheduledAt), + countdownAt: readAutoRunStateValue(source, ['autoRunCountdownAt', 'countdownAt'], currentAutoRun.countdownAt), + countdownTitle: readAutoRunStateValue(source, ['autoRunCountdownTitle', 'countdownTitle'], currentAutoRun.countdownTitle), + countdownNote: readAutoRunStateValue(source, ['autoRunCountdownNote', 'countdownNote'], currentAutoRun.countdownNote), }; } function isAutoRunLockedPhase() { - return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'waiting_step' || currentAutoRun.phase === 'retrying'; + return currentAutoRun.phase === 'running' + || currentAutoRun.phase === 'waiting_step' + || currentAutoRun.phase === 'retrying' + || currentAutoRun.phase === 'waiting_interval'; } function isAutoRunPausedPhase() { @@ -705,18 +727,46 @@ function stopScheduledCountdownTicker() { scheduledCountdownTimer = null; } +function getActiveAutoRunCountdown() { + if (isAutoRunScheduledPhase() && Number.isFinite(currentAutoRun.scheduledAt)) { + return { + at: currentAutoRun.scheduledAt, + title: '已计划自动运行', + note: `计划于 ${formatScheduleTime(currentAutoRun.scheduledAt)} 开始`, + tone: 'scheduled', + }; + } + + if (!Number.isFinite(currentAutoRun.countdownAt)) { + return null; + } + + return { + at: currentAutoRun.countdownAt, + title: currentAutoRun.countdownTitle || '等待中', + note: currentAutoRun.countdownNote || '', + tone: 'running', + }; +} + function renderScheduledAutoRunInfo() { if (!autoScheduleBar) { return; } - if (!isAutoRunScheduledPhase() || !Number.isFinite(currentAutoRun.scheduledAt)) { + const countdown = getActiveAutoRunCountdown(); + if (!countdown) { autoScheduleBar.style.display = 'none'; return; } - const remainingMs = currentAutoRun.scheduledAt - Date.now(); + const remainingMs = countdown.at - Date.now(); autoScheduleBar.style.display = 'flex'; + autoScheduleTitle.textContent = countdown.title; + autoScheduleMeta.textContent = remainingMs > 0 + ? `${countdown.note ? `${countdown.note},` : ''}剩余 ${formatCountdown(remainingMs)}` + : '倒计时即将结束,正在准备继续...'; + return; autoScheduleTitle.textContent = '已计划自动运行'; autoScheduleMeta.textContent = remainingMs > 0 ? `计划于 ${formatScheduleTime(currentAutoRun.scheduledAt)} 开始,剩余 ${formatCountdown(remainingMs)}` @@ -725,19 +775,20 @@ function renderScheduledAutoRunInfo() { function syncScheduledCountdownTicker() { renderScheduledAutoRunInfo(); - if (!isAutoRunScheduledPhase() || !Number.isFinite(currentAutoRun.scheduledAt)) { - stopScheduledCountdownTicker(); + if (getActiveAutoRunCountdown()) { + if (scheduledCountdownTimer) { + return; + } + + scheduledCountdownTimer = setInterval(() => { + renderScheduledAutoRunInfo(); + updateStatusDisplay(latestState); + }, 1000); return; } - if (scheduledCountdownTimer) { - return; - } - - scheduledCountdownTimer = setInterval(() => { - renderScheduledAutoRunInfo(); - updateStatusDisplay(latestState); - }, 1000); + stopScheduledCountdownTicker(); + return; } function setDefaultAutoRunButton() { @@ -1045,6 +1096,10 @@ function applyAutoRunStatus(payload = currentAutoRun) { autoContinueBar.style.display = 'none'; btnAutoRun.innerHTML = `重试中${runLabel}`; break; + case 'waiting_interval': + autoContinueBar.style.display = 'none'; + btnAutoRun.innerHTML = `等待中${runLabel}`; + break; default: autoContinueBar.style.display = 'none'; setDefaultAutoRunButton(); @@ -1881,6 +1936,16 @@ function updateStatusDisplay(state) { statusBar.className = 'status-bar'; + const countdown = getActiveAutoRunCountdown(); + if (countdown) { + const remainingMs = countdown.at - Date.now(); + displayStatus.textContent = remainingMs > 0 + ? `${countdown.title},剩余 ${formatCountdown(remainingMs)}` + : `${countdown.title},即将结束...`; + statusBar.classList.add(countdown.tone === 'scheduled' ? 'scheduled' : 'running'); + return; + } + if (isAutoRunScheduledPhase()) { const remainingMs = Number.isFinite(currentAutoRun.scheduledAt) ? currentAutoRun.scheduledAt - Date.now() @@ -2775,6 +2840,9 @@ btnReset.addEventListener('click', async () => { autoRunTotalRuns: 1, autoRunAttemptRun: 0, scheduledAutoRunAt: null, + autoRunCountdownAt: null, + autoRunCountdownTitle: '', + autoRunCountdownNote: '', }); displayOauthUrl.textContent = '等待中...'; displayOauthUrl.classList.remove('has-value'); @@ -3089,6 +3157,9 @@ chrome.runtime.onMessage.addListener((message) => { stepStatuses: STEP_DEFAULT_STATUSES, logs: [], scheduledAutoRunAt: null, + autoRunCountdownAt: null, + autoRunCountdownTitle: '', + autoRunCountdownNote: '', }); displayOauthUrl.textContent = '等待中...'; displayOauthUrl.classList.remove('has-value'); @@ -3100,6 +3171,17 @@ chrome.runtime.onMessage.addListener((message) => { logArea.innerHTML = ''; document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); + syncAutoRunState({ + autoRunning: false, + autoRunPhase: 'idle', + autoRunCurrentRun: 0, + autoRunTotalRuns: 1, + autoRunAttemptRun: 0, + scheduledAutoRunAt: null, + autoRunCountdownAt: null, + autoRunCountdownTitle: '', + autoRunCountdownNote: '', + }); applyAutoRunStatus(currentAutoRun); updateProgressCounter(); updateButtonStates(); @@ -3157,12 +3239,15 @@ chrome.runtime.onMessage.addListener((message) => { case 'AUTO_RUN_STATUS': { syncLatestState({ - autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying'].includes(message.payload.phase), + autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(message.payload.phase), autoRunPhase: message.payload.phase, autoRunCurrentRun: message.payload.currentRun, autoRunTotalRuns: message.payload.totalRuns, autoRunAttemptRun: message.payload.attemptRun, scheduledAutoRunAt: message.payload.scheduledAt ?? null, + autoRunCountdownAt: message.payload.countdownAt ?? null, + autoRunCountdownTitle: message.payload.countdownTitle ?? '', + autoRunCountdownNote: message.payload.countdownNote ?? '', }); applyAutoRunStatus(message.payload); updateStatusDisplay(latestState);