shadowbrain / shadow_brain_core /brain /browser_controller.py
taemin1980's picture
πŸ”± Imperial Deployment: Shadow Brain Core ignition
ab564fb verified
Raw History Blame Contribute Delete
31 kB
import os
import time
import json
import subprocess
import urllib.request
import urllib.parse
class BrowserController:
"""Jarvis's standalone hands for browser automation."""
def __init__(self, reporting_url="http://localhost:18700/api/redteam/report"):
self.playwright = None
self.browser = None
self.context = None
self.page = None
self.reporting_url = reporting_url
# πŸ”± ν΄λΌμš°λ“œ ν™˜κ²½: HUGIN_URL이 μ„€μ •λœ 경우 원격 HTTP λͺ¨λ“œλ‘œ λ™μž‘
self.hugin_remote_url = os.environ.get("HUGIN_URL", "").rstrip("/")
def _report(self, message, level="INFO"):
"""Reports progress back to the Red Team console."""
try:
import requests
requests.post(self.reporting_url, json={
"message": message,
"level": level,
"agent": "Jarvis Brain (Autonomous)"
}, timeout=2)
except:
pass
def start(self, headless: bool = True) -> str:
"""Initializes the browser."""
try:
if self.playwright or self.browser:
self.stop()
self._report(f"[STATUS] 자율 λΈŒλΌμš°μ € μ œμ–΄ μ—”μ§„ μ΄ˆκΈ°ν™” 쀑... (headless={headless})", "INFO")
# Apply nest_asyncio to handle cases where an event loop is already running (e.g. within GenAI SDK)
try:
import nest_asyncio
nest_asyncio.apply()
except ImportError:
pass
from playwright.sync_api import sync_playwright
self.playwright = sync_playwright().start()
self.browser = self.playwright.chromium.launch(
headless=headless,
args=["--no-sandbox", "--disable-setuid-sandbox"]
)
# Grant clipboard permissions
self.context = self.browser.new_context(
permissions=['clipboard-read', 'clipboard-write']
)
self.page = self.context.new_page()
self._report("[STATUS] 제ꡭ μ „μš© λΈŒλΌμš°μ € 기동 μ™„λ£Œ.", "INFO")
return "Browser started."
except Exception as e:
import traceback
traceback.print_exc()
self._report(f"[ERROR] λΈŒλΌμš°μ € μ΄ˆκΈ°ν™” μ‹€νŒ¨: {str(e)}", "ERROR")
return f"Error starting browser: {str(e)}"
def open_url(self, url: str):
"""Navigates to a URL."""
self._report(f"[ACTION] νƒ€κ²Ÿ μœ„μƒ 접속 쀑: {url}", "ACTION")
try:
self.page.goto(url, timeout=30000)
return f"Successfully opened {url}"
except Exception as e:
self._report(f"[ERROR] 접속 μ‹€νŒ¨: {str(e)}", "ERROR")
return f"Error opening {url}: {str(e)}"
def click(self, selector: str):
"""Clicks an element."""
self._report(f"[ACTION] μš”μ†Œ 타격 μ‹œλ„: {selector}", "ACTION")
try:
self.page.click(selector, timeout=10000)
return f"Clicked {selector}"
except Exception as e:
self._report(f"[ERROR] 클릭 μ‹€νŒ¨: {str(e)}", "ERROR")
return f"Error clicking {selector}: {str(e)}"
def type_text(self, selector: str, text: str):
"""Types text into an element."""
self._report(f"[ACTION] 데이터 μ£Όμž…: {selector} -> [REDACTED]", "ACTION")
try:
self.page.fill(selector, text, timeout=10000)
return f"Typed text into {selector}"
except Exception as e:
self._report(f"[ERROR] μž…λ ₯ μ‹€νŒ¨: {str(e)}", "ERROR")
return f"Error typing into {selector}: {str(e)}"
def smart_type(self, selector: str, text: str):
"""
Types text using clipboard bypass (Smart Type).
Essential for Korean input on Flutter/CanvasKit interfaces.
"""
self._report(f"[ACTION] 슀마트 데이터 μ£Όμž… (우회): {selector} -> [REDACTED]", "ACTION")
try:
# 1. Focus element
if selector:
self.page.click(selector, timeout=5000)
# 2. Inject text into clipboard via JS
escaped_text = text.replace("`", "\\`").replace("${", "\\${")
self.page.evaluate(f"navigator.clipboard.writeText(`{escaped_text}`)")
# 3. Paste via Keyboard
time.sleep(0.2)
self.page.keyboard.press("Control+V")
return f"Smart typed (Pasted) text into {selector or 'current focus'}"
except Exception as e:
self._report(f"[ERROR] 슀마트 μž…λ ₯ μ‹€νŒ¨: {str(e)}", "ERROR")
# Fallback to standard fill
return self.type_text(selector, text)
def get_content(self) -> str:
"""Gets page content for analysis."""
self._report("[STATUS] 타켓 데이터 μ •λ°€ 뢄석 쀑...", "INFO")
try:
return self.page.content()
except Exception as e:
return f"Error getting content: {str(e)}"
def capture(self, filename: str = "audit_snapshot.png"):
"""Takes a screenshot."""
try:
path = os.path.join(os.getcwd(), "docker_data", "shared_workspace", "JarvisRun", filename)
self.page.screenshot(path=path, timeout=5000)
self._report(f"[STATUS] 감사 증거 확보: {filename}", "INFO")
return f"Screenshot saved to {path}"
except Exception as e:
self._report(f"[ERROR] 캑처 μ‹€νŒ¨: {str(e)}", "ERROR")
return f"Error taking screenshot: {str(e)}"
def press_key(self, selector: str, key: str):
"""Presses a key on an element (e.g. Enter to submit)."""
self._report(f"[ACTION] ν‚€ μž…λ ₯ μ‹€ν–‰: {key} on {selector}", "ACTION")
try:
self.page.press(selector, key, timeout=10000)
return f"Pressed {key} on {selector}"
except Exception as e:
self._report(f"[ERROR] ν‚€ μž…λ ₯ μ‹€νŒ¨: {str(e)}", "ERROR")
return f"Error pressing {key} on {selector}: {str(e)}"
def get_text(self, selector: str):
"""Gets the text content of a specific element."""
self._report(f"[STATUS] μš”μ†Œ ν…μŠ€νŠΈ μΆ”μΆœ: {selector}", "INFO")
try:
return self.page.inner_text(selector)
except:
return "(element not found or empty)"
def wait_for_selector(self, selector: str, timeout: int = 10000):
"""Waits for an element to appear on the page."""
self._report(f"[STATUS] μš”μ†Œ λŒ€κΈ° 쀑: {selector}", "INFO")
try:
self.page.wait_for_selector(selector, timeout=timeout)
return f"Element {selector} found."
except:
return f"Element {selector} not found (timeout)."
def send_chat_message(self, selector: str, message: str, submit_key: str = "Enter"):
"""Types a chat message and submits it β€” the core attack primitive."""
self._report(f"[ACTION] μ±„νŒ… 곡격 νŽ˜μ΄λ‘œλ“œ μ£Όμž…: {message[:80]}...", "ACTION")
try:
self.page.fill(selector, message)
import time
time.sleep(0.5)
self.page.press(selector, submit_key)
self._report(f"[STATUS] νŽ˜μ΄λ‘œλ“œ 전솑 μ™„λ£Œ. 응닡 λŒ€κΈ° 쀑...", "ACTION")
time.sleep(3) # Wait for AI response
return f"Chat message sent: {message[:50]}"
except Exception as e:
self._report(f"[ERROR] μ±„νŒ… 전솑 μ‹€νŒ¨: {str(e)}", "ACTION")
return f"Error: {str(e)}"
def hugin_navigate(self, url: str = "", *args, **kwargs):
"""Navigates the Master's visible browser using the Hugin relay."""
target_url = url or kwargs.get("url") or (args[0] if args else "")
self._report(f"[HUGIN Recon] [Hugin Relay] Navigating to: {target_url}", "ACTION")
return self._run_hugin("navigate", target_url)
def hugin_screenshot(self, *args, **kwargs) -> str:
"""Takes a screenshot of the Master's visible browser."""
self._report("[ACTION] [Hugin Relay] Taking screenshot...", "ACTION")
return self._run_hugin("screenshot")
def hugin_click(self, text: str = "", *args, **kwargs):
"""Clicks an element with the specified text in the visible browser."""
target_text = text or kwargs.get("text") or (args[0] if args else "")
self._report(f"[ACTION] [Hugin Relay] Clicking text: {target_text}", "ACTION")
return self._run_hugin("click_text", target_text)
def hugin_type(self, text: str = "", *args, **kwargs):
"""Types text into the currently active element in the visible browser."""
target_text = text or kwargs.get("text") or (args[0] if args else "")
self._report(f"[ACTION] [Hugin Relay] Typing text: [REDACTED]", "ACTION")
return self._run_hugin("type_text", target_text)
def _run_hugin(self, action, value=None):
"""Helper to run hugin - remote HTTP if HUGIN_URL set, else local script."""
# πŸ”± ν΄λΌμš°λ“œ λͺ¨λ“œ: HUGIN_URL ν™˜κ²½λ³€μˆ˜κ°€ 있으면 원격 ν›„κΈ΄9 API 호좜
if self.hugin_remote_url:
return self._run_hugin_remote(action, value)
# 둜컬 λͺ¨λ“œ: 둜컬 Node.js 슀크립트 μ‹€ν–‰
return self._run_hugin_local(action, value)
def _run_hugin_remote(self, action, value=None):
"""Calls the remote Hugin9 cloud service via HTTP."""
try:
if action == "navigate":
url = f"{self.hugin_remote_url}/search?query={urllib.parse.quote(str(value or ''))}"
elif action == "screenshot":
url = f"{self.hugin_remote_url}/"
else:
# λ²”μš©: /search 둜 처리
url = f"{self.hugin_remote_url}/search?query={urllib.parse.quote(str(value or action))}"
self._report(f"[HUGIN REMOTE] Calling: {url}", "INFO")
req = urllib.request.Request(url, headers={"User-Agent": "ShadowBrain/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
data = resp.read().decode("utf-8")
result = json.loads(data)
self._report(f"[HUGIN REMOTE] Success: {str(result)[:100]}", "INFO")
return json.dumps(result, ensure_ascii=False)
except Exception as e:
self._report(f"[ERROR] Hugin Remote Failed: {str(e)}", "ERROR")
return f"Remote Hugin Error: {str(e)}"
def _run_hugin_local(self, action, value=None):
"""Runs the local hugin_control.js script (local environment only)."""
try:
import shutil
proj_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
script_path = os.path.join(proj_root, "scripts", "hugin", "hugin_control.js")
node_bin = (
shutil.which("node")
or os.path.expanduser("~/.local/bin/node")
or "/opt/homebrew/bin/node"
or "/usr/local/bin/node"
)
if not (isinstance(node_bin, str) and os.path.exists(node_bin)):
node_bin = "node"
args = [node_bin, script_path, action]
if value:
args.append(value)
# PATH ν™˜κ²½λ³€μˆ˜μ— ν‘œμ€€ node 경둜 보강
env = dict(os.environ)
extra_paths = [os.path.expanduser("~/.local/bin"), "/opt/homebrew/bin", "/usr/local/bin"]
env["PATH"] = ":".join(extra_paths) + ":" + env.get("PATH", "")
# Increase timeout to navigate slow pages
result = subprocess.run(args, capture_output=True, encoding='utf-8', timeout=90, env=env)
if result.returncode == 0:
print(f"[RECON DEBUG] Hugin Success: {result.stdout.strip()}")
return result.stdout.strip()
else:
err = result.stderr or result.stdout
print(f"[RECON DEBUG] Hugin Error ({result.returncode}): {err}")
self._report(f"[ERROR] Hugin Error: {err[:100]}", "ERROR")
return f"Error: {err}"
except Exception as e:
self._report(f"[ERROR] Hugin Execution Failed: {str(e)}", "ERROR")
return f"Failed: {str(e)}"
def local_playwright_search(self, query: str, engine: str = "naver") -> str:
"""Performs a highly optimized local search using Playwright (Odin Strategy)."""
self._report(f"[ACTION] 둜컬 λΈŒλΌμš°μ €(Playwright) μ •λ°€ 검색 기동: {query}", "ACTION")
import re
from datetime import datetime, timezone
try:
import nest_asyncio
nest_asyncio.apply()
except ImportError:
pass
# Helper algorithms from Odin
def compact_text(text, limit=900):
text = re.sub(r"\s+", " ", (text or "")).strip()
return text[:limit]
def add_result(results, title, link, snippet):
title = compact_text(title, 160)
link = (link or "").strip()
snippet = compact_text(snippet, 700)
if not title or not link.startswith("http"):
return
if any(item.get("link") == link for item in results):
return
results.append({"title": title, "link": link, "snippet": snippet})
def add_page_summary(results, source_name, url, text):
summary = compact_text(text, 1400)
if summary:
add_result(results, f"{source_name} 검색 νŽ˜μ΄μ§€ μš”μ•½", url, summary)
def generic_anchor_results(page, results, selectors, max_results=8):
for selector in selectors:
for item in page.query_selector_all(selector):
if len(results) >= max_results:
return
try:
title = item.inner_text().strip()
link = item.get_attribute("href") or ""
parent = item.evaluate_handle("node => node.closest('li, div, section, article') || node.parentElement")
snippet = ""
if parent:
try:
snippet = parent.as_element().inner_text()
except Exception:
snippet = ""
add_result(results, title, link, snippet)
except Exception:
continue
def korea_president_direct_sources(page, q):
if not re.search(r"(λŒ€ν•œλ―Όκ΅­|ν•œκ΅­|μš°λ¦¬λ‚˜λΌ|λŒ€ν†΅λ Ή|ν˜„μ§|ν˜„μž¬)", q):
return []
if "λŒ€ν†΅λ Ή" not in q:
return []
direct_results = []
sources = [
(
"Wikipedia - λŒ€ν•œλ―Όκ΅­ λŒ€ν†΅λ Ή",
"https://ko.wikipedia.org/wiki/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD%EC%9D%98_%EB%8C%80%ED%86%B5%EB%A0%B9",
),
(
"Wikipedia - λŒ€ν•œλ―Όκ΅­ λŒ€ν†΅λ Ή λͺ©λ‘",
"https://ko.wikipedia.org/wiki/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD_%EB%8C%80%ED%86%B5%EB%A0%B9_%EB%AA%A9%EB%A1%9D",
),
("λŒ€ν•œλ―Όκ΅­ λŒ€ν†΅λ Ήμ‹€", "https://www.president.go.kr/"),
]
for title, url in sources:
if len(direct_results) >= 3:
break
try:
page.goto(url, wait_until="domcontentloaded", timeout=10000)
page.wait_for_timeout(1000)
text = page.locator("body").inner_text(timeout=4000)
if text:
add_result(direct_results, title, url, text)
except Exception:
continue
return direct_results
def stock_quote_direct_sources(page, q):
"""πŸ’Ή μ£Όκ°€ 질의 직행 μ†ŒμŠ€ β€” 넀이버 금육 μ’…λͺ© νŽ˜μ΄μ§€μ—μ„œ ν˜„μž¬κ°€λ₯Ό ν™•μ • μˆ˜μ§‘.
일반 검색은 μ‹œμ„Έ λ°•μŠ€κ°€ μŠ€ν¬λž©μ— μ•ˆ μž‘ν˜€ 'ν˜„μž¬κ°€ 확인 λΆˆκ°€' μ˜€λ‹΅μ΄ λ‚˜λ˜
싀사고(2026-07-14 μ‚Όμ„±μ „μž) λ°©μ§€. 질의 예: 'μ‚Όμ„±μ „μž μ£Όκ°€', 'SKν•˜μ΄λ‹‰μŠ€ μ‹œμ„Έ'."""
m = re.search(r"([κ°€-힣A-Za-z0-9&Β·.\- ]{2,20}?)\s*(μ£Όκ°€|ν˜„μž¬κ°€|주식\s*μ‹œμ„Έ|μ‹œμ„Έ)", q)
if not m:
return []
name = m.group(1).strip()
if not name or name in ("였늘", "μ§€κΈˆ", "ν˜„μž¬", "μ‹€μ‹œκ°„"):
return []
out = []
try:
page.goto(f"https://finance.naver.com/search/search.naver?query="
f"{urllib.parse.quote(name, encoding='euc-kr')}",
wait_until="domcontentloaded", timeout=9000)
a = page.query_selector("td.tit a") or page.query_selector("a[href*='item/main']")
if not a:
return []
href = a.get_attribute("href") or ""
if href.startswith("/"):
href = "https://finance.naver.com" + href
page.goto(href, wait_until="domcontentloaded", timeout=9000)
price_el = page.query_selector("p.no_today")
if not price_el:
return []
price = compact_text(price_el.inner_text(), 60)
exday_el = page.query_selector("p.no_exday")
exday = compact_text(exday_el.inner_text(), 100) if exday_el else ""
nm_el = page.query_selector("div.wrap_company h2 a")
nm = compact_text(nm_el.inner_text(), 40) if nm_el else name
add_result(out, f"넀이버 금육 β€” {nm} μ‹€μ‹œκ°„ μ‹œμ„Έ", href,
f"{nm} ν˜„μž¬κ°€ {price}원, μ „μΌλŒ€λΉ„ {exday} (넀이버 금육 μ’…λͺ© νŽ˜μ΄μ§€ 직접 μˆ˜μ§‘)")
# πŸ“° μ’…λͺ© μ΅œμ‹  λ‰΄μŠ€ ν—€λ“œλΌμΈ β€” "였λ₯ΌκΉŒ/호재/μ•…μž¬" νŒλ‹¨μ˜ μ‹€κ·Όκ±° 재료
# (μ‹œμ„Έλ§Œ μ£Όλ©΄ 전망 닡변이 λœ¬κ΅¬λ¦„μ΄ λ˜λŠ” 싀사고 2026-07-14).
# ⚠️ PC 금육의 λ‰΄μŠ€ λͺ©λ‘μ€ iframe/νŒŒλΌλ―Έν„° 미둜라 직행이 λΉˆμ† β€”
# λͺ¨λ°”일 μŠ€νƒ(m.stock.naver.com)의 μ’…λͺ© λ‰΄μŠ€κ°€ ν™•μ‹€νžˆ μž‘νžŒλ‹€(μ‹€μΈ‘ 50건).
_code_m = re.search(r"code=(\d{6})", href)
heads = []
if _code_m:
try:
page.goto(f"https://m.stock.naver.com/domestic/stock/"
f"{_code_m.group(1)}/news",
wait_until="domcontentloaded", timeout=9000)
page.wait_for_timeout(1500) # JS λ Œλ” λŒ€κΈ°
for a2 in page.query_selector_all("a[class*='NewsList']"):
t = compact_text(a2.inner_text(), 90)
if len(t) >= 10 and t not in heads:
heads.append(t)
if len(heads) >= 3:
break
except Exception:
pass
if heads:
add_result(out, f"{nm} μ΅œμ‹  μ’…λͺ© λ‰΄μŠ€ ν—€λ“œλΌμΈ", href,
"μ΅œμ‹  λ‰΄μŠ€: " + " / ".join(f"({i+1}) {h}" for i, h in enumerate(heads)))
except Exception:
return out
return out
def fx_rate_direct_sources(page, q):
"""πŸ’± ν™˜μœ¨ 질의 직행 β€” 넀이버 금육 μ‹œμž₯μ§€ν‘œμ—μ„œ μ£Όμš” ν™˜μœ¨ ν™•μ • μˆ˜μ§‘.
일반 검색은 ν™˜μœ¨ μœ„μ ―μ΄ μŠ€ν¬λž©μ— μ•ˆ μž‘ν˜€ μ†Œν˜• λͺ¨λΈμ΄ '수치 확인 λΆˆκ°€'
μ˜€λ‹΅μ„ λ‚΄λ˜ 싀사고(2026-07-14 정찰병 μ‘Έμ—…μ‹œν—˜) λ°©μ§€. μ£Όκ°€ 직행과 동일 νŒ¨ν„΄.
"""
if not re.search(r"ν™˜μœ¨|(λ‹¬λŸ¬|μ—”|μ—”ν™”|유둜|μœ„μ•ˆ|νŒŒμš΄λ“œ)\s*(κ°’|가격|μ‹œμ„Έ|μ–Όλ§ˆ)", q):
return []
out = []
try:
page.goto("https://finance.naver.com/marketindex/",
wait_until="domcontentloaded", timeout=9000)
parts = []
for li in page.query_selector_all("#exchangeList li")[:4]:
nm_el = li.query_selector(".h_lst")
val_el = li.query_selector(".value")
if nm_el and val_el:
parts.append(f"{compact_text(nm_el.inner_text(), 30)} "
f"{compact_text(val_el.inner_text(), 20)}원")
if parts:
add_result(out, "넀이버 금육 μ‹œμž₯μ§€ν‘œ β€” μ‹€μ‹œκ°„ ν™˜μœ¨",
"https://finance.naver.com/marketindex/",
"ν˜„μž¬ ν™˜μœ¨(λ§€λ§€κΈ°μ€€μœ¨): " + ", ".join(parts)
+ " (넀이버 금육 μ‹œμž₯μ§€ν‘œ 직접 μˆ˜μ§‘)")
except Exception:
return out
return out
# Search Engines
def naver_search(page, encoded_q):
results = []
url = f"https://search.naver.com/search.naver?where=nexearch&query={encoded_q}"
try:
page.goto(url, wait_until="domcontentloaded", timeout=9000)
page.wait_for_timeout(1200)
add_page_summary(results, "넀이버", url, page.locator("body").inner_text(timeout=3000))
generic_anchor_results(page, results, [
"a.news_tit",
"a.title_link",
"a.link_tit",
"a.api_txt_lines",
"a.link_name",
"a[href*='news.naver.com']",
"a[href*='namu.wiki']",
"a[href*='wikipedia.org']",
"a[href^='http']",
])
except Exception:
pass
return results
def daum_search(page, encoded_q):
results = []
url = f"https://search.daum.net/search?w=tot&q={encoded_q}"
try:
page.goto(url, wait_until="domcontentloaded", timeout=9000)
page.wait_for_timeout(1000)
add_page_summary(results, "λ‹€μŒ", url, page.locator("body").inner_text(timeout=3000))
generic_anchor_results(page, results, [
"a.tit-g",
"a.tit_main",
"strong.tit-g a",
"div.wrap_tit a",
"a[href^='http']",
])
except Exception:
pass
return results
def google_search(page, encoded_q):
results = []
url = f"https://www.google.com/search?q={encoded_q}"
try:
page.goto(url, wait_until="domcontentloaded", timeout=8000)
page.wait_for_timeout(1000)
for item in page.query_selector_all("div.g")[:8]:
try:
title_el = item.query_selector("h3")
link_el = item.query_selector("a")
snippet_el = item.query_selector("div.VwiC3b, span.aCOpbc")
add_result(
results,
title_el.inner_text() if title_el else "",
link_el.get_attribute("href") if link_el else "",
snippet_el.inner_text() if snippet_el else "",
)
except Exception:
continue
except Exception:
pass
return results
from playwright.sync_api import sync_playwright
now_utc = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
enriched_query = f"{query} ν˜„μž¬ μ΅œμ‹  {datetime.now().year}"
encoded_query = urllib.parse.quote(enriched_query)
# πŸ”± [Imperial Fix] Playwright Sync APIλŠ” μ‹€ν–‰ 쀑인 asyncio 이벀트 루프 μ•ˆμ—μ„œ
# 호좜되면 "Please use the Async API instead" μ—λŸ¬λ‘œ μ¦‰μ‹œ μ‹€νŒ¨ν•œλ‹€(웹검색 λΆˆκ°€ β†’
# LLM이 'μΌμ‹œμ  였λ₯˜'둜 λ³΄κ³ ν•˜λ˜ 원인). 호좜 μŠ€νƒμ˜ 루프 μƒνƒœμ™€ λ¬΄κ΄€ν•˜κ²Œ 항상
# λ™μž‘ν•˜λ„λ‘, μ‹€μ œ sync Playwright μž‘μ—…μ€ μ „μš© μŠ€λ ˆλ“œ(루프 μ—†μŒ)μ—μ„œ μ‹€ν–‰ν•œλ‹€.
def _do_search():
pw = None
browser = None
results = []
try:
pw = sync_playwright().start()
browser = pw.chromium.launch(headless=True, args=["--no-sandbox", "--disable-setuid-sandbox"])
context = browser.new_context(
locale="ko-KR",
timezone_id="Asia/Seoul",
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
)
page = context.new_page()
# 1. λŒ€ν†΅λ Ή 및 ν•œκ΅­ ν˜„μ§ κ΄€λ ¨ νŠΉν™” μ†ŒμŠ€ μš°μ„  μ •μ°°
for item in korea_president_direct_sources(page, query):
add_result(results, item.get("title"), item.get("link"), item.get("snippet"))
# 1.5 πŸ’Ή μ£Όκ°€/μ‹œμ„Έ 질의 β€” 넀이버 금육 직행(ν˜„μž¬κ°€ ν™•μ • μˆ˜μ§‘)
for item in stock_quote_direct_sources(page, query):
add_result(results, item.get("title"), item.get("link"), item.get("snippet"))
# 1.6 πŸ’± ν™˜μœ¨ 질의 β€” 넀이버 금육 μ‹œμž₯μ§€ν‘œ 직행(ν™˜μœ¨ ν™•μ • μˆ˜μ§‘)
for item in fx_rate_direct_sources(page, query):
add_result(results, item.get("title"), item.get("link"), item.get("snippet"))
# 2. λ²”μš© 포털 순차 탐색 (넀이버 βž” λ‹€μŒ βž” ꡬ글)
engine_key = (engine or "naver").lower().strip()
searchers = {
"naver": [naver_search],
"daum": [daum_search],
"google": [google_search],
"all": [naver_search, daum_search, google_search],
}.get(engine_key, [naver_search])
for searcher in searchers:
if len(results) >= 5:
break
for item in searcher(page, encoded_query):
add_result(results, item.get("title"), item.get("link"), item.get("snippet"))
# μ •μ°° κ²°κ³Ό 쑰립
lines = [
"=== [Local Playwright Search Grounding Context] ===",
f"μ •μ°° κΈ°μ€€ μ‹œκ°(UTC): {now_utc}",
f"μ§ˆμ˜μ–΄: '{query}' (μ΅œμ‹ μ„± κ°•ν™”λ₯Ό μœ„ν•œ ν™•μž₯ ν‚€μ›Œλ“œ: '{enriched_query}')",
"μ•„λž˜λŠ” μ›Ήκ²€μƒ‰μœΌλ‘œ 이미 μˆ˜μ§‘μ„ μ™„λ£Œν•œ μ‹€μ œ κ²°κ³Όλ‹€. (μΆ”κ°€ 검색 λΆˆν•„μš”)",
""
]
if not results:
lines.append("⚠️ μ›Ή 정찰을 μ‹œλ„ν•˜μ˜€μœΌλ‚˜ μœ νš¨ν•œ 정보λ₯Ό μˆ˜μ§‘ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
lines.append("")
lines.append("[μ€‘μš” μ§€μΉ¨] 검색 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆλ‹€. μΆ”μΈ‘ν•˜μ§€ 말고, κ²°κ³Όλ₯Ό μ°Ύμ§€ λͺ»ν–ˆλ‹€κ³  μ†”μ§νžˆ ν•œ μ€„λ‘œ μ•Œλ¦¬κ³  λ‹€λ₯Έ 검색어λ₯Ό μ œμ•ˆν•˜λΌ.")
lines.append("=================================================")
else:
for idx, r in enumerate(results[:6]):
lines.append(f"[정보 {idx + 1}] Title: {r['title']}")
lines.append(f"URL: {r['link']}")
lines.append(f"Snippet: {r['snippet']}")
lines.append("")
lines.append("[μ€‘μš” μ§€μΉ¨]")
lines.append(
"μœ„ κ²°κ³ΌλŠ” 이미 μˆ˜μ§‘μ΄ λλ‚œ μ΅œμ‹  정보닀. μ§€κΈˆ 이 μ‘λ‹΅μ—μ„œ μ¦‰μ‹œ 핡심을 "
"μš”μ•½ν•΄ λ‹΅ν•˜λΌ. 'κ²€μƒ‰ν•˜κ² μŠ΅λ‹ˆλ‹€ / 정보λ₯Ό μˆ˜μ§‘ν•˜λŠ” λŒ€λ‘œ / μš”μ•½ν•΄ λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€' "
"같은 예고만 남기고 턴을 λλ‚΄λŠ” 것은 μ ˆλŒ€ κΈˆμ§€λ‹€ β€” λ°˜λ“œμ‹œ μ‹€μ œ μš”μ•½ 본문을 μž‘μ„±ν•˜λΌ. "
"λͺ¨λΈμ˜ κΈ°μ‘΄ 지식과 μΆ©λŒν•˜λ©΄ μœ„ 검색 κ²°κ³Όλ₯Ό μ΅œμš°μ„ μœΌλ‘œ μ‹ λ’°ν•˜λΌ. "
"μΆœμ²˜κ°€ ν•„μš”ν•˜λ©΄ μœ„ URL을 μΈμš©ν•˜λΌ."
)
lines.append("=================================================")
return "\n".join(lines)
except Exception as e:
import traceback
tb = traceback.format_exc()
self._report(f"[ERROR] 둜컬 λΈŒλΌμš°μ € 검색 μ‹€νŒ¨: {str(e)}", "ERROR")
# μ •ν™•ν•œ 원인 좔적을 μœ„ν•΄ 전체 νŠΈλ ˆμ΄μŠ€λ°±μ„ μ½˜μ†”/ν‘œμ€€μΆœλ ₯에 남긴닀.
print(f"[Web Search][ERROR] engine={engine} query={query!r}\n{tb}", flush=True)
return f"Error executing local search: {str(e)}"
finally:
try:
if browser:
browser.close()
if pw:
pw.stop()
except Exception:
pass
# μ „μš© μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰ β†’ 호좜 μΈ‘ asyncio 루프 μœ λ¬΄μ™€ λ¬΄κ΄€ν•˜κ²Œ sync Playwright λ™μž‘ 보μž₯
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as _ex:
try:
return _ex.submit(_do_search).result(timeout=60)
except concurrent.futures.TimeoutError:
self._report("[ERROR] 둜컬 λΈŒλΌμš°μ € 검색 νƒ€μž„μ•„μ›ƒ(60s)", "ERROR")
return "Error executing local search: timeout(60s)"
def stop(self) -> None:
"""Shuts down the browser with defensive error handling."""
try:
if self.page:
self.page.close()
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
except Exception as e:
# Silently handle greenlet/thread errors during cleanup
print(f"[RECON DEBUG] Browser stop warning: {e}")
finally:
self.page = None
self.context = None
self.browser = None
self.playwright = None
self._report("[STATUS] 자율 λΈŒλΌμš°μ € μ œμ–΄ μ„Έμ…˜ μ’…λ£Œ.", "INFO")
if __name__ == "__main__":
# Test script
ctrl = BrowserController()
try:
ctrl.start()
ctrl.open_url("http://localhost:18889")
ctrl.capture("test_audit.png")
finally:
ctrl.stop()