智能体从未见过它即将点击的那个页面。scrape_with_actions 却仍要求它一次性写出最多 20 个动作 — 在 #user 里输入、点击 .btn-primary、等待 1000 毫秒 — 然后在调用返回时关闭浏览器。第 3 步猜错,链条就停在那里,结果返回 success: false,而那 5 credits 已经花掉了。
CrawlForge MCP v6.6.0 修的不是猜测,而是这个循环。新的 browser_session 工具 — 我们的第 31 个 — 让一个真实的浏览器页面在多次工具调用之间保持存活,于是智能体可以打开页面、看一眼、针对看到的内容动手、再看一眼、然后读取结果。「看」才是关键的那一步,它有个名字:snapshot。
目录
- 本次发布了什么
- 为什么一次性链条是盲目的
- Snapshot:智能体先看后动
- 完整的会话循环
- ref 失败时会大声报错,绝不悄无声息
- snapshot 也进了 scrape_
with_ actions - 一次会话的成本
- 限制,说清楚
- 从 CLI 跑一次会话
- 什么时候用哪个
- 如何升级
本次发布了什么
v6.6.0 就是一个工具加一个原语:
browser_session— 单个工具,带一个operation枚举:open、snapshot、act、read、screenshot、close、list。页面、它的 cookies 和登录状态会在多次调用之间存活,直到会话过期。snapshot— 实时页面的无障碍风格树,其中每个可交互元素都带一个稳定的 ref(@e1、@e2)。任何动作的selector都可以用 ref 代替 CSS 选择器。- 工具数量从 30 增加到 31,两个接口都有:MCP 与 REST API。
- 同时发布的还有
crawlforge-browser-sessions智能体 skill 和crawlforge browserCLI 命令。
没有任何重命名,现有工具的形状和价格都没变,因此这是一次可直接替换的升级。
为什么一次性链条是盲目的
scrape_with_actions 是个好工具,但有一个结构性限制:动作数组必须在任何东西加载之前写好。智能体挑 #login-email、input[name="email"] 还是 .form-field:first-child,靠的是对登录表单通常长什么样的记忆,而不是这个登录表单。
其中一个猜测落空时,ActionExecutor 会中止链条 — continueOnError 默认为 false — 并且工具返回一个带 success: false 和错误信息的结果,而不是抛出异常。计费跟随调用而非结果,所以这 5 credits 花在了一条只走到第三个动作的链条上。
问题不在成本,而在于智能体的下一步只能再猜一次,掌握的信息并不比第一次多。一次性工具没法告诉它页面里有什么,因为结果到手时浏览器已经不在了。
Snapshot:智能体先看后动
snapshot 遍历实时 DOM,为每个有意义的节点输出一行缩进文本,参照无障碍树的模型给出角色、可访问名称,并为可交互节点给出 ref:
[document] "Sign in"
@e1 [textbox] "Email"
@e2 [textbox] "Password"
@e3 [button] "Sign in"
[link] "Forgot password?"有两点让它真正有用而不只是好看。结构性节点(标题、地标、表单)作为上下文出现,但不会拿到 ref,因为它们不是操作目标。而每个 ref 背后都有遍历过程中打在元素上的 data-cf-ref 属性,所以 @e1 会解析成普通的 [data-cf-ref="e1"] 选择器,天然适配现有的每一条动作路径 — 包括接收原始选择器字符串的 stealth 浏览器拟人行为代码。
我们自己写了这套遍历,而没有包装 Playwright 的 ariaSnapshot()(它输出的 YAML 完全没有元素 ref),也没有用 page._snapshotForAI()(私有 API,我们不会依赖)。
interactive_only 默认为 true,max_nodes 默认为 200,因此一个有 4000 个元素的应用页面返回的是智能体真的读得完的列表。
完整的会话循环
下面是一次登录加一次读取,使用 crawlforge-sdk 包。五次调用,一个浏览器页面,一套 cookies。
// npm install crawlforge-sdk
import { CrawlForge } from 'crawlforge-sdk';
const client = new CrawlForge({ apiKey: process.env.CRAWLFORGE_API_KEY });
// 1. Open the session (3 credits). One API key may hold one session at a time.
const opened = await client.browserSession({
operation: 'open',
url: 'https://app.example.com/login',
ttl: 600
});
const { sessionId } = opened.data as { sessionId: string };
try {
// 2. Look before acting (1 credit). Every interactive element comes back
// with a stable ref, so the next call targets what is actually there.
const seen = await client.browserSession({ operation: 'snapshot', session_id: sessionId });
console.log((seen.data as { snapshot: { tree: string } }).snapshot.tree);
// 3. Act on those refs in a separate call (1 credit) — same page, same cookies.
await client.browserSession({
operation: 'act',
session_id: sessionId,
actions: [
{ type: 'type', selector: '@e1', text: 'user@example.com' },
{ type: 'type', selector: '@e2', text: 'secret123' },
{ type: 'click', selector: '@e3' },
{ type: 'wait', duration: 1000 }
]
});
// 4. Read the page the login landed on (2 credits) — the live DOM with the
// session's cookies, not a fresh fetch of the URL.
const page = await client.browserSession({
operation: 'read',
session_id: sessionId,
formats: ['markdown']
});
const { title, content } = page.data as { title: string; content: { markdown: string } };
console.log(title, content.markdown.length);
} finally {
// 5. Close it rather than waiting for the TTL (1 credit).
await client.browserSession({ operation: 'close', session_id: sessionId });
}第 4 步值得多说一句。read 是从会话正握着的那个页面里提取内容,而不是重新请求它的 URL — 重新请求会缺少 cookies,也不包含会话已经点过的一切,而那正是拥有一个会话的全部意义。格式有 markdown、html、text 和 json,一次调用可以同时要多个。
ref 失败时会大声报错,绝不悄无声息
导航会让 ref 失效。这不是注意事项,而是设计:一个跨页面存活下来的 ref 会指向新文档里恰好排第三的元素,而智能体会在毫不知情的情况下点下去。
因此 ref 表存放在以页面为键的 WeakMap 里,并在每次主框架导航时清空。对失效的 ref 动手,你拿到的是原因和解法,而不是一团迷雾:
Stale element ref @e3: the page navigated since the last snapshot —
take a new snapshot before acting on refs.未知 ref 同样直白:"Unknown element ref @e9: the current snapshot has 4 refs (@e1-@e4) — take a new snapshot." 智能体看到这两种消息都能自行处理。悄无声息地点错元素,正是我们花设计预算去避免的那种失败。
snapshot 也进了 scrape_with_actions
想先看一眼,并不一定需要会话。snapshot 同时作为动作类型进入了 scrape_with_actions,所以一次性链条也可以先观察,再针对自己的 snapshot 产出的 ref 动手,全都在同一次 5 credits 的调用里:
{
"url": "https://news.ycombinator.com/login",
"actions": [
{ "type": "snapshot" },
{ "type": "type", "selector": "@e1", "text": "reader" },
{ "type": "click", "selector": "@e3" }
]
}这覆盖了常见情形:智能体需要看页面,但整个流程仍然装得进一次调用。当装不下时再动用会话 — 后面的决策取决于前一步返回了什么,或者登录状态必须跨多次读取保持。
一次会话的成本
browser_session 按操作计费,因为一次会话就是多次调用,统一定价会让每个便宜的操作都按上限收费:
| 操作 | Credits | 作用 |
|---|---|---|
open | 3 | 启动浏览器上下文并导航 |
read | 2 | 把实时 DOM 提取为你要的格式 |
snapshot | 1 | 对已打开页面注入一次遍历 |
act | 1 | 对已打开页面执行最多 20 个动作 |
screenshot | 1 | PNG 或 JPEG,整页或单个元素 |
close | 1 | 释放页面及其上下文 |
list | 1 | 你打开的会话及其计时器 |
上面那个登录流程是 8 credits:3 + 1 + 1 + 2 + 1。加上登录后导航所需要的那次重新 snapshot 就是 9。同样的事情用 scrape_with_actions 是 5 — 而面对一个从未见过的表单,这 5 个更可能白花。
工具参考把 browser_session 标为统一的 3 credits。那是 open 的价格,而且是上限而非费率:REST 路由在还读不到请求体时先预留 3,调用成功后再扣除该操作的真实成本。你付的永远不会超过公布的数字,通常还更少。credits 与其他工具共用同一份额度 — 见套餐与 credits 包。
限制,说清楚
一次会话占用的是真实基础设施,所以限制也是实打实的:
- 它会过期。
ttl默认从open起算 600 秒(范围 30-3600),activity_ttl默认自上次使用起 300 秒(范围 10-3600),以先到者为准。用完就关,别等它自己过期。 - 托管 API 上一次只能开一个。 一个 REST API key 只能持有一个会话;stdio 和自托管安装保留默认的三个。这是算术而非谨慎:一个客户开三个会话就会占满整个托管会话容量。
- 托管接口上没有
executeJavaScript。 在我们基础设施的浏览器里跑任意脚本,和在你自己笔记本上跑同一段脚本是两回事,所以远程传输上一律拒绝。改用click、type、select和press,或者通过 stdio 在本地运行 MCP server。 - 每次导航都会重新过闸。 长期会话是一个可重复的导航原语,因此会话内的每次
navigate都会重新经过 SSRF 防护、主机黑名单和 robots.txt 检查。开一个会话并不等于买到一次免检跳转。 - 登录状态不跨会话保留。 登录只在同一个会话内有效。没有从一个会话带到下一个会话的已保存配置文件。
从 CLI 跑一次会话
CLI 每次调用跑完整的一个会话 — open、snapshot、你的步骤、read、close — 因为会话活在打开它的那个进程里,而 CLI 进程在命令结束时就结束了:
# steps.json: [{"operation":"act","actions":[{"type":"click","selector":"@e2"}]}]
crawlforge browser https://news.ycombinator.com \
--steps steps.json \
--read --format markdown当会话必须比打开它的那次调用活得更久时,请用 MCP 工具或 REST API。并不存在返回一个 id 的 crawlforge browser open:那个 id 背后的页面会在进程退出时死掉。
什么时候用哪个
scrape_with_actions | browser_session | |
|---|---|---|
| 浏览器生命周期 | 一次调用 | 跨调用,直到 TTL |
| 选择器 | 事先写好,未曾见过 | 来自你读过的 snapshot 的 ref |
| 价格 | 统一 5 | 开启 3,之后每次 1-2 |
| 选错选择器的代价 | 整次 5 credits 的调用 | 那次 act 的 1 credit |
| 登录 | 每次调用重来 | 付一次,由会话保持 |
| 适合 | 可以事先写下来的流程 | 必须边看边走的流程 |
两者都不取代 scrape(2 credits):对于无需交互就能渲染的页面,它仍然是正确答案。而 browser_session 最适合目标是应用而非文档的团队:登录后的仪表盘、多步向导、点四次才出现的筛选结果。
如何升级
npm install -g crawlforge-mcp-server@latest
crawlforge --version # 6.6.0如果你的 MCP 客户端用 npx 启动服务器,下次重启就会用上 v6.6.0。现有工具的 schema、输出形状和费用都没有变化。完整的版本历史见changelog。
想要一个先看后点的智能体?免费开始,赠送 1,000 credits — 足够 125 次完整的登录加读取会话 — 并阅读 browser_