给 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 能力
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 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🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
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预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
TrueNix/agent-browser
给 AI 智能体用的加固本地浏览器。零运行时依赖。 通过 DevTools Protocol 驱动你已有的…
相关插件 · 浏览器
智能体插件框架deepseek-ai/deepseek-harness
DeepSeek Harnessdsh是由 DeepSeek AI 开发的开源 agent harness智能体框架。
★ 270,422已汉化DSH
71.9
多设备 Agent 看板YaoApp/yao
✨ 所有 Agent 和工作区集中管理,覆盖你的每一台设备。任务看板追踪进度,桌面端、移动端、浏览器、API…
★ 8,147已汉化DSH
71.2
可扩展侧边栏工作台omdsh-dev/DSH-better-sidebar
DSH 的双工作台:原生右侧栏 + 自绘底部面板,并把 ctx.betterSidebar 服务开放给所有插件
★ 4,761已汉化DSH
71.1
浏览器自动化桥Tencent/BrowserSkill
你的 Agent。你的浏览器。连接,就能协作。
★ 9,928已汉化DSH
71
AI 小说创作工作台EthanYoQ/AI-Novel-Writer
AI 小说作家 / AI Novel Writer
★ 1,591已汉化DSH
70.7
智能体浏览器引擎platonai/Browser4
DeepSeek Harness 集成
★ 1,175已汉化DSH
70.4
这是你的仓库?认领条目