diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index d147bb3..fb7c78c 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -60,6 +60,7 @@ if (mail.error) throw new Error(mail.error); const stepStartedAt = Date.now(); + const verificationSessionKey = `8:${stepStartedAt}`; const authTabId = await getTabId('signup-page'); if (authTabId) { @@ -130,7 +131,9 @@ ...state, step8VerificationTargetEmail: displayedVerificationEmail || '', }, mail, { - filterAfterTimestamp: stepStartedAt, + filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt, + sessionKey: verificationSessionKey, + disableTimeBudgetCap: mail.provider === '2925', getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''), requestFreshCodeFirst: false, targetEmail: fixedTargetEmail, diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index e7fe682..a729946 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -25,6 +25,7 @@ const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); const stepStartedAt = Date.now(); + const verificationSessionKey = `4:${stepStartedAt}`; const signupTabId = await getTabId('signup-page'); if (!signupTabId) { throw new Error('认证页面标签页已关闭,无法继续步骤 4。'); @@ -91,7 +92,9 @@ } await resolveVerificationStep(4, state, mail, { - filterAfterTimestamp: stepStartedAt, + filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt, + sessionKey: verificationSessionKey, + disableTimeBudgetCap: mail.provider === '2925', requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') ? 0 diff --git a/background/verification-flow.js b/background/verification-flow.js index 76e18b2..d751359 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -164,9 +164,10 @@ const nextPayload = { ...payload }; const intervalMs = Math.max(1, Number(nextPayload.intervalMs) || 3000); const baseMaxAttempts = Math.max(1, Number(nextPayload.maxAttempts) || 1); + const disableTimeBudgetCap = Boolean(options.disableTimeBudgetCap); const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel); - if (remainingMs !== null) { + if (!disableTimeBudgetCap && remainingMs !== null) { nextPayload.maxAttempts = Math.max( 1, Math.min(baseMaxAttempts, Math.floor(Math.max(0, remainingMs - 1000) / intervalMs) + 1) @@ -174,7 +175,7 @@ } const defaultResponseTimeoutMs = Math.max(45000, nextPayload.maxAttempts * intervalMs + 25000); - const responseTimeoutMs = remainingMs === null + const responseTimeoutMs = disableTimeBudgetCap || remainingMs === null ? defaultResponseTimeoutMs : Math.max(1000, Math.min(defaultResponseTimeoutMs, remainingMs)); @@ -564,7 +565,7 @@ getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst }) ) : getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst }); - const maxSubmitAttempts = 7; + const maxSubmitAttempts = 15; const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0); let lastResendAt = Number(options.lastResendAt) || 0; @@ -609,6 +610,7 @@ for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) { const pollOptions = { excludeCodes: [...rejectedCodes], + disableTimeBudgetCap: Boolean(options.disableTimeBudgetCap), getRemainingTimeMs: options.getRemainingTimeMs, maxResendRequests: remainingAutomaticResendCount, resendIntervalMs, diff --git a/content/mail-2925.js b/content/mail-2925.js index 404477c..6b7f33f 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -53,6 +53,10 @@ async function persistSeenCodes() { } function buildSeenCodeSessionKey(step, payload = {}) { + const explicitSessionKey = String(payload?.sessionKey || '').trim(); + if (explicitSessionKey) { + return explicitSessionKey; + } const timestamp = Number(payload?.filterAfterTimestamp); if (Number.isFinite(timestamp) && timestamp > 0) { return `${step}:${timestamp}`; @@ -551,11 +555,7 @@ async function handlePollEmail(step, payload) { throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。'); } - const knownMailIds = getCurrentMailIds(initialItems); log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`); - log(`步骤 ${step}:已记录当前 ${knownMailIds.size} 封旧邮件快照`); - - const FALLBACK_AFTER = 3; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts} 次`); @@ -568,26 +568,10 @@ async function handlePollEmail(step, payload) { const items = findMailItems(); if (items.length > 0) { - const useFallback = attempt > FALLBACK_AFTER; - const newMailIds = new Set(); - - items.forEach((item, index) => { - const itemId = getMailItemId(item, index); - if (!knownMailIds.has(itemId)) { - newMailIds.add(itemId); - } - }); - for (let index = 0; index < items.length; index += 1) { const item = items[index]; - const itemId = getMailItemId(item, index); - const isNewMail = newMailIds.has(itemId); const itemTimestamp = parseMailItemTimestamp(item); - if (!useFallback && !isNewMail) { - continue; - } - const previewText = getMailItemText(item); if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) { continue; @@ -613,21 +597,11 @@ async function handlePollEmail(step, payload) { seenCodes.add(candidateCode); persistSeenCodes(); - const source = useFallback && !isNewMail - ? (bodyCode ? '回退匹配邮件正文' : '回退匹配邮件') - : (bodyCode ? '新邮件正文' : '新邮件'); + const source = bodyCode ? '邮件正文' : '邮件预览'; const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : ''; log(`步骤 ${step}:已找到验证码:${candidateCode}(来源:${source}${timeLabel})`, 'ok'); return { ok: true, code: candidateCode, emailTimestamp: Date.now() }; } - - items.forEach((item, index) => { - knownMailIds.add(getMailItemId(item, index)); - }); - } - - if (attempt === FALLBACK_AFTER + 1) { - log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn'); } if (attempt < maxAttempts) { diff --git a/tests/mail-2925-content.test.js b/tests/mail-2925-content.test.js index 30df390..0b2f037 100644 --- a/tests/mail-2925-content.test.js +++ b/tests/mail-2925-content.test.js @@ -60,13 +60,22 @@ let refreshCalls = 0; const clickOrder = []; const readAndDeleteCalls = []; const seenCodes = new Set(); +const deletedMailIds = new Set(); const baselineMail = { id: 'baseline', text: 'OpenAI newsletter without code' }; const newMail = { id: 'new', text: 'OpenAI verification code 654321' }; function findMailItems() { - if (state === 'detail') return []; - if (state === 'baseline') return [baselineMail]; - return [baselineMail, newMail]; + const items = []; + if (state === 'detail') return items; + if (state === 'baseline' || state === 'with-new') { + if (!deletedMailIds.has('baseline')) { + items.push(baselineMail); + } + } + if (state === 'with-new' && !deletedMailIds.has('new')) { + items.push(newMail); + } + return items; } function getMailItemId(item) { @@ -99,7 +108,9 @@ async function sleepRandom() {} async function returnToInbox() { clickOrder.push('inbox'); - state = 'baseline'; + if (state === 'detail') { + state = 'baseline'; + } return true; } @@ -113,6 +124,7 @@ async function refreshInbox() { async function openMailAndDeleteAfterRead(item) { readAndDeleteCalls.push(item.id); + deletedMailIds.add(item.id); return item.id === 'new' ? 'Your ChatGPT code is 654321' : 'No code here'; } @@ -142,7 +154,7 @@ return { assert.equal(result.code, '654321'); assert.deepEqual(api.getClickOrder(), ['inbox', 'refresh', 'inbox', 'refresh']); - assert.deepEqual(api.getReadAndDeleteCalls(), ['new']); + assert.deepEqual(api.getReadAndDeleteCalls(), ['baseline', 'new']); }); test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => { diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index 60ee929..5cb5b40 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -301,6 +301,73 @@ test('verification flow caps mail polling timeout to the remaining oauth budget' assert.equal(mailPollCalls[0].payload.maxAttempts, 2); }); +test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even when oauth budget is smaller', async () => { + const mailPollCalls = []; + + 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 === 'FILL_CODE') { + return {}; + } + return {}; + }, + sendToMailContentScriptResilient: async (_mail, message, options) => { + mailPollCalls.push({ + type: message.type, + payload: message.payload, + options, + }); + return { code: '654321', emailTimestamp: 123 }; + }, + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await helpers.resolveVerificationStep( + 8, + { + email: 'user@example.com', + mailProvider: '2925', + lastLoginCode: null, + }, + { provider: '2925', label: '2925 邮箱' }, + { + getRemainingTimeMs: async () => 5000, + resendIntervalMs: 0, + disableTimeBudgetCap: true, + } + ); + + const pollCall = mailPollCalls.find((entry) => entry.type === 'POLL_EMAIL'); + assert.ok(pollCall); + assert.equal(pollCall.payload.maxAttempts, 15); + assert.ok(pollCall.options.timeoutMs >= 250000); +}); + test('verification flow keeps Hotmail request timestamp filtering on the first poll', async () => { const pollPayloads = []; diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 86b6c13..c99d8a2 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -304,12 +304,11 @@ 补充行为: -- `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 8 的自动重发间隔,减少因邮件延迟过早判负。 -- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 不再依赖时间窗,而是以“当前收件箱快照 + 刷新后新增邮件”作为优先匹配依据。 +- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。 +- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 -- 验证码提交重试上限当前为 7 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 -- `2925` 内容脚本会把每次成功读到的目标邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交。 -- `2925` 在连续 3 轮未发现新增邮件后,会回退检查列表里现存的匹配邮件;每轮轮询之间只刷新 1 次收件箱列表。 +- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 +- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。 - `2925` 当前不再对邮件里的收件邮箱做比对,只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。 - 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index db04dae..4600b97 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -50,7 +50,7 @@ - `background/panel-bridge.js`:CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 -- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 7 次,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息。 +- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息。 ## `background/steps/` @@ -75,7 +75,7 @@ - `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。 - `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。 - `content/mail-163.js`:163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。 -- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。 +- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 - `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。