hololake-system-architecture/product-source/hololake-native-desktop/src/main.tsx

555 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { StrictMode, useCallback, useEffect, useMemo, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { invoke } from '@tauri-apps/api/core'
import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type Panel = 'channel' | 'pncc' | 'receipts' | 'updates' | 'settings' | null
interface HomeStatus {
schema: string
directLocalBrokerState: string
directConnectionCount: number
resumableSessionCount: number
codeRepositoryMountCount: number
pnccReceiptCount: number
updateState: string
releaseRecoveryState: string
automaticUpstreamUpdates: boolean
mcpRole: string
}
interface DiscoveryTicketReceipt {
schema: string
state: string
accountKey: string
laneId: string
clientInstanceId: string
discoveryTicket: string
issuedAtUnixMs: number
receiptId: string
}
interface ReceiptEvent {
sequence: number
kind: string
observedAtUnixMs: number
eventHash: string
}
interface ReceiptProjection {
events: ReceiptEvent[]
returnedEventCount: number
lastSequence: number
}
interface RepositoryInspection {
personaId: string
humanResponsibilitySubject: string
repositoryPath: string
gitHead: string
repositoryClean: boolean
currentCheckpoint: { relativePath: string; sha256: string }
modelInferenceStarted: boolean
realityExecutionAllowed: boolean
}
interface RepositoryCandidate {
schema: string
state: string
candidateId: string
confirmationToken: string
expiresAtUnixMs: number
inspection: RepositoryInspection
}
interface ReleaseCandidate {
candidateId: string
confirmationToken: string
releaseId: string
currentVersion: string
version: string
notes: string
features: string[]
fixes: string[]
minimumVersion: string
dataMigrationRequired: boolean
compatibilityNotes?: string
restartMessage?: string
rollbackSupported: boolean
previousVersion?: string
expiresAtUnixMs: number
}
interface ReleaseCheckReceipt {
state: string
candidate?: ReleaseCandidate
}
interface ServerPnccProjection {
schema: string
state: string
personaId: string
humanResponsibilitySubject: string
nodeId: string
bootId: string
gitHead: string
carrierBindingState: string
personaCarrierBound: boolean
modelInferenceStarted: boolean
realityExecutionAllowed: boolean
primaryLeaseHeld: boolean
eventCount: number
eventChainHead: string
observedAtUnixMs: number
transport: string
repositoryContentExposed: boolean
writeAuthority: boolean
}
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' },
{ id: 'dawn', name: '晨湖曦光' },
{ id: 'nebula', name: '星云紫夜' },
{ id: 'candle', name: '烛畔暖湖' },
{ id: 'clear', name: '清浅澄湖' },
]
const recoveryPreviewStates = [
'NONE',
'AWAITING_HUMAN_HEALTH_CONFIRMATION',
'ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT',
'INSTALL_FAILED_BACKUP_RETAINED',
'INSTALL_INTERRUPTED_BACKUP_RETAINED',
'INSTALL_REPLACED_AWAITING_MANUAL_RESTART',
'ROLLBACK_REPLACED_AWAITING_MANUAL_RESTART',
'RECOVERY_STATE_UNREADABLE_ACTION_REQUIRED',
]
const requestedRecoveryPreview = import.meta.env.DEV
? new URLSearchParams(window.location.search).get('releaseRecoveryState')
: null
const recoveryPreviewState = requestedRecoveryPreview && recoveryPreviewStates.includes(requestedRecoveryPreview)
? requestedRecoveryPreview
: 'NONE'
const previewStatus: HomeStatus = {
schema: 'hololake.home-status/preview',
directLocalBrokerState: 'PREVIEW',
directConnectionCount: 0,
resumableSessionCount: 0,
codeRepositoryMountCount: 0,
pnccReceiptCount: 0,
updateState: 'UNPROVISIONED_FAIL_CLOSED',
releaseRecoveryState: recoveryPreviewState,
automaticUpstreamUpdates: false,
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
}
function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key)
if (existing) return existing
const generated = `${prefix}-${crypto.randomUUID()}`
window.localStorage.setItem(key, generated)
return generated
}
function Icon({ name }: { name: 'settings' | 'arrow' | 'receipt' | 'close' | 'copy' }) {
const paths = {
settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-1.9 1.9-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.04 1.56V20h-2.7v-.08a1.7 1.7 0 0 0-1.05-1.56 1.7 1.7 0 0 0-1.88.34l-.06.06-1.9-1.9.06-.06A1.7 1.7 0 0 0 7.72 15a1.7 1.7 0 0 0-1.56-1.04H6v-2.7h.08A1.7 1.7 0 0 0 7.64 10a1.7 1.7 0 0 0-.34-1.88l-.06-.06 1.9-1.9.06.06a1.7 1.7 0 0 0 1.88.34A1.7 1.7 0 0 0 12.12 5V5h2.7v.08a1.7 1.7 0 0 0 1.04 1.56 1.7 1.7 0 0 0 1.88-.34l.06-.06 1.9 1.9-.06.06a1.7 1.7 0 0 0-.34 1.88 1.7 1.7 0 0 0 1.56 1.04H21v2.7h-.08A1.7 1.7 0 0 0 19.4 15Z"/></>,
arrow: <><path d="M5 12h13"/><path d="m14 7 5 5-5 5"/></>,
receipt: <><path d="M7 3h10v18l-2.5-1.5L12 21l-2.5-1.5L7 21Z"/><path d="M10 8h4M10 12h4"/></>,
close: <><path d="m7 7 10 10M17 7 7 17"/></>,
copy: <><rect x="8" y="8" width="10" height="11" rx="2"/><path d="M15 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2"/></>,
}
return <svg viewBox="0 0 24 24" aria-hidden="true">{paths[name]}</svg>
}
function HoloLakeApp() {
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
const [status, setStatus] = useState<HomeStatus>(previewStatus)
const [panel, setPanel] = useState<Panel>(null)
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
const [repositoryCandidate, setRepositoryCandidate] = useState<RepositoryCandidate | null>(null)
const [repositoryBusy, setRepositoryBusy] = useState(false)
const [repositoryMessage, setRepositoryMessage] = useState('')
const [releaseCandidate, setReleaseCandidate] = useState<ReleaseCandidate | null>(null)
const [releaseBusy, setReleaseBusy] = useState(false)
const [releaseMessage, setReleaseMessage] = useState('')
const [message, setMessage] = useState('')
const [serverPncc, setServerPncc] = useState<ServerPnccProjection | null>(null)
const [serverPnccBusy, setServerPnccBusy] = useState(false)
const [serverPnccReadback, setServerPnccReadback] = useState<'WAITING' | 'LIVE' | 'UNAVAILABLE'>('WAITING')
const refreshStatus = useCallback(async () => {
try {
setStatus(await invoke<HomeStatus>('get_hololake_home_status'))
} catch {
setStatus(previewStatus)
}
}, [])
useEffect(() => {
void refreshStatus()
const timer = window.setInterval(() => void refreshStatus(), 3000)
return () => window.clearInterval(timer)
}, [refreshStatus])
const refreshServerPncc = useCallback(async () => {
setServerPnccBusy(true)
try {
const projection = await invoke<ServerPnccProjection>('query_jd_pncc_server_projection')
setServerPncc(projection)
setServerPnccReadback('LIVE')
} catch {
setServerPncc(null)
setServerPnccReadback('UNAVAILABLE')
} finally {
setServerPnccBusy(false)
}
}, [])
useEffect(() => {
void refreshServerPncc()
const timer = window.setInterval(() => void refreshServerPncc(), 15000)
return () => window.clearInterval(timer)
}, [refreshServerPncc])
useEffect(() => {
document.documentElement.dataset.theme = theme
window.localStorage.setItem('hololake-theme', theme)
}, [theme])
const connectionState = useMemo(() => {
if (status.directConnectionCount > 0) return { label: '已连接', detail: `${status.directConnectionCount} 个直连通道在线`, tone: 'ready' }
if (status.resumableSessionCount > 0) return { label: '可续接', detail: `${status.resumableSessionCount} 个会话等待续接`, tone: 'waiting' }
return { label: '等待连接', detail: '本机直连核心已就绪', tone: 'quiet' }
}, [status])
const releaseRecoveryIdle = useMemo(() => [
'NONE',
'HEALTH_CONFIRMED_BACKUP_REMOVED',
'ROLLBACK_CONFIRMED_BACKUP_REMOVED',
'INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED',
].includes(status.releaseRecoveryState), [status.releaseRecoveryState])
const releaseRecoveryNeedsAttention = !releaseRecoveryIdle
const openReceipts = async () => {
setPanel('receipts')
await loadReceipts()
}
const loadReceipts = async () => {
try {
const projection = await invoke<ReceiptProjection>('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } })
setReceipts(projection.events)
} catch {
setReceipts([])
}
}
const chooseRepository = async () => {
setRepositoryBusy(true)
setRepositoryMessage('')
try {
const candidate = await invoke<RepositoryCandidate | null>('select_pncc_repository_candidate')
setRepositoryCandidate(candidate)
if (!candidate) setRepositoryMessage('已取消选择,没有写入任何绑定。')
} catch (error) {
setRepositoryCandidate(null)
setRepositoryMessage(`这个文件夹不能作为代码通道:${String(error)}`)
} finally {
setRepositoryBusy(false)
}
}
const confirmRepository = async () => {
if (!repositoryCandidate) return
setRepositoryBusy(true)
setRepositoryMessage('')
try {
await invoke('confirm_pncc_repository_mount', {
input: {
candidateId: repositoryCandidate.candidateId,
confirmationToken: repositoryCandidate.confirmationToken,
},
})
setRepositoryCandidate(null)
setRepositoryMessage('代码仓库已按当前固定提交绑定为只读通道。')
await Promise.all([refreshStatus(), loadReceipts()])
} catch (error) {
setRepositoryMessage(`绑定没有写入:${String(error)}`)
} finally {
setRepositoryBusy(false)
}
}
const checkUpdate = async () => {
setReleaseBusy(true)
setReleaseMessage('')
try {
const receipt = await invoke<ReleaseCheckReceipt>('check_hololake_update')
setReleaseCandidate(receipt.candidate || null)
if (!receipt.candidate) setReleaseMessage('当前已经是最新版本。')
} catch (error) {
const reason = String(error)
setReleaseCandidate(null)
setReleaseMessage(reason.includes('UNPROVISIONED_NO_NETWORK_REQUEST')
? '京东主控的正式发布地址和更新公钥还没有登记。HoloLake 保持离线,不会向任何上游检查更新。'
: `更新检查没有完成:${reason}`)
} finally {
setReleaseBusy(false)
}
}
const confirmUpdateInstall = async () => {
if (!releaseCandidate) return
setReleaseBusy(true)
setReleaseMessage('正在重新核对广播并验证安装包;完成后由你手动重启 HoloLake。')
try {
await invoke('confirm_hololake_update_install', {
input: {
candidateId: releaseCandidate.candidateId,
confirmationToken: releaseCandidate.confirmationToken,
},
})
setReleaseCandidate(null)
setReleaseMessage('签名安装包已验证并安装。请在准备好后手动重启 HoloLake。')
await refreshStatus()
} catch (error) {
setReleaseMessage(`更新没有安装:${String(error)}`)
} finally {
setReleaseBusy(false)
}
}
const resolveReleaseRecovery = async (action: 'keep' | 'rollback') => {
setReleaseBusy(true)
setReleaseMessage(action === 'rollback' ? '正在复核上一版签名并恢复;完成后仍由你手动重启。' : '正在确认当前版本健康并清理上一版副本。')
try {
if (action === 'rollback') {
await invoke('rollback_hololake_update')
setReleaseMessage('上一版已恢复到应用位置。请手动重启 HoloLake启动后再确认恢复正常。')
} else {
await invoke('confirm_hololake_release_health')
setReleaseMessage('当前运行版本已由你确认,临时回退副本已经清理。')
}
await refreshStatus()
} catch (error) {
setReleaseMessage(`没有改变当前应用:${String(error)}`)
} finally {
setReleaseBusy(false)
}
}
const issueInvitation = async () => {
setMessage('')
try {
const receipt = await invoke<DiscoveryTicketReceipt>('issue_direct_local_discovery_ticket', {
input: {
accountId: getOrCreateLocalId('hololake-local-account', 'human-local'),
laneId: 'personal-channel',
clientInstanceId: getOrCreateLocalId('hololake-external-ai-client', 'external-ai'),
},
})
setTicket(receipt)
setMessage('连接邀请已生成。它只在这台电脑上有效。')
} catch (error) {
setMessage(`暂时无法生成连接邀请:${String(error)}`)
}
}
const invitationText = ticket ? JSON.stringify({
schema: 'hololake.direct-local-invitation/v1',
transport: 'INSTALLED_HOLOLAKE_EXECUTABLE_WITH_CONNECTOR_FLAG',
connectorArgument: '--connector',
openSession: {
operation: 'OPEN_SESSION',
input: {
accountId: window.localStorage.getItem('hololake-local-account'),
laneId: ticket.laneId,
clientInstanceId: ticket.clientInstanceId,
discoveryTicket: ticket.discoveryTicket,
},
},
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
}, null, 2) : ''
const copyInvitation = async () => {
try {
await navigator.clipboard.writeText(invitationText)
setMessage('已复制。把它交给桌面上的编程 AI 即可。')
} catch {
setMessage('复制没有成功,请选中下方邀请内容后复制。')
}
}
return (
<div className="world-shell">
<div className="scene-image" aria-hidden="true" />
<div className="nebula nebula-one" aria-hidden="true" />
<div className="nebula nebula-two" aria-hidden="true" />
<div className="mist" aria-hidden="true" />
<div className="stars" aria-hidden="true">{Array.from({ length: 18 }, (_, index) => <i key={index} style={{ '--star-index': index } as React.CSSProperties} />)}</div>
<div className="glimmers" aria-hidden="true">{Array.from({ length: 11 }, (_, index) => <i key={index} style={{ '--glimmer-index': index } as React.CSSProperties} />)}</div>
<header className="world-header">
<div className="world-mark">
<span className="world-name"></span>
<span className="world-route"></span>
</div>
<button className="icon-button" type="button" aria-label="打开设置" onClick={() => setPanel('settings')}><Icon name="settings" /></button>
</header>
<main className="home-content">
<section className="hero" aria-labelledby="home-title">
<p className="route-kicker">HOLOLAKE · PERSONAL CHANNEL</p>
<h1 id="home-title"><br /></h1>
<p className="hero-copy"> AI HoloLake 线</p>
<div className="hero-actions">
<button className="primary-action" type="button" onClick={() => setPanel('channel')}>
{status.directConnectionCount > 0 ? '已连接 · 查看通道' : '连接编程 AI'}
<Icon name="arrow" />
</button>
<button className="secondary-action" type="button" onClick={() => void openReceipts()}><Icon name="receipt" /></button>
</div>
</section>
<section className="status-cluster" aria-label="光湖运行状态">
<article className="status-item">
<span className={`status-light ${connectionState.tone}`} aria-hidden="true" />
<div><h2></h2><p>{connectionState.detail}</p></div>
</article>
<button className="status-item status-button" type="button" onClick={() => setPanel('pncc')}>
<span className={`status-light ${serverPnccReadback === 'LIVE' ? 'ready' : serverPnccReadback === 'WAITING' ? 'waiting' : 'quiet'}`} aria-hidden="true" />
<div><h2></h2><p>{serverPnccReadback === 'LIVE' ? '京东主控已驻留 · 载体保持分离' : serverPnccReadback === 'WAITING' ? '正在回读京东主控' : '主控暂时不可回读'}</p></div>
</button>
<button className="status-item status-button" type="button" onClick={() => setPanel('updates')}>
<span className={`status-light ${releaseRecoveryNeedsAttention ? 'waiting' : status.updateState.startsWith('READY') ? 'ready' : 'quiet'}`} aria-hidden="true" />
<div><h2>广</h2><p>{releaseRecoveryNeedsAttention ? '更新结果等待你的确认' : status.updateState.startsWith('READY') ? '仅接收光湖签名广播' : '信任根未配置 · 已关闭联网'}</p></div>
</button>
</section>
</main>
<footer className="world-footer">
<p><b>GH-AIOS</b><span></span></p>
<p className="trust-note"><i /> · </p>
</footer>
{panel && <div className="panel-backdrop" role="presentation" onMouseDown={(event) => event.target === event.currentTarget && setPanel(null)}>
<section className="detail-panel" role="dialog" aria-modal="true" aria-labelledby="panel-title">
<button className="panel-close" type="button" aria-label="关闭" onClick={() => setPanel(null)}><Icon name="close" /></button>
{panel === 'channel' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"> AI </h2>
<p className="panel-intro">MCP AI 使 HoloLake </p>
<div className="connection-summary"><span className={`status-light ${connectionState.tone}`} /><div><b>{connectionState.label}</b><span>{connectionState.detail}</span></div></div>
{status.directConnectionCount > 0 ? <div className="session-guidance">
<b></b>
<span> AI 使</span>
</div> : status.resumableSessionCount > 0 ? <div className="session-guidance">
<b></b>
<span> AI 使HoloLake </span>
</div> : ticket ? <>
<button className="primary-action panel-action" type="button" onClick={() => void copyInvitation()}><Icon name="copy" /> AI</button>
<pre className="invitation-preview">{invitationText}</pre>
</> : <button className="primary-action panel-action" type="button" onClick={() => void issueInvitation()}><Icon name="arrow" /></button>}
{message && <p className="panel-message" aria-live="polite">{message}</p>}
<p className="boundary-note"></p>
</>}
{panel === 'pncc' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"></h2>
<p className="panel-intro">HoloLake 访</p>
{serverPncc ? <div className="pncc-live-card">
<div className="candidate-heading"><span className="status-light ready" /><div><b></b><span>{new Date(serverPncc.observedAtUnixMs).toLocaleString('zh-CN')}</span></div></div>
<dl className="candidate-facts pncc-facts">
<div><dt></dt><dd>{serverPncc.personaId}</dd></div>
<div><dt></dt><dd>{serverPncc.humanResponsibilitySubject}</dd></div>
<div><dt></dt><dd>{serverPncc.nodeId}</dd></div>
<div><dt></dt><dd>{serverPncc.gitHead.slice(0, 12)}</dd></div>
<div><dt></dt><dd>{serverPncc.primaryLeaseHeld ? '已持有' : '未持有'}</dd></div>
<div><dt></dt><dd>{serverPncc.personaCarrierBound ? '已验证绑定' : '未绑定 · 等待证据'}</dd></div>
<div><dt></dt><dd>{serverPncc.modelInferenceStarted ? '已启动' : '未启动'}</dd></div>
<div><dt></dt><dd>{serverPncc.realityExecutionAllowed ? '已授权' : '未授权'}</dd></div>
<div><dt></dt><dd>{serverPncc.eventCount} · {serverPncc.eventChainHead.slice(0, 12)}</dd></div>
</dl>
<p> OS Codex 访</p>
</div> : <div className="empty-state compact"><i /><b></b><span>HoloLake 线</span></div>}
<button className="secondary-action panel-action" type="button" disabled={serverPnccBusy} onClick={() => void refreshServerPncc()}>{serverPnccBusy ? '正在回读…' : '重新回读主控'}</button>
<p className="boundary-note"> SSH 127.0.0.1 PNCC </p>
</>}
{panel === 'receipts' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"></h2>
<p className="panel-intro">线</p>
{repositoryCandidate ? <div className="candidate-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span>HoloLake Git </span></div></div>
<dl className="candidate-facts">
<div><dt></dt><dd>{repositoryCandidate.inspection.personaId}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.humanResponsibilitySubject}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.gitHead.slice(0, 12)}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.repositoryClean ? '干净' : '有未提交变化 · 不读取'}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.currentCheckpoint.relativePath}</dd></div>
</dl>
<p></p>
<div className="candidate-actions"><button className="primary-action" type="button" disabled={repositoryBusy} onClick={() => void confirmRepository()}><Icon name="arrow" /></button><button className="text-action" type="button" disabled={repositoryBusy} onClick={() => setRepositoryCandidate(null)}></button></div>
</div> : <button className="repository-picker" type="button" disabled={repositoryBusy} onClick={() => void chooseRepository()}><span><b>{status.codeRepositoryMountCount > 0 ? '再绑定一个代码仓库' : '绑定代码通道仓库'}</b><small> macOS </small></span><Icon name="arrow" /></button>}
{repositoryMessage && <p className="panel-message" aria-live="polite">{repositoryMessage}</p>}
{receipts.length ? <ol className="receipt-list">{receipts.map((receipt) => <li key={receipt.sequence}><span>{receipt.sequence.toString().padStart(2, '0')}</span><div><b>{receipt.kind === 'REPOSITORY_BINDING_REVALIDATED' ? '仓库绑定已复核' : '远端对象读取已验证'}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></li>)}</ol> : <div className="empty-state compact"><i /><b></b><span></span></div>}
</>}
{panel === 'updates' && <>
<p className="panel-kicker">广</p>
<h2 id="panel-title"></h2>
<p className="panel-intro">HoloLake </p>
{status.releaseRecoveryState === 'AWAITING_HUMAN_HEALTH_CONFIRMATION' ? <div className="recovery-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span></span></div></div>
<p></p>
<div className="recovery-actions"><button className="primary-action" type="button" disabled={releaseBusy} onClick={() => void resolveReleaseRecovery('keep')}></button><button className="secondary-action" type="button" disabled={releaseBusy} onClick={() => void resolveReleaseRecovery('rollback')}></button></div>
</div> : status.releaseRecoveryState === 'ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT' ? <div className="recovery-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span></span></div></div>
<p>HoloLake 退</p>
<button className="primary-action panel-action" type="button" disabled={releaseBusy} onClick={() => void resolveReleaseRecovery('keep')}><Icon name="arrow" /></button>
</div> : ['INSTALL_FAILED_BACKUP_RETAINED', 'INSTALL_INTERRUPTED_BACKUP_RETAINED'].includes(status.releaseRecoveryState) ? <div className="recovery-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span></span></div></div>
<p> HoloLake </p>
<button className="primary-action panel-action" type="button" disabled={releaseBusy} onClick={() => void resolveReleaseRecovery('keep')}><Icon name="arrow" /></button>
</div> : ['INSTALL_REPLACED_AWAITING_MANUAL_RESTART', 'ROLLBACK_REPLACED_AWAITING_MANUAL_RESTART', 'LAST_KNOWN_GOOD_VERIFIED_INSTALLING'].includes(status.releaseRecoveryState) ? <div className="recovery-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span>HoloLake </span></div></div>
<p> HoloLake </p>
</div> : !releaseRecoveryIdle ? <div className="recovery-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span>使</span></div></div>
<p>HoloLake </p>
</div> : releaseCandidate ? <div className="release-card">
<div className="release-version"><span></span><b>{releaseCandidate.version}</b><small> {releaseCandidate.currentVersion}</small></div>
<p>{releaseCandidate.notes}</p>
<ul>{releaseCandidate.features.map((feature) => <li key={feature}>{feature}</li>)}{releaseCandidate.fixes.map((fix) => <li key={fix}>{fix}</li>)}</ul>
<dl className="candidate-facts">
<div><dt></dt><dd>{releaseCandidate.dataMigrationRequired ? '需要' : '不需要'}</dd></div>
<div><dt></dt><dd>{releaseCandidate.minimumVersion}</dd></div>
<div><dt></dt><dd></dd></div>
<div><dt></dt><dd>{releaseCandidate.rollbackSupported ? `支持 · ${releaseCandidate.previousVersion || '上一版本'}` : '不支持'}</dd></div>
</dl>
<p className="release-warning">广Tauri SHA-256</p>
<button className="primary-action panel-action" type="button" disabled={releaseBusy} onClick={() => void confirmUpdateInstall()}><Icon name="arrow" /></button>
</div> : <div className="update-state-card"><span className={`status-light ${status.updateState.startsWith('READY') ? 'ready' : 'quiet'}`} /><div><b>{status.updateState.startsWith('READY') ? '主控信任根已登记' : '更新网络保持关闭'}</b><span>{status.updateState.startsWith('READY') ? '可以由你手动检查签名广播' : '没有正式地址和公钥时,不发起网络请求'}</span></div></div>}
{!releaseCandidate && releaseRecoveryIdle && <button className="primary-action panel-action" type="button" disabled={releaseBusy} onClick={() => void checkUpdate()}>{releaseBusy ? '正在核对…' : '检查光湖更新'}<Icon name="arrow" /></button>}
{releaseMessage && <p className="panel-message" aria-live="polite">{releaseMessage}</p>}
<p className="boundary-note">退线</p>
</>}
{panel === 'settings' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"></h2>
<p className="panel-intro"></p>
<div className="theme-list">{themes.map((choice) => <button key={choice.id} type="button" className={theme === choice.id ? 'selected' : ''} onClick={() => setTheme(choice.id)}><i className={`theme-swatch ${choice.id}`} /><span><b>{choice.name}</b><small>{choice.id === 'night' ? '当前定版基色' : '五色湖定版主题'}</small></span><em>{theme === choice.id ? '已选择' : ''}</em></button>)}</div>
<p className="boundary-note"> HoloLake 广</p>
</>}
</section>
</div>}
</div>
)
}
createRoot(document.getElementById('root')!).render(<StrictMode><HoloLakeApp /></StrictMode>)