diff --git a/background/verification-flow.js b/background/verification-flow.js index dcade7a..017b3bb 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -675,6 +675,68 @@ throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); } + function shouldRequestLuckmailResendBeforeRetry(error) { + const message = String(error?.message || error || ''); + if (!message) { + return true; + } + + return !/没有可用 token|token 对应邮箱与当前邮箱不一致/i.test(message); + } + + async function pollLuckmailVerificationCodeWithResend(step, state, pollOverrides = {}) { + const { + onResendRequestedAt, + maxRounds: _ignoredMaxRounds, + maxResendRequests: _ignoredMaxResendRequests, + ...cleanPollOverrides + } = pollOverrides; + const basePayload = { + ...getVerificationPollPayload(step, state), + ...cleanPollOverrides, + }; + const maxAttempts = Math.max(1, Number(basePayload.maxAttempts) || 1); + const intervalMs = Math.max(15000, Number(basePayload.intervalMs) || 15000); + let lastError = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + throwIfStopped(); + try { + return await pollLuckmailVerificationCode(step, state, { + ...basePayload, + maxAttempts: 1, + intervalMs, + }); + } catch (err) { + if (isStopError(err)) { + throw err; + } + + lastError = err; + const canRetry = attempt < maxAttempts; + if (!canRetry) { + break; + } + + if (shouldRequestLuckmailResendBeforeRetry(err)) { + try { + await requestVerificationCodeResend(step, pollOverrides); + } catch (resendError) { + if (isStopError(resendError)) { + throw resendError; + } + await addLog(`步骤 ${step}:LuckMail 点击重新发送验证码失败:${resendError.message},仍将在 ${Math.ceil(intervalMs / 1000)} 秒后继续轮询 /code 接口。`, 'warn'); + } + } + + await addLog(`步骤 ${step}:LuckMail 暂未获取到新的${getVerificationCodeLabel(step)}验证码,等待 ${Math.ceil(intervalMs / 1000)} 秒后继续轮询 /code 接口(${attempt + 1}/${maxAttempts})...`, 'warn'); + await sleepWithStop(intervalMs); + } + } + + throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); + } + async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) { const { onResendRequestedAt, @@ -697,7 +759,11 @@ ...getVerificationPollPayload(step, state), ...cleanPollOverrides, }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); - return pollLuckmailVerificationCode(step, state, timedPoll.payload); + return pollLuckmailVerificationCodeWithResend(step, state, { + ...cleanPollOverrides, + ...timedPoll.payload, + onResendRequestedAt, + }); } if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { const timedPoll = await applyMailPollingTimeBudget(step, { diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index 2be3c66..8dbb3f2 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -1030,6 +1030,75 @@ test('verification flow waits during resend cooldown instead of tight-looping', assert.ok(sleepCalls[0] >= 1000); }); +test('verification flow clicks resend before waiting for the next LuckMail /code retry', async () => { + const events = []; + 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 (_step, _state, payload) => { + pollCalls += 1; + events.push(['poll', payload.maxAttempts, payload.intervalMs]); + if (pollCalls === 1) { + throw new Error('步骤 4:LuckMail /code 接口暂未返回新的验证码。'); + } + return { + code: '654321', + emailTimestamp: 123, + }; + }, + sendToContentScript: async (_source, message) => { + if (message.type === 'RESEND_VERIFICATION_CODE') { + events.push(['resend', message.step]); + } + return {}; + }, + sendToMailContentScriptResilient: async () => ({}), + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async (ms) => { + events.push(['sleep', ms]); + }, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + const result = await helpers.pollFreshVerificationCode( + 4, + { + email: 'user@example.com', + lastSignupCode: null, + }, + { provider: 'luckmail-api', label: 'LuckMail(API 购邮)' }, + { + resendIntervalMs: 15000, + } + ); + + assert.equal(result.code, '654321'); + assert.deepStrictEqual(events, [ + ['poll', 1, 15000], + ['resend', 4], + ['sleep', 15000], + ['poll', 1, 15000], + ]); +}); + test('verification flow uses resilient signup-page transport when submitting verification code', async () => { const resilientCalls = []; diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 77acb92..dcf7ff2 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -354,6 +354,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 - `2925` 在执行自动登录后,如果登录页因为跳转或重载导致原内容脚本通信中断,后台不会立刻判失败;而是会等待当前标签页重新加载完成、重新确认内容脚本就绪后,再继续确认是否已经进入收件箱。这段登录恢复窗口当前按 2 分钟控制。 - 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 在 Step 4 / Step 8 会固定使用“步骤开始时间向前回看 10 分钟”的时间窗,不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中落在该固定时间窗内的匹配邮件。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 +- `LuckMail` 在 `/code` 接口暂未拿到新验证码时,会先点击一次认证页“重新发送验证码”,再等待 15 秒后继续轮询 `/code`,避免只空等不触发新邮件。 - 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 - `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。 - `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。