fix(desktop): session persistence — refresh/login/context/empty-content 4-bug fix
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)
This commit is contained in:
iven
2026-03-31 13:38:59 +08:00
parent 3e5d64484e
commit 6cae768401
29 changed files with 1982 additions and 933 deletions

View File

@@ -4,20 +4,24 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Card, Row, Col, Select, Spin, Alert, Statistic, Typography } from 'antd'
import { ColumnWidthOutlined, ThunderboltOutlined } from '@ant-design/icons'
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 { usageService } from '@/services/usage'
import { telemetryService } from '@/services/telemetry'
import { PageHeader } from '@/components/PageHeader'
import { ErrorState } from '@/components/ErrorState'
import type { DailyUsageStat, ModelUsageStat } from '@/types'
const { Title } = Typography
export default function Usage() {
const [days, setDays] = useState(30)
const { data: dailyData, isLoading: dailyLoading, error: dailyError } = useQuery({
const {
data: dailyData,
isLoading: dailyLoading,
error: dailyError,
refetch,
} = useQuery({
queryKey: ['usage-daily', days],
queryFn: ({ signal }) => telemetryService.dailyStats({ days }, signal),
})
@@ -28,7 +32,12 @@ export default function Usage() {
})
if (dailyError) {
return <Alert type="error" message="加载用量数据失败" description={(dailyError as Error).message} showIcon />
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
@@ -36,22 +45,52 @@ export default function Usage() {
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: '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: '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` : '-',
render: (_, r) => (r.avg_latency_ms ? `${Math.round(r.avg_latency_ms)}ms` : '-'),
},
{
title: '成功率',
@@ -63,34 +102,66 @@ export default function Usage() {
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Select
value={days}
onChange={setDays}
options={[
{ value: 7, label: '最近 7 天' },
{ value: 30, label: '最近 30 天' },
{ value: 90, label: '最近 90 天' },
]}
style={{ width: 140 }}
/>
</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"
/>
}
/>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col span={12}>
<Card>
<Statistic title="总请求数" value={totalRequests} prefix={<ThunderboltOutlined />} />
{/* 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 span={12}>
<Card>
<Statistic title="总 Token 数" value={totalTokens} prefix={<ColumnWidthOutlined />} />
<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>
<Card title="每日统计" style={{ marginBottom: 24 }} size="small">
{/* 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 ?? []}
@@ -103,7 +174,16 @@ export default function Usage() {
/>
</Card>
<Card title="按模型统计" size="small">
{/* 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 ?? []}