A server brain without hands calls its server the brain server, and tells the model which computers really exist

This commit is contained in:
王性驊 2026-11-05 16:03:34 +08:00
parent dabb4113c2
commit 591626408f
4 changed files with 69 additions and 7 deletions

View File

@ -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<String> = 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"),

View File

@ -145,6 +145,10 @@ pub fn needs_approval(name: &str, args: &str, state: &State) -> Option<String> {
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"))

View File

@ -224,9 +224,24 @@ pub fn specs_for(computers: &[String], default: &str) -> Vec<Value> {
// (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

View File

@ -629,10 +629,18 @@ pub fn specs_for(computers: &[String], default: &str) -> Vec<Value> {
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