// flows/openai/content/openai-auth.js — Content script for ChatGPT signup entry + OpenAI auth pages
// Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
// Dynamically injected on: chatgpt.com
console.log('[MultiPage:openai-auth] Content script loaded on', location.href);
const OPENAI_AUTH_LISTENER_SENTINEL = 'data-multipage-openai-auth-listener';
function getOperationDelayRunner() {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function'
? gate
: async (_metadata, operation) => operation();
}
if (document.documentElement.getAttribute(OPENAI_AUTH_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(OPENAI_AUTH_LISTENER_SENTINEL, '1');
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (
message.type === 'EXECUTE_NODE'
|| message.type === 'FILL_CODE'
|| message.type === 'STEP8_FIND_AND_CLICK'
|| message.type === 'STEP8_GET_STATE'
|| message.type === 'STEP8_TRIGGER_CONTINUE'
|| message.type === 'GET_LOGIN_AUTH_STATE'
|| message.type === 'SUBMIT_ADD_EMAIL'
|| message.type === 'GET_STEP5_SUBMIT_STATE'
|| message.type === 'PREPARE_SIGNUP_VERIFICATION'
|| message.type === 'RECOVER_AUTH_RETRY_PAGE'
|| message.type === 'RECOVER_STEP5_SUBMIT_RETRY_PAGE'
|| message.type === 'RESEND_VERIFICATION_CODE'
|| message.type === 'SUBMIT_PHONE_NUMBER'
|| message.type === 'SUBMIT_PHONE_VERIFICATION_CODE'
|| message.type === 'RESEND_PHONE_VERIFICATION_CODE'
|| message.type === 'CHECK_PHONE_RESEND_ERROR'
|| 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();
handleCommand(message).then((result) => {
sendResponse({ ok: true, ...(result || {}) });
}).catch(err => {
const reportedStep = Number(message.payload?.visibleStep) || message.step;
const reportedNodeId = resolveCommandNodeId(message);
if (isStopError(err)) {
if (reportedStep) {
log(`步骤 ${reportedStep || 8}:已被用户停止。`, 'warn');
}
sendResponse({ stopped: true, error: err.message });
return;
}
if (message.type === 'STEP8_FIND_AND_CLICK') {
log(err.message, 'error', { step: reportedStep || 9, stepKey: 'confirm-oauth' });
sendResponse({ error: err.message });
return;
}
if (reportedStep) {
reportError(reportedNodeId || reportedStep, err.message);
}
sendResponse({ error: err.message });
});
return true;
}
});
} else {
console.log('[MultiPage:openai-auth] 消息监听已存在,跳过重复注册');
}
const OPENAI_AUTH_NODE_HANDLERS = Object.freeze({
'submit-signup-email': (payload) => step2_clickRegister(payload),
'fill-password': (payload) => step3_fillEmailPassword(payload),
'fill-profile': (payload) => step5_fillNameBirthday(payload),
'oauth-login': (payload) => step6_login(payload),
'relogin-bound-email': (payload) => step6_login(payload),
'confirm-oauth': (_payload) => step8_findAndClick(),
});
function resolveCommandNodeId(message = {}) {
const directNodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
if (directNodeId) {
return directNodeId;
}
const visibleStep = Number(message.payload?.visibleStep || message.step) || 0;
if (visibleStep === 4) return 'fetch-signup-code';
if (message.type === 'FILL_CODE' && (visibleStep === 12 || visibleStep === 15)) {
return 'fetch-bound-email-login-code';
}
if (
(
message.type === 'SUBMIT_PHONE_NUMBER'
|| message.type === 'SUBMIT_PHONE_VERIFICATION_CODE'
|| message.type === 'RESEND_PHONE_VERIFICATION_CODE'
|| message.type === 'CHECK_PHONE_RESEND_ERROR'
|| message.type === 'RETURN_TO_ADD_PHONE'
)
&& (visibleStep === 13 || visibleStep === 16)
) {
return 'post-bound-email-phone-verification';
}
if (visibleStep === 8 || visibleStep === 11) return 'fetch-login-code';
if (visibleStep === 9 || visibleStep === 12) return 'post-login-phone-verification';
if (visibleStep === 10 || visibleStep === 13) return 'confirm-oauth';
if (visibleStep === 16) return 'confirm-oauth';
if (visibleStep === 14 || visibleStep === 15 || visibleStep === 17) return 'platform-verify';
if (visibleStep === 7) return 'oauth-login';
if (visibleStep === 5) return 'fill-profile';
if (visibleStep === 3) return 'fill-password';
if (visibleStep === 2) return 'submit-signup-email';
return '';
}
async function handleCommand(message) {
switch (message.type) {
case 'EXECUTE_NODE': {
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
const handler = OPENAI_AUTH_NODE_HANDLERS[nodeId];
if (!handler) {
throw new Error(`openai-auth.js 不处理节点 ${nodeId}`);
}
return await handler(message.payload || {});
}
case 'FILL_CODE':
// Step 4 = signup code, Step 7 = login code (same handler)
return await fillVerificationCode(message.step, message.payload);
case 'GET_LOGIN_AUTH_STATE':
return serializeLoginAuthState(inspectLoginAuthState());
case 'SUBMIT_ADD_EMAIL':
return await submitAddEmailAndContinue(message.payload);
case 'GET_STEP5_SUBMIT_STATE':
return getStep5SubmitState();
case 'PREPARE_SIGNUP_VERIFICATION':
return await prepareSignupVerificationFlow(message.payload);
case 'RECOVER_AUTH_RETRY_PAGE':
return await recoverCurrentAuthRetryPage(message.payload);
case 'RECOVER_STEP5_SUBMIT_RETRY_PAGE':
return await recoverStep5SubmitRetryPage(message.payload);
case 'RESEND_VERIFICATION_CODE':
return await resendVerificationCode(message.step);
case 'SUBMIT_PHONE_NUMBER':
return await phoneAuthHelpers.submitPhoneNumber(message.payload);
case 'SUBMIT_PHONE_VERIFICATION_CODE':
if (message.payload?.purpose === 'signup') {
return await fillVerificationCode(message.step || 4, message.payload);
}
if (message.payload?.purpose === 'login') {
return await fillVerificationCode(message.step || 8, message.payload);
}
return await submitPhoneVerificationCodeWithProfileFallback(message.payload);
case 'RESEND_PHONE_VERIFICATION_CODE':
return await phoneAuthHelpers.resendPhoneVerificationCode(undefined, message.payload || {});
case 'CHECK_PHONE_RESEND_ERROR':
return phoneAuthHelpers.checkPhoneResendError();
case 'RETURN_TO_ADD_PHONE':
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':
return await step8_findAndClick(message.payload);
case 'STEP8_GET_STATE':
return getStep8State();
case 'STEP8_TRIGGER_CONTINUE':
return await step8_triggerContinue(message.payload);
}
}
function resolveVisibleStep(payload = {}, fallback = 0) {
const step = Math.floor(Number(payload?.visibleStep) || 0);
return step > 0 ? step : fallback;
}
function stepLog(step, message, level = 'info', stepKey = '') {
return log(message, level, { step, stepKey });
}
const VERIFICATION_CODE_INPUT_SELECTOR = [
'input[name="code"]',
'input[name="otp"]',
'input[autocomplete="one-time-code"]',
'input[type="text"][maxlength="6"]',
'input[type="tel"][maxlength="6"]',
'input[aria-label*="code" i]',
'input[aria-label*="コード"]',
'input[aria-label*="認証"]',
'input[aria-label*="確認"]',
'input[placeholder*="code" i]',
'input[placeholder*="コード"]',
'input[placeholder*="認証"]',
'input[placeholder*="確認"]',
'input[inputmode="numeric"]',
].join(', ');
const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|ワンタイム(?:コード|パスコード)|一回限りの?(?:コード|パスコード)|(?:認証|確認)?コード(?:で|を使用して)?(?:ログイン|サインイン)|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
const LOGIN_ENTRY_ACTION_PATTERN = /(?:^|\b)(?:log\s*in|sign\s*in|continue\s+(?:with|using)\s+(?:email|chatgpt)|use\s+(?:an?\s+)?email|email\s+address)(?:\b|$)|登录|登陆|邮箱|电子邮件|ログイン|サインイン|メールアドレス|メール|電子メール/i;
const LOGIN_SWITCH_TO_PHONE_PATTERN = /继续使用(?:手机|手机号|电话)(?:号码)?登录|改用(?:手机|手机号|电话)(?:号码)?登录|手机号登录|(?:電話番号|電話|携帯電話|携帯)(?:で|を使用して)?(?:続行|続ける|ログイン|サインイン)|(?:続行|続ける|使用|ログイン|サインイン).*(?:電話番号|電話|携帯電話|携帯)|continue\s+(?:with|using)\s+(?:a\s+)?phone(?:\s+number)?|use\s+(?:a\s+)?phone(?:\s+number)?(?:\s+instead)?|sign\s*in\s+with\s+(?:a\s+)?phone/i;
const LOGIN_PHONE_ACTION_PATTERN = /手机|电话|phone|telephone|電話番号|電話|携帯電話|携帯/i;
const LOGIN_PHONE_ENTRY_PAGE_PATTERN = /(?:\+\s*\(?\d{1,4}\)?\s*)?(?:手机号码|手机号|电话号码|電話番号|携帯電話番号|携帯番号)|(?:phone|mobile)\s+number|telephone/i;
const LOGIN_MORE_OPTIONS_PATTERN = /更多(?:选项|登录方式|方式)|其他(?:登录方式|选项|方式)|显示更多|その他|他の(?:ログイン)?方法|別の(?:ログイン)?方法|もっと見る|オプション|more\s+(?:login\s+|sign[-\s]*in\s+)?options|other\s+(?:login\s+|sign[-\s]*in\s+)?(?:options|ways)|show\s+more/i;
const LOGIN_EXTERNAL_IDP_PATTERN = /google|microsoft|apple|sso|single\s+sign[-\s]*on|企业|工作区|workspace/i;
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 CHOOSE_ACCOUNT_CARD_SELECTOR = [
'[data-testid*="account" i]',
'[data-test-id*="account" i]',
'[class*="account" i]',
'[class*="user" i]',
'[class*="card" i]',
'[class*="option" i]',
'[class*="select" i]',
'[class*="list" i] > *',
'li',
].join(', ');
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;
function isVisibleElement(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getVisibleSplitVerificationInputs() {
return Array.from(document.querySelectorAll('input[maxlength="1"]'))
.filter(isVisibleElement);
}
function getVerificationCodeTarget() {
const splitInputs = getVisibleSplitVerificationInputs();
const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
if (codeInput && isVisibleElement(codeInput)) {
const maxLength = Number(codeInput.getAttribute?.('maxlength') || codeInput.maxLength || 0);
if (maxLength === 1 && splitInputs.length >= 6) {
return { type: 'split', elements: splitInputs };
}
return { type: 'single', element: codeInput };
}
if (splitInputs.length >= 6) {
return { type: 'split', elements: splitInputs };
}
return null;
}
function getActionText(el) {
return [
el?.textContent,
el?.value,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function isActionEnabled(el) {
return Boolean(el)
&& !el.disabled
&& el.getAttribute('aria-disabled') !== 'true';
}
function findOneTimeCodeLoginTrigger() {
const candidates = document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
for (const el of candidates) {
if (!isVisibleElement(el)) continue;
if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue;
const text = [
el.textContent,
el.value,
el.getAttribute('aria-label'),
el.getAttribute('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
if (text && ONE_TIME_CODE_LOGIN_PATTERN.test(text)) {
return el;
}
}
return null;
}
function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
const candidates = document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
for (const el of candidates) {
if (!isVisibleElement(el)) continue;
if (!allowDisabled && !isActionEnabled(el)) continue;
const text = getActionText(el);
if (text && RESEND_VERIFICATION_CODE_PATTERN.test(text)) {
return el;
}
}
return null;
}
function isEmailVerificationPage() {
return /\/email-verification(?:[/?#]|$)/i.test(location.pathname || '');
}
function getContactVerificationServerErrorText() {
const path = String(location?.pathname || '');
if (!/\/contact-verification(?:[/?#]|$)/i.test(path)) {
return '';
}
const text = String(getPageTextSnapshot?.() || document?.body?.textContent || '').replace(/\s+/g, ' ').trim();
const title = String(document?.title || '').replace(/\s+/g, ' ').trim();
const combined = `${title} ${text}`.trim();
if (!CONTACT_VERIFICATION_SERVER_ERROR_PATTERN.test(combined)) {
return '';
}
return combined || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.';
}
function buildContactVerificationServerError(errorText = '') {
const serverErrorPrefix = typeof PHONE_RESEND_SERVER_ERROR_PREFIX === 'string'
? PHONE_RESEND_SERVER_ERROR_PREFIX
: 'PHONE_RESEND_SERVER_ERROR::';
const resolvedText = String(errorText || '').trim()
|| 'OpenAI contact-verification 页面返回 HTTP ERROR 500。';
return new Error(
resolvedText.startsWith(serverErrorPrefix)
? resolvedText
: `${serverErrorPrefix}${resolvedText}`
);
}
function throwIfContactVerificationServerError() {
const serverErrorText = getContactVerificationServerErrorText();
if (serverErrorText) {
throw buildContactVerificationServerError(serverErrorText);
}
}
async function resendVerificationCode(step, timeout = 45000) {
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();
};
if (step === 8) {
await waitForLoginVerificationPageReady(10000, step);
}
const start = Date.now();
let action = null;
let loggedWaiting = false;
while (Date.now() - start < timeout) {
throwIfStopped();
throwIfContactVerificationServerError();
// Check for 405 error page and recover by clicking "Try again"
if (is405MethodNotAllowedPage()) {
await handle405ResendError(step, timeout - (Date.now() - start));
// After recovery, loop back to find the resend button again
loggedWaiting = false;
continue;
}
action = findResendVerificationCodeTrigger({ allowDisabled: true });
if (action && isActionEnabled(action)) {
log(`步骤 ${step}:重新发送验证码按钮已可用。`);
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: step === 8 ? 'oauth-login' : 'fetch-signup-code', kind: 'click', label: 'resend-verification-code' }, async () => {
simulateClick(action);
});
await sleep(1200);
// After clicking resend, check if 405 error appeared
if (is405MethodNotAllowedPage()) {
log(`步骤 ${step}:点击重新发送后出现 405 错误,正在恢复...`, 'warn');
await handle405ResendError(step, timeout - (Date.now() - start));
loggedWaiting = false;
continue;
}
throwIfContactVerificationServerError();
return {
resent: true,
buttonText: getActionText(action),
};
}
if (action && !loggedWaiting) {
loggedWaiting = true;
log(`步骤 ${step}:正在等待重新发送验证码按钮变为可点击...`);
}
await sleep(250);
}
throwIfContactVerificationServerError();
throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href);
}
function is405MethodNotAllowedPage() {
const pageText = document.body?.textContent || '';
return AUTH_ROUTE_ERROR_PATTERN.test(pageText);
}
function getStep405RecoveryStateKey(step) {
return `__MULTIPAGE_STEP_${Number(step) || '?'}_405_RECOVERY_COUNT__`;
}
function getStep405StorageScope() {
if (typeof window !== 'undefined' && window) {
return window;
}
if (typeof globalThis !== 'undefined' && globalThis) {
return globalThis;
}
return {};
}
function getStep405RecoveryLimit(step) {
if (Number(step) !== 4) {
return 0;
}
return typeof STEP4_405_RECOVERY_LIMIT !== 'undefined'
? STEP4_405_RECOVERY_LIMIT
: 3;
}
function getStep405RecoveryErrorPrefix(step) {
if (Number(step) !== 4) {
return '';
}
return typeof STEP4_405_RECOVERY_ERROR_PREFIX !== 'undefined'
? STEP4_405_RECOVERY_ERROR_PREFIX
: 'STEP4_405_RECOVERY_LIMIT::';
}
function getStep405RecoveryCount(step) {
const key = getStep405RecoveryStateKey(step);
let value = '';
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage?.getItem) {
value = sessionStorage.getItem(key) || '';
}
} catch {}
if (!value) {
value = getStep405StorageScope()[key];
}
return Math.max(0, Math.floor(Number(value) || 0));
}
function setStep405RecoveryCount(step, count) {
const key = getStep405RecoveryStateKey(step);
const value = String(Math.max(0, Math.floor(Number(count) || 0)));
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage?.setItem) {
sessionStorage.setItem(key, value);
}
} catch {}
getStep405StorageScope()[key] = value;
}
function clearStep405RecoveryCount(step) {
const key = getStep405RecoveryStateKey(step);
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage?.removeItem) {
sessionStorage.removeItem(key);
}
} catch {}
try {
delete getStep405StorageScope()[key];
} catch {}
}
function createStep405RecoveryLimitError(step, count) {
const normalizedStep = Number(step) || step || '?';
const limit = getStep405RecoveryLimit(normalizedStep) || count;
const message = `步骤 ${normalizedStep}:检测到 405 错误页面,已连续点击“重试”恢复 ${count}/${limit} 次仍未恢复,当前轮将结束并进入下一轮。URL: ${location.href}`;
return new Error(`${getStep405RecoveryErrorPrefix(normalizedStep)}${message}`);
}
async function handle405ResendError(step, remainingTimeout = 30000) {
const currentCount = getStep405RecoveryCount(step);
if (Number(step) === 4 && currentCount >= getStep405RecoveryLimit(step)) {
throw createStep405RecoveryLimitError(step, currentCount);
}
const nextCount = currentCount + 1;
setStep405RecoveryCount(step, nextCount);
const maxClickAttempts = Number(step) === 4 ? 1 : 5;
await recoverCurrentAuthRetryPage({
logLabel: Number(step) === 4
? `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复(总计 ${nextCount}/${getStep405RecoveryLimit(step)})`
: `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复`,
maxClickAttempts,
pathPatterns: [],
step,
timeoutMs: Math.max(1000, remainingTimeout),
});
if (is405MethodNotAllowedPage()) {
throw createStep405RecoveryLimitError(step, nextCount);
}
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:405 错误已恢复,页面已返回验证码页面。`);
}
// ============================================================
// Signup Entry Helpers
// ============================================================
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|無料でサインアップ|サインアップ|新規登録|登録する|登録|アカウントを作成|アカウント作成|sign\s*up|register|create\s*account|create\s+account/i;
const SIGNUP_EMAIL_INPUT_SELECTOR = [
'input[type="email"]',
'input[autocomplete="email"]',
'input[autocomplete="username"]',
'input[name="email"]',
'input[name="username"]',
'input[id*="email"]',
'input[placeholder*="email" i]',
'input[placeholder*="电子邮件"]',
'input[placeholder*="邮箱"]',
'input[placeholder*="メール"]',
'input[aria-label*="email" i]',
'input[aria-label*="电子邮件"]',
'input[aria-label*="邮箱"]',
'input[aria-label*="メール"]',
].join(', ');
const SIGNUP_PHONE_INPUT_SELECTOR = [
'input[type="tel"]:not([maxlength="6"])',
'input[name*="phone" i]',
'input[id*="phone" i]',
'input[autocomplete="tel"]',
'input[placeholder*="手机"]',
'input[placeholder*="電話"]',
'input[placeholder*="携帯"]',
'input[aria-label*="手机"]',
'input[aria-label*="電話"]',
'input[aria-label*="携帯"]',
].join(', ');
const SIGNUP_SWITCH_TO_EMAIL_PATTERN = new RegExp([
String.raw`\u7ee7\u7eed\u4f7f\u7528(?:\u7535\u5b50\u90ae\u4ef6\u5730\u5740|\u90ae\u7bb1)\u767b\u5f55`,
String.raw`\u6539\u7528(?:\u7535\u5b50\u90ae\u4ef6\u5730\u5740|\u90ae\u7bb1)\u767b\u5f55`,
String.raw`(?:メールアドレス|メール|電子メール)(?:で|を使用して)?(?:続行|続ける|ログイン|サインイン|サインアップ)`,
String.raw`(?:続行|続ける|使用|ログイン|サインイン|サインアップ).*(?:メールアドレス|メール|電子メール)`,
String.raw`continue\s+using\s+(?:an?\s+)?email(?:\s+address)?(?:\s+(?:to\s+)?(?:log\s*in|sign\s*in|sign\s*up))?`,
String.raw`continue\s+with\s+email(?:\s+address)?`,
String.raw`use\s+(?:an?\s+)?email(?:\s+address)?(?:\s+instead)?`,
String.raw`sign\s*(?:in|up)\s+with\s+email`,
].join('|'), 'i');
const SIGNUP_SWITCH_ACTION_PATTERN = /\u7ee7\u7eed\u4f7f\u7528|\u6539\u7528|continue|use|sign\s*(?:in|up)|続行|続ける|使用|ログイン|サイン(?:イン|アップ)/i;
const SIGNUP_EMAIL_ACTION_PATTERN = /\u7535\u5b50\u90ae\u4ef6|\u90ae\u7bb1|email|メールアドレス|メール|電子メール/i;
const SIGNUP_PHONE_ACTION_PATTERN = /手机|手机号|电话号码|phone|telephone|mobile|電話番号|電話|携帯電話|携帯/i;
const SIGNUP_SWITCH_TO_PHONE_PATTERN = new RegExp([
String.raw`\u7ee7\u7eed\u4f7f\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?\u767b\u5f55`,
String.raw`\u6539\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?\u767b\u5f55`,
String.raw`\u7ee7\u7eed\u4f7f\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?`,
String.raw`\u6539\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?`,
String.raw`\u4f7f\u7528(?:\u624b\u673a|\u624b\u673a\u53f7|\u624b\u673a\u53f7\u7801|\u7535\u8bdd\u53f7\u7801)(?:\u53f7\u7801)?`,
String.raw`(?:電話番号|電話|携帯電話|携帯)(?:で|を使用して)?(?:続行|続ける|ログイン|サインイン|サインアップ)`,
String.raw`(?:続行|続ける|使用|ログイン|サインイン|サインアップ).*(?:電話番号|電話|携帯電話|携帯)`,
String.raw`continue\s+(?:with|using)\s+(?:a\s+)?phone(?:\s+number)?`,
String.raw`use\s+(?:a\s+)?phone(?:\s+number)?(?:\s+instead)?`,
String.raw`sign\s*(?:in|up)\s+with\s+(?:a\s+)?phone`,
].join('|'), 'i');
const SIGNUP_MORE_OPTIONS_PATTERN = /更多选项|其它方式|其他方式|その他|他の方法|別の方法|もっと見る|オプション|more\s+options|show\s+more|other\s+(?:options|ways)/i;
const SIGNUP_WORK_EMAIL_PATTERN = /\u5de5\u4f5c|business|work\s+email/i;
function getSignupEmailInput() {
const input = document.querySelector(SIGNUP_EMAIL_INPUT_SELECTOR);
if (input && isVisibleElement(input)) {
return input;
}
const fallback = Array.from(document.querySelectorAll('input')).find((el) => {
if (!isVisibleElement(el)) return false;
const type = String(el.getAttribute?.('type') || '').trim().toLowerCase();
const name = String(el.getAttribute?.('name') || '').trim().toLowerCase();
const id = String(el.getAttribute?.('id') || '').trim().toLowerCase();
const placeholder = String(el.getAttribute?.('placeholder') || '').trim();
const ariaLabel = String(el.getAttribute?.('aria-label') || '').trim();
const autocomplete = String(el.getAttribute?.('autocomplete') || '').trim().toLowerCase();
const combinedText = `${placeholder} ${ariaLabel}`;
return type === 'email'
|| autocomplete === 'email'
|| autocomplete === 'username'
|| /email|username|mail/i.test(`${name} ${id}`)
|| /email|电子邮件|邮箱|メール|電子メール/i.test(combinedText);
});
return fallback || null;
}
function getSignupPhoneInput() {
const input = document.querySelector(SIGNUP_PHONE_INPUT_SELECTOR);
if (input && isVisibleElement(input)) {
return input;
}
const fallback = Array.from(document.querySelectorAll('input')).find((el) => {
if (!isVisibleElement(el)) return false;
const type = String(el.getAttribute?.('type') || '').trim().toLowerCase();
const name = String(el.getAttribute?.('name') || '').trim().toLowerCase();
const id = String(el.getAttribute?.('id') || '').trim().toLowerCase();
const placeholder = String(el.getAttribute?.('placeholder') || '').trim();
const ariaLabel = String(el.getAttribute?.('aria-label') || '').trim();
const autocomplete = String(el.getAttribute?.('autocomplete') || '').trim().toLowerCase();
const combinedText = `${placeholder} ${ariaLabel}`;
return type === 'tel'
|| autocomplete === 'tel'
|| /phone|tel/i.test(`${name} ${id}`)
|| /手机|电话|手机号|電話|電話番号|携帯|携帯電話/.test(combinedText);
});
return fallback || null;
}
function findSignupUseEmailTrigger() {
const candidates = document.querySelectorAll('button, a, [role="button"], [role="link"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
const text = getActionText(el);
if (!text) return false;
if (SIGNUP_WORK_EMAIL_PATTERN.test(text)) return false;
return SIGNUP_SWITCH_TO_EMAIL_PATTERN.test(text)
|| (SIGNUP_SWITCH_ACTION_PATTERN.test(text) && SIGNUP_EMAIL_ACTION_PATTERN.test(text));
}) || null;
}
function findSignupUsePhoneTrigger() {
const candidates = document.querySelectorAll('button, a, [role="button"], [role="link"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
const text = getActionText(el);
if (!text) return false;
return SIGNUP_SWITCH_TO_PHONE_PATTERN.test(text)
|| (SIGNUP_SWITCH_ACTION_PATTERN.test(text) && SIGNUP_PHONE_ACTION_PATTERN.test(text));
}) || null;
}
function findSignupMoreOptionsTrigger() {
const candidates = document.querySelectorAll('button, a, [role="button"], [role="link"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
const text = getActionText(el);
if (!text || !SIGNUP_MORE_OPTIONS_PATTERN.test(text)) return false;
const expanded = String(el.getAttribute?.('aria-expanded') || '').trim().toLowerCase();
const state = String(el.getAttribute?.('data-state') || '').trim().toLowerCase();
return expanded !== 'true' && state !== 'open';
}) || null;
}
function getSignupEmailContinueButton({ allowDisabled = false } = {}) {
const direct = document.querySelector('button[type="submit"], input[type="submit"]');
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
return direct;
}
const candidates = document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false;
return /continue|next|submit|继续|下一步|続行|続ける|次へ|送信/i.test(getActionText(el));
}) || null;
}
function findSignupEntryTrigger(options = {}) {
const { allowHiddenFallback = true } = options || {};
const candidates = document.querySelectorAll('a, button, [role="button"], [role="link"]');
let hiddenSignupTrigger = null;
for (const el of Array.from(candidates)) {
if (!isActionEnabled(el)) continue;
if (!SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el))) continue;
if (isVisibleElement(el)) {
return el;
}
if (!hiddenSignupTrigger) {
hiddenSignupTrigger = el;
}
}
if (!allowHiddenFallback || !hiddenSignupTrigger) {
return null;
}
const view = typeof window !== 'undefined' ? window : globalThis;
const collapsedViewport = Boolean(
Math.round(Number(view?.innerWidth) || 0) < 240
|| Math.round(Number(view?.innerHeight) || 0) < 160
|| Math.round(Number(view?.outerWidth) || 0) < 320
|| Math.round(Number(view?.outerHeight) || 0) < 180
);
const pageText = typeof getPageTextSnapshot === 'function'
? getPageTextSnapshot()
: '';
const looksLikeLoggedOutHome = /登录|登入|ログイン|サインイン|log\s*in|sign\s*in/i.test(pageText);
return collapsedViewport || looksLikeLoggedOutHome ? hiddenSignupTrigger : null;
}
function getSignupPasswordDisplayedEmail() {
const text = (document.body?.innerText || document.body?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
const matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig);
return matches?.[0] ? String(matches[0]).trim().toLowerCase() : '';
}
function inspectSignupEntryState() {
if (typeof isPhoneVerificationPageReady === 'function' && isPhoneVerificationPageReady()) {
return {
state: 'phone_verification_page',
verificationTarget: typeof getVerificationCodeTarget === 'function' ? getVerificationCodeTarget() : null,
displayedPhone: typeof getPhoneVerificationDisplayedPhone === 'function' ? getPhoneVerificationDisplayedPhone() : '',
url: location.href,
};
}
const postVerificationState = typeof getStep4PostVerificationState === 'function'
? getStep4PostVerificationState()
: null;
if (postVerificationState?.state === 'step5') {
return {
state: 'profile_page',
url: postVerificationState.url || location.href,
};
}
if (postVerificationState?.state === 'logged_in_home') {
return {
state: 'logged_in_home',
skipProfileStep: true,
url: postVerificationState.url || location.href,
};
}
if (typeof isVerificationPageStillVisible === 'function' && isVerificationPageStillVisible()) {
return {
state: 'verification_page',
verificationTarget: typeof getVerificationCodeTarget === 'function' ? getVerificationCodeTarget() : null,
url: location.href,
};
}
const passwordInput = getSignupPasswordInput();
if (isSignupPasswordPage() && passwordInput) {
return {
state: 'password_page',
passwordInput,
submitButton: getSignupPasswordSubmitButton({ allowDisabled: true }),
displayedEmail: getSignupPasswordDisplayedEmail(),
passwordErrorText: getSignupPasswordFieldErrorText(),
url: location.href,
};
}
const emailInput = getSignupEmailInput();
if (emailInput) {
return {
state: 'email_entry',
emailInput,
continueButton: getSignupEmailContinueButton({ allowDisabled: true }),
switchToPhoneTrigger: findSignupUsePhoneTrigger(),
url: location.href,
};
}
const phoneInput = getSignupPhoneInput();
if (phoneInput) {
return {
state: 'phone_entry',
phoneInput,
switchToEmailTrigger: findSignupUseEmailTrigger(),
url: location.href,
};
}
const signupTrigger = findSignupEntryTrigger();
if (signupTrigger) {
return {
state: 'entry_home',
signupTrigger,
url: location.href,
};
}
return {
state: 'unknown',
url: location.href,
};
}
function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) {
const summary = {
state: snapshot?.state || 'unknown',
url: snapshot?.url || location.href,
hasEmailInput: Boolean(snapshot?.emailInput || getSignupEmailInput()),
hasPhoneInput: Boolean(snapshot?.phoneInput || getSignupPhoneInput()),
hasPasswordInput: Boolean(snapshot?.passwordInput || getSignupPasswordInput()),
};
if (snapshot?.displayedEmail) {
summary.displayedEmail = snapshot.displayedEmail;
}
if (snapshot?.signupTrigger) {
summary.signupTrigger = {
tag: (snapshot.signupTrigger.tagName || '').toLowerCase(),
text: getActionText(snapshot.signupTrigger).slice(0, 80),
visible: isVisibleElement(snapshot.signupTrigger),
};
}
if (snapshot?.continueButton) {
summary.continueButton = {
tag: (snapshot.continueButton.tagName || '').toLowerCase(),
text: getActionText(snapshot.continueButton).slice(0, 80),
enabled: isActionEnabled(snapshot.continueButton),
};
}
if (snapshot?.switchToEmailTrigger) {
summary.switchToEmailTrigger = {
tag: (snapshot.switchToEmailTrigger.tagName || '').toLowerCase(),
text: getActionText(snapshot.switchToEmailTrigger).slice(0, 80),
enabled: isActionEnabled(snapshot.switchToEmailTrigger),
};
}
if (snapshot?.switchToPhoneTrigger) {
summary.switchToPhoneTrigger = {
tag: (snapshot.switchToPhoneTrigger.tagName || '').toLowerCase(),
text: getActionText(snapshot.switchToPhoneTrigger).slice(0, 80),
enabled: isActionEnabled(snapshot.switchToPhoneTrigger),
};
}
return summary;
}
function getSignupEntryDiagnostics() {
const view = typeof window !== 'undefined' ? window : globalThis;
const safeGetComputedStyle = (el) => {
if (!el || typeof view?.getComputedStyle !== 'function') {
return null;
}
try {
return view.getComputedStyle(el);
} catch {
return null;
}
};
const buildRectSummary = (el) => {
const rect = typeof el?.getBoundingClientRect === 'function'
? el.getBoundingClientRect()
: null;
return rect
? {
width: Math.round(rect.width || 0),
height: Math.round(rect.height || 0),
}
: null;
};
const buildVisibilityMeta = (el) => {
const style = safeGetComputedStyle(el);
return {
className: String(el?.className || '').slice(0, 200),
hidden: Boolean(el?.hidden),
ariaHidden: el?.getAttribute?.('aria-hidden') || '',
inert: typeof el?.hasAttribute === 'function' ? el.hasAttribute('inert') : false,
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
};
};
const findBlockingAncestor = (el) => {
let current = el?.parentElement || null;
while (current) {
const style = safeGetComputedStyle(current);
const rect = buildRectSummary(current);
const hidden = Boolean(current.hidden);
const ariaHidden = current.getAttribute?.('aria-hidden') || '';
const inert = typeof current.hasAttribute === 'function' ? current.hasAttribute('inert') : false;
const blockedByStyle = Boolean(
style
&& (
style.display === 'none'
|| style.visibility === 'hidden'
|| style.opacity === '0'
|| style.pointerEvents === 'none'
)
);
const blockedByRect = Boolean(rect && (rect.width === 0 || rect.height === 0));
if (hidden || ariaHidden === 'true' || inert || blockedByStyle || blockedByRect) {
return {
tag: (current.tagName || '').toLowerCase(),
id: current.id || '',
className: String(current.className || '').slice(0, 200),
hidden,
ariaHidden,
inert,
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
rect,
};
}
current = current.parentElement;
}
return null;
};
const actionCandidates = document.querySelectorAll(
'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
const allActions = Array.from(actionCandidates).map((el) => {
const text = getActionText(el);
return {
tag: (el.tagName || '').toLowerCase(),
type: el.getAttribute?.('type') || '',
text: text.slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
rect: buildRectSummary(el),
};
});
const visibleActions = Array.from(actionCandidates)
.filter(isVisibleElement)
.slice(0, 12)
.map((el) => ({
tag: (el.tagName || '').toLowerCase(),
type: el.getAttribute?.('type') || '',
text: getActionText(el).slice(0, 80),
enabled: isActionEnabled(el),
}))
.filter((item) => item.text);
const signupLikeActions = Array.from(actionCandidates)
.map((el) => {
const text = getActionText(el);
return {
tag: (el.tagName || '').toLowerCase(),
type: el.getAttribute?.('type') || '',
text: text.slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
rect: buildRectSummary(el),
...buildVisibilityMeta(el),
blockingAncestor: findBlockingAncestor(el),
};
})
.filter((item) => item.text && SIGNUP_ENTRY_TRIGGER_PATTERN.test(item.text))
.slice(0, 12);
return {
url: location.href,
title: document.title || '',
readyState: document.readyState || '',
viewport: {
innerWidth: Math.round(Number(view?.innerWidth) || 0),
innerHeight: Math.round(Number(view?.innerHeight) || 0),
outerWidth: Math.round(Number(view?.outerWidth) || 0),
outerHeight: Math.round(Number(view?.outerHeight) || 0),
devicePixelRatio: Number(view?.devicePixelRatio) || 0,
},
hasEmailInput: Boolean(getSignupEmailInput()),
hasPhoneInput: Boolean(getSignupPhoneInput()),
hasPasswordInput: Boolean(getSignupPasswordInput()),
hasSwitchToEmailAction: Boolean(findSignupUseEmailTrigger()),
bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()),
signupLikeActionCounts: {
total: signupLikeActions.length,
visible: signupLikeActions.filter((item) => item.visible).length,
hidden: signupLikeActions.filter((item) => !item.visible).length,
},
signupLikeActions,
visibleActions,
bodyTextPreview: getPageTextSnapshot().slice(0, 240),
};
}
function getSignupPasswordDiagnostics() {
const view = typeof window !== 'undefined' ? window : globalThis;
const safeGetComputedStyle = (el) => {
if (!el || typeof view?.getComputedStyle !== 'function') {
return null;
}
try {
return view.getComputedStyle(el);
} catch {
return null;
}
};
const buildRectSummary = (el) => {
const rect = typeof el?.getBoundingClientRect === 'function'
? el.getBoundingClientRect()
: null;
return rect
? {
width: Math.round(rect.width || 0),
height: Math.round(rect.height || 0),
}
: null;
};
const buildInputSummary = (el) => {
const style = safeGetComputedStyle(el);
return {
tag: (el?.tagName || '').toLowerCase(),
type: el?.getAttribute?.('type') || el?.type || '',
name: el?.getAttribute?.('name') || el?.name || '',
id: el?.id || '',
autocomplete: el?.getAttribute?.('autocomplete') || '',
placeholder: String(el?.getAttribute?.('placeholder') || '').slice(0, 80),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
valueLength: String(el?.value || '').length,
rect: buildRectSummary(el),
className: String(el?.className || '').slice(0, 200),
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
formAction: el?.form?.action || '',
};
};
const buildActionSummary = (el) => {
const style = safeGetComputedStyle(el);
return {
tag: (el?.tagName || '').toLowerCase(),
type: el?.getAttribute?.('type') || el?.type || '',
role: el?.getAttribute?.('role') || '',
text: getActionText(el).slice(0, 120),
visible: isVisibleElement(el),
enabled: isActionEnabled(el),
rect: buildRectSummary(el),
className: String(el?.className || '').slice(0, 200),
display: style?.display || '',
visibility: style?.visibility || '',
opacity: style?.opacity || '',
pointerEvents: style?.pointerEvents || '',
dataDdActionName: el?.getAttribute?.('data-dd-action-name') || '',
formAction: el?.form?.action || '',
};
};
const passwordInputs = Array.from(document.querySelectorAll(
'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]'
))
.map(buildInputSummary)
.slice(0, 8);
const actionCandidates = Array.from(document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
))
.map(buildActionSummary)
.filter((item) => item.text)
.slice(0, 16);
const visibleActions = actionCandidates.filter((item) => item.visible).slice(0, 12);
const submitButton = getSignupPasswordSubmitButton({ allowDisabled: true });
const oneTimeCodeTrigger = findOneTimeCodeLoginTrigger();
const retryState = getSignupPasswordTimeoutErrorPageState();
return {
url: location.href,
title: document.title || '',
readyState: document.readyState || '',
displayedEmail: getSignupPasswordDisplayedEmail(),
passwordErrorText: getSignupPasswordFieldErrorText(),
hasVisiblePasswordInput: Boolean(getSignupPasswordInput()),
passwordInputCount: passwordInputs.length,
visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length,
passwordInputs,
submitButton: submitButton ? buildActionSummary(submitButton) : null,
oneTimeCodeTrigger: oneTimeCodeTrigger ? buildActionSummary(oneTimeCodeTrigger) : null,
retryPage: Boolean(retryState),
retryEnabled: Boolean(retryState?.retryEnabled),
userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked),
visibleActions,
bodyTextPreview: getPageTextSnapshot().slice(0, 240),
};
}
function logSignupPasswordDiagnostics(context, level = 'warn') {
try {
log(`${context}:密码页诊断快照:${JSON.stringify(getSignupPasswordDiagnostics())}`, level);
} catch (error) {
console.warn('[MultiPage:openai-auth] failed to build signup password diagnostics:', error?.message || error);
}
}
async function waitForSignupEntryState(options = {}) {
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 {
timeout = 15000,
autoOpenEntry = false,
step = 2,
logDiagnostics = false,
} = options;
const start = Date.now();
const maxSignupEntryClickRetries = 5;
const maxSignupEntryClickAttempts = maxSignupEntryClickRetries + 1;
let lastTriggerClickAt = 0;
let clickAttempts = 0;
let lastState = '';
let slowSnapshotLogged = false;
let lastSwitchToEmailAt = 0;
let loggedMissingSwitchToEmail = false;
while (Date.now() - start < timeout) {
throwIfStopped();
const snapshot = inspectSignupEntryState();
if (logDiagnostics && snapshot.state !== lastState) {
lastState = snapshot.state;
log(`步骤 ${step}:注册入口状态切换为 ${snapshot.state},状态快照:${JSON.stringify(getSignupEntryStateSummary(snapshot))}`);
}
if (snapshot.state === 'password_page' || snapshot.state === 'email_entry') {
return snapshot;
}
if (snapshot.state === 'phone_entry') {
if (!autoOpenEntry) {
return snapshot;
}
if (snapshot.switchToEmailTrigger && Date.now() - lastSwitchToEmailAt >= 1500) {
lastSwitchToEmailAt = Date.now();
loggedMissingSwitchToEmail = false;
if (logDiagnostics) {
log(`步骤 ${step}:检测到手机号输入模式,准备点击切换邮箱入口:"${getActionText(snapshot.switchToEmailTrigger).slice(0, 80)}"`);
}
log('步骤 2:检测到手机号输入模式,正在切换到邮箱输入模式...');
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'switch-to-signup-email' }, async () => {
simulateClick(snapshot.switchToEmailTrigger);
});
} else if (!snapshot.switchToEmailTrigger && !loggedMissingSwitchToEmail) {
loggedMissingSwitchToEmail = true;
log('步骤 2:检测到手机号输入模式,但暂未识别到“改用邮箱/继续使用电子邮件地址登录”按钮,继续等待界面稳定...', 'warn');
}
if (logDiagnostics && !slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:等待手机号入口切换超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
await sleep(250);
continue;
}
if (snapshot.state === 'entry_home') {
if (!autoOpenEntry) {
return snapshot;
}
if (Date.now() - lastTriggerClickAt >= 1500) {
if (clickAttempts >= maxSignupEntryClickAttempts) {
log(`步骤 ${step}:官网注册入口已完成 ${maxSignupEntryClickRetries} 次重试,页面仍未进入邮箱输入页,停止重试。`, 'warn');
return snapshot;
}
lastTriggerClickAt = Date.now();
clickAttempts += 1;
const retryAttempt = clickAttempts - 1;
if (logDiagnostics) {
log(`步骤 ${step}:正在点击官网注册入口(第 ${clickAttempts}/${maxSignupEntryClickAttempts} 次):"${getActionText(snapshot.signupTrigger).slice(0, 80)}"`);
}
log(retryAttempt > 0
? `步骤 ${step}:上次点击后仍未进入邮箱输入页,等待 3 秒后重试点击官网注册入口(重试 ${retryAttempt}/${maxSignupEntryClickRetries})...`
: `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`);
await sleep(3000);
throwIfStopped();
const clickTarget = findSignupEntryTrigger({ allowHiddenFallback: false }) || snapshot.signupTrigger;
if (!isVisibleElement(clickTarget)) {
log(`步骤 ${step}:注册入口仍处于不可见状态,继续按入口重试节奏尝试恢复点击...`, 'warn');
}
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
simulateClick(clickTarget);
});
}
}
if (logDiagnostics && !slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:等待注册入口超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
await sleep(250);
}
const finalSnapshot = inspectSignupEntryState();
if (logDiagnostics) {
log(`步骤 ${step}:等待注册入口状态超时,最终状态快照:${JSON.stringify(getSignupEntryStateSummary(finalSnapshot))}`, 'warn');
}
return finalSnapshot;
}
async function ensureSignupEntryReady(timeout = 15000) {
const snapshot = await waitForSignupEntryState({ timeout, autoOpenEntry: false });
if (snapshot.state === 'entry_home' || snapshot.state === 'phone_entry' || snapshot.state === 'email_entry' || 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 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();
while (Date.now() - start < timeout) {
throwIfStopped();
const passwordInput = getSignupPasswordInput();
if (isSignupPasswordPage() && passwordInput) {
return {
ready: true,
state: 'password_page',
url: location.href,
};
}
await sleep(200);
}
throw new Error('等待进入密码页超时。URL: ' + location.href);
}
async function fillSignupEmailAndContinue(email, step) {
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();
};
if (!email) throw new Error(`未提供邮箱地址,步骤 ${step} 无法继续。`);
const normalizedEmail = String(email || '').trim().toLowerCase();
const snapshot = await waitForSignupEntryState({
timeout: 20000,
autoOpenEntry: true,
step,
logDiagnostics: step === 2,
});
if (snapshot.state === 'password_page') {
if (snapshot.displayedEmail && snapshot.displayedEmail !== normalizedEmail) {
throw new Error(`步骤 ${step}:当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`);
}
log(`步骤 ${step}:当前已在密码页,无需重复提交邮箱。`);
return {
alreadyOnPasswordPage: true,
url: snapshot.url || location.href,
};
}
if (snapshot.state !== 'email_entry' || !snapshot.emailInput) {
if (step === 2) {
log(`步骤 ${step}:未进入邮箱输入页,最终页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
throw new Error(`步骤 ${step}:未找到可用的邮箱输入入口。URL: ${location.href}`);
}
log(`步骤 ${step}:正在填写邮箱:${email}`);
await humanPause(500, 1400);
await performOperationWithDelay({ stepKey: step === 2 ? 'signup-entry' : 'fill-password', kind: 'fill', label: 'signup-email' }, async () => {
fillInput(snapshot.emailInput, email);
});
log(`步骤 ${step}:邮箱已填写`);
const continueButton = snapshot.continueButton || getSignupEmailContinueButton({ allowDisabled: true });
if (!continueButton || !isActionEnabled(continueButton)) {
throw new Error(`步骤 ${step}:未找到可点击的“继续”按钮。URL: ${location.href}`);
}
log(`步骤 ${step}:邮箱已准备提交,正在前往密码页...`);
window.setTimeout(async () => {
try {
throwIfStopped();
await performOperationWithDelay({ stepKey: step === 2 ? 'signup-entry' : 'fill-password', kind: 'submit', label: 'submit-signup-email' }, async () => {
simulateClick(continueButton);
});
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:openai-auth] deferred signup email submit failed:', error?.message || error);
}
}
}, 120);
return {
submitted: true,
email,
url: location.href,
};
}
function normalizePhoneDigits(value) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.normalizePhoneDigits === 'function') {
return phoneCountryUtils.normalizePhoneDigits(value);
}
let digits = String(value || '').replace(/\D+/g, '');
if (digits.startsWith('00')) {
digits = digits.slice(2);
}
return digits;
}
function extractDialCodeFromText(value) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.extractDialCodeFromText === 'function') {
return phoneCountryUtils.extractDialCodeFromText(value);
}
const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*\(\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/);
return String(match?.[1] || match?.[2] || match?.[3] || '').trim();
}
function dispatchSignupPhoneFieldEvents(element) {
if (!element) return;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
function normalizeSignupCountryLabel(value) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.normalizeCountryLabel === 'function') {
return phoneCountryUtils.normalizeCountryLabel(value);
}
return String(value || '')
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/&/g, ' and ')
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function getSignupCountryLabelAliases(value) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.getCountryLabelAliases === 'function') {
return phoneCountryUtils.getCountryLabelAliases(value);
}
const aliases = new Set();
const addAlias = (alias) => {
const normalized = normalizeSignupCountryLabel(alias);
if (normalized) {
aliases.add(normalized);
}
};
const raw = String(value || '').trim();
addAlias(raw);
const normalized = normalizeSignupCountryLabel(raw);
const compact = normalized.replace(/\s+/g, '');
if (
/(?:^|\s)(?:gb|uk)(?:\s|$)/i.test(raw)
|| /england|united\s*kingdom|great\s*britain|\bbritain\b/i.test(raw)
|| /英国|英格兰|大不列颠/.test(raw)
|| ['gb', 'uk', 'england', 'unitedkingdom', 'greatbritain', 'britain'].includes(compact)
) {
[
'GB',
'UK',
'United Kingdom',
'Great Britain',
'Britain',
'England',
'英国',
'英格兰',
'大不列颠',
].forEach(addAlias);
}
return Array.from(aliases);
}
function getSignupPhoneOptionLabel(option) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.getOptionLabel === 'function') {
return phoneCountryUtils.getOptionLabel(option);
}
return String(option?.textContent || option?.label || '')
.replace(/\s+/g, ' ')
.trim();
}
function normalizeSignupCountryOptionValue(value) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.normalizeCountryOptionValue === 'function') {
return phoneCountryUtils.normalizeCountryOptionValue(value);
}
return String(value || '').trim().toUpperCase();
}
function getSignupRegionDisplayName(regionCode, locale) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.getRegionDisplayName === 'function') {
return phoneCountryUtils.getRegionDisplayName(regionCode, locale);
}
const normalizedRegionCode = normalizeSignupCountryOptionValue(regionCode);
const normalizedLocale = String(locale || '').trim();
if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') {
return '';
}
try {
return String(
new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || ''
).trim();
} catch {
return '';
}
}
function getSignupPhoneCountryMatchLabels(option) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.getOptionMatchLabels === 'function') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
return phoneCountryUtils.getOptionMatchLabels(option, {
document: typeof document !== 'undefined' ? document : null,
navigator: rootScope?.navigator || globalThis?.navigator || null,
getOptionLabel: getSignupPhoneOptionLabel,
});
}
const labels = new Set();
const pushLabel = (value) => {
const label = String(value || '').replace(/\s+/g, ' ').trim();
if (label) {
labels.add(label);
}
};
pushLabel(getSignupPhoneOptionLabel(option));
const regionCode = normalizeSignupCountryOptionValue(option?.value);
if (/^[A-Z]{2}$/.test(regionCode)) {
pushLabel(regionCode);
pushLabel(getSignupRegionDisplayName(regionCode, 'en'));
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const pageLocale = String(
document?.documentElement?.lang
|| document?.documentElement?.getAttribute?.('lang')
|| rootScope?.navigator?.language
|| ''
).trim();
if (pageLocale && !/^en(?:[-_]|$)/i.test(pageLocale)) {
pushLabel(getSignupRegionDisplayName(regionCode, pageLocale));
}
}
return Array.from(labels);
}
function isSameSignupCountryOption(left, right) {
if (!left || !right) {
return false;
}
const leftValue = normalizeSignupCountryOptionValue(left.value);
const rightValue = normalizeSignupCountryOptionValue(right.value);
if (leftValue && rightValue) {
return leftValue === rightValue;
}
return normalizeSignupCountryLabel(getSignupPhoneOptionLabel(left)) === normalizeSignupCountryLabel(getSignupPhoneOptionLabel(right));
}
function getSignupPhoneForm(phoneInput = getSignupPhoneInput()) {
return phoneInput?.closest?.('form') || null;
}
function getSignupPhoneControlRoots(phoneInput = getSignupPhoneInput()) {
const roots = [];
const addRoot = (root) => {
if (root && !roots.includes(root)) {
roots.push(root);
}
};
addRoot(phoneInput?.closest?.('form'));
addRoot(phoneInput?.closest?.('fieldset'));
addRoot(phoneInput?.closest?.('[data-rac]'));
addRoot(phoneInput?.closest?.('[role="group"]'));
addRoot(phoneInput?.parentElement);
addRoot(phoneInput?.parentElement?.parentElement);
addRoot(document);
return roots;
}
function querySignupPhoneCountryElements(root, selector) {
if (!root || !selector) {
return [];
}
if (typeof root.querySelectorAll === 'function') {
const directMatches = Array.from(root.querySelectorAll(selector));
if (directMatches.length > 0) {
return directMatches;
}
}
if (typeof root.querySelector === 'function') {
const selectors = String(selector || '')
.split(',')
.map((part) => part.trim())
.filter(Boolean);
const matches = [];
for (const part of selectors) {
const element = root.querySelector(part);
if (element && !matches.includes(element)) {
matches.push(element);
}
}
return matches;
}
return [];
}
function isSignupPhoneCountrySelect(select) {
if (!select) {
return false;
}
return Array.from(select.options || []).some((option) => (
extractDialCodeFromText(getSignupPhoneOptionLabel(option))
|| /^[A-Z]{2}$/.test(normalizeSignupCountryOptionValue(option?.value))
));
}
function getSignupPhoneCountrySelect(phoneInput = getSignupPhoneInput()) {
const selects = [];
for (const root of getSignupPhoneControlRoots(phoneInput)) {
for (const select of querySignupPhoneCountryElements(root, 'select')) {
if (!selects.includes(select)) {
selects.push(select);
}
}
}
return selects.find(isSignupPhoneCountrySelect) || selects[0] || null;
}
function getSignupPhoneSelectedCountryOption(phoneInput = getSignupPhoneInput()) {
const select = getSignupPhoneCountrySelect(phoneInput);
if (!select || select.selectedIndex < 0) {
return null;
}
return select.options?.[select.selectedIndex] || null;
}
function getSignupPhoneCountryButtonText(phoneInput = getSignupPhoneInput()) {
const button = getSignupPhoneCountryButton(phoneInput);
if (!button) return '';
const valueNode = button.querySelector('.react-aria-SelectValue');
return String(valueNode?.textContent || button.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
function getSignupPhoneCountryButton(phoneInput = getSignupPhoneInput()) {
const candidates = [];
for (const root of getSignupPhoneControlRoots(phoneInput)) {
const buttons = querySignupPhoneCountryElements(
root,
'button[aria-haspopup="listbox"], [role="button"][aria-haspopup="listbox"], [role="combobox"][aria-haspopup="listbox"], button[aria-expanded]'
);
for (const button of buttons) {
if (!candidates.includes(button)) {
candidates.push(button);
}
}
}
return candidates.find((button) => isVisibleElement(button) && extractDialCodeFromText(getActionText(button)))
|| candidates.find(isVisibleElement)
|| null;
}
function getSignupPhoneDisplayedDialCode(phoneInput = getSignupPhoneInput()) {
const buttonDialCode = extractDialCodeFromText(getSignupPhoneCountryButtonText(phoneInput));
if (buttonDialCode) {
return buttonDialCode;
}
const inputRoot = phoneInput?.closest?.('fieldset, form, [data-rac], div') || document;
const visibleText = String(inputRoot?.textContent || '').replace(/\s+/g, ' ').trim();
const rootDialCode = extractDialCodeFromText(visibleText);
if (rootDialCode) {
return rootDialCode;
}
const pageDialCode = extractDialCodeFromText(getPageTextSnapshot());
if (pageDialCode) {
return pageDialCode;
}
return '';
}
function getSignupPhoneHiddenNumberInput(phoneInput = getSignupPhoneInput()) {
const form = getSignupPhoneForm(phoneInput);
if (!form || typeof form.querySelector !== 'function') {
return null;
}
return form.querySelector('input[name="phoneNumber"]');
}
function resolveSignupPhoneDialCodeFromNumber(phoneNumber = '', texts = []) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.resolveDialCodeFromPhoneNumber === 'function') {
return phoneCountryUtils.resolveDialCodeFromPhoneNumber(phoneNumber, texts);
}
const digits = normalizePhoneDigits(phoneNumber);
if (!digits) {
return '';
}
const textDialCodes = texts
.map((text) => normalizePhoneDigits(extractDialCodeFromText(text)))
.filter((dialCode) => dialCode && digits.startsWith(dialCode) && digits.length > dialCode.length)
.sort((left, right) => right.length - left.length);
if (textDialCodes[0]) {
return textDialCodes[0];
}
const knownDialCodes = [
'1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684',
'1721', '1758', '1767', '1784', '1809', '1829', '1849', '1868', '1869', '1876',
'971', '962', '886', '880', '856', '855', '852', '853', '673', '672', '670', '599', '598', '597', '596',
'595', '594', '593', '592', '591', '590', '509', '508', '507', '506', '505', '504', '503', '502', '501',
'423', '421', '420', '389', '387', '386', '385', '383', '382', '381', '380', '379', '378', '377', '376',
'375', '374', '373', '372', '371', '370', '359', '358', '357', '356', '355', '354', '353', '352', '351',
'350', '299', '298', '297', '291', '290', '269', '268', '267', '266', '265', '264', '263', '262', '261',
'260', '258', '257', '256', '255', '254', '253', '252', '251', '250', '249', '248', '247', '246', '245',
'244', '243', '242', '241', '240', '239', '238', '237', '236', '235', '234', '233', '232', '231', '230',
'229', '228', '227', '226', '225', '224', '223', '222', '221', '220', '218', '216', '213', '212', '211',
'98', '95', '94', '93', '92', '91', '90', '89', '88', '86', '84', '82', '81', '66', '65', '64', '63',
'62', '61', '60', '58', '57', '56', '55', '54', '53', '52', '51', '49', '48', '47', '46', '45', '44',
'43', '41', '40', '39', '36', '34', '33', '32', '31', '30', '27', '20', '7', '1',
];
return knownDialCodes.find((code) => digits.startsWith(code) && digits.length > code.length) || '';
}
function resolveSignupPhoneTargetDialCode(options = {}, targetOption = null) {
const optionDialCode = extractDialCodeFromText(getSignupPhoneOptionLabel(targetOption));
if (optionDialCode) {
return optionDialCode;
}
const countryText = String(options.countryLabel || '').trim();
if (/australia|澳大利亚/i.test(countryText)) return '61';
if (/thailand|泰国/i.test(countryText)) return '66';
if (/vietnam|越南/i.test(countryText)) return '84';
if (/england|united\s*kingdom|great\s*britain|\bbritain\b|英国|英格兰|uk|gb/i.test(countryText)) return '44';
return resolveSignupPhoneDialCodeFromNumber(options.phoneNumber);
}
function getSignupPhoneCountryTargetLabels(targetOption, options = {}) {
const labels = new Set();
const addLabel = (value) => {
getSignupCountryLabelAliases(value).forEach((alias) => labels.add(alias));
};
addLabel(options.countryLabel);
if (targetOption) {
getSignupPhoneCountryMatchLabels(targetOption).forEach(addLabel);
}
return Array.from(labels);
}
function doesSignupPhoneCountryTextMatchTarget(text, targetOption, options = {}) {
const normalizedText = normalizeSignupCountryLabel(text);
if (!normalizedText) {
return false;
}
const labels = getSignupPhoneCountryTargetLabels(targetOption, options);
if (labels.some((label) => (
label
&& (
normalizedText === label
|| (label.length > 1 && normalizedText.includes(label))
|| (normalizedText.length > 2 && label.includes(normalizedText))
)
))) {
return true;
}
const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption);
return Boolean(targetDialCode && extractDialCodeFromText(text) === targetDialCode);
}
function isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options = {}) {
const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption);
const displayedText = getSignupPhoneCountryButtonText(phoneInput);
const displayedDialCode = extractDialCodeFromText(displayedText);
if (targetDialCode && displayedDialCode) {
return displayedDialCode === targetDialCode
&& (!displayedText || doesSignupPhoneCountryTextMatchTarget(displayedText, targetOption, options));
}
if (displayedText && doesSignupPhoneCountryTextMatchTarget(displayedText, targetOption, options)) {
return true;
}
const selectedOption = getSignupPhoneSelectedCountryOption(phoneInput);
if (selectedOption && targetOption && isSameSignupCountryOption(selectedOption, targetOption)) {
return !displayedDialCode || !targetDialCode || displayedDialCode === targetDialCode;
}
return Boolean(selectedOption && !targetOption && targetDialCode && displayedDialCode === targetDialCode);
}
function findSignupPhoneCountryOptionByLabel(phoneInput, countryLabel) {
const select = getSignupPhoneCountrySelect(phoneInput);
if (!select) {
return null;
}
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.findOptionByCountryLabel === 'function') {
return phoneCountryUtils.findOptionByCountryLabel(select.options, countryLabel, {
document: typeof document !== 'undefined' ? document : null,
navigator: (typeof self !== 'undefined' ? self : globalThis)?.navigator || globalThis?.navigator || null,
getOptionLabel: getSignupPhoneOptionLabel,
});
}
const normalizedTargets = getSignupCountryLabelAliases(countryLabel);
if (normalizedTargets.length === 0) {
return null;
}
const options = Array.from(select.options || []);
return options.find((option) => (
getSignupPhoneCountryMatchLabels(option).some((label) => normalizedTargets.includes(normalizeSignupCountryLabel(label)))
))
|| options.find((option) => {
const normalizedLabels = getSignupPhoneCountryMatchLabels(option)
.map((label) => normalizeSignupCountryLabel(label))
.filter(Boolean);
return normalizedLabels.some((optionLabel) => normalizedTargets.some((normalizedTarget) => (
optionLabel.length > 2
&& normalizedTarget.length > 2
&& (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
)));
})
|| null;
}
function findSignupPhoneCountryOptionByPhoneNumber(phoneInput, phoneNumber) {
const select = getSignupPhoneCountrySelect(phoneInput);
if (!select) {
return null;
}
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.findOptionByPhoneNumber === 'function') {
return phoneCountryUtils.findOptionByPhoneNumber(select.options, phoneNumber, {
getOptionLabel: getSignupPhoneOptionLabel,
});
}
const digits = normalizePhoneDigits(phoneNumber);
if (!digits) {
return null;
}
let bestMatch = null;
let bestDialCodeLength = 0;
for (const option of Array.from(select.options || [])) {
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getSignupPhoneOptionLabel(option)));
if (!dialCode || !digits.startsWith(dialCode)) {
continue;
}
if (dialCode.length > bestDialCodeLength) {
bestMatch = option;
bestDialCodeLength = dialCode.length;
}
}
return bestMatch;
}
async function trySelectSignupPhoneCountryOption(select, targetOption, phoneInput = getSignupPhoneInput(), options = {}) {
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();
};
if (!select || !targetOption) {
return false;
}
const selectedOption = select.selectedIndex >= 0
? (select.options?.[select.selectedIndex] || null)
: null;
if (selectedOption && isSameSignupCountryOption(selectedOption, targetOption)) {
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'select', label: 'signup-phone-country-select' }, async () => {
dispatchSignupPhoneFieldEvents(select);
});
await sleep(120);
return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options);
}
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'select', label: 'signup-phone-country-select' }, async () => {
select.value = String(targetOption.value || '');
dispatchSignupPhoneFieldEvents(select);
});
await sleep(250);
return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options);
}
function getVisibleSignupPhoneCountryListboxOptions() {
const seen = new Set();
return Array.from(document.querySelectorAll('[role="listbox"] [role="option"], [role="option"]'))
.filter((option) => {
if (!option || seen.has(option)) {
return false;
}
seen.add(option);
return isVisibleElement(option);
});
}
function findSignupPhoneCountryListboxOption(targetOption, options = {}) {
const candidates = getVisibleSignupPhoneCountryListboxOptions();
const byLabel = candidates.find((option) => doesSignupPhoneCountryTextMatchTarget(getActionText(option), targetOption, options));
if (byLabel) {
return byLabel;
}
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.findElementByDialCode === 'function') {
const byPhoneNumber = phoneCountryUtils.findElementByDialCode(candidates, options.phoneNumber, {
getText: getActionText,
});
if (byPhoneNumber) {
return byPhoneNumber;
}
}
const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption);
if (!targetDialCode) {
const digits = normalizePhoneDigits(options.phoneNumber);
let bestMatch = null;
let bestDialCodeLength = 0;
for (const option of candidates) {
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getActionText(option)));
if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) {
continue;
}
bestMatch = option;
bestDialCodeLength = dialCode.length;
}
return bestMatch;
}
return candidates.find((option) => extractDialCodeFromText(getActionText(option)) === targetDialCode) || null;
}
async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options = {}) {
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 button = getSignupPhoneCountryButton(phoneInput);
if (!button) {
return false;
}
const getScrollableTargets = () => {
const seen = new Set();
const targets = [];
const pushTarget = (element) => {
if (!element || seen.has(element)) {
return;
}
seen.add(element);
const scrollHeight = Number(element.scrollHeight) || 0;
const clientHeight = Number(element.clientHeight) || 0;
if (scrollHeight > clientHeight + 2) {
targets.push(element);
}
};
getVisibleSignupPhoneCountryListboxOptions().forEach((option) => {
let current = option.parentElement || null;
let depth = 0;
while (current && depth < 6) {
pushTarget(current);
if (current === document.body || current === document.documentElement) {
break;
}
current = current.parentElement || null;
depth += 1;
}
});
Array.from(document.querySelectorAll('[role="listbox"]'))
.filter((listbox) => isVisibleElement(listbox))
.forEach(pushTarget);
return targets;
};
const dispatchListboxScroll = (element) => {
if (!element || typeof element.dispatchEvent !== 'function') {
return;
}
try {
element.dispatchEvent(typeof Event === 'function'
? new Event('scroll', { bubbles: true })
: { type: 'scroll' });
} catch {
try {
element.dispatchEvent({ type: 'scroll' });
} catch { }
}
};
const resetListboxScroll = () => {
getScrollableTargets().forEach((target) => {
if ((Number(target.scrollTop) || 0) > 0) {
target.scrollTop = 0;
dispatchListboxScroll(target);
}
});
};
const scrollListboxDown = () => {
let scrolled = false;
getScrollableTargets().forEach((target) => {
const before = Number(target.scrollTop) || 0;
const maxScrollTop = Math.max(0, (Number(target.scrollHeight) || 0) - (Number(target.clientHeight) || 0));
if (maxScrollTop <= before + 1) {
return;
}
const step = Math.max(360, Math.floor((Number(target.clientHeight) || 0) * 0.85));
target.scrollTop = Math.min(maxScrollTop, before + step);
dispatchListboxScroll(target);
scrolled = true;
});
return scrolled;
};
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-phone-country-listbox' }, async () => {
simulateClick(button);
});
await sleep(200);
resetListboxScroll();
const start = Date.now();
let reachedListEndAt = 0;
while (Date.now() - start < 8000) {
throwIfStopped();
const option = findSignupPhoneCountryListboxOption(targetOption, options);
if (option) {
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'select', label: 'signup-phone-country-listbox-option' }, async () => {
simulateClick(option);
});
await sleep(450);
if (isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options)) {
return true;
}
}
if (!scrollListboxDown()) {
reachedListEndAt += 1;
if (reachedListEndAt >= 6) {
break;
}
await sleep(150);
continue;
}
reachedListEndAt = 0;
await sleep(220);
}
return false;
}
async function ensureSignupPhoneCountrySelected(phoneInput, options = {}) {
const select = getSignupPhoneCountrySelect(phoneInput);
const hasCountryControl = Boolean(select || getSignupPhoneCountryButton(phoneInput));
if (!hasCountryControl) {
return {
hasSelect: false,
hasCountryControl: false,
matched: false,
selectedOption: null,
};
}
const byLabel = findSignupPhoneCountryOptionByLabel(phoneInput, options.countryLabel);
const byPhoneNumber = findSignupPhoneCountryOptionByPhoneNumber(phoneInput, options.phoneNumber);
const targets = [byLabel, byPhoneNumber, null].filter((target, index, list) => (
index === list.findIndex((item) => (
(!item && !target)
|| (item && target && isSameSignupCountryOption(item, target))
))
));
for (const targetOption of targets) {
if (await trySelectSignupPhoneCountryOption(select, targetOption, phoneInput, options)) {
return {
hasSelect: Boolean(select),
hasCountryControl: true,
matched: true,
selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
};
}
if (await trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options)) {
return {
hasSelect: Boolean(select),
hasCountryControl: true,
matched: true,
selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
};
}
}
return {
hasSelect: Boolean(select),
hasCountryControl: true,
matched: false,
selectedOption: getSignupPhoneSelectedCountryOption(phoneInput),
};
}
function toNationalPhoneNumber(value, dialCode) {
const digits = normalizePhoneDigits(value);
const normalizedDialCode = normalizePhoneDigits(dialCode);
const isExplicitInternational = /^\s*(?:\+|00)\s*\d/.test(String(value || '').trim());
if (!digits) {
return '';
}
if (normalizedDialCode && digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length) {
return digits.slice(normalizedDialCode.length);
}
if (isExplicitInternational) {
return digits;
}
return digits;
}
function toE164PhoneNumber(value, dialCode) {
const digits = normalizePhoneDigits(value);
const normalizedDialCode = normalizePhoneDigits(dialCode);
const isExplicitInternational = /^\s*(?:\+|00)\s*\d/.test(String(value || '').trim());
if (!digits) {
return '';
}
if (isExplicitInternational) {
return `+${digits}`;
}
if (!normalizedDialCode) {
return `+${digits}`;
}
if (digits.startsWith(normalizedDialCode)) {
return `+${digits}`;
}
if (digits.startsWith('0')) {
return `+${normalizedDialCode}${digits.slice(1)}`;
}
return `+${normalizedDialCode}${digits}`;
}
function getPhoneInputRenderedValue(phoneInput) {
return String(phoneInput?.value ?? phoneInput?.getAttribute?.('value') ?? '').trim();
}
function isPhoneInputValueVerified(actualValue, expectedValue, options = {}) {
const actualDigits = normalizePhoneDigits(actualValue);
const expectedDigits = normalizePhoneDigits(expectedValue);
if (!actualDigits || !expectedDigits) {
return false;
}
if (actualDigits === expectedDigits) {
return true;
}
const dialDigits = normalizePhoneDigits(options.dialCode);
const fullDigits = normalizePhoneDigits(options.phoneNumber);
if (fullDigits && actualDigits === fullDigits) {
return true;
}
if (!dialDigits) {
return false;
}
if (actualDigits === `${dialDigits}${expectedDigits}`) {
return true;
}
const localDigits = fullDigits && fullDigits.startsWith(dialDigits)
? fullDigits.slice(dialDigits.length)
: expectedDigits;
return dialDigits === '44' && actualDigits === `${dialDigits}0${localDigits}`;
}
async function waitForPhoneInputValue(phoneInput, expectedValue, options = {}) {
const {
timeout = 1800,
pollInterval = 100,
resolvePhoneInput = null,
phoneNumber = '',
dialCode = '',
} = options;
const startedAt = Date.now();
let currentInput = phoneInput;
while (Date.now() - startedAt < timeout) {
throwIfStopped();
currentInput = (typeof resolvePhoneInput === 'function' && resolvePhoneInput()) || currentInput;
if (isPhoneInputValueVerified(getPhoneInputRenderedValue(currentInput), expectedValue, { phoneNumber, dialCode })) {
return {
ok: true,
input: currentInput,
value: getPhoneInputRenderedValue(currentInput),
};
}
await sleep(pollInterval);
}
currentInput = (typeof resolvePhoneInput === 'function' && resolvePhoneInput()) || currentInput;
return {
ok: false,
input: currentInput,
value: getPhoneInputRenderedValue(currentInput),
};
}
function formatPhoneHiddenFormValue({ phoneNumber = '', dialCode = '', inputValue = '' } = {}) {
const fullDigits = normalizePhoneDigits(phoneNumber);
if (fullDigits) {
return `+${fullDigits}`;
}
const localDigits = normalizePhoneDigits(inputValue);
if (!localDigits) {
return '';
}
const dialDigits = normalizePhoneDigits(dialCode);
return dialDigits ? `+${dialDigits}${localDigits}` : localDigits;
}
function getPhoneHiddenValueInput(phoneInput) {
if (typeof getLoginPhoneHiddenValueInput === 'function') {
const loginHiddenInput = getLoginPhoneHiddenValueInput(phoneInput);
if (loginHiddenInput) {
return loginHiddenInput;
}
}
const form = phoneInput?.form || phoneInput?.closest?.('form') || null;
const root = form || phoneInput?.closest?.('fieldset, form, [data-rac], div') || document;
const candidates = Array.from(root?.querySelectorAll?.('input[name="phone"], input[name="phoneNumber"], input[type="hidden"][id*="phone" i]') || []);
return candidates.find((input) => {
if (!input || input === phoneInput) return false;
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
return type === 'hidden' || !isVisibleElement(input);
}) || null;
}
function setPhoneHiddenValue(input, value) {
const normalizedValue = String(value || '');
try {
const nativeInputValueSetter = typeof window !== 'undefined'
? Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
: null;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(input, normalizedValue);
} else {
input.value = normalizedValue;
}
} catch {
input.value = normalizedValue;
}
input.dispatchEvent?.(new Event('input', { bubbles: true }));
input.dispatchEvent?.(new Event('change', { bubbles: true }));
}
function syncPhoneHiddenFormValue(phoneInput, options = {}) {
const hiddenInput = getPhoneHiddenValueInput(phoneInput);
const hiddenValue = formatPhoneHiddenFormValue(options);
if (!hiddenInput || !hiddenValue) {
return null;
}
setPhoneHiddenValue(hiddenInput, hiddenValue);
return {
input: hiddenInput,
value: hiddenInput.value || '',
};
}
function isPhoneInputValueComplete(phoneInput, phoneNumber, dialCode, expectedLocalNumber = '') {
return isPhoneInputValueVerified(getPhoneInputRenderedValue(phoneInput), expectedLocalNumber || toNationalPhoneNumber(phoneNumber, dialCode), {
phoneNumber,
dialCode,
});
}
function getLoginPhoneFillCandidates(phoneNumber, dialCode, phoneInput = null) {
const inputValue = toNationalPhoneNumber(phoneNumber, dialCode);
const e164Value = toE164PhoneNumber(phoneNumber, dialCode);
const dialDigits = normalizePhoneDigits(dialCode);
const currentRenderedValue = getPhoneInputRenderedValue(phoneInput);
const currentDigits = normalizePhoneDigits(currentRenderedValue);
const shouldKeepDialPrefix = Boolean(
e164Value
&& (
String(currentRenderedValue || '').trim().startsWith('+')
|| (dialDigits && currentDigits === dialDigits)
)
);
const candidates = [];
const addCandidate = (value) => {
const normalizedValue = String(value || '').trim();
if (normalizedValue && !candidates.includes(normalizedValue)) {
candidates.push(normalizedValue);
}
};
if (shouldKeepDialPrefix) {
addCandidate(e164Value);
}
addCandidate(inputValue);
addCandidate(e164Value);
return candidates;
}
function getLoginPhoneSubmitButtonDiagnostics(button) {
if (!button) {
return {
present: false,
};
}
return {
present: true,
tag: (button.tagName || '').toLowerCase(),
type: String(button.getAttribute?.('type') || button.type || '').trim().toLowerCase(),
text: getActionText(button).slice(0, 80),
visible: isVisibleElement(button),
enabled: isActionEnabled(button),
disabled: Boolean(button.disabled),
ariaDisabled: String(button.getAttribute?.('aria-disabled') || '').trim(),
};
}
function getLoginPhoneInputCandidateDiagnostics(limit = 12) {
return collectPhoneInputCandidates('input', { allowGenericText: true }).slice(0, limit);
}
async function fillLoginPhoneInputAndConfirm(phoneInput, options = {}) {
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 {
phoneNumber = '',
dialCode = '',
visibleStep = 7,
resolvePhoneInput = null,
maxAttempts = 3,
} = options;
const inputValue = toNationalPhoneNumber(phoneNumber, dialCode);
if (!inputValue) {
throw new Error(`\u6b65\u9aa4 ${visibleStep}\uff1a\u624b\u673a\u53f7\u4e3a\u7a7a\uff0c\u65e0\u6cd5\u586b\u5199\u3002`);
}
let currentInput = phoneInput;
let lastVerification = { ok: false, input: currentInput, value: getPhoneInputRenderedValue(currentInput) };
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
throwIfStopped();
currentInput = (typeof resolvePhoneInput === 'function' && resolvePhoneInput()) || currentInput;
if (!currentInput) {
break;
}
const fillCandidates = getLoginPhoneFillCandidates(phoneNumber, dialCode, currentInput);
for (const attemptedValue of fillCandidates) {
currentInput.focus?.();
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'login-phone-number' }, async () => {
fillInput(currentInput, attemptedValue);
});
lastVerification = await waitForPhoneInputValue(currentInput, inputValue, {
resolvePhoneInput,
phoneNumber,
dialCode,
timeout: 1600,
pollInterval: 100,
});
if (lastVerification.ok) {
const verifiedInput = lastVerification.input || currentInput;
const hiddenSync = await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'hidden-sync', label: 'login-phone-hidden-sync' }, async () => (
syncPhoneHiddenFormValue(verifiedInput, { phoneNumber, dialCode, inputValue })
));
const expectedHiddenDigits = normalizePhoneDigits(phoneNumber) || `${normalizePhoneDigits(dialCode)}${normalizePhoneDigits(inputValue)}`;
if (hiddenSync && expectedHiddenDigits && normalizePhoneDigits(hiddenSync.value) !== expectedHiddenDigits) {
throw new Error(`\u6b65\u9aa4 ${visibleStep}\uff1a\u624b\u673a\u53f7\u9690\u85cf\u63d0\u4ea4\u5b57\u6bb5\u540c\u6b65\u5931\u8d25\uff0c\u671f\u671b ${expectedHiddenDigits}\uff0c\u5b9e\u9645 ${normalizePhoneDigits(hiddenSync.value) || '\u7a7a'}\u3002`);
}
log(
`\u6b65\u9aa4 ${visibleStep}\uff1a\u624b\u673a\u53f7\u8f93\u5165\u6821\u9a8c\u901a\u8fc7 ${JSON.stringify({
attemptedValue,
renderedValue: lastVerification.value,
input: getLoginPhoneInputDiagnostics(verifiedInput),
hidden: getLoginPhoneHiddenValueDiagnostics(hiddenSync?.input || getPhoneHiddenValueInput(verifiedInput)),
})}`,
'info',
{ step: visibleStep, stepKey: 'oauth-login' }
);
return {
input: verifiedInput,
inputValue,
attemptedValue,
renderedValue: lastVerification.value,
hiddenInput: hiddenSync?.input || null,
hiddenValue: hiddenSync?.value || '',
};
}
}
const currentDigits = normalizePhoneDigits(lastVerification.value);
log(
`\u6b65\u9aa4 ${visibleStep}\uff1a\u624b\u673a\u53f7\u8f93\u5165\u6846\u672a\u7a33\u5b9a\u5199\u5165\uff08\u7b2c ${attempt}/${maxAttempts} \u6b21\uff09\uff0c\u671f\u671b\u672c\u5730\u53f7 ${inputValue}\uff0c\u5f53\u524d\u503c ${currentDigits || '\u7a7a'}\uff0c\u51c6\u5907\u91cd\u8bd5\u3002`,
'warn',
{ step: visibleStep, stepKey: 'oauth-login' }
);
await sleep(200);
}
const actualDigits = normalizePhoneDigits(lastVerification.value);
throw new Error(`\u6b65\u9aa4 ${visibleStep}\uff1a\u624b\u673a\u53f7\u586b\u5199\u540e\u6821\u9a8c\u5931\u8d25\uff0c\u5b8c\u6574\u53f7\u7801 ${phoneNumber}\uff0c\u533a\u53f7 +${dialCode || '\u672a\u8bc6\u522b'}\uff0c\u671f\u671b\u8f93\u5165\u672c\u5730\u53f7 ${inputValue}\uff0c\u5b9e\u9645\u8f93\u5165\u6846\u4e3a ${actualDigits || '\u7a7a'}\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\u3002`);
}
function resolveSignupPhoneDialCode(phoneInput, options = {}) {
const { phoneNumber = '', countryLabel = '' } = options;
const displayedDialCode = getSignupPhoneDisplayedDialCode(phoneInput);
if (displayedDialCode) {
return displayedDialCode;
}
const countryText = String(countryLabel || '').trim();
if (/australia|澳大利亚/i.test(countryText)) return '61';
if (/thailand|泰国/i.test(countryText)) return '66';
if (/vietnam|越南/i.test(countryText)) return '84';
if (/england|united kingdom|英国|uk/i.test(countryText)) return '44';
const digits = normalizePhoneDigits(phoneNumber);
const knownDialCodes = ['66', '84', '61', '44', '1', '81', '82', '86', '852', '855', '856', '60', '62', '63', '65'];
return knownDialCodes.find((code) => digits.startsWith(code) && digits.length > code.length) || '';
}
async function waitForSignupPhoneEntryState(options = {}) {
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 {
timeout = 20000,
step = 2,
} = options;
const start = Date.now();
const maxSignupEntryClickRetries = 5;
const maxSignupEntryClickAttempts = maxSignupEntryClickRetries + 1;
let lastTriggerClickAt = 0;
let clickAttempts = 0;
let lastSwitchToPhoneAt = 0;
let lastMoreOptionsClickAt = 0;
let slowSnapshotLogged = false;
while (Date.now() - start < timeout) {
throwIfStopped();
const snapshot = inspectSignupEntryState();
if (snapshot.state === 'password_page') {
return snapshot;
}
if (snapshot.state === 'phone_entry' && snapshot.phoneInput) {
return snapshot;
}
if (snapshot.state === 'email_entry') {
const switchToPhone = snapshot.switchToPhoneTrigger || findSignupUsePhoneTrigger();
if (switchToPhone && Date.now() - lastSwitchToPhoneAt >= 1500) {
lastSwitchToPhoneAt = Date.now();
log(`步骤 ${step}:检测到邮箱输入模式,正在切换到手机号注册入口...`);
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'switch-to-signup-phone' }, async () => {
simulateClick(switchToPhone);
});
} else {
const moreOptionsTrigger = findSignupMoreOptionsTrigger();
if (moreOptionsTrigger && Date.now() - lastMoreOptionsClickAt >= 1500) {
lastMoreOptionsClickAt = Date.now();
log(`步骤 ${step}:手机号入口可能隐藏在更多选项中,正在展开更多选项...`);
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'signup-phone-more-options' }, async () => {
simulateClick(moreOptionsTrigger);
});
} else if (!switchToPhone && !slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:尚未找到手机号入口,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
}
await sleep(250);
continue;
}
if (snapshot.state === 'entry_home' && snapshot.signupTrigger) {
if (Date.now() - lastTriggerClickAt >= 1500) {
if (clickAttempts >= maxSignupEntryClickAttempts) {
log(`步骤 ${step}:官网注册入口已完成 ${maxSignupEntryClickRetries} 次重试,页面仍未进入手机号输入页,停止重试。`, 'warn');
return snapshot;
}
lastTriggerClickAt = Date.now();
clickAttempts += 1;
const retryAttempt = clickAttempts - 1;
log(retryAttempt > 0
? `步骤 ${step}:上次点击后仍未进入手机号输入页,等待 3 秒后重试点击官网注册入口(重试 ${retryAttempt}/${maxSignupEntryClickRetries})...`
: `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`);
await sleep(3000);
throwIfStopped();
const clickTarget = findSignupEntryTrigger({ allowHiddenFallback: false }) || snapshot.signupTrigger;
if (!isVisibleElement(clickTarget)) {
log(`步骤 ${step}:注册入口仍处于不可见状态,继续按入口重试节奏尝试恢复点击...`, 'warn');
}
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
simulateClick(clickTarget);
});
}
await sleep(250);
continue;
}
if (!slowSnapshotLogged && Date.now() - start >= 5000) {
slowSnapshotLogged = true;
log(`步骤 ${step}:等待手机号注册入口超过 5 秒,页面诊断快照:${JSON.stringify(getSignupEntryDiagnostics())}`, 'warn');
}
await sleep(250);
}
const finalSnapshot = inspectSignupEntryState();
log(`步骤 ${step}:等待手机号注册入口超时,最终状态快照:${JSON.stringify(getSignupEntryStateSummary(finalSnapshot))}`, 'warn');
return finalSnapshot;
}
async function submitSignupPhoneNumberAndContinue(payload = {}) {
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 phoneNumber = String(payload.phoneNumber || '').trim();
const countryLabel = String(payload.countryLabel || '').trim();
if (!phoneNumber) {
throw new Error('未提供手机号,步骤 2 无法继续。');
}
const snapshot = await waitForSignupPhoneEntryState({ timeout: 25000, step: 2 });
if (snapshot.state === 'password_page') {
log('步骤 2:当前已在密码页,无需重复提交手机号。');
return {
alreadyOnPasswordPage: true,
url: snapshot.url || location.href,
};
}
if (snapshot.state !== 'phone_entry' || !snapshot.phoneInput) {
throw new Error(`步骤 2:未找到可用的手机号输入入口。URL: ${location.href}`);
}
const countrySelection = await ensureSignupPhoneCountrySelected(snapshot.phoneInput, {
countryLabel,
phoneNumber,
});
if (countrySelection.hasCountryControl && !countrySelection.matched) {
const currentCountryText = getSignupPhoneCountryButtonText(snapshot.phoneInput) || '未知';
const targetDialCode = resolveSignupPhoneTargetDialCode({ countryLabel, phoneNumber }, countrySelection.selectedOption);
const targetLabel = targetDialCode
? `目标区号 +${targetDialCode}(号码 ${phoneNumber}${countryLabel ? `,国家 ${countryLabel}` : ''})`
: (countryLabel || phoneNumber);
throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${targetLabel},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
}
const dialCode = resolveSignupPhoneDialCode(snapshot.phoneInput, {
phoneNumber,
countryId: payload.countryId,
countryLabel,
});
const inputValue = toNationalPhoneNumber(phoneNumber, dialCode);
if (!inputValue) {
throw new Error('步骤 2:手机号为空,无法填写。');
}
log(`步骤 2:正在填写手机号:${phoneNumber}`);
await humanPause(500, 1400);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'fill', label: 'signup-phone-number' }, async () => {
fillInput(snapshot.phoneInput, inputValue);
});
const hiddenPhoneNumberInput = getSignupPhoneHiddenNumberInput(snapshot.phoneInput);
const e164PhoneNumber = toE164PhoneNumber(phoneNumber, dialCode);
if (hiddenPhoneNumberInput && e164PhoneNumber) {
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'hidden-sync', label: 'signup-phone-hidden-sync' }, async () => {
fillInput(hiddenPhoneNumberInput, e164PhoneNumber);
});
}
log(`步骤 2:手机号已填写:${phoneNumber}${dialCode ? `(区号 +${dialCode},本地号 ${inputValue})` : ''}`);
const continueButton = getSignupEmailContinueButton({ allowDisabled: true });
if (!continueButton || !isActionEnabled(continueButton)) {
throw new Error(`步骤 2:未找到可点击的“继续”按钮。URL: ${location.href}`);
}
log('步骤 2:手机号已准备提交,正在前往下一页...');
window.setTimeout(async () => {
try {
throwIfStopped();
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'submit', label: 'submit-signup-phone' }, async () => {
simulateClick(continueButton);
});
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:openai-auth] deferred signup phone submit failed:', error?.message || error);
}
}
}, 120);
return {
submitted: true,
deferredSubmit: true,
phoneNumber,
phoneInputValue: snapshot.phoneInput?.value || inputValue,
url: location.href,
};
}
// ============================================================
// Step 2: Click Register, fill email, then continue to password page
// ============================================================
async function step2_clickRegister(payload = {}) {
if (payload?.signupMethod === 'phone' || payload?.phoneNumber) {
return submitSignupPhoneNumberAndContinue(payload);
}
const { email } = payload;
return fillSignupEmailAndContinue(email, 2);
}
// ============================================================
// Step 3: Fill Password
// ============================================================
async function step3_fillEmailPassword(payload) {
const resolvePasswordPageInfo = (rawUrl = '') => {
const url = String(rawUrl || '').trim() || String(location.href || '');
let path = String(location.pathname || '').trim();
try {
path = new URL(url, 'https://auth.openai.com').pathname || path;
} catch {
// Keep the current pathname fallback; this only tags the completion payload.
}
const mode = /\/log-in\/password(?:[/?#]|$)/i.test(path)
? 'login'
: (/\/(?:create-account|u\/signup|signup)\/password(?:[/?#]|$)/i.test(path) ? 'signup' : '');
return { url, path, mode };
};
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 { email, password } = payload;
if (!password) throw new Error('未提供密码,步骤 3 需要可用密码。');
const normalizedEmail = String(email || '').trim().toLowerCase();
const accountIdentifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase() === 'phone'
? 'phone'
: 'email';
const accountIdentifier = String(payload?.accountIdentifier || email || payload?.phoneNumber || '').trim();
let snapshot = inspectSignupEntryState();
if (snapshot.state === 'entry_home') {
throw new Error('当前仍停留在 ChatGPT 官网首页,请先完成步骤 2。');
}
if (
snapshot.state === 'phone_verification_page'
|| snapshot.state === 'verification_page'
|| snapshot.state === 'profile_page'
|| snapshot.state === 'logged_in_home'
) {
const completionPayload = {
email: email || '',
phoneNumber: String(payload?.phoneNumber || '').trim(),
accountIdentifierType,
accountIdentifier,
signupVerificationRequestedAt: (
snapshot.state === 'phone_verification_page'
|| snapshot.state === 'verification_page'
) ? Date.now() : null,
skippedPasswordPage: true,
deferredSubmit: false,
...(snapshot.skipProfileStep ? { skipProfileStep: true } : {}),
};
log('步骤 3:当前页面已进入验证码或后续阶段,密码页按已跳过处理。', 'warn');
reportComplete(3, completionPayload);
return completionPayload;
}
if (snapshot.state === 'email_entry') {
const transition = await fillSignupEmailAndContinue(email, 3);
if (!transition.alreadyOnPasswordPage) {
await sleep(1200);
await ensureSignupPasswordPageReady();
}
snapshot = inspectSignupEntryState();
}
if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
await ensureSignupPasswordPageReady();
snapshot = inspectSignupEntryState();
}
if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
logSignupPasswordDiagnostics('步骤 3:未能识别可填写的密码输入框');
}
if (snapshot.state !== 'password_page' || !snapshot.passwordInput) {
throw new Error('在密码页未找到密码输入框。URL: ' + location.href);
}
if (normalizedEmail && snapshot.displayedEmail && snapshot.displayedEmail !== normalizedEmail) {
throw new Error(`当前密码页邮箱为 ${snapshot.displayedEmail},与目标邮箱 ${email} 不一致,请先回到步骤 1 重新开始。`);
}
const passwordPageInfo = resolvePasswordPageInfo(snapshot.url || location.href);
await humanPause(600, 1500);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'fill', label: 'signup-password' }, async () => {
fillInput(snapshot.passwordInput, password);
});
log('步骤 3:密码已填写');
const submitBtn = snapshot.submitButton
|| getSignupPasswordSubmitButton({ allowDisabled: true })
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|続行|続ける|次へ|サインアップ|登録|作成|create/i, 5000).catch(() => null);
if (!submitBtn) {
logSignupPasswordDiagnostics('步骤 3:未找到可提交的密码页按钮');
} else if (typeof findOneTimeCodeLoginTrigger === 'function' && findOneTimeCodeLoginTrigger()) {
logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info');
}
const isLoginPasswordPage = passwordPageInfo.mode === 'login';
const signupVerificationRequestedAt = submitBtn && !isLoginPasswordPage ? Date.now() : null;
const completionPayload = {
email,
phoneNumber: String(payload?.phoneNumber || '').trim(),
accountIdentifierType,
accountIdentifier,
signupVerificationRequestedAt,
deferredSubmit: Boolean(submitBtn),
passwordPageUrl: passwordPageInfo.url,
passwordPagePath: passwordPageInfo.path,
passwordPageMode: passwordPageInfo.mode,
...(isLoginPasswordPage ? { passwordLoginFlow: true } : {}),
};
reportComplete(3, completionPayload);
if (submitBtn) {
window.setTimeout(async () => {
try {
throwIfStopped();
await sleep(500);
await humanPause(500, 1300);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'submit', label: 'submit-signup-password' }, async () => {
simulateClick(submitBtn);
});
log('步骤 3:表单已提交');
} catch (error) {
if (!isStopError(error)) {
console.error('[MultiPage:openai-auth] deferred step 3 submit failed:', error?.message || error);
}
}
}, 120);
}
return completionPayload;
}
// ============================================================
// Fill Verification Code (used by step 4 and step 7)
// ============================================================
const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|验证码错误|コードが正しくありません|コードが間違っています|無効なコード|認証コードが正しくありません|確認コードが正しくありません|code\s+(?:is\s+)?incorrect|invalid\s+code|incorrect\s+code|try\s+again/i;
const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|代码不正确|受信(?:トレイ|箱)を確認|(?:認証|確認)?コードを入力|送信した(?:認証|確認)?コード|メールで送信|再送信|email\s+verification|check\s+your\s+inbox|enter\s+the\s+code|we\s+just\s+sent|we\s+emailed|resend/i;
const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|ChatGPT\s*(?:を使用して|で)\s*Codex\s*(?:に)?(?:ログイン|サインイン)|Codex\s*(?:に)?(?:ログイン|サインイン)|認可|承認|sign\s+in\s+to\s+codex(?:\s+with\s+chatgpt)?|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
const OAUTH_CONSENT_FORM_SELECTOR = 'form[action*="/sign-in-with-chatgpt/" i][action*="/consent" i]';
const CONTINUE_ACTION_PATTERN = /继续|続行|続ける|次へ|continue/i;
const ADD_PHONE_PAGE_PATTERN = /add[\s-]*(?:a\s+)?phone|添加(?:手机|手机号|电话号码)|绑定(?:手机|手机号|电话号码)|验证(?:你的|您)?(?:手机|手机号|电话号码)|需要(?:手机|手机号|电话号码)|提供(?:手机|手机号|电话号码)|電話番号(?:を)?(?:追加|登録|確認|認証)|電話番号が必要|携帯(?:電話)?番号(?:を)?(?:追加|登録|確認|認証)|provide\s+(?:a\s+)?phone\s+number|phone\s+number\s+(?:required|verification)|verify\s+(?:your\s+)?phone|confirm\s+(?:your\s+)?phone/i;
const ADD_EMAIL_PAGE_PATTERN = /add[\s-]*email|添加(?:电子邮件|邮箱)|要求提供(?:电子邮件|邮箱)地址|提供(?:电子邮件|邮箱)地址|(?:メールアドレス|メール|電子メール)(?:を)?(?:追加|登録|提供)|メールアドレスが必要|provide\s+(?:an?\s+)?email\s+address|email\s+address\s+required/i;
const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|アカウントを作成できません|アカウント作成に失敗|もう一度お試し|問題が発生しました|無効な(?:生年月日|誕生日|日付)|生年月日|誕生日|unable\s+to\s+create\s+(?:your\s+)?account|couldn'?t\s+create\s+(?:your\s+)?account|something\s+went\s+wrong|invalid\s+(?:birthday|birth|date)|生日|出生日期/i;
const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|問題が発生しました|エラーが発生しました|something\s+went\s+wrong|oops/i;
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时|タイムアウト|failed\s+to\s+fetch|network\s+error|fetch\s+failed|ネットワークエラー|取得に失敗/i;
const AUTH_ROUTE_ERROR_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/email-verification/i;
const STEP4_405_RECOVERY_ERROR_PREFIX = 'STEP4_405_RECOVERY_LIMIT::';
const STEP4_405_RECOVERY_LIMIT = 3;
const SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX = 'SIGNUP_USER_ALREADY_EXISTS::';
const SIGNUP_PHONE_PASSWORD_MISMATCH_ERROR_PREFIX = 'SIGNUP_PHONE_PASSWORD_MISMATCH::';
const AUTH_MAX_CHECK_ATTEMPTS_ERROR_PREFIX = 'AUTH_MAX_CHECK_ATTEMPTS::';
const STEP8_EMAIL_IN_USE_ERROR_PREFIX = 'STEP8_EMAIL_IN_USE::';
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|この(?:メールアドレス|メール|電子メール)(?:に関連付けられた)?アカウントは(?:既に|すでに)存在|メールアドレス.*(?:既に|すでに)存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
const SIGNUP_PHONE_PASSWORD_MISMATCH_PATTERN = /incorrect\s+phone\s+number\s+or\s+password|phone\s+number\s+or\s+password|電話番号またはパスワード|電話番号.*アカウントは(?:既に|すでに)存在|与此(?:电话|手机)号码相关联的帐户已存在|account\s+associated\s+with\s+this\s+phone\s+number\s+already\s+exists/i;
const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({
detailPattern: AUTH_TIMEOUT_ERROR_DETAIL_PATTERN,
getActionText,
getPageTextSnapshot,
humanPause,
isActionEnabled,
isVisibleElement,
log,
routeErrorPattern: AUTH_ROUTE_ERROR_PATTERN,
simulateClick,
sleep,
throwIfStopped,
titlePattern: AUTH_TIMEOUT_ERROR_TITLE_PATTERN,
}) || null;
function getVerificationErrorText() {
const messages = [];
const selectors = [
'.react-aria-FieldError',
'[slot="errorMessage"]',
'[id$="-error"]',
'[data-invalid="true"] + *',
'[aria-invalid="true"] + *',
'[class*="error"]',
];
for (const selector of selectors) {
document.querySelectorAll(selector).forEach((el) => {
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (text) {
messages.push(text);
}
});
}
const invalidInput = document.querySelector(`${VERIFICATION_CODE_INPUT_SELECTOR}[aria-invalid="true"], ${VERIFICATION_CODE_INPUT_SELECTOR}[data-invalid="true"]`);
if (invalidInput) {
const wrapper = invalidInput.closest('form, [data-rac], ._root_18qcl_51, div');
if (wrapper) {
const text = (wrapper.textContent || '').replace(/\s+/g, ' ').trim();
if (text) {
messages.push(text);
}
}
}
return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
}
function createSignupUserAlreadyExistsError() {
return new Error(
`${SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX}步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。`
);
}
function createSignupPhonePasswordMismatchError(detailText = '') {
const detail = String(detailText || '').replace(/\s+/g, ' ').trim();
const suffix = detail ? `页面提示:${detail}` : '页面提示注册手机号不可继续使用,需重新开始当前轮。';
return new Error(
`${SIGNUP_PHONE_PASSWORD_MISMATCH_ERROR_PREFIX}步骤 3:检测到注册手机号异常,需要重新开始当前轮。${suffix}`
);
}
function createAuthMaxCheckAttemptsError() {
return new Error(`${AUTH_MAX_CHECK_ATTEMPTS_ERROR_PREFIX}max_check_attempts on auth retry page; restart the current auth step without clicking Retry.`);
}
function createStep8EmailInUseError() {
return new Error(`${STEP8_EMAIL_IN_USE_ERROR_PREFIX}email_in_use on add-email verification page; choose a different email.`);
}
function getVisibleFieldErrorText() {
const selectors = [
'.react-aria-FieldError',
'[slot="errorMessage"]',
'[id$="-error"]',
'[data-invalid="true"] + *',
'[aria-invalid="true"] + *',
'[class*="error"]',
'[role="alert"]',
];
for (const selector of selectors) {
const match = Array.from(document.querySelectorAll(selector)).find((el) => {
if (!isVisibleElement(el)) return false;
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
return Boolean(text);
});
if (match) {
return (match.textContent || '').replace(/\s+/g, ' ').trim();
}
}
return '';
}
function getSignupPasswordFieldErrorText() {
const text = getVisibleFieldErrorText();
if (text && SIGNUP_PHONE_PASSWORD_MISMATCH_PATTERN.test(text)) {
return text;
}
const passwordInput = getSignupPasswordInput();
if (passwordInput) {
const wrapper = passwordInput.closest('form, [data-rac], [role="group"], section, div');
const wrapperText = (wrapper?.textContent || '').replace(/\s+/g, ' ').trim();
if (wrapperText && SIGNUP_PHONE_PASSWORD_MISMATCH_PATTERN.test(wrapperText)) {
return wrapperText;
}
}
return '';
}
function isStep5Ready() {
return Boolean(
document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
);
}
function isSignupProfilePageUrl(rawUrl = location.href) {
const url = String(rawUrl || '').trim();
if (!url) {
return false;
}
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
return false;
}
return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
} catch {
return false;
}
}
function isLikelyLoggedInChatgptHomeUrl(rawUrl = location.href) {
const url = String(rawUrl || '').trim();
if (!url) {
return false;
}
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(host)) {
return false;
}
const path = String(parsed.pathname || '');
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
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;
}
}
function isStep5CompletionChatgptUrl(rawUrl = location.href) {
const url = String(rawUrl || '').trim();
if (!url) {
return false;
}
try {
const parsed = new URL(url);
const protocol = String(parsed.protocol || '').toLowerCase();
const host = String(parsed.hostname || '').toLowerCase();
if (protocol !== 'https:' || !['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
return false;
}
const path = String(parsed.pathname || '');
return !/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path);
} catch {
return false;
}
}
function getStep4PostVerificationState(options = {}) {
const { ignoreVerificationVisibility = false } = options;
// Newer auth flows can briefly render profile fields before the email-verification
// form fully exits. Do not advance to Step 5 while verification UI is still present.
if (!ignoreVerificationVisibility && isVerificationPageStillVisible()) {
return null;
}
if (isStep5Ready() || isSignupProfilePageUrl()) {
return {
state: 'step5',
url: location.href,
};
}
if (isLikelyLoggedInChatgptHomeUrl()) {
return {
state: 'logged_in_home',
skipProfileStep: true,
url: location.href,
};
}
return null;
}
function getPageTextSnapshot() {
return (document.body?.innerText || document.body?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
function getLoginVerificationDisplayedEmail() {
const pageText = getPageTextSnapshot();
const matches = pageText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || [];
return matches[0] ? String(matches[0]).trim().toLowerCase() : '';
}
function getPhoneVerificationDisplayedPhone() {
const pageText = getPageTextSnapshot();
const matches = pageText.match(/\+\d[\d\s-]{6,}\d/g) || [];
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 resolveChooseAccountCardTarget(element, normalizedEmail = '') {
let current = element;
let bestTarget = null;
while (current && current !== document.body) {
if (isChooseAccountRemovalAction(current) || !isVisibleElement(current)) {
current = current.parentElement;
continue;
}
const actionTarget = resolveChooseAccountClickTarget(current);
if (actionTarget) {
return actionTarget;
}
const text = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(current));
if (text.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text)) {
bestTarget = current;
}
const closestCard = current.closest?.(CHOOSE_ACCOUNT_CARD_SELECTOR) || null;
if (
closestCard
&& closestCard !== current
&& closestCard !== document.body
&& isVisibleElement(closestCard)
&& !isChooseAccountRemovalAction(closestCard)
) {
const cardText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(closestCard));
if (cardText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(cardText)) {
const cardActionTarget = resolveChooseAccountClickTarget(closestCard);
return cardActionTarget || closestCard;
}
}
current = current.parentElement;
}
return bestTarget;
}
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) {
const target = resolveChooseAccountCardTarget(node, normalizedEmail);
if (target) {
return target;
}
}
return null;
}
function findChooseAccountOtherAccountButton() {
const candidates = Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR))
.filter((element) => isVisibleElement(element) && isActionEnabled(element));
return candidates.find((candidate) => {
if (isChooseAccountRemovalAction(candidate)) {
return false;
}
const text = getChooseAccountCandidateText(candidate);
return CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text);
}) || null;
}
function getChooseAccountListedEmails() {
const emailPattern = /[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/ig;
const emails = new Set();
const nodes = [
...Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR)),
...Array.from(document.querySelectorAll('body *')).filter((element) => isVisibleElement(element)),
];
for (const node of nodes) {
if (isChooseAccountRemovalAction(node)) {
continue;
}
const text = getChooseAccountCandidateText(node);
for (const match of text.matchAll(emailPattern)) {
emails.add(normalizeAuthAccountIdentifier(match[0]));
}
}
return Array.from(emails).filter(Boolean);
}
async function resolveChooseAccountAction(email, maxRounds = 8) {
let otherAccountButton = null;
let latestSnapshot = normalizeStep6Snapshot(inspectLoginAuthState());
for (let round = 0; round < maxRounds; round += 1) {
throwIfStopped();
latestSnapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (latestSnapshot.state !== 'unknown' && latestSnapshot.state !== 'choose_account_page') {
return {
snapshot: latestSnapshot,
};
}
const target = findChooseAccountButtonForEmail(email);
if (target) {
return {
target,
snapshot: latestSnapshot,
};
}
otherAccountButton = findChooseAccountOtherAccountButton() || otherAccountButton;
const listedEmails = getChooseAccountListedEmails();
if (otherAccountButton && round >= 2 && (listedEmails.length > 0 || round >= 4)) {
return {
otherAccountButton,
snapshot: latestSnapshot,
};
}
if (round < maxRounds - 1) {
await sleep(round === 0 ? 300 : 500);
}
}
return {
otherAccountButton,
snapshot: latestSnapshot,
};
}
function getOAuthConsentForm() {
return document.querySelector(OAUTH_CONSENT_FORM_SELECTOR);
}
function getPrimaryContinueButton() {
const consentForm = getOAuthConsentForm();
if (consentForm) {
const formButtons = Array.from(
consentForm.querySelectorAll('button[type="submit"], input[type="submit"], [role="button"]')
);
const formContinueButton = formButtons.find((el) => {
if (!isVisibleElement(el)) return false;
const ddActionName = el.getAttribute?.('data-dd-action-name') || '';
return ddActionName === 'Continue' || CONTINUE_ACTION_PATTERN.test(getActionText(el));
});
if (formContinueButton) {
return formContinueButton;
}
const firstVisibleSubmit = formButtons.find(isVisibleElement);
if (firstVisibleSubmit) {
return firstVisibleSubmit;
}
}
const continueBtn = document.querySelector(
`${OAUTH_CONSENT_FORM_SELECTOR} button[type="submit"], button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107`
);
if (continueBtn && isVisibleElement(continueBtn)) {
return continueBtn;
}
const buttons = document.querySelectorAll('button, [role="button"]');
return Array.from(buttons).find((el) => {
if (!isVisibleElement(el)) return false;
const ddActionName = el.getAttribute?.('data-dd-action-name') || '';
return ddActionName === 'Continue' || CONTINUE_ACTION_PATTERN.test(getActionText(el));
}) || null;
}
function isOAuthConsentPage() {
const pageText = getPageTextSnapshot();
if (OAUTH_CONSENT_PAGE_PATTERN.test(pageText)) {
return true;
}
if (getOAuthConsentForm()) {
return true;
}
return /\bcodex\b/i.test(pageText) && /\bchatgpt\b/i.test(pageText) && Boolean(getPrimaryContinueButton());
}
function isVerificationPageStillVisible() {
if (getCurrentAuthRetryPageState('signup_password') || getCurrentAuthRetryPageState('login')) {
return false;
}
if (isPhoneVerificationPageReady()) {
return false;
}
if (getVerificationCodeTarget()) return true;
if (findResendVerificationCodeTrigger({ allowDisabled: true })) return true;
if (document.querySelector('form[action*="email-verification" i]')) return true;
if (!isEmailVerificationPage()) {
return false;
}
return VERIFICATION_PAGE_PATTERN.test(getPageTextSnapshot());
}
function isAddPhonePageReady() {
const path = `${location.pathname || ''} ${location.href || ''}`;
if (/\/add-phone(?:[/?#]|$)/i.test(path)) return true;
const addPhoneForm = document.querySelector('form[action*="/add-phone" i]');
if (addPhoneForm && isVisibleElement(addPhoneForm)) {
return true;
}
return ADD_PHONE_PAGE_PATTERN.test(getPageTextSnapshot());
}
function isAddEmailPageReady() {
const path = `${location.pathname || ''} ${location.href || ''}`;
if (/\/add-email(?:[/?#]|$)/i.test(path)) {
return true;
}
const emailInput = getLoginEmailInput();
if (!emailInput) {
return false;
}
const form = emailInput.form || emailInput.closest?.('form') || null;
const formAction = String(form?.getAttribute?.('action') || form?.action || '');
if (/\/add-email(?:[/?#]|$)/i.test(formAction)) {
return true;
}
const pageText = getPageTextSnapshot();
return ADD_EMAIL_PAGE_PATTERN.test(pageText)
&& !/继续使用(?:电子邮件地址|邮箱)登录|(?:メールアドレス|メール|電子メール)(?:で|を使用して)?(?:続行|続ける|ログイン|サインイン)|continue\s+using\s+(?:an?\s+)?email(?:\s+address)?\s+(?:to\s+)?(?:log\s*in|sign\s*in)|continue\s+with\s+email/i.test(pageText);
}
function isPhoneVerificationPageReady() {
const path = `${location.pathname || ''} ${location.href || ''}`;
const isPhoneVerificationRoute = /\/phone-verification(?:[/?#]|$)/i.test(path);
const isContactVerificationRoute = /\/contact-verification(?:[/?#]|$)/i.test(path);
if (isContactVerificationRoute && getContactVerificationServerErrorText()) {
return false;
}
if (isPhoneVerificationRoute || isContactVerificationRoute) {
return true;
}
const form = document.querySelector('form[action*="/phone-verification" i]');
if (form && isVisibleElement(form)) {
return true;
}
if (document.querySelector('button[name="intent"][value="resend"]') && getPhoneVerificationDisplayedPhone()) {
return true;
}
const pageText = getPageTextSnapshot();
const displayedPhone = getPhoneVerificationDisplayedPhone();
return Boolean(getVerificationCodeTarget())
&& Boolean(displayedPhone)
&& /check\s+your\s+phone|phone\s+verification|verify\s+your\s+phone|sms|text\s+message|code\s+to\s+\+/.test(pageText);
}
function getDocumentReadyStateSnapshot() {
const readyState = typeof document !== 'undefined' && document
? String(document.readyState || '').trim().toLowerCase()
: '';
return readyState || 'complete';
}
function isDocumentLoadComplete() {
return getDocumentReadyStateSnapshot() === 'complete';
}
async function waitForDocumentLoadComplete(timeout = 15000, label = '页面') {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (isDocumentLoadComplete()) {
return true;
}
await sleep(150);
}
throw new Error(`${label}长时间未完成加载,当前 readyState=${getDocumentReadyStateSnapshot()}。URL: ${location.href}`);
}
function isSignupVerificationPageInteractiveReady(snapshot = null) {
if (!isDocumentLoadComplete()) {
return false;
}
const resolvedSnapshot = snapshot || inspectSignupVerificationState();
if (resolvedSnapshot?.state !== 'verification') {
return false;
}
return Boolean(getVerificationCodeTarget());
}
function isStep8Ready() {
const continueBtn = getPrimaryContinueButton();
if (!continueBtn) return false;
if (isVerificationPageStillVisible()) return false;
if (isPhoneVerificationPageReady()) return false;
if (isAddPhonePageReady()) return false;
if (isAddEmailPageReady()) return false;
return isOAuthConsentPage();
}
const phoneAuthHelpers = self.MultiPagePhoneAuth?.createPhoneAuthHelpers?.({
fillInput,
getActionText,
getPageTextSnapshot,
getVerificationErrorText,
humanPause,
isActionEnabled,
isAddPhonePageReady,
isConsentReady: isStep8Ready,
isPhoneVerificationPageReady,
isVisibleElement,
simulateClick,
sleep,
throwIfStopped,
waitForElement,
}) || {
submitPhoneNumber: async () => {
throw new Error('Phone auth helpers are unavailable.');
},
submitPhoneVerificationCode: async () => {
throw new Error('Phone auth helpers are unavailable.');
},
resendPhoneVerificationCode: async () => {
throw new Error('Phone auth helpers are unavailable.');
},
checkPhoneResendError: () => ({ hasError: false, reason: '', message: '', url: location.href }),
returnToAddPhone: async () => {
throw new Error('Phone auth helpers are unavailable.');
},
};
async function waitForPhoneVerificationProfileCompletion(timeout = 30000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (isStep8Ready()) {
return {
success: true,
consentReady: true,
url: location.href,
};
}
if (isAddPhonePageReady()) {
return {
returnedToAddPhone: true,
url: location.href,
};
}
await sleep(150);
}
if (isStep8Ready()) {
return {
success: true,
consentReady: true,
url: location.href,
};
}
return {
success: true,
assumed: true,
url: location.href,
};
}
async function submitPhoneVerificationCodeWithProfileFallback(payload = {}) {
const result = await phoneAuthHelpers.submitPhoneVerificationCode(payload);
if (!(isStep5Ready() || isSignupProfilePageUrl(result?.url || location.href))) {
return result;
}
const signupProfile = payload?.signupProfile || {};
if (!signupProfile.firstName || !signupProfile.lastName) {
throw new Error('手机号验证后进入资料页,但未提供步骤 5 所需的姓名数据。');
}
await step5_fillNameBirthday(signupProfile);
const nextState = await waitForPhoneVerificationProfileCompletion();
const mergedResult = {
...result,
...nextState,
profileCompleted: true,
};
if (nextState.consentReady || nextState.returnedToAddPhone) {
delete mergedResult.assumed;
}
return mergedResult;
}
function normalizeInlineText(text) {
return (text || '').replace(/\s+/g, ' ').trim();
}
function isStep5AllConsentText(text) {
const normalizedText = normalizeInlineText(text).toLowerCase();
if (!normalizedText) return false;
return /i\s+agree\s+to\s+all\s+of\s+the\s+following/i.test(normalizedText)
|| normalizedText.includes('\u4ee5\u4e0b\u306e\u3059\u3079\u3066\u306b\u540c\u610f')
|| normalizedText.includes('\u3059\u3079\u3066\u306b\u540c\u610f')
|| normalizedText.includes('\u540c\u610f\u3057\u307e\u3059')
|| normalizedText.includes('\u6211\u540c\u610f\u4ee5\u4e0b\u6240\u6709\u5404\u9879')
|| normalizedText.includes('\u540c\u610f\u4ee5\u4e0b\u6240\u6709\u5404\u9879')
|| normalizedText.includes('\u6211\u540c\u610f\u6240\u6709')
|| normalizedText.includes('\u5168\u90e8\u540c\u610f');
}
function findStep5AllConsentCheckbox() {
const namedCandidates = Array.from(document.querySelectorAll('input[name="allCheckboxes"][type="checkbox"]'))
.filter((el) => {
const checkboxLabel = el.closest?.('label') || null;
return isVisibleElement(el) || (checkboxLabel && isVisibleElement(checkboxLabel));
});
const namedMatch = namedCandidates.find((el) => {
const checkboxLabel = el.closest?.('label') || null;
const checkboxText = normalizeInlineText([
checkboxLabel?.textContent || '',
el.getAttribute?.('aria-label') || '',
el.getAttribute?.('title') || '',
el.getAttribute?.('name') || '',
].filter(Boolean).join(' '));
return isStep5AllConsentText(checkboxText);
});
if (namedMatch) {
return namedMatch;
}
if (namedCandidates.length > 0) {
return namedCandidates[0];
}
return Array.from(document.querySelectorAll('input[type="checkbox"]'))
.find((el) => {
const checkboxLabel = el.closest?.('label') || null;
if (!isVisibleElement(el) && !(checkboxLabel && isVisibleElement(checkboxLabel))) {
return false;
}
const checkboxText = normalizeInlineText([
checkboxLabel?.textContent || '',
el.getAttribute?.('aria-label') || '',
el.getAttribute?.('title') || '',
el.getAttribute?.('name') || '',
].filter(Boolean).join(' '));
return isStep5AllConsentText(checkboxText);
}) || null;
}
function isStep5CheckboxChecked(checkbox) {
if (!checkbox) return false;
if (checkbox.checked === true) return true;
const ariaChecked = String(
checkbox.getAttribute?.('aria-checked')
|| checkbox.closest?.('[role="checkbox"]')?.getAttribute?.('aria-checked')
|| ''
).toLowerCase();
return ariaChecked === 'true';
}
function findBirthdayReactAriaSelect(labelText) {
const normalizedLabels = (Array.isArray(labelText) ? labelText : [labelText])
.map((text) => normalizeInlineText(text))
.filter(Boolean);
const roots = document.querySelectorAll('.react-aria-Select');
for (const root of roots) {
const labelEl = Array.from(root.querySelectorAll('span')).find((el) => normalizedLabels.includes(normalizeInlineText(el.textContent)));
if (!labelEl) continue;
const item = root.closest('[class*="selectItem"], ._selectItem_ppsls_113') || root.parentElement;
const nativeSelect = item?.querySelector('[data-testid="hidden-select-container"] select') || null;
const button = root.querySelector('button[aria-haspopup="listbox"]') || null;
const valueEl = root.querySelector('.react-aria-SelectValue') || null;
return { root, item, labelEl, nativeSelect, button, valueEl };
}
return null;
}
async function setReactAriaBirthdaySelect(control, value) {
if (!control?.nativeSelect) {
throw new Error('未找到可写入的生日下拉框。');
}
const desiredValue = String(value);
const option = Array.from(control.nativeSelect.options).find((item) => item.value === desiredValue);
if (!option) {
throw new Error(`生日下拉框中不存在值 ${desiredValue}。`);
}
control.nativeSelect.value = desiredValue;
option.selected = true;
control.nativeSelect.dispatchEvent(new Event('input', { bubbles: true }));
control.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(120);
}
function getStep5ErrorText() {
const messages = [];
const selectors = [
'.react-aria-FieldError',
'[slot="errorMessage"]',
'[id$="-error"]',
'[id$="-errors"]',
'[role="alert"]',
'[aria-live="assertive"]',
'[aria-live="polite"]',
'[class*="error"]',
];
for (const selector of selectors) {
document.querySelectorAll(selector).forEach((el) => {
if (!isVisibleElement(el)) return;
const text = normalizeInlineText(el.textContent);
if (text) {
messages.push(text);
}
});
}
const invalidField = Array.from(document.querySelectorAll('[aria-invalid="true"], [data-invalid="true"]'))
.find((el) => isVisibleElement(el));
if (invalidField) {
const wrapper = invalidField.closest('form, fieldset, [data-rac], div');
if (wrapper) {
const text = normalizeInlineText(wrapper.textContent);
if (text) {
messages.push(text);
}
}
}
return messages.find((text) => STEP5_SUBMIT_ERROR_PATTERN.test(text)) || '';
}
function isSignupPasswordPage() {
return /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(location.pathname || '');
}
function getSignupPasswordInput() {
const input = document.querySelector('input[type="password"]');
return input && isVisibleElement(input) ? input : null;
}
function getSignupPasswordSubmitButton({ allowDisabled = false } = {}) {
const direct = document.querySelector('button[type="submit"]');
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
return direct;
}
const candidates = document.querySelectorAll('button, [role="button"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false;
const text = getActionText(el);
return /继续|continue|submit|创建|続行|続ける|次へ|サインアップ|登録|作成|create/i.test(text);
}) || null;
}
function getAuthRetryButton({ allowDisabled = false } = {}) {
if (authPageRecovery?.getAuthRetryButton) {
return authPageRecovery.getAuthRetryButton({ allowDisabled });
}
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
return direct;
}
const candidates = document.querySelectorAll('button, [role="button"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false;
const text = getActionText(el);
return /重试|try\s+again/i.test(text);
}) || null;
}
function getAuthTimeoutErrorPageState(options = {}) {
if (authPageRecovery?.getAuthTimeoutErrorPageState) {
return authPageRecovery.getAuthTimeoutErrorPageState(options);
}
const { pathPatterns = [] } = options;
const path = location.pathname || '';
if (pathPatterns.length && !pathPatterns.some((pattern) => pattern.test(path))) {
return null;
}
const retryButton = getAuthRetryButton({ allowDisabled: true });
if (!retryButton) {
return null;
}
const text = getPageTextSnapshot();
const titleMatched = AUTH_TIMEOUT_ERROR_TITLE_PATTERN.test(text)
|| AUTH_TIMEOUT_ERROR_TITLE_PATTERN.test(document.title || '');
const detailMatched = AUTH_TIMEOUT_ERROR_DETAIL_PATTERN.test(text);
const routeErrorMatched = AUTH_ROUTE_ERROR_PATTERN.test(text);
const fetchFailedMatched = /failed\s+to\s+fetch|network\s+error|fetch\s+failed/i.test(text);
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
const emailInUseBlocked = /email_in_use/i.test(text);
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
if (!titleMatched && !detailMatched && !routeErrorMatched && !fetchFailedMatched && !maxCheckAttemptsBlocked && !emailInUseBlocked && !userAlreadyExistsBlocked) {
return null;
}
return {
path,
url: location.href,
retryButton,
retryEnabled: isActionEnabled(retryButton),
titleMatched,
detailMatched,
routeErrorMatched,
fetchFailedMatched,
maxCheckAttemptsBlocked,
emailInUseBlocked,
userAlreadyExistsBlocked,
};
}
function getSignupAuthRetryPathPatterns() {
return [
/\/create-account\/password(?:[/?#]|$)/i,
/\/email-verification(?:[/?#]|$)/i,
];
}
function getLoginAuthRetryPathPatterns() {
return [
/\/log-in(?:[/?#]|$)/i,
/\/email-verification(?:[/?#]|$)/i,
];
}
function getAuthRetryPathPatternsForFlow(flow = 'auth') {
switch (flow) {
case 'signup':
case 'signup_password':
return getSignupAuthRetryPathPatterns();
case 'login':
return getLoginAuthRetryPathPatterns();
default:
return [];
}
}
function getCurrentAuthRetryPageState(flow = 'auth') {
return getAuthTimeoutErrorPageState({
pathPatterns: getAuthRetryPathPatternsForFlow(flow),
});
}
async function recoverCurrentAuthRetryPage(payload = {}) {
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 {
flow = 'auth',
logLabel = '',
maxClickAttempts = 5,
pathPatterns = null,
step = null,
timeoutMs = 12000,
waitAfterClickMs = 3000,
} = payload;
const resolvedPathPatterns = Array.isArray(pathPatterns)
? pathPatterns
: getAuthRetryPathPatternsForFlow(flow);
if (authPageRecovery?.recoverAuthRetryPage) {
return authPageRecovery.recoverAuthRetryPage({
logLabel,
maxClickAttempts,
pathPatterns: resolvedPathPatterns,
step,
stepKey: step === 8 || flow === 'login' ? 'oauth-login' : 'fetch-signup-code',
timeoutMs,
waitAfterClickMs,
});
}
const maxIdlePolls = timeoutMs > 0
? Math.max(1, Math.ceil(timeoutMs / Math.max(1, 250)))
: Number.POSITIVE_INFINITY;
let clickCount = 0;
let idlePollCount = 0;
while (clickCount < maxClickAttempts) {
throwIfStopped();
const retryState = getAuthTimeoutErrorPageState({ pathPatterns: resolvedPathPatterns });
if (!retryState) {
return {
recovered: clickCount > 0,
clickCount,
url: location.href,
};
}
if (retryState.maxCheckAttemptsBlocked) {
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
}
if (retryState.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (retryState.retryButton && retryState.retryEnabled) {
idlePollCount = 0;
clickCount += 1;
log(`${logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`}(第 ${clickCount} 次)...`, 'warn');
await humanPause(300, 800);
await performOperationWithDelay({ stepKey: step === 8 || flow === 'login' ? 'oauth-login' : 'fetch-signup-code', kind: 'click', label: 'auth-retry-click' }, async () => {
simulateClick(retryState.retryButton);
});
const settleStart = Date.now();
while (Date.now() - settleStart < waitAfterClickMs) {
throwIfStopped();
if (!getAuthTimeoutErrorPageState({ pathPatterns: resolvedPathPatterns })) {
return {
recovered: true,
clickCount,
url: location.href,
};
}
await sleep(250);
}
continue;
}
idlePollCount += 1;
if (idlePollCount >= maxIdlePolls) {
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时:重试按钮长时间不可点击。URL: ${location.href}`);
}
await sleep(250);
}
const finalRetryState = getAuthTimeoutErrorPageState({ pathPatterns: resolvedPathPatterns });
if (!finalRetryState) {
return {
recovered: clickCount > 0,
clickCount,
url: location.href,
};
}
if (finalRetryState.maxCheckAttemptsBlocked) {
throw new Error('CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器');
}
if (finalRetryState.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`);
}
function getSignupPasswordTimeoutErrorPageState() {
return getAuthTimeoutErrorPageState({
pathPatterns: getSignupAuthRetryPathPatterns(),
});
}
function getLoginTimeoutErrorPageState() {
return getAuthTimeoutErrorPageState({
pathPatterns: getLoginAuthRetryPathPatterns(),
});
}
function isLoginPhoneUsernameKind(rawUrl = location.href) {
const url = String(rawUrl || '').trim();
if (!url) {
return false;
}
try {
const parsed = new URL(url);
return /\/log-in(?:[/?#]|$)/i.test(parsed.pathname || '')
&& String(parsed.searchParams.get('usernameKind') || '').toLowerCase() === 'phone_number';
} catch {
return /\/log-in(?:[/?#]|$)/i.test(url) && /[?&]usernameKind=phone_number(?:[]|$)/i.test(url);
}
}
function isLoginPhoneEntryPageText(pageText = getPageTextSnapshot()) {
const normalizedText = String(pageText || '').replace(/\s+/g, ' ').trim();
if (!normalizedText) {
return false;
}
if (isAddPhonePageReady() || isPhoneVerificationPageReady()) {
return false;
}
return LOGIN_PHONE_ENTRY_PAGE_PATTERN.test(normalizedText);
}
function isInsideHiddenPhoneControl(element) {
if (!element) {
return true;
}
return Boolean(element.closest?.('[aria-hidden="true"], [hidden], [data-testid="hidden-select-container"], [data-react-aria-prevent-focus="true"]'));
}
function summarizePhoneInputCandidate(element, options = {}) {
const summary = {
tag: (element?.tagName || '').toLowerCase(),
type: '',
name: '',
id: '',
autocomplete: '',
placeholder: '',
ariaLabel: '',
visible: false,
hiddenControl: true,
readOnly: false,
maxLength: 0,
usable: false,
skipReason: '',
};
if (!element) {
summary.skipReason = 'missing_element';
return summary;
}
summary.visible = isVisibleElement(element);
summary.hiddenControl = isInsideHiddenPhoneControl(element);
summary.type = String(element.getAttribute?.('type') || element.type || '').trim().toLowerCase();
summary.name = String(element.getAttribute?.('name') || element.name || '').trim();
summary.id = String(element.getAttribute?.('id') || element.id || '').trim();
summary.autocomplete = String(element.getAttribute?.('autocomplete') || '').trim().toLowerCase();
summary.placeholder = String(element.getAttribute?.('placeholder') || '').trim().slice(0, 80);
summary.ariaLabel = String(element.getAttribute?.('aria-label') || '').trim().slice(0, 80);
const hasReadonlyAttribute = typeof element.hasAttribute === 'function'
? element.hasAttribute('readonly')
: element.readOnly === true;
summary.readOnly = element.readOnly === true
|| hasReadonlyAttribute
|| String(element.getAttribute?.('aria-readonly') || '').trim().toLowerCase() === 'true';
summary.maxLength = Number(element.getAttribute?.('maxlength') || element.maxLength || 0);
if (summary.hiddenControl) {
summary.skipReason = 'inside_hidden_control';
return summary;
}
if (!summary.visible) {
summary.skipReason = 'not_visible';
return summary;
}
if (summary.type === 'hidden') {
summary.skipReason = 'hidden_type';
return summary;
}
if (summary.readOnly) {
summary.skipReason = 'readonly';
return summary;
}
if (summary.maxLength === 6) {
summary.skipReason = 'verification_code_input';
return summary;
}
const normalizedName = summary.name.toLowerCase();
const normalizedId = summary.id.toLowerCase();
const combinedText = `${normalizedName} ${normalizedId} ${summary.placeholder} ${summary.ariaLabel}`;
if (isLoginEmailLikeInput(element)) {
summary.skipReason = 'email_like';
return summary;
}
if (
summary.type === 'tel'
|| summary.autocomplete === 'tel'
|| /phone|tel/i.test(`${normalizedName} ${normalizedId}`)
|| /手机|电话|手机号|电话号码|国家号码|phone|mobile|telephone/i.test(combinedText)
) {
summary.usable = true;
return summary;
}
if (options.allowGenericText && (!summary.type || summary.type === 'text')) {
summary.usable = true;
return summary;
}
summary.skipReason = 'not_phone_like';
return summary;
}
function isUsablePhoneInputElement(element, options = {}) {
return summarizePhoneInputCandidate(element, options).usable;
}
function collectPhoneInputCandidates(selector, options = {}) {
return Array.from(document.querySelectorAll(selector))
.map((element) => summarizePhoneInputCandidate(element, options));
}
function findUsablePhoneInput(selector, options = {}) {
return Array.from(document.querySelectorAll(selector))
.find((element) => isUsablePhoneInputElement(element, options)) || null;
}
function getLoginInputAttributeText(input) {
return {
type: String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase(),
autocomplete: String(input?.getAttribute?.('autocomplete') || '').trim().toLowerCase(),
name: String(input?.getAttribute?.('name') || input?.name || '').trim(),
id: String(input?.getAttribute?.('id') || input?.id || '').trim(),
placeholder: String(input?.getAttribute?.('placeholder') || '').trim(),
ariaLabel: String(input?.getAttribute?.('aria-label') || '').trim(),
};
}
function isLoginEmailLikeInput(input) {
const summary = getLoginInputAttributeText(input);
const nameId = `${summary.name} ${summary.id}`;
const labelText = `${summary.placeholder} ${summary.ariaLabel}`;
return summary.type === 'email'
|| summary.autocomplete === 'email'
|| /email|mail/i.test(nameId)
|| /email|电子邮件|邮箱|メールアドレス|メール|電子メール/i.test(labelText);
}
function getLoginEmailInput() {
const input = Array.from(document.querySelectorAll([
'input[type="email"]',
'input[autocomplete="email"]',
'input[name="email"]',
'input[name="username"]',
'input[autocomplete="username"]',
'input[id*="email" i]',
'input[placeholder*="email" i]',
'input[placeholder*="Email"]',
'input[placeholder*="电子邮件"]',
'input[placeholder*="邮箱"]',
'input[placeholder*="メール"]',
'input[aria-label*="email" i]',
'input[aria-label*="电子邮件"]',
'input[aria-label*="邮箱"]',
'input[aria-label*="メール"]',
].join(', '))).find((candidate) => isVisibleElement(candidate)) || null;
if (!input) {
return null;
}
if ((isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) && !isLoginEmailLikeInput(input)) {
return null;
}
return input;
}
function getLoginPhoneInput() {
const phonePage = isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText();
const selector = [
'input[type="tel"]:not([maxlength="6"])',
'input[name*="phone" i]:not([type="hidden"])',
'input[id*="phone" i]:not([type="hidden"])',
'input[autocomplete="tel"]',
'input[inputmode="tel"]',
'input[placeholder*="phone" i]',
'input[aria-label*="phone" i]',
'input[placeholder*="telephone" i]',
'input[aria-label*="telephone" i]',
'input[placeholder*="手机"]',
'input[aria-label*="手机"]',
'input[placeholder*="电话"]',
'input[aria-label*="电话"]',
'input[placeholder*="電話"]',
'input[aria-label*="電話"]',
'input[placeholder*="携帯"]',
'input[aria-label*="携帯"]',
phonePage ? 'input[name="username"]:not([maxlength="6"])' : '',
phonePage ? 'input[id*="username" i]:not([maxlength="6"])' : '',
phonePage ? 'input[autocomplete="username"]:not([maxlength="6"])' : '',
phonePage ? 'input[type="text"]:not([maxlength="6"])' : '',
].filter(Boolean).join(', ');
return findUsablePhoneInput(selector, { allowGenericText: phonePage });
}
function getLoginPhoneInputDiagnostics(phoneInput) {
return {
tag: (phoneInput?.tagName || '').toLowerCase(),
type: String(phoneInput?.getAttribute?.('type') || phoneInput?.type || '').trim().toLowerCase(),
name: String(phoneInput?.getAttribute?.('name') || phoneInput?.name || '').trim(),
id: String(phoneInput?.getAttribute?.('id') || phoneInput?.id || '').trim(),
autocomplete: String(phoneInput?.getAttribute?.('autocomplete') || '').trim().toLowerCase(),
placeholder: String(phoneInput?.getAttribute?.('placeholder') || '').trim().slice(0, 80),
ariaLabel: String(phoneInput?.getAttribute?.('aria-label') || '').trim().slice(0, 80),
value: getPhoneInputRenderedValue(phoneInput),
};
}
function getLoginPhoneHiddenValueInput(phoneInput) {
const form = phoneInput?.form || phoneInput?.closest?.('form') || null;
const root = form || phoneInput?.closest?.('fieldset, form, [data-rac], div') || document;
const candidates = Array.from(root?.querySelectorAll?.([
'input[name="phone"]',
'input[name*="phone" i]',
'input[name="phoneNumber"]',
'input[name*="telephone" i]',
'input[type="hidden"][id*="phone" i]',
'input[type="hidden"][id*="telephone" i]',
'input[type="hidden"][name*="phone" i]',
'input[type="hidden"][name*="telephone" i]',
].join(', ')) || []);
return candidates.find((input) => {
if (!input || input === phoneInput) return false;
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
return type === 'hidden' || !isVisibleElement(input);
}) || null;
}
function getLoginPhoneHiddenValueDiagnostics(hiddenInput) {
return {
tag: (hiddenInput?.tagName || '').toLowerCase(),
type: String(hiddenInput?.getAttribute?.('type') || hiddenInput?.type || '').trim().toLowerCase(),
name: String(hiddenInput?.getAttribute?.('name') || hiddenInput?.name || '').trim(),
id: String(hiddenInput?.getAttribute?.('id') || hiddenInput?.id || '').trim(),
value: String(hiddenInput?.value || hiddenInput?.getAttribute?.('value') || '').trim(),
};
}
function getLoginPasswordInput() {
const input = document.querySelector('input[type="password"]');
return input && isVisibleElement(input) ? input : null;
}
function getLoginSubmitButton({ allowDisabled = false } = {}) {
const direct = document.querySelector('button[type="submit"], input[type="submit"]');
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
return direct;
}
const candidates = document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false;
const text = getActionText(el);
if (!text || ONE_TIME_CODE_LOGIN_PATTERN.test(text)) return false;
return /continue|next|submit|sign\s*in|log\s*in|继续|下一步|登录|続行|続ける|次へ|ログイン|サインイン|送信/i.test(text);
}) || null;
}
function normalizeCountryLabel(value) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.normalizeCountryLabel === 'function') {
return phoneCountryUtils.normalizeCountryLabel(value);
}
return String(value || '')
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/&/g, ' and ')
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function getLoginPhoneCountrySelect(phoneInput) {
const scope = phoneInput?.closest?.('fieldset, form, [data-rac], div') || document;
const select = scope.querySelector?.('select');
return select && isVisibleElement(select) ? select : null;
}
function getLoginPhoneCountryOptionLabel(option) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.getOptionLabel === 'function') {
return phoneCountryUtils.getOptionLabel(option);
}
return String(option?.textContent || option?.label || '')
.replace(/\s+/g, ' ')
.trim();
}
function getLoginPhoneCountryOptionMatchLabels(option) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.getOptionMatchLabels === 'function') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
return phoneCountryUtils.getOptionMatchLabels(option, {
document: typeof document !== 'undefined' ? document : null,
navigator: rootScope?.navigator || globalThis?.navigator || null,
getOptionLabel: getLoginPhoneCountryOptionLabel,
});
}
const labels = new Set();
const pushLabel = (value) => {
const label = String(value || '').replace(/\s+/g, ' ').trim();
if (label) {
labels.add(label);
}
};
pushLabel(getLoginPhoneCountryOptionLabel(option));
pushLabel(option?.value);
return Array.from(labels);
}
function findLoginPhoneCountryOptionByLabel(select, countryLabel) {
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (select && typeof phoneCountryUtils.findOptionByCountryLabel === 'function') {
return phoneCountryUtils.findOptionByCountryLabel(select.options, countryLabel, {
document: typeof document !== 'undefined' ? document : null,
navigator: (typeof self !== 'undefined' ? self : globalThis)?.navigator || globalThis?.navigator || null,
getOptionLabel: getLoginPhoneCountryOptionLabel,
});
}
const normalizedTarget = normalizeCountryLabel(countryLabel);
if (!select || !normalizedTarget) {
return null;
}
const options = Array.from(select.options || []);
return options.find((option) => (
getLoginPhoneCountryOptionMatchLabels(option)
.some((label) => normalizeCountryLabel(label) === normalizedTarget)
)) || options.find((option) => {
const normalizedLabels = getLoginPhoneCountryOptionMatchLabels(option)
.map((label) => normalizeCountryLabel(label))
.filter(Boolean);
return normalizedLabels.some((optionLabel) => (
optionLabel.length > 2
&& normalizedTarget.length > 2
&& (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
));
}) || null;
}
function findLoginPhoneCountryOptionByNumber(select, phoneNumber) {
if (!select) {
return null;
}
const phoneCountryUtils = (typeof self !== 'undefined' ? self : globalThis)?.MultiPagePhoneCountryUtils
|| globalThis?.MultiPagePhoneCountryUtils
|| {};
if (typeof phoneCountryUtils.findOptionByPhoneNumber === 'function') {
return phoneCountryUtils.findOptionByPhoneNumber(select.options, phoneNumber, {
getOptionLabel: getLoginPhoneCountryOptionLabel,
});
}
const digits = normalizePhoneDigits(phoneNumber);
if (!digits) {
return null;
}
let bestMatch = null;
let bestDialCodeLength = 0;
for (const option of Array.from(select.options || [])) {
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getLoginPhoneCountryOptionLabel(option)));
if (!dialCode || !digits.startsWith(dialCode)) {
continue;
}
if (dialCode.length > bestDialCodeLength) {
bestMatch = option;
bestDialCodeLength = dialCode.length;
}
}
return bestMatch;
}
async function selectCountryForPhoneInput(phoneInput, phoneNumber = '', countryLabel = '', options = {}) {
const visibleStep = Math.floor(Number(options?.visibleStep) || 0) || 7;
const selection = await ensureSignupPhoneCountrySelected(phoneInput, {
countryLabel,
phoneNumber,
});
const selectedOption = selection.selectedOption || getSignupPhoneSelectedCountryOption(phoneInput);
const targetDialCode = resolveSignupPhoneTargetDialCode({ countryLabel, phoneNumber }, selectedOption);
const displayedDialCode = getSignupPhoneDisplayedDialCode(phoneInput);
if (selection.hasCountryControl && targetDialCode) {
if (!selection.matched || (displayedDialCode && displayedDialCode !== targetDialCode)) {
const currentCountryText = getSignupPhoneCountryButtonText(phoneInput) || displayedDialCode || '未知';
const targetLabel = `目标区号 +${targetDialCode}(号码 ${phoneNumber}${countryLabel ? `,国家 ${countryLabel}` : ''})`;
throw new Error(`步骤 ${visibleStep}:手机号登录国家下拉框未能自动切换到 ${targetLabel},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
}
return targetDialCode;
}
const select = getLoginPhoneCountrySelect(phoneInput);
const fallbackSelectedOption = select?.options?.[select.selectedIndex] || null;
return extractDialCodeFromText(getLoginPhoneCountryOptionLabel(fallbackSelectedOption))
|| displayedDialCode
|| resolveSignupPhoneDialCodeFromNumber(phoneNumber);
}
function resolveLoginPhoneDialCode(phoneInput, options = {}) {
return resolveSignupPhoneDialCode(phoneInput, options);
}
function findLoginEntryTrigger() {
const candidates = Array.from(document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
)).filter((el) => isVisibleElement(el) && isActionEnabled(el));
const preferred = candidates.find((el) => {
const text = getActionText(el);
if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false;
return /continue\s+(?:with|using)\s+email|use\s+(?:an?\s+)?email|email\s+address|邮箱|电子邮件|メールアドレス|メール|電子メール/i.test(text);
});
if (preferred) return preferred;
return candidates.find((el) => {
const text = getActionText(el);
if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false;
return LOGIN_ENTRY_ACTION_PATTERN.test(text);
}) || null;
}
function findLoginPhoneEntryTrigger() {
const candidates = Array.from(document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
)).filter((el) => isVisibleElement(el) && isActionEnabled(el));
return candidates.find((el) => {
const text = getActionText(el);
if (!text || LOGIN_CODE_ONLY_ACTION_PATTERN.test(text) || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false;
return LOGIN_SWITCH_TO_PHONE_PATTERN.test(text)
|| (
LOGIN_PHONE_ACTION_PATTERN.test(text)
&& !/email|邮箱|电子邮件|メールアドレス|メール|電子メール/i.test(text)
);
}) || null;
}
function findLoginMoreOptionsTrigger() {
const candidates = Array.from(document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
)).filter((el) => isVisibleElement(el) && isActionEnabled(el));
return candidates.find((el) => {
const text = getActionText(el);
if (!text || LOGIN_EXTERNAL_IDP_PATTERN.test(text)) return false;
return LOGIN_MORE_OPTIONS_PATTERN.test(text);
}) || null;
}
function inspectLoginAuthState() {
const retryState = getLoginTimeoutErrorPageState();
const verificationTarget = getVerificationCodeTarget();
const passwordInput = getLoginPasswordInput();
const emailInput = getLoginEmailInput();
const phoneInput = getLoginPhoneInput();
const switchTrigger = findOneTimeCodeLoginTrigger();
const loginEntryTrigger = findLoginEntryTrigger();
const phoneEntryTrigger = findLoginPhoneEntryTrigger();
const moreOptionsTrigger = findLoginMoreOptionsTrigger();
const submitButton = getLoginSubmitButton({ allowDisabled: true });
const verificationVisible = isVerificationPageStillVisible();
const addPhonePage = isAddPhonePageReady();
const addEmailPage = isAddEmailPageReady();
const phoneVerificationPage = isPhoneVerificationPageReady();
const consentReady = isStep8Ready();
const oauthConsentPage = isOAuthConsentPage();
const chooseAccountPage = isChooseAccountPage();
const baseState = {
state: 'unknown',
url: location.href,
path: location.pathname || '',
displayedEmail: getLoginVerificationDisplayedEmail(),
retryButton: retryState?.retryButton || null,
retryEnabled: Boolean(retryState?.retryEnabled),
titleMatched: Boolean(retryState?.titleMatched),
detailMatched: Boolean(retryState?.detailMatched),
maxCheckAttemptsBlocked: Boolean(retryState?.maxCheckAttemptsBlocked),
emailInUseBlocked: Boolean(retryState?.emailInUseBlocked),
verificationTarget,
passwordInput,
emailInput,
phoneInput,
submitButton,
switchTrigger,
loginEntryTrigger,
phoneEntryTrigger,
moreOptionsTrigger,
verificationVisible,
addPhonePage,
addEmailPage,
phoneVerificationPage,
oauthConsentPage,
consentReady,
chooseAccountPage,
};
if (retryState) {
return {
...baseState,
state: 'login_timeout_error_page',
};
}
if (phoneVerificationPage) {
return {
...baseState,
state: 'phone_verification_page',
displayedPhone: getPhoneVerificationDisplayedPhone(),
};
}
if (verificationTarget) {
return {
...baseState,
state: 'verification_page',
};
}
if (addPhonePage) {
return {
...baseState,
state: 'add_phone_page',
};
}
if (addEmailPage) {
return {
...baseState,
state: 'add_email_page',
};
}
if (passwordInput || switchTrigger) {
return {
...baseState,
state: 'password_page',
};
}
if (phoneInput) {
return {
...baseState,
state: 'phone_entry_page',
};
}
if (emailInput) {
return {
...baseState,
state: 'email_page',
};
}
if (chooseAccountPage) {
return {
...baseState,
state: 'choose_account_page',
};
}
if (verificationVisible) {
return {
...baseState,
state: 'verification_page',
};
}
if (consentReady) {
return {
...baseState,
state: 'oauth_consent_page',
};
}
if (loginEntryTrigger) {
return {
...baseState,
state: 'entry_page',
};
}
return baseState;
}
function serializeLoginAuthState(snapshot) {
return {
state: snapshot?.state || 'unknown',
url: snapshot?.url || location.href,
path: snapshot?.path || location.pathname || '',
displayedEmail: snapshot?.displayedEmail || '',
verificationErrorText: getVerificationErrorText(),
retryEnabled: Boolean(snapshot?.retryEnabled),
titleMatched: Boolean(snapshot?.titleMatched),
detailMatched: Boolean(snapshot?.detailMatched),
maxCheckAttemptsBlocked: Boolean(snapshot?.maxCheckAttemptsBlocked),
emailInUseBlocked: Boolean(snapshot?.emailInUseBlocked),
hasVerificationTarget: Boolean(snapshot?.verificationTarget),
hasPasswordInput: Boolean(snapshot?.passwordInput),
hasEmailInput: Boolean(snapshot?.emailInput),
hasPhoneInput: Boolean(snapshot?.phoneInput),
hasSubmitButton: Boolean(snapshot?.submitButton),
hasSwitchTrigger: Boolean(snapshot?.switchTrigger),
hasLoginEntryTrigger: Boolean(snapshot?.loginEntryTrigger),
hasPhoneEntryTrigger: Boolean(snapshot?.phoneEntryTrigger),
hasMoreOptionsTrigger: Boolean(snapshot?.moreOptionsTrigger),
verificationVisible: Boolean(snapshot?.verificationVisible),
addPhonePage: Boolean(snapshot?.addPhonePage),
addEmailPage: Boolean(snapshot?.addEmailPage),
phoneVerificationPage: Boolean(snapshot?.phoneVerificationPage),
oauthConsentPage: Boolean(snapshot?.oauthConsentPage),
consentReady: Boolean(snapshot?.consentReady),
chooseAccountPage: Boolean(snapshot?.chooseAccountPage),
};
}
function getLoginAuthStateLabel(snapshot) {
const state = snapshot?.state;
switch (state) {
case 'verification_page':
return '登录验证码页';
case 'password_page':
return '密码页';
case 'email_page':
return '邮箱输入页';
case 'phone_entry_page':
return '手机号输入页';
case 'phone_verification_page':
return '手机验证码页';
case 'login_timeout_error_page':
return '登录超时报错页';
case 'oauth_consent_page':
return 'OAuth 授权页';
case 'choose_account_page':
return 'OpenAI choose account page';
case 'entry_page':
return '登录入口页';
case 'add_phone_page':
return '手机号页';
case 'add_email_page':
return '添加邮箱页';
default:
return '未知页面';
}
}
async function waitForKnownLoginAuthState(timeout = 15000) {
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state !== 'unknown') {
return snapshot;
}
await sleep(200);
}
return snapshot;
}
function getAuthLoginStepForLoginCodeStep(step = 8) {
return Number(step) >= 11 ? 10 : 7;
}
async function waitForLoginVerificationPageReady(timeout = 10000, visibleStep = 8, options = {}) {
const start = Date.now();
let snapshot = inspectLoginAuthState();
const allowPhoneVerificationPage = Boolean(options?.allowPhoneVerificationPage);
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = inspectLoginAuthState();
if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) {
return snapshot;
}
if (snapshot.state !== 'unknown') {
break;
}
await sleep(200);
}
throw new Error(
`当前未进入登录验证码页面,请先重新完成步骤 ${Number(visibleStep) >= 11 ? 10 : 7}。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}`
);
}
function createStep6SuccessResult(snapshot, options = {}) {
const result = {
step6Outcome: 'success',
state: snapshot?.state || 'verification_page',
url: snapshot?.url || location.href,
via: options.via || '',
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
};
if (options.skipLoginVerificationStep) {
result.skipLoginVerificationStep = true;
}
if (options.directOAuthConsentPage) {
result.directOAuthConsentPage = true;
}
return result;
}
function createStep6OAuthConsentSuccessResult(snapshot, options = {}) {
return createStep6SuccessResult(snapshot, {
...options,
via: options.via || 'oauth_consent_page',
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
}
function createStep6AddEmailSuccessResult(snapshot, options = {}) {
return {
...createStep6SuccessResult(snapshot, {
...options,
via: options.via || 'add_email_page',
loginVerificationRequestedAt: null,
}),
addEmailPage: true,
};
}
function createStep6RecoverableResult(reason, snapshot, options = {}) {
return {
step6Outcome: 'recoverable',
reason,
state: snapshot?.state || 'unknown',
url: snapshot?.url || location.href,
message: options.message || '',
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
};
}
async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options = {}) {
const {
loginVerificationRequestedAt = null,
visibleStep = 7,
via = 'login_timeout_recovered',
allowPhoneVerificationPage = false,
} = options;
let resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
let recovered = false;
if (resolvedSnapshot?.state === 'login_timeout_error_page') {
try {
const recoveryResult = await recoverCurrentAuthRetryPage({
flow: 'login',
logLabel: `步骤 ${visibleStep}:检测到登录超时报错,正在点击“重试”恢复当前页面`,
step: visibleStep,
timeoutMs: 12000,
});
recovered = Boolean(recoveryResult?.recovered);
if (recovered) {
log('登录超时报错页已点击“重试”,正在按恢复后的页面状态继续当前流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' });
}
} catch (error) {
if (/CF_SECURITY_BLOCKED::/i.test(String(error?.message || error || ''))) {
throw error;
}
log(`登录超时报错页自动点击“重试”失败:${error.message}`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
}
}
resolvedSnapshot = recovered
? normalizeStep6Snapshot(await waitForKnownLoginAuthState(4000))
: normalizeStep6Snapshot(inspectLoginAuthState());
if (resolvedSnapshot.state === 'verification_page' || (allowPhoneVerificationPage && resolvedSnapshot.state === 'phone_verification_page')) {
return {
action: 'done',
result: createStep6SuccessResult(resolvedSnapshot, {
via,
loginVerificationRequestedAt,
}),
};
}
if (resolvedSnapshot.state === 'oauth_consent_page') {
return {
action: 'done',
result: createStep6OAuthConsentSuccessResult(resolvedSnapshot, {
via,
}),
};
}
if (resolvedSnapshot.state === 'add_email_page') {
return {
action: 'done',
result: createStep6AddEmailSuccessResult(resolvedSnapshot, {
via: `${via}_add_email`,
}),
};
}
if (resolvedSnapshot.state === 'password_page') {
log('登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return { action: 'password', snapshot: resolvedSnapshot };
}
if (resolvedSnapshot.state === 'phone_entry_page') {
log('登录超时报错页恢复后已进入手机号输入页,继续当前登录流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return { action: 'phone', snapshot: resolvedSnapshot };
}
if (resolvedSnapshot.state === 'email_page') {
log('登录超时报错页恢复后已回到邮箱输入页,继续当前登录流程。', 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return { action: 'email', snapshot: resolvedSnapshot };
}
return {
action: 'recoverable',
result: createStep6RecoverableResult(reason, resolvedSnapshot, {
message,
loginVerificationRequestedAt,
}),
};
}
async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message, options = {}) {
const transition = await createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options);
if (transition?.action === 'done' || transition?.action === 'recoverable') {
return transition.result;
}
return createStep6RecoverableResult(reason, transition?.snapshot || normalizeStep6Snapshot(inspectLoginAuthState()), {
message,
});
}
async function finalizeStep6VerificationReady(options = {}) {
const {
visibleStep = 7,
logLabel = `步骤 ${visibleStep} 收尾`,
loginVerificationRequestedAt = null,
timeout = 12000,
via = 'verification_page_ready',
allowPhoneVerificationPage = false,
} = options;
const start = Date.now();
const maxRounds = 3;
const settleDelayMs = 3000;
let round = 0;
while (Date.now() - start < timeout && round < maxRounds) {
throwIfStopped();
round += 1;
log(`确认页面是否稳定停留在登录验证码阶段(第 ${round}/${maxRounds} 轮,先等待 3 秒)...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(settleDelayMs);
const rawSnapshot = inspectLoginAuthState();
const snapshot = normalizeStep6Snapshot(rawSnapshot);
if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) {
log(
snapshot.state === 'phone_verification_page' ? '登录手机验证码页面已稳定就绪。' : '登录验证码页面已稳定就绪。',
'ok',
{ step: visibleStep, stepKey: 'oauth-login' }
);
return createStep6SuccessResult(snapshot, {
via,
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'oauth_consent_page') {
log('认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6OAuthConsentSuccessResult(snapshot, {
via: `${via}_oauth_consent`,
});
}
if (snapshot.state === 'add_email_page') {
log('认证页已进入添加邮箱页,登录阶段完成。', 'ok', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6AddEmailSuccessResult(snapshot, {
via: `${via}_add_email`,
});
}
if (snapshot.state === 'login_timeout_error_page') {
log(`页面进入登录超时报错页,准备自动恢复后重试步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'登录验证码页面准备就绪前进入登录超时报错页。',
{ visibleStep }
);
}
if (snapshot.state === 'password_page' || snapshot.state === 'email_page') {
return createStep6RecoverableResult('verification_page_unstable', snapshot, {
message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 ${visibleStep}。`,
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'add_phone_page') {
throw new Error(`登录验证码页面准备过程中页面进入手机号页面。URL: ${snapshot.url}`);
}
}
const rawSnapshot = inspectLoginAuthState();
const snapshot = normalizeStep6Snapshot(rawSnapshot);
if (snapshot.state === 'verification_page' || (allowPhoneVerificationPage && snapshot.state === 'phone_verification_page')) {
log(
snapshot.state === 'phone_verification_page' ? '登录手机验证码页面已稳定就绪。' : '登录验证码页面已稳定就绪。',
'ok',
{ step: visibleStep, stepKey: 'oauth-login' }
);
return createStep6SuccessResult(snapshot, {
via,
loginVerificationRequestedAt,
});
}
if (snapshot.state === 'oauth_consent_page') {
log('认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6OAuthConsentSuccessResult(snapshot, {
via: `${via}_oauth_consent`,
});
}
if (snapshot.state === 'add_email_page') {
log('认证页已进入添加邮箱页,登录阶段完成。', 'ok', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6AddEmailSuccessResult(snapshot, {
via: `${via}_add_email`,
});
}
if (snapshot.state === 'login_timeout_error_page') {
log(`页面进入登录超时报错页,准备自动恢复后重试步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'登录验证码页面准备就绪前进入登录超时报错页。',
{ visibleStep }
);
}
if (snapshot.state === 'password_page' || snapshot.state === 'email_page') {
return createStep6RecoverableResult('verification_page_unstable', snapshot, {
message: `页面曾进入登录验证码阶段,但又回到了${getLoginAuthStateLabel(snapshot)},准备重新执行步骤 ${visibleStep}。`,
loginVerificationRequestedAt,
});
}
return createStep6RecoverableResult('verification_page_finalize_unknown', snapshot, {
message: `登录验证码页面状态在收尾确认阶段未稳定,准备重新执行步骤 ${visibleStep}。`,
loginVerificationRequestedAt,
});
}
function normalizeStep6Snapshot(snapshot) {
return snapshot;
}
function throwForStep6FatalState(snapshot, visibleStep = 7) {
snapshot = normalizeStep6Snapshot(snapshot);
switch (snapshot?.state) {
case 'oauth_consent_page':
return;
case 'add_phone_page':
throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 ${visibleStep}。URL: ${snapshot.url}`);
case 'unknown':
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
default:
return;
}
}
async function triggerLoginSubmitAction(button, fallbackField) {
const form = button?.form || fallbackField?.form || button?.closest?.('form') || fallbackField?.closest?.('form') || null;
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();
};
await humanPause(400, 1100);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'submit', label: 'login-submit' }, async () => {
if (button && isActionEnabled(button)) {
simulateClick(button);
return;
}
if (form && typeof form.requestSubmit === 'function') {
if (button && button.form === form) {
form.requestSubmit(button);
} else {
form.requestSubmit();
}
return;
}
if (button && typeof button.click === 'function') {
button.click();
return;
}
throw new Error('未找到可用的登录提交按钮。URL: ' + location.href);
});
}
function isSignupPasswordErrorPage() {
return Boolean(getSignupPasswordTimeoutErrorPageState());
}
function isSignupEmailAlreadyExistsPage() {
return isSignupPasswordPage() && SIGNUP_EMAIL_EXISTS_PATTERN.test(getPageTextSnapshot());
}
function inspectSignupVerificationState() {
const postVerificationState = getStep4PostVerificationState();
if (postVerificationState?.state === 'step5') {
return { state: 'step5' };
}
if (postVerificationState?.state === 'logged_in_home') {
return {
state: 'logged_in_home',
skipProfileStep: true,
url: postVerificationState.url || location.href,
};
}
if (typeof isOAuthConsentPage === 'function' && isOAuthConsentPage()) {
return {
state: 'oauth_consent_page',
url: location.href,
};
}
if (isSignupPasswordErrorPage()) {
const timeoutPage = getSignupPasswordTimeoutErrorPageState();
return {
state: 'error',
retryButton: timeoutPage?.retryButton || null,
userAlreadyExistsBlocked: Boolean(timeoutPage?.userAlreadyExistsBlocked),
};
}
const contactVerificationServerErrorText = typeof getContactVerificationServerErrorText === 'function'
? getContactVerificationServerErrorText()
: '';
if (contactVerificationServerErrorText) {
return {
state: 'contact_verification_server_error',
serverErrorText: contactVerificationServerErrorText,
};
}
if (typeof isPhoneVerificationPageReady === 'function' && isPhoneVerificationPageReady()) {
return {
state: 'verification',
phoneVerificationPage: true,
};
}
if (isVerificationPageStillVisible()) {
return { state: 'verification' };
}
if (isSignupEmailAlreadyExistsPage()) {
return { state: 'email_exists' };
}
const passwordInput = getSignupPasswordInput();
if (passwordInput) {
return {
state: 'password',
passwordInput,
submitButton: getSignupPasswordSubmitButton({ allowDisabled: true }),
passwordErrorText: getSignupPasswordFieldErrorText(),
};
}
return { state: 'unknown' };
}
async function waitForSignupVerificationTransition(timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const snapshot = inspectSignupVerificationState();
if (snapshot.state === 'verification' && !isSignupVerificationPageInteractiveReady(snapshot)) {
await sleep(200);
continue;
}
if (
snapshot.state === 'step5'
|| snapshot.state === 'logged_in_home'
|| snapshot.state === 'oauth_consent_page'
|| snapshot.state === 'verification'
|| snapshot.state === 'contact_verification_server_error'
|| snapshot.state === 'error'
|| snapshot.state === 'email_exists'
) {
return snapshot;
}
await sleep(200);
}
return inspectSignupVerificationState();
}
async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
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 { password } = payload;
const prepareSource = String(payload?.prepareSource || '').trim() || 'step4_execute';
const prepareLogLabel = String(payload?.prepareLogLabel || '').trim()
|| (prepareSource === 'step3_finalize' ? '步骤 3 收尾' : '步骤 4 执行');
const start = Date.now();
let recoveryRound = 0;
const maxRecoveryRounds = 3;
let passwordPageDiagnosticsLogged = false;
const isPasswordSubmitButtonReadyForRetry = (button) => {
if (!button || !isActionEnabled(button)) {
return false;
}
const ariaBusy = String(button.getAttribute?.('aria-busy') || '').trim().toLowerCase();
if (ariaBusy === 'true') {
return false;
}
const pendingAttr = [
button.getAttribute?.('data-loading'),
button.getAttribute?.('data-pending'),
button.getAttribute?.('data-submitting'),
button.getAttribute?.('data-state'),
]
.map((value) => String(value || '').trim().toLowerCase())
.filter(Boolean)
.join(' ');
if (/\b(?:true|loading|pending|submitting|busy)\b/.test(pendingAttr)) {
return false;
}
let style = null;
try {
style = typeof window !== 'undefined' && window.getComputedStyle
? window.getComputedStyle(button)
: null;
} catch {
style = null;
}
if (style?.pointerEvents === 'none') {
return false;
}
const opacity = Number.parseFloat(style?.opacity || '');
if (Number.isFinite(opacity) && opacity < 0.8) {
return false;
}
return true;
};
while (Date.now() - start < timeout && recoveryRound < maxRecoveryRounds) {
throwIfStopped();
const roundNo = recoveryRound + 1;
log(`${prepareLogLabel}:等待页面进入验证码阶段(第 ${roundNo}/${maxRecoveryRounds} 轮,先等待 5 秒)...`, 'info');
const snapshot = await waitForSignupVerificationTransition(5000);
if (snapshot.state === 'step5') {
log(`${prepareLogLabel}:页面已进入验证码后的下一阶段,本步骤按已完成处理。`, 'ok');
return { ready: true, alreadyVerified: true, retried: recoveryRound, prepareSource };
}
if (snapshot.state === 'logged_in_home') {
log(`${prepareLogLabel}:页面已直接进入 ChatGPT 已登录态,本步骤按已完成处理,并将跳过步骤 5。`, 'ok');
return {
ready: true,
alreadyVerified: true,
skipProfileStep: true,
retried: recoveryRound,
prepareSource,
};
}
if (snapshot.state === 'oauth_consent_page') {
log(`${prepareLogLabel}:页面已直接进入 OAuth 授权页,本步骤按已完成处理。`, 'ok');
return {
ready: true,
alreadyVerified: true,
state: 'oauth_consent_page',
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
skipProfileStep: true,
retried: recoveryRound,
prepareSource,
};
}
if (snapshot.state === 'verification') {
await waitForDocumentLoadComplete(15000, `${prepareLogLabel}:注册验证码页面`);
await waitForVerificationCodeTarget(15000);
log(`${prepareLogLabel}:验证码页面已完成加载并就绪${recoveryRound ? `(期间自动恢复 ${recoveryRound} 次)` : ''}。`, 'ok');
return { ready: true, retried: recoveryRound, prepareSource };
}
if (snapshot.state === 'email_exists') {
throw new Error('当前邮箱已存在,需要重新开始新一轮。');
}
if (snapshot.state === 'contact_verification_server_error') {
const serverErrorText = String(snapshot.serverErrorText || '').trim()
|| 'OpenAI contact-verification 页面返回 HTTP ERROR 500。';
log(`${prepareLogLabel}:检测到 contact-verification 500 错误页,当前轮直接报错。`, 'warn');
throw buildContactVerificationServerError(serverErrorText);
}
if (snapshot.state === 'error') {
if (snapshot.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
recoveryRound += 1;
await recoverCurrentAuthRetryPage({
flow: 'signup',
logLabel: `${prepareLogLabel}:检测到注册认证重试页,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
step: 4,
timeoutMs: 12000,
});
continue;
}
if (snapshot.state === 'password') {
if (snapshot.passwordErrorText) {
log(`${prepareLogLabel}:检测到密码页报错“${snapshot.passwordErrorText}”,当前轮将回到步骤 1 重新开始。`, 'warn');
throw createSignupPhonePasswordMismatchError(snapshot.passwordErrorText);
}
if (!passwordPageDiagnosticsLogged) {
passwordPageDiagnosticsLogged = true;
logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`);
}
if (!password) {
throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。');
}
if ((snapshot.passwordInput.value || '') !== password) {
log(`${prepareLogLabel}:页面仍停留在密码页,正在重新填写密码...`, 'warn');
await humanPause(450, 1100);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'fill', label: 'retry-signup-password' }, async () => {
fillInput(snapshot.passwordInput, password);
});
}
if (snapshot.submitButton && isPasswordSubmitButtonReadyForRetry(snapshot.submitButton)) {
recoveryRound += 1;
log(`${prepareLogLabel}:页面仍停留在密码页,正在重新点击“继续”(第 ${recoveryRound}/${maxRecoveryRounds} 次)...`, 'warn');
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'fill-password', kind: 'submit', label: 'retry-submit-signup-password' }, async () => {
simulateClick(snapshot.submitButton);
});
await sleep(1200);
continue;
}
log(`${prepareLogLabel}:页面仍停留在密码页,但“继续”按钮暂不可用,准备继续等待(${recoveryRound}/${maxRecoveryRounds})...`, 'warn');
continue;
}
log(`${prepareLogLabel}:页面仍在切换中,准备继续等待(${recoveryRound}/${maxRecoveryRounds})...`, 'warn');
}
throw new Error(`等待注册验证码页面就绪超时或自动恢复失败(已尝试 ${recoveryRound}/${maxRecoveryRounds} 轮)。URL: ${location.href}`);
}
async function waitForVerificationSubmitOutcome(step, timeout, options = {}) {
const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000);
const purpose = options?.purpose || '';
const start = Date.now();
let recoveryCount = 0;
const maxRecoveryCount = 2;
while (Date.now() - start < resolvedTimeout) {
throwIfStopped();
const retryFlow = step === 4 ? 'signup' : 'login';
const retryState = getCurrentAuthRetryPageState(retryFlow);
if (retryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (step === 8 && retryState?.emailInUseBlocked) {
throw createStep8EmailInUseError();
}
if (step === 8 && retryState?.maxCheckAttemptsBlocked) {
throw createAuthMaxCheckAttemptsError();
}
if (retryState) {
if (recoveryCount >= maxRecoveryCount) {
throw new Error(`步骤 ${step}:验证码提交后连续进入认证重试页 ${maxRecoveryCount} 次,页面仍未恢复。URL: ${location.href}`);
}
recoveryCount += 1;
log(`步骤 ${step}:验证码提交后进入认证重试页,正在自动恢复(${recoveryCount}/${maxRecoveryCount})...`, 'warn');
await recoverCurrentAuthRetryPage({
flow: retryFlow,
logLabel: `步骤 ${step}:验证码提交后检测到认证重试页,正在点击“重试”恢复`,
step,
timeoutMs: 12000,
});
continue;
}
if (step === 4) {
const postVerificationState = getStep4PostVerificationState({ ignoreVerificationVisibility: true });
if (postVerificationState?.state === 'logged_in_home') {
return {
success: true,
skipProfileStep: true,
url: postVerificationState.url || location.href,
};
}
if (postVerificationState?.state === 'step5') {
return { success: true };
}
if (purpose === 'signup' && isEmailVerificationPage()) {
return {
success: true,
emailVerificationRequired: true,
emailVerificationPage: true,
url: location.href,
};
}
}
const errorText = getVerificationErrorText();
if (errorText) {
return { invalidCode: true, errorText };
}
if (step === 8 && isStep8Ready()) {
return { success: true };
}
if (step === 8 && isAddPhonePageReady()) {
return { success: true, addPhonePage: true, url: location.href };
}
await sleep(150);
}
if (step === 4) {
const signupRetryState = getCurrentAuthRetryPageState('signup');
if (signupRetryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
const postVerificationState = getStep4PostVerificationState({ ignoreVerificationVisibility: true });
if (postVerificationState?.state === 'logged_in_home') {
return {
success: true,
skipProfileStep: true,
url: postVerificationState.url || location.href,
};
}
if (postVerificationState?.state === 'step5') {
return { success: true };
}
if (purpose === 'signup' && isEmailVerificationPage()) {
return {
success: true,
emailVerificationRequired: true,
emailVerificationPage: true,
url: location.href,
};
}
}
if (isVerificationPageStillVisible()) {
return {
invalidCode: true,
errorText: getVerificationErrorText() || '提交后仍停留在验证码页面,准备重新发送验证码。',
};
}
return { success: true, assumed: true };
}
function getVerificationSubmitButtonForTarget(codeInput, options = {}) {
const { allowDisabled = false } = options;
const form = codeInput?.form || codeInput?.closest?.('form') || null;
const isUsableAction = (element) => {
if (!element || !isVisibleElement(element)) return false;
return allowDisabled || isActionEnabled(element);
};
const findSubmitInRoot = (root) => {
if (!root?.querySelectorAll) return null;
const directCandidates = root.querySelectorAll('button[type="submit"], input[type="submit"]');
for (const element of directCandidates) {
if (isUsableAction(element)) {
return element;
}
}
const textCandidates = root.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]');
return Array.from(textCandidates).find((element) => {
if (!isUsableAction(element)) return false;
const text = getActionText(element);
return /verify|confirm|submit|continue|确认|验证|继续|確認|認証|検証|続行|続ける|次へ|送信/i.test(text);
}) || null;
};
return findSubmitInRoot(form) || findSubmitInRoot(document);
}
async function waitForVerificationSubmitButton(codeInput, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (is405MethodNotAllowedPage()) {
throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码提交按钮。');
}
const button = getVerificationSubmitButtonForTarget(codeInput, { allowDisabled: false });
if (button) {
return button;
}
await sleep(150);
}
return null;
}
async function waitForVerificationCodeTarget(timeout = 10000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (is405MethodNotAllowedPage()) {
throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码输入框。');
}
const target = getVerificationCodeTarget();
if (target) {
return target;
}
await sleep(150);
}
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
async function waitForSplitVerificationInputsFilled(inputs, code, timeout = 2500) {
const expected = String(code || '').slice(0, 6);
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const current = Array.from(inputs || [])
.slice(0, expected.length)
.map((input) => String(input?.value || '').trim())
.join('');
if (current === expected) {
return true;
}
await sleep(100);
}
return false;
}
async function fillVerificationCode(step, payload) {
const { code, signupProfile } = payload;
if (!code) throw new Error('未提供验证码。');
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();
};
if (step === 4) {
const postVerificationState = getStep4PostVerificationState();
if (postVerificationState?.state === 'logged_in_home') {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:检测到页面已进入 ChatGPT 已登录态,本次验证码提交按成功处理。`, 'ok');
return {
success: true,
assumed: true,
alreadyAdvanced: true,
skipProfileStep: true,
url: postVerificationState.url || location.href,
};
}
if (postVerificationState?.state === 'step5') {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok');
return { success: true, assumed: true, alreadyAdvanced: true };
}
}
if (step === 8) {
if (isStep8Ready()) {
log(`步骤 ${step}:检测到页面已进入 OAuth 同意页,本次验证码提交按成功处理。`, 'ok');
return { success: true, assumed: true, alreadyAdvanced: true };
}
if (isAddPhonePageReady()) {
return { success: true, addPhonePage: true, url: location.href };
}
}
log(`步骤 ${step}:正在填写验证码:${code}`);
if (step === 8) {
await waitForLoginVerificationPageReady(10000, step, {
allowPhoneVerificationPage: payload?.purpose === 'login' || payload?.loginIdentifierType === 'phone',
});
}
if (step === 4) {
await waitForDocumentLoadComplete(15000, `步骤 ${step}:注册验证码页面`);
}
const combinedSignupProfilePage = step === 4
&& await waitForCombinedSignupVerificationProfilePage();
if (combinedSignupProfilePage) {
if (!signupProfile || !signupProfile.firstName || !signupProfile.lastName) {
throw new Error('当前注册验证码页面要求同时填写资料,但未提供姓名或生日数据。');
}
await step5_fillNameBirthday({
...signupProfile,
prefillOnly: true,
});
}
// Find code input — could be a single input or multiple separate inputs
// Retry with 405 error recovery if needed
const maxRetries = 3;
let codeInput = null;
let splitInputs = null;
for (let retry = 0; retry <= maxRetries; retry++) {
throwIfStopped();
// Before looking for input, check if page is in 405 error state
if (is405MethodNotAllowedPage()) {
log(`步骤 ${step}:检测到 405 错误页面,正在恢复...`, 'warn');
await handle405ResendError(step, 30000);
continue;
}
try {
const verificationTarget = await waitForVerificationCodeTarget(10000);
if (verificationTarget.type === 'split') {
splitInputs = verificationTarget.elements;
} else {
codeInput = verificationTarget.element;
}
break; // Found it
} catch {
// No input found — check if it's a 405 error and can be recovered
if (is405MethodNotAllowedPage() && retry < maxRetries) {
log(`步骤 ${step}:未找到验证码输入框且页面出现 405 错误,正在恢复...`, 'warn');
await handle405ResendError(step, 30000);
continue;
}
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
}
if (splitInputs?.length >= 6) {
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'grouped-code', label: 'split-code' }, async () => {
for (let i = 0; i < 6 && i < splitInputs.length; i++) {
const targetInput = splitInputs[i];
try {
targetInput.focus?.();
} catch {}
fillInput(splitInputs[i], code[i]);
try {
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true }));
} catch {}
}
});
const filled = await waitForSplitVerificationInputsFilled(splitInputs, code, 2500);
if (!filled) {
const current = Array.from(splitInputs)
.slice(0, 6)
.map((input) => String(input?.value || '').trim() || '_')
.join('');
log(`步骤 ${step}:分格验证码输入框未稳定呈现目标值,当前页面值为 ${current},准备继续观察提交流程。`, 'warn');
} else {
log(`步骤 ${step}:分格验证码输入框已稳定显示 ${code}。`, 'info');
}
await sleep(800);
const splitSubmitBtn = await waitForVerificationSubmitButton(splitInputs[0], 2000).catch(() => null);
if (splitSubmitBtn) {
await humanPause(450, 1200);
await performOperationWithDelay({ stepKey: 'fetch-signup-code', kind: 'submit', label: 'submit-code' }, async () => {
simulateClick(splitSubmitBtn);
});
log(`步骤 ${step}:分格验证码已提交`);
} else {
log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info');
}
const outcome = await waitForVerificationSubmitOutcome(step, undefined, payload);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.emailVerificationRequired) {
log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
}
if (combinedSignupProfilePage && !outcome.invalidCode) {
outcome.skipProfileStep = true;
outcome.skipProfileStepReason = 'combined_verification_profile';
}
return outcome;
}
if (!codeInput) {
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
await performOperationWithDelay({ stepKey: step === 8 ? 'oauth-login' : 'fetch-signup-code', kind: 'fill', label: 'verification-code' }, async () => {
fillInput(codeInput, code);
});
log(`步骤 ${step}:验证码已填写`);
// Submit
await sleep(800);
const submitBtn = await waitForVerificationSubmitButton(codeInput, 5000).catch(() => null);
if (submitBtn) {
await humanPause(450, 1200);
await performOperationWithDelay({ stepKey: step === 8 ? 'oauth-login' : 'fetch-signup-code', kind: 'submit', label: 'submit-code' }, async () => {
simulateClick(submitBtn);
});
log(`步骤 ${step}:验证码已提交`);
} else {
log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn');
}
const outcome = await waitForVerificationSubmitOutcome(step, undefined, payload);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.emailVerificationRequired) {
log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
}
if (combinedSignupProfilePage && !outcome.invalidCode) {
outcome.skipProfileStep = true;
outcome.skipProfileStepReason = 'combined_verification_profile';
}
return outcome;
}
// ============================================================
// Step 7: Login with registered account (on OAuth auth page)
// ============================================================
function getStep6OptionMessage(value, snapshot) {
return typeof value === 'function' ? value(snapshot) : String(value || '');
}
async function resolveStep6PostSubmitSnapshot(snapshot, options = {}) {
const normalizedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const {
via = 'post_submit',
loginVerificationRequestedAt = null,
oauthConsentVia = `${via}_oauth_consent`,
timeoutRecoveryReason = 'login_timeout_error_page',
timeoutRecoveryMessage = '登录提交后进入登录超时报错页。',
timeoutRecoveryVia = `${via}_timeout_recovered`,
allowPhoneVerificationPage = false,
allowPhoneAction = false,
allowPasswordAction = false,
allowEmailAction = false,
allowFinalPhoneAction = false,
allowFinalPasswordAction = false,
allowFinalEmailAction = false,
allowFinalSwitchAction = false,
visibleStep = 7,
final = false,
addPhoneMessage,
} = options;
if (normalizedSnapshot.state === 'verification_page' || (allowPhoneVerificationPage && normalizedSnapshot.state === 'phone_verification_page')) {
return {
action: 'done',
result: createStep6SuccessResult(normalizedSnapshot, {
via,
loginVerificationRequestedAt,
}),
};
}
if (normalizedSnapshot.state === 'oauth_consent_page') {
return {
action: 'done',
result: createStep6OAuthConsentSuccessResult(normalizedSnapshot, {
via: oauthConsentVia,
}),
};
}
if (normalizedSnapshot.state === 'add_email_page') {
return {
action: 'done',
result: createStep6AddEmailSuccessResult(normalizedSnapshot, {
via: `${via}_add_email`,
}),
};
}
if (normalizedSnapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
timeoutRecoveryReason,
normalizedSnapshot,
timeoutRecoveryMessage,
{
visibleStep,
loginVerificationRequestedAt,
via: timeoutRecoveryVia,
allowPhoneVerificationPage,
}
);
if (transition.action === 'done') {
return {
action: 'done',
result: transition.result,
};
}
if (transition.action === 'phone') {
return { action: 'phone', snapshot: transition.snapshot };
}
if (transition.action === 'password') {
return { action: 'password', snapshot: transition.snapshot };
}
if (transition.action === 'email') {
return { action: 'email', snapshot: transition.snapshot };
}
return {
action: 'recoverable',
result: transition.result,
};
}
if (normalizedSnapshot.state === 'phone_entry_page' && (allowPhoneAction || (final && allowFinalPhoneAction))) {
return { action: 'phone', snapshot: normalizedSnapshot };
}
if (normalizedSnapshot.state === 'password_page') {
if (allowPasswordAction || (final && allowFinalPasswordAction)) {
return { action: 'password', snapshot: normalizedSnapshot };
}
if (final && allowFinalSwitchAction && normalizedSnapshot.switchTrigger) {
return { action: 'switch', snapshot: normalizedSnapshot };
}
}
if (normalizedSnapshot.state === 'email_page' && (allowEmailAction || (final && allowFinalEmailAction))) {
return { action: 'email', snapshot: normalizedSnapshot };
}
if (normalizedSnapshot.state === 'add_phone_page') {
const message = getStep6OptionMessage(addPhoneMessage, normalizedSnapshot)
|| `登录提交后页面进入手机号页面。URL: ${normalizedSnapshot.url || location.href}`;
throw new Error(message);
}
return null;
}
async function waitForStep6PostSubmitTransition(options = {}) {
const {
timeout = 10000,
stalledReason = 'post_submit_stalled',
stalledMessage = '登录提交后未进入可识别的下一页。',
} = options;
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
const transition = await resolveStep6PostSubmitSnapshot(snapshot, {
...options,
final: false,
});
if (transition) {
return transition;
}
await sleep(250);
}
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
const transition = await resolveStep6PostSubmitSnapshot(snapshot, {
...options,
final: true,
});
if (transition) {
return transition;
}
return {
action: 'recoverable',
result: createStep6RecoverableResult(stalledReason, snapshot, {
message: stalledMessage,
loginVerificationRequestedAt: options.loginVerificationRequestedAt || null,
}),
};
}
async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000, options = {}) {
return waitForStep6PostSubmitTransition({
timeout,
visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7,
via: 'email_submit',
oauthConsentVia: 'email_submit_oauth_consent',
loginVerificationRequestedAt: emailSubmittedAt,
timeoutRecoveryMessage: '提交邮箱后进入登录超时报错页。',
timeoutRecoveryVia: 'email_submit_timeout_recovered',
allowPasswordAction: true,
stalledReason: 'email_submit_stalled',
stalledMessage: '提交邮箱后长时间未进入密码页或登录验证码页。',
addPhoneMessage: (snapshot) => `提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
}
async function waitForStep6PhoneSubmitTransition(phoneSubmittedAt, timeout = 12000, options = {}) {
return waitForStep6PostSubmitTransition({
timeout,
visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7,
via: 'phone_submit',
oauthConsentVia: 'phone_submit_oauth_consent',
loginVerificationRequestedAt: phoneSubmittedAt,
timeoutRecoveryMessage: '提交手机号后进入登录超时报错页。',
timeoutRecoveryVia: 'phone_submit_timeout_recovered',
allowPhoneVerificationPage: true,
allowPasswordAction: true,
allowFinalPhoneAction: true,
stalledReason: 'phone_submit_stalled',
stalledMessage: '提交手机号后长时间未进入密码页或手机验证码页。',
addPhoneMessage: (snapshot) => `提交手机号后页面直接进入手机号补全页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
}
async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000, options = {}) {
return waitForStep6PostSubmitTransition({
timeout,
visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7,
via: 'password_submit',
oauthConsentVia: 'password_submit_oauth_consent',
loginVerificationRequestedAt: passwordSubmittedAt,
timeoutRecoveryMessage: '提交密码后进入登录超时报错页。',
timeoutRecoveryVia: 'password_submit_timeout_recovered',
allowFinalSwitchAction: true,
stalledReason: 'password_submit_stalled',
stalledMessage: '提交密码后仍未进入登录验证码页。',
addPhoneMessage: (snapshot) => `提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
}
async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000, options = {}) {
const transition = await waitForStep6PostSubmitTransition({
timeout,
visibleStep: Math.floor(Number(options?.visibleStep) || 0) || 7,
via: 'switch_to_one_time_code_login',
oauthConsentVia: 'switch_to_one_time_code_oauth_consent',
loginVerificationRequestedAt,
timeoutRecoveryMessage: '切换到一次性验证码登录后进入登录超时报错页。',
timeoutRecoveryVia: 'switch_to_one_time_code_timeout_recovered',
stalledReason: 'one_time_code_switch_stalled',
stalledMessage: '点击一次性验证码登录后仍未进入登录验证码页。',
addPhoneMessage: (snapshot) => `切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`,
});
if (transition.action === 'done' || transition.action === 'recoverable') {
return transition.result;
}
return transition;
}
async function waitForLoginEntryOpenTransition(timeout = 10000) {
const start = Date.now();
let snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state !== 'unknown' && snapshot.state !== 'entry_page') {
return snapshot;
}
await sleep(250);
}
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 chooseAccountAction = await resolveChooseAccountAction(email);
if (
chooseAccountAction.snapshot
&& chooseAccountAction.snapshot.state !== 'unknown'
&& chooseAccountAction.snapshot.state !== 'choose_account_page'
) {
const resolvedSnapshot = chooseAccountAction.snapshot;
if (resolvedSnapshot.state === 'oauth_consent_page') {
return createStep6OAuthConsentSuccessResult(resolvedSnapshot, {
via: 'choose_account_oauth_consent_page',
});
}
if (resolvedSnapshot.oauthAuthorizationRoute || isPostChooseAccountOAuthRoute(resolvedSnapshot)) {
return createStep6OAuthConsentSuccessResult(resolvedSnapshot, {
via: 'choose_account_oauth_authorization_route',
});
}
if (resolvedSnapshot.state === 'email_page') {
return step6LoginFromEmailPage(payload, resolvedSnapshot);
}
if (resolvedSnapshot.state === 'entry_page') {
return step6OpenLoginEntry(payload, resolvedSnapshot);
}
if (resolvedSnapshot.state === 'password_page') {
return step6LoginFromPasswordPage(payload, resolvedSnapshot);
}
if (resolvedSnapshot.state === 'phone_entry_page') {
return step6LoginFromPhonePage(payload, resolvedSnapshot);
}
}
const target = chooseAccountAction.target;
if (!target) {
const otherAccountButton = chooseAccountAction.otherAccountButton;
if (otherAccountButton) {
const listedEmails = getChooseAccountListedEmails();
const listedLabel = listedEmails.length ? `页面已有账号:${listedEmails.join(', ')}。` : '页面未读取到已有账号邮箱。';
log(`OpenAI 选择账号页未列出目标邮箱 ${email},${listedLabel} 将点击“登录至另一个帐户”继续目标邮箱登录。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'choose-other-account' }, async () => {
simulateClick(otherAccountButton);
});
const otherAccountSnapshot = normalizeStep6Snapshot(await waitForChooseAccountTransition(15000));
if (otherAccountSnapshot.state === 'email_page') {
return step6LoginFromEmailPage(payload, otherAccountSnapshot);
}
if (otherAccountSnapshot.state === 'entry_page') {
return step6OpenLoginEntry(payload, otherAccountSnapshot);
}
if (otherAccountSnapshot.state === 'password_page') {
return step6LoginFromPasswordPage(payload, otherAccountSnapshot);
}
if (otherAccountSnapshot.state === 'phone_entry_page') {
return step6LoginFromPhonePage(payload, otherAccountSnapshot);
}
return createStep6RecoverableResult('choose_account_other_account_transition_stalled', otherAccountSnapshot, {
message: `Clicked another-account login because ${email} was not listed, but the page did not enter a supported login state.`,
});
}
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());
while (Date.now() - start < timeout) {
throwIfStopped();
snapshot = normalizeStep6Snapshot(inspectLoginAuthState());
if (snapshot.state !== 'unknown' && snapshot.state !== 'email_page' && snapshot.state !== 'entry_page') {
return snapshot;
}
await sleep(250);
}
return snapshot;
}
async function step6OpenLoginEntry(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 preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber);
const genericEntryTrigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger();
const phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
const trigger = genericEntryTrigger || (preferPhoneLogin ? phoneEntryTrigger : null);
if (!trigger || !isActionEnabled(trigger)) {
return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, {
message: preferPhoneLogin
? '当前登录入口页没有可点击的手机号登录入口。'
: '当前登录入口页没有可点击的邮箱登录入口。',
});
}
log(`检测到登录入口页,正在点击 "${getActionText(trigger).slice(0, 80)}"...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'open-login-entry' }, async () => {
simulateClick(trigger);
});
const nextSnapshot = await waitForLoginEntryOpenTransition();
if (nextSnapshot.state === 'email_page') {
if (preferPhoneLogin) {
return switchFromEmailPageToPhoneLogin(payload, nextSnapshot);
}
return step6LoginFromEmailPage(payload, nextSnapshot);
}
if (nextSnapshot.state === 'password_page') {
return step6LoginFromPasswordPage(payload, nextSnapshot);
}
if (nextSnapshot.state === 'phone_entry_page') {
return step6LoginFromPhonePage(payload, nextSnapshot);
}
if (nextSnapshot.state === 'verification_page') {
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: null,
via: 'entry_open_verification_page',
});
}
if (nextSnapshot.state === 'oauth_consent_page') {
return createStep6OAuthConsentSuccessResult(nextSnapshot, {
via: 'entry_open_oauth_consent_page',
});
}
if (nextSnapshot.state === 'add_email_page') {
return createStep6AddEmailSuccessResult(nextSnapshot, {
via: 'entry_open_add_email_page',
});
}
if (nextSnapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_after_entry_open',
nextSnapshot,
'点击登录入口后进入登录超时报错页。',
{ 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('login_entry_open_stalled', nextSnapshot, {
message: '点击登录入口后仍未进入手机号/邮箱/密码/验证码页。',
});
}
async function step6SwitchToOneTimeCodeLogin(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 switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger();
if (!switchTrigger || !isActionEnabled(switchTrigger)) {
return createStep6RecoverableResult('missing_one_time_code_trigger', normalizeStep6Snapshot(inspectLoginAuthState()), {
message: '当前登录页没有可用的一次性验证码登录入口。',
});
}
log('已检测到一次性验证码登录入口,准备切换...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
const loginVerificationRequestedAt = Date.now();
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'switch-one-time-code-login' }, async () => {
simulateClick(switchTrigger);
});
log('已点击一次性验证码登录', 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(1200);
const result = await waitForStep6SwitchTransition(loginVerificationRequestedAt, 10000, { visibleStep });
if (result?.step6Outcome === 'success') {
if (result.skipLoginVerificationStep || result.addEmailPage) {
return result;
}
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: result.loginVerificationRequestedAt || loginVerificationRequestedAt,
via: result.via || 'switch_to_one_time_code_login',
});
}
if (result?.action === 'password') {
return step6LoginFromPasswordPage(payload, result.snapshot);
}
if (result?.action === 'phone') {
return step6LoginFromPhonePage(payload, result.snapshot);
}
if (result?.action === 'email') {
return step6LoginFromEmailPage(payload, result.snapshot);
}
return result;
}
async function step6LoginFromPhonePage(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 phoneInput = currentSnapshot.phoneInput || getLoginPhoneInput();
const phoneNumber = String(payload?.phoneNumber || payload?.accountIdentifier || '').trim();
const countryLabel = String(payload?.countryLabel || '').trim();
const countryId = payload?.countryId;
if (!phoneNumber) {
return createStep6RecoverableResult('missing_phone_number', currentSnapshot, {
message: '手机号登录时缺少手机号,请重新执行步骤 2 获取号码。',
});
}
if (!phoneInput) {
return createStep6RecoverableResult('missing_phone_input', currentSnapshot, {
message: '当前登录页没有可用的手机号输入框。',
});
}
const dialCodeFromSelection = await selectCountryForPhoneInput(phoneInput, phoneNumber, countryLabel, { visibleStep });
const dialCode = dialCodeFromSelection || resolveLoginPhoneDialCode(phoneInput, {
phoneNumber,
countryId,
countryLabel,
});
const inputValue = toNationalPhoneNumber(phoneNumber, dialCode);
if (!inputValue) {
throw new Error(`步骤 ${visibleStep}:手机号为空,无法填写。`);
}
log(
`步骤 ${visibleStep}:手机号登录填写前诊断 ${JSON.stringify({
phoneNumber,
countryLabel,
countryId,
dialCode,
input: getLoginPhoneInputDiagnostics(phoneInput),
candidates: getLoginPhoneInputCandidateDiagnostics(),
})}`,
'info',
{ step: visibleStep, stepKey: 'oauth-login' }
);
log(`步骤 ${visibleStep}:正在填写手机号 ${phoneNumber}...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(500, 1400);
const fillResult = await fillLoginPhoneInputAndConfirm(phoneInput, {
phoneNumber,
dialCode,
visibleStep,
resolvePhoneInput: () => getLoginPhoneInput() || phoneInput,
});
log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode},本地号 ${fillResult.inputValue},可见提交值 ${fillResult.attemptedValue})` : ''}。`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(500);
const verifiedPhoneInput = fillResult.input || phoneInput;
const hiddenSync = await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'hidden-sync', label: 'login-phone-pre-submit-hidden-sync' }, async () => (
syncPhoneHiddenFormValue(verifiedPhoneInput, { phoneNumber, dialCode, inputValue })
));
const submitButton = getLoginSubmitButton({ allowDisabled: true }) || currentSnapshot.submitButton;
const preSubmitRenderedValue = getPhoneInputRenderedValue(verifiedPhoneInput);
const preSubmitHiddenInput = hiddenSync?.input || getPhoneHiddenValueInput(verifiedPhoneInput);
const preSubmitDiagnostics = {
renderedValue: preSubmitRenderedValue,
inputVerified: isPhoneInputValueComplete(verifiedPhoneInput, phoneNumber, dialCode, inputValue),
input: getLoginPhoneInputDiagnostics(verifiedPhoneInput),
hidden: getLoginPhoneHiddenValueDiagnostics(preSubmitHiddenInput),
submitButton: getLoginPhoneSubmitButtonDiagnostics(submitButton),
};
log(
`步骤 ${visibleStep}:手机号提交前复查 ${JSON.stringify(preSubmitDiagnostics)}`,
'info',
{ step: visibleStep, stepKey: 'oauth-login' }
);
if (!preSubmitDiagnostics.inputVerified) {
throw new Error(`步骤 ${visibleStep}:提交前手机号输入框复查失败,完整号码 ${phoneNumber},区号 +${dialCode || '未识别'},期望本地号 ${inputValue},当前输入框为 ${normalizePhoneDigits(preSubmitRenderedValue) || '空'},已停止提交。`);
}
const phoneSubmittedAt = Date.now();
await triggerLoginSubmitAction(submitButton, verifiedPhoneInput);
log(`步骤 ${visibleStep}:手机号已提交。`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
const transition = await waitForStep6PhoneSubmitTransition(phoneSubmittedAt, 12000, { visibleStep });
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep || transition.result?.addEmailPage) {
return transition.result;
}
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || phoneSubmittedAt,
via: transition.result.via || 'phone_submit',
allowPhoneVerificationPage: true,
});
}
if (transition.action === 'recoverable') {
log(transition.result.message || `提交手机号后仍未进入目标页面,准备重新执行步骤 ${visibleStep}。`, 'warn', {
step: visibleStep,
stepKey: 'oauth-login',
});
return transition.result;
}
if (transition.action === 'phone') {
return step6LoginFromPhonePage(payload, transition.snapshot);
}
if (transition.action === 'password') {
return step6LoginFromPasswordPage(payload, transition.snapshot);
}
if (transition.action === 'email') {
return step6LoginFromEmailPage(payload, transition.snapshot);
}
return createStep6RecoverableResult('phone_submit_unknown', normalizeStep6Snapshot(inspectLoginAuthState()), {
message: '提交手机号后未得到可用的下一步状态。',
});
}
async function switchFromEmailPageToPhoneLogin(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;
let currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
let phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
if (!phoneEntryTrigger || !isActionEnabled(phoneEntryTrigger)) {
const moreOptionsTrigger = currentSnapshot.moreOptionsTrigger || findLoginMoreOptionsTrigger();
if (moreOptionsTrigger && isActionEnabled(moreOptionsTrigger)) {
log(`步骤 ${visibleStep}:手机号入口可能隐藏在更多选项中,正在展开更多选项...`, 'info', {
step: visibleStep,
stepKey: 'oauth-login',
});
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'login-more-options' }, async () => {
simulateClick(moreOptionsTrigger);
});
await sleep(800);
currentSnapshot = normalizeStep6Snapshot(inspectLoginAuthState());
phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
}
}
if (!phoneEntryTrigger || !isActionEnabled(phoneEntryTrigger)) {
return createStep6RecoverableResult('missing_phone_login_entry_trigger', currentSnapshot, {
message: '本轮要求使用手机号登录,但当前邮箱登录页没有可用的手机号登录入口。',
});
}
log(`步骤 ${visibleStep}:当前在邮箱入口,正在切换到手机号登录...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'switch-phone-login' }, async () => {
simulateClick(phoneEntryTrigger);
});
const nextSnapshot = normalizeStep6Snapshot(await waitForPhoneLoginEntrySwitchTransition(20000));
if (nextSnapshot.state === 'phone_entry_page') {
return step6LoginFromPhonePage(payload, nextSnapshot);
}
if (nextSnapshot.state === 'password_page') {
return step6LoginFromPasswordPage(payload, nextSnapshot);
}
if (nextSnapshot.state === 'verification_page' || nextSnapshot.state === 'phone_verification_page') {
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: null,
via: 'phone_entry_switch_verification_page',
allowPhoneVerificationPage: true,
});
}
if (nextSnapshot.state === 'oauth_consent_page') {
return createStep6OAuthConsentSuccessResult(nextSnapshot, {
via: 'phone_entry_switch_oauth_consent_page',
});
}
if (nextSnapshot.state === 'add_email_page') {
return createStep6AddEmailSuccessResult(nextSnapshot, {
via: 'phone_entry_switch_add_email_page',
});
}
if (nextSnapshot.state === 'login_timeout_error_page') {
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_after_phone_entry_switch',
nextSnapshot,
'点击手机号登录入口后进入登录超时报错页。',
{
visibleStep,
allowPhoneVerificationPage: true,
}
);
if (transition.action === 'done') return transition.result;
if (transition.action === 'phone') return step6LoginFromPhonePage(payload, transition.snapshot);
if (transition.action === 'password') return step6LoginFromPasswordPage(payload, transition.snapshot);
if (transition.action === 'email') return step6LoginFromEmailPage(payload, transition.snapshot);
return transition.result;
}
return createStep6RecoverableResult('phone_login_entry_switch_stalled', nextSnapshot, {
message: `点击手机号登录入口后仍未进入手机号或密码页,当前停留在${getLoginAuthStateLabel(nextSnapshot)}。`,
});
}
async function step6LoginFromPasswordPage(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 hasPassword = Boolean(String(payload?.password || '').trim());
if (currentSnapshot.passwordInput) {
if (!hasPassword) {
if (currentSnapshot.switchTrigger) {
log('当前未提供密码,改走一次性验证码登录。', 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot);
}
return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, {
message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。',
});
}
log('已进入密码页,准备填写密码...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(550, 1450);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'login-password' }, async () => {
fillInput(currentSnapshot.passwordInput, payload.password);
});
log('已填写密码', 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(500);
const passwordSubmittedAt = Date.now();
await triggerLoginSubmitAction(currentSnapshot.submitButton, currentSnapshot.passwordInput);
log('已提交密码', 'info', { step: visibleStep, stepKey: 'oauth-login' });
const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt, 10000, { visibleStep });
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep || transition.result?.addEmailPage) {
return transition.result;
}
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || passwordSubmittedAt,
via: transition.result.via || 'password_submit',
});
}
if (transition.action === 'recoverable') {
log(transition.result.message || `提交密码后仍未进入登录验证码页面,准备重新执行步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return transition.result;
}
if (transition.action === 'password') {
return step6LoginFromPasswordPage(payload, transition.snapshot);
}
if (transition.action === 'phone') {
return step6LoginFromPhonePage(payload, transition.snapshot);
}
if (transition.action === 'email') {
return step6LoginFromEmailPage(payload, transition.snapshot);
}
if (transition.action === 'switch') {
return step6SwitchToOneTimeCodeLogin(payload, transition.snapshot);
}
return createStep6RecoverableResult('password_submit_unknown', normalizeStep6Snapshot(inspectLoginAuthState()), {
message: '提交密码后未得到可用的下一步状态。',
});
}
if (currentSnapshot.switchTrigger) {
return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot);
}
return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, {
message: '当前停留在登录页,但没有可提交密码的输入框,也没有一次性验证码登录入口。',
});
}
async function step6LoginFromEmailPage(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());
if (String(payload?.loginIdentifierType || '').trim() === 'phone' && payload?.phoneNumber) {
return switchFromEmailPageToPhoneLogin(payload, currentSnapshot);
}
const emailInput = currentSnapshot.emailInput || getLoginEmailInput();
if (!emailInput) {
throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
}
if ((emailInput.value || '').trim() !== payload.email) {
await humanPause(500, 1400);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'login-email' }, async () => {
fillInput(emailInput, payload.email);
});
log('已填写邮箱', 'info', { step: visibleStep, stepKey: 'oauth-login' });
} else {
log('邮箱已在输入框中,准备提交...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
}
await sleep(500);
const emailSubmittedAt = Date.now();
await triggerLoginSubmitAction(currentSnapshot.submitButton, emailInput);
log('已提交邮箱', 'info', { step: visibleStep, stepKey: 'oauth-login' });
const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt, 12000, { visibleStep });
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep || transition.result?.addEmailPage) {
return transition.result;
}
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || emailSubmittedAt,
via: transition.result.via || 'email_submit',
});
}
if (transition.action === 'recoverable') {
log(transition.result.message || `提交邮箱后仍未进入目标页面,准备重新执行步骤 ${visibleStep}。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' });
return transition.result;
}
if (transition.action === 'email') {
return step6LoginFromEmailPage(payload, transition.snapshot);
}
if (transition.action === 'password') {
return step6LoginFromPasswordPage(payload, transition.snapshot);
}
if (transition.action === 'phone') {
return step6LoginFromPhonePage(payload, transition.snapshot);
}
return createStep6RecoverableResult('email_submit_unknown', normalizeStep6Snapshot(inspectLoginAuthState()), {
message: '提交邮箱后未得到可用的下一步状态。',
});
}
async function step6_login(payload) {
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
const { email, phoneNumber } = payload;
const loginIdentifierType = String(payload?.loginIdentifierType || '').trim();
if (!email && !phoneNumber) throw new Error('登录时缺少邮箱地址或手机号。');
const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000));
if (snapshot.state === 'verification_page' || snapshot.state === 'phone_verification_page') {
log('认证页已在登录验证码页,开始确认页面是否稳定。', 'info', { step: visibleStep, stepKey: 'oauth-login' });
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: null,
via: snapshot.state === 'phone_verification_page'
? 'already_on_phone_verification_page'
: 'already_on_verification_page',
allowPhoneVerificationPage: snapshot.state === 'phone_verification_page',
});
}
if (snapshot.state === 'oauth_consent_page') {
log('认证页已直接进入 OAuth 授权页,跳过登录验证码步骤。', 'ok', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6OAuthConsentSuccessResult(snapshot, {
via: 'already_on_oauth_consent_page',
});
}
if (snapshot.state === 'add_email_page') {
log('认证页已在添加邮箱页,登录阶段完成。', 'ok', { step: visibleStep, stepKey: 'oauth-login' });
return createStep6AddEmailSuccessResult(snapshot, {
via: 'already_on_add_email_page',
});
}
if (snapshot.state === 'login_timeout_error_page') {
log('检测到登录超时报错页,先尝试恢复当前页面。', 'warn', { step: visibleStep, stepKey: 'oauth-login' });
const transition = await createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
snapshot,
'当前页面处于登录超时报错页。',
{
visibleStep,
loginVerificationRequestedAt: null,
via: 'login_timeout_initial_recovered',
allowPhoneVerificationPage: loginIdentifierType === 'phone' || Boolean(phoneNumber),
}
);
if (transition.action === 'done') {
if (transition.result?.skipLoginVerificationStep || transition.result?.addEmailPage) {
return transition.result;
}
return finalizeStep6VerificationReady({
visibleStep,
loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null,
via: transition.result.via || 'login_timeout_initial_recovered',
});
}
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;
}
if (snapshot.state === 'email_page') {
if (loginIdentifierType === 'phone' && phoneNumber) {
return switchFromEmailPageToPhoneLogin(payload, snapshot);
}
log(`正在使用 ${email} 登录...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
return step6LoginFromEmailPage(payload, snapshot);
}
if (snapshot.state === 'phone_entry_page') {
log('正在使用手机号登录...', 'info', { step: visibleStep, stepKey: 'oauth-login' });
return step6LoginFromPhonePage(payload, snapshot);
}
if (snapshot.state === 'password_page') {
log('认证页已在密码页,继续当前登录流程。', 'info', { step: visibleStep, stepKey: 'oauth-login' });
return step6LoginFromPasswordPage(payload, snapshot);
}
if (snapshot.state === 'choose_account_page') {
return step6ChooseExistingAccount(payload, snapshot);
}
if (snapshot.state === 'entry_page') {
return step6OpenLoginEntry(payload, snapshot);
}
throwForStep6FatalState(snapshot, visibleStep);
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
}
async function waitForAddEmailPageReady(timeout = 15000) {
const start = Date.now();
let sawAddEmailPage = false;
while (Date.now() - start < timeout) {
throwIfStopped();
if (isAddEmailPageReady()) {
sawAddEmailPage = true;
const snapshot = inspectLoginAuthState();
if (snapshot.emailInput || getLoginEmailInput()) {
return snapshot;
}
}
await sleep(200);
}
if (sawAddEmailPage) {
throw new Error('等待添加邮箱页面输入框就绪超时。URL: ' + location.href);
}
throw new Error('等待添加邮箱页面就绪超时。URL: ' + location.href);
}
async function waitForAddEmailSubmitOutcome(timeout = 45000) {
const start = Date.now();
let lastState = inspectLoginAuthState();
while (Date.now() - start < timeout) {
throwIfStopped();
lastState = inspectLoginAuthState();
if (lastState.state === 'verification_page') {
return {
success: true,
verificationPage: true,
displayedEmail: getLoginVerificationDisplayedEmail(),
url: location.href,
};
}
if (lastState.state === 'oauth_consent_page') {
return {
success: true,
directOAuthConsentPage: true,
url: location.href,
};
}
if (lastState.state === 'login_timeout_error_page') {
return {
retryPage: true,
maxCheckAttempts: Boolean(lastState.maxCheckAttemptsBlocked),
emailInUse: Boolean(lastState.emailInUseBlocked),
url: location.href,
};
}
const errorText = getVerificationErrorText();
if (errorText) {
return {
errorText,
url: location.href,
};
}
const addEmailErrorText = isAddEmailPageReady() ? getVisibleFieldErrorText() : '';
if (addEmailErrorText) {
return {
errorText: addEmailErrorText,
url: location.href,
};
}
await sleep(200);
}
throw new Error(`提交邮箱后未进入验证码页。当前状态:${getLoginAuthStateLabel(lastState)}。URL: ${lastState?.url || location.href}`);
}
async function submitAddEmailAndContinue(payload = {}) {
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 email = String(payload.email || '').trim().toLowerCase();
if (!email) {
throw new Error('未提供邮箱地址,无法添加邮箱。');
}
const snapshot = await waitForAddEmailPageReady();
const emailInput = snapshot.emailInput || getLoginEmailInput();
if (!emailInput) {
throw new Error('添加邮箱页未找到邮箱输入框。URL: ' + location.href);
}
await humanPause(500, 1400);
await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'fill', label: 'add-email' }, async () => {
fillInput(emailInput, email);
});
log(`步骤 8:已填写邮箱:${email}`);
await sleep(500);
const submitButton = snapshot.submitButton || getLoginSubmitButton({ allowDisabled: true });
if (!submitButton || !isActionEnabled(submitButton)) {
throw new Error('添加邮箱页未找到可点击的继续按钮。URL: ' + location.href);
}
await triggerLoginSubmitAction(submitButton, emailInput);
log('步骤 8:已提交邮箱,正在等待邮箱验证码页...');
const outcome = await waitForAddEmailSubmitOutcome();
if (outcome.errorText && (SIGNUP_EMAIL_EXISTS_PATTERN.test(outcome.errorText) || /email_in_use/i.test(outcome.errorText))) {
throw createStep8EmailInUseError();
}
if (outcome.errorText) {
throw new Error(`添加邮箱失败:${outcome.errorText}`);
}
if (outcome.emailInUse) {
throw createStep8EmailInUseError();
}
if (outcome.maxCheckAttempts) {
throw createAuthMaxCheckAttemptsError();
}
if (outcome.retryPage) {
throw new Error(`添加邮箱后进入认证重试页,请重新执行步骤 8。URL: ${outcome.url}`);
}
return {
submitted: true,
email,
...outcome,
};
}
// ============================================================
// Step 9: Find "继续" on OAuth consent page for debugger click
// ============================================================
// After login + verification, page shows:
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
// Background performs the actual click through the debugger Input API.
async function step8_findAndClick(payload = {}) {
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 9;
log('正在查找 OAuth 同意页的“继续”按钮...', 'info', { step: visibleStep, stepKey: 'confirm-oauth' });
const continueBtn = await prepareStep8ContinueButton();
const rect = getSerializableRect(continueBtn);
log('已找到“继续”按钮并准备好调试器点击坐标。', 'info', { step: visibleStep, stepKey: 'confirm-oauth' });
return {
rect,
buttonText: (continueBtn.textContent || '').trim(),
url: location.href,
};
}
function getStep8State() {
const continueBtn = getPrimaryContinueButton();
const retryState = getCurrentAuthRetryPageState('auth');
const state = {
url: location.href,
consentPage: isOAuthConsentPage(),
consentReady: isStep8Ready(),
verificationPage: isVerificationPageStillVisible(),
addPhonePage: isAddPhonePageReady(),
addEmailPage: isAddEmailPageReady(),
phoneVerificationPage: isPhoneVerificationPageReady(),
retryPage: Boolean(retryState),
retryEnabled: Boolean(retryState?.retryEnabled),
retryTitleMatched: Boolean(retryState?.titleMatched),
retryDetailMatched: Boolean(retryState?.detailMatched),
maxCheckAttemptsBlocked: Boolean(retryState?.maxCheckAttemptsBlocked),
buttonFound: Boolean(continueBtn),
buttonEnabled: isButtonEnabled(continueBtn),
buttonText: continueBtn ? getActionText(continueBtn) : '',
};
if (continueBtn) {
try {
state.rect = getSerializableRect(continueBtn);
} catch {
state.rect = null;
}
}
return state;
}
async function step8_triggerContinue(payload = {}) {
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 9;
const strategy = payload?.strategy || 'requestSubmit';
const continueBtn = await prepareStep8ContinueButton({
findTimeoutMs: payload?.findTimeoutMs,
enabledTimeoutMs: payload?.enabledTimeoutMs,
});
const form = continueBtn.form || continueBtn.closest('form');
switch (strategy) {
case 'requestSubmit':
if (!form || typeof form.requestSubmit !== 'function') {
throw new Error('“继续”按钮当前不在可提交的 form 中,无法使用 requestSubmit。URL: ' + location.href);
}
form.requestSubmit(continueBtn);
break;
case 'nativeClick':
continueBtn.click();
break;
case 'dispatchClick':
simulateClick(continueBtn);
break;
default:
throw new Error(`未知的 Step ${visibleStep} 触发策略:${strategy}`);
}
log(`continue button triggered via ${strategy}.`, 'info', { step: visibleStep, stepKey: 'confirm-oauth' });
return {
strategy,
...getStep8State(),
};
}
async function prepareStep8ContinueButton(options = {}) {
const {
findTimeoutMs = 10000,
enabledTimeoutMs = 8000,
} = options;
const continueBtn = await findContinueButton(findTimeoutMs);
await waitForButtonEnabled(continueBtn, enabledTimeoutMs);
await humanPause(250, 700);
continueBtn.scrollIntoView({ behavior: 'auto', block: 'center' });
continueBtn.focus();
await waitForStableButtonRect(continueBtn);
return continueBtn;
}
async function findContinueButton(timeout = 10000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (isAddPhonePageReady()) {
throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href);
}
if (isAddEmailPageReady()) {
throw new Error('当前页面已进入添加邮箱页面,不是 OAuth 授权同意页。URL: ' + location.href);
}
const button = getPrimaryContinueButton();
if (button && isStep8Ready()) {
return button;
}
await sleep(150);
}
throw new Error('在 OAuth 同意页未找到“继续”按钮,或页面尚未进入授权同意状态。URL: ' + location.href);
}
async function waitForButtonEnabled(button, timeout = 8000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (isButtonEnabled(button)) return;
await sleep(150);
}
throw new Error('“继续”按钮长时间不可点击。URL: ' + location.href);
}
function isButtonEnabled(button) {
return Boolean(button)
&& !button.disabled
&& button.getAttribute('aria-disabled') !== 'true';
}
async function waitForStableButtonRect(button, timeout = 1500) {
let previous = null;
let stableSamples = 0;
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const rect = button?.getBoundingClientRect?.();
if (rect && rect.width > 0 && rect.height > 0) {
const snapshot = {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
};
if (
previous
&& Math.abs(snapshot.left - previous.left) < 1
&& Math.abs(snapshot.top - previous.top) < 1
&& Math.abs(snapshot.width - previous.width) < 1
&& Math.abs(snapshot.height - previous.height) < 1
) {
stableSamples += 1;
if (stableSamples >= 2) {
return;
}
} else {
stableSamples = 0;
}
previous = snapshot;
}
await sleep(80);
}
}
function getSerializableRect(el) {
const rect = el.getBoundingClientRect();
if (!rect.width || !rect.height) {
throw new Error('滚动后“继续”按钮没有可点击尺寸。URL: ' + location.href);
}
return {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
centerX: rect.left + (rect.width / 2),
centerY: rect.top + (rect.height / 2),
};
}
// ============================================================
// Step 5: Fill Name & Birthday / Age
// ============================================================
function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, navigationEventType = '', outcome = null } = {}) {
const payload = {
profileSubmitted: true,
postSubmitChecked: !navigationStarted,
};
if (isAgeMode) {
payload.ageMode = true;
}
if (navigationStarted) {
payload.navigationStarted = true;
payload.handoffToBackground = true;
const resolvedNavigationEventType = String(navigationEventType || '').trim();
if (resolvedNavigationEventType) {
payload.navigationEventType = resolvedNavigationEventType;
}
if (typeof location !== 'undefined' && location?.href) {
payload.url = location.href;
}
}
if (outcome?.state) {
payload.postSubmitChecked = true;
payload.outcome = outcome.state;
}
if (outcome?.url) {
payload.url = outcome.url;
}
return payload;
}
function isCombinedSignupVerificationProfilePage() {
if (!isEmailVerificationPage() || !isVerificationPageStillVisible()) {
return false;
}
if (!document.querySelector('form[action*="email-verification/register" i]')) {
return false;
}
const nameInput = document.querySelector('input[name="name"], input[autocomplete="name"]');
if (!nameInput || !isVisibleElement(nameInput)) {
return false;
}
const ageInput = document.querySelector('input[name="age"]');
if (ageInput && isVisibleElement(ageInput)) {
return true;
}
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
return Boolean(
yearSpinner
&& monthSpinner
&& daySpinner
&& isVisibleElement(yearSpinner)
&& isVisibleElement(monthSpinner)
&& isVisibleElement(daySpinner)
);
}
async function waitForCombinedSignupVerificationProfilePage(timeout = 2500) {
const start = Date.now();
while (Date.now() - start < timeout) {
if (isCombinedSignupVerificationProfilePage()) {
return true;
}
await sleep(100);
}
return isCombinedSignupVerificationProfilePage();
}
function getStep5ProfilePathPatterns() {
return [
/\/create-account\/profile(?:[/?#]|$)/i,
/\/u\/signup\/profile(?:[/?#]|$)/i,
/\/signup\/profile(?:[/?#]|$)/i,
/\/about-you(?:[/?#]|$)/i,
];
}
function getStep5AuthRetryPathPatterns() {
const signupPatterns = typeof getSignupAuthRetryPathPatterns === 'function'
? getSignupAuthRetryPathPatterns()
: [];
return [
...signupPatterns,
...getStep5ProfilePathPatterns(),
];
}
function isStep5ProfilePageUrl(rawUrl = location.href) {
return isSignupProfilePageUrl(rawUrl);
}
function getStep5AuthRetryPageState() {
if (typeof getAuthTimeoutErrorPageState === 'function') {
return getAuthTimeoutErrorPageState({
pathPatterns: getStep5AuthRetryPathPatterns(),
});
}
if (typeof getCurrentAuthRetryPageState === 'function') {
return getCurrentAuthRetryPageState('signup');
}
return null;
}
function getStep5SubmitButton() {
const direct = document.querySelector('button[type="submit"], input[type="submit"]');
if (direct && isVisibleElement(direct)) {
return direct;
}
const candidates = document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el)) return false;
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();
return /完成|创建|create|continue|finish|done|agree|完了|作成|アカウント作成|アカウントを作成|続行|続ける|次へ|同意/i.test(text);
}) || null;
}
async function waitForStep5SubmitButton(timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const button = getStep5SubmitButton();
if (button) {
return button;
}
await sleep(150);
}
return null;
}
function isStep5SubmitButtonClickable(button) {
if (
!button
|| !isVisibleElement(button)
|| button.disabled
|| button.getAttribute?.('aria-disabled') === 'true'
) {
return false;
}
const ariaBusy = String(button.getAttribute?.('aria-busy') || '').trim().toLowerCase();
if (ariaBusy === 'true') {
return false;
}
const pendingAttr = [
button.getAttribute?.('data-loading'),
button.getAttribute?.('data-pending'),
button.getAttribute?.('data-submitting'),
button.getAttribute?.('data-state'),
]
.map((value) => String(value || '').trim().toLowerCase())
.filter(Boolean)
.join(' ');
if (/\b(?:true|loading|pending|submitting|busy)\b/.test(pendingAttr)) {
return false;
}
const pendingAncestor = button.closest?.([
'[aria-busy="true"]',
'[data-loading="true"]',
'[data-pending="true"]',
'[data-submitting="true"]',
'[data-state="loading"]',
'[data-state="pending"]',
'[data-state="submitting"]',
].join(', '));
if (pendingAncestor) {
return false;
}
let style = null;
try {
style = typeof window !== 'undefined' && window.getComputedStyle
? window.getComputedStyle(button)
: null;
} catch {
style = null;
}
if (style?.pointerEvents === 'none') {
return false;
}
const opacity = Number.parseFloat(style?.opacity || '');
if (Number.isFinite(opacity) && opacity < 0.8) {
return false;
}
return true;
}
function isStep5ProfileStillVisible() {
if (isStep5ProfilePageUrl()) {
return true;
}
return typeof isStep5Ready === 'function' ? isStep5Ready() : false;
}
function getStep5PostSubmitSuccessState() {
if (getStep5AuthRetryPageState()) {
return null;
}
if (isStep5CompletionChatgptUrl()) {
return {
state: 'logged_in_home',
url: location.href,
};
}
return null;
}
function getStep5SubmitState() {
const retryState = getStep5AuthRetryPageState();
const successState = getStep5PostSubmitSuccessState();
const errorText = typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '';
let signupAuthHost = false;
try {
const parsed = new URL(String(location.href || '').trim());
signupAuthHost = ['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com']
.includes(String(parsed.hostname || '').toLowerCase());
} catch {
signupAuthHost = false;
}
return {
url: location.href,
retryPage: Boolean(retryState),
retryEnabled: Boolean(retryState?.retryEnabled),
maxCheckAttemptsBlocked: Boolean(retryState?.maxCheckAttemptsBlocked),
userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked),
successState: successState?.state || '',
profileVisible: isStep5ProfileStillVisible(),
errorText,
unknownAuthPage: Boolean(
signupAuthHost
&& !retryState
&& !successState
&& !isStep5ProfileStillVisible()
),
};
}
function logStep5SubmitDebug(message, options = {}) {
const resolvedState = options?.state && typeof options.state === 'object'
? options.state
: getStep5SubmitState();
const summary = [
`url=${resolvedState?.url || location.href}`,
`retryPage=${Boolean(resolvedState?.retryPage)}`,
`retryEnabled=${Boolean(resolvedState?.retryEnabled)}`,
`successState=${resolvedState?.successState || 'none'}`,
`profileVisible=${Boolean(resolvedState?.profileVisible)}`,
`unknownAuthPage=${Boolean(resolvedState?.unknownAuthPage)}`,
`maxCheckAttemptsBlocked=${Boolean(resolvedState?.maxCheckAttemptsBlocked)}`,
`userAlreadyExistsBlocked=${Boolean(resolvedState?.userAlreadyExistsBlocked)}`,
resolvedState?.errorText ? `errorText=${resolvedState.errorText}` : null,
]
.filter(Boolean)
.join(' | ');
log(`步骤 5 [调试] ${message} | ${summary}`, options?.level || 'info', {
step: 5,
stepKey: 'fill-profile',
});
}
async function recoverStep5SubmitRetryPage(payload = {}) {
return recoverCurrentAuthRetryPage({
...payload,
flow: 'signup',
logLabel: payload?.logLabel || '步骤 5:资料提交后检测到认证重试页,正在点击“重试”恢复',
maxClickAttempts: payload?.maxClickAttempts ?? 2,
pathPatterns: Array.isArray(payload?.pathPatterns) ? payload.pathPatterns : getStep5AuthRetryPathPatterns(),
step: 5,
timeoutMs: payload?.timeoutMs ?? 12000,
});
}
function installStep5NavigationCompletionReporter(completeOnce) {
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
return () => {};
}
let reportedNavigation = false;
const debugLog = typeof logStep5SubmitDebug === 'function'
? logStep5SubmitDebug
: (message, options = {}) => {
if (typeof log === 'function') {
log(`步骤 5 [调试] ${message}`, options?.level || 'info', {
step: 5,
stepKey: 'fill-profile',
});
}
};
const onNavigationStarted = (event) => {
const eventType = String(event?.type || 'navigation').trim() || 'navigation';
if (reportedNavigation) {
return;
}
reportedNavigation = true;
if (typeof completeOnce === 'function') {
try {
completeOnce({
navigationStarted: true,
navigationEventType: eventType,
});
} catch (error) {
if (typeof log === 'function') {
log(`步骤 5 [调试] 导航交棒信号发送失败:${error?.message || error}`, 'warn', {
step: 5,
stepKey: 'fill-profile',
});
}
}
}
debugLog(`检测到页面开始导航(event=${eventType})。`, {
level: 'warn',
});
};
window.addEventListener('pagehide', onNavigationStarted, { once: true });
window.addEventListener('beforeunload', onNavigationStarted, { once: true });
return () => {
window.removeEventListener('pagehide', onNavigationStarted);
window.removeEventListener('beforeunload', onNavigationStarted);
};
}
async function waitForStep5SubmitOutcome(options = {}) {
const debugLog = typeof logStep5SubmitDebug === 'function'
? logStep5SubmitDebug
: (message, logOptions = {}) => {
if (typeof log === 'function') {
log(`步骤 5 [调试] ${message}`, logOptions?.level || 'info', {
step: 5,
stepKey: 'fill-profile',
});
}
};
const {
timeoutMs = 120000,
maxAuthRetryRecoveries = 2,
maxSubmitClicks = 3,
retryClickIntervalMs = 3500,
} = options;
const start = Date.now();
let authRetryRecoveryCount = 0;
let submitClickCount = 1;
let lastSubmitClickAt = Date.now();
let lastStep5Error = '';
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const retryState = getStep5AuthRetryPageState();
if (retryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (retryState?.maxCheckAttemptsBlocked) {
throw createAuthMaxCheckAttemptsError();
}
if (retryState) {
if (authRetryRecoveryCount >= maxAuthRetryRecoveries) {
throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${location.href}`);
}
authRetryRecoveryCount += 1;
debugLog(`检测到资料提交后的认证重试页,准备执行恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})。`, {
level: 'warn',
});
log(`步骤 5:资料提交后进入认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})...`, 'warn');
await recoverCurrentAuthRetryPage({
flow: 'signup',
logLabel: '步骤 5:资料提交后检测到认证重试页,正在点击“重试”恢复',
maxClickAttempts: 2,
pathPatterns: getStep5AuthRetryPathPatterns(),
step: 5,
timeoutMs: 12000,
});
debugLog('认证重试页恢复动作已完成,准备继续等待最终结果。', {
level: 'info',
});
lastSubmitClickAt = Date.now();
continue;
}
const successState = getStep5PostSubmitSuccessState();
if (successState) {
debugLog(`检测到资料提交成功状态:${successState.state || 'unknown'}`, {
level: 'ok',
});
return successState;
}
const step5Error = typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '';
if (step5Error) {
lastStep5Error = step5Error;
}
if (
isStep5ProfileStillVisible()
&& submitClickCount < maxSubmitClicks
&& Date.now() - lastSubmitClickAt >= retryClickIntervalMs
) {
const submitButton = getStep5SubmitButton();
if (isStep5SubmitButtonClickable(submitButton)) {
submitClickCount += 1;
log(`步骤 5:资料提交后仍停留在资料页,正在重新点击“完成帐户创建”(第 ${submitClickCount}/${maxSubmitClicks} 次)...`, 'warn');
await humanPause(350, 900);
simulateClick(submitButton);
lastSubmitClickAt = Date.now();
await sleep(1000);
continue;
}
}
await sleep(250);
}
const finalRetryState = getStep5AuthRetryPageState();
if (finalRetryState?.userAlreadyExistsBlocked) {
throw createSignupUserAlreadyExistsError();
}
if (finalRetryState?.maxCheckAttemptsBlocked) {
throw createAuthMaxCheckAttemptsError();
}
if (finalRetryState) {
throw new Error(`步骤 5:资料提交后仍停留在认证重试页,自动恢复未完成。URL: ${location.href}`);
}
const finalSuccessState = getStep5PostSubmitSuccessState();
if (finalSuccessState) {
return finalSuccessState;
}
const finalStep5Error = (typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '') || lastStep5Error;
if (finalStep5Error) {
throw new Error(`步骤 5:资料提交后页面返回错误:${finalStep5Error}。URL: ${location.href}`);
}
throw new Error(`步骤 5:资料提交后未检测到页面跳转或恢复成功(已点击提交 ${submitClickCount}/${maxSubmitClicks} 次)。URL: ${location.href}`);
}
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload;
if (!firstName || !lastName) throw new Error('未提供姓名数据。');
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 resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
throw new Error('未提供生日或年龄数据。');
}
const fullName = `${firstName} ${lastName}`;
log(`步骤 5:正在填写姓名:${fullName}`);
// Actual DOM structure:
// - Full name:
// - Birthday: React Aria DateField or hidden input[name="birthday"]
// - Age:
// --- Full Name (single field, not first+last) ---
let nameInput = null;
try {
nameInput = await waitForElement(
'input[name="name"], input[placeholder*="全名"], input[placeholder*="氏名"], input[placeholder*="名前"], input[placeholder*="お名前"], input[autocomplete="name"]',
10000
);
} catch {
throw new Error('未找到姓名输入框。URL: ' + location.href);
}
await humanPause(500, 1300);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-name' }, async () => {
fillInput(nameInput, fullName);
});
log(`步骤 5:姓名已填写:${fullName}`);
let birthdayMode = false;
let ageInput = null;
let yearSpinner = null;
let monthSpinner = null;
let daySpinner = null;
let hiddenBirthday = null;
let yearReactSelect = null;
let monthReactSelect = null;
let dayReactSelect = null;
let visibleAgeInput = false;
let visibleBirthdaySpinners = false;
let visibleBirthdaySelects = false;
const findBirthdaySelect = (...labels) => labels
.map((label) => findBirthdayReactAriaSelect(label))
.find(Boolean) || null;
for (let i = 0; i < 100; i++) {
yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
hiddenBirthday = document.querySelector('input[name="birthday"]');
ageInput = document.querySelector('input[name="age"]');
yearReactSelect = findBirthdaySelect('年', 'Year');
monthReactSelect = findBirthdaySelect('月', 'Month');
dayReactSelect = findBirthdaySelect('天', '日', 'Day');
visibleAgeInput = Boolean(ageInput && isVisibleElement(ageInput));
visibleBirthdaySpinners = Boolean(
yearSpinner
&& monthSpinner
&& daySpinner
&& isVisibleElement(yearSpinner)
&& isVisibleElement(monthSpinner)
&& isVisibleElement(daySpinner)
);
visibleBirthdaySelects = Boolean(
yearReactSelect?.button
&& monthReactSelect?.button
&& dayReactSelect?.button
&& isVisibleElement(yearReactSelect.button)
&& isVisibleElement(monthReactSelect.button)
&& isVisibleElement(dayReactSelect.button)
);
if (visibleAgeInput) break;
if (visibleBirthdaySpinners || visibleBirthdaySelects) {
birthdayMode = true;
break;
}
await sleep(100);
}
if (birthdayMode) {
if (!hasBirthdayData) {
throw new Error('检测到生日字段,但未提供生日数据。');
}
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
const yearReactSelect = findBirthdaySelect('年', 'Year');
const monthReactSelect = findBirthdaySelect('月', 'Month');
const dayReactSelect = findBirthdaySelect('天', '日', 'Day');
if (yearReactSelect?.nativeSelect && monthReactSelect?.nativeSelect && dayReactSelect?.nativeSelect) {
const desiredDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const hiddenBirthday = document.querySelector('input[name="birthday"]');
log('步骤 5:检测到 React Aria 下拉生日字段,正在填写生日...');
await humanPause(450, 1100);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'select', label: 'select-birthday-year' }, async () => {
await setReactAriaBirthdaySelect(yearReactSelect, year);
});
await humanPause(250, 650);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'select', label: 'select-birthday-month' }, async () => {
await setReactAriaBirthdaySelect(monthReactSelect, month);
});
await humanPause(250, 650);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'select', label: 'select-birthday-day' }, async () => {
await setReactAriaBirthdaySelect(dayReactSelect, day);
});
if (hiddenBirthday) {
const start = Date.now();
while (Date.now() - start < 2000) {
if ((hiddenBirthday.value || '') === desiredDate) break;
await sleep(100);
}
if ((hiddenBirthday.value || '') !== desiredDate) {
throw new Error(`生日值未成功写入页面。期望 ${desiredDate},实际 ${(hiddenBirthday.value || '空')}。`);
}
}
log(`步骤 5:React Aria 生日已填写:${desiredDate}`);
}
if (yearSpinner && monthSpinner && daySpinner) {
log('步骤 5:检测到生日字段,正在填写生日...');
async function setSpinButton(el, value) {
el.focus();
await sleep(100);
document.execCommand('selectAll', false, null);
await sleep(50);
const valueStr = String(value);
for (const char of valueStr) {
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
await sleep(50);
}
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
el.blur();
await sleep(100);
}
await humanPause(450, 1100);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday-year' }, async () => {
await setSpinButton(yearSpinner, year);
});
await humanPause(250, 650);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday-month' }, async () => {
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
});
await humanPause(250, 650);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday-day' }, async () => {
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
});
log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
}
const hiddenBirthday = document.querySelector('input[name="birthday"]');
if (hiddenBirthday) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'hidden-sync', label: 'profile-dom-sync' }, async () => {
hiddenBirthday.value = dateStr;
hiddenBirthday.dispatchEvent(new Event('input', { bubbles: true }));
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
});
log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
}
} else if (ageInput) {
if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
throw new Error('检测到年龄字段,但未提供年龄数据。');
}
await humanPause(500, 1300);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'fill', label: 'fill-birthday' }, async () => {
fillInput(ageInput, String(resolvedAge));
});
log(`步骤 5:年龄已填写:${resolvedAge}`);
} else {
throw new Error('未找到生日或年龄输入项。URL: ' + location.href);
}
// 韩国IP判断勾选框""I agree"
const allConsentCheckbox = findStep5AllConsentCheckbox();
if (allConsentCheckbox) {
if (!isStep5CheckboxChecked(allConsentCheckbox)) {
const checkboxLabel = allConsentCheckbox.closest('label');
await humanPause(500, 1500);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'click', label: 'accept-profile-consent' }, async () => {
if (checkboxLabel && isVisibleElement(checkboxLabel)) {
simulateClick(checkboxLabel);
} else {
simulateClick(allConsentCheckbox);
}
});
await sleep(250);
if (!isStep5CheckboxChecked(allConsentCheckbox)) {
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'click', label: 'accept-profile-consent-fallback' }, async () => {
allConsentCheckbox.click();
});
await sleep(250);
}
if (!isStep5CheckboxChecked(allConsentCheckbox)) {
throw new Error('未能勾选 “I agree to all of the following” 复选框。');
}
log('步骤 5:已勾选 “I agree to all of the following”。');
} else {
log('步骤 5:“I agree to all of the following” 已勾选,跳过。');
}
}
if (prefillOnly) {
log('步骤 4:混合注册页资料已预填,继续填写验证码。', 'info');
return { prefilled: true };
}
// Click "完成帐户创建" button
await sleep(500);
const completeBtn = await waitForStep5SubmitButton(5000)
|| await waitForElementByText('button', /完成|完了|作成|アカウント作成|アカウントを作成|続行|続ける|次へ|同意|create|continue|finish|done|agree/i, 5000).catch(() => null);
if (!completeBtn) {
throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href);
}
const isAgeMode = !birthdayMode && Boolean(ageInput);
if (isAgeMode) {
log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将等待页面结果。', 'info');
}
let reportedCompletionPayload = null;
const debugLog = typeof logStep5SubmitDebug === 'function'
? logStep5SubmitDebug
: (message, logOptions = {}) => {
if (typeof log === 'function') {
log(`步骤 5 [调试] ${message}`, logOptions?.level || 'info', {
step: 5,
stepKey: 'fill-profile',
});
}
};
function completeStep5Once(extra = {}) {
const completionReason = extra?.outcome?.state
|| (extra?.navigationStarted ? `navigation_started:${extra?.navigationEventType || 'unknown'}` : 'direct_completion');
if (reportedCompletionPayload) {
debugLog(`忽略重复完成信号(reason=${completionReason})。`, {
level: 'warn',
});
return reportedCompletionPayload;
}
const completionPayload = getStep5DirectCompletionPayload({
isAgeMode,
navigationStarted: Boolean(extra.navigationStarted),
navigationEventType: extra.navigationEventType || '',
outcome: extra.outcome || null,
});
reportedCompletionPayload = completionPayload;
if (extra?.navigationStarted && typeof reportNodeComplete === 'function') {
reportNodeComplete('fill-profile', completionPayload);
} else {
reportComplete(5, completionPayload);
}
debugLog(`准备发送完成信号(reason=${completionReason},isAgeMode=${isAgeMode})。`, {
level: extra?.navigationStarted ? 'warn' : 'info',
});
return completionPayload;
}
const cleanupNavigationReporter = installStep5NavigationCompletionReporter(completeStep5Once);
await humanPause(500, 1300);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'submit', label: 'submit-profile' }, async () => {
simulateClick(completeBtn);
});
log('步骤 5:已点击“完成帐户创建”,正在等待页面跳转、重试页或提交结果。');
try {
const outcome = await waitForStep5SubmitOutcome();
cleanupNavigationReporter();
const completionPayload = completeStep5Once({ outcome });
log(`步骤 5:资料提交结果已确认(${outcome.state || 'success'}),准备继续后续步骤。`, 'ok');
return completionPayload;
} catch (error) {
cleanupNavigationReporter();
throw error;
}
}