555 lines
31 KiB
TypeScript
555 lines
31 KiB
TypeScript
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>)
|