Hands: screen tools for the account's other computers; one pop-up per step; clearer TLS errors

- The brain's bots get computer_use_* for 手腳 (devices), as the prompt already said; before,
  they only had shell there and fell back to scrot, which can't see a Wayland desktop
- After saying yes in the 「使用電腦工具」 pop-up (it shows the step), that same step isn't asked
  again (sending messages still asks every time)
- A certificate failure says so instead of 「網路連線出了問題」
- install.sh restarts the background AnyBot after an update

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 07:56:19 +08:00
parent 2bc573f095
commit c71e8357b8
4 changed files with 25 additions and 5 deletions

View File

@ -292,6 +292,10 @@ case $MODE in
sh "$SRC/deploy/linux/install.sh" >/dev/null || die "安裝失敗。" "執行 sh $SRC/deploy/linux/install.sh 看錯誤。"
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(換成新版)"
fi
fi
fi
PATH="$HOME/.local/bin:$PATH"

View File

@ -630,6 +630,9 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
// Permission to use computers at all, and this Mac in particular, is asked for
// when it's needed instead of the step failing quietly.
let mut gate: Result<(), String> = Ok(());
// The person just said yes to this very step in a switch's pop-up (it shows the
// step): no second pop-up for it.
let mut said_yes = false;
// 直接操作: like Grok Bot after its one-time setup ("讓 Grok Bot 在你的電腦上開啟檔案和
// 執行任務。自動審核仍會先檢查所有操作", ENHi8c), nothing is asked except what the
// safety checks and the person's "一律詢問" rules stop.
@ -642,8 +645,11 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
if !tools_on {
let reason = format!("讓 {} 使用電腦工具:{}\n(在 {} 上)", job.via_name, what, t.name);
match ask_person(ApprovalKind::EnableTools, reason, "tools:on".into()) {
ApprovalAnswer::Once => {}
ApprovalAnswer::Always(_) => tools_on = true,
ApprovalAnswer::Once => said_yes = true,
ApprovalAnswer::Always(_) => {
tools_on = true;
said_yes = true;
}
ApprovalAnswer::Reject => gate = Err("使用者沒有允許使用電腦工具。請改用文字回答,或問使用者想怎麼做。".into()),
}
}
@ -654,8 +660,11 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
if gate.is_ok() && matches!(t.place, tools::Place::Local { .. }) && !local_on {
let reason = format!("在你的電腦上執行任務,而不只是在它自己的電腦上:{}{what}\n(在 {} 上)", if is_web { format!("{} ", web::label(&call.name)) } else { String::new() }, t.name);
match ask_person(ApprovalKind::EnableThisComputer, reason, "local:on".into()) {
ApprovalAnswer::Once => {}
ApprovalAnswer::Always(_) => local_on = true,
ApprovalAnswer::Once => said_yes = true,
ApprovalAnswer::Always(_) => {
local_on = true;
said_yes = true;
}
ApprovalAnswer::Reject => gate = Err(format!("使用者沒有允許在「{}」上執行。可以改用「{}」,或問使用者。", t.name, crate::model::BOT_COMPUTER_NAME)),
}
}
@ -764,6 +773,8 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
let approved = match decision {
_ if gate.is_err() => Err((gate.clone().unwrap_err(), ToolStatus::Denied)),
Decision::Allow => Ok(()),
// Sending (Messages, mail…) still asks every time, even right after a switch.
Decision::Ask { .. } if said_yes && !(is_mac && crate::macapps::is_send(&call.name)) => Ok(()),
Decision::Deny(r) => Err((format!("被安全規則擋下:{r}"), ToolStatus::Denied)),
Decision::Ask { reason, grant } => {
let machine = target.as_ref().map(|t| t.name.clone()).unwrap_or_default();

View File

@ -698,6 +698,8 @@ impl App {
if crate::computer::screen_supported() {
screens.push(crate::model::THIS_COMPUTER_NAME.to_string());
}
// …and the account's other computers (手腳): their own AnyBot looks and clicks there.
screens.extend(devices.iter().cloned());
if cfg!(target_os = "macos") {
tool_specs.extend(crate::macapps::specs().into_iter().filter(|t| self.state.messages_send || t["name"] != crate::macapps::SEND_MESSAGE));
}

View File

@ -720,6 +720,10 @@ pub fn why_unreachable(e: &ureq::Error) -> String {
use std::io::ErrorKind as K;
use ureq::Error as E;
let text = e.to_string().to_lowercase();
// A TLS failure often comes wrapped as an I/O error: look at the words first.
if text.contains("certificate") || text.contains("tls") || text.contains("handshake") {
return "和伺服器的安全連線失敗:對方的憑證不對(網址可能連到了別的服務,或憑證過期)".to_string();
}
let w = match e {
E::HostNotFound => "找不到伺服器:這台電腦可能沒連上網路,或伺服器網址打錯了",
E::ConnectionFailed => "伺服器沒有回應:可能沒開機,或網路把連線擋住了",
@ -731,7 +735,6 @@ pub fn why_unreachable(e: &ureq::Error) -> String {
K::NetworkUnreachable | K::HostUnreachable | K::NetworkDown | K::AddrNotAvailable => "這台電腦沒有連上網路",
_ => "網路連線出了問題",
},
_ if text.contains("tls") || text.contains("certificate") => "和伺服器的安全連線失敗(伺服器的憑證有問題)",
_ if text.contains("dns") || text.contains("resolve") => "找不到伺服器:這台電腦可能沒連上網路,或伺服器網址打錯了",
_ => "網路連線出了問題",
};