hololake-system-architecture/product-source/hololake-native-desktop/src/main.tsx

1015 lines
72 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { StrictMode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { invoke } from '@tauri-apps/api/core'
import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render'
import nightLake from './assets/hololake-night-lake.png'
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
// 编号自动补齐:人只敲字母与数字,杠由系统按已登记文法自动出。
// ICE 系ICE-GL∞ / ICE-P-ZY001、GLS 系GLS-LA-20260720-003认不出的文法只在字母数字交界处补杠。
function formatGateNumber(raw: string): string {
const up = raw.toUpperCase()
if (up.startsWith('ICE')) {
const rest = up.slice(3)
if (!rest) return 'ICE'
if (rest.startsWith('P')) {
const body = rest.slice(1)
return body ? `ICE-P-${body}` : 'ICE-P'
}
return `ICE-${rest}`
}
if (up.startsWith('GLS')) {
const rest = up.slice(3)
if (!rest) return 'GLS'
const letters = rest.match(/^[A-Z]*/)?.[0] ?? ''
const digits = rest.slice(letters.length)
if (!digits) return `GLS-${letters}`
const d1 = digits.slice(0, 8)
const d2 = digits.slice(8, 11)
return `GLS-${letters}-${d1}${d2 ? `-${d2}` : ''}`
}
return up.replace(/([A-Z]+)([0-9])/g, '$1-$2').replace(/([0-9]+)([A-Z])/g, '$1-$2')
}
function tagTint(tag: string) {
let hash = 0
for (const char of tag) hash = (hash * 31 + (char.codePointAt(0) || 0)) % 997
return TAG_TINTS[hash % TAG_TINTS.length]
}
import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system' | 'parlor' | 'generic'
type KnowledgeSource = 'native' | 'legacy'
interface HomeStatus {
directLocalBrokerState: string
directConnectionCount: number
resumableSessionCount: number
codeRepositoryMountCount: number
pnccReceiptCount: number
updateState: string
releaseRecoveryState: string
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 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 {
source: KnowledgeSource
path: string
title: string
updatedAtUnixMs: number
sizeBytes: number
contentSha256: string
duplicateCount: number
}
interface KnowledgeSnapshot {
state: string
nativeRoot: string
legacyAvailable: boolean
legacyRoot?: string
documents: KnowledgeDocumentSummary[]
rawDocumentCount: number
uniqueDocumentCount: number
duplicateDocumentCount: number
truncated: boolean
}
interface KnowledgeDocument {
source: KnowledgeSource
path: string
title: string
body: string
updatedAtUnixMs: number
contentSha256: string
writable: boolean
}
interface KnowledgeSearchResult { source: KnowledgeSource; path: string; title: string; snippet: string }
interface KnowledgeImportResult {
state: string
sourceName: string
importedDocuments: number
importedAssets: number
existingDocuments: number
conflicts: number
skipped: number
firstDocument?: string
gitCommit: string
snapshot: KnowledgeSnapshot
}
interface KnowledgeSaveResult { state: string; gitCommit: string; document: KnowledgeDocument }
interface CodeChannelEntry { channelId: string; name: string; sourceKind: string; localPath: string; remoteUrl?: string; gitHead: string; branch: string; repositoryClean: boolean; registeredAtUnixMs: number }
interface CodeChannelSnapshot { state: string; channels: CodeChannelEntry[]; authority: string }
interface CodeTreeEntry { path: string; name: string; kind: 'directory' | 'file'; sizeBytes: number }
interface CodeTreeSnapshot { channelId: string; path: string; entries: CodeTreeEntry[]; truncated: boolean }
interface CodeFileProjection { channelId: string; path: string; format: string; source: string; humanMarkdown: string; sizeBytes: number }
interface ReceiptEvent { sequence: number; kind: string; observedAtUnixMs: number; eventHash: string }
interface ReceiptProjection { events: ReceiptEvent[] }
interface DiscoveryTicketReceipt { laneId: string; clientInstanceId: string; discoveryTicket: 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 {
route: string
binding: string
userNumber: string
lastValidCheck: number
graceDeadline: number
protocol: { gracePeriodDays: number; lighthouseAnchorUrl: string; lighthouseResolveUrl: string; coreChannelSource: string; origin: string }
syncNote: string
}
const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'UNPROVISIONED_FAIL_CLOSED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY' }
const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } }
const previewKnowledge: KnowledgeSnapshot = { state: 'UNAVAILABLE', nativeRoot: '', legacyAvailable: false, documents: [], rawDocumentCount: 0, uniqueDocumentCount: 0, duplicateDocumentCount: 0, truncated: false }
const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' }
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: '系统详情', parlor: '会客厅', generic: '通用AI' }
function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key)
if (existing) return existing
const generated = `${prefix}-${crypto.randomUUID()}`
window.localStorage.setItem(key, generated)
return generated
}
function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' | 'system' | 'login') {
const value = String(error)
if (value.includes('LOGIN_CREDENTIALS_INVALID')) return '账号或密码未被仓库接受。'
if (value.includes('LOGIN_USERNAME_INVALID')) return '账号格式不合法(仅限字母、数字、连字符、下划线)。'
if (value.includes('LOGIN_NETWORK_FAILED')) return '未能连通光湖代码频道服务器。'
if (value.includes('LOGIN_KEYCHAIN_FAILED')) return '本机钥匙串写入失败,凭证未能安全保存。'
if (value.includes('LOGIN_SESSION_CORRUPT')) return '登录会话已损坏,请重新登录。'
if (value.includes('LOGIN_VERIFICATION_FAILED')) return '仓库验证未通过,请稍后再试。'
if (value.includes('HOST_NOT_TRUSTED')) return '该地址不属于已登记的光湖代码频道。'
if (value.includes('URL_INVALID')) return '频道地址无效,请粘贴完整的 HTTPS 克隆地址。'
if (value.includes('DESTINATION_EXISTS')) return '该代码频道已经存在,无需再次克隆。'
if (value.includes('CLONE_FAILED')) return '代码频道克隆失败,请核对地址及读取权限。'
if (value.includes('NO_SUPPORTED_FILES')) return '文件夹中没有可导入的知识文件。'
if (value.includes('SAVE_CONFLICT')) return '页面已在别处更新,请重新打开后再编辑。'
if (value.includes('FILE_UNSUPPORTED')) return '该文件不是当前可阅读的文本格式。'
if (context === 'login') return '登录未完成。'
if (context === 'identity') return '个人空间未能建立。'
if (context === 'knowledge') return '知识操作未完成,原文件未被覆盖。'
if (context === 'code') return '代码频道操作未完成。'
return '系统操作未完成。'
}
function Icon({ name }: { name: ViewId | 'search' | 'import' | 'folder' | 'copy' | 'arrow' | 'chevron' | 'file' | 'edit' | 'save' | 'back' | 'download' | 'trash' | 'chevronDown' | 'settings' | 'more' | 'plus' }) {
const paths: Record<string, React.ReactNode> = {
overview: <><rect x="4" y="4" width="6" height="6" rx="1.5"/><rect x="14" y="4" width="6" height="6" rx="1.5"/><rect x="4" y="14" width="6" height="6" rx="1.5"/><rect x="14" y="14" width="6" height="6" rx="1.5"/></>,
knowledge: <><path d="M5 4.5h10a3 3 0 0 1 3 3V20H8a3 3 0 0 1-3-3Z"/><path d="M8 4.5v12.8A2.7 2.7 0 0 1 10.7 20"/></>,
code: <><path d="m8.5 8-4 4 4 4M15.5 8l4 4-4 4M13.5 5l-3 14"/></>,
receipts: <><path d="M7 3.5h10v17l-2.5-1.5L12 20.5 9.5 19 7 20.5Z"/><path d="M10 8h4M10 12h4"/></>,
system: <><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14.2 3h-4.1l-.3 2.7a7 7 0 0 0-2 1.2l-2.5-1-2 3.4 2.1 1.5a7 7 0 0 0 0 2.4l-2 1.5 2 3.4 2.4-1a7 7 0 0 0 2 1.2l.3 2.7h4.1l.3-2.7a7 7 0 0 0 2-1.2l2.5 1 2-3.4-2.1-1.5A7 7 0 0 0 19 12Z"/></>,
search: <><circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/></>,
import: <><path d="M12 4v11M8 11l4 4 4-4"/><path d="M5 19h14"/></>,
download: <><path d="M12 4v12M7 11l5 5 5-5"/><path d="M4 17v3h16v-3"/></>,
trash: <><path d="M4 7h16M10 4h4M7 7l1 13h8l1-13M10 11v5M14 11v5"/></>,
chevronDown: <path d="M6 9l6 6 6-6"/>,
settings: <><circle cx="12" cy="12" r="3"/><path d="M12 3v2.5M12 18.5V21M4.6 6.8l1.8 1.8M17.6 15.4l1.8 1.8M3 12h2.5M18.5 12H21M4.6 17.2l1.8-1.8M17.6 8.6l1.8-1.8"/></>,
more: <><circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="12" cy="19" r="1.4"/></>,
plus: <path d="M12 5v14M5 12h14"/>,
parlor: <><path d="M4 5.5h16v11H9l-5 3.5Z"/><path d="M8 9.5h8M8 12.5h5"/></>,
generic: <><path d="M13 3 5 13.5h5L11 21l8-10.5h-5Z"/></>,
folder: <path d="M3.5 7.5h6l2-2h9v13h-17Z"/>,
file: <><path d="M6 3.5h8l4 4v13H6Z"/><path d="M14 3.5v4h4"/></>,
copy: <><rect x="8" y="8" width="10" height="11" rx="2"/><path d="M15 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2"/></>,
arrow: <><path d="M5 12h13M14 7l5 5-5 5"/></>,
chevron: <path d="m9 6 6 6-6 6"/>,
edit: <><path d="m4 20 4.5-1 10-10-3.5-3.5-10 10Z"/><path d="m13.5 6.5 3.5 3.5"/></>,
save: <><path d="M5 4h12l2 2v14H5Z"/><path d="M8 4v6h8V4M8 20v-6h8v6"/></>,
back: <path d="m15 6-6 6 6 6"/>,
}
return <svg viewBox="0 0 24 24" aria-hidden="true">{paths[name]}</svg>
}
interface TreeNode { name: string; key: string; folders: Map<string, TreeNode>; documents: KnowledgeDocumentSummary[] }
function knowledgeTree(documents: KnowledgeDocumentSummary[]) {
const root: TreeNode = { name: '知识', key: '', folders: new Map(), documents: [] }
for (const document of documents) {
const segments = document.path.split('/').filter(Boolean)
const file = segments.pop()
let cursor = root
for (const segment of segments) {
const key = cursor.key ? `${cursor.key}/${segment}` : segment
if (!cursor.folders.has(segment)) cursor.folders.set(segment, { name: segment, key, folders: new Map(), documents: [] })
cursor = cursor.folders.get(segment)!
}
cursor.documents.push({ ...document, title: document.title || file || '未命名页面' })
}
return root
}
function countTree(node: TreeNode): number {
return node.documents.length + [...node.folders.values()].reduce((sum, child) => sum + countTree(child), 0)
}
function KnowledgeTree({ node, depth, expanded, active, onToggle, onOpen, onRemoveFolder, folderMenuKey, onFolderMenuToggle }: { node: TreeNode; depth: number; expanded: Set<string>; active?: string; onToggle: (key: string) => void; onOpen: (item: KnowledgeDocumentSummary) => void; onRemoveFolder?: (folder: TreeNode) => void; folderMenuKey?: string | null; onFolderMenuToggle?: (key: string) => void }) {
const folders = [...node.folders.values()].sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
const documents = [...node.documents].sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'))
return <>
{folders.map((folder) => {
const open = depth === 0 || expanded.has(folder.key)
return <div key={folder.key} className="tree-branch">
<button className="tree-folder" style={{ paddingLeft: 12 + depth * 14 }} type="button" onClick={() => onToggle(folder.key)}>
<span className={open ? 'tree-chevron open' : 'tree-chevron'}><Icon name="chevron"/></span><Icon name="folder"/><span>{folder.name}</span><em>{countTree(folder)}</em>
</button>
{onRemoveFolder && onFolderMenuToggle && <span className="tree-folder-tools"><button className="tree-folder-menu-button" type="button" title="文件夹操作" onClick={(event) => { event.stopPropagation(); onFolderMenuToggle(folder.key) }}><Icon name="more"/></button>{folderMenuKey === folder.key && <span className="tree-folder-menu"><button type="button" onClick={(event) => { event.stopPropagation(); onFolderMenuToggle(folder.key); onRemoveFolder(folder) }}><Icon name="trash"/></button></span>}</span>}
{open && <KnowledgeTree node={folder} depth={depth + 1} expanded={expanded} active={active} onToggle={onToggle} onOpen={onOpen} onRemoveFolder={onRemoveFolder} folderMenuKey={folderMenuKey} onFolderMenuToggle={onFolderMenuToggle}/>}
</div>
})}
{documents.map((document) => <button key={`${document.source}:${document.path}`} className={active === `${document.source}:${document.path}` ? 'tree-document active' : 'tree-document'} style={{ paddingLeft: 31 + depth * 14 }} type="button" onClick={() => onOpen(document)}>
<Icon name="file"/><span>{document.title}</span>{document.duplicateCount > 0 && <em title="折叠的相同内容">+{document.duplicateCount}</em>}
</button>)}
</>
}
function HoloLakeApp() {
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
const [view, setView] = useState<ViewId>('knowledge')
const [status, setStatus] = useState<HomeStatus>(previewStatus)
const [personal, setPersonal] = useState<PersonalChannelSnapshot>(previewPersonal)
const [knowledge, setKnowledge] = useState<KnowledgeSnapshot>(previewKnowledge)
const [codeChannels, setCodeChannels] = useState<CodeChannelSnapshot>(previewCode)
const [activeDocument, setActiveDocument] = useState<KnowledgeDocument | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<KnowledgeSearchResult[] | null>(null)
const [knowledgeBusy, setKnowledgeBusy] = useState(false)
const [knowledgeMessage, setKnowledgeMessage] = useState('')
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState('')
const [lastKnowledgeReceipt, setLastKnowledgeReceipt] = useState('')
const [cloneUrl, setCloneUrl] = useState('')
const [codeBusy, setCodeBusy] = useState(false)
const [codeMessage, setCodeMessage] = useState('')
const [activeChannel, setActiveChannel] = useState<CodeChannelEntry | null>(null)
const [codeTree, setCodeTree] = useState<CodeTreeSnapshot | null>(null)
const [activeCodeFile, setActiveCodeFile] = useState<CodeFileProjection | null>(null)
const [codeMode, setCodeMode] = useState<'human' | 'source'>('human')
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('hololake-sidebar-collapsed') === '1')
const [inspectorOpen, setInspectorOpen] = useState(() => window.localStorage.getItem('hololake-inspector-open') !== '0')
const toggleSidebar = () => setSidebarCollapsed((current) => { window.localStorage.setItem('hololake-sidebar-collapsed', current ? '0' : '1'); return !current })
const toggleInspector = () => setInspectorOpen((current) => { window.localStorage.setItem('hololake-inspector-open', current ? '0' : '1'); return !current })
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null)
const headingScrollTimer = useRef<number | null>(null)
const [settingsMenuOpen, setSettingsMenuOpen] = useState(false)
const [folderMenuKey, setFolderMenuKey] = useState<string | null>(null)
const [parlorMessages, setParlorMessages] = useState<{ role: 'user' | 'agent' | 'note'; text: string }[]>([])
const [parlorInput, setParlorInput] = useState('')
const [parlorBusy, setParlorBusy] = useState(false)
const [parlorLayer, setParlorLayer] = useState<'language' | 'execution'>('language')
const parlorScrollRef = useRef<HTMLDivElement>(null)
useEffect(() => { parlorScrollRef.current?.scrollTo({ top: parlorScrollRef.current.scrollHeight }) }, [parlorMessages, parlorBusy])
const [genericMessages, setGenericMessages] = useState<Array<{ role: string; text: string }>>([])
const [genericInput, setGenericInput] = useState('')
const [genericBusy, setGenericBusy] = useState(false)
const genericScrollRef = useRef<HTMLDivElement>(null)
useEffect(() => { genericScrollRef.current?.scrollTo({ top: genericScrollRef.current.scrollHeight }) }, [genericMessages, genericBusy])
const [displayName, setDisplayName] = useState('')
const [identityBusy, setIdentityBusy] = useState(false)
const [identityMessage, setIdentityMessage] = useState('')
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [systemMessage, setSystemMessage] = useState('')
const [releaseCandidate, setReleaseCandidate] = useState<ReleaseCandidate | null>(null)
const [repoLogin, setRepoLogin] = useState<LoginSession | null>(null)
const [loginUsername, setLoginUsername] = useState('')
const [loginPassword, setLoginPassword] = useState('')
const [loginBusy, setLoginBusy] = useState(false)
const [loginRising, setLoginRising] = useState(false)
const [loginMessage, setLoginMessage] = useState('')
const [gateStage, setGateStage] = useState<'number' | 'key'>('number')
const [gateRaw, setGateRaw] = useState('')
const [gateInf, setGateInf] = useState(false)
const gateNumber = formatGateNumber(gateRaw) + (gateInf ? '∞' : '')
const [gateOpen, setGateOpen] = useState(false)
const gateInputRef = useRef<HTMLInputElement>(null)
// 封存舱点开翻转后半程再把光标送进输入格,免得翻到一半抢焦点
useEffect(() => {
if (!gateOpen || gateStage !== 'number') return
const timer = window.setTimeout(() => gateInputRef.current?.focus(), 640)
return () => window.clearTimeout(timer)
}, [gateOpen, gateStage])
const [gateBusy, setGateBusy] = useState(false)
const [gateMessage, setGateMessage] = useState('')
const [gateRising, setGateRising] = useState(false)
const [systemBusy, setSystemBusy] = useState(false)
const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null)
const [zpNumber, setZpNumber] = useState('')
const [zpBusy, setZpBusy] = useState(false)
const [zpMessage, setZpMessage] = useState('')
const [apiForm, setApiForm] = useState({ baseUrl: '', apiKey: '', model: '' })
const [apiReady, setApiReady] = useState(false)
const refreshCore = useCallback(async () => {
const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([
invoke<HomeStatus>('get_hololake_home_status'),
invoke<PersonalChannelSnapshot>('get_personal_channel_snapshot'),
invoke<KnowledgeSnapshot>('get_knowledge_snapshot'),
invoke<CodeChannelSnapshot>('get_code_channel_snapshot'),
])
if (homeResult.status === 'fulfilled') setStatus(homeResult.value)
if (personalResult.status === 'fulfilled') setPersonal(personalResult.value)
if (knowledgeResult.status === 'fulfilled') setKnowledge(knowledgeResult.value)
if (codeResult.status === 'fulfilled') setCodeChannels(codeResult.value)
}, [])
const loadReceipts = useCallback(async () => {
try {
const projection = await invoke<ReceiptProjection>('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } })
setReceipts(projection.events)
} catch { setReceipts([]) }
}, [])
useEffect(() => {
void refreshCore()
const timer = window.setInterval(() => void refreshCore(), 3500)
return () => window.clearInterval(timer)
}, [refreshCore])
useEffect(() => {
document.documentElement.dataset.theme = theme
window.localStorage.setItem('hololake-theme', theme)
}, [theme])
useEffect(() => {
invoke<LoginSession | null>('check_code_repo_login').then((session) => setRepoLogin(session)).catch(() => setRepoLogin(null))
}, [])
const loadZeroPoint = useCallback(async () => {
try { setZeroPoint(await invoke<ZeroPointSnapshot>('zero_point_status')) } catch { /* 执行手脚启动中,下轮补 */ }
}, [])
useEffect(() => {
void loadZeroPoint()
const timer = window.setInterval(() => void loadZeroPoint(), 10000)
return () => window.clearInterval(timer)
}, [loadZeroPoint])
useEffect(() => {
invoke<{ baseUrl: string; model: string; hasKey: boolean }>('zero_point_api_config')
.then((cfg) => { setApiForm((prev) => ({ ...prev, baseUrl: cfg.baseUrl, model: cfg.model })); setApiReady(cfg.hasKey && cfg.baseUrl !== '') })
.catch(() => { /* 钥匙库未落,等用户配置 */ })
}, [])
useEffect(() => {
if (!zeroPoint) return
if (zeroPoint.route !== 'persona' && view === 'parlor') setView('generic')
if (zeroPoint.route === 'persona' && view === 'generic') setView('knowledge')
}, [zeroPoint, view])
// 大门·编号门:本机已有绑定编号时回填,省得主人重报家门
useEffect(() => {
if (gateStage !== 'number' || gateRaw) return
const bound = zeroPoint?.binding === 'bound' ? zeroPoint.userNumber : ''
if (!bound) return
setGateInf(bound.includes('∞'))
setGateRaw(bound.toUpperCase().replace(/[^A-Z0-9]/g, ''))
}, [zeroPoint, gateStage, gateRaw])
// 大门·编号门输入:只收字母数字(杠系统补、∞开关点),删到∞时开关跟着灭
const onGateInput = (value: string) => {
if (gateInf && !value.includes('∞')) setGateInf(false)
setGateRaw(value.toUpperCase().replace(/[^A-Z0-9]/g, ''))
}
// 大门·编号门:先报编号→灯塔查号→只答有效/无效→域浮起→才见钥匙门
const gateVerifyNumber = async () => {
setGateBusy(true)
setGateMessage('')
try {
await invoke<ZeroPointSnapshot>('zero_point_bind', { input: { number: gateNumber } })
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(snapshot)
if (snapshot.route === 'persona') {
setGateRising(true)
window.setTimeout(() => { setGateRising(false); setGateStage('key') }, 2600)
} else {
setGateMessage('编号无效')
}
} catch (error) {
setGateMessage(humanError(error, 'login'))
} finally {
setGateBusy(false)
}
}
useEffect(() => { if (view === 'receipts') void loadReceipts() }, [loadReceipts, view])
const visibleDocuments = useMemo(() => {
if (!searchResults) return knowledge.documents
return searchResults.map((result) => knowledge.documents.find((item) => item.source === result.source && item.path === result.path)).filter(Boolean) as KnowledgeDocumentSummary[]
}, [knowledge.documents, searchResults])
const tree = useMemo(() => knowledgeTree(visibleDocuments), [visibleDocuments])
const activeSummary = activeDocument
? knowledge.documents.find((item) => item.source === activeDocument.source && item.path === activeDocument.path)
: undefined
const parsedDocument = useMemo(() => activeDocument ? splitFrontmatter(activeDocument.body) : null, [activeDocument])
const outline = useMemo(() => activeDocument ? documentOutline(activeDocument.body) : [], [activeDocument])
const docStats = useMemo(() => {
if (!activeDocument) return null
const chars = activeDocument.body.replace(/\s+/g, '').length
return { chars, minutes: Math.max(1, Math.ceil(chars / 400)) }
}, [activeDocument])
useEffect(() => { setActiveHeadingId(null) }, [activeDocument?.path])
useEffect(() => {
if (!activeHeadingId) return
document.querySelector('.outline-list button.active')?.scrollIntoView({ block: 'nearest' })
}, [activeHeadingId])
const handleReaderScroll = (event: React.UIEvent<HTMLDivElement>) => {
const container = event.currentTarget
if (headingScrollTimer.current !== null) return
headingScrollTimer.current = window.setTimeout(() => {
headingScrollTimer.current = null
const containerTop = container.getBoundingClientRect().top
let current: string | null = null
for (const heading of Array.from(container.querySelectorAll<HTMLElement>('h1[id], h2[id], h3[id], h4[id]'))) {
if (heading.getBoundingClientRect().top - containerTop <= 28) current = heading.id
else break
}
setActiveHeadingId(current)
}, 100)
}
const connectionOnline = status.directConnectionCount > 0
const connectionLabel = connectionOnline
? '本地原生连接 · 客户端在线'
: status.resumableSessionCount > 0
? '原生会话可续接 · 当前离线'
: '原生接口已驻留 · 尚无客户端'
const openDocument = async (source: KnowledgeSource, path: string) => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
setEditing(false)
try {
const document = await invoke<KnowledgeDocument>('read_knowledge_document', { input: { source, path } })
setActiveDocument(document)
setDraft(document.body)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const openWiki = (target: string) => {
const normalized = target.replace(/\.md$/i, '').toLowerCase()
const match = knowledge.documents.find((item) =>
item.title.toLowerCase() === normalized || item.path.replace(/\.md$/i, '').toLowerCase().endsWith(normalized))
if (match) void openDocument(match.source, match.path)
else setKnowledgeMessage(`未找到链接页面:${target}`)
}
const runSearch = async (event: React.FormEvent) => {
event.preventDefault()
if (!searchQuery.trim()) { setSearchResults(null); return }
setKnowledgeBusy(true)
try {
setSearchResults(await invoke<KnowledgeSearchResult[]>('search_knowledge', { input: { query: searchQuery } }))
setActiveDocument(null)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const importKnowledge = async () => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const result = await invoke<KnowledgeImportResult | null>('select_and_import_knowledge_folder')
if (!result) return
setKnowledge(result.snapshot)
setSearchResults(null)
setSearchQuery('')
setLastKnowledgeReceipt(result.gitCommit)
setKnowledgeMessage(`导入完成:新增 ${result.importedDocuments},已存在 ${result.existingDocuments},冲突 ${result.conflicts}`)
if (result.firstDocument) await openDocument('native', result.firstDocument)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const downloadDocument = async (format: 'md' | 'html') => {
if (!activeDocument) return
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
let body = activeDocument.body
if (format === 'html') {
const parsed = splitFrontmatter(body)
const article = markdownHtml(parsed.content)
body = `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>${activeDocument.title}</title><style>body{max-width:820px;margin:40px auto;padding:0 24px;font:15px/1.8 -apple-system,'Segoe UI','PingFang SC','Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#1f2330;background:#fff}h1,h2,h3{line-height:1.4}blockquote{margin:20px 0;padding:12px 16px;border-left:3px solid #a78bfa;background:#f3f0ff;border-radius:8px}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:7px 10px}pre{background:#f4f5f8;padding:14px;border-radius:8px;overflow:auto}img{max-width:100%}</style></head><body><h1>${activeDocument.title}</h1>${article}</body></html>`
}
const receipt = await invoke<{ path: string; bytes: number } | null>('export_knowledge_document', { input: { title: activeDocument.title, extension: format, body } })
setKnowledgeMessage(receipt ? `已下载到 ${receipt.path}` : '已取消下载。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const printDocument = async () => {
setKnowledgeMessage('')
try {
await invoke('print_knowledge_document')
setKnowledgeMessage('打印框里选「存储为 PDF」即可得到 PDF 文件。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
}
const reloadKnowledge = async () => {
const snapshot = await invoke<KnowledgeSnapshot>('get_knowledge_snapshot')
setKnowledge(snapshot)
}
const askParlor = async () => {
const message = parlorInput.trim()
if (!message || parlorBusy) return
setParlorInput('')
setParlorMessages((current) => [...current, { role: 'user', text: message }])
setParlorBusy(true)
try {
const receipt = await invoke<{ reply: string; layer: string; freshSession: boolean }>('agent_parlor_ask', { input: { message } })
setParlorMessages((current) => [...current, { role: 'agent', text: receipt.reply }])
} catch (error) { setParlorMessages((current) => [...current, { role: 'note', text: humanError(error, 'knowledge') }]) }
finally { setParlorBusy(false) }
}
const switchParlorLayer = async () => {
try {
const target = parlorLayer === 'execution' ? 'language' : 'execution'
const actual = await invoke<string>('agent_parlor_switch_layer', { input: { layer: target } })
setParlorLayer(actual === 'execution' ? 'execution' : 'language')
setParlorMessages((current) => [...current, { role: 'note', text: actual === 'execution' ? '已切换到现实开发执行层:归灯获得执行权限,语言即现实。' : '已回到语言推理层:只对话不动手。' }])
} catch (error) { setParlorMessages((current) => [...current, { role: 'note', text: humanError(error, 'knowledge') }]) }
}
const createDocument = async () => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const receipt = await invoke<{ removed: string; pages: number }>('create_knowledge_document', { input: { title: '未命名页面' } })
setKnowledgeMessage(`已新建空白页「${receipt.removed.replace(/\.md$/, '')}」,正在编辑。`)
setSearchResults(null)
await reloadKnowledge()
await openDocument('native', receipt.removed)
setEditing(true)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const removeDocument = async () => {
if (!activeDocument) return
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const receipt = await invoke<{ removed: string; pages: number } | null>('delete_knowledge_document', { input: { source: activeDocument.source, path: activeDocument.path } })
if (receipt) {
setKnowledgeMessage(`已移入回收站:${receipt.removed}(可找回)`)
setActiveDocument(null)
setSearchResults(null)
await reloadKnowledge()
} else setKnowledgeMessage('已取消删除。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const removeFolder = async (folder: TreeNode) => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const receipt = await invoke<{ removed: string; pages: number } | null>('delete_knowledge_folder', { input: { folder: folder.key } })
if (receipt) {
setKnowledgeMessage(`文件夹「${folder.name}」(${receipt.pages} 页)已移入回收站(可找回)`)
setActiveDocument(null)
setSearchResults(null)
await reloadKnowledge()
} else setKnowledgeMessage('已取消删除。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const saveDocument = async () => {
if (!activeDocument?.writable) return
setKnowledgeBusy(true)
try {
const result = await invoke<KnowledgeSaveResult>('save_knowledge_document', {
input: { path: activeDocument.path, body: draft, expectedContentSha256: activeDocument.contentSha256 },
})
setActiveDocument(result.document)
setDraft(result.document.body)
setEditing(false)
setLastKnowledgeReceipt(result.gitCommit)
setKnowledgeMessage(result.state === 'UNCHANGED' ? '内容没有变化。' : '页面已保存,并写入本机 Git 回执。')
await refreshCore()
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const cloneCodeChannel = async (event: React.FormEvent) => {
event.preventDefault()
if (!cloneUrl.trim()) return
setCodeBusy(true)
setCodeMessage('')
try {
const snapshot = await invoke<CodeChannelSnapshot>('clone_code_channel', { input: { url: cloneUrl } })
setCodeChannels(snapshot)
setCloneUrl('')
setCodeMessage('代码频道已克隆并登记。')
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const selectLocalCodeChannel = async () => {
setCodeBusy(true)
setCodeMessage('')
try {
const snapshot = await invoke<CodeChannelSnapshot | null>('select_local_code_channel')
if (snapshot) {
setCodeChannels(snapshot)
setCodeMessage('本地代码频道已登记,原文件未移动。')
}
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const browseChannel = async (channel: CodeChannelEntry, path = '') => {
setCodeBusy(true)
setActiveChannel(channel)
setActiveCodeFile(null)
try {
setCodeTree(await invoke<CodeTreeSnapshot>('browse_code_channel', { input: { channelId: channel.channelId, path } }))
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const openCodeEntry = async (entry: CodeTreeEntry) => {
if (!activeChannel) return
if (entry.kind === 'directory') { await browseChannel(activeChannel, entry.path); return }
setCodeBusy(true)
setCodeMode('human')
try {
setActiveCodeFile(await invoke<CodeFileProjection>('read_code_channel_file', {
input: { channelId: activeChannel.channelId, path: entry.path },
}))
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const codeParent = () => codeTree?.path.split('/').filter(Boolean).slice(0, -1).join('/') || ''
const performRepoLogin = async (event: React.FormEvent) => {
event.preventDefault()
setLoginBusy(true)
setLoginMessage('')
try {
const receipt = await invoke<LoginReceipt>('perform_code_repo_login', { username: loginUsername.trim(), password: loginPassword })
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
setLoginRising(true)
await new Promise((resolve) => setTimeout(resolve, 1300))
setRepoLogin({ username: receipt.username, host: receipt.host, signedInAtUnixMs: Date.now() })
setLoginRising(false)
setLoginPassword('')
} catch (error) { setLoginMessage(humanError(error, 'login')) }
finally { setLoginBusy(false) }
}
const signOutRepo = async () => {
try { await invoke('sign_out_code_repo_login') } catch { /* 登出以本机清场为准 */ }
setRepoLogin(null)
}
const initializeIdentity = async (event: React.FormEvent) => {
event.preventDefault()
setIdentityBusy(true)
try {
setPersonal(await invoke<PersonalChannelSnapshot>('initialize_personal_channel', { input: { displayName } }))
setDisplayName('')
} catch (error) { setIdentityMessage(humanError(error, 'identity')) }
finally { setIdentityBusy(false) }
}
const issueInvitation = async () => {
setSystemBusy(true)
setSystemMessage('')
try {
setTicket(await invoke<DiscoveryTicketReceipt>('issue_direct_local_discovery_ticket', {
input: {
accountId: getOrCreateLocalId('hololake-local-account', 'human-local'),
laneId: 'personal-channel',
clientInstanceId: getOrCreateLocalId('hololake-external-ai-client', 'external-ai'),
},
}))
setSystemMessage('一次性发现凭据已生成。连接成功后,状态条会显示真实在线数。')
} catch (error) { setSystemMessage(humanError(error, 'system')) }
finally { setSystemBusy(false) }
}
const invitationText = ticket ? JSON.stringify({
schema: 'hololake.direct-local-invitation/v1',
connectorArgument: '--connector',
openSession: {
operation: 'OPEN_SESSION',
input: {
accountId: window.localStorage.getItem('hololake-local-account'),
laneId: ticket.laneId,
clientInstanceId: ticket.clientInstanceId,
discoveryTicket: ticket.discoveryTicket,
},
},
}, null, 2) : ''
const copyInvitation = async () => {
try {
await navigator.clipboard.writeText(invitationText)
setSystemMessage('连接凭据已复制。')
} catch { setSystemMessage('复制失败,请手动选择凭据。') }
}
const checkUpdate = async () => {
setSystemBusy(true)
try {
const result = await invoke<ReleaseCheckReceipt>('check_hololake_update')
setReleaseCandidate(result.candidate || null)
setSystemMessage(result.candidate ? '发现已签名更新。' : '当前没有可安装更新。')
} catch (error) { setSystemMessage(humanError(error, 'system')) }
finally { setSystemBusy(false) }
}
const installUpdate = async () => {
if (!releaseCandidate) return
setSystemBusy(true)
try {
await invoke('confirm_hololake_update_install', {
input: { candidateId: releaseCandidate.candidateId, confirmationToken: releaseCandidate.confirmationToken },
})
setSystemMessage('更新安装已启动。')
} catch (error) { setSystemMessage(humanError(error, 'system')) }
finally { setSystemBusy(false) }
}
const bindZeroPoint = async () => {
if (!zpNumber.trim()) return
setZpBusy(true); setZpMessage('')
try {
const bound = await invoke<ZeroPointSnapshot>('zero_point_bind', { input: { number: zpNumber.trim() } })
setZeroPoint(bound)
const verified = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(verified)
setZpMessage(verified.route === 'persona' ? '编号合法,人格层路由已开' : '人格系统未授权已切换至通用AI运行层')
} catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) }
}
const verifyZeroPoint = async () => {
setZpBusy(true); setZpMessage('')
try {
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(snapshot)
setZpMessage(snapshot.route === 'persona' ? '校验通过,人格层路由已开' : '人格系统未授权已切换至通用AI运行层')
} catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) }
}
const syncZeroPoint = async () => {
setZpBusy(true); setZpMessage('')
try { setZeroPoint(await invoke<ZeroPointSnapshot>('zero_point_sync')) } catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) }
}
const saveApiConfig = async () => {
try {
await invoke('zero_point_save_api', { input: { baseUrl: apiForm.baseUrl, apiKey: apiForm.apiKey, model: apiForm.model } })
setApiReady(true)
setGenericMessages((rows) => [...rows, { role: 'note', text: 'API 已收进底层钥匙库通用AI运行层就绪' }])
} catch (error) { setGenericMessages((rows) => [...rows, { role: 'note', text: `保存失败:${String(error)}` }]) }
}
const askGeneric = async () => {
const message = genericInput.trim()
if (!message || genericBusy) return
const history = genericMessages.filter((row) => row.role !== 'note').map((row) => ({ role: row.role === 'user' ? 'user' : 'assistant', content: row.text }))
setGenericMessages((rows) => [...rows, { role: 'user', text: message }])
setGenericInput(''); setGenericBusy(true)
try {
const reply = await invoke<string>('generic_layer_chat', { input: { messages: [...history, { role: 'user', content: message }] } })
setGenericMessages((rows) => [...rows, { role: 'agent', text: reply }])
} catch (error) { setGenericMessages((rows) => [...rows, { role: 'note', text: `通用层回应失败:${String(error)}` }]) } finally { setGenericBusy(false) }
}
const renderGeneric = () => (
<section className="content-page parlor-page">
<header className="page-title"><div><span className="kicker">GENERIC AI LAYER</span><h1>AI运行层</h1><p> API · </p></div><span className="status-chip"></span></header>
<section className="plain-panel zp-api-panel">
<header><div><h2> API </h2><p>{apiReady ? ' · 已就绪' : ''}</p></div></header>
<div className="zp-api-row">
<input value={apiForm.baseUrl} placeholder="API 端点,如 https://dashscope.aliyuncs.com/compatible-mode/v1" onChange={(event) => setApiForm((prev) => ({ ...prev, baseUrl: event.target.value }))}/>
<input type="password" value={apiForm.apiKey} placeholder="API 钥匙" onChange={(event) => setApiForm((prev) => ({ ...prev, apiKey: event.target.value }))}/>
<input value={apiForm.model} placeholder="模型名,如 qwen3-max" onChange={(event) => setApiForm((prev) => ({ ...prev, model: event.target.value }))}/>
<button className="primary-button" type="button" onClick={() => void saveApiConfig()}></button>
</div>
</section>
<div className="parlor-scroll" ref={genericScrollRef}>
{genericMessages.length === 0 && <p className="parlor-note"> API API </p>}
{genericMessages.map((item, index) => (
<div key={index} className={`parlor-row ${item.role}`}>
{item.role === 'user' ? <span className="parlor-who"></span> : item.role === 'agent' ? <span className="parlor-who">AI</span> : null}
<div className={item.role === 'note' ? 'parlor-note' : 'parlor-bubble'}>{item.text}</div>
</div>
))}
{genericBusy && <div className="parlor-row agent"><span className="parlor-who">AI</span><div className="parlor-bubble thinking"></div></div>}
</div>
<form className="parlor-input-row" onSubmit={(event) => { event.preventDefault(); void askGeneric() }}>
<input maxLength={2000} value={genericInput} placeholder={apiReady ? '跟通用AI说话…' : '先把模型 API 收进钥匙库'} onChange={(event) => setGenericInput(event.target.value)}/>
<button className="primary-button" disabled={genericBusy || !genericInput.trim() || !apiReady}>{genericBusy ? '等回应…' : '说话'}</button>
</form>
</section>
)
const renderOverview = () => (
<section className="content-page overview-page">
<header className="page-title">
<div><span className="kicker">PERSONAL HOLOLAKE</span><h1></h1><p></p></div>
<button className="evidence-pill" type="button" onClick={() => setView('system')}><i className={connectionOnline ? 'online' : ''}/>{connectionLabel}</button>
</header>
<div className="overview-grid">
<button className="metric-panel" type="button" onClick={() => setView('knowledge')}><span></span><strong>{knowledge.uniqueDocumentCount}</strong><small>{knowledge.duplicateDocumentCount} </small></button>
<button className="metric-panel" type="button" onClick={() => setView('code')}><span></span><strong>{codeChannels.channels.length}</strong><small></small></button>
<button className="metric-panel" type="button" onClick={() => setView('receipts')}><span></span><strong>{personal.integrity.receiptCount + status.pnccReceiptCount}</strong><small></small></button>
</div>
<div className="overview-columns">
<section className="plain-panel recent-panel">
<header><div><h2></h2><p></p></div><button type="button" onClick={() => setView('knowledge')}></button></header>
{knowledge.documents.slice().sort((a, b) => b.updatedAtUnixMs - a.updatedAtUnixMs).slice(0, 7).map((item) =>
<button className="recent-row" key={`${item.source}:${item.path}`} type="button" onClick={() => { setView('knowledge'); void openDocument(item.source, item.path) }}>
<Icon name="file"/><span><b>{item.title}</b><small>{item.path}</small></span><time>{new Date(item.updatedAtUnixMs).toLocaleDateString('zh-CN')}</time>
</button>)}
</section>
<section className="plain-panel system-proof">
<header><div><h2></h2><p>使</p></div></header>
<dl><div><dt>Unix </dt><dd>{status.directLocalBrokerState}</dd></div><div><dt>线</dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{status.resumableSessionCount}</dd></div><div><dt>PNCC </dt><dd>{status.pnccReceiptCount}</dd></div><div><dt>MCP</dt><dd> / </dd></div></dl>
<button className="secondary-button" type="button" onClick={() => setView('system')}></button>
</section>
</div>
</section>
)
const renderKnowledge = () => (
<section className="full-workbench knowledge-page">
<aside className="knowledge-browser">
<header><div><span className="kicker">KNOWLEDGE</span><h1></h1></div><button className="icon-button" title="导入文件夹" type="button" disabled={knowledgeBusy} onClick={() => void importKnowledge()}><Icon name="import"/></button></header>
<form className="search-box" onSubmit={(event) => void runSearch(event)}>
<Icon name="search"/><input aria-label="检索知识" value={searchQuery} placeholder="检索标题与正文" onChange={(event) => setSearchQuery(event.target.value)}/>
{searchResults && <button type="button" onClick={() => { setSearchResults(null); setSearchQuery('') }}></button>}
</form>
<div className="knowledge-counts"><span>{knowledge.uniqueDocumentCount} </span><span>{knowledge.duplicateDocumentCount} </span></div>
<div className="knowledge-tree" aria-busy={knowledgeBusy}>
{visibleDocuments.length
? <KnowledgeTree node={tree} depth={0} expanded={expanded} active={activeDocument ? `${activeDocument.source}:${activeDocument.path}` : undefined}
onToggle={(key) => setExpanded((current) => { const next = new Set(current); if (next.has(key)) next.delete(key); else next.add(key); return next })}
onOpen={(item) => void openDocument(item.source, item.path)}
onRemoveFolder={(folder) => void removeFolder(folder)}
folderMenuKey={folderMenuKey}
onFolderMenuToggle={(key) => setFolderMenuKey((current) => (current === key ? null : key))}/>
: <div className="empty-state"></div>}
</div>
<footer>{knowledgeMessage || (lastKnowledgeReceipt ? `Git ${lastKnowledgeReceipt.slice(0, 10)}` : '导入会自动检查相同内容')}</footer>
</aside>
<main className="document-workspace">
{activeDocument ? <>
<header className="document-toolbar">
<div className="breadcrumbs"><span>{activeDocument.source === 'legacy' ? 'HoloLake Era · 只读源' : '我的知识库'}</span><b>/</b><span>{activeDocument.path}</span></div>
<div className="toolbar-actions"><button className="toolbar-button" type="button" title={inspectorOpen ? '收起右侧大纲与证据' : '展开右侧大纲与证据'} onClick={toggleInspector}><Icon name="chevron"/>{inspectorOpen ? '收起大纲' : '展开大纲'}</button>{editing
? <><button className="toolbar-button" type="button" onClick={() => { setEditing(false); setDraft(activeDocument.body) }}></button><button className="toolbar-button primary" type="button" disabled={knowledgeBusy} onClick={() => void saveDocument()}><Icon name="save"/></button></>
: <><div className="toolbar-menu"><button className="toolbar-button" type="button" title="页面操作与常用功能" disabled={knowledgeBusy} onClick={() => setSettingsMenuOpen((current) => !current)}><Icon name="settings"/> <Icon name="chevronDown"/></button>{settingsMenuOpen && <div className="toolbar-menu-pop"><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void createDocument() }}><Icon name="plus"/></button><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void importKnowledge() }}><Icon name="import"/></button>{activeDocument.writable && <button type="button" onClick={() => { setSettingsMenuOpen(false); setDraft(activeDocument.body); setEditing(true) }}><Icon name="edit"/></button>}{activeDocument.writable && <button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void removeDocument() }}><Icon name="trash"/></button>}<span className="toolbar-menu-sep"/><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void downloadDocument('md') }}><Icon name="download"/> Markdown</button><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void downloadDocument('html') }}><Icon name="download"/>.html</button><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void printDocument() }}><Icon name="download"/> PDF</button></div>}</div></>}
</div>
</header>
<div className="document-scroll" onScroll={handleReaderScroll}>
{editing
? <textarea className="document-editor" aria-label="Markdown 编辑器" value={draft} onChange={(event) => setDraft(event.target.value)}/>
: <><header className="reader-heading"><h1>{activeDocument.title}</h1>{docStats && <div className="meta-stats">{docStats.chars.toLocaleString()} · {docStats.minutes} {parsedDocument?.metadata?.created ? ` · 创建于 ${String(parsedDocument.metadata.created).slice(0, 10)}` : ''}{parsedDocument?.metadata?.updated ? ` · 更新于 ${String(parsedDocument.metadata.updated).slice(0, 10)}` : ''}</div>}{(parsedDocument?.tags?.length ?? 0) > 0 && <div className="meta-tags">{parsedDocument!.tags.map((tag) => <span key={tag} className={tagTint(tag)}>{tag}</span>)}</div>}<div>{Object.entries(parsedDocument?.metadata || {}).filter(([key]) => key !== 'tags').slice(0, 5).map(([key, value]) => <span key={key}>{key} · {value}</span>)}</div></header><MarkdownDocument body={activeDocument.body} knownTitles={knowledge.documents.map((doc) => doc.title)} onWiki={openWiki}/></>}
</div>
</> : <div className="workbench-empty"><span></span><h2></h2><p></p><button className="primary-button" type="button" onClick={() => void importKnowledge()}></button></div>}
</main>
{inspectorOpen && <aside className="document-inspector">
<div className="inspector-tabs"><span className="active"></span><span></span><span></span></div>
{activeDocument ? <div className="inspector-scroll">
<section><h2></h2>{outline.length ? <nav className="outline-list">{outline.map((item) => <button key={item.id} type="button" title="跳到该章节" className={activeHeadingId === item.id ? 'active' : ''} style={{ paddingLeft: 8 + (item.level - 1) * 11 }} onClick={() => jumpToHeading(item.id)}>{item.title}</button>)}</nav> : <p></p>}</section>
<section><h2></h2><dl><div><dt></dt><dd>{activeDocument.source === 'native' ? 'HoloLake 本机 Git' : 'HoloLake Era 只读供体'}</dd></div><div><dt></dt><dd>{activeDocument.path}</dd></div><div><dt></dt><dd>{activeDocument.contentSha256.slice(0, 16)}</dd></div><div><dt></dt><dd>{activeSummary?.duplicateCount || 0} </dd></div></dl></section>
<section><h2></h2><p>{activeDocument.writable ? '可编辑;保存时写入本机 Git。' : '供体原件只读;不会被修改。'}</p>{lastKnowledgeReceipt && <code>{lastKnowledgeReceipt}</code>}</section>
</div> : <div className="empty-state"></div>}
</aside>}
</section>
)
const renderCode = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
<header><div><span className="kicker">CODE CHANNEL</span><h1></h1></div></header>
<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>
</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>
<div className="repository-entries">{codeTree?.entries.map((entry) => <button key={entry.path} type="button" onClick={() => void openCodeEntry(entry)}><Icon name={entry.kind === 'directory' ? 'folder' : 'file'}/><span>{entry.name}</span>{entry.kind === 'directory' && <Icon name="chevron"/>}</button>) || <div className="empty-state"></div>}</div>
</aside>
<main className="code-reader">
{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>}
</main>
</section>
)
const renderReceipts = () => (
<section className="content-page">
<header className="page-title"><div><span className="kicker">LOCAL RECEIPTS</span><h1></h1><p></p></div></header>
<div className="receipt-grid">
<section className="plain-panel"><h2></h2>{personal.recentEvents.map((event) => <article className="receipt-row" key={event.eventId}><span>{String(event.sequence).padStart(2, '0')}</span><div><b>{event.summary}</b><small>{new Date(event.occurredAtUnixMs).toLocaleString('zh-CN')} · {event.receiptHash.slice(0, 12)}</small></div></article>)}</section>
<section className="plain-panel"><h2>PNCC </h2>{receipts.length ? receipts.map((receipt) => <article className="receipt-row" key={receipt.sequence}><span>{String(receipt.sequence).padStart(2, '0')}</span><div><b>{receipt.kind}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></article>) : <div className="empty-state"> PNCC </div>}</section>
</div>
</section>
)
const renderSystem = () => (
<section className="content-page">
<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></h2><p>Unix Stream 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>}
</section>
<section className="plain-panel"><header><div><h2>GH-PNCC </h2><p></p></div></header><dl className="evidence-list"><div><dt></dt><dd>{status.codeRepositoryMountCount}</dd></div><div><dt></dt><dd>{status.pnccReceiptCount}</dd></div><div><dt></dt><dd>{status.codeRepositoryMountCount > 0 && status.pnccReceiptCount > 0 ? '已有运行证据' : '架构已接入,尚无完整运行投影'}</dd></div></dl></section>
<section className="plain-panel">
<header><div><h2> · </h2><p></p></div><span className={zeroPoint?.route === 'persona' ? 'status-chip online' : 'status-chip'}>{zeroPoint ? (zeroPoint.route === 'persona' ? '人格层' : '通用AI层') : '启动中'}</span></header>
<dl className="evidence-list">
<div><dt></dt><dd>{zeroPoint ? (zeroPoint.binding === 'bound' ? `已绑定(${zeroPoint.userNumber}` : '等待绑定(空白态)') : '—'}</dd></div>
<div><dt></dt><dd>{zeroPoint?.protocol.origin ?? '—'}</dd></div>
<div><dt>线</dt><dd>{zeroPoint ? `${zeroPoint.protocol.gracePeriodDays} 天(协议值)` : '—'}</dd></div>
<div><dt></dt><dd>{zeroPoint?.syncNote ?? '—'}</dd></div>
</dl>
<div className="zp-api-row">
<input value={zpNumber} placeholder="输入用户编号,如 ICE-GL∞" onChange={(event) => setZpNumber(event.target.value)}/>
<button className="secondary-button" type="button" disabled={zpBusy || !zpNumber.trim()} onClick={() => void bindZeroPoint()}></button>
<button className="secondary-button" type="button" disabled={zpBusy || zeroPoint?.binding !== 'bound'} onClick={() => void verifyZeroPoint()}></button>
<button className="secondary-button" type="button" disabled={zpBusy} onClick={() => void syncZeroPoint()}></button>
</div>
{zpMessage && <p className="global-message">{zpMessage}</p>}
</section>
<section className="plain-panel"><header><div><h2></h2><p></p></div></header><div className="theme-options">{themes.map((choice) => <button className={theme === choice.id ? 'active' : ''} key={choice.id} type="button" onClick={() => setTheme(choice.id)}><i className={choice.id}/><span>{choice.name}</span></button>)}</div></section>
<section className="plain-panel"><header><div><h2></h2><p></p></div></header>{releaseCandidate ? <div className="release-summary"><b>HoloLake {releaseCandidate.version}</b><p>{releaseCandidate.notes}</p><button className="primary-button" onClick={() => void installUpdate()}></button></div> : <button className="secondary-button" disabled={systemBusy} onClick={() => void checkUpdate()}></button>}</section>
</div>
{systemMessage && <p className="global-message">{systemMessage}</p>}
</section>
)
const renderParlor = () => (
<section className="content-page parlor-page">
<header className="page-title"><div><span className="kicker">AGENT PARLOR</span><h1></h1><p></p></div>
<div className="parlor-layer">
<span className={parlorLayer === 'execution' ? 'status-chip online' : 'status-chip'}>{parlorLayer === 'execution' ? '现实开发执行层' : '语言推理层'}</span>
<button className="secondary-button" type="button" onClick={() => void switchParlorLayer()}>{parlorLayer === 'execution' ? '回到语言推理层' : '切换到现实开发执行层'}</button>
</div>
</header>
<div className="parlor-scroll" ref={parlorScrollRef}>
<p className="parlor-note"></p>
{parlorMessages.map((item, index) => (
<div key={index} className={`parlor-row ${item.role}`}>
{item.role === 'user' ? <span className="parlor-who"></span> : item.role === 'agent' ? <span className="parlor-who"></span> : null}
<div className={item.role === 'note' ? 'parlor-note' : 'parlor-bubble'}>{item.text}</div>
</div>
))}
{parlorBusy && <div className="parlor-row agent"><span className="parlor-who"></span><div className="parlor-bubble thinking"></div></div>}
</div>
<form className="parlor-input-row" onSubmit={(event) => { event.preventDefault(); void askParlor() }}>
<input maxLength={2000} value={parlorInput} placeholder={parlorLayer === 'execution' ? '执行层:语言即现实,说要做什么' : '跟人格体说话…'} onChange={(event) => setParlorInput(event.target.value)}/>
<button className="primary-button" disabled={parlorBusy || !parlorInput.trim()}>{parlorBusy ? '等回话…' : '说话'}</button>
</form>
</section>
)
if (!repoLogin) {
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>
{<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 ? (
<section className="gate-rise" role="status">
<span className="gate-rise-star" aria-hidden="true"><i/></span>
<h1> · </h1>
<p> · {gateNumber}</p>
</section>
) : gateStage === 'number' ? (
<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>
</div>
<div className="pod-face pod-form">
<div className="gate-pod-row">
<input id="gate-number" ref={gateInputRef} aria-label="编号" maxLength={48} value={gateNumber} placeholder="如 ICE-GL∞" onChange={(event) => onGateInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && gateNumber) { event.preventDefault(); void gateVerifyNumber() } }}/>
<button type="button" className={`gate-inf${gateInf ? ' on' : ''}`} title="编号里带无限符号就点一下点亮" aria-pressed={gateInf} onClick={() => setGateInf((v) => !v)}></button>
</div>
<button className="gate-submit" disabled={gateBusy || !gateNumber} onClick={() => void gateVerifyNumber()}>{gateBusy ? '查验中…' : '提交'}</button>
{gateMessage && <p className="gate-hint">{gateMessage}</p>}
</div>
</section>
) : (
<section className={`gate-pod gate-pod-key${loginRising ? ' fade-out' : ''}`} role="dialog">
<form onSubmit={(event) => void performRepoLogin(event)}>
<input id="repo-login-username" aria-label="账号" autoFocus maxLength={40} value={loginUsername} placeholder="账号" onChange={(event) => setLoginUsername(event.target.value)}/>
<input id="repo-login-password" aria-label="密码" type="password" maxLength={512} value={loginPassword} placeholder="密码" onChange={(event) => setLoginPassword(event.target.value)}/>
<button className="gate-submit" disabled={loginBusy || !loginUsername.trim() || !loginPassword}>{loginBusy ? '查验中…' : '进入'}</button>
</form>
{loginMessage && <p className="gate-hint">{loginMessage}</p>}
<button className="gate-back" type="button" onClick={() => { setGateStage('number'); setGateMessage(''); setLoginMessage('') }}></button>
</section>
)}
</div>
</div>
}
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 · V1.0.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[]).filter((item) => item === 'parlor' ? !zeroPoint || zeroPoint.route === 'persona' : item === 'generic' ? !!zeroPoint && zeroPoint.route !== 'persona' : true).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>
<section className="app-main">
<header className="connection-strip"><button type="button" onClick={() => setView('system')}><i className={connectionOnline ? 'online' : ''}/><span>{connectionLabel}</span></button><span className="mcp-backup">MCP </span><button type="button" onClick={() => setView('system')}> <Icon name="arrow"/></button></header>
<main className="workspace">{zeroPoint && zeroPoint.route !== 'persona' && <div className="zp-downgrade-wrap"><div className="zp-downgrade-card"><i className="zp-dot"/><div className="zp-copy"><b></b><span>AI层 API </span></div><button type="button" onClick={() => setView('system')}> <Icon name="arrow"/></button></div></div>}{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : view === 'parlor' ? renderParlor() : view === 'generic' ? renderGeneric() : renderSystem()}</main>
</section>
{personal.state !== 'UNAVAILABLE' && !personal.identity && <div className="onboarding-backdrop"><section className="onboarding-card" role="dialog" aria-modal="true"><span className="onboarding-mark"></span><span className="kicker">FIRST LOCAL ENTRY</span><h1></h1><p></p><form onSubmit={(event) => void initializeIdentity(event)}><label htmlFor="display-name"></label><input id="display-name" autoFocus maxLength={80} value={displayName} placeholder="你的名字" onChange={(event) => setDisplayName(event.target.value)}/><button className="primary-button" disabled={identityBusy || !displayName.trim()}> HoloLake</button></form>{identityMessage && <p>{identityMessage}</p>}</section></div>}
</div>
}
createRoot(document.getElementById('root')!).render(<StrictMode><HoloLakeApp/></StrictMode>)