fix(bridge): isolate DOM observation scope and strip UI noise (TypeScript declarations, metrics) from Discord pending approval embeds
This commit is contained in:
48
bot.py
48
bot.py
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ── Context extraction: TIGHT scope — only button's immediate context ──
|
||||
// v6 FIX: Never climb more than 4 parents. Never grab editor/sidebar content.
|
||||
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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]);
|
||||
// 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() + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.nodeType === 1) {
|
||||
var tg = node.tagName.toUpperCase();
|
||||
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
|
||||
parentText = parentText.trim();
|
||||
if (parentText.length > 3 && parentText.length < 300) {
|
||||
return cleanLines(parentText).substring(0, 300);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
return extractChatContextFromNode(botTurn);
|
||||
} catch(e) {
|
||||
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){
|
||||
|
||||
Reference in New Issue
Block a user