feat: merge HoloLake desktop into unified 0.3.0

This commit is contained in:
冰朔 2026-08-16 12:54:47 +08:00
commit b7461c66c5
37 changed files with 8732 additions and 613 deletions

View file

@ -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.

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,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,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"
}
}

View file

@ -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"
]
}