fix(tabs): lock automation to selected window
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
closeLocalhostCallbackTabs,
|
||||
createAutomationTab = null,
|
||||
getState,
|
||||
setState,
|
||||
} = deps;
|
||||
@@ -437,7 +438,9 @@
|
||||
}
|
||||
|
||||
if (!tab) {
|
||||
tab = await chrome.tabs.create({ url: normalizedUrl, active: true });
|
||||
tab = typeof createAutomationTab === 'function'
|
||||
? await createAutomationTab({ url: normalizedUrl, active: true })
|
||||
: await chrome.tabs.create({ url: normalizedUrl, active: true });
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
|
||||
@@ -30,6 +30,60 @@ let ipProxyLastRuntimeError = {
|
||||
fatal: false,
|
||||
at: 0,
|
||||
};
|
||||
|
||||
function normalizeAutomationWindowId(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) && numeric >= 0 ? numeric : null;
|
||||
}
|
||||
|
||||
async function createAutomationScopedTab(createProperties = {}, options = {}) {
|
||||
const windowId = normalizeAutomationWindowId(
|
||||
options?.automationWindowId
|
||||
?? options?.windowId
|
||||
?? options?.state?.automationWindowId
|
||||
?? null
|
||||
);
|
||||
if (windowId === null) {
|
||||
return chrome.tabs.create(createProperties || {});
|
||||
}
|
||||
|
||||
try {
|
||||
return await chrome.tabs.create({
|
||||
...(createProperties || {}),
|
||||
windowId,
|
||||
});
|
||||
} catch {
|
||||
return chrome.tabs.create(createProperties || {});
|
||||
}
|
||||
}
|
||||
|
||||
async function queryAutomationScopedTabs(queryInfo = {}, options = {}) {
|
||||
const windowId = normalizeAutomationWindowId(
|
||||
options?.automationWindowId
|
||||
?? options?.windowId
|
||||
?? options?.state?.automationWindowId
|
||||
?? null
|
||||
);
|
||||
if (windowId === null) {
|
||||
return chrome.tabs.query(queryInfo || {});
|
||||
}
|
||||
|
||||
const scopedQuery = {
|
||||
...(queryInfo || {}),
|
||||
windowId,
|
||||
};
|
||||
delete scopedQuery.currentWindow;
|
||||
try {
|
||||
return await chrome.tabs.query(scopedQuery);
|
||||
} catch {
|
||||
return chrome.tabs.query(queryInfo || {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const IP_PROXY_EXIT_PROBE_ENDPOINTS = [
|
||||
'http://ip-api.com/json?lang=en',
|
||||
'http://ipinfo.io/json',
|
||||
@@ -1974,8 +2028,8 @@ function extractProbeHostFromTabUrl(rawUrl = '') {
|
||||
}
|
||||
}
|
||||
|
||||
async function pickExistingPageContextProbeTabId() {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
async function pickExistingPageContextProbeTabId(options = {}) {
|
||||
const tabs = await queryAutomationScopedTabs({}, options);
|
||||
const candidates = tabs
|
||||
.filter((tab) => Number.isInteger(tab?.id))
|
||||
.filter((tab) => isPageContextProbeHost(extractProbeHostFromTabUrl(tab?.url || '')));
|
||||
@@ -2345,10 +2399,10 @@ async function detectIpProxyTargetReachabilityByPageContext(options = {}) {
|
||||
const targetUrl = appendProbeCacheBuster(endpoint, '_multipage_proxy_target');
|
||||
try {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
const tab = await chrome.tabs.create({
|
||||
const tab = await createAutomationScopedTab({
|
||||
url: targetUrl,
|
||||
active: false,
|
||||
});
|
||||
}, options);
|
||||
tabId = Number(tab?.id) || null;
|
||||
createdTabId = tabId;
|
||||
navigationErrorTracker.setTabId(tabId);
|
||||
@@ -2424,15 +2478,15 @@ async function detectProxyExitInfoByPageContext(options = {}) {
|
||||
let createdProbeTabId = null;
|
||||
const probeUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}`;
|
||||
try {
|
||||
const tab = await chrome.tabs.create({
|
||||
const tab = await createAutomationScopedTab({
|
||||
url: probeUrl,
|
||||
active: false,
|
||||
});
|
||||
}, options);
|
||||
probeTabId = Number(tab?.id) || null;
|
||||
createdProbeTabId = probeTabId;
|
||||
} catch (error) {
|
||||
errors.push(`probe:page_context:create_probe_tab_failed:${error?.message || error}`);
|
||||
probeTabId = await pickExistingPageContextProbeTabId();
|
||||
probeTabId = await pickExistingPageContextProbeTabId(options);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(probeTabId)) {
|
||||
@@ -2544,10 +2598,10 @@ async function detectProxyExitInfoByPageContext(options = {}) {
|
||||
errors.push(`probe:page_context:script_failed:${scriptError?.message || scriptError}`);
|
||||
if (!createdProbeTabId && isProbeErrorPageExecutionError(scriptError)) {
|
||||
const retryUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}&retry=1`;
|
||||
const retryTab = await chrome.tabs.create({
|
||||
const retryTab = await createAutomationScopedTab({
|
||||
url: retryUrl,
|
||||
active: false,
|
||||
});
|
||||
}, options);
|
||||
const retryTabId = Number(retryTab?.id) || 0;
|
||||
if (retryTabId > 0) {
|
||||
createdProbeTabId = retryTabId;
|
||||
@@ -2635,6 +2689,9 @@ async function detectProxyExitInfo(options = {}) {
|
||||
timeoutMs,
|
||||
errors,
|
||||
probeEndpoints,
|
||||
automationWindowId: options?.automationWindowId,
|
||||
windowId: options?.windowId,
|
||||
state: options?.state,
|
||||
});
|
||||
if (pageResult?.ip) {
|
||||
return pageResult;
|
||||
@@ -2669,6 +2726,9 @@ async function detectProxyExitInfo(options = {}) {
|
||||
timeoutMs: Math.max(5000, Math.min(9000, timeoutMs)),
|
||||
errors,
|
||||
probeEndpoints,
|
||||
automationWindowId: options?.automationWindowId,
|
||||
windowId: options?.windowId,
|
||||
state: options?.state,
|
||||
});
|
||||
errors.push(`probe:bg:abort_storm_page_fallback_result:${String(pageResult?.source || 'unknown')}`);
|
||||
if (pageResult?.ip) {
|
||||
@@ -3308,6 +3368,7 @@ async function applyIpProxySettingsFromState(state = {}, options = {}) {
|
||||
// 后台探测仅作为补充兜底,避免单一路径误判。
|
||||
preferPageContext: true,
|
||||
allowBackgroundFallback: true,
|
||||
state,
|
||||
}).catch(() => ({ ip: '', region: '' }));
|
||||
if (!exit?.ip && Boolean(status?.hasAuth)) {
|
||||
appendIpProxyAuthDiagnosticsToErrors(diagnostics);
|
||||
@@ -3339,6 +3400,7 @@ async function applyIpProxySettingsFromState(state = {}, options = {}) {
|
||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||
errors: targetDiagnostics,
|
||||
state,
|
||||
}).catch((error) => ({
|
||||
reachable: false,
|
||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||
@@ -3675,6 +3737,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
username: String(state?.ipProxyUsername || '').trim(),
|
||||
preferPageContext: true,
|
||||
allowBackgroundFallback: true,
|
||||
state,
|
||||
}).catch(() => ({ ip: '', region: '', endpoint: '', source: '' }));
|
||||
const status = {
|
||||
enabled: false,
|
||||
@@ -3774,6 +3837,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
// 手动探测与自动应用保持一致:先页面上下文,再后台补充。
|
||||
preferPageContext: true,
|
||||
allowBackgroundFallback: true,
|
||||
state,
|
||||
}).catch(() => ({ ip: '', region: '' }));
|
||||
if (!exit?.ip && Boolean(statusSeed?.hasAuth)) {
|
||||
appendIpProxyAuthDiagnosticsToErrors(diagnostics);
|
||||
@@ -3800,6 +3864,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||
errors: targetDiagnostics,
|
||||
state,
|
||||
}).catch((error) => ({
|
||||
reachable: false,
|
||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||
|
||||
@@ -210,6 +210,34 @@
|
||||
: '';
|
||||
}
|
||||
|
||||
function normalizeAutomationWindowId(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) && numeric >= 0 ? numeric : null;
|
||||
}
|
||||
|
||||
function resolveAutomationWindowIdFromMessage(message = {}, sender = {}) {
|
||||
return normalizeAutomationWindowId(
|
||||
message?.payload?.automationWindowId
|
||||
?? message?.payload?.windowId
|
||||
?? message?.automationWindowId
|
||||
?? message?.windowId
|
||||
?? sender?.tab?.windowId
|
||||
?? null
|
||||
);
|
||||
}
|
||||
|
||||
async function lockAutomationWindowFromMessage(message = {}, sender = {}) {
|
||||
const windowId = resolveAutomationWindowIdFromMessage(message, sender);
|
||||
if (windowId === null) {
|
||||
return null;
|
||||
}
|
||||
await setState({ automationWindowId: windowId });
|
||||
return windowId;
|
||||
}
|
||||
|
||||
async function syncStepAccountIdentityFromPayload(payload = {}) {
|
||||
const identifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = resolveSignupPhonePayload(payload);
|
||||
@@ -821,6 +849,7 @@
|
||||
case 'EXECUTE_STEP': {
|
||||
clearStopRequest();
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
await ensureManualInteractionAllowed('手动执行步骤');
|
||||
}
|
||||
const step = message.payload.step;
|
||||
@@ -848,6 +877,9 @@
|
||||
|
||||
case 'AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
}
|
||||
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
||||
await setContributionMode(true);
|
||||
if (typeof setState === 'function') {
|
||||
@@ -873,6 +905,9 @@
|
||||
|
||||
case 'SCHEDULE_AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
}
|
||||
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
||||
await setContributionMode(true);
|
||||
if (typeof setState === 'function') {
|
||||
@@ -894,6 +929,9 @@
|
||||
|
||||
case 'START_SCHEDULED_AUTO_RUN_NOW': {
|
||||
clearStopRequest();
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
}
|
||||
const started = await launchAutoRunTimerPlan('manual', {
|
||||
expectedKinds: [AUTO_RUN_TIMER_KIND_SCHEDULED_START],
|
||||
});
|
||||
@@ -913,6 +951,9 @@
|
||||
|
||||
case 'SKIP_AUTO_RUN_COUNTDOWN': {
|
||||
clearStopRequest();
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
}
|
||||
const skipped = await skipAutoRunCountdown();
|
||||
if (!skipped) {
|
||||
throw new Error('当前没有可立即开始的倒计时。');
|
||||
@@ -922,6 +963,9 @@
|
||||
|
||||
case 'RESUME_AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
}
|
||||
if (message.payload.email) {
|
||||
await setEmailState(message.payload.email);
|
||||
}
|
||||
@@ -1117,6 +1161,9 @@
|
||||
}
|
||||
|
||||
case 'PROBE_IP_PROXY_EXIT': {
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
}
|
||||
if (typeof probeIpProxyExit !== 'function') {
|
||||
throw new Error('IP 代理出口检测能力尚未接入。');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
chrome,
|
||||
addLog,
|
||||
closeConflictingTabsForSource,
|
||||
createAutomationTab = null,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
normalizeCodex2ApiUrl,
|
||||
@@ -266,7 +267,9 @@
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
||||
|
||||
const tab = await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
||||
const tab = typeof createAutomationTab === 'function'
|
||||
? await createAutomationTab({ url: sub2apiUrl, active: true })
|
||||
: await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
||||
const tabId = tab.id;
|
||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||
|
||||
|
||||
+121
-8
@@ -19,6 +19,88 @@
|
||||
|
||||
const pendingCommands = new Map();
|
||||
|
||||
function normalizeAutomationWindowId(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const numeric = Math.floor(Number(value));
|
||||
return Number.isInteger(numeric) && numeric >= 0 ? numeric : null;
|
||||
}
|
||||
|
||||
async function getAutomationWindowId(options = {}) {
|
||||
const directWindowId = normalizeAutomationWindowId(
|
||||
options.automationWindowId ?? options.windowId ?? null
|
||||
);
|
||||
if (directWindowId !== null) {
|
||||
return directWindowId;
|
||||
}
|
||||
|
||||
const state = await getState();
|
||||
return normalizeAutomationWindowId(state?.automationWindowId);
|
||||
}
|
||||
|
||||
async function withAutomationWindowScope(queryInfo = {}, options = {}) {
|
||||
const windowId = await getAutomationWindowId(options);
|
||||
if (windowId === null) {
|
||||
return { ...(queryInfo || {}) };
|
||||
}
|
||||
const scoped = {
|
||||
...(queryInfo || {}),
|
||||
windowId,
|
||||
};
|
||||
delete scoped.currentWindow;
|
||||
return scoped;
|
||||
}
|
||||
|
||||
async function queryTabsInAutomationWindow(queryInfo = {}, options = {}) {
|
||||
const scopedQuery = await withAutomationWindowScope(queryInfo, options);
|
||||
try {
|
||||
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 error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createAutomationTab(createProperties = {}, options = {}) {
|
||||
const windowId = await getAutomationWindowId(options);
|
||||
const properties = {
|
||||
...(createProperties || {}),
|
||||
...(windowId !== null ? { windowId } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
return await chrome.tabs.create(properties);
|
||||
} catch (error) {
|
||||
if (windowId !== null) {
|
||||
await setState({ automationWindowId: null }).catch(() => {});
|
||||
return chrome.tabs.create(createProperties || {});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function isTabInAutomationWindow(tabOrId, options = {}) {
|
||||
const windowId = await getAutomationWindowId(options);
|
||||
if (windowId === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let tab = tabOrId;
|
||||
if (Number.isInteger(tabOrId)) {
|
||||
if (typeof chrome?.tabs?.get !== 'function') {
|
||||
return true;
|
||||
}
|
||||
tab = await chrome.tabs.get(tabOrId).catch(() => null);
|
||||
}
|
||||
|
||||
const tabWindowId = normalizeAutomationWindowId(tab?.windowId);
|
||||
return tabWindowId === null || tabWindowId === windowId;
|
||||
}
|
||||
|
||||
async function sleepOrStop(ms) {
|
||||
if (typeof sleepWithStop === 'function') {
|
||||
await sleepWithStop(ms);
|
||||
@@ -86,7 +168,20 @@
|
||||
|
||||
async function registerTab(source, tabId) {
|
||||
const registry = await getTabRegistry();
|
||||
registry[source] = { tabId, ready: true };
|
||||
let windowId = null;
|
||||
if (chrome?.tabs?.get && Number.isInteger(tabId)) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
if (tab && !(await isTabInAutomationWindow(tab))) {
|
||||
console.log(LOG_PREFIX, `Ignored tab registration outside automation window: ${source} -> ${tabId}`);
|
||||
return;
|
||||
}
|
||||
windowId = normalizeAutomationWindowId(tab?.windowId);
|
||||
}
|
||||
registry[source] = {
|
||||
tabId,
|
||||
ready: true,
|
||||
...(windowId !== null ? { windowId } : {}),
|
||||
};
|
||||
await setState({ tabRegistry: registry });
|
||||
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
|
||||
}
|
||||
@@ -96,7 +191,12 @@
|
||||
const entry = registry[source];
|
||||
if (!entry) return false;
|
||||
try {
|
||||
await chrome.tabs.get(entry.tabId);
|
||||
const tab = await chrome.tabs.get(entry.tabId);
|
||||
if (!(await isTabInAutomationWindow(tab))) {
|
||||
registry[source] = null;
|
||||
await setState({ tabRegistry: registry });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
registry[source] = null;
|
||||
@@ -107,7 +207,16 @@
|
||||
|
||||
async function getTabId(source) {
|
||||
const registry = await getTabRegistry();
|
||||
return registry[source]?.tabId || null;
|
||||
const tabId = registry[source]?.tabId || null;
|
||||
if (!Number.isInteger(tabId)) {
|
||||
return null;
|
||||
}
|
||||
if (!(await isTabInAutomationWindow(tabId))) {
|
||||
registry[source] = null;
|
||||
await setState({ tabRegistry: registry });
|
||||
return null;
|
||||
}
|
||||
return tabId;
|
||||
}
|
||||
|
||||
async function rememberSourceLastUrl(source, url) {
|
||||
@@ -127,7 +236,7 @@
|
||||
|
||||
if (!referenceUrls.length) return;
|
||||
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = await queryTabsInAutomationWindow({});
|
||||
const matchedIds = tabs
|
||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||
.filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
|
||||
@@ -164,7 +273,7 @@
|
||||
|
||||
const { excludeTabIds = [] } = options;
|
||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = await queryTabsInAutomationWindow({});
|
||||
const matchedIds = tabs
|
||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||
.filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
|
||||
@@ -198,7 +307,7 @@
|
||||
const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
|
||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||
const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = await queryTabsInAutomationWindow({});
|
||||
const matchedIds = tabs
|
||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||
.filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
|
||||
@@ -534,7 +643,7 @@
|
||||
async function reuseOrCreateTab(source, url, options = {}) {
|
||||
if (options.forceNew) {
|
||||
await closeConflictingTabsForSource(source, url);
|
||||
const tab = await chrome.tabs.create({ url, active: true });
|
||||
const tab = await createAutomationTab({ url, active: true }, options);
|
||||
|
||||
if (options.inject) {
|
||||
await waitForTabUpdateComplete(tab.id);
|
||||
@@ -626,7 +735,7 @@
|
||||
}
|
||||
|
||||
await closeConflictingTabsForSource(source, url);
|
||||
const tab = await chrome.tabs.create({ url, active: true });
|
||||
const tab = await createAutomationTab({ url, active: true }, options);
|
||||
|
||||
if (options.inject) {
|
||||
await waitForTabUpdateComplete(tab.id);
|
||||
@@ -787,16 +896,20 @@
|
||||
closeConflictingTabsForSource,
|
||||
closeLocalhostCallbackTabs,
|
||||
closeTabsByUrlPrefix,
|
||||
createAutomationTab,
|
||||
ensureContentScriptReadyOnTab,
|
||||
flushCommand,
|
||||
getAutomationWindowId,
|
||||
getContentScriptResponseTimeoutMs,
|
||||
getMessageDebugLabel,
|
||||
getTabId,
|
||||
getTabRegistry,
|
||||
isLocalhostOAuthCallbackTabMatch,
|
||||
isTabAlive,
|
||||
isTabInAutomationWindow,
|
||||
pingContentScriptOnTab,
|
||||
queueCommand,
|
||||
queryTabsInAutomationWindow,
|
||||
registerTab,
|
||||
rememberSourceLastUrl,
|
||||
resolveResponseTimeoutMs,
|
||||
|
||||
Reference in New Issue
Block a user