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
This commit is contained in:
+70
-16
@@ -22,6 +22,8 @@ const DEFAULT_STATE = {
|
||||
flowStartTime: null,
|
||||
tabRegistry: {},
|
||||
logs: [],
|
||||
vpsUrl: 'http://154.26.182.181:8317/management.html#/oauth',
|
||||
mailProvider: 'qq', // 'qq' or '163'
|
||||
};
|
||||
|
||||
async function getState() {
|
||||
@@ -245,6 +247,14 @@ async function handleMessage(message, sender) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'SAVE_SETTING': {
|
||||
const updates = {};
|
||||
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
||||
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
||||
await setState(updates);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// Side panel data updates
|
||||
case 'SAVE_EMAIL': {
|
||||
await setState({ email: message.payload.email });
|
||||
@@ -445,17 +455,36 @@ async function resumeAutoRun() {
|
||||
// ============================================================
|
||||
|
||||
async function executeStep1(state) {
|
||||
const vpsUrl = state.vpsUrl;
|
||||
if (!vpsUrl) {
|
||||
throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
|
||||
}
|
||||
|
||||
// Ensure VPS panel tab is open
|
||||
const alive = await isTabAlive('vps-panel');
|
||||
if (!alive) {
|
||||
await addLog('Step 1: Opening VPS panel...');
|
||||
await chrome.tabs.create({ url: 'http://154.26.182.181:8317/management.html#/oauth', active: true });
|
||||
await addLog(`Step 1: Opening VPS panel: ${vpsUrl.slice(0, 60)}...`);
|
||||
const tab = await chrome.tabs.create({ url: vpsUrl, active: true });
|
||||
// Dynamically inject content scripts since VPS URL is configurable
|
||||
// Wait for page to load, then inject
|
||||
await new Promise(resolve => {
|
||||
const listener = (tabId, info) => {
|
||||
if (tabId === tab.id && info.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/utils.js', 'content/vps-panel.js'],
|
||||
});
|
||||
} else {
|
||||
const tabId = await getTabId('vps-panel');
|
||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
|
||||
// Send command — will queue if content script not ready yet, flush on READY signal
|
||||
await sendToContentScript('vps-panel', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 1,
|
||||
@@ -505,19 +534,27 @@ async function executeStep3(state) {
|
||||
// Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
|
||||
// ============================================================
|
||||
|
||||
function getMailConfig(state) {
|
||||
const provider = state.mailProvider || 'qq';
|
||||
if (provider === '163') {
|
||||
return { source: 'mail-163', url: 'https://mail.163.com/', label: '163 Mail' };
|
||||
}
|
||||
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
|
||||
}
|
||||
|
||||
async function executeStep4(state) {
|
||||
// Ensure QQ Mail tab is open
|
||||
const alive = await isTabAlive('qq-mail');
|
||||
const mail = getMailConfig(state);
|
||||
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (!alive) {
|
||||
await addLog('Step 4: Opening QQ Mail...');
|
||||
await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true });
|
||||
await addLog(`Step 4: Opening ${mail.label}...`);
|
||||
await chrome.tabs.create({ url: mail.url, active: true });
|
||||
} else {
|
||||
const tabId = await getTabId('qq-mail');
|
||||
const tabId = await getTabId(mail.source);
|
||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
|
||||
// Send poll command to qq-mail
|
||||
const result = await sendToContentScript('qq-mail', {
|
||||
const result = await sendToContentScript(mail.source, {
|
||||
type: 'POLL_EMAIL',
|
||||
step: 4,
|
||||
source: 'background',
|
||||
@@ -608,16 +645,18 @@ async function executeStep6(state) {
|
||||
// ============================================================
|
||||
|
||||
async function executeStep7(state) {
|
||||
const alive = await isTabAlive('qq-mail');
|
||||
const mail = getMailConfig(state);
|
||||
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (!alive) {
|
||||
await addLog('Step 7: Opening QQ Mail...');
|
||||
await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true });
|
||||
await addLog(`Step 7: Opening ${mail.label}...`);
|
||||
await chrome.tabs.create({ url: mail.url, active: true });
|
||||
} else {
|
||||
const tabId = await getTabId('qq-mail');
|
||||
const tabId = await getTabId(mail.source);
|
||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
|
||||
const result = await sendToContentScript('qq-mail', {
|
||||
const result = await sendToContentScript(mail.source, {
|
||||
type: 'POLL_EMAIL',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
@@ -746,11 +785,26 @@ async function executeStep9(state) {
|
||||
throw new Error('No localhost URL. Complete step 8 first.');
|
||||
}
|
||||
|
||||
const vpsUrl = state.vpsUrl || 'http://154.26.182.181:8317/management.html#/oauth';
|
||||
|
||||
// Switch to VPS panel tab
|
||||
const alive = await isTabAlive('vps-panel');
|
||||
if (!alive) {
|
||||
await addLog('Step 9: Opening VPS panel...');
|
||||
await chrome.tabs.create({ url: 'http://154.26.182.181:8317/management.html#/oauth', active: true });
|
||||
const tab = await chrome.tabs.create({ url: vpsUrl, active: true });
|
||||
await new Promise(resolve => {
|
||||
const listener = (tabId, info) => {
|
||||
if (tabId === tab.id && info.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content/utils.js', 'content/vps-panel.js'],
|
||||
});
|
||||
} else {
|
||||
const tabId = await getTabId('vps-panel');
|
||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// content/mail-163.js — Content script for 163 Mail (steps 4, 7)
|
||||
// Injected on: mail.163.com
|
||||
//
|
||||
// Actual 163 Mail DOM structure:
|
||||
// <div class="rF0" sign="letter" id="...Dom" aria-label="你的 ChatGPT 代码为 479637 发件人 : OpenAI ...">
|
||||
// <div class="dP0" sign="start-from">
|
||||
// <span class="nui-user">OpenAI</span>
|
||||
// </div>
|
||||
// <div class="il0">
|
||||
// <span class="da0">你的 ChatGPT 代码为 479637</span>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
const MAIL163_PREFIX = '[MultiPage:mail-163]';
|
||||
const isTopFrame = window === window.top;
|
||||
|
||||
console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
|
||||
|
||||
// ============================================================
|
||||
// Message Handler
|
||||
// ============================================================
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
handlePollEmail(message.step, message.payload).then(result => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
reportError(message.step, err.message);
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Get all current mail IDs
|
||||
// ============================================================
|
||||
|
||||
function getCurrentMailIds() {
|
||||
const ids = new Set();
|
||||
// 163 mail items have sign="letter" and id ending with "Dom"
|
||||
const items = findMailItems();
|
||||
for (const item of items) {
|
||||
const id = item.getAttribute('id') || '';
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function findMailItems() {
|
||||
// Try current document first
|
||||
let items = document.querySelectorAll('div[sign="letter"]');
|
||||
if (items.length > 0) return items;
|
||||
|
||||
// Try iframes (163 mail may use iframes)
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
for (const iframe of iframes) {
|
||||
try {
|
||||
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||
if (doc) {
|
||||
items = doc.querySelectorAll('div[sign="letter"]');
|
||||
if (items.length > 0) return items;
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Email Polling
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
|
||||
|
||||
log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`);
|
||||
|
||||
// Wait for mail list to load
|
||||
await sleep(3000);
|
||||
|
||||
let items = findMailItems();
|
||||
if (items.length === 0) {
|
||||
log(`Step ${step}: Waiting for mail list to appear...`);
|
||||
await sleep(5000);
|
||||
items = findMailItems();
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('163 Mail list did not load. Make sure inbox is open.');
|
||||
}
|
||||
|
||||
log(`Step ${step}: Mail list loaded, ${items.length} items found`);
|
||||
|
||||
const existingMailIds = getCurrentMailIds();
|
||||
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`);
|
||||
|
||||
if (attempt > 1) {
|
||||
await refreshInbox();
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
const allItems = findMailItems();
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
|
||||
for (const item of allItems) {
|
||||
const id = item.getAttribute('id') || '';
|
||||
|
||||
if (!useFallback && existingMailIds.has(id)) continue;
|
||||
|
||||
// Get sender from .nui-user
|
||||
const senderEl = item.querySelector('.nui-user');
|
||||
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
|
||||
|
||||
// Get subject from span.da0
|
||||
const subjectEl = item.querySelector('span.da0');
|
||||
const subject = subjectEl ? subjectEl.textContent : '';
|
||||
|
||||
// Also check aria-label which contains full info
|
||||
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
|
||||
|
||||
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
||||
const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
||||
if (code) {
|
||||
const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new';
|
||||
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
|
||||
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check inbox manually.'
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Inbox Refresh
|
||||
// ============================================================
|
||||
|
||||
async function refreshInbox() {
|
||||
// 163 mail: click the "收信" button in toolbar
|
||||
function tryRefresh(doc) {
|
||||
const btn = doc.querySelector(
|
||||
'a[title="收信"], [id*="refresh"], .nui-toolbar-item[title*="收"]'
|
||||
);
|
||||
if (btn) {
|
||||
btn.click();
|
||||
console.log(MAIL163_PREFIX, 'Clicked 收信 button');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tryRefresh(document)) { await sleep(500); return; }
|
||||
|
||||
// Try in iframes
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
for (const iframe of iframes) {
|
||||
try {
|
||||
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||
if (doc && tryRefresh(doc)) { await sleep(500); return; }
|
||||
} catch { }
|
||||
}
|
||||
|
||||
console.log(MAIL163_PREFIX, 'Could not find refresh button');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Verification Code Extraction
|
||||
// ============================================================
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
const match6 = text.match(/\b(\d{6})\b/);
|
||||
if (match6) return match6[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
+3
-2
@@ -2,11 +2,12 @@
|
||||
|
||||
const SCRIPT_SOURCE = (() => {
|
||||
const url = location.href;
|
||||
if (url.includes('154.26.182.181')) return 'vps-panel';
|
||||
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
|
||||
if (url.includes('mail.qq.com')) return 'qq-mail';
|
||||
if (url.includes('mail.163.com')) return 'mail-163';
|
||||
if (url.includes('chatgpt.com')) return 'chatgpt';
|
||||
return 'unknown';
|
||||
// VPS panel — detected dynamically since URL is configurable
|
||||
return 'vps-panel';
|
||||
})();
|
||||
|
||||
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,359 @@
|
||||
# Multi-Page Automation Chrome Extension - Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
A Chrome extension that automates a multi-step OAuth account registration and verification workflow. The extension operates across multiple websites (VPS panel, OpenAI auth, QQ Mail, ChatGPT) using a Side Panel as the control center and Content Scripts for DOM manipulation.
|
||||
|
||||
## User Story
|
||||
|
||||
The user needs to repeatedly perform a multi-step workflow involving:
|
||||
1. Getting an OAuth link from a VPS panel
|
||||
2. Registering a new account on the linked auth page (multi-page: email/password → verify code → name/birthday)
|
||||
3. Receiving email verification codes (twice: registration + login)
|
||||
4. Logging into ChatGPT with the new account
|
||||
5. Completing OAuth callback verification
|
||||
|
||||
The extension automates DOM interactions at each step, with the user triggering steps manually via the Side Panel (semi-automatic mode, with future upgrade to full automation).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Chrome Extension │
|
||||
├──────────┬──────────────┬───────────────────────┤
|
||||
│ Side Panel │ Background │ Content Scripts │
|
||||
│ (Control) │ (Service │ (Injected per site) │
|
||||
│ │ Worker) │ │
|
||||
│ - Step list│ - Tab mgmt │ - vps-panel.js │
|
||||
│ - Buttons │ - Msg relay │ - signup-page.js │
|
||||
│ - Status │ - State store│ - qq-mail.js │
|
||||
│ - Logs │ - Orchestrate│ - chatgpt.js │
|
||||
│ - Reset │ tab switch │ - utils.js (shared) │
|
||||
└──────────┴──────────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
- **Side Panel**: Persistent right-side panel showing all steps with action buttons, status indicators, shared data display (OAuth URL, email, etc.), debug log area, and a reset button. On open/reload, restores full state from `chrome.storage.session`.
|
||||
- **Background Service Worker**: Central dispatcher and orchestrator. Manages tab creation/switching, stores flow state in `chrome.storage.session` (NOT in-memory, as MV3 service workers can be terminated after 30s of inactivity), relays and routes messages between Side Panel and Content Scripts, and handles tab-switching choreography between steps.
|
||||
- **Content Scripts**: Per-domain scripts injected into target pages, each responsible for DOM operations (reading, filling forms, clicking buttons). All share `utils.js` for common utilities.
|
||||
|
||||
### Communication — Unified Message Protocol
|
||||
|
||||
All messages between components use a standard format:
|
||||
```js
|
||||
{
|
||||
type: "ACTION_NAME", // e.g. "FILL_SIGNUP", "CODE_FOUND", "STEP_COMPLETE", "STEP_ERROR", "LOG"
|
||||
source: "qq-mail", // sender identifier
|
||||
target: "signup-page", // intended receiver (optional, Background uses for routing)
|
||||
step: 4, // which workflow step this relates to
|
||||
payload: { ... }, // data
|
||||
error: null // error message string (if any)
|
||||
}
|
||||
```
|
||||
|
||||
**Message routing:**
|
||||
- Side Panel <-> Background: `chrome.runtime.sendMessage`
|
||||
- Background -> Content Script: `chrome.tabs.sendMessage` (only after content script reports ready)
|
||||
- Content Script -> Background: `chrome.runtime.sendMessage`
|
||||
- Background is the central router: it receives all messages and forwards to the correct target based on `target` field and `tabRegistry`.
|
||||
|
||||
### Content Script Readiness Protocol
|
||||
|
||||
Background must NOT send messages to a content script immediately after opening/navigating a tab. Instead:
|
||||
1. Content script sends `{ type: "CONTENT_SCRIPT_READY", source: "vps-panel" }` message on load
|
||||
2. Background registers the tab as ready in the `tabRegistry`
|
||||
3. Only then does Background send action commands to that tab
|
||||
4. If Background needs to send a command but the script isn't ready yet, it queues the command and sends it when the ready signal arrives (with a 15s timeout → error)
|
||||
|
||||
### Tab Registry
|
||||
|
||||
Background maintains a `tabRegistry` in `chrome.storage.session`:
|
||||
```json
|
||||
{
|
||||
"vps-panel": { "tabId": 123, "ready": true },
|
||||
"signup": { "tabId": 124, "ready": true },
|
||||
"qq-mail": { "tabId": 125, "ready": false },
|
||||
"chatgpt": { "tabId": 126, "ready": true }
|
||||
}
|
||||
```
|
||||
Before operating on a tab, Background checks if the tab still exists via `chrome.tabs.get()`. If the tab was closed, it reopens and waits for the ready signal.
|
||||
|
||||
### Tab Switching Orchestration
|
||||
|
||||
When a step requires cross-tab coordination (e.g., step 4: get code from QQ Mail → fill into signup page), the orchestration is always handled by Background:
|
||||
1. Content script A (e.g., qq-mail.js) sends result to Background: `{ type: "CODE_FOUND", payload: { code: "123456" } }`
|
||||
2. Background stores the data in `chrome.storage.session`
|
||||
3. Background activates target tab via `chrome.tabs.update(tabId, { active: true })`
|
||||
4. Background sends command to Content script B: `{ type: "FILL_CODE", payload: { code: "123456" } }`
|
||||
5. Content script B executes and reports `STEP_COMPLETE` or `STEP_ERROR`
|
||||
|
||||
Content scripts NEVER communicate directly with each other.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
| Step | Button | Executor | Action |
|
||||
|------|--------|----------|--------|
|
||||
| 1 | Get OAuth Link | `vps-panel.js` | Check VPS login state → Click OAuth login → Click Codex login → Read auth URL → Send to Background |
|
||||
| 2 | Open Signup & Click Register | Background + `signup-page.js` | Background opens auth URL in new tab → `signup-page.js` loads and clicks "Register" button |
|
||||
| 3 | Fill Email & Password | `signup-page.js` | Read email from Side Panel input (via Background) → Fill email + password (`mimashisha0.0`) → Submit |
|
||||
| 4 | Get Signup Verification Code | `qq-mail.js` → Background → `signup-page.js` | Background opens QQ Mail tab → `qq-mail.js` polls for new email from OpenAI → Extracts code → Background switches to signup tab → `signup-page.js` fills code and confirms |
|
||||
| 5 | Fill Name & Birthday | `signup-page.js` | After code verification, page transitions to profile form → Fill random English name + random birthday (age 19-25) → Click complete registration |
|
||||
| 6 | Login ChatGPT | Background + `chatgpt.js` | Background opens chatgpt.com in new tab → `chatgpt.js` clicks login → Enters email → If password field appears, fills password; otherwise waits for OTP flow → Submit |
|
||||
| 7 | Get Login Verification Code | `qq-mail.js` → Background → `chatgpt.js` | Background switches to QQ Mail tab → `qq-mail.js` polls for email newer than step 4's → Extracts code → Background switches to ChatGPT tab → `chatgpt.js` fills code → Login complete |
|
||||
| 8 | Complete OAuth | Background (`webNavigation`) + `chatgpt.js` | `chatgpt.js` navigates to step 1's `oauthUrl` → Background captures localhost redirect via `webNavigation` listener → Stores `localhostUrl` |
|
||||
| 9 | VPS Verify | `vps-panel.js` | Background switches to VPS panel tab → `vps-panel.js` pastes `localhostUrl` into input → Clicks verify button |
|
||||
|
||||
## Content Script URL Matching
|
||||
|
||||
| Script | Match Patterns | all_frames |
|
||||
|--------|---------------|------------|
|
||||
| `utils.js` | (included by all below) | — |
|
||||
| `vps-panel.js` | `http://154.26.182.181:8317/*` | false |
|
||||
| `signup-page.js` | `https://auth0.openai.com/*`, `https://auth.openai.com/*`, `https://accounts.openai.com/*` | false |
|
||||
| `qq-mail.js` | `https://mail.qq.com/*`, `https://wx.mail.qq.com/*` | true |
|
||||
| `chatgpt.js` | `https://chatgpt.com/*` | false |
|
||||
|
||||
Note: `qq-mail.js` uses `all_frames: true` because old QQ Mail (`mail.qq.com`) renders inbox inside iframes.
|
||||
|
||||
## Content Script Details
|
||||
|
||||
### utils.js (shared)
|
||||
|
||||
Loaded before every content script. Provides:
|
||||
|
||||
```js
|
||||
// Wait for a DOM element to appear, with timeout
|
||||
async function waitForElement(selector, timeout = 10000)
|
||||
|
||||
// React-compatible form filling — triggers proper event chain
|
||||
function fillInput(el, value) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype, 'value'
|
||||
).set;
|
||||
nativeInputValueSetter.call(el, value);
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
// Send log message to Side Panel (via Background)
|
||||
function log(message, level = 'info')
|
||||
|
||||
// Send step result to Background
|
||||
function reportComplete(step, data = {})
|
||||
function reportError(step, errorMessage)
|
||||
|
||||
// Standard ready signal
|
||||
function reportReady(source)
|
||||
```
|
||||
|
||||
### vps-panel.js
|
||||
- **Step 1**: Check login state (look for known logged-in DOM indicator); if not logged in → `reportError(1, "VPS panel not logged in, please log in first")`. Then: locate OAuth login button → click → wait for Codex login button to appear → click → wait for auth URL to appear in DOM → read it → `reportComplete(1, { oauthUrl })`
|
||||
- **Step 9**: `waitForElement` for URL input field → `fillInput` with `localhostUrl` → click verify button → `reportComplete(9)`
|
||||
|
||||
### signup-page.js
|
||||
- **Step 2**: On ready, detect if current page has a "Register" / "Sign up" button → click it → `reportComplete(2)`
|
||||
- **Step 3**: `waitForElement` for email input → `fillInput` email → `fillInput` password (`mimashisha0.0`) → click submit → `reportComplete(3)`
|
||||
- **Step 4 (receiving end)**: `waitForElement` for verification code input → `fillInput` code → click confirm → `reportComplete(4)`
|
||||
- **Step 5**: `waitForElement` for name/birthday fields (page transition after code verification) → `fillInput` random first name, last name → `fillInput` random birthday → click complete → `reportComplete(5)`
|
||||
- All DOM operations use `waitForElement` before acting
|
||||
- All input filling uses `fillInput` (React-compatible)
|
||||
|
||||
### qq-mail.js
|
||||
- Check login state first; if not logged in → `reportError(step, "QQ Mail not logged in, please log in first")`
|
||||
- **QQ Mail version handling**:
|
||||
- Old version (`mail.qq.com`): Content is inside iframes. With `all_frames: true`, the script runs in each frame. Only the frame containing the inbox list should act.
|
||||
- New version (`wx.mail.qq.com`): SPA, no iframes. All content in main frame with dynamic DOM updates.
|
||||
- **Polling strategy** (no page refresh):
|
||||
- Click inbox refresh button (or equivalent DOM trigger) every 3 seconds
|
||||
- Observe email list DOM for new entries matching sender filter (e.g., `openai`, `noreply`) or subject filter (e.g., `verify`, `verification`, `code`)
|
||||
- Extract verification code from email preview/snippet if visible, or click into email and extract from body
|
||||
- Log each poll attempt: `log("Polling QQ Mail... attempt 3/20")`
|
||||
- **Step 4**: Find email newer than flow start time → extract 6-digit code via regex → `reportComplete(4, { code, emailTimestamp })`
|
||||
- **Step 7**: Find email newer than step 4's `emailTimestamp` → extract 6-digit code → `reportComplete(7, { code })`
|
||||
- Timeout after 60 seconds (20 attempts) → `reportError(step, "No matching email found after 60s")`
|
||||
|
||||
### chatgpt.js
|
||||
- **Step 6**: `waitForElement` for login button → click → `waitForElement` for email input → `fillInput` email → submit → detect next state:
|
||||
- If password field appears → `fillInput` password → submit → `reportComplete(6)`
|
||||
- If no password field (OTP flow) → `reportComplete(6, { needsOTP: true })` (code will come from step 7)
|
||||
- **Step 7 (receiving end)**: `waitForElement` for code input → `fillInput` code → submit → `reportComplete(7)`
|
||||
- **Step 8**: Navigate to `oauthUrl` (from storage) → page will redirect → `log("Navigating to OAuth URL, waiting for redirect...")` → (localhost capture handled by Background)
|
||||
|
||||
## Data Flow
|
||||
|
||||
Data persisted in `chrome.storage.session` (survives service worker termination), passed between steps:
|
||||
|
||||
```
|
||||
Step 1 → oauthUrl (authorization link)
|
||||
Step 3 → email, password (from Side Panel input + hardcoded)
|
||||
Step 4 → lastEmailTimestamp (to distinguish step 7's email)
|
||||
Step 8 → localhostUrl (OAuth callback URL)
|
||||
```
|
||||
|
||||
Additionally stored:
|
||||
- `currentStep`: which step is active (for UI restore)
|
||||
- `stepStatuses`: `{ 1: "completed", 2: "completed", 3: "running", ... }` (for UI restore)
|
||||
- `tabRegistry`: tab ID mapping (for tab management)
|
||||
- `logs`: array of log entries (for UI restore)
|
||||
- `flowStartTime`: timestamp when flow began (for email filtering)
|
||||
|
||||
## Random Data Generation (in Background)
|
||||
|
||||
- **English names**: Built-in list of common first names + last names, randomly combined
|
||||
- **Birthday**: Current year minus 19-25 years, random month/day, formatted per the target form's expected format
|
||||
|
||||
## Side Panel UI
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Multi-Page Automation [Reset]│
|
||||
├──────────────────────────────────┤
|
||||
│ OAuth URL: [显示/未获取] │
|
||||
│ Email: [________粘贴邮箱______] │
|
||||
│ Status: Step 3 running... │
|
||||
├──────────────────────────────────┤
|
||||
│ 1 [Get OAuth Link] ✅ │
|
||||
│ 2 [Open Signup] ✅ │
|
||||
│ 3 [Fill Email/Password] ⏳ │
|
||||
│ 4 [Get Signup Code] ⬚ 禁用 │
|
||||
│ 5 [Fill Name/Birthday] ⬚ 禁用 │
|
||||
│ 6 [Login ChatGPT] ⬚ 禁用 │
|
||||
│ 7 [Get Login Code] ⬚ 禁用 │
|
||||
│ 8 [Complete OAuth] ⬚ 禁用 │
|
||||
│ 9 [VPS Verify] ⬚ 禁用 │
|
||||
├──────────────────────────────────┤
|
||||
│ Log: │
|
||||
│ 10:23:01 [INFO] Step 1 started │
|
||||
│ 10:23:03 [INFO] Found OAuth btn │
|
||||
│ 10:23:05 [OK] Step 1 done │
|
||||
│ 10:23:05 [INFO] oauthUrl saved │
|
||||
│ 10:23:08 [INFO] Step 2 started │
|
||||
│ 10:23:10 [INFO] Tab opened │
|
||||
│ 10:23:12 [OK] Register clicked│
|
||||
│ 10:23:15 [ERR] Step 3 failed: │
|
||||
│ email input not found │
|
||||
│ 10:23:15 [INFO] Retry step 3... │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
**UI behaviors:**
|
||||
- **Step interlock**: When a step is running, all other buttons are disabled. Next step button stays disabled until previous step succeeds. Failed steps show ❌ and can be retried. Completed steps show ✅ and can be re-run.
|
||||
- **State restore**: On Side Panel open/reload, read `currentStep`, `stepStatuses`, `logs`, and all data fields from `chrome.storage.session` and render.
|
||||
- **Reset button**: Clears all `chrome.storage.session` data, resets all steps to pending, clears logs. Does NOT close open tabs (user may want them).
|
||||
- **Email input**: Editable text field. User pastes DuckDuckGo-generated email here before step 3.
|
||||
- **Log area**: Scrollable, auto-scrolls to bottom. Shows timestamp, level (INFO/OK/WARN/ERR), and message. Each content script action logs what it's about to do and the result.
|
||||
|
||||
## Debugging & Observability
|
||||
|
||||
Every step must be fully debuggable. The user should never be stuck wondering "what went wrong".
|
||||
|
||||
### Log Everything
|
||||
|
||||
Each content script logs at these checkpoints:
|
||||
1. **Step start**: `"Step N started"`
|
||||
2. **Waiting for element**: `"Waiting for selector: #email-input..."`
|
||||
3. **Element found/not found**: `"Found #email-input"` or `"Timeout waiting for #email-input after 10s"`
|
||||
4. **Action taken**: `"Filled email input with xxx@duck.com"`, `"Clicked submit button"`
|
||||
5. **Page transition**: `"Page URL changed to https://..."`
|
||||
6. **Polling progress**: `"Polling QQ Mail... attempt 5/20, no match yet"`
|
||||
7. **Data extracted**: `"Verification code found: 123456"`
|
||||
8. **Step result**: `"Step N completed successfully"` or `"Step N failed: [reason]"`
|
||||
|
||||
### Error Recovery Guide
|
||||
|
||||
When a step fails, the error message should include actionable guidance:
|
||||
|
||||
| Error | Guidance shown in log |
|
||||
|-------|----------------------|
|
||||
| VPS not logged in | "Please log in to VPS panel at http://154.26.182.181:8317 and retry" |
|
||||
| QQ Mail not logged in | "Please log in to QQ Mail and retry" |
|
||||
| Element not found (timeout) | "Could not find [selector] on [url]. Page may have changed layout. Check DevTools." |
|
||||
| Email not received (timeout) | "No matching email after 60s. Check QQ Mail manually. Email may be in spam." |
|
||||
| Tab was closed | "Tab [name] was closed. Will reopen on retry." |
|
||||
| Content script not ready | "Content script on [url] did not respond in 15s. Try refreshing the tab and retry." |
|
||||
| Unknown error | "Unexpected error: [message]. Check DevTools console on [tab name] for details." |
|
||||
|
||||
### DevTools Integration
|
||||
|
||||
- Each content script logs to the browser console with a prefix: `[MultiPage:vps-panel]`, `[MultiPage:qq-mail]`, etc.
|
||||
- Background service worker also logs with `[MultiPage:bg]` prefix.
|
||||
- All chrome.storage writes are logged: `[MultiPage:bg] storage.set: stepStatuses = {...}`
|
||||
- This allows using Chrome DevTools on any tab or the service worker to see detailed execution trace.
|
||||
|
||||
## Robustness
|
||||
|
||||
### Login State Detection
|
||||
- **VPS panel** (step 1): Check for known logged-in DOM indicator before operating; if absent, `reportError` with guidance and pause.
|
||||
- **QQ Mail** (steps 4, 7): Check for inbox DOM indicator; if absent, `reportError` with guidance and pause.
|
||||
|
||||
### DOM Element Waiting
|
||||
All content script DOM operations use `waitForElement(selector, timeout)` from `utils.js`. Default timeout: 10 seconds. On timeout, `reportError` with the selector and URL for debugging.
|
||||
|
||||
### Email Polling
|
||||
- Poll QQ Mail inbox every 3 seconds for matching email (filter by sender/subject keywords)
|
||||
- Step 7 only accepts emails newer than step 4's `lastEmailTimestamp` to avoid duplicates
|
||||
- Each poll attempt is logged with attempt count
|
||||
- Timeout after 60 seconds (20 attempts); `reportError` with guidance to check spam folder
|
||||
|
||||
### Localhost Redirect Capture
|
||||
- `chrome.webNavigation.onBeforeNavigate` listener is registered in Background **only when step 8 starts**
|
||||
- Listener matches URL starting with `http://localhost`
|
||||
- On capture: store `localhostUrl`, remove listener, `reportComplete(8)`
|
||||
- If not captured within 30 seconds: `reportError` with guidance
|
||||
- Listener is always removed when step 8 ends (success or failure)
|
||||
|
||||
### Page Transitions
|
||||
- After form submissions, `signup-page.js` and `chatgpt.js` must handle page navigation/SPA route changes
|
||||
- Use `waitForElement` on the NEXT expected element after submission, not just submit and hope
|
||||
- If URL changes (full navigation), the content script re-injects and sends a new READY signal; Background re-sends the pending command
|
||||
|
||||
## Permissions (manifest.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"storage",
|
||||
"scripting",
|
||||
"activeTab"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://154.26.182.181:8317/*",
|
||||
"https://auth0.openai.com/*",
|
||||
"https://auth.openai.com/*",
|
||||
"https://accounts.openai.com/*",
|
||||
"https://mail.qq.com/*",
|
||||
"https://wx.mail.qq.com/*",
|
||||
"https://chatgpt.com/*",
|
||||
"http://localhost/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note: `scripting` permission enables `chrome.scripting.executeScript` for dynamic injection when the auth flow redirects to unexpected domains.
|
||||
|
||||
## Technical Decisions
|
||||
|
||||
- **Manifest V3**: Required for current Chrome extension development
|
||||
- **Side Panel API**: `chrome.sidePanel` for persistent control panel
|
||||
- **Pure Content Script approach**: No external dependencies, direct DOM manipulation
|
||||
- **State in `chrome.storage.session`**: MV3 service workers can terminate after 30s idle; all flow state must be persisted, not held in memory
|
||||
- **React-compatible form filling**: Use native setter + event dispatch pattern for all React-based target pages
|
||||
- **Content script readiness protocol**: Scripts report ready before receiving commands; commands queued if not ready
|
||||
- **Unified message protocol**: All inter-component messages use same format with type/source/target/step/payload/error
|
||||
- **Shared utils.js**: Common utilities loaded by all content scripts to avoid duplication
|
||||
- **Background as sole orchestrator**: All cross-tab coordination goes through Background; content scripts never talk to each other
|
||||
- **webNavigation listener scoped to step 8**: Registered on step start, removed on step end, avoids false triggers
|
||||
- **Semi-automatic first**: Each step triggered manually, upgrade to full automation later
|
||||
- **DuckDuckGo email generation**: Manual step (user clicks DuckDuckGo panel), user pastes email into Side Panel input
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Full automation mode (single button to run all steps)
|
||||
- DuckDuckGo API integration for automatic email generation
|
||||
- Batch execution support
|
||||
- Error recovery and auto-retry logic
|
||||
- Export logs for troubleshooting
|
||||
+10
-14
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Multi-Page Automation",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Automates multi-step OAuth registration workflow",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
@@ -12,14 +12,7 @@
|
||||
"activeTab"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://154.26.182.181:8317/*",
|
||||
"https://auth0.openai.com/*",
|
||||
"https://auth.openai.com/*",
|
||||
"https://accounts.openai.com/*",
|
||||
"https://mail.qq.com/*",
|
||||
"https://wx.mail.qq.com/*",
|
||||
"https://chatgpt.com/*",
|
||||
"http://localhost/*"
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
@@ -28,11 +21,6 @@
|
||||
"default_path": "sidepanel/sidepanel.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://154.26.182.181:8317/*"],
|
||||
"js": ["content/utils.js", "content/vps-panel.js"],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://auth0.openai.com/*",
|
||||
@@ -51,6 +39,14 @@
|
||||
"all_frames": true,
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://mail.163.com/*"
|
||||
],
|
||||
"js": ["content/utils.js", "content/mail-163.js"],
|
||||
"all_frames": true,
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": ["https://chatgpt.com/*"],
|
||||
"js": ["content/utils.js", "content/chatgpt.js"],
|
||||
|
||||
@@ -260,6 +260,23 @@ header {
|
||||
.data-input::placeholder { color: var(--text-muted); }
|
||||
.data-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
|
||||
|
||||
.data-select {
|
||||
flex: 1;
|
||||
padding: 5px 8px;
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition);
|
||||
min-width: 0;
|
||||
}
|
||||
.data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
|
||||
[data-theme="dark"] .data-select { color-scheme: dark; }
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
|
||||
@@ -41,13 +41,24 @@
|
||||
<section id="data-section">
|
||||
<div class="data-card">
|
||||
<div class="data-row">
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
|
||||
<span class="data-label">VPS</span>
|
||||
<input type="text" id="input-vps-url" class="data-input" placeholder="http://ip:port/management.html#/oauth" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Mail</span>
|
||||
<select id="select-mail-provider" class="data-select">
|
||||
<option value="qq">QQ Mail (wx.mail.qq.com)</option>
|
||||
<option value="163">163 Mail (mail.163.com)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Email</span>
|
||||
<input type="text" id="input-email" class="data-input" placeholder="Paste DuckDuckGo email" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Callback</span>
|
||||
<span id="display-localhost-url" class="data-value mono truncate">Waiting...</span>
|
||||
|
||||
+23
-1
@@ -19,6 +19,8 @@ const btnAutoRun = document.getElementById('btn-auto-run');
|
||||
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||
const autoContinueBar = document.getElementById('auto-continue-bar');
|
||||
const btnClearLog = document.getElementById('btn-clear-log');
|
||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||
|
||||
// ============================================================
|
||||
// State Restore on load
|
||||
@@ -39,6 +41,12 @@ async function restoreState() {
|
||||
if (state.email) {
|
||||
inputEmail.value = state.email;
|
||||
}
|
||||
if (state.vpsUrl) {
|
||||
inputVpsUrl.value = state.vpsUrl;
|
||||
}
|
||||
if (state.mailProvider) {
|
||||
selectMailProvider.value = state.mailProvider;
|
||||
}
|
||||
|
||||
if (state.stepStatuses) {
|
||||
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
||||
@@ -238,7 +246,7 @@ btnClearLog.addEventListener('click', () => {
|
||||
logArea.innerHTML = '';
|
||||
});
|
||||
|
||||
// Save email on change
|
||||
// Save settings on change
|
||||
inputEmail.addEventListener('change', async () => {
|
||||
const email = inputEmail.value.trim();
|
||||
if (email) {
|
||||
@@ -246,6 +254,20 @@ inputEmail.addEventListener('change', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
inputVpsUrl.addEventListener('change', async () => {
|
||||
const vpsUrl = inputVpsUrl.value.trim();
|
||||
if (vpsUrl) {
|
||||
await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { vpsUrl } });
|
||||
}
|
||||
});
|
||||
|
||||
selectMailProvider.addEventListener('change', async () => {
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING', source: 'sidepanel',
|
||||
payload: { mailProvider: selectMailProvider.value },
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Listen for Background broadcasts
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user