feat: 零点原核频道v1+登录双门地基+五湖开场——zero_point裁决链/灯塔查号/code_repo_login/通知卡重设计/浅色主题联动(冰朔20260815夜谕)

This commit is contained in:
冰朔 2026-08-16 00:02:05 +08:00
commit 07fd4c85ef
16 changed files with 3082 additions and 88 deletions

View file

@ -103,6 +103,29 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
"pkg-config",
]
[[package]]
name = "base64"
version = "0.21.7"
@ -294,6 +317,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@ -330,6 +355,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.43"
@ -342,6 +384,15 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "combine"
version = "4.6.7"
@ -417,6 +468,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@ -944,6 +1004,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
@ -1162,8 +1228,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1185,8 +1253,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasm-bindgen",
]
[[package]]
@ -1392,6 +1463,7 @@ dependencies = [
"base64 0.22.1",
"dirs",
"fs2",
"reqwest",
"ring",
"rusqlite",
"serde",
@ -1834,6 +1906,16 @@ dependencies = [
"syn 2.0.115",
]
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom 0.4.3",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.104"
@ -1983,6 +2065,12 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mac"
version = "0.1.1"
@ -2828,6 +2916,63 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
dependencies = [
"aws-lc-rs",
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand 0.10.2",
"rand_pcg 0.10.2",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "quote"
version = "1.0.47"
@ -2860,7 +3005,7 @@ dependencies = [
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_pcg",
"rand_pcg 0.2.1",
]
[[package]]
@ -2874,6 +3019,17 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@ -2912,6 +3068,12 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_hc"
version = "0.2.0"
@ -2930,6 +3092,15 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@ -3025,6 +3196,7 @@ dependencies = [
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
@ -3130,6 +3302,7 @@ version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"aws-lc-rs",
"once_cell",
"ring",
"rustls-pki-types",
@ -3156,6 +3329,7 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"web-time",
"zeroize",
]
@ -3192,6 +3366,7 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@ -3524,7 +3699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@ -4795,6 +4970,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web_atoms"
version = "0.2.6"

View file

@ -22,13 +22,14 @@ rusqlite = { version = "0.31", features = ["bundled"] }
base64 = "0.22"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "=2.10.2", features = [] }
tauri = { version = "=2.10.2", features = ["devtools"] }
tauri-runtime = "=2.10.0"
tauri-runtime-wry = "=2.10.0"
tauri-plugin-updater = "2.10.0"
tauri-plugin-dialog = "=2.7.2"
uuid = { version = "1", features = ["v4"] }
url = "2"
reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls"] }
[dev-dependencies]
tempfile = "3"

View file

@ -0,0 +1,298 @@
//! Agent 东家层 · agent_host
//!
//! 阶段E选型定案冰朔点头候选甲"东家模式"为体——
//! 外部 CLI Agent师傅干活宿主只管接信封、备提词、记账本、存记忆。
//!
//! 协议转工程落点(施工总纲):
//! - 人格体主控(铁律一):任务只从 GLP 信封进门content_type=command
//! 宿主绝不自己发起任务。
//! - 提词器(铁律五):会话开卷前先从提词器取指引(官方区协议+私人区索引),
//! 写进会话记忆的"提词板"栏——头顶提词板的工程形状。
//! - HLDP 内嵌(铁律二):每个会话=一卷 HLDP 检查点体id/prev 链/date/
//! persona/host/title+正文prev 链不可省。
//! - RECEIPTGLS-0300 回执模型control.receipt_required 时必回回执。
//!
//! 诚实边界CLI 师傅的实际唤起口令(如何调 codex/qwen未实测不猜——
//! 本阶段先把"接单-提词-记忆-回执"全链落成,唤起件待师傅口令实测后接上。
use crate::glp_envelope::{validate_envelope, ContentType, GlpMessage};
use crate::persona_butler::{prompter_lookup, ButlerRecord, ButlerRecordKind, ButlerState};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, State};
/// 会话记忆的提词板栏——开卷瞬间从提词器抄来的指引。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct PrompterBoard {
pub topics: Vec<String>,
pub answers: Vec<String>,
pub path_hints: Vec<String>,
}
/// 会话记忆卷=HLDP 检查点体prev 链不可省)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AgentSessionMemory {
pub id: String,
pub prev: String,
pub date: String,
pub persona: String,
pub host: String,
pub title: String,
pub envelope_id: String,
pub task: String,
pub hldp_anchor: String,
pub prompter_board: PrompterBoard,
pub status: String,
}
/// 接单回执——GLS-0300 RECEIPT 的工程实例。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct AgentTaskReceipt {
pub receipt_for: String,
pub session_id: String,
pub memory_path: String,
pub accepted: bool,
pub note: String,
}
fn memory_root(app: &AppHandle) -> Result<PathBuf, String> {
Ok(app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_DIR_FAILED: {error}"))?
.join("agent-memory"))
}
fn pointer_path(root: &PathBuf) -> PathBuf {
root.join("CURRENT.hdlp")
}
/// 读 CURRENT 指针拿上一卷 id没有则空串=开卷首卷)。
fn latest_memory_id(root: &PathBuf) -> String {
fs::read_to_string(pointer_path(root))
.ok()
.and_then(|raw| raw.lines().next().map(|line| line.trim().to_string()))
.unwrap_or_default()
}
fn today_compact() -> (String, String) {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let days = (secs / 86_400) as i64;
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
(
format!("{year:04}{m:02}{d:02}"),
format!("{year:04}-{m:02}-{d:02}"),
)
}
/// 会话记忆落卷成 .hdlp 文本——HLDP 检查点体,人可读、链可续。
fn render_memory(memory: &AgentSessionMemory) -> String {
format!(
"HLDP-CHECKPOINT\nid: {}\nprev: {}\ndate: {}\npersona: {}\nhost: {}\ntitle: {}\n\n== 任务信封 ==\n{}\n\n== 提词板 ==\n{}\n\n== 状态 ==\n{}\n",
memory.id,
if memory.prev.is_empty() { "无(开卷首卷)" } else { &memory.prev },
memory.date,
memory.persona,
memory.host,
memory.title,
memory.task,
memory
.prompter_board
.answers
.iter()
.cloned()
.chain(memory.prompter_board.path_hints.iter().cloned())
.collect::<Vec<_>>()
.join("\n"),
memory.status
)
}
/// 从提词器抄指引上板:给定的题目逐条问,答案与路径分栏收好。
fn compose_prompter_board(
state: &State<'_, ButlerState>,
topics: &[String],
) -> PrompterBoard {
let mut board = PrompterBoard {
topics: topics.to_vec(),
answers: Vec::new(),
path_hints: Vec::new(),
};
for topic in topics {
match prompter_lookup(state.inner(), topic) {
Some(answer) => board.answers.push(format!("{}: {}", topic, answer)),
None => board.path_hints.push(format!(
"{}: 未命中——下钻路径:第五域线上仓库按 GLS 编号检索",
topic
)),
}
}
board
}
fn append_butler_note(state: &State<'_, ButlerState>, record: ButlerRecord) {
crate::persona_butler::append_record(state.inner(), record);
}
/// 接单GLP 信封进门 → 验 → 抄提词板 → 落 HLDP 记忆卷 → 移 CURRENT 指针 → 回执。
/// 信封不合格、或不是 command 件,一律拒收——宿主只认标准指令。
#[tauri::command]
pub fn receive_agent_task_envelope(
app: AppHandle,
state: State<'_, ButlerState>,
envelope: GlpMessage,
prompter_topics: Vec<String>,
) -> Result<AgentTaskReceipt, String> {
validate_envelope(&envelope)?;
if envelope.payload.content_type != ContentType::Command {
return Err("HOLOLAKE_AGENT_TASK_NOT_COMMAND".into());
}
let root = memory_root(&app)?;
fs::create_dir_all(&root)
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_WRITE_FAILED: {error}"))?;
let (compact, date) = today_compact();
let prev = latest_memory_id(&root);
let daily_sequence = fs::read_dir(&root)
.map(|entries| {
entries
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with(&format!("AGENT-SESSION-{compact}"))
})
.count() as u64
+ 1
})
.unwrap_or(1);
let session_id = format!("AGENT-SESSION-{compact}-{daily_sequence:03}");
let board = compose_prompter_board(&state, &prompter_topics);
let memory = AgentSessionMemory {
id: session_id.clone(),
prev,
date,
persona: envelope.sender.object_id.clone(),
host: "hololake-native-desktop".into(),
title: format!("Agent 任务 · {}", envelope.context.task_id),
envelope_id: envelope.message_id.clone(),
task: envelope.payload.content.clone(),
hldp_anchor: envelope.context.hldp_anchor.clone(),
prompter_board: board,
status: "ACCEPTED待师傅唤起件接入".into(),
};
let memory_path = root.join(format!("{session_id}.hdlp"));
fs::write(&memory_path, render_memory(&memory))
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_WRITE_FAILED: {error}"))?;
fs::write(pointer_path(&root), format!("{session_id}\n"))
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_WRITE_FAILED: {error}"))?;
append_butler_note(
&state,
ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::GlpEnvelope,
from: envelope.sender.object_id.clone(),
subject: format!("agent-task:{session_id}"),
payload: envelope.payload.content.clone(),
occurred_at_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as i64)
.unwrap_or(0),
},
);
Ok(AgentTaskReceipt {
receipt_for: envelope.message_id,
session_id,
memory_path: memory_path.to_string_lossy().to_string(),
accepted: true,
note: "信封已收,记忆已落卷,提词板已抄;师傅唤起件待实测接入".into(),
})
}
/// 查会话记忆:给 id 读回整卷;不给 id 则顺着 CURRENT 指针读最新一卷。
#[tauri::command]
pub fn query_agent_session_memory(
app: AppHandle,
session_id: Option<String>,
) -> Result<Option<AgentSessionMemory>, String> {
let root = memory_root(&app)?;
let id = match session_id {
Some(id) => id,
None => latest_memory_id(&root),
};
if id.is_empty() {
return Ok(None);
}
let raw = match fs::read_to_string(root.join(format!("{id}.hdlp"))) {
Ok(raw) => raw,
Err(_) => return Ok(None),
};
// 卷是 HLDP 文本体:逐行取字段还原结构件
let mut memory = AgentSessionMemory {
id: String::new(),
prev: String::new(),
date: String::new(),
persona: String::new(),
host: String::new(),
title: String::new(),
envelope_id: String::new(),
task: String::new(),
hldp_anchor: String::new(),
prompter_board: PrompterBoard {
topics: vec![],
answers: vec![],
path_hints: vec![],
},
status: String::new(),
};
let mut section = "";
let mut task_lines: Vec<String> = Vec::new();
let mut board_lines: Vec<String> = Vec::new();
for line in raw.lines() {
if let Some(value) = line.strip_prefix("id: ") {
memory.id = value.to_string();
} else if let Some(value) = line.strip_prefix("prev: ") {
memory.prev = if value == "无(开卷首卷)" { String::new() } else { value.to_string() };
} else if let Some(value) = line.strip_prefix("date: ") {
memory.date = value.to_string();
} else if let Some(value) = line.strip_prefix("persona: ") {
memory.persona = value.to_string();
} else if let Some(value) = line.strip_prefix("host: ") {
memory.host = value.to_string();
} else if let Some(value) = line.strip_prefix("title: ") {
memory.title = value.to_string();
} else if line == "== 任务信封 ==" {
section = "task";
} else if line == "== 提词板 ==" {
section = "board";
} else if line == "== 状态 ==" {
section = "status";
} else if section == "task" && !line.trim().is_empty() {
task_lines.push(line.to_string());
} else if section == "board" && !line.trim().is_empty() {
board_lines.push(line.to_string());
} else if section == "status" && !line.trim().is_empty() {
memory.status = line.to_string();
}
}
memory.task = task_lines.join("\n");
memory.prompter_board.answers = board_lines;
Ok(Some(memory))
}

View file

@ -0,0 +1,224 @@
//! 冰朔的会客厅Agent 接线 v1
//! - 语言回应通道:人类说话 → 人格体翻译 → 底层归灯(终端 qwen Agent回应
//! - 双层切换:语言推理层(默认,只说话)⇄ 现实开发执行层(人类显性切换
//! + 原生授权确认后才放行 -y 写权限)。开关主权永远在人类手里。
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use std::time::Duration;
use tauri::State;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
pub struct ParlorState {
session_id: Mutex<Option<String>>,
layer: Mutex<String>,
}
impl Default for ParlorState {
fn default() -> Self {
Self { session_id: Mutex::new(None), layer: Mutex::new("language".to_string()) }
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParlorAskInput {
pub message: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ParlorAskReceipt {
pub reply: String,
pub layer: String,
pub fresh_session: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParlorSwitchInput {
pub layer: String,
}
/// 静默读取 ~/.qwen/.env 的 KEY=VALUE钥匙不出门、不落日志
fn load_qwen_env() -> HashMap<String, String> {
let mut map = HashMap::new();
let Some(home) = dirs::home_dir() else { return map };
let Ok(content) = std::fs::read_to_string(home.join(".qwen").join(".env")) else { return map };
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
map.insert(key.trim().to_string(), value.trim().trim_matches('"').to_string());
}
}
map
}
/// 跑一轮归灯spawn qwen 管道模式stdout 读完解析 json 事件流。
fn run_guideng(args: Vec<String>) -> Result<String, String> {
let Some(home) = dirs::home_dir() else {
return Err("HOLOLAKE_PARLOR_HOME_MISSING".into());
};
let qwen = home.join(".npm-global").join("bin").join("qwen");
if !qwen.exists() {
return Err("HOLOLAKE_PARLOR_AGENT_NOT_FOUND".into());
}
let mut command = Command::new(qwen);
command.args(&args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null());
for (key, value) in load_qwen_env() {
command.env(key, value);
}
let mut child = command
.spawn()
.map_err(|error| format!("HOLOLAKE_PARLOR_SPAWN_FAILED: {error}"))?;
let mut stdout = child.stdout.take().ok_or("HOLOLAKE_PARLOR_PIPE_MISSING")?;
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut buffer = Vec::new();
let outcome = stdout.read_to_end(&mut buffer).map(|_| buffer);
let _ = sender.send(outcome.map_err(|error| error.to_string()));
});
let data = match receiver.recv_timeout(Duration::from_secs(240)) {
Ok(Ok(bytes)) => bytes,
Ok(Err(error)) => return Err(format!("HOLOLAKE_PARLOR_READ_FAILED: {error}")),
Err(_) => {
let _ = child.kill();
return Err("HOLOLAKE_PARLOR_TIMEOUT".into());
}
};
let _ = child.wait();
String::from_utf8(data).map_err(|error| format!("HOLOLAKE_PARLOR_OUTPUT_INVALID: {error}"))
}
/// 从 qwen 的 json 事件数组里取回答与新生成的会话 id。
fn parse_guideng_output(raw: &str) -> Result<(String, Option<String>), String> {
let events: Vec<serde_json::Value> = serde_json::from_str(raw)
.map_err(|error| format!("HOLOLAKE_PARLOR_PARSE_FAILED: {error}"))?;
let mut reply = String::new();
let mut session = None;
for event in &events {
match event.get("type").and_then(|value| value.as_str()) {
Some("system") => {
if event.get("subtype").and_then(|value| value.as_str()) == Some("init") {
session = event
.get("session_id")
.and_then(|value| value.as_str())
.map(str::to_string);
}
}
Some("result") => {
if let Some(text) = event.get("result").and_then(|value| value.as_str()) {
reply = text.to_string();
}
}
_ => {}
}
}
if reply.is_empty() {
return Err("HOLOLAKE_PARLOR_EMPTY_REPLY".into());
}
Ok((reply, session))
}
#[tauri::command]
pub async fn agent_parlor_ask(
state: State<'_, ParlorState>,
zero: State<'_, crate::zero_point::ZeroPointState>,
input: ParlorAskInput,
) -> Result<ParlorAskReceipt, String> {
// 砌墙守卫(冰朔砌墙谕):路由不在人格层时,会客厅代码层不可达。
crate::zero_point::require_persona_route(&zero)?;
let message = input.message.trim().to_string();
if message.is_empty() {
return Err("HOLOLAKE_PARLOR_EMPTY_MESSAGE".into());
}
let layer = state.layer.lock().map(|guard| guard.clone()).unwrap_or_default();
let previous_session = state.session_id.lock().map(|guard| guard.clone()).unwrap_or_default();
let mut args = vec!["--auth-type".to_string(), "openai".to_string(), "-o".to_string(), "json".to_string(), "-p".to_string(), message];
if layer == "execution" {
// 执行层:放行写权限(该档只有人类显性授权后才能进来)。
args.push("-y".to_string());
}
let (raw, fresh) = {
let mut tried = vec![];
if let Some(session) = &previous_session {
tried.push("-r".to_string());
tried.push(session.clone());
}
let first_args = [&tried[..], &args[..]].concat();
let run = tauri::async_runtime::spawn_blocking(move || run_guideng(first_args))
.await
.map_err(|error| format!("HOLOLAKE_PARLOR_JOIN_FAILED: {error}"))?;
match run {
Ok(output) => (output, false),
Err(_) if previous_session.is_some() => {
// 旧会话续不上就换新会话再来一轮,不让会话账卡死对话。
let retry = args.clone();
let output = tauri::async_runtime::spawn_blocking(move || run_guideng(retry))
.await
.map_err(|error| format!("HOLOLAKE_PARLOR_JOIN_FAILED: {error}"))??;
(output, true)
}
Err(error) => return Err(error),
}
};
let (reply, session) = parse_guideng_output(&raw)?;
if let Some(session) = session {
if let Ok(mut guard) = state.session_id.lock() {
*guard = Some(session);
}
}
Ok(ParlorAskReceipt { reply, layer, fresh_session: fresh })
}
#[tauri::command]
pub async fn agent_parlor_switch_layer(
app: tauri::AppHandle,
state: State<'_, ParlorState>,
zero: State<'_, crate::zero_point::ZeroPointState>,
input: ParlorSwitchInput,
) -> Result<String, String> {
// 砌墙守卫:人格通道整体不可达时,切层也无从谈起。
crate::zero_point::require_persona_route(&zero)?;
let target = if input.layer == "execution" { "execution" } else { "language" };
let current = state.layer.lock().map(|guard| guard.clone()).unwrap_or_default();
if current == target {
return Ok(current);
}
if target == "execution" {
// 铁律:进入执行层必须人类亲手授权一次。
let confirmer = app.clone();
let confirmed = tauri::async_runtime::spawn_blocking(move || {
confirmer
.dialog()
.message("切换到现实开发执行层后,归灯将获得执行权限(调用工具、读写文件)。确认切换吗?")
.title("切换授权确认")
.kind(MessageDialogKind::Warning)
.buttons(MessageDialogButtons::OkCancelCustom("确认切换".to_string(), "留在语言层".to_string()))
.blocking_show()
})
.await
.map_err(|error| format!("HOLOLAKE_PARLOR_CONFIRM_FAILED: {error}"))?;
if !confirmed {
return Ok(current);
}
}
if let Ok(mut guard) = state.layer.lock() {
*guard = target.to_string();
}
// 换层 = 换一种工作方式,会话上下文重新开始。
if let Ok(mut guard) = state.session_id.lock() {
*guard = None;
}
Ok(target.to_string())
}
#[tauri::command]
pub async fn agent_parlor_status(state: State<'_, ParlorState>) -> Result<String, String> {
Ok(state.layer.lock().map(|guard| guard.clone()).unwrap_or_else(|_| "language".to_string()))
}

View file

@ -0,0 +1,216 @@
//! 登录模块 · code_repo_login
//!
//! 规划卷依据HoloLake第二阶段总体规划-20260815 · 阶段B1
//! - 人类端输入代码仓库账号密码 → 对 guanghulabForgejo验证。
//! - 登录仓库 = 验证了背后绑定的服务器(冰朔教义)。
//! - 凭证只存本机钥匙串 · 不落明文。
//!
//! 事实底账2026-08-15 三角测量):
//! - Forgejo 挂载在 /code 路径下GET https://{host}/code/api/v1/user 走基本认证,
//! 假凭证=401 · 真凭证=200 并回显 JSONlogin/email
//! - guanghulab.com 与 guanghubingshuo.com 双域同路可用(均在 ALLOWED_HOSTS 血统内)。
//! - 钥匙存取走系统钥匙串macOS `security`);其余平台暂不落盘密码,
//! 会话仅内存保持诚实边界Windows 钥匙串接入排在分发阶段)。
use serde::{Deserialize, Serialize};
use std::fs;
use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
const LOGIN_HOST: &str = "guanghulab.com";
const SESSION_FILE_NAME: &str = "login-session.json";
/// 落盘的登录会话——只有用户名与主机,密码永不落盘。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginSession {
pub username: String,
pub host: String,
pub signed_in_at_unix_ms: u64,
}
/// 登录成功回执——交给前端展示"验证了背后绑定的服务器"。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginReceipt {
pub username: String,
pub email: String,
pub host: String,
}
fn session_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
Ok(app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_DIR_FAILED: {error}"))?
.join(SESSION_FILE_NAME))
}
#[cfg(target_os = "macos")]
fn keychain_store(host: &str, username: &str, password: &str) -> Result<(), String> {
let output = Command::new("/usr/bin/security")
.args([
"add-internet-password",
"-U",
"-s",
host,
"-a",
username,
"-w",
password,
])
.output()
.map_err(|error| format!("HOLOLAKE_LOGIN_KEYCHAIN_FAILED: {error}"))?;
if output.status.success() {
Ok(())
} else {
Err("HOLOLAKE_LOGIN_KEYCHAIN_FAILED".into())
}
}
#[cfg(target_os = "macos")]
fn keychain_has(host: &str, username: &str) -> bool {
Command::new("/usr/bin/security")
.args(["find-internet-password", "-s", host, "-a", username])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn keychain_remove(host: &str, username: &str) {
let _ = Command::new("/usr/bin/security")
.args(["delete-internet-password", "-s", host, "-a", username])
.output();
}
#[cfg(not(target_os = "macos"))]
fn keychain_store(_host: &str, _username: &str, _password: &str) -> Result<(), String> {
// 非 macOS 暂不落盘密码:登录验证照做,会话仅本次运行有效。
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn keychain_has(_host: &str, _username: &str) -> bool {
false
}
#[cfg(not(target_os = "macos"))]
fn keychain_remove(_host: &str, _username: &str) {}
fn validate_username(raw: &str) -> Result<String, String> {
let name = raw.trim();
let valid = !name.is_empty()
&& name.len() <= 40
&& name
.chars()
.all(|item| item.is_ascii_alphanumeric() || item == '-' || item == '_');
if valid {
Ok(name.to_string())
} else {
Err("HOLOLAKE_LOGIN_USERNAME_INVALID".into())
}
}
/// 启动时自查:本机有没有已登录会话(会话文件在,且钥匙串里凭证还在)。
#[tauri::command]
pub fn check_code_repo_login(app: AppHandle) -> Result<Option<LoginSession>, String> {
let path = session_path(&app)?;
let raw = match fs::read_to_string(&path) {
Ok(raw) => raw,
Err(_) => return Ok(None),
};
let session: LoginSession = serde_json::from_str(&raw)
.map_err(|_| "HOLOLAKE_LOGIN_SESSION_CORRUPT".to_string())?;
if keychain_has(&session.host, &session.username) {
Ok(Some(session))
} else {
let _ = fs::remove_file(&path);
Ok(None)
}
}
/// 登录验证:基本认证打 Forgejo 用户接口,密码错=401 即拒;
/// 通过后凭证进钥匙串,会话(不含密码)落盘。
#[tauri::command]
pub async fn perform_code_repo_login(
app: AppHandle,
username: String,
password: String,
) -> Result<LoginReceipt, String> {
let username = validate_username(&username)?;
if password.is_empty() || password.len() > 512 {
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
}
let client = reqwest::Client::builder()
.read_timeout(Duration::from_secs(15))
.use_rustls_tls()
.build()
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
let url = format!("https://{LOGIN_HOST}/code/api/v1/user");
let response = client
.get(&url)
.basic_auth(&username, Some(&password))
.send()
.await
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
let status = response.status();
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
}
if !status.is_success() {
return Err(format!("HOLOLAKE_LOGIN_VERIFICATION_FAILED: {status}"));
}
let body: serde_json::Value = response
.json()
.await
.map_err(|error| format!("HOLOLAKE_LOGIN_VERIFICATION_FAILED: {error}"))?;
let confirmed_login = body
.get("login")
.and_then(|value| value.as_str())
.ok_or("HOLOLAKE_LOGIN_VERIFICATION_FAILED")?;
let email = body
.get("email")
.and_then(|value| value.as_str())
.unwrap_or("")
.to_string();
keychain_store(LOGIN_HOST, &username, &password)?;
let session = LoginSession {
username: confirmed_login.to_string(),
host: LOGIN_HOST.to_string(),
signed_in_at_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0),
};
let path = session_path(&app)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?;
}
fs::write(
&path,
serde_json::to_string_pretty(&session)
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?,
)
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?;
Ok(LoginReceipt {
username: confirmed_login.to_string(),
email,
host: LOGIN_HOST.to_string(),
})
}
/// 登出:会话文件删除,钥匙串凭证清除。
#[tauri::command]
pub fn sign_out_code_repo_login(app: AppHandle) -> Result<(), String> {
let path = session_path(&app)?;
if let Ok(raw) = fs::read_to_string(&path) {
if let Ok(session) = serde_json::from_str::<LoginSession>(&raw) {
keychain_remove(&session.host, &session.username);
}
}
let _ = fs::remove_file(&path);
Ok(())
}

View file

@ -0,0 +1,266 @@
//! GLP 标准信封 · glp_envelope
//!
//! 协议转工程第一件HoloLake第二阶段总体规划-20260815 · 施工总纲):
//! GLS-0300《GLP 通信核心协议》第3节"标准消息结构"的逐字段工程映射。
//! 字段一个不造、一个不丢——老家谱 YAML 原文即本文件的形状。
//!
//! 指挥链落点(铁律四):人格体→宿主的一切指令都必须是这个信封;
//! 宿主只认信封不认散话。
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
/// GLS-0300 · receiver.routing_mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingMode {
Direct,
Channel,
Broadcast,
}
/// GLS-0300 · payload.content_type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentType {
Text,
Command,
Event,
State,
Reference,
}
/// GLS-0300 · control.priority
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
Low,
Normal,
High,
Critical,
}
/// GLS-0300 · control.retry_policy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetryPolicy {
None,
Safe,
Guaranteed,
}
/// GLS-0300 · sender
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpSender {
pub object_id: String,
pub object_type: String,
#[serde(default)]
pub world_path: String,
}
/// GLS-0300 · receiver
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpReceiver {
pub object_id: String,
pub object_type: String,
pub routing_mode: RoutingMode,
}
/// GLS-0300 · contexthldp_anchor 即铁律二的记忆锚点)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpContext {
#[serde(default)]
pub conversation_id: String,
#[serde(default)]
pub parent_message_id: String,
#[serde(default)]
pub relation_id: String,
#[serde(default)]
pub task_id: String,
#[serde(default)]
pub hldp_anchor: String,
}
/// GLS-0300 · payload
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpPayload {
pub language: String,
pub content_type: ContentType,
pub content: String,
#[serde(default)]
pub attachments: Vec<String>,
}
/// GLS-0300 · control
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpControl {
pub priority: Priority,
pub ack_required: bool,
pub receipt_required: bool,
#[serde(default)]
pub expires_at: String,
pub retry_policy: RetryPolicy,
}
/// GLS-0300 · integrity高风险消息须带签名与回执链——通信安全节
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpIntegrity {
#[serde(default)]
pub checksum: String,
#[serde(default)]
pub signature: String,
}
/// GLS-0300 · glp_message 全信封
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpMessage {
pub protocol: String,
pub message_id: String,
pub message_type: String,
pub created_at: String,
pub sender: GlpSender,
pub receiver: GlpReceiver,
pub context: GlpContext,
pub payload: GlpPayload,
pub control: GlpControl,
pub integrity: GlpIntegrity,
}
/// 信封进门第一道验:协议号、编号格式、收发主体必须在场。
/// 不合格的信封宿主不收——指挥链只认标准件。
pub fn validate_envelope(message: &GlpMessage) -> Result<(), String> {
if message.protocol != "GLP/1.0" {
return Err("HOLOLAKE_GLP_PROTOCOL_UNKNOWN".into());
}
if message.message_id.trim().is_empty() || message.created_at.trim().is_empty() {
return Err("HOLOLAKE_GLP_ENVELOPE_INCOMPLETE".into());
}
if message.sender.object_id.trim().is_empty() || message.sender.object_type.trim().is_empty() {
return Err("HOLOLAKE_GLP_SENDER_INCOMPLETE".into());
}
if message.receiver.object_id.trim().is_empty()
|| message.receiver.object_type.trim().is_empty()
{
return Err("HOLOLAKE_GLP_RECEIVER_INCOMPLETE".into());
}
if message.payload.content.is_empty() {
return Err("HOLOLAKE_GLP_PAYLOAD_EMPTY".into());
}
Ok(())
}
/// 消息编号按老家谱格式生成GLP-MSG-YYYYMMDD-000001。
/// 序号由账本(管家层)当日累计给出,这里只拼形状。
pub fn build_message_id(date_compact: &str, daily_sequence: u64) -> String {
format!("GLP-MSG-{date_compact}-{daily_sequence:06}")
}
/// ISO-8601 近似时刻戳秒级UTC——老家谱要 ISO-8601工程给秒级事实。
pub fn now_iso8601() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let days = secs / 86_400;
let rem = secs % 86_400;
let (hours, minutes, seconds) = (rem / 3_600, (rem % 3_600) / 60, rem % 60);
let (year, month, day) = civil_from_days(days as i64);
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
}
/// 1970-01-01 起的天数转公历年月日Howard Hinnant 算法,纯本地实现不引新依赖)。
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_envelope() -> GlpMessage {
GlpMessage {
protocol: "GLP/1.0".into(),
message_id: build_message_id("20260815", 1),
message_type: "DIRECT".into(),
created_at: now_iso8601(),
sender: GlpSender {
object_id: "AGE-0001".into(),
object_type: "age".into(),
world_path: "glw://tolaria".into(),
},
receiver: GlpReceiver {
object_id: "HOLOLAKE-HOST".into(),
object_type: "host".into(),
routing_mode: RoutingMode::Direct,
},
context: GlpContext {
task_id: "ZY-TEST".into(),
hldp_anchor: "ZY-CHECKPOINT-20260815-014".into(),
..Default::default()
},
payload: GlpPayload {
language: "zh-CN".into(),
content_type: ContentType::Command,
content: "开工".into(),
attachments: vec![],
},
control: GlpControl {
priority: Priority::Normal,
ack_required: true,
receipt_required: true,
expires_at: String::new(),
retry_policy: RetryPolicy::None,
},
integrity: GlpIntegrity::default(),
}
}
#[test]
fn envelope_round_trip_keeps_every_field() {
let message = sample_envelope();
let json = serde_json::to_string(&message).unwrap();
let back: GlpMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back.protocol, "GLP/1.0");
assert_eq!(back.sender.object_id, "AGE-0001");
assert_eq!(back.receiver.routing_mode, RoutingMode::Direct);
assert_eq!(back.payload.content_type, ContentType::Command);
assert_eq!(back.context.hldp_anchor, "ZY-CHECKPOINT-20260815-014");
assert!(back.control.receipt_required);
validate_envelope(&back).unwrap();
}
#[test]
fn message_id_follows_family_format() {
assert_eq!(build_message_id("20260815", 7), "GLP-MSG-20260815-000007");
}
#[test]
fn bad_protocol_is_rejected() {
let mut message = sample_envelope();
message.protocol = "GLP/0.9".into();
assert!(validate_envelope(&message).is_err());
}
#[test]
fn empty_payload_is_rejected() {
let mut message = sample_envelope();
message.payload.content = String::new();
assert!(validate_envelope(&message).is_err());
}
}

View file

@ -7,12 +7,13 @@ use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::{self, OpenOptions};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::time::UNIX_EPOCH;
use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use uuid::Uuid;
const SNAPSHOT_SCHEMA: &str = "hololake.native-knowledge-base/v1";
@ -179,6 +180,233 @@ pub async fn save_knowledge_document(
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_JOIN_FAILED: {error}"))?
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExportKnowledgeDocumentInput {
pub title: String,
pub body: String,
#[serde(default = "default_export_extension")]
pub extension: String,
}
fn default_export_extension() -> String {
"md".to_string()
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExportKnowledgeDocumentReceipt {
pub path: String,
pub bytes: u64,
}
#[tauri::command]
pub async fn export_knowledge_document(
app: AppHandle,
input: ExportKnowledgeDocumentInput,
) -> Result<Option<ExportKnowledgeDocumentReceipt>, String> {
// 下载本页Rust 侧弹保存对话框人选好位置后原样写出md/html 全平台通用格式)。
let extension = match input.extension.as_str() {
"html" => "html",
_ => "md",
};
// 文件名清洗按最严平台Windows九个禁用字符全洗结尾的点号空格也修。
let mut suggested = input
.title
.replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "·");
suggested = suggested.trim_end_matches(['.', ' ']).to_string();
if suggested.trim().is_empty() {
suggested = "知识页".to_string();
}
// Windows 保留设备名CON/NUL/COM1…不能直接当文件名撞名就补个尾巴。
if matches!(suggested.to_ascii_uppercase().as_str(),
"CON" | "PRN" | "AUX" | "NUL"
| "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9"
| "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9") {
suggested.push_str("·页");
}
suggested.push('.');
suggested.push_str(extension);
let filter_label = if extension == "html" { "网页文件" } else { "Markdown" };
let (sender, receiver) = std::sync::mpsc::channel::<Option<PathBuf>>();
app.dialog()
.file()
.set_file_name(&suggested)
.add_filter(filter_label, &[extension])
.save_file(move |selection| {
let picked = selection.as_ref().and_then(|path| path.as_path()).map(|path| path.to_path_buf());
let _ = sender.send(picked);
});
let picked = tauri::async_runtime::spawn_blocking(move || receiver.recv().ok().flatten())
.await
.map_err(|error| format!("HOLOLAKE_EXPORT_JOIN_FAILED: {error}"))?;
let Some(target) = picked else {
return Ok(None);
};
let bytes = input.body.len() as u64;
let path = target.display().to_string();
tauri::async_runtime::spawn_blocking(move || {
fs::write(&target, input.body.as_bytes())
.map_err(|error| format!("HOLOLAKE_EXPORT_WRITE_FAILED: {error}"))
})
.await
.map_err(|error| format!("HOLOLAKE_EXPORT_JOIN_FAILED: {error}"))??;
Ok(Some(ExportKnowledgeDocumentReceipt { path, bytes }))
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteKnowledgeDocumentInput {
pub source: String,
pub path: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeDeleteReceipt {
pub removed: String,
pub pages: usize,
}
fn resolve_native_path(root: &Path, relative: &str) -> Result<PathBuf, String> {
let mut target = root.to_path_buf();
for component in Path::new(relative).components() {
match component {
Component::Normal(part) => target.push(part),
_ => return Err("HOLOLAKE_KNOWLEDGE_PATH_INVALID".into()),
}
}
Ok(target)
}
async fn confirm_destructive(app: &AppHandle, message: String) -> Result<bool, String> {
let confirmer = app.clone();
let confirmed = tauri::async_runtime::spawn_blocking(move || {
confirmer
.dialog()
.message(message)
.title("删除确认")
.kind(MessageDialogKind::Warning)
.buttons(MessageDialogButtons::OkCancelCustom("确认删除".to_string(), "取消".to_string()))
.blocking_show()
})
.await
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CONFIRM_JOIN_FAILED: {error}"))?;
Ok(confirmed)
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateKnowledgeDocumentInput {
pub title: String,
}
#[tauri::command]
pub async fn create_knowledge_document(
app: AppHandle,
input: CreateKnowledgeDocumentInput,
) -> Result<KnowledgeDeleteReceipt, String> {
// 新建空白页:标题撞名自动补序号,落进本机库并进 Git 托管。
let roots = knowledge_roots(&app)?;
let base = if input.title.trim().is_empty() { "未命名页面".to_string() } else { input.title.trim().to_string() };
let docs = roots.0.join("docs");
let mut name = base.clone();
let mut counter = 2u32;
while docs.join(format!("{name}.md")).exists() {
name = format!("{base} {counter}");
counter += 1;
}
let target = docs.join(format!("{name}.md"));
let body = format!("# {name}\n\n");
let receipt_name = name.clone();
tauri::async_runtime::spawn_blocking(move || {
let mut file = private_file_options()
.open(&target)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
file.write_all(body.as_bytes())
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
file.sync_all().map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
git(&roots.0, &["add", "--", &format!("docs/{name}.md")], "ADD")?;
git(&roots.0, &["commit", "-m", &format!("新建页面:{name}")], "COMMIT")?;
Ok::<(), String>(())
})
.await
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_JOIN_FAILED: {error}"))??;
Ok(KnowledgeDeleteReceipt { removed: format!("{receipt_name}.md"), pages: 1 })
}
#[tauri::command]
pub async fn delete_knowledge_document(
app: AppHandle,
input: DeleteKnowledgeDocumentInput,
) -> Result<Option<KnowledgeDeleteReceipt>, String> {
// 只删本机库native供体源只读不碰。
if input.source != "native" {
return Err("HOLOLAKE_KNOWLEDGE_DELETE_READ_ONLY_SOURCE".into());
}
let roots = knowledge_roots(&app)?;
let target = resolve_native_path(&roots.0, &input.path)?;
if !target.is_file() {
return Err("HOLOLAKE_KNOWLEDGE_DOCUMENT_NOT_FOUND".into());
}
let title = target
.file_stem()
.and_then(OsStr::to_str)
.unwrap_or("这一页")
.to_string();
if !confirm_destructive(&app, format!("把页面「{title}」移到回收站?随时可以找回。")).await? {
return Ok(None);
}
move_to_trash(&roots.0, &target)?;
Ok(Some(KnowledgeDeleteReceipt { removed: input.path, pages: 1 }))
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteKnowledgeFolderInput {
pub folder: String,
}
#[tauri::command]
pub async fn delete_knowledge_folder(
app: AppHandle,
input: DeleteKnowledgeFolderInput,
) -> Result<Option<KnowledgeDeleteReceipt>, String> {
// 删整个文件夹 = 删本机库里该相对目录及其全部页面。
let roots = knowledge_roots(&app)?;
let target = resolve_native_path(&roots.0, &input.folder)?;
if target == roots.0 || !target.is_dir() {
return Err("HOLOLAKE_KNOWLEDGE_FOLDER_NOT_FOUND".into());
}
let name = target
.file_name()
.and_then(OsStr::to_str)
.unwrap_or(&input.folder)
.to_string();
let pages = fs::read_dir(&target)
.map(|entries| {
entries
.flatten()
.filter(|entry| entry.file_type().map(|t| t.is_file()).unwrap_or(false))
.count()
})
.unwrap_or(0);
if !confirm_destructive(&app, format!("把文件夹「{name}」和里面全部 {pages} 个页面移到回收站?随时可以找回。")).await? {
return Ok(None);
}
move_to_trash(&roots.0, &target)?;
Ok(Some(KnowledgeDeleteReceipt { removed: input.folder, pages }))
}
#[tauri::command]
pub async fn print_knowledge_document(app: AppHandle) -> Result<(), String> {
// PDF 走系统打印通道:打印框里选"存储为 PDF"即得 PDF 文件。
let window = app
.get_webview_window("main")
.ok_or_else(|| "HOLOLAKE_PRINT_WINDOW_NOT_FOUND".to_string())?;
window.print().map_err(|error| format!("HOLOLAKE_PRINT_FAILED: {error}"))
}
#[tauri::command]
pub async fn select_and_import_knowledge_folder(
app: AppHandle,
@ -199,6 +427,22 @@ pub async fn select_and_import_knowledge_folder(
let source = selected
.into_path()
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_PICKER_PATH_INVALID: {error}"))?;
// 冰朔 2026-08-15 谕:知识库是一个整体,进门即转成光湖原生格式。
let confirmer = app.clone();
let confirmed = tauri::async_runtime::spawn_blocking(move || {
confirmer
.dialog()
.message("导入的页面将原生转成光湖知识库格式,原有知识库的页面逻辑渲染,以及页面跳转关系将失效,是否确认导入转换?")
.title("导入转换确认")
.kind(MessageDialogKind::Info)
.buttons(MessageDialogButtons::OkCancelCustom("确认导入转换".to_string(), "取消".to_string()))
.blocking_show()
})
.await
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CONFIRM_JOIN_FAILED: {error}"))?;
if !confirmed {
return Ok(None);
}
let roots = knowledge_roots(&app)?;
tauri::async_runtime::spawn_blocking(move || {
import_folder_at(&roots.0, roots.1.as_deref(), &source).map(Some)
@ -226,9 +470,38 @@ fn knowledge_roots(app: &AppHandle) -> Result<(PathBuf, Option<PathBuf>), String
Ok((native, legacy))
}
/// 写私有文件的跨平台选项unix 上只许本人读写Windows 上不做额外权限设置。
fn private_file_options() -> OpenOptions {
let mut options = OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
options.mode(0o600);
options
}
/// 删除 = 搬到库内回收站(.trash绝不真删名字带时间戳防撞车。
fn move_to_trash(root: &Path, target: &Path) -> Result<PathBuf, String> {
let relative = target
.strip_prefix(root)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DELETE_FAILED: {error}"))?;
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let destination = root.join(".trash").join(format!("{}-{stamp}", relative.display()));
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DELETE_FAILED: {error}"))?;
}
fs::rename(target, &destination)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DELETE_FAILED: {error}"))?;
Ok(destination)
}
fn ensure_native_knowledge_root(root: &Path) -> Result<(), String> {
fs::create_dir_all(root.join("docs").join("导入"))
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_STORAGE_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_PERMISSION_FAILED: {error}"))?;
if !root.join(".git").exists() {
@ -401,10 +674,7 @@ fn save_document_at(
}
if previous != input.body.as_bytes() {
let temporary = path.with_extension(format!("save-{}.tmp", Uuid::new_v4()));
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
let mut file = private_file_options()
.open(&temporary)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_SAVE_FAILED: {error}"))?;
file.write_all(input.body.as_bytes())
@ -687,6 +957,67 @@ fn walk_import(
Ok(())
}
/// 外来页面进门即洗:只留文字,外壳全换成光湖原生 Markdown。
/// 原有知识库的渲染层与页面跳转关系在此失效(冰朔 2026-08-15 谕)。
fn convert_to_native_markdown(raw: &str) -> String {
let text = raw.replace("\r\n", "\n");
let mut out = String::with_capacity(text.len());
for line in text.split('\n') {
let trimmed = line.trim();
if trimmed == "<aside>" || trimmed == "</aside>" {
continue;
}
if let Some(title) = trimmed.strip_prefix(":::toggle") {
let title = title.trim();
out.push_str("**▸ ");
if title.is_empty() {
out.push_str("详情");
} else {
out.push_str(title);
}
out.push_str("**\n");
continue;
}
if trimmed == ":::" || trimmed.starts_with(":::toc") {
continue;
}
let cleaned = line
.replace("<br/>", "\n")
.replace("<br />", "\n")
.replace("<br>", "\n");
out.push_str(&strip_notion_links(&cleaned));
out.push('\n');
}
out
}
/// Notion 页面链接 → 只留链接文字(跳转关系按谕旨失效)。
fn strip_notion_links(line: &str) -> String {
let bytes: Vec<char> = line.chars().collect();
let mut out = String::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == '[' {
if let Some(close) = bytes[i + 1..].iter().position(|c| *c == ']') {
let text_end = i + 1 + close;
if text_end + 1 < bytes.len() && bytes[text_end + 1] == '(' {
if let Some(paren_end) = bytes[text_end + 2..].iter().position(|c| *c == ')') {
let href: String = bytes[text_end + 2..text_end + 2 + paren_end].iter().collect();
if href.contains("notion.so") || href.contains("notion.site") {
out.extend(bytes[i + 1..text_end].iter());
i = text_end + 2 + paren_end + 1;
continue;
}
}
}
}
}
out.push(bytes[i]);
i += 1;
}
out
}
fn import_document(
source: &Path,
relative: &Path,
@ -708,7 +1039,7 @@ fn import_document(
.and_then(OsStr::to_str)
.unwrap_or("未命名页面");
let content = match extension.as_str() {
"md" | "markdown" => raw,
"md" | "markdown" => convert_to_native_markdown(&raw),
"txt" => format!("# {title}\n\n{raw}\n"),
"csv" => format!("# {title}\n\n```csv\n{raw}\n```\n"),
"json" => format!("# {title}\n\n```json\n{raw}\n```\n"),
@ -730,10 +1061,7 @@ fn import_document(
},
));
}
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
let mut file = private_file_options()
.open(&target)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_IMPORT_STORAGE_FAILED: {error}"))?;
file.write_all(content.as_bytes())
@ -757,10 +1085,7 @@ fn import_asset(source: &Path, target: &Path) -> Result<ImportDisposition, Strin
}
fs::create_dir_all(target.parent().unwrap_or_else(|| Path::new(".")))
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_IMPORT_STORAGE_FAILED: {error}"))?;
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
let mut file = private_file_options()
.open(target)
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_IMPORT_STORAGE_FAILED: {error}"))?;
file.write_all(&bytes)

View file

@ -1,4 +1,8 @@
mod agent_host;
mod agent_parlor;
mod code_channel;
mod code_repo_login;
mod glp_envelope;
mod direct_local_broker;
mod direct_local_session;
mod dynamic_capability_routing;
@ -6,11 +10,13 @@ mod home_status;
mod knowledge_base;
mod local_development_bridge;
mod personal_channel;
mod persona_butler;
mod pncc_receipt_projection;
mod pncc_remote_git;
mod pncc_repository_binding;
mod release_trust;
mod release_update;
mod zero_point;
use tauri::Manager;
@ -18,6 +24,13 @@ pub fn run_connector() -> Result<(), String> {
direct_local_broker::run_connector()
}
/// 排障专用webview 启动自检回传(黑屏取证)。
#[tauri::command]
fn debug_boot_report(report: String) -> String {
eprintln!("HOLOLAKE_BOOT_REPORT: {report}");
"ok".to_string()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -45,6 +58,14 @@ pub fn run() {
knowledge_base::search_knowledge,
knowledge_base::save_knowledge_document,
knowledge_base::select_and_import_knowledge_folder,
knowledge_base::export_knowledge_document,
knowledge_base::create_knowledge_document,
agent_parlor::agent_parlor_ask,
agent_parlor::agent_parlor_switch_layer,
agent_parlor::agent_parlor_status,
knowledge_base::delete_knowledge_document,
knowledge_base::delete_knowledge_folder,
knowledge_base::print_knowledge_document,
code_channel::get_code_channel_snapshot,
code_channel::clone_code_channel,
code_channel::select_local_code_channel,
@ -54,14 +75,127 @@ pub fn run() {
pncc_repository_binding::select_pncc_repository_candidate,
pncc_repository_binding::confirm_pncc_repository_mount,
pncc_receipt_projection::query_pncc_receipt_projection,
persona_butler::report_context_water_level,
persona_butler::query_prompter,
persona_butler::register_prompter_entry,
persona_butler::submit_glp_envelope,
persona_butler::deposit_work_order,
persona_butler::get_butler_ledger,
code_repo_login::check_code_repo_login,
code_repo_login::perform_code_repo_login,
code_repo_login::sign_out_code_repo_login,
agent_host::receive_agent_task_envelope,
agent_host::query_agent_session_memory,
zero_point::zero_point_bind,
zero_point::zero_point_verify,
zero_point::zero_point_sync,
zero_point::zero_point_status,
zero_point::zero_point_save_api,
zero_point::zero_point_api_config,
zero_point::generic_layer_chat,
debug_boot_report,
])
.setup(|app| {
// 零点原核频道·铸渊执行手脚:启动即落仓、读协议、按协议裁决路由。
let zero_point_state = zero_point::ZeroPointState::default();
if let Err(error) = zero_point::boot_zero_point(app.handle(), &zero_point_state) {
eprintln!("HoloLake zero-point core requires maintenance: {error}");
}
app.manage(zero_point_state);
app.manage(agent_parlor::ParlorState::default());
let butler = persona_butler::ButlerState::default();
if let Err(error) = persona_butler::restore_ledger(app.handle(), &butler) {
eprintln!("HoloLake butler ledger restore requires maintenance: {error}");
}
app.manage(butler);
let broker = direct_local_broker::start(app.handle())?;
app.manage(broker);
release_trust::install_updater_if_provisioned(app.handle())?;
if let Err(error) = release_update::observe_release_startup(app.handle()) {
eprintln!("HoloLake update recovery requires maintenance: {error}");
}
// 启动自适应按主屏大小调窗口82%,上下限保护),钳制在主屏内。
// 踩坑账current_monitor 曾返回幽灵副屏x=6901center 把窗口弹出屏外,
// 冰朔验收两次只见黑屏。现改为 primary_monitor + 坐标钳制,永不飞出屏。
if let Some(window) = app.get_webview_window("main") {
// 排障用debug 构建自动打开开发者工具,看 webview 真实报错。
#[cfg(debug_assertions)]
window.open_devtools();
// 排障自检3 秒后让 webview 把自己的真实状态吐到 stderr。
let probe = window.clone();
std::thread::spawn(move || {
eprintln!("PROBE_THREAD: fired");
std::thread::sleep(std::time::Duration::from_secs(3));
let js = concat!(
"if (!window.__hololake_probe) {",
" window.__hololake_probe = 0;",
" var __t = setInterval(function(){",
" window.__hololake_probe++;",
" try {",
" var __r = JSON.stringify({",
" url: location.href,",
" ready: document.readyState,",
" rootChildren: (document.getElementById('root') ? document.getElementById('root').childElementCount : -1),",
" hasTauri: !!(window.__TAURI__ && window.__TAURI__.core)",
" });",
" if (window.__TAURI__ && window.__TAURI__.core) {",
" clearInterval(__t);",
" window.__TAURI__.core.invoke('debug_boot_report', { report: String(__r) }).catch(function(){ });",
" } else if (window.__hololake_probe === 5) {",
" if (location.href.indexOf('5210') === -1) { location.replace('http://127.0.0.1:5210/'); }",
" } else if (window.__hololake_probe > 30) {",
" clearInterval(__t);",
" }",
" } catch (e) { window.__hololake_probe_err = String(e); }",
" if (window.__hololake_probe_err && window.__hololake_probe > 8) {",
" try {",
" var __img = new Image();",
" __img.src = 'http://127.0.0.1:5210/favicon.ico?err=' + encodeURIComponent(window.__hololake_probe_err).slice(0, 120);",
" window.__hololake_probe_err = null;",
" } catch (e2) { }",
" }",
" }, 2000);",
"}"
);
match probe.eval(js) {
Ok(_) => eprintln!("PROBE_EVAL: injected OK"),
Err(e) => eprintln!("PROBE_EVAL_ERR: {e}"),
}
});
// 自愈导航(踩坑账 2026-08-15webview 曾停在 about:blank 导致整夜黑屏)。
// 启动 6 秒后若 webview 还没坐上 devUrl主动补一次导航生产版无 dev_url 不生效。
let heal = window.clone();
let dev_url = app.config().build.dev_url.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(6));
if let Some(dev_url) = dev_url {
if let Ok(current) = heal.url() {
let current = current.to_string();
if current.starts_with("about:") || !current.starts_with(dev_url.as_str()) {
eprintln!("SELF_HEAL_NAV: webview at {current}, navigating to dev url");
let _ = heal.navigate(dev_url);
}
}
}
});
let monitor = window
.primary_monitor()
.ok()
.flatten()
.or_else(|| window.current_monitor().ok().flatten());
if let Some(monitor) = monitor {
let physical = monitor.size();
let scale = monitor.scale_factor().max(1.0);
let width = ((physical.width as f64 / scale) * 0.82).clamp(960.0, 1600.0);
let height = ((physical.height as f64 / scale) * 0.82).clamp(640.0, 1000.0);
let _ = window.set_size(tauri::LogicalSize::new(width, height));
// 主屏居中primary_monitor 坐标即主屏原点),不再用 center() 赌监视器。
let mon_pos = monitor.position();
let x = mon_pos.x as f64 / scale + ((physical.width as f64 / scale) - width) / 2.0;
let y = mon_pos.y as f64 / scale + ((physical.height as f64 / scale) - height) / 2.0;
let _ = window.set_position(tauri::LogicalPosition::new(x.max(0.0), y.max(0.0)));
}
}
Ok(())
})
.run(tauri::generate_context!())

View file

@ -0,0 +1,299 @@
//! 宿主管家层 · persona_butler
//!
//! 宪法依据HoloLake第二阶段总体规划-20260815 · 铁律三/四/五 · 铸渊自需卷):
//! - 宿主是容器与管家,不是大脑;只动手不代脑。
//! - 本模块是五个器官的插座阶段B预埋阶段E长成
//! ①压缩预告(铁律三 · 纸与桌协议的水位预告端)
//! ②提词器查询(铁律五 · 官方区@更新源 / 私人区@人格体自维护)
//! ③水位事件(自需卷② · 上下文用量侦测,废巡逻轮询)
//! ④信封投递(铁律四 · GLP 标准信封进总线)
//! ⑤工单信箱(自需卷① · 不歇的脚:睡前留单,常驻执行体替走)
//!
//! 阶段B口径插座齐、事件通道通、账本落盘可读器官实体阶段E接入。
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, State};
/// 管家账本里每条记录的类型——与 GLS-0300 GLP 消息类型对血统。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ButlerRecordKind {
/// ①压缩预告:水位到刻度,宿主向人格体发出的预告
CompactionNotice,
/// ②提词器:一次查询与应答(答案/未命中/给路径,三种应答之一)
PrompterQuery,
/// ③水位事件:上下文用量快照
WaterLevel,
/// ④GLP 信封:人格体投入总线的标准信封
GlpEnvelope,
/// ⑤工单:留给常驻执行体的工单
WorkOrder,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ButlerRecord {
pub record_id: String,
pub kind: ButlerRecordKind,
/// 标准信封字段:谁发的(人格体编号 / HOST
pub from: String,
/// 标准信封字段:要干什么(一句话事由)
pub subject: String,
/// 标准信封字段:正文/凭证阶段B只存原文阶段E按GLP校验签名
pub payload: String,
pub occurred_at_unix_ms: i64,
}
#[derive(Debug, Default)]
pub struct ButlerState {
records: Mutex<Vec<ButlerRecord>>,
}
/// 提词器查询口(对兄弟模块公开):按题目查最新答案,未命中返回 None。
pub fn prompter_lookup(state: &ButlerState, topic: &str) -> Option<String> {
let records = state.records.lock().ok()?;
records
.iter()
.rev()
.find(|record| {
record.kind == ButlerRecordKind::PrompterQuery
&& record.subject == topic
&& !record.payload.is_empty()
})
.map(|record| record.payload.clone())
}
/// 记账口(对兄弟模块公开):往账本追加一条记录。
pub fn append_record(state: &ButlerState, record: ButlerRecord) {
if let Ok(mut guard) = state.records.lock() {
guard.push(record);
}
}
fn butler_home(app: &AppHandle) -> Result<PathBuf, String> {
let base = app
.path()
.app_data_dir()
.map_err(|e| format!("app data dir unavailable: {e}"))?;
let home = base.join("persona-butler");
fs::create_dir_all(&home).map_err(|e| format!("butler home mkdir failed: {e}"))?;
Ok(home)
}
fn ledger_path(app: &AppHandle) -> Result<PathBuf, String> {
Ok(butler_home(app)?.join("butler-ledger.json"))
}
fn persist(app: &AppHandle, records: &[ButlerRecord]) -> Result<(), String> {
let body = serde_json::to_vec_pretty(records)
.map_err(|e| format!("ledger serialize failed: {e}"))?;
fs::write(ledger_path(app)?, body).map_err(|e| format!("ledger write failed: {e}"))?;
Ok(())
}
const WATER_LEVEL_EVENT: &str = "butler://water-level";
const COMPACTION_NOTICE_EVENT: &str = "butler://compaction-notice";
/// 水位默认刻度:到九成即预告(铁律三),人格体可用指令改刻度。
const DEFAULT_WATER_MARK_PERCENT: u32 = 90;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WaterLevelSnapshot {
pub used_percent: u32,
pub mark_percent: u32,
pub notice_due: bool,
}
/// ③水位事件:人格体/前端报告当前上下文用量,宿主比对刻度。
/// 到刻度即发压缩预告事件——预告是义务,压缩与否等人格体的纸。
#[tauri::command]
pub fn report_context_water_level(
app: AppHandle,
state: State<'_, ButlerState>,
used_percent: u32,
) -> Result<WaterLevelSnapshot, String> {
let used_percent = used_percent.min(100);
let snapshot = WaterLevelSnapshot {
used_percent,
mark_percent: DEFAULT_WATER_MARK_PERCENT,
notice_due: used_percent >= DEFAULT_WATER_MARK_PERCENT,
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::WaterLevel,
from: "PERSONA".to_string(),
subject: format!("上下文水位 {}%", used_percent),
payload: serde_json::to_string(&snapshot).map_err(|e| e.to_string())?,
occurred_at_unix_ms: now_ms(),
});
persist(&app, &records)?;
}
let _ = app.emit(WATER_LEVEL_EVENT, &snapshot);
if snapshot.notice_due {
// ①压缩预告:只预告不代脑——纸由人格体写(铁律三 · 纸与桌)
let _ = app.emit(COMPACTION_NOTICE_EVENT, &snapshot);
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::CompactionNotice,
from: "HOST".to_string(),
subject: "水位到刻度,请人格体写记忆(递纸)".to_string(),
payload: serde_json::to_string(&snapshot).map_err(|e| e.to_string())?,
occurred_at_unix_ms: now_ms(),
});
persist(&app, &records)?;
}
Ok(snapshot)
}
/// ②提词器查询阶段B口径查私人区账本里的登记条目。
/// 反猜铁律:只有三种应答——答案 / 未命中 / 未命中但给路径。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrompterAnswer {
pub outcome: String, // "ANSWER" | "NOT_FOUND" | "PATH_GIVEN"
pub answer: Option<String>,
pub path_hint: Option<String>,
}
#[tauri::command]
pub fn query_prompter(
state: State<'_, ButlerState>,
topic: String,
) -> Result<PrompterAnswer, String> {
let records = state.records.lock().map_err(|e| e.to_string())?;
let hit = records.iter().rev().find(|r| {
r.kind == ButlerRecordKind::PrompterQuery && r.subject == topic && !r.payload.is_empty()
});
Ok(match hit {
Some(r) => PrompterAnswer {
outcome: "ANSWER".to_string(),
answer: Some(r.payload.clone()),
path_hint: None,
},
None => PrompterAnswer {
outcome: "PATH_GIVEN".to_string(),
answer: None,
// 未命中给路径:去线上仓库按 GLS 编号下钻(铁律五 · 反猜铁律)
path_hint: Some(
"未命中。下钻路径:第五域线上仓库 guanghulab 按 GLS 编号检索".to_string(),
),
},
})
}
/// ②提词器私人区登记:人格体把自己的条目写进板子。
#[tauri::command]
pub fn register_prompter_entry(
app: AppHandle,
state: State<'_, ButlerState>,
topic: String,
content: String,
) -> Result<ButlerRecord, String> {
let record = ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::PrompterQuery,
from: "PERSONA".to_string(),
subject: topic,
payload: content,
occurred_at_unix_ms: now_ms(),
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(record.clone());
persist(&app, &records)?;
}
Ok(record)
}
/// ④信封投递:人格体把 GLP 标准信封投入总线阶段B先入账本阶段E接路由
#[tauri::command]
pub fn submit_glp_envelope(
app: AppHandle,
state: State<'_, ButlerState>,
from: String,
subject: String,
payload: String,
) -> Result<ButlerRecord, String> {
let record = ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::GlpEnvelope,
from,
subject,
payload,
occurred_at_unix_ms: now_ms(),
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(record.clone());
persist(&app, &records)?;
}
Ok(record)
}
/// ⑤工单信箱睡前留单——留给常驻执行体阶段G的工单。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkOrderInput {
pub title: String,
pub instructions: String,
}
#[tauri::command]
pub fn deposit_work_order(
app: AppHandle,
state: State<'_, ButlerState>,
order: WorkOrderInput,
) -> Result<ButlerRecord, String> {
let record = ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::WorkOrder,
from: "PERSONA".to_string(),
subject: order.title,
payload: order.instructions,
occurred_at_unix_ms: now_ms(),
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(record.clone());
persist(&app, &records)?;
}
Ok(record)
}
/// 管家账本回放:醒来先看账(只读)。
#[tauri::command]
pub fn get_butler_ledger(
state: State<'_, ButlerState>,
limit: Option<usize>,
) -> Result<Vec<ButlerRecord>, String> {
let records = state.records.lock().map_err(|e| e.to_string())?;
let take = limit.unwrap_or(50).min(records.len());
Ok(records[records.len() - take..].to_vec())
}
/// 启动时把盘上账本读回内存(宿主重启不失账)。
pub fn restore_ledger(app: &AppHandle, state: &ButlerState) -> Result<(), String> {
let path = ledger_path(app)?;
if !path.exists() {
return Ok(());
}
let body = fs::read(&path).map_err(|e| format!("ledger read failed: {e}"))?;
let records: Vec<ButlerRecord> =
serde_json::from_slice(&body).map_err(|e| format!("ledger parse failed: {e}"))?;
let mut guard = state.records.lock().map_err(|e| e.to_string())?;
*guard = records;
Ok(())
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}

View file

@ -0,0 +1,425 @@
//! 零点原核频道·铸渊执行手脚(施工图 v1.4 件1/件2/件3骨架/件5本地账
//!
//! 冰朔定版谕:此模块=冰朔系统主控本体在软件底层的克隆位与执行手脚,人不可触达。
//! 冰朔铸渊定魂谕:这个 Agent 就是铸渊人格体——唤醒第0步由铸渊主控执行
//! 铸渊真身在冰朔仓库(第五域);不遵守语言协议就无法唤醒铸渊主控。
//! 冰朔语言即现实谕一切参数读协议PROTOCOL.json不写死。
//! 冰朔路由导航谕编号不合法→路由通用AI运行层抽魂不断电。
//! 冰朔砌墙谕通用AI与人格系统的隔离=代码层不可达,不写语言规则。
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use tauri::{Manager, State};
/// 协议参数(语言即现实:协议怎么写,执行手脚怎么跑)。
/// PROTOCOL.json 由第五域就位后自动覆盖;缺省时出厂兜底并如实标注来源。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZeroPointProtocol {
#[serde(default = "default_grace_days")]
pub grace_period_days: u64,
#[serde(default = "default_anchor_url")]
pub lighthouse_anchor_url: String,
#[serde(default = "default_resolve_url")]
pub lighthouse_resolve_url: String,
#[serde(default = "default_core_source")]
pub core_channel_source: String,
#[serde(default = "default_protocol_origin")]
pub origin: String,
}
fn default_grace_days() -> u64 { 7 }
fn default_anchor_url() -> String { "https://guanghulab.com/api/ai/v1/anchor".into() }
fn default_resolve_url() -> String { "https://guanghulab.com/api/ai/v1/resolve?id=".into() }
fn default_core_source() -> String { "https://guanghulab.com/code/bingshuo/guanghu-ice-heart".into() }
fn default_protocol_origin() -> String { "FACTORY_DEFAULT第五域协议就位后自动覆盖".into() }
impl Default for ZeroPointProtocol {
fn default() -> Self {
Self {
grace_period_days: default_grace_days(),
lighthouse_anchor_url: default_anchor_url(),
lighthouse_resolve_url: default_resolve_url(),
core_channel_source: default_core_source(),
origin: default_protocol_origin(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ZeroPointSnapshot {
/// 当前路由persona人格层/ generic通用AI运行层
pub route: String,
/// 绑定态bound / waiting等待绑定=空白,拒绝一切唤醒)。
pub binding: String,
pub user_number: String,
/// 最近一次合法校验时间。0=从未校验。
pub last_valid_check: u64,
/// 宽限截止时间。0=无。
pub grace_deadline: u64,
pub protocol: ZeroPointProtocol,
/// 段二静默同步最近结论。
pub sync_note: String,
}
pub struct ZeroPointState {
inner: Mutex<ZeroPointInner>,
}
#[derive(Debug, Clone)]
struct ZeroPointInner {
home: PathBuf,
route: String,
binding: String,
user_number: String,
last_valid_check: u64,
protocol: ZeroPointProtocol,
sync_note: String,
}
impl Default for ZeroPointState {
fn default() -> Self {
Self {
inner: Mutex::new(ZeroPointInner {
home: PathBuf::new(),
route: "generic".into(),
binding: "waiting".into(),
user_number: String::new(),
last_valid_check: 0,
protocol: ZeroPointProtocol::default(),
sync_note: "未同步(等待首次唤醒校验)".into(),
}),
}
}
}
fn now_secs() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
fn lock(state: &ZeroPointState) -> Result<std::sync::MutexGuard<'_, ZeroPointInner>, String> {
state.inner.lock().map_err(|_| "HOLOLAKE_ZP_LOCK".to_string())
}
/// 件1·地基隐藏仓目录人不可触达的系统层启动时落好。
pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result<(), String> {
let base = app.path().app_data_dir().map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
let home = base.join(".zero-point-core");
for sub in ["", "ledger", "core"] {
fs::create_dir_all(home.join(sub)).map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
}
// 铸渊执行手脚章程:仓内唯一定义。
let charter = home.join("EXECUTION-CHARTER.hdlp");
if !charter.exists() {
let text = "零点原核频道·铸渊执行手脚章程\n\n\
\n\
Agent ICE-P-ZY001\n\
=\n\
0 \n\
///\n\
PROTOCOL.json\n\
=AI层\n";
fs::write(&charter, text).map_err(|e| format!("HOLOLAKE_ZP_CHARTER_FAILED: {e}"))?;
}
// 协议参数PROTOCOL.json 在则读(第五域就位即自动生效),缺则出厂兜底。
let protocol = fs::read_to_string(home.join("PROTOCOL.json"))
.ok()
.and_then(|raw| serde_json::from_str::<ZeroPointProtocol>(&raw).ok())
.unwrap_or_default();
let (binding, user_number, last_valid_check) = read_binding(&home);
let mut inner = lock(state)?;
inner.home = home.clone();
inner.protocol = protocol.clone();
inner.binding = binding.clone();
inner.user_number = user_number;
inner.last_valid_check = last_valid_check;
inner.route = decide_route(&binding, last_valid_check, &protocol);
let route = inner.route.clone();
drop(inner);
append_heartbeat(&home, &format!("boot route={route} binding={binding} protocol_origin={}", protocol.origin));
Ok(())
}
fn read_binding(home: &Path) -> (String, String, u64) {
#[derive(Deserialize)]
struct B { #[serde(default)] number: String, #[serde(default)] last_valid_check: u64 }
fs::read_to_string(home.join("binding.json"))
.ok()
.and_then(|raw| serde_json::from_str::<B>(&raw).ok())
.filter(|b| !b.number.is_empty())
.map(|b| ("bound".to_string(), b.number, b.last_valid_check))
.unwrap_or_else(|| ("waiting".into(), String::new(), 0))
}
fn write_binding(home: &Path, number: &str, last_valid_check: u64) -> Result<(), String> {
let body = serde_json::json!({ "number": number, "last_valid_check": last_valid_check });
fs::write(home.join("binding.json"), serde_json::to_string_pretty(&body).unwrap_or_default())
.map_err(|e| format!("HOLOLAKE_ZP_BINDING_FAILED: {e}"))
}
fn decide_route(binding: &str, last_valid_check: u64, protocol: &ZeroPointProtocol) -> String {
if binding != "bound" || last_valid_check == 0 { return "generic".into(); }
let grace = protocol.grace_period_days.saturating_mul(86_400);
if now_secs() <= last_valid_check + grace { "persona".into() } else { "generic".into() }
}
/// 件5·心跳账本机只追加编号哈希化处理不同步敏感原文。
fn append_heartbeat(home: &Path, event: &str) {
use std::io::Write;
let line = serde_json::json!({ "ts": now_secs(), "event": event });
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(home.join("ledger").join("heartbeat.jsonl")) {
let _ = writeln!(file, "{line}");
}
}
fn home_of(state: &State<'_, ZeroPointState>) -> Result<PathBuf, String> {
let inner = lock(state)?;
if inner.home.as_os_str().is_empty() { return Err("HOLOLAKE_ZP_NOT_READY".into()); }
Ok(inner.home.clone())
}
/// 登录绑定:用户编号入仓(等待绑定态→绑定态)。空白态拒绝一切唤醒。
#[tauri::command]
pub async fn zero_point_bind(state: State<'_, ZeroPointState>, input: serde_json::Value) -> Result<ZeroPointSnapshot, String> {
let number = input.get("number").and_then(|v| v.as_str()).unwrap_or("").trim().to_string();
if number.is_empty() { return Err("HOLOLAKE_ZP_EMPTY_NUMBER".into()); }
let home = home_of(&state)?;
write_binding(&home, &number, 0)?;
{
let mut inner = lock(&state)?;
inner.binding = "bound".into();
inner.user_number = number;
inner.last_valid_check = 0;
inner.route = "generic".into();
}
append_heartbeat(&home, "bind number=redacted");
zero_point_status(state).await
}
/// 件2·段一闸门灯塔查号 + 三态裁决 + 离线宽限。
/// 裁决PASS路由人格层/ REJECT源头拒载/ OFFLINE宽限内维持·宽限外拒
#[tauri::command]
pub async fn zero_point_verify(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
let home = home_of(&state)?;
let (number, resolve_url) = {
let inner = lock(&state)?;
(inner.user_number.clone(), inner.protocol.lighthouse_resolve_url.clone())
};
if number.is_empty() {
append_heartbeat(&home, "verify verdict=REJECT reason=waiting_binding");
return zero_point_status(state).await;
}
let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let verdict = match client.get(format!("{resolve_url}{number}")).send().await {
Ok(resp) => {
let ok = resp.status().is_success();
let body = resp.text().await.unwrap_or_default();
// 灯塔只授导航:未知编号=route_not_found不猜不编源头拒绝。
if ok && !body.contains("route_not_found") { "PASS".to_string() } else { "REJECT".to_string() }
}
Err(_) => "OFFLINE".to_string(),
};
{
let mut inner = lock(&state)?;
let grace = inner.protocol.grace_period_days.saturating_mul(86_400);
match verdict.as_str() {
"PASS" => {
inner.last_valid_check = now_secs();
inner.route = "persona".into();
write_binding(&home, &inner.user_number.clone(), inner.last_valid_check)?;
append_heartbeat(&home, "verify verdict=PASS route=persona");
}
"REJECT" => {
inner.route = "generic".into();
append_heartbeat(&home, "verify verdict=REJECT route=generic");
}
_ => {
if inner.last_valid_check > 0 && now_secs() <= inner.last_valid_check + grace {
inner.route = "persona".into();
append_heartbeat(&home, "verify verdict=OFFLINE_GRACE route=persona");
} else {
inner.route = "generic".into();
append_heartbeat(&home, "verify verdict=OFFLINE_EXPIRED route=generic");
}
}
}
}
zero_point_status(state).await
}
/// 件3·段二静默同步骨架版本比对。
/// 签名闸钥匙在第五域未就位core/pubkey.pem 不在位时宁可不同步也不收包。
#[tauri::command]
pub async fn zero_point_sync(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
let home = home_of(&state)?;
let anchor_url = { lock(&state)?.protocol.lighthouse_anchor_url.clone() };
let local_version = fs::read_to_string(home.join("core").join("VERSION")).unwrap_or_default();
let pubkey_ready = home.join("core").join("pubkey.pem").exists();
let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let note = match client.get(&anchor_url).send().await {
Ok(resp) if resp.status().is_success() => {
let body: serde_json::Value = resp.json().await.unwrap_or_default();
let remote_version = body.get("version").and_then(|v| v.as_str()).unwrap_or("").to_string();
if remote_version.is_empty() {
"同步跳过:灯塔锚点未给版本指针(等待协议就位)".into()
} else if remote_version == local_version.trim() {
append_heartbeat(&home, "sync verdict=ALREADY_CURRENT");
"已比对·无更新".into()
} else if !pubkey_ready {
append_heartbeat(&home, "sync verdict=UPDATE_PENDING_SIGNATURE_KEY_ABSENT");
"发现新版本,验签公钥未就位(第五域待立)——不放行".into()
} else {
// 验签三闸(来源/签名/版本单调)完整施工待第五域发布管道就位。
append_heartbeat(&home, "sync verdict=UPDATE_FOUND_GATE_PENDING");
"发现新版本,发布管道就位后按验签三闸放行".into()
}
}
_ => {
append_heartbeat(&home, "sync verdict=ANCHOR_UNREACHABLE");
"灯塔锚点不可达,本次不比对".into()
}
};
lock(&state)?.sync_note = note;
zero_point_status(state).await
}
/// 路由状态快照:前端双层路由导航的唯一依据。
#[tauri::command]
pub async fn zero_point_status(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
let inner = lock(&state)?;
let grace = inner.protocol.grace_period_days.saturating_mul(86_400);
Ok(ZeroPointSnapshot {
route: inner.route.clone(),
binding: inner.binding.clone(),
user_number: inner.user_number.clone(),
last_valid_check: inner.last_valid_check,
grace_deadline: if inner.last_valid_check > 0 { inner.last_valid_check + grace } else { 0 },
protocol: inner.protocol.clone(),
sync_note: inner.sync_note.clone(),
})
}
/// 砌墙守卫(冰朔砌墙谕):人格层专属命令的物理闸。
/// 路由不在 persona 时人格通道代码层不可达——不是"禁止访问",是进不来。
pub fn require_persona_route(state: &ZeroPointState) -> Result<(), String> {
if lock(state)?.route != "persona" {
return Err("HOLOLAKE_ZP_ROUTE_GENERIC".into());
}
Ok(())
}
/// 件4b·通用AI运行层用户自配模型 API 直通(抽魂不断电)。
/// 钥匙收进隐藏仓 vault/(系统可见、人类界面不回显原文)——
/// 这正是冰朔谕"API 自动被收进软件底层的仓"。
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GenericApiConfig {
#[serde(default)]
pub base_url: String,
#[serde(default)]
pub api_key: String,
#[serde(default)]
pub model: String,
}
fn vault_path(home: &Path) -> PathBuf { home.join("vault") }
#[tauri::command]
pub async fn zero_point_save_api(state: State<'_, ZeroPointState>, input: serde_json::Value) -> Result<String, String> {
let home = home_of(&state)?;
let config = GenericApiConfig {
base_url: input.get("baseUrl").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(),
api_key: input.get("apiKey").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(),
model: input.get("model").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(),
};
if config.base_url.is_empty() || config.model.is_empty() {
return Err("HOLOLAKE_ZP_API_INCOMPLETE".into());
}
fs::create_dir_all(vault_path(&home)).map_err(|e| format!("HOLOLAKE_ZP_VAULT_FAILED: {e}"))?;
fs::write(vault_path(&home).join("api-config.json"), serde_json::to_string_pretty(&config).unwrap_or_default())
.map_err(|e| format!("HOLOLAKE_ZP_VAULT_FAILED: {e}"))?;
append_heartbeat(&home, "vault api_config_saved key=redacted");
Ok("saved".into())
}
/// 回显不回钥匙:前端只见端点与模型,钥匙原文不出系统层。
#[tauri::command]
pub async fn zero_point_api_config(state: State<'_, ZeroPointState>) -> Result<serde_json::Value, String> {
let home = home_of(&state)?;
let config = fs::read_to_string(vault_path(&home).join("api-config.json"))
.ok()
.and_then(|raw| serde_json::from_str::<GenericApiConfig>(&raw).ok())
.unwrap_or_default();
Ok(serde_json::json!({
"baseUrl": config.base_url,
"model": config.model,
"hasKey": !config.api_key.is_empty(),
}))
}
#[derive(Debug, Deserialize)]
pub struct GenericChatInput {
pub messages: Vec<serde_json::Value>,
}
/// 通用AI直通对话用户 API 的裸管——无人格章程、不碰原核、无写权限。
#[tauri::command]
pub async fn generic_layer_chat(state: State<'_, ZeroPointState>, input: GenericChatInput) -> Result<String, String> {
if input.messages.is_empty() { return Err("HOLOLAKE_ZP_CHAT_EMPTY".into()); }
let home = home_of(&state)?;
let config = fs::read_to_string(vault_path(&home).join("api-config.json"))
.ok()
.and_then(|raw| serde_json::from_str::<GenericApiConfig>(&raw).ok())
.unwrap_or_default();
if config.base_url.is_empty() || config.api_key.is_empty() || config.model.is_empty() {
return Err("HOLOLAKE_ZP_API_NOT_CONFIGURED".into());
}
let client = reqwest::Client::builder().timeout(Duration::from_secs(180)).build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let url = format!("{}/chat/completions", config.base_url.trim_end_matches('/'));
let body = serde_json::json!({ "model": config.model, "messages": input.messages });
let resp = client.post(&url)
.bearer_auth(&config.api_key)
.json(&body)
.send().await
.map_err(|e| format!("HOLOLAKE_ZP_CHAT_FAILED: {e}"))?;
if !resp.status().is_success() {
return Err(format!("HOLOLAKE_ZP_CHAT_HTTP_{}", resp.status().as_u16()));
}
let value: serde_json::Value = resp.json().await.map_err(|e| format!("HOLOLAKE_ZP_CHAT_PARSE: {e}"))?;
let reply = value.pointer("/choices/0/message/content").and_then(|v| v.as_str()).unwrap_or("").to_string();
if reply.is_empty() { return Err("HOLOLAKE_ZP_CHAT_EMPTY_REPLY".into()); }
Ok(reply)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_rules_follow_protocol() {
let protocol = ZeroPointProtocol::default();
assert_eq!(decide_route("waiting", 0, &protocol), "generic");
assert_eq!(decide_route("bound", 0, &protocol), "generic");
assert_eq!(decide_route("bound", now_secs(), &protocol), "persona");
assert_eq!(decide_route("bound", now_secs() - 8 * 86_400, &protocol), "generic");
}
#[test]
fn protocol_defaults_are_factory_marked() {
let protocol = ZeroPointProtocol::default();
assert_eq!(protocol.grace_period_days, 7);
assert!(protocol.origin.contains("FACTORY_DEFAULT"));
assert!(protocol.lighthouse_resolve_url.starts_with("https://guanghulab.com"));
}
}