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

@@ -1,17 +1,16 @@
export function generateApprovalObserverScript(_port: number): string {
return `
// ── Gravity Bridge v7: Step-Aware AG Native DOM Parser ──
// Uses data-testid="conversation-view" + data-step-index for reliable parsing
// ── Gravity Bridge v8: Full-DOM AG Native Parser ──
// Full body dump + step-aware parsing — no hardcoded selector dependency
(function(){
'use strict';
var BASE='',_obs=false,_sent={},_ready=false;
var _scanScheduled=false,_lastScanTs=0;
var THROTTLE_MS=500;
var CLEANUP_MS=300000;
var _dumpSent=false; // one-time DOM dump
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
function dispatchReactClick(el){
@@ -210,60 +209,132 @@ export function generateApprovalObserverScript(_port: number): string {
});
// ══════════════════════════════════════════════════════════════════
// v7: DOM STRUCTURE DUMP (one-time, on first conversation-view detection)
// Posts the DOM tree structure to /dump-html for debugging
// v8: FULL DOM STRUCTURE DUMP (unconditional — no selector dependency)
// 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() {
if (_dumpSent || !_ready) return;
var cv = document.querySelector('[data-testid="conversation-view"]');
if (!cv) return;
_dumpSent = true;
var _dumpCount=0;
var MAX_DUMPS=5;
// Walk the DOM tree and capture structure (classes, data-attrs, tag names)
function walkNode(el, depth) {
if (depth > 8) return null;
var info = {
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
cls: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : '',
attrs: {},
text: '',
children: []
};
// Capture data-* and role attributes
if (el.attributes) {
for (var ai = 0; ai < el.attributes.length; ai++) {
var attr = el.attributes[ai];
if (attr.name.startsWith('data-') || attr.name === 'role' || attr.name === 'aria-label') {
info.attrs[attr.name] = (attr.value || '').substring(0, 100);
}
}
function walkNode(el, depth, maxDepth, maxChildren) {
if (depth > maxDepth) return {tag:'…',text:'depth limit'};
if (!el || !el.tagName) return null;
var info = {
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
cls: '',
attrs: {},
text: '',
children: []
};
// Capture className (string or SVGAnimatedString)
if (el.className) {
if (typeof el.className === 'string') info.cls = el.className.substring(0, 300);
else if (el.className.baseVal) info.cls = el.className.baseVal.substring(0, 300);
}
// 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) {
var t = (el.textContent || '').trim();
if (t.length > 0 && t.length < 100) info.text = t;
}
// For leaf nodes or shallow nodes, capture text content
if (!el.children || el.children.length === 0) {
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) {
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);
}
if (el.children.length > 10) {
info.children.push({tag: '...', text: '+' + (el.children.length - 10) + ' more'});
}
if (el.children.length > limit) {
info.children.push({tag: '…', text: '+' + (el.children.length - limit) + ' more children'});
}
}
return info;
}
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({
timestamp: new Date().toISOString(),
source: 'v7_dom_dump',
conversationView: structure
source: 'v8_full_body_dump',
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', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -271,6 +342,13 @@ export function generateApprovalObserverScript(_port: number): string {
}).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
// Scans [data-step-index] elements inside [data-testid="conversation-view"]
@@ -580,6 +658,8 @@ export function generateApprovalObserverScript(_port: number): string {
function startObserver(){
if(_obs)return;
log('startObserver() — scheduling auto-dumps and mutation observer');
scheduleAutoDumps();
new MutationObserver(function(mutations){
for(var i=0;i<mutations.length;i++){
if(mutations[i].addedNodes.length>0){
@@ -613,43 +693,42 @@ export function generateApprovalObserverScript(_port: number): string {
setTimeout(pollTriggerClick, 2000);
})();
// ── DEEP-INSPECT POLLING ──
// ── DEEP-INSPECT POLLING (v8: full body dump) ──
(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)return;
log('Deep inspect triggered');
var cv = document.querySelector('[data-testid="conversation-view"]');
log('Deep inspect triggered — full body dump');
// 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 = {
timestamp: new Date().toISOString(),
windowURL: window.location.href,
conversationViewFound: !!cv,
stepElements: [],
buttons: [],
title: document.title,
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 steps = cv.querySelectorAll('[data-step-index]');
for (var si = 0; si < steps.length; si++) {
var s = steps[si];
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 testIdEls = document.querySelectorAll('[data-testid]');
for (var dti = 0; dti < testIdEls.length; dti++) {
var dtid = testIdEls[dti].getAttribute('data-testid');
if (result.dataTestIds.indexOf(dtid) === -1) result.dataTestIds.push(dtid);
}
var allBtns = document.querySelectorAll('button');
for (var bi = 0; bi < Math.min(allBtns.length, 30); bi++) {
var allBtns = document.querySelectorAll('button, [role="button"]');
for (var bi = 0; bi < Math.min(allBtns.length, 50); bi++) {
var btn = allBtns[bi];
var btxt = cleanButtonText(btn);
var btxt = (btn.textContent || '').trim().substring(0, 80);
if (btxt.length > 0) {
var stepC = getStepContainer(btn);
result.buttons.push({
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'),
});
}