import { useQuery } from '@tanstack/react-query'; import { AlertTriangle, Coins, Gauge, RefreshCw, Rows3, Wrench, Zap } from 'lucide-react-native'; import { useMemo, useState } from 'react'; import { Platform, Pressable, Text, View, useWindowDimensions } from 'react-native'; import { BarChartCard } from '@/src/components/bar-chart-card'; import { DonutChartCard } from '@/src/components/donut-chart-card'; import { LineTrendChart } from '@/src/components/line-trend-chart'; import { ListCard } from '@/src/components/list-card'; import { ScreenShell } from '@/src/components/screen-shell'; import { useScreenInteractive } from '@/src/hooks/use-screen-interactive'; import { formatTokenValue } from '@/src/lib/formatters'; import { getAdminSettings, getDashboardModels, getDashboardStats, getDashboardTrend, listAccounts } from '@/src/services/admin'; import { adminConfigState } from '@/src/store/admin-config'; const { useSnapshot } = require('valtio/react'); type RangeKey = '24h' | '7d' | '30d'; function getDateRange(rangeKey: RangeKey) { const end = new Date(); const start = new Date(); if (rangeKey === '24h') { start.setHours(end.getHours() - 23, 0, 0, 0); } else if (rangeKey === '30d') { start.setDate(end.getDate() - 29); } else { start.setDate(end.getDate() - 6); } const toDate = (value: Date) => value.toISOString().slice(0, 10); return { start_date: toDate(start), end_date: toDate(end), granularity: rangeKey === '24h' ? ('hour' as const) : ('day' as const), }; } const RANGE_OPTIONS: Array<{ key: RangeKey; label: string }> = [ { key: '24h', label: '24H' }, { key: '7d', label: '7D' }, { key: '30d', label: '30D' }, ]; function getPointLabel(value: string, rangeKey: RangeKey) { if (rangeKey === '24h') { return value.slice(11, 13); } return value.slice(5, 10); } export default function MonitorScreen() { useScreenInteractive('monitor_interactive'); const config = useSnapshot(adminConfigState); const { width } = useWindowDimensions(); const contentWidth = Math.max(width - 24, 280); const [rangeKey, setRangeKey] = useState('7d'); const range = useMemo(() => getDateRange(rangeKey), [rangeKey]); const hasAccount = Boolean(config.baseUrl.trim()); const statsQuery = useQuery({ queryKey: ['monitor-stats'], queryFn: getDashboardStats, enabled: hasAccount, }); const trendQuery = useQuery({ queryKey: ['monitor-trend', rangeKey, range.start_date, range.end_date, range.granularity], queryFn: () => getDashboardTrend(range), enabled: hasAccount, }); const modelsQuery = useQuery({ queryKey: ['monitor-models', rangeKey, range.start_date, range.end_date], queryFn: () => getDashboardModels(range), enabled: hasAccount, }); const settingsQuery = useQuery({ queryKey: ['admin-settings'], queryFn: getAdminSettings, enabled: hasAccount, }); const accountsQuery = useQuery({ queryKey: ['monitor-accounts'], queryFn: () => listAccounts(''), enabled: hasAccount, }); const stats = statsQuery.data; const trend = trendQuery.data?.trend ?? []; const accounts = accountsQuery.data?.items ?? []; const siteName = settingsQuery.data?.site_name?.trim() || '管理控制台'; const throughputPoints = useMemo( () => trend.map((item) => ({ label: getPointLabel(item.date, rangeKey), value: item.total_tokens })), [rangeKey, trend] ); const requestPoints = useMemo( () => trend.map((item) => ({ label: getPointLabel(item.date, rangeKey), value: item.requests })), [rangeKey, trend] ); const costPoints = useMemo( () => trend.map((item) => ({ label: getPointLabel(item.date, rangeKey), value: item.cost })), [rangeKey, trend] ); const topModels = useMemo(() => (modelsQuery.data?.models ?? []).slice(0, 5), [modelsQuery.data?.models]); const incidentAccounts = useMemo( () => accounts.filter((item) => item.status === 'error' || item.error_message).slice(0, 5), [accounts] ); const totalInputTokens = useMemo(() => trend.reduce((sum, item) => sum + item.input_tokens, 0), [trend]); const totalOutputTokens = useMemo(() => trend.reduce((sum, item) => sum + item.output_tokens, 0), [trend]); const totalCacheReadTokens = useMemo(() => trend.reduce((sum, item) => sum + item.cache_read_tokens, 0), [trend]); const busyAccounts = useMemo( () => accounts.filter((item) => (item.current_concurrency ?? 0) > 0 && item.status !== 'error' && !item.error_message).length, [accounts] ); const pausedAccounts = useMemo( () => accounts.filter((item) => item.schedulable === false && item.status !== 'error' && !item.error_message).length, [accounts] ); const errorAccounts = useMemo( () => accounts.filter((item) => item.status === 'error' || item.error_message).length, [accounts] ); const healthyAccounts = Math.max(accounts.length - busyAccounts - pausedAccounts - errorAccounts, 0); const summaryCards = [ { label: 'Token', value: stats ? formatTokenValue(stats.today_tokens ?? 0) : '--', icon: Zap, tone: 'dark' as const, }, { label: '成本', value: stats ? `$${Number(stats.today_cost ?? 0).toFixed(2)}` : '--', icon: Coins, }, { label: '输出', value: stats ? formatTokenValue(stats.today_output_tokens ?? 0) : '--', icon: Rows3, }, { label: '账号', value: String(accounts.length || stats?.total_accounts || 0), detail: `${errorAccounts} 异常 / ${pausedAccounts} 暂停`, icon: Rows3, }, { label: 'TPM', value: String(stats?.tpm ?? '--'), icon: Gauge, }, { label: '健康', value: String(healthyAccounts), detail: `${busyAccounts} 繁忙`, icon: AlertTriangle, }, ]; const summaryRows = [0, 3].map((index) => summaryCards.slice(index, index + 3)); const useMasonry = Platform.OS === 'web' || width >= 640; const summaryCardWidth = Math.floor((contentWidth - 16) / 3); const cards = [ { key: 'throughput', node: throughputPoints.length > 1 ? ( ) : null, }, { key: 'requests', node: requestPoints.length > 1 ? ( ) : null, }, { key: 'cost', node: costPoints.length > 1 ? ( `$${value.toFixed(2)}`} compact={useMasonry} /> ) : null, }, { key: 'token-structure', node: ( ), }, { key: 'health', node: ( ), }, { key: 'models', node: ( ({ label: item.model, value: item.total_tokens, color: '#a34d2d', meta: `请求 ${item.requests} · 成本 $${Number(item.cost).toFixed(2)}`, }))} formatValue={formatTokenValue} /> ), }, { key: 'incidents', node: ( {incidentAccounts.map((item) => ( {item.name} {item.platform} · {item.status || 'unknown'} · {item.schedulable ? '可调度' : '暂停调度'} {item.error_message || '状态异常,建议从运维视角继续排查这个上游账号'} ))} {incidentAccounts.length === 0 ? 当前没有检测到异常账号。 : null} ), }, ].filter((item) => item.node); const leftColumn = cards.filter((_, index) => index % 2 === 0); const rightColumn = cards.filter((_, index) => index % 2 === 1); return ( {siteName} 的关键运行指标。} variant="minimal" horizontalInsetClassName="px-3" contentGapClassName="mt-3 gap-2" right={ {RANGE_OPTIONS.map((item) => { const active = item.key === rangeKey; return ( setRangeKey(item.key)} > {item.label} ); })} { statsQuery.refetch(); trendQuery.refetch(); modelsQuery.refetch(); accountsQuery.refetch(); settingsQuery.refetch(); }} > } > {summaryRows.map((row, rowIndex) => ( {row.map((item) => { const Icon = item.icon; return ( {item.label} {item.value} {'detail' in item && item.detail ? ( {item.detail} ) : null} ); })} ))} {useMasonry ? ( {leftColumn.map((item) => ( {item.node} ))} {rightColumn.map((item) => ( {item.node} ))} ) : ( cards.map((item) => {item.node}) )} ); }