diff --git a/README.md b/README.md index 806531c..b10eea5 100644 --- a/README.md +++ b/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 地址等敏感信息 - diff --git a/background.js b/background.js index 690796d..e3cbce3 100644 --- a/background.js +++ b/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); } })(); diff --git a/content/signup-page.js b/content/signup-page.js index c48cc11..12a75aa 100644 --- a/content/signup-page.js +++ b/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_clickContinue() { - log('Step 8: Looking for OAuth consent "继续" button for manual click...'); +async function step8_findAndClick() { + log('Step 8: Looking for OAuth consent "继续" button...'); - // Wait for the consent page to be ready - // Look for the submit button with text "继续" or data-dd-action-name="Continue" - let continueBtn = null; + const continueBtn = await findContinueButton(); + await waitForButtonEnabled(continueBtn); + + 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), + }; } // ============================================================ diff --git a/manifest.json b/manifest.json index 235ceb2..31efbd7 100644 --- a/manifest.json +++ b/manifest.json @@ -7,6 +7,7 @@ "sidePanel", "tabs", "webNavigation", + "debugger", "storage", "scripting", "activeTab" diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 4d73394..2bd827a 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -137,7 +137,7 @@