Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
1. App.tsx: add restoreSession() call on startup to prevent redirect to login page after refresh (isRestoring guard + BootstrapScreen) 2. CloneManager: call syncAgents() after loadClones() to restore currentAgent and conversation history on app load 3. zclaw-memory: add get_or_create_session() so frontend session UUID is persisted directly — kernel no longer creates mismatched IDs 4. openai.rs: assistant message content must be non-empty for Kimi/Qwen APIs — replace empty content with meaningful placeholders Also includes admin-v2 ModelServices unified page (merge providers + models + API keys into expandable row layout)
201 lines
5.9 KiB
TypeScript
201 lines
5.9 KiB
TypeScript
// ============================================================
|
|
// 用量统计
|
|
// ============================================================
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { Card, Col, Row, Select, Statistic } from 'antd'
|
|
import { ThunderboltOutlined, ColumnWidthOutlined } from '@ant-design/icons'
|
|
import type { ProColumns } from '@ant-design/pro-components'
|
|
import { ProTable } from '@ant-design/pro-components'
|
|
import { telemetryService } from '@/services/telemetry'
|
|
import { PageHeader } from '@/components/PageHeader'
|
|
import { ErrorState } from '@/components/ErrorState'
|
|
import type { DailyUsageStat, ModelUsageStat } from '@/types'
|
|
|
|
export default function Usage() {
|
|
const [days, setDays] = useState(30)
|
|
|
|
const {
|
|
data: dailyData,
|
|
isLoading: dailyLoading,
|
|
error: dailyError,
|
|
refetch,
|
|
} = useQuery({
|
|
queryKey: ['usage-daily', days],
|
|
queryFn: ({ signal }) => telemetryService.dailyStats({ days }, signal),
|
|
})
|
|
|
|
const { data: modelData, isLoading: modelLoading } = useQuery({
|
|
queryKey: ['usage-model', days],
|
|
queryFn: ({ signal }) => telemetryService.modelStats({}, signal),
|
|
})
|
|
|
|
if (dailyError) {
|
|
return (
|
|
<>
|
|
<PageHeader title="用量统计" description="查看模型使用情况和 Token 消耗" />
|
|
<ErrorState message={(dailyError as Error).message} onRetry={() => refetch()} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const totalRequests = dailyData?.reduce((s, d) => s + d.request_count, 0) ?? 0
|
|
const totalTokens = dailyData?.reduce((s, d) => s + d.input_tokens + d.output_tokens, 0) ?? 0
|
|
|
|
const dailyColumns: ProColumns<DailyUsageStat>[] = [
|
|
{ title: '日期', dataIndex: 'day', width: 120 },
|
|
{
|
|
title: '请求数',
|
|
dataIndex: 'request_count',
|
|
width: 100,
|
|
render: (_, r) => r.request_count.toLocaleString(),
|
|
},
|
|
{
|
|
title: '输入 Token',
|
|
dataIndex: 'input_tokens',
|
|
width: 120,
|
|
render: (_, r) => r.input_tokens.toLocaleString(),
|
|
},
|
|
{
|
|
title: '输出 Token',
|
|
dataIndex: 'output_tokens',
|
|
width: 120,
|
|
render: (_, r) => r.output_tokens.toLocaleString(),
|
|
},
|
|
{ title: '设备数', dataIndex: 'unique_devices', width: 80 },
|
|
]
|
|
|
|
const modelColumns: ProColumns<ModelUsageStat>[] = [
|
|
{ title: '模型', dataIndex: 'model_id', width: 200 },
|
|
{
|
|
title: '请求数',
|
|
dataIndex: 'request_count',
|
|
width: 100,
|
|
render: (_, r) => r.request_count.toLocaleString(),
|
|
},
|
|
{
|
|
title: '输入 Token',
|
|
dataIndex: 'input_tokens',
|
|
width: 120,
|
|
render: (_, r) => r.input_tokens.toLocaleString(),
|
|
},
|
|
{
|
|
title: '输出 Token',
|
|
dataIndex: 'output_tokens',
|
|
width: 120,
|
|
render: (_, r) => r.output_tokens.toLocaleString(),
|
|
},
|
|
{
|
|
title: '平均延迟',
|
|
dataIndex: 'avg_latency_ms',
|
|
width: 100,
|
|
render: (_, r) => (r.avg_latency_ms ? `${Math.round(r.avg_latency_ms)}ms` : '-'),
|
|
},
|
|
{
|
|
title: '成功率',
|
|
dataIndex: 'success_rate',
|
|
width: 100,
|
|
render: (_, r) => `${(r.success_rate * 100).toFixed(1)}%`,
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
title="用量统计"
|
|
description="查看模型使用情况和 Token 消耗"
|
|
actions={
|
|
<Select
|
|
value={days}
|
|
onChange={setDays}
|
|
options={[
|
|
{ value: 7, label: '最近 7 天' },
|
|
{ value: 30, label: '最近 30 天' },
|
|
{ value: 90, label: '最近 90 天' },
|
|
]}
|
|
className="w-36"
|
|
/>
|
|
}
|
|
/>
|
|
|
|
{/* Summary Cards */}
|
|
<Row gutter={[16, 16]} className="mb-6">
|
|
<Col xs={24} sm={12}>
|
|
<Card className="hover:shadow-md transition-shadow duration-200">
|
|
<Statistic
|
|
title={
|
|
<span className="text-neutral-500 dark:text-neutral-400 text-xs font-medium uppercase tracking-wide">
|
|
总请求数
|
|
</span>
|
|
}
|
|
value={totalRequests}
|
|
prefix={<ThunderboltOutlined style={{ color: '#863bff' }} />}
|
|
valueStyle={{ fontWeight: 600, color: '#863bff' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12}>
|
|
<Card className="hover:shadow-md transition-shadow duration-200">
|
|
<Statistic
|
|
title={
|
|
<span className="text-neutral-500 dark:text-neutral-400 text-xs font-medium uppercase tracking-wide">
|
|
总 Token 数
|
|
</span>
|
|
}
|
|
value={totalTokens}
|
|
prefix={<ColumnWidthOutlined style={{ color: '#47bfff' }} />}
|
|
valueStyle={{ fontWeight: 600, color: '#47bfff' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* Daily Stats */}
|
|
<Card
|
|
title={
|
|
<span className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
|
|
每日统计
|
|
</span>
|
|
}
|
|
className="mb-6"
|
|
size="small"
|
|
styles={{ body: { padding: 0 } }}
|
|
>
|
|
<ProTable<DailyUsageStat>
|
|
columns={dailyColumns}
|
|
dataSource={dailyData ?? []}
|
|
loading={dailyLoading}
|
|
rowKey="day"
|
|
search={false}
|
|
toolBarRender={false}
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
|
|
{/* Model Stats */}
|
|
<Card
|
|
title={
|
|
<span className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
|
|
按模型统计
|
|
</span>
|
|
}
|
|
size="small"
|
|
styles={{ body: { padding: 0 } }}
|
|
>
|
|
<ProTable<ModelUsageStat>
|
|
columns={modelColumns}
|
|
dataSource={modelData ?? []}
|
|
loading={modelLoading}
|
|
rowKey="model_id"
|
|
search={false}
|
|
toolBarRender={false}
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|