feat(stealth): 优化指纹信号并同步文档与包配置
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
+64
-37
@@ -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,27 +183,34 @@ 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
|
||||
// Check if symlink exists (indicates global install)
|
||||
try {
|
||||
const stat = lstatSync(symlinkPath);
|
||||
if (!stat.isSymbolicLink()) {
|
||||
continue; // Not a symlink, don't touch it
|
||||
}
|
||||
} catch {
|
||||
continue; // Symlink doesn't exist, not a global install
|
||||
}
|
||||
|
||||
// Replace symlink to point directly to native binary
|
||||
try {
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(binaryPath, symlinkPath);
|
||||
optimized = true;
|
||||
} catch (err) {
|
||||
// Permission error or other issue - not critical, JS wrapper still works
|
||||
console.log(`⚠ Could not optimize symlink (${commandName}): ${err.message}`);
|
||||
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
||||
}
|
||||
} catch {
|
||||
return; // Symlink doesn't exist, not a global install
|
||||
}
|
||||
|
||||
// Replace symlink to point directly to native binary
|
||||
try {
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(binaryPath, symlinkPath);
|
||||
if (optimized) {
|
||||
console.log('✓ Optimized: symlink points to native binary (zero overhead)');
|
||||
} catch (err) {
|
||||
// Permission error or other issue - not critical, JS wrapper still works
|
||||
console.log(`⚠ Could not optimize symlink: ${err.message}`);
|
||||
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,25 +228,28 @@ async function fixWindowsShims() {
|
||||
return; // Not a global install or npm not available
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
// Only fix if shims exist (indicates global install)
|
||||
if (!existsSync(cmdShim)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Path to native binary relative to npm prefix
|
||||
const relativeBinaryPath = 'node_modules\\agent-browser\\bin\\agent-browser-win32-x64.exe';
|
||||
const packagePath = packageName.replace(/\//g, '\\');
|
||||
const relativeBinaryPath = `node_modules\\${packagePath}\\bin\\${binaryName}`;
|
||||
let optimized = false;
|
||||
|
||||
try {
|
||||
// Overwrite .cmd shim
|
||||
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
|
||||
writeFileSync(cmdShim, cmdContent);
|
||||
for (const commandName of binCommands) {
|
||||
// The shims are in the npm prefix directory (not prefix/bin on Windows)
|
||||
const cmdShim = join(npmBinDir, `${commandName}.cmd`);
|
||||
const ps1Shim = join(npmBinDir, `${commandName}.ps1`);
|
||||
|
||||
// Overwrite .ps1 shim
|
||||
const ps1Content = `#!/usr/bin/env pwsh
|
||||
// Only fix if shims exist (indicates global install)
|
||||
if (!existsSync(cmdShim)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Overwrite .cmd shim
|
||||
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
|
||||
writeFileSync(cmdShim, cmdContent);
|
||||
|
||||
// Overwrite .ps1 shim
|
||||
const ps1Content = `#!/usr/bin/env pwsh
|
||||
$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
$exe = ""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
@@ -235,13 +258,17 @@ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
& "$basedir/${relativeBinaryPath.replace(/\\/g, '/')}" $args
|
||||
exit $LASTEXITCODE
|
||||
`;
|
||||
writeFileSync(ps1Shim, ps1Content);
|
||||
writeFileSync(ps1Shim, ps1Content);
|
||||
optimized = true;
|
||||
} catch (err) {
|
||||
// Permission error or other issue - not critical, JS wrapper still works
|
||||
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)');
|
||||
} catch (err) {
|
||||
// Permission error or other issue - not critical, JS wrapper still works
|
||||
console.log(`⚠ Could not optimize shims: ${err.message}`);
|
||||
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user