feat(hololake): add human-confirmed signed updates

This commit is contained in:
冰朔 2026-08-13 14:11:48 +08:00
commit 70556ed45f
8 changed files with 721 additions and 5 deletions

View file

@ -5,7 +5,7 @@ import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type Panel = 'channel' | 'receipts' | 'settings' | null
type Panel = 'channel' | 'receipts' | 'updates' | 'settings' | null
interface HomeStatus {
schema: string
@ -63,6 +63,29 @@ interface RepositoryCandidate {
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
}
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' },
{ id: 'dawn', name: '晨湖曦光' },
@ -111,6 +134,9 @@ function HoloLakeApp() {
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 refreshStatus = useCallback(async () => {
@ -188,6 +214,44 @@ function HoloLakeApp() {
}
}
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。')
} catch (error) {
setReleaseMessage(`更新没有安装:${String(error)}`)
} finally {
setReleaseBusy(false)
}
}
const issueInvitation = async () => {
setMessage('')
try {
@ -270,10 +334,10 @@ function HoloLakeApp() {
<span className={`status-light ${status.codeRepositoryMountCount > 0 ? 'ready' : 'quiet'}`} aria-hidden="true" />
<div><h2></h2><p>{status.codeRepositoryMountCount > 0 ? `已验证 · ${status.codeRepositoryMountCount} 个仓库` : '等待人类确认绑定'}</p></div>
</article>
<article className="status-item">
<button className="status-item status-button" type="button" onClick={() => setPanel('updates')}>
<span className={`status-light ${status.updateState.startsWith('READY') ? 'ready' : 'quiet'}`} aria-hidden="true" />
<div><h2>广</h2><p>{status.updateState.startsWith('READY') ? '仅接收光湖签名广播' : '信任根未配置 · 已关闭联网'}</p></div>
</article>
</button>
</section>
</main>
@ -316,6 +380,27 @@ function HoloLakeApp() {
{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>
{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 && <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>