Просмотр исходного кода

feat: configurable VPS URL, 163 mail support, mail provider selector

- VPS URL: now an input field in Side Panel, no longer hardcoded
  - Dynamic script injection via chrome.scripting.executeScript
  - host_permissions changed to <all_urls> for flexibility
- 163 Mail: new content script (mail-163.js) with actual selectors
  - Mail items: div[sign="letter"], sender: .nui-user, subject: span.da0
  - Supports aria-label fallback for matching
- Mail provider selector: dropdown to switch between QQ Mail and 163 Mail
  - Background routes steps 4/7 to correct mail content script
- Settings persist in chrome.storage.session
unknown 4 месяцев назад
Родитель
Сommit
c6f1070ace

+ 70 - 16
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 });

+ 201 - 0
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:
+// <div class="rF0" sign="letter" id="...Dom" aria-label="你的 ChatGPT 代码为 479637 发件人 : OpenAI ...">
+//   <div class="dP0" sign="start-from">
+//     <span class="nui-user">OpenAI</span>
+//   </div>
+//   <div class="il0">
+//     <span class="da0">你的 ChatGPT 代码为 479637</span>
+//   </div>
+// </div>
+
+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;
+}

+ 3 - 2
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}]`;

+ 2367 - 0
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<Element>}
+ */
+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
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Multi-Page Automation</title>
+  <link rel="stylesheet" href="sidepanel.css">
+</head>
+<body>
+  <header>
+    <h1>Multi-Page Automation</h1>
+    <button id="btn-reset" title="Reset all steps">Reset</button>
+  </header>
+
+  <section id="data-section">
+    <div class="data-row">
+      <label>OAuth URL:</label>
+      <span id="display-oauth-url" class="data-value">Not obtained</span>
+    </div>
+    <div class="data-row">
+      <label>Email:</label>
+      <input type="text" id="input-email" placeholder="Paste DuckDuckGo email here" />
+    </div>
+    <div class="data-row">
+      <label>Status:</label>
+      <span id="display-status" class="data-value">Waiting</span>
+    </div>
+  </section>
+
+  <section id="steps-section">
+    <div class="step-row" data-step="1">
+      <span class="step-num">1</span>
+      <button class="step-btn" data-step="1">Get OAuth Link</button>
+      <span class="step-status" data-step="1">⬚</span>
+    </div>
+    <div class="step-row" data-step="2">
+      <span class="step-num">2</span>
+      <button class="step-btn" data-step="2">Open Signup</button>
+      <span class="step-status" data-step="2">⬚</span>
+    </div>
+    <div class="step-row" data-step="3">
+      <span class="step-num">3</span>
+      <button class="step-btn" data-step="3">Fill Email/Password</button>
+      <span class="step-status" data-step="3">⬚</span>
+    </div>
+    <div class="step-row" data-step="4">
+      <span class="step-num">4</span>
+      <button class="step-btn" data-step="4">Get Signup Code</button>
+      <span class="step-status" data-step="4">⬚</span>
+    </div>
+    <div class="step-row" data-step="5">
+      <span class="step-num">5</span>
+      <button class="step-btn" data-step="5">Fill Name/Birthday</button>
+      <span class="step-status" data-step="5">⬚</span>
+    </div>
+    <div class="step-row" data-step="6">
+      <span class="step-num">6</span>
+      <button class="step-btn" data-step="6">Login ChatGPT</button>
+      <span class="step-status" data-step="6">⬚</span>
+    </div>
+    <div class="step-row" data-step="7">
+      <span class="step-num">7</span>
+      <button class="step-btn" data-step="7">Get Login Code</button>
+      <span class="step-status" data-step="7">⬚</span>
+    </div>
+    <div class="step-row" data-step="8">
+      <span class="step-num">8</span>
+      <button class="step-btn" data-step="8">Complete OAuth</button>
+      <span class="step-status" data-step="8">⬚</span>
+    </div>
+    <div class="step-row" data-step="9">
+      <span class="step-num">9</span>
+      <button class="step-btn" data-step="9">VPS Verify</button>
+      <span class="step-status" data-step="9">⬚</span>
+    </div>
+  </section>
+
+  <section id="log-section">
+    <h2>Log</h2>
+    <div id="log-area"></div>
+  </section>
+
+  <script src="sidepanel.js"></script>
+</body>
+</html>
+```
+
+- [ ] **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

+ 359 - 0
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

+ 10 - 14
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/*"
+    "<all_urls>"
   ],
   "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"],

+ 17 - 0
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;

+ 13 - 2
sidepanel/sidepanel.html

@@ -41,13 +41,24 @@
   <section id="data-section">
     <div class="data-card">
       <div class="data-row">
-        <span class="data-label">OAuth</span>
-        <span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
+        <span class="data-label">VPS</span>
+        <input type="text" id="input-vps-url" class="data-input" placeholder="http://ip:port/management.html#/oauth" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">Mail</span>
+        <select id="select-mail-provider" class="data-select">
+          <option value="qq">QQ Mail (wx.mail.qq.com)</option>
+          <option value="163">163 Mail (mail.163.com)</option>
+        </select>
       </div>
       <div class="data-row">
         <span class="data-label">Email</span>
         <input type="text" id="input-email" class="data-input" placeholder="Paste DuckDuckGo email" />
       </div>
+      <div class="data-row">
+        <span class="data-label">OAuth</span>
+        <span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
+      </div>
       <div class="data-row">
         <span class="data-label">Callback</span>
         <span id="display-localhost-url" class="data-value mono truncate">Waiting...</span>

+ 23 - 1
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
 // ============================================================