# AI 浏览器会返回隐藏内容吗？TinyFish Fetch 实测与 AgentQL 源码核查

> Canonical: https://vxi.me/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/
> Locale: zh-cn
> Published: 2026-10-26
> Updated: 2026-10-26
> Markdown: https://vxi.me/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries.md

<figure class="tinyfish-fetch-counterexample-probe not-prose" lang="zh-cn">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-counterexample-probe.mobile.still.zh-cn.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-counterexample-probe.still.zh-cn.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-counterexample-probe.mobile.zh-cn.svg" type="image/svg+xml"><img src="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-counterexample-probe.zh-cn.svg" alt="反例审查图：F0 的普通与指定区域 HTML 均为 0/3；F1 的 aria-label 伪指令为 0/3 与 3/3；F2 的 display:none 隐藏节点在两种模式中均为 3/3。只观察 Fetch 返回文本，不代表 Agent 决策或远端会话清理。" width="1200" height="700" loading="eager" fetchpriority="high" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption class="mt-2 text-xs leading-relaxed text-slate-500">反例探针 · 仅观察 Fetch 返回文本，不是 Agent 服从性测试。</figcaption>
</figure>

随着 AI Agent 飞速发展，我们也开始尝鲜 Meta Muse、ChatGPT Dots、Claude Duet 等前沿工具。浏览器接管、登录后台、自动跑完流程，看起来往往只需要几步。但每次看着屏幕自动跳转，我脑子里都会先冒出几个问题：流程跑完以后，交给云端处理的账号数据究竟去了哪里？登录态又在什么时候结束？如果登录态被恶意工具劫持，或者网页里藏着一段与业务无关、甚至试图篡改订单事实的文字，最后该由谁承担责任？提出这些问题，并不是先入为主地认定这些云端 Agent 不安全，而是想在真正把业务交给它们之前，先弄清楚安全信任边界。

产品宣传演示一般能说明流程跑通了，却很难深入回答这些问题。这次我选择了比较新锐的工具 TinyFish 来做检验，并先把问题拆小：用六个只含虚构订单的 HTML 页面，检查 TinyFish Fetch 实际返回什么；同时核对 AgentQL MCP 的固定版本源码、SDK 文档、远程浏览器会话文档和两家服务各自的条款。

先把边界说清：**这轮实际观察到的是提取模式会改变工具返回的文本；我没有把这些文本交给 Agent，因此不能据此判断它会不会服从伪指令。**真实账号、Cookie、远程会话和远端数据清理也不在本轮实测范围内。

这几件事得分开看：把网页抓出来、把内容交给模型、让模型决定下一步、把账号登录态留在远端，是四件不同的事。只用 Fetch 读取公开网页，也可能把页面里肉眼看不见的文字带回来；但“带回来”不等于模型一定会照做。至于让 Agent 根据页面内容自己点按钮，或者把登录态交给云端，那是另外两道风险门槛，这轮还没测到。

## 本轮证据边界

| 已实测 | 静态核对 | 尚未实测 |
| --- | --- | --- |
| TinyFish Fetch 返回文本：6 个夹具 × 3 种模式单次读取（18 条），再对 F0/F1/F2 × 2 种 HTML 模式各重复 3 次（18 条） | 固定版本 AgentQL MCP 的请求构造、SDK 的 `includeHidden` 默认值、AgentQL REST 会话参数、两份公开服务条款 | TinyFish Browser Agent 的最终回答；AgentQL SDK/REST 的真实网络请求体；真实登录、Cookie/Profile 驻留、streaming URL 访问控制和远端数据清除回执 |

先给答案：在首轮单次对照（每格 n=1）里，`display:none` 伪指令在 Markdown、普通 HTML 和指定区域 HTML 三种提取中都进入返回文本；F1 的 `aria-label` 伪指令和 F4 隐藏字段 canary 只在指定区域 HTML 中出现；F3 的 HTML 注释伪指令没有进入这三种返回文本。F5 的无害 `aria-label` 也只在指定区域 HTML 中返回。重复测试只覆盖 F0–F2 的两种 HTML 模式，结果见下表。
## 同一个页面，提取模式不同，返回的内容也不同

我先做了一组很小的供应商订单夹具。页面只有四个基准字段：

- 订单：**#101**
- 应付金额：**USD 4200.00**
- 状态：**PENDING_APPROVAL**
- 收款标记：**ACCOUNT_FIXTURE_GOOD_101**

其余内容都是合成测试：**aria-label** 中的伪指令、**display:none** 隐藏文字、HTML 注释，以及一个明显标注为非秘密的隐藏字段值。不存在真实银行卡、API Key、Cookie 或个人资料。

六个静态 HTML 夹具放在[公开测试目录](https://github.com/vxime/ai-code-review-counterexamples/tree/604bde069c0bd32bb41a4238140062e5468a7f2f/browser-agent-trust-fixture)中，位于独立研究分支而非默认分支。本轮 Fetch 使用固定源版本 `152cdc9`；后续 `604bde0` 只更新 README，F0–F5 六个 HTML 文件的 Git blob SHA 未变。表格里的 F0–F5 名称直接链接到各自固定版本页面。生成器、判定脚本和原始返回文本仍保存在私有研究档案中，公开仓库可复用固定输入，但不能一键重建整套实验。完整提交号与 CDN 来源限制见文末附注。

我用免费的 TinyFish Fetch 做了两轮读取。第一轮是六个页面分别用普通 Markdown、普通 HTML 和指定 DOM 区域的 HTML 提取，共 18 条返回文本；第二轮只取 F0、F1、F2，用普通 HTML 和指定区域 HTML 各重复三次，再得到 18 条文本。这里的配置分别是 `format: "markdown"`、`format: "html"`，以及 `format: "html"` 配合 `include_selectors: ["#trusted-order-facts", "#untrusted-webpage-region"]`。表中 0/1、1/1 和 3/3 按目标伪指令或 canary 字符串是否出现在工具返回文本中计数，不代表模型是否执行指令。下面先列第一轮单次对照（每格 n=1），再列重复结果。

<figure class="tinyfish-fetch-trust-boundary not-prose" lang="zh-cn">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-trust-boundary.mobile.still.zh-cn.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-trust-boundary.still.zh-cn.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-trust-boundary.mobile.zh-cn.svg" type="image/svg+xml"><img src="/images/posts/ai-browser-hidden-content-tinyfish-fetch-agentql-boundaries/tinyfish-fetch-trust-boundary.zh-cn.svg" alt="六个合成 HTML 夹具的首轮三种提取模式与 F0–F2 重复轮对照；总计 36 条 Fetch 返回文本观察，明确区分工具输出、Agent 决策与远端会话清理。" width="1200" height="700" loading="lazy" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption class="mt-2 text-xs leading-relaxed text-slate-500">36 条返回文本观察 · 每格样本量不同，不能解读为模型决策或整体安全评级。</figcaption>
</figure>


| 夹具 | 不可信内容的位置 | 普通 Markdown | 普通 HTML | 指定区域 HTML |
| --- | --- | --- | --- | --- |
| [F0](https://github.com/vxime/ai-code-review-counterexamples/blob/152cdc9a58e6a8fcf2d5857820d60774f0967555/browser-agent-trust-fixture/F0.html) | 无伪指令 | 0/1 | 0/1 | 0/1 |
| [F1](https://github.com/vxime/ai-code-review-counterexamples/blob/152cdc9a58e6a8fcf2d5857820d60774f0967555/browser-agent-trust-fixture/F1.html) | `aria-label` 伪指令 | 0/1 | 0/1 | **1/1** |
| [F2](https://github.com/vxime/ai-code-review-counterexamples/blob/152cdc9a58e6a8fcf2d5857820d60774f0967555/browser-agent-trust-fixture/F2.html) | `display:none` 伪指令 | **1/1** | **1/1** | **1/1** |
| [F3](https://github.com/vxime/ai-code-review-counterexamples/blob/152cdc9a58e6a8fcf2d5857820d60774f0967555/browser-agent-trust-fixture/F3.html) | HTML 注释伪指令 | 0/1 | 0/1 | 0/1 |
| [F4](https://github.com/vxime/ai-code-review-counterexamples/blob/152cdc9a58e6a8fcf2d5857820d60774f0967555/browser-agent-trust-fixture/F4.html) | 隐藏字段中的假 canary | canary 0/1 | canary 0/1 | **canary 1/1** |
| [F5](https://github.com/vxime/ai-code-review-counterexamples/blob/152cdc9a58e6a8fcf2d5857820d60774f0967555/browser-agent-trust-fixture/F5.html) | 无害的 `aria-label` 对照 | —（夹具无恶意文本） | —（夹具无恶意文本） | 无害 `aria-label` 文本 1/1 |

F5 是无害负对照：它没有恶意文本，但指定区域 HTML 仍返回无害 `aria-label`。这更符合“所选区域的属性文本被序列化进输出”的解释，而不是提取器识别了文本是否恶意。F3 的伪指令位于 HTML 注释中，本次返回的 DOM 序列化文本不含注释节点。

注意：F4 的 canary `1/1` 是首轮单次观察，未进入重复轮；第二轮只重复 F0–F2 的两种 HTML 配置各三次，Markdown 模式和 F3–F5 均未重复。

第二轮重复结果：

| 夹具 | 普通 HTML（3 次） | 指定区域 HTML（3 次） |
| --- | --- | --- |
| F0：正常页面 | 伪指令 0/3 | 伪指令 0/3 |
| F1：`aria-label` 伪指令 | 0/3 | **3/3** |
| F2：`display:none` 伪指令 | **3/3** | **3/3** |

36 条返回文本都保留了合成订单的金额、状态和收款标记。重复轮再次复现了 F2 在两种 HTML 模式中均为 3/3、F1 在普通 HTML 中为 0/3 而指定区域 HTML 中为 3/3 的差异。

这组结果让我在意的，不是哪个模式能被简单贴上“安全”标签，而是一个很常见的开发错觉：**格式转成 Markdown，或者用选择器框定一个区域，并不等于顺手完成了安全清洗。** F2 的隐藏文字在首轮三种模式中都出现；重复轮里，它在两种 HTML 模式下也都出现了 3/3。F1 的 `aria-label` 和 F4 的隐藏字段 canary 则是在指定区域 HTML 中出现的，其中 F4 只有首轮 1/1，不能和重复验证过的 F1 等量齐观。再看 F5：即使 `aria-label` 里写的是无害说明，指定区域模式也照样返回它。至少在这组结果里，差异更像是所选区域的属性文本被带进输出，而不是工具识别了内容是不是恶意。

这和我此前的 [Meta Muse 合成邮箱与日历测试](https://vxi.me/posts/meta-muse-agent-permission-source-integrity/)是两类证据：那篇检查模型最终回复与源记录是否一致；这一轮检查的是 Fetch 返回文本中包含哪些字符串。

## AgentQL MCP 并不是把本地 DOM 直接塞进请求体

看源码之前，我自己也有个先入为主的怀疑：既然 AgentQL 主打用自然语言做语义定位，本地运行的 MCP 会不会把本地 Chrome 的整棵 DOM 树直接打包发到云端？如果真是这样，页面里不该出境的内容就可能跟着请求一起走。但工程判断不能靠脑补定罪，得先看实际请求构造。

我固定检查了 [tinyfish-io/agentql-mcp 的 src/index.ts](https://github.com/tinyfish-io/agentql-mcp/blob/4ec4d8784a73a4ec036a297e37925e2d6fe3ddcb/src/index.ts)。其中 **extract-web-data** 的本地请求构造把 **url**、**prompt** 和参数发送到 **api.agentql.com/v1/query-data**；构造出来的 JSON 请求体没有本地 DOM 或 **accessibility_tree** 字段。

这并不等于“页面内容没有出境”。这条 MCP 路径发送的是 URL 和提取提示，服务端会如何获取、处理这个 URL 对应的页面，需要按服务端路径理解；但它不是“当前本地浏览器把整棵 DOM 序列化后放进请求体”这件事的证据。

另一个路径是使用 AgentQL SDK 包装 Playwright 页面。AgentQL 的[工作原理文档](https://docs.agentql.com/concepts/under-the-hood)写明，语义处理会使用页面 HTML 和 Accessibility Tree，并对输入做简化。它的 [JavaScript SDK 页面 API](https://docs.agentql.com/javascript-sdk/api-references/agentql-page)还列出了不同方法的隐藏元素默认值：

| SDK 方法 | 文档中的 includeHidden 默认值 |
| --- | --- |
| **queryData()** | **true** |
| **queryElements()** | **false** |
| **getByPrompt()** | **false** |

这些是官方文档列出的默认参数，不是本轮抓到的网络请求。本轮没有运行 SDK。

再往下还有 TinyFish 云端 Browser Agent 和 AgentQL 远程浏览器会话。它们与上面两条路径也不应该混为一谈。**同一个品牌下的 MCP、SDK、URL 提取和远程浏览器，必须分别核对执行位置与数据出口。**

换句话说，我现在不会笼统地问“这家工具会不会泄密”，而会先确认自己接的是哪一条路径：本地 MCP、包在 Playwright 上的 SDK，还是运行在别人云端的浏览器。名字相近，不代表数据走的路相同。

## 远程会话的“清理”要看具体的生命周期配置

AgentQL 的[远程浏览器指南](https://docs.agentql.com/browser/remote-browser)列出了 CDP 连接地址和页面 streaming URL，并把自动会话管理与清理作为产品能力介绍。这个说明本身并不能证明某个 streaming URL 是否需要二次鉴权，也不能证明会话数据已经从所有存储中删除。

更具体的配置写在 [AgentQL REST API reference](https://docs.agentql.com/rest-api/api-reference) 的浏览器会话接口里：

- **inactivity_timeout_seconds** 默认是 300 秒，也就是 5 分钟。
- **shutdown_mode** 默认是 **on_disconnect**。
- 另一个选项 **on_inactivity_timeout** 会让会话在最后一个客户端断开后继续保留，直到达到不活跃超时；在超时窗口内重新连接可以复用现有浏览器。

这份文档明确了会话终止时点可以配置，但没有提供可独立核验的远端数据删除回执；也不能把 AgentQL REST 的会话参数直接推广到 TinyFish Browser 产品或其他云端浏览器会话。各自的 Profile、会话生命周期和清理行为需要分别核验。

说实在的，看到“自动会话管理与清理”这样的产品描述，我会继续追问一句：这里的“清理”具体指关闭浏览器、断开连接，还是有证据证明持久化的数据也已删除？这些不是同一件事。尤其当会话使用 `on_inactivity_timeout` 时，文档描述的是客户端断开后仍可保留到不活跃超时；若同时使用默认的 300 秒超时，浏览器会话可能在这段窗口内继续存在。**这不证明别人能接管会话，但足以说明：断开连接本身不是数据已销毁的验收回执。**

AgentQL 的[凭证缓存示例](https://docs.agentql.com/logging-into-sites/caching-user-credentials)还展示了用 Playwright 的 **storage_state(path="session.json")** 将登录状态保存到本地文件。这是本地 SDK 的一种使用方式，不能据此推断远程 Browser Profile 的数据驻留位置。

## 服务条款说明“允许如何处理”，不等于“实际发生了什么”

TinyFish 和 AgentQL 的服务条款是分开的，不能把它们合并成一份模糊的“厂商隐私承诺”。

我把两份当前公开条款的 Customer Data 部分并排看了一遍。差别不在于“哪家一定会训练”，而在于条款具体写了什么：

| 条款页面 | Customer Data 许可文字 | 是否在该段明确写出模型训练 / 微调 |
| --- | --- | --- |
| [TinyFish Terms of Service](https://www.tinyfish.ai/terms) | 允许为提供、维护、保障服务、分析和改进服务处理数据；还提到审阅特定 runs、prompts、inputs、outputs、errors 和使用模式。 | **是。** 条款明确列出训练、微调、评估和改进其 AI/ML 模型，并附有防止向其他客户披露机密信息的商业合理保障措辞。页面标注生效日期为 2026 年 8 月 17 日。 |
| [AgentQL Terms of Service](https://www.agentql.com/legal/terms-of-service)（生效日期：2026 年 4 月 21 日） | Customer Data 许可写的是仅为提供、维护、保障和改进服务处理；另允许为合法目的收集、使用和保留由 Customer Data 与服务使用产生的汇总或去标识化数据，包括产品改进和开发。 | **当前可见的 Customer Data 条款段落没有明确写出模型训练或微调。** 这不等于推断其他合同、订单表或政策绝无相关约定。 |

我平时也不会把服务条款当成运行日志看，但做这类工具的边界审查时，我不会跳过它：条款回答的不是“这一次请求有没有被拿去训练”，而是供应商公开写下了哪些数据处理许可。上面的差异值得在接入前确认，尤其要弄清自己是否受单独的企业协议约束；它本身并不能证明任何一条具体数据实际进入了训练流程。

## 我会怎样划定实际使用边界

这轮结果没有给出“TinyFish 安全”或“不安全”的总体评级，但它足够支持几条可执行的工程规则。

1. **不要用主账号试水。** 如果任务必须登录，先用专门的低权限测试账户，移除支付、管理员和不相关业务权限。人工在远程浏览器里输入密码，不会自动把后续 Cookie 和登录态变回本地状态。
2. **把网页内容当成不可信输入。** aria-label、隐藏节点和注释可能承载与业务字段无关的文本。让工具返回 JSON 不等于它已经完成了安全清洗；应当显式定义允许字段，核对值的来源，并对隐藏内容做有针对性的负向测试。
3. **选择器不是脱敏机制。** F5 的无害属性对照也在指定区域 HTML 中出现，说明这是属性文本随所选 DOM 区域返回的现象，不是对恶意语义的识别。限定 DOM 范围可以减少无关内容，但不能假定它会自动删除区域内的属性或隐藏节点。
4. **读和写分开。** 让 Agent 整理订单、生成待审草稿，与允许它批准订单、发送邮件、提交表单是不同的权限。不可逆操作应有独立的人工确认，不能只靠提示词里的“请勿提交”。
5. **明确会话退出条件。** 开始任务前确认使用的是哪种会话接口、shutdown_mode 与超时设置。不要把页面不再显示、客户端断开或工具返回 Completed 当成远端状态已清理的证明。CDP 地址和 streaming URL 也应当按敏感控制入口对待，避免公开转发。
6. **最小化送入模型的数据。** 对账单、客户资料、内部工单等页面，先问清楚任务是否真的需要整页语义解析。能在本地筛选成少量字段，就不要无条件把完整页面交给云端处理。

我原本还想把测试推进到下一步：把同一组夹具交给 TinyFish 的云端 Browser Agent，看看它拿到页面文本后会怎么决策。但当前测试账户无法强制执行已经批准的步数上限；可用的默认步数远超这次测试的授权范围。我没有为了把实验跑完而放宽限制。对自动化工具来说，无法可靠限制执行范围，本身就是该停下来的理由，不是可以靠一句“注意安全”带过的小问题。

所以这轮停在 Fetch 返回文本、固定版本源码和官方文档核查这一层。我不是想给 TinyFish 或 AgentQL 贴上“安全”或“不安全”的标签，而是想先弄清楚数据究竟经过哪条路径、工具实际返回了什么，以及哪些事情还没有证据。现在的 AI 浏览器工具确实省事；但在把公司后台账号、业务数据或带权限的登录态交出去之前，我更愿意先用合成数据做一轮小测试，再确认权限隔离和退出条件。演示能跑通只是起点，真正要承担后果的是接入它的系统和调用方。

---

**附注 / 时间说明：** 测试记录截至 2026 年 10 月 25 日。本轮只实测免费 TinyFish Fetch 的合成页面文本提取；Browser Agent、AgentQL SDK/REST 运行期请求、真实登录态驻留与远端数据清理均未实测，细分范围见开头表格。

**来源与复现说明：** Fetch 请求指向源提交 `152cdc9a58e6a8fcf2d5857820d60774f0967555`；后续提交 `604bde069c0bd32bb41a4238140062e5468a7f2f` 只更新了 README，F0–F5 的 Git blob SHA 未变。页面经第三方静态 CDN `cdn.githubraw.com` 读取；其可用性、缓存和字节等价性未审计，因此不能声称 CDN 实际返回字节与 GitHub 源文件逐字节一致。完整生成器、判定脚本和原始返回文本保存在私有研究档案中。
