Files
GenericAgent/assets/tmwd_cdp_bridge/background.js
Liang Jiaqing 8e829730a9 smart truncation, iframe preservation, streamlit filter
- simphtml: truncate_biggest replaces naive string cut, targets fattest content block via 70% drill-down
- simphtml: iframe children preserved through div proxy (bypass BS4 limitation)
- background/content.js: filter streamlit tabs and skip content script injection
- ga.py: file_content tag regex allows attributes; maxchars 38k->35k
2026-04-06 12:19:20 +08:00

355 lines
15 KiB
JavaScript

// background.js - Cookie + CDP Bridge
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'] }
}]
});
});
async function handleExtMessage(msg, sender) {
if (msg.cmd === 'cookies') return await handleCookies(msg, sender);
if (msg.cmd === 'cdp') return await handleCDP(msg, sender);
if (msg.cmd === 'batch') return await handleBatch(msg, sender);
if (msg.cmd === 'tabs') {
try {
if (msg.method === 'switch') {
const tab = await chrome.tabs.update(msg.tabId, { active: true });
await chrome.windows.update(tab.windowId, { focused: true });
return { ok: true };
} else {
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 }));
return { ok: true, data };
}
} catch (e) { return { ok: false, error: e.message }; }
}
return { ok: false, error: 'Unknown cmd: ' + msg.cmd };
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
handleExtMessage(msg, sender).then(sendResponse);
return true;
});
async function handleCookies(msg, sender) {
try {
let url = msg.url || sender.tab?.url;
if (!url && msg.tabId) {
const tab = await chrome.tabs.get(msg.tabId);
url = tab.url;
}
const origin = url.match(/^https?:\/\/[^\/]+/)[0];
const all = await chrome.cookies.getAll({ url });
const part = await chrome.cookies.getAll({ url, partitionKey: { topLevelSite: origin } }).catch(() => []);
const merged = [...all];
for (const c of part) {
if (!merged.some(x => x.name === c.name && x.domain === c.domain)) merged.push(c);
}
return { ok: true, data: merged };
} catch (e) {
return { ok: false, error: e.message };
}
}
async function handleBatch(msg, sender) {
const R = [];
let attached = null;
const resolve$N = (params) => JSON.parse(JSON.stringify(params || {}).replace(/"\$(\d+)\.([^"]+)"/g,
(_, i, path) => { let v = R[+i]; for (const k of path.split('.')) v = v[k]; return JSON.stringify(v); }));
try {
for (const c of msg.commands) {
if (c.tabId === undefined && msg.tabId !== undefined) c.tabId = msg.tabId;
if (c.cmd === 'cookies') {
R.push(await handleCookies(c, sender));
} else if (c.cmd === 'tabs') {
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;
if (attached !== tabId) {
if (attached) { await chrome.debugger.detach({ tabId: attached }); attached = null; }
await chrome.debugger.attach({ tabId }, '1.3');
attached = tabId;
}
R.push(await chrome.debugger.sendCommand({ tabId }, c.method, resolve$N(c.params)));
} else {
R.push({ ok: false, error: 'unknown cmd: ' + c.cmd });
}
}
if (attached) await chrome.debugger.detach({ tabId: attached });
return { ok: true, results: R };
} catch (e) {
if (attached) try { await chrome.debugger.detach({ tabId: attached }); } catch (_) {}
return { ok: false, error: e.message, results: R };
}
}
async function handleCDP(msg, sender) {
const tabId = msg.tabId || sender.tab?.id;
if (!tabId) return { ok: false, error: 'no tabId' };
try {
await chrome.debugger.attach({ tabId }, '1.3');
const result = await chrome.debugger.sendCommand({ tabId }, msg.method, msg.params || {});
await chrome.debugger.detach({ tabId });
return { ok: true, data: result };
} catch (e) {
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/CDP script builder core ---
function buildExecScript(code, errorHandler) {
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) {
${errorHandler}
}
})()`;
}
function buildPageScript(code) {
return buildExecScript(code, `
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') };
`);
}
function buildCdpScript(code) {
return buildExecScript(code, `
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
}
function scheduleKeepalive() {
// Keep SW alive while WS is connected (~25s, under 30s SW timeout)
chrome.alarms.create('tmwd-ws-keepalive', { delayInMinutes: 0.4 }); // ~24s
}
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-keepalive') {
// Keepalive: ping to keep SW alive + detect dead connections
if (ws && ws.readyState === WebSocket.OPEN) {
try { ws.send('{"type":"ping"}'); } catch (_) {}
scheduleKeepalive();
} else {
// Connection lost, switch to probe mode
ws = null;
scheduleProbe();
}
}
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
}
}
});
async function handleWsExec(data) {
const tabId = data.tabId;
console.log('[TMWD-WS] Exec request', data.id, 'on tab', tabId);
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;
if (res === null || res === undefined) {
console.log('[TMWD-WS] executeScript returned null/undefined, treating as CSP issue');
res = { ok: false, error: { name: 'Error', message: 'executeScript returned null (possible CSP or context issue)', stack: '' }, csp: true };
}
} 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;
}
} 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 {
console.log(res);
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 || '' } }));
}
}
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!');
scheduleKeepalive(); // Keep SW alive while connected
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) {
let code = data.code;
// If code is a JSON string representing an object, parse it
if (typeof code === 'string') {
try { const p = JSON.parse(code); if (p && typeof p === 'object') code = p; } catch (_) {}
}
if (typeof code === 'object' && code !== null && code.cmd) {
// Custom protocol message → route to handleExtMessage
if (code.tabId === undefined && data.tabId !== undefined) code.tabId = data.tabId;
const res = await handleExtMessage(code, {});
ws.send(JSON.stringify({ type: res.ok ? 'result' : 'error', id: data.id, result: res.data ?? res.results ?? res, error: res.error }));
} else if (typeof code === 'string') {
// Plain JS code
await handleWsExec(data);
} else if (typeof code === 'object' && code !== null) {
// Object without cmd → legacy extension message
const msg = code.tabId === undefined && data.tabId !== undefined ? { ...code, tabId: data.tabId } : code;
const res = await handleExtMessage(msg, {});
ws.send(JSON.stringify({ type: res.ok ? 'result' : 'error', id: data.id, result: res.data ?? res.results ?? res, error: res.error }));
}
}
} 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) && !/streamlit/i.test(t.title));
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());