fix(cli): 增加 abs start 并修复 managed 9333 启动链路
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user