优化网络与系统检测为缓存优先,避免页面长时间停留在检测中。

将网络/IP 路由拆分并加入去重缓存,系统检测改为线程执行并支持强制刷新,前端改为先展示缓存快照后按需手动重测,降低阻塞与重复探测开销。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric
2026-07-08 14:38:59 -05:00
parent 1287b72193
commit 0661c33610
7 changed files with 375 additions and 126 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import { apiFetch, apiUrl } from './api'
import { saveWorkspaceSnapshot } from './workspace'
import { saveWorkspaceSnapshot, loadWorkspaceSnapshot } from './workspace'
import ProxySettings from './ProxySettings'
import { type FlowStep } from './components/worker/WorkerFlowStrip'
import LivePulse from './components/ui/LivePulse'
@@ -308,74 +308,44 @@ export default function NetworkDashboard() {
const [data, setData] = useState<NetworkPayload | null>(null)
const [ipProfile, setIpProfile] = useState<IpProfilePayload | null>(null)
const [ipProfileErr, setIpProfileErr] = useState<string | null>(null)
const [ipProfileLoading, setIpProfileLoading] = useState(true)
const [ipProfileLoading, setIpProfileLoading] = useState(false)
const [ipQuality, setIpQuality] = useState<IpQualityPayload | null>(null)
const [ipQualityErr, setIpQualityErr] = useState<string | null>(null)
const [ipQualityLoading, setIpQualityLoading] = useState(true)
const [ipQualityLoading, setIpQualityLoading] = useState(false)
const [proxyCfg, setProxyCfg] = useState<ProxyCfg>({ enabled: false, http: '', socks: '' })
const [err, setErr] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [hasCachedData, setHasCachedData] = useState(false)
const { leak: webrtcLeak } = useWebRtcLeak(true)
const loadIpProfile = useCallback(async () => {
setIpProfileErr(null)
setIpProfileLoading(true)
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''
const lang = navigator.language || ''
const q = new URLSearchParams({ tz, lang })
if (webrtcLeak != null) q.set('webrtc_leak', webrtcLeak ? '1' : '0')
const res = await apiFetch(apiUrl(`/ip_profile?${q.toString()}`))
const json = (await res.json()) as IpProfilePayload
if (!res.ok) {
setIpProfileErr(json.error || `IP 画像请求失败 ${res.status}`)
setIpProfile(null)
return
}
setIpProfile(json)
saveWorkspaceSnapshot('ip_profile', json)
} catch (e) {
setIpProfileErr(e instanceof Error ? e.message : String(e))
setIpProfile(null)
} finally {
setIpProfileLoading(false)
}
}, [webrtcLeak])
const loadIpQuality = useCallback(async () => {
setIpQualityErr(null)
setIpQualityLoading(true)
try {
const res = await apiFetch(apiUrl('/ip_quality'))
const json = (await res.json()) as IpQualityPayload
if (!res.ok) {
setIpQualityErr(json.error || `IP 质量请求失败 ${res.status}`)
setIpQuality(null)
return
}
setIpQuality(json)
saveWorkspaceSnapshot('ip_quality', json)
} catch (e) {
setIpQualityErr(e instanceof Error ? e.message : String(e))
setIpQuality(null)
} finally {
setIpQualityLoading(false)
}
}, [])
const load = useCallback(async (opts?: { silent?: boolean }) => {
const load = useCallback(async (opts?: { silent?: boolean; forceRefresh?: boolean }) => {
const silent = opts?.silent ?? false
const forceRefresh = opts?.forceRefresh ?? false
if (!forceRefresh) return
setErr(null)
if (!silent) setLoading(true)
else setRefreshing(true)
setIpProfileLoading(true)
setIpQualityLoading(true)
try {
const [netRes, proxyRes] = await Promise.all([
apiFetch(apiUrl('/network_status')),
const q = '?force_refresh=1'
const [netRes, proxyRes, profileRes, qualityRes] = await Promise.all([
apiFetch(apiUrl(`/network_status${q}`)),
apiFetch(apiUrl('/proxy_config')),
apiFetch(
apiUrl(
`/ip_profile?${new URLSearchParams({
tz: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
lang: navigator.language || '',
...(webrtcLeak != null ? { webrtc_leak: webrtcLeak ? '1' : '0' } : {}),
force_refresh: '1',
}).toString()}`,
),
),
apiFetch(apiUrl('/ip_quality?force_refresh=1')),
])
void loadIpProfile()
void loadIpQuality()
const json = (await netRes.json()) as NetworkPayload & { error?: string }
if (!netRes.ok) {
setErr(json.error || `请求失败 ${netRes.status}`)
@@ -383,7 +353,29 @@ export default function NetworkDashboard() {
} else {
setData(json)
saveWorkspaceSnapshot('network_status', json)
setHasCachedData(true)
}
const profileJson = (await profileRes.json()) as IpProfilePayload
if (!profileRes.ok) {
setIpProfileErr(profileJson.error || `IP 画像请求失败 ${profileRes.status}`)
setIpProfile(null)
} else {
setIpProfile(profileJson)
setIpProfileErr(null)
saveWorkspaceSnapshot('ip_profile', profileJson)
}
const qualityJson = (await qualityRes.json()) as IpQualityPayload
if (!qualityRes.ok) {
setIpQualityErr(qualityJson.error || `IP 质量请求失败 ${qualityRes.status}`)
setIpQuality(null)
} else {
setIpQuality(qualityJson)
setIpQualityErr(null)
saveWorkspaceSnapshot('ip_quality', qualityJson)
}
if (proxyRes.ok) {
const proxyJson = (await proxyRes.json()) as ProxyCfg
setProxyCfg({
@@ -398,22 +390,44 @@ export default function NetworkDashboard() {
} finally {
setLoading(false)
setRefreshing(false)
setIpProfileLoading(false)
setIpQualityLoading(false)
}
}, [loadIpProfile, loadIpQuality])
}, [webrtcLeak])
useEffect(() => {
void load()
}, [load])
useEffect(() => {
const id = window.setInterval(() => void load({ silent: true }), 45000)
return () => clearInterval(id)
}, [load])
useEffect(() => {
if (webrtcLeak == null) return
void loadIpProfile()
}, [webrtcLeak, loadIpProfile])
let cancelled = false
void (async () => {
const [net, profile, quality] = await Promise.all([
loadWorkspaceSnapshot<NetworkPayload>('network_status'),
loadWorkspaceSnapshot<IpProfilePayload>('ip_profile'),
loadWorkspaceSnapshot<IpQualityPayload>('ip_quality'),
])
if (cancelled) return
if (net) {
setData(net)
setHasCachedData(true)
}
if (profile) setIpProfile(profile)
if (quality) setIpQuality(quality)
try {
const proxyRes = await apiFetch(apiUrl('/proxy_config'))
if (!cancelled && proxyRes.ok) {
const proxyJson = (await proxyRes.json()) as ProxyCfg
setProxyCfg({
enabled: !!proxyJson.enabled,
http: proxyJson.http || '',
socks: proxyJson.socks || '',
})
}
} catch {
/* 代理配置读取失败不影响展示缓存结果 */
}
})()
return () => {
cancelled = true
}
}, [])
const view = data ?? DEFAULT_NETWORK
const status = overallStatus(view, loading)
@@ -479,7 +493,15 @@ export default function NetworkDashboard() {
: ['net-routes__pill--no', '境内外出口可能未区分']
const statusLabel =
loading ? '检测中' : status === 'ready' ? '网络就绪' : status === 'warn' ? '需关注' : '出口异常'
loading || refreshing
? '检测中'
: !hasCachedData
? '待检测'
: status === 'ready'
? '网络就绪'
: status === 'warn'
? '需关注'
: '出口异常'
const pulseTone = status === 'ready' ? 'live' : status === 'warn' ? 'warn' : 'idle'
@@ -517,10 +539,10 @@ export default function NetworkDashboard() {
type="button"
className="btn btn-primary btn--sm net-summary__btn"
disabled={loading || refreshing || ipProfileLoading || ipQualityLoading}
onClick={() => void load({ silent: !!data })}
onClick={() => void load({ silent: hasCachedData, forceRefresh: true })}
>
<IconRefresh className={loading || refreshing ? 'is-spinning' : ''} />
{loading || refreshing ? '检测中…' : '重新检测'}
{loading || refreshing ? '检测中…' : hasCachedData ? '重新检测' : '开始检测'}
</button>
<button type="button" className="btn btn-secondary btn--sm" onClick={openProxySettings}>
Windows

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { apiFetch, apiUrl } from './api'
import { saveWorkspaceSnapshot } from './workspace'
import { saveWorkspaceSnapshot, loadWorkspaceSnapshot } from './workspace'
import MetricBar, { toneFromPercent } from './components/ui/MetricBar'
type Metrics = {
@@ -81,10 +81,18 @@ function InfoRow({ label, value }: { label: string; value: string }) {
export default function SystemMetrics() {
const [data, setData] = useState<Metrics | null>(null)
const [err, setErr] = useState<string | null>(null)
const [refreshing, setRefreshing] = useState(false)
const [hasCachedData, setHasCachedData] = useState(false)
const inFlightRef = useRef(false)
const load = useCallback(async () => {
const load = useCallback(async (opts?: { forceRefresh?: boolean }) => {
const forceRefresh = opts?.forceRefresh ?? false
if (!forceRefresh) return
if (inFlightRef.current) return
inFlightRef.current = true
setRefreshing(true)
try {
const res = await apiFetch(apiUrl('/system_metrics'))
const res = await apiFetch(apiUrl('/system_metrics?force_refresh=1'))
const json = (await res.json()) as Metrics
if (!res.ok) {
setErr(json.error || `HTTP ${res.status}`)
@@ -92,17 +100,30 @@ export default function SystemMetrics() {
}
setErr(null)
setData(json)
setHasCachedData(true)
saveWorkspaceSnapshot('system_metrics', json)
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
} finally {
setRefreshing(false)
inFlightRef.current = false
}
}, [])
useEffect(() => {
void load()
const id = window.setInterval(() => void load(), 3000)
return () => clearInterval(id)
}, [load])
let cancelled = false
void (async () => {
const cached = await loadWorkspaceSnapshot<Metrics>('system_metrics')
if (cancelled) return
if (cached) {
setData(cached)
setHasCachedData(true)
}
})()
return () => {
cancelled = true
}
}, [])
const sys = data?.system
const hw = data?.hardware
@@ -122,7 +143,17 @@ export default function SystemMetrics() {
</h2>
<p className="card__desc"></p>
</div>
<span className="metrics-uptime"> {fmtUptime(sys?.boot_time_unix)}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="metrics-uptime"> {fmtUptime(sys?.boot_time_unix)}</span>
<button
type="button"
className="btn btn-secondary btn--sm"
disabled={refreshing}
onClick={() => void load({ forceRefresh: true })}
>
{refreshing ? '检测中…' : hasCachedData ? '重新检测' : '开始检测'}
</button>
</div>
</div>
{err ? (
@@ -209,7 +240,7 @@ export default function SystemMetrics() {
<div className="metrics-disks__title"></div>
<ul className="metrics-disk-list">
{data.disks.slice(0, 8).map((d) => (
<li key={d.mountpoint}>
<li key={`${d.mountpoint || 'unknown'}-${d.device || 'dev'}`}>
<span className="metrics-disk-list__mp">
{d.mountpoint}
{d.fstype ? ` (${d.fstype})` : ''}

View File

@@ -25,6 +25,18 @@ export function saveWorkspaceSnapshot(key: string, data: unknown): void {
void getWorkspace()?.saveSnapshot(key, data)
}
export async function loadWorkspaceSnapshot<T>(key: string): Promise<T | null> {
const ws = getWorkspace()
if (!ws) return null
try {
const dash = await ws.getDashboard()
const item = dash.snapshots[key]
return item?.data != null ? (item.data as T) : null
} catch {
return null
}
}
export function logUserAction(action: string, nav?: string, detail?: unknown): void {
void getWorkspace()?.logUserAction({ action, nav, detail })
}