feat(hololake): establish clean v1 personal language shell

This commit is contained in:
冰朔 2026-09-03 15:35:33 +08:00
commit c4f662e541
51 changed files with 16233 additions and 1 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
[package]
name = "hololake-clean-desktop"
version = "1.0.0"
description = "HoloLake clean personal language operating system shell"
authors = ["HoloLake"]
license = "AGPL-3.0-or-later"
edition = "2021"
rust-version = "1.77.2"
[lib]
name = "hololake_clean_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "=2.5.5", features = [] }
[dependencies]
tauri = { version = "=2.10.2", features = [] }
tauri-runtime = "=2.10.0"
tauri-runtime-wry = "=2.10.0"
tauri-plugin-dialog = "=2.7.2"
tauri-plugin-process = "=2.3.1"
tauri-plugin-updater = "=2.10.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
tempfile = "3"

View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "HoloLake clean shell: one typed IPC boundary plus signed update and native dialogs.",
"windows": ["main"],
"permissions": ["core:default", "dialog:default", "updater:default", "process:allow-restart"]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"default":{"identifier":"default","description":"HoloLake clean shell: one typed IPC boundary plus signed update and native dialogs.","local":true,"windows":["main"],"permissions":["core:default","dialog:default","updater:default","process:allow-restart"]}}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -0,0 +1,370 @@
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<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)?,
})
}
#[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, 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)?;
let waiting = storage::event(
SourceKind::SystemReceipt,
"系统回执",
"用户语言已按 USER_MESSAGE 保存;当前未绑定人格模型,不伪造人格回应。",
"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() {
copy_import(&p, dst, 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 dst = root.join("docs").join("导入");
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: "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")
}
}

View file

@ -0,0 +1,4 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
hololake_clean_desktop_lib::run();
}

View file

@ -0,0 +1,86 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Channel {
pub channel_id: String,
pub name: String,
pub created_at: String,
pub private_git_path: String,
pub public_distribution_state: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SourceKind {
UserMessage,
PersonaResponse,
SystemContext,
ProtocolEvent,
AgentAction,
ToolResult,
SystemReceipt,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimelineEvent {
pub event_id: String,
pub source_kind: SourceKind,
pub title: String,
pub content: String,
pub occurred_at: String,
pub state: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeSummary {
pub path: String,
pub title: String,
pub updated_at: String,
pub sha256: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeDocument {
pub path: String,
pub title: String,
pub body: String,
pub updated_at: String,
pub sha256: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExternalAiBridge {
pub bridge_id: String,
pub display_name: String,
pub inbox_path: String,
pub outbox_path: String,
pub state: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemSnapshot {
pub app_version: String,
pub channel: Option<Channel>,
pub timeline: Vec<TimelineEvent>,
pub documents: Vec<KnowledgeSummary>,
pub module_registry_version: String,
pub public_update_endpoint: String,
pub public_distribution_state: String,
pub external_ai_bridges: Vec<ExternalAiBridge>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MutationReceipt {
pub receipt_id: String,
pub state: String,
pub message: String,
pub git_commit: Option<String>,
pub event: TimelineEvent,
}

View 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)
}

View file

@ -0,0 +1,13 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HoloLake",
"version": "1.0.0",
"identifier": "world.guanghu.hololake",
"build": { "frontendDist": "../dist", "devUrl": "http://127.0.0.1:5211", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build" },
"app": {
"windows": [{ "label": "main", "title": "HoloLake", "width": 1320, "height": 860, "minWidth": 760, "minHeight": 560, "center": true }],
"security": { "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://guanghulab.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'" }
},
"plugins": { "updater": { "endpoints": ["https://guanghulab.com/hololake/releases/latest.json"], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDEzQkYzRTQ5QTE2MEFDMzEKUldReHJHQ2hTVDYvRTZNNDVqZDUxLzRZMmxuT1pSM2Q5RTRZbzRCZUZ0d0FxVVNidGJ0dUQyMnoK" } },
"bundle": { "active": true, "targets": ["app", "dmg"], "createUpdaterArtifacts": true, "category": "Productivity", "icon": ["icons/icon.icns", "icons/icon.png", "icons/icon.ico"], "macOS": { "signingIdentity": "Developer ID Application: bei sun (825A9L3G7Q)" } }
}