🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

AI 代理本地浏览器 TrueNix/agent-browser

MCP
TrueNix20 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

给 AI 智能体用的加固本地浏览器。零运行时依赖。 通过 DevTools Protocol 驱动你已有的…

30.4/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/17 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/TrueNix/agent-browser.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
生态插件
可安装,未声明 dsh 能力
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 20 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包@truenix/agent-browser(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=18 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/9/22 19:20:52

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 43 处,展示前 32 条)
  • 读取本地文件
    src/browser.mjs:70
    if (existsSync(candidate)) return candidate;
  • 读取本地文件
    src/browser.mjs:76
    if (dir && existsSync(join(dir, candidate))) return join(dir, candidate);
  • 读取本地文件
    src/browser.mjs:84
    try { return existsSync('/dev/dri/renderD128') || existsSync('/dev/dri/card0') || existsSync('/dev/dri/card1'); }
  • 执行外部命令固定命令
    src/browser.mjs:199
    try { inUse = execFileSync('ps', ['-eo', 'args'], { encoding: 'utf8', timeout: 5000 }); }
  • 读取本地文件
    src/browser.mjs:201
    try { entries = readdirSync(tmpdir()).filter((name) => name.startsWith('agentbrowser-')); }
  • 读取本地文件
    src/browser.mjs:207
    try { owner = JSON.parse(readFileSync(join(dir, OWNER_FILE), 'utf8')); }
  • 读取本地文件
    src/browser.mjs:221
    const { mtimeMs } = statSync(dir);
  • 删除本地文件递归
    src/browser.mjs:230
    try { rmSync(dir, { recursive: true, force: true }); profiles += 1; } catch { }
  • 执行外部命令来自配置/变量
    src/browser.mjs:272
    const reported = execFileSync(binary, ['--version'], { encoding: 'utf8', timeout: 10000 }).trim();
  • 执行外部命令来自配置/变量
    src/browser.mjs:400
    const child = spawn(binary, [...flags, 'about:blank'], {
  • 写入本地文件
    src/browser.mjs:408
    writeFileSync(join(profileDir, OWNER_FILE),
  • 删除本地文件递归
    src/browser.mjs:463
    try { rmSync(profileDir, { recursive: true, force: true }); } catch { }
  • 读取本地文件
    src/mcp.mjs:24
    try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version; }
  • 发起网络请求
    src/ws.mjs:55
    const request = (secure ? https : http).request({
  • 执行外部命令来自配置/变量
    src/daemon.mjs:104
    const child = spawn(process.execPath, [cli, 'daemon'], { detached: true, stdio: 'ignore' });
  • 读取本地文件
    src/daemon.mjs:161
    if (process.platform === 'win32' || !existsSync(path)) return;
  • 删除本地文件
    src/daemon.mjs:167
    if (notRunning(err)) { try { unlinkSync(path); } catch { } }
  • 删除本地文件
    src/daemon.mjs:205
    if (process.platform !== 'win32') { try { unlinkSync(socketPath()); } catch { } }
  • 写入本地文件
    src/cli.mjs:318
    writeFileSync(file, bytes);
  • 写入本地文件
    src/cli.mjs:325
    writeFileSync(file, bytes);
  • 读取本地文件
    src/install.mjs:20
    const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
  • 读取本地文件
    src/install.mjs:33
    let raw = existsSync(cordisPatchPath) ? readFileSync(cordisPatchPath, 'utf8') : '';
  • 读取本地文件
    src/install.mjs:33
    let raw = existsSync(cordisPatchPath) ? readFileSync(cordisPatchPath, 'utf8') : '';
  • 写入本地文件
    src/install.mjs:44
    writeFileSync(cordisPatchPath, next);
  • 读取本地文件
    src/install.mjs:103
    if (!existsSync(pkgPath)) return { changed: false };
  • 读取本地文件
    src/install.mjs:104
    const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
  • 写入本地文件
    src/install.mjs:107
    writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
  • 读取本地文件
    src/install.mjs:113
    if (!existsSync(dir)) {
  • 执行外部命令来自配置/变量
    src/install.mjs:138
    execSync(cmd, { cwd: dir, stdio: 'inherit' });
  • 写入本地文件
    src/install.mjs:158
    writeFileSync(patchPath, next);
  • 写入本地文件
    src/install.mjs:168
    writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
  • 执行外部命令来自配置/变量
    src/install.mjs:171
    try { execSync(cmd, { cwd: dir, stdio: 'inherit' }); }
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 驱动本地Chrome
  • 提取Markdown
  • 绕过机器人检测

为 AI 智能体打造的加固本地浏览器——零依赖 CDP 客户端、命令行、MCP 服务器和 Cordis 插件。Markdown 省 token,能过常规机器人检测。无需远程服务。

README

由 DeepSeek 最新模型翻译生成
agent-browser 给 AI 智能体用的加固本地浏览器。零运行时依赖。 通过 DevTools Protocol 驱动你已有的 Chrome,提取省 token 的 Markdown,而且不容易被识别为自动化。 | 入口 | 安装 | 用途 | | MCP 服务器 | npx -y @truenix/agent-browser mcp | Claude Code、Cursor、Codex、任何 MCP 客户端 | | 命令行 | npx -y @truenix/agent-browser markdown <url> | shell、脚本、CI | | 库 | import { withBrowser } from '@truenix/agent-browser' | 你自己的 Node 代码 | | DSH / Cordis 插件 | composition row | DSH harness 里的原生工具 | npx -y @truenix/agent-browser markdown https://news.ycombinator.com 一切都在本地运行。不用账号,不用 API key,不连远程服务,没有配额。 为什么 把完整的渲染后 DOM 喂给智能体,会浪费掉大部分上下文。
展开完整文档(README 剩余 1 节)
agent-browser

给 AI 智能体用的加固本地浏览器。零运行时依赖。 通过 DevTools Protocol 驱动你已有的 Chrome,提取省 token 的 Markdown,而且不容易被识别为自动化。

| 入口 | 安装 | 用途 |
| MCP 服务器 | npx -y @truenix/agent-browser mcp | Claude Code、Cursor、Codex、任何 MCP 客户端 |
| 命令行 | npx -y @truenix/agent-browser markdown <url> | shell、脚本、CI |
| 库 | import { withBrowser } from '@truenix/agent-browser' | 你自己的 Node 代码 |
| DSH / Cordis 插件 | composition row | DSH harness 里的原生工具 |

npx -y @truenix/agent-browser markdown https://news.ycombinator.com

一切都在本地运行。不用账号,不用 API key,不连远程服务,没有配额。

为什么

把完整的渲染后 DOM 喂给智能体,会浪费掉大部分上下文。在真实页面上实测:

而且,一个自称自动化的浏览器会被拦截、降级,或者拿到不一样的内容——这会悄悄污染智能体得出的结论。

渲染页面基准测试(实时 DOM vs Markdown)

每格是 UTF-8 字节数 / 精确的 o200k_base token 数。第一份数据是 JavaScript 执行完、页面 load 事件之后的序列化 DOM,不是 curl 响应。缩减倍数只比较页面输入的 token。

| 站点 | 渲染后 DOM | Markdown | --links text | 精确 token 缩减 |
| en.wikipedia.org/wiki/WebAssembly | 861 kB / 282k tok | 82.6 kB / 21.9k tok | 48.0 kB / 12.2k tok | 12.87× / 23.14× |
| react.dev | 273 kB / 108k tok | 11.3 kB / 3.1k tok | 8.6 kB / 1.9k tok | 34.73× / 57.23× |
| nextjs.org | 337 kB / 124k tok | 5.3 kB / 1.2k tok | 4.2 kB / 0.9k tok | 100× / 131.05× |
| github.com/trending | 666 kB / 218k tok | 7.7 kB / 2.2k tok | 4.2 kB / 1.1k tok | 100.96× / 200.63× |
| apple.com | 362 kB / 143k tok | 4.1 kB / 1.1k tok | 3.1 kB / 0.9k tok | 124.4× / 166.69× |
| news.ycombinator.com | 34.5 kB / 11.7k tok | 10.3 kB / 3.4k tok | 3.2 kB / 1.1k tok | 3.42× / 10.86× |

从源码检出重新生成两张图和这张表背后的测量数据,运行 npm run charts——它会实时测量,然后用 agent-browser 自己画出 PNG,更新表格,并把完整的机器可读快照写到 benchmark-results.json。
精确分词器是开发依赖;发布的浏览器包仍然零依赖。

少花页面输入的 token

在导航密集的页面上,链接可能占掉提取结果的大头。智能体在阅读而不是导航时,可以丢掉链接目标:

agent-browser markdown https://github.com/trending --links text
agent-browser markdown https://en.wikipedia.org/wiki/Rust --links relative

| 模式 | 渲染结果 | 什么时候用 |
| inline (默认) | text | 智能体接下来会跳转 |
| relative | text | 站内爬取;保留目标,去掉源站 |
| text | text | 阅读、总结、问答 |

重站点基准测试

页面输入 token:渲染后的 DOM 对比 agent-browser Hacker News + YC Blog 的精确 o200k_base 页面输入 token。不含提示词、工具 schema、重试、缓存、模型输出和服务商定价;这不是端到端成本估算。

加载后才渲染的页面

导航等待 load 事件,由 CDP 生命周期事件驱动,不用轮询循环加固定延迟。在真实页面上,输出字节完全一致,速度快 2–3 倍:

| 页面 | --wait load(默认) | 旧的固定 250 ms 等待 |
| example.com | 7 ms | 258 ms |
| github.com/trending | 115 ms | 340 ms |
| nextjs.org | 160 ms | 365 ms |
| news.ycombinator.com | 218 ms | 466 ms |

客户端渲染的应用,内容在 load 之后才到达,需要真实信号,不是把猜测时间调大——旧的 250 ms 等待同样会漏掉这些内容:

agent-browser markdown https://some-spa.example --wait idle

| 模式 | 等待什么 | 什么时候用 |
| domcontentloaded | DOM 解析完成 | 你只需要 HTML 里自带的标记 |
| load (默认) | load 事件 | 几乎总是 |
| idle | 网络安静下来 | 结果看起来像空壳 |

你不必自己发现空壳。结果看起来像未渲染的 Loading… 页面,或者站点对自己的数据请求做了限流(HTTP 429——社交站点对未登录访客很常见),agent-browser 会加一条警告,说明发生了什么、该怎么办:命令行输出到 stderr,--json 时放在 warnings 下,MCP 结果里则是单独的 [agent-browser warning] 块。

其他 harness

如果你的框架支持 JSON Schema,它就能驱动 agent-browser,而本项目无需知道这个框架存在。agent-browser tools 按你需要的形状打印工具目录:

agent-browser tools # MCP: {name, description, inputSchema}
agent-browser tools --format openai # OpenAI: {type:"function", function:{...}}
agent-browser tools --format anthropic # Anthropic: {name, description, input_schema}

之后每个工具都能以两种方式运行,取决于你的 harness 能做哪种:

import { findTool, TOOLS } from '@truenix/agent-browser/tools';
import { withBrowser } from '@truenix/agent-browser';

// in-process: the harness can hold a CDP session
const tool = findTool('browser_markdown');
const text = await withBrowser({}, async (session) => {
await session.navigate('https://example.com');
return tool.run(session, { url: 'https://example.com', links: 'text' });
});

// out-of-process: the harness can only run a command (sandboxes, shells)
tool.cli({ url: 'https://example.com', links: 'text' });
// => ['markdown', 'https://example.com', '--links', 'text']

安装

作为 MCP 服务器

Claude Code

claude mcp add browser -- npx -y @truenix/agent-browser mcp

Cursor / Windsurf / 通用 mcpServers JSON

{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "@truenix/agent-browser", "mcp"]
}
}
}

工具:browser_markdown、browser_text、browser_html、browser_links、browser_screenshot、browser_evaluate、browser_accessibility_tree、browser_pdf、browser_probe。

所有入口都从同一份共享目录暴露这九个工具,因此命令行、MCP 服务器和 DSH 插件永远不会各自跑偏。

作为库

npm install @truenix/agent-browser

import { withBrowser } from '@truenix/agent-browser';

const md = await withBrowser({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});

作为 DSH / Cordis 插件

一行命令(推荐 —— 你确实想装 DSH 时会自动接线):

npx -y @truenix/agent-browser install # adds the bundle to ~/.dsh/profiles/web/package.json, then pnpm install (the mount ships in the package's own cordis.patch.yml layer)
npx -y @truenix/agent-browser install --profile web --dry-run # preview
npx -y @truenix/agent-browser uninstall # remove again

重启 dsh —— 九个 browser_ 工具会作为原生工具出现。直接跑 npm install 不会执行任何处理逻辑;必须显式执行 install。

手动安装(如果你想自己编辑组合):

npm i -g @truenix/agent-browser
or inside the harness checkout: pnpm add @truenix/agent-browser
需要 Node ≥ 18,并安装 Chrome/Chromium。然后加到宿主组合里(工具注册表在宿主上,不在每个智能体上):

~/.dsh/profiles/web/cordis.patch.yml — persists for every web session
- insert:
- id: agent-browser
name: '@truenix/agent-browser/cordis'
config:
timeoutMs: 180000 # per-tool call budget; default respects AGENT_BROWSER_BIN / ENDPOINT
cli: 'npx -y @truenix/agent-browser' # override only if needed

简写形式(组合里已经包好 insert 时):

- '@truenix/agent-browser/cordis':
timeoutMs: 180000

环境变量覆盖:AGENT_BROWSER_BIN(Chrome 二进制路径)、AGENT_BROWSER_ENDPOINT(通过 --endpoint 挂到长期运行的浏览器),或 config.cli。

命令行

agent-browser <command> [options]

markdown <url> Extract the whole page as Markdown (main content by default)
text <url> Visible text only
html <url> Full serialized DOM after JavaScript runs
links <url> Every anchor as JSON
screenshot <url> PNG/JPEG (-o file, --full)
pdf <url> PDF (-o file)
a11y <url> Filtered accessibility tree
eval <url> <expr> 执行 JS,只返回结果值(开销最低) probe 浏览器、GPU 与能力报告 mcp 以 MCP 服务器方式运行在 stdio 上 tools 打印工具 schema(--format mcp|openai|anthropic)

选项:--headful、--no-stealth、--block-images、--gpu/--no-gpu、--width、--height、--viewport WxH、--main、--raw、--links、--max-rows、--limit、--wait、--settle、--world isolated|main、--full、--endpoint <ws>、--timeout、--json、-o。

用 markdown 阅读,用 eval 查东西

智能体用这个工具最容易犯的昂贵错误,是为了回答一个单行问题而渲染整个页面。三个重页面上的三个针对性问题,用 eval 只花 235 字节,而 Markdown 要 77 kB:

agent-browser eval https://github.com/trending \
"JSON.stringify(Array.from(document.querySelectorAll('article h2 a')).slice(0,3).map(a=>a.innerText.trim()))"
["openai / codex","mattpocock / skills","affaan-m / ECC"] -> 58 bytes

用 innerText,别用 textContent。textContent 返回的是原始源码里的空白("openai /\n\n codex");innerText 给的是实际渲染出来的内容("openai / codex")。智能体这里搞错,就要多付一整轮重试往返,代价远高于它省下的那点字节。

确实需要阅读、总结或搜索页面时,才用 markdown。

每次调用都会启动自己的浏览器(约 1.4 秒)。连续做几次查询时,保持一个浏览器存活,把 --endpoint 指向它——同样三个问题,三次冷启动共 5.9 秒,对着热浏览器只要 3.2 秒。

--endpoint 连接到已在运行的浏览器,而不是新启动一个——适合在多次调用间复用同一个长期存活的浏览器。

内存

Chrome 的地板大约是 18 个进程共 420 MB PSS,这还没加载任何东西,而且这个地板是 Chrome 的,不是本包的——调参数只能挪动约 5%,能把地板挪得更远的那个参数(--enable-low-end-device-mode)会让 navigator.deviceMemory: 2 和 24 核并列出现,一台不可能的机器,正是那种会让浏览器被标记的不一致。所以杠杆是更少的浏览器,不是更小的浏览器。

MCP 服务器保持一个浏览器,给每次工具调用分配自己独立的上下文。并发抓取三个重页面:

| | 峰值 PSS | 进程数 | 实际耗时 |
| 每次调用一个浏览器 | 1289 MB | 44 | 2114 ms |
| 一个浏览器,3 个上下文 | 654 MB | 20 | 2049 ms |

隔离性不变——提供隔离的一直是每次调用的浏览器上下文。浏览器空闲 30 秒后关闭(AGENT_BROWSER_IDLE_MS,设为 0 立即关闭),所以长期存活的服务器不会在对话之间占着 420 MB。热着的时候重复调用完全跳过启动,速度约快一倍。

代价是:任务共享一个进程树,所以浏览器级别的崩溃会带走所有在途调用共享同一个浏览器,而不是每次调用都新建。浏览器死掉后会被检测到,并在下次调用时重新拉起。如果需要按任务隔离影响范围,用 withBrowser,它仍然给每次调用分配独立浏览器。

import { withPooledSession, shutdownPool } from '@truenix/agent-browser/pool';

await withPooledSession({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});
await shutdownPool(); // or let it idle out

命令行这边,每次调用都是独立进程,所以想复用就得把 --endpoint 指向你自己保活的浏览器。

守护进程:所有命令行调用共用一个浏览器

每次命令行调用都是独立进程,默认各自冷启动一个 Chrome——十个并发调用就是十个浏览器。--daemon 改为共用一个常驻浏览器,每次调用仍然拿到自己隔离的上下文:

agent-browser markdown https://example.com --daemon
agent-browser daemon --status
agent-browser daemon --stop

| | 实际耗时 | Chrome 进程峰值 |
| 3 次串行查询,不用守护进程 | 5330 ms | — |
| 3 次串行查询,--daemon | 3254 ms | — |
| 10 个并发调用,不用守护进程 | 1443 ms | 140 |
| 10 个并发调用,--daemon | 1144 ms | 32 |

这是可选项(--daemon,或 AGENT_BROWSER_DAEMON=1),因为启动一个活得比你的命令还久的后台进程属于副作用,值得先问一声。它按需启动,空闲五分钟后退出(AGENT_BROWSER_DAEMON_IDLE_MS)。设置这个环境变量还会让 MCP 服务器也走它,一台机器上多个 MCP 客户端共用时值得这么做。

--headful、--width、--height、--block-images 和 --gpu 在浏览器启动时就固定了,共享浏览器没法满足。传了其中任何一个就以它为准:那次调用会悄悄拿到自己的私有浏览器,并在 stderr 上说明。

它围绕四种失败模式设计,每种都有测试验证:

- 有且只有一个守护进程。 十个同时发起的首次调用都会去启动,九个在抢 socket 绑定时落败,还没启动任何东西就退出。绑定 socket 本身就是锁,所以没有会过期的锁文件。
- 打开的 socket 就是引用计数。 客户端干活期间一直握着连接,所以即使被 SIGKILL 杀掉也会释放——内核会关掉 socket。换成「请释放」的消息,引用就会永久泄漏。
- 空闲退出,这样两段对话之间它不会一直占着约 420 MB。
- 被 SIGKILL 的守护进程不会留下烂摊子:它的配置档标记里写着 pid,下次启动时下面的清扫会回收那个浏览器。

自己收拾干净

每次启动都会往临时配置档里写一个属主标记,并清扫属主已死的配置档,杀掉还挂在它们上面的浏览器。配合 SIGINT/SIGTERM/SIGHUP 处理器,效果是:

| 调用如何结束 | 遗留进程 | 下次启动后 |
| 正常结束 | 0 | 0 |
| SIGTERM / SIGINT | 0 | 0 |
| SIGKILL(无法捕获) | 1 个浏览器 | 0 |

所以一次硬杀最多留下一个滞留的浏览器,而不是每个被中断的调用各留一个。十个并发命令行调用反复执行,也不会留下任何东西。活着的浏览器永远不会被清扫——它的属主进程还在运行;无法归属的配置档也会被放过,直到没有任何进程打开它、且它已空闲 60 s。

机器人检测

自己跑一遍:npm run test:bot。最新结果:

| 检测器 | 结果 |
| bot.sannysoft.com | 31 项通过,0 项失败 |
| bot-detector.rebrowser.net | 8 项绿色,0 项不安全;主动隔离探测保持灰色 |
| deviceandbrowserinfo.com | isBot: false,22 项检查中 0 项被标记 |
| arh.antoinevastel.com | SKIP,检测器返回 HTTP 502 |

普通无头 Chrome 在 sannysoft 上有四行失败(HEADCHR_UA、CHR_MEMORY、WebGL SwiftShader、旧 UA),会被判定为机器人。

网络故障或检测器侧 5xx 记为 SKIP,绝不算通过。已加载的 4xx、挑战页、不完整页和无法识别的页面一律按失败处理。这道关卡要求三次可达的通过,所以测试站点宕机的那一天不会被误判为成功。

环境比打补丁更重要

2.0.1 版本,在两处测量:

| | 本工作站 | GitHub Actions runner |
| IP | 住宅 | 数据中心 |
| GPU | 真实(NVIDIA) | 无 → SwiftShader |
| bot.sannysoft.com | 31 项通过,0 项失败 | 30 项通过,1 项失败(WebGL Renderer) |
| bot-detector.rebrowser.net | 6 项绿色,0 项红色 | 6 项绿色,0 项红色 |
| deviceandbrowserinfo.com | isBot: false | isBot: true(hasSuspiciousWeakSignals) |

那次运行中,这些检测套件所检验的每一项 CDP 层信号,在两处都保持非阳性。真正翻转结论的是环境:数据中心 ASN 加上软件渲染,触发了一个弱信号组合,再多的指纹补丁也解决不了。

这就是问题的真实形态。加固浏览器能去掉显而易见的破绽。其余由你在哪里运行决定。

加固做了什么,为什么

每一项都来自检测器告诉我们哪里不对:

- 不启用受自动化控制的 Blink 特性。 Chrome 的无头路径和 --remote-debugging-port=0 路径通常会暴露 navigator.webdriver = true。启动器禁用 AutomationControlled,也不加 --enable-automation,所以该值保持 false。
- 不启用 Runtime.enable。 它是最响亮的 CDP 破绽,也是经典 console/Error.stack 检测器的动力来源。Runtime.evaluate 不用它也能正常工作。
- 默认隔离求值。 读取和 Markdown 提取共享 DOM,但不碰页面安装的全局变量或原型钩子。eval --world main 是显式的逃生通道,供需要页面定义 JavaScript 的代码使用。
- 窗口和屏幕一起移动。 只给 --window-size、不给 --ozone-override-screen-size,会得到 outerWidth > screen.width,物理上不可能——这比单纯无头模式是更强的信号。
- 不覆盖默认设备指标。 常见的 1280×720 是 Playwright 的默认视口,检测器会点名标记它。只有确实需要时才设 --viewport。
- 有可用 GPU 就用真 GPU,拿到真实的 ANGLE (NVIDIA …) 渲染器,而不是 SwiftShader。
- UA 在启动时设置,不只走 CDP。 Emulation.setUserAgentOverride 到不了 Web Worker,于是 worker 继续上报无头 UA,页面却上报干净 UA(hasInconsistentWorkerValues)。
- 不覆盖 acceptLanguage。 CDP 靠拆分这个请求头来推导 navigator.languages,于是 "en-US,en;q=0.9" 变成 ["en-US","en;q=0.9"]——一个本不该合法存在的 q 值,又造成页面与 worker 不一致。--lang 才是正确做法。
- Client Hints 由二进制自身的版本推导,所以 Sec-CH-UA 不会和 navigator.userAgent 打架。
- 每个会话一个隔离的浏览器上下文,关闭时销毁——每个任务状态干净,不用再起第二个浏览器进程。

反复出现的教训:一致性胜过覆盖面。 上面有四条,都是部分伪装反而让检测更容易的情况,只有跑真实检测器才能发现。

为什么 WebGL 伪装默认关闭

spoofWebgl 确实存在,也实现得很小心——用 Proxy 包住原生 getParameter,所以 Function.prototype.toString 仍然报告 [native code]。它默认关闭,因为实测表明它会反噬。 来自 test/webgl-spoof-experiment.mjs:

| 组别 | 声称的渲染器 | maxTexture | 扩展数 | sannysoft | 结论 |
| 真 GPU,不伪装 | NVIDIA | 32768 | 37 | 0 项失败 | isBot: false ✅ |
| SwiftShader,如实上报 | SwiftShader | 8192 | 35 | 1 项失败 | isBot: false ✅ |
| SwiftShader + 伪装 | NVIDIA | 8192 | 35 | 0 项失败 | isBot: true ❌ |

声称你没有的硬件,只修好一行表面数据,却让综合检测器失败:注入的脚本到不了 Web Worker,worker 仍然报告 SwiftShader,MAX_TEXTURE_SIZE 还是软件值,渲染器字符串却声称是独立显卡。

如实上报的 SwiftShader 能过。精心编造的谎言过不了。不如直接给浏览器一块真 GPU——反正不要钱。

这个项目不做什么

指纹层面的检测就是全部范围。它不破解、也不打算破解:

- TLS/JA3-JA4 与 HTTP/2 指纹——在任何 JavaScript 运行之前就已判定
- IP 信誉——机房还是住宅 ASN,往往才是真正的拦路虎
- 行为分析——鼠标轨迹、时序、停留时长

商业风控产品主要靠这些,所以「过闸」的意思是不会被轻易判定为自动化,绝不是无法检测*。用途限于你自己的站点、测试、无障碍工作和日常智能体浏览。

内存

一套 Chrome 栈大约占 450 MB。关键在架构,不在启动参数:跑一个浏览器、开多个隔离上下文,而不是每个任务起一个浏览器。浏览器只启动一次,之后每次调用都用 --endpoint / AGENT_BROWSER_ENDPOINT 指向它。做文本处理时加 --block-images 有帮助。

零依赖

dependencies 为空,WebSocket 传输层也不例外。

Node 全局的 WebSocket(WHATWG)发不了请求头,而任何需要认证或走代理的 CDP 端点都离不开请求头;undici 又没法单独 import。所以 src/ws.mjs 直接在 node:http(s) 上实现 RFC 6455——握手、掩码、续帧分片、64 位长度、ping/pong——CDP 需要的全都在里面。

Markdown 转换器用显式栈遍历 DOM,不管嵌套多深,JS 调用深度都保持 O(1);叶子级行内节点直接用原生 innerText。这样既能在深层嵌套文档上安全递归,在大页面上也明显更快。

环境变量

| AGENT_BROWSER_BIN | Chrome/Chromium 可执行文件的路径 |
| AGENT_BROWSER_ENDPOINT | 连到这个 CDP 端点,而不是自己启动浏览器 |

要求

Node ≥ 18,装好 Chrome/Chromium。无需构建步骤。

致谢

本项目的加固几乎全部来自他人公开发布的检测研究——见 CREDITS.md。特别感谢 rebrowser-bot-detector、bot.sannysoft.com、deviceandbrowserinfo.com 和 Camoufox,它们示范了正确做法。

许可证

MIT
预览分享卡片长什么样 ↓TrueNix/agent-browser
分享 · DeepSeek Harness Hub
TrueNix/agent-browser

给 AI 智能体用的加固本地浏览器。零运行时依赖。 通过 DevTools Protocol 驱动你已有的…

这是你的仓库?认领条目

回到顶部