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

This commit is contained in:
Jimmy
2026-04-06 12:16:49 +08:00
parent 8b0ffa330f
commit 77cfdc1dd9
5 changed files with 163 additions and 38 deletions
+1 -2
View File
@@ -26,7 +26,7 @@ ChatGPT OAuth 批量注册自动化 Chrome 扩展。
6. **完善资料** — 自动随机生成英文姓名和生日,提交完成注册 6. **完善资料** — 自动随机生成英文姓名和生日,提交完成注册
7. **登录** — 重新打开 OAuth 链接,用刚注册的账号自动登录 7. **登录** — 重新打开 OAuth 链接,用刚注册的账号自动登录
8. **获取登录验证码** — 再次轮询邮箱拿验证码,自动填入 8. **获取登录验证码** — 再次轮询邮箱拿验证码,自动填入
9. **完成 OAuth 授权** — 自动点击授权页「继续」,后台捕获回调地址 9. **完成 OAuth 授权** — 自动点击授权页「继续」;如果页面不接受普通 DOM 点击,会自动切换到更强的点击兜底并继续捕获回调地址
10. **VPS 回调验证** — 自动将回调地址提交到 VPS 面板,完成认证 10. **VPS 回调验证** — 自动将回调地址提交到 VPS 面板,完成认证
> 全程仅需在第 3 步手动粘贴一次邮箱地址,其余全自动。 > 全程仅需在第 3 步手动粘贴一次邮箱地址,其余全自动。
@@ -38,4 +38,3 @@ ChatGPT OAuth 批量注册自动化 Chrome 扩展。
- 每次注册自动生成随机强密码(14位,含大小写+数字+符号) - 每次注册自动生成随机强密码(14位,含大小写+数字+符号)
- 所有状态存储在 `chrome.storage.session`,浏览器关闭即清除 - 所有状态存储在 `chrome.storage.session`,浏览器关闭即清除
- 代码中无硬编码的密码、IP 地址等敏感信息 - 代码中无硬编码的密码、IP 地址等敏感信息
+91 -17
View File
@@ -397,6 +397,58 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
await sleepWithStop(duration); 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() { async function broadcastStopToContentScripts() {
const registry = await getTabRegistry(); const registry = await getTabRegistry();
for (const entry of Object.values(registry)) { 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; let webNavListener = null;
@@ -1129,26 +1181,35 @@ async function executeStep8(state) {
throw new Error('No OAuth URL. Complete step 1 first.'); 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) // Register webNavigation listener (scoped to this step)
return new Promise((resolve, reject) => { 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) { if (webNavListener) {
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
webNavListener = null; 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); }, 120000);
webNavListener = (details) => { webNavListener = (details) => {
if (details.url.startsWith('http://localhost')) { if (details.url.startsWith('http://localhost')) {
console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`); console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); resolved = true;
webNavListener = null; cleanupListener();
clearTimeout(timeout); clearTimeout(timeout);
if (resolveCaptureWait) resolveCaptureWait(details.url);
setState({ localhostUrl: details.url }).then(() => { setState({ localhostUrl: details.url }).then(() => {
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok'); addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
@@ -1163,23 +1224,36 @@ async function executeStep8(state) {
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
// After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex") // 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 () => { (async () => {
try { try {
const signupTabId = await getTabId('signup-page'); let signupTabId = await getTabId('signup-page');
if (signupTabId) { if (signupTabId) {
await chrome.tabs.update(signupTabId, { active: true }); 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 { } else {
await reuseOrCreateTab('signup-page', state.oauthUrl); signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
await addLog('Step 8: Auth tab reopened. Please click "继续" manually to complete OAuth.', 'warn'); 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) { } catch (err) {
clearTimeout(timeout); clearTimeout(timeout);
if (webNavListener) { cleanupListener();
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
webNavListener = null;
}
reject(err); reject(err);
} }
})(); })();
+69 -18
View File
@@ -5,16 +5,23 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
// Listen for commands from Background // Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 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(); resetStopState();
handleCommand(message).then(() => { handleCommand(message).then((result) => {
sendResponse({ ok: true }); sendResponse({ ok: true, ...(result || {}) });
}).catch(err => { }).catch(err => {
if (isStopError(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 }); sendResponse({ stopped: true, error: err.message });
return; return;
} }
if (message.type === 'STEP8_FIND_AND_CLICK') {
log(`Step 8: ${err.message}`, 'error');
sendResponse({ error: err.message });
return;
}
reportError(message.step, err.message); reportError(message.step, err.message);
sendResponse({ error: err.message }); sendResponse({ error: err.message });
}); });
@@ -30,12 +37,14 @@ async function handleCommand(message) {
case 3: return await step3_fillEmailPassword(message.payload); case 3: return await step3_fillEmailPassword(message.payload);
case 5: return await step5_fillNameBirthday(message.payload); case 5: return await step5_fillNameBirthday(message.payload);
case 6: return await step6_login(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}`); default: throw new Error(`signup-page.js does not handle step ${message.step}`);
} }
case 'FILL_CODE': case 'FILL_CODE':
// Step 4 = signup code, Step 7 = login code (same handler) // Step 4 = signup code, Step 7 = login code (same handler)
return await fillVerificationCode(message.step, message.payload); 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: // After login + verification, page shows:
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button. // "使用 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() { async function step8_findAndClick() {
log('Step 8: Looking for OAuth consent "继续" button for manual click...'); log('Step 8: Looking for OAuth consent "继续" button...');
// Wait for the consent page to be ready const continueBtn = await findContinueButton();
// Look for the submit button with text "继续" or data-dd-action-name="Continue" await waitForButtonEnabled(continueBtn);
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 { try {
continueBtn = await waitForElement( return await waitForElement(
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107', 'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
10000 10000
); );
} catch { } catch {
try { try {
continueBtn = await waitForElementByText('button', /继续|Continue/, 5000); return await waitForElementByText('button', /继续|Continue/, 5000);
} catch { } catch {
throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href); throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
} }
} }
}
await humanPause(350, 900); async function waitForButtonEnabled(button, timeout = 8000) {
continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); const start = Date.now();
continueBtn.focus(); while (Date.now() - start < timeout) {
log('Step 8: Found "继续" button and focused it. Please click it manually.'); 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
View File
@@ -7,6 +7,7 @@
"sidePanel", "sidePanel",
"tabs", "tabs",
"webNavigation", "webNavigation",
"debugger",
"storage", "storage",
"scripting", "scripting",
"activeTab" "activeTab"
+1 -1
View File
@@ -137,7 +137,7 @@
</div> </div>
<div class="step-row" data-step="8"> <div class="step-row" data-step="8">
<div class="step-indicator" data-step="8"><span class="step-num">8</span></div> <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> <span class="step-status" data-step="8"></span>
</div> </div>
<div class="step-row" data-step="9"> <div class="step-row" data-step="9">