fix: handle OpenAI choose account login

This commit is contained in:
QLHazyCoder
2026-05-27 05:25:00 +08:00
parent 5f73c505a0
commit f4bf828653
7 changed files with 526 additions and 1 deletions
+2
View File
@@ -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':
+2
View File
@@ -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':
+261
View File
@@ -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);
}
@@ -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 超时,请重新创建任务'
+21
View File
@@ -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');
+238
View File
@@ -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');
});
+1 -1
View File
@@ -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` 节点。