From 8b9adb90856b0befe8ef17a02c51c75c9ca74dbd Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 12 May 2026 15:35:43 +0800 Subject: [PATCH] fix(tabs): stop fallback outside locked window --- background/ip-proxy-core.js | 15 ++-- background/message-router.js | 2 +- background/tab-runtime.js | 13 ++-- sidepanel/sidepanel.js | 2 +- tests/background-ip-proxy-core.test.js | 43 +++++++++++ tests/background-tab-runtime-module.test.js | 79 +++++++++++++++++++++ 6 files changed, 142 insertions(+), 12 deletions(-) diff --git a/background/ip-proxy-core.js b/background/ip-proxy-core.js index 3802e7c..2464329 100644 --- a/background/ip-proxy-core.js +++ b/background/ip-proxy-core.js @@ -35,10 +35,15 @@ function normalizeAutomationWindowId(value) { if (value === null || value === undefined || value === '') { return null; } - const numeric = Math.floor(Number(value)); + const numeric = Number(value); return Number.isInteger(numeric) && numeric >= 0 ? numeric : null; } +function buildAutomationWindowUnavailableError(error) { + const suffix = error?.message ? ` 原因:${error.message}` : ''; + return new Error(`自动任务窗口已不可用,请在目标 Chrome 窗口重新打开侧边栏并启动任务。${suffix}`); +} + async function createAutomationScopedTab(createProperties = {}, options = {}) { const windowId = normalizeAutomationWindowId( options?.automationWindowId @@ -55,8 +60,8 @@ async function createAutomationScopedTab(createProperties = {}, options = {}) { ...(createProperties || {}), windowId, }); - } catch { - return chrome.tabs.create(createProperties || {}); + } catch (error) { + throw buildAutomationWindowUnavailableError(error); } } @@ -78,8 +83,8 @@ async function queryAutomationScopedTabs(queryInfo = {}, options = {}) { delete scopedQuery.currentWindow; try { return await chrome.tabs.query(scopedQuery); - } catch { - return chrome.tabs.query(queryInfo || {}); + } catch (error) { + throw buildAutomationWindowUnavailableError(error); } } diff --git a/background/message-router.js b/background/message-router.js index 9a5449f..f3127a0 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -214,7 +214,7 @@ if (value === null || value === undefined || value === '') { return null; } - const numeric = Math.floor(Number(value)); + const numeric = Number(value); return Number.isInteger(numeric) && numeric >= 0 ? numeric : null; } diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 7429101..f08f1a3 100644 --- a/background/tab-runtime.js +++ b/background/tab-runtime.js @@ -23,10 +23,15 @@ if (value === null || value === undefined || value === '') { return null; } - const numeric = Math.floor(Number(value)); + const numeric = Number(value); return Number.isInteger(numeric) && numeric >= 0 ? numeric : null; } + function buildAutomationWindowUnavailableError(error) { + const suffix = error?.message ? ` 原因:${error.message}` : ''; + return new Error(`自动任务窗口已不可用,请在目标 Chrome 窗口重新打开侧边栏并启动任务。${suffix}`); + } + async function getAutomationWindowId(options = {}) { const directWindowId = normalizeAutomationWindowId( options.automationWindowId ?? options.windowId ?? null @@ -58,8 +63,7 @@ return await chrome.tabs.query(scopedQuery); } catch (error) { if (Object.prototype.hasOwnProperty.call(scopedQuery, 'windowId')) { - await setState({ automationWindowId: null }).catch(() => {}); - return chrome.tabs.query(queryInfo || {}); + throw buildAutomationWindowUnavailableError(error); } throw error; } @@ -76,8 +80,7 @@ return await chrome.tabs.create(properties); } catch (error) { if (windowId !== null) { - await setState({ automationWindowId: null }).catch(() => {}); - return chrome.tabs.create(createProperties || {}); + throw buildAutomationWindowUnavailableError(error); } throw error; } diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 1d559c1..7e7c5b3 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -1155,7 +1155,7 @@ function normalizeAutomationWindowId(value) { if (value === null || value === undefined || value === '') { return null; } - const numeric = Math.floor(Number(value)); + const numeric = Number(value); return Number.isInteger(numeric) && numeric >= 0 ? numeric : null; } diff --git a/tests/background-ip-proxy-core.test.js b/tests/background-ip-proxy-core.test.js index c0f4561..b3b8146 100644 --- a/tests/background-ip-proxy-core.test.js +++ b/tests/background-ip-proxy-core.test.js @@ -44,6 +44,8 @@ ${coreSource} return { applyExitRegionExpectation, buildIpProxyPacScript, + chrome, + createAutomationScopedTab, buildIpProxyRoutingStatePatch, applyTargetReachabilityExpectation, getAccountModeProxyPoolFromState, @@ -51,6 +53,7 @@ return { normalizeProxyPoolEntries, parseProxyExitProbePayload, parseIpProxyLine, + queryAutomationScopedTabs, resolveExitProbeEndpoints, resolveIpProxyAutoSwitchThreshold, resolveTargetReachabilityEndpoints, @@ -135,6 +138,46 @@ test('IP proxy routing state patch keeps exit probe endpoint for diagnostics', ( assert.equal(patch.ipProxyAppliedExitEndpoint, 'https://ipinfo.io/json'); }); +test('IP proxy page probes do not fall back to other windows when the locked window is unavailable', async () => { + const api = loadIpProxyCore(); + const created = []; + const queries = []; + api.chrome.tabs = { + create: async (payload) => { + created.push(payload); + if (payload.windowId === 77) { + throw new Error('No window with id: 77'); + } + return { id: 12, windowId: payload.windowId, url: payload.url }; + }, + query: async (queryInfo) => { + queries.push(queryInfo); + if (queryInfo.windowId === 77) { + throw new Error('No window with id: 77'); + } + return [{ id: 99, windowId: 1, url: 'https://ipinfo.io/json' }]; + }, + }; + + await assert.rejects( + () => api.createAutomationScopedTab( + { url: 'https://ipinfo.io/json', active: false }, + { state: { automationWindowId: 77 } } + ), + /自动任务窗口已不可用/ + ); + await assert.rejects( + () => api.queryAutomationScopedTabs( + { url: 'https://ipinfo.io/*' }, + { state: { automationWindowId: 77 } } + ), + /自动任务窗口已不可用/ + ); + + assert.deepEqual(created, [{ url: 'https://ipinfo.io/json', active: false, windowId: 77 }]); + assert.deepEqual(queries, [{ url: 'https://ipinfo.io/*', windowId: 77 }]); +}); + test('711 fixed-account mode applies region and sticky session parameters', () => { const api = loadIpProxyCore(); const pool = api.getAccountModeProxyPoolFromState({ diff --git a/tests/background-tab-runtime-module.test.js b/tests/background-tab-runtime-module.test.js index bea20c9..b1186bd 100644 --- a/tests/background-tab-runtime-module.test.js +++ b/tests/background-tab-runtime-module.test.js @@ -260,3 +260,82 @@ test('tab runtime scopes tab queries to the locked automation window', async () assert.deepEqual(queries[0], { active: true, windowId: 22 }); }); + +test('tab runtime does not create tabs outside an unavailable locked window', async () => { + const source = fs.readFileSync('background/tab-runtime.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope); + const created = []; + const runtime = api.createTabRuntime({ + LOG_PREFIX: '[test]', + addLog: async () => {}, + chrome: { + tabs: { + create: async (payload) => { + created.push(payload); + if (payload.windowId === 44) { + throw new Error('No window with id: 44'); + } + return { id: 99, windowId: payload.windowId, url: payload.url }; + }, + get: async () => ({ id: 1, windowId: 44, url: 'https://example.com' }), + query: async () => [], + }, + }, + getSourceLabel: (sourceName) => sourceName || 'unknown', + getState: async () => ({ + automationWindowId: 44, + tabRegistry: {}, + sourceLastUrls: {}, + }), + matchesSourceUrlFamily: () => false, + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => runtime.createAutomationTab({ url: 'https://example.com', active: true }), + /自动任务窗口已不可用/ + ); + + assert.deepEqual(created, [{ url: 'https://example.com', active: true, windowId: 44 }]); +}); + +test('tab runtime does not query all windows when the locked window is unavailable', async () => { + const source = fs.readFileSync('background/tab-runtime.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope); + const queries = []; + const runtime = api.createTabRuntime({ + LOG_PREFIX: '[test]', + addLog: async () => {}, + chrome: { + tabs: { + get: async () => ({ id: 1, windowId: 55, url: 'https://example.com' }), + query: async (queryInfo) => { + queries.push(queryInfo); + if (queryInfo.windowId === 55) { + throw new Error('No window with id: 55'); + } + return [{ id: 7, windowId: 1, url: 'https://other.example/' }]; + }, + }, + }, + getSourceLabel: (sourceName) => sourceName || 'unknown', + getState: async () => ({ + automationWindowId: 55, + tabRegistry: {}, + sourceLastUrls: {}, + }), + matchesSourceUrlFamily: () => false, + setState: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => runtime.queryTabsInAutomationWindow({ active: true }), + /自动任务窗口已不可用/ + ); + + assert.deepEqual(queries, [{ active: true, windowId: 55 }]); +});