fix(cli): 增加 abs start 并修复 managed 9333 启动链路

This commit is contained in:
leeguooooo
2026-03-10 12:45:50 +09:00
parent 8eee9310a4
commit fcd891e8a9
13 changed files with 255 additions and 68 deletions
+2
View File
@@ -47,6 +47,7 @@ abs install
### Minimal Usage
```bash
agent-browser start
agent-browser open https://example.com
agent-browser snapshot -i
agent-browser click @e2
@@ -176,6 +177,7 @@ flowchart TD
- `--profile` / `AGENT_BROWSER_PROFILE`
- `--channel` / `AGENT_BROWSER_CHANNEL`
- Default CLI policy uses a dedicated automation browser on CDP `localhost:9333`. If `:9333` is unavailable, agent-browser auto-starts Chrome with the persistent profile `~/.agent-browser/chrome-bot-profile`.
- Use `agent-browser start` (or `abs start`) when you want to pre-start that managed `:9333` browser before unattended work begins.
## Principle 2: Multi-Layer Fingerprint Hardening
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.17.0-fork.1"
version = "0.17.0-fork.2"
dependencies = [
"aes-gcm",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.17.0-fork.1"
version = "0.17.0-fork.2"
edition = "2021"
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0"
+24
View File
@@ -215,6 +215,14 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
}
Ok(nav_cmd)
}
// Prepare the managed automation browser on localhost:9333 without navigating.
// This reuses the existing launch action so both Node and native daemons stay aligned.
"start" => Ok(json!({
"id": id,
"action": "launch",
"cdpPort": 9333,
"headless": !flags.headed
})),
"back" => Ok(json!({ "id": id, "action": "back" })),
"forward" => Ok(json!({ "id": id, "action": "forward" })),
"reload" => Ok(json!({ "id": id, "action": "reload" })),
@@ -2363,6 +2371,22 @@ mod tests {
assert_eq!(cmd["url"], "https://example.com");
}
#[test]
fn test_start_command_uses_managed_cdp() {
let cmd = parse_command(&args("start"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "launch");
assert_eq!(cmd["cdpPort"], 9333);
assert_eq!(cmd["headless"], true);
}
#[test]
fn test_start_command_respects_headed_flag() {
let mut flags = default_flags();
flags.headed = true;
let cmd = parse_command(&args("start"), &flags).unwrap();
assert_eq!(cmd["headless"], false);
}
#[test]
fn test_navigate_with_headers() {
let mut flags = default_flags();
+53 -48
View File
@@ -11,11 +11,13 @@ mod validation;
use serde_json::json;
use std::env;
use std::net::{SocketAddr, TcpStream};
use std::process::exit;
use std::time::Duration;
use commands::{gen_id, parse_command, ParseError};
use connection::{ensure_daemon, list_live_sessions, send_command};
use flags::{clean_args, parse_flags};
use connection::{ensure_daemon, list_live_sessions, send_command, Response};
use flags::{clean_args, parse_flags, Flags};
use install::run_install;
use output::{print_command_help, print_help, print_response, print_version};
@@ -49,6 +51,29 @@ fn parse_proxy(proxy_str: &str) -> serde_json::Value {
})
}
fn should_try_default_cdp(flags: &Flags, command_name: Option<&str>) -> bool {
!matches!(command_name, Some("close"))
&& flags.cdp.is_none()
&& !flags.auto_connect
&& flags.provider.is_none()
&& flags.executable_path.is_none()
&& flags.state.is_none()
&& flags.proxy.is_none()
&& flags.args.is_none()
&& flags.user_agent.is_none()
&& !flags.ignore_https_errors
&& !flags.allow_file_access
&& flags.extensions.is_empty()
}
fn managed_cdp_port_ready() -> bool {
let addr: SocketAddr = match "127.0.0.1:9333".parse() {
Ok(addr) => addr,
Err(_) => return false,
};
TcpStream::connect_timeout(&addr, Duration::from_millis(300)).is_ok()
}
fn run_session(args: &[String], session: &str, json_mode: bool) {
let subcommand = args.get(1).map(|s| s.as_str());
@@ -113,17 +138,7 @@ fn main() {
flags.native = true;
}
let can_try_default_cdp = flags.cdp.is_none()
&& !flags.auto_connect
&& flags.provider.is_none()
&& flags.executable_path.is_none()
&& flags.state.is_none()
&& flags.proxy.is_none()
&& flags.args.is_none()
&& flags.user_agent.is_none()
&& !flags.ignore_https_errors
&& !flags.allow_file_access
&& flags.extensions.is_empty();
let can_try_default_cdp = should_try_default_cdp(&flags, command_name);
let can_force_native_for_cdp = !matches!(command_name, Some("close"));
if !flags.native
&& can_force_native_for_cdp
@@ -622,41 +637,6 @@ fn main() {
}
}
// Project policy: when no explicit connection mode is provided,
// commands should use the dedicated automation browser on localhost:9333.
// If 9333 is unavailable, the native daemon auto-starts a managed Chrome
// instance with a non-default profile and retries the CDP connection.
if can_try_default_cdp {
let mut launch_cmd = json!({
"id": gen_id(),
"action": "launch",
"cdpPort": 9333
});
if let Some(ref cs) = flags.color_scheme {
launch_cmd["colorScheme"] = json!(cs);
}
if let Some(ref tg) = flags.tab_group {
launch_cmd["tabGroup"] = json!(tg);
}
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
}
if let Ok(resp) = send_command(launch_cmd, &flags.session) {
attached_to_existing_browser = resp.success;
}
}
if can_try_default_cdp && !attached_to_existing_browser {
let msg = "Project policy requires using the dedicated automation browser on localhost:9333. Could not connect to or auto-start the managed Chrome profile. Start Chrome with --remote-debugging-port=9333 and a non-default --user-data-dir, or pass --cdp / --auto-connect explicitly.";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
// Launch headed browser or configure browser options (without CDP or provider)
if (flags.headed
|| flags.executable_path.is_some()
@@ -673,6 +653,7 @@ fn main() {
&& flags.cdp.is_none()
&& flags.provider.is_none()
&& !attached_to_existing_browser
&& !can_try_default_cdp
{
let mut launch_cmd = json!({
"id": gen_id(),
@@ -782,6 +763,18 @@ fn main() {
}
}
Err(e) => {
let is_start = command_name == Some("start")
&& cmd.get("action").and_then(|v| v.as_str()) == Some("launch")
&& cmd.get("cdpPort").and_then(|v| v.as_u64()) == Some(9333);
if is_start && managed_cdp_port_ready() {
let resp = Response {
success: true,
data: Some(json!({ "launched": true })),
error: None,
};
print_response(&resp, flags.json, Some("launch"));
return;
}
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
@@ -849,4 +842,16 @@ mod tests {
assert_eq!(result["username"], "user");
assert_eq!(result["password"], "p@ss:w0rd");
}
#[test]
fn test_should_try_default_cdp_for_open() {
let flags = parse_flags(&[]);
assert!(should_try_default_cdp(&flags, Some("open")));
}
#[test]
fn test_should_not_try_default_cdp_for_close() {
let flags = parse_flags(&[]);
assert!(!should_try_default_cdp(&flags, Some("close")));
}
}
+44 -1
View File
@@ -729,8 +729,12 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
let options = launch_options_from_env();
let engine = env::var("AGENT_BROWSER_ENGINE").ok();
let debug_enabled = env::var("AGENT_BROWSER_DEBUG").as_deref() == Ok("1");
if let Ok(cdp) = env::var("AGENT_BROWSER_CDP") {
if debug_enabled {
eprintln!("[DEBUG] auto_launch: connecting via AGENT_BROWSER_CDP={}", cdp);
}
let mgr = BrowserManager::connect_cdp(&cdp).await?;
state.browser = Some(mgr);
state.subscribe_to_browser_events();
@@ -739,6 +743,9 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
}
if env::var("AGENT_BROWSER_AUTO_CONNECT").is_ok() {
if debug_enabled {
eprintln!("[DEBUG] auto_launch: connecting via AGENT_BROWSER_AUTO_CONNECT");
}
let mgr = BrowserManager::connect_auto().await?;
state.browser = Some(mgr);
state.subscribe_to_browser_events();
@@ -746,13 +753,36 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
return Ok(());
}
let mgr = BrowserManager::launch(options, engine.as_deref()).await?;
let mgr = if should_auto_launch_managed_cdp(&options) {
if debug_enabled {
eprintln!("[DEBUG] auto_launch: launching managed Chrome on localhost:9333");
}
BrowserManager::launch_managed_cdp(options.executable_path.clone(), !options.headless)
.await?
} else {
if debug_enabled {
eprintln!("[DEBUG] auto_launch: launching local browser from env options");
}
BrowserManager::launch(options, engine.as_deref()).await?
};
state.browser = Some(mgr);
state.subscribe_to_browser_events();
try_auto_restore_state(state).await;
Ok(())
}
fn should_auto_launch_managed_cdp(options: &LaunchOptions) -> bool {
options.proxy.is_none()
&& options.proxy_bypass.is_none()
&& options.profile.is_none()
&& !options.allow_file_access
&& options.args.is_empty()
&& options.extensions.as_ref().is_none_or(|ext| ext.is_empty())
&& options.storage_state.is_none()
&& options.user_agent.is_none()
&& !options.ignore_https_errors
}
fn launch_options_from_env() -> LaunchOptions {
let headed = env::var("AGENT_BROWSER_HEADED")
.map(|v| v == "1" || v == "true")
@@ -5225,6 +5255,19 @@ mod tests {
);
}
#[test]
fn test_should_auto_launch_managed_cdp_for_bare_defaults() {
let opts = launch_options_from_env();
assert!(should_auto_launch_managed_cdp(&opts));
}
#[test]
fn test_should_not_auto_launch_managed_cdp_when_proxy_is_set() {
let mut opts = launch_options_from_env();
opts.proxy = Some("http://127.0.0.1:8080".to_string());
assert!(!should_auto_launch_managed_cdp(&opts));
}
#[tokio::test]
async fn test_execute_unknown_command() {
let mut state = DaemonState::new();
+42 -7
View File
@@ -111,6 +111,11 @@ pub fn to_ai_friendly_error(error: &str) -> String {
error.to_string()
}
fn is_startup_internal_page(url: &str) -> bool {
let lower = url.to_lowercase();
lower.starts_with("chrome://profile-picker")
}
#[derive(Debug, Clone)]
pub struct PageInfo {
pub target_id: String,
@@ -416,8 +421,22 @@ impl BrowserManager {
if attached_pages.is_empty() {
self.create_and_attach_blank_page().await?;
} else {
let preferred_index = attached_pages
.iter()
.position(|page| !is_startup_internal_page(&page.url));
self.pages = attached_pages;
self.active_page_index = 0;
if let Some(index) = preferred_index {
self.active_page_index = index;
} else {
if std::env::var("AGENT_BROWSER_DEBUG").as_deref() == Ok("1") {
eprintln!(
"[DEBUG] All discovered pages were Chrome startup pages; creating a fresh about:blank target"
);
}
self.create_and_attach_blank_page().await?;
}
}
}
@@ -450,6 +469,7 @@ impl BrowserManager {
pub async fn navigate(&mut self, url: &str, wait_until: WaitUntil) -> Result<Value, String> {
let session_id = self.active_session_id()?.to_string();
let rx = self.client.subscribe();
let nav_result: PageNavigateResult = self
.client
@@ -467,7 +487,7 @@ impl BrowserManager {
return Err(format!("Navigation failed: {}", error_text));
}
self.wait_for_lifecycle(wait_until, &session_id).await?;
self.wait_for_lifecycle(wait_until, &session_id, rx).await?;
let page_url = self.get_url().await.unwrap_or_else(|_| url.to_string());
let title = self.get_title().await.unwrap_or_default();
@@ -484,14 +504,14 @@ impl BrowserManager {
&self,
wait_until: WaitUntil,
session_id: &str,
mut rx: tokio::sync::broadcast::Receiver<CdpEvent>,
) -> Result<(), String> {
let event_name = match wait_until {
WaitUntil::Load => "Page.loadEventFired",
WaitUntil::DomContentLoaded => "Page.domContentEventFired",
WaitUntil::NetworkIdle => return self.wait_for_network_idle(session_id).await,
WaitUntil::NetworkIdle => return self.wait_for_network_idle(session_id, rx).await,
};
let mut rx = self.client.subscribe();
let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms);
tokio::time::timeout(timeout, async {
@@ -506,8 +526,11 @@ impl BrowserManager {
.map_err(|_| format!("Timeout waiting for {}", event_name))?
}
async fn wait_for_network_idle(&self, session_id: &str) -> Result<(), String> {
let mut rx = self.client.subscribe();
async fn wait_for_network_idle(
&self,
session_id: &str,
mut rx: tokio::sync::broadcast::Receiver<CdpEvent>,
) -> Result<(), String> {
let pending = Arc::new(Mutex::new(HashSet::<String>::new()));
let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms);
@@ -626,7 +649,8 @@ impl BrowserManager {
wait_until: WaitUntil,
session_id: &str,
) -> Result<(), String> {
self.wait_for_lifecycle(wait_until, session_id).await
self.wait_for_lifecycle(wait_until, session_id, self.client.subscribe())
.await
}
pub async fn close(&mut self) -> Result<(), String> {
@@ -1270,4 +1294,15 @@ mod tests {
let msg = "Some custom error message";
assert_eq!(to_ai_friendly_error(msg), msg);
}
#[test]
fn test_is_startup_internal_page_detects_profile_picker() {
assert!(is_startup_internal_page("chrome://profile-picker/"));
}
#[test]
fn test_is_startup_internal_page_ignores_normal_pages() {
assert!(!is_startup_internal_page("https://example.com/"));
assert!(!is_startup_internal_page("about:blank"));
}
}
+25
View File
@@ -435,6 +435,12 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
}
return;
}
// Launch/start
if data.get("launched").is_some() {
let label = "Browser ready";
println!("{} {}", color::success_indicator(), label);
return;
}
// Closed (browser or tab)
if data.get("closed").is_some() {
let label = match action {
@@ -884,6 +890,24 @@ Examples:
agent-browser open localhost:3000
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
# ^ Headers only sent to api.example.com, not other domains
"##
}
"start" => {
r##"
agent-browser start - Start the managed automation browser on localhost:9333
Usage: agent-browser start
Starts or reuses the dedicated automation Chrome profile on localhost:9333
without navigating to a page. Use this to pre-warm the managed browser for
unattended runs.
Global Options:
--json Output as JSON
Examples:
agent-browser start
abs start
"##
}
"back" => {
@@ -2339,6 +2363,7 @@ Aliases: agent-browser, agent-browser-stealth, abs
Core Commands:
open <url> Navigate to URL
start Start managed browser on localhost:9333
click <sel> Click element (or @ref)
dblclick <sel> Double-click element
type <sel> <text> [--delay <ms>] Type into element
+7
View File
@@ -8,6 +8,13 @@ Connect to an existing browser via Chrome DevTools Protocol:
Default behavior in this fork: when `--cdp` is omitted, agent-browser targets the managed automation browser on `localhost:9333`. If `:9333` is unavailable, it auto-starts Chrome with the persistent profile `~/.agent-browser/chrome-bot-profile` and retries the CDP connection.
If you want to pre-start that managed browser explicitly, run:
```bash
agent-browser start
abs start
```
Project policy:
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
+1
View File
@@ -9,6 +9,7 @@ Executable aliases: `agent-browser`, `agent-browser-stealth`, `abs`.
## Core
```bash
agent-browser start # Start/reuse managed browser on localhost:9333
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block when verification/captcha interstitial is detected
agent-browser click <sel> # Click element (--new-tab to open in new tab)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.17.0-fork.1",
"version": "0.17.0-fork.2",
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module",
"main": "dist/daemon.js",
@@ -23,7 +23,7 @@
"build": "tsc",
"build:native": "npm run version:sync && npm run native:clean && cargo build --release --manifest-path cli/Cargo.toml && node scripts/copy-native.js",
"build:linux": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-linux",
"build:macos": "npm run version:sync && npm run native:clean && (cargo build --release --manifest-path cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path cli/Cargo.toml --target x86_64-apple-darwin & wait) && cp cli/target/aarch64-apple-darwin/release/agent-browser bin/agent-browser-darwin-arm64 && cp cli/target/x86_64-apple-darwin/release/agent-browser bin/agent-browser-darwin-x64",
"build:macos": "npm run version:sync && npm run native:clean && (cargo build --release --manifest-path cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path cli/Cargo.toml --target x86_64-apple-darwin & wait) && node scripts/copy-native.js cli/target/aarch64-apple-darwin/release/agent-browser bin/agent-browser-darwin-arm64 && node scripts/copy-native.js cli/target/x86_64-apple-darwin/release/agent-browser bin/agent-browser-darwin-x64",
"build:windows": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-windows",
"build:all-platforms": "npm run version:sync && (npm run build:linux & npm run build:windows & wait) && npm run build:macos",
"build:docker": "docker build -t agent-browser-builder -f docker/Dockerfile.build .",
+50 -8
View File
@@ -1,26 +1,67 @@
#!/usr/bin/env node
/**
* Copies the compiled Rust binary to bin/ with platform-specific naming
* Copies the compiled Rust binary to bin/ with platform-specific naming.
* On macOS, re-apply an ad-hoc signature after copying so the binary remains
* executable from the packaged bin/ path.
*/
import { copyFileSync, existsSync, mkdirSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { platform, arch } from 'os';
import { spawnSync } from 'child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '..');
const sourceExt = platform() === 'win32' ? '.exe' : '';
const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);
const binDir = join(projectRoot, 'bin');
// Determine platform suffix
const platformKey = `${platform()}-${arch()}`;
const ext = platform() === 'win32' ? '.exe' : '';
const targetName = `agent-browser-${platformKey}${ext}`;
const targetPath = join(binDir, targetName);
function defaultPaths() {
const sourceExt = platform() === 'win32' ? '.exe' : '';
const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);
const platformKey = `${platform()}-${arch()}`;
const ext = platform() === 'win32' ? '.exe' : '';
const targetName = `agent-browser-${platformKey}${ext}`;
const targetPath = join(binDir, targetName);
return { sourcePath, targetPath };
}
function resolvePaths() {
const [sourceArg, targetArg] = process.argv.slice(2);
if (!sourceArg && !targetArg) {
return defaultPaths();
}
if (!sourceArg || !targetArg) {
console.error('Usage: node scripts/copy-native.js [source-binary target-binary]');
process.exit(1);
}
return {
sourcePath: join(projectRoot, sourceArg),
targetPath: join(projectRoot, targetArg),
};
}
function adHocSignIfNeeded(targetPath) {
if (platform() !== 'darwin') {
return;
}
const result = spawnSync('codesign', ['--force', '--sign', '-', targetPath], {
stdio: 'pipe',
encoding: 'utf8',
});
if (result.status !== 0) {
const message = result.stderr?.trim() || result.stdout?.trim() || 'unknown codesign error';
console.error(`Error: Failed to codesign ${targetPath}: ${message}`);
process.exit(result.status ?? 1);
}
}
const { sourcePath, targetPath } = resolvePaths();
if (!existsSync(sourcePath)) {
console.error(`Error: Native binary not found at ${sourcePath}`);
@@ -33,4 +74,5 @@ if (!existsSync(binDir)) {
}
copyFileSync(sourcePath, targetPath);
adHocSignIfNeeded(targetPath);
console.log(`✓ Copied native binary to ${targetPath}`);
+3
View File
@@ -8,6 +8,8 @@ allowed-tools: Bash(npx agent-browser-stealth:*), Bash(npx agent-browser:*), Bas
Install package: `pnpm add -g agent-browser-stealth` (CLI commands: `agent-browser`, `agent-browser-stealth`, and short alias `abs`). If global install is unavailable in your environment, use `pnpm dlx agent-browser-stealth <command>` for one-off runs.
Use `agent-browser start` when you want to pre-warm the managed automation browser on `localhost:9333` before the actual navigation or task commands run.
## Core Workflow
Every browser automation follows this pattern:
@@ -50,6 +52,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle
```bash
# Navigation
agent-browser start # Start/reuse managed browser on localhost:9333
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block if verification/captcha interstitial is detected
agent-browser doctor # Diagnose CDP + sourceURL + tab-group plugin health