No job gets stranded on a blank tab

- pick_tab: a page that went somewhere (http) beats about:blank at every
  rank — own live tab > free live tab > own blank > free blank. Headless
  Chrome boots on about:blank and a job that claims it was pinned there;
  desktop behaviour unchanged (the front tab is a live page anyway)
- the tabs list shows a page's url when its title is empty, so the model
  never reads a faceless '1. (current)' line

Live proof (headless Chrome + qwen brain, same scenario that failed
before): reads the number through the browser path, 172s -> 92s, and the
model now flags the page's own link as untrusted (S3 working)
This commit is contained in:
AnyBot Dev 2026-11-09 21:17:12 +08:00
parent 61c38dca66
commit cf1d4d618c
2 changed files with 24 additions and 7 deletions

View File

@ -542,15 +542,16 @@ pub fn release(job: &str) -> Vec<String> {
/// The tab of a page list (latest-used first) this step works on: its job's latest own tab, else /// The tab of a page list (latest-used first) this step works on: its job's latest own tab, else
/// the latest one no other job owns (a job's first step). /// the latest one no other job owns (a job's first step).
///
/// A page that actually went somewhere (http/https) beats a blank one at every step: a browser
/// that boots on about:blank (headless Chrome does) must not strand a job on the empty tab.
pub fn pick_tab(list: &[Value]) -> Option<&Value> { pub fn pick_tab(list: &[Value]) -> Option<&Value> {
let me = job(); let me = job();
let id = |t: &Value| t["id"].as_str().unwrap_or("").to_string(); let id = |t: &Value| t["id"].as_str().unwrap_or("").to_string();
if !me.is_empty() { let mine = |t: &Value| !me.is_empty() && owner(&id(t)).as_deref() == Some(me.as_str());
if let Some(t) = list.iter().find(|t| owner(&id(t)).as_deref() == Some(me.as_str())) { let live = |t: &Value| t["url"].as_str().unwrap_or("").starts_with("http");
return Some(t); let free = |t: &Value| !others_tab(&id(t));
} list.iter().find(|t| mine(t) && live(t)).or_else(|| list.iter().find(|t| free(t) && live(t))).or_else(|| list.iter().find(|t| mine(t))).or_else(|| list.iter().find(|t| free(t)))
}
list.iter().find(|t| !others_tab(&id(t)))
} }
/// The open page tabs' ids (none when the browser isn't open; it isn't started for this). /// The open page tabs' ids (none when the browser isn't open; it isn't started for this).
@ -889,6 +890,16 @@ mod tests {
assert_eq!(as_job("job-b", || pick_tab(&list).map(|t| t["id"].clone())), Some("tab-b".into())); assert_eq!(as_job("job-b", || pick_tab(&list).map(|t| t["id"].clone())), Some("tab-b".into()));
// A new job starts on the latest tab nobody has; another job's tabs are off limits. // A new job starts on the latest tab nobody has; another job's tabs are off limits.
assert_eq!(as_job("job-c", || pick_tab(&list).map(|t| t["id"].clone())), Some("tab-free".into())); assert_eq!(as_job("job-c", || pick_tab(&list).map(|t| t["id"].clone())), Some("tab-free".into()));
// A real page beats a blank tab, whoever's it is: the blank-tab trap (headless Chrome
// boots on about:blank; a job that claims it must not be stranded there).
let pages = vec![serde_json::json!({"type": "page", "id": "blank", "url": "about:blank"}), serde_json::json!({"type": "page", "id": "real", "url": "https://example.com"})];
assert_eq!(as_job("job-x", || { claim("blank"); pick_tab(&pages).map(|t| t["id"].clone()) }), Some("real".into()));
// A job's own real page still wins over a stranger's.
let mine = vec![serde_json::json!({"type": "page", "id": "theirs", "url": "https://other.example"}), serde_json::json!({"type": "page", "id": "mine", "url": "https://mine.example"})];
assert_eq!(as_job("job-y", || { claim("mine"); pick_tab(&mine).map(|t| t["id"].clone()) }), Some("mine".into()));
// All blank: keep the old behaviour (own tab, then any free one).
let blanks = vec![serde_json::json!({"type": "page", "id": "b1", "url": "about:blank"}), serde_json::json!({"type": "page", "id": "b2", "url": "about:blank"})];
assert_eq!(as_job("job-z", || { claim("b2"); pick_tab(&blanks).map(|t| t["id"].clone()) }), Some("b2".into()));
assert!(as_job("job-c", || others_tab("tab-a") && !others_tab("tab-free"))); assert!(as_job("job-c", || others_tab("tab-a") && !others_tab("tab-free")));
assert!(as_job("job-a", || !others_tab("tab-a"))); assert!(as_job("job-a", || !others_tab("tab-a")));
// A claimed tab stays its first owner's. // A claimed tab stays its first owner's.

View File

@ -1012,7 +1012,13 @@ fn tabs(args: &Value, profile: Option<&str>) -> Result<String, String> {
_ => Ok(if list.is_empty() { _ => Ok(if list.is_empty() {
"沒有開著的分頁。".into() "沒有開著的分頁。".into()
} else { } else {
list.iter().enumerate().map(|(i, t)| format!("{}. {}{}\n {}", i + 1, t["title"].as_str().unwrap_or(""), if t["id"].as_str() == Some(current.as_str()) { "(目前)" } else { "" }, t["url"].as_str().unwrap_or(""))).collect::<Vec<_>>().join("\n") // Untitled pages (about:blank, bare pages) show their url, so the model never sees an empty line.
list.iter().enumerate().map(|(i, t)| {
let url = t["url"].as_str().unwrap_or("");
let title = t["title"].as_str().unwrap_or("");
let title = if title.is_empty() { url } else { title };
format!("{}. {}{}\n {}", i + 1, title, if t["id"].as_str() == Some(current.as_str()) { "(目前)" } else { "" }, url)
}).collect::<Vec<_>>().join("\n")
}), }),
} }
} }