feat(hololake): add enterprise responsibility entrance

This commit is contained in:
冰朔 2026-09-03 21:21:20 +08:00
commit 97b378b60c
12 changed files with 599 additions and 16 deletions

View file

@ -10,6 +10,7 @@ import type {
SourceKind,
RuntimeOverview,
SystemSnapshot,
EnterpriseEntranceSnapshot,
} from "./types";
import {
MarkdownDocument,
@ -132,7 +133,7 @@ export default function App() {
{view === "knowledge" && <Knowledge data={data} refresh={refresh} />}{" "}
{view === "market" && <Market />}{" "}
{view === "history" && <History data={data} />}{" "}
{view === "portal" && <Portal />}{" "}
{view === "portal" && <Portal setError={setError} />}{" "}
{view === "settings" && (
<Settings
data={data}
@ -895,7 +896,38 @@ function History({ data }: { data: SystemSnapshot }) {
</div>
);
}
function Portal() {
function Portal({ setError }: { setError: (value: string) => void }) {
const [entrance, setEntrance] = useState<EnterpriseEntranceSnapshot | null>(null);
const [domainId, setDomainId] = useState("DOMAIN-MAIN");
const [humanId, setHumanId] = useState("");
const [personaId, setPersonaId] = useState("");
const [busy, setBusy] = useState(false);
const refresh = () => api.enterpriseEntranceSnapshot().then(setEntrance).catch((error) => setError(String(error)));
useEffect(() => {
void refresh();
}, []);
const prepareDevice = async () => {
setBusy(true);
try {
await api.prepareEnterpriseDevice();
await refresh();
} catch (error) {
setError(String(error));
} finally {
setBusy(false);
}
};
const prepareBinding = async () => {
setBusy(true);
try {
await api.prepareEnterpriseBinding(domainId, humanId, personaId);
await refresh();
} catch (error) {
setError(String(error));
} finally {
setBusy(false);
}
};
return (
<div className="page simple">
<header>
@ -906,24 +938,61 @@ function Portal() {
</header>
<section className="portal">
<div className="origin-light" />
<h2></h2>
<h2></h2>
<p>
HoloLake
</p>
<div className="portal-actions">
<a href="https://guanghu.chat" target="_blank" rel="noreferrer">
</a>
<a
href="https://guanghu.chat/code/user/login"
target="_blank"
rel="noreferrer"
>
</a>
</div>
</section>
<section className="enterprise-entrance" aria-label="光湖企业域责任入口">
<header>
<div>
<span className="eyebrow">HLP-MOD-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001</span>
<h2></h2>
<p></p>
</div>
<span className={`status ${entrance?.serverAuthorized ? "ok" : "pending"}`}>
{entrance?.serverAuthorized ? "企业会话已验证" : "企业服务器未授权"}
</span>
</header>
<div className="enterprise-proof-grid">
<article>
<strong></strong>
<p>{entrance?.device ? `${entrance.device.nodeId} · ${entrance.device.keyId}` : "尚未在系统钥匙串生成设备密钥"}</p>
<small></small>
{!entrance?.device && <button onClick={prepareDevice} disabled={busy}>{busy ? "正在准备…" : "准备本机证明"}</button>}
</article>
<article>
<strong></strong>
{entrance?.binding ? (
<>
<p>{entrance.binding.domainId} · {entrance.binding.responsibleHumanId} · {entrance.binding.personaId}</p>
<small>{entrance.binding.state}</small>
</>
) : (
<div className="enterprise-binding-form">
<select value={domainId} onChange={(event) => setDomainId(event.target.value)}>
<option value="DOMAIN-MAIN"></option>
<option value="DOMAIN-SUB"></option>
<option value="DOMAIN-ZERO"></option>
<option value="DOMAIN-ZS"></option>
</select>
<input value={humanId} onChange={(event) => setHumanId(event.target.value)} placeholder="域负责人编号" />
<input value={personaId} onChange={(event) => setPersonaId(event.target.value)} placeholder="绑定人格体编号" />
<button onClick={prepareBinding} disabled={busy || !entrance?.device || !humanId.trim() || !personaId.trim()}>
</button>
</div>
)}
</article>
</div>
<footer>{entrance?.nextAction ?? "正在读取本机责任入口状态…"}</footer>
</section>
</div>
);
}

View file

@ -11,6 +11,9 @@ import type {
RuntimeOverview,
SystemSnapshot,
TimelineEvent,
EnterpriseBindingRequest,
EnterpriseDeviceProof,
EnterpriseEntranceSnapshot,
} from "./types";
const tauri = () => "__TAURI_INTERNALS__" in window;
@ -291,3 +294,30 @@ export async function rejectProposal(
): Promise<AgentProposal> {
return invoke("reject_agent_proposal", { proposalId });
}
export async function enterpriseEntranceSnapshot(): Promise<EnterpriseEntranceSnapshot> {
if (tauri()) return invoke("enterprise_entrance_snapshot");
return {
schema: "preview",
state: "LOCAL_DEVICE_REGISTRATION_REQUIRED",
device: null,
binding: null,
enterpriseServerEmbedded: false,
serverAuthorized: false,
nextAction: "浏览器预览不生成设备凭证。",
};
}
export async function prepareEnterpriseDevice(): Promise<EnterpriseDeviceProof> {
if (!tauri()) throw new Error("设备密钥只能在已安装的 HoloLake 中生成");
return invoke("prepare_enterprise_device");
}
export async function prepareEnterpriseBinding(
domainId: string,
responsibleHumanId: string,
personaId: string,
): Promise<EnterpriseBindingRequest> {
if (!tauri()) throw new Error("企业责任绑定只能在已安装的 HoloLake 中准备");
return invoke("prepare_enterprise_binding", { domainId, responsibleHumanId, personaId });
}

View file

@ -980,6 +980,86 @@ main {
width: 72px;
height: 72px;
}
.enterprise-entrance {
margin-top: 18px;
padding: 24px;
border: 1px solid rgba(116, 174, 213, 0.2);
border-radius: 18px;
background:
radial-gradient(circle at 78% 0%, rgba(94, 163, 207, 0.1), transparent 34%),
rgba(6, 20, 36, 0.82);
}
.enterprise-entrance > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
padding: 0;
}
.enterprise-entrance h2 {
margin: 7px 0 5px;
font-size: 20px;
}
.enterprise-entrance p,
.enterprise-entrance small,
.enterprise-entrance footer {
color: var(--muted);
line-height: 1.6;
}
.enterprise-entrance .eyebrow {
color: #8ebde0;
font-size: 10px;
font-weight: 650;
letter-spacing: 0.08em;
}
.status.pending {
color: #e4ba67;
}
.enterprise-proof-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 20px;
}
.enterprise-proof-grid article {
display: grid;
align-content: start;
gap: 9px;
min-height: 176px;
padding: 18px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.035);
}
.enterprise-proof-grid article > strong {
font-size: 15px;
}
.enterprise-proof-grid article p,
.enterprise-proof-grid article small {
margin: 0;
overflow-wrap: anywhere;
}
.enterprise-proof-grid button {
justify-self: start;
margin-top: auto;
}
.enterprise-binding-form {
display: grid;
gap: 8px;
}
.enterprise-binding-form input,
.enterprise-binding-form select {
width: 100%;
min-height: 39px;
border: 1px solid var(--line);
border-radius: 10px;
padding: 0 12px;
color: var(--text);
background: rgba(2, 12, 23, 0.72);
}
.enterprise-entrance footer {
margin-top: 16px;
font-size: 12px;
}
.origin-light {
border-radius: 50%;
background: radial-gradient(

View file

@ -126,3 +126,36 @@ export interface RealtimeInvitation {
connectorCommand: string;
warning: string;
}
export interface EnterpriseDeviceProof {
schema: string;
state: string;
nodeId: string;
keyId: string;
publicKey: string;
auxiliaryMachineFingerprintSha256: string;
fingerprintIsSoleCredential: boolean;
privateKeyStorage: string;
createdAt: string;
}
export interface EnterpriseBindingRequest {
schema: string;
requestId: string;
state: string;
domainId: string;
responsibleHumanId: string;
personaId: string;
nodeId: string;
keyId: string;
requestedRepositoryScope: string;
createdAt: string;
serverAuthorized: boolean;
}
export interface EnterpriseEntranceSnapshot {
schema: string;
state: string;
device: EnterpriseDeviceProof | null;
binding: EnterpriseBindingRequest | null;
enterpriseServerEmbedded: boolean;
serverAuthorized: boolean;
nextAction: string;
}