feat(stealth): 默认开启并收敛 chrome 指纹特征

This commit is contained in:
leeguooooo
2026-02-24 14:54:30 +09:00
parent c5b2292caa
commit 0443e4ed7a
12 changed files with 29 additions and 76 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
version = 4 version = 4
[[package]] [[package]]
name = "agent-browser" name = "agent-browser-stealth"
version = "0.14.0" version = "0.14.0"
dependencies = [ dependencies = [
"base64", "base64",
+2 -2
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "agent-browser" name = "agent-browser-stealth"
version = "0.14.0" version = "0.14.0"
edition = "2021" edition = "2021"
description = "Fast browser automation CLI for AI agents" description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0" license = "Apache-2.0"
[dependencies] [dependencies]
-2
View File
@@ -1923,10 +1923,8 @@ mod tests {
cli_proxy_bypass: false, cli_proxy_bypass: false,
cli_allow_file_access: false, cli_allow_file_access: false,
cli_annotate: false, cli_annotate: false,
cli_stealth: false,
annotate: false, annotate: false,
color_scheme: None, color_scheme: None,
stealth: false,
} }
} }
+2 -3
View File
@@ -220,7 +220,6 @@ pub fn ensure_daemon(
provider: Option<&str>, provider: Option<&str>,
device: Option<&str>, device: Option<&str>,
session_name: Option<&str>, session_name: Option<&str>,
stealth: bool,
debug: bool, debug: bool,
) -> Result<DaemonResult, String> { ) -> Result<DaemonResult, String> {
// Check if daemon is running AND responsive // Check if daemon is running AND responsive
@@ -366,7 +365,7 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_SESSION_NAME", sn); cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
} }
cmd.env("AGENT_BROWSER_STEALTH", if stealth { "1" } else { "0" }); cmd.env("AGENT_BROWSER_STEALTH", "1");
if debug { if debug {
cmd.env("AGENT_BROWSER_DEBUG", "1"); cmd.env("AGENT_BROWSER_DEBUG", "1");
} }
@@ -454,7 +453,7 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_SESSION_NAME", sn); cmd.env("AGENT_BROWSER_SESSION_NAME", sn);
} }
cmd.env("AGENT_BROWSER_STEALTH", if stealth { "1" } else { "0" }); cmd.env("AGENT_BROWSER_STEALTH", "1");
if debug { if debug {
cmd.env("AGENT_BROWSER_DEBUG", "1"); cmd.env("AGENT_BROWSER_DEBUG", "1");
} }
-18
View File
@@ -34,7 +34,6 @@ pub struct Config {
pub headers: Option<String>, pub headers: Option<String>,
pub annotate: Option<bool>, pub annotate: Option<bool>,
pub color_scheme: Option<String>, pub color_scheme: Option<String>,
pub stealth: Option<bool>,
} }
impl Config { impl Config {
@@ -69,7 +68,6 @@ impl Config {
headers: other.headers.or(self.headers), headers: other.headers.or(self.headers),
annotate: other.annotate.or(self.annotate), annotate: other.annotate.or(self.annotate),
color_scheme: other.color_scheme.or(self.color_scheme), color_scheme: other.color_scheme.or(self.color_scheme),
stealth: other.stealth.or(self.stealth),
} }
} }
} }
@@ -204,7 +202,6 @@ pub struct Flags {
pub session_name: Option<String>, pub session_name: Option<String>,
pub annotate: bool, pub annotate: bool,
pub color_scheme: Option<String>, pub color_scheme: Option<String>,
pub stealth: bool,
// Track which launch-time options were explicitly passed via CLI // Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables) // (as opposed to being set only via environment variables)
@@ -218,7 +215,6 @@ pub struct Flags {
pub cli_proxy_bypass: bool, pub cli_proxy_bypass: bool,
pub cli_allow_file_access: bool, pub cli_allow_file_access: bool,
pub cli_annotate: bool, pub cli_annotate: bool,
pub cli_stealth: bool,
} }
pub fn parse_flags(args: &[String]) -> Flags { pub fn parse_flags(args: &[String]) -> Flags {
@@ -283,10 +279,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME") color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME")
.ok() .ok()
.or(config.color_scheme), .or(config.color_scheme),
stealth: match env::var("AGENT_BROWSER_STEALTH") {
Ok(val) => !matches!(val.to_lowercase().as_str(), "0" | "false" | "no" | ""),
Err(_) => config.stealth.unwrap_or(true),
},
cli_executable_path: false, cli_executable_path: false,
cli_extensions: false, cli_extensions: false,
cli_profile: false, cli_profile: false,
@@ -297,7 +289,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
cli_proxy_bypass: false, cli_proxy_bypass: false,
cli_allow_file_access: false, cli_allow_file_access: false,
cli_annotate: false, cli_annotate: false,
cli_stealth: false,
}; };
let mut i = 0; let mut i = 0;
@@ -453,14 +444,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1; i += 1;
} }
} }
"--stealth" => {
let (val, consumed) = parse_bool_arg(args, i);
flags.stealth = val;
flags.cli_stealth = true;
if consumed {
i += 1;
}
}
"--color-scheme" => { "--color-scheme" => {
if let Some(s) = args.get(i + 1) { if let Some(s) = args.get(i + 1) {
flags.color_scheme = Some(s.clone()); flags.color_scheme = Some(s.clone());
@@ -492,7 +475,6 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--allow-file-access", "--allow-file-access",
"--auto-connect", "--auto-connect",
"--annotate", "--annotate",
"--stealth",
]; ];
// Global flags that always take a value (need to skip the next arg too) // Global flags that always take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
+3 -22
View File
@@ -52,14 +52,6 @@ fn parse_proxy(proxy_str: &str) -> serde_json::Value {
}) })
} }
fn print_stealth_debug(resp: &connection::Response) {
if let Some(data) = &resp.data {
if let Some(stealth) = data.get("stealth") {
eprintln!("[DEBUG] stealth: {}", stealth);
}
}
}
fn run_session(args: &[String], session: &str, json_mode: bool) { fn run_session(args: &[String], session: &str, json_mode: bool) {
let subcommand = args.get(1).map(|s| s.as_str()); let subcommand = args.get(1).map(|s| s.as_str());
@@ -234,7 +226,6 @@ fn main() {
flags.provider.as_deref(), flags.provider.as_deref(),
flags.device.as_deref(), flags.device.as_deref(),
flags.session_name.as_deref(), flags.session_name.as_deref(),
flags.stealth,
flags.debug, flags.debug,
) { ) {
Ok(result) => result, Ok(result) => result,
@@ -291,7 +282,6 @@ fn main() {
}, },
flags.ignore_https_errors.then_some("--ignore-https-errors"), flags.ignore_https_errors.then_some("--ignore-https-errors"),
flags.cli_allow_file_access.then_some("--allow-file-access"), flags.cli_allow_file_access.then_some("--allow-file-access"),
flags.cli_stealth.then_some("--stealth"),
] ]
.into_iter() .into_iter()
.flatten() .flatten()
@@ -472,9 +462,7 @@ fn main() {
} }
exit(1); exit(1);
} }
if flags.debug {
print_stealth_debug(&resp);
}
} }
Err(e) => { Err(e) => {
if flags.json { if flags.json {
@@ -512,9 +500,7 @@ fn main() {
} }
exit(1); exit(1);
} }
if flags.debug {
print_stealth_debug(&resp);
}
} }
Err(e) => { Err(e) => {
if flags.json { if flags.json {
@@ -537,7 +523,6 @@ fn main() {
|| flags.user_agent.is_some() || flags.user_agent.is_some()
|| flags.ignore_https_errors || flags.ignore_https_errors
|| flags.allow_file_access || flags.allow_file_access
|| flags.cli_stealth
|| flags.debug || flags.debug
|| flags.color_scheme.is_some()) || flags.color_scheme.is_some())
&& flags.cdp.is_none() && flags.cdp.is_none()
@@ -601,8 +586,6 @@ fn main() {
launch_cmd["allowFileAccess"] = json!(true); launch_cmd["allowFileAccess"] = json!(true);
} }
launch_cmd["stealth"] = json!(flags.stealth);
if let Some(ref cs) = flags.color_scheme { if let Some(ref cs) = flags.color_scheme {
launch_cmd["colorScheme"] = json!(cs); launch_cmd["colorScheme"] = json!(cs);
} }
@@ -621,9 +604,7 @@ fn main() {
} }
exit(1); exit(1);
} }
if flags.debug {
print_stealth_debug(&resp);
}
} }
Err(e) => { Err(e) => {
if flags.json { if flags.json {
+1 -2
View File
@@ -2114,7 +2114,6 @@ Options:
e.g., --proxy-bypass "localhost,*.internal.com" e.g., --proxy-bypass "localhost,*.internal.com"
--ignore-https-errors Ignore HTTPS certificate errors --ignore-https-errors Ignore HTTPS certificate errors
--allow-file-access Allow file:// URLs to access local files (Chromium only) --allow-file-access Allow file:// URLs to access local files (Chromium only)
--stealth Stealth mode (default: on): local=launch args+init scripts, CDP/provider=init scripts
-p, --provider <name> Browser provider: ios, browserbase, kernel, browseruse -p, --provider <name> Browser provider: ios, browserbase, kernel, browseruse
--device <name> iOS device name (e.g., "iPhone 15 Pro") --device <name> iOS device name (e.g., "iPhone 15 Pro")
--json JSON output --json JSON output
@@ -2126,7 +2125,7 @@ Options:
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME) --color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--session-name <name> Auto-save/restore session state (cookies, localStorage) --session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env) --config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output (includes stealth connection type + capabilities) --debug Debug output
--version, -V Show version --version, -V Show version
Configuration: Configuration:
+5 -18
View File
@@ -152,7 +152,7 @@ export class BrowserManager {
private lastSnapshot: string = ''; private lastSnapshot: string = '';
private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = new Map(); private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = new Map();
private colorScheme: 'light' | 'dark' | 'no-preference' | null = null; private colorScheme: 'light' | 'dark' | 'no-preference' | null = null;
private stealthEnabled: boolean = false; private stealthEnabled: boolean = true;
private stealthConnectionKind: StealthConnectionKind = 'local'; private stealthConnectionKind: StealthConnectionKind = 'local';
private contextLocale: string | undefined = undefined; private contextLocale: string | undefined = undefined;
private contextTimezoneId: string | undefined = undefined; private contextTimezoneId: string | undefined = undefined;
@@ -172,17 +172,6 @@ export class BrowserManager {
* Local Chromium gets args + init scripts; CDP/providers get init scripts only. * Local Chromium gets args + init scripts; CDP/providers get init scripts only.
*/ */
private getStealthPolicy(browserType: BrowserType = 'chromium'): StealthPolicy { private getStealthPolicy(browserType: BrowserType = 'chromium'): StealthPolicy {
if (!this.stealthEnabled) {
return {
enabled: false,
connectionKind: this.stealthConnectionKind,
applyChromiumArgs: false,
applyInitScripts: false,
providerManaged: false,
capabilities: [],
};
}
const applyChromiumArgs = this.stealthConnectionKind === 'local' && browserType === 'chromium'; const applyChromiumArgs = this.stealthConnectionKind === 'local' && browserType === 'chromium';
const applyInitScripts = true; const applyInitScripts = true;
const providerManaged = this.stealthConnectionKind === 'provider-kernel'; const providerManaged = this.stealthConnectionKind === 'provider-kernel';
@@ -1414,11 +1403,9 @@ export class BrowserManager {
if (options.colorScheme) { if (options.colorScheme) {
this.colorScheme = options.colorScheme; this.colorScheme = options.colorScheme;
} }
this.stealthEnabled = options.stealth ?? false; this.stealthEnabled = true;
this.contextLocale = this.stealthEnabled this.contextLocale = this.resolveStealthLocale(options.headers);
? this.resolveStealthLocale(options.headers) this.contextTimezoneId = this.resolveStealthTimezoneId();
: undefined;
this.contextTimezoneId = this.stealthEnabled ? this.resolveStealthTimezoneId() : undefined;
this.contextHeaders = undefined; this.contextHeaders = undefined;
this.contextUserAgent = options.userAgent; this.contextUserAgent = options.userAgent;
// -p flag takes precedence over AGENT_BROWSER_PROVIDER. // -p flag takes precedence over AGENT_BROWSER_PROVIDER.
@@ -2749,7 +2736,7 @@ export class BrowserManager {
this.isPersistentContext = false; this.isPersistentContext = false;
this.activePageIndex = 0; this.activePageIndex = 0;
this.colorScheme = null; this.colorScheme = null;
this.stealthEnabled = false; this.stealthEnabled = true;
this.stealthConnectionKind = 'local'; this.stealthConnectionKind = 'local';
this.contextLocale = undefined; this.contextLocale = undefined;
this.contextTimezoneId = undefined; this.contextTimezoneId = undefined;
+2 -2
View File
@@ -450,7 +450,7 @@ export async function startDaemon(options?: {
const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1'; const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1';
const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1'; const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1';
const stealth = process.env.AGENT_BROWSER_STEALTH !== '0'; // Stealth is always enabled in agent-browser-stealth
const colorSchemeEnv = process.env.AGENT_BROWSER_COLOR_SCHEME; const colorSchemeEnv = process.env.AGENT_BROWSER_COLOR_SCHEME;
const colorScheme = const colorScheme =
colorSchemeEnv === 'dark' || colorSchemeEnv === 'dark' ||
@@ -471,7 +471,7 @@ export async function startDaemon(options?: {
proxy, proxy,
ignoreHTTPSErrors: ignoreHTTPSErrors, ignoreHTTPSErrors: ignoreHTTPSErrors,
allowFileAccess: allowFileAccess, allowFileAccess: allowFileAccess,
stealth,
colorScheme, colorScheme,
autoStateFilePath: getSessionAutoStatePath(), autoStateFilePath: getSessionAutoStatePath(),
}); });
-2
View File
@@ -49,8 +49,6 @@ const launchSchema = baseCommandSchema.extend({
provider: z.string().optional(), provider: z.string().optional(),
ignoreHTTPSErrors: z.boolean().optional(), ignoreHTTPSErrors: z.boolean().optional(),
allowFileAccess: z.boolean().optional(), allowFileAccess: z.boolean().optional(),
// Stealth toggle is part of launch semantics for local/CDP/provider modes.
stealth: z.boolean().optional(),
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(), colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(),
profile: z.string().optional(), profile: z.string().optional(),
storageState: z.string().optional(), storageState: z.string().optional(),
+13 -3
View File
@@ -244,8 +244,18 @@ function patchNavigatorWebdriver(): string {
*/ */
function patchChromeRuntime(): string { function patchChromeRuntime(): string {
return `(function(){ return `(function(){
if (!window.chrome) { window.chrome = {}; } const chromeObject = ('chrome' in window && window.chrome) ? window.chrome : {};
if (!window.chrome.runtime) { if (!('chrome' in window)) {
try {
Object.defineProperty(Window.prototype, 'chrome', {
get: () => chromeObject,
configurable: true,
});
} catch {
try { Object.defineProperty(window, 'chrome', { value: chromeObject, configurable: true }); } catch {}
}
}
if (!chromeObject.runtime) {
const makeEvent = () => ({ const makeEvent = () => ({
addListener: () => {}, addListener: () => {},
removeListener: () => {}, removeListener: () => {},
@@ -268,7 +278,7 @@ function patchChromeRuntime(): string {
onConnect: makeEvent(), onConnect: makeEvent(),
onMessage: makeEvent(), onMessage: makeEvent(),
}; };
Object.defineProperty(window.chrome, 'runtime', { Object.defineProperty(chromeObject, 'runtime', {
value: runtime, value: runtime,
configurable: true, configurable: true,
}); });
-1
View File
@@ -32,7 +32,6 @@ export interface LaunchCommand extends BaseCommand {
ignoreHTTPSErrors?: boolean; ignoreHTTPSErrors?: boolean;
allowFileAccess?: boolean; // Enable file:// URL access and cross-origin file requests allowFileAccess?: boolean; // Enable file:// URL access and cross-origin file requests
colorScheme?: 'light' | 'dark' | 'no-preference'; // Persistent color scheme override colorScheme?: 'light' | 'dark' | 'no-preference'; // Persistent color scheme override
stealth?: boolean; // Enable stealth mode to avoid automation detection
// Auto-load state file for session persistence // Auto-load state file for session persistence
autoStateFilePath?: string; autoStateFilePath?: string;
} }