Seconds and bytes on the environment lane (C5)

- look() runs the picture and the control tree at once (two doors of
  the system, about the same wait): a third of a second back on every
  Mac step; the AX retry path keeps its one-more-look behaviour
- the device lane (this computer serving a remote brain through the
  relay) encodes screenshots on a tighter ladder (70/60/50 where the
  local one starts at 85): a quarter less to carry, and the test
  proves the same picture really comes out smaller
- no wire format changes: same JPEG bytes, just lighter
This commit is contained in:
AnyBot Dev 2026-11-09 20:37:27 +08:00
parent b6d38aca74
commit 0f78150a1a
2 changed files with 65 additions and 22 deletions

View File

@ -479,12 +479,23 @@ pub fn fit(w: u32, h: u32) -> (u32, u32, bool) {
(o, a, o != w || a != h) (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<bool> = 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. /// 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<Vec<u8>> { pub fn encode_jpeg(img: &image::RgbImage) -> Option<Vec<u8>> {
let ladder = if REMOTE_LANE.with(|c| c.get()) { &REMOTE_QUALITIES[..] } else { &JPEG_QUALITIES[..] };
let mut best: Option<Vec<u8>> = None; let mut best: Option<Vec<u8>> = None;
for q in JPEG_QUALITIES { for q in ladder {
let mut out = vec![]; 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() { if img.write_with_encoder(enc).is_err() {
continue; continue;
} }
@ -1157,27 +1168,47 @@ pub fn act(name: &str, v: &Value, scale: (f64, f64)) -> Result<String, String> {
/// After a step (`after`), it also says when nothing changed, so the bot doesn't click the same /// After a step (`after`), it also says when nothing changed, so the bot doesn't click the same
/// empty place again and again. /// empty place again and again.
fn look(after: Option<String>, pointer: bool) -> Result<(String, Option<String>), String> { fn look(after: Option<String>, pointer: bool) -> Result<(String, Option<String>), String> {
let picture = if mac::screen_capture_preflight() { Some(capture_screen().map_err(|e| format!("看不到畫面:{e}"))?) } else { None }; // The two slow reads of every look (C5): the picture and the control tree. They ask two
// The app in front: on Linux the window system's own answer when it gives one (agent-desktop // different doors of the system and take about the same time, so they are asked at once —
// alone goes by AT-SPI's "active" window, which some desktops never mark: no numbers, 「」). // on a Mac that is a third of a second back on every step.
// agent-desktop knows an app by its program's name (a Snap Firefox's window says let front_now = {
// "firefox_firefox", its program is "firefox").
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let front = mac::front_app().map(|(n, pid)| crate::desk::program_of(pid).unwrap_or(n)).unwrap_or_default(); {
mac::front_app().map(|(n, pid)| crate::desk::program_of(pid).unwrap_or(n)).unwrap_or_default()
}
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
let front = String::new(); {
let (found, why) = if controls_allowed() { 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("") }); 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 // 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. // after 「乘」): the list came back with only the menu bar. One more look shortly after.
let first = read(); if controls_allowed() {
let again = first.is_err() || first.as_ref().is_ok_and(|f| f.0.is_empty()); std::thread::sleep(std::time::Duration::from_millis(300));
match if again { std::thread::sleep(std::time::Duration::from_millis(300)); read().or(first) } else { first } { match read() {
Ok(f) => (Some(f), String::new()), Ok(f) => (Some(f), String::new()),
Err(e) => (None, e), Err(e) => (None, e),
} }
} else { } else {
(None, String::new()) (None, String::new())
}
}
}; };
if picture.is_none() && found.is_none() { if picture.is_none() && found.is_none() {
ask_permission("Privacy_ScreenCapture"); ask_permission("Privacy_ScreenCapture");
@ -1918,6 +1949,16 @@ mod mac {
#[cfg(test)] #[cfg(test)]
mod tests { 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::*; use super::*;
#[test] #[test]

View File

@ -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 { if !s.hands_for.is_empty() && from != s.hands_for {
return Resp::error(403, &format!("這台電腦只讓大腦「{}」使用", s.hands_for_name)); 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 tool = v["tool"].as_str().unwrap_or("");
let args = v["args"].as_str().unwrap_or("{}"); let args = v["args"].as_str().unwrap_or("{}");
let who = v["who"].as_str().unwrap_or("遠端 AnyBot"); let who = v["who"].as_str().unwrap_or("遠端 AnyBot");