Files
BiliNote/BillNote_frontend/src/components/BackendInitDialog.tsx
huangjianwu 37f7ee6e15 feat(desktop): 后端健康监控韧性 + onboarding 修复 + 全局代理 UI
- useCheckBackend 重写:60s 总超时取代 while(true) 死轮询,订阅 Tauri
  backend-ready/terminated/startup-timeout 事件,裸 fetch 探测避免
  启动期 toast 叠堆
- Tauri lib.rs:spawn 后 HTTP 探针轮询 /api/sys_check 拿 200 才算就绪
  (之前 TCP connect 会被孤儿进程误判);RunEvent::Exit 钩子退出前
  kill sidecar,修孤儿进程占端口;restart 前发 backend-restarting
  让前端忽略主动 kill 引发的 terminated
- BackendInitDialog:失败态展示原因 + 最近 stderr + 重启/复制日志按钮
- StartupBanner:收到 restarted/ready 自动清「已退出」横幅
- BackendHealthIndicator:修 /api/api/sys_health 双前缀 404
- Onboarding:step1 后端连通改自动重试 + 事件触发 + 手动按钮;step2
  撞预置供应商名时改为更新已存在供应商;errText 统一错误文案
- 全局代理 UI:下载配置页新增代理卡片(services/proxy.ts + ProxyConfig)
- request.ts 加 suppressToast 配置位,预期失败不弹全局红 toast
- NoteForm/taskStore:捕获就绪门禁错误,引导去音频转写配置页下载
- providerCard:整行可点切换(之前只有 icon 区域响应)
- Monitor 页 Whisper 卡显示模型本地下载状态
- tauri/api 升级对齐 2.11,修 vite build 版本不匹配

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:01:37 +08:00

155 lines
5.9 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 { useMemo, useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Loader2, AlertTriangle, RotateCcw, Clipboard } from 'lucide-react'
import { useBackendEvents } from '@/components/BackendHealth/useBackendEvents'
// 失败态预览里最多展示几行 stderr。比这还多就请用户去 copyLogs() 拷出来。
const STDERR_PREVIEW_LINES = 6
interface Props {
/** 加载中:显示转圈对话框 */
open: boolean
/** 启动失败:显示错误 + 重启/复制日志按钮 */
failed?: boolean
/** 失败原因(来自 useCheckBackend.lastError 或 Tauri 事件 payload */
lastError?: string | null
/** 重新走一遍 useCheckBackend 的轮询(不重启 sidecar */
onRetry?: () => void
}
// 加载中 + 启动失败两个状态合并在一个 dialog 里。
// 失败态比加载态更紧急:用户能看到具体原因 + 一键重启 + 一键复制日志去 issue
// 而不是面对一个永远转圈的对话框。
function BackendInitDialog({ open, failed = false, lastError = null, onRetry }: Props) {
const { isTauri, restart, copyLogs, logs } = useBackendEvents()
const [restarting, setRestarting] = useState(false)
const [copyResult, setCopyResult] = useState<'idle' | 'ok' | 'fail'>('idle')
// 从 ring buffer 里挑最后几行 stderr —— 它们比 lastErrorhook 自己总结的那句)信息密度更高,
// 通常 Python traceback 的最后一行就是真正的错误类型 + 消息
const stderrPreview = useMemo(() => {
if (!failed || !logs?.length) return []
return logs
.filter((l) => l.level === 'error')
.slice(-STDERR_PREVIEW_LINES)
.map((l) => l.text)
}, [failed, logs])
// 任一态需要展示就保持 dialog 开着,关掉只在两个 flag 都熄灭时发生
const isOpen = open || failed
const handleRestart = async () => {
setRestarting(true)
try {
if (isTauri) await restart()
onRetry?.()
} catch {
// restart 内部已经 append 到 log这里不再 toast
} finally {
setRestarting(false)
}
}
const handleCopy = async () => {
const ok = await copyLogs()
setCopyResult(ok ? 'ok' : 'fail')
setTimeout(() => setCopyResult('idle'), 2000)
}
if (failed) {
return (
<Dialog open={isOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
</DialogTitle>
</DialogHeader>
<div className="space-y-3 mt-2 text-sm">
<p className="text-muted-foreground">
{lastError || '后端在预计时间内未就绪。'}
</p>
{stderrPreview.length > 0 && (
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">
{stderrPreview.length}
<span className="opacity-60"></span>:
</p>
<pre className="max-h-32 overflow-auto rounded bg-zinc-900 px-2 py-1.5 font-mono text-[11px] leading-snug text-red-200">
{stderrPreview.join('\n')}
</pre>
</div>
)}
<div className="text-xs text-muted-foreground space-y-1">
<p></p>
<ul className="list-disc list-inside space-y-0.5 pl-1">
<li> / PyInstaller </li>
<li> ffmpeg / 8483 </li>
<li> whisper </li>
</ul>
</div>
<div className="flex flex-wrap gap-2 pt-2">
<Button
size="sm"
onClick={handleRestart}
disabled={restarting}
className="gap-1.5"
>
{restarting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<RotateCcw className="w-4 h-4" />
)}
{isTauri ? (restarting ? '重启中…' : '重启后端') : '重试'}
</Button>
{isTauri && (
<Button size="sm" variant="outline" onClick={handleCopy} className="gap-1.5">
<Clipboard className="w-4 h-4" />
{copyResult === 'ok'
? '已复制 ✓'
: copyResult === 'fail'
? '复制失败'
: '复制启动日志'}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground pt-2">
&nbsp;
<a
href="https://github.com/JefferyHcool/BiliNote/issues"
target="_blank"
rel="noreferrer"
className="text-blue-600 underline"
>
GitHub Issues
</a>
&nbsp;
</p>
</div>
</DialogContent>
</Dialog>
)
}
// 默认加载态
return (
<Dialog open={isOpen}>
<DialogContent className="text-center">
<DialogHeader>
<DialogTitle className="flex items-center justify-center gap-2">
<Loader2 className="animate-spin w-5 h-5" />
</DialogTitle>
</DialogHeader>
<p className="text-muted-foreground mt-2">
10-30
</p>
</DialogContent>
</Dialog>
)
}
export default BackendInitDialog