feat: admit signed mobile sync bridge module

This commit is contained in:
冰朔 2026-08-19 04:14:58 +08:00
commit 292889f934
21 changed files with 1891 additions and 15 deletions

View file

@ -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>
}