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
+15 -2
View File
@@ -7198,6 +7198,7 @@ function getLoginAuthStateLabel(state) {
case 'login_timeout_error_page': return '登录超时报错页'; case 'login_timeout_error_page': return '登录超时报错页';
case 'oauth_consent_page': return 'OAuth 授权页'; case 'oauth_consent_page': return 'OAuth 授权页';
case 'add_phone_page': return '手机号页'; case 'add_phone_page': return '手机号页';
case 'add_email_page': return '添加邮箱页';
default: return '未知页面'; default: return '未知页面';
} }
} }
@@ -10345,10 +10346,12 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
isVerificationMailPollingError, isVerificationMailPollingError,
LUCKMAIL_PROVIDER, LUCKMAIL_PROVIDER,
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
resolveSignupEmailForFlow,
phoneVerificationHelpers, phoneVerificationHelpers,
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args), rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
resolveSignupMethod, resolveSignupMethod,
reuseOrCreateTab, reuseOrCreateTab,
sendToContentScriptResilient,
setState, setState,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
sleepWithStop, sleepWithStop,
@@ -11305,7 +11308,12 @@ async function ensureStep8VerificationPageReady(options = {}) {
...overrides, ...overrides,
}); });
let pageState = await inspectState(); let pageState = await inspectState();
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') { if (
pageState.state === 'verification_page'
|| pageState.state === 'oauth_consent_page'
|| (options.allowPhoneVerificationPage && pageState.state === 'phone_verification_page')
|| (options.allowAddEmailPage && pageState.state === 'add_email_page')
) {
return pageState; return pageState;
} }
@@ -11379,7 +11387,12 @@ async function ensureStep8VerificationPageReady(options = {}) {
logMessage: '认证页恢复后,正在确认验证码页是否可继续...', logMessage: '认证页恢复后,正在确认验证码页是否可继续...',
logStepKey: 'fetch-login-code', logStepKey: 'fetch-login-code',
}); });
if (pageState.state === 'verification_page' || pageState.state === 'oauth_consent_page') { if (
pageState.state === 'verification_page'
|| pageState.state === 'oauth_consent_page'
|| (options.allowPhoneVerificationPage && pageState.state === 'phone_verification_page')
|| (options.allowAddEmailPage && pageState.state === 'add_email_page')
) {
return pageState; return pageState;
} }
if (pageState.maxCheckAttemptsBlocked) { if (pageState.maxCheckAttemptsBlocked) {
+2
View File
@@ -101,6 +101,8 @@
return 'OAuth 授权页'; return 'OAuth 授权页';
case 'add_phone_page': case 'add_phone_page':
return '手机号页'; return '手机号页';
case 'add_email_page':
return '添加邮箱页';
case 'phone_verification_page': case 'phone_verification_page':
return '手机验证码页'; return '手机验证码页';
default: default:
+217 -29
View File
@@ -2,6 +2,8 @@
root.MultiPageBackgroundStep8 = factory(); root.MultiPageBackgroundStep8 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() { })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; 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 = {}) { function createStep8Executor(deps = {}) {
const { const {
@@ -22,11 +24,12 @@
isTabAlive, isTabAlive,
isVerificationMailPollingError, isVerificationMailPollingError,
LUCKMAIL_PROVIDER, LUCKMAIL_PROVIDER,
resolveSignupEmailForFlow,
resolveVerificationStep, resolveVerificationStep,
rerunStep7ForStep8Recovery, rerunStep7ForStep8Recovery,
reuseOrCreateTab, reuseOrCreateTab,
sendToContentScriptResilient,
phoneVerificationHelpers = null, phoneVerificationHelpers = null,
resolveSignupMethod = () => 'email',
setState, setState,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
sleepWithStop, sleepWithStop,
@@ -99,11 +102,98 @@
return String(value || '').trim().toLowerCase(); return String(value || '').trim().toLowerCase();
} }
function isPhoneLoginState(state = {}) { async function getLoginAuthStateFromContent(visibleStep, options = {}) {
return String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone' if (typeof sendToContentScriptResilient !== 'function') {
|| resolveSignupMethod(state) === 'phone' return {};
|| Boolean(state?.signupPhoneCompletedActivation) }
|| Boolean(state?.signupPhoneActivation); 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 = {}) { async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
@@ -130,12 +220,69 @@
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message); 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) { async function recoverStep8PollingFailure(currentState, visibleStep) {
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep); const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
try { try {
const pageState = await ensureStep8VerificationPageReady({ const pageState = await ensureStep8VerificationPageReady({
visibleStep, visibleStep,
authLoginStep, authLoginStep,
allowPhoneVerificationPage: true,
allowAddEmailPage: true,
timeoutMs: await getStep8ReadyTimeoutMs( timeoutMs: await getStep8ReadyTimeoutMs(
'登录验证码轮询异常后复核认证页状态', '登录验证码轮询异常后复核认证页状态',
currentState?.oauthUrl || '', currentState?.oauthUrl || '',
@@ -146,9 +293,9 @@
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true }); await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
return { outcome: 'completed' }; 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( await addLog(
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在验证码页,先在当前链路重试,不回到步骤 ${authLoginStep}`, `步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在当前登录后续页面,先在当前链路重试,不回到步骤 ${authLoginStep}`,
'warn' 'warn'
); );
return { outcome: 'retry_without_step7' }; return { outcome: 'retry_without_step7' };
@@ -250,40 +397,58 @@
await reuseOrCreateTab('signup-page', state.oauthUrl); 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; const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt); let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function' const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
? runtime.onResendRequestedAt ? runtime.onResendRequestedAt
: null; : 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(); throwIfStopped();
const pageState = await ensureStep8VerificationPageReady({ let pageState = await ensureStep8VerificationPageReady({
visibleStep, visibleStep,
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep), authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
allowPhoneVerificationPage: true,
allowAddEmailPage: true,
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep), timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
}); });
if (pageState?.state === 'oauth_consent_page') { if (pageState?.state === 'oauth_consent_page') {
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep); await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
return; 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 shouldCompareVerificationEmail = mail.provider !== '2925';
const displayedVerificationEmail = shouldCompareVerificationEmail const displayedVerificationEmail = shouldCompareVerificationEmail
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail) ? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
: ''; : '';
const fixedTargetEmail = shouldCompareVerificationEmail const fixedTargetEmail = shouldCompareVerificationEmail
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(state?.email)) ? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.email))
: ''; : '';
await setState({ await setState({
@@ -295,7 +460,7 @@
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info'); await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
} }
if (shouldUseCustomRegistrationEmail(state)) { if (shouldUseCustomRegistrationEmail(preparedState)) {
await confirmCustomVerificationStepBypass(8, { await confirmCustomVerificationStepBypass(8, {
completionStep: visibleStep, completionStep: visibleStep,
promptStep: visibleStep, promptStep: visibleStep,
@@ -306,7 +471,7 @@
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info'); await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
await ensureIcloudMailSession({ await ensureIcloudMailSession({
state, state: preparedState,
step: 8, step: 8,
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`, actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
}); });
@@ -323,10 +488,10 @@
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`); await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') { if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({ await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null, accountId: preparedState.currentMail2925AccountId || null,
forceRelogin: false, forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), allowLoginWhenOnLoginPage: Boolean(preparedState?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), expectedMailboxEmail: getExpectedMail2925MailboxEmail(preparedState),
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`, actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
}); });
} else { } else {
@@ -338,14 +503,14 @@
} }
await resolveVerificationStep(8, { await resolveVerificationStep(8, {
...state, ...preparedState,
step8VerificationTargetEmail: displayedVerificationEmail || '', step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, { }, mail, {
completionStep: visibleStep, completionStep: visibleStep,
filterAfterTimestamp: verificationFilterAfterTimestamp, filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey, sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925', disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep), getRemainingTimeMs: getStep8RemainingTimeResolver(preparedState?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false, requestFreshCodeFirst: false,
lastResendAt: latestResendAt, lastResendAt: latestResendAt,
onResendRequestedAt: async (requestedAt) => { onResendRequestedAt: async (requestedAt) => {
@@ -381,6 +546,7 @@
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0; let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let retryWithoutStep7Streak = 0; let retryWithoutStep7Streak = 0;
const maxRetryWithoutStep7Streak = 3; const maxRetryWithoutStep7Streak = 3;
let currentStepRecoveryAttempt = 0;
while (true) { while (true) {
try { try {
@@ -403,6 +569,28 @@
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep); const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
let currentError = err; let currentError = err;
let retryWithoutStep7 = false; 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); const isMailPollingError = isVerificationMailPollingError(err);
if (isMailPollingError && !isStep8RestartStep7Error(err)) { if (isMailPollingError && !isStep8RestartStep7Error(err)) {
const recovery = await recoverStep8PollingFailure(currentState, visibleStep); const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
+729 -108
View File
File diff suppressed because it is too large Load Diff
@@ -44,5 +44,6 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
true true
); );
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页'); assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
assert.equal(loggingStatus.getLoginAuthStateLabel('add_email_page'), '添加邮箱页');
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页'); assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
}); });
+175 -11
View File
@@ -85,16 +85,23 @@ test('step 8 submits login verification directly without replaying step 7', asyn
{ step8VerificationTargetEmail: 'display.user@example.com' }, { step8VerificationTargetEmail: 'display.user@example.com' },
]); ]);
assert.deepStrictEqual(calls.ensureReadyOptions, [ assert.deepStrictEqual(calls.ensureReadyOptions, [
{ visibleStep: 8, authLoginStep: 7, timeoutMs: 5000 }, {
visibleStep: 8,
authLoginStep: 7,
allowPhoneVerificationPage: true,
allowAddEmailPage: true,
timeoutMs: 5000,
},
]); ]);
assert.equal(calls.resolveOptions.completionStep, 8); assert.equal(calls.resolveOptions.completionStep, 8);
}); });
test('step 8 routes phone login verification through sms helper and skips mail provider setup', async () => { test('step 8 keeps phone-registered accounts on email-code flow when page is email verification', async () => {
const calls = { const calls = {
getMailConfigCalls: 0, getMailConfigCalls: 0,
helperCalls: [], helperCalls: [],
completions: [], completions: [],
resolveCalls: 0,
}; };
const executor = api.createStep8Executor({ const executor = api.createStep8Executor({
@@ -109,9 +116,7 @@ test('step 8 routes phone login verification through sms helper and skips mail p
calls.completions.push({ step, payload }); calls.completions.push({ step, payload });
}, },
confirmCustomVerificationStepBypass: async () => {}, confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => { ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'phone-user@example.com' }),
throw new Error('phone login branch should not probe email verification page readiness');
},
getOAuthFlowRemainingMs: async () => 5000, getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getMailConfig: () => { getMailConfig: () => {
@@ -141,7 +146,7 @@ test('step 8 routes phone login verification through sms helper and skips mail p
}, },
resolveSignupMethod: () => 'phone', resolveSignupMethod: () => 'phone',
resolveVerificationStep: async () => { resolveVerificationStep: async () => {
throw new Error('phone login branch should not call email verification flow'); calls.resolveCalls += 1;
}, },
rerunStep7ForStep8Recovery: async () => { rerunStep7ForStep8Recovery: async () => {
throw new Error('phone login branch should not rerun step 7 in this test'); throw new Error('phone login branch should not rerun step 7 in this test');
@@ -164,6 +169,73 @@ test('step 8 routes phone login verification through sms helper and skips mail p
oauthUrl: 'https://oauth.example/latest', oauthUrl: 'https://oauth.example/latest',
}); });
assert.equal(calls.getMailConfigCalls, 1);
assert.equal(calls.resolveCalls, 1);
assert.deepStrictEqual(calls.helperCalls, []);
assert.deepStrictEqual(calls.completions, []);
});
test('step 8 routes only a real phone verification page through sms helper', async () => {
const calls = {
getMailConfigCalls: 0,
helperCalls: [],
completions: [],
};
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
calls.completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'phone_verification_page' }),
getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getMailConfig: () => {
calls.getMailConfigCalls += 1;
return {
provider: 'qq',
label: 'QQ 邮箱',
};
},
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
phoneVerificationHelpers: {
completeLoginPhoneVerificationFlow: async (tabId, options) => {
calls.helperCalls.push({ tabId, visibleStep: options.visibleStep, state: options.state });
return { code: '654321' };
},
},
resolveVerificationStep: async () => {
throw new Error('real phone verification branch should not call email verification flow');
},
rerunStep7ForStep8Recovery: async () => {
throw new Error('real phone verification branch should not rerun step 7 in this test');
},
reuseOrCreateTab: async () => {},
setState: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
visibleStep: 8,
accountIdentifierType: 'phone',
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(calls.getMailConfigCalls, 0); assert.equal(calls.getMailConfigCalls, 0);
assert.deepStrictEqual(calls.helperCalls, [ assert.deepStrictEqual(calls.helperCalls, [
{ {
@@ -172,10 +244,6 @@ test('step 8 routes phone login verification through sms helper and skips mail p
state: { state: {
visibleStep: 8, visibleStep: 8,
accountIdentifierType: 'phone', accountIdentifierType: 'phone',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
},
oauthUrl: 'https://oauth.example/latest', oauthUrl: 'https://oauth.example/latest',
}, },
}, },
@@ -192,6 +260,96 @@ test('step 8 routes phone login verification through sms helper and skips mail p
]); ]);
}); });
test('step 8 submits add-email before polling the email verification code', async () => {
const calls = {
contentMessages: [],
resolvedStates: [],
setStates: [],
mailStates: [],
};
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'add_email_page', url: 'https://auth.openai.com/add-email' }),
getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getMailConfig: (state) => {
calls.mailStates.push(state);
return {
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
};
},
getState: async () => ({
email: '',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
}),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveSignupEmailForFlow: async (state) => {
calls.resolvedStates.push(state);
return 'new.user@example.com';
},
resolveVerificationStep: async (_step, state, _mail, options) => {
calls.resolvedVerification = { state, options };
},
rerunStep7ForStep8Recovery: async () => {},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async (_source, message) => {
calls.contentMessages.push(message);
assert.equal(message.type, 'SUBMIT_ADD_EMAIL');
assert.equal(message.payload.email, 'new.user@example.com');
return {
submitted: true,
displayedEmail: 'new.user@example.com',
url: 'https://auth.openai.com/email-verification',
};
},
setState: async (payload) => {
calls.setStates.push(payload);
},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
visibleStep: 8,
accountIdentifierType: 'phone',
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(calls.contentMessages.length, 1);
assert.equal(calls.resolvedStates.length, 1);
assert.equal(calls.mailStates[0].email, 'new.user@example.com');
assert.equal(calls.resolvedVerification.state.email, 'new.user@example.com');
assert.equal(calls.resolvedVerification.options.targetEmail, 'new.user@example.com');
assert.deepStrictEqual(calls.setStates, [
{
email: 'new.user@example.com',
step8VerificationTargetEmail: 'new.user@example.com',
},
{
step8VerificationTargetEmail: 'new.user@example.com',
},
]);
});
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => { test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
let resolvedStep = null; let resolvedStep = null;
let resolvedOptions = null; let resolvedOptions = null;
@@ -257,7 +415,13 @@ test('Plus login-code step reuses step 8 verification logic but completes visibl
assert.equal(resolvedStep, 8); assert.equal(resolvedStep, 8);
assert.equal(resolvedOptions.completionStep, 11); assert.equal(resolvedOptions.completionStep, 11);
assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com'); assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com');
assert.deepStrictEqual(readyOptions, { visibleStep: 11, authLoginStep: 10, timeoutMs: 9000 }); assert.deepStrictEqual(readyOptions, {
visibleStep: 11,
authLoginStep: 10,
allowPhoneVerificationPage: true,
allowAddEmailPage: true,
timeoutMs: 9000,
});
assert.deepStrictEqual(remainingStepCalls, [11, 11]); assert.deepStrictEqual(remainingStepCalls, [11, 11]);
}); });
+23
View File
@@ -124,6 +124,10 @@ function isAddPhonePageReady() {
return ${JSON.stringify(Boolean(overrides.addPhonePage))}; return ${JSON.stringify(Boolean(overrides.addPhonePage))};
} }
function isAddEmailPageReady() {
return ${JSON.stringify(Boolean(overrides.addEmailPage))};
}
function isVisibleElement() { function isVisibleElement() {
return true; return true;
} }
@@ -261,6 +265,20 @@ return {
assert.strictEqual(snapshot.state, 'phone_entry_page'); assert.strictEqual(snapshot.state, 'phone_entry_page');
} }
{
const api = createApi({
pathname: '/add-email',
href: 'https://auth.openai.com/add-email',
emailInput: { id: 'email' },
submitButton: { id: 'submit' },
addEmailPage: true,
});
const snapshot = api.inspectLoginAuthState();
assert.strictEqual(snapshot.state, 'add_email_page');
assert.strictEqual(snapshot.addEmailPage, true);
}
assert.ok( assert.ok(
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"), extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
'inspectLoginAuthState 应产出 oauth_consent_page 状态' 'inspectLoginAuthState 应产出 oauth_consent_page 状态'
@@ -271,4 +289,9 @@ assert.ok(
'inspectLoginAuthState 应产出 phone_entry_page 状态' 'inspectLoginAuthState 应产出 phone_entry_page 状态'
); );
assert.ok(
extractFunction('inspectLoginAuthState').includes("state: 'add_email_page'"),
'inspectLoginAuthState 应产出 add_email_page 状态'
);
console.log('step6 login state tests passed'); console.log('step6 login state tests passed');
+109 -8
View File
@@ -68,13 +68,15 @@ function createPhoneLoginEntryApi(options = {}) {
inputRootText = '', inputRootText = '',
pageText = '', pageText = '',
addPhoneForm = false, addPhoneForm = false,
phoneUsernameKind = false,
} = options; } = options;
return new Function(` return new Function(`
${extractConst('ADD_PHONE_PAGE_PATTERN')} ${extractConst('ADD_PHONE_PAGE_PATTERN')}
${extractConst('LOGIN_PHONE_ENTRY_PAGE_PATTERN')}
const location = { const location = {
href: ${JSON.stringify(href)}, href: ${JSON.stringify(phoneUsernameKind ? `${href}${href.includes('?') ? '&' : '?'}usernameKind=phone_number` : href)},
pathname: ${JSON.stringify(pathname)}, pathname: ${JSON.stringify(pathname)},
}; };
@@ -107,7 +109,7 @@ const document = {
return null; return null;
}, },
querySelectorAll(selector) { querySelectorAll(selector) {
if (selector === 'input') return [phoneInput]; if (String(selector || '').includes('input')) return [phoneInput];
return []; return [];
}, },
getElementById() { getElementById() {
@@ -125,7 +127,18 @@ function isVisibleElement(element) {
return Boolean(element); return Boolean(element);
} }
function isPhoneVerificationPageReady() {
return false;
}
${extractFunction('getPageTextSnapshot')} ${extractFunction('getPageTextSnapshot')}
${extractFunction('isLoginPhoneUsernameKind')}
${extractFunction('isLoginPhoneEntryPageText')}
${extractFunction('isInsideHiddenPhoneControl')}
${extractFunction('summarizePhoneInputCandidate')}
${extractFunction('isUsablePhoneInputElement')}
${extractFunction('collectPhoneInputCandidates')}
${extractFunction('findUsablePhoneInput')}
${extractFunction('getLoginPhoneInput')} ${extractFunction('getLoginPhoneInput')}
${extractFunction('isAddPhonePageReady')} ${extractFunction('isAddPhonePageReady')}
@@ -155,6 +168,26 @@ test('step 7 does not mistake email entry with a phone switch action for phone i
assert.equal(api.isAddPhonePageReady(), false); assert.equal(api.isAddPhonePageReady(), false);
}); });
test('step 7 detects username text input when usernameKind is phone_number', () => {
const api = createPhoneLoginEntryApi({
phoneUsernameKind: true,
pageText: '\u6b22\u8fce\u56de\u6765',
inputAttributes: { type: 'text', name: 'username', autocomplete: 'username' },
});
assert.ok(api.getLoginPhoneInput(), 'username text input should be treated as phone on phone login url');
});
test('step 7 ignores hidden phone inputs while resolving login phone entry', () => {
const api = createPhoneLoginEntryApi({
phoneUsernameKind: true,
pageText: '\u7535\u8bdd\u53f7\u7801',
inputAttributes: { type: 'hidden', name: 'phone' },
});
assert.equal(api.getLoginPhoneInput(), null);
});
test('add-phone detection stays true for real add-phone urls and forms', () => { test('add-phone detection stays true for real add-phone urls and forms', () => {
assert.equal( assert.equal(
createPhoneLoginEntryApi({ createPhoneLoginEntryApi({
@@ -312,29 +345,73 @@ return {
assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\u82f1\u56fd +(44)']); assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\u82f1\u56fd +(44)']);
}); });
function createPhoneFillApi(fillBehavior) { function createPhoneFillApi(fillBehavior, options = {}) {
const {
initialValue = '+44',
} = options;
return new Function('fillBehavior', ` return new Function('fillBehavior', `
const fills = []; const fills = [];
const phoneInput = { const phoneInput = {
value: '+44', value: ${JSON.stringify(initialValue)},
form: null,
getAttribute(name) { getAttribute(name) {
return name === 'value' ? this.value : ''; return name === 'value' ? this.value : '';
}, },
focus() {},
closest() {
return this.form;
},
}; };
const hiddenPhoneInput = {
type: 'hidden',
value: '',
events: [],
getAttribute(name) {
if (name === 'type') return 'hidden';
if (name === 'name') return 'phone';
return '';
},
dispatchEvent(event) {
this.events.push(event.type);
},
};
const root = {
querySelectorAll(selector) {
if (String(selector).includes('input[name="phone"]')) {
return [hiddenPhoneInput];
}
return [];
},
};
phoneInput.form = root;
function fillInput(input, value) { function fillInput(input, value) {
fills.push(value); fills.push(value);
fillBehavior(input, value); fillBehavior(input, value);
} }
async function sleep() {} async function sleep() {}
function throwIfStopped() {}
function isVisibleElement() { return false; }
function log() {}
${extractFunction('normalizePhoneDigits')} ${extractFunction('normalizePhoneDigits')}
${extractFunction('toNationalPhoneNumber')} ${extractFunction('toNationalPhoneNumber')}
${extractFunction('toE164PhoneNumber')} ${extractFunction('toE164PhoneNumber')}
${extractFunction('getPhoneInputRenderedValue')} ${extractFunction('getPhoneInputRenderedValue')}
${extractFunction('isPhoneInputValueVerified')}
${extractFunction('waitForPhoneInputValue')}
${extractFunction('formatPhoneHiddenFormValue')}
${extractFunction('getPhoneHiddenValueInput')}
${extractFunction('setPhoneHiddenValue')}
${extractFunction('syncPhoneHiddenFormValue')}
${extractFunction('isPhoneInputValueComplete')} ${extractFunction('isPhoneInputValueComplete')}
${extractFunction('getLoginPhoneFillCandidates')} ${extractFunction('getLoginPhoneFillCandidates')}
${extractFunction('getLoginPhoneInputDiagnostics')}
${extractFunction('getLoginPhoneHiddenValueDiagnostics')}
${extractFunction('fillLoginPhoneInputAndConfirm')} ${extractFunction('fillLoginPhoneInputAndConfirm')}
return { return {
@@ -343,6 +420,7 @@ return {
phoneNumber: '447780579093', phoneNumber: '447780579093',
dialCode: '44', dialCode: '44',
visibleStep: 7, visibleStep: 7,
maxAttempts: 2,
}); });
}, },
getFills() { getFills() {
@@ -351,13 +429,19 @@ return {
getValue() { getValue() {
return phoneInput.value; return phoneInput.value;
}, },
getHiddenValue() {
return hiddenPhoneInput.value;
},
getHiddenEvents() {
return hiddenPhoneInput.events.slice();
},
}; };
`)(fillBehavior); `)(fillBehavior);
} }
test('step 7 retries phone fill with e164 when the auth input keeps only the dial code', async () => { test('step 7 keeps visible dial prefix when filling phone login and syncs the hidden value', async () => {
const api = createPhoneFillApi((input, value) => { const api = createPhoneFillApi((input, value) => {
input.value = value === '7780579093' ? '+44' : value; input.value = value;
}); });
const result = await api.run(); const result = await api.run();
@@ -365,7 +449,24 @@ test('step 7 retries phone fill with e164 when the auth input keeps only the dia
assert.equal(result.inputValue, '7780579093'); assert.equal(result.inputValue, '7780579093');
assert.equal(result.attemptedValue, '+447780579093'); assert.equal(result.attemptedValue, '+447780579093');
assert.equal(api.getValue(), '+447780579093'); assert.equal(api.getValue(), '+447780579093');
assert.deepEqual(api.getFills(), ['7780579093', '+447780579093']); assert.equal(api.getHiddenValue(), '+447780579093');
assert.deepEqual(api.getHiddenEvents(), ['input', 'change']);
assert.deepEqual(api.getFills(), ['+447780579093']);
});
test('step 7 keeps national phone fill when visible login input has no dial prefix', async () => {
const api = createPhoneFillApi((input, value) => {
input.value = value;
}, { initialValue: '' });
const result = await api.run();
assert.equal(result.inputValue, '7780579093');
assert.equal(result.attemptedValue, '7780579093');
assert.equal(api.getValue(), '7780579093');
assert.equal(api.getHiddenValue(), '+447780579093');
assert.deepEqual(api.getHiddenEvents(), ['input', 'change']);
assert.deepEqual(api.getFills(), ['7780579093']);
}); });
test('step 7 stops before submit when phone fill never includes the local number', async () => { test('step 7 stops before submit when phone fill never includes the local number', async () => {
@@ -375,5 +476,5 @@ test('step 7 stops before submit when phone fill never includes the local number
await assert.rejects(api.run, /7780579093/); await assert.rejects(api.run, /7780579093/);
assert.equal(api.getValue(), '+44'); assert.equal(api.getValue(), '+44');
assert.deepEqual(api.getFills(), ['7780579093', '+447780579093', '447780579093']); assert.deepEqual(api.getFills(), ['+447780579093', '7780579093', '+447780579093', '7780579093']);
}); });
+31
View File
@@ -115,6 +115,37 @@ return {
); );
}); });
test('ensureStep8VerificationPageReady allows add-email handoff only when requested', async () => {
const api = new Function(`
function getLoginAuthStateLabel(state) {
return state === 'add_email_page' ? '添加邮箱页' : 'unknown page';
}
async function getLoginAuthStateFromContent() {
return {
state: 'add_email_page',
url: 'https://auth.openai.com/add-email',
};
}
${extractFunction(backgroundSource, 'ensureStep8VerificationPageReady')}
return {
run(options) {
return ensureStep8VerificationPageReady(options || {});
},
};
`)();
await assert.rejects(
() => api.run({}),
/当前未进入登录验证码页面/
);
const result = await api.run({ allowAddEmailPage: true });
assert.equal(result.state, 'add_email_page');
});
test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => { test('step 8 reruns step 7 when auth page enters login timeout retry state', async () => {
const calls = { const calls = {
rerunStep7: 0, rerunStep7: 0,
+16 -13
View File
@@ -366,9 +366,9 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
这两步共享验证码主流程: 这两步共享验证码主流程:
1. 先按账号身份分流验证码通道: 1. 先按注册阶段或认证页真实状态分流验证码通道:
Step 4:邮箱注册走邮箱验证码,手机号注册走短信验证码 Step 4:邮箱注册走邮箱验证码,手机号注册走短信验证码
Step 8邮箱登录走邮箱验证码,手机号登录走短信验证码 Step 8真实邮箱验证码页走邮箱验证码,真实手机验证码页才走短信验证码;如果先进入 `add-email`,先绑定邮箱再走邮箱验证码
2. 确定 provider 2. 确定 provider
3. 必要时重发验证码 3. 必要时重发验证码
4. 轮询邮箱、短信 API 或对应 provider 通道 4. 轮询邮箱、短信 API 或对应 provider 通道
@@ -438,7 +438,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
- 邮箱账号:沿用现有邮箱登录链路 - 邮箱账号:沿用现有邮箱登录链路
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号 - 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录 4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点 5. 确保真正进入验证码页或登录后续页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点,手机号注册后的 OAuth 登录也允许以真实 `add-email` 作为 Step 7 完成并交给 Step 8 接管
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询 6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
7. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试 7. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试
8. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程 8. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程
@@ -457,19 +457,21 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
流程: 流程:
1.按账号身份判断当前登录验证码通道 1.确认当前认证页真实状态,而不是只看 `accountIdentifierType`
2. 邮箱账号 2. 当前是 `add-email`
- 确认当前认证页已经进入邮箱验证码页 - 调用 `resolveSignupEmailForFlow` 获取或生成本轮绑定邮箱
- 内容脚本发送 `SUBMIT_ADD_EMAIL` 填写邮箱并提交
- 页面进入邮箱验证码页后,用绑定邮箱作为 `step8VerificationTargetEmail`
3. 当前是邮箱验证码页:
- 对非 2925 provider,固定当前验证码页显示邮箱为本次 Step 8 的目标邮箱 - 对非 2925 provider,固定当前验证码页显示邮箱为本次 Step 8 的目标邮箱
- 打开邮箱页或 API 轮询入口 - 打开邮箱页或 API 轮询入口
- 轮询登录验证码并回填 - 轮询登录验证码并回填
3. 手机号账号 4. 当前是真实 `phone-verification`
- 直接复用注册成功后的同一手机号订单,不重新申请新号 - 才复用手机号验证码共享流程,重新激活同一号码并轮询短信验证
- 重新激活该号码,轮询短信验证码
- 在当前 `phone-verification` 页回填短信验证码 - 在当前 `phone-verification` 页回填短信验证码
4. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则步骤 8 会保留“登录验证码已提交成功”的结果,并把后续手机号验证需求继续交给步骤 9 处理 5. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则邮箱注册模式下交给后续 OAuth 后置手机号验证链路;手机号注册模式不把 `signupPhone*` 身份误当成官方 add-phone 订单
5. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试 6. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试;若仍停在验证码页、手机验证码页或 add-email 页,则优先在当前链路重试
6. 获取到登录验证码后不再触发“刷新 OAuth 并重走 Step 7”的前置回放,直接在当前验证码页提交并继续进入 Step 9 7. 获取到登录验证码后不再触发“刷新 OAuth 并重走 Step 7”的前置回放,直接在当前验证码页提交并继续进入 Step 9
补充: 补充:
@@ -477,7 +479,8 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
-`2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。 -`2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
-`custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。 -`custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。
- 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。 - 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。
- 手机号登录分支不会再读取邮箱 provider 配置,也不会误调用邮箱轮询;如果缺少可复用的注册手机号激活记录,会直接要求回到步骤 7 重新开始登录 - 手机号注册身份本身不会让 Step 8 自动走短信;只有真实页面状态是 `phone_verification_page` 时才调用短信 helper。手机号注册登录后出现 `add-email` 时,Step 8 走“绑定邮箱 -> 邮箱验证码”链路
- `email_in_use` 会在 Step 8 当前步骤内清理当前邮箱并重新打开 `add-email` 获取新邮箱;`max_check_attempts` 不继续点击认证页 `重试`,只恢复当前 Step 8。
### Step 9 ### Step 9
+5 -5
View File
@@ -54,7 +54,7 @@
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。 - `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword` - `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和手机号 OAuth 登录 Step 8,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。 - `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。 - `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
@@ -65,7 +65,7 @@
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。 - `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。 - `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session,打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。 - `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session,打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按账号身份分发登录验证码流程:邮箱账号继续走邮箱轮询、验证码回填与回退控制;手机号账号则直接复用注册成功后的同一手机号订单,轮询短信验证码并在当前认证页提交;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。 - `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先生成/解析邮箱并提交绑定,再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。 - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal。 - `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal。
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。
@@ -93,7 +93,7 @@
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。 - `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
- `content/plus-checkout.js`ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。 - `content/plus-checkout.js`ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 登录链路会额外识别手机号输入页、手机号登录入口`phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页 - `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。
- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。 - `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。 - `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。
@@ -202,7 +202,7 @@
- `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
- `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。 - `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。
- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。 - `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。
- `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),并为 2925 关闭重发间隔。 - `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),覆盖邮箱验证码页、真实手机验证码页、`add-email` 绑定邮箱后收码、2925 固定回看窗口与关闭重发间隔。
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。 - `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。 - `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
@@ -246,7 +246,7 @@
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。 - `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。
- `tests/step5-age-consent.test.js`:测试步骤 5 在年龄页会先勾选顶部“我同意以下所有各项”复选框,再点击提交。 - `tests/step5-age-consent.test.js`:测试步骤 5 在年龄页会先勾选顶部“我同意以下所有各项”复选框,再点击提交。
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。 - `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页。 - `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页、真实 `add-email` 页面不会被误判成普通邮箱输入页
- `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。 - `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口、手机号登录页可视国家下拉框会按本轮号码区号自动切换,以及输入框只剩区号时不会继续提交。 - `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口、手机号登录页可视国家下拉框会按本轮号码区号自动切换,以及输入框只剩区号时不会继续提交。
- `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。 - `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。