From 9228c7080e5e390a63083b7017549bfd6ff5f382 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 21 Apr 2026 16:39:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=962925=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=B1=A0=E8=A1=A8=E5=8D=95=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=AD=A5=E9=AA=A47=E6=97=A0=E6=B3=95=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E9=A1=B5=E9=9D=A2bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- content/signup-page.js | 127 +++++++++++++-- sidepanel/mail-2925-manager.js | 52 ++++-- sidepanel/sidepanel.css | 14 ++ sidepanel/sidepanel.html | 42 ++--- sidepanel/sidepanel.js | 6 +- tests/sidepanel-mail2925-edit.test.js | 9 +- tests/sidepanel-mail2925-manager.test.js | 197 ++++++++++++++++++++++- tests/step6-login-state.test.js | 21 +++ tests/step6-timeout-recovery.test.js | 108 +++++++++++++ 项目完整链路说明.md | 3 + 11 files changed, 532 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index e5f912e..967bb11 100644 --- a/README.md +++ b/README.md @@ -579,9 +579,11 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 已刷新到最新 OAuth 链接 - 认证页已经真正进入登录验证码页面 +- 在真正把 Step 7 记为完成前,还会再做一轮收尾确认;如果页面只是短暂进入登录验证码页、随后又掉进登录重试页,则不会直接进入 Step 8,而是先按共享恢复逻辑处理并重跑 Step 7 - 如遇登录超时报错,会先尝试通过共享恢复逻辑最多自动点击 5 次认证页上的 `重试` 恢复当前页面;若仍未恢复,再按既有逻辑重跑整个 Step 7 - 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 7 - 如果重试页内容中出现 `max_check_attempts`,会立刻完全停止流程,并在侧边栏复用现有确认弹窗提示这是 Cloudflare 风控拦截,确认按钮显示为“我知道了” +- Step 8 不负责替代 Step 7 的收尾确认;它默认消费的是“已经由 Step 7 确认稳定进入”的登录验证码页,只在后台入口做防御性状态兜底 支持: @@ -598,7 +600,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 打开邮箱并轮询登录验证码 - 填写并提交登录验证码 - 验证码链路失败后按有限次数回退到 Step 7 -- 如果进入登录超时报错/重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试` +- 如果进入登录超时报错/重试页,包括 `/email-verification` 上的 `405 / Route Error` 登录重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试` - 如果重试页内容中出现 `max_check_attempts`,会直接完全停止整个流程,并复用现有确认弹窗提醒先等待 15 到 30 分钟或更换浏览器,确认按钮显示为“我知道了” 与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 diff --git a/content/signup-page.js b/content/signup-page.js index a6d44d8..cd55aa6 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1198,7 +1198,7 @@ function getSignupPasswordTimeoutErrorPageState() { function getLoginTimeoutErrorPageState() { return getAuthTimeoutErrorPageState({ - pathPatterns: [/\/log-in(?:[/?#]|$)/i], + pathPatterns: getLoginAuthRetryPathPatterns(), }); } @@ -1263,13 +1263,6 @@ function inspectLoginAuthState() { consentReady, }; - if (verificationTarget) { - return { - ...baseState, - state: 'verification_page', - }; - } - if (retryState) { return { ...baseState, @@ -1277,6 +1270,13 @@ function inspectLoginAuthState() { }; } + if (verificationTarget) { + return { + ...baseState, + state: 'verification_page', + }; + } + if (addPhonePage) { return { ...baseState, @@ -1434,6 +1434,86 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag }); } +async function finalizeStep6VerificationReady(options = {}) { + const { + logLabel = '步骤 7 收尾', + loginVerificationRequestedAt = null, + timeout = 12000, + via = 'verification_page_ready', + } = options; + const start = Date.now(); + const maxRounds = 3; + const settleDelayMs = 3000; + let round = 0; + + while (Date.now() - start < timeout && round < maxRounds) { + throwIfStopped(); + round += 1; + log(`${logLabel}:确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info'); + await sleep(settleDelayMs); + + const rawSnapshot = inspectLoginAuthState(); + const snapshot = normalizeStep6Snapshot(rawSnapshot); + + if (snapshot.state === 'verification_page') { + log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok'); + return createStep6SuccessResult(snapshot, { + via, + loginVerificationRequestedAt, + }); + } + + if (snapshot.state === 'login_timeout_error_page') { + log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); + return createStep6LoginTimeoutRecoverableResult( + 'login_timeout_error_page', + snapshot, + '登录验证码页面准备就绪前进入登录超时报错页。' + ); + } + + if (snapshot.state === 'password_page' || snapshot.state === 'email_page') { + return createStep6RecoverableResult('verification_page_unstable', snapshot, { + message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`, + loginVerificationRequestedAt, + }); + } + + if (snapshot.state === 'add_phone_page') { + throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`); + } + } + + const rawSnapshot = inspectLoginAuthState(); + const snapshot = normalizeStep6Snapshot(rawSnapshot); + if (snapshot.state === 'verification_page') { + log(`${logLabel}:登录验证码页面已稳定就绪。`, 'ok'); + return createStep6SuccessResult(snapshot, { + via, + loginVerificationRequestedAt, + }); + } + if (snapshot.state === 'login_timeout_error_page') { + log(`${logLabel}:页面进入登录超时报错页,准备自动恢复后重试步骤 7。`, 'warn'); + return createStep6LoginTimeoutRecoverableResult( + 'login_timeout_error_page', + snapshot, + '登录验证码页面准备就绪前进入登录超时报错页。' + ); + } + if (snapshot.state === 'password_page' || snapshot.state === 'email_page') { + return createStep6RecoverableResult('verification_page_unstable', snapshot, { + message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 7。`, + loginVerificationRequestedAt, + }); + } + + return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, { + message: '登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 7。', + loginVerificationRequestedAt, + }); +} + function normalizeStep6Snapshot(snapshot) { if (snapshot?.state !== 'oauth_consent_page') { return snapshot; @@ -1999,7 +2079,15 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) { simulateClick(switchTrigger); log('步骤 7:已点击一次性验证码登录'); await sleep(1200); - return waitForStep6SwitchTransition(loginVerificationRequestedAt); + const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt); + if (result?.step6Outcome === 'success') { + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt, + via: result.via || 'switch_to_one_time_code_login', + }); + } + return result; } async function step6LoginFromPasswordPage(payload, snapshot) { @@ -2030,8 +2118,11 @@ async function step6LoginFromPasswordPage(payload, snapshot) { const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt); if (transition.action === 'done') { - log('步骤 7:已进入登录验证码页面。', 'ok'); - return transition.result; + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt, + via: transition.result.via || 'password_submit', + }); } if (transition.action === 'recoverable') { log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn'); @@ -2077,8 +2168,11 @@ async function step6LoginFromEmailPage(payload, snapshot) { const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt); if (transition.action === 'done') { - log('步骤 7:已进入登录验证码页面。', 'ok'); - return transition.result; + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt, + via: transition.result.via || 'email_submit', + }); } if (transition.action === 'recoverable') { log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn'); @@ -2102,8 +2196,11 @@ async function step6_login(payload) { const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page') { - log('步骤 7:登录验证码页面已就绪。', 'ok'); - return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' }); + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: null, + via: 'already_on_verification_page', + }); } if (snapshot.state === 'login_timeout_error_page') { diff --git a/sidepanel/mail-2925-manager.js b/sidepanel/mail-2925-manager.js index e7793b8..0c83b32 100644 --- a/sidepanel/mail-2925-manager.js +++ b/sidepanel/mail-2925-manager.js @@ -16,6 +16,7 @@ let actionInFlight = false; let listExpanded = false; let editingAccountId = ''; + let formVisible = false; function getMail2925Accounts(currentState = state.getLatestState()) { return helpers.getMail2925Accounts(currentState); @@ -118,13 +119,39 @@ if (dom.inputMail2925Password) dom.inputMail2925Password.value = ''; } + function syncMail2925FormUi() { + if (dom.mail2925FormShell) { + dom.mail2925FormShell.hidden = !formVisible; + } + if (dom.btnToggleMail2925Form) { + dom.btnToggleMail2925Form.textContent = formVisible ? '取消添加' : '添加账号'; + dom.btnToggleMail2925Form.setAttribute('aria-expanded', String(formVisible)); + } + } + + function setMail2925FormVisible(visible, options = {}) { + const { + clearForm = false, + focusField = false, + } = options; + + formVisible = Boolean(visible); + if (!formVisible && clearForm) { + stopEditingAccount({ clearForm: true }); + } else if (clearForm) { + clearMail2925Form(); + } + + syncMail2925FormUi(); + if (formVisible && focusField) { + dom.inputMail2925Email?.focus?.(); + } + } + function syncEditUi() { if (dom.btnAddMail2925Account) { dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号'; } - if (dom.btnCancelMail2925Edit) { - dom.btnCancelMail2925Edit.style.display = editingAccountId ? '' : 'none'; - } } function startEditingAccount(account) { @@ -132,6 +159,7 @@ editingAccountId = account.id; if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim(); if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || ''); + setMail2925FormVisible(true, { focusField: false }); syncEditUi(); } @@ -234,7 +262,7 @@ } applyMail2925AccountMutation(response.account); - stopEditingAccount(); + setMail2925FormVisible(false, { clearForm: true }); helpers.showToast( updatingExisting ? `已更新 2925 账号 ${email}` @@ -332,7 +360,7 @@ mail2925Accounts: [], currentMail2925AccountId: null, }); - stopEditingAccount(); + setMail2925FormVisible(false, { clearForm: true }); refreshManagedAliasBaseEmail(); renderMail2925Accounts(); helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200); @@ -460,7 +488,7 @@ } state.syncLatestState(nextState); if (editingAccountId === accountId) { - stopEditingAccount(); + setMail2925FormVisible(false, { clearForm: true }); } refreshManagedAliasBaseEmail(); renderMail2925Accounts(); @@ -479,6 +507,14 @@ setMail2925ListExpanded(!listExpanded); }); + dom.btnToggleMail2925Form?.addEventListener('click', () => { + if (formVisible) { + setMail2925FormVisible(false, { clearForm: true }); + return; + } + setMail2925FormVisible(true, { clearForm: !editingAccountId, focusField: true }); + }); + dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => { if (actionInFlight) return; actionInFlight = true; @@ -493,12 +529,10 @@ }); dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account); - dom.btnCancelMail2925Edit?.addEventListener('click', () => { - stopEditingAccount(); - }); dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts); dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick); syncEditUi(); + syncMail2925FormUi(); } return { diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 87558d9..575b9cd 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -1112,6 +1112,20 @@ header { visibility: hidden; } +.mail2925-form-shell { + display: flex; + flex-direction: column; + gap: 10px; +} + +.mail2925-actions-inline { + width: 100%; +} + +.mail2925-import-action { + margin-left: auto; +} + .hotmail-import-row { align-items: flex-start; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index b4f4081..1d71932 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -450,31 +450,33 @@ 2925 账号池
+
-
- 邮箱 - -
-
- 密码 - -
-
- -
- - + -
- 批量导入 -
- - +
+ 密码 + +
+
+ +
+ + +
+
+
+ 批量导入 +
+ +