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
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:
@@ -3,7 +3,7 @@
|
||||
// ============================================================
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Card, Col, Row, Statistic, Table, Tag, Typography, Spin, Alert } from 'antd'
|
||||
import { Card, Col, Row, Statistic, Table, Tag, Spin } from 'antd'
|
||||
import {
|
||||
TeamOutlined,
|
||||
CloudServerOutlined,
|
||||
@@ -13,34 +13,18 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { statsService } from '@/services/stats'
|
||||
import { logService } from '@/services/logs'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { ErrorState } from '@/components/ErrorState'
|
||||
import { actionLabels, actionColors } from '@/constants/status'
|
||||
import type { OperationLog } from '@/types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const actionLabels: Record<string, string> = {
|
||||
login: '登录', logout: '登出',
|
||||
create_account: '创建账号', update_account: '更新账号', delete_account: '删除账号',
|
||||
create_provider: '创建服务商', update_provider: '更新服务商', delete_provider: '删除服务商',
|
||||
create_model: '创建模型', update_model: '更新模型', delete_model: '删除模型',
|
||||
create_token: '创建密钥', revoke_token: '撤销密钥',
|
||||
update_config: '更新配置',
|
||||
create_prompt: '创建提示词', update_prompt: '更新提示词', archive_prompt: '归档提示词',
|
||||
desktop_audit: '桌面端审计',
|
||||
}
|
||||
|
||||
const actionColors: Record<string, string> = {
|
||||
login: 'green', logout: 'default',
|
||||
create_account: 'blue', update_account: 'orange', delete_account: 'red',
|
||||
create_provider: 'blue', update_provider: 'orange', delete_provider: 'red',
|
||||
create_model: 'blue', update_model: 'orange', delete_model: 'red',
|
||||
create_token: 'blue', revoke_token: 'red',
|
||||
update_config: 'orange',
|
||||
create_prompt: 'blue', update_prompt: 'orange', archive_prompt: 'red',
|
||||
desktop_audit: 'default',
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data: stats, isLoading: statsLoading, error: statsError } = useQuery({
|
||||
const {
|
||||
data: stats,
|
||||
isLoading: statsLoading,
|
||||
error: statsError,
|
||||
refetch: refetchStats,
|
||||
} = useQuery({
|
||||
queryKey: ['dashboard-stats'],
|
||||
queryFn: ({ signal }) => statsService.dashboard(signal),
|
||||
})
|
||||
@@ -51,15 +35,28 @@ export default function Dashboard() {
|
||||
})
|
||||
|
||||
if (statsError) {
|
||||
return <Alert type="error" message="加载仪表盘数据失败" description={(statsError as Error).message} showIcon />
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="仪表盘" description="系统概览与最近活动" />
|
||||
<ErrorState
|
||||
message={(statsError as Error).message}
|
||||
onRetry={() => refetchStats()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const statCards = [
|
||||
{ title: '总账号', value: stats?.total_accounts ?? 0, icon: <TeamOutlined />, color: '#1677ff' },
|
||||
{ title: '活跃服务商', value: stats?.active_providers ?? 0, icon: <CloudServerOutlined />, color: '#52c41a' },
|
||||
{ title: '活跃模型', value: stats?.active_models ?? 0, icon: <ApiOutlined />, color: '#722ed1' },
|
||||
{ title: '今日请求', value: stats?.tasks_today ?? 0, icon: <ThunderboltOutlined />, color: '#fa8c16' },
|
||||
{ title: '今日 Token', value: ((stats?.tokens_today_input ?? 0) + (stats?.tokens_today_output ?? 0)), icon: <ColumnWidthOutlined />, color: '#eb2f96' },
|
||||
{ title: '总账号', value: stats?.total_accounts ?? 0, icon: <TeamOutlined />, color: '#863bff' },
|
||||
{ title: '活跃服务商', value: stats?.active_providers ?? 0, icon: <CloudServerOutlined />, color: '#47bfff' },
|
||||
{ title: '活跃模型', value: stats?.active_models ?? 0, icon: <ApiOutlined />, color: '#22c55e' },
|
||||
{ title: '今日请求', value: stats?.tasks_today ?? 0, icon: <ThunderboltOutlined />, color: '#f59e0b' },
|
||||
{
|
||||
title: '今日 Token',
|
||||
value: (stats?.tokens_today_input ?? 0) + (stats?.tokens_today_output ?? 0),
|
||||
icon: <ColumnWidthOutlined />,
|
||||
color: '#ef4444',
|
||||
},
|
||||
]
|
||||
|
||||
const logColumns = [
|
||||
@@ -74,7 +71,13 @@ export default function Dashboard() {
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '目标类型', dataIndex: 'target_type', key: 'target_type', width: 100, render: (v: string | null) => v || '-' },
|
||||
{
|
||||
title: '目标类型',
|
||||
dataIndex: 'target_type',
|
||||
key: 'target_type',
|
||||
width: 100,
|
||||
render: (v: string | null) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
@@ -86,19 +89,34 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 24 }}>仪表盘</Title>
|
||||
<PageHeader title="仪表盘" description="系统概览与最近活动" />
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{/* Stat Cards */}
|
||||
<Row gutter={[16, 16]} className="mb-6">
|
||||
{statsLoading ? (
|
||||
<Col span={24}><Spin /></Col>
|
||||
<Col span={24}>
|
||||
<div className="flex justify-center py-8">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</Col>
|
||||
) : (
|
||||
statCards.map((card) => (
|
||||
<Col xs={24} sm={12} md={8} lg={4} key={card.title}>
|
||||
<Card>
|
||||
<Card
|
||||
className="hover:shadow-md transition-shadow duration-200"
|
||||
styles={{ body: { padding: '20px 24px' } }}
|
||||
>
|
||||
<Statistic
|
||||
title={card.title}
|
||||
title={
|
||||
<span className="text-neutral-500 dark:text-neutral-400 text-xs font-medium uppercase tracking-wide">
|
||||
{card.title}
|
||||
</span>
|
||||
}
|
||||
value={card.value}
|
||||
prefix={<span style={{ color: card.color }}>{card.icon}</span>}
|
||||
valueStyle={{ fontSize: 28, fontWeight: 600, color: card.color }}
|
||||
prefix={
|
||||
<span style={{ color: card.color, marginRight: 4 }}>{card.icon}</span>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -106,7 +124,16 @@ export default function Dashboard() {
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Card title="最近操作日志" size="small">
|
||||
{/* Recent Logs */}
|
||||
<Card
|
||||
title={
|
||||
<span className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
|
||||
最近操作日志
|
||||
</span>
|
||||
}
|
||||
size="small"
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<Table<OperationLog>
|
||||
columns={logColumns}
|
||||
dataSource={logsData?.items ?? []}
|
||||
|
||||
Reference in New Issue
Block a user