feat: add HoloLake personal channel module runtime

This commit is contained in:
冰朔 2026-08-09 14:14:07 +08:00
commit a106b23f9b
21 changed files with 786 additions and 31 deletions

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
import { api, DocTreeNode, DocContent } from './api';
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';
@ -9,6 +9,7 @@ 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';
type View = 'editor' | 'history';
@ -63,7 +64,6 @@ export default function App() {
const [currentPath, setCurrentPath] = useState('');
const [view, setView] = useState<View>('editor');
const [sidebarOpen, setSidebarOpen] = useState(true);
const [moduleCollapsed, setModuleCollapsed] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [agentPanelOpen, setAgentPanelOpen] = useState(false);
@ -82,6 +82,12 @@ export default function App() {
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<ChannelState | null>(null);
const [moduleRegistry, setModuleRegistry] = useState<ModuleManifest[]>([]);
const [moduleLibraryOpen, setModuleLibraryOpen] = useState(false);
const [moduleBusy, setModuleBusy] = useState(false);
const [moduleMessage, setModuleMessage] = useState('');
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
@ -96,7 +102,6 @@ export default function App() {
setView('editor');
setActiveRoute('fifth');
setActiveModule('knowledge');
setModuleCollapsed(false);
} catch (err: any) {
setError(`加载文档失败: ${err.message}`);
} finally {
@ -158,11 +163,53 @@ export default function App() {
}
}, []);
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();
}, [refreshTree, refreshRepositoryStatus, refreshServerSession]);
refreshChannel();
}, [refreshTree, refreshRepositoryStatus, refreshServerSession, 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'
@ -260,7 +307,11 @@ export default function App() {
const activeRouteCopy = routeCopy[activeRoute];
const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库');
const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && !moduleCollapsed;
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],
@ -329,27 +380,27 @@ export default function App() {
channelTitle={channelTitle}
channelSubtitle={channelSubtitle}
activeRoute={activeRoute}
knowledgeInstalled={knowledgeInstalled}
knowledgeSelected={activeRoute === 'fifth' && activeModule === 'knowledge'}
onRouteSelect={route => {
setActiveRoute(route);
setActiveModule('knowledge');
setModuleCollapsed(false);
}}
onKnowledgeSelect={() => {
setActiveRoute('fifth');
setActiveModule('knowledge');
setModuleCollapsed(false);
if (!knowledgeMounted) void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true);
}}
onEducationSelect={() => {
setActiveRoute('sub');
setActiveModule('education');
setModuleCollapsed(false);
}}
onSettingsOpen={() => setHumanSettingsOpen(true)}
onAccountOpen={() => {
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
}}
onModuleLibraryOpen={() => setModuleLibraryOpen(true)}
/>
<section className="platform-workspace">
@ -358,7 +409,13 @@ export default function App() {
<div className="route-surface">
<h1></h1>
<p></p>
<button className="primary-button" onClick={() => setModuleCollapsed(false)}></button>
<button className="primary-button" onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true)}></button>
</div>
) : activeRoute === 'fifth' && !knowledgeInstalled ? (
<div className="route-surface">
<h1></h1>
<p>使</p>
<button className="primary-button" onClick={() => setModuleLibraryOpen(true)}></button>
</div>
) : (
<DomainSurface
@ -392,7 +449,7 @@ export default function App() {
<button className={view === 'history' ? 'selected' : ''} onClick={() => setView('history')}></button>
<button className="danger-action" onClick={deleteDoc} aria-label="删除当前页面"></button>
</>}
<button onClick={() => setModuleCollapsed(true)}></button>
<button onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, false)}></button>
</div>
</div>
@ -444,7 +501,7 @@ export default function App() {
{agentPanelOpen && (
<aside className="agent-drawer">
<div className="agent-drawer-scope"><span></span><strong> · {currentTitle}</strong></div>
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} />
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} onWorldChanged={refreshChannel} />
</aside>
)}
</div>
@ -488,6 +545,17 @@ export default function App() {
setStorageSheetOpen(true);
}}
/>
<ModuleLibrarySheet
open={moduleLibraryOpen}
registry={moduleRegistry}
channel={channelState}
busy={moduleBusy}
message={moduleMessage}
canUndo={Boolean(lastChannelReceipt)}
onClose={() => setModuleLibraryOpen(false)}
onChange={(moduleId, installed, mounted) => void changeModuleState(moduleId, installed, mounted)}
onUndo={() => void undoModuleChange()}
/>
</div>
);
}