Hands: their own virtual screen, even next to a logged-in desktop

A service set up without a screen (over SSH, or on Wayland) used to pick up the desktop
session's DISPLAY/WAYLAND_DISPLAY from systemd, so screenshots waited for a 「分享螢幕」
answer nobody could give. Now it starts its own virtual screen; `anybot setup --real-screen`
keeps the desktop. `anybot setup --refresh-service` rewrites the start script after an update
(install.sh runs it), so joined computers pick up such changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
This commit is contained in:
王性驊 2026-11-05 08:10:13 +08:00
parent 8615881634
commit 370dbd91f7
2 changed files with 48 additions and 3 deletions

View File

@ -303,8 +303,8 @@ case $MODE in
ok "程式放在 ~/.local/bin/anybot"
[ "$MODE" = app ] && ok "應用程式選單裡有 AnyBot"
# 更新: the one running in the background (anybot setup's service) switches to the new one.
if systemctl --user is-active --quiet anybot 2>/dev/null; then
systemctl --user restart anybot && ok "重新啟動背景執行的 AnyBot(換成新版)"
if [ -f "$HOME/.local/bin/anybot-run" ] && systemctl --user is-enabled --quiet anybot 2>/dev/null; then
"$HOME/.local/bin/anybot" setup --refresh-service >/dev/null 2>&1 && ok "重新啟動背景執行的 AnyBot(換成新版)"
fi
fi
fi

View File

@ -795,6 +795,42 @@ impl Plan {
}
}
/// `anybot setup --refresh-service` (install.sh runs it after an update): write the start script
/// again — a newer AnyBot may start differently — and restart. The computer stays joined as it was,
/// and a screen remembered at setup stays.
fn refresh_service() -> i32 {
let Some(home) = dirs::home_dir() else { return 1 };
let launcher = home.join(".local/bin/anybot-run");
let Ok(old) = std::fs::read_to_string(&launcher) else {
println!("這台沒有設成開機自動執行,不用更新服務。");
return 0;
};
let role = if old.contains("accessibility bus") { Role::Hands } else { Role::Brain };
let display = old.lines().find_map(|l| l.strip_prefix("export DISPLAY=\"${DISPLAY:-").and_then(|r| r.strip_suffix("}\""))).unwrap_or("").to_string();
let env = Env { linux: cfg!(target_os = "linux"), display, systemd: true };
let exe = match std::env::current_exe().and_then(|p| p.canonicalize()) {
Ok(e) => e,
Err(e) => {
eprintln!("✗ 找不到 AnyBot 程式:{e}");
return 1;
}
};
if let Err(e) = std::fs::write(&launcher, launcher_script(role, &exe, &env)) {
eprintln!("✗ 寫入 {} 失敗:{e}", launcher.display());
return 1;
}
match systemctl(&["restart", UNIT]) {
Ok(_) => {
println!("已更新並重新啟動 AnyBot 服務。");
0
}
Err(e) => {
eprintln!("✗ 重新啟動失敗:{e}");
1
}
}
}
fn launcher_script(role: Role, exe: &std::path::Path, env: &Env) -> String {
let q = |s: &str| format!("'{}'", s.replace('\'', "'\\''"));
let mut s = String::from("#!/bin/sh\n# Started by systemd (anybot.service); written by `anybot setup`.\n");
@ -806,11 +842,17 @@ fn launcher_script(role: Role, exe: &std::path::Path, env: &Env) -> String {
if role == Role::Hands {
s += "# The accessibility bus, so bots can read apps' buttons instead of only looking at pixels.\n";
s += "export NO_AT_BRIDGE=0 GTK_A11Y=atspi QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\n";
let real_screen = std::env::args().any(|a| a == "--real-screen");
if env.has_display() && env.display != "wayland" && env.display != "desktop" {
s += &format!("# The screen this computer had when it was set up.\nexport DISPLAY=\"${{DISPLAY:-{}}}\"\n", env.display);
if let Ok(x) = std::env::var("XAUTHORITY") {
s += &format!("export XAUTHORITY=\"${{XAUTHORITY:-{x}}}\"\n");
}
} else if !real_screen {
// Set up without a screen (over SSH, or on Wayland): the bots get a virtual screen of
// their own, even when a desktop session is logged in here — nobody would be there to
// answer its 「允許分享螢幕」 dialog. `anybot setup --real-screen` uses the desktop.
s += "unset DISPLAY WAYLAND_DISPLAY XDG_SESSION_TYPE\n";
}
s += "if [ -z \"$DISPLAY\" ] && [ -z \"$WAYLAND_DISPLAY\" ] && command -v xvfb-run >/dev/null; then\n";
s += " # No screen: a virtual one, a window manager, and a session bus for the accessibility bus.\n";
@ -843,9 +885,12 @@ const VALUED: [&str; 4] = ["--server", "--user", "--name", "--brain-name"];
pub fn run(args: &[String]) -> i32 {
let flag = |k: &str| args.iter().position(|a| a == k).and_then(|i| args.get(i + 1)).cloned();
if args.iter().any(|a| a == "-h" || a == "--help") {
println!("anybot setup:把這台電腦接到你的 AnyBot。\n\n anybot setup 網址 配對碼 (手機或電腦的 AnyBot 按「加一台電腦」會給你這一行)\n anybot setup 一個畫面:填網址和配對碼(或帳號密碼)\n 加 --brain 這台當大腦,不當手腳\n 加 --no-service 不設開機自動執行\n\n給程式用:anybot setup --server 網址 --user 帳號 [--brain-name 大腦名字],密碼放在 ANYBOT_ACCOUNT_PASSWORD。");
println!("anybot setup:把這台電腦接到你的 AnyBot。\n\n anybot setup 網址 配對碼 (手機或電腦的 AnyBot 按「加一台電腦」會給你這一行)\n anybot setup 一個畫面:填網址和配對碼(或帳號密碼)\n 加 --brain 這台當大腦,不當手腳\n 加 --no-service 不設開機自動執行\n 加 --real-screen 讓 Bot 用這台登入中的桌面(預設用自己的虛擬螢幕)\n\n給程式用:anybot setup --server 網址 --user 帳號 [--brain-name 大腦名字],密碼放在 ANYBOT_ACCOUNT_PASSWORD。");
return 0;
}
if args.iter().any(|a| a == "--refresh-service") {
return refresh_service();
}
let role = if args.iter().any(|a| a == "--brain") { Role::Brain } else { Role::Hands };
let mut pos = vec![];
let mut skip = false;