diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 4b29b34..8519a0f 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -223,7 +223,7 @@ export async function GET() { // 找出使用该 provider 的 agents const usedBy = agentsWithStatus .filter((a: any) => a.model.startsWith(providerId + "/")) - .map((a: any) => ({ id: a.id, emoji: a.emoji })); + .map((a: any) => ({ id: a.id, emoji: a.emoji, name: a.name })); return { id: providerId, diff --git a/app/api/stats-all/route.ts b/app/api/stats-all/route.ts new file mode 100644 index 0000000..56d7cc0 --- /dev/null +++ b/app/api/stats-all/route.ts @@ -0,0 +1,148 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw"); + +interface DayStat { + date: string; + inputTokens: number; + outputTokens: number; + totalTokens: number; + messageCount: number; + avgResponseMs: number; +} + +interface InternalDayStat extends DayStat { + responseTimes: number[]; +} + +function parseAgentSessions(agentId: string): InternalDayStat[] { + const sessionsDir = path.join(OPENCLAW_HOME, `agents/${agentId}/sessions`); + const dayMap: Record = {}; + + let files: string[]; + try { + files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".jsonl") && !f.includes(".deleted.")); + } catch { return []; } + + for (const file of files) { + let content: string; + try { content = fs.readFileSync(path.join(sessionsDir, file), "utf-8"); } catch { continue; } + + const lines = content.trim().split("\n"); + const messages: { role: string; ts: string; stopReason?: string }[] = []; + + for (const line of lines) { + let entry: any; + try { entry = JSON.parse(line); } catch { continue; } + if (entry.type !== "message") continue; + const msg = entry.message; + if (!msg || !entry.timestamp) continue; + + const ts = entry.timestamp; + const date = ts.slice(0, 10); + messages.push({ role: msg.role, ts, stopReason: msg.stopReason }); + + if (msg.role === "assistant" && msg.usage) { + if (!dayMap[date]) { + dayMap[date] = { date, inputTokens: 0, outputTokens: 0, totalTokens: 0, messageCount: 0, avgResponseMs: 0, responseTimes: [] }; + } + dayMap[date].inputTokens += msg.usage.input || 0; + dayMap[date].outputTokens += msg.usage.output || 0; + dayMap[date].totalTokens += msg.usage.totalTokens || 0; + dayMap[date].messageCount += 1; + } + } + + for (let i = 0; i < messages.length; i++) { + if (messages[i].role !== "user") continue; + for (let j = i + 1; j < messages.length; j++) { + if (messages[j].role === "assistant" && messages[j].stopReason === "stop") { + const diffMs = new Date(messages[j].ts).getTime() - new Date(messages[i].ts).getTime(); + if (diffMs > 0 && diffMs < 600000) { + const date = messages[i].ts.slice(0, 10); + if (!dayMap[date]) { + dayMap[date] = { date, inputTokens: 0, outputTokens: 0, totalTokens: 0, messageCount: 0, avgResponseMs: 0, responseTimes: [] }; + } + dayMap[date].responseTimes.push(diffMs); + } + break; + } + } + } + } + return Object.values(dayMap); +} + +function aggregateToWeeklyMonthly(daily: DayStat[]) { + const weekMap: Record = {}; + const monthMap: Record = {}; + + for (const d of daily) { + const dt = new Date(d.date + "T00:00:00Z"); + const day = dt.getUTCDay(); + const mondayOffset = day === 0 ? -6 : 1 - day; + const monday = new Date(dt.getTime() + mondayOffset * 86400000); + const weekKey = monday.toISOString().slice(0, 10); + + if (!weekMap[weekKey]) weekMap[weekKey] = { date: weekKey, inputTokens: 0, outputTokens: 0, totalTokens: 0, messageCount: 0, avgResponseMs: 0 }; + weekMap[weekKey].inputTokens += d.inputTokens; + weekMap[weekKey].outputTokens += d.outputTokens; + weekMap[weekKey].totalTokens += d.totalTokens; + weekMap[weekKey].messageCount += d.messageCount; + + const monthKey = d.date.slice(0, 7); + if (!monthMap[monthKey]) monthMap[monthKey] = { date: monthKey, inputTokens: 0, outputTokens: 0, totalTokens: 0, messageCount: 0, avgResponseMs: 0 }; + monthMap[monthKey].inputTokens += d.inputTokens; + monthMap[monthKey].outputTokens += d.outputTokens; + monthMap[monthKey].totalTokens += d.totalTokens; + monthMap[monthKey].messageCount += d.messageCount; + } + + return { + weekly: Object.values(weekMap).sort((a, b) => a.date.localeCompare(b.date)), + monthly: Object.values(monthMap).sort((a, b) => a.date.localeCompare(b.date)), + }; +} + +export async function GET() { + try { + const agentsDir = path.join(OPENCLAW_HOME, "agents"); + let agentIds: string[]; + try { agentIds = fs.readdirSync(agentsDir).filter(f => fs.statSync(path.join(agentsDir, f)).isDirectory()); } catch { agentIds = []; } + + // Merge all agents into one dayMap + const dayMap: Record = {}; + + for (const agentId of agentIds) { + const agentDays = parseAgentSessions(agentId); + for (const ad of agentDays) { + if (!dayMap[ad.date]) { + dayMap[ad.date] = { date: ad.date, inputTokens: 0, outputTokens: 0, totalTokens: 0, messageCount: 0, avgResponseMs: 0, responseTimes: [] }; + } + const d = dayMap[ad.date]; + d.inputTokens += ad.inputTokens; + d.outputTokens += ad.outputTokens; + d.totalTokens += ad.totalTokens; + d.messageCount += ad.messageCount; + d.responseTimes.push(...ad.responseTimes); + } + } + + const daily: DayStat[] = Object.values(dayMap) + .sort((a, b) => a.date.localeCompare(b.date)) + .map(({ responseTimes, ...rest }) => { + if (responseTimes.length > 0) { + rest.avgResponseMs = Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length); + } + return rest; + }); + + const { weekly, monthly } = aggregateToWeeklyMonthly(daily); + + return NextResponse.json({ daily, weekly, monthly }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/app/api/stats-models/route.ts b/app/api/stats-models/route.ts new file mode 100644 index 0000000..2537096 --- /dev/null +++ b/app/api/stats-models/route.ts @@ -0,0 +1,111 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw"); + +interface ModelStat { + modelId: string; + provider: string; + inputTokens: number; + outputTokens: number; + totalTokens: number; + messageCount: number; + avgResponseMs: number; +} + +interface InternalModelStat extends ModelStat { + responseTimes: number[]; +} + +export async function GET() { + try { + const agentsDir = path.join(OPENCLAW_HOME, "agents"); + let agentIds: string[]; + try { + agentIds = fs.readdirSync(agentsDir).filter(f => fs.statSync(path.join(agentsDir, f)).isDirectory()); + } catch { agentIds = []; } + + const modelMap: Record = {}; + + for (const agentId of agentIds) { + const sessionsDir = path.join(agentsDir, agentId, "sessions"); + let files: string[]; + try { + files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".jsonl") && !f.includes(".deleted.")); + } catch { continue; } + + for (const file of files) { + let content: string; + try { content = fs.readFileSync(path.join(sessionsDir, file), "utf-8"); } catch { continue; } + + const lines = content.trim().split("\n"); + const messages: { role: string; ts: string; stopReason?: string; model?: string; provider?: string }[] = []; + + for (const line of lines) { + let entry: any; + try { entry = JSON.parse(line); } catch { continue; } + if (entry.type !== "message") continue; + const msg = entry.message; + if (!msg || !entry.timestamp) continue; + + messages.push({ + role: msg.role, + ts: entry.timestamp, + stopReason: msg.stopReason, + model: msg.model, + provider: msg.provider, + }); + + if (msg.role === "assistant" && msg.usage && msg.model) { + const key = `${msg.provider || "unknown"}/${msg.model}`; + if (!modelMap[key]) { + modelMap[key] = { + modelId: msg.model, + provider: msg.provider || "unknown", + inputTokens: 0, outputTokens: 0, totalTokens: 0, + messageCount: 0, avgResponseMs: 0, responseTimes: [], + }; + } + const m = modelMap[key]; + m.inputTokens += msg.usage.input || 0; + m.outputTokens += msg.usage.output || 0; + m.totalTokens += msg.usage.totalTokens || 0; + m.messageCount += 1; + } + } + + // Response times per model + for (let i = 0; i < messages.length; i++) { + if (messages[i].role !== "user") continue; + for (let j = i + 1; j < messages.length; j++) { + if (messages[j].role === "assistant" && messages[j].stopReason === "stop") { + const diffMs = new Date(messages[j].ts).getTime() - new Date(messages[i].ts).getTime(); + if (diffMs > 0 && diffMs < 600000 && messages[j].model) { + const key = `${messages[j].provider || "unknown"}/${messages[j].model}`; + if (modelMap[key]) { + modelMap[key].responseTimes.push(diffMs); + } + } + break; + } + } + } + } + } + + const models: ModelStat[] = Object.values(modelMap).map(({ responseTimes, ...rest }) => { + if (responseTimes.length > 0) { + rest.avgResponseMs = Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length); + } + return rest; + }); + + // Sort by total tokens descending + models.sort((a, b) => b.totalTokens - a.totalTokens); + + return NextResponse.json({ models }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/app/api/test-model/route.ts b/app/api/test-model/route.ts new file mode 100644 index 0000000..fa9371e --- /dev/null +++ b/app/api/test-model/route.ts @@ -0,0 +1,101 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw"); +const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json"); + +export async function POST(req: Request) { + try { + const { provider: providerId, modelId } = await req.json(); + if (!providerId || !modelId) { + return NextResponse.json({ error: "Missing provider or modelId" }, { status: 400 }); + } + + const raw = fs.readFileSync(CONFIG_PATH, "utf-8"); + const config = JSON.parse(raw); + const provider = config.models?.providers?.[providerId]; + if (!provider) { + return NextResponse.json({ error: `Provider "${providerId}" not found` }, { status: 404 }); + } + + const baseUrl = provider.baseUrl; + const apiKey = provider.apiKey || ""; + const api = provider.api; + const headers: Record = { "Content-Type": "application/json" }; + + const startTime = Date.now(); + + if (api === "anthropic-messages") { + // Check for custom auth header + const authHeader = provider.authHeader || "x-api-key"; + headers[authHeader] = apiKey; + headers["anthropic-version"] = "2023-06-01"; + if (provider.headers) Object.assign(headers, provider.headers); + + const body = { + model: modelId, + max_tokens: 32, + messages: [{ role: "user", content: "Say hi in 3 words." }], + }; + + const resp = await fetch(`${baseUrl}/v1/messages`, { + method: "POST", + headers, + body: JSON.stringify(body), + signal: AbortSignal.timeout(100000), + }); + + const elapsed = Date.now() - startTime; + const data = await resp.json(); + + if (!resp.ok) { + return NextResponse.json({ + ok: false, + status: resp.status, + error: data.error?.message || JSON.stringify(data), + elapsed, + }); + } + + const text = data.content?.[0]?.text || ""; + return NextResponse.json({ ok: true, text, elapsed, model: data.model }); + + } else if (api === "openai-completions") { + headers["Authorization"] = `Bearer ${apiKey}`; + + const body = { + model: modelId, + max_tokens: 32, + messages: [{ role: "user", content: "Say hi in 3 words." }], + }; + + const resp = await fetch(`${baseUrl}/chat/completions`, { + method: "POST", + headers, + body: JSON.stringify(body), + signal: AbortSignal.timeout(100000), + }); + + const elapsed = Date.now() - startTime; + const data = await resp.json(); + + if (!resp.ok) { + return NextResponse.json({ + ok: false, + status: resp.status, + error: data.error?.message || JSON.stringify(data), + elapsed, + }); + } + + const text = data.choices?.[0]?.message?.content || ""; + return NextResponse.json({ ok: true, text, elapsed, model: data.model }); + + } else { + return NextResponse.json({ error: `Unknown API type: ${api}` }, { status: 400 }); + } + } catch (err: any) { + return NextResponse.json({ ok: false, error: err.message, elapsed: 0 }, { status: 500 }); + } +} diff --git a/app/models/page.tsx b/app/models/page.tsx index e20abdb..87c2562 100644 --- a/app/models/page.tsx +++ b/app/models/page.tsx @@ -16,7 +16,17 @@ interface Provider { id: string; api: string; models: Model[]; - usedBy: { id: string; emoji: string }[]; + usedBy: { id: string; emoji: string; name: string }[]; +} + +interface ModelStat { + modelId: string; + provider: string; + inputTokens: number; + outputTokens: number; + totalTokens: number; + messageCount: number; + avgResponseMs: number; } interface ConfigData { @@ -24,22 +34,92 @@ interface ConfigData { defaults: { model: string; fallbacks: string[] }; } +interface TestResult { + ok: boolean; + text?: string; + error?: string; + elapsed: number; + model?: string; +} + // 格式化数字 function formatNum(n: number) { if (n >= 1000) return `${(n / 1000).toFixed(0)}K`; return String(n); } +function formatTokens(n: number): string { + if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"; + if (n >= 1_000) return (n / 1_000).toFixed(1) + "k"; + return String(n); +} + +function formatMs(ms: number): string { + if (!ms) return "-"; + if (ms < 1000) return ms + "ms"; + return (ms / 1000).toFixed(1) + "s"; +} + export default function ModelsPage() { const [data, setData] = useState(null); + const [modelStats, setModelStats] = useState>({}); const [error, setError] = useState(null); + const [testing, setTesting] = useState>({}); + const [testResults, setTestResults] = useState>({}); + + const testModel = async (providerId: string, modelId: string) => { + const key = `${providerId}/${modelId}`; + setTesting((prev) => ({ ...prev, [key]: true })); + setTestResults((prev) => { const n = { ...prev }; delete n[key]; return n; }); + try { + const resp = await fetch("/api/test-model", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider: providerId, modelId }), + }); + const result = await resp.json(); + setTestResults((prev) => ({ ...prev, [key]: result })); + } catch (err: any) { + setTestResults((prev) => ({ ...prev, [key]: { ok: false, error: err.message, elapsed: 0 } })); + } finally { + setTesting((prev) => ({ ...prev, [key]: false })); + } + }; + + const testAllModels = async () => { + if (!data) return; + // Collect all model+provider pairs + const pairs: { provider: string; modelId: string }[] = []; + for (const p of data.providers) { + if (p.models.length > 0) { + for (const m of p.models) pairs.push({ provider: p.id, modelId: m.id }); + } else { + // For providers without explicit models, check modelStats for known models + const knownModels = Object.values(modelStats).filter(s => s.provider === p.id); + for (const s of knownModels) pairs.push({ provider: s.provider, modelId: s.modelId }); + } + } + // Run all tests in parallel + for (const pair of pairs) { + testModel(pair.provider, pair.modelId); + } + }; useEffect(() => { - fetch("/api/config") - .then((r) => r.json()) - .then((d) => { - if (d.error) setError(d.error); - else setData(d); + Promise.all([ + fetch("/api/config").then((r) => r.json()), + fetch("/api/stats-models").then((r) => r.json()), + ]) + .then(([configData, statsData]) => { + if (configData.error) setError(configData.error); + else setData(configData); + if (!statsData.error && statsData.models) { + const map: Record = {}; + for (const m of statsData.models) { + map[`${m.provider}/${m.modelId}`] = m; + } + setModelStats(map); + } }) .catch((e) => setError(e.message)); }, []); @@ -65,18 +145,31 @@ export default function ModelsPage() {

- 🧠 模型 Provider 列表 + OpenClaw接入模型列表

共 {data.providers.length} 个 Provider

- - ← 返回总览 - +
+ + + ← 返回总览 + +
@@ -96,8 +189,8 @@ export default function ModelsPage() {
使用中: {provider.usedBy.map((a) => ( - - {a.emoji} + + {a.emoji} {a.name || a.id} ))}
@@ -114,11 +207,20 @@ export default function ModelsPage() { 上下文窗口 最大输出 输入类型 - 推理 + 推理 + Input 用量 + Output 用量 + 平均响应 + 测试 - {provider.models.map((m) => ( + {provider.models.map((m) => { + const stat = modelStats[`${provider.id}/${m.id}`]; + const testKey = `${provider.id}/${m.id}`; + const isTesting = testing[testKey]; + const result = testResults[testKey]; + return ( {m.id} {m.name} @@ -136,16 +238,84 @@ export default function ModelsPage() { ))}
- {m.reasoning ? "✅" : "❌"} + {m.reasoning ? "✅" : "❌"} + {stat ? formatTokens(stat.inputTokens) : "-"} + {stat ? formatTokens(stat.outputTokens) : "-"} + {stat ? formatMs(stat.avgResponseMs) : "-"} + +
+ + {result && ( + + {result.ok ? `✅ ${formatMs(result.elapsed)}` : `❌ ${result.error?.slice(0, 30)}`} + + )} +
+ - ))} + ); + })} ) : ( -

- 无显式模型定义(通过 provider 名称推断) -

+
+

+ 无显式模型定义(通过 provider 名称推断) +

+ {(() => { + // Show aggregated stats for this provider even without explicit model defs + const providerStats = Object.values(modelStats).filter(s => s.provider === provider.id); + if (providerStats.length === 0) return null; + const totalInput = providerStats.reduce((s, m) => s + m.inputTokens, 0); + const totalOutput = providerStats.reduce((s, m) => s + m.outputTokens, 0); + const allRt = providerStats.filter(m => m.avgResponseMs > 0); + const avgRt = allRt.length > 0 ? Math.round(allRt.reduce((s, m) => s + m.avgResponseMs, 0) / allRt.length) : 0; + return ( +
+ {providerStats.map(s => { + const testKey = `${s.provider}/${s.modelId}`; + const isTesting = testing[testKey]; + const result = testResults[testKey]; + return ( +
+ {s.modelId} + Input: {formatTokens(s.inputTokens)} + Output: {formatTokens(s.outputTokens)} + {formatMs(s.avgResponseMs)} + + {result && ( + + {result.ok ? `✅ ${formatMs(result.elapsed)}` : `❌ ${result.error?.slice(0, 30)}`} + + )} +
+ ); + })} +
+ ); + })()} +
)} ))} diff --git a/app/page.tsx b/app/page.tsx index b0abb20..e7579a3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -38,6 +38,126 @@ interface ConfigData { groupChats?: GroupChat[]; } +interface DayStat { + date: string; + inputTokens: number; + outputTokens: number; + totalTokens: number; + messageCount: number; + avgResponseMs: number; +} + +interface AllStats { + daily: DayStat[]; + weekly: DayStat[]; + monthly: DayStat[]; +} + +type TimeRange = "daily" | "weekly" | "monthly"; +const RANGE_LABELS: Record = { daily: "按天", weekly: "按周", monthly: "按月" }; + +function formatTokens(n: number): string { + if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"; + if (n >= 1_000) return (n / 1_000).toFixed(1) + "k"; + return String(n); +} + +function formatMs(ms: number): string { + if (!ms) return "-"; + if (ms < 1000) return ms + "ms"; + return (ms / 1000).toFixed(1) + "s"; +} + +// 趋势折线图 +function TrendChart({ data, lines, height = 180 }: { data: DayStat[]; lines: { key: keyof DayStat; color: string; label: string }[]; height?: number }) { + if (data.length === 0) return
暂无数据
; + + const pad = { top: 16, right: 16, bottom: 50, left: 56 }; + const width = Math.max(500, data.length * 56 + pad.left + pad.right); + const chartW = width - pad.left - pad.right; + const chartH = height - pad.top - pad.bottom; + + let maxVal = 0; + for (const d of data) for (const l of lines) { const v = d[l.key] as number; if (v > maxVal) maxVal = v; } + if (maxVal === 0) maxVal = 1; + + const ticks = Array.from({ length: 5 }, (_, i) => Math.round((maxVal / 4) * i)); + + function toX(i: number) { return pad.left + (i / (data.length - 1 || 1)) * chartW; } + function toY(v: number) { return pad.top + chartH - (v / maxVal) * chartH; } + + return ( +
+ + {ticks.map((tick, i) => ( + + + {formatTokens(tick)} + + ))} + {lines.map((l) => { + const points = data.map((d, i) => `${toX(i)},${toY(d[l.key] as number)}`).join(" "); + return ; + })} + {lines.map((l) => data.map((d, i) => ( + + {`${d.date} ${l.label}: ${formatTokens(d[l.key] as number)}`} + + )))} + {data.map((d, i) => ( + {d.date.slice(5)} + ))} + + + +
+ ); +} + +// 响应时间趋势图 +function ResponseTrendChart({ data, height = 180 }: { data: DayStat[]; height?: number }) { + const filtered = data.filter(d => d.avgResponseMs > 0); + if (filtered.length === 0) return
暂无响应时间数据
; + + const pad = { top: 16, right: 16, bottom: 50, left: 56 }; + const width = Math.max(500, filtered.length * 56 + pad.left + pad.right); + const chartW = width - pad.left - pad.right; + const chartH = height - pad.top - pad.bottom; + const maxVal = Math.max(...filtered.map(d => d.avgResponseMs)); + + const ticks = Array.from({ length: 5 }, (_, i) => Math.round((maxVal / 4) * i)); + function toX(i: number) { return pad.left + (i / (filtered.length - 1 || 1)) * chartW; } + function toY(v: number) { return pad.top + chartH - (v / maxVal) * chartH; } + + const points = filtered.map((d, i) => `${toX(i)},${toY(d.avgResponseMs)}`).join(" "); + + return ( +
+ + {ticks.map((tick, i) => ( + + + {formatMs(tick)} + + ))} + + {filtered.map((d, i) => ( + + {`${d.date}: ${formatMs(d.avgResponseMs)}`} + + ))} + {filtered.map((d, i) => ( + {d.date.slice(5)} + ))} + + + +
+ ); +} + // 时间格式化 function formatTimeAgo(ts: number): string { const diff = Date.now() - ts; @@ -210,15 +330,20 @@ export default function Home() { const [refreshInterval, setRefreshInterval] = useState(0); const [lastUpdated, setLastUpdated] = useState(""); const [loading, setLoading] = useState(false); + const [allStats, setAllStats] = useState(null); + const [statsRange, setStatsRange] = useState("daily"); const timerRef = useRef(null); const fetchData = useCallback(() => { setLoading(true); - fetch("/api/config") - .then((r) => r.json()) - .then((d) => { - if (d.error) setError(d.error); - else { setData(d); setError(null); } + Promise.all([ + fetch("/api/config").then((r) => r.json()), + fetch("/api/stats-all").then((r) => r.json()), + ]) + .then(([configData, statsData]) => { + if (configData.error) setError(configData.error); + else { setData(configData); setError(null); } + if (!statsData.error) setAllStats(statsData); setLastUpdated(new Date().toLocaleTimeString("zh-CN")); }) .catch((e) => setError(e.message)) @@ -310,6 +435,65 @@ export default function Home() { ))} + {/* 汇总统计趋势 */} + {allStats && ( +
+
+

📊 全局统计趋势

+
+ {(Object.keys(RANGE_LABELS) as TimeRange[]).map((r) => ( + + ))} +
+
+ {(() => { + const currentData = allStats[statsRange]; + const totalInput = currentData.reduce((s, d) => s + d.inputTokens, 0); + const totalOutput = currentData.reduce((s, d) => s + d.outputTokens, 0); + const totalMsgs = currentData.reduce((s, d) => s + d.messageCount, 0); + return ( + <> +
+
+
总 Input Token
+
{formatTokens(totalInput)}
+
+
+
总 Output Token
+
{formatTokens(totalOutput)}
+
+
+
总消息数
+
{totalMsgs}
+
+
+
+
+ 🔢 Token 消耗趋势 +
+ Input + Output +
+
+ +
+ {statsRange === "daily" && ( +
+ ⏱️ 平均响应时间趋势 + +
+ )} + + ); + })()} +
+ )} + {/* 群聊管理 */} {data.groupChats && data.groupChats.length > 0 && (