mirror of
https://github.com/jackwener/wx-cli.git
synced 2026-10-09 07:45:45 +00:00
chore: Apache-2.0 license, Windows support, install.ps1
This commit is contained in:
+1
-145
@@ -4,9 +4,7 @@ pub mod server;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::config;
|
||||
|
||||
@@ -78,154 +76,12 @@ async fn async_run() -> Result<()> {
|
||||
|
||||
let names_arc = Arc::new(std::sync::RwLock::new(names));
|
||||
|
||||
// 启动 WAL watcher
|
||||
let (watch_tx, _) = broadcast::channel::<crate::ipc::WatchEvent>(500);
|
||||
let session_wal = cfg.db_dir.join("session").join("session.db-wal");
|
||||
|
||||
// SAFETY: 我们确保 db 和 names_arc 在 daemon 生命周期内有效
|
||||
// 使用 Arc 传递引用避免 'static 问题
|
||||
let db_arc = Arc::clone(&db);
|
||||
let names_arc2 = Arc::clone(&names_arc);
|
||||
let tx_clone = watch_tx.clone();
|
||||
let session_wal2 = session_wal.clone();
|
||||
tokio::spawn(async move {
|
||||
run_watcher(db_arc, names_arc2, tx_clone, session_wal2).await;
|
||||
});
|
||||
|
||||
// 启动 IPC server(阻塞)
|
||||
server::serve(Arc::clone(&db), Arc::clone(&names_arc), watch_tx).await?;
|
||||
server::serve(Arc::clone(&db), Arc::clone(&names_arc)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_watcher(
|
||||
db: Arc<cache::DbCache>,
|
||||
names: Arc<std::sync::RwLock<query::Names>>,
|
||||
tx: broadcast::Sender<crate::ipc::WatchEvent>,
|
||||
session_wal: PathBuf,
|
||||
) {
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use crate::ipc::WatchEvent;
|
||||
|
||||
let mut last_mtime = 0u64;
|
||||
let mut last_ts: HashMap<String, i64> = HashMap::new();
|
||||
let mut initialized = false;
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
if tx.receiver_count() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let wal_mtime = match mtime_nanos(&session_wal) {
|
||||
0 => continue,
|
||||
m => m,
|
||||
};
|
||||
if wal_mtime == last_mtime {
|
||||
continue;
|
||||
}
|
||||
last_mtime = wal_mtime;
|
||||
|
||||
let path = match db.get("session/session.db").await {
|
||||
Ok(Some(p)) => p,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let path2 = path.clone();
|
||||
let rows: Vec<(String, Vec<u8>, i64, i64, String)> = match tokio::task::spawn_blocking(move || {
|
||||
let conn = rusqlite::Connection::open(&path2)?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT username, summary, last_timestamp, last_msg_type, last_msg_sender
|
||||
FROM SessionTable WHERE last_timestamp > 0
|
||||
ORDER BY last_timestamp DESC LIMIT 50"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, Vec<u8>>(1)
|
||||
.or_else(|_| row.get::<_, String>(1).map(|s| s.into_bytes()))
|
||||
.unwrap_or_default(),
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, i64>(3).unwrap_or(0),
|
||||
row.get::<_, String>(4).unwrap_or_default(),
|
||||
))
|
||||
})?.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok::<_, anyhow::Error>(rows)
|
||||
}).await {
|
||||
Ok(Ok(r)) => r,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let names_guard = match names.read() {
|
||||
Ok(g) => g,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
for (username, summary_bytes, ts, msg_type, sender) in &rows {
|
||||
if !initialized {
|
||||
last_ts.insert(username.clone(), *ts);
|
||||
continue;
|
||||
}
|
||||
let prev_ts = last_ts.get(username).copied().unwrap_or(0);
|
||||
if *ts <= prev_ts {
|
||||
continue;
|
||||
}
|
||||
last_ts.insert(username.clone(), *ts);
|
||||
|
||||
let display = names_guard.display(username);
|
||||
let is_group = username.contains("@chatroom");
|
||||
let summary = decompress_or_str(summary_bytes);
|
||||
let summary = if summary.contains(":\n") {
|
||||
summary.splitn(2, ":\n").nth(1).unwrap_or(&summary).to_string()
|
||||
} else {
|
||||
summary
|
||||
};
|
||||
let sender_display = if !sender.is_empty() {
|
||||
names_guard.map.get(sender).cloned().unwrap_or_else(|| sender.clone())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let event = WatchEvent {
|
||||
event: "message".into(),
|
||||
time: Some(fmt_hhmm(*ts)),
|
||||
chat: Some(display),
|
||||
username: Some(username.clone()),
|
||||
is_group: Some(is_group),
|
||||
sender: Some(sender_display),
|
||||
content: Some(summary),
|
||||
msg_type: Some(query::fmt_type(*msg_type)),
|
||||
timestamp: Some(*ts),
|
||||
};
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
|
||||
if !initialized {
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use cache::mtime_nanos;
|
||||
|
||||
fn decompress_or_str(data: &[u8]) -> String {
|
||||
if data.is_empty() { return String::new(); }
|
||||
if let Ok(dec) = zstd::decode_all(data) {
|
||||
if let Ok(s) = String::from_utf8(dec) { return s; }
|
||||
}
|
||||
String::from_utf8_lossy(data).into_owned()
|
||||
}
|
||||
|
||||
fn fmt_hhmm(ts: i64) -> String {
|
||||
use chrono::{Local, TimeZone};
|
||||
Local.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.format("%H:%M").to_string())
|
||||
.unwrap_or_else(|| ts.to_string())
|
||||
}
|
||||
|
||||
/// 从 all_keys.json 提取 rel_key -> enc_key 映射
|
||||
///
|
||||
/// 兼容两种格式:
|
||||
|
||||
Reference in New Issue
Block a user