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
+
+ ⬚
+
+
+
+
+
+
+
+
+```
+
+- [ ] **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
// ============================================================