Export drops threads; a broken keyring stops with the secret recoverable

This commit is contained in:
王性驊 2026-11-05 15:48:20 +08:00
parent 96174c4776
commit caa580fce9
3 changed files with 18 additions and 7 deletions

View File

@ -40,7 +40,7 @@ fn scrub(v: &mut Value) {
pub fn export(b: &Bot) -> String {
let mut v = serde_json::to_value(b).unwrap_or(Value::Null);
if let Value::Object(m) = &mut v {
for k in ["messages", "allow_rules", "unread", "last_turn", "workdir", "pinned", "id"] {
for k in ["messages", "threads", "allow_rules", "unread", "last_turn", "workdir", "pinned", "id"] {
m.remove(k);
}
if let Some(Value::Array(routines)) = m.get_mut("routines") {

View File

@ -178,7 +178,10 @@ fn onepassword(host: &str) -> Option<Login> {
return None;
}
let items: Value = serde_json::from_slice(&list.stdout).ok()?;
let id = items.as_array()?.iter().find(|i| i["urls"].as_array().into_iter().flatten().any(|u| host_of(u["href"].as_str().unwrap_or("")).ends_with(host)))?["id"].as_str()?.to_string();
// Match the host exactly or as a subdomain (with the dot): "notgoogle.com" must not
// answer for "google.com" (a plain ends_with fills the wrong login on a lookalike domain).
let host_match = |h: &str, want: &str| h == want || h.ends_with(&format!(".{want}"));
let id = items.as_array()?.iter().find(|i| i["urls"].as_array().into_iter().flatten().any(|u| host_match(&host_of(u["href"].as_str().unwrap_or("")), host)))?["id"].as_str()?.to_string();
let got = Command::new("op").env("PATH", &path).args(["item", "get", &id, "--fields", "label=username,label=password", "--reveal", "--format", "json"]).output().ok()?;
let fields: Value = serde_json::from_slice(&got.stdout).ok()?;
let arr = fields.as_array()?;
@ -412,9 +415,6 @@ pub fn fill_login(plan: &LoginPlan, submit: bool) -> Result<String, String> {
let (username, password, source) = match (&plan.item, &plan.other) {
(Some(i), _) => {
let (u, p) = crate::vault_store::login_secret(&i.id).ok_or("保險庫裡沒有這筆登入資訊")?;
if !plan.matched {
crate::vault_store::add_site(&i.id, &plan.host)?;
}
(u, p, "保險庫")
}
(None, Some(l)) => (l.username.clone(), l.password.clone(), l.source),
@ -425,6 +425,13 @@ pub fn fill_login(plan: &LoginPlan, submit: bool) -> Result<String, String> {
if got["password"].as_bool() != Some(true) && got["user"].as_bool() != Some(true) {
return Err("這一頁找不到帳號或密碼欄位。先打開登入頁(有些網站要先按「登入」或先填帳號、按下一步才出現密碼欄)。".into());
}
// Remember the domain only after the fill worked: adding it on a failed (or lookalike)
// page made the next visit show matched:true and skip the anti-phishing warning.
if !plan.matched {
if let Some(i) = &plan.item {
crate::vault_store::add_site(&i.id, &plan.host)?;
}
}
Ok(format!(
"已在 {} 填入{}{}(來自{source},你看不到密碼)。{}",
plan.host,

View File

@ -381,8 +381,12 @@ fn store_key(key: &[u8]) -> Result<(), String> {
fn key(create: bool) -> Result<aead::LessSafeKey, String> {
let k = match read_key() {
Some(k) if k.len() == 32 => k,
_ if !create => return Err("找不到保險庫的金鑰(系統鑰匙圈裡的「AnyBot vault key」)".into()),
_ => {
// An entry exists but is not a key: generating a fresh one (and -U overwriting the
// entry) would destroy the only copy of the old key — every saved password would be
// undecryptable forever. Stop and tell the person instead.
Some(_) => return Err("保險庫金鑰損毀:鑰匙圈裡的「AnyBot vault key」內容不正確。已停止動作,避免覆蓋後舊密碼永遠解不開;若你確定不需要舊密碼,可手動刪除該鑰匙圈項目再試。".into()),
None if !create => return Err("找不到保險庫的金鑰(系統鑰匙圈裡的「AnyBot vault key」)".into()),
None => {
let mut k = vec![0u8; 32];
SystemRandom::new().fill(&mut k).map_err(|_| "產生金鑰失敗")?;
store_key(&k)?;