import { useEffect, useState } from 'react'; import { adminAPI } from '@/api'; interface Summary { totals: { calls: number; tokens: number; cost: number; users: number }; by_model: { model_name: string; calls: number; tokens: number; cost: number }[]; by_user: { user_id: string; username: string; calls: number; tokens: number; cost: number }[]; daily: { date: string; tokens: number; cost: number; calls: number }[]; recent: { trace_id: string | null; model_name: string; tokens: number; cost: number; created_at: string | null }[]; } const ACCENT = '#5B5FED'; const ACCENT_HOVER = '#4A4ECC'; export default function Observability() { const [days, setDays] = useState(30); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [err, setErr] = useState(''); useEffect(() => { let alive = true; setLoading(true); setErr(''); adminAPI.getObservabilitySummary(days) .then((d: Summary) => { if (alive) setData(d); }) .catch((e: any) => { if (alive) setErr(e?.message || '加载失败'); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, [days]); const maxTokens = data ? Math.max(1, ...data.daily.map(d => d.tokens)) : 1; const maxModel = data ? Math.max(1, ...data.by_model.map(m => m.tokens)) : 1; const fmt = (n: number) => n.toLocaleString('zh-CN'); const fmtCost = (n: number) => `¥${n.toFixed(4)}`; const card: React.CSSProperties = { background: '#fff', border: '1px solid #eceef5', borderRadius: 16, padding: 20, boxShadow: '0 1px 3px rgba(20,30,60,0.04)', marginBottom: 18, }; const stat: React.CSSProperties = { flex: 1, minWidth: 150, background: '#fff', border: '1px solid #eceef5', borderRadius: 16, padding: 18, boxShadow: '0 1px 3px rgba(20,30,60,0.04)', }; return (

调用可观测

媒体 / LLM 调用归因 · 基于 trace_id 关联请求链路(Phase 2.2)
{[7, 30, 90].map(d => ( ))}
{err &&
{err}
} {loading && !data &&
加载中…
} {data && ( <>
调用次数
{fmt(data.totals.calls)}
Token 消耗
{fmt(data.totals.tokens)}
成本
{fmtCost(data.totals.cost)}
活跃用户
{fmt(data.totals.users)}

按模型

{data.by_model.length === 0 &&
暂无数据
} {data.by_model.map(m => (
{m.model_name}
{fmt(m.tokens)}
{fmtCost(m.cost)}
))}

每日趋势(Token)

{data.daily.length === 0 &&
暂无数据
} {data.daily.map(d => (
{d.date.slice(5)}
))}

按用户(Top 20)

{data.by_user.map(u => ( ))} {data.by_user.length === 0 && }
用户调用Token成本
{u.username} {fmt(u.calls)} {fmt(u.tokens)} {fmtCost(u.cost)}
暂无数据

最近调用(trace_id 可下钻)

{data.recent.map((r, i) => ( ))} {data.recent.length === 0 && }
Trace ID模型Token成本时间
{r.trace_id || '—'} {r.model_name} {fmt(r.tokens)} {fmtCost(r.cost)} {r.created_at || '—'}
暂无数据
)}
); } const th: React.CSSProperties = { padding: '8px 6px', fontWeight: 600, fontSize: 12 }; const thR: React.CSSProperties = { ...th, textAlign: 'right' }; const td: React.CSSProperties = { padding: '8px 6px' }; const tdR: React.CSSProperties = { ...td, textAlign: 'right', color: '#5b6172' };