Spaces:
Running
Running
Download shadow_brain_core/brain/browser_controller.py from taemin1980/shadowbrain: direct link, hf CLI and curl.
- Browser
- Download file 31 kB
-
https://huggingface.co/spaces/taemin1980/shadowbrain/resolve/main/shadow_brain_core/brain/browser_controller.py
- Command line
-
hf download hf://spaces/taemin1980/shadowbrain/shadow_brain_core/brain/browser_controller.py
-
curl -L -o browser_controller.py https://huggingface.co/spaces/taemin1980/shadowbrain/resolve/main/shadow_brain_core/brain/browser_controller.py
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() | |