数据截至 (上游 commit 911b51b0d148)
动作体系与执行:从一个 ID 到 Playwright 落地
30 秒导读: 上一章讲了主循环怎么「决定下一步做什么」。本章只讲那之后的一小段路——LLM 最终吐出的一个动作(比如
{type: "click", id: "B1"}),是怎么被 Notte 类型化成一个强类型对象、把那个短短的B1解析回页面上真实的 DOM 元素、再翻译成一句 Playwright 调用(page.locator(...).click())可靠落地的。
本章承接 02-agent-loop.md 结尾的「单步执行」——主循环把动作交给 session.execute 之后,控制权就进入本章描述的这条流水线。凭据替换(把占位符换成真密码)和结构化抓取属于 04-enterprise.md,本章只在流程图上标出它们的位置,不展开。
1. 这是什么(零基础也能懂)
先建立一个画面
想象 LLM 看着页面,最后只说了一句话:
"点 id 为
B1的那个元素。"
它不知道、也不关心 B1 在真实 HTML 里对应哪个 <button>、CSS 选择器长什么样、是不是藏在 shadow DOM 里。它只知道一个短 ID——因为感知层在把页面喂给它时,就是用这种「带 ID 的动作空间」把复杂 DOM 压缩成一张清单的。
于是就有了一个翻译问题:
LLM 说的 浏览器能听懂的
───────────── ──────────────────────────
{type:"click", id:"B1"} → page.locator("xpath=//button[...]").click()
本章讲的就是这台翻译机。 它要干三件事:
| 步骤 | 白话 | 负责的模块 |
|---|---|---|
| ① 类型化 | 把一坨 JSON 变成一个强类型、可校验的动作对象 | notte-core/actions/actions.py |
| ② 解析 | 把短 ID B1 查回真实元素的选择器 | notte-browser/resolution.py |
| ③ 执行 | 按动作种类分派到对应的 Playwright 调用 | notte-browser/controller.py |
一句话直觉
把它想成餐厅点单:LLM 是顾客,只说「我要 3 号套餐」(id);resolution.py 是收银台,把「3 号」查回后厨真正要做的菜谱(selectors);controller.py 是厨房,按菜谱(动作类型)拿对应的锅具(Playwright API)真正做出来。顾客永远不碰锅。
2. 顶层全景(它大概怎么转)
一张图看懂整条流水线
下面这张图从上到下就是一次 session.execute(...) 的完整生命周期。怎么读:从上往下是时间顺序,每个方框标了负责它的文件。
LLM 输出 / SDK 调用
{type:"click", id:"B1", ...}
│
▼
┌─────────────────────────────┐
│ parse_action │ 把 JSON / kwargs 变成
│ (session.py) │ 强类型 BaseAction 子类
└─────────────────────────────┘
│ ClickAction(id="B1", selector=None)
▼
┌─────────────────────────────┐
│ ① 解析 NodeResolutionPipe │ 用 snapshot 的 selector_map
│ .forward (resolution.py) │ 把 id="B1" 查回 selectors
└─────────────────────────────┘
│ id 不在页面 → InvalidActionError
│ 命中 → action.selector = NodeSelectors(...)
▼
┌─────────────────────────────┐
│ (凭据替换 · 见第4章) │ fill 类动作才走这步
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ ③ 执行 BrowserController │ match/case 按动作种类分派
│ .execute (controller.py) │
└──────────────┬──────────────┘
┌─────────┴──────────┐
▼ ▼
interaction 动作 browser 动作
locate_element → goto / scroll /
locator.click() wait / press_key ...
(dom/locate.py) (直接调 page.*)
│
▼
┌─────────────────────────────┐
│ 包成 ExecutionResult │ success / message / data
│ (session.py) │ → 回到主循环给裁判验证
└─────────────────────────────┘
部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
BaseAction 及其子类 | 全部动作的强类型定义 + 自注册表 | notte-core/src/notte_core/actions/actions.py |
NodeResolutionPipe | 把 id 解析成 selectors(或报错) | notte-browser/src/notte_browser/resolution.py |
BrowserController | 按动作种类 match/case 分派到 Playwright | notte-browser/src/notte_browser/controller.py |
locate_element | 把 NodeSelectors 落成一个 Playwright Locator | notte-browser/src/notte_browser/dom/locate.py |
NotteSession.aexecute | 串起解析→执行→打包,产出 ExecutionResult | notte-browser/src/notte_browser/session.py |
3. Action 类型体系:一坨 JSON 如何变成强类型对象
本节讲:Notte 把「所有可能的动作」建成一棵 pydantic 类树,并让每个动作类在定义时就自己注册进一张表。