feat: separate enterprise work and personal routes

This commit is contained in:
冰朔 2026-08-17 00:16:24 +08:00
commit 7414810862
9 changed files with 546 additions and 41 deletions

View file

@ -19,6 +19,8 @@ API 路由对客户端开放:
- 编号解析:把 TCS-GL 人类编号路由到工作域、企业账号和私有仓库;
- 关系确认:由人类确认自己与人格体的认领关系;
- 责任回执:独立记录对域责任的接受、拒绝、延期或修改后接受;
- 回执入仓:关系与责任签名回执使用提交者自己的 Forgejo 会话写入本人私有工作仓库的
`.guanghu/receipts/`,稳定路径与读回校验保证重试不重复;服务不持有管理员仓库令牌;
- 仓库验证:登录凭证只透传给同机 Forgejo 验证,不写入数据库或日志。
- 首次换密:使用用户自己的一次性凭证进入 Forgejo 强制换密会话,不持有长期管理员令牌;
换密回执只记录账号、时间和成功状态,不记录旧密码或新密码。

View file

@ -39,6 +39,9 @@ FORGEJO_USER_API = os.environ.get(
FORGEJO_WEB_BASE = os.environ.get(
"GH_ENTERPRISE_FORGEJO_WEB_BASE", "https://guanghu.chat/code"
).rstrip("/")
FORGEJO_API_BASE = os.environ.get(
"GH_ENTERPRISE_FORGEJO_API_BASE", "http://127.0.0.1:3341/api/v1"
).rstrip("/")
RECEIPT_KEY = os.environ.get("GH_ENTERPRISE_RECEIPT_KEY", "")
MAX_BODY = 16_384
USERNAME = re.compile(r"^[A-Za-z0-9_-]{1,40}$")
@ -210,6 +213,90 @@ def signed_receipt(payload: dict) -> dict:
return {**payload, "receipt_hash": receipt_hash, "receipt_signature": signature}
def stable_receipt_id(prefix: str, human_number: str, idempotency_key: str) -> str:
"""Keep one receipt path across safe client retries without exposing the key."""
material = f"{prefix}\n{human_number}\n{idempotency_key}".encode()
digest = hmac.new(RECEIPT_KEY.encode(), material, hashlib.sha256).hexdigest()[:32]
return f"{prefix}-{digest.upper()}"
def repository_receipt_path(kind: str, receipt_id: str) -> str:
if kind not in {"relationship", "responsibility"} or not re.fullmatch(
r"GH-(?:REL|RESP)-[A-F0-9]{32}", receipt_id
):
raise ValueError("repository receipt path input invalid")
return f".guanghu/receipts/{kind}/{receipt_id}.json"
def project_receipt_to_repository(
human: dict, username: str, password: str, kind: str, receipt: dict
) -> dict:
"""Commit a signed receipt with the human's own Forgejo authority.
No administrator token or server-side repository credential is held. A
retry that finds the deterministic path already present must read back the
exact bytes before treating the projection as idempotent.
"""
repository = str(human["repository"])
if repository.split("/", 1)[0] != username:
raise RuntimeError("repository owner does not match authenticated user")
path = repository_receipt_path(kind, str(receipt["receipt_id"]))
endpoint = (
f"{FORGEJO_API_BASE}/repos/{urllib.parse.quote(repository, safe='/')}"
f"/contents/{urllib.parse.quote(path, safe='/')}"
)
content = canonical(receipt) + b"\n"
authorization = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
create_body = canonical(
{
"branch": "main",
"content": base64.b64encode(content).decode(),
"message": f"receipt({kind}): {receipt['receipt_id']}",
}
)
request = urllib.request.Request(endpoint, data=create_body, method="POST")
request.add_header("Authorization", authorization)
request.add_header("Accept", "application/json")
request.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if response.status != 201:
raise RuntimeError(f"repository projection returned {response.status}")
commit = str(result.get("commit", {}).get("sha", ""))
if not re.fullmatch(r"[0-9a-f]{40,64}", commit):
raise RuntimeError("repository projection commit missing")
return {
"state": "COMMITTED",
"repository": repository,
"path": path,
"commit": commit,
}
except urllib.error.HTTPError as error:
if error.code != 422:
raise RuntimeError(f"repository projection failed: {error.code}") from error
read_request = urllib.request.Request(endpoint + "?ref=main")
read_request.add_header("Authorization", authorization)
read_request.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(read_request, timeout=15) as response:
existing = json.load(response)
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError) as error:
raise RuntimeError("repository projection readback failed") from error
try:
existing_content = base64.b64decode(str(existing["content"]), validate=True)
except (KeyError, ValueError) as error:
raise RuntimeError("repository projection readback invalid") from error
if not hmac.compare_digest(existing_content, content):
raise RuntimeError("repository receipt path already contains different bytes")
return {
"state": "IDEMPOTENT_READBACK",
"repository": repository,
"path": path,
"commit": str(existing.get("sha", "")),
}
def public_projection(registry: dict, human: dict, db: sqlite3.Connection | None = None) -> dict:
projection = {
"status": "RESOLVED",
@ -273,7 +360,7 @@ class Handler(BaseHTTPRequestHandler):
raise ValueError("JSON object required")
return value
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str] | None:
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str, str] | None:
credentials = parse_basic(self.headers.get("Authorization", ""))
number = str(payload.get("human_number", ""))
human = find_human(registry, number)
@ -282,7 +369,7 @@ class Handler(BaseHTTPRequestHandler):
username, password = credentials
if not hmac.compare_digest(username, human["username"]) or not verify_forgejo(username, password):
return None
return human, username
return human, username, password
def do_GET(self) -> None:
try:
@ -349,7 +436,7 @@ class Handler(BaseHTTPRequestHandler):
authenticated = self.authenticated_human(registry, payload)
if not authenticated:
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
human, username = authenticated
human, username, password = authenticated
idempotency_key = str(payload.get("idempotency_key", ""))
if not re.fullmatch(r"[A-Za-z0-9_-]{16,96}", idempotency_key):
return self.respond(400, {"ok": False, "error": "valid idempotency_key required"})
@ -363,12 +450,16 @@ class Handler(BaseHTTPRequestHandler):
if existing:
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
observed = now()
receipt_id = "GH-REL-" + uuid.uuid4().hex.upper()
receipt_id = stable_receipt_id("GH-REL", human["human_number"], idempotency_key)
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"registry_version":registry["version"],"decision":decision,"observed_at":observed})
try:
projection = project_receipt_to_repository(human, username, password, "relationship", receipt)
except RuntimeError as error:
return self.respond(503, {"ok": False, "error": str(error)})
db.execute("INSERT INTO relationship_receipts VALUES (?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,registry["version"],decision,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RELATIONSHIP_CONFIRMATION",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
db.commit()
return self.respond(201, {"ok": True, "receipt": receipt})
return self.respond(201, {"ok": True, "receipt": receipt, "repository_projection": projection})
if self.path == "/v1/responsibility-receipts":
decision = str(payload.get("decision", ""))
note = str(payload.get("note", ""))[:1000]
@ -379,12 +470,16 @@ class Handler(BaseHTTPRequestHandler):
if existing:
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
observed = now()
receipt_id = "GH-RESP-" + uuid.uuid4().hex.upper()
receipt_id = stable_receipt_id("GH-RESP", human["human_number"], idempotency_key)
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"responsibility_domain":human["responsibility_domain"],"responsibility_version":version,"decision":decision,"note":note,"observed_at":observed})
try:
projection = project_receipt_to_repository(human, username, password, "responsibility", receipt)
except RuntimeError as error:
return self.respond(503, {"ok": False, "error": str(error)})
db.execute("INSERT INTO responsibility_receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,human["responsibility_domain"],version,decision,note,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RESPONSIBILITY_RECEIPT",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
db.commit()
return self.respond(201, {"ok": True, "receipt": receipt})
return self.respond(201, {"ok": True, "receipt": receipt, "repository_projection": projection})
if self.path == "/v1/me/entry":
return self.respond(200, {"ok": True, "entry": public_projection(registry, human, db)})
return self.respond(404, {"ok": False, "error": "not found"})

View file

@ -3,7 +3,9 @@ import json
import os
import tempfile
import unittest
import urllib.error
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).parent
REGISTRY = ROOT / "registry" / "enterprise-identity-registry.json"
@ -60,6 +62,53 @@ class EnterpriseIdentityTests(unittest.TestCase):
self.assertIn("/user/settings/change_password", source)
self.assertNotIn("FORGEJO_ADMIN_TOKEN", source)
def test_receipt_id_and_repository_path_are_stable_without_exposing_idempotency_key(self):
first = service.stable_receipt_id("GH-RESP", "TCS-GL-0007∞", "responsibility-1234567890")
second = service.stable_receipt_id("GH-RESP", "TCS-GL-0007∞", "responsibility-1234567890")
self.assertEqual(first, second)
self.assertRegex(first, r"^GH-RESP-[A-F0-9]{32}$")
self.assertNotIn("1234567890", first)
self.assertEqual(
service.repository_receipt_path("responsibility", first),
f".guanghu/receipts/responsibility/{first}.json",
)
def test_repository_projection_uses_the_humans_own_forgejo_authority(self):
registry = service.load_registry()
human = service.find_human(registry, "TCS-GL-0007∞")
receipt_id = service.stable_receipt_id("GH-REL", human["human_number"], "relationship-1234567890")
receipt = service.signed_receipt(
{"receipt_id": receipt_id, "human_number": human["human_number"], "username": "feimao"}
)
class Response:
status = 201
def __enter__(self): return self
def __exit__(self, *_): return False
def read(self):
return json.dumps({"commit": {"sha": "a" * 40}}).encode()
with mock.patch.object(service.urllib.request, "urlopen", return_value=Response()) as opened:
projection = service.project_receipt_to_repository(
human, "feimao", "one-use-secret", "relationship", receipt
)
request = opened.call_args.args[0]
self.assertEqual(projection["repository"], "feimao/guanghu-zero-sense-work")
self.assertIn("/repos/feimao/guanghu-zero-sense-work/contents/", request.full_url)
self.assertTrue(request.headers["Authorization"].startswith("Basic "))
self.assertNotIn("one-use-secret", request.data.decode())
def test_repository_projection_refuses_cross_owner_repository(self):
human = {"repository": "juzi/guanghu-zero-sense-work"}
with self.assertRaisesRegex(RuntimeError, "owner"):
service.project_receipt_to_repository(
human,
"feimao",
"secret",
"relationship",
{"receipt_id": "GH-REL-" + "A" * 32},
)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,35 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import test from 'node:test'
const frontend = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8')
const native = fs.readFileSync(new URL('../src-tauri/src/enterprise_work_channel.rs', import.meta.url), 'utf8')
const pncc = fs.readFileSync(new URL('../src-tauri/src/user_pncc_channel.rs', import.meta.url), 'utf8')
test('every enterprise login enters through Zero Sense and Guanghu Channel before its responsibility domain', () => {
assert.match(frontend, /光湖零感域/)
assert.match(frontend, /公共工作入口/)
assert.match(frontend, /光湖频道/)
assert.match(frontend, /本人责任工作域/)
assert.match(frontend, /worldStage === 'enterpriseWork'/)
assert.match(native, /work_entry_domain: "ZERO_SENSE_DOMAIN"/)
assert.match(native, /work_entry_channel: "GUANGHU_CHANNEL"/)
})
test('enterprise work repositories are exact private read-only projections and never personal channels', () => {
for (const repository of [
'huaer/guanghu-branch-work',
'yeye/guanghu-zero-work',
'feimao/guanghu-zero-sense-work',
'juzi/guanghu-zero-sense-work',
'awen/guanghu-main-work',
]) assert.match(native, new RegExp(repository.replace('/', '\\/')))
assert.match(native, /AUTHENTICATED_USER_READ_ONLY_NO_PUSH_NO_PERSONAL_NODE_AUTHORITY/)
assert.match(frontend, /个人生活区不会由企业工作账号代建/)
assert.match(pncc, /if domain != "FIFTH_DOMAIN"/)
})
test('enterprise responsibility receipts report their private Git projection to the human', () => {
assert.match(frontend, /repository_projection/)
assert.match(frontend, /写入本人私有工作仓库/)
})

View file

@ -92,7 +92,7 @@ fn keychain_has(host: &str, username: &str) -> bool {
}
#[cfg(target_os = "macos")]
fn keychain_read(host: &str, username: &str) -> Result<String, String> {
pub(crate) fn keychain_read(host: &str, username: &str) -> Result<String, String> {
let output = Command::new("/usr/bin/security")
.args(["find-internet-password", "-w", "-s", host, "-a", username])
.output()
@ -124,7 +124,7 @@ fn keychain_has(_host: &str, _username: &str) -> bool {
}
#[cfg(not(target_os = "macos"))]
fn keychain_read(_host: &str, _username: &str) -> Result<String, String> {
pub(crate) fn keychain_read(_host: &str, _username: &str) -> Result<String, String> {
Err("HOLOLAKE_PERSISTENT_CREDENTIAL_UNAVAILABLE".into())
}
@ -158,7 +158,7 @@ fn expected_account_for_human_number(number: &str) -> Option<&'static str> {
}
}
fn validate_number_account_binding(number: &str, username: &str) -> Result<(), String> {
pub(crate) 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()),

View file

@ -0,0 +1,251 @@
//! 企业四域私有责任仓库的本机只读工作投影。
//!
//! 企业账号先进入零感域的光湖频道,再展开本人责任工作域。仓库凭证只从
//! 系统钥匙串进入一次 Git 子进程环境,不写入 remote URL、配置或文件。
use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tauri::{AppHandle, State};
use crate::code_channel;
use crate::code_repo_login;
use crate::zero_point::{self, ZeroPointState};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EnterpriseWorkChannelSnapshot {
pub state: &'static str,
pub work_entry_domain: &'static str,
pub work_entry_channel: &'static str,
pub responsibility_domain: String,
pub repository: String,
pub remote_repository_url: String,
pub local_path: String,
pub channel_id: String,
pub authority: &'static str,
}
#[tauri::command]
pub async fn ensure_enterprise_work_channel(
app: AppHandle,
state: State<'_, ZeroPointState>,
) -> Result<EnterpriseWorkChannelSnapshot, String> {
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_ENTERPRISE_WORK_ROUTE_REQUIRED".into());
}
let session = code_repo_login::current_login_session_for_domain(&app, &domain)?
.ok_or_else(|| "HOLOLAKE_LOGIN_SESSION_REQUIRED".to_string())?;
code_repo_login::validate_number_account_binding(&human_number, &session.username)?;
let (repository, registered_domain) = enterprise_repository(&human_number)?;
if registered_domain != domain {
return Err("HOLOLAKE_ENTERPRISE_WORK_DOMAIN_MISMATCH".into());
}
let password = code_repo_login::keychain_read(&session.host, &session.username)?;
let root =
crate::authenticated_storage::account_storage_root(&app, "enterprise-work-channel-v1")?;
let remote = format!("https://guanghu.chat/code/{repository}.git");
let local = root.join("repository");
let username = session.username.clone();
let repository_owned = repository.to_string();
let remote_owned = remote.clone();
let local_for_sync = local.clone();
tauri::async_runtime::spawn_blocking(move || {
sync_repository_at(&root, &local_for_sync, &remote_owned, &username, &password)
})
.await
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_JOIN_FAILED: {error}"))??;
let entry = code_channel::register_managed_repository(
&app,
&local,
format!("{} · 企业责任工作仓库", display_domain(&domain)),
)?;
Ok(EnterpriseWorkChannelSnapshot {
state: "READY_READ_ONLY_WORK_PROJECTION",
work_entry_domain: "ZERO_SENSE_DOMAIN",
work_entry_channel: "GUANGHU_CHANNEL",
responsibility_domain: domain,
repository: repository_owned,
remote_repository_url: remote,
local_path: local.to_string_lossy().into_owned(),
channel_id: entry.channel_id,
authority: "AUTHENTICATED_USER_READ_ONLY_NO_PUSH_NO_PERSONAL_NODE_AUTHORITY",
})
}
fn enterprise_repository(number: &str) -> Result<(&'static str, &'static str), String> {
match number {
"TCS-GL-0005∞" => Ok(("huaer/guanghu-branch-work", "BRANCH_DOMAIN")),
"TCS-GL-0006∞" => Ok(("yeye/guanghu-zero-work", "ZERO_DOMAIN")),
"TCS-GL-0007∞" => Ok(("feimao/guanghu-zero-sense-work", "ZERO_SENSE_DOMAIN")),
"TCS-GL-0008∞" => Ok(("juzi/guanghu-zero-sense-work", "ZERO_SENSE_DOMAIN")),
"TCS-GL-0016∞" => Ok(("awen/guanghu-main-work", "MAIN_DOMAIN")),
_ => Err("HOLOLAKE_ENTERPRISE_WORK_BINDING_UNREGISTERED".into()),
}
}
fn display_domain(domain: &str) -> &'static str {
match domain {
"MAIN_DOMAIN" => "光湖主域",
"BRANCH_DOMAIN" => "光湖分域",
"ZERO_DOMAIN" => "光湖零域",
"ZERO_SENSE_DOMAIN" => "光湖零感域",
_ => "企业责任域",
}
}
fn sync_repository_at(
root: &Path,
repository: &Path,
remote: &str,
username: &str,
password: &str,
) -> Result<(), String> {
fs::create_dir_all(root)
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_STORAGE_FAILED: {error}"))?;
let askpass = write_askpass(root)?;
if repository.exists() {
let actual = git_output(
repository,
&askpass,
username,
password,
&["config", "--get", "remote.origin.url"],
)?;
if actual.trim() != remote {
return Err("HOLOLAKE_ENTERPRISE_WORK_REMOTE_MISMATCH".into());
}
git(
repository,
&askpass,
username,
password,
&["fetch", "--depth", "1", "origin", "main"],
)?;
git(
repository,
&askpass,
username,
password,
&["reset", "--hard", "FETCH_HEAD"],
)?;
git(repository, &askpass, username, password, &["clean", "-fd"])?;
} else {
let destination = repository.to_string_lossy().into_owned();
git(
root,
&askpass,
username,
password,
&[
"clone",
"--depth",
"1",
"--branch",
"main",
remote,
&destination,
],
)?;
}
Ok(())
}
fn write_askpass(root: &Path) -> Result<PathBuf, String> {
let path = root.join("git-askpass.sh");
let body = "#!/bin/sh\ncase \"$1\" in\n *Username*) printf '%s' \"$HOLOLAKE_GIT_USERNAME\" ;;\n *Password*) printf '%s' \"$HOLOLAKE_GIT_PASSWORD\" ;;\n *) exit 1 ;;\nesac\n";
fs::write(&path, body)
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_ASKPASS_FAILED: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&path, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_ASKPASS_FAILED: {error}"))?;
}
Ok(path)
}
fn git(
cwd: &Path,
askpass: &Path,
username: &str,
password: &str,
args: &[&str],
) -> Result<(), String> {
let output = git_command(cwd, askpass, username, password, args)
.output()
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED: {error}"))?;
if output.status.success() {
Ok(())
} else {
Err("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED".into())
}
}
fn git_output(
cwd: &Path,
askpass: &Path,
username: &str,
password: &str,
args: &[&str],
) -> Result<String, String> {
let output = git_command(cwd, askpass, username, password, args)
.output()
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED: {error}"))?;
if !output.status.success() {
return Err("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED".into());
}
String::from_utf8(output.stdout)
.map_err(|_| "HOLOLAKE_ENTERPRISE_WORK_GIT_OUTPUT_INVALID".into())
}
fn git_command(
cwd: &Path,
askpass: &Path,
username: &str,
password: &str,
args: &[&str],
) -> Command {
let mut command = Command::new("git");
command
.current_dir(cwd)
.args(["-c", "credential.helper="])
.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", askpass)
.env("HOLOLAKE_GIT_USERNAME", username)
.env("HOLOLAKE_GIT_PASSWORD", password);
command
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn every_enterprise_number_has_one_exact_private_repository() {
assert_eq!(
enterprise_repository("TCS-GL-0007∞").unwrap().0,
"feimao/guanghu-zero-sense-work"
);
assert_eq!(
enterprise_repository("TCS-GL-0008∞").unwrap().0,
"juzi/guanghu-zero-sense-work"
);
assert!(enterprise_repository("ICE-GL∞").is_err());
}
#[test]
fn askpass_script_contains_no_secret() {
let root = tempdir().unwrap();
let path = write_askpass(root.path()).unwrap();
let body = fs::read_to_string(path).unwrap();
assert!(body.contains("HOLOLAKE_GIT_PASSWORD"));
assert!(!body.contains("temporary-secret"));
}
}

View file

@ -5,6 +5,7 @@ mod code_repo_login;
mod direct_local_broker;
mod direct_local_session;
mod dynamic_capability_routing;
mod enterprise_work_channel;
mod glp_envelope;
mod home_status;
mod knowledge_base;
@ -74,6 +75,7 @@ pub fn run() {
code_repo_login::get_enterprise_entry,
code_repo_login::confirm_enterprise_persona_relationship,
code_repo_login::submit_enterprise_responsibility_receipt,
enterprise_work_channel::ensure_enterprise_work_channel,
code_repo_login::sign_out_code_repo_login,
user_pncc_channel::get_user_pncc_channel,
user_pncc_channel::ensure_user_pncc_channel,

View file

@ -104,6 +104,11 @@ fn trusted_subject(
let Some((number, domain)) = zero_point::verified_user_route(state)? else {
return Ok(None);
};
// 企业账号的代码入口是企业责任仓库;不能自动创建一个本机个人仓库
// 来冒充工作域或绕过之后的个人节点拥有权验证。
if domain != "FIFTH_DOMAIN" {
return Ok(None);
}
let Some(session) = code_repo_login::current_login_session_for_domain(app, &domain)? else {
return Ok(None);
};

View file

@ -49,7 +49,7 @@ import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system'
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'tool'
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'tool'
type KnowledgeSource = 'native' | 'legacy'
interface HomeStatus {
@ -239,6 +239,27 @@ interface EnterpriseEntry {
responsibility_receipt?: { decision: string; observed_at: string; receipt_hash: string; responsibility_version: string } | null
}
interface EnterpriseEntryEnvelope { ok: boolean; entry: EnterpriseEntry }
interface EnterpriseWorkChannelSnapshot {
state: string
workEntryDomain: string
workEntryChannel: string
responsibilityDomain: string
repository: string
remoteRepositoryUrl: string
localPath: string
channelId: string
authority: string
}
interface EnterpriseReceiptProjection {
state: string
repository: string
path: string
commit: string
}
interface EnterpriseReceiptEnvelope {
ok: boolean
repository_projection?: EnterpriseReceiptProjection
}
const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'UNPROVISIONED_FAIL_CLOSED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY' }
const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } }
@ -461,6 +482,7 @@ function HoloLakeApp() {
const [releaseCandidate, setReleaseCandidate] = useState<ReleaseCandidate | null>(null)
const [repoLogin, setRepoLogin] = useState<LoginSession | null>(null)
const [userPncc, setUserPncc] = useState<UserPnccChannelSnapshot | null>(null)
const [enterpriseWork, setEnterpriseWork] = useState<EnterpriseWorkChannelSnapshot | null>(null)
const [userPnccBusy, setUserPnccBusy] = useState(false)
const [userPnccMessage, setUserPnccMessage] = useState('')
const [loginUsername, setLoginUsername] = useState('')
@ -604,22 +626,32 @@ function HoloLakeApp() {
useEffect(() => {
void loadZeroPoint()
}, [loadZeroPoint])
// 已验证编号与已认证账号同时存在时,后端幂等建立或恢复该用户自己的 GH-PNCC。
// 第五域恢复本人的本机私人频道;企业成员只同步其唯一私有责任工作仓库。
// 两条路径不能互相冒充:企业工作账号不自动取得个人生活区或个人节点权限。
useEffect(() => {
if (!repoLogin || zeroPoint?.route !== 'verified') {
setUserPncc(null)
setEnterpriseWork(null)
return
}
let active = true
setUserPnccBusy(true)
setUserPnccMessage('')
invoke<UserPnccChannelSnapshot>('ensure_user_pncc_channel')
.then((snapshot) => {
if (!active) return
setUserPncc(snapshot)
setUserPnccMessage('本机原生代码频道已就绪。')
void refreshCore()
})
const channelReady = repoLogin.domain === 'FIFTH_DOMAIN'
? invoke<UserPnccChannelSnapshot>('ensure_user_pncc_channel').then((snapshot) => {
if (!active) return
setEnterpriseWork(null)
setUserPncc(snapshot)
setUserPnccMessage('本机原生私人频道已就绪。')
})
: invoke<EnterpriseWorkChannelSnapshot>('ensure_enterprise_work_channel').then((snapshot) => {
if (!active) return
setUserPncc(null)
setEnterpriseWork(snapshot)
setUserPnccMessage('本人私有责任工作仓库已接入。')
})
channelReady
.then(() => { if (active) void refreshCore() })
.catch((error) => { if (active) setUserPnccMessage(humanError(error, 'code')) })
.finally(() => { if (active) setUserPnccBusy(false) })
return () => { active = false }
@ -947,6 +979,7 @@ function HoloLakeApp() {
setActiveCodeFile(null)
setReceipts([])
setEnterpriseEntry(null)
setEnterpriseWork(null)
setEnterpriseReceiptMessage('')
setWorldStage('domain')
setRepoLogin(null)
@ -955,12 +988,15 @@ function HoloLakeApp() {
setEnterpriseReceiptBusy(true)
setEnterpriseReceiptMessage('')
try {
await invoke('confirm_enterprise_persona_relationship', {
const receipt = await invoke<EnterpriseReceiptEnvelope>('confirm_enterprise_persona_relationship', {
decision,
idempotencyKey: `relationship-${crypto.randomUUID().replaceAll('-', '')}`,
})
await loadEnterpriseEntry()
setEnterpriseReceiptMessage(decision === 'CONFIRM' ? '人格关系确认已签署并留存。' : '人格关系异议已签署并留存。')
const projected = receipt.repository_projection?.state === 'COMMITTED' || receipt.repository_projection?.state === 'IDEMPOTENT_READBACK'
setEnterpriseReceiptMessage(decision === 'CONFIRM'
? projected ? '人格关系确认已签署,并写入本人私有工作仓库。' : '人格关系确认已签署并留存。'
: projected ? '人格关系异议已签署,并写入本人私有工作仓库。' : '人格关系异议已签署并留存。')
} catch (error) {
setEnterpriseReceiptMessage(humanError(error, 'login'))
} finally {
@ -972,7 +1008,7 @@ function HoloLakeApp() {
setEnterpriseReceiptBusy(true)
setEnterpriseReceiptMessage('')
try {
await invoke('submit_enterprise_responsibility_receipt', {
const receipt = await invoke<EnterpriseReceiptEnvelope>('submit_enterprise_responsibility_receipt', {
decision,
note: responsibilityNote.trim(),
responsibilityVersion: enterpriseEntry.registry_version,
@ -980,7 +1016,10 @@ function HoloLakeApp() {
})
setResponsibilityNote('')
await loadEnterpriseEntry()
setEnterpriseReceiptMessage(decision === 'ACCEPT' ? '域责任已由本人接受并签署。' : '域责任拒绝回执已留存。')
const projected = receipt.repository_projection?.state === 'COMMITTED' || receipt.repository_projection?.state === 'IDEMPOTENT_READBACK'
setEnterpriseReceiptMessage(decision === 'ACCEPT'
? projected ? '域责任已由本人接受、签署,并写入本人私有工作仓库。' : '域责任已由本人接受并签署。'
: projected ? '域责任拒绝回执已签署,并写入本人私有工作仓库。' : '域责任拒绝回执已留存。')
} catch (error) {
setEnterpriseReceiptMessage(humanError(error, 'login'))
} finally {
@ -1153,19 +1192,20 @@ function HoloLakeApp() {
const renderCode = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
<header><div><span className="kicker">GH-PNCC</span><h1></h1></div></header>
<header><div><span className="kicker">{repoLogin?.domain === 'FIFTH_DOMAIN' ? 'GH-PNCC' : 'ENTERPRISE WORK REPOSITORY'}</span><h1>{repoLogin?.domain === 'FIFTH_DOMAIN' ? '人格原生代码频道' : '私有责任工作仓库'}</h1></div></header>
<section className="pncc-native-card">
<div className="pncc-native-card-title"><span className={userPncc ? 'identity-dot ready' : 'identity-dot'}/><div><b>{userPncc ? `${userPncc.accountUsername} · GH-PNCC` : '正在建立用户频道'}</b><small>{userPnccBusy ? '正在初始化本机 Git…' : userPncc ? `${userPncc.branch} · ${userPncc.gitHead.slice(0, 8)}` : '等待可信绑定'}</small></div></div>
{userPncc ? <dl><div><dt></dt><dd>{domainDisplayName(userPncc.domain)}</dd></div><div><dt></dt><dd>{userPncc.userNumber}</dd></div><div><dt></dt><dd>Git</dd></div><div><dt></dt><dd>HoloLake </dd></div><div><dt>Forgejo </dt><dd> · </dd></div></dl> : <p>{userPnccMessage || '系统先按编号确定所属域,再通过该域的账号与节点入口建立频道。'}</p>}
<div className="pncc-native-card-title"><span className={userPncc || enterpriseWork ? 'identity-dot ready' : 'identity-dot'}/><div><b>{repoLogin?.domain === 'FIFTH_DOMAIN' ? userPncc ? `${userPncc.accountUsername} · GH-PNCC` : '正在建立用户频道' : enterpriseWork ? `${repoLogin?.username} · 私有工作仓库` : '正在认证工作仓库'}</b><small>{userPnccBusy ? '正在同步 Git…' : userPncc ? `${userPncc.branch} · ${userPncc.gitHead.slice(0, 8)}` : enterpriseWork ? 'main · 认证只读投影' : '等待可信绑定'}</small></div></div>
{repoLogin?.domain === 'FIFTH_DOMAIN' && userPncc ? <dl><div><dt></dt><dd>{domainDisplayName(userPncc.domain)}</dd></div><div><dt></dt><dd>{userPncc.userNumber}</dd></div><div><dt></dt><dd>Git</dd></div><div><dt></dt><dd>HoloLake </dd></div><div><dt>Forgejo </dt><dd> · </dd></div></dl> : repoLogin?.domain !== 'FIFTH_DOMAIN' && enterpriseWork ? <dl><div><dt></dt><dd> · </dd></div><div><dt></dt><dd>{domainDisplayName(enterpriseWork.responsibilityDomain)}</dd></div><div><dt></dt><dd>{enterpriseWork.repository}</dd></div><div><dt>访</dt><dd> · · </dd></div><div><dt></dt><dd></dd></div></dl> : <p>{userPnccMessage || '系统先按编号确定所属域,再通过该域的账号与节点入口建立频道。'}</p>}
{userPncc && <button type="button" onClick={() => { const channel = codeChannels.channels.find((item) => item.channelId === userPncc.channelId); if (channel) void browseChannel(channel) }}></button>}
{enterpriseWork && <button type="button" onClick={() => void openEnterpriseRepository()}></button>}
</section>
<div className="channel-section-label"><b></b><small> Git</small></div>
{repoLogin?.domain === 'FIFTH_DOMAIN' && <><div className="channel-section-label"><b></b><small> Git</small></div>
<form className="clone-form" onSubmit={(event) => void cloneCodeChannel(event)}>
<label htmlFor="clone-url"></label><div><input id="clone-url" type="url" value={cloneUrl} placeholder="https://guanghulab.com/code/…" onChange={(event) => setCloneUrl(event.target.value)}/><button disabled={!cloneUrl.trim() || codeBusy}></button></div>
</form>
<button className="local-folder-button" type="button" onClick={() => void selectLocalCodeChannel()}><Icon name="folder"/> Git </button>
<button className="local-folder-button" type="button" onClick={() => void selectLocalCodeChannel()}><Icon name="folder"/> Git </button></>}
<div className="channel-selector">{codeChannels.channels.map((channel) => <button className={activeChannel?.channelId === channel.channelId ? 'active' : ''} key={channel.channelId} type="button" onClick={() => void browseChannel(channel)}><Icon name="code"/><span><b>{channel.name}</b><small>{channel.branch} · {channel.gitHead.slice(0, 8)}</small></span></button>)}</div>
<footer>{codeMessage || userPnccMessage || 'Git 负责耐久化Forgejo 仅作为远端协作适配器。'}</footer>
<footer>{codeMessage || userPnccMessage || (repoLogin?.domain === 'FIFTH_DOMAIN' ? 'Git 负责耐久化Forgejo 仅作为远端协作适配器。' : '企业仓库是工作区,不授予个人频道或跨仓访问权。')}</footer>
</aside>
<aside className="repository-tree">
<header><button className="icon-button" disabled={!codeTree?.path} type="button" onClick={() => activeChannel && void browseChannel(activeChannel, codeParent())}><Icon name="back"/></button><div><b>{activeChannel?.name || '尚未选择频道'}</b><small>/{codeTree?.path || ''}</small></div></header>
@ -1175,17 +1215,18 @@ function HoloLakeApp() {
{activeCodeFile ? <>
<header><div><span>{activeCodeFile.path}</span><small>{activeCodeFile.format.toUpperCase()} · {activeCodeFile.sizeBytes.toLocaleString()} bytes</small></div><div className="mode-switch"><button className={codeMode === 'human' ? 'active' : ''} type="button" onClick={() => setCodeMode('human')}></button><button className={codeMode === 'source' ? 'active' : ''} type="button" onClick={() => setCodeMode('source')}></button></div></header>
<div className="code-reader-scroll">{codeMode === 'human' ? <MarkdownDocument body={activeCodeFile.humanMarkdown}/> : <pre className="source-code"><code>{activeCodeFile.source}</code></pre>}</div>
</> : <div className="workbench-empty"><span>&lt;/&gt;</span><h2>GH-PNCC · </h2><p> HoloLake Git </p></div>}
</> : <div className="workbench-empty"><span>&lt;/&gt;</span><h2>{repoLogin?.domain === 'FIFTH_DOMAIN' ? 'GH-PNCC · ' : ''}</h2><p>{repoLogin?.domain === 'FIFTH_DOMAIN' ? ' HoloLake Git ' : ''}</p></div>}
</main>
</section>
)
const renderReceipts = () => (
<section className="content-page">
<header className="page-title"><div><span className="kicker">LOCAL RECEIPTS</span><h1></h1><p></p></div></header>
<header className="page-title"><div><span className="kicker">{repoLogin?.domain === 'FIFTH_DOMAIN' ? 'LOCAL RECEIPTS' : 'ENTERPRISE RESPONSIBILITY RECEIPTS'}</span><h1></h1><p>{repoLogin?.domain === 'FIFTH_DOMAIN' ? '本机身份、知识与代码读取的可核验记录' : '本人签署的关系确认与责任接受状态'}</p></div></header>
<div className="receipt-grid">
<section className="plain-panel"><h2></h2>{personal.recentEvents.map((event) => <article className="receipt-row" key={event.eventId}><span>{String(event.sequence).padStart(2, '0')}</span><div><b>{event.summary}</b><small>{new Date(event.occurredAtUnixMs).toLocaleString('zh-CN')} · {event.receiptHash.slice(0, 12)}</small></div></article>)}</section>
<section className="plain-panel"><h2>PNCC </h2>{receipts.length ? receipts.map((receipt) => <article className="receipt-row" key={receipt.sequence}><span>{String(receipt.sequence).padStart(2, '0')}</span><div><b>{receipt.kind}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></article>) : <div className="empty-state"> PNCC </div>}</section>
{repoLogin?.domain === 'FIFTH_DOMAIN' ? <><section className="plain-panel"><h2></h2>{personal.recentEvents.map((event) => <article className="receipt-row" key={event.eventId}><span>{String(event.sequence).padStart(2, '0')}</span><div><b>{event.summary}</b><small>{new Date(event.occurredAtUnixMs).toLocaleString('zh-CN')} · {event.receiptHash.slice(0, 12)}</small></div></article>)}</section>
<section className="plain-panel"><h2>PNCC </h2>{receipts.length ? receipts.map((receipt) => <article className="receipt-row" key={receipt.sequence}><span>{String(receipt.sequence).padStart(2, '0')}</span><div><b>{receipt.kind}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></article>) : <div className="empty-state"> PNCC </div>}</section></> : <><section className="plain-panel"><h2></h2><dl className="evidence-list"><div><dt></dt><dd>{enterpriseEntry?.canonical_id || zeroPoint?.userNumber}</dd></div><div><dt></dt><dd>{enterpriseEntry?.relationship_confirmation?.decision || '等待确认'}</dd></div><div><dt></dt><dd>{enterpriseEntry?.relationship_confirmation?.observed_at || '—'}</dd></div><div><dt></dt><dd>{enterpriseEntry?.relationship_confirmation?.receipt_hash?.slice(0, 16) || '—'}</dd></div></dl></section>
<section className="plain-panel"><h2></h2><dl className="evidence-list"><div><dt></dt><dd>{domainDisplayName(enterpriseEntry?.subject.domain || repoLogin?.domain || '')}</dd></div><div><dt></dt><dd>{enterpriseEntry?.responsibility_receipt?.decision || '等待本人确认'}</dd></div><div><dt></dt><dd>{enterpriseEntry?.responsibility_receipt?.responsibility_version || enterpriseEntry?.registry_version || '—'}</dd></div><div><dt></dt><dd>{enterpriseEntry?.repository_binding.repository || '—'}</dd></div></dl></section></>}
</div>
</section>
)
@ -1244,6 +1285,23 @@ function HoloLakeApp() {
setWorldStage('tool')
}
const openEnterpriseRepository = async () => {
if (!enterpriseWork) {
setUserPnccMessage('本人私有责任工作仓库尚未完成可信接入。')
return
}
const channel = codeChannels.channels.find((item) => item.channelId === enterpriseWork.channelId)
if (!channel) {
setUserPnccMessage('工作仓库已完成认证,正在等待本机只读投影登记。')
await refreshCore()
return
}
await browseChannel(channel)
setView('code')
setToolReturnStage('enterpriseWork')
setWorldStage('tool')
}
const enterpriseRelationshipPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && enterpriseEntry.relationship_confirmation?.decision !== 'CONFIRM')
const enterpriseResponsibilityPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && !enterpriseRelationshipPending && enterpriseEntry.responsibility_receipt?.decision !== 'ACCEPT')
@ -1315,9 +1373,9 @@ function HoloLakeApp() {
<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">
{worldStage === 'domain' && <section className="domain-home">
<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>
<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')}/>
<div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? domainDisplayName(repoLogin.domain) : '光湖零感域'}</h1><p>{repoLogin.domain === 'FIFTH_DOMAIN' ? '世界正在发生什么' : '公共工作入口 · 世界正在发生什么'}</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.0</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-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</section>}
{worldStage === 'heart' && isZhizhi && <section className="channel-world private-route-world">
@ -1356,13 +1414,21 @@ function HoloLakeApp() {
<LakePool className="channel-light system-branch-love" title="爱之核心子系统" meta="责任主体 · 之之" onClick={() => setWorldStage('love')}/>
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</> : <>
<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={`${codeChannels.channels.length} 个仓库`} onClick={() => openWorldTool('code')}/>
<LakePool className="channel-light" title="光之湖" meta="人格体居所" onClick={() => openWorldTool('receipts')}/>
<LakePool className="channel-main" title={domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} meta="本人责任工作域" open={enterpriseWork?.state === 'READY_READ_ONLY_WORK_PROJECTION'} onClick={() => setWorldStage('enterpriseWork')}/>
<LakePool className="channel-code" title="私有责任工作仓库" meta={enterpriseWork ? `${enterpriseWork.repository} · 已认证` : userPnccBusy ? '正在接入' : '暂不可用'} onClick={() => void openEnterpriseRepository()}/>
<LakePool className="channel-light" title="责任签署状态" meta={enterpriseEntry?.responsibility_receipt?.decision === 'ACCEPT' ? '已接受 · 已留存' : '等待本人确认'} onClick={() => openWorldTool('receipts')}/>
<LakePool className="channel-knowledge" title="前往我的频道" meta="等待个人服务器接入与归属校验" onClick={() => setUserPnccMessage('个人生活区不会由企业工作账号代建;登记本人服务器后,系统才会从零感域路由过去。')}/>
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</>}
</section>}
{worldStage === 'enterpriseWork' && repoLogin.domain !== 'FIFTH_DOMAIN' && <section className="channel-world enterprise-work-world">
<button className="world-back" type="button" onClick={() => setWorldStage('channel')}> 退</button>
<div className="world-location"><h1>{domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}</h1><p> · </p></div>
<LakePool className="channel-main" title="责任工作总览" meta={`${enterpriseEntry?.subject.name || repoLogin.username} · ${zeroPoint?.userNumber || ''}`} open onClick={() => openWorldTool('receipts')}/>
<LakePool className="channel-code" title="私有责任工作仓库" meta={enterpriseWork ? `${enterpriseWork.repository} · 只读投影` : '尚未接入'} onClick={() => void openEnterpriseRepository()}/>
<LakePool className="channel-status" title="工作节点状态" meta={enterpriseWork?.state === 'READY_READ_ONLY_WORK_PROJECTION' ? '认证在线 · 禁止跨仓' : '不可用'} onClick={() => openWorldTool('system')}/>
{userPnccMessage && <p className="private-route-note">{userPnccMessage}</p>}
</section>}
{worldStage === 'heartbeat' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world heartbeat-world">
<button className="world-back" type="button" onClick={() => setWorldStage('channel')}> 退</button>
<div className="world-location"><h1></h1><p> · ICE-GL · </p></div>
@ -1395,7 +1461,7 @@ function HoloLakeApp() {
<textarea value={responsibilityNote} maxLength={1000} placeholder="可选:填写责任确认说明" onChange={(event) => setResponsibilityNote(event.target.value)}/>
<div className="receipt-actions"><button type="button" disabled={enterpriseReceiptBusy} onClick={() => void submitEnterpriseResponsibility('DECLINE')}></button><button className="primary" type="button" disabled={enterpriseReceiptBusy} onClick={() => void submitEnterpriseResponsibility('ACCEPT')}></button></div>{enterpriseReceiptMessage && <p className="receipt-message">{enterpriseReceiptMessage}</p>}
</section></div>}
{personal.state !== 'UNAVAILABLE' && !personal.identity && <div className="onboarding-backdrop"><section className="onboarding-card" role="dialog" aria-modal="true"><span className="onboarding-mark"></span><span className="kicker">FIRST LOCAL ENTRY</span><h1></h1><p></p><form onSubmit={(event) => void initializeIdentity(event)}><label htmlFor="display-name"></label><input id="display-name" autoFocus maxLength={80} value={displayName} placeholder="请输入显示名称" onChange={(event) => setDisplayName(event.target.value)}/><button className="primary-button" disabled={identityBusy || !displayName.trim()}></button></form>{identityMessage && <p>{identityMessage}</p>}</section></div>}
{repoLogin.domain === 'FIFTH_DOMAIN' && personal.state !== 'UNAVAILABLE' && !personal.identity && <div className="onboarding-backdrop"><section className="onboarding-card" role="dialog" aria-modal="true"><span className="onboarding-mark"></span><span className="kicker">FIRST LOCAL ENTRY</span><h1></h1><p></p><form onSubmit={(event) => void initializeIdentity(event)}><label htmlFor="display-name"></label><input id="display-name" autoFocus maxLength={80} value={displayName} placeholder="请输入显示名称" onChange={(event) => setDisplayName(event.target.value)}/><button className="primary-button" disabled={identityBusy || !displayName.trim()}></button></form>{identityMessage && <p>{identityMessage}</p>}</section></div>}
</div>
}