fix(tabs): lock automation to selected window
This commit is contained in:
+38
-8
@@ -874,6 +874,7 @@ const DEFAULT_STATE = {
|
|||||||
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
||||||
codex2apiOAuthState: null, // Codex2API OAuth state。
|
codex2apiOAuthState: null, // Codex2API OAuth state。
|
||||||
plusCheckoutTabId: null, // Plus checkout / PayPal 标签页 ID。
|
plusCheckoutTabId: null, // Plus checkout / PayPal 标签页 ID。
|
||||||
|
automationWindowId: null, // 当前任务锁定的浏览器窗口 ID,避免新标签页跑到其它窗口。
|
||||||
plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。
|
plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。
|
||||||
plusCheckoutCountry: 'DE',
|
plusCheckoutCountry: 'DE',
|
||||||
plusCheckoutCurrency: 'EUR',
|
plusCheckoutCurrency: 'EUR',
|
||||||
@@ -3409,6 +3410,7 @@ async function resetState() {
|
|||||||
'luckmailPreserveTagId',
|
'luckmailPreserveTagId',
|
||||||
'luckmailPreserveTagName',
|
'luckmailPreserveTagName',
|
||||||
'preferredIcloudHost',
|
'preferredIcloudHost',
|
||||||
|
'automationWindowId',
|
||||||
...CONTRIBUTION_RUNTIME_KEYS,
|
...CONTRIBUTION_RUNTIME_KEYS,
|
||||||
]),
|
]),
|
||||||
getPersistedSettings(),
|
getPersistedSettings(),
|
||||||
@@ -3478,6 +3480,10 @@ async function resetState() {
|
|||||||
freeReusablePhoneActivation,
|
freeReusablePhoneActivation,
|
||||||
phoneReusableActivationPool,
|
phoneReusableActivationPool,
|
||||||
preferredIcloudHost: prev.preferredIcloudHost || '',
|
preferredIcloudHost: prev.preferredIcloudHost || '',
|
||||||
|
automationWindowId: Number.isInteger(Number(prev.automationWindowId))
|
||||||
|
&& Number(prev.automationWindowId) >= 0
|
||||||
|
? Number(prev.automationWindowId)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5542,7 +5548,7 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload
|
|||||||
|
|
||||||
async function getOpenIcloudHostPreference() {
|
async function getOpenIcloudHostPreference() {
|
||||||
try {
|
try {
|
||||||
const tabs = await chrome.tabs.query({
|
const tabs = await queryTabsInAutomationWindow({
|
||||||
url: ICLOUD_TAB_URL_PATTERNS,
|
url: ICLOUD_TAB_URL_PATTERNS,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5690,7 +5696,7 @@ function shouldEmitIcloudTransientLog(key, windowMs = 1500) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openIcloudLoginPage(preferredUrl) {
|
async function openIcloudLoginPage(preferredUrl) {
|
||||||
const tabs = await chrome.tabs.query({
|
const tabs = await queryTabsInAutomationWindow({
|
||||||
url: ICLOUD_TAB_URL_PATTERNS,
|
url: ICLOUD_TAB_URL_PATTERNS,
|
||||||
});
|
});
|
||||||
const preferredHost = new URL(preferredUrl).host;
|
const preferredHost = new URL(preferredUrl).host;
|
||||||
@@ -5714,7 +5720,7 @@ async function openIcloudLoginPage(preferredUrl) {
|
|||||||
return existingAnyIcloudTab.id;
|
return existingAnyIcloudTab.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await chrome.tabs.create({ url: preferredUrl, active: true });
|
const created = await createAutomationTab({ url: preferredUrl, active: true });
|
||||||
return created.id;
|
return created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5930,7 +5936,7 @@ async function ensureIcloudMailContextTab(tabs = [], targetHost = '', preferredH
|
|||||||
await chrome.tabs.update(mailTabs[0].id, { url: fallbackMailUrl, active: false });
|
await chrome.tabs.update(mailTabs[0].id, { url: fallbackMailUrl, active: false });
|
||||||
await waitForIcloudMailTabReady(mailTabs[0].id, 9000);
|
await waitForIcloudMailTabReady(mailTabs[0].id, 9000);
|
||||||
try {
|
try {
|
||||||
return await chrome.tabs.query({
|
return await queryTabsInAutomationWindow({
|
||||||
url: ICLOUD_TAB_URL_PATTERNS,
|
url: ICLOUD_TAB_URL_PATTERNS,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -5955,13 +5961,13 @@ async function ensureIcloudMailContextTab(tabs = [], targetHost = '', preferredH
|
|||||||
await chrome.tabs.update(anyIcloudTab.id, { url: fallbackMailUrl, active: false });
|
await chrome.tabs.update(anyIcloudTab.id, { url: fallbackMailUrl, active: false });
|
||||||
await waitForIcloudMailTabReady(anyIcloudTab.id, 9000);
|
await waitForIcloudMailTabReady(anyIcloudTab.id, 9000);
|
||||||
} else {
|
} else {
|
||||||
const created = await chrome.tabs.create({ url: fallbackMailUrl, active: false });
|
const created = await createAutomationTab({ url: fallbackMailUrl, active: false });
|
||||||
await waitForIcloudMailTabReady(created?.id, 9000);
|
await waitForIcloudMailTabReady(created?.id, 9000);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await chrome.tabs.query({
|
return await queryTabsInAutomationWindow({
|
||||||
url: ICLOUD_TAB_URL_PATTERNS,
|
url: ICLOUD_TAB_URL_PATTERNS,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -6004,7 +6010,7 @@ async function icloudRequestViaPageContext(method, url, options = {}) {
|
|||||||
const targetHost = configuredHost || normalizeIcloudHost(new URL(url).hostname);
|
const targetHost = configuredHost || normalizeIcloudHost(new URL(url).hostname);
|
||||||
const preferredHost = configuredHost || normalizeIcloudHost(state?.preferredIcloudHost);
|
const preferredHost = configuredHost || normalizeIcloudHost(state?.preferredIcloudHost);
|
||||||
|
|
||||||
let tabs = await chrome.tabs.query({
|
let tabs = await queryTabsInAutomationWindow({
|
||||||
url: ICLOUD_TAB_URL_PATTERNS,
|
url: ICLOUD_TAB_URL_PATTERNS,
|
||||||
});
|
});
|
||||||
tabs = await ensureIcloudMailContextTab(tabs, targetHost, preferredHost);
|
tabs = await ensureIcloudMailContextTab(tabs, targetHost, preferredHost);
|
||||||
@@ -6387,7 +6393,7 @@ async function resolveIcloudPremiumMailServiceViaPageContext(setupUrls, state, o
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
let tabs = [];
|
let tabs = [];
|
||||||
try {
|
try {
|
||||||
tabs = await chrome.tabs.query({
|
tabs = await queryTabsInAutomationWindow({
|
||||||
url: ICLOUD_TAB_URL_PATTERNS,
|
url: ICLOUD_TAB_URL_PATTERNS,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -7109,6 +7115,22 @@ async function getTabId(source) {
|
|||||||
return tabRuntime.getTabId(source);
|
return tabRuntime.getTabId(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAutomationWindowId(options = {}) {
|
||||||
|
return tabRuntime.getAutomationWindowId(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAutomationTab(createProperties = {}, options = {}) {
|
||||||
|
return tabRuntime.createAutomationTab(createProperties, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryTabsInAutomationWindow(queryInfo = {}, options = {}) {
|
||||||
|
return tabRuntime.queryTabsInAutomationWindow(queryInfo, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isTabInAutomationWindow(tabOrId, options = {}) {
|
||||||
|
return tabRuntime.isTabInAutomationWindow(tabOrId, options);
|
||||||
|
}
|
||||||
|
|
||||||
function parseUrlSafely(rawUrl) {
|
function parseUrlSafely(rawUrl) {
|
||||||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.parseUrlSafely) {
|
if (typeof navigationUtils !== 'undefined' && navigationUtils?.parseUrlSafely) {
|
||||||
return navigationUtils.parseUrlSafely(rawUrl);
|
return navigationUtils.parseUrlSafely(rawUrl);
|
||||||
@@ -9981,7 +10003,9 @@ const contributionOAuthManager = self.MultiPageBackgroundContributionOAuth?.crea
|
|||||||
broadcastDataUpdate,
|
broadcastDataUpdate,
|
||||||
chrome,
|
chrome,
|
||||||
closeLocalhostCallbackTabs,
|
closeLocalhostCallbackTabs,
|
||||||
|
createAutomationTab,
|
||||||
getState,
|
getState,
|
||||||
|
queryTabsInAutomationWindow,
|
||||||
setState,
|
setState,
|
||||||
});
|
});
|
||||||
contributionOAuthManager?.ensureCallbackListeners?.();
|
contributionOAuthManager?.ensureCallbackListeners?.();
|
||||||
@@ -11179,6 +11203,7 @@ const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
|
|||||||
chrome,
|
chrome,
|
||||||
addLog,
|
addLog,
|
||||||
closeConflictingTabsForSource,
|
closeConflictingTabsForSource,
|
||||||
|
createAutomationTab,
|
||||||
ensureContentScriptReadyOnTab,
|
ensureContentScriptReadyOnTab,
|
||||||
getPanelMode,
|
getPanelMode,
|
||||||
normalizeCodex2ApiUrl,
|
normalizeCodex2ApiUrl,
|
||||||
@@ -11465,6 +11490,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
|
|||||||
addLog,
|
addLog,
|
||||||
chrome,
|
chrome,
|
||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
|
createAutomationTab,
|
||||||
ensureContentScriptReadyOnTabUntilStopped,
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||||
markCurrentRegistrationAccountUsed,
|
markCurrentRegistrationAccountUsed,
|
||||||
@@ -11488,6 +11514,7 @@ const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?
|
|||||||
getTabId,
|
getTabId,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
markCurrentRegistrationAccountUsed,
|
markCurrentRegistrationAccountUsed,
|
||||||
|
queryTabsInAutomationWindow,
|
||||||
sendTabMessageUntilStopped,
|
sendTabMessageUntilStopped,
|
||||||
setState,
|
setState,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
@@ -11503,6 +11530,7 @@ const goPayManualConfirmExecutor = self.MultiPageBackgroundGoPayManualConfirm?.c
|
|||||||
getTabId,
|
getTabId,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
registerTab,
|
registerTab,
|
||||||
|
createAutomationTab,
|
||||||
setState,
|
setState,
|
||||||
});
|
});
|
||||||
const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({
|
const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({
|
||||||
@@ -11510,6 +11538,7 @@ const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPa
|
|||||||
chrome,
|
chrome,
|
||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
ensureContentScriptReadyOnTabUntilStopped,
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
queryTabsInAutomationWindow,
|
||||||
getTabId,
|
getTabId,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
sendTabMessageUntilStopped,
|
sendTabMessageUntilStopped,
|
||||||
@@ -11525,6 +11554,7 @@ const goPayApproveExecutor = self.MultiPageBackgroundGoPayApprove?.createGoPayAp
|
|||||||
ensureContentScriptReadyOnTabUntilStopped,
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
getTabId,
|
getTabId,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
|
queryTabsInAutomationWindow,
|
||||||
registerTab,
|
registerTab,
|
||||||
sendTabMessageUntilStopped,
|
sendTabMessageUntilStopped,
|
||||||
setState,
|
setState,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
broadcastDataUpdate,
|
broadcastDataUpdate,
|
||||||
chrome,
|
chrome,
|
||||||
closeLocalhostCallbackTabs,
|
closeLocalhostCallbackTabs,
|
||||||
|
createAutomationTab = null,
|
||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
} = deps;
|
} = deps;
|
||||||
@@ -437,7 +438,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!tab) {
|
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({
|
await applyRuntimeUpdates({
|
||||||
|
|||||||
@@ -30,6 +30,60 @@ let ipProxyLastRuntimeError = {
|
|||||||
fatal: false,
|
fatal: false,
|
||||||
at: 0,
|
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 = [
|
const IP_PROXY_EXIT_PROBE_ENDPOINTS = [
|
||||||
'http://ip-api.com/json?lang=en',
|
'http://ip-api.com/json?lang=en',
|
||||||
'http://ipinfo.io/json',
|
'http://ipinfo.io/json',
|
||||||
@@ -1974,8 +2028,8 @@ function extractProbeHostFromTabUrl(rawUrl = '') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickExistingPageContextProbeTabId() {
|
async function pickExistingPageContextProbeTabId(options = {}) {
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await queryAutomationScopedTabs({}, options);
|
||||||
const candidates = tabs
|
const candidates = tabs
|
||||||
.filter((tab) => Number.isInteger(tab?.id))
|
.filter((tab) => Number.isInteger(tab?.id))
|
||||||
.filter((tab) => isPageContextProbeHost(extractProbeHostFromTabUrl(tab?.url || '')));
|
.filter((tab) => isPageContextProbeHost(extractProbeHostFromTabUrl(tab?.url || '')));
|
||||||
@@ -2345,10 +2399,10 @@ async function detectIpProxyTargetReachabilityByPageContext(options = {}) {
|
|||||||
const targetUrl = appendProbeCacheBuster(endpoint, '_multipage_proxy_target');
|
const targetUrl = appendProbeCacheBuster(endpoint, '_multipage_proxy_target');
|
||||||
try {
|
try {
|
||||||
if (!Number.isInteger(tabId)) {
|
if (!Number.isInteger(tabId)) {
|
||||||
const tab = await chrome.tabs.create({
|
const tab = await createAutomationScopedTab({
|
||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
active: false,
|
active: false,
|
||||||
});
|
}, options);
|
||||||
tabId = Number(tab?.id) || null;
|
tabId = Number(tab?.id) || null;
|
||||||
createdTabId = tabId;
|
createdTabId = tabId;
|
||||||
navigationErrorTracker.setTabId(tabId);
|
navigationErrorTracker.setTabId(tabId);
|
||||||
@@ -2424,15 +2478,15 @@ async function detectProxyExitInfoByPageContext(options = {}) {
|
|||||||
let createdProbeTabId = null;
|
let createdProbeTabId = null;
|
||||||
const probeUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}`;
|
const probeUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}`;
|
||||||
try {
|
try {
|
||||||
const tab = await chrome.tabs.create({
|
const tab = await createAutomationScopedTab({
|
||||||
url: probeUrl,
|
url: probeUrl,
|
||||||
active: false,
|
active: false,
|
||||||
});
|
}, options);
|
||||||
probeTabId = Number(tab?.id) || null;
|
probeTabId = Number(tab?.id) || null;
|
||||||
createdProbeTabId = probeTabId;
|
createdProbeTabId = probeTabId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(`probe:page_context:create_probe_tab_failed:${error?.message || error}`);
|
errors.push(`probe:page_context:create_probe_tab_failed:${error?.message || error}`);
|
||||||
probeTabId = await pickExistingPageContextProbeTabId();
|
probeTabId = await pickExistingPageContextProbeTabId(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(probeTabId)) {
|
if (!Number.isInteger(probeTabId)) {
|
||||||
@@ -2544,10 +2598,10 @@ async function detectProxyExitInfoByPageContext(options = {}) {
|
|||||||
errors.push(`probe:page_context:script_failed:${scriptError?.message || scriptError}`);
|
errors.push(`probe:page_context:script_failed:${scriptError?.message || scriptError}`);
|
||||||
if (!createdProbeTabId && isProbeErrorPageExecutionError(scriptError)) {
|
if (!createdProbeTabId && isProbeErrorPageExecutionError(scriptError)) {
|
||||||
const retryUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}&retry=1`;
|
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,
|
url: retryUrl,
|
||||||
active: false,
|
active: false,
|
||||||
});
|
}, options);
|
||||||
const retryTabId = Number(retryTab?.id) || 0;
|
const retryTabId = Number(retryTab?.id) || 0;
|
||||||
if (retryTabId > 0) {
|
if (retryTabId > 0) {
|
||||||
createdProbeTabId = retryTabId;
|
createdProbeTabId = retryTabId;
|
||||||
@@ -2635,6 +2689,9 @@ async function detectProxyExitInfo(options = {}) {
|
|||||||
timeoutMs,
|
timeoutMs,
|
||||||
errors,
|
errors,
|
||||||
probeEndpoints,
|
probeEndpoints,
|
||||||
|
automationWindowId: options?.automationWindowId,
|
||||||
|
windowId: options?.windowId,
|
||||||
|
state: options?.state,
|
||||||
});
|
});
|
||||||
if (pageResult?.ip) {
|
if (pageResult?.ip) {
|
||||||
return pageResult;
|
return pageResult;
|
||||||
@@ -2669,6 +2726,9 @@ async function detectProxyExitInfo(options = {}) {
|
|||||||
timeoutMs: Math.max(5000, Math.min(9000, timeoutMs)),
|
timeoutMs: Math.max(5000, Math.min(9000, timeoutMs)),
|
||||||
errors,
|
errors,
|
||||||
probeEndpoints,
|
probeEndpoints,
|
||||||
|
automationWindowId: options?.automationWindowId,
|
||||||
|
windowId: options?.windowId,
|
||||||
|
state: options?.state,
|
||||||
});
|
});
|
||||||
errors.push(`probe:bg:abort_storm_page_fallback_result:${String(pageResult?.source || 'unknown')}`);
|
errors.push(`probe:bg:abort_storm_page_fallback_result:${String(pageResult?.source || 'unknown')}`);
|
||||||
if (pageResult?.ip) {
|
if (pageResult?.ip) {
|
||||||
@@ -3308,6 +3368,7 @@ async function applyIpProxySettingsFromState(state = {}, options = {}) {
|
|||||||
// 后台探测仅作为补充兜底,避免单一路径误判。
|
// 后台探测仅作为补充兜底,避免单一路径误判。
|
||||||
preferPageContext: true,
|
preferPageContext: true,
|
||||||
allowBackgroundFallback: true,
|
allowBackgroundFallback: true,
|
||||||
|
state,
|
||||||
}).catch(() => ({ ip: '', region: '' }));
|
}).catch(() => ({ ip: '', region: '' }));
|
||||||
if (!exit?.ip && Boolean(status?.hasAuth)) {
|
if (!exit?.ip && Boolean(status?.hasAuth)) {
|
||||||
appendIpProxyAuthDiagnosticsToErrors(diagnostics);
|
appendIpProxyAuthDiagnosticsToErrors(diagnostics);
|
||||||
@@ -3339,6 +3400,7 @@ async function applyIpProxySettingsFromState(state = {}, options = {}) {
|
|||||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||||
errors: targetDiagnostics,
|
errors: targetDiagnostics,
|
||||||
|
state,
|
||||||
}).catch((error) => ({
|
}).catch((error) => ({
|
||||||
reachable: false,
|
reachable: false,
|
||||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||||
@@ -3675,6 +3737,7 @@ async function probeIpProxyExit(options = {}) {
|
|||||||
username: String(state?.ipProxyUsername || '').trim(),
|
username: String(state?.ipProxyUsername || '').trim(),
|
||||||
preferPageContext: true,
|
preferPageContext: true,
|
||||||
allowBackgroundFallback: true,
|
allowBackgroundFallback: true,
|
||||||
|
state,
|
||||||
}).catch(() => ({ ip: '', region: '', endpoint: '', source: '' }));
|
}).catch(() => ({ ip: '', region: '', endpoint: '', source: '' }));
|
||||||
const status = {
|
const status = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -3774,6 +3837,7 @@ async function probeIpProxyExit(options = {}) {
|
|||||||
// 手动探测与自动应用保持一致:先页面上下文,再后台补充。
|
// 手动探测与自动应用保持一致:先页面上下文,再后台补充。
|
||||||
preferPageContext: true,
|
preferPageContext: true,
|
||||||
allowBackgroundFallback: true,
|
allowBackgroundFallback: true,
|
||||||
|
state,
|
||||||
}).catch(() => ({ ip: '', region: '' }));
|
}).catch(() => ({ ip: '', region: '' }));
|
||||||
if (!exit?.ip && Boolean(statusSeed?.hasAuth)) {
|
if (!exit?.ip && Boolean(statusSeed?.hasAuth)) {
|
||||||
appendIpProxyAuthDiagnosticsToErrors(diagnostics);
|
appendIpProxyAuthDiagnosticsToErrors(diagnostics);
|
||||||
@@ -3800,6 +3864,7 @@ async function probeIpProxyExit(options = {}) {
|
|||||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||||
errors: targetDiagnostics,
|
errors: targetDiagnostics,
|
||||||
|
state,
|
||||||
}).catch((error) => ({
|
}).catch((error) => ({
|
||||||
reachable: false,
|
reachable: false,
|
||||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
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 = {}) {
|
async function syncStepAccountIdentityFromPayload(payload = {}) {
|
||||||
const identifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase();
|
const identifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase();
|
||||||
const signupPhoneNumber = resolveSignupPhonePayload(payload);
|
const signupPhoneNumber = resolveSignupPhonePayload(payload);
|
||||||
@@ -821,6 +849,7 @@
|
|||||||
case 'EXECUTE_STEP': {
|
case 'EXECUTE_STEP': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
if (message.source === 'sidepanel') {
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
await ensureManualInteractionAllowed('手动执行步骤');
|
await ensureManualInteractionAllowed('手动执行步骤');
|
||||||
}
|
}
|
||||||
const step = message.payload.step;
|
const step = message.payload.step;
|
||||||
@@ -848,6 +877,9 @@
|
|||||||
|
|
||||||
case 'AUTO_RUN': {
|
case 'AUTO_RUN': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
|
}
|
||||||
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
||||||
await setContributionMode(true);
|
await setContributionMode(true);
|
||||||
if (typeof setState === 'function') {
|
if (typeof setState === 'function') {
|
||||||
@@ -873,6 +905,9 @@
|
|||||||
|
|
||||||
case 'SCHEDULE_AUTO_RUN': {
|
case 'SCHEDULE_AUTO_RUN': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
|
}
|
||||||
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
||||||
await setContributionMode(true);
|
await setContributionMode(true);
|
||||||
if (typeof setState === 'function') {
|
if (typeof setState === 'function') {
|
||||||
@@ -894,6 +929,9 @@
|
|||||||
|
|
||||||
case 'START_SCHEDULED_AUTO_RUN_NOW': {
|
case 'START_SCHEDULED_AUTO_RUN_NOW': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
|
}
|
||||||
const started = await launchAutoRunTimerPlan('manual', {
|
const started = await launchAutoRunTimerPlan('manual', {
|
||||||
expectedKinds: [AUTO_RUN_TIMER_KIND_SCHEDULED_START],
|
expectedKinds: [AUTO_RUN_TIMER_KIND_SCHEDULED_START],
|
||||||
});
|
});
|
||||||
@@ -913,6 +951,9 @@
|
|||||||
|
|
||||||
case 'SKIP_AUTO_RUN_COUNTDOWN': {
|
case 'SKIP_AUTO_RUN_COUNTDOWN': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
|
}
|
||||||
const skipped = await skipAutoRunCountdown();
|
const skipped = await skipAutoRunCountdown();
|
||||||
if (!skipped) {
|
if (!skipped) {
|
||||||
throw new Error('当前没有可立即开始的倒计时。');
|
throw new Error('当前没有可立即开始的倒计时。');
|
||||||
@@ -922,6 +963,9 @@
|
|||||||
|
|
||||||
case 'RESUME_AUTO_RUN': {
|
case 'RESUME_AUTO_RUN': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
|
}
|
||||||
if (message.payload.email) {
|
if (message.payload.email) {
|
||||||
await setEmailState(message.payload.email);
|
await setEmailState(message.payload.email);
|
||||||
}
|
}
|
||||||
@@ -1117,6 +1161,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'PROBE_IP_PROXY_EXIT': {
|
case 'PROBE_IP_PROXY_EXIT': {
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await lockAutomationWindowFromMessage(message, sender);
|
||||||
|
}
|
||||||
if (typeof probeIpProxyExit !== 'function') {
|
if (typeof probeIpProxyExit !== 'function') {
|
||||||
throw new Error('IP 代理出口检测能力尚未接入。');
|
throw new Error('IP 代理出口检测能力尚未接入。');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
chrome,
|
chrome,
|
||||||
addLog,
|
addLog,
|
||||||
closeConflictingTabsForSource,
|
closeConflictingTabsForSource,
|
||||||
|
createAutomationTab = null,
|
||||||
ensureContentScriptReadyOnTab,
|
ensureContentScriptReadyOnTab,
|
||||||
getPanelMode,
|
getPanelMode,
|
||||||
normalizeCodex2ApiUrl,
|
normalizeCodex2ApiUrl,
|
||||||
@@ -266,7 +267,9 @@
|
|||||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
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;
|
const tabId = tab.id;
|
||||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||||
|
|
||||||
|
|||||||
+121
-8
@@ -19,6 +19,88 @@
|
|||||||
|
|
||||||
const pendingCommands = new Map();
|
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) {
|
async function sleepOrStop(ms) {
|
||||||
if (typeof sleepWithStop === 'function') {
|
if (typeof sleepWithStop === 'function') {
|
||||||
await sleepWithStop(ms);
|
await sleepWithStop(ms);
|
||||||
@@ -86,7 +168,20 @@
|
|||||||
|
|
||||||
async function registerTab(source, tabId) {
|
async function registerTab(source, tabId) {
|
||||||
const registry = await getTabRegistry();
|
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 });
|
await setState({ tabRegistry: registry });
|
||||||
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
|
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
|
||||||
}
|
}
|
||||||
@@ -96,7 +191,12 @@
|
|||||||
const entry = registry[source];
|
const entry = registry[source];
|
||||||
if (!entry) return false;
|
if (!entry) return false;
|
||||||
try {
|
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;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
registry[source] = null;
|
registry[source] = null;
|
||||||
@@ -107,7 +207,16 @@
|
|||||||
|
|
||||||
async function getTabId(source) {
|
async function getTabId(source) {
|
||||||
const registry = await getTabRegistry();
|
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) {
|
async function rememberSourceLastUrl(source, url) {
|
||||||
@@ -127,7 +236,7 @@
|
|||||||
|
|
||||||
if (!referenceUrls.length) return;
|
if (!referenceUrls.length) return;
|
||||||
|
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await queryTabsInAutomationWindow({});
|
||||||
const matchedIds = tabs
|
const matchedIds = tabs
|
||||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||||
.filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
|
.filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
|
||||||
@@ -164,7 +273,7 @@
|
|||||||
|
|
||||||
const { excludeTabIds = [] } = options;
|
const { excludeTabIds = [] } = options;
|
||||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await queryTabsInAutomationWindow({});
|
||||||
const matchedIds = tabs
|
const matchedIds = tabs
|
||||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||||
.filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
|
.filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
|
||||||
@@ -198,7 +307,7 @@
|
|||||||
const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
|
const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
|
||||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||||
const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
|
const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await queryTabsInAutomationWindow({});
|
||||||
const matchedIds = tabs
|
const matchedIds = tabs
|
||||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||||
.filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
|
.filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
|
||||||
@@ -534,7 +643,7 @@
|
|||||||
async function reuseOrCreateTab(source, url, options = {}) {
|
async function reuseOrCreateTab(source, url, options = {}) {
|
||||||
if (options.forceNew) {
|
if (options.forceNew) {
|
||||||
await closeConflictingTabsForSource(source, url);
|
await closeConflictingTabsForSource(source, url);
|
||||||
const tab = await chrome.tabs.create({ url, active: true });
|
const tab = await createAutomationTab({ url, active: true }, options);
|
||||||
|
|
||||||
if (options.inject) {
|
if (options.inject) {
|
||||||
await waitForTabUpdateComplete(tab.id);
|
await waitForTabUpdateComplete(tab.id);
|
||||||
@@ -626,7 +735,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await closeConflictingTabsForSource(source, url);
|
await closeConflictingTabsForSource(source, url);
|
||||||
const tab = await chrome.tabs.create({ url, active: true });
|
const tab = await createAutomationTab({ url, active: true }, options);
|
||||||
|
|
||||||
if (options.inject) {
|
if (options.inject) {
|
||||||
await waitForTabUpdateComplete(tab.id);
|
await waitForTabUpdateComplete(tab.id);
|
||||||
@@ -787,16 +896,20 @@
|
|||||||
closeConflictingTabsForSource,
|
closeConflictingTabsForSource,
|
||||||
closeLocalhostCallbackTabs,
|
closeLocalhostCallbackTabs,
|
||||||
closeTabsByUrlPrefix,
|
closeTabsByUrlPrefix,
|
||||||
|
createAutomationTab,
|
||||||
ensureContentScriptReadyOnTab,
|
ensureContentScriptReadyOnTab,
|
||||||
flushCommand,
|
flushCommand,
|
||||||
|
getAutomationWindowId,
|
||||||
getContentScriptResponseTimeoutMs,
|
getContentScriptResponseTimeoutMs,
|
||||||
getMessageDebugLabel,
|
getMessageDebugLabel,
|
||||||
getTabId,
|
getTabId,
|
||||||
getTabRegistry,
|
getTabRegistry,
|
||||||
isLocalhostOAuthCallbackTabMatch,
|
isLocalhostOAuthCallbackTabMatch,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
|
isTabInAutomationWindow,
|
||||||
pingContentScriptOnTab,
|
pingContentScriptOnTab,
|
||||||
queueCommand,
|
queueCommand,
|
||||||
|
queryTabsInAutomationWindow,
|
||||||
registerTab,
|
registerTab,
|
||||||
rememberSourceLastUrl,
|
rememberSourceLastUrl,
|
||||||
resolveResponseTimeoutMs,
|
resolveResponseTimeoutMs,
|
||||||
|
|||||||
@@ -1618,7 +1618,10 @@ async function changeIpProxyExitBySession(options = {}) {
|
|||||||
|
|
||||||
async function probeIpProxyExit(options = {}) {
|
async function probeIpProxyExit(options = {}) {
|
||||||
const { silent = false } = options;
|
const { silent = false } = options;
|
||||||
const response = await chrome.runtime.sendMessage({
|
const sendMessage = typeof sendSidepanelMessage === 'function'
|
||||||
|
? sendSidepanelMessage
|
||||||
|
: (message) => chrome.runtime.sendMessage(message);
|
||||||
|
const response = await sendMessage({
|
||||||
type: 'PROBE_IP_PROXY_EXIT',
|
type: 'PROBE_IP_PROXY_EXIT',
|
||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: {},
|
payload: {},
|
||||||
|
|||||||
+68
-8
@@ -1151,6 +1151,66 @@ let currentReleaseSnapshot = null;
|
|||||||
let currentContributionContentSnapshot = null;
|
let currentContributionContentSnapshot = null;
|
||||||
let contributionContentSnapshotRequestInFlight = null;
|
let contributionContentSnapshotRequestInFlight = null;
|
||||||
|
|
||||||
|
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 getCurrentSidepanelWindowId() {
|
||||||
|
if (chrome?.windows?.getCurrent) {
|
||||||
|
try {
|
||||||
|
const currentWindow = await chrome.windows.getCurrent();
|
||||||
|
const windowId = normalizeAutomationWindowId(currentWindow?.id);
|
||||||
|
if (windowId !== null) {
|
||||||
|
return windowId;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get current sidepanel window:', error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeAutomationWindowId(latestState?.automationWindowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldAttachAutomationWindow(message = {}) {
|
||||||
|
const source = String(message?.source || '').trim();
|
||||||
|
if (source && source !== 'sidepanel') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'EXECUTE_STEP',
|
||||||
|
'AUTO_RUN',
|
||||||
|
'SCHEDULE_AUTO_RUN',
|
||||||
|
'RESUME_AUTO_RUN',
|
||||||
|
'START_SCHEDULED_AUTO_RUN_NOW',
|
||||||
|
'SKIP_AUTO_RUN_COUNTDOWN',
|
||||||
|
'PROBE_IP_PROXY_EXIT',
|
||||||
|
].includes(String(message?.type || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendSidepanelMessage(message = {}) {
|
||||||
|
const payload = {
|
||||||
|
...(message || {}),
|
||||||
|
source: message?.source || 'sidepanel',
|
||||||
|
};
|
||||||
|
if (shouldAttachAutomationWindow(payload)) {
|
||||||
|
const windowId = await getCurrentSidepanelWindowId();
|
||||||
|
if (windowId !== null) {
|
||||||
|
payload.payload = {
|
||||||
|
...(payload.payload || {}),
|
||||||
|
automationWindowId: windowId,
|
||||||
|
};
|
||||||
|
syncLatestState({ automationWindowId: windowId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chrome.runtime.sendMessage(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.sendSidepanelMessage = sendSidepanelMessage;
|
||||||
|
|
||||||
const DEFAULT_SUB2API_GROUP_OPTIONS = ['codex', 'openai-plus'];
|
const DEFAULT_SUB2API_GROUP_OPTIONS = ['codex', 'openai-plus'];
|
||||||
const editableListPickerModule = window.SidepanelEditableListPicker || {};
|
const editableListPickerModule = window.SidepanelEditableListPicker || {};
|
||||||
const normalizeEditableListValues = editableListPickerModule.normalizeEditableListValues
|
const normalizeEditableListValues = editableListPickerModule.normalizeEditableListValues
|
||||||
@@ -11186,12 +11246,12 @@ stepsList?.addEventListener('click', async (event) => {
|
|||||||
syncLatestState({ customPassword: inputPassword.value });
|
syncLatestState({ customPassword: inputPassword.value });
|
||||||
}
|
}
|
||||||
if (shouldExecuteStep3WithSignupPhoneIdentity(latestState)) {
|
if (shouldExecuteStep3WithSignupPhoneIdentity(latestState)) {
|
||||||
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
} else if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
|
} else if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
|
||||||
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
@@ -11201,7 +11261,7 @@ stepsList?.addEventListener('click', async (event) => {
|
|||||||
showToast(selectMailProvider.value === GMAIL_PROVIDER ? '请先填写 Gmail 原邮箱。' : '请先填写 2925 邮箱前缀。', 'warn');
|
showToast(selectMailProvider.value === GMAIL_PROVIDER ? '请先填写 Gmail 原邮箱。' : '请先填写 2925 邮箱前缀。', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, emailPrefix } });
|
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, emailPrefix } });
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
@@ -11222,13 +11282,13 @@ stepsList?.addEventListener('click', async (event) => {
|
|||||||
if (!validateCurrentRegistrationEmail(email, { showToastOnFailure: true })) {
|
if (!validateCurrentRegistrationEmail(email, { showToastOnFailure: true })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
@@ -11490,7 +11550,7 @@ async function startAutoRunFromCurrentSettings() {
|
|||||||
btnAutoRun.innerHTML = delayEnabled
|
btnAutoRun.innerHTML = delayEnabled
|
||||||
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 计划中...'
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 计划中...'
|
||||||
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await sendSidepanelMessage({
|
||||||
type: delayEnabled ? 'SCHEDULE_AUTO_RUN' : 'AUTO_RUN',
|
type: delayEnabled ? 'SCHEDULE_AUTO_RUN' : 'AUTO_RUN',
|
||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: {
|
payload: {
|
||||||
@@ -11532,14 +11592,14 @@ btnAutoContinue.addEventListener('click', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
await chrome.runtime.sendMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } });
|
await sendSidepanelMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } });
|
||||||
});
|
});
|
||||||
|
|
||||||
btnAutoRunNow?.addEventListener('click', async () => {
|
btnAutoRunNow?.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
btnAutoRunNow.disabled = true;
|
btnAutoRunNow.disabled = true;
|
||||||
const waitingInterval = currentAutoRun.phase === 'waiting_interval';
|
const waitingInterval = currentAutoRun.phase === 'waiting_interval';
|
||||||
await chrome.runtime.sendMessage({
|
await sendSidepanelMessage({
|
||||||
type: waitingInterval ? 'SKIP_AUTO_RUN_COUNTDOWN' : 'START_SCHEDULED_AUTO_RUN_NOW',
|
type: waitingInterval ? 'SKIP_AUTO_RUN_COUNTDOWN' : 'START_SCHEDULED_AUTO_RUN_NOW',
|
||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: {},
|
payload: {},
|
||||||
|
|||||||
@@ -188,3 +188,75 @@ test('tab runtime waitForTabStableComplete waits through a late navigation after
|
|||||||
assert.equal(result?.status, 'complete');
|
assert.equal(result?.status, 'complete');
|
||||||
assert.ok(getCalls >= 4);
|
assert.ok(getCalls >= 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tab runtime opens new automation tabs in the 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);
|
||||||
|
return { id: 17, windowId: payload.windowId, url: payload.url };
|
||||||
|
},
|
||||||
|
get: async () => ({ id: 17, windowId: 100, url: 'https://example.com' }),
|
||||||
|
query: async () => [],
|
||||||
|
onUpdated: {
|
||||||
|
addListener: () => {},
|
||||||
|
removeListener: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getSourceLabel: (sourceName) => sourceName || 'unknown',
|
||||||
|
getState: async () => ({
|
||||||
|
automationWindowId: 100,
|
||||||
|
tabRegistry: {},
|
||||||
|
sourceLastUrls: {},
|
||||||
|
}),
|
||||||
|
matchesSourceUrlFamily: () => false,
|
||||||
|
setState: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.reuseOrCreateTab('signup-page', 'https://example.com');
|
||||||
|
|
||||||
|
assert.equal(created.length, 1);
|
||||||
|
assert.equal(created[0].windowId, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tab runtime scopes tab queries to the locked automation window', 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: 22, url: 'https://example.com' }),
|
||||||
|
query: async (queryInfo) => {
|
||||||
|
queries.push(queryInfo);
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getSourceLabel: (sourceName) => sourceName || 'unknown',
|
||||||
|
getState: async () => ({
|
||||||
|
automationWindowId: 22,
|
||||||
|
tabRegistry: {},
|
||||||
|
sourceLastUrls: {},
|
||||||
|
}),
|
||||||
|
matchesSourceUrlFamily: () => false,
|
||||||
|
setState: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.queryTabsInAutomationWindow({ active: true, currentWindow: true });
|
||||||
|
|
||||||
|
assert.deepEqual(queries[0], { active: true, windowId: 22 });
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let index = start; index < source.length; index += 1) {
|
||||||
|
const ch = source[index];
|
||||||
|
if (ch === '(') parenDepth += 1;
|
||||||
|
if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) signatureEnded = true;
|
||||||
|
}
|
||||||
|
if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('message router locks automation window from sidepanel payload', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeAutomationWindowId'),
|
||||||
|
extractFunction('resolveAutomationWindowIdFromMessage'),
|
||||||
|
extractFunction('lockAutomationWindowFromMessage'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const updates = [];
|
||||||
|
async function setState(update) {
|
||||||
|
updates.push(update);
|
||||||
|
}
|
||||||
|
${bundle}
|
||||||
|
return {
|
||||||
|
lockAutomationWindowFromMessage,
|
||||||
|
updates,
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const windowId = await api.lockAutomationWindowFromMessage({
|
||||||
|
payload: { automationWindowId: 88 },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(windowId, 88);
|
||||||
|
assert.deepEqual(api.updates, [{ automationWindowId: 88 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('message router can fall back to sender tab window id', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeAutomationWindowId'),
|
||||||
|
extractFunction('resolveAutomationWindowIdFromMessage'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
${bundle}
|
||||||
|
return { resolveAutomationWindowIdFromMessage };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
api.resolveAutomationWindowIdFromMessage({}, { tab: { windowId: 19 } }),
|
||||||
|
19
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ function createExecutor({
|
|||||||
getTabId = async (source) => (source === 'paypal-flow' ? 1 : null),
|
getTabId = async (source) => (source === 'paypal-flow' ? 1 : null),
|
||||||
isTabAlive = async () => true,
|
isTabAlive = async () => true,
|
||||||
queryTabs = [],
|
queryTabs = [],
|
||||||
|
queryTabsInAutomationWindow = null,
|
||||||
}) {
|
}) {
|
||||||
const api = loadModule();
|
const api = loadModule();
|
||||||
const events = {
|
const events = {
|
||||||
@@ -61,6 +62,7 @@ function createExecutor({
|
|||||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||||
getTabId,
|
getTabId,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
|
...(typeof queryTabsInAutomationWindow === 'function' ? { queryTabsInAutomationWindow } : {}),
|
||||||
sendTabMessageUntilStopped: async (_tabId, _source, message) => {
|
sendTabMessageUntilStopped: async (_tabId, _source, message) => {
|
||||||
events.messages.push(message.type);
|
events.messages.push(message.type);
|
||||||
if (message.type === 'PAYPAL_GET_STATE') {
|
if (message.type === 'PAYPAL_GET_STATE') {
|
||||||
@@ -406,6 +408,41 @@ test('PayPal approve discovers an already open unregistered PayPal tab', async (
|
|||||||
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
|
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PayPal approve discovers PayPal tabs through the locked automation window query', async () => {
|
||||||
|
const queries = [];
|
||||||
|
const { executor, events } = createExecutor({
|
||||||
|
pageStates: [
|
||||||
|
{ needsLogin: false, approveReady: true },
|
||||||
|
],
|
||||||
|
submitResults: [],
|
||||||
|
getTabId: async () => null,
|
||||||
|
isTabAlive: async () => false,
|
||||||
|
queryTabsInAutomationWindow: async (queryInfo) => {
|
||||||
|
queries.push(queryInfo);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
active: true,
|
||||||
|
currentWindow: true,
|
||||||
|
url: 'https://www.paypal.com/pay/?token=BA-window',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
tabUrls: [
|
||||||
|
'https://www.paypal.com/pay/?token=BA-window',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePayPalApprove({
|
||||||
|
paypalEmail: 'user@example.com',
|
||||||
|
paypalPassword: 'secret',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(queries, [{}]);
|
||||||
|
assert.deepEqual(events.updatedTabs, [{ tabId: 9, updateInfo: { active: true } }]);
|
||||||
|
assert.deepEqual(events.completed.map((item) => item.step), [8]);
|
||||||
|
});
|
||||||
|
|
||||||
test('PayPal approve auto-detects split email then password pages', async () => {
|
test('PayPal approve auto-detects split email then password pages', async () => {
|
||||||
const { executor, events } = createExecutor({
|
const { executor, events } = createExecutor({
|
||||||
pageStates: [
|
pageStates: [
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ const console = {
|
|||||||
events.push({ type: 'warn', args });
|
events.push({ type: 'warn', args });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
async function sendSidepanelMessage(message) {
|
||||||
|
return chrome.runtime.sendMessage(message);
|
||||||
|
}
|
||||||
async function persistCurrentSettingsForAction() {
|
async function persistCurrentSettingsForAction() {
|
||||||
events.push({ type: 'sync-settings' });
|
events.push({ type: 'sync-settings' });
|
||||||
${persistImpl ? `return (${persistImpl})(events, {
|
${persistImpl ? `return (${persistImpl})(events, {
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let index = start; index < source.length; index += 1) {
|
||||||
|
const ch = source[index];
|
||||||
|
if (ch === '(') parenDepth += 1;
|
||||||
|
if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) signatureEnded = true;
|
||||||
|
}
|
||||||
|
if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApi() {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeAutomationWindowId'),
|
||||||
|
extractFunction('getCurrentSidepanelWindowId'),
|
||||||
|
extractFunction('shouldAttachAutomationWindow'),
|
||||||
|
extractFunction('sendSidepanelMessage'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
return new Function(`
|
||||||
|
let latestState = {};
|
||||||
|
const events = [];
|
||||||
|
const chrome = {
|
||||||
|
windows: {
|
||||||
|
async getCurrent() {
|
||||||
|
events.push({ type: 'get-current-window' });
|
||||||
|
return { id: 321 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
async sendMessage(message) {
|
||||||
|
events.push({ type: 'send', message });
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const console = { warn() {} };
|
||||||
|
function syncLatestState(patch) {
|
||||||
|
latestState = { ...latestState, ...patch };
|
||||||
|
events.push({ type: 'sync', patch });
|
||||||
|
}
|
||||||
|
${bundle}
|
||||||
|
return {
|
||||||
|
sendSidepanelMessage,
|
||||||
|
shouldAttachAutomationWindow,
|
||||||
|
getEvents() {
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getLatestState() {
|
||||||
|
return latestState;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('sidepanel attaches the current window id when starting automation actions', async () => {
|
||||||
|
const api = createApi();
|
||||||
|
|
||||||
|
await api.sendSidepanelMessage({
|
||||||
|
type: 'AUTO_RUN',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { totalRuns: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = api.getEvents().find((entry) => entry.type === 'send').message;
|
||||||
|
assert.equal(sent.payload.automationWindowId, 321);
|
||||||
|
assert.equal(api.getLatestState().automationWindowId, 321);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidepanel leaves non-automation messages unchanged', async () => {
|
||||||
|
const api = createApi();
|
||||||
|
|
||||||
|
await api.sendSidepanelMessage({
|
||||||
|
type: 'SAVE_SETTING',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { emailPrefix: 'demo' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = api.getEvents();
|
||||||
|
assert.equal(events.some((entry) => entry.type === 'get-current-window'), false);
|
||||||
|
assert.deepEqual(events.find((entry) => entry.type === 'send').message.payload, { emailPrefix: 'demo' });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user