fix: 引用消息 XML 转义解析 + 搜索容错跳过 corrupt DB

- 引用消息(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
pull/2/head
jackwener 2026-04-16 16:48:59 +08:00
parent a6fa82adb3
commit dfd020a2b9
1 changed files with 46 additions and 15 deletions

View File

@ -209,7 +209,7 @@ pub async fn q_search(
let md5_lookup = names.md5_to_uname.clone(); let md5_lookup = names.md5_to_uname.clone();
let names_map = names.map.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 conn = Connection::open(&path2)?;
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'" "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) 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); targets.extend(table_targets);
} }
@ -260,26 +264,35 @@ pub async fn q_search(
let limit2 = limit * 3; let limit2 = limit * 3;
let names_map2 = names.map.clone(); let names_map2 = names.map.clone();
let found: Vec<Value> = tokio::task::spawn_blocking(move || { let found: Vec<Value> = match tokio::task::spawn_blocking(move || {
let conn = Connection::open(&db_path)?; let conn = Connection::open(&db_path)?;
let mut all = Vec::new(); let mut all = Vec::new();
for (tname, display, uname) in &table_list { for (tname, display, uname) in &table_list {
let is_group = uname.contains("@chatroom"); let is_group = uname.contains("@chatroom");
let rows = search_in_table(&conn, tname, &uname, is_group, match search_in_table(&conn, tname, &uname, is_group,
&names_map2, &kw2, since2, until2, limit2)?; &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() { Ok(rows) => {
if let Some(obj) = row.as_object_mut() { for mut row in rows {
obj.insert("chat".into(), serde_json::Value::String( if row.get("chat").map(|v| v.as_str().unwrap_or("")).unwrap_or("").is_empty() {
if display.is_empty() { tname.clone() } else { display.clone() } 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) 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); results.extend(found);
} }
@ -654,8 +667,18 @@ fn parse_appmsg(text: &str) -> Option<String> {
"57" => { "57" => {
let ref_content = extract_xml_text(text, "content") let ref_content = extract_xml_text(text, "content")
.map(|s| { .map(|s| {
let s: String = s.split_whitespace().collect::<Vec<_>>().join(" "); // content 可能是 HTML 转义的 XML被引用的消息是 appmsg 时)
if s.len() > 80 { format!("{}...", &s[..80]) } else { s } let unescaped = unescape_html(&s);
// 如果解转义后是 XML尝试递归解析
if unescaped.contains("<appmsg") {
if let Some(parsed) = parse_appmsg(&unescaped) {
return parsed;
}
}
let s: String = unescaped.split_whitespace().collect::<Vec<_>>().join(" ");
if s.chars().count() > 40 {
format!("{}...", s.chars().take(40).collect::<String>())
} else { s }
}) })
.unwrap_or_default(); .unwrap_or_default();
let quote = if !title.is_empty() { format!("[引用] {}", title) } else { "[引用]".into() }; let quote = if !title.is_empty() { format!("[引用] {}", title) } else { "[引用]".into() };
@ -679,6 +702,14 @@ fn extract_xml_text(xml: &str, tag: &str) -> Option<String> {
Some(xml[content_start..content_start + end].trim().to_string()) Some(xml[content_start..content_start + end].trim().to_string())
} }
fn unescape_html(s: &str) -> String {
s.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&amp;", "&")
.replace("&quot;", "\"")
.replace("&apos;", "'")
}
fn fmt_time(ts: i64, fmt: &str) -> String { fn fmt_time(ts: i64, fmt: &str) -> String {
Local.timestamp_opt(ts, 0) Local.timestamp_opt(ts, 0)
.single() .single()