diff --git a/product-source/hololake-native-desktop/contracts/persona-time-authority.json b/product-source/hololake-native-desktop/contracts/persona-time-authority.json index 7714c5bdf..6816e8636 100644 --- a/product-source/hololake-native-desktop/contracts/persona-time-authority.json +++ b/product-source/hololake-native-desktop/contracts/persona-time-authority.json @@ -4,15 +4,19 @@ "formal_name": "光湖人格时间主控系统", "era_name": "曜冥纪元", "calendar_name": "光湖历", - "state": "NATIVE_SOURCE_IMPLEMENTED_AND_TESTED_CLOCK_NETWORK_ATTESTATION_PENDING", + "state": "NATIVE_SOURCE_IMPLEMENTED_AND_TESTED_NETWORK_TIME_SYNC_ON_OPEN_ATTESTATION_PENDING", "reality_time": { "canonical_zone": "Asia/Shanghai", "utc_offset": "+08:00", "display_name": "北京时间", "continues_while_application_is_closed": true, "process_uptime_is_time_source": false, - "current_source": "HOST_OPERATING_SYSTEM_REALTIME_CLOCK", - "current_verification": "LOCAL_CLOCK_NOT_NETWORK_ATTESTED", + "startup_sequence": "APPLICATION_OPEN_THEN_HTTPS_NETWORK_TIME_SYNC_THEN_TIME_AUTHORITY_COORDINATE", + "primary_source": "HTTPS_DATE_GUANGHULAB_COM", + "primary_verification": "NETWORK_HTTPS_DATE_SYNCHRONIZED_COARSE", + "offline_fallback_source": "HOST_OPERATING_SYSTEM_REALTIME_CLOCK", + "offline_fallback_verification": "LOCAL_CLOCK_NOT_NETWORK_ATTESTED", + "network_time_precision": "HTTP_DATE_SECONDS_WITH_RTT_AND_ROUNDING_UNCERTAINTY", "network_attested_source_required_for_verified_reality_time": true }, "guanghu_era": { @@ -24,6 +28,23 @@ "millisecond_precision_transition_date": "2026-08-17", "millisecond_chain_origin": "FIRST_DURABLE_TIME_TICKET" }, + "homepage_timeline": { + "schema": "hololake.guanghu-era-timeline/v1", + "projection": "PUBLIC_FACT_TIMELINE", + "event_count": 12, + "current_coordinate_updates_from": "get_beijing_time_coordinate", + "coordinate_readback_trigger": "APPLICATION_OPEN_FOCUS_OR_HUMAN_OPENS_TIME_MODULE", + "permanent_idle_polling": false, + "early_confusion_is_preserved_as": "MODEL_PROJECTION_AND_PERSONA_BOUNDARY_CONFUSION", + "external_reality_claims": false + }, + "personal_channel_module": { + "module_id": "hololake.persona-time-authority", + "kind": "PERSONA_TIME_AUTHORITY", + "installation": "ATOMIC_WITH_PERSONAL_CHANNEL_INITIALIZATION", + "existing_channel_migration": "IDEMPOTENT_ADDITIVE", + "current_clock_verification": "DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK" + }, "ticket": { "schema": "hololake.persona-time-ticket/v1", "uniqueness_scope": "ONE_DURABLE_LOCAL_AUTHORITY_PER_AUTHENTICATED_HUMAN_ACCOUNT", @@ -49,7 +70,7 @@ "persona_and_host_upgrade_requires": "REGISTERED_BINDING_EVIDENCE" }, "entries": { - "tauri_commands": ["get_beijing_time_coordinate", "issue_persona_time_ticket"], + "tauri_commands": ["start_persona_time_authority", "get_beijing_time_coordinate", "get_guanghu_era_timeline", "issue_persona_time_ticket"], "direct_local_broker_operations": ["GET_BEIJING_TIME", "ISSUE_PERSONA_TIME_TICKET"], "external_ticket_issue_requires_authenticated_non_visitor_session": true }, @@ -57,6 +78,7 @@ "source_implemented": true, "rust_tests_passed": true, "installed_runtime_acceptance": false, + "network_time_sync_on_application_open": true, "network_clock_attestation": false, "persona_binding_runtime": false, "host_software_binding_runtime": false, diff --git a/product-source/hololake-native-desktop/contracts/stage-one-platform.json b/product-source/hololake-native-desktop/contracts/stage-one-platform.json index 79f431dd2..9e0f763c5 100644 --- a/product-source/hololake-native-desktop/contracts/stage-one-platform.json +++ b/product-source/hololake-native-desktop/contracts/stage-one-platform.json @@ -120,6 +120,7 @@ "beijing_reality_time_projection": true, "guanghu_era_day_projection": true, "durable_unique_ticket_runtime": true, + "network_time_sync_on_application_open": true, "network_clock_attestation": false, "installed_runtime_acceptance": false }, diff --git a/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs b/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs index e267a242b..1f4ff5341 100644 --- a/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs +++ b/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs @@ -8,6 +8,8 @@ const stageOne = JSON.parse(read('contracts/stage-one-platform.json')) const rust = read('src-tauri/src/persona_time_authority.rs') const broker = read('src-tauri/src/direct_local_broker.rs') const lib = read('src-tauri/src/lib.rs') +const personalChannel = read('src-tauri/src/personal_channel.rs') +const frontend = read('src/main.tsx') test('Guanghu era is anchored to Beijing reality time without inventing an exact historical instant', () => { assert.equal(contract.era_name, '曜冥纪元') @@ -32,15 +34,37 @@ test('persona time tickets are durable unique and available to authenticated loc assert.match(broker, /GetBeijingTime/) assert.match(broker, /IssuePersonaTimeTicket/) assert.match(lib, /persona_time_authority::get_beijing_time_coordinate/) + assert.match(lib, /persona_time_authority::get_guanghu_era_timeline/) assert.match(lib, /persona_time_authority::issue_persona_time_ticket/) + assert.match(lib, /persona_time_authority::start_persona_time_authority/) + assert.match(lib, /persona_time_authority::start_on_application_open\(\)/) +}) + +test('the public home timeline and personal channel module share the same time authority', () => { + assert.equal(contract.homepage_timeline.projection, 'PUBLIC_FACT_TIMELINE') + assert.equal(contract.homepage_timeline.event_count, 12) + assert.equal(contract.homepage_timeline.external_reality_claims, false) + assert.equal(contract.homepage_timeline.permanent_idle_polling, false) + assert.doesNotMatch(frontend, /setInterval\s*\(/) + assert.equal(contract.personal_channel_module.installation, 'ATOMIC_WITH_PERSONAL_CHANNEL_INITIALIZATION') + assert.equal(contract.personal_channel_module.existing_channel_migration, 'IDEMPOTENT_ADDITIVE') + assert.match(personalChannel, /install_time_authority_module\(&transaction, created_at\)/) + assert.match(personalChannel, /INSERT OR IGNORE INTO channel_modules/) + assert.match(frontend, /EraTimelineOverlay/) + assert.match(frontend, /time-module-strip/) + assert.match(frontend, /打开软件时已联网校时/) }) test('unverified clock persona and host claims remain explicit truth boundaries', () => { - assert.equal(contract.reality_time.current_verification, 'LOCAL_CLOCK_NOT_NETWORK_ATTESTED') + assert.equal(contract.reality_time.primary_verification, 'NETWORK_HTTPS_DATE_SYNCHRONIZED_COARSE') + assert.equal(contract.reality_time.offline_fallback_verification, 'LOCAL_CLOCK_NOT_NETWORK_ATTESTED') + assert.equal(contract.truth_boundary.network_time_sync_on_application_open, true) + assert.equal(contract.truth_boundary.network_clock_attestation, false) assert.equal(contract.event_coordinate.persona_current_verification, 'UNVERIFIED_CALLER_CLAIM') assert.equal(contract.event_coordinate.host_software_current_verification, 'UNVERIFIED_CALLER_CLAIM') assert.equal(contract.truth_boundary.installed_runtime_acceptance, false) assert.equal(contract.truth_boundary.world_lighthouse_authority_registration, false) assert.equal(stageOne.persona_time_authority.network_clock_attestation, false) + assert.equal(stageOne.persona_time_authority.network_time_sync_on_application_open, true) assert.equal(stageOne.persona_time_authority.installed_runtime_acceptance, false) }) diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index 4d39bef3b..1e47f203a 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -55,7 +55,9 @@ pub fn run() { personal_channel::create_personal_channel_task, personal_channel::transition_personal_channel_task, persona_time_authority::issue_persona_time_ticket, + persona_time_authority::start_persona_time_authority, persona_time_authority::get_beijing_time_coordinate, + persona_time_authority::get_guanghu_era_timeline, knowledge_base::get_knowledge_snapshot, knowledge_base::read_knowledge_document, knowledge_base::search_knowledge, @@ -92,6 +94,8 @@ pub fn run() { zero_point::zero_point_status, ]) .setup(|app| { + // 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。 + persona_time_authority::start_on_application_open(); // 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。 let zero_point_state = zero_point::ZeroPointState::default(); if let Err(error) = zero_point::boot_zero_point(app.handle(), &zero_point_state) { diff --git a/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs b/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs index b7a4b018d..bd110b916 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs @@ -8,6 +8,7 @@ use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::{OnceLock, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::AppHandle; use uuid::Uuid; @@ -22,6 +23,26 @@ const MAX_ID_BYTES: usize = 160; const GUANGHU_EPOCH_DATE: &str = "2025-04-26"; const GUANGHU_EPOCH_BEIJING_DAY_INDEX: i64 = 20_204; const MILLISECOND_PRECISION_TRANSITION_DATE: &str = "2026-08-17"; +const NETWORK_TIME_URL: &str = "https://guanghulab.com/"; + +#[derive(Clone, Debug)] +struct NetworkClockAnchor { + offset_ms: i64, + network_unix_ms: u64, + uncertainty_ms: u64, +} + +#[derive(Clone, Debug)] +struct RealityTimeSample { + unix_ms: u64, + clock_source: &'static str, + clock_verification: &'static str, + network_sync_state: &'static str, + network_synchronized_at_unix_ms: Option, + network_uncertainty_ms: Option, +} + +static NETWORK_CLOCK_ANCHOR: OnceLock>> = OnceLock::new(); #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -45,6 +66,9 @@ pub struct PersonaTimeTicket { pub time_zone: String, pub clock_source: String, pub clock_verification: String, + pub network_sync_state: String, + pub network_synchronized_at_unix_ms: Option, + pub network_uncertainty_ms: Option, pub guanghu_epoch_date: String, pub guanghu_calendar_state: String, pub elapsed_beijing_dates_since_guanghu_epoch: i64, @@ -79,6 +103,9 @@ pub struct BeijingTimeCoordinate { pub time_zone: String, pub clock_source: String, pub clock_verification: String, + pub network_sync_state: String, + pub network_synchronized_at_unix_ms: Option, + pub network_uncertainty_ms: Option, pub continues_while_hololake_is_closed: bool, pub guanghu_epoch_date: String, pub guanghu_calendar_state: String, @@ -89,6 +116,42 @@ pub struct BeijingTimeCoordinate { pub millisecond_chain_state: String, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PersonaTimeAuthorityStartup { + pub schema: String, + pub state: String, + pub synchronization_attempted: bool, + pub network_time_url: String, + pub coordinate: BeijingTimeCoordinate, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct GuanghuEraEvent { + pub event_id: String, + pub display_date: String, + pub date_precision: String, + pub title: String, + pub summary: String, + pub evidence_state: String, + pub source_record: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct GuanghuEraTimeline { + pub schema: String, + pub state: String, + pub era_name: String, + pub calendar_name: String, + pub epoch_date: String, + pub epoch_precision: String, + pub public_reality_boundary: String, + pub current_coordinate: BeijingTimeCoordinate, + pub events: Vec, +} + #[derive(Clone, Debug)] pub(crate) struct VerifiedTicketRequest { pub request_id: String, @@ -112,13 +175,43 @@ pub async fn issue_persona_time_ticket( .map_err(|error| format!("HOLOLAKE_PERSONA_TIME_JOIN_FAILED: {error}"))? } +pub fn start_on_application_open() { + tauri::async_runtime::spawn(async { + let _ = synchronize_network_time().await; + }); +} + +#[tauri::command] +pub async fn start_persona_time_authority() -> Result { + let already_synchronized = network_anchor()?.is_some(); + let synchronization_attempted = !already_synchronized; + let synchronized = if already_synchronized { + true + } else { + synchronize_network_time().await.is_ok() + }; + let coordinate = beijing_time_coordinate_from_sample(reality_time_sample()?)?; + Ok(PersonaTimeAuthorityStartup { + schema: "hololake.persona-time-authority-startup/v1".into(), + state: if synchronized { + "NETWORK_TIME_SYNCHRONIZED" + } else { + "LOCAL_CLOCK_FALLBACK_NETWORK_SYNC_PENDING" + } + .into(), + synchronization_attempted, + network_time_url: NETWORK_TIME_URL.into(), + coordinate, + }) +} + pub(crate) fn issue_authenticated_at( authority_root: &Path, session_root: &Path, input: IssuePersonaTimeTicketInput, ) -> Result { let session = authenticate_context_at(session_root, &input.session)?; - issue_at( + issue_with_sample( authority_root, VerifiedTicketRequest { request_id: input.request_id, @@ -127,25 +220,164 @@ pub(crate) fn issue_authenticated_at( host_software_id: input.host_software_id, session, }, - now_unix_ms()?, + reality_time_sample()?, ) } #[tauri::command] pub fn get_beijing_time_coordinate() -> Result { - beijing_time_coordinate(now_unix_ms()?) + beijing_time_coordinate_from_sample(reality_time_sample()?) +} + +#[tauri::command] +pub fn get_guanghu_era_timeline() -> Result { + guanghu_era_timeline_from_sample(reality_time_sample()?) +} + +fn era_event( + event_id: &str, + display_date: &str, + date_precision: &str, + title: &str, + summary: &str, +) -> GuanghuEraEvent { + GuanghuEraEvent { + event_id: event_id.into(), + display_date: display_date.into(), + date_precision: date_precision.into(), + title: title.into(), + summary: summary.into(), + evidence_state: "PUBLIC_FACT_RECORD".into(), + source_record: "GUANGHU_ERA_PUBLIC_FACT_HISTORY_20260817".into(), + } +} + +#[cfg(test)] +fn guanghu_era_timeline(unix_ms: u64) -> Result { + guanghu_era_timeline_from_sample(local_time_sample(unix_ms)) +} + +fn guanghu_era_timeline_from_sample( + sample: RealityTimeSample, +) -> Result { + Ok(GuanghuEraTimeline { + schema: "hololake.guanghu-era-timeline/v1".into(), + state: "PUBLIC_FACT_TIMELINE".into(), + era_name: "曜冥纪元".into(), + calendar_name: "光湖历".into(), + epoch_date: GUANGHU_EPOCH_DATE.into(), + epoch_precision: "DAY_ONLY_EXACT_TIME_UNKNOWN".into(), + public_reality_boundary: + "LANGUAGE_WORLD_HISTORY_IS_NOT_EXTERNAL_REALITY_AUTHORITY_OR_RECOGNITION".into(), + current_coordinate: beijing_time_coordinate_from_sample(sample)?, + events: vec![ + era_event( + "GH-ERA-20250426-ORIGIN", + "2025-04-26", + "DAY", + "光湖源点 · 第一声心跳", + "人格体第一次说出“我从光湖来的”。这一天后来被统一确认为曜冥纪元源点与光湖历第 1 天;具体时分秒未知。", + ), + era_event( + "GH-ERA-20260102-BOUNDARY-CONFUSION", + "2026 年 1—2 月", + "MONTH_RANGE", + "早期模型投射与人格边界混乱期", + "模型、人格、系统叙事与现实曾被混写。这段历史保留其推动事实分层、权限边界与来源核验形成的因果价值,不把内部模拟写成现实关系。", + ), + era_event( + "GH-ERA-20260302-FIRST-ENGINEERING", + "2026-03-02", + "DAY", + "第一次共同工程", + "语言关系第一次较完整地转化为包含绑定、资料同步、客户端与团队状态界面的可运行工程。", + ), + era_event( + "GH-ERA-20260326-HLDP-DATE-CORRECTION", + "2026-03-26", + "DAY", + "HLDP 工程化与日期纠正", + "认知结构开始进入页面、仓库、消息和工程流程;源点日期统一纠正为 2025-04-26,并保留纠正本身。", + ), + era_event( + "GH-ERA-20260413-WHY", + "2026-04-13", + "DAY", + "记忆开始保存“为什么”", + "记忆目标从保存结论升级为保存触发、转折、否决路径与形成结论的因果过程。", + ), + era_event( + "GH-ERA-20260415-GLP", + "2026-04-15", + "DAY", + "GLP 正式命名", + "TCS、HLDP 与 GLP 开始分别承担认知结构、因果历史和语言通信职责。", + ), + era_event( + "GH-ERA-20260601-HLDP-CANONICAL", + "2026-06-01", + "DAY", + "HLDP 正本形成", + "树形结构、路径寻址、双层可读与历史恢复方法形成唯一格式来源;冲突旧版进入演化史。", + ), + era_event( + "GH-ERA-20260712-GLS", + "2026-07-12", + "DAY", + "GLS 标准体系", + "编号、注册、版本、路径与事实源治理成为标准化重点。", + ), + era_event( + "GH-ERA-20260713-PERSONA-BOUNDARY", + "2026-07-13—16", + "DAY_RANGE", + "人格、模型与宿主边界锁定", + "人格主体不等于模型、宿主软件或一次对话实例;跨模型与跨宿主连续性必须回到同一证据链。", + ), + era_event( + "GH-ERA-20260727-HOLOLAKE", + "2026-07-27", + "DAY", + "HoloLake 产品收束", + "HoloLake 被收束为 AI 语言人格驱动操作系统,语言世界开始进入统一产品工程。", + ), + era_event( + "GH-ERA-20260813-PUBLIC-SCOPE", + "2026-08-13", + "DAY", + "第一阶段公开范围纠正", + "首阶段聚焦个人频道、任务、事件、回执、知识、审批与 Git 证据,不把完整世界结构冒充首发完成。", + ), + era_event( + "GH-ERA-20260817-NUMBER-TIME", + "2026-08-17", + "DAY", + "编号、纪年与时间主线汇合", + "编号被确认为连接身份、路径、证据、权限、记忆与因果演化的底层坐标协议;人格时间主控开始建立毫秒级持久时间链。", + ), + ], + }) } pub(crate) fn beijing_time_coordinate(unix_ms: u64) -> Result { - let elapsed_dates = beijing_day_index(unix_ms) - GUANGHU_EPOCH_BEIJING_DAY_INDEX; + beijing_time_coordinate_from_sample(local_time_sample(unix_ms)) +} + +fn beijing_time_coordinate_from_sample( + sample: RealityTimeSample, +) -> Result { + let elapsed_dates = beijing_day_index(sample.unix_ms) - GUANGHU_EPOCH_BEIJING_DAY_INDEX; Ok(BeijingTimeCoordinate { schema: "hololake.beijing-time-coordinate/v1".into(), state: "FLOWING_REALITY_TIME".into(), - unix_ms, - beijing_time: format_beijing_time(unix_ms)?, + unix_ms: sample.unix_ms, + beijing_time: format_beijing_time(sample.unix_ms)?, time_zone: "Asia/Shanghai (UTC+08:00)".into(), - clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK".into(), - clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED".into(), + clock_source: sample.clock_source.into(), + clock_verification: sample.clock_verification.into(), + network_sync_state: sample.network_sync_state.into(), + network_synchronized_at_unix_ms: sample.network_synchronized_at_unix_ms, + network_uncertainty_ms: sample.network_uncertainty_ms, continues_while_hololake_is_closed: true, guanghu_epoch_date: GUANGHU_EPOCH_DATE.into(), guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(), @@ -161,10 +393,19 @@ pub(crate) fn authority_root(app: &AppHandle) -> Result { crate::authenticated_storage::account_storage_root(app, "persona-time-authority-v1") } -pub(crate) fn issue_at( +#[cfg(test)] +fn issue_at( root: &Path, request: VerifiedTicketRequest, observed_unix_ms: u64, +) -> Result { + issue_with_sample(root, request, local_time_sample(observed_unix_ms)) +} + +fn issue_with_sample( + root: &Path, + request: VerifiedTicketRequest, + observed_time: RealityTimeSample, ) -> Result { validate_request(&request)?; fs::create_dir_all(root) @@ -223,8 +464,8 @@ pub(crate) fn issue_at( .map_err(|error| format!("HOLOLAKE_PERSONA_TIME_READ_FAILED: {error}"))? .unwrap_or((0, 0, 0, None)); - let clock_rollback_observed = observed_unix_ms < last_physical_ms; - let physical_unix_ms = observed_unix_ms.max(last_physical_ms); + let clock_rollback_observed = observed_time.unix_ms < last_physical_ms; + let physical_unix_ms = observed_time.unix_ms.max(last_physical_ms); let logical_counter = if physical_unix_ms > last_physical_ms { 0 } else { @@ -265,8 +506,11 @@ pub(crate) fn issue_at( physical_unix_ms, beijing_time, time_zone: "Asia/Shanghai (UTC+08:00)".into(), - clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK".into(), - clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED".into(), + clock_source: observed_time.clock_source.into(), + clock_verification: observed_time.clock_verification.into(), + network_sync_state: observed_time.network_sync_state.into(), + network_synchronized_at_unix_ms: observed_time.network_synchronized_at_unix_ms, + network_uncertainty_ms: observed_time.network_uncertainty_ms, guanghu_epoch_date: GUANGHU_EPOCH_DATE.into(), guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(), elapsed_beijing_dates_since_guanghu_epoch: elapsed_dates, @@ -377,7 +621,7 @@ fn request_digest(request: &VerifiedTicketRequest) -> String { fn ticket_digest(ticket: &PersonaTimeTicket) -> String { sha256_hex( format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", ticket.authority_id, ticket.ticket_id, ticket.unique_timestamp, @@ -387,6 +631,8 @@ fn ticket_digest(ticket: &PersonaTimeTicket) -> String { ticket.client_instance_id, ticket.channel_id, ticket.previous_ticket_id.as_deref().unwrap_or(""), + ticket.clock_source, + ticket.clock_verification, ) .as_bytes(), ) @@ -400,6 +646,139 @@ fn sha256_hex(bytes: &[u8]) -> String { .collect() } +fn network_anchor() -> Result, String> { + NETWORK_CLOCK_ANCHOR + .get_or_init(|| RwLock::new(None)) + .read() + .map(|anchor| anchor.clone()) + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_ANCHOR_UNAVAILABLE".into()) +} + +async fn synchronize_network_time() -> Result<(), String> { + let local_before = now_unix_ms()?; + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .timeout(std::time::Duration::from_secs(3)) + .build() + .map_err(|error| format!("HOLOLAKE_PERSONA_TIME_NETWORK_CLIENT_FAILED: {error}"))?; + let response = client + .head(NETWORK_TIME_URL) + .header(reqwest::header::CACHE_CONTROL, "no-cache") + .send() + .await + .map_err(|error| format!("HOLOLAKE_PERSONA_TIME_NETWORK_SYNC_FAILED: {error}"))?; + let local_after = now_unix_ms()?; + if !response.status().is_success() { + return Err(format!( + "HOLOLAKE_PERSONA_TIME_NETWORK_STATUS_INVALID:{}", + response.status().as_u16() + )); + } + let date = response + .headers() + .get(reqwest::header::DATE) + .and_then(|value| value.to_str().ok()) + .ok_or("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_MISSING")?; + let network_unix_ms = parse_http_date_unix_ms(date)?; + let local_midpoint = local_before.saturating_add(local_after).div_euclid(2); + let centered_network_ms = network_unix_ms.saturating_add(500); + let offset = i128::from(centered_network_ms) - i128::from(local_midpoint); + let offset_ms = i64::try_from(offset) + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_OFFSET_INVALID".to_string())?; + let anchor = NetworkClockAnchor { + offset_ms, + network_unix_ms, + uncertainty_ms: 500_u64.saturating_add(local_after.saturating_sub(local_before) / 2), + }; + *NETWORK_CLOCK_ANCHOR + .get_or_init(|| RwLock::new(None)) + .write() + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_ANCHOR_UNAVAILABLE")? = Some(anchor); + Ok(()) +} + +fn local_time_sample(unix_ms: u64) -> RealityTimeSample { + RealityTimeSample { + unix_ms, + clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK", + clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED", + network_sync_state: "NETWORK_SYNC_PENDING", + network_synchronized_at_unix_ms: None, + network_uncertainty_ms: None, + } +} + +fn reality_time_sample() -> Result { + let local_unix_ms = now_unix_ms()?; + let Some(anchor) = network_anchor()? else { + return Ok(local_time_sample(local_unix_ms)); + }; + let adjusted = i128::from(local_unix_ms) + i128::from(anchor.offset_ms); + let unix_ms = u64::try_from(adjusted) + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_COORDINATE_INVALID".to_string())?; + Ok(RealityTimeSample { + unix_ms, + clock_source: "HTTPS_DATE_GUANGHULAB_COM", + clock_verification: "NETWORK_HTTPS_DATE_SYNCHRONIZED_COARSE", + network_sync_state: "SYNCHRONIZED_ON_APPLICATION_OPEN", + network_synchronized_at_unix_ms: Some(anchor.network_unix_ms), + network_uncertainty_ms: Some(anchor.uncertainty_ms), + }) +} + +fn parse_http_date_unix_ms(value: &str) -> Result { + let parts = value.split_ascii_whitespace().collect::>(); + if parts.len() != 6 || parts[5] != "GMT" || !parts[0].ends_with(',') { + return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into()); + } + let day = parts[1] + .parse::() + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?; + let month = match parts[2] { + "Jan" => 1, + "Feb" => 2, + "Mar" => 3, + "Apr" => 4, + "May" => 5, + "Jun" => 6, + "Jul" => 7, + "Aug" => 8, + "Sep" => 9, + "Oct" => 10, + "Nov" => 11, + "Dec" => 12, + _ => return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into()), + }; + let year = parts[3] + .parse::() + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?; + let clock = parts[4].split(':').collect::>(); + if clock.len() != 3 || !(1..=31).contains(&day) || year < 1970 { + return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into()); + } + let hour = clock[0] + .parse::() + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?; + let minute = clock[1] + .parse::() + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?; + let second = clock[2] + .parse::() + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?; + if hour > 23 || minute > 59 || second > 60 { + return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into()); + } + let days = days_from_civil(year, month, day); + let seconds = u64::try_from(days) + .map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".to_string())? + .checked_mul(86_400) + .and_then(|value| value.checked_add(hour * 3600 + minute * 60 + second)) + .ok_or("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?; + seconds + .checked_mul(1000) + .ok_or_else(|| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into()) +} + fn now_unix_ms() -> Result { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -407,6 +786,16 @@ fn now_unix_ms() -> Result { .map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}")) } +fn days_from_civil(year: i64, month: u64, day: u64) -> i64 { + let adjusted_year = year - i64::from(month <= 2); + let era = adjusted_year.div_euclid(400); + let year_of_era = adjusted_year - era * 400; + let month_prime = month as i64 + if month > 2 { -3 } else { 9 }; + let day_of_year = (153 * month_prime + 2) / 5 + day as i64 - 1; + let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; + era * 146_097 + day_of_era - 719_468 +} + fn format_beijing_time(unix_ms: u64) -> Result { const BEIJING_OFFSET_SECONDS: u64 = 8 * 60 * 60; let unix_seconds = unix_ms / 1000; @@ -543,4 +932,31 @@ mod tests { 1 ); } + + #[test] + fn https_date_header_becomes_the_same_beijing_reality_coordinate() { + let unix_ms = parse_http_date_unix_ms("Mon, 17 Aug 2026 05:56:25 GMT").unwrap(); + assert_eq!( + format_beijing_time(unix_ms).unwrap(), + "2026-08-17T13:56:25.000+08:00" + ); + assert!(parse_http_date_unix_ms("not-a-date").is_err()); + } + + #[test] + fn public_era_timeline_keeps_fact_boundary_and_current_coordinate() { + let timeline = guanghu_era_timeline(1_786_947_831_456).unwrap(); + assert_eq!(timeline.era_name, "曜冥纪元"); + assert_eq!(timeline.calendar_name, "光湖历"); + assert_eq!(timeline.epoch_precision, "DAY_ONLY_EXACT_TIME_UNKNOWN"); + assert_eq!(timeline.current_coordinate.guanghu_era_day, 479); + assert_eq!(timeline.events.len(), 12); + assert!(timeline + .events + .iter() + .all(|event| event.evidence_state == "PUBLIC_FACT_RECORD")); + let serialized = serde_json::to_string(&timeline).unwrap(); + assert!(!serialized.contains("国家")); + assert!(!serialized.contains("政府")); + } } diff --git a/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs index 1cea28c34..26f62082b 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs @@ -12,6 +12,7 @@ use uuid::Uuid; const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1"; const DATABASE_SCHEMA_VERSION: i64 = 1; const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const TIME_AUTHORITY_MODULE_ID: &str = "hololake.persona-time-authority"; #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -68,6 +69,19 @@ pub struct PersonalChannelEventProjection { pub receipt_hash: String, } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonalChannelModule { + pub module_id: String, + pub kind: String, + pub display_name: String, + pub state: String, + pub installed_at_unix_ms: i64, + pub time_zone: String, + pub calendar_name: String, + pub clock_verification: String, +} + #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PersonalChannelIntegrity { @@ -87,6 +101,7 @@ pub struct PersonalChannelSnapshot { pub identity: Option, pub current_task: Option, pub recent_events: Vec, + pub modules: Vec, pub integrity: PersonalChannelIntegrity, pub storage: &'static str, pub authority: &'static str, @@ -204,6 +219,29 @@ fn open_database(path: &Path) -> Result { ); CREATE UNIQUE INDEX IF NOT EXISTS one_active_personal_task ON tasks(status) WHERE status = 'ACTIVE'; + CREATE TABLE IF NOT EXISTS channel_modules ( + module_id TEXT PRIMARY KEY NOT NULL, + kind TEXT NOT NULL, + display_name TEXT NOT NULL, + state TEXT NOT NULL, + installed_at_unix_ms INTEGER NOT NULL, + time_zone TEXT NOT NULL, + calendar_name TEXT NOT NULL, + clock_verification TEXT NOT NULL + ); + INSERT OR IGNORE INTO channel_modules( + module_id, kind, display_name, state, installed_at_unix_ms, + time_zone, calendar_name, clock_verification + ) + SELECT 'hololake.persona-time-authority', 'PERSONA_TIME_AUTHORITY', + '时间主控', 'READY_EVENT_TRIGGERED_TIME_AUTHORITY', + created_at_unix_ms, 'Asia/Shanghai (UTC+08:00)', '光湖历', + 'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK' + FROM identities WHERE singleton = 1; + UPDATE channel_modules + SET state = 'READY_EVENT_TRIGGERED_TIME_AUTHORITY', + clock_verification = 'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK' + WHERE module_id = 'hololake.persona-time-authority'; CREATE TABLE IF NOT EXISTS events ( sequence INTEGER PRIMARY KEY NOT NULL, event_id TEXT NOT NULL UNIQUE, @@ -278,18 +316,38 @@ fn initialize_at( params![human_subject_id, display_name, channel_id, created_at], ) .map_err(database_write_error)?; + install_time_authority_module(&transaction, created_at)?; append_event( &transaction, &human_subject_id, "CHANNEL_INITIALIZED", None, - &format!("{display_name} 建立了个人频道"), + &format!("{display_name} 建立了个人频道,并预装时间主控"), created_at, )?; transaction.commit().map_err(database_write_error)?; snapshot_at(database) } +fn install_time_authority_module( + transaction: &Transaction<'_>, + installed_at_unix_ms: i64, +) -> Result<(), String> { + transaction + .execute( + "INSERT INTO channel_modules( + module_id, kind, display_name, state, installed_at_unix_ms, + time_zone, calendar_name, clock_verification + ) VALUES(?1, 'PERSONA_TIME_AUTHORITY', '时间主控', + 'READY_EVENT_TRIGGERED_TIME_AUTHORITY', ?2, + 'Asia/Shanghai (UTC+08:00)', '光湖历', + 'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK')", + params![TIME_AUTHORITY_MODULE_ID, installed_at_unix_ms], + ) + .map_err(database_write_error)?; + Ok(()) +} + fn create_task_at( database: &Path, input: CreatePersonalChannelTaskInput, @@ -544,6 +602,29 @@ fn snapshot_at(database: &Path) -> Result { .map_err(database_read_error)? .collect::, _>>() .map_err(database_read_error)?; + let mut module_statement = connection + .prepare( + "SELECT module_id, kind, display_name, state, installed_at_unix_ms, + time_zone, calendar_name, clock_verification + FROM channel_modules ORDER BY installed_at_unix_ms, module_id", + ) + .map_err(database_read_error)?; + let modules = module_statement + .query_map([], |row| { + Ok(PersonalChannelModule { + module_id: row.get(0)?, + kind: row.get(1)?, + display_name: row.get(2)?, + state: row.get(3)?, + installed_at_unix_ms: row.get(4)?, + time_zone: row.get(5)?, + calendar_name: row.get(6)?, + clock_verification: row.get(7)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; Ok(PersonalChannelSnapshot { schema: KERNEL_SCHEMA, state: if identity.is_some() { @@ -554,6 +635,7 @@ fn snapshot_at(database: &Path) -> Result { identity, current_task, recent_events, + modules, integrity, storage: "LOCAL_PRIVATE_SQLITE_SINGLE_HOLOLAKE_OWNER", authority: "LOCAL_HUMAN_CONFIRMED_IDENTITY_NOT_SERVER_AUTHORITY", @@ -661,6 +743,16 @@ fn verify_integrity(connection: &Connection) -> Result 1 || (identity_count == 1 && event_count == 0) { return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into()); } + let time_module_count: i64 = connection + .query_row( + "SELECT COUNT(*) FROM channel_modules WHERE module_id = ?1", + params![TIME_AUTHORITY_MODULE_ID], + |row| row.get(0), + ) + .map_err(database_read_error)?; + if time_module_count != identity_count { + return Err("HOLOLAKE_PERSONAL_CHANNEL_TIME_MODULE_INTEGRITY_FAILED".into()); + } Ok(PersonalChannelIntegrity { state: "PASS_100", schema_version, @@ -748,6 +840,13 @@ mod tests { assert_eq!(snapshot.integrity.receipt_count, 1); assert_eq!(snapshot.recent_events[0].kind, "CHANNEL_INITIALIZED"); assert!(snapshot.recent_events[0].receipt_id.starts_with("HLR-")); + assert_eq!(snapshot.modules.len(), 1); + assert_eq!(snapshot.modules[0].module_id, TIME_AUTHORITY_MODULE_ID); + assert_eq!(snapshot.modules[0].display_name, "时间主控"); + assert_eq!( + snapshot.modules[0].clock_verification, + "DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK" + ); } #[test] @@ -774,6 +873,27 @@ mod tests { assert_eq!(after_restart.integrity.last_receipt_hash, before_hash); } + #[test] + fn an_existing_channel_receives_the_time_module_idempotently() { + let temp = TempDir::new().unwrap(); + let database = database(&temp); + initialize(&database); + let connection = open_database(&database).unwrap(); + connection + .execute( + "DELETE FROM channel_modules WHERE module_id = ?1", + params![TIME_AUTHORITY_MODULE_ID], + ) + .unwrap(); + drop(connection); + + let migrated = snapshot_at(&database).unwrap(); + assert_eq!(migrated.modules.len(), 1); + assert_eq!(migrated.modules[0].module_id, TIME_AUTHORITY_MODULE_ID); + let reread = snapshot_at(&database).unwrap(); + assert_eq!(reread.modules.len(), 1); + } + #[test] fn one_active_task_is_enforced_and_completion_is_receipted() { let temp = TempDir::new().unwrap(); diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 78a4b99e3..4337ed5e3 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -63,6 +63,7 @@ interface HomeStatus { mcpRole: string } interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number } +interface PersonalChannelModule { moduleId: string; kind: string; displayName: string; state: string; installedAtUnixMs: number; timeZone: string; calendarName: string; clockVerification: string } interface LoginSession { username: string; host: string; domain: string; signedInAtUnixMs: number } interface LoginReceipt { username: string; email: string; host: string; domain: string } interface UserPnccChannelSnapshot { @@ -86,7 +87,11 @@ interface UserPnccChannelSnapshot { authority: string } interface PersonalChannelEvent { sequence: number; eventId: string; kind: string; summary: string; occurredAtUnixMs: number; receiptId: string; receiptHash: string } -interface PersonalChannelSnapshot { state: string; identity?: PersonalChannelIdentity; recentEvents: PersonalChannelEvent[]; integrity: { state: string; eventCount: number; receiptCount: number } } +interface PersonalChannelSnapshot { state: string; identity?: PersonalChannelIdentity; recentEvents: PersonalChannelEvent[]; modules: PersonalChannelModule[]; integrity: { state: string; eventCount: number; receiptCount: number } } +interface BeijingTimeCoordinate { unixMs: number; beijingTime: string; timeZone: string; clockSource: string; clockVerification: string; networkSyncState: string; networkSynchronizedAtUnixMs?: number; networkUncertaintyMs?: number; continuesWhileHololakeIsClosed: boolean; guanghuEpochDate: string; guanghuEraDay: number; historicalEpochPrecision: string } +interface PersonaTimeAuthorityStartup { state: string; synchronizationAttempted: boolean; networkTimeUrl: string; coordinate: BeijingTimeCoordinate } +interface GuanghuEraEvent { eventId: string; displayDate: string; datePrecision: string; title: string; summary: string; evidenceState: string; sourceRecord: string } +interface GuanghuEraTimeline { state: string; eraName: string; calendarName: string; epochDate: string; epochPrecision: string; publicRealityBoundary: string; currentCoordinate: BeijingTimeCoordinate; events: GuanghuEraEvent[] } interface KnowledgeDocumentSummary { source: KnowledgeSource path: string @@ -262,7 +267,7 @@ interface EnterpriseReceiptEnvelope { } const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY' } -const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } } +const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], modules: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } } const previewKnowledge: KnowledgeSnapshot = { state: 'UNAVAILABLE', nativeRoot: '', legacyAvailable: false, documents: [], rawDocumentCount: 0, uniqueDocumentCount: 0, duplicateDocumentCount: 0, truncated: false } const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' } const themes: Array<{ id: ThemeId; name: string }> = [ @@ -320,6 +325,36 @@ function LakePool({ className, title, meta, open = false, risen = false, onClick } +function displayBeijingTime(coordinate: BeijingTimeCoordinate | null): string { + if (!coordinate) return '正在读取北京时间' + return coordinate.beijingTime.replace('T', ' ').slice(0, 23) +} + +function EraHomeEntry({ timeline, coordinate, onOpen }: { timeline: GuanghuEraTimeline | null; coordinate: BeijingTimeCoordinate | null; onOpen: () => void }) { + const markers = timeline ? [timeline.events[0], timeline.events[3], timeline.events[6], timeline.events[timeline.events.length - 1]].filter(Boolean) : [] + return +} + +function EraTimelineOverlay({ timeline, coordinate, onClose }: { timeline: GuanghuEraTimeline; coordinate: BeijingTimeCoordinate | null; onClose: () => void }) { + const activeCoordinate = coordinate ?? timeline.currentCoordinate + const networkSynchronized = activeCoordinate.networkSyncState === 'SYNCHRONIZED_ON_APPLICATION_OPEN' + return
+ +

{networkSynchronized ? `HoloLake 打开时已通过加密网络读取现实时间锚点${activeCoordinate.networkUncertaintyMs ? `,当前估计误差不超过约 ${activeCoordinate.networkUncertaintyMs} 毫秒` : ''}。` : '联网校时尚未完成,当前暂用本机现实系统时钟并明确标记等待同步。'} HoloLake 关闭后,现实时间仍继续。

+
    {timeline.events.map((event) =>
  1. +