import { useEffect, useMemo, useState } from 'react'; interface ServerProfile { id: string; physicalNodeId?: string; name: string; purpose?: 'personal-fifth-domain' | 'enterprise-lighthouse'; connected: boolean; verified: boolean; } interface ServerSession { authenticated: boolean; nodeId: string; username?: string; } interface ServerRepository { name: string; fullName: string; private: boolean; defaultBranch: string; } interface Props { open: boolean; apiBase: string; currentRemote?: string | null; initialMode?: 'local' | 'server'; onClose: () => void; onApplied: (result: { mode: 'local' | 'server'; repository?: ServerRepository }) => void; } type ServerBridge = { list: () => Promise; connect: (nodeId: string) => Promise; session: (nodeId?: string) => Promise; login: (input: { nodeId: string; username: string; password: string }) => Promise; logout: () => Promise; repositories: () => Promise; createRepository: (input: { name: string; description?: string }) => Promise; gitRemote: (fullName: string) => Promise; }; function bridge(): ServerBridge | null { return (window as any).hololake?.server || null; } export function StorageLocationSheet({ open, apiBase, currentRemote, initialMode, onClose, onApplied }: Props) { const [mode, setMode] = useState<'local' | 'server'>(currentRemote ? 'server' : 'local'); const [servers, setServers] = useState([]); const [selectedNode, setSelectedNode] = useState(''); const [session, setSession] = useState({ authenticated: false, nodeId: selectedNode }); const [repositories, setRepositories] = useState([]); const [selectedRepository, setSelectedRepository] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [creating, setCreating] = useState(false); const [newRepository, setNewRepository] = useState('heartbeat-core-knowledge'); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(''); const selectedServer = useMemo( () => servers.find(server => server.id === selectedNode) || null, [servers, selectedNode], ); async function loadRepositories() { const server = bridge(); if (!server) return; const next = await server.repositories(); setRepositories(next); setSelectedRepository(previous => previous || next[0]?.fullName || ''); } useEffect(() => { if (!open) return; setMode(initialMode || (currentRemote ? 'server' : 'local')); setMessage(''); const server = bridge(); if (!server) { setMessage('服务器托管只在 HoloLake 桌面 App 中提供'); return; } server.list() .then(async profiles => { setServers(profiles); const personal = profiles.find(profile => profile.purpose === 'personal-fifth-domain'); const nodeId = selectedNode || personal?.id || ''; setSelectedNode(nodeId); const currentSession = await server.session(nodeId); setSession(currentSession); if (currentSession.authenticated) { setUsername(currentSession.username || ''); await loadRepositories(); } }) .catch(error => setMessage(error instanceof Error ? error.message : String(error))); }, [open, currentRemote, initialMode]); async function selectServer(nodeId: string) { const server = bridge(); setSelectedNode(nodeId); setRepositories([]); setSelectedRepository(''); setPassword(''); setMessage(''); if (!server) return; try { const nextSession = await server.session(nodeId); setSession(nextSession); if (nextSession.authenticated) { setUsername(nextSession.username || ''); await loadRepositories(); } else { setUsername(''); } } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } } async function connectServer() { const server = bridge(); if (!server) return; setBusy(true); setMessage(''); try { const profile = await server.connect(selectedNode); setServers(previous => previous.map(item => item.id === profile.id ? profile : item)); setMessage('服务器连接已验证'); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } } async function login() { const server = bridge(); if (!server) return; setBusy(true); setMessage(''); try { const nextSession = await server.login({ nodeId: selectedNode, username, password }); setPassword(''); setSession(nextSession); await loadRepositories(); setMessage(`已登录 ${nextSession.username}`); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } } async function createRepository() { const server = bridge(); if (!server || !newRepository.trim()) return; setBusy(true); setMessage(''); try { const created = await server.createRepository({ name: newRepository.trim(), description: 'HoloLake 频道知识库', }); setRepositories(previous => [...previous, created].sort((a, b) => a.name.localeCompare(b.name))); setSelectedRepository(created.fullName); setCreating(false); setMessage(`已建立私有仓库 ${created.fullName}`); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } } async function logout() { const server = bridge(); if (!server) return; setBusy(true); setMessage(''); try { const nextSession = await server.logout(); setSession(nextSession); setRepositories([]); setSelectedRepository(''); setUsername(''); setMode('local'); onApplied({ mode: 'local' }); setMessage('已退出服务器账号,并解除当前知识库的远端连接'); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } } async function apply() { if (mode === 'local') { setBusy(true); setMessage(''); try { if (currentRemote) { const response = await fetch(`${apiBase}/api/forgejo/remote`, { method: 'DELETE' }); const data = await response.json(); if (!data.ok) throw new Error(data.error || '解除服务器托管失败'); } onApplied({ mode: 'local' }); onClose(); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } return; } const server = bridge(); const repository = repositories.find(item => item.fullName === selectedRepository); if (!server || !repository) { setMessage(session.authenticated ? '请选择目标仓库' : '请先登录服务器'); return; } setBusy(true); setMessage(''); try { const remote = await server.gitRemote(repository.fullName); const response = await fetch(`${apiBase}/api/forgejo/remote`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: remote }), }); const data = await response.json(); if (!data.ok) throw new Error(data.error || '连接仓库失败'); onApplied({ mode: 'server', repository }); onClose(); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusy(false); } } if (!open) return null; return (
{ if (event.target === event.currentTarget) onClose(); }}>

知识库保存位置

{mode === 'server' && (

已选择的服务器

{selectedServer?.name || '我的第五域服务器'}{selectedNode}{selectedServer?.physicalNodeId && selectedServer.physicalNodeId !== selectedNode ? ` · ${selectedServer.physicalNodeId}` : ''}
{selectedServer?.connected ? '已连接' : '已验证'} {!selectedServer?.connected && }
{!session.authenticated ? (

代码频道账号

setUsername(event.target.value)} placeholder="账号" autoComplete="username" /> setPassword(event.target.value)} placeholder="密码" autoComplete="current-password" onKeyDown={event => { if (event.key === 'Enter') login(); }} />
密码只用于换取 HoloLake 代码频道应用令牌,不写入知识库或 Git 地址;登录代码频道不等于取得目标域运行权限。
) : (

目标仓库

已登录 {session.username}
{creating && (
setNewRepository(event.target.value)} placeholder="仓库名称" />
)}
)}

首次上传前会显示文件数量、目标仓库和分支,需要确认后才会推送。

)} {message &&
{message}
}
); }