Quellcode durchsuchen

feat: support configurable random delays between auto steps

- 合并 PR #44 的核心改动:新增 Auto 步间随机延迟配置,并在后台执行前应用该随机等待\n- 本地补充修复:无\n- 影响范围:background auto orchestration、sidepanel auto settings、README、tests
QLHazyCoder vor 4 Monaten
Ursprung
Commit
a336ea49c1
5 geänderte Dateien mit 265 neuen und 0 gelöschten Zeilen
  1. 5 0
      README.md
  2. 72 0
      background.js
  3. 13 0
      sidepanel/sidepanel.html
  4. 70 0
      sidepanel/sidepanel.js
  5. 105 0
      tests/auto-step-random-delay.test.js

+ 5 - 0
README.md

@@ -324,6 +324,10 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 
 支持多轮运行,运行次数由右上角数字框决定。
 
+`延迟` 里的“启动前倒计时”只控制整轮 Auto 开始前要不要先倒计时多少分钟。
+
+`步间随机` 控制 Auto 流程里**每一步真正执行前**的随机等待秒数,默认是 `12 ~ 18` 秒。这个设置只影响 Auto,不影响你手动单步点击执行。
+
 如果当前面板里已经存在未完成进度,点击 `Auto` 时会弹出选择:
 
 - `重新开始`:重置当前流程进度,从 Step 1 开始新一轮
@@ -366,6 +370,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 - Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
 - 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制
 - 选择 `继续当前` 时,后台不会先做大而全的前置校验,而是从当前步骤状态直接继续;缺什么条件,就在运行到那一步时再报错或暂停
+- 除了现有的页面切换等待外,Auto 还会在每一步执行前额外随机等待一段时间;默认是 `12 ~ 18` 秒,可在侧边栏 `步间随机` 中改成你自己的范围
 
 ## 详细步骤说明
 

+ 72 - 0
background.js

@@ -39,6 +39,10 @@ const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
 const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
 const AUTO_RUN_DELAY_MIN_MINUTES = 1;
 const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
+const AUTO_STEP_RANDOM_DELAY_MIN_ALLOWED_SECONDS = 0;
+const AUTO_STEP_RANDOM_DELAY_MAX_ALLOWED_SECONDS = 600;
+const AUTO_STEP_RANDOM_DELAY_DEFAULT_MIN_SECONDS = 12;
+const AUTO_STEP_RANDOM_DELAY_DEFAULT_MAX_SECONDS = 18;
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 
 initializeSessionStorageAccess();
@@ -60,6 +64,8 @@ const PERSISTED_SETTING_DEFAULTS = {
   autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
   autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
   autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
+  autoStepRandomDelayMinSeconds: AUTO_STEP_RANDOM_DELAY_DEFAULT_MIN_SECONDS, // 自动运行每一步执行前的最小随机等待秒数。
+  autoStepRandomDelayMaxSeconds: AUTO_STEP_RANDOM_DELAY_DEFAULT_MAX_SECONDS, // 自动运行每一步执行前的最大随机等待秒数。
   mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。
   emailGenerator: 'duck', // 注册邮箱生成方式:duck / cloudflare。
   inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
@@ -119,6 +125,32 @@ function normalizeAutoRunDelayMinutes(value) {
   );
 }
 
+function normalizeAutoStepRandomDelaySeconds(value, fallback = AUTO_STEP_RANDOM_DELAY_DEFAULT_MIN_SECONDS) {
+  const numeric = Number(value);
+  if (!Number.isFinite(numeric)) {
+    return fallback;
+  }
+  return Math.min(
+    AUTO_STEP_RANDOM_DELAY_MAX_ALLOWED_SECONDS,
+    Math.max(AUTO_STEP_RANDOM_DELAY_MIN_ALLOWED_SECONDS, Math.floor(numeric))
+  );
+}
+
+function normalizeAutoStepRandomDelayRange(settings = {}) {
+  const minSeconds = normalizeAutoStepRandomDelaySeconds(
+    settings.autoStepRandomDelayMinSeconds,
+    PERSISTED_SETTING_DEFAULTS.autoStepRandomDelayMinSeconds
+  );
+  const maxSeconds = Math.max(
+    minSeconds,
+    normalizeAutoStepRandomDelaySeconds(
+      settings.autoStepRandomDelayMaxSeconds,
+      PERSISTED_SETTING_DEFAULTS.autoStepRandomDelayMaxSeconds
+    )
+  );
+  return { minSeconds, maxSeconds };
+}
+
 function normalizeRunCount(value) {
   const numeric = Number(value);
   if (!Number.isFinite(numeric)) {
@@ -216,6 +248,16 @@ function normalizePersistentSettingValue(key, value) {
       return Boolean(value);
     case 'autoRunDelayMinutes':
       return normalizeAutoRunDelayMinutes(value);
+    case 'autoStepRandomDelayMinSeconds':
+      return normalizeAutoStepRandomDelaySeconds(
+        value,
+        PERSISTED_SETTING_DEFAULTS.autoStepRandomDelayMinSeconds
+      );
+    case 'autoStepRandomDelayMaxSeconds':
+      return normalizeAutoStepRandomDelaySeconds(
+        value,
+        PERSISTED_SETTING_DEFAULTS.autoStepRandomDelayMaxSeconds
+      );
     case 'mailProvider':
       return normalizeMailProvider(value);
     case 'emailGenerator':
@@ -264,6 +306,15 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
     payload.cloudflareDomains = domains;
   }
 
+  if (payload.autoStepRandomDelayMinSeconds !== undefined || payload.autoStepRandomDelayMaxSeconds !== undefined) {
+    const normalizedDelayRange = normalizeAutoStepRandomDelayRange({
+      autoStepRandomDelayMinSeconds: payload.autoStepRandomDelayMinSeconds,
+      autoStepRandomDelayMaxSeconds: payload.autoStepRandomDelayMaxSeconds,
+    });
+    payload.autoStepRandomDelayMinSeconds = normalizedDelayRange.minSeconds;
+    payload.autoStepRandomDelayMaxSeconds = normalizedDelayRange.maxSeconds;
+  }
+
   return payload;
 }
 
@@ -2422,6 +2473,12 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
   await sleepWithStop(duration);
 }
 
+function getAutoStepRandomDelayMs(min, max) {
+  const normalizedMin = Math.max(0, Math.floor(Number(min) || 0));
+  const normalizedMax = Math.max(normalizedMin, Math.floor(Number(max) || normalizedMin));
+  return Math.floor(Math.random() * (normalizedMax - normalizedMin + 1)) + normalizedMin;
+}
+
 async function clickWithDebugger(tabId, rect) {
   throwIfStopped();
   if (!tabId) {
@@ -3028,6 +3085,19 @@ async function executeStep(step) {
 async function executeStepAndWait(step, delayAfter = 2000) {
   throwIfStopped();
 
+  const delaySettings = normalizeAutoStepRandomDelayRange(await getState());
+  const randomDelayMs = getAutoStepRandomDelayMs(
+    delaySettings.minSeconds * 1000,
+    delaySettings.maxSeconds * 1000
+  );
+  if (randomDelayMs > 0) {
+    await addLog(
+      `自动运行:步骤 ${step} 执行前随机等待 ${delaySettings.minSeconds}-${delaySettings.maxSeconds} 秒,本次约 ${Math.round(randomDelayMs / 1000)} 秒,避免节奏过快。`,
+      'info'
+    );
+    await sleepWithStop(randomDelayMs);
+  }
+
   if (AUTO_RUN_BACKGROUND_COMPLETED_STEPS.has(step)) {
     await addLog(`自动运行:步骤 ${step} 由后台流程负责收尾,执行函数返回后将直接进入下一步。`, 'info');
     await executeStep(step);
@@ -3376,6 +3446,8 @@ async function autoRunLoop(totalRuns, options = {}) {
         autoRunSkipFailures: prevState.autoRunSkipFailures,
         autoRunDelayEnabled: prevState.autoRunDelayEnabled,
         autoRunDelayMinutes: prevState.autoRunDelayMinutes,
+        autoStepRandomDelayMinSeconds: prevState.autoStepRandomDelayMinSeconds,
+        autoStepRandomDelayMaxSeconds: prevState.autoStepRandomDelayMaxSeconds,
         mailProvider: prevState.mailProvider,
         emailGenerator: prevState.emailGenerator,
         inbucketHost: prevState.inbucketHost,

+ 13 - 0
sidepanel/sidepanel.html

@@ -192,6 +192,19 @@
           </div>
         </div>
       </div>
+      <div class="data-row">
+        <span class="data-label">步间随机</span>
+        <div class="data-inline auto-delay-inline">
+          <div class="auto-delay-controls">
+            <input type="number" id="input-auto-step-delay-min-seconds" class="data-input auto-delay-input" value="12" min="0"
+              max="600" step="1" title="自动运行每一步执行前的最小随机等待秒数" />
+            <span class="data-unit">~</span>
+            <input type="number" id="input-auto-step-delay-max-seconds" class="data-input auto-delay-input" value="18" min="0"
+              max="600" step="1" title="自动运行每一步执行前的最大随机等待秒数" />
+            <span class="data-unit">秒</span>
+          </div>
+        </div>
+      </div>
       <div class="data-row">
         <span class="data-label">OAuth</span>
         <span id="display-oauth-url" class="data-value mono truncate">等待中...</span>

+ 70 - 0
sidepanel/sidepanel.js

@@ -85,6 +85,8 @@ const inputRunCount = document.getElementById('input-run-count');
 const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
 const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
 const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
+const inputAutoStepDelayMinSeconds = document.getElementById('input-auto-step-delay-min-seconds');
+const inputAutoStepDelayMaxSeconds = document.getElementById('input-auto-step-delay-max-seconds');
 const autoStartModal = document.getElementById('auto-start-modal');
 const autoStartTitle = autoStartModal?.querySelector('.modal-title');
 const autoStartMessage = document.getElementById('auto-start-message');
@@ -108,6 +110,10 @@ const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
 const AUTO_DELAY_MIN_MINUTES = 1;
 const AUTO_DELAY_MAX_MINUTES = 1440;
 const AUTO_DELAY_DEFAULT_MINUTES = 30;
+const AUTO_STEP_DELAY_MIN_SECONDS = 0;
+const AUTO_STEP_DELAY_MAX_SECONDS = 600;
+const AUTO_STEP_DELAY_DEFAULT_MIN_SECONDS = 12;
+const AUTO_STEP_DELAY_DEFAULT_MAX_SECONDS = 18;
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
 
 let latestState = null;
@@ -447,6 +453,23 @@ function normalizeAutoDelayMinutes(value) {
   return Math.min(AUTO_DELAY_MAX_MINUTES, Math.max(AUTO_DELAY_MIN_MINUTES, Math.floor(numeric)));
 }
 
+function normalizeAutoStepDelaySeconds(value, fallback = AUTO_STEP_DELAY_DEFAULT_MIN_SECONDS) {
+  const numeric = Number(value);
+  if (!Number.isFinite(numeric)) {
+    return fallback;
+  }
+  return Math.min(AUTO_STEP_DELAY_MAX_SECONDS, Math.max(AUTO_STEP_DELAY_MIN_SECONDS, Math.floor(numeric)));
+}
+
+function normalizeAutoStepDelayRange(minValue, maxValue) {
+  const minSeconds = normalizeAutoStepDelaySeconds(minValue, AUTO_STEP_DELAY_DEFAULT_MIN_SECONDS);
+  const maxSeconds = Math.max(
+    minSeconds,
+    normalizeAutoStepDelaySeconds(maxValue, AUTO_STEP_DELAY_DEFAULT_MAX_SECONDS)
+  );
+  return { minSeconds, maxSeconds };
+}
+
 function updateAutoDelayInputState() {
   const scheduled = isAutoRunScheduledPhase();
   inputAutoDelayEnabled.disabled = scheduled;
@@ -598,6 +621,10 @@ function collectSettingsPayload() {
   const selectedCloudflareDomain = normalizeCloudflareDomainValue(
     !cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
   ) || activeDomain;
+  const autoStepDelayRange = normalizeAutoStepDelayRange(
+    inputAutoStepDelayMinSeconds.value,
+    inputAutoStepDelayMaxSeconds.value
+  );
   return {
     panelMode: selectPanelMode.value,
     vpsUrl: inputVpsUrl.value.trim(),
@@ -617,6 +644,8 @@ function collectSettingsPayload() {
     autoRunSkipFailures: inputAutoSkipFailures.checked,
     autoRunDelayEnabled: inputAutoDelayEnabled.checked,
     autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
+    autoStepRandomDelayMinSeconds: autoStepDelayRange.minSeconds,
+    autoStepRandomDelayMaxSeconds: autoStepDelayRange.maxSeconds,
   };
 }
 
@@ -858,6 +887,12 @@ function applySettingsState(state) {
   inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
   inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
   inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
+  const autoStepDelayRange = normalizeAutoStepDelayRange(
+    state?.autoStepRandomDelayMinSeconds,
+    state?.autoStepRandomDelayMaxSeconds
+  );
+  inputAutoStepDelayMinSeconds.value = String(autoStepDelayRange.minSeconds);
+  inputAutoStepDelayMaxSeconds.value = String(autoStepDelayRange.maxSeconds);
   if (state?.autoRunTotalRuns) {
     inputRunCount.value = String(state.autoRunTotalRuns);
   }
@@ -2380,6 +2415,33 @@ inputAutoDelayMinutes.addEventListener('blur', () => {
   saveSettings({ silent: true }).catch(() => { });
 });
 
+function syncAutoStepDelayInputs() {
+  const range = normalizeAutoStepDelayRange(
+    inputAutoStepDelayMinSeconds.value,
+    inputAutoStepDelayMaxSeconds.value
+  );
+  inputAutoStepDelayMinSeconds.value = String(range.minSeconds);
+  inputAutoStepDelayMaxSeconds.value = String(range.maxSeconds);
+}
+
+inputAutoStepDelayMinSeconds.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputAutoStepDelayMinSeconds.addEventListener('blur', () => {
+  syncAutoStepDelayInputs();
+  saveSettings({ silent: true }).catch(() => { });
+});
+
+inputAutoStepDelayMaxSeconds.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputAutoStepDelayMaxSeconds.addEventListener('blur', () => {
+  syncAutoStepDelayInputs();
+  saveSettings({ silent: true }).catch(() => { });
+});
+
 // ============================================================
 // Listen for Background broadcasts
 // ============================================================
@@ -2477,6 +2539,14 @@ chrome.runtime.onMessage.addListener((message) => {
       if (message.payload.autoRunDelayMinutes !== undefined) {
         inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(message.payload.autoRunDelayMinutes));
       }
+      if (message.payload.autoStepRandomDelayMinSeconds !== undefined || message.payload.autoStepRandomDelayMaxSeconds !== undefined) {
+        const autoStepDelayRange = normalizeAutoStepDelayRange(
+          message.payload.autoStepRandomDelayMinSeconds ?? inputAutoStepDelayMinSeconds.value,
+          message.payload.autoStepRandomDelayMaxSeconds ?? inputAutoStepDelayMaxSeconds.value
+        );
+        inputAutoStepDelayMinSeconds.value = String(autoStepDelayRange.minSeconds);
+        inputAutoStepDelayMaxSeconds.value = String(autoStepDelayRange.maxSeconds);
+      }
       break;
     }
 

+ 105 - 0
tests/auto-step-random-delay.test.js

@@ -0,0 +1,105 @@
+const assert = require('assert');
+const fs = require('fs');
+
+const source = fs.readFileSync('background.js', 'utf8');
+
+function extractFunction(name) {
+  const start = source.indexOf(`function ${name}(`);
+  if (start < 0) {
+    throw new Error(`missing function ${name}`);
+  }
+
+  const paramsStart = source.indexOf('(', start);
+  let paramsDepth = 0;
+  let paramsEnd = paramsStart;
+  for (; paramsEnd < source.length; paramsEnd += 1) {
+    const ch = source[paramsEnd];
+    if (ch === '(') paramsDepth += 1;
+    if (ch === ')') {
+      paramsDepth -= 1;
+      if (paramsDepth === 0) {
+        break;
+      }
+    }
+  }
+
+  const braceStart = source.indexOf('{', paramsEnd);
+  let depth = 0;
+  let end = braceStart;
+  for (; end < source.length; end++) {
+    const ch = source[end];
+    if (ch === '{') depth += 1;
+    if (ch === '}') {
+      depth -= 1;
+      if (depth === 0) {
+        end += 1;
+        break;
+      }
+    }
+  }
+
+  return source.slice(start, end);
+}
+
+const bundle = [
+  'const AUTO_STEP_RANDOM_DELAY_MIN_ALLOWED_SECONDS = 0;',
+  'const AUTO_STEP_RANDOM_DELAY_MAX_ALLOWED_SECONDS = 600;',
+  'const AUTO_STEP_RANDOM_DELAY_DEFAULT_MIN_SECONDS = 12;',
+  'const AUTO_STEP_RANDOM_DELAY_DEFAULT_MAX_SECONDS = 18;',
+  'const PERSISTED_SETTING_DEFAULTS = { autoStepRandomDelayMinSeconds: 12, autoStepRandomDelayMaxSeconds: 18 };',
+  extractFunction('normalizeAutoStepRandomDelaySeconds'),
+  extractFunction('normalizeAutoStepRandomDelayRange'),
+  extractFunction('getAutoStepRandomDelayMs'),
+].join('\n');
+
+const api = new Function(`${bundle}; return { normalizeAutoStepRandomDelaySeconds, normalizeAutoStepRandomDelayRange, getAutoStepRandomDelayMs };`)();
+
+const defaultRange = api.normalizeAutoStepRandomDelayRange({});
+assert.deepStrictEqual(defaultRange, { minSeconds: 12, maxSeconds: 18 }, 'missing settings should fall back to the default 12-18 second range');
+
+for (let i = 0; i < 200; i += 1) {
+  const delay = api.getAutoStepRandomDelayMs(defaultRange.minSeconds * 1000, defaultRange.maxSeconds * 1000);
+  assert.ok(delay >= 12000, `delay ${delay} should respect the lower bound`);
+  assert.ok(delay <= 18000, `delay ${delay} should respect the upper bound`);
+  assert.ok(Number.isInteger(delay), `delay ${delay} should be an integer`);
+}
+
+const samples = new Set(Array.from({ length: 50 }, () => api.getAutoStepRandomDelayMs(defaultRange.minSeconds * 1000, defaultRange.maxSeconds * 1000)));
+assert.ok(samples.size > 1, 'delay helper should produce randomized values');
+
+const customRange = api.normalizeAutoStepRandomDelayRange({
+  autoStepRandomDelayMinSeconds: 5,
+  autoStepRandomDelayMaxSeconds: 7,
+});
+assert.deepStrictEqual(customRange, { minSeconds: 5, maxSeconds: 7 }, 'configured ranges should be preserved when already valid');
+
+const collapsedRange = api.normalizeAutoStepRandomDelayRange({
+  autoStepRandomDelayMinSeconds: 20,
+  autoStepRandomDelayMaxSeconds: 10,
+});
+assert.deepStrictEqual(collapsedRange, { minSeconds: 20, maxSeconds: 20 }, 'max delay should collapse up to min delay when user input is inverted');
+
+assert.strictEqual(
+  api.getAutoStepRandomDelayMs(15000, 15000),
+  15000,
+  'equal bounds should produce a deterministic delay'
+);
+
+const originalRandom = Math.random;
+try {
+  Math.random = () => 0.6;
+  assert.strictEqual(
+    api.normalizeAutoStepRandomDelaySeconds(-50, 12),
+    0,
+    'negative configured delay should clamp to zero seconds'
+  );
+  assert.strictEqual(
+    api.getAutoStepRandomDelayMs(-50, 10),
+    6,
+    'lower bound should be clamped to zero and still allow random output'
+  );
+} finally {
+  Math.random = originalRandom;
+}
+
+console.log('auto step random delay tests passed');