feat(hololake): establish clean v1 personal language shell
This commit is contained in:
parent
9e04b0354c
commit
c4f662e541
51 changed files with 16233 additions and 1 deletions
244
product-source/hololake-clean-desktop/src-tauri/src/storage.rs
Normal file
244
product-source/hololake-clean-desktop/src-tauri/src/storage.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
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 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<PathBuf, String> {
|
||||
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)
|
||||
}
|
||||
fn read_json<T: serde::de::DeserializeOwned>(p: &Path) -> Result<T, String> {
|
||||
serde_json::from_slice(&fs::read(p).map_err(|e| e.to_string())?).map_err(|e| e.to_string())
|
||||
}
|
||||
fn write_json<T: serde::Serialize>(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<Option<Channel>, 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<String>,
|
||||
content: impl Into<String>,
|
||||
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<Vec<TimelineEvent>, 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::<Vec<_>>();
|
||||
v.reverse();
|
||||
Ok(v)
|
||||
}
|
||||
pub fn knowledge_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
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<PathBuf, String> {
|
||||
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<Option<String>, 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<String, String> {
|
||||
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<PathBuf, String> {
|
||||
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<Vec<KnowledgeSummary>, 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<KnowledgeSummary>) -> 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<KnowledgeDocument, String> {
|
||||
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<Vec<ExternalAiBridge>, 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)
|
||||
}
|
||||
Loading…
Reference in a new issue