feat(admin): add LLM call logs, system info, plugin list pages

- ai-core/llm: ring-buffer call logger with stats
- Gateway: /api/v1/admin/llm-calls, /system, /plugins, /sessions endpoints
- Frontend: LLMCallsPage (table + stats), SystemPage (version/plugins/sessions)
- Admin nav expanded to 7 items
This commit is contained in:
Aska committed 2026-08-12 16:56:52 +08:00
1 parent 8bfda096f7
commit 1a80ac2a00
7 files changed
+318 -1

No files matched your search

+4
View File
@@ -8,6 +8,8 @@ import ServicesPage from './pages/admin/ServicesPage'
import IoTManagementPage from './pages/admin/IoTManagementPage'
import PluginsPage from './pages/admin/PluginsPage'
import MemoryPage from './pages/admin/MemoryPage'
import LLMCallsPage from './pages/admin/LLMCallsPage'
import SystemPage from './pages/admin/SystemPage'
import LoginPage from './pages/LoginPage'
export default function App() {
@@ -35,6 +37,8 @@ export default function App() {
<Route path="iot" element={<IoTManagementPage />} />
<Route path="plugins" element={<PluginsPage />} />
<Route path="memory" element={<MemoryPage />} />
<Route path="llm-calls" element={<LLMCallsPage />} />
<Route path="system" element={<SystemPage />} />
</Route>
</Routes>
</AppLayout>
@@ -6,6 +6,8 @@ const navItems = [
{ to: '/admin/iot', label: 'IoT 设备', icon: '⌂' },
{ to: '/admin/plugins', label: '插件管理', icon: '◇' },
{ to: '/admin/memory', label: '记忆管理', icon: '♡' },
{ to: '/admin/llm-calls', label: 'LLM 日志', icon: '🧠' },
{ to: '/admin/system', label: '系统信息', icon: '⊞' },
]
export default function AdminLayout() {
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react'
import { getToken } from '../../api/client'
export default function LLMCallsPage() {
const [calls, setCalls] = useState<any[]>([])
const [stats, setStats] = useState<any>({})
useEffect(() => {
fetchLLM()
const interval = setInterval(fetchLLM, 5000)
return () => clearInterval(interval)
}, [])
const fetchLLM = async () => {
try {
const token = getToken()
const res = await fetch('/api/v1/admin/llm-calls', { headers: { Authorization: `Bearer ${token}` } })
const data = await res.json()
setCalls(data.calls || [])
setStats(data.stats || {})
} catch {}
}
return (
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">LLM 调用日志</h2>
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<MiniStat label="总调用" value={stats.total_calls || 0} />
<MiniStat label="成功率" value={stats.total_calls ? `${Math.round(stats.success_calls/stats.total_calls*100)}%` : '-'} />
<MiniStat label="平均耗时" value={stats.avg_duration_ms ? `${stats.avg_duration_ms}ms` : '-'} />
<MiniStat label="总 Token" value={stats.total_tokens || 0} />
</div>
{/* Calls table */}
<div className="bg-[#000] rounded-[18px] border border-[#404040] overflow-hidden">
{calls.length === 0 ? (
<div className="p-8 text-center text-white/30 text-sm">暂无 LLM 调用记录</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#393939] text-white/40 text-xs">
<th className="text-left px-4 py-2 font-normal">时间</th>
<th className="text-left px-4 py-2 font-normal">模型</th>
<th className="text-left px-4 py-2 font-normal">用途</th>
<th className="text-right px-4 py-2 font-normal">耗时</th>
<th className="text-right px-4 py-2 font-normal">Token</th>
<th className="text-center px-4 py-2 font-normal">状态</th>
</tr>
</thead>
<tbody>
{calls.map((c, i) => (
<tr key={c.id || i} className="border-b border-[#393939]/50">
<td className="px-4 py-2.5 text-white/50 text-xs font-mono">{c.time?.slice(11,19) || '-'}</td>
<td className="px-4 py-2.5 text-[#F2F2F2]">{c.model || '-'}</td>
<td className="px-4 py-2.5 text-white/50 text-xs">
<span className="px-1.5 py-0.5 rounded bg-[#2B3B54]/50 text-[#4788FF]">{c.purpose || '-'}</span>
</td>
<td className="px-4 py-2.5 text-white/50 text-xs text-right">{c.duration_ms}ms</td>
<td className="px-4 py-2.5 text-white/50 text-xs text-right">{(c.prompt_tokens||0)+(c.completion_tokens||0)}</td>
<td className="px-4 py-2.5 text-center">
<span className={`text-xs ${c.success ? 'text-[#4788FF]' : 'text-[#F12522]'}`}>
{c.success ? '✓' : '✗'}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)
}
function MiniStat({ label, value }: { label:string; value:string|number }) {
return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p>
</div>
)
}
@@ -0,0 +1,80 @@
import { useEffect, useState } from 'react'
import { getToken } from '../../api/client'
export default function SystemPage() {
const [info, setInfo] = useState<any>({})
const [sessions, setSessions] = useState<any[]>([])
const [plugins, setPlugins] = useState<any[]>([])
useEffect(() => {
fetchAll()
const interval = setInterval(fetchAll, 10000)
return () => clearInterval(interval)
}, [])
const fetchAll = async () => {
const token = getToken()
const headers = { Authorization: `Bearer ${token}` }
try {
const [sysRes, sessionRes, pluginRes] = await Promise.all([
fetch('/api/v1/admin/system', { headers }),
fetch('/api/v1/admin/sessions', { headers }),
fetch('/api/v1/admin/plugins', { headers }),
])
if (sysRes.ok) setInfo((await sysRes.json()).system)
if (sessionRes.ok) setSessions((await sessionRes.json()).sessions || [])
if (pluginRes.ok) setPlugins((await pluginRes.json()).plugins || [])
} catch {}
}
return (
<div className="p-6 space-y-6">
<h2 className="text-lg font-semibold">系统信息</h2>
{/* System Info */}
<div className="grid grid-cols-4 gap-3">
<StatRow label="版本" value={info.version || '-'} />
<StatRow label="Go 版本" value={info.go_version || '-'} />
<StatRow label="运行时间" value={info.uptime_sec ? `${Math.floor(info.uptime_sec/60)}m` : '-'} />
<StatRow label="WS 连接" value={info.ws_clients ?? 0} />
</div>
{/* Plugins list */}
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">已加载插件 ({plugins.length})</h3>
<div className="flex flex-wrap gap-2">
{plugins.map((p, i) => (
<span key={i} className="px-3 py-1.5 bg-[#000] border border-[#404040] rounded-full text-xs text-[#F2F2F2]">
{p.name}
</span>
))}
</div>
</div>
{/* Sessions */}
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">活跃会话 ({sessions.length})</h3>
{sessions.length === 0 ? (
<p className="text-xs text-white/30">暂无活跃会话</p>
) : (
<div className="space-y-1">
{sessions.map((s: any, i) => (
<div key={i} className="bg-[#000] border border-[#404040] rounded-[14px] px-4 py-2 text-xs text-white/50">
{s.session_id || s.id || '-'}
</div>
))}
</div>
)}
</div>
</div>
)
}
function StatRow({ label, value }: { label:string; value:string|number }) {
return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p>
</div>
)
}