A brain on a server knows which computer is the person's; the phone page opens the brain in use
- On a server brain whose hands are the person's computer, 你的電腦 is that computer; the server itself is 大腦伺服器 (its browser too), so 「我的 Mac」 means the Mac. - A brain with its own hands no longer lists the account's other brains as computers. - 查看電腦 lists the computers the bot really has, with their real names. - Phone page: with several brains on, it opens the one the account's computers use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8f79c33b35
commit
94cadca1b8
|
|
@ -388,6 +388,9 @@ async function pickHost() {
|
||||||
const hosts = all.filter((h) => !(h.uses && byId[h.uses]));
|
const hosts = all.filter((h) => !(h.uses && byId[h.uses]));
|
||||||
const sharers = (id) => all.filter((h) => h.uses === id).map((h) => h.name);
|
const sharers = (id) => all.filter((h) => h.uses === id).map((h) => h.name);
|
||||||
const live = hosts.filter((h) => h.online);
|
const live = hosts.filter((h) => h.online);
|
||||||
|
// Several brains on: the one this account's computers use is the one the chats are on.
|
||||||
|
const used = live.filter((h) => sharers(h.id).length);
|
||||||
|
if (used.length === 1 && !(S.host && sharers(target(S.host)).length)) S.host = used[0].id;
|
||||||
if (S.host) {
|
if (S.host) {
|
||||||
const t = target(S.host);
|
const t = target(S.host);
|
||||||
if (byId[t] && byId[t].online) { S.host = t; localStorage.setItem("anybot.host", t); return start(); }
|
if (byId[t] && byId[t].online) { S.host = t; localStorage.setItem("anybot.host", t); return start(); }
|
||||||
|
|
|
||||||
62
src/app.rs
62
src/app.rs
|
|
@ -690,14 +690,15 @@ impl App {
|
||||||
let mut tool_specs = appctl::specs();
|
let mut tool_specs = appctl::specs();
|
||||||
tool_specs.extend(tools::specs_for(&names, &default_computer));
|
tool_specs.extend(tools::specs_for(&names, &default_computer));
|
||||||
let devices: Vec<String> = computers.iter().filter(|t| matches!(t.place, tools::Place::Device { .. })).map(|t| t.name.clone()).collect();
|
let devices: Vec<String> = computers.iter().filter(|t| matches!(t.place, tools::Place::Device { .. })).map(|t| t.name.clone()).collect();
|
||||||
tool_specs.extend(web::specs_for(&devices));
|
let here = computers.iter().find(|t| matches!(t.place, tools::Place::Local { .. })).map(|t| t.name.clone()).unwrap_or_else(|| crate::model::THIS_COMPUTER_NAME.into());
|
||||||
|
tool_specs.extend(web::specs_for(&here, &devices));
|
||||||
// Screen tools: the bot's own desktop (when it has one) and this Mac.
|
// Screen tools: the bot's own desktop (when it has one) and this Mac.
|
||||||
let mut screens = vec![];
|
let mut screens = vec![];
|
||||||
if crate::cloudbox::available() {
|
if crate::cloudbox::available() {
|
||||||
screens.push(crate::model::BOT_COMPUTER_NAME.to_string());
|
screens.push(crate::model::BOT_COMPUTER_NAME.to_string());
|
||||||
}
|
}
|
||||||
if crate::computer::screen_supported() {
|
if crate::computer::screen_supported() {
|
||||||
screens.push(crate::model::THIS_COMPUTER_NAME.to_string());
|
screens.extend(computers.iter().filter(|t| matches!(t.place, tools::Place::Local { .. })).map(|t| t.name.clone()));
|
||||||
}
|
}
|
||||||
// …and the account's other computers (手腳): their own AnyBot looks and clicks there.
|
// …and the account's other computers (手腳): their own AnyBot looks and clicks there.
|
||||||
screens.extend(devices.iter().cloned());
|
screens.extend(devices.iter().cloned());
|
||||||
|
|
@ -804,9 +805,51 @@ impl App {
|
||||||
v.extend(self.state.remote_computers.iter().map(|c| tools::Target { name: c.name.clone(), place: tools::Place::Ssh { target: c.ssh_target.clone(), dir: c.dir.clone() } }));
|
v.extend(self.state.remote_computers.iter().map(|c| tools::Target { name: c.name.clone(), place: tools::Place::Ssh { target: c.ssh_target.clone(), dir: c.dir.clone() } }));
|
||||||
// 裝置連結: other AnyBots of this account that allow it.
|
// 裝置連結: other AnyBots of this account that allow it.
|
||||||
v.extend(self.linked_devices().into_iter().filter(|d| !v.iter().any(|t| t.name == d.name)).collect::<Vec<_>>());
|
v.extend(self.linked_devices().into_iter().filter(|d| !v.iter().any(|t| t.name == d.name)).collect::<Vec<_>>());
|
||||||
|
// A brain on a server whose hands are the person's computer: "你的電腦" is that computer
|
||||||
|
// (what the person means by "my Mac"), and the server itself goes by its own name.
|
||||||
|
if !cfg!(target_os = "macos") {
|
||||||
|
if let Some(hand) = crate::remote::my_hands(&self.state.remote.host_id).into_iter().next() {
|
||||||
|
if let Some(i) = v.iter().position(|t| matches!(&t.place, tools::Place::Device { host } if *host == hand)) {
|
||||||
|
for t in v.iter_mut() {
|
||||||
|
if matches!(t.place, tools::Place::Local { .. }) {
|
||||||
|
t.name = crate::model::BRAIN_HOST_NAME.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v[i].name = crate::model::THIS_COMPUTER_NAME.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 查看電腦: the computers this bot really has, in plain words.
|
||||||
|
fn computers_text(&self, b: &Bot) -> String {
|
||||||
|
self.targets_for(b)
|
||||||
|
.iter()
|
||||||
|
.map(|t| match &t.place {
|
||||||
|
tools::Place::Sealed { .. } | tools::Place::Box { .. } => format!("{}:你自己的電腦,一律可用", 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!(
|
||||||
|
"{}:{}「{}」,{}",
|
||||||
|
t.name,
|
||||||
|
if cfg!(target_os = "macos") { "使用者的這台 Mac" } else { "使用者的這台電腦" },
|
||||||
|
self.state.machine_label,
|
||||||
|
if self.state.local_execution { "已允許" } else { "還沒預先允許(直接用工具,App 會問使用者)" }
|
||||||
|
),
|
||||||
|
tools::Place::Ssh { target, .. } => format!("{}:透過 SSH 連到 {target}", t.name),
|
||||||
|
tools::Place::Device { host } => {
|
||||||
|
let real = crate::remote::peer_name(host);
|
||||||
|
if t.name == crate::model::THIS_COMPUTER_NAME {
|
||||||
|
format!("{}:使用者的電腦「{real}」(大腦透過它的 AnyBot 看畫面、點擊、開瀏覽器和跑指令)", t.name)
|
||||||
|
} else {
|
||||||
|
format!("{}:帳號裡的另一台電腦(裝置連結)", t.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
/// Record how a turn ended (success / cancelled / error) and clear its live state.
|
/// Record how a turn ended (success / cancelled / error) and clear its live state.
|
||||||
fn settle(&mut self, bot_id: u64, outcome: TurnOutcome) {
|
fn settle(&mut self, bot_id: u64, outcome: TurnOutcome) {
|
||||||
// 群組聊天: the next member answers after this one finishes well.
|
// 群組聊天: the next member answers after this one finishes well.
|
||||||
|
|
@ -913,7 +956,12 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app_action(&mut self, ctx: &egui::Context, bot_id: u64, call: AppCall) {
|
fn run_app_action(&mut self, ctx: &egui::Context, bot_id: u64, call: AppCall) {
|
||||||
let out = appctl::apply(&mut self.state, bot_id, &call.name, &call.args);
|
let out = if call.name == "list_computers" {
|
||||||
|
let text = self.state.bot(bot_id).cloned().map(|b| self.computers_text(&b)).unwrap_or_default();
|
||||||
|
appctl::Outcome { text, ok: true, wake: vec![], theme_changed: false, page: None }
|
||||||
|
} else {
|
||||||
|
appctl::apply(&mut self.state, bot_id, &call.name, &call.args)
|
||||||
|
};
|
||||||
// write_page: remember which page the call made, so its card shows under the reply.
|
// write_page: remember which page the call made, so its card shows under the reply.
|
||||||
if let Some(pid) = out.page {
|
if let Some(pid) = out.page {
|
||||||
if let Some(c) = self.state.bot_mut(bot_id).and_then(|b| b.messages.iter_mut().rev().take(12).flat_map(|m| m.tool_calls.iter_mut()).find(|c| c.id == call.call_id)) {
|
if let Some(c) = self.state.bot_mut(bot_id).and_then(|b| b.messages.iter_mut().rev().take(12).flat_map(|m| m.tool_calls.iter_mut()).find(|c| c.id == call.call_id)) {
|
||||||
|
|
@ -3668,11 +3716,17 @@ fn system_prompt(b: &Bot, st: &State, computers: &[tools::Target], default_compu
|
||||||
tools::Place::Local { workdir } => format!(
|
tools::Place::Local { workdir } => format!(
|
||||||
"- {}:{},起始資料夾 {}{}",
|
"- {}:{},起始資料夾 {}{}",
|
||||||
t.name,
|
t.name,
|
||||||
if cfg!(target_os = "macos") { "使用者的這台 Mac" } else if crate::platform::has_desktop() { "使用者的這台 Linux 電腦(快捷鍵用 Ctrl,不是 ⌘)" } else { "AnyBot 所在的伺服器(不是使用者的 Mac;要用使用者的 Mac 請選帳號裡那台電腦的名字)" },
|
if t.name == crate::model::BRAIN_HOST_NAME { "你(大腦)所在的伺服器,不是使用者的電腦;只有使用者明確要在伺服器上做事才用" } else if cfg!(target_os = "macos") { "使用者的這台 Mac" } else if crate::platform::has_desktop() { "使用者的這台 Linux 電腦(快捷鍵用 Ctrl,不是 ⌘)" } else { "AnyBot 所在的伺服器(不是使用者的 Mac;要用使用者的 Mac 請選帳號裡那台電腦的名字)" },
|
||||||
workdir.display(),
|
workdir.display(),
|
||||||
if st.local_execution { "" } else { "(還沒預先允許:直接使用工具即可,App 會跳出視窗問使用者)" }
|
if st.local_execution { "" } else { "(還沒預先允許:直接使用工具即可,App 會跳出視窗問使用者)" }
|
||||||
),
|
),
|
||||||
tools::Place::Ssh { target, .. } => format!("- {}:使用者的另一台電腦(SSH {target})", t.name),
|
tools::Place::Ssh { target, .. } => format!("- {}:使用者的另一台電腦(SSH {target})", t.name),
|
||||||
|
tools::Place::Device { host } if t.name == crate::model::THIS_COMPUTER_NAME => format!(
|
||||||
|
"- {}:使用者自己的電腦「{}」(你在伺服器上,透過它的 AnyBot 操作它:檔案、指令、瀏覽器和 computer_use_* 工具,computer=「{}」)。使用者說「我的 Mac」「我的電腦」「這台」都是指它。",
|
||||||
|
t.name,
|
||||||
|
crate::remote::peer_name(host),
|
||||||
|
t.name
|
||||||
|
),
|
||||||
tools::Place::Device { .. } => format!("- {}:使用者帳號裡的另一台電腦(裝置連結;可以用檔案、指令和 computer_use_* 工具,computer=「{}」)", t.name, t.name),
|
tools::Place::Device { .. } => format!("- {}:使用者帳號裡的另一台電腦(裝置連結;可以用檔案、指令和 computer_use_* 工具,computer=「{}」)", t.name, t.name),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
|
|
@ -720,12 +720,15 @@ impl App {
|
||||||
if self.state.remote.host_token.is_empty() {
|
if self.state.remote.host_token.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
let has_hands = !remote::my_hands(&self.state.remote.host_id).is_empty();
|
||||||
self.rm
|
self.rm
|
||||||
.peers
|
.peers
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, _, online)| !id.is_empty() && *online && *id != self.state.remote.host_id)
|
.filter(|(id, _, online)| !id.is_empty() && *online && *id != self.state.remote.host_id)
|
||||||
|
// A brain with hands of its own works on those, not on the account's other brains.
|
||||||
|
.filter(|(id, _, _)| !has_hands || remote::is_hands(id))
|
||||||
.map(|(id, name, _)| tools::Target { name: name.clone(), place: tools::Place::Device { host: id.clone() } })
|
.map(|(id, name, _)| tools::Target { name: name.clone(), place: tools::Place::Device { host: id.clone() } })
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,8 @@ fn is_zero(n: &u64) -> bool {
|
||||||
|
|
||||||
pub const BOT_COMPUTER_NAME: &str = "Bot 的電腦";
|
pub const BOT_COMPUTER_NAME: &str = "Bot 的電腦";
|
||||||
pub const THIS_COMPUTER_NAME: &str = "你的電腦";
|
pub const THIS_COMPUTER_NAME: &str = "你的電腦";
|
||||||
|
/// A brain on a server, when the person's own computer lends it hands (then that one is 你的電腦).
|
||||||
|
pub const BRAIN_HOST_NAME: &str = "大腦伺服器";
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
pub fn user(text: &str) -> Self {
|
pub fn user(text: &str) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -1222,12 +1222,39 @@ fn hands_ids() -> &'static Mutex<std::collections::HashSet<String>> {
|
||||||
H.get_or_init(Mutex::default)
|
H.get_or_init(Mutex::default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The account's computers as last seen: id → (name, the brain it uses, lends its hands).
|
||||||
|
fn peer_rows() -> &'static Mutex<HashMap<String, (String, String, bool)>> {
|
||||||
|
static P: OnceLock<Mutex<HashMap<String, (String, String, bool)>>> = OnceLock::new();
|
||||||
|
P.get_or_init(Mutex::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computers of the account that lend their hands to this brain (`me`), by id.
|
||||||
|
pub fn my_hands(me: &str) -> Vec<String> {
|
||||||
|
if me.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut v: Vec<String> = peer_rows().lock().unwrap().iter().filter(|(_, (_, uses, hands))| *hands && uses == me).map(|(id, _)| id.clone()).collect();
|
||||||
|
v.sort();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A computer's own name as the account server knows it.
|
||||||
|
pub fn peer_name(id: &str) -> String {
|
||||||
|
peer_rows().lock().unwrap().get(id).map(|r| r.0.clone()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn devices_online(s: &Settings) -> Vec<(String, String, bool)> {
|
pub fn devices_online(s: &Settings) -> Vec<(String, String, bool)> {
|
||||||
if s.relay_url.is_empty() {
|
if s.relay_url.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let Ok(mut r) = agent(10).get(&format!("{}/relay/host/peers", s.relay_url)).header("Authorization", &format!("Bearer {}", s.host_token)).call() else { return vec![] };
|
let Ok(mut r) = agent(10).get(&format!("{}/relay/host/peers", s.relay_url)).header("Authorization", &format!("Bearer {}", s.host_token)).call() else { return vec![] };
|
||||||
let v: Value = serde_json::from_str(&r.body_mut().read_to_string().unwrap_or_default()).unwrap_or(Value::Null);
|
let v: Value = serde_json::from_str(&r.body_mut().read_to_string().unwrap_or_default()).unwrap_or(Value::Null);
|
||||||
|
*peer_rows().lock().unwrap() = v["hosts"]
|
||||||
|
.as_array()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|h| Some((h["id"].as_str()?.to_string(), (h["name"].as_str().unwrap_or("").to_string(), h["uses"].as_str().unwrap_or("").to_string(), h["hands"].as_bool() == Some(true)))))
|
||||||
|
.collect();
|
||||||
*hands_ids().lock().unwrap() = v["hosts"].as_array().into_iter().flatten().filter(|h| h["hands"].as_bool() == Some(true) || !h["uses"].as_str().unwrap_or("").is_empty()).filter_map(|h| h["id"].as_str().map(String::from)).collect();
|
*hands_ids().lock().unwrap() = v["hosts"].as_array().into_iter().flatten().filter(|h| h["hands"].as_bool() == Some(true) || !h["uses"].as_str().unwrap_or("").is_empty()).filter_map(|h| h["id"].as_str().map(String::from)).collect();
|
||||||
v["hosts"].as_array().map(|a| a.iter().map(|h| (h["id"].as_str().unwrap_or("").to_string(), h["name"].as_str().unwrap_or("").to_string(), h["online"].as_bool().unwrap_or(false))).collect()).unwrap_or_default()
|
v["hosts"].as_array().map(|a| a.iter().map(|h| (h["id"].as_str().unwrap_or("").to_string(), h["name"].as_str().unwrap_or("").to_string(), h["online"].as_bool().unwrap_or(false))).collect()).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,17 +61,17 @@ pub fn read_needs_permission(args: &str) -> bool {
|
||||||
|
|
||||||
/// The browser tools, also when the browser is on another computer of the account (裝置連結):
|
/// The browser tools, also when the browser is on another computer of the account (裝置連結):
|
||||||
/// then each takes a `computer`, since each computer's AnyBot browser has its own logins.
|
/// then each takes a `computer`, since each computer's AnyBot browser has its own logins.
|
||||||
pub fn specs_for(devices: &[String]) -> Vec<Value> {
|
pub fn specs_for(here: &str, devices: &[String]) -> Vec<Value> {
|
||||||
let local = crate::botbrowser::available();
|
let local = crate::botbrowser::available();
|
||||||
let mut v = specs_inner(local || !devices.is_empty());
|
let mut v = specs_inner(local || !devices.is_empty());
|
||||||
if !devices.is_empty() {
|
if !devices.is_empty() {
|
||||||
let mut names: Vec<String> = devices.to_vec();
|
let mut names: Vec<String> = devices.to_vec();
|
||||||
if local {
|
if local {
|
||||||
names.insert(0, crate::model::THIS_COMPUTER_NAME.to_string());
|
names.insert(0, here.to_string());
|
||||||
}
|
}
|
||||||
let desc = format!(
|
let desc = format!(
|
||||||
"Which computer's AnyBot browser to use. Each computer's browser has its own logins (a site logged in on one isn't on another). {}",
|
"Which computer's AnyBot browser to use. Each computer's browser has its own logins (a site logged in on one isn't on another). {}",
|
||||||
if local { format!("Leave out for {}.", crate::model::THIS_COMPUTER_NAME) } else { "This computer has no browser: always name one.".to_string() }
|
if local { format!("Leave out for {here}.") } else { "This computer has no browser: always name one.".to_string() }
|
||||||
);
|
);
|
||||||
for t in v.iter_mut().filter(|t| t["name"].as_str().map(|n| n.starts_with("browser_")).unwrap_or(false)) {
|
for t in v.iter_mut().filter(|t| t["name"].as_str().map(|n| n.starts_with("browser_")).unwrap_or(false)) {
|
||||||
t["parameters"]["properties"]["computer"] = json!({"type": "string", "enum": names, "description": desc});
|
t["parameters"]["properties"]["computer"] = json!({"type": "string", "enum": names, "description": desc});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue