feat: Enhance login flow with add-email and phone verification support

- Added support for the add-email page in the logging status module.
- Updated step 8 to handle phone-registered accounts during email verification.
- Implemented logic to submit add-email before polling for email verification codes.
- Enhanced tests for step 7 to detect username input as phone number and ignore hidden inputs.
- Improved step 8 to allow add-email handoff only when requested.
- Updated documentation to reflect changes in the login verification flow.
This commit is contained in:
QLHazyCoder
2026-05-05 01:47:58 +08:00
parent 6b3917d4b7
commit d054ff65de
11 changed files with 1323 additions and 176 deletions
+217 -29
View File
@@ -2,6 +2,8 @@
root.MultiPageBackgroundStep8 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
const STEP8_ADD_EMAIL_URL = 'https://auth.openai.com/add-email';
const STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS = 3;
function createStep8Executor(deps = {}) {
const {
@@ -22,11 +24,12 @@
isTabAlive,
isVerificationMailPollingError,
LUCKMAIL_PROVIDER,
resolveSignupEmailForFlow,
resolveVerificationStep,
rerunStep7ForStep8Recovery,
reuseOrCreateTab,
sendToContentScriptResilient,
phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
setState,
shouldUseCustomRegistrationEmail,
sleepWithStop,
@@ -99,11 +102,98 @@
return String(value || '').trim().toLowerCase();
}
function isPhoneLoginState(state = {}) {
return String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|| resolveSignupMethod(state) === 'phone'
|| Boolean(state?.signupPhoneCompletedActivation)
|| Boolean(state?.signupPhoneActivation);
async function getLoginAuthStateFromContent(visibleStep, options = {}) {
if (typeof sendToContentScriptResilient !== 'function') {
return {};
}
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 15000);
const result = await sendToContentScriptResilient(
'signup-page',
{
type: 'GET_LOGIN_AUTH_STATE',
source: 'background',
payload: {},
},
{
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 600,
logMessage: options.logMessage || `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪...`,
logStep: visibleStep,
logStepKey: 'fetch-login-code',
}
);
if (result?.error) {
throw new Error(result.error);
}
return result || {};
}
async function submitAddEmailIfNeeded(state, visibleStep, initialPageState = null) {
if (typeof resolveSignupEmailForFlow !== 'function' || typeof sendToContentScriptResilient !== 'function') {
return { state, pageState: initialPageState };
}
const pageState = initialPageState?.state
? initialPageState
: await getLoginAuthStateFromContent(visibleStep, {
timeoutMs: 15000,
logMessage: `步骤 ${visibleStep}:正在确认是否已进入添加邮箱页...`,
});
if (pageState?.state !== 'add_email_page') {
return { state, pageState };
}
const latestState = typeof getState === 'function' ? await getState() : state;
const resolvedEmail = await resolveSignupEmailForFlow(latestState);
await addLog(`步骤 ${visibleStep}:检测到添加邮箱页,正在添加邮箱 ${resolvedEmail} 并进入邮箱验证码页...`);
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(60000, {
step: visibleStep,
actionLabel: '添加邮箱并进入验证码页',
oauthUrl: latestState?.oauthUrl || state?.oauthUrl || '',
})
: 60000;
const result = await sendToContentScriptResilient(
'signup-page',
{
type: 'SUBMIT_ADD_EMAIL',
source: 'background',
payload: { email: resolvedEmail },
},
{
timeoutMs,
responseTimeoutMs: timeoutMs,
retryDelayMs: 700,
logMessage: `步骤 ${visibleStep}:添加邮箱页面正在切换,等待邮箱验证码页就绪...`,
logStep: visibleStep,
logStepKey: 'fetch-login-code',
}
);
if (result?.error) {
throw new Error(result.error);
}
const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail);
await setState({
email: resolvedEmail,
step8VerificationTargetEmail: displayedEmail,
});
return {
state: {
...latestState,
email: resolvedEmail,
step8VerificationTargetEmail: displayedEmail,
},
pageState: {
state: result?.directOAuthConsentPage ? 'oauth_consent_page' : 'verification_page',
displayedEmail,
url: result?.url || pageState?.url || '',
},
};
}
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
@@ -130,12 +220,69 @@
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
}
function isStep8EmailInUseError(error) {
const message = String(error?.message || error || '');
return /STEP8_EMAIL_IN_USE::|email_in_use|email\s+(?:address\s+)?already\s+exists|already\s+associated\s+with\s+this\s+email/i.test(message);
}
function isStep8MaxCheckAttemptsError(error) {
const message = String(error?.message || error || '');
return /AUTH_MAX_CHECK_ATTEMPTS::|max_check_attempts/i.test(message);
}
async function openStep8AddEmailPage(state, visibleStep, reasonLabel = '') {
const tabId = typeof getTabId === 'function' ? await getTabId('signup-page') : 0;
const url = STEP8_ADD_EMAIL_URL;
if (tabId && chrome?.tabs?.update) {
await chrome.tabs.update(tabId, { url, active: true });
} else if (typeof reuseOrCreateTab === 'function') {
await reuseOrCreateTab('signup-page', url);
} else {
throw new Error(`Step ${visibleStep}: cannot reopen add-email page for Step 8 recovery.`);
}
if (typeof sleepWithStop === 'function') {
await sleepWithStop(1000);
}
await addLog(
`步骤 ${visibleStep}:重新打开添加邮箱页面${reasonLabel ? `${reasonLabel}` : ''}`,
'warn'
);
return {
...(state || {}),
oauthUrl: state?.oauthUrl || url,
};
}
async function resetStep8AfterEmailInUse(state, visibleStep) {
const currentEmail = String(state?.email || '').trim();
await setState({
email: null,
step8VerificationTargetEmail: '',
loginVerificationRequestedAt: null,
});
if (currentEmail) {
await addLog(`步骤 ${visibleStep}:检测到邮箱 ${currentEmail} 已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn');
} else {
await addLog(`步骤 ${visibleStep}:检测到邮箱已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn');
}
}
async function resetStep8AfterMaxCheckAttempts(visibleStep) {
await setState({
step8VerificationTargetEmail: '',
loginVerificationRequestedAt: null,
});
await addLog(`步骤 ${visibleStep}:检测到 max_check_attempts,将重新开始当前添加邮箱步骤,不继续点击重试。`, 'warn');
}
async function recoverStep8PollingFailure(currentState, visibleStep) {
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
try {
const pageState = await ensureStep8VerificationPageReady({
visibleStep,
authLoginStep,
allowPhoneVerificationPage: true,
allowAddEmailPage: true,
timeoutMs: await getStep8ReadyTimeoutMs(
'登录验证码轮询异常后复核认证页状态',
currentState?.oauthUrl || '',
@@ -146,9 +293,9 @@
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
return { outcome: 'completed' };
}
if (pageState?.state === 'verification_page') {
if (pageState?.state === 'verification_page' || pageState?.state === 'phone_verification_page' || pageState?.state === 'add_email_page') {
await addLog(
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在验证码页,先在当前链路重试,不回到步骤 ${authLoginStep}`,
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在当前登录后续页面,先在当前链路重试,不回到步骤 ${authLoginStep}`,
'warn'
);
return { outcome: 'retry_without_step7' };
@@ -250,40 +397,58 @@
await reuseOrCreateTab('signup-page', state.oauthUrl);
}
if (isPhoneLoginState(state)) {
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
}
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
? runtime.onResendRequestedAt
: null;
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `8:${stepStartedAt}`;
throwIfStopped();
const pageState = await ensureStep8VerificationPageReady({
let pageState = await ensureStep8VerificationPageReady({
visibleStep,
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
allowPhoneVerificationPage: true,
allowAddEmailPage: true,
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
});
if (pageState?.state === 'oauth_consent_page') {
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
return;
}
if (pageState?.state === 'phone_verification_page') {
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
}
let preparedState = state;
const addEmailPreparation = await submitAddEmailIfNeeded(preparedState, visibleStep, pageState);
preparedState = addEmailPreparation?.state || preparedState;
pageState = addEmailPreparation?.pageState || pageState;
if (pageState?.state === 'oauth_consent_page') {
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
return;
}
if (pageState?.state === 'phone_verification_page') {
return executeLoginPhoneCodeStep(preparedState, authTabId, visibleStep);
}
const preparedStateLastResendAt = Number(preparedState?.loginVerificationRequestedAt) || 0;
if (preparedStateLastResendAt > 0) {
latestResendAt = Math.max(latestResendAt, preparedStateLastResendAt);
}
const mail = getMailConfig(preparedState);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
const verificationSessionKey = `8:${stepStartedAt}`;
const shouldCompareVerificationEmail = mail.provider !== '2925';
const displayedVerificationEmail = shouldCompareVerificationEmail
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
: '';
const fixedTargetEmail = shouldCompareVerificationEmail
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(state?.email))
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.email))
: '';
await setState({
@@ -295,7 +460,7 @@
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
}
if (shouldUseCustomRegistrationEmail(state)) {
if (shouldUseCustomRegistrationEmail(preparedState)) {
await confirmCustomVerificationStepBypass(8, {
completionStep: visibleStep,
promptStep: visibleStep,
@@ -306,7 +471,7 @@
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
await ensureIcloudMailSession({
state,
state: preparedState,
step: 8,
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
});
@@ -323,10 +488,10 @@
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
accountId: preparedState.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
allowLoginWhenOnLoginPage: Boolean(preparedState?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(preparedState),
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
});
} else {
@@ -338,14 +503,14 @@
}
await resolveVerificationStep(8, {
...state,
...preparedState,
step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, {
completionStep: visibleStep,
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
getRemainingTimeMs: getStep8RemainingTimeResolver(preparedState?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false,
lastResendAt: latestResendAt,
onResendRequestedAt: async (requestedAt) => {
@@ -381,6 +546,7 @@
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let retryWithoutStep7Streak = 0;
const maxRetryWithoutStep7Streak = 3;
let currentStepRecoveryAttempt = 0;
while (true) {
try {
@@ -403,6 +569,28 @@
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
let currentError = err;
let retryWithoutStep7 = false;
if (isStep8EmailInUseError(currentError) || isStep8MaxCheckAttemptsError(currentError)) {
currentStepRecoveryAttempt += 1;
if (currentStepRecoveryAttempt > STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS) {
throw currentError;
}
if (isStep8EmailInUseError(currentError)) {
await resetStep8AfterEmailInUse(currentState, visibleStep);
await openStep8AddEmailPage(currentState, visibleStep, 'email_in_use');
} else {
await resetStep8AfterMaxCheckAttempts(visibleStep);
await openStep8AddEmailPage(currentState, visibleStep, 'max_check_attempts');
}
const latestState = typeof getState === 'function' ? await getState() : currentState;
currentState = {
...(currentState || {}),
...(latestState || {}),
oauthUrl: currentState?.oauthUrl || latestState?.oauthUrl || STEP8_ADD_EMAIL_URL,
};
continue;
}
const isMailPollingError = isVerificationMailPollingError(err);
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);