mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-07-06 23:21:22 +08:00
Merge pull request #151 from JefferyHcool/feature/1.8.0
feat(system): 添加后端初始化和健康检查功能
This commit is contained in:
@@ -2,10 +2,10 @@ import './App.css'
|
|||||||
import { HomePage } from './pages/HomePage/Home.tsx'
|
import { HomePage } from './pages/HomePage/Home.tsx'
|
||||||
import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
|
import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
|
||||||
import SettingPage from './pages/SettingPage/index.tsx'
|
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 { Route } from 'react-router-dom'
|
||||||
import Index from '@/pages/Index.tsx'
|
import Index from '@/pages/Index.tsx'
|
||||||
import NotFoundPage from '@/pages/NotFoundPage' //
|
import NotFoundPage from '@/pages/NotFoundPage'
|
||||||
import Model from '@/pages/SettingPage/Model.tsx'
|
import Model from '@/pages/SettingPage/Model.tsx'
|
||||||
import Transcriber from '@/pages/SettingPage/transcriber.tsx'
|
import Transcriber from '@/pages/SettingPage/transcriber.tsx'
|
||||||
import ProviderForm from '@/components/Form/modelForm/Form.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 AboutPage from '@/pages/SettingPage/about.tsx'
|
||||||
import Downloader from '@/pages/SettingPage/Downloader.tsx'
|
import Downloader from '@/pages/SettingPage/Downloader.tsx'
|
||||||
import DownloaderForm from '@/components/Form/DownloaderForm/Form.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() {
|
function App() {
|
||||||
useTaskPolling(3000) // 每 3 秒轮询一次
|
useTaskPolling(3000) // 每 3 秒轮询一次
|
||||||
const steps = [
|
const { loading, initialized } = useCheckBackend()
|
||||||
{ label: '解析链接', key: 'PARSING', icon: <Downloading /> },
|
|
||||||
{ label: '下载音频', key: 'DOWNLOADING' },
|
// 在后端初始化完成后执行系统检查
|
||||||
{ label: '转写文字', key: 'TRANSCRIBING' },
|
useEffect(() => {
|
||||||
{ label: '总结内容', key: 'SUMMARIZING' },
|
if (initialized) {
|
||||||
{ label: '保存完成', key: 'SUCCESS' },
|
systemCheck()
|
||||||
]
|
}
|
||||||
|
}, [initialized])
|
||||||
|
|
||||||
|
// 如果后端还未初始化,显示初始化对话框
|
||||||
|
if (!initialized) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BackendInitDialog open={loading} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端已初始化,渲染主应用
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
@@ -34,16 +51,12 @@ function App() {
|
|||||||
<Route index element={<Navigate to="model" replace />} />
|
<Route index element={<Navigate to="model" replace />} />
|
||||||
<Route path="model" element={<Model />}>
|
<Route path="model" element={<Model />}>
|
||||||
<Route path="new" element={<ProviderForm isCreate />} />
|
<Route path="new" element={<ProviderForm isCreate />} />
|
||||||
{/*<Route index element={<Navigate to="openai" replace />} />*/}
|
|
||||||
<Route path=":id" element={<ProviderForm />} />
|
<Route path=":id" element={<ProviderForm />} />
|
||||||
</Route>
|
</Route>
|
||||||
{/*<Route path="transcriber" element={<Transcriber />}></Route>*/}
|
|
||||||
{/*<Route path="prompt" element={<Prompt />}></Route>*/}
|
|
||||||
<Route path="download" element={<Downloader />}>
|
<Route path="download" element={<Downloader />}>
|
||||||
<Route path=":id" element={<DownloaderForm />} />
|
<Route path=":id" element={<DownloaderForm />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="about" element={<AboutPage />}></Route>
|
<Route path="about" element={<AboutPage />}></Route>
|
||||||
|
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
@@ -54,4 +67,4 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
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. 内容` 的形式作为标题。
|
||||||
|
|
||||||
请确保以下格式 **不会出现误渲染**:
|
请确保以下格式 **不会出现误渲染**:
|
||||||
❌ `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 = [{
|
messages = [{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": content
|
"content": content
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ class AudioDownloadResult:
|
|||||||
platform: str # 平台,如 "bilibili"
|
platform: str # 平台,如 "bilibili"
|
||||||
video_id: str # 唯一视频ID
|
video_id: str # 唯一视频ID
|
||||||
raw_info: dict # yt-dlp 的原始 info 字典
|
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.utils.response import ResponseWrapper as R
|
||||||
|
|
||||||
from app.services.cookie_manager import CookieConfigManager
|
from app.services.cookie_manager import CookieConfigManager
|
||||||
|
from ffmpeg_helper import ensure_ffmpeg_or_raise
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
cookie_manager = CookieConfigManager()
|
cookie_manager = CookieConfigManager()
|
||||||
@@ -30,3 +31,11 @@ def update_cookie(data: CookieUpdateRequest):
|
|||||||
return R.success(
|
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(),
|
resp.aiter_bytes(),
|
||||||
media_type=content_type,
|
media_type=content_type,
|
||||||
headers={
|
headers={
|
||||||
"Cache-Control": "public, max-age=86400", # ✅ 缓存一天
|
"Cache-Control": "public, max-age=86400", # 缓存一天
|
||||||
"Content-Type": content_type,
|
"Content-Type": content_type,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from app.services.provider import ProviderService
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# ✅ 新增 type 字段
|
# 新增 type 字段
|
||||||
class ProviderRequest(BaseModel):
|
class ProviderRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
api_key: str
|
api_key: str
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ class WhisperTranscriber(Transcriber):
|
|||||||
def is_cuda() -> bool:
|
def is_cuda() -> bool:
|
||||||
try:
|
try:
|
||||||
if is_cuda_available():
|
if is_cuda_available():
|
||||||
print("✅ CUDA 可用,使用 GPU")
|
print(" CUDA 可用,使用 GPU")
|
||||||
return True
|
return True
|
||||||
elif is_torch_installed():
|
elif is_torch_installed():
|
||||||
print("⚠️ 只装了 torch,但没有 CUDA,用 CPU")
|
print("⚠️ 只装了 torch,但没有 CUDA,用 CPU")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print("❌ 还没有安装 torch,请先安装")
|
print(" 还没有安装 torch,请先安装")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def ensure_ffmpeg_or_raise():
|
|||||||
if not check_ffmpeg_exists():
|
if not check_ffmpeg_exists():
|
||||||
logger.error("未检测到 ffmpeg,请先安装后再使用本功能。")
|
logger.error("未检测到 ffmpeg,请先安装后再使用本功能。")
|
||||||
raise EnvironmentError(
|
raise EnvironmentError(
|
||||||
"❌ 未检测到 ffmpeg,请先安装后再使用本功能。\n"
|
" 未检测到 ffmpeg,请先安装后再使用本功能。\n"
|
||||||
"👉 下载地址:https://ffmpeg.org/download.html\n"
|
"👉 下载地址:https://ffmpeg.org/download.html\n"
|
||||||
"🪟 Windows 推荐:https://www.gyan.dev/ffmpeg/builds/\n"
|
"🪟 Windows 推荐:https://www.gyan.dev/ffmpeg/builds/\n"
|
||||||
"💡 如果你已安装,请将其路径写入 `.env` 文件,例如:\n"
|
"💡 如果你已安装,请将其路径写入 `.env` 文件,例如:\n"
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ if not os.path.exists(out_dir):
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
register_handler()
|
register_handler()
|
||||||
ensure_ffmpeg_or_raise()
|
|
||||||
init_db()
|
init_db()
|
||||||
get_transcriber(transcriber_type=os.getenv("TRANSCRIBER_TYPE", "fast-whisper"))
|
get_transcriber(transcriber_type=os.getenv("TRANSCRIBER_TYPE", "fast-whisper"))
|
||||||
seed_default_providers()
|
seed_default_providers()
|
||||||
@@ -48,7 +47,7 @@ async def lifespan(app: FastAPI):
|
|||||||
app = create_app(lifespan=lifespan)
|
app = create_app(lifespan=lifespan)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["tauri://localhost"], # ✅ 加上 Tauri 的 origin
|
allow_origins=["tauri://localhost"], # 加上 Tauri 的 origin
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
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