feat: admit signed mobile sync bridge module
This commit is contained in:
parent
eb7223ea4c
commit
292889f934
21 changed files with 1891 additions and 15 deletions
|
|
@ -0,0 +1,164 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||
import './styles.css'
|
||||
|
||||
interface PairedDevice {
|
||||
deviceId: string
|
||||
displayName: string
|
||||
platform: string
|
||||
createdAtUnixMs: number
|
||||
lastSeenAtUnixMs?: number
|
||||
state: 'ACTIVE' | 'REVOKED'
|
||||
}
|
||||
|
||||
interface MobileSyncStatus {
|
||||
schema: string
|
||||
state: 'OFFLINE' | 'PAIRING_READY' | 'RUNNING'
|
||||
running: boolean
|
||||
lanAddress?: string
|
||||
port?: number
|
||||
pairingUri?: string
|
||||
pairingQrSvg?: string
|
||||
pairingExpiresAtUnixMs?: number
|
||||
pairedDevices: PairedDevice[]
|
||||
transport: string
|
||||
encryption: string
|
||||
desktopIsRootNode: boolean
|
||||
platformPrivateDataCustody: boolean
|
||||
}
|
||||
|
||||
interface MobileCapture {
|
||||
cursor: number
|
||||
captureId: string
|
||||
title: string
|
||||
body: string
|
||||
sourceDeviceId: string
|
||||
createdAtUnixMs: number
|
||||
}
|
||||
|
||||
interface MobileSyncSnapshot {
|
||||
state: string
|
||||
cursor: number
|
||||
generatedAtUnixMs: number
|
||||
desktopName: string
|
||||
rootNodeOnline: boolean
|
||||
personalChannel: { home: string; growthEventCount: number; integrity: string }
|
||||
webNovel: {
|
||||
workCount: number
|
||||
volumeCount: number
|
||||
chapterCount: number
|
||||
works: Array<{ workId: string; title: string; status: string; updatedAtUnixMs: number }>
|
||||
}
|
||||
education: {
|
||||
activeTableCount: number
|
||||
archivedTableCount: number
|
||||
unassignedTableCount: number
|
||||
sensitiveValuesIncluded: boolean
|
||||
}
|
||||
recentCaptures: MobileCapture[]
|
||||
boundary: {
|
||||
mobileRole: string
|
||||
remoteDesktopClone: boolean
|
||||
desktopOfflineExecution: boolean
|
||||
sensitiveEducationValues: string
|
||||
modelApi: string
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(value?: number) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未连接'
|
||||
}
|
||||
|
||||
export function MobileSyncPanel({ onBack }: { onBack: () => void }) {
|
||||
const [status, setStatus] = useState<MobileSyncStatus | null>(null)
|
||||
const [snapshot, setSnapshot] = useState<MobileSyncSnapshot | null>(null)
|
||||
const [message, setMessage] = useState('手机是当前个人节点的远程入口,不是第二套人格系统。')
|
||||
const [pending, setPending] = useState(false)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const next = await invoke<MobileSyncStatus>('get_mobile_sync_status')
|
||||
setStatus(next)
|
||||
if (next.running) setSnapshot(await invoke<MobileSyncSnapshot>('get_mobile_sync_snapshot'))
|
||||
else setSnapshot(null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
refresh().catch((error) => !cancelled && setMessage(String(error)))
|
||||
return () => { cancelled = true }
|
||||
}, [refresh])
|
||||
|
||||
const run = async (action: () => Promise<unknown>, success: string) => {
|
||||
if (pending) return
|
||||
setPending(true)
|
||||
try {
|
||||
await action()
|
||||
setMessage(success)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
setMessage(String(error))
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
const qrSource = useMemo(
|
||||
() => status?.pairingQrSvg ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(status.pairingQrSvg)}` : '',
|
||||
[status?.pairingQrSvg],
|
||||
)
|
||||
|
||||
return <section className="mobile-sync-world" aria-label="HoloLake 多端同步">
|
||||
<header className="mobile-sync-worldbar">
|
||||
<button type="button" onClick={onBack}>← 返回我的频道</button>
|
||||
<div><span>HOLOLAKE / SAME PERSONA SYSTEM</span><b>iPhone 多端同步</b></div>
|
||||
<em>{status?.running ? '个人主节点在线' : '同步未开启'}</em>
|
||||
</header>
|
||||
|
||||
<div className="mobile-sync-focus">
|
||||
<div className={`mobile-sync-orbit ${status?.running ? 'is-online' : ''}`} aria-hidden="true"><i/><i/><i/></div>
|
||||
<div><span>手机远程身体入口</span><h1>{status?.running ? '同一频道,正在等待 iPhone' : '由这台电脑守住唯一真相'}</h1><p>桌面保留身份、人格、数据与执行主权;手机只同步经过授权的投影与快速记录。</p></div>
|
||||
</div>
|
||||
|
||||
{!status?.running ? <section className="mobile-sync-start">
|
||||
<b>同一局域网 · 应用层端到端加密</b>
|
||||
<p>开启后只在当前登录账号的数据根建立同步库。平台服务器不托管你的作品、教育数据、人格记忆或设备密钥。</p>
|
||||
<button type="button" disabled={pending} onClick={() => void run(() => invoke('start_mobile_sync'), 'iPhone 同步入口已开启。')}>{pending ? '正在开启…' : '开启 iPhone 同步'}</button>
|
||||
</section> : <div className="mobile-sync-grid">
|
||||
<section className="mobile-sync-pairing">
|
||||
<header><div><span>PAIRING</span><h2>配对这台 iPhone</h2></div><button type="button" disabled={pending} onClick={() => void run(() => invoke('rotate_mobile_pairing'), '已换成新的十分钟配对凭据。')}>换一枚配对凭据</button></header>
|
||||
{status.pairingUri ? <>
|
||||
{qrSource && <img src={qrSource} alt="HoloLake iPhone 配对二维码"/>}
|
||||
<p>用 iPhone 版 HoloLake 扫描。二维码内含一次性高强度配对密钥,十分钟后或使用一次后失效。</p>
|
||||
<button type="button" onClick={() => void navigator.clipboard.writeText(status.pairingUri || '').then(() => setMessage('配对链接已复制。'))}>复制配对链接</button>
|
||||
<small>到期:{formatTime(status.pairingExpiresAtUnixMs)}</small>
|
||||
</> : <div className="mobile-sync-pairing-used"><b>当前配对凭据已使用或到期</b><p>需要添加另一台手机时,再生成一枚新的凭据。</p></div>}
|
||||
</section>
|
||||
|
||||
<section className="mobile-sync-projection">
|
||||
<header><span>ROOT NODE PROJECTION</span><h2>当前同步投影</h2></header>
|
||||
<div className="mobile-sync-metrics">
|
||||
<article><b>{snapshot?.webNovel.workCount ?? 0}</b><span>网文作品</span></article>
|
||||
<article><b>{snapshot?.webNovel.chapterCount ?? 0}</b><span>章节</span></article>
|
||||
<article><b>{snapshot?.education.activeTableCount ?? 0}</b><span>教育表格</span></article>
|
||||
<article><b>{snapshot?.personalChannel.growthEventCount ?? 0}</b><span>成长事件</span></article>
|
||||
</div>
|
||||
<p>教育敏感值:{snapshot?.education.sensitiveValuesIncluded ? '已包含' : '不会进入手机摘要'} · 电脑离线时手机不会冒充执行。</p>
|
||||
</section>
|
||||
|
||||
<section className="mobile-sync-devices">
|
||||
<header><span>DEVICES</span><h2>已配对设备</h2></header>
|
||||
{status.pairedDevices.length ? status.pairedDevices.map((device) => <article key={device.deviceId}>
|
||||
<div><b>{device.displayName}</b><span>{device.platform} · {device.state === 'ACTIVE' ? `最近连接 ${formatTime(device.lastSeenAtUnixMs)}` : '已撤销'}</span></div>
|
||||
{device.state === 'ACTIVE' && <button type="button" disabled={pending} onClick={() => void run(() => invoke('revoke_mobile_sync_device', { input: { deviceId: device.deviceId } }), `已撤销 ${device.displayName}。`)}>撤销</button>}
|
||||
</article>) : <p>还没有完成配对的 iPhone。</p>}
|
||||
</section>
|
||||
|
||||
<section className="mobile-sync-captures">
|
||||
<header><span>MOBILE CAPTURES</span><h2>手机快速记录</h2></header>
|
||||
{snapshot?.recentCaptures.length ? snapshot.recentCaptures.map((capture) => <article key={capture.captureId}><div><b>{capture.title || '未命名记录'}</b><time>{formatTime(capture.createdAtUnixMs)}</time></div><p>{capture.body}</p></article>) : <p>手机写下的想法会加密回到这台电脑,并出现在这里。</p>}
|
||||
</section>
|
||||
</div>}
|
||||
|
||||
<footer className="mobile-sync-footer"><span>{message}</span><div><button type="button" disabled={pending} onClick={() => void run(refresh, '状态已从本机根节点重新读取。')}>刷新状态</button>{status?.running && <button type="button" disabled={pending} onClick={() => void run(() => invoke('stop_mobile_sync'), '同步入口已关闭;已配对设备登记仍保留,可单独撤销。')}>关闭本机同步入口</button>}</div></footer>
|
||||
</section>
|
||||
}
|
||||
Loading…
Reference in a new issue