项目文件夹

文件
Saurav Panda 2ce5b870c1 helpers: use certifi CA bundle for http_get so it works on python.org macOS builds
http_get called urllib.request.urlopen with no explicit SSL context, so
it fell back to whatever trust store the interpreter shipped with. On
python.org's Python 3.11 for macOS the CA keychain is empty until the
user runs Install Certificates.command, which nobody does — every HTTPS
call raised CERTIFICATE_VERIFY_FAILED. Agents were then writing
domain-skills that suggested subprocess curl or, worse, disabling TLS
verification entirely.

Load certifi's bundle explicitly via ssl.create_default_context. Same
approach requests and httpx use, works across macOS/Linux/Windows and
across Python installers.
2026-04-18 21:39:04 -07:00

197 行
8.0 KiB
Python

"""Browser control via CDP. Read, edit, extend -- this file is yours."""
import base64, json, os, socket, time, urllib.request
from pathlib import Path
from urllib.parse import urlparse
def _load_env():
p = Path(__file__).parent / ".env"
if not p.exists():
return
for line in p.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
_load_env()
NAME = os.environ.get("BU_NAME", "default")
SOCK = f"/tmp/bu-{NAME}.sock"
INTERNAL = ("chrome://", "chrome-untrusted://", "devtools://", "chrome-extension://", "about:")
def _send(req):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(SOCK)
s.sendall((json.dumps(req) + "\n").encode())
data = b""
while not data.endswith(b"\n"):
chunk = s.recv(1 << 20)
if not chunk: break
data += chunk
s.close()
r = json.loads(data)
if "error" in r: raise RuntimeError(r["error"])
return r
def cdp(method, session_id=None, **params):
"""Raw CDP. cdp('Page.navigate', url='...'), cdp('DOM.getDocument', depth=-1)."""
return _send({"method": method, "params": params, "session_id": session_id}).get("result", {})
def drain_events(): return _send({"meta": "drain_events"})["events"]
# --- navigation / page ---
def goto(url):
r = cdp("Page.navigate", url=url)
d = (Path(__file__).parent / "domain-skills" / (urlparse(url).hostname or "").removeprefix("www.").split(".")[0])
return {**r, "domain_skills": sorted(p.name for p in d.rglob("*.md"))[:10]} if d.is_dir() else r
def page_info():
"""{url, title, w, h, sx, sy, pw, ph} — viewport + scroll + page size."""
r = cdp("Runtime.evaluate",
expression="JSON.stringify({url:location.href,title:document.title,w:innerWidth,h:innerHeight,sx:scrollX,sy:scrollY,pw:document.documentElement.scrollWidth,ph:document.documentElement.scrollHeight})",
returnByValue=True)
return json.loads(r["result"]["value"])
# --- input ---
def click(x, y, button="left", clicks=1):
cdp("Input.dispatchMouseEvent", type="mousePressed", x=x, y=y, button=button, clickCount=clicks)
cdp("Input.dispatchMouseEvent", type="mouseReleased", x=x, y=y, button=button, clickCount=clicks)
def type_text(text):
cdp("Input.insertText", text=text)
_KEYS = { # key → (windowsVirtualKeyCode, code, text)
"Enter": (13, "Enter", "\r"), "Tab": (9, "Tab", "\t"), "Backspace": (8, "Backspace", ""),
"Escape": (27, "Escape", ""), "Delete": (46, "Delete", ""), " ": (32, "Space", " "),
"ArrowLeft": (37, "ArrowLeft", ""), "ArrowUp": (38, "ArrowUp", ""),
"ArrowRight": (39, "ArrowRight", ""), "ArrowDown": (40, "ArrowDown", ""),
"Home": (36, "Home", ""), "End": (35, "End", ""),
"PageUp": (33, "PageUp", ""), "PageDown": (34, "PageDown", ""),
}
def press_key(key, modifiers=0):
"""Modifiers bitfield: 1=Alt, 2=Ctrl, 4=Meta(Cmd), 8=Shift.
Special keys (Enter, Tab, Arrow*, Backspace, etc.) carry their virtual key codes
so listeners checking e.keyCode / e.key all fire."""
vk, code, text = _KEYS.get(key, (ord(key[0]) if len(key) == 1 else 0, key, key if len(key) == 1 else ""))
base = {"key": key, "code": code, "modifiers": modifiers, "windowsVirtualKeyCode": vk, "nativeVirtualKeyCode": vk}
cdp("Input.dispatchKeyEvent", type="keyDown", **base, **({"text": text} if text else {}))
if text and len(text) == 1:
cdp("Input.dispatchKeyEvent", type="char", text=text, **{k: v for k, v in base.items() if k != "text"})
cdp("Input.dispatchKeyEvent", type="keyUp", **base)
def scroll(x, y, dy=-300, dx=0):
cdp("Input.dispatchMouseEvent", type="mouseWheel", x=x, y=y, deltaX=dx, deltaY=dy)
# --- visual ---
def screenshot(path="/tmp/shot.png", full=False):
r = cdp("Page.captureScreenshot", format="png", captureBeyondViewport=full)
open(path, "wb").write(base64.b64decode(r["data"]))
return path
# --- tabs ---
def list_tabs(include_chrome=True):
out = []
for t in cdp("Target.getTargets")["targetInfos"]:
if t["type"] != "page": continue
url = t.get("url", "")
if not include_chrome and url.startswith(INTERNAL): continue
out.append({"targetId": t["targetId"], "title": t.get("title", ""), "url": url})
return out
def current_tab():
t = cdp("Target.getTargetInfo").get("targetInfo", {})
return {"targetId": t.get("targetId"), "url": t.get("url", ""), "title": t.get("title", "")}
def switch_tab(target_id):
cdp("Target.activateTarget", targetId=target_id)
sid = cdp("Target.attachToTarget", targetId=target_id, flatten=True)["sessionId"]
_send({"meta": "set_session", "session_id": sid})
return sid
def new_tab(url="about:blank"):
tid = cdp("Target.createTarget", url=url)["targetId"]
switch_tab(tid)
return tid
def ensure_real_tab():
"""Switch to a real user tab if current is chrome:// / internal / stale."""
tabs = list_tabs(include_chrome=False)
if not tabs:
return None
try:
cur = current_tab()
if cur["url"] and not cur["url"].startswith(INTERNAL):
return cur
except Exception:
pass
switch_tab(tabs[0]["targetId"])
return tabs[0]
def iframe_target(url_substr):
"""First iframe target whose URL contains `url_substr`. Use with js(..., target_id=...)."""
for t in cdp("Target.getTargets")["targetInfos"]:
if t["type"] == "iframe" and url_substr in t.get("url", ""):
return t["targetId"]
return None
# --- utility ---
def wait(seconds=1.0):
time.sleep(seconds)
def wait_for_load(timeout=15.0):
"""Poll document.readyState == 'complete' or timeout."""
deadline = time.time() + timeout
while time.time() < deadline:
if js("document.readyState") == "complete": return True
time.sleep(0.3)
return False
def js(expression, target_id=None):
"""Run JS in the attached tab (default) or inside an iframe target (via iframe_target())."""
sid = cdp("Target.attachToTarget", targetId=target_id, flatten=True)["sessionId"] if target_id else None
r = cdp("Runtime.evaluate", session_id=sid, expression=expression, returnByValue=True, awaitPromise=True)
return r.get("result", {}).get("value")
_KC = {"Enter": 13, "Tab": 9, "Escape": 27, "Backspace": 8, " ": 32, "ArrowLeft": 37, "ArrowUp": 38, "ArrowRight": 39, "ArrowDown": 40}
def dispatch_key(selector, key="Enter", event="keypress"):
"""Dispatch a DOM KeyboardEvent on the matched element.
Use this when a site reacts to synthetic DOM key events on an element more reliably
than to raw CDP input events.
"""
kc = _KC.get(key, ord(key) if len(key) == 1 else 0)
js(
f"(()=>{{const e=document.querySelector({json.dumps(selector)});if(e){{e.focus();e.dispatchEvent(new KeyboardEvent({json.dumps(event)},{{key:{json.dumps(key)},code:{json.dumps(key)},keyCode:{kc},which:{kc},bubbles:true}}));}}}})()"
)
def upload_file(selector, path):
"""Set files on a file input via CDP DOM.setFileInputFiles. `path` is an absolute filepath (use tempfile.mkstemp if needed)."""
doc = cdp("DOM.getDocument", depth=-1)
nid = cdp("DOM.querySelector", nodeId=doc["root"]["nodeId"], selector=selector)["nodeId"]
if not nid: raise RuntimeError(f"no element for {selector}")
cdp("DOM.setFileInputFiles", files=[path] if isinstance(path, str) else list(path), nodeId=nid)
def http_get(url, headers=None, timeout=20.0):
"""Pure HTTP — no browser. Use for static pages / APIs. Wrap in ThreadPoolExecutor for bulk."""
import urllib.request, gzip, ssl, certifi
h = {"User-Agent": "Mozilla/5.0", "Accept-Encoding": "gzip"}
if headers: h.update(headers)
ctx = ssl.create_default_context(cafile=certifi.where())
with urllib.request.urlopen(urllib.request.Request(url, headers=h), timeout=timeout, context=ctx) as r:
data = r.read()
if r.headers.get("Content-Encoding") == "gzip": data = gzip.decompress(data)
return data.decode()