feat(stealth): humanized typing cadence + --humanize flag (v3)
- Typing: type_text_into_active_context now uses variable, human-like inter-keystroke gaps from humanize::keystroke_delays when no explicit --delay is given (Fast/Human); Off stays instant. Explicit --delay still wins. - CLI: `--humanize off|fast|human` surfaces AGENT_BROWSER_HUMANIZE so the session's daemon (a child that inherits this env) applies it, overriding the adaptive detector. Invalid values warn and are ignored. Verified headless: `--humanize human` + type lands "hello world" correctly. Deferred: in-bbox landing jitter (helper ready, needs bbox threaded) + wheel/drag easing.
This commit is contained in:
@@ -248,6 +248,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
|||||||
"--screenshot-format",
|
"--screenshot-format",
|
||||||
"--idle-timeout",
|
"--idle-timeout",
|
||||||
"--model",
|
"--model",
|
||||||
|
"--humanize",
|
||||||
];
|
];
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
@@ -796,6 +797,21 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--humanize" => {
|
||||||
|
// Human-like input motion level (off|fast|human). Surface it as
|
||||||
|
// AGENT_BROWSER_HUMANIZE so the daemon — spawned as a child that
|
||||||
|
// inherits this process's env — picks it up and it overrides the
|
||||||
|
// adaptive detector. Applies when the session's daemon launches.
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
match crate::native::humanize::HumanizeLevel::parse(s) {
|
||||||
|
Some(_) => std::env::set_var("AGENT_BROWSER_HUMANIZE", s),
|
||||||
|
None => eprintln!(
|
||||||
|
"warning: --humanize must be off|fast|human, got {s:?} (ignored)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"--screenshot-dir" => {
|
"--screenshot-dir" => {
|
||||||
if let Some(s) = args.get(i + 1) {
|
if let Some(s) = args.get(i + 1) {
|
||||||
flags.screenshot_dir = Some(s.clone());
|
flags.screenshot_dir = Some(s.clone());
|
||||||
@@ -922,6 +938,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
"--screenshot-format",
|
"--screenshot-format",
|
||||||
"--idle-timeout",
|
"--idle-timeout",
|
||||||
"--model",
|
"--model",
|
||||||
|
"--humanize",
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|||||||
@@ -350,9 +350,18 @@ pub async fn type_text_into_active_context(
|
|||||||
text: &str,
|
text: &str,
|
||||||
delay_ms: Option<u64>,
|
delay_ms: Option<u64>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let delay = delay_ms.unwrap_or(0);
|
// Per-character timing: an explicit `delay_ms` wins (caller asked for a
|
||||||
|
// fixed cadence); otherwise fall back to humanize — variable, human-like
|
||||||
|
// inter-keystroke gaps at Fast/Human, all-zero (instant) at Off.
|
||||||
|
let chars: Vec<char> = text.chars().collect();
|
||||||
|
let cadence: Vec<std::time::Duration> = match delay_ms {
|
||||||
|
Some(d) => vec![std::time::Duration::from_millis(d); chars.len()],
|
||||||
|
None => {
|
||||||
|
humanize::keystroke_delays(chars.len(), humanize::active_level(), humanize::next_seed())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for ch in text.chars() {
|
for (i, ch) in chars.into_iter().enumerate() {
|
||||||
if matches!(ch, '\n' | '\r' | '\t') {
|
if matches!(ch, '\n' | '\r' | '\t') {
|
||||||
let (key, code, key_code) = char_to_key_info(ch);
|
let (key, code, key_code) = char_to_key_info(ch);
|
||||||
let text_str = key_text(&key);
|
let text_str = key_text(&key);
|
||||||
@@ -404,8 +413,9 @@ pub async fn type_text_into_active_context(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if delay > 0 {
|
let gap = cadence[i];
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
|
if !gap.is_zero() {
|
||||||
|
tokio::time::sleep(gap).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user