From 9e8135cedad8c02a9edc59ad3808d5d1a0943b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Mon, 9 Nov 2026 10:19:18 +0800 Subject: [PATCH] Typing into a one-line field replaces it, a named control beats stray coordinates, and a box left by a hard stop starts on the second try MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Typed into the middle of Chromium's 「about:blank」, an address came out as chrome://blankchrome//settings/search, and every step after it went wrong. Typing into a field by name or number now selects a one-line field's text first (ctrl+a / cmd+a) so the text replaces it; append=true keeps it. Text areas are left alone. - A model sending name plus x: 0, y: 0 had its click land in the screen's corner: a name (or number) now always wins over coordinates. - After the VM went down hard, the box's first start failed in gVisor ("mount source already has a filestore file … repeated submounts are not supported") and the second worked: waking or restarting the box tries once more on exactly that error. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS --- src/cloudbox.rs | 33 ++++++++++++++++++++++++++++++--- src/computer.rs | 13 +++++++++---- src/marks.rs | 8 +++++++- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/cloudbox.rs b/src/cloudbox.rs index ea08cbe..deaea75 100644 --- a/src/cloudbox.rs +++ b/src/cloudbox.rs @@ -595,6 +595,21 @@ fn docker_prefix() -> String { } } +/// Start (or restart) the box, once more when gVisor trips over what an unclean stop left +/// behind: after the VM or the computer went down hard, the first start fails with "mount source +/// already has a filestore file … repeated submounts are not supported" and clears it itself, +/// and the second one works. +fn start_once_more(args: &[&str]) -> Result { + match dk(args) { + Err(e) if e.contains("filestore") || e.contains("repeated submounts") => { + log("上次沒有正常關機,留下的暫存檔擋住了開機,再試一次…"); + std::thread::sleep(Duration::from_secs(2)); + dk(args) + } + r => r, + } +} + /// Run docker; Ok(stdout) or Err(stderr). fn dk(args: &[&str]) -> Result { // Never block forever on the daemon: a hung docker daemon used to pile up a new stuck @@ -1060,7 +1075,7 @@ pub fn ensure_running() -> Result<(), String> { set_run(Run::Starting("喚醒中")); log("喚醒電腦…"); let st = dk(&["inspect", "-f", "{{.State.Status}}", container()]).unwrap_or_default(); - let woke = if st == "paused" { dk(&["unpause", container()]) } else { dk(&["start", container()]) }; + let woke = if st == "paused" { dk(&["unpause", container()]) } else { start_once_more(&["start", container()]) }; woke.map_err(|e| format!("叫不醒電腦:{e}")).and_then(|_| wait_ready(BOOT_WAIT_MS)) } Run::Starting(_) => { @@ -1072,7 +1087,7 @@ pub fn ensure_running() -> Result<(), String> { // A broken container: start it again. log(&format!("電腦狀態異常({e}),重新啟動…")); set_run(Run::Starting("連線中")); - dk(&["restart", container()]).map_err(|e| e.to_string()).and_then(|_| wait_ready(BOOT_WAIT_MS)) + start_once_more(&["restart", container()]).map_err(|e| e.to_string()).and_then(|_| wait_ready(BOOT_WAIT_MS)) } }; match &result { @@ -1752,11 +1767,17 @@ pub fn box_script(name: &str, v: &Value, scale: (f64, f64)) -> Result<(String, S return Err("請給要打的字".into()); } let mut script = String::new(); - // Into a field by its number: click it first, like a person would. + // Into a field by its number: click it first, like a person would. A one-line field + // (an address bar, a search box) gets what it holds selected first, so the text + // replaces it: typed into the middle of 「about:blank」 the address became garbage. + // ("append": true keeps what is there.) let field = match by_number(v) { Some(r) => { let ((x, y), what) = r?; script.push_str(&format!("xdotool mousemove {x} {y} sleep 0.03 click 1 sleep 0.15")); + if replaces_field(&what, v) { + script.push_str(" && xdotool key --clearmodifiers ctrl+a sleep 0.05"); + } format!("在 {what}") } None => String::new(), @@ -1801,6 +1822,12 @@ pub fn box_script(name: &str, v: &Value, scale: (f64, f64)) -> Result<(String, S /// Type text into the window in front: short plain text key by key; anything else (another /// language, or long text, which key by key could outlast the step's 30 s and then get typed /// twice when the bot tried again) pasted through the clipboard. +/// Typing into this control replaces what it holds: a one-line field (輸入框), unless the step +/// asks to add to it. A document's text area is left alone (selecting all would replace a file). +pub fn replaces_field(what: &str, v: &Value) -> bool { + what.starts_with("輸入框") && !v["append"].as_bool().unwrap_or(false) +} + fn type_script(text: &str) -> String { let q = crate::tools::sh_quote(text); if text.is_ascii() && text.chars().count() <= TYPE_KEYS_MAX_CHARS { diff --git a/src/computer.rs b/src/computer.rs index d800941..2e52f33 100644 --- a/src/computer.rs +++ b/src/computer.rs @@ -295,8 +295,8 @@ pub fn specs_for(computers: &[String], default: &str, chosen: bool) -> Vec Result { if text.is_empty() && !submit { return Err("請給要打的字".into()); } - // Into a field by its number: click it first, like a person would. + // Into a field by its number: click it first, like a person would; a one-line field + // gets what it holds replaced (cloudbox::replaces_field). let at = place_of(&json!({"ref": v["ref"]}), scale)?; - if let Some((x, y, _)) = &at { + if let Some((x, y, what)) = &at { mac::click(*x, *y, "left", 1, 0); std::thread::sleep(std::time::Duration::from_millis(150)); + if crate::cloudbox::replaces_field(what.split_once('(').map(|(_, w)| w).unwrap_or(what), v) { + press_keys("cmd+a", KEY_HOLD_SECS)?; + std::thread::sleep(std::time::Duration::from_millis(50)); + } } if !text.is_empty() { mac::type_text(text); diff --git a/src/marks.rs b/src/marks.rs index f31443a..49b07cb 100644 --- a/src/marks.rs +++ b/src/marks.rs @@ -72,7 +72,9 @@ pub fn names_to_numbers(v: &mut serde_json::Value, spots: Option<&Spots>) -> Res // ("name" in the screen tools; "text" too, the word the browser tools use.) let named = ["name", "text"].iter().find_map(|k| p[*k].as_str().map(str::trim).filter(|t| !t.is_empty()).map(String::from)); if let Some(t) = named { - if p["ref"].is_null() && p["x"].is_null() { + // A name wins over coordinates sent along with it (x: 0, y: 0 from a model filling + // every field): the click went to the screen's corner. + if p["ref"].is_null() || p["ref"].as_str().is_some_and(|r| r.trim().is_empty()) { let s = spots.ok_or("先用 computer_use_screenshot 看一下畫面,再照清單上的名字或編號操作。")?; p["ref"] = serde_json::Value::String(s.named(&t)?.to_string()); } @@ -348,6 +350,10 @@ mod tests { let mut one = serde_json::json!({"name": "自動更新"}); names_to_numbers(&mut one, Some(&spots)).unwrap(); assert_eq!(one["ref"], "4"); + // A name with x: 0, y: 0 beside it: the name. + let mut both = serde_json::json!({"name": "AC", "x": 0.0, "y": 0.0}); + names_to_numbers(&mut both, Some(&spots)).unwrap(); + assert_eq!(both["ref"], "2"); } #[test]