# -*- coding: utf-8 -*-
"""
CDP 协议级网络抓包脚本（在 browser-use 的 python 会话里运行）。

为什么用 CDP 而不是页内注入 hook：
  页内注入的 XHR/fetch 拦截器活在「页面」里，整页跳转 / location.reload() 会把它清空；
  CDP（Chrome DevTools Protocol）活在「浏览器」里，跨页面跳转依然有效，能抓到「页面加载时
  自动发出的接口」——而这些往往正是喂数据给表格 / 列表的关键接口。

用法（由 SKILL.md 指挥，通过环境变量传参）：
  CAPTURE_OUT    输出目录（默认 /tmp/api_capture），抓到的响应体逐个落盘 + manifest.json 索引
  CAPTURE_NAV    可选，要导航到的 URL；不传则对当前页 reload 触发接口
  CAPTURE_WAIT   触发后等待秒数（默认 8），接口慢就调大
  CAPTURE_FILTER 可选，只保存 url 含该子串的接口；不传则保存所有 XHR/Fetch

运行方式：
  CAPTURE_OUT=/path/out CAPTURE_WAIT=8 browser-use python --file scripts/capture.py
"""
import asyncio, os, json, base64, re

sess = browser._session          # noqa: F821  (browser 由 browser-use 注入)
cdp = sess.cdp_client

OUT = os.environ.get("CAPTURE_OUT", "/tmp/api_capture")
NAV = os.environ.get("CAPTURE_NAV", "").strip()
WAIT = float(os.environ.get("CAPTURE_WAIT", "8"))
FILTER = os.environ.get("CAPTURE_FILTER", "").strip()
os.makedirs(OUT, exist_ok=True)


async def run():
    cs = await sess.get_or_create_cdp_session()
    sid = cs.session_id
    meta = {}  # requestId -> {url, method, postData, status, type, mime}

    def on_req(p, session_id=None):
        rid = p.get("requestId"); req = p.get("request", {})
        m = meta.setdefault(rid, {})
        m["url"] = req.get("url"); m["method"] = req.get("method"); m["postData"] = req.get("postData")

    def on_resp(p, session_id=None):
        rid = p.get("requestId"); r = p.get("response", {})
        m = meta.setdefault(rid, {})
        m["url"] = r.get("url"); m["status"] = r.get("status")
        m["type"] = p.get("type"); m["mime"] = r.get("mimeType")

    cdp.register.Network.requestWillBeSent(on_req)
    cdp.register.Network.responseReceived(on_resp)

    # 关键 1：把缓冲区调大，否则响应体会被 "evicted from inspector cache"
    await cdp.send.Network.enable(
        params={"maxResourceBufferSize": 50_000_000, "maxTotalBufferSize": 300_000_000},
        session_id=sid,
    )
    await cdp.send.Page.enable(session_id=sid)

    # 关键 2：注册回调、触发、等待必须在同一个 await 协程里，事件循环才会持续运转把事件派发进回调
    if NAV:
        await cdp.send.Page.navigate(params={"url": NAV}, session_id=sid)
    else:
        await cdp.send.Page.reload(params={"ignoreCache": True}, session_id=sid)
    await asyncio.sleep(WAIT)

    manifest = []
    idx = 0
    for rid, m in meta.items():
        url = m.get("url") or ""
        if m.get("type") not in ("XHR", "Fetch"):
            continue
        if FILTER and FILTER not in url:
            continue
        body, err = None, None
        try:
            b = await cdp.send.Network.getResponseBody(params={"requestId": rid}, session_id=sid)
            body = b.get("body")
            if b.get("base64Encoded") and body:
                try:
                    body = base64.b64decode(body).decode("utf-8", "replace")
                except Exception:
                    pass
        except Exception as e:
            err = str(e)[:80]
        idx += 1
        stem = re.sub(r"[^a-zA-Z0-9]+", "_", (url.split("?")[0].split("/")[-1] or "root"))[:40]
        fn = os.path.join(OUT, f"{idx:03d}_{stem}.json")
        if body is not None:
            with open(fn, "w") as f:
                f.write(body)
        manifest.append({
            "idx": idx, "method": m.get("method"), "status": m.get("status"),
            "url": url, "postData": (m.get("postData") or "")[:2000],
            "file": os.path.basename(fn) if body is not None else None,
            "len": len(body) if body else 0, "error": err,
        })

    with open(os.path.join(OUT, "manifest.json"), "w") as f:
        json.dump(manifest, f, ensure_ascii=False, indent=2)
    return manifest


M = browser._run(run())  # noqa: F821
print(f"captured {len(M)} XHR/Fetch -> {OUT}")
for m in M:
    flag = "OK " if m["file"] else ("ERR" if m["error"] else "-- ")
    print(flag, m["method"], m["status"], m["len"], (m["url"] or "")[:110])
