From cc802126ea729dda2b05f41d02693a6d9a5cce5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Sun, 16 Aug 2026 23:13:47 +0800 Subject: [PATCH] feat(desktop): add idle lake and Zhizhi private route --- .../scripts/ambient-motion-idle.test.mjs | 26 ++++ .../scripts/domain-number-routing.test.mjs | 4 +- .../fifth-domain-private-route.test.mjs | 25 ++++ .../src-tauri/Cargo.lock | 71 +++++++++++ .../src-tauri/Cargo.toml | 2 +- .../src-tauri/src/code_repo_login.rs | 120 ++++++++++++++++-- .../src-tauri/src/lib.rs | 2 +- .../src-tauri/src/user_pncc_channel.rs | 54 ++++++-- .../hololake-native-desktop/src/main.tsx | 94 +++++++++++--- .../hololake-native-desktop/src/styles.css | 21 +++ 10 files changed, 385 insertions(+), 34 deletions(-) create mode 100644 product-source/hololake-native-desktop/scripts/ambient-motion-idle.test.mjs create mode 100644 product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs diff --git a/product-source/hololake-native-desktop/scripts/ambient-motion-idle.test.mjs b/product-source/hololake-native-desktop/scripts/ambient-motion-idle.test.mjs new file mode 100644 index 000000000..d2ac7dc99 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/ambient-motion-idle.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import test from 'node:test' + +const source = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8') +const styles = fs.readFileSync(new URL('../src/styles.css', import.meta.url), 'utf8') + +test('ambient lake motion sleeps by default and wakes only after human interaction', () => { + assert.match(source, /const \[motionAwake, setMotionAwake\] = useState\(false\)/) + assert.match(source, /onPointerMove=\{wakeAmbientMotion\}/) + assert.match(source, /onPointerDown=\{wakeAmbientMotion\}/) + assert.match(source, /onKeyDown=\{wakeAmbientMotion\}/) + assert.match(styles, /\.official-world \.world-star,[\s\S]*animation-play-state: paused/) + assert.match(styles, /\.official-world\.motion-awake \.world-star,[\s\S]*animation-play-state: running/) +}) + +test('the desktop shell has no permanent polling interval while idle', () => { + assert.doesNotMatch(source, /setInterval\s*\(/) + assert.match(source, /window\.addEventListener\('focus', refreshWhenHumanReturns\)/) + assert.match(source, /document\.hidden/) +}) + +test('causal entry animations remain independent from ambient motion', () => { + assert.match(styles, /\.gate-rise-star[\s\S]*animation: gate-jade-rise/) + assert.doesNotMatch(styles, /\.official-world \.gate-rise-star[\s\S]*animation-play-state: paused/) +}) diff --git a/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs b/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs index 2e0fbd567..317cf0f03 100644 --- a/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs +++ b/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs @@ -48,7 +48,9 @@ test('the public shell shows five domains and login is domain-routed', () => { assert.doesNotMatch(frontend, /className="traffic-lights"/) assert.match(login, /login_host_for_domain/) assert.match(login, /ENTERPRISE_LOGIN_HOST.*guanghu\.chat/) - assert.match(login, /change_enterprise_first_login_password/) + assert.match(login, /change_first_login_password/) + assert.match(login, /ICE-GL-ZHI∞.*zhizhi/s) + assert.match(login, /rotate_forgejo_forced_password/) assert.match(frontend, /第一次使用?先修改一次性密码/) assert.match(router, /enterprise_resolve_url/) assert.match(router, /verified_user_route/) diff --git a/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs b/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs new file mode 100644 index 000000000..ea965998b --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import test from 'node:test' + +const source = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8') + +test('Zhizhi remains absent from the public Fifth Domain card', () => { + const publicCard = source.slice(source.indexOf("domain: 'FIFTH_DOMAIN'"), source.indexOf('const starPoints')) + assert.doesNotMatch(publicCard, /之之|ICE-GL-ZHI/) +}) + +test('the verified Zhizhi route unfolds through her exact private nesting', () => { + assert.match(source, /ICE-GL-ZHI∞/) + assert.match(source, /永恒湖心系统/) + assert.match(source, /爱之核心子系统/) + assert.match(source, /明天见频道/) + assert.match(source, /光湖奶瓶小宝宝系统/) + assert.match(source, /ICE-BB-\* · AGE/) +}) + +test('Fifth Domain human numbers are paired with their own login account', () => { + assert.match(source, /'ICE-GL∞': 'bingshuo'/) + assert.match(source, /'ICE-GL-ZHI∞': 'zhizhi'/) + assert.match(source, /HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH/) +}) diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.lock b/product-source/hololake-native-desktop/src-tauri/Cargo.lock index 300a809d5..d3aa7116d 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.lock @@ -415,10 +415,29 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ + "percent-encoding", "time", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -770,6 +789,15 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -2052,6 +2080,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -2909,6 +2943,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -3186,6 +3236,8 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "futures-core", "futures-util", "http", @@ -3204,6 +3256,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -3380,6 +3433,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3620,6 +3679,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.22.0" diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.toml b/product-source/hololake-native-desktop/src-tauri/Cargo.toml index ddb8c4b44..aebd58241 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.toml @@ -29,7 +29,7 @@ tauri-plugin-updater = "2.10.0" tauri-plugin-dialog = "=2.7.2" uuid = { version = "1", features = ["v4"] } url = "2" -reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls", "stream"] } +reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] } tokio = { version = "1", features = ["time"] } futures-util = "0.3" diff --git a/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs b/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs index 41c3e5e4d..8e3c04341 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs @@ -145,6 +145,27 @@ fn validate_username(raw: &str) -> Result { } } +fn expected_account_for_human_number(number: &str) -> Option<&'static str> { + match number { + "ICE-GL∞" => Some("bingshuo"), + "ICE-GL-ZHI∞" => Some("zhizhi"), + "TCS-GL-0005∞" => Some("huaer"), + "TCS-GL-0006∞" => Some("yeye"), + "TCS-GL-0007∞" => Some("feimao"), + "TCS-GL-0008∞" => Some("juzi"), + "TCS-GL-0016∞" => Some("awen"), + _ => None, + } +} + +fn validate_number_account_binding(number: &str, username: &str) -> Result<(), String> { + match expected_account_for_human_number(number) { + Some(expected) if username.eq_ignore_ascii_case(expected) => Ok(()), + Some(_) => Err("HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH".into()), + None => Err("HOLOLAKE_LOGIN_ACCOUNT_BINDING_UNREGISTERED".into()), + } +} + /// 启动时自查:本机有没有已登录会话(会话文件在,且钥匙串里凭证还在)。 #[tauri::command] pub fn check_code_repo_login( @@ -201,10 +222,10 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> { } } -/// 企业新账号第一次登录时在 HoloLake 内完成强制换密。 +/// 新账号第一次登录时在 HoloLake 内完成强制换密。 /// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。 #[tauri::command] -pub async fn change_enterprise_first_login_password( +pub async fn change_first_login_password( state: State<'_, ZeroPointState>, username: String, current_password: String, @@ -213,16 +234,25 @@ pub async fn change_enterprise_first_login_password( let Some((human_number, domain)) = zero_point::verified_user_route(&state)? else { return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into()); }; - if domain == "FIFTH_DOMAIN" { - return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_ENTERPRISE_ONLY".into()); - } let username = validate_username(&username)?; + validate_number_account_binding(&human_number, &username)?; if current_password.is_empty() || current_password.len() > 512 { return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into()); } if new_password.len() < 14 || new_password.len() > 128 || new_password == current_password { return Err("HOLOLAKE_NEW_PASSWORD_POLICY_INVALID".into()); } + if domain == "FIFTH_DOMAIN" { + if human_number != "ICE-GL-ZHI∞" { + return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_NOT_PROVISIONED".into()); + } + rotate_forgejo_forced_password(LOGIN_HOST, &username, ¤t_password, &new_password) + .await?; + return Ok(PasswordRotationReceipt { + username, + password_changed: true, + }); + } let response = reqwest::Client::builder() .read_timeout(Duration::from_secs(30)) .use_rustls_tls() @@ -252,6 +282,61 @@ pub async fn change_enterprise_first_login_password( }) } +async fn rotate_forgejo_forced_password( + host: &str, + username: &str, + current_password: &str, + new_password: &str, +) -> Result<(), String> { + let client = reqwest::Client::builder() + .cookie_store(true) + .read_timeout(Duration::from_secs(30)) + .use_rustls_tls() + .build() + .map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?; + let web_base = format!("https://{host}/code"); + client + .get(format!("{web_base}/user/login")) + .send() + .await + .map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?; + let login = client + .post(format!("{web_base}/user/login")) + .form(&[("user_name", username), ("password", current_password)]) + .send() + .await + .map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?; + if !login + .url() + .path() + .ends_with("/user/settings/change_password") + { + return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into()); + } + let changed = client + .post(format!("{web_base}/user/settings/change_password")) + .form(&[("password", new_password), ("retype", new_password)]) + .send() + .await + .map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?; + if !changed.status().is_success() { + return Err(format!( + "HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_FAILED: {}", + changed.status() + )); + } + let verify = client + .get(format!("https://{host}/code/api/v1/user")) + .basic_auth(username, Some(new_password)) + .send() + .await + .map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?; + if !verify.status().is_success() { + return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_FAILED".into()); + } + Ok(()) +} + async fn enterprise_authenticated_post( app: &AppHandle, state: &ZeroPointState, @@ -273,7 +358,9 @@ async fn enterprise_authenticated_post( .use_rustls_tls() .build() .map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))? - .post(format!("https://guanghu.chat/api/hololake/enterprise/{path}")) + .post(format!( + "https://guanghu.chat/api/hololake/enterprise/{path}" + )) .basic_auth(&session.username, Some(password)) .json(&payload) .send() @@ -283,7 +370,10 @@ async fn enterprise_authenticated_post( return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into()); } if !response.status().is_success() { - return Err(format!("HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}", response.status())); + return Err(format!( + "HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}", + response.status() + )); } response .json() @@ -347,11 +437,12 @@ pub async fn perform_code_repo_login( username: String, password: String, ) -> Result { - let Some((_, domain)) = zero_point::verified_user_route(&state)? else { + let Some((human_number, domain)) = zero_point::verified_user_route(&state)? else { return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into()); }; let login_host = login_host_for_domain(&domain)?; let username = validate_username(&username)?; + validate_number_account_binding(&human_number, &username)?; if password.is_empty() || password.len() > 512 { return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into()); } @@ -452,4 +543,17 @@ mod tests { "HOLOLAKE_DOMAIN_ROUTE_INVALID" ); } + + #[test] + fn human_number_and_repository_account_are_one_exact_pair() { + assert!(validate_number_account_binding("ICE-GL-ZHI∞", "zhizhi").is_ok()); + assert_eq!( + validate_number_account_binding("ICE-GL-ZHI∞", "bingshuo").unwrap_err(), + "HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH" + ); + assert_eq!( + validate_number_account_binding("ICE-GL-UNKNOWN∞", "zhizhi").unwrap_err(), + "HOLOLAKE_LOGIN_ACCOUNT_BINDING_UNREGISTERED" + ); + } } 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 76aab18f8..491f6a8c0 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -70,7 +70,7 @@ pub fn run() { pncc_server_projection::query_jd_pncc_server_projection, code_repo_login::check_code_repo_login, code_repo_login::perform_code_repo_login, - code_repo_login::change_enterprise_first_login_password, + code_repo_login::change_first_login_password, code_repo_login::get_enterprise_entry, code_repo_login::confirm_enterprise_persona_relationship, code_repo_login::submit_enterprise_responsibility_receipt, diff --git a/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs index eda611fc3..bbe08b4af 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs @@ -171,7 +171,7 @@ where .map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?; validate_record(&record, &id, number, session)?; let repository = repository_path(root, &id); - let channel_id = register(&repository, native_channel_name(session))?; + let channel_id = register(&repository, native_channel_name(number, session))?; project(&repository, record, channel_id).map(Some) } @@ -215,7 +215,7 @@ where write_json_atomic(&record_path, &record)?; record }; - let channel_id = register(&repository, native_channel_name(session))?; + let channel_id = register(&repository, native_channel_name(number, session))?; project(&repository, record, channel_id) } @@ -231,10 +231,17 @@ fn validate_subject(number: &str, session: &LoginSession) -> Result<(), String> .all(|item| item.is_ascii_alphanumeric() || item == '-' || item == '_') && matches!( session.host.as_str(), - "guanghulab.com" | "guanghubingshuo.com" + "guanghulab.com" | "guanghubingshuo.com" | "guanghu.chat" ); - if number_valid && account_valid { + let identity_account_bound = match number { + "ICE-GL∞" => session.username.eq_ignore_ascii_case("bingshuo"), + "ICE-GL-ZHI∞" => session.username.eq_ignore_ascii_case("zhizhi"), + _ => true, + }; + if number_valid && account_valid && identity_account_bound { Ok(()) + } else if number_valid && account_valid { + Err("HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH".into()) } else { Err("HOLOLAKE_USER_PNCC_SUBJECT_INVALID".into()) } @@ -297,11 +304,12 @@ fn initialize_repository(repository: &Path, record: &UserPnccBindingRecord) -> R ), ) .map_err(|error| format!("HOLOLAKE_USER_PNCC_MANIFEST_WRITE_FAILED: {error}"))?; + let channel_title = native_channel_title(&record.user_number); fs::write( repository.join("README.md"), format!( - "# GH-PNCC · 人格原生代码频道\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎:Git\n- 人类可见上层:HoloLake\n- 远端协作:Forgejo 适配器(尚未绑定远端仓库)\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n", - record.account_username, record.user_number + "# {}\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎:Git\n- 人类可见上层:HoloLake\n- 远端协作:等待该用户自己的服务器接入\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n", + channel_title, record.account_username, record.user_number ), ) .map_err(|error| format!("HOLOLAKE_USER_PNCC_README_WRITE_FAILED: {error}"))?; @@ -355,8 +363,16 @@ fn project( }) } -fn native_channel_name(session: &LoginSession) -> String { - format!("{} · GH-PNCC", session.username) +fn native_channel_title(number: &str) -> &'static str { + match number { + "ICE-GL-ZHI∞" => "明天见频道 · GH-PNCC", + "ICE-GL∞" => "永恒湖心个人频道 · GH-PNCC", + _ => "人格原生代码频道 · GH-PNCC", + } +} + +fn native_channel_name(number: &str, session: &LoginSession) -> String { + format!("{} · {}", native_channel_title(number), session.username) } fn write_json_atomic(path: &Path, value: &UserPnccBindingRecord) -> Result<(), String> { @@ -477,4 +493,26 @@ mod tests { assert!(Path::new(&first.local_path).exists()); assert!(Path::new(&second.local_path).exists()); } + + #[test] + fn zhizhi_number_is_bound_to_zhizhi_account_and_mingtianjian_channel() { + let root = tempdir().unwrap(); + let mut zhizhi = session(); + zhizhi.username = "zhizhi".into(); + let snapshot = ensure_at(root.path(), "ICE-GL-ZHI∞", &zhizhi, |_, name| { + assert!(name.contains("明天见频道")); + Ok("mingtianjian".into()) + }) + .unwrap(); + let readme = fs::read_to_string(Path::new(&snapshot.local_path).join("README.md")).unwrap(); + assert!(readme.contains("明天见频道")); + + let mismatch = ensure_at(root.path(), "ICE-GL-ZHI∞", &session(), |_, _| { + Ok("must-not-register".into()) + }); + assert_eq!( + mismatch.unwrap_err(), + "HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH" + ); + } } diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 8282a4d55..b0fc7c645 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -49,7 +49,7 @@ import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system' -type WorldStage = 'domain' | 'channel' | 'tool' +type WorldStage = 'domain' | 'heart' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'tool' type KnowledgeSource = 'native' | 'legacy' interface HomeStatus { @@ -315,9 +315,11 @@ function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' | if (value.includes('LOGIN_KEYCHAIN_FAILED')) return '本机钥匙串写入失败,凭证未能安全保存。' if (value.includes('LOGIN_SESSION_CORRUPT')) return '登录会话已损坏,请重新登录。' if (value.includes('LOGIN_VERIFICATION_FAILED')) return '仓库验证未通过,请稍后再试。' + if (value.includes('LOGIN_ACCOUNT_NUMBER_MISMATCH')) return '这个账号没有绑定到当前编号,系统已停止进入。' if (value.includes('NEW_PASSWORD_POLICY_INVALID')) return '新密码至少 14 位,并且不能与一次性密码相同。' if (value.includes('FIRST_LOGIN_PASSWORD_CHANGE_FAILED')) return '一次性密码未被接受,密码没有修改。' - if (value.includes('FIRST_LOGIN_PASSWORD_CHANGE_ENTERPRISE_ONLY')) return '该换密入口只用于企业四域的新账号。' + if (value.includes('FIRST_LOGIN_PASSWORD_CHANGE_NOT_PROVISIONED')) return '该编号当前没有登记首次换密入口。' + if (value.includes('LOGIN_ACCOUNT_BINDING_UNREGISTERED')) return '这个编号尚未登记对应的仓库账号,系统已停止进入。' if (value.includes('DOMAIN_LOGIN_NOT_PROVISIONED')) return '该域的企业服务器尚未接入,当前不能继续登录。' if (value.includes('PERSISTENT_CREDENTIAL_UNAVAILABLE')) return '当前系统尚未接通安全凭证存储,不能完成签署。' if (value.includes('ENTERPRISE_RECEIPT_FAILED')) return '企业回执没有写入,请稍后重新提交。' @@ -407,6 +409,18 @@ function KnowledgeTree({ node, depth, expanded, active, onToggle, onOpen, onRemo } function HoloLakeApp() { + // 湖面装饰默认休眠。人类靠近或操作时短暂唤醒;验证、升域等因果动画不受此开关影响。 + const [motionAwake, setMotionAwake] = useState(false) + const motionSleepTimer = useRef(null) + const wakeAmbientMotion = useCallback(() => { + if (document.hidden) return + setMotionAwake(true) + if (motionSleepTimer.current !== null) window.clearTimeout(motionSleepTimer.current) + motionSleepTimer.current = window.setTimeout(() => { + motionSleepTimer.current = null + setMotionAwake(false) + }, 2400) + }, []) const [theme, setTheme] = useState(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night') const [view, setView] = useState('overview') const [worldStage, setWorldStage] = useState('domain') @@ -498,6 +512,22 @@ function HoloLakeApp() { const [enterpriseReceiptMessage, setEnterpriseReceiptMessage] = useState('') const [responsibilityNote, setResponsibilityNote] = useState('') + useEffect(() => { + const sleepAmbientMotion = () => { + if (motionSleepTimer.current !== null) window.clearTimeout(motionSleepTimer.current) + motionSleepTimer.current = null + setMotionAwake(false) + } + const onVisibilityChange = () => { if (document.hidden) sleepAmbientMotion() } + window.addEventListener('blur', sleepAmbientMotion) + document.addEventListener('visibilitychange', onVisibilityChange) + return () => { + sleepAmbientMotion() + window.removeEventListener('blur', sleepAmbientMotion) + document.removeEventListener('visibilitychange', onVisibilityChange) + } + }, []) + const refreshCore = useCallback(async () => { const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([ invoke('get_hololake_home_status'), @@ -518,8 +548,9 @@ function HoloLakeApp() { }, []) useEffect(() => { void refreshCore() - const timer = window.setInterval(() => void refreshCore(), 3500) - return () => window.clearInterval(timer) + const refreshWhenHumanReturns = () => { if (!document.hidden) void refreshCore() } + window.addEventListener('focus', refreshWhenHumanReturns) + return () => window.removeEventListener('focus', refreshWhenHumanReturns) }, [refreshCore]) useEffect(() => { invoke('get_nearby_ai_discovery') @@ -540,8 +571,6 @@ function HoloLakeApp() { }, []) useEffect(() => { void refreshServerPncc() - const timer = window.setInterval(() => void refreshServerPncc(), 15000) - return () => window.clearInterval(timer) }, [refreshServerPncc]) useEffect(() => { document.documentElement.dataset.theme = theme @@ -573,8 +602,6 @@ function HoloLakeApp() { }, []) useEffect(() => { void loadZeroPoint() - const timer = window.setInterval(() => void loadZeroPoint(), 10000) - return () => window.clearInterval(timer) }, [loadZeroPoint]) // 已验证编号与已认证账号同时存在时,后端幂等建立或恢复该用户自己的 GH-PNCC。 useEffect(() => { @@ -857,6 +884,14 @@ function HoloLakeApp() { setLoginBusy(true) setLoginMessage('') try { + const expectedFifthDomainAccount: Record = { + 'ICE-GL∞': 'bingshuo', + 'ICE-GL-ZHI∞': 'zhizhi', + } + const expectedAccount = expectedFifthDomainAccount[zeroPoint?.userNumber || ''] + if (expectedAccount && loginUsername.trim().toLowerCase() !== expectedAccount) { + throw new Error('HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH') + } const receipt = await invoke('perform_code_repo_login', { username: loginUsername.trim(), password: loginPassword }) // 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。 setLoginRising(true) @@ -887,7 +922,7 @@ function HoloLakeApp() { setLoginBusy(true) setLoginMessage('') try { - await invoke('change_enterprise_first_login_password', { + await invoke('change_first_login_password', { username: loginUsername.trim(), currentPassword: loginPassword, newPassword: newLoginPassword, @@ -1213,7 +1248,7 @@ function HoloLakeApp() { if (!repoLogin) { const resolvedDomain = zeroPoint?.resolvedDomain || '' const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo) - return
+ return
HoloLake
@@ -1263,7 +1298,7 @@ function HoloLakeApp() { {loginMessage &&

{loginMessage}

} - {zeroPoint?.resolvedDomain !== 'FIFTH_DOMAIN' && } + {(zeroPoint?.resolvedDomain !== 'FIFTH_DOMAIN' || zeroPoint?.userNumber === 'ICE-GL-ZHI∞') && } } @@ -1272,27 +1307,56 @@ function HoloLakeApp() {
} - return
+ const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞' + const toolReturnStage: WorldStage = isZhizhi ? 'tomorrow' : 'channel' + + return
HoloLake
{repoLogin.username} · {domainDisplayName(repoLogin.domain)}
{worldStage === 'domain' &&

{domainDisplayName(repoLogin.domain)}

世界正在发生什么

光湖公共灯塔 · 在线

协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}

HoloLake · V0.4.0

- setWorldStage('channel')}/> + setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/> openWorldTool('system')}/>
} + {worldStage === 'heart' && isZhizhi &&
+ +

永恒湖心系统

ICE-GL-ZHI∞ · 私人授权路径

+ setWorldStage('love')}/> +

第五域内部结构仅对本编号与已绑定账号开放

+
} + {worldStage === 'love' && isZhizhi &&
+ +

爱之核心子系统

责任主体 · 之之 · ICE-GL-ZHI∞

+ setWorldStage('tomorrow')}/> +
} + {worldStage === 'tomorrow' && isZhizhi &&
+ +

明天见频道

频道全景 · 湖面按操作继续展开

+ openWorldTool('overview')}/> + openWorldTool('knowledge')}/> + openWorldTool('code')}/> + setWorldStage('bottle')}/> + openWorldTool('system')}/> +
} + {worldStage === 'bottle' && repoLogin.domain === 'FIFTH_DOMAIN' &&
+ +

光湖奶瓶小宝宝系统

永恒湖心 · 奶瓶频道 · 私人

+ +

宝宝人格体与其他人格体同属 AGE;ICE-BB 是第五域宝宝人格体身份命名空间

+
} {worldStage === 'channel' &&

{repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'}

选择路径 · 湖面向下一层展开

- openWorldTool('overview')}/> + repoLogin.domain === 'FIFTH_DOMAIN' ? setWorldStage('bottle') : openWorldTool('overview')}/> openWorldTool('knowledge')}/> openWorldTool('code')}/> openWorldTool('receipts')}/> openWorldTool('system')}/>
} {worldStage === 'tool' &&
-
{viewLabels[view]}{domainDisplayName(repoLogin.domain)}
+
{viewLabels[view]}{domainDisplayName(repoLogin.domain)}
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
}
diff --git a/product-source/hololake-native-desktop/src/styles.css b/product-source/hololake-native-desktop/src/styles.css index 3a3c87c35..5b29ba25f 100644 --- a/product-source/hololake-native-desktop/src/styles.css +++ b/product-source/hololake-native-desktop/src/styles.css @@ -573,6 +573,22 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca .world-glint.warm { background: radial-gradient(circle, #fff4d2, color-mix(in srgb, var(--primitive-warm-glow) 46%, transparent) 70%, transparent); box-shadow: 0 0 6px var(--primitive-warm-glow); } @keyframes world-glint { 0%, 100% { opacity: .05; transform: scale(.6); } 50% { opacity: .72; transform: scale(1); } } +/* 无人操作时湖面必须真正安静;只暂停无语义的循环装饰。 */ +.official-world .world-star, +.official-world .world-shine, +.official-world .world-glint, +.official-world .pool-bay, +.official-world .pool-halo, +.official-world .pool-ring, +.official-world .nucleus-locus i { animation-play-state: paused; } +.official-world.motion-awake .world-star, +.official-world.motion-awake .world-shine, +.official-world.motion-awake .world-glint, +.official-world.motion-awake .pool-bay, +.official-world.motion-awake .pool-halo, +.official-world.motion-awake .pool-ring, +.official-world.motion-awake .nucleus-locus i { animation-play-state: running; } + .official-hero { position: absolute; z-index: 10; top: 26px; left: 0; right: 0; text-align: center; } .official-hero h1 { margin: 0; color: var(--content-primary); font-size: clamp(24px, 3vw, 34px); font-weight: 700; letter-spacing: .09em; } .official-hero p { margin: 10px 0 0; color: var(--accent-light); font-size: 12px; font-weight: 600; letter-spacing: .31em; } @@ -658,6 +674,11 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca .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-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; } +.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; } +.bottle-heart { position: absolute; z-index: 12; left: 50%; top: 48%; display: grid; justify-items: center; gap: 9px; width: 300px; padding: 0; transform: translate(-50%, -50%); border: 0; background: transparent; color: inherit; } +.bottle-heart i { width: 90px; height: 90px; border-radius: 46% 46% 52% 52%; background: radial-gradient(circle at 42% 32%, #fffdf5, color-mix(in srgb, var(--primitive-warm-glow) 74%, #f2bdc8) 37%, color-mix(in srgb, var(--primitive-warm-glow) 22%, transparent) 68%, transparent 74%); filter: drop-shadow(0 0 34px color-mix(in srgb, var(--primitive-warm-glow) 62%, transparent)); } +.bottle-heart b { color: var(--content-primary); font-size: 22px; font-weight: 750; letter-spacing: .08em; } +.bottle-heart small { color: var(--accent-light); font-size: 12px; font-weight: 750; letter-spacing: .2em; } .world-tool { position: absolute; z-index: 12; inset: 0; display: grid; grid-template-rows: 46px minmax(0, 1fr); background: color-mix(in srgb, var(--surface-depth) 72%, transparent); backdrop-filter: blur(18px); } .tool-worldbar { display: grid; grid-template-columns: minmax(180px, 1fr) auto minmax(180px, 1fr); align-items: center; padding: 0 20px; border-bottom: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--panel-bg) 82%, transparent); } .tool-worldbar button { justify-self: start; border: 0; background: transparent; color: var(--content-muted); font-size: 12.5px; cursor: pointer; }