fix: 深度 review 修复 10 个 bug/问题

Critical & High:
- daemon 日志:启动时将 stdout/stderr 重定向到 ~/.wx-cli/daemon.log
  而非 /dev/null,使 wx daemon logs 真正可用
- q_history 找不到聊天时改为 bail! 而非 ok:true+error 字段,
  避免 CLI 静默返回空输出
- init 写 config.json 默认路径改为 ~/.wx-cli/config.json,
  避免写入系统 bin 目录(/usr/local/bin/config.json)
- LIKE 通配符:搜索关键词中的 %/_/\ 现在正确转义
- WAL 路径:改用 OsString.push 拼接 "-wal" 后缀,
  避免 display() 在非 UTF-8 路径上失效
- cmd_stop:检查 kill() 返回值,ESRCH 时给出明确提示

Performance & Code quality:
- full_decrypt:改为流式逐页读写,峰值内存从 2×文件大小降为 O(1)
- Regex:msg_table_re() 用 OnceLock 静态编译,避免热路径重复编译
- mtime_nanos:消除 daemon/mod.rs 与 cache.rs 的重复定义
- use super::super::cli::transport → use super::transport
- 删除未使用的 save_config、Request::to_json_line 死代码
This commit is contained in:
jackwener
2026-04-16 17:07:15 +08:00
parent dfd020a2b9
commit 7f869e7c3b
15 changed files with 100 additions and 70 deletions
+11 -6
View File
@@ -80,11 +80,10 @@ impl DbCache {
continue;
}
let db_path = self.db_dir.join(rel_key.replace('\\', std::path::MAIN_SEPARATOR_STR).replace('/', std::path::MAIN_SEPARATOR_STR));
let wal_path_str = format!("{}-wal", db_path.display());
let wal_path = Path::new(&wal_path_str);
let wal_path = wal_path_for(&db_path);
let db_mt = mtime_nanos(&db_path);
let wal_mt = if wal_path.exists() { mtime_nanos(wal_path) } else { 0 };
let wal_mt = if wal_path.exists() { mtime_nanos(&wal_path) } else { 0 };
if db_mt == entry.db_mt && wal_mt == entry.wal_mt {
inner.insert(rel_key.clone(), CacheEntry {
@@ -135,8 +134,7 @@ impl DbCache {
return Ok(None);
}
let wal_path_str = format!("{}-wal", db_path.display());
let wal_path = Path::new(&wal_path_str).to_path_buf();
let wal_path = wal_path_for(&db_path);
let db_mt = mtime_nanos(&db_path);
let wal_mt = if wal_path.exists() { mtime_nanos(&wal_path) } else { 0 };
@@ -195,13 +193,20 @@ impl DbCache {
}
}
fn mtime_nanos(path: &Path) -> u64 {
pub(super) fn mtime_nanos(path: &Path) -> u64 {
std::fs::metadata(path)
.and_then(|m| m.modified())
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos() as u64)
.unwrap_or(0)
}
/// `foo/bar.db` → `foo/bar.db-wal`(用 OsString 拼接,避免 display() 的 UTF-8 问题)
fn wal_path_for(db_path: &Path) -> PathBuf {
let mut name = db_path.file_name().unwrap_or_default().to_os_string();
name.push("-wal");
db_path.with_file_name(name)
}
fn hex_to_32bytes(s: &str) -> Result<[u8; 32]> {
if s.len() != 64 {
anyhow::bail!("密钥 hex 长度应为 64,实际为 {}", s.len());
+1 -6
View File
@@ -208,12 +208,7 @@ async fn run_watcher(
}
}
fn mtime_nanos(path: &std::path::Path) -> u64 {
std::fs::metadata(path)
.and_then(|m| m.modified())
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos() as u64)
.unwrap_or(0)
}
use cache::mtime_nanos;
fn decompress_or_str(data: &[u8]) -> String {
if data.is_empty() { return String::new(); }
+13 -4
View File
@@ -4,9 +4,16 @@ use regex::Regex;
use rusqlite::Connection;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::OnceLock;
use super::cache::DbCache;
/// 静态编译的 Msg 表名正则,避免在热路径中重复编译
fn msg_table_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"^Msg_[0-9a-f]{32}$").unwrap())
}
/// 联系人名称缓存
#[derive(Clone)]
pub struct Names {
@@ -141,7 +148,7 @@ pub async fn q_history(
let tables = find_msg_tables(db, names, &username).await?;
if tables.is_empty() {
return Ok(json!({ "error": format!("找不到 {} 的消息记录", display) }));
anyhow::bail!("找不到 {} 的消息记录", display);
}
let mut all_msgs: Vec<Value> = Vec::new();
@@ -218,7 +225,7 @@ pub async fn q_search(
.filter_map(|r| r.ok())
.collect();
let re = Regex::new(r"^Msg_[0-9a-f]{32}$").unwrap();
let re = msg_table_re();
let mut result = Vec::new();
for tname in table_names {
if !re.is_match(&tname) {
@@ -483,8 +490,10 @@ fn search_in_table(
limit: usize,
) -> Result<Vec<Value>> {
let id2u = load_id2u(conn);
let mut clauses = vec!["message_content LIKE ?".to_string()];
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(format!("%{}%", keyword))];
// 转义 LIKE 通配符,使用 '\' 作为 ESCAPE 字符
let escaped_kw = keyword.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
let mut clauses = vec!["message_content LIKE ? ESCAPE '\\'".to_string()];
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(format!("%{}%", escaped_kw))];
if let Some(s) = since {
clauses.push("create_time >= ?".into());
params.push(Box::new(s));