Explorar el Código

feat: 统一验证码重发次数配置,优化相关逻辑和界面

QLHazyCoder hace 4 meses
padre
commit
3b28845d86

+ 18 - 9
background.js

@@ -153,9 +153,9 @@ const AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS = 0;
 const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
 const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
 const VERIFICATION_RESEND_COUNT_MIN = 0;
 const VERIFICATION_RESEND_COUNT_MIN = 0;
 const VERIFICATION_RESEND_COUNT_MAX = 20;
 const VERIFICATION_RESEND_COUNT_MAX = 20;
-const DEFAULT_SIGNUP_VERIFICATION_RESEND_COUNT = 5;
-const DEFAULT_LOGIN_VERIFICATION_RESEND_COUNT = 4;
+const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
 const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
 const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
+const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = ['signupVerificationResendCount', 'loginVerificationResendCount'];
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_CPA_CALLBACK_MODE = 'step9';
 const DEFAULT_CPA_CALLBACK_MODE = 'step9';
 const MAIL_2925_MODE_PROVIDE = 'provide';
 const MAIL_2925_MODE_PROVIDE = 'provide';
@@ -222,8 +222,7 @@ const PERSISTED_SETTING_DEFAULTS = {
   autoRunDelayEnabled: false,
   autoRunDelayEnabled: false,
   autoRunDelayMinutes: 30,
   autoRunDelayMinutes: 30,
   autoStepDelaySeconds: null,
   autoStepDelaySeconds: null,
-  signupVerificationResendCount: DEFAULT_SIGNUP_VERIFICATION_RESEND_COUNT,
-  loginVerificationResendCount: DEFAULT_LOGIN_VERIFICATION_RESEND_COUNT,
+  verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
   mailProvider: '163',
   mailProvider: '163',
   mail2925Mode: DEFAULT_MAIL_2925_MODE,
   mail2925Mode: DEFAULT_MAIL_2925_MODE,
   emailGenerator: 'duck',
   emailGenerator: 'duck',
@@ -804,10 +803,8 @@ function normalizePersistentSettingValue(key, value) {
       return normalizeAutoRunDelayMinutes(value);
       return normalizeAutoRunDelayMinutes(value);
     case 'autoStepDelaySeconds':
     case 'autoStepDelaySeconds':
       return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
       return normalizeAutoStepDelaySeconds(value, PERSISTED_SETTING_DEFAULTS.autoStepDelaySeconds);
-    case 'signupVerificationResendCount':
-      return normalizeVerificationResendCount(value, DEFAULT_SIGNUP_VERIFICATION_RESEND_COUNT);
-    case 'loginVerificationResendCount':
-      return normalizeVerificationResendCount(value, DEFAULT_LOGIN_VERIFICATION_RESEND_COUNT);
+    case 'verificationResendCount':
+      return normalizeVerificationResendCount(value, DEFAULT_VERIFICATION_RESEND_COUNT);
     case 'mailProvider':
     case 'mailProvider':
       return normalizeMailProvider(value);
       return normalizeMailProvider(value);
     case 'mail2925Mode':
     case 'mail2925Mode':
@@ -870,6 +867,14 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
       normalizedInput.autoStepDelaySeconds = legacyAutoStepDelaySeconds;
       normalizedInput.autoStepDelaySeconds = legacyAutoStepDelaySeconds;
     }
     }
   }
   }
+  if (normalizedInput.verificationResendCount === undefined) {
+    const legacyVerificationResendCount = normalizedInput.signupVerificationResendCount !== undefined
+      ? normalizedInput.signupVerificationResendCount
+      : normalizedInput.loginVerificationResendCount;
+    if (legacyVerificationResendCount !== undefined) {
+      normalizedInput.verificationResendCount = legacyVerificationResendCount;
+    }
+  }
 
 
   const payload = {};
   const payload = {};
   let matchedKeyCount = 0;
   let matchedKeyCount = 0;
@@ -905,7 +910,11 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
 }
 }
 
 
 async function getPersistedSettings() {
 async function getPersistedSettings() {
-  const stored = await chrome.storage.local.get([...PERSISTED_SETTING_KEYS, ...LEGACY_AUTO_STEP_DELAY_KEYS]);
+  const stored = await chrome.storage.local.get([
+    ...PERSISTED_SETTING_KEYS,
+    ...LEGACY_AUTO_STEP_DELAY_KEYS,
+    ...LEGACY_VERIFICATION_RESEND_COUNT_KEYS,
+  ]);
   return buildPersistentSettingsPayload(stored, { fillDefaults: true });
   return buildPersistentSettingsPayload(stored, { fillDefaults: true });
 }
 }
 
 

+ 174 - 89
background/verification-flow.js

@@ -1,4 +1,4 @@
-(function attachBackgroundVerificationFlow(root, factory) {
+(function attachBackgroundVerificationFlow(root, factory) {
   root.MultiPageBackgroundVerificationFlow = factory();
   root.MultiPageBackgroundVerificationFlow = factory();
 })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
 })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
   function createVerificationFlowHelpers(deps = {}) {
   function createVerificationFlowHelpers(deps = {}) {
@@ -36,6 +36,52 @@
       return step === 4 ? '注册' : '登录';
       return step === 4 ? '注册' : '登录';
     }
     }
 
 
+    function getVerificationResendStateKey() {
+      return 'verificationResendCount';
+    }
+
+    function normalizeVerificationResendCount(value, fallback = 0) {
+      const numeric = Number(value);
+      if (!Number.isFinite(numeric)) {
+        return Math.max(0, Math.floor(Number(fallback) || 0));
+      }
+
+      return Math.min(20, Math.max(0, Math.floor(numeric)));
+    }
+
+    function getLegacyVerificationResendCountDefault(step, options = {}) {
+      const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
+      const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
+      if (step === 4 && requestFreshCodeFirst) {
+        return legacyMaxRounds;
+      }
+      return Math.max(0, legacyMaxRounds - 1);
+    }
+
+    function getConfiguredVerificationResendCount(step, state, options = {}) {
+      const stateKey = getVerificationResendStateKey(step);
+      const configuredValue = state?.[stateKey] !== undefined
+        ? state[stateKey]
+        : (state?.signupVerificationResendCount ?? state?.loginVerificationResendCount);
+      return normalizeVerificationResendCount(
+        configuredValue,
+        getLegacyVerificationResendCountDefault(step, options)
+      );
+    }
+
+    function resolveMaxResendRequests(pollOverrides = {}) {
+      if (pollOverrides.maxResendRequests !== undefined) {
+        return normalizeVerificationResendCount(pollOverrides.maxResendRequests, 0);
+      }
+
+      const legacyMaxRounds = Number(pollOverrides.maxRounds);
+      if (Number.isFinite(legacyMaxRounds)) {
+        return Math.max(0, Math.floor(legacyMaxRounds) - 1);
+      }
+
+      return Math.max(0, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1) - 1);
+    }
+
     async function confirmCustomVerificationStepBypass(step) {
     async function confirmCustomVerificationStepBypass(step) {
       const verificationLabel = getVerificationCodeLabel(step);
       const verificationLabel = getVerificationCodeLabel(step);
       await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
       await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
@@ -144,6 +190,7 @@
 
 
       const {
       const {
         maxRounds: _ignoredMaxRounds,
         maxRounds: _ignoredMaxRounds,
+        maxResendRequests: _ignoredMaxResendRequests,
         resendIntervalMs: _ignoredResendIntervalMs,
         resendIntervalMs: _ignoredResendIntervalMs,
         lastResendAt: _ignoredLastResendAt,
         lastResendAt: _ignoredLastResendAt,
         onResendRequestedAt: _ignoredOnResendRequestedAt,
         onResendRequestedAt: _ignoredOnResendRequestedAt,
@@ -154,14 +201,18 @@
         : null;
         : null;
       let lastError = null;
       let lastError = null;
       let filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
       let filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
-      const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
+      const maxResendRequests = resolveMaxResendRequests(pollOverrides);
+      const totalRounds = maxResendRequests + 1;
+      const maxRounds = totalRounds;
       const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
       const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
       let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
       let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
+      let usedResendRequests = 0;
 
 
-      for (let round = 1; round <= maxRounds; round++) {
+      for (let round = 1; round <= totalRounds; round++) {
         throwIfStopped();
         throwIfStopped();
         if (round > 1) {
         if (round > 1) {
           lastResendAt = await requestVerificationCodeResend(step);
           lastResendAt = await requestVerificationCodeResend(step);
+          usedResendRequests += 1;
           if (onResendRequestedAt) {
           if (onResendRequestedAt) {
             const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt);
             const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt);
             if (nextFilterAfterTimestamp !== undefined) {
             if (nextFilterAfterTimestamp !== undefined) {
@@ -215,6 +266,7 @@
             return {
             return {
               ...result,
               ...result,
               lastResendAt,
               lastResendAt,
+              remainingResendRequests: Math.max(0, maxResendRequests - usedResendRequests),
             };
             };
           } catch (err) {
           } catch (err) {
             if (isStopError(err)) {
             if (isStopError(err)) {
@@ -246,7 +298,12 @@
     }
     }
 
 
     async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
     async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
-      const { onResendRequestedAt, ...cleanPollOverrides } = pollOverrides;
+      const {
+        onResendRequestedAt,
+        maxRounds: _ignoredMaxRounds,
+        maxResendRequests: _ignoredMaxResendRequests,
+        ...cleanPollOverrides
+      } = pollOverrides;
 
 
       if (mail.provider === HOTMAIL_PROVIDER) {
       if (mail.provider === HOTMAIL_PROVIDER) {
         const hotmailPollConfig = getHotmailVerificationPollConfig(step);
         const hotmailPollConfig = getHotmailVerificationPollConfig(step);
@@ -259,13 +316,13 @@
       if (mail.provider === LUCKMAIL_PROVIDER) {
       if (mail.provider === LUCKMAIL_PROVIDER) {
         return pollLuckmailVerificationCode(step, state, {
         return pollLuckmailVerificationCode(step, state, {
           ...getVerificationPollPayload(step, state),
           ...getVerificationPollPayload(step, state),
-          ...pollOverrides,
+          ...cleanPollOverrides,
         });
         });
       }
       }
       if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
       if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
         return pollCloudflareTempEmailVerificationCode(step, state, {
         return pollCloudflareTempEmailVerificationCode(step, state, {
           ...getVerificationPollPayload(step, state),
           ...getVerificationPollPayload(step, state),
-          ...pollOverrides,
+          ...cleanPollOverrides,
         });
         });
       }
       }
 
 
@@ -284,12 +341,15 @@
 
 
       let lastError = null;
       let lastError = null;
       let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
       let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
-      const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
+      const maxResendRequests = resolveMaxResendRequests(pollOverrides);
+      const maxRounds = maxResendRequests + 1;
+      let usedResendRequests = 0;
 
 
       for (let round = 1; round <= maxRounds; round++) {
       for (let round = 1; round <= maxRounds; round++) {
         throwIfStopped();
         throwIfStopped();
         if (round > 1) {
         if (round > 1) {
           const requestedAt = await requestVerificationCodeResend(step);
           const requestedAt = await requestVerificationCodeResend(step);
+          usedResendRequests += 1;
           if (typeof onResendRequestedAt === 'function') {
           if (typeof onResendRequestedAt === 'function') {
             const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
             const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
             if (nextFilterAfterTimestamp !== undefined) {
             if (nextFilterAfterTimestamp !== undefined) {
@@ -331,7 +391,10 @@
             throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
             throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
           }
           }
 
 
-          return result;
+          return {
+            ...result,
+            remainingResendRequests: Math.max(0, maxResendRequests - usedResendRequests),
+          };
         } catch (err) {
         } catch (err) {
           if (isStopError(err)) {
           if (isStopError(err)) {
             throw err;
             throw err;
@@ -386,6 +449,12 @@
       const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
       const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
         ? Boolean(options.requestFreshCodeFirst)
         ? Boolean(options.requestFreshCodeFirst)
         : (hotmailPollConfig?.requestFreshCodeFirst ?? false);
         : (hotmailPollConfig?.requestFreshCodeFirst ?? false);
+      let remainingAutomaticResendCount = options.maxResendRequests !== undefined
+        ? normalizeVerificationResendCount(
+          options.maxResendRequests,
+          getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
+        )
+        : getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
       const maxSubmitAttempts = 3;
       const maxSubmitAttempts = 3;
       const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
       const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
       let lastResendAt = Number(options.lastResendAt) || 0;
       let lastResendAt = Number(options.lastResendAt) || 0;
@@ -410,104 +479,120 @@
       };
       };
 
 
       if (requestFreshCodeFirst) {
       if (requestFreshCodeFirst) {
-        try {
-          lastResendAt = await requestVerificationCodeResend(step);
-          await updateFilterAfterTimestampForVerificationStep(lastResendAt);
-          await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
-        } catch (err) {
-          if (isStopError(err)) {
-            throw err;
+        if (remainingAutomaticResendCount <= 0) {
+          await addLog(`步骤 ${step}:当前自动重新发送验证码次数为 0,将直接使用当前时间窗口轮询邮箱。`, 'info');
+        } else {
+          try {
+            lastResendAt = await requestVerificationCodeResend(step);
+            remainingAutomaticResendCount -= 1;
+            await updateFilterAfterTimestampForVerificationStep(lastResendAt);
+            await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
+          } catch (err) {
+            if (isStopError(err)) {
+              throw err;
+            }
+            await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
           }
           }
-          await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
         }
         }
       }
       }
 
 
       if (mail.provider === HOTMAIL_PROVIDER) {
       if (mail.provider === HOTMAIL_PROVIDER) {
-        const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0;
-        if (initialDelayMs > 0) {
-          await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
-          await sleepWithStop(initialDelayMs);
-        }
-      }
-
-      for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
-        const pollOptions = {
-          excludeCodes: [...rejectedCodes],
-          resendIntervalMs,
-          lastResendAt,
-          onResendRequestedAt: updateFilterAfterTimestampForVerificationStep,
-        };
-        if (nextFilterAfterTimestamp !== null && nextFilterAfterTimestamp !== undefined) {
-          pollOptions.filterAfterTimestamp = nextFilterAfterTimestamp;
+          const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0;
+          if (initialDelayMs > 0) {
+            await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
+            await sleepWithStop(initialDelayMs);
+          }
         }
         }
-        const result = await pollFreshVerificationCode(step, state, mail, pollOptions);
-        lastResendAt = Number(result?.lastResendAt) || lastResendAt;
 
 
-        throwIfStopped();
-        await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
-        if (beforeSubmit) {
-          await beforeSubmit(result, {
-            attempt,
-            rejectedCodes: new Set(rejectedCodes),
-            filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
+        for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
+          const pollOptions = {
+            excludeCodes: [...rejectedCodes],
+            maxResendRequests: remainingAutomaticResendCount,
+            resendIntervalMs,
             lastResendAt,
             lastResendAt,
-          });
-        }
-        throwIfStopped();
-        const submitResult = await submitVerificationCode(step, result.code);
-
-        if (submitResult.invalidCode) {
-          rejectedCodes.add(result.code);
-          await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
+            onResendRequestedAt: updateFilterAfterTimestampForVerificationStep,
+          };
+          if (nextFilterAfterTimestamp !== null && nextFilterAfterTimestamp !== undefined) {
+            pollOptions.filterAfterTimestamp = nextFilterAfterTimestamp;
+          }
+          const result = await pollFreshVerificationCode(step, state, mail, pollOptions);
+          lastResendAt = Number(result?.lastResendAt) || lastResendAt;
+          remainingAutomaticResendCount = normalizeVerificationResendCount(
+            result?.remainingResendRequests,
+            remainingAutomaticResendCount
+          );
 
 
-          if (attempt >= maxSubmitAttempts) {
-            throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
+          throwIfStopped();
+          await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
+          if (beforeSubmit) {
+            await beforeSubmit(result, {
+              attempt,
+              rejectedCodes: new Set(rejectedCodes),
+              filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
+              lastResendAt,
+            });
           }
           }
+          throwIfStopped();
+          const submitResult = await submitVerificationCode(step, result.code);
 
 
-          const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
-            ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
-            : 0;
-          if (remainingBeforeResendMs > 0) {
-            await addLog(
-              `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
-              'warn'
-            );
+          if (submitResult.invalidCode) {
+            rejectedCodes.add(result.code);
+            await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
+
+            if (attempt >= maxSubmitAttempts) {
+              throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
+            }
+
+            const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
+              ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
+              : 0;
+            if (remainingBeforeResendMs > 0) {
+              await addLog(
+                `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
+                'warn'
+              );
+              continue;
+            }
+
+            if (remainingAutomaticResendCount <= 0) {
+              await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将直接使用当前时间窗口继续重试。`, 'warn');
+              continue;
+            }
+
+            lastResendAt = await requestVerificationCodeResend(step);
+            remainingAutomaticResendCount -= 1;
+            await updateFilterAfterTimestampForVerificationStep(lastResendAt);
+            await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
             continue;
             continue;
           }
           }
 
 
-          lastResendAt = await requestVerificationCodeResend(step);
-          await updateFilterAfterTimestampForVerificationStep(lastResendAt);
-          await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
-          continue;
-        }
-
-        await setState({
-          lastEmailTimestamp: result.emailTimestamp,
-          [stateKey]: result.code,
-        });
+          await setState({
+            lastEmailTimestamp: result.emailTimestamp,
+            [stateKey]: result.code,
+          });
 
 
-        await completeStepFromBackground(step, {
-          emailTimestamp: result.emailTimestamp,
-          code: result.code,
-        });
-        return;
+          await completeStepFromBackground(step, {
+            emailTimestamp: result.emailTimestamp,
+            code: result.code,
+          });
+          return;
+        }
       }
       }
+
+      return {
+        confirmCustomVerificationStepBypass,
+        getVerificationCodeLabel,
+        getVerificationCodeStateKey,
+        getVerificationPollPayload,
+        pollFreshVerificationCode,
+        pollFreshVerificationCodeWithResendInterval,
+        requestVerificationCodeResend,
+        resolveVerificationStep,
+        submitVerificationCode,
+      };
     }
     }
 
 
     return {
     return {
-      confirmCustomVerificationStepBypass,
-      getVerificationCodeLabel,
-      getVerificationCodeStateKey,
-      getVerificationPollPayload,
-      pollFreshVerificationCode,
-      pollFreshVerificationCodeWithResendInterval,
-      requestVerificationCodeResend,
-      resolveVerificationStep,
-      submitVerificationCode,
+      createVerificationFlowHelpers,
     };
     };
-  }
-
-  return {
-    createVerificationFlowHelpers,
-  };
-});
+  });

+ 5 - 0
sidepanel/sidepanel.css

@@ -1259,6 +1259,11 @@ header {
   margin-left: auto;
   margin-left: auto;
 }
 }
 
 
+.setting-inline-right {
+  min-width: 196px;
+  justify-content: flex-end;
+}
+
 .toggle-switch {
 .toggle-switch {
   position: relative;
   position: relative;
   display: inline-flex;
   display: inline-flex;

+ 29 - 22
sidepanel/sidepanel.html

@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!DOCTYPE html>
 <html lang="zh-CN">
 <html lang="zh-CN">
 
 
 <head>
 <head>
@@ -243,25 +243,6 @@
           <button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
           <button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
         </div>
         </div>
       </div>
       </div>
-      <div class="data-row">
-        <span class="data-label">自动重试</span>
-        <div class="data-inline fallback-inline">
-          <label class="toggle-switch" for="input-auto-skip-failures">
-            <input type="checkbox" id="input-auto-skip-failures" />
-            <span class="toggle-switch-track" aria-hidden="true">
-              <span class="toggle-switch-thumb"></span>
-            </span>
-          </label>
-          <div class="timing-field timing-field-labeled timing-field-right fallback-thread-interval">
-            <span class="auto-delay-caption">线程间隔</span>
-            <div class="auto-delay-controls">
-              <input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0"
-                min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
-              <span class="data-unit">分钟</span>
-            </div>
-          </div>
-        </div>
-      </div>
       <div class="data-row">
       <div class="data-row">
         <span class="data-label">延迟</span>
         <span class="data-label">延迟</span>
         <div class="data-inline auto-delay-inline">
         <div class="data-inline auto-delay-inline">
@@ -288,16 +269,42 @@
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
+      <div class="data-row">
+        <span class="data-label">自动重试</span>
+        <div class="data-inline fallback-inline">
+          <label class="toggle-switch" for="input-auto-skip-failures">
+            <input type="checkbox" id="input-auto-skip-failures" />
+            <span class="toggle-switch-track" aria-hidden="true">
+              <span class="toggle-switch-thumb"></span>
+            </span>
+          </label>
+          <div class="timing-field timing-field-labeled timing-field-right fallback-thread-interval">
+            <span class="auto-delay-caption">线程间隔</span>
+            <div class="auto-delay-controls">
+              <input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0"
+                min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
+              <span class="data-unit">分钟</span>
+            </div>
+          </div>
+        </div>
+      </div>
       <div class="data-row">
       <div class="data-row">
         <span class="data-label">本地留档</span>
         <span class="data-label">本地留档</span>
-        <div class="data-inline">
+        <div class="data-inline auto-delay-inline">
           <label class="toggle-switch" for="input-account-run-history-text-enabled">
           <label class="toggle-switch" for="input-account-run-history-text-enabled">
             <input type="checkbox" id="input-account-run-history-text-enabled" />
             <input type="checkbox" id="input-account-run-history-text-enabled" />
             <span class="toggle-switch-track" aria-hidden="true">
             <span class="toggle-switch-track" aria-hidden="true">
               <span class="toggle-switch-thumb"></span>
               <span class="toggle-switch-thumb"></span>
             </span>
             </span>
           </label>
           </label>
-          <span class="data-value">同步写入账号运行历史 txt</span>
+          <div class="timing-field timing-field-right setting-inline-right">
+            <span class="auto-delay-caption">验证码重发</span>
+            <div class="auto-delay-controls">
+              <input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
+                min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
+              <span class="data-unit">次</span>
+            </div>
+          </div>
         </div>
         </div>
       </div>
       </div>
       <div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
       <div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">

+ 65 - 0
sidepanel/sidepanel.js

@@ -164,6 +164,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('inpu
 const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
 const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
 const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
 const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
 const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
 const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
+const inputVerificationResendCount = document.getElementById('input-verification-resend-count');
 const inputAccountRunHistoryTextEnabled = document.getElementById('input-account-run-history-text-enabled');
 const inputAccountRunHistoryTextEnabled = document.getElementById('input-account-run-history-text-enabled');
 const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
 const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
 const inputAccountRunHistoryHelperBaseUrl = document.getElementById('input-account-run-history-helper-base-url');
 const inputAccountRunHistoryHelperBaseUrl = document.getElementById('input-account-run-history-helper-base-url');
@@ -193,6 +194,9 @@ const AUTO_FALLBACK_THREAD_INTERVAL_DEFAULT_MINUTES = 0;
 const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
 const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
 const AUTO_STEP_DELAY_MIN_SECONDS = 0;
 const AUTO_STEP_DELAY_MIN_SECONDS = 0;
 const AUTO_STEP_DELAY_MAX_SECONDS = 600;
 const AUTO_STEP_DELAY_MAX_SECONDS = 600;
+const VERIFICATION_RESEND_COUNT_MIN = 0;
+const VERIFICATION_RESEND_COUNT_MAX = 20;
+const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_CPA_CALLBACK_MODE = 'step9';
 const DEFAULT_CPA_CALLBACK_MODE = 'step9';
 const MAIL_2925_MODE_PROVIDE = 'provide';
 const MAIL_2925_MODE_PROVIDE = 'provide';
@@ -976,6 +980,23 @@ function normalizeAutoStepDelaySeconds(value) {
   return Math.min(AUTO_STEP_DELAY_MAX_SECONDS, Math.max(AUTO_STEP_DELAY_MIN_SECONDS, Math.floor(numeric)));
   return Math.min(AUTO_STEP_DELAY_MAX_SECONDS, Math.max(AUTO_STEP_DELAY_MIN_SECONDS, Math.floor(numeric)));
 }
 }
 
 
+function normalizeVerificationResendCount(value, fallback) {
+  const rawValue = String(value ?? '').trim();
+  if (!rawValue) {
+    return fallback;
+  }
+
+  const numeric = Number(rawValue);
+  if (!Number.isFinite(numeric)) {
+    return fallback;
+  }
+
+  return Math.min(
+    VERIFICATION_RESEND_COUNT_MAX,
+    Math.max(VERIFICATION_RESEND_COUNT_MIN, Math.floor(numeric))
+  );
+}
+
 function formatAutoStepDelayInputValue(value) {
 function formatAutoStepDelayInputValue(value) {
   const normalized = normalizeAutoStepDelaySeconds(value);
   const normalized = normalizeAutoStepDelaySeconds(value);
   return normalized === null ? '' : String(normalized);
   return normalized === null ? '' : String(normalized);
@@ -1310,6 +1331,10 @@ function collectSettingsPayload() {
     autoRunDelayEnabled: inputAutoDelayEnabled.checked,
     autoRunDelayEnabled: inputAutoDelayEnabled.checked,
     autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
     autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
     autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value),
     autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value),
+    verificationResendCount: normalizeVerificationResendCount(
+      inputVerificationResendCount?.value,
+      DEFAULT_VERIFICATION_RESEND_COUNT
+    ),
   };
   };
 }
 }
 
 
@@ -1729,6 +1754,14 @@ function applySettingsState(state) {
   inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
   inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
   inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
   inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
   inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds);
   inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds);
+  if (inputVerificationResendCount) {
+    const restoredVerificationResendCount = state?.verificationResendCount !== undefined
+      ? state.verificationResendCount
+      : (state?.signupVerificationResendCount ?? state?.loginVerificationResendCount);
+    inputVerificationResendCount.value = String(
+      normalizeVerificationResendCount(restoredVerificationResendCount, DEFAULT_VERIFICATION_RESEND_COUNT)
+    );
+  }
   if (state?.autoRunTotalRuns) {
   if (state?.autoRunTotalRuns) {
     inputRunCount.value = String(state.autoRunTotalRuns);
     inputRunCount.value = String(state.autoRunTotalRuns);
   }
   }
@@ -3992,6 +4025,20 @@ inputAutoStepDelaySeconds.addEventListener('blur', () => {
   saveSettings({ silent: true }).catch(() => { });
   saveSettings({ silent: true }).catch(() => { });
 });
 });
 
 
+inputVerificationResendCount?.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputVerificationResendCount?.addEventListener('blur', () => {
+  inputVerificationResendCount.value = String(
+    normalizeVerificationResendCount(
+      inputVerificationResendCount.value,
+      DEFAULT_VERIFICATION_RESEND_COUNT
+    )
+  );
+  saveSettings({ silent: true }).catch(() => { });
+});
+
 // ============================================================
 // ============================================================
 // Listen for Background broadcasts
 // Listen for Background broadcasts
 // ============================================================
 // ============================================================
@@ -4192,6 +4239,24 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
       if (message.payload.autoStepDelaySeconds !== undefined) {
       if (message.payload.autoStepDelaySeconds !== undefined) {
         inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(message.payload.autoStepDelaySeconds);
         inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(message.payload.autoStepDelaySeconds);
       }
       }
+      if (
+        (
+          message.payload.verificationResendCount !== undefined
+          || message.payload.signupVerificationResendCount !== undefined
+          || message.payload.loginVerificationResendCount !== undefined
+        )
+        && inputVerificationResendCount
+      ) {
+        const nextVerificationResendCount = message.payload.verificationResendCount !== undefined
+          ? message.payload.verificationResendCount
+          : (message.payload.signupVerificationResendCount ?? message.payload.loginVerificationResendCount);
+        inputVerificationResendCount.value = String(
+          normalizeVerificationResendCount(
+            nextVerificationResendCount,
+            DEFAULT_VERIFICATION_RESEND_COUNT
+          )
+        );
+      }
       break;
       break;
     }
     }
 
 

+ 6 - 0
tests/background-account-history-settings.test.js

@@ -52,6 +52,7 @@ test('background account history settings are normalized independently from hotm
   const bundle = [
   const bundle = [
     extractFunction('normalizeHotmailLocalBaseUrl'),
     extractFunction('normalizeHotmailLocalBaseUrl'),
     extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
     extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
+    extractFunction('normalizeVerificationResendCount'),
     extractFunction('normalizePersistentSettingValue'),
     extractFunction('normalizePersistentSettingValue'),
   ].join('\n');
   ].join('\n');
 
 
@@ -59,8 +60,11 @@ test('background account history settings are normalized independently from hotm
 const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
 const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
 const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
 const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
 const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
 const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
+const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
 const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
 const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
 const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
 const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
+const VERIFICATION_RESEND_COUNT_MIN = 0;
+const VERIFICATION_RESEND_COUNT_MAX = 20;
 const PERSISTED_SETTING_DEFAULTS = {
 const PERSISTED_SETTING_DEFAULTS = {
   autoStepDelaySeconds: null,
   autoStepDelaySeconds: null,
   mailProvider: '163',
   mailProvider: '163',
@@ -95,6 +99,8 @@ return {
   `)();
   `)();
 
 
   assert.equal(api.normalizePersistentSettingValue('accountRunHistoryTextEnabled', 1), true);
   assert.equal(api.normalizePersistentSettingValue('accountRunHistoryTextEnabled', 1), true);
+  assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
+  assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
   assert.equal(
   assert.equal(
     api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
     api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
     'http://127.0.0.1:17373'
     'http://127.0.0.1:17373'

+ 6 - 0
tests/background-icloud.test.js

@@ -54,6 +54,7 @@ function extractFunction(name) {
 const bundle = [
 const bundle = [
   extractFunction('normalizeEmailGenerator'),
   extractFunction('normalizeEmailGenerator'),
   extractFunction('getEmailGeneratorLabel'),
   extractFunction('getEmailGeneratorLabel'),
+  extractFunction('normalizeVerificationResendCount'),
   extractFunction('normalizePersistentSettingValue'),
   extractFunction('normalizePersistentSettingValue'),
   extractFunction('finalizeIcloudAliasAfterSuccessfulFlow'),
   extractFunction('finalizeIcloudAliasAfterSuccessfulFlow'),
 ].join('\n');
 ].join('\n');
@@ -66,6 +67,9 @@ const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
 const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
 const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
 const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
+const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
+const VERIFICATION_RESEND_COUNT_MIN = 0;
+const VERIFICATION_RESEND_COUNT_MAX = 20;
 const PERSISTED_SETTING_DEFAULTS = {
 const PERSISTED_SETTING_DEFAULTS = {
   mailProvider: '163',
   mailProvider: '163',
   autoStepDelaySeconds: null,
   autoStepDelaySeconds: null,
@@ -175,6 +179,8 @@ test('normalizePersistentSettingValue handles icloud settings', () => {
   assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com');
   assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com');
   assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto');
   assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto');
   assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true);
   assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true);
+  assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '6'), 6);
+  assert.equal(api.normalizePersistentSettingValue('verificationResendCount', 99), 20);
   assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailReceiveMailbox', ' Forward@Example.com '), 'forward@example.com');
   assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailReceiveMailbox', ' Forward@Example.com '), 'forward@example.com');
 });
 });
 
 

+ 118 - 0
tests/verification-flow-polling.test.js

@@ -232,3 +232,121 @@ test('verification flow refreshes Hotmail signup filter timestamp after step 4 r
   assert.equal(pollPayloads[0].filterAfterTimestamp, 85000);
   assert.equal(pollPayloads[0].filterAfterTimestamp, 85000);
   assert.equal(pollPayloads[1].filterAfterTimestamp, 185000);
   assert.equal(pollPayloads[1].filterAfterTimestamp, 185000);
 });
 });
+
+test('verification flow uses configured signup resend count for step 4', async () => {
+  const resendSteps = [];
+  let pollCalls = 0;
+
+  const helpers = api.createVerificationFlowHelpers({
+    addLog: async () => {},
+    chrome: { tabs: { update: async () => {} } },
+    CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
+    completeStepFromBackground: async () => {},
+    confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
+    getHotmailVerificationPollConfig: () => ({}),
+    getHotmailVerificationRequestTimestamp: () => 0,
+    getState: async () => ({}),
+    getTabId: async () => 1,
+    HOTMAIL_PROVIDER: 'hotmail-api',
+    isStopError: () => false,
+    LUCKMAIL_PROVIDER: 'luckmail-api',
+    MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
+    MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
+    pollCloudflareTempEmailVerificationCode: async () => ({}),
+    pollHotmailVerificationCode: async () => ({}),
+    pollLuckmailVerificationCode: async () => ({}),
+    sendToContentScript: async (_source, message) => {
+      if (message.type === 'RESEND_VERIFICATION_CODE') {
+        resendSteps.push(message.step);
+      }
+      return {};
+    },
+    sendToMailContentScriptResilient: async () => {
+      pollCalls += 1;
+      return pollCalls === 2
+        ? { code: '654321', emailTimestamp: 123 }
+        : {};
+    },
+    setState: async () => {},
+    setStepStatus: async () => {},
+    sleepWithStop: async () => {},
+    throwIfStopped: () => {},
+    VERIFICATION_POLL_MAX_ROUNDS: 5,
+  });
+
+  await helpers.resolveVerificationStep(
+    4,
+    {
+      email: 'user@example.com',
+      verificationResendCount: 2,
+      lastSignupCode: null,
+    },
+    { provider: 'qq', label: 'QQ 邮箱' },
+    {
+      requestFreshCodeFirst: true,
+      resendIntervalMs: 0,
+    }
+  );
+
+  assert.deepStrictEqual(resendSteps, [4, 4]);
+  assert.equal(pollCalls, 2);
+});
+
+test('verification flow uses configured login resend count for step 8', async () => {
+  const resendSteps = [];
+  let pollCalls = 0;
+
+  const helpers = api.createVerificationFlowHelpers({
+    addLog: async () => {},
+    chrome: { tabs: { update: async () => {} } },
+    CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
+    completeStepFromBackground: async () => {},
+    confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
+    getHotmailVerificationPollConfig: () => ({}),
+    getHotmailVerificationRequestTimestamp: () => 0,
+    getState: async () => ({}),
+    getTabId: async () => 1,
+    HOTMAIL_PROVIDER: 'hotmail-api',
+    isStopError: () => false,
+    LUCKMAIL_PROVIDER: 'luckmail-api',
+    MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
+    MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
+    pollCloudflareTempEmailVerificationCode: async () => ({}),
+    pollHotmailVerificationCode: async () => ({}),
+    pollLuckmailVerificationCode: async () => ({}),
+    sendToContentScript: async (_source, message) => {
+      if (message.type === 'RESEND_VERIFICATION_CODE') {
+        resendSteps.push(message.step);
+      }
+      return {};
+    },
+    sendToMailContentScriptResilient: async () => {
+      pollCalls += 1;
+      return pollCalls === 3
+        ? { code: '654321', emailTimestamp: 123 }
+        : {};
+    },
+    setState: async () => {},
+    setStepStatus: async () => {},
+    sleepWithStop: async () => {},
+    throwIfStopped: () => {},
+    VERIFICATION_POLL_MAX_ROUNDS: 5,
+  });
+
+  await helpers.resolveVerificationStep(
+    8,
+    {
+      email: 'user@example.com',
+      verificationResendCount: 2,
+      lastLoginCode: null,
+    },
+    { provider: 'qq', label: 'QQ 邮箱' },
+    {
+      requestFreshCodeFirst: false,
+      resendIntervalMs: 0,
+    }
+  );
+
+  assert.deepStrictEqual(resendSteps, [8, 8]);
+  assert.equal(pollCalls, 3);
+});

+ 2 - 1
项目完整链路说明.md

@@ -1,4 +1,4 @@
-# 项目完整链路说明
+# 项目完整链路说明
 
 
 本文档面向 AI 与开发者,目标是让阅读者在最短时间内理解“项目做什么、怎么跑、数据怎么流、功能链路怎么串”,从而在新增功能时不漏逻辑、不误改边界。
 本文档面向 AI 与开发者,目标是让阅读者在最短时间内理解“项目做什么、怎么跑、数据怎么流、功能链路怎么串”,从而在新增功能时不漏逻辑、不误改边界。
 
 
@@ -247,6 +247,7 @@
 
 
 - `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 8 的自动重发间隔,减少因邮件延迟过早判负。
 - `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 8 的自动重发间隔,减少因邮件延迟过早判负。
 - Hotmail 在 Step 4 / 8 首轮轮询时会优先使用最近一次验证码请求时间作为时间窗口;如果中途触发重发,轮询窗口也会同步前移,避免旧邮件反复命中。
 - Hotmail 在 Step 4 / 8 首轮轮询时会优先使用最近一次验证码请求时间作为时间窗口;如果中途触发重发,轮询窗口也会同步前移,避免旧邮件反复命中。
+- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
 - CPA 模式下,Step 8 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 7,降低验证码等待过久导致 OAuth 过期的概率。
 - CPA 模式下,Step 8 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 7,降低验证码等待过久导致 OAuth 过期的概率。
 
 
 - Auto 模式下,如果 Step 4 当前轮失败,后台会沿用当前邮箱回到 Step 1 重新开始当前轮,而不是立刻换邮箱开新尝试。
 - Auto 模式下,如果 Step 4 当前轮失败,后台会沿用当前邮箱回到 Step 1 重新开始当前轮,而不是立刻换邮箱开新尝试。

+ 3 - 3
项目文件结构说明.md

@@ -49,7 +49,7 @@
 - `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
 - `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。
 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。
 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。
 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。
-- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过以及 2925 长轮询参数。
+- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数。
 
 
 ## `background/steps/`
 ## `background/steps/`
 
 
@@ -113,8 +113,8 @@
 - `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
 - `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
 - `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
 - `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
 - `sidepanel/sidepanel.css`:侧边栏样式文件。
 - `sidepanel/sidepanel.css`:侧边栏样式文件。
-- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区标题下方预留账号运行历史摘要条带,会加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
-- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、账号运行历史摘要渲染、独立 txt 留档配置与广播接收;同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。
+- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区标题下方预留账号运行历史摘要条带,并新增共享“验证码重发”次数输入,同时会加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
+- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、账号运行历史摘要渲染、独立 txt 留档配置、共享验证码自动重发次数配置与广播接收;同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。
 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询与版本展示。
 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询与版本展示。
 
 
 ## `tests/`
 ## `tests/`