feat(observer): v8 full-DOM unconditional dump — body tree capture at 5s/15s/60s with depth 15, indexed dump files, deep-inspect overhaul #task-619

This commit is contained in:
Variet Worker
2026-04-12 07:37:25 +09:00
parent 353265bed8
commit 0e03b3a300
3 changed files with 155 additions and 73 deletions

View File

@@ -2,7 +2,7 @@
"name": "gravity-bridge", "name": "gravity-bridge",
"displayName": "Gravity Bridge", "displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.", "description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.36", "version": "0.5.37",
"publisher": "variet", "publisher": "variet",
"engines": { "engines": {
"vscode": "^1.100.0" "vscode": "^1.100.0"

View File

@@ -126,9 +126,12 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
req.on('data', (c: string) => dumpBody += c); req.on('data', (c: string) => dumpBody += c);
req.on('end', () => { req.on('end', () => {
try { try {
const fs = require('fs'); // Save indexed dump for history + latest as dump_html.json
const path = require('path'); let idx = 1;
try { const parsed = JSON.parse(dumpBody); idx = parsed.dumpIndex || idx; } catch {}
fs.writeFileSync(path.join(ctx.bridgePath, `dump_html_${idx}.json`), dumpBody, 'utf-8');
fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8'); fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8');
ctx.logToFile(`[HTTP] DOM dump #${idx} saved (${dumpBody.length} bytes)`);
} catch (e) { } } catch (e) { }
res.writeHead(200); res.end('ok'); res.writeHead(200); res.end('ok');
}); });

View File

@@ -1,17 +1,16 @@
export function generateApprovalObserverScript(_port: number): string { export function generateApprovalObserverScript(_port: number): string {
return ` return `
// ── Gravity Bridge v7: Step-Aware AG Native DOM Parser ── // ── Gravity Bridge v8: Full-DOM AG Native Parser ──
// Uses data-testid="conversation-view" + data-step-index for reliable parsing // Full body dump + step-aware parsing — no hardcoded selector dependency
(function(){ (function(){
'use strict'; 'use strict';
var BASE='',_obs=false,_sent={},_ready=false; var BASE='',_obs=false,_sent={},_ready=false;
var _scanScheduled=false,_lastScanTs=0; var _scanScheduled=false,_lastScanTs=0;
var THROTTLE_MS=500; var THROTTLE_MS=500;
var CLEANUP_MS=300000; var CLEANUP_MS=300000;
var _dumpSent=false; // one-time DOM dump
function log(m){console.log('[GB Observer] '+m);} function log(m){console.log('[GB Observer] '+m);}
log('v7 Script loaded — Step-Aware AG Native DOM Parser'); log('v8 Script loaded — Full-DOM AG Native Parser');
// React-Compatible Synthetic Clicker // React-Compatible Synthetic Clicker
function dispatchReactClick(el){ function dispatchReactClick(el){
@@ -210,60 +209,132 @@ export function generateApprovalObserverScript(_port: number): string {
}); });
// ══════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════
// v7: DOM STRUCTURE DUMP (one-time, on first conversation-view detection) // v8: FULL DOM STRUCTURE DUMP (unconditional — no selector dependency)
// Posts the DOM tree structure to /dump-html for debugging // Dumps entire document.body tree to /dump-html for real DOM analysis
// Auto-triggers at 5s, 15s, 60s after load to capture React-rendered state
// ══════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════
function dumpDOMStructure() { var _dumpCount=0;
if (_dumpSent || !_ready) return; var MAX_DUMPS=5;
var cv = document.querySelector('[data-testid="conversation-view"]');
if (!cv) return;
_dumpSent = true;
// Walk the DOM tree and capture structure (classes, data-attrs, tag names) function walkNode(el, depth, maxDepth, maxChildren) {
function walkNode(el, depth) { if (depth > maxDepth) return {tag:'…',text:'depth limit'};
if (depth > 8) return null; if (!el || !el.tagName) return null;
var info = { var info = {
tag: el.tagName ? el.tagName.toLowerCase() : '#text', tag: el.tagName ? el.tagName.toLowerCase() : '#text',
cls: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : '', cls: '',
attrs: {}, attrs: {},
text: '', text: '',
children: [] children: []
}; };
// Capture data-* and role attributes // Capture className (string or SVGAnimatedString)
if (el.attributes) { if (el.className) {
for (var ai = 0; ai < el.attributes.length; ai++) { if (typeof el.className === 'string') info.cls = el.className.substring(0, 300);
var attr = el.attributes[ai]; else if (el.className.baseVal) info.cls = el.className.baseVal.substring(0, 300);
if (attr.name.startsWith('data-') || attr.name === 'role' || attr.name === 'aria-label') { }
info.attrs[attr.name] = (attr.value || '').substring(0, 100); // Capture ALL attributes that might help identify structure
} if (el.attributes) {
} for (var ai = 0; ai < el.attributes.length; ai++) {
var attr = el.attributes[ai];
var n = attr.name;
if (n === 'class' || n === 'style') continue; // class already captured, style is noise
info.attrs[n] = (attr.value || '').substring(0, 200);
} }
// For leaf text nodes, capture short text }
if (!el.children || el.children.length === 0) { // For leaf nodes or shallow nodes, capture text content
var t = (el.textContent || '').trim(); if (!el.children || el.children.length === 0) {
if (t.length > 0 && t.length < 100) info.text = t; var t = (el.textContent || '').trim();
if (t.length > 0) info.text = t.substring(0, 200);
}
// Recurse children
if (el.children) {
var limit = Math.min(el.children.length, maxChildren);
for (var ci = 0; ci < limit; ci++) {
var childInfo = walkNode(el.children[ci], depth + 1, maxDepth, maxChildren);
if (childInfo) info.children.push(childInfo);
} }
// Recurse children (limit to first 10 per level) if (el.children.length > limit) {
if (el.children) { info.children.push({tag: '…', text: '+' + (el.children.length - limit) + ' more children'});
for (var ci = 0; ci < Math.min(el.children.length, 10); ci++) { }
var childInfo = walkNode(el.children[ci], depth + 1); }
if (childInfo) info.children.push(childInfo); return info;
} }
if (el.children.length > 10) {
info.children.push({tag: '...', text: '+' + (el.children.length - 10) + ' more'}); function dumpDOMStructure() {
} if (!_ready || _dumpCount >= MAX_DUMPS) return;
if (!document.body) return;
// Only dump if there are enough elements (DOM has actually rendered)
var totalEls = document.querySelectorAll('*').length;
if (totalEls < 20) {
log('DOM dump skipped: only ' + totalEls + ' elements (not rendered yet)');
return;
}
_dumpCount++;
// Walk entire body with generous limits
var structure = walkNode(document.body, 0, 15, 30);
// Also gather quick-reference info
var quickInfo = {
totalElements: totalEls,
title: document.title,
url: window.location.href,
hasConversationView: !!document.querySelector('[data-testid="conversation-view"]'),
hasStepIndex: !!document.querySelector('[data-step-index]'),
hasBotColor: !!document.querySelector('.text-ide-message-block-bot-color'),
hasMarkdownBody: !!document.querySelector('.markdown-body'),
hasProse: !!document.querySelector('.prose'),
buttons: [],
dataTestIds: [],
dataAttrs: []
};
// Collect all data-testid values in the page
var testIdEls = document.querySelectorAll('[data-testid]');
for (var ti = 0; ti < testIdEls.length; ti++) {
var tid = testIdEls[ti].getAttribute('data-testid');
if (quickInfo.dataTestIds.indexOf(tid) === -1) quickInfo.dataTestIds.push(tid);
}
// Collect all unique data-* attribute names
var allEls = document.querySelectorAll('*');
var seenAttrs = {};
for (var ei = 0; ei < Math.min(allEls.length, 2000); ei++) {
var elAttrs = allEls[ei].attributes;
if (!elAttrs) continue;
for (var ai2 = 0; ai2 < elAttrs.length; ai2++) {
var aName = elAttrs[ai2].name;
if (aName.startsWith('data-') && !seenAttrs[aName]) {
seenAttrs[aName] = true;
quickInfo.dataAttrs.push(aName + '=' + (elAttrs[ai2].value || '').substring(0, 50));
}
}
}
// Collect visible buttons
var allBtns = document.querySelectorAll('button, [role="button"]');
for (var bi2 = 0; bi2 < Math.min(allBtns.length, 50); bi2++) {
var btnEl = allBtns[bi2];
var btnText = (btnEl.textContent || '').trim().substring(0, 80);
if (btnText.length > 0) {
quickInfo.buttons.push({
text: btnText,
tag: btnEl.tagName.toLowerCase(),
cls: ((btnEl.className && typeof btnEl.className === 'string') ? btnEl.className : '').substring(0, 150),
visible: !!(btnEl.offsetParent || btnEl.style.display === 'fixed')
});
} }
return info;
} }
var structure = walkNode(cv, 0);
var payload = JSON.stringify({ var payload = JSON.stringify({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
source: 'v7_dom_dump', source: 'v8_full_body_dump',
conversationView: structure dumpIndex: _dumpCount,
quickInfo: quickInfo,
body: structure
}); });
log('DOM dump: conversation-view found, sending ' + payload.length + ' bytes'); log('DOM dump #' + _dumpCount + ': ' + totalEls + ' elements, ' + payload.length + ' bytes');
fetch(BASE + '/dump-html', { fetch(BASE + '/dump-html', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -271,6 +342,13 @@ export function generateApprovalObserverScript(_port: number): string {
}).catch(function(e) { log('DOM dump failed: ' + e.message); }); }).catch(function(e) { log('DOM dump failed: ' + e.message); });
} }
// Auto-dump at multiple intervals to catch React rendering stages
function scheduleAutoDumps() {
setTimeout(function(){ log('Auto-dump @5s'); dumpDOMStructure(); }, 5000);
setTimeout(function(){ log('Auto-dump @15s'); dumpDOMStructure(); }, 15000);
setTimeout(function(){ log('Auto-dump @60s'); dumpDOMStructure(); }, 60000);
}
// ══════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════
// v7: STEP-AWARE CHAT BODY SCANNING // v7: STEP-AWARE CHAT BODY SCANNING
// Scans [data-step-index] elements inside [data-testid="conversation-view"] // Scans [data-step-index] elements inside [data-testid="conversation-view"]
@@ -580,6 +658,8 @@ export function generateApprovalObserverScript(_port: number): string {
function startObserver(){ function startObserver(){
if(_obs)return; if(_obs)return;
log('startObserver() — scheduling auto-dumps and mutation observer');
scheduleAutoDumps();
new MutationObserver(function(mutations){ new MutationObserver(function(mutations){
for(var i=0;i<mutations.length;i++){ for(var i=0;i<mutations.length;i++){
if(mutations[i].addedNodes.length>0){ if(mutations[i].addedNodes.length>0){
@@ -613,43 +693,42 @@ export function generateApprovalObserverScript(_port: number): string {
setTimeout(pollTriggerClick, 2000); setTimeout(pollTriggerClick, 2000);
})(); })();
// ── DEEP-INSPECT POLLING ── // ── DEEP-INSPECT POLLING (v8: full body dump) ──
(function pollDeepInspect(){ (function pollDeepInspect(){
if(_ready&&BASE){ if(_ready&&BASE){
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){ fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.inspect)return; if(!d.inspect)return;
log('Deep inspect triggered'); log('Deep inspect triggered — full body dump');
var cv = document.querySelector('[data-testid="conversation-view"]'); // Force a fresh DOM dump
_dumpCount = Math.max(0, _dumpCount - 1); // allow one more dump
dumpDOMStructure();
// Also send the result to deep-inspect-result endpoint
var bodyTree = walkNode(document.body, 0, 12, 25);
var result = { var result = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
windowURL: window.location.href, windowURL: window.location.href,
conversationViewFound: !!cv, title: document.title,
stepElements: [],
buttons: [],
totalElements: document.body ? document.querySelectorAll('*').length : 0, totalElements: document.body ? document.querySelectorAll('*').length : 0,
hasConversationView: !!document.querySelector('[data-testid="conversation-view"]'),
hasStepIndex: !!document.querySelector('[data-step-index]'),
dataTestIds: [],
buttons: [],
bodyTree: bodyTree
}; };
if (cv) { var testIdEls = document.querySelectorAll('[data-testid]');
var steps = cv.querySelectorAll('[data-step-index]'); for (var dti = 0; dti < testIdEls.length; dti++) {
for (var si = 0; si < steps.length; si++) { var dtid = testIdEls[dti].getAttribute('data-testid');
var s = steps[si]; if (result.dataTestIds.indexOf(dtid) === -1) result.dataTestIds.push(dtid);
var text = (s.textContent || '').trim().substring(0, 200);
result.stepElements.push({
stepIndex: s.getAttribute('data-step-index'),
classes: (s.className || '').substring(0, 200),
textPreview: text,
childCount: s.children ? s.children.length : 0,
});
}
} }
var allBtns = document.querySelectorAll('button'); var allBtns = document.querySelectorAll('button, [role="button"]');
for (var bi = 0; bi < Math.min(allBtns.length, 30); bi++) { for (var bi = 0; bi < Math.min(allBtns.length, 50); bi++) {
var btn = allBtns[bi]; var btn = allBtns[bi];
var btxt = cleanButtonText(btn); var btxt = (btn.textContent || '').trim().substring(0, 80);
if (btxt.length > 0) { if (btxt.length > 0) {
var stepC = getStepContainer(btn);
result.buttons.push({ result.buttons.push({
text: btxt, text: btxt,
stepIndex: stepC ? stepC.getAttribute('data-step-index') : null, tag: btn.tagName.toLowerCase(),
cls: ((btn.className && typeof btn.className === 'string') ? btn.className : '').substring(0, 150),
visible: !!(btn.offsetParent || btn.style.display === 'fixed'), visible: !!(btn.offsetParent || btn.style.display === 'fixed'),
}); });
} }