mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-12 19:20:00 +08:00
feat(desktop): 启动期路径诊断 + 顶端横幅,主动暴露已知失败因素
桌面端历史痛点:PyInstaller sidecar 在含非 ASCII / 含空格 / 不可写的安装路径下
直接炸(README:36 仅文字警告"不要中文路径",无主动防御)。Sidecar 退出事件 lib.rs
已 emit 但前端没 listener 消化,用户看到的是空白主页。
- src-tauri/src/lib.rs:
· setup 中 env::current_exe() 之后做 InstallPathDiagnostics(is_ascii / 空格 /
父目录 write_probe),命中任一异常就 emit 'backend-warning' 给前端
· 用 std::thread::spawn + 1500ms sleep 等首屏 listener 挂上再 emit,避免事件丢失
· 新增 Tauri command get_install_path_diagnostics,前端可主动重查(用户卸载到
新目录后首次启动)
- BillNote_frontend/src/components/SystemDiagnostic/StartupBanner.tsx(新建):
· 监听 backend-warning(路径警告,可关闭)+ backend-terminated(致命,常驻)
· 纯 web 环境(无 __TAURI_INTERNALS__)下静默不挂载,对桌面端定向起效
· backend-error(stderr 噪音)暂不展示,留给后续 P2 日志面板
- App.tsx:StartupBanner 在 BackendInitDialog 之前就挂载,让"后端还在初始化时"
也能看见路径警告(正是该场景容易炸)
不改任何业务逻辑;纯桌面端 UX 加固。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
|
||||
import { useCheckBackend } from '@/hooks/useCheckBackend.ts'
|
||||
import { systemCheck } from '@/services/system.ts'
|
||||
import BackendInitDialog from '@/components/BackendInitDialog'
|
||||
import StartupBanner from '@/components/SystemDiagnostic/StartupBanner'
|
||||
import Index from '@/pages/Index.tsx'
|
||||
import { HomePage } from './pages/HomePage/Home.tsx'
|
||||
|
||||
@@ -34,6 +35,7 @@ function App() {
|
||||
if (!initialized) {
|
||||
return (
|
||||
<>
|
||||
<StartupBanner />
|
||||
<BackendInitDialog open={loading} />
|
||||
</>
|
||||
)
|
||||
@@ -42,6 +44,7 @@ function App() {
|
||||
// 后端已初始化,渲染主应用
|
||||
return (
|
||||
<>
|
||||
<StartupBanner />
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<div className="flex h-screen items-center justify-center">加载中…</div>}>
|
||||
<Routes>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
// backend-error 是 sidecar stderr,量大噪音多,这里不直接展示,留给 P2 的日志面板。
|
||||
unlisteners = [offWarning, offTerminated]
|
||||
})()
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user