// 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)); 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());