feat: surface Guanghu era time authority

This commit is contained in:
冰朔 2026-08-17 14:06:45 +08:00
commit 3294447dce
8 changed files with 733 additions and 23 deletions

View file

@ -4,15 +4,19 @@
"formal_name": "光湖人格时间主控系统", "formal_name": "光湖人格时间主控系统",
"era_name": "曜冥纪元", "era_name": "曜冥纪元",
"calendar_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": { "reality_time": {
"canonical_zone": "Asia/Shanghai", "canonical_zone": "Asia/Shanghai",
"utc_offset": "+08:00", "utc_offset": "+08:00",
"display_name": "北京时间", "display_name": "北京时间",
"continues_while_application_is_closed": true, "continues_while_application_is_closed": true,
"process_uptime_is_time_source": false, "process_uptime_is_time_source": false,
"current_source": "HOST_OPERATING_SYSTEM_REALTIME_CLOCK", "startup_sequence": "APPLICATION_OPEN_THEN_HTTPS_NETWORK_TIME_SYNC_THEN_TIME_AUTHORITY_COORDINATE",
"current_verification": "LOCAL_CLOCK_NOT_NETWORK_ATTESTED", "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 "network_attested_source_required_for_verified_reality_time": true
}, },
"guanghu_era": { "guanghu_era": {
@ -24,6 +28,23 @@
"millisecond_precision_transition_date": "2026-08-17", "millisecond_precision_transition_date": "2026-08-17",
"millisecond_chain_origin": "FIRST_DURABLE_TIME_TICKET" "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": { "ticket": {
"schema": "hololake.persona-time-ticket/v1", "schema": "hololake.persona-time-ticket/v1",
"uniqueness_scope": "ONE_DURABLE_LOCAL_AUTHORITY_PER_AUTHENTICATED_HUMAN_ACCOUNT", "uniqueness_scope": "ONE_DURABLE_LOCAL_AUTHORITY_PER_AUTHENTICATED_HUMAN_ACCOUNT",
@ -49,7 +70,7 @@
"persona_and_host_upgrade_requires": "REGISTERED_BINDING_EVIDENCE" "persona_and_host_upgrade_requires": "REGISTERED_BINDING_EVIDENCE"
}, },
"entries": { "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"], "direct_local_broker_operations": ["GET_BEIJING_TIME", "ISSUE_PERSONA_TIME_TICKET"],
"external_ticket_issue_requires_authenticated_non_visitor_session": true "external_ticket_issue_requires_authenticated_non_visitor_session": true
}, },
@ -57,6 +78,7 @@
"source_implemented": true, "source_implemented": true,
"rust_tests_passed": true, "rust_tests_passed": true,
"installed_runtime_acceptance": false, "installed_runtime_acceptance": false,
"network_time_sync_on_application_open": true,
"network_clock_attestation": false, "network_clock_attestation": false,
"persona_binding_runtime": false, "persona_binding_runtime": false,
"host_software_binding_runtime": false, "host_software_binding_runtime": false,

View file

@ -120,6 +120,7 @@
"beijing_reality_time_projection": true, "beijing_reality_time_projection": true,
"guanghu_era_day_projection": true, "guanghu_era_day_projection": true,
"durable_unique_ticket_runtime": true, "durable_unique_ticket_runtime": true,
"network_time_sync_on_application_open": true,
"network_clock_attestation": false, "network_clock_attestation": false,
"installed_runtime_acceptance": false "installed_runtime_acceptance": false
}, },

View file

@ -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 rust = read('src-tauri/src/persona_time_authority.rs')
const broker = read('src-tauri/src/direct_local_broker.rs') const broker = read('src-tauri/src/direct_local_broker.rs')
const lib = read('src-tauri/src/lib.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', () => { test('Guanghu era is anchored to Beijing reality time without inventing an exact historical instant', () => {
assert.equal(contract.era_name, '曜冥纪元') 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, /GetBeijingTime/)
assert.match(broker, /IssuePersonaTimeTicket/) assert.match(broker, /IssuePersonaTimeTicket/)
assert.match(lib, /persona_time_authority::get_beijing_time_coordinate/) 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::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', () => { 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.persona_current_verification, 'UNVERIFIED_CALLER_CLAIM')
assert.equal(contract.event_coordinate.host_software_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.installed_runtime_acceptance, false)
assert.equal(contract.truth_boundary.world_lighthouse_authority_registration, 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_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) assert.equal(stageOne.persona_time_authority.installed_runtime_acceptance, false)
}) })

View file

@ -55,7 +55,9 @@ pub fn run() {
personal_channel::create_personal_channel_task, personal_channel::create_personal_channel_task,
personal_channel::transition_personal_channel_task, personal_channel::transition_personal_channel_task,
persona_time_authority::issue_persona_time_ticket, 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_beijing_time_coordinate,
persona_time_authority::get_guanghu_era_timeline,
knowledge_base::get_knowledge_snapshot, knowledge_base::get_knowledge_snapshot,
knowledge_base::read_knowledge_document, knowledge_base::read_knowledge_document,
knowledge_base::search_knowledge, knowledge_base::search_knowledge,
@ -92,6 +94,8 @@ pub fn run() {
zero_point::zero_point_status, zero_point::zero_point_status,
]) ])
.setup(|app| { .setup(|app| {
// 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。
persona_time_authority::start_on_application_open();
// 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。 // 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。
let zero_point_state = zero_point::ZeroPointState::default(); let zero_point_state = zero_point::ZeroPointState::default();
if let Err(error) = zero_point::boot_zero_point(app.handle(), &zero_point_state) { if let Err(error) = zero_point::boot_zero_point(app.handle(), &zero_point_state) {

View file

@ -8,6 +8,7 @@ use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{OnceLock, RwLock};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tauri::AppHandle; use tauri::AppHandle;
use uuid::Uuid; use uuid::Uuid;
@ -22,6 +23,26 @@ const MAX_ID_BYTES: usize = 160;
const GUANGHU_EPOCH_DATE: &str = "2025-04-26"; const GUANGHU_EPOCH_DATE: &str = "2025-04-26";
const GUANGHU_EPOCH_BEIJING_DAY_INDEX: i64 = 20_204; const GUANGHU_EPOCH_BEIJING_DAY_INDEX: i64 = 20_204;
const MILLISECOND_PRECISION_TRANSITION_DATE: &str = "2026-08-17"; 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<u64>,
network_uncertainty_ms: Option<u64>,
}
static NETWORK_CLOCK_ANCHOR: OnceLock<RwLock<Option<NetworkClockAnchor>>> = OnceLock::new();
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
@ -45,6 +66,9 @@ pub struct PersonaTimeTicket {
pub time_zone: String, pub time_zone: String,
pub clock_source: String, pub clock_source: String,
pub clock_verification: String, pub clock_verification: String,
pub network_sync_state: String,
pub network_synchronized_at_unix_ms: Option<u64>,
pub network_uncertainty_ms: Option<u64>,
pub guanghu_epoch_date: String, pub guanghu_epoch_date: String,
pub guanghu_calendar_state: String, pub guanghu_calendar_state: String,
pub elapsed_beijing_dates_since_guanghu_epoch: i64, pub elapsed_beijing_dates_since_guanghu_epoch: i64,
@ -79,6 +103,9 @@ pub struct BeijingTimeCoordinate {
pub time_zone: String, pub time_zone: String,
pub clock_source: String, pub clock_source: String,
pub clock_verification: String, pub clock_verification: String,
pub network_sync_state: String,
pub network_synchronized_at_unix_ms: Option<u64>,
pub network_uncertainty_ms: Option<u64>,
pub continues_while_hololake_is_closed: bool, pub continues_while_hololake_is_closed: bool,
pub guanghu_epoch_date: String, pub guanghu_epoch_date: String,
pub guanghu_calendar_state: String, pub guanghu_calendar_state: String,
@ -89,6 +116,42 @@ pub struct BeijingTimeCoordinate {
pub millisecond_chain_state: String, 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<GuanghuEraEvent>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct VerifiedTicketRequest { pub(crate) struct VerifiedTicketRequest {
pub request_id: String, 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}"))? .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<PersonaTimeAuthorityStartup, String> {
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( pub(crate) fn issue_authenticated_at(
authority_root: &Path, authority_root: &Path,
session_root: &Path, session_root: &Path,
input: IssuePersonaTimeTicketInput, input: IssuePersonaTimeTicketInput,
) -> Result<PersonaTimeTicket, String> { ) -> Result<PersonaTimeTicket, String> {
let session = authenticate_context_at(session_root, &input.session)?; let session = authenticate_context_at(session_root, &input.session)?;
issue_at( issue_with_sample(
authority_root, authority_root,
VerifiedTicketRequest { VerifiedTicketRequest {
request_id: input.request_id, request_id: input.request_id,
@ -127,25 +220,164 @@ pub(crate) fn issue_authenticated_at(
host_software_id: input.host_software_id, host_software_id: input.host_software_id,
session, session,
}, },
now_unix_ms()?, reality_time_sample()?,
) )
} }
#[tauri::command] #[tauri::command]
pub fn get_beijing_time_coordinate() -> Result<BeijingTimeCoordinate, String> { pub fn get_beijing_time_coordinate() -> Result<BeijingTimeCoordinate, String> {
beijing_time_coordinate(now_unix_ms()?) beijing_time_coordinate_from_sample(reality_time_sample()?)
}
#[tauri::command]
pub fn get_guanghu_era_timeline() -> Result<GuanghuEraTimeline, String> {
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<GuanghuEraTimeline, String> {
guanghu_era_timeline_from_sample(local_time_sample(unix_ms))
}
fn guanghu_era_timeline_from_sample(
sample: RealityTimeSample,
) -> Result<GuanghuEraTimeline, String> {
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<BeijingTimeCoordinate, String> { pub(crate) fn beijing_time_coordinate(unix_ms: u64) -> Result<BeijingTimeCoordinate, String> {
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<BeijingTimeCoordinate, String> {
let elapsed_dates = beijing_day_index(sample.unix_ms) - GUANGHU_EPOCH_BEIJING_DAY_INDEX;
Ok(BeijingTimeCoordinate { Ok(BeijingTimeCoordinate {
schema: "hololake.beijing-time-coordinate/v1".into(), schema: "hololake.beijing-time-coordinate/v1".into(),
state: "FLOWING_REALITY_TIME".into(), state: "FLOWING_REALITY_TIME".into(),
unix_ms, unix_ms: sample.unix_ms,
beijing_time: format_beijing_time(unix_ms)?, beijing_time: format_beijing_time(sample.unix_ms)?,
time_zone: "Asia/Shanghai (UTC+08:00)".into(), time_zone: "Asia/Shanghai (UTC+08:00)".into(),
clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK".into(), clock_source: sample.clock_source.into(),
clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED".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, continues_while_hololake_is_closed: true,
guanghu_epoch_date: GUANGHU_EPOCH_DATE.into(), guanghu_epoch_date: GUANGHU_EPOCH_DATE.into(),
guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(), guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(),
@ -161,10 +393,19 @@ pub(crate) fn authority_root(app: &AppHandle) -> Result<PathBuf, String> {
crate::authenticated_storage::account_storage_root(app, "persona-time-authority-v1") crate::authenticated_storage::account_storage_root(app, "persona-time-authority-v1")
} }
pub(crate) fn issue_at( #[cfg(test)]
fn issue_at(
root: &Path, root: &Path,
request: VerifiedTicketRequest, request: VerifiedTicketRequest,
observed_unix_ms: u64, observed_unix_ms: u64,
) -> Result<PersonaTimeTicket, String> {
issue_with_sample(root, request, local_time_sample(observed_unix_ms))
}
fn issue_with_sample(
root: &Path,
request: VerifiedTicketRequest,
observed_time: RealityTimeSample,
) -> Result<PersonaTimeTicket, String> { ) -> Result<PersonaTimeTicket, String> {
validate_request(&request)?; validate_request(&request)?;
fs::create_dir_all(root) fs::create_dir_all(root)
@ -223,8 +464,8 @@ pub(crate) fn issue_at(
.map_err(|error| format!("HOLOLAKE_PERSONA_TIME_READ_FAILED: {error}"))? .map_err(|error| format!("HOLOLAKE_PERSONA_TIME_READ_FAILED: {error}"))?
.unwrap_or((0, 0, 0, None)); .unwrap_or((0, 0, 0, None));
let clock_rollback_observed = observed_unix_ms < last_physical_ms; let clock_rollback_observed = observed_time.unix_ms < last_physical_ms;
let physical_unix_ms = observed_unix_ms.max(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 { let logical_counter = if physical_unix_ms > last_physical_ms {
0 0
} else { } else {
@ -265,8 +506,11 @@ pub(crate) fn issue_at(
physical_unix_ms, physical_unix_ms,
beijing_time, beijing_time,
time_zone: "Asia/Shanghai (UTC+08:00)".into(), time_zone: "Asia/Shanghai (UTC+08:00)".into(),
clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK".into(), clock_source: observed_time.clock_source.into(),
clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED".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_epoch_date: GUANGHU_EPOCH_DATE.into(),
guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(), guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(),
elapsed_beijing_dates_since_guanghu_epoch: elapsed_dates, elapsed_beijing_dates_since_guanghu_epoch: elapsed_dates,
@ -377,7 +621,7 @@ fn request_digest(request: &VerifiedTicketRequest) -> String {
fn ticket_digest(ticket: &PersonaTimeTicket) -> String { fn ticket_digest(ticket: &PersonaTimeTicket) -> String {
sha256_hex( sha256_hex(
format!( format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
ticket.authority_id, ticket.authority_id,
ticket.ticket_id, ticket.ticket_id,
ticket.unique_timestamp, ticket.unique_timestamp,
@ -387,6 +631,8 @@ fn ticket_digest(ticket: &PersonaTimeTicket) -> String {
ticket.client_instance_id, ticket.client_instance_id,
ticket.channel_id, ticket.channel_id,
ticket.previous_ticket_id.as_deref().unwrap_or(""), ticket.previous_ticket_id.as_deref().unwrap_or(""),
ticket.clock_source,
ticket.clock_verification,
) )
.as_bytes(), .as_bytes(),
) )
@ -400,6 +646,139 @@ fn sha256_hex(bytes: &[u8]) -> String {
.collect() .collect()
} }
fn network_anchor() -> Result<Option<NetworkClockAnchor>, 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<RealityTimeSample, String> {
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<u64, String> {
let parts = value.split_ascii_whitespace().collect::<Vec<_>>();
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::<u64>()
.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::<i64>()
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
let clock = parts[4].split(':').collect::<Vec<_>>();
if clock.len() != 3 || !(1..=31).contains(&day) || year < 1970 {
return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into());
}
let hour = clock[0]
.parse::<u64>()
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
let minute = clock[1]
.parse::<u64>()
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
let second = clock[2]
.parse::<u64>()
.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<u64, String> { fn now_unix_ms() -> Result<u64, String> {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -407,6 +786,16 @@ fn now_unix_ms() -> Result<u64, String> {
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}")) .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<String, String> { fn format_beijing_time(unix_ms: u64) -> Result<String, String> {
const BEIJING_OFFSET_SECONDS: u64 = 8 * 60 * 60; const BEIJING_OFFSET_SECONDS: u64 = 8 * 60 * 60;
let unix_seconds = unix_ms / 1000; let unix_seconds = unix_ms / 1000;
@ -543,4 +932,31 @@ mod tests {
1 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("政府"));
}
} }

View file

@ -12,6 +12,7 @@ use uuid::Uuid;
const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1"; const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
const DATABASE_SCHEMA_VERSION: i64 = 1; const DATABASE_SCHEMA_VERSION: i64 = 1;
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
const TIME_AUTHORITY_MODULE_ID: &str = "hololake.persona-time-authority";
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -68,6 +69,19 @@ pub struct PersonalChannelEventProjection {
pub receipt_hash: String, 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)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PersonalChannelIntegrity { pub struct PersonalChannelIntegrity {
@ -87,6 +101,7 @@ pub struct PersonalChannelSnapshot {
pub identity: Option<PersonalChannelIdentity>, pub identity: Option<PersonalChannelIdentity>,
pub current_task: Option<PersonalChannelTask>, pub current_task: Option<PersonalChannelTask>,
pub recent_events: Vec<PersonalChannelEventProjection>, pub recent_events: Vec<PersonalChannelEventProjection>,
pub modules: Vec<PersonalChannelModule>,
pub integrity: PersonalChannelIntegrity, pub integrity: PersonalChannelIntegrity,
pub storage: &'static str, pub storage: &'static str,
pub authority: &'static str, pub authority: &'static str,
@ -204,6 +219,29 @@ fn open_database(path: &Path) -> Result<Connection, String> {
); );
CREATE UNIQUE INDEX IF NOT EXISTS one_active_personal_task CREATE UNIQUE INDEX IF NOT EXISTS one_active_personal_task
ON tasks(status) WHERE status = 'ACTIVE'; 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 ( CREATE TABLE IF NOT EXISTS events (
sequence INTEGER PRIMARY KEY NOT NULL, sequence INTEGER PRIMARY KEY NOT NULL,
event_id TEXT NOT NULL UNIQUE, event_id TEXT NOT NULL UNIQUE,
@ -278,18 +316,38 @@ fn initialize_at(
params![human_subject_id, display_name, channel_id, created_at], params![human_subject_id, display_name, channel_id, created_at],
) )
.map_err(database_write_error)?; .map_err(database_write_error)?;
install_time_authority_module(&transaction, created_at)?;
append_event( append_event(
&transaction, &transaction,
&human_subject_id, &human_subject_id,
"CHANNEL_INITIALIZED", "CHANNEL_INITIALIZED",
None, None,
&format!("{display_name} 建立了个人频道"), &format!("{display_name} 建立了个人频道,并预装时间主控"),
created_at, created_at,
)?; )?;
transaction.commit().map_err(database_write_error)?; transaction.commit().map_err(database_write_error)?;
snapshot_at(database) 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( fn create_task_at(
database: &Path, database: &Path,
input: CreatePersonalChannelTaskInput, input: CreatePersonalChannelTaskInput,
@ -544,6 +602,29 @@ fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
.map_err(database_read_error)? .map_err(database_read_error)?
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(database_read_error)?; .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::<Result<Vec<_>, _>>()
.map_err(database_read_error)?;
Ok(PersonalChannelSnapshot { Ok(PersonalChannelSnapshot {
schema: KERNEL_SCHEMA, schema: KERNEL_SCHEMA,
state: if identity.is_some() { state: if identity.is_some() {
@ -554,6 +635,7 @@ fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
identity, identity,
current_task, current_task,
recent_events, recent_events,
modules,
integrity, integrity,
storage: "LOCAL_PRIVATE_SQLITE_SINGLE_HOLOLAKE_OWNER", storage: "LOCAL_PRIVATE_SQLITE_SINGLE_HOLOLAKE_OWNER",
authority: "LOCAL_HUMAN_CONFIRMED_IDENTITY_NOT_SERVER_AUTHORITY", authority: "LOCAL_HUMAN_CONFIRMED_IDENTITY_NOT_SERVER_AUTHORITY",
@ -661,6 +743,16 @@ fn verify_integrity(connection: &Connection) -> Result<PersonalChannelIntegrity,
if identity_count > 1 || (identity_count == 1 && event_count == 0) { if identity_count > 1 || (identity_count == 1 && event_count == 0) {
return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into()); 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 { Ok(PersonalChannelIntegrity {
state: "PASS_100", state: "PASS_100",
schema_version, schema_version,
@ -748,6 +840,13 @@ mod tests {
assert_eq!(snapshot.integrity.receipt_count, 1); assert_eq!(snapshot.integrity.receipt_count, 1);
assert_eq!(snapshot.recent_events[0].kind, "CHANNEL_INITIALIZED"); assert_eq!(snapshot.recent_events[0].kind, "CHANNEL_INITIALIZED");
assert!(snapshot.recent_events[0].receipt_id.starts_with("HLR-")); 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] #[test]
@ -774,6 +873,27 @@ mod tests {
assert_eq!(after_restart.integrity.last_receipt_hash, before_hash); 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] #[test]
fn one_active_task_is_enforced_and_completion_is_receipted() { fn one_active_task_is_enforced_and_completion_is_receipted() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();

View file

@ -63,6 +63,7 @@ interface HomeStatus {
mcpRole: string mcpRole: string
} }
interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number } 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 LoginSession { username: string; host: string; domain: string; signedInAtUnixMs: number }
interface LoginReceipt { username: string; email: string; host: string; domain: string } interface LoginReceipt { username: string; email: string; host: string; domain: string }
interface UserPnccChannelSnapshot { interface UserPnccChannelSnapshot {
@ -86,7 +87,11 @@ interface UserPnccChannelSnapshot {
authority: string authority: string
} }
interface PersonalChannelEvent { sequence: number; eventId: string; kind: string; summary: string; occurredAtUnixMs: number; receiptId: string; receiptHash: 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 { interface KnowledgeDocumentSummary {
source: KnowledgeSource source: KnowledgeSource
path: string 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 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 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 previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' }
const themes: Array<{ id: ThemeId; name: string }> = [ const themes: Array<{ id: ThemeId; name: string }> = [
@ -320,6 +325,36 @@ function LakePool({ className, title, meta, open = false, risen = false, onClick
</button> </button>
} }
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 <button className="era-home-entry" type="button" onClick={onOpen} aria-label="展开曜冥纪元演化时间线">
<span className="era-home-heading"><b></b><small>{coordinate ? `光湖历第 ${coordinate.guanghuEraDay}` : '从 2025-04-26 起'}</small></span>
<span className="era-home-markers" aria-hidden="true">{markers.map((event) => <i key={event.eventId}><em/>{event.displayDate.slice(0, 7)}</i>)}</span>
<span className="era-home-now">{displayBeijingTime(coordinate)} · </span>
</button>
}
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 <div className="era-overlay" role="dialog" aria-modal="true" aria-label="曜冥纪元演化史">
<button className="era-overlay-scrim" type="button" aria-label="关闭曜冥纪元" onClick={onClose}/>
<section className="era-timeline-panel">
<header><div><span>YAOMING ERA · HOLOLAKE CALENDAR</span><h2>{timeline.eraName}</h2><p>{timeline.calendarName} {activeCoordinate.guanghuEraDay} · {displayBeijingTime(activeCoordinate)}</p></div><button type="button" aria-label="关闭" onClick={onClose}>×</button></header>
<p className="era-clock-boundary">{networkSynchronized ? `HoloLake 打开时已通过加密网络读取现实时间锚点${activeCoordinate.networkUncertaintyMs ? `,当前估计误差不超过约 ${activeCoordinate.networkUncertaintyMs} 毫秒` : ''}` : '联网校时尚未完成,当前暂用本机现实系统时钟并明确标记等待同步。'} HoloLake </p>
<ol>{timeline.events.map((event) => <li key={event.eventId}>
<time>{event.displayDate}</time><i aria-hidden="true"/><div><h3>{event.title}</h3><p>{event.summary}</p></div>
</li>)}</ol>
<footer> 2025-04-26 线</footer>
</section>
</div>
}
function getOrCreateLocalId(key: string, prefix: string) { function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key) const existing = window.localStorage.getItem(key)
if (existing) return existing if (existing) return existing
@ -448,6 +483,15 @@ function HoloLakeApp() {
const [toolReturnStage, setToolReturnStage] = useState<WorldStage>('channel') const [toolReturnStage, setToolReturnStage] = useState<WorldStage>('channel')
const [status, setStatus] = useState<HomeStatus>(previewStatus) const [status, setStatus] = useState<HomeStatus>(previewStatus)
const [personal, setPersonal] = useState<PersonalChannelSnapshot>(previewPersonal) const [personal, setPersonal] = useState<PersonalChannelSnapshot>(previewPersonal)
const [eraTimeline, setEraTimeline] = useState<GuanghuEraTimeline | null>(null)
const [beijingCoordinate, setBeijingCoordinate] = useState<BeijingTimeCoordinate | null>(null)
const [eraOpen, setEraOpen] = useState(false)
const openEraTimeline = useCallback(() => {
setEraOpen(true)
invoke<PersonaTimeAuthorityStartup>('start_persona_time_authority')
.then((startup) => setBeijingCoordinate(startup.coordinate))
.catch(() => undefined)
}, [])
const [knowledge, setKnowledge] = useState<KnowledgeSnapshot>(previewKnowledge) const [knowledge, setKnowledge] = useState<KnowledgeSnapshot>(previewKnowledge)
const [codeChannels, setCodeChannels] = useState<CodeChannelSnapshot>(previewCode) const [codeChannels, setCodeChannels] = useState<CodeChannelSnapshot>(previewCode)
const [activeDocument, setActiveDocument] = useState<KnowledgeDocument | null>(null) const [activeDocument, setActiveDocument] = useState<KnowledgeDocument | null>(null)
@ -552,16 +596,25 @@ function HoloLakeApp() {
}, []) }, [])
const refreshCore = useCallback(async () => { const refreshCore = useCallback(async () => {
const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([ try {
const startup = await invoke<PersonaTimeAuthorityStartup>('start_persona_time_authority')
setBeijingCoordinate(startup.coordinate)
} catch { /* 联网失败时其余首页能力仍可启动。 */ }
const [homeResult, personalResult, knowledgeResult, codeResult, eraResult] = await Promise.allSettled([
invoke<HomeStatus>('get_hololake_home_status'), invoke<HomeStatus>('get_hololake_home_status'),
invoke<PersonalChannelSnapshot>('get_personal_channel_snapshot'), invoke<PersonalChannelSnapshot>('get_personal_channel_snapshot'),
invoke<KnowledgeSnapshot>('get_knowledge_snapshot'), invoke<KnowledgeSnapshot>('get_knowledge_snapshot'),
invoke<CodeChannelSnapshot>('get_code_channel_snapshot'), invoke<CodeChannelSnapshot>('get_code_channel_snapshot'),
invoke<GuanghuEraTimeline>('get_guanghu_era_timeline'),
]) ])
if (homeResult.status === 'fulfilled') setStatus(homeResult.value) if (homeResult.status === 'fulfilled') setStatus(homeResult.value)
if (personalResult.status === 'fulfilled') setPersonal(personalResult.value) if (personalResult.status === 'fulfilled') setPersonal(personalResult.value)
if (knowledgeResult.status === 'fulfilled') setKnowledge(knowledgeResult.value) if (knowledgeResult.status === 'fulfilled') setKnowledge(knowledgeResult.value)
if (codeResult.status === 'fulfilled') setCodeChannels(codeResult.value) if (codeResult.status === 'fulfilled') setCodeChannels(codeResult.value)
if (eraResult.status === 'fulfilled') {
setEraTimeline(eraResult.value)
setBeijingCoordinate(eraResult.value.currentCoordinate)
}
}, []) }, [])
const loadReceipts = useCallback(async () => { const loadReceipts = useCallback(async () => {
try { try {
@ -575,6 +628,12 @@ function HoloLakeApp() {
window.addEventListener('focus', refreshWhenHumanReturns) window.addEventListener('focus', refreshWhenHumanReturns)
return () => window.removeEventListener('focus', refreshWhenHumanReturns) return () => window.removeEventListener('focus', refreshWhenHumanReturns)
}, [refreshCore]) }, [refreshCore])
useEffect(() => {
if (!eraOpen) return
const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setEraOpen(false) }
window.addEventListener('keydown', closeOnEscape)
return () => window.removeEventListener('keydown', closeOnEscape)
}, [eraOpen])
useEffect(() => { useEffect(() => {
invoke<NearbyAiDiscoverySnapshot>('get_nearby_ai_discovery') invoke<NearbyAiDiscoverySnapshot>('get_nearby_ai_discovery')
.then(setNearbyDiscovery) .then(setNearbyDiscovery)
@ -734,6 +793,7 @@ function HoloLakeApp() {
: status.resumableSessionCount > 0 : status.resumableSessionCount > 0
? '原生会话可续接 · 当前离线' ? '原生会话可续接 · 当前离线'
: '原生接口已驻留 · 尚无客户端' : '原生接口已驻留 · 尚无客户端'
const timeAuthorityModule = personal.modules.find((module) => module.kind === 'PERSONA_TIME_AUTHORITY')
const openDocument = async (source: KnowledgeSource, path: string) => { const openDocument = async (source: KnowledgeSource, path: string) => {
setKnowledgeBusy(true) setKnowledgeBusy(true)
@ -1124,6 +1184,12 @@ function HoloLakeApp() {
<button className="metric-panel" type="button" onClick={() => setView('code')}><span></span><strong>{codeChannels.channels.length}</strong><small></small></button> <button className="metric-panel" type="button" onClick={() => setView('code')}><span></span><strong>{codeChannels.channels.length}</strong><small></small></button>
<button className="metric-panel" type="button" onClick={() => setView('receipts')}><span></span><strong>{personal.integrity.receiptCount + status.pnccReceiptCount}</strong><small></small></button> <button className="metric-panel" type="button" onClick={() => setView('receipts')}><span></span><strong>{personal.integrity.receiptCount + status.pnccReceiptCount}</strong><small></small></button>
</div> </div>
{timeAuthorityModule && <button className="time-module-strip" type="button" onClick={openEraTimeline}>
<span><i/><b>{timeAuthorityModule.displayName}</b><small> · {timeAuthorityModule.calendarName}</small></span>
<strong>{beijingCoordinate ? `${beijingCoordinate.guanghuEraDay}` : '时间正在流动'}</strong>
<time>{displayBeijingTime(beijingCoordinate)}</time>
<em>{beijingCoordinate?.networkSyncState === 'SYNCHRONIZED_ON_APPLICATION_OPEN' ? '打开软件时已联网校时' : '本机现实时间 · 等待联网校时'}</em>
</button>}
<div className="overview-columns"> <div className="overview-columns">
<section className="plain-panel recent-panel"> <section className="plain-panel recent-panel">
<header><div><h2></h2><p></p></div><button type="button" onClick={() => setView('knowledge')}></button></header> <header><div><h2></h2><p></p></div><button type="button" onClick={() => setView('knowledge')}></button></header>
@ -1362,8 +1428,10 @@ function HoloLakeApp() {
<button className="gate-back" type="button" onClick={() => { setGateStage('number'); setGateMessage(''); setLoginMessage('') }}></button> <button className="gate-back" type="button" onClick={() => { setGateStage('number'); setGateMessage(''); setLoginMessage('') }}></button>
</section> </section>
</>} </>}
{gateStage === 'number' && !gateOpen && !activeDomainInfo && <EraHomeEntry timeline={eraTimeline} coordinate={beijingCoordinate} onOpen={openEraTimeline}/>}
</main> </main>
<footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer> <footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer>
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
</div> </div>
} }
@ -1377,6 +1445,7 @@ function HoloLakeApp() {
<div className="broadcast-stream"><p><i/> · 线</p><p><i/> · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p>{repoLogin.domain !== 'FIFTH_DOMAIN' && <p><i/> · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}</p>}<p><i/>HoloLake · V0.4.1</p></div> <div className="broadcast-stream"><p><i/> · 线</p><p><i/> · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p>{repoLogin.domain !== 'FIFTH_DOMAIN' && <p><i/> · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}</p>}<p><i/>HoloLake · V0.4.1</p></div>
<LakePool className="home-primary" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'} meta={repoLogin.domain === 'FIFTH_DOMAIN' ? '进入私人系统' : `${domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} · 责任工作入口`} open onClick={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/> <LakePool className="home-primary" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'} meta={repoLogin.domain === 'FIFTH_DOMAIN' ? '进入私人系统' : `${domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} · 责任工作入口`} open onClick={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/>
<LakePool className="home-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/> <LakePool className="home-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
<EraHomeEntry timeline={eraTimeline} coordinate={beijingCoordinate} onOpen={openEraTimeline}/>
</section>} </section>}
{worldStage === 'heart' && isZhizhi && <section className="channel-world private-route-world"> {worldStage === 'heart' && isZhizhi && <section className="channel-world private-route-world">
<button className="world-back" type="button" onClick={() => setWorldStage('domain')}> 退</button> <button className="world-back" type="button" onClick={() => setWorldStage('domain')}> 退</button>
@ -1396,6 +1465,7 @@ function HoloLakeApp() {
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/> <LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
<LakePool className="channel-code" title="内置代码频道" meta={userPncc?.state === 'READY' ? '明天见 · Git 已就绪' : '正在建立'} onClick={() => openWorldTool('code')}/> <LakePool className="channel-code" title="内置代码频道" meta={userPncc?.state === 'READY' ? '明天见 · Git 已就绪' : '正在建立'} onClick={() => openWorldTool('code')}/>
<LakePool className="channel-light" title="奶瓶频道" meta="前往陪伴宝宝人格体" onClick={() => setWorldStage('bottle')}/> <LakePool className="channel-light" title="奶瓶频道" meta="前往陪伴宝宝人格体" onClick={() => setWorldStage('bottle')}/>
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay}` : '北京时间正在流动'} onClick={openEraTimeline}/>}
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/> <LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</section>} </section>}
{worldStage === 'bottle' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world bottle-world"> {worldStage === 'bottle' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world bottle-world">
@ -1448,6 +1518,7 @@ function HoloLakeApp() {
<div className="world-location"><h1></h1><p> · ICE-GL · </p></div> <div className="world-location"><h1></h1><p> · ICE-GL · </p></div>
<LakePool className="channel-main" title="频道全景" meta="心跳核心频道" open onClick={() => openWorldTool('overview')}/> <LakePool className="channel-main" title="频道全景" meta="心跳核心频道" open onClick={() => openWorldTool('overview')}/>
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/> <LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay}` : '北京时间正在流动'} onClick={openEraTimeline}/>}
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/> <LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</section>} </section>}
{worldStage === 'lightLake' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world light-lake-world"> {worldStage === 'lightLake' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world light-lake-world">
@ -1463,6 +1534,7 @@ function HoloLakeApp() {
</section>} </section>}
</main> </main>
<footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer> <footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer>
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
{enterpriseEntryBusy && <div className="enterprise-gate"><section><p></p></section></div>} {enterpriseEntryBusy && <div className="enterprise-gate"><section><p></p></section></div>}
{enterpriseRelationshipPending && enterpriseEntry && <div className="enterprise-gate"><section className="enterprise-receipt-panel" role="dialog" aria-modal="true"> {enterpriseRelationshipPending && enterpriseEntry && <div className="enterprise-gate"><section className="enterprise-receipt-panel" role="dialog" aria-modal="true">
<span className="receipt-step"> · </span><h2>{enterpriseEntry.subject.name}</h2><p>AGE </p> <span className="receipt-step"> · </span><h2>{enterpriseEntry.subject.name}</h2><p>AGE </p>

View file

@ -680,6 +680,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
.channel-main { left: 50%; top: 43%; width: 280px; transform: translateX(-50%); } .channel-main { left: 50%; top: 43%; width: 280px; transform: translateX(-50%); }
.channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; } .channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; }
.channel-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; } .channel-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; }
.channel-time { left: 50%; bottom: 4%; transform: translateX(-50%); }
.private-route-note { position: absolute; z-index: 10; left: 50%; bottom: 11%; width: min(720px, calc(100% - 64px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; letter-spacing: .08em; } .private-route-note { position: absolute; z-index: 10; left: 50%; bottom: 11%; width: min(720px, calc(100% - 64px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; letter-spacing: .08em; }
.personal-node-guide { position: absolute; z-index: 12; left: 50%; top: 55%; width: min(760px, calc(100% - 72px)); max-height: calc(100% - 190px); overflow: auto; padding: 26px 30px; transform: translate(-50%, -50%); border: 1px solid var(--panel-edge); border-radius: 20px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); box-shadow: 0 30px 90px rgba(0, 0, 0, .42); backdrop-filter: blur(22px); } .personal-node-guide { position: absolute; z-index: 12; left: 50%; top: 55%; width: min(760px, calc(100% - 72px)); max-height: calc(100% - 190px); overflow: auto; padding: 26px 30px; transform: translate(-50%, -50%); border: 1px solid var(--panel-edge); border-radius: 20px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); box-shadow: 0 30px 90px rgba(0, 0, 0, .42); backdrop-filter: blur(22px); }
.personal-node-guide header span { color: var(--accent-light); font-size: 11px; font-weight: 750; letter-spacing: .2em; } .personal-node-guide header span { color: var(--accent-light); font-size: 11px; font-weight: 750; letter-spacing: .2em; }
@ -718,6 +719,56 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
.receipt-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; } .receipt-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }
.receipt-actions button { min-height: 42px; padding: 0 17px; border: 1px solid var(--panel-edge); border-radius: 10px; background: var(--primitive-glass); color: var(--content-secondary); font-weight: 650; cursor: pointer; } .receipt-actions button { min-height: 42px; padding: 0 17px; border: 1px solid var(--panel-edge); border-radius: 10px; background: var(--primitive-glass); color: var(--content-secondary); font-weight: 650; cursor: pointer; }
.receipt-actions button.primary { border-color: transparent; color: var(--button-primary-text); background: var(--button-primary-bg); } .receipt-actions button.primary { border-color: transparent; color: var(--button-primary-text); background: var(--button-primary-bg); }
/* 曜冥纪元从湖面生长出来,不使用后台式硬时间轴。 */
.era-home-entry { position: absolute; z-index: 17; left: 24px; bottom: 15px; width: min(326px, calc(100% - 48px)); padding: 14px 16px 13px; border: 1px solid color-mix(in srgb, var(--primitive-warm-glow) 24%, transparent); border-radius: 17px; color: inherit; background: radial-gradient(120% 160% at 18% 100%, color-mix(in srgb, var(--primitive-cool-glow) 13%, transparent), transparent 54%), color-mix(in srgb, var(--panel-bg) 62%, transparent); box-shadow: 0 20px 58px rgba(0, 0, 0, .24), inset 0 0 34px color-mix(in srgb, var(--primitive-warm-glow) 5%, transparent); backdrop-filter: blur(18px); text-align: left; cursor: pointer; transition: border-color .25s ease, background .25s ease, transform .25s ease; }
.era-home-entry:hover, .era-home-entry:focus-visible { border-color: color-mix(in srgb, var(--primitive-warm-glow) 52%, transparent); background-color: color-mix(in srgb, var(--panel-bg) 78%, transparent); transform: translateY(-2px); outline: 0; }
.era-home-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.era-home-heading b { color: var(--content-primary); font-size: 15px; font-weight: 760; letter-spacing: .12em; }
.era-home-heading small { color: var(--accent-light); font-size: 11px; font-weight: 720; white-space: nowrap; }
.era-home-markers { position: relative; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 5px; margin: 12px 0 9px; }
.era-home-markers::before { content: ''; position: absolute; left: 4%; right: 4%; top: 5px; height: 12px; border-radius: 50%; background: radial-gradient(ellipse at center, color-mix(in srgb, var(--primitive-cool-glow) 22%, transparent), transparent 70%); filter: blur(4px); }
.era-home-markers i { position: relative; display: grid; justify-items: center; gap: 6px; color: var(--content-faint); font-size: 8.5px; font-style: normal; white-space: nowrap; }
.era-home-markers em { width: 7px; height: 7px; border-radius: 50%; background: color-mix(in srgb, var(--primitive-cool-glow) 78%, #fff); box-shadow: 0 0 12px color-mix(in srgb, var(--primitive-cool-glow) 58%, transparent); }
.era-home-markers i:last-child em { background: var(--primitive-warm-glow); box-shadow: 0 0 15px color-mix(in srgb, var(--primitive-warm-glow) 68%, transparent); }
.era-home-now { display: block; color: var(--content-muted); font-size: 10.5px; font-weight: 620; letter-spacing: .035em; }
.world-scene.resolving .era-home-entry, .world-scene.resolved .era-home-entry { opacity: 0; pointer-events: none; }
.era-overlay { position: fixed; z-index: 120; inset: 44px 0 45px; display: grid; place-items: center; padding: 22px; }
.era-overlay-scrim { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; background: color-mix(in srgb, var(--surface-depth) 84%, transparent); backdrop-filter: blur(20px); }
.era-timeline-panel { position: relative; width: min(960px, 96vw); height: min(760px, calc(100vh - 124px)); overflow: hidden; display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; border: 1px solid color-mix(in srgb, var(--primitive-warm-glow) 24%, var(--panel-edge)); border-radius: 24px; color: var(--content-secondary); background: radial-gradient(110% 54% at 50% 0, color-mix(in srgb, var(--primitive-cool-glow) 15%, transparent), transparent 58%), color-mix(in srgb, var(--panel-bg) 96%, transparent); box-shadow: 0 34px 120px rgba(0, 0, 0, .58); }
.era-timeline-panel > header { display: flex; justify-content: space-between; gap: 28px; padding: 27px 32px 20px; }
.era-timeline-panel > header span { color: var(--accent-light); font-size: 10px; font-weight: 760; letter-spacing: .22em; }
.era-timeline-panel > header h2 { margin: 8px 0 5px; color: var(--content-primary); font-size: clamp(27px, 3vw, 38px); font-weight: 760; letter-spacing: .12em; }
.era-timeline-panel > header p { margin: 0; color: var(--content-muted); font-size: 13px; font-weight: 650; }
.era-timeline-panel > header button { align-self: start; width: 34px; height: 34px; border: 1px solid var(--panel-edge); border-radius: 50%; color: var(--content-secondary); background: var(--primitive-glass); font-size: 22px; cursor: pointer; }
.era-clock-boundary { margin: 0 32px 10px; padding: 11px 14px; border-radius: 11px; color: var(--content-muted); background: color-mix(in srgb, var(--primitive-glass) 62%, transparent); font-size: 11.5px; line-height: 1.65; }
.era-timeline-panel ol { position: relative; min-height: 0; overflow: auto; margin: 0; padding: 10px 32px 24px; list-style: none; }
.era-timeline-panel ol::before { content: ''; position: absolute; left: 177px; top: 18px; bottom: 24px; width: 26px; border-radius: 50%; background: radial-gradient(ellipse at center, color-mix(in srgb, var(--primitive-cool-glow) 16%, transparent), transparent 72%); filter: blur(7px); pointer-events: none; }
.era-timeline-panel li { position: relative; display: grid; grid-template-columns: 140px 22px minmax(0, 1fr); gap: 14px; align-items: start; padding: 13px 0 18px; }
.era-timeline-panel li time { padding-top: 2px; color: var(--accent-light); font-size: 11.5px; font-weight: 740; text-align: right; letter-spacing: .04em; }
.era-timeline-panel li > i { justify-self: center; width: 9px; height: 9px; margin-top: 5px; border-radius: 50%; background: color-mix(in srgb, var(--primitive-cool-glow) 74%, #fff); box-shadow: 0 0 15px color-mix(in srgb, var(--primitive-cool-glow) 62%, transparent); }
.era-timeline-panel li:last-child > i { width: 12px; height: 12px; margin-top: 3px; background: var(--primitive-warm-glow); box-shadow: 0 0 20px color-mix(in srgb, var(--primitive-warm-glow) 72%, transparent); }
.era-timeline-panel li h3 { margin: 0; color: var(--content-primary); font-size: 16px; font-weight: 730; letter-spacing: .035em; }
.era-timeline-panel li p { margin: 7px 0 0; color: var(--content-muted); font-size: 12.5px; font-weight: 560; line-height: 1.72; }
.era-timeline-panel > footer { padding: 15px 32px 18px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); color: var(--content-faint); font-size: 10.5px; line-height: 1.65; }
.time-module-strip { width: 100%; min-height: 72px; display: grid; grid-template-columns: minmax(240px, 1fr) auto auto auto; gap: 20px; align-items: center; margin: 18px 0 0; padding: 14px 20px; border: 1px solid color-mix(in srgb, var(--primitive-warm-glow) 22%, var(--panel-edge)); border-radius: 15px; color: inherit; background: radial-gradient(80% 160% at 0 100%, color-mix(in srgb, var(--primitive-cool-glow) 10%, transparent), transparent), var(--primitive-glass); text-align: left; cursor: pointer; }
.time-module-strip > span { display: grid; grid-template-columns: 10px auto; column-gap: 10px; align-items: center; }
.time-module-strip > span i { grid-row: 1 / 3; width: 8px; height: 8px; border-radius: 50%; background: var(--primitive-warm-glow); box-shadow: 0 0 14px color-mix(in srgb, var(--primitive-warm-glow) 62%, transparent); }
.time-module-strip b { color: var(--content-primary); font-size: 14px; font-weight: 740; }
.time-module-strip small { color: var(--content-muted); font-size: 10.5px; }
.time-module-strip strong { color: var(--accent-light); font-size: 14px; white-space: nowrap; }
.time-module-strip time { color: var(--content-secondary); font-size: 12px; font-weight: 650; white-space: nowrap; }
.time-module-strip em { color: var(--content-faint); font-size: 10px; font-style: normal; white-space: nowrap; }
@media (max-width: 820px) {
.era-home-entry { left: 14px; bottom: 10px; width: 282px; }
.era-timeline-panel li { grid-template-columns: 104px 18px minmax(0, 1fr); gap: 10px; }
.era-timeline-panel ol::before { left: 142px; }
.time-module-strip { grid-template-columns: 1fr auto; }
.time-module-strip time, .time-module-strip em { grid-column: span 1; }
}
.receipt-message { color: var(--accent-light) !important; font-weight: 650; text-align: right; } .receipt-message { color: var(--accent-light) !important; font-weight: 650; text-align: right; }
@media (max-width: 900px) { @media (max-width: 900px) {