signup-page.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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') {
  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. }
  46. }
  47. // ============================================================
  48. // Step 2: Click Register
  49. // ============================================================
  50. async function step2_clickRegister() {
  51. log('Step 2: Looking for Register/Sign up button...');
  52. let registerBtn = null;
  53. try {
  54. registerBtn = await waitForElementByText(
  55. 'a, button, [role="button"], [role="link"]',
  56. /sign\s*up|register|create\s*account|注册/i,
  57. 10000
  58. );
  59. } catch {
  60. // Some pages may have a direct link
  61. try {
  62. registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
  63. } catch {
  64. throw new Error(
  65. 'Could not find Register/Sign up button. ' +
  66. 'Check auth page DOM in DevTools. URL: ' + location.href
  67. );
  68. }
  69. }
  70. await humanPause(450, 1200);
  71. reportComplete(2);
  72. simulateClick(registerBtn);
  73. log('Step 2: Clicked Register button');
  74. }
  75. // ============================================================
  76. // Step 3: Fill Email & Password
  77. // ============================================================
  78. async function step3_fillEmailPassword(payload) {
  79. const { email } = payload;
  80. if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
  81. log(`Step 3: Filling email: ${email}`);
  82. // Find email input
  83. let emailInput = null;
  84. try {
  85. emailInput = await waitForElement(
  86. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
  87. 10000
  88. );
  89. } catch {
  90. throw new Error('Could not find email input field on signup page. URL: ' + location.href);
  91. }
  92. await humanPause(500, 1400);
  93. fillInput(emailInput, email);
  94. log('Step 3: Email filled');
  95. // Check if password field is on the same page
  96. let passwordInput = document.querySelector('input[type="password"]');
  97. if (!passwordInput) {
  98. // Need to submit email first to get to password page
  99. log('Step 3: No password field yet, submitting email first...');
  100. const submitBtn = document.querySelector('button[type="submit"]')
  101. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  102. if (submitBtn) {
  103. await humanPause(400, 1100);
  104. simulateClick(submitBtn);
  105. log('Step 3: Submitted email, waiting for password field...');
  106. await sleep(2000);
  107. }
  108. try {
  109. passwordInput = await waitForElement('input[type="password"]', 10000);
  110. } catch {
  111. throw new Error('Could not find password input after submitting email. URL: ' + location.href);
  112. }
  113. }
  114. if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
  115. await humanPause(600, 1500);
  116. fillInput(passwordInput, payload.password);
  117. log('Step 3: Password filled');
  118. // Report complete BEFORE submit, because submit causes page navigation
  119. // which kills the content script connection
  120. reportComplete(3, { email });
  121. // Submit the form (page will navigate away after this)
  122. await sleep(500);
  123. const submitBtn = document.querySelector('button[type="submit"]')
  124. || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
  125. if (submitBtn) {
  126. await humanPause(500, 1300);
  127. simulateClick(submitBtn);
  128. log('Step 3: Form submitted');
  129. }
  130. }
  131. // ============================================================
  132. // Click "重新发送电子邮件" (used before step 4 and step 7 polling)
  133. // ============================================================
  134. async function clickResendEmail(step) {
  135. log(`Step ${step}: Looking for "重新发送电子邮件" button...`);
  136. let resendBtn = null;
  137. try {
  138. resendBtn = await waitForElementByText(
  139. 'a, button, [role="button"], [role="link"], span',
  140. /重新发送电子邮件|resend\s*email/i,
  141. 10000
  142. );
  143. } catch {
  144. log(`Step ${step}: "重新发送电子邮件" button not found, skipping`, 'warn');
  145. return;
  146. }
  147. // Prevent parent form POST submission (Remix/React Router route without action)
  148. const parentForm = resendBtn.closest('form');
  149. const blockSubmit = (e) => e.preventDefault();
  150. if (parentForm) parentForm.addEventListener('submit', blockSubmit, { once: true });
  151. await humanPause(400, 1000);
  152. resendBtn.click();
  153. log(`Step ${step}: Clicked "重新发送电子邮件"`, 'ok');
  154. await sleep(2000);
  155. if (parentForm) parentForm.removeEventListener('submit', blockSubmit);
  156. }
  157. // ============================================================
  158. // Fill Verification Code (used by step 4 and step 7)
  159. // ============================================================
  160. async function fillVerificationCode(step, payload) {
  161. const { code } = payload;
  162. if (!code) throw new Error('No verification code provided.');
  163. log(`Step ${step}: Filling verification code: ${code}`);
  164. // Find code input — could be a single input or multiple separate inputs
  165. let codeInput = null;
  166. try {
  167. codeInput = await waitForElement(
  168. 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
  169. 10000
  170. );
  171. } catch {
  172. // Check for multiple single-digit inputs (common pattern)
  173. const singleInputs = document.querySelectorAll('input[maxlength="1"]');
  174. if (singleInputs.length >= 6) {
  175. log(`Step ${step}: Found single-digit code inputs, filling individually...`);
  176. for (let i = 0; i < 6 && i < singleInputs.length; i++) {
  177. fillInput(singleInputs[i], code[i]);
  178. await sleep(100);
  179. }
  180. await sleep(1000);
  181. reportComplete(step);
  182. return;
  183. }
  184. throw new Error('Could not find verification code input. URL: ' + location.href);
  185. }
  186. fillInput(codeInput, code);
  187. log(`Step ${step}: Code filled`);
  188. // Report complete BEFORE submit (page may navigate away)
  189. reportComplete(step);
  190. // Submit
  191. await sleep(500);
  192. const submitBtn = document.querySelector('button[type="submit"]')
  193. || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
  194. if (submitBtn) {
  195. await humanPause(450, 1200);
  196. simulateClick(submitBtn);
  197. log(`Step ${step}: Verification submitted`);
  198. }
  199. }
  200. // ============================================================
  201. // Step 6: Login with registered account (on OAuth auth page)
  202. // ============================================================
  203. async function step6_login(payload) {
  204. const { email, password } = payload;
  205. if (!email) throw new Error('No email provided for login.');
  206. log(`Step 6: Logging in with ${email}...`);
  207. // Wait for email input on the auth page
  208. let emailInput = null;
  209. try {
  210. emailInput = await waitForElement(
  211. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
  212. 15000
  213. );
  214. } catch {
  215. throw new Error('Could not find email input on login page. URL: ' + location.href);
  216. }
  217. await humanPause(500, 1400);
  218. fillInput(emailInput, email);
  219. log('Step 6: Email filled');
  220. // Submit email
  221. await sleep(500);
  222. const submitBtn1 = document.querySelector('button[type="submit"]')
  223. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  224. if (submitBtn1) {
  225. await humanPause(400, 1100);
  226. simulateClick(submitBtn1);
  227. log('Step 6: Submitted email');
  228. }
  229. const passwordInput = await waitForLoginPasswordField();
  230. if (passwordInput) {
  231. log('Step 6: Password field found, filling password...');
  232. await humanPause(550, 1450);
  233. fillInput(passwordInput, password);
  234. await sleep(500);
  235. const submitBtn2 = document.querySelector('button[type="submit"]')
  236. || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
  237. // Report complete BEFORE submit in case page navigates
  238. reportComplete(6, { needsOTP: true });
  239. if (submitBtn2) {
  240. await humanPause(450, 1200);
  241. simulateClick(submitBtn2);
  242. log('Step 6: Submitted password, may need verification code (step 7)');
  243. }
  244. return;
  245. }
  246. // No password field — OTP flow
  247. log('Step 6: No password field. OTP flow or auto-redirect.');
  248. reportComplete(6, { needsOTP: true });
  249. }
  250. async function waitForLoginPasswordField(timeout = 25000) {
  251. const start = Date.now();
  252. while (Date.now() - start < timeout) {
  253. throwIfStopped();
  254. const passwordInput = findVisiblePasswordInput();
  255. if (passwordInput) {
  256. return passwordInput;
  257. }
  258. await sleep(250);
  259. }
  260. log(`Step 6: Password field did not appear within ${Math.round(timeout / 1000)}s.`, 'warn');
  261. return null;
  262. }
  263. function findVisiblePasswordInput() {
  264. const inputs = document.querySelectorAll('input[type="password"]');
  265. for (const input of inputs) {
  266. if (isElementVisible(input)) {
  267. return input;
  268. }
  269. }
  270. return null;
  271. }
  272. function isElementVisible(el) {
  273. if (!el) return false;
  274. const style = window.getComputedStyle(el);
  275. if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
  276. return false;
  277. }
  278. const rect = el.getBoundingClientRect();
  279. return rect.width > 0 && rect.height > 0;
  280. }
  281. // ============================================================
  282. // Step 8: Find "继续" on OAuth consent page for debugger click
  283. // ============================================================
  284. // After login + verification, page shows:
  285. // "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
  286. // Background performs the actual click through the debugger Input API.
  287. async function step8_findAndClick() {
  288. log('Step 8: Looking for OAuth consent "继续" button...');
  289. const continueBtn = await findContinueButton();
  290. await waitForButtonEnabled(continueBtn);
  291. await humanPause(350, 900);
  292. continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
  293. continueBtn.focus();
  294. await sleep(250);
  295. const rect = getSerializableRect(continueBtn);
  296. log('Step 8: Found "继续" button and prepared debugger click coordinates.');
  297. return {
  298. rect,
  299. buttonText: (continueBtn.textContent || '').trim(),
  300. url: location.href,
  301. };
  302. }
  303. async function findContinueButton() {
  304. try {
  305. return await waitForElement(
  306. 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
  307. 10000
  308. );
  309. } catch {
  310. try {
  311. return await waitForElementByText('button', /继续|Continue/, 5000);
  312. } catch {
  313. throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
  314. }
  315. }
  316. }
  317. async function waitForButtonEnabled(button, timeout = 8000) {
  318. const start = Date.now();
  319. while (Date.now() - start < timeout) {
  320. throwIfStopped();
  321. if (isButtonEnabled(button)) return;
  322. await sleep(150);
  323. }
  324. throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href);
  325. }
  326. function isButtonEnabled(button) {
  327. return Boolean(button)
  328. && !button.disabled
  329. && button.getAttribute('aria-disabled') !== 'true';
  330. }
  331. function getSerializableRect(el) {
  332. const rect = el.getBoundingClientRect();
  333. if (!rect.width || !rect.height) {
  334. throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href);
  335. }
  336. return {
  337. left: rect.left,
  338. top: rect.top,
  339. width: rect.width,
  340. height: rect.height,
  341. centerX: rect.left + (rect.width / 2),
  342. centerY: rect.top + (rect.height / 2),
  343. };
  344. }
  345. // ============================================================
  346. // Step 5: Fill Name & Birthday / Age
  347. // ============================================================
  348. async function step5_fillNameBirthday(payload) {
  349. const { firstName, lastName, age, year, month, day } = payload;
  350. if (!firstName || !lastName) throw new Error('No name data provided.');
  351. const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
  352. const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
  353. if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
  354. throw new Error('No birthday or age data provided.');
  355. }
  356. const fullName = `${firstName} ${lastName}`;
  357. log(`Step 5: Filling name: ${fullName}`);
  358. // Actual DOM structure:
  359. // - Full name: <input name="name" placeholder="全名" type="text">
  360. // - Birthday: React Aria DateField or hidden input[name="birthday"]
  361. // - Age: <input name="age" type="text|number">
  362. // --- Full Name (single field, not first+last) ---
  363. let nameInput = null;
  364. try {
  365. nameInput = await waitForElement(
  366. 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
  367. 10000
  368. );
  369. } catch {
  370. throw new Error('Could not find name input. URL: ' + location.href);
  371. }
  372. await humanPause(500, 1300);
  373. fillInput(nameInput, fullName);
  374. log(`Step 5: Name filled: ${fullName}`);
  375. let birthdayMode = false;
  376. let ageInput = null;
  377. for (let i = 0; i < 100; i++) {
  378. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  379. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  380. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  381. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  382. ageInput = document.querySelector('input[name="age"]');
  383. // Some pages include a hidden birthday input even though the real UI is "age".
  384. // In that case we must prioritize filling age to satisfy required validation.
  385. if (ageInput) break;
  386. if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday) {
  387. birthdayMode = true;
  388. break;
  389. }
  390. await sleep(100);
  391. }
  392. if (birthdayMode) {
  393. if (!hasBirthdayData) {
  394. throw new Error('Birthday field detected, but no birthday data provided.');
  395. }
  396. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  397. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  398. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  399. if (yearSpinner && monthSpinner && daySpinner) {
  400. log('Step 5: Birthday fields detected, filling birthday...');
  401. async function setSpinButton(el, value) {
  402. el.focus();
  403. await sleep(100);
  404. document.execCommand('selectAll', false, null);
  405. await sleep(50);
  406. const valueStr = String(value);
  407. for (const char of valueStr) {
  408. el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
  409. el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
  410. el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
  411. el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
  412. await sleep(50);
  413. }
  414. el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
  415. el.blur();
  416. await sleep(100);
  417. }
  418. await humanPause(450, 1100);
  419. await setSpinButton(yearSpinner, year);
  420. await humanPause(250, 650);
  421. await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
  422. await humanPause(250, 650);
  423. await setSpinButton(daySpinner, String(day).padStart(2, '0'));
  424. log(`Step 5: Birthday filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
  425. }
  426. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  427. if (hiddenBirthday) {
  428. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  429. hiddenBirthday.value = dateStr;
  430. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  431. log(`Step 5: Hidden birthday input set: ${dateStr}`);
  432. }
  433. } else if (ageInput) {
  434. if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
  435. throw new Error('Age field detected, but no age data provided.');
  436. }
  437. await humanPause(500, 1300);
  438. fillInput(ageInput, String(resolvedAge));
  439. log(`Step 5: Age filled: ${resolvedAge}`);
  440. // Some age-mode pages still submit a hidden birthday field.
  441. // Keep it aligned with generated data so backend validation won't reject.
  442. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  443. if (hiddenBirthday && hasBirthdayData) {
  444. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  445. hiddenBirthday.value = dateStr;
  446. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  447. log(`Step 5: Hidden birthday input set (age mode): ${dateStr}`);
  448. }
  449. } else {
  450. throw new Error('Could not find birthday or age input. URL: ' + location.href);
  451. }
  452. // Click "完成帐户创建" button
  453. await sleep(500);
  454. const completeBtn = document.querySelector('button[type="submit"]')
  455. || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
  456. // Report complete BEFORE submit (page navigates to add-phone after this)
  457. reportComplete(5);
  458. if (completeBtn) {
  459. await humanPause(500, 1300);
  460. simulateClick(completeBtn);
  461. log('Step 5: Clicked "完成帐户创建"');
  462. }
  463. }