Files
Mark-XXXIX-OR/code_helper.py
T
2026-05-10 17:13:45 +03:00

520 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import threading
import concurrent.futures
import platform
import shutil
import subprocess
from pathlib import Path
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeout
def _get_default_browser_id() -> str:
"""Returns raw default browser identifier string for current OS."""
system = platform.system()
try:
if system == "Windows":
import winreg
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"
)
prog_id = winreg.QueryValueEx(key, "ProgId")[0].lower()
winreg.CloseKey(key)
return prog_id
elif system == "Darwin":
result = subprocess.run(
["defaults", "read",
"com.apple.LaunchServices/com.apple.launchservices.secure",
"LSHandlers"],
capture_output=True, text=True, timeout=5
)
return result.stdout.lower()
elif system == "Linux":
result = subprocess.run(
["xdg-settings", "get", "default-web-browser"],
capture_output=True, text=True, timeout=5
)
return result.stdout.lower()
except Exception:
pass
return ""
_BROWSER_BINARIES = {
"Windows": {
"opera": ["opera.exe"],
"brave": ["brave.exe"],
"vivaldi": ["vivaldi.exe"],
"chrome": ["chrome.exe"],
"firefox": ["firefox.exe"],
},
"Darwin": {
"opera": ["opera"],
"brave": ["brave browser", "brave"],
"vivaldi": ["vivaldi"],
"chrome": ["google chrome", "google-chrome"],
"firefox": ["firefox"],
},
"Linux": {
"opera": ["opera", "opera-stable"],
"brave": ["brave-browser", "brave"],
"vivaldi": ["vivaldi-stable", "vivaldi"],
"chrome": ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"],
"firefox": ["firefox"],
},
}
def _get_opera_executable() -> str | None:
if platform.system() != "Windows":
return None
try:
import winreg
candidate_keys = [
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\opera.exe",
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\launcher.exe",
r"SOFTWARE\Clients\StartMenuInternet\OperaStable\shell\open\command",
r"SOFTWARE\Clients\StartMenuInternet\OperaGXStable\shell\open\command",
]
for key_path in candidate_keys:
for hive in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
try:
key = winreg.OpenKey(hive, key_path)
val = winreg.QueryValue(key, None)
winreg.CloseKey(key)
exe = val.strip().strip('"').split('"')[0].split(" --")[0].strip()
if exe and Path(exe).exists():
print(f"[Browser] 🔍 Opera found via registry: {exe}")
return exe
except Exception:
continue
except Exception:
pass
return None
def _find_browser_executable(prog_id: str) -> tuple:
"""
Returns (engine_name, exe_path, channel, is_opera).
is_opera=True → extra args needed to prevent private-mode launch.
"""
system = platform.system()
os_bins = _BROWSER_BINARIES.get(system, {})
if any(x in prog_id for x in ["firefox", "mozilla"]):
return "firefox", None, None, False
if "safari" in prog_id:
return "webkit", None, None, False
if "edge" in prog_id:
return "chromium", None, "msedge", False
if "opera" in prog_id:
exe = _get_opera_executable()
if exe:
return "chromium", exe, None, True
for binary in os_bins.get("opera", []):
path = shutil.which(binary)
if path:
return "chromium", path, None, True
browser_patterns = {
"brave": ["brave"],
"vivaldi": ["vivaldi"],
"chrome": ["chrome"],
}
for browser_name, patterns in browser_patterns.items():
if not any(p in prog_id for p in patterns):
continue
binaries = os_bins.get(browser_name, [])
for binary in binaries:
path = shutil.which(binary)
if path:
print(f"[Browser] 🔍 Found {browser_name} at: {path}")
return "chromium", path, None, False
if "chrome" in prog_id or not prog_id:
return "chromium", None, "chrome", False
return "chromium", None, None, False
class _BrowserThread:
def __init__(self):
self._loop = None
self._thread = None
self._ready = threading.Event()
self._playwright = None
self._browser = None
self._context = None
self._page = None
self._engine_name = "chromium"
self._exe_path = None
self._channel = None
self._is_opera = False
def start(self):
if self._thread and self._thread.is_alive():
return
self._thread = threading.Thread(
target=self._run_loop, daemon=True, name="BrowserThread"
)
self._thread.start()
self._ready.wait(timeout=15)
def _run_loop(self):
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._loop.run_until_complete(self._init())
self._ready.set()
self._loop.run_forever()
async def _init(self):
self._playwright = await async_playwright().start()
def run(self, coro, timeout: int = 30):
if not self._loop:
raise RuntimeError("BrowserThread not started.")
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
return future.result(timeout=timeout)
# ── Tarayıcı ve sayfa yönetimi ───────────────────────────────────────────
async def _launch_browser_if_needed(self):
"""
Tarayıcıyı başlatır. Zaten açıksa hiçbir şey yapmaz.
Her zaman default tarayıcıyı kullanır, özel sekme açmaz.
"""
if self._browser and self._browser.is_connected():
return
prog_id = _get_default_browser_id()
self._engine_name, self._exe_path, self._channel, self._is_opera = _find_browser_executable(prog_id)
engine = getattr(self._playwright, self._engine_name)
# Temel chromium argümanları
chromium_args = ["--start-maximized"]
if self._is_opera:
# Opera GX bazı sürümlerde varsayılan olarak private modda başlar.
# Aşağıdaki flag'ler bunu engeller.
chromium_args += [
"--disable-features=OperaPrivacyMode",
"--no-private",
]
print("[Browser] 🎭 Opera detected — disabling private-mode flags")
launch_kwargs = {"headless": False}
if self._engine_name == "chromium":
launch_kwargs["args"] = chromium_args
if self._exe_path:
launch_kwargs["executable_path"] = self._exe_path
elif self._channel:
launch_kwargs["channel"] = self._channel
try:
self._browser = await engine.launch(**launch_kwargs)
print(
f"[Browser] ✅ Launched ({self._engine_name}"
f"{' / ' + self._channel if self._channel else ''}"
f"{' / ' + self._exe_path if self._exe_path else ''})"
)
except Exception as e:
print(f"[Browser] ⚠️ Launch failed ({e}), falling back to built-in Chromium")
self._browser = await self._playwright.chromium.launch(
headless=False,
args=["--start-maximized"]
)
async def _get_page(self):
"""
Mevcut sayfayı döndürür.
- Tarayıcı kapalıysa açar.
- Context yoksa oluşturur.
- Sayfa kapalıysa yeni sekme açar (aynı pencerede).
- Sayfa zaten açıksa aynı sayfayı döndürür (yeni pencere açmaz).
"""
await self._launch_browser_if_needed()
if self._context is None:
self._context = await self._browser.new_context(
viewport=None,
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
)
if self._page is None or self._page.is_closed():
self._page = await self._context.new_page()
return self._page
# ── Eylemler ─────────────────────────────────────────────────────────────
async def _go_to(self, url: str) -> str:
if not url.startswith("http"):
url = "https://" + url
page = await self._get_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=15000)
return f"Opened: {page.url}"
except PlaywrightTimeout:
return f"Timeout loading: {url}"
except Exception as e:
return f"Navigation error: {e}"
async def _search(self, query: str, engine: str = "google") -> str:
engines = {
"google": f"https://www.google.com/search?q={query.replace(' ', '+')}",
"bing": f"https://www.bing.com/search?q={query.replace(' ', '+')}",
"duckduckgo": f"https://duckduckgo.com/?q={query.replace(' ', '+')}",
}
url = engines.get(engine.lower(), engines["google"])
return await self._go_to(url)
async def _click(self, selector=None, text=None) -> str:
page = await self._get_page()
try:
if text:
await page.get_by_text(text, exact=False).first.click(timeout=8000)
return f"Clicked: '{text}'"
elif selector:
await page.click(selector, timeout=8000)
return f"Clicked: {selector}"
return "No selector or text provided."
except PlaywrightTimeout:
return "Element not found or not clickable."
except Exception as e:
return f"Click error: {e}"
async def _type(self, selector=None, text: str = "", clear_first: bool = True) -> str:
page = await self._get_page()
try:
element = page.locator(selector).first if selector else page.locator(":focus")
if clear_first:
await element.clear()
await element.type(text, delay=50)
return "Text typed."
except Exception as e:
return f"Type error: {e}"
async def _scroll(self, direction: str = "down", amount: int = 500) -> str:
page = await self._get_page()
try:
y = amount if direction == "down" else -amount
await page.mouse.wheel(0, y)
return f"Scrolled {direction}."
except Exception as e:
return f"Scroll error: {e}"
async def _press(self, key: str) -> str:
page = await self._get_page()
try:
await page.keyboard.press(key)
return f"Pressed: {key}"
except Exception as e:
return f"Key error: {e}"
async def _get_text(self) -> str:
page = await self._get_page()
try:
text = await page.inner_text("body")
return text[:4000] if len(text) > 4000 else text
except Exception as e:
return f"Could not get page text: {e}"
async def _fill_form(self, fields: dict) -> str:
page = await self._get_page()
results = []
for selector, value in fields.items():
try:
el = page.locator(selector).first
await el.clear()
await el.type(str(value), delay=40)
results.append(f"✓ {selector}")
except Exception as e:
results.append(f"✗ {selector}: {e}")
return "Form filled: " + ", ".join(results)
async def _smart_click(self, description: str) -> str:
page = await self._get_page()
desc_lower = description.lower()
role_hints = {
"button": ["button", "buton", "btn"],
"link": ["link", "bağlantı"],
"searchbox": ["search", "arama"],
"textbox": ["input", "field", "alan"],
}
for role, keywords in role_hints.items():
if any(k in desc_lower for k in keywords):
try:
await page.get_by_role(role).first.click(timeout=5000)
return f"Clicked ({role}): '{description}'"
except Exception:
pass
try:
await page.get_by_text(description, exact=False).first.click(timeout=5000)
return f"Clicked (text): '{description}'"
except Exception:
pass
try:
await page.get_by_placeholder(description, exact=False).first.click(timeout=5000)
return f"Clicked (placeholder): '{description}'"
except Exception:
pass
return f"Could not find: '{description}'"
async def _smart_type(self, description: str, text: str) -> str:
page = await self._get_page()
for method, locator in [
("placeholder", page.get_by_placeholder(description, exact=False)),
("label", page.get_by_label(description, exact=False)),
("role", page.get_by_role("textbox")),
]:
try:
el = locator.first
await el.clear()
await el.type(text, delay=50)
return f"Typed into ({method}): '{description}'"
except Exception:
continue
return f"Could not find input: '{description}'"
async def _close_browser(self) -> str:
if self._browser:
await self._browser.close()
self._browser = None
self._context = None
self._page = None
if self._playwright:
await self._playwright.stop()
self._playwright = None
return "Browser closed."
# ── Singleton browser thread ─────────────────────────────────────────────────
_bt = _BrowserThread()
_bt_started = False
_bt_lock = threading.Lock()
def _ensure_started():
global _bt_started
with _bt_lock:
if not _bt_started:
_bt.start()
_bt_started = True
# ── Public API ───────────────────────────────────────────────────────────────
def browser_control(
parameters: dict,
response=None,
player=None,
session_memory=None
) -> str:
"""
Browser controller — auto-detects and uses system default browser.
Always reuses the existing browser window/page; never opens incognito.
parameters:
action : go_to | search | click | type | scroll | fill_form |
smart_click | smart_type | get_text | press | close
url : URL for go_to
query : search query
engine : google | bing | duckduckgo (default: google)
selector : CSS selector for click/type
text : text to click or type
description : element description for smart_click/smart_type
direction : up | down for scroll
amount : scroll amount in pixels (default: 500)
key : key name for press (e.g. Enter, Escape, Tab)
fields : {selector: value} dict for fill_form
clear_first : bool, clear input before typing (default: True)
"""
_ensure_started()
action = (parameters or {}).get("action", "").lower().strip()
result = "Unknown action."
try:
if action == "go_to":
result = _bt.run(_bt._go_to(parameters.get("url", "")))
elif action == "search":
result = _bt.run(_bt._search(
parameters.get("query", ""),
parameters.get("engine", "google"),
))
elif action == "click":
result = _bt.run(_bt._click(
selector=parameters.get("selector"),
text=parameters.get("text"),
))
elif action == "type":
result = _bt.run(_bt._type(
selector=parameters.get("selector"),
text=parameters.get("text", ""),
clear_first=parameters.get("clear_first", True),
))
elif action == "scroll":
result = _bt.run(_bt._scroll(
direction=parameters.get("direction", "down"),
amount=parameters.get("amount", 500),
))
elif action == "fill_form":
result = _bt.run(_bt._fill_form(parameters.get("fields", {})))
elif action == "smart_click":
result = _bt.run(_bt._smart_click(parameters.get("description", "")))
elif action == "smart_type":
result = _bt.run(_bt._smart_type(
parameters.get("description", ""),
parameters.get("text", ""),
))
elif action == "get_text":
result = _bt.run(_bt._get_text())
elif action == "press":
result = _bt.run(_bt._press(parameters.get("key", "Enter")))
elif action == "close":
result = _bt.run(_bt._close_browser())
else:
result = f"Unknown action: {action}"
except concurrent.futures.TimeoutError:
result = "Browser action timed out."
except Exception as e:
result = f"Browser error: {e}"
print(f"[Browser] {result[:80]}")
if player:
player.write_log(f"[browser] {result[:60]}")
return result