mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-06 20:42:52 +08:00
feat(system): 添加后端初始化和健康检查功能
- 新增后端初始化对话框组件 - 实现后端健康检查和初始化逻辑 - 在 App 组件中集成后端初始化和健康检查 - 新增系统健康检查 API 和相关服务
This commit is contained in:
@@ -5,7 +5,7 @@ import SettingPage from './pages/SettingPage/index.tsx'
|
||||
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 />} />
|
||||
|
||||
23
BillNote_frontend/src/components/BackendInitDialog.tsx
Normal file
23
BillNote_frontend/src/components/BackendInitDialog.tsx
Normal 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
|
||||
141
BillNote_frontend/src/components/ui/dialog.tsx
Normal file
141
BillNote_frontend/src/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
52
BillNote_frontend/src/hooks/useCheckBackend.ts
Normal file
52
BillNote_frontend/src/hooks/useCheckBackend.ts
Normal 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 }
|
||||
}
|
||||
5
BillNote_frontend/src/services/system.ts
Normal file
5
BillNote_frontend/src/services/system.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const systemCheck=async()=>{
|
||||
return await request.get('/sys_health')
|
||||
}
|
||||
@@ -22,8 +22,8 @@ BASE_PROMPT = '''
|
||||
- 或者使用 `## 1. 内容` 的形式作为标题。
|
||||
|
||||
请确保以下格式 **不会出现误渲染**:
|
||||
❌ `1. **xxx**`
|
||||
✅ `1\. **xxx**` 或 `## 1. xxx`
|
||||
`1. **xxx**`
|
||||
`1\. **xxx**` 或 `## 1. xxx`
|
||||
|
||||
视频分段(格式:开始时间 - 内容):
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class UniversalGPT(GPT):
|
||||
}
|
||||
})
|
||||
|
||||
# ✅ 正确格式:整体包在一个 message 里,role + content array
|
||||
# 正确格式:整体包在一个 message 里,role + content array
|
||||
messages = [{
|
||||
"role": "user",
|
||||
"content": content
|
||||
|
||||
@@ -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 # 新增字段:可选视频文件路径
|
||||
|
||||
|
||||
@@ -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 请先进行安装")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.services.provider import ProviderService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ✅ 新增 type 字段
|
||||
# 新增 type 字段
|
||||
class ProviderRequest(BaseModel):
|
||||
name: str
|
||||
api_key: str
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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=["*"],
|
||||
|
||||
BIN
doc/wechat.png
BIN
doc/wechat.png
Binary file not shown.
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 623 KiB |
Reference in New Issue
Block a user