mod model; mod storage; use model::*; use std::{fs, path::Path}; use tauri::AppHandle; use tauri_plugin_dialog::DialogExt; const UPDATE_ENDPOINT: &str = "https://guanghulab.com/hololake/releases/latest.json"; const PUBLIC_STATE: &str = "SIGNED_UPDATE_FEED_READY_NO_RELEASE"; fn receipt( app: &AppHandle, state: &str, message: &str, git: Option, event: TimelineEvent, ) -> Result { storage::append_event(app, &event)?; Ok(MutationReceipt { receipt_id: storage::id("HL-RCP"), state: state.into(), message: message.into(), git_commit: git, event, }) } #[tauri::command] fn system_snapshot(app: AppHandle) -> Result { Ok(SystemSnapshot { app_version: env!("CARGO_PKG_VERSION").into(), channel: storage::channel(&app)?, timeline: storage::events(&app)?, documents: storage::list_docs(&app)?, module_registry_version: "1".into(), public_update_endpoint: UPDATE_ENDPOINT.into(), public_distribution_state: PUBLIC_STATE.into(), external_ai_bridges: storage::bridges(&app)?, }) } #[tauri::command] fn create_channel(app: AppHandle, name: String) -> Result { let name = name.trim(); if name.is_empty() || name.chars().count() > 48 { return Err("CHANNEL_NAME_INVALID".into()); } if storage::channel(&app)?.is_some() { return Err("CHANNEL_ALREADY_EXISTS".into()); } let repo = storage::private_repo_root(&app)?; let c = Channel { channel_id: format!( "HL-CH-{}", uuid::Uuid::new_v4().simple().to_string()[..8].to_uppercase() ), name: name.into(), created_at: storage::now(), private_git_path: repo.to_string_lossy().into_owned(), public_distribution_state: PUBLIC_STATE.into(), }; storage::save_channel(&app, &c)?; let e = storage::event( SourceKind::SystemReceipt, "频道已建立", format!("{} 已登记;私人 Git 已初始化。", c.channel_id), "SUCCEEDED", ); storage::append_event(&app, &e)?; storage::git_commit( &repo, "chore(channel): initialize personal HoloLake channel", )?; Ok(c) } #[tauri::command] fn submit_user_message(app: AppHandle, content: String) -> Result { let content = content.trim(); if content.is_empty() { return Err("EMPTY_USER_MESSAGE".into()); } if storage::channel(&app)?.is_none() { return Err("CHANNEL_NOT_INITIALIZED".into()); } let e = storage::event(SourceKind::UserMessage, "用户语言", content, "RECORDED"); storage::append_event(&app, &e)?; let waiting = storage::event( SourceKind::SystemReceipt, "系统回执", "用户语言已按 USER_MESSAGE 保存;当前未绑定人格模型,不伪造人格回应。", "WAITING", ); receipt( &app, "WAITING_PERSONA", "用户语言已记录,等待人格体接入。", None, waiting, ) } fn clean_title(title: &str) -> Result { let t = title.trim(); if t.is_empty() || t.chars().count() > 120 { return Err("KNOWLEDGE_TITLE_INVALID".into()); } Ok(t.chars() .map(|c| if "\\/:*?\"<>|".contains(c) { '-' } else { c }) .collect()) } #[tauri::command] fn read_knowledge_document(app: AppHandle, path: String) -> Result { storage::read_doc(&app, &path) } #[tauri::command] fn create_knowledge_document(app: AppHandle, title: String) -> Result { let title = clean_title(&title)?; let root = storage::knowledge_root(&app)?; let rel = format!("{}.md", title); let p = root.join("docs").join(&rel); if p.exists() { return Err("KNOWLEDGE_DOCUMENT_EXISTS".into()); } fs::write(&p, format!("# {}\n\n", title)).map_err(|e| e.to_string())?; let git = storage::git_commit(&root, "feat(knowledge): create document")?; let e = storage::event( SourceKind::AgentAction, "Agent 执行", format!("已创建知识页 {rel}"), "SUCCEEDED", ); receipt(&app, "SUCCEEDED", "知识页已创建。", git, e) } #[tauri::command] fn save_knowledge_document( app: AppHandle, path: String, body: String, ) -> Result { let rel = storage::safe_rel(&path)?; if rel.extension().and_then(|x| x.to_str()) != Some("md") { return Err("KNOWLEDGE_DOCUMENT_TYPE_REJECTED".into()); } let root = storage::knowledge_root(&app)?; let p = root.join("docs").join(rel); if !p.exists() { return Err("KNOWLEDGE_DOCUMENT_NOT_FOUND".into()); } fs::write(&p, body).map_err(|e| e.to_string())?; let git = storage::git_commit(&root, "docs(knowledge): save document")?; let e = storage::event( SourceKind::SystemReceipt, "系统回执", format!("{path} 已写入私人 Git。"), "SUCCEEDED", ); receipt(&app, "SUCCEEDED", "保存完成。", git, e) } #[tauri::command] fn delete_knowledge_document(app: AppHandle, path: String) -> Result { let rel = storage::safe_rel(&path)?; let root = storage::knowledge_root(&app)?; let from = root.join("docs").join(&rel); if !from.exists() { return Err("KNOWLEDGE_DOCUMENT_NOT_FOUND".into()); } let trash = root.join(".trash").join(format!( "{}-{}", storage::id("deleted"), rel.file_name() .and_then(|x| x.to_str()) .unwrap_or("document.md") )); fs::create_dir_all(trash.parent().unwrap()).map_err(|e| e.to_string())?; fs::rename(from, &trash).map_err(|e| e.to_string())?; let git = storage::git_commit(&root, "chore(knowledge): move document to trash")?; let e = storage::event( SourceKind::SystemReceipt, "系统回执", format!("{path} 已移入可恢复回收站。"), "SUCCEEDED", ); receipt(&app, "SUCCEEDED", "已移入回收站。", git, e) } fn copy_import(src: &Path, dst: &Path, count: &mut usize) -> Result<(), String> { if *count >= 1000 { return Err("IMPORT_FILE_LIMIT_EXCEEDED".into()); } for entry in fs::read_dir(src).map_err(|e| e.to_string())? { let entry = entry.map_err(|e| e.to_string())?; let p = entry.path(); if p.file_name() .and_then(|x| x.to_str()) .map(|x| x.starts_with('.')) .unwrap_or(false) { continue; } if p.is_dir() { let folder_name = clean_title( p.file_name() .and_then(|value| value.to_str()) .unwrap_or("未命名分类"), )?; let child = dst.join(folder_name); fs::create_dir_all(&child).map_err(|e| e.to_string())?; copy_import(&p, &child, count)?; continue; } let ext = p .extension() .and_then(|x| x.to_str()) .unwrap_or("") .to_lowercase(); if !["md", "txt", "markdown"].contains(&ext.as_str()) { continue; } let bytes = fs::read(&p).map_err(|e| e.to_string())?; if bytes.len() > 10 * 1024 * 1024 { continue; } let body = String::from_utf8_lossy(&bytes); let title = p.file_stem().and_then(|x| x.to_str()).unwrap_or("导入页面"); let name = format!("{}-{}.md", clean_title(title)?, &storage::sha(&bytes)[..8]); fs::write( dst.join(name), if ext == "md" || ext == "markdown" { body.into_owned() } else { format!("# {}\n\n{}", title, body) }, ) .map_err(|e| e.to_string())?; *count += 1 } Ok(()) } #[tauri::command] async fn import_knowledge_folder(app: AppHandle) -> Result, String> { let Some(folder) = app .dialog() .file() .set_title("导入到光湖知识库") .blocking_pick_folder() else { return Ok(None); }; let src = folder.into_path().map_err(|e| e.to_string())?; let root = storage::knowledge_root(&app)?; let source_name = clean_title( src.file_name() .and_then(|value| value.to_str()) .unwrap_or("外部知识库"), )?; let dst = root.join("docs").join("导入").join(source_name); fs::create_dir_all(&dst).map_err(|e| e.to_string())?; let mut count = 0; copy_import(&src, &dst, &mut count)?; let git = storage::git_commit(&root, "feat(knowledge): import external folder")?; let protocol = storage::event( SourceKind::ProtocolEvent, "协议运行", format!("导入边界校验完成;接受 Markdown/纯文本,共 {count} 页。原目录未改写。"), "SUCCEEDED", ); storage::append_event(&app, &protocol)?; let e = storage::event( SourceKind::SystemReceipt, "系统回执", format!("外部文件夹已转换为 {count} 个光湖知识页。"), "SUCCEEDED", ); Ok(Some(receipt( &app, "SUCCEEDED", "文件夹导入完成。", git, e, )?)) } #[tauri::command] async fn export_knowledge_document(app: AppHandle, path: String) -> Result, String> { let doc = storage::read_doc(&app, &path)?; let Some(target) = app .dialog() .file() .set_title("下载知识页") .set_file_name(&path) .blocking_save_file() else { return Ok(None); }; let p = target.into_path().map_err(|e| e.to_string())?; fs::write(&p, doc.body).map_err(|e| e.to_string())?; let e = storage::event( SourceKind::ToolResult, "工具结果", format!("知识页已下载到 {}", p.display()), "SUCCEEDED", ); storage::append_event(&app, &e)?; Ok(Some(p.to_string_lossy().into_owned())) } #[tauri::command] fn register_external_ai_bridge( app: AppHandle, display_name: String, ) -> Result { let name = display_name.trim(); if name.is_empty() || name.chars().count() > 64 { return Err("EXTERNAL_AI_NAME_INVALID".into()); } let bridge_id = format!( "HL-AI-{}", uuid::Uuid::new_v4().simple().to_string()[..8].to_uppercase() ); let root = storage::root(&app)?.join("external-ai").join(&bridge_id); let inbox = root.join("inbox"); let outbox = root.join("outbox"); fs::create_dir_all(&inbox).map_err(|e| e.to_string())?; fs::create_dir_all(&outbox).map_err(|e| e.to_string())?; let bridge = ExternalAiBridge { bridge_id, display_name: name.into(), inbox_path: inbox.to_string_lossy().into_owned(), outbox_path: outbox.to_string_lossy().into_owned(), state: "LOCAL_EXPRESSION_ONLY_NO_EXECUTION_AUTHORITY".into(), }; let mut all = storage::bridges(&app)?; all.push(bridge.clone()); storage::save_bridges(&app, &all)?; let e = storage::event( SourceKind::SystemReceipt, "外部 AI 入口已建立", format!( "{} 仅获得来源明确的语言收发目录,不获得人格与执行权限。", bridge.bridge_id ), "SUCCEEDED", ); storage::append_event(&app, &e)?; Ok(bridge) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .invoke_handler(tauri::generate_handler![ system_snapshot, create_channel, submit_user_message, read_knowledge_document, create_knowledge_document, save_knowledge_document, delete_knowledge_document, import_knowledge_folder, export_knowledge_document, register_external_ai_bridge ]) .run(tauri::generate_context!()) .expect("HoloLake runtime failed") } #[cfg(test)] mod tests { use super::*; #[test] fn title_rejects_empty() { assert!(clean_title(" ").is_err()) } #[test] fn title_sanitizes_paths() { assert_eq!(clean_title("a/b").unwrap(), "a-b") } #[test] fn folder_import_preserves_classification_tree() { let source = tempfile::tempdir().unwrap(); let destination = tempfile::tempdir().unwrap(); let category = source.path().join("产品架构"); fs::create_dir_all(&category).unwrap(); fs::write(category.join("决定.txt"), "分类内容").unwrap(); let mut count = 0; copy_import(source.path(), destination.path(), &mut count).unwrap(); assert_eq!(count, 1); let imported = fs::read_dir(destination.path().join("产品架构")) .unwrap() .collect::, _>>() .unwrap(); assert_eq!(imported.len(), 1); assert_eq!( imported[0] .path() .extension() .and_then(|value| value.to_str()), Some("md") ); } }