feat(site): auto-sync + auto-suggest adapters (auto-trigger)
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Make `site` trigger itself so an agent doesn't have to know adapters exist.
Auto-sync: the pack refreshes on first use and on a TTL (default 7d), both in
the `site` command path (blocking, fast) and as a non-blocking background task
on daemon startup — so ~/.chrome-use/sites/.index.json is always populated with
zero added latency. Tune via AGENT_BROWSER_SITES_TTL_DAYS; disable with
AGENT_BROWSER_SITES_NO_AUTO_UPDATE=1. `update` now writes .last_update + a
domain→adapters .index.json (read-only adapters ordered first).
Auto-suggest: `open`/`navigate`/`snapshot` onto a domain with adapters attaches
`siteAdapters: {domain, commands}` to the response; the CLI prints a
`💡 site adapters for <domain>` hint (stderr) and the field rides along in --json.
SKILL.md tells the agent to prefer the listed `site <name>/<cmd>` over scraping.
This keeps the 'never auto-disrupt user tabs' guarantee — it suggests, the agent
decides; nothing auto-runs on navigation.
site.rs: needs_refresh/adapters_for_domain/write_domain_index + timestamp/index
in update(). daemon.rs: background bootstrap. actions.rs: with_site_hint on
navigate + snapshot. output.rs: hint render. Verified live: open github.com →
hint leads with read-only github/issues; --json carries siteAdapters.
This commit is contained in:
@@ -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 <domain>` 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
|
||||
|
||||
@@ -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 自己的命令,断言编译成一次检查:
|
||||
|
||||
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrome-use"
|
||||
version = "1.5.19"
|
||||
version = "1.5.20"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -840,6 +840,27 @@ fn main() {
|
||||
// `info` are CLI-side (download/filesystem); `site <name>/<cmd> [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");
|
||||
|
||||
@@ -2478,7 +2478,10 @@ async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value,
|
||||
wb.navigate(url).await?;
|
||||
let new_url = wb.get_url().await.unwrap_or_else(|_| url.to_string());
|
||||
let title = wb.get_title().await.unwrap_or_default();
|
||||
return Ok(json!({ "url": new_url, "title": title }));
|
||||
return Ok(with_site_hint(
|
||||
json!({ "url": new_url, "title": title }),
|
||||
url,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2545,7 +2548,7 @@ async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value,
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Ok(Some(switched)) = mgr.reuse_tab_for_url(url).await {
|
||||
return Ok(switched);
|
||||
return Ok(with_site_hint(switched, url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2553,7 +2556,34 @@ async fn handle_navigate(cmd: &Value, state: &mut DaemonState) -> Result<Value,
|
||||
// Adaptive humanize: sample the freshly loaded page for known behavioural
|
||||
// anti-bot vendors and escalate this session to Human if any are present.
|
||||
detect_and_set_humanize(mgr).await;
|
||||
Ok(result)
|
||||
Ok(with_site_hint(result, url))
|
||||
}
|
||||
|
||||
/// Annotate a navigation/snapshot result with the `site` adapters available for
|
||||
/// the page's domain (auto-trigger): when you land on e.g. github.com, the
|
||||
/// response carries `siteAdapters: { domain, commands: ["github/issues", …] }` so
|
||||
/// the agent reaches for a structured-data adapter instead of scraping. No-op
|
||||
/// when nothing matches or the pack isn't synced yet.
|
||||
fn with_site_hint(mut result: Value, fallback_url: &str) -> 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<Value,
|
||||
let ref_count = refs.len();
|
||||
let mut out = json!({ "snapshot": tree, "origin": url, "refs": refs });
|
||||
|
||||
// Auto-trigger: if this domain has site adapters, surface them so the agent
|
||||
// pulls structured data instead of walking the tree. `with_site_hint` reads
|
||||
// the `url` field, so pass it under that key.
|
||||
if let Some(hint) = with_site_hint(json!({ "url": url }), &url).get("siteAdapters") {
|
||||
out["siteAdapters"] = hint.clone();
|
||||
}
|
||||
|
||||
// Canvas/WebGL apps (games, map/3D viewers, drawing tools) paint to a
|
||||
// <canvas> and expose almost no accessibility tree, so `snapshot` comes back
|
||||
// near-empty and agents get stuck looking for refs that will never exist
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <name>/<cmd>` 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") {
|
||||
|
||||
+118
@@ -199,9 +199,127 @@ pub async fn update() -> Result<usize, String> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<String, Vec<(bool, String)>> = 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<String, Vec<String>> = 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::<u64>().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::<u64>() {
|
||||
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<String> {
|
||||
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::<std::collections::BTreeMap<String, Vec<String>>>(&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<String> = 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.
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 <domain>` line on stderr, and a `siteAdapters: {domain, commands}`
|
||||
> field in `--json`. **When you see that, prefer the listed `site <name>/<cmd>` 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
|
||||
|
||||
Reference in New Issue
Block a user