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 ### Minimal Usage
```bash ```bash
agent-browser start
agent-browser open https://example.com agent-browser open https://example.com
agent-browser snapshot -i agent-browser snapshot -i
agent-browser click @e2 agent-browser click @e2
@@ -176,6 +177,7 @@ flowchart TD
- `--profile` / `AGENT_BROWSER_PROFILE` - `--profile` / `AGENT_BROWSER_PROFILE`
- `--channel` / `AGENT_BROWSER_CHANNEL` - `--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`. - 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 ## Principle 2: Multi-Layer Fingerprint Hardening
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]] [[package]]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.17.0-fork.1" version = "0.17.0-fork.2"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.17.0-fork.1" version = "0.17.0-fork.2"
edition = "2021" edition = "2021"
description = "Stealth browser automation CLI for AI agents with anti-bot evasions" description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0" 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) 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" })), "back" => Ok(json!({ "id": id, "action": "back" })),
"forward" => Ok(json!({ "id": id, "action": "forward" })), "forward" => Ok(json!({ "id": id, "action": "forward" })),
"reload" => Ok(json!({ "id": id, "action": "reload" })), "reload" => Ok(json!({ "id": id, "action": "reload" })),
@@ -2363,6 +2371,22 @@ mod tests {
assert_eq!(cmd["url"], "https://example.com"); 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] #[test]
fn test_navigate_with_headers() { fn test_navigate_with_headers() {
let mut flags = default_flags(); let mut flags = default_flags();
+53 -48
View File
@@ -11,11 +11,13 @@ mod validation;
use serde_json::json; use serde_json::json;
use std::env; use std::env;
use std::net::{SocketAddr, TcpStream};
use std::process::exit; use std::process::exit;
use std::time::Duration;
use commands::{gen_id, parse_command, ParseError}; use commands::{gen_id, parse_command, ParseError};
use connection::{ensure_daemon, list_live_sessions, send_command}; use connection::{ensure_daemon, list_live_sessions, send_command, Response};
use flags::{clean_args, parse_flags}; use flags::{clean_args, parse_flags, Flags};
use install::run_install; use install::run_install;
use output::{print_command_help, print_help, print_response, print_version}; 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) { 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());
@@ -113,17 +138,7 @@ fn main() {
flags.native = true; flags.native = true;
} }
let can_try_default_cdp = flags.cdp.is_none() let can_try_default_cdp = should_try_default_cdp(&flags, command_name);
&& !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_force_native_for_cdp = !matches!(command_name, Some("close")); let can_force_native_for_cdp = !matches!(command_name, Some("close"));
if !flags.native if !flags.native
&& can_force_native_for_cdp && 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) // Launch headed browser or configure browser options (without CDP or provider)
if (flags.headed if (flags.headed
|| flags.executable_path.is_some() || flags.executable_path.is_some()
@@ -673,6 +653,7 @@ fn main() {
&& flags.cdp.is_none() && flags.cdp.is_none()
&& flags.provider.is_none() && flags.provider.is_none()
&& !attached_to_existing_browser && !attached_to_existing_browser
&& !can_try_default_cdp
{ {
let mut launch_cmd = json!({ let mut launch_cmd = json!({
"id": gen_id(), "id": gen_id(),
@@ -782,6 +763,18 @@ fn main() {
} }
} }
Err(e) => { 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 { if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e); println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else { } else {
@@ -849,4 +842,16 @@ mod tests {
assert_eq!(result["username"], "user"); assert_eq!(result["username"], "user");
assert_eq!(result["password"], "p@ss:w0rd"); 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> { async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
let options = launch_options_from_env(); let options = launch_options_from_env();
let engine = env::var("AGENT_BROWSER_ENGINE").ok(); 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 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?; let mgr = BrowserManager::connect_cdp(&cdp).await?;
state.browser = Some(mgr); state.browser = Some(mgr);
state.subscribe_to_browser_events(); 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 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?; let mgr = BrowserManager::connect_auto().await?;
state.browser = Some(mgr); state.browser = Some(mgr);
state.subscribe_to_browser_events(); state.subscribe_to_browser_events();
@@ -746,13 +753,36 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
return Ok(()); 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.browser = Some(mgr);
state.subscribe_to_browser_events(); state.subscribe_to_browser_events();
try_auto_restore_state(state).await; try_auto_restore_state(state).await;
Ok(()) 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 { fn launch_options_from_env() -> LaunchOptions {
let headed = env::var("AGENT_BROWSER_HEADED") let headed = env::var("AGENT_BROWSER_HEADED")
.map(|v| v == "1" || v == "true") .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] #[tokio::test]
async fn test_execute_unknown_command() { async fn test_execute_unknown_command() {
let mut state = DaemonState::new(); 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() 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)] #[derive(Debug, Clone)]
pub struct PageInfo { pub struct PageInfo {
pub target_id: String, pub target_id: String,
@@ -416,8 +421,22 @@ impl BrowserManager {
if attached_pages.is_empty() { if attached_pages.is_empty() {
self.create_and_attach_blank_page().await?; self.create_and_attach_blank_page().await?;
} else { } else {
let preferred_index = attached_pages
.iter()
.position(|page| !is_startup_internal_page(&page.url));
self.pages = attached_pages; 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> { pub async fn navigate(&mut self, url: &str, wait_until: WaitUntil) -> Result<Value, String> {
let session_id = self.active_session_id()?.to_string(); let session_id = self.active_session_id()?.to_string();
let rx = self.client.subscribe();
let nav_result: PageNavigateResult = self let nav_result: PageNavigateResult = self
.client .client
@@ -467,7 +487,7 @@ impl BrowserManager {
return Err(format!("Navigation failed: {}", error_text)); 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 page_url = self.get_url().await.unwrap_or_else(|_| url.to_string());
let title = self.get_title().await.unwrap_or_default(); let title = self.get_title().await.unwrap_or_default();
@@ -484,14 +504,14 @@ impl BrowserManager {
&self, &self,
wait_until: WaitUntil, wait_until: WaitUntil,
session_id: &str, session_id: &str,
mut rx: tokio::sync::broadcast::Receiver<CdpEvent>,
) -> Result<(), String> { ) -> Result<(), String> {
let event_name = match wait_until { let event_name = match wait_until {
WaitUntil::Load => "Page.loadEventFired", WaitUntil::Load => "Page.loadEventFired",
WaitUntil::DomContentLoaded => "Page.domContentEventFired", 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); let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms);
tokio::time::timeout(timeout, async { tokio::time::timeout(timeout, async {
@@ -506,8 +526,11 @@ impl BrowserManager {
.map_err(|_| format!("Timeout waiting for {}", event_name))? .map_err(|_| format!("Timeout waiting for {}", event_name))?
} }
async fn wait_for_network_idle(&self, session_id: &str) -> Result<(), String> { async fn wait_for_network_idle(
let mut rx = self.client.subscribe(); &self,
session_id: &str,
mut rx: tokio::sync::broadcast::Receiver<CdpEvent>,
) -> Result<(), String> {
let pending = Arc::new(Mutex::new(HashSet::<String>::new())); let pending = Arc::new(Mutex::new(HashSet::<String>::new()));
let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms); let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms);
@@ -626,7 +649,8 @@ impl BrowserManager {
wait_until: WaitUntil, wait_until: WaitUntil,
session_id: &str, session_id: &str,
) -> Result<(), String> { ) -> 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> { pub async fn close(&mut self) -> Result<(), String> {
@@ -1270,4 +1294,15 @@ mod tests {
let msg = "Some custom error message"; let msg = "Some custom error message";
assert_eq!(to_ai_friendly_error(msg), msg); 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; return;
} }
// Launch/start
if data.get("launched").is_some() {
let label = "Browser ready";
println!("{} {}", color::success_indicator(), label);
return;
}
// Closed (browser or tab) // Closed (browser or tab)
if data.get("closed").is_some() { if data.get("closed").is_some() {
let label = match action { let label = match action {
@@ -884,6 +890,24 @@ Examples:
agent-browser open localhost:3000 agent-browser open localhost:3000
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}' agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
# ^ Headers only sent to api.example.com, not other domains # ^ 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" => { "back" => {
@@ -2339,6 +2363,7 @@ Aliases: agent-browser, agent-browser-stealth, abs
Core Commands: Core Commands:
open <url> Navigate to URL open <url> Navigate to URL
start Start managed browser on localhost:9333
click <sel> Click element (or @ref) click <sel> Click element (or @ref)
dblclick <sel> Double-click element dblclick <sel> Double-click element
type <sel> <text> [--delay <ms>] Type into 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. 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: Project policy:
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden - `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
+1
View File
@@ -9,6 +9,7 @@ Executable aliases: `agent-browser`, `agent-browser-stealth`, `abs`.
## Core ## Core
```bash ```bash
agent-browser start # Start/reuse managed browser on localhost:9333
agent-browser open <url> # Navigate (aliases: goto, navigate) agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block when verification/captcha interstitial is detected 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) 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", "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", "description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module", "type": "module",
"main": "dist/daemon.js", "main": "dist/daemon.js",
@@ -23,7 +23,7 @@
"build": "tsc", "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: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: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: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: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 .", "build:docker": "docker build -t agent-browser-builder -f docker/Dockerfile.build .",
+50 -8
View File
@@ -1,26 +1,67 @@
#!/usr/bin/env node #!/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 { copyFileSync, existsSync, mkdirSync } from 'fs';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { platform, arch } from 'os'; import { platform, arch } from 'os';
import { spawnSync } from 'child_process';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '..'); const projectRoot = join(__dirname, '..');
const sourceExt = platform() === 'win32' ? '.exe' : '';
const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);
const binDir = join(projectRoot, 'bin'); const binDir = join(projectRoot, 'bin');
// Determine platform suffix function defaultPaths() {
const platformKey = `${platform()}-${arch()}`; const sourceExt = platform() === 'win32' ? '.exe' : '';
const ext = platform() === 'win32' ? '.exe' : ''; const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`);
const targetName = `agent-browser-${platformKey}${ext}`;
const targetPath = join(binDir, targetName); 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)) { if (!existsSync(sourcePath)) {
console.error(`Error: Native binary not found at ${sourcePath}`); console.error(`Error: Native binary not found at ${sourcePath}`);
@@ -33,4 +74,5 @@ if (!existsSync(binDir)) {
} }
copyFileSync(sourcePath, targetPath); copyFileSync(sourcePath, targetPath);
adHocSignIfNeeded(targetPath);
console.log(`✓ Copied native binary to ${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. 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 ## Core Workflow
Every browser automation follows this pattern: Every browser automation follows this pattern:
@@ -50,6 +52,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle
```bash ```bash
# Navigation # Navigation
agent-browser start # Start/reuse managed browser on localhost:9333
agent-browser open <url> # Navigate (aliases: goto, navigate) agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block if verification/captcha interstitial is detected agent-browser --risk-mode block open <url> # Block if verification/captcha interstitial is detected
agent-browser doctor # Diagnose CDP + sourceURL + tab-group plugin health agent-browser doctor # Diagnose CDP + sourceURL + tab-group plugin health