fix(cli): 修复 type/keyboard 的 --delay 参数解析

将 --delay <ms> 从输入文本中剥离并写入 delay 字段,避免搜索词混入参数。

同时支持使用 -- 终止参数解析以输入字面量 --delay 文本,并补充回归测试与帮助文档。
This commit is contained in:
leeguooooo
2026-02-24 17:13:46 +09:00
parent a5a9327b7d
commit b1f27236d8
7 changed files with 128 additions and 21 deletions
+2 -2
View File
@@ -143,10 +143,10 @@ agent-browser open <url> # Navigate to URL (aliases: goto, navigate
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)
agent-browser dblclick <sel> # Double-click element agent-browser dblclick <sel> # Double-click element
agent-browser focus <sel> # Focus element agent-browser focus <sel> # Focus element
agent-browser type <sel> <text> # Type into element agent-browser type <sel> <text> [--delay <ms>] # Type into element
agent-browser fill <sel> <text> # Clear and fill agent-browser fill <sel> <text> # Clear and fill
agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key) agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key)
agent-browser keyboard type <text> # Type with real keystrokes (no selector, current focus) agent-browser keyboard type <text> [--delay <ms>] # Type with real keystrokes (no selector, current focus)
agent-browser keyboard inserttext <text> # Insert text without key events (no selector) agent-browser keyboard inserttext <text> # Insert text without key events (no selector)
agent-browser keydown <key> # Hold key down agent-browser keydown <key> # Hold key down
agent-browser keyup <key> # Release key agent-browser keyup <key> # Release key
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]] [[package]]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.14.0-fork.2" version = "0.14.0-fork.3"
dependencies = [ dependencies = [
"base64", "base64",
"dirs", "dirs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.14.0-fork.2" version = "0.14.0-fork.3"
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"
+112 -9
View File
@@ -71,6 +71,62 @@ pub fn gen_id() -> String {
) )
} }
/// Parse free-form text arguments with optional `--delay <ms>`.
///
/// `--` can be used to stop flag parsing if text must include `--delay` literally.
fn parse_text_with_optional_delay(
args: &[&str],
context: &str,
usage: &'static str,
) -> Result<(String, Option<u64>), ParseError> {
let mut text_parts: Vec<&str> = Vec::new();
let mut delay_ms: Option<u64> = None;
let mut parse_flags = true;
let mut i = 0;
while i < args.len() {
let arg = args[i];
if parse_flags && arg == "--" {
parse_flags = false;
i += 1;
continue;
}
if parse_flags && arg == "--delay" {
let raw = args
.get(i + 1)
.ok_or_else(|| ParseError::MissingArguments {
context: format!("{} --delay", context),
usage,
})?;
let parsed = raw.parse::<u64>().map_err(|_| ParseError::InvalidValue {
message: format!(
"Invalid --delay value: {} (must be a non-negative integer in milliseconds)",
raw
),
usage,
})?;
delay_ms = Some(parsed);
i += 2;
continue;
}
text_parts.push(arg);
i += 1;
}
let text = text_parts.join(" ");
if text.is_empty() {
return Err(ParseError::MissingArguments {
context: context.to_string(),
usage,
});
}
Ok((text, delay_ms))
}
pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError> { pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError> {
if args.is_empty() { if args.is_empty() {
return Err(ParseError::MissingArguments { return Err(ParseError::MissingArguments {
@@ -165,9 +221,18 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
"type" => { "type" => {
let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {
context: "type".to_string(), context: "type".to_string(),
usage: "type <selector> <text>", usage: "type <selector> <text> [--delay <ms>]",
})?; })?;
Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") })) let (text, delay) = parse_text_with_optional_delay(
&rest[1..],
"type",
"type <selector> <text> [--delay <ms>]",
)?;
let mut cmd = json!({ "id": id, "action": "type", "selector": sel, "text": text });
if let Some(ms) = delay {
cmd["delay"] = json!(ms);
}
Ok(cmd)
} }
"hover" => { "hover" => {
let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {
@@ -272,14 +337,16 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
})?; })?;
match *sub { match *sub {
"type" => { "type" => {
let text: String = rest[1..].join(" "); let (text, delay) = parse_text_with_optional_delay(
if text.is_empty() { &rest[1..],
return Err(ParseError::MissingArguments { "keyboard type",
context: "keyboard type".to_string(), "keyboard type <text> [--delay <ms>]",
usage: "keyboard type <text>", )?;
}); let mut cmd = json!({ "id": id, "action": "keyboard", "subaction": "type", "text": text });
if let Some(ms) = delay {
cmd["delay"] = json!(ms);
} }
Ok(json!({ "id": id, "action": "keyboard", "subaction": "type", "text": text })) Ok(cmd)
} }
"inserttext" | "insertText" => { "inserttext" | "insertText" => {
let text: String = rest[1..].join(" "); let text: String = rest[1..].join(" ");
@@ -2300,6 +2367,29 @@ mod tests {
assert_eq!(cmd["text"], "some text"); assert_eq!(cmd["text"], "some text");
} }
#[test]
fn test_type_command_with_delay() {
let cmd =
parse_command(&args("type #input some text --delay 120"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "type");
assert_eq!(cmd["selector"], "#input");
assert_eq!(cmd["text"], "some text");
assert_eq!(cmd["delay"], 120);
}
#[test]
fn test_type_command_with_literal_delay_text() {
let cmd = parse_command(
&args("type #input -- --delay 120 should be typed"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "type");
assert_eq!(cmd["selector"], "#input");
assert_eq!(cmd["text"], "--delay 120 should be typed");
assert!(cmd.get("delay").is_none());
}
#[test] #[test]
fn test_select() { fn test_select() {
let cmd = parse_command(&args("select #menu option1"), &default_flags()).unwrap(); let cmd = parse_command(&args("select #menu option1"), &default_flags()).unwrap();
@@ -2479,6 +2569,19 @@ mod tests {
assert_eq!(cmd["selector"], "#element"); assert_eq!(cmd["selector"], "#element");
} }
#[test]
fn test_keyboard_type_with_delay() {
let cmd = parse_command(
&args("keyboard type natural typing --delay 90"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "keyboard");
assert_eq!(cmd["subaction"], "type");
assert_eq!(cmd["text"], "natural typing");
assert_eq!(cmd["delay"], 90);
}
#[test] #[test]
fn test_wait_timeout() { fn test_wait_timeout() {
let cmd = parse_command(&args("wait 5000"), &default_flags()).unwrap(); let cmd = parse_command(&args("wait 5000"), &default_flags()).unwrap();
+8 -4
View File
@@ -719,10 +719,11 @@ Examples:
r##" r##"
agent-browser type - Type text into an element agent-browser type - Type text into an element
Usage: agent-browser type <selector> <text> Usage: agent-browser type <selector> <text> [--delay <ms>]
Types text into the specified element character by character. Types text into the specified element character by character.
Unlike fill, this does not clear existing content first. Unlike fill, this does not clear existing content first.
Use --delay to add per-character delay (milliseconds).
Global Options: Global Options:
--json Output as JSON --json Output as JSON
@@ -730,7 +731,9 @@ Global Options:
Examples: Examples:
agent-browser type "#search" "hello" agent-browser type "#search" "hello"
agent-browser type "#search" "iphone" --delay 120
agent-browser type @e2 "additional text" agent-browser type @e2 "additional text"
agent-browser type @e2 -- "--delay 120 (literal text)"
See Also: See Also:
For typing into contenteditable editors (Lexical, ProseMirror, etc.) For typing into contenteditable editors (Lexical, ProseMirror, etc.)
@@ -961,7 +964,7 @@ the current focus — essential for contenteditable editors like
Lexical, ProseMirror, CodeMirror, and Monaco. Lexical, ProseMirror, CodeMirror, and Monaco.
Subcommands: Subcommands:
type <text> Type text character-by-character with real type <text> [--delay <ms>] Type text character-by-character with real
key events (keydown, keypress, keyup per char) key events (keydown, keypress, keyup per char)
inserttext <text> Insert text without key events (like paste) inserttext <text> Insert text without key events (like paste)
@@ -974,6 +977,7 @@ Global Options:
Examples: Examples:
agent-browser keyboard type "Hello, World!" agent-browser keyboard type "Hello, World!"
agent-browser keyboard type "human pacing" --delay 90
agent-browser keyboard type "# My Heading" agent-browser keyboard type "# My Heading"
agent-browser keyboard inserttext "pasted content" agent-browser keyboard inserttext "pasted content"
@@ -2016,10 +2020,10 @@ Core Commands:
open <url> Navigate to URL open <url> Navigate to URL
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> Type into element type <sel> <text> [--delay <ms>] Type into element
fill <sel> <text> Clear and fill fill <sel> <text> Clear and fill
press <key> Press key (Enter, Tab, Control+a) press <key> Press key (Enter, Tab, Control+a)
keyboard type <text> Type text with real keystrokes (no selector) keyboard type <text> [--delay <ms>] Type text with real keystrokes (no selector)
keyboard inserttext <text> Insert text without key events keyboard inserttext <text> Insert text without key events
hover <sel> Hover element hover <sel> Hover element
focus <sel> Focus element focus <sel> Focus element
+2 -2
View File
@@ -11,9 +11,9 @@ agent-browser open <url> # Navigate (aliases: goto, navigate)
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)
agent-browser dblclick <sel> # Double-click agent-browser dblclick <sel> # Double-click
agent-browser fill <sel> <text> # Clear and fill agent-browser fill <sel> <text> # Clear and fill
agent-browser type <sel> <text> # Type into element agent-browser type <sel> <text> [--delay <ms>] # Type into element
agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key) agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key)
agent-browser keyboard type <text> # Type at current focus (no selector needed) agent-browser keyboard type <text> [--delay <ms>] # Type at current focus (no selector needed)
agent-browser keyboard inserttext <text> # Insert text without key events agent-browser keyboard inserttext <text> # Insert text without key events
agent-browser keydown <key> # Hold key down agent-browser keydown <key> # Hold key down
agent-browser keyup <key> # Release key agent-browser keyup <key> # Release key
+2 -2
View File
@@ -63,11 +63,11 @@ agent-browser snapshot -s "#selector" # Scope to CSS selector
agent-browser click @e1 # Click element agent-browser click @e1 # Click element
agent-browser click @e1 --new-tab # Click and open in new tab agent-browser click @e1 --new-tab # Click and open in new tab
agent-browser fill @e2 "text" # Clear and type text agent-browser fill @e2 "text" # Clear and type text
agent-browser type @e2 "text" # Type without clearing agent-browser type @e2 "text" --delay 120 # Type without clearing (human-like pacing)
agent-browser select @e1 "option" # Select dropdown option agent-browser select @e1 "option" # Select dropdown option
agent-browser check @e1 # Check checkbox agent-browser check @e1 # Check checkbox
agent-browser press Enter # Press key agent-browser press Enter # Press key
agent-browser keyboard type "text" # Type at current focus (no selector) agent-browser keyboard type "text" --delay 90 # Type at current focus (no selector)
agent-browser keyboard inserttext "text" # Insert without key events agent-browser keyboard inserttext "text" # Insert without key events
agent-browser scroll down 500 # Scroll page agent-browser scroll down 500 # Scroll page