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

@ -47,9 +47,10 @@ interface Props {
apiBase: string;
runtimeRevision?: number;
onClose?: () => void;
onWorldChanged?: () => void;
}
export default function AgentChat({ apiBase, runtimeRevision = 0, onClose }: Props) {
export default function AgentChat({ apiBase, runtimeRevision = 0, onClose, onWorldChanged }: Props) {
const [messages, setMessages] = useState<Message[]>([]);
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
const [activeConversationId, setActiveConversationId] = useState('');
@ -172,6 +173,7 @@ export default function AgentChat({ apiBase, runtimeRevision = 0, onClose }: Pro
setPendingActions(data.actions || []);
setMessages(data.messages || messages);
await refreshConversationList();
if (decision === 'confirm') onWorldChanged?.();
} catch (error) {
setMessages(previous => [...previous, { role: 'assistant', content: `动作未执行:${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString() }]);
} finally {

View file

@ -0,0 +1,55 @@
import type { ChannelState, ModuleManifest } from '../api';
interface Props {
open: boolean;
registry: ModuleManifest[];
channel: ChannelState | null;
busy: boolean;
message: string;
onClose: () => void;
onChange: (moduleId: string, installed: boolean, mounted: boolean) => void;
onUndo: () => void;
canUndo: boolean;
}
export function ModuleLibrarySheet({ open, registry, channel, busy, message, onClose, onChange, onUndo, canUndo }: Props) {
if (!open) return null;
return (
<div className="sheet-backdrop" role="presentation" onMouseDown={event => { if (event.target === event.currentTarget) onClose(); }}>
<section className="module-library-sheet" role="dialog" aria-modal="true" aria-label="频道模块">
<header>
<div><small></small><h2></h2><p></p></div>
<button type="button" onClick={onClose} aria-label="关闭">×</button>
</header>
<div className="module-library-list">
{registry.map(manifest => {
const state = channel?.modules.find(module => module.id === manifest.id);
const installed = state?.installed === true;
const mounted = installed && state?.mounted === true;
return (
<article className="module-library-card" key={manifest.id}>
<div className="module-library-icon" aria-hidden="true"><span /></div>
<div className="module-library-copy">
<div className="module-library-title"><h3>{manifest.name}</h3><span>{manifest.state === 'LIVE' ? '可用' : manifest.state}</span></div>
<code>{manifest.id}</code>
<p>{manifest.description}</p>
<small>{manifest.dataPolicy}</small>
</div>
<div className="module-library-actions">
{!installed && <button className="primary-button" disabled={busy} onClick={() => onChange(manifest.id, true, true)}></button>}
{installed && !mounted && <button className="primary-button" disabled={busy} onClick={() => onChange(manifest.id, true, true)}></button>}
{installed && mounted && <button className="secondary-button" disabled={busy} onClick={() => onChange(manifest.id, true, false)}></button>}
{installed && <button className="quiet-danger" disabled={busy} onClick={() => onChange(manifest.id, false, false)}></button>}
</div>
</article>
);
})}
</div>
<footer>
<span>{message || `频道状态修订 ${channel?.revision ?? 0}`}</span>
{canUndo && <button type="button" onClick={onUndo} disabled={busy}></button>}
</footer>
</section>
</div>
);
}

View file

@ -5,11 +5,13 @@ type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense';
interface Props {
activeRoute: RouteId;
knowledgeSelected: boolean;
knowledgeInstalled: boolean;
onRouteSelect: (route: RouteId) => void;
onKnowledgeSelect: () => void;
onEducationSelect: () => void;
onSettingsOpen: () => void;
onAccountOpen: () => void;
onModuleLibraryOpen: () => void;
channelTitle: string;
channelSubtitle: string;
}
@ -48,11 +50,13 @@ function ModuleIcon({ kind }: { kind: 'knowledge' | 'education' }) {
export function PlatformNavigation({
activeRoute,
knowledgeSelected,
knowledgeInstalled,
onRouteSelect,
onKnowledgeSelect,
onEducationSelect,
onSettingsOpen,
onAccountOpen,
onModuleLibraryOpen,
channelTitle,
channelSubtitle,
}: Props) {
@ -111,11 +115,12 @@ export function PlatformNavigation({
</nav>
)}
<div className="module-heading"><span></span><button type="button" aria-label="添加模块"></button></div>
<div className="module-heading"><span></span><button type="button" aria-label="添加模块" onClick={onModuleLibraryOpen}></button></div>
<nav className="platform-module-list" aria-label="已安装模块">
<button type="button" className={knowledgeSelected ? 'selected' : ''} onClick={onKnowledgeSelect}>
{knowledgeInstalled && <button type="button" className={knowledgeSelected ? 'selected' : ''} onClick={onKnowledgeSelect}>
<ModuleIcon kind="knowledge" /><span></span>
</button>
</button>}
{!knowledgeInstalled && <button type="button" className="module-empty-entry" onClick={onModuleLibraryOpen}><span></span><span></span></button>}
</nav>
<div className="platform-nav-footer">