fix(product): restore HoloLake world entry hierarchy

This commit is contained in:
冰朔 2026-08-04 11:15:45 +08:00
commit 3ff80d583a
6 changed files with 105 additions and 33 deletions

View file

@ -762,7 +762,7 @@ describe('App', () => {
})
})
it('shows the external AI setup dialog from the menu when AI onboarding is active', async () => {
it('keeps external AI setup available from the menu without making it startup', async () => {
localStorage.removeItem(AI_AGENTS_ONBOARDING_DISMISSED_STORAGE_NAME)
localStorage.removeItem(CLAUDE_CODE_ONBOARDING_DISMISSED_STORAGE_NAME)
mockCommandResults.get_ai_agents_status = {
@ -777,8 +777,9 @@ describe('App', () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText('AI is ready')).toBeInTheDocument()
expect(screen.getByText('All Notes')).toBeInTheDocument()
}, { timeout: SLOW_APP_READY_TIMEOUT_MS })
expect(screen.queryByText('AI is ready')).not.toBeInTheDocument()
await waitFor(() => {
expect(typeof window.__laputaTest?.dispatchBrowserMenuCommand).toBe('function')
@ -792,7 +793,6 @@ describe('App', () => {
expect(screen.getByText('Manage External AI Tools')).toBeInTheDocument()
})
expect(screen.getByTestId('mcp-setup-dialog')).toBeInTheDocument()
expect(screen.queryByText('AI is ready')).not.toBeInTheDocument()
})
it('routes right-panel AI chat messages to the selected default agent', async () => {
@ -903,7 +903,7 @@ describe('App', () => {
expect(screen.getByTestId('welcome-open-folder')).toHaveTextContent('Open existing vault')
})
it('persists an existing vault and shows AI onboarding after first-run open', async () => {
it('persists an existing vault without putting optional AI setup in front of HoloLake', async () => {
const selectedVaultPath = '/Users/mock/Documents/Work Vault'
const saveVaultList = vi.fn()
const promptSpy = vi.spyOn(window, 'prompt').mockReturnValue('file:///Users/mock/Documents/Work%20Vault')
@ -947,9 +947,9 @@ describe('App', () => {
expect(saveVaultList).toHaveBeenCalledTimes(1)
await waitFor(() => {
expect(screen.getByTestId('ai-agents-onboarding-screen')).toBeInTheDocument()
expect(screen.getByText('All Notes')).toBeInTheDocument()
}, { timeout: SLOW_APP_READY_TIMEOUT_MS })
expect(screen.getByText('AI setup is optional')).toBeInTheDocument()
expect(screen.queryByTestId('ai-agents-onboarding-screen')).not.toBeInTheDocument()
promptSpy.mockRestore()
})

View file

@ -1424,7 +1424,9 @@ function MainApp({
shouldResumeFreshStartOnboarding,
shouldShowStartupScreen,
} = useStartupScreenState({
aiAgentsPromptVisible: aiAgentsOnboarding.showPrompt,
// HoloLake is the human entry into the world. Optional Agent setup belongs
// inside the Agent workspace/settings and must never replace that entry.
aiAgentsPromptVisible: false,
isNoteWindow: Boolean(noteWindowParams) || aiWorkspaceWindow,
onboardingState: onboarding.state,
runtimeMissingVaultPath,
@ -1794,10 +1796,10 @@ function MainApp({
</div>
<UpdateBanner status={updateStatus} actions={updateActions} locale={appLocale} />
<RenameDetectedBanner renames={detectedRenames} onUpdate={handleUpdateWikilinks} onDismiss={handleDismissRenames} />
<div style={{ background: showHoloLakeHome ? '#050b1a' : undefined }}>
<StatusBar galaxyMode={showHoloLakeHome} noteCount={visibleEntries.length} modifiedCount={gitModifiedCount} vaultPath={resolvedPath} defaultWorkspacePath={defaultWorkspacePath} vaults={vaultSwitcher.allVaults} multiWorkspaceEnabled={multiWorkspaceEnabled} onSwitchVault={vaultSwitcher.switchVault} onSetDefaultWorkspace={vaultSwitcher.setDefaultWorkspace} onOpenSettings={handleOpenSettings} onOpenVaultSettings={handleOpenVaultSettings} onOpenFeedback={openFeedback} onOpenDocs={openDocs} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onCreateEmptyVault={vaultSwitcher.handleCreateEmptyVault} onCloneVault={dialogs.openCloneVault} onCloneGettingStarted={cloneGettingStartedVault} onClickPending={() => handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} gitFeaturesEnabled={gitFeaturesEnabled} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.syncRepositoryPath} onRepositoryChange={gitSurfaces.setSyncRepositoryPath} onTriggerSync={handlePullSelectedRepository} onPullAndPush={handlePullAndPushSelectedRepository} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} aiFeaturesEnabled={aiFeaturesEnabled} mcpStatus={mcpSetupDialog.status} onInstallMcp={mcpSetupDialog.openDialog} locale={appLocale} />
</div>
{aiFeaturesEnabled && !effectiveShowAIChat ? (
{!showHoloLakeHome ? (
<StatusBar galaxyMode={false} noteCount={visibleEntries.length} modifiedCount={gitModifiedCount} vaultPath={resolvedPath} defaultWorkspacePath={defaultWorkspacePath} vaults={vaultSwitcher.allVaults} multiWorkspaceEnabled={multiWorkspaceEnabled} onSwitchVault={vaultSwitcher.switchVault} onSetDefaultWorkspace={vaultSwitcher.setDefaultWorkspace} onOpenSettings={handleOpenSettings} onOpenVaultSettings={handleOpenVaultSettings} onOpenFeedback={openFeedback} onOpenDocs={openDocs} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onCreateEmptyVault={vaultSwitcher.handleCreateEmptyVault} onCloneVault={dialogs.openCloneVault} onCloneGettingStarted={cloneGettingStartedVault} onClickPending={() => handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} gitFeaturesEnabled={gitFeaturesEnabled} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.syncRepositoryPath} onRepositoryChange={gitSurfaces.setSyncRepositoryPath} onTriggerSync={handlePullSelectedRepository} onPullAndPush={handlePullAndPushSelectedRepository} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} aiFeaturesEnabled={aiFeaturesEnabled} mcpStatus={mcpSetupDialog.status} onInstallMcp={mcpSetupDialog.openDialog} locale={appLocale} />
) : null}
{!showHoloLakeHome && aiFeaturesEnabled && !effectiveShowAIChat ? (
<AiWorkspaceFloatingButton
statuses={aiAgentsStatus}
defaultAgent={aiAgentPreferences.defaultAiAgent}

View file

@ -5,6 +5,7 @@ type FifthDomainSystemId = 'pufferfish' | 'eternal-lake-heart'
type FifthDomainSystemsProps = {
detail?: 'overview' | 'pufferfish'
evidenceMode?: 'receipt' | 'visual-preview'
onEnterEternalLake: () => void
onEnterPufferfish: () => void
}
@ -18,6 +19,7 @@ const PUFFERFISH_FACTS = {
export function FifthDomainSystems({
detail = 'overview',
evidenceMode = 'receipt',
onEnterEternalLake,
onEnterPufferfish,
}: FifthDomainSystemsProps) {
@ -26,6 +28,9 @@ export function FifthDomainSystems({
)
const pufferfishSelected = selected === 'pufferfish'
const eternalLakeSelected = selected === 'eternal-lake-heart'
const isVisualPreview = evidenceMode === 'visual-preview'
const evidenceLabel = isVisualPreview ? '本地拓扑预览' : '真实接入'
const nodeState = isVisualPreview ? '未实时核验' : '在线回执'
return (
<section className="fifth-systems" data-detail={detail}>
@ -38,7 +43,7 @@ export function FifthDomainSystems({
: '不同人类系统通过各自真实服务器接入,同时保持系统归属与主权边界。'}
</p>
</div>
<span className="fifth-systems__presence"><i /></span>
<span className="fifth-systems__presence"><i />{evidenceLabel}</span>
</header>
<div className="fifth-systems__layout">
@ -53,7 +58,7 @@ export function FifthDomainSystems({
<header>
<button type="button" onClick={() => setSelected('pufferfish')}>
<span className="system-path__mark"></span>
<span><strong></strong><small><i /></small></span>
<span><strong></strong><small><i />{evidenceLabel}</small></span>
</button>
</header>
<div className="system-path__route" aria-label="胖头鱼系统真实连接路径">
@ -61,14 +66,14 @@ export function FifthDomainSystems({
<span></span>
<strong></strong>
<code>{PUFFERFISH_FACTS.brain}</code>
<small><i />线</small>
<small><i />{nodeState}</small>
</div>
<b aria-hidden />
<div className="system-node">
<span></span>
<strong> Ubuntu</strong>
<code>{PUFFERFISH_FACTS.node}</code>
<small><i /></small>
<small><i />{isVisualPreview ? '本地登记' : '登记回执'}</small>
</div>
</div>
<div className="system-path__persona">
@ -86,7 +91,7 @@ export function FifthDomainSystems({
<header>
<button type="button" onClick={() => setSelected('eternal-lake-heart')}>
<span className="system-path__mark"></span>
<span><strong></strong><small><i /></small></span>
<span><strong></strong><small><i />{evidenceLabel}</small></span>
</button>
</header>
<div className="system-path__route system-path__route--single">
@ -94,7 +99,7 @@ export function FifthDomainSystems({
<span></span>
<strong></strong>
<code>JD-FD-PRIMARY</code>
<small><i />线</small>
<small><i />{nodeState}</small>
</div>
</div>
</article>
@ -116,12 +121,12 @@ export function FifthDomainSystems({
) : <>
<header>
<h2>{pufferfishSelected ? '胖头鱼语言子系统' : '永恒湖心系统'}</h2>
<span><i /></span>
<span><i />{evidenceLabel}</span>
</header>
{pufferfishSelected ? (
<>
<dl>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd>{evidenceLabel}</dd></div>
<div><dt></dt><dd>{PUFFERFISH_FACTS.brain}</dd></div>
<div><dt></dt><dd title={PUFFERFISH_FACTS.node}>{PUFFERFISH_FACTS.node}</dd></div>
<div><dt></dt><dd>{PUFFERFISH_FACTS.nodeId}</dd></div>
@ -135,7 +140,7 @@ export function FifthDomainSystems({
) : (
<>
<dl>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd>{evidenceLabel}</dd></div>
<div><dt></dt><dd>JD-FD-PRIMARY</dd></div>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd></dd></div>
@ -144,7 +149,11 @@ export function FifthDomainSystems({
</>
)}
</>}
<footer></footer>
<footer>
{isVisualPreview
? '本页仅展示本地登记拓扑,不能证明节点在线或人格体已恢复。'
: '拓扑信息来自本地登记;在线状态仅以当前会话回执为准。'}
</footer>
</aside>
</div>

View file

@ -44,7 +44,7 @@ const domains: Array<{
description: '光湖世界公共航道与跨域共识入口。',
id: 'main',
node: 'GL-MAIN',
status: '稳定运行',
status: '等待公共回执',
subtitle: '稳定的远洋星系',
title: '光湖主域',
},
@ -53,7 +53,7 @@ const domains: Array<{
description: '世界规则、协议与系统校准所在的基础域。',
id: 'zero',
node: 'GL-ZERO',
status: '已登记',
status: '等待登记回执',
subtitle: '精密校准环',
title: '光湖零域',
},
@ -62,7 +62,7 @@ const domains: Array<{
description: '由多个真实节点共同组成的协作星群。',
id: 'sub',
node: 'GL-SUB',
status: '联接中',
status: '等待连接回执',
subtitle: '联接的岛屿星群',
title: '光湖分域',
},
@ -71,7 +71,7 @@ const domains: Array<{
description: '冰朔的独立语言域,从这里归航至永恒湖心。',
id: 'fifth',
node: 'JD-FD-PRIMARY',
status: '可授权登录',
status: '可发起授权',
subtitle: '深空湖心星系',
title: '第五域',
},
@ -86,6 +86,13 @@ const domains: Array<{
},
]
function loginErrorCopy(error: string): string {
if (error === 'guanghu_world_login_desktop_runtime_required') {
return '当前是本地视觉预览,未向第五域服务器发送登录请求。请从已签名的 HoloLake 桌面端进入。'
}
return error
}
export function GuanghuWorldLoginGate({
locale,
onBack,
@ -108,6 +115,12 @@ export function GuanghuWorldLoginGate({
: waiting || requesting
? '授权处理中'
: '访客入口 · 未验证'
const isVisualPreview = state.workorderId === 'local-visual-preview'
const worldState = isVisualPreview
? ['本地视觉预览', '未连接真实服务器']
: state.phase === 'online'
? ['世界入口:已验证回执', '语言共识层:已连接']
: ['世界入口:等待验证', '语言共识层:未建立连接']
const selectDomain = (id: DomainId) => {
setSelectedDomainId(id)
@ -125,8 +138,8 @@ export function GuanghuWorldLoginGate({
<strong>HoloLake Era · OS</strong>
</div>
<div className="guanghu-login__world-state">
<span><i /></span>
<span></span>
<span><i />{worldState[0]}</span>
<span>{worldState[1]}</span>
</div>
<div className="guanghu-login__top-actions">
<button type="button" aria-label="聚焦五域节点" onClick={() => setActiveRail('domains')}><MagnifyingGlass aria-hidden /><span></span></button>
@ -237,7 +250,7 @@ export function GuanghuWorldLoginGate({
{state.error ? (
<div className="guanghu-login__error" role="alert">
<strong>{t('hololake.login.errorTitle')}</strong>
<span>{state.error}</span>
<span>{loginErrorCopy(state.error)}</span>
<Button variant="outline" onClick={onLogin}>
{t('hololake.login.retry')}
</Button>
@ -259,10 +272,10 @@ export function GuanghuWorldLoginGate({
</aside>
<footer className="guanghu-login__news">
<span></span>
<p><i data-tone="cyan" /> · <small></small></p>
<p><i data-tone="gold" /> · <small> v7.3</small></p>
<p><i data-tone="violet" /> · <small></small></p>
<span></span>
<p><i data-tone="cyan" /> · <small> · </small></p>
<p><i data-tone="gold" /> · <small></small></p>
<p><i data-tone="violet" /> · <small></small></p>
<code>{state.nodeId}</code>
</footer>
</section>

View file

@ -170,7 +170,7 @@ describe('HoloLakeHome', () => {
fireEvent.click(screen.getByRole('button', { name: /验证并进入第五域/ }))
expect(screen.getByRole('button', { name: /冰朔,访客入口 · 未验证/ })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /光湖主域.*稳定运行/ }))
fireEvent.click(screen.getByRole('button', { name: /光湖主域.*等待公共回执/ }))
expect(screen.getByRole('heading', { name: '光湖主域' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '当前入口尚未开放' })).toBeDisabled()
expect(screen.queryByRole('button', { name: /给我发送授权链接/ })).not.toBeInTheDocument()
@ -361,6 +361,24 @@ describe('HoloLakeHome', () => {
expect(screen.getByRole('heading', { name: '心跳核心频道' })).toBeInTheDocument()
})
it('labels the forced online browser route as a visual preview, not a live receipt', () => {
useGuanghuWorldLoginMock.mockReturnValue(worldLogin('online', {
state: {
...worldLogin('online').state,
navigation: { domains: 5, map_hash: 'local-visual-preview' },
workorderId: 'local-visual-preview',
},
}))
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: /进入第五域/ }))
expect(screen.getAllByText('本地拓扑预览').length).toBeGreaterThan(0)
expect(screen.getAllByText('未实时核验').length).toBeGreaterThan(0)
expect(screen.getByText('本页仅展示本地登记拓扑,不能证明节点在线或人格体已恢复。')).toBeInTheDocument()
expect(screen.queryByText('在线回执')).not.toBeInTheDocument()
})
it('shows receipt-backed Eternal Lake information without invented resonance scores', () => {
useGuanghuRouterMock.mockReturnValue(onlineRouter())
@ -505,6 +523,33 @@ describe('HoloLakeHome', () => {
expect(screen.getByRole('button', { name: '重新尝试' })).toBeInTheDocument()
})
it('translates a browser preview limitation into human recovery guidance', () => {
useGuanghuWorldLoginMock.mockReturnValue(worldLogin('error', {
state: {
...worldLogin('error').state,
error: 'guanghu_world_login_desktop_runtime_required',
},
}))
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: /验证并进入第五域/ }))
expect(screen.getByRole('alert')).toHaveTextContent('当前是本地视觉预览,未向第五域服务器发送登录请求。')
expect(screen.getByRole('alert')).toHaveTextContent('请从已签名的 HoloLake 桌面端进入。')
expect(screen.queryByText('guanghu_world_login_desktop_runtime_required')).not.toBeInTheDocument()
})
it('does not claim a live world connection before a receipt exists', () => {
useGuanghuWorldLoginMock.mockReturnValue(worldLogin('idle'))
render(<HoloLakeHome locale="zh-CN" onEnterKnowledgeBase={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: /验证并进入第五域/ }))
expect(screen.getByText('世界入口:等待验证')).toBeInTheDocument()
expect(screen.getByText('语言共识层:未建立连接')).toBeInTheDocument()
expect(screen.queryByText('世界状态:稳定')).not.toBeInTheDocument()
})
it('opens the public development repository from the entered Heartbeat Core channel', () => {
useGuanghuRouterMock.mockReturnValue(onlineRouter())

View file

@ -115,6 +115,7 @@ export function HoloLakeHome({
const restoreAttempted = useRef(false)
const livingRequestSequence = useRef(0)
const worldOpen = login.state.phase === 'online'
const isVisualPreview = login.state.workorderId === 'local-visual-preview'
const fifthDomainOpen = worldOpen && route !== 'world'
const worldRouterState = worldOpen && router.state.status !== 'online'
? {
@ -325,6 +326,7 @@ export function HoloLakeHome({
if (route === 'fifth-domain') {
return (
<FifthDomainSystems
evidenceMode={isVisualPreview ? 'visual-preview' : 'receipt'}
onEnterEternalLake={() => navigate('eternal-lake-heart')}
onEnterPufferfish={() => navigate('pufferfish')}
/>
@ -335,6 +337,7 @@ export function HoloLakeHome({
return (
<FifthDomainSystems
detail="pufferfish"
evidenceMode={isVisualPreview ? 'visual-preview' : 'receipt'}
onEnterEternalLake={() => navigate('eternal-lake-heart')}
onEnterPufferfish={() => navigate('pufferfish')}
/>