diff --git a/background.js b/background.js index 0cc354d..f07be54 100644 --- a/background.js +++ b/background.js @@ -23,7 +23,7 @@ const DEFAULT_STATE = { flowStartTime: null, tabRegistry: {}, logs: [], - vpsUrl: 'http://154.26.182.181:8317/management.html#/oauth', + vpsUrl: '', mailProvider: '163', // 'qq' or '163' }; @@ -39,14 +39,16 @@ async function setState(updates) { async function resetState() { console.log(LOG_PREFIX, 'Resetting all state'); - // Preserve seenCodes, accounts, and tabRegistry across resets - const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry']); + // Preserve settings and persistent data across resets + const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry', 'vpsUrl', 'mailProvider']); await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, seenCodes: prev.seenCodes || [], accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, + vpsUrl: prev.vpsUrl || '', + mailProvider: prev.mailProvider || '163', }); } @@ -149,25 +151,39 @@ async function reuseOrCreateTab(source, url, options = {}) { const alive = await isTabAlive(source); if (alive) { const tabId = await getTabId(source); - // Navigate existing tab to new URL (or just activate if same URL) + + // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly + const registry = await getTabRegistry(); + if (registry[source]) registry[source].ready = false; + await setState({ tabRegistry: registry }); + + // Navigate existing tab to new URL await chrome.tabs.update(tabId, { url, active: true }); console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`); - // Wait for page load - await new Promise(resolve => { + // Wait for page load complete (with 30s timeout) + await new Promise((resolve) => { + const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000); const listener = (tid, info) => { if (tid === tabId && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); + clearTimeout(timer); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); }); - // Mark as not ready — content script will re-inject and send READY - const registry = await getTabRegistry(); - if (registry[source]) registry[source].ready = false; - await setState({ tabRegistry: registry }); + // If dynamic injection needed (VPS panel), re-inject after navigation + if (options.inject) { + await chrome.scripting.executeScript({ + target: { tabId }, + files: options.inject, + }); + } + + // Wait a bit for content script to inject and send READY + await new Promise(r => setTimeout(r, 500)); return tabId; } @@ -178,10 +194,12 @@ async function reuseOrCreateTab(source, url, options = {}) { // If dynamic injection needed (VPS panel), inject scripts after load if (options.inject) { - await new Promise(resolve => { + await new Promise((resolve) => { + const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000); const listener = (tabId, info) => { if (tabId === tab.id && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); + clearTimeout(timer); resolve(); } }; @@ -550,8 +568,13 @@ async function autoRunLoop(totalRuns) { } } - await addLog(`=== All ${autoRunTotalRuns} runs finished ===`, 'ok'); - chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns } }).catch(() => {}); + const completedRuns = autoRunCurrentRun; + if (completedRuns >= autoRunTotalRuns) { + await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok'); + } else { + await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn'); + } + chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {}); autoRunActive = false; await setState({ autoRunning: false }); } @@ -895,13 +918,41 @@ async function executeStep9(state) { } await addLog('Step 9: Opening VPS panel...'); - await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'] }); - await sendToContentScript('vps-panel', { + let tabId = await getTabId('vps-panel'); + const alive = tabId && await isTabAlive('vps-panel'); + + if (!alive) { + // Create new tab + const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true }); + tabId = tab.id; + await new Promise(resolve => { + const listener = (tid, info) => { + if (tid === tabId && info.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + }); + } else { + await chrome.tabs.update(tabId, { active: true }); + } + + // Inject scripts directly and wait for them to be ready + await chrome.scripting.executeScript({ + target: { tabId }, + files: ['content/utils.js', 'content/vps-panel.js'], + }); + await new Promise(r => setTimeout(r, 1000)); + + // Send command directly — bypass queue/ready mechanism + await addLog(`Step 9: Filling callback URL...`); + await chrome.tabs.sendMessage(tabId, { type: 'EXECUTE_STEP', step: 9, source: 'background', - payload: {}, + payload: { localhostUrl: state.localhostUrl }, }); } diff --git a/content/chatgpt.js b/content/chatgpt.js deleted file mode 100644 index 584a446..0000000 --- a/content/chatgpt.js +++ /dev/null @@ -1,211 +0,0 @@ -// content/chatgpt.js — Content script for ChatGPT (steps 6, 7-receive, 8) -// Injected on: chatgpt.com - -console.log('[MultiPage:chatgpt] 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') { - handleCommand(message).then(() => { - sendResponse({ ok: true }); - }).catch(err => { - reportError(message.step, err.message); - sendResponse({ error: err.message }); - }); - return true; - } -}); - -async function handleCommand(message) { - switch (message.type) { - case 'EXECUTE_STEP': - switch (message.step) { - case 6: return await step6_loginChatGPT(); - case 8: return await step8_navigateOAuth(); - default: throw new Error(`chatgpt.js does not handle step ${message.step}`); - } - case 'FILL_CODE': - return await step7_fillLoginCode(message.payload); - } -} - -// ============================================================ -// Step 6: Login ChatGPT -// ============================================================ - -async function step6_loginChatGPT() { - log('Step 6: Looking for login button on ChatGPT...'); - - // Get state for email and password - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - const email = state.email; - const password = state.password; - - if (!email) throw new Error('No email found in state. Complete earlier steps first.'); - - // Find login button on ChatGPT homepage - let loginBtn = null; - try { - loginBtn = await waitForElementByText( - 'a, button, [role="button"], [role="link"]', - /log\s*in|sign\s*in|登录/i, - 10000 - ); - } catch { - try { - loginBtn = await waitForElement( - '[data-testid="login-button"], a[href*="auth"], a[href*="login"]', - 5000 - ); - } catch { - throw new Error('Could not find Login button on ChatGPT. URL: ' + location.href); - } - } - - simulateClick(loginBtn); - log('Step 6: Clicked Login button, waiting for auth page...'); - await sleep(3000); - - // We may be redirected to an auth page (auth0.openai.com etc.) - // Or the login form may appear on the same page - // Try to find email input - let emailInput = null; - try { - emailInput = await waitForElement( - 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i]', - 15000 - ); - } catch { - throw new Error('Could not find email input on login page. URL: ' + location.href); - } - - fillInput(emailInput, email); - log(`Step 6: Filled email: ${email}`); - - // Submit email - await sleep(500); - const submitBtn1 = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|next|submit|继续/i, 5000).catch(() => null); - if (submitBtn1) { - simulateClick(submitBtn1); - log('Step 6: Submitted email'); - } - - await sleep(2000); - - // Check: password field or OTP? - const passwordInput = document.querySelector('input[type="password"]'); - if (passwordInput) { - log('Step 6: Password field found, filling password...'); - fillInput(passwordInput, password); - - await sleep(500); - const submitBtn2 = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录/i, 5000).catch(() => null); - if (submitBtn2) { - simulateClick(submitBtn2); - log('Step 6: Submitted password'); - } - - await sleep(3000); - - // Check if we need OTP after password - const codeInput = document.querySelector( - 'input[name="code"], input[maxlength="6"], input[inputmode="numeric"], input[type="text"][maxlength="6"]' - ); - if (codeInput) { - log('Step 6: OTP code input found after password. Waiting for step 7...'); - reportComplete(6, { needsOTP: true }); - } else { - // Check if we're actually logged in (redirected to chatgpt.com main page) - log('Step 6: Login appears successful (no OTP needed)'); - reportComplete(6, { needsOTP: false }); - } - } else { - // No password field — check for OTP input or "check your email" message - const codeInput = document.querySelector( - 'input[name="code"], input[maxlength="6"], input[inputmode="numeric"]' - ); - if (codeInput) { - log('Step 6: OTP flow detected (no password). Waiting for step 7...'); - reportComplete(6, { needsOTP: true }); - } else { - // Maybe the page is still loading or transitioning - log('Step 6: No password or OTP field found. May need email verification. Waiting for step 7...'); - reportComplete(6, { needsOTP: true }); - } - } -} - -// ============================================================ -// Step 7 (receiving end): Fill Login Verification Code -// ============================================================ - -async function step7_fillLoginCode(payload) { - const { code } = payload; - if (!code) throw new Error('No verification code provided.'); - - log(`Step 7: Filling login verification code: ${code}`); - - // Find code input — single input or multiple single-digit inputs - let codeInput = null; - try { - codeInput = await waitForElement( - 'input[name="code"], input[name="otp"], input[maxlength="6"], input[type="text"][inputmode="numeric"], input[aria-label*="code" i], input[placeholder*="code" i]', - 10000 - ); - } catch { - // Check for multiple single-digit inputs - const singleInputs = document.querySelectorAll('input[maxlength="1"]'); - if (singleInputs.length >= 6) { - log('Step 7: Found single-digit code inputs, filling individually...'); - for (let i = 0; i < 6 && i < singleInputs.length; i++) { - fillInput(singleInputs[i], code[i]); - await sleep(100); - } - await sleep(1000); - - // Auto-submit may happen; if not, try clicking submit - const submitBtn = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|verify|submit|confirm/i, 3000).catch(() => null); - if (submitBtn) simulateClick(submitBtn); - - await sleep(3000); - reportComplete(7); - return; - } - - throw new Error('Could not find verification code input on ChatGPT login. URL: ' + location.href); - } - - fillInput(codeInput, code); - - await sleep(500); - const submitBtn = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|verify|submit|confirm|确认/i, 5000).catch(() => null); - if (submitBtn) { - simulateClick(submitBtn); - log('Step 7: Login verification code submitted'); - } - - await sleep(3000); - reportComplete(7); -} - -// ============================================================ -// Step 8: Navigate to OAuth URL -// ============================================================ - -async function step8_navigateOAuth() { - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - const oauthUrl = state.oauthUrl; - if (!oauthUrl) throw new Error('No OAuth URL found. Complete step 1 first.'); - - log(`Step 8: Navigating to OAuth URL: ${oauthUrl.slice(0, 80)}...`); - log('Step 8: Waiting for localhost redirect (captured by background)...'); - - // Navigate — the webNavigation listener in background will capture localhost redirect - window.location.href = oauthUrl; - - // Don't reportComplete here — background handles it via webNavigation -} diff --git a/content/signup-page.js b/content/signup-page.js index c7db04f..2a214c4 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -40,7 +40,6 @@ async function handleCommand(message) { async function step2_clickRegister() { log('Step 2: Looking for Register/Sign up button...'); - // TODO: Adjust selectors based on actual OpenAI auth page let registerBtn = null; try { registerBtn = await waitForElementByText( diff --git a/content/vps-panel.js b/content/vps-panel.js index 4855a05..f885889 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -1,5 +1,5 @@ // content/vps-panel.js — Content script for VPS panel (steps 1, 9) -// Injected on: http://154.26.182.181:8317/* +// Injected on: VPS panel (user-configured URL) // // Actual DOM structure (after login click): //
@@ -41,7 +41,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { async function handleStep(step, payload) { switch (step) { case 1: return await step1_getOAuthLink(); - case 9: return await step9_vpsVerify(); + case 9: return await step9_vpsVerify(payload); default: throw new Error(`vps-panel.js does not handle step ${step}`); } @@ -105,14 +105,18 @@ async function step1_getOAuthLink() { // Step 9: VPS Verify — paste localhost URL and submit // ============================================================ -async function step9_vpsVerify() { - log('Step 9: Getting localhost URL from storage...'); - - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - const localhostUrl = state.localhostUrl; +async function step9_vpsVerify(payload) { + // Get localhostUrl from payload (passed directly by background) or fallback to state + let localhostUrl = payload?.localhostUrl; + if (!localhostUrl) { + log('Step 9: localhostUrl not in payload, fetching from state...'); + const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); + localhostUrl = state.localhostUrl; + } if (!localhostUrl) { throw new Error('No localhost URL found. Complete step 8 first.'); } + log(`Step 9: Got localhostUrl: ${localhostUrl.slice(0, 60)}...`); log('Step 9: Looking for callback URL input...'); diff --git a/docs/superpowers/plans/2026-04-05-multi-page-automation-extension.md b/docs/superpowers/plans/2026-04-05-multi-page-automation-extension.md deleted file mode 100644 index 4d7a2bc..0000000 --- a/docs/superpowers/plans/2026-04-05-multi-page-automation-extension.md +++ /dev/null @@ -1,2367 +0,0 @@ -# Multi-Page Automation Chrome Extension — Implementation Plan - -> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a Chrome extension (MV3) that automates a 9-step OAuth registration workflow across VPS panel, OpenAI auth, QQ Mail, and ChatGPT, controlled via a persistent Side Panel. - -**Architecture:** Side Panel (UI control) ↔ Background Service Worker (orchestration, state, tab management) ↔ Content Scripts (per-domain DOM automation). All state in `chrome.storage.session`. Unified message protocol across all components. Shared `utils.js` for DOM utilities. - -**Tech Stack:** Chrome Extension Manifest V3, vanilla JS, Chrome APIs (sidePanel, tabs, webNavigation, storage, scripting) - -**Spec:** `docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md` - ---- - -## File Structure - -``` -multiPagePlugins/ -├── manifest.json # Extension manifest (MV3) -├── background.js # Service worker: orchestration, tab mgmt, state, message routing -├── sidepanel/ -│ ├── sidepanel.html # Side Panel markup -│ ├── sidepanel.css # Side Panel styles -│ └── sidepanel.js # Side Panel logic: buttons, log, state restore -├── content/ -│ ├── utils.js # Shared utilities: waitForElement, fillInput, log, report* -│ ├── vps-panel.js # Content script for VPS panel (steps 1, 9) -│ ├── signup-page.js # Content script for OpenAI auth (steps 2, 3, 4-receive, 5) -│ ├── qq-mail.js # Content script for QQ Mail (steps 4, 7) -│ └── chatgpt.js # Content script for ChatGPT (steps 6, 7-receive, 8) -├── data/ -│ └── names.js # English first/last name lists for random generation -├── icons/ -│ ├── icon16.png -│ ├── icon48.png -│ └── icon128.png -└── docs/ - └── superpowers/ - ├── specs/... - └── plans/... -``` - -**Responsibilities by file:** - -| File | Responsibility | -|------|---------------| -| `manifest.json` | Permissions, content script registration, side panel config | -| `background.js` | Message routing, tab registry, state persistence, step orchestration, webNavigation listener, random data generation | -| `sidepanel/sidepanel.js` | Render UI, handle button clicks, display logs, state restore on open, send step commands to background | -| `content/utils.js` | `waitForElement`, `fillInput`, `log`, `reportReady`, `reportComplete`, `reportError` | -| `content/vps-panel.js` | DOM ops on VPS panel for steps 1 and 9 | -| `content/signup-page.js` | DOM ops on OpenAI auth pages for steps 2, 3, 4(fill code), 5 | -| `content/qq-mail.js` | Email polling, code extraction for steps 4 and 7 | -| `content/chatgpt.js` | DOM ops on ChatGPT for steps 6, 7(fill code), 8 | -| `data/names.js` | Static arrays of first names and last names | - ---- - -## Chunk 1: Foundation (manifest, utils, background core, side panel shell) - -### Task 1: Create manifest.json - -**Files:** -- Create: `manifest.json` - -- [ ] **Step 1: Write manifest.json** - -```json -{ - "manifest_version": 3, - "name": "Multi-Page Automation", - "version": "1.0.0", - "description": "Automates multi-step OAuth registration workflow", - "permissions": [ - "sidePanel", - "tabs", - "webNavigation", - "storage", - "scripting", - "activeTab" - ], - "host_permissions": [ - "http://154.26.182.181:8317/*", - "https://auth0.openai.com/*", - "https://auth.openai.com/*", - "https://accounts.openai.com/*", - "https://mail.qq.com/*", - "https://wx.mail.qq.com/*", - "https://chatgpt.com/*", - "http://localhost/*" - ], - "background": { - "service_worker": "background.js" - }, - "side_panel": { - "default_path": "sidepanel/sidepanel.html" - }, - "content_scripts": [ - { - "matches": ["http://154.26.182.181:8317/*"], - "js": ["content/utils.js", "content/vps-panel.js"], - "run_at": "document_idle" - }, - { - "matches": [ - "https://auth0.openai.com/*", - "https://auth.openai.com/*", - "https://accounts.openai.com/*" - ], - "js": ["content/utils.js", "content/signup-page.js"], - "run_at": "document_idle" - }, - { - "matches": [ - "https://mail.qq.com/*", - "https://wx.mail.qq.com/*" - ], - "js": ["content/utils.js", "content/qq-mail.js"], - "all_frames": true, - "run_at": "document_idle" - }, - { - "matches": ["https://chatgpt.com/*"], - "js": ["content/utils.js", "content/chatgpt.js"], - "run_at": "document_idle" - } - ], - "action": { - "default_icon": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } - }, - "icons": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } -} -``` - -- [ ] **Step 2: Create placeholder icons** - -Create simple colored square PNGs at 16x16, 48x48, 128x128 (can use canvas or any generator). These are required for Chrome to load the extension. - -- [ ] **Step 3: Verify manifest loads** - -Load the extension in `chrome://extensions` (Developer mode → Load unpacked). Confirm no manifest errors. - -- [ ] **Step 4: Commit** - -```bash -git add manifest.json icons/ -git commit -m "feat: add manifest.json with MV3 config and placeholder icons" -``` - ---- - -### Task 2: Create shared utils.js - -**Files:** -- Create: `content/utils.js` - -- [ ] **Step 1: Write utils.js with all shared functions** - -```js -// content/utils.js — Shared utilities for all content scripts - -const SCRIPT_SOURCE = (() => { - const url = location.href; - if (url.includes('154.26.182.181')) return 'vps-panel'; - if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page'; - if (url.includes('mail.qq.com')) return 'qq-mail'; - if (url.includes('chatgpt.com')) return 'chatgpt'; - return 'unknown'; -})(); - -const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`; - -/** - * Wait for a DOM element to appear. - * @param {string} selector - CSS selector - * @param {number} timeout - Max wait time in ms (default 10000) - * @returns {Promise} - */ -function waitForElement(selector, timeout = 10000) { - return new Promise((resolve, reject) => { - const existing = document.querySelector(selector); - if (existing) { - console.log(LOG_PREFIX, `Found immediately: ${selector}`); - log(`Found element: ${selector}`); - resolve(existing); - return; - } - - console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`); - log(`Waiting for selector: ${selector}...`); - - const observer = new MutationObserver(() => { - const el = document.querySelector(selector); - if (el) { - observer.disconnect(); - clearTimeout(timer); - console.log(LOG_PREFIX, `Found after wait: ${selector}`); - log(`Found element: ${selector}`); - resolve(el); - } - }); - - observer.observe(document.body || document.documentElement, { - childList: true, - subtree: true, - }); - - const timer = setTimeout(() => { - observer.disconnect(); - const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`; - console.error(LOG_PREFIX, msg); - reject(new Error(msg)); - }, timeout); - }); -} - -/** - * React-compatible form filling. - * Sets value via native setter and dispatches input + change events. - * @param {HTMLInputElement} el - * @param {string} value - */ -function fillInput(el, value) { - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, - 'value' - ).set; - nativeInputValueSetter.call(el, value); - el.dispatchEvent(new Event('input', { bubbles: true })); - el.dispatchEvent(new Event('change', { bubbles: true })); - console.log(LOG_PREFIX, `Filled input ${el.name || el.id || el.type} with: ${value}`); - log(`Filled input [${el.name || el.id || el.type || 'unknown'}]`); -} - -/** - * Send a log message to Side Panel via Background. - * @param {string} message - * @param {string} level - 'info' | 'ok' | 'warn' | 'error' - */ -function log(message, level = 'info') { - chrome.runtime.sendMessage({ - type: 'LOG', - source: SCRIPT_SOURCE, - step: null, - payload: { message, level, timestamp: Date.now() }, - error: null, - }); -} - -/** - * Report that this content script is loaded and ready. - */ -function reportReady() { - console.log(LOG_PREFIX, 'Content script ready'); - chrome.runtime.sendMessage({ - type: 'CONTENT_SCRIPT_READY', - source: SCRIPT_SOURCE, - step: null, - payload: {}, - error: null, - }); -} - -/** - * Report step completion. - * @param {number} step - * @param {Object} data - Step output data - */ -function reportComplete(step, data = {}) { - console.log(LOG_PREFIX, `Step ${step} completed`, data); - log(`Step ${step} completed successfully`, 'ok'); - chrome.runtime.sendMessage({ - type: 'STEP_COMPLETE', - source: SCRIPT_SOURCE, - step, - payload: data, - error: null, - }); -} - -/** - * Report step error. - * @param {number} step - * @param {string} errorMessage - */ -function reportError(step, errorMessage) { - console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`); - log(`Step ${step} failed: ${errorMessage}`, 'error'); - chrome.runtime.sendMessage({ - type: 'STEP_ERROR', - source: SCRIPT_SOURCE, - step, - payload: {}, - error: errorMessage, - }); -} - -/** - * Simulate a click with proper event dispatching. - * @param {Element} el - */ -function simulateClick(el) { - el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); - console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`); - log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`); -} - -// Auto-report ready on load -reportReady(); -``` - -- [ ] **Step 2: Reload extension, visit any matched URL** - -Load extension in Chrome. Open `http://154.26.182.181:8317/management.html#/oauth`. Check the Background service worker console in `chrome://extensions` — should see the `CONTENT_SCRIPT_READY` message from vps-panel. - -- [ ] **Step 3: Commit** - -```bash -git add content/utils.js -git commit -m "feat: add shared utils.js with waitForElement, fillInput, logging, and ready protocol" -``` - ---- - -### Task 3: Create name data - -**Files:** -- Create: `data/names.js` - -- [ ] **Step 1: Write names.js** - -```js -// data/names.js — English name lists for random generation - -const FIRST_NAMES = [ - 'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Christopher', - 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Barbara', 'Elizabeth', 'Susan', 'Jessica', 'Sarah', 'Karen', - 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Andrew', 'Paul', 'Joshua', 'Kenneth', - 'Emma', 'Olivia', 'Ava', 'Isabella', 'Sophia', 'Mia', 'Charlotte', 'Amelia', 'Harper', 'Evelyn', -]; - -const LAST_NAMES = [ - 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez', - 'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin', - 'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson', -]; - -/** - * Generate a random full name. - * @returns {{ firstName: string, lastName: string }} - */ -function generateRandomName() { - const firstName = FIRST_NAMES[Math.floor(Math.random() * FIRST_NAMES.length)]; - const lastName = LAST_NAMES[Math.floor(Math.random() * LAST_NAMES.length)]; - return { firstName, lastName }; -} - -/** - * Generate a random birthday (age 19-25). - * @returns {{ year: number, month: number, day: number }} - */ -function generateRandomBirthday() { - const currentYear = new Date().getFullYear(); - const age = 19 + Math.floor(Math.random() * 7); // 19 to 25 - const year = currentYear - age; - const month = 1 + Math.floor(Math.random() * 12); // 1 to 12 - const maxDay = new Date(year, month, 0).getDate(); // days in that month - const day = 1 + Math.floor(Math.random() * maxDay); - return { year, month, day }; -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add data/names.js -git commit -m "feat: add random English name and birthday generation data" -``` - ---- - -### Task 4: Create Background Service Worker — core infrastructure - -**Files:** -- Create: `background.js` - -This task covers the core infrastructure: state management, tab registry, message routing, and command queuing. Step orchestration (per-step logic) is added in later tasks. - -- [ ] **Step 1: Write background.js core** - -```js -// background.js — Service Worker: orchestration, state, tab management, message routing - -const LOG_PREFIX = '[MultiPage:bg]'; - -// ============================================================ -// State Management (chrome.storage.session) -// ============================================================ - -const DEFAULT_STATE = { - currentStep: 0, - stepStatuses: { 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending', 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending' }, - oauthUrl: null, - email: null, - password: 'mimashisha0.0', - lastEmailTimestamp: null, - localhostUrl: null, - flowStartTime: null, - tabRegistry: {}, - logs: [], -}; - -async function getState() { - const state = await chrome.storage.session.get(null); - return { ...DEFAULT_STATE, ...state }; -} - -async function setState(updates) { - console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); - await chrome.storage.session.set(updates); -} - -async function resetState() { - console.log(LOG_PREFIX, 'Resetting all state'); - await chrome.storage.session.clear(); - await chrome.storage.session.set({ ...DEFAULT_STATE }); -} - -// ============================================================ -// Tab Registry -// ============================================================ - -async function getTabRegistry() { - const state = await getState(); - return state.tabRegistry || {}; -} - -async function registerTab(source, tabId) { - const registry = await getTabRegistry(); - registry[source] = { tabId, ready: true }; - await setState({ tabRegistry: registry }); - console.log(LOG_PREFIX, `Tab registered: ${source} → ${tabId}`); -} - -async function isTabAlive(source) { - const registry = await getTabRegistry(); - const entry = registry[source]; - if (!entry) return false; - try { - await chrome.tabs.get(entry.tabId); - return true; - } catch { - // Tab no longer exists - registry[source] = null; - await setState({ tabRegistry: registry }); - return false; - } -} - -async function getTabId(source) { - const registry = await getTabRegistry(); - return registry[source]?.tabId || null; -} - -// ============================================================ -// Command Queue (for content scripts not yet ready) -// ============================================================ - -const pendingCommands = new Map(); // source → { message, resolve, reject, timer } - -function queueCommand(source, message, timeout = 15000) { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - pendingCommands.delete(source); - const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`; - console.error(LOG_PREFIX, err); - reject(new Error(err)); - }, timeout); - pendingCommands.set(source, { message, resolve, reject, timer }); - console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`); - }); -} - -function flushCommand(source, tabId) { - const pending = pendingCommands.get(source); - if (pending) { - clearTimeout(pending.timer); - pendingCommands.delete(source); - chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject); - console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`); - } -} - -// ============================================================ -// Send command to content script (with readiness check) -// ============================================================ - -async function sendToContentScript(source, message) { - const registry = await getTabRegistry(); - const entry = registry[source]; - - if (!entry || !entry.ready) { - console.log(LOG_PREFIX, `${source} not ready, queuing command`); - return queueCommand(source, message); - } - - // Verify tab is still alive - const alive = await isTabAlive(source); - if (!alive) { - throw new Error(`Tab [${source}] was closed. Will reopen on retry.`); - } - - console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type); - return chrome.tabs.sendMessage(entry.tabId, message); -} - -// ============================================================ -// Logging -// ============================================================ - -async function addLog(message, level = 'info') { - const state = await getState(); - const logs = state.logs || []; - const entry = { message, level, timestamp: Date.now() }; - logs.push(entry); - // Keep last 500 logs - if (logs.length > 500) logs.splice(0, logs.length - 500); - await setState({ logs }); - // Broadcast to side panel - chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {}); -} - -// ============================================================ -// Step Status Management -// ============================================================ - -async function setStepStatus(step, status) { - const state = await getState(); - const statuses = { ...state.stepStatuses }; - statuses[step] = status; - await setState({ stepStatuses: statuses, currentStep: step }); - // Broadcast to side panel - chrome.runtime.sendMessage({ - type: 'STEP_STATUS_CHANGED', - payload: { step, status }, - }).catch(() => {}); -} - -// ============================================================ -// Message Handler (central router) -// ============================================================ - -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message); - - handleMessage(message, sender).then(response => { - sendResponse(response); - }).catch(err => { - console.error(LOG_PREFIX, 'Handler error:', err); - sendResponse({ error: err.message }); - }); - - return true; // async response -}); - -async function handleMessage(message, sender) { - switch (message.type) { - case 'CONTENT_SCRIPT_READY': { - const tabId = sender.tab?.id; - if (tabId && message.source) { - await registerTab(message.source, tabId); - flushCommand(message.source, tabId); - await addLog(`Content script ready: ${message.source} (tab ${tabId})`); - } - return { ok: true }; - } - - case 'LOG': { - const { message: msg, level } = message.payload; - await addLog(`[${message.source}] ${msg}`, level); - return { ok: true }; - } - - case 'STEP_COMPLETE': { - await setStepStatus(message.step, 'completed'); - await addLog(`Step ${message.step} completed`, 'ok'); - // Store step-specific data - await handleStepData(message.step, message.payload); - return { ok: true }; - } - - case 'STEP_ERROR': { - await setStepStatus(message.step, 'failed'); - await addLog(`Step ${message.step} failed: ${message.error}`, 'error'); - return { ok: true }; - } - - case 'GET_STATE': { - return await getState(); - } - - case 'RESET': { - await resetState(); - await addLog('Flow reset', 'info'); - return { ok: true }; - } - - case 'EXECUTE_STEP': { - const step = message.payload.step; - await executeStep(step); - return { ok: true }; - } - - default: - console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`); - return { error: `Unknown message type: ${message.type}` }; - } -} - -// ============================================================ -// Step Data Handlers -// ============================================================ - -async function handleStepData(step, payload) { - switch (step) { - case 1: - if (payload.oauthUrl) await setState({ oauthUrl: payload.oauthUrl }); - break; - case 3: - if (payload.email) await setState({ email: payload.email }); - break; - case 4: - if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp }); - break; - case 8: - if (payload.localhostUrl) await setState({ localhostUrl: payload.localhostUrl }); - break; - } -} - -// ============================================================ -// Step Execution (stub — each step implemented in later tasks) -// ============================================================ - -async function executeStep(step) { - console.log(LOG_PREFIX, `Executing step ${step}`); - await setStepStatus(step, 'running'); - await addLog(`Step ${step} started`); - - const state = await getState(); - - // Set flow start time on first step - if (step === 1 && !state.flowStartTime) { - await setState({ flowStartTime: Date.now() }); - } - - try { - switch (step) { - case 1: await executeStep1(state); break; - case 2: await executeStep2(state); break; - case 3: await executeStep3(state); break; - case 4: await executeStep4(state); break; - case 5: await executeStep5(state); break; - case 6: await executeStep6(state); break; - case 7: await executeStep7(state); break; - case 8: await executeStep8(state); break; - case 9: await executeStep9(state); break; - default: - throw new Error(`Unknown step: ${step}`); - } - } catch (err) { - await setStepStatus(step, 'failed'); - await addLog(`Step ${step} failed: ${err.message}`, 'error'); - } -} - -// Step stubs — implemented in subsequent tasks -async function executeStep1(state) { await sendToContentScript('vps-panel', { type: 'EXECUTE_STEP', step: 1, payload: {} }); } -async function executeStep2(state) { /* Task 7 */ } -async function executeStep3(state) { /* Task 7 */ } -async function executeStep4(state) { /* Task 8 */ } -async function executeStep5(state) { /* Task 7 */ } -async function executeStep6(state) { /* Task 9 */ } -async function executeStep7(state) { /* Task 8 */ } -async function executeStep8(state) { /* Task 9 */ } -async function executeStep9(state) { await sendToContentScript('vps-panel', { type: 'EXECUTE_STEP', step: 9, payload: {} }); } - -// ============================================================ -// Open Side Panel on extension icon click -// ============================================================ - -chrome.action.onClicked.addListener((tab) => { - chrome.sidePanel.open({ windowId: tab.windowId }); -}); - -// Enable side panel for all URLs -chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); -``` - -- [ ] **Step 2: Reload extension, check service worker** - -Reload in `chrome://extensions`. Click "Service Worker" link to open DevTools for Background. Confirm no errors. Check that `chrome.storage.session` is initialized (can run `chrome.storage.session.get(null)` in console). - -- [ ] **Step 3: Commit** - -```bash -git add background.js -git commit -m "feat: add background service worker with state management, tab registry, message routing, and command queue" -``` - ---- - -### Task 5: Create Side Panel (HTML + CSS + JS) - -**Files:** -- Create: `sidepanel/sidepanel.html` -- Create: `sidepanel/sidepanel.css` -- Create: `sidepanel/sidepanel.js` - -- [ ] **Step 1: Write sidepanel.html** - -```html - - - - - - Multi-Page Automation - - - -
-

Multi-Page Automation

- -
- -
-
- - Not obtained -
-
- - -
-
- - Waiting -
-
- -
-
- 1 - - -
-
- 2 - - -
-
- 3 - - -
-
- 4 - - -
-
- 5 - - -
-
- 6 - - -
-
- 7 - - -
-
- 8 - - -
-
- 9 - - -
-
- -
-

Log

-
-
- - - - -``` - -- [ ] **Step 2: Write sidepanel.css** - -```css -/* sidepanel/sidepanel.css */ -* { margin: 0; padding: 0; box-sizing: border-box; } - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 13px; - color: #333; - background: #f8f9fa; - padding: 12px; - width: 100%; - min-height: 100vh; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -header h1 { - font-size: 15px; - font-weight: 600; -} - -#btn-reset { - padding: 4px 10px; - font-size: 12px; - background: #dc3545; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} -#btn-reset:hover { background: #c82333; } - -/* Data Section */ -#data-section { - background: #fff; - border: 1px solid #dee2e6; - border-radius: 6px; - padding: 10px; - margin-bottom: 12px; -} - -.data-row { - display: flex; - align-items: center; - margin-bottom: 6px; -} -.data-row:last-child { margin-bottom: 0; } - -.data-row label { - width: 80px; - font-weight: 600; - font-size: 12px; - color: #666; - flex-shrink: 0; -} - -.data-value { - font-size: 12px; - color: #999; - word-break: break-all; -} - -#input-email { - flex: 1; - padding: 4px 8px; - border: 1px solid #ced4da; - border-radius: 4px; - font-size: 12px; -} - -/* Steps Section */ -#steps-section { - margin-bottom: 12px; -} - -.step-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.step-num { - width: 20px; - text-align: center; - font-weight: 600; - font-size: 12px; - color: #666; -} - -.step-btn { - flex: 1; - padding: 6px 10px; - font-size: 12px; - background: #007bff; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - text-align: left; -} -.step-btn:hover:not(:disabled) { background: #0069d9; } -.step-btn:disabled { - background: #ccc; - cursor: not-allowed; -} - -.step-status { - width: 24px; - text-align: center; - font-size: 14px; -} - -/* Log Section */ -#log-section h2 { - font-size: 13px; - font-weight: 600; - margin-bottom: 6px; -} - -#log-area { - background: #1e1e1e; - color: #d4d4d4; - font-family: 'Consolas', 'Courier New', monospace; - font-size: 11px; - line-height: 1.5; - padding: 8px; - border-radius: 6px; - height: 250px; - overflow-y: auto; - white-space: pre-wrap; - word-break: break-all; -} - -.log-info { color: #d4d4d4; } -.log-ok { color: #4ec9b0; } -.log-warn { color: #dcdcaa; } -.log-error { color: #f44747; } -``` - -- [ ] **Step 3: Write sidepanel.js** - -```js -// sidepanel/sidepanel.js — Side Panel logic - -const STATUS_ICONS = { - pending: '⬚', - running: '⏳', - completed: '✅', - failed: '❌', -}; - -const logArea = document.getElementById('log-area'); -const displayOauthUrl = document.getElementById('display-oauth-url'); -const displayStatus = document.getElementById('display-status'); -const inputEmail = document.getElementById('input-email'); -const btnReset = document.getElementById('btn-reset'); - -// ============================================================ -// State Restore on load -// ============================================================ - -async function restoreState() { - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); - - // Restore data fields - if (state.oauthUrl) { - displayOauthUrl.textContent = state.oauthUrl; - displayOauthUrl.style.color = '#333'; - } - if (state.email) { - inputEmail.value = state.email; - } - - // Restore step statuses - if (state.stepStatuses) { - for (const [step, status] of Object.entries(state.stepStatuses)) { - updateStepUI(Number(step), status); - } - } - - // Restore logs - if (state.logs) { - for (const entry of state.logs) { - appendLog(entry); - } - } - - updateStatusDisplay(state); -} - -// ============================================================ -// UI Updates -// ============================================================ - -function updateStepUI(step, status) { - const statusEl = document.querySelector(`.step-status[data-step="${step}"]`); - const btnEl = document.querySelector(`.step-btn[data-step="${step}"]`); - if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '⬚'; - - // Interlock logic - updateButtonStates(); -} - -function updateButtonStates() { - // Get all current statuses from DOM - const statuses = {}; - document.querySelectorAll('.step-status').forEach(el => { - const step = Number(el.dataset.step); - const icon = el.textContent; - const status = Object.entries(STATUS_ICONS).find(([, v]) => v === icon)?.[0] || 'pending'; - statuses[step] = status; - }); - - // Find if any step is running - const anyRunning = Object.values(statuses).some(s => s === 'running'); - - for (let step = 1; step <= 9; step++) { - const btn = document.querySelector(`.step-btn[data-step="${step}"]`); - if (!btn) continue; - - if (anyRunning) { - // When any step is running, disable all buttons - btn.disabled = true; - } else if (step === 1) { - // Step 1 is always available (unless running) - btn.disabled = false; - } else { - // Steps 2-9: enabled if previous step completed (or current step failed for retry) - const prevStatus = statuses[step - 1]; - const currentStatus = statuses[step]; - btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed'); - } - } -} - -function updateStatusDisplay(state) { - if (!state.stepStatuses) return; - const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running'); - if (running) { - displayStatus.textContent = `Step ${running[0]} running...`; - } else { - const lastCompleted = Object.entries(state.stepStatuses) - .filter(([, s]) => s === 'completed') - .map(([k]) => Number(k)) - .sort((a, b) => b - a)[0]; - if (lastCompleted === 9) { - displayStatus.textContent = 'All steps completed!'; - } else if (lastCompleted) { - displayStatus.textContent = `Step ${lastCompleted} done. Ready for step ${lastCompleted + 1}.`; - } else { - displayStatus.textContent = 'Waiting'; - } - } -} - -function appendLog(entry) { - const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false }); - const levelLabel = entry.level.toUpperCase().padEnd(5); - const line = document.createElement('div'); - line.className = `log-${entry.level}`; - line.textContent = `${time} [${levelLabel}] ${entry.message}`; - logArea.appendChild(line); - logArea.scrollTop = logArea.scrollHeight; -} - -// ============================================================ -// Button Handlers -// ============================================================ - -document.querySelectorAll('.step-btn').forEach(btn => { - btn.addEventListener('click', async () => { - const step = Number(btn.dataset.step); - - // Save email if step 3 and email input has value - if (step === 3) { - const email = inputEmail.value.trim(); - if (!email) { - appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() }); - return; - } - await chrome.runtime.sendMessage({ - type: 'EXECUTE_STEP', - source: 'sidepanel', - payload: { step, email }, - }); - } else { - await chrome.runtime.sendMessage({ - type: 'EXECUTE_STEP', - source: 'sidepanel', - payload: { step }, - }); - } - }); -}); - -// Reset button -btnReset.addEventListener('click', async () => { - if (confirm('Reset all steps and data?')) { - await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' }); - // Clear UI - displayOauthUrl.textContent = 'Not obtained'; - displayOauthUrl.style.color = '#999'; - inputEmail.value = ''; - displayStatus.textContent = 'Waiting'; - logArea.innerHTML = ''; - document.querySelectorAll('.step-status').forEach(el => el.textContent = '⬚'); - updateButtonStates(); - } -}); - -// ============================================================ -// Listen for Background broadcasts -// ============================================================ - -chrome.runtime.onMessage.addListener((message) => { - switch (message.type) { - case 'LOG_ENTRY': - appendLog(message.payload); - break; - - case 'STEP_STATUS_CHANGED': { - const { step, status } = message.payload; - updateStepUI(step, status); - // Update status display - chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay); - // Update data displays - if (status === 'completed') { - chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => { - if (state.oauthUrl) { - displayOauthUrl.textContent = state.oauthUrl; - displayOauthUrl.style.color = '#333'; - } - }); - } - break; - } - } -}); - -// ============================================================ -// Init -// ============================================================ - -restoreState().then(() => { - updateButtonStates(); -}); -``` - -- [ ] **Step 4: Reload extension, open Side Panel** - -Click the extension icon → Side Panel should open on the right. Verify: -- All 9 step buttons visible -- Only step 1 button enabled, rest disabled -- Log area is empty -- Reset button is present -- Email input field is editable - -- [ ] **Step 5: Test Reset button** - -Click Reset → confirm dialog → all steps should reset to ⬚. - -- [ ] **Step 6: Commit** - -```bash -git add sidepanel/ -git commit -m "feat: add Side Panel UI with step buttons, log area, state restore, and interlock logic" -``` - ---- - -## Chunk 2: Content Scripts — VPS Panel & Signup Page - -### Task 6: Create vps-panel.js content script - -**Files:** -- Create: `content/vps-panel.js` - -- [ ] **Step 1: Write vps-panel.js** - -```js -// content/vps-panel.js — Content script for VPS panel (steps 1, 9) -// Injected on: http://154.26.182.181:8317/* - -console.log('[MultiPage:vps-panel] Content script loaded on', location.href); - -// Listen for commands from Background -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'EXECUTE_STEP') { - handleStep(message.step, message.payload).then(() => { - sendResponse({ ok: true }); - }).catch(err => { - reportError(message.step, err.message); - sendResponse({ error: err.message }); - }); - return true; - } -}); - -async function handleStep(step, payload) { - switch (step) { - case 1: return await step1_getOAuthLink(); - case 9: return await step9_vpsVerify(payload); - default: - throw new Error(`vps-panel.js does not handle step ${step}`); - } -} - -// ============================================================ -// Step 1: Get OAuth Link -// ============================================================ - -async function step1_getOAuthLink() { - log('Step 1: Checking VPS panel login state...'); - - // TODO: Adjust selector after inspecting actual VPS panel DOM - // Check login state — look for a known element that only appears when logged in - // For now, we check if the page has loaded the management content - const pageContent = document.body.innerText; - if (pageContent.includes('login') && !pageContent.includes('oauth')) { - throw new Error('VPS panel not logged in. Please log in to VPS panel at http://154.26.182.181:8317 and retry.'); - } - - log('Step 1: Looking for OAuth login button...'); - - // TODO: Adjust selectors based on actual VPS panel DOM structure - // These are placeholder selectors — must be updated during debugging - const oauthBtn = await waitForElement('[data-action="oauth-login"], .oauth-login-btn, button:has-text("OAuth")', 10000) - .catch(() => { - // Fallback: try to find any button with OAuth-related text - const buttons = [...document.querySelectorAll('button, a, [role="button"]')]; - const btn = buttons.find(b => /oauth/i.test(b.textContent)); - if (btn) return btn; - throw new Error('Could not find OAuth login button. Check VPS panel page structure in DevTools.'); - }); - - simulateClick(oauthBtn); - log('Step 1: Clicked OAuth login, waiting for Codex login...'); - - // Wait for Codex login option to appear - await new Promise(r => setTimeout(r, 1000)); // Brief wait for UI transition - - const codexBtn = await waitForElement('[data-action="codex-login"], .codex-login-btn', 10000) - .catch(() => { - const buttons = [...document.querySelectorAll('button, a, [role="button"]')]; - const btn = buttons.find(b => /codex/i.test(b.textContent)); - if (btn) return btn; - throw new Error('Could not find Codex login button. Check VPS panel DOM after clicking OAuth.'); - }); - - simulateClick(codexBtn); - log('Step 1: Clicked Codex login, waiting for auth URL...'); - - // Wait for the auth URL to appear in the page - await new Promise(r => setTimeout(r, 2000)); // Wait for URL generation - - // TODO: Adjust how the URL is extracted — may be in a text field, link, or copied to clipboard - const urlElement = await waitForElement('input[readonly], .auth-url, textarea, code', 10000) - .catch(() => { - // Fallback: search all text nodes for a URL matching auth pattern - throw new Error('Could not find auth URL element. Check VPS panel DOM for the generated URL.'); - }); - - const oauthUrl = urlElement.value || urlElement.textContent || urlElement.innerText; - if (!oauthUrl || !oauthUrl.startsWith('http')) { - throw new Error(`Invalid OAuth URL found: "${oauthUrl?.slice(0, 50)}". Expected URL starting with http.`); - } - - log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`); - reportComplete(1, { oauthUrl: oauthUrl.trim() }); -} - -// ============================================================ -// Step 9: VPS Verify -// ============================================================ - -async function step9_vpsVerify(payload) { - log('Step 9: Looking for URL input field on VPS panel...'); - - // Get localhostUrl from storage (passed via Background) - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - const localhostUrl = state.localhostUrl; - if (!localhostUrl) { - throw new Error('No localhost URL found. Complete step 8 first.'); - } - - // TODO: Adjust selector for the URL input field on VPS panel - const urlInput = await waitForElement('input[placeholder*="localhost"], input[name="callback_url"], .callback-url-input', 10000) - .catch(() => { - throw new Error('Could not find URL input field on VPS panel. Check DOM structure.'); - }); - - fillInput(urlInput, localhostUrl); - log(`Step 9: Filled URL input with: ${localhostUrl}`); - - // Find and click verify button - const verifyBtn = await waitForElement('button:has-text("verify"), .verify-btn, [data-action="verify"]', 10000) - .catch(() => { - const buttons = [...document.querySelectorAll('button, [role="button"]')]; - const btn = buttons.find(b => /verif|确认|验证/i.test(b.textContent)); - if (btn) return btn; - throw new Error('Could not find verify button. Check VPS panel DOM.'); - }); - - simulateClick(verifyBtn); - log('Step 9: Clicked verify button'); - reportComplete(9); -} -``` - -- [ ] **Step 2: Reload extension, open VPS panel** - -Navigate to `http://154.26.182.181:8317/management.html#/oauth`. Open DevTools console. Confirm `[MultiPage:vps-panel] Content script loaded` appears. - -- [ ] **Step 3: Test step 1 via Side Panel** - -Open Side Panel → click "Get OAuth Link" → observe logs. **Expected**: Will likely fail on selectors (TODO placeholders). Use DevTools to inspect actual DOM, note the correct selectors, then update the script. - -- [ ] **Step 4: Commit** - -```bash -git add content/vps-panel.js -git commit -m "feat: add vps-panel content script for steps 1 (get OAuth link) and 9 (VPS verify)" -``` - ---- - -### Task 7: Create signup-page.js content script + wire up steps 2, 3, 5 - -**Files:** -- Create: `content/signup-page.js` -- Modify: `background.js` — implement `executeStep2`, `executeStep3`, `executeStep5` - -- [ ] **Step 1: Write signup-page.js** - -```js -// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5) -// Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com - -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') { - handleCommand(message).then(() => { - sendResponse({ ok: true }); - }).catch(err => { - reportError(message.step, err.message); - sendResponse({ error: err.message }); - }); - return true; - } -}); - -async function handleCommand(message) { - switch (message.type) { - case 'EXECUTE_STEP': - switch (message.step) { - case 2: return await step2_clickRegister(); - case 3: return await step3_fillEmailPassword(message.payload); - case 5: return await step5_fillNameBirthday(message.payload); - default: throw new Error(`signup-page.js does not handle step ${message.step}`); - } - case 'FILL_CODE': - return await step4_fillVerificationCode(message.payload); - } -} - -// ============================================================ -// Step 2: Click Register -// ============================================================ - -async function step2_clickRegister() { - log('Step 2: Looking for Register/Sign up button...'); - - // TODO: Adjust selectors based on actual OpenAI auth page - const registerBtn = await waitForElement('a[href*="signup"], button:has-text("Sign up"), [data-action="signup"]', 10000) - .catch(() => { - const links = [...document.querySelectorAll('a, button')]; - const btn = links.find(b => /sign\s*up|register|注册|create/i.test(b.textContent)); - if (btn) return btn; - throw new Error('Could not find Register/Sign up button. Check auth page DOM. URL: ' + location.href); - }); - - simulateClick(registerBtn); - log('Step 2: Clicked Register button'); - reportComplete(2); -} - -// ============================================================ -// Step 3: Fill Email & Password -// ============================================================ - -async function step3_fillEmailPassword(payload) { - const { email } = payload; - if (!email) throw new Error('No email provided. Paste email in Side Panel first.'); - - log(`Step 3: Filling email: ${email}`); - - // TODO: Adjust selectors - const emailInput = await waitForElement('input[type="email"], input[name="email"], input[name="username"]', 10000) - .catch(() => { throw new Error('Could not find email input field on signup page. URL: ' + location.href); }); - - fillInput(emailInput, email); - log('Step 3: Email filled'); - - // Some signup flows show email first, then password on next page - // Try to find password field — if not found, submit email first - let passwordInput = document.querySelector('input[type="password"]'); - - if (!passwordInput) { - // Maybe need to submit email first to get to password page - log('Step 3: No password field yet, looking for continue/submit button...'); - const submitBtn = await waitForElement('button[type="submit"], input[type="submit"]', 5000) - .catch(() => { - const buttons = [...document.querySelectorAll('button')]; - return buttons.find(b => /continue|next|submit|继续/i.test(b.textContent)); - }); - - if (submitBtn) { - simulateClick(submitBtn); - log('Step 3: Submitted email, waiting for password field...'); - await new Promise(r => setTimeout(r, 2000)); - } - - passwordInput = await waitForElement('input[type="password"]', 10000) - .catch(() => { throw new Error('Could not find password input after submitting email. URL: ' + location.href); }); - } - - fillInput(passwordInput, 'mimashisha0.0'); - log('Step 3: Password filled'); - - // Submit - const submitBtn = await waitForElement('button[type="submit"], input[type="submit"]', 5000) - .catch(() => { - const buttons = [...document.querySelectorAll('button')]; - return buttons.find(b => /continue|sign\s*up|submit|注册|创建/i.test(b.textContent)); - }); - - if (submitBtn) { - simulateClick(submitBtn); - log('Step 3: Form submitted'); - } - - reportComplete(3, { email }); -} - -// ============================================================ -// Step 4 (receiving end): Fill Verification Code -// ============================================================ - -async function step4_fillVerificationCode(payload) { - const { code } = payload; - if (!code) throw new Error('No verification code provided.'); - - log(`Step 4: Filling verification code: ${code}`); - - // TODO: Adjust selector for code input - const codeInput = await waitForElement('input[name="code"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"]', 10000) - .catch(() => { throw new Error('Could not find verification code input. URL: ' + location.href); }); - - fillInput(codeInput, code); - log('Step 4: Code filled'); - - // Submit - const submitBtn = document.querySelector('button[type="submit"]') - || [...document.querySelectorAll('button')].find(b => /verify|confirm|submit|continue|确认|验证/i.test(b.textContent)); - - if (submitBtn) { - simulateClick(submitBtn); - log('Step 4: Verification submitted'); - } - - // Wait for page transition - await new Promise(r => setTimeout(r, 2000)); - reportComplete(4); -} - -// ============================================================ -// Step 5: Fill Name & Birthday -// ============================================================ - -async function step5_fillNameBirthday(payload) { - const { firstName, lastName, year, month, day } = payload; - if (!firstName || !lastName) throw new Error('No name data provided.'); - - log(`Step 5: Filling name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`); - - // TODO: Adjust selectors based on actual profile form - // First name - const firstNameInput = await waitForElement('input[name="firstName"], input[name="first_name"], input[placeholder*="first"]', 10000) - .catch(() => { throw new Error('Could not find first name input. URL: ' + location.href); }); - fillInput(firstNameInput, firstName); - - // Last name - const lastNameInput = await waitForElement('input[name="lastName"], input[name="last_name"], input[placeholder*="last"]', 5000) - .catch(() => { throw new Error('Could not find last name input. URL: ' + location.href); }); - fillInput(lastNameInput, lastName); - - // Birthday — could be separate fields or dropdowns - // TODO: This varies greatly by form. Adjust after inspecting actual page. - const birthdayInput = document.querySelector('input[name="birthday"], input[type="date"], input[name="dob"]'); - if (birthdayInput) { - const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - fillInput(birthdayInput, dateStr); - log(`Step 5: Birthday filled: ${dateStr}`); - } else { - // Try separate month/day/year selects or inputs - log('Step 5: Looking for separate birthday fields...'); - const monthInput = document.querySelector('select[name="month"], input[name="month"]'); - const dayInput = document.querySelector('select[name="day"], input[name="day"]'); - const yearInput = document.querySelector('select[name="year"], input[name="year"]'); - - if (monthInput) { - if (monthInput.tagName === 'SELECT') { monthInput.value = String(month); monthInput.dispatchEvent(new Event('change', { bubbles: true })); } - else fillInput(monthInput, String(month)); - } - if (dayInput) { - if (dayInput.tagName === 'SELECT') { dayInput.value = String(day); dayInput.dispatchEvent(new Event('change', { bubbles: true })); } - else fillInput(dayInput, String(day)); - } - if (yearInput) { - if (yearInput.tagName === 'SELECT') { yearInput.value = String(year); yearInput.dispatchEvent(new Event('change', { bubbles: true })); } - else fillInput(yearInput, String(year)); - } - - if (!monthInput && !dayInput && !yearInput) { - log('Step 5: WARNING — Could not find any birthday fields. May need to adjust selectors.', 'warn'); - } - } - - // Submit / Complete - const completeBtn = document.querySelector('button[type="submit"]') - || [...document.querySelectorAll('button')].find(b => /complete|continue|finish|done|create|完成|创建/i.test(b.textContent)); - - if (completeBtn) { - simulateClick(completeBtn); - log('Step 5: Profile form submitted'); - } - - await new Promise(r => setTimeout(r, 2000)); - reportComplete(5); -} -``` - -- [ ] **Step 2: Wire up executeStep2, executeStep3, executeStep5 in background.js** - -Replace the stubs in `background.js`: - -```js -async function executeStep2(state) { - if (!state.oauthUrl) { - throw new Error('No OAuth URL. Complete step 1 first.'); - } - await addLog(`Step 2: Opening auth URL in new tab: ${state.oauthUrl.slice(0, 80)}...`); - const tab = await chrome.tabs.create({ url: state.oauthUrl, active: true }); - // signup-page.js will auto-inject via manifest content_scripts - // When it reports ready, Background queues step 2 command - // But signup-page.js step 2 is triggered after READY signal - // So we queue the command now — it will flush when script is ready - await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 2, - source: 'background', - payload: {}, - }); -} - -async function executeStep3(state) { - if (!state.email) { - throw new Error('No email address. Paste email in Side Panel first.'); - } - await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 3, - source: 'background', - payload: { email: state.email }, - }); -} - -async function executeStep5(state) { - // Generate random name and birthday - // Import names data (loaded via importScripts or inline) - const { firstName, lastName } = generateRandomName(); - const { year, month, day } = generateRandomBirthday(); - - await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`); - - await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 5, - source: 'background', - payload: { firstName, lastName, year, month, day }, - }); -} -``` - -Also add to the top of `background.js`, after the LOG_PREFIX line: - -```js -importScripts('data/names.js'); -``` - -And update the `EXECUTE_STEP` handler in `handleMessage` to save email when step 3 is called from sidepanel: - -```js -case 'EXECUTE_STEP': { - const step = message.payload.step; - // Save email if provided (from side panel step 3) - if (message.payload.email) { - await setState({ email: message.payload.email }); - } - await executeStep(step); - return { ok: true }; -} -``` - -- [ ] **Step 3: Reload extension, verify no errors** - -Reload extension. Open Service Worker DevTools — confirm no syntax errors. Open Side Panel — confirm step buttons still work. - -- [ ] **Step 4: Commit** - -```bash -git add content/signup-page.js background.js data/names.js -git commit -m "feat: add signup-page content script (steps 2,3,4-fill,5) and wire up background orchestration" -``` - ---- - -## Chunk 3: Content Scripts — QQ Mail & ChatGPT - -### Task 8: Create qq-mail.js content script + wire up steps 4, 7 - -**Files:** -- Create: `content/qq-mail.js` -- Modify: `background.js` — implement `executeStep4`, `executeStep7` - -- [ ] **Step 1: Write qq-mail.js** - -```js -// content/qq-mail.js — Content script for QQ Mail (steps 4, 7) -// Injected on: mail.qq.com, wx.mail.qq.com -// NOTE: all_frames: true — this script runs in every frame on QQ Mail - -console.log('[MultiPage:qq-mail] Content script loaded on', location.href, 'frame:', window === window.top ? 'top' : 'child'); - -// Only act in the correct frame: -// - wx.mail.qq.com (new version): top frame only -// - mail.qq.com (old version): need to find the inbox frame -const isNewVersion = location.hostname === 'wx.mail.qq.com'; -const isTopFrame = window === window.top; - -// For old QQ Mail, only act in the frame that contains the inbox -// Skip reporting ready from irrelevant frames -if (!isNewVersion && isTopFrame) { - // Old version top frame — still report ready but inbox is in iframe - // The iframe instance of this script will handle actual email operations - console.log('[MultiPage:qq-mail] Old QQ Mail top frame — waiting for inbox frame'); -} - -// Detect if this frame contains the email list -function isInboxFrame() { - if (isNewVersion) return isTopFrame; - // Old version: check if this frame has email list elements - return !!document.querySelector('#mailList, .mail-list, [id*="mailList"]'); -} - -// Listen for commands from Background -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'POLL_EMAIL') { - // Only handle in the correct frame - if (!isInboxFrame()) { - sendResponse({ ok: false, reason: 'wrong-frame' }); - return; - } - handlePollEmail(message.step, message.payload).then(result => { - sendResponse(result); - }).catch(err => { - sendResponse({ error: err.message }); - }); - return true; - } - - if (message.type === 'CHECK_LOGIN') { - if (!isTopFrame) { sendResponse({ ok: false }); return; } - const loggedIn = checkLoginState(); - sendResponse({ loggedIn }); - return; - } -}); - -// ============================================================ -// Login State Check -// ============================================================ - -function checkLoginState() { - if (isNewVersion) { - // wx.mail.qq.com: check for inbox/compose button - return !!document.querySelector('[class*="folder"], [class*="compose"], [class*="inbox"]'); - } else { - // mail.qq.com: check for known logged-in element - return !!document.querySelector('#folder_1, .folder_inbox, #composebtn'); - } -} - -// ============================================================ -// Email Polling -// ============================================================ - -async function handlePollEmail(step, payload) { - const { filterAfterTimestamp, senderFilters, subjectFilters, maxAttempts, intervalMs } = payload; - - log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`); - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`, 'info'); - - // Try to refresh inbox - await refreshInbox(); - - // Search for matching email - const result = await findMatchingEmail(filterAfterTimestamp, senderFilters, subjectFilters); - - if (result) { - log(`Step ${step}: Found matching email! Extracting code...`); - const code = extractVerificationCode(result.content); - if (code) { - log(`Step ${step}: Verification code found: ${code}`, 'ok'); - return { ok: true, code, emailTimestamp: Date.now() }; - } else { - log(`Step ${step}: Email found but no 6-digit code in content. Content preview: ${result.content.slice(0, 100)}`, 'warn'); - } - } - - if (attempt < maxAttempts) { - await new Promise(r => setTimeout(r, intervalMs)); - } - } - - throw new Error(`No matching email found after ${maxAttempts * intervalMs / 1000}s. Check QQ Mail manually. Email may be in spam.`); -} - -async function refreshInbox() { - if (isNewVersion) { - // wx.mail.qq.com: click refresh or trigger inbox reload - // TODO: Find the actual refresh button selector - const refreshBtn = document.querySelector('[class*="refresh"], [title*="刷新"], button[aria-label*="refresh"]'); - if (refreshBtn) { - simulateClick(refreshBtn); - await new Promise(r => setTimeout(r, 500)); - } - } else { - // mail.qq.com: old version refresh - const refreshBtn = document.querySelector('#refresh, .refresh_btn'); - if (refreshBtn) { - simulateClick(refreshBtn); - await new Promise(r => setTimeout(r, 500)); - } - } -} - -async function findMatchingEmail(afterTimestamp, senderFilters, subjectFilters) { - // Get email list items - // TODO: Adjust selectors for actual QQ Mail DOM structure - let emailItems; - if (isNewVersion) { - emailItems = document.querySelectorAll('[class*="mail-item"], [class*="list-item"], tr[class*="mail"]'); - } else { - emailItems = document.querySelectorAll('.toarea tr, #mailList tr, .mail_list tr'); - } - - for (const item of emailItems) { - const text = item.textContent || ''; - const senderMatch = senderFilters.some(f => text.toLowerCase().includes(f.toLowerCase())); - const subjectMatch = subjectFilters.some(f => text.toLowerCase().includes(f.toLowerCase())); - - if (senderMatch || subjectMatch) { - // Try to get content from preview/snippet first - let content = text; - - // If we need more content, click into the email - // TODO: May need to click the email item and wait for content to load - // For now, try to extract from the visible text - if (!extractVerificationCode(content)) { - // Click to open email for full content - simulateClick(item); - await new Promise(r => setTimeout(r, 1000)); - - // Read email body - const bodyEl = document.querySelector('[class*="mail-body"], [class*="mail_body"], .body_content, #contentDiv'); - if (bodyEl) { - content = bodyEl.textContent || bodyEl.innerText || ''; - } - } - - return { content }; - } - } - - return null; -} - -function extractVerificationCode(text) { - // Match 6-digit code (most common format for verification codes) - const match = text.match(/\b(\d{6})\b/); - return match ? match[1] : null; -} -``` - -- [ ] **Step 2: Wire up executeStep4 and executeStep7 in background.js** - -Replace the stubs: - -```js -async function executeStep4(state) { - // Check if QQ Mail tab exists, open if not - const alive = await isTabAlive('qq-mail'); - if (!alive) { - await addLog('Step 4: Opening QQ Mail...'); - await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true }); - // Wait for content script ready — the sendToContentScript will queue - } - - // First check login state - const tabId = await getTabId('qq-mail'); - if (tabId) { - await chrome.tabs.update(tabId, { active: true }); - } - - // Send poll command - const result = await sendToContentScript('qq-mail', { - type: 'POLL_EMAIL', - step: 4, - source: 'background', - payload: { - filterAfterTimestamp: state.flowStartTime || 0, - senderFilters: ['openai', 'noreply', 'verify', 'auth'], - subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'], - maxAttempts: 20, - intervalMs: 3000, - }, - }); - - if (result.error) { - throw new Error(result.error); - } - - if (result.code) { - await setState({ lastEmailTimestamp: result.emailTimestamp }); - await addLog(`Step 4: Got verification code: ${result.code}`); - - // Switch to signup tab and fill code - const signupTabId = await getTabId('signup-page'); - if (signupTabId) { - await chrome.tabs.update(signupTabId, { active: true }); - await sendToContentScript('signup-page', { - type: 'FILL_CODE', - step: 4, - source: 'background', - payload: { code: result.code }, - }); - } else { - throw new Error('Signup page tab was closed. Cannot fill verification code.'); - } - } -} - -async function executeStep7(state) { - const alive = await isTabAlive('qq-mail'); - if (!alive) { - await addLog('Step 7: Opening QQ Mail...'); - await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true }); - } - - const tabId = await getTabId('qq-mail'); - if (tabId) { - await chrome.tabs.update(tabId, { active: true }); - } - - const result = await sendToContentScript('qq-mail', { - type: 'POLL_EMAIL', - step: 7, - source: 'background', - payload: { - filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0, - senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'], - subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'], - maxAttempts: 20, - intervalMs: 3000, - }, - }); - - if (result.error) { - throw new Error(result.error); - } - - if (result.code) { - await addLog(`Step 7: Got login verification code: ${result.code}`); - - // Switch to ChatGPT tab and fill code - const chatgptTabId = await getTabId('chatgpt'); - if (chatgptTabId) { - await chrome.tabs.update(chatgptTabId, { active: true }); - await sendToContentScript('chatgpt', { - type: 'FILL_CODE', - step: 7, - source: 'background', - payload: { code: result.code }, - }); - } else { - throw new Error('ChatGPT tab was closed. Cannot fill verification code.'); - } - } -} -``` - -- [ ] **Step 3: Reload extension, test qq-mail content script loads** - -Navigate to `https://wx.mail.qq.com/`. Open DevTools console. Confirm `[MultiPage:qq-mail] Content script loaded` appears. - -- [ ] **Step 4: Commit** - -```bash -git add content/qq-mail.js background.js -git commit -m "feat: add qq-mail content script with email polling and wire up steps 4, 7 orchestration" -``` - ---- - -### Task 9: Create chatgpt.js content script + wire up steps 6, 8 - -**Files:** -- Create: `content/chatgpt.js` -- Modify: `background.js` — implement `executeStep6`, `executeStep8` - -- [ ] **Step 1: Write chatgpt.js** - -```js -// content/chatgpt.js — Content script for ChatGPT (steps 6, 7-receive, 8) -// Injected on: chatgpt.com - -console.log('[MultiPage:chatgpt] 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') { - handleCommand(message).then(() => { - sendResponse({ ok: true }); - }).catch(err => { - reportError(message.step, err.message); - sendResponse({ error: err.message }); - }); - return true; - } -}); - -async function handleCommand(message) { - switch (message.type) { - case 'EXECUTE_STEP': - switch (message.step) { - case 6: return await step6_loginChatGPT(message.payload); - case 8: return await step8_navigateOAuth(message.payload); - default: throw new Error(`chatgpt.js does not handle step ${message.step}`); - } - case 'FILL_CODE': - return await step7_fillLoginCode(message.payload); - } -} - -// ============================================================ -// Step 6: Login ChatGPT -// ============================================================ - -async function step6_loginChatGPT(payload) { - log('Step 6: Looking for login button on ChatGPT...'); - - // TODO: Adjust selectors based on actual ChatGPT login page - const loginBtn = await waitForElement('[data-testid="login-button"], a[href*="auth"], button', 10000) - .catch(() => { - const links = [...document.querySelectorAll('a, button')]; - const btn = links.find(b => /log\s*in|sign\s*in|登录/i.test(b.textContent)); - if (btn) return btn; - throw new Error('Could not find Login button on ChatGPT. URL: ' + location.href); - }); - - simulateClick(loginBtn); - log('Step 6: Clicked Login button, waiting for auth page...'); - - // Wait for redirect to auth page — email input should appear - await new Promise(r => setTimeout(r, 3000)); - - // Get email from storage - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - const email = state.email; - if (!email) throw new Error('No email found in state. Complete earlier steps first.'); - - // Find email input (may be on redirected auth page) - const emailInput = await waitForElement('input[type="email"], input[name="email"], input[name="username"]', 15000) - .catch(() => { throw new Error('Could not find email input on login page. URL: ' + location.href); }); - - fillInput(emailInput, email); - log(`Step 6: Filled email: ${email}`); - - // Submit email - const submitBtn = document.querySelector('button[type="submit"]') - || [...document.querySelectorAll('button')].find(b => /continue|next|submit/i.test(b.textContent)); - if (submitBtn) simulateClick(submitBtn); - - await new Promise(r => setTimeout(r, 2000)); - - // Check: password field or OTP? - const passwordInput = document.querySelector('input[type="password"]'); - if (passwordInput) { - log('Step 6: Password field found, filling password...'); - fillInput(passwordInput, state.password || 'mimashisha0.0'); - - const submitBtn2 = document.querySelector('button[type="submit"]') - || [...document.querySelectorAll('button')].find(b => /continue|log\s*in|submit/i.test(b.textContent)); - if (submitBtn2) simulateClick(submitBtn2); - - await new Promise(r => setTimeout(r, 2000)); - - // Check if we need OTP after password - const codeInput = document.querySelector('input[name="code"], input[maxlength="6"]'); - if (codeInput) { - log('Step 6: OTP code required after password. Waiting for step 7...'); - reportComplete(6, { needsOTP: true }); - } else { - log('Step 6: Login appears successful (no OTP needed)'); - reportComplete(6, { needsOTP: false }); - } - } else { - // OTP flow — no password - log('Step 6: No password field. OTP flow detected. Waiting for step 7...'); - reportComplete(6, { needsOTP: true }); - } -} - -// ============================================================ -// Step 7 (receiving end): Fill Login Verification Code -// ============================================================ - -async function step7_fillLoginCode(payload) { - const { code } = payload; - if (!code) throw new Error('No verification code provided.'); - - log(`Step 7: Filling login verification code: ${code}`); - - const codeInput = await waitForElement('input[name="code"], input[maxlength="6"], input[type="text"][inputmode="numeric"]', 10000) - .catch(() => { throw new Error('Could not find verification code input on ChatGPT login. URL: ' + location.href); }); - - fillInput(codeInput, code); - - const submitBtn = document.querySelector('button[type="submit"]') - || [...document.querySelectorAll('button')].find(b => /continue|verify|submit|confirm/i.test(b.textContent)); - if (submitBtn) simulateClick(submitBtn); - - log('Step 7: Login verification code submitted'); - await new Promise(r => setTimeout(r, 3000)); - - reportComplete(7); -} - -// ============================================================ -// Step 8: Navigate to OAuth URL -// ============================================================ - -async function step8_navigateOAuth(payload) { - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - const oauthUrl = state.oauthUrl; - if (!oauthUrl) throw new Error('No OAuth URL found. Complete step 1 first.'); - - log(`Step 8: Navigating to OAuth URL: ${oauthUrl.slice(0, 80)}...`); - log('Step 8: Waiting for localhost redirect (captured by background)...'); - - // Navigate — the webNavigation listener in background will capture localhost redirect - window.location.href = oauthUrl; - - // Don't reportComplete here — background handles it via webNavigation -} -``` - -- [ ] **Step 2: Wire up executeStep6 and executeStep8 in background.js** - -Replace the stubs: - -```js -async function executeStep6(state) { - // Open ChatGPT - const alive = await isTabAlive('chatgpt'); - if (!alive) { - await addLog('Step 6: Opening ChatGPT...'); - await chrome.tabs.create({ url: 'https://chatgpt.com/', active: true }); - } else { - const tabId = await getTabId('chatgpt'); - await chrome.tabs.update(tabId, { active: true }); - } - - await sendToContentScript('chatgpt', { - type: 'EXECUTE_STEP', - step: 6, - source: 'background', - payload: {}, - }); -} - -let webNavListener = null; - -async function executeStep8(state) { - if (!state.oauthUrl) { - throw new Error('No OAuth URL. Complete step 1 first.'); - } - - await addLog('Step 8: Setting up localhost redirect listener...'); - - // Register webNavigation listener (scoped to this step) - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - if (webNavListener) { - chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); - webNavListener = null; - } - reject(new Error('Localhost redirect not captured after 30s. Check if OAuth authorization completed.')); - }, 30000); - - webNavListener = (details) => { - if (details.url.startsWith('http://localhost')) { - console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`); - chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); - webNavListener = null; - clearTimeout(timeout); - - setState({ localhostUrl: details.url }).then(() => { - addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok'); - setStepStatus(8, 'completed'); - resolve(); - }); - } - }; - - chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); - - // Tell chatgpt.js to navigate to OAuth URL - sendToContentScript('chatgpt', { - type: 'EXECUTE_STEP', - step: 8, - source: 'background', - payload: {}, - }).catch(reject); - }); -} -``` - -- [ ] **Step 3: Reload extension, verify no errors** - -Reload extension. Check Service Worker DevTools — no errors. Open `chatgpt.com` in a tab, verify `[MultiPage:chatgpt] Content script loaded` appears in console. - -- [ ] **Step 4: Commit** - -```bash -git add content/chatgpt.js background.js -git commit -m "feat: add chatgpt content script (steps 6,7-fill,8) and wire up background with webNavigation listener" -``` - ---- - -## Chunk 4: Integration Testing & Selector Tuning - -### Task 10: End-to-end debugging — Step 1 (VPS Panel) - -**Files:** -- Modify: `content/vps-panel.js` — update selectors based on actual DOM - -- [ ] **Step 1: Inspect VPS panel DOM** - -Open `http://154.26.182.181:8317/management.html#/oauth` in Chrome. Open DevTools → Elements tab. Identify: -1. The OAuth login button (tag, class, id, text content) -2. The Codex login button that appears after clicking OAuth -3. Where the auth URL appears (input, textarea, span, etc.) -4. The URL input field for step 9 -5. The verify button for step 9 -6. A DOM element that indicates logged-in state - -Document found selectors in a comment at the top of `vps-panel.js`. - -- [ ] **Step 2: Update vps-panel.js selectors** - -Replace all `TODO` placeholder selectors with actual selectors found in step 1. - -- [ ] **Step 3: Test step 1 via Side Panel** - -Click "Get OAuth Link" in Side Panel. Watch log output. Should complete and show OAuth URL. - -- [ ] **Step 4: Commit** - -```bash -git add content/vps-panel.js -git commit -m "fix: update vps-panel selectors to match actual DOM structure" -``` - ---- - -### Task 11: End-to-end debugging — Steps 2, 3, 4, 5 (Signup Flow) - -**Files:** -- Modify: `content/signup-page.js` — update selectors -- Modify: `content/qq-mail.js` — update selectors - -- [ ] **Step 1: Inspect OpenAI auth page DOM** - -Open the OAuth URL from step 1 in a new tab. Use DevTools to identify: -1. Register / Sign up button -2. Email input field -3. Password input field (same page or next page?) -4. Submit / Continue button -5. Verification code input field -6. Name fields (first name, last name) -7. Birthday fields (format? single input or separate?) -8. Complete registration button - -Document actual selectors. - -- [ ] **Step 2: Inspect QQ Mail DOM** - -Open `https://wx.mail.qq.com/`. Use DevTools to identify: -1. Login state indicator element -2. Inbox refresh button -3. Email list item structure (how to iterate emails) -4. How to identify sender / subject in list -5. Email body content element (after clicking an email) - -Document actual selectors. - -- [ ] **Step 3: Update signup-page.js selectors** - -Replace all TODO placeholder selectors with actual ones. - -- [ ] **Step 4: Update qq-mail.js selectors** - -Replace all TODO placeholder selectors with actual ones. - -- [ ] **Step 5: Test steps 2-5 sequentially** - -Run through steps 2-5 in Side Panel, checking log output at each step. Fix any selector or timing issues. - -- [ ] **Step 6: Commit** - -```bash -git add content/signup-page.js content/qq-mail.js -git commit -m "fix: update signup-page and qq-mail selectors to match actual DOM structures" -``` - ---- - -### Task 12: End-to-end debugging — Steps 6, 7, 8, 9 (Login + OAuth) - -**Files:** -- Modify: `content/chatgpt.js` — update selectors -- Modify: `content/vps-panel.js` — update step 9 selectors if needed - -- [ ] **Step 1: Inspect ChatGPT login page DOM** - -Open `https://chatgpt.com/`. Use DevTools to identify: -1. Login button -2. Email input on auth redirect -3. Password field (if present) -4. Submit buttons at each stage -5. Verification code input (if OTP flow) - -Document actual selectors. - -- [ ] **Step 2: Update chatgpt.js selectors** - -Replace all TODO placeholder selectors with actual ones. - -- [ ] **Step 3: Test steps 6-9 sequentially** - -Run through steps 6-9 in Side Panel. Fix any issues. - -- [ ] **Step 4: Test step 9 VPS verify selectors** - -If step 9 selectors need updating based on actual VPS panel verify form, update now. - -- [ ] **Step 5: Commit** - -```bash -git add content/chatgpt.js content/vps-panel.js -git commit -m "fix: update chatgpt and vps-panel selectors for login and verify flows" -``` - ---- - -### Task 13: Full end-to-end test - -- [ ] **Step 1: Reset and run all 9 steps** - -Click Reset in Side Panel. Run through all 9 steps sequentially: -1. Get OAuth Link → verify URL appears in Side Panel -2. Open Signup → verify registration page loads -3. (Paste DuckDuckGo email in Side Panel) Fill Email/Password → verify form filled -4. Get Signup Code → verify QQ Mail polled and code filled -5. Fill Name/Birthday → verify profile completed -6. Login ChatGPT → verify login initiated -7. Get Login Code → verify code retrieved and filled -8. Complete OAuth → verify localhost URL captured -9. VPS Verify → verify URL pasted and verify clicked - -- [ ] **Step 2: Fix any remaining issues** - -Address any timing, selector, or orchestration issues found in the full run. - -- [ ] **Step 3: Commit final fixes** - -```bash -git add -u -git commit -m "fix: address issues found in full end-to-end testing" -``` - ---- - -## Notes for the implementer - -### About TODO selectors - -Every content script has `// TODO: Adjust selector` comments. These are intentional — the actual DOM selectors for VPS panel, OpenAI auth, QQ Mail, and ChatGPT **must be determined by inspecting the live pages in DevTools**. The placeholder selectors are educated guesses. Tasks 10-12 are specifically for this selector tuning. - -### Debugging workflow - -When a step fails: -1. Check Side Panel log — it shows what went wrong -2. Open DevTools on the target tab — look for `[MultiPage:xxx]` console messages -3. Open Service Worker DevTools — look for `[MultiPage:bg]` messages -4. Use Elements tab to find the correct selector -5. Update the content script, reload extension, retry - -### Chrome extension reload - -After modifying any file: -1. Go to `chrome://extensions` -2. Click the reload (↻) button on the extension card -3. Refresh any open tabs that use content scripts (or close and reopen) -4. Reopen Side Panel if it was open diff --git a/docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md b/docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md deleted file mode 100644 index 875f5aa..0000000 --- a/docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md +++ /dev/null @@ -1,359 +0,0 @@ -# Multi-Page Automation Chrome Extension - Design Spec - -## Overview - -A Chrome extension that automates a multi-step OAuth account registration and verification workflow. The extension operates across multiple websites (VPS panel, OpenAI auth, QQ Mail, ChatGPT) using a Side Panel as the control center and Content Scripts for DOM manipulation. - -## User Story - -The user needs to repeatedly perform a multi-step workflow involving: -1. Getting an OAuth link from a VPS panel -2. Registering a new account on the linked auth page (multi-page: email/password → verify code → name/birthday) -3. Receiving email verification codes (twice: registration + login) -4. Logging into ChatGPT with the new account -5. Completing OAuth callback verification - -The extension automates DOM interactions at each step, with the user triggering steps manually via the Side Panel (semi-automatic mode, with future upgrade to full automation). - -## Architecture - -``` -┌─────────────────────────────────────────────────┐ -│ Chrome Extension │ -├──────────┬──────────────┬───────────────────────┤ -│ Side Panel │ Background │ Content Scripts │ -│ (Control) │ (Service │ (Injected per site) │ -│ │ Worker) │ │ -│ - Step list│ - Tab mgmt │ - vps-panel.js │ -│ - Buttons │ - Msg relay │ - signup-page.js │ -│ - Status │ - State store│ - qq-mail.js │ -│ - Logs │ - Orchestrate│ - chatgpt.js │ -│ - Reset │ tab switch │ - utils.js (shared) │ -└──────────┴──────────────┴───────────────────────┘ -``` - -### Components - -- **Side Panel**: Persistent right-side panel showing all steps with action buttons, status indicators, shared data display (OAuth URL, email, etc.), debug log area, and a reset button. On open/reload, restores full state from `chrome.storage.session`. -- **Background Service Worker**: Central dispatcher and orchestrator. Manages tab creation/switching, stores flow state in `chrome.storage.session` (NOT in-memory, as MV3 service workers can be terminated after 30s of inactivity), relays and routes messages between Side Panel and Content Scripts, and handles tab-switching choreography between steps. -- **Content Scripts**: Per-domain scripts injected into target pages, each responsible for DOM operations (reading, filling forms, clicking buttons). All share `utils.js` for common utilities. - -### Communication — Unified Message Protocol - -All messages between components use a standard format: -```js -{ - type: "ACTION_NAME", // e.g. "FILL_SIGNUP", "CODE_FOUND", "STEP_COMPLETE", "STEP_ERROR", "LOG" - source: "qq-mail", // sender identifier - target: "signup-page", // intended receiver (optional, Background uses for routing) - step: 4, // which workflow step this relates to - payload: { ... }, // data - error: null // error message string (if any) -} -``` - -**Message routing:** -- Side Panel <-> Background: `chrome.runtime.sendMessage` -- Background -> Content Script: `chrome.tabs.sendMessage` (only after content script reports ready) -- Content Script -> Background: `chrome.runtime.sendMessage` -- Background is the central router: it receives all messages and forwards to the correct target based on `target` field and `tabRegistry`. - -### Content Script Readiness Protocol - -Background must NOT send messages to a content script immediately after opening/navigating a tab. Instead: -1. Content script sends `{ type: "CONTENT_SCRIPT_READY", source: "vps-panel" }` message on load -2. Background registers the tab as ready in the `tabRegistry` -3. Only then does Background send action commands to that tab -4. If Background needs to send a command but the script isn't ready yet, it queues the command and sends it when the ready signal arrives (with a 15s timeout → error) - -### Tab Registry - -Background maintains a `tabRegistry` in `chrome.storage.session`: -```json -{ - "vps-panel": { "tabId": 123, "ready": true }, - "signup": { "tabId": 124, "ready": true }, - "qq-mail": { "tabId": 125, "ready": false }, - "chatgpt": { "tabId": 126, "ready": true } -} -``` -Before operating on a tab, Background checks if the tab still exists via `chrome.tabs.get()`. If the tab was closed, it reopens and waits for the ready signal. - -### Tab Switching Orchestration - -When a step requires cross-tab coordination (e.g., step 4: get code from QQ Mail → fill into signup page), the orchestration is always handled by Background: -1. Content script A (e.g., qq-mail.js) sends result to Background: `{ type: "CODE_FOUND", payload: { code: "123456" } }` -2. Background stores the data in `chrome.storage.session` -3. Background activates target tab via `chrome.tabs.update(tabId, { active: true })` -4. Background sends command to Content script B: `{ type: "FILL_CODE", payload: { code: "123456" } }` -5. Content script B executes and reports `STEP_COMPLETE` or `STEP_ERROR` - -Content scripts NEVER communicate directly with each other. - -## Workflow Steps - -| Step | Button | Executor | Action | -|------|--------|----------|--------| -| 1 | Get OAuth Link | `vps-panel.js` | Check VPS login state → Click OAuth login → Click Codex login → Read auth URL → Send to Background | -| 2 | Open Signup & Click Register | Background + `signup-page.js` | Background opens auth URL in new tab → `signup-page.js` loads and clicks "Register" button | -| 3 | Fill Email & Password | `signup-page.js` | Read email from Side Panel input (via Background) → Fill email + password (`mimashisha0.0`) → Submit | -| 4 | Get Signup Verification Code | `qq-mail.js` → Background → `signup-page.js` | Background opens QQ Mail tab → `qq-mail.js` polls for new email from OpenAI → Extracts code → Background switches to signup tab → `signup-page.js` fills code and confirms | -| 5 | Fill Name & Birthday | `signup-page.js` | After code verification, page transitions to profile form → Fill random English name + random birthday (age 19-25) → Click complete registration | -| 6 | Login ChatGPT | Background + `chatgpt.js` | Background opens chatgpt.com in new tab → `chatgpt.js` clicks login → Enters email → If password field appears, fills password; otherwise waits for OTP flow → Submit | -| 7 | Get Login Verification Code | `qq-mail.js` → Background → `chatgpt.js` | Background switches to QQ Mail tab → `qq-mail.js` polls for email newer than step 4's → Extracts code → Background switches to ChatGPT tab → `chatgpt.js` fills code → Login complete | -| 8 | Complete OAuth | Background (`webNavigation`) + `chatgpt.js` | `chatgpt.js` navigates to step 1's `oauthUrl` → Background captures localhost redirect via `webNavigation` listener → Stores `localhostUrl` | -| 9 | VPS Verify | `vps-panel.js` | Background switches to VPS panel tab → `vps-panel.js` pastes `localhostUrl` into input → Clicks verify button | - -## Content Script URL Matching - -| Script | Match Patterns | all_frames | -|--------|---------------|------------| -| `utils.js` | (included by all below) | — | -| `vps-panel.js` | `http://154.26.182.181:8317/*` | false | -| `signup-page.js` | `https://auth0.openai.com/*`, `https://auth.openai.com/*`, `https://accounts.openai.com/*` | false | -| `qq-mail.js` | `https://mail.qq.com/*`, `https://wx.mail.qq.com/*` | true | -| `chatgpt.js` | `https://chatgpt.com/*` | false | - -Note: `qq-mail.js` uses `all_frames: true` because old QQ Mail (`mail.qq.com`) renders inbox inside iframes. - -## Content Script Details - -### utils.js (shared) - -Loaded before every content script. Provides: - -```js -// Wait for a DOM element to appear, with timeout -async function waitForElement(selector, timeout = 10000) - -// React-compatible form filling — triggers proper event chain -function fillInput(el, value) { - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - ).set; - nativeInputValueSetter.call(el, value); - el.dispatchEvent(new Event('input', { bubbles: true })); - el.dispatchEvent(new Event('change', { bubbles: true })); -} - -// Send log message to Side Panel (via Background) -function log(message, level = 'info') - -// Send step result to Background -function reportComplete(step, data = {}) -function reportError(step, errorMessage) - -// Standard ready signal -function reportReady(source) -``` - -### vps-panel.js -- **Step 1**: Check login state (look for known logged-in DOM indicator); if not logged in → `reportError(1, "VPS panel not logged in, please log in first")`. Then: locate OAuth login button → click → wait for Codex login button to appear → click → wait for auth URL to appear in DOM → read it → `reportComplete(1, { oauthUrl })` -- **Step 9**: `waitForElement` for URL input field → `fillInput` with `localhostUrl` → click verify button → `reportComplete(9)` - -### signup-page.js -- **Step 2**: On ready, detect if current page has a "Register" / "Sign up" button → click it → `reportComplete(2)` -- **Step 3**: `waitForElement` for email input → `fillInput` email → `fillInput` password (`mimashisha0.0`) → click submit → `reportComplete(3)` -- **Step 4 (receiving end)**: `waitForElement` for verification code input → `fillInput` code → click confirm → `reportComplete(4)` -- **Step 5**: `waitForElement` for name/birthday fields (page transition after code verification) → `fillInput` random first name, last name → `fillInput` random birthday → click complete → `reportComplete(5)` -- All DOM operations use `waitForElement` before acting -- All input filling uses `fillInput` (React-compatible) - -### qq-mail.js -- Check login state first; if not logged in → `reportError(step, "QQ Mail not logged in, please log in first")` -- **QQ Mail version handling**: - - Old version (`mail.qq.com`): Content is inside iframes. With `all_frames: true`, the script runs in each frame. Only the frame containing the inbox list should act. - - New version (`wx.mail.qq.com`): SPA, no iframes. All content in main frame with dynamic DOM updates. -- **Polling strategy** (no page refresh): - - Click inbox refresh button (or equivalent DOM trigger) every 3 seconds - - Observe email list DOM for new entries matching sender filter (e.g., `openai`, `noreply`) or subject filter (e.g., `verify`, `verification`, `code`) - - Extract verification code from email preview/snippet if visible, or click into email and extract from body - - Log each poll attempt: `log("Polling QQ Mail... attempt 3/20")` -- **Step 4**: Find email newer than flow start time → extract 6-digit code via regex → `reportComplete(4, { code, emailTimestamp })` -- **Step 7**: Find email newer than step 4's `emailTimestamp` → extract 6-digit code → `reportComplete(7, { code })` -- Timeout after 60 seconds (20 attempts) → `reportError(step, "No matching email found after 60s")` - -### chatgpt.js -- **Step 6**: `waitForElement` for login button → click → `waitForElement` for email input → `fillInput` email → submit → detect next state: - - If password field appears → `fillInput` password → submit → `reportComplete(6)` - - If no password field (OTP flow) → `reportComplete(6, { needsOTP: true })` (code will come from step 7) -- **Step 7 (receiving end)**: `waitForElement` for code input → `fillInput` code → submit → `reportComplete(7)` -- **Step 8**: Navigate to `oauthUrl` (from storage) → page will redirect → `log("Navigating to OAuth URL, waiting for redirect...")` → (localhost capture handled by Background) - -## Data Flow - -Data persisted in `chrome.storage.session` (survives service worker termination), passed between steps: - -``` -Step 1 → oauthUrl (authorization link) -Step 3 → email, password (from Side Panel input + hardcoded) -Step 4 → lastEmailTimestamp (to distinguish step 7's email) -Step 8 → localhostUrl (OAuth callback URL) -``` - -Additionally stored: -- `currentStep`: which step is active (for UI restore) -- `stepStatuses`: `{ 1: "completed", 2: "completed", 3: "running", ... }` (for UI restore) -- `tabRegistry`: tab ID mapping (for tab management) -- `logs`: array of log entries (for UI restore) -- `flowStartTime`: timestamp when flow began (for email filtering) - -## Random Data Generation (in Background) - -- **English names**: Built-in list of common first names + last names, randomly combined -- **Birthday**: Current year minus 19-25 years, random month/day, formatted per the target form's expected format - -## Side Panel UI - -``` -┌──────────────────────────────────┐ -│ Multi-Page Automation [Reset]│ -├──────────────────────────────────┤ -│ OAuth URL: [显示/未获取] │ -│ Email: [________粘贴邮箱______] │ -│ Status: Step 3 running... │ -├──────────────────────────────────┤ -│ 1 [Get OAuth Link] ✅ │ -│ 2 [Open Signup] ✅ │ -│ 3 [Fill Email/Password] ⏳ │ -│ 4 [Get Signup Code] ⬚ 禁用 │ -│ 5 [Fill Name/Birthday] ⬚ 禁用 │ -│ 6 [Login ChatGPT] ⬚ 禁用 │ -│ 7 [Get Login Code] ⬚ 禁用 │ -│ 8 [Complete OAuth] ⬚ 禁用 │ -│ 9 [VPS Verify] ⬚ 禁用 │ -├──────────────────────────────────┤ -│ Log: │ -│ 10:23:01 [INFO] Step 1 started │ -│ 10:23:03 [INFO] Found OAuth btn │ -│ 10:23:05 [OK] Step 1 done │ -│ 10:23:05 [INFO] oauthUrl saved │ -│ 10:23:08 [INFO] Step 2 started │ -│ 10:23:10 [INFO] Tab opened │ -│ 10:23:12 [OK] Register clicked│ -│ 10:23:15 [ERR] Step 3 failed: │ -│ email input not found │ -│ 10:23:15 [INFO] Retry step 3... │ -└──────────────────────────────────┘ -``` - -**UI behaviors:** -- **Step interlock**: When a step is running, all other buttons are disabled. Next step button stays disabled until previous step succeeds. Failed steps show ❌ and can be retried. Completed steps show ✅ and can be re-run. -- **State restore**: On Side Panel open/reload, read `currentStep`, `stepStatuses`, `logs`, and all data fields from `chrome.storage.session` and render. -- **Reset button**: Clears all `chrome.storage.session` data, resets all steps to pending, clears logs. Does NOT close open tabs (user may want them). -- **Email input**: Editable text field. User pastes DuckDuckGo-generated email here before step 3. -- **Log area**: Scrollable, auto-scrolls to bottom. Shows timestamp, level (INFO/OK/WARN/ERR), and message. Each content script action logs what it's about to do and the result. - -## Debugging & Observability - -Every step must be fully debuggable. The user should never be stuck wondering "what went wrong". - -### Log Everything - -Each content script logs at these checkpoints: -1. **Step start**: `"Step N started"` -2. **Waiting for element**: `"Waiting for selector: #email-input..."` -3. **Element found/not found**: `"Found #email-input"` or `"Timeout waiting for #email-input after 10s"` -4. **Action taken**: `"Filled email input with xxx@duck.com"`, `"Clicked submit button"` -5. **Page transition**: `"Page URL changed to https://..."` -6. **Polling progress**: `"Polling QQ Mail... attempt 5/20, no match yet"` -7. **Data extracted**: `"Verification code found: 123456"` -8. **Step result**: `"Step N completed successfully"` or `"Step N failed: [reason]"` - -### Error Recovery Guide - -When a step fails, the error message should include actionable guidance: - -| Error | Guidance shown in log | -|-------|----------------------| -| VPS not logged in | "Please log in to VPS panel at http://154.26.182.181:8317 and retry" | -| QQ Mail not logged in | "Please log in to QQ Mail and retry" | -| Element not found (timeout) | "Could not find [selector] on [url]. Page may have changed layout. Check DevTools." | -| Email not received (timeout) | "No matching email after 60s. Check QQ Mail manually. Email may be in spam." | -| Tab was closed | "Tab [name] was closed. Will reopen on retry." | -| Content script not ready | "Content script on [url] did not respond in 15s. Try refreshing the tab and retry." | -| Unknown error | "Unexpected error: [message]. Check DevTools console on [tab name] for details." | - -### DevTools Integration - -- Each content script logs to the browser console with a prefix: `[MultiPage:vps-panel]`, `[MultiPage:qq-mail]`, etc. -- Background service worker also logs with `[MultiPage:bg]` prefix. -- All chrome.storage writes are logged: `[MultiPage:bg] storage.set: stepStatuses = {...}` -- This allows using Chrome DevTools on any tab or the service worker to see detailed execution trace. - -## Robustness - -### Login State Detection -- **VPS panel** (step 1): Check for known logged-in DOM indicator before operating; if absent, `reportError` with guidance and pause. -- **QQ Mail** (steps 4, 7): Check for inbox DOM indicator; if absent, `reportError` with guidance and pause. - -### DOM Element Waiting -All content script DOM operations use `waitForElement(selector, timeout)` from `utils.js`. Default timeout: 10 seconds. On timeout, `reportError` with the selector and URL for debugging. - -### Email Polling -- Poll QQ Mail inbox every 3 seconds for matching email (filter by sender/subject keywords) -- Step 7 only accepts emails newer than step 4's `lastEmailTimestamp` to avoid duplicates -- Each poll attempt is logged with attempt count -- Timeout after 60 seconds (20 attempts); `reportError` with guidance to check spam folder - -### Localhost Redirect Capture -- `chrome.webNavigation.onBeforeNavigate` listener is registered in Background **only when step 8 starts** -- Listener matches URL starting with `http://localhost` -- On capture: store `localhostUrl`, remove listener, `reportComplete(8)` -- If not captured within 30 seconds: `reportError` with guidance -- Listener is always removed when step 8 ends (success or failure) - -### Page Transitions -- After form submissions, `signup-page.js` and `chatgpt.js` must handle page navigation/SPA route changes -- Use `waitForElement` on the NEXT expected element after submission, not just submit and hope -- If URL changes (full navigation), the content script re-injects and sends a new READY signal; Background re-sends the pending command - -## Permissions (manifest.json) - -```json -{ - "permissions": [ - "sidePanel", - "tabs", - "webNavigation", - "storage", - "scripting", - "activeTab" - ], - "host_permissions": [ - "http://154.26.182.181:8317/*", - "https://auth0.openai.com/*", - "https://auth.openai.com/*", - "https://accounts.openai.com/*", - "https://mail.qq.com/*", - "https://wx.mail.qq.com/*", - "https://chatgpt.com/*", - "http://localhost/*" - ] -} -``` - -Note: `scripting` permission enables `chrome.scripting.executeScript` for dynamic injection when the auth flow redirects to unexpected domains. - -## Technical Decisions - -- **Manifest V3**: Required for current Chrome extension development -- **Side Panel API**: `chrome.sidePanel` for persistent control panel -- **Pure Content Script approach**: No external dependencies, direct DOM manipulation -- **State in `chrome.storage.session`**: MV3 service workers can terminate after 30s idle; all flow state must be persisted, not held in memory -- **React-compatible form filling**: Use native setter + event dispatch pattern for all React-based target pages -- **Content script readiness protocol**: Scripts report ready before receiving commands; commands queued if not ready -- **Unified message protocol**: All inter-component messages use same format with type/source/target/step/payload/error -- **Shared utils.js**: Common utilities loaded by all content scripts to avoid duplication -- **Background as sole orchestrator**: All cross-tab coordination goes through Background; content scripts never talk to each other -- **webNavigation listener scoped to step 8**: Registered on step start, removed on step end, avoids false triggers -- **Semi-automatic first**: Each step triggered manually, upgrade to full automation later -- **DuckDuckGo email generation**: Manual step (user clicks DuckDuckGo panel), user pastes email into Side Panel input - -## Future Enhancements - -- Full automation mode (single button to run all steps) -- DuckDuckGo API integration for automatic email generation -- Batch execution support -- Error recovery and auto-retry logic -- Export logs for troubleshooting diff --git a/icons/icon128.png b/icons/icon128.png index 7e91d25..cfe63a0 100644 Binary files a/icons/icon128.png and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png index fe4eb41..fea6c40 100644 Binary files a/icons/icon16.png and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png index 6314286..4986d82 100644 Binary files a/icons/icon48.png and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json index 01f5fe8..2db9ea0 100644 --- a/manifest.json +++ b/manifest.json @@ -46,11 +46,6 @@ "js": ["content/utils.js", "content/mail-163.js"], "all_frames": true, "run_at": "document_idle" - }, - { - "matches": ["https://chatgpt.com/*"], - "js": ["content/utils.js", "content/chatgpt.js"], - "run_at": "document_idle" } ], "action": { diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index f4c6da0..d33f7c8 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -75,7 +75,7 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 13px; + font-size: 14px; color: var(--text-primary); background: var(--bg-base); padding: 12px; @@ -107,7 +107,7 @@ header { .header-left svg { color: var(--blue); } .header-left h1 { - font-size: 14px; + font-size: 16px; font-weight: 700; letter-spacing: -0.02em; color: var(--text-primary); @@ -149,10 +149,10 @@ header { .btn { display: inline-flex; align-items: center; - gap: 5px; - padding: 6px 12px; + gap: 6px; + padding: 7px 14px; font-family: inherit; - font-size: 12px; + font-size: 13px; font-weight: 600; border: 1px solid transparent; border-radius: var(--radius-sm); @@ -180,21 +180,22 @@ header { } .run-count-input { - width: 38px; - padding: 5px 4px; + width: 42px; + padding: 6px 4px; text-align: center; background: var(--bg-base); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-primary); font-family: 'JetBrains Mono', monospace; - font-size: 12px; + font-size: 13px; font-weight: 600; outline: none; } .run-count-input:focus { border-color: var(--blue); } .run-count-input::-webkit-inner-spin-button { opacity: 0.5; } -.btn-success:disabled { background: var(--bg-elevated); color: var(--text-muted); cursor: not-allowed; box-shadow: none; } +.btn-success:disabled, .btn-primary:disabled { background: var(--bg-elevated); color: var(--text-muted); cursor: not-allowed; box-shadow: none; } +.run-count-input:disabled { opacity: 0.5; cursor: not-allowed; } .btn-ghost { background: transparent; @@ -211,8 +212,8 @@ header { } .btn-outline:hover { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); } -.btn-sm { padding: 4px 10px; font-size: 11px; } -.btn-xs { padding: 3px 8px; font-size: 10px; } +.btn-sm { padding: 5px 12px; font-size: 12px; } +.btn-xs { padding: 4px 10px; font-size: 11px; } /* ============================================================ Data Card @@ -224,10 +225,10 @@ header { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-md); - padding: 10px 12px; + padding: 12px 14px; display: flex; flex-direction: column; - gap: 7px; + gap: 9px; box-shadow: var(--shadow-sm); } @@ -238,8 +239,8 @@ header { } .data-label { - width: 54px; - font-size: 10px; + width: 56px; + font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; @@ -248,7 +249,7 @@ header { } .data-value { - font-size: 11px; + font-size: 13px; color: var(--text-muted); min-width: 0; } @@ -257,7 +258,7 @@ header { .mono { font-family: 'JetBrains Mono', 'Consolas', monospace; - font-size: 10.5px; + font-size: 12px; } .truncate { @@ -268,13 +269,13 @@ header { .data-input { flex: 1; - padding: 5px 8px; + padding: 7px 10px; background: var(--bg-base); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-primary); font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 13px; outline: none; transition: border-color var(--transition), box-shadow var(--transition); min-width: 0; @@ -284,13 +285,13 @@ header { .data-select { flex: 1; - padding: 5px 8px; + padding: 7px 10px; background: var(--bg-base); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text-primary); font-family: inherit; - font-size: 11px; + font-size: 13px; outline: none; cursor: pointer; transition: border-color var(--transition); @@ -305,19 +306,19 @@ header { align-items: center; gap: 8px; margin-top: 8px; - padding: 6px 10px; + padding: 8px 12px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-sm); - font-size: 11px; + font-size: 13px; font-weight: 500; color: var(--text-secondary); box-shadow: var(--shadow-sm); } .status-dot { - width: 7px; - height: 7px; + width: 8px; + height: 8px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; @@ -353,7 +354,7 @@ header { gap: 8px; } .auto-continue-bar svg { color: var(--orange); flex-shrink: 0; } -.auto-hint { font-size: 11px; color: var(--orange); flex: 1; font-weight: 500; } +.auto-hint { font-size: 13px; color: var(--orange); flex: 1; font-weight: 500; } /* ============================================================ Steps Section @@ -369,7 +370,7 @@ header { } .section-label { - font-size: 10px; + font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; @@ -378,7 +379,7 @@ header { .steps-progress { font-family: 'JetBrains Mono', monospace; - font-size: 10px; + font-size: 11px; font-weight: 600; color: var(--text-muted); background: var(--bg-surface); @@ -390,7 +391,7 @@ header { .steps-list { display: flex; flex-direction: column; - gap: 3px; + gap: 5px; } .step-row { @@ -403,8 +404,8 @@ header { /* Step Number Indicator */ .step-indicator { - width: 22px; - height: 22px; + width: 26px; + height: 26px; border-radius: 50%; background: var(--bg-surface); border: 1.5px solid var(--border); @@ -416,7 +417,7 @@ header { } .step-num { - font-size: 10px; + font-size: 11px; font-weight: 700; color: var(--text-muted); transition: color var(--transition); @@ -435,9 +436,9 @@ header { /* Step Button */ .step-btn { flex: 1; - padding: 6px 10px; + padding: 8px 12px; font-family: inherit; - font-size: 12px; + font-size: 13px; font-weight: 500; color: var(--text-primary); background: var(--bg-surface); @@ -456,9 +457,9 @@ header { .step-row.failed .step-btn { border-color: var(--red); color: var(--red); } .step-status { - width: 18px; + width: 20px; text-align: center; - font-size: 12px; + font-size: 14px; font-weight: 700; flex-shrink: 0; } @@ -480,11 +481,11 @@ header { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-md); - padding: 8px 10px; + padding: 10px 12px; height: 220px; overflow-y: auto; font-family: 'JetBrains Mono', 'Consolas', monospace; - font-size: 10.5px; + font-size: 12px; line-height: 1.7; color: var(--text-secondary); box-shadow: var(--shadow-sm); @@ -495,7 +496,7 @@ header { #log-area::-webkit-scrollbar-thumb { background: var(--bg-elevated); border-radius: 4px; } #log-area::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } -.log-line { padding: 1.5px 0; } +.log-line { padding: 2.5px 0; } .log-line + .log-line { border-top: 1px solid var(--border-subtle); } .log-time { color: var(--text-muted); } @@ -508,10 +509,10 @@ header { .log-step-tag { display: inline-block; - padding: 0 4px; + padding: 1px 5px; border-radius: 3px; font-weight: 700; - font-size: 9.5px; + font-size: 11px; margin-right: 3px; } .log-step-tag.step-1 { color: var(--cyan); background: rgba(8, 145, 178, 0.08); } @@ -561,9 +562,9 @@ header { display: flex; align-items: center; gap: 8px; - padding: 10px 12px; + padding: 10px 14px; border-radius: var(--radius-md); - font-size: 12px; + font-size: 13px; font-weight: 500; box-shadow: var(--shadow-md), 0 4px 12px rgba(0,0,0,0.1); pointer-events: auto; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index d72afe9..746ce6c 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -45,7 +45,7 @@
VPS - +
Mail diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 7578886..9b4d441 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -107,7 +107,6 @@ async function restoreState() { function updateStepUI(step, status) { const statusEl = document.querySelector(`.step-status[data-step="${step}"]`); const row = document.querySelector(`.step-row[data-step="${step}"]`); - const indicator = document.querySelector(`.step-indicator[data-step="${step}"]`); if (statusEl) statusEl.textContent = STATUS_ICONS[status] || ''; if (row) {