From 745010d6c97338f17c80b9df636728c3d7bb62d8 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 12 May 2026 00:26:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=AD=A5=E9=AA=A4=20?= =?UTF-8?q?5=20=E6=8F=90=E4=BA=A4=E7=8A=B6=E6=80=81=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=92=8C=E9=87=8D=E8=AF=95=E9=A1=B5=E9=9D=A2=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 149 +++++++++++++++++- background/signup-flow-helpers.js | 2 +- background/verification-flow.js | 2 +- content/signup-page.js | 61 ++++++- ...d-step5-post-completion-validation.test.js | 140 ++++++++++++++++ tests/step5-direct-complete.test.js | 108 +++++++++++++ 6 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 tests/background-step5-post-completion-validation.test.js diff --git a/background.js b/background.js index 63ee547..2173c2b 100644 --- a/background.js +++ b/background.js @@ -7068,6 +7068,15 @@ function isSignupEntryHost(hostname = '') { return ['chatgpt.com', 'chat.openai.com'].includes(hostname); } +function isLikelyLoggedInChatgptHomeUrl(rawUrl) { + const parsed = parseUrlSafely(rawUrl); + if (!parsed) return false; + if (!isSignupEntryHost(String(parsed.hostname || '').toLowerCase())) { + return false; + } + return !/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(parsed.pathname || ''); +} + function isSignupPasswordPageUrl(rawUrl) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupPasswordPageUrl) { return navigationUtils.isSignupPasswordPageUrl(rawUrl); @@ -9542,6 +9551,7 @@ async function executeStep(step, options = {}) { */ async function executeStepAndWait(step, delayAfter = 2000) { throwIfStopped(); + let completionPayload = null; const delaySeconds = normalizeAutoStepDelaySeconds((await getState()).autoStepDelaySeconds, null); if (delaySeconds > 0) { @@ -9570,7 +9580,7 @@ async function executeStepAndWait(step, delayAfter = 2000) { await addLog(`自动运行:步骤 ${step} 已执行返回,当前状态为 ${latestState.stepStatuses?.[step] || 'pending'},准备继续后续步骤。`, 'info'); } else if (doesStepUseCompletionSignal(step, executionState)) { await addLog(`自动运行:步骤 ${step} 已发起,正在等待完成信号(超时 ${AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS / 1000} 秒)。`, 'info'); - await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS); + completionPayload = await executeStepViaCompletionSignal(step, AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS); await addLog(`自动运行:步骤 ${step} 已收到完成信号,准备继续后续步骤。`, 'info'); } else { await executeStep(step); @@ -9586,6 +9596,13 @@ async function executeStepAndWait(step, delayAfter = 2000) { stableMs: 1000, initialDelayMs: 800, }); + try { + await validateStep5PostCompletion(signupTabId, completionPayload || {}); + } catch (step5ValidationError) { + await setStepStatus(5, 'failed'); + await addLog(`失败:${getErrorMessage(step5ValidationError)}`, 'error', { step: 5 }); + throw step5ValidationError; + } } } @@ -11067,7 +11084,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe }, isSignupProfilePageUrl: (rawUrl) => { const parsed = parseUrlSafely(rawUrl); - return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(parsed.pathname || '')); + return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(parsed.pathname || '')); }, isRetryableContentScriptTransportError, isHotmailProvider, @@ -12144,6 +12161,134 @@ async function getLoginAuthStateFromContent(options = {}) { return result || {}; } +async function getStep5SubmitStateFromContent(options = {}) { + const result = await sendToContentScriptResilient( + 'signup-page', + { + type: 'GET_STEP5_SUBMIT_STATE', + source: 'background', + payload: {}, + }, + { + timeoutMs: options.timeoutMs ?? 15000, + retryDelayMs: options.retryDelayMs ?? 600, + responseTimeoutMs: options.responseTimeoutMs ?? (options.timeoutMs ?? 15000), + logMessage: options.logMessage || '步骤 5:资料页正在切换,等待页面恢复后确认提交结果...', + logStep: 5, + logStepKey: options.logStepKey || 'fill-profile', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + + return result || {}; +} + +async function recoverStep5SubmitRetryPageOnTab(options = {}) { + const result = await sendToContentScriptResilient( + 'signup-page', + { + type: 'RECOVER_STEP5_SUBMIT_RETRY_PAGE', + source: 'background', + payload: { + timeoutMs: options.timeoutMs ?? 12000, + maxClickAttempts: options.maxClickAttempts ?? 2, + }, + }, + { + timeoutMs: options.timeoutMs ?? 15000, + retryDelayMs: options.retryDelayMs ?? 600, + responseTimeoutMs: options.responseTimeoutMs ?? (options.timeoutMs ?? 15000), + logMessage: options.logMessage || '步骤 5:资料提交后正在尝试恢复认证重试页...', + logStep: 5, + logStepKey: options.logStepKey || 'fill-profile', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + + return result || {}; +} + +async function validateStep5PostCompletion(tabId, completionPayload = {}) { + if (!Number.isInteger(tabId)) { + throw new Error('步骤 5:缺少有效的资料页标签页,无法确认提交后的最终状态。'); + } + + const maxAuthRetryRecoveries = Math.max(1, Number(completionPayload?.maxAuthRetryRecoveries) || 2); + let authRetryRecoveryCount = 0; + + while (true) { + const tab = await chrome.tabs.get(tabId).catch(() => null); + const currentUrl = String(tab?.url || completionPayload?.url || '').trim(); + if (currentUrl && isLikelyLoggedInChatgptHomeUrl(currentUrl)) { + return { + successState: 'logged_in_home', + url: currentUrl, + }; + } + + const pageState = await getStep5SubmitStateFromContent({ + timeoutMs: 15000, + responseTimeoutMs: 15000, + retryDelayMs: 500, + logMessage: '步骤 5:资料提交已触发页面跳转,正在确认最终页面状态...', + }); + + if (pageState.userAlreadyExistsBlocked) { + throw new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 5:检测到 user_already_exists,当前轮将直接停止。'); + } + if (pageState.maxCheckAttemptsBlocked) { + throw new Error('AUTH_MAX_CHECK_ATTEMPTS::max_check_attempts on step 5 auth retry page; restart the current auth step without clicking Retry.'); + } + + if (pageState.retryPage) { + if (authRetryRecoveryCount >= maxAuthRetryRecoveries) { + throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${pageState.url || currentUrl || 'unknown'}`); + } + authRetryRecoveryCount += 1; + await addLog(`步骤 5:提交完成信号后检测到认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})...`, 'warn', { + step: 5, + stepKey: 'fill-profile', + }); + await recoverStep5SubmitRetryPageOnTab({ + timeoutMs: 15000, + retryDelayMs: 600, + logMessage: '步骤 5:资料提交后的认证重试页正在恢复,等待“重试”按钮重新就绪...', + }); + await waitForTabStableComplete(tabId, { + timeoutMs: 30000, + retryDelayMs: 300, + stableMs: 1000, + initialDelayMs: 300, + }).catch(() => null); + continue; + } + + if (pageState.successState === 'logged_in_home' || pageState.successState === 'oauth_consent' || pageState.successState === 'add_phone') { + return pageState; + } + + if (pageState.errorText) { + throw new Error(`步骤 5:资料提交后页面返回错误:${pageState.errorText}。URL: ${pageState.url || currentUrl || 'unknown'}`); + } + + if (pageState.profileVisible) { + throw new Error(`步骤 5:资料提交完成信号已收到,但页面仍停留在资料页,当前流程将直接报错。URL: ${pageState.url || currentUrl || 'unknown'}`); + } + + if (pageState.unknownAuthPage) { + throw new Error(`步骤 5:资料提交后进入未识别的认证页,无法确认成功。URL: ${pageState.url || currentUrl || 'unknown'}`); + } + + throw new Error(`步骤 5:资料提交后未能确认最终状态。URL: ${pageState.url || currentUrl || 'unknown'}`); + } +} + async function ensureStep8VerificationPageReady(options = {}) { const visibleStep = Number(options.visibleStep) || 8; const authLoginStep = Number(options.authLoginStep) || (visibleStep >= 11 ? 10 : 7); diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index ef15a39..5e1068a 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -125,7 +125,7 @@ function fallbackSignupProfilePageUrl(rawUrl) { const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; - return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(parsed.pathname || ''); + return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(parsed.pathname || ''); } function resolveSignupPostIdentityState(rawUrl) { diff --git a/background/verification-flow.js b/background/verification-flow.js index ad820e6..25fcb3d 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -159,7 +159,7 @@ if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { return false; } - return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || '')); + return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(String(parsed.pathname || '')); } catch { return false; } diff --git a/content/signup-page.js b/content/signup-page.js index d6b282f..6cf4c93 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -27,8 +27,10 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' || message.type === 'STEP8_TRIGGER_CONTINUE' || message.type === 'GET_LOGIN_AUTH_STATE' || message.type === 'SUBMIT_ADD_EMAIL' + || message.type === 'GET_STEP5_SUBMIT_STATE' || message.type === 'PREPARE_SIGNUP_VERIFICATION' || message.type === 'RECOVER_AUTH_RETRY_PAGE' + || message.type === 'RECOVER_STEP5_SUBMIT_RETRY_PAGE' || message.type === 'RESEND_VERIFICATION_CODE' || message.type === 'SUBMIT_PHONE_NUMBER' || message.type === 'SUBMIT_PHONE_VERIFICATION_CODE' @@ -88,10 +90,14 @@ async function handleCommand(message) { return serializeLoginAuthState(inspectLoginAuthState()); case 'SUBMIT_ADD_EMAIL': return await submitAddEmailAndContinue(message.payload); + case 'GET_STEP5_SUBMIT_STATE': + return getStep5SubmitState(); case 'PREPARE_SIGNUP_VERIFICATION': return await prepareSignupVerificationFlow(message.payload); case 'RECOVER_AUTH_RETRY_PAGE': return await recoverCurrentAuthRetryPage(message.payload); + case 'RECOVER_STEP5_SUBMIT_RETRY_PAGE': + return await recoverStep5SubmitRetryPage(message.payload); case 'RESEND_VERIFICATION_CODE': return await resendVerificationCode(message.step); case 'SUBMIT_PHONE_NUMBER': @@ -2820,7 +2826,7 @@ function isSignupProfilePageUrl(rawUrl = location.href) { if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { return false; } - return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || '')); + return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(String(parsed.pathname || '')); } catch { return false; } @@ -6422,6 +6428,7 @@ function getStep5ProfilePathPatterns() { /\/create-account\/profile(?:[/?#]|$)/i, /\/u\/signup\/profile(?:[/?#]|$)/i, /\/signup\/profile(?:[/?#]|$)/i, + /\/about-you(?:[/?#]|$)/i, ]; } @@ -6535,6 +6542,15 @@ function getStep5PostSubmitSuccessState() { } if (!isStep5ProfileStillVisible()) { + try { + const parsed = new URL(String(location.href || '').trim()); + const host = String(parsed.hostname || '').toLowerCase(); + if (['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { + return null; + } + } catch { + // Fall through to the generic "left_profile" success state. + } return { state: 'left_profile', url: location.href, @@ -6544,6 +6560,49 @@ function getStep5PostSubmitSuccessState() { return null; } +function getStep5SubmitState() { + const retryState = getStep5AuthRetryPageState(); + const successState = getStep5PostSubmitSuccessState(); + const errorText = typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : ''; + let signupAuthHost = false; + try { + const parsed = new URL(String(location.href || '').trim()); + signupAuthHost = ['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'] + .includes(String(parsed.hostname || '').toLowerCase()); + } catch { + signupAuthHost = false; + } + + return { + url: location.href, + retryPage: Boolean(retryState), + retryEnabled: Boolean(retryState?.retryEnabled), + maxCheckAttemptsBlocked: Boolean(retryState?.maxCheckAttemptsBlocked), + userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked), + successState: successState?.state || '', + profileVisible: isStep5ProfileStillVisible(), + errorText, + unknownAuthPage: Boolean( + signupAuthHost + && !retryState + && !successState + && !isStep5ProfileStillVisible() + ), + }; +} + +async function recoverStep5SubmitRetryPage(payload = {}) { + return recoverCurrentAuthRetryPage({ + ...payload, + flow: 'signup', + logLabel: payload?.logLabel || '步骤 5:资料提交后检测到认证重试页,正在点击“重试”恢复', + maxClickAttempts: payload?.maxClickAttempts ?? 2, + pathPatterns: Array.isArray(payload?.pathPatterns) ? payload.pathPatterns : getStep5AuthRetryPathPatterns(), + step: 5, + timeoutMs: payload?.timeoutMs ?? 12000, + }); +} + function installStep5NavigationCompletionReporter(completeOnce) { if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') { return () => {}; diff --git a/tests/background-step5-post-completion-validation.test.js b/tests/background-step5-post-completion-validation.test.js new file mode 100644 index 0000000..d7ae08d --- /dev/null +++ b/tests/background-step5-post-completion-validation.test.js @@ -0,0 +1,140 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background.js', 'utf8'); + +function extractFunction(name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map((marker) => source.indexOf(marker)) + .find((index) => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < source.length; i += 1) { + const ch = source[i]; + if (ch === '(') { + parenDepth += 1; + } else if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < source.length; end += 1) { + const ch = source[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + +test('step 5 post-completion validation recovers about-you retry page before allowing success', async () => { + const api = new Function(` +const logs = []; +const messages = []; +let stateReadCount = 0; + +const chrome = { + tabs: { + async get() { + return { url: 'https://auth.openai.com/about-you' }; + }, + }, +}; + +async function sendToContentScriptResilient(source, message) { + messages.push({ source, type: message.type }); + if (message.type === 'GET_STEP5_SUBMIT_STATE') { + stateReadCount += 1; + if (stateReadCount === 1) { + return { + retryPage: true, + retryEnabled: true, + maxCheckAttemptsBlocked: false, + userAlreadyExistsBlocked: false, + successState: '', + profileVisible: false, + errorText: '', + unknownAuthPage: false, + url: 'https://auth.openai.com/about-you', + }; + } + return { + retryPage: false, + retryEnabled: false, + maxCheckAttemptsBlocked: false, + userAlreadyExistsBlocked: false, + successState: 'logged_in_home', + profileVisible: false, + errorText: '', + unknownAuthPage: false, + url: 'https://chatgpt.com/', + }; + } + if (message.type === 'RECOVER_STEP5_SUBMIT_RETRY_PAGE') { + return { recovered: true, clickCount: 1 }; + } + throw new Error('unexpected message type: ' + message.type); +} + +async function addLog(message, level, meta) { + logs.push({ message, level, meta }); +} + +async function waitForTabStableComplete() {} + +${extractFunction('parseUrlSafely')} +${extractFunction('isSignupEntryHost')} +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} +${extractFunction('getStep5SubmitStateFromContent')} +${extractFunction('recoverStep5SubmitRetryPageOnTab')} +${extractFunction('validateStep5PostCompletion')} + +return { + async run() { + return validateStep5PostCompletion(99, {}); + }, + snapshot() { + return { logs, messages, stateReadCount }; + }, +}; +`)(); + + const result = await api.run(); + const snapshot = api.snapshot(); + + assert.equal(result.successState, 'logged_in_home'); + assert.deepStrictEqual( + snapshot.messages.map(({ type }) => type), + ['GET_STEP5_SUBMIT_STATE', 'RECOVER_STEP5_SUBMIT_RETRY_PAGE', 'GET_STEP5_SUBMIT_STATE'] + ); + assert.equal(snapshot.stateReadCount, 2); + assert.equal( + snapshot.logs.some(({ message }) => /检测到认证重试页/.test(message)), + true + ); +}); diff --git a/tests/step5-direct-complete.test.js b/tests/step5-direct-complete.test.js index 6430f58..953b2be 100644 --- a/tests/step5-direct-complete.test.js +++ b/tests/step5-direct-complete.test.js @@ -875,3 +875,111 @@ return { }); assert.equal(api.snapshot().recoverCalls, 1); }); + +test('step 5 profile url helper treats about-you as profile page', () => { + const api = new Function(` +${extractFunction('isSignupProfilePageUrl')} + +return { + run(url) { + return isSignupProfilePageUrl(url); + }, +}; +`)(); + + assert.equal(api.run('https://auth.openai.com/about-you'), true); + assert.equal(api.run('https://auth.openai.com/create-account/profile'), true); + assert.equal(api.run('https://chatgpt.com/about-you'), false); +}); + +test('step 5 recovers about-you auth retry page after profile submit', async () => { + const api = new Function(` +let retryVisible = true; +let recoverCalls = 0; +const location = { + href: 'https://auth.openai.com/about-you', + pathname: '/about-you', +}; +const document = { + querySelector() { return null; }, + querySelectorAll() { return []; }, +}; + +function throwIfStopped() {} +function log() {} +async function sleep() {} +async function humanPause() {} +function simulateClick() {} +function isVisibleElement() { return true; } +function isActionEnabled() { return true; } +function getActionText(el) { return el?.textContent || ''; } +function getSignupAuthRetryPathPatterns() { return []; } +function getAuthTimeoutErrorPageState(options) { + const matchesAboutYou = Array.isArray(options?.pathPatterns) + && options.pathPatterns.some((pattern) => pattern.test(location.pathname)); + if (!retryVisible || !matchesAboutYou) return null; + return { + retryEnabled: true, + userAlreadyExistsBlocked: false, + maxCheckAttemptsBlocked: false, + }; +} +async function recoverCurrentAuthRetryPage() { + recoverCalls += 1; + retryVisible = false; + location.href = 'https://chatgpt.com/'; + location.pathname = '/'; +} +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); } +function getStep5ErrorText() { return ''; } +function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); } +function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); } +function isOAuthConsentPage() { return false; } +function isAddPhonePageReady() { return false; } + +${extractFunction('isSignupProfilePageUrl')} +${getStep5OutcomeBundle()} + +return { + run() { + return waitForStep5SubmitOutcome({ timeoutMs: 1000 }); + }, + snapshot() { + return { recoverCalls }; + }, +}; +`)(); + + const result = await api.run(); + + assert.deepStrictEqual(result, { + state: 'logged_in_home', + url: 'https://chatgpt.com/', + }); + assert.equal(api.snapshot().recoverCalls, 1); +}); + +test('step 5 does not treat unknown auth page as left_profile success', () => { + const api = new Function(` +const location = { + href: 'https://auth.openai.com/unexpected-state', +}; + +function getStep5AuthRetryPageState() { return null; } +function isLikelyLoggedInChatgptHomeUrl() { return false; } +function isOAuthConsentPage() { return false; } +function isAddPhonePageReady() { return false; } +function isStep5ProfileStillVisible() { return false; } + +${extractFunction('getStep5PostSubmitSuccessState')} + +return { + run() { + return getStep5PostSubmitSuccessState(); + }, +}; +`)(); + + assert.equal(api.run(), null); +});