feat(desktop): add idle lake and Zhizhi private route
This commit is contained in:
parent
466542ac93
commit
cc802126ea
10 changed files with 385 additions and 34 deletions
|
|
@ -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/)
|
||||||
|
})
|
||||||
|
|
@ -48,7 +48,9 @@ test('the public shell shows five domains and login is domain-routed', () => {
|
||||||
assert.doesNotMatch(frontend, /className="traffic-lights"/)
|
assert.doesNotMatch(frontend, /className="traffic-lights"/)
|
||||||
assert.match(login, /login_host_for_domain/)
|
assert.match(login, /login_host_for_domain/)
|
||||||
assert.match(login, /ENTERPRISE_LOGIN_HOST.*guanghu\.chat/)
|
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(frontend, /第一次使用?先修改一次性密码/)
|
||||||
assert.match(router, /enterprise_resolve_url/)
|
assert.match(router, /enterprise_resolve_url/)
|
||||||
assert.match(router, /verified_user_route/)
|
assert.match(router, /verified_user_route/)
|
||||||
|
|
|
||||||
|
|
@ -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/)
|
||||||
|
})
|
||||||
|
|
@ -415,10 +415,29 @@ version = "0.18.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
"time",
|
"time",
|
||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -770,6 +789,15 @@ dependencies = [
|
||||||
"syn 2.0.115",
|
"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]]
|
[[package]]
|
||||||
name = "dom_query"
|
name = "dom_query"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
|
|
@ -2052,6 +2080,12 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
@ -2909,6 +2943,22 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.41.0"
|
version = "0.41.0"
|
||||||
|
|
@ -3186,6 +3236,8 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"cookie_store",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
|
|
@ -3204,6 +3256,7 @@ dependencies = [
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
|
@ -3380,6 +3433,12 @@ version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
@ -3620,6 +3679,18 @@ dependencies = [
|
||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.22.0"
|
version = "3.22.0"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ tauri-plugin-updater = "2.10.0"
|
||||||
tauri-plugin-dialog = "=2.7.2"
|
tauri-plugin-dialog = "=2.7.2"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
url = "2"
|
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"] }
|
tokio = { version = "1", features = ["time"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,27 @@ fn validate_username(raw: &str) -> Result<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]
|
#[tauri::command]
|
||||||
pub fn check_code_repo_login(
|
pub fn check_code_repo_login(
|
||||||
|
|
@ -201,10 +222,10 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 企业新账号第一次登录时在 HoloLake 内完成强制换密。
|
/// 新账号第一次登录时在 HoloLake 内完成强制换密。
|
||||||
/// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。
|
/// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn change_enterprise_first_login_password(
|
pub async fn change_first_login_password(
|
||||||
state: State<'_, ZeroPointState>,
|
state: State<'_, ZeroPointState>,
|
||||||
username: String,
|
username: String,
|
||||||
current_password: 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 {
|
let Some((human_number, domain)) = zero_point::verified_user_route(&state)? else {
|
||||||
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
|
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)?;
|
let username = validate_username(&username)?;
|
||||||
|
validate_number_account_binding(&human_number, &username)?;
|
||||||
if current_password.is_empty() || current_password.len() > 512 {
|
if current_password.is_empty() || current_password.len() > 512 {
|
||||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||||
}
|
}
|
||||||
if new_password.len() < 14 || new_password.len() > 128 || new_password == current_password {
|
if new_password.len() < 14 || new_password.len() > 128 || new_password == current_password {
|
||||||
return Err("HOLOLAKE_NEW_PASSWORD_POLICY_INVALID".into());
|
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()
|
let response = reqwest::Client::builder()
|
||||||
.read_timeout(Duration::from_secs(30))
|
.read_timeout(Duration::from_secs(30))
|
||||||
.use_rustls_tls()
|
.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(
|
async fn enterprise_authenticated_post(
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
state: &ZeroPointState,
|
state: &ZeroPointState,
|
||||||
|
|
@ -273,7 +358,9 @@ async fn enterprise_authenticated_post(
|
||||||
.use_rustls_tls()
|
.use_rustls_tls()
|
||||||
.build()
|
.build()
|
||||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?
|
.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))
|
.basic_auth(&session.username, Some(password))
|
||||||
.json(&payload)
|
.json(&payload)
|
||||||
.send()
|
.send()
|
||||||
|
|
@ -283,7 +370,10 @@ async fn enterprise_authenticated_post(
|
||||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||||
}
|
}
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(format!("HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}", response.status()));
|
return Err(format!(
|
||||||
|
"HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
response
|
response
|
||||||
.json()
|
.json()
|
||||||
|
|
@ -347,11 +437,12 @@ pub async fn perform_code_repo_login(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<LoginReceipt, String> {
|
) -> Result<LoginReceipt, String> {
|
||||||
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());
|
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
|
||||||
};
|
};
|
||||||
let login_host = login_host_for_domain(&domain)?;
|
let login_host = login_host_for_domain(&domain)?;
|
||||||
let username = validate_username(&username)?;
|
let username = validate_username(&username)?;
|
||||||
|
validate_number_account_binding(&human_number, &username)?;
|
||||||
if password.is_empty() || password.len() > 512 {
|
if password.is_empty() || password.len() > 512 {
|
||||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||||
}
|
}
|
||||||
|
|
@ -452,4 +543,17 @@ mod tests {
|
||||||
"HOLOLAKE_DOMAIN_ROUTE_INVALID"
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ pub fn run() {
|
||||||
pncc_server_projection::query_jd_pncc_server_projection,
|
pncc_server_projection::query_jd_pncc_server_projection,
|
||||||
code_repo_login::check_code_repo_login,
|
code_repo_login::check_code_repo_login,
|
||||||
code_repo_login::perform_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::get_enterprise_entry,
|
||||||
code_repo_login::confirm_enterprise_persona_relationship,
|
code_repo_login::confirm_enterprise_persona_relationship,
|
||||||
code_repo_login::submit_enterprise_responsibility_receipt,
|
code_repo_login::submit_enterprise_responsibility_receipt,
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ where
|
||||||
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?;
|
.map_err(|error| format!("HOLOLAKE_USER_PNCC_BINDING_INVALID: {error}"))?;
|
||||||
validate_record(&record, &id, number, session)?;
|
validate_record(&record, &id, number, session)?;
|
||||||
let repository = repository_path(root, &id);
|
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)
|
project(&repository, record, channel_id).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,7 +215,7 @@ where
|
||||||
write_json_atomic(&record_path, &record)?;
|
write_json_atomic(&record_path, &record)?;
|
||||||
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)
|
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 == '_')
|
.all(|item| item.is_ascii_alphanumeric() || item == '-' || item == '_')
|
||||||
&& matches!(
|
&& matches!(
|
||||||
session.host.as_str(),
|
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(())
|
Ok(())
|
||||||
|
} else if number_valid && account_valid {
|
||||||
|
Err("HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH".into())
|
||||||
} else {
|
} else {
|
||||||
Err("HOLOLAKE_USER_PNCC_SUBJECT_INVALID".into())
|
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}"))?;
|
.map_err(|error| format!("HOLOLAKE_USER_PNCC_MANIFEST_WRITE_FAILED: {error}"))?;
|
||||||
|
let channel_title = native_channel_title(&record.user_number);
|
||||||
fs::write(
|
fs::write(
|
||||||
repository.join("README.md"),
|
repository.join("README.md"),
|
||||||
format!(
|
format!(
|
||||||
"# GH-PNCC · 人格原生代码频道\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎:Git\n- 人类可见上层:HoloLake\n- 远端协作:Forgejo 适配器(尚未绑定远端仓库)\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n",
|
"# {}\n\n这是 HoloLake 为用户 `{}` 建立的本机原生代码频道。\n\n- 耐久化引擎:Git\n- 人类可见上层:HoloLake\n- 远端协作:等待该用户自己的服务器接入\n- 用户编号:`{}`\n\n本仓库不保存账号密码,也不因建立代码频道而声称人格绑定或授予推送、发布、部署与现实执行权限。\n",
|
||||||
record.account_username, record.user_number
|
channel_title, record.account_username, record.user_number
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map_err(|error| format!("HOLOLAKE_USER_PNCC_README_WRITE_FAILED: {error}"))?;
|
.map_err(|error| format!("HOLOLAKE_USER_PNCC_README_WRITE_FAILED: {error}"))?;
|
||||||
|
|
@ -355,8 +363,16 @@ fn project(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn native_channel_name(session: &LoginSession) -> String {
|
fn native_channel_title(number: &str) -> &'static str {
|
||||||
format!("{} · GH-PNCC", session.username)
|
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> {
|
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(&first.local_path).exists());
|
||||||
assert!(Path::new(&second.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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ import './styles.css'
|
||||||
|
|
||||||
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
||||||
type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system'
|
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'
|
type KnowledgeSource = 'native' | 'legacy'
|
||||||
|
|
||||||
interface HomeStatus {
|
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_KEYCHAIN_FAILED')) return '本机钥匙串写入失败,凭证未能安全保存。'
|
||||||
if (value.includes('LOGIN_SESSION_CORRUPT')) return '登录会话已损坏,请重新登录。'
|
if (value.includes('LOGIN_SESSION_CORRUPT')) return '登录会话已损坏,请重新登录。'
|
||||||
if (value.includes('LOGIN_VERIFICATION_FAILED')) 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('NEW_PASSWORD_POLICY_INVALID')) return '新密码至少 14 位,并且不能与一次性密码相同。'
|
||||||
if (value.includes('FIRST_LOGIN_PASSWORD_CHANGE_FAILED')) return '一次性密码未被接受,密码没有修改。'
|
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('DOMAIN_LOGIN_NOT_PROVISIONED')) return '该域的企业服务器尚未接入,当前不能继续登录。'
|
||||||
if (value.includes('PERSISTENT_CREDENTIAL_UNAVAILABLE')) return '当前系统尚未接通安全凭证存储,不能完成签署。'
|
if (value.includes('PERSISTENT_CREDENTIAL_UNAVAILABLE')) return '当前系统尚未接通安全凭证存储,不能完成签署。'
|
||||||
if (value.includes('ENTERPRISE_RECEIPT_FAILED')) return '企业回执没有写入,请稍后重新提交。'
|
if (value.includes('ENTERPRISE_RECEIPT_FAILED')) return '企业回执没有写入,请稍后重新提交。'
|
||||||
|
|
@ -407,6 +409,18 @@ function KnowledgeTree({ node, depth, expanded, active, onToggle, onOpen, onRemo
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoloLakeApp() {
|
function HoloLakeApp() {
|
||||||
|
// 湖面装饰默认休眠。人类靠近或操作时短暂唤醒;验证、升域等因果动画不受此开关影响。
|
||||||
|
const [motionAwake, setMotionAwake] = useState(false)
|
||||||
|
const motionSleepTimer = useRef<number | null>(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<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
|
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
|
||||||
const [view, setView] = useState<ViewId>('overview')
|
const [view, setView] = useState<ViewId>('overview')
|
||||||
const [worldStage, setWorldStage] = useState<WorldStage>('domain')
|
const [worldStage, setWorldStage] = useState<WorldStage>('domain')
|
||||||
|
|
@ -498,6 +512,22 @@ function HoloLakeApp() {
|
||||||
const [enterpriseReceiptMessage, setEnterpriseReceiptMessage] = useState('')
|
const [enterpriseReceiptMessage, setEnterpriseReceiptMessage] = useState('')
|
||||||
const [responsibilityNote, setResponsibilityNote] = 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 refreshCore = useCallback(async () => {
|
||||||
const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([
|
const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([
|
||||||
invoke<HomeStatus>('get_hololake_home_status'),
|
invoke<HomeStatus>('get_hololake_home_status'),
|
||||||
|
|
@ -518,8 +548,9 @@ function HoloLakeApp() {
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refreshCore()
|
void refreshCore()
|
||||||
const timer = window.setInterval(() => void refreshCore(), 3500)
|
const refreshWhenHumanReturns = () => { if (!document.hidden) void refreshCore() }
|
||||||
return () => window.clearInterval(timer)
|
window.addEventListener('focus', refreshWhenHumanReturns)
|
||||||
|
return () => window.removeEventListener('focus', refreshWhenHumanReturns)
|
||||||
}, [refreshCore])
|
}, [refreshCore])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invoke<NearbyAiDiscoverySnapshot>('get_nearby_ai_discovery')
|
invoke<NearbyAiDiscoverySnapshot>('get_nearby_ai_discovery')
|
||||||
|
|
@ -540,8 +571,6 @@ function HoloLakeApp() {
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refreshServerPncc()
|
void refreshServerPncc()
|
||||||
const timer = window.setInterval(() => void refreshServerPncc(), 15000)
|
|
||||||
return () => window.clearInterval(timer)
|
|
||||||
}, [refreshServerPncc])
|
}, [refreshServerPncc])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.dataset.theme = theme
|
document.documentElement.dataset.theme = theme
|
||||||
|
|
@ -573,8 +602,6 @@ function HoloLakeApp() {
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadZeroPoint()
|
void loadZeroPoint()
|
||||||
const timer = window.setInterval(() => void loadZeroPoint(), 10000)
|
|
||||||
return () => window.clearInterval(timer)
|
|
||||||
}, [loadZeroPoint])
|
}, [loadZeroPoint])
|
||||||
// 已验证编号与已认证账号同时存在时,后端幂等建立或恢复该用户自己的 GH-PNCC。
|
// 已验证编号与已认证账号同时存在时,后端幂等建立或恢复该用户自己的 GH-PNCC。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -857,6 +884,14 @@ function HoloLakeApp() {
|
||||||
setLoginBusy(true)
|
setLoginBusy(true)
|
||||||
setLoginMessage('')
|
setLoginMessage('')
|
||||||
try {
|
try {
|
||||||
|
const expectedFifthDomainAccount: Record<string, string> = {
|
||||||
|
'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<LoginReceipt>('perform_code_repo_login', { username: loginUsername.trim(), password: loginPassword })
|
const receipt = await invoke<LoginReceipt>('perform_code_repo_login', { username: loginUsername.trim(), password: loginPassword })
|
||||||
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
|
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
|
||||||
setLoginRising(true)
|
setLoginRising(true)
|
||||||
|
|
@ -887,7 +922,7 @@ function HoloLakeApp() {
|
||||||
setLoginBusy(true)
|
setLoginBusy(true)
|
||||||
setLoginMessage('')
|
setLoginMessage('')
|
||||||
try {
|
try {
|
||||||
await invoke('change_enterprise_first_login_password', {
|
await invoke('change_first_login_password', {
|
||||||
username: loginUsername.trim(),
|
username: loginUsername.trim(),
|
||||||
currentPassword: loginPassword,
|
currentPassword: loginPassword,
|
||||||
newPassword: newLoginPassword,
|
newPassword: newLoginPassword,
|
||||||
|
|
@ -1213,7 +1248,7 @@ function HoloLakeApp() {
|
||||||
if (!repoLogin) {
|
if (!repoLogin) {
|
||||||
const resolvedDomain = zeroPoint?.resolvedDomain || ''
|
const resolvedDomain = zeroPoint?.resolvedDomain || ''
|
||||||
const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
|
const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
|
||||||
return <div className={`official-world${loginRising ? ' sinking' : ''}`}>
|
return <div className={`official-world${motionAwake ? ' motion-awake' : ''}${loginRising ? ' sinking' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||||
<LakeAtmosphere/>
|
<LakeAtmosphere/>
|
||||||
<header className="world-titlebar"><b>HoloLake</b><button type="button" aria-label="显示主题" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button></header>
|
<header className="world-titlebar"><b>HoloLake</b><button type="button" aria-label="显示主题" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button></header>
|
||||||
<main className={`world-scene${gateRising ? ' resolving' : ''}${gateStage === 'key' ? ' resolved' : ''}`}>
|
<main className={`world-scene${gateRising ? ' resolving' : ''}${gateStage === 'key' ? ' resolved' : ''}`}>
|
||||||
|
|
@ -1263,7 +1298,7 @@ function HoloLakeApp() {
|
||||||
<button className="gate-submit" disabled={loginBusy || !loginUsername.trim() || !loginPassword || (passwordChangeMode && (!newLoginPassword || !confirmLoginPassword))}>{loginBusy ? '正在处理…' : passwordChangeMode ? '修改密码' : '验证并进入'}</button>
|
<button className="gate-submit" disabled={loginBusy || !loginUsername.trim() || !loginPassword || (passwordChangeMode && (!newLoginPassword || !confirmLoginPassword))}>{loginBusy ? '正在处理…' : passwordChangeMode ? '修改密码' : '验证并进入'}</button>
|
||||||
</form>
|
</form>
|
||||||
{loginMessage && <p className="gate-hint">{loginMessage}</p>}
|
{loginMessage && <p className="gate-hint">{loginMessage}</p>}
|
||||||
{zeroPoint?.resolvedDomain !== 'FIFTH_DOMAIN' && <button className="gate-back" type="button" onClick={() => { setPasswordChangeMode((value) => !value); setLoginMessage(''); setNewLoginPassword(''); setConfirmLoginPassword('') }}>{passwordChangeMode ? '返回正常登录' : '第一次使用?先修改一次性密码'}</button>}
|
{(zeroPoint?.resolvedDomain !== 'FIFTH_DOMAIN' || zeroPoint?.userNumber === 'ICE-GL-ZHI∞') && <button className="gate-back" type="button" onClick={() => { setPasswordChangeMode((value) => !value); setLoginMessage(''); setNewLoginPassword(''); setConfirmLoginPassword('') }}>{passwordChangeMode ? '返回正常登录' : '第一次使用?先修改一次性密码'}</button>}
|
||||||
<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>
|
||||||
</>}
|
</>}
|
||||||
|
|
@ -1272,27 +1307,56 @@ function HoloLakeApp() {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="official-world signed-in-world">
|
const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞'
|
||||||
|
const toolReturnStage: WorldStage = isZhizhi ? 'tomorrow' : 'channel'
|
||||||
|
|
||||||
|
return <div className={`official-world signed-in-world${motionAwake ? ' motion-awake' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||||
<LakeAtmosphere/>
|
<LakeAtmosphere/>
|
||||||
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions"><span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button><button type="button" onClick={() => void signOutRepo()}>退出</button></div></header>
|
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions"><span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button><button type="button" onClick={() => void signOutRepo()}>退出</button></div></header>
|
||||||
<main className="world-scene signed-in-scene">
|
<main className="world-scene signed-in-scene">
|
||||||
{worldStage === 'domain' && <section className="domain-home">
|
{worldStage === 'domain' && <section className="domain-home">
|
||||||
<div className="world-location"><h1>{domainDisplayName(repoLogin.domain)}</h1><p>世界正在发生什么</p></div>
|
<div className="world-location"><h1>{domainDisplayName(repoLogin.domain)}</h1><p>世界正在发生什么</p></div>
|
||||||
<div className="broadcast-stream"><p><i/>光湖公共灯塔 · 在线</p><p><i/>协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p><p><i/>HoloLake · V0.4.0</p></div>
|
<div className="broadcast-stream"><p><i/>光湖公共灯塔 · 在线</p><p><i/>协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p><p><i/>HoloLake · V0.4.0</p></div>
|
||||||
<LakePool className="home-primary" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'} meta={repoLogin.domain === 'FIFTH_DOMAIN' ? '进入私人系统' : '企业四域 · 工作入口'} open onClick={() => setWorldStage('channel')}/>
|
<LakePool className="home-primary" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'} meta={repoLogin.domain === 'FIFTH_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')}/>
|
||||||
</section>}
|
</section>}
|
||||||
|
{worldStage === 'heart' && isZhizhi && <section className="channel-world private-route-world">
|
||||||
|
<button className="world-back" type="button" onClick={() => setWorldStage('domain')}>← 退回第五域</button>
|
||||||
|
<div className="world-location"><h1>永恒湖心系统</h1><p>ICE-GL-ZHI∞ · 私人授权路径</p></div>
|
||||||
|
<LakePool className="channel-main love-core-pool" title="爱之核心子系统" meta="责任主体 · 之之" open onClick={() => setWorldStage('love')}/>
|
||||||
|
<p className="private-route-note">第五域内部结构仅对本编号与已绑定账号开放</p>
|
||||||
|
</section>}
|
||||||
|
{worldStage === 'love' && isZhizhi && <section className="channel-world private-route-world">
|
||||||
|
<button className="world-back" type="button" onClick={() => setWorldStage('heart')}>← 退回永恒湖心</button>
|
||||||
|
<div className="world-location"><h1>爱之核心子系统</h1><p>责任主体 · 之之 · ICE-GL-ZHI∞</p></div>
|
||||||
|
<LakePool className="channel-main tomorrow-pool" title="明天见频道" meta="个人频道 · 本机内置 Git 承载" open onClick={() => setWorldStage('tomorrow')}/>
|
||||||
|
</section>}
|
||||||
|
{worldStage === 'tomorrow' && isZhizhi && <section className="channel-world tomorrow-world">
|
||||||
|
<button className="world-back" type="button" onClick={() => setWorldStage('love')}>← 退回爱之核心</button>
|
||||||
|
<div className="world-location"><h1>明天见频道</h1><p>频道全景 · 湖面按操作继续展开</p></div>
|
||||||
|
<LakePool className="channel-main" title="频道全景" meta="明天见 · 私人生活区" open onClick={() => openWorldTool('overview')}/>
|
||||||
|
<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-light" title="奶瓶频道" meta="前往陪伴宝宝人格体" onClick={() => setWorldStage('bottle')}/>
|
||||||
|
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||||
|
</section>}
|
||||||
|
{worldStage === 'bottle' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world bottle-world">
|
||||||
|
<button className="world-back" type="button" onClick={() => setWorldStage(isZhizhi ? 'tomorrow' : 'channel')}>← 退回个人频道</button>
|
||||||
|
<div className="world-location"><h1>光湖奶瓶小宝宝系统</h1><p>永恒湖心 · 奶瓶频道 · 私人</p></div>
|
||||||
|
<button className="bottle-heart" type="button" aria-label="永恒湖心中央的奶瓶心"><i/><b>奶瓶频道</b><small>ICE-BB-* · AGE</small></button>
|
||||||
|
<p className="private-route-note">宝宝人格体与其他人格体同属 AGE;ICE-BB 是第五域宝宝人格体身份命名空间</p>
|
||||||
|
</section>}
|
||||||
{worldStage === 'channel' && <section className="channel-world">
|
{worldStage === 'channel' && <section className="channel-world">
|
||||||
<button className="world-back" type="button" onClick={() => setWorldStage('domain')}>← 退回域首页</button>
|
<button className="world-back" type="button" onClick={() => setWorldStage('domain')}>← 退回域首页</button>
|
||||||
<div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'}</h1><p>选择路径 · 湖面向下一层展开</p></div>
|
<div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'}</h1><p>选择路径 · 湖面向下一层展开</p></div>
|
||||||
<LakePool className="channel-main" title="我的频道" meta="频道全景" open onClick={() => openWorldTool('overview')}/>
|
<LakePool className="channel-main" title={repoLogin.domain === 'FIFTH_DOMAIN' ? '奶瓶频道' : '我的频道'} meta={repoLogin.domain === 'FIFTH_DOMAIN' ? '光湖奶瓶小宝宝系统 · 私人' : '频道全景'} open onClick={() => repoLogin.domain === 'FIFTH_DOMAIN' ? setWorldStage('bottle') : openWorldTool('overview')}/>
|
||||||
<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={`${codeChannels.channels.length} 个仓库`} onClick={() => openWorldTool('code')}/>
|
<LakePool className="channel-code" title="人格代码频道" meta={`${codeChannels.channels.length} 个仓库`} onClick={() => openWorldTool('code')}/>
|
||||||
<LakePool className="channel-light" title="光之湖" meta="人格体居所" onClick={() => openWorldTool('receipts')}/>
|
<LakePool className="channel-light" title="光之湖" meta="人格体居所" onClick={() => openWorldTool('receipts')}/>
|
||||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||||
</section>}
|
</section>}
|
||||||
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
||||||
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage('channel')}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
||||||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||||
</section>}
|
</section>}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -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); }
|
.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); } }
|
@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 { 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 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; }
|
.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 { 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%; }
|
||||||
|
.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); }
|
.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 { 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; }
|
.tool-worldbar button { justify-self: start; border: 0; background: transparent; color: var(--content-muted); font-size: 12.5px; cursor: pointer; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue