feat(bridge): deep-inspect HTTP endpoint + recursive DOM inspector #task-264
This commit is contained in:
@@ -411,6 +411,10 @@ let observerHttpServer = null;
|
||||
const pendingResponses = new Map();
|
||||
// Click trigger: extension sets this, renderer polls and clicks button
|
||||
let clickTrigger = null;
|
||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||
let deepInspectRequested = false;
|
||||
let deepInspectResult = null;
|
||||
let deepInspectWaiters = [];
|
||||
/** Derive a deterministic port from project name (range 10000-60000) */
|
||||
function getDeterministicPort(name) {
|
||||
let hash = 0;
|
||||
@@ -507,6 +511,65 @@ function startObserverHttpBridge() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// GET /deep-inspect — trigger deep DOM inspection from renderer
|
||||
if (req.method === 'GET' && url.pathname === '/deep-inspect') {
|
||||
deepInspectRequested = true;
|
||||
logToFile('[HTTP] deep-inspect triggered — waiting for renderer result...');
|
||||
// Wait up to 10s for renderer to POST result
|
||||
const timeout = setTimeout(() => {
|
||||
deepInspectWaiters = deepInspectWaiters.filter(w => w !== waiter);
|
||||
if (deepInspectResult) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(deepInspectResult));
|
||||
}
|
||||
else {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'timeout', message: 'Renderer did not respond in 10s. Is the v3 script loaded?' }));
|
||||
}
|
||||
}, 10000);
|
||||
const waiter = (data) => {
|
||||
clearTimeout(timeout);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
};
|
||||
deepInspectWaiters.push(waiter);
|
||||
return;
|
||||
}
|
||||
// GET /deep-inspect-trigger — renderer polls this
|
||||
if (req.method === 'GET' && url.pathname === '/deep-inspect-trigger') {
|
||||
const requested = deepInspectRequested;
|
||||
deepInspectRequested = false;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ inspect: requested }));
|
||||
return;
|
||||
}
|
||||
// POST /deep-inspect-result — renderer posts inspection results here
|
||||
if (req.method === 'POST' && url.pathname === '/deep-inspect-result') {
|
||||
let body = '';
|
||||
req.on('data', (c) => body += c);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
deepInspectResult = data;
|
||||
logToFile(`[HTTP] deep-inspect result received (${body.length} bytes)`);
|
||||
// Write to file for reference
|
||||
const inspectFile = path.join(bridgePath, 'deep-inspect-result.json');
|
||||
fs.writeFileSync(inspectFile, JSON.stringify(data, null, 2));
|
||||
// Notify waiters
|
||||
const waiters = [...deepInspectWaiters];
|
||||
deepInspectWaiters = [];
|
||||
waiters.forEach(w => w(data));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[HTTP] deep-inspect-result parse error: ${e.message}`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// GET /ping — health check
|
||||
if (url.pathname === '/ping') {
|
||||
res.writeHead(200);
|
||||
@@ -642,55 +705,133 @@ function generateApprovalObserverScript(_port) {
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
// ── DOM Structure Dump (one-time on startup) ──
|
||||
function dumpDOMStructure(){
|
||||
if(_domDumped)return;
|
||||
_domDumped=true;
|
||||
try{
|
||||
// Count iframes
|
||||
var iframes=document.querySelectorAll('iframe');
|
||||
log('DOM-DUMP: '+iframes.length+' iframes found');
|
||||
for(var i=0;i<iframes.length;i++){
|
||||
var f=iframes[i];
|
||||
var fInfo=' iframe#'+i+' class="'+f.className.substring(0,50)+'" src="'+(f.src||'').substring(0,80)+'"';
|
||||
try{
|
||||
var idoc=f.contentDocument;
|
||||
if(idoc){
|
||||
var btns=idoc.querySelectorAll('button');
|
||||
fInfo+=' → ACCESSIBLE ('+btns.length+' buttons)';
|
||||
// Dump first 5 button texts
|
||||
for(var b=0;b<Math.min(5,btns.length);b++){
|
||||
log(' btn['+b+']: "'+((btns[b].textContent||'').trim()).substring(0,40)+'"');
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
fInfo+=' → BLOCKED ('+e.message.substring(0,40)+')';
|
||||
// ── Deep DOM Inspector (recursive, POSTs results to bridge) ──
|
||||
function runDeepInspect(){
|
||||
var result={timestamp:new Date().toISOString(),windowURL:window.location.href,windowOrigin:window.location.origin,windowProtocol:window.location.protocol,framesCount:window.frames.length,nodes:[]};
|
||||
log('DEEP-INSPECT: starting recursive DOM analysis...');
|
||||
|
||||
function inspectDoc(doc,depth,label){
|
||||
var node={label:label,depth:depth,accessible:true,url:'',buttons:[],roleBtns:[],iframes:[],webviews:[],shadowDOMs:0,totalElements:0};
|
||||
if(!doc){node.accessible=false;node.error='null document';result.nodes.push(node);return;}
|
||||
try{node.url=(doc.URL||doc.documentURI||'unknown').substring(0,200);}catch(e){node.url='blocked';}
|
||||
try{node.title=(doc.title||'').substring(0,100);}catch(e){}
|
||||
try{node.readyState=doc.readyState;}catch(e){}
|
||||
|
||||
// CSP
|
||||
try{
|
||||
var csp=doc.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
|
||||
if(csp.length>0){node.csp=[];for(var c=0;c<csp.length;c++){node.csp.push((csp[c].content||'').substring(0,200));}}
|
||||
}catch(e){}
|
||||
|
||||
try{
|
||||
var allEls=doc.querySelectorAll('*');
|
||||
node.totalElements=allEls.length;
|
||||
// Buttons
|
||||
var btns=doc.querySelectorAll('button');
|
||||
for(var i=0;i<btns.length;i++){
|
||||
var b=btns[i];
|
||||
var txt=(b.textContent||'').trim().substring(0,80);
|
||||
if(!txt)continue;
|
||||
var cls=(b.className||'').substring(0,60);
|
||||
var disabled=b.disabled;
|
||||
var hidden=b.hidden||false;
|
||||
try{if(!b.offsetParent&&b.style.display!=='fixed')hidden=true;}catch(e){}
|
||||
var aria=b.getAttribute('aria-label')||'';
|
||||
var ttl=b.getAttribute('title')||'';
|
||||
node.buttons.push({text:txt,class:cls,disabled:disabled,hidden:hidden,aria:aria,title:ttl});
|
||||
}
|
||||
log(fInfo);
|
||||
}
|
||||
// Count webview elements
|
||||
var webviews=document.querySelectorAll('webview');
|
||||
log('DOM-DUMP: '+webviews.length+' <webview> elements');
|
||||
for(var w=0;w<webviews.length;w++){
|
||||
var wInfo=' webview#'+w+' src="'+(webviews[w].src||'').substring(0,80)+'"';
|
||||
// role=button
|
||||
var rbs=doc.querySelectorAll('[role="button"]');
|
||||
for(var r=0;r<rbs.length;r++){
|
||||
if(rbs[r].tagName==='BUTTON')continue;
|
||||
var rtxt=(rbs[r].textContent||'').trim().substring(0,60);
|
||||
node.roleBtns.push({tag:rbs[r].tagName.toLowerCase(),text:rtxt});
|
||||
}
|
||||
// Shadow DOMs
|
||||
for(var s=0;s<allEls.length;s++){
|
||||
var sr=allEls[s].shadowRoot;
|
||||
if(sr){node.shadowDOMs++;inspectDoc(sr,depth+1,'shadow(<'+allEls[s].tagName.toLowerCase()+' class="'+(allEls[s].className||'').substring(0,30)+'">)');}
|
||||
}
|
||||
// Iframes
|
||||
var ifs=doc.querySelectorAll('iframe');
|
||||
for(var fi=0;fi<ifs.length;fi++){
|
||||
var f=ifs[fi];
|
||||
var finfo={index:fi,class:(f.className||'').substring(0,60),src:(f.src||'').substring(0,150),id:f.id||'',sandbox:f.getAttribute('sandbox')||'',allow:f.getAttribute('allow')||'',accessible:false,cwExists:false,cwFrames:0};
|
||||
try{
|
||||
var idoc=f.contentDocument||(f.contentWindow&&f.contentWindow.document);
|
||||
if(idoc){finfo.accessible=true;inspectDoc(idoc,depth+1,'iframe#'+fi+'('+finfo.class.substring(0,30)+')');
|
||||
}else{finfo.error='contentDocument=null';}
|
||||
}catch(e){finfo.error=e.message.substring(0,80);}
|
||||
try{var cw=f.contentWindow;if(cw){finfo.cwExists=true;finfo.cwFrames=cw.length;try{finfo.cwLocation=cw.location.href;}catch(e2){finfo.cwLocation='blocked: '+e2.message.substring(0,40);}}}
|
||||
catch(e){}
|
||||
node.iframes.push(finfo);
|
||||
}
|
||||
// Webviews
|
||||
var wvs=doc.querySelectorAll('webview');
|
||||
for(var wi=0;wi<wvs.length;wi++){
|
||||
var wv=wvs[wi];
|
||||
var winfo={index:wi,src:(wv.src||'').substring(0,150),class:(wv.className||'').substring(0,60),partition:wv.getAttribute('partition')||'',preload:wv.getAttribute('preload')||'',nodeintegration:wv.getAttribute('nodeintegration')||'',webpreferences:wv.getAttribute('webpreferences')||'',hasExecJS:typeof wv.executeJavaScript==='function',contentDocAccessible:false};
|
||||
try{var wdoc=wv.contentDocument;if(wdoc){winfo.contentDocAccessible=true;inspectDoc(wdoc,depth+1,'webview#'+wi+'.contentDocument');}}catch(e){winfo.contentDocError=e.message.substring(0,60);}
|
||||
node.webviews.push(winfo);
|
||||
}
|
||||
}catch(e){node.error=e.message;}
|
||||
result.nodes.push(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
inspectDoc(document,0,'MainDocument');
|
||||
|
||||
// Webview executeJavaScript probe (async)
|
||||
var webviews=document.querySelectorAll('webview');
|
||||
var probesPending=webviews.length;
|
||||
result.webviewProbes=[];
|
||||
if(probesPending===0)postResults();
|
||||
for(var pw=0;pw<webviews.length;pw++){
|
||||
(function(wv,idx){
|
||||
if(typeof wv.executeJavaScript!=='function'){result.webviewProbes.push({index:idx,error:'executeJavaScript not available'});probesPending--;if(probesPending<=0)postResults();return;}
|
||||
try{
|
||||
var wdoc=webviews[w].contentDocument;
|
||||
if(wdoc)wInfo+=' → ACCESSIBLE';
|
||||
else wInfo+=' → null contentDocument';
|
||||
}catch(e){wInfo+=' → BLOCKED ('+e.message.substring(0,40)+')';}
|
||||
log(wInfo);
|
||||
wv.executeJavaScript('(function(){var btns=document.querySelectorAll("button");var allEls=document.querySelectorAll("*");var ifs=document.querySelectorAll("iframe");var wvs=document.querySelectorAll("webview");var btnArr=[];for(var i=0;i<btns.length;i++){var b=btns[i];var txt=(b.textContent||"").trim();var cls=(b.className||"").substring(0,50);var dis=b.disabled;var hid=b.hidden||!b.offsetParent;btnArr.push({text:txt.substring(0,60),class:cls,disabled:dis,hidden:hid,aria:b.getAttribute("aria-label")||"",title:b.getAttribute("title")||""});}var rbs=document.querySelectorAll("[role=button]");var rbArr=[];for(var j=0;j<rbs.length;j++){if(rbs[j].tagName!=="BUTTON")rbArr.push({tag:rbs[j].tagName.toLowerCase(),text:(rbs[j].textContent||"").trim().substring(0,40)});}var sc=0;for(var k=0;k<allEls.length;k++){if(allEls[k].shadowRoot)sc++;}return JSON.stringify({url:document.URL,title:document.title,totalElements:allEls.length,buttons:btnArr,roleBtns:rbArr,iframes:ifs.length,webviews:wvs.length,shadowDOMs:sc});})()')
|
||||
.then(function(r){
|
||||
try{var d=JSON.parse(r);result.webviewProbes.push({index:idx,success:true,data:d});log('DEEP-INSPECT: webview#'+idx+' probe OK: '+d.buttons.length+' buttons, '+d.totalElements+' elements');}catch(e){result.webviewProbes.push({index:idx,parseError:e.message,raw:r});}
|
||||
probesPending--;if(probesPending<=0)postResults();
|
||||
})
|
||||
.catch(function(e){
|
||||
result.webviewProbes.push({index:idx,execError:e.message});
|
||||
log('DEEP-INSPECT: webview#'+idx+' execJS error: '+e.message);
|
||||
probesPending--;if(probesPending<=0)postResults();
|
||||
});
|
||||
}catch(e){
|
||||
result.webviewProbes.push({index:idx,callError:e.message});
|
||||
probesPending--;if(probesPending<=0)postResults();
|
||||
}
|
||||
})(webviews[pw],pw);
|
||||
}
|
||||
|
||||
function postResults(){
|
||||
var summary='nodes='+result.nodes.length;
|
||||
var totalBtns=0;for(var n=0;n<result.nodes.length;n++)totalBtns+=result.nodes[n].buttons.length;
|
||||
summary+=' totalButtons='+totalBtns+' webviewProbes='+result.webviewProbes.length;
|
||||
log('DEEP-INSPECT complete: '+summary);
|
||||
// Also log buttons from each node
|
||||
for(var n2=0;n2<result.nodes.length;n2++){
|
||||
var nd=result.nodes[n2];
|
||||
if(nd.buttons.length>0){
|
||||
log(' '+nd.label+': '+nd.buttons.length+' buttons');
|
||||
for(var bi=0;bi<Math.min(15,nd.buttons.length);bi++){
|
||||
log(' ['+bi+'] "'+nd.buttons[bi].text+'"'+(nd.buttons[bi].disabled?' DISABLED':'')+(nd.buttons[bi].hidden?' HIDDEN':''));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Main doc buttons
|
||||
var mainBtns=document.querySelectorAll('button');
|
||||
log('DOM-DUMP: '+mainBtns.length+' buttons in main document');
|
||||
for(var m=0;m<Math.min(10,mainBtns.length);m++){
|
||||
log(' main-btn['+m+']: "'+((mainBtns[m].textContent||'').trim()).substring(0,50)+'"');
|
||||
}
|
||||
// Report to bridge
|
||||
fetch(BASE+'/ping?dom_dump='+iframes.length+'f_'+webviews.length+'wv_'+mainBtns.length+'btn').catch(function(){});
|
||||
}catch(e){log('DOM-DUMP error: '+e.message);}
|
||||
// POST to bridge
|
||||
fetch(BASE+'/deep-inspect-result',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(result)})
|
||||
.then(function(){log('DEEP-INSPECT results posted to bridge');})
|
||||
.catch(function(e){log('DEEP-INSPECT post error: '+e.message);});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-dump on startup (3s delay)
|
||||
function dumpDOMStructure(){runDeepInspect();}
|
||||
|
||||
// ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ──
|
||||
var HARDCODED_PORT=${_port};
|
||||
|
||||
@@ -975,6 +1116,14 @@ function generateApprovalObserverScript(_port) {
|
||||
// FALLBACK: periodic scan every 3s for any missed mutations
|
||||
setInterval(scheduleScan,3000);
|
||||
|
||||
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
|
||||
setInterval(function(){
|
||||
if(!_ready||!BASE)return;
|
||||
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||||
if(d.inspect){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();}
|
||||
}).catch(function(){});
|
||||
},2000);
|
||||
|
||||
// ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ──
|
||||
// Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks
|
||||
// v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs
|
||||
|
||||
Reference in New Issue
Block a user