diff --git a/background/logging-status.js b/background/logging-status.js index b3de763..d351908 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -121,6 +121,8 @@ return '登录超时报错页'; case 'oauth_consent_page': return 'OAuth 授权页'; + case 'choose_account_page': + return 'OpenAI choose account page'; case 'add_phone_page': return '手机号页'; case 'add_email_page': diff --git a/core/flow-kernel/logging-status.js b/core/flow-kernel/logging-status.js index b3de763..d351908 100644 --- a/core/flow-kernel/logging-status.js +++ b/core/flow-kernel/logging-status.js @@ -121,6 +121,8 @@ return '登录超时报错页'; case 'oauth_consent_page': return 'OAuth 授权页'; + case 'choose_account_page': + return 'OpenAI choose account page'; case 'add_phone_page': return '手机号页'; case 'add_email_page': diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index cde7b69..0defb8a 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -210,6 +210,27 @@ const LOGIN_EXTERNAL_IDP_PATTERN = /google|microsoft|apple|sso|single\s+sign[-\s const LOGIN_CODE_ONLY_ACTION_PATTERN = /one[-\s]*time|passcode|use\s+(?:a\s+)?code|验证码|一次性|ワンタイム|パスコード|認証コード|確認コード/i; const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|(?:コード|メール|確認コード|認証コード)(?:を)?再送信|再送信|新しい(?:コード|確認コード|認証コード)|届かない|受信していません|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i; +const CHOOSE_ACCOUNT_PAGE_PATTERN = new RegExp([ + String.raw`choose\s+(?:an?\s+)?account`, + String.raw`select\s+(?:an?\s+)?account`, + String.raw`welcome\s+back`, + String.raw`\u9009\u62e9(?:\u4e00\u4e2a)?(?:\u5e10\u6237|\u8d26\u6237|\u8d26\u53f7)`, + String.raw`\u6b22\u8fce\u56de\u6765`, + String.raw`\u30a2\u30ab\u30a6\u30f3\u30c8.*(?:\u9078\u629e|\u9078\u3093\u3067)`, +].join('|'), 'i'); +const CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN = /remove|delete|forget|close|dismiss|trash|\u79fb\u9664|\u5220\u9664|\u522a\u9664|\u524a\u9664|\u9589\u3058\u308b|\u524a\u9664/i; +const CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN = new RegExp([ + String.raw`another\s+account`, + String.raw`different\s+account`, + String.raw`other\s+account`, + String.raw`use\s+(?:a\s+)?different`, + String.raw`sign\s*in\s+(?:with\s+)?(?:another|different)`, + String.raw`log\s*in\s+(?:with\s+)?(?:another|different)`, + String.raw`\u5176\u4ed6(?:\u5e10\u6237|\u8d26\u6237|\u8d26\u53f7)`, + String.raw`\u53e6\u4e00\u4e2a(?:\u5e10\u6237|\u8d26\u6237|\u8d26\u53f7)`, + String.raw`\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8`, +].join('|'), 'i'); +const CHOOSE_ACCOUNT_ACTION_SELECTOR = 'button, a, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])'; const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::'; const CONTACT_VERIFICATION_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i; @@ -3039,6 +3060,123 @@ function getPhoneVerificationDisplayedPhone() { return matches[0] ? String(matches[0]).replace(/\s+/g, ' ').trim() : ''; } +function normalizeAuthAccountIdentifier(value) { + return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); +} + +function getChooseAccountCandidateText(element) { + const parts = [ + element?.textContent, + element?.value, + element?.getAttribute?.('aria-label'), + element?.getAttribute?.('title'), + element?.getAttribute?.('data-testid'), + element?.getAttribute?.('data-test-id'), + ]; + return parts + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function isChooseAccountPage() { + const path = `${location.pathname || ''} ${location.href || ''}`; + if (/\/choose-an-account(?:[/?#]|$)/i.test(path)) { + return true; + } + const pageText = getPageTextSnapshot(); + if (!CHOOSE_ACCOUNT_PAGE_PATTERN.test(pageText)) { + return false; + } + return Boolean(findChooseAccountButtonForEmail(getLoginVerificationDisplayedEmail())); +} + +function isChooseAccountRemovalAction(element) { + if (!element) return false; + const text = getChooseAccountCandidateText(element); + const role = String(element.getAttribute?.('role') || '').trim().toLowerCase(); + return CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN.test(text) + || role === 'menuitem' + || role === 'switch'; +} + +function resolveChooseAccountClickTarget(element) { + if (!element || isChooseAccountRemovalAction(element)) { + return null; + } + + const tag = String(element.tagName || '').trim().toLowerCase(); + const role = String(element.getAttribute?.('role') || '').trim().toLowerCase(); + const tabIndex = Number(element.getAttribute?.('tabindex') ?? element.tabIndex ?? NaN); + const actionable = tag === 'button' + || tag === 'a' + || role === 'button' + || role === 'link' + || (Number.isFinite(tabIndex) && tabIndex >= 0); + + if (actionable && isActionEnabled(element) && isVisibleElement(element)) { + return element; + } + + const closestAction = element.closest?.(CHOOSE_ACCOUNT_ACTION_SELECTOR) || null; + if ( + closestAction + && closestAction !== element + && isActionEnabled(closestAction) + && isVisibleElement(closestAction) + && !isChooseAccountRemovalAction(closestAction) + ) { + return closestAction; + } + + return null; +} + +function findChooseAccountButtonForEmail(email) { + const normalizedEmail = normalizeAuthAccountIdentifier(email); + if (!normalizedEmail || !normalizedEmail.includes('@')) { + return null; + } + + const candidates = Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR)) + .filter((element) => isVisibleElement(element) && isActionEnabled(element)); + + for (const candidate of candidates) { + if (isChooseAccountRemovalAction(candidate)) continue; + const text = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(candidate)); + if (!text || !text.includes(normalizedEmail)) continue; + if (CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text)) continue; + const target = resolveChooseAccountClickTarget(candidate); + if (target) { + return target; + } + } + + const emailNodes = Array.from(document.querySelectorAll('body *')) + .filter((element) => { + if (!isVisibleElement(element)) return false; + const text = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(element)); + return text.includes(normalizedEmail); + }); + + for (const node of emailNodes) { + let current = node; + while (current && current !== document.body) { + const target = resolveChooseAccountClickTarget(current); + if (target) { + const targetText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(target)); + if (targetText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(targetText)) { + return target; + } + } + current = current.parentElement; + } + } + + return null; +} + function getOAuthConsentForm() { return document.querySelector(OAUTH_CONSENT_FORM_SELECTOR); } @@ -4194,6 +4332,7 @@ function inspectLoginAuthState() { const phoneVerificationPage = isPhoneVerificationPageReady(); const consentReady = isStep8Ready(); const oauthConsentPage = isOAuthConsentPage(); + const chooseAccountPage = isChooseAccountPage(); const baseState = { state: 'unknown', url: location.href, @@ -4220,6 +4359,7 @@ function inspectLoginAuthState() { phoneVerificationPage, oauthConsentPage, consentReady, + chooseAccountPage, }; if (retryState) { @@ -4279,6 +4419,13 @@ function inspectLoginAuthState() { }; } + if (chooseAccountPage) { + return { + ...baseState, + state: 'choose_account_page', + }; + } + if (verificationVisible) { return { ...baseState, @@ -4330,6 +4477,7 @@ function serializeLoginAuthState(snapshot) { phoneVerificationPage: Boolean(snapshot?.phoneVerificationPage), oauthConsentPage: Boolean(snapshot?.oauthConsentPage), consentReady: Boolean(snapshot?.consentReady), + chooseAccountPage: Boolean(snapshot?.chooseAccountPage), }; } @@ -4350,6 +4498,8 @@ function getLoginAuthStateLabel(snapshot) { return '登录超时报错页'; case 'oauth_consent_page': return 'OAuth 授权页'; + case 'choose_account_page': + return 'OpenAI choose account page'; case 'entry_page': return '登录入口页'; case 'add_phone_page': @@ -5617,6 +5767,113 @@ async function waitForLoginEntryOpenTransition(timeout = 10000) { return snapshot; } +async function waitForChooseAccountTransition(timeout = 15000) { + const start = Date.now(); + let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + if (snapshot.state !== 'unknown' && snapshot.state !== 'choose_account_page') { + return snapshot; + } + await sleep(250); + } + + return snapshot; +} + +async function step6ChooseExistingAccount(payload, snapshot) { + const performOperationWithDelay = typeof getOperationDelayRunner === 'function' + ? getOperationDelayRunner() + : async (metadata, operation) => { + const rootScope = typeof window !== 'undefined' ? window : globalThis; + const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay; + return typeof gate === 'function' ? gate(metadata, operation) : operation(); + }; + const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; + const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); + const loginIdentifierType = String(payload?.loginIdentifierType || '').trim(); + if (loginIdentifierType === 'phone') { + return createStep6RecoverableResult('choose_account_requires_email_identifier', currentSnapshot, { + message: 'OpenAI choose-account page requires an email account card; current login identifier is phone.', + }); + } + + const email = normalizeAuthAccountIdentifier(payload?.email || payload?.accountIdentifier || ''); + if (!email || !email.includes('@')) { + return createStep6RecoverableResult('missing_choose_account_email', currentSnapshot, { + message: 'OpenAI choose-account page is visible, but the target email is missing.', + }); + } + + const target = findChooseAccountButtonForEmail(email); + if (!target) { + return createStep6RecoverableResult('choose_account_target_not_found', currentSnapshot, { + message: `OpenAI choose-account page does not contain target email ${email}.`, + }); + } + + log(`Detected OpenAI choose-account page, selecting ${email}...`, 'info', { step: visibleStep, stepKey: 'oauth-login' }); + await humanPause(350, 900); + await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'choose-existing-account' }, async () => { + simulateClick(target); + }); + + const nextSnapshot = normalizeStep6Snapshot(await waitForChooseAccountTransition(15000)); + if (nextSnapshot.state === 'oauth_consent_page') { + return createStep6OAuthConsentSuccessResult(nextSnapshot, { + via: 'choose_account_oauth_consent_page', + }); + } + if (nextSnapshot.state === 'add_email_page') { + return createStep6AddEmailSuccessResult(nextSnapshot, { + via: 'choose_account_add_email_page', + }); + } + if (nextSnapshot.state === 'verification_page') { + return finalizeStep6VerificationReady({ + visibleStep, + loginVerificationRequestedAt: null, + via: 'choose_account_verification_page', + }); + } + if (nextSnapshot.state === 'phone_verification_page') { + return finalizeStep6VerificationReady({ + visibleStep, + loginVerificationRequestedAt: null, + via: 'choose_account_phone_verification_page', + allowPhoneVerificationPage: true, + }); + } + if (nextSnapshot.state === 'password_page') { + return step6LoginFromPasswordPage(payload, nextSnapshot); + } + if (nextSnapshot.state === 'email_page') { + return step6LoginFromEmailPage(payload, nextSnapshot); + } + if (nextSnapshot.state === 'phone_entry_page') { + return step6LoginFromPhonePage(payload, nextSnapshot); + } + if (nextSnapshot.state === 'login_timeout_error_page') { + const transition = await createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_after_choose_account', + nextSnapshot, + 'Clicking the existing OpenAI account entered the login timeout page.', + { visibleStep } + ); + if (transition.action === 'done') return transition.result; + if (transition.action === 'phone') return step6LoginFromPhonePage(payload, transition.snapshot); + if (transition.action === 'email') return step6LoginFromEmailPage(payload, transition.snapshot); + if (transition.action === 'password') return step6LoginFromPasswordPage(payload, transition.snapshot); + return transition.result; + } + + return createStep6RecoverableResult('choose_account_transition_stalled', nextSnapshot, { + message: `Clicked ${email} on OpenAI choose-account page, but the page did not enter a supported next state.`, + }); +} + async function waitForPhoneLoginEntrySwitchTransition(timeout = 10000) { const start = Date.now(); let snapshot = normalizeStep6Snapshot(inspectLoginAuthState()); @@ -6191,6 +6448,10 @@ async function step6_login(payload) { return step6LoginFromPasswordPage(payload, snapshot); } + if (snapshot.state === 'choose_account_page') { + return step6ChooseExistingAccount(payload, snapshot); + } + if (snapshot.state === 'entry_page') { return step6OpenLoginEntry(payload, snapshot); } diff --git a/tests/background-logging-status-module.test.js b/tests/background-logging-status-module.test.js index 186e1bd..c72eade 100644 --- a/tests/background-logging-status-module.test.js +++ b/tests/background-logging-status-module.test.js @@ -46,6 +46,7 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu 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('choose_account_page'), 'OpenAI choose account page'); assert.equal( loggingStatus.getErrorMessage(new Error('GPC_TASK_ENDED::GPC OTP 超时,请重新创建任务')), 'GPC OTP 超时,请重新创建任务' diff --git a/tests/step6-login-state.test.js b/tests/step6-login-state.test.js index b1d4630..ba2cb26 100644 --- a/tests/step6-login-state.test.js +++ b/tests/step6-login-state.test.js @@ -143,6 +143,10 @@ function isOAuthConsentPage() { return ${JSON.stringify(Boolean(overrides.oauthConsentPage))}; } +function isChooseAccountPage() { + return ${JSON.stringify(Boolean(overrides.chooseAccountPage))}; +} + ${bundle} return { @@ -287,6 +291,18 @@ return { assert.strictEqual(snapshot.state, 'entry_page'); } +{ + const api = createApi({ + pathname: '/choose-an-account', + href: 'https://auth.openai.com/choose-an-account', + chooseAccountPage: true, + }); + + const snapshot = api.inspectLoginAuthState(); + assert.strictEqual(snapshot.state, 'choose_account_page'); + assert.strictEqual(snapshot.chooseAccountPage, true); +} + { const api = createApi({ phoneInput: { id: 'phone' }, @@ -326,4 +342,9 @@ assert.ok( 'inspectLoginAuthState 应产出 add_email_page 状态' ); +assert.ok( + extractFunction('inspectLoginAuthState').includes("state: 'choose_account_page'"), + 'inspectLoginAuthState should produce choose_account_page state' +); + console.log('step6 login state tests passed'); diff --git a/tests/step6-oauth-consent-skip.test.js b/tests/step6-oauth-consent-skip.test.js index a7a1ce6..28c1873 100644 --- a/tests/step6-oauth-consent-skip.test.js +++ b/tests/step6-oauth-consent-skip.test.js @@ -51,6 +51,15 @@ function extractFunction(name) { return source.slice(start, end); } +function extractConst(name) { + const pattern = new RegExp(`const\\s+${name}\\s*=\\s*[\\s\\S]*?;`); + const match = source.match(pattern); + if (!match) { + throw new Error(`missing const ${name}`); + } + return match[0]; +} + test('password submit treats direct OAuth consent as a login-code skip', async () => { const api = new Function(` const location = { href: 'https://auth.openai.com/authorize' }; @@ -132,3 +141,232 @@ return { assert.equal(result.directOAuthConsentPage, true); assert.equal(logs.some(({ level }) => level === 'ok'), true); }); + +test('step 7 clicks matching choose-account card and skips login code after OAuth consent', async () => { + const api = new Function(` +let pageState = 'choose_account_page'; +const clicked = []; +const location = { + href: 'https://auth.openai.com/choose-an-account', + pathname: '/choose-an-account', +}; +const targetCard = { + id: 'target-card', + textContent: 'Tall Slept Fancy tall-slept-fancy@duck.com', + value: '', + parentElement: null, + disabled: false, + getAttribute(name) { + if (name === 'role') return 'button'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; +const removeButton = { + id: 'remove-button', + textContent: '', + value: '', + parentElement: targetCard, + disabled: false, + getAttribute(name) { + if (name === 'aria-label') return 'Remove tall-slept-fancy@duck.com'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; +const otherCard = { + id: 'other-card', + textContent: 'Other User other@example.com', + value: '', + parentElement: null, + disabled: false, + getAttribute(name) { + if (name === 'role') return 'button'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; + +const document = { + body: { + innerText: 'Welcome back Choose an account tall-slept-fancy@duck.com other@example.com', + textContent: 'Welcome back Choose an account tall-slept-fancy@duck.com other@example.com', + }, + querySelectorAll(selector) { + if (String(selector).includes('body *')) return [removeButton, targetCard, otherCard]; + return [removeButton, targetCard, otherCard]; + }, +}; + +function getOperationDelayRunner() { + return async (_metadata, operation) => operation(); +} +function isVisibleElement(element) { + return Boolean(element); +} +function isActionEnabled(element) { + return Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true'; +} +function simulateClick(element) { + clicked.push(element.id); + if (element === targetCard) { + pageState = 'oauth_consent_page'; + location.href = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent'; + location.pathname = '/sign-in-with-chatgpt/codex/consent'; + } +} +function inspectLoginAuthState() { + return { state: pageState, url: location.href, chooseAccountPage: pageState === 'choose_account_page' }; +} +function throwIfStopped() {} +async function sleep() {} +async function humanPause() {} +function log() {} +async function finalizeStep6VerificationReady() { return { routed: 'verification' }; } +async function step6LoginFromPasswordPage() { return { routed: 'password' }; } +async function step6LoginFromEmailPage() { return { routed: 'email' }; } +async function step6LoginFromPhonePage() { return { routed: 'phone' }; } +async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'timeout' } }; } + +${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractFunction('getPageTextSnapshot')} +${extractFunction('normalizeAuthAccountIdentifier')} +${extractFunction('getChooseAccountCandidateText')} +${extractFunction('isChooseAccountPage')} +${extractFunction('isChooseAccountRemovalAction')} +${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6OAuthConsentSuccessResult')} +${extractFunction('createStep6AddEmailSuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForChooseAccountTransition')} +${extractFunction('step6ChooseExistingAccount')} + +return { + clicked, + run() { + return step6ChooseExistingAccount( + { email: 'TALL-SLEPT-FANCY@DUCK.COM', loginIdentifierType: 'email', visibleStep: 7 }, + { state: 'choose_account_page', url: location.href } + ); + }, +}; +`)(); + + const result = await api.run(); + + assert.deepEqual(api.clicked, ['target-card']); + assert.equal(result.step6Outcome, 'success'); + assert.equal(result.state, 'oauth_consent_page'); + assert.equal(result.skipLoginVerificationStep, true); + assert.equal(result.directOAuthConsentPage, true); + assert.equal(result.via, 'choose_account_oauth_consent_page'); +}); + +test('step 7 does not click choose-account page when target email is missing', async () => { + const api = new Function(` +const clicked = []; +const location = { + href: 'https://auth.openai.com/choose-an-account', + pathname: '/choose-an-account', +}; +const otherCard = { + id: 'other-card', + textContent: 'Other User other@example.com', + value: '', + parentElement: null, + disabled: false, + getAttribute(name) { + if (name === 'role') return 'button'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; +const document = { + body: { + innerText: 'Welcome back Choose an account other@example.com', + textContent: 'Welcome back Choose an account other@example.com', + }, + querySelectorAll() { + return [otherCard]; + }, +}; + +function getOperationDelayRunner() { + return async (_metadata, operation) => operation(); +} +function isVisibleElement(element) { + return Boolean(element); +} +function isActionEnabled(element) { + return Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true'; +} +function simulateClick(element) { + clicked.push(element.id); +} +function inspectLoginAuthState() { + return { state: 'choose_account_page', url: location.href, chooseAccountPage: true }; +} +function throwIfStopped() {} +async function sleep() {} +async function humanPause() {} +function log() {} +async function finalizeStep6VerificationReady() { return { routed: 'verification' }; } +async function step6LoginFromPasswordPage() { return { routed: 'password' }; } +async function step6LoginFromEmailPage() { return { routed: 'email' }; } +async function step6LoginFromPhonePage() { return { routed: 'phone' }; } +async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'timeout' } }; } + +${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractFunction('getPageTextSnapshot')} +${extractFunction('normalizeAuthAccountIdentifier')} +${extractFunction('getChooseAccountCandidateText')} +${extractFunction('isChooseAccountPage')} +${extractFunction('isChooseAccountRemovalAction')} +${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6OAuthConsentSuccessResult')} +${extractFunction('createStep6AddEmailSuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('waitForChooseAccountTransition')} +${extractFunction('step6ChooseExistingAccount')} + +return { + clicked, + run() { + return step6ChooseExistingAccount( + { email: 'target@example.com', loginIdentifierType: 'email', visibleStep: 7 }, + { state: 'choose_account_page', url: location.href } + ); + }, +}; +`)(); + + const result = await api.run(); + + assert.deepEqual(api.clicked, []); + assert.equal(result.step6Outcome, 'recoverable'); + assert.equal(result.reason, 'choose_account_target_not_found'); +}); diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 3e90ee6..ed3c223 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -122,7 +122,7 @@ - `flows/openai/content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。 - `flows/openai/content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 -- `flows/openai/content/openai-auth.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、官网注册入口点击前固定等待 3 秒、入口点击失败后最多 5 次重试、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。 +- `flows/openai/content/openai-auth.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、官网注册入口点击前固定等待 3 秒、入口点击失败后最多 5 次重试、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`choose-an-account` 账号选择页、`phone-verification` 与真实 `add-email` 页面;当 `choose-an-account` 页存在目标邮箱账号卡时会点击该账号并确认跳转,进入 OAuth 同意页后沿用登录验证码跳过机制,避免把手机验证码页、添加邮箱页或账号选择页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。 - `flows/openai/content/sub2api-panel.js`:SUB2API 后台内容脚本,保留后台页面内生成 OAuth 地址和提交 localhost 回调的兼容能力;当前主流程已迁移到 `background/sub2api-api.js` 的管理接口直连路径。 - `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。 - `flows/openai/content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10,以及 Plus 仍走 OAuth 尾链时的 `platform-verify` 节点。