feat(stealth): 优化指纹信号并同步文档与包配置

This commit is contained in:
leeguooooo
2026-02-24 14:23:59 +09:00
parent 02ebc9f328
commit 3a91aef4c9
12 changed files with 290 additions and 74 deletions
+9 -9
View File
@@ -1,6 +1,6 @@
# agent-browser
Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallback.
Stealth browser automation CLI for AI agents with anti-bot evasions. Fast Rust CLI with Node.js fallback.
## Installation
@@ -9,7 +9,7 @@ Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallba
Installs the native Rust binary for maximum performance:
```bash
npm install -g agent-browser
npm install -g agent-browser-stealth
agent-browser install # Download Chromium
```
@@ -20,8 +20,8 @@ This is the fastest option -- commands run through the native Rust CLI directly
Run directly with `npx` if you want to try it without installing globally:
```bash
npx agent-browser install # Download Chromium (first time only)
npx agent-browser open example.com
npx agent-browser-stealth install # Download Chromium (first time only)
npx agent-browser-stealth open example.com
```
> **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.
@@ -31,14 +31,14 @@ npx agent-browser open example.com
For projects that want to pin the version in `package.json`:
```bash
npm install agent-browser
npx agent-browser install
npm install agent-browser-stealth
npx agent-browser-stealth install
```
Then use via `npx` or `package.json` scripts:
```bash
npx agent-browser open example.com
npx agent-browser-stealth open example.com
```
### Homebrew (macOS)
@@ -722,7 +722,7 @@ AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
```typescript
import chromium from '@sparticuz/chromium';
import { BrowserManager } from 'agent-browser';
import { BrowserManager } from 'agent-browser-stealth';
export async function handler() {
const browser = new BrowserManager();
@@ -931,7 +931,7 @@ Connect to `ws://localhost:9223` to receive frames and send input:
For advanced use, control streaming directly via the protocol:
```typescript
import { BrowserManager } from 'agent-browser';
import { BrowserManager } from 'agent-browser-stealth';
const browser = new BrowserManager();
await browser.launch({ headless: true });
+2 -2
View File
@@ -2176,11 +2176,11 @@ Environment:
AGENT_BROWSER_IOS_UDID Default iOS device UDID
Install (recommended, fastest - native Rust CLI):
npm install -g agent-browser
npm install -g agent-browser-stealth
agent-browser install # Download Chromium (first time)
Try without installing (slower, routes through Node.js):
npx agent-browser open example.com
npx agent-browser-stealth open example.com
Examples:
agent-browser open example.com
+1 -1
View File
@@ -16,7 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for agent-brows
GitHub repository: https://github.com/leeguooooo/agent-browser
Documentation: https://agent-browser.dev
npm package: agent-browser
npm package: agent-browser-stealth
You have access to the full agent-browser documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.
+7 -7
View File
@@ -9,7 +9,7 @@ export const metadata = pageMetadata("installation")
Installs the native Rust binary for maximum performance:
```bash
npm install -g agent-browser
npm install -g agent-browser-stealth
agent-browser install # Download Chromium
```
@@ -20,8 +20,8 @@ This is the fastest option -- commands run through the native Rust CLI directly
Run directly with `npx` if you want to try it without installing globally:
```bash
npx agent-browser install # Download Chromium (first time only)
npx agent-browser open example.com
npx agent-browser-stealth install # Download Chromium (first time only)
npx agent-browser-stealth open example.com
```
> **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.
@@ -31,14 +31,14 @@ npx agent-browser open example.com
For projects that want to pin the version in `package.json`:
```bash
npm install agent-browser
npx agent-browser install
npm install agent-browser-stealth
npx agent-browser-stealth install
```
Then use via `npx` or `package.json` scripts:
```bash
npx agent-browser open example.com
npx agent-browser-stealth open example.com
```
## Homebrew (macOS)
@@ -89,7 +89,7 @@ AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
```typescript
import chromium from '@sparticuz/chromium';
import { BrowserManager } from 'agent-browser';
import { BrowserManager } from 'agent-browser-stealth';
export async function handler() {
const browser = new BrowserManager();
+2 -2
View File
@@ -7,11 +7,11 @@ export const metadata = pageMetadata("")
Browser automation CLI designed for AI agents. Compact text output minimizes context usage. Fast Rust CLI with Node.js fallback.
```bash
npm install -g agent-browser # all platforms (fastest, native Rust CLI)
npm install -g agent-browser-stealth # all platforms (fastest, native Rust CLI)
brew install agent-browser # macOS
# or try without installing
npx agent-browser open example.com
npx agent-browser-stealth open example.com
```
## Features
+1 -1
View File
@@ -176,7 +176,7 @@ Send input events to control the browser remotely.
For advanced use, control streaming directly via the TypeScript API:
```typescript
import { BrowserManager } from 'agent-browser';
import { BrowserManager } from 'agent-browser-stealth';
const browser = new BrowserManager();
await browser.launch({ headless: true });
+1 -1
View File
@@ -69,7 +69,7 @@ export function Header() {
<span>14k</span>
</a>
<a
href="https://www.npmjs.com/package/agent-browser"
href="https://www.npmjs.com/package/agent-browser-stealth"
target="_blank"
rel="noopener noreferrer"
className="hidden sm:block text-sm text-muted-foreground hover:text-foreground transition-colors"
+6 -2
View File
@@ -1,7 +1,7 @@
{
"name": "agent-browser",
"name": "agent-browser-stealth",
"version": "0.14.0",
"description": "Headless browser automation CLI for AI agents",
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module",
"main": "dist/daemon.js",
"files": [
@@ -11,6 +11,7 @@
"skills"
],
"bin": {
"agent-browser-stealth": "./bin/agent-browser.js",
"agent-browser": "./bin/agent-browser.js"
},
"scripts": {
@@ -43,6 +44,9 @@
"browser",
"automation",
"headless",
"stealth",
"anti-bot",
"anti-detection",
"playwright",
"cli",
"agent"
+41 -14
View File
@@ -29,6 +29,8 @@ const binaryPath = join(binDir, binaryName);
// Package info
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
const version = packageJson.version;
const packageName = packageJson.name;
const binCommands = getBinCommands(packageJson);
// GitHub release URL
const GITHUB_REPO = getGitHubRepoFromPackage(packageJson);
@@ -49,6 +51,17 @@ function getGitHubRepoFromPackage(pkg) {
return 'vercel-labs/agent-browser';
}
function getBinCommands(pkg) {
const bin = pkg?.bin;
if (typeof bin === 'string') {
return [pkg.name.replace(/^@[^/]+\//, '')];
}
if (bin && typeof bin === 'object') {
return Object.keys(bin);
}
return ['agent-browser'];
}
async function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
@@ -170,30 +183,37 @@ async function fixUnixSymlink() {
return; // npm not available
}
const symlinkPath = join(npmBinDir, 'agent-browser');
let optimized = false;
for (const commandName of binCommands) {
const symlinkPath = join(npmBinDir, commandName);
// Check if symlink exists (indicates global install)
try {
const stat = lstatSync(symlinkPath);
if (!stat.isSymbolicLink()) {
return; // Not a symlink, don't touch it
continue; // Not a symlink, don't touch it
}
} catch {
return; // Symlink doesn't exist, not a global install
continue; // Symlink doesn't exist, not a global install
}
// Replace symlink to point directly to native binary
try {
unlinkSync(symlinkPath);
symlinkSync(binaryPath, symlinkPath);
console.log('✓ Optimized: symlink points to native binary (zero overhead)');
optimized = true;
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize symlink: ${err.message}`);
console.log(`⚠ Could not optimize symlink (${commandName}): ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
}
}
if (optimized) {
console.log('✓ Optimized: symlink points to native binary (zero overhead)');
}
}
/**
* Fix npm-generated shims on Windows global installs.
* npm generates shims that try to run /bin/sh, which doesn't exist on Windows.
@@ -208,18 +228,21 @@ async function fixWindowsShims() {
return; // Not a global install or npm not available
}
// Path to native binary relative to npm prefix
const packagePath = packageName.replace(/\//g, '\\');
const relativeBinaryPath = `node_modules\\${packagePath}\\bin\\${binaryName}`;
let optimized = false;
for (const commandName of binCommands) {
// The shims are in the npm prefix directory (not prefix/bin on Windows)
const cmdShim = join(npmBinDir, 'agent-browser.cmd');
const ps1Shim = join(npmBinDir, 'agent-browser.ps1');
const cmdShim = join(npmBinDir, `${commandName}.cmd`);
const ps1Shim = join(npmBinDir, `${commandName}.ps1`);
// Only fix if shims exist (indicates global install)
if (!existsSync(cmdShim)) {
return;
continue;
}
// Path to native binary relative to npm prefix
const relativeBinaryPath = 'node_modules\\agent-browser\\bin\\agent-browser-win32-x64.exe';
try {
// Overwrite .cmd shim
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
@@ -236,13 +259,17 @@ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
exit $LASTEXITCODE
`;
writeFileSync(ps1Shim, ps1Content);
console.log('✓ Optimized: shims point to native binary (zero overhead)');
optimized = true;
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize shims: ${err.message}`);
console.log(`⚠ Could not optimize shims (${commandName}): ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
}
}
if (optimized) {
console.log('✓ Optimized: shims point to native binary (zero overhead)');
}
}
main().catch(console.error);
+3 -1
View File
@@ -1,11 +1,13 @@
---
name: agent-browser
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*)
allowed-tools: Bash(npx agent-browser-stealth:*), Bash(npx agent-browser:*), Bash(agent-browser:*)
---
# Browser Automation with agent-browser
Install package: `npm install -g agent-browser-stealth` (CLI command remains `agent-browser` for compatibility).
## Core Workflow
Every browser automation follows this pattern:
+20
View File
@@ -94,4 +94,24 @@ describe('Stealth mode', () => {
expect(userAgentSignals.pageUA).not.toContain('HeadlessChrome');
expect(userAgentSignals.workerUA).not.toContain('HeadlessChrome');
});
it('exposes realistic mimeTypes/pdf/share signals', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const signals = await browser.getPage().evaluate(() => ({
mimeTypesLength: navigator.mimeTypes ? navigator.mimeTypes.length : 0,
pdfViewerEnabled: navigator.pdfViewerEnabled,
hasShare: typeof navigator.share === 'function',
hasCanShare: typeof navigator.canShare === 'function',
hasConnectionDownlinkMax:
!!navigator.connection && typeof navigator.connection.downlinkMax === 'number',
}));
expect(signals.mimeTypesLength).toBeGreaterThan(0);
expect(signals.pdfViewerEnabled).toBe(true);
expect(signals.hasShare).toBe(true);
expect(signals.hasCanShare).toBe(true);
expect(signals.hasConnectionDownlinkMax).toBe(true);
});
});
+174 -11
View File
@@ -60,12 +60,17 @@ function buildStealthScript(options: StealthScriptOptions): string {
patchNavigatorWebdriver(),
patchChromeRuntime(),
patchNavigatorLanguages(),
patchNavigatorPlugins(),
patchNavigatorPluginsAndMimeTypes(),
patchNavigatorPermissions(),
patchWebGLVendor(),
patchCdcProperties(),
patchWindowDimensions(),
patchScreenAvailability(),
patchNavigatorHardwareConcurrency(),
patchNavigatorConnection(),
patchNavigatorShare(),
patchNavigatorContacts(),
patchPdfViewerEnabled(),
patchMediaDevices(),
patchUserAgentData(),
patchUserAgent(),
@@ -158,36 +163,88 @@ function patchNavigatorLanguages(): string {
}
/**
* Inject a realistic navigator.plugins array.
* Inject realistic navigator.plugins and navigator.mimeTypes arrays.
* Headless Chrome reports an empty PluginArray; real Chrome always has a few.
*/
function patchNavigatorPlugins(): string {
function patchNavigatorPluginsAndMimeTypes(): string {
return `(function(){
const makePlugin = (name, description, filename, mimeType) => {
const mime = { type: mimeType, suffixes: '', description, enabledPlugin: null };
const plugin = { name, description, filename, length: 1, 0: mime };
mime.enabledPlugin = plugin;
const makeMimeType = (type, suffixes, description) => {
const mime = Object.create(MimeType.prototype);
Object.defineProperties(mime, {
type: { value: type, enumerable: true },
suffixes: { value: suffixes, enumerable: true },
description: { value: description, enumerable: true },
enabledPlugin: { value: null, writable: true, enumerable: true },
});
return mime;
};
const makePlugin = (name, description, filename, mimes) => {
const plugin = Object.create(Plugin.prototype);
Object.defineProperties(plugin, {
name: { value: name, enumerable: true },
description: { value: description, enumerable: true },
filename: { value: filename, enumerable: true },
length: { value: mimes.length, enumerable: true },
});
mimes.forEach((mime, i) => {
Object.defineProperty(plugin, i, {
value: mime,
enumerable: true,
});
Object.defineProperty(plugin, mime.type, {
value: mime,
enumerable: false,
});
try { mime.enabledPlugin = plugin; } catch {}
});
return plugin;
};
const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format');
const chromePdfMime = makeMimeType(
'application/x-google-chrome-pdf',
'pdf',
'Portable Document Format'
);
const naclMime = makeMimeType('application/x-nacl', '', 'Native Client Executable');
const pnaclMime = makeMimeType('application/x-pnacl', '', 'Portable Native Client Executable');
const plugins = [
makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', 'application/x-google-chrome-pdf'),
makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', 'application/pdf'),
makePlugin('Native Client', '', 'internal-nacl-plugin', 'application/x-nacl'),
makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', [chromePdfMime]),
makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', [pdfMime]),
makePlugin('Native Client', '', 'internal-nacl-plugin', [naclMime, pnaclMime]),
];
const pluginArray = Object.create(PluginArray.prototype);
plugins.forEach((p, i) => {
Object.setPrototypeOf(p, Plugin.prototype);
pluginArray[i] = p;
pluginArray[p.name] = p;
});
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
pluginArray.item = (i) => plugins[i] || null;
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) || null;
pluginArray.refresh = () => {};
pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
const mimeTypes = [chromePdfMime, pdfMime, naclMime, pnaclMime];
const mimeTypeArray = Object.create(MimeTypeArray.prototype);
mimeTypes.forEach((m, i) => {
mimeTypeArray[i] = m;
mimeTypeArray[m.type] = m;
});
Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length });
mimeTypeArray.item = (i) => mimeTypes[i] || null;
mimeTypeArray.namedItem = (name) => mimeTypes.find(m => m.type === name) || null;
mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; };
Object.defineProperty(navigator, 'plugins', {
get: () => pluginArray,
configurable: true,
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => mimeTypeArray,
configurable: true,
});
})();`;
}
@@ -343,6 +400,38 @@ function patchWindowDimensions(): string {
})();`;
}
/**
* Make Screen avail* values look like a desktop with taskbar/menu bar reserved space.
*/
function patchScreenAvailability(): string {
return `(function(){
const patchNumber = (target, key, value) => {
try {
Object.defineProperty(target, key, {
get: () => value,
configurable: true,
});
} catch {}
};
const availWidth = Number(screen.availWidth);
const availHeight = Number(screen.availHeight);
const width = Number(screen.width);
const height = Number(screen.height);
if (Number.isFinite(width) && Number.isFinite(availWidth) && availWidth >= width) {
patchNumber(screen, 'availWidth', Math.max(width - 8, 0));
}
if (Number.isFinite(height) && Number.isFinite(availHeight) && availHeight >= height) {
patchNumber(screen, 'availHeight', Math.max(height - 40, 0));
}
if (Number.isFinite(screen.availLeft) && screen.availLeft === 0) {
patchNumber(screen, 'availLeft', 0);
}
if (Number.isFinite(screen.availTop) && screen.availTop === 0) {
patchNumber(screen, 'availTop', 24);
}
})();`;
}
/**
* navigator.hardwareConcurrency: headless often reports 2 (CI);
* real desktops typically have >= 4 cores.
@@ -358,6 +447,80 @@ function patchNavigatorHardwareConcurrency(): string {
})();`;
}
/**
* Add missing connection.downlinkMax in Chromium headless environments.
*/
function patchNavigatorConnection(): string {
return `(function(){
if (!navigator.connection) return;
const conn = navigator.connection;
if (typeof conn.downlinkMax === 'number') return;
try {
Object.defineProperty(conn, 'downlinkMax', {
get: () => 10,
configurable: true,
});
} catch {}
})();`;
}
/**
* Add share/canShare APIs expected on modern Chromium desktop.
*/
function patchNavigatorShare(): string {
return `(function(){
if (typeof navigator.share !== 'function') {
try {
Object.defineProperty(navigator, 'share', {
value: async () => undefined,
configurable: true,
});
} catch {}
}
if (typeof navigator.canShare !== 'function') {
try {
Object.defineProperty(navigator, 'canShare', {
value: () => true,
configurable: true,
});
} catch {}
}
})();`;
}
/**
* Add Contacts Manager stub to avoid "missing contacts manager" signals.
*/
function patchNavigatorContacts(): string {
return `(function(){
if (navigator.contacts) return;
try {
Object.defineProperty(navigator, 'contacts', {
value: {
select: async () => [],
getProperties: () => ['name', 'email', 'tel', 'address', 'icon'],
},
configurable: true,
});
} catch {}
})();`;
}
/**
* Chromium exposes navigator.pdfViewerEnabled=true in normal browsing mode.
*/
function patchPdfViewerEnabled(): string {
return `(function(){
if (navigator.pdfViewerEnabled === true) return;
try {
Object.defineProperty(navigator, 'pdfViewerEnabled', {
get: () => true,
configurable: true,
});
} catch {}
})();`;
}
/**
* navigator.mediaDevices.enumerateDevices should return at least some devices
* instead of an empty array (headless default).