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

@ -9,6 +9,7 @@ const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace })))
const WebNovelWorkspace = lazy(() => import('./modules/web-novel/WebNovelWorkspace').then((module) => ({ default: module.WebNovelWorkspace })))
const MobileSyncPanel = lazy(() => import('./modules/mobile-sync').then((module) => ({ default: module.MobileSyncPanel })))
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
@ -55,7 +56,7 @@ import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'persona' | 'code' | 'receipts' | 'system'
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'receipts' | 'system'
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
type KnowledgeSource = 'native' | 'legacy'
@ -369,7 +370,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
]
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', mobileSync: '移动同步桥', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
const domainGates = [
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
@ -615,6 +616,9 @@ function HoloLakeApp() {
const [webNovelModule, setWebNovelModule] = useState<BundledModuleDescriptor | null>(null)
const [webNovelBusy, setWebNovelBusy] = useState(false)
const [webNovelMessage, setWebNovelMessage] = useState('')
const [mobileSyncModule, setMobileSyncModule] = useState<BundledModuleDescriptor | null>(null)
const [mobileSyncBusy, setMobileSyncBusy] = useState(false)
const [mobileSyncMessage, setMobileSyncMessage] = useState('')
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState('')
@ -1207,6 +1211,27 @@ function HoloLakeApp() {
finally { setWebNovelBusy(false) }
}
const refreshMobileSyncModule = async () => {
try {
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001') || null
setMobileSyncModule(module)
return module
} catch (error) { setMobileSyncMessage(humanError(error, 'system')); return null }
}
const openMobileSync = () => { openWorldTool('mobileSync'); void refreshMobileSyncModule() }
const activateMobileSyncModule = async () => {
setMobileSyncBusy(true)
setMobileSyncMessage('正在验证官方签名、登记通信编号并确认五项本机网络边界……')
try {
await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001', humanConfirmedPermissionExpansion: true } })
const module = await refreshMobileSyncModule()
if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE')
setMobileSyncMessage('移动同步桥已通过签名、编号、权限和自检验收;监听器仍保持关闭,等待本人显式开启。')
} catch (error) { setMobileSyncMessage(humanError(error, 'system')) }
finally { setMobileSyncBusy(false) }
}
const cloneCodeChannel = async (event: React.FormEvent) => {
event.preventDefault()
if (!cloneUrl.trim()) return
@ -1635,6 +1660,20 @@ function HoloLakeApp() {
</section>
)
const renderMobileSync = () => (
<section className="full-workbench mobile-sync-workbench-world">
{mobileSyncModule?.installedState === 'ACTIVE'
? <Suspense fallback={<div className="workbench-empty"><p></p></div>}><MobileSyncPanel onBack={() => setWorldStage(toolReturnStage)}/></Suspense>
: <div className="workbench-empty">
<span></span><h2></h2>
<p></p>
<code>HLP-MOD-OFFICIAL-MOBILE-SYNC-0001 · 5 </code>
<button className="primary-button" type="button" disabled={mobileSyncBusy} onClick={() => void activateMobileSyncModule()}>{mobileSyncBusy ? '正在验收官方模块…' : '确认五项边界并启用'}</button>
{mobileSyncMessage && <p>{mobileSyncMessage}</p>}
</div>}
</section>
)
const renderCode = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
@ -1906,6 +1945,7 @@ function HoloLakeApp() {
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
<LakePool className="channel-education" title="教育工作台" meta="官方编号模块 · 教育文档、表格与自动化" onClick={openEducation}/>
<LakePool className="channel-main" title="网文作者工作台" meta="官方编号模块 · 码字、设定、编辑与交付" onClick={openWebNovel}/>
<LakePool className="channel-mobile" title="移动同步桥" meta="同一人格系统 · 局域网加密入口" onClick={openMobileSync}/>
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay}` : '北京时间正在流动'} onClick={openEraTimeline}/>}
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</section>}
@ -1919,7 +1959,7 @@ function HoloLakeApp() {
</section>}
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}> 退</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
</section>}
</main>
<footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer>

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

File diff suppressed because one or more lines are too long

View file

@ -1120,6 +1120,54 @@ const ROUTES = {
"moduleNumber": "HLP-NIPC-MOD-0029",
"operationNumber": "HLP-NIPC-OP-0140",
"targetNumber": "HLP-NIPC-TGT-0029"
},
"start_mobile_sync": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0141",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"get_mobile_sync_status": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0142",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"rotate_mobile_pairing": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0143",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"stop_mobile_sync": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0144",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"revoke_mobile_sync_device": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0145",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"get_mobile_sync_snapshot": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0146",
"targetNumber": "HLP-NIPC-TGT-0030"
}
} as const

View file

@ -681,6 +681,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
.channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; }
.channel-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; }
.channel-time { left: 50%; bottom: 4%; transform: translateX(-50%); }
.channel-mobile { right: 7%; top: 70%; }
.private-route-note { position: absolute; z-index: 10; left: 50%; bottom: 11%; width: min(720px, calc(100% - 64px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; letter-spacing: .08em; }
.personal-node-guide { position: absolute; z-index: 12; left: 50%; top: 55%; width: min(760px, calc(100% - 72px)); max-height: calc(100% - 190px); overflow: auto; padding: 26px 30px; transform: translate(-50%, -50%); border: 1px solid var(--panel-edge); border-radius: 20px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); box-shadow: 0 30px 90px rgba(0, 0, 0, .42); backdrop-filter: blur(22px); }
.personal-node-guide header span { color: var(--accent-light); font-size: 11px; font-weight: 750; letter-spacing: .2em; }