From 591626408f68f84f455c3e9b00bb81e4938f05d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Thu, 5 Nov 2026 16:03:34 +0800 Subject: [PATCH] A server brain without hands calls its server the brain server, and tells the model which computers really exist --- src/app.rs | 32 ++++++++++++++++++++++++++++++-- src/appctl.rs | 15 +++++++++++++-- src/computer.rs | 19 +++++++++++++++++-- src/tools.rs | 10 +++++++++- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2391d59..a1f0c33 100644 --- a/src/app.rs +++ b/src/app.rs @@ -817,6 +817,15 @@ impl App { } v[i].name = crate::model::THIS_COMPUTER_NAME.into(); } + } else if self.headless { + // A server brain with no hands connected: the machine it runs on is a server, + // not anyone's computer. Wearing the name 你的電腦 here made bots "work on your + // computer" on the server while the person watched their own untouched desktop. + for t in v.iter_mut() { + if matches!(t.place, tools::Place::Local { .. }) { + t.name = crate::model::BRAIN_HOST_NAME.into(); + } + } } } v @@ -827,7 +836,7 @@ impl App { self.targets_for(b) .iter() .map(|t| match &t.place { - tools::Place::Sealed { .. } | tools::Place::Box { .. } => format!("{}:你自己的電腦,一律可用", t.name), + tools::Place::Sealed { .. } | tools::Place::Box { .. } => format!("{}:這個 Bot 自己的 Linux 桌面(AI 的電腦,不是使用者的電腦),一律可用", t.name), tools::Place::Local { .. } if t.name == crate::model::BRAIN_HOST_NAME => format!("{}:AnyBot 大腦所在的伺服器「{}」(不是使用者的電腦)", t.name, self.state.remote.host_name), tools::Place::Local { .. } => format!( "{}:{}「{}」,{}", @@ -3278,6 +3287,7 @@ impl App { } pub fn serve(ctx: &egui::Context) -> ! { + crate::appctl::SERVING.store(true, std::sync::atomic::Ordering::Relaxed); let mut app = App::new(ctx, None); app.headless = true; if let Ok(pw) = std::env::var("ANYBOT_PHONE_PASSWORD") { @@ -3679,6 +3689,24 @@ fn system_prompt(b: &Bot, st: &State, computers: &[tools::Target], default_compu s.push_str("你是使用者的主要 Bot:主要聯絡窗口,可以主動幫忙,也能用 message_bot 把工作交給其他 Bot。"); } let now = crate::routines::now_in(&st.timezone); + // Which screens can really be operated, named as the model must name them: the fixed + // wording promised "你的電腦(使用者的 Mac)" on server brains that have no such computer, + // and bots then "worked on your computer" on the server itself. + let mut screens: Vec = Vec::new(); + for t in computers { + match &t.place { + tools::Place::Box { .. } => screens.push(format!("{}(AI 自己的 Linux 桌面,不是使用者的電腦)", t.name)), + tools::Place::Local { .. } => { + if crate::computer::screen_supported() { + screens.push(format!("{}(這台電腦自己的畫面)", t.name)); + } + } + tools::Place::Device { host } if t.name == crate::model::THIS_COMPUTER_NAME => screens.push(format!("{}(使用者自己的電腦「{}」的畫面)", t.name, crate::remote::peer_name(host))), + tools::Place::Device { .. } => screens.push(format!("{}(帳號裡另一台電腦的畫面)", t.name)), + tools::Place::Ssh { .. } | tools::Place::Sealed { .. } => {} + } + } + let screens = if screens.is_empty() { "目前沒有任何可操作的螢幕".to_string() } else { screens.join(";") }; s.push_str(&format!( "\n\n現在是 {}({},時區 {})。\n\ 你在 AnyBot 這個 App 裡,可以直接操作 App:\n\ @@ -3694,7 +3722,7 @@ fn system_prompt(b: &Bot, st: &State, computers: &[tools::Target], default_compu - 絕對不要直接呼叫網站的內部 API、下載網站的程式碼來研究,或用程式大量抓資料:網站會把帳號當成機器人(例如跳出拼圖驗證、封鎖帳號)。出現驗證碼、拼圖或簡訊驗證時,停下來請使用者在 AnyBot 瀏覽器視窗完成。 - 如果讀到的是登入頁,請使用者在跳出來的 AnyBot 瀏覽器視窗登入一次(之後會記住),登入好再讀。絕對不要叫使用者去改瀏覽器的設定或選單(例如「允許來自 Apple 事件的 JavaScript」),那不需要。也不要說要從 Chrome 複製登入或 Cookie,沒有這個功能。\n\ - AnyBot 瀏覽器有設定檔(profile),每個有自己的登入。使用者說要換 profile/設定檔/另一個帳號時,用 browser_profiles:list 看有哪些,use 切換,add 新增(新的要登入一次)。也可以在 browser_open / browser_read 帶 profile 參數。使用者想用自己平常的 Chrome(自己的 Chrome 設定檔、已經登入的帳號)時,用 profile「你的瀏覽器」:請使用者在 Chrome 切到想要的設定檔,再 browser_read。\n\ - - 你也能像人一樣操作螢幕:你自己的電腦(Bot 的電腦,Linux 桌面)或使用者的 Mac(你的電腦,用在使用者自己的 App)。computer_use_screenshot 看畫面,再用 computer_use_click / computer_use_typing / computer_use_press_key / computer_use_scroll / computer_use_drag 操作;座標用最新截圖上的像素。每做一步就再截圖確認。用在檔案和網頁工具做不到的事(例如操作其他 App、在 Chrome 切換設定檔)。\n\ + - 你也能像人一樣操作螢幕({screens})。computer_use_screenshot 看畫面,再用 computer_use_click / computer_use_typing / computer_use_press_key / computer_use_scroll / computer_use_drag 操作;座標用最新截圖上的像素。每做一步就再截圖確認。用在檔案和網頁工具做不到的事(例如操作其他 App、在 Chrome 切換設定檔)。\n\ - 使用者 Mac 上的「提醒事項、行事曆、聯絡人、備忘錄、郵件、訊息」有專門的工具(ios_read_reminders、mobile_read_calendar、mobile_read_contacts、mac_read_notes、mac_read_mail 等),比操作畫面快又準;寄信或傳訊息每次都會先請使用者確認,起草郵件(mac_draft_mail)則讓使用者自己按寄出。\n\ - 不要說「我沒有瀏覽器或工具」:先試這些工具。需要權限時 App 會自己跳出視窗問使用者;工具回報要開什麼設定時,照實轉告。", now.format("%Y-%m-%d %H:%M"), diff --git a/src/appctl.rs b/src/appctl.rs index 5d5a1d8..aa0fa46 100644 --- a/src/appctl.rs +++ b/src/appctl.rs @@ -145,6 +145,10 @@ pub fn needs_approval(name: &str, args: &str, state: &State) -> Option { None } +/// True while running as a headless brain (`anybot --serve`): "this computer" is a server, +/// not a person's desktop, and the computer-wording tools must say so. Set in App::serve. +pub static SERVING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + pub struct Outcome { pub text: String, pub ok: bool, @@ -543,9 +547,16 @@ pub fn apply(state: &mut State, bot_id: u64, name: &str, args: &str) -> Outcome Outcome { text: format!("已交給「{name}」,它的回覆會出現在它自己的聊天裡。"), ok: true, wake: vec![(target, msg)], theme_changed: false, page: None } } "list_computers" => { + // On a server brain (serve), Local is the server, not the person's computer — + // matching what the app's own list_computers says after its rename. + let serving = SERVING.load(std::sync::atomic::Ordering::Relaxed); let mut lines = vec![ - format!("{}:你自己的電腦(獨立的資料夾),一律可用", model::BOT_COMPUTER_NAME), - format!("{}:{},{}", model::THIS_COMPUTER_NAME, if cfg!(target_os = "macos") { "這台 Mac" } else { "這台電腦" }, if state.local_execution { "已允許" } else { "使用者還沒在設定裡允許(設定 → 電腦 → 在這台電腦上執行)" }), + format!("{}:{}", model::BOT_COMPUTER_NAME, if serving { "這個 Bot 自己的 Linux 電腦(AI 的電腦,不是使用者的電腦,獨立資料夾),一律可用" } else { "你自己的電腦(獨立的資料夾),一律可用" }), + if serving { + format!("{}:AnyBot 大腦所在的伺服器(不是使用者的電腦)。使用者的電腦要在它上面裝 AnyBot 並登入同一帳號,才會以「{}」出現", model::BRAIN_HOST_NAME, model::THIS_COMPUTER_NAME) + } else { + format!("{}:{},{}", model::THIS_COMPUTER_NAME, if cfg!(target_os = "macos") { "這台 Mac" } else { "這台電腦" }, if state.local_execution { "已允許" } else { "使用者還沒在設定裡允許(設定 → 電腦 → 在這台電腦上執行)" }) + }, ]; lines.extend(state.remote_computers.iter().map(|c| format!("{}:透過 SSH 連到 {}", c.name, c.ssh_target))); ok(lines.join("\n")) diff --git a/src/computer.rs b/src/computer.rs index 8dbbd35..918a1df 100644 --- a/src/computer.rs +++ b/src/computer.rs @@ -224,9 +224,24 @@ pub fn specs_for(computers: &[String], default: &str) -> Vec { // (a 手腳 computer): the bot should say whose screen it means. let own = [crate::model::BOT_COMPUTER_NAME, crate::model::THIS_COMPUTER_NAME]; if computers.len() > 1 || computers.iter().any(|c| !own.contains(&c.as_str())) { + // Explain each name that is actually offered (the old text promised "the person's Mac" + // on machines where it does not exist, and the bot grabbed the next best thing). + let mut parts = Vec::new(); + if computers.iter().any(|c| c == crate::model::BOT_COMPUTER_NAME) { + parts.push(format!("{} is a private Linux desktop for the bot (the AI's own computer, not the person's).", crate::model::BOT_COMPUTER_NAME)); + } + if computers.iter().any(|c| c == crate::model::THIS_COMPUTER_NAME) { + parts.push(format!("{} is the person's own computer (use it for the person's own apps).", crate::model::THIS_COMPUTER_NAME)); + } + if computers.iter().any(|c| c == crate::model::BRAIN_HOST_NAME) { + parts.push(format!("{} is the server the brain runs on — not a person's screen; a person asking about \"my screen\" never means it.", crate::model::BRAIN_HOST_NAME)); + } + if computers.iter().any(|c| c != crate::model::BOT_COMPUTER_NAME && c != crate::model::THIS_COMPUTER_NAME && c != crate::model::BRAIN_HOST_NAME) { + parts.push("Any other name is another computer of the account.".to_string()); + } + let desc = format!("Whose screen. Default: {default}. {}", parts.join(" ")); for t in v.iter_mut() { - t["parameters"]["properties"]["computer"] = json!({"type": "string", "enum": computers, - "description": format!("Whose screen. Default: {default}. {} is the bot's own Linux desktop; {} is the person's Mac (use it for the person's own apps).", crate::model::BOT_COMPUTER_NAME, crate::model::THIS_COMPUTER_NAME)}); + t["parameters"]["properties"]["computer"] = json!({"type": "string", "enum": computers, "description": desc}); } } v diff --git a/src/tools.rs b/src/tools.rs index f5b6cb6..041fc69 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -629,10 +629,18 @@ pub fn specs_for(computers: &[String], default: &str) -> Vec { if t["name"] == "read_terminal" || t["name"] == "write_terminal" { continue; } + // None of these being the person's own computer (a server brain with no hands): + // a bot that "helped on your computer" then worked on the server, while the person + // watched their own desktop stay untouched. Say what is true instead. + let desc = if computers.iter().any(|c| c == crate::model::THIS_COMPUTER_NAME) { + format!("Which computer to work on. Default: {default}. Use the one the person names (\"在我的 Mac 上\" = {}).", crate::model::THIS_COMPUTER_NAME) + } else { + format!("Which computer to work on. Default: {default}. None of these is the person's own computer (it is not connected): use the default, and when the person means their own computer, tell them it is not connected (App › 設定 › 電腦) instead of working on another machine and calling it theirs.") + }; t["parameters"]["properties"]["computer"] = json!({ "type": "string", "enum": computers, - "description": format!("Which computer to work on. Default: {default}. Use the one the person names (\"在我的 Mac 上\" = {}).", crate::model::THIS_COMPUTER_NAME) + "description": desc }); } v