Просмотр исходного кода

feat: 增强步骤 5 的错误处理,添加生日选择功能并优化提交结果检测

QLHazyCoder 4 месяцев назад
Родитель
Сommit
fb69e587c1
1 измененных файлов с 156 добавлено и 6 удалено
  1. 156 6
      content/signup-page.js

+ 156 - 6
content/signup-page.js

@@ -356,6 +356,7 @@ const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|
 const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i;
 const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
 const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
+const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|unable\s+to\s+create\s+(?:your\s+)?account|couldn'?t\s+create\s+(?:your\s+)?account|something\s+went\s+wrong|invalid\s+(?:birthday|birth|date)|生日|出生日期/i;
 const SIGNUP_PASSWORD_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
 const SIGNUP_PASSWORD_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
 
@@ -448,6 +449,118 @@ function isStep8Ready() {
   return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
 }
 
+function normalizeInlineText(text) {
+  return (text || '').replace(/\s+/g, ' ').trim();
+}
+
+function findBirthdayReactAriaSelect(labelText) {
+  const normalizedLabel = normalizeInlineText(labelText);
+  const roots = document.querySelectorAll('.react-aria-Select');
+
+  for (const root of roots) {
+    const labelEl = Array.from(root.querySelectorAll('span')).find((el) => normalizeInlineText(el.textContent) === normalizedLabel);
+    if (!labelEl) continue;
+
+    const item = root.closest('[class*="selectItem"], ._selectItem_ppsls_113') || root.parentElement;
+    const nativeSelect = item?.querySelector('[data-testid="hidden-select-container"] select') || null;
+    const button = root.querySelector('button[aria-haspopup="listbox"]') || null;
+    const valueEl = root.querySelector('.react-aria-SelectValue') || null;
+
+    return { root, item, labelEl, nativeSelect, button, valueEl };
+  }
+
+  return null;
+}
+
+async function setReactAriaBirthdaySelect(control, value) {
+  if (!control?.nativeSelect) {
+    throw new Error('未找到可写入的生日下拉框。');
+  }
+
+  const desiredValue = String(value);
+  const option = Array.from(control.nativeSelect.options).find((item) => item.value === desiredValue);
+  if (!option) {
+    throw new Error(`生日下拉框中不存在值 ${desiredValue}。`);
+  }
+
+  control.nativeSelect.value = desiredValue;
+  option.selected = true;
+  control.nativeSelect.dispatchEvent(new Event('input', { bubbles: true }));
+  control.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
+  await sleep(120);
+}
+
+function getStep5ErrorText() {
+  const messages = [];
+  const selectors = [
+    '.react-aria-FieldError',
+    '[slot="errorMessage"]',
+    '[id$="-error"]',
+    '[id$="-errors"]',
+    '[role="alert"]',
+    '[aria-live="assertive"]',
+    '[aria-live="polite"]',
+    '[class*="error"]',
+  ];
+
+  for (const selector of selectors) {
+    document.querySelectorAll(selector).forEach((el) => {
+      if (!isVisibleElement(el)) return;
+      const text = normalizeInlineText(el.textContent);
+      if (text) {
+        messages.push(text);
+      }
+    });
+  }
+
+  const invalidField = Array.from(document.querySelectorAll('[aria-invalid="true"], [data-invalid="true"]'))
+    .find((el) => isVisibleElement(el));
+  if (invalidField) {
+    const wrapper = invalidField.closest('form, fieldset, [data-rac], div');
+    if (wrapper) {
+      const text = normalizeInlineText(wrapper.textContent);
+      if (text) {
+        messages.push(text);
+      }
+    }
+  }
+
+  return messages.find((text) => STEP5_SUBMIT_ERROR_PATTERN.test(text)) || '';
+}
+
+async function waitForStep5SubmitOutcome(timeout = 15000) {
+  const start = Date.now();
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+
+    const errorText = getStep5ErrorText();
+    if (errorText) {
+      return { invalidProfile: true, errorText };
+    }
+
+    if (isAddPhonePageReady()) {
+      return { success: true, addPhonePage: true };
+    }
+
+    if (isStep8Ready()) {
+      return { success: true };
+    }
+
+    await sleep(150);
+  }
+
+  const errorText = getStep5ErrorText();
+  if (errorText) {
+    return { invalidProfile: true, errorText };
+  }
+
+  return {
+    invalidProfile: true,
+    errorText: '提交后未进入下一阶段,请检查生日是否真正被页面接受。',
+  };
+}
+
 function isSignupPasswordPage() {
   return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
 }
@@ -864,6 +977,36 @@ async function step5_fillNameBirthday(payload) {
     const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
     const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
     const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
+    const yearReactSelect = findBirthdayReactAriaSelect('年');
+    const monthReactSelect = findBirthdayReactAriaSelect('月');
+    const dayReactSelect = findBirthdayReactAriaSelect('天');
+
+    if (yearReactSelect?.nativeSelect && monthReactSelect?.nativeSelect && dayReactSelect?.nativeSelect) {
+      const desiredDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
+      const hiddenBirthday = document.querySelector('input[name="birthday"]');
+
+      log('步骤 5:检测到 React Aria 下拉生日字段,正在填写生日...');
+      await humanPause(450, 1100);
+      await setReactAriaBirthdaySelect(yearReactSelect, year);
+      await humanPause(250, 650);
+      await setReactAriaBirthdaySelect(monthReactSelect, month);
+      await humanPause(250, 650);
+      await setReactAriaBirthdaySelect(dayReactSelect, day);
+
+      if (hiddenBirthday) {
+        const start = Date.now();
+        while (Date.now() - start < 2000) {
+          if ((hiddenBirthday.value || '') === desiredDate) break;
+          await sleep(100);
+        }
+
+        if ((hiddenBirthday.value || '') !== desiredDate) {
+          throw new Error(`生日值未成功写入页面。期望 ${desiredDate},实际 ${(hiddenBirthday.value || '空')}。`);
+        }
+      }
+
+      log(`步骤 5:React Aria 生日已填写:${desiredDate}`);
+    }
 
     if (yearSpinner && monthSpinner && daySpinner) {
       log('步骤 5:检测到生日字段,正在填写生日...');
@@ -901,6 +1044,7 @@ async function step5_fillNameBirthday(payload) {
     if (hiddenBirthday) {
       const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
       hiddenBirthday.value = dateStr;
+      hiddenBirthday.dispatchEvent(new Event('input', { bubbles: true }));
       hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
       log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
     }
@@ -919,13 +1063,19 @@ async function step5_fillNameBirthday(payload) {
   await sleep(500);
   const completeBtn = document.querySelector('button[type="submit"]')
     || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
+  if (!completeBtn) {
+    throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href);
+  }
 
-  // Report complete BEFORE submit (page navigates to add-phone after this)
-  reportComplete(5);
+  await humanPause(500, 1300);
+  simulateClick(completeBtn);
+  log('步骤 5:已点击“完成帐户创建”,正在等待页面结果...');
 
-  if (completeBtn) {
-    await humanPause(500, 1300);
-    simulateClick(completeBtn);
-    log('步骤 5:已点击“完成帐户创建”');
+  const outcome = await waitForStep5SubmitOutcome();
+  if (outcome.invalidProfile) {
+    throw new Error(`步骤 5:${outcome.errorText}`);
   }
+
+  log(`步骤 5:资料已通过。`, 'ok');
+  reportComplete(5, { addPhonePage: Boolean(outcome.addPhonePage) });
 }