chore: Apache-2.0 license, Windows support, install.ps1

This commit is contained in:
jackwener
2026-04-16 22:30:45 +08:00
parent 6d40c7f737
commit 6cdc806642
27 changed files with 1687 additions and 866 deletions
+1 -145
View File
@@ -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 映射
///
/// 兼容两种格式: