Переглянути джерело

feat: multi-run support — execute N times with one click

- 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
unknown 4 місяців тому
батько
коміт
bcc33ecd12
6 змінених файлів з 170 додано та 63 видалено
  1. 80 43
      background.js
  2. 28 11
      content/mail-163.js
  3. 5 1
      content/utils.js
  4. 22 0
      sidepanel/sidepanel.css
  5. 8 5
      sidepanel/sidepanel.html
  6. 27 3
      sidepanel/sidepanel.js

+ 80 - 43
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(() => {});
 
-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;
+      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
     }
+  }
 
-    // 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 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 });
+}
 
-    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 "认证成功!"
+// Promise-based pause/resume mechanism
+let resumeResolver = null;
 
-    await addLog('=== Auto Run COMPLETE! All 9 steps finished successfully ===', 'ok');
-    chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete' } }).catch(() => {});
+function waitForResume() {
+  return new Promise((resolve) => {
+    resumeResolver = resolve;
+  });
+}
 
-  } 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 });
+async function resumeAutoRun() {
+  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;
   }
 }
 

+ 28 - 11
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

+ 5 - 1
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();
+}

+ 22 - 0
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 {

+ 8 - 5
sidepanel/sidepanel.html

@@ -18,10 +18,13 @@
       <h1>MultiPage</h1>
     </div>
     <div class="header-btns">
-      <button id="btn-auto-run" class="btn btn-success" title="Run all steps automatically">
-        <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
-        Auto
-      </button>
+      <div class="run-group">
+        <input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="Number of runs" />
+        <button id="btn-auto-run" class="btn btn-success" title="Run all steps automatically">
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
+          Auto
+        </button>
+      </div>
       <button id="btn-reset" class="btn btn-ghost" title="Reset all steps">
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
           <polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
@@ -47,8 +50,8 @@
       <div class="data-row">
         <span class="data-label">Mail</span>
         <select id="select-mail-provider" class="data-select">
-          <option value="qq">QQ Mail (wx.mail.qq.com)</option>
           <option value="163">163 Mail (mail.163.com)</option>
+          <option value="qq">QQ Mail (wx.mail.qq.com)</option>
         </select>
       </div>
       <div class="data-row">

+ 27 - 3
sidepanel/sidepanel.js

@@ -21,6 +21,7 @@ const autoContinueBar = document.getElementById('auto-continue-bar');
 const btnClearLog = document.getElementById('btn-clear-log');
 const inputVpsUrl = document.getElementById('input-vps-url');
 const selectMailProvider = document.getElementById('select-mail-provider');
+const inputRunCount = document.getElementById('input-run-count');
 
 // ============================================================
 // State Restore on load
@@ -204,9 +205,11 @@ document.querySelectorAll('.step-btn').forEach(btn => {
 
 // Auto Run
 btnAutoRun.addEventListener('click', async () => {
+  const totalRuns = parseInt(inputRunCount.value) || 1;
   btnAutoRun.disabled = true;
+  inputRunCount.disabled = true;
   btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> Running...';
-  await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel' });
+  await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel', payload: { totalRuns } });
 });
 
 btnAutoContinue.addEventListener('click', async () => {
@@ -297,6 +300,21 @@ chrome.runtime.onMessage.addListener((message) => {
       break;
     }
 
+    case 'AUTO_RUN_RESET': {
+      // Reset UI for next run (but keep buttons disabled since auto-run is still going)
+      displayOauthUrl.textContent = 'Waiting...';
+      displayOauthUrl.classList.remove('has-value');
+      displayLocalhostUrl.textContent = 'Waiting...';
+      displayLocalhostUrl.classList.remove('has-value');
+      inputEmail.value = '';
+      displayStatus.textContent = 'Ready';
+      statusBar.className = 'status-bar';
+      document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
+      document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
+      updateProgressCounter();
+      break;
+    }
+
     case 'DATA_UPDATED': {
       if (message.payload.oauthUrl) {
         displayOauthUrl.textContent = message.payload.oauthUrl;
@@ -310,19 +328,25 @@ chrome.runtime.onMessage.addListener((message) => {
     }
 
     case 'AUTO_RUN_STATUS': {
-      const { phase } = message.payload;
+      const { phase, currentRun, totalRuns } = message.payload;
+      const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns})` : '';
       switch (phase) {
         case 'waiting_email':
           autoContinueBar.style.display = 'flex';
-          btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Paused';
+          btnAutoRun.innerHTML = `Paused${runLabel}`;
+          break;
+        case 'running':
+          btnAutoRun.innerHTML = `Running${runLabel}`;
           break;
         case 'complete':
           btnAutoRun.disabled = false;
+          inputRunCount.disabled = false;
           btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
           autoContinueBar.style.display = 'none';
           break;
         case 'stopped':
           btnAutoRun.disabled = false;
+          inputRunCount.disabled = false;
           btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
           autoContinueBar.style.display = 'none';
           break;