feat(hololake): implement GHS-014 stage-one home
This commit is contained in:
parent
f4c896d15c
commit
4d2415a35b
15 changed files with 891 additions and 49 deletions
|
|
@ -5,6 +5,7 @@
|
|||
*.tsbuildinfo
|
||||
/vite.config.js
|
||||
/vite.config.d.ts
|
||||
/work/
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"schema": "hololake.ghs-014-stage-one-ui-receipt/v1",
|
||||
"receipt_id": "GHS-014-STAGE-ONE-UI-20260813-001",
|
||||
"state": "IMPLEMENTED_AND_VISUALLY_VERIFIED",
|
||||
"source": {
|
||||
"repository": "REPO-012",
|
||||
"verified_main_commit": "457bf99b3a2af8b97290ed71ca3515e599fc17ae",
|
||||
"brain_entry": {
|
||||
"path": "skills/shared/guanghu-client-ui-brain/BRAIN.hdlp",
|
||||
"sha256": "b895b7f0eb8af6125b25caaa90c18fde966d884bbe1dec661d5bdfe08ff4e97a"
|
||||
},
|
||||
"five_lakes_tokens": {
|
||||
"path": "skills/shared/guanghu-client-ui-brain/references/theme-tokens-five-lakes.md",
|
||||
"sha256": "563f6ce6afe665deb52af64671d0a31b9673c120a596ea7ff34548fe7359b5a8"
|
||||
},
|
||||
"locked_home_style": {
|
||||
"path": "skills/shared/guanghu-client-ui-brain/references/home-screen-style-locked-v1.2.md",
|
||||
"sha256": "23f0ab6655cd128de2956f28451fead7b78b0a0c920fb0497efba312299ef167"
|
||||
}
|
||||
},
|
||||
"stage_one_correction": {
|
||||
"visible_five_domain_gates": false,
|
||||
"reason": "The current human decision submerges all five domains as dynamic routed infrastructure during stage one.",
|
||||
"visible_focus": "PERSONAL_CHANNEL_EXTERNAL_PROGRAMMING_AI_ENTRY"
|
||||
},
|
||||
"implemented": {
|
||||
"theme_ids": ["night", "dawn", "nebula", "candle", "clear"],
|
||||
"theme_names": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"],
|
||||
"theme_changes_tokens_only": true,
|
||||
"primary_window_verified": "1180x760",
|
||||
"compact_window_verified": "800x600",
|
||||
"hard_status_dividers_present": false,
|
||||
"native_home_status_projection": true,
|
||||
"false_connected_state_allowed": false
|
||||
},
|
||||
"concept_to_render_fidelity_ledger": [
|
||||
{
|
||||
"point": "lake atmosphere and horizon",
|
||||
"result": "PRESERVED",
|
||||
"note": "Night lake, mist, star fragments and warm horizon remain the visual field."
|
||||
},
|
||||
{
|
||||
"point": "single primary focus",
|
||||
"result": "PRESERVED_AND_SIMPLIFIED",
|
||||
"note": "The personal-channel language entry is the only above-fold product focus."
|
||||
},
|
||||
{
|
||||
"point": "connection state",
|
||||
"result": "CORRECTED_TO_RUNTIME_TRUTH",
|
||||
"note": "The concept sample said connected; the product says waiting, resumable or connected from native state."
|
||||
},
|
||||
{
|
||||
"point": "status separation",
|
||||
"result": "CORRECTED_TO_GHS_014",
|
||||
"note": "Concept divider lines were removed in favor of spacing and soft light."
|
||||
},
|
||||
{
|
||||
"point": "update state",
|
||||
"result": "CORRECTED_TO_FAIL_CLOSED_TRUTH",
|
||||
"note": "Unprovisioned release trust is shown as networking disabled, never as current/latest."
|
||||
}
|
||||
],
|
||||
"above_fold_copy_diff": {
|
||||
"concept": ["光湖", "个人频道", "语言进入这里,成为可见、可续的现实。"],
|
||||
"render": ["光湖", "个人频道", "语言进入这里,成为可见、可续的现实。"],
|
||||
"difference": "NONE"
|
||||
},
|
||||
"verification": {
|
||||
"browser_visual_inspection": "PASS",
|
||||
"semantic_dom_inspection": "PASS",
|
||||
"five_theme_invariant_check": "PASS",
|
||||
"latest_render_visually_compared_with_concept": true
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
The stage-one shell is a Tauri v2 application with a Rust-owned local core and a React human projection. The five domains and their server fleet remain submerged system infrastructure rather than primary stage-one navigation.
|
||||
|
||||
## Stage-one human projection
|
||||
|
||||
The home surface follows the verified GHS-014 five-lakes visual grammar while applying the current stage-one correction: it exposes one personal-channel entry, not five domain gates. The five named lake themes are token groups only and cannot change layout, copy, routing or authority. The surface reads a native status projection; only an authenticated direct connector can illuminate the live-connected state. A stored resumable session remains visibly distinct from a live connection.
|
||||
|
||||
## External programming AI entry
|
||||
|
||||
MCP may discover HoloLake, but it does not own continuity. The installed application starts a user-only Unix socket broker. A programming AI opens or resumes a HoloLake-issued local session, then uses the installed executable's `--connector` mode for newline-delimited protocol traffic. Session secrets are stored only as hashes. Events use exact cursors and idempotency keys.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
{
|
||||
"schema": "hololake.native-desktop-foundation/v1",
|
||||
"record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001",
|
||||
"state": "TAURI_FOUNDATION_WITH_DIRECT_LOCAL_BROKER_DYNAMIC_ROUTING_AND_PNCC_READ_ONLY_CORE",
|
||||
"state": "TAURI_STAGE_ONE_HOME_WITH_DIRECT_LOCAL_BROKER_DYNAMIC_ROUTING_AND_PNCC_READ_ONLY_CORE",
|
||||
"canonical_shell": "TAURI_V2_RUST_REACT",
|
||||
"product_ui_implementation_started": false,
|
||||
"selected_visual_direction_present": false,
|
||||
"product_ui_implementation_started": true,
|
||||
"selected_visual_direction_present": true,
|
||||
"selected_visual_direction": "GHS-014_FIVE_LAKES_PERSONAL_CHANNEL_STAGE_ONE",
|
||||
"selected_visual_direction_receipt": "audit/ghs-014-stage-one-ui-receipt.json",
|
||||
"home_status_is_native_projection": true,
|
||||
"home_online_state_requires_authenticated_direct_connection": true,
|
||||
"five_lake_themes_implemented": true,
|
||||
"upstream_product_update_endpoints": [],
|
||||
"production_update_endpoint_owner": "HOLOLAKE_ONLY",
|
||||
"release_broadcast_contract": "contracts/release-broadcast.schema.json",
|
||||
|
|
@ -37,7 +42,7 @@
|
|||
"pncc_receipt_projection_implemented": true,
|
||||
"pncc_authenticated_direct_broker_integration_implemented": true,
|
||||
"pncc_human_mount_registration_implemented": false,
|
||||
"pncc_human_mount_registration_gate": "NATIVE_FILE_PICKER_EXACT_CONFIRMATION_AND_UI_ACCEPTANCE_REQUIRED",
|
||||
"pncc_human_mount_registration_gate": "NATIVE_FILE_PICKER_EXACT_CONFIRMATION_REQUIRED",
|
||||
"pncc_internal_model_inference_implemented": false,
|
||||
"pncc_execution_limb_implemented": false,
|
||||
"pncc_persona_lease_acquisition_implemented": false,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
mpsc, Arc,
|
||||
};
|
||||
use std::thread;
|
||||
|
|
@ -40,10 +40,33 @@ const MAX_REQUEST_BYTES: u64 = 1024 * 1024;
|
|||
|
||||
pub struct DirectLocalBrokerHandle {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
authenticated_connections: Arc<AtomicUsize>,
|
||||
socket_path: PathBuf,
|
||||
worker: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl DirectLocalBrokerHandle {
|
||||
pub fn active_connection_count(&self) -> usize {
|
||||
self.authenticated_connections.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticatedConnectionGuard(Arc<AtomicUsize>);
|
||||
|
||||
impl Drop for AuthenticatedConnectionGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
struct BrokerStorageRoots {
|
||||
session: PathBuf,
|
||||
routing: PathBuf,
|
||||
pncc_mount: PathBuf,
|
||||
pncc_remote: PathBuf,
|
||||
pncc_projection: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for DirectLocalBrokerHandle {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown.store(true, Ordering::Release);
|
||||
|
|
@ -296,7 +319,9 @@ fn start_at(
|
|||
)?;
|
||||
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let authenticated_connections = Arc::new(AtomicUsize::new(0));
|
||||
let worker_shutdown = Arc::clone(&shutdown);
|
||||
let worker_authenticated_connections = Arc::clone(&authenticated_connections);
|
||||
let worker_socket = socket_path.clone();
|
||||
let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
|
||||
let worker = thread::Builder::new()
|
||||
|
|
@ -305,12 +330,15 @@ fn start_at(
|
|||
let _ = ready_sender.send(());
|
||||
serve(
|
||||
listener,
|
||||
session_root,
|
||||
routing_root,
|
||||
pncc_mount_root,
|
||||
pncc_remote_root,
|
||||
pncc_projection_root,
|
||||
BrokerStorageRoots {
|
||||
session: session_root,
|
||||
routing: routing_root,
|
||||
pncc_mount: pncc_mount_root,
|
||||
pncc_remote: pncc_remote_root,
|
||||
pncc_projection: pncc_projection_root,
|
||||
},
|
||||
&worker_shutdown,
|
||||
&worker_authenticated_connections,
|
||||
);
|
||||
let _ = fs::remove_file(worker_socket);
|
||||
})
|
||||
|
|
@ -321,6 +349,7 @@ fn start_at(
|
|||
|
||||
Ok(DirectLocalBrokerHandle {
|
||||
shutdown,
|
||||
authenticated_connections,
|
||||
socket_path,
|
||||
worker: Some(worker),
|
||||
})
|
||||
|
|
@ -328,12 +357,9 @@ fn start_at(
|
|||
|
||||
fn serve(
|
||||
listener: UnixListener,
|
||||
session_root: PathBuf,
|
||||
routing_root: PathBuf,
|
||||
pncc_mount_root: PathBuf,
|
||||
pncc_remote_root: PathBuf,
|
||||
pncc_projection_root: PathBuf,
|
||||
roots: BrokerStorageRoots,
|
||||
shutdown: &AtomicBool,
|
||||
authenticated_connections: &Arc<AtomicUsize>,
|
||||
) {
|
||||
while !shutdown.load(Ordering::Acquire) {
|
||||
match listener.accept() {
|
||||
|
|
@ -341,11 +367,15 @@ fn serve(
|
|||
if shutdown.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
let root = session_root.clone();
|
||||
let routes = routing_root.clone();
|
||||
let pncc_mounts = pncc_mount_root.clone();
|
||||
let pncc_remote = pncc_remote_root.clone();
|
||||
let pncc_projection = pncc_projection_root.clone();
|
||||
if stream.set_nonblocking(false).is_err() {
|
||||
continue;
|
||||
}
|
||||
let root = roots.session.clone();
|
||||
let routes = roots.routing.clone();
|
||||
let pncc_mounts = roots.pncc_mount.clone();
|
||||
let pncc_remote = roots.pncc_remote.clone();
|
||||
let pncc_projection = roots.pncc_projection.clone();
|
||||
let client_authenticated_connections = Arc::clone(authenticated_connections);
|
||||
let _ = thread::Builder::new()
|
||||
.name("hololake-direct-local-client".into())
|
||||
.spawn(move || {
|
||||
|
|
@ -356,6 +386,7 @@ fn serve(
|
|||
&pncc_mounts,
|
||||
&pncc_remote,
|
||||
&pncc_projection,
|
||||
client_authenticated_connections,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
@ -374,12 +405,14 @@ fn serve_connection(
|
|||
pncc_mount_root: &Path,
|
||||
pncc_remote_root: &Path,
|
||||
pncc_projection_root: &Path,
|
||||
authenticated_connections: Arc<AtomicUsize>,
|
||||
) {
|
||||
let read_stream = match stream.try_clone() {
|
||||
Ok(stream) => stream,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut reader = BufReader::new(read_stream);
|
||||
let mut authenticated_connection = None;
|
||||
loop {
|
||||
let mut bytes = Vec::new();
|
||||
let read = match reader
|
||||
|
|
@ -393,6 +426,7 @@ fn serve_connection(
|
|||
if read == 0 {
|
||||
return;
|
||||
}
|
||||
let request_authenticates = request_establishes_authenticated_connection(&bytes);
|
||||
let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") {
|
||||
BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE")
|
||||
} else {
|
||||
|
|
@ -405,6 +439,12 @@ fn serve_connection(
|
|||
&bytes[..bytes.len() - 1],
|
||||
)
|
||||
};
|
||||
if response.ok && request_authenticates && authenticated_connection.is_none() {
|
||||
authenticated_connections.fetch_add(1, Ordering::AcqRel);
|
||||
authenticated_connection = Some(AuthenticatedConnectionGuard(Arc::clone(
|
||||
&authenticated_connections,
|
||||
)));
|
||||
}
|
||||
if serde_json::to_writer(&mut stream, &response).is_err()
|
||||
|| stream.write_all(b"\n").is_err()
|
||||
|| stream.flush().is_err()
|
||||
|
|
@ -417,6 +457,18 @@ fn serve_connection(
|
|||
}
|
||||
}
|
||||
|
||||
fn request_establishes_authenticated_connection(bytes: &[u8]) -> bool {
|
||||
serde_json::from_slice::<serde_json::Value>(bytes)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("operation")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.is_some_and(|operation| matches!(operation.as_str(), "OPEN_SESSION" | "RESUME_SESSION"))
|
||||
}
|
||||
|
||||
fn dispatch(
|
||||
session_root: &Path,
|
||||
routing_root: &Path,
|
||||
|
|
@ -840,4 +892,68 @@ mod tests {
|
|||
assert_eq!(verified["result"]["emptyMeansOffline"], false);
|
||||
assert_eq!(verified["result"]["returnedEventCount"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_authenticated_persistent_connector_counts_as_online() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let socket = temp.path().join("runtime/broker.sock");
|
||||
let descriptor = temp.path().join("broker.json");
|
||||
let sessions = temp.path().join("sessions");
|
||||
let routes = temp.path().join("routes");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
fs::create_dir_all(&routes).unwrap();
|
||||
let broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap();
|
||||
|
||||
let probe = UnixStream::connect(&socket).unwrap();
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
assert_eq!(broker.active_connection_count(), 0);
|
||||
drop(probe);
|
||||
|
||||
let ticket = issue_ticket_at(
|
||||
&sessions,
|
||||
IssueDiscoveryTicketInput {
|
||||
account_id: "human-1".into(),
|
||||
lane_id: "DEV-1".into(),
|
||||
client_instance_id: "codex-1".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut connector = UnixStream::connect(&socket).unwrap();
|
||||
serde_json::to_writer(
|
||||
&mut connector,
|
||||
&serde_json::json!({
|
||||
"operation": "OPEN_SESSION",
|
||||
"input": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "DEV-1",
|
||||
"clientInstanceId": "codex-1",
|
||||
"discoveryTicket": ticket.discovery_ticket
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
connector.write_all(b"\n").unwrap();
|
||||
assert!(request_establishes_authenticated_connection(
|
||||
br#"{"operation":"OPEN_SESSION"}"#
|
||||
));
|
||||
let mut response = String::new();
|
||||
let mut response_reader = BufReader::new(connector.try_clone().unwrap());
|
||||
response_reader.read_line(&mut response).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&response).unwrap()["ok"],
|
||||
true
|
||||
);
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
assert_eq!(broker.active_connection_count(), 1);
|
||||
|
||||
drop(connector);
|
||||
drop(response_reader);
|
||||
for _ in 0..20 {
|
||||
if broker.active_connection_count() == 0 {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(broker.active_connection_count(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,6 +203,43 @@ pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn active_session_count_at(root: &Path) -> Result<usize, String> {
|
||||
let accounts = root.join("accounts");
|
||||
if !accounts.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(&accounts)
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?
|
||||
{
|
||||
let entry = entry
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let kind = entry
|
||||
.file_type()
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
if !kind.is_dir() || kind.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
let active_path = entry.path().join("active-session.json");
|
||||
if !active_path.exists() {
|
||||
continue;
|
||||
}
|
||||
let metadata = fs::symlink_metadata(&active_path)
|
||||
.map_err(|error| format!("HOLOLAKE_ACTIVE_SESSION_INVALID: {error}"))?;
|
||||
if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
|
||||
return Err("HOLOLAKE_ACTIVE_SESSION_INVALID".into());
|
||||
}
|
||||
let active: ActiveSessionRecord = read_json(&active_path, "ACTIVE_SESSION")?;
|
||||
if active.schema != SESSION_SCHEMA
|
||||
|| active.account_key != entry.file_name().to_string_lossy()
|
||||
{
|
||||
return Err("HOLOLAKE_ACTIVE_SESSION_INVALID".into());
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) fn open_at(
|
||||
root: &Path,
|
||||
input: OpenSessionInput,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::direct_local_broker::DirectLocalBrokerHandle;
|
||||
use crate::direct_local_session::{active_session_count_at, direct_session_root};
|
||||
use crate::pncc_receipt_projection::{pncc_projection_root, projection_event_count_at};
|
||||
use crate::pncc_repository_binding::{mounted_repository_count_at, pncc_repository_mount_root};
|
||||
use crate::release_trust::release_trust_state;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HoloLakeHomeStatus {
|
||||
pub schema: &'static str,
|
||||
pub direct_local_broker_state: &'static str,
|
||||
pub direct_connection_count: usize,
|
||||
pub resumable_session_count: usize,
|
||||
pub code_repository_mount_count: usize,
|
||||
pub pncc_receipt_count: usize,
|
||||
pub update_state: &'static str,
|
||||
pub automatic_upstream_updates: bool,
|
||||
pub mcp_role: &'static str,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_hololake_home_status(
|
||||
app: AppHandle,
|
||||
broker: State<'_, DirectLocalBrokerHandle>,
|
||||
) -> Result<HoloLakeHomeStatus, String> {
|
||||
let session_root = direct_session_root(&app)?;
|
||||
let mount_root = pncc_repository_mount_root(&app)?;
|
||||
let projection_root = pncc_projection_root(&app)?;
|
||||
Ok(HoloLakeHomeStatus {
|
||||
schema: "hololake.home-status/v1",
|
||||
direct_local_broker_state: "READY",
|
||||
direct_connection_count: broker.active_connection_count(),
|
||||
resumable_session_count: active_session_count_at(&session_root)?,
|
||||
code_repository_mount_count: mounted_repository_count_at(&mount_root)?,
|
||||
pncc_receipt_count: projection_event_count_at(&projection_root)?,
|
||||
update_state: release_trust_state()?,
|
||||
automatic_upstream_updates: false,
|
||||
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
mod direct_local_broker;
|
||||
mod direct_local_session;
|
||||
mod dynamic_capability_routing;
|
||||
mod home_status;
|
||||
mod local_development_bridge;
|
||||
mod pncc_receipt_projection;
|
||||
mod pncc_remote_git;
|
||||
|
|
@ -17,6 +18,7 @@ pub fn run_connector() -> Result<(), String> {
|
|||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
home_status::get_hololake_home_status,
|
||||
direct_local_session::issue_direct_local_discovery_ticket,
|
||||
direct_local_session::open_direct_local_session,
|
||||
direct_local_session::resume_direct_local_session,
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn projection_event_count_at(root: &Path) -> Result<usize, String> {
|
||||
let _lock = lock_projection(root)?;
|
||||
Ok(load_and_verify(root)?.events.len())
|
||||
}
|
||||
|
||||
pub(crate) fn append_repository_binding_at(
|
||||
root: &Path,
|
||||
receipt: &PnccRepositoryMountReceipt,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,32 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
|
|||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn mounted_repository_count_at(root: &Path) -> Result<usize, String> {
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(root)
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?
|
||||
{
|
||||
let entry =
|
||||
entry.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let kind = entry
|
||||
.file_type()
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let path = entry.path();
|
||||
if kind.is_symlink()
|
||||
|| !kind.is_file()
|
||||
|| path.extension().and_then(|value| value.to_str()) != Some("json")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let record: PnccRepositoryMountRecord = read_json(&path)?;
|
||||
if record.schema != MOUNT_SCHEMA || mount_path(root, &record.mount_id) != path {
|
||||
return Err("PNCC_REPOSITORY_MOUNT_RECORD_INVALID".into());
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_mounted_at(
|
||||
root: &Path,
|
||||
input: InspectMountedPnccRepositoryInput,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ enum ValidatedReleaseTrust {
|
|||
},
|
||||
}
|
||||
|
||||
pub(crate) fn release_trust_state() -> Result<&'static str, String> {
|
||||
match validate_release_trust(EMBEDDED_RELEASE_TRUST)? {
|
||||
ValidatedReleaseTrust::Disabled => Ok("UNPROVISIONED_FAIL_CLOSED"),
|
||||
ValidatedReleaseTrust::Enabled { .. } => Ok("READY_HUMAN_CONFIRMATION_REQUIRED"),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_release_trust(raw: &str) -> Result<ValidatedReleaseTrust, String> {
|
||||
let trust: ReleaseTrust = serde_json::from_str(raw)
|
||||
.map_err(|error| format!("invalid HoloLake release trust document: {error}"))?;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
167
product-source/hololake-native-desktop/src/design-tokens.css
Normal file
167
product-source/hololake-native-desktop/src/design-tokens.css
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
:root,
|
||||
[data-theme='night'] {
|
||||
/* Primitive tokens — GHS-014 five-lakes v1.3, night lake. */
|
||||
--primitive-bg-a: #0a1020;
|
||||
--primitive-bg-b: #0b1220;
|
||||
--primitive-bg-c: #070c16;
|
||||
--primitive-bg-d: #04070d;
|
||||
--primitive-star-core: #f7ebc8;
|
||||
--primitive-star-bright: #ffefc2;
|
||||
--primitive-text-primary: rgba(234, 238, 248, 0.96);
|
||||
--primitive-text-body: rgba(222, 228, 242, 0.88);
|
||||
--primitive-text-aux: rgba(196, 206, 228, 0.62);
|
||||
--primitive-text-faint: rgba(180, 192, 220, 0.4);
|
||||
--primitive-nebula-a: rgba(91, 111, 196, 0.13);
|
||||
--primitive-nebula-b: rgba(138, 111, 201, 0.05);
|
||||
--primitive-glass: rgba(178, 198, 255, 0.045);
|
||||
--primitive-glass-hover: rgba(178, 198, 255, 0.075);
|
||||
--primitive-glass-edge: rgba(202, 216, 255, 0.13);
|
||||
--primitive-glass-top: rgba(255, 255, 255, 0.06);
|
||||
--primitive-mist-a: rgba(157, 180, 255, 0.1);
|
||||
--primitive-mist-b: rgba(247, 235, 200, 0.04);
|
||||
--primitive-cool-glow: rgba(157, 180, 255, 0.42);
|
||||
--primitive-warm-glow: rgba(247, 235, 200, 0.5);
|
||||
--primitive-shadow: rgba(0, 0, 0, 0.34);
|
||||
--primitive-scene-opacity: 0.76;
|
||||
|
||||
/* Semantic tokens. */
|
||||
--surface-sky: var(--primitive-bg-a);
|
||||
--surface-horizon: var(--primitive-bg-b);
|
||||
--surface-lake: var(--primitive-bg-c);
|
||||
--surface-depth: var(--primitive-bg-d);
|
||||
--content-primary: var(--primitive-text-primary);
|
||||
--content-secondary: var(--primitive-text-body);
|
||||
--content-muted: var(--primitive-text-aux);
|
||||
--content-faint: var(--primitive-text-faint);
|
||||
--accent-light: var(--primitive-star-core);
|
||||
--accent-light-strong: var(--primitive-star-bright);
|
||||
--state-ready: #b8d7c4;
|
||||
--state-waiting: #cfbddf;
|
||||
--state-quiet: var(--primitive-text-aux);
|
||||
--focus-ring: rgba(247, 235, 200, 0.7);
|
||||
|
||||
/* Component tokens. */
|
||||
--button-primary-bg: rgba(247, 235, 200, 0.92);
|
||||
--button-primary-text: #15181f;
|
||||
--button-primary-shadow: 0 12px 42px rgba(247, 235, 200, 0.14);
|
||||
--button-secondary-bg: var(--primitive-glass);
|
||||
--button-secondary-text: var(--content-secondary);
|
||||
--button-secondary-edge: var(--primitive-glass-edge);
|
||||
--panel-bg: rgba(8, 13, 25, 0.72);
|
||||
--panel-edge: var(--primitive-glass-edge);
|
||||
--status-dot: var(--accent-light);
|
||||
}
|
||||
|
||||
[data-theme='dawn'] {
|
||||
--primitive-bg-a: #f5f7fc;
|
||||
--primitive-bg-b: #eaeff8;
|
||||
--primitive-bg-c: #dfe8f4;
|
||||
--primitive-bg-d: #d3e0ef;
|
||||
--primitive-star-core: #c9a24b;
|
||||
--primitive-star-bright: #a96f2c;
|
||||
--primitive-text-primary: rgba(22, 33, 58, 0.94);
|
||||
--primitive-text-body: rgba(36, 49, 76, 0.86);
|
||||
--primitive-text-aux: rgba(72, 88, 120, 0.62);
|
||||
--primitive-text-faint: rgba(98, 112, 142, 0.48);
|
||||
--primitive-nebula-a: rgba(122, 144, 220, 0.12);
|
||||
--primitive-nebula-b: rgba(240, 196, 150, 0.12);
|
||||
--primitive-glass: rgba(255, 255, 255, 0.34);
|
||||
--primitive-glass-hover: rgba(255, 255, 255, 0.52);
|
||||
--primitive-glass-edge: rgba(30, 50, 90, 0.12);
|
||||
--primitive-glass-top: rgba(255, 255, 255, 0.65);
|
||||
--primitive-mist-a: rgba(255, 255, 255, 0.55);
|
||||
--primitive-mist-b: rgba(250, 214, 150, 0.3);
|
||||
--primitive-cool-glow: rgba(96, 124, 205, 0.35);
|
||||
--primitive-warm-glow: rgba(201, 162, 75, 0.5);
|
||||
--primitive-shadow: rgba(42, 66, 105, 0.16);
|
||||
--primitive-scene-opacity: 0.11;
|
||||
--state-ready: #3f7058;
|
||||
--state-waiting: #725b82;
|
||||
--panel-bg: rgba(247, 250, 255, 0.76);
|
||||
--button-primary-bg: rgba(45, 59, 84, 0.92);
|
||||
--button-primary-text: #f8f4e9;
|
||||
--button-primary-shadow: 0 12px 36px rgba(45, 59, 84, 0.16);
|
||||
}
|
||||
|
||||
[data-theme='nebula'] {
|
||||
--primitive-bg-a: #150e26;
|
||||
--primitive-bg-b: #160f28;
|
||||
--primitive-bg-c: #0e0919;
|
||||
--primitive-bg-d: #080510;
|
||||
--primitive-star-core: #ede4f6;
|
||||
--primitive-star-bright: #f8f1ff;
|
||||
--primitive-text-primary: rgba(238, 234, 248, 0.96);
|
||||
--primitive-text-body: rgba(226, 220, 240, 0.88);
|
||||
--primitive-text-aux: rgba(202, 194, 226, 0.62);
|
||||
--primitive-text-faint: rgba(186, 178, 214, 0.42);
|
||||
--primitive-nebula-a: rgba(138, 111, 201, 0.24);
|
||||
--primitive-nebula-b: rgba(91, 111, 196, 0.11);
|
||||
--primitive-glass: rgba(196, 178, 255, 0.05);
|
||||
--primitive-glass-hover: rgba(196, 178, 255, 0.09);
|
||||
--primitive-glass-edge: rgba(210, 198, 255, 0.14);
|
||||
--primitive-glass-top: rgba(255, 255, 255, 0.07);
|
||||
--primitive-mist-a: rgba(170, 150, 255, 0.12);
|
||||
--primitive-mist-b: rgba(239, 217, 168, 0.05);
|
||||
--primitive-cool-glow: rgba(170, 150, 255, 0.45);
|
||||
--primitive-warm-glow: rgba(239, 217, 168, 0.5);
|
||||
--primitive-shadow: rgba(0, 0, 0, 0.38);
|
||||
--primitive-scene-opacity: 0.44;
|
||||
--panel-bg: rgba(18, 12, 34, 0.76);
|
||||
}
|
||||
|
||||
[data-theme='candle'] {
|
||||
--primitive-bg-a: #1b130b;
|
||||
--primitive-bg-b: #181108;
|
||||
--primitive-bg-c: #100b05;
|
||||
--primitive-bg-d: #0a0703;
|
||||
--primitive-star-core: #ffd9a0;
|
||||
--primitive-star-bright: #ffe7be;
|
||||
--primitive-text-primary: rgba(246, 238, 226, 0.96);
|
||||
--primitive-text-body: rgba(236, 226, 210, 0.88);
|
||||
--primitive-text-aux: rgba(214, 198, 176, 0.62);
|
||||
--primitive-text-faint: rgba(196, 180, 158, 0.42);
|
||||
--primitive-nebula-a: rgba(214, 150, 80, 0.13);
|
||||
--primitive-nebula-b: rgba(160, 110, 70, 0.07);
|
||||
--primitive-glass: rgba(255, 214, 160, 0.045);
|
||||
--primitive-glass-hover: rgba(255, 214, 160, 0.08);
|
||||
--primitive-glass-edge: rgba(255, 220, 170, 0.13);
|
||||
--primitive-glass-top: rgba(255, 255, 255, 0.06);
|
||||
--primitive-mist-a: rgba(255, 196, 110, 0.09);
|
||||
--primitive-mist-b: rgba(255, 214, 140, 0.04);
|
||||
--primitive-cool-glow: rgba(214, 178, 140, 0.4);
|
||||
--primitive-warm-glow: rgba(255, 196, 110, 0.55);
|
||||
--primitive-shadow: rgba(0, 0, 0, 0.38);
|
||||
--primitive-scene-opacity: 0.4;
|
||||
--panel-bg: rgba(24, 17, 9, 0.78);
|
||||
}
|
||||
|
||||
[data-theme='clear'] {
|
||||
--primitive-bg-a: #f1f5f8;
|
||||
--primitive-bg-b: #e8eef3;
|
||||
--primitive-bg-c: #dee7ed;
|
||||
--primitive-bg-d: #d2dee7;
|
||||
--primitive-star-core: #5f7e9f;
|
||||
--primitive-star-bright: #41628a;
|
||||
--primitive-text-primary: rgba(20, 36, 52, 0.94);
|
||||
--primitive-text-body: rgba(34, 50, 68, 0.86);
|
||||
--primitive-text-aux: rgba(66, 86, 106, 0.62);
|
||||
--primitive-text-faint: rgba(92, 110, 128, 0.48);
|
||||
--primitive-nebula-a: rgba(100, 140, 180, 0.1);
|
||||
--primitive-nebula-b: rgba(140, 170, 200, 0.08);
|
||||
--primitive-glass: rgba(255, 255, 255, 0.32);
|
||||
--primitive-glass-hover: rgba(255, 255, 255, 0.5);
|
||||
--primitive-glass-edge: rgba(20, 50, 80, 0.12);
|
||||
--primitive-glass-top: rgba(255, 255, 255, 0.65);
|
||||
--primitive-mist-a: rgba(255, 255, 255, 0.55);
|
||||
--primitive-mist-b: rgba(190, 214, 234, 0.35);
|
||||
--primitive-cool-glow: rgba(78, 112, 150, 0.35);
|
||||
--primitive-warm-glow: rgba(170, 138, 70, 0.5);
|
||||
--primitive-shadow: rgba(37, 66, 90, 0.14);
|
||||
--primitive-scene-opacity: 0.08;
|
||||
--state-ready: #3f7058;
|
||||
--state-waiting: #6a5b7a;
|
||||
--panel-bg: rgba(246, 250, 253, 0.76);
|
||||
--button-primary-bg: rgba(45, 66, 85, 0.92);
|
||||
--button-primary-text: #f6f2e9;
|
||||
--button-primary-shadow: 0 12px 36px rgba(45, 66, 85, 0.15);
|
||||
}
|
||||
|
|
@ -1,24 +1,256 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { StrictMode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import './design-tokens.css'
|
||||
import './styles.css'
|
||||
|
||||
function FoundationScreen() {
|
||||
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
||||
type Panel = 'channel' | 'receipts' | 'settings' | null
|
||||
|
||||
interface HomeStatus {
|
||||
schema: string
|
||||
directLocalBrokerState: string
|
||||
directConnectionCount: number
|
||||
resumableSessionCount: number
|
||||
codeRepositoryMountCount: number
|
||||
pnccReceiptCount: number
|
||||
updateState: string
|
||||
automaticUpstreamUpdates: boolean
|
||||
mcpRole: string
|
||||
}
|
||||
|
||||
interface DiscoveryTicketReceipt {
|
||||
schema: string
|
||||
state: string
|
||||
accountKey: string
|
||||
laneId: string
|
||||
clientInstanceId: string
|
||||
discoveryTicket: string
|
||||
issuedAtUnixMs: number
|
||||
receiptId: string
|
||||
}
|
||||
|
||||
interface ReceiptEvent {
|
||||
sequence: number
|
||||
kind: string
|
||||
observedAtUnixMs: number
|
||||
eventHash: string
|
||||
}
|
||||
|
||||
interface ReceiptProjection {
|
||||
events: ReceiptEvent[]
|
||||
returnedEventCount: number
|
||||
lastSequence: number
|
||||
}
|
||||
|
||||
const themes: Array<{ id: ThemeId; name: string }> = [
|
||||
{ id: 'night', name: '夜湖星光' },
|
||||
{ id: 'dawn', name: '晨湖曦光' },
|
||||
{ id: 'nebula', name: '星云紫夜' },
|
||||
{ id: 'candle', name: '烛畔暖湖' },
|
||||
{ id: 'clear', name: '清浅澄湖' },
|
||||
]
|
||||
|
||||
const previewStatus: HomeStatus = {
|
||||
schema: 'hololake.home-status/preview',
|
||||
directLocalBrokerState: 'PREVIEW',
|
||||
directConnectionCount: 0,
|
||||
resumableSessionCount: 0,
|
||||
codeRepositoryMountCount: 0,
|
||||
pnccReceiptCount: 0,
|
||||
updateState: 'UNPROVISIONED_FAIL_CLOSED',
|
||||
automaticUpstreamUpdates: false,
|
||||
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
|
||||
}
|
||||
|
||||
function getOrCreateLocalId(key: string, prefix: string) {
|
||||
const existing = window.localStorage.getItem(key)
|
||||
if (existing) return existing
|
||||
const generated = `${prefix}-${crypto.randomUUID()}`
|
||||
window.localStorage.setItem(key, generated)
|
||||
return generated
|
||||
}
|
||||
|
||||
function Icon({ name }: { name: 'settings' | 'arrow' | 'receipt' | 'close' | 'copy' }) {
|
||||
const paths = {
|
||||
settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-1.9 1.9-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.04 1.56V20h-2.7v-.08a1.7 1.7 0 0 0-1.05-1.56 1.7 1.7 0 0 0-1.88.34l-.06.06-1.9-1.9.06-.06A1.7 1.7 0 0 0 7.72 15a1.7 1.7 0 0 0-1.56-1.04H6v-2.7h.08A1.7 1.7 0 0 0 7.64 10a1.7 1.7 0 0 0-.34-1.88l-.06-.06 1.9-1.9.06.06a1.7 1.7 0 0 0 1.88.34A1.7 1.7 0 0 0 12.12 5V5h2.7v.08a1.7 1.7 0 0 0 1.04 1.56 1.7 1.7 0 0 0 1.88-.34l.06-.06 1.9 1.9-.06.06a1.7 1.7 0 0 0-.34 1.88 1.7 1.7 0 0 0 1.56 1.04H21v2.7h-.08A1.7 1.7 0 0 0 19.4 15Z"/></>,
|
||||
arrow: <><path d="M5 12h13"/><path d="m14 7 5 5-5 5"/></>,
|
||||
receipt: <><path d="M7 3h10v18l-2.5-1.5L12 21l-2.5-1.5L7 21Z"/><path d="M10 8h4M10 12h4"/></>,
|
||||
close: <><path d="m7 7 10 10M17 7 7 17"/></>,
|
||||
copy: <><rect x="8" y="8" width="10" height="11" rx="2"/><path d="M15 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2"/></>,
|
||||
}
|
||||
return <svg viewBox="0 0 24 24" aria-hidden="true">{paths[name]}</svg>
|
||||
}
|
||||
|
||||
function HoloLakeApp() {
|
||||
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
|
||||
const [status, setStatus] = useState<HomeStatus>(previewStatus)
|
||||
const [panel, setPanel] = useState<Panel>(null)
|
||||
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
|
||||
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
try {
|
||||
setStatus(await invoke<HomeStatus>('get_hololake_home_status'))
|
||||
} catch {
|
||||
setStatus(previewStatus)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshStatus()
|
||||
const timer = window.setInterval(() => void refreshStatus(), 3000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [refreshStatus])
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = theme
|
||||
window.localStorage.setItem('hololake-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const connectionState = useMemo(() => {
|
||||
if (status.directConnectionCount > 0) return { label: '已连接', detail: `${status.directConnectionCount} 个直连通道在线`, tone: 'ready' }
|
||||
if (status.resumableSessionCount > 0) return { label: '可续接', detail: `${status.resumableSessionCount} 个会话等待续接`, tone: 'waiting' }
|
||||
return { label: '等待连接', detail: '本机直连核心已就绪', tone: 'quiet' }
|
||||
}, [status])
|
||||
|
||||
const openReceipts = async () => {
|
||||
setPanel('receipts')
|
||||
try {
|
||||
const projection = await invoke<ReceiptProjection>('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } })
|
||||
setReceipts(projection.events)
|
||||
} catch {
|
||||
setReceipts([])
|
||||
}
|
||||
}
|
||||
|
||||
const issueInvitation = async () => {
|
||||
setMessage('')
|
||||
try {
|
||||
const receipt = await invoke<DiscoveryTicketReceipt>('issue_direct_local_discovery_ticket', {
|
||||
input: {
|
||||
accountId: getOrCreateLocalId('hololake-local-account', 'human-local'),
|
||||
laneId: 'personal-channel',
|
||||
clientInstanceId: getOrCreateLocalId('hololake-external-ai-client', 'external-ai'),
|
||||
},
|
||||
})
|
||||
setTicket(receipt)
|
||||
setMessage('连接邀请已生成。它只在这台电脑上有效。')
|
||||
} catch (error) {
|
||||
setMessage(`暂时无法生成连接邀请:${String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const invitationText = ticket ? JSON.stringify({
|
||||
schema: 'hololake.direct-local-invitation/v1',
|
||||
transport: 'INSTALLED_HOLOLAKE_EXECUTABLE_WITH_CONNECTOR_FLAG',
|
||||
connectorArgument: '--connector',
|
||||
openSession: {
|
||||
operation: 'OPEN_SESSION',
|
||||
input: {
|
||||
accountId: window.localStorage.getItem('hololake-local-account'),
|
||||
laneId: ticket.laneId,
|
||||
clientInstanceId: ticket.clientInstanceId,
|
||||
discoveryTicket: ticket.discoveryTicket,
|
||||
},
|
||||
},
|
||||
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
|
||||
}, null, 2) : ''
|
||||
|
||||
const copyInvitation = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(invitationText)
|
||||
setMessage('已复制。把它交给桌面上的编程 AI 即可。')
|
||||
} catch {
|
||||
setMessage('复制没有成功,请选中下方邀请内容后复制。')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<p className="eyebrow">HoloLake Native Desktop</p>
|
||||
<h1>新的光湖桌面主线正在建立信任根</h1>
|
||||
<p>
|
||||
当前只完成最小 Tauri 外壳与更新安全边界。知识工作区将在双供体审计和视觉方向确定后开始。
|
||||
</p>
|
||||
<dl>
|
||||
<div><dt>上游更新</dt><dd>已关闭</dd></div>
|
||||
<div><dt>光湖发布广播</dt><dd>等待正式端点与公钥</dd></div>
|
||||
<div><dt>下载与安装</dt><dd>必须由人类选择</dd></div>
|
||||
</dl>
|
||||
</main>
|
||||
<div className="world-shell">
|
||||
<div className="scene-image" aria-hidden="true" />
|
||||
<div className="nebula nebula-one" aria-hidden="true" />
|
||||
<div className="nebula nebula-two" aria-hidden="true" />
|
||||
<div className="mist" aria-hidden="true" />
|
||||
<div className="stars" aria-hidden="true">{Array.from({ length: 18 }, (_, index) => <i key={index} style={{ '--star-index': index } as React.CSSProperties} />)}</div>
|
||||
<div className="glimmers" aria-hidden="true">{Array.from({ length: 11 }, (_, index) => <i key={index} style={{ '--glimmer-index': index } as React.CSSProperties} />)}</div>
|
||||
|
||||
<header className="world-header">
|
||||
<div className="world-mark">
|
||||
<span className="world-name">光湖</span>
|
||||
<span className="world-route">个人频道</span>
|
||||
</div>
|
||||
<button className="icon-button" type="button" aria-label="打开设置" onClick={() => setPanel('settings')}><Icon name="settings" /></button>
|
||||
</header>
|
||||
|
||||
<main className="home-content">
|
||||
<section className="hero" aria-labelledby="home-title">
|
||||
<p className="route-kicker">HOLOLAKE · PERSONAL CHANNEL</p>
|
||||
<h1 id="home-title">语言进入这里,<br />成为可见、可续的现实。</h1>
|
||||
<p className="hero-copy">外部编程 AI 从本机进入。HoloLake 保存路线、边界与回执;五个域安静地运行在湖底。</p>
|
||||
<div className="hero-actions">
|
||||
<button className="primary-action" type="button" onClick={() => setPanel('channel')}>
|
||||
{status.directConnectionCount > 0 ? '已连接 · 查看通道' : '连接编程 AI'}
|
||||
<Icon name="arrow" />
|
||||
</button>
|
||||
<button className="secondary-action" type="button" onClick={() => void openReceipts()}><Icon name="receipt" />查看回执</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="status-cluster" aria-label="光湖运行状态">
|
||||
<article className="status-item">
|
||||
<span className={`status-light ${connectionState.tone}`} aria-hidden="true" />
|
||||
<div><h2>本机直连通道</h2><p>{connectionState.detail}</p></div>
|
||||
</article>
|
||||
<article className="status-item">
|
||||
<span className={`status-light ${status.codeRepositoryMountCount > 0 ? 'ready' : 'quiet'}`} aria-hidden="true" />
|
||||
<div><h2>代码通道仓库</h2><p>{status.codeRepositoryMountCount > 0 ? `已验证 · ${status.codeRepositoryMountCount} 个仓库` : '等待人类确认绑定'}</p></div>
|
||||
</article>
|
||||
<article className="status-item">
|
||||
<span className={`status-light ${status.updateState.startsWith('READY') ? 'ready' : 'quiet'}`} aria-hidden="true" />
|
||||
<div><h2>更新广播</h2><p>{status.updateState.startsWith('READY') ? '仅接收光湖签名广播' : '信任根未配置 · 已关闭联网'}</p></div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="world-footer">
|
||||
<p><b>GH-AIOS</b><span>光湖人工智能操作系统</span></p>
|
||||
<p className="trust-note"><i />你的工作留在本机 · 连接与更新均需确认</p>
|
||||
</footer>
|
||||
|
||||
{panel && <div className="panel-backdrop" role="presentation" onMouseDown={(event) => event.target === event.currentTarget && setPanel(null)}>
|
||||
<section className="detail-panel" role="dialog" aria-modal="true" aria-labelledby="panel-title">
|
||||
<button className="panel-close" type="button" aria-label="关闭" onClick={() => setPanel(null)}><Icon name="close" /></button>
|
||||
{panel === 'channel' && <>
|
||||
<p className="panel-kicker">个人频道</p>
|
||||
<h2 id="panel-title">让编程 AI 从本机进入</h2>
|
||||
<p className="panel-intro">MCP 只负责发现和恢复。进入后,编程 AI 使用 HoloLake 的本机直连通道;电脑不关,驻留核心就一直在。</p>
|
||||
<div className="connection-summary"><span className={`status-light ${connectionState.tone}`} /><div><b>{connectionState.label}</b><span>{connectionState.detail}</span></div></div>
|
||||
{ticket ? <>
|
||||
<button className="primary-action panel-action" type="button" onClick={() => void copyInvitation()}><Icon name="copy" />复制给编程 AI</button>
|
||||
<pre className="invitation-preview">{invitationText}</pre>
|
||||
</> : <button className="primary-action panel-action" type="button" onClick={() => void issueInvitation()}>生成一次性连接邀请<Icon name="arrow" /></button>}
|
||||
{message && <p className="panel-message" aria-live="polite">{message}</p>}
|
||||
<p className="boundary-note">连接邀请不赋予服务器权限。写入、发布与更新仍分别确认并留下回执。</p>
|
||||
</>}
|
||||
{panel === 'receipts' && <>
|
||||
<p className="panel-kicker">可核验的发生</p>
|
||||
<h2 id="panel-title">代码通道回执</h2>
|
||||
<p className="panel-intro">这里是只读投影,不是权限来源。没有回执,也不代表服务器离线。</p>
|
||||
{receipts.length ? <ol className="receipt-list">{receipts.map((receipt) => <li key={receipt.sequence}><span>{receipt.sequence.toString().padStart(2, '0')}</span><div><b>{receipt.kind === 'REPOSITORY_BINDING_REVALIDATED' ? '仓库绑定已复核' : '远端对象读取已验证'}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></li>)}</ol> : <div className="empty-state"><i /><b>湖面还没有新回执</b><span>完成代码仓库绑定或读取后,这里会出现可核验记录。</span></div>}
|
||||
</>}
|
||||
{panel === 'settings' && <>
|
||||
<p className="panel-kicker">湖面设置</p>
|
||||
<h2 id="panel-title">选择你看见光湖的方式</h2>
|
||||
<p className="panel-intro">只改变光与色。布局、文案、连接边界和底层路由保持不变。</p>
|
||||
<div className="theme-list">{themes.map((choice) => <button key={choice.id} type="button" className={theme === choice.id ? 'selected' : ''} onClick={() => setTheme(choice.id)}><i className={`theme-swatch ${choice.id}`} /><span><b>{choice.name}</b><small>{choice.id === 'night' ? '当前定版基色' : '五色湖定版主题'}</small></span><em>{theme === choice.id ? '已选择' : ''}</em></button>)}</div>
|
||||
<p className="boundary-note">上游自动更新保持关闭。正式更新只接受 HoloLake 自有信任根的签名广播,并由你确认。</p>
|
||||
</>}
|
||||
</section>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode><FoundationScreen /></StrictMode>,
|
||||
)
|
||||
createRoot(document.getElementById('root')!).render(<StrictMode><HoloLakeApp /></StrictMode>)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,138 @@
|
|||
* { box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
color: #eaf0ff;
|
||||
background: #07101f;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei UI", sans-serif;
|
||||
color: var(--content-primary);
|
||||
background: var(--surface-depth);
|
||||
font-synthesis: none;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; }
|
||||
main { max-width: 720px; margin: 0 auto; padding: 12vh 32px; }
|
||||
.eyebrow { color: #82a7ff; letter-spacing: .08em; text-transform: uppercase; }
|
||||
h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1.05; }
|
||||
p { color: #b7c5e5; line-height: 1.7; }
|
||||
dl { margin-top: 48px; border-top: 1px solid #263554; }
|
||||
dl div { display: flex; justify-content: space-between; gap: 24px; padding: 16px 0; border-bottom: 1px solid #263554; }
|
||||
dt { color: #8fa4cf; }
|
||||
dd { margin: 0; text-align: right; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; overflow: hidden; }
|
||||
button { font: inherit; }
|
||||
button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }
|
||||
|
||||
.world-shell {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
background: linear-gradient(180deg, var(--surface-sky) 0%, var(--surface-horizon) 40%, var(--surface-lake) 65%, var(--surface-depth) 100%);
|
||||
transition: background 450ms ease, color 300ms ease;
|
||||
}
|
||||
|
||||
.scene-image {
|
||||
position: absolute;
|
||||
z-index: -4;
|
||||
inset: 25% 0 0;
|
||||
background-image: linear-gradient(180deg, transparent 0%, var(--surface-depth) 100%), url('./assets/hololake-night-lake.png');
|
||||
background-size: cover;
|
||||
background-position: center 42%;
|
||||
opacity: var(--primitive-scene-opacity);
|
||||
mix-blend-mode: screen;
|
||||
transition: opacity 450ms ease;
|
||||
}
|
||||
|
||||
.nebula { position: absolute; z-index: -3; border-radius: 50%; filter: blur(42px); pointer-events: none; }
|
||||
.nebula-one { width: 58vw; height: 38vw; left: -12vw; top: -18vw; background: radial-gradient(circle, var(--primitive-nebula-a), transparent 68%); }
|
||||
.nebula-two { width: 48vw; height: 32vw; right: -12vw; top: 4vh; background: radial-gradient(circle, var(--primitive-nebula-b), transparent 70%); }
|
||||
.mist { position: absolute; z-index: -2; left: 0; right: 0; top: 38%; height: 36%; background: radial-gradient(ellipse at 50% 52%, var(--primitive-mist-a), var(--primitive-mist-b) 45%, transparent 72%); filter: blur(24px); pointer-events: none; }
|
||||
|
||||
.stars i, .glimmers i { position: absolute; display: block; border-radius: 50%; pointer-events: none; }
|
||||
.stars i { width: calc(1px + (var(--star-index) % 3) * 1px); height: calc(1px + (var(--star-index) % 3) * 1px); left: calc(4% + (var(--star-index) * 5.2%)); top: calc(5% + (var(--star-index) % 5) * 5.4%); background: var(--accent-light); opacity: calc(.16 + (var(--star-index) % 4) * .09); box-shadow: 0 0 8px var(--primitive-warm-glow); animation: breathe calc(4s + (var(--star-index) % 5) * .7s) ease-in-out infinite; }
|
||||
.glimmers i { width: calc(2px + (var(--glimmer-index) % 3) * 2px); height: 2px; left: calc(9% + (var(--glimmer-index) * 8.1%)); top: calc(52% + (var(--glimmer-index) % 4) * 6.4%); background: var(--accent-light); box-shadow: 0 0 12px var(--primitive-cool-glow); opacity: .18; animation: glimmer calc(5s + (var(--glimmer-index) % 4) * 1.2s) ease-in-out infinite; }
|
||||
@keyframes breathe { 50% { opacity: .75; transform: scale(1.5); } }
|
||||
@keyframes glimmer { 50% { opacity: .7; transform: translateX(16px) scaleX(2.4); } }
|
||||
|
||||
.world-header { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(28px, 5vw, 70px); }
|
||||
.world-mark { display: flex; align-items: baseline; gap: 17px; }
|
||||
.world-name { font-size: 21px; font-weight: 610; letter-spacing: .18em; }
|
||||
.world-route { color: var(--content-muted); font-size: 12px; letter-spacing: .16em; }
|
||||
.icon-button, .panel-close { border: 0; color: var(--content-muted); background: transparent; display: grid; place-items: center; cursor: pointer; border-radius: 50%; transition: color 180ms ease, background 180ms ease; }
|
||||
.icon-button { width: 42px; height: 42px; }
|
||||
.icon-button:hover, .panel-close:hover { color: var(--accent-light); background: var(--primitive-glass-hover); }
|
||||
.icon-button svg, .panel-close svg, .hero-actions svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
|
||||
|
||||
.home-content { min-height: calc(100vh - 156px); display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(285px, .45fr); align-items: center; gap: clamp(48px, 7vw, 112px); padding: 0 clamp(54px, 8vw, 126px) 28px; }
|
||||
.hero { max-width: 680px; transform: translateY(-2vh); }
|
||||
.route-kicker, .panel-kicker { margin: 0 0 21px; color: var(--accent-light); font-size: 11px; font-weight: 630; letter-spacing: .22em; }
|
||||
.hero h1 { margin: 0; max-width: 700px; color: var(--content-primary); font-size: clamp(40px, 4.15vw, 58px); font-weight: 430; line-height: 1.22; letter-spacing: -.035em; text-wrap: balance; }
|
||||
.hero-copy { max-width: 540px; margin: 27px 0 0; color: var(--content-muted); font-size: 14px; line-height: 1.9; letter-spacing: .035em; }
|
||||
.hero-actions { display: flex; align-items: center; gap: 14px; margin-top: 37px; }
|
||||
.primary-action, .secondary-action { min-height: 46px; border-radius: 999px; padding: 0 22px; display: inline-flex; align-items: center; justify-content: center; gap: 12px; cursor: pointer; transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease; }
|
||||
.primary-action { border: 0; color: var(--button-primary-text); background: var(--button-primary-bg); box-shadow: var(--button-primary-shadow); font-weight: 620; }
|
||||
.secondary-action { color: var(--button-secondary-text); border: 1px solid var(--button-secondary-edge); background: var(--button-secondary-bg); backdrop-filter: blur(18px); }
|
||||
.primary-action:hover, .secondary-action:hover { transform: translateY(-2px); }
|
||||
.secondary-action:hover { background: var(--primitive-glass-hover); }
|
||||
|
||||
.status-cluster { display: grid; gap: 30px; width: min(100%, 330px); justify-self: end; }
|
||||
.status-item { display: grid; grid-template-columns: 10px 1fr; align-items: start; gap: 15px; padding: 5px 0; }
|
||||
.status-item h2 { margin: 0 0 8px; color: var(--content-secondary); font-size: 13px; font-weight: 590; letter-spacing: .06em; }
|
||||
.status-item p { margin: 0; color: var(--content-muted); opacity: .72; font-size: 11.5px; line-height: 1.5; letter-spacing: .04em; }
|
||||
.status-light { width: 6px; height: 6px; margin-top: 5px; flex: 0 0 auto; border-radius: 50%; background: var(--state-quiet); box-shadow: 0 0 14px currentColor; }
|
||||
.status-light.ready { color: var(--state-ready); background: currentColor; }
|
||||
.status-light.waiting { color: var(--state-waiting); background: currentColor; }
|
||||
.status-light.quiet { color: var(--state-quiet); background: currentColor; }
|
||||
|
||||
.world-footer { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(28px, 5vw, 70px); color: var(--content-faint); font-size: 10.5px; letter-spacing: .09em; }
|
||||
.world-footer p { margin: 0; display: flex; align-items: center; gap: 13px; }
|
||||
.world-footer b { color: var(--accent-light); font-size: 11.5px; letter-spacing: .14em; }
|
||||
.trust-note i { width: 4px; height: 4px; border-radius: 50%; background: var(--accent-light); opacity: .65; }
|
||||
|
||||
.panel-backdrop { position: fixed; z-index: 20; inset: 0; display: flex; align-items: stretch; justify-content: flex-end; background: rgba(2, 4, 9, .28); backdrop-filter: blur(5px); }
|
||||
.detail-panel { position: relative; width: min(500px, 92vw); height: 100%; overflow-y: auto; padding: 80px 54px 50px; color: var(--content-primary); background: var(--panel-bg); box-shadow: -26px 0 80px var(--primitive-shadow); backdrop-filter: blur(38px) saturate(115%); animation: panel-in 240ms ease-out; }
|
||||
@keyframes panel-in { from { transform: translateX(24px); opacity: 0; } }
|
||||
.panel-close { position: absolute; right: 34px; top: 30px; width: 38px; height: 38px; }
|
||||
.detail-panel h2 { margin: 0; max-width: 380px; font-size: 31px; font-weight: 480; line-height: 1.28; letter-spacing: -.025em; }
|
||||
.panel-intro { margin: 19px 0 30px; color: var(--content-muted); font-size: 13px; line-height: 1.85; }
|
||||
.connection-summary { display: flex; align-items: flex-start; gap: 15px; margin: 32px 0; padding: 20px 22px; border-radius: 20px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
|
||||
.connection-summary div { display: grid; gap: 6px; }
|
||||
.connection-summary b { color: var(--content-secondary); font-size: 13px; }
|
||||
.connection-summary span { color: var(--content-faint); font-size: 11.5px; }
|
||||
.panel-action { min-height: 46px; }
|
||||
.panel-message { color: var(--content-muted); font-size: 11.5px; line-height: 1.6; }
|
||||
.invitation-preview { max-height: 180px; overflow: auto; margin: 18px 0; padding: 17px; border-radius: 16px; color: var(--content-faint); background: rgba(0, 0, 0, .13); font: 10px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-all; }
|
||||
.boundary-note { margin-top: 34px; color: var(--content-faint); font-size: 10.5px; line-height: 1.75; }
|
||||
|
||||
.receipt-list { list-style: none; display: grid; gap: 14px; margin: 30px 0 0; padding: 0; }
|
||||
.receipt-list li { display: grid; grid-template-columns: 34px 1fr; gap: 14px; align-items: start; padding: 16px 18px; border-radius: 17px; background: var(--primitive-glass); }
|
||||
.receipt-list li > span { color: var(--accent-light); font: 11px ui-monospace, monospace; }
|
||||
.receipt-list li div { display: grid; gap: 7px; }
|
||||
.receipt-list b { color: var(--content-secondary); font-size: 12px; font-weight: 580; }
|
||||
.receipt-list small { color: var(--content-faint); font-size: 10px; }
|
||||
.empty-state { min-height: 260px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
|
||||
.empty-state i { width: 7px; height: 7px; margin-bottom: 20px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 32px 12px var(--primitive-warm-glow); }
|
||||
.empty-state b { color: var(--content-secondary); font-size: 13px; font-weight: 570; }
|
||||
.empty-state span { max-width: 260px; margin-top: 10px; color: var(--content-faint); font-size: 11px; line-height: 1.7; }
|
||||
|
||||
.theme-list { display: grid; gap: 9px; margin-top: 30px; }
|
||||
.theme-list button { width: 100%; min-height: 66px; display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: 14px; padding: 10px 14px; border: 0; border-radius: 17px; color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }
|
||||
.theme-list button:hover, .theme-list button.selected { background: var(--primitive-glass-hover); }
|
||||
.theme-list span { display: grid; gap: 5px; }
|
||||
.theme-list b { font-size: 12.5px; font-weight: 590; white-space: nowrap; }
|
||||
.theme-list small, .theme-list em { color: var(--content-faint); font-size: 10px; font-style: normal; }
|
||||
.theme-list button.selected em { color: var(--accent-light); }
|
||||
.theme-swatch { width: 34px; height: 34px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(255,255,255,.18), 0 6px 18px var(--primitive-shadow); }
|
||||
.theme-swatch.night { background: linear-gradient(145deg, #0a1020, #04070d 70%); }
|
||||
.theme-swatch.dawn { background: linear-gradient(145deg, #f5f7fc, #d3e0ef 70%); }
|
||||
.theme-swatch.nebula { background: linear-gradient(145deg, #281b47, #080510 70%); }
|
||||
.theme-swatch.candle { background: linear-gradient(145deg, #4a2d13, #0a0703 70%); }
|
||||
.theme-swatch.clear { background: linear-gradient(145deg, #f1f5f8, #b9ccda 70%); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.home-content { grid-template-columns: 1fr; align-content: center; gap: 50px; padding-inline: 62px; }
|
||||
.hero { transform: none; }
|
||||
.status-cluster { width: 100%; grid-template-columns: repeat(3, 1fr); justify-self: stretch; gap: 26px; }
|
||||
}
|
||||
|
||||
@media (max-height: 640px) {
|
||||
.world-header, .world-footer { height: 62px; }
|
||||
.home-content { min-height: calc(100vh - 124px); }
|
||||
.hero h1 { font-size: clamp(36px, 5vw, 56px); }
|
||||
.hero-copy { margin-top: 18px; }
|
||||
.hero-actions { margin-top: 25px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue