diff --git a/README.md b/README.md index 0db9326..11bcbc3 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,7 @@ Step 3 使用的注册邮箱。 - localhost 回调地址 - 账号记录 - tab 注册信息 +- 各来源最近一次打开的地址(用于打开新地址前清理旧标签) - 自定义设置 - Auto 当前阶段、当前轮次、暂停信息 @@ -305,6 +306,7 @@ Step 3 使用的注册邮箱。 - 浏览器会话级存储 - 扩展运行期间可在多个步骤之间共享 - 代码里已启用 `storage.session` 对 content script 的访问 +- 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留 ## 项目结构 diff --git a/background.js b/background.js index ad43bbc..3dcd2a2 100644 --- a/background.js +++ b/background.js @@ -30,6 +30,7 @@ const DEFAULT_STATE = { localhostUrl: null, flowStartTime: null, tabRegistry: {}, + sourceLastUrls: {}, logs: [], vpsUrl: '', vpsPassword: '', @@ -93,6 +94,7 @@ async function resetState() { 'seenInbucketMailIds', 'accounts', 'tabRegistry', + 'sourceLastUrls', 'vpsUrl', 'vpsPassword', 'customPassword', @@ -108,6 +110,7 @@ async function resetState() { seenInbucketMailIds: prev.seenInbucketMailIds || [], accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, + sourceLastUrls: prev.sourceLastUrls || {}, vpsUrl: prev.vpsUrl || '', vpsPassword: prev.vpsPassword || '', customPassword: prev.customPassword || '', @@ -180,6 +183,81 @@ async function getTabId(source) { return registry[source]?.tabId || null; } +function parseUrlSafely(rawUrl) { + if (!rawUrl) return null; + try { + return new URL(rawUrl); + } catch { + return null; + } +} + +function isSignupPageHost(hostname = '') { + return ['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com'].includes(hostname); +} + +function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { + const candidate = parseUrlSafely(candidateUrl); + if (!candidate) return false; + + const reference = parseUrlSafely(referenceUrl); + + switch (source) { + case 'signup-page': + return isSignupPageHost(candidate.hostname); + case 'duck-mail': + return candidate.hostname === 'duckduckgo.com' && candidate.pathname.startsWith('/email/'); + case 'qq-mail': + return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com'; + case 'mail-163': + return candidate.hostname === 'mail.163.com'; + case 'inbucket-mail': + return Boolean(reference) + && candidate.origin === reference.origin + && candidate.pathname.startsWith('/m/'); + case 'vps-panel': + return Boolean(reference) && candidate.origin === reference.origin; + default: + return false; + } +} + +async function rememberSourceLastUrl(source, url) { + if (!source || !url) return; + const state = await getState(); + const sourceLastUrls = { ...(state.sourceLastUrls || {}) }; + sourceLastUrls[source] = url; + await setState({ sourceLastUrls }); +} + +async function closeConflictingTabsForSource(source, currentUrl, options = {}) { + const { excludeTabIds = [] } = options; + const excluded = new Set(excludeTabIds.filter(id => Number.isInteger(id))); + const state = await getState(); + const lastUrl = state.sourceLastUrls?.[source]; + const referenceUrls = [currentUrl, lastUrl].filter(Boolean); + + if (!referenceUrls.length) return; + + const tabs = await chrome.tabs.query({}); + const matchedIds = tabs + .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id)) + .filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl))) + .map(tab => tab.id); + + if (!matchedIds.length) return; + + await chrome.tabs.remove(matchedIds).catch(() => {}); + + const registry = await getTabRegistry(); + if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) { + registry[source] = null; + await setState({ tabRegistry: registry }); + } + + await addLog(`已关闭 ${matchedIds.length} 个旧的${getSourceLabel(source)}标签页。`, 'info'); +} + // ============================================================ // Command Queue (for content scripts not yet ready) // ============================================================ @@ -226,6 +304,7 @@ async function reuseOrCreateTab(source, url, options = {}) { const alive = await isTabAlive(source); if (alive) { const tabId = await getTabId(source); + await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] }); const currentTab = await chrome.tabs.get(tabId); const sameUrl = currentTab.url === url; const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl; @@ -273,6 +352,7 @@ async function reuseOrCreateTab(source, url, options = {}) { await new Promise(r => setTimeout(r, 500)); } + await rememberSourceLastUrl(source, url); return tabId; } @@ -317,10 +397,12 @@ async function reuseOrCreateTab(source, url, options = {}) { // Wait a bit for content script to inject and send READY await new Promise(r => setTimeout(r, 500)); + await rememberSourceLastUrl(source, url); return tabId; } // Create new tab + await closeConflictingTabsForSource(source, url); const tab = await chrome.tabs.create({ url, active: true }); console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`); @@ -352,6 +434,7 @@ async function reuseOrCreateTab(source, url, options = {}) { }); } + await rememberSourceLastUrl(source, url); return tab.id; } @@ -1810,9 +1893,11 @@ async function executeStep9(state) { const alive = tabId && await isTabAlive('vps-panel'); if (!alive) { + await closeConflictingTabsForSource('vps-panel', state.vpsUrl); // Create new tab const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true }); tabId = tab.id; + await rememberSourceLastUrl('vps-panel', state.vpsUrl); await new Promise(resolve => { const listener = (tid, info) => { if (tid === tabId && info.status === 'complete') { @@ -1823,7 +1908,9 @@ async function executeStep9(state) { chrome.tabs.onUpdated.addListener(listener); }); } else { + await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] }); await chrome.tabs.update(tabId, { active: true }); + await rememberSourceLastUrl('vps-panel', state.vpsUrl); } // Inject scripts directly and wait for them to be ready