diff --git a/deploy/mac/gvisor-migrate.sh b/deploy/mac/gvisor-migrate.sh index 694a7b8..23b09f7 100755 --- a/deploy/mac/gvisor-migrate.sh +++ b/deploy/mac/gvisor-migrate.sh @@ -14,9 +14,9 @@ VMHOST="unix://$HOME/.lima/$VM/sock/docker.sock" command -v docker >/dev/null 2>&1 || { echo "找不到 docker 指令"; exit 1; } if [ -n "${DOCKER_HOST:-}" ]; then - echo "現在環境裡有 DOCKER_HOST=$DOCKER_HOST(要從本機 Docker 搬出,請用普通終端機跑)"; exit 1 + echo "現在環境裡有 DOCKER_HOST=${DOCKER_HOST}(要從本機 Docker 搬出,請用普通終端機跑)"; exit 1 fi -docker volume inspect "$VOL" >/dev/null 2>&1 || { echo "本機找不到資料卷 $VOL(box 還沒建立過?)"; exit 1; } +docker volume inspect "$VOL" >/dev/null 2>&1 || { echo "本機找不到資料卷 ${VOL}(box 還沒建立過?)"; exit 1; } limactl list "$VM" --format '{{.Status}}' 2>/dev/null | grep -q Running || { echo "VM $VM 沒在跑,先跑 gvisor-vm.sh"; exit 1; } # The Mac app's process is "AnyBot" and `anybot --serve` is "anybot": match either case. if pgrep -ix anybot >/dev/null 2>&1; then diff --git a/deploy/mac/gvisor-vm.sh b/deploy/mac/gvisor-vm.sh index 1e1e25e..fde3c49 100755 --- a/deploy/mac/gvisor-vm.sh +++ b/deploy/mac/gvisor-vm.sh @@ -20,7 +20,7 @@ DISK=60 # GiB if [ "${1:-}" = "--rm" ]; then limactl delete --force "$VM" 2>/dev/null || true - echo "已移除 VM $VM。" + echo "已移除 VM ${VM}。" exit 0 fi @@ -30,7 +30,7 @@ STATUS=$(limactl list "$VM" --format '{{.Status}}' 2>/dev/null || true) if [ "$STATUS" = "Running" ]; then echo "VM $VM 已在執行。" elif [ -n "$STATUS" ]; then - echo "VM $VM 是 $STATUS,啟動中…" + echo "VM $VM 是 ${STATUS},啟動中…" limactl start --tty=false "$VM" else echo "建立 VM ${VM}($CPUS CPU / ${MEM}GB / ${DISK}GB,第一次要下載映像檔,幾分鐘)…" @@ -50,15 +50,19 @@ if ! command -v runsc >/dev/null 2>&1; then sudo apt-get install -y -qq runsc fi # Register the runtime in the VM'\''s Docker daemon (merge, do not clobber other settings). -sudo python3 - < { + app.show_settings = true; + app.settings_tab = SettingsTab::Remote; + } + Ok("computers") => { + app.show_settings = true; + app.settings_tab = SettingsTab::Computers; + } + _ => {} + } } // …or the side panel, or the computer viewer, as they look with that data. if app.screenshot.is_some() && !app.demo { @@ -3716,6 +3726,18 @@ impl eframe::App for App { } } } + // ANYBOT_SHOT_SCROLL="x,y,dy@frame": from that frame on, a wheel step of dy over x,y on + // each of 100 frames (egui eases a wheel in over real time; screenshot frames are quick). + if let Ok(sc) = std::env::var("ANYBOT_SHOT_SCROLL") { + if let Some((v, at)) = sc.split_once('@') { + let n: Vec = v.split(',').filter_map(|x| x.trim().parse().ok()).collect(); + let (at, f) = (at.trim().parse::().unwrap_or(30), self.frames + 1); + if n.len() == 3 && f >= at && f < at + 100 { + raw.events.push(egui::Event::PointerMoved(egui::pos2(n[0], n[1]))); + raw.events.push(egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta: vec2(0.0, n[2]), phase: egui::TouchPhase::Move, source: egui::MouseWheelSource::Unknown, modifiers: Default::default() }); + } + } + } let Ok(spec) = std::env::var("ANYBOT_SHOT_CLICK") else { return }; for one in spec.split(';') { let (pos, right) = match one.strip_suffix(":right") { Some(p) => (p, true), None => (one, false) }; diff --git a/src/app/computers_ui.rs b/src/app/computers_ui.rs index 542b3de..d1b77d3 100644 --- a/src/app/computers_ui.rs +++ b/src/app/computers_ui.rs @@ -641,14 +641,15 @@ impl App { let rt = if self.cu.box_runtime.is_empty() { rt_now.clone() } else { self.cu.box_runtime.clone() }; let std_on = rt == "runc"; let gvis_on = rt == "runsc"; - if ui.selectable_label(std_on, t("標準")).clicked() { - self.cu.box_runtime = "runc".into(); - } + // (The row lays out right to left: 強隔離 first puts 標準 on its left.) let g = ui.add_enabled_ui(gvisor_here || gvis_on, |ui| ui.selectable_label(gvis_on, t("強隔離(gVisor)"))).inner; if g.clicked() { self.cu.box_runtime = "runsc".into(); } g.on_disabled_hover_text(t("這台 Docker 主機沒有裝 gVisor(runsc)")); + if ui.selectable_label(std_on, t("標準")).clicked() { + self.cu.box_runtime = "runc".into(); + } if !self.cu.box_runtime.is_empty() && self.cu.box_runtime != rt_now { if ui.add(primary_button("套用", p)).clicked() { cloudbox::set_box_runtime(&self.cu.box_runtime); diff --git a/src/browser.rs b/src/browser.rs index 686ae88..c11221f 100644 --- a/src/browser.rs +++ b/src/browser.rs @@ -764,7 +764,7 @@ pub fn run(name: &str, args: &Value) -> Result<(String, Option), String> let bytes = base64::engine::general_purpose::STANDARD.decode(r["data"].as_str().unwrap_or("")).map_err(|e| e.to_string())?; let (title, _, _) = snapshot(&mut tab); let safe: String = title.chars().filter(|c| !"/\\:*?\"<>|".contains(*c)).take(60).collect(); - let name = format!("{}-{}.pdf", if safe.trim().is_empty() { "網頁".into() } else { safe }, chrono::Local::now().format("%Y%m%d-%H%M%S")); + let name = format!("{}-{}.pdf", if safe.trim().is_empty() { "網頁".into() } else { safe }, chrono::Utc::now().with_timezone(&crate::routines::person_tz()).format("%Y%m%d-%H%M%S")); // Saved where the bot can open it: on Bot 的電腦 when that's the browser's computer. let path = if botbrowser::using_box() { let p = format!("{}/{name}", crate::cloudbox::BOX_DOWNLOADS); diff --git a/src/cloudbox.rs b/src/cloudbox.rs index 5bbec2c..cb32e1e 100644 --- a/src/cloudbox.rs +++ b/src/cloudbox.rs @@ -420,7 +420,8 @@ fn maybe_scheduled_update() { (c.auto_update_at.clone(), c.auto_update_done.clone()) }; let Some(at) = at else { return }; - let now = chrono::Local::now(); + // The person's clock (a brain on a server keeps UTC: 03:00 used to be 11:00 in Taipei). + let now = chrono::Utc::now().with_timezone(&crate::routines::person_tz()); let today = now.format("%Y-%m-%d").to_string(); if done == today || !in_schedule_window(&at, now.time()) || is_busy() || !update_available() { return; @@ -2240,13 +2241,6 @@ mod tests { /// (xterm has no accessibility; clicking a number there used to type into the terminal). #[test] #[ignore] - fn box_paths_are_the_bot_users() { - assert_eq!(box_path("~/bot-6/附件/IMG_0587.jpeg"), "/home/bot/bot-6/附件/IMG_0587.jpeg"); - assert_eq!(box_path(" ~ "), "/home/bot"); - assert_eq!(box_path("/tmp/a.jpg"), "/tmp/a.jpg"); - } - - #[test] fn box_numbers_only_on_what_is_seen() { ensure_running().expect("box starts"); let cancel = AtomicBool::new(false); @@ -2576,6 +2570,13 @@ mod tests { let _ = run_on("shell", r#"{"command":"cd .. && rm -rf bot-998"}"#, &t, &cancel, &ids); } + #[test] + fn box_paths_are_the_bot_users() { + assert_eq!(box_path("~/bot-6/附件/IMG_0587.jpeg"), "/home/bot/bot-6/附件/IMG_0587.jpeg"); + assert_eq!(box_path(" ~ "), "/home/bot"); + assert_eq!(box_path("/tmp/a.jpg"), "/tmp/a.jpg"); + } + #[test] fn schedule_window_is_one_hour() { let t = |s: &str| chrono::NaiveTime::parse_from_str(s, "%H:%M").unwrap(); @@ -2585,6 +2586,10 @@ mod tests { assert!(!in_schedule_window("03:00", t("02:59"))); assert!(in_schedule_window("23:30", t("00:10"))); assert!(!in_schedule_window("bad", t("00:10"))); + // On a server (UTC), 03:00 is the person's 03:00: 19:00 UTC in Taipei. + use chrono::TimeZone; + let at = chrono::Utc.with_ymd_and_hms(2026, 11, 6, 19, 0, 0).unwrap().with_timezone(&chrono_tz::Asia::Taipei); + assert!(in_schedule_window("03:00", at.time())); assert_eq!(wanted_version(), "7"); }