Merge remote-tracking branch 'origin/dev' into codex/pr278-review
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,158 @@ async function waitForLoginEntryOpenTransition(timeout = 10000) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function isOpenAiOAuthAuthorizationRoute(rawUrl = '') {
|
||||
const value = String(rawUrl || '').trim();
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(value, location.href);
|
||||
const hostname = String(parsed.hostname || '').toLowerCase();
|
||||
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pathname = parsed.pathname || '/';
|
||||
return /^\/(?:authorize|oauth(?:2)?\/|api\/oauth\/oauth2\/auth|sign-in-with-chatgpt\/)/i.test(pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isPostChooseAccountOAuthRoute(snapshot = {}) {
|
||||
const url = String(snapshot?.url || location.href || '').trim();
|
||||
if (!isOpenAiOAuthAuthorizationRoute(url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url, location.href);
|
||||
return !/\/choose-an-account(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
} catch {
|
||||
return !/\/choose-an-account(?:[/?#]|$)/i.test(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForChooseAccountTransition(timeout = 15000) {
|
||||
const start = Date.now();
|
||||
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
|
||||
if (isPostChooseAccountOAuthRoute(snapshot)) {
|
||||
return {
|
||||
...snapshot,
|
||||
oauthAuthorizationRoute: true,
|
||||
};
|
||||
}
|
||||
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.oauthAuthorizationRoute || isPostChooseAccountOAuthRoute(nextSnapshot)) {
|
||||
return createStep6OAuthConsentSuccessResult(nextSnapshot, {
|
||||
via: 'choose_account_oauth_authorization_route',
|
||||
});
|
||||
}
|
||||
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 +6493,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);
|
||||
}
|
||||
|
||||
@@ -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 超时,请重新创建任务'
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,343 @@ 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('isOpenAiOAuthAuthorizationRoute')}
|
||||
${extractFunction('isPostChooseAccountOAuthRoute')}
|
||||
${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 skips login code when choose-account leaves for OAuth authorize route before consent DOM is ready', 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 document = {
|
||||
body: {
|
||||
innerText: 'Welcome back Choose an account tall-slept-fancy@duck.com',
|
||||
textContent: 'Welcome back Choose an account tall-slept-fancy@duck.com',
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (String(selector).includes('body *')) return [targetCard];
|
||||
return [targetCard];
|
||||
},
|
||||
};
|
||||
|
||||
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 = 'unknown';
|
||||
location.href = 'https://auth.openai.com/authorize?client_id=codex-test&state=oauth-state';
|
||||
location.pathname = '/authorize';
|
||||
}
|
||||
}
|
||||
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('isOpenAiOAuthAuthorizationRoute')}
|
||||
${extractFunction('isPostChooseAccountOAuthRoute')}
|
||||
${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, 'unknown');
|
||||
assert.equal(result.skipLoginVerificationStep, true);
|
||||
assert.equal(result.directOAuthConsentPage, true);
|
||||
assert.equal(result.via, 'choose_account_oauth_authorization_route');
|
||||
});
|
||||
|
||||
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('isOpenAiOAuthAuthorizationRoute')}
|
||||
${extractFunction('isPostChooseAccountOAuthRoute')}
|
||||
${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');
|
||||
});
|
||||
|
||||
+1
-1
@@ -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` 节点。
|
||||
|
||||
Reference in New Issue
Block a user