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