feat: CDP bridge extension integration - ext_ws session type, DOM-channel CDP/batch/cookies/tabs, autofill bringToFront fix, await support in SOP
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
// background.js - Cookie + CDP Bridge
|
||||
chrome.runtime.onInstalled.addListener(() => console.log('CDP Bridge installed'));
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log('CDP Bridge installed');
|
||||
// Strip CSP headers to allow eval/inline scripts
|
||||
chrome.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: [9999],
|
||||
addRules: [{
|
||||
id: 9999, priority: 1,
|
||||
action: { type: 'modifyHeaders', responseHeaders: [
|
||||
{ header: 'content-security-policy', operation: 'remove' },
|
||||
{ header: 'content-security-policy-report-only', operation: 'remove' }
|
||||
]},
|
||||
condition: { urlFilter: '*', resourceTypes: ['main_frame', 'sub_frame'] }
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.action === 'cookies') {
|
||||
@@ -22,7 +36,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
sendResponse({ ok: true });
|
||||
} else {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
||||
const data = tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }));
|
||||
sendResponse({ ok: true, data });
|
||||
}
|
||||
@@ -58,7 +72,7 @@ async function handleBatch(msg, sender) {
|
||||
if (c.cmd === 'cookies') {
|
||||
R.push(await handleCookies(c, sender));
|
||||
} else if (c.cmd === 'tabs') {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
||||
R.push({ ok: true, data: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })) });
|
||||
} else if (c.cmd === 'cdp') {
|
||||
const tabId = c.tabId || msg.tabId || sender.tab?.id;
|
||||
@@ -92,4 +106,219 @@ async function handleCDP(msg, sender) {
|
||||
try { await chrome.debugger.detach({ tabId }); } catch (_) {}
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter out chrome:// and other internal tabs that can't be scripted
|
||||
const isScriptable = url => url && /^https?:/.test(url);
|
||||
|
||||
// --- Shared page-script builder (used by both executeScript and CDP fallback) ---
|
||||
function buildPageScript(code) {
|
||||
return `(async () => {
|
||||
function smartProcessResult(result) {
|
||||
if (result === null || result === undefined || typeof result !== 'object') return result;
|
||||
if (typeof jQuery !== 'undefined' && result instanceof jQuery) {
|
||||
const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements;
|
||||
}
|
||||
if (result instanceof NodeList || result instanceof HTMLCollection) {
|
||||
const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements;
|
||||
}
|
||||
if (result.nodeType === 1) return result.outerHTML;
|
||||
if (!Array.isArray(result) && typeof result === 'object' && 'length' in result && typeof result.length === 'number') {
|
||||
const firstElement = result[0];
|
||||
if (firstElement && firstElement.nodeType === 1) {
|
||||
const elements = []; const length = Math.min(result.length, 100);
|
||||
for (let i = 0; i < length; i++) { const elem = result[i]; if (elem && elem.nodeType === 1) elements.push(elem.outerHTML); } return elements;
|
||||
}
|
||||
}
|
||||
try { return JSON.parse(JSON.stringify(result, function(key, value) { if (typeof value === 'object' && value !== null) { if (value.nodeType === 1) return value.outerHTML; if (value === window || value === document) return '[Object]'; } return value; })); } catch (e) { return '[无法序列化: ' + e.message + ']'; }
|
||||
}
|
||||
try {
|
||||
const jsCode = ${JSON.stringify(code)}.trim();
|
||||
const lines = jsCode.split(/\\r?\\n/).filter(l => l.trim());
|
||||
const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : '';
|
||||
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
||||
let r;
|
||||
if (lastLine.startsWith('return')) {
|
||||
r = await (new AsyncFunction(jsCode))();
|
||||
} else {
|
||||
try { r = eval(jsCode); if (r instanceof Promise) r = await r; } catch (e) {
|
||||
if (e instanceof SyntaxError && (/return/i.test(e.message) || /await/i.test(e.message))) { r = await (new AsyncFunction(jsCode))(); } else throw e;
|
||||
}
|
||||
}
|
||||
return { ok: true, data: smartProcessResult(r) };
|
||||
} catch (e) {
|
||||
const errMsg = e.message || String(e);
|
||||
return { ok: false, error: { name: e.name || 'Error', message: errMsg, stack: e.stack || '' },
|
||||
csp: errMsg.includes('Refused to evaluate') || errMsg.includes('unsafe-eval') || errMsg.includes('Content Security Policy') };
|
||||
}
|
||||
})()`;
|
||||
}
|
||||
|
||||
// --- Minimal CDP script: no smartProcessResult, returnByValue handles serialization ---
|
||||
function buildCdpScript(code) {
|
||||
return `(async () => {
|
||||
try {
|
||||
const jsCode = ${JSON.stringify(code)}.trim();
|
||||
const lines = jsCode.split(/\\r?\\n/).filter(l => l.trim());
|
||||
const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : '';
|
||||
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
||||
let r;
|
||||
if (lastLine.startsWith('return')) {
|
||||
r = await (new AsyncFunction(jsCode))();
|
||||
} else {
|
||||
try { r = eval(jsCode); if (r instanceof Promise) r = await r; } catch (e) {
|
||||
if (e instanceof SyntaxError && (/return/i.test(e.message) || /await/i.test(e.message))) { r = await (new AsyncFunction(jsCode))(); } else throw e;
|
||||
}
|
||||
}
|
||||
return { ok: true, data: r };
|
||||
} catch (e) {
|
||||
return { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' } };
|
||||
}
|
||||
})()`;
|
||||
}
|
||||
|
||||
// --- WebSocket Client for TMWebDriver ---
|
||||
let ws = null;
|
||||
const WS_URL = 'ws://127.0.0.1:18765';
|
||||
|
||||
function scheduleProbe() {
|
||||
// Use chrome.alarms to survive MV3 service worker suspension
|
||||
chrome.alarms.create('tmwd-ws-probe', { delayInMinutes: 0.083 }); // ~5s
|
||||
}
|
||||
|
||||
async function isServerAlive() {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 2000);
|
||||
await fetch('http://127.0.0.1:18765', { signal: ctrl.signal });
|
||||
return true; // Got HTTP response → port is listening
|
||||
} catch (e) {
|
||||
return false; // Network error (connection refused) or timeout → server not alive
|
||||
}
|
||||
}
|
||||
|
||||
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
||||
if (alarm.name === 'tmwd-ws-probe') {
|
||||
if (ws && ws.readyState <= 1) return; // Already connected/connecting
|
||||
if (await isServerAlive()) {
|
||||
console.log('[TMWD-WS] Server detected, connecting...');
|
||||
connectWS();
|
||||
} else {
|
||||
scheduleProbe(); // Server not up, keep probing
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function connectWS() {
|
||||
if (ws && ws.readyState <= 1) return; // CONNECTING or OPEN
|
||||
ws = null;
|
||||
console.log('[TMWD-WS] Connecting to', WS_URL);
|
||||
try {
|
||||
ws = new WebSocket(WS_URL);
|
||||
} catch (e) {
|
||||
console.error('[TMWD-WS] Constructor error:', e);
|
||||
ws = null;
|
||||
scheduleProbe();
|
||||
return;
|
||||
}
|
||||
ws.onopen = async () => {
|
||||
console.log('[TMWD-WS] Connected!');
|
||||
chrome.alarms.clear('tmwd-ws-probe');
|
||||
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ext_ready',
|
||||
tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
|
||||
}));
|
||||
console.log('[TMWD-WS] Sent ext_ready with', tabs.length, 'tabs');
|
||||
};
|
||||
ws.onmessage = async (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.id && data.code) {
|
||||
const tabId = data.tabId;
|
||||
console.log('[TMWD-WS] Exec request', data.id, 'on tab', tabId);
|
||||
// Send ACK immediately so Python side resets timeout timer
|
||||
ws.send(JSON.stringify({ type: 'ack', id: data.id }));
|
||||
if (!tabId) {
|
||||
ws.send(JSON.stringify({ type: 'error', id: data.id, error: 'No tabId provided' }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tabsBefore = new Set((await chrome.tabs.query({})).map(t => t.id));
|
||||
let res;
|
||||
try {
|
||||
const result = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: 'MAIN',
|
||||
func: async (s) => await eval(s),
|
||||
args: [buildPageScript(data.code)]
|
||||
});
|
||||
res = result[0]?.result;
|
||||
} catch (e) {
|
||||
console.log('[TMWD-WS] scripting.executeScript failed:', e.message);
|
||||
res = { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' }, csp: true };
|
||||
}
|
||||
// CDP fallback for CSP-restricted pages
|
||||
if (res && !res.ok && res.csp) {
|
||||
console.log('[TMWD-WS] CDP fallback for tab', tabId);
|
||||
const wrappedCode = buildCdpScript(data.code);
|
||||
try {
|
||||
await chrome.debugger.attach({ tabId }, '1.3');
|
||||
const cdpRes = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
||||
expression: wrappedCode, awaitPromise: true, returnByValue: true
|
||||
});
|
||||
await chrome.debugger.detach({ tabId });
|
||||
if (cdpRes.exceptionDetails) {
|
||||
const desc = cdpRes.exceptionDetails.exception?.description || 'CDP Error';
|
||||
res = { ok: false, error: { name: 'Error', message: desc, stack: desc } };
|
||||
} else {
|
||||
res = cdpRes.result.value; // Already {ok, data/error} from the wrapper
|
||||
}
|
||||
} catch (cdpErr) {
|
||||
try { await chrome.debugger.detach({ tabId }); } catch (_) {}
|
||||
res = { ok: false, error: { name: 'Error', message: 'CDP fallback failed: ' + cdpErr.message, stack: '' } };
|
||||
}
|
||||
}
|
||||
const newTabs = (await chrome.tabs.query({})).filter(t => !tabsBefore.has(t.id)).map(t => ({id: t.id, url: t.url, title: t.title}));
|
||||
if (res?.ok) {
|
||||
ws.send(JSON.stringify({ type: 'result', id: data.id, result: res.data, newTabs }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'error', id: data.id, error: res?.error || 'Unknown error', newTabs }));
|
||||
}
|
||||
} catch (e) {
|
||||
ws.send(JSON.stringify({ type: 'error', id: data.id, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' } }));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TMWD-WS] message parse error', e);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log('[TMWD-WS] Disconnected');
|
||||
ws = null;
|
||||
scheduleProbe();
|
||||
};
|
||||
ws.onerror = (e) => {
|
||||
console.error('[TMWD-WS] Error:', e);
|
||||
// onclose will fire after this, which triggers reconnect
|
||||
};
|
||||
}
|
||||
|
||||
// Initial connect + wake-up hooks
|
||||
connectWS();
|
||||
chrome.runtime.onStartup.addListener(() => connectWS());
|
||||
chrome.runtime.onInstalled.addListener(() => connectWS());
|
||||
|
||||
// Sync tab list on changes
|
||||
async function sendTabsUpdate() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
|
||||
ws.send(JSON.stringify({
|
||||
type: 'tabs_update',
|
||||
tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
|
||||
}));
|
||||
}
|
||||
chrome.tabs.onUpdated.addListener((_, changeInfo) => {
|
||||
if (changeInfo.status === 'complete') sendTabsUpdate();
|
||||
});
|
||||
chrome.tabs.onRemoved.addListener(() => sendTabsUpdate());
|
||||
chrome.tabs.onCreated.addListener(() => sendTabsUpdate());
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
|
||||
// Remove meta CSP tags
|
||||
document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(e => e.remove());
|
||||
|
||||
// Indicator badge at bottom-right (userscript style)
|
||||
(function(){
|
||||
if(window.self!==window.top)return;
|
||||
const d=document.createElement('div');
|
||||
d.id='ljq-ind';
|
||||
d.innerText='ljq_driver: 已连接';
|
||||
d.style.cssText='position:fixed;bottom:8px;right:8px;background:#4CAF50;color:white;padding:4px 7px;border-radius:4px;font-size:11px;font-weight:bold;z-index:99999;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.2);opacity:0.5;';
|
||||
d.addEventListener('click',()=>alert('会话活跃\nURL: '+location.href));
|
||||
(document.body||document.documentElement).appendChild(d);
|
||||
})();
|
||||
|
||||
new MutationObserver(muts => {
|
||||
for (const m of muts) for (const n of m.addedNodes) {
|
||||
if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
"cookies",
|
||||
"tabs",
|
||||
"activeTab",
|
||||
"debugger"
|
||||
"debugger",
|
||||
"scripting",
|
||||
"alarms",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
|
||||
Reference in New Issue
Block a user