mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-02 15:19:33 +08:00
feat(desktop): Sidecar 健康度面板 + 重启后端能力
P1 已经把 backend-warning / backend-terminated 横幅做出来了;P2 把
lib.rs 那条 stdout/stderr/terminated 信息流真正落到一个常驻 UI 上:
- 右下角浮动状态点(绿/黄/红),轮询 /api/sys_health 决定颜色
- 点开抽屉看最近 200 行日志(ring buffer),含「重启后端」「复制日志」按钮
Rust:
- src-tauri/src/lib.rs:把 sidecar 启动抽出 spawn_backend_sidecar(),
CommandChild 存进 SidecarHandle(Mutex<Option<CommandChild>>) 这个 state
- 新增 #[tauri::command] restart_backend_sidecar:kill 旧 child + 重新 spawn +
emit 'backend-restarted' 给前端
- 监听任务 stdout/stderr emit 时不再用 format!("'{}'", ...) 包引号,原文直传;
前端 hook 同时兼容旧形式(兜底剥引号)
前端:
- components/BackendHealth/useBackendEvents.ts:listen 四个事件 +
ring buffer (MAX 200 行) + invoke restart + clipboard 复制日志
- BackendHealthIndicator.tsx:右下角浮动状态点,5s 轮询 /api/sys_health;
连续 3 次失败或 backend-terminated 触发 → 红
- BackendLogPanel.tsx:右侧抽屉,深色 monospace 日志区 + 操作按钮
- 纯 web 环境(无 __TAURI_INTERNALS__)下静默不挂载
P3 / P4 还在路上。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useBackendEvents } from './useBackendEvents'
|
||||
import BackendLogPanel from './BackendLogPanel'
|
||||
|
||||
// 健康度判定:
|
||||
// - 绿:sidecar running 且 /sys_health 通
|
||||
// - 黄:sidecar running 但 /sys_health 失败 (ffmpeg 缺等)
|
||||
// - 红:sidecar terminated 或 /sys_health 连续 3 次失败
|
||||
|
||||
type Health = 'green' | 'yellow' | 'red' | 'unknown'
|
||||
|
||||
const HEALTH_POLL_MS = 5000
|
||||
const SYS_HEALTH_PATH = '/api/sys_health'
|
||||
|
||||
function backendBase(): string {
|
||||
// 与 services/request.ts 用的一致
|
||||
const fromEnv = (import.meta as any).env?.VITE_API_BASE_URL as string | undefined
|
||||
return (fromEnv ?? '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
const BackendHealthIndicator = () => {
|
||||
const { status, isTauri, exitCode, logs, restart, copyLogs } = useBackendEvents()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [healthCheckFailures, setHealthCheckFailures] = useState(0)
|
||||
const [lastHealthOk, setLastHealthOk] = useState<boolean | null>(null)
|
||||
|
||||
// 仅在 Tauri 环境挂指示器;纯 web 用户由 useCheckBackend 接管
|
||||
useEffect(() => {
|
||||
if (!isTauri) return
|
||||
let mounted = true
|
||||
|
||||
async function ping() {
|
||||
try {
|
||||
const res = await fetch(`${backendBase()}${SYS_HEALTH_PATH}`)
|
||||
const ok = res.ok
|
||||
if (!mounted) return
|
||||
if (ok) {
|
||||
setHealthCheckFailures(0)
|
||||
setLastHealthOk(true)
|
||||
}
|
||||
else {
|
||||
setHealthCheckFailures(c => c + 1)
|
||||
setLastHealthOk(false)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if (!mounted) return
|
||||
setHealthCheckFailures(c => c + 1)
|
||||
setLastHealthOk(false)
|
||||
}
|
||||
}
|
||||
|
||||
ping()
|
||||
const t = setInterval(ping, HEALTH_POLL_MS)
|
||||
return () => {
|
||||
mounted = false
|
||||
clearInterval(t)
|
||||
}
|
||||
}, [isTauri])
|
||||
|
||||
if (!isTauri) return null
|
||||
|
||||
const health: Health = (() => {
|
||||
if (status === 'terminated') return 'red'
|
||||
if (healthCheckFailures >= 3) return 'red'
|
||||
if (lastHealthOk === false) return 'yellow'
|
||||
if (lastHealthOk === true) return 'green'
|
||||
return 'unknown'
|
||||
})()
|
||||
|
||||
const colorMap: Record<Health, string> = {
|
||||
green: 'bg-green-500',
|
||||
yellow: 'bg-amber-500',
|
||||
red: 'bg-red-500',
|
||||
unknown: 'bg-gray-400',
|
||||
}
|
||||
|
||||
const labelMap: Record<Health, string> = {
|
||||
green: '后端运行正常',
|
||||
yellow: '后端运行中(部分检查未通过)',
|
||||
red: status === 'terminated' ? `后端已退出 (code=${exitCode ?? 'unknown'})` : '后端无响应',
|
||||
unknown: '后端状态未知',
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="fixed right-3 bottom-3 z-[9998] flex items-center gap-2 rounded-full border bg-white px-3 py-1.5 text-xs shadow hover:shadow-md"
|
||||
title={labelMap[health]}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<span className={`inline-block h-2 w-2 rounded-full ${colorMap[health]}${health === 'red' || health === 'yellow' ? ' animate-pulse' : ''}`} />
|
||||
<span className="text-gray-700">后端</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<BackendLogPanel
|
||||
status={status}
|
||||
exitCode={exitCode}
|
||||
logs={logs}
|
||||
health={health}
|
||||
onRestart={restart}
|
||||
onCopyLogs={copyLogs}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackendHealthIndicator
|
||||
Reference in New Issue
Block a user