From c6f1070ace49d0d549c36c8f9969bad696e54d88 Mon Sep 17 00:00:00 2001 From: unknown <1249156074@qq.com> Date: Sun, 5 Apr 2026 09:56:48 +0800 Subject: [PATCH] feat: configurable VPS URL, 163 mail support, mail provider selector - VPS URL: now an input field in Side Panel, no longer hardcoded - Dynamic script injection via chrome.scripting.executeScript - host_permissions changed to for flexibility - 163 Mail: new content script (mail-163.js) with actual selectors - Mail items: div[sign="letter"], sender: .nui-user, subject: span.da0 - Supports aria-label fallback for matching - Mail provider selector: dropdown to switch between QQ Mail and 163 Mail - Background routes steps 4/7 to correct mail content script - Settings persist in chrome.storage.session --- background.js | 86 +- content/mail-163.js | 201 ++ content/utils.js | 5 +- ...6-04-05-multi-page-automation-extension.md | 2367 +++++++++++++++++ ...-multi-page-automation-extension-design.md | 359 +++ manifest.json | 24 +- sidepanel/sidepanel.css | 17 + sidepanel/sidepanel.html | 15 +- sidepanel/sidepanel.js | 24 +- 9 files changed, 3063 insertions(+), 35 deletions(-) create mode 100644 content/mail-163.js create mode 100644 docs/superpowers/plans/2026-04-05-multi-page-automation-extension.md create mode 100644 docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md diff --git a/background.js b/background.js index 5757a8b..c0086a3 100644 --- a/background.js +++ b/background.js @@ -22,6 +22,8 @@ const DEFAULT_STATE = { flowStartTime: null, tabRegistry: {}, logs: [], + vpsUrl: 'http://154.26.182.181:8317/management.html#/oauth', + mailProvider: 'qq', // 'qq' or '163' }; async function getState() { @@ -245,6 +247,14 @@ async function handleMessage(message, sender) { return { ok: true }; } + case 'SAVE_SETTING': { + const updates = {}; + if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl; + if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider; + await setState(updates); + return { ok: true }; + } + // Side panel data updates case 'SAVE_EMAIL': { await setState({ email: message.payload.email }); @@ -445,17 +455,36 @@ async function resumeAutoRun() { // ============================================================ async function executeStep1(state) { + const vpsUrl = state.vpsUrl; + if (!vpsUrl) { + throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.'); + } + // Ensure VPS panel tab is open const alive = await isTabAlive('vps-panel'); if (!alive) { - await addLog('Step 1: Opening VPS panel...'); - await chrome.tabs.create({ url: 'http://154.26.182.181:8317/management.html#/oauth', active: true }); + await addLog(`Step 1: Opening VPS panel: ${vpsUrl.slice(0, 60)}...`); + const tab = await chrome.tabs.create({ url: vpsUrl, active: true }); + // Dynamically inject content scripts since VPS URL is configurable + // Wait for page to load, then inject + await new Promise(resolve => { + const listener = (tabId, info) => { + if (tabId === tab.id && info.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + }); + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ['content/utils.js', 'content/vps-panel.js'], + }); } else { const tabId = await getTabId('vps-panel'); if (tabId) await chrome.tabs.update(tabId, { active: true }); } - // Send command — will queue if content script not ready yet, flush on READY signal await sendToContentScript('vps-panel', { type: 'EXECUTE_STEP', step: 1, @@ -505,19 +534,27 @@ async function executeStep3(state) { // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js) // ============================================================ +function getMailConfig(state) { + const provider = state.mailProvider || 'qq'; + if (provider === '163') { + return { source: 'mail-163', url: 'https://mail.163.com/', label: '163 Mail' }; + } + return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' }; +} + async function executeStep4(state) { - // Ensure QQ Mail tab is open - const alive = await isTabAlive('qq-mail'); + const mail = getMailConfig(state); + + const alive = await isTabAlive(mail.source); if (!alive) { - await addLog('Step 4: Opening QQ Mail...'); - await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true }); + await addLog(`Step 4: Opening ${mail.label}...`); + await chrome.tabs.create({ url: mail.url, active: true }); } else { - const tabId = await getTabId('qq-mail'); + const tabId = await getTabId(mail.source); if (tabId) await chrome.tabs.update(tabId, { active: true }); } - // Send poll command to qq-mail - const result = await sendToContentScript('qq-mail', { + const result = await sendToContentScript(mail.source, { type: 'POLL_EMAIL', step: 4, source: 'background', @@ -608,16 +645,18 @@ async function executeStep6(state) { // ============================================================ async function executeStep7(state) { - const alive = await isTabAlive('qq-mail'); + const mail = getMailConfig(state); + + const alive = await isTabAlive(mail.source); if (!alive) { - await addLog('Step 7: Opening QQ Mail...'); - await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true }); + await addLog(`Step 7: Opening ${mail.label}...`); + await chrome.tabs.create({ url: mail.url, active: true }); } else { - const tabId = await getTabId('qq-mail'); + const tabId = await getTabId(mail.source); if (tabId) await chrome.tabs.update(tabId, { active: true }); } - const result = await sendToContentScript('qq-mail', { + const result = await sendToContentScript(mail.source, { type: 'POLL_EMAIL', step: 7, source: 'background', @@ -746,11 +785,26 @@ async function executeStep9(state) { throw new Error('No localhost URL. Complete step 8 first.'); } + const vpsUrl = state.vpsUrl || 'http://154.26.182.181:8317/management.html#/oauth'; + // Switch to VPS panel tab const alive = await isTabAlive('vps-panel'); if (!alive) { await addLog('Step 9: Opening VPS panel...'); - await chrome.tabs.create({ url: 'http://154.26.182.181:8317/management.html#/oauth', active: true }); + const tab = await chrome.tabs.create({ url: vpsUrl, active: true }); + await new Promise(resolve => { + const listener = (tabId, info) => { + if (tabId === tab.id && info.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + }); + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ['content/utils.js', 'content/vps-panel.js'], + }); } else { const tabId = await getTabId('vps-panel'); if (tabId) await chrome.tabs.update(tabId, { active: true }); diff --git a/content/mail-163.js b/content/mail-163.js new file mode 100644 index 0000000..cfdc12b --- /dev/null +++ b/content/mail-163.js @@ -0,0 +1,201 @@ +// content/mail-163.js — Content script for 163 Mail (steps 4, 7) +// Injected on: mail.163.com +// +// Actual 163 Mail DOM structure: +//
+//
+// OpenAI +//
+//
+// 你的 ChatGPT 代码为 479637 +//
+//
+ +const MAIL163_PREFIX = '[MultiPage:mail-163]'; +const isTopFrame = window === window.top; + +console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child'); + +// ============================================================ +// Message Handler +// ============================================================ + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'POLL_EMAIL') { + handlePollEmail(message.step, message.payload).then(result => { + sendResponse(result); + }).catch(err => { + reportError(message.step, err.message); + sendResponse({ error: err.message }); + }); + return true; + } +}); + +// ============================================================ +// Get all current mail IDs +// ============================================================ + +function getCurrentMailIds() { + const ids = new Set(); + // 163 mail items have sign="letter" and id ending with "Dom" + const items = findMailItems(); + for (const item of items) { + const id = item.getAttribute('id') || ''; + if (id) ids.add(id); + } + return ids; +} + +function findMailItems() { + // Try current document first + let items = document.querySelectorAll('div[sign="letter"]'); + if (items.length > 0) return items; + + // Try iframes (163 mail may use iframes) + const iframes = document.querySelectorAll('iframe'); + for (const iframe of iframes) { + try { + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if (doc) { + items = doc.querySelectorAll('div[sign="letter"]'); + if (items.length > 0) return items; + } + } catch { } + } + return []; +} + +// ============================================================ +// Email Polling +// ============================================================ + +async function handlePollEmail(step, payload) { + const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload; + + log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`); + + // Wait for mail list to load + await sleep(3000); + + let items = findMailItems(); + if (items.length === 0) { + log(`Step ${step}: Waiting for mail list to appear...`); + await sleep(5000); + items = findMailItems(); + } + + if (items.length === 0) { + throw new Error('163 Mail list did not load. Make sure inbox is open.'); + } + + log(`Step ${step}: Mail list loaded, ${items.length} items found`); + + const existingMailIds = getCurrentMailIds(); + log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`); + + const FALLBACK_AFTER = 3; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`); + + if (attempt > 1) { + await refreshInbox(); + await sleep(1000); + } + + const allItems = findMailItems(); + const useFallback = attempt > FALLBACK_AFTER; + + for (const item of allItems) { + const id = item.getAttribute('id') || ''; + + if (!useFallback && existingMailIds.has(id)) continue; + + // Get sender from .nui-user + const senderEl = item.querySelector('.nui-user'); + const sender = senderEl ? senderEl.textContent.toLowerCase() : ''; + + // Get subject from span.da0 + const subjectEl = item.querySelector('span.da0'); + const subject = subjectEl ? subjectEl.textContent : ''; + + // Also check aria-label which contains full info + const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase(); + + const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase())); + const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase())); + + if (senderMatch || subjectMatch) { + const code = extractVerificationCode(subject + ' ' + ariaLabel); + if (code) { + const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new'; + log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok'); + return { ok: true, code, emailTimestamp: Date.now(), mailId: id }; + } + } + } + + if (attempt === FALLBACK_AFTER + 1) { + log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn'); + } + + if (attempt < maxAttempts) { + await sleep(intervalMs); + } + } + + throw new Error( + `No matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` + + 'Check inbox manually.' + ); +} + +// ============================================================ +// Inbox Refresh +// ============================================================ + +async function refreshInbox() { + // 163 mail: click the "收信" button in toolbar + function tryRefresh(doc) { + const btn = doc.querySelector( + 'a[title="收信"], [id*="refresh"], .nui-toolbar-item[title*="收"]' + ); + if (btn) { + btn.click(); + console.log(MAIL163_PREFIX, 'Clicked 收信 button'); + return true; + } + return false; + } + + if (tryRefresh(document)) { await sleep(500); return; } + + // Try in iframes + const iframes = document.querySelectorAll('iframe'); + for (const iframe of iframes) { + try { + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if (doc && tryRefresh(doc)) { await sleep(500); return; } + } catch { } + } + + console.log(MAIL163_PREFIX, 'Could not find refresh button'); +} + +// ============================================================ +// Verification Code Extraction +// ============================================================ + +function extractVerificationCode(text) { + const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/); + if (matchCn) return matchCn[1]; + + const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i); + if (matchEn) return matchEn[1] || matchEn[2]; + + const match6 = text.match(/\b(\d{6})\b/); + if (match6) return match6[1]; + + return null; +} diff --git a/content/utils.js b/content/utils.js index 99ee28e..e82f556 100644 --- a/content/utils.js +++ b/content/utils.js @@ -2,11 +2,12 @@ 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('mail.163.com')) return 'mail-163'; if (url.includes('chatgpt.com')) return 'chatgpt'; - return 'unknown'; + // VPS panel — detected dynamically since URL is configurable + return 'vps-panel'; })(); const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`; 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 new file mode 100644 index 0000000..4d7a2bc --- /dev/null +++ b/docs/superpowers/plans/2026-04-05-multi-page-automation-extension.md @@ -0,0 +1,2367 @@ +# 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 new file mode 100644 index 0000000..875f5aa --- /dev/null +++ b/docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md @@ -0,0 +1,359 @@ +# 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/manifest.json b/manifest.json index 42f4249..01f5fe8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Multi-Page Automation", - "version": "1.0.0", + "version": "1.1.0", "description": "Automates multi-step OAuth registration workflow", "permissions": [ "sidePanel", @@ -12,14 +12,7 @@ "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" @@ -28,11 +21,6 @@ "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/*", @@ -51,6 +39,14 @@ "all_frames": true, "run_at": "document_idle" }, + { + "matches": [ + "https://mail.163.com/*" + ], + "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"], diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index baf8b60..1caac53 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -260,6 +260,23 @@ header { .data-input::placeholder { color: var(--text-muted); } .data-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } +.data-select { + flex: 1; + padding: 5px 8px; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: inherit; + font-size: 11px; + outline: none; + cursor: pointer; + transition: border-color var(--transition); + min-width: 0; +} +.data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } +[data-theme="dark"] .data-select { color-scheme: dark; } + /* Status Bar */ .status-bar { display: flex; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index a16f6d7..bf7e08a 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -41,13 +41,24 @@
- OAuth - Waiting... + VPS + +
+
+ Mail +
Email
+
+ OAuth + Waiting... +
Callback Waiting... diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 27f0772..bc5ed41 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -19,6 +19,8 @@ const btnAutoRun = document.getElementById('btn-auto-run'); const btnAutoContinue = document.getElementById('btn-auto-continue'); const autoContinueBar = document.getElementById('auto-continue-bar'); const btnClearLog = document.getElementById('btn-clear-log'); +const inputVpsUrl = document.getElementById('input-vps-url'); +const selectMailProvider = document.getElementById('select-mail-provider'); // ============================================================ // State Restore on load @@ -39,6 +41,12 @@ async function restoreState() { if (state.email) { inputEmail.value = state.email; } + if (state.vpsUrl) { + inputVpsUrl.value = state.vpsUrl; + } + if (state.mailProvider) { + selectMailProvider.value = state.mailProvider; + } if (state.stepStatuses) { for (const [step, status] of Object.entries(state.stepStatuses)) { @@ -238,7 +246,7 @@ btnClearLog.addEventListener('click', () => { logArea.innerHTML = ''; }); -// Save email on change +// Save settings on change inputEmail.addEventListener('change', async () => { const email = inputEmail.value.trim(); if (email) { @@ -246,6 +254,20 @@ inputEmail.addEventListener('change', async () => { } }); +inputVpsUrl.addEventListener('change', async () => { + const vpsUrl = inputVpsUrl.value.trim(); + if (vpsUrl) { + await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { vpsUrl } }); + } +}); + +selectMailProvider.addEventListener('change', async () => { + await chrome.runtime.sendMessage({ + type: 'SAVE_SETTING', source: 'sidepanel', + payload: { mailProvider: selectMailProvider.value }, + }); +}); + // ============================================================ // Listen for Background broadcasts // ============================================================