feat: 添加源地址管理功能,确保在打开新标签前清理旧标签

This commit is contained in:
QLHazyCoder
2026-04-08 11:01:39 +08:00
parent cdf46cde55
commit 9d4b86e7e0
2 changed files with 89 additions and 0 deletions
+2
View File
@@ -297,6 +297,7 @@ Step 3 使用的注册邮箱。
- localhost 回调地址
- 账号记录
- tab 注册信息
- 各来源最近一次打开的地址(用于打开新地址前清理旧标签)
- 自定义设置
- Auto 当前阶段、当前轮次、暂停信息
@@ -305,6 +306,7 @@ Step 3 使用的注册邮箱。
- 浏览器会话级存储
- 扩展运行期间可在多个步骤之间共享
- 代码里已启用 `storage.session` 对 content script 的访问
- 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留
## 项目结构
+87
View File
@@ -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