From 21b156291dd8ef178d3650d7340457fe88d83763 Mon Sep 17 00:00:00 2001 From: QLHazyCoder Date: Mon, 4 May 2026 15:41:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7=E6=B3=A8=E5=86=8C=E6=B5=81=E7=A8=8B=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=89=8B=E6=9C=BA=E5=8F=B7=E8=BE=93=E5=85=A5=E9=A1=B5?= =?UTF-8?q?=E5=B0=B1=E7=BB=AA=E5=90=8E=E5=86=8D=E7=94=B3=E8=AF=B7=E5=8F=B7?= =?UTF-8?q?=E7=A0=81=E5=B9=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/phone-verification-flow.js | 20 +- background/steps/submit-signup-email.js | 114 +++++++++-- content/signup-page.js | 66 +++++++ .../background-signup-step2-branching.test.js | 82 +++++++- tests/signup-step2-email-switch.test.js | 185 ++++++++++++++++++ tests/signup-verification-state-guard.test.js | 65 ++++++ 项目完整链路说明.md | 2 +- 项目文件结构说明.md | 2 +- 8 files changed, 512 insertions(+), 24 deletions(-) diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index a41506f..f46ecae 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -99,6 +99,10 @@ return step > 0 ? step : null; } + function getActivePhoneVerificationVisibleStep(fallback = 9) { + return normalizeLogStep(activePhoneVerificationLogStep) || fallback; + } + function normalizePhoneVerificationLogMessage(message) { return String(message || '') .replace(/^Step\s+9\s+diagnostics\s*:\s*/i, 'diagnostics: ') @@ -2122,7 +2126,7 @@ ? config.countryCandidates : []; if (!allCountryCandidates.length) { - throw new Error('Step 9: 5sim countries are empty. Please select at least one country in 接码设置。'); + throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: 5sim countries are empty. Please select at least one country in 接码设置。`); } const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) @@ -2610,7 +2614,7 @@ ? config.countryCandidates : resolveNexSmsCountryCandidates(state); if (!allCountryCandidates.length) { - throw new Error('Step 9: NexSMS countries are empty. Please select at least one country in 接码设置。'); + throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: NexSMS countries are empty. Please select at least one country in 接码设置。`); } const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) @@ -2872,7 +2876,7 @@ ? config.countryCandidates : resolveCountryCandidates(state); if (!allCountryCandidates.length) { - throw new Error('Step 9: HeroSMS countries are empty. Please select at least one country in 接码设置。'); + throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: HeroSMS countries are empty. Please select at least one country in 接码设置。`); } const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) @@ -2916,10 +2920,6 @@ `步骤 9:HeroSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`, 'info' ); - await addLog( - `步骤 9:HeroSMS 正在获取手机号(第 ${round}/${maxAcquireRounds} 轮)...`, - 'info' - ); } const countryAttempts = countryCandidates.map((countryConfig, index) => ({ @@ -3868,7 +3868,7 @@ (provider === PHONE_SMS_PROVIDER_5SIM || provider === PHONE_SMS_PROVIDER_NEXSMS) && !countryCandidates.length ) { - throw new Error(`Step 9: ${provider === PHONE_SMS_PROVIDER_5SIM ? '5sim' : 'NexSMS'} countries are empty. Please select at least one country in 接码设置。`); + throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: ${provider === PHONE_SMS_PROVIDER_5SIM ? '5sim' : 'NexSMS'} countries are empty. Please select at least one country in 接码设置。`); } const normalizeCountryKey = (value) => ( provider === PHONE_SMS_PROVIDER_5SIM @@ -4064,9 +4064,9 @@ const skippedSuffix = skippedFallbackProviders.length ? ` | skipped fallback providers: ${skippedFallbackProviders.join('; ')}` : ''; - throw new Error(`Step 9: all provider candidates failed to acquire number. ${providerErrors.join(' | ')}${skippedSuffix}`); + throw new Error(`Step ${getActivePhoneVerificationVisibleStep()}: all provider candidates failed to acquire number. ${providerErrors.join(' | ')}${skippedSuffix}`); } - throw lastProviderError || new Error('Step 9: failed to acquire phone activation.'); + throw lastProviderError || new Error(`Step ${getActivePhoneVerificationVisibleStep()}: failed to acquire phone activation.`); } async function prepareSignupPhoneActivation(state = {}, options = {}) { diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index 431b864..d0c2524 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -29,6 +29,11 @@ return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/i.test(message); } + function isSignupPhoneEntryUnavailableErrorMessage(errorLike) { + const message = getErrorMessage(errorLike); + return /未找到可用的手机号输入入口|当前页面没有可用的手机号注册入口,也不在密码页/i.test(message); + } + function isRetryableStep2TransportErrorMessage(errorLike) { const message = getErrorMessage(errorLike); return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message); @@ -58,17 +63,58 @@ } } - async function shouldForceAuthEntryRetry(tabId) { - if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') { - return false; + function isReadySignupEntryState(state = '') { + const normalized = String(state || '').trim().toLowerCase(); + return normalized === 'entry_home' + || normalized === 'email_entry' + || normalized === 'phone_entry' + || normalized === 'password_page'; + } + + async function getSignupEntryReadyState(tabId) { + if (!Number.isInteger(tabId) || typeof sendToContentScriptResilient !== 'function') { + return ''; } try { - const tab = await chrome.tabs.get(tabId); - return isLikelyLoggedInChatgptHomeUrl(tab?.url); + const result = await sendToContentScriptResilient('signup-page', { + type: 'ENSURE_SIGNUP_ENTRY_READY', + step: 2, + source: 'background', + payload: {}, + }, { + timeoutMs: 12000, + retryDelayMs: 500, + logMessage: '步骤 2:正在检查官网注册入口状态...', + }); + if (result?.error) { + return ''; + } + return String(result?.state || '').trim().toLowerCase(); } catch { + return ''; + } + } + + async function isLikelyLoggedInChatgptHomeTab(tabId) { + if (typeof chrome?.tabs?.get !== 'function') { return false; } + + const readyState = await getSignupEntryReadyState(tabId); + if (isReadySignupEntryState(readyState)) { + return false; + } + + const currentUrl = await getTabUrl(tabId); + return isLikelyLoggedInChatgptHomeUrl(currentUrl); + } + + async function shouldForceAuthEntryRetry(tabId) { + if (!Number.isInteger(tabId)) { + return false; + } + return isLikelyLoggedInChatgptHomeTab(tabId); } async function getTabUrl(tabId) { @@ -85,8 +131,7 @@ } async function failStep2OnLoggedInSession(tabId, reasonMessage = '') { - const currentUrl = await getTabUrl(tabId); - if (!isLikelyLoggedInChatgptHomeUrl(currentUrl)) { + if (!(await isLikelyLoggedInChatgptHomeTab(tabId))) { return false; } @@ -120,6 +165,28 @@ } } + async function ensureSignupPhoneEntryReady(tabId) { + if (!Number.isInteger(tabId)) { + throw new Error('步骤 2:未找到可用的注册页标签,无法切换到手机号注册入口。'); + } + + const result = await sendToContentScriptResilient('signup-page', { + type: 'ENSURE_SIGNUP_PHONE_ENTRY_READY', + step: 2, + source: 'background', + payload: {}, + }, { + timeoutMs: 30000, + retryDelayMs: 700, + logMessage: '步骤 2:正在打开官网注册入口并切换到手机号注册...', + }); + + if (result?.error) { + throw new Error(result.error); + } + return result || {}; + } + async function submitSignupEmail(resolvedEmail, options = {}) { return sendSignupIdentity({ email: resolvedEmail }, options); } @@ -174,6 +241,26 @@ } } + try { + await ensureSignupPhoneEntryReady(signupTabId); + } catch (entryError) { + const entryErrorMessage = getErrorMessage(entryError); + if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) { + return; + } + if ( + isSignupPhoneEntryUnavailableErrorMessage(entryErrorMessage) + || isSignupEntryUnavailableErrorMessage(entryErrorMessage) + || isRetryableStep2TransportErrorMessage(entryErrorMessage) + ) { + await addLog('步骤 2:手机号注册入口尚未就绪,正在重新打开官网入口后重试一次...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + await ensureSignupPhoneEntryReady(signupTabId); + } else { + throw entryError; + } + } + const activation = await phoneVerificationHelpers.prepareSignupPhoneActivation(state); let step2Result = await submitSignupPhone(activation.phoneNumber, activation, { timeoutMs: 45000, @@ -183,13 +270,18 @@ if (step2Result?.error) { const errorMessage = getErrorMessage(step2Result.error); - if (isSignupEntryUnavailableErrorMessage(errorMessage) || isRetryableStep2TransportErrorMessage(errorMessage)) { - await addLog('步骤 2:手机号注册入口不可用或通信超时,正在切换认证入口页后重试一次...', 'warn'); - signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + if ( + isSignupPhoneEntryUnavailableErrorMessage(errorMessage) + || isSignupEntryUnavailableErrorMessage(errorMessage) + || isRetryableStep2TransportErrorMessage(errorMessage) + ) { + await addLog('步骤 2:手机号注册入口不可用或通信超时,正在重新准备手机号注册入口后重试一次...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + await ensureSignupPhoneEntryReady(signupTabId); step2Result = await submitSignupPhone(activation.phoneNumber, activation, { timeoutMs: 45000, retryDelayMs: 700, - logMessage: '步骤 2:认证入口页已打开,正在重新提交手机号...', + logMessage: '步骤 2:手机号注册入口已就绪,正在重新提交手机号...', }); } } diff --git a/content/signup-page.js b/content/signup-page.js index bb5fd2a..c309cb1 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -26,6 +26,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1' || message.type === 'RESEND_PHONE_VERIFICATION_CODE' || message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'ENSURE_SIGNUP_ENTRY_READY' + || message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY' || message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY' ) { resetStopState(); @@ -97,6 +98,8 @@ async function handleCommand(message) { return await phoneAuthHelpers.returnToAddPhone(); case 'ENSURE_SIGNUP_ENTRY_READY': return await ensureSignupEntryReady(); + case 'ENSURE_SIGNUP_PHONE_ENTRY_READY': + return await ensureSignupPhoneEntryReady(); case 'ENSURE_SIGNUP_PASSWORD_PAGE_READY': return await ensureSignupPasswordPageReady(); case 'STEP8_FIND_AND_CLICK': @@ -1041,6 +1044,23 @@ async function ensureSignupEntryReady(timeout = 15000) { throw new Error('当前页面没有可用的注册入口,也不在邮箱/密码页。URL: ' + location.href); } +async function ensureSignupPhoneEntryReady(timeout = 25000) { + const snapshot = await waitForSignupPhoneEntryState({ timeout, step: 2 }); + if ( + (snapshot.state === 'phone_entry' && snapshot.phoneInput) + || snapshot.state === 'password_page' + ) { + return { + ready: true, + state: snapshot.state, + url: snapshot.url || location.href, + }; + } + + log(`手机号注册入口识别失败,诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn'); + throw new Error('当前页面没有可用的手机号注册入口,也不在密码页。URL: ' + location.href); +} + async function ensureSignupPasswordPageReady(timeout = 20000) { const start = Date.now(); @@ -1540,6 +1560,52 @@ function isLikelyLoggedInChatgptHomeUrl(rawUrl = location.href) { return false; } + const signupTrigger = typeof findSignupEntryTrigger === 'function' + ? findSignupEntryTrigger() + : null; + if (signupTrigger) { + return false; + } + + if (typeof document !== 'undefined' && document && typeof document.querySelectorAll === 'function') { + const loginActionPattern = /登录|log\s*in|sign\s*in/i; + const candidates = document.querySelectorAll( + 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + ); + + for (const el of candidates) { + 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(); + if (!text || !loginActionPattern.test(text)) { + continue; + } + + const visible = typeof isVisibleElement === 'function' + ? isVisibleElement(el) + : true; + if (!visible) { + continue; + } + + const enabled = typeof isActionEnabled === 'function' + ? isActionEnabled(el) + : (Boolean(el) && !el.disabled && el?.getAttribute?.('aria-disabled') !== 'true'); + if (enabled) { + return false; + } + } + } + return true; } catch { return false; diff --git a/tests/background-signup-step2-branching.test.js b/tests/background-signup-step2-branching.test.js index bac440d..fc1bccf 100644 --- a/tests/background-signup-step2-branching.test.js +++ b/tests/background-signup-step2-branching.test.js @@ -90,6 +90,7 @@ test('step 2 keeps password flow when landing on password page', async () => { test('step 2 uses phone activation when resolved signup method is phone', async () => { const completedPayloads = []; + const sequence = []; const sentPayloads = []; const activation = { activationId: 'signup-activation', @@ -120,7 +121,10 @@ test('step 2 uses phone activation when resolved signup method is phone', async getTabId: async () => 14, isTabAlive: async () => true, phoneVerificationHelpers: { - prepareSignupPhoneActivation: async () => activation, + prepareSignupPhoneActivation: async () => { + sequence.push('prepareSignupPhoneActivation'); + return activation; + }, cancelSignupPhoneActivation: async () => { throw new Error('activation should not be cancelled on success'); }, @@ -130,6 +134,15 @@ test('step 2 uses phone activation when resolved signup method is phone', async throw new Error('email resolver should not run for phone signup'); }, sendToContentScriptResilient: async (_source, message) => { + if (message.type === 'ENSURE_SIGNUP_PHONE_ENTRY_READY') { + sequence.push('ensureSignupPhoneEntryReady'); + return { + ready: true, + state: 'phone_entry', + url: 'https://chatgpt.com/', + }; + } + sequence.push('submitSignupPhone'); sentPayloads.push(message.payload); return { submitted: true }; }, @@ -138,6 +151,11 @@ test('step 2 uses phone activation when resolved signup method is phone', async await executor.executeStep2({ signupMethod: 'phone' }); + assert.deepStrictEqual(sequence, [ + 'ensureSignupPhoneEntryReady', + 'prepareSignupPhoneActivation', + 'submitSignupPhone', + ]); assert.deepStrictEqual(sentPayloads, [ { signupMethod: 'phone', @@ -204,6 +222,68 @@ test('step 2 stops with an explicit error instead of silently skipping 3/4/5 on assert.ok(logs.some((item) => /3\/4\/5/.test(item.message))); }); +test('step 2 does not force auth-entry retry on logged-out chatgpt home when content reports entry_home', async () => { + const completedPayloads = []; + const logs = []; + const sentPayloads = []; + let authEntryCalls = 0; + + const executor = step2Api.createStep2Executor({ + addLog: async (message, level = 'info') => { + logs.push({ message, level }); + }, + chrome: { + tabs: { + update: async () => {}, + get: async () => ({ url: 'https://chatgpt.com/' }), + }, + }, + completeStepFromBackground: async (step, payload) => { + completedPayloads.push({ step, payload }); + }, + ensureContentScriptReadyOnTab: async () => {}, + ensureSignupAuthEntryPageReady: async () => { + authEntryCalls += 1; + return { tabId: 15 }; + }, + ensureSignupEntryPageReady: async () => ({ tabId: 15 }), + ensureSignupPostEmailPageReadyInTab: async () => ({ + state: 'password_page', + url: 'https://auth.openai.com/create-account/password', + }), + getTabId: async () => 15, + isTabAlive: async () => true, + resolveSignupEmailForFlow: async () => 'user@example.com', + sendToContentScriptResilient: async (_source, message) => { + if (message.type === 'ENSURE_SIGNUP_ENTRY_READY') { + return { ready: true, state: 'entry_home', url: 'https://chatgpt.com/' }; + } + sentPayloads.push(message.payload); + return { submitted: true }; + }, + SIGNUP_PAGE_INJECT_FILES: [], + }); + + await executor.executeStep2({ email: 'user@example.com' }); + + assert.equal(authEntryCalls, 0); + assert.deepStrictEqual(sentPayloads, [{ email: 'user@example.com' }]); + assert.deepStrictEqual(completedPayloads, [ + { + step: 2, + payload: { + email: 'user@example.com', + accountIdentifierType: 'email', + accountIdentifier: 'user@example.com', + nextSignupState: 'password_page', + nextSignupUrl: 'https://auth.openai.com/create-account/password', + skippedPasswordStep: false, + }, + }, + ]); + assert.equal(logs.some((item) => /已登录 ChatGPT 首页/.test(item.message)), false); +}); + test('signup flow helper recognizes email verification page as post-email landing page', async () => { let ensureCalls = 0; let passwordReadyChecks = 0; diff --git a/tests/signup-step2-email-switch.test.js b/tests/signup-step2-email-switch.test.js index d231908..48dbfd2 100644 --- a/tests/signup-step2-email-switch.test.js +++ b/tests/signup-step2-email-switch.test.js @@ -440,6 +440,191 @@ return { assert.equal(api.run()?.kind, 'localized-email'); }); +test('ensureSignupPhoneEntryReady opens free signup before switching to the phone entry', async () => { + const api = new Function(` +const logs = []; +const clicks = []; +let phase = 'entry'; +let now = 0; + +const signupButton = { + textContent: '免费注册', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, + getBoundingClientRect() { + return { width: 120, height: 36 }; + }, +}; + +const switchButton = { + textContent: 'Continue with phone number', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 48 }; + }, +}; + +const emailInput = { + kind: 'email', + getAttribute(name) { + if (name === 'type') return 'email'; + return ''; + }, +}; + +const phoneInput = { + kind: 'phone', + getAttribute(name) { + if (name === 'type') return 'tel'; + return ''; + }, +}; + +const document = { + querySelector(selector) { + if (selector === SIGNUP_EMAIL_INPUT_SELECTOR) { + return phase === 'email' ? emailInput : null; + } + if (selector === SIGNUP_PHONE_INPUT_SELECTOR) { + return phase === 'phone' ? phoneInput : null; + } + return null; + }, + querySelectorAll(selector) { + if (selector === 'button, a, [role="button"], [role="link"]') { + return phase === 'email' ? [switchButton] : []; + } + if (selector === 'a, button, [role="button"], [role="link"]') { + return phase === 'entry' ? [signupButton] : []; + } + if (selector === 'input') { + if (phase === 'email') return [emailInput]; + if (phase === 'phone') return [phoneInput]; + return []; + } + return []; + }, +}; + +const location = { + href: 'https://chatgpt.com/', +}; + +const Date = { + now() { + return now; + }, +}; + +${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')} +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} +${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')} +${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')} +${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')} +${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')} +${extractConst('SIGNUP_WORK_EMAIL_PATTERN')} +${extractConst('SIGNUP_PHONE_ACTION_PATTERN')} +${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')} +${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')} + +function isVisibleElement(el) { + return Boolean(el); +} + +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; +} + +function getActionText(el) { + return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')] + .filter(Boolean) + .join(' ') + .replace(/\\s+/g, ' ') + .trim(); +} + +function getSignupPasswordInput() { + return null; +} + +function isSignupPasswordPage() { + return false; +} + +function getSignupPasswordSubmitButton() { + return null; +} + +function getSignupPasswordDisplayedEmail() { + return ''; +} + +function getPageTextSnapshot() { + return phase === 'entry' ? '登录 免费注册' : ''; +} + +function throwIfStopped() {} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function humanPause() {} + +function simulateClick(target) { + clicks.push(getActionText(target)); + if (target === signupButton) { + phase = 'email'; + } else if (target === switchButton) { + phase = 'phone'; + } +} + +async function sleep(ms) { + now += ms; +} + +${extractFunction('getSignupEmailInput')} +${extractFunction('getSignupPhoneInput')} +${extractFunction('findSignupUseEmailTrigger')} +${extractFunction('findSignupUsePhoneTrigger')} +${extractFunction('findSignupMoreOptionsTrigger')} +${extractFunction('getSignupEmailContinueButton')} +${extractFunction('findSignupEntryTrigger')} +${extractFunction('inspectSignupEntryState')} +${extractFunction('waitForSignupPhoneEntryState')} +function getSignupEntryDiagnostics() { return {}; } +${extractFunction('ensureSignupPhoneEntryReady')} + +return { + async run() { + return ensureSignupPhoneEntryReady(); + }, + getClicks() { + return clicks.slice(); + }, +}; +`)(); + + const result = await api.run(); + + assert.deepEqual(result, { + ready: true, + state: 'phone_entry', + url: 'https://chatgpt.com/', + }); + assert.deepEqual(api.getClicks(), ['免费注册', 'Continue with phone number']); +}); + test('submitSignupPhoneNumberAndContinue switches from email mode to phone mode and submits local number', async () => { const api = new Function(` const logs = []; diff --git a/tests/signup-verification-state-guard.test.js b/tests/signup-verification-state-guard.test.js index d1e32cc..be7a2c8 100644 --- a/tests/signup-verification-state-guard.test.js +++ b/tests/signup-verification-state-guard.test.js @@ -304,3 +304,68 @@ return { state: 'verification', }); }); + +test('logged-out chatgpt homepage with signup and login actions is not treated as logged-in home', () => { + const api = new Function(` +const location = { + href: 'https://chatgpt.com/', +}; + +const signupButton = { + textContent: '免费注册', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, +}; + +const loginButton = { + textContent: '登录', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, +}; + +const document = { + querySelectorAll(selector) { + if (selector === 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]') { + return [signupButton, loginButton]; + } + return []; + }, +}; + +function findSignupEntryTrigger() { + return signupButton; +} + +function getActionText(el) { + return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')] + .filter(Boolean) + .join(' ') + .replace(/\\s+/g, ' ') + .trim(); +} + +function isVisibleElement() { + return true; +} + +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; +} + +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} + +return { + run() { + return isLikelyLoggedInChatgptHomeUrl(); + }, +}; +`)(); + + assert.equal(api.run(), false); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index c935da9..5f01a26 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -319,7 +319,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 2. 打开或复用注册页 3. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式 4. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label` -5. 手机号注册分支:从接码平台申请号码,切换到手机号注册入口,必要时展开更多选项,填写手机号并提交 +5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,再从接码平台申请号码,填写手机号并提交 6. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号 7. 等待身份提交后的真实落地页 8. 如果进入密码页,则继续执行 Step 3 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 7138cad..36ef5f2 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -76,7 +76,7 @@ - `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。 - `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。 - `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。 -- `background/steps/submit-signup-email.js`:步骤 2 实现,负责按 `signupMethod` 在邮箱注册与手机号注册之间分发;邮箱分支继续提交邮箱,手机号分支会申请接码平台号码、切换手机号注册入口并提交手机号,再根据落地页判断是否跳过后续密码步骤。 +- `background/steps/submit-signup-email.js`:步骤 2 实现,负责按 `signupMethod` 在邮箱注册与手机号注册之间分发;邮箱分支会先点击官网“免费注册”再提交邮箱,手机号分支会先点击官网“免费注册”并切到手机号注册入口,确认手机号输入页就绪后再申请接码平台号码并提交手机号,最后根据落地页判断是否跳过后续密码步骤。 ## `content/`