feat(hololake): add enterprise responsibility entrance
This commit is contained in:
parent
3df6b1d3ca
commit
97b378b60c
12 changed files with 599 additions and 16 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue