diff --git a/README.md b/README.md index 5c606bf..8662040 100644 --- a/README.md +++ b/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` 秒,可在侧边栏 `步间随机` 中改成你自己的范围 ## 详细步骤说明 diff --git a/background.js b/background.js index e234edf..101a65c 100644 --- a/background.js +++ b/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, diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 890936e..c1f0e84 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -192,6 +192,19 @@ +
+ 步间随机 +
+
+ + ~ + + +
+
+
OAuth 等待中... diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 65aee43..cd71d9b 100644 --- a/sidepanel/sidepanel.js +++ b/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; } diff --git a/tests/auto-step-random-delay.test.js b/tests/auto-step-random-delay.test.js new file mode 100644 index 0000000..4a07b65 --- /dev/null +++ b/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');