|
|
@@ -1,245 +1,305 @@
|
|
|
-// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5)
|
|
|
-// Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
|
|
|
-
|
|
|
-console.log('[MultiPage:signup-page] Content script loaded on', location.href);
|
|
|
-
|
|
|
-// Listen for commands from Background
|
|
|
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
- if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK' || message.type === 'CLICK_RESEND_EMAIL' || message.type === 'HANDLE_ABOUT_YOU') {
|
|
|
- resetStopState();
|
|
|
- handleCommand(message).then((result) => {
|
|
|
- sendResponse({ ok: true, ...(result || {}) });
|
|
|
- }).catch(err => {
|
|
|
- if (isStopError(err)) {
|
|
|
- log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
|
|
|
- sendResponse({ stopped: true, error: err.message });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (message.type === 'STEP8_FIND_AND_CLICK') {
|
|
|
- log(`Step 8: ${err.message}`, 'error');
|
|
|
- sendResponse({ error: err.message });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- reportError(message.step, err.message);
|
|
|
- sendResponse({ error: err.message });
|
|
|
- });
|
|
|
- return true;
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-async function handleCommand(message) {
|
|
|
- switch (message.type) {
|
|
|
- case 'EXECUTE_STEP':
|
|
|
- switch (message.step) {
|
|
|
- case 2: return await step2_clickRegister();
|
|
|
- case 3: return await step3_fillEmailPassword(message.payload);
|
|
|
- case 5: return await step5_fillNameBirthday(message.payload);
|
|
|
- case 6: return await step6_login(message.payload);
|
|
|
- case 8: return await step8_findAndClick();
|
|
|
- default: throw new Error(`signup-page.js does not handle step ${message.step}`);
|
|
|
- }
|
|
|
- case 'FILL_CODE':
|
|
|
- // Step 4 = signup code, Step 7 = login code (same handler)
|
|
|
- return await fillVerificationCode(message.step, message.payload);
|
|
|
- case 'CLICK_RESEND_EMAIL':
|
|
|
- return await clickResendEmail(message.step);
|
|
|
- case 'STEP8_FIND_AND_CLICK':
|
|
|
- return await step8_findAndClick();
|
|
|
- case 'HANDLE_ABOUT_YOU':
|
|
|
- return await handleAboutYouPage(message.payload);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Step 2: Click Register
|
|
|
-// ============================================================
|
|
|
-
|
|
|
+// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5)
|
|
|
+// Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
|
|
|
+
|
|
|
+console.log('[MultiPage:signup-page] Content script loaded on', location.href);
|
|
|
+
|
|
|
+// Listen for commands from Background
|
|
|
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
+ if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK' || message.type === 'CLICK_RESEND_EMAIL' || message.type === 'HANDLE_ABOUT_YOU') {
|
|
|
+ resetStopState();
|
|
|
+ handleCommand(message).then((result) => {
|
|
|
+ sendResponse({ ok: true, ...(result || {}) });
|
|
|
+ }).catch(err => {
|
|
|
+ if (isStopError(err)) {
|
|
|
+ log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
|
|
|
+ sendResponse({ stopped: true, error: err.message });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (message.type === 'STEP8_FIND_AND_CLICK') {
|
|
|
+ log(`Step 8: ${err.message}`, 'error');
|
|
|
+ sendResponse({ error: err.message });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ reportError(message.step, err.message);
|
|
|
+ sendResponse({ error: err.message });
|
|
|
+ });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+async function handleCommand(message) {
|
|
|
+ switch (message.type) {
|
|
|
+ case 'EXECUTE_STEP':
|
|
|
+ switch (message.step) {
|
|
|
+ case 2: return await step2_clickRegister();
|
|
|
+ case 3: return await step3_fillEmailPassword(message.payload);
|
|
|
+ case 5: return await step5_fillNameBirthday(message.payload);
|
|
|
+ case 6: return await step6_login(message.payload);
|
|
|
+ case 8: return await step8_findAndClick();
|
|
|
+ default: throw new Error(`signup-page.js does not handle step ${message.step}`);
|
|
|
+ }
|
|
|
+ case 'FILL_CODE':
|
|
|
+ // Step 4 = signup code, Step 7 = login code (same handler)
|
|
|
+ return await fillVerificationCode(message.step, message.payload);
|
|
|
+ case 'CLICK_RESEND_EMAIL':
|
|
|
+ return await clickResendEmail(message.step);
|
|
|
+ case 'STEP8_FIND_AND_CLICK':
|
|
|
+ return await step8_findAndClick();
|
|
|
+ case 'HANDLE_ABOUT_YOU':
|
|
|
+ return await handleAboutYouPage(message.payload);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Step 2: Click Register
|
|
|
+// ============================================================
|
|
|
+
|
|
|
async function step2_clickRegister() {
|
|
|
log('Step 2: Looking for Register/Sign up button...');
|
|
|
-
|
|
|
- let registerBtn = null;
|
|
|
- try {
|
|
|
- registerBtn = await waitForElementByText(
|
|
|
- 'a, button, [role="button"], [role="link"]',
|
|
|
- /sign\s*up|register|create\s*account|注册/i,
|
|
|
- 10000
|
|
|
- );
|
|
|
- } catch {
|
|
|
- // Some pages may have a direct link
|
|
|
- try {
|
|
|
- registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
|
|
|
- } catch {
|
|
|
- throw new Error(
|
|
|
- 'Could not find Register/Sign up button. ' +
|
|
|
- 'Check auth page DOM in DevTools. URL: ' + location.href
|
|
|
- );
|
|
|
- }
|
|
|
+
|
|
|
+ let registerBtn = null;
|
|
|
+ try {
|
|
|
+ registerBtn = await waitForElementByText(
|
|
|
+ 'a, button, [role="button"], [role="link"]',
|
|
|
+ /sign\s*up|register|create\s*account|注册/i,
|
|
|
+ 10000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ // Some pages may have a direct link
|
|
|
+ try {
|
|
|
+ registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
|
|
|
+ } catch {
|
|
|
+ throw new Error(
|
|
|
+ 'Could not find Register/Sign up button. ' +
|
|
|
+ 'Check auth page DOM in DevTools. URL: ' + location.href
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
await humanPause(450, 1200);
|
|
|
- reportComplete(2);
|
|
|
simulateClick(registerBtn);
|
|
|
log('Step 2: Clicked Register button');
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Step 3: Fill Email & Password
|
|
|
-// ============================================================
|
|
|
|
|
|
-async function step3_fillEmailPassword(payload) {
|
|
|
- const { email } = payload;
|
|
|
- if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
|
|
|
-
|
|
|
- log(`Step 3: Filling email: ${email}`);
|
|
|
-
|
|
|
- // Find email input
|
|
|
- let emailInput = null;
|
|
|
+ // Don't mark step 2 complete until the signup form is actually reachable.
|
|
|
+ // This avoids auto-run racing ahead while a human-verification challenge
|
|
|
+ // or page transition is still in progress.
|
|
|
try {
|
|
|
- emailInput = await waitForElement(
|
|
|
+ await waitForElement(
|
|
|
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
|
|
|
- 10000
|
|
|
+ 30000
|
|
|
);
|
|
|
+ log('Step 2: Signup form is ready after human verification/page transition');
|
|
|
} catch {
|
|
|
- throw new Error('Could not find email input field on signup page. URL: ' + location.href);
|
|
|
- }
|
|
|
-
|
|
|
- await humanPause(500, 1400);
|
|
|
- fillInput(emailInput, email);
|
|
|
- log('Step 3: Email filled');
|
|
|
-
|
|
|
- // Check if password field is on the same page
|
|
|
- let passwordInput = document.querySelector('input[type="password"]');
|
|
|
-
|
|
|
- if (!passwordInput) {
|
|
|
- // Need to submit email first to get to password page
|
|
|
- log('Step 3: No password field yet, submitting email first...');
|
|
|
- const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
|
|
-
|
|
|
- if (submitBtn) {
|
|
|
- await humanPause(400, 1100);
|
|
|
- simulateClick(submitBtn);
|
|
|
- log('Step 3: Submitted email, waiting for password field...');
|
|
|
- await sleep(2000);
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- passwordInput = await waitForElement('input[type="password"]', 10000);
|
|
|
- } catch {
|
|
|
- throw new Error('Could not find password input after submitting email. URL: ' + location.href);
|
|
|
+ // Fallback: some auth pages may delay rendering, but URL/path has already moved on.
|
|
|
+ if (/signup|register|email|continue/i.test(location.href)) {
|
|
|
+ log('Step 2: Signup page transition detected, proceeding', 'warn');
|
|
|
+ } else {
|
|
|
+ throw new Error('Register clicked, but signup form did not become ready after 30s. Human verification may still be pending. URL: ' + location.href);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
|
|
|
- await humanPause(600, 1500);
|
|
|
- fillInput(passwordInput, payload.password);
|
|
|
- log('Step 3: Password filled');
|
|
|
-
|
|
|
- // Report complete BEFORE submit, because submit causes page navigation
|
|
|
- // which kills the content script connection
|
|
|
- reportComplete(3, { email });
|
|
|
-
|
|
|
- // Submit the form (page will navigate away after this)
|
|
|
- await sleep(500);
|
|
|
- const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
|
|
|
-
|
|
|
- if (submitBtn) {
|
|
|
- await humanPause(500, 1300);
|
|
|
- simulateClick(submitBtn);
|
|
|
- log('Step 3: Form submitted');
|
|
|
- }
|
|
|
+ reportComplete(2);
|
|
|
}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Click "重新发送电子邮件" (used before step 4 and step 7 polling)
|
|
|
-// ============================================================
|
|
|
-
|
|
|
-async function clickResendEmail(step) {
|
|
|
- log(`Step ${step}: Looking for "重新发送电子邮件" button...`);
|
|
|
-
|
|
|
- let resendBtn = null;
|
|
|
- try {
|
|
|
- resendBtn = await waitForElementByText(
|
|
|
- 'a, button, [role="button"], [role="link"], span',
|
|
|
- /重新发送电子邮件|resend\s*email/i,
|
|
|
- 10000
|
|
|
- );
|
|
|
- } catch {
|
|
|
- log(`Step ${step}: "重新发送电子邮件" button not found, skipping`, 'warn');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Prevent parent form POST submission (Remix/React Router route without action)
|
|
|
- const parentForm = resendBtn.closest('form');
|
|
|
- const blockSubmit = (e) => e.preventDefault();
|
|
|
- if (parentForm) parentForm.addEventListener('submit', blockSubmit, { once: true });
|
|
|
-
|
|
|
- await humanPause(400, 1000);
|
|
|
- resendBtn.click();
|
|
|
- log(`Step ${step}: Clicked "重新发送电子邮件"`, 'ok');
|
|
|
- await sleep(2000);
|
|
|
-
|
|
|
- if (parentForm) parentForm.removeEventListener('submit', blockSubmit);
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Step 3: Fill Email & Password
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function step3_fillEmailPassword(payload) {
|
|
|
+ const { email } = payload;
|
|
|
+ if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
|
|
|
+
|
|
|
+ log(`Step 3: Filling email: ${email}`);
|
|
|
+
|
|
|
+ // Find email input
|
|
|
+ let emailInput = null;
|
|
|
+ try {
|
|
|
+ emailInput = await waitForElement(
|
|
|
+ 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
|
|
|
+ 10000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ throw new Error('Could not find email input field on signup page. URL: ' + location.href);
|
|
|
+ }
|
|
|
+
|
|
|
+ await humanPause(500, 1400);
|
|
|
+ fillInput(emailInput, email);
|
|
|
+ log('Step 3: Email filled');
|
|
|
+
|
|
|
+ // Check if password field is on the same page
|
|
|
+ let passwordInput = document.querySelector('input[type="password"]');
|
|
|
+
|
|
|
+ if (!passwordInput) {
|
|
|
+ // Need to submit email first to get to password page
|
|
|
+ log('Step 3: No password field yet, submitting email first...');
|
|
|
+ const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
|
|
+
|
|
|
+ if (submitBtn) {
|
|
|
+ await humanPause(400, 1100);
|
|
|
+ simulateClick(submitBtn);
|
|
|
+ log('Step 3: Submitted email, waiting for password field...');
|
|
|
+ await sleep(2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ passwordInput = await waitForElement('input[type="password"]', 10000);
|
|
|
+ } catch {
|
|
|
+ throw new Error('Could not find password input after submitting email. URL: ' + location.href);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
|
|
|
+ await humanPause(600, 1500);
|
|
|
+ fillInput(passwordInput, payload.password);
|
|
|
+ log('Step 3: Password filled');
|
|
|
+
|
|
|
+ // Report complete BEFORE submit, because submit causes page navigation
|
|
|
+ // which kills the content script connection
|
|
|
+ reportComplete(3, { email });
|
|
|
+
|
|
|
+ // Submit the form (page will navigate away after this)
|
|
|
+ await sleep(500);
|
|
|
+ const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
|
|
|
+
|
|
|
+ if (submitBtn) {
|
|
|
+ await humanPause(500, 1300);
|
|
|
+ simulateClick(submitBtn);
|
|
|
+ log('Step 3: Form submitted');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Click "重新发送电子邮件" (used before step 4 and step 7 polling)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function clickResendEmail(step) {
|
|
|
+ log(`Step ${step}: Looking for "重新发送电子邮件" button...`);
|
|
|
+
|
|
|
+ let resendBtn = null;
|
|
|
+ try {
|
|
|
+ resendBtn = await waitForElementByText(
|
|
|
+ 'a, button, [role="button"], [role="link"], span',
|
|
|
+ /重新发送电子邮件|resend\s*email/i,
|
|
|
+ 10000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ log(`Step ${step}: "重新发送电子邮件" button not found, skipping`, 'warn');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prevent parent form POST submission (Remix/React Router route without action)
|
|
|
+ const parentForm = resendBtn.closest('form');
|
|
|
+ const blockSubmit = (e) => e.preventDefault();
|
|
|
+ if (parentForm) parentForm.addEventListener('submit', blockSubmit, { once: true });
|
|
|
+
|
|
|
+ await humanPause(400, 1000);
|
|
|
+ resendBtn.click();
|
|
|
+ log(`Step ${step}: Clicked "重新发送电子邮件"`, 'ok');
|
|
|
+ await sleep(2000);
|
|
|
+
|
|
|
+ if (parentForm) parentForm.removeEventListener('submit', blockSubmit);
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Fill Verification Code (used by step 4 and step 7)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function fillVerificationCode(step, payload) {
|
|
|
+ const { code } = payload;
|
|
|
+ if (!code) throw new Error('No verification code provided.');
|
|
|
+
|
|
|
+ log(`Step ${step}: Filling verification code: ${code}`);
|
|
|
+
|
|
|
+ // Find code input — could be a single input or multiple separate inputs
|
|
|
+ let codeInput = null;
|
|
|
+ try {
|
|
|
+ codeInput = await waitForElement(
|
|
|
+ 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
|
|
|
+ 10000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ // Check for multiple single-digit inputs (common pattern)
|
|
|
+ const singleInputs = document.querySelectorAll('input[maxlength="1"]');
|
|
|
+ if (singleInputs.length >= 6) {
|
|
|
+ log(`Step ${step}: Found single-digit code inputs, filling individually...`);
|
|
|
+ for (let i = 0; i < 6 && i < singleInputs.length; i++) {
|
|
|
+ fillInput(singleInputs[i], code[i]);
|
|
|
+ await sleep(100);
|
|
|
+ }
|
|
|
+ await sleep(1000);
|
|
|
+ // Verify page navigated away from verification page
|
|
|
+ await verifyCodeAccepted(step);
|
|
|
+ reportComplete(step);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw new Error('Could not find verification code input. URL: ' + location.href);
|
|
|
+ }
|
|
|
+
|
|
|
+ fillInput(codeInput, code);
|
|
|
+ log(`Step ${step}: Code filled`);
|
|
|
+
|
|
|
+ // Submit
|
|
|
+ await sleep(500);
|
|
|
+ const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证|继续/i, 5000).catch(() => null);
|
|
|
+
|
|
|
+ if (submitBtn) {
|
|
|
+ await humanPause(450, 1200);
|
|
|
+ simulateClick(submitBtn);
|
|
|
+ log(`Step ${step}: Verification submitted`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Wait and verify the page actually moved past the verification page
|
|
|
+ await verifyCodeAccepted(step);
|
|
|
+ reportComplete(step);
|
|
|
+}
|
|
|
+
|
|
|
+function hasVisibleVerificationInputs() {
|
|
|
+ const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
|
|
|
+ .filter(isElementVisible);
|
|
|
+ if (singleInputs.length >= 6) return true;
|
|
|
+
|
|
|
+ const selectors = [
|
|
|
+ 'input[name="code"]',
|
|
|
+ 'input[name="otp"]',
|
|
|
+ 'input[type="text"][maxlength="6"]',
|
|
|
+ 'input[aria-label*="code" i]',
|
|
|
+ 'input[placeholder*="code" i]',
|
|
|
+ 'input[inputmode="numeric"]',
|
|
|
+ ];
|
|
|
+ return selectors.some(selector => Array.from(document.querySelectorAll(selector)).some(isElementVisible));
|
|
|
}
|
|
|
|
|
|
-// ============================================================
|
|
|
-// Fill Verification Code (used by step 4 and step 7)
|
|
|
-// ============================================================
|
|
|
-
|
|
|
-async function fillVerificationCode(step, payload) {
|
|
|
- const { code } = payload;
|
|
|
- if (!code) throw new Error('No verification code provided.');
|
|
|
-
|
|
|
- log(`Step ${step}: Filling verification code: ${code}`);
|
|
|
-
|
|
|
- // Find code input — could be a single input or multiple separate inputs
|
|
|
- let codeInput = null;
|
|
|
- try {
|
|
|
- codeInput = await waitForElement(
|
|
|
- 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
|
|
|
- 10000
|
|
|
- );
|
|
|
- } catch {
|
|
|
- // Check for multiple single-digit inputs (common pattern)
|
|
|
- const singleInputs = document.querySelectorAll('input[maxlength="1"]');
|
|
|
- if (singleInputs.length >= 6) {
|
|
|
- log(`Step ${step}: Found single-digit code inputs, filling individually...`);
|
|
|
- for (let i = 0; i < 6 && i < singleInputs.length; i++) {
|
|
|
- fillInput(singleInputs[i], code[i]);
|
|
|
- await sleep(100);
|
|
|
- }
|
|
|
- await sleep(1000);
|
|
|
- // Verify page navigated away from verification page
|
|
|
- await verifyCodeAccepted(step);
|
|
|
- reportComplete(step);
|
|
|
- return;
|
|
|
- }
|
|
|
- throw new Error('Could not find verification code input. URL: ' + location.href);
|
|
|
- }
|
|
|
-
|
|
|
- fillInput(codeInput, code);
|
|
|
- log(`Step ${step}: Code filled`);
|
|
|
-
|
|
|
- // Submit
|
|
|
- await sleep(500);
|
|
|
- const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证|继续/i, 5000).catch(() => null);
|
|
|
-
|
|
|
- if (submitBtn) {
|
|
|
- await humanPause(450, 1200);
|
|
|
- simulateClick(submitBtn);
|
|
|
- log(`Step ${step}: Verification submitted`);
|
|
|
+function hasPostVerificationSignals() {
|
|
|
+ const currentPath = location.pathname;
|
|
|
+ if (currentPath.includes('/about-you')) return true;
|
|
|
+
|
|
|
+ const nextStepSelectors = [
|
|
|
+ 'input[name="name"]',
|
|
|
+ 'input[autocomplete="name"]',
|
|
|
+ 'input[name="birthday"]',
|
|
|
+ '[role="spinbutton"][data-type="year"]',
|
|
|
+ 'button[data-testid*="continue"]',
|
|
|
+ ];
|
|
|
+ if (nextStepSelectors.some(selector => Array.from(document.querySelectorAll(selector)).some(isElementVisible))) {
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- // Wait and verify the page actually moved past the verification page
|
|
|
- await verifyCodeAccepted(step);
|
|
|
- reportComplete(step);
|
|
|
+ const pageText = (document.body?.innerText || '').slice(0, 4000).toLowerCase();
|
|
|
+ return (
|
|
|
+ pageText.includes('使用 chatgpt 登录到 codex')
|
|
|
+ || pageText.includes('continue to codex')
|
|
|
+ || pageText.includes('consent')
|
|
|
+ || pageText.includes('生日')
|
|
|
+ || pageText.includes('full name')
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
-async function verifyCodeAccepted(step, timeout = 8000) {
|
|
|
+async function verifyCodeAccepted(step, timeout = 15000) {
|
|
|
const start = Date.now();
|
|
|
const verificationPaths = ['/email-verification', '/verify', '/otp'];
|
|
|
|
|
|
@@ -247,491 +307,494 @@ async function verifyCodeAccepted(step, timeout = 8000) {
|
|
|
throwIfStopped();
|
|
|
const currentPath = location.pathname;
|
|
|
const stillOnVerification = verificationPaths.some(p => currentPath.includes(p));
|
|
|
- if (!stillOnVerification) {
|
|
|
+ const verificationInputsVisible = hasVisibleVerificationInputs();
|
|
|
+ const postVerificationSignals = hasPostVerificationSignals();
|
|
|
+
|
|
|
+ if (!stillOnVerification || (!verificationInputsVisible && postVerificationSignals)) {
|
|
|
log(`Step ${step}: Verification code accepted, page navigated to ${currentPath}`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Check for error messages on the page (wrong code)
|
|
|
- const errorEl = document.querySelector('[class*="error"], [class*="Error"], [role="alert"]');
|
|
|
- if (errorEl) {
|
|
|
- const errorText = (errorEl.textContent || '').trim();
|
|
|
- if (errorText && errorText.length < 200) {
|
|
|
- throw new Error(`Verification code rejected: ${errorText}. URL: ${location.href}`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- await sleep(500);
|
|
|
- }
|
|
|
-
|
|
|
- // Still on verification page after timeout — code was likely wrong
|
|
|
- throw new Error(`Verification code ${step === 4 ? 'signup' : 'login'} was not accepted (page did not navigate). URL: ${location.href}`);
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Step 6: Login with registered account (on OAuth auth page)
|
|
|
-// ============================================================
|
|
|
-
|
|
|
-async function step6_login(payload) {
|
|
|
- const { email, password } = payload;
|
|
|
- if (!email) throw new Error('No email provided for login.');
|
|
|
-
|
|
|
- log(`Step 6: Logging in with ${email}...`);
|
|
|
-
|
|
|
- // Wait for email input on the auth page
|
|
|
- let emailInput = null;
|
|
|
- try {
|
|
|
- emailInput = await waitForElement(
|
|
|
- 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
|
|
|
- 15000
|
|
|
- );
|
|
|
- } catch {
|
|
|
- throw new Error('Could not find email input on login page. URL: ' + location.href);
|
|
|
- }
|
|
|
-
|
|
|
- await humanPause(500, 1400);
|
|
|
- fillInput(emailInput, email);
|
|
|
- log('Step 6: Email filled');
|
|
|
-
|
|
|
- // Submit email
|
|
|
- await sleep(500);
|
|
|
- const submitBtn1 = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
|
|
- if (submitBtn1) {
|
|
|
- await humanPause(400, 1100);
|
|
|
- simulateClick(submitBtn1);
|
|
|
- log('Step 6: Submitted email');
|
|
|
- }
|
|
|
-
|
|
|
- const passwordInput = await waitForLoginPasswordField();
|
|
|
- if (passwordInput) {
|
|
|
- log('Step 6: Password field found, filling password...');
|
|
|
- await humanPause(550, 1450);
|
|
|
- fillInput(passwordInput, password);
|
|
|
-
|
|
|
- await sleep(500);
|
|
|
- const submitBtn2 = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
|
|
|
- // Report complete BEFORE submit in case page navigates
|
|
|
- reportComplete(6, { needsOTP: true });
|
|
|
-
|
|
|
- if (submitBtn2) {
|
|
|
- await humanPause(450, 1200);
|
|
|
- simulateClick(submitBtn2);
|
|
|
- log('Step 6: Submitted password, may need verification code (step 7)');
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // No password field — OTP flow
|
|
|
- log('Step 6: No password field. OTP flow or auto-redirect.');
|
|
|
- reportComplete(6, { needsOTP: true });
|
|
|
-}
|
|
|
-
|
|
|
-async function waitForLoginPasswordField(timeout = 25000) {
|
|
|
- const start = Date.now();
|
|
|
-
|
|
|
- while (Date.now() - start < timeout) {
|
|
|
- throwIfStopped();
|
|
|
-
|
|
|
- const passwordInput = findVisiblePasswordInput();
|
|
|
- if (passwordInput) {
|
|
|
- return passwordInput;
|
|
|
- }
|
|
|
-
|
|
|
- await sleep(250);
|
|
|
- }
|
|
|
-
|
|
|
- log(`Step 6: Password field did not appear within ${Math.round(timeout / 1000)}s.`, 'warn');
|
|
|
- return null;
|
|
|
-}
|
|
|
-
|
|
|
-function findVisiblePasswordInput() {
|
|
|
- const inputs = document.querySelectorAll('input[type="password"]');
|
|
|
- for (const input of inputs) {
|
|
|
- if (isElementVisible(input)) {
|
|
|
- return input;
|
|
|
- }
|
|
|
- }
|
|
|
- return null;
|
|
|
-}
|
|
|
-
|
|
|
-function isElementVisible(el) {
|
|
|
- if (!el) return false;
|
|
|
- const style = window.getComputedStyle(el);
|
|
|
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
|
- return false;
|
|
|
- }
|
|
|
- const rect = el.getBoundingClientRect();
|
|
|
- return rect.width > 0 && rect.height > 0;
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Step 8: 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() {
|
|
|
- log('Step 8: Looking for OAuth consent "继续" button...');
|
|
|
-
|
|
|
- const continueBtn = await findContinueButton();
|
|
|
- await waitForButtonEnabled(continueBtn);
|
|
|
-
|
|
|
- await humanPause(350, 900);
|
|
|
- continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
- continueBtn.focus();
|
|
|
- await sleep(250);
|
|
|
-
|
|
|
- // Click using native .click() — more reliable than synthetic MouseEvent for trusted forms
|
|
|
- continueBtn.click();
|
|
|
- log('Step 8: Clicked "继续" button via .click()', 'ok');
|
|
|
-
|
|
|
- // Fallback: if the button is inside a form, also try submitting the form directly
|
|
|
- await sleep(2000);
|
|
|
- const form = continueBtn.closest('form');
|
|
|
- if (form && location.href.includes('authorize')) {
|
|
|
- log('Step 8: Also submitting parent form as fallback...');
|
|
|
- form.requestSubmit ? form.requestSubmit(continueBtn) : form.submit();
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- clicked: true,
|
|
|
- buttonText: (continueBtn.textContent || '').trim(),
|
|
|
- url: location.href,
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-async function findContinueButton() {
|
|
|
- try {
|
|
|
- return await waitForElement(
|
|
|
- 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
|
|
|
- 10000
|
|
|
- );
|
|
|
- } catch {
|
|
|
- try {
|
|
|
- return await waitForElementByText('button', /继续|Continue/, 5000);
|
|
|
- } catch {
|
|
|
- throw new Error('Could not find "继续" button on OAuth consent page. 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('"继续" button stayed disabled for too long. URL: ' + location.href);
|
|
|
-}
|
|
|
-
|
|
|
-function isButtonEnabled(button) {
|
|
|
- return Boolean(button)
|
|
|
- && !button.disabled
|
|
|
- && button.getAttribute('aria-disabled') !== 'true';
|
|
|
-}
|
|
|
-
|
|
|
-function getSerializableRect(el) {
|
|
|
- const rect = el.getBoundingClientRect();
|
|
|
- if (!rect.width || !rect.height) {
|
|
|
- throw new Error('"继续" button has no clickable size after scrolling. 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),
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Handle /about-you page (appears after login if birthday was missing)
|
|
|
-// ============================================================
|
|
|
-
|
|
|
-async function handleAboutYouPage(payload) {
|
|
|
- if (!location.href.includes('/about-you')) {
|
|
|
- return { handled: false };
|
|
|
- }
|
|
|
-
|
|
|
- log('Detected /about-you page, filling birthday info...');
|
|
|
-
|
|
|
- const { year, month, day, fullName } = payload || {};
|
|
|
- if (!year || !month || !day) {
|
|
|
- throw new Error('No birthday data available for about-you page.');
|
|
|
- }
|
|
|
-
|
|
|
- // Wait for the page to fully load
|
|
|
- await sleep(1000);
|
|
|
-
|
|
|
- // Fill name if present and empty
|
|
|
- const nameInput = document.querySelector('input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]');
|
|
|
- if (nameInput && !nameInput.value && fullName) {
|
|
|
- fillInput(nameInput, fullName);
|
|
|
- log('About-you: Name filled');
|
|
|
- await humanPause(300, 800);
|
|
|
- }
|
|
|
-
|
|
|
- // Fill birthday using the shared helper
|
|
|
- await fillBirthdayFields(year, month, day, 'About-you');
|
|
|
-
|
|
|
- // Click continue/submit button
|
|
|
- await sleep(500);
|
|
|
- const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /continue|继续|完成|done|agree|submit/i, 5000).catch(() => null);
|
|
|
- if (submitBtn) {
|
|
|
- await humanPause(500, 1300);
|
|
|
- simulateClick(submitBtn);
|
|
|
- log('About-you: Submitted form');
|
|
|
- }
|
|
|
-
|
|
|
- return { handled: true };
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Shared birthday filling helper (used by Step 5 and about-you)
|
|
|
-// ============================================================
|
|
|
-
|
|
|
-async function fillBirthdayFields(year, month, day, logPrefix) {
|
|
|
- const prefix = logPrefix || 'Birthday';
|
|
|
-
|
|
|
- // Strategy 1: React Aria DateField spinbuttons
|
|
|
- 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"]');
|
|
|
-
|
|
|
- if (yearSpinner && monthSpinner && daySpinner) {
|
|
|
- log(`${prefix}: Filling spinbutton date fields...`);
|
|
|
-
|
|
|
- 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 setSpinButton(yearSpinner, year);
|
|
|
- await humanPause(250, 650);
|
|
|
- await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
|
|
|
- await humanPause(250, 650);
|
|
|
- await setSpinButton(daySpinner, String(day).padStart(2, '0'));
|
|
|
- log(`${prefix}: Spinbutton date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Strategy 2: Select dropdowns (month/day/year)
|
|
|
- const selects = document.querySelectorAll('select');
|
|
|
- if (selects.length >= 2) {
|
|
|
- let monthSelect = null, daySelect = null, yearSelect = null;
|
|
|
- for (const sel of selects) {
|
|
|
- const name = (sel.name || sel.id || sel.getAttribute('aria-label') || '').toLowerCase();
|
|
|
- const opts = Array.from(sel.options).map(o => o.value);
|
|
|
- if (name.includes('month') || name.includes('mm')) {
|
|
|
- monthSelect = sel;
|
|
|
- } else if (name.includes('day') || name.includes('dd')) {
|
|
|
- daySelect = sel;
|
|
|
- } else if (name.includes('year') || name.includes('yyyy')) {
|
|
|
- yearSelect = sel;
|
|
|
- } else {
|
|
|
- // Heuristic: identify by option count and values
|
|
|
- const numericOpts = opts.filter(v => /^\d+$/.test(v));
|
|
|
- if (!monthSelect && numericOpts.length >= 12 && numericOpts.length <= 13) {
|
|
|
- monthSelect = sel;
|
|
|
- } else if (!daySelect && numericOpts.length >= 28 && numericOpts.length <= 32) {
|
|
|
- daySelect = sel;
|
|
|
- } else if (!yearSelect && numericOpts.some(v => Number(v) > 1900 && Number(v) < 2100)) {
|
|
|
- yearSelect = sel;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (monthSelect || daySelect || yearSelect) {
|
|
|
- log(`${prefix}: Filling select dropdown date fields...`);
|
|
|
- if (monthSelect) {
|
|
|
- setSelectValue(monthSelect, String(month));
|
|
|
- await humanPause(200, 500);
|
|
|
- }
|
|
|
- if (daySelect) {
|
|
|
- setSelectValue(daySelect, String(day));
|
|
|
- await humanPause(200, 500);
|
|
|
- }
|
|
|
- if (yearSelect) {
|
|
|
- setSelectValue(yearSelect, String(year));
|
|
|
- await humanPause(200, 500);
|
|
|
- }
|
|
|
- log(`${prefix}: Select date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Strategy 3: input[type="date"]
|
|
|
- const dateInput = document.querySelector('input[type="date"]');
|
|
|
- if (dateInput) {
|
|
|
- const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
|
- const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
|
- nativeSetter.call(dateInput, dateStr);
|
|
|
- dateInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
- dateInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- log(`${prefix}: Date input filled: ${dateStr}`);
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Strategy 4: Hidden input[name="birthday"] (fallback)
|
|
|
- const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
|
|
- if (hiddenBirthday) {
|
|
|
- const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
|
- hiddenBirthday.value = dateStr;
|
|
|
- hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- log(`${prefix}: Hidden birthday input set: ${dateStr}`);
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
-function setSelectValue(selectEl, value) {
|
|
|
- // Try exact match first, then try padded value
|
|
|
- const candidates = [value, value.padStart(2, '0')];
|
|
|
- for (const v of candidates) {
|
|
|
- const option = Array.from(selectEl.options).find(o => o.value === v || o.textContent.trim() === v);
|
|
|
- if (option) {
|
|
|
- selectEl.value = option.value;
|
|
|
- selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- selectEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- // Last resort: set by index if value is numeric
|
|
|
- const numVal = Number(value);
|
|
|
- if (!isNaN(numVal) && numVal > 0 && numVal < selectEl.options.length) {
|
|
|
- selectEl.selectedIndex = numVal;
|
|
|
- selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- selectEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ============================================================
|
|
|
-// Step 5: Fill Name & Birthday / Age
|
|
|
-// ============================================================
|
|
|
-
|
|
|
-async function step5_fillNameBirthday(payload) {
|
|
|
- const { firstName, lastName, age, year, month, day } = payload;
|
|
|
- if (!firstName || !lastName) throw new Error('No name data provided.');
|
|
|
-
|
|
|
- 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('No birthday or age data provided.');
|
|
|
- }
|
|
|
-
|
|
|
- const fullName = `${firstName} ${lastName}`;
|
|
|
- log(`Step 5: Filling name: ${fullName}`);
|
|
|
-
|
|
|
- // Actual DOM structure:
|
|
|
- // - Full name: <input name="name" placeholder="全名" type="text">
|
|
|
- // - Birthday: React Aria DateField or hidden input[name="birthday"]
|
|
|
- // - Age: <input name="age" type="text|number">
|
|
|
-
|
|
|
- // --- Full Name (single field, not first+last) ---
|
|
|
- let nameInput = null;
|
|
|
- try {
|
|
|
- nameInput = await waitForElement(
|
|
|
- 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
|
|
|
- 10000
|
|
|
- );
|
|
|
- } catch {
|
|
|
- throw new Error('Could not find name input. URL: ' + location.href);
|
|
|
- }
|
|
|
- await humanPause(500, 1300);
|
|
|
- fillInput(nameInput, fullName);
|
|
|
- log(`Step 5: Name filled: ${fullName}`);
|
|
|
-
|
|
|
- // Detect birthday/age input type with polling
|
|
|
- let ageInput = null;
|
|
|
- let hasBirthdayUI = false;
|
|
|
-
|
|
|
- for (let i = 0; i < 100; i++) {
|
|
|
- ageInput = document.querySelector('input[name="age"]');
|
|
|
- // Some pages include a hidden birthday input even though the real UI is "age".
|
|
|
- // In that case we must prioritize filling age to satisfy required validation.
|
|
|
- if (ageInput) break;
|
|
|
-
|
|
|
- // Check for any supported birthday UI (spinbuttons, selects, date input, hidden)
|
|
|
- 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 hiddenBirthday = document.querySelector('input[name="birthday"]');
|
|
|
- const dateInput = document.querySelector('input[type="date"]');
|
|
|
- const selects = document.querySelectorAll('select');
|
|
|
-
|
|
|
- if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday || dateInput || selects.length >= 2) {
|
|
|
- hasBirthdayUI = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- await sleep(100);
|
|
|
- }
|
|
|
-
|
|
|
- if (hasBirthdayUI) {
|
|
|
- if (!hasBirthdayData) {
|
|
|
- throw new Error('Birthday field detected, but no birthday data provided.');
|
|
|
- }
|
|
|
-
|
|
|
- // Use shared helper that tries spinbuttons → selects → date input → hidden input
|
|
|
- const filled = await fillBirthdayFields(year, month, day, 'Step 5');
|
|
|
- if (!filled) {
|
|
|
- log('Step 5: Warning - could not fill any visible birthday control', 'warn');
|
|
|
- }
|
|
|
- } else if (ageInput) {
|
|
|
- if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
|
|
|
- throw new Error('Age field detected, but no age data provided.');
|
|
|
- }
|
|
|
- await humanPause(500, 1300);
|
|
|
- fillInput(ageInput, String(resolvedAge));
|
|
|
- log(`Step 5: Age filled: ${resolvedAge}`);
|
|
|
-
|
|
|
- // Some age-mode pages still submit a hidden birthday field.
|
|
|
- // Keep it aligned with generated data so backend validation won't reject.
|
|
|
- const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
|
|
- if (hiddenBirthday && hasBirthdayData) {
|
|
|
- const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
|
- hiddenBirthday.value = dateStr;
|
|
|
- hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- log(`Step 5: Hidden birthday input set (age mode): ${dateStr}`);
|
|
|
- }
|
|
|
- } else {
|
|
|
- throw new Error('Could not find birthday or age input. URL: ' + location.href);
|
|
|
- }
|
|
|
-
|
|
|
- // Click "完成帐户创建" button
|
|
|
- await sleep(500);
|
|
|
- const completeBtn = document.querySelector('button[type="submit"]')
|
|
|
- || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
|
|
|
-
|
|
|
- // Report complete BEFORE submit (page navigates to add-phone after this)
|
|
|
- reportComplete(5);
|
|
|
-
|
|
|
- if (completeBtn) {
|
|
|
- await humanPause(500, 1300);
|
|
|
- simulateClick(completeBtn);
|
|
|
- log('Step 5: Clicked "完成帐户创建"');
|
|
|
- }
|
|
|
-}
|
|
|
+ const errorEl = document.querySelector('[class*="error"], [class*="Error"], [role="alert"]');
|
|
|
+ if (errorEl) {
|
|
|
+ const errorText = (errorEl.textContent || '').trim();
|
|
|
+ if (errorText && errorText.length < 200) {
|
|
|
+ throw new Error(`Verification code rejected: ${errorText}. URL: ${location.href}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await sleep(500);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Still on verification page after timeout — code was likely wrong
|
|
|
+ throw new Error(`Verification code ${step === 4 ? 'signup' : 'login'} was not accepted (page did not navigate). URL: ${location.href}`);
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Step 6: Login with registered account (on OAuth auth page)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function step6_login(payload) {
|
|
|
+ const { email, password } = payload;
|
|
|
+ if (!email) throw new Error('No email provided for login.');
|
|
|
+
|
|
|
+ log(`Step 6: Logging in with ${email}...`);
|
|
|
+
|
|
|
+ // Wait for email input on the auth page
|
|
|
+ let emailInput = null;
|
|
|
+ try {
|
|
|
+ emailInput = await waitForElement(
|
|
|
+ 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
|
|
|
+ 15000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ throw new Error('Could not find email input on login page. URL: ' + location.href);
|
|
|
+ }
|
|
|
+
|
|
|
+ await humanPause(500, 1400);
|
|
|
+ fillInput(emailInput, email);
|
|
|
+ log('Step 6: Email filled');
|
|
|
+
|
|
|
+ // Submit email
|
|
|
+ await sleep(500);
|
|
|
+ const submitBtn1 = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
|
|
+ if (submitBtn1) {
|
|
|
+ await humanPause(400, 1100);
|
|
|
+ simulateClick(submitBtn1);
|
|
|
+ log('Step 6: Submitted email');
|
|
|
+ }
|
|
|
+
|
|
|
+ const passwordInput = await waitForLoginPasswordField();
|
|
|
+ if (passwordInput) {
|
|
|
+ log('Step 6: Password field found, filling password...');
|
|
|
+ await humanPause(550, 1450);
|
|
|
+ fillInput(passwordInput, password);
|
|
|
+
|
|
|
+ await sleep(500);
|
|
|
+ const submitBtn2 = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
|
|
|
+ // Report complete BEFORE submit in case page navigates
|
|
|
+ reportComplete(6, { needsOTP: true });
|
|
|
+
|
|
|
+ if (submitBtn2) {
|
|
|
+ await humanPause(450, 1200);
|
|
|
+ simulateClick(submitBtn2);
|
|
|
+ log('Step 6: Submitted password, may need verification code (step 7)');
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // No password field — OTP flow
|
|
|
+ log('Step 6: No password field. OTP flow or auto-redirect.');
|
|
|
+ reportComplete(6, { needsOTP: true });
|
|
|
+}
|
|
|
+
|
|
|
+async function waitForLoginPasswordField(timeout = 25000) {
|
|
|
+ const start = Date.now();
|
|
|
+
|
|
|
+ while (Date.now() - start < timeout) {
|
|
|
+ throwIfStopped();
|
|
|
+
|
|
|
+ const passwordInput = findVisiblePasswordInput();
|
|
|
+ if (passwordInput) {
|
|
|
+ return passwordInput;
|
|
|
+ }
|
|
|
+
|
|
|
+ await sleep(250);
|
|
|
+ }
|
|
|
+
|
|
|
+ log(`Step 6: Password field did not appear within ${Math.round(timeout / 1000)}s.`, 'warn');
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function findVisiblePasswordInput() {
|
|
|
+ const inputs = document.querySelectorAll('input[type="password"]');
|
|
|
+ for (const input of inputs) {
|
|
|
+ if (isElementVisible(input)) {
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function isElementVisible(el) {
|
|
|
+ if (!el) return false;
|
|
|
+ const style = window.getComputedStyle(el);
|
|
|
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const rect = el.getBoundingClientRect();
|
|
|
+ return rect.width > 0 && rect.height > 0;
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Step 8: 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() {
|
|
|
+ log('Step 8: Looking for OAuth consent "继续" button...');
|
|
|
+
|
|
|
+ const continueBtn = await findContinueButton();
|
|
|
+ await waitForButtonEnabled(continueBtn);
|
|
|
+
|
|
|
+ await humanPause(350, 900);
|
|
|
+ continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
+ continueBtn.focus();
|
|
|
+ await sleep(250);
|
|
|
+
|
|
|
+ // Click using native .click() — more reliable than synthetic MouseEvent for trusted forms
|
|
|
+ continueBtn.click();
|
|
|
+ log('Step 8: Clicked "继续" button via .click()', 'ok');
|
|
|
+
|
|
|
+ // Fallback: if the button is inside a form, also try submitting the form directly
|
|
|
+ await sleep(2000);
|
|
|
+ const form = continueBtn.closest('form');
|
|
|
+ if (form && location.href.includes('authorize')) {
|
|
|
+ log('Step 8: Also submitting parent form as fallback...');
|
|
|
+ form.requestSubmit ? form.requestSubmit(continueBtn) : form.submit();
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ clicked: true,
|
|
|
+ buttonText: (continueBtn.textContent || '').trim(),
|
|
|
+ url: location.href,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function findContinueButton() {
|
|
|
+ try {
|
|
|
+ return await waitForElement(
|
|
|
+ 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
|
|
|
+ 10000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ try {
|
|
|
+ return await waitForElementByText('button', /继续|Continue/, 5000);
|
|
|
+ } catch {
|
|
|
+ throw new Error('Could not find "继续" button on OAuth consent page. 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('"继续" button stayed disabled for too long. URL: ' + location.href);
|
|
|
+}
|
|
|
+
|
|
|
+function isButtonEnabled(button) {
|
|
|
+ return Boolean(button)
|
|
|
+ && !button.disabled
|
|
|
+ && button.getAttribute('aria-disabled') !== 'true';
|
|
|
+}
|
|
|
+
|
|
|
+function getSerializableRect(el) {
|
|
|
+ const rect = el.getBoundingClientRect();
|
|
|
+ if (!rect.width || !rect.height) {
|
|
|
+ throw new Error('"继续" button has no clickable size after scrolling. 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),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Handle /about-you page (appears after login if birthday was missing)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function handleAboutYouPage(payload) {
|
|
|
+ if (!location.href.includes('/about-you')) {
|
|
|
+ return { handled: false };
|
|
|
+ }
|
|
|
+
|
|
|
+ log('Detected /about-you page, filling birthday info...');
|
|
|
+
|
|
|
+ const { year, month, day, fullName } = payload || {};
|
|
|
+ if (!year || !month || !day) {
|
|
|
+ throw new Error('No birthday data available for about-you page.');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Wait for the page to fully load
|
|
|
+ await sleep(1000);
|
|
|
+
|
|
|
+ // Fill name if present and empty
|
|
|
+ const nameInput = document.querySelector('input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]');
|
|
|
+ if (nameInput && !nameInput.value && fullName) {
|
|
|
+ fillInput(nameInput, fullName);
|
|
|
+ log('About-you: Name filled');
|
|
|
+ await humanPause(300, 800);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fill birthday using the shared helper
|
|
|
+ await fillBirthdayFields(year, month, day, 'About-you');
|
|
|
+
|
|
|
+ // Click continue/submit button
|
|
|
+ await sleep(500);
|
|
|
+ const submitBtn = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /continue|继续|完成|done|agree|submit/i, 5000).catch(() => null);
|
|
|
+ if (submitBtn) {
|
|
|
+ await humanPause(500, 1300);
|
|
|
+ simulateClick(submitBtn);
|
|
|
+ log('About-you: Submitted form');
|
|
|
+ }
|
|
|
+
|
|
|
+ return { handled: true };
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Shared birthday filling helper (used by Step 5 and about-you)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function fillBirthdayFields(year, month, day, logPrefix) {
|
|
|
+ const prefix = logPrefix || 'Birthday';
|
|
|
+
|
|
|
+ // Strategy 1: React Aria DateField spinbuttons
|
|
|
+ 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"]');
|
|
|
+
|
|
|
+ if (yearSpinner && monthSpinner && daySpinner) {
|
|
|
+ log(`${prefix}: Filling spinbutton date fields...`);
|
|
|
+
|
|
|
+ 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 setSpinButton(yearSpinner, year);
|
|
|
+ await humanPause(250, 650);
|
|
|
+ await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
|
|
|
+ await humanPause(250, 650);
|
|
|
+ await setSpinButton(daySpinner, String(day).padStart(2, '0'));
|
|
|
+ log(`${prefix}: Spinbutton date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Strategy 2: Select dropdowns (month/day/year)
|
|
|
+ const selects = document.querySelectorAll('select');
|
|
|
+ if (selects.length >= 2) {
|
|
|
+ let monthSelect = null, daySelect = null, yearSelect = null;
|
|
|
+ for (const sel of selects) {
|
|
|
+ const name = (sel.name || sel.id || sel.getAttribute('aria-label') || '').toLowerCase();
|
|
|
+ const opts = Array.from(sel.options).map(o => o.value);
|
|
|
+ if (name.includes('month') || name.includes('mm')) {
|
|
|
+ monthSelect = sel;
|
|
|
+ } else if (name.includes('day') || name.includes('dd')) {
|
|
|
+ daySelect = sel;
|
|
|
+ } else if (name.includes('year') || name.includes('yyyy')) {
|
|
|
+ yearSelect = sel;
|
|
|
+ } else {
|
|
|
+ // Heuristic: identify by option count and values
|
|
|
+ const numericOpts = opts.filter(v => /^\d+$/.test(v));
|
|
|
+ if (!monthSelect && numericOpts.length >= 12 && numericOpts.length <= 13) {
|
|
|
+ monthSelect = sel;
|
|
|
+ } else if (!daySelect && numericOpts.length >= 28 && numericOpts.length <= 32) {
|
|
|
+ daySelect = sel;
|
|
|
+ } else if (!yearSelect && numericOpts.some(v => Number(v) > 1900 && Number(v) < 2100)) {
|
|
|
+ yearSelect = sel;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (monthSelect || daySelect || yearSelect) {
|
|
|
+ log(`${prefix}: Filling select dropdown date fields...`);
|
|
|
+ if (monthSelect) {
|
|
|
+ setSelectValue(monthSelect, String(month));
|
|
|
+ await humanPause(200, 500);
|
|
|
+ }
|
|
|
+ if (daySelect) {
|
|
|
+ setSelectValue(daySelect, String(day));
|
|
|
+ await humanPause(200, 500);
|
|
|
+ }
|
|
|
+ if (yearSelect) {
|
|
|
+ setSelectValue(yearSelect, String(year));
|
|
|
+ await humanPause(200, 500);
|
|
|
+ }
|
|
|
+ log(`${prefix}: Select date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Strategy 3: input[type="date"]
|
|
|
+ const dateInput = document.querySelector('input[type="date"]');
|
|
|
+ if (dateInput) {
|
|
|
+ const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
|
+ const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
|
+ nativeSetter.call(dateInput, dateStr);
|
|
|
+ dateInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
+ dateInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ log(`${prefix}: Date input filled: ${dateStr}`);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Strategy 4: Hidden input[name="birthday"] (fallback)
|
|
|
+ const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
|
|
+ if (hiddenBirthday) {
|
|
|
+ const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
|
+ hiddenBirthday.value = dateStr;
|
|
|
+ hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ log(`${prefix}: Hidden birthday input set: ${dateStr}`);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+function setSelectValue(selectEl, value) {
|
|
|
+ // Try exact match first, then try padded value
|
|
|
+ const candidates = [value, value.padStart(2, '0')];
|
|
|
+ for (const v of candidates) {
|
|
|
+ const option = Array.from(selectEl.options).find(o => o.value === v || o.textContent.trim() === v);
|
|
|
+ if (option) {
|
|
|
+ selectEl.value = option.value;
|
|
|
+ selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ selectEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Last resort: set by index if value is numeric
|
|
|
+ const numVal = Number(value);
|
|
|
+ if (!isNaN(numVal) && numVal > 0 && numVal < selectEl.options.length) {
|
|
|
+ selectEl.selectedIndex = numVal;
|
|
|
+ selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ selectEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// Step 5: Fill Name & Birthday / Age
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+async function step5_fillNameBirthday(payload) {
|
|
|
+ const { firstName, lastName, age, year, month, day } = payload;
|
|
|
+ if (!firstName || !lastName) throw new Error('No name data provided.');
|
|
|
+
|
|
|
+ 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('No birthday or age data provided.');
|
|
|
+ }
|
|
|
+
|
|
|
+ const fullName = `${firstName} ${lastName}`;
|
|
|
+ log(`Step 5: Filling name: ${fullName}`);
|
|
|
+
|
|
|
+ // Actual DOM structure:
|
|
|
+ // - Full name: <input name="name" placeholder="全名" type="text">
|
|
|
+ // - Birthday: React Aria DateField or hidden input[name="birthday"]
|
|
|
+ // - Age: <input name="age" type="text|number">
|
|
|
+
|
|
|
+ // --- Full Name (single field, not first+last) ---
|
|
|
+ let nameInput = null;
|
|
|
+ try {
|
|
|
+ nameInput = await waitForElement(
|
|
|
+ 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
|
|
|
+ 10000
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ throw new Error('Could not find name input. URL: ' + location.href);
|
|
|
+ }
|
|
|
+ await humanPause(500, 1300);
|
|
|
+ fillInput(nameInput, fullName);
|
|
|
+ log(`Step 5: Name filled: ${fullName}`);
|
|
|
+
|
|
|
+ // Detect birthday/age input type with polling
|
|
|
+ let ageInput = null;
|
|
|
+ let hasBirthdayUI = false;
|
|
|
+
|
|
|
+ for (let i = 0; i < 100; i++) {
|
|
|
+ ageInput = document.querySelector('input[name="age"]');
|
|
|
+ // Some pages include a hidden birthday input even though the real UI is "age".
|
|
|
+ // In that case we must prioritize filling age to satisfy required validation.
|
|
|
+ if (ageInput) break;
|
|
|
+
|
|
|
+ // Check for any supported birthday UI (spinbuttons, selects, date input, hidden)
|
|
|
+ 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 hiddenBirthday = document.querySelector('input[name="birthday"]');
|
|
|
+ const dateInput = document.querySelector('input[type="date"]');
|
|
|
+ const selects = document.querySelectorAll('select');
|
|
|
+
|
|
|
+ if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday || dateInput || selects.length >= 2) {
|
|
|
+ hasBirthdayUI = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ await sleep(100);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hasBirthdayUI) {
|
|
|
+ if (!hasBirthdayData) {
|
|
|
+ throw new Error('Birthday field detected, but no birthday data provided.');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use shared helper that tries spinbuttons → selects → date input → hidden input
|
|
|
+ const filled = await fillBirthdayFields(year, month, day, 'Step 5');
|
|
|
+ if (!filled) {
|
|
|
+ log('Step 5: Warning - could not fill any visible birthday control', 'warn');
|
|
|
+ }
|
|
|
+ } else if (ageInput) {
|
|
|
+ if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
|
|
|
+ throw new Error('Age field detected, but no age data provided.');
|
|
|
+ }
|
|
|
+ await humanPause(500, 1300);
|
|
|
+ fillInput(ageInput, String(resolvedAge));
|
|
|
+ log(`Step 5: Age filled: ${resolvedAge}`);
|
|
|
+
|
|
|
+ // Some age-mode pages still submit a hidden birthday field.
|
|
|
+ // Keep it aligned with generated data so backend validation won't reject.
|
|
|
+ const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
|
|
+ if (hiddenBirthday && hasBirthdayData) {
|
|
|
+ const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
|
+ hiddenBirthday.value = dateStr;
|
|
|
+ hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ log(`Step 5: Hidden birthday input set (age mode): ${dateStr}`);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error('Could not find birthday or age input. URL: ' + location.href);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Click "完成帐户创建" button
|
|
|
+ await sleep(500);
|
|
|
+ const completeBtn = document.querySelector('button[type="submit"]')
|
|
|
+ || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
|
|
|
+
|
|
|
+ // Report complete BEFORE submit (page navigates to add-phone after this)
|
|
|
+ reportComplete(5);
|
|
|
+
|
|
|
+ if (completeBtn) {
|
|
|
+ await humanPause(500, 1300);
|
|
|
+ simulateClick(completeBtn);
|
|
|
+ log('Step 5: Clicked "完成帐户创建"');
|
|
|
+ }
|
|
|
+}
|