feat(hololake): add domain-routed language membrane and user PNCC

This commit is contained in:
冰朔 2026-08-16 15:10:31 +08:00
commit d563e6fd73
34 changed files with 1949 additions and 144 deletions

View file

@ -54,8 +54,28 @@ interface HomeStatus {
mcpRole: string
}
interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number }
interface LoginSession { username: string; host: string; signedInAtUnixMs: number }
interface LoginReceipt { username: string; email: string; host: string }
interface LoginSession { username: string; host: string; domain: string; signedInAtUnixMs: number }
interface LoginReceipt { username: string; email: string; host: string; domain: string }
interface UserPnccChannelSnapshot {
state: string
repositoryId: string
channelId: string
userNumber: string
domain: string
accountUsername: string
accountHost: string
localPath: string
branch: string
gitHead: string
repositoryClean: boolean
createdAtUnixMs: number
gitEngine: string
humanProjection: string
forgejoAdapterState: string
remoteRepositoryUrl?: string
personaBindingClaimed: boolean
authority: string
}
interface PersonalChannelEvent { sequence: number; eventId: string; kind: string; summary: string; occurredAtUnixMs: number; receiptId: string; receiptHash: string }
interface PersonalChannelSnapshot { state: string; identity?: PersonalChannelIdentity; recentEvents: PersonalChannelEvent[]; integrity: { state: string; eventCount: number; receiptCount: number } }
interface KnowledgeDocumentSummary {
@ -109,6 +129,18 @@ interface CodeFileProjection { channelId: string; path: string; format: string;
interface ReceiptEvent { sequence: number; kind: string; observedAtUnixMs: number; eventHash: string }
interface ReceiptProjection { events: ReceiptEvent[] }
interface DiscoveryTicketReceipt { laneId: string; clientInstanceId: string; discoveryTicket: string }
interface NearbyAiDiscoverySnapshot {
state: string
serviceName: string
transport: string
languageProtocol: string
automaticSameDeviceDiscovery: boolean
largeInvitationCopyRequired: boolean
genericAiVisitor: string
guanghuPersona: string
localNetworkDiscovery: string
authority: string
}
interface ReleaseCandidate { candidateId: string; confirmationToken: string; currentVersion: string; version: string; notes: string; features: string[]; fixes: string[]; dataMigrationRequired: boolean }
interface ReleaseCheckReceipt { candidate?: ReleaseCandidate }
interface ZeroPointSnapshot {
@ -134,6 +166,10 @@ function domainDisplayName(domain: string): string {
return names[domain] || '已登记光湖域'
}
function domainLightIndex(domain: string): number {
return ({ MAIN_DOMAIN: 1, BRANCH_DOMAIN: 2, ZERO_DOMAIN: 3, ZERO_SENSE_DOMAIN: 4, FIFTH_DOMAIN: 5 } as Record<string, number>)[domain] || 0
}
function protocolOriginDisplayName(origin: string): string {
return origin.startsWith('FACTORY_DEFAULT') ? '内置默认协议(签名发布通道尚未配置)' : origin
}
@ -165,7 +201,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: '知识空间', code: '代码频道', receipts: '运行回执', system: '系统详情' }
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key)
@ -183,6 +219,10 @@ function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' |
if (value.includes('LOGIN_KEYCHAIN_FAILED')) return '本机钥匙串写入失败,凭证未能安全保存。'
if (value.includes('LOGIN_SESSION_CORRUPT')) return '登录会话已损坏,请重新登录。'
if (value.includes('LOGIN_VERIFICATION_FAILED')) return '仓库验证未通过,请稍后再试。'
if (value.includes('DOMAIN_LOGIN_NOT_PROVISIONED')) return '该域的企业服务器尚未接入,当前不能继续登录。'
if (value.includes('DOMAIN_ROUTE_REQUIRED') || value.includes('ZP_DOMAIN_ROUTE_REQUIRED')) return '编号尚未解析到已登记域,不能开始登录。'
if (value.includes('USER_PNCC_TRUSTED_SUBJECT_REQUIRED')) return '建立人格代码频道前,需要有效的用户编号与仓库账号登录。'
if (value.includes('USER_PNCC_BINDING_MISMATCH')) return '本机频道绑定记录与当前账号不一致,系统已停止加载。'
if (value.includes('HOST_NOT_TRUSTED')) return '该地址不属于已登记的光湖代码频道。'
if (value.includes('URL_INVALID')) return '频道地址无效,请粘贴完整的 HTTPS 克隆地址。'
if (value.includes('DESTINATION_EXISTS')) return '该代码频道已经存在,无需再次克隆。'
@ -300,9 +340,13 @@ function HoloLakeApp() {
const [identityMessage, setIdentityMessage] = useState('')
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [nearbyDiscovery, setNearbyDiscovery] = useState<NearbyAiDiscoverySnapshot | null>(null)
const [systemMessage, setSystemMessage] = useState('')
const [releaseCandidate, setReleaseCandidate] = useState<ReleaseCandidate | null>(null)
const [repoLogin, setRepoLogin] = useState<LoginSession | null>(null)
const [userPncc, setUserPncc] = useState<UserPnccChannelSnapshot | null>(null)
const [userPnccBusy, setUserPnccBusy] = useState(false)
const [userPnccMessage, setUserPnccMessage] = useState('')
const [loginUsername, setLoginUsername] = useState('')
const [loginPassword, setLoginPassword] = useState('')
const [loginBusy, setLoginBusy] = useState(false)
@ -355,6 +399,11 @@ function HoloLakeApp() {
const timer = window.setInterval(() => void refreshCore(), 3500)
return () => window.clearInterval(timer)
}, [refreshCore])
useEffect(() => {
invoke<NearbyAiDiscoverySnapshot>('get_nearby_ai_discovery')
.then(setNearbyDiscovery)
.catch(() => setNearbyDiscovery(null))
}, [])
const refreshServerPncc = useCallback(async () => {
setServerPnccBusy(true)
try {
@ -387,6 +436,26 @@ function HoloLakeApp() {
const timer = window.setInterval(() => void loadZeroPoint(), 10000)
return () => window.clearInterval(timer)
}, [loadZeroPoint])
// 已验证编号与已认证账号同时存在时,后端幂等建立或恢复该用户自己的 GH-PNCC。
useEffect(() => {
if (!repoLogin || zeroPoint?.route !== 'verified') {
setUserPncc(null)
return
}
let active = true
setUserPnccBusy(true)
setUserPnccMessage('')
invoke<UserPnccChannelSnapshot>('ensure_user_pncc_channel')
.then((snapshot) => {
if (!active) return
setUserPncc(snapshot)
setUserPnccMessage('本机原生代码频道已就绪。')
void refreshCore()
})
.catch((error) => { if (active) setUserPnccMessage(humanError(error, 'code')) })
.finally(() => { if (active) setUserPnccBusy(false) })
return () => { active = false }
}, [repoLogin, zeroPoint?.route, zeroPoint?.userNumber, refreshCore])
// 本机存在已绑定编号时自动回填,减少重复输入。
useEffect(() => {
if (gateStage !== 'number' || gateRaw) return
@ -649,7 +718,7 @@ function HoloLakeApp() {
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
setLoginRising(true)
await new Promise((resolve) => setTimeout(resolve, 1300))
setRepoLogin({ username: receipt.username, host: receipt.host, signedInAtUnixMs: Date.now() })
setRepoLogin({ username: receipt.username, host: receipt.host, domain: receipt.domain, signedInAtUnixMs: Date.now() })
setLoginRising(false)
setLoginPassword('')
} catch (error) { setLoginMessage(humanError(error, 'login')) }
@ -825,13 +894,19 @@ function HoloLakeApp() {
const renderCode = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
<header><div><span className="kicker">CODE CHANNEL</span><h1></h1></div></header>
<header><div><span className="kicker">GH-PNCC</span><h1></h1></div></header>
<section className="pncc-native-card">
<div className="pncc-native-card-title"><span className={userPncc ? 'identity-dot ready' : 'identity-dot'}/><div><b>{userPncc ? `${userPncc.accountUsername} · GH-PNCC` : '正在建立用户频道'}</b><small>{userPnccBusy ? '正在初始化本机 Git…' : userPncc ? `${userPncc.branch} · ${userPncc.gitHead.slice(0, 8)}` : '等待可信绑定'}</small></div></div>
{userPncc ? <dl><div><dt></dt><dd>{domainDisplayName(userPncc.domain)}</dd></div><div><dt></dt><dd>{userPncc.userNumber}</dd></div><div><dt></dt><dd>Git</dd></div><div><dt></dt><dd>HoloLake </dd></div><div><dt>Forgejo </dt><dd> · </dd></div></dl> : <p>{userPnccMessage || '系统先按编号确定所属域,再通过该域的账号与节点入口建立频道。'}</p>}
{userPncc && <button type="button" onClick={() => { const channel = codeChannels.channels.find((item) => item.channelId === userPncc.channelId); if (channel) void browseChannel(channel) }}></button>}
</section>
<div className="channel-section-label"><b></b><small> Git</small></div>
<form className="clone-form" onSubmit={(event) => void cloneCodeChannel(event)}>
<label htmlFor="clone-url"></label><div><input id="clone-url" type="url" value={cloneUrl} placeholder="https://guanghulab.com/code/…" onChange={(event) => setCloneUrl(event.target.value)}/><button disabled={!cloneUrl.trim() || codeBusy}></button></div>
</form>
<button className="local-folder-button" type="button" onClick={() => void selectLocalCodeChannel()}><Icon name="folder"/> Git </button>
<div className="channel-selector">{codeChannels.channels.map((channel) => <button className={activeChannel?.channelId === channel.channelId ? 'active' : ''} key={channel.channelId} type="button" onClick={() => void browseChannel(channel)}><Icon name="code"/><span><b>{channel.name}</b><small>{channel.branch} · {channel.gitHead.slice(0, 8)}</small></span></button>)}</div>
<footer>{codeMessage || '仅提供本机读取,不授予推送、发布或部署权限。'}</footer>
<footer>{codeMessage || userPnccMessage || 'Git 负责耐久化Forgejo 仅作为远端协作适配器。'}</footer>
</aside>
<aside className="repository-tree">
<header><button className="icon-button" disabled={!codeTree?.path} type="button" onClick={() => activeChannel && void browseChannel(activeChannel, codeParent())}><Icon name="back"/></button><div><b>{activeChannel?.name || '尚未选择频道'}</b><small>/{codeTree?.path || ''}</small></div></header>
@ -841,7 +916,7 @@ function HoloLakeApp() {
{activeCodeFile ? <>
<header><div><span>{activeCodeFile.path}</span><small>{activeCodeFile.format.toUpperCase()} · {activeCodeFile.sizeBytes.toLocaleString()} bytes</small></div><div className="mode-switch"><button className={codeMode === 'human' ? 'active' : ''} type="button" onClick={() => setCodeMode('human')}></button><button className={codeMode === 'source' ? 'active' : ''} type="button" onClick={() => setCodeMode('source')}></button></div></header>
<div className="code-reader-scroll">{codeMode === 'human' ? <MarkdownDocument body={activeCodeFile.humanMarkdown}/> : <pre className="source-code"><code>{activeCodeFile.source}</code></pre>}</div>
</> : <div className="workbench-empty"><span>&lt;/&gt;</span><h2></h2><p> MarkdownJSONHDLPYAMLTOML </p></div>}
</> : <div className="workbench-empty"><span>&lt;/&gt;</span><h2>GH-PNCC · </h2><p> HoloLake Git </p></div>}
</main>
</section>
)
@ -861,9 +936,10 @@ function HoloLakeApp() {
<header className="page-title"><div><span className="kicker">SYSTEM EVIDENCE</span><h1></h1><p></p></div></header>
<div className="system-grid">
<section className="plain-panel connection-panel">
<header><div><h2> AI </h2><p>MCP </p></div><span className={connectionOnline ? 'status-chip online' : 'status-chip'}>{connectionOnline ? `${status.directConnectionCount} 个客户端在线` : '当前无客户端'}</span></header>
<dl className="evidence-list"><div><dt></dt><dd>{status.directLocalBrokerState}</dd></div><div><dt></dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{status.resumableSessionCount}</dd></div><div><dt></dt><dd>UNIX_STREAM_JSON_LINES</dd></div><div><dt>MCP </dt><dd> / / </dd></div></dl>
{ticket ? <><button className="secondary-button" type="button" onClick={() => void copyInvitation()}><Icon name="copy"/></button><pre className="invitation-data">{invitationText}</pre></> : <button className="primary-button" type="button" disabled={systemBusy} onClick={() => void issueInvitation()}></button>}
<header><div><h2></h2><p> AI HoloLakeMCP </p></div><span className={nearbyDiscovery?.automaticSameDeviceDiscovery ? 'status-chip online' : 'status-chip'}>{nearbyDiscovery?.automaticSameDeviceDiscovery ? '本机自动发现已开启' : '自动发现不可用'}</span></header>
<dl className="evidence-list"><div><dt></dt><dd>GLP/1.0 · </dd></div><div><dt> AI 访</dt><dd>{nearbyDiscovery?.genericAiVisitor === 'EXPRESSION_ONLY_READY' ? '可连接 · 仅语言表达' : '不可用'}</dd></div><div><dt></dt><dd>{nearbyDiscovery?.guanghuPersona === 'BINDING_EVIDENCE_REQUIRED' ? '等待人格绑定证据' : '可连接'}</dd></div><div><dt></dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{nearbyDiscovery?.localNetworkDiscovery === 'DEFERRED_UNTIL_ENCRYPTED_TRANSPORT_AND_APPROVAL' ? '等待加密传输与确认闭环' : '已开启'}</dd></div><div><dt>MCP</dt><dd> / </dd></div></dl>
<p className="boundary-note">访</p>
{ticket ? <><button className="secondary-button" type="button" onClick={() => void copyInvitation()}><Icon name="copy"/></button><pre className="invitation-data">{invitationText}</pre></> : <button className="secondary-button" type="button" disabled={systemBusy} onClick={() => void issueInvitation()}></button>}
</section>
<section className="plain-panel">
<header><div><h2> · GH-PNCC</h2><p></p></div><span className={serverPnccReadback === 'LIVE' ? 'status-chip online' : 'status-chip'}>{serverPnccReadback === 'LIVE' ? '实时回读正常' : serverPnccReadback === 'UNAVAILABLE' ? '当前不可回读' : '正在读取'}</span></header>
@ -907,13 +983,20 @@ function HoloLakeApp() {
return <div className="app-shell">
<div className={`gate-lake${loginRising ? ' sinking' : ''}`} style={{ '--lake-img': `url(${nightLake})` } as React.CSSProperties}>
<i className="lake-mist" aria-hidden="true"/>
<div className="lake-lights" aria-hidden="true">{[1, 2, 3, 4, 5].map((n) => <span key={n} className={`lake-light l${n}${(gateRising || loginRising) && n === 5 ? ' rising' : ''}`}/>)}</div>
<div className="lake-lights" aria-hidden="true">{[1, 2, 3, 4, 5].map((n) => <span key={n} className={`lake-light l${n}${(gateRising || loginRising) && n === domainLightIndex(zeroPoint?.resolvedDomain || '') ? ' rising' : ''}`}/>)}</div>
{<header className={`gate-hero${gateRising ? ' veil' : ''}`} aria-hidden={gateRising || undefined}>
<span className="gate-emblem" aria-hidden="true"><i/></span>
<b className="gate-product">HoloLake</b>
<small className="gate-techname"> · </small>
<small className="gate-abbr">GH-AIOS · GuangHu AI Operating System</small>
</header>}
{!gateRising && gateStage === 'number' && <section className="gate-domain-map" aria-label="光湖五域公开入口">
<article><i/><b></b><small> · </small></article>
<article><i/><b></b><small> · </small></article>
<article><i/><b></b><small> · </small></article>
<article><i/><b></b><small> · </small></article>
<article className="fifth"><i/><b> · </b><small> · </small></article>
</section>}
{gateRising ? (
<section className="gate-rise" role="status">
<span className="gate-rise-star" aria-hidden="true"><i/></span>
@ -924,8 +1007,8 @@ function HoloLakeApp() {
<section className={`gate-pod gate-flip${gateOpen ? ' open' : ''}${loginRising ? ' fade-out' : ''}`} role={gateOpen ? 'dialog' : 'button'} tabIndex={gateOpen ? undefined : 0} aria-expanded={gateOpen} onClick={gateOpen ? undefined : () => setGateOpen(true)} onKeyDown={gateOpen ? undefined : (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setGateOpen(true) } }}>
<div className="pod-face pod-sealed" aria-hidden={gateOpen}>
<span className="gate-emblem sealed-emblem" aria-hidden="true"><i/></span>
<b className="sealed-title"></b>
<small className="sealed-sub"></small>
<b className="sealed-title"></b>
<small className="sealed-sub"></small>
</div>
<div className="pod-face pod-form">
<div className="gate-pod-row">
@ -953,7 +1036,7 @@ function HoloLakeApp() {
return <div className={`app-shell${sidebarCollapsed ? ' sidebar-collapsed' : ''}${inspectorOpen ? '' : ' inspector-closed'}`}>
<aside className="sidebar">
<div className="brand"><span className="brand-mark brand-lamp" title={sidebarCollapsed ? '展开导航' : '光湖 · HoloLake'} onClick={sidebarCollapsed ? toggleSidebar : undefined}><i className="lamp-glow"/><i className="lamp-core"/><i className="lake-shimmer"/></span><div className="brand-text"><b>HoloLake</b><small className="brand-sub"> · </small><small>GH-AIOS · V0.3.0</small></div><button className="icon-button sidebar-toggle" type="button" title={sidebarCollapsed ? '展开导航' : '收起导航'} onClick={toggleSidebar}><Icon name="chevron"/></button></div>
<div className="brand"><span className="brand-mark brand-lamp" title={sidebarCollapsed ? '展开导航' : '光湖 · HoloLake'} onClick={sidebarCollapsed ? toggleSidebar : undefined}><i className="lamp-glow"/><i className="lamp-core"/><i className="lake-shimmer"/></span><div className="brand-text"><b>HoloLake</b><small className="brand-sub"> · </small><small>GH-AIOS · V0.4.0</small></div><button className="icon-button sidebar-toggle" type="button" title={sidebarCollapsed ? '展开导航' : '收起导航'} onClick={toggleSidebar}><Icon name="chevron"/></button></div>
<nav>{(Object.keys(viewLabels) as ViewId[]).map((item) => <button className={view === item ? 'active' : ''} key={item} type="button" onClick={() => setView(item)}><Icon name={item}/><span>{viewLabels[item]}</span>{item === 'knowledge' && <em>{knowledge.uniqueDocumentCount}</em>}</button>)}</nav>
<div className="sidebar-foot"><span className="identity-dot"/><div><b>{personal.identity?.displayName || '本机空间'}</b><small>{repoLogin ? `${repoLogin.username}@${repoLogin.host}` : (personal.identity ? '个人频道已建立' : '等待首次设置')}</small></div><button className="sign-out" type="button" title="退出代码仓库登录" onClick={() => void signOutRepo()}>退</button></div>
</aside>