feat: enhance signup flow with multiple profile page patterns and improved error handling during verification

This commit is contained in:
QLHazyCoder
2026-05-09 20:42:36 +08:00
parent 5d6792f5fb
commit dcdb3a3e8d
7 changed files with 807 additions and 64 deletions
+291 -15
View File
@@ -2627,7 +2627,7 @@ function isSignupProfilePageUrl(rawUrl = location.href) {
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
return false;
}
return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
} catch {
return false;
}
@@ -3991,6 +3991,7 @@ function serializeLoginAuthState(snapshot) {
url: snapshot?.url || location.href,
path: snapshot?.path || location.pathname || '',
displayedEmail: snapshot?.displayedEmail || '',
verificationErrorText: getVerificationErrorText(),
retryEnabled: Boolean(snapshot?.retryEnabled),
titleMatched: Boolean(snapshot?.titleMatched),
detailMatched: Boolean(snapshot?.detailMatched),
@@ -6028,14 +6029,23 @@ function getSerializableRect(el) {
// Step 5: Fill Name & Birthday / Age
// ============================================================
function getStep5DirectCompletionPayload({ isAgeMode = false } = {}) {
function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, outcome = null } = {}) {
const payload = {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
};
if (isAgeMode) {
payload.ageMode = true;
}
if (navigationStarted) {
payload.navigationStarted = true;
}
if (outcome?.state) {
payload.outcome = outcome.state;
}
if (outcome?.url) {
payload.url = outcome.url;
}
return payload;
}
@@ -6084,6 +6094,252 @@ async function waitForCombinedSignupVerificationProfilePage(timeout = 2500) {
return isCombinedSignupVerificationProfilePage();
}
function getStep5ProfilePathPatterns() {
return [
/\/create-account\/profile(?:[/?#]|$)/i,
/\/u\/signup\/profile(?:[/?#]|$)/i,
/\/signup\/profile(?:[/?#]|$)/i,
];
}
function getStep5AuthRetryPathPatterns() {
const signupPatterns = typeof getSignupAuthRetryPathPatterns === 'function'
? getSignupAuthRetryPathPatterns()
: [];
return [
...signupPatterns,
...getStep5ProfilePathPatterns(),
];
}
function isStep5ProfilePageUrl(rawUrl = location.href) {
return isSignupProfilePageUrl(rawUrl);
}
function getStep5AuthRetryPageState() {
if (typeof getAuthTimeoutErrorPageState === 'function') {
return getAuthTimeoutErrorPageState({
pathPatterns: getStep5AuthRetryPathPatterns(),
});
}
if (typeof getCurrentAuthRetryPageState === 'function') {
return getCurrentAuthRetryPageState('signup');
}
return null;
}
function getStep5SubmitButton() {
const direct = document.querySelector('button[type="submit"], input[type="submit"]');
if (direct && isVisibleElement(direct)) {
return direct;
}
const candidates = document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el)) return false;
const text = typeof getActionText === 'function'
? getActionText(el)
: [
el?.textContent,
el?.value,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
return /完成|创建|create|continue|finish|done|agree/i.test(text);
}) || null;
}
async function waitForStep5SubmitButton(timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const button = getStep5SubmitButton();
if (button) {
return button;
}
await sleep(150);
}
return null;
}
function isStep5SubmitButtonClickable(button) {
return Boolean(button)
&& isVisibleElement(button)
&& !button.disabled
&& button.getAttribute?.('aria-disabled') !== 'true';
}
function isStep5ProfileStillVisible() {
if (isStep5ProfilePageUrl()) {
return true;
}
return typeof isStep5Ready === 'function' ? isStep5Ready() : false;
}
function getStep5PostSubmitSuccessState() {
if (getStep5AuthRetryPageState()) {
return null;
}
if (isLikelyLoggedInChatgptHomeUrl()) {
return {
state: 'logged_in_home',
url: location.href,
};
}
if (typeof isOAuthConsentPage === 'function' && isOAuthConsentPage()) {
return {
state: 'oauth_consent',
url: location.href,
};
}
if (typeof isAddPhonePageReady === 'function' && isAddPhonePageReady()) {
return {
state: 'add_phone',
url: location.href,
};
}
if (!isStep5ProfileStillVisible()) {
return {
state: 'left_profile',
url: location.href,
};
}
return null;
}
function installStep5NavigationCompletionReporter(completeOnce) {
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
return () => {};
}
const onNavigationStarted = () => {
completeOnce({
navigationStarted: true,
outcome: {
state: 'navigation_started',
url: location.href,
},
});
};
window.addEventListener('pagehide', onNavigationStarted, { once: true });
window.addEventListener('beforeunload', onNavigationStarted, { once: true });
return () => {
window.removeEventListener('pagehide', onNavigationStarted);
window.removeEventListener('beforeunload', onNavigationStarted);
};
}
async function waitForStep5SubmitOutcome(options = {}) {
const {
timeoutMs = 45000,
maxAuthRetryRecoveries = 2,
maxSubmitClicks = 3,
retryClickIntervalMs = 3500,
} = options;
const start = Date.now();
let authRetryRecoveryCount = 0;
let submitClickCount = 1;
let lastSubmitClickAt = Date.now();
let lastStep5Error = '';
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const retryState = getStep5AuthRetryPageState();
if (retryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (retryState?.maxCheckAttemptsBlocked) {
throw createAuthMaxCheckAttemptsError();
}
if (retryState) {
if (authRetryRecoveryCount >= maxAuthRetryRecoveries) {
throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${location.href}`);
}
authRetryRecoveryCount += 1;
log(`步骤 5:资料提交后进入认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries}...`, 'warn');
await recoverCurrentAuthRetryPage({
flow: 'signup',
logLabel: '步骤 5:资料提交后检测到认证重试页,正在点击“重试”恢复',
maxClickAttempts: 2,
pathPatterns: getStep5AuthRetryPathPatterns(),
step: 5,
timeoutMs: 12000,
});
lastSubmitClickAt = Date.now();
continue;
}
const successState = getStep5PostSubmitSuccessState();
if (successState) {
return successState;
}
const step5Error = typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '';
if (step5Error) {
lastStep5Error = step5Error;
}
if (
isStep5ProfileStillVisible()
&& submitClickCount < maxSubmitClicks
&& Date.now() - lastSubmitClickAt >= retryClickIntervalMs
) {
const submitButton = getStep5SubmitButton();
if (isStep5SubmitButtonClickable(submitButton)) {
submitClickCount += 1;
log(`步骤 5:资料提交后仍停留在资料页,正在重新点击“完成帐户创建”(第 ${submitClickCount}/${maxSubmitClicks} 次)...`, 'warn');
await humanPause(350, 900);
simulateClick(submitButton);
lastSubmitClickAt = Date.now();
await sleep(1000);
continue;
}
}
await sleep(250);
}
const finalRetryState = getStep5AuthRetryPageState();
if (finalRetryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (finalRetryState?.maxCheckAttemptsBlocked) {
throw createAuthMaxCheckAttemptsError();
}
if (finalRetryState) {
throw new Error(`步骤 5:资料提交后仍停留在认证重试页,自动恢复未完成。URL: ${location.href}`);
}
const finalSuccessState = getStep5PostSubmitSuccessState();
if (finalSuccessState) {
return finalSuccessState;
}
const finalStep5Error = (typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '') || lastStep5Error;
if (finalStep5Error) {
throw new Error(`步骤 5:资料提交后页面返回错误:${finalStep5Error}。URL: ${location.href}`);
}
throw new Error(`步骤 5:资料提交后未检测到页面跳转或恢复成功(已点击提交 ${submitClickCount}/${maxSubmitClicks} 次)。URL: ${location.href}`);
}
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload;
if (!firstName || !lastName) throw new Error('未提供姓名数据。');
@@ -6291,7 +6547,7 @@ async function step5_fillNameBirthday(payload) {
// Click "完成帐户创建" button
await sleep(500);
const completeBtn = document.querySelector('button[type="submit"]')
const completeBtn = await waitForStep5SubmitButton(5000)
|| await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
if (!completeBtn) {
throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href);
@@ -6299,20 +6555,40 @@ async function step5_fillNameBirthday(payload) {
const isAgeMode = !birthdayMode && Boolean(ageInput);
if (isAgeMode) {
log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将直接完成当前步骤。', 'warn');
log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将等待页面结果。', 'info');
}
await humanPause(500, 1300);
simulateClick(completeBtn);
let reportedCompletionPayload = null;
function completeStep5Once(extra = {}) {
if (reportedCompletionPayload) {
return reportedCompletionPayload;
}
const completionPayload = getStep5DirectCompletionPayload({ isAgeMode });
reportComplete(5, completionPayload);
if (isAgeMode) {
log('步骤 5:年龄模式已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。', 'warn');
const completionPayload = getStep5DirectCompletionPayload({
isAgeMode,
navigationStarted: Boolean(extra.navigationStarted),
outcome: extra.outcome || null,
});
reportedCompletionPayload = completionPayload;
reportComplete(5, completionPayload);
return completionPayload;
}
log('步骤 5:已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。');
return completionPayload;
const cleanupNavigationReporter = installStep5NavigationCompletionReporter(completeStep5Once);
await humanPause(500, 1300);
simulateClick(completeBtn);
log('步骤 5:已点击“完成帐户创建”,正在等待页面跳转、重试页或提交结果。');
try {
const outcome = await waitForStep5SubmitOutcome();
cleanupNavigationReporter();
const completionPayload = completeStep5Once({ outcome });
log(`步骤 5:资料提交结果已确认(${outcome.state || 'success'}),准备继续后续步骤。`, 'ok');
return completionPayload;
} catch (error) {
cleanupNavigationReporter();
throw error;
}
}