feat: add Auto Run mode for full automated flow

- Background: add step completion waiting (promise-based), autoRun/resumeAutoRun
  chains steps 1→2 (pause for email) → 3→4→5→6→7→8→9 automatically
- Side Panel: add "Auto Run" button and "Continue Auto" bar for email pause point
- signup-page.js: add step 8 handler to click "继续" on OAuth consent page
- Fix all steps to reportComplete before form submit (prevents connection loss on navigation)
- qq-mail.js: fallback after 3 attempts to first matching email
- vps-panel.js: step 9 waits for "认证成功!" status badge
This commit is contained in:
unknown
2026-04-05 09:42:17 +08:00
parent 887709e824
commit 1c1e589bdc
7 changed files with 743 additions and 431 deletions
+197 -30
View File
@@ -200,14 +200,15 @@ async function handleMessage(message, sender) {
case 'STEP_COMPLETE': {
await setStepStatus(message.step, 'completed');
await addLog(`Step ${message.step} completed`, 'ok');
// Store step-specific data
await handleStepData(message.step, message.payload);
notifyStepComplete(message.step, message.payload);
return { ok: true };
}
case 'STEP_ERROR': {
await setStepStatus(message.step, 'failed');
await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
notifyStepError(message.step, message.error);
return { ok: true };
}
@@ -231,6 +232,19 @@ async function handleMessage(message, sender) {
return { ok: true };
}
case 'AUTO_RUN': {
autoRun(); // fire-and-forget, runs in background
return { ok: true };
}
case 'RESUME_AUTO_RUN': {
if (message.payload.email) {
await setState({ email: message.payload.email });
}
resumeAutoRun(); // fire-and-forget
return { ok: true };
}
// Side panel data updates
case 'SAVE_EMAIL': {
await setState({ email: message.payload.email });
@@ -277,6 +291,37 @@ async function handleStepData(step, payload) {
}
}
// ============================================================
// Step Completion Waiting
// ============================================================
// Map of step -> { resolve, reject } for waiting on step completion
const stepWaiters = new Map();
function waitForStepComplete(step, timeoutMs = 120000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
stepWaiters.delete(step);
reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
}, timeoutMs);
stepWaiters.set(step, {
resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
});
});
}
function notifyStepComplete(step, payload) {
const waiter = stepWaiters.get(step);
if (waiter) waiter.resolve(payload);
}
function notifyStepError(step, error) {
const waiter = stepWaiters.get(step);
if (waiter) waiter.reject(new Error(error));
}
// ============================================================
// Step Execution
// ============================================================
@@ -313,11 +358,104 @@ async function executeStep(step) {
}
}
/**
* Execute a step and wait for it to complete before returning.
* @param {number} step
* @param {number} delayAfter - ms to wait after completion (for page transitions)
*/
async function executeStepAndWait(step, delayAfter = 2000) {
const promise = waitForStepComplete(step, 120000);
await executeStep(step);
await promise;
// Extra delay for page transitions / DOM updates
if (delayAfter > 0) {
await new Promise(r => setTimeout(r, delayAfter));
}
}
// ============================================================
// Auto Run Flow
// ============================================================
let autoRunActive = false;
async function autoRun() {
if (autoRunActive) {
await addLog('Auto run already in progress', 'warn');
return;
}
autoRunActive = true;
await setState({ autoRunning: true });
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'running' } }).catch(() => {});
try {
// Phase 1: Steps 1-2 (get OAuth link, open signup)
await addLog('=== Auto Run Phase 1: Get OAuth link & open signup ===', 'info');
await executeStepAndWait(1, 2000);
await executeStepAndWait(2, 2000);
// Pause: ask user to generate DuckDuckGo email
await addLog('=== Auto Run PAUSED: Please paste DuckDuckGo email and click "Continue Auto" ===', 'warn');
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'waiting_email' } }).catch(() => {});
// Wait here — resumed by RESUME_AUTO_RUN message from side panel
} catch (err) {
await addLog(`Auto run failed at Phase 1: ${err.message}`, 'error');
autoRunActive = false;
await setState({ autoRunning: false });
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped' } }).catch(() => {});
}
}
async function resumeAutoRun() {
try {
const state = await getState();
if (!state.email) {
await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
return;
}
// Phase 2: Steps 3-9 (fill form, get codes, login, OAuth, verify)
await addLog('=== Auto Run Phase 2: Register, verify, login, complete OAuth ===', 'info');
await executeStepAndWait(3, 3000); // Fill email/password → page navigates to code input
await executeStepAndWait(4, 2000); // Get signup code from QQ Mail → fill in
await executeStepAndWait(5, 3000); // Fill name/birthday → page navigates to add-phone
await executeStepAndWait(6, 3000); // Login via OAuth URL → fill email/password
await executeStepAndWait(7, 2000); // Get login code from QQ Mail → fill in
await executeStepAndWait(8, 2000); // Click "继续" → localhost redirect captured
await executeStepAndWait(9, 1000); // VPS verify → wait for "认证成功!"
await addLog('=== Auto Run COMPLETE! All 9 steps finished successfully ===', 'ok');
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete' } }).catch(() => {});
} catch (err) {
await addLog(`Auto run failed: ${err.message}`, 'error');
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped' } }).catch(() => {});
} finally {
autoRunActive = false;
await setState({ autoRunning: false });
}
}
// ============================================================
// Step 1: Get OAuth Link (via vps-panel.js)
// ============================================================
async function executeStep1(state) {
// 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 });
} 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,
@@ -439,20 +577,29 @@ async function executeStep5(state) {
// ============================================================
async function executeStep6(state) {
const alive = await isTabAlive('chatgpt');
if (!alive) {
await addLog('Step 6: Opening ChatGPT...');
await chrome.tabs.create({ url: 'https://chatgpt.com/', active: true });
} else {
const tabId = await getTabId('chatgpt');
if (tabId) await chrome.tabs.update(tabId, { active: true });
if (!state.oauthUrl) {
throw new Error('No OAuth URL. Complete step 1 first.');
}
if (!state.email) {
throw new Error('No email. Complete step 3 first.');
}
await sendToContentScript('chatgpt', {
// Open the OAuth URL again in a new tab to start the login flow
// Close the old signup tab first (it's on add-phone page, not needed)
const oldSignupTabId = await getTabId('signup-page');
if (oldSignupTabId) {
try { await chrome.tabs.remove(oldSignupTabId); } catch {}
}
await addLog(`Step 6: Opening OAuth URL for login: ${state.oauthUrl.slice(0, 60)}...`);
await chrome.tabs.create({ url: state.oauthUrl, active: true });
// signup-page.js will inject (same auth.openai.com domain) and handle login
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 6,
source: 'background',
payload: {},
payload: { email: state.email, password: state.password || 'mimashisha0.0' },
});
}
@@ -490,18 +637,18 @@ async function executeStep7(state) {
if (result && result.code) {
await addLog(`Step 7: Got login verification code: ${result.code}`);
// Switch to ChatGPT tab and fill code
const chatgptTabId = await getTabId('chatgpt');
if (chatgptTabId) {
await chrome.tabs.update(chatgptTabId, { active: true });
await sendToContentScript('chatgpt', {
// Switch to signup/auth tab and fill code
const signupTabId = await getTabId('signup-page');
if (signupTabId) {
await chrome.tabs.update(signupTabId, { active: true });
await sendToContentScript('signup-page', {
type: 'FILL_CODE',
step: 7,
source: 'background',
payload: { code: result.code },
});
} else {
throw new Error('ChatGPT tab was closed. Cannot fill verification code.');
throw new Error('Auth page tab was closed. Cannot fill verification code.');
}
}
}
@@ -541,7 +688,7 @@ async function executeStep8(state) {
setState({ localhostUrl: details.url }).then(() => {
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
setStepStatus(8, 'completed');
// Broadcast to side panel
notifyStepComplete(8, { localhostUrl: details.url });
chrome.runtime.sendMessage({
type: 'DATA_UPDATED',
payload: { localhostUrl: details.url },
@@ -553,20 +700,40 @@ async function executeStep8(state) {
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
// Tell chatgpt.js to navigate to OAuth URL
sendToContentScript('chatgpt', {
type: 'EXECUTE_STEP',
step: 8,
source: 'background',
payload: {},
}).catch(err => {
clearTimeout(timeout);
if (webNavListener) {
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
webNavListener = null;
// After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
// with a "继续" button. We need to click it, which triggers the localhost redirect.
(async () => {
try {
const signupTabId = await getTabId('signup-page');
if (signupTabId) {
await chrome.tabs.update(signupTabId, { active: true });
await addLog('Step 8: Switching to auth page, clicking "继续" to complete OAuth...');
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 8,
source: 'background',
payload: {},
});
} else {
// Auth tab was closed, reopen OAuth URL
await chrome.tabs.create({ url: state.oauthUrl, active: true });
await addLog('Step 8: Auth tab closed, reopening OAuth URL...');
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 8,
source: 'background',
payload: {},
});
}
} catch (err) {
clearTimeout(timeout);
if (webNavListener) {
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
webNavListener = null;
}
reject(err);
}
reject(err);
});
})();
});
}
+100 -186
View File
@@ -1,64 +1,27 @@
// content/qq-mail.js — Content script for QQ Mail (steps 4, 7)
// Injected on: mail.qq.com, wx.mail.qq.com
// NOTE: all_frames: true — this script runs in every frame on QQ Mail
// NOTE: all_frames: true
//
// Strategy for avoiding stale codes:
// 1. On poll start, snapshot all existing mail IDs as "old"
// 2. On each poll cycle, refresh inbox and look for NEW items (not in snapshot)
// 3. Only extract codes from NEW items that match sender/subject filters
const QQ_MAIL_PREFIX = '[MultiPage:qq-mail]';
const isNewVersion = location.hostname === 'wx.mail.qq.com';
const isTopFrame = window === window.top;
console.log(QQ_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
// For old QQ Mail with iframes, only report ready from the top frame
// to avoid duplicate registrations. The inbox frame will handle email ops.
if (!isTopFrame && isNewVersion) {
console.log(QQ_MAIL_PREFIX, 'Skipping non-top frame on new QQ Mail');
// Don't do anything in child frames of new version
}
// ============================================================
// Frame detection
// ============================================================
function isInboxFrame() {
if (isNewVersion) return isTopFrame;
// Old version: check if this frame has email list elements
return !!document.querySelector(
'#mailList, .mail-list, [id*="mailList"], #frm_main, .toarea, .mailList'
);
}
// ============================================================
// Login State Check
// ============================================================
function checkLoginState() {
if (isNewVersion) {
// wx.mail.qq.com: look for compose button or folder list
return !!(
document.querySelector('[class*="folder"], [class*="compose"], [class*="sidebar"]') ||
document.querySelector('nav, [role="navigation"]') ||
document.querySelector('[class*="mail-list"], [class*="mailList"]')
);
} else {
// mail.qq.com: look for known logged-in elements
return !!(
document.querySelector('#folder_1, .folder_inbox, #composebtn, #mainFrameContainer')
);
}
}
// ============================================================
// Message Handler
// ============================================================
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
// For old QQ Mail, only handle in the inbox frame
if (!isNewVersion && !isInboxFrame()) {
if (!isTopFrame) {
sendResponse({ ok: false, reason: 'wrong-frame' });
return;
}
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
@@ -67,60 +30,93 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
return true; // async response
}
if (message.type === 'CHECK_LOGIN') {
if (!isTopFrame) {
sendResponse({ loggedIn: false });
return;
}
sendResponse({ loggedIn: checkLoginState() });
return;
}
});
// ============================================================
// Get all current mail IDs from the list
// ============================================================
function getCurrentMailIds() {
const ids = new Set();
document.querySelectorAll('.mail-list-page-item[data-mailid]').forEach(item => {
ids.add(item.getAttribute('data-mailid'));
});
return ids;
}
// ============================================================
// Email Polling
// ============================================================
async function handlePollEmail(step, payload) {
const { filterAfterTimestamp, senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
// Check login state first
if (isTopFrame && !checkLoginState()) {
throw new Error('QQ Mail not logged in. Please log in to QQ Mail and retry.');
}
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
// Wait for mail list to load
try {
await waitForElement('.mail-list-page-item', 10000);
log(`Step ${step}: Mail list loaded`);
} catch {
throw new Error('Mail list did not load. Make sure QQ Mail inbox is open.');
}
// Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email
const existingMailIds = getCurrentMailIds();
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails as "old"`);
// Fallback after just 3 attempts (~10s). In practice, the email is usually
// already in the list but has the same mailid (page was already open).
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`);
// Try to refresh inbox
await refreshInbox();
await sleep(800); // Wait for refresh to take effect
// Refresh inbox (skip on first attempt, list is fresh)
if (attempt > 1) {
await refreshInbox();
await sleep(800);
}
// Search for matching email
const result = await findMatchingEmail(senderFilters, subjectFilters);
const allItems = document.querySelectorAll('.mail-list-page-item[data-mailid]');
const useFallback = attempt > FALLBACK_AFTER;
if (result) {
log(`Step ${step}: Found matching email! Extracting code...`);
const code = extractVerificationCode(result.content);
if (code) {
log(`Step ${step}: Verification code found: ${code}`, 'ok');
return { ok: true, code, emailTimestamp: Date.now() };
} else {
log(`Step ${step}: Email found but no 6-digit code in content. Preview: ${result.content.slice(0, 100)}`, 'warn');
// Phase 1 (attempt 1~3): only look at NEW emails (not in snapshot)
// Phase 2 (attempt 4+): fallback to first matching email in list
for (const item of allItems) {
const mailId = item.getAttribute('data-mailid');
if (!useFallback && existingMailIds.has(mailId)) continue;
const sender = (item.querySelector('.cmp-account-nick')?.textContent || '').toLowerCase();
const subject = (item.querySelector('.mail-subject')?.textContent || '').toLowerCase();
const digest = item.querySelector('.mail-digest')?.textContent || '';
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
if (senderMatch || subjectMatch) {
const code = extractVerificationCode(subject + ' ' + digest);
if (code) {
const source = useFallback && existingMailIds.has(mailId) ? 'fallback-first-match' : 'new';
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
return { ok: true, code, emailTimestamp: Date.now(), mailId };
}
}
}
if (attempt === FALLBACK_AFTER + 1) {
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first matching email`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`No matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
'Check QQ Mail manually. Email may be in spam folder.'
`No new matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
'Check QQ Mail manually. Email may be delayed or in spam folder.'
);
}
@@ -129,114 +125,33 @@ async function handlePollEmail(step, payload) {
// ============================================================
async function refreshInbox() {
if (isNewVersion) {
// wx.mail.qq.com: try multiple refresh selectors
const refreshBtn = document.querySelector(
'[class*="refresh"], [title*="刷新"], button[aria-label*="refresh"], ' +
'[data-action="refresh"], .toolbar-refresh'
);
if (refreshBtn) {
simulateClick(refreshBtn);
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button (new version)');
} else {
// Fallback: click on "Inbox" folder to force reload
const inboxLink = document.querySelector(
'[class*="inbox"], [title*="收件箱"], a[href*="inbox"]'
);
if (inboxLink) {
simulateClick(inboxLink);
console.log(QQ_MAIL_PREFIX, 'Clicked inbox link to refresh');
}
}
} else {
// mail.qq.com: old version
const refreshBtn = document.querySelector(
'#refresh, .refresh_btn, [id*="refresh"], a[title*="刷新"]'
);
if (refreshBtn) {
simulateClick(refreshBtn);
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button (old version)');
}
}
}
// Try multiple strategies to refresh the mail list
// ============================================================
// Find Matching Email
// ============================================================
async function findMatchingEmail(senderFilters, subjectFilters) {
let emailItems;
if (isNewVersion) {
// wx.mail.qq.com: email list items
emailItems = document.querySelectorAll(
'[class*="mail-item"], [class*="list-item"], [class*="mail_item"], ' +
'tr[class*="mail"], div[class*="letter"], [class*="thread"]'
);
} else {
// mail.qq.com: email list inside table
emailItems = document.querySelectorAll(
'.toarea tr, #mailList tr, .mail_list tr, [id*="mail_"] tr'
);
// Strategy 1: Click any visible refresh button
const refreshBtn = document.querySelector('[class*="refresh"], [title*="刷新"]');
if (refreshBtn) {
simulateClick(refreshBtn);
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button');
await sleep(500);
return;
}
console.log(QQ_MAIL_PREFIX, `Found ${emailItems.length} email items to scan`);
for (const item of emailItems) {
const text = (item.textContent || '').toLowerCase();
const senderMatch = senderFilters.some(f => text.includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => text.includes(f.toLowerCase()));
if (senderMatch || subjectMatch) {
console.log(QQ_MAIL_PREFIX, 'Found matching email item:', text.slice(0, 100));
// Try to get content from the visible text first
let content = item.textContent || '';
// Check if we can already extract a code from the preview
if (extractVerificationCode(content)) {
return { content };
}
// Need to click into the email for full body
log('Clicking email to read full content...');
simulateClick(item);
await sleep(1500);
// Read email body
const bodyEl = document.querySelector(
'[class*="mail-body"], [class*="mail_body"], [class*="letter-body"], ' +
'.body_content, #contentDiv, [class*="read-content"], [class*="mail-detail"]'
);
if (bodyEl) {
content = bodyEl.textContent || bodyEl.innerText || '';
console.log(QQ_MAIL_PREFIX, 'Email body content:', content.slice(0, 200));
}
// Go back to inbox after reading
await goBackToInbox();
return { content };
}
// Strategy 2: Click inbox in sidebar to reload list
const sidebarInbox = document.querySelector('a[href*="inbox"], [class*="folder-item"][class*="inbox"], [title="收件箱"]');
if (sidebarInbox) {
simulateClick(sidebarInbox);
console.log(QQ_MAIL_PREFIX, 'Clicked sidebar inbox');
await sleep(500);
return;
}
return null;
}
async function goBackToInbox() {
if (isNewVersion) {
// Click back or inbox link
const backBtn = document.querySelector(
'[class*="back"], [aria-label*="back"], [title*="返回"], [class*="return"]'
);
if (backBtn) {
simulateClick(backBtn);
await sleep(500);
}
// Strategy 3: Click the folder name in toolbar
const folderName = document.querySelector('.toolbar-folder-name');
if (folderName) {
simulateClick(folderName);
console.log(QQ_MAIL_PREFIX, 'Clicked toolbar folder name');
await sleep(500);
}
// Old version typically uses frames, no need to go back
}
// ============================================================
@@ -244,18 +159,17 @@ async function goBackToInbox() {
// ============================================================
function extractVerificationCode(text) {
// Try various patterns for 6-digit verification codes
// Pattern 1: standalone 6 digits
// Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
// Pattern 2: English format "code is 370794" or "code: 370794"
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
// Pattern 3: standalone 6-digit number (first occurrence)
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
// Pattern 2: code/verification followed by digits
const matchLabeled = text.match(/(?:code|验证码|verification|verify)[:\s]*(\d{4,8})/i);
if (matchLabeled) return matchLabeled[1];
// Pattern 3: digits followed by "code" label (Chinese: 是您的验证码)
const matchReverse = text.match(/(\d{4,8})\s*(?:是|为|is)?\s*(?:您的)?(?:验证码|code)/i);
if (matchReverse) return matchReverse[1];
return null;
}
+190 -74
View File
@@ -23,10 +23,13 @@ async function handleCommand(message) {
case 2: return await step2_clickRegister();
case 3: return await step3_fillEmailPassword(message.payload);
case 5: return await step5_fillNameBirthday(message.payload);
case 6: return await step6_login(message.payload);
case 8: return await step8_clickContinue();
default: throw new Error(`signup-page.js does not handle step ${message.step}`);
}
case 'FILL_CODE':
return await step4_fillVerificationCode(message.payload);
// Step 4 = signup code, Step 7 = login code (same handler)
return await fillVerificationCode(message.step, message.payload);
}
}
@@ -57,12 +60,9 @@ async function step2_clickRegister() {
}
}
reportComplete(2);
simulateClick(registerBtn);
log('Step 2: Clicked Register button');
// Wait for page transition
await sleep(2000);
reportComplete(2);
}
// ============================================================
@@ -114,7 +114,11 @@ async function step3_fillEmailPassword(payload) {
fillInput(passwordInput, 'mimashisha0.0');
log('Step 3: Password filled');
// Submit the form
// Report complete BEFORE submit, because submit causes page navigation
// which kills the content script connection
reportComplete(3, { email });
// Submit the form (page will navigate away after this)
await sleep(500);
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
@@ -123,20 +127,17 @@ async function step3_fillEmailPassword(payload) {
simulateClick(submitBtn);
log('Step 3: Form submitted');
}
await sleep(2000);
reportComplete(3, { email });
}
// ============================================================
// Step 4 (receiving end): Fill Verification Code
// Fill Verification Code (used by step 4 and step 7)
// ============================================================
async function step4_fillVerificationCode(payload) {
async function fillVerificationCode(step, payload) {
const { code } = payload;
if (!code) throw new Error('No verification code provided.');
log(`Step 4: Filling verification code: ${code}`);
log(`Step ${step}: Filling verification code: ${code}`);
// Find code input — could be a single input or multiple separate inputs
let codeInput = null;
@@ -149,20 +150,23 @@ async function step4_fillVerificationCode(payload) {
// Check for multiple single-digit inputs (common pattern)
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
if (singleInputs.length >= 6) {
log('Step 4: Found single-digit code inputs, filling individually...');
log(`Step ${step}: 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);
reportComplete(4);
reportComplete(step);
return;
}
throw new Error('Could not find verification code input. URL: ' + location.href);
}
fillInput(codeInput, code);
log('Step 4: Code filled');
log(`Step ${step}: Code filled`);
// Report complete BEFORE submit (page may navigate away)
reportComplete(step);
// Submit
await sleep(500);
@@ -171,12 +175,100 @@ async function step4_fillVerificationCode(payload) {
if (submitBtn) {
simulateClick(submitBtn);
log('Step 4: Verification submitted');
log(`Step ${step}: Verification submitted`);
}
}
// ============================================================
// Step 6: Login with registered account (on OAuth auth page)
// ============================================================
async function step6_login(payload) {
const { email, password } = payload;
if (!email) throw new Error('No email provided for login.');
log(`Step 6: Logging in with ${email}...`);
// Wait for email input on the auth page
let emailInput = null;
try {
emailInput = await waitForElement(
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
15000
);
} catch {
throw new Error('Could not find email input on login page. URL: ' + location.href);
}
fillInput(emailInput, email);
log('Step 6: Email filled');
// 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');
}
// Wait for page transition
await sleep(2000);
reportComplete(4);
// Check for password field
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);
// Report complete BEFORE submit in case page navigates
reportComplete(6, { needsOTP: true });
if (submitBtn2) {
simulateClick(submitBtn2);
log('Step 6: Submitted password, may need verification code (step 7)');
}
return;
}
// No password field — OTP flow
log('Step 6: No password field. OTP flow or auto-redirect.');
reportComplete(6, { needsOTP: true });
}
// ============================================================
// Step 8: Click "继续" on OAuth consent page
// ============================================================
// After login + verification, page shows:
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
// Clicking it triggers redirect to localhost URL.
async function step8_clickContinue() {
log('Step 8: Looking for OAuth consent "继续" button...');
// Wait for the consent page to be ready
// Look for the submit button with text "继续" or data-dd-action-name="Continue"
let continueBtn = null;
try {
continueBtn = await waitForElement(
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
10000
);
} catch {
try {
continueBtn = await waitForElementByText('button', /继续|Continue/, 5000);
} catch {
throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
}
}
log('Step 8: Found "继续" button, clicking...');
simulateClick(continueBtn);
log('Step 8: Clicked "继续", redirecting to localhost... (background will capture URL)');
// Don't reportComplete — background handles it via webNavigation listener
}
// ============================================================
@@ -187,80 +279,104 @@ async function step5_fillNameBirthday(payload) {
const { firstName, lastName, year, month, day } = payload;
if (!firstName || !lastName) throw new Error('No name data provided.');
log(`Step 5: Filling name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
const fullName = `${firstName} ${lastName}`;
log(`Step 5: Filling name: ${fullName}, Birthday: ${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`);
// --- First name ---
let firstNameInput = null;
// Actual DOM structure:
// - Full name: <input name="name" placeholder="全名" type="text">
// - Birthday: React Aria DateField with 3 spinbutton divs (year/month/day)
// + <input type="hidden" name="birthday" value="2026-04-05">
// --- Full Name (single field, not first+last) ---
let nameInput = null;
try {
firstNameInput = await waitForElement(
'input[name="firstName"], input[name="first_name"], input[name="fname"], input[placeholder*="first" i], input[placeholder*="First"], input[id*="first" i]',
nameInput = await waitForElement(
'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
10000
);
} catch {
throw new Error('Could not find first name input. URL: ' + location.href);
throw new Error('Could not find name input. URL: ' + location.href);
}
fillInput(firstNameInput, firstName);
log(`Step 5: First name filled: ${firstName}`);
fillInput(nameInput, fullName);
log(`Step 5: Name filled: ${fullName}`);
// --- Last name ---
let lastNameInput = null;
try {
lastNameInput = await waitForElement(
'input[name="lastName"], input[name="last_name"], input[name="lname"], input[placeholder*="last" i], input[placeholder*="Last"], input[id*="last" i]',
5000
);
} catch {
throw new Error('Could not find last name input. URL: ' + location.href);
}
fillInput(lastNameInput, lastName);
log(`Step 5: Last name filled: ${lastName}`);
// --- Birthday (React Aria DateField with spinbutton segments) ---
// The date field has three contenteditable divs with role="spinbutton"
// and data-type="year", data-type="month", data-type="day"
// There's also a hidden input[name="birthday"] that stores the actual value
// --- Birthday ---
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
// Try single date input first
const dateInput = document.querySelector('input[type="date"], input[name="birthday"], input[name="dob"], input[name="birthdate"]');
if (dateInput) {
fillInput(dateInput, dateStr);
log(`Step 5: Birthday filled (single input): ${dateStr}`);
if (yearSpinner && monthSpinner && daySpinner) {
log('Step 5: Found React Aria DateField spinbuttons');
// Helper to set a spinbutton value via focus + keyboard input
async function setSpinButton(el, value) {
el.focus();
await sleep(100);
// Select all existing text
document.execCommand('selectAll', false, null);
await sleep(50);
// Type the new value digit by digit
const valueStr = String(value);
for (const char of valueStr) {
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
// Also use InputEvent for React Aria
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
await sleep(50);
}
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
el.blur();
await sleep(100);
}
await setSpinButton(yearSpinner, year);
log(`Step 5: Year set: ${year}`);
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
log(`Step 5: Month set: ${month}`);
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
log(`Step 5: Day set: ${day}`);
// Also update the hidden input directly as a safety measure
const hiddenBirthday = document.querySelector('input[type="hidden"][name="birthday"]');
if (hiddenBirthday) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
hiddenBirthday.value = dateStr;
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
log(`Step 5: Hidden birthday input set: ${dateStr}`);
}
} else {
// Try separate fields (month/day/year selects or inputs)
log('Step 5: Looking for separate birthday fields...');
const monthEl = document.querySelector('select[name*="month" i], input[name*="month" i], input[placeholder*="month" i], select[id*="month" i]');
const dayEl = document.querySelector('select[name*="day" i], input[name*="day" i], input[placeholder*="day" i], select[id*="day" i]');
const yearEl = document.querySelector('select[name*="year" i], input[name*="year" i], input[placeholder*="year" i], select[id*="year" i]');
if (monthEl) {
if (monthEl.tagName === 'SELECT') fillSelect(monthEl, String(month));
else fillInput(monthEl, String(month).padStart(2, '0'));
}
if (dayEl) {
if (dayEl.tagName === 'SELECT') fillSelect(dayEl, String(day));
else fillInput(dayEl, String(day).padStart(2, '0'));
}
if (yearEl) {
if (yearEl.tagName === 'SELECT') fillSelect(yearEl, String(year));
else fillInput(yearEl, String(year));
}
if (!monthEl && !dayEl && !yearEl) {
log('Step 5: WARNING - Could not find any birthday fields. May need to adjust selectors.', 'warn');
// Fallback: try setting hidden input directly
const hiddenBirthday = document.querySelector('input[name="birthday"]');
if (hiddenBirthday) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
hiddenBirthday.value = dateStr;
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
log(`Step 5: Birthday set via hidden input: ${dateStr}`);
} else {
log(`Step 5: Birthday filled (separate fields): ${year}-${month}-${day}`);
log('Step 5: WARNING - Could not find birthday fields. May need to adjust selectors.', 'warn');
}
}
// Submit / Complete
// Click "完成帐户创建" button
await sleep(500);
const completeBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /complete|continue|finish|done|create|agree|完成|创建|同意/i, 5000).catch(() => null);
|| await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
// Report complete BEFORE submit (page navigates to add-phone after this)
reportComplete(5);
if (completeBtn) {
simulateClick(completeBtn);
log('Step 5: Profile form submitted');
log('Step 5: Clicked "完成帐户创建"');
}
await sleep(2000);
reportComplete(5);
}
+100 -137
View File
@@ -1,5 +1,27 @@
// content/vps-panel.js — Content script for VPS panel (steps 1, 9)
// Injected on: http://154.26.182.181:8317/*
//
// Actual DOM structure (after login click):
// <div class="card">
// <div class="card-header">
// <span class="OAuthPage-module__cardTitle___yFaP0">Codex OAuth</span>
// <button class="btn btn-primary"><span>登录</span></button>
// </div>
// <div class="OAuthPage-module__cardContent___1sXLA">
// <div class="OAuthPage-module__authUrlBox___Iu1d4">
// <div class="OAuthPage-module__authUrlLabel___mYFJB">授权链接:</div>
// <div class="OAuthPage-module__authUrlValue___axvUJ">https://auth.openai.com/...</div>
// <div class="OAuthPage-module__authUrlActions___venPj">
// <button class="btn btn-secondary btn-sm"><span>复制链接</span></button>
// <button class="btn btn-secondary btn-sm"><span>打开链接</span></button>
// </div>
// </div>
// <div class="OAuthPage-module__callbackSection___8kA31">
// <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
// <button class="btn btn-secondary btn-sm"><span>提交回调 URL</span></button>
// </div>
// </div>
// </div>
console.log('[MultiPage:vps-panel] Content script loaded on', location.href);
@@ -30,178 +52,119 @@ async function handleStep(step, payload) {
// ============================================================
async function step1_getOAuthLink() {
log('Step 1: Checking VPS panel state...');
log('Step 1: Waiting for VPS panel to load (auto-login may take a moment)...');
// --- Selector strategy ---
// The VPS panel is at management.html#/oauth
// We need to: 1) find OAuth login section, 2) click Codex login, 3) read auth URL
// TODO: These selectors MUST be adjusted after inspecting the actual VPS panel DOM
// The selectors below are best-guess placeholders.
// Try to find OAuth login button by text content
log('Step 1: Looking for OAuth login button...');
let oauthBtn = null;
// The page may start at #/login and auto-redirect to #/oauth.
// Wait for the Codex OAuth card to appear (up to 30s for auto-login + redirect).
let loginBtn = null;
try {
oauthBtn = await waitForElementByText(
'button, a, [role="button"], .el-button, div[class*="btn"]',
/oauth|OAuth/i,
10000
);
// Wait for any card-header containing "Codex" to appear
const header = await waitForElementByText('.card-header', /codex/i, 30000);
loginBtn = header.querySelector('button.btn.btn-primary, button.btn');
log('Step 1: Found Codex OAuth card');
} catch {
// Fallback: try common UI framework selectors
try {
oauthBtn = await waitForElement('.oauth-login-btn, [data-action="oauth-login"]', 5000);
} catch {
throw new Error(
'Could not find OAuth login button. ' +
'Please inspect the VPS panel page in DevTools and update the selector in vps-panel.js. ' +
'URL: ' + location.href
);
}
}
simulateClick(oauthBtn);
log('Step 1: Clicked OAuth login, waiting for Codex login option...');
await sleep(1500);
// Wait for Codex login option to appear
let codexBtn = null;
try {
codexBtn = await waitForElementByText(
'button, a, [role="button"], .el-button, div[class*="btn"], span',
/codex/i,
10000
);
} catch {
try {
codexBtn = await waitForElement('[data-action="codex-login"], .codex-login-btn', 5000);
} catch {
throw new Error(
'Could not find Codex login button after clicking OAuth. ' +
'Check the VPS panel DOM in DevTools. URL: ' + location.href
);
}
}
simulateClick(codexBtn);
log('Step 1: Clicked Codex login, waiting for auth URL...');
await sleep(2000);
// Extract the auth URL — could be in various elements
let oauthUrl = null;
// Strategy 1: Look for an input/textarea with a URL value
const inputs = document.querySelectorAll('input[readonly], input[type="text"], textarea, code, pre');
for (const el of inputs) {
const val = (el.value || el.textContent || '').trim();
if (val.startsWith('http') && val.length > 30) {
oauthUrl = val;
log(`Step 1: Found URL in <${el.tagName}>: ${val.slice(0, 80)}...`);
break;
}
}
// Strategy 2: Look for any element containing a long URL
if (!oauthUrl) {
const allElements = document.querySelectorAll('span, p, div, a, code, pre');
for (const el of allElements) {
const text = (el.textContent || '').trim();
// Match a URL that looks like an OAuth authorization URL
const urlMatch = text.match(/(https?:\/\/[^\s<>"']{30,})/);
if (urlMatch) {
oauthUrl = urlMatch[1];
log(`Step 1: Found URL in text: ${oauthUrl.slice(0, 80)}...`);
break;
}
}
}
// Strategy 3: Check clipboard (if the page auto-copies)
if (!oauthUrl) {
try {
oauthUrl = await navigator.clipboard.readText();
if (oauthUrl && oauthUrl.startsWith('http') && oauthUrl.length > 30) {
log(`Step 1: Found URL in clipboard: ${oauthUrl.slice(0, 80)}...`);
} else {
oauthUrl = null;
}
} catch {
// Clipboard access may be denied
}
}
if (!oauthUrl) {
throw new Error(
'Could not find auth URL. The URL may be displayed in a format we cannot detect. ' +
'Please check the VPS panel page and copy the URL manually, or update the extraction logic in vps-panel.js. ' +
'URL: ' + location.href
'Codex OAuth card did not appear after 30s. Page may still be loading or not logged in. ' +
'Current URL: ' + location.href
);
}
if (!loginBtn) {
throw new Error('Found Codex OAuth card but no login button inside it. URL: ' + location.href);
}
// Check if button is disabled (already clicked / loading)
if (loginBtn.disabled) {
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
} else {
simulateClick(loginBtn);
log('Step 1: Clicked login button, waiting for auth URL...');
}
// Wait for the auth URL to appear in the specific div
let authUrlEl = null;
try {
authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
} catch {
throw new Error(
'Auth URL did not appear after clicking login. ' +
'Check if VPS panel is logged in and Codex service is running. URL: ' + location.href
);
}
const oauthUrl = (authUrlEl.textContent || '').trim();
if (!oauthUrl || !oauthUrl.startsWith('http')) {
throw new Error(`Invalid OAuth URL found: "${oauthUrl.slice(0, 50)}". Expected URL starting with http.`);
}
log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok');
reportComplete(1, { oauthUrl: oauthUrl.trim() });
reportComplete(1, { oauthUrl });
}
// ============================================================
// Step 9: VPS Verify
// Step 9: VPS Verify — paste localhost URL and submit
// ============================================================
async function step9_vpsVerify() {
log('Step 9: Getting localhost URL from storage...');
// Get localhostUrl from storage (via Background)
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
const localhostUrl = state.localhostUrl;
if (!localhostUrl) {
throw new Error('No localhost URL found. Complete step 8 first.');
}
log(`Step 9: Looking for URL input field on VPS panel...`);
log('Step 9: Looking for callback URL input...');
// Try to find URL input field
// Find the callback URL input
// Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
let urlInput = null;
try {
urlInput = await waitForElement(
'input[placeholder*="localhost"], input[placeholder*="callback"], input[placeholder*="URL"], input[placeholder*="url"], input[name*="callback"], input[name*="url"]',
10000
);
urlInput = await waitForElement('[class*="callbackSection"] input.input', 10000);
} catch {
// Fallback: find any text input that's visible and empty
const inputs = document.querySelectorAll('input[type="text"], input:not([type])');
for (const input of inputs) {
if (input.offsetParent !== null && !input.value) {
urlInput = input;
log('Step 9: Using fallback empty input field');
break;
}
}
if (!urlInput) {
throw new Error(
'Could not find URL input field on VPS panel. ' +
'Check DOM structure in DevTools. URL: ' + location.href
);
try {
urlInput = await waitForElement('input[placeholder*="localhost"]', 5000);
} catch {
throw new Error('Could not find callback URL input on VPS panel. URL: ' + location.href);
}
}
fillInput(urlInput, localhostUrl);
log(`Step 9: Filled URL input with: ${localhostUrl}`);
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
// Find and click verify button
let verifyBtn = null;
// Find and click "提交回调 URL" button
let submitBtn = null;
try {
verifyBtn = await waitForElementByText(
'button, [role="button"], .el-button, a',
/verif|确认|验证|submit|提交/i,
10000
submitBtn = await waitForElementByText(
'[class*="callbackActions"] button, [class*="callbackSection"] button',
/提交/,
5000
);
} catch {
throw new Error(
'Could not find verify/submit button. ' +
'Check VPS panel DOM in DevTools. URL: ' + location.href
);
try {
submitBtn = await waitForElementByText('button.btn', /提交回调/, 5000);
} catch {
throw new Error('Could not find "提交回调 URL" button. URL: ' + location.href);
}
}
simulateClick(submitBtn);
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
// Wait for "认证成功!" status badge to appear
try {
await waitForElementByText('.status-badge, [class*="status"]', /认证成功/, 30000);
log('Step 9: Authentication successful!', 'ok');
} catch {
// Check if there's an error message instead
const statusEl = document.querySelector('.status-badge, [class*="status"]');
const statusText = statusEl ? statusEl.textContent : 'unknown';
if (/成功|success/i.test(statusText)) {
log('Step 9: Authentication successful!', 'ok');
} else {
log(`Step 9: Status after submit: "${statusText}". May still be processing.`, 'warn');
}
}
simulateClick(verifyBtn);
log('Step 9: Clicked verify button', 'ok');
reportComplete(9);
}
+78 -2
View File
@@ -23,6 +23,11 @@ header h1 {
font-weight: 600;
}
.header-btns {
display: flex;
gap: 6px;
}
#btn-reset {
padding: 4px 10px;
font-size: 12px;
@@ -34,6 +39,49 @@ header h1 {
}
#btn-reset:hover { background: #c82333; }
.btn-auto {
padding: 4px 10px;
font-size: 12px;
background: #28a745;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
.btn-auto:hover { background: #218838; }
.btn-auto:disabled { background: #6c757d; cursor: not-allowed; }
.auto-continue-bar {
margin-top: 8px;
padding: 8px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.auto-hint {
font-size: 11px;
color: #856404;
flex: 1;
}
.btn-continue {
padding: 4px 12px;
font-size: 12px;
background: #007bff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
white-space: nowrap;
}
.btn-continue:hover { background: #0069d9; }
/* Data Section */
#data-section {
background: #fff;
@@ -141,6 +189,34 @@ header h1 {
}
.log-info { color: #d4d4d4; }
.log-ok { color: #4ec9b0; }
.log-ok { color: #4ec9b0; font-weight: bold; }
.log-warn { color: #dcdcaa; }
.log-error { color: #f44747; }
.log-error { color: #f44747; font-weight: bold; }
.log-step-tag {
display: inline-block;
background: #3a3d41;
color: #569cd6;
border-radius: 3px;
padding: 0 4px;
margin-right: 4px;
font-weight: bold;
font-size: 10px;
}
.log-step-tag.step-1 { color: #4fc1ff; }
.log-step-tag.step-2 { color: #c586c0; }
.log-step-tag.step-3 { color: #dcdcaa; }
.log-step-tag.step-4 { color: #ce9178; }
.log-step-tag.step-5 { color: #b5cea8; }
.log-step-tag.step-6 { color: #4fc1ff; }
.log-step-tag.step-7 { color: #ce9178; }
.log-step-tag.step-8 { color: #c586c0; }
.log-step-tag.step-9 { color: #b5cea8; }
.log-time { color: #6a9955; }
.log-level { font-weight: bold; }
.log-level-info { color: #569cd6; }
.log-level-ok { color: #4ec9b0; }
.log-level-warn { color: #dcdcaa; }
.log-level-error { color: #f44747; }
+8 -1
View File
@@ -9,7 +9,10 @@
<body>
<header>
<h1>Multi-Page Automation</h1>
<button id="btn-reset" title="Reset all steps">Reset</button>
<div class="header-btns">
<button id="btn-auto-run" class="btn-auto" title="Run all steps automatically">Auto Run</button>
<button id="btn-reset" title="Reset all steps">Reset</button>
</div>
</header>
<section id="data-section">
@@ -29,6 +32,10 @@
<label>Status:</label>
<span id="display-status" class="data-value">Waiting</span>
</div>
<div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
<span class="auto-hint">Paste DuckDuckGo email above, then:</span>
<button id="btn-auto-continue" class="btn-continue">Continue Auto</button>
</div>
</section>
<section id="steps-section">
+70 -1
View File
@@ -128,11 +128,30 @@ function appendLog(entry) {
const levelLabel = entry.level.toUpperCase().padEnd(5);
const line = document.createElement('div');
line.className = `log-${entry.level}`;
line.textContent = `${time} [${levelLabel}] ${entry.message}`;
// Extract step number from message (e.g., "Step 4: ..." or "[vps-panel] Step 1: ...")
const stepMatch = entry.message.match(/Step (\d)/);
const stepNum = stepMatch ? stepMatch[1] : null;
// Build rich HTML
let html = `<span class="log-time">${time}</span> `;
html += `<span class="log-level log-level-${entry.level}">[${levelLabel}]</span> `;
if (stepNum) {
html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
}
html += `<span>${escapeHtml(entry.message)}</span>`;
line.innerHTML = html;
logArea.appendChild(line);
logArea.scrollTop = logArea.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ============================================================
// Button Handlers
// ============================================================
@@ -163,6 +182,32 @@ document.querySelectorAll('.step-btn').forEach(btn => {
});
});
// Auto Run button
const btnAutoRun = document.getElementById('btn-auto-run');
const btnAutoContinue = document.getElementById('btn-auto-continue');
const autoContinueBar = document.getElementById('auto-continue-bar');
btnAutoRun.addEventListener('click', async () => {
btnAutoRun.disabled = true;
btnAutoRun.textContent = 'Running...';
await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel' });
});
btnAutoContinue.addEventListener('click', async () => {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
return;
}
autoContinueBar.style.display = 'none';
btnAutoRun.textContent = 'Running...';
await chrome.runtime.sendMessage({
type: 'RESUME_AUTO_RUN',
source: 'sidepanel',
payload: { email },
});
});
// Reset button
btnReset.addEventListener('click', async () => {
if (confirm('Reset all steps and data?')) {
@@ -177,6 +222,9 @@ btnReset.addEventListener('click', async () => {
displayStatus.classList.remove('has-value');
logArea.innerHTML = '';
document.querySelectorAll('.step-status').forEach(el => el.textContent = '\u2B1A');
btnAutoRun.disabled = false;
btnAutoRun.textContent = 'Auto Run';
autoContinueBar.style.display = 'none';
updateButtonStates();
}
});
@@ -222,6 +270,27 @@ chrome.runtime.onMessage.addListener((message) => {
}
break;
}
case 'AUTO_RUN_STATUS': {
const { phase } = message.payload;
switch (phase) {
case 'waiting_email':
autoContinueBar.style.display = 'flex';
btnAutoRun.textContent = 'Waiting...';
break;
case 'complete':
btnAutoRun.disabled = false;
btnAutoRun.textContent = 'Auto Run';
autoContinueBar.style.display = 'none';
break;
case 'stopped':
btnAutoRun.disabled = false;
btnAutoRun.textContent = 'Auto Run';
autoContinueBar.style.display = 'none';
break;
}
break;
}
}
});