import { useCallback, useEffect, useState } from 'react'; import * as api from '../api/client'; import type { AdminScopes, User } from '../types'; import { CheckIcon, LockIcon, UsersIcon, ChevronRightIcon, KeyIcon, BotIcon, CpuIcon } from './icons'; import KeyPanel from './KeyPanel'; import QuotaPanel from './QuotaPanel'; import ModelScopePanel from './ModelScopePanel'; type Tab = 'users' | 'keys' | 'quotas' | 'models'; export default function AdminUsersPage() { const [tab, setTab] = useState('users'); const [users, setUsers] = useState([]); const [scopes, setScopes] = useState({ agents: [], paths: [] }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [creating, setCreating] = useState(false); const [editing, setEditing] = useState(null); // Agent 密钥面板 const [keys, setKeys] = useState([]); const [keyBusy, setKeyBusy] = useState(false); const [keyError, setKeyError] = useState(null); const [newToken, setNewToken] = useState(null); const load = async () => { setLoading(true); setError(null); try { const [u, s] = await Promise.all([api.adminListUsers(), api.adminListScopes()]); setUsers(u.users || []); setScopes(s); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const loadKeys = useCallback(async () => { try { const r = await api.adminListAgentKeys(); setKeys(r.keys); setKeyError(null); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } }, []); useEffect(() => { if (tab === 'keys') loadKeys(); }, [tab, loadKeys]); const createKey = async (payload: api.CreateKeyPayload) => { setKeyBusy(true); setKeyError(null); try { const r = await api.adminCreateAgentKey(payload); // 登记客户端已有密钥时对方已经持有全文,无需再弹一次 setNewToken(payload.key_token ? null : r.key.key_token ?? null); await loadKeys(); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } finally { setKeyBusy(false); } }; const deleteKey = async (id: string) => { setKeyError(null); try { await api.adminDeleteAgentKey(id); await loadKeys(); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } }; const bindKey = async (id: string, agentName: string) => { setKeyError(null); try { await api.adminBindAgentKey(id, agentName); await loadKeys(); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } }; const flash = (msg: string) => { setNotice(msg); setTimeout(() => setNotice(null), 2500); }; return (
setTab('users')}> 用户管理 {users.length} setTab('keys')}> Agent 密钥 setTab('quotas')}> Agent 管理 setTab('models')}> 模型范围
{notice && {notice}} {tab === 'users' && ( )}
{error &&

{error}

} {tab === 'models' ? (
) : tab === 'quotas' ? (
) : tab === 'keys' ? (
setNewToken(null)} />
) : ( <> {creating && { setCreating(false); flash('用户已创建'); load(); }} onError={setError} />}
{users.map(u => ( setEditing(editing === u.user_id ? null : u.user_id)} onSaved={flash} onReload={load} setError={setError} /> ))} {loading && users.length === 0 &&

加载中

}
)}
); } function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode; }) { return ( ); } /* ── UserCard ── */ function UserCard({ user, scopes, expanded, onToggle, onSaved, onReload, setError }: { user: User; scopes: AdminScopes; expanded: boolean; onToggle: () => void; onSaved: (msg: string) => void; onReload: () => void; setError: (msg: string) => void; }) { return (
{user.username} {user.display_name} {user.role === 'admin' ? '管理员' : '用户'} {user.status === 'active' ? '启用' : '禁用'} {user.role !== 'admin' && (user.allowed_agents.length > 0 || user.allowed_paths.length > 0) && ( 受限 )}
{user.last_login || '从未登录'}
{expanded && }
); } /* ── UserEditor ── */ function UserEditor({ user, scopes, onSaved, onReload, setError }: { user: User; scopes: AdminScopes; onSaved: (msg: string) => void; onReload: () => void; setError: (msg: string) => void; }) { const [displayName, setDisplayName] = useState(user.display_name); const [role, setRole] = useState<'admin' | 'user'>(user.role as 'admin' | 'user'); const [agents, setAgents] = useState(user.allowed_agents); const [paths, setPaths] = useState(user.allowed_paths); const [pw, setPw] = useState(''); const [busy, setBusy] = useState(false); const toggleAgent = (a: string) => setAgents(p => p.includes(a) ? p.filter(x => x !== a) : [...p, a]); const togglePath = (p: string) => setPaths(prev => prev.includes(p) ? prev.filter(x => x !== p) : [...prev, p]); const save = async () => { setBusy(true); try { await api.adminUpdateUser(user.user_id, { display_name: displayName, role, allowed_agents: agents, allowed_paths: paths }); onSaved('用户已更新'); await onReload(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } }; const disableUser = async () => { try { await api.adminDisableUser(user.user_id); onSaved('用户已禁用'); await onReload(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; const enableUser = async () => { try { await api.adminUpdateUser(user.user_id, { status: 'active' }); onSaved('用户已启用'); await onReload(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; const resetPassword = async () => { if (pw.length < 8) return; setBusy(true); try { await api.adminResetPassword(user.user_id, pw); onSaved('密码已重置'); setPw(''); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } }; return (
setDisplayName(e.target.value)} className="w-full text-sm border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
{user.status === 'active' ? ( ) : ( )}
{user.role !== 'admin' && ( <>
{scopes.agents.map(a => ( ))}
{scopes.paths.map(p => ( ))}
)}
setPw(e.target.value)} placeholder="新密码(至少 8 位)" className="w-40 text-xs border border-gray-300 rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
); } /* ── CreateUserForm ── */ function CreateUserForm({ scopes, onDone, onError }: { scopes: AdminScopes; onDone: () => void; onError: (msg: string) => void; }) { const [username, setUsername] = useState(''); const [displayName, setDisplayName] = useState(''); const [password, setPassword] = useState(''); const [role, setRole] = useState<'admin' | 'user'>('user'); const [agents, setAgents] = useState([]); const [paths, setPaths] = useState([]); const [busy, setBusy] = useState(false); const ok = username.trim().length >= 2 && password.length >= 8 && !busy; const submit = async () => { if (!ok) return; setBusy(true); try { await api.adminCreateUser({ username: username.trim().toLowerCase(), password, display_name: displayName.trim(), role: role, allowed_agents: role === 'admin' ? [] : agents, allowed_paths: role === 'admin' ? [] : paths, }); setUsername(''); setDisplayName(''); setPassword(''); setRole('user'); setAgents([]); setPaths([]); onDone(); } catch (err) { onError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } }; return (
setUsername(e.target.value)} placeholder="alice" spellCheck={false} className="w-full text-sm font-mono border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" /> setDisplayName(e.target.value)} placeholder="Alice" className="w-full text-sm border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" /> setPassword(e.target.value)} className="w-full text-sm border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
{role !== 'admin' && ( <> setAgents(p => p.includes(a) ? p.filter(x => x !== a) : [...p, a])} color="blue" /> setPaths(prev => prev.includes(p) ? prev.filter(x => x !== p) : [...prev, p])} color="green" /> )}
); } function ScopePick({ label, items, selected, onToggle, color }: { label: string; items: string[]; selected: string[]; onToggle: (item: string) => void; color: 'blue' | 'green'; }) { const active = color === 'blue' ? 'bg-blue-50 border-blue-300 text-blue-700' : 'bg-green-50 border-green-300 text-green-700'; return (
{items.map(i => ( ))}
); } function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{hint && {hint}}
{children}
); } /** * 模型范围页。 * * Agent 列表复用 `/admin/quotas` —— 它返回的就是全部已注册 Agent。 * 另开一个「列出 Agent」接口只会多一条做同一件事的路径。 */ function ModelScopeTab() { const [agents, setAgents] = useState([]); const [err, setErr] = useState(''); useEffect(() => { api .adminListAgentStats() .then(res => setAgents(res.quotas)) .catch(e => setErr(e instanceof Error ? e.message : String(e))); }, []); if (err) return

{err}

; return ; }