Bläddra i källkod

feat: 修复线程间隔自动运行倒计时功能,增强状态管理

QLHazyCoder 4 månader sedan
förälder
incheckning
ee44367167
2 ändrade filer med 163 tillägg och 22 borttagningar
  1. 60 4
      background.js
  2. 103 18
      sidepanel/sidepanel.js

+ 60 - 4
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) {

+ 103 - 18
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();
-    return;
-  }
+  if (getActiveAutoRunCountdown()) {
+    if (scheduledCountdownTimer) {
+      return;
+    }
 
-  if (scheduledCountdownTimer) {
+    scheduledCountdownTimer = setInterval(() => {
+      renderScheduledAutoRunInfo();
+      updateStatusDisplay(latestState);
+    }, 1000);
     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);