import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react'; import { api, DocTreeNode, DocContent } 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 { cleanDisplayText } from './presentation'; type View = 'editor' | 'history'; type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense'; 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; } const routeCopy: Record = { fifth: { title: '我的第五域', body: '当前账号的个人第五域频道。模块在这里按需挂载、收起和组合。' }, main: { title: '光湖主域', body: '公共产品事实、发布与公告入口。当前客户端尚未取得该域的操作权限。' }, sub: { title: '光湖分域', body: '行业入口与初始化频道目录。教育行业与网文行业从这里进入。' }, zero: { title: '光湖零域', body: '实验、模块试装、对比与质量验证入口。' }, 'zero-sense': { title: '光湖零感域', body: '治理、部署审批、事故、回滚与审计入口。' }, }; 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 [moduleCollapsed, setModuleCollapsed] = useState(false); 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 [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 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'); setModuleCollapsed(false); } 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 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: '' }); } }, []); useEffect(() => { refreshTree(); refreshRepositoryStatus(); refreshServerSession(); }, [refreshTree, refreshRepositoryStatus, refreshServerSession]); useEffect(() => { const resolvedLanguage = humanPreferences.language === 'system' ? navigator.language : humanPreferences.language; document.documentElement.lang = resolvedLanguage; }, [humanPreferences.language]); 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 activeRouteCopy = routeCopy[activeRoute]; const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库'); const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && !moduleCollapsed; 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 || '当前频道'; return (
{channelTitle} · {channelSubtitle}
{ setActiveRoute(route); setModuleCollapsed(false); }} onKnowledgeSelect={() => { setActiveRoute('fifth'); setActiveModule('knowledge'); setModuleCollapsed(false); }} onEducationSelect={() => { setActiveRoute('sub'); setActiveModule('education'); setModuleCollapsed(false); }} onSettingsOpen={() => setHumanSettingsOpen(true)} onAccountOpen={() => { setStorageSheetInitialMode('server'); setStorageSheetOpen(true); }} />
{!contentVisible ? ( moduleCollapsed && activeRoute === 'fifth' ? (

知识库已收起

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

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

把已有资料带进知识空间

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

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

知识库已准备好

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

)} {currentDoc && view === 'editor' && } {currentDoc && view === 'history' && } {currentDoc &&
{breadcrumb}
}
)} {contentVisible && ( <> {agentPanelOpen && ( )} )}
setStorageSheetOpen(false)} onApplied={() => { refreshRepositoryStatus(); refreshServerSession(); }} /> setHumanSettingsOpen(false)} onChange={setHumanPreferences} onAgentChanged={() => setAgentRevision(revision => revision + 1)} onManageServer={() => { setHumanSettingsOpen(false); setStorageSheetInitialMode('server'); setStorageSheetOpen(true); }} />
); }