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:
+67
-16
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 84 B After Width: | Height: | Size: 327 B |
Binary file not shown.
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 1.0 KiB |
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user