signup-page.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  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 (
  7. message.type === 'EXECUTE_STEP'
  8. || message.type === 'FILL_CODE'
  9. || message.type === 'STEP8_FIND_AND_CLICK'
  10. || message.type === 'PREPARE_LOGIN_CODE'
  11. || message.type === 'PREPARE_SIGNUP_VERIFICATION'
  12. || message.type === 'RESEND_VERIFICATION_CODE'
  13. ) {
  14. resetStopState();
  15. handleCommand(message).then((result) => {
  16. sendResponse({ ok: true, ...(result || {}) });
  17. }).catch(err => {
  18. if (isStopError(err)) {
  19. log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
  20. sendResponse({ stopped: true, error: err.message });
  21. return;
  22. }
  23. if (message.type === 'STEP8_FIND_AND_CLICK') {
  24. log(`步骤 8:${err.message}`, 'error');
  25. sendResponse({ error: err.message });
  26. return;
  27. }
  28. reportError(message.step, err.message);
  29. sendResponse({ error: err.message });
  30. });
  31. return true;
  32. }
  33. });
  34. async function handleCommand(message) {
  35. switch (message.type) {
  36. case 'EXECUTE_STEP':
  37. switch (message.step) {
  38. case 2: return await step2_clickRegister();
  39. case 3: return await step3_fillEmailPassword(message.payload);
  40. case 5: return await step5_fillNameBirthday(message.payload);
  41. case 6: return await step6_login(message.payload);
  42. case 8: return await step8_findAndClick();
  43. default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`);
  44. }
  45. case 'FILL_CODE':
  46. // Step 4 = signup code, Step 7 = login code (same handler)
  47. return await fillVerificationCode(message.step, message.payload);
  48. case 'PREPARE_SIGNUP_VERIFICATION':
  49. return await prepareSignupVerificationFlow(message.payload);
  50. case 'PREPARE_LOGIN_CODE':
  51. return await prepareLoginCodeFlow();
  52. case 'RESEND_VERIFICATION_CODE':
  53. return await resendVerificationCode(message.step);
  54. case 'STEP8_FIND_AND_CLICK':
  55. return await step8_findAndClick();
  56. }
  57. }
  58. const VERIFICATION_CODE_INPUT_SELECTOR = [
  59. 'input[name="code"]',
  60. 'input[name="otp"]',
  61. 'input[autocomplete="one-time-code"]',
  62. 'input[type="text"][maxlength="6"]',
  63. 'input[type="tel"][maxlength="6"]',
  64. 'input[aria-label*="code" i]',
  65. 'input[placeholder*="code" i]',
  66. 'input[inputmode="numeric"]',
  67. ].join(', ');
  68. const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
  69. const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
  70. function isVisibleElement(el) {
  71. if (!el) return false;
  72. const style = window.getComputedStyle(el);
  73. const rect = el.getBoundingClientRect();
  74. return style.display !== 'none'
  75. && style.visibility !== 'hidden'
  76. && rect.width > 0
  77. && rect.height > 0;
  78. }
  79. function getVerificationCodeTarget() {
  80. const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
  81. if (codeInput && isVisibleElement(codeInput)) {
  82. return { type: 'single', element: codeInput };
  83. }
  84. const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
  85. .filter(isVisibleElement);
  86. if (singleInputs.length >= 6) {
  87. return { type: 'split', elements: singleInputs };
  88. }
  89. return null;
  90. }
  91. function getActionText(el) {
  92. return [
  93. el?.textContent,
  94. el?.value,
  95. el?.getAttribute?.('aria-label'),
  96. el?.getAttribute?.('title'),
  97. ]
  98. .filter(Boolean)
  99. .join(' ')
  100. .replace(/\s+/g, ' ')
  101. .trim();
  102. }
  103. function isActionEnabled(el) {
  104. return Boolean(el)
  105. && !el.disabled
  106. && el.getAttribute('aria-disabled') !== 'true';
  107. }
  108. function findOneTimeCodeLoginTrigger() {
  109. const candidates = document.querySelectorAll(
  110. 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
  111. );
  112. for (const el of candidates) {
  113. if (!isVisibleElement(el)) continue;
  114. if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue;
  115. const text = [
  116. el.textContent,
  117. el.value,
  118. el.getAttribute('aria-label'),
  119. el.getAttribute('title'),
  120. ]
  121. .filter(Boolean)
  122. .join(' ')
  123. .replace(/\s+/g, ' ')
  124. .trim();
  125. if (text && ONE_TIME_CODE_LOGIN_PATTERN.test(text)) {
  126. return el;
  127. }
  128. }
  129. return null;
  130. }
  131. function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
  132. const candidates = document.querySelectorAll(
  133. 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
  134. );
  135. for (const el of candidates) {
  136. if (!isVisibleElement(el)) continue;
  137. if (!allowDisabled && !isActionEnabled(el)) continue;
  138. const text = getActionText(el);
  139. if (text && RESEND_VERIFICATION_CODE_PATTERN.test(text)) {
  140. return el;
  141. }
  142. }
  143. return null;
  144. }
  145. async function prepareLoginCodeFlow(timeout = 15000) {
  146. const readyTarget = getVerificationCodeTarget();
  147. if (readyTarget) {
  148. log('步骤 7:验证码输入框已就绪。');
  149. return { ready: true, mode: readyTarget.type };
  150. }
  151. const start = Date.now();
  152. let switchClickCount = 0;
  153. let lastSwitchAttemptAt = 0;
  154. let loggedPasswordPage = false;
  155. while (Date.now() - start < timeout) {
  156. throwIfStopped();
  157. const target = getVerificationCodeTarget();
  158. if (target) {
  159. log('步骤 7:验证码页面已就绪。');
  160. return { ready: true, mode: target.type };
  161. }
  162. const passwordInput = document.querySelector('input[type="password"]');
  163. const switchTrigger = findOneTimeCodeLoginTrigger();
  164. if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) {
  165. switchClickCount += 1;
  166. lastSwitchAttemptAt = Date.now();
  167. loggedPasswordPage = false;
  168. log('步骤 7:检测到密码页,正在切换到一次性验证码登录...');
  169. await humanPause(350, 900);
  170. simulateClick(switchTrigger);
  171. await sleep(1200);
  172. continue;
  173. }
  174. if (passwordInput && !loggedPasswordPage) {
  175. loggedPasswordPage = true;
  176. log('步骤 7:正在等待密码页上的一次性验证码登录入口...');
  177. }
  178. await sleep(200);
  179. }
  180. throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href);
  181. }
  182. async function resendVerificationCode(step, timeout = 45000) {
  183. if (step === 7) {
  184. await prepareLoginCodeFlow();
  185. }
  186. const start = Date.now();
  187. let action = null;
  188. let loggedWaiting = false;
  189. while (Date.now() - start < timeout) {
  190. throwIfStopped();
  191. action = findResendVerificationCodeTrigger({ allowDisabled: true });
  192. if (action && isActionEnabled(action)) {
  193. log(`步骤 ${step}:重新发送验证码按钮已可用。`);
  194. await humanPause(350, 900);
  195. simulateClick(action);
  196. await sleep(1200);
  197. return {
  198. resent: true,
  199. buttonText: getActionText(action),
  200. };
  201. }
  202. if (action && !loggedWaiting) {
  203. loggedWaiting = true;
  204. log(`步骤 ${step}:正在等待重新发送验证码按钮变为可点击...`);
  205. }
  206. await sleep(250);
  207. }
  208. throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href);
  209. }
  210. // ============================================================
  211. // Step 2: Click Register
  212. // ============================================================
  213. async function step2_clickRegister() {
  214. log('步骤 2:正在查找注册按钮...');
  215. let registerBtn = null;
  216. try {
  217. registerBtn = await waitForElementByText(
  218. 'a, button, [role="button"], [role="link"]',
  219. /sign\s*up|register|create\s*account|注册/i,
  220. 10000
  221. );
  222. } catch {
  223. // Some pages may have a direct link
  224. try {
  225. registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
  226. } catch {
  227. throw new Error(
  228. '未找到注册按钮。' +
  229. '请在 DevTools 中检查认证页面 DOM。URL: ' + location.href
  230. );
  231. }
  232. }
  233. await humanPause(450, 1200);
  234. reportComplete(2);
  235. simulateClick(registerBtn);
  236. log('步骤 2:已点击注册按钮');
  237. }
  238. // ============================================================
  239. // Step 3: Fill Email & Password
  240. // ============================================================
  241. async function step3_fillEmailPassword(payload) {
  242. const { email } = payload;
  243. if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。');
  244. log(`步骤 3:正在填写邮箱:${email}`);
  245. // Find email input
  246. let emailInput = null;
  247. try {
  248. emailInput = await waitForElement(
  249. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
  250. 10000
  251. );
  252. } catch {
  253. throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href);
  254. }
  255. await humanPause(500, 1400);
  256. fillInput(emailInput, email);
  257. log('步骤 3:邮箱已填写');
  258. // Check if password field is on the same page
  259. let passwordInput = document.querySelector('input[type="password"]');
  260. if (!passwordInput) {
  261. // Need to submit email first to get to password page
  262. log('步骤 3:暂未发现密码输入框,先提交邮箱...');
  263. const submitBtn = document.querySelector('button[type="submit"]')
  264. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  265. if (submitBtn) {
  266. await humanPause(400, 1100);
  267. simulateClick(submitBtn);
  268. log('步骤 3:邮箱已提交,正在等待密码输入框...');
  269. await sleep(2000);
  270. }
  271. try {
  272. passwordInput = await waitForElement('input[type="password"]', 10000);
  273. } catch {
  274. throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href);
  275. }
  276. }
  277. if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。');
  278. await humanPause(600, 1500);
  279. fillInput(passwordInput, payload.password);
  280. log('步骤 3:密码已填写');
  281. // Report complete BEFORE submit, because submit causes page navigation
  282. // which kills the content script connection
  283. reportComplete(3, { email });
  284. // Submit the form (page will navigate away after this)
  285. await sleep(500);
  286. const submitBtn = document.querySelector('button[type="submit"]')
  287. || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
  288. if (submitBtn) {
  289. await humanPause(500, 1300);
  290. simulateClick(submitBtn);
  291. log('步骤 3:表单已提交');
  292. }
  293. }
  294. // ============================================================
  295. // Fill Verification Code (used by step 4 and step 7)
  296. // ============================================================
  297. const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|验证码错误|code\s+(?:is\s+)?incorrect|invalid\s+code|incorrect\s+code|try\s+again/i;
  298. const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i;
  299. const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
  300. const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
  301. const SIGNUP_PASSWORD_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
  302. const SIGNUP_PASSWORD_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
  303. function getVerificationErrorText() {
  304. const messages = [];
  305. const selectors = [
  306. '.react-aria-FieldError',
  307. '[slot="errorMessage"]',
  308. '[id$="-error"]',
  309. '[data-invalid="true"] + *',
  310. '[aria-invalid="true"] + *',
  311. '[class*="error"]',
  312. ];
  313. for (const selector of selectors) {
  314. document.querySelectorAll(selector).forEach((el) => {
  315. const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
  316. if (text) {
  317. messages.push(text);
  318. }
  319. });
  320. }
  321. const invalidInput = document.querySelector(`${VERIFICATION_CODE_INPUT_SELECTOR}[aria-invalid="true"], ${VERIFICATION_CODE_INPUT_SELECTOR}[data-invalid="true"]`);
  322. if (invalidInput) {
  323. const wrapper = invalidInput.closest('form, [data-rac], ._root_18qcl_51, div');
  324. if (wrapper) {
  325. const text = (wrapper.textContent || '').replace(/\s+/g, ' ').trim();
  326. if (text) {
  327. messages.push(text);
  328. }
  329. }
  330. }
  331. return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
  332. }
  333. function isStep5Ready() {
  334. return Boolean(
  335. document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
  336. );
  337. }
  338. function getPageTextSnapshot() {
  339. return (document.body?.innerText || document.body?.textContent || '')
  340. .replace(/\s+/g, ' ')
  341. .trim();
  342. }
  343. function getPrimaryContinueButton() {
  344. const continueBtn = document.querySelector(
  345. 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107'
  346. );
  347. if (continueBtn && isVisibleElement(continueBtn)) {
  348. return continueBtn;
  349. }
  350. const buttons = document.querySelectorAll('button, [role="button"]');
  351. return Array.from(buttons).find((el) => isVisibleElement(el) && /继续|Continue/i.test(el.textContent || '')) || null;
  352. }
  353. function isVerificationPageStillVisible() {
  354. if (getVerificationCodeTarget()) return true;
  355. if (findResendVerificationCodeTrigger({ allowDisabled: true })) return true;
  356. if (document.querySelector('form[action*="email-verification" i]')) return true;
  357. return VERIFICATION_PAGE_PATTERN.test(getPageTextSnapshot());
  358. }
  359. function isAddPhonePageReady() {
  360. const path = `${location.pathname || ''} ${location.href || ''}`;
  361. if (/\/add-phone(?:[/?#]|$)/i.test(path)) return true;
  362. const phoneInput = document.querySelector(
  363. 'input[type="tel"]:not([maxlength="6"]), input[name*="phone" i], input[id*="phone" i], input[autocomplete="tel"]'
  364. );
  365. if (phoneInput && isVisibleElement(phoneInput)) {
  366. return true;
  367. }
  368. return ADD_PHONE_PAGE_PATTERN.test(getPageTextSnapshot());
  369. }
  370. function isStep8Ready() {
  371. const continueBtn = getPrimaryContinueButton();
  372. if (!continueBtn) return false;
  373. if (isVerificationPageStillVisible()) return false;
  374. if (isAddPhonePageReady()) return false;
  375. return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
  376. }
  377. function isSignupPasswordPage() {
  378. return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
  379. }
  380. function getSignupPasswordInput() {
  381. const input = document.querySelector('input[type="password"]');
  382. return input && isVisibleElement(input) ? input : null;
  383. }
  384. function getSignupPasswordSubmitButton() {
  385. const direct = document.querySelector('button[type="submit"]');
  386. if (direct && isVisibleElement(direct) && isActionEnabled(direct)) {
  387. return direct;
  388. }
  389. const candidates = document.querySelectorAll('button, [role="button"]');
  390. return Array.from(candidates).find((el) => {
  391. if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
  392. const text = getActionText(el);
  393. return /继续|continue|submit|创建|create/i.test(text);
  394. }) || null;
  395. }
  396. function getSignupRetryButton() {
  397. const direct = document.querySelector('button[data-dd-action-name="Try again"]');
  398. if (direct && isVisibleElement(direct) && isActionEnabled(direct)) {
  399. return direct;
  400. }
  401. const candidates = document.querySelectorAll('button, [role="button"]');
  402. return Array.from(candidates).find((el) => {
  403. if (!isVisibleElement(el) || !isActionEnabled(el)) return false;
  404. const text = getActionText(el);
  405. return /重试|try\s+again/i.test(text);
  406. }) || null;
  407. }
  408. function isSignupPasswordErrorPage() {
  409. if (!isSignupPasswordPage()) return false;
  410. const text = getPageTextSnapshot();
  411. return Boolean(
  412. getSignupRetryButton()
  413. && (SIGNUP_PASSWORD_ERROR_TITLE_PATTERN.test(text)
  414. || SIGNUP_PASSWORD_ERROR_DETAIL_PATTERN.test(text)
  415. || SIGNUP_PASSWORD_ERROR_TITLE_PATTERN.test(document.title || ''))
  416. );
  417. }
  418. async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
  419. const { password } = payload;
  420. const start = Date.now();
  421. let retried = 0;
  422. let lastSubmitAt = 0;
  423. while (Date.now() - start < timeout) {
  424. throwIfStopped();
  425. if (isStep5Ready()) {
  426. log('步骤 4:页面已进入验证码后的下一阶段,本步骤按已完成处理。', 'ok');
  427. return { ready: true, alreadyVerified: true, retried };
  428. }
  429. if (isVerificationPageStillVisible()) {
  430. log(`步骤 4:验证码页面已就绪${retried ? `(期间自动重试 ${retried} 次)` : ''}。`, 'ok');
  431. return { ready: true, retried };
  432. }
  433. if (isSignupPasswordErrorPage()) {
  434. const retryBtn = getSignupRetryButton();
  435. if (!retryBtn) {
  436. throw new Error('检测到密码页超时报错,但未找到可点击的“重试”按钮。URL: ' + location.href);
  437. }
  438. retried += 1;
  439. log(`步骤 4:检测到密码页超时报错,正在点击“重试”(第 ${retried} 次)...`, 'warn');
  440. await humanPause(350, 900);
  441. simulateClick(retryBtn);
  442. await sleep(1500);
  443. continue;
  444. }
  445. const passwordInput = getSignupPasswordInput();
  446. if (passwordInput) {
  447. if (!password) {
  448. throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。');
  449. }
  450. if ((passwordInput.value || '') !== password) {
  451. log('步骤 4:已回到密码页,正在重新填写密码...', 'warn');
  452. await humanPause(450, 1100);
  453. fillInput(passwordInput, password);
  454. }
  455. const submitBtn = getSignupPasswordSubmitButton();
  456. if (!submitBtn) {
  457. throw new Error('密码页存在,但未找到“继续”提交按钮。URL: ' + location.href);
  458. }
  459. if (Date.now() - lastSubmitAt > 1800) {
  460. log('步骤 4:正在重新提交密码,等待验证码页面...', 'warn');
  461. lastSubmitAt = Date.now();
  462. await humanPause(350, 900);
  463. simulateClick(submitBtn);
  464. await sleep(1800);
  465. continue;
  466. }
  467. }
  468. await sleep(200);
  469. }
  470. throw new Error('等待注册验证码页面就绪超时。URL: ' + location.href);
  471. }
  472. async function waitForVerificationSubmitOutcome(step, timeout) {
  473. const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000);
  474. const start = Date.now();
  475. while (Date.now() - start < resolvedTimeout) {
  476. throwIfStopped();
  477. const errorText = getVerificationErrorText();
  478. if (errorText) {
  479. return { invalidCode: true, errorText };
  480. }
  481. if (step === 4 && isStep5Ready()) {
  482. return { success: true };
  483. }
  484. if (step === 7 && isStep8Ready()) {
  485. return { success: true };
  486. }
  487. if (step === 7 && isAddPhonePageReady()) {
  488. return { success: true, addPhonePage: true };
  489. }
  490. await sleep(150);
  491. }
  492. if (isVerificationPageStillVisible()) {
  493. return {
  494. invalidCode: true,
  495. errorText: getVerificationErrorText() || '提交后仍停留在验证码页面,准备重新发送验证码。',
  496. };
  497. }
  498. return { success: true, assumed: true };
  499. }
  500. async function fillVerificationCode(step, payload) {
  501. const { code } = payload;
  502. if (!code) throw new Error('未提供验证码。');
  503. log(`步骤 ${step}:正在填写验证码:${code}`);
  504. if (step === 7) {
  505. await prepareLoginCodeFlow();
  506. }
  507. // Find code input — could be a single input or multiple separate inputs
  508. let codeInput = null;
  509. try {
  510. codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000);
  511. } catch {
  512. // Check for multiple single-digit inputs (common pattern)
  513. const singleInputs = document.querySelectorAll('input[maxlength="1"]');
  514. if (singleInputs.length >= 6) {
  515. log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
  516. for (let i = 0; i < 6 && i < singleInputs.length; i++) {
  517. fillInput(singleInputs[i], code[i]);
  518. await sleep(100);
  519. }
  520. const outcome = await waitForVerificationSubmitOutcome(step);
  521. if (outcome.invalidCode) {
  522. log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
  523. } else if (outcome.addPhonePage) {
  524. log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
  525. } else {
  526. log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
  527. }
  528. return outcome;
  529. }
  530. throw new Error('未找到验证码输入框。URL: ' + location.href);
  531. }
  532. fillInput(codeInput, code);
  533. log(`步骤 ${step}:验证码已填写`);
  534. // Report complete BEFORE submit (page may navigate away)
  535. // Submit
  536. await sleep(500);
  537. const submitBtn = document.querySelector('button[type="submit"]')
  538. || await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
  539. if (submitBtn) {
  540. await humanPause(450, 1200);
  541. simulateClick(submitBtn);
  542. log(`步骤 ${step}:验证码已提交`);
  543. }
  544. const outcome = await waitForVerificationSubmitOutcome(step);
  545. if (outcome.invalidCode) {
  546. log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
  547. } else if (outcome.addPhonePage) {
  548. log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
  549. } else {
  550. log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
  551. }
  552. return outcome;
  553. }
  554. // ============================================================
  555. // Step 6: Login with registered account (on OAuth auth page)
  556. // ============================================================
  557. async function step6_login(payload) {
  558. const { email, password } = payload;
  559. if (!email) throw new Error('登录时缺少邮箱地址。');
  560. log(`步骤 6:正在使用 ${email} 登录...`);
  561. // Wait for email input on the auth page
  562. let emailInput = null;
  563. try {
  564. emailInput = await waitForElement(
  565. 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
  566. 15000
  567. );
  568. } catch {
  569. throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
  570. }
  571. await humanPause(500, 1400);
  572. fillInput(emailInput, email);
  573. log('步骤 6:邮箱已填写');
  574. // Submit email
  575. await sleep(500);
  576. const submitBtn1 = document.querySelector('button[type="submit"]')
  577. || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
  578. if (submitBtn1) {
  579. await humanPause(400, 1100);
  580. simulateClick(submitBtn1);
  581. log('步骤 6:邮箱已提交');
  582. }
  583. await sleep(2000);
  584. // Check for password field
  585. const passwordInput = document.querySelector('input[type="password"]');
  586. if (passwordInput) {
  587. log('步骤 6:已找到密码输入框,正在填写密码...');
  588. await humanPause(550, 1450);
  589. fillInput(passwordInput, password);
  590. await sleep(500);
  591. const submitBtn2 = document.querySelector('button[type="submit"]')
  592. || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
  593. // Report complete BEFORE submit in case page navigates
  594. reportComplete(6, { needsOTP: true });
  595. if (submitBtn2) {
  596. await humanPause(450, 1200);
  597. simulateClick(submitBtn2);
  598. log('步骤 6:密码已提交,可能还需要验证码(步骤 7)');
  599. }
  600. return;
  601. }
  602. // No password field — OTP flow
  603. log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。');
  604. reportComplete(6, { needsOTP: true });
  605. }
  606. // ============================================================
  607. // Step 8: Find "继续" on OAuth consent page for debugger click
  608. // ============================================================
  609. // After login + verification, page shows:
  610. // "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
  611. // Background performs the actual click through the debugger Input API.
  612. async function step8_findAndClick() {
  613. log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...');
  614. const continueBtn = await findContinueButton();
  615. await waitForButtonEnabled(continueBtn);
  616. await humanPause(350, 900);
  617. continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
  618. continueBtn.focus();
  619. await sleep(250);
  620. const rect = getSerializableRect(continueBtn);
  621. log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。');
  622. return {
  623. rect,
  624. buttonText: (continueBtn.textContent || '').trim(),
  625. url: location.href,
  626. };
  627. }
  628. async function findContinueButton() {
  629. const start = Date.now();
  630. while (Date.now() - start < 10000) {
  631. throwIfStopped();
  632. if (isAddPhonePageReady()) {
  633. throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href);
  634. }
  635. const button = getPrimaryContinueButton();
  636. if (button && isStep8Ready()) {
  637. return button;
  638. }
  639. await sleep(150);
  640. }
  641. throw new Error('在 OAuth 同意页未找到“继续”按钮,或页面尚未进入授权同意状态。URL: ' + location.href);
  642. }
  643. async function waitForButtonEnabled(button, timeout = 8000) {
  644. const start = Date.now();
  645. while (Date.now() - start < timeout) {
  646. throwIfStopped();
  647. if (isButtonEnabled(button)) return;
  648. await sleep(150);
  649. }
  650. throw new Error('“继续”按钮长时间不可点击。URL: ' + location.href);
  651. }
  652. function isButtonEnabled(button) {
  653. return Boolean(button)
  654. && !button.disabled
  655. && button.getAttribute('aria-disabled') !== 'true';
  656. }
  657. function getSerializableRect(el) {
  658. const rect = el.getBoundingClientRect();
  659. if (!rect.width || !rect.height) {
  660. throw new Error('滚动后“继续”按钮没有可点击尺寸。URL: ' + location.href);
  661. }
  662. return {
  663. left: rect.left,
  664. top: rect.top,
  665. width: rect.width,
  666. height: rect.height,
  667. centerX: rect.left + (rect.width / 2),
  668. centerY: rect.top + (rect.height / 2),
  669. };
  670. }
  671. // ============================================================
  672. // Step 5: Fill Name & Birthday / Age
  673. // ============================================================
  674. async function step5_fillNameBirthday(payload) {
  675. const { firstName, lastName, age, year, month, day } = payload;
  676. if (!firstName || !lastName) throw new Error('未提供姓名数据。');
  677. const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
  678. const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
  679. if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
  680. throw new Error('未提供生日或年龄数据。');
  681. }
  682. const fullName = `${firstName} ${lastName}`;
  683. log(`步骤 5:正在填写姓名:${fullName}`);
  684. // Actual DOM structure:
  685. // - Full name: <input name="name" placeholder="全名" type="text">
  686. // - Birthday: React Aria DateField or hidden input[name="birthday"]
  687. // - Age: <input name="age" type="text|number">
  688. // --- Full Name (single field, not first+last) ---
  689. let nameInput = null;
  690. try {
  691. nameInput = await waitForElement(
  692. 'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
  693. 10000
  694. );
  695. } catch {
  696. throw new Error('未找到姓名输入框。URL: ' + location.href);
  697. }
  698. await humanPause(500, 1300);
  699. fillInput(nameInput, fullName);
  700. log(`步骤 5:姓名已填写:${fullName}`);
  701. let birthdayMode = false;
  702. let ageInput = null;
  703. for (let i = 0; i < 100; i++) {
  704. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  705. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  706. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  707. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  708. ageInput = document.querySelector('input[name="age"]');
  709. if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday) {
  710. birthdayMode = true;
  711. break;
  712. }
  713. if (ageInput) break;
  714. await sleep(100);
  715. }
  716. if (birthdayMode) {
  717. if (!hasBirthdayData) {
  718. throw new Error('检测到生日字段,但未提供生日数据。');
  719. }
  720. const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
  721. const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
  722. const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
  723. if (yearSpinner && monthSpinner && daySpinner) {
  724. log('步骤 5:检测到生日字段,正在填写生日...');
  725. async function setSpinButton(el, value) {
  726. el.focus();
  727. await sleep(100);
  728. document.execCommand('selectAll', false, null);
  729. await sleep(50);
  730. const valueStr = String(value);
  731. for (const char of valueStr) {
  732. el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
  733. el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
  734. el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
  735. el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
  736. await sleep(50);
  737. }
  738. el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
  739. el.blur();
  740. await sleep(100);
  741. }
  742. await humanPause(450, 1100);
  743. await setSpinButton(yearSpinner, year);
  744. await humanPause(250, 650);
  745. await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
  746. await humanPause(250, 650);
  747. await setSpinButton(daySpinner, String(day).padStart(2, '0'));
  748. log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
  749. }
  750. const hiddenBirthday = document.querySelector('input[name="birthday"]');
  751. if (hiddenBirthday) {
  752. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  753. hiddenBirthday.value = dateStr;
  754. hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
  755. log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
  756. }
  757. } else if (ageInput) {
  758. if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
  759. throw new Error('检测到年龄字段,但未提供年龄数据。');
  760. }
  761. await humanPause(500, 1300);
  762. fillInput(ageInput, String(resolvedAge));
  763. log(`步骤 5:年龄已填写:${resolvedAge}`);
  764. } else {
  765. throw new Error('未找到生日或年龄输入项。URL: ' + location.href);
  766. }
  767. // Click "完成帐户创建" button
  768. await sleep(500);
  769. const completeBtn = document.querySelector('button[type="submit"]')
  770. || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
  771. // Report complete BEFORE submit (page navigates to add-phone after this)
  772. reportComplete(5);
  773. if (completeBtn) {
  774. await humanPause(500, 1300);
  775. simulateClick(completeBtn);
  776. log('步骤 5:已点击“完成帐户创建”');
  777. }
  778. }