chore: pre-release cleanup

- Remove dead chatgpt.js (unused — all auth flows go through signup-page.js)
- Remove chatgpt.com from manifest content_scripts
- Fix resetState() to preserve vpsUrl and mailProvider on manual reset
- Add 30s timeout to all tabs.onUpdated listeners (prevent hanging)
- Fix misleading "All runs finished" log when loop breaks early
- Remove stale TODO comment in signup-page.js
- Add disabled styles for .btn-primary and .run-count-input
- Remove unused `indicator` variable in sidepanel.js
- Fix trailing comma in manifest.json
This commit is contained in:
unknown
2026-04-05 11:33:42 +08:00
parent 1286130023
commit 387e177e00
13 changed files with 123 additions and 3011 deletions
+67 -16
View File
@@ -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 },
});
}
-211
View File
@@ -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
}
-1
View File
@@ -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(
+11 -7
View File
@@ -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):
// <div class="card">
@@ -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...');
File diff suppressed because it is too large Load Diff
@@ -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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

After

Width:  |  Height:  |  Size: 327 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 1.0 KiB

-5
View File
@@ -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": {
+44 -43
View File
@@ -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;
+1 -1
View File
@@ -45,7 +45,7 @@
<div class="data-card">
<div class="data-row">
<span class="data-label">VPS</span>
<input type="text" id="input-vps-url" class="data-input" placeholder="http://ip:port/management.html#/oauth" />
<input type="password" id="input-vps-url" class="data-input" placeholder="http://ip:port/management.html#/oauth" />
</div>
<div class="data-row">
<span class="data-label">Mail</span>
-1
View File
@@ -107,7 +107,6 @@ async function restoreState() {
function updateStepUI(step, status) {
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
const row = document.querySelector(`.step-row[data-step="${step}"]`);
const indicator = document.querySelector(`.step-indicator[data-step="${step}"]`);
if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '';
if (row) {