From ccad06332e9c2cf5b54c009fb6f3cb348eec167e Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 26 Apr 2026 14:03:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E8=A7=A6=E5=8F=91=E5=99=A8=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/signup-page.js | 103 ++++++++++++++++++++++++++++++++ tests/step6-login-state.test.js | 13 ++++ 2 files changed, 116 insertions(+) diff --git a/content/signup-page.js b/content/signup-page.js index 9c6caf6..f36207b 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -114,6 +114,9 @@ const VERIFICATION_CODE_INPUT_SELECTOR = [ ].join(', '); const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i; +const LOGIN_ENTRY_ACTION_PATTERN = /(?:^|\b)(?:log\s*in|sign\s*in|continue\s+(?:with|using)\s+(?:email|chatgpt)|use\s+(?:an?\s+)?email|email\s+address)(?:\b|$)|登录|登陆|邮箱|电子邮件/i; +const LOGIN_EXTERNAL_IDP_PATTERN = /google|microsoft|apple|sso|single\s+sign[-\s]*on|企业|工作区|workspace/i; +const LOGIN_CODE_ONLY_ACTION_PATTERN = /one[-\s]*time|passcode|use\s+(?:a\s+)?code|验证码|一次性/i; const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i; @@ -1719,12 +1722,32 @@ function getLoginSubmitButton({ allowDisabled = false } = {}) { }) || null; } +function findLoginEntryTrigger() { + const candidates = Array.from(document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + )).filter((el) => isVisibleElement(el) && isActionEnabled(el)); + + const preferred = candidates.find((el) => { + const text = getActionText(el); + if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false; + return /continue\s+(?:with|using)\s+email|use\s+(?:an?\s+)?email|email\s+address|邮箱|电子邮件/i.test(text); + }); + if (preferred) return preferred; + + return candidates.find((el) => { + const text = getActionText(el); + if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false; + return LOGIN_ENTRY_ACTION_PATTERN.test(text); + }) || null; +} + function inspectLoginAuthState() { const retryState = getLoginTimeoutErrorPageState(); const verificationTarget = getVerificationCodeTarget(); const passwordInput = getLoginPasswordInput(); const emailInput = getLoginEmailInput(); const switchTrigger = findOneTimeCodeLoginTrigger(); + const loginEntryTrigger = findLoginEntryTrigger(); const submitButton = getLoginSubmitButton({ allowDisabled: true }); const verificationVisible = isVerificationPageStillVisible(); const addPhonePage = isAddPhonePageReady(); @@ -1746,6 +1769,7 @@ function inspectLoginAuthState() { emailInput, submitButton, switchTrigger, + loginEntryTrigger, verificationVisible, addPhonePage, phoneVerificationPage, @@ -1810,6 +1834,13 @@ function inspectLoginAuthState() { }; } + if (loginEntryTrigger) { + return { + ...baseState, + state: 'entry_page', + }; + } + return baseState; } @@ -1828,6 +1859,7 @@ function serializeLoginAuthState(snapshot) { hasEmailInput: Boolean(snapshot?.emailInput), hasSubmitButton: Boolean(snapshot?.submitButton), hasSwitchTrigger: Boolean(snapshot?.switchTrigger), + hasLoginEntryTrigger: Boolean(snapshot?.loginEntryTrigger), verificationVisible: Boolean(snapshot?.verificationVisible), addPhonePage: Boolean(snapshot?.addPhonePage), phoneVerificationPage: Boolean(snapshot?.phoneVerificationPage), @@ -1849,6 +1881,8 @@ function getLoginAuthStateLabel(snapshot) { return '登录超时报错页'; case 'oauth_consent_page': return 'OAuth 授权页'; + case 'entry_page': + return '登录入口页'; case 'add_phone_page': return '手机号页'; default: @@ -2816,6 +2850,71 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou return transition; } +async function waitForLoginEntryOpenTransition(timeout = 10000) { + const start = Date.now(); + let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + if (snapshot.state !== 'unknown' && snapshot.state !== 'entry_page') { + return snapshot; + } + await sleep(250); + } + + return snapshot; +} + +async function step6OpenLoginEntry(payload, snapshot) { + const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); + const trigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger(); + if (!trigger || !isActionEnabled(trigger)) { + return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, { + message: '当前登录入口页没有可点击的邮箱登录入口。', + }); + } + + log(`步骤 7:检测到登录入口页,正在点击 "${getActionText(trigger).slice(0, 80)}"...`); + await humanPause(350, 900); + simulateClick(trigger); + const nextSnapshot = await waitForLoginEntryOpenTransition(); + + if (nextSnapshot.state === 'email_page') { + return step6LoginFromEmailPage(payload, nextSnapshot); + } + if (nextSnapshot.state === 'password_page') { + return step6LoginFromPasswordPage(payload, nextSnapshot); + } + if (nextSnapshot.state === 'verification_page') { + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: null, + via: 'entry_open_verification_page', + }); + } + if (nextSnapshot.state === 'oauth_consent_page') { + return createStep6OAuthConsentSuccessResult(nextSnapshot, { + via: 'entry_open_oauth_consent_page', + }); + } + if (nextSnapshot.state === 'login_timeout_error_page') { + const transition = await createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_after_entry_open', + nextSnapshot, + '点击登录入口后进入登录超时报错页。' + ); + if (transition.action === 'done') return transition.result; + if (transition.action === 'email') return step6LoginFromEmailPage(payload, transition.snapshot); + if (transition.action === 'password') return step6LoginFromPasswordPage(payload, transition.snapshot); + return transition.result; + } + + return createStep6RecoverableResult('login_entry_open_stalled', nextSnapshot, { + message: '点击登录入口后仍未进入邮箱/密码/验证码页。', + }); +} + async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger(); if (!switchTrigger || !isActionEnabled(switchTrigger)) { @@ -3024,6 +3123,10 @@ async function step6_login(payload) { return step6LoginFromPasswordPage(payload, snapshot); } + if (snapshot.state === 'entry_page') { + return step6OpenLoginEntry(payload, snapshot); + } + throwForStep6FatalState(snapshot); throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); } diff --git a/tests/step6-login-state.test.js b/tests/step6-login-state.test.js index f9f2f95..cc640de 100644 --- a/tests/step6-login-state.test.js +++ b/tests/step6-login-state.test.js @@ -96,6 +96,10 @@ function findOneTimeCodeLoginTrigger() { return ${JSON.stringify(overrides.switchTrigger || null)}; } +function findLoginEntryTrigger() { + return ${JSON.stringify(overrides.loginEntryTrigger || null)}; +} + function getLoginSubmitButton() { return ${JSON.stringify(overrides.submitButton || null)}; } @@ -226,6 +230,15 @@ return { assert.strictEqual(snapshot.state, 'oauth_consent_page', '第六步应保留 oauth_consent_page 状态'); } +{ + const api = createApi({ + loginEntryTrigger: { id: 'continue-email' }, + }); + + const snapshot = api.inspectLoginAuthState(); + assert.strictEqual(snapshot.state, 'entry_page'); +} + assert.ok( extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"), 'inspectLoginAuthState 应产出 oauth_consent_page 状态'