Files
BiliNote/BillNote_frontend/src/components/BackendHealth/BackendHealthIndicator.tsx
huangjianwu 1329390f98 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>
2026-05-09 14:27:02 +08:00

112 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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