signup-page.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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') {
  7. handleCommand(message).then(() => {
  8. sendResponse({ ok: true });
  9. }).catch(err => {
  10. reportError(message.step, err.message);
  11. sendResponse({ error: err.message });
  12. });
  13. return true;
  14. }
  15. });
  16. async function handleCommand(message) {
  17. switch (message.type) {
  18. case 'EXECUTE_STEP':
  19. switch (message.step) {
  20. case 2: return await step2_clickRegister();
  21. case 3: return await step3_fillEmailPassword(message.payload);
  22. case 5: return await step5_fillNameBirthday(message.payload);
  23. case 6: return await step6_login(message.payload);
  24. case 8: return await step8_clickContinue();
  25. default: throw new Error(`signup-page.js does not handle step ${message.step}`);
  26. }
  27. case 'FILL_CODE':
  28. // Step 4 = signup code, Step 7 = login code (same handler)
  29. return await fillVerificationCode(message.step, message.payload);
  30. }
  31. }
  32. // ============================================================
  33. // Step 2: Click Register
  34. // ============================================================
  35. async function step2_clickRegister() {
  36. log('Step 2: Looking for Register/Sign up button...');
  37. // TODO: Adjust selectors based on actual OpenAI auth page
  38. let registerBtn = null;
  39. try {
  40. registerBtn = await waitForElementByText(
  41. 'a, button, [role="button"], [role="link"]',
  42. /sign\s*up|register|create\s*account|注册/i,
  43. 10000
  44. );
  45. } catch {
  46. // Some pages may have a direct link
  47. try {
  48. registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
  49. } catch {
  50. throw new Error(
  51. 'Could not find Register/Sign up button. ' +
  52. 'Check auth page DOM in DevTools. URL: ' + location.href
  53. );
  54. }
  55. }
  56. reportComplete(2);
  57. simulateClick(registerBtn);
  58. log('Step 2: Clicked Register button');
  59. }
  60. // ============================================================
  61. // Step 3: Fill Email & Password
  62. // ============================================================
  63. async function step3_fillEmailPassword(payload) {
  64. const { email } = payload;
  65. if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
  66. log(`Step 3: Filling email: ${email}`);
  67. // Find email input
  68. let emailInput = null;
  69. try {
  70. emailInput = await waitForElement(
  71. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
  72. 10000
  73. );
  74. } catch {
  75. throw new Error('Could not find email input field on signup page. URL: ' + location.href);
  76. }
  77. fillInput(emailInput, email);
  78. log('Step 3: Email filled');
  79. // Check if password field is on the same page
  80. let passwordInput = document.querySelector('input[type="password"]');
  81. if (!passwordInput) {
  82. // Need to submit email first to get to password page
  83. log('Step 3: No password field yet, submitting email first...');
  84. const submitBtn = document.querySelector('button[type="submit"]')
  85. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  86. if (submitBtn) {
  87. simulateClick(submitBtn);
  88. log('Step 3: Submitted email, waiting for password field...');
  89. await sleep(2000);
  90. }
  91. try {
  92. passwordInput = await waitForElement('input[type="password"]', 10000);
  93. } catch {
  94. throw new Error('Could not find password input after submitting email. URL: ' + location.href);
  95. }
  96. }
  97. if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
  98. fillInput(passwordInput, payload.password);
  99. log('Step 3: Password filled');
  100. // Report complete BEFORE submit, because submit causes page navigation
  101. // which kills the content script connection
  102. reportComplete(3, { email });
  103. // Submit the form (page will navigate away after this)
  104. await sleep(500);
  105. const submitBtn = document.querySelector('button[type="submit"]')
  106. || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
  107. if (submitBtn) {
  108. simulateClick(submitBtn);
  109. log('Step 3: Form submitted');
  110. }
  111. }
  112. // ============================================================
  113. // Fill Verification Code (used by step 4 and step 7)
  114. // ============================================================
  115. async function fillVerificationCode(step, payload) {
  116. const { code } = payload;
  117. if (!code) throw new Error('No verification code provided.');
  118. log(`Step ${step}: Filling verification code: ${code}`);
  119. // Find code input — could be a single input or multiple separate inputs
  120. let codeInput = null;
  121. try {
  122. codeInput = await waitForElement(
  123. 'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
  124. 10000
  125. );
  126. } catch {
  127. // Check for multiple single-digit inputs (common pattern)
  128. const singleInputs = document.querySelectorAll('input[maxlength="1"]');
  129. if (singleInputs.length >= 6) {
  130. log(`Step ${step}: Found single-digit code inputs, filling individually...`);
  131. for (let i = 0; i < 6 && i < singleInputs.length; i++) {
  132. fillInput(singleInputs[i], code[i]);
  133. await sleep(100);
  134. }
  135. await sleep(1000);
  136. reportComplete(step);
  137. return;
  138. }
  139. throw new Error('Could not find verification code input. URL: ' + location.href);
  140. }
  141. fillInput(codeInput, code);
  142. log(`Step ${step}: Code filled`);
  143. // Report complete BEFORE submit (page may navigate away)
  144. reportComplete(step);
  145. // Submit
  146. await sleep(500);
  147. const submitBtn = document.querySelector('button[type="submit"]')
  148. || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
  149. if (submitBtn) {
  150. simulateClick(submitBtn);
  151. log(`Step ${step}: Verification submitted`);
  152. }
  153. }
  154. // ============================================================
  155. // Step 6: Login with registered account (on OAuth auth page)
  156. // ============================================================
  157. async function step6_login(payload) {
  158. const { email, password } = payload;
  159. if (!email) throw new Error('No email provided for login.');
  160. log(`Step 6: Logging in with ${email}...`);
  161. // Wait for email input on the auth page
  162. let emailInput = null;
  163. try {
  164. emailInput = await waitForElement(
  165. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
  166. 15000
  167. );
  168. } catch {
  169. throw new Error('Could not find email input on login page. URL: ' + location.href);
  170. }
  171. fillInput(emailInput, email);
  172. log('Step 6: Email filled');
  173. // Submit email
  174. await sleep(500);
  175. const submitBtn1 = document.querySelector('button[type="submit"]')
  176. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  177. if (submitBtn1) {
  178. simulateClick(submitBtn1);
  179. log('Step 6: Submitted email');
  180. }
  181. await sleep(2000);
  182. // Check for password field
  183. const passwordInput = document.querySelector('input[type="password"]');
  184. if (passwordInput) {
  185. log('Step 6: Password field found, filling password...');
  186. fillInput(passwordInput, password);
  187. await sleep(500);
  188. const submitBtn2 = document.querySelector('button[type="submit"]')
  189. || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
  190. // Report complete BEFORE submit in case page navigates
  191. reportComplete(6, { needsOTP: true });
  192. if (submitBtn2) {
  193. simulateClick(submitBtn2);
  194. log('Step 6: Submitted password, may need verification code (step 7)');
  195. }
  196. return;
  197. }
  198. // No password field — OTP flow
  199. log('Step 6: No password field. OTP flow or auto-redirect.');
  200. reportComplete(6, { needsOTP: true });
  201. }
  202. // ============================================================
  203. // Step 8: Click "继续" on OAuth consent page
  204. // ============================================================
  205. // After login + verification, page shows:
  206. // "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
  207. // Clicking it triggers redirect to localhost URL.
  208. async function step8_clickContinue() {
  209. log('Step 8: Looking for OAuth consent "继续" button...');
  210. // Wait for the consent page to be ready
  211. // Look for the submit button with text "继续" or data-dd-action-name="Continue"
  212. let continueBtn = null;
  213. try {
  214. continueBtn = await waitForElement(
  215. 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
  216. 10000
  217. );
  218. } catch {
  219. try {
  220. continueBtn = await waitForElementByText('button', /继续|Continue/, 5000);
  221. } catch {
  222. throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
  223. }
  224. }
  225. log('Step 8: Found "继续" button, clicking...');
  226. // Use native .click() — simulateClick (dispatchEvent) may not trigger form submit
  227. continueBtn.click();
  228. log('Step 8: Clicked via .click()');
  229. // Also try submitting the form directly as a fallback
  230. await sleep(500);
  231. const form = continueBtn.closest('form');
  232. if (form) {
  233. form.requestSubmit(continueBtn);
  234. log('Step 8: Also triggered form.requestSubmit()');
  235. }
  236. log('Step 8: Redirecting to localhost... (background will capture URL)');
  237. // Don't reportComplete — background handles it via webNavigation listener
  238. }
  239. // ============================================================
  240. // Step 5: Fill Name & Birthday
  241. // ============================================================
  242. async function step5_fillNameBirthday(payload) {
  243. const { firstName, lastName, year, month, day } = payload;
  244. if (!firstName || !lastName) throw new Error('No name data provided.');
  245. const fullName = `${firstName} ${lastName}`;
  246. log(`Step 5: Filling name: ${fullName}, Birthday: ${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`);
  247. // Actual DOM structure:
  248. // - Full name: <input name="name" placeholder="全名" type="text">
  249. // - Birthday: React Aria DateField with 3 spinbutton divs (year/month/day)
  250. // + <input type="hidden" name="birthday" value="2026-04-05">
  251. // --- Full Name (single field, not first+last) ---
  252. let nameInput = null;
  253. try {
  254. nameInput = await waitForElement(
  255. 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
  256. 10000
  257. );
  258. } catch {
  259. throw new Error('Could not find name input. URL: ' + location.href);
  260. }
  261. fillInput(nameInput, fullName);
  262. log(`Step 5: Name filled: ${fullName}`);
  263. // --- Birthday (React Aria DateField with spinbutton segments) ---
  264. // The date field has three contenteditable divs with role="spinbutton"
  265. // and data-type="year", data-type="month", data-type="day"
  266. // There's also a hidden input[name="birthday"] that stores the actual value
  267. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  268. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  269. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  270. if (yearSpinner && monthSpinner && daySpinner) {
  271. log('Step 5: Found React Aria DateField spinbuttons');
  272. // Helper to set a spinbutton value via focus + keyboard input
  273. async function setSpinButton(el, value) {
  274. el.focus();
  275. await sleep(100);
  276. // Select all existing text
  277. document.execCommand('selectAll', false, null);
  278. await sleep(50);
  279. // Type the new value digit by digit
  280. const valueStr = String(value);
  281. for (const char of valueStr) {
  282. el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
  283. el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
  284. // Also use InputEvent for React Aria
  285. el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
  286. el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
  287. await sleep(50);
  288. }
  289. el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
  290. el.blur();
  291. await sleep(100);
  292. }
  293. await setSpinButton(yearSpinner, year);
  294. log(`Step 5: Year set: ${year}`);
  295. await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
  296. log(`Step 5: Month set: ${month}`);
  297. await setSpinButton(daySpinner, String(day).padStart(2, '0'));
  298. log(`Step 5: Day set: ${day}`);
  299. // Also update the hidden input directly as a safety measure
  300. const hiddenBirthday = document.querySelector('input[type="hidden"][name="birthday"]');
  301. if (hiddenBirthday) {
  302. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  303. hiddenBirthday.value = dateStr;
  304. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  305. log(`Step 5: Hidden birthday input set: ${dateStr}`);
  306. }
  307. } else {
  308. // Fallback: try setting hidden input directly
  309. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  310. if (hiddenBirthday) {
  311. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  312. hiddenBirthday.value = dateStr;
  313. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  314. log(`Step 5: Birthday set via hidden input: ${dateStr}`);
  315. } else {
  316. log('Step 5: WARNING - Could not find birthday fields. May need to adjust selectors.', 'warn');
  317. }
  318. }
  319. // Click "完成帐户创建" button
  320. await sleep(500);
  321. const completeBtn = document.querySelector('button[type="submit"]')
  322. || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
  323. // Report complete BEFORE submit (page navigates to add-phone after this)
  324. reportComplete(5);
  325. if (completeBtn) {
  326. simulateClick(completeBtn);
  327. log('Step 5: Clicked "完成帐户创建"');
  328. }
  329. }