import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react'; import { api, DocTreeNode, DocContent, type ChannelState, type ModuleManifest } from './api'; import { DocTree } from './components/DocTree'; import { Editor } from './components/Editor'; import { SearchBar } from './components/SearchBar'; import { VersionHistory } from './components/VersionHistory'; import AgentChat from './components/AgentChat'; import { PlatformNavigation } from './components/PlatformNavigation'; import { StorageLocationSheet } from './components/StorageLocationSheet'; import { HumanSettings, HumanPreferences, loadHumanPreferences } from './components/HumanSettings'; import { DomainSurface } from './components/DomainSurface'; import { ModuleLibrarySheet } from './components/ModuleLibrarySheet'; import { cleanDisplayText } from './presentation'; import { WorldEntry } from './components/WorldEntry'; import { DomainConnectionSheet } from './components/DomainConnectionSheet'; import type { DomainAccessProjection } from './domain-connection'; import { createDomainEntryTarget, type DomainEntryTarget, type DomainNodeType } from './domain-entry-state'; import type { DomainRouteId } from './public-domain-directory'; type View = 'editor' | 'history'; type RouteId = DomainRouteId; type ModuleId = 'knowledge' | 'education'; interface RepositoryStatus { branch: string; head: string; clean: boolean; ahead: number; behind: number; remote: { name: string; url: string } | null; } interface ServerSession { authenticated: boolean; nodeId: string; username?: string; } interface ServerProfile { id: string; name: string; purpose: 'personal-fifth-domain' | 'enterprise-lighthouse'; channelTitle?: string; channelSubtitle?: string; } function findFirstDocument(nodes: DocTreeNode[]): string | null { for (const node of nodes) { if (node.type === 'document') return node.path; const child = findFirstDocument(node.children || []); if (child) return child; } return null; } export default function App() { const agentApiBase = window.location.protocol === 'file:' ? 'http://127.0.0.1:3890' : ''; const [tree, setTree] = useState([]); const [treeLoaded, setTreeLoaded] = useState(false); const [currentDoc, setCurrentDoc] = useState(null); const [currentPath, setCurrentPath] = useState(''); const [view, setView] = useState('editor'); const [sidebarOpen, setSidebarOpen] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [agentPanelOpen, setAgentPanelOpen] = useState(false); const [importing, setImporting] = useState(false); const [importMessage, setImportMessage] = useState(null); const [emptyDismissed, setEmptyDismissed] = useState(false); const [activeRoute, setActiveRoute] = useState('fifth'); const [activeModule, setActiveModule] = useState('knowledge'); const [storageSheetOpen, setStorageSheetOpen] = useState(false); const [domainConnectionOpen, setDomainConnectionOpen] = useState(false); const [domainEntryTarget, setDomainEntryTarget] = useState(null); const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>(); const [serverSession, setServerSession] = useState({ authenticated: false, nodeId: '' }); const [serverProfiles, setServerProfiles] = useState([]); const [repositoryStatus, setRepositoryStatus] = useState(null); const [humanSettingsOpen, setHumanSettingsOpen] = useState(false); const [humanPreferences, setHumanPreferences] = useState(() => loadHumanPreferences()); const [agentRevision, setAgentRevision] = useState(0); const [treeWidth, setTreeWidth] = useState(() => Number(localStorage.getItem('hololake.layout.tree-width')) || 258); const [agentWidth, setAgentWidth] = useState(() => Number(localStorage.getItem('hololake.layout.agent-width')) || 460); const [channelState, setChannelState] = useState(null); const [moduleRegistry, setModuleRegistry] = useState([]); const [moduleLibraryOpen, setModuleLibraryOpen] = useState(false); const [moduleBusy, setModuleBusy] = useState(false); const [moduleMessage, setModuleMessage] = useState(''); const [lastChannelReceipt, setLastChannelReceipt] = useState(''); const [worldEntered, setWorldEntered] = useState(false); const [domainAccess, setDomainAccess] = useState({ blockers: [], runtimeReady: false, stage: 'checking' }); const domainAccessRequest = useRef(0); const storageMode = repositoryStatus?.remote ? 'server' : 'local'; const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机'; const openDoc = useCallback(async (docPath: string) => { setLoading(true); setError(null); try { const doc = await api.getDoc(docPath); setCurrentDoc(doc); setCurrentPath(docPath); setView('editor'); setActiveRoute('fifth'); setActiveModule('knowledge'); } catch (err: any) { setError(`加载文档失败: ${err.message}`); } finally { setLoading(false); } }, []); const refreshTree = useCallback(async () => { try { const nextTree = await api.getTree(); setTree(nextTree); setTreeLoaded(true); } catch (err: any) { setError(`加载文档树失败: ${err.message}`); setTreeLoaded(true); } }, []); const openWikiTarget = useCallback(async (rawTarget: string) => { const target = rawTarget.trim().replace(/^\/+/, ''); const normalized = target.toLocaleLowerCase().replace(/\.md$/u, ''); const flatten = (nodes: DocTreeNode[]): DocTreeNode[] => nodes.flatMap(node => [node, ...(node.children ? flatten(node.children) : [])]); const match = flatten(tree).find(node => { if (node.type !== 'document') return false; const path = node.path.toLocaleLowerCase().replace(/\.md$/u, ''); const name = node.name.toLocaleLowerCase().replace(/\.md$/u, ''); return path === normalized || name === normalized || path.endsWith(`/${normalized}`); }); if (match) { await openDoc(match.path); return; } setError(`没有找到知识页面:${target}`); }, [openDoc, tree]); const refreshRepositoryStatus = useCallback(async () => { try { const response = await fetch(`${agentApiBase}/api/forgejo/status`); const data = await response.json(); if (data.ok) setRepositoryStatus(data.status); } catch { setRepositoryStatus(null); } }, [agentApiBase]); const refreshServerSession = useCallback(async () => { const server = (window as any).hololake?.server; if (!server?.session) return; try { const profiles = await server.list() as ServerProfile[]; setServerProfiles(profiles); const personal = profiles.find(profile => profile.purpose === 'personal-fifth-domain'); if (personal) { try { await server.connect(personal.id); } catch { /* 会话状态继续按真实结果显示 */ } } setServerSession(await server.session(personal?.id)); } catch { setServerSession({ authenticated: false, nodeId: '' }); } }, []); const refreshDomainAccess = useCallback(async (domainId = 'DOM-FIFTH-0001', nodeType: DomainNodeType = 'local-terminal') => { const request = ++domainAccessRequest.current; const server = (window as any).hololake?.server; if (!server?.domainAccess) { if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['desktop_runtime_required'], domainId, nodeType, runtimeReady: false, stage: 'login-required' }); return; } try { const result = await server.domainAccess(domainId, nodeType); if (request === domainAccessRequest.current) setDomainAccess(result); } catch { if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['domain_access_probe_failed'], domainId, nodeType, runtimeReady: false, stage: 'login-required' }); } }, []); const openDomainConnection = useCallback((routeId: DomainRouteId | null) => { const target = routeId ? createDomainEntryTarget(routeId) : null; setDomainEntryTarget(target); setDomainConnectionOpen(true); if (target) { setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, runtimeReady: false, stage: 'checking' }); void refreshDomainAccess(target.domain.stableDomainId, target.nodeType); } }, [refreshDomainAccess]); const selectDomainNodeType = useCallback((nodeType: DomainNodeType) => { if (!domainEntryTarget) return; const target = createDomainEntryTarget(domainEntryTarget.domain.routeId, nodeType); setDomainEntryTarget(target); setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, nodeType, runtimeReady: false, stage: 'checking' }); void refreshDomainAccess(target.domain.stableDomainId, nodeType); }, [domainEntryTarget, refreshDomainAccess]); const refreshChannel = useCallback(async () => { try { const [channel, registry] = await Promise.all([api.getChannel(), api.getModules()]); setChannelState(channel); setModuleRegistry(registry); } catch (err: any) { setError(`频道状态读取失败: ${err.message}`); } }, []); useEffect(() => { refreshTree(); refreshRepositoryStatus(); refreshServerSession(); refreshDomainAccess(); refreshChannel(); }, [refreshTree, refreshRepositoryStatus, refreshServerSession, refreshDomainAccess, refreshChannel]); const changeModuleState = useCallback(async (moduleId: string, installed: boolean, mounted: boolean) => { setModuleBusy(true); setModuleMessage(''); try { const result = await api.patchChannel({ operation: 'set_module_state', moduleId, installed, mounted }); setChannelState(result.channel); setLastChannelReceipt(result.receipt.id); setModuleMessage(installed ? (mounted ? '模块已安装并打开,已保留可撤销检查点' : '模块已收起,数据保持不变') : '模块入口已移除,知识数据与历史没有删除'); if (!installed || !mounted) setAgentPanelOpen(false); } catch (err: any) { setModuleMessage(err.message); } finally { setModuleBusy(false); } }, []); const undoModuleChange = useCallback(async () => { if (!lastChannelReceipt) return; setModuleBusy(true); try { const result = await api.undoChannel(lastChannelReceipt); setChannelState(result.channel); setLastChannelReceipt(''); setModuleMessage('已恢复上一步频道状态;用户数据始终保留'); } catch (err: any) { setModuleMessage(err.message); } finally { setModuleBusy(false); } }, [lastChannelReceipt]); useEffect(() => { const resolvedLanguage = humanPreferences.language === 'system' ? navigator.language : humanPreferences.language; document.documentElement.lang = resolvedLanguage; }, [humanPreferences.language]); useEffect(() => { localStorage.setItem('hololake.human-preferences.v1', JSON.stringify(humanPreferences)); }, [humanPreferences]); useEffect(() => { if (!treeLoaded || currentPath || loading) return; const firstDocument = findFirstDocument(tree); if (firstDocument) openDoc(firstDocument); }, [tree, treeLoaded, currentPath, loading, openDoc]); const saveDoc = useCallback(async (title: string, body: string) => { if (!currentPath) return; setLoading(true); try { const doc = await api.updateDoc(currentPath, title, body); setCurrentDoc(doc); await refreshTree(); } catch (err: any) { setError(`保存失败: ${err.message}`); } finally { setLoading(false); } }, [currentPath, refreshTree]); const createDoc = useCallback(async (parentPath: string) => { const name = prompt('文档文件名(不含 .md):'); if (!name) return; const title = prompt('文档标题:') || name; const docPath = parentPath ? `${parentPath}/${name}.md` : `${name}.md`; try { const doc = await api.createDoc(docPath, title, `# ${title}\n\n在这里开始写作...\n`); setCurrentDoc(doc); setCurrentPath(docPath); setEmptyDismissed(false); await refreshTree(); } catch (err: any) { setError(`创建失败: ${err.message}`); } }, [refreshTree]); const deleteDoc = useCallback(async () => { if (!currentPath || !confirm(`确认删除 ${currentPath}?`)) return; try { await api.deleteDoc(currentPath); setCurrentDoc(null); setCurrentPath(''); await refreshTree(); } catch (err: any) { setError(`删除失败: ${err.message}`); } }, [currentPath, refreshTree]); const importFolder = useCallback(async () => { const knowledge = (window as any).hololake?.knowledge; if (!knowledge?.importFolder) { setError('本地文件夹导入只在 HoloLake 桌面 App 中提供'); return; } setImporting(true); setError(null); setImportMessage(null); try { const result = await knowledge.importFolder(); if (result.cancelled) { setImportMessage('已取消导入,现有知识库没有变化'); return; } if (!result.imported) { setImportMessage(`没有找到可导入的文档;已跳过 ${result.skipped || 0} 个文件`); return; } await refreshTree(); if (result.firstDocument) await openDoc(result.firstDocument); setEmptyDismissed(false); setImportMessage([ `已导入 ${result.imported} 篇文档`, result.assets ? `${result.assets} 个图片资源` : '', result.skipped ? `跳过 ${result.skipped} 个暂不支持的文件` : '', result.failed?.length ? `${result.failed.length} 个文件失败` : '', ].filter(Boolean).join(' · ')); } catch (err: any) { setError(`导入失败: ${err.message}`); } finally { setImporting(false); } }, [openDoc, refreshTree]); const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库'); const knowledgeState = channelState?.modules.find(module => module.id === 'HL-MOD-KNOWLEDGE-001'); const knowledgeInstalled = knowledgeState?.installed !== false; const knowledgeMounted = knowledgeInstalled && knowledgeState?.mounted !== false; const moduleCollapsed = knowledgeInstalled && !knowledgeMounted; const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && knowledgeMounted; const breadcrumb = useMemo( () => currentPath ? currentPath.split('/').map(cleanDisplayText).join(' / ') : '知识库', [currentPath], ); const language = humanPreferences.language === 'system' ? (navigator.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en') : humanPreferences.language; const fontFamily = humanPreferences.font === 'serif' ? 'ui-serif, "Songti SC", Georgia, serif' : humanPreferences.font === 'accessible' ? 'Arial, "PingFang SC", sans-serif' : 'Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif'; const personalServer = serverProfiles.find(profile => profile.purpose === 'personal-fifth-domain'); const channelTitle = personalServer?.channelTitle || '我的第五域'; const channelSubtitle = personalServer?.channelSubtitle || '当前频道'; const startResize = useCallback((kind: 'tree' | 'agent', event: ReactPointerEvent) => { event.preventDefault(); const startX = event.clientX; const startWidth = kind === 'tree' ? treeWidth : agentWidth; document.body.classList.add('is-resizing'); const move = (moveEvent: PointerEvent) => { if (kind === 'tree') { setTreeWidth(Math.max(210, Math.min(420, startWidth + moveEvent.clientX - startX))); } else { setAgentWidth(Math.max(360, Math.min(Math.max(360, window.innerWidth * 0.52), startWidth - moveEvent.clientX + startX))); } }; const stop = () => { document.body.classList.remove('is-resizing'); window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', stop); }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', stop, { once: true }); }, [treeWidth, agentWidth]); useEffect(() => { localStorage.setItem('hololake.layout.tree-width', String(Math.round(treeWidth))); }, [treeWidth]); useEffect(() => { localStorage.setItem('hololake.layout.agent-width', String(Math.round(agentWidth))); }, [agentWidth]); return ( <> {!worldEntered && ( { setActiveRoute('fifth'); setActiveModule('knowledge'); setWorldEntered(true); }} onEnterLocalWorkspace={() => { setActiveRoute('fifth'); setActiveModule('knowledge'); setWorldEntered(true); }} onOpenConnection={openDomainConnection} /> )}
{channelTitle} · {channelSubtitle}
{ setActiveRoute(route); setActiveModule('knowledge'); }} onKnowledgeSelect={() => { setActiveRoute('fifth'); setActiveModule('knowledge'); if (!knowledgeMounted) void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true); }} onEducationSelect={() => { setActiveRoute('sub'); setActiveModule('education'); }} onSettingsOpen={() => setHumanSettingsOpen(true)} onAccountOpen={() => { setStorageSheetInitialMode('server'); setStorageSheetOpen(true); }} onModuleLibraryOpen={() => setModuleLibraryOpen(true)} />
{!contentVisible ? ( moduleCollapsed && activeRoute === 'fifth' ? (

知识库已收起

模块仍安装在永恒湖心频道中,重新打开不会改变本地或服务器数据。

) : activeRoute === 'fifth' && !knowledgeInstalled ? (

这是你的初始化频道

频道目前没有安装模块。资料仍保留在个人状态域中,可以随时重新安装知识库继续使用。

) : ( setHumanSettingsOpen(true)} /> ) ) : ( <>
{currentTitle}
{currentDoc && <> }
{sidebarOpen && ( )} {sidebarOpen &&
} {loading &&
正在打开页面…
} {!currentDoc && treeLoaded && !loading && !emptyDismissed && (

把已有资料带进知识空间

只有空知识库才显示这个入口。也可以先关闭,稍后再导入。

支持 Markdown、TXT、CSV、JSON 与 YAML
)} {!currentDoc && emptyDismissed && !loading && (

知识库已准备好

导入文件夹或新建页面开始使用。

)} {currentDoc && view === 'editor' && } {currentDoc && view === 'history' && } {currentDoc &&
{breadcrumb}
} {agentPanelOpen &&
)} {contentVisible && ( <> {!agentPanelOpen && } )}
setStorageSheetOpen(false)} onApplied={() => { refreshRepositoryStatus(); refreshServerSession(); refreshDomainAccess(); }} /> setDomainConnectionOpen(false)} onSelectTarget={routeId => openDomainConnection(routeId)} onSelectNodeType={selectDomainNodeType} onEnterRuntime={() => { if (!domainEntryTarget || domainEntryTarget.domain.routeId !== 'fifth' || !domainAccess.runtimeReady || domainAccess.domainId !== domainEntryTarget.domain.stableDomainId) return; setActiveRoute('fifth'); setActiveModule('knowledge'); setWorldEntered(true); setDomainConnectionOpen(false); }} onOpenCodeChannel={() => { setDomainConnectionOpen(false); setStorageSheetInitialMode('server'); setStorageSheetOpen(true); }} /> setHumanSettingsOpen(false)} onChange={setHumanPreferences} onAgentChanged={() => setAgentRevision(revision => revision + 1)} onManageServer={() => { setHumanSettingsOpen(false); setStorageSheetInitialMode('server'); setStorageSheetOpen(true); }} /> setModuleLibraryOpen(false)} onChange={(moduleId, installed, mounted) => void changeModuleState(moduleId, installed, mounted)} onUndo={() => void undoModuleChange()} /> ); }