diff --git a/README.md b/README.md index 0e8b31d..f90a833 100644 --- a/README.md +++ b/README.md @@ -517,7 +517,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 ### 操作间延迟 -`操作间延迟` 默认开启。开启后,自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 +`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。 diff --git a/background.js b/background.js index 277d4b8..424e752 100644 --- a/background.js +++ b/background.js @@ -3033,7 +3033,7 @@ function normalizePersistentSettingValue(key, value) { case 'autoRunDelayEnabled': return Boolean(value); case 'operationDelayEnabled': - return typeof value === 'boolean' ? value : true; + return true; case 'step6CookieCleanupEnabled': return Boolean(value); case 'stepExecutionRangeByFlow': diff --git a/content/operation-delay.js b/content/operation-delay.js index ce21536..3f8e9af 100644 --- a/content/operation-delay.js +++ b/content/operation-delay.js @@ -7,7 +7,7 @@ let operationDelaySettingRevision = 0; function normalizeOperationDelayEnabled(value) { - return typeof value === 'boolean' ? value : true; + return true; } function getOperationDelayEnabled() { diff --git a/docs/使用教程/使用教程.md b/docs/使用教程/使用教程.md index e20465c..dcd4e07 100644 --- a/docs/使用教程/使用教程.md +++ b/docs/使用教程/使用教程.md @@ -75,6 +75,6 @@ ## 7. 操作间延迟 -`操作间延迟` 默认开启。开启后,自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 +`操作间延迟` 固定开启。自动流程和手动单步在每个页面输入、选择、点击、提交、继续或授权操作完成后固定等待 2 秒;第一项页面操作不会提前等待。分格 OTP/验证码会先整组填完,然后只等待一次。 该开关不同于步间间隔,不影响邮箱轮询、短信/WhatsApp 轮询、后台 API、网络重试、后台定时器或存储持久化,也不影响 `confirm-oauth` 和 `platform-verify` 的交互节奏。 diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 64e4909..21798fd 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -596,26 +596,6 @@ -
- 执行范围 -
-
- -
-
- 步骤 - - - -
-
-
第六步
@@ -644,21 +624,6 @@
-
- 操作间延迟 -
-
- - 页面输入、选择、点击、提交、继续、授权后等待 2 秒 -
-
-
自动重试
@@ -704,6 +669,26 @@
+
+ 执行范围 +
+
+ +
+
+ 步骤 + + + +
+
+
OAuth 等待中... diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 8dc586e..e8d764b 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -399,7 +399,6 @@ const inputStep6CookieCleanupEnabled = document.getElementById('input-step6-cook const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled'); const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes'); const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds'); -const inputOperationDelayEnabled = document.getElementById('input-operation-delay-enabled'); const inputOAuthFlowTimeoutEnabled = document.getElementById('input-oauth-flow-timeout-enabled'); const rowStepExecutionRange = document.getElementById('row-step-execution-range'); const inputStepExecutionRangeEnabled = document.getElementById('input-step-execution-range-enabled'); @@ -554,7 +553,6 @@ let currentSignupMethod = DEFAULT_SIGNUP_METHOD; let currentPhoneSignupReloginAfterBindEmailEnabled = DEFAULT_PHONE_SIGNUP_RELOGIN_AFTER_BIND_EMAIL_ENABLED; let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID; let phoneSignupReuseUiWasLocked = false; -let lastConfirmedOperationDelayEnabled = true; let heroSmsCountrySelectionOrder = []; let phoneSmsProviderOrderSelection = []; let heroSmsCountryMenuSearchKeyword = ''; @@ -2634,7 +2632,7 @@ function scheduleAccountRunHistoryRefresh(delayMs = 150) { } function normalizeOperationDelayEnabled(value) { - return typeof value === 'boolean' ? value : true; + return true; } function appendOperationDelayLog(enabled, level = 'info', message = '') { @@ -2642,15 +2640,13 @@ function appendOperationDelayLog(enabled, level = 'info', message = '') { timestamp: Date.now(), level, message: message || (enabled - ? '操作间延迟已开启:页面输入、选择、点击、提交、继续、授权后等待 2 秒。' - : '操作间延迟已关闭:页面操作将连续执行。'), + ? '操作间延迟已固定开启。' + : '操作间延迟保持固定开启。'), }); } function applyOperationDelayState(state = latestState, options = {}) { const enabled = options.restoreFailed ? true : normalizeOperationDelayEnabled(state?.operationDelayEnabled); - lastConfirmedOperationDelayEnabled = enabled; - if (inputOperationDelayEnabled) inputOperationDelayEnabled.checked = enabled; if (typeof syncLatestState === 'function') { syncLatestState({ operationDelayEnabled: enabled }); } @@ -2660,6 +2656,8 @@ function applyOperationDelayState(state = latestState, options = {}) { } async function persistOperationDelayToggle() { + applyOperationDelayState({ operationDelayEnabled: true }); + return true; const nextEnabled = normalizeOperationDelayEnabled(inputOperationDelayEnabled?.checked); try { const response = await chrome.runtime.sendMessage({ @@ -13741,10 +13739,6 @@ inputPlusModeEnabled?.addEventListener('change', () => { saveSettings({ silent: true }).catch(() => { }); }); -inputOperationDelayEnabled?.addEventListener('change', () => { - persistOperationDelayToggle().catch(() => { }); -}); - selectPlusPaymentMethod?.addEventListener('change', () => { selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value); updatePlusModeUI(); diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js index f1190ba..97d0b36 100644 --- a/tests/background-message-router-module.test.js +++ b/tests/background-message-router-module.test.js @@ -276,7 +276,7 @@ test('SAVE_SETTING broadcasts operation delay setting without background success addLog: async (message, level = 'info') => logs.push({ message, level }), buildLuckmailSessionSettingsPayload: () => ({}), buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'operationDelayEnabled') - ? { operationDelayEnabled: input.operationDelayEnabled === false ? false : true } + ? { operationDelayEnabled: true } : {}, broadcastDataUpdate: (payload) => broadcasts.push(payload), getState: async () => ({ ...state }), @@ -291,8 +291,8 @@ test('SAVE_SETTING broadcasts operation delay setting without background success }); assert.equal(response.ok, true); - assert.equal(state.operationDelayEnabled, false); - assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: false }); + assert.equal(state.operationDelayEnabled, true); + assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: true }); assert.equal(logs.length, 0); }); diff --git a/tests/background-operation-delay-settings.test.js b/tests/background-operation-delay-settings.test.js index 9f3d955..93be86b 100644 --- a/tests/background-operation-delay-settings.test.js +++ b/tests/background-operation-delay-settings.test.js @@ -46,7 +46,7 @@ test('operationDelayEnabled defaults to enabled and strictly normalizes values', assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', value), true); } assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', true), true); - assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', false), false); + assert.equal(api.normalizePersistentSettingValue('operationDelayEnabled', false), true); }); test('operationDelayEnabled is normalized through the background settings payload path', () => { @@ -63,5 +63,5 @@ test('operationDelayEnabled is normalized through the background settings payloa for (const value of [undefined, null, '', 0, 'false', true]) { assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: value }, { fillDefaults: true }).operationDelayEnabled, true); } - assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: false }).operationDelayEnabled, false); + assert.equal(api.buildPersistentSettingsPayload({ operationDelayEnabled: false }).operationDelayEnabled, true); }); diff --git a/tests/content-operation-delay.test.js b/tests/content-operation-delay.test.js index 85c635d..ac4306d 100644 --- a/tests/content-operation-delay.test.js +++ b/tests/content-operation-delay.test.js @@ -34,9 +34,9 @@ test('operation delay runs action first and waits once after completion', async assert.deepStrictEqual(events, ['operation:start', 'operation:end', 'sleep:2000']); }); -test('operation delay skips disabled mode and excluded step keys', async () => { +test('operation delay ignores disabled flags but still skips excluded step keys', async () => { const api = loadOperationDelayApi(); - assert.equal(api.shouldDelayOperation({ enabled: false, stepKey: 'fill-profile', kind: 'click' }), false); + assert.equal(api.shouldDelayOperation({ enabled: false, stepKey: 'fill-profile', kind: 'click' }), true); assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'confirm-oauth', kind: 'click' }), false); assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'platform-verify', kind: 'submit' }), false); assert.equal(api.shouldDelayOperation({ enabled: true, stepKey: 'fill-profile', kind: 'fill' }), true); @@ -49,7 +49,7 @@ test('operation delay defaults unresolved settings to enabled', async () => { assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); -test('persisted disabled setting prevents first operation delay even before initial restore resolves', async () => { +test('persisted disabled setting is normalized back to enabled before first operation delay resolves', async () => { const events = []; let resolveStorage; const storageReady = new Promise((resolve) => { resolveStorage = resolve; }); @@ -69,10 +69,10 @@ test('persisted disabled setting prevents first operation delay even before init resolveStorage({ operationDelayEnabled: false }); await pendingOperation; - assert.deepStrictEqual(events, ['operation']); + assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); -test('newer disabled storage change wins over older pending restore', async () => { +test('disabled storage changes are normalized back to enabled even when they arrive before restore', async () => { const events = []; let changeListener = null; let resolveStorage; @@ -88,17 +88,17 @@ test('newer disabled storage change wins over older pending restore', async () = }); changeListener({ operationDelayEnabled: { newValue: false } }, 'local'); - assert.equal(api.getOperationDelayEnabled(), false); + assert.equal(api.getOperationDelayEnabled(), true); resolveStorage({ operationDelayEnabled: true }); await waitForTimer(); - assert.equal(api.getOperationDelayEnabled(), false); + assert.equal(api.getOperationDelayEnabled(), true); await api.performOperationWithDelay( { stepKey: 'fill-profile', kind: 'fill' }, async () => events.push('operation'), { sleep: async (ms) => events.push(`sleep:${ms}`) } ); - assert.deepStrictEqual(events, ['operation']); + assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); test('newer enabled storage change wins over older pending restore', async () => { @@ -130,7 +130,7 @@ test('newer enabled storage change wins over older pending restore', async () => assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); -test('first operation honors async persisted disabled setting before fallback budget expires', async () => { +test('first operation keeps the delay even when async persisted state says disabled', async () => { const events = []; const api = loadOperationDelayApi({ events, @@ -147,7 +147,7 @@ test('first operation honors async persisted disabled setting before fallback bu async () => events.push('operation'), { sleep: async (ms) => events.push(`sleep:${ms}`) } ); - assert.deepStrictEqual(events, ['operation']); + assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); test('hanging initial storage restore falls back to enabled delay and returns', async () => { @@ -167,7 +167,7 @@ test('hanging initial storage restore falls back to enabled delay and returns', assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); -test('explicit disabled metadata does not wait for hanging initial storage restore', async () => { +test('explicit disabled metadata is normalized back to enabled during hanging initial restore', async () => { const events = []; const api = loadOperationDelayApi({ events, @@ -181,7 +181,7 @@ test('explicit disabled metadata does not wait for hanging initial storage resto ); assert.equal(await resolveOrPending(operation), 'resolved'); - assert.deepStrictEqual(events, ['operation']); + assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); test('invalid persisted values still resolve to enabled', async () => { @@ -190,14 +190,14 @@ test('invalid persisted values still resolve to enabled', async () => { assert.equal(api.getOperationDelayEnabled(), true); }); -test('shouldDelayOperation uses cached disabled setting by default', async () => { +test('shouldDelayOperation normalizes cached disabled setting back to enabled by default', async () => { const api = loadOperationDelayApi({ chrome: { storage: { local: { get: async () => ({ operationDelayEnabled: false }) }, onChanged: { addListener() {} } } } }); await api.refreshOperationDelaySetting(); - assert.equal(api.getOperationDelayEnabled(), false); - assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), false); + assert.equal(api.getOperationDelayEnabled(), true); + assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), true); }); -test('storage change to disabled is honored by later delayed operations', async () => { +test('storage change to disabled is normalized back to enabled for later delayed operations', async () => { const events = []; let changeListener = null; const api = loadOperationDelayApi({ @@ -212,14 +212,14 @@ test('storage change to disabled is honored by later delayed operations', async await api.refreshOperationDelaySetting(); changeListener({ operationDelayEnabled: { newValue: false } }, 'local'); - assert.equal(api.getOperationDelayEnabled(), false); - assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), false); + assert.equal(api.getOperationDelayEnabled(), true); + assert.equal(api.shouldDelayOperation({ stepKey: 'fill-profile', kind: 'fill' }), true); await api.performOperationWithDelay( { stepKey: 'fill-profile', kind: 'fill' }, async () => events.push('operation'), { sleep: async (ms) => events.push(`sleep:${ms}`) } ); - assert.deepStrictEqual(events, ['operation']); + assert.deepStrictEqual(events, ['operation', 'sleep:2000']); }); test('storage change to enabled is honored by later delayed operations', async () => { diff --git a/tests/sidepanel-operation-delay.test.js b/tests/sidepanel-operation-delay.test.js index 42117ed..8ac3004 100644 --- a/tests/sidepanel-operation-delay.test.js +++ b/tests/sidepanel-operation-delay.test.js @@ -38,95 +38,43 @@ function extractFunction(name) { throw new Error(`unterminated ${name}`); } -test('sidepanel exposes one operation delay switch in the existing settings surface', () => { - assert.match(html, /id="row-operation-delay-settings"/); - assert.match(html, /id="input-operation-delay-enabled"/); - assert.match(html, /操作间延迟/); - assert.match(html, /2\s*秒/); - assert.match(html, /id="row-operation-delay-settings"[\s\S]{0,900}输入[\s\S]{0,900}选择[\s\S]{0,900}点击[\s\S]{0,900}提交[\s\S]{0,900}继续[\s\S]{0,900}授权/); +test('sidepanel no longer exposes operation delay switch and places step execution range below oauth timeout', () => { + assert.doesNotMatch(html, /id="row-operation-delay-settings"/); + assert.doesNotMatch(html, /id="input-operation-delay-enabled"/); + + const autoDelayIndex = html.indexOf('id="row-auto-delay-settings"'); + const oauthTimeoutIndex = html.indexOf('id="row-oauth-flow-timeout"'); + const stepRangeIndex = html.indexOf('id="row-step-execution-range"'); + const oauthDisplayIndex = html.indexOf('id="row-oauth-display"'); + + assert.notEqual(autoDelayIndex, -1); + assert.notEqual(oauthTimeoutIndex, -1); + assert.notEqual(stepRangeIndex, -1); + assert.notEqual(oauthDisplayIndex, -1); + assert.ok(stepRangeIndex > autoDelayIndex, 'step execution range should still remain below the step6 toggle row'); + assert.ok(stepRangeIndex > oauthTimeoutIndex, 'step execution range should render below oauth timeout'); + assert.ok(stepRangeIndex < oauthDisplayIndex, 'step execution range should stay above oauth runtime display'); }); -test('sidepanel owns operation delay toggle feedback exactly once', () => { - assert.match(source, /inputOperationDelayEnabled/); - assert.match(source, /operationDelayEnabled/); - assert.match(source, /appendLog\(\{[\s\S]*操作间延迟/); - assert.doesNotMatch(source, /operationDelayEnabled[\s\S]{0,240}addLog\(/); -}); - -test('sidepanel operation delay restore and save failure contracts are explicit', () => { - assert.match(source, /normalizeOperationDelayEnabled/); - assert.match(source, /lastConfirmedOperationDelayEnabled/); - assert.match(source, /操作间延迟设置读取失败/); - assert.match(source, /操作间延迟设置保存失败/); - assert.match(source, /inputOperationDelayEnabled\.checked\s*=\s*lastConfirmedOperationDelayEnabled/); - assert.match(source, /typeof\s+applyOperationDelayState\s*===\s*['"]function['"]/); -}); - -test('sidepanel syncs operation delay DATA_UPDATED without becoming the success-log owner', () => { - assert.match(source, /message\.payload\.operationDelayEnabled\s*!==\s*undefined[\s\S]{0,240}applyOperationDelayState\(message\.payload\)/); -}); - -function loadOperationDelaySidepanelHarness() { - const runtimeMessages = []; - const logs = []; - const inputOperationDelayEnabled = { checked: true, disabled: false }; - - const chrome = { - runtime: { - sendMessage: async (message) => { - runtimeMessages.push(message); - return { ok: true, state: { operationDelayEnabled: message.payload.operationDelayEnabled } }; - }, - }, - }; - - const harness = new Function('chrome', 'appendLog', 'inputOperationDelayEnabled', 'initialLatestState', ` - let latestState = initialLatestState; - let lastConfirmedOperationDelayEnabled = true; +test('sidepanel operation delay state is always normalized back to enabled', () => { + const harness = new Function(` + let latestState = { operationDelayEnabled: false }; function syncLatestState(nextState) { latestState = { ...(latestState || {}), ...(nextState || {}) }; } ${extractFunction('normalizeOperationDelayEnabled')} - ${extractFunction('appendOperationDelayLog')} ${extractFunction('applyOperationDelayState')} - ${extractFunction('persistOperationDelayToggle')} return { + normalizeOperationDelayEnabled, applyOperationDelayState, - persistOperationDelayToggle, getLatestState: () => latestState, - getLastConfirmedOperationDelayEnabled: () => lastConfirmedOperationDelayEnabled, - setLastConfirmedOperationDelayEnabled: (value) => { lastConfirmedOperationDelayEnabled = value; }, }; - `)(chrome, (entry) => logs.push(entry), inputOperationDelayEnabled, { operationDelayEnabled: true }); + `)(); - return { chrome, harness, inputOperationDelayEnabled, logs, runtimeMessages }; -} + assert.equal(harness.normalizeOperationDelayEnabled(undefined), true); + assert.equal(harness.normalizeOperationDelayEnabled(false), true); + assert.equal(harness.normalizeOperationDelayEnabled(true), true); -test('operation delay switch logs once, defaults restore failures to enabled, and rolls back save failures', async () => { - const { chrome, harness: helpers, inputOperationDelayEnabled: input, logs, runtimeMessages: sentMessages } = loadOperationDelaySidepanelHarness(); - - helpers.applyOperationDelayState({ operationDelayEnabled: false }); - assert.equal(input.checked, false); - assert.equal(helpers.getLatestState().operationDelayEnabled, false); - - helpers.applyOperationDelayState({ operationDelayEnabled: undefined }, { restoreFailed: true }); - assert.equal(input.checked, true); - assert.equal(helpers.getLatestState().operationDelayEnabled, true); - assert.equal(logs.filter((entry) => entry.level === 'warn').length, 1); - - logs.length = 0; - input.checked = false; - await helpers.persistOperationDelayToggle(); - assert.equal(sentMessages.at(-1).payload.operationDelayEnabled, false); - assert.equal(logs.length, 1); - assert.match(logs[0].message, /关闭|off/i); - - sentMessages.length = 0; - chrome.runtime.sendMessage = async () => { throw new Error('network down'); }; - logs.length = 0; - input.checked = true; - helpers.setLastConfirmedOperationDelayEnabled(false); - await assert.rejects(() => helpers.persistOperationDelayToggle(), /network down/); - assert.equal(input.checked, false); - assert.equal(logs.filter((entry) => entry.level === 'error').length, 1); + harness.applyOperationDelayState({ operationDelayEnabled: false }); + assert.equal(harness.getLatestState().operationDelayEnabled, true); });