feat(bridge): deep-inspect HTTP endpoint + recursive DOM inspector #task-264

This commit is contained in:
2026-03-09 18:24:41 +09:00
parent dddbd2b96f
commit a07d9d3803
5 changed files with 756 additions and 89 deletions

View File

@@ -385,6 +385,11 @@ const pendingResponses = new Map<string, { approved: boolean } | null>();
// Click trigger: extension sets this, renderer polls and clicks button
let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null;
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
let deepInspectRequested = false;
let deepInspectResult: any = null;
let deepInspectWaiters: Array<(data: any) => void> = [];
/** Derive a deterministic port from project name (range 10000-60000) */
function getDeterministicPort(name: string): number {
let hash = 0;
@@ -475,6 +480,65 @@ function startObserverHttpBridge(): Promise<number> {
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: any) => {
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: string) => 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: any) {
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); res.end('pong');
@@ -614,55 +678,133 @@ function generateApprovalObserverScript(_port: number): string {
}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};
@@ -947,6 +1089,14 @@ function generateApprovalObserverScript(_port: number): string {
// 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