mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-21 15:44:07 +08:00
- 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>
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
|
||
// 桌面端启动诊断横幅。监听 Tauri 侧 emit 的 backend-warning / backend-error / backend-terminated。
|
||
// 只在 Tauri 环境生效;纯 web 环境(无 window.__TAURI_INTERNALS__)下静默不挂载。
|
||
|
||
type Severity = 'info' | 'warning' | 'error'
|
||
|
||
interface DiagnosticPayload {
|
||
exe_path?: string
|
||
path_has_non_ascii?: boolean
|
||
path_has_space?: boolean
|
||
parent_writable?: boolean
|
||
platform?: string
|
||
}
|
||
|
||
interface BannerState {
|
||
severity: Severity
|
||
title: string
|
||
detail: string
|
||
payload?: DiagnosticPayload
|
||
dismissible: boolean
|
||
}
|
||
|
||
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
||
|
||
function describeWarning(payload: DiagnosticPayload): { title: string; detail: string } {
|
||
const parts: string[] = []
|
||
if (payload.path_has_non_ascii) {
|
||
parts.push('安装路径包含非 ASCII 字符(中文 / 日文等)')
|
||
}
|
||
if (payload.path_has_space) {
|
||
parts.push('安装路径包含空格')
|
||
}
|
||
if (payload.parent_writable === false) {
|
||
parts.push('安装目录不可写(缺少权限或只读)')
|
||
}
|
||
return {
|
||
title: '检测到可能导致后端启动失败的安装路径',
|
||
detail:
|
||
`${parts.join(';')}。\n` +
|
||
'建议把 BiliNote 重新安装到一个纯英文、无空格、可写的路径下(如 C:\\BiliNote\\ 或 /Applications/)。\n' +
|
||
`当前路径:${payload.exe_path || '未知'}`,
|
||
}
|
||
}
|
||
|
||
const StartupBanner = () => {
|
||
const [banner, setBanner] = useState<BannerState | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (!isTauri) return
|
||
|
||
let unlisteners: Array<() => void> = []
|
||
|
||
;(async () => {
|
||
const { listen } = await import('@tauri-apps/api/event')
|
||
|
||
const offWarning = await listen<DiagnosticPayload>('backend-warning', event => {
|
||
const { title, detail } = describeWarning(event.payload || {})
|
||
setBanner({
|
||
severity: 'warning',
|
||
title,
|
||
detail,
|
||
payload: event.payload,
|
||
dismissible: true,
|
||
})
|
||
})
|
||
|
||
const offTerminated = await listen<number | null>('backend-terminated', event => {
|
||
setBanner({
|
||
severity: 'error',
|
||
title: '后端进程已退出',
|
||
detail: `退出码:${event.payload ?? '未知'}。打开「部署监控」或重启应用以恢复。`,
|
||
dismissible: false,
|
||
})
|
||
})
|
||
|
||
// 后端被「重启后端」按钮拉起来后 / Rust ready-probe 检测到新 sidecar 真的就绪后,
|
||
// 自动清掉 terminated 横幅。之前 dismissible:false + 没自动清逻辑 = banner 永远卡。
|
||
const offRestarted = await listen('backend-restarted', () => {
|
||
setBanner(b => (b?.severity === 'error' ? null : b))
|
||
})
|
||
const offReady = await listen('backend-ready', () => {
|
||
setBanner(b => (b?.severity === 'error' ? null : b))
|
||
})
|
||
|
||
// backend-error 是 sidecar stderr,量大噪音多,这里不直接展示,留给 P2 的日志面板。
|
||
unlisteners = [offWarning, offTerminated, offRestarted, offReady]
|
||
})()
|
||
|
||
return () => {
|
||
unlisteners.forEach(fn => fn())
|
||
}
|
||
}, [])
|
||
|
||
if (!banner) return null
|
||
|
||
const colorByLevel: Record<Severity, string> = {
|
||
info: 'bg-blue-50 border-blue-300 text-blue-900',
|
||
warning: 'bg-amber-50 border-amber-300 text-amber-900',
|
||
error: 'bg-red-50 border-red-300 text-red-900',
|
||
}
|
||
|
||
const iconByLevel: Record<Severity, string> = {
|
||
info: 'ℹ️',
|
||
warning: '⚠️',
|
||
error: '✕',
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={`fixed left-0 right-0 top-0 z-[9999] flex items-start gap-3 border-b px-4 py-2 text-sm shadow-sm ${colorByLevel[banner.severity]}`}
|
||
>
|
||
<span className="text-lg">{iconByLevel[banner.severity]}</span>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="font-medium">{banner.title}</div>
|
||
<pre className="mt-0.5 whitespace-pre-wrap break-words font-sans text-xs opacity-90">
|
||
{banner.detail}
|
||
</pre>
|
||
</div>
|
||
{banner.dismissible && (
|
||
<button
|
||
className="shrink-0 rounded px-2 py-0.5 text-xs hover:bg-black/10"
|
||
onClick={() => setBanner(null)}
|
||
>
|
||
知道了
|
||
</button>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default StartupBanner
|