signup-page.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. // content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5)
  2. // Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
  3. console.log('[MultiPage:signup-page] Content script loaded on', location.href);
  4. // Listen for commands from Background
  5. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  6. 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') {
  7. resetStopState();
  8. handleCommand(message).then((result) => {
  9. sendResponse({ ok: true, ...(result || {}) });
  10. }).catch(err => {
  11. if (isStopError(err)) {
  12. log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
  13. sendResponse({ stopped: true, error: err.message });
  14. return;
  15. }
  16. if (message.type === 'STEP8_FIND_AND_CLICK') {
  17. log(`Step 8: ${err.message}`, 'error');
  18. sendResponse({ error: err.message });
  19. return;
  20. }
  21. reportError(message.step, err.message);
  22. sendResponse({ error: err.message });
  23. });
  24. return true;
  25. }
  26. });
  27. async function handleCommand(message) {
  28. switch (message.type) {
  29. case 'EXECUTE_STEP':
  30. switch (message.step) {
  31. case 2: return await step2_clickRegister();
  32. case 3: return await step3_fillEmailPassword(message.payload);
  33. case 5: return await step5_fillNameBirthday(message.payload);
  34. case 6: return await step6_login(message.payload);
  35. case 8: return await step8_findAndClick();
  36. default: throw new Error(`signup-page.js does not handle step ${message.step}`);
  37. }
  38. case 'FILL_CODE':
  39. // Step 4 = signup code, Step 7 = login code (same handler)
  40. return await fillVerificationCode(message.step, message.payload);
  41. case 'CLICK_RESEND_EMAIL':
  42. return await clickResendEmail(message.step);
  43. case 'STEP8_FIND_AND_CLICK':
  44. return await step8_findAndClick();
  45. case 'HANDLE_ABOUT_YOU':
  46. return await handleAboutYouPage(message.payload);
  47. }
  48. }
  49. // ============================================================
  50. // Step 2: Click Register
  51. // ============================================================
  52. async function step2_clickRegister() {
  53. log('Step 2: Looking for Register/Sign up button...');
  54. let registerBtn = null;
  55. try {
  56. registerBtn = await waitForElementByText(
  57. 'a, button, [role="button"], [role="link"]',
  58. /sign\s*up|register|create\s*account|注册/i,
  59. 10000
  60. );
  61. } catch {
  62. // Some pages may have a direct link
  63. try {
  64. registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
  65. } catch {
  66. throw new Error(
  67. 'Could not find Register/Sign up button. ' +
  68. 'Check auth page DOM in DevTools. URL: ' + location.href
  69. );
  70. }
  71. }
  72. await humanPause(450, 1200);
  73. reportComplete(2);
  74. simulateClick(registerBtn);
  75. log('Step 2: Clicked Register button');
  76. }
  77. // ============================================================
  78. // Step 3: Fill Email & Password
  79. // ============================================================
  80. async function step3_fillEmailPassword(payload) {
  81. const { email } = payload;
  82. if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
  83. log(`Step 3: Filling email: ${email}`);
  84. // Find email input
  85. let emailInput = null;
  86. try {
  87. emailInput = await waitForElement(
  88. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
  89. 10000
  90. );
  91. } catch {
  92. throw new Error('Could not find email input field on signup page. URL: ' + location.href);
  93. }
  94. await humanPause(500, 1400);
  95. fillInput(emailInput, email);
  96. log('Step 3: Email filled');
  97. // Check if password field is on the same page
  98. let passwordInput = document.querySelector('input[type="password"]');
  99. if (!passwordInput) {
  100. // Need to submit email first to get to password page
  101. log('Step 3: No password field yet, submitting email first...');
  102. const submitBtn = document.querySelector('button[type="submit"]')
  103. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  104. if (submitBtn) {
  105. await humanPause(400, 1100);
  106. simulateClick(submitBtn);
  107. log('Step 3: Submitted email, waiting for password field...');
  108. await sleep(2000);
  109. }
  110. try {
  111. passwordInput = await waitForElement('input[type="password"]', 10000);
  112. } catch {
  113. throw new Error('Could not find password input after submitting email. URL: ' + location.href);
  114. }
  115. }
  116. if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
  117. await humanPause(600, 1500);
  118. fillInput(passwordInput, payload.password);
  119. log('Step 3: Password filled');
  120. // Report complete BEFORE submit, because submit causes page navigation
  121. // which kills the content script connection
  122. reportComplete(3, { email });
  123. // Submit the form (page will navigate away after this)
  124. await sleep(500);
  125. const submitBtn = document.querySelector('button[type="submit"]')
  126. || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
  127. if (submitBtn) {
  128. await humanPause(500, 1300);
  129. simulateClick(submitBtn);
  130. log('Step 3: Form submitted');
  131. }
  132. }
  133. // ============================================================
  134. // Click "重新发送电子邮件" (used before step 4 and step 7 polling)
  135. // ============================================================
  136. async function clickResendEmail(step) {
  137. log(`Step ${step}: Looking for "重新发送电子邮件" button...`);
  138. let resendBtn = null;
  139. try {
  140. resendBtn = await waitForElementByText(
  141. 'a, button, [role="button"], [role="link"], span',
  142. /重新发送电子邮件|resend\s*email/i,
  143. 10000
  144. );
  145. } catch {
  146. log(`Step ${step}: "重新发送电子邮件" button not found, skipping`, 'warn');
  147. return;
  148. }
  149. // Prevent parent form POST submission (Remix/React Router route without action)
  150. const parentForm = resendBtn.closest('form');
  151. const blockSubmit = (e) => e.preventDefault();
  152. if (parentForm) parentForm.addEventListener('submit', blockSubmit, { once: true });
  153. await humanPause(400, 1000);
  154. resendBtn.click();
  155. log(`Step ${step}: Clicked "重新发送电子邮件"`, 'ok');
  156. await sleep(2000);
  157. if (parentForm) parentForm.removeEventListener('submit', blockSubmit);
  158. }
  159. // ============================================================
  160. // Fill Verification Code (used by step 4 and step 7)
  161. // ============================================================
  162. async function fillVerificationCode(step, payload) {
  163. const { code } = payload;
  164. if (!code) throw new Error('No verification code provided.');
  165. log(`Step ${step}: Filling verification code: ${code}`);
  166. // Find code input — could be a single input or multiple separate inputs
  167. let codeInput = null;
  168. try {
  169. codeInput = await waitForElement(
  170. 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
  171. 10000
  172. );
  173. } catch {
  174. // Check for multiple single-digit inputs (common pattern)
  175. const singleInputs = document.querySelectorAll('input[maxlength="1"]');
  176. if (singleInputs.length >= 6) {
  177. log(`Step ${step}: Found single-digit code inputs, filling individually...`);
  178. for (let i = 0; i < 6 && i < singleInputs.length; i++) {
  179. fillInput(singleInputs[i], code[i]);
  180. await sleep(100);
  181. }
  182. await sleep(1000);
  183. // Verify page navigated away from verification page
  184. await verifyCodeAccepted(step);
  185. reportComplete(step);
  186. return;
  187. }
  188. throw new Error('Could not find verification code input. URL: ' + location.href);
  189. }
  190. fillInput(codeInput, code);
  191. log(`Step ${step}: Code filled`);
  192. // Submit
  193. await sleep(500);
  194. const submitBtn = document.querySelector('button[type="submit"]')
  195. || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证|继续/i, 5000).catch(() => null);
  196. if (submitBtn) {
  197. await humanPause(450, 1200);
  198. simulateClick(submitBtn);
  199. log(`Step ${step}: Verification submitted`);
  200. }
  201. // Wait and verify the page actually moved past the verification page
  202. await verifyCodeAccepted(step);
  203. reportComplete(step);
  204. }
  205. async function verifyCodeAccepted(step, timeout = 8000) {
  206. const start = Date.now();
  207. const verificationPaths = ['/email-verification', '/verify', '/otp'];
  208. while (Date.now() - start < timeout) {
  209. throwIfStopped();
  210. const currentPath = location.pathname;
  211. const stillOnVerification = verificationPaths.some(p => currentPath.includes(p));
  212. if (!stillOnVerification) {
  213. log(`Step ${step}: Verification code accepted, page navigated to ${currentPath}`);
  214. return;
  215. }
  216. // Check for error messages on the page (wrong code)
  217. const errorEl = document.querySelector('[class*="error"], [class*="Error"], [role="alert"]');
  218. if (errorEl) {
  219. const errorText = (errorEl.textContent || '').trim();
  220. if (errorText && errorText.length < 200) {
  221. throw new Error(`Verification code rejected: ${errorText}. URL: ${location.href}`);
  222. }
  223. }
  224. await sleep(500);
  225. }
  226. // Still on verification page after timeout — code was likely wrong
  227. throw new Error(`Verification code ${step === 4 ? 'signup' : 'login'} was not accepted (page did not navigate). URL: ${location.href}`);
  228. }
  229. // ============================================================
  230. // Step 6: Login with registered account (on OAuth auth page)
  231. // ============================================================
  232. async function step6_login(payload) {
  233. const { email, password } = payload;
  234. if (!email) throw new Error('No email provided for login.');
  235. log(`Step 6: Logging in with ${email}...`);
  236. // Wait for email input on the auth page
  237. let emailInput = null;
  238. try {
  239. emailInput = await waitForElement(
  240. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
  241. 15000
  242. );
  243. } catch {
  244. throw new Error('Could not find email input on login page. URL: ' + location.href);
  245. }
  246. await humanPause(500, 1400);
  247. fillInput(emailInput, email);
  248. log('Step 6: Email filled');
  249. // Submit email
  250. await sleep(500);
  251. const submitBtn1 = document.querySelector('button[type="submit"]')
  252. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  253. if (submitBtn1) {
  254. await humanPause(400, 1100);
  255. simulateClick(submitBtn1);
  256. log('Step 6: Submitted email');
  257. }
  258. const passwordInput = await waitForLoginPasswordField();
  259. if (passwordInput) {
  260. log('Step 6: Password field found, filling password...');
  261. await humanPause(550, 1450);
  262. fillInput(passwordInput, password);
  263. await sleep(500);
  264. const submitBtn2 = document.querySelector('button[type="submit"]')
  265. || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
  266. // Report complete BEFORE submit in case page navigates
  267. reportComplete(6, { needsOTP: true });
  268. if (submitBtn2) {
  269. await humanPause(450, 1200);
  270. simulateClick(submitBtn2);
  271. log('Step 6: Submitted password, may need verification code (step 7)');
  272. }
  273. return;
  274. }
  275. // No password field — OTP flow
  276. log('Step 6: No password field. OTP flow or auto-redirect.');
  277. reportComplete(6, { needsOTP: true });
  278. }
  279. async function waitForLoginPasswordField(timeout = 25000) {
  280. const start = Date.now();
  281. while (Date.now() - start < timeout) {
  282. throwIfStopped();
  283. const passwordInput = findVisiblePasswordInput();
  284. if (passwordInput) {
  285. return passwordInput;
  286. }
  287. await sleep(250);
  288. }
  289. log(`Step 6: Password field did not appear within ${Math.round(timeout / 1000)}s.`, 'warn');
  290. return null;
  291. }
  292. function findVisiblePasswordInput() {
  293. const inputs = document.querySelectorAll('input[type="password"]');
  294. for (const input of inputs) {
  295. if (isElementVisible(input)) {
  296. return input;
  297. }
  298. }
  299. return null;
  300. }
  301. function isElementVisible(el) {
  302. if (!el) return false;
  303. const style = window.getComputedStyle(el);
  304. if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
  305. return false;
  306. }
  307. const rect = el.getBoundingClientRect();
  308. return rect.width > 0 && rect.height > 0;
  309. }
  310. // ============================================================
  311. // Step 8: Find "继续" on OAuth consent page for debugger click
  312. // ============================================================
  313. // After login + verification, page shows:
  314. // "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
  315. // Background performs the actual click through the debugger Input API.
  316. async function step8_findAndClick() {
  317. log('Step 8: Looking for OAuth consent "继续" button...');
  318. const continueBtn = await findContinueButton();
  319. await waitForButtonEnabled(continueBtn);
  320. await humanPause(350, 900);
  321. continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
  322. continueBtn.focus();
  323. await sleep(250);
  324. // Click directly from content script — no debugger needed
  325. simulateClick(continueBtn);
  326. log('Step 8: Clicked "继续" button directly.', 'ok');
  327. return {
  328. clicked: true,
  329. buttonText: (continueBtn.textContent || '').trim(),
  330. url: location.href,
  331. };
  332. }
  333. async function findContinueButton() {
  334. try {
  335. return await waitForElement(
  336. 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
  337. 10000
  338. );
  339. } catch {
  340. try {
  341. return await waitForElementByText('button', /继续|Continue/, 5000);
  342. } catch {
  343. throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
  344. }
  345. }
  346. }
  347. async function waitForButtonEnabled(button, timeout = 8000) {
  348. const start = Date.now();
  349. while (Date.now() - start < timeout) {
  350. throwIfStopped();
  351. if (isButtonEnabled(button)) return;
  352. await sleep(150);
  353. }
  354. throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href);
  355. }
  356. function isButtonEnabled(button) {
  357. return Boolean(button)
  358. && !button.disabled
  359. && button.getAttribute('aria-disabled') !== 'true';
  360. }
  361. function getSerializableRect(el) {
  362. const rect = el.getBoundingClientRect();
  363. if (!rect.width || !rect.height) {
  364. throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href);
  365. }
  366. return {
  367. left: rect.left,
  368. top: rect.top,
  369. width: rect.width,
  370. height: rect.height,
  371. centerX: rect.left + (rect.width / 2),
  372. centerY: rect.top + (rect.height / 2),
  373. };
  374. }
  375. // ============================================================
  376. // Handle /about-you page (appears after login if birthday was missing)
  377. // ============================================================
  378. async function handleAboutYouPage(payload) {
  379. if (!location.href.includes('/about-you')) {
  380. return { handled: false };
  381. }
  382. log('Detected /about-you page, filling birthday info...');
  383. const { year, month, day, fullName } = payload || {};
  384. if (!year || !month || !day) {
  385. throw new Error('No birthday data available for about-you page.');
  386. }
  387. // Wait for the page to fully load
  388. await sleep(1000);
  389. // Fill name if present and empty
  390. const nameInput = document.querySelector('input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]');
  391. if (nameInput && !nameInput.value && fullName) {
  392. fillInput(nameInput, fullName);
  393. log('About-you: Name filled');
  394. await humanPause(300, 800);
  395. }
  396. // Fill birthday using the shared helper
  397. await fillBirthdayFields(year, month, day, 'About-you');
  398. // Click continue/submit button
  399. await sleep(500);
  400. const submitBtn = document.querySelector('button[type="submit"]')
  401. || await waitForElementByText('button', /continue|继续|完成|done|agree|submit/i, 5000).catch(() => null);
  402. if (submitBtn) {
  403. await humanPause(500, 1300);
  404. simulateClick(submitBtn);
  405. log('About-you: Submitted form');
  406. }
  407. return { handled: true };
  408. }
  409. // ============================================================
  410. // Shared birthday filling helper (used by Step 5 and about-you)
  411. // ============================================================
  412. async function fillBirthdayFields(year, month, day, logPrefix) {
  413. const prefix = logPrefix || 'Birthday';
  414. // Strategy 1: React Aria DateField spinbuttons
  415. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  416. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  417. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  418. if (yearSpinner && monthSpinner && daySpinner) {
  419. log(`${prefix}: Filling spinbutton date fields...`);
  420. async function setSpinButton(el, value) {
  421. el.focus();
  422. await sleep(100);
  423. document.execCommand('selectAll', false, null);
  424. await sleep(50);
  425. const valueStr = String(value);
  426. for (const char of valueStr) {
  427. el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
  428. el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
  429. el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
  430. el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
  431. await sleep(50);
  432. }
  433. el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
  434. el.blur();
  435. await sleep(100);
  436. }
  437. await humanPause(450, 1100);
  438. await setSpinButton(yearSpinner, year);
  439. await humanPause(250, 650);
  440. await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
  441. await humanPause(250, 650);
  442. await setSpinButton(daySpinner, String(day).padStart(2, '0'));
  443. log(`${prefix}: Spinbutton date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
  444. return true;
  445. }
  446. // Strategy 2: Select dropdowns (month/day/year)
  447. const selects = document.querySelectorAll('select');
  448. if (selects.length >= 2) {
  449. let monthSelect = null, daySelect = null, yearSelect = null;
  450. for (const sel of selects) {
  451. const name = (sel.name || sel.id || sel.getAttribute('aria-label') || '').toLowerCase();
  452. const opts = Array.from(sel.options).map(o => o.value);
  453. if (name.includes('month') || name.includes('mm')) {
  454. monthSelect = sel;
  455. } else if (name.includes('day') || name.includes('dd')) {
  456. daySelect = sel;
  457. } else if (name.includes('year') || name.includes('yyyy')) {
  458. yearSelect = sel;
  459. } else {
  460. // Heuristic: identify by option count and values
  461. const numericOpts = opts.filter(v => /^\d+$/.test(v));
  462. if (!monthSelect && numericOpts.length >= 12 && numericOpts.length <= 13) {
  463. monthSelect = sel;
  464. } else if (!daySelect && numericOpts.length >= 28 && numericOpts.length <= 32) {
  465. daySelect = sel;
  466. } else if (!yearSelect && numericOpts.some(v => Number(v) > 1900 && Number(v) < 2100)) {
  467. yearSelect = sel;
  468. }
  469. }
  470. }
  471. if (monthSelect || daySelect || yearSelect) {
  472. log(`${prefix}: Filling select dropdown date fields...`);
  473. if (monthSelect) {
  474. setSelectValue(monthSelect, String(month));
  475. await humanPause(200, 500);
  476. }
  477. if (daySelect) {
  478. setSelectValue(daySelect, String(day));
  479. await humanPause(200, 500);
  480. }
  481. if (yearSelect) {
  482. setSelectValue(yearSelect, String(year));
  483. await humanPause(200, 500);
  484. }
  485. log(`${prefix}: Select date filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
  486. return true;
  487. }
  488. }
  489. // Strategy 3: input[type="date"]
  490. const dateInput = document.querySelector('input[type="date"]');
  491. if (dateInput) {
  492. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  493. const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
  494. nativeSetter.call(dateInput, dateStr);
  495. dateInput.dispatchEvent(new Event('input', { bubbles: true }));
  496. dateInput.dispatchEvent(new Event('change', { bubbles: true }));
  497. log(`${prefix}: Date input filled: ${dateStr}`);
  498. return true;
  499. }
  500. // Strategy 4: Hidden input[name="birthday"] (fallback)
  501. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  502. if (hiddenBirthday) {
  503. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  504. hiddenBirthday.value = dateStr;
  505. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  506. log(`${prefix}: Hidden birthday input set: ${dateStr}`);
  507. return true;
  508. }
  509. return false;
  510. }
  511. function setSelectValue(selectEl, value) {
  512. // Try exact match first, then try padded value
  513. const candidates = [value, value.padStart(2, '0')];
  514. for (const v of candidates) {
  515. const option = Array.from(selectEl.options).find(o => o.value === v || o.textContent.trim() === v);
  516. if (option) {
  517. selectEl.value = option.value;
  518. selectEl.dispatchEvent(new Event('change', { bubbles: true }));
  519. selectEl.dispatchEvent(new Event('input', { bubbles: true }));
  520. return;
  521. }
  522. }
  523. // Last resort: set by index if value is numeric
  524. const numVal = Number(value);
  525. if (!isNaN(numVal) && numVal > 0 && numVal < selectEl.options.length) {
  526. selectEl.selectedIndex = numVal;
  527. selectEl.dispatchEvent(new Event('change', { bubbles: true }));
  528. selectEl.dispatchEvent(new Event('input', { bubbles: true }));
  529. }
  530. }
  531. // ============================================================
  532. // Step 5: Fill Name & Birthday / Age
  533. // ============================================================
  534. async function step5_fillNameBirthday(payload) {
  535. const { firstName, lastName, age, year, month, day } = payload;
  536. if (!firstName || !lastName) throw new Error('No name data provided.');
  537. const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
  538. const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
  539. if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
  540. throw new Error('No birthday or age data provided.');
  541. }
  542. const fullName = `${firstName} ${lastName}`;
  543. log(`Step 5: Filling name: ${fullName}`);
  544. // Actual DOM structure:
  545. // - Full name: <input name="name" placeholder="全名" type="text">
  546. // - Birthday: React Aria DateField or hidden input[name="birthday"]
  547. // - Age: <input name="age" type="text|number">
  548. // --- Full Name (single field, not first+last) ---
  549. let nameInput = null;
  550. try {
  551. nameInput = await waitForElement(
  552. 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
  553. 10000
  554. );
  555. } catch {
  556. throw new Error('Could not find name input. URL: ' + location.href);
  557. }
  558. await humanPause(500, 1300);
  559. fillInput(nameInput, fullName);
  560. log(`Step 5: Name filled: ${fullName}`);
  561. // Detect birthday/age input type with polling
  562. let ageInput = null;
  563. let hasBirthdayUI = false;
  564. for (let i = 0; i < 100; i++) {
  565. ageInput = document.querySelector('input[name="age"]');
  566. // Some pages include a hidden birthday input even though the real UI is "age".
  567. // In that case we must prioritize filling age to satisfy required validation.
  568. if (ageInput) break;
  569. // Check for any supported birthday UI (spinbuttons, selects, date input, hidden)
  570. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  571. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  572. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  573. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  574. const dateInput = document.querySelector('input[type="date"]');
  575. const selects = document.querySelectorAll('select');
  576. if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday || dateInput || selects.length >= 2) {
  577. hasBirthdayUI = true;
  578. break;
  579. }
  580. await sleep(100);
  581. }
  582. if (hasBirthdayUI) {
  583. if (!hasBirthdayData) {
  584. throw new Error('Birthday field detected, but no birthday data provided.');
  585. }
  586. // Use shared helper that tries spinbuttons → selects → date input → hidden input
  587. const filled = await fillBirthdayFields(year, month, day, 'Step 5');
  588. if (!filled) {
  589. log('Step 5: Warning - could not fill any visible birthday control', 'warn');
  590. }
  591. } else if (ageInput) {
  592. if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
  593. throw new Error('Age field detected, but no age data provided.');
  594. }
  595. await humanPause(500, 1300);
  596. fillInput(ageInput, String(resolvedAge));
  597. log(`Step 5: Age filled: ${resolvedAge}`);
  598. // Some age-mode pages still submit a hidden birthday field.
  599. // Keep it aligned with generated data so backend validation won't reject.
  600. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  601. if (hiddenBirthday && hasBirthdayData) {
  602. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  603. hiddenBirthday.value = dateStr;
  604. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  605. log(`Step 5: Hidden birthday input set (age mode): ${dateStr}`);
  606. }
  607. } else {
  608. throw new Error('Could not find birthday or age input. URL: ' + location.href);
  609. }
  610. // Click "完成帐户创建" button
  611. await sleep(500);
  612. const completeBtn = document.querySelector('button[type="submit"]')
  613. || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
  614. // Report complete BEFORE submit (page navigates to add-phone after this)
  615. reportComplete(5);
  616. if (completeBtn) {
  617. await humanPause(500, 1300);
  618. simulateClick(completeBtn);
  619. log('Step 5: Clicked "完成帐户创建"');
  620. }
  621. }