Linux: the front app is found by its program, and accessibility is switched on when it's off

agent-desktop knows an app by its program's name, but the X11 window says its class: a Snap
Firefox is "firefox_firefox", so the front app was never found and the bot got a bare picture
(「看不出最前面是哪個 App(WINDOW_NOT_FOUND)」). The window's process now gives the name.

When there are no controls at all and the desktop's accessibility switch (org.a11y.Status
IsEnabled) is off — most apps then show none — AnyBot turns it on, as a screen reader would, and
tells the bot that apps already open show their numbers after a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
This commit is contained in:
王性驊 2026-11-08 00:58:18 +08:00
parent 645db96d9c
commit 28c043617e
2 changed files with 41 additions and 1 deletions

View File

@ -768,7 +768,12 @@ fn look(after: Option<String>, 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, 「」).
let front = if cfg!(target_os = "linux") { mac::front_app().map(|(n, _)| n).unwrap_or_default() } else { String::new() };
// 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() {
match crate::desktop::elements(&front).or_else(|e| if front.is_empty() { Err(e) } else { crate::desktop::elements("") }) {
Ok(f) => (Some(f), String::new()),
@ -781,6 +786,15 @@ fn look(after: Option<String>, pointer: bool) -> Result<(String, Option<String>)
ask_permission("Privacy_ScreenCapture");
return Err(NO_SCREEN_RECORDING.into());
}
// No controls at all, and the desktop's accessibility switch was off: turn it on (Linux).
#[cfg(target_os = "linux")]
let a11y_note = if controls_allowed() && found.as_ref().is_none_or(|f| f.2.is_empty()) && crate::desk::accessibility_switched_on() {
"\n(這台的無障礙介面原本是關著的,AnyBot 剛把它打開:之後打開的 App 才會有編號,已經開著的 App 要關掉重開。網頁請改用 browser_* 工具。)"
} else {
""
};
#[cfg(not(target_os = "linux"))]
let a11y_note = "";
let (app, window, mut els) = found.unwrap_or_default();
// Only what is on this screen gets a number (a link below the fold was numbered at the
// picture's edge, and a click there landed on the edge).
@ -828,6 +842,7 @@ fn look(after: Option<String>, pointer: bool) -> Result<(String, Option<String>)
} else {
text.push_str(&format!("\n「{app}」上可以操作的東西(圖上同樣的編號,用 ref 點):\n{}", lines.join("\n")));
}
text.push_str(a11y_note);
Ok((text, img_b64))
}

View File

@ -262,6 +262,31 @@ pub fn front_app() -> Option<(String, i32)> {
}
}
// ---------- accessibility (the numbered controls) ----------
/// The program a window belongs to, the way agent-desktop names apps (the executable's name): a
/// Snap Firefox's window says "firefox_firefox", its program is "firefox".
pub fn program_of(pid: i32) -> Option<String> {
if pid <= 0 {
return None;
}
let exe = std::fs::read_link(format!("/proc/{pid}/exe")).ok()?;
let name = exe.file_name()?.to_str()?.trim_end_matches(" (deleted)").to_string();
(!name.is_empty()).then_some(name)
}
/// Turn the desktop's accessibility switch on when it is off (what a screen reader does):
/// without it most apps never show their controls, so the bot gets no numbers. True when it was
/// off and is on now; apps already open show theirs once restarted.
pub fn accessibility_switched_on() -> bool {
let Ok(conn) = zbus::blocking::Connection::session() else { return false };
let Ok(p) = zbus::blocking::Proxy::new(&conn, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Status") else { return false };
if p.get_property::<bool>("IsEnabled").unwrap_or(true) {
return false;
}
p.set_property("IsEnabled", true).is_ok()
}
// ---------- idle and screensaver ----------
fn dbus_call<R: serde::de::DeserializeOwned + zbus::zvariant::Type>(dest: &str, path: &str, iface: &str, method: &str) -> Option<R> {