Преглед на файлове

feat: enhance OAuth flow with automatic button click and debugger integration

Jimmy преди 4 месеца
родител
ревизия
77cfdc1dd9
променени са 5 файла, в които са добавени 163 реда и са изтрити 38 реда
  1. 1 2
      README.md
  2. 91 17
      background.js
  3. 69 18
      content/signup-page.js
  4. 1 0
      manifest.json
  5. 1 1
      sidepanel/sidepanel.html

+ 1 - 2
README.md

@@ -26,7 +26,7 @@ ChatGPT OAuth 批量注册自动化 Chrome 扩展。
 6. **完善资料** — 自动随机生成英文姓名和生日,提交完成注册
 7. **登录** — 重新打开 OAuth 链接,用刚注册的账号自动登录
 8. **获取登录验证码** — 再次轮询邮箱拿验证码,自动填入
-9. **完成 OAuth 授权** — 自动点击授权页「继续」,后台捕获回调地址
+9. **完成 OAuth 授权** — 自动点击授权页「继续」;如果页面不接受普通 DOM 点击,会自动切换到更强的点击兜底并继续捕获回调地址
 10. **VPS 回调验证** — 自动将回调地址提交到 VPS 面板,完成认证
 
 > 全程仅需在第 3 步手动粘贴一次邮箱地址,其余全自动。
@@ -38,4 +38,3 @@ ChatGPT OAuth 批量注册自动化 Chrome 扩展。
 - 每次注册自动生成随机强密码(14位,含大小写+数字+符号)
 - 所有状态存储在 `chrome.storage.session`,浏览器关闭即清除
 - 代码中无硬编码的密码、IP 地址等敏感信息
-

+ 91 - 17
background.js

@@ -397,6 +397,58 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
   await sleepWithStop(duration);
 }
 
+async function clickWithDebugger(tabId, rect) {
+  if (!tabId) {
+    throw new Error('No auth tab found for debugger click.');
+  }
+  if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
+    throw new Error('Step 8 debugger fallback needs a valid button position.');
+  }
+
+  const target = { tabId };
+  try {
+    await chrome.debugger.attach(target, '1.3');
+  } catch (err) {
+    throw new Error(
+      `Debugger attach failed during step 8 fallback: ${err.message}. ` +
+      'If DevTools is open on the auth tab, close it and retry.'
+    );
+  }
+
+  try {
+    const x = Math.round(rect.centerX);
+    const y = Math.round(rect.centerY);
+
+    await chrome.debugger.sendCommand(target, 'Page.bringToFront');
+    await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
+      type: 'mouseMoved',
+      x,
+      y,
+      button: 'none',
+      buttons: 0,
+      clickCount: 0,
+    });
+    await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
+      type: 'mousePressed',
+      x,
+      y,
+      button: 'left',
+      buttons: 1,
+      clickCount: 1,
+    });
+    await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
+      type: 'mouseReleased',
+      x,
+      y,
+      button: 'left',
+      buttons: 0,
+      clickCount: 1,
+    });
+  } finally {
+    await chrome.debugger.detach(target).catch(() => {});
+  }
+}
+
 async function broadcastStopToContentScripts() {
   const registry = await getTabRegistry();
   for (const entry of Object.values(registry)) {
@@ -1119,7 +1171,7 @@ async function executeStep7(state) {
 }
 
 // ============================================================
-// Step 8: Complete OAuth (manual click + localhost listener)
+// Step 8: Complete OAuth (auto click + localhost listener)
 // ============================================================
 
 let webNavListener = null;
@@ -1129,26 +1181,35 @@ async function executeStep8(state) {
     throw new Error('No OAuth URL. Complete step 1 first.');
   }
 
-  await addLog('Step 8: Setting up localhost redirect listener for manual confirmation...');
+  await addLog('Step 8: Setting up localhost redirect listener...');
 
   // Register webNavigation listener (scoped to this step)
   return new Promise((resolve, reject) => {
-    const timeout = setTimeout(() => {
+    let resolved = false;
+    let resolveCaptureWait = null;
+    const captureWait = new Promise((resolveCapture) => {
+      resolveCaptureWait = resolveCapture;
+    });
+
+    const cleanupListener = () => {
       if (webNavListener) {
         chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
         webNavListener = null;
       }
-      setStepStatus(8, 'failed');
-      addLog('Step 8: Localhost redirect not captured after 120s. Please confirm you clicked "继续" on the OAuth page.', 'error');
-      reject(new Error('Localhost redirect not captured after 120s. Please click "继续" on the OAuth page.'));
+    };
+
+    const timeout = setTimeout(() => {
+      cleanupListener();
+      reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
     }, 120000);
 
     webNavListener = (details) => {
       if (details.url.startsWith('http://localhost')) {
         console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
-        chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
-        webNavListener = null;
+        resolved = true;
+        cleanupListener();
         clearTimeout(timeout);
+        if (resolveCaptureWait) resolveCaptureWait(details.url);
 
         setState({ localhostUrl: details.url }).then(() => {
           addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
@@ -1163,23 +1224,36 @@ async function executeStep8(state) {
     chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
 
     // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
-    // with a "继续" button. The user must click it manually.
+    // with a "继续" button. We locate the button in-page, then click it through
+    // the debugger Input API directly.
     (async () => {
       try {
-        const signupTabId = await getTabId('signup-page');
+        let signupTabId = await getTabId('signup-page');
         if (signupTabId) {
           await chrome.tabs.update(signupTabId, { active: true });
-          await addLog('Step 8: Switched to auth page. Please click "继续" manually to complete OAuth.', 'warn');
+          await addLog('Step 8: Switched to auth page. Preparing debugger click...');
         } else {
-          await reuseOrCreateTab('signup-page', state.oauthUrl);
-          await addLog('Step 8: Auth tab reopened. Please click "继续" manually to complete OAuth.', 'warn');
+          signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
+          await addLog('Step 8: Auth tab reopened. Preparing debugger click...');
+        }
+
+        const clickResult = await sendToContentScript('signup-page', {
+          type: 'STEP8_FIND_AND_CLICK',
+          source: 'background',
+          payload: {},
+        });
+
+        if (clickResult?.error) {
+          throw new Error(clickResult.error);
+        }
+
+        if (!resolved) {
+          await clickWithDebugger(signupTabId, clickResult?.rect);
+          await addLog('Step 8: Debugger click dispatched, waiting for redirect...');
         }
       } catch (err) {
         clearTimeout(timeout);
-        if (webNavListener) {
-          chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
-          webNavListener = null;
-        }
+        cleanupListener();
         reject(err);
       }
     })();

+ 69 - 18
content/signup-page.js

@@ -5,16 +5,23 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
 
 // Listen for commands from Background
 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE') {
+  if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK') {
     resetStopState();
-    handleCommand(message).then(() => {
-      sendResponse({ ok: true });
+    handleCommand(message).then((result) => {
+      sendResponse({ ok: true, ...(result || {}) });
     }).catch(err => {
       if (isStopError(err)) {
-        log(`Step ${message.step}: Stopped by user.`, 'warn');
+        log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
         sendResponse({ stopped: true, error: err.message });
         return;
       }
+
+      if (message.type === 'STEP8_FIND_AND_CLICK') {
+        log(`Step 8: ${err.message}`, 'error');
+        sendResponse({ error: err.message });
+        return;
+      }
+
       reportError(message.step, err.message);
       sendResponse({ error: err.message });
     });
@@ -30,12 +37,14 @@ async function handleCommand(message) {
         case 3: return await step3_fillEmailPassword(message.payload);
         case 5: return await step5_fillNameBirthday(message.payload);
         case 6: return await step6_login(message.payload);
-        case 8: return await step8_clickContinue();
+        case 8: return await step8_findAndClick();
         default: throw new Error(`signup-page.js does not handle step ${message.step}`);
       }
     case 'FILL_CODE':
       // Step 4 = signup code, Step 7 = login code (same handler)
       return await fillVerificationCode(message.step, message.payload);
+    case 'STEP8_FIND_AND_CLICK':
+      return await step8_findAndClick();
   }
 }
 
@@ -255,35 +264,77 @@ async function step6_login(payload) {
 }
 
 // ============================================================
-// Step 8: Focus "继续" on OAuth consent page for manual click
+// Step 8: Find "继续" on OAuth consent page for debugger click
 // ============================================================
 // After login + verification, page shows:
 // "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
-// We only locate and focus it so the user can click manually.
+// Background performs the actual click through the debugger Input API.
+
+async function step8_findAndClick() {
+  log('Step 8: Looking for OAuth consent "继续" button...');
 
-async function step8_clickContinue() {
-  log('Step 8: Looking for OAuth consent "继续" button for manual click...');
+  const continueBtn = await findContinueButton();
+  await waitForButtonEnabled(continueBtn);
 
-  // Wait for the consent page to be ready
-  // Look for the submit button with text "继续" or data-dd-action-name="Continue"
-  let continueBtn = null;
+  await humanPause(350, 900);
+  continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
+  continueBtn.focus();
+  await sleep(250);
+
+  const rect = getSerializableRect(continueBtn);
+  log('Step 8: Found "继续" button and prepared debugger click coordinates.');
+  return {
+    rect,
+    buttonText: (continueBtn.textContent || '').trim(),
+    url: location.href,
+  };
+}
+
+async function findContinueButton() {
   try {
-    continueBtn = await waitForElement(
+    return await waitForElement(
       'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
       10000
     );
   } catch {
     try {
-      continueBtn = await waitForElementByText('button', /继续|Continue/, 5000);
+      return await waitForElementByText('button', /继续|Continue/, 5000);
     } catch {
       throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
     }
   }
+}
 
-  await humanPause(350, 900);
-  continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
-  continueBtn.focus();
-  log('Step 8: Found "继续" button and focused it. Please click it manually.');
+async function waitForButtonEnabled(button, timeout = 8000) {
+  const start = Date.now();
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+    if (isButtonEnabled(button)) return;
+    await sleep(150);
+  }
+  throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href);
+}
+
+function isButtonEnabled(button) {
+  return Boolean(button)
+    && !button.disabled
+    && button.getAttribute('aria-disabled') !== 'true';
+}
+
+function getSerializableRect(el) {
+  const rect = el.getBoundingClientRect();
+  if (!rect.width || !rect.height) {
+    throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href);
+  }
+
+  return {
+    left: rect.left,
+    top: rect.top,
+    width: rect.width,
+    height: rect.height,
+    centerX: rect.left + (rect.width / 2),
+    centerY: rect.top + (rect.height / 2),
+  };
 }
 
 // ============================================================

+ 1 - 0
manifest.json

@@ -7,6 +7,7 @@
     "sidePanel",
     "tabs",
     "webNavigation",
+    "debugger",
     "storage",
     "scripting",
     "activeTab"

+ 1 - 1
sidepanel/sidepanel.html

@@ -137,7 +137,7 @@
       </div>
       <div class="step-row" data-step="8">
         <div class="step-indicator" data-step="8"><span class="step-num">8</span></div>
-        <button class="step-btn" data-step="8">Manual OAuth Confirm</button>
+        <button class="step-btn" data-step="8">OAuth Auto Confirm</button>
         <span class="step-status" data-step="8"></span>
       </div>
       <div class="step-row" data-step="9">