Files
FlowPilot/docs/superpowers/plans/2026-04-05-multi-page-automation-extension.md
T
unknown c6f1070ace feat: configurable VPS URL, 163 mail support, mail provider selector
- VPS URL: now an input field in Side Panel, no longer hardcoded
  - Dynamic script injection via chrome.scripting.executeScript
  - host_permissions changed to <all_urls> for flexibility
- 163 Mail: new content script (mail-163.js) with actual selectors
  - Mail items: div[sign="letter"], sender: .nui-user, subject: span.da0
  - Supports aria-label fallback for matching
- Mail provider selector: dropdown to switch between QQ Mail and 163 Mail
  - Background routes steps 4/7 to correct mail content script
- Settings persist in chrome.storage.session
2026-04-05 09:56:48 +08:00

2368 lines
76 KiB
Markdown

# Multi-Page Automation Chrome Extension — Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Chrome extension (MV3) that automates a 9-step OAuth registration workflow across VPS panel, OpenAI auth, QQ Mail, and ChatGPT, controlled via a persistent Side Panel.
**Architecture:** Side Panel (UI control) ↔ Background Service Worker (orchestration, state, tab management) ↔ Content Scripts (per-domain DOM automation). All state in `chrome.storage.session`. Unified message protocol across all components. Shared `utils.js` for DOM utilities.
**Tech Stack:** Chrome Extension Manifest V3, vanilla JS, Chrome APIs (sidePanel, tabs, webNavigation, storage, scripting)
**Spec:** `docs/superpowers/specs/2026-04-05-multi-page-automation-extension-design.md`
---
## File Structure
```
multiPagePlugins/
├── manifest.json # Extension manifest (MV3)
├── background.js # Service worker: orchestration, tab mgmt, state, message routing
├── sidepanel/
│ ├── sidepanel.html # Side Panel markup
│ ├── sidepanel.css # Side Panel styles
│ └── sidepanel.js # Side Panel logic: buttons, log, state restore
├── content/
│ ├── utils.js # Shared utilities: waitForElement, fillInput, log, report*
│ ├── vps-panel.js # Content script for VPS panel (steps 1, 9)
│ ├── signup-page.js # Content script for OpenAI auth (steps 2, 3, 4-receive, 5)
│ ├── qq-mail.js # Content script for QQ Mail (steps 4, 7)
│ └── chatgpt.js # Content script for ChatGPT (steps 6, 7-receive, 8)
├── data/
│ └── names.js # English first/last name lists for random generation
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── docs/
└── superpowers/
├── specs/...
└── plans/...
```
**Responsibilities by file:**
| File | Responsibility |
|------|---------------|
| `manifest.json` | Permissions, content script registration, side panel config |
| `background.js` | Message routing, tab registry, state persistence, step orchestration, webNavigation listener, random data generation |
| `sidepanel/sidepanel.js` | Render UI, handle button clicks, display logs, state restore on open, send step commands to background |
| `content/utils.js` | `waitForElement`, `fillInput`, `log`, `reportReady`, `reportComplete`, `reportError` |
| `content/vps-panel.js` | DOM ops on VPS panel for steps 1 and 9 |
| `content/signup-page.js` | DOM ops on OpenAI auth pages for steps 2, 3, 4(fill code), 5 |
| `content/qq-mail.js` | Email polling, code extraction for steps 4 and 7 |
| `content/chatgpt.js` | DOM ops on ChatGPT for steps 6, 7(fill code), 8 |
| `data/names.js` | Static arrays of first names and last names |
---
## Chunk 1: Foundation (manifest, utils, background core, side panel shell)
### Task 1: Create manifest.json
**Files:**
- Create: `manifest.json`
- [ ] **Step 1: Write manifest.json**
```json
{
"manifest_version": 3,
"name": "Multi-Page Automation",
"version": "1.0.0",
"description": "Automates multi-step OAuth registration workflow",
"permissions": [
"sidePanel",
"tabs",
"webNavigation",
"storage",
"scripting",
"activeTab"
],
"host_permissions": [
"http://154.26.182.181:8317/*",
"https://auth0.openai.com/*",
"https://auth.openai.com/*",
"https://accounts.openai.com/*",
"https://mail.qq.com/*",
"https://wx.mail.qq.com/*",
"https://chatgpt.com/*",
"http://localhost/*"
],
"background": {
"service_worker": "background.js"
},
"side_panel": {
"default_path": "sidepanel/sidepanel.html"
},
"content_scripts": [
{
"matches": ["http://154.26.182.181:8317/*"],
"js": ["content/utils.js", "content/vps-panel.js"],
"run_at": "document_idle"
},
{
"matches": [
"https://auth0.openai.com/*",
"https://auth.openai.com/*",
"https://accounts.openai.com/*"
],
"js": ["content/utils.js", "content/signup-page.js"],
"run_at": "document_idle"
},
{
"matches": [
"https://mail.qq.com/*",
"https://wx.mail.qq.com/*"
],
"js": ["content/utils.js", "content/qq-mail.js"],
"all_frames": true,
"run_at": "document_idle"
},
{
"matches": ["https://chatgpt.com/*"],
"js": ["content/utils.js", "content/chatgpt.js"],
"run_at": "document_idle"
}
],
"action": {
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
```
- [ ] **Step 2: Create placeholder icons**
Create simple colored square PNGs at 16x16, 48x48, 128x128 (can use canvas or any generator). These are required for Chrome to load the extension.
- [ ] **Step 3: Verify manifest loads**
Load the extension in `chrome://extensions` (Developer mode → Load unpacked). Confirm no manifest errors.
- [ ] **Step 4: Commit**
```bash
git add manifest.json icons/
git commit -m "feat: add manifest.json with MV3 config and placeholder icons"
```
---
### Task 2: Create shared utils.js
**Files:**
- Create: `content/utils.js`
- [ ] **Step 1: Write utils.js with all shared functions**
```js
// content/utils.js — Shared utilities for all content scripts
const SCRIPT_SOURCE = (() => {
const url = location.href;
if (url.includes('154.26.182.181')) return 'vps-panel';
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
if (url.includes('mail.qq.com')) return 'qq-mail';
if (url.includes('chatgpt.com')) return 'chatgpt';
return 'unknown';
})();
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
/**
* Wait for a DOM element to appear.
* @param {string} selector - CSS selector
* @param {number} timeout - Max wait time in ms (default 10000)
* @returns {Promise<Element>}
*/
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const existing = document.querySelector(selector);
if (existing) {
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
log(`Found element: ${selector}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
log(`Waiting for selector: ${selector}...`);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
clearTimeout(timer);
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
log(`Found element: ${selector}`);
resolve(el);
}
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
const timer = setTimeout(() => {
observer.disconnect();
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
});
}
/**
* React-compatible form filling.
* Sets value via native setter and dispatches input + change events.
* @param {HTMLInputElement} el
* @param {string} value
*/
function fillInput(el, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
nativeInputValueSetter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
console.log(LOG_PREFIX, `Filled input ${el.name || el.id || el.type} with: ${value}`);
log(`Filled input [${el.name || el.id || el.type || 'unknown'}]`);
}
/**
* Send a log message to Side Panel via Background.
* @param {string} message
* @param {string} level - 'info' | 'ok' | 'warn' | 'error'
*/
function log(message, level = 'info') {
chrome.runtime.sendMessage({
type: 'LOG',
source: SCRIPT_SOURCE,
step: null,
payload: { message, level, timestamp: Date.now() },
error: null,
});
}
/**
* Report that this content script is loaded and ready.
*/
function reportReady() {
console.log(LOG_PREFIX, 'Content script ready');
chrome.runtime.sendMessage({
type: 'CONTENT_SCRIPT_READY',
source: SCRIPT_SOURCE,
step: null,
payload: {},
error: null,
});
}
/**
* Report step completion.
* @param {number} step
* @param {Object} data - Step output data
*/
function reportComplete(step, data = {}) {
console.log(LOG_PREFIX, `Step ${step} completed`, data);
log(`Step ${step} completed successfully`, 'ok');
chrome.runtime.sendMessage({
type: 'STEP_COMPLETE',
source: SCRIPT_SOURCE,
step,
payload: data,
error: null,
});
}
/**
* Report step error.
* @param {number} step
* @param {string} errorMessage
*/
function reportError(step, errorMessage) {
console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`);
log(`Step ${step} failed: ${errorMessage}`, 'error');
chrome.runtime.sendMessage({
type: 'STEP_ERROR',
source: SCRIPT_SOURCE,
step,
payload: {},
error: errorMessage,
});
}
/**
* Simulate a click with proper event dispatching.
* @param {Element} el
*/
function simulateClick(el) {
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
}
// Auto-report ready on load
reportReady();
```
- [ ] **Step 2: Reload extension, visit any matched URL**
Load extension in Chrome. Open `http://154.26.182.181:8317/management.html#/oauth`. Check the Background service worker console in `chrome://extensions` — should see the `CONTENT_SCRIPT_READY` message from vps-panel.
- [ ] **Step 3: Commit**
```bash
git add content/utils.js
git commit -m "feat: add shared utils.js with waitForElement, fillInput, logging, and ready protocol"
```
---
### Task 3: Create name data
**Files:**
- Create: `data/names.js`
- [ ] **Step 1: Write names.js**
```js
// data/names.js — English name lists for random generation
const FIRST_NAMES = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Christopher',
'Mary', 'Patricia', 'Jennifer', 'Linda', 'Barbara', 'Elizabeth', 'Susan', 'Jessica', 'Sarah', 'Karen',
'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Andrew', 'Paul', 'Joshua', 'Kenneth',
'Emma', 'Olivia', 'Ava', 'Isabella', 'Sophia', 'Mia', 'Charlotte', 'Amelia', 'Harper', 'Evelyn',
];
const LAST_NAMES = [
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez',
'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin',
'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson',
];
/**
* Generate a random full name.
* @returns {{ firstName: string, lastName: string }}
*/
function generateRandomName() {
const firstName = FIRST_NAMES[Math.floor(Math.random() * FIRST_NAMES.length)];
const lastName = LAST_NAMES[Math.floor(Math.random() * LAST_NAMES.length)];
return { firstName, lastName };
}
/**
* Generate a random birthday (age 19-25).
* @returns {{ year: number, month: number, day: number }}
*/
function generateRandomBirthday() {
const currentYear = new Date().getFullYear();
const age = 19 + Math.floor(Math.random() * 7); // 19 to 25
const year = currentYear - age;
const month = 1 + Math.floor(Math.random() * 12); // 1 to 12
const maxDay = new Date(year, month, 0).getDate(); // days in that month
const day = 1 + Math.floor(Math.random() * maxDay);
return { year, month, day };
}
```
- [ ] **Step 2: Commit**
```bash
git add data/names.js
git commit -m "feat: add random English name and birthday generation data"
```
---
### Task 4: Create Background Service Worker — core infrastructure
**Files:**
- Create: `background.js`
This task covers the core infrastructure: state management, tab registry, message routing, and command queuing. Step orchestration (per-step logic) is added in later tasks.
- [ ] **Step 1: Write background.js core**
```js
// background.js — Service Worker: orchestration, state, tab management, message routing
const LOG_PREFIX = '[MultiPage:bg]';
// ============================================================
// State Management (chrome.storage.session)
// ============================================================
const DEFAULT_STATE = {
currentStep: 0,
stepStatuses: { 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending', 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending' },
oauthUrl: null,
email: null,
password: 'mimashisha0.0',
lastEmailTimestamp: null,
localhostUrl: null,
flowStartTime: null,
tabRegistry: {},
logs: [],
};
async function getState() {
const state = await chrome.storage.session.get(null);
return { ...DEFAULT_STATE, ...state };
}
async function setState(updates) {
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
await chrome.storage.session.set(updates);
}
async function resetState() {
console.log(LOG_PREFIX, 'Resetting all state');
await chrome.storage.session.clear();
await chrome.storage.session.set({ ...DEFAULT_STATE });
}
// ============================================================
// Tab Registry
// ============================================================
async function getTabRegistry() {
const state = await getState();
return state.tabRegistry || {};
}
async function registerTab(source, tabId) {
const registry = await getTabRegistry();
registry[source] = { tabId, ready: true };
await setState({ tabRegistry: registry });
console.log(LOG_PREFIX, `Tab registered: ${source}${tabId}`);
}
async function isTabAlive(source) {
const registry = await getTabRegistry();
const entry = registry[source];
if (!entry) return false;
try {
await chrome.tabs.get(entry.tabId);
return true;
} catch {
// Tab no longer exists
registry[source] = null;
await setState({ tabRegistry: registry });
return false;
}
}
async function getTabId(source) {
const registry = await getTabRegistry();
return registry[source]?.tabId || null;
}
// ============================================================
// Command Queue (for content scripts not yet ready)
// ============================================================
const pendingCommands = new Map(); // source → { message, resolve, reject, timer }
function queueCommand(source, message, timeout = 15000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingCommands.delete(source);
const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
console.error(LOG_PREFIX, err);
reject(new Error(err));
}, timeout);
pendingCommands.set(source, { message, resolve, reject, timer });
console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
});
}
function flushCommand(source, tabId) {
const pending = pendingCommands.get(source);
if (pending) {
clearTimeout(pending.timer);
pendingCommands.delete(source);
chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
}
}
// ============================================================
// Send command to content script (with readiness check)
// ============================================================
async function sendToContentScript(source, message) {
const registry = await getTabRegistry();
const entry = registry[source];
if (!entry || !entry.ready) {
console.log(LOG_PREFIX, `${source} not ready, queuing command`);
return queueCommand(source, message);
}
// Verify tab is still alive
const alive = await isTabAlive(source);
if (!alive) {
throw new Error(`Tab [${source}] was closed. Will reopen on retry.`);
}
console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
return chrome.tabs.sendMessage(entry.tabId, message);
}
// ============================================================
// Logging
// ============================================================
async function addLog(message, level = 'info') {
const state = await getState();
const logs = state.logs || [];
const entry = { message, level, timestamp: Date.now() };
logs.push(entry);
// Keep last 500 logs
if (logs.length > 500) logs.splice(0, logs.length - 500);
await setState({ logs });
// Broadcast to side panel
chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
}
// ============================================================
// Step Status Management
// ============================================================
async function setStepStatus(step, status) {
const state = await getState();
const statuses = { ...state.stepStatuses };
statuses[step] = status;
await setState({ stepStatuses: statuses, currentStep: step });
// Broadcast to side panel
chrome.runtime.sendMessage({
type: 'STEP_STATUS_CHANGED',
payload: { step, status },
}).catch(() => {});
}
// ============================================================
// Message Handler (central router)
// ============================================================
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
handleMessage(message, sender).then(response => {
sendResponse(response);
}).catch(err => {
console.error(LOG_PREFIX, 'Handler error:', err);
sendResponse({ error: err.message });
});
return true; // async response
});
async function handleMessage(message, sender) {
switch (message.type) {
case 'CONTENT_SCRIPT_READY': {
const tabId = sender.tab?.id;
if (tabId && message.source) {
await registerTab(message.source, tabId);
flushCommand(message.source, tabId);
await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
}
return { ok: true };
}
case 'LOG': {
const { message: msg, level } = message.payload;
await addLog(`[${message.source}] ${msg}`, level);
return { ok: true };
}
case 'STEP_COMPLETE': {
await setStepStatus(message.step, 'completed');
await addLog(`Step ${message.step} completed`, 'ok');
// Store step-specific data
await handleStepData(message.step, message.payload);
return { ok: true };
}
case 'STEP_ERROR': {
await setStepStatus(message.step, 'failed');
await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
return { ok: true };
}
case 'GET_STATE': {
return await getState();
}
case 'RESET': {
await resetState();
await addLog('Flow reset', 'info');
return { ok: true };
}
case 'EXECUTE_STEP': {
const step = message.payload.step;
await executeStep(step);
return { ok: true };
}
default:
console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
return { error: `Unknown message type: ${message.type}` };
}
}
// ============================================================
// Step Data Handlers
// ============================================================
async function handleStepData(step, payload) {
switch (step) {
case 1:
if (payload.oauthUrl) await setState({ oauthUrl: payload.oauthUrl });
break;
case 3:
if (payload.email) await setState({ email: payload.email });
break;
case 4:
if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
break;
case 8:
if (payload.localhostUrl) await setState({ localhostUrl: payload.localhostUrl });
break;
}
}
// ============================================================
// Step Execution (stub — each step implemented in later tasks)
// ============================================================
async function executeStep(step) {
console.log(LOG_PREFIX, `Executing step ${step}`);
await setStepStatus(step, 'running');
await addLog(`Step ${step} started`);
const state = await getState();
// Set flow start time on first step
if (step === 1 && !state.flowStartTime) {
await setState({ flowStartTime: Date.now() });
}
try {
switch (step) {
case 1: await executeStep1(state); break;
case 2: await executeStep2(state); break;
case 3: await executeStep3(state); break;
case 4: await executeStep4(state); break;
case 5: await executeStep5(state); break;
case 6: await executeStep6(state); break;
case 7: await executeStep7(state); break;
case 8: await executeStep8(state); break;
case 9: await executeStep9(state); break;
default:
throw new Error(`Unknown step: ${step}`);
}
} catch (err) {
await setStepStatus(step, 'failed');
await addLog(`Step ${step} failed: ${err.message}`, 'error');
}
}
// Step stubs — implemented in subsequent tasks
async function executeStep1(state) { await sendToContentScript('vps-panel', { type: 'EXECUTE_STEP', step: 1, payload: {} }); }
async function executeStep2(state) { /* Task 7 */ }
async function executeStep3(state) { /* Task 7 */ }
async function executeStep4(state) { /* Task 8 */ }
async function executeStep5(state) { /* Task 7 */ }
async function executeStep6(state) { /* Task 9 */ }
async function executeStep7(state) { /* Task 8 */ }
async function executeStep8(state) { /* Task 9 */ }
async function executeStep9(state) { await sendToContentScript('vps-panel', { type: 'EXECUTE_STEP', step: 9, payload: {} }); }
// ============================================================
// Open Side Panel on extension icon click
// ============================================================
chrome.action.onClicked.addListener((tab) => {
chrome.sidePanel.open({ windowId: tab.windowId });
});
// Enable side panel for all URLs
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
```
- [ ] **Step 2: Reload extension, check service worker**
Reload in `chrome://extensions`. Click "Service Worker" link to open DevTools for Background. Confirm no errors. Check that `chrome.storage.session` is initialized (can run `chrome.storage.session.get(null)` in console).
- [ ] **Step 3: Commit**
```bash
git add background.js
git commit -m "feat: add background service worker with state management, tab registry, message routing, and command queue"
```
---
### Task 5: Create Side Panel (HTML + CSS + JS)
**Files:**
- Create: `sidepanel/sidepanel.html`
- Create: `sidepanel/sidepanel.css`
- Create: `sidepanel/sidepanel.js`
- [ ] **Step 1: Write sidepanel.html**
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Page Automation</title>
<link rel="stylesheet" href="sidepanel.css">
</head>
<body>
<header>
<h1>Multi-Page Automation</h1>
<button id="btn-reset" title="Reset all steps">Reset</button>
</header>
<section id="data-section">
<div class="data-row">
<label>OAuth URL:</label>
<span id="display-oauth-url" class="data-value">Not obtained</span>
</div>
<div class="data-row">
<label>Email:</label>
<input type="text" id="input-email" placeholder="Paste DuckDuckGo email here" />
</div>
<div class="data-row">
<label>Status:</label>
<span id="display-status" class="data-value">Waiting</span>
</div>
</section>
<section id="steps-section">
<div class="step-row" data-step="1">
<span class="step-num">1</span>
<button class="step-btn" data-step="1">Get OAuth Link</button>
<span class="step-status" data-step="1"></span>
</div>
<div class="step-row" data-step="2">
<span class="step-num">2</span>
<button class="step-btn" data-step="2">Open Signup</button>
<span class="step-status" data-step="2"></span>
</div>
<div class="step-row" data-step="3">
<span class="step-num">3</span>
<button class="step-btn" data-step="3">Fill Email/Password</button>
<span class="step-status" data-step="3"></span>
</div>
<div class="step-row" data-step="4">
<span class="step-num">4</span>
<button class="step-btn" data-step="4">Get Signup Code</button>
<span class="step-status" data-step="4"></span>
</div>
<div class="step-row" data-step="5">
<span class="step-num">5</span>
<button class="step-btn" data-step="5">Fill Name/Birthday</button>
<span class="step-status" data-step="5"></span>
</div>
<div class="step-row" data-step="6">
<span class="step-num">6</span>
<button class="step-btn" data-step="6">Login ChatGPT</button>
<span class="step-status" data-step="6"></span>
</div>
<div class="step-row" data-step="7">
<span class="step-num">7</span>
<button class="step-btn" data-step="7">Get Login Code</button>
<span class="step-status" data-step="7"></span>
</div>
<div class="step-row" data-step="8">
<span class="step-num">8</span>
<button class="step-btn" data-step="8">Complete OAuth</button>
<span class="step-status" data-step="8"></span>
</div>
<div class="step-row" data-step="9">
<span class="step-num">9</span>
<button class="step-btn" data-step="9">VPS Verify</button>
<span class="step-status" data-step="9"></span>
</div>
</section>
<section id="log-section">
<h2>Log</h2>
<div id="log-area"></div>
</section>
<script src="sidepanel.js"></script>
</body>
</html>
```
- [ ] **Step 2: Write sidepanel.css**
```css
/* sidepanel/sidepanel.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #333;
background: #f8f9fa;
padding: 12px;
width: 100%;
min-height: 100vh;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
header h1 {
font-size: 15px;
font-weight: 600;
}
#btn-reset {
padding: 4px 10px;
font-size: 12px;
background: #dc3545;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
#btn-reset:hover { background: #c82333; }
/* Data Section */
#data-section {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 10px;
margin-bottom: 12px;
}
.data-row {
display: flex;
align-items: center;
margin-bottom: 6px;
}
.data-row:last-child { margin-bottom: 0; }
.data-row label {
width: 80px;
font-weight: 600;
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.data-value {
font-size: 12px;
color: #999;
word-break: break-all;
}
#input-email {
flex: 1;
padding: 4px 8px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 12px;
}
/* Steps Section */
#steps-section {
margin-bottom: 12px;
}
.step-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.step-num {
width: 20px;
text-align: center;
font-weight: 600;
font-size: 12px;
color: #666;
}
.step-btn {
flex: 1;
padding: 6px 10px;
font-size: 12px;
background: #007bff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: left;
}
.step-btn:hover:not(:disabled) { background: #0069d9; }
.step-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.step-status {
width: 24px;
text-align: center;
font-size: 14px;
}
/* Log Section */
#log-section h2 {
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
}
#log-area {
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
padding: 8px;
border-radius: 6px;
height: 250px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.log-info { color: #d4d4d4; }
.log-ok { color: #4ec9b0; }
.log-warn { color: #dcdcaa; }
.log-error { color: #f44747; }
```
- [ ] **Step 3: Write sidepanel.js**
```js
// sidepanel/sidepanel.js — Side Panel logic
const STATUS_ICONS = {
pending: '⬚',
running: '⏳',
completed: '✅',
failed: '❌',
};
const logArea = document.getElementById('log-area');
const displayOauthUrl = document.getElementById('display-oauth-url');
const displayStatus = document.getElementById('display-status');
const inputEmail = document.getElementById('input-email');
const btnReset = document.getElementById('btn-reset');
// ============================================================
// State Restore on load
// ============================================================
async function restoreState() {
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
// Restore data fields
if (state.oauthUrl) {
displayOauthUrl.textContent = state.oauthUrl;
displayOauthUrl.style.color = '#333';
}
if (state.email) {
inputEmail.value = state.email;
}
// Restore step statuses
if (state.stepStatuses) {
for (const [step, status] of Object.entries(state.stepStatuses)) {
updateStepUI(Number(step), status);
}
}
// Restore logs
if (state.logs) {
for (const entry of state.logs) {
appendLog(entry);
}
}
updateStatusDisplay(state);
}
// ============================================================
// UI Updates
// ============================================================
function updateStepUI(step, status) {
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
const btnEl = document.querySelector(`.step-btn[data-step="${step}"]`);
if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '⬚';
// Interlock logic
updateButtonStates();
}
function updateButtonStates() {
// Get all current statuses from DOM
const statuses = {};
document.querySelectorAll('.step-status').forEach(el => {
const step = Number(el.dataset.step);
const icon = el.textContent;
const status = Object.entries(STATUS_ICONS).find(([, v]) => v === icon)?.[0] || 'pending';
statuses[step] = status;
});
// Find if any step is running
const anyRunning = Object.values(statuses).some(s => s === 'running');
for (let step = 1; step <= 9; step++) {
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
if (!btn) continue;
if (anyRunning) {
// When any step is running, disable all buttons
btn.disabled = true;
} else if (step === 1) {
// Step 1 is always available (unless running)
btn.disabled = false;
} else {
// Steps 2-9: enabled if previous step completed (or current step failed for retry)
const prevStatus = statuses[step - 1];
const currentStatus = statuses[step];
btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed');
}
}
}
function updateStatusDisplay(state) {
if (!state.stepStatuses) return;
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
if (running) {
displayStatus.textContent = `Step ${running[0]} running...`;
} else {
const lastCompleted = Object.entries(state.stepStatuses)
.filter(([, s]) => s === 'completed')
.map(([k]) => Number(k))
.sort((a, b) => b - a)[0];
if (lastCompleted === 9) {
displayStatus.textContent = 'All steps completed!';
} else if (lastCompleted) {
displayStatus.textContent = `Step ${lastCompleted} done. Ready for step ${lastCompleted + 1}.`;
} else {
displayStatus.textContent = 'Waiting';
}
}
}
function appendLog(entry) {
const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
const levelLabel = entry.level.toUpperCase().padEnd(5);
const line = document.createElement('div');
line.className = `log-${entry.level}`;
line.textContent = `${time} [${levelLabel}] ${entry.message}`;
logArea.appendChild(line);
logArea.scrollTop = logArea.scrollHeight;
}
// ============================================================
// Button Handlers
// ============================================================
document.querySelectorAll('.step-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const step = Number(btn.dataset.step);
// Save email if step 3 and email input has value
if (step === 3) {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
return;
}
await chrome.runtime.sendMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step, email },
});
} else {
await chrome.runtime.sendMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step },
});
}
});
});
// Reset button
btnReset.addEventListener('click', async () => {
if (confirm('Reset all steps and data?')) {
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
// Clear UI
displayOauthUrl.textContent = 'Not obtained';
displayOauthUrl.style.color = '#999';
inputEmail.value = '';
displayStatus.textContent = 'Waiting';
logArea.innerHTML = '';
document.querySelectorAll('.step-status').forEach(el => el.textContent = '⬚');
updateButtonStates();
}
});
// ============================================================
// Listen for Background broadcasts
// ============================================================
chrome.runtime.onMessage.addListener((message) => {
switch (message.type) {
case 'LOG_ENTRY':
appendLog(message.payload);
break;
case 'STEP_STATUS_CHANGED': {
const { step, status } = message.payload;
updateStepUI(step, status);
// Update status display
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
// Update data displays
if (status === 'completed') {
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
if (state.oauthUrl) {
displayOauthUrl.textContent = state.oauthUrl;
displayOauthUrl.style.color = '#333';
}
});
}
break;
}
}
});
// ============================================================
// Init
// ============================================================
restoreState().then(() => {
updateButtonStates();
});
```
- [ ] **Step 4: Reload extension, open Side Panel**
Click the extension icon → Side Panel should open on the right. Verify:
- All 9 step buttons visible
- Only step 1 button enabled, rest disabled
- Log area is empty
- Reset button is present
- Email input field is editable
- [ ] **Step 5: Test Reset button**
Click Reset → confirm dialog → all steps should reset to ⬚.
- [ ] **Step 6: Commit**
```bash
git add sidepanel/
git commit -m "feat: add Side Panel UI with step buttons, log area, state restore, and interlock logic"
```
---
## Chunk 2: Content Scripts — VPS Panel & Signup Page
### Task 6: Create vps-panel.js content script
**Files:**
- Create: `content/vps-panel.js`
- [ ] **Step 1: Write vps-panel.js**
```js
// content/vps-panel.js — Content script for VPS panel (steps 1, 9)
// Injected on: http://154.26.182.181:8317/*
console.log('[MultiPage:vps-panel] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP') {
handleStep(message.step, message.payload).then(() => {
sendResponse({ ok: true });
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
async function handleStep(step, payload) {
switch (step) {
case 1: return await step1_getOAuthLink();
case 9: return await step9_vpsVerify(payload);
default:
throw new Error(`vps-panel.js does not handle step ${step}`);
}
}
// ============================================================
// Step 1: Get OAuth Link
// ============================================================
async function step1_getOAuthLink() {
log('Step 1: Checking VPS panel login state...');
// TODO: Adjust selector after inspecting actual VPS panel DOM
// Check login state — look for a known element that only appears when logged in
// For now, we check if the page has loaded the management content
const pageContent = document.body.innerText;
if (pageContent.includes('login') && !pageContent.includes('oauth')) {
throw new Error('VPS panel not logged in. Please log in to VPS panel at http://154.26.182.181:8317 and retry.');
}
log('Step 1: Looking for OAuth login button...');
// TODO: Adjust selectors based on actual VPS panel DOM structure
// These are placeholder selectors — must be updated during debugging
const oauthBtn = await waitForElement('[data-action="oauth-login"], .oauth-login-btn, button:has-text("OAuth")', 10000)
.catch(() => {
// Fallback: try to find any button with OAuth-related text
const buttons = [...document.querySelectorAll('button, a, [role="button"]')];
const btn = buttons.find(b => /oauth/i.test(b.textContent));
if (btn) return btn;
throw new Error('Could not find OAuth login button. Check VPS panel page structure in DevTools.');
});
simulateClick(oauthBtn);
log('Step 1: Clicked OAuth login, waiting for Codex login...');
// Wait for Codex login option to appear
await new Promise(r => setTimeout(r, 1000)); // Brief wait for UI transition
const codexBtn = await waitForElement('[data-action="codex-login"], .codex-login-btn', 10000)
.catch(() => {
const buttons = [...document.querySelectorAll('button, a, [role="button"]')];
const btn = buttons.find(b => /codex/i.test(b.textContent));
if (btn) return btn;
throw new Error('Could not find Codex login button. Check VPS panel DOM after clicking OAuth.');
});
simulateClick(codexBtn);
log('Step 1: Clicked Codex login, waiting for auth URL...');
// Wait for the auth URL to appear in the page
await new Promise(r => setTimeout(r, 2000)); // Wait for URL generation
// TODO: Adjust how the URL is extracted — may be in a text field, link, or copied to clipboard
const urlElement = await waitForElement('input[readonly], .auth-url, textarea, code', 10000)
.catch(() => {
// Fallback: search all text nodes for a URL matching auth pattern
throw new Error('Could not find auth URL element. Check VPS panel DOM for the generated URL.');
});
const oauthUrl = urlElement.value || urlElement.textContent || urlElement.innerText;
if (!oauthUrl || !oauthUrl.startsWith('http')) {
throw new Error(`Invalid OAuth URL found: "${oauthUrl?.slice(0, 50)}". Expected URL starting with http.`);
}
log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`);
reportComplete(1, { oauthUrl: oauthUrl.trim() });
}
// ============================================================
// Step 9: VPS Verify
// ============================================================
async function step9_vpsVerify(payload) {
log('Step 9: Looking for URL input field on VPS panel...');
// Get localhostUrl from storage (passed via Background)
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
const localhostUrl = state.localhostUrl;
if (!localhostUrl) {
throw new Error('No localhost URL found. Complete step 8 first.');
}
// TODO: Adjust selector for the URL input field on VPS panel
const urlInput = await waitForElement('input[placeholder*="localhost"], input[name="callback_url"], .callback-url-input', 10000)
.catch(() => {
throw new Error('Could not find URL input field on VPS panel. Check DOM structure.');
});
fillInput(urlInput, localhostUrl);
log(`Step 9: Filled URL input with: ${localhostUrl}`);
// Find and click verify button
const verifyBtn = await waitForElement('button:has-text("verify"), .verify-btn, [data-action="verify"]', 10000)
.catch(() => {
const buttons = [...document.querySelectorAll('button, [role="button"]')];
const btn = buttons.find(b => /verif|确认|验证/i.test(b.textContent));
if (btn) return btn;
throw new Error('Could not find verify button. Check VPS panel DOM.');
});
simulateClick(verifyBtn);
log('Step 9: Clicked verify button');
reportComplete(9);
}
```
- [ ] **Step 2: Reload extension, open VPS panel**
Navigate to `http://154.26.182.181:8317/management.html#/oauth`. Open DevTools console. Confirm `[MultiPage:vps-panel] Content script loaded` appears.
- [ ] **Step 3: Test step 1 via Side Panel**
Open Side Panel → click "Get OAuth Link" → observe logs. **Expected**: Will likely fail on selectors (TODO placeholders). Use DevTools to inspect actual DOM, note the correct selectors, then update the script.
- [ ] **Step 4: Commit**
```bash
git add content/vps-panel.js
git commit -m "feat: add vps-panel content script for steps 1 (get OAuth link) and 9 (VPS verify)"
```
---
### Task 7: Create signup-page.js content script + wire up steps 2, 3, 5
**Files:**
- Create: `content/signup-page.js`
- Modify: `background.js` — implement `executeStep2`, `executeStep3`, `executeStep5`
- [ ] **Step 1: Write signup-page.js**
```js
// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5)
// Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
console.log('[MultiPage:signup-page] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE') {
handleCommand(message).then(() => {
sendResponse({ ok: true });
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
async function handleCommand(message) {
switch (message.type) {
case 'EXECUTE_STEP':
switch (message.step) {
case 2: return await step2_clickRegister();
case 3: return await step3_fillEmailPassword(message.payload);
case 5: return await step5_fillNameBirthday(message.payload);
default: throw new Error(`signup-page.js does not handle step ${message.step}`);
}
case 'FILL_CODE':
return await step4_fillVerificationCode(message.payload);
}
}
// ============================================================
// Step 2: Click Register
// ============================================================
async function step2_clickRegister() {
log('Step 2: Looking for Register/Sign up button...');
// TODO: Adjust selectors based on actual OpenAI auth page
const registerBtn = await waitForElement('a[href*="signup"], button:has-text("Sign up"), [data-action="signup"]', 10000)
.catch(() => {
const links = [...document.querySelectorAll('a, button')];
const btn = links.find(b => /sign\s*up|register|注册|create/i.test(b.textContent));
if (btn) return btn;
throw new Error('Could not find Register/Sign up button. Check auth page DOM. URL: ' + location.href);
});
simulateClick(registerBtn);
log('Step 2: Clicked Register button');
reportComplete(2);
}
// ============================================================
// Step 3: Fill Email & Password
// ============================================================
async function step3_fillEmailPassword(payload) {
const { email } = payload;
if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
log(`Step 3: Filling email: ${email}`);
// TODO: Adjust selectors
const emailInput = await waitForElement('input[type="email"], input[name="email"], input[name="username"]', 10000)
.catch(() => { throw new Error('Could not find email input field on signup page. URL: ' + location.href); });
fillInput(emailInput, email);
log('Step 3: Email filled');
// Some signup flows show email first, then password on next page
// Try to find password field — if not found, submit email first
let passwordInput = document.querySelector('input[type="password"]');
if (!passwordInput) {
// Maybe need to submit email first to get to password page
log('Step 3: No password field yet, looking for continue/submit button...');
const submitBtn = await waitForElement('button[type="submit"], input[type="submit"]', 5000)
.catch(() => {
const buttons = [...document.querySelectorAll('button')];
return buttons.find(b => /continue|next|submit|继续/i.test(b.textContent));
});
if (submitBtn) {
simulateClick(submitBtn);
log('Step 3: Submitted email, waiting for password field...');
await new Promise(r => setTimeout(r, 2000));
}
passwordInput = await waitForElement('input[type="password"]', 10000)
.catch(() => { throw new Error('Could not find password input after submitting email. URL: ' + location.href); });
}
fillInput(passwordInput, 'mimashisha0.0');
log('Step 3: Password filled');
// Submit
const submitBtn = await waitForElement('button[type="submit"], input[type="submit"]', 5000)
.catch(() => {
const buttons = [...document.querySelectorAll('button')];
return buttons.find(b => /continue|sign\s*up|submit|注册|创建/i.test(b.textContent));
});
if (submitBtn) {
simulateClick(submitBtn);
log('Step 3: Form submitted');
}
reportComplete(3, { email });
}
// ============================================================
// Step 4 (receiving end): Fill Verification Code
// ============================================================
async function step4_fillVerificationCode(payload) {
const { code } = payload;
if (!code) throw new Error('No verification code provided.');
log(`Step 4: Filling verification code: ${code}`);
// TODO: Adjust selector for code input
const codeInput = await waitForElement('input[name="code"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"]', 10000)
.catch(() => { throw new Error('Could not find verification code input. URL: ' + location.href); });
fillInput(codeInput, code);
log('Step 4: Code filled');
// Submit
const submitBtn = document.querySelector('button[type="submit"]')
|| [...document.querySelectorAll('button')].find(b => /verify|confirm|submit|continue|确认|验证/i.test(b.textContent));
if (submitBtn) {
simulateClick(submitBtn);
log('Step 4: Verification submitted');
}
// Wait for page transition
await new Promise(r => setTimeout(r, 2000));
reportComplete(4);
}
// ============================================================
// Step 5: Fill Name & Birthday
// ============================================================
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, year, month, day } = payload;
if (!firstName || !lastName) throw new Error('No name data provided.');
log(`Step 5: Filling name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
// TODO: Adjust selectors based on actual profile form
// First name
const firstNameInput = await waitForElement('input[name="firstName"], input[name="first_name"], input[placeholder*="first"]', 10000)
.catch(() => { throw new Error('Could not find first name input. URL: ' + location.href); });
fillInput(firstNameInput, firstName);
// Last name
const lastNameInput = await waitForElement('input[name="lastName"], input[name="last_name"], input[placeholder*="last"]', 5000)
.catch(() => { throw new Error('Could not find last name input. URL: ' + location.href); });
fillInput(lastNameInput, lastName);
// Birthday — could be separate fields or dropdowns
// TODO: This varies greatly by form. Adjust after inspecting actual page.
const birthdayInput = document.querySelector('input[name="birthday"], input[type="date"], input[name="dob"]');
if (birthdayInput) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
fillInput(birthdayInput, dateStr);
log(`Step 5: Birthday filled: ${dateStr}`);
} else {
// Try separate month/day/year selects or inputs
log('Step 5: Looking for separate birthday fields...');
const monthInput = document.querySelector('select[name="month"], input[name="month"]');
const dayInput = document.querySelector('select[name="day"], input[name="day"]');
const yearInput = document.querySelector('select[name="year"], input[name="year"]');
if (monthInput) {
if (monthInput.tagName === 'SELECT') { monthInput.value = String(month); monthInput.dispatchEvent(new Event('change', { bubbles: true })); }
else fillInput(monthInput, String(month));
}
if (dayInput) {
if (dayInput.tagName === 'SELECT') { dayInput.value = String(day); dayInput.dispatchEvent(new Event('change', { bubbles: true })); }
else fillInput(dayInput, String(day));
}
if (yearInput) {
if (yearInput.tagName === 'SELECT') { yearInput.value = String(year); yearInput.dispatchEvent(new Event('change', { bubbles: true })); }
else fillInput(yearInput, String(year));
}
if (!monthInput && !dayInput && !yearInput) {
log('Step 5: WARNING — Could not find any birthday fields. May need to adjust selectors.', 'warn');
}
}
// Submit / Complete
const completeBtn = document.querySelector('button[type="submit"]')
|| [...document.querySelectorAll('button')].find(b => /complete|continue|finish|done|create|完成|创建/i.test(b.textContent));
if (completeBtn) {
simulateClick(completeBtn);
log('Step 5: Profile form submitted');
}
await new Promise(r => setTimeout(r, 2000));
reportComplete(5);
}
```
- [ ] **Step 2: Wire up executeStep2, executeStep3, executeStep5 in background.js**
Replace the stubs in `background.js`:
```js
async function executeStep2(state) {
if (!state.oauthUrl) {
throw new Error('No OAuth URL. Complete step 1 first.');
}
await addLog(`Step 2: Opening auth URL in new tab: ${state.oauthUrl.slice(0, 80)}...`);
const tab = await chrome.tabs.create({ url: state.oauthUrl, active: true });
// signup-page.js will auto-inject via manifest content_scripts
// When it reports ready, Background queues step 2 command
// But signup-page.js step 2 is triggered after READY signal
// So we queue the command now — it will flush when script is ready
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 2,
source: 'background',
payload: {},
});
}
async function executeStep3(state) {
if (!state.email) {
throw new Error('No email address. Paste email in Side Panel first.');
}
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 3,
source: 'background',
payload: { email: state.email },
});
}
async function executeStep5(state) {
// Generate random name and birthday
// Import names data (loaded via importScripts or inline)
const { firstName, lastName } = generateRandomName();
const { year, month, day } = generateRandomBirthday();
await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 5,
source: 'background',
payload: { firstName, lastName, year, month, day },
});
}
```
Also add to the top of `background.js`, after the LOG_PREFIX line:
```js
importScripts('data/names.js');
```
And update the `EXECUTE_STEP` handler in `handleMessage` to save email when step 3 is called from sidepanel:
```js
case 'EXECUTE_STEP': {
const step = message.payload.step;
// Save email if provided (from side panel step 3)
if (message.payload.email) {
await setState({ email: message.payload.email });
}
await executeStep(step);
return { ok: true };
}
```
- [ ] **Step 3: Reload extension, verify no errors**
Reload extension. Open Service Worker DevTools — confirm no syntax errors. Open Side Panel — confirm step buttons still work.
- [ ] **Step 4: Commit**
```bash
git add content/signup-page.js background.js data/names.js
git commit -m "feat: add signup-page content script (steps 2,3,4-fill,5) and wire up background orchestration"
```
---
## Chunk 3: Content Scripts — QQ Mail & ChatGPT
### Task 8: Create qq-mail.js content script + wire up steps 4, 7
**Files:**
- Create: `content/qq-mail.js`
- Modify: `background.js` — implement `executeStep4`, `executeStep7`
- [ ] **Step 1: Write qq-mail.js**
```js
// content/qq-mail.js — Content script for QQ Mail (steps 4, 7)
// Injected on: mail.qq.com, wx.mail.qq.com
// NOTE: all_frames: true — this script runs in every frame on QQ Mail
console.log('[MultiPage:qq-mail] Content script loaded on', location.href, 'frame:', window === window.top ? 'top' : 'child');
// Only act in the correct frame:
// - wx.mail.qq.com (new version): top frame only
// - mail.qq.com (old version): need to find the inbox frame
const isNewVersion = location.hostname === 'wx.mail.qq.com';
const isTopFrame = window === window.top;
// For old QQ Mail, only act in the frame that contains the inbox
// Skip reporting ready from irrelevant frames
if (!isNewVersion && isTopFrame) {
// Old version top frame — still report ready but inbox is in iframe
// The iframe instance of this script will handle actual email operations
console.log('[MultiPage:qq-mail] Old QQ Mail top frame — waiting for inbox frame');
}
// Detect if this frame contains the email list
function isInboxFrame() {
if (isNewVersion) return isTopFrame;
// Old version: check if this frame has email list elements
return !!document.querySelector('#mailList, .mail-list, [id*="mailList"]');
}
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
// Only handle in the correct frame
if (!isInboxFrame()) {
sendResponse({ ok: false, reason: 'wrong-frame' });
return;
}
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
sendResponse({ error: err.message });
});
return true;
}
if (message.type === 'CHECK_LOGIN') {
if (!isTopFrame) { sendResponse({ ok: false }); return; }
const loggedIn = checkLoginState();
sendResponse({ loggedIn });
return;
}
});
// ============================================================
// Login State Check
// ============================================================
function checkLoginState() {
if (isNewVersion) {
// wx.mail.qq.com: check for inbox/compose button
return !!document.querySelector('[class*="folder"], [class*="compose"], [class*="inbox"]');
} else {
// mail.qq.com: check for known logged-in element
return !!document.querySelector('#folder_1, .folder_inbox, #composebtn');
}
}
// ============================================================
// Email Polling
// ============================================================
async function handlePollEmail(step, payload) {
const { filterAfterTimestamp, senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`, 'info');
// Try to refresh inbox
await refreshInbox();
// Search for matching email
const result = await findMatchingEmail(filterAfterTimestamp, senderFilters, subjectFilters);
if (result) {
log(`Step ${step}: Found matching email! Extracting code...`);
const code = extractVerificationCode(result.content);
if (code) {
log(`Step ${step}: Verification code found: ${code}`, 'ok');
return { ok: true, code, emailTimestamp: Date.now() };
} else {
log(`Step ${step}: Email found but no 6-digit code in content. Content preview: ${result.content.slice(0, 100)}`, 'warn');
}
}
if (attempt < maxAttempts) {
await new Promise(r => setTimeout(r, intervalMs));
}
}
throw new Error(`No matching email found after ${maxAttempts * intervalMs / 1000}s. Check QQ Mail manually. Email may be in spam.`);
}
async function refreshInbox() {
if (isNewVersion) {
// wx.mail.qq.com: click refresh or trigger inbox reload
// TODO: Find the actual refresh button selector
const refreshBtn = document.querySelector('[class*="refresh"], [title*="刷新"], button[aria-label*="refresh"]');
if (refreshBtn) {
simulateClick(refreshBtn);
await new Promise(r => setTimeout(r, 500));
}
} else {
// mail.qq.com: old version refresh
const refreshBtn = document.querySelector('#refresh, .refresh_btn');
if (refreshBtn) {
simulateClick(refreshBtn);
await new Promise(r => setTimeout(r, 500));
}
}
}
async function findMatchingEmail(afterTimestamp, senderFilters, subjectFilters) {
// Get email list items
// TODO: Adjust selectors for actual QQ Mail DOM structure
let emailItems;
if (isNewVersion) {
emailItems = document.querySelectorAll('[class*="mail-item"], [class*="list-item"], tr[class*="mail"]');
} else {
emailItems = document.querySelectorAll('.toarea tr, #mailList tr, .mail_list tr');
}
for (const item of emailItems) {
const text = item.textContent || '';
const senderMatch = senderFilters.some(f => text.toLowerCase().includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => text.toLowerCase().includes(f.toLowerCase()));
if (senderMatch || subjectMatch) {
// Try to get content from preview/snippet first
let content = text;
// If we need more content, click into the email
// TODO: May need to click the email item and wait for content to load
// For now, try to extract from the visible text
if (!extractVerificationCode(content)) {
// Click to open email for full content
simulateClick(item);
await new Promise(r => setTimeout(r, 1000));
// Read email body
const bodyEl = document.querySelector('[class*="mail-body"], [class*="mail_body"], .body_content, #contentDiv');
if (bodyEl) {
content = bodyEl.textContent || bodyEl.innerText || '';
}
}
return { content };
}
}
return null;
}
function extractVerificationCode(text) {
// Match 6-digit code (most common format for verification codes)
const match = text.match(/\b(\d{6})\b/);
return match ? match[1] : null;
}
```
- [ ] **Step 2: Wire up executeStep4 and executeStep7 in background.js**
Replace the stubs:
```js
async function executeStep4(state) {
// Check if QQ Mail tab exists, open if not
const alive = await isTabAlive('qq-mail');
if (!alive) {
await addLog('Step 4: Opening QQ Mail...');
await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true });
// Wait for content script ready — the sendToContentScript will queue
}
// First check login state
const tabId = await getTabId('qq-mail');
if (tabId) {
await chrome.tabs.update(tabId, { active: true });
}
// Send poll command
const result = await sendToContentScript('qq-mail', {
type: 'POLL_EMAIL',
step: 4,
source: 'background',
payload: {
filterAfterTimestamp: state.flowStartTime || 0,
senderFilters: ['openai', 'noreply', 'verify', 'auth'],
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
maxAttempts: 20,
intervalMs: 3000,
},
});
if (result.error) {
throw new Error(result.error);
}
if (result.code) {
await setState({ lastEmailTimestamp: result.emailTimestamp });
await addLog(`Step 4: Got verification code: ${result.code}`);
// Switch to signup tab and fill code
const signupTabId = await getTabId('signup-page');
if (signupTabId) {
await chrome.tabs.update(signupTabId, { active: true });
await sendToContentScript('signup-page', {
type: 'FILL_CODE',
step: 4,
source: 'background',
payload: { code: result.code },
});
} else {
throw new Error('Signup page tab was closed. Cannot fill verification code.');
}
}
}
async function executeStep7(state) {
const alive = await isTabAlive('qq-mail');
if (!alive) {
await addLog('Step 7: Opening QQ Mail...');
await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true });
}
const tabId = await getTabId('qq-mail');
if (tabId) {
await chrome.tabs.update(tabId, { active: true });
}
const result = await sendToContentScript('qq-mail', {
type: 'POLL_EMAIL',
step: 7,
source: 'background',
payload: {
filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'],
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
maxAttempts: 20,
intervalMs: 3000,
},
});
if (result.error) {
throw new Error(result.error);
}
if (result.code) {
await addLog(`Step 7: Got login verification code: ${result.code}`);
// Switch to ChatGPT tab and fill code
const chatgptTabId = await getTabId('chatgpt');
if (chatgptTabId) {
await chrome.tabs.update(chatgptTabId, { active: true });
await sendToContentScript('chatgpt', {
type: 'FILL_CODE',
step: 7,
source: 'background',
payload: { code: result.code },
});
} else {
throw new Error('ChatGPT tab was closed. Cannot fill verification code.');
}
}
}
```
- [ ] **Step 3: Reload extension, test qq-mail content script loads**
Navigate to `https://wx.mail.qq.com/`. Open DevTools console. Confirm `[MultiPage:qq-mail] Content script loaded` appears.
- [ ] **Step 4: Commit**
```bash
git add content/qq-mail.js background.js
git commit -m "feat: add qq-mail content script with email polling and wire up steps 4, 7 orchestration"
```
---
### Task 9: Create chatgpt.js content script + wire up steps 6, 8
**Files:**
- Create: `content/chatgpt.js`
- Modify: `background.js` — implement `executeStep6`, `executeStep8`
- [ ] **Step 1: Write chatgpt.js**
```js
// content/chatgpt.js — Content script for ChatGPT (steps 6, 7-receive, 8)
// Injected on: chatgpt.com
console.log('[MultiPage:chatgpt] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE') {
handleCommand(message).then(() => {
sendResponse({ ok: true });
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
async function handleCommand(message) {
switch (message.type) {
case 'EXECUTE_STEP':
switch (message.step) {
case 6: return await step6_loginChatGPT(message.payload);
case 8: return await step8_navigateOAuth(message.payload);
default: throw new Error(`chatgpt.js does not handle step ${message.step}`);
}
case 'FILL_CODE':
return await step7_fillLoginCode(message.payload);
}
}
// ============================================================
// Step 6: Login ChatGPT
// ============================================================
async function step6_loginChatGPT(payload) {
log('Step 6: Looking for login button on ChatGPT...');
// TODO: Adjust selectors based on actual ChatGPT login page
const loginBtn = await waitForElement('[data-testid="login-button"], a[href*="auth"], button', 10000)
.catch(() => {
const links = [...document.querySelectorAll('a, button')];
const btn = links.find(b => /log\s*in|sign\s*in|登录/i.test(b.textContent));
if (btn) return btn;
throw new Error('Could not find Login button on ChatGPT. URL: ' + location.href);
});
simulateClick(loginBtn);
log('Step 6: Clicked Login button, waiting for auth page...');
// Wait for redirect to auth page — email input should appear
await new Promise(r => setTimeout(r, 3000));
// Get email from storage
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
const email = state.email;
if (!email) throw new Error('No email found in state. Complete earlier steps first.');
// Find email input (may be on redirected auth page)
const emailInput = await waitForElement('input[type="email"], input[name="email"], input[name="username"]', 15000)
.catch(() => { throw new Error('Could not find email input on login page. URL: ' + location.href); });
fillInput(emailInput, email);
log(`Step 6: Filled email: ${email}`);
// Submit email
const submitBtn = document.querySelector('button[type="submit"]')
|| [...document.querySelectorAll('button')].find(b => /continue|next|submit/i.test(b.textContent));
if (submitBtn) simulateClick(submitBtn);
await new Promise(r => setTimeout(r, 2000));
// Check: password field or OTP?
const passwordInput = document.querySelector('input[type="password"]');
if (passwordInput) {
log('Step 6: Password field found, filling password...');
fillInput(passwordInput, state.password || 'mimashisha0.0');
const submitBtn2 = document.querySelector('button[type="submit"]')
|| [...document.querySelectorAll('button')].find(b => /continue|log\s*in|submit/i.test(b.textContent));
if (submitBtn2) simulateClick(submitBtn2);
await new Promise(r => setTimeout(r, 2000));
// Check if we need OTP after password
const codeInput = document.querySelector('input[name="code"], input[maxlength="6"]');
if (codeInput) {
log('Step 6: OTP code required after password. Waiting for step 7...');
reportComplete(6, { needsOTP: true });
} else {
log('Step 6: Login appears successful (no OTP needed)');
reportComplete(6, { needsOTP: false });
}
} else {
// OTP flow — no password
log('Step 6: No password field. OTP flow detected. Waiting for step 7...');
reportComplete(6, { needsOTP: true });
}
}
// ============================================================
// Step 7 (receiving end): Fill Login Verification Code
// ============================================================
async function step7_fillLoginCode(payload) {
const { code } = payload;
if (!code) throw new Error('No verification code provided.');
log(`Step 7: Filling login verification code: ${code}`);
const codeInput = await waitForElement('input[name="code"], input[maxlength="6"], input[type="text"][inputmode="numeric"]', 10000)
.catch(() => { throw new Error('Could not find verification code input on ChatGPT login. URL: ' + location.href); });
fillInput(codeInput, code);
const submitBtn = document.querySelector('button[type="submit"]')
|| [...document.querySelectorAll('button')].find(b => /continue|verify|submit|confirm/i.test(b.textContent));
if (submitBtn) simulateClick(submitBtn);
log('Step 7: Login verification code submitted');
await new Promise(r => setTimeout(r, 3000));
reportComplete(7);
}
// ============================================================
// Step 8: Navigate to OAuth URL
// ============================================================
async function step8_navigateOAuth(payload) {
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
const oauthUrl = state.oauthUrl;
if (!oauthUrl) throw new Error('No OAuth URL found. Complete step 1 first.');
log(`Step 8: Navigating to OAuth URL: ${oauthUrl.slice(0, 80)}...`);
log('Step 8: Waiting for localhost redirect (captured by background)...');
// Navigate — the webNavigation listener in background will capture localhost redirect
window.location.href = oauthUrl;
// Don't reportComplete here — background handles it via webNavigation
}
```
- [ ] **Step 2: Wire up executeStep6 and executeStep8 in background.js**
Replace the stubs:
```js
async function executeStep6(state) {
// Open ChatGPT
const alive = await isTabAlive('chatgpt');
if (!alive) {
await addLog('Step 6: Opening ChatGPT...');
await chrome.tabs.create({ url: 'https://chatgpt.com/', active: true });
} else {
const tabId = await getTabId('chatgpt');
await chrome.tabs.update(tabId, { active: true });
}
await sendToContentScript('chatgpt', {
type: 'EXECUTE_STEP',
step: 6,
source: 'background',
payload: {},
});
}
let webNavListener = null;
async function executeStep8(state) {
if (!state.oauthUrl) {
throw new Error('No OAuth URL. Complete step 1 first.');
}
await addLog('Step 8: Setting up localhost redirect listener...');
// Register webNavigation listener (scoped to this step)
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (webNavListener) {
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
webNavListener = null;
}
reject(new Error('Localhost redirect not captured after 30s. Check if OAuth authorization completed.'));
}, 30000);
webNavListener = (details) => {
if (details.url.startsWith('http://localhost')) {
console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
webNavListener = null;
clearTimeout(timeout);
setState({ localhostUrl: details.url }).then(() => {
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
setStepStatus(8, 'completed');
resolve();
});
}
};
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
// Tell chatgpt.js to navigate to OAuth URL
sendToContentScript('chatgpt', {
type: 'EXECUTE_STEP',
step: 8,
source: 'background',
payload: {},
}).catch(reject);
});
}
```
- [ ] **Step 3: Reload extension, verify no errors**
Reload extension. Check Service Worker DevTools — no errors. Open `chatgpt.com` in a tab, verify `[MultiPage:chatgpt] Content script loaded` appears in console.
- [ ] **Step 4: Commit**
```bash
git add content/chatgpt.js background.js
git commit -m "feat: add chatgpt content script (steps 6,7-fill,8) and wire up background with webNavigation listener"
```
---
## Chunk 4: Integration Testing & Selector Tuning
### Task 10: End-to-end debugging — Step 1 (VPS Panel)
**Files:**
- Modify: `content/vps-panel.js` — update selectors based on actual DOM
- [ ] **Step 1: Inspect VPS panel DOM**
Open `http://154.26.182.181:8317/management.html#/oauth` in Chrome. Open DevTools → Elements tab. Identify:
1. The OAuth login button (tag, class, id, text content)
2. The Codex login button that appears after clicking OAuth
3. Where the auth URL appears (input, textarea, span, etc.)
4. The URL input field for step 9
5. The verify button for step 9
6. A DOM element that indicates logged-in state
Document found selectors in a comment at the top of `vps-panel.js`.
- [ ] **Step 2: Update vps-panel.js selectors**
Replace all `TODO` placeholder selectors with actual selectors found in step 1.
- [ ] **Step 3: Test step 1 via Side Panel**
Click "Get OAuth Link" in Side Panel. Watch log output. Should complete and show OAuth URL.
- [ ] **Step 4: Commit**
```bash
git add content/vps-panel.js
git commit -m "fix: update vps-panel selectors to match actual DOM structure"
```
---
### Task 11: End-to-end debugging — Steps 2, 3, 4, 5 (Signup Flow)
**Files:**
- Modify: `content/signup-page.js` — update selectors
- Modify: `content/qq-mail.js` — update selectors
- [ ] **Step 1: Inspect OpenAI auth page DOM**
Open the OAuth URL from step 1 in a new tab. Use DevTools to identify:
1. Register / Sign up button
2. Email input field
3. Password input field (same page or next page?)
4. Submit / Continue button
5. Verification code input field
6. Name fields (first name, last name)
7. Birthday fields (format? single input or separate?)
8. Complete registration button
Document actual selectors.
- [ ] **Step 2: Inspect QQ Mail DOM**
Open `https://wx.mail.qq.com/`. Use DevTools to identify:
1. Login state indicator element
2. Inbox refresh button
3. Email list item structure (how to iterate emails)
4. How to identify sender / subject in list
5. Email body content element (after clicking an email)
Document actual selectors.
- [ ] **Step 3: Update signup-page.js selectors**
Replace all TODO placeholder selectors with actual ones.
- [ ] **Step 4: Update qq-mail.js selectors**
Replace all TODO placeholder selectors with actual ones.
- [ ] **Step 5: Test steps 2-5 sequentially**
Run through steps 2-5 in Side Panel, checking log output at each step. Fix any selector or timing issues.
- [ ] **Step 6: Commit**
```bash
git add content/signup-page.js content/qq-mail.js
git commit -m "fix: update signup-page and qq-mail selectors to match actual DOM structures"
```
---
### Task 12: End-to-end debugging — Steps 6, 7, 8, 9 (Login + OAuth)
**Files:**
- Modify: `content/chatgpt.js` — update selectors
- Modify: `content/vps-panel.js` — update step 9 selectors if needed
- [ ] **Step 1: Inspect ChatGPT login page DOM**
Open `https://chatgpt.com/`. Use DevTools to identify:
1. Login button
2. Email input on auth redirect
3. Password field (if present)
4. Submit buttons at each stage
5. Verification code input (if OTP flow)
Document actual selectors.
- [ ] **Step 2: Update chatgpt.js selectors**
Replace all TODO placeholder selectors with actual ones.
- [ ] **Step 3: Test steps 6-9 sequentially**
Run through steps 6-9 in Side Panel. Fix any issues.
- [ ] **Step 4: Test step 9 VPS verify selectors**
If step 9 selectors need updating based on actual VPS panel verify form, update now.
- [ ] **Step 5: Commit**
```bash
git add content/chatgpt.js content/vps-panel.js
git commit -m "fix: update chatgpt and vps-panel selectors for login and verify flows"
```
---
### Task 13: Full end-to-end test
- [ ] **Step 1: Reset and run all 9 steps**
Click Reset in Side Panel. Run through all 9 steps sequentially:
1. Get OAuth Link → verify URL appears in Side Panel
2. Open Signup → verify registration page loads
3. (Paste DuckDuckGo email in Side Panel) Fill Email/Password → verify form filled
4. Get Signup Code → verify QQ Mail polled and code filled
5. Fill Name/Birthday → verify profile completed
6. Login ChatGPT → verify login initiated
7. Get Login Code → verify code retrieved and filled
8. Complete OAuth → verify localhost URL captured
9. VPS Verify → verify URL pasted and verify clicked
- [ ] **Step 2: Fix any remaining issues**
Address any timing, selector, or orchestration issues found in the full run.
- [ ] **Step 3: Commit final fixes**
```bash
git add -u
git commit -m "fix: address issues found in full end-to-end testing"
```
---
## Notes for the implementer
### About TODO selectors
Every content script has `// TODO: Adjust selector` comments. These are intentional — the actual DOM selectors for VPS panel, OpenAI auth, QQ Mail, and ChatGPT **must be determined by inspecting the live pages in DevTools**. The placeholder selectors are educated guesses. Tasks 10-12 are specifically for this selector tuning.
### Debugging workflow
When a step fails:
1. Check Side Panel log — it shows what went wrong
2. Open DevTools on the target tab — look for `[MultiPage:xxx]` console messages
3. Open Service Worker DevTools — look for `[MultiPage:bg]` messages
4. Use Elements tab to find the correct selector
5. Update the content script, reload extension, retry
### Chrome extension reload
After modifying any file:
1. Go to `chrome://extensions`
2. Click the reload (↻) button on the extension card
3. Refresh any open tabs that use content scripts (or close and reopen)
4. Reopen Side Panel if it was open