refactor(extension): 모듈 분리 + Hub 통합 테스트 #task-395
- extension.ts 3,446→1,289줄 (-63%) - step-probe.ts (1,435줄): setupMonitor, processResponseFile, tryApprovalStrategies - observer-script.ts (687줄): DOM observer script - ws-client.ts (390줄): WSBridgeClient - step-utils.ts (114줄): step 파싱 유틸 - auth.py (115줄): JWT + registration code - hub.py (581줄): WSHub + per-client queue - Hub WS 연동 테스트 통과 (auth, chat, register) - VSIX v0.4.0 빌드
This commit is contained in:
698
extension/src/observer-script.ts
Normal file
698
extension/src/observer-script.ts
Normal file
@@ -0,0 +1,698 @@
|
||||
/**
|
||||
* Approval Observer Script — injected into AG's renderer process.
|
||||
*
|
||||
* This is a self-contained JavaScript string template that runs in the
|
||||
* browser context (no Node.js APIs). It scans the DOM for approval buttons,
|
||||
* reports them to the HTTP bridge, and handles trigger clicks.
|
||||
*
|
||||
* Extracted from extension.ts for maintainability.
|
||||
*/
|
||||
|
||||
export function generateApprovalObserverScript(_port: number): string {
|
||||
// Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge
|
||||
return `
|
||||
// ── Gravity Bridge v3: Approval Observer (deep DOM traversal — iframes, webviews, shadow DOMs) ──
|
||||
(function(){
|
||||
'use strict';
|
||||
var BASE='',_obs=false,_sent={},_ready=false;
|
||||
var _scanScheduled=false,_lastScanTs=0;
|
||||
var THROTTLE_MS=100;
|
||||
var CLEANUP_MS=300000;
|
||||
var _domDumped=false;
|
||||
|
||||
function log(m){console.log('[GB Observer] '+m);}
|
||||
log('v3 Script loaded — deep DOM traversal enabled');
|
||||
|
||||
// ── Deep DOM Traversal: find buttons across ALL boundaries ──
|
||||
// Searches: main document → iframes (contentDocument) → webview elements → shadow DOMs
|
||||
function deepFindButtons(patterns){
|
||||
var results=[];
|
||||
// 1. Main document buttons
|
||||
collectButtons(document,results,patterns,'main');
|
||||
// 2. Iframe traversal (try contentDocument — works if same-origin or webSecurity off)
|
||||
var iframes=document.querySelectorAll('iframe');
|
||||
for(var i=0;i<iframes.length;i++){
|
||||
try{
|
||||
var idoc=iframes[i].contentDocument||iframes[i].contentWindow.document;
|
||||
if(idoc){collectButtons(idoc,results,patterns,'iframe#'+i+'('+iframes[i].className.substring(0,30)+')');}
|
||||
}catch(e){
|
||||
// Cross-origin — can't access. Log only on first dom dump
|
||||
if(!_domDumped)log('iframe#'+i+' cross-origin: '+e.message.substring(0,60));
|
||||
}
|
||||
}
|
||||
// 3. Webview elements (Electron <webview> tag — has executeJavaScript)
|
||||
var webviews=document.querySelectorAll('webview');
|
||||
for(var w=0;w<webviews.length;w++){
|
||||
try{
|
||||
var wvDoc=webviews[w].contentDocument;
|
||||
if(wvDoc){collectButtons(wvDoc,results,patterns,'webview#'+w);}
|
||||
}catch(e){
|
||||
if(!_domDumped)log('webview#'+w+' access error: '+e.message.substring(0,60));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function collectButtons(doc,results,patterns,source){
|
||||
if(!doc||!doc.querySelectorAll)return;
|
||||
var btns=doc.querySelectorAll('button');
|
||||
for(var i=0;i<btns.length;i++){
|
||||
var b=btns[i];
|
||||
if(b.disabled||b.hidden)continue;
|
||||
try{if(!b.offsetParent&&b.style.display!=='fixed')continue;}catch(e){}
|
||||
var txt=(b.textContent||'').trim();
|
||||
if(!txt)continue;
|
||||
for(var p=0;p<patterns.length;p++){
|
||||
if(patterns[p].test(txt)){
|
||||
results.push({btn:b,text:txt,source:source});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 4. Recurse into shadow DOMs
|
||||
try{
|
||||
var allEls=doc.querySelectorAll('*');
|
||||
for(var j=0;j<allEls.length;j++){
|
||||
var sr=allEls[j].shadowRoot;
|
||||
if(sr)collectButtons(sr,results,patterns,source+'>shadow');
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
// ── 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});
|
||||
}
|
||||
// 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{
|
||||
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':''));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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};
|
||||
|
||||
function tryPingAsync(port){
|
||||
return fetch('http://127.0.0.1:'+port+'/ping?t='+Date.now(),{signal:AbortSignal.timeout(2000)})
|
||||
.then(function(r){return r.text();})
|
||||
.then(function(t){return t==='pong';})
|
||||
.catch(function(){return false;});
|
||||
}
|
||||
|
||||
function discoverPort(cb){
|
||||
log('Trying hardcoded port '+HARDCODED_PORT+'...');
|
||||
tryPingAsync(HARDCODED_PORT).then(function(ok){
|
||||
if(ok){log('Port discovered (hardcoded): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||
log('Hardcoded port failed, retrying with backoff...');
|
||||
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||
tryPingAsync(HARDCODED_PORT).then(function(ok2){
|
||||
if(ok2){clearInterval(timer);log('Port discovered (retry #'+attempts+'): '+HARDCODED_PORT);cb(HARDCODED_PORT);}
|
||||
});
|
||||
},2000);
|
||||
});
|
||||
}
|
||||
|
||||
discoverPort(function(port){
|
||||
BASE='http://127.0.0.1:'+port;
|
||||
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
|
||||
if(t==='pong'){log('Bridge connected on port '+port);_ready=true;startObserver();setTimeout(dumpDOMStructure,3000);}
|
||||
else log('Bridge ping failed: '+t);
|
||||
}).catch(function(e){log('Bridge unreachable: '+e.message);});
|
||||
});
|
||||
|
||||
// ── Button patterns to detect (order matters: first match wins per scan) ──
|
||||
// ONLY positive triggers should initiate a pending request group.
|
||||
// Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings.
|
||||
var PATS=[
|
||||
{re:/^Run/i, type:'terminal_command'},
|
||||
{re:/^Accept all$/i, type:'diff_review'},
|
||||
{re:/^Accept$/i, type:'agent_step'},
|
||||
{re:/^Allow/i, type:'permission'},
|
||||
{re:/^Approve/i, type:'agent_step'},
|
||||
{re:/^Retry$/i, type:'error_recovery'},
|
||||
];
|
||||
|
||||
// ALL actionable button patterns (for grouping siblings in same container)
|
||||
var ALL_ACTION_RE=[/^Run/i,/^Accept/i,/^Reject/i,/^Allow/i,/^Deny/i,/^Approve/i,/^Cancel$/i,/^Retry$/i,/^Dismiss$/i,/^Stop$/i,/^Decline$/i];
|
||||
|
||||
// Reject button patterns for finding the counterpart
|
||||
var REJECT_RE=[/^reject$/i,/^reject all$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/i,/^dismiss$/i];
|
||||
|
||||
// ── Stable button fingerprint (no getBoundingClientRect — scroll-safe) ──
|
||||
function btnId(b,type){
|
||||
// Use: type + button text + parent's first 40 chars of text content
|
||||
var txt=(b.textContent||'').trim();
|
||||
var parent=b.parentElement;
|
||||
var pctx=parent?(parent.textContent||'').substring(0,40).replace(/\\s+/g,' '):'';
|
||||
// Also use DOM position: nth-child among sibling buttons
|
||||
var idx=0;
|
||||
if(parent){
|
||||
var siblings=parent.querySelectorAll('button');
|
||||
for(var i=0;i<siblings.length;i++){if(siblings[i]===b){idx=i;break;}}
|
||||
}
|
||||
return type+'|'+txt+'|'+idx+'|'+pctx.substring(0,20);
|
||||
}
|
||||
|
||||
// ── Context extraction — walk up DOM to find command/code description ──
|
||||
function extractContext(b){
|
||||
// Strategy 1: Look for code/pre/terminal blocks near the button
|
||||
var container=b.closest('[class*="step"]')
|
||||
||b.closest('[class*="action"]')
|
||||
||b.closest('[class*="tool"]')
|
||||
||b.closest('[class*="cascade"]')
|
||||
||b.closest('[class*="message"]');
|
||||
if(!container)container=b.parentElement;
|
||||
if(!container)return '';
|
||||
|
||||
// Look for code blocks
|
||||
var codeEl=container.querySelector('pre,code,[class*="command"],[class*="terminal"],[class*="code-block"]');
|
||||
if(codeEl){
|
||||
var codeText=(codeEl.textContent||'').trim();
|
||||
if(codeText.length>0)return codeText.substring(0,500);
|
||||
}
|
||||
|
||||
// Strategy 2: Get surrounding text (exclude button text itself)
|
||||
var full=(container.textContent||'');
|
||||
var btnText=(b.textContent||'');
|
||||
var desc=full.replace(btnText,'').trim();
|
||||
// Trim to reasonable length
|
||||
return desc.substring(0,500);
|
||||
}
|
||||
|
||||
// ── Find common container of related buttons ──
|
||||
function findButtonContainer(btn){
|
||||
return btn.closest('[class*="step"]')
|
||||
||btn.closest('[class*="action"]')
|
||||
||btn.closest('[class*="tool"]')
|
||||
||btn.closest('[class*="cascade"]')
|
||||
||btn.closest('[class*="message"]')
|
||||
||btn.closest('[class*="dialog"]')
|
||||
||btn.closest('[class*="notification"]')
|
||||
||btn.parentElement;
|
||||
}
|
||||
|
||||
// ── Collect all actionable sibling buttons from a container ──
|
||||
function collectSiblingButtons(container,triggerBtn){
|
||||
if(!container)return [];
|
||||
var siblings=container.querySelectorAll('button');
|
||||
var result=[];
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var sb=siblings[i];
|
||||
if(sb.disabled||sb.hidden)continue;
|
||||
try{if(!sb.offsetParent&&sb.style.display!=='fixed')continue;}catch(e){}
|
||||
var stxt=(sb.textContent||'').trim();
|
||||
stxt=stxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
||||
if(!stxt)continue;
|
||||
// Check if this button matches any actionable pattern
|
||||
var isAction=false;
|
||||
for(var a=0;a<ALL_ACTION_RE.length;a++){
|
||||
if(ALL_ACTION_RE[a].test(stxt)){isAction=true;break;}
|
||||
}
|
||||
if(!isAction)continue;
|
||||
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Find the React app container (Antigravity's main UI root) ──
|
||||
function findPanel(){
|
||||
// Priority order of panel selectors (most specific first)
|
||||
var selectors=[
|
||||
'.antigravity-agent-side-panel',
|
||||
'#jetski-agent-panel',
|
||||
'.react-app-container',
|
||||
'[class*="agent-panel"]',
|
||||
'[class*="agentPanel"]',
|
||||
];
|
||||
for(var i=0;i<selectors.length;i++){
|
||||
var el=document.querySelector(selectors[i]);
|
||||
if(el)return el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Core scan — finds actionable buttons and reports to bridge ──
|
||||
// Groups related buttons from same container into a single pending
|
||||
function scan(){
|
||||
if(!_ready)return;
|
||||
var now=Date.now();
|
||||
|
||||
var panel=findPanel();
|
||||
// Expand search: panel-scoped first, then full body for review bars
|
||||
var searchRoots=[];
|
||||
if(panel)searchRoots.push(panel);
|
||||
// Always also scan body for diff review bar (Accept all/Reject all)
|
||||
// which lives outside the agent panel in the editor notification area
|
||||
if(document.body)searchRoots.push(document.body);
|
||||
if(!searchRoots.length)return;
|
||||
|
||||
var seen={}; // dedupe buttons across search roots
|
||||
for(var r=0;r<searchRoots.length;r++){
|
||||
var allBtns=searchRoots[r].querySelectorAll('button');
|
||||
if(!allBtns.length)continue;
|
||||
|
||||
for(var j=0;j<allBtns.length;j++){
|
||||
var b=allBtns[j];
|
||||
if(b.disabled||b.hidden)continue;
|
||||
// Check visibility (offsetParent null = hidden via CSS)
|
||||
if(!b.offsetParent&&b.style.display!=='fixed')continue;
|
||||
|
||||
var txt=(b.textContent||'').trim();
|
||||
if(!txt)continue;
|
||||
// Strip keyboard shortcut suffixes (e.g. "RunAlt+↵" → "Run")
|
||||
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
||||
if(!txt)continue;
|
||||
|
||||
// Match against patterns
|
||||
var matchedType=null;
|
||||
for(var p=0;p<PATS.length;p++){
|
||||
if(PATS[p].re.test(txt)){matchedType=PATS[p].type;break;}
|
||||
}
|
||||
if(!matchedType)continue;
|
||||
|
||||
// Generate stable ID for the GROUP (use container-based key)
|
||||
var container=findButtonContainer(b);
|
||||
var groupKey=matchedType+'|group|'+(container?(container.textContent||'').substring(0,40).replace(/\\s+/g,' '):'none');
|
||||
if(_sent[groupKey])continue;
|
||||
|
||||
// Collect ALL related buttons from the same container
|
||||
var siblings=collectSiblingButtons(container,b);
|
||||
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
|
||||
|
||||
// Build buttons array for multi-choice support
|
||||
var buttonsArr=[];
|
||||
var btnRefs=[];
|
||||
var bidList=[];
|
||||
for(var si=0;si<siblings.length;si++){
|
||||
var sb=siblings[si];
|
||||
var sbid=btnId(sb.btn,matchedType);
|
||||
buttonsArr.push({text:sb.text,index:si,is_primary:sb.isPrimary});
|
||||
btnRefs.push(sb.btn);
|
||||
bidList.push(sbid);
|
||||
}
|
||||
|
||||
// Extract context from trigger button
|
||||
var desc=extractContext(b);
|
||||
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
|
||||
|
||||
// Mark entire group as sent
|
||||
_sent[groupKey]={rid:rid,ts:now};
|
||||
for(var mk=0;mk<bidList.length;mk++){_sent[bidList[mk]]={rid:rid,ts:now};}
|
||||
|
||||
log('DETECTED GROUP '+matchedType+': '+buttonsArr.map(function(x){return '"'+x.text+'"';}).join(', ')+' → pending to bridge');
|
||||
|
||||
// Send to bridge (closure to capture refs)
|
||||
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
|
||||
var payload={
|
||||
request_id:rid2,
|
||||
command:txt2,
|
||||
description:desc2,
|
||||
step_type:type2,
|
||||
buttons:buttonsArr2
|
||||
};
|
||||
fetch(BASE+'/pending',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify(payload)
|
||||
}).then(function(r){return r.json();}).then(function(d){
|
||||
log('Pending created: '+d.request_id+' for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
|
||||
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
|
||||
}).catch(function(e){
|
||||
log('POST error: '+e.message);
|
||||
delete _sent[groupKey2];
|
||||
for(var di=0;di<bidList2.length;di++){delete _sent[bidList2[di]];}
|
||||
});
|
||||
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
|
||||
|
||||
// Process ONE button GROUP per scan cycle (avoid flooding)
|
||||
return;
|
||||
}
|
||||
} // end searchRoots loop
|
||||
}
|
||||
|
||||
// ── Poll for Discord response (multi-button group aware) ──
|
||||
function pollResponseGroup(rid,btnRefs,bidList,groupKey){
|
||||
var polls=0;
|
||||
var maxPolls=200; // 5 minutes at 1500ms interval
|
||||
var timer=setInterval(function(){
|
||||
polls++;
|
||||
// Check if ANY button in the group is still in DOM
|
||||
var anyAlive=false;
|
||||
for(var ai=0;ai<btnRefs.length;ai++){
|
||||
if(document.body.contains(btnRefs[ai])){anyAlive=true;break;}
|
||||
}
|
||||
if(!anyAlive){
|
||||
log('All buttons removed from DOM — stopping poll for '+rid);
|
||||
clearInterval(timer);
|
||||
delete _sent[groupKey];
|
||||
for(var ci=0;ci<bidList.length;ci++){delete _sent[bidList[ci]];}
|
||||
return;
|
||||
}
|
||||
if(polls>maxPolls){
|
||||
log('Poll timeout for '+rid);
|
||||
clearInterval(timer);
|
||||
delete _sent[groupKey];
|
||||
for(var ti=0;ti<bidList.length;ti++){delete _sent[bidList[ti]];}
|
||||
return;
|
||||
}
|
||||
fetch(BASE+'/response/'+rid).then(function(r){return r.json();}).then(function(d){
|
||||
if(d.waiting)return;
|
||||
clearInterval(timer);
|
||||
var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1;
|
||||
if(btnIdx>=0&&btnIdx<btnRefs.length){
|
||||
// Multi-choice: click specific button by index
|
||||
var targetBtn=btnRefs[btnIdx];
|
||||
var targetTxt=(targetBtn.textContent||'').trim();
|
||||
log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+'] "'+targetTxt+'"');
|
||||
targetBtn.click();
|
||||
} else if(d.approved){
|
||||
// Legacy single-button: click first (primary) button
|
||||
var primaryBtn=btnRefs[0];
|
||||
log('✅ APPROVED '+rid+' → clicking "'+((primaryBtn.textContent||'').trim())+'"');
|
||||
primaryBtn.click();
|
||||
} else {
|
||||
// Legacy reject: find and click reject/deny button
|
||||
log('❌ REJECTED '+rid+' → finding reject button');
|
||||
clickRejectButton(btnRefs[0]);
|
||||
}
|
||||
delete _sent[groupKey];
|
||||
for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];}
|
||||
}).catch(function(){});
|
||||
},1500);
|
||||
}
|
||||
|
||||
// Legacy pollResponse for backward compatibility (single button)
|
||||
function pollResponse(rid,btn,bid){
|
||||
pollResponseGroup(rid,[btn],[bid],bid);
|
||||
}
|
||||
|
||||
// ── Find and click the reject/cancel counterpart button ──
|
||||
function clickRejectButton(approveBtn){
|
||||
// Walk up to find the container, then search for reject buttons
|
||||
var container=approveBtn.closest('[class*="step"]')
|
||||
||approveBtn.closest('[class*="action"]')
|
||||
||approveBtn.closest('[class*="tool"]')
|
||||
||approveBtn.parentElement;
|
||||
if(!container){log('No container for reject');return;}
|
||||
|
||||
var siblings=container.querySelectorAll('button');
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var t=(siblings[i].textContent||'').trim();
|
||||
for(var r=0;r<REJECT_RE.length;r++){
|
||||
if(REJECT_RE[r].test(t)){
|
||||
log('Clicking reject: "'+t+'"');
|
||||
siblings[i].click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
log('No reject button found near approve button');
|
||||
}
|
||||
|
||||
// ── Throttled scan — leading-edge: fires immediately, then locks ──
|
||||
function scheduleScan(){
|
||||
if(!_ready)return;
|
||||
var now=Date.now();
|
||||
if(now-_lastScanTs>=THROTTLE_MS){
|
||||
_lastScanTs=now;
|
||||
scan();
|
||||
} else if(!_scanScheduled){
|
||||
_scanScheduled=true;
|
||||
setTimeout(function(){
|
||||
_scanScheduled=false;
|
||||
_lastScanTs=Date.now();
|
||||
scan();
|
||||
},THROTTLE_MS-(now-_lastScanTs));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Periodic cleanup of stale _sent entries ──
|
||||
setInterval(function(){
|
||||
var now=Date.now();
|
||||
var keys=Object.keys(_sent);
|
||||
for(var i=0;i<keys.length;i++){
|
||||
var entry=_sent[keys[i]];
|
||||
if(entry&&entry.ts&&(now-entry.ts)>CLEANUP_MS){
|
||||
log('Cleanup stale entry: '+keys[i]);
|
||||
delete _sent[keys[i]];
|
||||
}
|
||||
}
|
||||
},60000);
|
||||
|
||||
// ── Start observation ──
|
||||
function startObserver(){
|
||||
if(_obs)return;
|
||||
// PRIMARY: MutationObserver — reacts instantly to DOM changes
|
||||
new MutationObserver(function(mutations){
|
||||
// Only scan if mutations contain added nodes (new buttons potentially)
|
||||
for(var i=0;i<mutations.length;i++){
|
||||
if(mutations[i].addedNodes.length>0){
|
||||
scheduleScan();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}).observe(document.body,{childList:true,subtree:true});
|
||||
|
||||
// FALLBACK: periodic scan every 3s for any missed mutations
|
||||
setInterval(scheduleScan,3000);
|
||||
|
||||
// ── Adaptive idle detection for HTTP polls ──
|
||||
var _lastActivity=Date.now();
|
||||
var _idleThreshold=60000; // 60s without DOM changes → slow mode
|
||||
new MutationObserver(function(){_lastActivity=Date.now();}).observe(document.body,{childList:true,subtree:true,attributes:true});
|
||||
function getAdaptiveInterval(){return (Date.now()-_lastActivity>_idleThreshold)?10000:2000;}
|
||||
|
||||
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
|
||||
(function pollDeepInspect(){
|
||||
if(_ready&&BASE){
|
||||
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(){});
|
||||
}
|
||||
setTimeout(pollDeepInspect,getAdaptiveInterval());
|
||||
})();
|
||||
|
||||
// ── 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
|
||||
(function pollTriggerClick(){
|
||||
if(_ready&&BASE){
|
||||
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||||
if(!d.action)return;
|
||||
log('🔔 TRIGGER-CLICK received: action='+d.action);
|
||||
|
||||
var approveRe=[/^Run$/i,/^Run /i,/^Accept/i,/^Allow/i,/^Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i];
|
||||
var rejectRe=[/^Reject/i,/^Cancel$/i,/^Deny$/i,/^Stop$/i,/^Decline$/i,/^Dismiss$/i];
|
||||
var patterns=(d.action==='approve')?approveRe:rejectRe;
|
||||
var emoji=(d.action==='approve')?'✅':'❌';
|
||||
|
||||
// Phase 1: deepFindButtons in main doc + accessible iframes + shadow DOMs
|
||||
var found=deepFindButtons(patterns);
|
||||
if(found.length>0){
|
||||
log(emoji+' TRIGGER-CLICK: clicking "'+found[0].text+'" from '+found[0].source);
|
||||
found[0].btn.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 2: Try <webview>.executeJavaScript for inaccessible webviews
|
||||
var webviews=document.querySelectorAll('webview');
|
||||
if(webviews.length>0){
|
||||
log('TRIGGER-CLICK: trying '+webviews.length+' webview(s) via executeJavaScript...');
|
||||
var patternsStr=patterns.map(function(re){return re.source;}).join('|');
|
||||
var clickScript='(function(){'+
|
||||
'var re=new RegExp("'+patternsStr+'","i");'+
|
||||
'var btns=document.querySelectorAll("button");'+
|
||||
'for(var i=0;i<btns.length;i++){'+
|
||||
'var b=btns[i];if(b.disabled||b.hidden)continue;'+
|
||||
'var t=(b.textContent||"").trim();'+
|
||||
'if(re.test(t)){b.click();return "CLICKED:"+t;}'+
|
||||
'}'+
|
||||
'return "NOT_FOUND:"+btns.length+"_buttons";'+
|
||||
'})()';
|
||||
for(var w=0;w<webviews.length;w++){
|
||||
(function(wv,idx){
|
||||
try{
|
||||
if(typeof wv.executeJavaScript==='function'){
|
||||
wv.executeJavaScript(clickScript).then(function(result){
|
||||
log(emoji+' TRIGGER-CLICK webview#'+idx+': '+result);
|
||||
}).catch(function(e){
|
||||
log('TRIGGER-CLICK webview#'+idx+' execJS error: '+e.message);
|
||||
});
|
||||
}
|
||||
}catch(e){
|
||||
log('TRIGGER-CLICK webview#'+idx+' error: '+e.message);
|
||||
}
|
||||
})(webviews[w],w);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Try iframes via postMessage (cross-origin fallback)
|
||||
var iframes=document.querySelectorAll('iframe');
|
||||
if(iframes.length>0){
|
||||
log('TRIGGER-CLICK: trying '+iframes.length+' iframe(s) — checking accessibility...');
|
||||
var clickedAny=false;
|
||||
for(var fi=0;fi<iframes.length;fi++){
|
||||
try{
|
||||
var idoc=iframes[fi].contentDocument||iframes[fi].contentWindow.document;
|
||||
if(!idoc)continue;
|
||||
var ibtns=idoc.querySelectorAll('button');
|
||||
for(var bi=0;bi<ibtns.length;bi++){
|
||||
var ib=ibtns[bi];
|
||||
if(ib.disabled||ib.hidden)continue;
|
||||
var itxt=(ib.textContent||'').trim();
|
||||
for(var pi=0;pi<patterns.length;pi++){
|
||||
if(patterns[pi].test(itxt)){
|
||||
log(emoji+' TRIGGER-CLICK iframe#'+fi+': clicking "'+itxt+'"');
|
||||
ib.click();
|
||||
clickedAny=true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
if(!found.length){
|
||||
// Log what we DID find for debugging
|
||||
var allBtns=document.querySelectorAll('button');
|
||||
var btnTexts=[];
|
||||
for(var di=0;di<Math.min(10,allBtns.length);di++){
|
||||
btnTexts.push('"'+((allBtns[di].textContent||'').trim()).substring(0,30)+'"');
|
||||
}
|
||||
log('⚠️ TRIGGER-CLICK: no '+d.action+' button found. Main DOM has '+allBtns.length+' btns: ['+btnTexts.join(',')+']');
|
||||
log('⚠️ iframes='+document.querySelectorAll('iframe').length+' webviews='+document.querySelectorAll('webview').length);
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
setTimeout(pollTriggerClick,getAdaptiveInterval());
|
||||
})();
|
||||
|
||||
_obs=true;
|
||||
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user