From dfd020a2b938dd323f808c16fa7c8288aba7795c Mon Sep 17 00:00:00 2001 From: jackwener Date: Thu, 16 Apr 2026 16:48:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BC=95=E7=94=A8=E6=B6=88=E6=81=AF=20X?= =?UTF-8?q?ML=20=E8=BD=AC=E4=B9=89=E8=A7=A3=E6=9E=90=20+=20=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=AE=B9=E9=94=99=E8=B7=B3=E8=BF=87=20corrupt=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引用消息(type=57)的 ref_content 可能是 HTML 转义的 XML,新增 unescape_html() 先反转义,再递归调用 parse_appmsg 解析嵌套结构 - 全局搜索遍历 msg_db_keys 时,单个 DB open/query 失败改为 eprintln+continue 而非传播错误,避免一个 corrupt cache 导致整个搜索失败 - search_in_table 失败也改为 skip 而非 abort --- src/daemon/query.rs | 61 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/daemon/query.rs b/src/daemon/query.rs index 612b5b1..659ccca 100644 --- a/src/daemon/query.rs +++ b/src/daemon/query.rs @@ -209,7 +209,7 @@ pub async fn q_search( let md5_lookup = names.md5_to_uname.clone(); let names_map = names.map.clone(); - let table_targets: Vec<(String, String, String, String)> = tokio::task::spawn_blocking(move || { + let table_targets: Vec<(String, String, String, String)> = match tokio::task::spawn_blocking(move || { let conn = Connection::open(&path2)?; let mut stmt = conn.prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'" @@ -239,7 +239,11 @@ pub async fn q_search( )); } Ok::<_, anyhow::Error>(result) - }).await??; + }).await { + Ok(Ok(v)) => v, + Ok(Err(e)) => { eprintln!("[search] skip DB {}: {}", rel_key, e); continue; } + Err(e) => { eprintln!("[search] task error {}: {}", rel_key, e); continue; } + }; targets.extend(table_targets); } @@ -260,26 +264,35 @@ pub async fn q_search( let limit2 = limit * 3; let names_map2 = names.map.clone(); - let found: Vec = tokio::task::spawn_blocking(move || { + let found: Vec = match tokio::task::spawn_blocking(move || { let conn = Connection::open(&db_path)?; let mut all = Vec::new(); for (tname, display, uname) in &table_list { let is_group = uname.contains("@chatroom"); - let rows = search_in_table(&conn, tname, &uname, is_group, - &names_map2, &kw2, since2, until2, limit2)?; - for mut row in rows { - if row.get("chat").map(|v| v.as_str().unwrap_or("")).unwrap_or("").is_empty() { - if let Some(obj) = row.as_object_mut() { - obj.insert("chat".into(), serde_json::Value::String( - if display.is_empty() { tname.clone() } else { display.clone() } - )); + match search_in_table(&conn, tname, &uname, is_group, + &names_map2, &kw2, since2, until2, limit2) + { + Ok(rows) => { + for mut row in rows { + if row.get("chat").map(|v| v.as_str().unwrap_or("")).unwrap_or("").is_empty() { + if let Some(obj) = row.as_object_mut() { + obj.insert("chat".into(), serde_json::Value::String( + if display.is_empty() { tname.clone() } else { display.clone() } + )); + } + } + all.push(row); } } - all.push(row); + Err(e) => eprintln!("[search] skip table {}: {}", tname, e), } } Ok::<_, anyhow::Error>(all) - }).await??; + }).await { + Ok(Ok(v)) => v, + Ok(Err(e)) => { eprintln!("[search] skip DB: {}", e); continue; } + Err(e) => { eprintln!("[search] task error: {}", e); continue; } + }; results.extend(found); } @@ -654,8 +667,18 @@ fn parse_appmsg(text: &str) -> Option { "57" => { let ref_content = extract_xml_text(text, "content") .map(|s| { - let s: String = s.split_whitespace().collect::>().join(" "); - if s.len() > 80 { format!("{}...", &s[..80]) } else { s } + // content 可能是 HTML 转义的 XML(被引用的消息是 appmsg 时) + let unescaped = unescape_html(&s); + // 如果解转义后是 XML,尝试递归解析 + if unescaped.contains(">().join(" "); + if s.chars().count() > 40 { + format!("{}...", s.chars().take(40).collect::()) + } else { s } }) .unwrap_or_default(); let quote = if !title.is_empty() { format!("[引用] {}", title) } else { "[引用]".into() }; @@ -679,6 +702,14 @@ fn extract_xml_text(xml: &str, tag: &str) -> Option { Some(xml[content_start..content_start + end].trim().to_string()) } +fn unescape_html(s: &str) -> String { + s.replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace(""", "\"") + .replace("'", "'") +} + fn fmt_time(ts: i64, fmt: &str) -> String { Local.timestamp_opt(ts, 0) .single()