use crate::model::{ Channel, ExternalAiBridge, KnowledgeDocument, KnowledgeSummary, SourceKind, TimelineEvent, }; use sha2::{Digest, Sha256}; use std::{ fs, path::{Component, Path, PathBuf}, process::Command, time::{SystemTime, UNIX_EPOCH}, }; use tauri::{AppHandle, Manager}; pub fn now() -> String { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| format!("{}.{:03}Z", d.as_secs(), d.subsec_millis())) .unwrap_or_else(|_| "0Z".into()) } pub fn now_unix_ms() -> u128 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis()) .unwrap_or(0) } pub fn id(prefix: &str) -> String { format!("{}-{}", prefix, UuidPart::new()) } struct UuidPart(String); impl UuidPart { fn new() -> Self { Self(uuid::Uuid::new_v4().simple().to_string()[..12].to_uppercase()) } } impl std::fmt::Display for UuidPart { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } pub fn sha(bytes: &[u8]) -> String { format!("{:x}", Sha256::digest(bytes)) } pub fn root(app: &AppHandle) -> Result { if let Some(value) = std::env::var_os("HOLOLAKE_RUNTIME_TEST_ROOT") { let path = PathBuf::from(value); if !path.is_absolute() || !path.starts_with(std::env::temp_dir()) { return Err("HOLOLAKE_RUNTIME_TEST_ROOT_OUT_OF_SCOPE".into()); } fs::create_dir_all(&path).map_err(|e| e.to_string())?; return Ok(path); } let p = app .path() .app_data_dir() .map_err(|e| e.to_string())? .join("runtime-v1"); fs::create_dir_all(&p).map_err(|e| e.to_string())?; Ok(p) } pub fn read_json(p: &Path) -> Result { serde_json::from_slice(&fs::read(p).map_err(|e| e.to_string())?).map_err(|e| e.to_string()) } pub fn write_json(p: &Path, v: &T) -> Result<(), String> { if let Some(x) = p.parent() { fs::create_dir_all(x).map_err(|e| e.to_string())? } let tmp = p.with_extension("tmp"); fs::write( &tmp, serde_json::to_vec_pretty(v).map_err(|e| e.to_string())?, ) .map_err(|e| e.to_string())?; fs::rename(tmp, p).map_err(|e| e.to_string()) } pub fn channel(app: &AppHandle) -> Result, String> { let p = root(app)?.join("channel.json"); if p.exists() { read_json(&p).map(Some) } else { Ok(None) } } pub fn save_channel(app: &AppHandle, c: &Channel) -> Result<(), String> { write_json(&root(app)?.join("channel.json"), c) } pub fn event( kind: SourceKind, title: impl Into, content: impl Into, state: &str, ) -> TimelineEvent { TimelineEvent { event_id: id("HL-EVT"), source_kind: kind, title: title.into(), content: content.into(), occurred_at: now(), state: state.into(), } } pub fn append_event(app: &AppHandle, e: &TimelineEvent) -> Result<(), String> { let p = root(app)?.join("events.jsonl"); let mut s = if p.exists() { fs::read_to_string(&p).map_err(|e| e.to_string())? } else { String::new() }; s.push_str(&serde_json::to_string(e).map_err(|e| e.to_string())?); s.push('\n'); fs::write(p, s).map_err(|e| e.to_string()) } pub fn events(app: &AppHandle) -> Result, String> { let p = root(app)?.join("events.jsonl"); if !p.exists() { return Ok(vec![]); }; let mut v = fs::read_to_string(p) .map_err(|e| e.to_string())? .lines() .filter_map(|l| serde_json::from_str(l).ok()) .collect::>(); v.reverse(); Ok(v) } pub fn knowledge_root(app: &AppHandle) -> Result { let p = root(app)?.join("private-repository").join("knowledge"); fs::create_dir_all(p.join("docs")).map_err(|e| e.to_string())?; ensure_git(&p)?; Ok(p) } pub fn private_repo_root(app: &AppHandle) -> Result { let p = root(app)?.join("private-repository"); fs::create_dir_all(&p).map_err(|e| e.to_string())?; ensure_git(&p)?; Ok(p) } pub fn ensure_git(p: &Path) -> Result<(), String> { if !p.join(".git").exists() { run_git(p, &["init", "--initial-branch=main"])?; run_git(p, &["config", "user.name", "HoloLake Local"])?; run_git(p, &["config", "user.email", "local@hololake.invalid"])?; } Ok(()) } pub fn git_commit(p: &Path, msg: &str) -> Result, String> { run_git(p, &["add", "."])?; let status = run_git(p, &["status", "--porcelain"])?; if status.trim().is_empty() { return Ok(None); }; run_git(p, &["commit", "-m", msg])?; Ok(Some(run_git(p, &["rev-parse", "HEAD"])?.trim().into())) } fn run_git(p: &Path, args: &[&str]) -> Result { let o = Command::new("git") .current_dir(p) .args(args) .output() .map_err(|e| format!("GIT_UNAVAILABLE: {e}"))?; if !o.status.success() { return Err(format!( "GIT_FAILED: {}", String::from_utf8_lossy(&o.stderr) )); }; Ok(String::from_utf8_lossy(&o.stdout).into_owned()) } pub fn safe_rel(value: &str) -> Result { let p = Path::new(value); if p.is_absolute() || p.components().any(|c| { matches!( c, Component::ParentDir | Component::RootDir | Component::Prefix(_) ) }) { return Err("PATH_OUT_OF_SCOPE".into()); } Ok(p.to_path_buf()) } pub fn list_docs(app: &AppHandle) -> Result, String> { let root = knowledge_root(app)?.join("docs"); let mut out = vec![]; collect(&root, &root, &mut out)?; out.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); Ok(out) } fn collect(root: &Path, p: &Path, out: &mut Vec) -> Result<(), String> { for e in fs::read_dir(p).map_err(|e| e.to_string())? { let e = e.map_err(|e| e.to_string())?; let path = e.path(); if path.is_dir() { collect(root, &path, out)? } else if path.extension().and_then(|x| x.to_str()) == Some("md") { let b = fs::read(&path).map_err(|e| e.to_string())?; let rel = path .strip_prefix(root) .map_err(|e| e.to_string())? .to_string_lossy() .into_owned(); let body = String::from_utf8_lossy(&b); let title = body .lines() .find_map(|x| x.strip_prefix("# ")) .unwrap_or_else(|| { path.file_stem() .and_then(|x| x.to_str()) .unwrap_or("未命名") }) .into(); let updated = e .metadata() .map_err(|e| e.to_string())? .modified() .ok() .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) .map(|d| d.as_secs().to_string()) .unwrap_or_else(|| "0".into()); out.push(KnowledgeSummary { path: rel, title, updated_at: updated, sha256: sha(&b), }) } } Ok(()) } pub fn read_doc(app: &AppHandle, path: &str) -> Result { let rel = safe_rel(path)?; let p = knowledge_root(app)?.join("docs").join(rel); let b = fs::read(&p).map_err(|e| e.to_string())?; let body = String::from_utf8(b.clone()).map_err(|_| "DOCUMENT_NOT_UTF8".to_string())?; let title = body .lines() .find_map(|x| x.strip_prefix("# ")) .unwrap_or_else(|| p.file_stem().and_then(|x| x.to_str()).unwrap_or("未命名")) .into(); Ok(KnowledgeDocument { path: path.into(), title, body, updated_at: now(), sha256: sha(&b), }) } pub fn bridges(app: &AppHandle) -> Result, String> { let p = root(app)?.join("external-ai-bridges.json"); if p.exists() { read_json(&p) } else { Ok(vec![]) } } pub fn save_bridges(app: &AppHandle, v: &[ExternalAiBridge]) -> Result<(), String> { write_json(&root(app)?.join("external-ai-bridges.json"), &v) }