2026-09-03 19:44:19 +08:00
|
|
|
|
mod agent_executor;
|
2026-09-03 21:21:20 +08:00
|
|
|
|
mod enterprise_entrance;
|
2026-09-03 15:35:33 +08:00
|
|
|
|
mod model;
|
2026-09-03 19:44:19 +08:00
|
|
|
|
mod persona_runtime;
|
|
|
|
|
|
mod realtime_bridge;
|
2026-09-03 15:35:33 +08:00
|
|
|
|
mod storage;
|
|
|
|
|
|
|
|
|
|
|
|
use model::*;
|
2026-09-03 19:44:19 +08:00
|
|
|
|
use serde::Serialize;
|
2026-09-03 15:35:33 +08:00
|
|
|
|
use std::{fs, path::Path};
|
2026-09-03 19:44:19 +08:00
|
|
|
|
use tauri::{AppHandle, Manager, State};
|
2026-09-03 15:35:33 +08:00
|
|
|
|
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";
|
|
|
|
|
|
|
2026-09-03 21:24:51 +08:00
|
|
|
|
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}"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 15:35:33 +08:00
|
|
|
|
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)?,
|
2026-09-03 21:24:51 +08:00
|
|
|
|
modules: bundled_modules()?,
|
2026-09-03 15:35:33 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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]
|
2026-09-03 19:44:19 +08:00
|
|
|
|
fn submit_user_message(
|
|
|
|
|
|
app: AppHandle,
|
|
|
|
|
|
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
|
|
|
|
|
|
content: String,
|
|
|
|
|
|
) -> Result<MutationReceipt, String> {
|
2026-09-03 15:35:33 +08:00
|
|
|
|
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)?;
|
2026-09-03 19:44:19 +08:00
|
|
|
|
realtime.broadcast_value(serde_json::json!({"type":"user_message","event":e}));
|
2026-09-03 15:35:33 +08:00
|
|
|
|
let waiting = storage::event(
|
|
|
|
|
|
SourceKind::SystemReceipt,
|
|
|
|
|
|
"系统回执",
|
2026-09-03 19:44:19 +08:00
|
|
|
|
"用户语言已按 USER_MESSAGE 保存并实时发送到已连接的外部 AI;系统不伪造人格回应。",
|
2026-09-03 15:35:33 +08:00
|
|
|
|
"WAITING",
|
|
|
|
|
|
);
|
|
|
|
|
|
receipt(
|
|
|
|
|
|
&app,
|
|
|
|
|
|
"WAITING_PERSONA",
|
2026-09-03 19:44:19 +08:00
|
|
|
|
"用户语言已记录并投递到实时桥。",
|
2026-09-03 15:35:33 +08:00
|
|
|
|
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() {
|
2026-09-03 16:40:56 +08:00
|
|
|
|
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)?;
|
2026-09-03 15:35:33 +08:00
|
|
|
|
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)?;
|
2026-09-03 16:40:56 +08:00
|
|
|
|
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);
|
2026-09-03 15:35:33 +08:00
|
|
|
|
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(),
|
2026-09-03 19:44:19 +08:00
|
|
|
|
state: "GLP_REALTIME_REGISTERED_NO_EXECUTION_AUTHORITY".into(),
|
2026-09-03 15:35:33 +08:00
|
|
|
|
};
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 19:44:19 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 21:21:20 +08:00
|
|
|
|
#[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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 15:35:33 +08:00
|
|
|
|
#[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())
|
2026-09-03 19:44:19 +08:00
|
|
|
|
.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(())
|
|
|
|
|
|
})
|
2026-09-03 15:35:33 +08:00
|
|
|
|
.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,
|
2026-09-03 19:44:19 +08:00
|
|
|
|
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
|
2026-09-03 21:21:20 +08:00
|
|
|
|
,enterprise_entrance_snapshot
|
|
|
|
|
|
,prepare_enterprise_device
|
|
|
|
|
|
,prepare_enterprise_binding
|
|
|
|
|
|
,sign_enterprise_challenge
|
2026-09-03 15:35:33 +08:00
|
|
|
|
])
|
|
|
|
|
|
.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")
|
|
|
|
|
|
}
|
2026-09-03 16:40:56 +08:00
|
|
|
|
#[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")
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-03 15:35:33 +08:00
|
|
|
|
}
|