feat(hololake): add persistent signed update rollback
This commit is contained in:
parent
70556ed45f
commit
73eb8bc1ca
9 changed files with 727 additions and 15 deletions
|
|
@ -15,6 +15,7 @@ interface HomeStatus {
|
|||
codeRepositoryMountCount: number
|
||||
pnccReceiptCount: number
|
||||
updateState: string
|
||||
releaseRecoveryState: string
|
||||
automaticUpstreamUpdates: boolean
|
||||
mcpRole: string
|
||||
}
|
||||
|
|
@ -94,6 +95,23 @@ const themes: Array<{ id: ThemeId; name: string }> = [
|
|||
{ 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',
|
||||
|
|
@ -102,6 +120,7 @@ const previewStatus: HomeStatus = {
|
|||
codeRepositoryMountCount: 0,
|
||||
pnccReceiptCount: 0,
|
||||
updateState: 'UNPROVISIONED_FAIL_CLOSED',
|
||||
releaseRecoveryState: recoveryPreviewState,
|
||||
automaticUpstreamUpdates: false,
|
||||
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
|
||||
}
|
||||
|
|
@ -164,6 +183,15 @@ function HoloLakeApp() {
|
|||
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()
|
||||
|
|
@ -245,6 +273,7 @@ function HoloLakeApp() {
|
|||
})
|
||||
setReleaseCandidate(null)
|
||||
setReleaseMessage('签名安装包已验证并安装。请在准备好后手动重启 HoloLake。')
|
||||
await refreshStatus()
|
||||
} catch (error) {
|
||||
setReleaseMessage(`更新没有安装:${String(error)}`)
|
||||
} finally {
|
||||
|
|
@ -252,6 +281,25 @@ function HoloLakeApp() {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -335,8 +383,8 @@ function HoloLakeApp() {
|
|||
<div><h2>代码通道仓库</h2><p>{status.codeRepositoryMountCount > 0 ? `已验证 · ${status.codeRepositoryMountCount} 个仓库` : '等待人类确认绑定'}</p></div>
|
||||
</article>
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -384,7 +432,25 @@ function HoloLakeApp() {
|
|||
<p className="panel-kicker">光湖签名广播</p>
|
||||
<h2 id="panel-title">更新由你决定何时进入</h2>
|
||||
<p className="panel-intro">HoloLake 不接收开源上游自动推送。只有登记到本机的光湖主控地址和公钥可以参与更新,并且检查、安装、重启是三件分开的事。</p>
|
||||
{releaseCandidate ? <div className="release-card">
|
||||
{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>
|
||||
|
|
@ -397,9 +463,9 @@ function HoloLakeApp() {
|
|||
<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>}
|
||||
{!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>
|
||||
<p className="boundary-note">持久回退底座已进入本机容器。公共更新仍需京东主控正式地址、公钥、签名发布流水线与苹果公证全部到位后才会启用。</p>
|
||||
</>}
|
||||
{panel === 'settings' && <>
|
||||
<p className="panel-kicker">湖面设置</p>
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px
|
|||
.release-card > p { color: var(--content-muted); font-size: 11.5px; line-height: 1.7; }
|
||||
.release-card ul { margin: 18px 0; padding-left: 18px; color: var(--content-secondary); font-size: 11px; line-height: 1.8; }
|
||||
.release-card .release-warning { color: var(--content-faint); font-size: 10px; }
|
||||
.recovery-card { display: grid; gap: 17px; margin-top: 27px; padding: 21px; border: 1px solid var(--primitive-line); border-radius: 21px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
|
||||
.recovery-card > p { margin: 0; color: var(--content-muted); font-size: 11.5px; line-height: 1.7; }
|
||||
.recovery-actions { display: flex; flex-wrap: wrap; gap: 11px; }
|
||||
|
||||
.theme-list { display: grid; gap: 9px; margin-top: 30px; }
|
||||
.theme-list button { width: 100%; min-height: 66px; display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: 14px; padding: 10px 14px; border: 0; border-radius: 17px; color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }
|
||||
|
|
|
|||
1
product-source/hololake-native-desktop/src/vite-env.d.ts
vendored
Normal file
1
product-source/hololake-native-desktop/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
Loading…
Reference in a new issue