feat: merge HoloLake desktop into unified 0.3.0
This commit is contained in:
commit
b7461c66c5
37 changed files with 8732 additions and 613 deletions
|
|
@ -0,0 +1,8 @@
|
|||
# Dormant Qoder agent prototype
|
||||
|
||||
These files preserve the unintegrated Qoder prototype for historical and future design review. They are stored as
|
||||
plain audit artifacts, are not Rust modules, are not compiled, and are not reachable from the HoloLake WebView.
|
||||
|
||||
The current stage-one product does not expose internal AI chat, model API configuration, model selection or an AI
|
||||
workbench. Any future reuse must begin from the current zero-point system/persona/carrier/authority separation and
|
||||
must receive a new architecture, security and product-surface review.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,298 @@
|
|||
//! Agent 东家层 · agent_host
|
||||
//!
|
||||
//! 阶段E选型定案(冰朔点头):候选甲"东家模式"为体——
|
||||
//! 外部 CLI Agent(师傅)干活,宿主只管:接信封、备提词、记账本、存记忆。
|
||||
//!
|
||||
//! 协议转工程落点(施工总纲):
|
||||
//! - 人格体主控(铁律一):任务只从 GLP 信封进门(content_type=command),
|
||||
//! 宿主绝不自己发起任务。
|
||||
//! - 提词器(铁律五):会话开卷前先从提词器取指引(官方区协议+私人区索引),
|
||||
//! 写进会话记忆的"提词板"栏——头顶提词板的工程形状。
|
||||
//! - HLDP 内嵌(铁律二):每个会话=一卷 HLDP 检查点体(id/prev 链/date/
|
||||
//! persona/host/title+正文),prev 链不可省。
|
||||
//! - RECEIPT(GLS-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))
|
||||
}
|
||||
|
|
@ -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()))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"schema": "hololake.knowledge-workspace-migration-provenance/v1",
|
||||
"record_id": "HLP-KNOWLEDGE-MIGRATION-001",
|
||||
"state": "CLEAN_ROOM_SOURCE_MIGRATION_IMPLEMENTED_LOCAL_RUNTIME_ACCEPTED",
|
||||
"target": "product-source/hololake-native-desktop",
|
||||
"donors": [
|
||||
{
|
||||
"source": "product-source/hololake-desktop",
|
||||
"role": "READ_ONLY_FOLDER_IMPORT_BEHAVIOR_DONOR",
|
||||
"runtime_copied": false
|
||||
},
|
||||
{
|
||||
"source": "product-source/guanghu-knowledge-base",
|
||||
"role": "READ_ONLY_PAGE_TREE_SEARCH_READING_UX_DONOR",
|
||||
"runtime_copied": false
|
||||
},
|
||||
{
|
||||
"source": "HoloLake Era.app 0.8.0",
|
||||
"role": "READ_ONLY_INSTALLED_BEHAVIOR_AND_COMPATIBILITY_DONOR",
|
||||
"runtime_copied": false
|
||||
},
|
||||
{
|
||||
"source": "Singapore Outline 0.80.2",
|
||||
"role": "READ_ONLY_INTERACTION_AND_INFORMATION_ARCHITECTURE_DONOR",
|
||||
"runtime_copied": false,
|
||||
"live_content_documents_observed": 4,
|
||||
"content_migration_source": false
|
||||
}
|
||||
],
|
||||
"migration_units": [
|
||||
"PAGE_TREE_CONTRACT",
|
||||
"DOCUMENT_READING_CONTRACT",
|
||||
"BOUNDED_SEARCH_CONTRACT",
|
||||
"NATIVE_FOLDER_IMPORT_CONTRACT",
|
||||
"LOCAL_GIT_COMMIT_RECEIPT",
|
||||
"NESTED_OUTLINE_NAVIGATION",
|
||||
"INDEPENDENT_SCROLL_REGIONS",
|
||||
"GFM_AND_WIKI_LINK_RENDERING",
|
||||
"EXACT_CONTENT_DEDUPLICATION",
|
||||
"OPTIMISTIC_NATIVE_EDITING"
|
||||
],
|
||||
"excluded": [
|
||||
"ELECTRON_RUNTIME",
|
||||
"OUTLINE_SERVER_RUNTIME",
|
||||
"TOLARIA_SURFACE",
|
||||
"LEGACY_WRITE_PATH",
|
||||
"IN_PLACE_DATA_MIGRATION"
|
||||
],
|
||||
"legacy_data_boundary": {
|
||||
"mode": "READ_ONLY_COMPATIBILITY_SOURCE",
|
||||
"modified": false,
|
||||
"migrated": false,
|
||||
"deletion_allowed": false,
|
||||
"observed_markdown_documents": 279,
|
||||
"observed_exact_duplicate_groups": 121,
|
||||
"observed_exact_duplicate_extras": 121
|
||||
},
|
||||
"native_data_audit": {
|
||||
"observed_markdown_documents_before_reconciliation": 130,
|
||||
"cross_source_identical_documents": 121,
|
||||
"union_unique_documents": 167,
|
||||
"display_policy": "PREFER_NATIVE_THEN_COALESCE_EXACT_SHA256"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"schema": "hololake.local-runtime-acceptance/v1",
|
||||
"receipt_id": "GH-HOLOLAKE-LOCAL-RUNTIME-20260815-001",
|
||||
"state": "PASS_100_LOCAL_RUNTIME_ONLY",
|
||||
"observed_at": "2026-08-15T09:18:00+08:00",
|
||||
"installed_application": {
|
||||
"path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake 第一阶段.app",
|
||||
"bundle_identifier": "world.guanghu.hololake",
|
||||
"executable_sha256": "310c1b71a96b67d79435b62a553fcdba2377ab3c4ae04a19a1ffa0113ce5bcea",
|
||||
"cdhash": "7a856cc7495c74ea7aa7c3b6127b73aabf6fe9fb",
|
||||
"signature": "AD_HOC_HARDENED_RUNTIME_LOCAL_ONLY",
|
||||
"developer_id_team": null,
|
||||
"apple_notarized": false
|
||||
},
|
||||
"personal_space": {
|
||||
"display_name_readback": "冰朔",
|
||||
"identity_cardinality": 1,
|
||||
"restart_readback": true,
|
||||
"manual_task_form_present": false
|
||||
},
|
||||
"knowledge": {
|
||||
"legacy_source": "HOLOLAKE_ERA_READ_ONLY",
|
||||
"legacy_document_count": 279,
|
||||
"native_document_count": 130,
|
||||
"raw_document_count": 409,
|
||||
"unique_document_count": 167,
|
||||
"exact_duplicate_documents_folded": 242,
|
||||
"native_git_head": "520d5c053dd22b7d73b375973ba1f31edfee82e6",
|
||||
"folder_import": "PASS",
|
||||
"repeated_folder_import": "ALREADY_PRESENT_NO_SECOND_TREE",
|
||||
"nested_tree_open": "PASS",
|
||||
"document_independent_scroll": "PASS",
|
||||
"gfm_table_and_wiki_link_projection": "PASS",
|
||||
"native_edit_with_optimistic_hash_and_git_receipt": "PASS",
|
||||
"search_query": "连续性验收",
|
||||
"search_result": "导入/hololake-runtime-acceptance-knowledge/notes/continuity.md",
|
||||
"restart_readback": true,
|
||||
"legacy_source_modified": false
|
||||
},
|
||||
"code_channel": {
|
||||
"input_url": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart",
|
||||
"normalized_remote": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
|
||||
"git_head": "a4f67a0634dd3664452bb2ea2747d13df1479f99",
|
||||
"branch": "main",
|
||||
"repository_clean": true,
|
||||
"repository_tree_open": "PASS",
|
||||
"json_machine_to_knowledge_projection": "PASS",
|
||||
"credential_prompt_observed": false,
|
||||
"restart_readback": true,
|
||||
"push_or_deploy_attempted": false
|
||||
},
|
||||
"desktop_convergence": {
|
||||
"active_desktop_entry_count": 1,
|
||||
"active_entry": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake 第一阶段.app",
|
||||
"recoverable_archive": "/Users/bingshuolingdianyuanhe/Documents/HoloLake Archive/2026-08-15-0912-pre-real-knowledge",
|
||||
"deleted": false
|
||||
},
|
||||
"automated_gates": {
|
||||
"node_tests": "50_PASS",
|
||||
"rust_tests": "59_PASS",
|
||||
"typescript_vite_build": "PASS",
|
||||
"npm_dependency_audit": "0_VULNERABILITIES",
|
||||
"cargo_clippy_deny_warnings": "PASS",
|
||||
"codesign_strict_verification": "PASS"
|
||||
},
|
||||
"exclusions": [
|
||||
"FULL_STAGE_ONE_PRODUCT_COMPLETION",
|
||||
"PUBLIC_DEVELOPER_ID_SIGNATURE",
|
||||
"APPLE_NOTARIZATION",
|
||||
"PUBLIC_RELEASE_ACTIVATION",
|
||||
"SOURCE_PUBLICATION",
|
||||
"SERVER_DEPLOYMENT",
|
||||
"PERSONA_BINDING_RECOVERY_COMPLETION",
|
||||
"CURRENT_CODEX_NATIVE_CONNECTION_RUNTIME_ACCEPTANCE",
|
||||
"GH_PNCC_NONZERO_RUNTIME_PROJECTION"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue