From 1c1e589bdcebfc4a04e3ada30621ee5418e262cd Mon Sep 17 00:00:00 2001 From: unknown <1249156074@qq.com> Date: Sun, 5 Apr 2026 09:42:17 +0800 Subject: [PATCH] feat: add Auto Run mode for full automated flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- background.js | 227 +++++++++++++++++++++++++++---- content/qq-mail.js | 286 ++++++++++++++------------------------- content/signup-page.js | 264 ++++++++++++++++++++++++++---------- content/vps-panel.js | 237 ++++++++++++++------------------ sidepanel/sidepanel.css | 80 ++++++++++- sidepanel/sidepanel.html | 9 +- sidepanel/sidepanel.js | 71 +++++++++- 7 files changed, 743 insertions(+), 431 deletions(-) diff --git a/background.js b/background.js index 4de8f6e..5757a8b 100644 --- a/background.js +++ b/background.js @@ -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); - }); + })(); }); } diff --git a/content/qq-mail.js b/content/qq-mail.js index e830b6d..d2cabaa 100644 --- a/content/qq-mail.js +++ b/content/qq-mail.js @@ -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; } diff --git a/content/signup-page.js b/content/signup-page.js index 213b03d..3861ec6 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -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: + // - Birthday: React Aria DateField with 3 spinbutton divs (year/month/day) + // + + + // --- 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); } diff --git a/content/vps-panel.js b/content/vps-panel.js index 5617c74..4855a05 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -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): +//
+//
+// Codex OAuth +// +//
+//
+//
+//
授权链接:
+//
https://auth.openai.com/...
+//
+// +// +//
+//
+//
+// +// +//
+//
+//
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: 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); } diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 526d650..cc4022d 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -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; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index bf2a0d0..5915d59 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -9,7 +9,10 @@

Multi-Page Automation

- +
+ + +
@@ -29,6 +32,10 @@ Waiting +
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 802e4db..36947f0 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -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 = `${time} `; + html += `[${levelLabel}] `; + if (stepNum) { + html += `S${stepNum}`; + } + html += `${escapeHtml(entry.message)}`; + + 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; + } } });