perf(bridge): 3 optimizations — pollResponseGroup 1500ms, renderer adaptive idle, Bot single-pass scanner

This commit is contained in:
2026-03-15 10:51:22 +09:00
parent f96203646e
commit ae0509fbb5
2 changed files with 68 additions and 72 deletions

108
bot.py
View File

@@ -611,12 +611,14 @@ class GravityBot(commands.Bot):
self._sent_commands[req.request_id] = req.command self._sent_commands[req.request_id] = req.command
await self._send_approval_request(channel, req) await self._send_approval_request(channel, req)
# ── Check for auto_resolved pendings (approved directly in AG) ── # ── Single-pass: handle auto_resolved, expired, and MERGE in one glob ──
for f in self.bridge.pending_dir.glob("*.json"): for f in self.bridge.pending_dir.glob("*.json"):
try: try:
data = json.loads(f.read_text(encoding="utf-8-sig")) data = json.loads(f.read_text(encoding="utf-8-sig"))
if data.get("status") == "auto_resolved": status = data.get("status", "pending")
rid = data.get("request_id", "") rid = data.get("request_id", "")
if status == "auto_resolved":
# FIX #5: Use _approval_messages as fallback when discord_message_id is 0 # FIX #5: Use _approval_messages as fallback when discord_message_id is 0
msg_id = data.get("discord_message_id", 0) or self._approval_messages.get(rid, 0) msg_id = data.get("discord_message_id", 0) or self._approval_messages.get(rid, 0)
project = data.get("project_name", Config.PROJECT_NAME) project = data.get("project_name", Config.PROJECT_NAME)
@@ -630,25 +632,18 @@ class GravityBot(commands.Bot):
description=f"```\n{data.get('command', '')[:500]}\n```", description=f"```\n{data.get('command', '')[:500]}\n```",
color=discord.Color.green(), color=discord.Color.green(),
) )
embed.set_footer(text=f"ID: {data.get('request_id', '')}") embed.set_footer(text=f"ID: {rid}")
await msg.edit(embed=embed, view=None) await msg.edit(embed=embed, view=None)
except discord.NotFound: except discord.NotFound:
pass pass
f.unlink() f.unlink()
self._deferred_ids.pop(data.get("request_id", ""), None) self._deferred_ids.pop(rid, None)
self._sent_commands.pop(data.get("request_id", ""), None) self._sent_commands.pop(rid, None)
self._approval_messages.pop(data.get("request_id", ""), None) self._approval_messages.pop(rid, None)
self._sent_approval_ids.discard(data.get("request_id", "")) self._sent_approval_ids.discard(rid)
except (json.JSONDecodeError, OSError):
pass
# ── Check for expired pendings — update Discord card ── elif status == "expired":
for f in self.bridge.pending_dir.glob("*.json"):
try:
data = json.loads(f.read_text(encoding="utf-8-sig"))
if data.get("status") == "expired":
msg_id = data.get("discord_message_id", 0) msg_id = data.get("discord_message_id", 0)
rid = data.get("request_id", "")
project = data.get("project_name", Config.PROJECT_NAME) project = data.get("project_name", Config.PROJECT_NAME)
if msg_id: if msg_id:
channel = await self._get_channel(project) channel = await self._get_channel(project)
@@ -668,52 +663,43 @@ class GravityBot(commands.Bot):
self._deferred_ids.pop(rid, None) self._deferred_ids.pop(rid, None)
self._sent_commands.pop(rid, None) self._sent_commands.pop(rid, None)
self._sent_approval_ids.discard(rid) self._sent_approval_ids.discard(rid)
except (json.JSONDecodeError, OSError):
pass
# ── Check for MERGE updates (step_probe updated command in already-sent pending) ── elif status == "pending":
for f in self.bridge.pending_dir.glob("*.json"): # MERGE check: step_probe updated command in already-sent pending
try: if rid not in self._sent_approval_ids:
data = json.loads(f.read_text(encoding="utf-8-sig")) continue
rid = data.get("request_id", "") msg_id = data.get("discord_message_id", 0)
if rid not in self._sent_approval_ids: if not msg_id:
continue continue
if data.get("status") != "pending": new_cmd = data.get("command", "")
continue old_cmd = self._sent_commands.get(rid, "")
msg_id = data.get("discord_message_id", 0) if new_cmd and new_cmd != old_cmd and len(new_cmd) > len(old_cmd):
if not msg_id: self._sent_commands[rid] = new_cmd
continue project = data.get("project_name", Config.PROJECT_NAME)
# Check if command was updated via MERGE channel = await self._get_channel(project)
new_cmd = data.get("command", "") if channel:
old_cmd = self._sent_commands.get(rid, "") try:
if new_cmd and new_cmd != old_cmd and len(new_cmd) > len(old_cmd): msg = await channel.fetch_message(msg_id)
# MERGE detected — edit Discord message buttons = data.get("buttons")
self._sent_commands[rid] = new_cmd desc_parts = [f"**명령어:**\n```\n{new_cmd[:1000]}\n```"]
project = data.get("project_name", Config.PROJECT_NAME) if buttons and len(buttons) > 1:
channel = await self._get_channel(project) btn_names = [b.get("text", "?") for b in buttons]
if channel: desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
try: desc = data.get("description", "")
msg = await channel.fetch_message(msg_id) if desc:
# Rebuild embed with full command desc_parts.append(desc[:500])
buttons = data.get("buttons") embed = discord.Embed(
desc_parts = [f"**명령어:**\n```\n{new_cmd[:1000]}\n```"] title="⚠️ 승인 요청",
if buttons and len(buttons) > 1: description="\n".join(desc_parts),
btn_names = [b.get("text", "?") for b in buttons] color=discord.Color.orange(),
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}") timestamp=datetime.now(timezone.utc),
desc = data.get("description", "") )
if desc: embed.set_footer(text=f"ID: {rid}")
desc_parts.append(desc[:500]) await msg.edit(embed=embed)
embed = discord.Embed( logger.info(f"MERGE edit: {rid[:12]} cmd='{new_cmd[:60]}'")
title="⚠️ 승인 요청", except discord.NotFound:
description="\n".join(desc_parts), pass
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc),
)
embed.set_footer(text=f"ID: {rid}")
await msg.edit(embed=embed)
logger.info(f"MERGE edit: {rid[:12]} cmd='{new_cmd[:60]}'")
except discord.NotFound:
pass
except (json.JSONDecodeError, OSError): except (json.JSONDecodeError, OSError):
pass pass

View File

@@ -1429,7 +1429,7 @@ function generateApprovalObserverScript(_port: number): string {
// ── Poll for Discord response (multi-button group aware) ── // ── Poll for Discord response (multi-button group aware) ──
function pollResponseGroup(rid,btnRefs,bidList,groupKey){ function pollResponseGroup(rid,btnRefs,bidList,groupKey){
var polls=0; var polls=0;
var maxPolls=600; // 5 minutes at 500ms interval var maxPolls=200; // 5 minutes at 1500ms interval
var timer=setInterval(function(){ var timer=setInterval(function(){
polls++; polls++;
// Check if ANY button in the group is still in DOM // Check if ANY button in the group is still in DOM
@@ -1474,7 +1474,7 @@ function generateApprovalObserverScript(_port: number): string {
delete _sent[groupKey]; delete _sent[groupKey];
for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];} for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];}
}).catch(function(){}); }).catch(function(){});
},500); },1500);
} }
// Legacy pollResponse for backward compatibility (single button) // Legacy pollResponse for backward compatibility (single button)
@@ -1552,19 +1552,27 @@ function generateApprovalObserverScript(_port: number): string {
// FALLBACK: periodic scan every 3s for any missed mutations // FALLBACK: periodic scan every 3s for any missed mutations
setInterval(scheduleScan,3000); 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 ── // ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
setInterval(function(){ (function pollDeepInspect(){
if(!_ready||!BASE)return; 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){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();} if(d.inspect){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();}
}).catch(function(){}); }).catch(function(){});
},2000); }
setTimeout(pollDeepInspect,getAdaptiveInterval());
})();
// ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ── // ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ──
// Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks // Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks
// v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs // v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs
setInterval(function(){ (function pollTriggerClick(){
if(!_ready||!BASE)return; if(_ready&&BASE){
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){ fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.action)return; if(!d.action)return;
log('🔔 TRIGGER-CLICK received: action='+d.action); log('🔔 TRIGGER-CLICK received: action='+d.action);
@@ -1652,7 +1660,9 @@ function generateApprovalObserverScript(_port: number): string {
log('⚠️ iframes='+document.querySelectorAll('iframe').length+' webviews='+document.querySelectorAll('webview').length); log('⚠️ iframes='+document.querySelectorAll('iframe').length+' webviews='+document.querySelectorAll('webview').length);
} }
}).catch(function(){}); }).catch(function(){});
},1000); }
setTimeout(pollTriggerClick,getAdaptiveInterval());
})();
_obs=true; _obs=true;
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling'); log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');