-
Notifications
You must be signed in to change notification settings - Fork 0
Snapshot与Ref
这页解决:怎么把页面变成 LLM 能读的东西,以及怎么让模型稳定地"指到"某个元素。
snapshot() 把页面压成一棵无障碍树,每个可交互元素带一个 [ref];
snap.ref("e1") 把它解析回一个能点能输入的元素。
snap = s.snapshot()
print(snap.text())
# RootWebArea "Checkout"
# heading "Your order"
# textbox "Card number" [e1]
# button "Pay now" [e2]
s.type(snap.ref("e1"), "4242 4242 4242 4242")
s.click(snap.ref("e2")) # 真实 isTrusted 事件,走拟人轨迹 + 双重命中校验关键:snap.ref(...) 返回的东西满足 ElementLike 协议,直接喂给现有的
click/type——拟人轨迹、两次命中校验、真实 CDP 输入一律照旧,输入链一行代码都没改。
snapshot(*, max_depth: int | None = None, cross_frame: bool = True) -> Snapshot| 参数 | 作用 |
|---|---|
max_depth |
树的最大深度(含下钻进 frame 的深度)。深页用它压上下文 |
cross_frame |
是否合并子 frame,默认合并 |
Snapshot 上:text() 渲染成文本 · refs 全部 ref 列表 · ref(r) 解析 · root 根节点 ·
generation 取快照时的 loaderId。
只有可交互 role 才发 ref——省得模型上下文里全是噪声:
button link textbox searchbox combobox listbox checkbox radio
menuitem menuitemcheckbox menuitemradio tab switch slider
spinbutton option treeitem textfield
标题、段落、表格这类结构性节点会出现在树里(给模型上下文),但不发 ref。
内部用 (loaderId, backendNodeId) 复合键(对齐 Google chrome-devtools-mcp 的设计):
- 同一节点跨多次快照 → 同一个 ref。多轮 agent 循环不会每次都换编号。
-
页面一导航 → 全部失效。
snap.ref("e1")抛StaleRef,提示重新拍快照。
snap = s.snapshot()
s.open("https://other.example")
snap.ref("e1") # StaleRef: from a stale snapshot (page navigated since)这是有意的:绝不静默降级去点"此刻恰好占着那个位置"的另一个元素。
不同 frame 里恰好相同的 backendNodeId(OOPIF 子 session 各自编号)会拿到不同的 ref。
原始 AX 树对模型来说噪声太多,输出前做三件事:
-
InlineTextBox永远去掉 —— 那是最底层文本几何,对模型零价值。 -
与父节点同名的
StaticText折叠 —— 按钮/标题/链接的文字标签,不重复占一行。 -
无语义容器上提 ——
generic/none/无 role 且无 ref 无名的节点,把子节点提上来,自己不占行。
效果对比:
蒸馏前 蒸馏后
RootWebArea "T" RootWebArea "T"
group group
heading "Head" heading "Head"
StaticText "Head" button "Go" [e1]
InlineTextBox "Head" textbox "search" [e2]
button "Go" [e1]
StaticText "Go"
InlineTextBox "Go"
textbox "search" [e2]
不做 token 计数:三个成熟实现(playwright-mcp / chrome-devtools-mcp / browser-use)
都不在服务端算 token,而是用"结构蒸馏 + depth 上限 + 只给可交互节点 ref"。这里照做——
省一个 tokenizer 依赖,也不和某家分词器耦合。要压上下文就用 max_depth。
cross_frame=True(默认)会把子 frame 的内容并进同一棵树,同进程 iframe 和跨源 OOPIF 都算:
RootWebArea "P"
group
button "ParentBtn" [e1]
Iframe
group
button "ChildBtn" [e2] ← iframe 里的元素,照样有 ref
textbox "childinput" [e3]
s.click(snap.ref("e2")) 直接点进 iframe 里的按钮,不用手动切 frame。
两种 frame 的底层机制不同(同进程复用主 session 的 backendNodeId;OOPIF 走它自己 attach 出来的子 session + 父页偏移),但在树里没有区别。细节见 Frame与ShadowDOM。
取不到的子 frame 当叶子处理,不崩——比如 OOPIF 还没就绪或 attach 失败时,
那个 Iframe 节点就是空的,不会伪装成"里面没东西"。
snapshot() |
parse() |
|
|---|---|---|
| 用途 | 给模型看、按 ref 交互 | 批量只读抓数据 |
| 内容 | 无障碍语义(role + name) | 完整 DOM |
| 能交互 | ✅ | ❌ |
| 查询方式 | 遍历树 / find
|
CSS / XPath |
| 速度 | 一次 AX 抓取 | 一次 HTML 抓取(批量读快 ~18x) |
见 数据提取。
下一步 → MCP与Agent网关(把这套暴露给 LLM)· Frame与ShadowDOM