mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-13 09:00:54 +08:00
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>
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
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
|