feat: initial implementation of Multi-Page Automation Chrome extension

- manifest.json: MV3 config with sidePanel, tabs, webNavigation, storage, scripting permissions
- background.js: service worker with state management (chrome.storage.session), tab registry,
  message routing, command queue, and step orchestration for all 9 steps
- sidepanel/: persistent side panel UI with 9 step buttons, interlock logic, log area,
  state restore, and reset functionality
- content/utils.js: shared utilities (waitForElement, fillInput, simulateClick, log, report*)
- content/vps-panel.js: VPS panel automation (steps 1, 9)
- content/signup-page.js: OpenAI auth page automation (steps 2, 3, 4-fill, 5)
- content/qq-mail.js: QQ Mail email polling and code extraction (steps 4, 7)
- content/chatgpt.js: ChatGPT login automation (steps 6, 7-fill, 8)
- data/names.js: random English name and birthday generation
This commit is contained in:
unknown
2026-04-05 08:59:03 +08:00
commit 887709e824
14 changed files with 2354 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
/* sidepanel/sidepanel.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #333;
background: #f8f9fa;
padding: 12px;
width: 100%;
min-height: 100vh;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
header h1 {
font-size: 15px;
font-weight: 600;
}
#btn-reset {
padding: 4px 10px;
font-size: 12px;
background: #dc3545;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
#btn-reset:hover { background: #c82333; }
/* Data Section */
#data-section {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 10px;
margin-bottom: 12px;
}
.data-row {
display: flex;
align-items: center;
margin-bottom: 6px;
}
.data-row:last-child { margin-bottom: 0; }
.data-row label {
width: 80px;
font-weight: 600;
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.data-value {
font-size: 12px;
color: #999;
word-break: break-all;
}
.data-value.has-value {
color: #333;
}
#input-email {
flex: 1;
padding: 4px 8px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 12px;
}
/* Steps Section */
#steps-section {
margin-bottom: 12px;
}
.step-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.step-num {
width: 20px;
text-align: center;
font-weight: 600;
font-size: 12px;
color: #666;
}
.step-btn {
flex: 1;
padding: 6px 10px;
font-size: 12px;
background: #007bff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: left;
}
.step-btn:hover:not(:disabled) { background: #0069d9; }
.step-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.step-status {
width: 24px;
text-align: center;
font-size: 14px;
}
/* Log Section */
#log-section h2 {
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
}
#log-area {
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
padding: 8px;
border-radius: 6px;
height: 250px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.log-info { color: #d4d4d4; }
.log-ok { color: #4ec9b0; }
.log-warn { color: #dcdcaa; }
.log-error { color: #f44747; }
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Page Automation</title>
<link rel="stylesheet" href="sidepanel.css">
</head>
<body>
<header>
<h1>Multi-Page Automation</h1>
<button id="btn-reset" title="Reset all steps">Reset</button>
</header>
<section id="data-section">
<div class="data-row">
<label>OAuth URL:</label>
<span id="display-oauth-url" class="data-value">Not obtained</span>
</div>
<div class="data-row">
<label>Email:</label>
<input type="text" id="input-email" placeholder="Paste DuckDuckGo email here" />
</div>
<div class="data-row">
<label>Localhost:</label>
<span id="display-localhost-url" class="data-value">Not captured</span>
</div>
<div class="data-row">
<label>Status:</label>
<span id="display-status" class="data-value">Waiting</span>
</div>
</section>
<section id="steps-section">
<div class="step-row" data-step="1">
<span class="step-num">1</span>
<button class="step-btn" data-step="1">Get OAuth Link</button>
<span class="step-status" data-step="1"></span>
</div>
<div class="step-row" data-step="2">
<span class="step-num">2</span>
<button class="step-btn" data-step="2">Open Signup</button>
<span class="step-status" data-step="2"></span>
</div>
<div class="step-row" data-step="3">
<span class="step-num">3</span>
<button class="step-btn" data-step="3">Fill Email/Password</button>
<span class="step-status" data-step="3"></span>
</div>
<div class="step-row" data-step="4">
<span class="step-num">4</span>
<button class="step-btn" data-step="4">Get Signup Code</button>
<span class="step-status" data-step="4"></span>
</div>
<div class="step-row" data-step="5">
<span class="step-num">5</span>
<button class="step-btn" data-step="5">Fill Name/Birthday</button>
<span class="step-status" data-step="5"></span>
</div>
<div class="step-row" data-step="6">
<span class="step-num">6</span>
<button class="step-btn" data-step="6">Login ChatGPT</button>
<span class="step-status" data-step="6"></span>
</div>
<div class="step-row" data-step="7">
<span class="step-num">7</span>
<button class="step-btn" data-step="7">Get Login Code</button>
<span class="step-status" data-step="7"></span>
</div>
<div class="step-row" data-step="8">
<span class="step-num">8</span>
<button class="step-btn" data-step="8">Complete OAuth</button>
<span class="step-status" data-step="8"></span>
</div>
<div class="step-row" data-step="9">
<span class="step-num">9</span>
<button class="step-btn" data-step="9">VPS Verify</button>
<span class="step-status" data-step="9"></span>
</div>
</section>
<section id="log-section">
<h2>Log</h2>
<div id="log-area"></div>
</section>
<script src="sidepanel.js"></script>
</body>
</html>
+234
View File
@@ -0,0 +1,234 @@
// sidepanel/sidepanel.js — Side Panel logic
const STATUS_ICONS = {
pending: '\u2B1A', // ⬚
running: '\u23F3', // ⏳
completed: '\u2705', // ✅
failed: '\u274C', // ❌
};
const logArea = document.getElementById('log-area');
const displayOauthUrl = document.getElementById('display-oauth-url');
const displayLocalhostUrl = document.getElementById('display-localhost-url');
const displayStatus = document.getElementById('display-status');
const inputEmail = document.getElementById('input-email');
const btnReset = document.getElementById('btn-reset');
// ============================================================
// State Restore on load
// ============================================================
async function restoreState() {
try {
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
// Restore data fields
if (state.oauthUrl) {
displayOauthUrl.textContent = state.oauthUrl;
displayOauthUrl.classList.add('has-value');
}
if (state.localhostUrl) {
displayLocalhostUrl.textContent = state.localhostUrl;
displayLocalhostUrl.classList.add('has-value');
}
if (state.email) {
inputEmail.value = state.email;
}
// Restore step statuses
if (state.stepStatuses) {
for (const [step, status] of Object.entries(state.stepStatuses)) {
updateStepUI(Number(step), status);
}
}
// Restore logs
if (state.logs) {
for (const entry of state.logs) {
appendLog(entry);
}
}
updateStatusDisplay(state);
} catch (err) {
console.error('Failed to restore state:', err);
}
}
// ============================================================
// UI Updates
// ============================================================
function updateStepUI(step, status) {
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '\u2B1A';
// Interlock logic
updateButtonStates();
}
function updateButtonStates() {
// Get all current statuses from DOM
const statuses = {};
document.querySelectorAll('.step-status').forEach(el => {
const step = Number(el.dataset.step);
const icon = el.textContent;
const status = Object.entries(STATUS_ICONS).find(([, v]) => v === icon)?.[0] || 'pending';
statuses[step] = status;
});
// Find if any step is running
const anyRunning = Object.values(statuses).some(s => s === 'running');
for (let step = 1; step <= 9; step++) {
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
if (!btn) continue;
if (anyRunning) {
// When any step is running, disable all buttons
btn.disabled = true;
} else if (step === 1) {
// Step 1 is always available (unless running)
btn.disabled = false;
} else {
// Steps 2-9: enabled if previous step completed (or current step failed for retry)
const prevStatus = statuses[step - 1];
const currentStatus = statuses[step];
btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed');
}
}
}
function updateStatusDisplay(state) {
if (!state || !state.stepStatuses) return;
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
if (running) {
displayStatus.textContent = `Step ${running[0]} running...`;
displayStatus.classList.add('has-value');
} else {
const lastCompleted = Object.entries(state.stepStatuses)
.filter(([, s]) => s === 'completed')
.map(([k]) => Number(k))
.sort((a, b) => b - a)[0];
if (lastCompleted === 9) {
displayStatus.textContent = 'All steps completed!';
displayStatus.classList.add('has-value');
} else if (lastCompleted) {
displayStatus.textContent = `Step ${lastCompleted} done. Ready for step ${lastCompleted + 1}.`;
displayStatus.classList.add('has-value');
} else {
displayStatus.textContent = 'Waiting';
displayStatus.classList.remove('has-value');
}
}
}
function appendLog(entry) {
const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
const levelLabel = entry.level.toUpperCase().padEnd(5);
const line = document.createElement('div');
line.className = `log-${entry.level}`;
line.textContent = `${time} [${levelLabel}] ${entry.message}`;
logArea.appendChild(line);
logArea.scrollTop = logArea.scrollHeight;
}
// ============================================================
// Button Handlers
// ============================================================
document.querySelectorAll('.step-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const step = Number(btn.dataset.step);
// Save email if step 3 and email input has value
if (step === 3) {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
return;
}
await chrome.runtime.sendMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step, email },
});
} else {
await chrome.runtime.sendMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step },
});
}
});
});
// Reset button
btnReset.addEventListener('click', async () => {
if (confirm('Reset all steps and data?')) {
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
// Clear UI
displayOauthUrl.textContent = 'Not obtained';
displayOauthUrl.classList.remove('has-value');
displayLocalhostUrl.textContent = 'Not captured';
displayLocalhostUrl.classList.remove('has-value');
inputEmail.value = '';
displayStatus.textContent = 'Waiting';
displayStatus.classList.remove('has-value');
logArea.innerHTML = '';
document.querySelectorAll('.step-status').forEach(el => el.textContent = '\u2B1A');
updateButtonStates();
}
});
// Save email when user types/pastes
inputEmail.addEventListener('change', async () => {
const email = inputEmail.value.trim();
if (email) {
await chrome.runtime.sendMessage({
type: 'SAVE_EMAIL',
source: 'sidepanel',
payload: { email },
});
}
});
// ============================================================
// Listen for Background broadcasts
// ============================================================
chrome.runtime.onMessage.addListener((message) => {
switch (message.type) {
case 'LOG_ENTRY':
appendLog(message.payload);
break;
case 'STEP_STATUS_CHANGED': {
const { step, status } = message.payload;
updateStepUI(step, status);
// Update status display
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
break;
}
case 'DATA_UPDATED': {
if (message.payload.oauthUrl) {
displayOauthUrl.textContent = message.payload.oauthUrl;
displayOauthUrl.classList.add('has-value');
}
if (message.payload.localhostUrl) {
displayLocalhostUrl.textContent = message.payload.localhostUrl;
displayLocalhostUrl.classList.add('has-value');
}
break;
}
}
});
// ============================================================
// Init
// ============================================================
restoreState().then(() => {
updateButtonStates();
});