feat: add CDP tab-group plugin handshake with silent fallback
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
(() => {
|
||||
const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST';
|
||||
const RESPONSE_TYPE = 'AB_TAB_GROUP_RESPONSE';
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
if (!data || data.type !== REQUEST_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = {
|
||||
type: REQUEST_TYPE,
|
||||
nonce: data.nonce,
|
||||
session: data.session,
|
||||
groupTitle: data.groupTitle,
|
||||
pluginId: data.pluginId,
|
||||
};
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
chrome.runtime.sendMessage(request, (response) => {
|
||||
const lastError = chrome.runtime.lastError;
|
||||
if (lastError) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: RESPONSE_TYPE,
|
||||
nonce: request.nonce,
|
||||
ok: false,
|
||||
error: lastError.message,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = response && typeof response === 'object' ? response : { ok: false };
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: RESPONSE_TYPE,
|
||||
nonce: request.nonce,
|
||||
ok: payload.ok === true,
|
||||
extensionId:
|
||||
typeof payload.extensionId === 'string' && payload.extensionId.length > 0
|
||||
? payload.extensionId
|
||||
: chrome.runtime.id,
|
||||
groupId: typeof payload.groupId === 'number' ? payload.groupId : undefined,
|
||||
error: typeof payload.error === 'string' ? payload.error : undefined,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
window.postMessage(
|
||||
{
|
||||
type: RESPONSE_TYPE,
|
||||
nonce: request.nonce,
|
||||
ok: false,
|
||||
error: errorMessage,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Agent Browser CDP Tab Grouper",
|
||||
"version": "0.1.0",
|
||||
"description": "Groups tabs by Agent Browser session when requested from CDP-driven pages.",
|
||||
"permissions": ["tabs", "tabGroups"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "service-worker.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content-script.js"],
|
||||
"run_at": "document_start",
|
||||
"match_about_blank": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST';
|
||||
const DEFAULT_GROUP_TITLE = 'Agent Browser Stealth';
|
||||
const sessionGroupCache = new Map();
|
||||
|
||||
function normalizeSession(session) {
|
||||
if (typeof session !== 'string') return 'default';
|
||||
const trimmed = session.trim();
|
||||
return trimmed.length > 0 ? trimmed.slice(0, 64) : 'default';
|
||||
}
|
||||
|
||||
function normalizeGroupTitle(title) {
|
||||
if (typeof title !== 'string') return DEFAULT_GROUP_TITLE;
|
||||
const trimmed = title.trim();
|
||||
return trimmed.length > 0 ? trimmed.slice(0, 80) : DEFAULT_GROUP_TITLE;
|
||||
}
|
||||
|
||||
function cacheKey(windowId, session) {
|
||||
return `${windowId}:${session}`;
|
||||
}
|
||||
|
||||
async function findExistingGroup(windowId, groupTitle) {
|
||||
const tabs = await chrome.tabs.query({ windowId });
|
||||
const checked = new Set();
|
||||
|
||||
for (const tab of tabs) {
|
||||
if (typeof tab.groupId !== 'number' || tab.groupId < 0 || checked.has(tab.groupId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
checked.add(tab.groupId);
|
||||
try {
|
||||
const group = await chrome.tabGroups.get(tab.groupId);
|
||||
if (group.title === groupTitle) {
|
||||
return tab.groupId;
|
||||
}
|
||||
} catch {
|
||||
// Ignore stale group references and continue.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureSessionGroup(tabId, windowId, session, groupTitle) {
|
||||
const key = cacheKey(windowId, session);
|
||||
let groupId = sessionGroupCache.get(key);
|
||||
|
||||
if (typeof groupId === 'number') {
|
||||
try {
|
||||
await chrome.tabGroups.get(groupId);
|
||||
} catch {
|
||||
groupId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof groupId !== 'number') {
|
||||
const existing = await findExistingGroup(windowId, groupTitle);
|
||||
if (typeof existing === 'number') {
|
||||
groupId = existing;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof groupId === 'number') {
|
||||
await chrome.tabs.group({ groupId, tabIds: [tabId] });
|
||||
} else {
|
||||
groupId = await chrome.tabs.group({
|
||||
tabIds: [tabId],
|
||||
createProperties: { windowId },
|
||||
});
|
||||
}
|
||||
|
||||
await chrome.tabGroups.update(groupId, {
|
||||
title: groupTitle,
|
||||
color: 'blue',
|
||||
collapsed: false,
|
||||
});
|
||||
|
||||
sessionGroupCache.set(key, groupId);
|
||||
return groupId;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || message.type !== REQUEST_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = sender.tab?.id;
|
||||
const windowId = sender.tab?.windowId;
|
||||
const nonce = typeof message.nonce === 'string' ? message.nonce : undefined;
|
||||
|
||||
if (typeof tabId !== 'number' || typeof windowId !== 'number') {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
error: 'missing-tab-context',
|
||||
extensionId: chrome.runtime.id,
|
||||
nonce,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof message.pluginId === 'string' && message.pluginId !== chrome.runtime.id) {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
error: 'plugin-id-mismatch',
|
||||
extensionId: chrome.runtime.id,
|
||||
nonce,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const session = normalizeSession(message.session);
|
||||
const groupTitle = normalizeGroupTitle(message.groupTitle);
|
||||
|
||||
ensureSessionGroup(tabId, windowId, session, groupTitle)
|
||||
.then((groupId) => {
|
||||
sendResponse({
|
||||
ok: true,
|
||||
groupId,
|
||||
extensionId: chrome.runtime.id,
|
||||
nonce,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
sendResponse({
|
||||
ok: false,
|
||||
error: errorMessage,
|
||||
extensionId: chrome.runtime.id,
|
||||
nonce,
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
Reference in New Issue
Block a user