feat(system): 添加后端初始化和健康检查功能

- 新增后端初始化对话框组件
- 实现后端健康检查和初始化逻辑
- 在 App 组件中集成后端初始化和健康检查
- 新增系统健康检查 API 和相关服务
This commit is contained in:
JefferyHcool
2025-06-20 13:05:42 +08:00
parent 7f8d4faa44
commit f23ed6ec6c
15 changed files with 267 additions and 25 deletions

View File

@@ -2,10 +2,10 @@ import './App.css'
import { HomePage } from './pages/HomePage/Home.tsx'
import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
import SettingPage from './pages/SettingPage/index.tsx'
import { BrowserRouter,HashRouter, Navigate, Routes } from 'react-router-dom'
import { BrowserRouter, HashRouter, Navigate, Routes } from 'react-router-dom'
import { Route } from 'react-router-dom'
import Index from '@/pages/Index.tsx'
import NotFoundPage from '@/pages/NotFoundPage' //
import NotFoundPage from '@/pages/NotFoundPage'
import Model from '@/pages/SettingPage/Model.tsx'
import Transcriber from '@/pages/SettingPage/transcriber.tsx'
import ProviderForm from '@/components/Form/modelForm/Form.tsx'
@@ -15,15 +15,32 @@ import Prompt from '@/pages/SettingPage/Prompt.tsx'
import AboutPage from '@/pages/SettingPage/about.tsx'
import Downloader from '@/pages/SettingPage/Downloader.tsx'
import DownloaderForm from '@/components/Form/DownloaderForm/Form.tsx'
import { useEffect } from 'react'
import { systemCheck } from '@/services/system.ts'
import { useCheckBackend } from '@/hooks/useCheckBackend.ts'
import BackendInitDialog from '@/components/BackendInitDialog'
function App() {
useTaskPolling(3000) // 每 3 秒轮询一次
const steps = [
{ label: '解析链接', key: 'PARSING', icon: <Downloading /> },
{ label: '下载音频', key: 'DOWNLOADING' },
{ label: '转写文字', key: 'TRANSCRIBING' },
{ label: '总结内容', key: 'SUMMARIZING' },
{ label: '保存完成', key: 'SUCCESS' },
]
const { loading, initialized } = useCheckBackend()
// 在后端初始化完成后执行系统检查
useEffect(() => {
if (initialized) {
systemCheck()
}
}, [initialized])
// 如果后端还未初始化,显示初始化对话框
if (!initialized) {
return (
<>
<BackendInitDialog open={loading} />
</>
)
}
// 后端已初始化,渲染主应用
return (
<>
<HashRouter>
@@ -34,16 +51,12 @@ function App() {
<Route index element={<Navigate to="model" replace />} />
<Route path="model" element={<Model />}>
<Route path="new" element={<ProviderForm isCreate />} />
{/*<Route index element={<Navigate to="openai" replace />} />*/}
<Route path=":id" element={<ProviderForm />} />
</Route>
{/*<Route path="transcriber" element={<Transcriber />}></Route>*/}
{/*<Route path="prompt" element={<Prompt />}></Route>*/}
<Route path="download" element={<Downloader />}>
<Route path=":id" element={<DownloaderForm />} />
</Route>
<Route path="about" element={<AboutPage />}></Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
@@ -54,4 +67,4 @@ function App() {
)
}
export default App
export default App

View File

@@ -0,0 +1,23 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Loader2 } from 'lucide-react'
interface Props {
open: boolean
}
function BackendInitDialog({ open }: Props) {
return (
<Dialog open={open}>
<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"></p>
</DialogContent>
</Dialog>
)
}
export default BackendInitDialog

View File

@@ -0,0 +1,141 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react'
import request from '@/utils/request'
const MAX_RETRIES = 3
const RETRY_INTERVAL = 10000 // 10秒
export const useCheckBackend = () => {
const [loading, setLoading] = useState(false)
const [initialized, setInitialized] = useState(false)
useEffect(() => {
let retries = 0
const check = async () => {
try {
await request.get('/sys_health')
setInitialized(true)
setLoading(false)
} catch {
if (retries === 0) {
// 第一次失败时开始显示加载状态
setLoading(true)
}
if (retries < MAX_RETRIES) {
retries++
setTimeout(check, RETRY_INTERVAL)
} else {
// 达到重试上限,继续轮询直到后端就绪
waitUntilBackendReady()
}
}
}
const waitUntilBackendReady = async () => {
while (true) {
try {
await request.get('/sys_health')
setInitialized(true)
setLoading(false)
break
} catch {
await new Promise(res => setTimeout(res, RETRY_INTERVAL))
}
}
}
check()
}, [])
return { loading, initialized }
}

View File

@@ -0,0 +1,5 @@
import request from '@/utils/request'
export const systemCheck=async()=>{
return await request.get('/sys_health')
}

View File

@@ -22,8 +22,8 @@ BASE_PROMPT = '''
- 或者使用 `## 1. 内容` 的形式作为标题。
请确保以下格式 **不会出现误渲染**
`1. **xxx**`
`1\. **xxx**` 或 `## 1. xxx`
`1. **xxx**`
`1\. **xxx**` 或 `## 1. xxx`
视频分段(格式:开始时间 - 内容):

View File

@@ -52,7 +52,7 @@ class UniversalGPT(GPT):
}
})
# 正确格式:整体包在一个 message 里role + content array
# 正确格式:整体包在一个 message 里role + content array
messages = [{
"role": "user",
"content": content

View File

@@ -11,5 +11,5 @@ class AudioDownloadResult:
platform: str # 平台,如 "bilibili"
video_id: str # 唯一视频ID
raw_info: dict # yt-dlp 的原始 info 字典
video_path: Optional[str] = None # 新增字段:可选视频文件路径
video_path: Optional[str] = None # 新增字段:可选视频文件路径

View File

@@ -4,6 +4,7 @@ from typing import Optional
from app.utils.response import ResponseWrapper as R
from app.services.cookie_manager import CookieConfigManager
from ffmpeg_helper import ensure_ffmpeg_or_raise
router = APIRouter()
cookie_manager = CookieConfigManager()
@@ -30,3 +31,11 @@ def update_cookie(data: CookieUpdateRequest):
return R.success(
)
@router.get("/sys_health")
async def sys_health():
try:
ensure_ffmpeg_or_raise()
return R.success()
except EnvironmentError:
return R.error(msg="系统未安装 ffmpeg 请先进行安装")

View File

@@ -238,7 +238,7 @@ async def image_proxy(request: Request, url: str):
resp.aiter_bytes(),
media_type=content_type,
headers={
"Cache-Control": "public, max-age=86400", # 缓存一天
"Cache-Control": "public, max-age=86400", # 缓存一天
"Content-Type": content_type,
}
)

View File

@@ -10,7 +10,7 @@ from app.services.provider import ProviderService
router = APIRouter()
# 新增 type 字段
# 新增 type 字段
class ProviderRequest(BaseModel):
name: str
api_key: str

View File

@@ -79,13 +79,13 @@ class WhisperTranscriber(Transcriber):
def is_cuda() -> bool:
try:
if is_cuda_available():
print(" CUDA 可用,使用 GPU")
print(" CUDA 可用,使用 GPU")
return True
elif is_torch_installed():
print("⚠️ 只装了 torch但没有 CUDA用 CPU")
return False
else:
print(" 还没有安装 torch请先安装")
print(" 还没有安装 torch请先安装")
return False
except ImportError:

View File

@@ -31,7 +31,7 @@ def ensure_ffmpeg_or_raise():
if not check_ffmpeg_exists():
logger.error("未检测到 ffmpeg请先安装后再使用本功能。")
raise EnvironmentError(
" 未检测到 ffmpeg请先安装后再使用本功能。\n"
" 未检测到 ffmpeg请先安装后再使用本功能。\n"
"👉 下载地址https://ffmpeg.org/download.html\n"
"🪟 Windows 推荐https://www.gyan.dev/ffmpeg/builds/\n"
"💡 如果你已安装,请将其路径写入 `.env` 文件,例如:\n"

View File

@@ -39,7 +39,6 @@ if not os.path.exists(out_dir):
@asynccontextmanager
async def lifespan(app: FastAPI):
register_handler()
ensure_ffmpeg_or_raise()
init_db()
get_transcriber(transcriber_type=os.getenv("TRANSCRIBER_TYPE", "fast-whisper"))
seed_default_providers()
@@ -48,7 +47,7 @@ async def lifespan(app: FastAPI):
app = create_app(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["tauri://localhost"], # 加上 Tauri 的 origin
allow_origins=["tauri://localhost"], # 加上 Tauri 的 origin
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 623 KiB