fix(bridge): isolate DOM observation scope and strip UI noise (TypeScript declarations, metrics) from Discord pending approval embeds

This commit is contained in:
Variet Worker
2026-04-11 17:25:33 +09:00
parent 7630bf1f8c
commit 70dc301dca
2 changed files with 182 additions and 99 deletions

48
bot.py
View File

@@ -724,15 +724,55 @@ class GravityBot(commands.Bot):
desc_parts = []
if header:
desc_parts.append(header)
desc_parts.append(f"**명령:** `{request.command[:200]}`")
# Clean command text (remove "Running2" artifacts → "Running 2")
cmd_text = request.command[:200]
import re
cmd_text = re.sub(r'Running(\d)', r'Running \1', cmd_text)
desc_parts.append(f"**명령:** `{cmd_text}`")
if buttons:
btn_names = [b.get("text", "?") for b in buttons]
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
if request.description:
desc_parts.append(request.description[:500])
# Clean description: strip noise headers and garbage
desc_raw = request.description or ""
# Remove old-style headers
desc_raw = re.sub(r'\[AI 본문 요약\]\s*', '', desc_raw)
desc_raw = re.sub(r'\[결행 명령\]\s*', '', desc_raw)
# Remove lines that are clearly noise
desc_lines = desc_raw.split('\n')
clean_desc_lines = []
for dline in desc_lines:
dline_stripped = dline.strip()
if not dline_stripped:
continue
# Skip UI artifacts
if dline_stripped in ('chevron_right', 'chevron_left', 'close', 'check',
'content_copy', 'expand_more', 'expand_less',
'Show more', 'Show less', 'Copy', 'Edit', 'Copied!'):
continue
# Skip "Thought for Xs"
if re.match(r'^Thought for \d+', dline_stripped):
continue
# Skip TypeScript declarations and file paths
if re.match(r'^(declare|import|export)\s+(class|function|interface|type|enum|const)', dline_stripped):
continue
if re.search(r'\.ts:\d+:', dline_stripped):
continue
if re.search(r'extension.*src.*sdk', dline_stripped, re.IGNORECASE):
continue
clean_desc_lines.append(dline_stripped)
clean_desc = '\n'.join(clean_desc_lines).strip()
if clean_desc and len(clean_desc) > 3:
# Truncate and wrap in code block for readability
if len(clean_desc) > 300:
clean_desc = clean_desc[:300] + ''
desc_parts.append(f"```\n{clean_desc}\n```")
embed = discord.Embed(
title="⚠️ 승인 요청",
title=f"⚠️ 승인 요청{request.step_type or 'action'}",
description="\n".join(desc_parts),
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc),

View File

@@ -1,6 +1,6 @@
export function generateApprovalObserverScript(_port: number): string {
return `
// ── Gravity Bridge v5: Context-First DOM Extraction ──
// ── Gravity Bridge v6: Clean Context Extraction ──
(function(){
'use strict';
var BASE='',_obs=false,_sent={},_ready=false;
@@ -9,7 +9,7 @@ export function generateApprovalObserverScript(_port: number): string {
var CLEANUP_MS=300000;
function log(m){console.log('[GB Observer] '+m);}
log('v5 Script loaded — Context-First Tailored Extraction');
log('v6 Script loaded — Clean Context Extraction');
// React-Compatible Synthetic Clicker
function dispatchReactClick(el){
@@ -25,16 +25,58 @@ export function generateApprovalObserverScript(_port: number): string {
}
}
// ── Noise filter: lines that are UI artifacts, not real content ──
var NOISE_PATTERNS = [
/^chevron_right$/i,
/^chevron_left$/i,
/^arrow_/i,
/^Thought for \\\\d+/i,
/^expand_/i,
/^close$/i,
/^more_/i,
/^content_copy$/i,
/^check$/i,
/^\\\\d+ lines?$/i,
/^Show more$/i,
/^Show less$/i,
/^Copy$/i,
/^Edit$/i,
/^Copied!$/i,
/^\\\\s*$/,
/^declare\\\\s+(class|function|interface|type|enum|const|var|let)\\\\s/, // TypeScript declarations
/^(import|export|from)\\\\s/, // JS imports
/^\\\\s*[{}()\\\\[\\\\];]\\\\s*$/, // lone brackets
/\\\\.ts:\\\\d+:/, // file:line references
/extension.*src.*sdk/i, // SDK file paths
];
function isNoiseLine(line) {
if (!line || line.trim().length < 2) return true;
var trimmed = line.trim();
for (var i = 0; i < NOISE_PATTERNS.length; i++) {
if (NOISE_PATTERNS[i].test(trimmed)) return true;
}
return false;
}
function cleanLines(text) {
if (!text) return '';
var lines = text.split('\\\\n');
var clean = [];
for (var i = 0; i < lines.length; i++) {
if (!isNoiseLine(lines[i])) clean.push(lines[i].trim());
}
return clean.join('\\\\n').trim();
}
function cleanButtonText(btn) {
if (!btn) return '';
var clone = btn.cloneNode(true);
var icons = clone.querySelectorAll('.google-symbols, .codicon');
var icons = clone.querySelectorAll('.google-symbols, .codicon, [class*="icon"], svg');
for(var i=0; i<icons.length; i++) {
if(icons[i].parentNode) icons[i].parentNode.removeChild(icons[i]);
}
var tr = clone.querySelector('.truncate');
var txt = (tr ? tr.textContent : clone.textContent) || '';
return txt.trim().replace(/^[\s\u200B-\u200D\uFEFF\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
return txt.trim().replace(/^[\\\\s\\\\u200B-\\\\u200D\\\\uFEFF\\\\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
}
function btnId(b,type){
@@ -48,95 +90,81 @@ export function generateApprovalObserverScript(_port: number): string {
return type+'|'+txt+'|'+idx;
}
function extractCommandContext(b){
var container = b.closest('.p-1') || b.parentElement.parentElement;
if (!container) return "";
var titleSpans = container.querySelectorAll('span[title^="command("]');
if (titleSpans && titleSpans.length > 0) {
var t = titleSpans[0].getAttribute('title');
if (t && t.length > 5) return t.substring(0, 800);
}
var preEls = container.querySelectorAll('pre');
if (preEls && preEls.length > 0) {
var t2 = (preEls[preEls.length-1].textContent || '').trim();
if (t2.length > 2) return t2.substring(0, 800);
}
var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim();
return fallback.substring(0, 500);
}
// ── Context extraction: TIGHT scope — only button's immediate context ──
// v6 FIX: Never climb more than 4 parents. Never grab editor/sidebar content.
function extractChatContextFromNode(botTurn) {
if (!botTurn) return '';
var res = '';
// Use innerText if available on the markdown container (preserves spacing perfectly)
var md = botTurn.querySelector('.markdown-body') || botTurn.querySelector('.prose');
if (md && md.innerText && md.innerText.trim().length > 10) {
res = md.innerText.trim();
return res.substring(0, 3500);
function extractCommandContext(b) {
// Strategy 1: aria-label or title on button itself
var ariaLabel = b.getAttribute('aria-label') || b.getAttribute('title') || '';
if (ariaLabel && ariaLabel.length > 5 && ariaLabel.length < 500) {
return ariaLabel;
}
var toolContainer = botTurn.querySelector('.bg-agent-convo-background') || botTurn.querySelector('.bg-ide-background-color');
var textParts = [];
function walk(node) {
if (toolContainer && node === toolContainer) return;
if (node.id === 'antigravity.agentSidePanelInputBox') return;
if (node.nodeType === 1) {
var tag = node.tagName.toUpperCase();
if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return;
// Skip tool action blocks aggressively if they masquerade as normal divs
if (node !== botTurn && node.classList && (node.classList.contains('bg-ide-background-color') || node.classList.contains('bg-agent-convo-background'))) return;
// Strategy 2: Look for command text in button's DIRECT parent chain (max 3 levels)
var el = b.parentElement;
for (var depth = 0; depth < 3 && el; depth++) {
// Check for code/pre elements (command text)
var pres = el.querySelectorAll('pre, code');
for (var pi = 0; pi < pres.length; pi++) {
var preText = (pres[pi].textContent || '').trim();
if (preText.length > 2 && preText.length < 500 && !isNoiseLine(preText)) {
return preText.substring(0, 400);
}
}
if (node.nodeType === 3) {
var val = node.nodeValue;
if (val && val.trim()) textParts.push(val.trim());
} else {
if (node.childNodes && node.childNodes.length > 0) {
for(var i=0; i<node.childNodes.length; i++) {
walk(node.childNodes[i]);
}
}
}
if (node.nodeType === 1) {
var tg = node.tagName.toUpperCase();
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
// Check for span with title attribute containing command info
var titleSpans = el.querySelectorAll('span[title]');
for (var ti = 0; ti < titleSpans.length; ti++) {
var spanTitle = titleSpans[ti].getAttribute('title') || '';
if (spanTitle.length > 5 && spanTitle.length < 500) {
return spanTitle.substring(0, 400);
}
}
el = el.parentElement;
}
walk(botTurn);
res = textParts.join(' ').replace(/ \\n /g, '\\n').replace(/\\n+/g, '\\n').trim();
return res.substring(0, 3500);
}
function extractChatContext(b) {
try {
var botTurn = b.closest('.text-ide-message-block-bot-color') || b.closest('.bg-agent-convo-background');
if (!botTurn) {
var container = b.closest('.p-1') || b.parentElement;
botTurn = container ? container.parentElement : null;
// Strategy 3: Immediate parent's text only (NOT full page)
var immediateParent = b.parentElement;
if (immediateParent) {
var parentText = '';
var children = immediateParent.childNodes;
for (var ci = 0; ci < children.length; ci++) {
var child = children[ci];
if (child.nodeType === 3 && child.nodeValue && child.nodeValue.trim()) {
parentText += child.nodeValue.trim() + ' ';
} else if (child.nodeType === 1 && child.tagName !== 'BUTTON' && child.tagName !== 'SVG') {
var childText = child.textContent || '';
if (childText.trim().length > 2 && childText.trim().length < 200) {
parentText += childText.trim() + ' ';
}
}
}
parentText = parentText.trim();
if (parentText.length > 3 && parentText.length < 300) {
return cleanLines(parentText).substring(0, 300);
}
return extractChatContextFromNode(botTurn);
} catch(e) {
return '';
}
return '';
}
function extractContext(b) {
var cmd = extractCommandContext(b);
var chat = extractChatContext(b);
if (!chat && !cmd) return "";
var combined = "";
if (chat && chat.length > 5) combined += "[AI 본문 요약]\\n" + chat + "\\n\\n";
if (cmd && cmd.length > 2) combined += "[결행 명령]\\n" + cmd;
return combined.trim();
var cmd = cleanButtonText(b);
var detail = extractCommandContext(b);
if (!detail) return cmd;
// Deduplicate: if detail contains button text, just show detail
if (detail.includes(cmd)) return cleanLines(detail);
return cmd + ': ' + cleanLines(detail);
}
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', '결행', '수락', '반영', '허용', '승인'];
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', 'Always allow', '결행', '수락', '반영', '허용', '승인'];
var REJECT_WORDS = ['Reject', 'Cancel', 'Deny', 'Stop', 'Decline', 'Dismiss', '거절', '거부', '취소', '차단', '정지', '무시'];
function isActionBtn(txt) {
for(var i=0; i<ACTION_WORDS.length; i++) {
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
}
// "Running N command(s)" pattern
if (/Running\\\\d*\\\\s*command/i.test(txt)) return true;
return false;
}
function isRejectBtn(txt) {
@@ -174,7 +202,7 @@ export function generateApprovalObserverScript(_port: number): string {
var items = document.querySelectorAll('[aria-label^="Gravity Bridge Control"], [title^="Gravity Bridge Control"]');
if (items.length > 0) {
var text = items[0].getAttribute('aria-label') || items[0].getAttribute('title') || '';
var m = text.match(/port:(\\d+)/);
var m = text.match(/port:(\\\\d+)/);
if (m && m[1]) {
clearInterval(timer);
tryPingAsync(parseInt(m[1], 10)).then(function(ok){ cb(ok ? parseInt(m[1],10) : HARDCODED_PORT); });
@@ -194,13 +222,33 @@ export function generateApprovalObserverScript(_port: number): string {
startObserver();
});
// ── Chat body scanning (for Discord relay of AI responses) ──
var _lastText = "";
var _lastTextTime = 0;
var _lastTextSent = false;
function extractCleanChatText(container) {
if (!container) return '';
// Try markdown body first
var md = container.querySelector('.markdown-body') || container.querySelector('.prose');
var rawText = '';
if (md && md.innerText && md.innerText.trim().length > 10) {
rawText = md.innerText.trim();
} else {
rawText = (container.innerText || container.textContent || '').trim();
}
// Clean the text
return cleanLines(rawText).substring(0, 3500);
}
function scanChatBodies() {
if(!_ready)return;
var botTurns = document.querySelectorAll('.text-ide-message-block-bot-color');
// Find bot response containers — try multiple selectors for compatibility
var botTurns = document.querySelectorAll(
'.text-ide-message-block-bot-color, ' +
'[data-testid*="bot"], [data-testid*="assistant"], ' +
'[class*="agent-convo"], [class*="bot-message"]'
);
if (botTurns.length === 0) return;
var lastTurn = botTurns[botTurns.length - 1];
@@ -217,7 +265,7 @@ export function generateApprovalObserverScript(_port: number): string {
if (Date.now() - _lastTextTime > 3000) {
_lastTextSent = true;
lastTurn.dataset.agChatScraped = "pending";
var finalTxt = extractChatContextFromNode(lastTurn);
var finalTxt = extractCleanChatText(lastTurn);
if (finalTxt && finalTxt.length > 5 && finalTxt !== "Review Changes") {
fetch(BASE+'/chat', {
method: 'POST',
@@ -250,13 +298,13 @@ export function generateApprovalObserverScript(_port: number): string {
if(txt.length <= 1) continue;
if(!isActionBtn(txt)) continue;
// Skip inline code lens buttons unless they actually match the pattern properly
// Skip inline code lens buttons
if (b.closest('.codelens-decoration') && !txt.includes('Accept') && !txt.includes('수락') && !txt.includes('반영')) {
continue;
}
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') ? 'command' : 'permission');
var container=b.closest('.p-1') || b.parentElement.parentElement;
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') || /Running\\\\d/.test(txt) ? 'command' : 'permission');
var container=b.parentElement;
var groupKey=matchedType+'|'+btnId(b,matchedType);
if(_sent[groupKey])continue;
@@ -275,27 +323,21 @@ export function generateApprovalObserverScript(_port: number): string {
}
var desc=extractContext(b);
var is_dom_dummy = false;
if (!desc || desc.trim().length <= 2) {
desc = "MISSING_DESCRIPTION_FROM_DOM_FALLBACK_TO_STEP_PROBE";
is_dom_dummy = true;
}
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
_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(', '));
log('DETECTED '+matchedType+': '+txt+' ['+desc.substring(0,80)+']');
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2,isDummy2){
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
var payload={
request_id:rid2,
command:txt2,
description:desc2,
step_type:type2,
buttons:buttonsArr2,
is_dom_dummy: isDummy2
buttons:buttonsArr2
};
fetch(BASE+'/pending',{
method:'POST',
@@ -312,7 +354,7 @@ export function generateApprovalObserverScript(_port: number): string {
delete _sent[groupKey2];
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
});
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr,is_dom_dummy);
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
break;
}
@@ -353,7 +395,8 @@ export function generateApprovalObserverScript(_port: number): string {
}
function clickRejectButton(approveBtn){
var container=approveBtn.closest('.p-1') || approveBtn.parentElement.parentElement;
var container=approveBtn.parentElement;
if(!container) container = approveBtn.parentElement && approveBtn.parentElement.parentElement;
if(!container)return;
var siblings=container.querySelectorAll('button');
for(var i=0;i<siblings.length;i++){
@@ -405,7 +448,7 @@ export function generateApprovalObserverScript(_port: number): string {
}).observe(document.body,{childList:true,subtree:true});
setInterval(scheduleScan,3000);
// ── TRIGGER-CLICK POLLING (Fallback for missed pushes) ──
// ── TRIGGER-CLICK POLLING ──
(function pollTriggerClick(){
if(_ready&&BASE){
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){