From bcc33ecd1218b436d2e8d42c29fe0e6224d4d026 Mon Sep 17 00:00:00 2001 From: unknown <1249156074@qq.com> Date: Sun, 5 Apr 2026 10:15:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20multi-run=20support=20=E2=80=94=20execu?= =?UTF-8?q?te=20N=20times=20with=20one=20click?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Side Panel: number input next to Auto button (1-50) - Background: autoRunLoop wraps full flow in a for-loop - Each run: reset state (keep VPS/mail settings) → steps 1-9 - Pause for email on each run, resume continues - Stop on error - Side Panel shows run progress: "Running (2/5)", "Paused (3/5)" - AUTO_RUN_RESET message resets UI between runs - Default mail provider changed to 163 - 163 mail: faster polling (500ms intervals), skip child iframe ready signals --- background.js | 127 +++++++++++++++++++++++++-------------- content/mail-163.js | 39 ++++++++---- content/utils.js | 6 +- sidepanel/sidepanel.css | 22 +++++++ sidepanel/sidepanel.html | 13 ++-- sidepanel/sidepanel.js | 30 ++++++++- 6 files changed, 172 insertions(+), 65 deletions(-) diff --git a/background.js b/background.js index 1f5d04d..6d50e72 100644 --- a/background.js +++ b/background.js @@ -23,7 +23,7 @@ const DEFAULT_STATE = { tabRegistry: {}, logs: [], vpsUrl: 'http://154.26.182.181:8317/management.html#/oauth', - mailProvider: 'qq', // 'qq' or '163' + mailProvider: '163', // 'qq' or '163' }; async function getState() { @@ -235,7 +235,8 @@ async function handleMessage(message, sender) { } case 'AUTO_RUN': { - autoRun(); // fire-and-forget, runs in background + const totalRuns = message.payload?.totalRuns || 1; + autoRunLoop(totalRuns); // fire-and-forget return { ok: true }; } @@ -388,65 +389,101 @@ async function executeStepAndWait(step, delayAfter = 2000) { // ============================================================ let autoRunActive = false; +let autoRunCurrentRun = 0; +let autoRunTotalRuns = 1; -async function autoRun() { +// Outer loop: runs the full flow N times +async function autoRunLoop(totalRuns) { if (autoRunActive) { await addLog('Auto run already in progress', 'warn'); return; } autoRunActive = true; + autoRunTotalRuns = totalRuns; 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); + for (let run = 1; run <= totalRuns; run++) { + autoRunCurrentRun = run; - // 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(() => {}); + if (run > 1) { + // Reset state for next run (keep vpsUrl, mailProvider settings) + await addLog(`=== Resetting for run ${run}/${totalRuns} ===`, 'info'); + const state = await getState(); + const keepSettings = { vpsUrl: state.vpsUrl, mailProvider: state.mailProvider }; + await resetState(); + await setState(keepSettings); + // Broadcast reset to side panel + chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {}); + await new Promise(r => setTimeout(r, 1000)); + } - // Wait here — resumed by RESUME_AUTO_RUN message from side panel + await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info'); + const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } }); - } 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(() => {}); + try { + chrome.runtime.sendMessage(status('running')).catch(() => {}); + + await executeStepAndWait(1, 2000); + await executeStepAndWait(2, 2000); + + // Pause for email + await addLog(`=== Run ${run}/${totalRuns} PAUSED: Paste DuckDuckGo email, click Continue ===`, 'warn'); + chrome.runtime.sendMessage(status('waiting_email')).catch(() => {}); + + // Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves + await waitForResume(); + + const state = await getState(); + if (!state.email) { + await addLog('Cannot resume: no email address.', 'error'); + break; + } + + await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info'); + chrome.runtime.sendMessage(status('running')).catch(() => {}); + + await executeStepAndWait(3, 3000); + await executeStepAndWait(4, 2000); + await executeStepAndWait(5, 3000); + await executeStepAndWait(6, 3000); + await executeStepAndWait(7, 2000); + await executeStepAndWait(8, 2000); + await executeStepAndWait(9, 1000); + + await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok'); + + } catch (err) { + await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error'); + chrome.runtime.sendMessage(status('stopped')).catch(() => {}); + break; // Stop on error + } } + + await addLog(`=== All ${autoRunTotalRuns} runs finished ===`, 'ok'); + chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns } }).catch(() => {}); + autoRunActive = false; + await setState({ autoRunning: false }); +} + +// Promise-based pause/resume mechanism +let resumeResolver = null; + +function waitForResume() { + return new Promise((resolve) => { + resumeResolver = resolve; + }); } 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 }); + const state = await getState(); + if (!state.email) { + await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error'); + return; + } + if (resumeResolver) { + resumeResolver(); + resumeResolver = null; } } diff --git a/content/mail-163.js b/content/mail-163.js index 778caea..d04f841 100644 --- a/content/mail-163.js +++ b/content/mail-163.js @@ -16,8 +16,14 @@ const isTopFrame = window === window.top; console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child'); +// Only operate in the top frame — child iframes don't have the inbox +if (!isTopFrame) { + console.log(MAIL163_PREFIX, 'Skipping child frame'); + // Don't report ready or handle messages from child frames +} else { + // ============================================================ -// Message Handler +// Message Handler (top frame only) // ============================================================ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { @@ -75,25 +81,34 @@ async function handlePollEmail(step, payload) { log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`); - // First, click on "收件箱" in left sidebar to ensure we're in inbox view - await sleep(2000); - const inboxLink = document.querySelector('.nui-tree-item-text[title="收件箱"]'); - if (inboxLink) { + // Wait for sidebar to load, then click "收件箱" + log(`Step ${step}: Waiting for 163 Mail sidebar to load...`); + try { + const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000); inboxLink.click(); log(`Step ${step}: Clicked inbox in sidebar`); - await sleep(2000); + } catch { + log(`Step ${step}: Could not find inbox link, trying to proceed anyway...`, 'warn'); + } + + // Wait for mail list — poll every 500ms, max 10s + log(`Step ${step}: Waiting for mail list...`); + let items = []; + for (let i = 0; i < 20; i++) { + items = findMailItems(); + if (items.length > 0) break; + await sleep(500); } - // Wait for mail list to load - let items = findMailItems(); if (items.length === 0) { - log(`Step ${step}: Waiting for mail list to appear...`); - await sleep(5000); + log(`Step ${step}: Mail list not found, trying refresh...`, 'warn'); + await refreshInbox(); + await sleep(2000); items = findMailItems(); } if (items.length === 0) { - throw new Error('163 Mail list did not load. Make sure inbox is open.'); + throw new Error('163 Mail list did not load. Make sure inbox is open and has emails.'); } log(`Step ${step}: Mail list loaded, ${items.length} items found`); @@ -206,3 +221,5 @@ function extractVerificationCode(text) { return null; } + +} // end of isTopFrame else block diff --git a/content/utils.js b/content/utils.js index e82f556..6e1c218 100644 --- a/content/utils.js +++ b/content/utils.js @@ -224,4 +224,8 @@ function sleep(ms) { } // Auto-report ready on load -reportReady(); +// Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration +const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163') && window !== window.top; +if (!_isMailChildFrame) { + reportReady(); +} diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 1caac53..fd30ea0 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -172,6 +172,28 @@ header { color: #fff; } .btn-success:hover { opacity: 0.9; box-shadow: 0 2px 8px var(--green-soft); } + +.run-group { + display: flex; + align-items: center; + gap: 4px; +} + +.run-count-input { + width: 38px; + padding: 5px 4px; + text-align: center; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 600; + outline: none; +} +.run-count-input:focus { border-color: var(--blue); } +.run-count-input::-webkit-inner-spin-button { opacity: 0.5; } .btn-success:disabled { background: var(--bg-elevated); color: var(--text-muted); cursor: not-allowed; box-shadow: none; } .btn-ghost { diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index bf7e08a..bb5ecc7 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -18,10 +18,13 @@

MultiPage

- +
+ + +