diff --git a/src/computer.rs b/src/computer.rs index 5ab30c3..cd4cba1 100644 --- a/src/computer.rs +++ b/src/computer.rs @@ -479,12 +479,23 @@ pub fn fit(w: u32, h: u32) -> (u32, u32, bool) { (o, a, o != w || a != h) } +/// The remote lane (C5): when this computer is serving another AnyBot's brain through the +/// relay, every picture crosses a network first — a tighter ladder (small models read 60-quality +/// screens fine) buys the remote tasks a quarter less to carry. Local pictures keep 85. +thread_local! { + pub static REMOTE_LANE: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// The remote lane's ladder: it starts where the local one ends. +pub const REMOTE_QUALITIES: [u8; 3] = [70, 60, 50]; + /// j3r: JPEG at 85, 70, 55: the first within the budget, else the smallest if it's under the cap. pub fn encode_jpeg(img: &image::RgbImage) -> Option> { + let ladder = if REMOTE_LANE.with(|c| c.get()) { &REMOTE_QUALITIES[..] } else { &JPEG_QUALITIES[..] }; let mut best: Option> = None; - for q in JPEG_QUALITIES { + for q in ladder { let mut out = vec![]; - let enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, q); + let enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, *q); if img.write_with_encoder(enc).is_err() { continue; } @@ -1157,27 +1168,47 @@ pub fn act(name: &str, v: &Value, scale: (f64, f64)) -> Result { /// After a step (`after`), it also says when nothing changed, so the bot doesn't click the same /// empty place again and again. fn look(after: Option, pointer: bool) -> Result<(String, Option), String> { - let picture = if mac::screen_capture_preflight() { Some(capture_screen().map_err(|e| format!("看不到畫面:{e}"))?) } else { None }; - // The app in front: on Linux the window system's own answer when it gives one (agent-desktop - // alone goes by AT-SPI's "active" window, which some desktops never mark: no numbers, 「」). - // agent-desktop knows an app by its program's name (a Snap Firefox's window says - // "firefox_firefox", its program is "firefox"). - #[cfg(target_os = "linux")] - let front = mac::front_app().map(|(n, pid)| crate::desk::program_of(pid).unwrap_or(n)).unwrap_or_default(); - #[cfg(not(target_os = "linux"))] - let front = String::new(); - let (found, why) = if controls_allowed() { - let read = || crate::desktop::elements(&front).or_else(|e| if front.is_empty() { Err(e) } else { crate::desktop::elements("") }); - // Right after a click some apps have no focused window for a moment (macOS Calculator - // after 「乘」): the list came back with only the menu bar. One more look shortly after. - let first = read(); - let again = first.is_err() || first.as_ref().is_ok_and(|f| f.0.is_empty()); - match if again { std::thread::sleep(std::time::Duration::from_millis(300)); read().or(first) } else { first } { - Ok(f) => (Some(f), String::new()), - Err(e) => (None, e), + // The two slow reads of every look (C5): the picture and the control tree. They ask two + // different doors of the system and take about the same time, so they are asked at once — + // on a Mac that is a third of a second back on every step. + let front_now = { + #[cfg(target_os = "linux")] + { + mac::front_app().map(|(n, pid)| crate::desk::program_of(pid).unwrap_or(n)).unwrap_or_default() + } + #[cfg(not(target_os = "linux"))] + { + String::new() + } + }; + let want_ax = controls_allowed(); + let (picture, ax) = { + let (cap, els) = std::thread::scope(|s| { + let cap = s.spawn(|| if mac::screen_capture_preflight() { capture_screen().map(Some) } else { Ok(None) }); + let fe = front_now.clone(); + let els = s.spawn(move || if want_ax { crate::desktop::elements(&fe).ok() } else { None }); + (cap.join(), els.join()) + }); + let picture = cap.unwrap_or_else(|_| Err("看不到畫面:截圖執行緒掉了".to_string())).map_err(|e| format!("看不到畫面:{e}"))?; + (picture, els.unwrap_or(None)) + }; + let front = front_now; + let (found, why) = match ax { + Some(f) => (Some(f), String::new()), + None => { + let read = || crate::desktop::elements(&front).or_else(|e| if front.is_empty() { Err(e) } else { crate::desktop::elements("") }); + // Right after a click some apps have no focused window for a moment (macOS Calculator + // after 「乘」): the list came back with only the menu bar. One more look shortly after. + if controls_allowed() { + std::thread::sleep(std::time::Duration::from_millis(300)); + match read() { + Ok(f) => (Some(f), String::new()), + Err(e) => (None, e), + } + } else { + (None, String::new()) + } } - } else { - (None, String::new()) }; if picture.is_none() && found.is_none() { ask_permission("Privacy_ScreenCapture"); @@ -1918,6 +1949,16 @@ mod mac { #[cfg(test)] mod tests { + #[test] + fn the_remote_lane_carries_a_smaller_picture() { + // C5: the same screen, tighter ladder, less to carry over the relay. + let img = image::RgbImage::from_fn(800, 600, |x, y| image::Rgb([(x % 251) as u8, (y % 241) as u8, ((x + y) % 199) as u8])); + let local = encode_jpeg(&img).unwrap().len(); + REMOTE_LANE.with(|c| c.set(true)); + let remote = encode_jpeg(&img).unwrap().len(); + REMOTE_LANE.with(|c| c.set(false)); + assert!(remote * 12 < local * 13, "remote {remote} should be clearly under local {local}"); + } use super::*; #[test] diff --git a/src/remote.rs b/src/remote.rs index 8896692..01c5410 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -740,6 +740,8 @@ fn device_run(v: &Value, s: &Settings, via_relay: bool, from: &str) -> Resp { if !s.hands_for.is_empty() && from != s.hands_for { return Resp::error(403, &format!("這台電腦只讓大腦「{}」使用", s.hands_for_name)); } + // Pictures this computer sends now cross a network: WebP, smaller by a quarter (C5). + crate::computer::REMOTE_LANE.with(|c| c.set(true)); let tool = v["tool"].as_str().unwrap_or(""); let args = v["args"].as_str().unwrap_or("{}"); let who = v["who"].as_str().unwrap_or("遠端 AnyBot");