hololake-system-architecture/product-source/hololake-clean-desktop/src-tauri/src/lib.rs

642 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

mod agent_executor;
mod enterprise_entrance;
mod model;
mod persona_runtime;
mod realtime_bridge;
mod storage;
use model::*;
use serde::Serialize;
use std::{fs, path::Path};
use tauri::{AppHandle, Manager, State};
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";
pub(crate) fn bundled_modules() -> Result<Vec<ModuleRecord>, String> {
let registry: serde_json::Value =
serde_json::from_str(include_str!("../../registries/module-registry.json"))
.map_err(|error| format!("MODULE_REGISTRY_INVALID: {error}"))?;
serde_json::from_value(registry["modules"].clone())
.map_err(|error| format!("MODULE_REGISTRY_INVALID: {error}"))
}
fn receipt(
app: &AppHandle,
state: &str,
message: &str,
git: Option<String>,
event: TimelineEvent,
) -> Result<MutationReceipt, String> {
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<SystemSnapshot, String> {
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)?,
modules: bundled_modules()?,
})
}
#[tauri::command]
fn create_channel(app: AppHandle, name: String) -> Result<Channel, String> {
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,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
content: String,
) -> Result<MutationReceipt, String> {
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)?;
realtime.broadcast_value(serde_json::json!({"type":"user_message","event":e}));
let waiting = storage::event(
SourceKind::SystemReceipt,
"系统回执",
"用户语言已按 USER_MESSAGE 保存并实时发送到已连接的外部 AI系统不伪造人格回应。",
"WAITING",
);
receipt(
&app,
"WAITING_PERSONA",
"用户语言已记录并投递到实时桥。",
None,
waiting,
)
}
fn clean_title(title: &str) -> Result<String, String> {
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<KnowledgeDocument, String> {
storage::read_doc(&app, &path)
}
#[tauri::command]
fn create_knowledge_document(app: AppHandle, title: String) -> Result<MutationReceipt, String> {
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<MutationReceipt, String> {
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<MutationReceipt, String> {
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<Option<MutationReceipt>, 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<Option<String>, 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<ExternalAiBridge, String> {
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: "GLP_REALTIME_REGISTERED_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)
}
fn ensure_default_external_ai_bridge(app: &AppHandle) -> Result<(), String> {
let mut bridges = storage::bridges(app)?;
if bridges
.iter()
.any(|bridge| bridge.bridge_id == "HLP-BRIDGE-LOCAL-DEFAULT")
{
return Ok(());
}
let root = storage::root(app)?
.join("external-ai")
.join("HLP-BRIDGE-LOCAL-DEFAULT");
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())?;
bridges.push(ExternalAiBridge {
bridge_id: "HLP-BRIDGE-LOCAL-DEFAULT".into(),
display_name: "本机编程 AI".into(),
inbox_path: inbox.to_string_lossy().into_owned(),
outbox_path: outbox.to_string_lossy().into_owned(),
state: "GLP_REALTIME_REGISTERED_NO_EXECUTION_AUTHORITY".into(),
});
storage::save_bridges(app, &bridges)
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RuntimeOverview {
realtime: realtime_bridge::RealtimeBridgeStatus,
persona: persona_runtime::PersonaRuntimeSnapshot,
proposals: Vec<agent_executor::AgentProposal>,
}
#[tauri::command]
fn get_runtime_overview(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
) -> Result<RuntimeOverview, String> {
Ok(RuntimeOverview {
realtime: realtime.status(),
persona: persona_runtime::snapshot(&app)?,
proposals: agent_executor::list(&app)?,
})
}
#[tauri::command]
fn get_realtime_invitation(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
) -> Result<realtime_bridge::RealtimeInvitation, String> {
realtime_bridge::invitation(&app, &realtime)
}
#[tauri::command]
fn register_public_persona(
app: AppHandle,
display_name: String,
) -> Result<persona_runtime::PublicPersona, String> {
persona_runtime::register(&app, display_name)
}
#[tauri::command]
fn delete_trial_persona(
app: AppHandle,
persona_id: String,
exact_confirmation: String,
) -> Result<persona_runtime::PersonaRuntimeSnapshot, String> {
persona_runtime::delete_trial(&app, &persona_id, &exact_confirmation)
}
#[tauri::command]
fn compile_tcs_agent_proposal(
app: AppHandle,
request: agent_executor::TcsCompileRequest,
) -> Result<agent_executor::AgentProposal, String> {
agent_executor::queue(&app, request, "HOLOLAKE_LOCAL_UI".into(), None)
}
#[tauri::command]
fn approve_agent_proposal(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
proposal_id: String,
) -> Result<agent_executor::AgentReceipt, String> {
let receipt = agent_executor::approve(&app, &proposal_id)?;
realtime.broadcast_value(serde_json::json!({"type":"agent_receipt","receipt":receipt}));
Ok(receipt)
}
#[tauri::command]
fn reject_agent_proposal(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
proposal_id: String,
) -> Result<agent_executor::AgentProposal, String> {
let proposal = agent_executor::reject(&app, &proposal_id)?;
realtime
.broadcast_value(serde_json::json!({"type":"proposal_rejected","proposalId":proposal_id}));
Ok(proposal)
}
#[tauri::command]
fn enterprise_entrance_snapshot(
app: AppHandle,
) -> Result<enterprise_entrance::EnterpriseEntranceSnapshot, String> {
enterprise_entrance::snapshot(&app)
}
#[tauri::command]
fn prepare_enterprise_device(
app: AppHandle,
) -> Result<enterprise_entrance::EnterpriseDeviceProof, String> {
enterprise_entrance::prepare_device(&app)
}
#[tauri::command]
fn prepare_enterprise_binding(
app: AppHandle,
domain_id: String,
responsible_human_id: String,
persona_id: String,
) -> Result<enterprise_entrance::EnterpriseBindingRequest, String> {
enterprise_entrance::prepare_binding(&app, &domain_id, &responsible_human_id, &persona_id)
}
#[tauri::command]
fn sign_enterprise_challenge(
app: AppHandle,
request_id: String,
challenge_id: String,
nonce: String,
expires_unix_ms: u128,
) -> Result<enterprise_entrance::EnterpriseChallengeProof, String> {
enterprise_entrance::sign_challenge(&app, &request_id, &challenge_id, &nonce, expires_unix_ms)
}
#[tauri::command]
fn enroll_enterprise_device(
app: AppHandle,
username: String,
password: String,
) -> Result<serde_json::Value, String> {
enterprise_entrance::enroll_device(&app, &username, &password)
}
#[tauri::command]
fn authenticate_enterprise_entrance(
app: AppHandle,
) -> Result<enterprise_entrance::EnterpriseSession, String> {
enterprise_entrance::authenticate(&app)
}
#[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())
.setup(|app| {
let diagnostic = storage::root(app.handle())
.map_err(std::io::Error::other)?
.join("startup-runtime-diagnostic.json");
storage::write_json(
&diagnostic,
&serde_json::json!({"state":"SETUP_ENTERED","recordedAt":storage::now()}),
)
.map_err(std::io::Error::other)?;
if std::env::var_os("HOLOLAKE_RUNTIME_TEST_ROOT").is_some() {
if let Ok(channel_name) = std::env::var("HOLOLAKE_TEST_CHANNEL_NAME") {
create_channel(app.handle().clone(), channel_name)
.map_err(std::io::Error::other)?;
}
if let Ok(display_name) = std::env::var("HOLOLAKE_TEST_PERSONA_NAME") {
persona_runtime::register(app.handle(), display_name)
.map_err(std::io::Error::other)?;
}
}
ensure_default_external_ai_bridge(app.handle()).map_err(std::io::Error::other)?;
let state = match realtime_bridge::start(app.handle().clone()) {
Ok(state) => state,
Err(error) => {
let _ = storage::write_json(
&diagnostic,
&serde_json::json!({"state":"LISTENER_FAILED","error":error.clone(),"recordedAt":storage::now()}),
);
return Err(std::io::Error::other(error).into());
}
};
storage::write_json(
&diagnostic,
&serde_json::json!({"state":"LISTENER_BOUND","status":state.status(),"recordedAt":storage::now()}),
)
.map_err(std::io::Error::other)?;
let invitation =
realtime_bridge::invitation(app.handle(), &state).map_err(std::io::Error::other)?;
storage::write_json(
&storage::root(app.handle())
.map_err(std::io::Error::other)?
.join("startup-runtime-receipt.json"),
&serde_json::json!({
"schema":"hololake.startup-runtime-receipt/v1",
"state":"GLP_LISTENING",
"protocol":invitation.protocol,
"endpoint":invitation.endpoint,
"recordedAt":storage::now()
}),
)
.map_err(std::io::Error::other)?;
app.manage(state);
Ok(())
})
.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,
get_runtime_overview,
get_realtime_invitation,
register_public_persona,
delete_trial_persona,
compile_tcs_agent_proposal,
approve_agent_proposal,
reject_agent_proposal,
enterprise_entrance_snapshot,
prepare_enterprise_device,
prepare_enterprise_binding,
sign_enterprise_challenge,
enroll_enterprise_device,
authenticate_enterprise_entrance
])
.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::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(imported.len(), 1);
assert_eq!(
imported[0]
.path()
.extension()
.and_then(|value| value.to_str()),
Some("md")
);
}
}