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:
@@ -12,7 +12,7 @@ class Session:
|
|||||||
self.connect_at = time.time()
|
self.connect_at = time.time()
|
||||||
self.disconnect_at = None
|
self.disconnect_at = None
|
||||||
self.type = info.get('type', 'ws')
|
self.type = info.get('type', 'ws')
|
||||||
self.ws_client = client if self.type == 'ws' else None
|
self.ws_client = client if self.type in ('ws', 'ext_ws') else None
|
||||||
self.http_queue = client if self.type == 'http' else None
|
self.http_queue = client if self.type == 'http' else None
|
||||||
@property
|
@property
|
||||||
def url(self): return self.info.get('url', '')
|
def url(self): return self.info.get('url', '')
|
||||||
@@ -22,7 +22,7 @@ class Session:
|
|||||||
def reconnect(self, client, info):
|
def reconnect(self, client, info):
|
||||||
self.info = info
|
self.info = info
|
||||||
self.type = info.get('type', 'ws')
|
self.type = info.get('type', 'ws')
|
||||||
if self.type == 'ws':
|
if self.type in ('ws', 'ext_ws'):
|
||||||
self.ws_client = client
|
self.ws_client = client
|
||||||
self.http_queue = None
|
self.http_queue = None
|
||||||
elif self.type == 'http':
|
elif self.type == 'http':
|
||||||
@@ -79,7 +79,7 @@ class TMWebDriver:
|
|||||||
if data.get('type') == 'result':
|
if data.get('type') == 'result':
|
||||||
self.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])}
|
self.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])}
|
||||||
elif data.get('type') == 'error':
|
elif data.get('type') == 'error':
|
||||||
self.results[data.get('id')] = {'success': False, 'data': data.get('error')}
|
self.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])}
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
@app.route('/link', method=['GET','POST'])
|
@app.route('/link', method=['GET','POST'])
|
||||||
@@ -98,7 +98,7 @@ class TMWebDriver:
|
|||||||
print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' '))
|
print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' '))
|
||||||
return json.dumps({'r': result}, ensure_ascii=False)
|
return json.dumps({'r': result}, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({'error': str(e)}, ensure_ascii=False)
|
return json.dumps({'r': {'error': str(e)}}, ensure_ascii=False)
|
||||||
return 'ok'
|
return 'ok'
|
||||||
def run():
|
def run():
|
||||||
from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler
|
from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler
|
||||||
@@ -128,11 +128,24 @@ class TMWebDriver:
|
|||||||
session_info = {'url': data.get('url'), 'title': data.get('title', ''),
|
session_info = {'url': data.get('url'), 'title': data.get('title', ''),
|
||||||
'connected_at': time.time(), 'type': 'ws'}
|
'connected_at': time.time(), 'type': 'ws'}
|
||||||
driver._register_client(session_id, self, session_info)
|
driver._register_client(session_id, self, session_info)
|
||||||
|
elif data.get('type') in ['ext_ready', 'tabs_update']:
|
||||||
|
tabs = data.get('tabs', [])
|
||||||
|
current_tab_ids = {str(tab['id']) for tab in tabs}
|
||||||
|
for sid in list(driver.sessions.keys()):
|
||||||
|
sess = driver.sessions[sid]
|
||||||
|
if sess.type == 'ext_ws' and sess.ws_client == self and sid not in current_tab_ids:
|
||||||
|
sess.mark_disconnected()
|
||||||
|
for tab in tabs:
|
||||||
|
session_id = str(tab['id'])
|
||||||
|
session_info = {'url': tab.get('url'), 'title': tab.get('title', ''), 'connected_at': time.time(), 'type': 'ext_ws'}
|
||||||
|
sess = driver.sessions.get(session_id)
|
||||||
|
if sess and sess.is_active(): sess.info = session_info
|
||||||
|
else: driver._register_client(session_id, self, session_info)
|
||||||
elif data.get('type') == 'ack': driver.acks[data.get('id','')] = True
|
elif data.get('type') == 'ack': driver.acks[data.get('id','')] = True
|
||||||
elif data.get('type') == 'result':
|
elif data.get('type') == 'result':
|
||||||
driver.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])}
|
driver.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])}
|
||||||
elif data.get('type') == 'error':
|
elif data.get('type') == 'error':
|
||||||
driver.results[data.get('id')] = {'success': False, 'data': data.get('error')}
|
driver.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error handling message: {e}")
|
print(f"Error handling message: {e}")
|
||||||
if hasattr(self, 'data'): print(self.data)
|
if hasattr(self, 'data'): print(self.data)
|
||||||
@@ -190,11 +203,13 @@ class TMWebDriver:
|
|||||||
raise ValueError(f"会话ID {session_id} 未连接")
|
raise ValueError(f"会话ID {session_id} 未连接")
|
||||||
|
|
||||||
tp = session.type
|
tp = session.type
|
||||||
assert tp in ['ws', 'http'], f"Unsupported session type: {tp}"
|
assert tp in ['ws', 'http', 'ext_ws'], f"Unsupported session type: {tp}"
|
||||||
exec_id = str(uuid.uuid4())
|
exec_id = str(uuid.uuid4())
|
||||||
payload = json.dumps({'id': exec_id, 'code': code})
|
payload_dict = {'id': exec_id, 'code': code}
|
||||||
|
if tp == 'ext_ws': payload_dict['tabId'] = int(session.id)
|
||||||
|
payload = json.dumps(payload_dict)
|
||||||
|
|
||||||
if tp == 'ws': session.ws_client.send_message(payload)
|
if tp in ['ws', 'ext_ws']: session.ws_client.send_message(payload)
|
||||||
elif tp == 'http': session.http_queue.put(payload)
|
elif tp == 'http': session.http_queue.put(payload)
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -205,12 +220,12 @@ class TMWebDriver:
|
|||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
if not acked and exec_id in self.acks:
|
if not acked and exec_id in self.acks:
|
||||||
acked = True; start_time = time.time()
|
acked = True; start_time = time.time()
|
||||||
if tp == 'ws':
|
if tp in ['ws', 'ext_ws']:
|
||||||
if not session.is_active(): hasjump = True
|
if not session.is_active(): hasjump = True
|
||||||
if hasjump and session.is_active():
|
if hasjump and session.is_active():
|
||||||
return {'result': f"Session {session_id} reloaded.", "closed":1}
|
return {'result': f"Session {session_id} reloaded.", "closed":1}
|
||||||
if time.time() - start_time > timeout:
|
if time.time() - start_time > timeout:
|
||||||
if tp == 'ws':
|
if tp in ['ws', 'ext_ws']:
|
||||||
if hasjump: return {'result': f"Session {session_id} reloaded and new page is loading...", 'closed':1}
|
if hasjump: return {'result': f"Session {session_id} reloaded and new page is loading...", 'closed':1}
|
||||||
if acked: return {"result": f"No response data in {timeout}s (ACK received, script may still be running)"}
|
if acked: return {"result": f"No response data in {timeout}s (ACK received, script may still be running)"}
|
||||||
return {"result": f"No response data in {timeout}s (no ACK, script may not have been delivered)"}
|
return {"result": f"No response data in {timeout}s (no ACK, script may not have been delivered)"}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ class GeneraticAgent:
|
|||||||
finally:
|
finally:
|
||||||
if self.stop_sig:
|
if self.stop_sig:
|
||||||
print('User aborted the task.')
|
print('User aborted the task.')
|
||||||
with self.task_queue.mutex: self.task_queue.queue.clear()
|
#with self.task_queue.mutex: self.task_queue.queue.clear()
|
||||||
self.is_running = self.stop_sig = False
|
self.is_running = self.stop_sig = False
|
||||||
self.task_queue.task_done()
|
self.task_queue.task_done()
|
||||||
if self.handler is not None: self.handler.code_stop_signal.append(1)
|
if self.handler is not None: self.handler.code_stop_signal.append(1)
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
// background.js - Cookie + CDP Bridge
|
// 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) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
if (msg.action === 'cookies') {
|
if (msg.action === 'cookies') {
|
||||||
@@ -22,7 +36,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
await chrome.windows.update(tab.windowId, { focused: true });
|
await chrome.windows.update(tab.windowId, { focused: true });
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
} else {
|
} 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 }));
|
const data = tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }));
|
||||||
sendResponse({ ok: true, data });
|
sendResponse({ ok: true, data });
|
||||||
}
|
}
|
||||||
@@ -58,7 +72,7 @@ async function handleBatch(msg, sender) {
|
|||||||
if (c.cmd === 'cookies') {
|
if (c.cmd === 'cookies') {
|
||||||
R.push(await handleCookies(c, sender));
|
R.push(await handleCookies(c, sender));
|
||||||
} else if (c.cmd === 'tabs') {
|
} 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 })) });
|
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') {
|
} else if (c.cmd === 'cdp') {
|
||||||
const tabId = c.tabId || msg.tabId || sender.tab?.id;
|
const tabId = c.tabId || msg.tabId || sender.tab?.id;
|
||||||
@@ -93,3 +107,218 @@ async function handleCDP(msg, sender) {
|
|||||||
return { ok: false, error: e.message };
|
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 => {
|
new MutationObserver(muts => {
|
||||||
for (const m of muts) for (const n of m.addedNodes) {
|
for (const m of muts) for (const n of m.addedNodes) {
|
||||||
if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
|
if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
"cookies",
|
"cookies",
|
||||||
"tabs",
|
"tabs",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"debugger"
|
"debugger",
|
||||||
|
"scripting",
|
||||||
|
"alarms",
|
||||||
|
"declarativeNetRequest"
|
||||||
],
|
],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"background": {
|
"background": {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
- 非Selenium/Playwright,不需调试浏览器或新数据目录
|
- 非Selenium/Playwright,不需调试浏览器或新数据目录
|
||||||
- 支撑 `web_scan`(只读DOM) / `web_execute_js`(执行JS) 等高层工具
|
- 支撑 `web_scan`(只读DOM) / `web_execute_js`(执行JS) 等高层工具
|
||||||
|
|
||||||
## 通用限制
|
## 通用特性
|
||||||
- ⚠web_execute_js中**禁止使用await**,会报SyntaxError(非async上下文)(BBS#34验证)
|
- ✅web_execute_js**完美支持顶层await**(v0.4+),可直接`await fetch()`/`await new Promise()`等
|
||||||
- 需要异步操作时用`.then()`链式调用或回调
|
- ⚠使用await时需**显式`return`**才能拿到返回值(底层async包裹,不写return则返回null)
|
||||||
|
|
||||||
## 限制(isTrusted)
|
## 限制(isTrusted)
|
||||||
- JS dispatch的事件`isTrusted=false`,敏感操作(文件上传/部分按钮)会被浏览器拦截
|
- JS dispatch的事件`isTrusted=false`,敏感操作(文件上传/部分按钮)会被浏览器拦截
|
||||||
@@ -53,7 +53,7 @@ fetch('PDF_URL').then(r=>r.blob()).then(b=>{
|
|||||||
⚠TID密钥:首次运行自动生成到`assets/tmwd_cdp_bridge/config.js`(已gitignore),扩展通过manifest引用
|
⚠TID密钥:首次运行自动生成到`assets/tmwd_cdp_bridge/config.js`(已gitignore),扩展通过manifest引用
|
||||||
调用:MutationObserver监听addedNodes(id=TID),⚠每次必须remove旧→createElement新→设textContent JSON→appendChild
|
调用:MutationObserver监听addedNodes(id=TID),⚠每次必须remove旧→createElement新→设textContent JSON→appendChild
|
||||||
```js
|
```js
|
||||||
// TID从assets/tmwd_cdp_bridge/config.js读取,示例用'__ljq_ctrl'占位
|
// TID从assets/tmwd_cdp_bridge/config.js读取
|
||||||
const old = document.getElementById(TID);
|
const old = document.getElementById(TID);
|
||||||
if (old) old.remove();
|
if (old) old.remove();
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
@@ -128,14 +128,16 @@ document.body.appendChild(el); // 响应写回el.textContent
|
|||||||
|
|
||||||
## autofill获取与登录 (需 v0.4+ 脚本支持 await)
|
## autofill获取与登录 (需 v0.4+ 脚本支持 await)
|
||||||
检测:web_scan输出input带`data-autofilled="true"`,value显示为受保护提示(非真实值,Chrome安全保护需点击释放)
|
检测:web_scan输出input带`data-autofilled="true"`,value显示为受保护提示(非真实值,Chrome安全保护需点击释放)
|
||||||
- ⭐**一键释放与登录**:利用 v0.4 脚本的顶层 `await`,在单次 `web_execute_js` 中连贯完成:
|
- ⚠**前置条件:必须先CDP `Page.bringToFront` 切tab到前台**,Chrome仅在前台tab释放autofill保护值,后台tab物理点击无效
|
||||||
1. JS获取输入框坐标。
|
- ⭐**一键释放与登录**:利用顶层 `await`,在单次 `web_execute_js` 中连贯完成:
|
||||||
2. CDP发送 `Input.dispatchMouseEvent` (mousePressed) 物理点击释放autofill。
|
1. CDP batch发送 `Page.bringToFront` 切到前台。
|
||||||
3. `await new Promise(r => setTimeout(r, 500))` 等待释放。
|
2. JS获取输入框坐标。
|
||||||
4. 派发 `input`/`change` 事件唤醒前端框架(解禁登录按钮)。
|
3. CDP发送 `Input.dispatchMouseEvent` (mousePressed) 物理点击释放autofill。
|
||||||
5. 触发登录点击。
|
4. `await new Promise(r => setTimeout(r, 500))` 等待释放。
|
||||||
|
5. 派发 `input`/`change` 事件唤醒前端框架(解禁登录按钮)。
|
||||||
|
6. 触发登录点击。
|
||||||
- ⚠只需 `mousePressed`,无需 `mouseReleased`。点击一个字段即释放全页。
|
- ⚠只需 `mousePressed`,无需 `mouseReleased`。点击一个字段即释放全页。
|
||||||
- ⚠已淘汰旧版跨 tab 查 tabId 或 Python 轮询的繁琐流程,直接在当前页异步完成。
|
- ⚠使用await时需显式`return`返回值,否则async包裹层默认返回null。
|
||||||
|
|
||||||
## 验证码/页面视觉截图
|
## 验证码/页面视觉截图
|
||||||
- ⭐首选CDP截图:`Page.captureScreenshot`(format:'png')→返回base64,无需前台/后台tab也行,全页高清
|
- ⭐首选CDP截图:`Page.captureScreenshot`(format:'png')→返回base64,无需前台/后台tab也行,全页高清
|
||||||
|
|||||||
14
simphtml.py
14
simphtml.py
@@ -866,12 +866,14 @@ def execute_js_rich(script, driver, no_monitor=False):
|
|||||||
"environment": {"reloaded": reloaded},
|
"environment": {"reloaded": reloaded},
|
||||||
"tab_id": driver.default_session_id
|
"tab_id": driver.default_session_id
|
||||||
}
|
}
|
||||||
after = driver.get_session_dict()
|
if response.get('newTabs'): rr['environment']['newTabs'] = response['newTabs']
|
||||||
new_sids = {k: v for k, v in after.items() if k not in before_sids}
|
else:
|
||||||
if new_sids:
|
after = driver.get_session_dict()
|
||||||
newTabs = [{'id': k, 'url': v} for k, v in new_sids.items()]
|
new_sids = {k: v for k, v in after.items() if k not in before_sids}
|
||||||
rr['environment']['newTabs'] = newTabs
|
if new_sids:
|
||||||
rr['suggestion'] = "页面已刷新,以上新标签页在执行期间连接。"
|
newTabs = [{'id': k, 'url': v} for k, v in new_sids.items()]
|
||||||
|
rr['environment']['newTabs'] = newTabs
|
||||||
|
rr['suggestion'] = "页面已刷新,以上新标签页在执行期间连接。"
|
||||||
if error_msg: rr['error'] = error_msg
|
if error_msg: rr['error'] = error_msg
|
||||||
if no_monitor: return rr
|
if no_monitor: return rr
|
||||||
if not reloaded:
|
if not reloaded:
|
||||||
|
|||||||
Reference in New Issue
Block a user