From b24d5b54525fd06678c10322c4870b374f2c3c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Mon, 10 Aug 2026 05:59:17 +0800 Subject: [PATCH] feat: bind public vestibules to domain login gates --- .../guanghu-knowledge-base/src/App.tsx | 38 ++++++++++++++----- .../src/components/DomainConnectionSheet.tsx | 29 +++++++++----- .../src/components/WorldEntry.tsx | 7 ++-- .../src/domain-entry-state.test.ts | 30 +++++++++++++++ .../src/domain-entry-state.ts | 36 ++++++++++++++++++ .../guanghu-knowledge-base/src/styles/app.css | 29 +++++++++++++- 6 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 product-source/guanghu-knowledge-base/src/domain-entry-state.test.ts create mode 100644 product-source/guanghu-knowledge-base/src/domain-entry-state.ts diff --git a/product-source/guanghu-knowledge-base/src/App.tsx b/product-source/guanghu-knowledge-base/src/App.tsx index f13e551..6315b6f 100644 --- a/product-source/guanghu-knowledge-base/src/App.tsx +++ b/product-source/guanghu-knowledge-base/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react'; import { api, DocTreeNode, DocContent, type ChannelState, type ModuleManifest } from './api'; import { DocTree } from './components/DocTree'; import { Editor } from './components/Editor'; @@ -13,6 +13,7 @@ import { ModuleLibrarySheet } from './components/ModuleLibrarySheet'; import { cleanDisplayText } from './presentation'; import { WorldEntry } from './components/WorldEntry'; import { DomainConnectionSheet } from './components/DomainConnectionSheet'; +import { createDomainEntryTarget, type DomainEntryTarget, type DomainNodeType } from './domain-entry-state'; import type { DomainRouteId } from './public-domain-directory'; type View = 'editor' | 'history'; @@ -84,6 +85,7 @@ export default function App() { const [activeModule, setActiveModule] = useState('knowledge'); const [storageSheetOpen, setStorageSheetOpen] = useState(false); const [domainConnectionOpen, setDomainConnectionOpen] = useState(false); + const [domainEntryTarget, setDomainEntryTarget] = useState(null); const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>(); const [serverSession, setServerSession] = useState({ authenticated: false, nodeId: '' }); const [serverProfiles, setServerProfiles] = useState([]); @@ -101,6 +103,7 @@ export default function App() { const [lastChannelReceipt, setLastChannelReceipt] = useState(''); const [worldEntered, setWorldEntered] = useState(false); const [domainAccess, setDomainAccess] = useState({ blockers: [], runtimeReady: false, stage: 'checking' }); + const domainAccessRequest = useRef(0); const storageMode = repositoryStatus?.remote ? 'server' : 'local'; const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机'; @@ -176,19 +179,35 @@ export default function App() { } }, []); - const refreshDomainAccess = useCallback(async () => { + const refreshDomainAccess = useCallback(async (domainId = 'DOM-FIFTH-0001') => { + const request = ++domainAccessRequest.current; const server = (window as any).hololake?.server; if (!server?.domainAccess) { - setDomainAccess({ blockers: ['desktop_runtime_required'], runtimeReady: false, stage: 'login-required' }); + if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['desktop_runtime_required'], domainId, runtimeReady: false, stage: 'login-required' }); return; } try { - setDomainAccess(await server.domainAccess('DOM-FIFTH-0001')); + const result = await server.domainAccess(domainId); + if (request === domainAccessRequest.current) setDomainAccess(result); } catch { - setDomainAccess({ blockers: ['domain_access_probe_failed'], runtimeReady: false, stage: 'login-required' }); + if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['domain_access_probe_failed'], domainId, runtimeReady: false, stage: 'login-required' }); } }, []); + const openDomainConnection = useCallback((routeId: DomainRouteId | null) => { + const target = routeId ? createDomainEntryTarget(routeId) : null; + setDomainEntryTarget(target); + setDomainConnectionOpen(true); + if (target) { + setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, runtimeReady: false, stage: 'checking' }); + void refreshDomainAccess(target.domain.stableDomainId); + } + }, [refreshDomainAccess]); + + const selectDomainNodeType = useCallback((nodeType: DomainNodeType) => { + setDomainEntryTarget(current => current ? createDomainEntryTarget(current.domain.routeId, nodeType) : null); + }, []); + const refreshChannel = useCallback(async () => { try { const [channel, registry] = await Promise.all([api.getChannel(), api.getModules()]); @@ -393,9 +412,7 @@ export default function App() { setActiveModule('knowledge'); setWorldEntered(true); }} - onOpenConnection={() => { - setDomainConnectionOpen(true); - }} + onOpenConnection={openDomainConnection} /> )}
setDomainConnectionOpen(false)} + onSelectTarget={routeId => openDomainConnection(routeId)} + onSelectNodeType={selectDomainNodeType} onEnterRuntime={() => { - if (!domainAccess.runtimeReady) return; + if (!domainEntryTarget || domainEntryTarget.domain.routeId !== 'fifth' || !domainAccess.runtimeReady || domainAccess.domainId !== domainEntryTarget.domain.stableDomainId) return; setActiveRoute('fifth'); setActiveModule('knowledge'); setWorldEntered(true); diff --git a/product-source/guanghu-knowledge-base/src/components/DomainConnectionSheet.tsx b/product-source/guanghu-knowledge-base/src/components/DomainConnectionSheet.tsx index f8b417f..e3f7041 100644 --- a/product-source/guanghu-knowledge-base/src/components/DomainConnectionSheet.tsx +++ b/product-source/guanghu-knowledge-base/src/components/DomainConnectionSheet.tsx @@ -1,17 +1,23 @@ import { Cloud, Laptop, ShieldCheck, X } from 'lucide-react'; import { projectDomainConnectionSteps, projectDomainTrustSource, type DomainAccessProjection } from '../domain-connection'; +import { canEnterSelectedDomainRuntime, projectDomainRuntimeBoundary, type DomainEntryTarget, type DomainNodeType } from '../domain-entry-state'; +import { publicDomainDirectory, type DomainRouteId } from '../public-domain-directory'; interface Props { access: DomainAccessProjection; + target: DomainEntryTarget | null; open: boolean; onClose: () => void; + onSelectTarget: (routeId: DomainRouteId) => void; + onSelectNodeType: (nodeType: DomainNodeType) => void; onEnterRuntime: () => void; onOpenCodeChannel: () => void; } -export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, onOpenCodeChannel }: Props) { +export function DomainConnectionSheet({ access, target, open, onClose, onSelectTarget, onSelectNodeType, onEnterRuntime, onOpenCodeChannel }: Props) { if (!open) return null; const steps = projectDomainConnectionSteps(access); + const canEnter = canEnterSelectedDomainRuntime(target, access); return (
{ @@ -20,18 +26,22 @@ export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, o
- DOM-FIFTH-0001 -

接入第五域 · 光湖本源域

+ {target?.domain.stableDomainId ?? 'SELECT-DOMAIN'} +

{target ? `接入${target.domain.displayName}` : '选择要接入的域'}

进入域不是切换页面。系统必须验证账户、登记节点、目标域授权、会话能力和在线回执。

+
+ {publicDomainDirectory.map(domain => )} +
+ {!target ?

{projectDomainRuntimeBoundary(target, access)}

: <>

{projectDomainTrustSource(access)}

-
-
+ +
    @@ -44,14 +54,15 @@ export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, o ))}
-

代码频道账号只用于仓库与同步,是节点身份的辅助证明之一,不能单独签发第五域权限。

+

{projectDomainRuntimeBoundary(target, access)} 代码频道账号只用于仓库与同步,不能单独签发域权限。

+ }
- + -
diff --git a/product-source/guanghu-knowledge-base/src/components/WorldEntry.tsx b/product-source/guanghu-knowledge-base/src/components/WorldEntry.tsx index a564bc5..147a75a 100644 --- a/product-source/guanghu-knowledge-base/src/components/WorldEntry.tsx +++ b/product-source/guanghu-knowledge-base/src/components/WorldEntry.tsx @@ -6,7 +6,7 @@ interface Props { access: { blockers: string[]; runtimeReady: boolean; stage: 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready' }; onEnterFifthRuntime: () => void; onEnterLocalWorkspace: () => void; - onOpenConnection: () => void; + onOpenConnection: (routeId: DomainRouteId | null) => void; } export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace, onOpenConnection }: Props) { @@ -55,7 +55,7 @@ export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace,
{access.stage === 'runtime-ready' ? '节点、域清单、会话能力与连接回执已验证' : access.stage === 'identity-verified' ? '账户与节点已验证 · 尚缺域清单、会话能力和连接回执' : access.stage === 'checking' ? '正在核对真实接入证据' : '公开门厅可查看 · 尚未登录或接入节点'}
- +
@@ -82,7 +82,7 @@ export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace, - +
@@ -106,6 +106,7 @@ export function WorldEntry({ access, onEnterFifthRuntime, onEnterLocalWorkspace, {selectedVestibule.stableDomainId} {selectedVestibule.themeLabel} 仅公开门厅资料 · 私有资源未装载 + )} diff --git a/product-source/guanghu-knowledge-base/src/domain-entry-state.test.ts b/product-source/guanghu-knowledge-base/src/domain-entry-state.test.ts new file mode 100644 index 0000000..2901e47 --- /dev/null +++ b/product-source/guanghu-knowledge-base/src/domain-entry-state.test.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { canEnterSelectedDomainRuntime, createDomainEntryTarget, projectDomainRuntimeBoundary } from './domain-entry-state.js'; + +const ready = (domainId: string) => ({ blockers: [], domainId, runtimeReady: true, stage: 'runtime-ready' as const }); + +test('a generic login request does not silently target the Fifth Domain', () => { + assert.equal(canEnterSelectedDomainRuntime(null, ready('DOM-FIFTH-0001')), false); + assert.match(projectDomainRuntimeBoundary(null, ready('DOM-FIFTH-0001')), /先选择目标域/); +}); + +test('the selected stable domain id must match the returned access evidence', () => { + const fifth = createDomainEntryTarget('fifth', 'cloud-resident'); + assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOMAIN-MAIN')), false); + assert.match(projectDomainRuntimeBoundary(fifth, ready('DOMAIN-MAIN')), /另一个域/); +}); + +test('enterprise vestibules cannot reuse the Fifth Domain renderer', () => { + const main = createDomainEntryTarget('main'); + assert.equal(main.domain.stableDomainId, 'DOMAIN-MAIN'); + assert.equal(main.nodeType, 'local-terminal'); + assert.equal(canEnterSelectedDomainRuntime(main, ready('DOMAIN-MAIN')), false); + assert.match(projectDomainRuntimeBoundary(main, ready('DOMAIN-MAIN')), /独立运行端点与主题包尚未登记/); +}); + +test('the Fifth Domain opens only with matching runtime-ready evidence', () => { + const fifth = createDomainEntryTarget('fifth', 'cloud-resident'); + assert.equal(canEnterSelectedDomainRuntime(fifth, ready('DOM-FIFTH-0001')), true); + assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001')), /已经匹配/); +}); diff --git a/product-source/guanghu-knowledge-base/src/domain-entry-state.ts b/product-source/guanghu-knowledge-base/src/domain-entry-state.ts new file mode 100644 index 0000000..70c6495 --- /dev/null +++ b/product-source/guanghu-knowledge-base/src/domain-entry-state.ts @@ -0,0 +1,36 @@ +import { getPublicDomainVestibule, type DomainRouteId, type PublicDomainVestibule } from './public-domain-directory.js'; +import type { DomainAccessProjection } from './domain-connection.js'; + +export type DomainNodeType = 'local-terminal' | 'cloud-resident'; + +export interface DomainEntryTarget { + domain: PublicDomainVestibule; + nodeType: DomainNodeType; +} + +export function createDomainEntryTarget(routeId: DomainRouteId, nodeType: DomainNodeType = 'local-terminal'): DomainEntryTarget { + const domain = getPublicDomainVestibule(routeId); + if (!domain.supportedNodeTypes.includes(nodeType)) throw new Error('DOMAIN_NODE_TYPE_NOT_SUPPORTED'); + return Object.freeze({ domain, nodeType }); +} + +export function canEnterSelectedDomainRuntime(target: DomainEntryTarget | null, access: DomainAccessProjection): boolean { + if (!target || !access.runtimeReady) return false; + if (access.domainId !== target.domain.stableDomainId) return false; + // The present desktop bundle contains only the Fifth Domain renderer. The four + // enterprise domains must provide their own signed runtime package and endpoint. + return target.domain.routeId === 'fifth'; +} + +export function projectDomainRuntimeBoundary(target: DomainEntryTarget | null, access: DomainAccessProjection): string { + if (!target) return '请先选择目标域;系统不会把通用登录请求默认路由到第五域。'; + if (access.domainId && access.domainId !== target.domain.stableDomainId) { + return '回读证据属于另一个域;当前入口保持关闭。'; + } + if (target.domain.routeId !== 'fifth') { + return `${target.domain.displayName}的独立运行端点与主题包尚未登记;当前只能查看公开门厅。`; + } + return access.runtimeReady + ? '第五域运行端点、签名清单、会话能力与在线回执已经匹配。' + : '第五域入口保持关闭,直到四项真实接入证据全部匹配。'; +} diff --git a/product-source/guanghu-knowledge-base/src/styles/app.css b/product-source/guanghu-knowledge-base/src/styles/app.css index 3798a7c..441aa29 100644 --- a/product-source/guanghu-knowledge-base/src/styles/app.css +++ b/product-source/guanghu-knowledge-base/src/styles/app.css @@ -3142,6 +3142,28 @@ select:focus-visible { line-height: 1.65; } +.domain-target-picker { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 6px; + padding: 14px 20px 0; +} + +.domain-target-picker button { + display: grid; + gap: 4px; + min-height: 52px; + padding: 8px 6px; + border: 1px solid var(--lake-border-soft); + border-radius: 9px; + background: rgba(7, 17, 29, .62); + color: var(--lake-text-soft); + font-size: 9px; +} + +.domain-target-picker button span { color: var(--lake-muted); } +.domain-target-picker button[data-selected="true"] { border-color: var(--lake-accent); background: var(--lake-accent-soft); color: var(--lake-text); } + .domain-node-modes { display: grid; grid-template-columns: 1fr 1fr; @@ -3149,15 +3171,19 @@ select:focus-visible { padding: 16px 20px; } -.domain-node-modes article { +.domain-node-modes button { display: flex; gap: 11px; padding: 13px; border: 1px solid var(--lake-border-soft); border-radius: 11px; background: rgba(7, 17, 29, .62); + color: var(--lake-text); + text-align: left; } +.domain-node-modes button[data-selected="true"] { border-color: var(--lake-accent); background: var(--lake-accent-soft); } + .domain-node-modes svg { width: 19px; color: var(--lake-accent); @@ -3224,6 +3250,7 @@ select:focus-visible { } @media (max-width: 660px) { + .domain-target-picker { grid-template-columns: 1fr 1fr; } .domain-node-modes { grid-template-columns: 1fr; } .domain-connection-sheet .storage-sheet-footer { flex-wrap: wrap; } }