Handle combined signup verification/profile page

This commit is contained in:
EmptyDust
2026-05-02 11:19:22 +08:00
parent 99313843c1
commit 2bfa277aa1
8 changed files with 479 additions and 7 deletions
+2
View File
@@ -8976,6 +8976,8 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
chrome,
completeStepFromBackground,
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
generateRandomBirthday,
generateRandomName,
ensureMail2925MailboxSession,
ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
getMailConfig,
+5 -1
View File
@@ -328,7 +328,11 @@
const step5Status = latestState.stepStatuses?.[5];
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
await setStepStatus(5, 'skipped');
await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn');
if (payload.skipProfileStepReason === 'combined_verification_profile') {
await addLog('步骤 4:当前验证码页已内嵌完成注册资料提交,已自动跳过步骤 5。', 'warn');
} else {
await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn');
}
}
}
break;
+19
View File
@@ -9,6 +9,8 @@
chrome,
completeStepFromBackground,
confirmCustomVerificationStepBypass,
generateRandomBirthday,
generateRandomName,
ensureMail2925MailboxSession,
ensureIcloudMailSession,
getMailConfig,
@@ -25,6 +27,21 @@
throwIfStopped,
} = deps;
function buildSignupProfileForVerificationStep() {
const name = typeof generateRandomName === 'function' ? generateRandomName() : null;
const birthday = typeof generateRandomBirthday === 'function' ? generateRandomBirthday() : null;
if (!name?.firstName || !name?.lastName || !birthday) {
return null;
}
return {
firstName: name.firstName,
lastName: name.lastName,
year: birthday.year,
month: birthday.month,
day: birthday.day,
};
}
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
@@ -152,12 +169,14 @@
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
].includes(mail.provider);
const signupProfile = buildSignupProfileForVerificationStep();
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
signupProfile,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
+4 -1
View File
@@ -909,7 +909,10 @@
type: 'FILL_CODE',
step,
source: 'background',
payload: { code },
payload: {
code,
...(step === 4 && options.signupProfile ? { signupProfile: options.signupProfile } : {}),
},
};
let result;
if (typeof sendToContentScriptResilient === 'function') {
+81 -5
View File
@@ -1145,6 +1145,12 @@ function isLikelyLoggedInChatgptHomeUrl(rawUrl = location.href) {
}
function getStep4PostVerificationState() {
// Newer auth flows can briefly render profile fields before the email-verification
// form fully exits. Do not advance to Step 5 while verification UI is still present.
if (isVerificationPageStillVisible()) {
return null;
}
if (isStep5Ready() || isSignupProfilePageUrl()) {
return {
state: 'step5',
@@ -2531,12 +2537,25 @@ async function waitForSplitVerificationInputsFilled(inputs, code, timeout = 2500
}
async function fillVerificationCode(step, payload) {
const { code } = payload;
const { code, signupProfile } = payload;
if (!code) throw new Error('未提供验证码。');
if (step === 4 && isStep5Ready()) {
log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok');
return { success: true, assumed: true, alreadyAdvanced: true };
if (step === 4) {
const postVerificationState = getStep4PostVerificationState();
if (postVerificationState?.state === 'logged_in_home') {
log(`步骤 ${step}:检测到页面已进入 ChatGPT 已登录态,本次验证码提交按成功处理。`, 'ok');
return {
success: true,
assumed: true,
alreadyAdvanced: true,
skipProfileStep: true,
url: postVerificationState.url || location.href,
};
}
if (postVerificationState?.state === 'step5') {
log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok');
return { success: true, assumed: true, alreadyAdvanced: true };
}
}
if (step === 8) {
if (isStep8Ready()) {
@@ -2554,6 +2573,17 @@ async function fillVerificationCode(step, payload) {
await waitForLoginVerificationPageReady();
}
const combinedSignupProfilePage = step === 4 && isCombinedSignupVerificationProfilePage();
if (combinedSignupProfilePage) {
if (!signupProfile || !signupProfile.firstName || !signupProfile.lastName) {
throw new Error('当前注册验证码页面要求同时填写资料,但未提供姓名或生日数据。');
}
await step5_fillNameBirthday({
...signupProfile,
prefillOnly: true,
});
}
// Find code input — could be a single input or multiple separate inputs
// Retry with 405 error recovery if needed
const maxRetries = 3;
@@ -2632,6 +2662,10 @@ async function fillVerificationCode(step, payload) {
} else {
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
if (combinedSignupProfilePage && !outcome.invalidCode) {
outcome.skipProfileStep = true;
outcome.skipProfileStepReason = 'combined_verification_profile';
}
return outcome;
}
@@ -2663,6 +2697,11 @@ async function fillVerificationCode(step, payload) {
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
if (combinedSignupProfilePage && !outcome.invalidCode) {
outcome.skipProfileStep = true;
outcome.skipProfileStepReason = 'combined_verification_profile';
}
return outcome;
}
@@ -3333,8 +3372,40 @@ function getStep5DirectCompletionPayload({ isAgeMode = false } = {}) {
return payload;
}
function isCombinedSignupVerificationProfilePage() {
if (!isEmailVerificationPage() || !isVerificationPageStillVisible()) {
return false;
}
if (!document.querySelector('form[action*="email-verification/register" i]')) {
return false;
}
const nameInput = document.querySelector('input[name="name"], input[autocomplete="name"]');
if (!nameInput || !isVisibleElement(nameInput)) {
return false;
}
const ageInput = document.querySelector('input[name="age"]');
if (ageInput && isVisibleElement(ageInput)) {
return true;
}
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
return Boolean(
yearSpinner
&& monthSpinner
&& daySpinner
&& isVisibleElement(yearSpinner)
&& isVisibleElement(monthSpinner)
&& isVisibleElement(daySpinner)
);
}
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, age, year, month, day } = payload;
const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload;
if (!firstName || !lastName) throw new Error('未提供姓名数据。');
const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
@@ -3533,6 +3604,11 @@ async function step5_fillNameBirthday(payload) {
}
if (prefillOnly) {
log('步骤 4:混合注册页资料已预填,继续填写验证码。', 'info');
return { prefilled: true };
}
// Click "完成帐户创建" button
await sleep(500);
const completeBtn = document.querySelector('button[type="submit"]')
@@ -253,3 +253,54 @@ return {
state: 'step5',
});
});
test('signup verification state keeps verification priority when email-verification page also shows profile fields', () => {
const api = new Function(`
const location = {
href: 'https://auth.openai.com/email-verification/register',
};
function isStep5Ready() {
return true;
}
function isVerificationPageStillVisible() {
return true;
}
function isSignupPasswordErrorPage() {
return false;
}
function getSignupPasswordTimeoutErrorPageState() {
return null;
}
function isSignupEmailAlreadyExistsPage() {
return false;
}
function getSignupPasswordInput() {
return null;
}
function getSignupPasswordSubmitButton() {
return null;
}
${extractFunction('isSignupProfilePageUrl')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('getStep4PostVerificationState')}
${extractFunction('inspectSignupVerificationState')}
return {
run() {
return inspectSignupVerificationState();
},
};
`)();
assert.deepStrictEqual(api.run(), {
state: 'verification',
});
});
+257
View File
@@ -118,6 +118,8 @@ async function sleep() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVerificationPageStillVisible() { return false; }
function isEmailVerificationPage() { return true; }
function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
@@ -131,6 +133,7 @@ ${extractFunction('getVerificationSubmitButtonForTarget')}
${extractFunction('waitForVerificationSubmitButton')}
${extractFunction('waitForVerificationCodeTarget')}
${extractFunction('waitForSplitVerificationInputsFilled')}
${extractFunction('isCombinedSignupVerificationProfilePage')}
${extractFunction('isSignupProfilePageUrl')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('getStep4PostVerificationState')}
@@ -160,3 +163,257 @@ return {
assert.equal(snapshot.submitClicked, true);
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
});
test('fillVerificationCode does not short-circuit on mixed email-verification profile page before verification exits', async () => {
const api = new Function(`
const logs = [];
const location = { href: 'https://auth.openai.com/email-verification/register' };
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
async function waitForLoginVerificationPageReady() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function fillInput() {}
async function sleep() {}
function isStep5Ready() { return true; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVisibleElement() { return true; }
function isActionEnabled() { return true; }
function getActionText(el) { return el?.textContent || ''; }
async function humanPause() {}
function simulateClick() {}
function getCurrentAuthRetryPageState() { return null; }
function isPhoneVerificationPageReady() { return false; }
function getVerificationCodeTarget() { return { type: 'single', element: { value: '', form: null, closest() { return null; } } }; }
function findResendVerificationCodeTrigger() { return { textContent: 'Resend' }; }
function isEmailVerificationPage() { return true; }
function getPageTextSnapshot() { return 'Enter the verification code we just sent'; }
const document = {
querySelector(selector) {
if (selector === 'form[action*="email-verification" i]') return {};
return null;
},
querySelectorAll() {
return [];
},
};
${extractFunction('isVerificationPageStillVisible')}
${extractFunction('isCombinedSignupVerificationProfilePage')}
${extractFunction('isSignupProfilePageUrl')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('getStep4PostVerificationState')}
${extractFunction('fillVerificationCode')}
return {
async run() {
try {
await fillVerificationCode(4, { code: '123456' });
return { ok: true };
} catch (error) {
return { ok: false, message: String(error?.message || error) };
}
},
};
`)();
const result = await api.run();
assert.deepStrictEqual(result, {
ok: false,
message: '未找到验证码输入框。URL: https://auth.openai.com/email-verification/register',
});
});
test('fillVerificationCode prefills profile on combined verification/register page and skips step 5 after success', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
const filledValues = [];
let submitClicked = false;
const VERIFICATION_CODE_INPUT_SELECTOR = 'input[name="code"]';
const location = {
href: 'https://auth.openai.com/email-verification/register',
pathname: '/email-verification/register',
};
function KeyboardEvent(type, init = {}) {
this.type = type;
Object.assign(this, init);
}
const nameInput = {
value: '',
disabled: false,
getAttribute(name) {
if (name === 'name') return 'name';
if (name === 'autocomplete') return 'name';
return '';
},
};
const ageInput = {
value: '',
disabled: false,
getAttribute(name) {
if (name === 'name') return 'age';
return '';
},
};
const codeInput = {
value: '',
disabled: false,
form: null,
getAttribute(name) {
if (name === 'maxlength') return '6';
if (name === 'aria-disabled') return 'false';
if (name === 'name') return 'code';
return '';
},
closest() { return null; },
focus() {},
dispatchEvent() {},
};
const submitBtn = {
tagName: 'BUTTON',
textContent: 'Continue',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
return '';
},
click() {
submitClicked = true;
},
};
const document = {
querySelector(selector) {
switch (selector) {
case 'input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]':
return nameInput;
case 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]':
return nameInput;
case 'input[name="age"]':
return ageInput;
case '[role="spinbutton"][data-type="year"]':
case '[role="spinbutton"][data-type="month"]':
case '[role="spinbutton"][data-type="day"]':
case 'input[name="birthday"]':
return null;
case 'form[action*="email-verification/register" i]':
case 'form[action*="email-verification" i]':
return { action: '/email-verification/register' };
case VERIFICATION_CODE_INPUT_SELECTOR:
return codeInput;
case 'button[type="submit"]':
return submitBtn;
default:
return null;
}
},
querySelectorAll(selector) {
if (selector === 'input[maxlength="1"]') return [];
if (selector === 'button[type="submit"], input[type="submit"]') return [submitBtn];
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitBtn];
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') return [];
if (selector === 'input[type="checkbox"]') return [];
return [];
},
execCommand() {},
};
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
async function waitForLoginVerificationPageReady() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function fillInput(el, value) {
el.value = value;
filledValues.push({ target: el === nameInput ? 'name' : (el === ageInput ? 'age' : 'code'), value });
}
async function sleep() {}
function isStep5Ready() { return true; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVisibleElement(el) { return Boolean(el) && !el.disabled; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
async function humanPause() {}
function simulateClick(el) { el.click(); clicks.push(el.textContent); }
function getCurrentAuthRetryPageState() { return null; }
function isPhoneVerificationPageReady() { return false; }
function findResendVerificationCodeTrigger() { return null; }
function isEmailVerificationPage() { return true; }
function isCombinedSignupVerificationProfilePage() { return true; }
function getPageTextSnapshot() { return 'Create your account Enter the verification code we just sent Tell us about you'; }
function findBirthdayReactAriaSelect() { return null; }
async function setReactAriaBirthdaySelect() {}
async function waitForElement(selector) {
if (/input\\[name=\"name\"\\]/.test(selector)) {
return nameInput;
}
throw new Error('unexpected selector ' + selector);
}
async function waitForElementByText() { return submitBtn; }
function isStep5AllConsentText() { return false; }
function findStep5AllConsentCheckbox() { return null; }
function isStep5CheckboxChecked() { return false; }
async function waitForVerificationSubmitOutcome() {
return { success: true, skipProfileStep: true };
}
${extractFunction('getStep5DirectCompletionPayload')}
${extractFunction('isVerificationPageStillVisible')}
${extractFunction('isSignupProfilePageUrl')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('getStep4PostVerificationState')}
${extractFunction('getVisibleSplitVerificationInputs')}
${extractFunction('getVerificationCodeTarget')}
${extractFunction('getVerificationSubmitButtonForTarget')}
${extractFunction('waitForVerificationSubmitButton')}
${extractFunction('waitForVerificationCodeTarget')}
${extractFunction('waitForSplitVerificationInputsFilled')}
${extractFunction('step5_fillNameBirthday')}
${extractFunction('fillVerificationCode')}
return {
run() {
return fillVerificationCode(4, {
code: '123456',
signupProfile: {
firstName: 'Ada',
lastName: 'Lovelace',
age: 22,
},
});
},
snapshot() {
return {
logs,
clicks,
filledValues,
submitClicked,
nameValue: nameInput.value,
ageValue: ageInput.value,
codeValue: codeInput.value,
};
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
success: true,
skipProfileStep: true,
skipProfileStepReason: 'combined_verification_profile',
});
assert.equal(snapshot.nameValue, 'Ada Lovelace');
assert.equal(snapshot.ageValue, '22');
assert.equal(snapshot.codeValue, '123456');
assert.equal(snapshot.submitClicked, true);
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
});
+60
View File
@@ -1219,6 +1219,66 @@ test('verification flow uses resilient signup-page transport when submitting ver
assert.ok(resilientCalls[0].options.timeoutMs >= 30000);
});
test('verification flow forwards optional signup profile payload when submitting signup verification code', async () => {
const resilientCalls = [];
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 () => {
throw new Error('should not use non-resilient channel');
},
sendToContentScriptResilient: async (_source, message) => {
resilientCalls.push(message);
return { success: true, skipProfileStep: true };
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(4, '654321', {
signupProfile: {
firstName: 'Ada',
lastName: 'Lovelace',
year: 2003,
month: 6,
day: 19,
},
});
assert.deepStrictEqual(result, { success: true, skipProfileStep: true });
assert.deepStrictEqual(resilientCalls[0].payload.signupProfile, {
firstName: 'Ada',
lastName: 'Lovelace',
year: 2003,
month: 6,
day: 19,
});
});
test('verification flow treats retryable submit transport failure as success when step 4 already redirected to logged-in home', async () => {
const logs = [];