Replies in Traditional Chinese when that is the setting; a cloud brain's browser is the person's
- 語言 繁體中文: a reply's Simplified characters are turned into Traditional with Taiwan wording (zhconv), since some models (Qwen) slip despite the instruction. - A brain on a server: browser steps go to the person's computer (its hands) unless the server's own browser is named; 你的電腦 is listed first. - update_bot says the default computer it set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
94cadca1b8
commit
4c0db53647
|
|
@ -10,6 +10,7 @@ name = "anybot"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
zhconv = "0.4"
|
||||||
eframe = "0.37"
|
eframe = "0.37"
|
||||||
ureq = { version = "3", features = ["json"] }
|
ureq = { version = "3", features = ["json"] }
|
||||||
# 長連線 to the account server (WebSocket over the same TLS ureq uses), and compressing what goes over it.
|
# 長連線 to the account server (WebSocket over the same TLS ureq uses), and compressing what goes over it.
|
||||||
|
|
@ -48,7 +49,6 @@ agent-desktop-core = { git = "https://github.com/vercel-labs/agent-desktop", tag
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||||
zhconv = "0.4"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
agent-desktop-linux = { git = "https://github.com/vercel-labs/agent-desktop", tag = "v1.1.0" }
|
agent-desktop-linux = { git = "https://github.com/vercel-labs/agent-desktop", tag = "v1.1.0" }
|
||||||
|
|
|
||||||
12
src/agent.rs
12
src/agent.rs
|
|
@ -114,6 +114,11 @@ fn target_for(job: &Job, args: &str) -> Result<tools::Target, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Does the call name a computer?
|
||||||
|
fn names_a_computer(args: &str) -> bool {
|
||||||
|
tools::args_of(args).get("computer").and_then(|c| c.as_str()).map(|c| !c.trim().is_empty()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// The other computer (裝置連結) a call names, if it names one.
|
/// The other computer (裝置連結) a call names, if it names one.
|
||||||
fn device_named(job: &Job, args: &str) -> Option<String> {
|
fn device_named(job: &Job, args: &str) -> Option<String> {
|
||||||
let v = tools::args_of(args);
|
let v = tools::args_of(args);
|
||||||
|
|
@ -285,7 +290,7 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
||||||
}
|
}
|
||||||
let msg = Message {
|
let msg = Message {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
text: out.text,
|
text: crate::i18n::reply_text(&out.text),
|
||||||
thinking: out.thinking,
|
thinking: out.thinking,
|
||||||
via: job.via.clone(),
|
via: job.via.clone(),
|
||||||
tool_calls: out.tool_calls,
|
tool_calls: out.tool_calls,
|
||||||
|
|
@ -625,11 +630,16 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
||||||
}
|
}
|
||||||
// The browser tools use the person's own browser, so always this Mac.
|
// The browser tools use the person's own browser, so always this Mac.
|
||||||
// …and so does screen control: it's the person's own screen.
|
// …and so does screen control: it's the person's own screen.
|
||||||
|
let person_device = job.computers.iter().find(|t| t.name == crate::model::THIS_COMPUTER_NAME && matches!(t.place, tools::Place::Device { .. })).cloned();
|
||||||
let target = if is_screen {
|
let target = if is_screen {
|
||||||
screen_target(&job, &call.args)
|
screen_target(&job, &call.args)
|
||||||
} else if is_web && device_named(&job, &call.args).is_some() {
|
} else if is_web && device_named(&job, &call.args).is_some() {
|
||||||
// The browser on another computer of the account, as named.
|
// The browser on another computer of the account, as named.
|
||||||
target_for(&job, &call.args)
|
target_for(&job, &call.args)
|
||||||
|
} else if is_web && call.name.starts_with("browser_") && person_device.is_some() && !names_a_computer(&call.args) {
|
||||||
|
// A brain on a server: the browser is the person's computer's (the one they
|
||||||
|
// can see), unless the server's own is named.
|
||||||
|
Ok(person_device.clone().unwrap())
|
||||||
} else if is_web && call.name.starts_with("browser_") && !crate::botbrowser::available() && job.computers.iter().any(|t| matches!(t.place, tools::Place::Device { .. })) {
|
} else if is_web && call.name.starts_with("browser_") && !crate::botbrowser::available() && job.computers.iter().any(|t| matches!(t.place, tools::Place::Device { .. })) {
|
||||||
// No browser here (a brain on a server): the 手腳 computer's.
|
// No browser here (a brain on a server): the 手腳 computer's.
|
||||||
job.computers.iter().find(|t| matches!(t.place, tools::Place::Device { .. })).cloned().ok_or_else(|| "找不到這台電腦".to_string())
|
job.computers.iter().find(|t| matches!(t.place, tools::Place::Device { .. })).cloned().ok_or_else(|| "找不到這台電腦".to_string())
|
||||||
|
|
|
||||||
|
|
@ -1251,7 +1251,7 @@ impl App {
|
||||||
other => {
|
other => {
|
||||||
let Some(m) = b.messages.last_mut() else { continue };
|
let Some(m) = b.messages.last_mut() else { continue };
|
||||||
match other {
|
match other {
|
||||||
StreamEvent::Text(t) => m.text.push_str(&t),
|
StreamEvent::Text(t) => m.text.push_str(&crate::i18n::reply_text(&t)),
|
||||||
StreamEvent::Thinking(t) => m.thinking.push_str(&t),
|
StreamEvent::Thinking(t) => m.thinking.push_str(&t),
|
||||||
StreamEvent::ToolCall(c) => m.tool_calls.push(c),
|
StreamEvent::ToolCall(c) => m.tool_calls.push(c),
|
||||||
StreamEvent::StepDone(full) => {
|
StreamEvent::StepDone(full) => {
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,7 @@ pub fn apply(state: &mut State, bot_id: u64, name: &str, args: &str) -> Outcome
|
||||||
if changed.is_empty() {
|
if changed.is_empty() {
|
||||||
return err("沒有要改的項目".into());
|
return err("沒有要改的項目".into());
|
||||||
}
|
}
|
||||||
ok(format!("已更新「{}」的{}(現在:{} · {})", b.name, changed.join("、"), b.provider.label(), b.model))
|
ok(format!("已更新「{}」的{}(現在:{} · {},預設電腦「{}」)", b.name, changed.join("、"), b.provider.label(), b.model, b.default_computer))
|
||||||
}
|
}
|
||||||
"message_bot" => {
|
"message_bot" => {
|
||||||
let Some(n) = s("bot") else { return err("要指定 Bot".into()) };
|
let Some(n) = s("bot") else { return err("要指定 Bot".into()) };
|
||||||
|
|
|
||||||
16
src/i18n.rs
16
src/i18n.rs
|
|
@ -55,8 +55,22 @@ fn current() -> &'static RwLock<String> {
|
||||||
L.get_or_init(|| RwLock::new("zh-TW".into()))
|
L.get_or_init(|| RwLock::new("zh-TW".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replies are wanted in Traditional Chinese (the language setting says so, not "auto").
|
||||||
|
static REPLIES_HANT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// What the bot wrote, in Traditional Chinese with Taiwan wording when that is the setting.
|
||||||
|
/// Models (Qwen especially) slip into Simplified characters despite the instruction.
|
||||||
|
pub fn reply_text(s: &str) -> String {
|
||||||
|
if REPLIES_HANT.load(std::sync::atomic::Ordering::Relaxed) && !s.is_empty() {
|
||||||
|
zhconv::zhconv(s, zhconv::Variant::ZhTW)
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// "auto" follows the system language.
|
/// "auto" follows the system language.
|
||||||
pub fn set(tag: &str) {
|
pub fn set(tag: &str) {
|
||||||
|
REPLIES_HANT.store(tag == "zh-TW", std::sync::atomic::Ordering::Relaxed);
|
||||||
let t = if tag == "auto" || tag.is_empty() { system() } else { tag.to_string() };
|
let t = if tag == "auto" || tag.is_empty() { system() } else { tag.to_string() };
|
||||||
*current().write().unwrap() = t;
|
*current().write().unwrap() = t;
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +222,8 @@ mod tests {
|
||||||
fn languages_and_tables() {
|
fn languages_and_tables() {
|
||||||
assert_eq!(LANGS.len(), 37);
|
assert_eq!(LANGS.len(), 37);
|
||||||
assert_eq!(match_tag("zh-Hant-TW"), "zh-TW");
|
assert_eq!(match_tag("zh-Hant-TW"), "zh-TW");
|
||||||
|
set("zh-TW");
|
||||||
|
assert_eq!(reply_text("那边的软件"), "那邊的軟體");
|
||||||
assert_eq!(match_tag("zh-Hans-CN"), "zh-CN");
|
assert_eq!(match_tag("zh-Hans-CN"), "zh-CN");
|
||||||
assert_eq!(match_tag("ja-JP"), "ja");
|
assert_eq!(match_tag("ja-JP"), "ja");
|
||||||
assert_eq!(match_tag("pt-PT"), "pt-PT");
|
assert_eq!(match_tag("pt-PT"), "pt-PT");
|
||||||
|
|
|
||||||
12
src/web.rs
12
src/web.rs
|
|
@ -66,12 +66,20 @@ pub fn specs_for(here: &str, devices: &[String]) -> Vec<Value> {
|
||||||
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();
|
||||||
|
// A brain on a server with the person's computer as its hands: that one's browser first.
|
||||||
|
let person = devices.iter().any(|d| d == crate::model::THIS_COMPUTER_NAME);
|
||||||
if local {
|
if local {
|
||||||
names.insert(0, here.to_string());
|
names.insert(if person { names.len() } else { 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 {here}.") } else { "This computer has no browser: always name one.".to_string() }
|
if person {
|
||||||
|
format!("Leave out for {} (the person's own computer, where they can see the window).", crate::model::THIS_COMPUTER_NAME)
|
||||||
|
} else 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