feat: add lighthouse and fifth-domain entry flow
This commit is contained in:
parent
674c9e7764
commit
a35d61e227
12 changed files with 1056 additions and 3 deletions
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -11,6 +11,7 @@ import { HumanSettings, HumanPreferences, loadHumanPreferences } from './compone
|
|||
import { DomainSurface } from './components/DomainSurface';
|
||||
import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
|
||||
import { cleanDisplayText } from './presentation';
|
||||
import { WorldEntry } from './components/WorldEntry';
|
||||
|
||||
type View = 'editor' | 'history';
|
||||
type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense';
|
||||
|
|
@ -88,6 +89,7 @@ export default function App() {
|
|||
const [moduleBusy, setModuleBusy] = useState(false);
|
||||
const [moduleMessage, setModuleMessage] = useState('');
|
||||
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
|
||||
const [worldEntered, setWorldEntered] = useState(false);
|
||||
|
||||
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
||||
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
||||
|
|
@ -353,7 +355,21 @@ export default function App() {
|
|||
useEffect(() => { localStorage.setItem('hololake.layout.agent-width', String(Math.round(agentWidth))); }, [agentWidth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
<>
|
||||
{!worldEntered && (
|
||||
<WorldEntry
|
||||
onEnterFifth={() => {
|
||||
setActiveRoute('fifth');
|
||||
setActiveModule('knowledge');
|
||||
setWorldEntered(true);
|
||||
}}
|
||||
onOpenConnection={() => {
|
||||
setStorageSheetInitialMode('server');
|
||||
setStorageSheetOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="hololake-shell"
|
||||
data-language={language}
|
||||
data-appearance={humanPreferences.appearance}
|
||||
|
|
@ -556,6 +572,7 @@ export default function App() {
|
|||
onChange={(moduleId, installed, mounted) => void changeModuleState(moduleId, installed, mounted)}
|
||||
onUndo={() => void undoModuleChange()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import { useState } from 'react';
|
||||
import { BookOpen, Crosshair, DoorOpen, Landmark, Link2, LogIn, ShieldCheck } from 'lucide-react';
|
||||
|
||||
type DomainId = 'main' | 'sub' | 'zero' | 'zero-sense' | 'fifth';
|
||||
|
||||
interface Props {
|
||||
onEnterFifth: () => void;
|
||||
onOpenConnection: () => void;
|
||||
}
|
||||
|
||||
const domains: Array<{ id: DomainId; number: string; name: string; summary: string }> = [
|
||||
{ id: 'main', number: '01', name: '光湖主域', summary: '公共产品事实、发布与公告入口' },
|
||||
{ id: 'sub', number: '02', name: '光湖分域', summary: '行业入口与初始化频道目录' },
|
||||
{ id: 'zero', number: '03', name: '光湖零域', summary: '实验、模块试装与质量验证入口' },
|
||||
{ id: 'zero-sense', number: '04', name: '光湖零感域', summary: '治理、审批、回滚与审计入口' },
|
||||
{ id: 'fifth', number: '05', name: '第五域 · 光湖本源域', summary: '零点原核的工程本体 · 语言架构层' },
|
||||
];
|
||||
|
||||
export function WorldEntry({ onEnterFifth, onOpenConnection }: Props) {
|
||||
const [stage, setStage] = useState<'lighthouse' | 'directory' | 'fifth'>('lighthouse');
|
||||
const [selectedDomain, setSelectedDomain] = useState<DomainId | null>(null);
|
||||
|
||||
const selectDomain = (id: DomainId) => {
|
||||
setSelectedDomain(id);
|
||||
if (id === 'fifth') setStage('fifth');
|
||||
};
|
||||
|
||||
if (stage === 'fifth') {
|
||||
return (
|
||||
<main className="world-entry fifth-vestibule">
|
||||
<div className="entry-backdrop" aria-hidden="true" />
|
||||
<div className="entry-vignette" aria-hidden="true" />
|
||||
|
||||
<header className="entry-brand fifth-brand">
|
||||
<span>HoloLake</span>
|
||||
<strong>GH-AIOS</strong>
|
||||
<small>光湖语言系统 · 通用人工智能操作平台</small>
|
||||
</header>
|
||||
|
||||
<nav className="fifth-domain-rail" aria-label="五域公开导航">
|
||||
{domains.map(domain => (
|
||||
<button
|
||||
key={domain.id}
|
||||
type="button"
|
||||
className={domain.id === 'fifth' ? 'selected' : ''}
|
||||
onClick={() => domain.id === 'fifth' ? undefined : (setStage('directory'), setSelectedDomain(domain.id))}
|
||||
>
|
||||
<span>{domain.number}</span>
|
||||
<strong>{domain.name}</strong>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<section className="fifth-focus-panel" aria-labelledby="fifth-title">
|
||||
<small>当前聚焦域</small>
|
||||
<h1 id="fifth-title">第五域 · 光湖本源域</h1>
|
||||
<dl>
|
||||
<div><dt><ShieldCheck aria-hidden="true" /><span>稳定编号</span></dt><dd>DOM-FIFTH-0001</dd></div>
|
||||
<div><dt><Crosshair aria-hidden="true" /><span>定位</span></dt><dd>零点原核的工程本体 · 语言架构层</dd></div>
|
||||
<div><dt><Link2 aria-hidden="true" /><span>接入方式</span></dt><dd>本地终端节点 / 云端常驻节点</dd></div>
|
||||
<div><dt><DoorOpen aria-hidden="true" /><span>状态</span></dt><dd>公开门厅可查看 · 进入后装载独立域运行体</dd></div>
|
||||
</dl>
|
||||
<div className="fifth-actions">
|
||||
<button className="fifth-primary" type="button" onClick={onEnterFifth}><LogIn aria-hidden="true" />进入第五域</button>
|
||||
<button type="button" onClick={() => setStage('directory')}><BookOpen aria-hidden="true" />查看公开介绍</button>
|
||||
<button type="button" onClick={() => setStage('lighthouse')}><Landmark aria-hidden="true" />返回灯塔</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`world-entry public-lighthouse ${stage === 'directory' ? 'is-directory' : ''}`}>
|
||||
<div className="entry-backdrop" aria-hidden="true" />
|
||||
<div className="entry-vignette" aria-hidden="true" />
|
||||
|
||||
<section className="public-entry-copy" aria-labelledby="public-entry-title">
|
||||
<header className="entry-brand">
|
||||
<span>HoloLake</span>
|
||||
<strong>GH-AIOS</strong>
|
||||
<small>光湖语言系统 · 通用人工智能操作平台</small>
|
||||
</header>
|
||||
<div className="public-divider" aria-hidden="true" />
|
||||
<h1 id="public-entry-title">{stage === 'directory' ? '五域公开门厅' : '公共灯塔'}</h1>
|
||||
<p>{stage === 'directory' ? '先在门口了解每个域;进入时再由对应节点完成接入。' : '从这里进入光湖世界'}</p>
|
||||
<div className="public-entry-actions">
|
||||
<button className="public-primary" type="button" onClick={() => setStage(stage === 'directory' ? 'lighthouse' : 'directory')}>
|
||||
{stage === 'directory' ? '返回公共灯塔' : '查看五域'}
|
||||
</button>
|
||||
<button type="button" onClick={onOpenConnection}>登录或接入节点</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav className="public-domain-strip" aria-label="五域公开门厅">
|
||||
{domains.map(domain => (
|
||||
<button
|
||||
key={domain.id}
|
||||
type="button"
|
||||
className={selectedDomain === domain.id ? 'selected' : ''}
|
||||
onClick={() => selectDomain(domain.id)}
|
||||
>
|
||||
<span>{domain.number}</span>
|
||||
<strong>{domain.name}</strong>
|
||||
{stage === 'directory' && <small>{domain.summary}</small>}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<footer className="public-entry-footer">支持本地终端节点与云端常驻节点</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
|
|||
import App from './App';
|
||||
import './styles/app.css';
|
||||
import './styles/workspace-0.7.1.css';
|
||||
import './styles/world-entry.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
415
product-source/guanghu-knowledge-base/src/styles/world-entry.css
Normal file
415
product-source/guanghu-knowledge-base/src/styles/world-entry.css
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
.world-entry {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
min-width: 860px;
|
||||
min-height: 620px;
|
||||
overflow: hidden;
|
||||
color: #edf4ff;
|
||||
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
||||
background: #06142b;
|
||||
}
|
||||
|
||||
.entry-backdrop,
|
||||
.entry-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.public-lighthouse .entry-backdrop {
|
||||
background: url('/gh-aios-public-lighthouse.png') center / cover no-repeat;
|
||||
}
|
||||
|
||||
.fifth-vestibule .entry-backdrop {
|
||||
background: url('/fifth-domain-lighthouse.png') center / cover no-repeat;
|
||||
}
|
||||
|
||||
.public-lighthouse .entry-vignette {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(1, 10, 27, .16) 0%, rgba(2, 13, 31, 0) 48%, rgba(1, 10, 24, .72) 100%),
|
||||
radial-gradient(circle at 50% 32%, rgba(85, 131, 205, .08), rgba(0, 6, 19, .18) 64%, rgba(0, 5, 16, .42) 100%);
|
||||
}
|
||||
|
||||
.fifth-vestibule .entry-vignette {
|
||||
background: linear-gradient(90deg, rgba(1, 5, 20, .28), rgba(1, 6, 25, .05) 65%, rgba(2, 4, 17, .2));
|
||||
}
|
||||
|
||||
.entry-brand {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.entry-brand span {
|
||||
font-size: clamp(24px, 2.7vw, 40px);
|
||||
font-weight: 500;
|
||||
letter-spacing: -.03em;
|
||||
}
|
||||
|
||||
.entry-brand strong {
|
||||
color: #dbe9ff;
|
||||
font-family: "Helvetica Neue", Inter, sans-serif;
|
||||
font-size: clamp(58px, 7.3vw, 108px);
|
||||
font-weight: 400;
|
||||
line-height: .94;
|
||||
letter-spacing: .05em;
|
||||
text-shadow: 0 5px 24px rgba(73, 133, 220, .18);
|
||||
}
|
||||
|
||||
.entry-brand small {
|
||||
color: rgba(232, 240, 255, .82);
|
||||
font-size: clamp(14px, 1.35vw, 21px);
|
||||
font-weight: 400;
|
||||
letter-spacing: .25em;
|
||||
}
|
||||
|
||||
.public-entry-copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(720px, 62vw);
|
||||
margin: clamp(72px, 10vh, 120px) auto 0;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.public-divider {
|
||||
width: 190px;
|
||||
height: 1px;
|
||||
margin: 30px 0 30px;
|
||||
background: linear-gradient(90deg, transparent, rgba(202, 222, 250, .36), transparent);
|
||||
}
|
||||
|
||||
.public-entry-copy h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(42px, 5vw, 68px);
|
||||
font-weight: 500;
|
||||
letter-spacing: .11em;
|
||||
}
|
||||
|
||||
.public-entry-copy > p {
|
||||
margin: 15px 0 34px;
|
||||
color: rgba(228, 237, 252, .72);
|
||||
font-size: clamp(16px, 1.65vw, 24px);
|
||||
letter-spacing: .22em;
|
||||
}
|
||||
|
||||
.public-entry-actions {
|
||||
width: min(570px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.public-entry-actions button,
|
||||
.fifth-actions button {
|
||||
min-height: 66px;
|
||||
border: 1px solid rgba(172, 203, 245, .62);
|
||||
border-radius: 12px;
|
||||
background: rgba(13, 35, 72, .38);
|
||||
color: #edf4ff;
|
||||
font: inherit;
|
||||
font-size: 19px;
|
||||
font-weight: 500;
|
||||
letter-spacing: .08em;
|
||||
backdrop-filter: blur(14px);
|
||||
transition: border-color .18s ease, background .18s ease, transform .18s ease;
|
||||
}
|
||||
|
||||
.public-entry-actions button:hover,
|
||||
.fifth-actions button:hover,
|
||||
.public-domain-strip button:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(224, 237, 255, .92);
|
||||
}
|
||||
|
||||
.public-entry-actions .public-primary {
|
||||
border-color: rgba(231, 243, 255, .84);
|
||||
background: linear-gradient(135deg, rgba(222, 237, 255, .92), rgba(131, 181, 243, .82));
|
||||
color: #09214a;
|
||||
box-shadow: 0 12px 35px rgba(65, 128, 216, .18);
|
||||
}
|
||||
|
||||
.public-domain-strip {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 8.5vw;
|
||||
right: 8.5vw;
|
||||
bottom: clamp(116px, 15vh, 168px);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
border-bottom: 1px solid rgba(179, 207, 239, .22);
|
||||
}
|
||||
|
||||
.public-domain-strip button {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 86px;
|
||||
padding: 8px 16px 26px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
color: rgba(230, 239, 252, .84);
|
||||
font: inherit;
|
||||
text-align: center;
|
||||
transition: color .18s ease, border-color .18s ease, transform .18s ease;
|
||||
}
|
||||
|
||||
.public-domain-strip button + button::before {
|
||||
content: '/';
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: 22px;
|
||||
color: rgba(158, 197, 244, .72);
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.public-domain-strip span,
|
||||
.public-domain-strip strong,
|
||||
.public-domain-strip small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.public-domain-strip span {
|
||||
margin-bottom: 8px;
|
||||
color: rgba(154, 192, 239, .82);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.public-domain-strip strong {
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.public-domain-strip small {
|
||||
margin-top: 8px;
|
||||
color: rgba(208, 222, 242, .64);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.public-domain-strip button.selected,
|
||||
.public-domain-strip button:focus-visible {
|
||||
border-bottom-color: #b8d6ff;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.public-entry-footer {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
bottom: 65px;
|
||||
color: rgba(202, 219, 241, .6);
|
||||
font-size: 14px;
|
||||
letter-spacing: .16em;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.public-lighthouse.is-directory .public-entry-copy {
|
||||
margin-top: clamp(45px, 6vh, 75px);
|
||||
}
|
||||
|
||||
.public-lighthouse.is-directory .entry-brand strong {
|
||||
font-size: clamp(46px, 5vw, 74px);
|
||||
}
|
||||
|
||||
.public-lighthouse.is-directory .public-divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.public-lighthouse.is-directory .public-domain-strip {
|
||||
bottom: clamp(92px, 12vh, 130px);
|
||||
}
|
||||
|
||||
.public-lighthouse.is-directory .public-domain-strip button {
|
||||
min-height: 118px;
|
||||
}
|
||||
|
||||
.fifth-brand {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 62px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.fifth-brand span { font-size: 27px; }
|
||||
.fifth-brand strong {
|
||||
color: #ffe9b7;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(60px, 6.5vw, 92px);
|
||||
text-shadow: 0 0 24px rgba(255, 201, 116, .16);
|
||||
}
|
||||
.fifth-brand small { color: rgba(255, 237, 204, .82); font-size: 14px; }
|
||||
|
||||
.fifth-domain-rail {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 4vw;
|
||||
top: 15vh;
|
||||
bottom: 23vh;
|
||||
width: 238px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fifth-domain-rail::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
top: 24px;
|
||||
bottom: 32px;
|
||||
width: 1px;
|
||||
background: linear-gradient(#9d83ce, #c6a063 78%, #ffe2a0);
|
||||
box-shadow: 0 0 18px rgba(208, 168, 106, .22);
|
||||
}
|
||||
|
||||
.fifth-domain-rail button {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 58px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 60px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(235, 231, 247, .86);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fifth-domain-rail button span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(197, 166, 227, .8);
|
||||
border-radius: 50%;
|
||||
background: rgba(11, 11, 41, .74);
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.fifth-domain-rail button strong {
|
||||
max-width: 130px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fifth-domain-rail button:focus-visible {
|
||||
outline: 1px solid #ffe1a1;
|
||||
outline-offset: 5px;
|
||||
}
|
||||
|
||||
.fifth-domain-rail button.selected span {
|
||||
border-color: #ffe1a1;
|
||||
color: #ffe5ad;
|
||||
font-size: 27px;
|
||||
box-shadow: 0 0 0 11px rgba(255, 207, 126, .1), 0 0 30px rgba(255, 206, 124, .35);
|
||||
}
|
||||
|
||||
.fifth-domain-rail button.selected strong {
|
||||
color: #ffe1a1;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.fifth-focus-panel {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 27vh;
|
||||
left: max(320px, 24vw);
|
||||
width: min(820px, 56vw);
|
||||
padding: 32px 42px 36px;
|
||||
border: 1px solid rgba(174, 174, 217, .42);
|
||||
border-radius: 18px;
|
||||
background: rgba(6, 12, 43, .68);
|
||||
box-shadow: 0 22px 80px rgba(1, 3, 18, .28);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.fifth-focus-panel > small {
|
||||
color: #f3d49b;
|
||||
font-size: 15px;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.fifth-focus-panel h1 {
|
||||
margin: 7px 0 13px;
|
||||
color: #ffe1a8;
|
||||
font-size: clamp(35px, 3.4vw, 52px);
|
||||
font-weight: 600;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.fifth-focus-panel dl { margin: 0; }
|
||||
.fifth-focus-panel dl > div {
|
||||
display: grid;
|
||||
grid-template-columns: 150px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
min-height: 63px;
|
||||
border-top: 1px solid rgba(181, 181, 220, .22);
|
||||
}
|
||||
|
||||
.fifth-focus-panel dt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: rgba(234, 230, 246, .72);
|
||||
}
|
||||
.fifth-focus-panel dt svg { width: 22px; height: 22px; stroke-width: 1.6; }
|
||||
.fifth-focus-panel dd { margin: 0; color: rgba(244, 243, 251, .88); }
|
||||
|
||||
.fifth-actions {
|
||||
position: absolute;
|
||||
top: calc(100% + 24px);
|
||||
left: 50%;
|
||||
width: min(650px, 86%);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.fifth-actions button {
|
||||
min-height: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.fifth-actions button svg { width: 22px; height: 22px; stroke-width: 1.6; }
|
||||
.fifth-actions .fifth-primary {
|
||||
grid-column: 1 / -1;
|
||||
width: min(510px, 82%);
|
||||
min-height: 82px;
|
||||
justify-self: center;
|
||||
border-color: rgba(255, 238, 189, .92);
|
||||
background: linear-gradient(135deg, rgba(240, 191, 111, .9), rgba(176, 129, 72, .8));
|
||||
color: #fff9ec;
|
||||
font-size: 19px;
|
||||
box-shadow: 0 0 0 3px rgba(255, 216, 144, .16), 0 0 26px rgba(255, 196, 103, .34);
|
||||
}
|
||||
|
||||
@media (max-height: 760px) {
|
||||
.public-entry-copy { margin-top: 38px; }
|
||||
.public-divider { margin: 17px 0; }
|
||||
.public-entry-copy > p { margin-bottom: 22px; }
|
||||
.public-domain-strip { bottom: 82px; }
|
||||
.public-entry-footer { bottom: 28px; }
|
||||
.fifth-brand { top: 24px; }
|
||||
.fifth-focus-panel { top: 25vh; }
|
||||
.fifth-actions { top: calc(100% + 14px); }
|
||||
}
|
||||
68
product-source/hololake-desktop/design-qa.md
Normal file
68
product-source/hololake-desktop/design-qa.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# HoloLake 0.8.0 · 公共灯塔与第五域门厅视觉验收
|
||||
|
||||
## 视觉事实源
|
||||
|
||||
- 公共灯塔选定稿:`/Users/bingshuolingdianyuanhe/.codex/generated_images/019fe458-4bb4-7530-aad0-f73f760ad018/exec-d674d5cb-760b-4415-9eb1-09e8dd84fec4.png`
|
||||
- 第五域选定稿:`/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-09/new-chat/work/DEV-20260809-007/ui-audit-20260809/selected-fifth-domain-visual-target-v2.png`
|
||||
- 公共灯塔实现截图:`/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-09/new-chat/work/DEV-20260809-007/ui-audit-20260809/implementation-public-lighthouse-final.png`
|
||||
- 第五域实现截图:`/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-09/new-chat/work/DEV-20260809-007/ui-audit-20260809/implementation-fifth-domain-final.png`
|
||||
- 公共灯塔同屏对照:`/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-09/new-chat/work/DEV-20260809-007/ui-audit-20260809/comparison-public-final.png`
|
||||
- 第五域同屏对照:`/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-09/new-chat/work/DEV-20260809-007/ui-audit-20260809/comparison-fifth-final.png`
|
||||
|
||||
## 归一化条件
|
||||
|
||||
- CSS 视口:1488 × 1058。
|
||||
- deviceScaleFactor:1。
|
||||
- 选定稿像素:1487 × 1058;只在对照图中等比归一化为 1488 × 1058。
|
||||
- 实现截图像素:1488 × 1058。
|
||||
- 状态:未登录公共灯塔;第五域公开门厅;桌面端深色主题。
|
||||
- 浏览器渲染:HoloLake 0.8.0 本地生产构建预览。
|
||||
|
||||
## 最终对照结论
|
||||
|
||||
### 公共灯塔
|
||||
|
||||
- 字体与层级:HoloLake、GH-AIOS、公共灯塔、说明和操作按钮保持选定稿的单焦点层级;字重、字距和正文对比度均可读。
|
||||
- 布局与节奏:中央品牌、双按钮、五域横向门厅和底部节点说明与选定稿同构;没有额外卡片或多栏抢焦点。
|
||||
- 色彩与视觉 token:公共外壳保持克制深蓝,主按钮使用冷白蓝,灯塔只承担单一暖光焦点;未复制第五域的淡金专属主题。
|
||||
- 图像质量:使用独立的无文字高分辨率湖面灯塔背景,不以界面截图充当背景;1488 × 1058 下无拉伸、马赛克或明显压缩伪影。
|
||||
- 文案:软件全称、公共灯塔、五域名称、第五域正式名和“光湖零域”均正确。
|
||||
|
||||
### 第五域 · 光湖本源域
|
||||
|
||||
- 字体与层级:淡金 GH-AIOS、当前聚焦域、第五域名称、真实状态表和进入动作形成清楚的单一路径。
|
||||
- 布局与节奏:五域轨道、中央真实信息面板、主操作和两个次操作与选定稿的空间关系一致;核心内容没有散落或互相争抢。
|
||||
- 色彩与视觉 token:深蓝紫黑湖面、淡金暖白主光和蓝紫方向性光带符合第五域“湖面映星”语法,与公共主题保持同源但不相同。
|
||||
- 图像与图标:背景为独立生成的无界面资产;状态与操作使用 Lucide 正式图标组件,没有手绘 SVG、字符图标或占位符。
|
||||
- 文案:“光湖零域”纠正已经进入所有门厅;第五域稳定编号、零点原核定位、接入方式和独立运行体状态均为真实产品语义,不使用装饰假数据。
|
||||
|
||||
## 交互与运行检查
|
||||
|
||||
- “查看五域”可进入五域公开门厅。
|
||||
- “登录或接入节点”可打开现有真实节点/保存位置入口。
|
||||
- 第五域入口可进入第五域公开门厅。
|
||||
- “进入第五域”可打开现有知识、Agent 与频道工作区。
|
||||
- 14 项现有自动化测试全部通过。
|
||||
- HoloLake 0.8.0 生产构建通过。
|
||||
- 核心流程浏览器控制台错误:0。
|
||||
|
||||
## 对照迭代历史
|
||||
|
||||
1. v1:第五域信息面板偏窄,主次操作被压成同一行,和选定稿的动作层级不一致(P2)。
|
||||
- 修正:扩大信息面板、恢复一主两次的两层操作布局、调整轨道间距与字号。
|
||||
- 证据:`implementation-fifth-domain-v1.png` → `implementation-fifth-domain-v2.png`。
|
||||
2. v2:状态行与按钮缺少选定稿中的语义图标(P2)。
|
||||
- 修正:接入可树摇的 Lucide ESM 图标;生产包主脚本保持约 302 KB,没有因整库引入形成大包回退。
|
||||
- 证据:`implementation-fifth-domain-v2.png` → `implementation-fifth-domain-final.png`。
|
||||
3. 最终复验:公共与第五域选定稿和实现图均以相同视口同屏比较;没有剩余 P0、P1 或 P2 差异。
|
||||
|
||||
## 可接受的 P3 差异
|
||||
|
||||
- 字体由系统可用字体替代选定稿中的生成式展示字形,字面宽度存在轻微差别,但没有改变层级、换行或可读性。
|
||||
- 第五域背景光轨与选定稿不是逐像素复制,而是同一艺术方向的独立无文字生产资产;主体位置、色调与内容留白一致。
|
||||
|
||||
## Focused region comparison
|
||||
|
||||
第五域中央面板和操作区在 1488 × 1058 原始像素下单独检查了字号、行高、分隔线、图标、按钮宽高与焦点状态;这些细节在全屏同屏对照中仍清晰可读,因此无需另行裁切放大。
|
||||
|
||||
final result: passed
|
||||
11
product-source/hololake-desktop/package-lock.json
generated
11
product-source/hololake-desktop/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"diff": "^9.0.0",
|
||||
"express": "^5.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.0",
|
||||
"simple-git": "^3.27.0"
|
||||
},
|
||||
|
|
@ -4747,6 +4748,15 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.468.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
|
||||
"integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
|
|
@ -5527,7 +5537,6 @@
|
|||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"diff": "^9.0.0",
|
||||
"express": "^5.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.0",
|
||||
"simple-git": "^3.27.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default defineConfig({
|
|||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom/index.js'),
|
||||
'react': path.resolve(__dirname, 'node_modules/react/index.js'),
|
||||
'marked': path.resolve(__dirname, 'node_modules/marked/lib/marked.esm.js'),
|
||||
'lucide-react': path.resolve(__dirname, 'node_modules/lucide-react/dist/esm/lucide-react.js'),
|
||||
},
|
||||
},
|
||||
root: path.resolve(__dirname, '../guanghu-knowledge-base'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
INITIAL_DOMAIN_ACCESS_STATE,
|
||||
acceptDomainConnectionReceipt,
|
||||
acceptDomainSession,
|
||||
assertVerifiedDomainManifest,
|
||||
beginDomainAccess,
|
||||
disconnectDomainRuntime,
|
||||
enterDomainRuntime,
|
||||
projectPublicDomainVestibule,
|
||||
selectPublicDomain,
|
||||
type DomainConnectionReceipt,
|
||||
type DomainSessionCapability,
|
||||
type VerifiedDomainManifest,
|
||||
} from './domainRuntimeContract'
|
||||
|
||||
const DIGEST = 'a'.repeat(64)
|
||||
const NOW = 1_786_291_200_000
|
||||
|
||||
function manifest(): VerifiedDomainManifest {
|
||||
return {
|
||||
public: {
|
||||
accessModes: ['LOCAL_TERMINAL_NODE', 'CLOUD_RESIDENT_NODE'],
|
||||
displayName: '第五域 · 光湖本源域',
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
formalName: '光湖本源域',
|
||||
publicStatus: 'PUBLIC_PREVIEW',
|
||||
purpose: '零点原核的工程本体与语言架构源点域',
|
||||
responsibleParty: '第五域主控',
|
||||
themePreview: {
|
||||
assetRef: 'theme-preview://origin-domain/lake-reflects-stars',
|
||||
description: '湖面映星公开预览',
|
||||
},
|
||||
},
|
||||
provenance: {
|
||||
digest: DIGEST,
|
||||
repositoryId: 'REPO-012',
|
||||
signature: 'signature-envelope-reference',
|
||||
signerId: 'SIGNER-FIFTH-DOMAIN-001',
|
||||
sourceCommit: '6e26d1f35439759542efb317c3ddd7661ba27d58',
|
||||
},
|
||||
runtime: {
|
||||
backendVersion: 'not-published',
|
||||
channelIds: ['CHANNEL-ZERO-CORE'],
|
||||
forbiddenDataScopes: ['private-relationship-core'],
|
||||
frontendVersion: 'not-published',
|
||||
healthCheckRef: 'health://origin-domain/runtime',
|
||||
moduleIds: ['MODULE-LANGUAGE-ARCHITECTURE'],
|
||||
rollbackVersion: 'not-published',
|
||||
routeRef: 'domain-route://origin-domain/runtime',
|
||||
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
|
||||
},
|
||||
schema: 'gh-aios.domain-manifest/v1',
|
||||
verification: {
|
||||
manifestDigest: DIGEST,
|
||||
state: 'verified',
|
||||
verifiedAt: NOW,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function session(overrides: Partial<DomainSessionCapability> = {}): DomainSessionCapability {
|
||||
return {
|
||||
capabilityId: 'CAP-001',
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
expiresAt: NOW + 60_000,
|
||||
nodeId: 'LOCAL-NODE-001',
|
||||
scopes: ['domain:enter'],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function receipt(overrides: Partial<DomainConnectionReceipt> = {}): DomainConnectionReceipt {
|
||||
return {
|
||||
connectionId: 'CONNECTION-001',
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
manifestDigest: DIGEST,
|
||||
nodeId: 'LOCAL-NODE-001',
|
||||
receiptId: 'RECEIPT-001',
|
||||
state: 'online',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('domain runtime contract', () => {
|
||||
it('projects only public vestibule fields', () => {
|
||||
const publicView = projectPublicDomainVestibule(manifest())
|
||||
const serialized = JSON.stringify(publicView)
|
||||
|
||||
expect(publicView.displayName).toBe('第五域 · 光湖本源域')
|
||||
expect(serialized).not.toContain('domain-route://')
|
||||
expect(serialized).not.toContain('CHANNEL-ZERO-CORE')
|
||||
expect(serialized).not.toContain('private-relationship-core')
|
||||
expect(serialized).not.toContain('REPO-012')
|
||||
})
|
||||
|
||||
it('rejects a manifest that has not passed the external signature verifier', () => {
|
||||
const candidate = manifest()
|
||||
candidate.verification.state = 'verified'
|
||||
candidate.verification.manifestDigest = 'b'.repeat(64)
|
||||
|
||||
expect(() => assertVerifiedDomainManifest(candidate)).toThrow('domain_manifest_digest_invalid')
|
||||
})
|
||||
|
||||
it.each(['LOCAL_TERMINAL_NODE', 'CLOUD_RESIDENT_NODE'] as const)(
|
||||
'accepts %s as a user-owned access node type',
|
||||
(nodeType) => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), nodeType)
|
||||
|
||||
expect(access).toMatchObject({ nodeType, stage: 'access' })
|
||||
},
|
||||
)
|
||||
|
||||
it('does not enter a runtime with only a selected vestibule or frontend login state', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
|
||||
expect(() => enterDomainRuntime(access, NOW)).toThrow('domain_runtime_session_missing')
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
expect(() => enterDomainRuntime(withSession, NOW)).toThrow('domain_runtime_connection_receipt_missing')
|
||||
})
|
||||
|
||||
it('enters only after a matching live session and connection receipt', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
const connected = acceptDomainConnectionReceipt(withSession, receipt())
|
||||
const runtime = enterDomainRuntime(connected, NOW)
|
||||
|
||||
expect(runtime).toMatchObject({
|
||||
mode: 'connected',
|
||||
nodeType: 'LOCAL_TERMINAL_NODE',
|
||||
stage: 'runtime',
|
||||
})
|
||||
})
|
||||
|
||||
it('fails closed when the session or receipt belongs to another domain', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
|
||||
expect(() => acceptDomainSession(access, session({ domainId: 'DOMAIN-OTHER' }), NOW))
|
||||
.toThrow('domain_session_domain_mismatch')
|
||||
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
expect(() => acceptDomainConnectionReceipt(withSession, receipt({ domainId: 'DOMAIN-OTHER' })))
|
||||
.toThrow('domain_connection_domain_mismatch')
|
||||
})
|
||||
|
||||
it('drops execution authority and keeps an explicit read-only scene after disconnect', () => {
|
||||
const selected = selectPublicDomain(INITIAL_DOMAIN_ACCESS_STATE, manifest().public)
|
||||
const access = beginDomainAccess(selected, manifest(), 'LOCAL_TERMINAL_NODE')
|
||||
const withSession = acceptDomainSession(access, session(), NOW)
|
||||
const connected = acceptDomainConnectionReceipt(withSession, receipt())
|
||||
const runtime = enterDomainRuntime(connected, NOW)
|
||||
|
||||
expect(disconnectDomainRuntime(runtime).mode).toBe('disconnected-readonly')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
export type DomainNodeType = 'LOCAL_TERMINAL_NODE' | 'CLOUD_RESIDENT_NODE'
|
||||
|
||||
export type PublicDomainStatus = 'PUBLIC_PREVIEW' | 'RESTRICTED_PREVIEW' | 'UNAVAILABLE'
|
||||
|
||||
export interface PublicThemePreview {
|
||||
assetRef: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PublicDomainVestibule {
|
||||
accessModes: DomainNodeType[]
|
||||
displayName: string
|
||||
domainId: string
|
||||
formalName: string
|
||||
publicStatus: PublicDomainStatus
|
||||
purpose: string
|
||||
responsibleParty: string
|
||||
themePreview: PublicThemePreview | null
|
||||
}
|
||||
|
||||
export interface DomainManifestProvenance {
|
||||
digest: string
|
||||
repositoryId: string
|
||||
signature: string
|
||||
signerId: string
|
||||
sourceCommit: string
|
||||
}
|
||||
|
||||
export interface DomainRuntimeDescriptor {
|
||||
backendVersion: string
|
||||
channelIds: string[]
|
||||
forbiddenDataScopes: string[]
|
||||
frontendVersion: string
|
||||
healthCheckRef: string
|
||||
moduleIds: string[]
|
||||
rollbackVersion: string
|
||||
routeRef: string
|
||||
themePackageRef: string
|
||||
}
|
||||
|
||||
export interface VerifiedDomainManifest {
|
||||
public: PublicDomainVestibule
|
||||
provenance: DomainManifestProvenance
|
||||
runtime: DomainRuntimeDescriptor
|
||||
schema: 'gh-aios.domain-manifest/v1'
|
||||
verification: {
|
||||
manifestDigest: string
|
||||
state: 'verified'
|
||||
verifiedAt: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DomainSessionCapability {
|
||||
capabilityId: string
|
||||
domainId: string
|
||||
expiresAt: number
|
||||
nodeId: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
export interface DomainConnectionReceipt {
|
||||
connectionId: string
|
||||
domainId: string
|
||||
manifestDigest: string
|
||||
nodeId: string
|
||||
receiptId: string
|
||||
state: 'online'
|
||||
}
|
||||
|
||||
export interface LighthouseState {
|
||||
stage: 'lighthouse'
|
||||
}
|
||||
|
||||
export interface VestibuleState {
|
||||
domain: PublicDomainVestibule
|
||||
stage: 'vestibule'
|
||||
}
|
||||
|
||||
export interface AccessState {
|
||||
manifest: VerifiedDomainManifest
|
||||
nodeType: DomainNodeType
|
||||
receipt: DomainConnectionReceipt | null
|
||||
session: DomainSessionCapability | null
|
||||
stage: 'access'
|
||||
}
|
||||
|
||||
export interface RuntimeState {
|
||||
manifest: VerifiedDomainManifest
|
||||
mode: 'connected' | 'disconnected-readonly'
|
||||
nodeType: DomainNodeType
|
||||
receipt: DomainConnectionReceipt
|
||||
session: DomainSessionCapability
|
||||
stage: 'runtime'
|
||||
}
|
||||
|
||||
export type DomainAccessState = LighthouseState | VestibuleState | AccessState | RuntimeState
|
||||
|
||||
export class DomainContractError extends Error {
|
||||
readonly code: string
|
||||
|
||||
constructor(code: string) {
|
||||
super(code)
|
||||
this.code = code
|
||||
this.name = 'DomainContractError'
|
||||
}
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/
|
||||
const SOURCE_COMMIT = /^[a-f0-9]{40,64}$/
|
||||
const REQUIRED_ENTER_SCOPE = 'domain:enter'
|
||||
|
||||
function required(value: string, code: string): string {
|
||||
if (!value.trim()) throw new DomainContractError(code)
|
||||
return value
|
||||
}
|
||||
|
||||
function publicDomain(domain: PublicDomainVestibule): PublicDomainVestibule {
|
||||
required(domain.domainId, 'domain_manifest_public_id_missing')
|
||||
required(domain.formalName, 'domain_manifest_formal_name_missing')
|
||||
required(domain.displayName, 'domain_manifest_display_name_missing')
|
||||
required(domain.purpose, 'domain_manifest_public_purpose_missing')
|
||||
required(domain.responsibleParty, 'domain_manifest_responsible_party_missing')
|
||||
if (!domain.accessModes.length) {
|
||||
throw new DomainContractError('domain_manifest_access_modes_missing')
|
||||
}
|
||||
if (domain.accessModes.some(mode => mode !== 'LOCAL_TERMINAL_NODE' && mode !== 'CLOUD_RESIDENT_NODE')) {
|
||||
throw new DomainContractError('domain_manifest_access_mode_invalid')
|
||||
}
|
||||
if (domain.themePreview) {
|
||||
required(domain.themePreview.assetRef, 'domain_manifest_theme_preview_ref_missing')
|
||||
required(domain.themePreview.description, 'domain_manifest_theme_preview_description_missing')
|
||||
}
|
||||
return {
|
||||
accessModes: [...domain.accessModes],
|
||||
displayName: domain.displayName,
|
||||
domainId: domain.domainId,
|
||||
formalName: domain.formalName,
|
||||
publicStatus: domain.publicStatus,
|
||||
purpose: domain.purpose,
|
||||
responsibleParty: domain.responsibleParty,
|
||||
themePreview: domain.themePreview ? { ...domain.themePreview } : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function assertVerifiedDomainManifest(
|
||||
manifest: VerifiedDomainManifest,
|
||||
): VerifiedDomainManifest {
|
||||
if (manifest.schema !== 'gh-aios.domain-manifest/v1') {
|
||||
throw new DomainContractError('domain_manifest_schema_unsupported')
|
||||
}
|
||||
publicDomain(manifest.public)
|
||||
if (manifest.verification.state !== 'verified' || !Number.isFinite(manifest.verification.verifiedAt)) {
|
||||
throw new DomainContractError('domain_manifest_not_verified')
|
||||
}
|
||||
if (!SHA256.test(manifest.provenance.digest)
|
||||
|| manifest.verification.manifestDigest !== manifest.provenance.digest) {
|
||||
throw new DomainContractError('domain_manifest_digest_invalid')
|
||||
}
|
||||
if (!SOURCE_COMMIT.test(manifest.provenance.sourceCommit)) {
|
||||
throw new DomainContractError('domain_manifest_source_commit_invalid')
|
||||
}
|
||||
required(manifest.provenance.repositoryId, 'domain_manifest_repository_missing')
|
||||
required(manifest.provenance.signerId, 'domain_manifest_signer_missing')
|
||||
required(manifest.provenance.signature, 'domain_manifest_signature_missing')
|
||||
required(manifest.runtime.routeRef, 'domain_manifest_route_missing')
|
||||
required(manifest.runtime.themePackageRef, 'domain_manifest_theme_package_missing')
|
||||
required(manifest.runtime.healthCheckRef, 'domain_manifest_health_check_missing')
|
||||
required(manifest.runtime.rollbackVersion, 'domain_manifest_rollback_version_missing')
|
||||
return manifest
|
||||
}
|
||||
|
||||
export function projectPublicDomainVestibule(
|
||||
manifest: VerifiedDomainManifest,
|
||||
): PublicDomainVestibule {
|
||||
return publicDomain(assertVerifiedDomainManifest(manifest).public)
|
||||
}
|
||||
|
||||
export const INITIAL_DOMAIN_ACCESS_STATE: LighthouseState = { stage: 'lighthouse' }
|
||||
|
||||
export function selectPublicDomain(
|
||||
state: LighthouseState | VestibuleState,
|
||||
domain: PublicDomainVestibule,
|
||||
): VestibuleState {
|
||||
void state
|
||||
return { domain: publicDomain(domain), stage: 'vestibule' }
|
||||
}
|
||||
|
||||
export function beginDomainAccess(
|
||||
state: VestibuleState,
|
||||
manifest: VerifiedDomainManifest,
|
||||
nodeType: DomainNodeType,
|
||||
): AccessState {
|
||||
const verified = assertVerifiedDomainManifest(manifest)
|
||||
if (verified.public.domainId !== state.domain.domainId) {
|
||||
throw new DomainContractError('domain_manifest_selected_domain_mismatch')
|
||||
}
|
||||
if (!verified.public.accessModes.includes(nodeType)) {
|
||||
throw new DomainContractError('domain_node_type_not_allowed')
|
||||
}
|
||||
return {
|
||||
manifest: verified,
|
||||
nodeType,
|
||||
receipt: null,
|
||||
session: null,
|
||||
stage: 'access',
|
||||
}
|
||||
}
|
||||
|
||||
export function acceptDomainSession(
|
||||
state: AccessState,
|
||||
session: DomainSessionCapability,
|
||||
now: number,
|
||||
): AccessState {
|
||||
if (session.domainId !== state.manifest.public.domainId) {
|
||||
throw new DomainContractError('domain_session_domain_mismatch')
|
||||
}
|
||||
if (session.expiresAt <= now) {
|
||||
throw new DomainContractError('domain_session_expired')
|
||||
}
|
||||
if (!session.scopes.includes(REQUIRED_ENTER_SCOPE)) {
|
||||
throw new DomainContractError('domain_session_enter_scope_missing')
|
||||
}
|
||||
required(session.capabilityId, 'domain_session_capability_id_missing')
|
||||
required(session.nodeId, 'domain_session_node_id_missing')
|
||||
return { ...state, session: { ...session, scopes: [...session.scopes] } }
|
||||
}
|
||||
|
||||
export function acceptDomainConnectionReceipt(
|
||||
state: AccessState,
|
||||
receipt: DomainConnectionReceipt,
|
||||
): AccessState {
|
||||
if (!state.session) {
|
||||
throw new DomainContractError('domain_connection_session_missing')
|
||||
}
|
||||
if (receipt.domainId !== state.manifest.public.domainId) {
|
||||
throw new DomainContractError('domain_connection_domain_mismatch')
|
||||
}
|
||||
if (receipt.nodeId !== state.session.nodeId) {
|
||||
throw new DomainContractError('domain_connection_node_mismatch')
|
||||
}
|
||||
if (receipt.manifestDigest !== state.manifest.provenance.digest) {
|
||||
throw new DomainContractError('domain_connection_manifest_mismatch')
|
||||
}
|
||||
required(receipt.connectionId, 'domain_connection_id_missing')
|
||||
required(receipt.receiptId, 'domain_connection_receipt_id_missing')
|
||||
return { ...state, receipt: { ...receipt } }
|
||||
}
|
||||
|
||||
export function enterDomainRuntime(state: AccessState, now: number): RuntimeState {
|
||||
if (!state.session) throw new DomainContractError('domain_runtime_session_missing')
|
||||
if (!state.receipt) throw new DomainContractError('domain_runtime_connection_receipt_missing')
|
||||
if (state.session.expiresAt <= now) throw new DomainContractError('domain_session_expired')
|
||||
return {
|
||||
manifest: state.manifest,
|
||||
mode: 'connected',
|
||||
nodeType: state.nodeType,
|
||||
receipt: state.receipt,
|
||||
session: state.session,
|
||||
stage: 'runtime',
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectDomainRuntime(state: RuntimeState): RuntimeState {
|
||||
return { ...state, mode: 'disconnected-readonly' }
|
||||
}
|
||||
|
||||
export function returnToLighthouse(): LighthouseState {
|
||||
return INITIAL_DOMAIN_ACCESS_STATE
|
||||
}
|
||||
Loading…
Reference in a new issue