search_web 标明所用后端,serp_rank 标记诱饵结果
6.21.0 配合 CrawlForge 从 Google Custom Search(Google 将于 2027-01-01 停用)迁移到 Brave Search API。search_web 现在会报告实际响应的后端;serp_rank 会标记看起来像诱饵的结果页:Google 已开始向部分自动化排名查询返回看似真实、却与查询无关的结果。升级说明:search_web 的结果来自 Brave 的独立索引,不再报告 total_results,related_searches 为空。6.21.1 修复了 search_web 在 Brave 上的分页问题:offset 不是 limit 的整数倍时会返回错误的结果,offset 为 90 且 limit 较小时返回空结果。
- 当前几条结果中提到关键词的不足五分之一时,serp_rank 会返回 suspect: true 和一条警告。2026-10-19 对 "crawlforge" 的一次查询,前 10 条结果没有一条提到它,而 Search Console 显示该网站排名第 1。仍为 5 credits
- search_web 将 provider.backend 报告为 Brave Search API,而不是写死的 Google Search;Creator Mode 也可以直接调用 Brave Search API
- .mcpb 包在 macOS 和 Windows 上可以读取 PDF:自 6.0.0 起每个包只带 Linux 原生二进制文件,process_document 因此报错 "DOMMatrix is not defined"。manifest 现在列出全部 31 个工具
浏览器操作遵守 Crawl-delay,本地文件路径不再离开本机
6.20.0 让浏览器工具与抓取工具同样守礼,并补上两个不易察觉的缺口。scrape_with_actions 和 browser_session 中的点击、按键、select 或复选框,现在会先等待所在页面 robots.txt 的 Crawl-delay 再执行,与 navigate 一致;同一时刻发往同一主机的请求会按完整的间隔排队。升级说明:scrape 每次调用最多只接受一个 highlights 格式和一个 question 格式。第二个会被拒绝且不收费,而以前它会覆盖第一个。
- scrape_with_actions 与 browser_session:click、press、select 或 check 会先等待页面所在主机的 Crawl-delay 再执行,因为浏览器会在任何检查之前就发出它引发的请求。navigate 和首次加载此前已有间隔
- 同时到达、发往设有 Crawl-delay 主机的请求,现在在 MCP server 和 REST API 上都会按完整间隔排队。以前,同一等待期间到来的请求会在同一时刻发出;map_site 和 generate_llms_txt 现在也会为站点地图抓取设置间隔
- process_document 会读取本地文件,其用量报告曾把完整路径发送到 crawlforge.dev。现在只报告文件类型,例如 [local file].pdf;网站会在接收时屏蔽旧版服务器发来的路径,已存储的记录也已清理
- scrape:同一次调用中的第二个 highlights 或 question 格式会作为校验错误被拒绝且不收费,而不再在照收 +1 积分附加费的同时悄悄覆盖第一个
十月实测发现的每个缺陷均已修复或作出决定
6.19.0 完成了 2026-10-03 对全部 31 个工具的实测。原先不起作用的参数现在要么生效、要么已移除,文本类工具不再把段落粘连在一起,错误会针对实际失败原因给出下一步,另有八个工具支持 max_inline_chars。升级说明:移除了 stealth_mode 的 enable 和 disable 以及 scrape_with_actions 的 captureScreenshots,browser_session 的 read 返回整个页面,并精简了 extract_content、search_web、map_site 和 scrape_with_actions 中若干过大的字段。
- Chromium 上的 stealth_mode 曾导致 Cloudflare 自己的验证无法加载:三个强制添加的请求头使每个跨域请求都发送 CORS 预检,而 challenges.cloudflare.com 会拒绝预检。现在验证可以加载,页面会被报告为被 cloudflare 拦截。6.19.1 也从本地化浏览器上下文中移除了同类强制请求头
- extract_text 的表格在 MCP server 和 REST API 上都是统一的网格,图片和链接 URL 均为绝对地址,由 crawlforge-extractors 1.17.0 共享实现;REST 的 scrape markdown 也保留表格
- reddit_search 的 comment_count 统计返回的全部评论,comments_collapsed 统计未返回的评论;serp_rank 说明目标按主机匹配、每次查询只是一个样本,REST API 返回 se_results_count 和 check_url。6.19.2 将两者的默认 depth 设为 30
- 错误会针对实际失败原因(robots、SSRF、404、超时或参数错误)给出下一步,analyze_content 新增十一种语言的停用词
extract_text 和 extract_links 可读取被拦截的页面,并与 REST API 保持一致
两个最便宜的页面读取工具获得了 scrape 的抓取阶梯。使用 escalate: true 时,被拦截的页面(已识别的厂商、403、429 或 444)会经由 scrape 所用的同一 stealth 阶段重新读取,预估 6 credits,普通抓取拿到页面时只收 1;404 或 5xx 绝不升级。不使用它时,失败信息现在会说明目标返回了什么,以及是哪家厂商拦截了它。PDF 或其他二进制文件会直接失败并指向 process_document,而不是返回乱码;JSON 响应体会附带一条说明返回;抓取超时为 15 秒,遇到较短的 Retry-After 会重试一次;客户端渲染的空壳页会以 rendered: false 标记。两个工具现在返回与 REST API 相同的内容:extract_text 按每个块级元素读取一行,并支持 selector 和 max_length;extract_links 在两个表面上返回同一种记录结构。升级说明:extract_links 的记录不再带有 is_external,请改读 type。6.18.1 修复了一次覆盖全部 31 个工具的实测所发现的问题:crawl_deep 在起始 URL 被拒绝时不再崩溃,并在达到 max_pages 时停止;map_site 无法读取起始 URL 时会报错;batch_scrape 会拒绝 PDF,而不是把其字节当作文本返回;scrape 会将 Fastly 的客户端挑战页识别为拦截。升级说明:crawl_deep 的 pages_crawled 现在只统计成功抓取的页面,track_changes 的 monitoringOptions.enabled: false 不再启动监控。
- 同一个 URL 经由 MCP server 和 REST API 返回相同的链接:在 Wikipedia、Hacker News 和 BBC News 的抓取快照上分别为 327、196 和 123 个链接,前 20 个完全一致
- 链接记录为 { href, text, type, domain, rel, original_href }:mailto:、tel: 和 javascript: 链接的 type 为 "other",会遵循 <base href>,去重时忽略片段和末尾斜杠
- scrape_with_actions 的 browserOptions.timeout 现在约束页面加载;超出上限的 scrape_with_actions 或 browser_session 结果会将其拦截报告保留在内联部分
- 6.18.1:在链接密集的站点上,max_pages: 3 的 crawl_deep 曾耗时 232 秒并处理了 2,290 个排队链接,现在达到上限即停止。formAutoFill 会勾选单选按钮和复选框并选择下拉选项,browser_session 的 act 以资源 URI 而非内联 base64 返回截图,agent 对限定在起始 URL 范围内的问题不再发起网页搜索
链路遇到的每道拦截都会被点名,动作链路也可使用你自己的代理
scrape_with_actions 现在会检查自己发起的每一次导航,而不只是最终页面:初始加载和每个 navigate 动作都会列在 navigations 下,附带其 HTTP 状态,遇到拦截时还会附带厂商名称。AWS WAF 的验证过渡页——也就是 Amazon 用来应答无头浏览器的那个 202 页面——现在在所有表面上都被标记为 aws-waf。stealth 链路可以通过 browserOptions.proxyRotation 经由调用方自己的住宅代理出站(仅限 Chromium;CrawlForge 不提供代理),凭据会从结果和用量报告中移除。在 Camoufox 上,按调用指定的代理会被拒绝:Camoufox 在多次调用之间共用一个浏览器,否则就会承载其他调用方的流量。6.17.1 还让 stealth 的 Cloudflare 等待能识别 Cloudflare 当前的“Performing security verification”页面,并按原定设计将该等待限制在 10 秒:一条被拦截的 doordash.com 链路从 39.5 秒降到 19.5 秒。这里没有任何改动用于通过验证。
- 每条链路都带有 navigations: [{ url, finalUrl, httpStatus, blocked? }],每个 navigate 结果都带有 httpStatus 和 blocked;顶层的 success 和 blocked 仍由最终页面决定
- 调用方代理的主机若是 loopback、link-local 或云元数据地址,会在浏览器启动之前被拒绝
- aws-waf 规则位于 crawlforge-extractors 1.12.0 中,因此 MCP server 和 REST API 的命名方式一致
extract_embedded_state 解码 Nuxt 3、SvelteKit、YouTube 和 Shopify,每次重定向都须经过检查
extract_embedded_state 现在无需浏览器即可解码普通抓取已带回的内容:Nuxt 3 的 __NUXT_DATA__ 解码为对象而不是索引数组,还有 SvelteKit 的页面数据、ytInitialData 和 ytInitialPlayerResponse(YouTube 观看页会返回其 videoDetails)、Remix、Inertia 的 data-page、Shopify 的 ShopifyAnalytics.meta 和商品 JSON,以及较大的 JSON data-* 属性。赋值为 JavaScript 字面量而非 JSON 的全局变量由静态求值器读取,因此不会运行任何页面代码。Next.js Flight 行中对其他行的引用会被解析,data_rows 索引指向承载数据而非标记的行,find 返回键名匹配的每一条路径,可直接作为 path 传回。同一版本还对每次重定向设卡:每一跳在跟随之前都要经过阻止列表和 robots.txt 检查,等待主机的 Crawl-delay,并为 Web Bot Auth 重新签名。价格仍为 2。
- find 最多返回 50 个匹配项,格式为 { path, preview },附带该值的前 200 个字符;raw: true 还会保留未解码的 Nuxt 数组。两者在 REST API 上同样可用
- 重定向到 robots.txt 禁止的路径或被阻止的主机时,会在发出请求之前被拒绝,因此而失败的调用不收费。浏览器类工具会检查每一跳以及页面最终落地的 URL,scrape_with_actions 或 browser_session 保留的页面在每个动作之后都会再次检查
- 审计日志行不再保留 URL 中的凭据:userinfo 会被丢弃,名称类似 key 或 token 的查询参数会被脱敏
extract_embedded_state:大载荷改为预览,并支持 stealth 重读
超过 max_inline_chars 的结果现在会以预览加 result_handle 的形式返回,而不是整个载荷:producthunt.com 和 zappos.com 此前会内联返回 1-2 MB。内联部分保留 found 列表、字节大小、data 的顶层键,以及一段在行边界处截断(绝不截在某个值中间)的格式化预览;read_result 可按 json_path 读取完整载荷的任意部分。keys_only: true 返回两层键,每个值替换为其类型,便于低成本探查。使用 escalate: true 时,被拦截的页面(403、429、验证页或空壳页)会经由 scrape 所用的同一 stealth 阶段重新读取并再次解析;在浏览器中,JavaScript 运行后还会从 window 上读取框架全局变量,放在 window_state 下。不使用 escalate 时,遇到拦截现在会返回指出厂商名称的错误,而不是什么都没找到的成功结果。价格仍为 2;escalate 预估 7,普通抓取拿到页面时只收 2。
- 已在 max_inline_chars: 3000 下验证:producthunt.com 内联 2,979 个字符,zappos.com 内联 2,970 个字符,其余部分只需一次 read_result 调用
- doordash.com(Cloudflare)通过 escalate 在 impit、Camoufox 和 Chromium 上都返回了 103-106 行 Next.js Flight 数据
- window_state 读取 __NEXT_DATA__、__NUXT__、__remixContext、ytInitialData、ytInitialPlayerResponse、Redux 和 Apollo 的 store、__TGT_DATA__ 以及 __PWS_DATA__;在 Camoufox 上,读取经由 Firefox 的隔离环境(isolated world)进行,因此不会有任何脚本在页面自身的环境中运行
snapshot 深入 Shadow DOM 与 iframe,并可自动处理 cookie 横幅
snapshot 现在使用 Playwright 原生的可访问性快照,因此开放 shadow root 和 iframe(包括跨源 iframe)中的元素都会出现在树中,并带有 act 可以点击的普通 @e ref。树的格式不变,旧的页面遍历方式保留为后备,每个 snapshot 都会说明实际运行的是哪一种。scrape_with_actions 还新增了 browserOptions.consent:"reject" 会在首次导航后以及每个 navigate 动作后各运行一次 DuckDuckGo autoconsent 的拒绝(opt-out)流程,"accept" 运行其同意(opt-in)流程,默认值 "off" 不处理横幅,因此自行点击横幅的链路照常工作。价格不变。
- 每个 snapshot 都带有 source: "aria" | "walk";browser_session 的 snapshot 得到同样的树,屏幕上可见的 aria-hidden 控件现在也会列出
- 结果中带有 consent: { cmp, action, ms },每次导航最多 2 秒;没有匹配横幅的页面报告 action: "none",绝不会让链路失败。已在 The Guardian 的 Sourcepoint 对话框以及 Camoufox 下的 Ecosia 上验证
- stealth 的滚动和阅读时长辅助函数接受 Playwright 选择器,因此 shadow root 和 frame 中的 ref 在 stealth 路径上同样可用
snapshot 会等待页面就绪,超限时排队而非拒绝
第二轮 scrape_with_actions 修复。snapshot 现在会先等页面渲染完成再读取:先等 load 事件,再等一段安静窗口——300 毫秒内没有 DOM 变化、500 毫秒内没有进行中的请求——这段等待有上限,且绝不超过该动作的超时时间。Amazon 首页的 snapshot 从 0 个节点变为 236 个,搜索框也在其中。点击、按键和选择之后也会以同样方式等待,因此单页应用的路由切换能被看到。超出浏览器上限的一批突发调用现在会排队最多 60 秒,而不是以“Maximum concurrent sessions”失败;browser_session 仅在打开页面期间共用这些槽位。Camoufox 上的 stealth 点击不再卡住:此前叠加了两层光标拟人化处理,Ecosia 同意弹窗的点击耗时从 12-20 秒降到 1.4 秒。价格不变。
- snapshot 结果会报告 waited_ms 和 settled_by(quiet、cap 或 closed);browser_session 的 snapshot 也采用同样的等待
- CRAWLFORGE_MAX_ACTION_SESSIONS 设定浏览器槽位数(默认 3),CRAWLFORGE_ACTION_QUEUE_TIMEOUT_MS 设定排队时长(默认 60 秒);结果带有 queued_ms,executionTime 不再计入等待时间
- 在 Camoufox 上,点击模拟器只移动一次,由 Camoufox 自己绘制轨迹,每次移动上限 0.5 秒;simulateClick 现在遵守该动作的超时时间,而不是 Playwright 默认的 30 秒
识别应用错误页,scrape_with_actions 失败的代价更低
这是对 6.11.0 impit 步骤的后续改进。文本以应用错误开头的短页面——“Something went wrong”“Oops!”“An error occurred”或 Next.js 的客户端异常——即使标题正常,现在也会被视为软拦截,因此会交给 stealth 浏览器处理,而不是作为页面返回。quora.com 的“Something went wrong. Wait a moment and try again.”此前在每条路径上都能通过,而不只是经由 impit。这条规则来自 crawlforge-extractors 1.10.0,服务器现在要求该版本。新增的 CRAWLFORGE_IMPIT=off 设置可为整个部署关闭 impit 步骤:从托管实例的数据中心 IP 测量,它没有突破基准测试中的任何一道拦截,因此托管服务将其关闭,而 npm 安装版保持开启。价格不变。6.12.1 是根据一次实测评审完成的第一轮 scrape_with_actions 修复:maxRetries 现在默认为 0,失败的链路不再运行两次(点击一个不存在的选择器,耗时从 20.8 秒降到约 4 秒),调用方确实要求的每次运行都会在 attempts[] 中报告。browserOptions.timeout 现在作用于每个动作,screenshotOnError: false 会被遵守,截图字节也不再与其资源 URI 一起内联泄露。
- 少于 200 个字符、文本以应用错误短语开头的文档,无论标题如何都算软拦截,因此错误兜底页会在浏览器中重试,而不是作为内容返回
- CRAWLFORGE_IMPIT=off 会在出口 IP 无法受益的部署上跳过升级阶段的 impit 步骤,且每次调用都会读取。托管服务将其关闭;npm 安装版保持开启,在住宅 IP 上它能通过 indeed.com
- REST API 的 scrape 升级按决定仍只使用浏览器;通过 impit 重放 Cloudflare 放行 cookie 的方案被否决:该 cookie 绑定在获取它时所用的 User-Agent 上,重放就意味着要发送浏览器 User-Agent,而不是 CrawlForge 自己的
- scrape_with_actions:maxRetries 默认为 0,每次运行都在 attempts[] 中报告;browserOptions.timeout 是每个动作的截止时间;错误恢复每个动作最多 3 秒,遇到等待动作或匹配不到任何元素的选择器时直接跳过;带一张截图的链路返回不到 5 KB,而不是约 118 KB
启动浏览器之前先尝试 Chrome TLS
当 scrape 以 escalate: true 运行,或 agent 重试被拦截的页面,且引擎为 "auto"(默认值)时,现在会在启动任何浏览器之前先尝试一次 Chrome TLS 握手。它携带诚实的 CrawlForge User-Agent。如果拿到了页面,就不会启动浏览器:在住宅 IP 上,普通抓取会被 Cloudflare 拦截的 indeed.com 大约一秒就返回了。如果没拿到,浏览器照常运行,两种情况价格都是 2 + 5。该步骤使用 impit,这是一个带预编译二进制的新可选依赖;没有它时,升级行为与 6.10.0 完全相同。在 Chromium 上,仍被带 Turnstile 框架的 Cloudflare 拦截页挡住的 stealth 渲染现在会点击一次复选框;只嵌入了 Turnstile 小组件的 200 页面不再被判定为拦截;每次升级都会在合规审计日志中写入一行。
- impit 只在升级阶段内运行:从不用于普通抓取,调用方指定 playwright 或 camoufox 时也不会使用。由它拿到的页面会报告 stealth.engine: "impit" 并附带说明它的警告;每一跳重定向都会经过带 DNS 解析的 SSRF 检查;可见文本少于 200 个字符的页面会继续交给浏览器
- 在 Chromium 上通过普通的 Playwright 鼠标输入点击 Turnstile 复选框,仅在常规等待后拦截仍在、且页面含有 challenges.cloudflare.com 框架时进行。已在 Cloudflare 的强制交互测试 sitekey 上验证,这只证明了机制本身:真实网站是否接受这次点击,仍取决于它对 IP 和指纹的评分
- nowsecure.nl 以 200 返回一个使用 Cloudflare 测试 sitekey 的 Turnstile 小组件,此前对它记录的每一次“Blocked”都来自我们自己的判定层。现在,有标题、有文本且只有小组件标记的 200 文档会被放行;真正的拦截(403)和空壳页面仍被拦截
- scrape 使用 escalate、agent 的 stealth 重试以及 stealth_mode 都会向 logs/compliance-audit.log 写入一行 stealth_escalation,写入时间在合规检查之后、浏览器导航之前,记录的是经过哈希的密钥和所有者 ID 而非原始值。robots_override 行在线上服务器上不再显示 apiKeyId: "anonymous"
Clearance jar:解过的验证不必再解
当 stealth 渲染突破 Cloudflare 或 DataDome 的拦截后,该厂商的放行 cookie——严格限定为 cf_clearance、__cf_bm 和 datadome——会被保存,并重放到下一个引擎、User-Agent 和代理都相同的 stealth 上下文中。这涵盖 scrape 的升级阶段、stealth_mode、agent 的 stealth 重试、browser_session 和 scrape_with_actions。在住宅 IP 上访问 stackoverflow.com,第一次 stealth 调用用 4.1 秒通过了 Cloudflare 的过渡页;第二次没有任何验证,1.6 秒直接拿到 200。其他 cookie 一律不保存,因此一个调用方的登录状态不会进入另一个调用方的上下文。
- 只保存这三个具名 cookie,按引擎、User-Agent 和代理分别存放。再次遇到拦截的渲染会丢弃该主机的放行 cookie;有效期沿用 cookie 自身的设置,最长 24 小时;最多保存 32 个身份,每个 200 个 cookie
- 持久化在 ~/.crawlforge/stealth-clearance.json(权限 0600,键经过哈希),因此第二个进程也能从磁盘复用放行状态:indeed.com 加载时没有发出任何验证平台请求。CRAWLFORGE_CLEARANCE_JAR=off 可将其关闭
- 有意没有实现持久化的 Chromium 配置文件池:共享配置文件会保留登录状态和站点存储,在托管实例上这会在客户之间泄露
agent 在 stealth 浏览器中重试被拦截的页面
在 6.9.0 之前,agent 工具的执行阶段只做普通抓取。遇到验证的种子页面会被丢弃,答案由搜索摘要拼凑而成,在评审的 Indeed 测试中得出了“3.3 stars, based on 3.3 reviews”。现在,遇到验证、403 或 429、空壳页面或超时的页面,会通过 scrape 的 escalate: true 所用的同一个 stealth 阶段重试,使用相同的合规检查、引擎选择和代理。使用 Chromium 引擎时,同一测试读取了种子页面本身,答案是基于 58,942 条评价的 3.3 星。计费按实际结果:8 credits,每次成功拿到页面的 stealth 重试另加 5。
- 调用方指定的 URL 优先重试;发现的 URL 只有在没有相关搜索摘要时才重试。拒绝、404 和 5xx 错误从不重试,每次运行最多重试 2 次,剩余时间不足 20 秒时不会开始重试
- 再次遇到拦截或抛出错误的重试不收费。结果会报告 stealth_retries(尝试次数)和 stealth_retries_charged(计费次数),来自重试的证据标记为 via: "stealth",公布的上限是 18 credits,没有重试时为 8。REST 路由同样按 8 + 每次计费重试 5 收费
- 如实说明测量结果:在同一 IP 上使用 Camoufox 引擎时,Cloudflare 拦截了重试,未作改动的 scrape 升级在那里也同样失败
stealth 基准测试,以及它测出的漏洞已修复
2026-09 stealth 评审中的基准测试原本是手动运行的;6.8.0 把它变成了一条命令 npm run bench:stealth,它让 stealth 浏览器和普通抓取分别面对十二道机器人拦截和五个检测页面,并在结果矩阵顶部标明主机操作系统、出口 IP 类型、引擎和浏览器版本。以它为准,漏洞已被修复:navigator.webdriver 返回 false 而不是被删除,navigator 上不再有任何自有属性;userAgentData 去掉了 HeadlessChrome 品牌;Chrome 主版本号取自已安装的二进制;Web Worker 的回答现在与文档一致。检测自检在两个引擎上都达到 19 项通过、0 项失败、1 项跳过。通过拦截最多的 Camoufox 引擎现在经由 "auto" 成为默认引擎,回退到 Chromium 时会明确报告,而不是悄悄进行。
- npm run bench:stealth:ci 运行十项无需第三方网站的页内自检,只有在出现基线文件中未列出的回退时才失败;另有 --self-check 反向对照,强制把 navigator.webdriver 设为 true,并且必须被检出
- 只嵌入 Turnstile 小组件的页面不再被判定为 Cloudflare 拦截(quora.com 现在在两个引擎上都能通过),自定义标题的过渡页也会得到应有的等待时间。stealth 页面不再拦截请求,也不再随机丢弃图片、字体和样式表;stealth 浏览器去掉了 --disable-web-security
- scrape 的 escalate_engine 和 stealth_mode 的 engine 默认改为 "auto":先用 Camoufox,其二进制缺失时改用 Chromium 并给出警告。browser_session 和 scrape_with_actions 新增 engine 参数;CRAWLFORGE_STEALTH_PROXIES 为每条 stealth 路径提供服务器级代理列表;修复了约一半启动中 Camoufox User-Agent 两个版本号不一致的问题
真正生效的代理轮换,以及始终是 Firefox 的 Camoufox
一次针对 6.6.2 的机器人检测测试建议通过住宅代理来绕过 Cloudflare 的 IP 信誉检查。但这项配置根本没有生效,原因有三:代理被放进了 Chromium 的一个启动参数,而该参数无法携带每个住宅代理都附带的 user:pass;Camoufox 的启动路径无论如何都不走代理;代理只在启动时读取一次,因此轮换永远不会发生。这三点都已修复,stealthConfig.proxyRotation 现在接受普通的代理 URL,凭据按浏览器上下文应用。排查过程中还发现 stealth 路径更多自相矛盾之处:因“无迹可查”而被选中的 Camoufox,却被塞进了 Chrome 的 User-Agent 和 Chromium 的 client hints。
- proxyRotation 接受 http、https、socks4 和 socks5 URL,在两个引擎上都能认证;格式错误的条目会报错,而不是变成不走代理的请求;从第一个条目开始;get_stats 不再返回代理密码。CrawlForge 不提供代理,这里走的是你自己的代理
- Camoufox 以自身的 Firefox 身份出现,不再发送 sec-ch-ua 请求头,也不再注入为 Chromium 设计的脚本,并开启其自带的 geoip、block_webrtc 和 humanize 功能。在代理后面,它会根据出口 IP 决定语言、时区和位置
- stealth_mode 的 scrape 会等待页面的 load 事件以及 DOM 停止变化后再读取,并报告 waited_for_render_ms;Firefox 页面自身未捕获的 JavaScript 错误也不再导致进程退出
browser_session:第 31 个工具
一个比打开它的那次调用活得更久的浏览器页面。browser_session 是一个带 operation 参数的工具:open 在某个 URL 上打开会话并返回 session id;snapshot 返回页面的可访问性树,并为每个可交互元素附上稳定的 ref(@e1 [textbox] "Email"、@e3 [button] "Sign in");act 执行 1 到 20 个浏览器动作,其 selector 可以就是这些 ref;read 从实时 DOM 中提取 markdown、HTML、文本或元数据,反映会话点击之后的当前状态;screenshot 截取整页或单个元素;close 结束会话;list 说明某个 API key 当前持有哪个会话。这个循环是先看再做:agent 读取 snapshot 交回的 ref,而不是猜测 CSS 选择器,登录也只需付费一次,而不是每次调用都付一次——scrape_with_actions 做不到这一点,因为调用返回时它的浏览器就关闭了。每个 operation 单独计价,最低 1 credit:open 3、read 2,snapshot、act、screenshot、close 和 list 各 1,对外公布的价格是 open 的 3。会话按设计就是短命的:ttl(默认 600 秒,范围 30-3600)是绝对时钟,activity_ttl(默认 300 秒,范围 10-3600)是空闲时钟,哪个先到期就关闭页面。同样的“先看后做”也延伸到一次性链路:scrape_with_actions 新增了 snapshot 动作,因此一条链路可以先列出页面提供了什么,再去指定选择器。6.6.1 紧随其后,带来针对该工具本身的首轮实测结果:被拦截的页面现在会被明确标识,而不再作为内容返回;executeJavaScript 会交回它的返回值;read 也和其他内容类工具一样受到上限约束。6.6.2 完成了第二轮实测:超过 stdio 消息上限的整页截图曾会关闭整个 MCP 连接,现在会被拒绝,并说明其大小以及应改为截取什么。
- 新增 browser_session(每个 operation 1-3 credits),在多次 API 调用之间保持同一个浏览器页面存活:open、snapshot、act、read、screenshot、close 和 list,open 返回的 session id 需要带在之后的每次调用上。每个 operation 按自己的价格计费——open 3、read 2,其余各 1——因此一次廉价的查看绝不会按昂贵的 open 收费,而一个登录并读取的流程(open、snapshot、act、act、read、close)合计 9 credits
- snapshot 把页面返回为可访问性树,每个可交互元素都带一个稳定的 ref,act 的 selector 直接指向 @e1,而不是猜出来的 CSS 选择器。ref 只属于取得该 snapshot 的那个文档:任何导航都会让全部 ref 失效,对失效的 ref 执行动作会明确报错并提示重新 snapshot,而不会默默点错地方。scrape_with_actions 也新增了同样的 snapshot 动作,因此一次性链路同样可以先看后做
- 已在 REST API 上线:POST /api/v1/tools/browser_session,使用与其他工具相同的响应信封。一个 REST API key 同一时间只能持有一个会话,已有会话仍然存活时再次 open 会被具名错误拒绝,而不会排队,所以用完就 close,不要等 TTL 自己到期
- 6.6.1 修复了最要紧的那个问题:机器人拦截页或 HTTP 错误页不再被当作成功的会话。open 和 read 现在会报告 httpStatus 并指出拦截方的名字——此前 DataDome 的 403 会以 success 返回,页面内容只有 "g2.com" 一个词;executeJavaScript 无需额外参数就会返回脚本的值;read 则受 max_inline_chars 限制,改为给出 result_handle,而不是内联返回五十多万个字符
- 6.6.2 将截图资源限制在 stdio 消息上限以内——一张 18.5 MB 的整页 PNG 曾关闭整个 MCP 会话,连带该连接上的所有工具——并且 respect_robots: false 现在会从 open 和 act 返回警告,审计记录中记为 browser_session 而非 scrape_with_actions
跨行业实测轮次:DOCX 支持与七项修复
对全部 30 个工具在十三个行业约 600 个 URL 上的实测轮次发现了六个缺陷和四个缺口:房地产、医疗、金融、教育、体育、餐饮、娱乐、法律与政府、工业供应、电信与 SaaS 定价、非营利组织、户外与科学。process_document 现在能读取 Word 文档:以 URL 方式获取的 .docx 此前会以一兆字节的 ZIP 字节作为页面文本返回;现在由获取到的内容决定读取方式,以普通 URL 提供的 PDF 会进入 PDF 解析器,压缩包或图片则会按名称拒绝。scrape 曾把 irs.gov 的税率级距页面缩成页眉,因为隐藏内容处理把打印样式表当作屏幕 CSS、把内联样式折叠成通用规则,并在每个逗号处拆分 :has() 选择器列表;三处均已修复,页面重新返回四张税率表。track_changes 把新增了一整条记录的数据源判为无变化,extract_structured 在模型超出输出预算时整个表格提取失败,map_site 忽略起始 URL 的路径及其搜索词,被截断的 scrape 则丢失了 highlights 和 question 的答案。正式发布的 6.5.0 还会在抓取之前拒绝任何 reddit.com URL(reddit.com 会拦截所有非浏览器客户端),并给出能读取相同帖子的 reddit_search 调用;reddit_search 现在先尝试 Arctic Shift,再尝试 PullPush。
- process_document 用 mammoth 读取 DOCX,并把以 sourceType url 提供的 PDF 交给 PDF 解析器;无法读取的内容按名称拒绝,而不是以字节形式返回
- scrape 的隐藏内容处理跳过打印样式表,不再把内联样式当作通用规则,且只在顶层逗号处拆分选择器列表,irs.gov 的税率表因此得以返回;被截断的结果会内联保留 highlights、question 和 json 的答案
- track_changes 能识别新增一条记录的纯文本文档;extract_structured 以 partial: true 保留被截断响应中的完整行并给出真实的回退原因;map_site 限定在起始 URL 的路径内并按各 URL 中的搜索词排序;robots.txt 中较长的 Crawl-delay 会在响应中注明
- reddit.com 的 URL 在任何抓取之前即以 USE_REDDIT_SEARCH 被拒绝,并指向该 URL 对应的确切 reddit_search 调用;reddit_search 先尝试 Arctic Shift,再尝试 PullPush
零售、旅游与航空实测轮次修复
对全部 30 个工具在 415 个零售、旅游和航空 URL 上进行实测,发现五个缺陷和五处不足。与上一轮一样,每个缺陷都返回了成功却缺少内容,而不是报错。map_site 只读到了 boeing.com 站点地图 1,878 个 URL 中的 75 个,因为该站点地图使用相对的 loc 路径,解析器遇到即抛错;现在 loc 会相对于站点地图自身的 URL 解析,坏条目单独跳过,空的解析结果既不写入缓存也不从缓存返回。scrape 在默认的 onlyMainContent 下丢掉了 WestJet 6 行的托运行李费用表——它小于数据表的尺寸阈值,又位于一个被 Readability 判为负分的组件内;现在带有 th 表头单元格的被丢弃表格无论大小都会被找回,而以空角单元格开头的表格会渲染为管道表而非压扁的文本。scrape_with_actions 在一个 JavaScript 帮助中心上把占位符 Content not available in markdown format 当作成功返回;当提取器一无所获时,markdown 和 html 现在取自操作后的 DOM。agent 被问到 Southwest 第一件托运行李收费多少时,只规划了单纯的实体查询,并回答费用未说明;只有一个查询的规划现在会把任务本身的措辞作为第二个查询,只有实体查询为实时根页面投票,答案为 $45,引自 southwest.com。crawl_deep 的预览在每一页都是站点的大菜单;页面现在经过与 scrape 相同的主内容处理。没有任何工具改变价格。
- map_site 能读取以相对 loc 路径编写的站点地图。boeing.com 含 1,878 个 URL 的站点地图曾被解析为空,因为 normalizeUrl 在 / 上抛错,工具于是退回到爬取首页链接并返回 75。loc 现在相对于站点地图 URL 解析,一个坏条目不再丢弃其余条目,空的解析结果永远不会写入或从站点地图缓存返回——此前解析器修好后它仍持续一小时回答 75
- 表格与 JavaScript 页面:scrape 会找回作者标记了 th 表头单元格的被丢弃表格,无论其大小,因此 WestJet 的费用表在默认 onlyMainContent 下得以保留;首行其余均为 th 时,空角单元格会被提升,使表格带列渲染。当 Readability 找不到文章时,scrape_with_actions 从操作后的 DOM 提供 markdown 和 html,并标注 markdownSource: body,而不是占位符。crawl_deep 的内容是页面主内容而非菜单
- 当当前状态规划止步于单纯的实体名时,agent 会把任务本身的措辞加为第二个搜索查询,仅以实体查询为实时根页面投票,并在实时页面未给出答案时可以越过它。get_batch_results 接受 max_inline_chars,超限时返回预览和 result_handle;extract_links 不再把 javascript: 伪链接计为链接;当 Arctic Shift 在调用方选定的时间窗内超时时,reddit_search 会指出该窗口以及如何收窄
嵌套校验、版本权威与 Linux 登录
R19 实测轮次发现的四个缺陷——每个都是自信的错误答案而非报错——外加一个仅在 Linux 上的崩溃。extract_structured 只校验一层,于是一个由零散页面文本组成的数组通过了要求对象数组的 schema;校验器现在集中在一个模块中,所有使用方包括 CSS 选择器回退都使用它,并且当必填字段存在但形状错误时 success 为 false。同一工具在大表格上无解释地退回到 CSS 选择器,因为输出预算把数组当作一个字段;预算现在随 schema 中的数组规模伸缩,被截断的响应会以两倍预算重试一次,失败信息会指出 token 上限——可用范围从约 43 行提升到约 156 行。agent 曾根据论坛帖子回答版本问题并通过了来源核查,因为该字符串确实出现在抓取的页面上;对于版本问题,版本现在必须出现在非讨论页的来源中,未通过的会标记在 provenance.unsupported_versions 中。analyze_content 曾为非拉丁文本编造实体和可读性分数;两者对西里尔文、希腊文、阿拉伯文和天城文均报告 notApplicable,俄语停用词也已加入列表。6.3.1:crawlforge login 在 Linux 上因一个 import 使用了错误的大小写而以 ERR_MODULE_NOT_FOUND 崩溃,大小写不敏感的文件系统掩盖了这一点;新增的测试会把每个相对 import 与 git ls-files 对照。
- extract_structured 校验嵌套形状:countries 作为字符串数组对照对象 schema 不再报告为有效。src/utils/schemaValidate.js 中的单一校验器服务于所有使用方,错误会指出路径(Field countries.0.capital: expected string, got number),最多十条并附计数,且当必填字段存在但形状错误时 success 为 false——这对依据它分支的调用方是可见的变化
- extract_structured 的输出 token 预算随 schema 中的数组规模伸缩,而不是把数组当作一个字段;在对象中途被截断的响应会以两倍预算重试一次,失败信息为 model response was cut off at the N-token output limit 而非报告 JSON 偏移量;可用的表格范围从约 43 行提升到约 156 行。agent 不再根据讨论页回答版本问题:版本必须出现在非论坛来源中,会执行一次纠正性重写,仍未通过的标记在 provenance.unsupported_versions 中;provenance.checked 现在报告核查是否运行过
- analyze_content 对非拉丁文本的实体和可读性报告 notApplicable 而不是编造(rain and gusty 曾被当作组织返回,一份俄语天气报告得分 Very Easy),把 CJK 防护扩展到西里尔文、希腊文、阿拉伯文和天城文。6.3.1 修复了 Linux 上的 crawlforge login:一处对 AuthManager.js 的 import 使用了错误的大小写,macOS 和 Windows 能解析而 Linux 不能,因此自 6.1.0 起每个 Linux 用户运行该命令都在 import 时崩溃;tests/unit/importCaseExactness.test.js 现在会对照 git ls-files 检查每个相对 import
从 MCP server 创建托管监控
MCP server 上的计划监控现在分为两种。本地监控——和以前一样通过 create_scheduled_monitor 操作创建——持久化在 ~/.crawlforge/monitors 下,只在 MCP server 进程存活期间于进程内触发,错过的运行会在重启时补跑,并从您的机器通过 webhook 或 Slack 发送通知;由系统 cron 执行的 crawlforge monitor:run-due 仍是有保证的本地路径,而本地监控从不发送邮件。传入带 hosted: true 的 scheduledMonitorOptions,服务器就会改为使用已配置的 API key 把监控注册到 /api/v1/monitors 的托管服务,并返回托管 id 和一个控制台 URL。从此它就是 REST API 和控制台所管理的同一个托管监控:CrawlForge 自己的调度器负责抓取、对比、记录每一次检查,并通过邮件和已签名的 webhook 发送通知,您这边无需运行任何东西;间隔会被转换为 cron,下限为 5 分钟,时区为 UTC。没有任何工具改变价格:创建或停止托管监控不计费,创建本地监控仍收取 3 credits,每次托管检查按每个已对比目标收取 track_changes 的价格。
- 计划监控上的 hosted: true。create_scheduled_monitor 使用已配置的 API key 通过 /api/v1/monitors 把监控注册到托管服务,并返回托管 id 和一个控制台 URL;邮件发往 notificationOptions.email.recipients,已签名的 webhook 发往 notificationOptions.webhook。list_scheduled_monitors 会把本地和托管监控一起列出,每个都带有 hosted 标志,stop_scheduled_monitor 可以删除任意一种。从 MCP server 创建或停止托管监控不计费,与 REST 监控 API 一样;创建本地监控仍收取 3 credits
- 本地监控不再假装发送邮件。带有邮件通知设置的本地监控现在会报告一个指向 hosted: true 的错误,而不是伪造的成功;本地的邮件路径一直只是一个占位符。本地监控从您的机器通过 webhook 或 Slack 发送通知,只在 MCP server 进程运行期间触发,错过的运行会在重启时补跑,而由系统 cron 执行的 crawlforge monitor:run-due 才是有保证的本地路径
- track_changes 的输入 schema 只声明一次,由服务器注册和工具共用,行为没有变化。AlertNotificationSystem.js——601 行、没有任何地方导入的代码——已被删除:它的活跃对应物是 notifier 模块,而邮件和已签名的 webhook 由托管服务发送
两个协议时代上的确认提示
会在昂贵操作之前先征询的五个工具——crawl_deep 超过 500 个页面、batch_scrape 同步模式超过 25 个 URL、deep_research 超过 50 个 URL、agent 使用 pro 模型,以及 extract_structured 在未配置 LLM 且必填字段多于三个时——加上低 credits 余额提醒,都建立在 2025 时代的内联 elicitation/create 请求之上,而 2026-07-28 修订版根本没有服务器到客户端的请求通道:在该时代这些提示会悄然不触发,操作未经询问就继续执行。现在它们改为多轮往返的 input_required 返回,SDK 在两个时代都提供这种形式,因此无论哪一边,提示都会送到你面前。返回并要求确认的调用没有做任何工作,因此不消耗 credits,也不产生用量记录;费用只在调用真正完成时收取一次,而拒绝确认也不花任何 credits。同一次发布还修复了 HTTP 上的 elicitation——它其实从未生效:两条 HTTP 路径都由服务器实例的克隆提供服务,而真正被连接的始终只有克隆,因此辅助函数在每个 HTTP 请求上读到的都是未定义的客户端能力,每个会话都未经询问就继续执行——自 v3.2.0 起就已损坏,并且一直是向安全一侧失败。现在传输层会把提供服务的那个克隆标记到请求上下文上,stdio 则从未受影响。
- 确认提示现在也能送达 2026-07-28 时代的客户端:crawl_deep 超过 500 个页面、batch_scrape 同步模式超过 25 个 URL、deep_research 超过 50 个 URL、agent 使用 pro 模型,以及 extract_structured 在未配置 LLM 且必填字段多于三个时,加上低 credits 余额提醒,都会在运行前先询问。它们此前建立在 2025 时代的内联 elicitation/create 请求之上,而 2026-07-28 修订版没有服务器到客户端的请求通道,因此在该时代提示会悄然不触发,操作未经询问就继续执行。现在每一个都是多轮往返的 input_required 返回,SDK 在两个时代都提供这种形式——无论是 2025 时代还是 2026 时代的客户端,提示都会送到用户面前
- 确认的那一轮往返永远不计费。返回并要求确认的调用没有做任何工作,因此花费 0 credits,也不产生用量记录;费用只在调用真正完成时收取一次。拒绝确认不花任何 credits,本次发布也不改变任何工具的 credits 价格
- HTTP 上的 elicitation 已修复,而它其实从未生效:两条 HTTP 路径都由服务器实例的克隆提供服务,而真正被连接的始终只有克隆,因此辅助函数在每个 HTTP 请求上读到的都是未定义的客户端能力,每个会话都未经询问就继续执行——自 v3.2.0 起就已损坏,并且一直是向安全一侧失败:宁可从不询问,也没有问错。现在传输层会把提供服务的那个克隆标记到请求上下文上。这只涉及托管的 HTTP 端点:stdio 连接——npx crawlforge-mcp-server 以及每个桌面客户端都以此连接——从未受影响。另有修复:CI 中的 .mcpb 捆绑包冒烟测试不再设置占位用的 API key
- 新增 `crawlforge login` 命令:通过浏览器交接 API 密钥。CLI 打印一个批准 URL,已登录的用户在网站上批准,密钥仅向 CLI 交付一次并保存到 ~/.crawlforge/config.json。它从不修改任何客户端配置;编码代理可以运行该命令并转达 URL,这样密钥永远不会被粘贴到终端中
2026-07-28 协议迁移
这是一次大版本发布,因为有三处调用方能看见的东西被移除或改变。MCP server 现在在同一个 /mcp 端点上以无状态方式讲 2026-07-28 协议修订版——这个端点此前已经服务于 2025 时代——没有握手、没有会话 id,并以 server/discover 取代 initialize。一个请求属于哪个时代,由 MCP SDK 自己的分类器决定,而不是由我们自己的规则决定,因此端点在边界请求上永远不会与 SDK 判断不一致;两个时代由同一份工具定义提供服务,因此它们不会彼此漂移。这只涉及 HTTP 端点:stdio 连接——npx crawlforge-mcp-server 以及每个桌面客户端都以此连接——仍然协商 2025 时代,其中没有任何变化。移除:crawl_deep、batch_scrape、deep_research 与 agent 上的 task 参数与异步任务模式,因为规范把它们所依赖的 SDK 接口直接删除了,而不是把它搬到别处;以及 --legacy-http 标志,距其自身的警告所说的移除时间已过了四个小版本。另有修复:托管 HTTP 端点从未应用过 stdio 一直都有的协议规范化处理,因此它按注册顺序返回工具,没有图标,也没有 schema 方言。
- HTTP /mcp 端点上的 2026-07-28 修订版,与有会话的 2025 时代在同一路由上以无状态方式并行提供。现代请求携带按请求的 _meta 信封(协议版本、clientInfo、clientCapabilities)以及 Mcp-Method 头,工具调用时还带 Mcp-Name;它会拿到自己的服务器实例,且没有会话 id。路由、请求头与请求体不一致的校验,以及非 JSON content type 上的 415,全部是 SDK 自己的代码而非重新实现,并且它们每一个都在工具运行之前就拒绝该请求,因此被拒绝的调用永远不会计费。认证没有变化,两个时代完全一致——每个请求都用 Bearer 或 X-API-Key——而且它在时代路由之前执行,因此不存在按时代划出的缺口。GET /health 现在会列出该端点所服务的每一个修订版
- 破坏性变更:params.task 与异步任务模式已从 crawl_deep、batch_scrape、deep_research 与 agent 上移除。它们构建在 MCP SDK 的实验性 tasks API 之上,而 SEP-2663 把该 API 整个删除,并未将其挪为扩展——在 2026 时代的连接上,即使注册了处理器,传入的 tasks/get 也会以 -32601 作答,因此已经没有可迁移的去处。这四个工具不再声明 execution.taskSupport,现在同步执行——这正是从不传 task 的调用方一直以来所得到的。对于不应长时间占用连接的耗时作业,请用 batch_scrape 的异步 webhook 模式,或 result_handle 与 read_result 的组合。另一项破坏性变更:CRAWLFORGE_LEGACY_HTTP 与 --legacy-http 连同它们所控制的 v3.1 无状态模式一并移除;2025 时代的客户端不受影响,因为它们由自 v3.2.0 起就是默认的有会话传输提供服务
- 修复:托管 HTTP 端点现在返回与 stdio 客户端一直以来相同的 tools/list。协议规范化包装——按字母排序、图标、JSON Schema 2020-12 方言,以及十个只读工具上的可缓存标记——位于模板服务器自身的协议实例上,而不在每个 HTTP 会话的克隆所复制的注册表中,因此 HTTP 会话一直按注册顺序拿到工具、四项一个都没有,而 stdio 四项俱全。这会改变托管端点在 2025 时代的返回内容:输出顺序重排,并且每个工具都多了图标与 schema 方言。这是附加的元数据加上一次重排——没有任何东西被移除——但固定依赖旧顺序的客户端会看到它挪位。另有修复:elicitation 提示重新送达那些自 SDK 升级以来就悄然收不到它们的客户端,原因是 SDK 的便捷方法拒绝了一种按它自己的规则算作有效的能力形状