diff --git a/README.md b/README.md index 5f3e4c5..19af40c 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,22 @@ Positional args fill the adapter's declared args in order; `--key value` overrid by name. Adapters are authored by the bb-sites community and remain their authors' property — chrome-use just runs them. +**Auto-sync + auto-suggest.** You rarely type `site update` yourself: chrome-use +syncs the pack on first use and refreshes it weekly in the background (tune with +`AGENT_BROWSER_SITES_TTL_DAYS`, disable with `AGENT_BROWSER_SITES_NO_AUTO_UPDATE=1`). +And when you `open`/`snapshot` a page whose domain has adapters, chrome-use surfaces +them right in the output — a `💡 site adapters for ` line, plus a +`siteAdapters` field under `--json` — so an agent reaches for the structured-data +adapter instead of scraping the DOM: + +```text +$ chrome-use open https://github.com +💡 site adapters for github.com — prefer these for structured data: + github/issues, github/me, github/repo, … + e.g. chrome-use site github/issues --json +✓ GitHub +``` + ## Automated testing (`chrome-use test`) Turn the repetitive "open it, click around, check it's right" work into a diff --git a/README.zh.md b/README.zh.md index e13db94..bf5d19d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -188,6 +188,20 @@ chrome-use site bilibili/feed --json # 能用,因为走的是你的 位置参数按适配器声明的参数顺序填入;`--key value` 按名覆盖。适配器由 bb-sites 社区编写、 版权归各自作者所有 —— chrome-use 只负责运行它们。 +**自动同步 + 自动提示。** 你基本不用手动 `site update`:chrome-use 首次使用时自动拉取, +之后每周后台刷新一次(`AGENT_BROWSER_SITES_TTL_DAYS` 调周期,`AGENT_BROWSER_SITES_NO_AUTO_UPDATE=1` +关闭)。而当你 `open`/`snapshot` 一个有适配器的域名时,chrome-use 会在输出里直接把可用命令 +亮出来 —— 一行 `💡 site adapters for <域名>`,`--json` 下则是 `siteAdapters` 字段 —— 这样 +agent 会直接改用结构化适配器,而不是去扒 DOM: + +```text +$ chrome-use open https://github.com +💡 site adapters for github.com — prefer these for structured data: + github/issues, github/me, github/repo, … + e.g. chrome-use site github/issues --json +✓ GitHub +``` + ## 自动化测试(`chrome-use test`) 把反复的「打开它、点一圈、看对不对」变成**可重跑的测试套件** —— 前端的单元测试。用 YAML 写用例;步骤复用 chrome-use 自己的命令,断言编译成一次检查: diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 1c54e5d..5fcafe9 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrome-use" -version = "1.5.19" +version = "1.5.20" dependencies = [ "aes", "aes-gcm", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b725a0f..ed944f7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chrome-use" -version = "1.5.19" +version = "1.5.20" edition = "2021" description = "Fast browser automation CLI for AI agents" license = "Apache-2.0" diff --git a/cli/src/main.rs b/cli/src/main.rs index e85ef5a..da91365 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -840,6 +840,27 @@ fn main() { // `info` are CLI-side (download/filesystem); `site / [args]` falls // through to the daemon dispatch below (navigate to the adapter's domain + eval). if clean.first().map(|s| s.as_str()) == Some("site") { + // Auto-sync the adapter pack on first use and periodically (TTL, default + // 7d) so adapters stay fresh without a manual `site update`. Skipped for an + // explicit `update` (full sync below). Best-effort: offline → cached pack. + // Disable with AGENT_BROWSER_SITES_NO_AUTO_UPDATE=1. + if clean.get(1).map(|s| s.as_str()) != Some("update") && site::needs_refresh() { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + match rt.block_on(site::update()) { + Ok(n) => { + eprintln!( + "{}", + color::dim(&format!("site: synced {n} adapters (auto)")) + ) + } + Err(e) => eprintln!( + "{}", + color::dim(&format!( + "site: auto-sync skipped ({e}); using cached adapters" + )) + ), + } + } match clean.get(1).map(|s| s.as_str()) { Some("update") => { let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index f1c5255..5ca0db5 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -2478,7 +2478,10 @@ async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result Result Result Value { + let url = result + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or(fallback_url); + let host = url::Url::parse(url) + .ok() + .and_then(|u| u.host_str().map(String::from)); + if let Some(host) = host { + let adapters = crate::site::adapters_for_domain(&host); + if !adapters.is_empty() { + if let Some(obj) = result.as_object_mut() { + obj.insert( + "siteAdapters".to_string(), + json!({ "domain": host, "commands": adapters }), + ); + } + } + } + result } /// After navigation, probe the page for known anti-bot vendor fingerprints @@ -2936,6 +2966,13 @@ async fn handle_snapshot(cmd: &Value, state: &mut DaemonState) -> Result and expose almost no accessibility tree, so `snapshot` comes back // near-empty and agents get stuck looking for refs that will never exist diff --git a/cli/src/native/daemon.rs b/cli/src/native/daemon.rs index 200599f..9bb0d5a 100644 --- a/cli/src/native/daemon.rs +++ b/cli/src/native/daemon.rs @@ -21,6 +21,16 @@ pub async fn run_daemon(session: &str) { // (via the ab-connect extension) land in a per-session Chrome tab group. let _ = super::browser::DAEMON_SESSION.set(session.to_string()); + // Bootstrap / refresh the site-adapter pack in the background (first-run + + // periodic TTL). This populates ~/.chrome-use/sites/.index.json so navigation + // can auto-suggest `site` commands for the page you land on, with zero added + // latency to any command. Best-effort; offline is a no-op. + if crate::site::needs_refresh() { + tokio::spawn(async { + let _ = crate::site::update().await; + }); + } + let socket_dir = get_daemon_socket_dir(); if !socket_dir.exists() { let _ = fs::create_dir_all(&socket_dir); diff --git a/cli/src/output.rs b/cli/src/output.rs index f39bd07..a3343f7 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -186,6 +186,26 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou } if let Some(data) = &resp.data { + // Auto-trigger: when you land on / read a page whose domain has site + // adapters, surface them so the agent pulls structured data via + // `chrome-use site /` instead of scraping the DOM. (In --json + // mode this same info rides along in the `siteAdapters` field above.) + if let Some(hint) = data.get("siteAdapters") { + let domain = hint.get("domain").and_then(|v| v.as_str()).unwrap_or(""); + let cmds: Vec<&str> = hint + .get("commands") + .and_then(|v| v.as_array()) + .map(|a| a.iter().filter_map(|v| v.as_str()).collect()) + .unwrap_or_default(); + if !cmds.is_empty() { + eprintln!("💡 site adapters for {domain} — prefer these for structured data:"); + eprintln!(" {}", color::dim(&cmds.join(", "))); + eprintln!( + " {}", + color::dim(&format!("e.g. chrome-use site {} --json", cmds[0])) + ); + } + } // A click that opened a new tab: surface it so the agent doesn't read the // unchanged old page as a failed click (issue #24-A). if let Some(opened) = data.get("openedTab") { diff --git a/cli/src/site.rs b/cli/src/site.rs index d229ed6..8d43744 100644 --- a/cli/src/site.rs +++ b/cli/src/site.rs @@ -199,9 +199,127 @@ pub async fn update() -> Result { count += 1; } } + // Build the domain→adapters index and stamp the sync time so navigation can + // suggest adapters (auto-trigger) and `needs_refresh` can pace re-syncs. + write_domain_index(&dir); + if let Some(p) = last_update_path() { + let _ = std::fs::write(p, now_secs().to_string()); + } Ok(count) } +/// `~/.chrome-use/sites/.last_update` — unix-seconds marker of the last sync. +fn last_update_path() -> Option { + sites_dir().map(|d| d.join(".last_update")) +} + +/// `~/.chrome-use/sites/.index.json` — `{ "github.com": ["github/issues", …], … }`, +/// built on `update` so navigation can look up adapters by domain without parsing +/// all ~145 adapter files on every command. +fn index_path() -> Option { + sites_dir().map(|d| d.join(".index.json")) +} + +fn now_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// Parse every installed adapter and write the domain→adapters index. Within a +/// domain, read-only adapters are listed first (then alphabetical) so the +/// auto-suggested example leads with a safe read, not a write action. +fn write_domain_index(dir: &std::path::Path) { + let mut by_domain: std::collections::BTreeMap> = Default::default(); + for spec in list_adapters().unwrap_or_default() { + if let Ok(a) = load_adapter(&spec) { + if let Some(d) = a.domain() { + let read_only = a + .meta + .get("readOnly") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + by_domain + .entry(d.to_string()) + .or_default() + .push((read_only, spec)); + } + } + } + let ordered: std::collections::BTreeMap> = by_domain + .into_iter() + .map(|(domain, mut v)| { + // read-only (true) first, then by spec name + v.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1))); + (domain, v.into_iter().map(|(_, s)| s).collect()) + }) + .collect(); + if let Ok(json) = serde_json::to_string(&ordered) { + let _ = std::fs::write(dir.join(".index.json"), json); + } +} + +const DEFAULT_TTL_DAYS: u64 = 7; + +/// Whether the adapter pack should be (re)synced: true on first use (nothing +/// installed) or when the last sync is older than the TTL. Disabled by +/// `AGENT_BROWSER_SITES_NO_AUTO_UPDATE=1`; TTL overridable via +/// `AGENT_BROWSER_SITES_TTL_DAYS` (0 = always). +pub fn needs_refresh() -> bool { + if std::env::var_os("AGENT_BROWSER_SITES_NO_AUTO_UPDATE").is_some() { + return false; + } + let Some(dir) = sites_dir() else { + return false; + }; + // First use: no adapters installed yet. + if list_adapters().map(|l| l.is_empty()).unwrap_or(true) { + let _ = &dir; + return true; + } + let ttl_days = std::env::var("AGENT_BROWSER_SITES_TTL_DAYS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(DEFAULT_TTL_DAYS); + let ttl = ttl_days.saturating_mul(86_400); + match last_update_path().and_then(|p| std::fs::read_to_string(p).ok()) { + Some(s) => match s.trim().parse::() { + Ok(ts) => now_secs().saturating_sub(ts) >= ttl, + Err(_) => true, + }, + None => true, // no marker → treat as stale + } +} + +/// Adapters whose `@meta.domain` matches `host` (exact, or `host` is a subdomain +/// of it) — for auto-suggesting `site` commands when you land on a known site. +/// Reads the prebuilt `.index.json`; empty if the pack isn't synced yet. +pub fn adapters_for_domain(host: &str) -> Vec { + let host = host.trim_start_matches("www."); + let Some(raw) = index_path().and_then(|p| std::fs::read_to_string(p).ok()) else { + return Vec::new(); + }; + let Ok(idx) = serde_json::from_str::>>(&raw) + else { + return Vec::new(); + }; + // Preserve the index's per-domain ordering (read-only adapters first); just + // dedup if a host somehow matches multiple domain keys. + let mut out: Vec = Vec::new(); + for (domain, specs) in idx { + let d = domain.trim_start_matches("www."); + if host == d || host.ends_with(&format!(".{d}")) { + for s in specs { + if !out.contains(&s) { + out.push(s); + } + } + } + } + out +} + /// Map CLI args to the adapter's `args` object. Positional args fill the adapter's /// declared `args` keys in order; `--key value` overrides by name. The adapter /// validates required args itself. diff --git a/package.json b/package.json index fb9a2fc..2585cc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chrome-use", - "version": "1.5.19", + "version": "1.5.20", "description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default", "type": "module", "packageManager": "pnpm@11.1.3", diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md index a4b30f3..8d75a74 100644 --- a/skill-data/core/SKILL.md +++ b/skill-data/core/SKILL.md @@ -218,6 +218,13 @@ chrome-use site github/issues owner/repo --json # run it → JSON (navigates t - If no adapter fits, fall back to the normal `snapshot`/`eval` loop. Adapters come from the [bb-sites](https://github.com/epiral/bb-sites) community pack; chrome-use fetches & runs them. +> **Auto-trigger — act on it.** chrome-use keeps the pack synced automatically (first use + +> weekly), and when you `open`/`navigate`/`snapshot` a page whose domain has adapters it tells +> you: a `💡 site adapters for ` line on stderr, and a `siteAdapters: {domain, commands}` +> field in `--json`. **When you see that, prefer the listed `site /` over snapshot+click +> for reading data** — it's the cheaper, more reliable path and it's already installed. You don't +> need to run `site update` yourself; just use the command it names. + ## Quickstart ```bash